diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/DisplayListManager.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/DisplayListManager.java index 14ed04bb6..40ffd6200 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/DisplayListManager.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/DisplayListManager.java @@ -133,7 +133,10 @@ public class DisplayListManager { } public static UnsupportedOperationException unsupportedInList(String what) { - return new UnsupportedOperationException(what + " in display lists not yet implemented - if you see this, please report! " + describeCurrentCompilation()); + final UnsupportedOperationException e = new UnsupportedOperationException( what + " in display lists not yet implemented - if you see this, please report! " + describeCurrentCompilation()); + GLStateManager.warnOnce("unsupported:" + what, "Aborting display list {} compilation: {} unsupported during recording", glListId, what); + abortCompilation(); + return e; } public static String describeCurrentCompilation() { @@ -798,7 +801,8 @@ public class DisplayListManager { */ public static void glEndList() { if (glListMode == 0) { - throw new RuntimeException("glEndList called outside of a display list!"); + GLStateManager.warnOnce("endlist-outside", "glEndList called outside of a display list!"); + return; } try { finishCurrentList(); @@ -906,17 +910,56 @@ public class DisplayListManager { // Parent's COMPILING callback is now active again } else { // Not nested - clear all compilation state - currentRecorder = null; - recordingThread = null; - accumulatedDraws = null; - pendingDraw = null; - transformCallback = null; - compilationStackTrace = null; - pendingTransformOps = null; - multMatrixSources = null; - drawRangeSources = null; - glListId = -1; - glListMode = 0; + resetCompilationState(); + } + } + + /** Clear all root-level compilation state back to "not recording". */ + private static void resetCompilationState() { + currentRecorder = null; + recordingThread = null; + accumulatedDraws = null; + pendingDraw = null; + transformCallback = null; + compilationStackTrace = null; + pendingTransformOps = null; + multMatrixSources = null; + drawRangeSources = null; + glListId = -1; + glListMode = 0; + } + + public static void abortCompilation() { + if (glListMode == 0 && compilationStack.isEmpty()) { + return; + } + + final int levels = compilationStack.size() + (glListMode != 0 ? 1 : 0); + for (int i = 0; i < levels; i++) { + try { + TessellatorManager.stopCapturingDirect(); + } catch (IllegalStateException ignored) { + break; + } + } + + if (currentRecorder != null) { + currentRecorder.delete(); + } + for (CompilationContext ctx : compilationStack) { + if (ctx.recorder() != null) { + ctx.recorder().delete(); + } + } + compilationStack.clear(); + + resetCompilationState(); + } + + public static void abortIfLeaked() { + if (glListMode != 0 || !compilationStack.isEmpty()) { + GLStateManager.warnOnce("frame-leak", "Display list {} left open across a frame boundary - aborting to recover", glListId); + abortCompilation(); } } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java index 82cb7c005..fefb11996 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java @@ -95,6 +95,7 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.IntSupplier; import static com.gtnewhorizons.angelica.glsm.Vendor.AMD; @@ -120,6 +121,12 @@ public class GLStateManager { public static final Logger LOGGER = LogManager.getLogger("GLSM"); + private static final Set WARN_ONCE = ConcurrentHashMap.newKeySet(); + + public static void warnOnce(String key, String fmt, Object... args) { + if (WARN_ONCE.add(key)) LOGGER.warn(fmt, args); + } + // Thread Checking - must be early in static init order so isMainThread() works for state initialization @Getter private static final Thread MainThread = Thread.currentThread(); @@ -2248,70 +2255,99 @@ public class GLStateManager { } public static void glDrawElements(int mode, ByteBuffer indices) { - if (DisplayListManager.isRecording()) { - throw DisplayListManager.unsupportedInList("glDrawElements"); - } - if (FeedbackManager.isFeedbackMode()) { + CommandRecorder savedRecorder = null; + final RecordMode recordMode = DisplayListManager.getRecordMode(); + if (recordMode != RecordMode.NONE) { + final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, indices.remaining(), GL11.GL_UNSIGNED_BYTE, MemoryUtilities.memAddress(indices), indices.remaining()); + if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture); + if (recordMode == RecordMode.COMPILE) return; + savedRecorder = DisplayListManager.pauseRecording(); + } else if (FeedbackManager.isFeedbackMode()) { FeedbackManager.processDrawElements(mode, indices); return; } - if (mode == GL11.GL_QUADS) { - QuadConverter.drawQuadElementsAsTriangles(indices.remaining(), GL11.GL_UNSIGNED_BYTE, indices); - return; + try { + if (mode == GL11.GL_QUADS) { + QuadConverter.drawQuadElementsAsTriangles(indices.remaining(), GL11.GL_UNSIGNED_BYTE, indices); + return; + } + preDraw(mode); + RENDER_BACKEND.drawElements(mode, indices); + } finally { + if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder); } - preDraw(mode); - RENDER_BACKEND.drawElements(mode, indices); } public static void glDrawElements(int mode, IntBuffer indices) { - if (DisplayListManager.isRecording()) { - throw DisplayListManager.unsupportedInList("glDrawElements"); - } - if (FeedbackManager.isFeedbackMode()) { + CommandRecorder savedRecorder = null; + final RecordMode recordMode = DisplayListManager.getRecordMode(); + if (recordMode != RecordMode.NONE) { + final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, indices.remaining(), GL11.GL_UNSIGNED_INT, MemoryUtilities.memAddress(indices), (long) indices.remaining() << 2); + if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture); + if (recordMode == RecordMode.COMPILE) return; + savedRecorder = DisplayListManager.pauseRecording(); + } else if (FeedbackManager.isFeedbackMode()) { FeedbackManager.processDrawElements(mode, indices); return; } - if (mode == GL11.GL_QUADS) { - QuadConverter.drawQuadElementsAsTriangles(indices); - return; + try { + if (mode == GL11.GL_QUADS) { + QuadConverter.drawQuadElementsAsTriangles(indices); + return; + } + preDraw(mode); + RENDER_BACKEND.drawElements(mode, indices); + } finally { + if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder); } - - preDraw(mode); - RENDER_BACKEND.drawElements(mode, indices); } public static void glDrawElements(int mode, ShortBuffer indices) { - if (DisplayListManager.isRecording()) { - throw DisplayListManager.unsupportedInList("glDrawElements"); - } - if (FeedbackManager.isFeedbackMode()) { + CommandRecorder savedRecorder = null; + final RecordMode recordMode = DisplayListManager.getRecordMode(); + if (recordMode != RecordMode.NONE) { + final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, indices.remaining(), GL11.GL_UNSIGNED_SHORT, MemoryUtilities.memAddress(indices), (long) indices.remaining() << 1); + if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture); + if (recordMode == RecordMode.COMPILE) return; + savedRecorder = DisplayListManager.pauseRecording(); + } else if (FeedbackManager.isFeedbackMode()) { FeedbackManager.processDrawElements(mode, indices); return; } - if (mode == GL11.GL_QUADS) { - QuadConverter.drawQuadElementsAsTriangles(indices); - return; + try { + if (mode == GL11.GL_QUADS) { + QuadConverter.drawQuadElementsAsTriangles(indices); + return; + } + preDraw(mode); + RENDER_BACKEND.drawElements(mode, indices); + } finally { + if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder); } - - preDraw(mode); - RENDER_BACKEND.drawElements(mode, indices); } public static void glDrawElements(int mode, int count, int type, ByteBuffer indices) { - if (DisplayListManager.isRecording()) { - throw DisplayListManager.unsupportedInList("glDrawElements"); - } - if (FeedbackManager.isFeedbackMode()) { + CommandRecorder savedRecorder = null; + final RecordMode recordMode = DisplayListManager.getRecordMode(); + if (recordMode != RecordMode.NONE) { + final IndexedDrawCapture capture = IndexedDrawCapture.createFromClientIndices(mode, count, type, MemoryUtilities.memAddress(indices), indices.remaining()); + if (capture != null) DisplayListManager.recordIndexedDrawCapture(capture); + if (recordMode == RecordMode.COMPILE) return; + savedRecorder = DisplayListManager.pauseRecording(); + } else if (FeedbackManager.isFeedbackMode()) { FeedbackManager.processDrawElements(mode, count, type, indices); return; } - if (mode == GL11.GL_QUADS) { - QuadConverter.drawQuadElementsAsTriangles(count, type, indices); - return; + try { + if (mode == GL11.GL_QUADS) { + QuadConverter.drawQuadElementsAsTriangles(count, type, indices); + return; + } + preDraw(mode); + RENDER_BACKEND.drawElements(mode, count, type, indices); + } finally { + if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder); } - - preDraw(mode); - RENDER_BACKEND.drawElements(mode, count, type, indices); } public static void glDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) { @@ -2332,14 +2368,16 @@ public class GLStateManager { FeedbackManager.processDrawElements(mode, indices_count, type, indices_buffer_offset); return; } - if (mode == GL11.GL_QUADS) { - QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset); - return; + try { + if (mode == GL11.GL_QUADS) { + QuadConverter.drawQuadElementsAsTriangles(indices_count, type, indices_buffer_offset); + return; + } + preDraw(mode); + RENDER_BACKEND.drawElements(mode, indices_count, type, indices_buffer_offset); + } finally { + if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder); } - - preDraw(mode); - RENDER_BACKEND.drawElements(mode, indices_count, type, indices_buffer_offset); - if (savedRecorder != null) DisplayListManager.resumeRecording(savedRecorder); } public static void glDrawBuffer(int mode) { diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/IndexedDrawCapture.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/IndexedDrawCapture.java index ca413b79d..495a6f56f 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/IndexedDrawCapture.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/IndexedDrawCapture.java @@ -9,8 +9,6 @@ import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL15; import java.nio.ByteBuffer; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt; import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetLong; @@ -52,20 +50,14 @@ public final class IndexedDrawCapture { return freed; } - private static final Set 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); + GLStateManager.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); + GLStateManager.warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 — skipping", indicesCount); return null; } final int srcIndexSize = VAOManager.Attrib.glTypeSizeBytes(srcIndexType); @@ -74,29 +66,53 @@ public final class IndexedDrawCapture { final int prevEBO = GLStateManager.getBoundEBO(); final int prevVBO = GLStateManager.getBoundVBO(); ByteBuffer eboSrc = null; + 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) { + GLStateManager.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) { + GLStateManager.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); + return bake(mode, indicesCount, srcIndexType, MemoryUtilities.memAddress0(eboSrc)); + } finally { + if (eboSrc != null) MemoryUtilities.memFree(eboSrc); + GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, prevEBO); + GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, prevVBO); + } + } + + public static IndexedDrawCapture createFromClientIndices(int mode, int indicesCount, int srcIndexType, long indexSrcAddr, long srcByteLength) { + if (indicesCount == 0) return null; + if (indexSrcAddr == 0L) return null; + if (mode == GL11.GL_QUADS && (indicesCount % 4) != 0) { + GLStateManager.warnOnce("quads-misaligned", "[IndexedDrawCapture] GL_QUADS with indicesCount={} not a multiple of 4 - skipping", indicesCount); + return null; + } + final int idxSize = VAOManager.Attrib.glTypeSizeBytes(srcIndexType); + if (idxSize <= 0) return null; + if ((long) indicesCount * idxSize > srcByteLength) { + GLStateManager.warnOnce("client-idx-range", "[IndexedDrawCapture] index count {} ({} bytes) exceeds client buffer {} bytes - skipping", indicesCount, (long) indicesCount * idxSize, srcByteLength); + return null; + } + return bake(mode, indicesCount, srcIndexType, indexSrcAddr); + } + + private static IndexedDrawCapture bake(int mode, int indicesCount, int srcIndexType, long indexSrcAddr) { 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); + final long minMax = QuadConverter.scanMinMaxIndex(indexSrcAddr, srcIndexType, indicesCount); if (minMax == -1L) { - warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType); + GLStateManager.warnOnce("min-max", "[IndexedDrawCapture] failed to determine min/max vertex for indexType={}", srcIndexType); return null; } final int minVtx = (int) (minMax & 0xFFFFFFFFL); @@ -110,7 +126,7 @@ public final class IndexedDrawCapture { if (snap.get(i) != null) enabledCount++; } if (enabledCount == 0) { - warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping"); + GLStateManager.warnOnce("no-attribs", "[IndexedDrawCapture] no enabled attributes — skipping"); return null; } final int[] locations = new int[enabledCount]; @@ -137,6 +153,13 @@ public final class IndexedDrawCapture { for (int a = 0; a < enabledCount; a++) { final AttribSnapshot.AttribDesc d = snap.get(locations[a]); final int relOffset = (int) (d.offset() - d.readBufferBaseOffset()); + + final long readStart = relOffset + (long) minVtx * d.effectiveStride(); + final long readEnd = relOffset + (long) maxVtx * d.effectiveStride() + (long) d.size() * d.typeSizeBytes(); + if (readStart < 0 || readEnd > d.readBuffer().capacity()) { + GLStateManager.warnOnce("vtx-oob", "[IndexedDrawCapture] attrib {} read extent {} exceeds snapshot {} bytes - skipping", d.location(), readEnd, d.readBuffer().capacity()); + return null; + } if (a == 0) { sharedSrcBuffer = d.readBuffer(); sharedSrcStride = d.effectiveStride(); @@ -169,12 +192,12 @@ public final class IndexedDrawCapture { 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); + QuadConverter.triangulateQuads(indexSrcAddr, 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); + QuadConverter.widenIndices(indexSrcAddr, srcIndexType, MemoryUtilities.memAddress0(indexData), GL11.GL_UNSIGNED_INT, indicesCount, minVtx); bakedDrawMode = mode; } @@ -184,14 +207,11 @@ public final class IndexedDrawCapture { 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); } } diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/DisplayListCompilationGLSMTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/DisplayListCompilationGLSMTest.java index 05c92f1c1..8da004c5b 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/DisplayListCompilationGLSMTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/DisplayListCompilationGLSMTest.java @@ -9,9 +9,11 @@ import org.lwjgl.opengl.GL11; import java.nio.FloatBuffer; +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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** Tests GLSM display list behavior matches OpenGL spec. */ @@ -257,4 +259,30 @@ class DisplayListCompilationGLSMTest { GLStateManager.glEndList(); } + + @Test + void glsm_unsupportedOpInList_abortsCleanly() { + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + assertTrue(DisplayListManager.isRecording(), "recording after glNewList"); + + assertThrows(UnsupportedOperationException.class, () -> GLStateManager.glReadBuffer(GL11.GL_FRONT)); + + assertFalse(DisplayListManager.isRecording(), "recording state must be reset after an in-list throw"); + assertEquals(DisplayListManager.RecordMode.NONE, DisplayListManager.getRecordMode()); + + assertDoesNotThrow(() -> GLStateManager.glReadBuffer(GL11.GL_FRONT)); + assertDoesNotThrow(GLStateManager::glEndList); + } + + @Test + void glsm_abortIfLeaked_recoversOpenList() { + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + assertTrue(DisplayListManager.isRecording()); + + DisplayListManager.abortIfLeaked(); + + assertFalse(DisplayListManager.isRecording(), "frame-begin backstop must clear a leaked open list"); + } } diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/recording/IndexedDrawBatchBuilderTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/recording/IndexedDrawBatchBuilderTest.java index dfe724473..5f3895512 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/recording/IndexedDrawBatchBuilderTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/recording/IndexedDrawBatchBuilderTest.java @@ -9,6 +9,9 @@ import com.gtnewhorizons.angelica.glsm.recording.commands.IndexedDrawBatch; import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; @@ -17,7 +20,10 @@ import org.lwjgl.opengl.GL30; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; import java.util.List; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -25,6 +31,7 @@ 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; +import static org.junit.jupiter.params.provider.Arguments.arguments; class IndexedDrawBatchBuilderTest extends DisplayListTestFixture { @@ -521,7 +528,153 @@ class IndexedDrawBatchBuilderTest extends DisplayListTestFixture { "listBA: batch[1] must match the second-recorded layout"); } - // === local helpers === + private static Stream clientIndexedDrawCases() { + return Stream.of( + arguments("byte indices", GL11.GL_COMPILE, (Runnable) () -> { + final ByteBuffer idx = BufferUtils.createByteBuffer(3); + idx.put(new byte[]{0, 1, 2}).flip(); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx); + }, GL11.GL_TRIANGLES, 3), + arguments("short indices", GL11.GL_COMPILE, (Runnable) () -> { + final ShortBuffer idx = BufferUtils.createShortBuffer(3); + idx.put(new short[]{0, 1, 2}).flip(); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx); + }, GL11.GL_TRIANGLES, 3), + arguments("int indices", GL11.GL_COMPILE, (Runnable) () -> { + final IntBuffer idx = BufferUtils.createIntBuffer(3); + idx.put(new int[]{0, 1, 2}).flip(); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx); + }, GL11.GL_TRIANGLES, 3), + arguments("explicit count+type", GL11.GL_COMPILE, (Runnable) () -> { + final ByteBuffer idx = BufferUtils.createByteBuffer(3 * 2).order(ByteOrder.nativeOrder()); + idx.putShort((short) 0).putShort((short) 1).putShort((short) 2).flip(); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, idx); + }, GL11.GL_TRIANGLES, 3), + arguments("quads triangulate at record", GL11.GL_COMPILE, (Runnable) () -> { + final IntBuffer idx = BufferUtils.createIntBuffer(4); + idx.put(new int[]{0, 1, 2, 3}).flip(); + GLStateManager.glDrawElements(GL11.GL_QUADS, idx); + }, GL11.GL_TRIANGLES, 6), + arguments("compile-and-execute fires live and bakes", GL11.GL_COMPILE_AND_EXECUTE, (Runnable) () -> { + final IntBuffer idx = BufferUtils.createIntBuffer(3); + idx.put(new int[]{0, 1, 2}).flip(); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx); + }, GL11.GL_TRIANGLES, 3) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("clientIndexedDrawCases") + void clientIndexedDraw_capturedAndWidenedToUnsignedInt(String name, int listMode, Runnable draw, int expectedMode, int expectedCount) { + final int list = recordClientIndexDraw(listMode, draw); + final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list); + assertNotNull(cmd); + assertEquals(expectedMode, cmd.getDrawMode()); + assertEquals(expectedCount, cmd.getIndexCount()); + assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType(), "client indices always widen to UNSIGNED_INT"); + } + + @Test + void clientVerticesAndIndices_pointersSetDuringRecording_bake() { + final float[][] verts = {{0f, 0f, 0f}, {1f, 0f, 0f}, {1f, 1f, 0f}, {0f, 1f, 0f}}; + final ByteBuffer clientVerts = BufferUtils.createByteBuffer(verts.length * 12).order(ByteOrder.nativeOrder()); + for (float[] v : verts) { + clientVerts.putFloat(v[0]); + clientVerts.putFloat(v[1]); + clientVerts.putFloat(v[2]); + } + clientVerts.flip(); + final IntBuffer indices = BufferUtils.createIntBuffer(6); + indices.put(new int[]{0, 1, 2, 2, 3, 0}).flip(); + + final int vao = newVao(); + final int list = newList(); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glBindVertexArray(vao); + GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + + VAOManager.setAttribute(0, 3, GL11.GL_FLOAT, false, 0, clientVerts); + VAOManager.enableAttribute(0); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, indices); + GLStateManager.glBindVertexArray(0); + GLStateManager.glEndList(); + + final BatchedIndexedDrawCmd cmd = firstBatchedCmd(list); + assertNotNull(cmd); + assertEquals(GL11.GL_TRIANGLES, cmd.getDrawMode()); + assertEquals(6, cmd.getIndexCount()); + assertEquals(GL11.GL_UNSIGNED_INT, cmd.getIndexType()); + + final ByteBuffer owned = readBufferBytes(GL15.GL_ARRAY_BUFFER, firstBatch(list).getSharedVBO(), verts.length * 12); + for (int i = 0; i < verts.length; i++) { + assertEquals(verts[i][0], owned.getFloat(i * 12), 0f, "pos.x v=" + i); + assertEquals(verts[i][1], owned.getFloat(i * 12 + 4), 0f, "pos.y v=" + i); + assertEquals(verts[i][2], owned.getFloat(i * 12 + 8), 0f, "pos.z v=" + i); + } + MemoryUtilities.memFree(owned); + } + + @Test + void quadOffsetDraw_compileAndExecute_doesNotPauseRecordingForRestOfList() { + final int vao = setupPosColorVao(defaultQuadVbo()); + final int quadEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3); + final int triEbo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2); + + final int list = newList(); + GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE); + GLStateManager.glBindVertexArray(vao); + GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, quadEbo); + GLStateManager.glDrawElements(GL11.GL_QUADS, 4, GL11.GL_UNSIGNED_SHORT, 0L); + assertTrue(DisplayListManager.isRecording(), "recording must resume after a GL_QUADS offset draw"); + GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, triEbo); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 3, GL11.GL_UNSIGNED_SHORT, 0L); + GLStateManager.glBindVertexArray(0); + GLStateManager.glEndList(); + + assertEquals(2, allBatchedCmds(list).size(), "both the QUADS draw and the following triangle draw must be recorded"); + } + + private enum MalformedClientDraw { OVERSIZED_COUNT, INDEX_PAST_VERTEX_BUFFER } + + @ParameterizedTest(name = "{0}") + @EnumSource(MalformedClientDraw.class) + void malformedClientDraw_skippedAndRecordingSurvives(MalformedClientDraw scenario) { + final int list = newList(); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); + switch (scenario) { + case OVERSIZED_COUNT -> { + GLStateManager.glBindVertexArray(setupPosColorVao(defaultQuadVbo())); + final ByteBuffer idx = BufferUtils.createByteBuffer(3 * 2).order(ByteOrder.nativeOrder()); + idx.putShort((short) 0).putShort((short) 1).putShort((short) 2).flip(); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, 100, GL11.GL_UNSIGNED_SHORT, idx); + } + case INDEX_PAST_VERTEX_BUFFER -> { + final int vbo = uploadPosColorVbo(new float[]{0f, 0f, 1f, 0f}, new byte[][]{{1, 1, 1, 1}, {2, 2, 2, 2}}); + GLStateManager.glBindVertexArray(setupPosColorVao(vbo)); + final IntBuffer idx = BufferUtils.createIntBuffer(4); + idx.put(new int[]{0, 1, 2, 3}).flip(); + GLStateManager.glDrawElements(GL11.GL_TRIANGLES, idx); + } + } + assertTrue(DisplayListManager.isRecording(), "recording survives a skipped malformed draw"); + GLStateManager.glBindVertexArray(0); + GLStateManager.glEndList(); + + assertNull(firstBatchedCmd(list), "malformed draw must be skipped, not captured or over-read"); + } + + private int recordClientIndexDraw(int listMode, Runnable draw) { + final int vao = setupPosColorVao(defaultQuadVbo()); + final int list = newList(); + GLStateManager.glNewList(list, listMode); + GLStateManager.glBindVertexArray(vao); + GLStateManager.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); + draw.run(); + GLStateManager.glBindVertexArray(0); + GLStateManager.glEndList(); + return list; + } private static void assertUintIndices(ByteBuffer ib, int... expected) { for (int i = 0; i < expected.length; i++) { diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft_FrameHook.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft_FrameHook.java index 73f8d7ea6..719c27f6c 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft_FrameHook.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft_FrameHook.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.mixins.early.angelica; +import com.gtnewhorizons.angelica.glsm.DisplayListManager; import com.gtnewhorizons.angelica.glsm.backend.BackendManager; import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND; import net.minecraft.client.Minecraft; @@ -20,6 +21,7 @@ public class MixinMinecraft_FrameHook { @Inject(method = "runGameLoop", at = @At("HEAD")) private void angelica$onFrameBegin(CallbackInfo ci) { + DisplayListManager.abortIfLeaked(); RENDER_BACKEND.onFrameBegin(); }