Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f43fbd4d9f | ||
|
|
dbabc4b3dc | ||
|
|
e01097264b | ||
|
|
0a922f3129 | ||
|
|
4a2dcd0b22 | ||
|
|
0db691db0b | ||
|
|
e5cd1372e7 | ||
|
|
fbd07cc575 | ||
|
|
91f2cf9dfb | ||
|
|
34a6967c71 | ||
|
|
9c295a1e22 | ||
|
|
9fd02900ef | ||
|
|
27e9a4d4fd | ||
|
|
dfc08053d5 | ||
|
|
1af03fa369 | ||
|
|
3ae3b2527d | ||
|
|
fd2a14a000 | ||
|
|
9f0f6a6232 | ||
|
|
2942c7eb84 | ||
|
|
47762f921e | ||
|
|
bdc4431760 | ||
|
|
7e1beadfb5 | ||
|
|
59a4b84023 | ||
|
|
8d53e25e0a | ||
|
|
bbf83db379 | ||
|
|
ec82280b9b | ||
|
|
d7588e4c14 | ||
|
|
54c99386d4 | ||
|
|
465abf889a | ||
|
|
4acb83be6a | ||
|
|
80f9724ee6 | ||
|
|
8cf55bb216 | ||
|
|
3ebc8868c7 | ||
|
|
0938e6d975 | ||
|
|
d3022a40d2 | ||
|
|
2645c803f9 | ||
|
|
d91c356a99 | ||
|
|
189ff4a80f | ||
|
|
ffb4ca9472 | ||
|
|
7d8c8d306d | ||
|
|
117ff0cacf | ||
|
|
83fdd57534 | ||
|
|
951be04e9a | ||
|
|
0ad69f3433 | ||
|
|
0dabecc784 | ||
|
|
9b3fad4c6d | ||
|
|
e1e8256861 | ||
|
|
e65c01c2a3 | ||
|
|
b9de4df18e | ||
|
|
a5d31cd1b6 | ||
|
|
29f8d718cc | ||
|
|
52b0db2566 | ||
|
|
e9eb45f4f2 | ||
|
|
92e28a4cd5 | ||
|
|
f46a86889c | ||
|
|
c827d446d5 | ||
|
|
00ef355ce8 |
No files matched your search
@@ -3,7 +3,7 @@
|
||||
/.idea/
|
||||
/.vs/
|
||||
/.vscode/
|
||||
/run/
|
||||
run/
|
||||
build/
|
||||
/eclipse/
|
||||
.classpath
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
---
|
||||
@@ -83,7 +83,7 @@ dependencies {
|
||||
shadowImplementation("net.minecraftforge:eventbus:7.0-beta.15")
|
||||
|
||||
shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0
|
||||
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-26.g5ee7d96-GTNH") {
|
||||
shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-32.g7dd88a4-GTNH") {
|
||||
exclude module: "antlr4" // we only want to shadow the runtime module
|
||||
}
|
||||
shadowImplementation("org.antlr:antlr4-runtime:4.13.2")
|
||||
@@ -97,6 +97,8 @@ dependencies {
|
||||
devOnlyNonPublishable('com.github.GTNewHorizons:NotEnoughItems:2.8.35-GTNH:dev')
|
||||
devOnlyNonPublishable('com.github.GTNewHorizons:CodeChickenCore:1.4.7:dev')
|
||||
|
||||
//devOnlyNonPublishable('com.github.GTNewHorizons:worldedit-gtnh:v0.0.9')
|
||||
|
||||
// Notfine Deps
|
||||
compileOnly("thaumcraft:Thaumcraft:1.7.10-4.2.3.5:dev")
|
||||
devOnlyNonPublishable("com.github.GTNewHorizons:Baubles-Expanded:2.2.2-GTNH:dev")
|
||||
@@ -119,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"))
|
||||
|
||||
@@ -76,7 +76,7 @@ dependencies {
|
||||
compileOnly("org.lwjgl.lwjgl:lwjgl:${property("lwjglVersion")}")
|
||||
compileOnly("org.lwjgl.lwjgl:lwjgl_util:${property("lwjglVersion")}")
|
||||
|
||||
compileOnly("org.ow2.asm:asm-tree:9.7")
|
||||
compileOnly("org.ow2.asm:asm-tree:5.0.3")
|
||||
|
||||
// Our Deps
|
||||
api("com.github.GTNewHorizons:GTNHLib:${property("gtnhlibVersion")}:dev")
|
||||
@@ -100,6 +100,7 @@ dependencies {
|
||||
testImplementation(sourceSets["stubs"].output)
|
||||
testImplementation(platform("org.junit:junit-bom:${property("junitBomVersion")}"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testImplementation("org.ow2.asm:asm-tree:9.7")
|
||||
testImplementation("org.lwjgl.lwjgl:lwjgl:${property("lwjglVersion")}")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
testRuntimeOnly("org.apache.logging.log4j:log4j-core:${property("log4jVersion")}")
|
||||
@@ -145,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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -345,6 +345,27 @@ public class FeedbackManager {
|
||||
}
|
||||
break;
|
||||
|
||||
case GL11.GL_QUAD_STRIP:
|
||||
for (int i = 0; i + 3 < count; i += 2) {
|
||||
feedbackToken(GL11.GL_POLYGON_TOKEN);
|
||||
feedbackToken(4);
|
||||
emitVertex(reader.read(i), stride);
|
||||
emitVertex(reader.read(i + 1), stride);
|
||||
emitVertex(reader.read(i + 3), stride);
|
||||
emitVertex(reader.read(i + 2), stride);
|
||||
}
|
||||
break;
|
||||
|
||||
case GL11.GL_POLYGON:
|
||||
if (count >= 3) {
|
||||
feedbackToken(GL11.GL_POLYGON_TOKEN);
|
||||
feedbackToken(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
emitVertex(reader.read(i), stride);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Feedback mode: unsupported primitive mode 0x" + Integer.toHexString(mode));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -419,6 +421,9 @@ public class GLStateManager {
|
||||
@Getter protected static int boundVBO;
|
||||
@Getter protected static int boundEBO;
|
||||
@Getter protected static int boundVAO;
|
||||
private static int clientArraysVBO = 0;
|
||||
private static int clientArraysVBOCapacity = 0;
|
||||
private static final int[] clientArraysVBOOffsets = new int[VertexAttribState.MAX_ATTRIBS];
|
||||
private static int boundPixelUnpackBuffer;
|
||||
private static int boundPixelPackBuffer;
|
||||
private static final Int2IntOpenHashMap vaoEboMap = new Int2IntOpenHashMap();
|
||||
@@ -946,10 +951,11 @@ public class GLStateManager {
|
||||
|
||||
switch (pname) {
|
||||
case GL11.GL_COLOR_WRITEMASK -> {
|
||||
params.put((byte) (colorMask.red ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
params.put((byte) (colorMask.green ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
params.put((byte) (colorMask.blue ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
params.put((byte) (colorMask.alpha ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
final int pos = params.position();
|
||||
params.put(pos, (byte) (colorMask.red ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
params.put(pos + 1, (byte) (colorMask.green ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
params.put(pos + 2, (byte) (colorMask.blue ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
params.put(pos + 3, (byte) (colorMask.alpha ? GL11.GL_TRUE : GL11.GL_FALSE));
|
||||
}
|
||||
default -> {
|
||||
if (!HAS_MULTIPLE_SET.contains(pname)) {
|
||||
@@ -1060,7 +1066,7 @@ public class GLStateManager {
|
||||
case GL11.GL_DIFFUSE -> state.diffuse.get(0, params);
|
||||
case GL11.GL_SPECULAR -> state.specular.get(0, params);
|
||||
case GL11.GL_EMISSION -> state.emission.get(0, params);
|
||||
case GL11.GL_SHININESS -> params.put(state.shininess);
|
||||
case GL11.GL_SHININESS -> params.put(params.position(), state.shininess);
|
||||
case GL11.GL_COLOR_INDEXES -> state.colorIndexes.get(0, params);
|
||||
default -> {
|
||||
}
|
||||
@@ -1075,11 +1081,11 @@ public class GLStateManager {
|
||||
case GL11.GL_SPECULAR -> state.specular.get(0, params);
|
||||
case GL11.GL_POSITION -> state.position.get(0, params);
|
||||
case GL11.GL_SPOT_DIRECTION -> state.spotDirection.get(0, params);
|
||||
case GL11.GL_SPOT_EXPONENT -> params.put(state.spotExponent);
|
||||
case GL11.GL_SPOT_CUTOFF -> params.put(state.spotCutoff);
|
||||
case GL11.GL_CONSTANT_ATTENUATION -> params.put(state.constantAttenuation);
|
||||
case GL11.GL_LINEAR_ATTENUATION -> params.put(state.linearAttenuation);
|
||||
case GL11.GL_QUADRATIC_ATTENUATION -> params.put(state.quadraticAttenuation);
|
||||
case GL11.GL_SPOT_EXPONENT -> params.put(params.position(), state.spotExponent);
|
||||
case GL11.GL_SPOT_CUTOFF -> params.put(params.position(), state.spotCutoff);
|
||||
case GL11.GL_CONSTANT_ATTENUATION -> params.put(params.position(), state.constantAttenuation);
|
||||
case GL11.GL_LINEAR_ATTENUATION -> params.put(params.position(), state.linearAttenuation);
|
||||
case GL11.GL_QUADRATIC_ATTENUATION -> params.put(params.position(), state.quadraticAttenuation);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
@@ -1098,21 +1104,39 @@ public class GLStateManager {
|
||||
case GL11.GL_COLOR_CLEAR_VALUE -> clearColor.get(params);
|
||||
case GL11.GL_CURRENT_COLOR -> color.get(params);
|
||||
case GL11.GL_DEPTH_RANGE -> {
|
||||
params.put((float) viewportState.depthRangeNear);
|
||||
params.put((float) viewportState.depthRangeFar);
|
||||
final int pos = params.position();
|
||||
params.put(pos, (float) viewportState.depthRangeNear);
|
||||
params.put(pos + 1, (float) viewportState.depthRangeFar);
|
||||
}
|
||||
case GL14.GL_BLEND_COLOR -> {
|
||||
final int pos = params.position();
|
||||
params.put(pos, blendState.getBlendColorR());
|
||||
params.put(pos + 1, blendState.getBlendColorG());
|
||||
params.put(pos + 2, blendState.getBlendColorB());
|
||||
params.put(pos + 3, blendState.getBlendColorA());
|
||||
}
|
||||
case GL14.GL_BLEND_COLOR -> params.put(blendState.getBlendColorR()).put(blendState.getBlendColorG()).put(blendState.getBlendColorB()).put(blendState.getBlendColorA());
|
||||
case GL11.GL_CURRENT_NORMAL -> {
|
||||
final Vector3f normal = ShaderManager.getCurrentNormal();
|
||||
params.put(normal.x).put(normal.y).put(normal.z);
|
||||
final int pos = params.position();
|
||||
params.put(pos, normal.x);
|
||||
params.put(pos + 1, normal.y);
|
||||
params.put(pos + 2, normal.z);
|
||||
}
|
||||
case GL11.GL_CURRENT_TEXTURE_COORDS -> {
|
||||
final Vector4f tc = ShaderManager.getCurrentTexCoord();
|
||||
params.put(tc.x).put(tc.y).put(tc.z).put(tc.w);
|
||||
final int pos = params.position();
|
||||
params.put(pos, tc.x);
|
||||
params.put(pos + 1, tc.y);
|
||||
params.put(pos + 2, tc.z);
|
||||
params.put(pos + 3, tc.w);
|
||||
}
|
||||
case GL11.GL_FOG_COLOR -> {
|
||||
final FloatBuffer fogBuf = fogState.getFogColorBuffer();
|
||||
params.put(fogBuf.get(0)).put(fogBuf.get(1)).put(fogBuf.get(2)).put(fogBuf.get(3));
|
||||
final int pos = params.position();
|
||||
params.put(pos, fogBuf.get(0));
|
||||
params.put(pos + 1, fogBuf.get(1));
|
||||
params.put(pos + 2, fogBuf.get(2));
|
||||
params.put(pos + 3, fogBuf.get(3));
|
||||
}
|
||||
case GL11.GL_LIGHT_MODEL_AMBIENT -> lightModel.ambient.get(0, params);
|
||||
default -> {
|
||||
@@ -1614,6 +1638,9 @@ public class GLStateManager {
|
||||
}
|
||||
alphaTest.enable();
|
||||
fragmentGeneration++;
|
||||
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
|
||||
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void disableAlphaTest() {
|
||||
@@ -1631,6 +1658,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) {
|
||||
@@ -1650,6 +1680,9 @@ public class GLStateManager {
|
||||
alphaState.setFunction(function);
|
||||
alphaState.setReference(reference);
|
||||
fragmentGeneration++;
|
||||
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
|
||||
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2145,6 +2178,7 @@ public class GLStateManager {
|
||||
}
|
||||
prepareWideLineEmulation(mode);
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
RENDER_BACKEND.drawElements(mode, indices);
|
||||
}
|
||||
|
||||
@@ -2158,6 +2192,7 @@ public class GLStateManager {
|
||||
}
|
||||
prepareWideLineEmulation(mode);
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
if (mode == GL11.GL_QUADS) {
|
||||
QuadConverter.drawQuadElementsAsTriangles(indices);
|
||||
} else {
|
||||
@@ -2175,6 +2210,7 @@ public class GLStateManager {
|
||||
}
|
||||
prepareWideLineEmulation(mode);
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
if (mode == GL11.GL_QUADS) {
|
||||
QuadConverter.drawQuadElementsAsTriangles(indices);
|
||||
} else {
|
||||
@@ -2192,6 +2228,7 @@ public class GLStateManager {
|
||||
}
|
||||
prepareWideLineEmulation(mode);
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
if (mode == GL11.GL_QUADS) {
|
||||
QuadConverter.drawQuadElementsAsTriangles(count, type, indices);
|
||||
} else {
|
||||
@@ -2203,15 +2240,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;
|
||||
@@ -2223,6 +2257,7 @@ public class GLStateManager {
|
||||
}
|
||||
prepareWideLineEmulation(mode);
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
if (mode == GL11.GL_QUADS) {
|
||||
QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset);
|
||||
} else {
|
||||
@@ -2245,20 +2280,72 @@ public class GLStateManager {
|
||||
|
||||
public static void glMultiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride) {
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
RENDER_BACKEND.multiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
|
||||
}
|
||||
|
||||
public static void glMultiDrawElementsBaseVertex(int mode, long pCount, int type, long pIndices, int drawcount, long pBaseVertex) {
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
RENDER_BACKEND.multiDrawElementsBaseVertex(mode, pCount, type, pIndices, drawcount, pBaseVertex);
|
||||
}
|
||||
|
||||
public static void glDrawElementsBaseVertex(int mode, int count, int type, long indices, int basevertex) {
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
RENDER_BACKEND.drawElementsBaseVertex(mode, count, type, indices, basevertex);
|
||||
}
|
||||
|
||||
public static void glDrawElementsInstanced(int mode, int count, int type, long indices, int primcount) { RENDER_BACKEND.drawElementsInstanced(mode, count, type, indices, primcount); }
|
||||
public static void glDrawElementsInstanced(int mode, int count, int type, long indices, int primcount) {
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
RENDER_BACKEND.drawElementsInstanced(mode, count, type, indices, primcount);
|
||||
}
|
||||
|
||||
private static void prepareClientArrays() {
|
||||
if (ShaderManager.getInstance().isEnabled() && VertexAttribState.hasAnyClientSideEnabledAttrib()) {
|
||||
uploadClientArraysToVBO();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If any enabled vertex attribute uses a client-side pointer (no VBO), upload all such
|
||||
* attribs into a shared stream VBO so the draw succeeds under core profile.
|
||||
*/
|
||||
private static void uploadClientArraysToVBO() {
|
||||
int totalBytes = 0;
|
||||
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
|
||||
clientArraysVBOOffsets[i] = -1;
|
||||
final VertexAttribState.Attrib a = VertexAttribState.get(i);
|
||||
if (!a.enabled || a.clientPointer == null) continue;
|
||||
clientArraysVBOOffsets[i] = totalBytes;
|
||||
totalBytes += a.clientPointer.remaining();
|
||||
}
|
||||
if (totalBytes == 0) return;
|
||||
|
||||
if (clientArraysVBO == 0) {
|
||||
clientArraysVBO = RENDER_BACKEND.genBuffers();
|
||||
}
|
||||
|
||||
final int savedVBO = boundVBO;
|
||||
glBindBuffer(GL15.GL_ARRAY_BUFFER, clientArraysVBO);
|
||||
|
||||
if (totalBytes > clientArraysVBOCapacity) {
|
||||
int newCap = Math.max(4096, clientArraysVBOCapacity);
|
||||
while (newCap < totalBytes) newCap *= 2;
|
||||
clientArraysVBOCapacity = newCap;
|
||||
}
|
||||
RENDER_BACKEND.bufferData(GL15.GL_ARRAY_BUFFER, clientArraysVBOCapacity, GL15.GL_STREAM_DRAW);
|
||||
|
||||
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
|
||||
if (clientArraysVBOOffsets[i] < 0) continue;
|
||||
final VertexAttribState.Attrib a = VertexAttribState.get(i);
|
||||
RENDER_BACKEND.bufferSubData(GL15.GL_ARRAY_BUFFER, clientArraysVBOOffsets[i], a.clientPointer.duplicate());
|
||||
RENDER_BACKEND.vertexAttribPointer(i, a.size, a.type, a.normalized, a.stride, (long) clientArraysVBOOffsets[i]);
|
||||
}
|
||||
|
||||
glBindBuffer(GL15.GL_ARRAY_BUFFER, savedVBO);
|
||||
}
|
||||
|
||||
public static void glDrawArrays(int mode, int first, int count) {
|
||||
CommandRecorder savedRecorder = null;
|
||||
@@ -2278,8 +2365,13 @@ public class GLStateManager {
|
||||
}
|
||||
prepareWideLineEmulation(mode);
|
||||
ShaderManager.getInstance().preDraw();
|
||||
prepareClientArrays();
|
||||
if (mode == GL11.GL_QUADS) {
|
||||
QuadConverter.drawQuadsAsTriangles(first, count);
|
||||
} else if (mode == GL11.GL_QUAD_STRIP) {
|
||||
RENDER_BACKEND.drawArrays(GL11.GL_TRIANGLE_STRIP, first, count & ~1);
|
||||
} else if (mode == GL11.GL_POLYGON) {
|
||||
RENDER_BACKEND.drawArrays(GL11.GL_TRIANGLE_FAN, first, count);
|
||||
} else {
|
||||
RENDER_BACKEND.drawArrays(mode, first, count);
|
||||
}
|
||||
@@ -2298,6 +2390,10 @@ public class GLStateManager {
|
||||
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_FLOAT, false, stride, MemoryUtilities.memByteBuffer(pointer));
|
||||
}
|
||||
|
||||
public static void glVertexPointer(int size, int stride, DoubleBuffer pointer) {
|
||||
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
|
||||
}
|
||||
|
||||
public static void glVertexPointer(int size, int type, int stride, ByteBuffer pointer) {
|
||||
glVertexAttribPointer(Usage.POSITION.getAttributeLocation(), size, type, false, stride, pointer);
|
||||
}
|
||||
@@ -2339,6 +2435,10 @@ public class GLStateManager {
|
||||
glNormalPointer(GL11.GL_INT, stride, pointer);
|
||||
}
|
||||
|
||||
public static void glNormalPointer(int stride, DoubleBuffer pointer) {
|
||||
glNormalPointer(GL11.GL_DOUBLE, stride, pointer);
|
||||
}
|
||||
|
||||
public static void glNormalPointer(int type, int stride, ByteBuffer pointer) {
|
||||
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer);
|
||||
}
|
||||
@@ -2355,6 +2455,10 @@ public class GLStateManager {
|
||||
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_INT, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
|
||||
}
|
||||
|
||||
public static void glNormalPointer(int type, int stride, DoubleBuffer pointer) {
|
||||
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, GL11.GL_DOUBLE, Usage.NORMAL.isNormalized(), stride, MemoryUtilities.memByteBuffer(pointer));
|
||||
}
|
||||
|
||||
public static void glNormalPointer(int type, int stride, long pointer_buffer_offset) {
|
||||
glVertexAttribPointer(Usage.NORMAL.getAttributeLocation(), 3, type, Usage.NORMAL.isNormalized(), stride, pointer_buffer_offset);
|
||||
}
|
||||
@@ -2377,6 +2481,10 @@ public class GLStateManager {
|
||||
glTexCoordPointer(size, GL11.GL_SHORT, stride, pointer);
|
||||
}
|
||||
|
||||
public static void glTexCoordPointer(int size, int stride, DoubleBuffer pointer) {
|
||||
glTexCoordPointer(size, GL11.GL_DOUBLE, stride, pointer);
|
||||
}
|
||||
|
||||
public static void glTexCoordPointer(int size, int type, int stride, FloatBuffer pointer) {
|
||||
final int loc = texCoordAttributeLocation();
|
||||
if (loc < 0) return;
|
||||
@@ -2395,6 +2503,12 @@ public class GLStateManager {
|
||||
glVertexAttribPointer(loc, size, GL11.GL_INT, false, stride, MemoryUtilities.memByteBuffer(pointer));
|
||||
}
|
||||
|
||||
public static void glTexCoordPointer(int size, int type, int stride, DoubleBuffer pointer) {
|
||||
final int loc = texCoordAttributeLocation();
|
||||
if (loc < 0) return;
|
||||
glVertexAttribPointer(loc, size, GL11.GL_DOUBLE, false, stride, MemoryUtilities.memByteBuffer(pointer));
|
||||
}
|
||||
|
||||
public static void glTexCoordPointer(int size, int type, int stride, long pointer_buffer_offset) {
|
||||
final int loc = texCoordAttributeLocation();
|
||||
if (loc < 0) return;
|
||||
@@ -2441,32 +2555,23 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, FloatBuffer pointer) {
|
||||
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, DoubleBuffer pointer) {
|
||||
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, IntBuffer pointer) {
|
||||
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, ShortBuffer pointer) {
|
||||
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, bb, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, bb);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, MemoryUtilities.memByteBuffer(pointer), 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribPointer(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer) {
|
||||
VertexAttribState.set(index, size, type, normalized, stride, pointer, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribPointer(index, size, type, normalized, stride, pointer);
|
||||
VertexAttribState.set(index, size, type, normalized, stride, pointer, 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribIPointer(int index, int size, int type, int stride, long offset) {
|
||||
@@ -2475,20 +2580,15 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static void glVertexAttribIPointer(int index, int size, int type, int stride, ByteBuffer pointer) {
|
||||
VertexAttribState.set(index, size, type, false, stride, pointer, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribIPointer(index, size, type, stride, pointer);
|
||||
VertexAttribState.set(index, size, type, false, stride, pointer, 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribIPointer(int index, int size, int type, int stride, IntBuffer pointer) {
|
||||
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
|
||||
VertexAttribState.set(index, size, type, false, stride, bb, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribIPointer(index, size, type, stride, bb);
|
||||
VertexAttribState.set(index, size, type, false, stride, MemoryUtilities.memByteBuffer(pointer), 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribIPointer(int index, int size, int type, int stride, ShortBuffer pointer) {
|
||||
final ByteBuffer bb = MemoryUtilities.memByteBuffer(pointer);
|
||||
VertexAttribState.set(index, size, type, false, stride, bb, boundVBO);
|
||||
RENDER_BACKEND.vertexAttribIPointer(index, size, type, stride, bb);
|
||||
VertexAttribState.set(index, size, type, false, stride, MemoryUtilities.memByteBuffer(pointer), 0);
|
||||
}
|
||||
|
||||
public static void glVertexAttribDivisor(int index, int divisor) { RENDER_BACKEND.vertexAttribDivisor(index, divisor); }
|
||||
@@ -4297,9 +4397,20 @@ public class GLStateManager {
|
||||
RENDER_BACKEND.finish();
|
||||
}
|
||||
|
||||
public static int glGetError() { return RENDER_BACKEND.getError(); }
|
||||
public static String glGetString(int pname) { return RENDER_BACKEND.getString(pname); }
|
||||
public static String glGetStringi(int name, int index) { return RENDER_BACKEND.getStringi(name, index); }
|
||||
public static int glGetError() {
|
||||
if (!RENDER_BACKEND.hasContext()) return 0;
|
||||
return RENDER_BACKEND.getError();
|
||||
}
|
||||
|
||||
public static String glGetString(int pname) {
|
||||
if (!RENDER_BACKEND.hasContext()) return "no valid GL/render context";
|
||||
return RENDER_BACKEND.getString(pname);
|
||||
}
|
||||
|
||||
public static String glGetStringi(int name, int index) {
|
||||
if (!RENDER_BACKEND.hasContext()) return "no valid GL/render context";
|
||||
return RENDER_BACKEND.getStringi(name, index);
|
||||
}
|
||||
|
||||
public static void glShaderSource(int shader, CharSequence source) {
|
||||
String src = source.toString();
|
||||
@@ -4845,12 +4956,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4967,14 +5080,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;
|
||||
}
|
||||
@@ -5558,7 +5663,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);
|
||||
}
|
||||
|
||||
@@ -5618,6 +5726,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,
|
||||
@@ -5814,5 +5930,7 @@ public class GLStateManager {
|
||||
public static void glGetUniform(int program, int location, IntBuffer params) {
|
||||
RENDER_BACKEND.getUniformiv(program, location, params);
|
||||
}
|
||||
|
||||
public static void glGetTexImage(int target, int level, int format, int type, long pixels) {
|
||||
GL11.glGetTexImage(target, level, format, type, pixels);
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,12 @@ public class GlslTransformUtils {
|
||||
/** Reserved words added in later GLSL versions that may appear as identifiers in older shaders. */
|
||||
private record ReservedWordRename(Pattern pattern, String replacement) {}
|
||||
private static final Map<Integer, List<ReservedWordRename>> VERSIONED_RESERVED_WORDS = Map.of(
|
||||
400, List.of(
|
||||
// Some reserved words to always rename
|
||||
0, List.of(
|
||||
new ReservedWordRename(Pattern.compile("\\bsample\\b"), RENAMED_PREFIX + "sample"),
|
||||
new ReservedWordRename(Pattern.compile("\\bnew\\b"), RENAMED_PREFIX + "new")
|
||||
),
|
||||
400, List.of(
|
||||
new ReservedWordRename(Pattern.compile("\\bsampler\\b(?!\\d)"), RENAMED_PREFIX + "sampler")
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
/**
|
||||
* Tessellator data needed by {@link com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer}.
|
||||
*/
|
||||
public interface ITessellatorData {
|
||||
|
||||
boolean isDrawing();
|
||||
void setDrawing(boolean drawing);
|
||||
int getVertexCount();
|
||||
int[] getRawBuffer();
|
||||
int getRawBufferIndex();
|
||||
int getRawBufferSize();
|
||||
void setRawBufferSize(int size);
|
||||
void setRawBuffer(int[] buffer);
|
||||
int getDrawMode();
|
||||
boolean hasTexture();
|
||||
boolean hasColor();
|
||||
boolean hasNormals();
|
||||
boolean hasBrightness();
|
||||
void angelica$reset();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -432,7 +432,7 @@ public class RenderSystem {
|
||||
}
|
||||
|
||||
public static void deleteBuffers(int buffer) {
|
||||
RENDER_BACKEND.deleteBuffers(buffer);
|
||||
GLStateManager.glDeleteBuffers(buffer);
|
||||
}
|
||||
|
||||
public static long getVRAM() {
|
||||
@@ -491,7 +491,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) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.lwjgl.opengl.ARBDebugOutputCallback;
|
||||
import org.lwjgl.opengl.ARBDirectStateAccess;
|
||||
import org.lwjgl.opengl.ContextCapabilities;
|
||||
import org.lwjgl.opengl.EXTDirectStateAccess;
|
||||
import org.lwjgl.opengl.GLContext;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
@@ -67,7 +68,7 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
caps = org.lwjgl.opengl.GLContext.getCapabilities();
|
||||
caps = GLContext.getCapabilities();
|
||||
activeDebugExtension = 0;
|
||||
}
|
||||
|
||||
@@ -86,6 +87,15 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
|
||||
return "OpenGL (LWJGL2)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasContext() {
|
||||
try {
|
||||
return GLContext.getCapabilities() != null;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinGLSLVersion() {return 330;}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ public abstract class RenderBackend {
|
||||
public abstract boolean isAvailable();
|
||||
public abstract String getName();
|
||||
|
||||
/** Returns true if the current thread has a valid render context. Safe to call from any thread. */
|
||||
public abstract boolean hasContext();
|
||||
|
||||
/** Higher priority backends are preferred. */
|
||||
public int getPriority() { return 0; }
|
||||
|
||||
|
||||
@@ -273,21 +273,27 @@ public final class FragmentShaderGenerator {
|
||||
sb.append(" // Alpha test\n");
|
||||
final int func = FragmentKey.decodeAlphaFunc(key.alphaTestFunc());
|
||||
|
||||
|
||||
sb.append(" float qa = round(color.a * 255.0);\n");
|
||||
switch (func) {
|
||||
case GL11.GL_NEVER ->
|
||||
sb.append(" discard;\n");
|
||||
case GL11.GL_LESS ->
|
||||
sb.append(" if (color.a >= u_AlphaRef) discard;\n");
|
||||
case GL11.GL_EQUAL ->
|
||||
sb.append(" if (color.a != u_AlphaRef) discard;\n");
|
||||
sb.append(" if (qa >= u_AlphaRef * 255.0) discard;\n");
|
||||
case GL11.GL_EQUAL -> {
|
||||
sb.append(" float qr = round(u_AlphaRef * 255.0);\n");
|
||||
sb.append(" if (qa != qr) discard;\n");
|
||||
}
|
||||
case GL11.GL_LEQUAL ->
|
||||
sb.append(" if (color.a > u_AlphaRef) discard;\n");
|
||||
sb.append(" if (qa > u_AlphaRef * 255.0) discard;\n");
|
||||
case GL11.GL_GREATER ->
|
||||
sb.append(" if (color.a <= u_AlphaRef) discard;\n");
|
||||
case GL11.GL_NOTEQUAL ->
|
||||
sb.append(" if (color.a == u_AlphaRef) discard;\n");
|
||||
sb.append(" if (qa <= u_AlphaRef * 255.0) discard;\n");
|
||||
case GL11.GL_NOTEQUAL -> {
|
||||
sb.append(" float qr = round(u_AlphaRef * 255.0);\n");
|
||||
sb.append(" if (qa == qr) discard;\n");
|
||||
}
|
||||
case GL11.GL_GEQUAL ->
|
||||
sb.append(" if (color.a < u_AlphaRef) discard;\n");
|
||||
sb.append(" if (qa < u_AlphaRef * 255.0) discard;\n");
|
||||
case GL11.GL_ALWAYS -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ public class Program {
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
RENDER_BACKEND.deleteProgram(programId);
|
||||
GLStateManager.glDeleteProgram(programId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.gtnewhorizons.angelica.glsm.loading;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Verifies required classes/resources are present on the classpath at load time.
|
||||
* Fails fast with a descriptive error message if a dependency is missing.
|
||||
*/
|
||||
public final class DependencyVerifier {
|
||||
|
||||
/**
|
||||
* A single dependency check: a classpath resource that must exist.
|
||||
* @param resourcePath Resource path to check (e.g. "/com/example/SomeClass.class")
|
||||
* @param errorMessage Human-readable error message if missing (include download URL)
|
||||
*/
|
||||
public record Check(String resourcePath, String errorMessage) {}
|
||||
|
||||
/**
|
||||
* Verify all checks pass. Throws RuntimeException on first failure.
|
||||
* @param anchor Class whose classloader to use for resource lookups
|
||||
* @param checks Dependency checks to run
|
||||
*/
|
||||
public static void verify(Class<?> anchor, List<Check> checks) {
|
||||
for (Check check : checks) {
|
||||
if (anchor.getResource(check.resourcePath()) == null) {
|
||||
throw new RuntimeException(check.errorMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DependencyVerifier() {}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.gtnewhorizons.angelica.glsm.loading;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.redirect.GLSMRedirector;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class EarlyRedirectorCore {
|
||||
|
||||
private final GLSMRedirector core;
|
||||
private final String[] targets;
|
||||
private final String[] exclusions;
|
||||
|
||||
public EarlyRedirectorCore(String... extraExclusions) {
|
||||
this.core = new GLSMRedirector();
|
||||
this.targets = EcosystemNarrowRules.EARLY_REDIRECTOR_TARGETS.clone();
|
||||
|
||||
final List<String> excl = new ArrayList<>(Arrays.asList(core.getCoreExclusions()));
|
||||
if (extraExclusions != null && extraExclusions.length > 0) {
|
||||
Collections.addAll(excl, extraExclusions);
|
||||
}
|
||||
this.exclusions = excl.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public byte[] transform(String transformedName, byte[] basicClass) {
|
||||
if (basicClass == null) return null;
|
||||
|
||||
if (!startsWithAny(transformedName, targets)) return basicClass;
|
||||
if (startsWithAny(transformedName, exclusions)) return basicClass;
|
||||
if (!core.shouldTransform(basicClass)) return basicClass;
|
||||
|
||||
final ClassReader cr = new ClassReader(basicClass);
|
||||
final ClassNode cn = new ClassNode();
|
||||
cr.accept(cn, 0);
|
||||
if (core.transformClassNode(transformedName, cn)) {
|
||||
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cn.accept(cw);
|
||||
return cw.toByteArray();
|
||||
}
|
||||
return basicClass;
|
||||
}
|
||||
|
||||
private static boolean startsWithAny(String name, String[] prefixes) {
|
||||
for (String p : prefixes) {
|
||||
if (name.startsWith(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.gtnewhorizons.angelica.glsm.loading;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.loading.TransformerNarrower.NarrowRule;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class EcosystemNarrowRules {
|
||||
|
||||
public static final List<NarrowRule> ALL = List.of(
|
||||
new NarrowRule("DragonAPI", "Reika.DragonAPI.ASM", List.of(
|
||||
"Reika.DragonAPI.ASM.Patchers",
|
||||
"Reika.DragonAPI.ASM.Profiling",
|
||||
"Reika.DragonAPI.ASM.APIStripper",
|
||||
"Reika.DragonAPI.ASM.ClassReparenter",
|
||||
"Reika.DragonAPI.ASM.DependentMethodStripper",
|
||||
"Reika.DragonAPI.ASM.DragonAPIClassTransformer",
|
||||
"Reika.DragonAPI.ASM.FMLItemBlockPatch",
|
||||
"Reika.DragonAPI.ASM.FluidNamePatch",
|
||||
"Reika.DragonAPI.ASM.InterfaceInjector",
|
||||
"Reika.DragonAPI.ASM.SpecialBiomePlacement",
|
||||
"Reika.DragonAPI.ASM.StructureLootHooks"
|
||||
)),
|
||||
|
||||
new NarrowRule("Xaeros_common", "xaero.common.core", List.of(
|
||||
"xaero.common.core.transformer"
|
||||
)),
|
||||
|
||||
new NarrowRule("Xaeros_map", "xaero.map.core", List.of(
|
||||
"xaero.map.core.transformer"
|
||||
)),
|
||||
|
||||
new NarrowRule("AdvancedLightsabers", "com.fiskmods.lightsabers.asm", List.of(
|
||||
"com.fiskmods.lightsabers.asm.ALLoadingPlugin",
|
||||
"com.fiskmods.lightsabers.asm.ASMHooks",
|
||||
"com.fiskmods.lightsabers.asm.transformers"
|
||||
)),
|
||||
|
||||
new NarrowRule("Ears", "com.unascribed.ears", List.of(
|
||||
"com.unascribed.ears.asm",
|
||||
"com.unascribed.ears.common.agent"
|
||||
)),
|
||||
|
||||
new NarrowRule("Alfheim", "alfheim.common.core.asm.hook", List.of(
|
||||
"alfheim.common.core.asm.hook.AlfheimFieldHookHandler",
|
||||
"alfheim.common.core.asm.hook.AlfheimHPHooks",
|
||||
"alfheim.common.core.asm.hook.Botania18AndUpBackport",
|
||||
"alfheim.common.core.asm.hook.ElementalDamageAdapter",
|
||||
"alfheim.common.core.asm.hook.integration",
|
||||
"alfheim.common.core.asm.hook.extender.FurnaceExtender",
|
||||
"alfheim.common.core.asm.hook.extender.ItemAuraRingExtender",
|
||||
"alfheim.common.core.asm.hook.extender.ItemLensExtender",
|
||||
"alfheim.common.core.asm.hook.extender.ItemTwigWandExtender",
|
||||
"alfheim.common.core.asm.hook.extender.LightRelayExtender",
|
||||
"alfheim.common.core.asm.hook.extender.PureDaisyExtender",
|
||||
"alfheim.common.core.asm.hook.extender.QuartzExtender",
|
||||
"alfheim.common.core.asm.hook.extender.RelicHooks",
|
||||
"alfheim.common.core.asm.hook.extender.SparkExtender",
|
||||
"alfheim.common.core.asm.hook.fixes.BotaniaGlowingRenderFixes",
|
||||
"alfheim.common.core.asm.hook.fixes.CorporeaInputFix",
|
||||
"alfheim.common.core.asm.hook.fixes.GodAttributesHooks",
|
||||
"alfheim.common.core.asm.hook.fixes.RecipeAncientWillsFix"
|
||||
))
|
||||
);
|
||||
|
||||
public static final String[] LWJGL3IFY_EXCLUSIONS_SHARED = {
|
||||
"org.embeddedt.embeddium",
|
||||
"org.taumc.celeritas",
|
||||
"com.mitchej123.lwjgl.lwjgl3",
|
||||
"com.mitchej123.glsm",
|
||||
"com.gtnewhorizons.angelica.lwjgl3",
|
||||
};
|
||||
|
||||
public static final String[] EARLY_REDIRECTOR_TARGETS = {
|
||||
"cn.tesseract.mycelium.",
|
||||
};
|
||||
|
||||
private EcosystemNarrowRules() {}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.gtnewhorizons.angelica.glsm.loading;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Narrows overly-broad transformer exclusions that prevent the GLSM redirector from
|
||||
* transforming GL calls in mod code. Some coremods register exclusions covering their
|
||||
* entire package tree, which inadvertently shields rendering code from redirection.
|
||||
* <p>
|
||||
* This utility removes the broad exclusion and re-adds narrower ones that cover only
|
||||
* the coremod's own transformer/ASM classes, leaving rendering code exposed.
|
||||
* <p>
|
||||
* Operates on a {@code Set<String>} (the transformer exceptions set from LaunchClassLoader)
|
||||
* and a {@code Map<String, Object>} (the Launch blackboard). Callers extract these from
|
||||
* the Forge/launchwrapper infrastructure.
|
||||
*/
|
||||
public final class TransformerNarrower {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("GLSM");
|
||||
|
||||
/**
|
||||
* A rule that replaces a broad transformer exclusion with narrower ones.
|
||||
* @param name Human-readable name for logging (e.g. "DragonAPI")
|
||||
* @param broad The broad exclusion to remove (e.g. "Reika.DragonAPI.ASM")
|
||||
* @param narrow Narrower exclusions to add in its place
|
||||
*/
|
||||
public record NarrowRule(String name, String broad, List<String> narrow) {}
|
||||
|
||||
/**
|
||||
* Apply narrowing rules to the transformer exceptions set.
|
||||
*
|
||||
* @param exceptions The mutable Set from LaunchClassLoader.transformerExceptions
|
||||
* @param blackboard The Launch.blackboard map (checked for disable flags)
|
||||
* @param keyPrefix Blackboard key prefix for per-rule opt-out (e.g. "angelica" or "umbra").
|
||||
* Rule "DragonAPI" checks key "{prefix}.narrow.DragonAPI". If explicitly
|
||||
* set to Boolean.FALSE, the rule is skipped.
|
||||
* @param callerName Name for log messages (e.g. "Angelica" or "Umbra")
|
||||
* @param rules The narrowing rules to apply
|
||||
* @return number of rules that were applied
|
||||
*/
|
||||
public static int narrow(Set<String> exceptions, Map<String, Object> blackboard,
|
||||
String keyPrefix, String callerName, List<NarrowRule> rules) {
|
||||
int applied = 0;
|
||||
for (NarrowRule rule : rules) {
|
||||
final String key = keyPrefix + ".narrow." + rule.name();
|
||||
if (Boolean.FALSE.equals(blackboard.get(key))) {
|
||||
continue;
|
||||
}
|
||||
if (exceptions.remove(rule.broad())) {
|
||||
for (String narrow : rule.narrow()) {
|
||||
exceptions.add(narrow);
|
||||
}
|
||||
LOGGER.info("[{}] Narrowed {} transformer exclusion to allow GL redirection", callerName, rule.broad());
|
||||
applied++;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
private TransformerNarrower() {}
|
||||
}
|
||||
@@ -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 ===
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,6 +75,12 @@ public final class DisplayListVBOBuilder {
|
||||
if (data.drawMode == GL11.GL_QUADS) {
|
||||
final IVertexArrayObject indexedVAO = new IndexedVAO(vbo, IndexBuffer.convertQuadsToTrigs(start, start + vertexCount));
|
||||
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(indexedVAO, GL11.GL_TRIANGLES, 0, vertexCount / 4 * 6);
|
||||
} else if (data.drawMode == GL11.GL_QUAD_STRIP) {
|
||||
// GL_QUAD_STRIP is removed in core profile; an even-length GL_TRIANGLE_STRIP produces the same quads
|
||||
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, GL11.GL_TRIANGLE_STRIP, start, vertexCount & ~1);
|
||||
} else if (data.drawMode == GL11.GL_POLYGON) {
|
||||
// GL_POLYGON is removed in core profile; convert to GL_TRIANGLE_FAN which is equivalent for convex polygons
|
||||
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, GL11.GL_TRIANGLE_FAN, start, vertexCount);
|
||||
} else {
|
||||
vbos[data.drawIndex] = new DisplayListVBO.SubVBO(vao, data.drawMode, start, vertexCount);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,19 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.objectweb.asm.Handle;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.IntInsnNode;
|
||||
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
|
||||
import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.TypeInsnNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -79,6 +82,9 @@ public class GLSMRedirector {
|
||||
glCapRedirects.put(org.lwjgl.opengl.GL11.GL_SCISSOR_TEST, "ScissorTest");
|
||||
|
||||
final var gl11 = RedirectMap.newMap()
|
||||
// glEnable/Disable - Special cased in GLSMRedirector, but included here for the external API
|
||||
.add("glEnable")
|
||||
.add("glDisable")
|
||||
.add("glAlphaFunc")
|
||||
.add("glBegin")
|
||||
.add("glBindTexture")
|
||||
@@ -495,6 +501,22 @@ public class GLSMRedirector {
|
||||
return CORE_EXCLUSIONS.clone();
|
||||
}
|
||||
|
||||
/** The internal-name prefix used to match GL version classes (e.g., {@code "org/lwjgl/opengl/GL"}). */
|
||||
public static String getGLPrefix() { return GL_PREFIX; }
|
||||
|
||||
/** The internal name of the redirect target class. */
|
||||
public static String getTargetClassName() { return GLStateManager; }
|
||||
|
||||
/** Method redirects for GL-prefix classes. Key: original method name, Value: GLStateManager method name. */
|
||||
public static Map<String, String> getGLPrefixMethodRedirects() {
|
||||
return Collections.unmodifiableMap(glMethodRedirects);
|
||||
}
|
||||
|
||||
/** Method redirects for named (non-GL-prefix) classes. Key: internal class name, Value: per-method redirect map. */
|
||||
public static Map<String, Map<String, String>> getNamedClassMethodRedirects() {
|
||||
return Collections.unmodifiableMap(methodRedirects);
|
||||
}
|
||||
|
||||
public boolean shouldTransform(byte[] basicClass) {
|
||||
return cstPoolParser.find(basicClass, true);
|
||||
}
|
||||
@@ -504,7 +526,7 @@ public class GLSMRedirector {
|
||||
boolean changed = false;
|
||||
final boolean isOpenGlHelper = transformedName.equals("net.minecraft.client.renderer.OpenGlHelper");
|
||||
|
||||
for (MethodNode mn : cn.methods) {
|
||||
for (MethodNode mn : (List<MethodNode>) cn.methods) {
|
||||
if (isOpenGlHelper && (mn.name.equals("glBlendFunc") || mn.name.equals("func_148821_a"))) {
|
||||
continue;
|
||||
}
|
||||
@@ -590,6 +612,22 @@ public class GLSMRedirector {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} else if (node instanceof InvokeDynamicInsnNode dynNode) {
|
||||
// Redirect method handles in invokedynamic bootstrap arguments
|
||||
for (int i = 0; i < dynNode.bsmArgs.length; i++) {
|
||||
if (!(dynNode.bsmArgs[i] instanceof Handle handle)) continue;
|
||||
final Map<String, String> redirects = handle.getOwner().startsWith(GL_PREFIX) ? glMethodRedirects : methodRedirects.get(handle.getOwner());
|
||||
if (redirects == null) continue;
|
||||
final String glsmName = redirects.get(handle.getName());
|
||||
if (glsmName == null) continue;
|
||||
if (LOG_SPAM) {
|
||||
final String shortOwner = handle.getOwner().substring(handle.getOwner().lastIndexOf("/") + 1);
|
||||
LOGGER.info("Redirecting invokedynamic handle in {} from {}.{}{} to GLStateManager.{}{}", transformedName, shortOwner, handle.getName(), handle.getDesc(), glsmName, handle.getDesc());
|
||||
}
|
||||
dynNode.bsmArgs[i] = new Handle(handle.getTag(), GLStateManager, glsmName, handle.getDesc());
|
||||
changed = true;
|
||||
redirectInMethod = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ASSERT_MAIN_THREAD && redirectInMethod && !transformedName.startsWith(SplashProgress) && !(transformedName.startsWith(MinecraftClient) && ExcludedMinecraftMainThreadChecks.contains(mn.name))) {
|
||||
|
||||
@@ -103,9 +103,12 @@ public class MaterialState implements ISettableState<MaterialState> {
|
||||
}
|
||||
}
|
||||
|
||||
public void setShininess(FloatBuffer newBuffer) { setShininess(newBuffer.get()); }
|
||||
// On the buffer versions of shininess, we need to call `get(0)` explicitly specifying the index
|
||||
// otherwise calling `get()` advances the buffer position, and some mods may re-use the same buffer
|
||||
// in subsequent calls, resulting in a BufferUnderflowException.
|
||||
public void setShininess(FloatBuffer newBuffer) { setShininess(newBuffer.get(0)); }
|
||||
|
||||
public void setShininess(IntBuffer newBuffer) { setShininess((float) newBuffer.get());}
|
||||
public void setShininess(IntBuffer newBuffer) { setShininess((float) newBuffer.get(0)); }
|
||||
|
||||
public void setShininess(int val) { setShininess((float) val);}
|
||||
|
||||
|
||||
@@ -16,16 +16,19 @@ public class VertexAttribState {
|
||||
private static final Int2ObjectOpenHashMap<Attrib[]> vaoAttribs = new Int2ObjectOpenHashMap<>();
|
||||
private static Attrib[] current;
|
||||
private static final ArrayDeque<Attrib[]> pool = new ArrayDeque<>();
|
||||
private static int clientSideEnabledCount;
|
||||
|
||||
public static void init(int defaultVAO) {
|
||||
vaoAttribs.clear();
|
||||
pool.clear();
|
||||
current = allocAttribArray();
|
||||
vaoAttribs.put(defaultVAO, current);
|
||||
clientSideEnabledCount = 0;
|
||||
}
|
||||
|
||||
public static void onBindVertexArray(int newVAO) {
|
||||
current = vaoAttribs.computeIfAbsent(newVAO, k -> allocAttribArray());
|
||||
recomputeClientSideCount();
|
||||
}
|
||||
|
||||
public static void onDeleteVertexArray(int vaoId) {
|
||||
@@ -39,6 +42,7 @@ public class VertexAttribState {
|
||||
vaoAttribs.clear();
|
||||
pool.clear();
|
||||
current = null;
|
||||
clientSideEnabledCount = 0;
|
||||
}
|
||||
|
||||
private static Attrib[] allocAttribArray() {
|
||||
@@ -55,6 +59,7 @@ public class VertexAttribState {
|
||||
public static void set(int index, int size, int type, boolean normalized, int stride, long offset, int vboId) {
|
||||
if (index < 0 || index >= MAX_ATTRIBS) return;
|
||||
final Attrib a = current[index];
|
||||
final boolean was = a.isClientSide();
|
||||
a.size = size;
|
||||
a.type = type;
|
||||
a.normalized = normalized;
|
||||
@@ -62,11 +67,13 @@ public class VertexAttribState {
|
||||
a.offset = offset;
|
||||
a.vboId = vboId;
|
||||
a.clientPointer = null;
|
||||
if (was) clientSideEnabledCount--;
|
||||
}
|
||||
|
||||
public static void set(int index, int size, int type, boolean normalized, int stride, ByteBuffer pointer, int vboId) {
|
||||
if (index < 0 || index >= MAX_ATTRIBS) return;
|
||||
final Attrib a = current[index];
|
||||
final boolean was = a.isClientSide();
|
||||
a.size = size;
|
||||
a.type = type;
|
||||
a.normalized = normalized;
|
||||
@@ -74,24 +81,41 @@ public class VertexAttribState {
|
||||
a.offset = 0;
|
||||
a.vboId = vboId;
|
||||
a.clientPointer = (vboId == 0) ? pointer : null;
|
||||
final boolean now = a.isClientSide();
|
||||
if (was != now) clientSideEnabledCount += now ? 1 : -1;
|
||||
}
|
||||
|
||||
public static void setEnabled(int index, boolean enabled) {
|
||||
if (index < 0 || index >= MAX_ATTRIBS) return;
|
||||
current[index].enabled = enabled;
|
||||
final Attrib a = current[index];
|
||||
if (a.clientPointer != null && a.enabled != enabled) {
|
||||
clientSideEnabledCount += enabled ? 1 : -1;
|
||||
}
|
||||
a.enabled = enabled;
|
||||
}
|
||||
|
||||
public static Attrib get(int index) {
|
||||
return current[index];
|
||||
}
|
||||
|
||||
public static boolean hasVBOBoundAttrib() {
|
||||
|
||||
|
||||
private static void recomputeClientSideCount() {
|
||||
int count = 0;
|
||||
for (int i = 0; i < MAX_ATTRIBS; i++) {
|
||||
if (current[i].enabled && current[i].vboId != 0) return true;
|
||||
if (current[i].isClientSide()) count++;
|
||||
}
|
||||
return false;
|
||||
clientSideEnabledCount = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* offsets into a non-existent buffer, causing a native crash at draw time.
|
||||
*/
|
||||
public static boolean hasAnyClientSideEnabledAttrib() {
|
||||
return clientSideEnabledCount > 0;
|
||||
}
|
||||
public static class Attrib {
|
||||
public boolean enabled;
|
||||
public int size;
|
||||
@@ -102,6 +126,10 @@ public class VertexAttribState {
|
||||
public int vboId;
|
||||
public ByteBuffer clientPointer;
|
||||
|
||||
public boolean isClientSide() {
|
||||
return enabled && clientPointer != null;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
enabled = false;
|
||||
size = 0;
|
||||
@@ -132,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package com.gtnewhorizons.angelica.client.rendering;
|
||||
package com.gtnewhorizons.angelica.glsm.streaming;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ITessellatorData;
|
||||
import com.gtnewhorizons.angelica.glsm.QuadConverter;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.OrphanStreamingBuffer;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.PersistentStreamingBuffer;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
@@ -71,42 +69,42 @@ public class TessellatorStreamingDrawer {
|
||||
/**
|
||||
* Draw the vanilla Tessellator's data using streaming VBO+VAO instead of FFP client arrays.
|
||||
*/
|
||||
public static int draw(Tessellator tess) {
|
||||
if (!tess.isDrawing) {
|
||||
public static int draw(ITessellatorData tess) {
|
||||
if (!tess.isDrawing()) {
|
||||
throw new IllegalStateException("Not tesselating!");
|
||||
}
|
||||
|
||||
tess.isDrawing = false;
|
||||
tess.setDrawing(false);
|
||||
|
||||
final int vertexCount = tess.vertexCount;
|
||||
final int vertexCount = tess.getVertexCount();
|
||||
if (vertexCount == 0) {
|
||||
final int result = tess.rawBufferIndex * 4;
|
||||
tess.reset();
|
||||
final int result = tess.getRawBufferIndex() * 4;
|
||||
tess.angelica$reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Determine the optimal vertex format from the tessellator's flags
|
||||
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
|
||||
final int flags = VertexFlags.convertToFlags(tess.hasTexture(), tess.hasColor(), tess.hasNormals(), tess.hasBrightness());
|
||||
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
|
||||
final int vertexSize = format.getVertexSize();
|
||||
|
||||
final int requiredBytes = vertexCount * vertexSize;
|
||||
ensureRepackCapacity(requiredBytes);
|
||||
|
||||
final long writePtr = format.writeToBuffer0(repackAddress, tess.rawBuffer, tess.rawBufferIndex);
|
||||
final long writePtr = format.writeToBuffer0(repackAddress, tess.getRawBuffer(), tess.getRawBufferIndex());
|
||||
repackBuffer.position(0);
|
||||
repackBuffer.limit((int)(writePtr - repackAddress));
|
||||
|
||||
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.drawMode, vertexCount);
|
||||
uploadAndDraw(repackBuffer, flags, format, vertexSize, tess.getDrawMode(), vertexCount);
|
||||
|
||||
// Shrink rawBuffer if oversized
|
||||
if (tess.rawBufferSize > 0x20000 && tess.rawBufferIndex < (tess.rawBufferSize << 3)) {
|
||||
tess.rawBufferSize = 0x10000;
|
||||
tess.rawBuffer = new int[tess.rawBufferSize];
|
||||
if (tess.getRawBufferSize() > 0x20000 && tess.getRawBufferIndex() < (tess.getRawBufferSize() << 3)) {
|
||||
tess.setRawBufferSize(0x10000);
|
||||
tess.setRawBuffer(new int[tess.getRawBufferSize()]);
|
||||
}
|
||||
|
||||
final int result = tess.rawBufferIndex * 4;
|
||||
tess.reset();
|
||||
final int result = tess.getRawBufferIndex() * 4;
|
||||
tess.angelica$reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -128,6 +126,19 @@ public class TessellatorStreamingDrawer {
|
||||
uploadAndDraw(buffer, flags, format, vertexSize, drawMode, vertexCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload pre-packed vertex data and draw. Public API for external batch systems.
|
||||
* @param packedData buffer positioned at 0 with limit set to total bytes
|
||||
* @param drawMode GL draw mode (GL_QUADS, GL_TRIANGLES, etc.)
|
||||
* @param flags vertex format flags (from VertexFlags)
|
||||
* @param vertexCount number of vertices
|
||||
*/
|
||||
public static void drawPacked(ByteBuffer packedData, int drawMode, int flags, int vertexCount) {
|
||||
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
|
||||
final int vertexSize = format.getVertexSize();
|
||||
uploadAndDraw(packedData, flags, format, vertexSize, drawMode, vertexCount);
|
||||
}
|
||||
|
||||
private static String cachedDebugInfo = "Stream: not initialized";
|
||||
private static long lastDebugUpdateNanos;
|
||||
private static final long DEBUG_UPDATE_INTERVAL_NS = 500_000_000L; // 500ms
|
||||
@@ -172,34 +183,6 @@ public class TessellatorStreamingDrawer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a merged batch of pre-packed vertex data from a DeferredBatchTessellator.
|
||||
* Copies byte ranges into repackBuffer, then uploads and draws.
|
||||
*/
|
||||
static void drawPackedBatch(DeferredBatchTessellator source,
|
||||
java.util.List<DeferredBatchTessellator.DrawRange> ranges,
|
||||
int from, int to,
|
||||
int totalBytes, int totalVertices,
|
||||
int drawMode, int flags) {
|
||||
if (totalVertices == 0) return;
|
||||
|
||||
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[flags];
|
||||
final int vertexSize = format.getVertexSize();
|
||||
|
||||
ensureRepackCapacity(totalBytes);
|
||||
|
||||
long writePos = repackAddress;
|
||||
for (int j = from; j < to; j++) {
|
||||
final DeferredBatchTessellator.DrawRange r = ranges.get(j);
|
||||
source.copyRange(r.byteOffset(), r.byteLength(), writePos);
|
||||
writePos += r.byteLength();
|
||||
}
|
||||
repackBuffer.position(0);
|
||||
repackBuffer.limit(totalBytes);
|
||||
|
||||
uploadAndDraw(repackBuffer, flags, format, vertexSize, drawMode, totalVertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload packed vertex data to a streaming buffer and issue the draw call.
|
||||
* Tries the persistent ring buffer first, falls back to orphan buffer on overflow.
|
||||
@@ -239,7 +222,11 @@ public class TessellatorStreamingDrawer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureRepackCapacity(int requiredBytes) {
|
||||
/**
|
||||
* Ensure the repack buffer is large enough for the given byte count.
|
||||
* Public for use by external batch systems that need to pack data before calling {@link #drawPacked}.
|
||||
*/
|
||||
public static void ensureRepackCapacity(int requiredBytes) {
|
||||
if (requiredBytes <= repackCapacity) return;
|
||||
|
||||
int newCapacity = repackCapacity;
|
||||
@@ -253,6 +240,16 @@ public class TessellatorStreamingDrawer {
|
||||
repackCapacity = newCapacity;
|
||||
}
|
||||
|
||||
/** Get the repack buffer's native address. Valid until next {@link #ensureRepackCapacity} call. */
|
||||
public static long getRepackAddress() {
|
||||
return repackAddress;
|
||||
}
|
||||
|
||||
/** Get the repack ByteBuffer. Caller must set position/limit before passing to {@link #drawPacked}. */
|
||||
public static ByteBuffer getRepackBuffer() {
|
||||
return repackBuffer;
|
||||
}
|
||||
|
||||
private static void ensureVAO(int flags, VertexFormat format) {
|
||||
init();
|
||||
|
||||
@@ -596,4 +596,21 @@ class CompatShaderTransformerTest {
|
||||
assertFalse(fragResult.contains("varying"), "no 'varying' keyword in fragment");
|
||||
assertTrue(fragResult.contains("in vec2 v_TexCoord"), "varying → in in fragment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReservedWordNewIsRenamed() {
|
||||
String src = """
|
||||
#version 120
|
||||
uniform sampler2D tex;
|
||||
void main() {
|
||||
vec4 color = texture2D(tex, vec2(0.5));
|
||||
float new = color.r > 0.5 ? 1.0 : 0.0;
|
||||
gl_FragColor = vec4(new, new, new, 1.0);
|
||||
}
|
||||
""";
|
||||
|
||||
String result = CompatShaderTransformer.transform(src, true);
|
||||
assertFalse(result.matches("(?s).*\\bnew\\b(?!\\s*\\().*"), "'new' as identifier must be renamed in core profile output\n\n" + result);
|
||||
assertTrue(result.contains("angelica_renamed_new"), "'new' should be renamed to angelica_renamed_new\n\n" + result);
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,108 @@ class FeedbackManagerTest {
|
||||
GLStateManager.glDeleteVertexArrays(vao);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPolygonFeedback_GL2D() {
|
||||
final FloatBuffer buf = BufferUtils.createFloatBuffer(128);
|
||||
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
|
||||
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
|
||||
|
||||
GLStateManager.getModelViewMatrix().identity();
|
||||
GLStateManager.getProjectionMatrix().identity();
|
||||
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
|
||||
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
|
||||
|
||||
final int vao = GL30.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
|
||||
final float[] verts = {
|
||||
0f, 0f, 0f,
|
||||
1f, 0f, 0f,
|
||||
1f, 1f, 0f,
|
||||
0f, 1f, 0f
|
||||
};
|
||||
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(verts.length);
|
||||
vertBuf.put(verts).flip();
|
||||
|
||||
final int vbo = GL15.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
|
||||
FeedbackManager.processDrawArrays(GL11.GL_POLYGON, 0, 4);
|
||||
|
||||
final int count = FeedbackManager.glRenderMode(GL11.GL_RENDER);
|
||||
|
||||
assertEquals(10, count, "Polygon in GL_2D should produce 10 floats");
|
||||
assertEquals(GL11.GL_POLYGON_TOKEN, buf.get(0), 0.001f);
|
||||
assertEquals(4.0f, buf.get(1), 0.001f);
|
||||
assertEquals(400.0f, buf.get(2), 0.01f);
|
||||
assertEquals(300.0f, buf.get(3), 0.01f);
|
||||
assertEquals(800.0f, buf.get(4), 0.01f);
|
||||
assertEquals(300.0f, buf.get(5), 0.01f);
|
||||
assertEquals(800.0f, buf.get(6), 0.01f);
|
||||
assertEquals(600.0f, buf.get(7), 0.01f);
|
||||
assertEquals(400.0f, buf.get(8), 0.01f);
|
||||
assertEquals(600.0f, buf.get(9), 0.01f);
|
||||
|
||||
GLStateManager.glDisableVertexAttribArray(0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GL15.glDeleteBuffers(vbo);
|
||||
GLStateManager.glDeleteVertexArrays(vao);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testQuadStripFeedback_GL2D() {
|
||||
final FloatBuffer buf = BufferUtils.createFloatBuffer(128);
|
||||
FeedbackManager.glFeedbackBuffer(GL11.GL_2D, buf);
|
||||
FeedbackManager.glRenderMode(GL11.GL_FEEDBACK);
|
||||
|
||||
GLStateManager.getModelViewMatrix().identity();
|
||||
GLStateManager.getProjectionMatrix().identity();
|
||||
GLStateManager.getViewportState().setViewPort(0, 0, 800, 600);
|
||||
GLStateManager.getViewportState().setDepthRange(0.0, 1.0);
|
||||
|
||||
final int vao = GL30.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
|
||||
final float[] verts = {
|
||||
0f, 0f, 0f,
|
||||
0f, 1f, 0f,
|
||||
1f, 0f, 0f,
|
||||
1f, 1f, 0f
|
||||
};
|
||||
final FloatBuffer vertBuf = BufferUtils.createFloatBuffer(verts.length);
|
||||
vertBuf.put(verts).flip();
|
||||
|
||||
final int vbo = GL15.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, vertBuf, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, 12, 0);
|
||||
GLStateManager.glEnableVertexAttribArray(0);
|
||||
|
||||
FeedbackManager.processDrawArrays(GL11.GL_QUAD_STRIP, 0, 4);
|
||||
|
||||
final int count = FeedbackManager.glRenderMode(GL11.GL_RENDER);
|
||||
|
||||
assertEquals(10, count, "Quad strip in GL_2D should produce 10 floats");
|
||||
assertEquals(GL11.GL_POLYGON_TOKEN, buf.get(0), 0.001f);
|
||||
assertEquals(4.0f, buf.get(1), 0.001f);
|
||||
assertEquals(400.0f, buf.get(2), 0.01f);
|
||||
assertEquals(300.0f, buf.get(3), 0.01f);
|
||||
assertEquals(400.0f, buf.get(4), 0.01f);
|
||||
assertEquals(600.0f, buf.get(5), 0.01f);
|
||||
assertEquals(800.0f, buf.get(6), 0.01f);
|
||||
assertEquals(600.0f, buf.get(7), 0.01f);
|
||||
assertEquals(800.0f, buf.get(8), 0.01f);
|
||||
assertEquals(300.0f, buf.get(9), 0.01f);
|
||||
|
||||
GLStateManager.glDisableVertexAttribArray(0);
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
GL15.glDeleteBuffers(vbo);
|
||||
GLStateManager.glDeleteVertexArrays(vao);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIdentityTransform_viewportMapping() {
|
||||
final FloatBuffer buf = BufferUtils.createFloatBuffer(64);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.gtnewhorizons.angelica.glsm.redirect;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Handle;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class GLSMRedirectorTest {
|
||||
|
||||
private static final String GLSM = GLSMRedirector.getTargetClassName();
|
||||
|
||||
private static final Handle LAMBDA_METAFACTORY = new Handle(
|
||||
Opcodes.H_INVOKESTATIC,
|
||||
"java/lang/invoke/LambdaMetafactory",
|
||||
"metafactory",
|
||||
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;"
|
||||
+ "Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)"
|
||||
+ "Ljava/lang/invoke/CallSite;",
|
||||
false);
|
||||
|
||||
private static ClassNode makeClassWithInvokeDynamic(Handle implHandle) {
|
||||
final ClassNode cn = new ClassNode();
|
||||
cn.version = Opcodes.V1_8;
|
||||
cn.access = Opcodes.ACC_PUBLIC;
|
||||
cn.name = "com/example/TestClass";
|
||||
cn.superName = "java/lang/Object";
|
||||
cn.methods = new ArrayList<>();
|
||||
|
||||
final MethodNode mn = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "test", "()V", null, null);
|
||||
mn.instructions.add(new InvokeDynamicInsnNode(
|
||||
"run",
|
||||
"()Ljava/lang/Runnable;",
|
||||
LAMBDA_METAFACTORY,
|
||||
Type.getType("()V"),
|
||||
implHandle,
|
||||
Type.getType("()V")));
|
||||
mn.instructions.add(new InsnNode(Opcodes.RETURN));
|
||||
cn.methods.add(mn);
|
||||
|
||||
return cn;
|
||||
}
|
||||
|
||||
private static Handle getResultHandle(ClassNode cn) {
|
||||
final InvokeDynamicInsnNode dyn = (InvokeDynamicInsnNode) cn.methods.get(0).instructions.get(0);
|
||||
return (Handle) dyn.bsmArgs[1];
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeDynamicGL11HandleIsRedirected() {
|
||||
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11", "glPopMatrix", "()V", false);
|
||||
final ClassNode cn = makeClassWithInvokeDynamic(impl);
|
||||
|
||||
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
|
||||
|
||||
assertTrue(changed);
|
||||
final Handle result = getResultHandle(cn);
|
||||
assertEquals(GLSM, result.getOwner());
|
||||
assertEquals("glPopMatrix", result.getName());
|
||||
assertEquals("()V", result.getDesc());
|
||||
assertEquals(Opcodes.H_INVOKESTATIC, result.getTag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeDynamicNamedClassHandleIsRedirected() {
|
||||
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/EXTBlendFuncSeparate", "glBlendFuncSeparateEXT", "(IIII)V", false);
|
||||
final ClassNode cn = makeClassWithInvokeDynamic(impl);
|
||||
|
||||
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
|
||||
|
||||
assertTrue(changed);
|
||||
final Handle result = getResultHandle(cn);
|
||||
assertEquals(GLSM, result.getOwner());
|
||||
assertEquals("tryBlendFuncSeparate", result.getName());
|
||||
assertEquals("(IIII)V", result.getDesc());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeDynamicNonGLHandleIsNotTouched() {
|
||||
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
|
||||
final ClassNode cn = makeClassWithInvokeDynamic(impl);
|
||||
|
||||
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
|
||||
|
||||
assertFalse(changed);
|
||||
final Handle result = getResultHandle(cn);
|
||||
assertEquals("java/lang/System", result.getOwner());
|
||||
assertEquals("currentTimeMillis", result.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeDynamicUnknownGLMethodIsNotTouched() {
|
||||
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11", "glNonexistent", "()V", false);
|
||||
final ClassNode cn = makeClassWithInvokeDynamic(impl);
|
||||
|
||||
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
|
||||
|
||||
assertFalse(changed);
|
||||
final Handle result = getResultHandle(cn);
|
||||
assertEquals("org/lwjgl/opengl/GL11", result.getOwner());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokeDynamicGLCVariantIsRedirected() {
|
||||
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11C", "glDrawArrays", "(III)V", false);
|
||||
final ClassNode cn = makeClassWithInvokeDynamic(impl);
|
||||
|
||||
final boolean changed = new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
|
||||
|
||||
assertTrue(changed);
|
||||
final Handle result = getResultHandle(cn);
|
||||
assertEquals(GLSM, result.getOwner());
|
||||
assertEquals("glDrawArrays", result.getName());
|
||||
assertEquals("(III)V", result.getDesc());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transformedClassProducesValidBytecode() {
|
||||
final Handle impl = new Handle(Opcodes.H_INVOKESTATIC, "org/lwjgl/opengl/GL11", "glPopMatrix", "()V", false);
|
||||
final ClassNode cn = makeClassWithInvokeDynamic(impl);
|
||||
|
||||
new GLSMRedirector().transformClassNode("com.example.TestClass", cn);
|
||||
|
||||
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
assertDoesNotThrow(() -> {
|
||||
cn.accept(cw);
|
||||
final byte[] bytes = cw.toByteArray();
|
||||
assertTrue(bytes.length > 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 216 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 192 B |
|
After Width: | Height: | Size: 316 B |
@@ -68,6 +68,16 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
|
||||
return "OpenGL (LWJGL3)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasContext() {
|
||||
try {
|
||||
GL.getCapabilities();
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() { return 50; }
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.gtnewhorizons.angelica.api;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.redirect.GLSMRedirector;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Public API for querying Angelica's GL call redirections.
|
||||
* <p>
|
||||
* Angelica's bytecode transformer redirects GL calls (e.g., {@code GL11.glPushMatrix()}) to
|
||||
* {@link com.gtnewhorizons.angelica.glsm.GLStateManager GLStateManager} for state tracking and
|
||||
* core profile compatibility. This works for normal Java bytecode loaded through the Forge classloader,
|
||||
* but callers that bypass class transformation (e.g., Nashorn/script engines, reflection, MethodHandles)
|
||||
* need to perform their own redirection.
|
||||
* <p>
|
||||
* This class provides the redirect mapping data and a precompiled regex pattern for transforming
|
||||
* script source text before evaluation.
|
||||
*
|
||||
* <h3>Example: transforming a Nashorn script before eval</h3>
|
||||
* Given a script that uses {@code importPackage(Packages.org.lwjgl.opengl)} and calls
|
||||
* {@code GL11.glPushMatrix()}, the text transformation rewrites those calls before evaluation:
|
||||
* <pre>{@code
|
||||
* // Make GLStateManager available to the script -- either via importPackage:
|
||||
* script = "importPackage(Packages.com.gtnewhorizons.angelica.glsm);\n" + script;
|
||||
* // ... or via Java.type for an explicit binding:
|
||||
* // script = "var GLStateManager = Java.type('" + GLRedirects.TARGET_CLASS + "');\n" + script;
|
||||
*
|
||||
* // Rewrite: GL11.glPushMatrix() -> GLStateManager.glPushMatrix()
|
||||
* script = GLRedirects.getMethodRedirectPattern().matcher(script).replaceAll(mr -> {
|
||||
* String target = GLRedirects.getTargetMethodName(mr.group(0));
|
||||
* return target != null ? "GLStateManager." + target : mr.group(0);
|
||||
* });
|
||||
*
|
||||
* engine.eval(script);
|
||||
* }</pre>
|
||||
*/
|
||||
public final class GLRedirects {
|
||||
/** Fully qualified name of the GLStateManager class that all methods redirect to. */
|
||||
@SuppressWarnings("unused")
|
||||
public static final String TARGET_CLASS = "com.gtnewhorizons.angelica.glsm.GLStateManager";
|
||||
|
||||
private static final String SHORT_GL_PREFIX;
|
||||
private static final Map<String, Map<String, String>> NAMED_CLASS_SHORT_NAME_MAP;
|
||||
private static final Map<String, String> GL_PREFIX_METHOD_REDIRECTS;
|
||||
private static final Pattern METHOD_REDIRECT_PATTERN;
|
||||
|
||||
static {
|
||||
SHORT_GL_PREFIX = buildShortGLPrefix();
|
||||
NAMED_CLASS_SHORT_NAME_MAP = buildNamedClassShortNameMap();
|
||||
GL_PREFIX_METHOD_REDIRECTS = GLSMRedirector.getGLPrefixMethodRedirects();
|
||||
METHOD_REDIRECT_PATTERN = buildMethodRedirectPattern();
|
||||
}
|
||||
|
||||
private GLRedirects() {}
|
||||
|
||||
/**
|
||||
* Returns a precompiled pattern that matches redirected {@code ClassName.methodName} references
|
||||
* using short (simple) class names, as they might typically appear in script text.
|
||||
*
|
||||
* @return compiled regex pattern
|
||||
*/
|
||||
public static Pattern getMethodRedirectPattern() {
|
||||
return METHOD_REDIRECT_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the GLStateManager target method name for a short-name reference.
|
||||
*
|
||||
* @param classAndMethod short class name and method, e.g., {@code "GL11.glPushMatrix"}
|
||||
* @return the target method name on GLStateManager (e.g., {@code "glPushMatrix"}), or {@code null} if this reference is not redirected
|
||||
*/
|
||||
public static String getTargetMethodName(String classAndMethod) {
|
||||
final int dot = classAndMethod.indexOf('.');
|
||||
if (dot < 0) return null;
|
||||
|
||||
final String className = classAndMethod.substring(0, dot);
|
||||
final String methodName = classAndMethod.substring(dot + 1);
|
||||
|
||||
final Map<String, String> classRedirects = NAMED_CLASS_SHORT_NAME_MAP.get(className);
|
||||
if (classRedirects != null) {
|
||||
return classRedirects.get(methodName);
|
||||
}
|
||||
|
||||
if (className.startsWith(SHORT_GL_PREFIX)) {
|
||||
return GL_PREFIX_METHOD_REDIRECTS.get(methodName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String buildShortGLPrefix() {
|
||||
final String glPrefix = GLSMRedirector.getGLPrefix();
|
||||
return glPrefix.substring(glPrefix.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, String>> buildNamedClassShortNameMap() {
|
||||
final Map<String, Map<String, String>> named = new HashMap<>();
|
||||
for (final var entry : GLSMRedirector.getNamedClassMethodRedirects().entrySet()) {
|
||||
final String internalName = entry.getKey();
|
||||
final String shortName = internalName.substring(internalName.lastIndexOf('/') + 1);
|
||||
named.put(shortName, entry.getValue());
|
||||
}
|
||||
return named;
|
||||
}
|
||||
|
||||
private static Pattern buildMethodRedirectPattern() {
|
||||
final TreeSet<String> alternatives = new TreeSet<>();
|
||||
alternatives.add(Pattern.quote(SHORT_GL_PREFIX) + "\\w+");
|
||||
for (final String shortName : NAMED_CLASS_SHORT_NAME_MAP.keySet()) {
|
||||
alternatives.add(Pattern.quote(shortName));
|
||||
}
|
||||
final String classAlt = String.join("|", alternatives);
|
||||
return Pattern.compile("\\b(" + classAlt + ")\\.\\w+\\b");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.client.rendering;
|
||||
|
||||
import com.github.bsideup.jabel.Desugar;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.ITessellatorInstance;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
@@ -33,10 +34,30 @@ class DeferredBatchTessellator extends DirectTessellator {
|
||||
private final Vector3f scratch = new Vector3f();
|
||||
private Matrix4fc currentTransform;
|
||||
|
||||
private Tessellator parentTessellator;
|
||||
private boolean inIntercept;
|
||||
|
||||
DeferredBatchTessellator(ByteBuffer buffer) {
|
||||
super(buffer, false); // deleteAfter=false: keep buffer across resets for reuse
|
||||
}
|
||||
|
||||
void setParentTessellator(Tessellator parent) {
|
||||
this.parentTessellator = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int draw() {
|
||||
if (!inIntercept) {
|
||||
final Tessellator parent = parentTessellator;
|
||||
if (parent != null && parent.isDrawing) {
|
||||
interceptDraw(parent);
|
||||
((ITessellatorInstance) parent).discard();
|
||||
}
|
||||
}
|
||||
isDrawing = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long writeVertexData(VertexFormat format, int[] rawBuffer, int rawBufferIndex) {
|
||||
if (currentTransform != null) {
|
||||
@@ -53,27 +74,32 @@ class DeferredBatchTessellator extends DirectTessellator {
|
||||
|
||||
@Override
|
||||
protected int interceptDraw(Tessellator tess) {
|
||||
// Compute delta: if current MV differs from default, pre-transform vertices
|
||||
final Matrix4f currentMV = GLStateManager.getModelViewMatrix();
|
||||
if (!currentMV.equals(defaultModelview)) {
|
||||
defaultModelviewInverse.mul(currentMV, deltaMatrix);
|
||||
this.currentTransform = deltaMatrix;
|
||||
} else {
|
||||
this.currentTransform = null;
|
||||
inIntercept = true;
|
||||
try {
|
||||
// Compute delta: if current MV differs from default, pre-transform vertices
|
||||
final Matrix4f currentMV = GLStateManager.getModelViewMatrix();
|
||||
if (!currentMV.equals(defaultModelview)) {
|
||||
defaultModelviewInverse.mul(currentMV, deltaMatrix);
|
||||
this.currentTransform = deltaMatrix;
|
||||
} else {
|
||||
this.currentTransform = null;
|
||||
}
|
||||
|
||||
final int byteOffsetBefore = bufferLimit();
|
||||
final int vertCount = tess.vertexCount;
|
||||
final int drawMode = tess.drawMode;
|
||||
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
|
||||
final long stateKey = DeferredDrawBatcher.captureStateKey();
|
||||
|
||||
final int result = super.interceptDraw(tess); // repacks int[] -> ByteBuffer + transforms if vertexTransform set
|
||||
|
||||
final int byteOffsetAfter = bufferLimit();
|
||||
ranges.add(new DrawRange(stateKey, byteOffsetBefore, byteOffsetAfter - byteOffsetBefore, vertCount, drawMode, flags));
|
||||
this.currentTransform = null; // clear for next draw
|
||||
return result;
|
||||
} finally {
|
||||
inIntercept = false;
|
||||
}
|
||||
|
||||
final int byteOffsetBefore = bufferLimit();
|
||||
final int vertCount = tess.vertexCount;
|
||||
final int drawMode = tess.drawMode;
|
||||
final int flags = VertexFlags.convertToFlags(tess.hasTexture, tess.hasColor, tess.hasNormals, tess.hasBrightness);
|
||||
final long stateKey = DeferredDrawBatcher.captureStateKey();
|
||||
|
||||
final int result = super.interceptDraw(tess); // repacks int[] → ByteBuffer + transforms if vertexTransform set
|
||||
|
||||
final int byteOffsetAfter = bufferLimit();
|
||||
ranges.add(new DrawRange(stateKey, byteOffsetBefore, byteOffsetAfter - byteOffsetBefore, vertCount, drawMode, flags));
|
||||
this.currentTransform = null; // clear for next draw
|
||||
return result;
|
||||
}
|
||||
|
||||
List<DrawRange> getRanges() { return ranges; }
|
||||
@@ -86,6 +112,7 @@ class DeferredBatchTessellator extends DirectTessellator {
|
||||
|
||||
@Override
|
||||
protected void onRemovedFromStack() {
|
||||
parentTessellator = null;
|
||||
clearRanges();
|
||||
super.onRemovedFromStack(); // calls reset(), keeps baseBuffer (deleteAfter=false)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.gtnewhorizons.angelica.client.rendering;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import lombok.Getter;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.Comparator;
|
||||
@@ -37,6 +39,7 @@ public class DeferredDrawBatcher {
|
||||
batchTessellator = new DeferredBatchTessellator(memAlloc(INITIAL_BUFFER_SIZE));
|
||||
}
|
||||
batchTessellator.clearRanges();
|
||||
batchTessellator.setParentTessellator(Tessellator.instance);
|
||||
batchTessellator.snapshotDefaultModelview();
|
||||
TessellatorManager.startCapturingDirect(batchTessellator);
|
||||
}
|
||||
@@ -106,12 +109,33 @@ public class DeferredDrawBatcher {
|
||||
break;
|
||||
}
|
||||
|
||||
TessellatorStreamingDrawer.drawPackedBatch(batchTessellator, ranges, i, subEnd, totalBytes, totalVertices, drawMode, flags);
|
||||
drawPackedBatch(batchTessellator, ranges, i, subEnd, totalBytes, totalVertices, drawMode, flags);
|
||||
|
||||
i = subEnd;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack ranges from a DeferredBatchTessellator into the streaming drawer's repack buffer
|
||||
* and issue a single draw call for the merged batch.
|
||||
*/
|
||||
private static void drawPackedBatch(DeferredBatchTessellator source, List<DeferredBatchTessellator.DrawRange> ranges, int from, int to, int totalBytes, int totalVertices, int drawMode, int flags) {
|
||||
if (totalVertices == 0) return;
|
||||
|
||||
TessellatorStreamingDrawer.ensureRepackCapacity(totalBytes);
|
||||
long writePos = TessellatorStreamingDrawer.getRepackAddress();
|
||||
for (int j = from; j < to; j++) {
|
||||
final DeferredBatchTessellator.DrawRange r = ranges.get(j);
|
||||
source.copyRange(r.byteOffset(), r.byteLength(), writePos);
|
||||
writePos += r.byteLength();
|
||||
}
|
||||
final java.nio.ByteBuffer buf = TessellatorStreamingDrawer.getRepackBuffer();
|
||||
buf.position(0);
|
||||
buf.limit(totalBytes);
|
||||
|
||||
TessellatorStreamingDrawer.drawPacked(buf, drawMode, flags, totalVertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack current GLSM state into a long key for grouping.
|
||||
* Layout: [tex1En:1][tex0En:1][textureId:20][srcRgb:12][dstRgb:12][blendEnabled:1][depthMask:1] = 48 bits
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.client.rendering;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import net.coderbot.iris.gl.state.StateUpdateNotifiers;
|
||||
import net.minecraft.client.renderer.texture.AbstractTexture;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -15,11 +13,15 @@ public class TextureTracker {
|
||||
*/
|
||||
|
||||
public static final TextureTracker INSTANCE = new TextureTracker();
|
||||
|
||||
|
||||
private static Runnable bindTextureListener;
|
||||
|
||||
static {
|
||||
StateUpdateNotifiers.bindTextureNotifier = listener -> bindTextureListener = listener;
|
||||
}
|
||||
|
||||
private final Int2ObjectMap<AbstractTexture> textures = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
private boolean lockBindCallback;
|
||||
|
||||
private TextureTracker() {
|
||||
}
|
||||
|
||||
@@ -31,23 +33,13 @@ public class TextureTracker {
|
||||
public AbstractTexture getTexture(int id) {
|
||||
return textures.get(id);
|
||||
}
|
||||
|
||||
public void onBindTexture(int id) {
|
||||
if (lockBindCallback) {
|
||||
return;
|
||||
}
|
||||
if (GLStateManager.getActiveTextureUnit() == 0) {
|
||||
lockBindCallback = true;
|
||||
if (GLSMHooks.TEXTURE_BIND.hasListeners()) {
|
||||
GLSMHooks.textureBindEvent.textureId = id;
|
||||
GLSMHooks.TEXTURE_BIND.post(GLSMHooks.textureBindEvent);
|
||||
}
|
||||
// Reset texture state
|
||||
RenderSystem.bindTextureToUnit(0, id);
|
||||
lockBindCallback = false;
|
||||
|
||||
public void onBindTexture() {
|
||||
if (bindTextureListener != null) {
|
||||
bindTextureListener.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void onDeleteTexture(int id) {
|
||||
textures.remove(id);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class ModStatus {
|
||||
public static boolean isThaumicHorizonsLoaded;
|
||||
public static boolean isBaublesLoaded;
|
||||
public static boolean isFluidLoggedLoaded;
|
||||
public static boolean isBOPLoaded;
|
||||
|
||||
public static void preInit() {
|
||||
if (Loader.isModLoaded("backhand")) {
|
||||
@@ -45,6 +46,7 @@ public class ModStatus {
|
||||
isFluidLoggedLoaded = Loader.isModLoaded("fluidlogged");
|
||||
|
||||
isHoloInventoryLoaded = Loader.isModLoaded("holoinventory");
|
||||
isBOPLoaded = Loader.isModLoaded("BiomesOPlenty");
|
||||
|
||||
// remove compat with original release of BG2
|
||||
if (isBattlegearLoaded){
|
||||
|
||||
@@ -26,12 +26,11 @@ public class Camera {
|
||||
|
||||
private float pitch;
|
||||
private float yaw;
|
||||
private EntityLivingBase entity;
|
||||
private boolean isSleeping;
|
||||
private boolean thirdPerson;
|
||||
private float partialTicks;
|
||||
|
||||
private Camera() {
|
||||
}
|
||||
private Camera() {}
|
||||
|
||||
/**
|
||||
* Updates the camera position and orientation based on the current entity and partial ticks.
|
||||
@@ -41,7 +40,7 @@ public class Camera {
|
||||
*/
|
||||
public void update(EntityLivingBase entity, float partialTicks) {
|
||||
this.partialTicks = partialTicks;
|
||||
this.entity = entity;
|
||||
this.isSleeping = entity.isPlayerSleeping();
|
||||
|
||||
// Entity position (interpolated, at eye level)
|
||||
final double entityX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks;
|
||||
|
||||
@@ -235,6 +235,11 @@ public class AngelicaConfig {
|
||||
@Config.DefaultBoolean(false)
|
||||
public static boolean enableHardcodedCustomUniforms;
|
||||
|
||||
@Config.Comment("Modern MC_VERSION to try if shader pack has no 1.7.10 section. 0 = default (260101)")
|
||||
@Config.DefaultInt(0)
|
||||
@Config.RangeInt(min = 0)
|
||||
public static int modernFallbackMcVersion;
|
||||
|
||||
@Config.Comment("Define IS_IRIS in shader macros.")
|
||||
@Config.DefaultBoolean(true)
|
||||
public static boolean defineIsIris;
|
||||
@@ -262,6 +267,11 @@ public class AngelicaConfig {
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public boolean narrowAlfheim;
|
||||
|
||||
@Config.Comment("Narrow Ears transformer exclusions to allow GL redirection")
|
||||
@Config.DefaultBoolean(true)
|
||||
@Config.RequiresMcRestart
|
||||
public boolean narrowEars;
|
||||
}
|
||||
|
||||
@Config.Comment("Renders chunks before neighbors are ready. Improves loading at render distance edges, useful for low render distance servers.")
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.iris;
|
||||
|
||||
import com.gtnewhorizons.angelica.client.rendering.TextureTracker;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredAlphaHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredDepthColorHandler;
|
||||
@@ -10,6 +11,7 @@ import net.coderbot.iris.gl.blending.AlphaTestStorage;
|
||||
import net.coderbot.iris.gl.blending.BlendModeStorage;
|
||||
import net.coderbot.iris.gl.blending.DepthColorStorage;
|
||||
import net.coderbot.iris.gl.program.ProgramUniforms;
|
||||
import net.coderbot.iris.gl.state.StateUpdateNotifiers;
|
||||
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
|
||||
import net.coderbot.iris.pipeline.WorldRenderingPipeline;
|
||||
import net.coderbot.iris.samplers.IrisSamplers;
|
||||
@@ -18,6 +20,24 @@ 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() {
|
||||
@@ -76,8 +96,31 @@ public class IrisGLSMBridge {
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
if (fogStartListener != null) fogStartListener.run();
|
||||
if (fogEndListener != null) fogEndListener.run();
|
||||
if (fogDensityListener != null) fogDensityListener.run();
|
||||
}
|
||||
});
|
||||
|
||||
GLSMHooks.TEXTURE_BIND.addListener(event -> {
|
||||
if (Iris.enabled) {
|
||||
TextureTracker.INSTANCE.onBindTexture();
|
||||
final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
|
||||
if (pipeline != null) {
|
||||
pipeline.onBindTexture(event.textureId);
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.gtnewhorizon.gtnhmixins.builders.ITransformers;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.CompatConfig;
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.DependencyVerifier;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.EcosystemNarrowRules;
|
||||
import com.gtnewhorizons.angelica.loading.fml.compat.CompatHandlers;
|
||||
import com.gtnewhorizons.angelica.mixins.Mixins;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.SharedConfig;
|
||||
@@ -91,30 +93,22 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (handle != null) {
|
||||
handle.exclusions().add("org.embeddedt.embeddium");
|
||||
handle.exclusions().add("org.taumc.celeritas");
|
||||
handle.exclusions().add("com.mitchej123.lwjgl.lwjgl3");
|
||||
handle.exclusions().add("com.mitchej123.glsm");
|
||||
handle.exclusions().add("com.gtnewhorizons.angelica.lwjgl3");
|
||||
for (String exclusion : EcosystemNarrowRules.LWJGL3IFY_EXCLUSIONS_SHARED) {
|
||||
handle.exclusions().add(exclusion);
|
||||
}
|
||||
handle.exclusions().add("com.gtnewhorizons.angelica.vulkan");
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyDependencies() {
|
||||
// Check for fastutil (bundled with GTNHLib since 0.2.1)
|
||||
if (AngelicaTweaker.class.getResource("/it/unimi/dsi/fastutil/ints/Int2ObjectMap.class") == null) {
|
||||
throw new RuntimeException("Missing dependency: Angelica requires GTNHLib! Download: https://modrinth.com/mod/gtnhlib");
|
||||
}
|
||||
|
||||
// Check for PrimitiveExtractor/cel.model classes (added in GTNHLib 0.8.21)
|
||||
if (AngelicaTweaker.class.getResource("/com/gtnewhorizon/gtnhlib/client/renderer/PrimitiveExtractor.class") == null) {
|
||||
throw new RuntimeException("GTNHLib is outdated: Angelica requires GTNHLib 0.8.21 or newer! Download: https://modrinth.com/mod/gtnhlib");
|
||||
}
|
||||
|
||||
// Check for jvmdowngrader stubs (bundled with GTNHLib since 0.9.0)
|
||||
if (AngelicaTweaker.class.getResource("/xyz/wagyourtail/jvmdg/exc/MissingStubError.class") == null) {
|
||||
throw new RuntimeException("GTNHLib is outdated: Angelica requires GTNHLib 0.9.0 or newer! Download: https://modrinth.com/mod/gtnhlib");
|
||||
}
|
||||
DependencyVerifier.verify(AngelicaTweaker.class, List.of(
|
||||
new DependencyVerifier.Check(
|
||||
"/it/unimi/dsi/fastutil/ints/Int2ObjectMap.class",
|
||||
"Missing dependency: Angelica requires GTNHLib! 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")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -152,6 +146,7 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
|
||||
narrowTransformerConfig("Xaeros", AngelicaConfig.transformerCompat.narrowXaeros);
|
||||
narrowTransformerConfig("AdvancedLightsabers", AngelicaConfig.transformerCompat.narrowAdvancedLightsabers);
|
||||
narrowTransformerConfig("Alfheim", AngelicaConfig.transformerCompat.narrowAlfheim);
|
||||
narrowTransformerConfig("Ears", AngelicaConfig.transformerCompat.narrowEars);
|
||||
|
||||
tweaks.add("com.gtnewhorizons.angelica.loading.fml.tweakers.IncompatibleModsDisablerTweaker");
|
||||
if (AngelicaConfig.enableHudCaching) {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.loading.fml.transformers;
|
||||
|
||||
import com.gtnewhorizons.angelica.loading.shared.transformers.AngelicaRedirector;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.EarlyRedirectorCore;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.EcosystemNarrowRules;
|
||||
import net.minecraft.launchwrapper.IClassTransformer;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
/**
|
||||
* A scoped redirector that only transforms classes from known-misbehaving (core)mod packages.
|
||||
@@ -13,52 +11,20 @@ import org.objectweb.asm.tree.ClassNode;
|
||||
* Those classes get missed by the late-registered {@link AngelicaRedirectorTransformer}, so their
|
||||
* GL calls permanently bypass GLSM.
|
||||
* <p>
|
||||
* This transformer is registered in the {@code AngelicaTweaker} constructor (before any
|
||||
* coremod's {@code injectData}) and is removed by {@code AngelicaLateTweaker} once the
|
||||
* full redirector is in its proper post-mixin position.
|
||||
* <p>
|
||||
* The mixin service will add this to its delegation list, but it no-ops on game classes
|
||||
* since it only targets specific mod packages.
|
||||
* Registered in {@code AngelicaClientTweaker} constructor. Removed by {@code AngelicaLateTweaker}
|
||||
* once the full redirector is in its proper post-mixin position.
|
||||
*
|
||||
* @see EcosystemNarrowRules#EARLY_REDIRECTOR_TARGETS
|
||||
*/
|
||||
public class EarlyRedirectorTransformer implements IClassTransformer {
|
||||
|
||||
private static final String[] TARGET_PACKAGES = {
|
||||
"cn.tesseract.mycelium.",
|
||||
};
|
||||
|
||||
private final AngelicaRedirector inner = new AngelicaRedirector();
|
||||
private final String[] exclusions = inner.getTransformerExclusions();
|
||||
private final EarlyRedirectorCore impl = new EarlyRedirectorCore(
|
||||
"com.gtnewhorizons.angelica.lwjgl3.",
|
||||
"com.gtnewhorizons.angelica.transform"
|
||||
);
|
||||
|
||||
@Override
|
||||
public byte[] transform(String name, String transformedName, byte[] basicClass) {
|
||||
if (basicClass == null) return null;
|
||||
|
||||
boolean targeted = false;
|
||||
for (String pkg : TARGET_PACKAGES) {
|
||||
if (transformedName.startsWith(pkg)) {
|
||||
targeted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!targeted) return basicClass;
|
||||
|
||||
for (String exclusion : exclusions) {
|
||||
if (transformedName.startsWith(exclusion)) return basicClass;
|
||||
}
|
||||
|
||||
if (!inner.shouldTransform(basicClass)) {
|
||||
return basicClass;
|
||||
}
|
||||
|
||||
final ClassReader cr = new ClassReader(basicClass);
|
||||
final ClassNode cn = new ClassNode();
|
||||
cr.accept(cn, 0);
|
||||
final boolean changed = inner.transformClassNode(transformedName, cn);
|
||||
if (changed) {
|
||||
final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cn.accept(cw);
|
||||
return cw.toByteArray();
|
||||
}
|
||||
return basicClass;
|
||||
return impl.transform(transformedName, basicClass);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.loading.fml.tweakers;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.loading.EcosystemNarrowRules;
|
||||
import com.gtnewhorizons.angelica.glsm.loading.TransformerNarrower;
|
||||
import cpw.mods.fml.relauncher.FMLRelaunchLog;
|
||||
import net.minecraft.launchwrapper.IClassTransformer;
|
||||
import net.minecraft.launchwrapper.ITweaker;
|
||||
@@ -27,8 +29,6 @@ public class AngelicaLateTweaker implements ITweaker {
|
||||
|
||||
/**
|
||||
* Reduces overly broad transformer exclusions that prevent AngelicaRedirector from doing its job.
|
||||
*
|
||||
* Looking at you, DragonAPI.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void narrowTransformerExclusions(LaunchClassLoader classLoader) {
|
||||
@@ -37,73 +37,14 @@ public class AngelicaLateTweaker implements ITweaker {
|
||||
exceptionsField.setAccessible(true);
|
||||
final Set<String> exceptions = (Set<String>) exceptionsField.get(classLoader);
|
||||
|
||||
if (narrowEnabled("DragonAPI") && exceptions.remove("Reika.DragonAPI.ASM")) {
|
||||
// Re-add non-GL classes; ASMCallsClient and utility classes with GL calls are left exposed
|
||||
exceptions.add("Reika.DragonAPI.ASM.Patchers");
|
||||
exceptions.add("Reika.DragonAPI.ASM.Profiling");
|
||||
exceptions.add("Reika.DragonAPI.ASM.APIStripper");
|
||||
exceptions.add("Reika.DragonAPI.ASM.ClassReparenter");
|
||||
exceptions.add("Reika.DragonAPI.ASM.DependentMethodStripper");
|
||||
exceptions.add("Reika.DragonAPI.ASM.DragonAPIClassTransformer");
|
||||
exceptions.add("Reika.DragonAPI.ASM.FMLItemBlockPatch");
|
||||
exceptions.add("Reika.DragonAPI.ASM.FluidNamePatch");
|
||||
exceptions.add("Reika.DragonAPI.ASM.InterfaceInjector");
|
||||
exceptions.add("Reika.DragonAPI.ASM.SpecialBiomePlacement");
|
||||
exceptions.add("Reika.DragonAPI.ASM.StructureLootHooks");
|
||||
FMLRelaunchLog.info("[Angelica] Narrowed Reika.DragonAPI.ASM transformer exclusion to allow GL redirection");
|
||||
}
|
||||
TransformerNarrower.narrow(exceptions, Launch.blackboard, "angelica", "Angelica",
|
||||
EcosystemNarrowRules.ALL);
|
||||
|
||||
if (narrowEnabled("Xaeros")) {
|
||||
if (exceptions.remove("xaero.common.core")) {
|
||||
exceptions.add("xaero.common.core.transformer");
|
||||
FMLRelaunchLog.info("[Angelica] Narrowed xaero.common.core transformer exclusion to allow GL redirection");
|
||||
}
|
||||
if (exceptions.remove("xaero.map.core")) {
|
||||
exceptions.add("xaero.map.core.transformer");
|
||||
FMLRelaunchLog.info("[Angelica] Narrowed xaero.map.core transformer exclusion to allow GL redirection");
|
||||
}
|
||||
}
|
||||
|
||||
if (narrowEnabled("AdvancedLightsabers") && exceptions.remove("com.fiskmods.lightsabers.asm")) {
|
||||
exceptions.add("com.fiskmods.lightsabers.asm.ALLoadingPlugin");
|
||||
exceptions.add("com.fiskmods.lightsabers.asm.ASMHooks");
|
||||
exceptions.add("com.fiskmods.lightsabers.asm.transformers");
|
||||
FMLRelaunchLog.info("[Angelica] Narrowed com.fiskmods.lightsabers.asm transformer exclusion to allow GL redirection");
|
||||
}
|
||||
|
||||
if (narrowEnabled("Alfheim") && exceptions.remove("alfheim.common.core.asm.hook")) {
|
||||
exceptions.add("alfheim.common.core.asm.hook.AlfheimFieldHookHandler");
|
||||
exceptions.add("alfheim.common.core.asm.hook.AlfheimHPHooks");
|
||||
exceptions.add("alfheim.common.core.asm.hook.Botania18AndUpBackport");
|
||||
exceptions.add("alfheim.common.core.asm.hook.ElementalDamageAdapter");
|
||||
|
||||
exceptions.add("alfheim.common.core.asm.hook.integration");
|
||||
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.FurnaceExtender");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.ItemAuraRingExtender");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.ItemLensExtender");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.ItemTwigWandExtender");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.LightRelayExtender");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.PureDaisyExtender");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.QuartzExtender");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.RelicHooks");
|
||||
exceptions.add("alfheim.common.core.asm.hook.extender.SparkExtender");
|
||||
|
||||
exceptions.add("alfheim.common.core.asm.hook.fixes.BotaniaGlowingRenderFixes");
|
||||
exceptions.add("alfheim.common.core.asm.hook.fixes.CorporeaInputFix");
|
||||
exceptions.add("alfheim.common.core.asm.hook.fixes.GodAttributesHooks");
|
||||
exceptions.add("alfheim.common.core.asm.hook.fixes.RecipeAncientWillsFix");
|
||||
FMLRelaunchLog.info("[Angelica] Narrowed alfheim.common.core.asm.hook transformer exclusion to allow GL redirection");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
FMLRelaunchLog.warning("[Angelica] Failed to narrow transformer exclusions: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean narrowEnabled(String mod) {
|
||||
return !Boolean.FALSE.equals(Launch.blackboard.get("angelica.narrow." + mod));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLaunchTarget() {
|
||||
return null;
|
||||
|
||||
@@ -15,6 +15,7 @@ public class AngelicaRfbPlugin implements RfbPlugin {
|
||||
@Override
|
||||
public void onConstruction(@NotNull PluginContext ctx) {
|
||||
Launch.blackboard.put("angelica.rfbPluginLoaded", Boolean.TRUE);
|
||||
Launch.classLoader.addClassLoaderExclusion("com.gtnewhorizons.angelica.glsm.redirect.");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,7 +37,14 @@ public enum Mixins implements IMixins {
|
||||
, "angelica.MixinMinecraft_FrameHook"
|
||||
, "angelica.MixinMinecraftServer"
|
||||
, "angelica.bugfixes.MixinItemRenderer_EdgeDepth"
|
||||
, "angelica.bugfixes.MixinModelCreeper_AuraBodyInflate"
|
||||
, "angelica.bugfixes.MixinModelWither_ArmorCentering"
|
||||
, "angelica.bugfixes.MixinRenderBlocks_CrossedSquaresNormal"
|
||||
, "angelica.bugfixes.MixinRenderCreeper_AuraDepth"
|
||||
, "angelica.bugfixes.MixinRenderGlobal_DeferredEntityOverlay"
|
||||
, "angelica.bugfixes.MixinRenderGlobal_DestroyBlock"
|
||||
, "angelica.bugfixes.MixinRendererLivingEntity_DeferredEntityOverlay"
|
||||
, "angelica.bugfixes.MixinRenderWither_ArmorCentering"
|
||||
, "angelica.bugfixes.MixinRendererLivingEntity_EyeDepth"
|
||||
, "angelica.debug.MixinMinecraft_FPSCap"
|
||||
, "angelica.ffp.MixinTessellator_CoreProfile"
|
||||
@@ -237,6 +244,7 @@ public enum Mixins implements IMixins {
|
||||
.setApplyIf(() -> AngelicaConfig.enableIris)
|
||||
.addClientMixins(
|
||||
"shaders.MixinDroppedItemGlintEdges"
|
||||
, "shaders.MixinHeldItemGlintEdges"
|
||||
, "shaders.MixinEntityPickupFX"
|
||||
, "shaders.MixinEntityRenderer"
|
||||
, "shaders.MixinGuiIngameForge"
|
||||
@@ -249,6 +257,7 @@ public enum Mixins implements IMixins {
|
||||
, "shaders.MixinRenderEntityFlame"
|
||||
, "shaders.MixinRendererLivingEntity"
|
||||
, "shaders.MixinRenderGlobal"
|
||||
, "shaders.AccessorEntityHorse"
|
||||
, "shaders.MixinRenderHorse"
|
||||
, "shaders.MixinRenderItem"
|
||||
, "shaders.MixinRenderNameTag"
|
||||
@@ -278,6 +287,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)
|
||||
@@ -371,17 +388,16 @@ public enum Mixins implements IMixins {
|
||||
.addRequiredMod(TargetedMod.MINEFACTORY_RELOADED)
|
||||
.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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.proxy;
|
||||
|
||||
import biomesoplenty.api.content.BOPCBlocks;
|
||||
import com.google.common.base.Objects;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
|
||||
@@ -42,6 +43,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;
|
||||
@@ -85,6 +87,7 @@ public final class ClientProxy extends CommonProxy {
|
||||
private final Minecraft mc = Minecraft.getMinecraft();
|
||||
private final FrametimeGraph frametimeGraph = new FrametimeGraph();
|
||||
private final TPSGraph tpsGraph = new TPSGraph();
|
||||
public static Block bopGrass;
|
||||
|
||||
public static SodiumGameOptions options() {
|
||||
if (CONFIG == null) {
|
||||
@@ -182,6 +185,9 @@ public final class ClientProxy extends CommonProxy {
|
||||
LOGGER.error("Could not replace LOTR handle render code with thread safe version");
|
||||
}
|
||||
}
|
||||
if(ModStatus.isBOPLoaded) {
|
||||
bopGrass = BOPCBlocks.newBopGrass;
|
||||
}
|
||||
}
|
||||
|
||||
float lastIntegratedTickTime;
|
||||
@@ -337,10 +343,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,215 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import lombok.Getter;
|
||||
import net.coderbot.iris.layer.GbufferPrograms;
|
||||
import net.coderbot.iris.uniforms.CapturedRenderingState;
|
||||
import net.coderbot.iris.uniforms.EntityIdHelper;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import net.minecraft.client.renderer.entity.RendererLivingEntity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Defers entity overlay rendering (auras, armor effects, etc.) to after all entities have been drawn.
|
||||
* This lets us fix z-fighting between overlay's own coplanar faces by disabling the depth mask,
|
||||
* and ensures the overlay composites correctly on top of all opaque geometry.
|
||||
*
|
||||
* Used by both the charged creeper aura and the Wither armor overlay.
|
||||
*
|
||||
* The deferred render replays shouldRenderPass on the original renderer instance so that any
|
||||
* mod mixins targeting that method still fire during the deferred pass.
|
||||
*/
|
||||
public class DeferredEntityOverlay {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ShouldRenderPassFn {
|
||||
int invoke(EntityLivingBase entity, int pass, float partialTick);
|
||||
}
|
||||
|
||||
private static final int INITIAL_CAPACITY = 4;
|
||||
private static final List<DeferredEntry> deferred = new ArrayList<>(INITIAL_CAPACITY);
|
||||
private static final List<DeferredEntry> pool = new ArrayList<>(INITIAL_CAPACITY);
|
||||
|
||||
private static final FloatBuffer MATRIX_BUF = BufferUtils.createFloatBuffer(16);
|
||||
|
||||
// Set by the shouldRenderPass inject, consumed by the doRender WrapOperation.
|
||||
@Getter
|
||||
private static boolean overlayPassActive = false;
|
||||
|
||||
// Set during deferred flush to prevent re-deferring when shouldRenderPass is replayed.
|
||||
@Getter
|
||||
private static boolean replaying = false;
|
||||
|
||||
// Pending context from the shouldRenderPass inject, consumed by deferRender.
|
||||
private static ShouldRenderPassFn pendingShouldRenderPass;
|
||||
private static RendererLivingEntity pendingRenderer;
|
||||
private static EntityLivingBase pendingEntity;
|
||||
private static float pendingPartialTick;
|
||||
|
||||
/**
|
||||
* Called from shouldRenderPass HEAD injects. Marks that the upcoming renderPassModel.render()
|
||||
* should be deferred. If a previous mark was not consumed (shouldRenderPass returned <= 0,
|
||||
* so render() was never called).
|
||||
*/
|
||||
public static void markOverlayPass(ShouldRenderPassFn shouldRenderPass, RendererLivingEntity renderer,
|
||||
EntityLivingBase entity, float partialTick) {
|
||||
overlayPassActive = true;
|
||||
pendingShouldRenderPass = shouldRenderPass;
|
||||
pendingRenderer = renderer;
|
||||
pendingEntity = entity;
|
||||
pendingPartialTick = partialTick;
|
||||
}
|
||||
|
||||
public static void clearStaleOverlayFlag() {
|
||||
overlayPassActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the WrapOperation on renderPassModel.render() in doRender.
|
||||
* Captures the modelview matrix and animation params, skipping the actual render.
|
||||
*/
|
||||
public static void deferRender(float limbSwing, float limbSwingAmount, float ageInTicks,
|
||||
float headYaw, float headPitch, float scale) {
|
||||
overlayPassActive = false;
|
||||
|
||||
DeferredEntry entry = acquireEntry();
|
||||
entry.set(pendingShouldRenderPass, pendingRenderer, pendingEntity, pendingPartialTick,
|
||||
limbSwing, limbSwingAmount, ageInTicks, headYaw, headPitch, scale);
|
||||
|
||||
MATRIX_BUF.clear();
|
||||
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, MATRIX_BUF);
|
||||
MATRIX_BUF.get(entry.matrix);
|
||||
|
||||
deferred.add(entry);
|
||||
|
||||
pendingShouldRenderPass = null;
|
||||
pendingRenderer = null;
|
||||
pendingEntity = null;
|
||||
}
|
||||
|
||||
private static DeferredEntry acquireEntry() {
|
||||
int size = pool.size();
|
||||
if (size > 0) {
|
||||
return pool.remove(size - 1);
|
||||
}
|
||||
return new DeferredEntry();
|
||||
}
|
||||
|
||||
private static void recycle() {
|
||||
for (DeferredEntry entry : deferred) {
|
||||
entry.shouldRenderPass = null;
|
||||
entry.renderer = null;
|
||||
entry.entity = null;
|
||||
pool.add(entry);
|
||||
}
|
||||
deferred.clear();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
recycle();
|
||||
overlayPassActive = false;
|
||||
replaying = false;
|
||||
}
|
||||
|
||||
public static void renderAll() {
|
||||
if (deferred.isEmpty()) return;
|
||||
|
||||
GbufferPrograms.beginEntities();
|
||||
try {
|
||||
for (DeferredEntry entry : deferred) {
|
||||
CapturedRenderingState.INSTANCE.setCurrentEntity(EntityIdHelper.getEntityId(entry.entity));
|
||||
renderOverlay(entry);
|
||||
}
|
||||
} finally {
|
||||
CapturedRenderingState.INSTANCE.setCurrentEntity(-1);
|
||||
GbufferPrograms.endEntities();
|
||||
}
|
||||
recycle();
|
||||
}
|
||||
|
||||
private static void renderOverlay(DeferredEntry entry) {
|
||||
EntityLivingBase entity = entry.entity;
|
||||
|
||||
// Restore the entity's lightmap coords
|
||||
int brightness = entity.getBrightnessForRender(entry.partialTick);
|
||||
OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit,
|
||||
brightness % 65536, (float) brightness / 65536);
|
||||
|
||||
// Restore the saved modelview matrix
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glPushMatrix();
|
||||
boolean depthMaskDisabled = false;
|
||||
try {
|
||||
MATRIX_BUF.clear();
|
||||
MATRIX_BUF.put(entry.matrix);
|
||||
MATRIX_BUF.flip();
|
||||
GLStateManager.glLoadMatrix(MATRIX_BUF);
|
||||
|
||||
// Replay shouldRenderPass pass 1
|
||||
replaying = true;
|
||||
int result = entry.shouldRenderPass.invoke(entity, 1, entry.partialTick);
|
||||
if (result > 0) {
|
||||
// Disable depth writes so coplanar faces don't z-fight each other
|
||||
GLStateManager.glDepthMask(false);
|
||||
depthMaskDisabled = true;
|
||||
|
||||
// Disable backface culling so both sides of the overlay shell are visible
|
||||
GLStateManager.glDisable(GL11.GL_CULL_FACE);
|
||||
|
||||
// Render using the model set by shouldRenderPass
|
||||
entry.renderer.renderPassModel.setLivingAnimations(entity,
|
||||
entry.limbSwing, entry.limbSwingAmount, entry.partialTick);
|
||||
|
||||
entry.renderer.renderPassModel.render(entity,
|
||||
entry.limbSwing, entry.limbSwingAmount, entry.ageInTicks,
|
||||
entry.headYaw, entry.headPitch, entry.scale);
|
||||
}
|
||||
|
||||
// Replay shouldRenderPass pass 2
|
||||
entry.shouldRenderPass.invoke(entity, 2, entry.partialTick);
|
||||
} finally {
|
||||
replaying = false;
|
||||
if (depthMaskDisabled) {
|
||||
GLStateManager.glDepthMask(true);
|
||||
GLStateManager.glEnable(GL11.GL_CULL_FACE);
|
||||
}
|
||||
GLStateManager.glPopMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeferredEntry {
|
||||
final float[] matrix = new float[16];
|
||||
ShouldRenderPassFn shouldRenderPass;
|
||||
RendererLivingEntity renderer;
|
||||
EntityLivingBase entity;
|
||||
float partialTick;
|
||||
float limbSwing;
|
||||
float limbSwingAmount;
|
||||
float ageInTicks;
|
||||
float headYaw;
|
||||
float headPitch;
|
||||
float scale;
|
||||
|
||||
void set(ShouldRenderPassFn shouldRenderPass, RendererLivingEntity renderer,
|
||||
EntityLivingBase entity, float partialTick,
|
||||
float limbSwing, float limbSwingAmount, float ageInTicks,
|
||||
float headYaw, float headPitch, float scale) {
|
||||
this.shouldRenderPass = shouldRenderPass;
|
||||
this.renderer = renderer;
|
||||
this.entity = entity;
|
||||
this.partialTick = partialTick;
|
||||
this.limbSwing = limbSwing;
|
||||
this.limbSwingAmount = limbSwingAmount;
|
||||
this.ageInTicks = ageInTicks;
|
||||
this.headYaw = headYaw;
|
||||
this.headPitch = headPitch;
|
||||
this.scale = scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import net.minecraft.client.model.ModelBox;
|
||||
import net.minecraft.client.model.ModelHorse;
|
||||
import net.minecraft.client.model.ModelRenderer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Horse model with all boxes inflated outward, used as a separate armor layer.
|
||||
* Extends ModelHorse to inherit all animation and render logic. The constructor
|
||||
* builds the default geometry, then we rebuild every box with inflate applied.
|
||||
* This mirrors how modern MC uses a separate ModelLayers.HORSE_ARMOR.
|
||||
*/
|
||||
public class ModelHorseArmor extends ModelHorse {
|
||||
|
||||
public ModelHorseArmor(float inflate) {
|
||||
super();
|
||||
for (ModelRenderer renderer : this.boxList) {
|
||||
List<ModelBox> originalBoxes = new ArrayList<>(renderer.cubeList);
|
||||
renderer.cubeList.clear();
|
||||
for (ModelBox box : originalBoxes) {
|
||||
int width = Math.round(box.posX2 - box.posX1);
|
||||
int height = Math.round(box.posY2 - box.posY1);
|
||||
int depth = Math.round(box.posZ2 - box.posZ1);
|
||||
renderer.addBox(box.posX1, box.posY1, box.posZ1, width, height, depth, inflate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,8 @@ public interface StateAwareTessellator {
|
||||
void angelica$setCeleritasMeshing(boolean active);
|
||||
|
||||
int[] angelica$getVertexStates();
|
||||
|
||||
int[] angelica$getShaderOverrideBlockIds();
|
||||
|
||||
void angelica$setShaderOverrideBlockId(short blockId);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
public class WitherArmorState {
|
||||
public static boolean pendingInflate = false;
|
||||
}
|
||||
@@ -115,6 +115,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void copyRawBuffer(int[] rawBuffer, int vertexCount, int[] vertexStates,
|
||||
int[] shaderOverrideBlockIds,
|
||||
ChunkBuildBuffers buffers, Material material, boolean isShaderPackOverride,
|
||||
boolean blockAllowsSmoothLighting) {
|
||||
if (vertexCount == 0) {
|
||||
@@ -145,6 +146,8 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
final int worldY = originY + blockY;
|
||||
final int worldZ = originZ + blockZ;
|
||||
|
||||
final short originalBlockId = blockRenderContext.blockId;
|
||||
|
||||
int stateIdx = 0;
|
||||
int facesAtBaseMaterial = 0;
|
||||
|
||||
@@ -239,6 +242,9 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext {
|
||||
}
|
||||
final var builder = buffers.get(correctMaterial);
|
||||
|
||||
final int shaderOverrideBlockId = shaderOverrideBlockIds[quadIdx * 4];
|
||||
blockRenderContext.blockId = shaderOverrideBlockId != -1 ? (short) shaderOverrideBlockId : originalBlockId;
|
||||
|
||||
if (correctMaterial != material && builder.getEncoder() instanceof IrisExtendedChunkVertexEncoder iris) {
|
||||
iris.setContext(blockRenderContext);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import net.minecraft.crash.CrashReport;
|
||||
import net.minecraft.crash.CrashReportCategory;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.ReportedException;
|
||||
import net.minecraft.world.IBlockAccess;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.RenderSection;
|
||||
@@ -139,7 +140,25 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
if (block.hasTileEntity(meta)) {
|
||||
final TileEntity tileEntity = region.getTileEntity(x, y, z);
|
||||
if (tileEntity != null && TileEntityRendererDispatcher.instance.hasSpecialRenderer(tileEntity)) {
|
||||
(TileEntityRenderBoundsRegistry.isAlwaysInfiniteExtent(tileEntity) ? renderData.globalBlockEntities : renderData.culledBlockEntities).add(tileEntity);
|
||||
final boolean isGlobal;
|
||||
if (TileEntityRenderBoundsRegistry.isAlwaysInfiniteExtent(tileEntity)) {
|
||||
isGlobal = true;
|
||||
} else {
|
||||
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 = true;
|
||||
}
|
||||
}
|
||||
(isGlobal ? renderData.globalBlockEntities : renderData.culledBlockEntities).add(tileEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,6 +275,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
&& block.getLightValue() == 0; // does not emit real block light
|
||||
buildContext.copyRawBuffer(tessellator.rawBuffer, tessellator.vertexCount,
|
||||
((StateAwareTessellator)tessellator).angelica$getVertexStates(),
|
||||
((StateAwareTessellator)tessellator).angelica$getShaderOverrideBlockIds(),
|
||||
buffers, passMaterial, isShaderPackOverride, blockAllowsSmoothLighting);
|
||||
tessellator.reset();
|
||||
tessellator.isDrawing = false;
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.client.rendering.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.glsm.states.FogState;
|
||||
import cpw.mods.fml.common.eventhandler.EventPriority;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
|
||||
@@ -52,10 +52,10 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
|
||||
private static final double MAX_ENTITY_CHECK_VOLUME = 16 * 16 * 16 * 15;
|
||||
|
||||
// For sorting transparent TESRs
|
||||
private RenderSectionOrderer renderSectionOrderer = new RenderSectionOrderer();
|
||||
private TileEntityOrderer tileEntityOrderer = new TileEntityOrderer();
|
||||
private ArrayList<RenderSection> sortedRenderSections = new ArrayList<>();
|
||||
private ArrayList<TileEntity> sortedTileEntities = new ArrayList<>();
|
||||
private final RenderSectionOrderer renderSectionOrderer = new RenderSectionOrderer();
|
||||
private final TileEntityOrderer tileEntityOrderer = new TileEntityOrderer();
|
||||
private final ArrayList<RenderSection> sortedRenderSections = new ArrayList<>();
|
||||
private final ArrayList<TileEntity> sortedTileEntities = new ArrayList<>();
|
||||
|
||||
private CeleritasWorldRenderer(Minecraft mc) {
|
||||
// Private constructor for singleton
|
||||
@@ -92,6 +92,10 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
|
||||
@Override
|
||||
protected void unloadWorld() {
|
||||
DynamicLights.setActiveRenderer(null);
|
||||
this.sortedRenderSections.clear();
|
||||
this.sortedTileEntities.clear();
|
||||
ShadowRenderer.visibleTileEntities.clear();
|
||||
ShadowRenderer.globalTileEntities.clear();
|
||||
super.unloadWorld();
|
||||
}
|
||||
|
||||
|
||||
@@ -79,13 +79,13 @@ public class IrisExtendedChunkVertexEncoder implements ContextAwareChunkVertexEn
|
||||
|
||||
baseEncoder.write(ptr, material, vertex, sectionIndex);
|
||||
|
||||
// Per-vertex: renderType, midBlock, lightValue
|
||||
memPutShort(ptr + MC_ENTITY_OFFSET + 2, ctx.renderType);
|
||||
// Per-vertex: mc_Entity (packed blockId + renderType), midBlock, lightValue
|
||||
memPutInt(ptr + MC_ENTITY_OFFSET, ((ctx.blockId + 1) << 1) | (ctx.renderType & 1));
|
||||
final int midBlock = ExtendedDataHelper.computeMidBlock(vertex.x, vertex.y, vertex.z, ctx.localPosX, ctx.localPosY, ctx.localPosZ);
|
||||
memPutInt(ptr + MID_BLOCK_OFFSET, midBlock);
|
||||
memPutByte(ptr + MID_BLOCK_OFFSET + 3, ctx.lightValue);
|
||||
|
||||
// Per-quad: midTexCoord, blockId, normal, tangent
|
||||
// Per-quad: midTexCoord, normal, tangent
|
||||
if (vertexCount == 4) {
|
||||
vertexCount = 0;
|
||||
|
||||
@@ -98,11 +98,6 @@ public class IrisExtendedChunkVertexEncoder implements ContextAwareChunkVertexEn
|
||||
memPutInt(ptr + MID_TEX_OFFSET - STRIDE * 2, midUV);
|
||||
memPutInt(ptr + MID_TEX_OFFSET - STRIDE * 3, midUV);
|
||||
|
||||
memPutShort(ptr + MC_ENTITY_OFFSET, ctx.blockId);
|
||||
memPutShort(ptr + MC_ENTITY_OFFSET - STRIDE, ctx.blockId);
|
||||
memPutShort(ptr + MC_ENTITY_OFFSET - STRIDE * 2, ctx.blockId);
|
||||
memPutShort(ptr + MC_ENTITY_OFFSET - STRIDE * 3, ctx.blockId);
|
||||
|
||||
quad.setup(ptr, STRIDE);
|
||||
NormalHelper.computeFaceNormal(normal, quad);
|
||||
final int packedNormal = NormI8.pack(normal);
|
||||
|
||||
@@ -13,7 +13,7 @@ import org.embeddedt.embeddium.impl.render.chunk.vertex.format.ChunkVertexType;
|
||||
* mc_midTexCoord ushort[2] - quad center UV (average of 4 vertices)
|
||||
* at_tangent byte[4] - tangent vector, normalized
|
||||
* iris_Normal byte[3]+1 - face normal, normalized
|
||||
* mc_Entity short[2] - (blockId, renderType)
|
||||
* mc_Entity uint - packed as ((blockId + 1) << 1) | (renderType & 1)
|
||||
* at_midBlock byte[4] - (xyz offset from block center, lightValue)
|
||||
*/
|
||||
public class IrisExtendedChunkVertexType implements ChunkVertexType {
|
||||
@@ -25,7 +25,7 @@ public class IrisExtendedChunkVertexType implements ChunkVertexType {
|
||||
.addElement("mc_midTexCoord", GlVertexFormat.NEXT_ALIGNED_POINTER, GlVertexAttributeFormat.UNSIGNED_SHORT, 2, false, false)
|
||||
.addElement("at_tangent", GlVertexFormat.NEXT_ALIGNED_POINTER, GlVertexAttributeFormat.BYTE, 4, true, false)
|
||||
.addElement("iris_Normal", GlVertexFormat.NEXT_ALIGNED_POINTER, GlVertexAttributeFormat.BYTE, 3, true, false)
|
||||
.addElement("mc_Entity", GlVertexFormat.NEXT_ALIGNED_POINTER, GlVertexAttributeFormat.SHORT, 2, false, false)
|
||||
.addElement("mc_Entity", GlVertexFormat.NEXT_ALIGNED_POINTER, GlVertexAttributeFormat.UNSIGNED_INT, 1, false, true)
|
||||
.addElement("at_midBlock", GlVertexFormat.NEXT_ALIGNED_POINTER, GlVertexAttributeFormat.BYTE, 4, false, false)
|
||||
.build();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
@@ -43,6 +43,20 @@ public class CTMUtils {
|
||||
private static Overrides newOverrides = null;
|
||||
private static TileLoader tileLoader;
|
||||
|
||||
private static final ThreadLocal<TileOverrideImpl.CTMCompact> CURRENT_COMPACT = new ThreadLocal<>();
|
||||
|
||||
public static void setCurrentCompact(TileOverrideImpl.CTMCompact compact) {
|
||||
CURRENT_COMPACT.set(compact);
|
||||
}
|
||||
|
||||
public static TileOverrideImpl.CTMCompact getCurrentCompact() {
|
||||
return CURRENT_COMPACT.get();
|
||||
}
|
||||
|
||||
public static void clearCurrentCompact() {
|
||||
CURRENT_COMPACT.remove();
|
||||
}
|
||||
|
||||
private static final StampedLock lock = new StampedLock();
|
||||
|
||||
private static class StateAndIterator {
|
||||
@@ -219,6 +233,7 @@ public class CTMUtils {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
clearCurrentCompact();
|
||||
}
|
||||
|
||||
private static boolean checkFace(int face) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.prupe.mcpatcher.ctm;
|
||||
|
||||
import com.prupe.mcpatcher.mal.resource.PropertiesFile;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
|
||||
|
||||
public class CompactConnectingCtmProperties {
|
||||
|
||||
private final boolean innerSeams;
|
||||
private final String orientation;
|
||||
private final Int2IntMap tileReplacementMap = new Int2IntOpenHashMap();
|
||||
|
||||
public CompactConnectingCtmProperties(PropertiesFile properties) {
|
||||
this.innerSeams = properties.getBoolean("innerSeams", false);
|
||||
this.orientation = properties.getString("orientation", "none").toLowerCase();
|
||||
|
||||
for (int i = 0; i < 47; i++) {
|
||||
String val = properties.getString("ctm." + i, "");
|
||||
if (!val.isEmpty()) {
|
||||
try {
|
||||
int tileIdx = Integer.parseInt(val.trim());
|
||||
tileReplacementMap.put(i, tileIdx);
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getInnerSeams() {
|
||||
return innerSeams;
|
||||
}
|
||||
|
||||
public String getOrientation() {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
public Int2IntMap getTileReplacementMap() {
|
||||
return tileReplacementMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
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
|
||||
* 1 = top-right
|
||||
* 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 final IIcon[] sprites;
|
||||
private final CompactConnectingCtmProperties props;
|
||||
|
||||
CompactCtmQuadProcessor(IIcon[] sprites, CompactConnectingCtmProperties props) {
|
||||
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;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
int ctmIndex = CTM_47_BY_CONNECTIONS[connections & 0xFF];
|
||||
IIcon replacement = getReplacementIcon(ctmIndex);
|
||||
if (replacement != null) {
|
||||
renderWholeFace(rb, world, x, y, z, face, replacement);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (connections == 0 || connections == 0xFF) {
|
||||
IIcon sprite = sprites[getCompactSpriteIndex(connections)];
|
||||
if (sprite != null) {
|
||||
renderWholeFace(rb, world, x, y, z, face, sprite);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
renderSplitFace(rb, world, x, y, z, face, connections);
|
||||
return true;
|
||||
}
|
||||
|
||||
private IIcon getReplacementIcon(int ctmIndex) {
|
||||
Int2IntMap map = props.getTileReplacementMap();
|
||||
if (map == null || !map.containsKey(ctmIndex)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int spriteIndex = map.get(ctmIndex);
|
||||
if (spriteIndex < 0 || spriteIndex >= sprites.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sprites[spriteIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact 5-sprite variant:
|
||||
* 0 = default / isolated / mixed
|
||||
* 1 = all neighbors
|
||||
* 2 = horizontal line
|
||||
* 3 = vertical line
|
||||
*/
|
||||
private int getCompactSpriteIndex(int connections) {
|
||||
if (connections == 0xFF) {
|
||||
return SPRITE_ALL;
|
||||
}
|
||||
|
||||
if ((connections & ((1 << LEFT) | (1 << RIGHT))) == ((1 << LEFT) | (1 << RIGHT))) {
|
||||
return SPRITE_HORIZONTAL;
|
||||
}
|
||||
|
||||
if ((connections & ((1 << UP) | (1 << DOWN))) == ((1 << UP) | (1 << DOWN))) {
|
||||
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);
|
||||
|
||||
double minX = rb.renderMinX;
|
||||
double minY = rb.renderMinY;
|
||||
double minZ = rb.renderMinZ;
|
||||
double maxX = rb.renderMaxX;
|
||||
double maxY = rb.renderMaxY;
|
||||
double maxZ = rb.renderMaxZ;
|
||||
|
||||
for (int q = 0; q < 4; q++) {
|
||||
IIcon icon = sprites[getQuadrantSprite(q, connections)];
|
||||
if (icon == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setQuadrantBounds(rb, face, q, minX, minY, minZ, maxX, maxY, maxZ);
|
||||
rb.overrideBlockTexture = icon;
|
||||
renderFace(rb, block, x, y, z, face, icon);
|
||||
}
|
||||
|
||||
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);
|
||||
IIcon saved = rb.overrideBlockTexture;
|
||||
rb.overrideBlockTexture = icon;
|
||||
try {
|
||||
renderFace(rb, block, x, y, z, face, icon);
|
||||
} finally {
|
||||
rb.overrideBlockTexture = saved;
|
||||
}
|
||||
}
|
||||
|
||||
private void renderFace(RenderBlocks rb, Block block, int x, int y, int z, int face, IIcon icon) {
|
||||
boolean originalFlip = rb.field_152631_f;
|
||||
rb.field_152631_f = (face == 2 || face == 5);
|
||||
try {
|
||||
switch (face) {
|
||||
case 0 -> rb.renderFaceYNeg(block, x, y, z, icon);
|
||||
case 1 -> rb.renderFaceYPos(block, x, y, z, icon);
|
||||
case 2 -> rb.renderFaceZNeg(block, x, y, z, icon);
|
||||
case 3 -> rb.renderFaceZPos(block, x, y, z, icon);
|
||||
case 4 -> rb.renderFaceXNeg(block, x, y, z, icon);
|
||||
case 5 -> rb.renderFaceXPos(block, x, y, z, icon);
|
||||
}
|
||||
} finally {
|
||||
rb.field_152631_f = originalFlip;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quadrants are always interpreted as:
|
||||
* 0 = top-left, 1 = top-right, 2 = bottom-right, 3 = bottom-left
|
||||
* in the face-local view.
|
||||
*/
|
||||
private void setQuadrantBounds(RenderBlocks rb, int face, int quadrant,
|
||||
double minX, double minY, double minZ,
|
||||
double maxX, double maxY, double maxZ) {
|
||||
double midX = (minX + maxX) * 0.5;
|
||||
double midY = (minY + maxY) * 0.5;
|
||||
double midZ = (minZ + maxZ) * 0.5;
|
||||
|
||||
switch (face) {
|
||||
case 0:
|
||||
case 1:
|
||||
if (quadrant == 0) rb.setRenderBounds(minX, minY, minZ, midX, maxY, midZ);
|
||||
if (quadrant == 1) rb.setRenderBounds(midX, minY, minZ, maxX, maxY, midZ);
|
||||
if (quadrant == 2) rb.setRenderBounds(midX, minY, midZ, maxX, maxY, maxZ);
|
||||
if (quadrant == 3) rb.setRenderBounds(minX, minY, midZ, midX, maxY, maxZ);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (quadrant == 0) rb.setRenderBounds(midX, midY, minZ, maxX, maxY, maxZ);
|
||||
if (quadrant == 1) rb.setRenderBounds(minX, midY, minZ, midX, maxY, maxZ);
|
||||
if (quadrant == 2) rb.setRenderBounds(minX, minY, minZ, midX, midY, maxZ);
|
||||
if (quadrant == 3) rb.setRenderBounds(midX, minY, minZ, maxX, midY, maxZ);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (quadrant == 0) rb.setRenderBounds(minX, midY, minZ, midX, maxY, maxZ);
|
||||
if (quadrant == 1) rb.setRenderBounds(midX, midY, minZ, maxX, maxY, maxZ);
|
||||
if (quadrant == 2) rb.setRenderBounds(midX, minY, minZ, maxX, midY, maxZ);
|
||||
if (quadrant == 3) rb.setRenderBounds(minX, minY, minZ, midX, midY, maxZ);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
if (quadrant == 0) rb.setRenderBounds(minX, midY, minZ, maxX, maxY, midZ);
|
||||
if (quadrant == 1) rb.setRenderBounds(minX, midY, midZ, maxX, maxY, maxZ);
|
||||
if (quadrant == 2) rb.setRenderBounds(minX, minY, midZ, maxX, midY, maxZ);
|
||||
if (quadrant == 3) rb.setRenderBounds(minX, minY, minZ, maxX, midY, midZ);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
if (quadrant == 0) rb.setRenderBounds(minX, midY, midZ, maxX, maxY, maxZ);
|
||||
if (quadrant == 1) rb.setRenderBounds(minX, midY, minZ, maxX, maxY, midZ);
|
||||
if (quadrant == 2) rb.setRenderBounds(minX, minY, minZ, maxX, midY, midZ);
|
||||
if (quadrant == 3) rb.setRenderBounds(minX, minY, midZ, maxX, midY, maxZ);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A quadrant is built from two cardinal sides and the diagonal between them.
|
||||
*/
|
||||
private int getQuadrantSprite(int quadrant, int connections) {
|
||||
int sideA = QUAD_A[quadrant];
|
||||
int sideB = QUAD_B[quadrant];
|
||||
int diag = QUAD_DIAG[quadrant];
|
||||
|
||||
boolean connA = (connections & (1 << sideA)) != 0;
|
||||
boolean connB = (connections & (1 << sideB)) != 0;
|
||||
|
||||
if (connA && connB) {
|
||||
return (connections & (1 << diag)) != 0 ? SPRITE_ALL : SPRITE_MIXED;
|
||||
}
|
||||
|
||||
if (connA ^ connB) {
|
||||
int connectedBit = connA ? sideA : sideB;
|
||||
return (connectedBit == LEFT || connectedBit == RIGHT) ? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import com.prupe.mcpatcher.mal.resource.PropertiesFile;
|
||||
import com.prupe.mcpatcher.mal.resource.TexturePackAPI;
|
||||
import com.prupe.mcpatcher.mal.tile.TileLoader;
|
||||
|
||||
abstract class TileOverride implements Comparable<TileOverride> {
|
||||
public abstract class TileOverride implements Comparable<TileOverride> {
|
||||
|
||||
private static final MCLogger logger = MCLogger.getLogger(MCLogger.Category.CONNECTED_TEXTURES, "CTM");
|
||||
|
||||
@@ -80,6 +80,7 @@ abstract class TileOverride implements Comparable<TileOverride> {
|
||||
tileLoader);
|
||||
case "sandstone", "top" -> override = new TileOverrideImpl.Top(properties, tileLoader);
|
||||
case "repeat", "pattern" -> override = new TileOverrideImpl.Repeat(properties, tileLoader);
|
||||
case "ctm_compact", "compact" -> override = new TileOverrideImpl.CTMCompact(properties, tileLoader);
|
||||
default -> properties.error("unknown method \"%s\"", method);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.prupe.mcpatcher.mal.resource.PropertiesFile;
|
||||
import com.prupe.mcpatcher.mal.tile.TileLoader;
|
||||
import com.prupe.mcpatcher.mal.util.WeightedIndex;
|
||||
|
||||
class TileOverrideImpl {
|
||||
public class TileOverrideImpl {
|
||||
|
||||
final static class CTM extends TileOverride {
|
||||
|
||||
@@ -481,4 +481,49 @@ class TileOverrideImpl {
|
||||
return icons[0];
|
||||
}
|
||||
}
|
||||
|
||||
final public static class CTMCompact extends TileOverride {
|
||||
|
||||
private final CompactConnectingCtmProperties properties;
|
||||
private volatile CompactCtmQuadProcessor processor;
|
||||
|
||||
CTMCompact(PropertiesFile propertiesFile, TileLoader tileLoader) {
|
||||
super(propertiesFile, tileLoader);
|
||||
this.properties = new CompactConnectingCtmProperties(propertiesFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getMethod() {
|
||||
return "compact";
|
||||
}
|
||||
|
||||
@Override
|
||||
String checkTileMap() {
|
||||
return getNumberOfTiles() == 5 ? null : "requires exactly 5 tiles";
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean requiresFace() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
IIcon getTileWorld_Impl(RenderBlockState renderBlockState, IIcon origIcon) {
|
||||
return origIcon;
|
||||
}
|
||||
|
||||
@Override
|
||||
IIcon getTileHeld_Impl(RenderBlockState renderBlockState, IIcon origIcon) {
|
||||
return icons.length > 0 ? icons[0] : origIcon;
|
||||
}
|
||||
|
||||
public CompactCtmQuadProcessor getProcessor() {
|
||||
CompactCtmQuadProcessor p = this.processor;
|
||||
if (p == null) {
|
||||
p = new CompactCtmQuadProcessor(icons, properties);
|
||||
this.processor = p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,7 @@ abstract public class TileOverrideIterator implements Iterator<TileOverride> {
|
||||
}
|
||||
|
||||
public TileOverride go(RenderBlockState renderBlockState, IIcon origIcon) {
|
||||
CTMUtils.clearCurrentCompact();
|
||||
this.renderBlockState = renderBlockState;
|
||||
renderBlockState.setFilter(null);
|
||||
currentIcon = origIcon;
|
||||
@@ -121,6 +122,9 @@ abstract public class TileOverrideIterator implements Iterator<TileOverride> {
|
||||
TileOverride override = next();
|
||||
IIcon newIcon = getTile(override, renderBlockState, origIcon);
|
||||
if (newIcon != null) {
|
||||
if (override instanceof TileOverrideImpl.CTMCompact compact) {
|
||||
CTMUtils.setCurrentCompact(compact);
|
||||
}
|
||||
lastMatchedOverride = override;
|
||||
skipOverrides.add(override);
|
||||
currentIcon = newIcon;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package net.coderbot.iris;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
|
||||
import com.gtnewhorizons.angelica.Tags;
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.proxy.ClientProxy;
|
||||
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
|
||||
import cpw.mods.fml.client.registry.ClientRegistry;
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
@@ -13,7 +13,6 @@ import cpw.mods.fml.common.gameevent.InputEvent;
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
|
||||
import lombok.Getter;
|
||||
import net.coderbot.iris.block_context.BlockContextHolder;
|
||||
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
|
||||
import net.coderbot.iris.celeritas.IrisCeleritasShaderProvider;
|
||||
import net.coderbot.iris.compat.dh.DHCompat;
|
||||
@@ -915,35 +914,25 @@ public class Iris {
|
||||
ClientRegistry.registerKeyBinding(shaderpackScreenKeybind);
|
||||
}
|
||||
|
||||
static BlockContextHolder contextHolder;
|
||||
|
||||
private static int getShaderMaterialOverrideId(Block block, int meta) {
|
||||
if (contextHolder == null) {
|
||||
final Reference2ObjectMap<Block, Int2IntMap> blockMetaMatches = BlockRenderingSettings.INSTANCE.getBlockMetaMatches();
|
||||
if (blockMetaMatches == null) {
|
||||
return -1;
|
||||
}
|
||||
contextHolder = new BlockContextHolder(blockMetaMatches);
|
||||
|
||||
}
|
||||
contextHolder.set(block, meta, (short) block.getRenderType());
|
||||
return contextHolder.blockId;
|
||||
}
|
||||
|
||||
public static void setShaderMaterialOverride(Block block, int meta) {
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
int blockId = getShaderMaterialOverrideId(block, meta);
|
||||
final Reference2ObjectMap<Block, Int2IntMap> blockMetaMatches = BlockRenderingSettings.INSTANCE.getBlockMetaMatches();
|
||||
if (blockMetaMatches == null)
|
||||
return;
|
||||
|
||||
if (TessellatorManager.get() instanceof CapturingTessellator tess)
|
||||
tess.setShaderBlockId(blockId);
|
||||
final Int2IntMap metaMap = blockMetaMatches.get(block);
|
||||
final int blockId = metaMap != null ? metaMap.get(meta) : -1;
|
||||
|
||||
if (TessellatorManager.get() instanceof StateAwareTessellator tess)
|
||||
tess.angelica$setShaderOverrideBlockId((short) blockId);
|
||||
}
|
||||
|
||||
public static void resetShaderMaterialOverride() {
|
||||
if (!enabled)
|
||||
return;
|
||||
if (TessellatorManager.get() instanceof CapturingTessellator tess)
|
||||
tess.setShaderBlockId(-1);
|
||||
if (TessellatorManager.get() instanceof StateAwareTessellator tess)
|
||||
tess.angelica$setShaderOverrideBlockId((short) -1);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package net.coderbot.iris.block_context;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
|
||||
import net.minecraft.block.Block;
|
||||
|
||||
/**
|
||||
* Holds block context for shader material ID lookups.
|
||||
*/
|
||||
public class BlockContextHolder {
|
||||
private final Reference2ObjectMap<Block, Int2IntMap> blockMetaMatches;
|
||||
public int blockId;
|
||||
public short renderType;
|
||||
|
||||
public BlockContextHolder(Reference2ObjectMap<Block, Int2IntMap> blockMetaMatches) {
|
||||
this.blockMetaMatches = blockMetaMatches;
|
||||
this.blockId = -1;
|
||||
this.renderType = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the material ID for a block.
|
||||
* @param block The block
|
||||
* @param meta The block metadata
|
||||
* @param renderType The render type
|
||||
*/
|
||||
public void set(Block block, int meta, short renderType) {
|
||||
Int2IntMap metaMap = this.blockMetaMatches != null ? this.blockMetaMatches.get(block) : null;
|
||||
int id = metaMap != null ? metaMap.get(meta) : -1;
|
||||
|
||||
this.blockId = id;
|
||||
this.renderType = renderType;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.blockId = -1;
|
||||
this.renderType = -1;
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ 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 net.coderbot.iris.Iris;
|
||||
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;
|
||||
@@ -39,8 +41,16 @@ public class BlockMaterialMapping {
|
||||
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 = resolveBlock(id);
|
||||
|
||||
// Try flattening map for modern names
|
||||
if ((block == null || block == Blocks.air) && "minecraft".equals(id.getNamespace())) {
|
||||
List<BlockEntry> legacyEntries = FlatteningMap.toLegacy(id.getName(), Map.of());
|
||||
if (legacyEntries != null) {
|
||||
// Use the first entry for render type (render type applies per-block)
|
||||
block = resolveBlock(legacyEntries.getFirst().getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (block == null || block == Blocks.air) {
|
||||
return;
|
||||
@@ -73,19 +83,32 @@ public class BlockMaterialMapping {
|
||||
* 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) {
|
||||
final NamespacedId id = entry.getId();
|
||||
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
|
||||
NamespacedId id = entry.getId();
|
||||
String name = id.getName();
|
||||
boolean hasStateProps = !entry.getStateProperties().isEmpty();
|
||||
boolean hasExplicitMetas = !entry.getMetas().isEmpty();
|
||||
|
||||
final Block block = (Block) Block.blockRegistry.getObject(resourceLocation.toString());
|
||||
if ("minecraft".equals(id.getNamespace()) && (hasStateProps || !hasExplicitMetas)) {
|
||||
List<BlockEntry> legacyEntries = FlatteningMap.toLegacy(name, entry.getStateProperties());
|
||||
if (legacyEntries != null) {
|
||||
for (BlockEntry legacy : legacyEntries) {
|
||||
applyMetas(resolveBlock(legacy.getId()), legacy.getMetas(), idMap, intId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Fall back to registry with the entry's own metas
|
||||
Block block = resolveBlock(id);
|
||||
applyMetas(block, entry.getMetas(), idMap, intId);
|
||||
}
|
||||
// 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
|
||||
private static void applyMetas(Block block, Set<Integer> metas, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId) {
|
||||
if (block == null || block == Blocks.air) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<Integer> metas = entry.getMetas();
|
||||
|
||||
Int2IntMap metaMap = idMap.get(block);
|
||||
if (metaMap == null) {
|
||||
metaMap = new Int2IntOpenHashMap();
|
||||
@@ -94,15 +117,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);
|
||||
}
|
||||
} else {
|
||||
// Add only specific metadata values
|
||||
for (int meta : metas) {
|
||||
metaMap.putIfAbsent(meta, intId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Block resolveBlock(NamespacedId id) {
|
||||
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
|
||||
return (Block) Block.blockRegistry.getObject(resourceLocation.toString());
|
||||
}
|
||||
}
|
||||
@@ -72,9 +72,11 @@ public class CeleritasTerrainPipeline {
|
||||
Optional<ProgramSource> terrainSource,
|
||||
Optional<ProgramSource> translucentSource,
|
||||
Optional<ProgramSource> shadowSource,
|
||||
Optional<ProgramSource> shadowTranslucentSource,
|
||||
CompletableFuture<Map<PatchShaderType, String>> terrainFuture,
|
||||
CompletableFuture<Map<PatchShaderType, String>> translucentFuture,
|
||||
CompletableFuture<Map<PatchShaderType, String>> shadowFuture,
|
||||
CompletableFuture<Map<PatchShaderType, String>> shadowTranslucentFuture,
|
||||
RenderTargets renderTargets,
|
||||
ImmutableSet<Integer> flippedAfterPrepare,
|
||||
ImmutableSet<Integer> flippedAfterTranslucent,
|
||||
@@ -92,6 +94,7 @@ public class CeleritasTerrainPipeline {
|
||||
gbufferProgramSource.put(IrisTerrainPass.GBUFFER_CUTOUT, terrainSource);
|
||||
gbufferProgramSource.put(IrisTerrainPass.GBUFFER_TRANSLUCENT, translucentSource.isPresent() ? translucentSource : terrainSource);
|
||||
gbufferProgramSource.put(IrisTerrainPass.SHADOW, shadowSource);
|
||||
gbufferProgramSource.put(IrisTerrainPass.SHADOW_TRANSLUCENT, shadowTranslucentSource.isPresent() ? shadowTranslucentSource : shadowSource);
|
||||
|
||||
// Initialize PassInfo, framebuffers, blend modes, and alpha in single pass
|
||||
for (IrisTerrainPass pass : IrisTerrainPass.VALUES) {
|
||||
@@ -101,11 +104,12 @@ public class CeleritasTerrainPipeline {
|
||||
// Set up framebuffer, blend mode, and buffer blend overrides
|
||||
final ProgramId programId = switch (pass) {
|
||||
case GBUFFER_TRANSLUCENT -> ProgramId.Water;
|
||||
case SHADOW_TRANSLUCENT -> ProgramId.ShadowWater;
|
||||
case SHADOW, SHADOW_CUTOUT -> ProgramId.Shadow;
|
||||
default -> ProgramId.Terrain;
|
||||
};
|
||||
|
||||
if (pass == IrisTerrainPass.SHADOW || pass == IrisTerrainPass.SHADOW_CUTOUT) {
|
||||
if (pass.isShadow()) {
|
||||
passInfo.framebuffer = shadowFramebuffer;
|
||||
passInfo.blendModeOverride = programId.getBlendModeOverride();
|
||||
passInfo.bufferBlendOverrides = Collections.emptyList();
|
||||
@@ -145,6 +149,7 @@ public class CeleritasTerrainPipeline {
|
||||
processShaderFuture(terrainFuture, terrainSource, passInfoMap.get(IrisTerrainPass.GBUFFER_SOLID), passInfoMap.get(IrisTerrainPass.GBUFFER_CUTOUT));
|
||||
processShaderFuture(translucentFuture, translucentSource, passInfoMap.get(IrisTerrainPass.GBUFFER_TRANSLUCENT));
|
||||
processShaderFuture(shadowFuture, shadowSource, passInfoMap.get(IrisTerrainPass.SHADOW), passInfoMap.get(IrisTerrainPass.SHADOW_CUTOUT));
|
||||
processShaderFuture(shadowTranslucentFuture, shadowTranslucentSource.isPresent() ? shadowTranslucentSource : shadowSource, passInfoMap.get(IrisTerrainPass.SHADOW_TRANSLUCENT));
|
||||
}
|
||||
|
||||
private void processShaderFuture(@Nullable CompletableFuture<Map<PatchShaderType, String>> future, Optional<ProgramSource> source, PassInfo... targets) {
|
||||
|
||||
@@ -136,8 +136,10 @@ public class IrisCeleritasChunkProgramOverrides {
|
||||
if (celeritasPipeline != null && !celeritasPipeline.hasShadowPass()) {
|
||||
throw new IllegalStateException("Shadow program requested, but shader pack has no shadow pass");
|
||||
}
|
||||
return programs.get(pass.supportsFragmentDiscard() ?
|
||||
IrisTerrainPass.SHADOW_CUTOUT : IrisTerrainPass.SHADOW);
|
||||
if (pass.isReverseOrder()) {
|
||||
return programs.get(IrisTerrainPass.SHADOW_TRANSLUCENT);
|
||||
}
|
||||
return programs.get(pass.supportsFragmentDiscard() ? IrisTerrainPass.SHADOW_CUTOUT : IrisTerrainPass.SHADOW);
|
||||
} else {
|
||||
if (pass.supportsFragmentDiscard()) {
|
||||
return programs.get(IrisTerrainPass.GBUFFER_CUTOUT);
|
||||
|
||||