Compare commits

...
70 Commits
Author SHA1 Message Date
Ruling-0 edc0081b7c Fix CTM inventory texture bleed (#1721) 2026-05-04 12:25:51 -07:00
Alexdoru 913256b032 fix world client leak in CTMUtils (#1724) 2026-05-04 12:24:25 -07:00
sisyphussy e1f5083d3e Fix intel detection (#1722) 2026-05-04 12:23:50 -07:00
sisyphussy b55e693ad2 Remove DSA from GLSM & other stuff (#1723) 2026-05-04 12:23:38 -07:00
sisyphussy 21c01a1aa4 Fix terrible performance on Intel (#1718) 2026-05-03 14:32:58 -07:00
Caedis 83a26b2291 Add Cosmetic Armor support for Dynamic Lights (#1708) 2026-05-03 14:31:49 -07:00
Kotmatross 7e44c1efcb Fix stale uniforms when switching shader program (#1715) 2026-05-03 14:30:46 -07:00
VortexSo4 155f209db5 remove ctm guide as it is worse than wiki (#1706) 2026-04-30 11:03:18 -07:00
SpacEagle17 c4a6c165a0 Increase MAX_COLOR_BUFFERS limit to 32 (#1704) 2026-04-28 12:33:51 -07:00
Eclipse-Sol 624fe6b453 Add targeting of snow-covered blocks. (#1696) 2026-04-26 15:26:10 -07:00
YoshyandJason Mitchell fd45fda0ab Fix AO Lighting for compact CTM (#1697)
Co-authored-by: Jason Mitchell <[email protected]>
2026-04-26 12:55:24 -07:00
Alexdoru 00fc97a611 Fix dynamic lights world client leaks (#1700) 2026-04-26 10:01:46 -07:00
Alexdoru 445dc487b9 fix world client leak caused by lambda in IrisExclusiveUniforms$WorldInfoUniforms (#1701) 2026-04-26 09:56:08 -07:00
Yoshy f43fbd4d9f Fix CTM to not spread to nearby TEs (#1695) 2026-04-25 13:09:38 -07:00
Jason Mitchell dbabc4b3dc Maybe fix DragonAPI on OSX - take two (#1689)
* Maybe fix DragonAPI on OSX - take two
* Bind a default VAO on the SharedDrawable
* Mixin that tries to swap out glGetProgramInfoLog when appropriate

* no direct gl calls
2026-04-21 15:12:02 -07:00
Jason Mitchell e01097264b Fix infinite extent AABB crash w/ kotlin, and ensure proper sizing/padding on VAO/VBO snapshots. (#1687) 2026-04-20 10:11:46 -07:00
Jason Mitchell 0a922f3129 AE2 Crash Fix (#1684)
* Snapshot all relevant data into display lists (Avoids referencing stale/recycled data and a crash on Nvidia)
* Route deletes through GLStateManager
2026-04-19 08:19:02 -07:00
Eclipse-Sol 4a2dcd0b22 Automate the collection of legacy IDs for very old shaderpacks. (#1631) 2026-04-18 09:53:06 -07:00
Eclipse-Sol 0db691db0b Add FlatteningMap and EntityFlatteningMap (#1635) 2026-04-18 09:20:24 -07:00
danyadev e5cd1372e7 always request 0.9.0+ GTNHLib version when a player has an outdated version (#1672) 2026-04-18 09:18:28 -07:00
Kotmatross fbd07cc575 NTM:Space compat rework and update (#1675) 2026-04-18 09:14:49 -07:00
George Paton 91f2cf9dfb Add "Mods Only" Dynamic FOV option (#1680) 2026-04-18 09:13:39 -07:00
Kaiz 34a6967c71 fix(iris): implement full GL alpha test (#1673) 2026-04-18 09:12:46 -07:00
Alexdoru 9c295a1e22 Fix cached item in RenderItemFrame leaking both world client and server (#1659) 2026-04-13 15:04:36 -07:00
DarkShadow44 9fd02900ef Load modern block properties (#1626) 2026-04-13 15:03:23 -07:00
Jason Mitchell 27e9a4d4fd Fix class circularity error with early redirector (#1670) 2026-04-13 15:02:26 -07:00
Darren Eberly dfc08053d5 Don't advance buffer positions for Material shininess in GLSM (#1668) 2026-04-12 22:25:25 -07:00
VortexSo4 1af03fa369 add CTM guide (#1667) 2026-04-11 17:49:38 -07:00
NealDeal34 3ae3b2527d Update zh_CN.lang (#1623)
* Update zh_CN.lang

* update zh_CN.lang for dynamic lights
2026-04-07 08:21:34 +02:00
Jason Mitchell fd2a14a000 Various fixes (#1652) 2026-04-06 17:35:15 -07:00
DarkShadow44 9f0f6a6232 Always render clouds late for shaders (#1649) 2026-04-06 09:52:34 -07:00
DarkShadow44 2942c7eb84 Disable clouds when shaders are active (#1648) 2026-04-06 09:52:21 -07:00
Alexdoru 47762f921e fix two world client memory leaks (#1650) 2026-04-06 16:38:03 +00:00
Jason Mitchell bdc4431760 Fix some issues someone was having with nixos (#1647) 2026-04-05 16:21:58 -07:00
DarkShadow44 7e1beadfb5 DH: Fix mismatch from upstream for solas shaders (#1645) 2026-04-04 17:27:19 -07:00
Jason Mitchell 59a4b84023 Standalone GLSM mod - Umbra (#1642) 2026-04-03 21:51:35 -07:00
Jason Mitchell 8d53e25e0a Compact CTM Fix (#1640) 2026-04-03 16:48:08 -07:00
VortexSo4 bbf83db379 Implement compact ctm (#1636) 2026-04-02 22:32:28 +00:00
Jason Mitchell ec82280b9b Match OpenGL Buffer Behavior for glGet (#1637) 2026-04-02 14:20:06 -07:00
DarkShadow44andJason Mitchell d7588e4c14 Adapt shader block id override for Angelica 2.x (#1601)
Co-authored-by: Jason Mitchell <[email protected]>
2026-04-02 14:18:55 -07:00
Jason Mitchell 54c99386d4 Do a similar fix as MixinDroppedItemGlintEdges to held items (#1632) 2026-03-31 18:31:21 -07:00
DarkShadow44 465abf889a Pass mc_Entity like upstream does (#1627) 2026-03-31 16:18:35 -07:00
DarkShadow44 4acb83be6a Port shadow translucent (aka shadow_water) from upstream (#1628) 2026-03-31 16:09:25 -07:00
DarkShadow44 80f9724ee6 Handle GL_QUAD_STRIP in legacy conversion (#1629) 2026-03-31 15:57:57 -07:00
Jason Mitchell 8cf55bb216 Safely handle off client thread crash reports/glGetString (#1630) 2026-03-31 15:49:47 -07:00
Eclipse-Sol 3ebc8868c7 Fix item reset hopefully for the last time (#1621) 2026-03-30 19:32:47 -07:00
JohnButlerThird 0938e6d975 Only run the logic on bop grass (fix my mod bugging out) (#1620) 2026-03-30 18:17:42 -07:00
Eclipse-Sol d3022a40d2 Create new deferred overlay system to fix z-fighting issues (#1605) 2026-03-30 15:50:18 -07:00
Eclipse-Sol 2645c803f9 Better horse support (#1599) 2026-03-30 15:41:53 -07:00
Jason Mitchell d91c356a99 Shader fixes (#1619) 2026-03-30 15:34:54 -07:00
Jason Mitchell 189ff4a80f Defer client attributes. They're uploaded/handled elsewhere. Should fix a crash on j8/lwjgl2 (#1618) 2026-03-30 09:58:40 -07:00
Jason Mitchell ffb4ca9472 Get rid of some allocations, and use immutable variants for the suppliers (#1612) 2026-03-30 09:58:27 -07:00
Jason Mitchell 7d8c8d306d More Double overloads (#1613) 2026-03-29 15:09:13 -07:00
Kotmatross 117ff0cacf Fix other dynamic uniforms not updating properly (#1610) 2026-03-29 12:53:09 -07:00
Jason Mitchell 83fdd57534 Narrow another overly broad transformer exclusion 2026-03-29 09:24:50 -07:00
Kaiz 951be04e9a fix: correctly handle cross-chunk TileEntity render bounds 2026-03-29 09:20:13 -07:00
Jason Mitchell 0ad69f3433 FINE, we'll work with java8/forge. 2026-03-28 20:09:42 -07:00
Fabian Maurer 0dabecc784 Handle GL_POLYGON in legacy conversion 2026-03-28 14:41:23 -07:00
Jason Mitchell 9b3fad4c6d Ensure sufficient coverage + optimize the check 2026-03-28 13:16:54 -07:00
Kaiz e1e8256861 fix: crash when client-side vertex attrib pointers are used in core profile 2026-03-28 13:16:54 -07:00
Jason Mitchell e65c01c2a3 Handle method handles in invokedynamic 2026-03-28 12:46:19 -07:00
Jason Mitchell b9de4df18e Update alpha test to hopefully work with RTM and AE2 (#1597) 2026-03-28 09:40:10 -07:00
Kotmatross a5d31cd1b6 Return phaseChangeNotifier (#1596) 2026-03-28 09:15:46 -07:00
DarkShadow44 29f8d718cc Pass texturemap to composite shader and honor texture replacements (#1589) 2026-03-27 19:07:45 -07:00
12411234 52b0db2566 Fix missing glGetTexImage method crash when using XaerosWorldMap (#1592) 2026-03-27 19:02:51 -07:00
Eclipse-Sol e9eb45f4f2 Centers the Wither Armor (#1590) 2026-03-27 18:37:17 -07:00
Jason Mitchell 92e28a4cd5 Blur Compat (#1593) 2026-03-27 18:32:24 -07:00
Jason Mitchell f46a86889c Fix crash with fancy block particles. (#1591) 2026-03-27 17:52:14 -07:00
Jason MitchellandAlexdoru c827d446d5 External Redirector API (#1580)
Co-authored-by: Alexdoru <[email protected]>
2026-03-27 11:20:29 -07:00
Eclipse-Sol 00ef355ce8 Fix sky lighting on drawCrossSquare rendered blocks. (#1576) 2026-03-25 18:58:40 -07:00
221 changed files with 9822 additions and 1646 deletions

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
/.idea/
/.vs/
/.vscode/
/run/
run/
build/
/eclipse/
.classpath
+6 -2
View File
@@ -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"))
@@ -127,6 +129,8 @@ dependencies {
compileOnly(rfg.deobf("curse.maven:et-futurum-requiem-441392:5873837"))
compileOnly(rfg.deobf("curse.maven:biomes-o-plenty-220318:2499612"))
devOnlyNonPublishable("com.github.GTNewHorizons:CosmeticArmorReworked:1.0.3-GTNH:dev")
// For testing alternative splash screens — https://github.com/MalTeeez/ModernSplash/releases
// devOnlyNonPublishable("com.github.MalTeeez:modernsplash:1.7.10-1.2.4:dev")
+6 -1
View File
@@ -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)
@@ -9,13 +9,10 @@ import com.gtnewhorizons.angelica.glsm.states.MaterialState;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import java.nio.FloatBuffer;
@@ -15,6 +15,9 @@ import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBO;
import com.gtnewhorizons.angelica.glsm.recording.DisplayListVBOBuilder;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatchBuilder;
import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawCapture;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@@ -566,12 +569,16 @@ public class DisplayListManager {
currentRecorder.recordDrawBuffers(count, bufs);
}
// Complex commands are texture uploads - not draw barriers
public static void recordComplexCommand(DisplayListCommand cmd) {
if (currentRecorder == null) return;
currentRecorder.recordComplexCommand(cmd);
}
public static void recordIndexedDrawCapture(IndexedDrawCapture capture) {
if (currentRecorder == null) return;
currentRecorder.recordIndexedDrawCapture(capture);
}
public static void recordLoadMatrix(Matrix4f matrix) {
if (currentRecorder != null) currentRecorder.recordLoadMatrix(matrix);
}
@@ -580,14 +587,6 @@ public class DisplayListManager {
if (currentRecorder != null) currentRecorder.recordLoadIdentity();
}
public static void recordDrawArrays(int mode, int start, int count) {
if (currentRecorder != null) currentRecorder.recordDrawArrays(mode, start, count);
}
public static void recordDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
if (currentRecorder != null) currentRecorder.recordDrawElements(mode, indices_count, type, indices_buffer_offset);
}
public static void recordBindVBO(int vbo) { if (currentRecorder != null) currentRecorder.recordBindVBO(vbo); }
public static void recordBindVAO(int vao) { if (currentRecorder != null) currentRecorder.recordBindVAO(vao); }
@@ -822,17 +821,34 @@ public class DisplayListManager {
final boolean hasDraws = accumulatedDraws != null && !accumulatedDraws.isEmpty();
if (hasCommands || hasDraws) {
// Phase 1: Compile format-based VBOs (shared by both optimized and unoptimized paths)
final DisplayListVBO compiledBuffers = new DisplayListVBOBuilder().addDraws(accumulatedDraws).build();
try {
final CommandBuffer finalBuffer = new CommandBuffer();
CommandBufferBuilder.buildFromRawBuffer(rawCommandBuffer, accumulatedDraws, finalBuffer);
// Build to CommandBuffer - optimize raw buffer to final buffer
final CommandBuffer finalBuffer = new CommandBuffer();
CommandBufferBuilder.buildFromRawBuffer(rawCommandBuffer, accumulatedDraws, finalBuffer);
final IndexedDrawBatchBuilder indexedBuilder = currentRecorder.getIndexedDraws();
final List<IndexedDrawBatch> indexedBatches;
if (indexedBuilder.isEmpty()) {
indexedBatches = Collections.emptyList();
} else {
final CommandRecorder paused = pauseRecording();
try {
indexedBatches = indexedBuilder.build();
} finally {
try {
for (IndexedDrawCapture c : indexedBuilder.getCaptures()) {
c.freeBuffers();
}
} finally {
resumeRecording(paused);
}
}
}
// Free the recorder (and its buffer) after optimization
currentRecorder.free();
compiled = new CompiledDisplayList(finalBuffer.toBuffer(), finalBuffer.getComplexObjects(), compiledBuffers);
compiled = new CompiledDisplayList(finalBuffer.toBuffer(), finalBuffer.getComplexObjects(), compiledBuffers, indexedBatches);
} finally {
currentRecorder.free();
}
} else {
// Free the recorder even if empty
if (currentRecorder != null) {
@@ -841,8 +857,10 @@ public class DisplayListManager {
// Empty display list - per OpenGL spec, still valid after glNewList/glEndList
compiled = CompiledDisplayList.EMPTY;
}
// Store the compiled list (even if empty - an empty list is still a valid list)
displayListCache.put(glListId, compiled);
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
}
// Log compilation details if enabled (before context restoration changes glListId)
if (LOG_DISPLAY_LIST_COMPILATION) {
@@ -895,7 +913,7 @@ public class DisplayListManager {
}
// Merge the previous draw call if possible
final AccumulatedDraw previous = accumulatedDraws.get(accumulatedDraws.size() - 1);
final AccumulatedDraw previous = accumulatedDraws.getLast();
if (previous.format == tessellator.getVertexFormat() && previous.stateGeneration == stateGeneration) {
previous.mergeDraw(tessellator, copyLast);
return;
@@ -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;
@@ -186,6 +188,8 @@ public class GLStateManager {
@Getter private static Vendor VENDOR;
@Getter private static boolean windows;
// This setting varies depending on driver, so it gets queried at runtime
public static int DEFAULT_DRAW_BUFFER = 0x0405; // GL_BACK
@@ -419,6 +423,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();
@@ -513,6 +520,12 @@ public class GLStateManager {
// After Display.create(), viewport is (0, 0, width, height)
viewportState.setViewPort(0, 0, displayWidth, displayHeight);
}
final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR);
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
final String os = System.getProperty("os.name").toLowerCase();
windows = os.contains("win");
}
static void init(Runnable initCallback) {
@@ -520,9 +533,6 @@ public class GLStateManager {
DEFAULT_DRAW_BUFFER = RENDER_BACKEND.getInteger(GL11.GL_DRAW_BUFFER);
final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR);
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
// The initial mask value should be defined as all 1's. However, some drivers have it set to 0's.
// To ensure consistency & correctness across all drivers, we're setting them to 0xFF.
RENDER_BACKEND.stencilFunc(stencilState.getFuncFront(), stencilState.getRefFront(), 0xFF);
@@ -616,16 +626,6 @@ public class GLStateManager {
return BYPASS_CACHE || !isCachingEnabled();
}
/**
* Returns true if we should use DSA for texture operations. DSA relies on cached texture bindings, which aren't tracked when caching is disabled. Also,
* proxy textures are special query textures that shouldn't use DSA.
*/
private static boolean shouldUseDSA(int target) {
if (!isCachingEnabled()) return false;
// Proxy textures are for capability queries, not real textures
return target != GL11.GL_PROXY_TEXTURE_1D && target != GL11.GL_PROXY_TEXTURE_2D && target != GL12.GL_PROXY_TEXTURE_3D;
}
// LWJGL Overrides
public static void glEnable(int cap) {
final RecordMode mode = DisplayListManager.getRecordMode();
@@ -946,10 +946,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 +1061,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 +1076,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 +1099,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 +1633,9 @@ public class GLStateManager {
}
alphaTest.enable();
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
public static void disableAlphaTest() {
@@ -1631,6 +1653,9 @@ public class GLStateManager {
}
alphaTest.disable();
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
public static void glAlphaFunc(int function, float reference) {
@@ -1650,6 +1675,9 @@ public class GLStateManager {
alphaState.setFunction(function);
alphaState.setReference(reference);
fragmentGeneration++;
if (GLSMHooks.ALPHA_STATE_CHANGE.hasListeners()) {
GLSMHooks.ALPHA_STATE_CHANGE.post(GLSMHooks.alphaStateChangeEvent);
}
}
}
@@ -1795,13 +1823,7 @@ public class GLStateManager {
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels);
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
} else {
// Non-main thread or proxy texture - use direct GL call
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -1846,11 +1868,7 @@ public class GLStateManager {
}
TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels != null ? pixels.asIntBuffer() : null);
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
RenderSystem.textureImage2D(getBoundTextureForServerState(), target, level, internalformat, width, height, border, format, type, pixels);
} else {
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
}
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -2145,6 +2163,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
RENDER_BACKEND.drawElements(mode, indices);
}
@@ -2158,6 +2177,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
} else {
@@ -2175,6 +2195,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(indices);
} else {
@@ -2192,6 +2213,7 @@ public class GLStateManager {
}
prepareWideLineEmulation(mode);
ShaderManager.getInstance().preDraw();
prepareClientArrays();
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTriangles(count, type, indices);
} else {
@@ -2203,15 +2225,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 +2242,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 +2265,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 +2350,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 +2375,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 +2420,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 +2440,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 +2466,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 +2488,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 +2540,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 +2565,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); }
@@ -4195,6 +4280,7 @@ public class GLStateManager {
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
GLSMHooks.programChangeEvent.previousProgram = prev;
GLSMHooks.programChangeEvent.newProgram = 0;
GLSMHooks.programChangeEvent.postBind = false;
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
}
ffp.activate();
@@ -4215,6 +4301,7 @@ public class GLStateManager {
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
GLSMHooks.programChangeEvent.previousProgram = prev;
GLSMHooks.programChangeEvent.newProgram = program;
GLSMHooks.programChangeEvent.postBind = false;
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
}
if (initConfig != null && initConfig.isLwjglDebug()) {
@@ -4223,6 +4310,12 @@ public class GLStateManager {
}
RENDER_BACKEND.useProgram(program);
CompatUniformManager.onUseProgram(program);
if (GLSMHooks.PROGRAM_CHANGE.hasListeners()) {
GLSMHooks.programChangeEvent.previousProgram = prev;
GLSMHooks.programChangeEvent.newProgram = program;
GLSMHooks.programChangeEvent.postBind = true;
GLSMHooks.PROGRAM_CHANGE.post(GLSMHooks.programChangeEvent);
}
}
}
@@ -4297,9 +4390,21 @@ 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;
if (initConfig.noErrorChecks()) 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();
@@ -4342,13 +4447,7 @@ public class GLStateManager {
}
}
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture - keeps GL binding state unchanged
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
} else {
// Non-main thread or proxy texture - use direct GL call
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
}
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -4361,13 +4460,7 @@ public class GLStateManager {
}
}
suspendPixelUnpackBuffer();
if (shouldUseDSA(target)) {
// Use DSA to upload directly to the texture
RenderSystem.textureSubImage2D(getBoundTextureForServerState(), target, level, xoffset, yoffset, width, height, format, type, pixels);
} else {
// Non-main thread or proxy texture - use direct GL call
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
}
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
}
@@ -4845,12 +4938,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 +5062,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;
}
@@ -5093,7 +5180,7 @@ public class GLStateManager {
}
public static boolean isFramebufferEnabled() {
return initConfig != null && initConfig.isFramebufferSupported() && initConfig.isFboEnabled();
return true;
}
private static final boolean FFP_WARN_ON_UNSUPPORTED = Boolean.parseBoolean(System.getProperty("angelica.ffp.warnOnUnsupported", "false"));
@@ -5558,7 +5645,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 +5708,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 +5912,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);
}
}
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.glsm.dsa.DSACore;
import com.gtnewhorizons.angelica.glsm.dsa.DSAEXT;
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
@@ -61,7 +62,13 @@ public class RenderSystem {
if (rendererInitialized) return;
rendererInitialized = true;
try {
if (GLStateManager.capabilities.OpenGL45) {
if (GLStateManager.vendorIsIntel() && GLStateManager.isWindows()) {
dsaState = new DSAUnsupported();
GLStateManager.LOGGER.info("Detected Intel drivers on Windows, disabling DSA.");
} else if (!GLStateManager.getInitConfig().isDSAEnabled()) {
dsaState = new DSAUnsupported();
GLStateManager.LOGGER.info("enableDSA is set to false, disabling DSA.");
} else if (GLStateManager.capabilities.OpenGL45) {
dsaState = (Runtime.version().feature() > 8 && GLStateManager.capabilities.GL_EXT_direct_state_access) ? new DSAEXT() : new DSACore();
GLStateManager.LOGGER.info("OpenGL 4.5 detected, enabling DSA.");
}
@@ -432,7 +439,7 @@ public class RenderSystem {
}
public static void deleteBuffers(int buffer) {
RENDER_BACKEND.deleteBuffers(buffer);
GLStateManager.glDeleteBuffers(buffer);
}
public static long getVRAM() {
@@ -491,7 +498,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() {}
}
@@ -8,24 +8,24 @@ import java.util.function.Consumer;
public final class GLSMInitConfig {
private final boolean lwjglDebug;
private final StreamingUploader.UploadStrategy streamingUploadStrategy;
private final boolean framebufferSupported;
private final boolean fboEnabled;
private final Consumer<DirectTessellator> directDrawer;
private final Runnable streamingDrawerDestroy;
private final int displayWidth;
private final int displayHeight;
private final Runnable postInitCallback;
private final boolean enableDSA;
private final boolean noErrorChecks;
private GLSMInitConfig(Builder builder) {
this.lwjglDebug = builder.lwjglDebug;
this.streamingUploadStrategy = builder.streamingUploadStrategy;
this.framebufferSupported = builder.framebufferSupported;
this.fboEnabled = builder.fboEnabled;
this.noErrorChecks = builder.noErrorChecks;
this.directDrawer = builder.directDrawer;
this.streamingDrawerDestroy = builder.streamingDrawerDestroy;
this.displayWidth = builder.displayWidth;
this.displayHeight = builder.displayHeight;
this.postInitCallback = builder.postInitCallback;
this.enableDSA = builder.enableDSA;
}
public static Builder builder() {
@@ -34,24 +34,24 @@ public final class GLSMInitConfig {
public boolean isLwjglDebug() { return lwjglDebug; }
public StreamingUploader.UploadStrategy getStreamingUploadStrategy() { return streamingUploadStrategy; }
public boolean isFramebufferSupported() { return framebufferSupported; }
public boolean isFboEnabled() { return fboEnabled; }
public Consumer<DirectTessellator> getDirectDrawer() { return directDrawer; }
public Runnable getStreamingDrawerDestroy() { return streamingDrawerDestroy; }
public int getDisplayWidth() { return displayWidth; }
public int getDisplayHeight() { return displayHeight; }
public Runnable getPostInitCallback() { return postInitCallback; }
public boolean isDSAEnabled() { return enableDSA; }
public boolean noErrorChecks() { return noErrorChecks; }
public static final class Builder {
private boolean lwjglDebug = false;
private StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.BUFFER_DATA;
private boolean framebufferSupported = false;
private boolean fboEnabled = false;
private Consumer<DirectTessellator> directDrawer = null;
private Runnable streamingDrawerDestroy = null;
private int displayWidth = 0;
private int displayHeight = 0;
private Runnable postInitCallback = null;
private boolean enableDSA = false;
private boolean noErrorChecks;
private Builder() {}
@@ -65,13 +65,8 @@ public final class GLSMInitConfig {
return this;
}
public Builder framebufferSupported(boolean framebufferSupported) {
this.framebufferSupported = framebufferSupported;
return this;
}
public Builder fboEnabled(boolean fboEnabled) {
this.fboEnabled = fboEnabled;
public Builder noErrorChecks(boolean noErrorChecks) {
this.noErrorChecks = noErrorChecks;
return this;
}
@@ -96,6 +91,11 @@ public final class GLSMInitConfig {
return this;
}
public Builder enableDSA(boolean enableDSA) {
this.enableDSA = enableDSA;
return this;
}
public GLSMInitConfig build() {
return new GLSMInitConfig(this);
}
@@ -0,0 +1,9 @@
package com.gtnewhorizons.angelica.glsm.hooks.events;
import net.minecraftforge.eventbus.api.event.MutableEvent;
/**
* AlphaTest changed - Listeners should query GLSM alpha state directly if they care what changed.
*/
public final class AlphaStateChangeEvent extends MutableEvent {
}
@@ -5,4 +5,5 @@ import net.minecraftforge.eventbus.api.event.MutableEvent;
public final class ProgramChangeEvent extends MutableEvent {
public int previousProgram;
public int newProgram;
public boolean postBind;
}
@@ -0,0 +1,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);
});
}
}
@@ -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);
}
@@ -24,7 +24,9 @@ public class ModStatus {
public static boolean isThaumcraftLoaded;
public static boolean isThaumicHorizonsLoaded;
public static boolean isBaublesLoaded;
public static boolean isCosmeticArmorReworkedLoaded;
public static boolean isFluidLoggedLoaded;
public static boolean isBOPLoaded;
public static void preInit() {
if (Loader.isModLoaded("backhand")) {
@@ -42,9 +44,11 @@ public class ModStatus {
isThaumcraftLoaded = Loader.isModLoaded("Thaumcraft");
isThaumicHorizonsLoaded = Loader.isModLoaded("ThaumicHorizons");
isBaublesLoaded = Loader.isModLoaded("Baubles");
isCosmeticArmorReworkedLoaded = Loader.isModLoaded("cosmeticarmorreworked");
isFluidLoggedLoaded = Loader.isModLoaded("fluidlogged");
isHoloInventoryLoaded = Loader.isModLoaded("holoinventory");
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;
@@ -63,6 +63,11 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean enableVAO;
@Config.Comment("Enables DSA (Direct State Access) for faster bindings. Disable if you notice terrible performance.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean enableDSA;
@Config.Comment("Enable NotFine features")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
@@ -235,6 +240,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 +272,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.")
@@ -287,4 +302,9 @@ public class AngelicaConfig {
@Config.RequiresMcRestart
public static boolean disableGLVersionPinning;
@Config.Comment("Disables GL Error checks. Always set to false in dev env or if LWJGL debug is on. Improves performance.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean disableErrorChecks;
}
@@ -45,23 +45,9 @@ public class CompatConfig {
@Config.RequiresMcRestart
public static boolean fixSecurityCraft;
@Config.Comment("Apply tweaks to NTM:Space")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean tweakNTMSpace;
@Config.Comment("Apply fixes to NTM:Space")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean fixNTMSpace;
@Config.Comment("Disables rendering of the planet below the player at high altitudes [Requires tweakNTMSpace]")
@Config.DefaultEnum("ShadersOnly")
public static altitudePlanetRendererState NTMSpace_disableAltitudePlanetRenderer;
public enum altitudePlanetRendererState {
Always,
ShadersOnly,
Never
}
}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.dynamiclights;
import baubles.common.lib.PlayerHandler;
import lain.mods.cos.CosmeticArmorReworked;
import com.gtnewhorizon.gtnhlib.blockpos.BlockPos;
import com.gtnewhorizon.gtnhlib.blockpos.IBlockPos;
import com.gtnewhorizon.gtnhlib.util.CoordinatePacker;
@@ -180,6 +181,19 @@ public class DynamicLights {
this.lightSourcesLock.writeLock().unlock();
}
/**
* Removes all tracked light sources, to call when unloading the client world
*/
public void removeAllLightSources() {
this.lightSourcesLock.writeLock().lock();
this.dynamicLightSources.clear();
this.lightSourceCount = 0;
rebuildLightSourceArray();
this.lightSourcesLock.writeLock().unlock();
}
/**
* Removes light sources if the filter matches.
*
@@ -625,6 +639,18 @@ public class DynamicLights {
}
}
}
if (ModStatus.isCosmeticArmorReworkedLoaded) {
final var cosArmor = CosmeticArmorReworked.invMan.getCosArmorInventoryClient(player.getUniqueID());
if (cosArmor != null) {
for (int i = 0; i < cosArmor.getSizeInventory(); i++) {
final var stack = cosArmor.getStackInSlot(i);
if (stack != null) {
luminance = Math.max(luminance, getLuminanceFromItemStack(stack));
}
}
}
}
}
return luminance;
Whitespace-only changes.
@@ -76,7 +76,7 @@ public class HUDCaching {
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.enableTexture();
if (!AngelicaConfig.hudCachingActive || !GLStateManager.isFramebufferEnabled()) {
if (!AngelicaConfig.hudCachingActive) {
ingame.renderGameOverlay(partialTicks, hasScreen, mouseX, mouseY);
return;
}
@@ -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,14 +11,32 @@ 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;
import net.coderbot.iris.texture.pbr.PBRTextureManager;
import net.coderbot.iris.vertices.ImmediateState;
public class IrisGLSMBridge {
private static Runnable alphaFuncListener = null;
private static Runnable alphaTestListener = null;
private static Runnable blendFuncListener = null;
private static Runnable fogModeListener = null;
private static Runnable fogStartListener = null;
private static Runnable fogEndListener = null;
private static Runnable fogDensityListener = null;
static {
StateUpdateNotifiers.alphaFuncNotifier = listener -> alphaFuncListener = listener;
StateUpdateNotifiers.alphaTestNotifier = listener -> alphaTestListener = listener;
StateUpdateNotifiers.blendFuncNotifier = listener -> blendFuncListener = listener;
StateUpdateNotifiers.fogModeNotifier = listener -> fogModeListener = listener;
StateUpdateNotifiers.fogStartNotifier = listener -> fogStartListener = listener;
StateUpdateNotifiers.fogEndNotifier = listener -> fogEndListener = listener;
StateUpdateNotifiers.fogDensityNotifier = listener -> fogDensityListener = listener;
}
public static void register() {
IrisSamplers.initRenderer();
GLSMHooks.blendHandler = new DeferredBlendHandler() {
@@ -76,8 +95,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);
@@ -106,20 +148,37 @@ public class IrisGLSMBridge {
}
});
GLSMHooks.PROGRAM_CHANGE.addListener(event -> ProgramUniforms.clearActiveUniforms());
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
if (!Iris.enabled) return;
if (event.postBind) return;
ProgramUniforms.clearActiveUniforms();
});
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
if (!Iris.enabled) return;
if (event.postBind) return;
final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
if (!(pipeline instanceof DeferredWorldRenderingPipeline drp)) return;
if (!drp.shouldOverrideShaders()) return;
if (drp.getActivePassProgramId() == -1) return;
if (!ImmediateState.isRenderingLevel || DepthColorStorage.isOwnedProgram(event.newProgram)) {
if (DepthColorStorage.isOwnedProgram(event.newProgram)) {
DepthColorStorage.unlockDepthColor();
} else {
drp.onModProgramOverride();
}
});
GLSMHooks.PROGRAM_CHANGE.addListener(event -> {
if (!Iris.enabled) return;
if (!event.postBind) return;
WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable();
if (pipeline instanceof DeferredWorldRenderingPipeline drp) {
DeferredWorldRenderingPipeline.Pass activePass = drp.getActivePassProgram();
if (activePass != null && activePass.getProgram() != null && activePass.getProgram().getProgramId() == event.newProgram) {
activePass.getProgram().getUniforms().update();
}
}
});
}
}
@@ -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) {
@@ -103,26 +106,6 @@ public final class ClientProxy extends CommonProxy {
blockError = new BlockError();
}
@SubscribeEvent
public void worldLoad(WorldEvent.Load event) {
if (GLStateManager.isRunningSplash()) {
GLStateManager.setRunningSplash(false);
LOGGER.info("World loaded - Enabling GLSM Cache");
}
if (AngelicaConfig.enableCeleritas) {
ChunkTaskRegistry.reset();
ChunkTaskRegistry.registerProvider(DefaultChunkTaskProvider.INSTANCE);
ChunkTaskRegistry.registerProvider(ThreadedChunkTaskProvider.INSTANCE);
// Register all blocks. Because blockids are unique to a world, this must be done each load
GameData.getBlockRegistry().typeSafeIterable().forEach(o -> {
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, true, true);
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, false, true);
});
}
}
@Override
public void init(FMLInitializationEvent event) {
super.init(event);
@@ -167,7 +150,6 @@ public final class ClientProxy extends CommonProxy {
}
}
@Override
public void postInit(FMLPostInitializationEvent event) {
super.postInit(event);
@@ -182,13 +164,43 @@ 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;
}
}
@SubscribeEvent
public void worldLoad(WorldEvent.Load event) {
if (!event.world.isRemote) return;
if (GLStateManager.isRunningSplash()) {
GLStateManager.setRunningSplash(false);
LOGGER.info("World loaded - Enabling GLSM Cache");
}
if (AngelicaConfig.enableCeleritas) {
ChunkTaskRegistry.reset();
ChunkTaskRegistry.registerProvider(DefaultChunkTaskProvider.INSTANCE);
ChunkTaskRegistry.registerProvider(ThreadedChunkTaskProvider.INSTANCE);
// Register all blocks. Because blockids are unique to a world, this must be done each load
GameData.getBlockRegistry().typeSafeIterable().forEach(o -> {
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, true, true);
AngelicaBlockSafetyRegistry.canBlockRenderOffThread(o, false, true);
});
}
}
@SubscribeEvent
public void onWorldUnload(WorldEvent.Unload event) {
if (!event.world.isRemote) return;
DynamicLights.get().removeAllLightSources();
}
float lastIntegratedTickTime;
@SubscribeEvent
public void onTick(TickEvent.ServerTickEvent event) {
if (FMLCommonHandler.instance().getSide().isClient() && event.phase == TickEvent.Phase.END) {
if (event.phase == TickEvent.Phase.END) {
final IntegratedServer srv = Minecraft.getMinecraft().getIntegratedServer();
if (srv != null) {
final long currentTickTime = srv.tickTimeArray[srv.getTickCounter() % 100];
@@ -337,10 +349,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);
}
@@ -8,7 +8,10 @@ import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
import com.gtnewhorizons.angelica.rendering.celeritas.iris.BlockRenderContext;
import com.gtnewhorizons.angelica.rendering.celeritas.iris.ContextAwareChunkVertexEncoder;
import com.prupe.mcpatcher.mal.block.RenderBlocksUtils;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
import net.coderbot.iris.block_rendering.BlockMaterialMapping;
import net.coderbot.iris.block_rendering.BlockRenderingSettings;
import net.coderbot.iris.vertices.ExtendedDataHelper;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
@@ -20,6 +23,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 +143,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);
}
}
@@ -242,7 +264,13 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
if (isFluid) {
contextEncoder.prepareToRenderFluid(blockRenderContext, block, lightValue);
} else {
contextEncoder.prepareToRenderBlock(blockRenderContext, block, metadata,
int effectiveMeta = metadata;
if (BlockRenderingSettings.INSTANCE.hasSnowyEntries()
&& BlockRenderingSettings.INSTANCE.getSnowyBlocks().contains(block)
&& RenderBlocksUtils.isSnowCovered(renderBlocks.blockAccess, x, y, z)) {
effectiveMeta |= BlockMaterialMapping.SNOWY_META_BIT;
}
contextEncoder.prepareToRenderBlock(blockRenderContext, block, effectiveMeta,
ExtendedDataHelper.BLOCK_RENDER_TYPE, lightValue);
}
}
@@ -256,6 +284,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 {
@@ -166,6 +180,7 @@ public class CTMUtils {
local.renderBlockState.setBlock(block, blockAccess, x, y, z);
local.renderBlockState.setFace(face);
TileOverride lastOverride = local.ijkIterator.go(local.renderBlockState, icon);
local.renderBlockState.blockAccess = null;
if (lastOverride != null) {
return skipDefaultRendering(block) ? RenderBlocksUtils.blankIcon : local.ijkIterator.getIcon();
}
@@ -219,6 +234,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,411 @@
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};
/** Maps split-face quadrant (0=TL,1=TR,2=BR,3=BL face-local) to RenderBlocks per-face TL/TR/BR/BL orientation. */
private static final int[][] RENDER_FACE_QUADRANT_MAP = {
{3, 2, 1, 0}, // Y-
{2, 3, 0, 1}, // Y+
{3, 0, 1, 2}, // Z-
{0, 1, 2, 3}, // Z+
{3, 0, 1, 2}, // X-
{1, 2, 3, 0} // X+
};
private static final float[] QUADRANT_MIN_U = {0.0f, 0.5f, 0.5f, 0.0f};
private static final float[] QUADRANT_MIN_V = {0.0f, 0.0f, 0.5f, 0.5f};
private final IIcon[] sprites;
private final CompactConnectingCtmProperties props;
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) {
final Block block = world.getBlock(x, y, z);
final double minX = rb.renderMinX, minY = rb.renderMinY, minZ = rb.renderMinZ;
final double maxX = rb.renderMaxX, maxY = rb.renderMaxY, maxZ = rb.renderMaxZ;
// Without AO the corner color/brightness fields are unused; skip the lighting dance.
final boolean ao = rb.enableAO;
final float rTL, rTR, rBL, rBR, gTL, gTR, gBL, gBR, bTL, bTR, bBL, bBR;
final int brTL, brTR, brBL, brBR;
if (ao) {
rTL = rb.colorRedTopLeft; rTR = rb.colorRedTopRight;
rBL = rb.colorRedBottomLeft; rBR = rb.colorRedBottomRight;
gTL = rb.colorGreenTopLeft; gTR = rb.colorGreenTopRight;
gBL = rb.colorGreenBottomLeft; gBR = rb.colorGreenBottomRight;
bTL = rb.colorBlueTopLeft; bTR = rb.colorBlueTopRight;
bBL = rb.colorBlueBottomLeft; bBR = rb.colorBlueBottomRight;
brTL = rb.brightnessTopLeft; brTR = rb.brightnessTopRight;
brBL = rb.brightnessBottomLeft; brBR = rb.brightnessBottomRight;
} else {
rTL = rTR = rBL = rBR = 0f;
gTL = gTR = gBL = gBR = 0f;
bTL = bTR = bBL = bBR = 0f;
brTL = brTR = brBL = brBR = 0;
}
for (int q = 0; q < 4; q++) {
final IIcon icon = sprites[getQuadrantSprite(q, connections)];
if (icon == null) {
continue;
}
if (ao) {
applyQuadrantLighting(rb, face, q, rTL, rTR, rBL, rBR, gTL, gTR, gBL, gBR, bTL, bTR, bBL, bBR, brTL, brTR, brBL, brBR);
}
setQuadrantBounds(rb, face, q, minX, minY, minZ, maxX, maxY, maxZ);
rb.overrideBlockTexture = icon;
renderFace(rb, block, x, y, z, face, icon);
}
if (ao) {
rb.colorRedTopLeft = rTL; rb.colorRedTopRight = rTR;
rb.colorRedBottomLeft = rBL; rb.colorRedBottomRight = rBR;
rb.colorGreenTopLeft = gTL; rb.colorGreenTopRight = gTR;
rb.colorGreenBottomLeft = gBL; rb.colorGreenBottomRight = gBR;
rb.colorBlueTopLeft = bTL; rb.colorBlueTopRight = bTR;
rb.colorBlueBottomLeft = bBL; rb.colorBlueBottomRight = bBR;
rb.brightnessTopLeft = brTL; rb.brightnessTopRight = brTR;
rb.brightnessBottomLeft = brBL; rb.brightnessBottomRight = brBR;
}
rb.overrideBlockTexture = null;
rb.setRenderBounds(minX, minY, minZ, maxX, maxY, maxZ);
}
private static void applyQuadrantLighting(RenderBlocks rb, int face, int quadrant, float rTL, float rTR, float rBL, float rBR, float gTL, float gTR, float gBL, float gBR, float bTL, float bTR, float bBL, float bBR, int brTL, int brTR, int brBL, int brBR) {
final int rq = RENDER_FACE_QUADRANT_MAP[face][quadrant];
final float u0 = QUADRANT_MIN_U[rq], u1 = u0 + 0.5f;
final float v0 = QUADRANT_MIN_V[rq], v1 = v0 + 0.5f;
rb.colorRedTopLeft = bilerp(rTL, rTR, rBL, rBR, u0, v0);
rb.colorRedTopRight = bilerp(rTL, rTR, rBL, rBR, u1, v0);
rb.colorRedBottomLeft = bilerp(rTL, rTR, rBL, rBR, u0, v1);
rb.colorRedBottomRight = bilerp(rTL, rTR, rBL, rBR, u1, v1);
rb.colorGreenTopLeft = bilerp(gTL, gTR, gBL, gBR, u0, v0);
rb.colorGreenTopRight = bilerp(gTL, gTR, gBL, gBR, u1, v0);
rb.colorGreenBottomLeft = bilerp(gTL, gTR, gBL, gBR, u0, v1);
rb.colorGreenBottomRight = bilerp(gTL, gTR, gBL, gBR, u1, v1);
rb.colorBlueTopLeft = bilerp(bTL, bTR, bBL, bBR, u0, v0);
rb.colorBlueTopRight = bilerp(bTL, bTR, bBL, bBR, u1, v0);
rb.colorBlueBottomLeft = bilerp(bTL, bTR, bBL, bBR, u0, v1);
rb.colorBlueBottomRight = bilerp(bTL, bTR, bBL, bBR, u1, v1);
rb.brightnessTopLeft = bilerpLight(brTL, brTR, brBL, brBR, u0, v0);
rb.brightnessTopRight = bilerpLight(brTL, brTR, brBL, brBR, u1, v0);
rb.brightnessBottomLeft = bilerpLight(brTL, brTR, brBL, brBR, u0, v1);
rb.brightnessBottomRight = bilerpLight(brTL, brTR, brBL, brBR, u1, v1);
}
private static float bilerp(float c00, float c10, float c01, float c11, float u, float v) {
final float top = c00 + (c10 - c00) * u;
final float bot = c01 + (c11 - c01) * u;
return top + (bot - top) * v;
}
/** Brightness packs sky in upper 16 bits, block in lower; bilerp each independently then repack. */
private static int bilerpLight(int c00, int c10, int c01, int c11, float u, float v) {
final int b00 = c00 & 0xFFFF, b10 = c10 & 0xFFFF, b01 = c01 & 0xFFFF, b11 = c11 & 0xFFFF;
final int s00 = c00 >>> 16, s10 = c10 >>> 16, s01 = c01 >>> 16, s11 = c11 >>> 16;
final float bTop = b00 + (b10 - b00) * u;
final float bBot = b01 + (b11 - b01) * u;
final float sTop = s00 + (s10 - s00) * u;
final float sBot = s01 + (s11 - s01) * u;
final int block = Math.round(bTop + (bBot - bTop) * v);
final int sky = Math.round(sTop + (sBot - sTop) * v);
return (sky << 16) | (block & 0xFFFF);
}
private void renderWholeFace(RenderBlocks rb, 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;
@@ -157,7 +157,7 @@ public class RenderBlocksUtils {
return isSnow ? Blocks.snow_layer.getIcon(blockAccess, x, y, z, face) : topIcon;
}
private static boolean isSnowCovered(IBlockAccess blockAccess, int x, int y, int z) {
public static boolean isSnowCovered(IBlockAccess blockAccess, int x, int y, int z) {
Block topBlock = blockAccess.getBlock(x, y + 1, z);
return topBlock == Blocks.snow_layer || topBlock == Blocks.snow;
}
+3 -2
View File
@@ -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;
}
}
+11 -22
View File
@@ -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,11 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import it.unimi.dsi.fastutil.objects.ReferenceSet;
import net.coderbot.iris.shaderpack.materialmap.BlockEntry;
import net.coderbot.iris.shaderpack.materialmap.BlockRenderType;
import net.coderbot.iris.shaderpack.materialmap.FlatteningMap;
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
import net.minecraft.block.Block;
import net.minecraft.init.Blocks;
@@ -19,37 +22,46 @@ import java.util.Map;
import java.util.Set;
public class BlockMaterialMapping {
/** Meta-key bit OR'd in at runtime when a snowy-tagged block has snow above it. */
public static final int SNOWY_META_BIT = 0x10;
/**
* Creates a two-level map structure for block material IDs.
* Based on Iris's BlockState mapping approach adapted for 1.7.10's metadata system.
*/
public static Reference2ObjectMap<Block, Int2IntMap> createBlockMetaIdMap(Int2ObjectMap<List<BlockEntry>> blockPropertiesMap) {
Reference2ObjectMap<Block, Int2IntMap> blockMatches = new Reference2ObjectOpenHashMap<>();
public static Reference2ObjectMap<Block, Int2IntMap> createBlockMetaIdMap(Int2ObjectMap<List<BlockEntry>> blockPropertiesMap, boolean skipFlattening) {
final Reference2ObjectMap<Block, Int2IntMap> blockMatches = new Reference2ObjectOpenHashMap<>();
final ReferenceSet<Block> snowyBlocks = new ReferenceOpenHashSet<>();
blockPropertiesMap.forEach((intId, entries) -> {
for (BlockEntry entry : entries) {
addBlockMetas(entry, blockMatches, intId);
addBlockMetas(entry, blockMatches, intId, skipFlattening, snowyBlocks);
}
});
BlockRenderingSettings.INSTANCE.setHasSnowyEntries(!snowyBlocks.isEmpty());
BlockRenderingSettings.INSTANCE.setSnowyBlocks(snowyBlocks);
return blockMatches;
}
public static Map<Block, BlockRenderLayer> createBlockTypeMap(Map<NamespacedId, BlockRenderType> blockPropertiesMap) {
Map<Block, BlockRenderLayer> blockTypeIds = new Reference2ReferenceOpenHashMap<>();
final Map<Block, BlockRenderLayer> blockTypeIds = new Reference2ReferenceOpenHashMap<>();
blockPropertiesMap.forEach((id, blockType) -> {
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
final Block block = Block.getBlockFromName(resourceLocation.toString());
Block block = resolveBlockOrNull(id);
if (block == null || block == Blocks.air) {
return;
// Modern names like "grass_block" don't exist in 1.7.10's registry; fall back to flattening.
if (block == null && "minecraft".equals(id.getNamespace())) {
final List<BlockEntry> legacyEntries = FlatteningMap.toLegacy(id.getName(), Map.of());
if (legacyEntries != null) {
block = resolveBlockOrNull(legacyEntries.getFirst().getId());
}
}
if (block == null) return;
final BlockRenderLayer layer = convertBlockToRenderLayer(blockType);
if (layer != null) {
blockTypeIds.put(block, layer);
}
if (layer != null) blockTypeIds.put(block, layer);
});
return blockTypeIds;
@@ -72,20 +84,29 @@ public class BlockMaterialMapping {
* Adds block+metadata combinations to the material ID map.
* Based on Iris's addBlockStates method, adapted for 1.7.10 metadata system.
*/
private static void addBlockMetas(BlockEntry entry, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId) {
private static void addBlockMetas(BlockEntry entry, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId, boolean skipFlattening, ReferenceSet<Block> snowyBlocks) {
final NamespacedId id = entry.getId();
final ResourceLocation resourceLocation = new ResourceLocation(id.getNamespace(), id.getName());
final Map<String, String> stateProps = entry.getStateProperties();
final String snowy = stateProps.get("snowy");
final int snowyBit = "true".equals(snowy) ? SNOWY_META_BIT : 0;
final Block block = (Block) Block.blockRegistry.getObject(resourceLocation.toString());
// If the block doesn't exist, by default the registry will return AIR. That probably isn't what we want.
// TODO: Assuming that Registry.BLOCK.getDefaultId() == "minecraft:air" here
if (block == null || block == Blocks.air) {
return;
// Vanilla modern names go through FlatteningMap; legacy-section packs and modded blocks
// resolve directly from the registry.
List<BlockEntry> targets = null;
if (!skipFlattening && "minecraft".equals(id.getNamespace()) && (!stateProps.isEmpty() || entry.getMetas().isEmpty())) {
targets = FlatteningMap.toLegacy(id.getName(), stateProps);
}
if (targets == null) targets = List.of(entry);
Set<Integer> metas = entry.getMetas();
for (BlockEntry target : targets) {
final Block block = resolveBlockOrNull(target.getId());
if (block == null) continue;
applyMetas(block, target.getMetas(), idMap, intId, snowyBit);
if (snowy != null) snowyBlocks.add(block);
}
}
private static void applyMetas(Block block, Set<Integer> metas, Reference2ObjectMap<Block, Int2IntMap> idMap, int intId, int snowyBit) {
Int2IntMap metaMap = idMap.get(block);
if (metaMap == null) {
metaMap = new Int2IntOpenHashMap();
@@ -94,15 +115,18 @@ public class BlockMaterialMapping {
}
if (metas.isEmpty()) {
// Add all metadata values (0-15) if there aren't any specific ones
for (int meta = 0; meta < 16; meta++) {
metaMap.putIfAbsent(meta, intId);
}
for (int meta = 0; meta < 16; meta++) metaMap.putIfAbsent(meta | snowyBit, intId);
} else {
// Add only specific metadata values
for (int meta : metas) {
metaMap.putIfAbsent(meta, intId);
}
for (int meta : metas) metaMap.putIfAbsent(meta | snowyBit, intId);
}
}
/**
* Returns the registered Block for an id, or null if unknown.
* The registry returns Blocks.air (its default) for missing keys, which we coerce to null.
*/
private static Block resolveBlockOrNull(NamespacedId id) {
final Block block = (Block) Block.blockRegistry.getObject(new ResourceLocation(id.getNamespace(), id.getName()).toString());
return block == Blocks.air ? null : block;
}
}
@@ -4,7 +4,10 @@ import com.gtnewhorizons.angelica.rendering.celeritas.BlockRenderLayer;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntFunction;
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
import it.unimi.dsi.fastutil.objects.ReferenceSet;
import it.unimi.dsi.fastutil.objects.ReferenceSets;
import lombok.Getter;
import lombok.Setter;
import net.coderbot.iris.shaderpack.materialmap.NamespacedId;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
@@ -19,12 +22,21 @@ public class BlockRenderingSettings {
private boolean reloadRequired;
private Reference2ObjectMap<Block, Int2IntMap> blockMetaMatches;
private Map<Block, BlockRenderLayer> blockTypeIds;
private Object2IntFunction<NamespacedId> entityIds;
private Object2IntFunction<NamespacedId> itemIds;
private float ambientOcclusionLevel;
// note: no reload needed, entities are rebuilt every frame.
@Setter
private Object2IntFunction<NamespacedId> entityIds;
// note: no reload needed, items are rendered every frame.
@Setter
private Object2IntFunction<NamespacedId> itemIds;
@Getter
private float ambientOcclusionLevel;
private boolean disableDirectionalShading;
private boolean useSeparateAo;
private boolean useExtendedVertexFormat;
@Setter
private boolean hasSnowyEntries;
@Getter
private ReferenceSet<Block> snowyBlocks = ReferenceSets.emptySet();
public BlockRenderingSettings() {
reloadRequired = false;
@@ -34,6 +46,7 @@ public class BlockRenderingSettings {
disableDirectionalShading = false;
useSeparateAo = false;
useExtendedVertexFormat = false;
hasSnowyEntries = false;
}
public void clearReloadRequired() {
@@ -74,6 +87,14 @@ public class BlockRenderingSettings {
this.blockMetaMatches = blockMetaIds;
}
public boolean hasSnowyEntries() {
return hasSnowyEntries;
}
public void setSnowyBlocks(ReferenceSet<Block> snowyBlocks) {
this.snowyBlocks = snowyBlocks != null ? snowyBlocks : ReferenceSets.emptySet();
}
public void setBlockTypeIds(Map<Block, BlockRenderLayer> blockTypeIds) {
if (this.blockTypeIds != null && this.blockTypeIds.equals(blockTypeIds)) {
return;
@@ -83,21 +104,7 @@ public class BlockRenderingSettings {
this.blockTypeIds = blockTypeIds;
}
public void setEntityIds(Object2IntFunction<NamespacedId> entityIds) {
// note: no reload needed, entities are rebuilt every frame.
this.entityIds = entityIds;
}
public void setItemIds(Object2IntFunction<NamespacedId> itemIds) {
// note: no reload needed, items are rendered every frame.
this.itemIds = itemIds;
}
public float getAmbientOcclusionLevel() {
return ambientOcclusionLevel;
}
public void setAmbientOcclusionLevel(float ambientOcclusionLevel) {
public void setAmbientOcclusionLevel(float ambientOcclusionLevel) {
if (ambientOcclusionLevel == this.ambientOcclusionLevel) {
return;
}
@@ -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);
Loaded 100 of 221 files, more files were not shown because too many files have changed in this diff. Show more