Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab2713fe9 | ||
|
|
7cf652cd4e | ||
|
|
48eba2fd96 | ||
|
|
356ba376fe | ||
|
|
ef6b0742a9 | ||
|
|
787929d0bd | ||
|
|
0e1fd9d0e9 | ||
|
|
58938e82a7 | ||
|
|
be6c8c85b5 | ||
|
|
39bca37d0d | ||
|
|
1c61be04a7 | ||
|
|
7a97cd9386 | ||
|
|
5b09bde7a0 | ||
|
|
edc0081b7c | ||
|
|
913256b032 | ||
|
|
e1f5083d3e | ||
|
|
b55e693ad2 | ||
|
|
21c01a1aa4 | ||
|
|
83a26b2291 | ||
|
|
7e44c1efcb | ||
|
|
155f209db5 | ||
|
|
c4a6c165a0 | ||
|
|
624fe6b453 | ||
|
|
fd45fda0ab | ||
|
|
00fc97a611 | ||
|
|
445dc487b9 | ||
|
|
f43fbd4d9f | ||
|
|
dbabc4b3dc | ||
|
|
e01097264b | ||
|
|
0a922f3129 | ||
|
|
4a2dcd0b22 | ||
|
|
0db691db0b | ||
|
|
e5cd1372e7 | ||
|
|
fbd07cc575 | ||
|
|
91f2cf9dfb | ||
|
|
34a6967c71 |
No files matched your search
@@ -1,302 +0,0 @@
|
||||
# CTM Guide<img src="img/logo-readme.png" align="right">
|
||||
###### by Vortex [@vortex_so in discord, any suggestions are accepted]<br>
|
||||
|
||||
This file describes how to use CTM, structure of `.properties` file and some useful (or not) techniques
|
||||
|
||||
### Note:
|
||||
- All textures for CTM textures are in `[modname]/mcpatcher/ctm` folder<br>
|
||||
the overrides apply globally so it doesn't matter in which directory you put them<br>
|
||||
|
||||
- in the `ctm` folder you must have a folder with your block name (or any name actually)<br>
|
||||
in that folder you will have all texture files (animations allowed via mcmeta file for every texture) and `.properties` file<br>
|
||||
multiple `.properties` files are allowed for a single folder and set of textures for more complex behaviour (see examples)
|
||||
|
||||
## Quick Links
|
||||
- **[Method](#method)**
|
||||
- **[MatchBlocks](#matchblocks)**
|
||||
- **[MatchTiles](#matchtiles)**
|
||||
- **[Tiles](#tiles)**
|
||||
- **[Connect](#connect)**
|
||||
- **[Faces](#faces)**
|
||||
- **[Biomes](#biomes)**
|
||||
- **[Heights](#heights)**
|
||||
- **[RenderPass](#renderpass)**
|
||||
- **[InnerSeams](#innerseams)**
|
||||
- **[Weights](#weights)**<br>
|
||||
- **[Examples](#examples)**
|
||||
|
||||
## Structure of `.properties` files
|
||||
|
||||
---
|
||||
|
||||
## `matchBlocks`
|
||||
Override for a specific block, you can get the value from the tooltip if you have this feature enabled.<br>
|
||||
Can accept several values which allows to apply one CTM for different blocks.<br>
|
||||
if no value for this property was provided, Angelica will fallback to name of the `.property` file
|
||||
|
||||
examples:<br>
|
||||
`matchBlocks=gregtech:gt.blockcasings4`<br>
|
||||
`matchBlocks=minecraft:glass minecraft:glass_pane`
|
||||
|
||||
---
|
||||
|
||||
## `matchTiles`
|
||||
Override for a specific texture, main difference from matchBlocks is that this will only match a certain texture, not the block itself<br>
|
||||
|
||||
example:<br>
|
||||
`matchTiles:minecraft:blocks/stone`
|
||||
|
||||
---
|
||||
|
||||
## `tiles`
|
||||
The list of texture names for CTM. Supports ranges and explicit names.<br>
|
||||
You probably would want to use texture names like `0.png`, `1.png` as it is easier, however this allows for any names
|
||||
|
||||
examples:<br>
|
||||
`tiles=0-46` - classic for full CTM, expects files from `0.png` to `46.png`<br>
|
||||
`tiles=0-3,5, 8` - expects files `0.png`, `1.png`, `2.png`, `3.png`, `5.png`, `8.png`<br>
|
||||
`tiles=sometext,somemoretext` - expects files `sometext.png`, `somemoretext.png`
|
||||
|
||||
---
|
||||
|
||||
## `connect`
|
||||
Sets how the blocks are connected, can accept values: `block`, `tile`, `material`<br>
|
||||
`block` connects based on block (who would guess)<br>
|
||||
`tile` connects based on texture<br>
|
||||
`material` connects based on material (didn't really understand this part, ig it is for stone, iron, wood etc)<br>
|
||||
Fallbacks to `block` if no value for `matchTiles` is set, otherwise fallbacks to `tile`<br>
|
||||
Most of the time you don't need to set this value because of that fallback rule
|
||||
|
||||
example:<br>
|
||||
`connect=block`
|
||||
|
||||
---
|
||||
|
||||
## `faces`
|
||||
Limit to specific faces of a block, can accept several values<br>
|
||||
Fallbacks to `all` if value not present in the `.properties` file
|
||||
|
||||
examples:<br>
|
||||
`faces=top`<br>
|
||||
`faces=all`<br>
|
||||
`faces=bottom`<br>
|
||||
`faces=sides`<br>
|
||||
`faces=north south east west`<br>
|
||||
`faces=top bottom sides`
|
||||
|
||||
---
|
||||
|
||||
## `biomes`
|
||||
Limit to specific biomes, can accept several values<br>
|
||||
Fallbacks to all if value not present in the `.properties` file
|
||||
|
||||
examples:<br>
|
||||
`biomes=plains`<br>
|
||||
`biomes=desert forest`<br>
|
||||
`biomes=taiga jungle savanna`
|
||||
|
||||
---
|
||||
|
||||
## `heights`
|
||||
Limit to certain Y levels, can only accept several values<br>
|
||||
can be set through either `minHeight` and `maxHeight` or `heights`
|
||||
|
||||
examples:<br>
|
||||
`heights=0-64`<br>
|
||||
`heights=64-255`<br>
|
||||
example of maxHeight and minHeight:<br>
|
||||
`minHeight=64`<br>
|
||||
`maxHeight=128`
|
||||
|
||||
---
|
||||
|
||||
## `renderPass`
|
||||
Controls which rendering layer a CTM rule is applied in<br>
|
||||
In most cases you only need it to avoid rendering issues with transparency
|
||||
|
||||
`renderPass=solid` used for fully opaque blocks<br>
|
||||
`renderPass=cutout` used for fences, glass CTM, textured blocks with hard transparency<br>
|
||||
`renderPass=translucent` used for transparent blocks like glass or water<br>
|
||||
`renderPass=cutout_mipped` no idea for what<br>
|
||||
`renderPass=overlay` used for blocks with an overlay, for example GT hatches or damaged anvils<br>
|
||||
`renderPass=backface` used for blocks with a backface, however it is very rare to use this one
|
||||
|
||||
---
|
||||
|
||||
## `innerSeams`
|
||||
Adds internal seams, fallback to `innerSeams=false` if not set<br>
|
||||
|
||||
example:<br>
|
||||
`innerSeams=true`
|
||||
|
||||
---
|
||||
|
||||
## `weights`
|
||||
Priority, used for `Random` type to determine which textures have certain chances to be used for render<br>
|
||||
accepts several values, assigns values correspondingly to `tiles` list values
|
||||
|
||||
examples:<br>
|
||||
`weight=10`<br>
|
||||
`weights=1 3 3 2 1`
|
||||
|
||||
---
|
||||
|
||||
# `Method`
|
||||
|
||||
Selects CTM mode:
|
||||
- [LINK](#full-ctm-ctm) `ctm` / `glass` / `default`
|
||||
- [LINK](#compact-ctm) `compact` / `ctm_compact`
|
||||
- [LINK](#horizontal)`horizontal` / `bookshelf`
|
||||
- [LINK](#vertical)`vertical`
|
||||
- [LINK](#horizontalvertical-hv)`horizontal+vertical` / `h+v`
|
||||
- [LINK](#verticalhorizontal-vh)`vertical+horizontal` / `v+h`
|
||||
- [LINK](#top)`top` / `sandstone`
|
||||
- [LINK](#repeat)`repeat` / `pattern`
|
||||
- [LINK](#random)`random`
|
||||
- [LINK](#fixed)`fixed` / `static`
|
||||
|
||||
example:<br>
|
||||
`method=ctm`
|
||||
|
||||
### Full CTM (ctm)
|
||||
|
||||
Requires exactly 47 tiles, the most versatile but the most size-heavy method
|
||||
|
||||
Here's a visual explanation of how the textures should be numbered:
|
||||
|
||||
<img src="img/CTM/CTM-pattern.png" width="624" style="image-rendering: pixelated;">
|
||||
|
||||
---
|
||||
|
||||
### Compact CTM
|
||||
|
||||
Requires exactly 5 tiles.
|
||||
|
||||
<img src="img/CTM/CCTM-pattern.png" width="284" style="image-rendering: pixelated;">
|
||||
|
||||
Splits face into 4 quadrants and builds final texture per quadrant.
|
||||
|
||||
Here's a visual explanation of how it works on an example of 3x3 blocks pattern with a cutout:
|
||||
|
||||
<img src="img/CTM/CCTM-principle.png" width="400" style="image-rendering: pixelated;">
|
||||
|
||||
as you can see, compact CTM has a single limitation:<br>
|
||||
the size of any part of texture, such as an edge or a corner, can't be larger than `8x8` pixel<br>
|
||||
this is the main case where only full CTM can be used.
|
||||
|
||||
---
|
||||
|
||||
### Horizontal
|
||||
|
||||
Requires exactly 4 tiles. connects only horizontally, used for bookshelves for example
|
||||
|
||||
<img src="img/CTM/HCTM-pattern.png" width="284" style="image-rendering: pixelated;">
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Vertical
|
||||
|
||||
Requires exactly 4 tiles.
|
||||
|
||||
<img src="img/CTM/VCTM-pattern.png" height="284" style="image-rendering: pixelated;">
|
||||
|
||||
---
|
||||
|
||||
### Horizontal+Vertical (h+v)
|
||||
|
||||
Requires exactly 7 tiles.
|
||||
|
||||
Two-stage: horizontal first, then vertical if none horizontal variations are found.
|
||||
|
||||
<img src="img/CTM/HVCTM-pattern.png" height="284" style="image-rendering: pixelated;">
|
||||
|
||||
---
|
||||
|
||||
### Vertical+Horizontal (v+h)
|
||||
|
||||
Requires exactly 7 tiles.
|
||||
|
||||
Two-stage: vertical first, then horizontal if none vertical variations are found.
|
||||
|
||||
<img src="img/CTM/VHCTM-pattern.png" height="284" style="image-rendering: pixelated;">
|
||||
|
||||
---
|
||||
|
||||
### Top
|
||||
|
||||
Requires exactly 1 tile.
|
||||
|
||||
Applies tile on side faces when block above matches, for example used for smooth sandstone
|
||||
|
||||
---
|
||||
|
||||
### Repeat
|
||||
|
||||
Requires width * height tiles.
|
||||
|
||||
Properties:
|
||||
- `width` - defines the number of tiles in width
|
||||
- `height` - defines the number of tiles in height
|
||||
- `symmetry` - defines the symmetry of the pattern, can be either `none` or `opposite`
|
||||
|
||||
Just uses N*M grid, for example<br>
|
||||
`0 1 2 3 4`<br>
|
||||
`5 6 7 8 9`
|
||||
|
||||
---
|
||||
|
||||
### Random
|
||||
|
||||
Any number of tiles.
|
||||
|
||||
Properties:
|
||||
- `weights` - described [here](#weights)
|
||||
- `symmetry` - accepts `all`, `opposite`, `none`, groups opposite faces together, otherwise keeps them independent
|
||||
- `linked` - makes the textures consistent between render states, not needed much
|
||||
|
||||
---
|
||||
|
||||
### Fixed
|
||||
|
||||
Requires exactly 1 tile. Always uses the same tile.<br>
|
||||
useful only if combined with height or other properties (see examples)
|
||||
|
||||
---
|
||||
|
||||
# Examples
|
||||
|
||||
## Glass via full CTM
|
||||
```properties
|
||||
matchBlocks=minecraft:glass minecraft:glass_pane
|
||||
metadata=0
|
||||
method=ctm
|
||||
faces=all
|
||||
tiles=0-46
|
||||
connect=block
|
||||
```
|
||||
|
||||
## Multiple properties files usage
|
||||
### Use multiple .properties files with different heights for more detailed worldgen
|
||||
this one will use one texture for lower part of the world and another for the rest of the world
|
||||
```properties
|
||||
matchBlocks=minecraft:dirt
|
||||
metadata=0
|
||||
method=fixed
|
||||
faces=all
|
||||
tiles=0
|
||||
connect=block
|
||||
maxHeight=40
|
||||
```
|
||||
|
||||
```properties
|
||||
matchBlocks=minecraft:dirt
|
||||
metadata=0
|
||||
method=fixed
|
||||
faces=all
|
||||
tiles=0
|
||||
connect=block
|
||||
minHeight=41
|
||||
```
|
||||
|
||||
---
|
||||
@@ -76,7 +76,7 @@ dependencies {
|
||||
|
||||
// Iris Shaders
|
||||
compileOnly('org.jetbrains:annotations:26.0.2')
|
||||
api("com.github.GTNewHorizons:GTNHLib:0.9.36:dev")
|
||||
api("com.github.GTNewHorizons:GTNHLib:0.9.52:dev")
|
||||
|
||||
shadowImplementation(project(":glsm")) { transitive = false }
|
||||
shadowImplementation(project(":lwjgl3-backend")) { transitive = false }
|
||||
@@ -121,7 +121,7 @@ dependencies {
|
||||
|
||||
compileOnly("com.github.GTNewHorizons:NotEnoughIds:2.1.10:dev")
|
||||
|
||||
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5634_H261"))
|
||||
compileOnly(rfg.deobf("maven.modrinth:ntmspace:X5663_H261"))
|
||||
|
||||
compileOnly(rfg.deobf("curse.maven:campfirebackport-387444:4611675"))
|
||||
compileOnly(rfg.deobf("curse.maven:xaeros-minimap-263420:5060684"))
|
||||
@@ -129,6 +129,8 @@ dependencies {
|
||||
compileOnly(rfg.deobf("curse.maven:et-futurum-requiem-441392:5873837"))
|
||||
compileOnly(rfg.deobf("curse.maven:biomes-o-plenty-220318:2499612"))
|
||||
|
||||
devOnlyNonPublishable("com.github.GTNewHorizons:CosmeticArmorReworked:1.0.3-GTNH:dev")
|
||||
|
||||
// For testing alternative splash screens — https://github.com/MalTeeez/ModernSplash/releases
|
||||
// devOnlyNonPublishable("com.github.MalTeeez:modernsplash:1.7.10-1.2.4:dev")
|
||||
|
||||
|
||||
@@ -146,6 +146,10 @@ tasks.test {
|
||||
|
||||
javaLauncher = javaToolchains.launcherFor {
|
||||
languageVersion = JavaLanguageVersion.of(8)
|
||||
if (System.getProperty("os.name").lowercase().contains("mac")
|
||||
&& System.getProperty("os.arch") == "aarch64") {
|
||||
vendor = JvmVendorSpec.AZUL
|
||||
}
|
||||
}
|
||||
|
||||
dependsOn(downgradeMainClasses, downgradeTestClasses, downgradeDepsForTest)
|
||||
|
||||
@@ -9,13 +9,10 @@ import com.gtnewhorizons.angelica.glsm.states.MaterialState;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBO;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBOBuilder;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import lombok.Getter;
|
||||
@@ -566,12 +569,16 @@ public class DisplayListManager {
|
||||
currentRecorder.recordDrawBuffers(count, bufs);
|
||||
}
|
||||
|
||||
// Complex commands are texture uploads - not draw barriers
|
||||
public static void recordComplexCommand(DisplayListCommand cmd) {
|
||||
if (currentRecorder == null) return;
|
||||
currentRecorder.recordComplexCommand(cmd);
|
||||
}
|
||||
|
||||
public static void recordIndexedDrawCapture(IndexedDrawCapture capture) {
|
||||
if (currentRecorder == null) return;
|
||||
currentRecorder.recordIndexedDrawCapture(capture);
|
||||
}
|
||||
|
||||
public static void recordLoadMatrix(Matrix4f matrix) {
|
||||
if (currentRecorder != null) currentRecorder.recordLoadMatrix(matrix);
|
||||
}
|
||||
@@ -580,14 +587,6 @@ public class DisplayListManager {
|
||||
if (currentRecorder != null) currentRecorder.recordLoadIdentity();
|
||||
}
|
||||
|
||||
public static void recordDrawArrays(int mode, int start, int count) {
|
||||
if (currentRecorder != null) currentRecorder.recordDrawArrays(mode, start, count);
|
||||
}
|
||||
|
||||
public static void recordDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
|
||||
if (currentRecorder != null) currentRecorder.recordDrawElements(mode, indices_count, type, indices_buffer_offset);
|
||||
}
|
||||
|
||||
public static void recordBindVBO(int vbo) { if (currentRecorder != null) currentRecorder.recordBindVBO(vbo); }
|
||||
public static void recordBindVAO(int vao) { if (currentRecorder != null) currentRecorder.recordBindVAO(vao); }
|
||||
|
||||
@@ -822,17 +821,34 @@ public class DisplayListManager {
|
||||
final boolean hasDraws = accumulatedDraws != null && !accumulatedDraws.isEmpty();
|
||||
|
||||
if (hasCommands || hasDraws) {
|
||||
// Phase 1: Compile format-based VBOs (shared by both optimized and unoptimized paths)
|
||||
final DisplayListVBO compiledBuffers = new DisplayListVBOBuilder().addDraws(accumulatedDraws).build();
|
||||
try {
|
||||
final CommandBuffer finalBuffer = new CommandBuffer();
|
||||
CommandBufferBuilder.buildFromRawBuffer(rawCommandBuffer, accumulatedDraws, finalBuffer);
|
||||
|
||||
// Build to CommandBuffer - optimize raw buffer to final buffer
|
||||
final CommandBuffer finalBuffer = new CommandBuffer();
|
||||
CommandBufferBuilder.buildFromRawBuffer(rawCommandBuffer, accumulatedDraws, finalBuffer);
|
||||
final IndexedDrawBatchBuilder indexedBuilder = currentRecorder.getIndexedDraws();
|
||||
final List<IndexedDrawBatch> indexedBatches;
|
||||
if (indexedBuilder.isEmpty()) {
|
||||
indexedBatches = Collections.emptyList();
|
||||
} else {
|
||||
final CommandRecorder paused = pauseRecording();
|
||||
try {
|
||||
indexedBatches = indexedBuilder.build();
|
||||
} finally {
|
||||
try {
|
||||
for (IndexedDrawCapture c : indexedBuilder.getCaptures()) {
|
||||
c.freeBuffers();
|
||||
}
|
||||
} finally {
|
||||
resumeRecording(paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Free the recorder (and its buffer) after optimization
|
||||
currentRecorder.free();
|
||||
|
||||
compiled = new CompiledDisplayList(finalBuffer.toBuffer(), finalBuffer.getComplexObjects(), compiledBuffers);
|
||||
compiled = new CompiledDisplayList(finalBuffer.toBuffer(), finalBuffer.getComplexObjects(), compiledBuffers, indexedBatches);
|
||||
} finally {
|
||||
currentRecorder.free();
|
||||
}
|
||||
} else {
|
||||
// Free the recorder even if empty
|
||||
if (currentRecorder != null) {
|
||||
@@ -841,8 +857,10 @@ public class DisplayListManager {
|
||||
// Empty display list - per OpenGL spec, still valid after glNewList/glEndList
|
||||
compiled = CompiledDisplayList.EMPTY;
|
||||
}
|
||||
// Store the compiled list (even if empty - an empty list is still a valid list)
|
||||
displayListCache.put(glListId, compiled);
|
||||
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
|
||||
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
|
||||
previous.delete();
|
||||
}
|
||||
|
||||
// Log compilation details if enabled (before context restoration changes glListId)
|
||||
if (LOG_DISPLAY_LIST_COMPILATION) {
|
||||
@@ -895,7 +913,7 @@ public class DisplayListManager {
|
||||
}
|
||||
|
||||
// Merge the previous draw call if possible
|
||||
final AccumulatedDraw previous = accumulatedDraws.get(accumulatedDraws.size() - 1);
|
||||
final AccumulatedDraw previous = accumulatedDraws.getLast();
|
||||
if (previous.format == tessellator.getVertexFormat() && previous.stateGeneration == stateGeneration) {
|
||||
previous.mergeDraw(tessellator, copyLast);
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.CommandRecorder;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.TexImage2DCmd;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.TexSubImage2DCmd;
|
||||
import com.gtnewhorizons.angelica.glsm.stacks.AlphaStateStack;
|
||||
@@ -43,6 +44,7 @@ import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
|
||||
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
@@ -186,6 +188,8 @@ public class GLStateManager {
|
||||
|
||||
@Getter private static Vendor VENDOR;
|
||||
|
||||
@Getter private static boolean windows;
|
||||
|
||||
// This setting varies depending on driver, so it gets queried at runtime
|
||||
public static int DEFAULT_DRAW_BUFFER = 0x0405; // GL_BACK
|
||||
|
||||
@@ -516,6 +520,12 @@ public class GLStateManager {
|
||||
// After Display.create(), viewport is (0, 0, width, height)
|
||||
viewportState.setViewPort(0, 0, displayWidth, displayHeight);
|
||||
}
|
||||
|
||||
final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR);
|
||||
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
|
||||
|
||||
final String os = System.getProperty("os.name").toLowerCase();
|
||||
windows = os.contains("win");
|
||||
}
|
||||
|
||||
static void init(Runnable initCallback) {
|
||||
@@ -523,9 +533,6 @@ public class GLStateManager {
|
||||
|
||||
DEFAULT_DRAW_BUFFER = RENDER_BACKEND.getInteger(GL11.GL_DRAW_BUFFER);
|
||||
|
||||
final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR);
|
||||
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
|
||||
|
||||
// The initial mask value should be defined as all 1's. However, some drivers have it set to 0's.
|
||||
// To ensure consistency & correctness across all drivers, we're setting them to 0xFF.
|
||||
RENDER_BACKEND.stencilFunc(stencilState.getFuncFront(), stencilState.getRefFront(), 0xFF);
|
||||
@@ -619,16 +626,6 @@ public class GLStateManager {
|
||||
return BYPASS_CACHE || !isCachingEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we should use DSA for texture operations. DSA relies on cached texture bindings, which aren't tracked when caching is disabled. Also,
|
||||
* proxy textures are special query textures that shouldn't use DSA.
|
||||
*/
|
||||
private static boolean shouldUseDSA(int target) {
|
||||
if (!isCachingEnabled()) return false;
|
||||
// Proxy textures are for capability queries, not real textures
|
||||
return target != GL11.GL_PROXY_TEXTURE_1D && target != GL11.GL_PROXY_TEXTURE_2D && target != GL12.GL_PROXY_TEXTURE_3D;
|
||||
}
|
||||
|
||||
// LWJGL Overrides
|
||||
public static void glEnable(int cap) {
|
||||
final RecordMode mode = DisplayListManager.getRecordMode();
|
||||
@@ -1636,6 +1633,9 @@ public class GLStateManager {
|
||||
}
|
||||
alphaTest.enable();
|
||||
fragmentGeneration++;
|
||||
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
|
||||
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void disableAlphaTest() {
|
||||
@@ -1653,6 +1653,9 @@ public class GLStateManager {
|
||||
}
|
||||
alphaTest.disable();
|
||||
fragmentGeneration++;
|
||||
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
|
||||
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void glAlphaFunc(int function, float reference) {
|
||||
@@ -1672,6 +1675,9 @@ public class GLStateManager {
|
||||
alphaState.setFunction(function);
|
||||
alphaState.setReference(reference);
|
||||
fragmentGeneration++;
|
||||
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
|
||||
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1817,13 +1823,7 @@ public class GLStateManager {
|
||||
}
|
||||
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
suspendPixelUnpackBuffer();
|
||||
if (shouldUseDSA(target)) {
|
||||
// Use DSA to upload directly to the texture
|
||||
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
|
||||
} else {
|
||||
// Non-main thread or proxy texture - use direct GL call
|
||||
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
}
|
||||
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
@@ -1868,11 +1868,7 @@ public class GLStateManager {
|
||||
}
|
||||
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels != null ? pixels.asIntBuffer() : null);
|
||||
suspendPixelUnpackBuffer();
|
||||
if (shouldUseDSA(target)) {
|
||||
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
|
||||
} else {
|
||||
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
}
|
||||
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
@@ -1995,7 +1991,9 @@ public class GLStateManager {
|
||||
}
|
||||
RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, id);
|
||||
RENDER_BACKEND.texParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
|
||||
suspendPixelUnpackBuffer();
|
||||
RENDER_BACKEND.texImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, 1, 1, 0, GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
|
||||
restorePixelUnpackBuffer();
|
||||
|
||||
// Restore previous binding on active unit
|
||||
RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, savedBinding == id ? 0 : savedBinding);
|
||||
@@ -2229,15 +2227,12 @@ public class GLStateManager {
|
||||
CommandRecorder savedRecorder = null;
|
||||
final RecordMode recordMode = DisplayListManager.getRecordMode();
|
||||
if (recordMode != RecordMode.NONE) {
|
||||
if (isVAOBound()) {
|
||||
DisplayListManager.recordDrawElements(mode, indices_count, type, indices_buffer_offset);
|
||||
} else if (isVBOBound() || VertexAttribState.hasVBOBoundAttrib()) {
|
||||
final DirectTessellator result = ImmediateModeRecorder.processDrawElementsFromVBO(mode, indices_count, type, indices_buffer_offset, boundEBO);
|
||||
if (result != null) {
|
||||
DisplayListManager.addImmediateModeDraw(result);
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!");
|
||||
// Core profile: a default VAO is generated at init and glBindVertexArray(0)
|
||||
// is redirected to it, so a VAO is always bound here — no fallback branches.
|
||||
DisplayListManager.flushMatrix();
|
||||
final IndexedDrawCapture capture = IndexedDrawCapture.create(mode, indices_count, type, indices_buffer_offset, boundEBO);
|
||||
if (capture != null) {
|
||||
DisplayListManager.recordIndexedDrawCapture(capture);
|
||||
}
|
||||
if (recordMode == RecordMode.COMPILE) {
|
||||
return;
|
||||
@@ -4287,6 +4282,7 @@ public class GLStateManager {
|
||||
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
|
||||
GLSMHooks.programChangeEvent.previousProgram = prev;
|
||||
GLSMHooks.programChangeEvent.newProgram = 0;
|
||||
GLSMHooks.programChangeEvent.postBind = false;
|
||||
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
|
||||
}
|
||||
ffp.activate();
|
||||
@@ -4307,6 +4303,7 @@ public class GLStateManager {
|
||||
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
|
||||
GLSMHooks.programChangeEvent.previousProgram = prev;
|
||||
GLSMHooks.programChangeEvent.newProgram = program;
|
||||
GLSMHooks.programChangeEvent.postBind = false;
|
||||
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
|
||||
}
|
||||
if (initConfig != null && initConfig.isLwjglDebug()) {
|
||||
@@ -4315,6 +4312,12 @@ public class GLStateManager {
|
||||
}
|
||||
RENDER_BACKEND.useProgram(program);
|
||||
CompatUniformManager.onUseProgram(program);
|
||||
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
|
||||
GLSMHooks.programChangeEvent.previousProgram = prev;
|
||||
GLSMHooks.programChangeEvent.newProgram = program;
|
||||
GLSMHooks.programChangeEvent.postBind = true;
|
||||
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4391,6 +4394,7 @@ public class GLStateManager {
|
||||
|
||||
public static int glGetError() {
|
||||
if (!RENDER_BACKEND.hasContext()) return 0;
|
||||
if (initConfig.noErrorChecks()) return 0;
|
||||
return RENDER_BACKEND.getError();
|
||||
}
|
||||
|
||||
@@ -4445,13 +4449,7 @@ public class GLStateManager {
|
||||
}
|
||||
}
|
||||
suspendPixelUnpackBuffer();
|
||||
if (shouldUseDSA(target)) {
|
||||
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
|
||||
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
} else {
|
||||
// Non-main thread or proxy texture - use direct GL call
|
||||
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
}
|
||||
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
@@ -4464,13 +4462,7 @@ public class GLStateManager {
|
||||
}
|
||||
}
|
||||
suspendPixelUnpackBuffer();
|
||||
if (shouldUseDSA(target)) {
|
||||
// Use DSA to upload directly to the texture
|
||||
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
} else {
|
||||
// Non-main thread or proxy texture - use direct GL call
|
||||
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
}
|
||||
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
@@ -4948,12 +4940,14 @@ public class GLStateManager {
|
||||
|
||||
private static void invalidateDeletedBuffer(int buffer) {
|
||||
if (buffer == 0) return;
|
||||
if (boundVBO == buffer) {
|
||||
boundVBO = 0;
|
||||
}
|
||||
if (boundEBO == buffer) {
|
||||
boundEBO = 0;
|
||||
vaoEboMap.put(boundVAO, 0);
|
||||
if (boundVBO == buffer) boundVBO = 0;
|
||||
if (boundEBO == buffer) boundEBO = 0;
|
||||
if (boundPixelUnpackBuffer == buffer) boundPixelUnpackBuffer = 0;
|
||||
if (boundPixelPackBuffer == buffer) boundPixelPackBuffer = 0;
|
||||
|
||||
// Sweep all VAOs: an unbound VAO may later rebind with a dangling EBO id.
|
||||
for (Int2IntMap.Entry e : vaoEboMap.int2IntEntrySet()) {
|
||||
if (e.getIntValue() == buffer) e.setValue(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4972,25 +4966,25 @@ public class GLStateManager {
|
||||
RENDER_BACKEND.bindBuffer(target, buffer);
|
||||
}
|
||||
|
||||
private static void suspendPixelUnpackBuffer() {
|
||||
static void suspendPixelUnpackBuffer() {
|
||||
if (boundPixelUnpackBuffer != 0) {
|
||||
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void restorePixelUnpackBuffer() {
|
||||
static void restorePixelUnpackBuffer() {
|
||||
if (boundPixelUnpackBuffer != 0) {
|
||||
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_UNPACK_BUFFER, boundPixelUnpackBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void suspendPixelPackBuffer() {
|
||||
static void suspendPixelPackBuffer() {
|
||||
if (boundPixelPackBuffer != 0) {
|
||||
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void restorePixelPackBuffer() {
|
||||
static void restorePixelPackBuffer() {
|
||||
if (boundPixelPackBuffer != 0) {
|
||||
RENDER_BACKEND.bindBuffer(GL21.GL_PIXEL_PACK_BUFFER, boundPixelPackBuffer);
|
||||
}
|
||||
@@ -5070,14 +5064,6 @@ public class GLStateManager {
|
||||
glBindVertexArray(array);
|
||||
}
|
||||
|
||||
public static boolean isVBOBound() {
|
||||
return boundVBO != 0;
|
||||
}
|
||||
|
||||
public static boolean isVAOBound() {
|
||||
return boundVAO != 0;
|
||||
}
|
||||
|
||||
public static boolean vendorIsAMD() {
|
||||
return VENDOR == AMD;
|
||||
}
|
||||
@@ -5196,7 +5182,7 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static boolean isFramebufferEnabled() {
|
||||
return initConfig != null && initConfig.isFramebufferSupported() && initConfig.isFboEnabled();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final boolean FFP_WARN_ON_UNSUPPORTED = Boolean.parseBoolean(System.getProperty("angelica.ffp.warnOnUnsupported", "false"));
|
||||
@@ -5661,7 +5647,10 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static void glDeleteProgram(int program) {
|
||||
if (program == 0) return;
|
||||
CompatUniformManager.onDeleteProgram(program);
|
||||
|
||||
if (activeProgram == program) activeProgram = 0;
|
||||
RENDER_BACKEND.deleteProgram(program);
|
||||
}
|
||||
|
||||
@@ -5721,6 +5710,14 @@ public class GLStateManager {
|
||||
return RENDER_BACKEND.getProgramInfoLog(program);
|
||||
}
|
||||
|
||||
public static boolean glIsProgram(int obj) {
|
||||
return RENDER_BACKEND.isProgram(obj);
|
||||
}
|
||||
|
||||
public static boolean glIsShader(int obj) {
|
||||
return RENDER_BACKEND.isShader(obj);
|
||||
}
|
||||
|
||||
public static void glBindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format) { RENDER_BACKEND.bindImageTexture(unit, texture, level, layered, layer, access, format); }
|
||||
public static void glMemoryBarrier(int barriers) { RENDER_BACKEND.memoryBarrier(barriers); }
|
||||
public static void glCopyImageSubData(int srcName, int srcTarget, int srcLevel, int srcX, int srcY, int srcZ,
|
||||
|
||||
@@ -146,42 +146,11 @@ public final class QuadConverter {
|
||||
|
||||
final int bytesPerIndex = VertexAttribState.Attrib.glTypeSizeBytes(type);
|
||||
|
||||
// Read source indices from caller's EBO
|
||||
final ByteBuffer src = memAlloc(indexCount * bytesPerIndex);
|
||||
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, offset, src);
|
||||
final long srcAddr = memAddress0(src);
|
||||
|
||||
// Allocate output as GL_UNSIGNED_INT
|
||||
final ByteBuffer dst = memAlloc(triIndexCount * 4);
|
||||
final long dstAddr = memAddress0(dst);
|
||||
|
||||
for (int i = 0; i < quadCount; i++) {
|
||||
final int a, b, c, d;
|
||||
switch (type) {
|
||||
case GL11.GL_UNSIGNED_INT -> {
|
||||
final long p = srcAddr + (long) i * 16;
|
||||
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
|
||||
}
|
||||
case GL11.GL_UNSIGNED_SHORT -> {
|
||||
final long p = srcAddr + (long) i * 8;
|
||||
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
|
||||
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
|
||||
}
|
||||
default /* GL_UNSIGNED_BYTE */ -> {
|
||||
final long p = srcAddr + (long) i * 4;
|
||||
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
|
||||
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
|
||||
}
|
||||
}
|
||||
// GL_LAST_VERTEX_CONVENTION: (a,b,d), (b,c,d)
|
||||
final long o = (long) i * 24;
|
||||
memPutInt(dstAddr + o, a);
|
||||
memPutInt(dstAddr + o + 4, b);
|
||||
memPutInt(dstAddr + o + 8, d);
|
||||
memPutInt(dstAddr + o + 12, b);
|
||||
memPutInt(dstAddr + o + 16, c);
|
||||
memPutInt(dstAddr + o + 20, d);
|
||||
}
|
||||
triangulateQuads(memAddress0(src), type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
|
||||
|
||||
memFree(src);
|
||||
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
|
||||
@@ -198,16 +167,8 @@ public final class QuadConverter {
|
||||
final int triIndexCount = quadCount * 6;
|
||||
|
||||
final ByteBuffer dst = memAlloc(triIndexCount * 4);
|
||||
final long dstAddr = memAddress0(dst);
|
||||
final int basePos = indices.position();
|
||||
|
||||
for (int i = 0; i < quadCount; i++) {
|
||||
final int base = basePos + i * 4;
|
||||
final int a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
|
||||
final long o = (long) i * 24;
|
||||
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
|
||||
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
|
||||
}
|
||||
final long srcAddr = memAddress0(indices) + (long) indices.position() * 4;
|
||||
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
|
||||
|
||||
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
|
||||
}
|
||||
@@ -223,16 +184,8 @@ public final class QuadConverter {
|
||||
final int triIndexCount = quadCount * 6;
|
||||
|
||||
final ByteBuffer dst = memAlloc(triIndexCount * 2);
|
||||
final long dstAddr = memAddress0(dst);
|
||||
final int basePos = indices.position();
|
||||
|
||||
for (int i = 0; i < quadCount; i++) {
|
||||
final int base = basePos + i * 4;
|
||||
final short a = indices.get(base), b = indices.get(base + 1), c = indices.get(base + 2), d = indices.get(base + 3);
|
||||
final long o = (long) i * 12;
|
||||
memPutShort(dstAddr + o, a); memPutShort(dstAddr + o + 2, b); memPutShort(dstAddr + o + 4, d);
|
||||
memPutShort(dstAddr + o + 6, b); memPutShort(dstAddr + o + 8, c); memPutShort(dstAddr + o + 10, d);
|
||||
}
|
||||
final long srcAddr = memAddress0(indices) + (long) indices.position() * 2;
|
||||
triangulateQuads(srcAddr, GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, quadCount);
|
||||
|
||||
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_SHORT, 2);
|
||||
}
|
||||
@@ -248,31 +201,7 @@ public final class QuadConverter {
|
||||
final long srcAddr = memAddress0(indices) + indices.position();
|
||||
|
||||
final ByteBuffer dst = memAlloc(triIndexCount * 4);
|
||||
final long dstAddr = memAddress0(dst);
|
||||
|
||||
for (int i = 0; i < quadCount; i++) {
|
||||
final int a, b, c, d;
|
||||
switch (type) {
|
||||
case GL11.GL_UNSIGNED_INT -> {
|
||||
final long p = srcAddr + (long) i * 16;
|
||||
a = memGetInt(p); b = memGetInt(p + 4); c = memGetInt(p + 8); d = memGetInt(p + 12);
|
||||
}
|
||||
case GL11.GL_UNSIGNED_SHORT -> {
|
||||
final long p = srcAddr + (long) i * 8;
|
||||
a = memGetShort(p) & 0xFFFF; b = memGetShort(p + 2) & 0xFFFF;
|
||||
c = memGetShort(p + 4) & 0xFFFF; d = memGetShort(p + 6) & 0xFFFF;
|
||||
}
|
||||
case GL11.GL_UNSIGNED_BYTE -> {
|
||||
final long p = srcAddr + (long) i * 4;
|
||||
a = memGetByte(p) & 0xFF; b = memGetByte(p + 1) & 0xFF;
|
||||
c = memGetByte(p + 2) & 0xFF; d = memGetByte(p + 3) & 0xFF;
|
||||
}
|
||||
default -> { memFree(dst); return; }
|
||||
}
|
||||
final long o = (long) i * 24;
|
||||
memPutInt(dstAddr + o, a); memPutInt(dstAddr + o + 4, b); memPutInt(dstAddr + o + 8, d);
|
||||
memPutInt(dstAddr + o + 12, b); memPutInt(dstAddr + o + 16, c); memPutInt(dstAddr + o + 20, d);
|
||||
}
|
||||
triangulateQuads(srcAddr, type, memAddress0(dst), GL11.GL_UNSIGNED_INT, quadCount);
|
||||
|
||||
uploadAndDraw(dst, triIndexCount, GL11.GL_UNSIGNED_INT, 4);
|
||||
}
|
||||
@@ -282,14 +211,140 @@ public final class QuadConverter {
|
||||
*/
|
||||
public static void destroy() {
|
||||
if (eboId != 0) {
|
||||
RENDER_BACKEND.deleteBuffers(eboId);
|
||||
GLStateManager.glDeleteBuffers(eboId);
|
||||
eboId = 0;
|
||||
maxQuads = 0;
|
||||
}
|
||||
if (scratchEboId != 0) {
|
||||
RENDER_BACKEND.deleteBuffers(scratchEboId);
|
||||
GLStateManager.glDeleteBuffers(scratchEboId);
|
||||
scratchEboId = 0;
|
||||
scratchEboCapacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one index at {@code srcAddr + i * sizeof(srcType)} and return its
|
||||
* zero-extended unsigned value.
|
||||
*/
|
||||
private static int readIndex(long srcAddr, int srcType, int i) {
|
||||
return switch (srcType) {
|
||||
case GL11.GL_UNSIGNED_BYTE -> memGetByte(srcAddr + i) & 0xFF;
|
||||
case GL11.GL_UNSIGNED_SHORT -> memGetShort(srcAddr + i * 2L) & 0xFFFF;
|
||||
case GL11.GL_UNSIGNED_INT -> memGetInt(srcAddr + i * 4L);
|
||||
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(srcType));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one index at {@code dstAddr + i * sizeof(dstType)}.
|
||||
*/
|
||||
private static void writeIndex(long dstAddr, int dstType, int i, int value) {
|
||||
switch (dstType) {
|
||||
case GL11.GL_UNSIGNED_SHORT -> memPutShort(dstAddr + i * 2L, (short) value);
|
||||
case GL11.GL_UNSIGNED_INT -> memPutInt(dstAddr + i * 4L, value);
|
||||
default -> throw new IllegalArgumentException("unsupported index type 0x" + Integer.toHexString(dstType));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read {@code quadCount} quads from {@code srcAddr} and emit 6 triangle indices
|
||||
* per quad at {@code dstAddr} using {@code GL_LAST_VERTEX_CONVENTION}:
|
||||
* {@code (a,b,d), (b,c,d)}. v3 is the provoking vertex for both triangles;
|
||||
* diagonal runs b→d. Matches Mesa's {@code do_quad} LAST path.
|
||||
*
|
||||
* <p>Caller allocates dst of size {@code quadCount * 6 * sizeof(dstType)}.
|
||||
*
|
||||
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT}
|
||||
* / {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
|
||||
* {@code GL_UNSIGNED_INT}. Widening only.
|
||||
*/
|
||||
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount) {
|
||||
triangulateQuads(srcAddr, srcType, dstAddr, dstType, quadCount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #triangulateQuads(long, int, long, int, int)} but subtracts
|
||||
* {@code minVtx} from every emitted index, rebasing to a 0-based buffer in
|
||||
* one pass.
|
||||
*/
|
||||
public static void triangulateQuads(long srcAddr, int srcType, long dstAddr, int dstType, int quadCount, int minVtx) {
|
||||
for (int i = 0; i < quadCount; i++) {
|
||||
final int a = readIndex(srcAddr, srcType, i * 4) - minVtx;
|
||||
final int b = readIndex(srcAddr, srcType, i * 4 + 1) - minVtx;
|
||||
final int c = readIndex(srcAddr, srcType, i * 4 + 2) - minVtx;
|
||||
final int d = readIndex(srcAddr, srcType, i * 4 + 3) - minVtx;
|
||||
final int o = i * 6;
|
||||
writeIndex(dstAddr, dstType, o, a);
|
||||
writeIndex(dstAddr, dstType, o + 1, b);
|
||||
writeIndex(dstAddr, dstType, o + 2, d);
|
||||
writeIndex(dstAddr, dstType, o + 3, b);
|
||||
writeIndex(dstAddr, dstType, o + 4, c);
|
||||
writeIndex(dstAddr, dstType, o + 5, d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy {@code count} indices from {@code srcAddr} to {@code dstAddr}, converting
|
||||
* from {@code srcType} to {@code dstType}.
|
||||
*
|
||||
* <p>Supported src types: {@code GL_UNSIGNED_BYTE} / {@code GL_UNSIGNED_SHORT} /
|
||||
* {@code GL_UNSIGNED_INT}. Supported dst types: {@code GL_UNSIGNED_SHORT} /
|
||||
* {@code GL_UNSIGNED_INT}. Widening only; narrowing is rejected by
|
||||
* {@link #writeIndex(long, int, int, int) writeIndex}.
|
||||
*/
|
||||
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count) {
|
||||
widenIndices(srcAddr, srcType, dstAddr, dstType, count, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #widenIndices(long, int, long, int, int)} but subtracts
|
||||
* {@code minVtx} from every written index in one pass.
|
||||
*/
|
||||
public static void widenIndices(long srcAddr, int srcType, long dstAddr, int dstType, int count, int minVtx) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
writeIndex(dstAddr, dstType, i, readIndex(srcAddr, srcType, i) - minVtx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan {@code count} indices at {@code srcAddr} of {@code srcType}. Returns
|
||||
* {@code (max << 32) | (min & 0xFFFFFFFFL)} or {@code -1L} for an unsupported
|
||||
* type or empty range.
|
||||
*/
|
||||
public static long scanMinMaxIndex(long srcAddr, int srcType, int count) {
|
||||
if (count <= 0) return -1L;
|
||||
long min = Long.MAX_VALUE;
|
||||
long max = Long.MIN_VALUE;
|
||||
switch (srcType) {
|
||||
case GL11.GL_UNSIGNED_BYTE -> {
|
||||
for (int i = 0; i < count; i++) {
|
||||
final int v = memGetByte(srcAddr + i) & 0xFF;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
}
|
||||
case GL11.GL_UNSIGNED_SHORT -> {
|
||||
for (int i = 0; i < count; i++) {
|
||||
final int v = memGetShort(srcAddr + i * 2L) & 0xFFFF;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
}
|
||||
case GL11.GL_UNSIGNED_INT -> {
|
||||
for (int i = 0; i < count; i++) {
|
||||
final long v = memGetInt(srcAddr + i * 4L) & 0xFFFFFFFFL;
|
||||
if (v < min) min = v;
|
||||
if (v > max) max = v;
|
||||
}
|
||||
// readIndex / writeIndex / rebase all route UINT through signed int.
|
||||
// Reject the > 2^31 slice so the caller bails rather than silently
|
||||
// emitting negative indices. MC never hits this in practice.
|
||||
if (max > Integer.MAX_VALUE) return -1L;
|
||||
}
|
||||
default -> {
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
return (max << 32) | (min & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.glsm.dsa.DSACore;
|
||||
import com.gtnewhorizons.angelica.glsm.dsa.DSAEXT;
|
||||
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
@@ -61,7 +62,13 @@ public class RenderSystem {
|
||||
if (rendererInitialized) return;
|
||||
rendererInitialized = true;
|
||||
try {
|
||||
if (GLStateManager.capabilities.OpenGL45) {
|
||||
if (GLStateManager.vendorIsIntel() && GLStateManager.isWindows()) {
|
||||
dsaState = new DSAUnsupported();
|
||||
GLStateManager.LOGGER.info("Detected Intel drivers on Windows, disabling DSA.");
|
||||
} else if (!GLStateManager.getInitConfig().isDSAEnabled()) {
|
||||
dsaState = new DSAUnsupported();
|
||||
GLStateManager.LOGGER.info("enableDSA is set to false, disabling DSA.");
|
||||
} else if (GLStateManager.capabilities.OpenGL45) {
|
||||
dsaState = (Runtime.version().feature() > 8 && GLStateManager.capabilities.GL_EXT_direct_state_access) ? new DSAEXT() : new DSACore();
|
||||
GLStateManager.LOGGER.info("OpenGL 4.5 detected, enabling DSA.");
|
||||
}
|
||||
@@ -149,7 +156,9 @@ public class RenderSystem {
|
||||
|
||||
public static void texImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) {
|
||||
GLStateManager.glBindTexture(target, texture);
|
||||
GLStateManager.suspendPixelUnpackBuffer();
|
||||
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
GLStateManager.restorePixelUnpackBuffer();
|
||||
if (target == GL11.GL_TEXTURE_2D && level == 0) {
|
||||
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
|
||||
}
|
||||
@@ -218,29 +227,41 @@ public class RenderSystem {
|
||||
}
|
||||
|
||||
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) {
|
||||
GLStateManager.suspendPixelUnpackBuffer();
|
||||
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
|
||||
GLStateManager.restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) {
|
||||
GLStateManager.suspendPixelUnpackBuffer();
|
||||
dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels);
|
||||
GLStateManager.restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) {
|
||||
GLStateManager.suspendPixelUnpackBuffer();
|
||||
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
GLStateManager.restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) {
|
||||
GLStateManager.suspendPixelUnpackBuffer();
|
||||
dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels);
|
||||
GLStateManager.restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
public static void texImage1D(int texture, int target, int level, int internalformat, int width, int border, int format, int type, ByteBuffer pixels) {
|
||||
RENDER_BACKEND.bindTexture(target, texture);
|
||||
GLStateManager.suspendPixelUnpackBuffer();
|
||||
RENDER_BACKEND.texImage1D(target, level, internalformat, width, border, format, type, pixels);
|
||||
GLStateManager.restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
public static void texImage3D(int texture, int target, int level, int internalformat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) {
|
||||
RENDER_BACKEND.bindTexture(target, texture);
|
||||
GLStateManager.suspendPixelUnpackBuffer();
|
||||
RENDER_BACKEND.texImage3D(target, level, internalformat, width, height, depth, border, format, type, pixels);
|
||||
GLStateManager.restorePixelUnpackBuffer();
|
||||
}
|
||||
|
||||
public static String getProgramInfoLog(int program) {
|
||||
@@ -264,7 +285,9 @@ public class RenderSystem {
|
||||
}
|
||||
|
||||
public static void readPixels(int x, int y, int width, int height, int format, int type, FloatBuffer pixels) {
|
||||
GLStateManager.suspendPixelPackBuffer();
|
||||
RENDER_BACKEND.readPixels(x, y, width, height, format, type, pixels);
|
||||
GLStateManager.restorePixelPackBuffer();
|
||||
}
|
||||
|
||||
public static void bufferData(int target, FloatBuffer data, int usage) {
|
||||
@@ -432,7 +455,7 @@ public class RenderSystem {
|
||||
}
|
||||
|
||||
public static void deleteBuffers(int buffer) {
|
||||
RENDER_BACKEND.deleteBuffers(buffer);
|
||||
GLStateManager.glDeleteBuffers(buffer);
|
||||
}
|
||||
|
||||
public static long getVRAM() {
|
||||
@@ -491,7 +514,7 @@ public class RenderSystem {
|
||||
|
||||
public static void destroySampler(int sampler) {
|
||||
if (!supportsSamplerObjects || sampler == 0) return;
|
||||
RENDER_BACKEND.deleteSamplers(sampler);
|
||||
GLStateManager.glDeleteSamplers(sampler);
|
||||
}
|
||||
|
||||
public static void samplerParameteri(int sampler, int pname, int param) {
|
||||
|
||||
@@ -178,7 +178,7 @@ public class Program {
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
RENDER_BACKEND.deleteProgram(programId);
|
||||
GLStateManager.glDeleteProgram(programId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.glsm.states.LightState;
|
||||
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
|
||||
import com.gtnewhorizons.angelica.glsm.states.TexGenState;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
|
||||
import org.joml.Math;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
@@ -42,8 +43,8 @@ public class Uniforms {
|
||||
private final org.joml.Vector3f tempVec3 = new org.joml.Vector3f();
|
||||
|
||||
// Pre-computed constants for fog params (Mesa STATE_FOG_PARAMS_OPTIMIZED)
|
||||
private static final double LN2 = Math.log(2.0);
|
||||
private static final double SQRT_LN2 = Math.sqrt(LN2);
|
||||
private static final double LN2 = java.lang.Math.log(2.0);
|
||||
private static final double SQRT_LN2 = java.lang.Math.sqrt(LN2);
|
||||
|
||||
// Dirty tracking: last-uploaded generation per category + program ID.
|
||||
// Program change forces full re-upload since uniform locations differ.
|
||||
@@ -315,10 +316,10 @@ public class Uniforms {
|
||||
// Upload the current color from GLSM
|
||||
final var color = GLStateManager.getColor();
|
||||
vec4Buf.clear();
|
||||
vec4Buf.put(color.getRed());
|
||||
vec4Buf.put(color.getGreen());
|
||||
vec4Buf.put(color.getBlue());
|
||||
vec4Buf.put(color.getAlpha());
|
||||
vec4Buf.put(Math.clamp(0f, 1f, color.getRed()));
|
||||
vec4Buf.put(Math.clamp(0f, 1f, color.getGreen()));
|
||||
vec4Buf.put(Math.clamp(0f, 1f, color.getBlue()));
|
||||
vec4Buf.put(Math.clamp(0f, 1f, color.getAlpha()));
|
||||
vec4Buf.flip();
|
||||
RENDER_BACKEND.uniform4(program.locCurrentColor, vec4Buf);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.glsm.hooks;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.BlendFuncChangeEvent;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.FogStateChangeEvent;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.LightmapCoordsEvent;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.ProgramChangeEvent;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureBindEvent;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureDeleteEvent;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.TextureUnitStateEvent;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.events.*;
|
||||
import net.minecraftforge.eventbus.api.bus.EventBus;
|
||||
|
||||
public final class GLSMHooks {
|
||||
@@ -23,6 +17,7 @@ public final class GLSMHooks {
|
||||
public static final EventBus<BlendFuncChangeEvent> BLEND_FUNC_CHANGE = EventBus.create(BlendFuncChangeEvent.class);
|
||||
public static final EventBus<FogStateChangeEvent> FOG_STATE_CHANGE = EventBus.create(FogStateChangeEvent.class);
|
||||
public static final EventBus<LightmapCoordsEvent> LIGHTMAP_COORDS = EventBus.create(LightmapCoordsEvent.class);
|
||||
public static final EventBus<AlphaStateChangeEvent> ALPHA_STATE_CHANGE = EventBus.create(AlphaStateChangeEvent.class);
|
||||
|
||||
// Reusable event instances
|
||||
public static final TextureBindEvent textureBindEvent = new TextureBindEvent();
|
||||
@@ -32,6 +27,7 @@ public final class GLSMHooks {
|
||||
public static final BlendFuncChangeEvent blendFuncChangeEvent = new BlendFuncChangeEvent();
|
||||
public static final FogStateChangeEvent fogStateChangeEvent = new FogStateChangeEvent();
|
||||
public static final LightmapCoordsEvent lightmapCoordsEvent = new LightmapCoordsEvent();
|
||||
public static final AlphaStateChangeEvent alphaStateChangeEvent = new AlphaStateChangeEvent();
|
||||
|
||||
private GLSMHooks() {}
|
||||
}
|
||||
@@ -8,24 +8,24 @@ import java.util.function.Consumer;
|
||||
public final class GLSMInitConfig {
|
||||
private final boolean lwjglDebug;
|
||||
private final StreamingUploader.UploadStrategy streamingUploadStrategy;
|
||||
private final boolean framebufferSupported;
|
||||
private final boolean fboEnabled;
|
||||
private final Consumer<DirectTessellator> directDrawer;
|
||||
private final Runnable streamingDrawerDestroy;
|
||||
private final int displayWidth;
|
||||
private final int displayHeight;
|
||||
private final Runnable postInitCallback;
|
||||
private final boolean enableDSA;
|
||||
private final boolean noErrorChecks;
|
||||
|
||||
private GLSMInitConfig(Builder builder) {
|
||||
this.lwjglDebug = builder.lwjglDebug;
|
||||
this.streamingUploadStrategy = builder.streamingUploadStrategy;
|
||||
this.framebufferSupported = builder.framebufferSupported;
|
||||
this.fboEnabled = builder.fboEnabled;
|
||||
this.noErrorChecks = builder.noErrorChecks;
|
||||
this.directDrawer = builder.directDrawer;
|
||||
this.streamingDrawerDestroy = builder.streamingDrawerDestroy;
|
||||
this.displayWidth = builder.displayWidth;
|
||||
this.displayHeight = builder.displayHeight;
|
||||
this.postInitCallback = builder.postInitCallback;
|
||||
this.enableDSA = builder.enableDSA;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -34,24 +34,24 @@ public final class GLSMInitConfig {
|
||||
|
||||
public boolean isLwjglDebug() { return lwjglDebug; }
|
||||
public StreamingUploader.UploadStrategy getStreamingUploadStrategy() { return streamingUploadStrategy; }
|
||||
public boolean isFramebufferSupported() { return framebufferSupported; }
|
||||
public boolean isFboEnabled() { return fboEnabled; }
|
||||
public Consumer<DirectTessellator> getDirectDrawer() { return directDrawer; }
|
||||
public Runnable getStreamingDrawerDestroy() { return streamingDrawerDestroy; }
|
||||
public int getDisplayWidth() { return displayWidth; }
|
||||
public int getDisplayHeight() { return displayHeight; }
|
||||
public Runnable getPostInitCallback() { return postInitCallback; }
|
||||
public boolean isDSAEnabled() { return enableDSA; }
|
||||
public boolean noErrorChecks() { return noErrorChecks; }
|
||||
|
||||
public static final class Builder {
|
||||
private boolean lwjglDebug = false;
|
||||
private StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.BUFFER_DATA;
|
||||
private boolean framebufferSupported = false;
|
||||
private boolean fboEnabled = false;
|
||||
private Consumer<DirectTessellator> directDrawer = null;
|
||||
private Runnable streamingDrawerDestroy = null;
|
||||
private int displayWidth = 0;
|
||||
private int displayHeight = 0;
|
||||
private Runnable postInitCallback = null;
|
||||
private boolean enableDSA = false;
|
||||
private boolean noErrorChecks;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@@ -65,13 +65,8 @@ public final class GLSMInitConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder framebufferSupported(boolean framebufferSupported) {
|
||||
this.framebufferSupported = framebufferSupported;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder fboEnabled(boolean fboEnabled) {
|
||||
this.fboEnabled = fboEnabled;
|
||||
public Builder noErrorChecks(boolean noErrorChecks) {
|
||||
this.noErrorChecks = noErrorChecks;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -96,6 +91,11 @@ public final class GLSMInitConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder enableDSA(boolean enableDSA) {
|
||||
this.enableDSA = enableDSA;
|
||||
return this;
|
||||
}
|
||||
|
||||
public GLSMInitConfig build() {
|
||||
return new GLSMInitConfig(this);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.gtnewhorizons.angelica.glsm.hooks.events;
|
||||
|
||||
import net.minecraftforge.eventbus.api.event.MutableEvent;
|
||||
|
||||
/**
|
||||
* AlphaTest changed - Listeners should query GLSM alpha state directly if they care what changed.
|
||||
*/
|
||||
public final class AlphaStateChangeEvent extends MutableEvent {
|
||||
}
|
||||
@@ -5,4 +5,5 @@ import net.minecraftforge.eventbus.api.event.MutableEvent;
|
||||
public final class ProgramChangeEvent extends MutableEvent {
|
||||
public int previousProgram;
|
||||
public int newProgram;
|
||||
public boolean postBind;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
|
||||
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class AttribSnapshot {
|
||||
|
||||
public record AttribDesc(int location, int size, int type, boolean normalized, int stride, long offset, int sourceVboId, ByteBuffer readBuffer, long readBufferBaseOffset) {
|
||||
public int effectiveStride() {
|
||||
return stride != 0 ? stride : size * VertexAttribState.Attrib.glTypeSizeBytes(type);
|
||||
}
|
||||
|
||||
public int typeSizeBytes() {
|
||||
return VertexAttribState.Attrib.glTypeSizeBytes(type);
|
||||
}
|
||||
}
|
||||
|
||||
private final AttribDesc[] perLocation;
|
||||
private final List<ByteBuffer> allocatedBuffers;
|
||||
private boolean freed;
|
||||
|
||||
private AttribSnapshot(AttribDesc[] perLocation, List<ByteBuffer> allocatedBuffers) {
|
||||
this.perLocation = perLocation;
|
||||
this.allocatedBuffers = allocatedBuffers;
|
||||
}
|
||||
|
||||
public AttribDesc get(int location) {
|
||||
return (location >= 0 && location < perLocation.length) ? perLocation[location] : null;
|
||||
}
|
||||
|
||||
public int maxLocations() {
|
||||
return perLocation.length;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
if (freed) return;
|
||||
freed = true;
|
||||
for (ByteBuffer buf : allocatedBuffers) {
|
||||
MemoryUtilities.memFree(buf);
|
||||
}
|
||||
allocatedBuffers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot attribs over a tight vertex window {@code [firstVertex, firstVertex+vertexCount)}.
|
||||
*
|
||||
* <p>Hot path (single VBO backing every enabled attrib — the typical draw) runs without
|
||||
* allocating any hashmaps. A second distinct VBO promotes to the map-backed fallback.
|
||||
*/
|
||||
public static AttribSnapshot snapshot(int firstVertex, int vertexCount) {
|
||||
final AttribDesc[] out = new AttribDesc[VertexAttribState.MAX_ATTRIBS];
|
||||
final List<ByteBuffer> allocated = new ArrayList<>();
|
||||
final int prevVBO = GLStateManager.getBoundVBO();
|
||||
|
||||
// Pass 1 — unioned [start,end] per vboId. Hot path: one source VBO, zero allocations.
|
||||
int rangeSoleVboId = 0;
|
||||
long rangeSoleStart = Long.MAX_VALUE;
|
||||
long rangeSoleEnd = Long.MIN_VALUE;
|
||||
Int2ObjectOpenHashMap<long[]> vboRanges = null;
|
||||
|
||||
final long lastVtx = (long) firstVertex + vertexCount - 1;
|
||||
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
|
||||
final VertexAttribState.Attrib a = VertexAttribState.get(i);
|
||||
if (!a.enabled || a.vboId == 0) continue;
|
||||
final int stride = a.effectiveStride();
|
||||
final long typeBytes = (long) a.size * VertexAttribState.Attrib.glTypeSizeBytes(a.type);
|
||||
final long start = a.offset + (long) firstVertex * stride;
|
||||
final long end = a.offset + lastVtx * stride + typeBytes;
|
||||
|
||||
if (vboRanges != null) {
|
||||
long[] r = vboRanges.get(a.vboId);
|
||||
if (r == null) {
|
||||
vboRanges.put(a.vboId, new long[] { start, end });
|
||||
} else {
|
||||
if (start < r[0]) r[0] = start;
|
||||
if (end > r[1]) r[1] = end;
|
||||
}
|
||||
} else if (rangeSoleVboId == 0 || rangeSoleVboId == a.vboId) {
|
||||
rangeSoleVboId = a.vboId;
|
||||
if (start < rangeSoleStart) rangeSoleStart = start;
|
||||
if (end > rangeSoleEnd) rangeSoleEnd = end;
|
||||
} else {
|
||||
// Spill: second distinct vboId. Promote the accumulated range to the map.
|
||||
vboRanges = new Int2ObjectOpenHashMap<>();
|
||||
vboRanges.put(rangeSoleVboId, new long[] { rangeSoleStart, rangeSoleEnd });
|
||||
vboRanges.put(a.vboId, new long[] { start, end });
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2 — readback cache. Same spill pattern.
|
||||
int readSoleVboId = 0;
|
||||
ByteBuffer readSoleBuf = null;
|
||||
long readSoleOffset = 0;
|
||||
Int2ObjectOpenHashMap<ByteBuffer> vboBuffers = null;
|
||||
Int2LongOpenHashMap vboReadOffsets = null;
|
||||
|
||||
try {
|
||||
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
|
||||
final VertexAttribState.Attrib a = VertexAttribState.get(i);
|
||||
if (!a.enabled) continue;
|
||||
|
||||
if (a.vboId != 0) {
|
||||
ByteBuffer buf;
|
||||
long readOffset;
|
||||
if (vboBuffers != null) {
|
||||
buf = vboBuffers.get(a.vboId);
|
||||
readOffset = buf != null ? vboReadOffsets.get(a.vboId) : 0;
|
||||
} else if (readSoleVboId == a.vboId) {
|
||||
buf = readSoleBuf;
|
||||
readOffset = readSoleOffset;
|
||||
} else {
|
||||
buf = null;
|
||||
readOffset = 0;
|
||||
}
|
||||
|
||||
if (buf == null) {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
|
||||
final int bufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
|
||||
if (bufferSize <= 0) {
|
||||
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
|
||||
continue;
|
||||
}
|
||||
|
||||
final long rStart;
|
||||
final long rEnd;
|
||||
if (vboRanges != null) {
|
||||
final long[] range = vboRanges.get(a.vboId);
|
||||
rStart = range[0];
|
||||
rEnd = range[1];
|
||||
} else {
|
||||
rStart = rangeSoleStart;
|
||||
rEnd = rangeSoleEnd;
|
||||
}
|
||||
final long ro = Math.max(0, rStart);
|
||||
final int rs = (int) (Math.min(bufferSize, rEnd) - ro);
|
||||
if (rs <= 0) continue;
|
||||
|
||||
buf = MemoryUtilities.memAlloc(rs);
|
||||
GLStateManager.glGetBufferSubData(GL15.GL_ARRAY_BUFFER, ro, buf);
|
||||
allocated.add(buf);
|
||||
readOffset = ro;
|
||||
|
||||
if (vboBuffers != null) {
|
||||
vboBuffers.put(a.vboId, buf);
|
||||
vboReadOffsets.put(a.vboId, ro);
|
||||
} else if (readSoleVboId == 0) {
|
||||
readSoleVboId = a.vboId;
|
||||
readSoleBuf = buf;
|
||||
readSoleOffset = ro;
|
||||
} else {
|
||||
vboBuffers = new Int2ObjectOpenHashMap<>();
|
||||
vboReadOffsets = new Int2LongOpenHashMap();
|
||||
vboBuffers.put(readSoleVboId, readSoleBuf);
|
||||
vboReadOffsets.put(readSoleVboId, readSoleOffset);
|
||||
vboBuffers.put(a.vboId, buf);
|
||||
vboReadOffsets.put(a.vboId, ro);
|
||||
}
|
||||
}
|
||||
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.offset, a.vboId, buf, readOffset);
|
||||
} else if (a.clientPointer != null) {
|
||||
out[i] = new AttribDesc(i, a.size, a.type, a.normalized, a.stride, a.clientPointer.position(), 0, a.clientPointer, 0L);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
|
||||
}
|
||||
|
||||
return new AttribSnapshot(out, allocated);
|
||||
}
|
||||
}
|
||||
@@ -600,23 +600,6 @@ public final class CommandBuffer {
|
||||
writeInt(listId);
|
||||
}
|
||||
|
||||
public void writeDrawArrays(int mode, int start, int count) {
|
||||
ensureCapacity(16);
|
||||
writeInt(GLCommand.DRAW_ARRAYS);
|
||||
writeInt(mode);
|
||||
writeInt(start);
|
||||
writeInt(count);
|
||||
}
|
||||
|
||||
public void writeDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
|
||||
ensureCapacity(24);
|
||||
writeInt(GLCommand.DRAW_ELEMENTS);
|
||||
writeInt(mode);
|
||||
writeInt(indices_count);
|
||||
writeInt(type);
|
||||
writeLong(indices_buffer_offset);
|
||||
}
|
||||
|
||||
public void writeBindVBO(int vbo) {
|
||||
ensureCapacity(8);
|
||||
writeInt(GLCommand.BIND_VBO);
|
||||
|
||||
@@ -530,14 +530,6 @@ public final class CommandBufferExecutor {
|
||||
ptr += 4 * MAX_DRAW_BUFFERS;
|
||||
GLStateManager.glDrawBuffers(DRAW_BUFFERS_BUFFER);
|
||||
}
|
||||
case GLCommand.DRAW_ARRAYS -> {
|
||||
GLStateManager.glDrawArrays(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8));
|
||||
ptr += 12;
|
||||
}
|
||||
case GLCommand.DRAW_ELEMENTS -> {
|
||||
GLStateManager.glDrawElements(memGetInt(ptr), memGetInt(ptr + 4), memGetInt(ptr + 8), memGetLong(ptr + 12));
|
||||
ptr += 20;
|
||||
}
|
||||
case GLCommand.BIND_VBO -> {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, memGetInt(ptr));
|
||||
ptr += 4;
|
||||
|
||||
@@ -150,8 +150,6 @@ public final class CommandBufferProcessor {
|
||||
// Draw/call commands
|
||||
case GLCommand.DRAW_RANGE -> out.writeDrawRange(raw.readInt());
|
||||
case GLCommand.CALL_LIST -> out.writeCallList(raw.readInt());
|
||||
case GLCommand.DRAW_ARRAYS -> out.writeDrawArrays(raw.readInt(), raw.readInt(), raw.readInt());
|
||||
case GLCommand.DRAW_ELEMENTS -> out.writeDrawElements(raw.readInt(), raw.readInt(), raw.readInt(), raw.readLong());
|
||||
case GLCommand.BIND_VBO -> out.writeBindVBO(raw.readInt());
|
||||
case GLCommand.BIND_VAO -> out.writeBindVAO(raw.readInt());
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
@@ -11,6 +13,7 @@ import java.nio.IntBuffer;
|
||||
*/
|
||||
public final class CommandRecorder {
|
||||
private final CommandBuffer buffer;
|
||||
private final IndexedDrawBatchBuilder indexedDraws = new IndexedDrawBatchBuilder();
|
||||
private int commandCount;
|
||||
|
||||
public CommandRecorder() {
|
||||
@@ -26,7 +29,19 @@ public final class CommandRecorder {
|
||||
return commandCount;
|
||||
}
|
||||
|
||||
public IndexedDrawBatchBuilder getIndexedDraws() {
|
||||
return indexedDraws;
|
||||
}
|
||||
|
||||
public void recordIndexedDrawCapture(IndexedDrawCapture capture) {
|
||||
indexedDraws.add(capture);
|
||||
recordComplexCommand(capture.placeholder);
|
||||
}
|
||||
|
||||
public void free() {
|
||||
for (IndexedDrawCapture c : indexedDraws.getCaptures()) {
|
||||
c.freeBuffers();
|
||||
}
|
||||
buffer.free();
|
||||
}
|
||||
|
||||
@@ -347,16 +362,6 @@ public final class CommandRecorder {
|
||||
commandCount++;
|
||||
}
|
||||
|
||||
public void recordDrawArrays(int mode, int start, int count) {
|
||||
buffer.writeDrawArrays(mode, start, count);
|
||||
commandCount++;
|
||||
}
|
||||
|
||||
public void recordDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
|
||||
buffer.writeDrawElements(mode, indices_count, type, indices_buffer_offset);
|
||||
commandCount++;
|
||||
}
|
||||
|
||||
public void recordBindVBO(int vbo) {
|
||||
buffer.writeBindVBO(vbo);
|
||||
commandCount++;
|
||||
|
||||
@@ -2,12 +2,15 @@ package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
@@ -26,16 +29,18 @@ public final class CompiledDisplayList {
|
||||
* Empty display list singleton. Per OpenGL spec, an empty list is still valid.
|
||||
* This instance does nothing when rendered and is never deleted (shared singleton).
|
||||
*/
|
||||
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null);
|
||||
public static final CompiledDisplayList EMPTY = new CompiledDisplayList(null, null, null, Collections.emptyList());
|
||||
|
||||
private final ByteBuffer commandBuffer; // Off-heap command storage, must be freed
|
||||
private final Object[] complexObjects; // Complex commands (TexImage2D, etc.)
|
||||
private final DisplayListVBO ownedVbos; // GPU resources referenced by index
|
||||
private final List<IndexedDrawBatch> indexedBatches; // Shared VAO/VBO/EBO triples per layout group
|
||||
|
||||
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, DisplayListVBO ownedVbos) {
|
||||
public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, DisplayListVBO ownedVbos, List<IndexedDrawBatch> indexedBatches) {
|
||||
this.commandBuffer = commandBuffer;
|
||||
this.complexObjects = complexObjects;
|
||||
this.ownedVbos = ownedVbos;
|
||||
this.indexedBatches = indexedBatches;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +56,6 @@ public final class CompiledDisplayList {
|
||||
* Delete all resources held by this display list.
|
||||
*/
|
||||
public void delete() {
|
||||
// Delete complex objects that hold resources
|
||||
if (complexObjects != null) {
|
||||
for (Object obj : complexObjects) {
|
||||
if (obj instanceof DisplayListCommand cmd) {
|
||||
@@ -60,11 +64,18 @@ public final class CompiledDisplayList {
|
||||
}
|
||||
}
|
||||
|
||||
// Close VBOs
|
||||
// Close VBOs from immediate-mode DisplayListVBOBuilder.
|
||||
if (ownedVbos != null) {
|
||||
ownedVbos.delete();
|
||||
}
|
||||
|
||||
// Release shared (VAO, VBO, EBO) triples from IndexedDrawBatchBuilder.
|
||||
if (indexedBatches != null) {
|
||||
for (IndexedDrawBatch batch : indexedBatches) {
|
||||
batch.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Free the command buffer (off-heap memory)
|
||||
if (commandBuffer != null) {
|
||||
memFree(commandBuffer);
|
||||
@@ -89,6 +100,10 @@ public final class CompiledDisplayList {
|
||||
return ownedVbos;
|
||||
}
|
||||
|
||||
public List<IndexedDrawBatch> getIndexedBatches() {
|
||||
return indexedBatches;
|
||||
}
|
||||
|
||||
// === Test inspection methods ===
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,8 +97,6 @@ public final class GLCommand {
|
||||
public static final int CALL_LIST = 102; // [cmd:4][listId:4] = 8 bytes
|
||||
|
||||
|
||||
public static final int DRAW_ARRAYS = 110; // [cmd:4][drawMode:4][start:4][count:4] = 20 bytes
|
||||
public static final int DRAW_ELEMENTS = 111; // [cmd:4][mode:4][indices_count:4][type:4][indices_buffer_offset:8] = 28 bytes
|
||||
public static final int DRAW_BUFFER = 112; // [cmd:4][mode:4] = 8 bytes
|
||||
public static final int DRAW_BUFFERS = 113; // [cmd:4][count:4][bufs:4*8] = 40 bytes (up to 8 buffers)
|
||||
/**
|
||||
@@ -219,7 +217,7 @@ public final class GLCommand {
|
||||
// Three int commands (16 bytes)
|
||||
case GLCommand.STENCIL_FUNC, GLCommand.STENCIL_OP, GLCommand.TEX_PARAMETERI,
|
||||
GLCommand.LIGHTF, GLCommand.LIGHTI,
|
||||
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF, GLCommand.DRAW_ARRAYS -> 16;
|
||||
GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF -> 16;
|
||||
|
||||
// Four int commands (20 bytes)
|
||||
case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK,
|
||||
@@ -227,8 +225,6 @@ public final class GLCommand {
|
||||
GLCommand.SCISSOR,
|
||||
GLCommand.COLOR, GLCommand.CLEAR_COLOR, GLCommand.BLEND_COLOR -> 20;
|
||||
|
||||
case GLCommand.DRAW_ELEMENTS -> 24;
|
||||
|
||||
// DRAW_RANGE_RESTORE: 28 bytes
|
||||
case GLCommand.DRAW_RANGE_RESTORE -> 28;
|
||||
|
||||
|
||||
@@ -5,15 +5,11 @@ import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
|
||||
|
||||
/**
|
||||
* Records immediate mode GL calls (glBegin/glEnd/glVertex) via {@link DirectTessellator}.
|
||||
* Used both during display list compilation and for live immediate mode emulation in core profile.
|
||||
@@ -167,19 +163,14 @@ public final class ImmediateModeRecorder {
|
||||
.mapToInt(Usage::getAttributeLocation)
|
||||
.max().orElse(0);
|
||||
|
||||
private static final ByteBuffer[] scratchAttribBuffers = new ByteBuffer[NUM_ATTRIBS];
|
||||
private static final long[] scratchAttribBaseOffsets = new long[NUM_ATTRIBS];
|
||||
private static final int[] scratchVBOIds = new int[NUM_ATTRIBS];
|
||||
private static final ByteBuffer[] scratchVBOBuffers = new ByteBuffer[NUM_ATTRIBS];
|
||||
private static final long[] scratchVBOReadOffsets = new long[NUM_ATTRIBS];
|
||||
|
||||
private static final class AttribReader {
|
||||
ByteBuffer data;
|
||||
int stride;
|
||||
int baseOffset;
|
||||
int size;
|
||||
int loc;
|
||||
VertexAttribState.Attrib attrib;
|
||||
int type;
|
||||
boolean normalized;
|
||||
}
|
||||
|
||||
private static final AttribReader[] readers = new AttribReader[NUM_ATTRIBS];
|
||||
@@ -198,167 +189,32 @@ public final class ImmediateModeRecorder {
|
||||
* Reads vertex data from VBOs or client memory referenced by VertexAttribState and converts to immediate mode geometry.
|
||||
*/
|
||||
public static DirectTessellator processDrawArraysFromAttribs(int mode, int first, int count) {
|
||||
final int readVBOCount = readbackAttribVBOs(first, count);
|
||||
|
||||
final AttribSnapshot snap = AttribSnapshot.snapshot(first, count);
|
||||
try {
|
||||
prepareReaders();
|
||||
prepareReaders(snap);
|
||||
if (positionReaderIndex < 0) return null;
|
||||
return readVerticesFromBuffer(mode, first, count);
|
||||
} finally {
|
||||
freeReadbackBuffers(readVBOCount);
|
||||
snap.free();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a glDrawElements call during display list recording when a VBO is bound.
|
||||
*/
|
||||
public static DirectTessellator processDrawElementsFromVBO(int mode, int indicesCount, int indexType, long indicesOffset, int eboId) {
|
||||
if (eboId == 0) return null;
|
||||
|
||||
final int readVBOCount = readbackAttribVBOs(-1, 0);
|
||||
|
||||
try {
|
||||
prepareReaders();
|
||||
if (positionReaderIndex < 0) return null;
|
||||
|
||||
final int prevEBO = GLStateManager.getBoundEBO();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
|
||||
final int eboSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
|
||||
if (eboSize <= 0) {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
|
||||
return null;
|
||||
}
|
||||
|
||||
final int indexSize = VertexAttribState.Attrib.glTypeSizeBytes(indexType);
|
||||
final int eboReadOffset = (int) indicesOffset;
|
||||
final int eboReadSize = indicesCount * indexSize;
|
||||
if (eboReadOffset + eboReadSize > eboSize) {
|
||||
GLStateManager.LOGGER.warn("[EBO] Read range {}+{} exceeds buffer size {}", eboReadOffset, eboReadSize, eboSize);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
|
||||
return null;
|
||||
}
|
||||
final ByteBuffer eboData = MemoryUtilities.memAlloc(eboReadSize);
|
||||
RENDER_BACKEND.getBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboReadOffset, eboData);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
|
||||
|
||||
try {
|
||||
return readVerticesFromBufferIndexed(mode, eboData, indexType, 0, indicesCount);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(eboData);
|
||||
}
|
||||
} finally {
|
||||
freeReadbackBuffers(readVBOCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read data for all enabled attribs into scratchAttribBuffers.
|
||||
* For VBO-backed attribs, reads VBO data. For client-memory attribs, uses the client pointer directly.
|
||||
*/
|
||||
private static int readbackAttribVBOs(int firstVertex, int vertexCount) {
|
||||
int readVBOCount = 0;
|
||||
final int prevVBO = GLStateManager.getBoundVBO();
|
||||
final boolean partialRead = firstVertex >= 0;
|
||||
|
||||
for (int i = 0; i < NUM_ATTRIBS; i++) {
|
||||
scratchAttribBuffers[i] = null;
|
||||
scratchAttribBaseOffsets[i] = 0;
|
||||
final VertexAttribState.Attrib a = VertexAttribState.get(i);
|
||||
if (!a.enabled) continue;
|
||||
|
||||
if (a.vboId != 0) {
|
||||
// VBO-backed attrib — read from GPU
|
||||
ByteBuffer existing = null;
|
||||
int existingIdx = -1;
|
||||
for (int j = 0; j < readVBOCount; j++) {
|
||||
if (scratchVBOIds[j] == a.vboId) {
|
||||
existing = scratchVBOBuffers[j];
|
||||
existingIdx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existing != null) {
|
||||
scratchAttribBuffers[i] = existing;
|
||||
scratchAttribBaseOffsets[i] = scratchVBOReadOffsets[existingIdx];
|
||||
} else {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, a.vboId);
|
||||
final int bufferSize = RENDER_BACKEND.getBufferParameteri(GL15.GL_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
|
||||
if (bufferSize <= 0) {
|
||||
GLStateManager.LOGGER.warn("[VBO Readback] GL_BUFFER_SIZE={} for VBO {}", bufferSize, a.vboId);
|
||||
continue;
|
||||
}
|
||||
|
||||
final long readOffset;
|
||||
final int readSize;
|
||||
if (partialRead) {
|
||||
final int stride = a.effectiveStride();
|
||||
long rangeStart = a.offset + (long) firstVertex * stride;
|
||||
long rangeEnd = a.offset + (long) (firstVertex + vertexCount - 1) * stride + (long) a.size * a.typeSizeBytes();
|
||||
|
||||
for (int k = 0; k < NUM_ATTRIBS; k++) {
|
||||
if (k == i) continue;
|
||||
final VertexAttribState.Attrib ak = VertexAttribState.get(k);
|
||||
if (!ak.enabled || ak.vboId != a.vboId) continue;
|
||||
final int ks = ak.effectiveStride();
|
||||
rangeStart = Math.min(rangeStart, ak.offset + (long) firstVertex * ks);
|
||||
rangeEnd = Math.max(rangeEnd, ak.offset + (long) (firstVertex + vertexCount - 1) * ks
|
||||
+ (long) ak.size * ak.typeSizeBytes());
|
||||
}
|
||||
rangeStart = Math.max(0, rangeStart);
|
||||
rangeEnd = Math.min(bufferSize, rangeEnd);
|
||||
readOffset = rangeStart;
|
||||
readSize = (int) (rangeEnd - rangeStart);
|
||||
} else {
|
||||
readOffset = 0;
|
||||
readSize = bufferSize;
|
||||
}
|
||||
|
||||
if (readSize <= 0) continue;
|
||||
|
||||
final ByteBuffer buf = MemoryUtilities.memAlloc(readSize);
|
||||
RENDER_BACKEND.getBufferSubData(GL15.GL_ARRAY_BUFFER, readOffset, buf);
|
||||
scratchAttribBuffers[i] = buf;
|
||||
scratchAttribBaseOffsets[i] = readOffset;
|
||||
scratchVBOIds[readVBOCount] = a.vboId;
|
||||
scratchVBOReadOffsets[readVBOCount] = readOffset;
|
||||
scratchVBOBuffers[readVBOCount] = buf;
|
||||
readVBOCount++;
|
||||
}
|
||||
} else if (a.clientPointer != null) {
|
||||
// Client-memory attrib — use pointer directly (no allocation needed)
|
||||
scratchAttribBuffers[i] = a.clientPointer;
|
||||
}
|
||||
}
|
||||
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
|
||||
|
||||
return readVBOCount;
|
||||
}
|
||||
|
||||
private static void freeReadbackBuffers(int readVBOCount) {
|
||||
for (int i = 0; i < readVBOCount; i++) {
|
||||
MemoryUtilities.memFree(scratchVBOBuffers[i]);
|
||||
scratchVBOBuffers[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void prepareReaders() {
|
||||
private static void prepareReaders(AttribSnapshot snap) {
|
||||
nonPositionCount = 0;
|
||||
positionReaderIndex = -1;
|
||||
|
||||
for (int i = 0; i < NUM_ATTRIBS; i++) {
|
||||
final ByteBuffer buf = scratchAttribBuffers[i];
|
||||
if (buf == null) continue;
|
||||
final VertexAttribState.Attrib a = VertexAttribState.get(i);
|
||||
final AttribSnapshot.AttribDesc d = snap.get(i);
|
||||
if (d == null) continue;
|
||||
|
||||
final AttribReader r = readers[i];
|
||||
r.data = buf;
|
||||
r.stride = a.effectiveStride();
|
||||
r.baseOffset = (int) (a.offset - scratchAttribBaseOffsets[i]);
|
||||
r.size = a.size;
|
||||
r.data = d.readBuffer();
|
||||
r.stride = d.effectiveStride();
|
||||
r.baseOffset = (int) (d.offset() - d.readBufferBaseOffset());
|
||||
r.size = d.size();
|
||||
r.loc = i;
|
||||
r.attrib = a;
|
||||
r.type = d.type();
|
||||
r.normalized = d.normalized();
|
||||
|
||||
if (i == LOC_POSITION) {
|
||||
positionReaderIndex = i;
|
||||
@@ -379,18 +235,6 @@ public final class ImmediateModeRecorder {
|
||||
return end();
|
||||
}
|
||||
|
||||
private static DirectTessellator readVerticesFromBufferIndexed(int mode, ByteBuffer eboData, int indexType, int eboOffset, int indicesCount) {
|
||||
begin(mode);
|
||||
final DirectTessellator t = tessellator();
|
||||
|
||||
for (int i = 0; i < indicesCount; i++) {
|
||||
final int index = readIndex(eboData, indexType, eboOffset, i);
|
||||
readVertexFromBuffer(t, index);
|
||||
}
|
||||
|
||||
return end();
|
||||
}
|
||||
|
||||
public static int readIndex(ByteBuffer ebo, int indexType, int eboOffset, int i) {
|
||||
return switch (indexType) {
|
||||
case GL11.GL_UNSIGNED_BYTE -> ebo.get(eboOffset + i) & 0xFF;
|
||||
@@ -411,39 +255,43 @@ public final class ImmediateModeRecorder {
|
||||
final AttribReader r = readers[positionReaderIndex];
|
||||
final int base = r.baseOffset + vertexIndex * r.stride;
|
||||
t.addVertex(
|
||||
r.attrib.readComponent(r.data, base, 0),
|
||||
r.attrib.readComponent(r.data, base, 1),
|
||||
(r.size >= 3) ? r.attrib.readComponent(r.data, base, 2) : 0.0f);
|
||||
readComponent(r, base, 0),
|
||||
readComponent(r, base, 1),
|
||||
(r.size >= 3) ? readComponent(r, base, 2) : 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void emitAttrib(DirectTessellator t, AttribReader r, int base) {
|
||||
if (r.loc == LOC_NORMAL) {
|
||||
t.setNormal(
|
||||
r.attrib.readComponent(r.data, base, 0),
|
||||
r.attrib.readComponent(r.data, base, 1),
|
||||
r.attrib.readComponent(r.data, base, 2));
|
||||
readComponent(r, base, 0),
|
||||
readComponent(r, base, 1),
|
||||
readComponent(r, base, 2));
|
||||
} else if (r.loc == LOC_UV0) {
|
||||
t.setTextureUV(
|
||||
r.attrib.readComponent(r.data, base, 0),
|
||||
(r.size >= 2) ? r.attrib.readComponent(r.data, base, 1) : 0.0f);
|
||||
readComponent(r, base, 0),
|
||||
(r.size >= 2) ? readComponent(r, base, 1) : 0.0f);
|
||||
} else if (r.loc == LOC_UV1) {
|
||||
if (r.attrib.type == GL11.GL_SHORT) {
|
||||
if (r.type == GL11.GL_SHORT) {
|
||||
final int s = r.data.getShort(base) & 0xFFFF;
|
||||
final int u = (r.size >= 2) ? (r.data.getShort(base + 2) & 0xFFFF) : 0;
|
||||
t.setBrightness((u << 16) | s);
|
||||
} else if (r.attrib.type == GL11.GL_FLOAT) {
|
||||
} else if (r.type == GL11.GL_FLOAT) {
|
||||
final int s = (int) r.data.getFloat(base);
|
||||
final int u = (r.size >= 2) ? (int) r.data.getFloat(base + 4) : 0;
|
||||
t.setBrightness((u << 16) | (s & 0xFFFF));
|
||||
}
|
||||
} else if (r.loc == LOC_COLOR) {
|
||||
final float cr = r.attrib.readComponent(r.data, base, 0);
|
||||
final float cg = r.attrib.readComponent(r.data, base, 1);
|
||||
final float cb = r.attrib.readComponent(r.data, base, 2);
|
||||
final float ca = (r.size >= 4) ? r.attrib.readComponent(r.data, base, 3) : 1.0f;
|
||||
final float cr = readComponent(r, base, 0);
|
||||
final float cg = readComponent(r, base, 1);
|
||||
final float cb = readComponent(r, base, 2);
|
||||
final float ca = (r.size >= 4) ? readComponent(r, base, 3) : 1.0f;
|
||||
t.setColorRGBA((int) (cr * 255.0f), (int) (cg * 255.0f), (int) (cb * 255.0f), (int) (ca * 255.0f));
|
||||
}
|
||||
}
|
||||
|
||||
private static float readComponent(AttribReader r, int base, int component) {
|
||||
return VertexAttribState.Attrib.readComponent(r.type, r.normalized, r.data, base, component);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording.commands;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class AttribLayoutKey {
|
||||
final int[] locations;
|
||||
final int[] sizes;
|
||||
final int[] types;
|
||||
final boolean[] normalized;
|
||||
private final int[] offsets;
|
||||
private final int stride;
|
||||
private final int hash;
|
||||
|
||||
public AttribLayoutKey(int[] locations, int[] sizes, int[] types, boolean[] normalized) {
|
||||
this.locations = locations;
|
||||
this.sizes = sizes;
|
||||
this.types = types;
|
||||
this.normalized = normalized;
|
||||
|
||||
final int n = locations.length;
|
||||
final int[] off = new int[n];
|
||||
int base = 0;
|
||||
int maxAlign = 1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
final int typeSize = VertexAttribState.Attrib.glTypeSizeBytes(types[i]);
|
||||
if (typeSize > maxAlign) maxAlign = typeSize;
|
||||
base = (base + typeSize - 1) & -typeSize;
|
||||
off[i] = base;
|
||||
base += sizes[i] * typeSize;
|
||||
}
|
||||
this.offsets = off;
|
||||
this.stride = (base + maxAlign - 1) & -maxAlign;
|
||||
|
||||
int h = Arrays.hashCode(locations);
|
||||
h = 31 * h + Arrays.hashCode(sizes);
|
||||
h = 31 * h + Arrays.hashCode(types);
|
||||
h = 31 * h + Arrays.hashCode(normalized);
|
||||
this.hash = h;
|
||||
}
|
||||
|
||||
public int offset(int i) {
|
||||
return offsets[i];
|
||||
}
|
||||
|
||||
public int stride() {
|
||||
return stride;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof AttribLayoutKey k)) return false;
|
||||
return Arrays.equals(locations, k.locations)
|
||||
&& Arrays.equals(sizes, k.sizes)
|
||||
&& Arrays.equals(types, k.types)
|
||||
&& Arrays.equals(normalized, k.normalized);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording.commands;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
|
||||
public final class BatchedIndexedDrawCmd implements DisplayListCommand {
|
||||
private int sharedVAO;
|
||||
private int drawMode;
|
||||
private int indexCount;
|
||||
private int indexType;
|
||||
private long indexByteOffset;
|
||||
|
||||
public void fill(int sharedVAO, int drawMode, int indexCount, int indexType, long indexByteOffset) {
|
||||
this.sharedVAO = sharedVAO;
|
||||
this.drawMode = drawMode;
|
||||
this.indexCount = indexCount;
|
||||
this.indexType = indexType;
|
||||
this.indexByteOffset = indexByteOffset;
|
||||
}
|
||||
|
||||
public int getSharedVAO() { return sharedVAO; }
|
||||
public int getDrawMode() { return drawMode; }
|
||||
public int getIndexCount() { return indexCount; }
|
||||
public int getIndexType() { return indexType; }
|
||||
public long getIndexByteOffset() { return indexByteOffset; }
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
if (sharedVAO == 0) return;
|
||||
GLStateManager.glBindVertexArray(sharedVAO);
|
||||
GLStateManager.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BatchedIndexedDrawCmd(vao=" + sharedVAO + ", mode=" + drawMode + ", count=" + indexCount + ", type=" + indexType + ", offset=" + indexByteOffset + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording.commands;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
|
||||
public final class IndexedDrawBatch {
|
||||
private int sharedVAO;
|
||||
private int sharedVBO;
|
||||
private int sharedEBO;
|
||||
private boolean deleted;
|
||||
|
||||
public IndexedDrawBatch(int sharedVAO, int sharedVBO, int sharedEBO) {
|
||||
this.sharedVAO = sharedVAO;
|
||||
this.sharedVBO = sharedVBO;
|
||||
this.sharedEBO = sharedEBO;
|
||||
}
|
||||
|
||||
public int getSharedVAO() { return sharedVAO; }
|
||||
public int getSharedVBO() { return sharedVBO; }
|
||||
public int getSharedEBO() { return sharedEBO; }
|
||||
|
||||
public void delete() {
|
||||
if (deleted) return;
|
||||
deleted = true;
|
||||
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
|
||||
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
|
||||
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
|
||||
sharedVAO = sharedVBO = sharedEBO = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IndexedDrawBatch(vao=" + sharedVAO + ", vbo=" + sharedVBO + ", ebo=" + sharedEBO + ")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording.commands;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
|
||||
|
||||
public final class IndexedDrawBatchBuilder {
|
||||
|
||||
private final List<IndexedDrawCapture> captures = new ArrayList<>();
|
||||
|
||||
public boolean isEmpty() {
|
||||
return captures.isEmpty();
|
||||
}
|
||||
|
||||
public void add(IndexedDrawCapture capture) {
|
||||
captures.add(capture);
|
||||
}
|
||||
|
||||
public List<IndexedDrawCapture> getCaptures() {
|
||||
return captures;
|
||||
}
|
||||
|
||||
public List<IndexedDrawBatch> build() {
|
||||
final List<IndexedDrawBatch> out = new ArrayList<>();
|
||||
if (captures.isEmpty()) return out;
|
||||
|
||||
final int prevVAO = GLStateManager.getBoundVAO();
|
||||
final int prevVBO = GLStateManager.getBoundVBO();
|
||||
final int prevEBO = GLStateManager.getBoundEBO();
|
||||
|
||||
final List<GroupResult> results = new ArrayList<>();
|
||||
boolean success = false;
|
||||
try {
|
||||
final Object2ObjectLinkedOpenHashMap<AttribLayoutKey, List<IndexedDrawCapture>> groups = new Object2ObjectLinkedOpenHashMap<>();
|
||||
for (IndexedDrawCapture c : captures) {
|
||||
groups.computeIfAbsent(c.layoutKey, k -> new ArrayList<>()).add(c);
|
||||
}
|
||||
|
||||
for (List<IndexedDrawCapture> group : groups.values()) {
|
||||
results.add(buildGroup(group));
|
||||
}
|
||||
// Placeholder fill is deferred until every group's GL work has succeeded.
|
||||
for (GroupResult r : results) r.commit();
|
||||
for (GroupResult r : results) out.add(r.batch);
|
||||
success = true;
|
||||
} finally {
|
||||
if (!success) {
|
||||
for (GroupResult r : results) r.batch.delete();
|
||||
}
|
||||
GLStateManager.glBindVertexArray(prevVAO);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private record GroupResult(IndexedDrawBatch batch, List<IndexedDrawCapture> captures,
|
||||
int[] drawModes, int[] indexCounts, long[] indexOffsets) {
|
||||
void commit() {
|
||||
final int sharedVAO = batch.getSharedVAO();
|
||||
for (int i = 0; i < captures.size(); i++) {
|
||||
captures.get(i).placeholder.fill(sharedVAO, drawModes[i], indexCounts[i], GL11.GL_UNSIGNED_INT, indexOffsets[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static GroupResult buildGroup(List<IndexedDrawCapture> group) {
|
||||
long totalVertexBytes = 0;
|
||||
long totalIndexBytes = 0;
|
||||
for (IndexedDrawCapture c : group) {
|
||||
totalVertexBytes += (long) c.vertexCount * c.layoutKey.stride();
|
||||
totalIndexBytes += (long) c.indexCount * 4; // always GL_UNSIGNED_INT
|
||||
}
|
||||
if (totalVertexBytes > Integer.MAX_VALUE || totalIndexBytes > Integer.MAX_VALUE) {
|
||||
throw new IllegalStateException("batch exceeds 2GiB — split needed");
|
||||
}
|
||||
|
||||
int sharedVAO = 0;
|
||||
int sharedVBO = 0;
|
||||
int sharedEBO = 0;
|
||||
ByteBuffer eboData = null;
|
||||
boolean success = false;
|
||||
|
||||
final int n = group.size();
|
||||
final int[] drawModes = new int[n];
|
||||
final int[] indexCounts = new int[n];
|
||||
final long[] indexOffsets = new long[n];
|
||||
|
||||
try {
|
||||
sharedVAO = GLStateManager.glGenVertexArrays();
|
||||
sharedVBO = GLStateManager.glGenBuffers();
|
||||
sharedEBO = GLStateManager.glGenBuffers();
|
||||
eboData = MemoryUtilities.memAlloc((int) totalIndexBytes);
|
||||
|
||||
GLStateManager.glBindVertexArray(sharedVAO);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, sharedVBO);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, totalVertexBytes, GL15.GL_STATIC_DRAW);
|
||||
|
||||
int vboBytePos = 0;
|
||||
int eboBytePos = 0;
|
||||
int vertexBase = 0;
|
||||
final long eboAddr = memAddress0(eboData);
|
||||
|
||||
for (int g = 0; g < n; g++) {
|
||||
final IndexedDrawCapture c = group.get(g);
|
||||
final int vBytes = c.vertexCount * c.layoutKey.stride();
|
||||
GLStateManager.glBufferSubData(GL15.GL_ARRAY_BUFFER, vboBytePos, c.vertexData);
|
||||
|
||||
final long srcIdxAddr = memAddress0(c.indexData);
|
||||
final int idxBytes = c.indexCount * 4;
|
||||
if (vertexBase == 0) {
|
||||
// First capture (or a group that starts at 0) — indices are already
|
||||
// rebased to [0..vertexCount) in IndexedDrawCapture, so a raw bulk copy
|
||||
// is correct and strictly faster than the per-index loop.
|
||||
memCopy(srcIdxAddr, eboAddr + eboBytePos, idxBytes);
|
||||
} else {
|
||||
final long dstBase = eboAddr + eboBytePos;
|
||||
for (int i = 0; i < c.indexCount; i++) {
|
||||
memPutInt(dstBase + i * 4L, memGetInt(srcIdxAddr + i * 4L) + vertexBase);
|
||||
}
|
||||
}
|
||||
|
||||
drawModes[g] = c.drawMode;
|
||||
indexCounts[g] = c.indexCount;
|
||||
indexOffsets[g] = eboBytePos;
|
||||
|
||||
vboBytePos += vBytes;
|
||||
eboBytePos += idxBytes;
|
||||
vertexBase += c.vertexCount;
|
||||
}
|
||||
|
||||
eboData.position(0).limit((int) totalIndexBytes);
|
||||
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEBO);
|
||||
GLStateManager.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, eboData, GL15.GL_STATIC_DRAW);
|
||||
|
||||
final AttribLayoutKey layout = group.get(0).layoutKey;
|
||||
final int stride = layout.stride();
|
||||
for (int i = 0; i < layout.locations.length; i++) {
|
||||
final int loc = layout.locations[i];
|
||||
final int size = layout.sizes[i];
|
||||
final int type = layout.types[i];
|
||||
GLStateManager.glVertexAttribPointer(loc, size, type, layout.normalized[i], stride, layout.offset(i));
|
||||
GLStateManager.glEnableVertexAttribArray(loc);
|
||||
}
|
||||
|
||||
success = true;
|
||||
return new GroupResult(new IndexedDrawBatch(sharedVAO, sharedVBO, sharedEBO),
|
||||
group, drawModes, indexCounts, indexOffsets);
|
||||
} finally {
|
||||
if (eboData != null) MemoryUtilities.memFree(eboData);
|
||||
if (!success) {
|
||||
if (sharedVAO != 0) GLStateManager.glDeleteVertexArrays(sharedVAO);
|
||||
if (sharedVBO != 0) GLStateManager.glDeleteBuffers(sharedVBO);
|
||||
if (sharedEBO != 0) GLStateManager.glDeleteBuffers(sharedEBO);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording.commands;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.QuadConverter;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.AttribSnapshot;
|
||||
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetLong;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutLong;
|
||||
|
||||
|
||||
/** {@link #create} returns null on any recoverable validation failure; callers must not record the placeholder. */
|
||||
public final class IndexedDrawCapture {
|
||||
public final AttribLayoutKey layoutKey;
|
||||
public final int drawMode;
|
||||
public final int vertexCount;
|
||||
public final int indexCount;
|
||||
public final ByteBuffer vertexData;
|
||||
public final ByteBuffer indexData;
|
||||
public final BatchedIndexedDrawCmd placeholder;
|
||||
private boolean freed;
|
||||
|
||||
public IndexedDrawCapture(AttribLayoutKey layoutKey, int drawMode, int vertexCount, int indexCount,
|
||||
ByteBuffer vertexData, ByteBuffer indexData,
|
||||
BatchedIndexedDrawCmd placeholder) {
|
||||
this.layoutKey = layoutKey;
|
||||
this.drawMode = drawMode;
|
||||
this.vertexCount = vertexCount;
|
||||
this.indexCount = indexCount;
|
||||
this.vertexData = vertexData;
|
||||
this.indexData = indexData;
|
||||
this.placeholder = placeholder;
|
||||
}
|
||||
|
||||
public void freeBuffers() {
|
||||
if (freed) return;
|
||||
freed = true;
|
||||
if (vertexData != null) MemoryUtilities.memFree(vertexData);
|
||||
if (indexData != null) MemoryUtilities.memFree(indexData);
|
||||
}
|
||||
|
||||
public boolean isFreed() {
|
||||
return freed;
|
||||
}
|
||||
|
||||
private static final Set<String> WARN_ONCE = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private static void warnOnce(String key, String fmt, Object... args) {
|
||||
if (WARN_ONCE.add(key)) GLStateManager.LOGGER.warn(fmt, args);
|
||||
}
|
||||
|
||||
public static IndexedDrawCapture create(int mode, int indicesCount, int srcIndexType, long indicesOffset, int eboId) {
|
||||
if (indicesCount == 0) return null;
|
||||
if (eboId == 0) {
|
||||
warnOnce("no-ebo", "[IndexedDrawCapture] glDrawElements in display list with no EBO bound — skipping (mode={} count={} offset={})", mode, indicesCount, indicesOffset);
|
||||
return null;
|
||||
}
|
||||
if (mode == GL11.GL_QUADS && (indicesCount % 4) != 0) {
|
||||
warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 — skipping", indicesCount);
|
||||
return null;
|
||||
}
|
||||
final int srcIndexSize = VertexAttribState.Attrib.glTypeSizeBytes(srcIndexType);
|
||||
if (srcIndexSize <= 0) return null;
|
||||
|
||||
final int prevEBO = GLStateManager.getBoundEBO();
|
||||
final int prevVBO = GLStateManager.getBoundVBO();
|
||||
ByteBuffer eboSrc = null;
|
||||
AttribSnapshot snap = null;
|
||||
ByteBuffer vertexData = null;
|
||||
ByteBuffer indexData = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboId);
|
||||
final int eboBufferSize = GLStateManager.glGetBufferParameteri(GL15.GL_ELEMENT_ARRAY_BUFFER, GL15.GL_BUFFER_SIZE);
|
||||
if (eboBufferSize <= 0) {
|
||||
warnOnce("ebo-size", "[IndexedDrawCapture] GL_BUFFER_SIZE={} for EBO {}", eboBufferSize, eboId);
|
||||
return null;
|
||||
}
|
||||
final long eboReadSize = (long) indicesCount * srcIndexSize;
|
||||
if (indicesOffset < 0 || indicesOffset + eboReadSize > eboBufferSize) {
|
||||
warnOnce("ebo-range", "[IndexedDrawCapture] EBO range {}+{} exceeds buffer size {} — skipping", indicesOffset, eboReadSize, eboBufferSize);
|
||||
return null;
|
||||
}
|
||||
eboSrc = MemoryUtilities.memAlloc((int) eboReadSize);
|
||||
GLStateManager.glGetBufferSubData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesOffset, eboSrc);
|
||||
final long eboSrcAddr = MemoryUtilities.memAddress0(eboSrc);
|
||||
|
||||
final long minMax = QuadConverter.scanMinMaxIndex(eboSrcAddr, srcIndexType, indicesCount);
|
||||
if (minMax == -1L) {
|
||||
warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType);
|
||||
return null;
|
||||
}
|
||||
final int minVtx = (int) (minMax & 0xFFFFFFFFL);
|
||||
final int maxVtx = (int) (minMax >>> 32);
|
||||
final int vertexCount = maxVtx - minVtx + 1;
|
||||
|
||||
snap = AttribSnapshot.snapshot(minVtx, vertexCount);
|
||||
|
||||
int enabledCount = 0;
|
||||
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
|
||||
if (snap.get(i) != null) enabledCount++;
|
||||
}
|
||||
if (enabledCount == 0) {
|
||||
warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping");
|
||||
return null;
|
||||
}
|
||||
final int[] locations = new int[enabledCount];
|
||||
final int[] sizes = new int[enabledCount];
|
||||
final int[] types = new int[enabledCount];
|
||||
final boolean[] normalized = new boolean[enabledCount];
|
||||
int k = 0;
|
||||
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
|
||||
final AttribSnapshot.AttribDesc d = snap.get(i);
|
||||
if (d == null) continue;
|
||||
locations[k] = i;
|
||||
sizes[k] = d.size();
|
||||
types[k] = d.type();
|
||||
normalized[k] = d.normalized();
|
||||
k++;
|
||||
}
|
||||
final AttribLayoutKey layoutKey = new AttribLayoutKey(locations, sizes, types, normalized);
|
||||
final int tightStride = layoutKey.stride();
|
||||
|
||||
ByteBuffer sharedSrcBuffer = null;
|
||||
long sharedSrcBase = 0;
|
||||
int sharedSrcStride = -1;
|
||||
boolean tightSrc = true;
|
||||
for (int a = 0; a < enabledCount; a++) {
|
||||
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
|
||||
final int relOffset = (int) (d.offset() - d.readBufferBaseOffset());
|
||||
if (a == 0) {
|
||||
sharedSrcBuffer = d.readBuffer();
|
||||
sharedSrcStride = d.effectiveStride();
|
||||
sharedSrcBase = MemoryUtilities.memAddress0(sharedSrcBuffer) + relOffset + (long) minVtx * sharedSrcStride;
|
||||
if (relOffset != layoutKey.offset(0)) tightSrc = false;
|
||||
} else if (d.readBuffer() != sharedSrcBuffer || d.effectiveStride() != sharedSrcStride || relOffset != layoutKey.offset(a)) {
|
||||
tightSrc = false;
|
||||
}
|
||||
}
|
||||
if (tightSrc && sharedSrcStride != tightStride) tightSrc = false;
|
||||
|
||||
vertexData = MemoryUtilities.memAlloc(vertexCount * tightStride);
|
||||
final long dstAddr = MemoryUtilities.memAddress0(vertexData);
|
||||
if (tightSrc) {
|
||||
MemoryUtilities.memCopy(sharedSrcBase, dstAddr, (long) vertexCount * tightStride);
|
||||
} else {
|
||||
for (int a = 0; a < enabledCount; a++) {
|
||||
final AttribSnapshot.AttribDesc d = snap.get(locations[a]);
|
||||
final int attribBytes = d.size() * d.typeSizeBytes();
|
||||
final int srcStride = d.effectiveStride();
|
||||
final long srcStart = MemoryUtilities.memAddress0(d.readBuffer()) + (d.offset() - d.readBufferBaseOffset()) + (long) minVtx * srcStride;
|
||||
final long dstStart = dstAddr + layoutKey.offset(a);
|
||||
copyStrided(srcStart, srcStride, dstStart, tightStride, attribBytes, vertexCount);
|
||||
}
|
||||
}
|
||||
|
||||
final int bakedDrawMode;
|
||||
final int bakedIndexCount;
|
||||
if (mode == GL11.GL_QUADS) {
|
||||
final int quadCount = indicesCount / 4;
|
||||
bakedIndexCount = quadCount * 6;
|
||||
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
|
||||
QuadConverter.triangulateQuads(eboSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, quadCount, minVtx);
|
||||
bakedDrawMode = GL11.GL_TRIANGLES;
|
||||
} else {
|
||||
bakedIndexCount = indicesCount;
|
||||
indexData = MemoryUtilities.memAlloc(bakedIndexCount * 4);
|
||||
QuadConverter.widenIndices(eboSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, indicesCount, minVtx);
|
||||
bakedDrawMode = mode;
|
||||
}
|
||||
|
||||
final BatchedIndexedDrawCmd placeholder = new BatchedIndexedDrawCmd();
|
||||
|
||||
final IndexedDrawCapture out = new IndexedDrawCapture(layoutKey, bakedDrawMode, vertexCount, bakedIndexCount, vertexData, indexData, placeholder);
|
||||
success = true;
|
||||
return out;
|
||||
} finally {
|
||||
if (eboSrc != null) MemoryUtilities.memFree(eboSrc);
|
||||
if (snap != null) snap.free();
|
||||
if (!success) {
|
||||
if (vertexData != null) MemoryUtilities.memFree(vertexData);
|
||||
if (indexData != null) MemoryUtilities.memFree(indexData);
|
||||
}
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy {@code attribBytes} per vertex from {@code src} (stride {@code srcStride}) to
|
||||
* {@code dst} (stride {@code dstStride}) for {@code vertexCount} vertices.
|
||||
*
|
||||
* <p>Specializes on the common attribute sizes (4 / 8 / 12 / 16) so the hot path is
|
||||
* one or two {@code memPut} calls per vertex instead of a JNI-ish {@code memCopy}
|
||||
* dispatch. Falls through to {@code memCopy} for exotic sizes.
|
||||
*/
|
||||
private static void copyStrided(long src, int srcStride, long dst, int dstStride, int attribBytes, int vertexCount) {
|
||||
switch (attribBytes) {
|
||||
case 4 -> {
|
||||
for (int v = 0; v < vertexCount; v++) {
|
||||
memPutInt(dst, memGetInt(src));
|
||||
src += srcStride; dst += dstStride;
|
||||
}
|
||||
}
|
||||
case 8 -> {
|
||||
for (int v = 0; v < vertexCount; v++) {
|
||||
memPutLong(dst, memGetLong(src));
|
||||
src += srcStride; dst += dstStride;
|
||||
}
|
||||
}
|
||||
case 12 -> {
|
||||
for (int v = 0; v < vertexCount; v++) {
|
||||
memPutLong(dst, memGetLong(src));
|
||||
memPutInt(dst + 8, memGetInt(src + 8));
|
||||
src += srcStride; dst += dstStride;
|
||||
}
|
||||
}
|
||||
case 16 -> {
|
||||
for (int v = 0; v < vertexCount; v++) {
|
||||
memPutLong(dst, memGetLong(src));
|
||||
memPutLong(dst + 8, memGetLong(src + 8));
|
||||
src += srcStride; dst += dstStride;
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
for (int v = 0; v < vertexCount; v++) {
|
||||
MemoryUtilities.memCopy(src, dst, attribBytes);
|
||||
src += srcStride; dst += dstStride;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,13 +108,6 @@ public class VertexAttribState {
|
||||
clientSideEnabledCount = count;
|
||||
}
|
||||
|
||||
public static boolean hasVBOBoundAttrib() {
|
||||
for (int i = 0; i < MAX_ATTRIBS; i++) {
|
||||
if (current[i].enabled && current[i].vboId != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any currently enabled vertex attribute was registered without a VBO
|
||||
* (i.e. as a client-side pointer). In core profile, such attribs are treated as null
|
||||
@@ -167,6 +160,10 @@ public class VertexAttribState {
|
||||
}
|
||||
|
||||
public float readComponent(ByteBuffer buf, int base, int component) {
|
||||
return readComponent(type, normalized, buf, base, component);
|
||||
}
|
||||
|
||||
public static float readComponent(int type, boolean normalized, ByteBuffer buf, int base, int component) {
|
||||
return switch (type) {
|
||||
case GL11.GL_FLOAT -> buf.getFloat(base + component * 4);
|
||||
case GL11.GL_DOUBLE -> (float) buf.getDouble(base + component * 8);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.glsm.streaming;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -40,7 +41,7 @@ public final class OrphanStreamingBuffer {
|
||||
|
||||
public void destroy() {
|
||||
if (bufferId != 0) {
|
||||
RENDER_BACKEND.deleteBuffers(bufferId);
|
||||
GLStateManager.glDeleteBuffers(bufferId);
|
||||
bufferId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.glsm.streaming;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
|
||||
import org.embeddedt.embeddium.impl.gl.sync.GlFence;
|
||||
@@ -52,7 +53,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
|
||||
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
|
||||
if (this.mappedBuffer == null) {
|
||||
RENDER_BACKEND.deleteBuffers(bufferId);
|
||||
GLStateManager.glDeleteBuffers(bufferId);
|
||||
bufferId = 0;
|
||||
throw new RuntimeException("Failed to persistently map streaming buffer");
|
||||
}
|
||||
@@ -147,7 +148,7 @@ public class PersistentStreamingBuffer implements StreamingBuffer {
|
||||
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, bufferId);
|
||||
RENDER_BACKEND.unmapBuffer(GL15.GL_ARRAY_BUFFER);
|
||||
RENDER_BACKEND.bindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
RENDER_BACKEND.deleteBuffers(bufferId);
|
||||
GLStateManager.glDeleteBuffers(bufferId);
|
||||
bufferId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class QuadConverterTest {
|
||||
|
||||
@Test
|
||||
void triangulateQuads_ubyteToUint_lastVertexConvention() {
|
||||
// 2 quads: [0,1,2,3] and [4,5,6,7]. Expected LAST-convention output:
|
||||
// quad0 → (0,1,3), (1,2,3)
|
||||
// quad1 → (4,5,7), (5,6,7)
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(8);
|
||||
for (int i = 0; i < 8; i++) src.put(i, (byte) i);
|
||||
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(12 * 4);
|
||||
try {
|
||||
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_INT, 2);
|
||||
|
||||
assertUintIndices(dst, 0, 1, 3, 1, 2, 3, 4, 5, 7, 5, 6, 7);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void triangulateQuads_ushortToUshort_preservesType() {
|
||||
// USHORT in, USHORT out — same-size path for live draws.
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 2);
|
||||
for (int i = 0; i < 4; i++) src.putShort(i * 2, (short) (i + 100));
|
||||
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 2);
|
||||
try {
|
||||
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_SHORT, 1);
|
||||
|
||||
assertUshortIndices(dst, 100, 101, 103, 101, 102, 103);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void triangulateQuads_ushortToUint_widens() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 2);
|
||||
for (int i = 0; i < 4; i++) src.putShort(i * 2, (short) (0xFF00 | i));
|
||||
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 4);
|
||||
try {
|
||||
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1);
|
||||
|
||||
// Zero-extended USHORT→UINT, LAST winding.
|
||||
assertUintIndices(dst, 0xFF00, 0xFF01, 0xFF03, 0xFF01, 0xFF02, 0xFF03);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void triangulateQuads_uintToUint_identity() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(4 * 4);
|
||||
for (int i = 0; i < 4; i++) src.putInt(i * 4, 1_000_000 + i);
|
||||
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(6 * 4);
|
||||
try {
|
||||
QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1);
|
||||
|
||||
assertUintIndices(dst, 1_000_000, 1_000_001, 1_000_003, 1_000_001, 1_000_002, 1_000_003);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void triangulateQuads_zeroQuads_noop() {
|
||||
QuadConverter.triangulateQuads(0L, GL11.GL_UNSIGNED_INT, 0L, GL11.GL_UNSIGNED_INT, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void triangulateQuads_unsupportedTypes_throw() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(16);
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(24);
|
||||
try {
|
||||
assertThrows(IllegalArgumentException.class, () -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_FLOAT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 1));
|
||||
assertThrows(IllegalArgumentException.class, () -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 1));
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void widenIndices_ubyteToUint() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(5);
|
||||
for (int i = 0; i < 5; i++) src.put(i, (byte) (200 + i));
|
||||
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(5 * 4);
|
||||
try {
|
||||
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_INT, 5);
|
||||
|
||||
assertUintIndices(dst, 200, 201, 202, 203, 204);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void widenIndices_ushortToUint_zeroExtends() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(3 * 2);
|
||||
src.putShort(0, (short) 0xFFFF);
|
||||
src.putShort(2, (short) 0x8001);
|
||||
src.putShort(4, (short) 0);
|
||||
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(3 * 4);
|
||||
try {
|
||||
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_SHORT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 3);
|
||||
|
||||
assertUintIndices(dst, 0xFFFF, 0x8001, 0);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void widenIndices_uintToUint_copiesBitsExactly() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(3 * 4);
|
||||
src.putInt(0, 0x01234567);
|
||||
src.putInt(4, 0xDEADBEEF);
|
||||
src.putInt(8, 0);
|
||||
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(3 * 4);
|
||||
try {
|
||||
QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_INT, memAddress0(dst), GL11.GL_UNSIGNED_INT, 3);
|
||||
|
||||
assertUintIndices(dst, 0x01234567, 0xDEADBEEF, 0);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanMinMaxIndex_ushort_typical() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(6 * 2);
|
||||
final int[] data = {42, 7, 1000, 99, 500, 3};
|
||||
for (int i = 0; i < data.length; i++) src.putShort(i * 2, (short) data[i]);
|
||||
|
||||
try {
|
||||
final long packed = QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_SHORT, data.length);
|
||||
final int min = (int) (packed & 0xFFFFFFFFL);
|
||||
final int max = (int) (packed >>> 32);
|
||||
assertEquals(3, min);
|
||||
assertEquals(1000, max);
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanMinMaxIndex_emptyCountReturnsMinusOne() {
|
||||
assertEquals(-1L, QuadConverter.scanMinMaxIndex(0L, GL11.GL_UNSIGNED_SHORT, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanMinMaxIndex_unsupportedTypeReturnsMinusOne() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(4);
|
||||
try {
|
||||
assertEquals(-1L, QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_FLOAT, 1));
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanMinMaxIndex_uintOverIntMaxRejected() {
|
||||
// readIndex/writeIndex/rebase route UINT through signed int; values above
|
||||
// Integer.MAX_VALUE would silently wrap. scanMinMaxIndex must bail.
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(2 * 4);
|
||||
src.putInt(0, 10);
|
||||
src.putInt(4, 0x80000000); // 2^31
|
||||
try {
|
||||
assertEquals(-1L, QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_INT, 2));
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void scanMinMaxIndex_uintUpToIntMaxAccepted() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(2 * 4);
|
||||
src.putInt(0, 5);
|
||||
src.putInt(4, Integer.MAX_VALUE);
|
||||
try {
|
||||
final long packed = QuadConverter.scanMinMaxIndex(memAddress0(src), GL11.GL_UNSIGNED_INT, 2);
|
||||
assertEquals(5, (int) (packed & 0xFFFFFFFFL));
|
||||
assertEquals(Integer.MAX_VALUE, (int) (packed >>> 32));
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void widenIndices_ubyteDstRejected() {
|
||||
// UBYTE is not a supported destination — widening only.
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(2);
|
||||
src.put(0, (byte) 1).put(1, (byte) 2);
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(2);
|
||||
try {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> QuadConverter.widenIndices(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 2));
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void triangulateQuads_ubyteDstRejected() {
|
||||
final ByteBuffer src = MemoryUtilities.memAlloc(4);
|
||||
for (int i = 0; i < 4; i++) src.put(i, (byte) i);
|
||||
final ByteBuffer dst = MemoryUtilities.memAlloc(6);
|
||||
try {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> QuadConverter.triangulateQuads(memAddress0(src), GL11.GL_UNSIGNED_BYTE, memAddress0(dst), GL11.GL_UNSIGNED_BYTE, 1));
|
||||
} finally {
|
||||
MemoryUtilities.memFree(src);
|
||||
MemoryUtilities.memFree(dst);
|
||||
}
|
||||
}
|
||||
|
||||
// === helpers ===
|
||||
|
||||
private static void assertUintIndices(ByteBuffer buf, int... expected) {
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
assertEquals(expected[i], buf.getInt(i * 4), "uint index[" + i + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertUshortIndices(ByteBuffer buf, int... expected) {
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
assertEquals(expected[i], buf.getShort(i * 2) & 0xFFFF, "ushort index[" + i + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.LWJGLException;
|
||||
import org.lwjgl.opengl.ContextAttribs;
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.lwjgl.opengl.DisplayMode;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.PixelFormat;
|
||||
import org.lwjgl.opengl.SharedDrawable;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
class SharedDrawableVaoTest {
|
||||
|
||||
private static final String VERT_330 = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
private static final String FRAG_330 = "#version 330 core\nout vec4 o;\nvoid main() { o = vec4(0.0); }\n";
|
||||
|
||||
@BeforeAll
|
||||
static void createContext() throws LWJGLException {
|
||||
assumeTrue(!Display.isCreated(), "inherited Display -- skipping to avoid profile drift");
|
||||
Display.setDisplayModeAndFullscreen(new DisplayMode(800, 600));
|
||||
Display.setResizable(false);
|
||||
Display.setFullscreen(false);
|
||||
Display.create(
|
||||
new PixelFormat().withDepthBits(24).withStencilBits(8),
|
||||
new ContextAttribs(4, 1).withProfileCore(true).withForwardCompatible(true));
|
||||
GL30.glBindVertexArray(GL30.glGenVertexArrays());
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void destroyContext() {
|
||||
if (Display.isCreated()) Display.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateProgramSucceedsOnSharedDrawableWithVao() throws LWJGLException {
|
||||
SharedDrawable shared = new SharedDrawable(Display.getDrawable());
|
||||
Display.getDrawable().releaseContext();
|
||||
shared.makeCurrent();
|
||||
try {
|
||||
GL30.glBindVertexArray(GL30.glGenVertexArrays());
|
||||
int program = linkTrivialProgram();
|
||||
GL20.glValidateProgram(program);
|
||||
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(program, GL20.GL_VALIDATE_STATUS), () -> GL20.glGetProgramInfoLog(program, 4096));
|
||||
GL20.glDeleteProgram(program);
|
||||
} finally {
|
||||
shared.releaseContext();
|
||||
Display.getDrawable().makeCurrent();
|
||||
shared.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private static int linkTrivialProgram() {
|
||||
int vs = compile(GL20.GL_VERTEX_SHADER, VERT_330);
|
||||
int fs = compile(GL20.GL_FRAGMENT_SHADER, FRAG_330);
|
||||
int program = GL20.glCreateProgram();
|
||||
GL20.glAttachShader(program, vs);
|
||||
GL20.glAttachShader(program, fs);
|
||||
GL20.glLinkProgram(program);
|
||||
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(program, GL20.GL_LINK_STATUS), () -> GL20.glGetProgramInfoLog(program, 4096));
|
||||
GL20.glDeleteShader(vs);
|
||||
GL20.glDeleteShader(fs);
|
||||
return program;
|
||||
}
|
||||
|
||||
private static int compile(int type, String src) {
|
||||
int shader = GL20.glCreateShader(type);
|
||||
GL20.glShaderSource(shader, src);
|
||||
GL20.glCompileShader(shader);
|
||||
assertEquals(GL11.GL_TRUE, GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS), () -> GL20.glGetShaderInfoLog(shader, 4096));
|
||||
return shader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.gtnewhorizons.angelica.glsm.architecture;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DeletePathConventionTest {
|
||||
|
||||
private static final Pattern FORBIDDEN = Pattern.compile("\\bRENDER_BACKEND\\s*\\.\\s*delete[A-Z][A-Za-z]*\\b");
|
||||
private static final String ALLOWLIST_MARKER = "// glsm-allow-direct-delete:";
|
||||
|
||||
/** Only this file is allowed to call RENDER_BACKEND.delete* — it is the cache-invalidation layer. */
|
||||
private static final String GATEKEEPER_FILE_NAME = "GLStateManager.java";
|
||||
|
||||
@Test
|
||||
void glsmMainSources_onlyCallBackendDeleteFromGLStateManager() throws IOException {
|
||||
final Path root = locateGlsmMainRoot();
|
||||
final List<String> violations = new ArrayList<>();
|
||||
|
||||
try (Stream<Path> paths = Files.walk(root)) {
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".java"))
|
||||
.filter(p -> !p.getFileName().toString().equals(GATEKEEPER_FILE_NAME))
|
||||
.forEach(p -> scanFile(p, violations));
|
||||
}
|
||||
|
||||
assertTrue(violations.isEmpty(),
|
||||
() -> "Direct RENDER_BACKEND.delete*(...) call found outside " + GATEKEEPER_FILE_NAME + ".\n"
|
||||
+ "Use GLStateManager.glDelete*() instead\n"
|
||||
+ "Violations:\n " + String.join("\n ", violations));
|
||||
}
|
||||
|
||||
private static void scanFile(Path file, List<String> violations) {
|
||||
final List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(file, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
violations.add(file + ": I/O error reading file: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
final String line = lines.get(i);
|
||||
final Matcher m = FORBIDDEN.matcher(line);
|
||||
if (!m.find()) continue;
|
||||
if (line.contains(ALLOWLIST_MARKER)) continue;
|
||||
violations.add(file + ":" + (i + 1) + " " + line.trim());
|
||||
}
|
||||
}
|
||||
|
||||
private static Path locateGlsmMainRoot() {
|
||||
final String[] candidates = {
|
||||
"src/main/java/com/gtnewhorizons/angelica/glsm",
|
||||
"glsm/src/main/java/com/gtnewhorizons/angelica/glsm",
|
||||
};
|
||||
for (String c : candidates) {
|
||||
final Path p = Paths.get(c).toAbsolutePath();
|
||||
if (Files.isDirectory(p)) return p;
|
||||
}
|
||||
throw new IllegalStateException("Could not find glsm main sources — cwd=" + Paths.get("").toAbsolutePath());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
|
||||
import com.gtnewhorizons.angelica.glsm.states.VertexAttribState;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class IndexedDrawBatchBuilderTest extends DisplayListTestFixture {
|
||||
|
||||
@Test
|
||||
void singleDraw_bakedSurvivesVboMutation() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
|
||||
assertNotNull(cmd);
|
||||
assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode());
|
||||
assertEquals(6, cmd.getIndexCount());
|
||||
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "batched indices always widen to GL_UNSIGNED_INT");
|
||||
|
||||
// Overwrite source VBO with garbage — baked batch must be unaffected.
|
||||
overwriteBufferWithGarbage(GL15.GL_ARRAY_BUFFER, vbo, 4 * POS_COLOR_STRIDE);
|
||||
|
||||
final ByteBuffer read = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), 4 * POS_COLOR_STRIDE);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
assertEquals(DEFAULT_QUAD_POSITIONS[i * 2], read.getFloat(i * POS_COLOR_STRIDE), 0f, "pos.x v=" + i);
|
||||
assertEquals(DEFAULT_QUAD_POSITIONS[i * 2 + 1], read.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "pos.y v=" + i);
|
||||
for (int c = 0; c < 4; c++) {
|
||||
assertEquals(DEFAULT_SOLID_COLORS[i][c], read.get(i * POS_COLOR_STRIDE + 8 + c), "color v=" + i + " c=" + c);
|
||||
}
|
||||
}
|
||||
MemoryUtilities.memFree(read);
|
||||
}
|
||||
|
||||
@Test
|
||||
void compileAndExecute_firesLiveAndBakes() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
|
||||
assertNotNull(cmd);
|
||||
assertEquals(3, cmd.getIndexCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void quads_triangulateAtRecord() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
|
||||
assertNotNull(cmd);
|
||||
assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode());
|
||||
assertEquals(6, cmd.getIndexCount());
|
||||
|
||||
// EBO is widened to UNSIGNED_INT and uses GL_LAST_VERTEX_CONVENTION:
|
||||
// (a,b,d,b,c,d) — v3 is the provoking vertex for both triangles, diagonal b→d.
|
||||
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, firstBatch(list).getSharedEBO(), 6 * 4);
|
||||
assertUintIndices(ib, 0, 1, 3, 1, 2, 3);
|
||||
MemoryUtilities.memFree(ib);
|
||||
}
|
||||
|
||||
@Test
|
||||
void offsetAndMinMaxRebase() {
|
||||
// 8-vertex VBO; draw references only vertices 5..7 via a 6L byte offset into the EBO.
|
||||
final float[] positions = new float[16];
|
||||
final byte[][] colors = new byte[8][4];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
positions[i * 2] = i;
|
||||
positions[i * 2 + 1] = i * 2;
|
||||
colors[i][0] = (byte) i;
|
||||
colors[i][1] = (byte) (i + 1);
|
||||
colors[i][2] = (byte) (i + 2);
|
||||
colors[i][3] = (byte) (i + 3);
|
||||
}
|
||||
final int vbo = uploadPosColorVbo(positions, colors);
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 5, 6, 7);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 6L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
assertEquals(1, allBatchedCmds(list).size());
|
||||
|
||||
final IndexedDrawBatch batch = firstBatch(list);
|
||||
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, batch.getSharedEBO(), 3 * 4);
|
||||
// Indices rebased against the tight [minVtx..maxVtx] range.
|
||||
assertUintIndices(ib, 0, 1, 2);
|
||||
MemoryUtilities.memFree(ib);
|
||||
|
||||
final ByteBuffer vboRead = readBufferBytes(GL15.GL_ARRAY_BUFFER, batch.getSharedVBO(), 3 * POS_COLOR_STRIDE);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
final int orig = 5 + i;
|
||||
assertEquals(positions[orig * 2], vboRead.getFloat(i * POS_COLOR_STRIDE), 0f, "pos.x v=" + orig);
|
||||
assertEquals(positions[orig * 2 + 1], vboRead.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "pos.y v=" + orig);
|
||||
for (int c = 0; c < 4; c++) {
|
||||
assertEquals(colors[orig][c], vboRead.get(i * POS_COLOR_STRIDE + 8 + c), "col v=" + orig + " c=" + c);
|
||||
}
|
||||
}
|
||||
MemoryUtilities.memFree(vboRead);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "indexType=0x{0}")
|
||||
@ValueSource(ints = {GL11.GL_UNSIGNED_BYTE, GL11.GL_UNSIGNED_SHORT, GL11.GL_UNSIGNED_INT})
|
||||
void anyIndexType_widensToUnsignedInt(int indexType) {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(indexType, 0, 1, 2, 2, 3, 0);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, indexType);
|
||||
|
||||
final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list);
|
||||
assertNotNull(cmd);
|
||||
assertEquals(6, cmd.getIndexCount());
|
||||
assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "batching always widens indices to UINT for safe base-vertex shifting");
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyRecorder_producesNoBatches() {
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
assertTrue(batchesOf(list).isEmpty(), "recorder with no draws produces no batches");
|
||||
assertTrue(allBatchedCmds(list).isEmpty(), "no placeholders either");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noEboBoundSkipsAndDoesNotBake() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
assertTrue(allBatchedCmds(list).isEmpty(), "no EBO bound → no batched command attached");
|
||||
assertTrue(batchesOf(list).isEmpty(), "no batches either");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customLoc3FloatAttribRoundtrips() {
|
||||
// 4-attrib layout: pos(2f) | uv(2f) | col(4ub) | texBounds(4f) = 36 bytes/vertex.
|
||||
final int STRIDE = 36;
|
||||
final int N = 4;
|
||||
final float[][] pos = {{0f,0f},{1f,0f},{1f,1f},{0f,1f}};
|
||||
final float[][] uv = {{0.1f,0.2f},{0.3f,0.4f},{0.5f,0.6f},{0.7f,0.8f}};
|
||||
final byte[][] col = {{10,20,30,40},{50,60,70,(byte)80},{(byte)90,(byte)100,(byte)110,(byte)120},{(byte)130,(byte)140,(byte)150,(byte)160}};
|
||||
final float[][] tb = {{1f,2f,3f,4f},{5f,6f,7f,8f},{9f,10f,11f,12f},{13f,14f,15f,16f}};
|
||||
final int vbo = newBuffer();
|
||||
final ByteBuffer data = MemoryUtilities.memAlloc(N * STRIDE).order(ByteOrder.nativeOrder());
|
||||
for (int i = 0; i < N; i++) {
|
||||
data.putFloat(pos[i][0]); data.putFloat(pos[i][1]);
|
||||
data.putFloat(uv[i][0]); data.putFloat(uv[i][1]);
|
||||
data.put(col[i][0]); data.put(col[i][1]); data.put(col[i][2]); data.put(col[i][3]);
|
||||
data.putFloat(tb[i][0]); data.putFloat(tb[i][1]); data.putFloat(tb[i][2]); data.putFloat(tb[i][3]);
|
||||
}
|
||||
data.flip();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
MemoryUtilities.memFree(data);
|
||||
|
||||
final int vao = newVao();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, STRIDE, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
GLStateManager.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, STRIDE, 8);
|
||||
GLStateManager.glEnableVertexAttribArray(1);
|
||||
GLStateManager.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, 16);
|
||||
GLStateManager.glEnableVertexAttribArray(2);
|
||||
GLStateManager.glVertexAttribPointer(3, 4, GL11.GL_FLOAT, false, STRIDE, 20);
|
||||
GLStateManager.glEnableVertexAttribArray(3);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
assertNotNull(firstBatchedCmd(list), "loc3 attrib should not cause the baker to skip");
|
||||
|
||||
// Baked layout is tight interleaved in ascending-location order — same stride as source.
|
||||
final ByteBuffer read = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), N * STRIDE);
|
||||
for (int i = 0; i < N; i++) {
|
||||
final int base = i * STRIDE;
|
||||
assertEquals(pos[i][0], read.getFloat(base), 0f, "pos.x v=" + i);
|
||||
assertEquals(pos[i][1], read.getFloat(base + 4), 0f, "pos.y v=" + i);
|
||||
assertEquals(uv[i][0], read.getFloat(base + 8), 0f, "uv.s v=" + i);
|
||||
assertEquals(uv[i][1], read.getFloat(base + 12), 0f, "uv.t v=" + i);
|
||||
for (int c = 0; c < 4; c++) {
|
||||
assertEquals(col[i][c], read.get(base + 16 + c), "col v=" + i + " c=" + c);
|
||||
}
|
||||
for (int c = 0; c < 4; c++) {
|
||||
assertEquals(tb[i][c], read.getFloat(base + 20 + c * 4), 0f, "tb v=" + i + " c=" + c);
|
||||
}
|
||||
}
|
||||
MemoryUtilities.memFree(read);
|
||||
}
|
||||
|
||||
@Test
|
||||
void glDeleteLists_freesBatchedObjects() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
final IndexedDrawBatch batch = firstBatch(list);
|
||||
final int sharedVAO = batch.getSharedVAO();
|
||||
final int sharedVBO = batch.getSharedVBO();
|
||||
final int sharedEBO = batch.getSharedEBO();
|
||||
assertTrue(GL15.glIsBuffer(sharedVBO), "shared VBO live");
|
||||
assertTrue(GL15.glIsBuffer(sharedEBO), "shared EBO live");
|
||||
assertTrue(GL30.glIsVertexArray(sharedVAO), "shared VAO live");
|
||||
|
||||
GLStateManager.glDeleteLists(list, 1);
|
||||
untrackList(list);
|
||||
|
||||
assertFalse(GL15.glIsBuffer(sharedVBO), "shared VBO freed");
|
||||
assertFalse(GL15.glIsBuffer(sharedEBO), "shared EBO freed");
|
||||
assertFalse(GL30.glIsVertexArray(sharedVAO), "shared VAO freed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reRecord_freesOldBatchedObjects() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
final IndexedDrawBatch first = firstBatch(list);
|
||||
final int oldVAO = first.getSharedVAO();
|
||||
final int oldVBO = first.getSharedVBO();
|
||||
final int oldEBO = first.getSharedEBO();
|
||||
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
assertFalse(GL15.glIsBuffer(oldVBO), "old shared VBO freed on re-record");
|
||||
assertFalse(GL15.glIsBuffer(oldEBO), "old shared EBO freed on re-record");
|
||||
assertFalse(GL30.glIsVertexArray(oldVAO), "old shared VAO freed on re-record");
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientPointerAttribsBakeIntoSharedVbo() {
|
||||
final int N = 4;
|
||||
final ByteBuffer clientData = BufferUtils.createByteBuffer(N * POS_COLOR_STRIDE).order(ByteOrder.nativeOrder());
|
||||
final float[][] pos = {{0f,0f},{1f,0f},{1f,1f},{0f,1f}};
|
||||
final byte[][] col = {{(byte)0xAA,(byte)0xBB,(byte)0xCC,(byte)0xDD},
|
||||
{(byte)0x11,(byte)0x22,(byte)0x33,(byte)0x44},
|
||||
{(byte)0x55,(byte)0x66,(byte)0x77,(byte)0x88},
|
||||
{(byte)0x99,(byte)0x00,(byte)0x0F,(byte)0xF0}};
|
||||
for (int i = 0; i < N; i++) {
|
||||
clientData.putFloat(pos[i][0]); clientData.putFloat(pos[i][1]);
|
||||
clientData.put(col[i][0]); clientData.put(col[i][1]);
|
||||
clientData.put(col[i][2]); clientData.put(col[i][3]);
|
||||
}
|
||||
clientData.flip();
|
||||
|
||||
final int vao = newVao();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
final ByteBuffer colorSlice = clientData.duplicate().order(ByteOrder.nativeOrder());
|
||||
colorSlice.position(8);
|
||||
VertexAttribState.set(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, clientData, 0);
|
||||
VertexAttribState.setEnabled(0, true);
|
||||
VertexAttribState.set(1, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, colorSlice, 0);
|
||||
VertexAttribState.setEnabled(1, true);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
assertNotNull(firstBatchedCmd(list));
|
||||
|
||||
// Tight interleaved baked layout for 3 drawn vertices = 36 bytes total.
|
||||
final ByteBuffer owned = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), 3 * POS_COLOR_STRIDE);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
assertEquals(pos[i][0], owned.getFloat(i * POS_COLOR_STRIDE), 0f, "loc0 pos.x v=" + i);
|
||||
assertEquals(pos[i][1], owned.getFloat(i * POS_COLOR_STRIDE + 4), 0f, "loc0 pos.y v=" + i);
|
||||
for (int c = 0; c < 4; c++) {
|
||||
assertEquals(col[i][c], owned.get(i * POS_COLOR_STRIDE + 8 + c), "loc1 color v=" + i + " c=" + c);
|
||||
}
|
||||
}
|
||||
MemoryUtilities.memFree(owned);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiDrawSameFormat_mergesIntoSingleBatch() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int eboA = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
final int eboB = uploadEbo(GL11.GL_UNSIGNED_SHORT, 2, 3, 0);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboA);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, eboB);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
|
||||
assertEquals(2, cmds.size(), "two indexed draws → two batched commands");
|
||||
final List<IndexedDrawBatch> batches = batchesOf(list);
|
||||
assertEquals(1, batches.size(), "same layout → one shared batch");
|
||||
|
||||
assertEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "shared VAO across draws in the same batch");
|
||||
assertEquals(batches.getFirst().getSharedVAO(), cmds.get(0).getSharedVAO());
|
||||
|
||||
assertEquals(0L, cmds.get(0).getIndexByteOffset());
|
||||
assertEquals(3 * 4L, cmds.get(1).getIndexByteOffset(), "second draw offset = first.indexCount * sizeof(UINT)");
|
||||
|
||||
// Shared EBO holds concatenated + rebased UINT indices. First draw minVtx=0 → [0,1,2];
|
||||
// second draw minVtx=0, vertexBase=3 → [2+3, 3+3, 0+3] = [5, 6, 3].
|
||||
final ByteBuffer ib = readBufferBytes(GL15.GL_ELEMENT_ARRAY_BUFFER, batches.getFirst().getSharedEBO(), 6 * 4);
|
||||
assertUintIndices(ib, 0, 1, 2, 5, 6, 3);
|
||||
MemoryUtilities.memFree(ib);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiDrawDifferentFormats_splitBatches() {
|
||||
// Draw A: pos+color (stride=12). Draw B: pos-only (stride=8). Different layout → two batches.
|
||||
final int vboA = defaultQuadVbo();
|
||||
final int vaoA = setupPosColorVao(vboA);
|
||||
|
||||
final int vboB = newBuffer();
|
||||
final ByteBuffer bData = MemoryUtilities.memAlloc(4 * 8).order(ByteOrder.nativeOrder());
|
||||
for (int i = 0; i < 4; i++) {
|
||||
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2]);
|
||||
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2 + 1]);
|
||||
}
|
||||
bData.flip();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, bData, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
MemoryUtilities.memFree(bData);
|
||||
|
||||
final int vaoB = newVao();
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
|
||||
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 8, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vaoA);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
|
||||
assertEquals(2, cmds.size());
|
||||
assertEquals(2, batchesOf(list).size(), "incompatible layouts → two batches");
|
||||
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameSizesDifferentLocations_splitIntoSeparateBatches() {
|
||||
// Both VAOs: pos 2f + color 4ub (same sizes/types/normalized). Only the enabled
|
||||
// location sets differ — VAO-A uses {0,1}, VAO-B uses {0,2}. If AttribLayoutKey
|
||||
// forgot about locations they'd merge and VAO-B's color data would get wired to
|
||||
// loc 1 instead of loc 2.
|
||||
final int vbo = defaultQuadVbo();
|
||||
|
||||
final int vaoA = setupPosColorVao(vbo);
|
||||
|
||||
final int vaoB = newVao();
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
GLStateManager.glVertexAttribPointer(2, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, 8);
|
||||
GLStateManager.glEnableVertexAttribArray(2);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vaoA);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
|
||||
assertEquals(2, cmds.size());
|
||||
assertEquals(2, batchesOf(list).size(), "different enabled locations → two batches");
|
||||
assertNotEquals(cmds.get(0).getSharedVAO(), cmds.get(1).getSharedVAO(), "VAOs from separate batches must be distinct");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullListProbeCompiles() {
|
||||
assertNull(DisplayListManager.getDisplayList(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiLayoutDraws_emitInInsertionOrder() {
|
||||
// Two distinct layouts. The builder uses a LinkedHashMap, so the first
|
||||
// recorded layout must always be the first batch out.
|
||||
final int vboA = defaultQuadVbo();
|
||||
final int vaoA = setupPosColorVao(vboA);
|
||||
|
||||
final int vboB = newBuffer();
|
||||
final ByteBuffer bData = MemoryUtilities.memAlloc(4 * 8).order(ByteOrder.nativeOrder());
|
||||
for (int i = 0; i < 4; i++) {
|
||||
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2]);
|
||||
bData.putFloat(DEFAULT_QUAD_POSITIONS[i * 2 + 1]);
|
||||
}
|
||||
bData.flip();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, bData, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
MemoryUtilities.memFree(bData);
|
||||
|
||||
final int vaoB = newVao();
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboB);
|
||||
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, 8, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2);
|
||||
|
||||
// Record (A, B) and (B, A) as independent lists; each list's batch order
|
||||
// must match its own recording order.
|
||||
final int listAB = newList();
|
||||
GLStateManager.glNewList(listAB, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vaoA);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
final int listBA = newList();
|
||||
GLStateManager.glNewList(listBA, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(vaoA);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
final List<BatchedIndexedDrawCmd> ab = allBatchedCmds(listAB);
|
||||
final List<BatchedIndexedDrawCmd> ba = allBatchedCmds(listBA);
|
||||
// Placeholders sit in command-stream order (A-first vs B-first).
|
||||
// Batches must be ordered so that placeholder[0] points at batch[0].
|
||||
assertEquals(ab.get(0).getSharedVAO(), batchesOf(listAB).get(0).getSharedVAO(),
|
||||
"listAB: batch[0] must match the first-recorded layout");
|
||||
assertEquals(ab.get(1).getSharedVAO(), batchesOf(listAB).get(1).getSharedVAO(),
|
||||
"listAB: batch[1] must match the second-recorded layout");
|
||||
assertEquals(ba.get(0).getSharedVAO(), batchesOf(listBA).get(0).getSharedVAO(),
|
||||
"listBA: batch[0] must match the first-recorded layout");
|
||||
assertEquals(ba.get(1).getSharedVAO(), batchesOf(listBA).get(1).getSharedVAO(),
|
||||
"listBA: batch[1] must match the second-recorded layout");
|
||||
}
|
||||
|
||||
// === local helpers ===
|
||||
|
||||
private static void assertUintIndices(ByteBuffer ib, int... expected) {
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
assertEquals(expected[i], ib.getInt(i * 4), "index[" + i + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class IndexedDrawDeleteSafetyTest extends DisplayListTestFixture {
|
||||
|
||||
@Test
|
||||
void vaoEboMapSweep_clearsStaleEntryOnForeignVao() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
final IndexedDrawBatch batch = firstBatch(list);
|
||||
final int sharedEbo = batch.getSharedEBO();
|
||||
|
||||
final int foreignVao = newVao();
|
||||
GLStateManager.glBindVertexArray(foreignVao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
GLStateManager.glDeleteLists(list, 1);
|
||||
untrackList(list);
|
||||
|
||||
GLStateManager.glBindVertexArray(foreignVao);
|
||||
assertEquals(0, GLStateManager.getBoundEBO(), "vaoEboMap still references the freed sharedEBO — non-gen crash imminent");
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindBufferCycle_afterListDelete_isGlNoError() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
final int sharedEbo = firstBatch(list).getSharedEBO();
|
||||
final int foreignVao = newVao();
|
||||
GLStateManager.glBindVertexArray(foreignVao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
GLStateManager.glDeleteLists(list, 1);
|
||||
untrackList(list);
|
||||
|
||||
GLStateManager.glBindVertexArray(foreignVao);
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
final int prevEbo = GLStateManager.getBoundEBO();
|
||||
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
|
||||
final int err = GL11.glGetError();
|
||||
assertEquals(GL11.GL_NO_ERROR, err, () -> "bindBuffer(prevEbo=" + prevEbo + ") hit GL 0x" + Integer.toHexString(err) + " — cache carried a freed id through a VAO rebind");
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleVaos_sharingOneEbo_allClearedOnEboDelete() {
|
||||
// Two VAOs, same EBO. Deleting the EBO must clear vaoEboMap for both,
|
||||
// not just whichever VAO happens to be currently bound.
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vaoA = newVao();
|
||||
final int vaoB = newVao();
|
||||
|
||||
GLStateManager.glBindVertexArray(vaoA);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
GLStateManager.glDeleteBuffers(ebo);
|
||||
|
||||
GLStateManager.glBindVertexArray(vaoA);
|
||||
assertEquals(0, GLStateManager.getBoundEBO(), "vaoA still references freed EBO");
|
||||
GLStateManager.glBindVertexArray(vaoB);
|
||||
assertEquals(0, GLStateManager.getBoundEBO(), "vaoB still references freed EBO");
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void glDeleteProgram_zero_isNoOpAndLeavesActiveProgram() {
|
||||
final int prog = linkMinimalProgram();
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
|
||||
GLStateManager.glUseProgram(prog);
|
||||
assertEquals(prog, GLStateManager.getActiveProgram());
|
||||
|
||||
GLStateManager.glDeleteProgram(0);
|
||||
|
||||
assertEquals(prog, GLStateManager.getActiveProgram(), "glDeleteProgram(0) must be a silent no-op");
|
||||
|
||||
GLStateManager.glUseProgram(0);
|
||||
GLStateManager.glDeleteProgram(prog);
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
}
|
||||
|
||||
@Test
|
||||
void activeProgram_clearedOnDelete() {
|
||||
final int prog = linkMinimalProgram();
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
|
||||
GLStateManager.glUseProgram(prog);
|
||||
assertEquals(prog, GLStateManager.getActiveProgram());
|
||||
|
||||
GLStateManager.glDeleteProgram(prog);
|
||||
|
||||
assertEquals(0, GLStateManager.getActiveProgram(), "activeProgram not cleared when the active program was deleted");
|
||||
|
||||
GLStateManager.glUseProgram(0);
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
}
|
||||
|
||||
private static int linkMinimalProgram() {
|
||||
final int vert = GLStateManager.glCreateShader(GL20.GL_VERTEX_SHADER);
|
||||
GLStateManager.glShaderSource(vert, MIN_VERT_SRC);
|
||||
GLStateManager.glCompileShader(vert);
|
||||
if (GL20.glGetShaderi(vert, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
|
||||
throw new AssertionError("vertex shader failed to compile: " + GL20.glGetShaderInfoLog(vert, 4096));
|
||||
}
|
||||
|
||||
final int frag = GLStateManager.glCreateShader(GL20.GL_FRAGMENT_SHADER);
|
||||
GLStateManager.glShaderSource(frag, MIN_FRAG_SRC);
|
||||
GLStateManager.glCompileShader(frag);
|
||||
if (GL20.glGetShaderi(frag, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
|
||||
throw new AssertionError("fragment shader failed to compile: " + GL20.glGetShaderInfoLog(frag, 4096));
|
||||
}
|
||||
|
||||
final int prog = GLStateManager.glCreateProgram();
|
||||
GLStateManager.glAttachShader(prog, vert);
|
||||
GLStateManager.glAttachShader(prog, frag);
|
||||
GLStateManager.glLinkProgram(prog);
|
||||
if (GL20.glGetProgrami(prog, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) {
|
||||
throw new AssertionError("program failed to link: " + GL20.glGetProgramInfoLog(prog, 4096));
|
||||
}
|
||||
GLStateManager.glDetachShader(prog, vert);
|
||||
GLStateManager.glDetachShader(prog, frag);
|
||||
GLStateManager.glDeleteShader(vert);
|
||||
GLStateManager.glDeleteShader(frag);
|
||||
return prog;
|
||||
}
|
||||
|
||||
private static final String MIN_VERT_SRC = "#version 330 core\nvoid main(){gl_Position=vec4(0.0);}";
|
||||
private static final String MIN_FRAG_SRC = "#version 330 core\nout vec4 oC;void main(){oC=vec4(1.0);}";
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.AttribLayoutKey;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
class IndexedDrawReplayTest extends DisplayListTestFixture {
|
||||
@Test
|
||||
void compileAndExecute_firesLiveAndBakes() {
|
||||
// COMPILE_AND_EXECUTE must do two things: (1) run the draw immediately,
|
||||
// (2) record a placeholder so the list can replay it later. A samples-passed
|
||||
// query wrapping the record block confirms the live half; asserting the
|
||||
// placeholder post-endList confirms the baked half.
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
|
||||
GL11.glViewport(0, 0, 16, 16);
|
||||
GL11.glDisable(GL11.GL_DEPTH_TEST);
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
|
||||
final int query = GL15.glGenQueries();
|
||||
GL15.glBeginQuery(GL15.GL_SAMPLES_PASSED, query);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
GL15.glEndQuery(GL15.GL_SAMPLES_PASSED);
|
||||
final int samples = GL15.glGetQueryObjecti(query, GL15.GL_QUERY_RESULT);
|
||||
GL15.glDeleteQueries(query);
|
||||
|
||||
assertTrue(samples > 0, "COMPILE_AND_EXECUTE did not fire the live draw (samples=" + samples + ")");
|
||||
|
||||
final BatchedIndexedDrawCmd baked = firstBatchedCmd(list);
|
||||
assertNotNull(baked, "COMPILE_AND_EXECUTE did not bake a placeholder");
|
||||
assertEquals(6, baked.getIndexCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sharedVaoRetainsItsEbo() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int callerEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, callerEbo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
final BatchedIndexedDrawCmd batched = firstBatchedCmd(list);
|
||||
assertNotNull(batched);
|
||||
final IndexedDrawBatch batch = firstBatch(list);
|
||||
|
||||
GLStateManager.glBindVertexArray(batched.getSharedVAO());
|
||||
assertEquals(batch.getSharedEBO(), GLStateManager.getBoundEBO(),
|
||||
"batched VAO's EBO binding corrupted");
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replayAfterSourceMutationAndStateChange() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int callerEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 2, 3, 0);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, callerEbo, GL11.GL_TRIANGLES, 6, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
overwriteBufferWithGarbage(GL15.GL_ARRAY_BUFFER, vbo, 4 * POS_COLOR_STRIDE);
|
||||
overwriteBufferWithGarbage(GL15.GL_ELEMENT_ARRAY_BUFFER, callerEbo, 6 * 2);
|
||||
|
||||
// Bind unrelated state so the baked command has to restore its own bindings.
|
||||
final int distractorVao = newVao();
|
||||
GLStateManager.glBindVertexArray(distractorVao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
|
||||
compiledOf(list).render();
|
||||
|
||||
final int err = GL11.glGetError();
|
||||
assertEquals(GL11.GL_NO_ERROR, err,
|
||||
() -> "replay produced GL error 0x" + Integer.toHexString(err));
|
||||
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replayThenDelete_thenBindBufferCycle_isGlNoError() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
final int list = recordSingleIndexedDraw(vao, ebo, GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT);
|
||||
|
||||
compiledOf(list).render();
|
||||
|
||||
|
||||
final int sharedEbo = firstBatch(list).getSharedEBO();
|
||||
final int foreignVao = newVao();
|
||||
GLStateManager.glBindVertexArray(foreignVao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, sharedEbo);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
|
||||
GLStateManager.glDeleteLists(list, 1);
|
||||
untrackList(list);
|
||||
|
||||
GLStateManager.glBindVertexArray(foreignVao);
|
||||
assertEquals(0, GLStateManager.getBoundEBO(), "batched EBO id survived list delete in vaoEboMap — non-gen crash imminent");
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
final int prevEbo = GLStateManager.getBoundEBO();
|
||||
GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEbo);
|
||||
final int err = GL11.glGetError();
|
||||
assertEquals(GL11.GL_NO_ERROR, err, () -> "post-delete bindBuffer(" + prevEbo + ") hit GL 0x" + Integer.toHexString(err));
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_doesNotRestoreVAO_backToBackDraws() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(list);
|
||||
assertEquals(2, cmds.size());
|
||||
final int sharedVAO = cmds.getFirst().getSharedVAO();
|
||||
assertEquals(sharedVAO, cmds.get(1).getSharedVAO());
|
||||
|
||||
final int distractor = newVao();
|
||||
GLStateManager.glBindVertexArray(distractor);
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
|
||||
compiledOf(list).render();
|
||||
|
||||
assertEquals(sharedVAO, GLStateManager.getBoundVAO());
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void build_partialFailure_deletesEarlierBatchesAndLeavesPlaceholdersUnfilled() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
|
||||
final IndexedDrawCapture good = IndexedDrawCapture.create(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L, ebo);
|
||||
assertNotNull(good);
|
||||
|
||||
// Synthetic capture with a different layout that trips the >2GiB size guard.
|
||||
final AttribLayoutKey otherLayout = new AttribLayoutKey(
|
||||
new int[]{2}, new int[]{4}, new int[]{GL11.GL_FLOAT}, new boolean[]{false});
|
||||
final ByteBuffer tinyVtx = MemoryUtilities.memAlloc(16);
|
||||
final ByteBuffer tinyIdx = MemoryUtilities.memAlloc(16);
|
||||
final IndexedDrawCapture huge = new IndexedDrawCapture(
|
||||
otherLayout, GL11.GL_TRIANGLES,
|
||||
Integer.MAX_VALUE / 4, 1, tinyVtx, tinyIdx,
|
||||
new BatchedIndexedDrawCmd());
|
||||
|
||||
final IndexedDrawBatchBuilder builder = new IndexedDrawBatchBuilder();
|
||||
builder.add(good);
|
||||
builder.add(huge);
|
||||
|
||||
try {
|
||||
assertThrows(IllegalStateException.class, builder::build);
|
||||
assertEquals(0, good.placeholder.getSharedVAO());
|
||||
assertEquals(0, huge.placeholder.getSharedVAO());
|
||||
} finally {
|
||||
MemoryUtilities.memFree(tinyVtx);
|
||||
MemoryUtilities.memFree(tinyIdx);
|
||||
good.freeBuffers();
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void recorderFree_releasesUnfilledCaptureNativeBuffers() {
|
||||
final CommandRecorder rec = new CommandRecorder();
|
||||
final ByteBuffer vtx = MemoryUtilities.memAlloc(64);
|
||||
final ByteBuffer idx = MemoryUtilities.memAlloc(32);
|
||||
final IndexedDrawCapture cap = new IndexedDrawCapture(
|
||||
new AttribLayoutKey(new int[]{0}, new int[]{2}, new int[]{GL11.GL_FLOAT}, new boolean[]{false}),
|
||||
GL11.GL_TRIANGLES, 4, 6, vtx, idx, new BatchedIndexedDrawCmd());
|
||||
|
||||
rec.recordIndexedDrawCapture(cap);
|
||||
assertFalse(cap.isFreed());
|
||||
|
||||
rec.free();
|
||||
|
||||
assertTrue(cap.isFreed());
|
||||
cap.freeBuffers();
|
||||
}
|
||||
|
||||
@Test
|
||||
void execute_idempotentReplay_doesNotErrorOnSecondPass() {
|
||||
final int vbo = defaultQuadVbo();
|
||||
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
|
||||
final int vao = setupPosColorVao(vbo);
|
||||
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L);
|
||||
GLStateManager.glEndList();
|
||||
|
||||
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
|
||||
|
||||
compiledOf(list).render();
|
||||
compiledOf(list).render();
|
||||
|
||||
final int err = GL11.glGetError();
|
||||
assertEquals(GL11.GL_NO_ERROR, err,
|
||||
() -> "double replay produced GL error 0x" + Integer.toHexString(err));
|
||||
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording.commands;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
class AttribLayoutKeyTest {
|
||||
|
||||
private static int[] offsetsOf(AttribLayoutKey k) {
|
||||
final int[] out = new int[k.locations.length];
|
||||
for (int i = 0; i < out.length; i++) out[i] = k.offset(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Test
|
||||
void standardPosColorUvNormal_isAlignedAndPacked() {
|
||||
final AttribLayoutKey k = new AttribLayoutKey(
|
||||
new int[]{0, 1, 2, 4},
|
||||
new int[]{3, 4, 2, 3},
|
||||
new int[]{GL11.GL_FLOAT, GL11.GL_UNSIGNED_BYTE, GL11.GL_FLOAT, GL11.GL_BYTE},
|
||||
new boolean[]{false, true, false, true});
|
||||
assertArrayEquals(new int[]{0, 12, 16, 24}, offsetsOf(k));
|
||||
assertEquals(28, k.stride());
|
||||
}
|
||||
|
||||
@Test
|
||||
void posUvNormal3_roundsTailStrideTo24() {
|
||||
final AttribLayoutKey k = new AttribLayoutKey(
|
||||
new int[]{0, 2, 4},
|
||||
new int[]{3, 2, 3},
|
||||
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT, GL11.GL_BYTE},
|
||||
new boolean[]{false, false, true});
|
||||
assertArrayEquals(new int[]{0, 12, 20}, offsetsOf(k));
|
||||
assertEquals(24, k.stride());
|
||||
}
|
||||
|
||||
@Test
|
||||
void interiorHazardOrder_roundsMidAttribOffset() {
|
||||
final AttribLayoutKey k = new AttribLayoutKey(
|
||||
new int[]{0, 4, 2},
|
||||
new int[]{3, 3, 2},
|
||||
new int[]{GL11.GL_FLOAT, GL11.GL_BYTE, GL11.GL_FLOAT},
|
||||
new boolean[]{false, true, false});
|
||||
assertArrayEquals(new int[]{0, 12, 16}, offsetsOf(k));
|
||||
assertEquals(24, k.stride());
|
||||
}
|
||||
|
||||
@Test
|
||||
void allByteLayout_packsTight() {
|
||||
final AttribLayoutKey k = new AttribLayoutKey(
|
||||
new int[]{0, 1, 2},
|
||||
new int[]{3, 4, 2},
|
||||
new int[]{GL11.GL_BYTE, GL11.GL_UNSIGNED_BYTE, GL11.GL_BYTE},
|
||||
new boolean[]{true, true, false});
|
||||
assertArrayEquals(new int[]{0, 3, 7}, offsetsOf(k));
|
||||
assertEquals(9, k.stride());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortsFollowingByte_roundToEvenAndStrideToEven() {
|
||||
final AttribLayoutKey k = new AttribLayoutKey(
|
||||
new int[]{0, 1},
|
||||
new int[]{1, 2},
|
||||
new int[]{GL11.GL_BYTE, GL11.GL_SHORT},
|
||||
new boolean[]{true, false});
|
||||
assertArrayEquals(new int[]{0, 2}, offsetsOf(k));
|
||||
assertEquals(6, k.stride());
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalInputs_produceEqualKeys() {
|
||||
final AttribLayoutKey a = new AttribLayoutKey(
|
||||
new int[]{0, 2}, new int[]{3, 2},
|
||||
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT}, new boolean[]{false, false});
|
||||
final AttribLayoutKey b = new AttribLayoutKey(
|
||||
new int[]{0, 2}, new int[]{3, 2},
|
||||
new int[]{GL11.GL_FLOAT, GL11.GL_FLOAT}, new boolean[]{false, false});
|
||||
assertEquals(a, b);
|
||||
assertEquals(a.hashCode(), b.hashCode());
|
||||
assertEquals(a.stride(), b.stride());
|
||||
assertArrayEquals(offsetsOf(a), offsetsOf(b));
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentNormalized_producesDifferentKeys() {
|
||||
final AttribLayoutKey a = new AttribLayoutKey(
|
||||
new int[]{0}, new int[]{4}, new int[]{GL11.GL_UNSIGNED_BYTE}, new boolean[]{true});
|
||||
final AttribLayoutKey b = new AttribLayoutKey(
|
||||
new int[]{0}, new int[]{4}, new int[]{GL11.GL_UNSIGNED_BYTE}, new boolean[]{false});
|
||||
assertNotEquals(a, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.gtnewhorizons.angelica.glsm.recording.support;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
|
||||
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
|
||||
import com.gtnewhorizons.angelica.glsm.GLSMExtension;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.BatchedIndexedDrawCmd;
|
||||
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
@ExtendWith(GLSMExtension.class)
|
||||
public abstract class DisplayListTestFixture {
|
||||
|
||||
public static final float[] DEFAULT_QUAD_POSITIONS = {0f, 0f, 1f, 0f, 1f, 1f, 0f, 1f};
|
||||
public static final byte[][] DEFAULT_SOLID_COLORS = {
|
||||
{1, 1, 1, 1}, {2, 2, 2, 2}, {3, 3, 3, 3}, {4, 4, 4, 4}
|
||||
};
|
||||
|
||||
public static final int POS_COLOR_STRIDE = 12;
|
||||
|
||||
private final List<Integer> listsToCleanup = new ArrayList<>();
|
||||
private final List<Integer> buffersToCleanup = new ArrayList<>();
|
||||
private final List<Integer> vaosToCleanup = new ArrayList<>();
|
||||
|
||||
@AfterEach
|
||||
void cleanupFixtureResources() {
|
||||
for (int list : listsToCleanup) GLStateManager.glDeleteLists(list, 1);
|
||||
for (int buf : buffersToCleanup) {
|
||||
if (GL15.glIsBuffer(buf)) GL15.glDeleteBuffers(buf);
|
||||
}
|
||||
for (int vao : vaosToCleanup) {
|
||||
if (GL30.glIsVertexArray(vao)) GLStateManager.glDeleteVertexArrays(vao);
|
||||
}
|
||||
listsToCleanup.clear();
|
||||
buffersToCleanup.clear();
|
||||
vaosToCleanup.clear();
|
||||
}
|
||||
|
||||
protected final int newList() {
|
||||
final int id = GL11.glGenLists(1);
|
||||
listsToCleanup.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
protected final void untrackList(int list) {
|
||||
listsToCleanup.remove((Integer) list);
|
||||
}
|
||||
|
||||
protected final int newBuffer() {
|
||||
final int id = GL15.glGenBuffers();
|
||||
buffersToCleanup.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
protected final int newVao() {
|
||||
final int id = GL30.glGenVertexArrays();
|
||||
vaosToCleanup.add(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/** 2 floats pos + 4 bytes color, stride 12, one vertex per entry in {@code colors}. */
|
||||
protected final int uploadPosColorVbo(float[] positions, byte[][] colors) {
|
||||
if (positions.length / 2 != colors.length) {
|
||||
throw new IllegalArgumentException("positions/colors length mismatch");
|
||||
}
|
||||
final int n = colors.length;
|
||||
final int vbo = newBuffer();
|
||||
final ByteBuffer data = MemoryUtilities.memAlloc(n * POS_COLOR_STRIDE).order(ByteOrder.nativeOrder());
|
||||
for (int i = 0; i < n; i++) {
|
||||
data.putFloat(positions[i * 2]);
|
||||
data.putFloat(positions[i * 2 + 1]);
|
||||
data.put(colors[i][0]);
|
||||
data.put(colors[i][1]);
|
||||
data.put(colors[i][2]);
|
||||
data.put(colors[i][3]);
|
||||
}
|
||||
data.flip();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
MemoryUtilities.memFree(data);
|
||||
return vbo;
|
||||
}
|
||||
|
||||
/** Pos+color VBO populated with {@link #DEFAULT_QUAD_POSITIONS} / {@link #DEFAULT_SOLID_COLORS}. */
|
||||
protected final int defaultQuadVbo() {
|
||||
return uploadPosColorVbo(DEFAULT_QUAD_POSITIONS, DEFAULT_SOLID_COLORS);
|
||||
}
|
||||
|
||||
protected final int uploadEbo(int indexType, int... values) {
|
||||
final int bytesPer = switch (indexType) {
|
||||
case GL11.GL_UNSIGNED_BYTE -> 1;
|
||||
case GL11.GL_UNSIGNED_SHORT -> 2;
|
||||
case GL11.GL_UNSIGNED_INT -> 4;
|
||||
default -> throw new IllegalArgumentException("unsupported indexType 0x" + Integer.toHexString(indexType));
|
||||
};
|
||||
final int ebo = newBuffer();
|
||||
final ByteBuffer data = MemoryUtilities.memAlloc(values.length * bytesPer).order(ByteOrder.nativeOrder());
|
||||
for (int v : values) {
|
||||
switch (indexType) {
|
||||
case GL11.GL_UNSIGNED_BYTE -> data.put((byte) v);
|
||||
case GL11.GL_UNSIGNED_SHORT -> data.putShort((short) v);
|
||||
case GL11.GL_UNSIGNED_INT -> data.putInt(v);
|
||||
}
|
||||
}
|
||||
data.flip();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
MemoryUtilities.memFree(data);
|
||||
return ebo;
|
||||
}
|
||||
|
||||
/** VAO with loc0 = 2*FLOAT pos, loc1 = 4*UNSIGNED_BYTE norm color, stride 12. */
|
||||
protected final int setupPosColorVao(int vbo) {
|
||||
final int vao = newVao();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glVertexAttribPointer(0, 2, GL11.GL_FLOAT, false, POS_COLOR_STRIDE, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
GLStateManager.glVertexAttribPointer(1, 4, GL11.GL_UNSIGNED_BYTE, true, POS_COLOR_STRIDE, 8);
|
||||
GLStateManager.glEnableVertexAttribArray(1);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
return vao;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record one {@code glDrawElements} into a {@code GL_COMPILE} list with the given VAO and EBO
|
||||
* already-bound setup. Returns the list id (tracked for cleanup).
|
||||
*/
|
||||
protected final int recordSingleIndexedDraw(int vao, int ebo, int drawMode, int indexCount, int indexType) {
|
||||
final int list = newList();
|
||||
GLStateManager.glNewList(list, GL11.GL_COMPILE);
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
|
||||
GLStateManager.glDrawElements(drawMode, indexCount, indexType, 0L);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GLStateManager.glEndList();
|
||||
return list;
|
||||
}
|
||||
|
||||
protected static CompiledDisplayList compiledOf(int listId) {
|
||||
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(listId);
|
||||
assertNotNull(compiled, "compiled display list must exist for id " + listId);
|
||||
return compiled;
|
||||
}
|
||||
|
||||
protected static List<BatchedIndexedDrawCmd> allBatchedCmds(int listId) {
|
||||
final List<BatchedIndexedDrawCmd> out = new ArrayList<>();
|
||||
final Object[] complex = compiledOf(listId).getComplexObjects();
|
||||
if (complex == null) return out;
|
||||
for (Object o : complex) {
|
||||
if (o instanceof BatchedIndexedDrawCmd b) out.add(b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
protected static BatchedIndexedDrawCmd firstBatchedCmd(int listId) {
|
||||
final List<BatchedIndexedDrawCmd> cmds = allBatchedCmds(listId);
|
||||
return cmds.isEmpty() ? null : cmds.getFirst();
|
||||
}
|
||||
|
||||
protected static List<IndexedDrawBatch> batchesOf(int listId) {
|
||||
return compiledOf(listId).getIndexedBatches();
|
||||
}
|
||||
|
||||
protected static IndexedDrawBatch firstBatch(int listId) {
|
||||
return batchesOf(listId).getFirst();
|
||||
}
|
||||
|
||||
protected static ByteBuffer readBufferBytes(int target, int bufferId, int size) {
|
||||
final int prev;
|
||||
if (target == GL15.GL_ARRAY_BUFFER) prev = GLStateManager.getBoundVBO();
|
||||
else if (target == GL15.GL_ELEMENT_ARRAY_BUFFER) prev = GLStateManager.getBoundEBO();
|
||||
else prev = 0;
|
||||
|
||||
GLStateManager.glBindBuffer(target, bufferId);
|
||||
final ByteBuffer buf = MemoryUtilities.memAlloc(size).order(ByteOrder.nativeOrder());
|
||||
GL15.glGetBufferSubData(target, 0, buf);
|
||||
GLStateManager.glBindBuffer(target, prev);
|
||||
return buf;
|
||||
}
|
||||
|
||||
protected static void overwriteBufferWithGarbage(int target, int bufferId, int size) {
|
||||
final ByteBuffer garbage = MemoryUtilities.memAlloc(size);
|
||||
for (int i = 0; i < garbage.capacity(); i++) garbage.put(i, (byte) 0xAA);
|
||||
GLStateManager.glBindBuffer(target, bufferId);
|
||||
GL15.glBufferData(target, garbage, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glBindBuffer(target, 0);
|
||||
MemoryUtilities.memFree(garbage);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 187 B |
|
Before Width: | Height: | Size: 310 B |
|
Before Width: | Height: | Size: 192 B |
|
Before Width: | Height: | Size: 316 B |
@@ -1,8 +1,10 @@
|
||||
package com.gtnewhorizons.angelica.api;
|
||||
|
||||
import net.minecraft.item.ItemStack;
|
||||
|
||||
/**
|
||||
* To be used for modded items that don't have blocks. Baubles for example.
|
||||
*/
|
||||
public interface IDynamicLightProducer {
|
||||
int getLuminance();
|
||||
int getLuminance(ItemStack stack);
|
||||
}
|
||||
@@ -4,7 +4,14 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
|
||||
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.GRADIENT_PAYLOAD;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LENGTH;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_PAYLOAD;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
|
||||
@@ -213,12 +220,14 @@ public class BatchingFontRenderer {
|
||||
pushQuadIdx();
|
||||
}
|
||||
|
||||
private void pushTexRect(float x, float y, float w, float h, float itOff, int rgba, float uStart, float vStart, float uSz, float vSz) {
|
||||
private void pushTexRect(float x, float y, float w, float h, float itOff, int rgba, float uStart, float vStart, float uSz, float vSz, boolean flipV) {
|
||||
ensureCapacity();
|
||||
pushVtx(x + itOff, y, rgba, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
pushVtx(x - itOff, y + h, rgba, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
pushVtx(x + itOff + w, y, rgba, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
pushVtx(x - itOff + w, y + h, rgba, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
float vTop = flipV ? vStart + vSz : vStart;
|
||||
float vBot = flipV ? vStart : vStart + vSz;
|
||||
pushVtx(x + itOff, y, rgba, uStart, vTop, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
pushVtx(x - itOff, y + h, rgba, uStart, vBot, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
pushVtx(x + itOff + w, y, rgba, uStart + uSz, vTop, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
pushVtx(x - itOff + w, y + h, rgba, uStart + uSz, vBot, uStart, uStart + uSz, vStart, vStart + vSz);
|
||||
pushQuadIdx();
|
||||
}
|
||||
|
||||
@@ -442,6 +451,55 @@ public class BatchingFontRenderer {
|
||||
return (what >= fromInclusive) && (what <= toInclusive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count visible characters from {@code start} to {@code end}, skipping format codes.
|
||||
* Stops at §r, any color code (§0-f, §x), or any color effect (§q, §g).
|
||||
* Style codes (§k-o) and non-terminating effects (§z wave, §v dinnerbone) are skipped.
|
||||
*/
|
||||
private static int countVisibleChars(CharSequence str, int start, int end) {
|
||||
int count = 0;
|
||||
for (int i = start; i < end; i++) {
|
||||
char ch = str.charAt(i);
|
||||
if (ch == FORMATTING_CHAR && i + 1 < end) {
|
||||
char code = Character.toLowerCase(str.charAt(i + 1));
|
||||
if (code == 'r' || (code >= '0' && code <= '9') || (code >= 'a' && code <= 'f')
|
||||
|| code == 'x' || code == 'q' || code == 'g') {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return Math.max(count, 1);
|
||||
}
|
||||
|
||||
private static int hsvToRgb(float hue, float sat, float val) {
|
||||
int h = (int)(hue / 60f) % 6;
|
||||
float f = hue / 60f - h;
|
||||
float p = val * (1 - sat);
|
||||
float q = val * (1 - f * sat);
|
||||
float t = val * (1 - (1 - f) * sat);
|
||||
float r, g, b;
|
||||
switch (h) {
|
||||
case 0: r=val; g=t; b=p; break;
|
||||
case 1: r=q; g=val; b=p; break;
|
||||
case 2: r=p; g=val; b=t; break;
|
||||
case 3: r=p; g=q; b=val; break;
|
||||
case 4: r=t; g=p; b=val; break;
|
||||
default: r=val; g=p; b=q; break;
|
||||
}
|
||||
return ((int)(r*255) << 16) | ((int)(g*255) << 8) | (int)(b*255);
|
||||
}
|
||||
|
||||
private static final int RAINBOW_LUT_SIZE = 24;
|
||||
private static final int[] RAINBOW_LUT = new int[RAINBOW_LUT_SIZE];
|
||||
static {
|
||||
for (int i = 0; i < RAINBOW_LUT_SIZE; i++) {
|
||||
RAINBOW_LUT[i] = hsvToRgb(i * 15f, 1f, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean forceDefaults() {
|
||||
return this.bookMode || this.isSGA || this.isSplash;
|
||||
}
|
||||
@@ -466,7 +524,8 @@ public class BatchingFontRenderer {
|
||||
return forceDefaults() ? 1 : FontConfig.fontShadowOffset;
|
||||
}
|
||||
|
||||
private static final char FORMATTING_CHAR = 167; // §
|
||||
private static final double WAVE_TIME_SCALE = 5e-9;
|
||||
private static final float WAVE_FREQUENCY = 0.5f;
|
||||
|
||||
public float drawString(final float anchorX, final float anchorY, final int color, final boolean enableShadow,
|
||||
final boolean unicodeFlag, final CharSequence string, int stringOffset, int stringLength) {
|
||||
@@ -497,6 +556,15 @@ public class BatchingFontRenderer {
|
||||
boolean curBold = false;
|
||||
boolean curStrikethrough = false;
|
||||
boolean curUnderline = false;
|
||||
boolean curRainbow = false;
|
||||
boolean curWave = false;
|
||||
boolean curDinnerbone = false;
|
||||
boolean curGradient = false;
|
||||
int gradientStartRgb = 0, gradientEndRgb = 0;
|
||||
int gradientCharIndex = 0, gradientTotalChars = 0;
|
||||
float gradientStep = 0f;
|
||||
int rainbowCharIndex = 0;
|
||||
int visibleCharIndex = 0;
|
||||
|
||||
float glyphScaleY = getGlyphScaleY();
|
||||
float glyphScaleX = getGlyphScaleX();
|
||||
@@ -534,6 +602,16 @@ public class BatchingFontRenderer {
|
||||
strikethroughStartX = strikethroughEndX;
|
||||
}
|
||||
|
||||
if (fmtCode == 'x' && AngelicaConfig.enableRGBColors && charIdx + SECTION_X_PAYLOAD < stringEnd) {
|
||||
int rgb = ColorCodeUtils.parseHexPairs(string, charIdx + 1, 6);
|
||||
if (rgb != -1) {
|
||||
curRainbow = false;
|
||||
curGradient = false;
|
||||
curColor = (curColor & 0xFF000000) | (rgb & 0x00FFFFFF);
|
||||
curShadowColor = (curShadowColor & 0xFF000000) | ((rgb & 0xFCFCFC) >> 2);
|
||||
charIdx += SECTION_X_PAYLOAD;
|
||||
}
|
||||
} else {
|
||||
final boolean is09 = charInRange(fmtCode, '0', '9');
|
||||
final boolean isAF = charInRange(fmtCode, 'a', 'f');
|
||||
if (is09 || isAF) {
|
||||
@@ -542,6 +620,9 @@ public class BatchingFontRenderer {
|
||||
curStrikethrough = false;
|
||||
curUnderline = false;
|
||||
curItalic = false;
|
||||
curRainbow = false;
|
||||
curGradient = false;
|
||||
// wave/dinnerbone NOT reset — they're positional effects, independent of color
|
||||
|
||||
final int colorIdx = is09 ? (fmtCode - '0') : (fmtCode - 'a' + 10);
|
||||
final int rgb = this.colorCode[colorIdx];
|
||||
@@ -562,15 +643,41 @@ public class BatchingFontRenderer {
|
||||
underlineEndX = underlineStartX;
|
||||
} else if (fmtCode == 'o') {
|
||||
curItalic = true;
|
||||
} else if (fmtCode == 'q' && AngelicaConfig.enableRainbow) {
|
||||
curRainbow = true;
|
||||
curGradient = false;
|
||||
rainbowCharIndex = 0;
|
||||
} else if (fmtCode == 'z' && AngelicaConfig.enableWaveText) {
|
||||
curWave = !curWave;
|
||||
} else if (fmtCode == 'v' && AngelicaConfig.enableDinnerboneText) {
|
||||
curDinnerbone = !curDinnerbone;
|
||||
} else if (fmtCode == 'g' && AngelicaConfig.enableGradients && charIdx + GRADIENT_PAYLOAD < stringEnd) {
|
||||
int color1 = ColorCodeUtils.parseSectionXAt(string, charIdx + 1);
|
||||
int color2 = ColorCodeUtils.parseSectionXAt(string, charIdx + 1 + SECTION_X_LENGTH);
|
||||
if (color1 != -1 && color2 != -1) {
|
||||
curGradient = true;
|
||||
curRainbow = false;
|
||||
gradientStartRgb = color1;
|
||||
gradientEndRgb = color2;
|
||||
gradientCharIndex = 0;
|
||||
gradientTotalChars = countVisibleChars(string, charIdx + 1 + GRADIENT_PAYLOAD, stringEnd);
|
||||
gradientStep = gradientTotalChars > 1 ? 1f / (gradientTotalChars - 1) : 0f;
|
||||
charIdx += GRADIENT_PAYLOAD;
|
||||
}
|
||||
} else if (fmtCode == 'r') {
|
||||
curRandom = false;
|
||||
curBold = false;
|
||||
curStrikethrough = false;
|
||||
curUnderline = false;
|
||||
curItalic = false;
|
||||
curRainbow = false;
|
||||
curWave = false;
|
||||
curDinnerbone = false;
|
||||
curGradient = false;
|
||||
curColor = color;
|
||||
curShadowColor = shadowColor;
|
||||
}
|
||||
} // close else block for non-§x codes
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -597,12 +704,31 @@ public class BatchingFontRenderer {
|
||||
heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY * fontProvider.getYScaleMultiplier() / 2);
|
||||
float heightSouth = (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY * fontProvider.getYScaleMultiplier();
|
||||
|
||||
visibleCharIndex++;
|
||||
|
||||
// Check ASCII space, NBSP, NNBSP
|
||||
if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') {
|
||||
curX += 4 * this.getWhitespaceScale() + (curBold ? 1 : 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (curRainbow) {
|
||||
int rgbEffect = RAINBOW_LUT[rainbowCharIndex % RAINBOW_LUT_SIZE];
|
||||
curColor = (curColor & 0xFF000000) | rgbEffect;
|
||||
curShadowColor = (curShadowColor & 0xFF000000) | ((rgbEffect & 0xFCFCFC) >> 2);
|
||||
rainbowCharIndex++;
|
||||
}
|
||||
if (curGradient && gradientTotalChars > 0) {
|
||||
float t = Math.min(gradientCharIndex * gradientStep, 1f);
|
||||
int gr = (int)((gradientStartRgb >> 16 & 0xFF) * (1-t) + (gradientEndRgb >> 16 & 0xFF) * t);
|
||||
int gg = (int)((gradientStartRgb >> 8 & 0xFF) * (1-t) + (gradientEndRgb >> 8 & 0xFF) * t);
|
||||
int gb = (int)((gradientStartRgb & 0xFF) * (1-t) + (gradientEndRgb & 0xFF) * t);
|
||||
int rgbEffect = (gr << 16) | (gg << 8) | gb;
|
||||
curColor = (curColor & 0xFF000000) | rgbEffect;
|
||||
curShadowColor = (curShadowColor & 0xFF000000) | ((rgbEffect & 0xFCFCFC) >> 2);
|
||||
gradientCharIndex++;
|
||||
}
|
||||
|
||||
final float uStart = fontProvider.getUStart(chr);
|
||||
final float vStart = fontProvider.getVStart(chr);
|
||||
final float xAdvance = fontProvider.getXAdvance(chr) * glyphScaleX;
|
||||
@@ -616,23 +742,30 @@ public class BatchingFontRenderer {
|
||||
final ResourceLocation texture = fontProvider.getTexture(chr);
|
||||
final int idxId = idxWriterIndex;
|
||||
|
||||
// Wave: Y offset via sine wave
|
||||
float renderY = heightNorth;
|
||||
if (curWave) {
|
||||
float time = HUDCaching.renderingCacheOverride ? 0f : (float)((System.nanoTime() & 0xFFFFFFFFFFFFL) * WAVE_TIME_SCALE);
|
||||
renderY += (float) Math.sin(visibleCharIndex * WAVE_FREQUENCY + time) * AngelicaConfig.waveAmplitude;
|
||||
}
|
||||
|
||||
if (enableShadow) {
|
||||
for (int n = 1; n <= shadowCopies; n++) {
|
||||
final float shadowOffsetPart = shadowOffset * ((float) n / shadowCopies);
|
||||
pushTexRect(curX + shadowOffsetPart, heightNorth + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz);
|
||||
pushTexRect(curX + shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
|
||||
if (curBold) {
|
||||
pushTexRect(curX + 2.0f * shadowOffsetPart, heightNorth + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz);
|
||||
pushTexRect(curX + 2.0f * shadowOffsetPart, renderY + shadowOffsetPart, glyphW - 1.0f, heightSouth, itOff, curShadowColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pushTexRect(curX, heightNorth, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz);
|
||||
pushTexRect(curX, renderY, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
|
||||
if (curBold) {
|
||||
for (int n = 1; n <= boldCopies; n++) {
|
||||
final float shadowOffsetPart = shadowOffset * ((float) n / boldCopies);
|
||||
pushTexRect(curX + shadowOffsetPart, heightNorth, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz);
|
||||
pushTexRect(curX + shadowOffsetPart, renderY, glyphW - 1.0f, heightSouth, itOff, curColor, uStart, vStart, uSz, vSz, curDinnerbone);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,6 +786,19 @@ public class BatchingFontRenderer {
|
||||
if (bookMode) { curX = (int) curX; }
|
||||
underlineEndX = curX;
|
||||
strikethroughEndX = curX;
|
||||
|
||||
if ((curRainbow || curGradient) && curUnderline && underlineStartX != underlineEndX) {
|
||||
final int ulIdx = idxWriterIndex;
|
||||
pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, curColor);
|
||||
pushDrawCmd(ulIdx, 6, null, false);
|
||||
underlineStartX = underlineEndX;
|
||||
}
|
||||
if ((curRainbow || curGradient) && curStrikethrough && strikethroughStartX != strikethroughEndX) {
|
||||
final int stIdx = idxWriterIndex;
|
||||
pushUntexRect(strikethroughStartX, strikethroughY, strikethroughEndX - strikethroughStartX, glyphScaleY, curColor);
|
||||
pushDrawCmd(stIdx, 6, null, false);
|
||||
strikethroughStartX = strikethroughEndX;
|
||||
}
|
||||
}
|
||||
|
||||
if (curUnderline && underlineStartX != underlineEndX) {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.gtnewhorizons.angelica.client.font;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
|
||||
/**
|
||||
* Utility for converting {@code &}-based shorthand into {@code §}-based
|
||||
* format codes understood by the batched font renderer.
|
||||
* <p>
|
||||
* {@code &#RRGGBB} -> {@code §x§R§R§G§G§B§B} (RGB color)<br>
|
||||
* {@code &g&#RRGGBB&#RRGGBB} -> {@code §g§x§R§R§G§G§B§B§x§R§R§G§G§B§B} (gradient)<br>
|
||||
* {@code &c}, {@code &l}, {@code &r}, {@code &q}, etc. -> {@code §c}, {@code §l}, {@code §r}, {@code §q}
|
||||
* <p>
|
||||
* Effect codes use low-collision letters to avoid false positives on natural text:
|
||||
* rainbow = {@code &q}, wave = {@code &z}, dinnerbone = {@code &v}, gradient = {@code &g} (only with &#-prefixed colors).
|
||||
*/
|
||||
public final class ColorCodeUtils {
|
||||
|
||||
private ColorCodeUtils() {}
|
||||
|
||||
public static final char FORMATTING_CHAR = '§';
|
||||
|
||||
public static final int SECTION_X_PAYLOAD = 12;
|
||||
public static final int SECTION_X_LENGTH = 14;
|
||||
public static final int GRADIENT_PAYLOAD = 28;
|
||||
public static final int GRADIENT_LENGTH = 30;
|
||||
|
||||
/** Valid single {@code &} codes. {@code g} is excluded; {@code &g} only converts as part of a gradient. */
|
||||
public static final String VALID_SINGLE_CODES = "0123456789abcdefklmnorqzv";
|
||||
|
||||
private static final String[] SECTION_PREFIX = new String[128];
|
||||
static {
|
||||
for (int i = 0; i < VALID_SINGLE_CODES.length(); i++) {
|
||||
char c = VALID_SINGLE_CODES.charAt(i);
|
||||
SECTION_PREFIX[c] = String.valueOf(FORMATTING_CHAR) + c;
|
||||
}
|
||||
SECTION_PREFIX['x'] = String.valueOf(FORMATTING_CHAR) + 'x';
|
||||
SECTION_PREFIX['g'] = String.valueOf(FORMATTING_CHAR) + 'g';
|
||||
}
|
||||
|
||||
public static String sectionPrefix(char code) {
|
||||
char c = (code < 128) ? Character.toLowerCase(code) : code;
|
||||
return (c < 128) ? SECTION_PREFIX[c] : null;
|
||||
}
|
||||
|
||||
public static int parseHexPairs(CharSequence str, int start, int count) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int p = start + i * 2;
|
||||
if (str.charAt(p) != FORMATTING_CHAR) return -1;
|
||||
int digit = Character.digit(str.charAt(p + 1), 16);
|
||||
if (digit == -1) return -1;
|
||||
result = (result << 4) | digit;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int parseSectionXAt(CharSequence str, int start) {
|
||||
if (start + SECTION_X_LENGTH > str.length()) return -1;
|
||||
if (str.charAt(start) != FORMATTING_CHAR) return -1;
|
||||
if (Character.toLowerCase(str.charAt(start + 1)) != 'x') return -1;
|
||||
return parseHexPairs(str, start + 2, 6);
|
||||
}
|
||||
|
||||
public static boolean isValidSectionX(CharSequence str, int start) {
|
||||
return parseSectionXAt(str, start) != -1;
|
||||
}
|
||||
|
||||
private static String lastConversionInput;
|
||||
private static String lastConversionOutput;
|
||||
|
||||
public static String convertAmpersandToSectionX(String text) {
|
||||
if (text == null || !AngelicaConfig.enableAmpersandConversion) return text;
|
||||
|
||||
if (text == lastConversionInput) return lastConversionOutput;
|
||||
|
||||
final String out = convertImpl(text);
|
||||
lastConversionInput = text;
|
||||
lastConversionOutput = out;
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String convertImpl(String text) {
|
||||
int idx = text.indexOf('&');
|
||||
if (idx == -1) return text;
|
||||
|
||||
final int len = text.length();
|
||||
StringBuilder sb = null;
|
||||
int last = 0;
|
||||
while (idx != -1 && idx + 1 < len) {
|
||||
if (text.charAt(idx + 1) == '#' && idx + 7 < len) {
|
||||
boolean validHex = true;
|
||||
for (int i = 2; i <= 7; i++) {
|
||||
if (Character.digit(text.charAt(idx + i), 16) == -1) {
|
||||
validHex = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (validHex) {
|
||||
if (sb == null) sb = new StringBuilder(len + 16);
|
||||
sb.append(text, last, idx);
|
||||
sb.append(FORMATTING_CHAR).append('x');
|
||||
for (int i = 2; i <= 7; i++) {
|
||||
sb.append(FORMATTING_CHAR).append(text.charAt(idx + i));
|
||||
}
|
||||
last = idx + 8;
|
||||
idx = text.indexOf('&', last);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (Character.toLowerCase(text.charAt(idx + 1)) == 'g' && idx + 17 < len
|
||||
&& text.charAt(idx + 2) == '&' && text.charAt(idx + 3) == '#'
|
||||
&& text.charAt(idx + 10) == '&' && text.charAt(idx + 11) == '#') {
|
||||
boolean valid1 = true, valid2 = true;
|
||||
for (int i = 4; i <= 9; i++) {
|
||||
if (Character.digit(text.charAt(idx + i), 16) == -1) { valid1 = false; break; }
|
||||
}
|
||||
if (valid1) {
|
||||
for (int i = 12; i <= 17; i++) {
|
||||
if (Character.digit(text.charAt(idx + i), 16) == -1) { valid2 = false; break; }
|
||||
}
|
||||
}
|
||||
if (valid1 && valid2) {
|
||||
if (sb == null) sb = new StringBuilder(len + 16);
|
||||
sb.append(text, last, idx);
|
||||
sb.append(FORMATTING_CHAR).append('g');
|
||||
last = idx + 2;
|
||||
idx = text.indexOf('&', last);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
char code = Character.toLowerCase(text.charAt(idx + 1));
|
||||
if (VALID_SINGLE_CODES.indexOf(code) != -1) {
|
||||
if (sb == null) sb = new StringBuilder(len + 16);
|
||||
sb.append(text, last, idx);
|
||||
sb.append(FORMATTING_CHAR).append(text.charAt(idx + 1));
|
||||
last = idx + 2;
|
||||
}
|
||||
idx = text.indexOf('&', idx + 1);
|
||||
}
|
||||
if (sb == null) return text;
|
||||
sb.append(text, last, len);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.gtnewhorizons.angelica.compat;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.util.font.FontRendering;
|
||||
import com.gtnewhorizons.angelica.client.font.ColorCodeUtils;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
|
||||
public class GTNHLibCompat {
|
||||
|
||||
public static final boolean HAS_TEXT_PREPROCESSOR;
|
||||
|
||||
static {
|
||||
boolean found = false;
|
||||
try {
|
||||
FontRendering.class.getMethod("setTextPreprocessor", Function.class);
|
||||
found = true;
|
||||
} catch (NoSuchMethodException ignored) {}
|
||||
HAS_TEXT_PREPROCESSOR = found;
|
||||
}
|
||||
|
||||
/** Only call when {@link #HAS_TEXT_PREPROCESSOR} is true. */
|
||||
public static void registerPreprocessor() {
|
||||
FontRendering.setTextPreprocessor(new FontRendering.TextPreprocessor() {
|
||||
|
||||
@Override
|
||||
public String apply(String text) {
|
||||
return AngelicaConfig.enableAmpersandConversion
|
||||
? ColorCodeUtils.convertAmpersandToSectionX(text)
|
||||
: text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handlesAmpCodes() {
|
||||
return AngelicaConfig.enableAmpersandConversion;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public class ModStatus {
|
||||
public static boolean isThaumcraftLoaded;
|
||||
public static boolean isThaumicHorizonsLoaded;
|
||||
public static boolean isBaublesLoaded;
|
||||
public static boolean isCosmeticArmorReworkedLoaded;
|
||||
public static boolean isFluidLoggedLoaded;
|
||||
public static boolean isBOPLoaded;
|
||||
|
||||
@@ -43,6 +44,7 @@ public class ModStatus {
|
||||
isThaumcraftLoaded = Loader.isModLoaded("Thaumcraft");
|
||||
isThaumicHorizonsLoaded = Loader.isModLoaded("ThaumicHorizons");
|
||||
isBaublesLoaded = Loader.isModLoaded("Baubles");
|
||||
isCosmeticArmorReworkedLoaded = Loader.isModLoaded("cosmeticarmorreworked");
|
||||
isFluidLoggedLoaded = Loader.isModLoaded("fluidlogged");
|
||||
|
||||
isHoloInventoryLoaded = Loader.isModLoaded("holoinventory");
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.gtnewhorizons.angelica.compat.bettercrashes;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ITessellatorData;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IRenderGlobalExt;
|
||||
import cpw.mods.fml.common.Optional;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import vfyjxf.bettercrashes.utils.StateManager;
|
||||
|
||||
@Optional.Interface(modid = "angelica", iface = "vfyjxf.bettercrashes.utils.StateManager.IResettable")
|
||||
@@ -25,6 +27,7 @@ public class BetterCrashesCompat implements StateManager.IResettable {
|
||||
|
||||
@Override
|
||||
public void resetState() {
|
||||
((ITessellatorData) Tessellator.instance).setDrawing(false);
|
||||
AngelicaMod.LOGGER.info("Reloading SodiumRenderer");
|
||||
((IRenderGlobalExt) Minecraft.getMinecraft().renderGlobal).angelica$reload();
|
||||
AngelicaMod.LOGGER.info("Resetting GLStateManager");
|
||||
|
||||
@@ -63,6 +63,11 @@ public class AngelicaConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableVAO;
|
||||
|
||||
@Config.Comment("Enables DSA (Direct State Access) for faster bindings. Disable if you notice terrible performance.")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableDSA;
|
||||
|
||||
@Config.Comment("Enable NotFine features")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
@@ -107,6 +112,35 @@ public class AngelicaConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean enableFontRenderer;
|
||||
|
||||
@Config.Comment("Enable full RGB color support (16.7M colors) using &#RRGGBB syntax in text")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean enableRGBColors;
|
||||
|
||||
@Config.Comment("Enable gradient text (&g&#start&#end)")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean enableGradients;
|
||||
|
||||
@Config.Comment("Enable rainbow cycling text (&q)")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean enableRainbow;
|
||||
|
||||
@Config.Comment("Enable wave/bounce animated text (&z)")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean enableWaveText;
|
||||
|
||||
@Config.Comment("Enable upside-down text (&v)")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean enableDinnerboneText;
|
||||
|
||||
@Config.Comment("Wave text amplitude (how far characters bounce)")
|
||||
@Config.DefaultFloat(2.0f)
|
||||
@Config.RangeFloat(min = 1.0f, max = 8.0f)
|
||||
public static float waveAmplitude;
|
||||
|
||||
@Config.Comment("Convert &-prefix format codes (&#RRGGBB, &c, &l, etc.) at render time")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean enableAmpersandConversion;
|
||||
|
||||
@Config.Comment("Enable Dynamic Lights")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
@@ -297,4 +331,9 @@ public class AngelicaConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean disableGLVersionPinning;
|
||||
|
||||
@Config.Comment("Disables GL Error checks. Always set to false in dev env or if LWJGL debug is on. Improves performance.")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean disableErrorChecks;
|
||||
|
||||
}
|
||||
@@ -45,23 +45,9 @@ public class CompatConfig {
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean fixSecurityCraft;
|
||||
|
||||
@Config.Comment("Apply tweaks to NTM:Space")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean tweakNTMSpace;
|
||||
|
||||
@Config.Comment("Apply fixes to NTM:Space")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public static boolean fixNTMSpace;
|
||||
|
||||
@Config.Comment("Disables rendering of the planet below the player at high altitudes [Requires tweakNTMSpace]")
|
||||
@Config.DefaultEnum("ShadersOnly")
|
||||
public static altitudePlanetRendererState NTMSpace_disableAltitudePlanetRenderer;
|
||||
public enum altitudePlanetRendererState {
|
||||
Always,
|
||||
ShadersOnly,
|
||||
Never
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.dynamiclights;
|
||||
|
||||
import baubles.common.lib.PlayerHandler;
|
||||
import lain.mods.cos.CosmeticArmorReworked;
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
|
||||
import com.gtnewhorizon.gtnhlib.util.CoordinatePacker;
|
||||
@@ -180,6 +181,19 @@ public class DynamicLights {
|
||||
this.lightSourcesLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all tracked light sources, to call when unloading the client world
|
||||
*/
|
||||
public void removeAllLightSources() {
|
||||
this.lightSourcesLock.writeLock().lock();
|
||||
|
||||
this.dynamicLightSources.clear();
|
||||
this.lightSourceCount = 0;
|
||||
rebuildLightSourceArray();
|
||||
|
||||
this.lightSourcesLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes light sources if the filter matches.
|
||||
*
|
||||
@@ -559,13 +573,13 @@ public class DynamicLights {
|
||||
*/
|
||||
public static int getLuminanceFromItemStack(@NotNull ItemStack stack) {
|
||||
final Item item = stack.getItem();
|
||||
if (item instanceof ItemBlock itemBlock) {
|
||||
if (item instanceof IDynamicLightProducer lightProducer) {
|
||||
return lightProducer.getLuminance(stack);
|
||||
} else if (item instanceof ItemBlock itemBlock) {
|
||||
final Block block = itemBlock.field_150939_a;
|
||||
if (block != null) {
|
||||
return block.getLightValue();
|
||||
}
|
||||
} else if (item instanceof IDynamicLightProducer lightProducer){
|
||||
return lightProducer.getLuminance();
|
||||
}
|
||||
|
||||
if (item == Items.lava_bucket) return Blocks.lava.getLightValue();
|
||||
@@ -625,6 +639,18 @@ public class DynamicLights {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ModStatus.isCosmeticArmorReworkedLoaded) {
|
||||
final var cosArmor = CosmeticArmorReworked.invMan.getCosArmorInventoryClient(player.getUniqueID());
|
||||
if (cosArmor != null) {
|
||||
for (int i = 0; i < cosArmor.getSizeInventory(); i++) {
|
||||
final var stack = cosArmor.getStackInSlot(i);
|
||||
if (stack != null) {
|
||||
luminance = Math.max(luminance, getLuminanceFromItemStack(stack));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return luminance;
|
||||
|
||||
@@ -76,7 +76,7 @@ public class HUDCaching {
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
GLStateManager.enableTexture();
|
||||
|
||||
if (!AngelicaConfig.hudCachingActive || !GLStateManager.isFramebufferEnabled()) {
|
||||
if (!AngelicaConfig.hudCachingActive) {
|
||||
ingame.renderGameOverlay(partialTicks, hasScreen, mouseX, mouseY);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,24 +16,27 @@ import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
|
||||
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
|
||||
import net.coderbot.iris.samplers.IrisSamplers;
|
||||
import net.coderbot.iris.texture.pbr.PBRTextureManager;
|
||||
import net.coderbot.iris.vertices.ImmediateState;
|
||||
|
||||
public class IrisGLSMBridge {
|
||||
|
||||
|
||||
private static Runnable alphaFuncListener = null;
|
||||
private static Runnable alphaTestListener = null;
|
||||
private static Runnable blendFuncListener = null;
|
||||
private static Runnable fogModeListener = null;
|
||||
private static Runnable fogStartListener = null;
|
||||
private static Runnable fogEndListener = null;
|
||||
private static Runnable fogDensityListener = null;
|
||||
|
||||
|
||||
static {
|
||||
StateUpdateNotifiers.alphaFuncNotifier = listener -> alphaFuncListener = listener;
|
||||
StateUpdateNotifiers.alphaTestNotifier = listener -> alphaTestListener = listener;
|
||||
StateUpdateNotifiers.blendFuncNotifier = listener -> blendFuncListener = listener;
|
||||
StateUpdateNotifiers.fogModeNotifier = listener -> fogModeListener = listener;
|
||||
StateUpdateNotifiers.fogStartNotifier = listener -> fogStartListener = listener;
|
||||
StateUpdateNotifiers.fogEndNotifier = listener -> fogEndListener = listener;
|
||||
StateUpdateNotifiers.fogDensityNotifier = listener -> fogDensityListener = listener;
|
||||
}
|
||||
|
||||
|
||||
public static void register() {
|
||||
IrisSamplers.initRenderer();
|
||||
GLSMHooks.blendHandler = new DeferredBlendHandler() {
|
||||
@@ -91,13 +94,20 @@ public class IrisGLSMBridge {
|
||||
DepthColorStorage.deferColorMask(r, g, b, a);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
GLSMHooks.ALPHA_STATE_CHANGE.addListener(event -> {
|
||||
if (Iris.enabled) {
|
||||
if (alphaFuncListener != null) alphaFuncListener.run();
|
||||
if (alphaTestListener != null) alphaTestListener.run();
|
||||
}
|
||||
});
|
||||
|
||||
GLSMHooks.BLEND_FUNC_CHANGE.addListener(event -> {
|
||||
if (Iris.enabled) {
|
||||
if (blendFuncListener != null) blendFuncListener.run();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
GLSMHooks.FOG_STATE_CHANGE.addListener(event -> {
|
||||
if (Iris.enabled) {
|
||||
if (fogModeListener != null) fogModeListener.run();
|
||||
@@ -106,7 +116,7 @@ public class IrisGLSMBridge {
|
||||
if (fogDensityListener != null) fogDensityListener.run();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
GLSMHooks.TEXTURE_BIND.addListener(event -> {
|
||||
if (Iris.enabled) {
|
||||
TextureTracker.INSTANCE.onBindTexture();
|
||||
@@ -138,20 +148,37 @@ public class IrisGLSMBridge {
|
||||
}
|
||||
});
|
||||
|
||||
GLSMHooks.PROGRAM_CHANGE.addListener(event -> ProgramUniforms.clearActiveUniforms());
|
||||
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
|
||||
if (!Iris.enabled) return;
|
||||
if (event.postBind) return;
|
||||
ProgramUniforms.clearActiveUniforms();
|
||||
});
|
||||
|
||||
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
|
||||
if (!Iris.enabled) return;
|
||||
if (event.postBind) return;
|
||||
|
||||
final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
|
||||
if (!(pipeline instanceof DeferredWorldRenderingPipeline drp)) return;
|
||||
if (!drp.shouldOverrideShaders()) return;
|
||||
if (drp.getActivePassProgramId() == -1) return;
|
||||
|
||||
if (!ImmediateState.isRenderingLevel || DepthColorStorage.isOwnedProgram(event.newProgram)) {
|
||||
|
||||
if (DepthColorStorage.isOwnedProgram(event.newProgram)) {
|
||||
DepthColorStorage.unlockDepthColor();
|
||||
} else {
|
||||
drp.onModProgramOverride();
|
||||
}
|
||||
});
|
||||
|
||||
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
|
||||
if (!Iris.enabled) return;
|
||||
if (!event.postBind) return;
|
||||
WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
|
||||
if (pipeline instanceof DeferredWorldRenderingPipeline drp) {
|
||||
DeferredWorldRenderingPipeline.Pass activePass = drp.getActivePassProgram();
|
||||
if (activePass != null && activePass.getProgram() != null && activePass.getProgram().getProgramId() == event.newProgram) {
|
||||
activePass.getProgram().getUniforms().update();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -105,9 +105,6 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
|
||||
new DependencyVerifier.Check(
|
||||
"/it/unimi/dsi/fastutil/ints/Int2ObjectMap.class",
|
||||
"Missing dependency: Angelica requires GTNHLib! Download: https://modrinth.com/mod/gtnhlib"),
|
||||
new DependencyVerifier.Check(
|
||||
"/com/gtnewhorizon/gtnhlib/client/renderer/PrimitiveExtractor.class",
|
||||
"GTNHLib is outdated: Angelica requires GTNHLib 0.8.21 or newer! Download: https://modrinth.com/mod/gtnhlib"),
|
||||
new DependencyVerifier.Check(
|
||||
"/xyz/wagyourtail/jvmdg/exc/MissingStubError.class",
|
||||
"GTNHLib is outdated: Angelica requires GTNHLib 0.9.0 or newer! Download: https://modrinth.com/mod/gtnhlib")
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.gtnewhorizons.angelica.loading.fml.compat.handlers.ThaumcraftCompatHa
|
||||
import com.gtnewhorizons.angelica.loading.fml.compat.handlers.ThaumicHorizonsCompatHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -45,20 +44,19 @@ public enum CompatHandlers {
|
||||
|
||||
/**
|
||||
* Returns extra transformers as well as the main transformer.
|
||||
* Returns an empty list if no handlers registered.
|
||||
*/
|
||||
public static List<String> getTransformers() {
|
||||
final List<ICompatHandler> handlers = getHandlers();
|
||||
if (handlers.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> transformers = new ArrayList<>();
|
||||
List<String> transformers = new ArrayList<>();
|
||||
for (ICompatHandler handler : handlers) {
|
||||
if (handler.extraTransformers() != null) {
|
||||
transformers.addAll(handler.extraTransformers());
|
||||
}
|
||||
}
|
||||
transformers.add("com.gtnewhorizons.angelica.loading.fml.transformers.GenericCompatTransformer");
|
||||
if (!handlers.isEmpty()) {
|
||||
transformers.add("com.gtnewhorizons.angelica.loading.fml.transformers.GenericCompatTransformer");
|
||||
}
|
||||
transformers.add("com.gtnewhorizons.angelica.loading.fml.transformers.IsbrhTessellatorAbuseClassTransformer");
|
||||
return transformers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.gtnewhorizons.angelica.loading.fml.transformers;
|
||||
|
||||
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
|
||||
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
|
||||
import net.minecraft.launchwrapper.IClassTransformer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
public class IsbrhTessellatorAbuseClassTransformer implements IClassTransformer {
|
||||
private static final Logger LOGGER = LogManager.getLogger("IsbrhTessellatorAbuse");
|
||||
private static final String ISBRH = "cpw/mods/fml/client/registry/ISimpleBlockRenderingHandler";
|
||||
private static final String TESSELLATOR = "net/minecraft/client/renderer/Tessellator";
|
||||
private static final String RENDER_WORLD_BLOCK_DESC = "(Lnet/minecraft/world/IBlockAccess;IIILnet/minecraft/block/Block;ILnet/minecraft/client/renderer/RenderBlocks;)Z";
|
||||
|
||||
private final String draw;
|
||||
private final String startDrawingQuads;
|
||||
private final String startDrawing;
|
||||
|
||||
public IsbrhTessellatorAbuseClassTransformer() {
|
||||
this(AngelicaClientTweaker.isObfEnv());
|
||||
}
|
||||
|
||||
public IsbrhTessellatorAbuseClassTransformer(boolean isObf) {
|
||||
this.draw = isObf ? "func_78381_a" : "draw";
|
||||
this.startDrawingQuads = isObf ? "func_78382_b" : "startDrawingQuads";
|
||||
this.startDrawing = isObf ? "func_78371_b" : "startDrawing";
|
||||
}
|
||||
|
||||
public boolean shouldTransform(byte[] classBytes) {
|
||||
final ClassReader cr = new ClassReader(classBytes);
|
||||
for (String iface : cr.getInterfaces()) {
|
||||
if (ISBRH.equals(iface)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean transformClassNode(String transformedName, ClassNode cn) {
|
||||
boolean changed = false;
|
||||
for (MethodNode mn : cn.methods) {
|
||||
if (!"renderWorldBlock".equals(mn.name) || !RENDER_WORLD_BLOCK_DESC.equals(mn.desc)) continue;
|
||||
|
||||
for (AbstractInsnNode insn : mn.instructions.toArray()) {
|
||||
if (!(insn instanceof MethodInsnNode min) || min.getOpcode() != Opcodes.INVOKEVIRTUAL || !TESSELLATOR.equals(min.owner)) continue;
|
||||
|
||||
final int popOp;
|
||||
final boolean pushIntResult;
|
||||
if (min.name.equals(draw) && "()I".equals(min.desc)) {
|
||||
popOp = Opcodes.POP;
|
||||
pushIntResult = true;
|
||||
} else if (min.name.equals(startDrawingQuads) && "()V".equals(min.desc)) {
|
||||
popOp = Opcodes.POP;
|
||||
pushIntResult = false;
|
||||
} else if (min.name.equals(startDrawing) && "(I)V".equals(min.desc)) {
|
||||
popOp = Opcodes.POP2;
|
||||
pushIntResult = false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
mn.instructions.insertBefore(insn, new InsnNode(popOp));
|
||||
if (pushIntResult) mn.instructions.insertBefore(insn, new InsnNode(Opcodes.ICONST_0));
|
||||
mn.instructions.remove(insn);
|
||||
LOGGER.info("Stripped Tessellator.{} from {}#renderWorldBlock", min.name, cn.name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] transform(String name, String transformedName, byte[] basicClass) {
|
||||
if (basicClass == null || !shouldTransform(basicClass)) return basicClass;
|
||||
|
||||
final ClassReader cr = new ClassReader(basicClass);
|
||||
final ClassNode cn = new ClassNode();
|
||||
cr.accept(cn, 0);
|
||||
if (!transformClassNode(transformedName, cn)) return basicClass;
|
||||
|
||||
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cn.accept(cw);
|
||||
final byte[] bytes = cw.toByteArray();
|
||||
AngelicaClassDump.dumpClass(transformedName, basicClass, bytes, this);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.loading.rfb;
|
||||
|
||||
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBAngelicaRedirector;
|
||||
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBCeleritasBlockTransformer;
|
||||
import com.gtnewhorizons.angelica.loading.rfb.transformers.RFBIsbrhTessellatorAbuseTransformer;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.PluginContext;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.RetroFuturaBootstrap;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
|
||||
@@ -28,7 +29,8 @@ public class AngelicaRfbPlugin implements RfbPlugin {
|
||||
final boolean isObf = RetroFuturaBootstrap.API.launchClassLoader().findClassMetadata("net.minecraft.world.World") == null;
|
||||
return new RfbClassTransformer[] {
|
||||
new RFBAngelicaRedirector(),
|
||||
new RFBCeleritasBlockTransformer(isObf)
|
||||
new RFBCeleritasBlockTransformer(isObf),
|
||||
new RFBIsbrhTessellatorAbuseTransformer(isObf)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.gtnewhorizons.angelica.loading.rfb.transformers;
|
||||
|
||||
import com.gtnewhorizons.angelica.loading.fml.transformers.IsbrhTessellatorAbuseClassTransformer;
|
||||
import com.gtnewhorizons.angelica.loading.shared.AngelicaClassDump;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.ClassNodeHandle;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.ExtensibleClassLoader;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.api.RfbClassTransformer;
|
||||
import org.intellij.lang.annotations.Pattern;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/** RfbClassTransformer wrapper for {@link IsbrhTessellatorAbuseClassTransformer} */
|
||||
public class RFBIsbrhTessellatorAbuseTransformer implements RfbClassTransformer {
|
||||
private final IsbrhTessellatorAbuseClassTransformer inner;
|
||||
|
||||
public RFBIsbrhTessellatorAbuseTransformer(boolean isObf) {
|
||||
inner = new IsbrhTessellatorAbuseClassTransformer(isObf);
|
||||
}
|
||||
|
||||
@Pattern("[a-z0-9-]+")
|
||||
@Override
|
||||
public @NotNull String id() {
|
||||
return "isbrh-tessellator-abuse";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String @Nullable [] sortAfter() {
|
||||
return new String[] {"*", "mixin:mixin"};
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String @Nullable [] sortBefore() {
|
||||
return new String[] {"lwjgl3ify:redirect"};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldTransformClass(@NotNull ExtensibleClassLoader classLoader,
|
||||
@NotNull RfbClassTransformer.Context context, @Nullable Manifest manifest, @NotNull String className,
|
||||
@NotNull ClassNodeHandle classNode) {
|
||||
if (!classNode.isPresent()) return false;
|
||||
return inner.shouldTransform(classNode.getOriginalBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transformClass(@NotNull ExtensibleClassLoader classLoader, @NotNull RfbClassTransformer.Context context,
|
||||
@Nullable Manifest manifest, @NotNull String className, @NotNull ClassNodeHandle classNode) {
|
||||
if (inner.transformClassNode(className, classNode.getNode())) {
|
||||
classNode.computeMaxs();
|
||||
AngelicaClassDump.dumpRFBClass(className, classNode, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public enum Mixins implements IMixins {
|
||||
, "angelica.MixinMinecraftServer"
|
||||
, "angelica.bugfixes.MixinItemRenderer_EdgeDepth"
|
||||
, "angelica.bugfixes.MixinModelCreeper_AuraBodyInflate"
|
||||
, "angelica.bugfixes.MixinModelSkeleton_LegPelvisZFight"
|
||||
, "angelica.bugfixes.MixinModelWither_ArmorCentering"
|
||||
, "angelica.bugfixes.MixinRenderBlocks_CrossedSquaresNormal"
|
||||
, "angelica.bugfixes.MixinRenderCreeper_AuraDepth"
|
||||
@@ -194,6 +195,7 @@ public enum Mixins implements IMixins {
|
||||
, "rendering.MixinTileEntity"
|
||||
, "rendering.MixinTileEntityMobSpawner"
|
||||
, "rendering.MixinTileEntityRendererDispatcher"
|
||||
, "rendering.MixinRenderBlocksEmissive"
|
||||
)
|
||||
),
|
||||
|
||||
@@ -287,6 +289,14 @@ public enum Mixins implements IMixins {
|
||||
)
|
||||
),
|
||||
|
||||
DRAGONAPI_SHADER_REGISTRY_PARSE_ERROR(new MixinBuilder()
|
||||
.setPhase(Phase.EARLY)
|
||||
.addRequiredMod(TargetedMod.DRAGON_API)
|
||||
.addClientMixins(
|
||||
"dragonapi.MixinShaderRegistry_ParseError"
|
||||
)
|
||||
),
|
||||
|
||||
IRIS_RENDERING_NOBACKHAND(new MixinBuilder("Iris Hand Shaders")
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaConfig.enableIris)
|
||||
@@ -381,16 +391,15 @@ public enum Mixins implements IMixins {
|
||||
.setApplyIf(() -> CompatConfig.fixMinefactoryReloaded)
|
||||
.addClientMixins("client.minefactoryreloaded.MixinRedNetCableRenderer")),
|
||||
|
||||
NTM_SPACE_TWEAKS(new MixinBuilder("Support for 'Disable Horizon' & 'disableAltitudePlanetRenderer' options in NTM:Space")
|
||||
.setPhase(Phase.LATE)
|
||||
.addRequiredMod(TargetedMod.NTM_SPACE)
|
||||
.setApplyIf(() -> CompatConfig.tweakNTMSpace)
|
||||
.addClientMixins("client.ntmSpace.MixinSkyProviderCelestial_Tweaks")),
|
||||
NTM_SPACE_SHADER_COMPAT(new MixinBuilder("Multiple shader fixes for NTM:Space")
|
||||
NTM_SPACE_COMPAT(new MixinBuilder("Multiple fixes for NTM:Space")
|
||||
.setPhase(Phase.LATE)
|
||||
.addRequiredMod(TargetedMod.NTM_SPACE)
|
||||
.setApplyIf(() -> CompatConfig.fixNTMSpace && AngelicaConfig.enableIris)
|
||||
.addClientMixins("client.ntmSpace.MixinSkyProviderCelestial_ShaderCompat", "client.ntmSpace.MixinSkyProviderLaytheSunset")),
|
||||
.addClientMixins(
|
||||
"client.ntmSpace.MixinSkyProviderCelestial",
|
||||
"client.ntmSpace.MixinSkyProviderOrbit",
|
||||
"client.ntmSpace.MixinSkyProviderLaytheSunset"
|
||||
)),
|
||||
|
||||
SPEEDUP_CAMPFIRE_BACKPORT_ANIMATIONS(new MixinBuilder("Add animation speedup support to Campfire Backport")
|
||||
.setPhase(Phase.LATE)
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
|
||||
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import com.gtnewhorizons.angelica.render.CloudRenderer;
|
||||
import com.gtnewhorizons.angelica.render.EmissiveTextureAutoloader;
|
||||
import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasSetup;
|
||||
@@ -43,6 +44,7 @@ import cpw.mods.fml.relauncher.ReflectionHelper;
|
||||
import jss.notfine.core.Settings;
|
||||
import jss.notfine.gui.GuiCustomMenu;
|
||||
import jss.notfine.gui.NotFineGameOptionPages;
|
||||
import jss.notfine.gui.options.named.FOVMode;
|
||||
import me.flashyreese.mods.reeses_sodium_options.client.gui.ReeseSodiumVideoOptionsScreen;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumOptionsGUI;
|
||||
@@ -101,30 +103,11 @@ public final class ClientProxy extends CommonProxy {
|
||||
super.preInit(event);
|
||||
FMLCommonHandler.instance().bus().register(this);
|
||||
MinecraftForge.EVENT_BUS.register(this);
|
||||
MinecraftForge.EVENT_BUS.register(new EmissiveTextureAutoloader());
|
||||
ModelRegistry.registerModid(MOD_ID);
|
||||
blockError = new BlockError();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void worldLoad(WorldEvent.Load event) {
|
||||
if (GLStateManager.isRunningSplash()) {
|
||||
GLStateManager.setRunningSplash(false);
|
||||
LOGGER.info("World loaded - Enabling GLSM Cache");
|
||||
}
|
||||
|
||||
if (AngelicaConfig.enableCeleritas) {
|
||||
ChunkTaskRegistry.reset();
|
||||
ChunkTaskRegistry.registerProvider(DefaultChunkTaskProvider.INSTANCE);
|
||||
ChunkTaskRegistry.registerProvider(ThreadedChunkTaskProvider.INSTANCE);
|
||||
|
||||
// Register all blocks. Because blockids are unique to a world, this must be done each load
|
||||
GameData.getBlockRegistry().typeSafeIterable().forEach(o -> {
|
||||
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, true, true);
|
||||
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, false, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FMLInitializationEvent event) {
|
||||
super.init(event);
|
||||
@@ -169,7 +152,6 @@ public final class ClientProxy extends CommonProxy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void postInit(FMLPostInitializationEvent event) {
|
||||
super.postInit(event);
|
||||
@@ -189,11 +171,38 @@ public final class ClientProxy extends CommonProxy {
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void worldLoad(WorldEvent.Load event) {
|
||||
if (!event.world.isRemote) return;
|
||||
if (GLStateManager.isRunningSplash()) {
|
||||
GLStateManager.setRunningSplash(false);
|
||||
LOGGER.info("World loaded - Enabling GLSM Cache");
|
||||
}
|
||||
|
||||
if (AngelicaConfig.enableCeleritas) {
|
||||
ChunkTaskRegistry.reset();
|
||||
ChunkTaskRegistry.registerProvider(DefaultChunkTaskProvider.INSTANCE);
|
||||
ChunkTaskRegistry.registerProvider(ThreadedChunkTaskProvider.INSTANCE);
|
||||
|
||||
// Register all blocks. Because blockids are unique to a world, this must be done each load
|
||||
GameData.getBlockRegistry().typeSafeIterable().forEach(o -> {
|
||||
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, true, true);
|
||||
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, false, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onWorldUnload(WorldEvent.Unload event) {
|
||||
if (!event.world.isRemote) return;
|
||||
DynamicLights.get().removeAllLightSources();
|
||||
}
|
||||
|
||||
float lastIntegratedTickTime;
|
||||
|
||||
@SubscribeEvent
|
||||
public void onTick(TickEvent.ServerTickEvent event) {
|
||||
if (FMLCommonHandler.instance().getSide().isClient() && event.phase == TickEvent.Phase.END) {
|
||||
if (event.phase == TickEvent.Phase.END) {
|
||||
final IntegratedServer srv = Minecraft.getMinecraft().getIntegratedServer();
|
||||
if (srv != null) {
|
||||
final long currentTickTime = srv.tickTimeArray[srv.getTickCounter() % 100];
|
||||
@@ -342,10 +351,19 @@ public final class ClientProxy extends CommonProxy {
|
||||
}
|
||||
}
|
||||
|
||||
// This is a bit of a hack to prevent the FOV from being modified by other mods
|
||||
// This only disables FOV changes from vanilla, leaving mod FOV changes untouched
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public void onFOVModifierUpdateEarly(FOVUpdateEvent event) {
|
||||
if(Settings.DYNAMIC_FOV.option.getStore() == FOVMode.MODS) {
|
||||
event.fov = 1.0F;
|
||||
event.newfov = 1.0F;
|
||||
}
|
||||
}
|
||||
|
||||
// This removes all FOV dynamics, both from vanilla (speed/sprinting), and mods
|
||||
@SubscribeEvent(priority = EventPriority.LOWEST)
|
||||
public void onFOVModifierUpdate(FOVUpdateEvent event) {
|
||||
if (!(boolean) Settings.DYNAMIC_FOV.option.getStore()) {
|
||||
public void onFOVModifierUpdateLate(FOVUpdateEvent event) {
|
||||
if(Settings.DYNAMIC_FOV.option.getStore() == FOVMode.NONE) {
|
||||
event.newfov = 1.0F;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.TextureMap;
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraftforge.client.event.TextureStitchEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class EmissiveTextureAutoloader {
|
||||
|
||||
@SubscribeEvent
|
||||
public void onTextureStitchPre(TextureStitchEvent.Pre event) {
|
||||
if (event.map.getTextureType() != 0) return;
|
||||
|
||||
TextureMap map = event.map;
|
||||
EmissiveTextureHelper.clearCache();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> registeredSprites = map.mapRegisteredSprites;
|
||||
|
||||
List<String> toRegister = new ArrayList<>();
|
||||
|
||||
for (String baseName : registeredSprites.keySet()) {
|
||||
if (baseName.endsWith("_emissive")) continue;
|
||||
|
||||
String emissiveName = baseName + "_emissive";
|
||||
if (resourceExists(emissiveName)) {
|
||||
toRegister.add(baseName);
|
||||
}
|
||||
}
|
||||
|
||||
for (String baseName : toRegister) {
|
||||
String emissiveName = baseName + "_emissive";
|
||||
IIcon emissiveIcon = map.registerIcon(emissiveName);
|
||||
EmissiveTextureHelper.cacheEmissive(baseName, emissiveIcon);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean resourceExists(String iconName) {
|
||||
ResourceLocation loc = new ResourceLocation(iconName);
|
||||
ResourceLocation full = new ResourceLocation(
|
||||
loc.getResourceDomain(),
|
||||
"textures/blocks/" + loc.getResourcePath() + ".png"
|
||||
);
|
||||
try {
|
||||
Minecraft.getMinecraft().getResourceManager().getResource(full);
|
||||
return true;
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.gtnewhorizons.angelica.render;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.renderer.RenderBlocks;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.renderer.texture.IIconRegister;
|
||||
import net.minecraft.util.IIcon;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public final class EmissiveTextureHelper {
|
||||
private static final ConcurrentHashMap<String, IIcon> EMISSIVE_ICON_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
public static IIcon getEmissiveIcon(Block block, int side, int metadata) {
|
||||
IIcon baseIcon = block.getIcon(side, metadata);
|
||||
if (baseIcon == null) return null;
|
||||
|
||||
String baseIconName = baseIcon.getIconName();
|
||||
if (baseIconName == null) return null;
|
||||
|
||||
return EMISSIVE_ICON_CACHE.get(baseIconName);
|
||||
}
|
||||
|
||||
public static void clearCache() {
|
||||
EMISSIVE_ICON_CACHE.clear();
|
||||
}
|
||||
|
||||
public static void cacheEmissive(String baseIconName, IIcon emissiveIcon) {
|
||||
EMISSIVE_ICON_CACHE.put(baseIconName, emissiveIcon);
|
||||
}
|
||||
|
||||
public static void renderEmissiveBlockOverlay(RenderBlocks renderer, Block block, int x, int y, int z, int metadata) {
|
||||
Tessellator tess = Tessellator.instance;
|
||||
|
||||
for (int side = 0; side < 6; side++) {
|
||||
IIcon icon = getEmissiveIcon(block, side, metadata);
|
||||
if (icon == null) continue;
|
||||
|
||||
tess.setColorOpaque_F(1f, 1f, 1f);
|
||||
|
||||
switch (side) {
|
||||
case 0 -> renderer.renderFaceYNeg(block, x, y, z, icon);
|
||||
case 1 -> renderer.renderFaceYPos(block, x, y, z, icon);
|
||||
case 2 -> renderer.renderFaceZNeg(block, x, y, z, icon);
|
||||
case 3 -> renderer.renderFaceZPos(block, x, y, z, icon);
|
||||
case 4 -> renderer.renderFaceXNeg(block, x, y, z, icon);
|
||||
case 5 -> renderer.renderFaceXPos(block, x, y, z, icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,13 @@ public final class TileEntityRenderBoundsRegistry {
|
||||
}
|
||||
|
||||
private static boolean probeAndCache(TileEntity te, Class<? extends TileEntity> clazz) {
|
||||
boolean isInfinite = false;
|
||||
boolean isInfinite;
|
||||
try {
|
||||
final AxisAlignedBB aabb = te.getRenderBoundingBox();
|
||||
isInfinite = isInfiniteExtentsBox(aabb);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Throwable t) {
|
||||
isInfinite = true;
|
||||
}
|
||||
|
||||
synchronized (classRegistry) {
|
||||
classRegistry.put(clazz, isInfinite ? INFINITE : FINITE);
|
||||
|
||||
@@ -8,7 +8,10 @@ import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.iris.BlockRenderContext;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.iris.ContextAwareChunkVertexEncoder;
|
||||
import com.prupe.mcpatcher.mal.block.RenderBlocksUtils;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
|
||||
import net.coderbot.iris.block_rendering.BlockMaterialMapping;
|
||||
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
|
||||
import net.coderbot.iris.vertices.ExtendedDataHelper;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -144,13 +147,18 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
if (TileEntityRenderBoundsRegistry.isAlwaysInfiniteExtent(tileEntity)) {
|
||||
isGlobal = true;
|
||||
} else {
|
||||
final AxisAlignedBB aabb = tileEntity.getRenderBoundingBox();
|
||||
AxisAlignedBB aabb;
|
||||
try {
|
||||
aabb = tileEntity.getRenderBoundingBox();
|
||||
} catch (Throwable t) {
|
||||
aabb = null;
|
||||
}
|
||||
if (aabb != null) {
|
||||
final int secMinX = x & ~15, secMinY = y & ~15, secMinZ = z & ~15;
|
||||
isGlobal = aabb.minX < secMinX || aabb.minY < secMinY || aabb.minZ < secMinZ
|
||||
|| aabb.maxX > secMinX + 16 || aabb.maxY > secMinY + 16 || aabb.maxZ > secMinZ + 16;
|
||||
} else {
|
||||
isGlobal = false;
|
||||
isGlobal = true;
|
||||
}
|
||||
}
|
||||
(isGlobal ? renderData.globalBlockEntities : renderData.culledBlockEntities).add(tileEntity);
|
||||
@@ -256,7 +264,13 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
if (isFluid) {
|
||||
contextEncoder.prepareToRenderFluid(blockRenderContext, block, lightValue);
|
||||
} else {
|
||||
contextEncoder.prepareToRenderBlock(blockRenderContext, block, metadata,
|
||||
int effectiveMeta = metadata;
|
||||
if (BlockRenderingSettings.INSTANCE.hasSnowyEntries()
|
||||
&& BlockRenderingSettings.INSTANCE.getSnowyBlocks().contains(block)
|
||||
&& RenderBlocksUtils.isSnowCovered(renderBlocks.blockAccess, x, y, z)) {
|
||||
effectiveMeta |= BlockMaterialMapping.SNOWY_META_BIT;
|
||||
}
|
||||
contextEncoder.prepareToRenderBlock(blockRenderContext, block, effectiveMeta,
|
||||
ExtendedDataHelper.BLOCK_RENDER_TYPE, lightValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.gtnewhorizons.angelica.utils;
|
||||
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
|
||||
public class WorkaroundUtils {
|
||||
|
||||
public static ResourceLocation GL_TEXTURE_2D_WORKAROUND = new ResourceLocation("angelica:textures/shaders_workaround.png");
|
||||
|
||||
}
|
||||
@@ -158,6 +158,34 @@ final class BlockOrientation extends RenderBlockState {
|
||||
return origIcon == neighbor.getIcon(blockAccess, neighborX, neighborY, neighborZ, getTextureFaceOrig());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockOrientation copy() {
|
||||
BlockOrientation copy = new BlockOrientation();
|
||||
copy.x = x;
|
||||
copy.y = y;
|
||||
copy.z = z;
|
||||
copy.metadata = metadata;
|
||||
copy.altMetadata = altMetadata;
|
||||
copy.metadataBits = metadataBits;
|
||||
copy.renderType = renderType;
|
||||
copy.blockFace = blockFace;
|
||||
copy.textureFace = textureFace;
|
||||
copy.textureFaceOrig = textureFaceOrig;
|
||||
copy.rotateUV = rotateUV;
|
||||
|
||||
copy.blockAccess = blockAccess;
|
||||
copy.block = block;
|
||||
copy.useAO = useAO;
|
||||
copy.inWorld = inWorld;
|
||||
copy.matcher = matcher;
|
||||
copy.offsetsComputed = offsetsComputed;
|
||||
copy.haveOffsets = haveOffsets;
|
||||
copy.dx = dx;
|
||||
copy.dy = dy;
|
||||
copy.dz = dz;
|
||||
return copy;
|
||||
}
|
||||
|
||||
void setBlock(Block block, IBlockAccess blockAccess, int x, int y, int z) {
|
||||
this.block = block;
|
||||
this.blockAccess = blockAccess;
|
||||
|
||||
@@ -15,6 +15,7 @@ import net.minecraft.util.IIcon;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
|
||||
import com.github.bsideup.jabel.Desugar;
|
||||
import com.prupe.mcpatcher.MCLogger;
|
||||
import com.prupe.mcpatcher.MCPatcherUtils;
|
||||
import com.prupe.mcpatcher.mal.block.BlockAPI;
|
||||
@@ -43,13 +44,16 @@ public class CTMUtils {
|
||||
private static Overrides newOverrides = null;
|
||||
private static TileLoader tileLoader;
|
||||
|
||||
private static final ThreadLocal<TileOverrideImpl.CTMCompact> CURRENT_COMPACT = new ThreadLocal<>();
|
||||
@Desugar
|
||||
public record CTMCompactContext(TileOverrideImpl.CTMCompact compact, RenderBlockState renderBlockState) {}
|
||||
|
||||
public static void setCurrentCompact(TileOverrideImpl.CTMCompact compact) {
|
||||
CURRENT_COMPACT.set(compact);
|
||||
private static final ThreadLocal<CTMCompactContext> CURRENT_COMPACT = new ThreadLocal<>();
|
||||
|
||||
public static void setCurrentCompact(TileOverrideImpl.CTMCompact compact, RenderBlockState renderBlockState) {
|
||||
CURRENT_COMPACT.set(new CTMCompactContext(compact, renderBlockState.copy()));
|
||||
}
|
||||
|
||||
public static TileOverrideImpl.CTMCompact getCurrentCompact() {
|
||||
public static CTMCompactContext getCurrentCompact() {
|
||||
return CURRENT_COMPACT.get();
|
||||
}
|
||||
|
||||
@@ -180,6 +184,7 @@ public class CTMUtils {
|
||||
local.renderBlockState.setBlock(block, blockAccess, x, y, z);
|
||||
local.renderBlockState.setFace(face);
|
||||
TileOverride lastOverride = local.ijkIterator.go(local.renderBlockState, icon);
|
||||
local.renderBlockState.blockAccess = null;
|
||||
if (lastOverride != null) {
|
||||
return skipDefaultRendering(block) ? RenderBlocksUtils.blankIcon : local.ijkIterator.getIcon();
|
||||
}
|
||||
@@ -233,6 +238,7 @@ public class CTMUtils {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
clearCurrentCompact();
|
||||
}
|
||||
|
||||
private static boolean checkFace(int face) {
|
||||
|
||||
@@ -1,70 +1,18 @@
|
||||
package com.prupe.mcpatcher.ctm;
|
||||
|
||||
import com.github.bsideup.jabel.Desugar;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.renderer.RenderBlocks;
|
||||
import net.minecraft.util.IIcon;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
|
||||
public final class CompactCtmQuadProcessor {
|
||||
|
||||
private static final int LEFT = 0;
|
||||
private static final int UP_LEFT = 1;
|
||||
private static final int UP = 2;
|
||||
private static final int UP_RIGHT = 3;
|
||||
private static final int RIGHT = 4;
|
||||
private static final int DOWN_RIGHT = 5;
|
||||
private static final int DOWN = 6;
|
||||
private static final int DOWN_LEFT = 7;
|
||||
|
||||
private static final int SPRITE_DEFAULT = 0;
|
||||
private static final int SPRITE_ALL = 1;
|
||||
private static final int SPRITE_HORIZONTAL = 2;
|
||||
private static final int SPRITE_VERTICAL = 3;
|
||||
private static final int SPRITE_MIXED = 4;
|
||||
|
||||
private static final int[] CTM_47_BY_CONNECTIONS = {
|
||||
0,3,0,3,12,5,12,15,0,3,0,3,12,5,12,15,
|
||||
1,2,1,2,4,7,4,29,1,2,1,2,13,31,13,14,
|
||||
0,3,0,3,12,5,12,15,0,3,0,3,12,5,12,15,
|
||||
1,2,1,2,4,7,4,29,1,2,1,2,13,31,13,14,
|
||||
36,17,36,17,24,19,24,43,36,17,36,17,24,19,24,43,
|
||||
16,18,16,18,6,46,6,21,16,18,16,18,28,9,28,22,
|
||||
36,17,36,17,24,19,24,43,36,17,36,17,24,19,24,43,
|
||||
37,40,37,40,30,8,30,34,37,40,37,40,25,23,25,45,
|
||||
0,3,0,3,12,5,12,15,0,3,0,3,12,5,12,15,
|
||||
1,2,1,2,4,7,4,29,1,2,1,2,13,31,13,14,
|
||||
0,3,0,3,12,5,12,15,0,3,0,3,12,5,12,15,
|
||||
1,2,1,2,4,7,4,29,1,2,1,2,13,31,13,14,
|
||||
36,39,36,39,24,41,24,27,36,39,36,39,24,41,24,27,
|
||||
16,42,16,42,6,20,6,10,16,42,16,42,28,35,28,44,
|
||||
36,39,36,39,24,41,24,27,36,39,36,39,24,41,24,27,
|
||||
37,38,37,38,30,11,30,32,37,38,37,38,25,33,25,26
|
||||
};
|
||||
|
||||
/**
|
||||
* Face-local basis:
|
||||
* right = increasing U on the face
|
||||
* down = increasing V on the face
|
||||
*/
|
||||
private static final FaceBasis[] FACE_BASIS = {
|
||||
new FaceBasis( 1, 0, 0, 0, 0, 1), // 0: Y-
|
||||
new FaceBasis( 1, 0, 0, 0, 0, 1), // 1: Y+
|
||||
new FaceBasis(-1, 0, 0, 0, -1, 0), // 2: Z-
|
||||
new FaceBasis( 1, 0, 0, 0, -1, 0), // 3: Z+
|
||||
new FaceBasis( 0, 0, 1, 0, -1, 0), // 4: X-
|
||||
new FaceBasis( 0, 0, -1, 0, -1, 0) // 5: X+
|
||||
};
|
||||
|
||||
/**
|
||||
* Neighbor offsets in face-local coordinates:
|
||||
* 0 = left, 1 = up-left, 2 = up, 3 = up-right,
|
||||
* 4 = right, 5 = down-right, 6 = down, 7 = down-left
|
||||
*/
|
||||
private static final int[] RIGHT_FACTOR = {-1, -1, 0, 1, 1, 1, 0, -1};
|
||||
private static final int[] DOWN_FACTOR = { 0, -1, -1, -1, 0, 1, 1, 1};
|
||||
|
||||
/**
|
||||
* Quadrant side references:
|
||||
* 0 = top-left
|
||||
@@ -72,41 +20,58 @@ public final class CompactCtmQuadProcessor {
|
||||
* 2 = bottom-right
|
||||
* 3 = bottom-left
|
||||
*/
|
||||
private static final int[] QUAD_A = {LEFT, UP, RIGHT, DOWN};
|
||||
private static final int[] QUAD_B = {UP, RIGHT, DOWN, LEFT};
|
||||
private static final int[] QUAD_DIAG = {UP_LEFT, UP_RIGHT, DOWN_RIGHT, DOWN_LEFT};
|
||||
private static final int[] QUAD_A = {RenderBlockState.REL_L, RenderBlockState.REL_U,
|
||||
RenderBlockState.REL_R, RenderBlockState.REL_D};
|
||||
private static final int[] QUAD_B = {RenderBlockState.REL_U, RenderBlockState.REL_R,
|
||||
RenderBlockState.REL_D, RenderBlockState.REL_L};
|
||||
private static final int[] QUAD_DIAG = {RenderBlockState.REL_UL, RenderBlockState.REL_UR,
|
||||
RenderBlockState.REL_DR, RenderBlockState.REL_DL};
|
||||
|
||||
/** Maps split-face quadrant (0=TL,1=TR,2=BR,3=BL face-local) to RenderBlocks per-face TL/TR/BR/BL orientation. */
|
||||
private static final int[][] RENDER_FACE_QUADRANT_MAP = {
|
||||
{3, 2, 1, 0}, // Y-
|
||||
{2, 3, 0, 1}, // Y+
|
||||
{3, 0, 1, 2}, // Z-
|
||||
{0, 1, 2, 3}, // Z+
|
||||
{3, 0, 1, 2}, // X-
|
||||
{1, 2, 3, 0} // X+
|
||||
};
|
||||
private static final float[] QUADRANT_MIN_U = {0.0f, 0.5f, 0.5f, 0.0f};
|
||||
private static final float[] QUADRANT_MIN_V = {0.0f, 0.0f, 0.5f, 0.5f};
|
||||
|
||||
private final IIcon[] sprites;
|
||||
private final CompactConnectingCtmProperties props;
|
||||
private final TileOverrideImpl.CTMCompact override;
|
||||
|
||||
CompactCtmQuadProcessor(IIcon[] sprites, CompactConnectingCtmProperties props) {
|
||||
CompactCtmQuadProcessor(IIcon[] sprites, CompactConnectingCtmProperties props, TileOverrideImpl.CTMCompact compact) {
|
||||
this.sprites = new IIcon[5];
|
||||
if (sprites != null) {
|
||||
System.arraycopy(sprites, 0, this.sprites, 0, Math.min(sprites.length, this.sprites.length));
|
||||
}
|
||||
this.props = props;
|
||||
this.override = compact;
|
||||
}
|
||||
|
||||
public boolean processFace(RenderBlocks rb, IBlockAccess world, int x, int y, int z, int face, IIcon origIcon) {
|
||||
int connections = CtmConnectionHelper.getConnections(world, x, y, z, face, props.getInnerSeams());
|
||||
public boolean processFace(RenderBlocks rb, RenderBlockState renderBlockState, IIcon origIcon) {
|
||||
int neighborBits = override.getNeighborBits(renderBlockState, origIcon);
|
||||
|
||||
int ctmIndex = CTM_47_BY_CONNECTIONS[connections & 0xFF];
|
||||
int ctmIndex = TileOverride.neighborMap[neighborBits & 0xFF];
|
||||
IIcon replacement = getReplacementIcon(ctmIndex);
|
||||
if (replacement != null) {
|
||||
renderWholeFace(rb, world, x, y, z, face, replacement);
|
||||
renderWholeFace(rb, renderBlockState, replacement);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (connections == 0 || connections == 0xFF) {
|
||||
IIcon sprite = sprites[getCompactSpriteIndex(connections)];
|
||||
if (neighborBits == 0 || neighborBits == 0xFF) {
|
||||
IIcon sprite = sprites[getCompactSpriteIndex(neighborBits)];
|
||||
if (sprite != null) {
|
||||
renderWholeFace(rb, world, x, y, z, face, sprite);
|
||||
renderWholeFace(rb, renderBlockState, sprite);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
renderSplitFace(rb, world, x, y, z, face, connections);
|
||||
renderSplitFace(rb, renderBlockState, neighborBits);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,54 +101,137 @@ public final class CompactCtmQuadProcessor {
|
||||
return SPRITE_ALL;
|
||||
}
|
||||
|
||||
if ((connections & ((1 << LEFT) | (1 << RIGHT))) == ((1 << LEFT) | (1 << RIGHT))) {
|
||||
if ((connections & ((1 << RenderBlockState.REL_L) | (1 << RenderBlockState.REL_R))) ==
|
||||
((1 << RenderBlockState.REL_L) | (1 << RenderBlockState.REL_R))) {
|
||||
return SPRITE_HORIZONTAL;
|
||||
}
|
||||
|
||||
if ((connections & ((1 << UP) | (1 << DOWN))) == ((1 << UP) | (1 << DOWN))) {
|
||||
if ((connections & ((1 << RenderBlockState.REL_U) | (1 << RenderBlockState.REL_D))) ==
|
||||
((1 << RenderBlockState.REL_U) | (1 << RenderBlockState.REL_D))) {
|
||||
return SPRITE_VERTICAL;
|
||||
}
|
||||
|
||||
return SPRITE_DEFAULT;
|
||||
}
|
||||
|
||||
private void renderSplitFace(RenderBlocks rb, IBlockAccess world, int x, int y, int z, int face, int connections) {
|
||||
Block block = world.getBlock(x, y, z);
|
||||
private void renderSplitFace(RenderBlocks rb, RenderBlockState renderBlockState, int connections) {
|
||||
final int face = renderBlockState.getBlockFace();
|
||||
|
||||
double minX = rb.renderMinX;
|
||||
double minY = rb.renderMinY;
|
||||
double minZ = rb.renderMinZ;
|
||||
double maxX = rb.renderMaxX;
|
||||
double maxY = rb.renderMaxY;
|
||||
double maxZ = rb.renderMaxZ;
|
||||
final double minX = rb.renderMinX, minY = rb.renderMinY, minZ = rb.renderMinZ;
|
||||
final double maxX = rb.renderMaxX, maxY = rb.renderMaxY, maxZ = rb.renderMaxZ;
|
||||
|
||||
// Without AO the corner color/brightness fields are unused; skip the lighting dance.
|
||||
final boolean ao = rb.enableAO;
|
||||
|
||||
final float rTL, rTR, rBL, rBR, gTL, gTR, gBL, gBR, bTL, bTR, bBL, bBR;
|
||||
final int brTL, brTR, brBL, brBR;
|
||||
if (ao) {
|
||||
rTL = rb.colorRedTopLeft; rTR = rb.colorRedTopRight;
|
||||
rBL = rb.colorRedBottomLeft; rBR = rb.colorRedBottomRight;
|
||||
gTL = rb.colorGreenTopLeft; gTR = rb.colorGreenTopRight;
|
||||
gBL = rb.colorGreenBottomLeft; gBR = rb.colorGreenBottomRight;
|
||||
bTL = rb.colorBlueTopLeft; bTR = rb.colorBlueTopRight;
|
||||
bBL = rb.colorBlueBottomLeft; bBR = rb.colorBlueBottomRight;
|
||||
brTL = rb.brightnessTopLeft; brTR = rb.brightnessTopRight;
|
||||
brBL = rb.brightnessBottomLeft; brBR = rb.brightnessBottomRight;
|
||||
} else {
|
||||
rTL = rTR = rBL = rBR = 0f;
|
||||
gTL = gTR = gBL = gBR = 0f;
|
||||
bTL = bTR = bBL = bBR = 0f;
|
||||
brTL = brTR = brBL = brBR = 0;
|
||||
}
|
||||
|
||||
for (int q = 0; q < 4; q++) {
|
||||
IIcon icon = sprites[getQuadrantSprite(q, connections)];
|
||||
final IIcon icon = sprites[getQuadrantSprite(q, connections)];
|
||||
if (icon == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ao) {
|
||||
applyQuadrantLighting(rb, face, q, rTL, rTR, rBL, rBR, gTL, gTR, gBL, gBR, bTL, bTR, bBL, bBR, brTL, brTR, brBL, brBR);
|
||||
}
|
||||
setQuadrantBounds(rb, face, q, minX, minY, minZ, maxX, maxY, maxZ);
|
||||
rb.overrideBlockTexture = icon;
|
||||
renderFace(rb, block, x, y, z, face, icon);
|
||||
renderFace(rb, renderBlockState, icon);
|
||||
}
|
||||
|
||||
if (ao) {
|
||||
rb.colorRedTopLeft = rTL; rb.colorRedTopRight = rTR;
|
||||
rb.colorRedBottomLeft = rBL; rb.colorRedBottomRight = rBR;
|
||||
rb.colorGreenTopLeft = gTL; rb.colorGreenTopRight = gTR;
|
||||
rb.colorGreenBottomLeft = gBL; rb.colorGreenBottomRight = gBR;
|
||||
rb.colorBlueTopLeft = bTL; rb.colorBlueTopRight = bTR;
|
||||
rb.colorBlueBottomLeft = bBL; rb.colorBlueBottomRight = bBR;
|
||||
rb.brightnessTopLeft = brTL; rb.brightnessTopRight = brTR;
|
||||
rb.brightnessBottomLeft = brBL; rb.brightnessBottomRight = brBR;
|
||||
}
|
||||
rb.overrideBlockTexture = null;
|
||||
rb.setRenderBounds(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
|
||||
private void renderWholeFace(RenderBlocks rb, IBlockAccess world, int x, int y, int z, int face, IIcon icon) {
|
||||
Block block = world.getBlock(x, y, z);
|
||||
private static void applyQuadrantLighting(RenderBlocks rb, int face, int quadrant, float rTL, float rTR, float rBL, float rBR, float gTL, float gTR, float gBL, float gBR, float bTL, float bTR, float bBL, float bBR, int brTL, int brTR, int brBL, int brBR) {
|
||||
final int rq = RENDER_FACE_QUADRANT_MAP[face][quadrant];
|
||||
final float u0 = QUADRANT_MIN_U[rq], u1 = u0 + 0.5f;
|
||||
final float v0 = QUADRANT_MIN_V[rq], v1 = v0 + 0.5f;
|
||||
|
||||
rb.colorRedTopLeft = bilerp(rTL, rTR, rBL, rBR, u0, v0);
|
||||
rb.colorRedTopRight = bilerp(rTL, rTR, rBL, rBR, u1, v0);
|
||||
rb.colorRedBottomLeft = bilerp(rTL, rTR, rBL, rBR, u0, v1);
|
||||
rb.colorRedBottomRight = bilerp(rTL, rTR, rBL, rBR, u1, v1);
|
||||
|
||||
rb.colorGreenTopLeft = bilerp(gTL, gTR, gBL, gBR, u0, v0);
|
||||
rb.colorGreenTopRight = bilerp(gTL, gTR, gBL, gBR, u1, v0);
|
||||
rb.colorGreenBottomLeft = bilerp(gTL, gTR, gBL, gBR, u0, v1);
|
||||
rb.colorGreenBottomRight = bilerp(gTL, gTR, gBL, gBR, u1, v1);
|
||||
|
||||
rb.colorBlueTopLeft = bilerp(bTL, bTR, bBL, bBR, u0, v0);
|
||||
rb.colorBlueTopRight = bilerp(bTL, bTR, bBL, bBR, u1, v0);
|
||||
rb.colorBlueBottomLeft = bilerp(bTL, bTR, bBL, bBR, u0, v1);
|
||||
rb.colorBlueBottomRight = bilerp(bTL, bTR, bBL, bBR, u1, v1);
|
||||
|
||||
rb.brightnessTopLeft = bilerpLight(brTL, brTR, brBL, brBR, u0, v0);
|
||||
rb.brightnessTopRight = bilerpLight(brTL, brTR, brBL, brBR, u1, v0);
|
||||
rb.brightnessBottomLeft = bilerpLight(brTL, brTR, brBL, brBR, u0, v1);
|
||||
rb.brightnessBottomRight = bilerpLight(brTL, brTR, brBL, brBR, u1, v1);
|
||||
}
|
||||
|
||||
private static float bilerp(float c00, float c10, float c01, float c11, float u, float v) {
|
||||
final float top = c00 + (c10 - c00) * u;
|
||||
final float bot = c01 + (c11 - c01) * u;
|
||||
return top + (bot - top) * v;
|
||||
}
|
||||
|
||||
/** Brightness packs sky in upper 16 bits, block in lower; bilerp each independently then repack. */
|
||||
private static int bilerpLight(int c00, int c10, int c01, int c11, float u, float v) {
|
||||
final int b00 = c00 & 0xFFFF, b10 = c10 & 0xFFFF, b01 = c01 & 0xFFFF, b11 = c11 & 0xFFFF;
|
||||
final int s00 = c00 >>> 16, s10 = c10 >>> 16, s01 = c01 >>> 16, s11 = c11 >>> 16;
|
||||
|
||||
final float bTop = b00 + (b10 - b00) * u;
|
||||
final float bBot = b01 + (b11 - b01) * u;
|
||||
final float sTop = s00 + (s10 - s00) * u;
|
||||
final float sBot = s01 + (s11 - s01) * u;
|
||||
|
||||
final int block = Math.round(bTop + (bBot - bTop) * v);
|
||||
final int sky = Math.round(sTop + (sBot - sTop) * v);
|
||||
return (sky << 16) | (block & 0xFFFF);
|
||||
}
|
||||
|
||||
private void renderWholeFace(RenderBlocks rb, RenderBlockState renderBlockState, IIcon icon) {
|
||||
IIcon saved = rb.overrideBlockTexture;
|
||||
rb.overrideBlockTexture = icon;
|
||||
try {
|
||||
renderFace(rb, block, x, y, z, face, icon);
|
||||
renderFace(rb, renderBlockState, icon);
|
||||
} finally {
|
||||
rb.overrideBlockTexture = saved;
|
||||
}
|
||||
}
|
||||
|
||||
private void renderFace(RenderBlocks rb, Block block, int x, int y, int z, int face, IIcon icon) {
|
||||
private void renderFace(RenderBlocks rb, RenderBlockState renderBlockState, IIcon icon) {
|
||||
int face = renderBlockState.getBlockFace();
|
||||
Block block = renderBlockState.getBlock();
|
||||
int x = renderBlockState.getX();
|
||||
int y = renderBlockState.getY();
|
||||
int z = renderBlockState.getZ();
|
||||
boolean originalFlip = rb.field_152631_f;
|
||||
rb.field_152631_f = (face == 2 || face == 5);
|
||||
try {
|
||||
@@ -271,52 +319,10 @@ public final class CompactCtmQuadProcessor {
|
||||
|
||||
if (connA ^ connB) {
|
||||
int connectedBit = connA ? sideA : sideB;
|
||||
return (connectedBit == LEFT || connectedBit == RIGHT) ? SPRITE_VERTICAL : SPRITE_HORIZONTAL;
|
||||
return (connectedBit == RenderBlockState.REL_L || connectedBit == RenderBlockState.REL_R) ? SPRITE_VERTICAL
|
||||
: SPRITE_HORIZONTAL;
|
||||
}
|
||||
|
||||
return SPRITE_DEFAULT;
|
||||
}
|
||||
|
||||
@Desugar
|
||||
private record FaceBasis(int rightX, int rightY, int rightZ, int downX, int downY, int downZ) { }
|
||||
|
||||
private static final class CtmConnectionHelper {
|
||||
|
||||
public static int getConnections(IBlockAccess world, int x, int y, int z, int face, boolean innerSeams) {
|
||||
Block self = world.getBlock(x, y, z);
|
||||
FaceBasis basis = FACE_BASIS[face];
|
||||
|
||||
int bits = 0;
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int dx = RIGHT_FACTOR[i] * basis.rightX + DOWN_FACTOR[i] * basis.downX;
|
||||
int dy = RIGHT_FACTOR[i] * basis.rightY + DOWN_FACTOR[i] * basis.downY;
|
||||
int dz = RIGHT_FACTOR[i] * basis.rightZ + DOWN_FACTOR[i] * basis.downZ;
|
||||
|
||||
int nx = x + dx;
|
||||
int ny = y + dy;
|
||||
int nz = z + dz;
|
||||
|
||||
if (world.getBlock(nx, ny, nz) != self) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (innerSeams && !shouldRenderInnerSeam(world, nx, ny, nz, face)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bits |= (1 << i);
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
private static boolean shouldRenderInnerSeam(IBlockAccess world, int x, int y, int z, int face) {
|
||||
int[] normal = RenderBlockState.NORMALS[face];
|
||||
int sx = x + normal[0];
|
||||
int sy = y + normal[1];
|
||||
int sz = z + normal[2];
|
||||
return world.getBlock(sx, sy, sz).shouldSideBeRendered(world, sx, sy, sz, face);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,4 +139,6 @@ abstract public class RenderBlockState {
|
||||
|
||||
abstract public boolean shouldConnectByTile(Block neighbor, IIcon origIcon, int neighborX, int neighborY,
|
||||
int neighborZ);
|
||||
|
||||
abstract public RenderBlockState copy();
|
||||
}
|
||||
@@ -48,6 +48,28 @@ public abstract class TileOverride implements Comparable<TileOverride> {
|
||||
private final List<ResourceLocation> tileNames = new ArrayList<>();
|
||||
protected IIcon[] icons;
|
||||
|
||||
// Index into this array is formed from these bit values:
|
||||
// 128 64 32
|
||||
// 1 * 16
|
||||
// 2 4 8
|
||||
protected static final int[] neighborMap = new int[] {
|
||||
0, 3, 0, 3, 12, 5, 12, 15, 0, 3, 0, 3, 12, 5, 12, 15,
|
||||
1, 2, 1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14,
|
||||
0, 3, 0, 3, 12, 5, 12, 15, 0, 3, 0, 3, 12, 5, 12, 15,
|
||||
1, 2, 1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14,
|
||||
36, 17, 36, 17, 24, 19, 24, 43, 36, 17, 36, 17, 24, 19, 24, 43,
|
||||
16, 18, 16, 18, 6, 46, 6, 21, 16, 18, 16, 18, 28, 9, 28, 22,
|
||||
36, 17, 36, 17, 24, 19, 24, 43, 36, 17, 36, 17, 24, 19, 24, 43,
|
||||
37, 40, 37, 40, 30, 8, 30, 34, 37, 40, 37, 40, 25, 23, 25, 45,
|
||||
0, 3, 0, 3, 12, 5, 12, 15, 0, 3, 0, 3, 12, 5, 12, 15,
|
||||
1, 2, 1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14,
|
||||
0, 3, 0, 3, 12, 5, 12, 15, 0, 3, 0, 3, 12, 5, 12, 15,
|
||||
1, 2, 1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14,
|
||||
36, 39, 36, 39, 24, 41, 24, 27, 36, 39, 36, 39, 24, 41, 24, 27,
|
||||
16, 42, 16, 42, 6, 20, 6, 10, 16, 42, 16, 42, 28, 35, 28, 44,
|
||||
36, 39, 36, 39, 24, 41, 24, 27, 36, 39, 36, 39, 24, 41, 24, 27,
|
||||
37, 38, 37, 38, 30, 11, 30, 32, 37, 38, 37, 38, 25, 33, 25, 26, };
|
||||
|
||||
static TileOverride create(ResourceLocation propertiesFile, TileLoader tileLoader) {
|
||||
if (propertiesFile == null) {
|
||||
return null;
|
||||
@@ -353,6 +375,16 @@ public abstract class TileOverride implements Comparable<TileOverride> {
|
||||
return shouldConnect(renderBlockState, icon, renderBlockState.getOffset(blockFace, relativeDirection));
|
||||
}
|
||||
|
||||
protected final int getNeighborBits(RenderBlockState renderBlockState, IIcon origIcon) {
|
||||
int neighborBits = 0;
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
if (shouldConnect(renderBlockState, origIcon, bit)) {
|
||||
neighborBits |= (1 << bit);
|
||||
}
|
||||
}
|
||||
return neighborBits;
|
||||
}
|
||||
|
||||
private boolean shouldConnect(RenderBlockState renderBlockState, IIcon icon, int[] offset) {
|
||||
IBlockAccess blockAccess = renderBlockState.getBlockAccess();
|
||||
Block block = renderBlockState.getBlock();
|
||||
|
||||
@@ -22,21 +22,6 @@ public class TileOverrideImpl {
|
||||
|
||||
final static class CTM extends TileOverride {
|
||||
|
||||
// Index into this array is formed from these bit values:
|
||||
// 128 64 32
|
||||
// 1 * 16
|
||||
// 2 4 8
|
||||
private static final int[] neighborMap = new int[] { 0, 3, 0, 3, 12, 5, 12, 15, 0, 3, 0, 3, 12, 5, 12, 15, 1, 2,
|
||||
1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14, 0, 3, 0, 3, 12, 5, 12, 15, 0, 3, 0, 3, 12, 5, 12, 15, 1, 2,
|
||||
1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14, 36, 17, 36, 17, 24, 19, 24, 43, 36, 17, 36, 17, 24, 19, 24,
|
||||
43, 16, 18, 16, 18, 6, 46, 6, 21, 16, 18, 16, 18, 28, 9, 28, 22, 36, 17, 36, 17, 24, 19, 24, 43, 36, 17, 36,
|
||||
17, 24, 19, 24, 43, 37, 40, 37, 40, 30, 8, 30, 34, 37, 40, 37, 40, 25, 23, 25, 45, 0, 3, 0, 3, 12, 5, 12,
|
||||
15, 0, 3, 0, 3, 12, 5, 12, 15, 1, 2, 1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14, 0, 3, 0, 3, 12, 5, 12,
|
||||
15, 0, 3, 0, 3, 12, 5, 12, 15, 1, 2, 1, 2, 4, 7, 4, 29, 1, 2, 1, 2, 13, 31, 13, 14, 36, 39, 36, 39, 24, 41,
|
||||
24, 27, 36, 39, 36, 39, 24, 41, 24, 27, 16, 42, 16, 42, 6, 20, 6, 10, 16, 42, 16, 42, 28, 35, 28, 44, 36,
|
||||
39, 36, 39, 24, 41, 24, 27, 36, 39, 36, 39, 24, 41, 24, 27, 37, 38, 37, 38, 30, 11, 30, 32, 37, 38, 37, 38,
|
||||
25, 33, 25, 26, };
|
||||
|
||||
CTM(PropertiesFile properties, TileLoader tileLoader) {
|
||||
super(properties, tileLoader);
|
||||
}
|
||||
@@ -62,13 +47,7 @@ public class TileOverrideImpl {
|
||||
|
||||
@Override
|
||||
IIcon getTileWorld_Impl(RenderBlockState renderBlockState, IIcon origIcon) {
|
||||
int neighborBits = 0;
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
if (shouldConnect(renderBlockState, origIcon, bit)) {
|
||||
neighborBits |= (1 << bit);
|
||||
}
|
||||
}
|
||||
return icons[neighborMap[neighborBits]];
|
||||
return icons[neighborMap[getNeighborBits(renderBlockState, origIcon)]];
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -520,7 +499,7 @@ public class TileOverrideImpl {
|
||||
public CompactCtmQuadProcessor getProcessor() {
|
||||
CompactCtmQuadProcessor p = this.processor;
|
||||
if (p == null) {
|
||||
p = new CompactCtmQuadProcessor(icons, properties);
|
||||
p = new CompactCtmQuadProcessor(icons, properties, this);
|
||||
this.processor = p;
|
||||
}
|
||||
return p;
|
||||
|
||||
@@ -105,7 +105,6 @@ abstract public class TileOverrideIterator implements Iterator<TileOverride> {
|
||||
|
||||
public TileOverride go(RenderBlockState renderBlockState, IIcon origIcon) {
|
||||
CTMUtils.clearCurrentCompact();
|
||||
|
||||
this.renderBlockState = renderBlockState;
|
||||
renderBlockState.setFilter(null);
|
||||
currentIcon = origIcon;
|
||||
@@ -124,7 +123,7 @@ abstract public class TileOverrideIterator implements Iterator<TileOverride> {
|
||||
IIcon newIcon = getTile(override, renderBlockState, origIcon);
|
||||
if (newIcon != null) {
|
||||
if (override instanceof TileOverrideImpl.CTMCompact compact) {
|
||||
CTMUtils.setCurrentCompact(compact);
|
||||
CTMUtils.setCurrentCompact(compact, renderBlockState);
|
||||
}
|
||||
lastMatchedOverride = override;
|
||||
skipOverrides.add(override);
|
||||
|
||||
@@ -157,7 +157,7 @@ public class RenderBlocksUtils {
|
||||
return isSnow ? Blocks.snow_layer.getIcon(blockAccess, x, y, z, face) : topIcon;
|
||||
}
|
||||
|
||||
private static boolean isSnowCovered(IBlockAccess blockAccess, int x, int y, int z) {
|
||||
public static boolean isSnowCovered(IBlockAccess blockAccess, int x, int y, int z) {
|
||||
Block topBlock = blockAccess.getBlock(x, y + 1, z);
|
||||
return topBlock == Blocks.snow_layer || topBlock == Blocks.snow;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import jss.notfine.gui.options.control.NotFineControlValueFormatter;
|
||||
import jss.notfine.gui.options.named.AlwaysNever;
|
||||
import jss.notfine.gui.options.named.BackgroundSelect;
|
||||
import jss.notfine.gui.options.named.DownfallQuality;
|
||||
import jss.notfine.gui.options.named.FOVMode;
|
||||
import jss.notfine.gui.options.named.GraphicsQualityOff;
|
||||
import jss.notfine.gui.options.named.LeavesQuality;
|
||||
import jss.notfine.gui.options.named.GraphicsToggle;
|
||||
@@ -49,7 +50,7 @@ public enum Settings {
|
||||
SettingsManager.downfallDistanceUpdated();
|
||||
}
|
||||
},
|
||||
DYNAMIC_FOV(new NotFineOptionTickBox(true, null)),
|
||||
DYNAMIC_FOV(new NotFineOptionCycling<>(FOVMode.ALL, OptionImpact.VARIES)),
|
||||
HURT_SHAKE(new NotFineOptionSliderPercentage(100, 0, 300, 5, OptionImpact.LOW)),
|
||||
DYNAMIC_LIGHTS(new NotFineOptionCycling<>(DynamicLightsMode.FANCY, OptionImpact.VARIES)){
|
||||
@Override
|
||||
@@ -99,7 +100,7 @@ public enum Settings {
|
||||
ChunkRebuildManager.setMaxTicksWaiting((int) this.option.getStore());
|
||||
}
|
||||
},
|
||||
HORIZON(new NotFineOptionTickBox(true, OptionImpact.LOW)),
|
||||
HORIZON(new NotFineOptionTickBox(false, OptionImpact.LOW)),
|
||||
TERRAIN_FOG(new NotFineOptionTickBox(true, OptionImpact.LOW)),
|
||||
FOG_NEAR_DISTANCE(new NotFineOptionSliderPercentage(75, 1, 100, 1, OptionImpact.LOW)),
|
||||
GUI_BACKGROUND(new NotFineOptionCycling<>(BackgroundSelect.DEFAULT, null)) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package jss.notfine.gui.options.named;
|
||||
|
||||
import me.jellysquid.mods.sodium.client.gui.options.named.NamedState;
|
||||
|
||||
public enum FOVMode implements NamedState {
|
||||
ALL("options.fov.all"),
|
||||
MODS("options.fov.mods"),
|
||||
NONE("options.fov.none");
|
||||
|
||||
private final String name;
|
||||
|
||||
FOVMode(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,9 @@ import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ReeseSodiumVideoOptionsScreen extends SodiumOptionsGUI {
|
||||
private static final float ASPECT_RATIO = 5f / 4f;
|
||||
private static final int MINIMUM_WIDTH = 550;
|
||||
|
||||
@Nullable
|
||||
private Element focused;
|
||||
|
||||
@@ -88,10 +91,10 @@ public class ReeseSodiumVideoOptionsScreen extends SodiumOptionsGUI {
|
||||
protected BasicFrame.Builder parentFrameBuilder() {
|
||||
final BasicFrame.Builder basicFrameBuilder;
|
||||
|
||||
// Calculates if resolution exceeds 16:9 ratio, force 16:9
|
||||
// Clamp width on wide screens
|
||||
int newWidth = this.width;
|
||||
if ((float) this.width / (float) this.height > 1.77777777778) {
|
||||
newWidth = (int) (this.height * 1.77777777778);
|
||||
if (this.width > MINIMUM_WIDTH && (float) this.width / (float) this.height > ASPECT_RATIO) {
|
||||
newWidth = Math.max(MINIMUM_WIDTH, (int) (this.height * ASPECT_RATIO));
|
||||
}
|
||||
|
||||
final Dim2i basicFrameDim = new Dim2i((this.width - newWidth) / 2, 0, newWidth, this.height);
|
||||
|
||||
@@ -534,4 +534,61 @@ public class SodiumGameOptionPages {
|
||||
return new OptionPage(I18n.format("sodium.options.pages.appearance"), ImmutableList.copyOf(groups));
|
||||
}
|
||||
|
||||
public static OptionPage text() {
|
||||
final List<OptionGroup> groups = new ArrayList<>();
|
||||
|
||||
groups.add(OptionGroup.createBuilder()
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName("RGB Colors")
|
||||
.setTooltip("Enable hex colors via &#RRGGBB syntax")
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.enableRGBColors = value,
|
||||
opts -> AngelicaConfig.enableRGBColors)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName("Gradients")
|
||||
.setTooltip("Smooth color transitions via &g&#start&#end")
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.enableGradients = value,
|
||||
opts -> AngelicaConfig.enableGradients)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName("Rainbow")
|
||||
.setTooltip("Cycling rainbow colors via &q")
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.enableRainbow = value,
|
||||
opts -> AngelicaConfig.enableRainbow)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName("Wave Text")
|
||||
.setTooltip("Animated bouncing text via &z")
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.enableWaveText = value,
|
||||
opts -> AngelicaConfig.enableWaveText)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
|
||||
.setName("Wave Amplitude")
|
||||
.setTooltip("How far wave text bounces (default 2.0)")
|
||||
.setControl(option -> new SliderControl(option, 10, 80, 5, v -> (v / 10) + "." + (v % 10)))
|
||||
.setBinding((opts, value) -> AngelicaConfig.waveAmplitude = value / 10f, opts -> Math.round(AngelicaConfig.waveAmplitude * 10f))
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName("Dinnerbone Text")
|
||||
.setTooltip("Flipped upside down text via &v")
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.enableDinnerboneText = value,
|
||||
opts -> AngelicaConfig.enableDinnerboneText)
|
||||
.build())
|
||||
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
|
||||
.setName("Ampersand Conversion")
|
||||
.setTooltip("Convert & prefix codes to formatting codes at render time")
|
||||
.setControl(TickBoxControl::new)
|
||||
.setBinding((opts, value) -> AngelicaConfig.enableAmpersandConversion = value,
|
||||
opts -> AngelicaConfig.enableAmpersandConversion)
|
||||
.build())
|
||||
.build());
|
||||
|
||||
return new OptionPage("Text Effects", ImmutableList.copyOf(groups));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,6 +63,7 @@ public class SodiumOptionsGUI extends ScrollableGuiScreen {
|
||||
this.pages.add(SodiumGameOptionPages.advanced());
|
||||
this.pages.add(SodiumGameOptionPages.performance());
|
||||
this.pages.add(SodiumGameOptionPages.appearance());
|
||||
this.pages.add(SodiumGameOptionPages.text());
|
||||
|
||||
// Add Dynamic Lights page if enabled
|
||||
if (DynamicLights.configEnabled) {
|
||||
|
||||
@@ -7,8 +7,11 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceSet;
|
||||
import net.coderbot.iris.shaderpack.materialmap.BlockEntry;
|
||||
import net.coderbot.iris.shaderpack.materialmap.BlockRenderType;
|
||||
import net.coderbot.iris.shaderpack.materialmap.FlatteningMap;
|
||||
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.init.Blocks;
|
||||
@@ -19,37 +22,46 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class BlockMaterialMapping {
|
||||
|
||||
/** Meta-key bit OR'd in at runtime when a snowy-tagged block has snow above it. */
|
||||
public static final int SNOWY_META_BIT = 0x10;
|
||||
|
||||
/**
|
||||
* Creates a two-level map structure for block material IDs.
|
||||
* Based on Iris's BlockState mapping approach adapted for 1.7.10's metadata system.
|
||||
*/
|
||||
public static Reference2ObjectMap<Block, Int2IntMap> createBlockMetaIdMap(Int2ObjectMap<List<BlockEntry>> blockPropertiesMap) {
|
||||
Reference2ObjectMap<Block, Int2IntMap> blockMatches = new Reference2ObjectOpenHashMap<>();
|
||||
public static Reference2ObjectMap<Block, Int2IntMap> createBlockMetaIdMap(Int2ObjectMap<List<BlockEntry>> blockPropertiesMap, boolean skipFlattening) {
|
||||
final Reference2ObjectMap<Block, Int2IntMap> blockMatches = new Reference2ObjectOpenHashMap<>();
|
||||
final ReferenceSet<Block> snowyBlocks = new ReferenceOpenHashSet<>();
|
||||
|
||||
blockPropertiesMap.forEach((intId, entries) -> {
|
||||
for (BlockEntry entry : entries) {
|
||||
addBlockMetas(entry, blockMatches, intId);
|
||||
addBlockMetas(entry, blockMatches, intId, skipFlattening, snowyBlocks);
|
||||
}
|
||||
});
|
||||
|
||||
BlockRenderingSettings.INSTANCE.setHasSnowyEntries(!snowyBlocks.isEmpty());
|
||||
BlockRenderingSettings.INSTANCE.setSnowyBlocks(snowyBlocks);
|
||||
return blockMatches;
|
||||
}
|
||||
|
||||
public static Map<Block, BlockRenderLayer> createBlockTypeMap(Map<NamespacedId, BlockRenderType> blockPropertiesMap) {
|
||||
Map<Block, BlockRenderLayer> blockTypeIds = new Reference2ReferenceOpenHashMap<>();
|
||||
final Map<Block, BlockRenderLayer> blockTypeIds = new Reference2ReferenceOpenHashMap<>();
|
||||
|
||||
blockPropertiesMap.forEach((id, blockType) -> {
|
||||
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
|
||||
final Block block = Block.getBlockFromName(resourceLocation.toString());
|
||||
Block block = resolveBlockOrNull(id);
|
||||
|
||||
if (block == null || block == Blocks.air) {
|
||||
return;
|
||||
// Modern names like "grass_block" don't exist in 1.7.10's registry; fall back to flattening.
|
||||
if (block == null && "minecraft".equals(id.getNamespace())) {
|
||||
final List<BlockEntry> legacyEntries = FlatteningMap.toLegacy(id.getName(), Map.of());
|
||||
if (legacyEntries != null) {
|
||||
block = resolveBlockOrNull(legacyEntries.getFirst().getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (block == null) return;
|
||||
final BlockRenderLayer layer = convertBlockToRenderLayer(blockType);
|
||||
if (layer != null) {
|
||||
blockTypeIds.put(block, layer);
|
||||
}
|
||||
if (layer != null) blockTypeIds.put(block, layer);
|
||||
});
|
||||
|
||||
return blockTypeIds;
|
||||
@@ -72,20 +84,29 @@ public class BlockMaterialMapping {
|
||||
* Adds block+metadata combinations to the material ID map.
|
||||
* Based on Iris's addBlockStates method, adapted for 1.7.10 metadata system.
|
||||
*/
|
||||
private static void addBlockMetas(BlockEntry entry, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId) {
|
||||
private static void addBlockMetas(BlockEntry entry, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId, boolean skipFlattening, ReferenceSet<Block> snowyBlocks) {
|
||||
final NamespacedId id = entry.getId();
|
||||
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
|
||||
final Map<String, String> stateProps = entry.getStateProperties();
|
||||
final String snowy = stateProps.get("snowy");
|
||||
final int snowyBit = "true".equals(snowy) ? SNOWY_META_BIT : 0;
|
||||
|
||||
final Block block = (Block) Block.blockRegistry.getObject(resourceLocation.toString());
|
||||
|
||||
// If the block doesn't exist, by default the registry will return AIR. That probably isn't what we want.
|
||||
// TODO: Assuming that Registry.BLOCK.getDefaultId() == "minecraft:air" here
|
||||
if (block == null || block == Blocks.air) {
|
||||
return;
|
||||
// Vanilla modern names go through FlatteningMap; legacy-section packs and modded blocks
|
||||
// resolve directly from the registry.
|
||||
List<BlockEntry> targets = null;
|
||||
if (!skipFlattening && "minecraft".equals(id.getNamespace()) && (!stateProps.isEmpty() || entry.getMetas().isEmpty())) {
|
||||
targets = FlatteningMap.toLegacy(id.getName(), stateProps);
|
||||
}
|
||||
if (targets == null) targets = List.of(entry);
|
||||
|
||||
Set<Integer> metas = entry.getMetas();
|
||||
for (BlockEntry target : targets) {
|
||||
final Block block = resolveBlockOrNull(target.getId());
|
||||
if (block == null) continue;
|
||||
applyMetas(block, target.getMetas(), idMap, intId, snowyBit);
|
||||
if (snowy != null) snowyBlocks.add(block);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyMetas(Block block, Set<Integer> metas, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId, int snowyBit) {
|
||||
Int2IntMap metaMap = idMap.get(block);
|
||||
if (metaMap == null) {
|
||||
metaMap = new Int2IntOpenHashMap();
|
||||
@@ -94,15 +115,18 @@ public class BlockMaterialMapping {
|
||||
}
|
||||
|
||||
if (metas.isEmpty()) {
|
||||
// Add all metadata values (0-15) if there aren't any specific ones
|
||||
for (int meta = 0; meta < 16; meta++) {
|
||||
metaMap.putIfAbsent(meta, intId);
|
||||
}
|
||||
for (int meta = 0; meta < 16; meta++) metaMap.putIfAbsent(meta | snowyBit, intId);
|
||||
} else {
|
||||
// Add only specific metadata values
|
||||
for (int meta : metas) {
|
||||
metaMap.putIfAbsent(meta, intId);
|
||||
}
|
||||
for (int meta : metas) metaMap.putIfAbsent(meta | snowyBit, intId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the registered Block for an id, or null if unknown.
|
||||
* The registry returns Blocks.air (its default) for missing keys, which we coerce to null.
|
||||
*/
|
||||
private static Block resolveBlockOrNull(NamespacedId id) {
|
||||
final Block block = (Block) Block.blockRegistry.getObject(new ResourceLocation(id.getNamespace(), id.getName()).toString());
|
||||
return block == Blocks.air ? null : block;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import com.gtnewhorizons.angelica.rendering.celeritas.BlockRenderLayer;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntFunction;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceSet;
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceSets;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -19,12 +22,21 @@ public class BlockRenderingSettings {
|
||||
private boolean reloadRequired;
|
||||
private Reference2ObjectMap<Block, Int2IntMap> blockMetaMatches;
|
||||
private Map<Block, BlockRenderLayer> blockTypeIds;
|
||||
private Object2IntFunction<NamespacedId> entityIds;
|
||||
private Object2IntFunction<NamespacedId> itemIds;
|
||||
private float ambientOcclusionLevel;
|
||||
// note: no reload needed, entities are rebuilt every frame.
|
||||
@Setter
|
||||
private Object2IntFunction<NamespacedId> entityIds;
|
||||
// note: no reload needed, items are rendered every frame.
|
||||
@Setter
|
||||
private Object2IntFunction<NamespacedId> itemIds;
|
||||
@Getter
|
||||
private float ambientOcclusionLevel;
|
||||
private boolean disableDirectionalShading;
|
||||
private boolean useSeparateAo;
|
||||
private boolean useExtendedVertexFormat;
|
||||
@Setter
|
||||
private boolean hasSnowyEntries;
|
||||
@Getter
|
||||
private ReferenceSet<Block> snowyBlocks = ReferenceSets.emptySet();
|
||||
|
||||
public BlockRenderingSettings() {
|
||||
reloadRequired = false;
|
||||
@@ -34,6 +46,7 @@ public class BlockRenderingSettings {
|
||||
disableDirectionalShading = false;
|
||||
useSeparateAo = false;
|
||||
useExtendedVertexFormat = false;
|
||||
hasSnowyEntries = false;
|
||||
}
|
||||
|
||||
public void clearReloadRequired() {
|
||||
@@ -74,6 +87,14 @@ public class BlockRenderingSettings {
|
||||
this.blockMetaMatches = blockMetaIds;
|
||||
}
|
||||
|
||||
public boolean hasSnowyEntries() {
|
||||
return hasSnowyEntries;
|
||||
}
|
||||
|
||||
public void setSnowyBlocks(ReferenceSet<Block> snowyBlocks) {
|
||||
this.snowyBlocks = snowyBlocks != null ? snowyBlocks : ReferenceSets.emptySet();
|
||||
}
|
||||
|
||||
public void setBlockTypeIds(Map<Block, BlockRenderLayer> blockTypeIds) {
|
||||
if (this.blockTypeIds != null && this.blockTypeIds.equals(blockTypeIds)) {
|
||||
return;
|
||||
@@ -83,21 +104,7 @@ public class BlockRenderingSettings {
|
||||
this.blockTypeIds = blockTypeIds;
|
||||
}
|
||||
|
||||
public void setEntityIds(Object2IntFunction<NamespacedId> entityIds) {
|
||||
// note: no reload needed, entities are rebuilt every frame.
|
||||
this.entityIds = entityIds;
|
||||
}
|
||||
|
||||
public void setItemIds(Object2IntFunction<NamespacedId> itemIds) {
|
||||
// note: no reload needed, items are rendered every frame.
|
||||
this.itemIds = itemIds;
|
||||
}
|
||||
|
||||
public float getAmbientOcclusionLevel() {
|
||||
return ambientOcclusionLevel;
|
||||
}
|
||||
|
||||
public void setAmbientOcclusionLevel(float ambientOcclusionLevel) {
|
||||
public void setAmbientOcclusionLevel(float ambientOcclusionLevel) {
|
||||
if (ambientOcclusionLevel == this.ambientOcclusionLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package net.coderbot.iris.gl.program;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.coderbot.iris.gl.GlResource;
|
||||
import net.coderbot.iris.gl.blending.DepthColorStorage;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
|
||||
public final class Program extends GlResource {
|
||||
|
||||
@Getter
|
||||
private final ProgramUniforms uniforms;
|
||||
|
||||
@Getter
|
||||
private final ProgramSamplers samplers;
|
||||
|
||||
@Getter
|
||||
private final ProgramImages images;
|
||||
|
||||
Program(int program, ProgramUniforms uniforms, ProgramSamplers samplers, ProgramImages images) {
|
||||
@@ -18,15 +25,15 @@ public final class Program extends GlResource {
|
||||
|
||||
DepthColorStorage.registerOwnedProgram(program);
|
||||
}
|
||||
|
||||
|
||||
public void use() {
|
||||
GLStateManager.glUseProgram(getGlId());
|
||||
|
||||
|
||||
uniforms.update();
|
||||
samplers.update();
|
||||
images.update();
|
||||
}
|
||||
|
||||
|
||||
public static void unbind() {
|
||||
ProgramUniforms.clearActiveUniforms();
|
||||
ProgramSamplers.clearActiveSamplers();
|
||||
@@ -38,12 +45,7 @@ public final class Program extends GlResource {
|
||||
DepthColorStorage.unregisterOwnedProgram(getGlId());
|
||||
GLStateManager.glDeleteProgram(getGlId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the OpenGL ID of this program.
|
||||
* @deprecated this should be encapsulated eventually
|
||||
*/
|
||||
@Deprecated
|
||||
|
||||
public int getProgramId() {
|
||||
return getGlId();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ public class StateUpdateNotifiers {
|
||||
public static ValueUpdateNotifier fogStartNotifier;
|
||||
public static ValueUpdateNotifier fogEndNotifier;
|
||||
public static ValueUpdateNotifier fogDensityNotifier;
|
||||
public static ValueUpdateNotifier alphaFuncNotifier;
|
||||
public static ValueUpdateNotifier alphaTestNotifier;
|
||||
public static ValueUpdateNotifier blendFuncNotifier;
|
||||
public static ValueUpdateNotifier bindTextureNotifier;
|
||||
public static ValueUpdateNotifier normalTextureChangeNotifier;
|
||||
|
||||
@@ -181,9 +181,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
private final boolean allowConcurrentCompute;
|
||||
private final OptionalInt forcedShadowRenderDistanceChunks;
|
||||
private final CloudSetting dhCloudSetting;
|
||||
|
||||
private Pass current = null;
|
||||
|
||||
private WorldRenderingPhase overridePhase = null;
|
||||
private WorldRenderingPhase phase = WorldRenderingPhase.NONE;
|
||||
private boolean isBeforeTranslucent;
|
||||
@@ -279,7 +277,9 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
holder -> CommonUniforms.addNonDynamicUniforms(holder, programs.getPack().getIdMap(), programs.getPackDirectives(), this.updateNotifier)
|
||||
);
|
||||
|
||||
BlockRenderingSettings.INSTANCE.setBlockMetaMatches(BlockMaterialMapping.createBlockMetaIdMap(programs.getPack().getIdMap().getBlockProperties()));
|
||||
BlockRenderingSettings.INSTANCE.setBlockMetaMatches(BlockMaterialMapping.createBlockMetaIdMap(
|
||||
programs.getPack().getIdMap().getBlockProperties(),
|
||||
programs.getPack().getIdMap().hasLegacySection()));
|
||||
BlockRenderingSettings.INSTANCE.setBlockTypeIds(BlockMaterialMapping.createBlockTypeMap(programs.getPack().getIdMap().getBlockRenderTypeMap()));
|
||||
|
||||
BlockRenderingSettings.INSTANCE.setEntityIds(programs.getPack().getIdMap().getEntityIdMap());
|
||||
@@ -767,7 +767,11 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
public boolean shouldOverrideShaders() {
|
||||
return isRenderingWorld && !isRenderingFullScreenPass && !isPostChain && isMainBound;
|
||||
}
|
||||
|
||||
|
||||
public Pass getActivePassProgram() {
|
||||
return current;
|
||||
}
|
||||
|
||||
public int getActivePassProgramId() {
|
||||
if (current == null) return -1;
|
||||
final Program p = current.getProgram();
|
||||
@@ -1048,7 +1052,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
|
||||
return shadowRenderTargets != null;
|
||||
}
|
||||
|
||||
private final class Pass {
|
||||
public final class Pass {
|
||||
@Nullable
|
||||
private final Program program;
|
||||
private final GlFramebuffer framebufferBeforeTranslucents;
|
||||
|
||||
@@ -46,7 +46,22 @@ public class CommonTransformer {
|
||||
// runtime discard using the GLSM-tracked alpha reference value.
|
||||
if (found.contains(0) && parameters.patch != Patch.COMPOSITE && parameters.patch != Patch.COMPUTE) {
|
||||
root.injectVariable("uniform float iris_currentAlphaTest;");
|
||||
root.appendMain("if (iris_FragData0.a <= iris_currentAlphaTest) discard;");
|
||||
root.injectVariable("uniform int iris_currentAlphaFunc;");
|
||||
root.injectFunction(
|
||||
"bool iris_alphaTestPass(float a) {" +
|
||||
" if (iris_currentAlphaFunc == 7) return true;" + // ALWAYS / disabled
|
||||
" if (iris_currentAlphaFunc == 0) return false;" + // NEVER
|
||||
" float qa = round(a * 255.0);" +
|
||||
" float qr = round(iris_currentAlphaTest * 255.0);" +
|
||||
" if (iris_currentAlphaFunc == 1) return qa < qr;" + // LESS
|
||||
" if (iris_currentAlphaFunc == 2) return qa == qr;" + // EQUAL
|
||||
" if (iris_currentAlphaFunc == 3) return qa <= qr;" + // LEQUAL
|
||||
" if (iris_currentAlphaFunc == 4) return qa > qr;" + // GREATER
|
||||
" if (iris_currentAlphaFunc == 5) return qa != qr;" + // NOTEQUAL
|
||||
" return qa >= qr;" + // GEQUAL (6)
|
||||
"}"
|
||||
);
|
||||
root.appendMain("if (!iris_alphaTestPass(iris_FragData0.a)) discard;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import it.unimi.dsi.fastutil.objects.Object2IntFunction;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import lombok.Getter;
|
||||
import net.coderbot.iris.Iris;
|
||||
import net.coderbot.iris.shaderpack.materialmap.BlockEntry;
|
||||
import net.coderbot.iris.shaderpack.materialmap.EntityFlatteningMap;
|
||||
import net.coderbot.iris.shaderpack.materialmap.BlockRenderType;
|
||||
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
|
||||
import net.coderbot.iris.shaderpack.option.ShaderPackOptions;
|
||||
@@ -33,6 +35,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A utility class for parsing entries in item.properties, block.properties, and entities.properties files in shaderpacks
|
||||
@@ -56,16 +59,27 @@ public class IdMap {
|
||||
/**
|
||||
* A set of render type overrides for specific blocks. Allows shader packs to move blocks to different render types.
|
||||
*/
|
||||
private Map<NamespacedId, BlockRenderType> blockRenderTypeMap;
|
||||
@Getter
|
||||
private Map<NamespacedId, BlockRenderType> blockRenderTypeMap;
|
||||
|
||||
/** True when block.properties contained a preprocessor directive guarding a 1.7.10-specific section. */
|
||||
private final boolean hasLegacySection;
|
||||
|
||||
public boolean hasLegacySection() {
|
||||
return hasLegacySection;
|
||||
}
|
||||
|
||||
private static final Pattern LEGACY_DIRECTIVE_PATTERN = Pattern.compile(
|
||||
"(?m)^\\s*#\\s*(?:if|elif|ifdef|ifndef)\\b[^\\n]*\\bMC_VERSION\\b[^\\n]*\\b10710\\b");
|
||||
|
||||
IdMap(Path shaderPath, ShaderPackOptions shaderPackOptions, Iterable<StringPair> environmentDefines) {
|
||||
// Check if block.properties has a dedicated 1.7.10 section
|
||||
String rawBlockProperties = readProperties(shaderPath, "block.properties");
|
||||
boolean hasLegacySection = rawBlockProperties != null
|
||||
&& rawBlockProperties.contains("MC_VERSION") && rawBlockProperties.contains("10710");
|
||||
this.hasLegacySection = rawBlockProperties != null
|
||||
&& LEGACY_DIRECTIVE_PATTERN.matcher(rawBlockProperties).find();
|
||||
|
||||
Iterable<StringPair> resolvedDefines;
|
||||
if (hasLegacySection) {
|
||||
if (this.hasLegacySection) {
|
||||
// Pack has a 1.7.10 section
|
||||
resolvedDefines = environmentDefines;
|
||||
loadProperties(shaderPath, "block.properties", shaderPackOptions, environmentDefines).ifPresent(blockProperties -> {
|
||||
@@ -89,7 +103,6 @@ public class IdMap {
|
||||
loadProperties(shaderPath, "block.properties", shaderPackOptions, modernDefines).ifPresent(blockProperties -> {
|
||||
blockPropertiesMap = parseBlockMap(blockProperties, "block.", "block.properties");
|
||||
blockRenderTypeMap = parseRenderTypeMap(blockProperties, "layer.", "block.properties");
|
||||
blockPropertiesMap = LegacyIdMap.convertModernBlockEntries(blockPropertiesMap);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,7 +154,7 @@ public class IdMap {
|
||||
private static String readProperties(Path shaderPath, String name) {
|
||||
try {
|
||||
// ID maps should be encoded in ISO_8859_1.
|
||||
return new String(Files.readAllBytes(shaderPath.resolve(name)), StandardCharsets.ISO_8859_1);
|
||||
return Files.readString(shaderPath.resolve(name), StandardCharsets.ISO_8859_1);
|
||||
} catch (NoSuchFileException e) {
|
||||
Iris.logger.debug("An " + name + " file was not found in the current shaderpack");
|
||||
|
||||
@@ -158,7 +171,24 @@ public class IdMap {
|
||||
}
|
||||
|
||||
private static Object2IntMap<NamespacedId> parseEntityIdMap(Properties properties) {
|
||||
return parseIdMap(properties, "entity.", "entity.properties");
|
||||
Object2IntMap<NamespacedId> idMap = parseIdMap(properties, "entity.", "entity.properties");
|
||||
|
||||
// For modern shader packs: translate modern entity names to 1.7.10 names
|
||||
// so runtime lookups match EntityList's registered names directly.
|
||||
Object2IntMap<NamespacedId> augmented = new Object2IntOpenHashMap<>(idMap);
|
||||
augmented.defaultReturnValue(-1);
|
||||
|
||||
for (Object2IntMap.Entry<NamespacedId> entry : idMap.object2IntEntrySet()) {
|
||||
NamespacedId id = entry.getKey();
|
||||
if ("minecraft".equals(id.getNamespace())) {
|
||||
String legacyName = EntityFlatteningMap.toLegacy(id.getName());
|
||||
if (legacyName != null) {
|
||||
augmented.putIfAbsent(new NamespacedId(legacyName), entry.getIntValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object2IntMaps.unmodifiable(augmented);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +240,7 @@ public class IdMap {
|
||||
} else if (c == '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (Character.isWhitespace(c) && !inQuotes) {
|
||||
if (current.length() > 0) {
|
||||
if (!current.isEmpty()) {
|
||||
result.add(current.toString());
|
||||
current.setLength(0);
|
||||
}
|
||||
@@ -230,7 +260,7 @@ public class IdMap {
|
||||
}
|
||||
|
||||
// Add final token
|
||||
if (current.length() > 0) {
|
||||
if (!current.isEmpty()) {
|
||||
result.add(current.toString());
|
||||
}
|
||||
|
||||
@@ -382,11 +412,7 @@ public class IdMap {
|
||||
return entityIdMap;
|
||||
}
|
||||
|
||||
public Map<NamespacedId, BlockRenderType> getBlockRenderTypeMap() {
|
||||
return blockRenderTypeMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
|
||||
@@ -8,5 +8,5 @@ public class IrisLimits {
|
||||
* It's not recommended to raise this higher than 16 until code for avoiding allocation of unused color textures
|
||||
* is implemented.
|
||||
*/
|
||||
public static final int MAX_COLOR_BUFFERS = 16;
|
||||
public static final int MAX_COLOR_BUFFERS = 32;
|
||||
}
|
||||
@@ -1,131 +1,49 @@
|
||||
package net.coderbot.iris.shaderpack;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import it.unimi.dsi.fastutil.Function;
|
||||
import cpw.mods.fml.common.registry.GameData;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.coderbot.iris.shaderpack.materialmap.BlockEntry;
|
||||
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
|
||||
import net.minecraft.block.Block;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class LegacyIdMap {
|
||||
private static final ImmutableList<String> COLORS =
|
||||
ImmutableList.of("white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray",
|
||||
"light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black");
|
||||
|
||||
private static final ImmutableList<String> WOOD_TYPES =
|
||||
ImmutableList.of("oak", "birch", "jungle", "spruce", "acacia", "dark_oak");
|
||||
|
||||
private static final Map<String, String> MODERN_TO_LEGACY_BLOCK_IDS = Map.of(
|
||||
"grass", "tallgrass",
|
||||
"grass_block", "grass"
|
||||
);
|
||||
|
||||
/**
|
||||
* Fills the block ID map with all vanilla blocks in the case where block.properties is not found.
|
||||
*
|
||||
* ShadersMod passed block IDs as a signed byte, so IDs 128-255 wrapped to -128 to -1.
|
||||
* For example, emerald_block (ID 133) becomes -123. Shader packs like SEUS v11 were written
|
||||
* against this behavior, so we replicate it here.
|
||||
*/
|
||||
public static void addLegacyValues(Int2ObjectMap<List<BlockEntry>> blockIdMap) {
|
||||
add(blockIdMap, 1, block("stone"), block("granite"), block("diorite"), block("andesite"));
|
||||
add(blockIdMap, 2, block("grass_block"));
|
||||
add(blockIdMap, 4, block("cobblestone"));
|
||||
|
||||
add(blockIdMap, 50, block("torch"));
|
||||
add(blockIdMap, 89, block("glowstone"));
|
||||
|
||||
// TODO: what about inactive redstone lamps?
|
||||
add(blockIdMap, 124, block("redstone_lamp"));
|
||||
|
||||
add(blockIdMap, 12, block("sand"));
|
||||
add(blockIdMap, 24, block("sandstone"));
|
||||
|
||||
add(blockIdMap, 41, block("gold_block"));
|
||||
add(blockIdMap, 42, block("iron_block"));
|
||||
add(blockIdMap, 57, block("diamond_block"));
|
||||
// Apparently this is what SEUS v11 expects? Maybe old shadersmod was buggy.
|
||||
add(blockIdMap, -123, block("emerald_block"));
|
||||
|
||||
addMany(blockIdMap, 35, COLORS, color -> block(color + "_wool"));
|
||||
|
||||
// NB: Use the "still" IDs for water and lava, since some shader packs don't properly support the "flowing"
|
||||
// versions: https://github.com/IrisShaders/Iris/issues/1462
|
||||
add(blockIdMap, 9, block("water"));
|
||||
add(blockIdMap, 11, block("lava"));
|
||||
add(blockIdMap, 79, block("ice"));
|
||||
|
||||
addMany(blockIdMap, 18, WOOD_TYPES, woodType -> block(woodType + "_leaves"));
|
||||
|
||||
addMany(blockIdMap, 95, COLORS, color -> block(color + "_stained_glass"));
|
||||
addMany(blockIdMap, 160, COLORS, color -> block(color + "_stained_glass_pane"));
|
||||
|
||||
// Short grass / bush
|
||||
add(blockIdMap, 31, block("grass"), block("seagrass"), block("sweet_berry_bush"));
|
||||
|
||||
// Crops (59 = wheat), but we include carrots and potatoes too.
|
||||
add(blockIdMap, 59, block("wheat"), block("carrots"), block("potatoes"));
|
||||
|
||||
// Small flowers
|
||||
add(blockIdMap, 37, block("dandelion"), block("poppy"), block("blue_orchid"),
|
||||
block("allium"), block("azure_bluet"), block("red_tulip"), block("pink_tulip"),
|
||||
block("white_tulip"), block("orange_tulip"), block("oxeye_daisy"),
|
||||
block("cornflower"), block("lily_of_the_valley"), block("wither_rose"));
|
||||
|
||||
// Big tall grass / flowers
|
||||
// Also include seagrass here
|
||||
add(blockIdMap, 175, block("sunflower"), block("lilac"), block("tall_grass"),
|
||||
block("large_fern"), block("rose_bush"), block("peony"), block("tall_seagrass"));
|
||||
|
||||
// Fire
|
||||
add(blockIdMap, 51, block("fire"));
|
||||
|
||||
// Lily pad
|
||||
add(blockIdMap, 111, block("lily_pad"));
|
||||
|
||||
// TODO: 76 -> redstone_torch (on)
|
||||
}
|
||||
|
||||
public static Int2ObjectMap<List<BlockEntry>> convertModernBlockEntries(Int2ObjectMap<List<BlockEntry>> blockIdMap) {
|
||||
Int2ObjectMap<List<BlockEntry>> converted = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
blockIdMap.forEach((intId, entries) -> {
|
||||
List<BlockEntry> convertedEntries = new ArrayList<>(entries.size());
|
||||
for (BlockEntry entry : entries) {
|
||||
NamespacedId id = entry.getId();
|
||||
if (!"minecraft".equals(id.getNamespace())) {
|
||||
convertedEntries.add(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
String convertedName = MODERN_TO_LEGACY_BLOCK_IDS.get(id.getName());
|
||||
if (convertedName == null) {
|
||||
convertedEntries.add(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
convertedEntries.add(new BlockEntry(new NamespacedId(id.getNamespace(), convertedName), entry.getMetas()));
|
||||
for (String name : (Iterable<String>) GameData.getBlockRegistry().getKeys()) {
|
||||
// Only include vanilla minecraft blocks
|
||||
if (!name.startsWith("minecraft:")) {
|
||||
continue;
|
||||
}
|
||||
converted.put(intId, Collections.unmodifiableList(convertedEntries));
|
||||
});
|
||||
|
||||
return converted;
|
||||
}
|
||||
final Block block = (Block) GameData.getBlockRegistry().getObject(name);
|
||||
if (block == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
private static BlockEntry block(String name) {
|
||||
return new BlockEntry(new NamespacedId("minecraft", name), Collections.emptySet());
|
||||
}
|
||||
final int id = GameData.getBlockRegistry().getIDForObject(block);
|
||||
if (id <= 0 || id > 255) {
|
||||
continue;
|
||||
}
|
||||
|
||||
private static void addMany(Int2ObjectMap<List<BlockEntry>> blockIdMap, int id, List<String> prefixes, Function<String, BlockEntry> toId) {
|
||||
List<BlockEntry> entries = new ArrayList<>();
|
||||
// Apply signed byte overflow
|
||||
final int overflowId = (byte) id;
|
||||
|
||||
for (String prefix : prefixes) {
|
||||
entries.add(toId.apply(prefix));
|
||||
final String blockName = name.substring("minecraft:".length());
|
||||
final BlockEntry entry = new BlockEntry(new NamespacedId("minecraft", blockName), Collections.emptySet());
|
||||
|
||||
blockIdMap.computeIfAbsent(overflowId, k -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
|
||||
blockIdMap.put(id, entries);
|
||||
}
|
||||
|
||||
private static void add(Int2ObjectMap<List<BlockEntry>> blockIdMap, int id, BlockEntry... entries) {
|
||||
blockIdMap.put(id, Arrays.asList(entries));
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -14,10 +16,16 @@ import java.util.Set;
|
||||
public class BlockEntry {
|
||||
private final NamespacedId id;
|
||||
private final Set<Integer> metas;
|
||||
private final Map<String, String> stateProperties;
|
||||
|
||||
public BlockEntry(NamespacedId id, Set<Integer> metas) {
|
||||
public BlockEntry(NamespacedId id, Set<Integer> metas) {
|
||||
this(id, metas, Collections.emptyMap());
|
||||
}
|
||||
|
||||
public BlockEntry(NamespacedId id, Set<Integer> metas, Map<String, String> stateProperties) {
|
||||
this.id = id;
|
||||
this.metas = metas;
|
||||
this.stateProperties = stateProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,60 +47,66 @@ public class BlockEntry {
|
||||
return new BlockEntry(new NamespacedId("minecraft", entry), Collections.emptySet());
|
||||
}
|
||||
|
||||
// Examples of what we'll accept
|
||||
// Examples of what we accept:
|
||||
// stone
|
||||
// stone:0
|
||||
// minecraft:stone:0
|
||||
// minecraft:stone:0,1,2 # Theoretically valid, but I haven't seen any examples in actual shaders
|
||||
// minecraft:stone:0,1,2
|
||||
// minecraft:furnace:lit=true (blockstate property)
|
||||
// minecraft:oak_log:axis=y (blockstate property)
|
||||
|
||||
// Examples of what we don't (Yet?) accept - Seems to be from MC 1.8+
|
||||
// minecraft:farmland:moisture=0
|
||||
// minecraft:farmland:moisture=1
|
||||
// minecraft:double_plant:half=lower
|
||||
// minecraft:double_plant:half=upper
|
||||
// minecraft:grass:snowy=true
|
||||
// minecraft:unpowered_comparator:powered=false
|
||||
|
||||
|
||||
// Less trivial case: no metas involved, just a namespace
|
||||
//
|
||||
// The first term MUST be a valid ResourceLocation component
|
||||
// The second term, if it is not numeric, must be a valid ResourceLocation component.
|
||||
if (splitStates.length == 2 && !StringUtils.isNumeric(splitStates[1].substring(0, 1))) {
|
||||
// Less trivial case: no metas or state properties, just a namespace
|
||||
if (splitStates.length == 2 && !StringUtils.isNumeric(splitStates[1].substring(0, 1))
|
||||
&& !splitStates[1].contains("=")) {
|
||||
return new BlockEntry(new NamespacedId(splitStates[0], splitStates[1]), Collections.emptySet());
|
||||
}
|
||||
|
||||
// Complex case: One or more states involved...
|
||||
// Complex case: metas and/or state properties
|
||||
final int statesStart;
|
||||
final NamespacedId id;
|
||||
|
||||
if (StringUtils.isNumeric(splitStates[1].substring(0, 1))) {
|
||||
// We have an entry of the form "stone:0"
|
||||
if (splitStates.length == 2) {
|
||||
// "stone:0" or "stone:lit=true"
|
||||
statesStart = 1;
|
||||
id = new NamespacedId("minecraft", splitStates[0]);
|
||||
} else if (StringUtils.isNumeric(splitStates[1].substring(0, 1)) || splitStates[1].contains("=")) {
|
||||
// "stone:0:something" or "stone:lit=true:something" — unlikely but handle it
|
||||
statesStart = 1;
|
||||
id = new NamespacedId("minecraft", splitStates[0]);
|
||||
} else {
|
||||
// We have an entry of the form "minecraft:stone:0"
|
||||
// "minecraft:stone:0" or "minecraft:furnace:lit=true"
|
||||
statesStart = 2;
|
||||
id = new NamespacedId(splitStates[0], splitStates[1]);
|
||||
}
|
||||
|
||||
final Set<Integer> metas = new HashSet<>();
|
||||
final Set<Integer> metas = new HashSet<>();
|
||||
final Map<String, String> properties = new HashMap<>();
|
||||
|
||||
for (int index = statesStart; index < splitStates.length; index++) {
|
||||
// Parse out one or more metadata ids
|
||||
final String[] metaParts = splitStates[index].split(",");
|
||||
String segment = splitStates[index];
|
||||
|
||||
for (String metaPart : metaParts) {
|
||||
try {
|
||||
metas.add(Integer.parseInt(metaPart));
|
||||
} catch (NumberFormatException e) {
|
||||
Iris.logger.warn("Warning: the block ID map entry \"" + entry + "\" could not be fully parsed:");
|
||||
Iris.logger.warn("- Metadata ids must be a comma separated list of one or more integers, but "+ splitStates[index] + " is not of that form!");
|
||||
}
|
||||
}
|
||||
if (segment.contains("=")) {
|
||||
// Blockstate property: key=value
|
||||
for (String prop : segment.split(",")) {
|
||||
int eq = prop.indexOf('=');
|
||||
if (eq > 0 && eq < prop.length() - 1) {
|
||||
properties.put(prop.substring(0, eq), prop.substring(eq + 1));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Metadata IDs: comma-separated integers
|
||||
for (String metaPart : segment.split(",")) {
|
||||
try {
|
||||
metas.add(Integer.parseInt(metaPart));
|
||||
} catch (NumberFormatException e) {
|
||||
Iris.logger.warn("Warning: the block ID map entry \"" + entry + "\" could not be fully parsed:");
|
||||
Iris.logger.warn("- Metadata ids must be a comma separated list of one or more integers, but " + segment + " is not of that form!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockEntry(id, metas);
|
||||
return new BlockEntry(id, metas, properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -100,11 +114,11 @@ public class BlockEntry {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final BlockEntry that = (BlockEntry) o;
|
||||
return Objects.equals(id, that.id) && Objects.equals(metas, that.metas);
|
||||
return Objects.equals(id, that.id) && Objects.equals(metas, that.metas) && Objects.equals(stateProperties, that.stateProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, metas);
|
||||
return Objects.hash(id, metas, stateProperties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.coderbot.iris.shaderpack.materialmap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Maps modern entity names to their 1.7.10 equivalents.
|
||||
*
|
||||
* In 1.11+, entity IDs were changed to namespaced: "minecraft:cave_spider".
|
||||
* In 1.7.10, entities are registered with names like "CaveSpider".
|
||||
* Some entities were also renamed or split into multiple types.
|
||||
*
|
||||
* This map is used at lookup time: when a modern shader pack references an entity by its
|
||||
* modern name, we resolve it to the 1.7.10 name that EntityList uses.
|
||||
*/
|
||||
public class EntityFlatteningMap {
|
||||
|
||||
/** Maps modern entity name -> 1.7.10 entity name. */
|
||||
private static final Map<String, String> MODERN_TO_LEGACY = new HashMap<>();
|
||||
|
||||
static {
|
||||
// Simple case changes
|
||||
simple("Arrow", "arrow");
|
||||
simple("Bat", "bat");
|
||||
simple("Blaze", "blaze");
|
||||
simple("Boat", "boat");
|
||||
simple("Chicken", "chicken");
|
||||
simple("Cow", "cow");
|
||||
simple("Creeper", "creeper");
|
||||
simple("Enderman", "enderman");
|
||||
simple("Ghast", "ghast");
|
||||
simple("Giant", "giant");
|
||||
simple("Item", "item");
|
||||
simple("Painting", "painting");
|
||||
simple("Pig", "pig");
|
||||
simple("Sheep", "sheep");
|
||||
simple("Silverfish", "silverfish");
|
||||
simple("Skeleton", "skeleton");
|
||||
simple("Slime", "slime");
|
||||
simple("Snowball", "snowball");
|
||||
simple("Spider", "spider");
|
||||
simple("Squid", "squid");
|
||||
simple("Villager", "villager");
|
||||
simple("Witch", "witch");
|
||||
simple("Wolf", "wolf");
|
||||
simple("Zombie", "zombie");
|
||||
|
||||
// Renamed entities
|
||||
simple("CaveSpider", "cave_spider");
|
||||
simple("EnderCrystal", "end_crystal");
|
||||
simple("EnderDragon", "ender_dragon");
|
||||
simple("EntityHorse", "horse");
|
||||
simple("EyeOfEnderSignal", "eye_of_ender");
|
||||
simple("FallingSand", "falling_block");
|
||||
simple("Fireball", "fireball");
|
||||
simple("FireworksRocketEntity", "firework_rocket");
|
||||
simple("ItemFrame", "item_frame");
|
||||
simple("LavaSlime", "magma_cube");
|
||||
simple("LeashKnot", "leash_knot");
|
||||
simple("MushroomCow", "mooshroom");
|
||||
simple("Ozelot", "ocelot");
|
||||
simple("PigZombie", "zombified_piglin");
|
||||
simple("PrimedTnt", "tnt");
|
||||
simple("SmallFireball", "small_fireball");
|
||||
simple("SnowMan", "snow_golem");
|
||||
simple("ThrownEnderpearl", "ender_pearl");
|
||||
simple("ThrownExpBottle", "experience_bottle");
|
||||
simple("ThrownPotion", "potion");
|
||||
simple("VillagerGolem", "iron_golem");
|
||||
simple("WitherBoss", "wither");
|
||||
simple("WitherSkull", "wither_skull");
|
||||
simple("XPOrb", "experience_orb");
|
||||
|
||||
// Minecarts
|
||||
simple("MinecartRideable", "minecart");
|
||||
simple("MinecartChest", "chest_minecart");
|
||||
simple("MinecartCommandBlock", "command_block_minecart");
|
||||
simple("MinecartFurnace", "furnace_minecart");
|
||||
simple("MinecartHopper", "hopper_minecart");
|
||||
simple("MinecartSpawner", "spawner_minecart");
|
||||
simple("MinecartTNT", "tnt_minecart");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 1.7.10 entity name for a modern entity name, or null if no mapping exists.
|
||||
* The input name should not include the "minecraft:" namespace prefix.
|
||||
*/
|
||||
public static String toLegacy(String modernName) {
|
||||
return MODERN_TO_LEGACY.get(modernName);
|
||||
}
|
||||
|
||||
private static void simple(String legacy, String modern) {
|
||||
MODERN_TO_LEGACY.put(modern, legacy);
|
||||
}
|
||||
}
|
||||
@@ -128,9 +128,9 @@ public class IrisExclusiveUniforms {
|
||||
|
||||
public static class WorldInfoUniforms {
|
||||
public static void addWorldInfoUniforms(UniformHolder uniforms) {
|
||||
final WorldClient level = Minecraft.getMinecraft().theWorld;
|
||||
uniforms.uniform1i(UniformUpdateFrequency.PER_FRAME, "bedrockLevel", () -> 0);
|
||||
uniforms.uniform1f(UniformUpdateFrequency.PER_FRAME, "cloudHeight", () -> {
|
||||
final WorldClient level = Minecraft.getMinecraft().theWorld;
|
||||
if (level != null && level.provider != null) {
|
||||
return level.provider.getCloudHeight();
|
||||
} else {
|
||||
@@ -138,6 +138,7 @@ public class IrisExclusiveUniforms {
|
||||
}
|
||||
});
|
||||
uniforms.uniform1i(UniformUpdateFrequency.PER_FRAME, "heightLimit", () -> {
|
||||
final WorldClient level = Minecraft.getMinecraft().theWorld;
|
||||
if (level != null && level.provider != null) {
|
||||
return level.provider.getHeight();
|
||||
} else {
|
||||
@@ -145,6 +146,7 @@ public class IrisExclusiveUniforms {
|
||||
}
|
||||
});
|
||||
uniforms.uniform1b(UniformUpdateFrequency.PER_FRAME, "hasCeiling", () -> {
|
||||
final WorldClient level = Minecraft.getMinecraft().theWorld;
|
||||
if (level != null && level.provider != null) {
|
||||
return level.provider.hasNoSky;
|
||||
} else {
|
||||
@@ -152,6 +154,7 @@ public class IrisExclusiveUniforms {
|
||||
}
|
||||
});
|
||||
uniforms.uniform1b(UniformUpdateFrequency.PER_FRAME, "hasSkylight", () -> {
|
||||
final WorldClient level = Minecraft.getMinecraft().theWorld;
|
||||
if (level != null && level.provider != null) {
|
||||
return !level.provider.hasNoSky;
|
||||
} else {
|
||||
@@ -159,6 +162,7 @@ public class IrisExclusiveUniforms {
|
||||
}
|
||||
});
|
||||
uniforms.uniform1f(UniformUpdateFrequency.PER_FRAME, "ambientLight", () -> {
|
||||
final WorldClient level = Minecraft.getMinecraft().theWorld;
|
||||
if (level != null && level.provider != null) {
|
||||
return level.provider.lightBrightnessTable[0];
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.coderbot.iris.uniforms;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import net.coderbot.iris.gl.state.FogMode;
|
||||
import net.coderbot.iris.gl.state.StateUpdateNotifiers;
|
||||
import net.coderbot.iris.gl.uniform.DynamicUniformHolder;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector4f;
|
||||
@@ -22,6 +23,12 @@ public class IrisInternalUniforms {
|
||||
return GLStateManager.getAlphaState().getReference();
|
||||
}
|
||||
|
||||
private static int getEffectiveAlphaFunc() {
|
||||
if (!GLStateManager.getAlphaTest().isEnabled()) return 7;
|
||||
final int func = GLStateManager.getAlphaState().getFunction();
|
||||
return func & 0x7;
|
||||
}
|
||||
|
||||
public static void addFogUniforms(DynamicUniformHolder uniforms, FogMode fogMode) {
|
||||
uniforms.uniform4f(PER_FRAME, "iris_FogColor", () -> {
|
||||
final Vector3d color = GLStateManager.getFogState().getFogColor();
|
||||
@@ -34,7 +41,8 @@ public class IrisInternalUniforms {
|
||||
.uniform1f(PER_FRAME, "iris_FogDensity", () -> Math.max(0.0F, GLStateManager.getFogState().getDensity()));
|
||||
|
||||
uniforms
|
||||
.uniform1f(PER_FRAME, "iris_currentAlphaTest", IrisInternalUniforms::getEffectiveAlphaRef)
|
||||
.uniform1f(PER_FRAME, "alphaTestRef", IrisInternalUniforms::getEffectiveAlphaRef);
|
||||
.uniform1f("iris_currentAlphaTest", IrisInternalUniforms::getEffectiveAlphaRef, StateUpdateNotifiers.alphaTestNotifier)
|
||||
.uniform1f("alphaTestRef", IrisInternalUniforms::getEffectiveAlphaRef, StateUpdateNotifiers.alphaTestNotifier)
|
||||
.uniform1i("iris_currentAlphaFunc", IrisInternalUniforms::getEffectiveAlphaFunc, StateUpdateNotifiers.alphaFuncNotifier);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package net.coderbot.iris.vertices;
|
||||
|
||||
/**
|
||||
* Some annoying global state needed for the extended vertex format disabling optimization.
|
||||
*/
|
||||
public class ImmediateState {
|
||||
public static boolean isRenderingLevel = false;
|
||||
public static boolean renderWithExtendedVertexFormat = true;
|
||||
}
|
||||
@@ -13,4 +13,6 @@ public net.minecraft.block.BlockLeaves field_150129_M
|
||||
#For the interpolated icon class
|
||||
public net.minecraft.client.renderer.texture.TextureAtlasSprite field_110982_k #animationMetadata
|
||||
#For interpolated icon mixin
|
||||
public net.minecraft.client.resources.SimpleResource field_110530_g #mcmetaJson
|
||||
public net.minecraft.client.resources.SimpleResource field_110530_g #mcmetaJson
|
||||
#To prevent mods from un-disabling FOV changes
|
||||
public-f net.minecraftforge.client.event.FOVUpdateEvent fov # fov
|
||||
@@ -19,13 +19,16 @@ options.gui_background.tooltip=Background
|
||||
options.downfall_distance=Weather Effects
|
||||
options.downfall_distance.tooltip=Weather Effects
|
||||
options.dynamic_fov=Dynamic FOV
|
||||
options.dynamic_fov.tooltip=If enabled, the game's field of view will change based on the player's speed.
|
||||
options.dynamic_fov.tooltip=Depending on the mode, the game's field of view will change based on the player's speed or from certain mod interactions (including tool zooming/weapon scopes).
|
||||
options.horizon=Horizon
|
||||
options.horizon.tooltip=Horizon
|
||||
options.terrain_fog=Terrain Fog
|
||||
options.terrain_fog.tooltip=Terrain Fog (aka View Distance Fog)
|
||||
options.fog_near_distance=Near Fog Distance
|
||||
options.fog_near_distance.tooltip=Distance away from the player when the view distance fog will start (0%% opacity then linear increase to render distance)
|
||||
options.fov.all=Enabled
|
||||
options.fov.mods=Mods Only
|
||||
options.fov.none=Disabled
|
||||
options.mode_cloud_translucency=Cloud Translucency
|
||||
options.mode_cloud_translucency.tooltip=Cloud Translucency
|
||||
options.mode_clouds=Clouds
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.gtnewhorizons.angelica.mixins.early.angelica.bugfixes;
|
||||
|
||||
import net.minecraft.client.model.ModelSkeleton;
|
||||
import net.minecraft.entity.Entity;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/**
|
||||
* Prevents z-fighting between the skeleton's leg tops and the pelvis (bipedBody) bottom face.
|
||||
*/
|
||||
@Mixin(ModelSkeleton.class)
|
||||
public class MixinModelSkeleton_LegPelvisZFight {
|
||||
|
||||
@Unique
|
||||
private static final float LEG_PIVOT_Y = Float.intBitsToFloat(Float.floatToIntBits(12.0F) + 128);
|
||||
|
||||
@Inject(
|
||||
method = "setRotationAngles(FFFFFFLnet/minecraft/entity/Entity;)V",
|
||||
at = @At("TAIL")
|
||||
)
|
||||
private void angelica$nudgeLegsBelowPelvis(float swing, float swingAmount, float ticks,
|
||||
float headYaw, float headPitch, float scale,
|
||||
Entity entity, CallbackInfo ci) {
|
||||
final ModelSkeleton self = (ModelSkeleton) (Object) this;
|
||||
self.bipedRightLeg.rotationPointY = LEG_PIVOT_Y;
|
||||
self.bipedLeftLeg.rotationPointY = LEG_PIVOT_Y;
|
||||
}
|
||||
}
|
||||