Properly handle display lists with the push/pop optimizations (#2126)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-09-22 09:23:49 -07:00
1 parent 9dd60f6a75
commit 53776684c8
5 files changed
+361 -23

No files matched your search

@@ -79,6 +79,8 @@ public class DisplayListManager {
// Display list compilation state (current/active context)
private static int glListMode = 0;
private static int glListId = -1;
private static int recordedStatePushes;
private static final DisplayListCommand POP_STATE = GLStateManager::popState;
private static CommandRecorder currentRecorder = null; // Command recorder (null when not recording)
private static volatile Thread recordingThread = null; // Thread that started recording (for thread-safety)
private static List<AccumulatedDraw> accumulatedDraws = null; // Accumulates quad draws for batching
@@ -114,6 +116,7 @@ public class DisplayListManager {
List<AccumulatedDraw> draws,
DisplayListCallback callback,
StackTraceElement[] stackTrace,
int recordedStatePushes,
// Debug logging fields (only used when LOG_DISPLAY_LIST_COMPILATION)
List<String> pendingOps,
@@ -451,6 +454,25 @@ public class DisplayListManager {
currentRecorder.writePopAttrib();
}
public static int virtualStateDepth(RecordMode mode) {
return mode == RecordMode.COMPILE ? recordedStatePushes : 0;
}
public static void recordPushState(StateSet set) {
currentRecorder.writeComplexCommand(set.pushCommand);
recordedStatePushes++;
}
public static boolean recordPopStateIfPending() {
if (recordedStatePushes > 0) {
drawBarrier();
currentRecorder.writeComplexCommand(POP_STATE);
recordedStatePushes--;
return true;
}
return false;
}
public static void recordFogf(int pname, float param) {
drawBarrier();
currentRecorder.writeFogf(pname, param);
@@ -775,7 +797,7 @@ public class DisplayListManager {
// Save current compilation context and start fresh for nested list
final CompilationContext parentContext = new CompilationContext(
glListId, glListMode, currentRecorder, accumulatedDraws, transformCallback,
compilationStackTrace, pendingTransformOps, multMatrixSources, drawRangeSources
compilationStackTrace, recordedStatePushes, pendingTransformOps, multMatrixSources, drawRangeSources
);
compilationStack.push(parentContext);
}
@@ -787,6 +809,7 @@ public class DisplayListManager {
currentRecorder = new CommandRecorder(); // Create command recorder
accumulatedDraws = new ArrayList<>(8); // Fewer draws than commands typically
transformCallback = new DisplayListCallback();
recordedStatePushes = 0;
compilationStackTrace = SystemProperties.LOG_DISPLAY_LIST_COMPILATION ? Thread.currentThread().getStackTrace() : null;
// Initialize debug logging fields (only when logging enabled)
@@ -831,6 +854,12 @@ public class DisplayListManager {
// Stop compiling mode (works for both root and nested lists now)
TessellatorManager.stopCapturingDirect();
boolean unbalancedStatePush = false;
while (recordPopStateIfPending()) unbalancedStatePush = true;
if (unbalancedStatePush) {
GLStateManager.warnOnce("unbalanced-state-push", "Display list {} ended with unpopped internal state pushes - closing them", glListId);
}
flushAll();
// Reset back to MODELVIEW (default state)
@@ -921,6 +950,7 @@ public class DisplayListManager {
accumulatedDraws = parentContext.draws;
transformCallback = parentContext.callback;
compilationStackTrace = parentContext.stackTrace;
recordedStatePushes = parentContext.recordedStatePushes;
pendingTransformOps = parentContext.pendingOps;
multMatrixSources = parentContext.matrixSources;
drawRangeSources = parentContext.drawSources;
@@ -946,6 +976,7 @@ public class DisplayListManager {
drawRangeSources = null;
glListId = -1;
glListMode = 0;
recordedStatePushes = 0;
}
public static void abortCompilation() {
@@ -233,7 +233,7 @@ public class GLStateManager {
public static ContextCapabilities capabilities;
// Software stack depths for FFP emulation
public static final int MAX_ATTRIB_STACK_DEPTH = 16 + 2;
public static final int MAX_ATTRIB_STACK_DEPTH = 32;
public static final int MAX_MODELVIEW_STACK_DEPTH = 32 + 2;
public static final int MAX_PROJECTION_STACK_DEPTH = 4;
public static final int MAX_TEXTURE_STACK_DEPTH = 4;
@@ -3731,15 +3731,27 @@ public class GLStateManager {
}
public static void pushState(int mask) {
pushState(StateSet.forMask(mask));
pushStateLive(StateSet.forMask(mask));
}
public static int pushState(StateSet set) {
final RecordMode mode = DisplayListManager.getRecordMode();
if (mode != RecordMode.NONE) {
final int token = ctx().attribDepth + DisplayListManager.virtualStateDepth(mode);
if (token >= MAX_ATTRIB_STACK_DEPTH) {
throw new IllegalStateException("Attrib stack overflow: max depth " + MAX_ATTRIB_STACK_DEPTH + " reached");
}
DisplayListManager.recordPushState(set);
if (mode == RecordMode.COMPILE) {
return token;
}
}
return pushStateLive(set);
}
static int pushStateLive(StateSet set) {
attribPushes++;
final GLContextState glCtx = ctx();
if (set.glMask == 0 && DisplayListManager.getRecordMode() == RecordMode.COMPILE) {
throw new IllegalStateException("Internal state set pushed while compiling a display list");
}
if (glCtx.attribDepth >= MAX_ATTRIB_STACK_DEPTH) {
throw new IllegalStateException("Attrib stack overflow: max depth " + MAX_ATTRIB_STACK_DEPTH + " reached");
}
@@ -3783,7 +3795,10 @@ public class GLStateManager {
public static void popStateTo(int depth) {
final GLContextState glCtx = ctx();
while (glCtx.attribDepth > depth) {
final RecordMode mode = DisplayListManager.getRecordMode();
while (glCtx.attribDepth + DisplayListManager.virtualStateDepth(mode) > depth) {
if (mode == RecordMode.COMPILE && DisplayListManager.recordPopStateIfPending()) continue;
if (mode == RecordMode.COMPILE_AND_EXECUTE) DisplayListManager.recordPopStateIfPending();
popState();
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand;
import com.gtnewhorizons.angelica.glsm.stacks.CowStateStack;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@@ -38,6 +39,7 @@ public final class StateSet {
public final int restore;
public final int bump;
public final boolean blendSnapshot;
public final DisplayListCommand pushCommand;
StateSet(int glMask, long[] members, int restore, int bump, boolean blendSnapshot) {
this.glMask = glMask;
@@ -45,6 +47,7 @@ public final class StateSet {
this.restore = restore;
this.bump = bump;
this.blendSnapshot = blendSnapshot;
this.pushCommand = () -> GLStateManager.pushStateLive(this);
}
public boolean contains(int id) {
@@ -0,0 +1,305 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import java.util.ArrayList;
import java.util.List;
import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyIsEnabled;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@GLCoreTest
public class GLSM_DisplayList_StateSet_GLTest {
private final List<Integer> listsToDelete = new ArrayList<>();
@AfterEach
void cleanup() {
DisplayListManager.abortCompilation();
while (GLStateManager.getAttribDepth() > 0) {
GLStateManager.popState();
}
for (int id : listsToDelete) {
GLStateManager.glDeleteLists(id, 1);
}
listsToDelete.clear();
GLStateManager.disableBlend();
GLStateManager.disableCull();
GLStateManager.glCullFace(GL11.GL_BACK);
GLStateManager.disableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_ALWAYS, 0.0f);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.disableTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
}
private int newList() {
final int id = GLStateManager.glGenLists(1);
listsToDelete.add(id);
return id;
}
private static Int2IntMap commandCounts(int listId) {
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(listId);
assertNotNull(compiled, "compiled display list must exist for id " + listId);
return compiled.getCommandCounts();
}
@Test
void compileBracketDoesNotThrowAndLeavesLiveStateUntouched() {
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.5f);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final int func0 = GLStateManager.getAlphaState().getFunction();
final float ref0 = GLStateManager.getAlphaState().getReference();
final int unit0 = GLStateManager.getActiveTextureUnit();
final int depth0 = GLStateManager.getAttribDepth();
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
final int d = GLStateManager.pushState(StateSet.CUTOUT);
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.enableTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.popStateTo(d);
GLStateManager.glEndList();
verifyIsEnabled(GL11.GL_ALPHA_TEST, true, "cache alpha test enable unchanged after COMPILE-only bracket");
assertEquals(func0, GLStateManager.getAlphaState().getFunction(), "cache alpha func unchanged after COMPILE-only bracket");
assertEquals(ref0, GLStateManager.getAlphaState().getReference(), 0.0001f, "cache alpha ref unchanged after COMPILE-only bracket");
assertEquals(unit0, GLStateManager.getActiveTextureUnit(), "cache active unit unchanged after COMPILE-only bracket");
assertEquals(depth0, GLStateManager.getAttribDepth(), "live attrib depth unchanged after COMPILE-only bracket");
assertEquals(2, commandCounts(list).getOrDefault(GLCommand.COMPLEX_REF, 0), "push and pop each record one COMPLEX_REF");
}
@Test
void compiledBracketReplayRestoresState() {
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
final int d = GLStateManager.pushState(StateSet.CUTOUT);
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.enableTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.popStateTo(d);
GLStateManager.glEndList();
GLStateManager.glAlphaFunc(GL11.GL_LESS, 0.9f);
GLStateManager.disableAlphaTest();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final int depth0 = GLStateManager.getAttribDepth();
GLStateManager.glCallList(list);
assertEquals(GL11.GL_LESS, GLStateManager.getAlphaState().getFunction(), "cache alpha func restored by replay");
assertEquals(0.9f, GLStateManager.getAlphaState().getReference(), 0.0001f, "cache alpha ref restored by replay");
verifyIsEnabled(GL11.GL_ALPHA_TEST, false, "cache alpha test restored disabled by replay");
assertEquals(0, GLStateManager.getActiveTextureUnit(), "cache active unit restored by replay");
assertEquals(GL13.GL_TEXTURE0, GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE), "driver active unit restored by replay");
assertEquals(depth0, GLStateManager.getAttribDepth(), "live attrib depth unchanged by balanced replay");
}
@Test
void compileAndExecuteBracketIsBalanced() {
GLStateManager.glAlphaFunc(GL11.GL_LESS, 0.9f);
GLStateManager.disableAlphaTest();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final int depth0 = GLStateManager.getAttribDepth();
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE);
final int d = GLStateManager.pushState(StateSet.CUTOUT);
GLStateManager.enableAlphaTest();
GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f);
GLStateManager.glActiveTexture(GL13.GL_TEXTURE1);
GLStateManager.enableTexture();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
GLStateManager.popStateTo(d);
GLStateManager.glEndList();
assertEquals(GL11.GL_LESS, GLStateManager.getAlphaState().getFunction(), "cache alpha func restored after GL_COMPILE_AND_EXECUTE bracket");
assertEquals(0.9f, GLStateManager.getAlphaState().getReference(), 0.0001f, "cache alpha ref restored after GL_COMPILE_AND_EXECUTE bracket");
verifyIsEnabled(GL11.GL_ALPHA_TEST, false, "cache alpha test restored disabled after GL_COMPILE_AND_EXECUTE bracket");
assertEquals(0, GLStateManager.getActiveTextureUnit(), "cache active unit restored after GL_COMPILE_AND_EXECUTE bracket");
assertEquals(depth0, GLStateManager.getAttribDepth(), "live attrib depth unchanged after balanced GL_COMPILE_AND_EXECUTE bracket");
GLStateManager.glCallList(list);
GLStateManager.glCallList(list);
assertEquals(GL11.GL_LESS, GLStateManager.getAlphaState().getFunction(), "cache alpha func restored after replay");
assertEquals(0.9f, GLStateManager.getAlphaState().getReference(), 0.0001f, "cache alpha ref restored after replay");
verifyIsEnabled(GL11.GL_ALPHA_TEST, false, "cache alpha test restored disabled after replay");
assertEquals(0, GLStateManager.getActiveTextureUnit(), "cache active unit restored after replay");
assertEquals(depth0, GLStateManager.getAttribDepth(), "live attrib depth unchanged after replay");
}
@Test
void nestedListsPushInBothLevels() {
GLStateManager.enableBlend();
GLStateManager.enableCull();
GLStateManager.glCullFace(GL11.GL_BACK);
final boolean cull0 = GLStateManager.glIsEnabled(GL11.GL_CULL_FACE);
final int depth0 = GLStateManager.getAttribDepth();
final int parent = newList();
final int child = newList();
GLStateManager.glNewList(parent, GL11.GL_COMPILE);
final int dParent = GLStateManager.pushState(StateSet.BLEND);
GLStateManager.glNewList(child, GL11.GL_COMPILE);
final int dChild = GLStateManager.pushState(StateSet.CULL);
if (cull0) {
GLStateManager.disableCull();
} else {
GLStateManager.enableCull();
}
GLStateManager.popStateTo(dChild);
GLStateManager.glEndList();
GLStateManager.disableBlend();
GLStateManager.popStateTo(dParent);
GLStateManager.glEndList();
assertEquals(depth0, GLStateManager.getAttribDepth(), "live attrib depth unchanged by nested COMPILE-only bracket");
GLStateManager.glCallList(parent);
verifyIsEnabled(GL11.GL_BLEND, true, "blend enable restored by the parent BLEND bracket after parent replay");
verifyIsEnabled(GL11.GL_CULL_FACE, cull0, "cull enable restored after parent replay");
GLStateManager.glCallList(child);
verifyIsEnabled(GL11.GL_CULL_FACE, cull0, "cull enable restored after child replay");
}
@Test
void nestedCompileAndExecuteChildInsideCompileParent() {
GLStateManager.enableCull();
GLStateManager.glCullFace(GL11.GL_BACK);
final boolean cull0 = GLStateManager.glIsEnabled(GL11.GL_CULL_FACE);
final int depth0 = GLStateManager.getAttribDepth();
final int parent = newList();
final int child = newList();
GLStateManager.glNewList(parent, GL11.GL_COMPILE);
final int dParent = GLStateManager.pushState(StateSet.BLEND);
GLStateManager.glNewList(child, GL11.GL_COMPILE_AND_EXECUTE);
final int dChild = GLStateManager.pushState(StateSet.CULL);
if (cull0) {
GLStateManager.disableCull();
} else {
GLStateManager.enableCull();
}
GLStateManager.popStateTo(dChild);
GLStateManager.glEndList();
verifyIsEnabled(GL11.GL_CULL_FACE, cull0, "cull enable restored live immediately after the COMPILE_AND_EXECUTE child bracket closes");
GLStateManager.popStateTo(dParent);
GLStateManager.glEndList();
assertEquals(depth0, GLStateManager.getAttribDepth(), "live attrib depth unchanged after balanced nested bracket");
assertEquals(2, commandCounts(parent).getOrDefault(GLCommand.COMPLEX_REF, 0), "parent list records exactly the BLEND push and pop");
assertEquals(2, commandCounts(child).getOrDefault(GLCommand.COMPLEX_REF, 0), "child list records exactly the CULL push and pop");
}
@Test
void unbalancedBracketIsClosedAtEndList() {
GLStateManager.enableBlend();
final boolean blend0 = GLStateManager.glIsEnabled(GL11.GL_BLEND);
final int depth0 = GLStateManager.getAttribDepth();
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.pushState(StateSet.BATCH);
GLStateManager.disableBlend();
GLStateManager.glEndList();
assertEquals(2, commandCounts(list).getOrDefault(GLCommand.COMPLEX_REF, 0), "glEndList closes the unbalanced push, recording a matching pop");
GLStateManager.glCallList(list);
assertEquals(depth0, GLStateManager.getAttribDepth(), "live attrib depth unchanged after replaying an auto-closed bracket");
verifyIsEnabled(GL11.GL_BLEND, blend0, "blend enable unchanged after replaying an auto-closed bracket");
final int other = newList();
assertDoesNotThrow(() -> {
GLStateManager.glNewList(other, GL11.GL_COMPILE);
GLStateManager.glEndList();
}, "an unrelated list must compile normally after an auto-closed bracket");
}
@Test
void glPushAttribRecordsExactlyOnce() {
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT);
GLStateManager.glPopAttrib();
GLStateManager.glEndList();
final Int2IntMap counts = commandCounts(list);
assertEquals(1, counts.getOrDefault(GLCommand.PUSH_ATTRIB, 0), "glPushAttrib records exactly one PUSH_ATTRIB opcode");
assertEquals(1, counts.getOrDefault(GLCommand.POP_ATTRIB, 0), "glPopAttrib records exactly one POP_ATTRIB opcode");
assertEquals(0, counts.getOrDefault(GLCommand.COMPLEX_REF, 0), "glPushAttrib/glPopAttrib never record a COMPLEX_REF");
}
@Test
void livePushStraddlesOpenList() {
GLStateManager.enableBlend();
final int d = GLStateManager.pushState(StateSet.BLEND);
GLStateManager.disableBlend();
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.glEndList();
GLStateManager.popStateTo(d);
assertEquals(d, GLStateManager.getAttribDepth(), "live attrib depth restored to the pre-bracket value");
verifyIsEnabled(GL11.GL_BLEND, true, "blend enable restored by the live pop that straddled the open-then-closed list");
}
@Test
void livePopInsideOpenListPopsLiveAndRecordsNothing() {
GLStateManager.enableBlend();
final int d = GLStateManager.pushState(StateSet.BLEND);
GLStateManager.disableBlend();
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
GLStateManager.popStateTo(d);
GLStateManager.glEndList();
assertEquals(d, GLStateManager.getAttribDepth(), "live attrib depth restored by the live pop performed inside the open list");
verifyIsEnabled(GL11.GL_BLEND, true, "blend enable restored live by the pop of a pre-list push");
assertEquals(0, commandCounts(list).getOrDefault(GLCommand.COMPLEX_REF, 0), "popping a pre-existing live push records nothing into the list");
}
@Test
void virtualDepthOverflowThrowsAtRecordTime() {
final int startDepth = GLStateManager.getAttribDepth();
final int list = newList();
GLStateManager.glNewList(list, GL11.GL_COMPILE);
final int pushes = GLStateManager.MAX_ATTRIB_STACK_DEPTH - startDepth;
for (int i = 0; i < pushes; i++) {
GLStateManager.pushState(StateSet.CUTOUT);
}
assertThrows(IllegalStateException.class, () -> GLStateManager.pushState(StateSet.CUTOUT), "virtual depth overflow beyond MAX_ATTRIB_STACK_DEPTH must throw at record time");
DisplayListManager.abortCompilation();
assertEquals(startDepth, GLStateManager.getAttribDepth(), "aborting the overflowed compilation leaves live attrib depth untouched");
}
}
@@ -495,22 +495,6 @@ public class GLSM_StateSet_GLTest {
}
}
@Test
void internalPushDuringDisplayListCompileThrowsAndLeavesDepthUnchanged() {
final int list = GLStateManager.glGenLists(1);
final int depthBefore = GLStateManager.getAttribDepth();
try {
GLStateManager.glNewList(list, GL11.GL_COMPILE);
assertThrows(IllegalStateException.class, () -> GLStateManager.pushState(StateSet.BLEND));
GLStateManager.pushState(StateSet.forMask(GL11.GL_COLOR_BUFFER_BIT));
GLStateManager.popState();
assertEquals(depthBefore, GLStateManager.getAttribDepth(), "mask-based push/pop is recorded, not executed, and leaves live depth unchanged");
} finally {
GLStateManager.glEndList();
GLStateManager.glDeleteLists(list, 1);
}
}
@Test
void allAttribBitsNestedInsideInternalBracketRestoresBothLevels() {
try {