diff --git a/build.gradle.kts b/build.gradle.kts index 7de56bc9d..19114a9bf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,8 +13,14 @@ tasks.test { testLogging { events("passed", "skipped", "failed") } + // On macOS ARM64, use an ARM64-compatible JVM for tests + val isMacOsArm64 = System.getProperty("os.name").lowercase().contains("mac") && + System.getProperty("os.arch") == "aarch64" javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(8) + if (isMacOsArm64) { + vendor = JvmVendorSpec.AZUL + } } dependsOn(tasks.extractNatives2) jvmArgs("-Djava.library.path=${tasks.extractNatives2.get().destinationFolder.asFile.get().path}") diff --git a/dependencies.gradle b/dependencies.gradle index 03e1f5dc0..bb0bc2149 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -58,7 +58,7 @@ dependencies { // Iris Shaders compileOnly('org.jetbrains:annotations:26.0.2') - api("com.github.GTNewHorizons:GTNHLib:0.8.6:dev") + api("com.github.GTNewHorizons:GTNHLib:0.8.18:dev") shadowImplementation("org.anarres:jcpp:1.4.14") // Apache 2.0 shadowImplementation("org.taumc:glsl-transformation-lib:0.2.0-4.g6b42bca") { exclude module: "antlr4" // we only want to shadow the runtime module diff --git a/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java b/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java index 92e6384cc..36ec511c5 100644 --- a/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java +++ b/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java @@ -18,7 +18,7 @@ import cpw.mods.fml.common.event.FMLPreInitializationEvent; name = "Angelica", version = Tags.VERSION, dependencies = """ - required-after:gtnhlib@[0.6.6,);\ + required-after:gtnhlib@[0.8.18,);\ before:lwjgl3ify@[1.5.3,);\ after:hodgepodge@[2.4.4,);\ after:CodeChickenCore@[1.2.0,);\ diff --git a/src/main/java/com/gtnewhorizons/angelica/config/AngelicaConfig.java b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaConfig.java index b59954e14..abf9d1d15 100644 --- a/src/main/java/com/gtnewhorizons/angelica/config/AngelicaConfig.java +++ b/src/main/java/com/gtnewhorizons/angelica/config/AngelicaConfig.java @@ -17,7 +17,7 @@ public class AngelicaConfig { @Config.DefaultBoolean(true) public static boolean enableReesesSodiumOptions; - @Config.Comment("Enable Sodium fluid rendering") + @Config.Comment("Enable Sodium fluid rendering [Experimental]") @Config.DefaultBoolean(false) @Config.RequiresMcRestart public static boolean enableSodiumFluidRendering; @@ -42,10 +42,10 @@ public class AngelicaConfig { @Config.RequiresMcRestart public static boolean enableMCPatcherForgeFeatures; - @Config.Comment("Replace some vanilla render paths with more optimized versions. Disable if you encounter mixin conflicts.") + @Config.Comment("Replace cloud renderer with a VBO version.") @Config.DefaultBoolean(true) @Config.RequiresMcRestart - public static boolean enableVBO; + public static boolean enableVBOClouds; @Config.Comment("Enable NotFine features") @Config.DefaultBoolean(true) diff --git a/src/main/java/com/gtnewhorizons/angelica/dynamiclights/DynamicLights.java b/src/main/java/com/gtnewhorizons/angelica/dynamiclights/DynamicLights.java index 8a7b37f9b..27a33dab8 100644 --- a/src/main/java/com/gtnewhorizons/angelica/dynamiclights/DynamicLights.java +++ b/src/main/java/com/gtnewhorizons/angelica/dynamiclights/DynamicLights.java @@ -37,6 +37,8 @@ public class DynamicLights { public static DynamicLightsMode Mode = DynamicLightsMode.OFF; public static boolean ShaderForce = false; + public static final boolean configEnabled = AngelicaConfig.enableDynamicLights; + private static final double MAX_RADIUS = 7.75; private static final double MAX_RADIUS_SQUARED = MAX_RADIUS * MAX_RADIUS; private final Set dynamicLightSources = new ObjectOpenHashSet<>(); @@ -51,7 +53,7 @@ public class DynamicLights { } public static boolean isEnabled() { - return AngelicaConfig.enableDynamicLights && + return configEnabled && Mode.isEnabled() && // if shader force is enabled then true // if not, then true when shaders are not in use diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/CommandBufferBuilder.java b/src/main/java/com/gtnewhorizons/angelica/glsm/CommandBufferBuilder.java new file mode 100644 index 000000000..caaa39931 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/CommandBufferBuilder.java @@ -0,0 +1,172 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw; +import com.gtnewhorizons.angelica.glsm.recording.CommandBuffer; +import com.gtnewhorizons.angelica.glsm.recording.CommandBufferProcessor; +import com.gtnewhorizons.angelica.glsm.recording.CommandBufferProcessor.BufferTransformOptimizer; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Builds final command buffers from raw command buffers and compiled VBO data. + * Bridges the gap between VBO compilation (glsm types) and buffer processing (recording types). + */ +public final class CommandBufferBuilder { + + private CommandBufferBuilder() {} + + /** + * Build commands with transform collapsing optimization. + * Accumulates MODELVIEW transforms and emits MultMatrix at barriers (draws, CallList). + * Uses mergedRanges where consecutive same-transform draws are combined. + */ + public static void buildOptimizedFromRawBuffer( + CommandBuffer rawBuffer, + Map compiledQuadBuffers, + CompiledLineBuffer compiledLineBuffer, + VertexBuffer[] ownedVbos, + int glListId, + CommandBuffer finalBuffer) { + + // Build VBO index map for efficient lookup (no boxing) + final Object2IntMap vboIndexMap = new Object2IntOpenHashMap<>(); + vboIndexMap.defaultReturnValue(-1); + for (int i = 0; i < ownedVbos.length; i++) { + vboIndexMap.put(ownedVbos[i], i); + } + + // Collect all merged draw ranges sorted by command index + final List allRanges = collectDrawRanges(compiledQuadBuffers, compiledLineBuffer); + + // Buffer optimizer state + final BufferTransformOptimizer opt = new BufferTransformOptimizer(); + + rawBuffer.resetRead(); + int cmdIndex = 0; + int rangeIndex = 0; + + while (rawBuffer.hasRemaining()) { + // Emit draw ranges at this command position + while (rangeIndex < allRanges.size() && allRanges.get(rangeIndex).range().commandIndex() == cmdIndex) { + emitDrawRangeToBuffer(allRanges.get(rangeIndex++), opt, finalBuffer, vboIndexMap); + } + + // Read and process the command + final int opcode = rawBuffer.readInt(); + CommandBufferProcessor.processCommandForOptimization(opcode, rawBuffer, opt, finalBuffer); + cmdIndex++; + } + + // Emit remaining draw ranges at end of command stream + while (rangeIndex < allRanges.size()) { + emitDrawRangeToBuffer(allRanges.get(rangeIndex++), opt, finalBuffer, vboIndexMap); + } + + // Emit residual transform if not identity + if (!opt.isIdentity()) { + opt.emitPendingTransform(finalBuffer); + } + } + + /** + * Build commands without transform collapsing. + * Copies state commands directly (no transform optimization), but still uses + * merged draw ranges (draws already collapsed by format + transform in VBO compilation). + */ + public static void buildUnoptimizedFromRawBuffer( + CommandBuffer rawBuffer, + List accumulatedDraws, + Map compiledQuadBuffers, + CompiledLineBuffer compiledLineBuffer, + VertexBuffer[] ownedVbos, + CommandBuffer finalBuffer) { + + // Build VBO index map for efficient lookup (no boxing) + final Object2IntMap vboIndexMap = new Object2IntOpenHashMap<>(); + vboIndexMap.defaultReturnValue(-1); + for (int i = 0; i < ownedVbos.length; i++) { + vboIndexMap.put(ownedVbos[i], i); + } + + // Collect all merged draw ranges sorted by command index + final List allRanges = collectDrawRanges(compiledQuadBuffers, compiledLineBuffer); + + rawBuffer.resetRead(); + int cmdIndex = 0; + int rangeIndex = 0; + + while (rawBuffer.hasRemaining()) { + // Emit draw ranges at this command position + while (rangeIndex < allRanges.size() && allRanges.get(rangeIndex).range().commandIndex() == cmdIndex) { + final DrawRangeWithBuffer drb = allRanges.get(rangeIndex++); + final int vboIdx = vboIndexMap.getInt(drb.vbo()); + if (vboIdx >= 0) { + finalBuffer.writeDrawRange(vboIdx, drb.range().startVertex(), drb.range().vertexCount(), drb.hasBrightness()); + } + } + + // Copy the command directly + CommandBufferProcessor.copyCommand(rawBuffer, finalBuffer); + cmdIndex++; + } + + // Emit remaining draw ranges at end of command stream + while (rangeIndex < allRanges.size()) { + final DrawRangeWithBuffer drb = allRanges.get(rangeIndex++); + final int vboIdx = vboIndexMap.getInt(drb.vbo()); + if (vboIdx >= 0) { + finalBuffer.writeDrawRange(vboIdx, drb.range().startVertex(), drb.range().vertexCount(), drb.hasBrightness()); + } + } + } + + /** + * Collect all draw ranges from compiled buffers, sorted by command index. + */ + private static List collectDrawRanges( + Map compiledQuadBuffers, + CompiledLineBuffer compiledLineBuffer) { + + final List allRanges = new ArrayList<>(); + + for (CompiledFormatBuffer compiledBuffer : compiledQuadBuffers.values()) { + for (DrawRange range : compiledBuffer.mergedRanges()) { + allRanges.add(new DrawRangeWithBuffer(range, compiledBuffer.vbo(), compiledBuffer.flags().hasBrightness)); + } + } + + if (compiledLineBuffer != null) { + for (DrawRange range : compiledLineBuffer.mergedRanges()) { + allRanges.add(new DrawRangeWithBuffer(range, compiledLineBuffer.vbo(), false)); + } + } + + allRanges.sort(Comparator.comparingInt(r -> r.range().commandIndex())); + return allRanges; + } + + /** + * Emit a draw range to the output buffer, with transform if needed. + */ + private static void emitDrawRangeToBuffer( + DrawRangeWithBuffer drb, + BufferTransformOptimizer opt, + CommandBuffer out, + Object2IntMap vboIndexMap) { + // Emit transform to reach the draw's expected transform + opt.emitTransformTo(out, drb.range().transform()); + + // Write the draw range command + final int vboIdx = vboIndexMap.getInt(drb.vbo()); + if (vboIdx >= 0) { + out.writeDrawRange(vboIdx, drb.range().startVertex(), drb.range().vertexCount(), drb.hasBrightness()); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/CompiledFormatBuffer.java b/src/main/java/com/gtnewhorizons/angelica/glsm/CompiledFormatBuffer.java new file mode 100644 index 000000000..596365edf --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/CompiledFormatBuffer.java @@ -0,0 +1,23 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; + +/** + * A compiled VBO with its associated draw ranges. + * All ranges reference vertices within this single VBO. + *

+ * Contains two range arrays: + *

    + *
  • mergedRanges: For optimized path - consecutive same-transform draws merged
  • + *
  • perDrawRanges: For unoptimized path - 1:1 with original draws
  • + *
+ */ +@Desugar +record CompiledFormatBuffer( + VertexBuffer vbo, + CapturingTessellator.Flags flags, + DrawRange[] mergedRanges, + DrawRange[] perDrawRanges +) {} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/CompiledLineBuffer.java b/src/main/java/com/gtnewhorizons/angelica/glsm/CompiledLineBuffer.java new file mode 100644 index 000000000..029fb8cc9 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/CompiledLineBuffer.java @@ -0,0 +1,21 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; + +/** + * A compiled VBO containing all line geometry with associated draw ranges. + * All ranges reference vertices within this single VBO. + *

+ * Contains two range arrays: + *

    + *
  • mergedRanges: For optimized path - consecutive same-transform draws merged
  • + *
  • perDrawRanges: For unoptimized path - 1:1 with original draws
  • + *
+ */ +@Desugar +record CompiledLineBuffer( + VertexBuffer vbo, + DrawRange[] mergedRanges, + DrawRange[] perDrawRanges +) {} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/DisplayListManager.java b/src/main/java/com/gtnewhorizons/angelica/glsm/DisplayListManager.java new file mode 100644 index 000000000..11db348c0 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/DisplayListManager.java @@ -0,0 +1,871 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable; +import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VBOManager; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat; +import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw; +import com.gtnewhorizons.angelica.glsm.recording.AccumulatedLineDraw; +import com.gtnewhorizons.angelica.glsm.recording.CommandBuffer; +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.DisplayListCommand; +import com.gtnewhorizons.angelica.glsm.recording.commands.OptimizationContext; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; +import lombok.experimental.UtilityClass; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.joml.Matrix4f; +import org.joml.Matrix4fStack; +import org.joml.Vector3f; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages display list compilation, caching, and playback. + * Handles VBO-based display list emulation with command recording and batching. + * + *

This class is responsible for: + *

    + *
  • Display list lifecycle (glNewList, glEndList, glCallList, glDeleteLists)
  • + *
  • Command recording and accumulation during compilation
  • + *
  • Building optimized (batched) and unoptimized (nested-safe) command lists
  • + *
  • Transform tracking and collapsing for optimal GL call counts
  • + *
  • Cache management for compiled display lists
  • + *
+ * + *

Architecture: Transform Collapsing

+ *

The optimization strategy collapses consecutive MODELVIEW transforms into single MultMatrix + * commands, emitting them at "barriers" (draws, CallList, exit). This approach:

+ *
    + *
  • Keeps vertices canonical (untransformed) in VBOs
  • + *
  • Properly handles nested display lists (CallList) - GL state is correct before sub-list
  • + *
  • Reduces GL calls (multiple transforms → one MultMatrix)
  • + *
  • Maintains proper Push/Pop stack semantics
  • + *
+ * + *

Transform Tracking State

+ *
    + *
  • accumulated: Total relative transform (what GL matrix should be)
  • + *
  • lastEmitted: What we've emitted (what GL matrix is)
  • + *
  • stack: Saved accumulated values at Push points
  • + *
+ * + *

Batching Strategy

+ *

Format-based batching: All draws with the same vertex format share a single VBO via + * {@link FormatBuffer}. Each draw's position within the VBO is tracked as a {@link DrawRange}. + * Consecutive draws with the same transform are merged into single ranges. Transforms are + * handled via delta-based emission using {@link TransformOptimizer}.

+ * + * @see com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList + * @see com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw + * @see com.gtnewhorizons.angelica.glsm.recording.commands.DrawRangeCmd + */ +@UtilityClass +public class DisplayListManager { + private static final Logger LOGGER = LogManager.getLogger("DisplayListManager"); + + /** + * Debug flag to enable building unoptimized display list commands. + * When false (default), unoptimized commands are not built + * When true, unoptimized commands are built for debugging GL behavior. + * Set via: -Dangelica.debugDisplayLists=true + */ + private static final boolean DEBUG_DISPLAY_LISTS = Boolean.getBoolean("angelica.debugDisplayLists"); + + // Track which display list is currently being rendered + @Getter private static int currentRenderingList = -1; + + /** + * Check if we're currently rendering a display list. + * Used by format overrides to avoid interfering with display list VBOs. + */ + public static boolean isRenderingDisplayList() { + return currentRenderingList != -1; + } + + static boolean isIdentity(Matrix4f m) { + return (m.properties() & Matrix4f.PROPERTY_IDENTITY) != 0; + } + + /** Transform operation types for relative transform tracking. */ + public enum TransformOp { TRANSLATE, SCALE, ROTATE } + + // Display list compilation state (current/active context) + private static int glListMode = 0; + private static int glListId = -1; + private static boolean tessellatorCompiling = false; // Track if we started compiling + 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 accumulatedDraws = null; // Accumulates quad draws for batching + private static List accumulatedLineDraws = null; // Accumulates line draws + private static Matrix4fStack relativeTransform = null; // Tracks relative transforms during compilation (with push/pop support) + @Getter private static ImmediateModeRecorder immediateModeRecorder = null; // Records glBegin/glEnd/glVertex during compilation + + // Nested compilation support - stack of parent contexts + private static final Deque compilationStack = new ArrayDeque<>(); + + // Display list cache + private static final Int2ObjectMap displayListCache = new Int2ObjectOpenHashMap<>(); + + /** + * Compilation context for a single display list being compiled. + * Used to support nested glNewList() calls (spec-violating but needed for mod compatibility). + */ + @Desugar + private record CompilationContext( + int listId, + int listMode, + CommandRecorder recorder, + List draws, + List lineDraws, + Matrix4fStack transform, + boolean wasTessellatorCompiling, + ImmediateModeRecorder immediateRecorder + ) {} + + /** + * Check if the current thread is recording a display list. + * Only returns true for the thread that started recording. + * This prevents other threads from having their GL calls captured. + */ + public static boolean isRecording() { + return currentRecorder != null && Thread.currentThread() == recordingThread; + } + + /** + * Get the ID of the display list currently being recorded. + * @return The list ID, or -1 if not recording + */ + public static int getRecordingListId() { + return glListId; + } + + /** + * Get the current display list mode (GL_COMPILE or GL_COMPILE_AND_EXECUTE). + * @return The current list mode, or 0 if not recording + */ + public static int getListMode() { + return glListMode; + } + + /** + * Get the current command count (for draw position tracking). + * @return The current command position + */ + public static int getCommandCount() { + return currentRecorder != null ? currentRecorder.getCommandCount() : 0; + } + + // ==================== Recording Method Delegates ==================== + // These delegate to the current CommandRecorder during compilation. + + public static void recordEnable(int cap) { if (currentRecorder != null) currentRecorder.recordEnable(cap); } + public static void recordDisable(int cap) { if (currentRecorder != null) currentRecorder.recordDisable(cap); } + public static void recordClear(int mask) { if (currentRecorder != null) currentRecorder.recordClear(mask); } + public static void recordClearColor(float r, float g, float b, float a) { if (currentRecorder != null) currentRecorder.recordClearColor(r, g, b, a); } + public static void recordClearStencil(int s) { if (currentRecorder != null) currentRecorder.recordClearStencil(s); } + public static void recordColor(float r, float g, float b, float a) { if (currentRecorder != null) currentRecorder.recordColor(r, g, b, a); } + public static void recordColorMask(boolean r, boolean g, boolean b, boolean a) { if (currentRecorder != null) currentRecorder.recordColorMask(r, g, b, a); } + public static void recordDepthMask(boolean flag) { if (currentRecorder != null) currentRecorder.recordDepthMask(flag); } + public static void recordDepthFunc(int func) { if (currentRecorder != null) currentRecorder.recordDepthFunc(func); } + public static void recordBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { if (currentRecorder != null) currentRecorder.recordBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); } + public static void recordAlphaFunc(int func, float ref) { if (currentRecorder != null) currentRecorder.recordAlphaFunc(func, ref); } + public static void recordCullFace(int mode) { if (currentRecorder != null) currentRecorder.recordCullFace(mode); } + public static void recordShadeModel(int mode) { if (currentRecorder != null) currentRecorder.recordShadeModel(mode); } + public static void recordBindTexture(int target, int texture) { if (currentRecorder != null) currentRecorder.recordBindTexture(target, texture); } + public static void recordTexParameteri(int target, int pname, int param) { if (currentRecorder != null) currentRecorder.recordTexParameteri(target, pname, param); } + public static void recordTexParameterf(int target, int pname, float param) { if (currentRecorder != null) currentRecorder.recordTexParameterf(target, pname, param); } + public static void recordMatrixMode(int mode) { if (currentRecorder != null) currentRecorder.recordMatrixMode(mode); } + public static void recordLoadIdentity(int matrixMode) { if (currentRecorder != null) currentRecorder.recordLoadIdentity(matrixMode); } + public static void recordPushMatrix(int matrixMode) { if (currentRecorder != null) currentRecorder.recordPushMatrix(matrixMode); } + public static void recordPopMatrix(int matrixMode) { if (currentRecorder != null) currentRecorder.recordPopMatrix(matrixMode); } + public static void recordTranslate(int matrixMode, double x, double y, double z) { if (currentRecorder != null) currentRecorder.recordTranslate(matrixMode, x, y, z); } + public static void recordRotate(int matrixMode, double angle, double x, double y, double z) { if (currentRecorder != null) currentRecorder.recordRotate(matrixMode, angle, x, y, z); } + public static void recordScale(int matrixMode, double x, double y, double z) { if (currentRecorder != null) currentRecorder.recordScale(matrixMode, x, y, z); } + public static void recordMultMatrix(int matrixMode, Matrix4f matrix) { if (currentRecorder != null) currentRecorder.recordMultMatrix(matrixMode, matrix); } + public static void recordLoadMatrix(int matrixMode, Matrix4f matrix) { if (currentRecorder != null) currentRecorder.recordLoadMatrix(matrixMode, matrix); } + public static void recordOrtho(double left, double right, double bottom, double top, double zNear, double zFar) { if (currentRecorder != null) currentRecorder.recordOrtho(left, right, bottom, top, zNear, zFar); } + public static void recordFrustum(double left, double right, double bottom, double top, double zNear, double zFar) { if (currentRecorder != null) currentRecorder.recordFrustum(left, right, bottom, top, zNear, zFar); } + public static void recordViewport(int x, int y, int width, int height) { if (currentRecorder != null) currentRecorder.recordViewport(x, y, width, height); } + public static void recordNormal(float nx, float ny, float nz) { if (currentRecorder != null) currentRecorder.recordNormal(nx, ny, nz); } + public static void recordPointSize(float size) { if (currentRecorder != null) currentRecorder.recordPointSize(size); } + public static void recordLineWidth(float width) { if (currentRecorder != null) currentRecorder.recordLineWidth(width); } + public static void recordLineStipple(int factor, int pattern) { if (currentRecorder != null) currentRecorder.recordLineStipple(factor, pattern); } + public static void recordPolygonOffset(float factor, float units) { if (currentRecorder != null) currentRecorder.recordPolygonOffset(factor, units); } + public static void recordPolygonMode(int face, int mode) { if (currentRecorder != null) currentRecorder.recordPolygonMode(face, mode); } + public static void recordColorMaterial(int face, int mode) { if (currentRecorder != null) currentRecorder.recordColorMaterial(face, mode); } + public static void recordLogicOp(int opcode) { if (currentRecorder != null) currentRecorder.recordLogicOp(opcode); } + public static void recordActiveTexture(int texture) { if (currentRecorder != null) currentRecorder.recordActiveTexture(texture); } + public static void recordUseProgram(int program) { if (currentRecorder != null) currentRecorder.recordUseProgram(program); } + public static void recordPushAttrib(int mask) { if (currentRecorder != null) currentRecorder.recordPushAttrib(mask); } + public static void recordPopAttrib() { if (currentRecorder != null) currentRecorder.recordPopAttrib(); } + public static void recordFogf(int pname, float param) { if (currentRecorder != null) currentRecorder.recordFogf(pname, param); } + public static void recordFogi(int pname, int param) { if (currentRecorder != null) currentRecorder.recordFogi(pname, param); } + public static void recordFog(int pname, java.nio.FloatBuffer params) { if (currentRecorder != null) currentRecorder.recordFog(pname, params); } + public static void recordLightf(int light, int pname, float param) { if (currentRecorder != null) currentRecorder.recordLightf(light, pname, param); } + public static void recordLighti(int light, int pname, int param) { if (currentRecorder != null) currentRecorder.recordLighti(light, pname, param); } + public static void recordLight(int light, int pname, java.nio.FloatBuffer params) { if (currentRecorder != null) currentRecorder.recordLight(light, pname, params); } + public static void recordLightModelf(int pname, float param) { if (currentRecorder != null) currentRecorder.recordLightModelf(pname, param); } + public static void recordLightModeli(int pname, int param) { if (currentRecorder != null) currentRecorder.recordLightModeli(pname, param); } + public static void recordLightModel(int pname, java.nio.FloatBuffer params) { if (currentRecorder != null) currentRecorder.recordLightModel(pname, params); } + public static void recordMaterialf(int face, int pname, float val) { if (currentRecorder != null) currentRecorder.recordMaterialf(face, pname, val); } + public static void recordMaterial(int face, int pname, java.nio.FloatBuffer params) { if (currentRecorder != null) currentRecorder.recordMaterial(face, pname, params); } + public static void recordStencilFunc(int func, int ref, int mask) { if (currentRecorder != null) currentRecorder.recordStencilFunc(func, ref, mask); } + public static void recordStencilFuncSeparate(int face, int func, int ref, int mask) { if (currentRecorder != null) currentRecorder.recordStencilFuncSeparate(face, func, ref, mask); } + public static void recordStencilOp(int fail, int zfail, int zpass) { if (currentRecorder != null) currentRecorder.recordStencilOp(fail, zfail, zpass); } + public static void recordStencilOpSeparate(int face, int sfail, int dpfail, int dppass) { if (currentRecorder != null) currentRecorder.recordStencilOpSeparate(face, sfail, dpfail, dppass); } + public static void recordStencilMask(int mask) { if (currentRecorder != null) currentRecorder.recordStencilMask(mask); } + public static void recordStencilMaskSeparate(int face, int mask) { if (currentRecorder != null) currentRecorder.recordStencilMaskSeparate(face, mask); } + public static void recordCallList(int listId) { if (currentRecorder != null) currentRecorder.recordCallList(listId); } + public static void recordComplexCommand(DisplayListCommand cmd) { if (currentRecorder != null) currentRecorder.recordComplexCommand(cmd); } + + /** + * Add an immediate mode draw to the current display list compilation. + * Called by GLStateManager.glEnd() when immediate mode geometry is ready. + * + *

This method captures the current transform and command position, creating + * an AccumulatedDraw that will be interleaved correctly with other commands + * during display list playback. + * + * @param result The immediate mode result containing quads, lines, and flags + */ + public static void addImmediateModeDraw(ImmediateModeRecorder.Result result) { + if (accumulatedDraws == null || result == null) { + return; + } + + // Get relative transform (changes since glNewList, not absolute matrix state) + final Matrix4f currentTransform = new Matrix4f(relativeTransform); + + // Add quad draw if we have quads + if (!result.quads().isEmpty()) { + final AccumulatedDraw quadDraw = new AccumulatedDraw(result.quads(), currentTransform, result.flags(), getCommandCount()); + accumulatedDraws.add(quadDraw); + } + + // Add line draw if we have lines + if (!result.lines().isEmpty()) { + final AccumulatedLineDraw lineDraw = new AccumulatedLineDraw(result.lines(), currentTransform, result.flags(), getCommandCount()); + accumulatedLineDraws.add(lineDraw); + } + } + + /** + * Update the relative transform during display list compilation. + * Called by GLStateManager matrix methods (glTranslate, glRotate, glScale). + * + * @param x Translation x, scale x, or rotation angle (degrees) + * @param y Translation y or scale y (ignored for rotate) + * @param z Translation z or scale z (ignored for rotate) + * @param op The operation type + * @param rotationAxis For rotation only, the axis vector (null otherwise) + */ + public static void updateRelativeTransform(float x, float y, float z, TransformOp op, Vector3f rotationAxis) { + if (relativeTransform == null || !GLStateManager.isModelViewMatrix()) { + return; + } + + switch (op) { + case TRANSLATE -> relativeTransform.translate(x, y, z); + case SCALE -> relativeTransform.scale(x, y, z); + case ROTATE -> { + if (rotationAxis != null) { + relativeTransform.rotate((float) Math.toRadians(x), rotationAxis); + } + } + } + } + + /** + * Update the relative transform with a matrix multiplication during display list compilation. + * Called by GLStateManager.glMultMatrix(). + * + * @param matrix The matrix to multiply + */ + public static void updateRelativeTransform(Matrix4f matrix) { + if (relativeTransform == null) { + return; + } + + // Only track MODELVIEW - projection/texture don't affect baked geometry + if (!GLStateManager.isModelViewMatrix()) { + return; + } + + relativeTransform.mul(matrix); + } + + /** + * Push the current MODELVIEW transform onto the stack. + * Only operates when current matrix mode is MODELVIEW. + * Called by GLStateManager.glPushMatrix() during recording. + */ + public static void pushRelativeTransform() { + if (relativeTransform == null) { + return; + } + + // Only track MODELVIEW - projection/texture don't affect baked geometry + if (!GLStateManager.isModelViewMatrix()) { + return; + } + + relativeTransform.pushMatrix(); + } + + /** + * Pop the MODELVIEW transform from stack. + * Only operates when current matrix mode is MODELVIEW. + * Called by GLStateManager.glPopMatrix() during recording. + */ + public static void popRelativeTransform() { + if (relativeTransform == null) { + return; + } + + // Only track MODELVIEW - projection/texture don't affect baked geometry + if (!GLStateManager.isModelViewMatrix()) { + return; + } + + try { + relativeTransform.popMatrix(); + } catch (RuntimeException e) { + LOGGER.warn("[DisplayList] Transform stack underflow in list={}", glListId); + } + } + + /** + * Reset the relative transform to identity during display list compilation. + * Called by GLStateManager.glLoadMatrix() - after loading an absolute matrix, + * subsequent transforms are relative to that loaded matrix (i.e., start from identity). + */ + public static void resetRelativeTransform() { + if (relativeTransform == null) { + return; + } + relativeTransform.identity(); + } + + /** + * Check if a display list exists (has been compiled and stored). + * Checks both DisplayListManager's cache and VBOManager for GTNHLib compatibility. + * + * @param list The display list ID to check + * @return true if the display list exists, false otherwise + */ + public static boolean displayListExists(int list) { + if (displayListCache.containsKey(list)) { + return true; + } + // Check VBOManager for GTNHLib compatibility (negative IDs) + if (list < -1) { + return VBOManager.get(list) != null; + } + return false; + } + + /** + * Get a compiled display list from the cache. + * Used by CallListCmd to execute nested display lists with the unoptimized version. + * + * @param list The display list ID + * @return The CompiledDisplayList, or null if not found + */ + public static CompiledDisplayList getDisplayList(int list) { + return displayListCache.get(list); + } + + /** + * Start display list compilation. + * Supports nested glNewList() calls (spec-violating but needed for mod compatibility). + */ + public static void glNewList(int list, int mode) { + // Assert main thread for display list compilation + if (!Thread.currentThread().equals(GLStateManager.getMainThread())) { + throw new IllegalStateException("Display list compilation must happen on main thread"); + } + + // Handle nested compilation - push current context onto stack + final boolean isNested = glListMode > 0; + + if (isNested) { + // Nested display list compilation violates OpenGL spec, but some of our optimizations require it + // Save current compilation context and start fresh for nested list + final CompilationContext parentContext = new CompilationContext( + glListId, glListMode, currentRecorder, accumulatedDraws, accumulatedLineDraws, + relativeTransform, tessellatorCompiling, immediateModeRecorder + ); + compilationStack.push(parentContext); + } + + // Initialize fresh context for this (possibly nested) list + glListId = list; + glListMode = mode; + recordingThread = Thread.currentThread(); // Track which thread is recording + currentRecorder = new CommandRecorder(); // Create command recorder + accumulatedDraws = new ArrayList<>(64); // Fewer draws than commands typically + accumulatedLineDraws = new ArrayList<>(16); // Line draws are less common + relativeTransform = new Matrix4fStack(GLStateManager.MAX_MODELVIEW_STACK_DEPTH); + relativeTransform.identity(); // Track relative transforms from identity + immediateModeRecorder = new ImmediateModeRecorder(); // For glBegin/glEnd/glVertex + + // Start compiling mode with per-draw callback (works for both root and nested lists now) + TessellatorManager.setCompiling((quads, flags) -> { + if (quads.isEmpty()) { + return; + } + + // Get relative transform (changes since glNewList, not absolute matrix state) + final Matrix4f currentTransform = new Matrix4f(relativeTransform); + + // Accumulate this draw for batching at glEndList() + final AccumulatedDraw draw = new AccumulatedDraw(quads, currentTransform, flags, getCommandCount()); + accumulatedDraws.add(draw); + }); + tessellatorCompiling = true; + + // We hijack display list compilation completely - no GL11.glNewList() calls + // During compile don't actually apply any changes to GL, but do track them + GLStateManager.getModelViewMatrix().pushMatrix(); // Save current model view matrix state + GLStateManager.pushState(GL11.GL_ALL_ATTRIB_BITS); + } + + /** + * End display list compilation and build optimized/unoptimized versions. + */ + public static void glEndList() { + if (glListMode == 0) { + throw new RuntimeException("glEndList called outside of a display list!"); + } + final boolean needsRender = glListMode == GL11.GL_COMPILE_AND_EXECUTE; + final boolean isNested = !compilationStack.isEmpty(); + + // Stop compiling mode (works for both root and nested lists now) + if (tessellatorCompiling) { + TessellatorManager.stopCompiling(); + tessellatorCompiling = false; + } + + final CompiledDisplayList compiled; + // Create CompiledDisplayList with both unoptimized and optimized versions + final CommandBuffer rawCommandBuffer = currentRecorder != null ? currentRecorder.getBuffer() : null; + final boolean hasCommands = rawCommandBuffer != null && rawCommandBuffer.size() > 0; + final boolean hasDraws = accumulatedDraws != null && !accumulatedDraws.isEmpty(); + final boolean hasLineDraws = accumulatedLineDraws != null && !accumulatedLineDraws.isEmpty(); + + if (hasCommands || hasDraws || hasLineDraws) { + // Phase 1: Compile format-based VBOs (shared by both optimized and unoptimized paths) + final Map compiledQuadBuffers = compileFormatBasedVBOs(accumulatedDraws); + + // Phase 2: Compile line VBO (shared by both paths) + final CompiledLineBuffer compiledLineBuffer = compileLineBuffer(accumulatedLineDraws); + + // Collect all owned VBOs (quads + lines) + final VertexBuffer[] ownedVbos = extractOwnedVbos(compiledQuadBuffers, compiledLineBuffer); + + // Build to CommandBuffer - optimize raw buffer to final buffer + final CommandBuffer finalBuffer = new CommandBuffer(); + if (DEBUG_DISPLAY_LISTS) { + CommandBufferBuilder.buildUnoptimizedFromRawBuffer(rawCommandBuffer, accumulatedDraws, compiledQuadBuffers, compiledLineBuffer, ownedVbos, finalBuffer); + } else { + CommandBufferBuilder.buildOptimizedFromRawBuffer(rawCommandBuffer, compiledQuadBuffers, compiledLineBuffer, ownedVbos, glListId, finalBuffer); + } + + // Free the recorder (and its buffer) after optimization + currentRecorder.free(); + + compiled = new CompiledDisplayList(finalBuffer.toBuffer(), finalBuffer.getComplexObjects(), ownedVbos); + displayListCache.put(glListId, compiled); + } else { + // Free the recorder even if empty + if (currentRecorder != null) { + currentRecorder.free(); + } + compiled = null; + } + + // Handle nested compilation - restore parent context + if (isNested) { + final CompilationContext parentContext = compilationStack.pop(); + + // Restore parent context + glListId = parentContext.listId; + glListMode = parentContext.listMode; + currentRecorder = parentContext.recorder; + accumulatedDraws = parentContext.draws; + accumulatedLineDraws = parentContext.lineDraws; + relativeTransform = parentContext.transform; + tessellatorCompiling = parentContext.wasTessellatorCompiling; + immediateModeRecorder = parentContext.immediateRecorder; + + // Note: TessellatorManager callback stack was popped by stopCompiling() + // Parent's COMPILING callback is now active again + } else { + // Not nested - clear all compilation state + currentRecorder = null; + recordingThread = null; + accumulatedDraws = null; + accumulatedLineDraws = null; + relativeTransform = null; + immediateModeRecorder = null; + glListId = -1; + glListMode = 0; + } + + GLStateManager.getModelViewMatrix().popMatrix(); + GLStateManager.popState(); + + // Only execute if COMPILE_AND_EXECUTE AND this is the root (non-nested) list + // Nested lists will execute naturally when the parent executes via CallListCmd + if (needsRender && !isNested && compiled != null) { + compiled.render(); + } + } + + /** + * Execute a compiled display list. + */ + public static void glCallList(int list) { + // If we're currently recording, record a reference to this list Per OpenGL spec: the inner list is NOT executed during compilation. + // It only executes when the parent list is played back. + // For GL_COMPILE_AND_EXECUTE: the entire parent list (including this CallListCmd) executes at the end of glEndList, not during compilation. + if (currentRecorder != null) { + // Record a CallList command that will execute the referenced list during playback + recordCallList(list); + return; + } + + // Normal playback mode + final CompiledDisplayList compiled = displayListCache.get(list); + if (compiled != null) { + GLStateManager.trySyncProgram(); + final int prevList = currentRenderingList; + currentRenderingList = list; + compiled.render(); + currentRenderingList = prevList; + return; + } + + // Fall back to VBOManager or legacy display list + if (list < 0) { + // Negative IDs are VBOManager space (entity models, etc.) + final VertexBuffer vbo = VBOManager.get(list); + if (vbo != null) { + vbo.render(); + } + // Per OpenGL spec: if list is undefined, glCallList has no effect + } else { + // Positive IDs - fall back to native GL display lists + // This happens for lists allocated but never compiled via glNewList + GLStateManager.trySyncProgram(); + GL11.glCallList(list); + } + } + + /** + * Delete display lists and free their VBO resources. + */ + public static void glDeleteLists(int list, int range) { + for (int i = list; i < list + range; i++) { + CompiledDisplayList compiled = displayListCache.remove(i); + if (compiled != null) { + compiled.delete(); + } + } + // Also call legacy glDeleteLists for any lists we didn't handle + GL11.glDeleteLists(list, range); + } + + /** + * Compile format-based VBOs from accumulated draws. + * Groups draws by vertex format and compiles each format's geometry into a single VBO. + * Both optimized and unoptimized paths share these VBOs. + * + * @param accumulatedDraws The accumulated draws to compile + * @return Map from format flags to compiled buffers (VBO + ranges) + */ + private static Map compileFormatBasedVBOs( + List accumulatedDraws) { + if (accumulatedDraws == null || accumulatedDraws.isEmpty()) { + return Collections.emptyMap(); + } + + // Group draws by vertex format + final Map formatBuffers = new HashMap<>(); + for (AccumulatedDraw draw : accumulatedDraws) { + formatBuffers.computeIfAbsent(draw.flags, FormatBuffer::new).addDraw(draw); + } + + // Compile each format's geometry into a single VBO + final Map compiled = new HashMap<>(); + for (var entry : formatBuffers.entrySet()) { + compiled.put(entry.getKey(), entry.getValue().finish()); + } + return compiled; + } + + /** + * Compile line draws into a shared VBO. + * + * @param lineDraws The accumulated line draws + * @return Compiled line buffer, or null if no lines + */ + private static CompiledLineBuffer compileLineBuffer(List lineDraws) { + if (lineDraws == null || lineDraws.isEmpty()) { + return null; + } + + final LineBuffer buffer = new LineBuffer(); + for (AccumulatedLineDraw draw : lineDraws) { + buffer.addDraw(draw); + } + return buffer.finish(); + } + + /** + * Extract owned VBOs from compiled buffers (quads + lines). + * These VBOs are shared by both optimized and unoptimized paths. + * + * @param compiledQuadBuffers The compiled quad format buffers + * @param compiledLineBuffer The compiled line buffer (may be null) + * @return Array of VBOs that need to be deleted when the display list is deleted + */ + private static VertexBuffer[] extractOwnedVbos( + Map compiledQuadBuffers, + CompiledLineBuffer compiledLineBuffer) { + final List vbos = new ArrayList<>(); + + // Add quad VBOs + for (CompiledFormatBuffer buffer : compiledQuadBuffers.values()) { + vbos.add(buffer.vbo()); + } + + // Add line VBO if present + if (compiledLineBuffer != null) { + vbos.add(compiledLineBuffer.vbo()); + } + + return vbos.toArray(new VertexBuffer[0]); + } + + // ==================== Buffer-to-Buffer Optimization ==================== + /** + * Build optimized command list using pre-compiled VBOs. + * Uses mergedRanges (consecutive same-transform draws combined) and collapses MODELVIEW transforms. + * (Package-private for testing) + * + * @param currentCommands Original matrix/state commands + * @param compiledQuadBuffers Pre-compiled format-based quad VBOs + * @param compiledLineBuffer Pre-compiled line VBO (may be null) + * @param glListId Display list ID for logging + * @return Optimized command array + */ + static DisplayListCommand[] buildOptimizedCommands( + List currentCommands, + Map compiledQuadBuffers, + CompiledLineBuffer compiledLineBuffer, + int glListId) { + + final List optimized = new ArrayList<>(); + final TransformOptimizer transformOpt = new TransformOptimizer(glListId); + + // Collect all merged draw ranges (quads + lines) sorted by command index + final List allRanges = new ArrayList<>(); + + // Add quad ranges + for (CompiledFormatBuffer compiledBuffer : compiledQuadBuffers.values()) { + for (DrawRange range : compiledBuffer.mergedRanges()) { + allRanges.add(new DrawRangeWithBuffer(range, compiledBuffer.vbo(), compiledBuffer.flags().hasBrightness)); + } + } + + // Add line ranges + if (compiledLineBuffer != null) { + for (DrawRange range : compiledLineBuffer.mergedRanges()) { + allRanges.add(new DrawRangeWithBuffer(range, compiledLineBuffer.vbo(), false)); + } + } + + allRanges.sort(Comparator.comparingInt(r -> r.range().commandIndex())); + + // Process command stream with interleaved draws + final OptimizationContextImpl ctx = new OptimizationContextImpl(transformOpt, optimized); + + int rangeIndex = 0; + for (int i = 0; i < currentCommands.size(); i++) { + // Emit draw ranges (quads and lines) at this command position + while (rangeIndex < allRanges.size() && allRanges.get(rangeIndex).range().commandIndex() == i) { + emitDrawRangeLegacy(allRanges.get(rangeIndex++), transformOpt, optimized); + } + + // Process the original command + final DisplayListCommand cmd = currentCommands.get(i); + if (cmd.handleOptimization(ctx)) { + optimized.add(cmd); + } + } + + // Emit remaining draw ranges at end of command stream + while (rangeIndex < allRanges.size()) { + emitDrawRangeLegacy(allRanges.get(rangeIndex++), transformOpt, optimized); + } + + // Emit residual transform to match expected GL state + if (!transformOpt.isIdentity()) { + transformOpt.emitPendingTransform(optimized); + } + + return optimized.toArray(new DisplayListCommand[0]); + } + + /** + * Build optimized display list: batches draws with same flags, collapses MODELVIEW transforms. + * Instead of baking transforms into vertices, we emit collapsed MultMatrix commands at barriers. + * This properly handles nested display lists (CallList) which need GL state to be correct. + * + *

Transform collapsing strategy: + *

    + *
  • Track accumulated MODELVIEW transform during command stream analysis
  • + *
  • At barriers (Draw, CallList), emit a single MultMatrix if transform changed
  • + *
  • Push/Pop maintain proper stack semantics
  • + *
  • Vertices stay canonical (untransformed) in VBOs
  • + *
+ */ + // Package-private for testing + static OptimizedListResult buildOptimizedDisplayList( + List currentCommands, + List accumulatedDraws, + List accumulatedLineDraws, + int glListId) { + // Compile quad VBOs + final Map compiledQuadBuffers = compileFormatBasedVBOs(accumulatedDraws); + + // Compile line VBO + final CompiledLineBuffer compiledLineBuffer = compileLineBuffer(accumulatedLineDraws); + + // Extract owned VBOs + final VertexBuffer[] ownedVbos = extractOwnedVbos(compiledQuadBuffers, compiledLineBuffer); + + // Build optimized commands using the new unified path + final DisplayListCommand[] optimized = buildOptimizedCommands(currentCommands, compiledQuadBuffers, compiledLineBuffer, glListId); + + return new OptimizedListResult(optimized, ownedVbos); + } + + /** + * Emit a draw range command for the legacy path (testing). + * Uses delta-based transform emission via TransformOptimizer. + */ + private static void emitDrawRangeLegacy(DrawRangeWithBuffer drwb, TransformOptimizer transformOpt, + List output) { + final DrawRange range = drwb.range(); + + // Delta-based: only emit MultMatrix if transform differs from last emitted + if (transformOpt.needsTransformForDraw(range.transform())) { + transformOpt.emitTransformTo(output, range.transform()); + } + + output.add(new com.gtnewhorizons.angelica.glsm.recording.commands.DrawRangeCmd( + drwb.vbo(), + range.startVertex(), + range.vertexCount(), + drwb.hasBrightness() + )); + } + + /** + * Implementation of OptimizationContext that wraps transform optimizer. + */ + @Desugar + private record OptimizationContextImpl( + TransformOptimizer transformOpt, + List output + ) implements OptimizationContext { + @Override public Matrix4f getAccumulatedTransform() { return transformOpt.getAccumulated(); } + @Override public void loadIdentity() { transformOpt.loadIdentity(); } + @Override public void pushTransform() { transformOpt.pushTransform(); } + @Override public void popTransform() { transformOpt.popTransform(); } + @Override public void emitPendingTransform() { transformOpt.emitPendingTransform(output); } + @Override public void emitTransformTo(Matrix4f target) { transformOpt.emitTransformTo(output, target); } + @Override public void emit(DisplayListCommand cmd) { output.add(cmd); } + @Override public void markAbsoluteMatrix() { transformOpt.markAbsoluteMatrix(); } + @Override public boolean checkAndClearAbsoluteMatrix() { return transformOpt.checkAndClearAbsoluteMatrix(); } + } + + + /** + * Selects the optimal VertexFormat from GTNHLib defaults based on actual attribute usage. + * This reduces memory usage by excluding unused attributes. + */ + private static VertexFormat selectOptimalFormat(CapturingTessellator.Flags flags) { + final boolean hasColor = flags.hasColor; + final boolean hasTexture = flags.hasTexture; + final boolean hasBrightness = flags.hasBrightness; + final boolean hasNormals = flags.hasNormals; + + final VertexFormat format; + + // Map flags to GTNHLib formats, ordered by guess at frequency/likelihood + if (!hasColor && hasTexture && !hasBrightness && !hasNormals) { + format = DefaultVertexFormat.POSITION_TEXTURE; // Entity models, common + } else if (!hasColor && !hasTexture && !hasBrightness && !hasNormals) { + format = DefaultVertexFormat.POSITION; // Sky rendering, simple geometry + } else if (hasColor && hasTexture && !hasBrightness && !hasNormals) { + format = DefaultVertexFormat.POSITION_COLOR_TEXTURE; // Colored blocks/UI + } else if (!hasColor && hasTexture && hasBrightness && hasNormals) { + format = DefaultVertexFormat.POSITION_TEXTURE_LIGHT_NORMAL; // BuildCraft fluids + } else if (!hasColor && hasTexture && !hasBrightness && hasNormals) { + format = DefaultVertexFormat.POSITION_TEXTURE_NORMAL; // Lit geometry with normals + } else if (hasColor && hasTexture && hasBrightness && !hasNormals) { + format = DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP; // Colored lit blocks + } else if (hasColor && !hasTexture && !hasBrightness && !hasNormals) { + format = DefaultVertexFormat.POSITION_COLOR; // Colored lines/particles + } else { + format = DefaultVertexFormat.POSITION_COLOR_TEXTURE_LIGHT_NORMAL; // Full format fallback + } + + return format; + } + + /** + * Compile captured Tessellator quads into a VertexBuffer (VAO or VBO). + * Uses VertexBuffer for proper compatibility with fixed-function pipeline. + * + * @param quads The quads to compile + * @param flags The vertex attribute flags + * @return The uploaded VertexBuffer + */ + static VertexBuffer compileQuads(List quads, CapturingTessellator.Flags flags) { + // Select optimal predefined format from GTNHLib + final VertexFormat format = selectOptimalFormat(flags); + + // Use VAOManager to create VAO with optimal format (falls back to VBO if unsupported) + final VertexBuffer vao = VAOManager.createVAO(format, GL11.GL_QUADS); + + final ByteBuffer buffer = CapturingTessellator.quadsToBuffer(quads, format); + + vao.upload(buffer); + + return vao; + } + +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/DrawRange.java b/src/main/java/com/gtnewhorizons/angelica/glsm/DrawRange.java new file mode 100644 index 000000000..37d9ddee8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/DrawRange.java @@ -0,0 +1,11 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.github.bsideup.jabel.Desugar; +import org.joml.Matrix4f; + +/** + * Represents a vertex range within a shared VBO, along with its transform. + * Used during format-based batching to track where each draw's vertices are stored. + */ +@Desugar +record DrawRange(int startVertex, int vertexCount, Matrix4f transform, int commandIndex) {} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/DrawRangeWithBuffer.java b/src/main/java/com/gtnewhorizons/angelica/glsm/DrawRangeWithBuffer.java new file mode 100644 index 000000000..bc881f041 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/DrawRangeWithBuffer.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; + +/** + * Helper record pairing a DrawRange with its VBO and brightness flag. + * Used for sorting ranges by command index while retaining VBO reference. + * Works for both quad ranges (from CompiledFormatBuffer) and line ranges (from CompiledLineBuffer). + */ +@Desugar +record DrawRangeWithBuffer(DrawRange range, VertexBuffer vbo, boolean hasBrightness) {} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java b/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java index c85cb7130..d230c2387 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java @@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm; import com.google.common.collect.ImmutableSet; import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; +import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import org.lwjgl.opengl.GL11; @@ -22,6 +23,7 @@ public class Feature { GL11.GL_STENCIL_BUFFER_BIT, GL11.GL_TEXTURE_BIT, GL11.GL_TRANSFORM_BIT, GL11.GL_VIEWPORT_BIT }; static final Int2ObjectMap>> maskToFeaturesMap = new Int2ObjectOpenHashMap<>(); + static final Int2ObjectMap[]> maskToNonBooleanStacksMap = new Int2ObjectOpenHashMap<>(); static List> maskToFeatures(int mask) { if(maskToFeaturesMap.containsKey(mask)) { @@ -42,6 +44,29 @@ public class Feature { return asList; } + /** + * Returns only non-BooleanStateStack instances for the given mask. + * These use traditional push/pop without global depth tracking. + */ + static IStateStack[] maskToNonBooleanStacks(int mask) { + IStateStack[] cached = maskToNonBooleanStacksMap.get(mask); + if (cached != null) { + return cached; + } + + final List> all = maskToFeatures(mask); + final List> nonBooleans = new ArrayList<>(); + for (int i = 0; i < all.size(); i++) { + final IStateStack stack = all.get(i); + if (!(stack instanceof BooleanStateStack)) { + nonBooleans.add(stack); + } + } + cached = nonBooleans.toArray(new IStateStack[0]); + maskToNonBooleanStacksMap.put(mask, cached); + return cached; + } + private static final Map>> attribToFeatures = new HashMap<>(); /** @@ -223,10 +248,9 @@ public class Feature { , GLStateManager.shadeModelState // GL_SHADE_MODEL setting )); attribToFeatures.put(GL11.GL_LINE_BIT, ImmutableSet.of( - // GL_LINE_SMOOTH flag - // GL_LINE_STIPPLE enable bit - // Line stipple pattern and repeat counter - // Line width + GLStateManager.lineSmoothState // GL_LINE_SMOOTH flag + , GLStateManager.lineStippleState // GL_LINE_STIPPLE enable bit + , GLStateManager.lineState // Line stipple pattern, repeat counter, and width )); attribToFeatures.put(GL11.GL_LIST_BIT, ImmutableSet.of( // GL_LIST_BASE setting @@ -251,21 +275,17 @@ public class Feature { // GL_READ_BUFFER setting )); attribToFeatures.put(GL11.GL_POINT_BIT, ImmutableSet.of( - // GL_POINT_SMOOTH flag - // Point size + GLStateManager.pointSmoothState // GL_POINT_SMOOTH flag + , GLStateManager.pointState // Point size )); attribToFeatures.put(GL11.GL_POLYGON_BIT, ImmutableSet.of( GLStateManager.cullState // GL_CULL_FACE enable bit - // GL_CULL_FACE_MODE value - // GL_FRONT_FACE indicator - // GL_POLYGON_MODE setting - // GL_POLYGON_SMOOTH flag - // GL_POLYGON_STIPPLE enable bit - // GL_POLYGON_OFFSET_FILL flag - // GL_POLYGON_OFFSET_LINE flag - // GL_POLYGON_OFFSET_POINT flag - // GL_POLYGON_OFFSET_FACTOR - // GL_POLYGON_OFFSET_UNITS + , GLStateManager.polygonSmoothState // GL_POLYGON_SMOOTH flag + , GLStateManager.polygonStippleState // GL_POLYGON_STIPPLE enable bit + , GLStateManager.polygonOffsetFillState // GL_POLYGON_OFFSET_FILL flag + , GLStateManager.polygonOffsetLineState // GL_POLYGON_OFFSET_LINE flag + , GLStateManager.polygonOffsetPointState // GL_POLYGON_OFFSET_POINT flag + , GLStateManager.polygonState // GL_CULL_FACE_MODE, GL_FRONT_FACE, GL_POLYGON_MODE, GL_POLYGON_OFFSET_FACTOR/UNITS )); attribToFeatures.put(GL11.GL_POLYGON_STIPPLE_BIT, ImmutableSet.of( // Polygon stipple pattern @@ -275,12 +295,8 @@ public class Feature { // Scissor box )); attribToFeatures.put(GL11.GL_STENCIL_BUFFER_BIT, ImmutableSet.of( - // GL_STENCIL_TEST enable bit - // Stencil function and reference value - // Stencil value mask - // Stencil fail, pass, and depth buffer pass actions - // Stencil buffer clear value - // Stencil buffer writemask + GLStateManager.stencilTest // GL_STENCIL_TEST enable bit + , GLStateManager.stencilState // Stencil function, ref, mask, ops, writemask, clear value )); final Set> textureAttribs = new HashSet<>(ImmutableSet.of( GLStateManager.activeTextureUnit // Active texture unit diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/FormatBuffer.java b/src/main/java/com/gtnewhorizons/angelica/glsm/FormatBuffer.java new file mode 100644 index 000000000..92d9bd438 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/FormatBuffer.java @@ -0,0 +1,96 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw; +import org.joml.Matrix4f; + +import java.util.ArrayList; +import java.util.List; + +/** + * Accumulates geometry for a single vertex format during display list compilation. + * All draws with the same format are collected here, then compiled into a single VBO. + *

+ * Tracks two types of ranges: + *

    + *
  • mergedRanges: Consecutive same-transform draws merged for optimized path
  • + *
  • perDrawRanges: One range per input draw for unoptimized path (1:1 with addDraw calls)
  • + *
+ */ +class FormatBuffer { + final CapturingTessellator.Flags flags; + final List allQuads = new ArrayList<>(); + + // Merged ranges for optimized path (consecutive same-transform draws combined) + final List mergedRanges = new ArrayList<>(); + + // Per-draw ranges for unoptimized path (1:1 with input draws) + final List perDrawRanges = new ArrayList<>(); + + int currentVertexOffset = 0; + + // Track the current range being built for merging consecutive same-transform draws + private int pendingStartVertex = -1; + private int pendingVertexCount = 0; + private Matrix4f pendingTransform = null; + private int pendingCommandIndex = -1; + + FormatBuffer(CapturingTessellator.Flags flags) { + this.flags = flags; + } + + /** + * Add a draw to this format buffer. + * Tracks both merged (for optimized) and per-draw (for unoptimized) ranges. + */ + void addDraw(AccumulatedDraw draw) { + int vertexCount = draw.quads.size() * 4; // 4 vertices per quad + + // Always track per-draw range for unoptimized path + perDrawRanges.add(new DrawRange(currentVertexOffset, vertexCount, draw.transform, draw.commandIndex)); + + // Merge logic for optimized path + if (pendingTransform != null && pendingTransform.equals(draw.transform)) { + // Extend the pending range + pendingVertexCount += vertexCount; + } else { + // Flush pending range if any + flushPendingRange(); + + // Start new pending range + pendingStartVertex = currentVertexOffset; + pendingVertexCount = vertexCount; + pendingTransform = new Matrix4f(draw.transform); + pendingCommandIndex = draw.commandIndex; + } + + allQuads.addAll(draw.quads); + currentVertexOffset += vertexCount; + } + + private void flushPendingRange() { + if (pendingTransform != null) { + mergedRanges.add(new DrawRange(pendingStartVertex, pendingVertexCount, pendingTransform, pendingCommandIndex)); + pendingTransform = null; + } + } + + /** + * Compile all accumulated quads into a single VBO. + * @return The compiled buffer with VBO and both range types + */ + CompiledFormatBuffer finish() { + // Flush any remaining pending range + flushPendingRange(); + + VertexBuffer vbo = DisplayListManager.compileQuads(allQuads, flags); + return new CompiledFormatBuffer( + vbo, + flags, + mergedRanges.toArray(new DrawRange[0]), + perDrawRanges.toArray(new DrawRange[0]) + ); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/GLDebug.java b/src/main/java/com/gtnewhorizons/angelica/glsm/GLDebug.java index 8c1de9fb7..1f6ad44f0 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/GLDebug.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/GLDebug.java @@ -9,7 +9,11 @@ import org.lwjgl.opengl.AMDDebugOutput; import org.lwjgl.opengl.AMDDebugOutputCallback; import org.lwjgl.opengl.ARBDebugOutput; import org.lwjgl.opengl.ARBDebugOutputCallback; +import org.lwjgl.opengl.EXTBlendColor; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL30; import org.lwjgl.opengl.GL43; import org.lwjgl.opengl.KHRDebug; @@ -269,6 +273,304 @@ public final class GLDebug { }; } + public static String getMatrixModeName(int mode) { + return switch (mode) { + case GL11.GL_MODELVIEW -> "MODELVIEW"; + case GL11.GL_PROJECTION -> "PROJECTION"; + case GL11.GL_TEXTURE -> "TEXTURE"; + case GL11.GL_COLOR -> "COLOR"; + default -> String.format("0x%X", mode); + }; + } + + public static String getCapabilityName(int cap) { + return switch (cap) { + case GL11.GL_ALPHA_TEST -> "ALPHA_TEST"; + case GL11.GL_BLEND -> "BLEND"; + case GL11.GL_COLOR_MATERIAL -> "COLOR_MATERIAL"; + case GL11.GL_CULL_FACE -> "CULL_FACE"; + case GL11.GL_DEPTH_TEST -> "DEPTH_TEST"; + case GL11.GL_FOG -> "FOG"; + case GL11.GL_LIGHTING -> "LIGHTING"; + case GL11.GL_LINE_SMOOTH -> "LINE_SMOOTH"; + case GL11.GL_NORMALIZE -> "NORMALIZE"; + case GL11.GL_POINT_SMOOTH -> "POINT_SMOOTH"; + case GL11.GL_POLYGON_OFFSET_FILL -> "POLYGON_OFFSET_FILL"; + case GL11.GL_POLYGON_OFFSET_LINE -> "POLYGON_OFFSET_LINE"; + case GL11.GL_POLYGON_SMOOTH -> "POLYGON_SMOOTH"; + case GL12.GL_RESCALE_NORMAL -> "RESCALE_NORMAL"; + case GL11.GL_SCISSOR_TEST -> "SCISSOR_TEST"; + case GL11.GL_STENCIL_TEST -> "STENCIL_TEST"; + case GL11.GL_TEXTURE_1D -> "TEXTURE_1D"; + case GL11.GL_TEXTURE_2D -> "TEXTURE_2D"; + case GL11.GL_LIGHT0 -> "LIGHT0"; + case GL11.GL_LIGHT1 -> "LIGHT1"; + case GL11.GL_LIGHT2 -> "LIGHT2"; + case GL11.GL_LIGHT3 -> "LIGHT3"; + case GL11.GL_LIGHT4 -> "LIGHT4"; + case GL11.GL_LIGHT5 -> "LIGHT5"; + case GL11.GL_LIGHT6 -> "LIGHT6"; + case GL11.GL_LIGHT7 -> "LIGHT7"; + default -> String.format("0x%X", cap); + }; + } + + public static String getComparisonFuncName(int func) { + return switch (func) { + case GL11.GL_NEVER -> "NEVER"; + case GL11.GL_LESS -> "LESS"; + case GL11.GL_EQUAL -> "EQUAL"; + case GL11.GL_LEQUAL -> "LEQUAL"; + case GL11.GL_GREATER -> "GREATER"; + case GL11.GL_NOTEQUAL -> "NOTEQUAL"; + case GL11.GL_GEQUAL -> "GEQUAL"; + case GL11.GL_ALWAYS -> "ALWAYS"; + default -> String.format("0x%X", func); + }; + } + + public static String getBlendFactorName(int factor) { + return switch (factor) { + case GL11.GL_ZERO -> "ZERO"; + case GL11.GL_ONE -> "ONE"; + case GL11.GL_SRC_COLOR -> "SRC_COLOR"; + case GL11.GL_ONE_MINUS_SRC_COLOR -> "ONE_MINUS_SRC_COLOR"; + case GL11.GL_DST_COLOR -> "DST_COLOR"; + case GL11.GL_ONE_MINUS_DST_COLOR -> "ONE_MINUS_DST_COLOR"; + case GL11.GL_SRC_ALPHA -> "SRC_ALPHA"; + case GL11.GL_ONE_MINUS_SRC_ALPHA -> "ONE_MINUS_SRC_ALPHA"; + case GL11.GL_DST_ALPHA -> "DST_ALPHA"; + case GL11.GL_ONE_MINUS_DST_ALPHA -> "ONE_MINUS_DST_ALPHA"; + case EXTBlendColor.GL_CONSTANT_COLOR_EXT -> "CONSTANT_COLOR"; + case EXTBlendColor.GL_ONE_MINUS_CONSTANT_COLOR_EXT -> "ONE_MINUS_CONSTANT_COLOR"; + case EXTBlendColor.GL_CONSTANT_ALPHA_EXT -> "CONSTANT_ALPHA"; + case EXTBlendColor.GL_ONE_MINUS_CONSTANT_ALPHA_EXT -> "ONE_MINUS_CONSTANT_ALPHA"; + case GL11.GL_SRC_ALPHA_SATURATE -> "SRC_ALPHA_SATURATE"; + default -> String.format("0x%X", factor); + }; + } + + public static String getShadeModelName(int mode) { + return switch (mode) { + case GL11.GL_FLAT -> "FLAT"; + case GL11.GL_SMOOTH -> "SMOOTH"; + default -> String.format("0x%X", mode); + }; + } + + public static String getTextureTargetName(int target) { + return switch (target) { + case GL11.GL_TEXTURE_1D -> "TEXTURE_1D"; + case GL11.GL_TEXTURE_2D -> "TEXTURE_2D"; + case GL12.GL_TEXTURE_3D -> "TEXTURE_3D"; + case GL13.GL_TEXTURE_CUBE_MAP -> "TEXTURE_CUBE_MAP"; + default -> String.format("0x%X", target); + }; + } + + public static String getTexturePnameName(int pname) { + return switch (pname) { + case GL11.GL_TEXTURE_MIN_FILTER -> "TEXTURE_MIN_FILTER"; + case GL11.GL_TEXTURE_MAG_FILTER -> "TEXTURE_MAG_FILTER"; + case GL11.GL_TEXTURE_WRAP_S -> "TEXTURE_WRAP_S"; + case GL11.GL_TEXTURE_WRAP_T -> "TEXTURE_WRAP_T"; + case GL12.GL_TEXTURE_WRAP_R -> "TEXTURE_WRAP_R"; + case GL12.GL_TEXTURE_MIN_LOD -> "TEXTURE_MIN_LOD"; + case GL12.GL_TEXTURE_MAX_LOD -> "TEXTURE_MAX_LOD"; + case GL12.GL_TEXTURE_BASE_LEVEL -> "TEXTURE_BASE_LEVEL"; + case GL12.GL_TEXTURE_MAX_LEVEL -> "TEXTURE_MAX_LEVEL"; + case GL11.GL_TEXTURE_BORDER_COLOR -> "TEXTURE_BORDER_COLOR"; + case GL11.GL_TEXTURE_PRIORITY -> "TEXTURE_PRIORITY"; + case GL14.GL_TEXTURE_LOD_BIAS -> "TEXTURE_LOD_BIAS"; + case GL14.GL_GENERATE_MIPMAP -> "GENERATE_MIPMAP"; + default -> String.format("0x%X", pname); + }; + } + + public static String getTextureFormatName(int format) { + return switch (format) { + case GL11.GL_RED -> "RED"; + case GL11.GL_GREEN -> "GREEN"; + case GL11.GL_BLUE -> "BLUE"; + case GL11.GL_ALPHA -> "ALPHA"; + case GL11.GL_RGB -> "RGB"; + case GL11.GL_RGBA -> "RGBA"; + case GL11.GL_LUMINANCE -> "LUMINANCE"; + case GL11.GL_LUMINANCE_ALPHA -> "LUMINANCE_ALPHA"; + case GL12.GL_BGR -> "BGR"; + case GL12.GL_BGRA -> "BGRA"; + case GL11.GL_DEPTH_COMPONENT -> "DEPTH_COMPONENT"; + case GL11.GL_STENCIL_INDEX -> "STENCIL_INDEX"; + case GL30.GL_DEPTH_STENCIL -> "DEPTH_STENCIL"; + // Internal formats + case GL11.GL_RGB8 -> "RGB8"; + case GL11.GL_RGBA8 -> "RGBA8"; + case GL11.GL_ALPHA8 -> "ALPHA8"; + case GL11.GL_LUMINANCE8 -> "LUMINANCE8"; + case GL11.GL_LUMINANCE8_ALPHA8 -> "LUMINANCE8_ALPHA8"; + default -> String.format("0x%X", format); + }; + } + + public static String getDataTypeName(int type) { + return switch (type) { + case GL11.GL_UNSIGNED_BYTE -> "UNSIGNED_BYTE"; + case GL11.GL_BYTE -> "BYTE"; + case GL11.GL_UNSIGNED_SHORT -> "UNSIGNED_SHORT"; + case GL11.GL_SHORT -> "SHORT"; + case GL11.GL_UNSIGNED_INT -> "UNSIGNED_INT"; + case GL11.GL_INT -> "INT"; + case GL11.GL_FLOAT -> "FLOAT"; + case GL11.GL_DOUBLE -> "DOUBLE"; + case GL12.GL_UNSIGNED_BYTE_3_3_2 -> "UNSIGNED_BYTE_3_3_2"; + case GL12.GL_UNSIGNED_SHORT_4_4_4_4 -> "UNSIGNED_SHORT_4_4_4_4"; + case GL12.GL_UNSIGNED_SHORT_5_5_5_1 -> "UNSIGNED_SHORT_5_5_5_1"; + case GL12.GL_UNSIGNED_INT_8_8_8_8 -> "UNSIGNED_INT_8_8_8_8"; + case GL12.GL_UNSIGNED_INT_10_10_10_2 -> "UNSIGNED_INT_10_10_10_2"; + default -> String.format("0x%X", type); + }; + } + + public static String getLightName(int light) { + return switch (light) { + case GL11.GL_LIGHT0 -> "LIGHT0"; + case GL11.GL_LIGHT1 -> "LIGHT1"; + case GL11.GL_LIGHT2 -> "LIGHT2"; + case GL11.GL_LIGHT3 -> "LIGHT3"; + case GL11.GL_LIGHT4 -> "LIGHT4"; + case GL11.GL_LIGHT5 -> "LIGHT5"; + case GL11.GL_LIGHT6 -> "LIGHT6"; + case GL11.GL_LIGHT7 -> "LIGHT7"; + default -> String.format("0x%X", light); + }; + } + + public static String getLightPnameName(int pname) { + return switch (pname) { + case GL11.GL_AMBIENT -> "AMBIENT"; + case GL11.GL_DIFFUSE -> "DIFFUSE"; + case GL11.GL_SPECULAR -> "SPECULAR"; + case GL11.GL_POSITION -> "POSITION"; + case GL11.GL_SPOT_DIRECTION -> "SPOT_DIRECTION"; + case GL11.GL_SPOT_EXPONENT -> "SPOT_EXPONENT"; + case GL11.GL_SPOT_CUTOFF -> "SPOT_CUTOFF"; + case GL11.GL_CONSTANT_ATTENUATION -> "CONSTANT_ATTENUATION"; + case GL11.GL_LINEAR_ATTENUATION -> "LINEAR_ATTENUATION"; + case GL11.GL_QUADRATIC_ATTENUATION -> "QUADRATIC_ATTENUATION"; + default -> String.format("0x%X", pname); + }; + } + + public static String getMaterialPnameName(int pname) { + return switch (pname) { + case GL11.GL_AMBIENT -> "AMBIENT"; + case GL11.GL_DIFFUSE -> "DIFFUSE"; + case GL11.GL_SPECULAR -> "SPECULAR"; + case GL11.GL_EMISSION -> "EMISSION"; + case GL11.GL_SHININESS -> "SHININESS"; + case GL11.GL_AMBIENT_AND_DIFFUSE -> "AMBIENT_AND_DIFFUSE"; + case GL11.GL_COLOR_INDEXES -> "COLOR_INDEXES"; + default -> String.format("0x%X", pname); + }; + } + + public static String getFaceName(int face) { + return switch (face) { + case GL11.GL_FRONT -> "FRONT"; + case GL11.GL_BACK -> "BACK"; + case GL11.GL_FRONT_AND_BACK -> "FRONT_AND_BACK"; + default -> String.format("0x%X", face); + }; + } + + public static String getColorMaterialModeName(int mode) { + return switch (mode) { + case GL11.GL_AMBIENT -> "AMBIENT"; + case GL11.GL_DIFFUSE -> "DIFFUSE"; + case GL11.GL_SPECULAR -> "SPECULAR"; + case GL11.GL_EMISSION -> "EMISSION"; + case GL11.GL_AMBIENT_AND_DIFFUSE -> "AMBIENT_AND_DIFFUSE"; + default -> String.format("0x%X", mode); + }; + } + + public static String getFogPnameName(int pname) { + return switch (pname) { + case GL11.GL_FOG_MODE -> "FOG_MODE"; + case GL11.GL_FOG_DENSITY -> "FOG_DENSITY"; + case GL11.GL_FOG_START -> "FOG_START"; + case GL11.GL_FOG_END -> "FOG_END"; + case GL11.GL_FOG_INDEX -> "FOG_INDEX"; + case GL11.GL_FOG_COLOR -> "FOG_COLOR"; + default -> String.format("0x%X", pname); + }; + } + + public static String getLightModelPnameName(int pname) { + return switch (pname) { + case GL11.GL_LIGHT_MODEL_AMBIENT -> "LIGHT_MODEL_AMBIENT"; + case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> "LIGHT_MODEL_LOCAL_VIEWER"; + case GL11.GL_LIGHT_MODEL_TWO_SIDE -> "LIGHT_MODEL_TWO_SIDE"; + case GL12.GL_LIGHT_MODEL_COLOR_CONTROL -> "LIGHT_MODEL_COLOR_CONTROL"; + default -> String.format("0x%X", pname); + }; + } + + public static String getClearMaskString(int mask) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + if ((mask & GL11.GL_COLOR_BUFFER_BIT) != 0) { + sb.append("COLOR"); + first = false; + } + if ((mask & GL11.GL_DEPTH_BUFFER_BIT) != 0) { + if (!first) sb.append("|"); + sb.append("DEPTH"); + first = false; + } + if ((mask & GL11.GL_STENCIL_BUFFER_BIT) != 0) { + if (!first) sb.append("|"); + sb.append("STENCIL"); + first = false; + } + if ((mask & GL11.GL_ACCUM_BUFFER_BIT) != 0) { + if (!first) sb.append("|"); + sb.append("ACCUM"); + } + return sb.toString(); + } + + public static String getCullFaceName(int mode) { + return switch (mode) { + case GL11.GL_FRONT -> "FRONT"; + case GL11.GL_BACK -> "BACK"; + case GL11.GL_FRONT_AND_BACK -> "FRONT_AND_BACK"; + default -> String.format("0x%X", mode); + }; + } + + public static String getLogicOpName(int opcode) { + return switch (opcode) { + case GL11.GL_CLEAR -> "CLEAR"; + case GL11.GL_AND -> "AND"; + case GL11.GL_AND_REVERSE -> "AND_REVERSE"; + case GL11.GL_COPY -> "COPY"; + case GL11.GL_AND_INVERTED -> "AND_INVERTED"; + case GL11.GL_NOOP -> "NOOP"; + case GL11.GL_XOR -> "XOR"; + case GL11.GL_OR -> "OR"; + case GL11.GL_NOR -> "NOR"; + case GL11.GL_EQUIV -> "EQUIV"; + case GL11.GL_INVERT -> "INVERT"; + case GL11.GL_OR_REVERSE -> "OR_REVERSE"; + case GL11.GL_COPY_INVERTED -> "COPY_INVERTED"; + case GL11.GL_OR_INVERTED -> "OR_INVERTED"; + case GL11.GL_NAND -> "NAND"; + case GL11.GL_SET -> "SET"; + default -> String.format("0x%X", opcode); + }; + } + private static DebugState debugState; private interface DebugState { diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java b/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java index 3d8d573c0..7a598eb65 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java @@ -1,9 +1,11 @@ package com.gtnewhorizons.angelica.glsm; import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; -import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VBOManager; import com.gtnewhorizons.angelica.AngelicaMod; -import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList; +import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder; +import com.gtnewhorizons.angelica.glsm.recording.commands.TexImage2DCmd; +import com.gtnewhorizons.angelica.glsm.recording.commands.TexSubImage2DCmd; import com.gtnewhorizons.angelica.glsm.stacks.AlphaStateStack; import com.gtnewhorizons.angelica.glsm.stacks.BlendStateStack; import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack; @@ -14,12 +16,16 @@ import com.gtnewhorizons.angelica.glsm.stacks.FogStateStack; import com.gtnewhorizons.angelica.glsm.stacks.IntegerStateStack; import com.gtnewhorizons.angelica.glsm.stacks.LightModelStateStack; import com.gtnewhorizons.angelica.glsm.stacks.LightStateStack; +import com.gtnewhorizons.angelica.glsm.stacks.LineStateStack; import com.gtnewhorizons.angelica.glsm.stacks.MaterialStateStack; import com.gtnewhorizons.angelica.glsm.stacks.MatrixModeStack; +import com.gtnewhorizons.angelica.glsm.stacks.PointStateStack; +import com.gtnewhorizons.angelica.glsm.stacks.PolygonStateStack; +import com.gtnewhorizons.angelica.glsm.stacks.StencilStateStack; import com.gtnewhorizons.angelica.glsm.stacks.TextureBindingStack; import com.gtnewhorizons.angelica.glsm.stacks.ViewPortStateStack; +import com.gtnewhorizons.angelica.glsm.states.ClientArrayState; import com.gtnewhorizons.angelica.glsm.states.Color4; -import com.gtnewhorizons.angelica.glsm.states.ISettableState; import com.gtnewhorizons.angelica.glsm.states.TextureBinding; import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray; import com.gtnewhorizons.angelica.glsm.texture.TextureInfo; @@ -28,13 +34,9 @@ import com.gtnewhorizons.angelica.glsm.texture.TextureTracker; import com.gtnewhorizons.angelica.hudcaching.HUDCaching; import com.gtnewhorizons.angelica.loading.AngelicaTweaker; import cpw.mods.fml.relauncher.ReflectionHelper; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntStack; -import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; -import it.unimi.dsi.fastutil.objects.ObjectArraySet; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; @@ -47,6 +49,7 @@ import net.coderbot.iris.gl.state.StateUpdateNotifiers; import net.coderbot.iris.pipeline.WorldRenderingPipeline; import net.coderbot.iris.samplers.IrisSamplers; import net.coderbot.iris.texture.pbr.PBRTextureManager; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.OpenGlHelper; import org.joml.Matrix4d; import org.joml.Matrix4f; @@ -64,6 +67,7 @@ import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GLContext; import org.lwjgl.opengl.KHRDebug; @@ -75,10 +79,8 @@ import java.nio.DoubleBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; -import java.util.AbstractMap; +import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.function.IntSupplier; import static com.gtnewhorizons.angelica.glsm.Vendor.AMD; @@ -100,12 +102,21 @@ import static com.gtnewhorizons.angelica.loading.AngelicaTweaker.LOGGER; */ @SuppressWarnings("unused") // Used in ASM public class GLStateManager { + + // Thread Checking - must be early in static init order so isMainThread() works for state initialization + @Getter private static final Thread MainThread = Thread.currentThread(); + + public static boolean isMainThread() { + return Thread.currentThread() == MainThread; + } + public static ContextCapabilities capabilities; @Getter protected static boolean poppingAttributes; public static boolean BYPASS_CACHE = Boolean.parseBoolean(System.getProperty("angelica.disableGlCache", "false")); - public static final int MAX_ATTRIB_STACK_DEPTH = GL11.glGetInteger(GL11.GL_MAX_ATTRIB_STACK_DEPTH); - public static final int MAX_MODELVIEW_STACK_DEPTH = GL11.glGetInteger(GL11.GL_MAX_MODELVIEW_STACK_DEPTH); + // +2 headroom for internal operations (display list compilation) that push cache but not driver + public static final int MAX_ATTRIB_STACK_DEPTH = GL11.glGetInteger(GL11.GL_MAX_ATTRIB_STACK_DEPTH) + 2; + public static final int MAX_MODELVIEW_STACK_DEPTH = GL11.glGetInteger(GL11.GL_MAX_MODELVIEW_STACK_DEPTH) + 2; public static final int MAX_PROJECTION_STACK_DEPTH = GL11.glGetInteger(GL11.GL_MAX_PROJECTION_STACK_DEPTH); public static final int MAX_TEXTURE_STACK_DEPTH = GL11.glGetInteger(GL11.GL_MAX_TEXTURE_STACK_DEPTH); public static final int MAX_TEXTURE_UNITS = GL11.glGetInteger(GL20.GL_MAX_TEXTURE_IMAGE_UNITS); @@ -118,11 +129,45 @@ public class GLStateManager { // This setting varies depending on driver, so it gets queried at runtime public static final int DEFAULT_DRAW_BUFFER = GL11.glGetInteger(GL11.GL_DRAW_BUFFER); + + private static Thread CurrentThread = MainThread; + @Setter @Getter private static boolean runningSplash = true; + + private static volatile boolean splashComplete = false; + private static volatile Thread drawableGLHolder = MainThread; + // Reference to SharedDrawable so makeCurrent() can distinguish it from DrawableGL + @Setter private static volatile Drawable sharedDrawable = null; + + public static boolean isCachingEnabled() { + if (splashComplete) return true; + return Thread.currentThread() == drawableGLHolder; + } + // GLStateManager State Trackers private static final IntStack attribs = new IntArrayList(MAX_ATTRIB_STACK_DEPTH); + + // Lazy copy-on-write tracking: only iterate modified states during popState + @Getter private static int attribDepth = 0; + @SuppressWarnings("unchecked") + private static final List>[] modifiedAtDepth = new List[MAX_ATTRIB_STACK_DEPTH]; + static { + for (int i = 0; i < MAX_ATTRIB_STACK_DEPTH; i++) { + modifiedAtDepth[i] = new ArrayList<>(); + } + } + + /** Register a state stack as modified at the current depth (called from beforeModify). */ + public static void registerModifiedState(IStateStack stack) { + if (attribDepth > 0) { + modifiedAtDepth[attribDepth - 1].add(stack); + } + } protected static final IntegerStateStack activeTextureUnit = new IntegerStateStack(0); protected static final IntegerStateStack shadeModelState = new IntegerStateStack(GL11.GL_SMOOTH); + // Client array state (not push/pop tracked, but needed for display list compilation) + @Getter private static final ClientArrayState clientArrayState = new ClientArrayState(); + @Getter protected static final TextureUnitArray textures = new TextureUnitArray(); @Getter protected static final BlendStateStack blendState = new BlendStateStack(); @Getter protected static final BooleanStateStack blendMode = new BooleanStateStack(GL11.GL_BLEND); @@ -159,11 +204,23 @@ public class GLStateManager { @Getter protected static final BooleanStateStack colorLogicOpState = new BooleanStateStack(GL11.GL_COLOR_LOGIC_OP); @Getter protected static final BooleanStateStack indexLogicOpState = new BooleanStateStack(GL11.GL_INDEX_LOGIC_OP); - // Polygon offset states + // Polygon offset states (enable bits) @Getter protected static final BooleanStateStack polygonOffsetPointState = new BooleanStateStack(GL11.GL_POLYGON_OFFSET_POINT); @Getter protected static final BooleanStateStack polygonOffsetLineState = new BooleanStateStack(GL11.GL_POLYGON_OFFSET_LINE); @Getter protected static final BooleanStateStack polygonOffsetFillState = new BooleanStateStack(GL11.GL_POLYGON_OFFSET_FILL); + // Line state (GL_LINE_BIT) + @Getter protected static final LineStateStack lineState = new LineStateStack(); + + // Point state (GL_POINT_BIT) + @Getter protected static final PointStateStack pointState = new PointStateStack(); + + // Polygon state (GL_POLYGON_BIT) - mode, offset values, cull face mode, front face + @Getter protected static final PolygonStateStack polygonState = new PolygonStateStack(); + + // Stencil state (GL_STENCIL_BUFFER_BIT) + @Getter protected static final StencilStateStack stencilState = new StencilStateStack(); + // Evaluator states @Getter protected static final BooleanStateStack autoNormalState = new BooleanStateStack(GL11.GL_AUTO_NORMAL); @Getter protected static final BooleanStateStack map1Color4State = new BooleanStateStack(GL11.GL_MAP1_COLOR_4); @@ -256,18 +313,16 @@ public class GLStateManager { private static Runnable fogEndListener = null; private static Runnable fogDensityListener = null; - // Thread Checking - @Getter private static final Thread MainThread = Thread.currentThread(); - private static Thread CurrentThread = MainThread; - @Setter @Getter private static boolean runningSplash = true; - - private static int glListMode = 0; - private static int glListNesting = 0; - private static int glListId = -1; - private static final Map, ISettableState> glListStates = new Object2ObjectArrayMap<>(); - private static final Int2ObjectMap, ISettableState>>> glListChanges = new Int2ObjectOpenHashMap<>(); - - + /** + * Check if a display list exists (has been compiled and stored). + * Delegates to DisplayListManager. + * + * @param list The display list ID to check + * @return true if the display list exists, false otherwise + */ + public static boolean displayListExists(int list) { + return DisplayListManager.displayListExists(list); + } public static class GLFeatureSet extends IntOpenHashSet { @@ -310,21 +365,26 @@ public class GLStateManager { .addFeature(GL11.GL_TEXTURE_MATRIX) .addFeature(GL11.GL_VIEWPORT); - String glVendor = GL11.glGetString(GL11.GL_VENDOR); + final String glVendor = GL11.glGetString(GL11.GL_VENDOR); VENDOR = Vendor.getVendor(glVendor.toLowerCase()); if (vendorIsAMD()) { // AMD Drivers seem to default to 0 for the matrix mode, so we need to set it to the default GL11.glMatrixMode(GL11.GL_MODELVIEW); } + + final Minecraft mc = Minecraft.getMinecraft(); + if (mc != null) { + // Initialize viewport state from display dimensions. + // After Display.create(), viewport is (0, 0, width, height) + viewportState.setViewPort(0, 0, mc.displayWidth, mc.displayHeight); + } } public static void init() { - - RenderSystem.initRenderer(); - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { StateUpdateNotifiers.blendFuncNotifier = listener -> blendFuncListener = listener; StateUpdateNotifiers.fogToggleNotifier = listener -> fogToggleListener = listener; StateUpdateNotifiers.fogModeNotifier = listener -> fogModeListener = listener; @@ -343,12 +403,8 @@ public class GLStateManager { GLDebug.debugMessage("Angelica Debug Annotator Initialized"); } - } - public static boolean isMainThread() { - return Thread.currentThread() == MainThread; - } public static void assertMainThread() { if (Thread.currentThread() != CurrentThread && !runningSplash) { @@ -357,11 +413,28 @@ public class GLStateManager { } public static boolean shouldBypassCache() { - return BYPASS_CACHE || runningSplash; + // Bypass cache when not using the main DrawableGL context + 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) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordEnable(cap); + } // Handle clip planes dynamically (supports up to MAX_CLIP_PLANES) if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) { clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].enable(); @@ -434,6 +507,9 @@ public class GLStateManager { } public static void glDisable(int cap) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordDisable(cap); + } // Handle clip planes dynamically (supports up to MAX_CLIP_PLANES) if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) { clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].disable(); @@ -679,7 +755,8 @@ public class GLStateManager { @SneakyThrows public static int getMatrixStackDepth(Matrix4fStack stack) { - return (int) MAT4_STACK_CURR_DEPTH.invokeExact(stack); + // JOML's curr is 0-based (0 = no pushes), but OpenGL starts at 1 (base matrix counts) + return (int) MAT4_STACK_CURR_DEPTH.invokeExact(stack) + 1; } public static int glGetInteger(int pname) { @@ -690,7 +767,7 @@ public class GLStateManager { return switch (pname) { case GL11.GL_ALPHA_TEST_FUNC -> alphaState.getFunction(); case GL11.GL_DEPTH_FUNC -> depthState.getFunc(); - case GL11.GL_LIST_MODE -> glListMode; + case GL11.GL_LIST_MODE -> DisplayListManager.getListMode(); case GL11.GL_MATRIX_MODE -> matrixMode.getMode(); case GL11.GL_SHADE_MODEL -> shadeModelState.getValue(); case GL11.GL_TEXTURE_BINDING_2D -> getBoundTexture(); @@ -732,7 +809,7 @@ public class GLStateManager { return; } - MaterialStateStack state; + final MaterialStateStack state; if (face == GL11.GL_FRONT) { state = frontMaterial; } else if (face == GL11.GL_BACK) { @@ -758,7 +835,7 @@ public class GLStateManager { return; } - LightStateStack state = lightDataStates[light - GL11.GL_LIGHT0]; + final LightStateStack state = lightDataStates[light - GL11.GL_LIGHT0]; switch (pname) { case GL11.GL_AMBIENT -> state.ambient.get(0, params); case GL11.GL_DIFFUSE -> state.diffuse.get(0, params); @@ -803,11 +880,14 @@ public class GLStateManager { // GLStateManager Functions public static void glBlendColor(float red, float green, float blue, float alpha) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glBlendColor in display lists not yet implemented - if you see this, please report!"); + } GL14.glBlendColor(red, green, blue, alpha); } public static void enableBlend() { - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { if (BlendModeStorage.isBlendLocked()) { BlendModeStorage.deferBlendModeToggle(true); return; @@ -817,7 +897,7 @@ public class GLStateManager { } public static void disableBlend() { - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { if (BlendModeStorage.isBlendLocked()) { BlendModeStorage.deferBlendModeToggle(false); return; @@ -835,24 +915,32 @@ public class GLStateManager { } public static void glBlendFunc(int srcFactor, int dstFactor) { - if (AngelicaConfig.enableIris) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordBlendFunc(srcFactor, dstFactor, srcFactor, dstFactor); + } + if (Iris.enabled) { if (BlendModeStorage.isBlendLocked()) { BlendModeStorage.deferBlendFunc(srcFactor, dstFactor, srcFactor, dstFactor); return; } } + // Cache thread check - only update state on main thread, but always make GL call if needed + final boolean caching = isCachingEnabled(); if (HUDCaching.renderingCacheOverride) { - blendState.setSrcRgb(srcFactor); - blendState.setDstRgb(dstFactor); - blendState.setSrcAlpha(GL11.GL_ONE); - blendState.setDstAlpha(GL11.GL_ONE_MINUS_SRC_ALPHA); - OpenGlHelper.glBlendFunc(srcFactor, dstFactor, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + if (caching) blendState.setAll(srcFactor, dstFactor, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + // Skip actual GL call during display list recording (state tracking only) + if (!isRecordingDisplayList()) { + OpenGlHelper.glBlendFunc(srcFactor, dstFactor, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); + } return; } - if (shouldBypassCache() || blendState.getSrcRgb() != srcFactor || blendState.getDstRgb() != dstFactor) { - blendState.setSrcRgb(srcFactor); - blendState.setDstRgb(dstFactor); - GL11.glBlendFunc(srcFactor, dstFactor); + final boolean bypass = BYPASS_CACHE || !caching; + if (bypass || blendState.getSrcRgb() != srcFactor || blendState.getDstRgb() != dstFactor) { + if (caching) blendState.setSrcDstRgb(srcFactor, dstFactor); + // Skip actual GL call during display list recording (state tracking only) + if (!isRecordingDisplayList()) { + GL11.glBlendFunc(srcFactor, dstFactor); + } } // Iris @@ -860,15 +948,24 @@ public class GLStateManager { } public static void glBlendEquation(int mode) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glBlendEquation in display lists not yet implemented - if you see this, please report!"); + } GL14.glBlendEquation(mode); } public static void glBlendEquationSeparate(int modeRGB, int modeAlpha) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glBlendEquationSeparate in display lists not yet implemented - if you see this, please report!"); + } GL20.glBlendEquationSeparate(modeRGB, modeAlpha); } public static void tryBlendFuncSeparate(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { - if (AngelicaConfig.enableIris) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); + } + if (Iris.enabled) { if (BlendModeStorage.isBlendLocked()) { BlendModeStorage.deferBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); return; @@ -878,50 +975,104 @@ public class GLStateManager { srcAlpha = GL11.GL_ONE; dstAlpha = GL11.GL_ONE_MINUS_SRC_ALPHA; } - if (shouldBypassCache() || blendState.getSrcRgb() != srcRgb || blendState.getDstRgb() != dstRgb || blendState.getSrcAlpha() - != srcAlpha || blendState.getDstAlpha() != dstAlpha) { - blendState.setSrcRgb(srcRgb); - blendState.setDstRgb(dstRgb); - blendState.setSrcAlpha(srcAlpha); - blendState.setDstAlpha(dstAlpha); - OpenGlHelper.glBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); + // Cache thread check - only update state on main thread, but always make GL call if needed + final boolean caching = isCachingEnabled(); + final boolean bypass = BYPASS_CACHE || !caching; + if (bypass || blendState.getSrcRgb() != srcRgb || blendState.getDstRgb() != dstRgb || blendState.getSrcAlpha() != srcAlpha || blendState.getDstAlpha() != dstAlpha) { + if (caching) blendState.setAll(srcRgb, dstRgb, srcAlpha, dstAlpha); + // Skip actual GL call during display list recording (state tracking only) + if (!isRecordingDisplayList()) { + OpenGlHelper.glBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); + } } // Iris if (blendFuncListener != null) blendFuncListener.run(); } + public static void checkCompiling() { + checkCompiling(""); + } + public static void checkCompiling(String name) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("Not yet implemented (" + name + ")"); + } + } public static void glNormal3b(byte nx, byte ny, byte nz) { - GL11.glNormal3b(nx, ny, nz); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordNormal(nx, ny, nz); + // Also update immediate mode recorder for glBegin/glEnd capture + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setNormal(b2f(nx), b2f(ny), b2f(nz)); + } + } else { + GL11.glNormal3b(nx, ny, nz); + } } public static void glNormal3d(double nx, double ny, double nz) { - GL11.glNormal3d(nx, ny, nz); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordNormal((float)nx, (float)ny, (float)nz); + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setNormal((float) nx, (float) ny, (float) nz); + } + } else { + GL11.glNormal3d(nx, ny, nz); + } } public static void glNormal3f(float nx, float ny, float nz) { - GL11.glNormal3f(nx, ny, nz); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordNormal(nx, ny, nz); + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setNormal(nx, ny, nz); + } + } else { + GL11.glNormal3f(nx, ny, nz); + } } public static void glNormal3i(int nx, int ny, int nz) { - GL11.glNormal3i(nx, ny, nz); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordNormal(nx, ny, nz); + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setNormal((float) nx, (float) ny, (float) nz); + } + } else { + GL11.glNormal3i(nx, ny, nz); + } } public static void glDepthFunc(int func) { - if (shouldBypassCache() || func != depthState.getFunc() ) { - depthState.setFunc(func); - GL11.glDepthFunc(func); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordDepthFunc(func); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || func != depthState.getFunc()) { + if (caching) depthState.setFunc(func); + if (!isRecordingDisplayList()) { + GL11.glDepthFunc(func); + } } } public static void glDepthMask(boolean mask) { - if (AngelicaConfig.enableIris) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordDepthMask(mask); + } + if (Iris.enabled) { if (DepthColorStorage.isDepthColorLocked()) { DepthColorStorage.deferDepthEnable(mask); return; } } - - if (shouldBypassCache() || mask != depthState.isEnabled()) { - depthState.setEnabled(mask); - GL11.glDepthMask(mask); + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || mask != depthState.isEnabled()) { + if (caching) depthState.setEnabled(mask); + if (!isRecordingDisplayList()) { + GL11.glDepthMask(mask); + } } } @@ -930,50 +1081,90 @@ public class GLStateManager { } public static void glColor4f(float red, float green, float blue, float alpha) { - if (changeColor(red, green, blue, alpha)) { - GL11.glColor4f(red, green, blue, alpha); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor(red, green, blue, alpha); + changeColor(red, green, blue, alpha); // Update state for glGet queries + } else { + if (changeColor(red, green, blue, alpha)) { + GL11.glColor4f(red, green, blue, alpha); + } } } public static void glColor4d(double red, double green, double blue, double alpha) { - if (changeColor((float) red, (float) green, (float) blue, (float) alpha)) { - GL11.glColor4d(red, green, blue, alpha); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor((float) red, (float) green, (float) blue, (float) alpha); + changeColor((float) red, (float) green, (float) blue, (float) alpha); // Update state for glGet queries + } else { + if (changeColor((float) red, (float) green, (float) blue, (float) alpha)) { + GL11.glColor4d(red, green, blue, alpha); + } } } public static void glColor4b(byte red, byte green, byte blue, byte alpha) { - if (changeColor(b2f(red), b2f(green), b2f(blue), b2f(alpha))) { - GL11.glColor4b(red, green, blue, alpha); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor(b2f(red), b2f(green), b2f(blue), b2f(alpha)); + changeColor(b2f(red), b2f(green), b2f(blue), b2f(alpha)); // Update state for glGet queries + } else { + if (changeColor(b2f(red), b2f(green), b2f(blue), b2f(alpha))) { + GL11.glColor4b(red, green, blue, alpha); + } } } public static void glColor4ub(byte red, byte green, byte blue, byte alpha) { - if (changeColor(ub2f(red), ub2f(green), ub2f(blue), ub2f(alpha))) { - GL11.glColor4ub(red, green, blue, alpha); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor(ub2f(red), ub2f(green), ub2f(blue), ub2f(alpha)); + changeColor(ub2f(red), ub2f(green), ub2f(blue), ub2f(alpha)); // Update state for glGet queries + } else { + if (changeColor(ub2f(red), ub2f(green), ub2f(blue), ub2f(alpha))) { + GL11.glColor4ub(red, green, blue, alpha); + } } } public static void glColor3f(float red, float green, float blue) { - if (changeColor(red, green, blue, 1.0F)) { - GL11.glColor3f(red, green, blue); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor(red, green, blue, 1.0F); + changeColor(red, green, blue, 1.0F); // Update state for glGet queries + } else { + if (changeColor(red, green, blue, 1.0F)) { + GL11.glColor3f(red, green, blue); + } } } public static void glColor3d(double red, double green, double blue) { - if (changeColor((float) red, (float) green, (float) blue, 1.0F)) { - GL11.glColor3d(red, green, blue); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor((float) red, (float) green, (float) blue, 1.0F); + changeColor((float) red, (float) green, (float) blue, 1.0F); // Update state for glGet queries + } else { + if (changeColor((float) red, (float) green, (float) blue, 1.0F)) { + GL11.glColor3d(red, green, blue); + } } } public static void glColor3b(byte red, byte green, byte blue) { - if (changeColor(b2f(red), b2f(green), b2f(blue), 1.0F)) { - GL11.glColor3b(red, green, blue); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor(b2f(red), b2f(green), b2f(blue), 1.0F); + changeColor(b2f(red), b2f(green), b2f(blue), 1.0F); // Update state for immediate mode vertex capture + } else { + if (changeColor(b2f(red), b2f(green), b2f(blue), 1.0F)) { + GL11.glColor3b(red, green, blue); + } } } public static void glColor3ub(byte red, byte green, byte blue) { - if (changeColor(ub2f(red), ub2f(green), ub2f(blue), 1.0F)) { - GL11.glColor3ub(red, green, blue); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColor(ub2f(red), ub2f(green), ub2f(blue), 1.0F); + changeColor(ub2f(red), ub2f(green), ub2f(blue), 1.0F); // Update state for immediate mode vertex capture + } else { + if (changeColor(ub2f(red), ub2f(green), ub2f(blue), 1.0F)) { + GL11.glColor3ub(red, green, blue); + } } } @@ -985,15 +1176,18 @@ public class GLStateManager { return ((b - Byte.MIN_VALUE) & 0xFF) / 255.0F; } - public static float i2f(int i) { return ((i - Integer.MIN_VALUE) & 0xFFFFFF) / 4294967295.0F; } + public static float i2f(int i) { return ((i - Integer.MIN_VALUE) & 0xFFFFFFFFL) / 4294967295.0F; } private static boolean changeColor(float red, float green, float blue, float alpha) { // Helper function for glColor* - if (shouldBypassCache() || red != color.getRed() || green != color.getGreen() || blue != color.getBlue() || alpha != color.getAlpha()) { - color.setRed(red); - color.setGreen(green); - color.setBlue(blue); - color.setAlpha(alpha); + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || red != color.getRed() || green != color.getGreen() || blue != color.getBlue() || alpha != color.getAlpha()) { + if (caching) { + color.setRed(red); + color.setGreen(green); + color.setBlue(blue); + color.setAlpha(alpha); + } return true; } return false; @@ -1006,39 +1200,59 @@ public class GLStateManager { } public static void glColorMask(boolean red, boolean green, boolean blue, boolean alpha) { - if (AngelicaConfig.enableIris) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColorMask(red, green, blue, alpha); + } + if (Iris.enabled) { if (DepthColorStorage.isDepthColorLocked()) { DepthColorStorage.deferColorMask(red, green, blue, alpha); return; } } - if (shouldBypassCache() || red != colorMask.red || green != colorMask.green || blue != colorMask.blue || alpha != colorMask.alpha) { - colorMask.red = red; - colorMask.green = green; - colorMask.blue = blue; - colorMask.alpha = alpha; - GL11.glColorMask(red, green, blue, alpha); + // Cache thread check - only update state on main thread, but always make GL call if needed + final boolean caching = isCachingEnabled(); + final boolean bypass = BYPASS_CACHE || !caching; + if (bypass || red != colorMask.red || green != colorMask.green || blue != colorMask.blue || alpha != colorMask.alpha) { + if (caching) colorMask.setAll(red, green, blue, alpha); + if (!isRecordingDisplayList()) { + GL11.glColorMask(red, green, blue, alpha); + } } } // Clear Color public static void glClearColor(float red, float green, float blue, float alpha) { - if (shouldBypassCache() || red != clearColor.getRed() || green != clearColor.getGreen() || blue != clearColor.getBlue() || alpha != clearColor.getAlpha()) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordClearColor(red, green, blue, alpha); + // Update state for glGet queries (recording only happens on main thread) clearColor.setRed(red); clearColor.setGreen(green); clearColor.setBlue(blue); clearColor.setAlpha(alpha); - GL11.glClearColor(red, green, blue, alpha); + } else { + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || red != clearColor.getRed() || green != clearColor.getGreen() || blue != clearColor.getBlue() || alpha != clearColor.getAlpha()) { + if (caching) { + clearColor.setRed(red); + clearColor.setGreen(green); + clearColor.setBlue(blue); + clearColor.setAlpha(alpha); + } + GL11.glClearColor(red, green, blue, alpha); + } } } public static void glClearDepth(double depth) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glClearDepth in display lists not yet implemented - if you see this, please report!"); + } GL11.glClearDepth(depth); } // ALPHA public static void enableAlphaTest() { - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { if (AlphaTestStorage.isAlphaTestLocked()) { AlphaTestStorage.deferAlphaTestToggle(true); return; @@ -1048,7 +1262,7 @@ public class GLStateManager { } public static void disableAlphaTest() { - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { if (AlphaTestStorage.isAlphaTestLocked()) { AlphaTestStorage.deferAlphaTestToggle(false); return; @@ -1058,30 +1272,53 @@ public class GLStateManager { } public static void glAlphaFunc(int function, float reference) { - if (AngelicaConfig.enableIris) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordAlphaFunc(function, reference); + } + if (Iris.enabled) { if (AlphaTestStorage.isAlphaTestLocked()) { AlphaTestStorage.deferAlphaFunc(function, reference); return; } } - alphaState.setFunction(function); - alphaState.setReference(reference); - GL11.glAlphaFunc(function, reference); + if (isCachingEnabled()) { + alphaState.setFunction(function); + alphaState.setReference(reference); + } + if (!isRecordingDisplayList()) { + GL11.glAlphaFunc(function, reference); + } } // Textures public static void glActiveTexture(int texture) { + // Recording mode: record command and update state tracking (only on main thread) + if (DisplayListManager.isRecording()) { + DisplayListManager.recordActiveTexture(texture); + final int newTexture = texture - GL13.GL_TEXTURE0; + activeTextureUnit.setValue(newTexture); // Update state for glGet queries + return; // Don't execute during recording + } final int newTexture = texture - GL13.GL_TEXTURE0; - if (shouldBypassCache() || getActiveTextureUnit() != newTexture) { - activeTextureUnit.setValue(newTexture); + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || getActiveTextureUnit() != newTexture) { + if (caching) activeTextureUnit.setValue(newTexture); GL13.glActiveTexture(texture); } } public static void glActiveTextureARB(int texture) { + // Recording mode: record command and update state tracking (only on main thread) + if (DisplayListManager.isRecording()) { + DisplayListManager.recordActiveTexture(texture); + final int newTexture = texture - GL13.GL_TEXTURE0; + activeTextureUnit.setValue(newTexture); // Update state for glGet queries + return; // Don't execute during recording + } final int newTexture = texture - GL13.GL_TEXTURE0; - if (shouldBypassCache() || getActiveTextureUnit() != newTexture) { - activeTextureUnit.setValue(newTexture); + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || getActiveTextureUnit() != newTexture) { + if (caching) activeTextureUnit.setValue(newTexture); ARBMultitexture.glActiveTextureARB(texture); } } @@ -1095,68 +1332,172 @@ public class GLStateManager { } public static void glBindTexture(int target, int texture) { - if(target != GL11.GL_TEXTURE_2D) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordBindTexture(target, texture); + } + if (target != GL11.GL_TEXTURE_2D) { // We're only supporting 2D textures for now - GL11.glBindTexture(target, texture); + if (!isRecordingDisplayList()) { + GL11.glBindTexture(target, texture); + } + LOGGER.info("SKIPPING glBindTexture for target {}", target); return; } - final TextureBinding textureUnit = textures.getTextureUnitBindings(GLStateManager.activeTextureUnit.getValue()); + final int activeUnit = GLStateManager.activeTextureUnit.getValue(); + final TextureBinding textureUnit = textures.getTextureUnitBindings(activeUnit); + final int cachedBinding = textureUnit.getBinding(); + final boolean bypassCache = shouldBypassCache(); + final boolean cacheMatches = cachedBinding == texture; + final boolean willBind = bypassCache || !cacheMatches; - if (shouldBypassCache() || textureUnit.getBinding() != texture) { - GL11.glBindTexture(target, texture); - textureUnit.setBinding(texture); - TextureTracker.INSTANCE.onBindTexture(texture); + if (willBind) { + if (!isRecordingDisplayList()) { + GL11.glBindTexture(target, texture); + } + if (isCachingEnabled()) { + textureUnit.setBinding(texture); + TextureTracker.INSTANCE.onBindTexture(texture); + } } } public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordComplexCommand(TexImage2DCmd.fromIntBuffer(target, level, internalformat, width, height, border, format, type, pixels)); + } + // Always update cache for glGet queries TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); - GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + if (shouldUseDSA(target)) { + // Use DSA to upload directly to the texture + RenderSystem.textureImage2D(getBoundTexture(), target, level, internalformat, width, height, border, format, type, pixels); + } else { + // Non-main thread or proxy texture - use direct GL call + GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + } } public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, FloatBuffer pixels) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordComplexCommand(TexImage2DCmd.fromFloatBuffer(target, level, internalformat, width, height, border, format, type, pixels)); + } + // Always update cache for glGet queries TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + // FloatBuffer not in DSA interface - use direct GL call GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); } public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, DoubleBuffer pixels) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordComplexCommand(TexImage2DCmd.fromDoubleBuffer(target, level, internalformat, width, height, border, format, type, pixels)); + } + // Always update cache for glGet queries TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + // DoubleBuffer not in DSA interface - use direct GL call GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); } public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) { - TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels != null ? pixels.asIntBuffer() : (IntBuffer) null); - GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordComplexCommand(TexImage2DCmd.fromByteBuffer(target, level, internalformat, width, height, border, format, type, pixels)); + } + // Always update cache for glGet queries + TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels != null ? pixels.asIntBuffer() : null); + if (shouldUseDSA(target)) { + // Use DSA to upload directly to the texture - keeps GL binding state unchanged + RenderSystem.textureImage2D(getBoundTexture(), target, level, internalformat, width, height, border, format, type, pixels); + } else { + // Non-main thread or proxy texture - use direct GL call + GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + } } public static void glTexImage2D(int target, int level, int internalformat, int width, int height, int border, int format, int type, long pixels_buffer_offset) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glTexImage2D with buffer offset in display lists not yet supported"); + } TextureInfoCache.INSTANCE.onTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset); GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels_buffer_offset); } public static void glTexCoord1f(float s) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord(s, 0.0f); + } + return; + } GL11.glTexCoord1f(s); } public static void glTexCoord1d(double s) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord((float) s, 0.0f); + } + return; + } GL11.glTexCoord1d(s); } public static void glTexCoord2f(float s, float t) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord(s, t); + } + return; + } GL11.glTexCoord2f(s, t); } public static void glTexCoord2d(double s, double t) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord((float) s, (float) t); + } + return; + } GL11.glTexCoord2d(s, t); } public static void glTexCoord3f(float s, float t, float r) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord(s, t); // Only track s,t for 2D textures + } + return; + } GL11.glTexCoord3f(s, t, r); } public static void glTexCoord3d(double s, double t, double r) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord((float) s, (float) t); + } + return; + } GL11.glTexCoord3d(s, t, r); } public static void glTexCoord4f(float s, float t, float r, float q) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord(s, t); + } + return; + } GL11.glTexCoord4f(s, t, r, q); } public static void glTexCoord4d(double s, double t, double r, double q) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.setTexCoord((float) s, (float) t); + } + return; + } GL11.glTexCoord4d(s, t, r, q); } @@ -1175,8 +1516,11 @@ public class GLStateManager { } public static void enableTexture() { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordEnable(GL11.GL_TEXTURE_2D); + } final int textureUnit = getActiveTextureUnit(); - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { // Iris boolean updatePipeline = false; if (textureUnit == IrisSamplers.ALBEDO_TEXTURE_UNIT) { @@ -1195,8 +1539,11 @@ public class GLStateManager { } public static void disableTexture() { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordDisable(GL11.GL_TEXTURE_2D); + } final int textureUnit = getActiveTextureUnit(); - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { // Iris boolean updatePipeline = false; if (textureUnit == IrisSamplers.ALBEDO_TEXTURE_UNIT) { @@ -1215,36 +1562,57 @@ public class GLStateManager { } public static void glRasterPos2f(float x, float y) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glRasterPos in display lists not yet implemented"); + } GL11.glRasterPos2f(x, y); } public static void glRasterPos2d(double x, double y) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glRasterPos in display lists not yet implemented"); + } GL11.glRasterPos2d(x, y); } public static void glRasterPos2i(int x, int y) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glRasterPos in display lists not yet implemented"); + } GL11.glRasterPos2i(x, y); } public static void glRasterPos3f(float x, float y, float z) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glRasterPos in display lists not yet implemented"); + } GL11.glRasterPos3f(x, y, z); } public static void glRasterPos3d(double x, double y, double z) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glRasterPos in display lists not yet implemented"); + } GL11.glRasterPos3d(x, y, z); } public static void glRasterPos3i(int x, int y, int z) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glRasterPos in display lists not yet implemented"); + } GL11.glRasterPos3i(x, y, z); } public static void glRasterPos4f(float x, float y, float z, float w) { + checkCompiling("glRasterPos"); GL11.glRasterPos4f(x, y, z, w); } public static void glRasterPos4d(double x, double y, double z, double w) { + checkCompiling("glRasterPos"); GL11.glRasterPos4d(x, y, z, w); } public static void glRasterPos4i(int x, int y, int z, int w) { + checkCompiling("glRasterPos"); GL11.glRasterPos4i(x, y, z, w); } public static void setFilter(boolean bilinear, boolean mipmap) { - int i, j; + final int i, j; if (bilinear) { i = mipmap ? GL11.GL_LINEAR_MIPMAP_LINEAR : GL11.GL_LINEAR; j = GL11.GL_LINEAR; @@ -1257,52 +1625,208 @@ public class GLStateManager { } public static void trySyncProgram() { - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { Iris.getPipelineManager().getPipeline().ifPresent(WorldRenderingPipeline::syncProgram); } } public static void glBegin(int mode) { + if (DisplayListManager.isRecording()) { + // Record to immediate mode recorder + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.begin(mode); + } + return; // Don't call actual GL during recording + } trySyncProgram(); GL11.glBegin(mode); } + public static void glEnd() { + if (DisplayListManager.isRecording()) { + // Record to immediate mode recorder and flush geometry immediately + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + // end() returns quads immediately (like tessellator callback) + final ImmediateModeRecorder.Result result = recorder.end(); + if (result != null) { + // Add draw at current command position for correct interleaving + DisplayListManager.addImmediateModeDraw(result); + } + } + return; // Don't call actual GL during recording + } + GL11.glEnd(); + } + + // Vertex methods for display list recording + private static boolean loggedVertex = false; + public static void glVertex2f(float x, float y) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.vertex(x, y, 0.0f); + } + return; + } + GL11.glVertex2f(x, y); + } + + public static void glVertex2d(double x, double y) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.vertex((float) x, (float) y, 0.0f); + } + return; + } + GL11.glVertex2d(x, y); + } + + public static void glVertex3f(float x, float y, float z) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.vertex(x, y, z); + } + return; + } + GL11.glVertex3f(x, y, z); + } + + public static void glVertex3d(double x, double y, double z) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + recorder.vertex((float) x, (float) y, (float) z); + } + return; + } + GL11.glVertex3d(x, y, z); + } public static void glDrawElements(int mode, ByteBuffer indices) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!"); + } trySyncProgram(); GL11.glDrawElements(mode, indices); } public static void glDrawElements(int mode, IntBuffer indices) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!"); + } trySyncProgram(); GL11.glDrawElements(mode, indices); } public static void glDrawElements(int mode, ShortBuffer indices) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!"); + } trySyncProgram(); GL11.glDrawElements(mode, indices); } public static void glDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!"); + } trySyncProgram(); GL11.glDrawElements(mode, indices_count, type, indices_buffer_offset); } public static void glDrawElements(int mode, int count, int type, ByteBuffer indices) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glDrawElements in display lists not yet implemented - if you see this, please report!"); + } trySyncProgram(); GL11.glDrawElements(mode, count, type, indices); } public static void glDrawBuffer(int mode) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glDrawBuffer in display lists not yet implemented"); + } trySyncProgram(); GL11.glDrawBuffer(mode); } public static void glDrawArrays(int mode, int first, int count) { + if (DisplayListManager.isRecording()) { + final ImmediateModeRecorder recorder = DisplayListManager.getImmediateModeRecorder(); + if (recorder != null) { + final ImmediateModeRecorder.Result result = recorder.processDrawArrays(mode, first, count); + if (result != null) { + DisplayListManager.addImmediateModeDraw(result); + } + } + return; + } trySyncProgram(); GL11.glDrawArrays(mode, first, count); } + // Client array state interception methods (for glDrawArrays conversion) + // State is tracked globally in clientArrayState, which ImmediateModeRecorder reads from directly. + + public static void glVertexPointer(int size, int stride, FloatBuffer pointer) { + clientArrayState.setVertexPointer(size, GL11.GL_FLOAT, stride, pointer); + GL11.glVertexPointer(size, stride, pointer); + } + + public static void glVertexPointer(int size, int type, int stride, ByteBuffer pointer) { + clientArrayState.setVertexPointer(size, type, stride, pointer); + GL11.glVertexPointer(size, type, stride, pointer); + } + + public static void glVertexPointer(int size, int type, int stride, long pointer_buffer_offset) { + // VBO offset - can't track buffer reference, clear it + clientArrayState.setVertexPointer(size, type, stride, null); + GL11.glVertexPointer(size, type, stride, pointer_buffer_offset); + } + + public static void glColorPointer(int size, int stride, FloatBuffer pointer) { + clientArrayState.setColorPointer(size, GL11.GL_FLOAT, stride, pointer); + GL11.glColorPointer(size, stride, pointer); + } + + public static void glColorPointer(int size, int type, int stride, ByteBuffer pointer) { + clientArrayState.setColorPointer(size, type, stride, pointer); + GL11.glColorPointer(size, type, stride, pointer); + } + + public static void glColorPointer(int size, boolean unsigned, int stride, ByteBuffer pointer) { + final int type = unsigned ? GL11.GL_UNSIGNED_BYTE : GL11.GL_BYTE; + clientArrayState.setColorPointer(size, type, stride, pointer); + GL11.glColorPointer(size, unsigned, stride, pointer); + } + + public static void glColorPointer(int size, int type, int stride, long pointer_buffer_offset) { + // VBO offset - can't track buffer reference, clear it + clientArrayState.setColorPointer(size, type, stride, null); + GL11.glColorPointer(size, type, stride, pointer_buffer_offset); + } + + public static void glEnableClientState(int cap) { + if (cap == GL11.GL_VERTEX_ARRAY) clientArrayState.setVertexArrayEnabled(true); + else if (cap == GL11.GL_COLOR_ARRAY) clientArrayState.setColorArrayEnabled(true); + GL11.glEnableClientState(cap); + } + + public static void glDisableClientState(int cap) { + if (cap == GL11.GL_VERTEX_ARRAY) clientArrayState.setVertexArrayEnabled(false); + else if (cap == GL11.GL_COLOR_ARRAY) clientArrayState.setColorArrayEnabled(false); + GL11.glDisableClientState(cap); + } + public static void glLogicOp(int opcode) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLogicOp(opcode); + return; + } GL11.glLogicOp(opcode); } @@ -1401,10 +1925,16 @@ public class GLStateManager { } public static void glFog(int pname, FloatBuffer param) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordFog(pname, param); + } // TODO: Iris Notifier if (HAS_MULTIPLE_SET.contains(pname)) { - GL11.glFog(pname, param); - if (pname == GL11.GL_FOG_COLOR) { + if (!isRecordingDisplayList()) { + GL11.glFog(pname, param); + } + // Only update cached state on main thread + if (pname == GL11.GL_FOG_COLOR && isCachingEnabled()) { final float red = param.get(0); final float green = param.get(1); final float blue = param.get(2); @@ -1423,44 +1953,64 @@ public class GLStateManager { return fogState.getFogColor(); } + private static final FloatBuffer fogColorTempBuffer = BufferUtils.createFloatBuffer(4); public static void fogColor(float red, float green, float blue, float alpha) { - if (shouldBypassCache() || red != fogState.getFogColor().x || green != fogState.getFogColor().y || blue != fogState.getFogColor().z || alpha != fogState.getFogAlpha()) { - fogState.getFogColor().set(red, green, blue); - fogState.setFogAlpha(alpha); - fogState.getFogColorBuffer().clear(); - fogState.getFogColorBuffer().put(red).put(green).put(blue).put(alpha).flip(); - GL11.glFog(GL11.GL_FOG_COLOR, fogState.getFogColorBuffer()); + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || red != fogState.getFogColor().x || green != fogState.getFogColor().y || blue != fogState.getFogColor().z || alpha != fogState.getFogAlpha()) { + if (caching) { + fogState.getFogColor().set(red, green, blue); + fogState.setFogAlpha(alpha); + fogState.getFogColorBuffer().clear(); + fogState.getFogColorBuffer().put(red).put(green).put(blue).put(alpha).flip(); + GL11.glFog(GL11.GL_FOG_COLOR, fogState.getFogColorBuffer()); + } else { + // Use temp buffer for non-main thread to avoid corrupting shared state + fogColorTempBuffer.clear(); + fogColorTempBuffer.put(red).put(green).put(blue).put(alpha).flip(); + GL11.glFog(GL11.GL_FOG_COLOR, fogColorTempBuffer); + } } } public static void glFogf(int pname, float param) { - GL11.glFogf(pname, param); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordFogf(pname, param); + } else { + GL11.glFogf(pname, param); + } // Note: Does not handle GL_FOG_INDEX - switch (pname) { - case GL11.GL_FOG_DENSITY -> { - fogState.setDensity(param); - if (fogDensityListener != null) { - fogDensityListener.run(); + // Only update cached state when caching is enabled + if (isCachingEnabled()) { + switch (pname) { + case GL11.GL_FOG_DENSITY -> { + fogState.setDensity(param); + if (fogDensityListener != null) { + fogDensityListener.run(); + } } - } - case GL11.GL_FOG_START -> { - fogState.setStart(param); - if (fogStartListener != null) { - fogStartListener.run(); + case GL11.GL_FOG_START -> { + fogState.setStart(param); + if (fogStartListener != null) { + fogStartListener.run(); + } } - } - case GL11.GL_FOG_END -> { - fogState.setEnd(param); - if (fogEndListener != null) { - fogEndListener.run(); + case GL11.GL_FOG_END -> { + fogState.setEnd(param); + if (fogEndListener != null) { + fogEndListener.run(); + } } } } } public static void glFogi(int pname, int param) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordFogi(pname, param); + } GL11.glFogi(pname, param); - if (pname == GL11.GL_FOG_MODE) { + // Only update cached state when caching is enabled + if (isCachingEnabled() && pname == GL11.GL_FOG_MODE) { fogState.setFogMode(param); if (fogModeListener != null) { fogModeListener.run(); @@ -1473,9 +2023,21 @@ public class GLStateManager { } public static void glShadeModel(int mode) { - if (shouldBypassCache() || shadeModelState.getValue() != mode) { - shadeModelState.setValue(mode); - GL11.glShadeModel(mode); + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordShadeModel(mode); + } + final boolean caching = isCachingEnabled(); + final int oldValue = shadeModelState.getValue(); + final boolean needsUpdate = BYPASS_CACHE || !caching || oldValue != mode; + + if (needsUpdate) { + if (caching) { + shadeModelState.setValue(mode); + } + if (!isRecordingDisplayList()) { + GL11.glShadeModel(mode); + } } } @@ -1483,177 +2045,220 @@ public class GLStateManager { private static void onDeleteTexture(int id) { TextureTracker.INSTANCE.onDeleteTexture(id); TextureInfoCache.INSTANCE.onDeleteTexture(id); - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { PBRTextureManager.INSTANCE.onDeleteTexture(id); } - for(int i = 0; i < GLStateManager.MAX_TEXTURE_UNITS; i++) { - if(textures.getTextureUnitBindings(i).getBinding() == id) { - textures.getTextureUnitBindings(i).setBinding(0); + // Only update cached texture bindings on main thread + if (isCachingEnabled()) { + for (int i = 0; i < GLStateManager.MAX_TEXTURE_UNITS; i++) { + if (textures.getTextureUnitBindings(i).getBinding() == id) { + textures.getTextureUnitBindings(i).setBinding(0); + } } } } public static void makeCurrent(Drawable drawable) throws LWJGLException { drawable.makeCurrent(); - final Thread currentThread = Thread.currentThread(); + CurrentThread = Thread.currentThread(); - CurrentThread = currentThread; - LOGGER.info("Current thread: {}", currentThread.getName()); + // During splash, track which thread holds DrawableGL for caching + if (!splashComplete && sharedDrawable != null) { + if (drawable == sharedDrawable) { + // Switching to SharedDrawable - disable caching for this thread + if (drawableGLHolder == CurrentThread) { + drawableGLHolder = null; + } + } else { + // Switching to DrawableGL - enable caching for this thread + drawableGLHolder = CurrentThread; + } + } + } + + /** + * Set whether the current thread holds DrawableGL (the main display context). + * Called by mixins at context switch points during splash. + */ + public static void setCachingEnabled(boolean enabled) { + final Thread current = Thread.currentThread(); + if (enabled) { + drawableGLHolder = current; + } else if (drawableGLHolder == current) { + drawableGLHolder = null; + } + } + + /** + * Mark splash as complete - enables fast path that always caches. + * Called when finish() permanently switches to DrawableGL for the main game loop. + */ + public static void markSplashComplete() { + splashComplete = true; + drawableGLHolder = null; + sharedDrawable = null; } public static void glNewList(int list, int mode) { - if(glListMode > 0) { - glListNesting += 1; - return; - } - - glListId = list; - glListMode = mode; - GL11.glNewList(list, mode); - - List> stacks = Feature.maskToFeatures(GL11.GL_ALL_ATTRIB_BITS); - final int size = stacks.size(); - //noinspection ForLoopReplaceableByForEach - for(int i = 0; i < size; i++) { - IStateStack stack = stacks.get(i); - - // Feature Stack, copy of current feature state - glListStates.put(stack, (ISettableState) ((ISettableState)stack).copy()); - } - - if(glListMode == GL11.GL_COMPILE) { - pushState(GL11.GL_ALL_ATTRIB_BITS); - } - - // Band-aid fix to prevent color & texture leaks - color.setRed(-1); - color.setGreen(-1); - color.setBlue(-1); - textures.getTextureUnitBindings(getActiveTextureUnit()).setBinding(-1); + DisplayListManager.glNewList(list, mode); } public static void glEndList() { - if (glListNesting > 0) { - glListNesting -= 1; - return; - } + DisplayListManager.glEndList(); + } - if(glListMode == 0) { - throw new RuntimeException("glEndList called outside of a display list!"); - } + /** + * Check if we're currently recording a display list. + */ + public static boolean isRecordingDisplayList() { + return DisplayListManager.isRecording(); + } - final Set, ISettableState>> changedStates = new ObjectArraySet<>(); - for(Map.Entry, ISettableState> entry : glListStates.entrySet()) { - // If the current stack state is different than the copy of the state at the start - if(!((ISettableState)entry.getKey()).sameAs(entry.getValue()) - && !(entry.getKey() instanceof TextureBinding) - && !(entry.getKey() instanceof Color4)) { - // Then we want to put into the change set the stack and the copy of the state now - changedStates.add(new AbstractMap.SimpleEntry<>(entry.getKey(), (ISettableState) ((ISettableState) entry.getKey()).copy())); - } - } + public static int getRecordingDisplayListId() { + return DisplayListManager.getRecordingListId(); + } - final TextureBindingStack activeBinding = textures.getTextureUnitBindings(getActiveTextureUnit()); - if (activeBinding.getBinding() != -1) { - changedStates.add(new AbstractMap.SimpleEntry<>( - activeBinding, - activeBinding.copy() - )); - } else { - activeBinding.set(glListStates.get(activeBinding)); - } - if (color.getRed() != -1 || color.getGreen() != -1 || color.getBlue() != -1) { - changedStates.add(new AbstractMap.SimpleEntry<>( - color, - color.copy() - )); - } else { - color.set(glListStates.get(color)); - } - - if(!changedStates.isEmpty()) { - glListChanges.put(glListId, changedStates); - } - if(glListMode == GL11.GL_COMPILE) { - // GL_COMPILE doesn't actually apply the state, just stores it for replay in glCallList, so we'll - // roll back any changes that we tracked - popState(); - } - glListId = -1; - glListStates.clear(); - glListMode = 0; - GL11.glEndList(); + /** + * Delete display lists and free their VBO resources. + */ + public static void glDeleteLists(int list, int range) { + DisplayListManager.glDeleteLists(list, range); + } + /** + * Get a compiled display list from the cache. + * Used by CallListCmd to execute nested display lists with the unoptimized version. + * @param list The display list ID + * @return The CompiledDisplayList, or null if not found + */ + public static CompiledDisplayList getDisplayList(int list) { + return DisplayListManager.getDisplayList(list); } public static void glCallList(int list) { - if(list < 0) { - VBOManager.get(list).render(); - } else { - trySyncProgram(); - GL11.glCallList(list); - final Set, ISettableState>> changes = glListChanges.get(list); - if(changes != null) { - for(Map.Entry, ISettableState> entry : changes) { - // Set the stack to the cached state at the end of the call list compilation - ((ISettableState)entry.getKey()).set(entry.getValue()); - } - } - } + DisplayListManager.glCallList(list); } public static void pushState(int mask) { + if (attribDepth >= MAX_ATTRIB_STACK_DEPTH) { + throw new IllegalStateException("Attrib stack overflow: max depth " + MAX_ATTRIB_STACK_DEPTH + " reached"); + } attribs.push(mask); - List> stacks = Feature.maskToFeatures(mask); - int size = stacks.size(); + // Clear modified list for this depth level + modifiedAtDepth[attribDepth].clear(); + attribDepth++; - for(int i = 0; i < size; i++) { - stacks.get(i).push(); + // Only iterate non-boolean stacks - BooleanStateStack uses global depth tracking + // so pushDepth() is a no-op for them + final IStateStack[] nonBooleanStacks = Feature.maskToNonBooleanStacks(mask); + for (IStateStack stack : nonBooleanStacks) { + stack.pushDepth(); } } public static void popState() { final int mask = attribs.popInt(); + attribDepth--; - List> stacks = Feature.maskToFeatures(mask); - int size = stacks.size(); + // First: restore BooleanStateStack states that were actually modified (fast path) + // These use lazy copy-on-write with global depth tracking + final List> modified = modifiedAtDepth[attribDepth]; + for (int i = 0; i < modified.size(); i++) { + modified.get(i).popDepth(); + } + modified.clear(); - for(int i = 0; i < size; i++) { - stacks.get(i).pop(); + // Second: restore non-boolean state stacks the traditional way + final IStateStack[] nonBooleanStacks = Feature.maskToNonBooleanStacks(mask); + for (IStateStack stack : nonBooleanStacks) { + stack.popDepth(); } } public static void glClear(int mask) { - // TODO: Implement + if (DisplayListManager.isRecording()) { + DisplayListManager.recordClear(mask); + } + // Always execute - clears are immediate operations GL11.glClear(mask); } public static void glPushAttrib(int mask) { - pushState(mask); - GL11.glPushAttrib(mask); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordPushAttrib(mask); + } else { + GL11.glPushAttrib(mask); + } + // Only track on main thread (splash thread has separate GL context) + if (isCachingEnabled()) { + pushState(mask); + } } public static void glPopAttrib() { - poppingAttributes = true; - popState(); - GL11.glPopAttrib(); - poppingAttributes = false; + if (DisplayListManager.isRecording()) { + DisplayListManager.recordPopAttrib(); + } else { + GL11.glPopAttrib(); + } + // Only track on main thread (splash thread has separate GL context) + if (isCachingEnabled()) { + poppingAttributes = true; + popState(); + poppingAttributes = false; + } } // Matrix Operations public static void glMatrixMode(int mode) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordMatrixMode(mode); + } matrixMode.setMode(mode); } + /** + * Check if the current matrix mode is MODELVIEW. + * Used by DisplayListManager to determine which transforms to track. + */ + public static boolean isModelViewMatrix() { + return matrixMode.getMode() == GL11.GL_MODELVIEW; + } + public static void glLoadMatrix(FloatBuffer m) { - getMatrixStack().set(m); + if (DisplayListManager.isRecording()) { + final Matrix4f matrix = new Matrix4f().set(m); + m.rewind(); + DisplayListManager.recordLoadMatrix(matrixMode.getMode(), matrix); + // Reset relative transform for MODELVIEW - subsequent transforms are relative to loaded matrix + if (isModelViewMatrix()) { + DisplayListManager.resetRelativeTransform(); + } + return; + } + if (isCachingEnabled()) getMatrixStack().set(m); GL11.glLoadMatrix(m); } public static void glLoadMatrix(DoubleBuffer m) { - conversionMatrix4d.set(m); - getMatrixStack().set(conversionMatrix4d); + if (DisplayListManager.isRecording()) { + // Convert double buffer to float buffer for recording + conversionMatrix4d.set(m); + m.rewind(); + final Matrix4f floatMatrix = new Matrix4f(); + floatMatrix.set(conversionMatrix4d); + DisplayListManager.recordLoadMatrix(matrixMode.getMode(), floatMatrix); + // Reset relative transform for MODELVIEW - subsequent transforms are relative to loaded matrix + if (isModelViewMatrix()) { + DisplayListManager.resetRelativeTransform(); + } + return; + } + if (isCachingEnabled()) { + conversionMatrix4d.set(m); + getMatrixStack().set(conversionMatrix4d); + } GL11.glLoadMatrix(m); } @@ -1673,86 +2278,189 @@ public class GLStateManager { } public static void glLoadIdentity() { - GL11.glLoadIdentity(); - getMatrixStack().identity(); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLoadIdentity(matrixMode.getMode()); + } else { + GL11.glLoadIdentity(); + } + if (isCachingEnabled()) getMatrixStack().identity(); } public static void glTranslatef(float x, float y, float z) { - GL11.glTranslatef(x, y, z); - getMatrixStack().translate(x, y, z); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordTranslate(matrixMode.getMode(), x, y, z); + DisplayListManager.updateRelativeTransform(x, y, z, DisplayListManager.TransformOp.TRANSLATE, null); + } else { + GL11.glTranslatef(x, y, z); + } + if (isCachingEnabled()) getMatrixStack().translate(x, y, z); } + public static void glTranslated(double x, double y, double z) { - GL11.glTranslated(x, y, z); - getMatrixStack().translate((float) x, (float) y, (float) z); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordTranslate(matrixMode.getMode(), x, y, z); + DisplayListManager.updateRelativeTransform((float) x, (float) y, (float) z, DisplayListManager.TransformOp.TRANSLATE, null); + } else { + GL11.glTranslated(x, y, z); + } + if (isCachingEnabled()) getMatrixStack().translate((float) x, (float) y, (float) z); } public static void glScalef(float x, float y, float z) { - GL11.glScalef(x, y, z); - getMatrixStack().scale(x, y, z); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordScale(matrixMode.getMode(), x, y, z); + DisplayListManager.updateRelativeTransform(x, y, z, DisplayListManager.TransformOp.SCALE, null); + } else { + GL11.glScalef(x, y, z); + } + if (isCachingEnabled()) getMatrixStack().scale(x, y, z); } public static void glScaled(double x, double y, double z) { - GL11.glScaled(x, y, z); - getMatrixStack().scale((float) x, (float) y, (float) z); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordScale(matrixMode.getMode(), x, y, z); + DisplayListManager.updateRelativeTransform((float) x, (float) y, (float) z, DisplayListManager.TransformOp.SCALE, null); + } else { + GL11.glScaled(x, y, z); + } + if (isCachingEnabled()) getMatrixStack().scale((float) x, (float) y, (float) z); } - private static final Matrix4f tempMatrix4f = new Matrix4f(); + private static final Matrix4f multMatrix = new Matrix4f(); public static void glMultMatrix(FloatBuffer floatBuffer) { - GL11.glMultMatrix(floatBuffer); - tempMatrix4f.set(floatBuffer); - getMatrixStack().mul(tempMatrix4f); + multMatrix.set(floatBuffer); + final int currentMode = matrixMode.getMode(); + + if (DisplayListManager.isRecording()) { + // Record with the mode it's being applied to + DisplayListManager.recordMultMatrix(currentMode, multMatrix); + + // Only track if MODELVIEW (for baking into VBO vertices) + if (currentMode == GL11.GL_MODELVIEW) { + DisplayListManager.updateRelativeTransform(multMatrix); + } + } else { + GL11.glMultMatrix(floatBuffer); + } + if (isCachingEnabled()) getMatrixStack().mul(multMatrix); } public static final Matrix4d conversionMatrix4d = new Matrix4d(); public static final Matrix4f conversionMatrix4f = new Matrix4f(); public static void glMultMatrix(DoubleBuffer matrix) { - GL11.glMultMatrix(matrix); conversionMatrix4d.set(matrix); conversionMatrix4f.set(conversionMatrix4d); - getMatrixStack().mul(conversionMatrix4f); + final int currentMode = matrixMode.getMode(); + + if (DisplayListManager.isRecording()) { + // Record with the mode it's being applied to + DisplayListManager.recordMultMatrix(currentMode, conversionMatrix4f); + + // Only track if MODELVIEW (for baking into VBO vertices) + if (currentMode == GL11.GL_MODELVIEW) { + DisplayListManager.updateRelativeTransform(conversionMatrix4f); + } + } else { + GL11.glMultMatrix(matrix); + } + if (isCachingEnabled()) getMatrixStack().mul(conversionMatrix4f); } private static final Vector3f rotation = new Vector3f(); public static void glRotatef(float angle, float x, float y, float z) { - GL11.glRotatef(angle, x, y, z); - rotation.set(x, y, z).normalize(); - getMatrixStack().rotate((float)Math.toRadians(angle), rotation); + final boolean recording = DisplayListManager.isRecording(); + final boolean caching = isCachingEnabled(); + + if (recording) { + DisplayListManager.recordRotate(matrixMode.getMode(), angle, x, y, z); + } else { + GL11.glRotatef(angle, x, y, z); + } + + // Compute rotation vector once for relative transform tracking and/or matrix stack + if (recording || caching) { + rotation.set(x, y, z).normalize(); + if (recording) { + DisplayListManager.updateRelativeTransform(angle, 0, 0, DisplayListManager.TransformOp.ROTATE, rotation); + } + if (caching) { + getMatrixStack().rotate((float)Math.toRadians(angle), rotation); + } + } } public static void glRotated(double angle, double x, double y, double z) { - GL11.glRotated(angle, x, y, z); - rotation.set(x, y, z).normalize(); - getMatrixStack().rotate((float)Math.toRadians(angle), rotation); + final boolean recording = DisplayListManager.isRecording(); + final boolean caching = isCachingEnabled(); + + if (recording) { + DisplayListManager.recordRotate(matrixMode.getMode(), angle, x, y, z); + } else { + GL11.glRotated(angle, x, y, z); + } + + // Compute rotation vector once for relative transform tracking and/or matrix stack + if (recording || caching) { + rotation.set((float) x, (float) y, (float) z).normalize(); + if (recording) { + DisplayListManager.updateRelativeTransform((float) angle, 0, 0, DisplayListManager.TransformOp.ROTATE, rotation); + } + if (caching) { + getMatrixStack().rotate((float)Math.toRadians(angle), rotation); + } + } } public static void glOrtho(double left, double right, double bottom, double top, double zNear, double zFar) { - GL11.glOrtho(left, right, bottom, top, zNear, zFar); - getMatrixStack().ortho((float)left, (float)right, (float)bottom, (float)top, (float)zNear, (float)zFar); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordOrtho(left, right, bottom, top, zNear, zFar); + } else { + GL11.glOrtho(left, right, bottom, top, zNear, zFar); + } + if (isCachingEnabled()) getMatrixStack().ortho((float)left, (float)right, (float)bottom, (float)top, (float)zNear, (float)zFar); } public static void glFrustum(double left, double right, double bottom, double top, double zNear, double zFar) { - GL11.glFrustum(left, right, bottom, top, zNear, zFar); - getMatrixStack().frustum((float)left, (float)right, (float)bottom, (float)top, (float)zNear, (float)zFar); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordFrustum(left, right, bottom, top, zNear, zFar); + } else { + GL11.glFrustum(left, right, bottom, top, zNear, zFar); + } + if (isCachingEnabled()) getMatrixStack().frustum((float)left, (float)right, (float)bottom, (float)top, (float)zNear, (float)zFar); } public static void glPushMatrix() { - GL11.glPushMatrix(); - try { - getMatrixStack().pushMatrix(); - } catch(IllegalStateException ignored) { - // Ignore - if(AngelicaMod.lwjglDebug) - AngelicaTweaker.LOGGER.warn("Matrix stack overflow ", new Throwable()); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordPushMatrix(matrixMode.getMode()); + DisplayListManager.pushRelativeTransform(); // Track MODELVIEW transform stack + } else { + GL11.glPushMatrix(); + } + // Only track stack on main thread (splash thread has separate GL context) + if (isCachingEnabled()) { + try { + getMatrixStack().pushMatrix(); + } catch(IllegalStateException ignored) { + if(AngelicaMod.lwjglDebug) + AngelicaTweaker.LOGGER.warn("Matrix stack overflow ", new Throwable()); + } } } public static void glPopMatrix() { - GL11.glPopMatrix(); - try { - getMatrixStack().popMatrix(); - } catch(IllegalStateException ignored) { - // Ignore - if(AngelicaMod.lwjglDebug) - AngelicaTweaker.LOGGER.warn("Matrix stack underflow ", new Throwable()); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordPopMatrix(matrixMode.getMode()); + DisplayListManager.popRelativeTransform(); // Track MODELVIEW transform stack + } else { + GL11.glPopMatrix(); + } + // Only track stack on main thread (splash thread has separate GL context) + if (isCachingEnabled()) { + try { + getMatrixStack().popMatrix(); + } catch(IllegalStateException ignored) { + if(AngelicaMod.lwjglDebug) + AngelicaTweaker.LOGGER.warn("Matrix stack underflow ", new Throwable()); + } } } @@ -1762,15 +2470,19 @@ public class GLStateManager { perspectiveMatrix.identity().perspective((float)Math.toRadians(fovy), aspect, zNear, zFar); perspectiveMatrix.get(0, perspectiveBuffer); - GL11.glMultMatrix(perspectiveBuffer); - - getMatrixStack().mul(perspectiveMatrix); - + GLStateManager.glMultMatrix(perspectiveBuffer); } public static void glViewport(int x, int y, int width, int height) { - GL11.glViewport(x, y, width, height); - viewportState.setViewPort(x, y, width, height); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordViewport(x, y, width, height); + } else { + GL11.glViewport(x, y, width, height); + } + // Only update cached state when caching is enabled + if (isCachingEnabled()) { + viewportState.setViewPort(x, y, width, height); + } } public static int getActiveTextureUnit() { @@ -1778,7 +2490,7 @@ public class GLStateManager { } public static int getListMode() { - return glListMode; + return DisplayListManager.getListMode(); } @@ -1844,15 +2556,25 @@ public class GLStateManager { GL11.glTexParameter(target, pname, params); } - public static void glTexParameteri(int target, int pname, int param) { + // Recording mode: record command but continue to update state tracking + if (DisplayListManager.isRecording()) { + DisplayListManager.recordTexParameteri(target, pname, param); + // Don't return - fall through to update cache for glGet queries + } if (target != GL11.GL_TEXTURE_2D) { - GL11.glTexParameteri(target, pname, param); + // Skip actual GL call during display list recording (state tracking only) + if (!isRecordingDisplayList()) { + GL11.glTexParameteri(target, pname, param); + } return; } - if(!updateTexParameteriCache(target, getBoundTexture(), pname, param)) return; + if (!updateTexParameteriCache(target, getBoundTexture(), pname, param)) return; - GL11.glTexParameteri(target, pname, param); + // Skip actual GL call during display list recording (state tracking only) + if (!isRecordingDisplayList()) { + GL11.glTexParameteri(target, pname, param); + } } @@ -1878,18 +2600,34 @@ public class GLStateManager { } public static void glTexParameterf(int target, int pname, float param) { + // Recording mode: record command but continue to update state tracking + if (DisplayListManager.isRecording()) { + DisplayListManager.recordTexParameterf(target, pname, param); + // Don't return - fall through to update cache for glGet queries + } if (target != GL11.GL_TEXTURE_2D) { - GL11.glTexParameterf(target, pname, param); + // Skip actual GL call during display list recording (state tracking only) + if (!isRecordingDisplayList()) { + GL11.glTexParameterf(target, pname, param); + } return; } - if(!updateTexParameterfCache(getActiveTextureUnit(), target, pname, param)) return; + if (!updateTexParameterfCache(getActiveTextureUnit(), target, pname, param)) return; - GL11.glTexParameterf(target, pname, param); + // Skip actual GL call during display list recording (state tracking only) + if (!isRecordingDisplayList()) { + GL11.glTexParameterf(target, pname, param); + } } public static int getTexParameterOrDefault(int texture, int pname, IntSupplier defaultSupplier) { final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(texture); if (info == null) { + if (isRecordingDisplayList()) { + throw new IllegalStateException(String.format( + "glGetTexParameteri called during display list recording with no cached TextureInfo for texture %d. " + + "Cannot query OpenGL state during compilation!", texture)); + } return defaultSupplier.getAsInt(); } return switch (pname) { @@ -1900,7 +2638,14 @@ public class GLStateManager { case GL12.GL_TEXTURE_MAX_LEVEL -> info.getMaxLevel(); case GL12.GL_TEXTURE_MIN_LOD -> info.getMinLod(); case GL12.GL_TEXTURE_MAX_LOD -> info.getMaxLod(); - default -> defaultSupplier.getAsInt(); + default -> { + if (isRecordingDisplayList()) { + throw new IllegalStateException(String.format( + "glGetTexParameteri called during display list recording with uncached pname 0x%s for texture %d. " + + "Cannot query OpenGL state during compilation!", Integer.toHexString(pname), texture)); + } + yield defaultSupplier.getAsInt(); + } }; } public static int glGetTexParameteri(int target, int pname) { @@ -1916,13 +2661,25 @@ public class GLStateManager { } final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(getBoundTexture()); if(info == null) { + if (isRecordingDisplayList()) { + throw new IllegalStateException(String.format( + "glGetTexParameterf called during display list recording with no cached TextureInfo for texture %d. " + + "Cannot query OpenGL state during compilation!", getBoundTexture())); + } return GL11.glGetTexParameterf(target, pname); } return switch (pname) { case EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> info.getMaxAnisotropy(); case GL14.GL_TEXTURE_LOD_BIAS -> info.getLodBias(); - default -> GL11.glGetTexParameterf(target, pname); + default -> { + if (isRecordingDisplayList()) { + throw new IllegalStateException(String.format( + "glGetTexParameterf called during display list recording with uncached pname 0x%s for texture %d. " + + "Cannot query OpenGL state during compilation!", Integer.toHexString(pname), getBoundTexture())); + } + yield GL11.glGetTexParameterf(target, pname); + } }; } @@ -1932,13 +2689,25 @@ public class GLStateManager { } final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(getBoundTexture()); if (info == null) { + if (isRecordingDisplayList()) { + throw new IllegalStateException(String.format( + "glGetTexLevelParameteri called during display list recording with no cached TextureInfo for texture %d. " + + "Cannot query OpenGL state during compilation!", getBoundTexture())); + } return GL11.glGetTexLevelParameteri(target, level, pname); } return switch (pname) { case GL11.GL_TEXTURE_WIDTH -> info.getWidth(); case GL11.GL_TEXTURE_HEIGHT -> info.getHeight(); case GL11.GL_TEXTURE_INTERNAL_FORMAT -> info.getInternalFormat(); - default -> GL11.glGetTexLevelParameteri(target, level, pname); + default -> { + if (isRecordingDisplayList()) { + throw new IllegalStateException(String.format( + "glGetTexLevelParameteri called during display list recording with uncached pname 0x%s for texture %d. " + + "Cannot query OpenGL state during compilation!", Integer.toHexString(pname), getBoundTexture())); + } + yield GL11.glGetTexLevelParameteri(target, level, pname); + } }; } @@ -2003,6 +2772,9 @@ public class GLStateManager { } public static void glMaterial(int face, int pname, FloatBuffer params) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordMaterial(face, pname, params); + } if (face == GL11.GL_FRONT) { glMaterialFront(pname, params); } else if (face == GL11.GL_BACK) { @@ -2026,9 +2798,39 @@ public class GLStateManager { } else { throw new RuntimeException("Unsupported face value for glMaterial: " + face); } + + // Capture already-converted float values from state + // For FRONT_AND_BACK, capture from frontMaterial (both should have same values) + if (DisplayListManager.isRecording()) { + final MaterialStateStack material = (face == GL11.GL_BACK) ? backMaterial : frontMaterial; + final float[] floatParams; + switch (pname) { + case GL11.GL_AMBIENT -> floatParams = new float[] {material.ambient.x, material.ambient.y, material.ambient.z, material.ambient.w}; + case GL11.GL_DIFFUSE -> floatParams = new float[] {material.diffuse.x, material.diffuse.y, material.diffuse.z, material.diffuse.w}; + case GL11.GL_SPECULAR -> floatParams = new float[] {material.specular.x, material.specular.y, material.specular.z, material.specular.w}; + case GL11.GL_EMISSION -> floatParams = new float[] {material.emission.x, material.emission.y, material.emission.z, material.emission.w}; + case GL11.GL_SHININESS -> floatParams = new float[] {material.shininess}; + case GL11.GL_AMBIENT_AND_DIFFUSE -> // For AMBIENT_AND_DIFFUSE, capture ambient (could also use diffuse, they're the same) + floatParams = new float[] {material.ambient.x, material.ambient.y, material.ambient.z, material.ambient.w}; + case GL11.GL_COLOR_INDEXES -> floatParams = new float[] {material.colorIndexes.x, material.colorIndexes.y, material.colorIndexes.z}; + default -> { + // Fallback for unknown pname - do simple cast + floatParams = new float[params.remaining()]; + params.mark(); + for (int i = 0; i < floatParams.length; i++) { + floatParams[i] = params.get(); + } + params.reset(); + } + } + DisplayListManager.recordMaterial(face, pname, java.nio.FloatBuffer.wrap(floatParams)); + } } public static void glMaterialf(int face, int pname, float val) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordMaterialf(face, pname, val); + } if (pname != GL11.GL_SHININESS) { // it is only valid to call glMaterialf for the GL_SHININESS parameter return; @@ -2048,11 +2850,15 @@ public class GLStateManager { public static void glMateriali(int face, int pname, int val) { // This will end up no-opping if pname != GL_SHININESS, it is invalid to call this with another pname + // Command recording happens in glMaterialf glMaterialf(face, pname, (float) val); } public static void glLight(int light, int pname, FloatBuffer params) { - LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLight(light, pname, params); + } + final LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; switch (pname) { case GL11.GL_AMBIENT -> lightState.setAmbient(params); case GL11.GL_DIFFUSE -> lightState.setDiffuse(params); @@ -2069,7 +2875,8 @@ public class GLStateManager { } public static void glLight(int light, int pname, IntBuffer params) { - LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; + // Call state setter first to convert IntBuffer to float + final LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; switch (pname) { case GL11.GL_AMBIENT -> lightState.setAmbient(params); case GL11.GL_DIFFUSE -> lightState.setDiffuse(params); @@ -2083,10 +2890,40 @@ public class GLStateManager { case GL11.GL_QUADRATIC_ATTENUATION -> lightState.setQuadraticAttenuation(params); default -> GL11.glLight(light, pname, params); } + + // Capture already-converted float values from state + if (DisplayListManager.isRecording()) { + final float[] floatParams; + switch (pname) { + case GL11.GL_AMBIENT -> floatParams = new float[] {lightState.ambient.x, lightState.ambient.y, lightState.ambient.z, lightState.ambient.w}; + case GL11.GL_DIFFUSE -> floatParams = new float[] {lightState.diffuse.x, lightState.diffuse.y, lightState.diffuse.z, lightState.diffuse.w}; + case GL11.GL_SPECULAR -> floatParams = new float[] {lightState.specular.x, lightState.specular.y, lightState.specular.z, lightState.specular.w}; + case GL11.GL_POSITION -> floatParams = new float[] {lightState.position.x, lightState.position.y, lightState.position.z, lightState.position.w}; + case GL11.GL_SPOT_DIRECTION -> floatParams = new float[] {lightState.spotDirection.x, lightState.spotDirection.y, lightState.spotDirection.z}; + case GL11.GL_SPOT_EXPONENT -> floatParams = new float[] {lightState.spotExponent}; + case GL11.GL_SPOT_CUTOFF -> floatParams = new float[] {lightState.spotCutoff}; + case GL11.GL_CONSTANT_ATTENUATION -> floatParams = new float[] {lightState.constantAttenuation}; + case GL11.GL_LINEAR_ATTENUATION -> floatParams = new float[] {lightState.linearAttenuation}; + case GL11.GL_QUADRATIC_ATTENUATION -> floatParams = new float[] {lightState.quadraticAttenuation}; + default -> { + // Fallback for unknown pname - do simple cast + floatParams = new float[params.remaining()]; + params.mark(); + for (int i = 0; i < floatParams.length; i++) { + floatParams[i] = params.get(); + } + params.reset(); + } + } + DisplayListManager.recordLight(light, pname, java.nio.FloatBuffer.wrap(floatParams)); + } } public static void glLightf(int light, int pname, float param) { - LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLightf(light, pname, param); + } + final LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; switch (pname) { case GL11.GL_SPOT_EXPONENT -> lightState.setSpotExponent(param); case GL11.GL_SPOT_CUTOFF -> lightState.setSpotCutoff(param); @@ -2098,7 +2935,10 @@ public class GLStateManager { } public static void glLighti(int light, int pname, int param) { - LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLighti(light, pname, param); + } + final LightStateStack lightState = lightDataStates[light - GL11.GL_LIGHT0]; switch (pname) { case GL11.GL_SPOT_EXPONENT -> lightState.setSpotExponent(param); case GL11.GL_SPOT_CUTOFF -> lightState.setSpotCutoff(param); @@ -2110,6 +2950,9 @@ public class GLStateManager { } public static void glLightModel(int pname, FloatBuffer params) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLightModel(pname, params); + } switch (pname) { case GL11.GL_LIGHT_MODEL_AMBIENT -> lightModel.setAmbient(params); case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> lightModel.setLocalViewer(params); @@ -2118,6 +2961,7 @@ public class GLStateManager { } } public static void glLightModel(int pname, IntBuffer params) { + // Call state setter first to convert IntBuffer to float switch (pname) { case GL11.GL_LIGHT_MODEL_AMBIENT -> lightModel.setAmbient(params); case GL12.GL_LIGHT_MODEL_COLOR_CONTROL -> lightModel.setColorControl(params); @@ -2125,27 +2969,70 @@ public class GLStateManager { case GL11.GL_LIGHT_MODEL_TWO_SIDE -> lightModel.setTwoSide(params); default -> GL11.glLightModel(pname, params); } + + // Capture already-converted float values from state + if (DisplayListManager.isRecording()) { + final float[] floatParams; + switch (pname) { + case GL11.GL_LIGHT_MODEL_AMBIENT -> floatParams = new float[] {lightModel.ambient.x, lightModel.ambient.y, lightModel.ambient.z, lightModel.ambient.w}; + case GL12.GL_LIGHT_MODEL_COLOR_CONTROL -> floatParams = new float[] {(float) lightModel.colorControl}; + case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> floatParams = new float[] {lightModel.localViewer}; + case GL11.GL_LIGHT_MODEL_TWO_SIDE -> floatParams = new float[] {lightModel.twoSide}; + default -> { + // Fallback for unknown pname - do simple cast + floatParams = new float[params.remaining()]; + params.mark(); + for (int i = 0; i < floatParams.length; i++) { + floatParams[i] = params.get(); + } + params.reset(); + } + } + DisplayListManager.recordLightModel(pname, java.nio.FloatBuffer.wrap(floatParams)); + } } public static void glLightModelf(int pname, float param) { - switch (pname) { - case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> lightModel.setLocalViewer(param); - case GL11.GL_LIGHT_MODEL_TWO_SIDE -> lightModel.setTwoSide(param); - default -> GL11.glLightModelf(pname, param); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLightModelf(pname, param); + } + // Only update cached state on main thread + if (isCachingEnabled()) { + switch (pname) { + case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> lightModel.setLocalViewer(param); + case GL11.GL_LIGHT_MODEL_TWO_SIDE -> lightModel.setTwoSide(param); + default -> GL11.glLightModelf(pname, param); + } + } else { + GL11.glLightModelf(pname, param); } } public static void glLightModeli(int pname, int param) { - switch (pname) { - case GL12.GL_LIGHT_MODEL_COLOR_CONTROL -> lightModel.setColorControl(param); - case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> lightModel.setLocalViewer(param); - case GL11.GL_LIGHT_MODEL_TWO_SIDE -> lightModel.setTwoSide(param); - default -> GL11.glLightModeli(pname, param); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordLightModeli(pname, param); + } + // Only update cached state on main thread + if (isCachingEnabled()) { + switch (pname) { + case GL12.GL_LIGHT_MODEL_COLOR_CONTROL -> lightModel.setColorControl(param); + case GL11.GL_LIGHT_MODEL_LOCAL_VIEWER -> lightModel.setLocalViewer(param); + case GL11.GL_LIGHT_MODEL_TWO_SIDE -> lightModel.setTwoSide(param); + default -> GL11.glLightModeli(pname, param); + } + } else { + GL11.glLightModeli(pname, param); } } public static void glColorMaterial(int face, int mode) { - if (shouldBypassCache() || (colorMaterialFace.getValue() != face || colorMaterialParameter.getValue() != mode)) { - colorMaterialFace.setValue(face); - colorMaterialParameter.setValue(mode); + if (DisplayListManager.isRecording()) { + DisplayListManager.recordColorMaterial(face, mode); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || colorMaterialFace.getValue() != face || colorMaterialParameter.getValue() != mode) { + if (caching) { + colorMaterialFace.setValue(face); + colorMaterialParameter.setValue(mode); + } GL11.glColorMaterial(face, mode); } } @@ -2155,9 +3042,16 @@ public class GLStateManager { } public static void glUseProgram(int program) { - if(program != activeProgram || shouldBypassCache()) { - activeProgram = program; - if(AngelicaMod.lwjglDebug) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordUseProgram(program); + return; + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || program != activeProgram) { + if (caching) { + activeProgram = program; + } + if (AngelicaMod.lwjglDebug) { final String programName = GLDebug.getObjectLabel(KHRDebug.GL_PROGRAM, program); GLDebug.debugMessage("Activating Program - " + program + ":" + programName); } @@ -2165,6 +3059,468 @@ public class GLStateManager { } } + // Missing GL commands from Mesa cross-check + public static void glTexImage1D(int target, int level, int internalformat, int width, int border, int format, int type, ByteBuffer pixels) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glTexImage1D in display lists not yet implemented"); + } + GL11.glTexImage1D(target, level, internalformat, width, border, format, type, pixels); + } + + public static void glTexImage3D(int target, int level, int internalformat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glTexImage3D in display lists not yet implemented"); + } + GL12.glTexImage3D(target, level, internalformat, width, height, depth, border, format, type, pixels); + } + + public static void glTexSubImage1D(int target, int level, int xoffset, int width, int format, int type, ByteBuffer pixels) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glTexSubImage1D in display lists not yet implemented"); + } + GL11.glTexSubImage1D(target, level, xoffset, width, format, type, pixels); + } + + public static void glLineWidth(float width) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordLineWidth(width); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || lineState.getWidth() != width) { + if (caching) { + lineState.setWidth(width); + } + if (!recording) { + GL11.glLineWidth(width); + } + } + } + + // Texture commands + public static void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordComplexCommand(TexSubImage2DCmd.fromByteBuffer(target, level, xoffset, yoffset, width, height, format, type, pixels)); + } + if (shouldUseDSA(target)) { + // Use DSA to upload directly to the texture - keeps GL binding state unchanged + RenderSystem.textureSubImage2D(getBoundTexture(), target, level, xoffset, yoffset, width, height, format, type, pixels); + } else { + // Non-main thread or proxy texture - use direct GL call + GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + } + } + + public static void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordComplexCommand(TexSubImage2DCmd.fromIntBuffer(target, level, xoffset, yoffset, width, height, format, type, pixels)); + } + if (shouldUseDSA(target)) { + // Use DSA to upload directly to the texture + RenderSystem.textureSubImage2D(getBoundTexture(), target, level, xoffset, yoffset, width, height, format, type, pixels); + } else { + // Non-main thread or proxy texture - use direct GL call + GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + } + } + + public static void glTexSubImage2D(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, long pixels_buffer_offset) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glTexSubImage2D with buffer offset in display lists not yet supported"); + } + GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset); + } + + public static void glTexSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int width, int height, int depth, int format, int type, ByteBuffer pixels) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glTexSubImage3D in display lists not yet implemented - if you see this, please report!"); + } + GL12.glTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, pixels); + } + + public static void glCopyTexImage1D(int target, int level, int internalFormat, int x, int y, int width, int border) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glCopyTexImage1D in display lists not yet implemented - if you see this, please report!"); + } + GL11.glCopyTexImage1D(target, level, internalFormat, x, y, width, border); + } + + public static void glCopyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glCopyTexImage2D in display lists not yet implemented - if you see this, please report!"); + } + GL11.glCopyTexImage2D(target, level, internalFormat, x, y, width, height, border); + } + + public static void glCopyTexSubImage1D(int target, int level, int xoffset, int x, int y, int width) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glCopyTexSubImage1D in display lists not yet implemented - if you see this, please report!"); + } + GL11.glCopyTexSubImage1D(target, level, xoffset, x, y, width); + } + + public static void glCopyTexSubImage2D(int target, int level, int xoffset, int yoffset, int x, int y, int width, int height) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glCopyTexSubImage2D in display lists not yet implemented - if you see this, please report!"); + } + GL11.glCopyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); + } + + public static void glCopyTexSubImage3D(int target, int level, int xoffset, int yoffset, int zoffset, int x, int y, int width, int height) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glCopyTexSubImage3D in display lists not yet implemented - if you see this, please report!"); + } + GL12.glCopyTexSubImage3D(target, level, xoffset, yoffset, zoffset, x, y, width, height); + } + + // State commands + public static void glCullFace(int mode) { + if (DisplayListManager.isRecording()) { + DisplayListManager.recordCullFace(mode); + return; + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || polygonState.getCullFaceMode() != mode) { + if (caching) { + polygonState.setCullFaceMode(mode); + } + GL11.glCullFace(mode); + } + } + + public static void glFrontFace(int mode) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glFrontFace in display lists not yet implemented - if you see this, please report!"); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || polygonState.getFrontFace() != mode) { + if (caching) { + polygonState.setFrontFace(mode); + } + GL11.glFrontFace(mode); + } + } + + public static void glHint(int target, int mode) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glHint in display lists not yet implemented - if you see this, please report!"); + } + GL11.glHint(target, mode); + } + + public static void glLineStipple(int factor, short pattern) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordLineStipple(factor, pattern); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || lineState.getStippleFactor() != factor || lineState.getStipplePattern() != pattern) { + if (caching) { + lineState.setStippleFactor(factor); + lineState.setStipplePattern(pattern); + } + if (!recording) { + GL11.glLineStipple(factor, pattern); + } + } + } + + public static void glPointSize(float size) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordPointSize(size); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || pointState.getSize() != size) { + if (caching) { + pointState.setSize(size); + } + if (!recording) { + GL11.glPointSize(size); + } + } + } + + public static void glPolygonMode(int face, int mode) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordPolygonMode(face, mode); + } + final boolean caching = isCachingEnabled(); + final boolean needsUpdate; + if (face == GL11.GL_FRONT) { + needsUpdate = BYPASS_CACHE || !caching || polygonState.getFrontMode() != mode; + if (caching && needsUpdate) polygonState.setFrontMode(mode); + } else if (face == GL11.GL_BACK) { + needsUpdate = BYPASS_CACHE || !caching || polygonState.getBackMode() != mode; + if (caching && needsUpdate) polygonState.setBackMode(mode); + } else { // GL_FRONT_AND_BACK + needsUpdate = BYPASS_CACHE || !caching || polygonState.getFrontMode() != mode || polygonState.getBackMode() != mode; + if (caching && needsUpdate) { + polygonState.setFrontMode(mode); + polygonState.setBackMode(mode); + } + } + if (needsUpdate && !recording) { + GL11.glPolygonMode(face, mode); + } + } + + public static void glPolygonOffset(float factor, float units) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordPolygonOffset(factor, units); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || polygonState.getOffsetFactor() != factor || polygonState.getOffsetUnits() != units) { + if (caching) { + polygonState.setOffsetFactor(factor); + polygonState.setOffsetUnits(units); + } + if (!recording) { + GL11.glPolygonOffset(factor, units); + } + } + } + + public static void glReadBuffer(int mode) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glReadBuffer in display lists not yet implemented - if you see this, please report!"); + } + GL11.glReadBuffer(mode); + } + + public static void glScissor(int x, int y, int width, int height) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glScissor in display lists not yet implemented - if you see this, please report!"); + } + GL11.glScissor(x, y, width, height); + } + + public static void glStencilFunc(int func, int ref, int mask) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordStencilFunc(func, ref, mask); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || stencilState.getFuncFront() != func || stencilState.getRefFront() != ref || stencilState.getValueMaskFront() != mask) { + if (caching) { + stencilState.setFunc(func, ref, mask); + } + if (!recording) { + GL11.glStencilFunc(func, ref, mask); + } + } + } + + public static void glStencilMask(int mask) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordStencilMask(mask); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || stencilState.getWriteMaskFront() != mask) { + if (caching) { + stencilState.setWriteMask(mask); + } + if (!recording) { + GL11.glStencilMask(mask); + } + } + } + + public static void glStencilOp(int fail, int zfail, int zpass) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordStencilOp(fail, zfail, zpass); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || stencilState.getFailOpFront() != fail || stencilState.getZFailOpFront() != zfail || stencilState.getZPassOpFront() != zpass) { + if (caching) { + stencilState.setOp(fail, zfail, zpass); + } + if (!recording) { + GL11.glStencilOp(fail, zfail, zpass); + } + } + } + + public static void glPixelStorei(int pname, int param) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glPixelStorei in display lists not yet implemented - if you see this, please report!"); + } + GL11.glPixelStorei(pname, param); + } + + public static void glPixelStoref(int pname, float param) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glPixelStoref in display lists not yet implemented - if you see this, please report!"); + } + GL11.glPixelStoref(pname, param); + } + + // Display List Commands + public static void glCallLists(IntBuffer lists) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glCallLists in display lists not yet implemented - if you see this, please report!"); + } + GL11.glCallLists(lists); + } + + public static void glCallLists(ByteBuffer lists) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glCallLists in display lists not yet implemented - if you see this, please report!"); + } + GL11.glCallLists(lists); + } + + public static void glListBase(int base) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glListBase in display lists not yet implemented - if you see this, please report!"); + } + GL11.glListBase(base); + } + + // Clip Plane Commands + public static void glClipPlane(int plane, DoubleBuffer equation) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glClipPlane in display lists not yet implemented - if you see this, please report!"); + } + GL11.glClipPlane(plane, equation); + } + + // Clear Commands + public static void glClearStencil(int s) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordClearStencil(s); + } + final boolean caching = isCachingEnabled(); + if (BYPASS_CACHE || !caching || stencilState.getClearValue() != s) { + if (caching) { + stencilState.setClearValue(s); + } + if (!recording) { + GL11.glClearStencil(s); + } + } + } + + // Draw Buffer Commands (GL 2.0+) + public static void glDrawBuffers(IntBuffer bufs) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glDrawBuffers in display lists not yet implemented - if you see this, please report!"); + } + GL20.glDrawBuffers(bufs); + } + + // Multisample Commands + public static void glSampleCoverage(float value, boolean invert) { + if (DisplayListManager.isRecording()) { + throw new UnsupportedOperationException("glSampleCoverage in display lists not yet implemented - if you see this, please report!"); + } + GL13.glSampleCoverage(value, invert); + } + + // Stencil Separate Functions (GL 2.0+) + public static void glStencilFuncSeparate(int face, int func, int ref, int mask) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordStencilFuncSeparate(face, func, ref, mask); + } + final boolean caching = isCachingEnabled(); + boolean needsUpdate = BYPASS_CACHE || !caching; + if (!needsUpdate) { + if (face == GL11.GL_FRONT || face == GL11.GL_FRONT_AND_BACK) { + needsUpdate = stencilState.getFuncFront() != func || stencilState.getRefFront() != ref || stencilState.getValueMaskFront() != mask; + } + if (!needsUpdate && (face == GL11.GL_BACK || face == GL11.GL_FRONT_AND_BACK)) { + needsUpdate = stencilState.getFuncBack() != func || stencilState.getRefBack() != ref || stencilState.getValueMaskBack() != mask; + } + } + if (needsUpdate) { + if (caching) { + if (face == GL11.GL_FRONT || face == GL11.GL_FRONT_AND_BACK) { + stencilState.setFuncFront(func); + stencilState.setRefFront(ref); + stencilState.setValueMaskFront(mask); + } + if (face == GL11.GL_BACK || face == GL11.GL_FRONT_AND_BACK) { + stencilState.setFuncBack(func); + stencilState.setRefBack(ref); + stencilState.setValueMaskBack(mask); + } + } + if (!recording) { + GL20.glStencilFuncSeparate(face, func, ref, mask); + } + } + } + + public static void glStencilMaskSeparate(int face, int mask) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordStencilMaskSeparate(face, mask); + } + final boolean caching = isCachingEnabled(); + boolean needsUpdate = BYPASS_CACHE || !caching; + if (!needsUpdate) { + if (face == GL11.GL_FRONT || face == GL11.GL_FRONT_AND_BACK) { + needsUpdate = stencilState.getWriteMaskFront() != mask; + } + if (!needsUpdate && (face == GL11.GL_BACK || face == GL11.GL_FRONT_AND_BACK)) { + needsUpdate = stencilState.getWriteMaskBack() != mask; + } + } + if (needsUpdate) { + if (caching) { + if (face == GL11.GL_FRONT || face == GL11.GL_FRONT_AND_BACK) { + stencilState.setWriteMaskFront(mask); + } + if (face == GL11.GL_BACK || face == GL11.GL_FRONT_AND_BACK) { + stencilState.setWriteMaskBack(mask); + } + } + if (!recording) { + GL20.glStencilMaskSeparate(face, mask); + } + } + } + + public static void glStencilOpSeparate(int face, int sfail, int dpfail, int dppass) { + final boolean recording = DisplayListManager.isRecording(); + if (recording) { + DisplayListManager.recordStencilOpSeparate(face, sfail, dpfail, dppass); + } + final boolean caching = isCachingEnabled(); + boolean needsUpdate = BYPASS_CACHE || !caching; + if (!needsUpdate) { + if (face == GL11.GL_FRONT || face == GL11.GL_FRONT_AND_BACK) { + needsUpdate = stencilState.getFailOpFront() != sfail || stencilState.getZFailOpFront() != dpfail || stencilState.getZPassOpFront() != dppass; + } + if (!needsUpdate && (face == GL11.GL_BACK || face == GL11.GL_FRONT_AND_BACK)) { + needsUpdate = stencilState.getFailOpBack() != sfail || stencilState.getZFailOpBack() != dpfail || stencilState.getZPassOpBack() != dppass; + } + } + if (needsUpdate) { + if (caching) { + if (face == GL11.GL_FRONT || face == GL11.GL_FRONT_AND_BACK) { + stencilState.setFailOpFront(sfail); + stencilState.setZFailOpFront(dpfail); + stencilState.setZPassOpFront(dppass); + } + if (face == GL11.GL_BACK || face == GL11.GL_FRONT_AND_BACK) { + stencilState.setFailOpBack(sfail); + stencilState.setZFailOpBack(dpfail); + stencilState.setZPassOpBack(dppass); + } + } + if (!recording) { + GL20.glStencilOpSeparate(face, sfail, dpfail, dppass); + } + } + } + public static boolean vendorIsAMD() { return VENDOR == AMD; } diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/LineBuffer.java b/src/main/java/com/gtnewhorizons/angelica/glsm/LineBuffer.java new file mode 100644 index 000000000..124f7ae84 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/LineBuffer.java @@ -0,0 +1,125 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat; +import com.gtnewhorizons.angelica.glsm.recording.AccumulatedLineDraw; +import com.gtnewhorizons.angelica.glsm.recording.ImmediateModeRecorder; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Accumulates line geometry during display list compilation. + * All line draws are collected here, then compiled into a single VBO. + *

+ * Lines all use the same format (POSITION_COLOR), so no format-based grouping needed. + *

+ * Tracks two types of ranges: + *

    + *
  • mergedRanges: Consecutive same-transform draws merged for optimized path
  • + *
  • perDrawRanges: One range per input draw for unoptimized path (1:1 with addDraw calls)
  • + *
+ */ +class LineBuffer { + final List allVertices = new ArrayList<>(); + + // Merged ranges for optimized path (consecutive same-transform draws combined) + final List mergedRanges = new ArrayList<>(); + + // Per-draw ranges for unoptimized path (1:1 with input draws) + final List perDrawRanges = new ArrayList<>(); + + int currentVertexOffset = 0; + + // Track the current range being built for merging consecutive same-transform draws + private int pendingStartVertex = -1; + private int pendingVertexCount = 0; + private Matrix4f pendingTransform = null; + private int pendingCommandIndex = -1; + + /** + * Add a line draw to this buffer. + * Tracks both merged (for optimized) and per-draw (for unoptimized) ranges. + */ + void addDraw(AccumulatedLineDraw draw) { + int vertexCount = draw.lines.size(); // Each LineVertex is one vertex, lines come in pairs + + // Always track per-draw range for unoptimized path + perDrawRanges.add(new DrawRange(currentVertexOffset, vertexCount, draw.transform, draw.commandIndex)); + + // Merge logic for optimized path + if (pendingTransform != null && pendingTransform.equals(draw.transform)) { + // Extend the pending range + pendingVertexCount += vertexCount; + } else { + // Flush pending range if any + flushPendingRange(); + + // Start new pending range + pendingStartVertex = currentVertexOffset; + pendingVertexCount = vertexCount; + pendingTransform = new Matrix4f(draw.transform); + pendingCommandIndex = draw.commandIndex; + } + + allVertices.addAll(draw.lines); + currentVertexOffset += vertexCount; + } + + private void flushPendingRange() { + if (pendingTransform != null) { + mergedRanges.add(new DrawRange(pendingStartVertex, pendingVertexCount, pendingTransform, pendingCommandIndex)); + pendingTransform = null; + } + } + + /** + * Compile all accumulated line vertices into a single VBO. + * @return The compiled buffer with VBO and both range types, or null if empty + */ + CompiledLineBuffer finish() { + if (allVertices.isEmpty()) { + return null; + } + + // Flush any remaining pending range + flushPendingRange(); + + VertexBuffer vbo = compileLinesToVBO(allVertices); + return new CompiledLineBuffer( + vbo, + mergedRanges.toArray(new DrawRange[0]), + perDrawRanges.toArray(new DrawRange[0]) + ); + } + + /** + * Compile line vertices into a VertexBuffer (VAO or VBO). + * Lines use POSITION_COLOR format (x, y, z, color). + */ + private static VertexBuffer compileLinesToVBO(List lines) { + final VertexFormat format = DefaultVertexFormat.POSITION_COLOR; + final VertexBuffer vao = VAOManager.createVAO(format, GL11.GL_LINES); + + // POSITION_COLOR format: 3 floats (12 bytes) + 4 bytes color = 16 bytes per vertex + final int vertexSize = format.getVertexSize(); + final ByteBuffer buffer = org.lwjgl.BufferUtils.createByteBuffer(vertexSize * lines.size()); + + for (ImmediateModeRecorder.LineVertex v : lines) { + buffer.putFloat(v.x); + buffer.putFloat(v.y); + buffer.putFloat(v.z); + buffer.putInt(v.color); + } + buffer.flip(); + + vao.upload(buffer); + + return vao; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/OptimizedListResult.java b/src/main/java/com/gtnewhorizons/angelica/glsm/OptimizedListResult.java new file mode 100644 index 000000000..f001fb653 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/OptimizedListResult.java @@ -0,0 +1,12 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; + +/** + * Result of building an optimized display list. + * Contains both the commands and the shared VBOs that need lifecycle management. + */ +@Desugar +record OptimizedListResult(DisplayListCommand[] commands, VertexBuffer[] ownedVbos) {} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java b/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java index 71e47e5ac..955ab2f60 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java @@ -131,6 +131,22 @@ public class RenderSystem { dsaState.texParameterf(texture, target, pname, param); } + public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) { + dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels); + } + + public static void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) { + dsaState.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels); + } + + public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) { + dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels); + } + + public static void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) { + dsaState.textureSubImage2D(texture, target, level, xoffset, yoffset, width, height, format, type, pixels); + } + public static String getProgramInfoLog(int program) { return GL20.glGetProgramInfoLog(program, GL20.glGetProgrami(program, GL20.GL_INFO_LOG_LENGTH)); } diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/TransformOptimizer.java b/src/main/java/com/gtnewhorizons/angelica/glsm/TransformOptimizer.java new file mode 100644 index 000000000..1fef31607 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/TransformOptimizer.java @@ -0,0 +1,137 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; +import com.gtnewhorizons.angelica.glsm.recording.commands.MultMatrixCmd; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; + +/** + * Helper class for tracking and collapsing MODELVIEW transforms during optimization. + * Implements the transform accumulation and emission strategy: + *
    + *
  • accumulated: total relative transform (what GL matrix should be)
  • + *
  • lastEmitted: what we've emitted (what GL matrix is)
  • + *
  • stack: saved accumulated values at Push points
  • + *
+ */ +class TransformOptimizer { + private static final Logger LOGGER = LogManager.getLogger("TransformOptimizer"); + + private final Matrix4f accumulated = new Matrix4f(); // Current accumulated transform + private final Matrix4f lastEmitted = new Matrix4f(); // Last emitted transform + private final Deque stack = new ArrayDeque<>(); // For Push/Pop + private final int listId; // For logging + private boolean absoluteMatrixFlag = false; // True after LoadMatrix, cleared by LoadIdentity emit + + TransformOptimizer(int listId) { + this.listId = listId; + accumulated.identity(); + lastEmitted.identity(); + } + + Matrix4f getAccumulated() { + return accumulated; + } + + void loadIdentity() { + accumulated.identity(); + } + + void pushTransform() { + stack.push(new Matrix4f(accumulated)); + } + + void popTransform() { + if (!stack.isEmpty()) { + final Matrix4f popped = stack.pop(); + accumulated.set(popped); + // After Pop, GL state is restored to the pushed value so lastEmitted should also reflect this + lastEmitted.set(accumulated); + } else { + LOGGER.warn("[TransformOptimizer] list={} Pop with empty stack - resetting to identity", listId); + accumulated.identity(); + lastEmitted.identity(); + } + } + + boolean isIdentity() { + return DisplayListManager.isIdentity(accumulated); + } + + /** + * Check if there's a pending transform that hasn't been emitted yet. + */ + boolean hasPendingTransform() { + return !accumulated.equals(lastEmitted); + } + + /** + * Emit a MultMatrix command if accumulated differs from lastEmitted. + * Updates lastEmitted to match accumulated after emission. + */ + void emitPendingTransform(List output) { + if (!accumulated.equals(lastEmitted)) { + emitTransformTo(output, accumulated); + } + } + + /** + * Emit a MultMatrix command to make GL state match a target transform. + * Used when a draw expects a specific transform that may differ from accumulated. + * Updates lastEmitted to match target after emission. + * + * @param output The command list to emit to + * @param target The target transform to reach + */ + void emitTransformTo(List output, Matrix4f target) { + if (target.equals(lastEmitted)) { + // Already at target - nothing to emit + return; + } + + // Calculate delta: what we need to multiply to go from lastEmitted to target + // delta = inverse(lastEmitted) * target + // But for simplicity, if lastEmitted is identity, just emit target + if (DisplayListManager.isIdentity(lastEmitted)) { + // Simple case: just emit target + output.add(MultMatrixCmd.create(target, GL11.GL_MODELVIEW)); + } else { + // Need to emit delta: inv(lastEmitted) * target + final Matrix4f delta = new Matrix4f(lastEmitted).invert().mul(target); + output.add(MultMatrixCmd.create(delta, GL11.GL_MODELVIEW)); + } + lastEmitted.set(target); + } + + /** + * Check if a draw's expected transform differs from what we've emitted. + */ + boolean needsTransformForDraw(Matrix4f drawTransform) { + return !drawTransform.equals(lastEmitted); + } + + /** + * Mark that the GL matrix is at an absolute value (after LoadMatrix). + * Subsequent LoadIdentity commands must be emitted to actually reset GL to identity. + */ + void markAbsoluteMatrix() { + absoluteMatrixFlag = true; + } + + /** + * Check if the GL matrix is at an absolute value and clear the flag. + * Used by LoadIdentityCmd to determine if it needs to emit. + * @return true if LoadMatrix was called and not yet reset by LoadIdentity + */ + boolean checkAndClearAbsoluteMatrix() { + boolean was = absoluteMatrixFlag; + absoluteMatrixFlag = false; + return was; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java b/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java index 765c8c6bf..9af7dc038 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java @@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager; import org.lwjgl.opengl.ARBDirectStateAccess; import org.lwjgl.opengl.GL45; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; @@ -14,6 +15,29 @@ public class DSAARB extends DSAUnsupported { ARBDirectStateAccess.glGenerateTextureMipmap(texture); } + @Override + public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) { + // Note: DSA glTextureImage2D doesn't exist in ARB_direct_state_access + // We need to use glTextureStorage2D + glTextureSubImage2D, or fall back to bind-based approach + // For simplicity, use the fallback which binds, uploads, and restores + super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels); + } + + @Override + public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) { + super.textureImage2D(texture, target, level, internalformat, width, height, border, format, type, pixels); + } + + @Override + public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) { + ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels); + } + + @Override + public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) { + ARBDirectStateAccess.glTextureSubImage2D(texture, level, xoffset, yoffset, width, height, format, type, pixels); + } + @Override public void texParameteri(int texture, int target, int pname, int param) { if(!GLStateManager.updateTexParameteriCache(target, texture, pname, param)) return; diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAAccess.java b/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAAccess.java index e0154eef6..babc2de55 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAAccess.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAAccess.java @@ -1,11 +1,18 @@ package com.gtnewhorizons.angelica.glsm.dsa; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; public interface DSAAccess { void generateMipmaps(int texture, int target); + // Texture image upload - DSA allows specifying texture directly without binding + void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels); + void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels); + void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels); + void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels); + void texParameteri(int texture, int target, int pname, int param); void texParameterf(int texture, int target, int pname, float param); void texParameteriv(int texture, int target, int pname, IntBuffer params); diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java b/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java index 5bdc8e971..813e120e9 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java @@ -9,6 +9,7 @@ import org.lwjgl.opengl.GL15; import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL30; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; @@ -20,6 +21,39 @@ public class DSAUnsupported implements DSAAccess { GL30.glGenerateMipmap(target); } + @Override + public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) { + // Get what cache thinks is bound - we'll restore to this to keep cache/GL in sync + final int cachedBinding = GLStateManager.getBoundTexture(); + GL11.glBindTexture(target, texture); + GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + GL11.glBindTexture(target, cachedBinding); // Restore to cache value + } + + @Override + public void textureImage2D(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, IntBuffer pixels) { + final int cachedBinding = GLStateManager.getBoundTexture(); + GL11.glBindTexture(target, texture); + GL11.glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels); + GL11.glBindTexture(target, cachedBinding); + } + + @Override + public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, ByteBuffer pixels) { + final int cachedBinding = GLStateManager.getBoundTexture(); + GL11.glBindTexture(target, texture); + GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + GL11.glBindTexture(target, cachedBinding); + } + + @Override + public void textureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, IntBuffer pixels) { + final int cachedBinding = GLStateManager.getBoundTexture(); + GL11.glBindTexture(target, texture); + GL11.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels); + GL11.glBindTexture(target, cachedBinding); + } + @Override public void texParameteri(int texture, int target, int pname, int param) { GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/AccumulatedDraw.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/AccumulatedDraw.java new file mode 100644 index 000000000..2a7a07d57 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/AccumulatedDraw.java @@ -0,0 +1,35 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil; +import org.joml.Matrix4f; + +import java.util.List; + +/** + * Represents an accumulated draw call during display list compilation. + * Matrix-as-Data Architecture: Stores untransformed vertices + captured matrix. + * The matrix will be applied at runtime, not baked into vertices. + */ +public class AccumulatedDraw { + public final List quads; + public final Matrix4f transform; + public final CapturingTessellator.Flags flags; + public final int commandIndex; // Position in command list for state tracking + + public AccumulatedDraw(List quads, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex) { + // Deep copy quads - pooled quads are reused after callback + this.quads = ModelQuadUtil.deepCopyQuads(quads); + + this.transform = new Matrix4f(transform); // Snapshot for runtime application + this.flags = flags; + this.commandIndex = commandIndex; + } + + @Override + public String toString() { + return String.format("AccumulatedDraw[quads=%d, cmdIdx=%d, flags=C%dT%dL%dN%d]", + quads.size(), commandIndex, flags.hasColor ? 1 : 0, flags.hasTexture ? 1 : 0, flags.hasBrightness ? 1 : 0, flags.hasNormals ? 1 : 0); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/AccumulatedLineDraw.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/AccumulatedLineDraw.java new file mode 100644 index 000000000..1a7e816dd --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/AccumulatedLineDraw.java @@ -0,0 +1,33 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import org.joml.Matrix4f; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an accumulated line draw call during display list compilation. + * Similar to AccumulatedDraw but for line primitives. + * Stores untransformed line vertices + captured matrix. + */ +public class AccumulatedLineDraw { + public final List lines; + public final Matrix4f transform; + public final CapturingTessellator.Flags flags; + public final int commandIndex; // Position in command list for state tracking + + public AccumulatedLineDraw(List lines, Matrix4f transform, CapturingTessellator.Flags flags, int commandIndex) { + // Copy lines - the source list may be cleared + this.lines = new ArrayList<>(lines); + this.transform = new Matrix4f(transform); // Snapshot for runtime application + this.flags = flags; + this.commandIndex = commandIndex; + } + + @Override + public String toString() { + return String.format("AccumulatedLineDraw[lines=%d, cmdIdx=%d, flags=C%dT%dL%dN%d]", + lines.size() / 2, commandIndex, flags.hasColor ? 1 : 0, flags.hasTexture ? 1 : 0, flags.hasBrightness ? 1 : 0, flags.hasNormals ? 1 : 0); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBuffer.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBuffer.java new file mode 100644 index 000000000..43af1f36d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBuffer.java @@ -0,0 +1,660 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; +import org.joml.Matrix4f; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.List; + +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*; + +public final class CommandBuffer { + private static final int DEFAULT_CAPACITY = 4096; // Guess at typical display list size + + private ByteBuffer buffer; + private long basePointer; + private long writePointer; + private final List complexObjects = new ArrayList<>(); + + public CommandBuffer() { + this(DEFAULT_CAPACITY); + } + + public CommandBuffer(int initialCapacity) { + this.buffer = memAlloc(initialCapacity); + this.basePointer = memAddress(buffer); + this.writePointer = basePointer; + } + + // === Low-level write operations === + + private void writeInt(int value) { + memPutInt(writePointer, value); + writePointer += 4; + } + + private void writeFloat(float value) { + memPutFloat(writePointer, value); + writePointer += 4; + } + + private void writeDouble(double value) { + memPutDouble(writePointer, value); + writePointer += 8; + } + + private void ensureCapacity(int needed) { + final int size = size(); + final int remaining = buffer.capacity() - size; + if (remaining < needed) { + final int newCapacity = Math.max(buffer.capacity() * 2, buffer.capacity() + needed); + buffer = memRealloc(buffer, newCapacity); + basePointer = memAddress(buffer); + writePointer = basePointer + size; + } + } + + // === Single int commands === + + public void writeEnable(int cap) { + ensureCapacity(8); + writeInt(GLCommand.ENABLE); + writeInt(cap); + } + + public void writeDisable(int cap) { + ensureCapacity(8); + writeInt(GLCommand.DISABLE); + writeInt(cap); + } + + public void writeClear(int mask) { + ensureCapacity(8); + writeInt(GLCommand.CLEAR); + writeInt(mask); + } + + public void writeClearStencil(int s) { + ensureCapacity(8); + writeInt(GLCommand.CLEAR_STENCIL); + writeInt(s); + } + + public void writeCullFace(int mode) { + ensureCapacity(8); + writeInt(GLCommand.CULL_FACE); + writeInt(mode); + } + + public void writeDepthFunc(int func) { + ensureCapacity(8); + writeInt(GLCommand.DEPTH_FUNC); + writeInt(func); + } + + public void writeShadeModel(int mode) { + ensureCapacity(8); + writeInt(GLCommand.SHADE_MODEL); + writeInt(mode); + } + + public void writeLogicOp(int opcode) { + ensureCapacity(8); + writeInt(GLCommand.LOGIC_OP); + writeInt(opcode); + } + + public void writeMatrixMode(int mode) { + ensureCapacity(8); + writeInt(GLCommand.MATRIX_MODE); + writeInt(mode); + } + + public void writeActiveTexture(int texture) { + ensureCapacity(8); + writeInt(GLCommand.ACTIVE_TEXTURE); + writeInt(texture); + } + + public void writeUseProgram(int program) { + ensureCapacity(8); + writeInt(GLCommand.USE_PROGRAM); + writeInt(program); + } + + public void writePushAttrib(int mask) { + ensureCapacity(8); + writeInt(GLCommand.PUSH_ATTRIB); + writeInt(mask); + } + + public void writePopAttrib() { + ensureCapacity(8); + writeInt(GLCommand.POP_ATTRIB); + writeInt(0); // unused padding + } + + public void writeLoadIdentity(int matrixMode) { + ensureCapacity(8); + writeInt(GLCommand.LOAD_IDENTITY); + writeInt(matrixMode); + } + + public void writePushMatrix(int matrixMode) { + ensureCapacity(8); + writeInt(GLCommand.PUSH_MATRIX); + writeInt(matrixMode); + } + + public void writePopMatrix(int matrixMode) { + ensureCapacity(8); + writeInt(GLCommand.POP_MATRIX); + writeInt(matrixMode); + } + + public void writeStencilMask(int mask) { + ensureCapacity(8); + writeInt(GLCommand.STENCIL_MASK); + writeInt(mask); + } + + public void writeDepthMask(boolean flag) { + ensureCapacity(8); + writeInt(GLCommand.DEPTH_MASK); + writeInt(flag ? 1 : 0); + } + + // === Two int commands === + + public void writeBindTexture(int target, int texture) { + ensureCapacity(12); + writeInt(GLCommand.BIND_TEXTURE); + writeInt(target); + writeInt(texture); + } + + public void writePolygonMode(int face, int mode) { + ensureCapacity(12); + writeInt(GLCommand.POLYGON_MODE); + writeInt(face); + writeInt(mode); + } + + public void writeColorMaterial(int face, int mode) { + ensureCapacity(12); + writeInt(GLCommand.COLOR_MATERIAL); + writeInt(face); + writeInt(mode); + } + + public void writeLineStipple(int factor, int pattern) { + ensureCapacity(12); + writeInt(GLCommand.LINE_STIPPLE); + writeInt(factor); + writeInt(pattern); + } + + public void writeStencilMaskSeparate(int face, int mask) { + ensureCapacity(12); + writeInt(GLCommand.STENCIL_MASK_SEPARATE); + writeInt(face); + writeInt(mask); + } + + public void writeFogi(int pname, int param) { + ensureCapacity(12); + writeInt(GLCommand.FOGI); + writeInt(pname); + writeInt(param); + } + + // === Three int commands === + + public void writeStencilFunc(int func, int ref, int mask) { + ensureCapacity(16); + writeInt(GLCommand.STENCIL_FUNC); + writeInt(func); + writeInt(ref); + writeInt(mask); + } + + public void writeStencilOp(int fail, int zfail, int zpass) { + ensureCapacity(16); + writeInt(GLCommand.STENCIL_OP); + writeInt(fail); + writeInt(zfail); + writeInt(zpass); + } + + public void writeTexParameteri(int target, int pname, int param) { + ensureCapacity(16); + writeInt(GLCommand.TEX_PARAMETERI); + writeInt(target); + writeInt(pname); + writeInt(param); + } + + // === Four int commands === + + public void writeViewport(int x, int y, int width, int height) { + ensureCapacity(20); + writeInt(GLCommand.VIEWPORT); + writeInt(x); + writeInt(y); + writeInt(width); + writeInt(height); + } + + public void writeBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { + ensureCapacity(20); + writeInt(GLCommand.BLEND_FUNC); + writeInt(srcRgb); + writeInt(dstRgb); + writeInt(srcAlpha); + writeInt(dstAlpha); + } + + public void writeColorMask(boolean red, boolean green, boolean blue, boolean alpha) { + ensureCapacity(20); + writeInt(GLCommand.COLOR_MASK); + writeInt(red ? 1 : 0); + writeInt(green ? 1 : 0); + writeInt(blue ? 1 : 0); + writeInt(alpha ? 1 : 0); + } + + public void writeStencilFuncSeparate(int face, int func, int ref, int mask) { + ensureCapacity(20); + writeInt(GLCommand.STENCIL_FUNC_SEPARATE); + writeInt(face); + writeInt(func); + writeInt(ref); + writeInt(mask); + } + + public void writeStencilOpSeparate(int face, int sfail, int dpfail, int dppass) { + ensureCapacity(20); + writeInt(GLCommand.STENCIL_OP_SEPARATE); + writeInt(face); + writeInt(sfail); + writeInt(dpfail); + writeInt(dppass); + } + + // === Float commands === + + public void writePointSize(float size) { + ensureCapacity(8); + writeInt(GLCommand.POINT_SIZE); + writeFloat(size); + } + + public void writeLineWidth(float width) { + ensureCapacity(8); + writeInt(GLCommand.LINE_WIDTH); + writeFloat(width); + } + + public void writePolygonOffset(float factor, float units) { + ensureCapacity(12); + writeInt(GLCommand.POLYGON_OFFSET); + writeFloat(factor); + writeFloat(units); + } + + public void writeNormal(float x, float y, float z) { + ensureCapacity(16); + writeInt(GLCommand.NORMAL); + writeFloat(x); + writeFloat(y); + writeFloat(z); + } + + public void writeColor(float r, float g, float b, float a) { + ensureCapacity(20); + writeInt(GLCommand.COLOR); + writeFloat(r); + writeFloat(g); + writeFloat(b); + writeFloat(a); + } + + public void writeClearColor(float r, float g, float b, float a) { + ensureCapacity(20); + writeInt(GLCommand.CLEAR_COLOR); + writeFloat(r); + writeFloat(g); + writeFloat(b); + writeFloat(a); + } + + // === Mixed int+float commands === + + public void writeAlphaFunc(int func, float ref) { + ensureCapacity(12); + writeInt(GLCommand.ALPHA_FUNC); + writeInt(func); + writeFloat(ref); + } + + public void writeFogf(int pname, float param) { + ensureCapacity(12); + writeInt(GLCommand.FOGF); + writeInt(pname); + writeFloat(param); + } + + public void writeLightf(int light, int pname, float param) { + ensureCapacity(16); + writeInt(GLCommand.LIGHTF); + writeInt(light); + writeInt(pname); + writeFloat(param); + } + + public void writeLightModelf(int pname, float param) { + ensureCapacity(12); + writeInt(GLCommand.LIGHT_MODELF); + writeInt(pname); + writeFloat(param); + } + + public void writeLighti(int light, int pname, int param) { + ensureCapacity(16); + writeInt(GLCommand.LIGHTI); + writeInt(light); + writeInt(pname); + writeInt(param); + } + + public void writeLightModeli(int pname, int param) { + ensureCapacity(12); + writeInt(GLCommand.LIGHT_MODELI); + writeInt(pname); + writeInt(param); + } + + public void writeMaterialf(int face, int pname, float param) { + ensureCapacity(16); + writeInt(GLCommand.MATERIALF); + writeInt(face); + writeInt(pname); + writeFloat(param); + } + + public void writeTexParameterf(int target, int pname, float param) { + ensureCapacity(16); + writeInt(GLCommand.TEX_PARAMETERF); + writeInt(target); + writeInt(pname); + writeFloat(param); + } + + // === Double commands === + + public void writeTranslate(int matrixMode, double x, double y, double z) { + ensureCapacity(32); + writeInt(GLCommand.TRANSLATE); + writeInt(matrixMode); + writeDouble(x); + writeDouble(y); + writeDouble(z); + } + + public void writeRotate(int matrixMode, double angle, double x, double y, double z) { + ensureCapacity(40); + writeInt(GLCommand.ROTATE); + writeInt(matrixMode); + writeDouble(angle); + writeDouble(x); + writeDouble(y); + writeDouble(z); + } + + public void writeScale(int matrixMode, double x, double y, double z) { + ensureCapacity(32); + writeInt(GLCommand.SCALE); + writeInt(matrixMode); + writeDouble(x); + writeDouble(y); + writeDouble(z); + } + + public void writeOrtho(double left, double right, double bottom, double top, double zNear, double zFar) { + ensureCapacity(52); + writeInt(GLCommand.ORTHO); + writeDouble(left); + writeDouble(right); + writeDouble(bottom); + writeDouble(top); + writeDouble(zNear); + writeDouble(zFar); + } + + public void writeFrustum(double left, double right, double bottom, double top, double zNear, double zFar) { + ensureCapacity(52); + writeInt(GLCommand.FRUSTUM); + writeDouble(left); + writeDouble(right); + writeDouble(bottom); + writeDouble(top); + writeDouble(zNear); + writeDouble(zFar); + } + + // === Matrix commands === + + public void writeMultMatrix(int matrixMode, Matrix4f matrix) { + ensureCapacity(72); + writeInt(GLCommand.MULT_MATRIX); + writeInt(matrixMode); + writeMatrix4f(matrix); + } + + public void writeLoadMatrix(int matrixMode, Matrix4f matrix) { + ensureCapacity(72); + writeInt(GLCommand.LOAD_MATRIX); + writeInt(matrixMode); + writeMatrix4f(matrix); + } + + /** + * Write a Matrix4f to the buffer in column-major order (OpenGL convention). + * Uses accessor methods instead of getToAddress() to avoid JOML MemUtil issues. + */ + private void writeMatrix4f(Matrix4f m) { + // Column 0 + writeFloat(m.m00()); writeFloat(m.m01()); writeFloat(m.m02()); writeFloat(m.m03()); + // Column 1 + writeFloat(m.m10()); writeFloat(m.m11()); writeFloat(m.m12()); writeFloat(m.m13()); + // Column 2 + writeFloat(m.m20()); writeFloat(m.m21()); writeFloat(m.m22()); writeFloat(m.m23()); + // Column 3 + writeFloat(m.m30()); writeFloat(m.m31()); writeFloat(m.m32()); writeFloat(m.m33()); + } + + // === Buffer commands (inline 4 floats) === + + public void writeFog(int pname, FloatBuffer params) { + final int count = Math.min(params.remaining(), 4); + ensureCapacity(12 + count * 4); + writeInt(GLCommand.FOG); + writeInt(pname); + writeInt(count); + final int pos = params.position(); + for (int i = 0; i < count; i++) { + writeFloat(params.get(pos + i)); + } + } + + public void writeLight(int light, int pname, FloatBuffer params) { + final int count = Math.min(params.remaining(), 4); + ensureCapacity(16 + count * 4); + writeInt(GLCommand.LIGHT); + writeInt(light); + writeInt(pname); + writeInt(count); + final int pos = params.position(); + for (int i = 0; i < count; i++) { + writeFloat(params.get(pos + i)); + } + } + + public void writeLightModel(int pname, FloatBuffer params) { + final int count = Math.min(params.remaining(), 4); + ensureCapacity(12 + count * 4); + writeInt(GLCommand.LIGHT_MODEL); + writeInt(pname); + writeInt(count); + final int pos = params.position(); + for (int i = 0; i < count; i++) { + writeFloat(params.get(pos + i)); + } + } + + public void writeMaterial(int face, int pname, FloatBuffer params) { + final int count = Math.min(params.remaining(), 4); + ensureCapacity(16 + count * 4); + writeInt(GLCommand.MATERIAL); + writeInt(face); + writeInt(pname); + writeInt(count); + final int pos = params.position(); + for (int i = 0; i < count; i++) { + writeFloat(params.get(pos + i)); + } + } + + // === Draw commands === + + public void writeDrawRange(int vboIndex, int startVertex, int vertexCount, boolean hasBrightness) { + ensureCapacity(20); + writeInt(GLCommand.DRAW_RANGE); + writeInt(vboIndex); + writeInt(startVertex); + writeInt(vertexCount); + writeInt(hasBrightness ? 1 : 0); + } + + public void writeCallList(int listId) { + ensureCapacity(8); + writeInt(GLCommand.CALL_LIST); + writeInt(listId); + } + + // === Complex object reference === + + public void writeComplexRef(DisplayListCommand cmd) { + ensureCapacity(8); + final int index = complexObjects.size(); + complexObjects.add(cmd); + writeInt(GLCommand.COMPLEX_REF); + writeInt(index); + } + + // === Reading methods for optimization pass === + + private long readPointer = 0; + + /** + * Reset read position to the beginning of the buffer. + */ + public void resetRead() { + readPointer = basePointer; + } + + /** + * Check if there's more data to read. + */ + public boolean hasRemaining() { + return readPointer < writePointer; + } + + /** + * Read an int from the current position. + */ + public int readInt() { + final int value = memGetInt(readPointer); + readPointer += 4; + return value; + } + + /** + * Read a float from the current position. + */ + public float readFloat() { + final float value = memGetFloat(readPointer); + readPointer += 4; + return value; + } + + /** + * Read a double from the current position. + */ + public double readDouble() { + final double value = memGetDouble(readPointer); + readPointer += 8; + return value; + } + + /** + * Read a Matrix4f from the current position (16 floats in column-major order). + */ + public Matrix4f readMatrix4f(Matrix4f dest) { + dest.m00(readFloat()); dest.m01(readFloat()); dest.m02(readFloat()); dest.m03(readFloat()); + dest.m10(readFloat()); dest.m11(readFloat()); dest.m12(readFloat()); dest.m13(readFloat()); + dest.m20(readFloat()); dest.m21(readFloat()); dest.m22(readFloat()); dest.m23(readFloat()); + dest.m30(readFloat()); dest.m31(readFloat()); dest.m32(readFloat()); dest.m33(readFloat()); + return dest; + } + + // === Lifecycle === + + public int size() { + return (int) (writePointer - basePointer); + } + + public long address() { + return basePointer; + } + + public ByteBuffer toBuffer() { + buffer.limit(size()); + buffer.position(0); + return buffer; + } + + public Object[] getComplexObjects() { + return complexObjects.toArray(); + } + + public Object getComplexObject(int index) { + return complexObjects.get(index); + } + + public boolean isEmpty() { + return size() == 0 && complexObjects.isEmpty(); + } + + /** + * Clear the buffer for reuse. Does NOT free memory. + */ + public void clear() { + writePointer = basePointer; + complexObjects.clear(); + } + + /** + * Free the underlying buffer. Must be called when done. + */ + public void free() { + if (buffer != null) { + memFree(buffer); + buffer = null; + basePointer = 0; + writePointer = 0; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBufferExecutor.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBufferExecutor.java new file mode 100644 index 000000000..f92c0f113 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBufferExecutor.java @@ -0,0 +1,494 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizons.angelica.glsm.DisplayListManager; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; +import org.lwjgl.BufferUtils; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*; + +/** + * Executes commands from a CommandBuffer + */ +public final class CommandBufferExecutor { + private CommandBufferExecutor() {} + + // Static reusable buffer for matrix ops (render thread only, LWJGL2/3 compatible) + private static final FloatBuffer MATRIX_BUFFER = BufferUtils.createFloatBuffer(16); + // Static reusable buffer for fog/light/material params + private static final FloatBuffer PARAMS_BUFFER = BufferUtils.createFloatBuffer(4); + + /** + * Execute all commands in the buffer. + * + * @param buffer The command buffer (position should be at 0, limit at size) + * @param complexObjects Array of complex objects (TexImage2DCmd, etc.) + * @param ownedVbos Array of VBOs owned by the display list (indexed by DrawRange commands) + */ + public static void execute(ByteBuffer buffer, Object[] complexObjects, VertexBuffer[] ownedVbos) { + long ptr = memAddress(buffer); + final long end = ptr + buffer.limit(); + + while (ptr < end) { + final int cmd = memGetInt(ptr); + ptr += 4; + + switch (cmd) { + // === Single int commands === + case GLCommand.ENABLE -> { + GLStateManager.glEnable(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.DISABLE -> { + GLStateManager.glDisable(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.CLEAR -> { + GLStateManager.glClear(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.CLEAR_STENCIL -> { + GLStateManager.glClearStencil(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.CULL_FACE -> { + GLStateManager.glCullFace(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.DEPTH_FUNC -> { + GLStateManager.glDepthFunc(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.SHADE_MODEL -> { + GLStateManager.glShadeModel(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.LOGIC_OP -> { + GLStateManager.glLogicOp(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.MATRIX_MODE -> { + GLStateManager.glMatrixMode(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.ACTIVE_TEXTURE -> { + GLStateManager.glActiveTexture(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.USE_PROGRAM -> { + GLStateManager.glUseProgram(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.PUSH_ATTRIB -> { + GLStateManager.glPushAttrib(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.POP_ATTRIB -> { + GLStateManager.glPopAttrib(); + ptr += 4; // Skip unused padding + } + case GLCommand.LOAD_IDENTITY -> { + final int mode = memGetInt(ptr); + ptr += 4; + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glLoadIdentity(); + GLStateManager.glMatrixMode(savedMode); + } + case GLCommand.PUSH_MATRIX -> { + final int mode = memGetInt(ptr); + ptr += 4; + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glPushMatrix(); + GLStateManager.glMatrixMode(savedMode); + } + case GLCommand.POP_MATRIX -> { + final int mode = memGetInt(ptr); + ptr += 4; + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glPopMatrix(); + GLStateManager.glMatrixMode(savedMode); + } + case GLCommand.STENCIL_MASK -> { + GLStateManager.glStencilMask(memGetInt(ptr)); + ptr += 4; + } + case GLCommand.DEPTH_MASK -> { + GLStateManager.glDepthMask(memGetInt(ptr) != 0); + ptr += 4; + } + + // === Two int commands === + case GLCommand.BIND_TEXTURE -> { + final int target = memGetInt(ptr); + final int texture = memGetInt(ptr + 4); + ptr += 8; + GLStateManager.glBindTexture(target, texture); + } + case GLCommand.POLYGON_MODE -> { + final int face = memGetInt(ptr); + final int mode = memGetInt(ptr + 4); + ptr += 8; + GLStateManager.glPolygonMode(face, mode); + } + case GLCommand.COLOR_MATERIAL -> { + final int face = memGetInt(ptr); + final int mode = memGetInt(ptr + 4); + ptr += 8; + GLStateManager.glColorMaterial(face, mode); + } + case GLCommand.LINE_STIPPLE -> { + final int factor = memGetInt(ptr); + final int pattern = memGetInt(ptr + 4); + ptr += 8; + GLStateManager.glLineStipple(factor, (short) pattern); + } + case GLCommand.STENCIL_MASK_SEPARATE -> { + final int face = memGetInt(ptr); + final int mask = memGetInt(ptr + 4); + ptr += 8; + GLStateManager.glStencilMaskSeparate(face, mask); + } + case GLCommand.FOGI -> { + final int pname = memGetInt(ptr); + final int param = memGetInt(ptr + 4); + ptr += 8; + GLStateManager.glFogi(pname, param); + } + + // === Three int commands === + case GLCommand.STENCIL_FUNC -> { + final int func = memGetInt(ptr); + final int ref = memGetInt(ptr + 4); + final int mask = memGetInt(ptr + 8); + ptr += 12; + GLStateManager.glStencilFunc(func, ref, mask); + } + case GLCommand.STENCIL_OP -> { + final int fail = memGetInt(ptr); + final int zfail = memGetInt(ptr + 4); + final int zpass = memGetInt(ptr + 8); + ptr += 12; + GLStateManager.glStencilOp(fail, zfail, zpass); + } + case GLCommand.TEX_PARAMETERI -> { + final int target = memGetInt(ptr); + final int pname = memGetInt(ptr + 4); + final int param = memGetInt(ptr + 8); + ptr += 12; + GLStateManager.glTexParameteri(target, pname, param); + } + + // === Four int commands === + case GLCommand.VIEWPORT -> { + final int x = memGetInt(ptr); + final int y = memGetInt(ptr + 4); + final int width = memGetInt(ptr + 8); + final int height = memGetInt(ptr + 12); + ptr += 16; + GLStateManager.glViewport(x, y, width, height); + } + case GLCommand.BLEND_FUNC -> { + final int srcRgb = memGetInt(ptr); + final int dstRgb = memGetInt(ptr + 4); + final int srcAlpha = memGetInt(ptr + 8); + final int dstAlpha = memGetInt(ptr + 12); + ptr += 16; + if (srcRgb == srcAlpha && dstRgb == dstAlpha) { + GLStateManager.glBlendFunc(srcRgb, dstRgb); + } else { + GLStateManager.tryBlendFuncSeparate(srcRgb, dstRgb, srcAlpha, dstAlpha); + } + } + case GLCommand.COLOR_MASK -> { + final boolean r = memGetInt(ptr) != 0; + final boolean g = memGetInt(ptr + 4) != 0; + final boolean b = memGetInt(ptr + 8) != 0; + final boolean a = memGetInt(ptr + 12) != 0; + ptr += 16; + GLStateManager.glColorMask(r, g, b, a); + } + case GLCommand.STENCIL_FUNC_SEPARATE -> { + final int face = memGetInt(ptr); + final int func = memGetInt(ptr + 4); + final int ref = memGetInt(ptr + 8); + final int mask = memGetInt(ptr + 12); + ptr += 16; + GLStateManager.glStencilFuncSeparate(face, func, ref, mask); + } + case GLCommand.STENCIL_OP_SEPARATE -> { + final int face = memGetInt(ptr); + final int sfail = memGetInt(ptr + 4); + final int dpfail = memGetInt(ptr + 8); + final int dppass = memGetInt(ptr + 12); + ptr += 16; + GLStateManager.glStencilOpSeparate(face, sfail, dpfail, dppass); + } + + // === Float commands === + case GLCommand.POINT_SIZE -> { + GLStateManager.glPointSize(memGetFloat(ptr)); + ptr += 4; + } + case GLCommand.LINE_WIDTH -> { + GLStateManager.glLineWidth(memGetFloat(ptr)); + ptr += 4; + } + case GLCommand.POLYGON_OFFSET -> { + final float factor = memGetFloat(ptr); + final float units = memGetFloat(ptr + 4); + ptr += 8; + GLStateManager.glPolygonOffset(factor, units); + } + case GLCommand.NORMAL -> { + final float x = memGetFloat(ptr); + final float y = memGetFloat(ptr + 4); + final float z = memGetFloat(ptr + 8); + ptr += 12; + GLStateManager.glNormal3f(x, y, z); + } + case GLCommand.COLOR -> { + final float r = memGetFloat(ptr); + final float g = memGetFloat(ptr + 4); + final float b = memGetFloat(ptr + 8); + final float a = memGetFloat(ptr + 12); + ptr += 16; + GLStateManager.glColor4f(r, g, b, a); + } + case GLCommand.CLEAR_COLOR -> { + final float r = memGetFloat(ptr); + final float g = memGetFloat(ptr + 4); + final float b = memGetFloat(ptr + 8); + final float a = memGetFloat(ptr + 12); + ptr += 16; + GLStateManager.glClearColor(r, g, b, a); + } + + // === Mixed int+float commands === + case GLCommand.ALPHA_FUNC -> { + final int func = memGetInt(ptr); + final float ref = memGetFloat(ptr + 4); + ptr += 8; + GLStateManager.glAlphaFunc(func, ref); + } + case GLCommand.FOGF -> { + final int pname = memGetInt(ptr); + final float param = memGetFloat(ptr + 4); + ptr += 8; + GLStateManager.glFogf(pname, param); + } + case GLCommand.LIGHTF -> { + final int light = memGetInt(ptr); + final int pname = memGetInt(ptr + 4); + final float param = memGetFloat(ptr + 8); + ptr += 12; + GLStateManager.glLightf(light, pname, param); + } + case GLCommand.LIGHT_MODELF -> { + final int pname = memGetInt(ptr); + final float param = memGetFloat(ptr + 4); + ptr += 8; + GLStateManager.glLightModelf(pname, param); + } + case GLCommand.LIGHTI -> { + final int light = memGetInt(ptr); + final int pname = memGetInt(ptr + 4); + final int param = memGetInt(ptr + 8); + ptr += 12; + GLStateManager.glLighti(light, pname, param); + } + case GLCommand.LIGHT_MODELI -> { + final int pname = memGetInt(ptr); + final int param = memGetInt(ptr + 4); + ptr += 8; + GLStateManager.glLightModeli(pname, param); + } + case GLCommand.MATERIALF -> { + final int face = memGetInt(ptr); + final int pname = memGetInt(ptr + 4); + final float param = memGetFloat(ptr + 8); + ptr += 12; + GLStateManager.glMaterialf(face, pname, param); + } + case GLCommand.TEX_PARAMETERF -> { + final int target = memGetInt(ptr); + final int pname = memGetInt(ptr + 4); + final float param = memGetFloat(ptr + 8); + ptr += 12; + GLStateManager.glTexParameterf(target, pname, param); + } + + // === Double commands === + case GLCommand.TRANSLATE -> { + final int mode = memGetInt(ptr); + final double x = memGetDouble(ptr + 4); + final double y = memGetDouble(ptr + 12); + final double z = memGetDouble(ptr + 20); + ptr += 28; + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glTranslated(x, y, z); + GLStateManager.glMatrixMode(savedMode); + } + case GLCommand.ROTATE -> { + final int mode = memGetInt(ptr); + final double angle = memGetDouble(ptr + 4); + final double x = memGetDouble(ptr + 12); + final double y = memGetDouble(ptr + 20); + final double z = memGetDouble(ptr + 28); + ptr += 36; + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glRotated(angle, x, y, z); + GLStateManager.glMatrixMode(savedMode); + } + case GLCommand.SCALE -> { + final int mode = memGetInt(ptr); + final double x = memGetDouble(ptr + 4); + final double y = memGetDouble(ptr + 12); + final double z = memGetDouble(ptr + 20); + ptr += 28; + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glScaled(x, y, z); + GLStateManager.glMatrixMode(savedMode); + } + case GLCommand.ORTHO -> { + final double left = memGetDouble(ptr); + final double right = memGetDouble(ptr + 8); + final double bottom = memGetDouble(ptr + 16); + final double top = memGetDouble(ptr + 24); + final double zNear = memGetDouble(ptr + 32); + final double zFar = memGetDouble(ptr + 40); + ptr += 48; + GLStateManager.glOrtho(left, right, bottom, top, zNear, zFar); + } + case GLCommand.FRUSTUM -> { + final double left = memGetDouble(ptr); + final double right = memGetDouble(ptr + 8); + final double bottom = memGetDouble(ptr + 16); + final double top = memGetDouble(ptr + 24); + final double zNear = memGetDouble(ptr + 32); + final double zFar = memGetDouble(ptr + 40); + ptr += 48; + GLStateManager.glFrustum(left, right, bottom, top, zNear, zFar); + } + + // === Matrix commands === + case GLCommand.MULT_MATRIX -> { + final int mode = memGetInt(ptr); + ptr += 4; + MATRIX_BUFFER.clear(); + for (int i = 0; i < 16; i++) { + MATRIX_BUFFER.put(memGetFloat(ptr)); + ptr += 4; + } + MATRIX_BUFFER.flip(); + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glMultMatrix(MATRIX_BUFFER); + GLStateManager.glMatrixMode(savedMode); + } + case GLCommand.LOAD_MATRIX -> { + final int mode = memGetInt(ptr); + ptr += 4; + MATRIX_BUFFER.clear(); + for (int i = 0; i < 16; i++) { + MATRIX_BUFFER.put(memGetFloat(ptr)); + ptr += 4; + } + MATRIX_BUFFER.flip(); + final int savedMode = GLStateManager.getMatrixMode().getMode(); + GLStateManager.glMatrixMode(mode); + GLStateManager.glLoadMatrix(MATRIX_BUFFER); + GLStateManager.glMatrixMode(savedMode); + } + + // === Buffer commands === + case GLCommand.FOG -> { + final int pname = memGetInt(ptr); + final int count = memGetInt(ptr + 4); + ptr += 8; + PARAMS_BUFFER.clear(); + for (int i = 0; i < count; i++) { + PARAMS_BUFFER.put(memGetFloat(ptr)); + ptr += 4; + } + PARAMS_BUFFER.flip(); + GLStateManager.glFog(pname, PARAMS_BUFFER); + } + case GLCommand.LIGHT -> { + final int light = memGetInt(ptr); + final int pname = memGetInt(ptr + 4); + final int count = memGetInt(ptr + 8); + ptr += 12; + PARAMS_BUFFER.clear(); + for (int i = 0; i < count; i++) { + PARAMS_BUFFER.put(memGetFloat(ptr)); + ptr += 4; + } + PARAMS_BUFFER.flip(); + GLStateManager.glLight(light, pname, PARAMS_BUFFER); + } + case GLCommand.LIGHT_MODEL -> { + final int pname = memGetInt(ptr); + final int count = memGetInt(ptr + 4); + ptr += 8; + PARAMS_BUFFER.clear(); + for (int i = 0; i < count; i++) { + PARAMS_BUFFER.put(memGetFloat(ptr)); + ptr += 4; + } + PARAMS_BUFFER.flip(); + GLStateManager.glLightModel(pname, PARAMS_BUFFER); + } + case GLCommand.MATERIAL -> { + final int face = memGetInt(ptr); + final int pname = memGetInt(ptr + 4); + final int count = memGetInt(ptr + 8); + ptr += 12; + PARAMS_BUFFER.clear(); + for (int i = 0; i < count; i++) { + PARAMS_BUFFER.put(memGetFloat(ptr)); + ptr += 4; + } + PARAMS_BUFFER.flip(); + GLStateManager.glMaterial(face, pname, PARAMS_BUFFER); + } + + // === Draw commands === + case GLCommand.DRAW_RANGE -> { + final int vboIndex = memGetInt(ptr); + final int start = memGetInt(ptr + 4); + final int count = memGetInt(ptr + 8); + final boolean hasBrightness = memGetInt(ptr + 12) != 0; + ptr += 16; + final VertexBuffer vbo = ownedVbos[vboIndex]; + vbo.setupState(); + vbo.draw(start, count); + vbo.cleanupState(); + } + case GLCommand.CALL_LIST -> { + final int listId = memGetInt(ptr); + ptr += 4; + DisplayListManager.glCallList(listId); + } + + // === Complex object reference === + case GLCommand.COMPLEX_REF -> { + final int index = memGetInt(ptr); + ptr += 4; + ((DisplayListCommand) complexObjects[index]).execute(); + } + + default -> throw new IllegalStateException("Unknown command opcode: " + cmd); + } + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBufferProcessor.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBufferProcessor.java new file mode 100644 index 000000000..dc2a61523 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandBufferProcessor.java @@ -0,0 +1,352 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; +import java.util.ArrayDeque; +import java.util.Deque; + +public final class CommandBufferProcessor { + private static final Logger LOGGER = LogManager.getLogger("CommandBufferProcessor"); + private static final Matrix4f tempMatrix = new Matrix4f(); + + private CommandBufferProcessor() {} + + public static void processCommandForOptimization(int opcode, CommandBuffer raw, BufferTransformOptimizer opt, CommandBuffer out) { + switch (opcode) { + // === MODELVIEW matrix commands - accumulate instead of copying === + case GLCommand.TRANSLATE -> { + final int mode = raw.readInt(); + final double x = raw.readDouble(); + final double y = raw.readDouble(); + final double z = raw.readDouble(); + if (mode == GL11.GL_MODELVIEW) { + opt.translate(x, y, z); + } else { + out.writeTranslate(mode, x, y, z); + } + } + case GLCommand.ROTATE -> { + final int mode = raw.readInt(); + final double angle = raw.readDouble(); + final double x = raw.readDouble(); + final double y = raw.readDouble(); + final double z = raw.readDouble(); + if (mode == GL11.GL_MODELVIEW) { + opt.rotate(angle, x, y, z); + } else { + out.writeRotate(mode, angle, x, y, z); + } + } + case GLCommand.SCALE -> { + final int mode = raw.readInt(); + final double x = raw.readDouble(); + final double y = raw.readDouble(); + final double z = raw.readDouble(); + if (mode == GL11.GL_MODELVIEW) { + opt.scale(x, y, z); + } else { + out.writeScale(mode, x, y, z); + } + } + case GLCommand.MULT_MATRIX -> { + final int mode = raw.readInt(); + raw.readMatrix4f(tempMatrix); + if (mode == GL11.GL_MODELVIEW) { + opt.multMatrix(tempMatrix); + } else { + out.writeMultMatrix(mode, tempMatrix); + } + } + case GLCommand.LOAD_IDENTITY -> { + final int mode = raw.readInt(); + if (mode == GL11.GL_MODELVIEW) { + if (opt.checkAndClearAbsoluteMatrix()) { + out.writeLoadIdentity(mode); + } + opt.loadIdentity(); + } else { + out.writeLoadIdentity(mode); + } + } + case GLCommand.LOAD_MATRIX -> { + final int mode = raw.readInt(); + raw.readMatrix4f(tempMatrix); + if (mode == GL11.GL_MODELVIEW) { + opt.emitPendingTransform(out); + out.writeLoadMatrix(mode, tempMatrix); + opt.loadIdentity(); + opt.markAbsoluteMatrix(); + } else { + out.writeLoadMatrix(mode, tempMatrix); + } + } + + // === Stack operations - update optimizer state and copy === + case GLCommand.PUSH_MATRIX -> { + final int mode = raw.readInt(); + if (mode == GL11.GL_MODELVIEW) { + opt.emitPendingTransform(out); + opt.push(); + } + out.writePushMatrix(mode); + } + case GLCommand.POP_MATRIX -> { + final int mode = raw.readInt(); + if (mode == GL11.GL_MODELVIEW) { + opt.pop(); + } + out.writePopMatrix(mode); + } + + // === Barriers - emit pending transform before copying === + case GLCommand.CALL_LIST -> { + final int listId = raw.readInt(); + opt.emitPendingTransform(out); + out.writeCallList(listId); + } + case GLCommand.PUSH_ATTRIB -> { + final int mask = raw.readInt(); + if ((mask & GL11.GL_TRANSFORM_BIT) != 0) { + opt.emitPendingTransform(out); + } + out.writePushAttrib(mask); + } + + // === All other commands - just copy === + default -> copyCommandData(opcode, raw, out); + } + } + + /** + * Copy a complete command (opcode already read) from raw to final buffer. + */ + public static void copyCommand(CommandBuffer raw, CommandBuffer out) { + final int opcode = raw.readInt(); + copyCommandData(opcode, raw, out); + } + + /** + * Copy command data based on opcode (opcode already consumed from raw). + */ + public static void copyCommandData(int opcode, CommandBuffer raw, CommandBuffer out) { + switch (opcode) { + // Single int commands + case GLCommand.ENABLE -> out.writeEnable(raw.readInt()); + case GLCommand.DISABLE -> out.writeDisable(raw.readInt()); + case GLCommand.CLEAR -> out.writeClear(raw.readInt()); + case GLCommand.CLEAR_STENCIL -> out.writeClearStencil(raw.readInt()); + case GLCommand.CULL_FACE -> out.writeCullFace(raw.readInt()); + case GLCommand.DEPTH_FUNC -> out.writeDepthFunc(raw.readInt()); + case GLCommand.SHADE_MODEL -> out.writeShadeModel(raw.readInt()); + case GLCommand.LOGIC_OP -> out.writeLogicOp(raw.readInt()); + case GLCommand.MATRIX_MODE -> out.writeMatrixMode(raw.readInt()); + case GLCommand.ACTIVE_TEXTURE -> out.writeActiveTexture(raw.readInt()); + case GLCommand.USE_PROGRAM -> out.writeUseProgram(raw.readInt()); + case GLCommand.PUSH_ATTRIB -> out.writePushAttrib(raw.readInt()); + case GLCommand.POP_ATTRIB -> { raw.readInt(); out.writePopAttrib(); } + case GLCommand.LOAD_IDENTITY -> out.writeLoadIdentity(raw.readInt()); + case GLCommand.PUSH_MATRIX -> out.writePushMatrix(raw.readInt()); + case GLCommand.POP_MATRIX -> out.writePopMatrix(raw.readInt()); + case GLCommand.STENCIL_MASK -> out.writeStencilMask(raw.readInt()); + case GLCommand.DEPTH_MASK -> out.writeDepthMask(raw.readInt() != 0); + + // Two int commands + case GLCommand.BIND_TEXTURE -> out.writeBindTexture(raw.readInt(), raw.readInt()); + case GLCommand.POLYGON_MODE -> out.writePolygonMode(raw.readInt(), raw.readInt()); + case GLCommand.COLOR_MATERIAL -> out.writeColorMaterial(raw.readInt(), raw.readInt()); + case GLCommand.LINE_STIPPLE -> out.writeLineStipple(raw.readInt(), raw.readInt()); + case GLCommand.STENCIL_MASK_SEPARATE -> out.writeStencilMaskSeparate(raw.readInt(), raw.readInt()); + case GLCommand.FOGI -> out.writeFogi(raw.readInt(), raw.readInt()); + + // Three int commands + case GLCommand.STENCIL_FUNC -> out.writeStencilFunc(raw.readInt(), raw.readInt(), raw.readInt()); + case GLCommand.STENCIL_OP -> out.writeStencilOp(raw.readInt(), raw.readInt(), raw.readInt()); + case GLCommand.TEX_PARAMETERI -> out.writeTexParameteri(raw.readInt(), raw.readInt(), raw.readInt()); + + // Four int commands + case GLCommand.VIEWPORT -> out.writeViewport(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt()); + case GLCommand.BLEND_FUNC -> out.writeBlendFunc(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt()); + case GLCommand.COLOR_MASK -> out.writeColorMask(raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0, raw.readInt() != 0); + case GLCommand.STENCIL_FUNC_SEPARATE -> out.writeStencilFuncSeparate(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt()); + case GLCommand.STENCIL_OP_SEPARATE -> out.writeStencilOpSeparate(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt()); + + // Float commands + case GLCommand.POINT_SIZE -> out.writePointSize(raw.readFloat()); + case GLCommand.LINE_WIDTH -> out.writeLineWidth(raw.readFloat()); + case GLCommand.POLYGON_OFFSET -> out.writePolygonOffset(raw.readFloat(), raw.readFloat()); + case GLCommand.NORMAL -> out.writeNormal(raw.readFloat(), raw.readFloat(), raw.readFloat()); + case GLCommand.COLOR -> out.writeColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat()); + case GLCommand.CLEAR_COLOR -> out.writeClearColor(raw.readFloat(), raw.readFloat(), raw.readFloat(), raw.readFloat()); + + // Mixed int+float commands + case GLCommand.ALPHA_FUNC -> out.writeAlphaFunc(raw.readInt(), raw.readFloat()); + case GLCommand.FOGF -> out.writeFogf(raw.readInt(), raw.readFloat()); + case GLCommand.LIGHTF -> out.writeLightf(raw.readInt(), raw.readInt(), raw.readFloat()); + case GLCommand.LIGHT_MODELF -> out.writeLightModelf(raw.readInt(), raw.readFloat()); + case GLCommand.LIGHTI -> out.writeLighti(raw.readInt(), raw.readInt(), raw.readInt()); + case GLCommand.LIGHT_MODELI -> out.writeLightModeli(raw.readInt(), raw.readInt()); + case GLCommand.MATERIALF -> out.writeMaterialf(raw.readInt(), raw.readInt(), raw.readFloat()); + case GLCommand.TEX_PARAMETERF -> out.writeTexParameterf(raw.readInt(), raw.readInt(), raw.readFloat()); + + // Double commands + case GLCommand.TRANSLATE -> out.writeTranslate(raw.readInt(), raw.readDouble(), raw.readDouble(), raw.readDouble()); + case GLCommand.ROTATE -> out.writeRotate(raw.readInt(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble()); + case GLCommand.SCALE -> out.writeScale(raw.readInt(), raw.readDouble(), raw.readDouble(), raw.readDouble()); + case GLCommand.ORTHO -> out.writeOrtho(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble()); + case GLCommand.FRUSTUM -> out.writeFrustum(raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble(), raw.readDouble()); + + // Matrix commands + case GLCommand.MULT_MATRIX -> { int mode = raw.readInt(); out.writeMultMatrix(mode, raw.readMatrix4f(tempMatrix)); } + case GLCommand.LOAD_MATRIX -> { int mode = raw.readInt(); out.writeLoadMatrix(mode, raw.readMatrix4f(tempMatrix)); } + + // FloatBuffer commands (inline 4 floats max) + case GLCommand.FOG -> { + final int pname = raw.readInt(); + final int count = raw.readInt(); + final FloatBuffer fb = FloatBuffer.allocate(count); + for (int i = 0; i < count; i++) fb.put(raw.readFloat()); + fb.flip(); + out.writeFog(pname, fb); + } + case GLCommand.LIGHT -> { + final int light = raw.readInt(); + final int pname = raw.readInt(); + final int count = raw.readInt(); + final FloatBuffer fb = FloatBuffer.allocate(count); + for (int i = 0; i < count; i++) fb.put(raw.readFloat()); + fb.flip(); + out.writeLight(light, pname, fb); + } + case GLCommand.LIGHT_MODEL -> { + final int pname = raw.readInt(); + final int count = raw.readInt(); + final FloatBuffer fb = FloatBuffer.allocate(count); + for (int i = 0; i < count; i++) fb.put(raw.readFloat()); + fb.flip(); + out.writeLightModel(pname, fb); + } + case GLCommand.MATERIAL -> { + final int face = raw.readInt(); + final int pname = raw.readInt(); + final int count = raw.readInt(); + final FloatBuffer fb = FloatBuffer.allocate(count); + for (int i = 0; i < count; i++) fb.put(raw.readFloat()); + fb.flip(); + out.writeMaterial(face, pname, fb); + } + + // Draw/call commands + case GLCommand.DRAW_RANGE -> out.writeDrawRange(raw.readInt(), raw.readInt(), raw.readInt(), raw.readInt() != 0); + case GLCommand.CALL_LIST -> out.writeCallList(raw.readInt()); + + // Complex object reference - transfer the object to output buffer + case GLCommand.COMPLEX_REF -> { + final int index = raw.readInt(); + final Object obj = raw.getComplexObject(index); + out.writeComplexRef((DisplayListCommand) obj); + } + + default -> LOGGER.warn("[CommandBufferProcessor] Unknown opcode in buffer copy: {}", opcode); + } + } + + /** + * Check if a matrix is identity (within epsilon). + */ + public static boolean isIdentity(Matrix4f m) { + final float eps = 1e-6f; + return Math.abs(m.m00() - 1) < eps && Math.abs(m.m01()) < eps && Math.abs(m.m02()) < eps && Math.abs(m.m03()) < eps + && Math.abs(m.m10()) < eps && Math.abs(m.m11() - 1) < eps && Math.abs(m.m12()) < eps && Math.abs(m.m13()) < eps + && Math.abs(m.m20()) < eps && Math.abs(m.m21()) < eps && Math.abs(m.m22() - 1) < eps && Math.abs(m.m23()) < eps + && Math.abs(m.m30()) < eps && Math.abs(m.m31()) < eps && Math.abs(m.m32()) < eps && Math.abs(m.m33() - 1) < eps; + } + + /** + * Lightweight transform optimizer for buffer-to-buffer processing. + * Tracks accumulated MODELVIEW transform and emits MultMatrix commands when needed. + */ + public static class BufferTransformOptimizer { + private final Matrix4f accumulated = new Matrix4f(); + private final Matrix4f lastEmitted = new Matrix4f(); + private final Deque stack = new ArrayDeque<>(); + private boolean absoluteMatrixFlag = false; + + public BufferTransformOptimizer() { + accumulated.identity(); + lastEmitted.identity(); + } + + public void translate(double x, double y, double z) { + accumulated.translate((float) x, (float) y, (float) z); + } + + public void rotate(double angle, double x, double y, double z) { + accumulated.rotate((float) Math.toRadians(angle), (float) x, (float) y, (float) z); + } + + public void scale(double x, double y, double z) { + accumulated.scale((float) x, (float) y, (float) z); + } + + public void multMatrix(Matrix4f m) { + accumulated.mul(m); + } + + public void loadIdentity() { + accumulated.identity(); + } + + public void push() { + stack.push(new Matrix4f(accumulated)); + } + + public void pop() { + if (!stack.isEmpty()) { + accumulated.set(stack.pop()); + lastEmitted.set(accumulated); + } else { + accumulated.identity(); + lastEmitted.identity(); + } + } + + public boolean isIdentity() { + return CommandBufferProcessor.isIdentity(accumulated); + } + + public void markAbsoluteMatrix() { + absoluteMatrixFlag = true; + } + + public boolean checkAndClearAbsoluteMatrix() { + final boolean was = absoluteMatrixFlag; + absoluteMatrixFlag = false; + return was; + } + + public void emitPendingTransform(CommandBuffer out) { + if (!accumulated.equals(lastEmitted)) { + emitTransformTo(out, accumulated); + } + } + + public void emitTransformTo(CommandBuffer out, Matrix4f target) { + if (target.equals(lastEmitted)) { + return; + } + + if (CommandBufferProcessor.isIdentity(lastEmitted)) { + out.writeMultMatrix(GL11.GL_MODELVIEW, target); + } else { + final Matrix4f delta = new Matrix4f(lastEmitted).invert().mul(target); + out.writeMultMatrix(GL11.GL_MODELVIEW, delta); + } + lastEmitted.set(target); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandRecorder.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandRecorder.java new file mode 100644 index 000000000..609cf05b6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CommandRecorder.java @@ -0,0 +1,328 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; +import org.joml.Matrix4f; + +import java.nio.FloatBuffer; + +/** + * Records GL commands to a CommandBuffer during display list compilation. + */ +public class CommandRecorder { + private final CommandBuffer buffer; + private int commandCount; + + public CommandRecorder() { + this.buffer = new CommandBuffer(); + this.commandCount = 0; + } + + public CommandBuffer getBuffer() { + return buffer; + } + + public int getCommandCount() { + return commandCount; + } + + public void free() { + buffer.free(); + } + + // ==================== Recording Methods ==================== + + public void recordEnable(int cap) { + buffer.writeEnable(cap); + commandCount++; + } + + public void recordDisable(int cap) { + buffer.writeDisable(cap); + commandCount++; + } + + public void recordClear(int mask) { + buffer.writeClear(mask); + commandCount++; + } + + public void recordClearColor(float r, float g, float b, float a) { + buffer.writeClearColor(r, g, b, a); + commandCount++; + } + + public void recordClearStencil(int s) { + buffer.writeClearStencil(s); + commandCount++; + } + + public void recordColor(float r, float g, float b, float a) { + buffer.writeColor(r, g, b, a); + commandCount++; + } + + public void recordColorMask(boolean r, boolean g, boolean b, boolean a) { + buffer.writeColorMask(r, g, b, a); + commandCount++; + } + + public void recordDepthMask(boolean flag) { + buffer.writeDepthMask(flag); + commandCount++; + } + + public void recordDepthFunc(int func) { + buffer.writeDepthFunc(func); + commandCount++; + } + + public void recordBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { + buffer.writeBlendFunc(srcRgb, dstRgb, srcAlpha, dstAlpha); + commandCount++; + } + + public void recordAlphaFunc(int func, float ref) { + buffer.writeAlphaFunc(func, ref); + commandCount++; + } + + public void recordCullFace(int mode) { + buffer.writeCullFace(mode); + commandCount++; + } + + public void recordShadeModel(int mode) { + buffer.writeShadeModel(mode); + commandCount++; + } + + public void recordBindTexture(int target, int texture) { + buffer.writeBindTexture(target, texture); + commandCount++; + } + + public void recordTexParameteri(int target, int pname, int param) { + buffer.writeTexParameteri(target, pname, param); + commandCount++; + } + + public void recordTexParameterf(int target, int pname, float param) { + buffer.writeTexParameterf(target, pname, param); + commandCount++; + } + + public void recordMatrixMode(int mode) { + buffer.writeMatrixMode(mode); + commandCount++; + } + + public void recordLoadIdentity(int matrixMode) { + buffer.writeLoadIdentity(matrixMode); + commandCount++; + } + + public void recordPushMatrix(int matrixMode) { + buffer.writePushMatrix(matrixMode); + commandCount++; + } + + public void recordPopMatrix(int matrixMode) { + buffer.writePopMatrix(matrixMode); + commandCount++; + } + + public void recordTranslate(int matrixMode, double x, double y, double z) { + buffer.writeTranslate(matrixMode, x, y, z); + commandCount++; + } + + public void recordRotate(int matrixMode, double angle, double x, double y, double z) { + buffer.writeRotate(matrixMode, angle, x, y, z); + commandCount++; + } + + public void recordScale(int matrixMode, double x, double y, double z) { + buffer.writeScale(matrixMode, x, y, z); + commandCount++; + } + + public void recordMultMatrix(int matrixMode, Matrix4f matrix) { + buffer.writeMultMatrix(matrixMode, matrix); + commandCount++; + } + + public void recordLoadMatrix(int matrixMode, Matrix4f matrix) { + buffer.writeLoadMatrix(matrixMode, matrix); + commandCount++; + } + + public void recordOrtho(double left, double right, double bottom, double top, double zNear, double zFar) { + buffer.writeOrtho(left, right, bottom, top, zNear, zFar); + commandCount++; + } + + public void recordFrustum(double left, double right, double bottom, double top, double zNear, double zFar) { + buffer.writeFrustum(left, right, bottom, top, zNear, zFar); + commandCount++; + } + + public void recordViewport(int x, int y, int width, int height) { + buffer.writeViewport(x, y, width, height); + commandCount++; + } + + public void recordNormal(float nx, float ny, float nz) { + buffer.writeNormal(nx, ny, nz); + commandCount++; + } + + public void recordPointSize(float size) { + buffer.writePointSize(size); + commandCount++; + } + + public void recordLineWidth(float width) { + buffer.writeLineWidth(width); + commandCount++; + } + + public void recordLineStipple(int factor, int pattern) { + buffer.writeLineStipple(factor, pattern); + commandCount++; + } + + public void recordPolygonOffset(float factor, float units) { + buffer.writePolygonOffset(factor, units); + commandCount++; + } + + public void recordPolygonMode(int face, int mode) { + buffer.writePolygonMode(face, mode); + commandCount++; + } + + public void recordColorMaterial(int face, int mode) { + buffer.writeColorMaterial(face, mode); + commandCount++; + } + + public void recordLogicOp(int opcode) { + buffer.writeLogicOp(opcode); + commandCount++; + } + + public void recordActiveTexture(int texture) { + buffer.writeActiveTexture(texture); + commandCount++; + } + + public void recordUseProgram(int program) { + buffer.writeUseProgram(program); + commandCount++; + } + + public void recordPushAttrib(int mask) { + buffer.writePushAttrib(mask); + commandCount++; + } + + public void recordPopAttrib() { + buffer.writePopAttrib(); + commandCount++; + } + + public void recordFogf(int pname, float param) { + buffer.writeFogf(pname, param); + commandCount++; + } + + public void recordFogi(int pname, int param) { + buffer.writeFogi(pname, param); + commandCount++; + } + + public void recordFog(int pname, FloatBuffer params) { + buffer.writeFog(pname, params); + commandCount++; + } + + public void recordLightf(int light, int pname, float param) { + buffer.writeLightf(light, pname, param); + commandCount++; + } + + public void recordLighti(int light, int pname, int param) { + buffer.writeLighti(light, pname, param); + commandCount++; + } + + public void recordLight(int light, int pname, FloatBuffer params) { + buffer.writeLight(light, pname, params); + commandCount++; + } + + public void recordLightModelf(int pname, float param) { + buffer.writeLightModelf(pname, param); + commandCount++; + } + + public void recordLightModeli(int pname, int param) { + buffer.writeLightModeli(pname, param); + commandCount++; + } + + public void recordLightModel(int pname, FloatBuffer params) { + buffer.writeLightModel(pname, params); + commandCount++; + } + + public void recordMaterialf(int face, int pname, float val) { + buffer.writeMaterialf(face, pname, val); + commandCount++; + } + + public void recordMaterial(int face, int pname, FloatBuffer params) { + buffer.writeMaterial(face, pname, params); + commandCount++; + } + + public void recordStencilFunc(int func, int ref, int mask) { + buffer.writeStencilFunc(func, ref, mask); + commandCount++; + } + + public void recordStencilFuncSeparate(int face, int func, int ref, int mask) { + buffer.writeStencilFuncSeparate(face, func, ref, mask); + commandCount++; + } + + public void recordStencilOp(int fail, int zfail, int zpass) { + buffer.writeStencilOp(fail, zfail, zpass); + commandCount++; + } + + public void recordStencilOpSeparate(int face, int sfail, int dpfail, int dppass) { + buffer.writeStencilOpSeparate(face, sfail, dpfail, dppass); + commandCount++; + } + + public void recordStencilMask(int mask) { + buffer.writeStencilMask(mask); + commandCount++; + } + + public void recordStencilMaskSeparate(int face, int mask) { + buffer.writeStencilMaskSeparate(face, mask); + commandCount++; + } + + public void recordCallList(int listId) { + buffer.writeCallList(listId); + commandCount++; + } + + public void recordComplexCommand(DisplayListCommand cmd) { + buffer.writeComplexRef(cmd); + commandCount++; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CompiledDisplayList.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CompiledDisplayList.java new file mode 100644 index 000000000..e1024d638 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/CompiledDisplayList.java @@ -0,0 +1,184 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress; +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree; +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt; + +/** + * Represents a compiled display list. + * + *

Contains commands serialized to a direct ByteBuffer for off-heap storage, + * with complex objects (TexImage2DCmd, TexSubImage2DCmd) stored separately. + * + *

The ownedVbos array contains VBOs that are referenced by DrawRange commands. + */ +public final class CompiledDisplayList { + private final ByteBuffer commandBuffer; // Off-heap command storage, must be freed + private final Object[] complexObjects; // Complex commands (TexImage2D, etc.) + private final VertexBuffer[] ownedVbos; // GPU resources referenced by index + + public CompiledDisplayList(ByteBuffer commandBuffer, Object[] complexObjects, VertexBuffer[] ownedVbos) { + this.commandBuffer = commandBuffer; + this.complexObjects = complexObjects; + this.ownedVbos = ownedVbos; + } + + /** + * Render this display list by executing all commands. + */ + public void render() { + if (commandBuffer != null && commandBuffer.limit() > 0) { + CommandBufferExecutor.execute(commandBuffer, complexObjects, ownedVbos); + } + } + + /** + * 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) { + cmd.delete(); + } + } + } + + // Close VBOs + if (ownedVbos != null) { + for (VertexBuffer vbo : ownedVbos) { + if (vbo != null) { + vbo.close(); + } + } + } + + // Free the command buffer (off-heap memory) + if (commandBuffer != null) { + memFree(commandBuffer); + } + } + + /** + * Get the command buffer for debugging/inspection. + */ + public ByteBuffer getCommandBuffer() { + return commandBuffer; + } + + /** + * Get complex objects for inlining into another display list (nested lists). + */ + public Object[] getComplexObjects() { + return complexObjects; + } + + /** + * Get owned VBOs for inlining into another display list. + */ + public VertexBuffer[] getOwnedVbos() { + return ownedVbos; + } + + // === Test inspection methods === + + /** + * Get counts of each command type in the buffer. + * Used for testing to verify optimization results. + */ + public Map getCommandCounts() { + Map counts = new HashMap<>(); + if (commandBuffer == null || commandBuffer.limit() == 0) { + return counts; + } + long ptr = memAddress(commandBuffer); + long end = ptr + commandBuffer.limit(); + while (ptr < end) { + int cmd = memGetInt(ptr); + counts.merge(cmd, 1, Integer::sum); + ptr += getCommandSize(cmd, ptr); + } + return counts; + } + + /** + * Get the sequence of command opcodes in order. + * Used for testing to verify command ordering. + */ + public List getCommandOpcodes() { + List opcodes = new ArrayList<>(); + if (commandBuffer == null || commandBuffer.limit() == 0) { + return opcodes; + } + long ptr = memAddress(commandBuffer); + long end = ptr + commandBuffer.limit(); + while (ptr < end) { + int cmd = memGetInt(ptr); + opcodes.add(cmd); + ptr += getCommandSize(cmd, ptr); + } + return opcodes; + } + + /** + * Get size of a command in bytes (including the opcode). + */ + private static int getCommandSize(int cmd, long ptr) { + return switch (cmd) { + // Single int commands (8 bytes) + case GLCommand.ENABLE, GLCommand.DISABLE, GLCommand.CLEAR, GLCommand.CLEAR_STENCIL, + GLCommand.CULL_FACE, GLCommand.DEPTH_FUNC, GLCommand.SHADE_MODEL, GLCommand.LOGIC_OP, + GLCommand.MATRIX_MODE, GLCommand.ACTIVE_TEXTURE, GLCommand.USE_PROGRAM, + GLCommand.PUSH_ATTRIB, GLCommand.POP_ATTRIB, GLCommand.LOAD_IDENTITY, + GLCommand.PUSH_MATRIX, GLCommand.POP_MATRIX, GLCommand.STENCIL_MASK, + GLCommand.DEPTH_MASK, GLCommand.POINT_SIZE, GLCommand.LINE_WIDTH, + GLCommand.CALL_LIST, GLCommand.COMPLEX_REF -> 8; + + // Two int commands (12 bytes) + case GLCommand.BIND_TEXTURE, GLCommand.POLYGON_MODE, GLCommand.COLOR_MATERIAL, + GLCommand.LINE_STIPPLE, GLCommand.STENCIL_MASK_SEPARATE, GLCommand.FOGI, + GLCommand.POLYGON_OFFSET, GLCommand.ALPHA_FUNC, GLCommand.FOGF, + GLCommand.LIGHT_MODELF, GLCommand.LIGHT_MODELI -> 12; + + // Three int commands (16 bytes) + case GLCommand.STENCIL_FUNC, GLCommand.STENCIL_OP, GLCommand.TEX_PARAMETERI, + GLCommand.NORMAL, GLCommand.LIGHTF, GLCommand.LIGHTI, + GLCommand.MATERIALF, GLCommand.TEX_PARAMETERF -> 16; + + // Four int commands (20 bytes) + case GLCommand.VIEWPORT, GLCommand.BLEND_FUNC, GLCommand.COLOR_MASK, + GLCommand.STENCIL_FUNC_SEPARATE, GLCommand.STENCIL_OP_SEPARATE, + GLCommand.COLOR, GLCommand.CLEAR_COLOR, GLCommand.DRAW_RANGE -> 20; + + // Double commands + case GLCommand.TRANSLATE, GLCommand.SCALE -> 32; // cmd + mode + 3 doubles + case GLCommand.ROTATE -> 40; // cmd + mode + 4 doubles + case GLCommand.ORTHO, GLCommand.FRUSTUM -> 52; // cmd + 6 doubles + + // Matrix commands (72 bytes) + case GLCommand.MULT_MATRIX, GLCommand.LOAD_MATRIX -> 72; + + // Buffer commands (variable, read count) + case GLCommand.FOG, GLCommand.LIGHT_MODEL -> { + int count = memGetInt(ptr + 8); // pname at +4, count at +8 + yield 12 + count * 4; + } + case GLCommand.LIGHT, GLCommand.MATERIAL -> { + int count = memGetInt(ptr + 12); // light/face at +4, pname at +8, count at +12 + yield 16 + count * 4; + } + + default -> throw new IllegalStateException("Unknown command: " + cmd); + }; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/GLCommand.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/GLCommand.java new file mode 100644 index 000000000..98d08a775 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/GLCommand.java @@ -0,0 +1,161 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +/** + * Command opcodes for ByteBuffer-based display list serialization. + * Each command has a fixed layout described in comments. + * All values are 4-byte aligned. + */ +public final class GLCommand { + private GLCommand() {} + + // === Single int commands (8 bytes: opcode + param) === + public static final int ENABLE = 1; // [cmd:4][cap:4] + public static final int DISABLE = 2; // [cmd:4][cap:4] + public static final int CLEAR = 3; // [cmd:4][mask:4] + public static final int CLEAR_STENCIL = 4; // [cmd:4][s:4] + public static final int CULL_FACE = 5; // [cmd:4][mode:4] + public static final int DEPTH_FUNC = 6; // [cmd:4][func:4] + public static final int SHADE_MODEL = 7; // [cmd:4][mode:4] + public static final int LOGIC_OP = 8; // [cmd:4][opcode:4] + public static final int MATRIX_MODE = 9; // [cmd:4][mode:4] + public static final int ACTIVE_TEXTURE = 10; // [cmd:4][texture:4] + public static final int USE_PROGRAM = 11; // [cmd:4][program:4] + public static final int PUSH_ATTRIB = 12; // [cmd:4][mask:4] + public static final int POP_ATTRIB = 13; // [cmd:4][unused:4] + public static final int LOAD_IDENTITY = 14; // [cmd:4][matrixMode:4] + public static final int PUSH_MATRIX = 15; // [cmd:4][matrixMode:4] + public static final int POP_MATRIX = 16; // [cmd:4][matrixMode:4] + public static final int STENCIL_MASK = 17; // [cmd:4][mask:4] + public static final int DEPTH_MASK = 18; // [cmd:4][flag:4] (0 or 1) + + // === Two int commands (12 bytes) === + public static final int BIND_TEXTURE = 20; // [cmd:4][target:4][texture:4] + public static final int POLYGON_MODE = 21; // [cmd:4][face:4][mode:4] + public static final int COLOR_MATERIAL = 22; // [cmd:4][face:4][mode:4] + public static final int LINE_STIPPLE = 23; // [cmd:4][factor:4][pattern:4] + public static final int STENCIL_MASK_SEPARATE = 24; // [cmd:4][face:4][mask:4] + public static final int FOGI = 25; // [cmd:4][pname:4][param:4] + + // === Three int commands (16 bytes) === + public static final int STENCIL_FUNC = 30; // [cmd:4][func:4][ref:4][mask:4] + public static final int STENCIL_OP = 31; // [cmd:4][fail:4][zfail:4][zpass:4] + public static final int TEX_PARAMETERI = 32; // [cmd:4][target:4][pname:4][param:4] + + // === Four int commands (20 bytes) === + public static final int VIEWPORT = 40; // [cmd:4][x:4][y:4][width:4][height:4] + public static final int BLEND_FUNC = 41; // [cmd:4][srcRgb:4][dstRgb:4][srcAlpha:4][dstAlpha:4] + public static final int COLOR_MASK = 42; // [cmd:4][r:4][g:4][b:4][a:4] (0 or 1 each) + public static final int STENCIL_FUNC_SEPARATE = 43; // [cmd:4][face:4][func:4][ref:4][mask:4] + public static final int STENCIL_OP_SEPARATE = 44; // [cmd:4][face:4][sfail:4][dpfail:4][dppass:4] + + // === Float commands === + public static final int POINT_SIZE = 50; // [cmd:4][size:4f] + public static final int LINE_WIDTH = 51; // [cmd:4][width:4f] + public static final int POLYGON_OFFSET = 52; // [cmd:4][factor:4f][units:4f] + public static final int NORMAL = 53; // [cmd:4][x:4f][y:4f][z:4f] + public static final int COLOR = 54; // [cmd:4][r:4f][g:4f][b:4f][a:4f] + public static final int CLEAR_COLOR = 55; // [cmd:4][r:4f][g:4f][b:4f][a:4f] + + // === Mixed int+float commands === + public static final int ALPHA_FUNC = 60; // [cmd:4][func:4][ref:4f] + public static final int FOGF = 61; // [cmd:4][pname:4][param:4f] + public static final int LIGHTF = 62; // [cmd:4][light:4][pname:4][param:4f] + public static final int LIGHT_MODELF = 63; // [cmd:4][pname:4][param:4f] + public static final int LIGHTI = 64; // [cmd:4][light:4][pname:4][param:4] + public static final int LIGHT_MODELI = 65; // [cmd:4][pname:4][param:4] + public static final int MATERIALF = 66; // [cmd:4][face:4][pname:4][param:4f] + public static final int TEX_PARAMETERF = 67; // [cmd:4][target:4][pname:4][param:4f] + + // === Double commands === + public static final int TRANSLATE = 70; // [cmd:4][mode:4][x:8d][y:8d][z:8d] = 32 bytes + public static final int ROTATE = 71; // [cmd:4][mode:4][angle:8d][x:8d][y:8d][z:8d] = 40 bytes + public static final int SCALE = 72; // [cmd:4][mode:4][x:8d][y:8d][z:8d] = 32 bytes + public static final int ORTHO = 73; // [cmd:4][left:8d][right:8d][bottom:8d][top:8d][zNear:8d][zFar:8d] = 52 bytes + public static final int FRUSTUM = 74; // [cmd:4][left:8d][right:8d][bottom:8d][top:8d][zNear:8d][zFar:8d] = 52 bytes + + // === Matrix commands (72 bytes) === + public static final int MULT_MATRIX = 80; // [cmd:4][mode:4][m00-m33:64f] = 72 bytes + public static final int LOAD_MATRIX = 81; // [cmd:4][mode:4][m00-m33:64f] = 72 bytes + + // === Buffer commands (inline 4 floats max) === + public static final int FOG = 90; // [cmd:4][pname:4][count:4][params:16f] = 28 bytes + public static final int LIGHT = 91; // [cmd:4][light:4][pname:4][count:4][params:16f] = 32 bytes + public static final int LIGHT_MODEL = 92; // [cmd:4][pname:4][count:4][params:16f] = 28 bytes + public static final int MATERIAL = 93; // [cmd:4][face:4][pname:4][count:4][params:16f] = 32 bytes + + // === Draw commands === + public static final int DRAW_RANGE = 100; // [cmd:4][vboIndex:4][start:4][count:4][hasBrightness:4] = 20 bytes + public static final int CALL_LIST = 101; // [cmd:4][listId:4] = 8 bytes + + // === Complex object reference === + public static final int COMPLEX_REF = 255; // [cmd:4][index:4] = 8 bytes + + /** + * Get a human-readable name for an opcode (for debugging). + */ + public static String getName(int opcode) { + return switch (opcode) { + case ENABLE -> "ENABLE"; + case DISABLE -> "DISABLE"; + case CLEAR -> "CLEAR"; + case CLEAR_STENCIL -> "CLEAR_STENCIL"; + case CULL_FACE -> "CULL_FACE"; + case DEPTH_FUNC -> "DEPTH_FUNC"; + case SHADE_MODEL -> "SHADE_MODEL"; + case LOGIC_OP -> "LOGIC_OP"; + case MATRIX_MODE -> "MATRIX_MODE"; + case ACTIVE_TEXTURE -> "ACTIVE_TEXTURE"; + case USE_PROGRAM -> "USE_PROGRAM"; + case PUSH_ATTRIB -> "PUSH_ATTRIB"; + case POP_ATTRIB -> "POP_ATTRIB"; + case LOAD_IDENTITY -> "LOAD_IDENTITY"; + case PUSH_MATRIX -> "PUSH_MATRIX"; + case POP_MATRIX -> "POP_MATRIX"; + case STENCIL_MASK -> "STENCIL_MASK"; + case DEPTH_MASK -> "DEPTH_MASK"; + case BIND_TEXTURE -> "BIND_TEXTURE"; + case POLYGON_MODE -> "POLYGON_MODE"; + case COLOR_MATERIAL -> "COLOR_MATERIAL"; + case LINE_STIPPLE -> "LINE_STIPPLE"; + case STENCIL_MASK_SEPARATE -> "STENCIL_MASK_SEPARATE"; + case FOGI -> "FOGI"; + case STENCIL_FUNC -> "STENCIL_FUNC"; + case STENCIL_OP -> "STENCIL_OP"; + case TEX_PARAMETERI -> "TEX_PARAMETERI"; + case VIEWPORT -> "VIEWPORT"; + case BLEND_FUNC -> "BLEND_FUNC"; + case COLOR_MASK -> "COLOR_MASK"; + case STENCIL_FUNC_SEPARATE -> "STENCIL_FUNC_SEPARATE"; + case STENCIL_OP_SEPARATE -> "STENCIL_OP_SEPARATE"; + case POINT_SIZE -> "POINT_SIZE"; + case LINE_WIDTH -> "LINE_WIDTH"; + case POLYGON_OFFSET -> "POLYGON_OFFSET"; + case NORMAL -> "NORMAL"; + case COLOR -> "COLOR"; + case CLEAR_COLOR -> "CLEAR_COLOR"; + case ALPHA_FUNC -> "ALPHA_FUNC"; + case FOGF -> "FOGF"; + case LIGHTF -> "LIGHTF"; + case LIGHT_MODELF -> "LIGHT_MODELF"; + case LIGHTI -> "LIGHTI"; + case LIGHT_MODELI -> "LIGHT_MODELI"; + case MATERIALF -> "MATERIALF"; + case TEX_PARAMETERF -> "TEX_PARAMETERF"; + case TRANSLATE -> "TRANSLATE"; + case ROTATE -> "ROTATE"; + case SCALE -> "SCALE"; + case ORTHO -> "ORTHO"; + case FRUSTUM -> "FRUSTUM"; + case MULT_MATRIX -> "MULT_MATRIX"; + case LOAD_MATRIX -> "LOAD_MATRIX"; + case FOG -> "FOG"; + case LIGHT -> "LIGHT"; + case LIGHT_MODEL -> "LIGHT_MODEL"; + case MATERIAL -> "MATERIAL"; + case DRAW_RANGE -> "DRAW_RANGE"; + case CALL_LIST -> "CALL_LIST"; + case COMPLEX_REF -> "COMPLEX_REF"; + default -> "UNKNOWN(" + opcode + ")"; + }; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/ImmediateModeRecorder.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/ImmediateModeRecorder.java new file mode 100644 index 000000000..596c820c4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/ImmediateModeRecorder.java @@ -0,0 +1,708 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.api.util.NormI8; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuad; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Records immediate mode GL calls (glBegin/glEnd/glVertex) during display list compilation. + * Converts immediate mode geometry to ModelQuad format for VBO compilation. + * + *

Tracks current immediate mode state: + *

    + *
  • TexCoord: Set by glTexCoord* calls
  • + *
  • Normal: Set by glNormal* calls
  • + *
  • Color: Read from GLStateManager.getColor() at vertex emission time
  • + *
+ * + *

Supported primitive types: + *

    + *
  • GL_QUADS: 4 vertices per quad (native support)
  • + *
  • GL_TRIANGLES: 3 vertices per triangle (converted to degenerate quad)
  • + *
  • GL_TRIANGLE_FAN: Fan of triangles (converted to individual quads)
  • + *
  • GL_TRIANGLE_STRIP: Strip of triangles (converted to individual quads)
  • + *
  • GL_QUAD_STRIP: Strip of quads (converted to individual quads)
  • + *
  • GL_POLYGON: Polygon (treated as triangle fan)
  • + *
+ * + *

Unsupported primitive types (logged warning, geometry discarded): + *

    + *
  • GL_LINES, GL_LINE_STRIP, GL_LINE_LOOP: Line primitives
  • + *
  • GL_POINTS: Point primitives
  • + *
+ */ +public class ImmediateModeRecorder { + private static final Logger LOGGER = LogManager.getLogger("ImmediateModeRecorder"); + + // Accumulated quads from completed primitives + private final List quads = new ArrayList<>(); + + // Accumulated line vertices (pairs of vertices for GL_LINES) + private final List lineVertices = new ArrayList<>(); + + // Current primitive state + private int currentPrimitiveType = -1; + private boolean inPrimitive = false; + + // Vertices for current primitive (cleared on glEnd) + private final List currentVertices = new ArrayList<>(); + + // Current immediate mode state (set by glTexCoord*, glNormal*) + private float texCoordS = 0.0f; + private float texCoordT = 0.0f; + private float normalX = 0.0f; + private float normalY = 0.0f; + private float normalZ = 1.0f; // Default normal points +Z + + // Track which attributes have been set (for flags) + private boolean hasTexCoord = false; + private boolean hasNormal = false; + private boolean hasColor = false; + + /** + * Represents a single vertex captured during immediate mode. + */ + private static class ImmediateVertex { + float x, y, z; // Position + int color; // Packed ABGR color + float texU, texV; // Texture coordinates + int normal; // Packed normal + + ImmediateVertex(float x, float y, float z, int color, float texU, float texV, int normal) { + this.x = x; + this.y = y; + this.z = z; + this.color = color; + this.texU = texU; + this.texV = texV; + this.normal = normal; + } + } + + /** + * Represents a vertex for line primitives. + * Simpler than ImmediateVertex - just position and color. + */ + public static class LineVertex { + public final float x, y, z; + public final int color; // Packed ABGR color + + public LineVertex(float x, float y, float z, int color) { + this.x = x; + this.y = y; + this.z = z; + this.color = color; + } + } + + public ImmediateModeRecorder() { + // Recorder is ready to capture geometry + } + + /** + * Set the current texture coordinate. + * Called by GLStateManager.glTexCoord*. + */ + public void setTexCoord(float s, float t) { + this.texCoordS = s; + this.texCoordT = t; + this.hasTexCoord = true; + } + + /** + * Set the current normal vector. + * Called by GLStateManager.glNormal*. + */ + public void setNormal(float x, float y, float z) { + this.normalX = x; + this.normalY = y; + this.normalZ = z; + this.hasNormal = true; + } + + /** + * Check if currently in a primitive (between glBegin and glEnd). + */ + public boolean isInPrimitive() { + return inPrimitive; + } + + /** + * Start recording a new primitive. + */ + public void begin(int primitiveType) { + if (inPrimitive) { + throw new IllegalStateException("glBegin called while already in primitive"); + } + this.currentPrimitiveType = primitiveType; + this.currentVertices.clear(); + this.inPrimitive = true; + } + + /** + * End the current primitive, convert to quads/lines, and return them immediately. + * This allows the caller to create AccumulatedDraw at the correct command position. + * + * @return Result containing quads, lines, and flags, or null if no geometry was produced + */ + public Result end() { + if (!inPrimitive) { + throw new IllegalStateException("glEnd called without glBegin"); + } + + // Convert this primitive's vertices to quads or lines + convertPrimitiveToQuads(); + this.currentVertices.clear(); + this.inPrimitive = false; + + // Return geometry from this primitive immediately (don't accumulate) + if (quads.isEmpty() && lineVertices.isEmpty()) { + return null; + } + + // Copy geometry and create flags + List resultQuads = new ArrayList<>(quads); + List resultLines = new ArrayList<>(lineVertices); + CapturingTessellator.Flags flags = new CapturingTessellator.Flags( + hasTexCoord, // hasTexture + false, // hasBrightness - immediate mode doesn't typically use lightmap + hasColor, // hasColor + hasNormal // hasNormals + ); + + // Clear for next primitive (but keep attribute tracking state) + quads.clear(); + lineVertices.clear(); + + return new Result(resultQuads, resultLines, flags); + } + + /** + * Record a vertex with current attributes. + * Color is read from GLStateManager at call time. + */ + public void vertex(float x, float y, float z) { + if (!inPrimitive) { + throw new IllegalStateException("glVertex called outside glBegin/glEnd"); + } + + // Get current color from GLStateManager + int packedColor = packColor( + GLStateManager.getColor().getRed(), + GLStateManager.getColor().getGreen(), + GLStateManager.getColor().getBlue(), + GLStateManager.getColor().getAlpha() + ); + if (packedColor != 0xFFFFFFFF) { + hasColor = true; + } + + // Pack normal + int packedNormal = NormI8.pack(normalX, normalY, normalZ); + + currentVertices.add(new ImmediateVertex( + x, y, z, + packedColor, + texCoordS, texCoordT, + packedNormal + )); + } + + /** + * Pack RGBA floats (0-1) into ABGR int format used by ModelQuad. + */ + private static int packColor(float r, float g, float b, float a) { + int ri = (int) (r * 255.0f) & 0xFF; + int gi = (int) (g * 255.0f) & 0xFF; + int bi = (int) (b * 255.0f) & 0xFF; + int ai = (int) (a * 255.0f) & 0xFF; + // ABGR format (little-endian: stored as RGBA bytes) + return (ai << 24) | (bi << 16) | (gi << 8) | ri; + } + + /** + * Convert the current primitive vertices to quads. + * Handles different primitive types. + */ + private void convertPrimitiveToQuads() { + int vertexCount = currentVertices.size(); + if (vertexCount == 0) { + return; + } + + switch (currentPrimitiveType) { + case GL11.GL_QUADS -> convertQuads(); + case GL11.GL_TRIANGLES -> convertTriangles(); + case GL11.GL_TRIANGLE_FAN, GL11.GL_POLYGON -> convertTriangleFan(); + case GL11.GL_TRIANGLE_STRIP -> convertTriangleStrip(); + case GL11.GL_QUAD_STRIP -> convertQuadStrip(); + case GL11.GL_LINES -> convertLines(); + case GL11.GL_LINE_STRIP -> convertLineStripToLines(); + case GL11.GL_LINE_LOOP -> convertLineLoopToLines(); + case GL11.GL_POINTS -> { + LOGGER.warn("[ImmediateModeRecorder] Point primitives not supported, discarding {} vertices", vertexCount); + } + default -> { + LOGGER.warn("[ImmediateModeRecorder] Unknown primitive type {}, discarding {} vertices", + currentPrimitiveType, vertexCount); + } + } + } + + /** + * Convert GL_QUADS vertices to ModelQuads (4 vertices per quad). + */ + private void convertQuads() { + int vertexCount = currentVertices.size(); + int fullQuads = vertexCount / 4; + + for (int i = 0; i < fullQuads; i++) { + int base = i * 4; + addQuad( + currentVertices.get(base), + currentVertices.get(base + 1), + currentVertices.get(base + 2), + currentVertices.get(base + 3) + ); + } + + int remaining = vertexCount % 4; + if (remaining > 0) { + LOGGER.warn("[ImmediateModeRecorder] GL_QUADS: Incomplete quad, discarding {} vertices", remaining); + } + } + + /** + * Convert GL_TRIANGLES vertices to degenerate quads (3 vertices per triangle, duplicate last). + */ + private void convertTriangles() { + int vertexCount = currentVertices.size(); + int fullTriangles = vertexCount / 3; + + for (int i = 0; i < fullTriangles; i++) { + int base = i * 3; + ImmediateVertex v0 = currentVertices.get(base); + ImmediateVertex v1 = currentVertices.get(base + 1); + ImmediateVertex v2 = currentVertices.get(base + 2); + // Degenerate quad: duplicate third vertex + addQuad(v0, v1, v2, v2); + } + + int remaining = vertexCount % 3; + if (remaining > 0) { + LOGGER.warn("[ImmediateModeRecorder] GL_TRIANGLES: Incomplete triangle, discarding {} vertices", remaining); + } + } + + /** + * Convert GL_TRIANGLE_FAN vertices to triangles then to degenerate quads. + * First vertex is center, subsequent vertices form fan. + */ + private void convertTriangleFan() { + int vertexCount = currentVertices.size(); + if (vertexCount < 3) { + LOGGER.warn("[ImmediateModeRecorder] GL_TRIANGLE_FAN: Need at least 3 vertices, got {}", vertexCount); + return; + } + + ImmediateVertex center = currentVertices.get(0); + for (int i = 1; i < vertexCount - 1; i++) { + ImmediateVertex v1 = currentVertices.get(i); + ImmediateVertex v2 = currentVertices.get(i + 1); + // Triangle: center, v1, v2 -> degenerate quad + addQuad(center, v1, v2, v2); + } + } + + /** + * Convert GL_TRIANGLE_STRIP vertices to triangles then to degenerate quads. + */ + private void convertTriangleStrip() { + int vertexCount = currentVertices.size(); + if (vertexCount < 3) { + LOGGER.warn("[ImmediateModeRecorder] GL_TRIANGLE_STRIP: Need at least 3 vertices, got {}", vertexCount); + return; + } + + for (int i = 0; i < vertexCount - 2; i++) { + ImmediateVertex v0, v1, v2; + if ((i & 1) == 0) { + // Even triangle: v[i], v[i+1], v[i+2] + v0 = currentVertices.get(i); + v1 = currentVertices.get(i + 1); + v2 = currentVertices.get(i + 2); + } else { + // Odd triangle: v[i+1], v[i], v[i+2] (reverse winding) + v0 = currentVertices.get(i + 1); + v1 = currentVertices.get(i); + v2 = currentVertices.get(i + 2); + } + addQuad(v0, v1, v2, v2); + } + } + + /** + * Convert GL_QUAD_STRIP vertices to quads. + * Vertices come in pairs: (v0,v1), (v2,v3), (v4,v5)... + * Each quad uses 4 consecutive vertices in pattern: v[2i], v[2i+1], v[2i+3], v[2i+2] + */ + private void convertQuadStrip() { + int vertexCount = currentVertices.size(); + if (vertexCount < 4) { + LOGGER.warn("[ImmediateModeRecorder] GL_QUAD_STRIP: Need at least 4 vertices, got {}", vertexCount); + return; + } + + int numQuads = (vertexCount - 2) / 2; + for (int i = 0; i < numQuads; i++) { + int base = i * 2; + // Quad strip winding: v[0], v[1], v[3], v[2] + addQuad( + currentVertices.get(base), + currentVertices.get(base + 1), + currentVertices.get(base + 3), + currentVertices.get(base + 2) + ); + } + } + + /** + * Convert GL_LINES vertices to line segments (pairs of vertices). + */ + private void convertLines() { + int vertexCount = currentVertices.size(); + int fullLines = vertexCount / 2; + + for (int i = 0; i < fullLines; i++) { + int base = i * 2; + ImmediateVertex v0 = currentVertices.get(base); + ImmediateVertex v1 = currentVertices.get(base + 1); + lineVertices.add(new LineVertex(v0.x, v0.y, v0.z, v0.color)); + lineVertices.add(new LineVertex(v1.x, v1.y, v1.z, v1.color)); + } + + int remaining = vertexCount % 2; + if (remaining > 0) { + LOGGER.warn("[ImmediateModeRecorder] GL_LINES: Incomplete line, discarding {} vertices", remaining); + } + } + + /** + * Convert GL_LINE_STRIP to individual line segments. + * Vertices 0-1-2-3 become lines: 0-1, 1-2, 2-3 + */ + private void convertLineStripToLines() { + int vertexCount = currentVertices.size(); + if (vertexCount < 2) { + LOGGER.warn("[ImmediateModeRecorder] GL_LINE_STRIP: Need at least 2 vertices, got {}", vertexCount); + return; + } + + for (int i = 0; i < vertexCount - 1; i++) { + ImmediateVertex v0 = currentVertices.get(i); + ImmediateVertex v1 = currentVertices.get(i + 1); + lineVertices.add(new LineVertex(v0.x, v0.y, v0.z, v0.color)); + lineVertices.add(new LineVertex(v1.x, v1.y, v1.z, v1.color)); + } + } + + /** + * Convert GL_LINE_LOOP to individual line segments. + * Like LINE_STRIP but also closes the loop from last to first vertex. + */ + private void convertLineLoopToLines() { + int vertexCount = currentVertices.size(); + if (vertexCount < 2) { + LOGGER.warn("[ImmediateModeRecorder] GL_LINE_LOOP: Need at least 2 vertices, got {}", vertexCount); + return; + } + + // Convert as strip first + convertLineStripToLines(); + + // Close the loop: add last-to-first segment + ImmediateVertex vLast = currentVertices.get(vertexCount - 1); + ImmediateVertex vFirst = currentVertices.get(0); + lineVertices.add(new LineVertex(vLast.x, vLast.y, vLast.z, vLast.color)); + lineVertices.add(new LineVertex(vFirst.x, vFirst.y, vFirst.z, vFirst.color)); + } + + /** + * Create a ModelQuad from 4 ImmediateVertex objects. + */ + private void addQuad(ImmediateVertex v0, ImmediateVertex v1, ImmediateVertex v2, ImmediateVertex v3) { + ModelQuad quad = new ModelQuad(); + + setVertex(quad, 0, v0); + setVertex(quad, 1, v1); + setVertex(quad, 2, v2); + setVertex(quad, 3, v3); + + // Set light face based on computed normal + quad.setLightFace(ModelQuadUtil.findLightFace(quad.getComputedFaceNormal())); + + quads.add(quad); + } + + /** + * Set a single vertex in a ModelQuad. + */ + private void setVertex(ModelQuad quad, int idx, ImmediateVertex v) { + quad.setX(idx, v.x); + quad.setY(idx, v.y); + quad.setZ(idx, v.z); + quad.setColor(idx, v.color); + quad.setTexU(idx, v.texU); + quad.setTexV(idx, v.texV); + quad.setForgeNormal(idx, v.normal); + // Light defaults to full brightness (no lightmap for immediate mode typically) + quad.setLight(idx, ModelQuadUtil.DEFAULT_LIGHTMAP); + } + + /** + * Check if any immediate mode geometry has been captured. + * + *

Note: With immediate flush mode, this will typically return false as geometry + * is returned from end() immediately. This method is kept for edge cases + * where primitives might fail to convert (e.g., incomplete quads). + */ + public boolean hasGeometry() { + return !quads.isEmpty() || !lineVertices.isEmpty(); + } + + /** + * Get any remaining accumulated geometry and clear the recorder. + * + *

Note: With immediate flush mode, this will typically return null as geometry + * is returned from end() immediately. This method is kept for cleanup and + * edge cases where geometry might remain (e.g., if end() was never called). + * + * @return Result containing remaining quads, lines, and flags, or null if no geometry + */ + public Result getQuadsAndClear() { + if (inPrimitive) { + throw new IllegalStateException("Cannot get geometry while in primitive (missing glEnd)"); + } + + if (quads.isEmpty() && lineVertices.isEmpty()) { + return null; + } + + // Copy geometry and create flags + List resultQuads = new ArrayList<>(quads); + List resultLines = new ArrayList<>(lineVertices); + CapturingTessellator.Flags flags = new CapturingTessellator.Flags( + hasTexCoord, // hasTexture + false, // hasBrightness - immediate mode doesn't typically use lightmap + hasColor, // hasColor + hasNormal // hasNormals + ); + + // Clear state for reuse + quads.clear(); + lineVertices.clear(); + hasColor = false; + hasTexCoord = false; + hasNormal = false; + // Note: current texcoord/normal values are NOT reset - they persist per OpenGL spec + + return new Result(resultQuads, resultLines, flags); + } + + /** + * Result of immediate mode recording: quads, lines, and their attribute flags. + */ + @com.github.bsideup.jabel.Desugar + public record Result(List quads, List lines, CapturingTessellator.Flags flags) {} + + /** + * Reset all state including current texcoord/normal. + * Called when starting a new display list. + */ + public void reset() { + quads.clear(); + lineVertices.clear(); + currentVertices.clear(); + inPrimitive = false; + currentPrimitiveType = -1; + texCoordS = 0.0f; + texCoordT = 0.0f; + normalX = 0.0f; + normalY = 0.0f; + normalZ = 1.0f; + hasColor = false; + hasTexCoord = false; + hasNormal = false; + } + + /** + * Process a glDrawArrays call during display list recording. + * Reads vertex data from client arrays (tracked globally in GLStateManager) and converts to immediate mode geometry. + * + * @param mode Primitive type (GL_QUADS, GL_LINES, etc.) + * @param first Starting index in arrays + * @param count Number of vertices + * @return Result containing converted geometry, or null if no geometry produced + */ + public Result processDrawArrays(int mode, int first, int count) { + final var cas = GLStateManager.getClientArrayState(); + final var vertexPointer = cas.isVertexArrayEnabled() ? cas.getVertexPointer() : null; + final var colorPointer = cas.isColorArrayEnabled() ? cas.getColorPointer() : null; + + return convertClientArrays( + mode, first, count, + vertexPointer, + cas.getVertexPointerType(), cas.getVertexPointerSize(), cas.getVertexPointerStride(), + colorPointer, + cas.getColorPointerType(), cas.getColorPointerSize(), cas.getColorPointerStride() + ); + } + + /** + * Convert client array data to immediate mode. + * Called by glDrawArrays during display list recording. + * Like Mesa's save_DrawArrays, reads vertex data from arrays and emits as immediate mode. + * + * @param mode Primitive type (GL_QUADS, GL_LINES, etc.) + * @param first Starting index in arrays + * @param count Number of vertices + * @param vertexPointer Vertex position buffer (or null if not enabled) + * @param vertexType GL type (GL_FLOAT, GL_DOUBLE, etc.) + * @param vertexSize Size of vertex (2, 3, or 4) + * @param vertexStride Stride in bytes (0 for tightly packed) + * @param colorPointer Color buffer (or null if not enabled) + * @param colorType GL type (GL_FLOAT, GL_UNSIGNED_BYTE, etc.) + * @param colorSize Size of color (3 or 4) + * @param colorStride Stride in bytes (0 for tightly packed) + * @return Result containing converted geometry, or null if no geometry produced + */ + public Result convertClientArrays( + int mode, int first, int count, + java.nio.Buffer vertexPointer, int vertexType, int vertexSize, int vertexStride, + java.nio.Buffer colorPointer, int colorType, int colorSize, int colorStride + ) { + begin(mode); + + // Hoist type checks and stride calculations outside the loop for performance. + // Mesa's algorithm: offset_bytes = index * effective_stride + // effective_stride = (stride == 0) ? (numComponents * sizeof(element)) : stride + + // Determine color reader (hoisted out of loop) + final ColorReader colorReader; + final int colorEffectiveStride; + if (colorPointer == null) { + colorReader = null; + colorEffectiveStride = 0; + } else if (colorType == GL11.GL_FLOAT && colorPointer instanceof FloatBuffer fb) { + colorEffectiveStride = (colorStride == 0) ? colorSize * 4 : colorStride; + final int strideInFloats = colorEffectiveStride / 4; + colorReader = (idx) -> { + final int offset = idx * strideInFloats; + final float r = fb.get(offset); + final float g = fb.get(offset + 1); + final float b = fb.get(offset + 2); + final float a = (colorSize == 4) ? fb.get(offset + 3) : 1.0f; + GLStateManager.glColor4f(r, g, b, a); + }; + } else if ((colorType == GL11.GL_UNSIGNED_BYTE || colorType == GL11.GL_BYTE) && colorPointer instanceof ByteBuffer bb) { + colorEffectiveStride = (colorStride == 0) ? colorSize : colorStride; + colorReader = (idx) -> { + final int offset = idx * colorEffectiveStride; + final float r = (bb.get(offset) & 0xFF) / 255.0f; + final float g = (bb.get(offset + 1) & 0xFF) / 255.0f; + final float b = (bb.get(offset + 2) & 0xFF) / 255.0f; + final float a = (colorSize == 4) ? (bb.get(offset + 3) & 0xFF) / 255.0f : 1.0f; + GLStateManager.glColor4f(r, g, b, a); + }; + } else { + // Unsupported color type - use white + colorEffectiveStride = 0; + colorReader = (idx) -> GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + } + + // Determine vertex reader (hoisted out of loop) + final VertexReader vertexReader; + if (vertexPointer == null) { + vertexReader = null; + } else if (vertexType == GL11.GL_FLOAT && vertexPointer instanceof FloatBuffer fb) { + final int effectiveStride = (vertexStride == 0) ? vertexSize * 4 : vertexStride; + final int strideInFloats = effectiveStride / 4; + vertexReader = (idx) -> { + final int offset = idx * strideInFloats; + final float x = fb.get(offset); + final float y = fb.get(offset + 1); + final float z = (vertexSize >= 3) ? fb.get(offset + 2) : 0.0f; + vertex(x, y, z); + }; + } else if (vertexType == GL11.GL_DOUBLE && vertexPointer instanceof DoubleBuffer db) { + final int effectiveStride = (vertexStride == 0) ? vertexSize * 8 : vertexStride; + final int strideInDoubles = effectiveStride / 8; + vertexReader = (idx) -> { + final int offset = idx * strideInDoubles; + final float x = (float) db.get(offset); + final float y = (float) db.get(offset + 1); + final float z = (vertexSize >= 3) ? (float) db.get(offset + 2) : 0.0f; + vertex(x, y, z); + }; + } else if (vertexType == GL11.GL_INT && vertexPointer instanceof IntBuffer ib) { + final int effectiveStride = (vertexStride == 0) ? vertexSize * 4 : vertexStride; + final int strideInInts = effectiveStride / 4; + vertexReader = (idx) -> { + final int offset = idx * strideInInts; + final float x = ib.get(offset); + final float y = ib.get(offset + 1); + final float z = (vertexSize >= 3) ? ib.get(offset + 2) : 0.0f; + vertex(x, y, z); + }; + } else if (vertexType == GL11.GL_SHORT && vertexPointer instanceof ShortBuffer sb) { + final int effectiveStride = (vertexStride == 0) ? vertexSize * 2 : vertexStride; + final int strideInShorts = effectiveStride / 2; + vertexReader = (idx) -> { + final int offset = idx * strideInShorts; + final float x = sb.get(offset); + final float y = sb.get(offset + 1); + final float z = (vertexSize >= 3) ? sb.get(offset + 2) : 0.0f; + vertex(x, y, z); + }; + } else { + // Unsupported vertex type + vertexReader = null; + } + + // Main loop - no type checks, just function calls + for (int i = first; i < first + count; i++) { + if (colorReader != null) { + colorReader.read(i); + } + if (vertexReader != null) { + vertexReader.read(i); + } + } + + return end(); + } + + @FunctionalInterface + private interface ColorReader { + void read(int index); + } + + @FunctionalInterface + private interface VertexReader { + void read(int index); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/RecordedGeometry.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/RecordedGeometry.java new file mode 100644 index 000000000..69364689c --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/RecordedGeometry.java @@ -0,0 +1,19 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.recording.commands.DrawCommand; +import me.jellysquid.mods.sodium.client.gl.attribute.BufferVertexFormat; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Represents recorded geometry from a display list compilation. + * Contains the vertex data buffer and metadata about draw commands. + */ +@Desugar +public record RecordedGeometry(ByteBuffer buffer, BufferVertexFormat format, List drawCommands, int totalVertexCount) { + public int getByteSize() { + return buffer.remaining(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/AccumulableMatrixCommand.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/AccumulableMatrixCommand.java new file mode 100644 index 000000000..e14e97653 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/AccumulableMatrixCommand.java @@ -0,0 +1,21 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import org.joml.Matrix4f; + +/** + * Interface for matrix commands that can be accumulated during transform optimization. + * Implemented by TranslateCmd, RotateCmd, ScaleCmd, and MultMatrixCmd. + */ +public interface AccumulableMatrixCommand extends DisplayListCommand { + boolean isModelView(); + void applyTo(Matrix4f matrix); + + @Override + default boolean handleOptimization(OptimizationContext ctx) { + if (isModelView()) { + applyTo(ctx.getAccumulatedTransform()); + return false; // Accumulated, don't emit + } + return true; // Non-MODELVIEW, emit as-is + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/CallListCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/CallListCmd.java new file mode 100644 index 000000000..faf74dcf3 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/CallListCmd.java @@ -0,0 +1,61 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +/** + * Command: Call another display list (glCallList) + * Records a reference to another display list to be executed during playback. + * Per OpenGL spec: the referenced list is NOT executed during compilation, + * only the reference is recorded. + * + */ +@Desugar +public record CallListCmd(int listId) implements DisplayListCommand { + private static final Logger LOGGER = LogManager.getLogger("CallListCmd"); + + @Override + public void execute() { + final CompiledDisplayList compiled = GLStateManager.getDisplayList(listId); + + if (compiled != null) { + GLDebug.pushGroup("CallList(" + listId + ")"); + try { + compiled.render(); + } finally { + GLDebug.popGroup(); + } + } else { + // Fallback if list not found (shouldn't happen in normal operation) + LOGGER.warn("[CallListCmd] List {} not found in cache, using fallback glCallList", listId); + GLStateManager.glCallList(listId); + } + } + + @Override + public void delete() { + // No resources to free - we just hold a reference + } + + @Override + public boolean needsTransformSync() { + return true; // Nested list uses current GL matrix state + } + + @Override + public boolean handleOptimization(OptimizationContext ctx) { + // Nested list needs current GL matrix state + ctx.emitPendingTransform(); + return true; // Emit this command + } + + @Override + public @NotNull String toString() { + return "CallList(" + listId + ")"; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DisplayListCommand.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DisplayListCommand.java new file mode 100644 index 000000000..7d5e5f64e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DisplayListCommand.java @@ -0,0 +1,55 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +/** + * A single command recorded during display list compilation. + * Commands are executed in sequence during glCallList() to replay the display list. + */ +public interface DisplayListCommand { + /** + * Execute this command by calling the appropriate GL function. + */ + void execute(); + + /** + * Release any resources held by this command (e.g., VBOs). + * Called when the display list is deleted. + */ + default void delete() { + // Most commands don't hold resources + } + + /** + * Returns true if this barrier command requires the MODELVIEW transform to be + * synchronized with the accumulated state before execution. + * + *

This should return true for commands where the GL matrix state matters, + * such as CallListCmd (nested lists use current GL state) or PushAttribCmd + * with GL_TRANSFORM_BIT (saves the current matrix).

+ * + *

Commands like PushAttribCmd with only GL_COLOR_BUFFER_BIT don't need + * transform sync because they don't interact with the matrix stack.

+ * + * @return true if transform must be synced to accumulated before this barrier + */ + default boolean needsTransformSync() { + return false; + } + + /** + * Handle this command during display list optimization. + * + *

Commands override this to implement their optimization behavior: + *

    + *
  • Matrix transforms: accumulate into the optimizer, return false
  • + *
  • Push/Pop: update stack, return true to emit
  • + *
  • Barriers: emit pending transforms, return true to emit
  • + *
  • State changes: return true to emit
  • + *
+ * + * @param ctx The optimization context providing access to transform state + * @return true if this command should be added to output, false if handled internally + */ + default boolean handleOptimization(OptimizationContext ctx) { + return true; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DrawCommand.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DrawCommand.java new file mode 100644 index 000000000..88be7bdfc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DrawCommand.java @@ -0,0 +1,14 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; + +/** + * Represents a single draw call within a compiled display list. + * Each DrawCommand corresponds to one glBegin/glEnd block. + */ +@Desugar +public record DrawCommand( + int firstVertex, + int vertexCount, + int primitiveType +) {} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DrawRangeCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DrawRangeCmd.java new file mode 100644 index 000000000..2815272d5 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/DrawRangeCmd.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import org.jetbrains.annotations.NotNull; + +/** + * Command: Draw a range of vertices from a shared VBO. + *

+ * DrawRangeCmd references a shared VBO owned by CompiledDisplayList.ownedVbos. + * Multiple DrawRangeCmds can reference different ranges within the same VBO, + * enabling format-based batching where all draws with the same vertex format + * share a single VBO. + *

+ * Transform is NOT stored here - handled by TransformOptimizer emitting + * MultMatrixCmd before this command during optimization. + */ +@Desugar +public record DrawRangeCmd( + VertexBuffer sharedVbo, + int startVertex, + int vertexCount, + boolean hasBrightness +) implements DisplayListCommand { + + @Override + public void execute() { + sharedVbo.setupState(); + sharedVbo.draw(startVertex, vertexCount); + sharedVbo.cleanupState(); + } + + @Override + public void delete() { + // No-op: VBO lifetime is managed by CompiledDisplayList.ownedVbos + } + + @Override + public @NotNull String toString() { + return "DrawRange(start=" + startVertex + ", count=" + vertexCount + ", hasBrightness=" + hasBrightness + ")"; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/LoadIdentityCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/LoadIdentityCmd.java new file mode 100644 index 000000000..cf0754302 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/LoadIdentityCmd.java @@ -0,0 +1,44 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL11; + +/** + * Command: glLoadIdentity() + * Replaces the current matrix with the identity matrix for the specified matrix mode. + */ +@Desugar +public record LoadIdentityCmd(int matrixMode) implements DisplayListCommand { + @Override + public void execute() { + GLStateManager.glLoadIdentity(); + } + + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public boolean handleOptimization(OptimizationContext ctx) { + if (isModelView()) { + // Check if we're at an absolute matrix (after LoadMatrix) + // If so, we must emit LoadIdentity to actually reset GL state + if (ctx.checkAndClearAbsoluteMatrix()) { + ctx.emit(this); + ctx.loadIdentity(); + return false; // Already emitted + } + ctx.loadIdentity(); + return false; // Accumulated, don't emit + } + return true; // Non-MODELVIEW, emit as-is + } + + @Override + public @NotNull String toString() { + return String.format("LoadIdentity(%s)", GLDebug.getMatrixModeName(matrixMode)); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/LoadMatrixCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/LoadMatrixCmd.java new file mode 100644 index 000000000..59687100f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/LoadMatrixCmd.java @@ -0,0 +1,75 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; + +/** + * Command: glLoadMatrix(matrix) + * Replaces the current matrix with the specified matrix for the given matrix mode. + * + *

Unlike MultMatrixCmd which can be absorbed into the accumulated transform, + * LoadMatrixCmd must always be emitted because it sets an absolute value, not a relative one. + * After a LoadMatrix command, the accumulated transform is reset to identity since + * subsequent transforms are relative to the newly loaded matrix.

+ */ +@Desugar +public record LoadMatrixCmd(Matrix4f matrix, int matrixMode, FloatBuffer buffer) implements DisplayListCommand { + + /** + * Create a LoadMatrixCmd with pre-allocated buffer. + */ + public static LoadMatrixCmd create(Matrix4f matrix, int matrixMode) { + final FloatBuffer buffer = BufferUtils.createFloatBuffer(16); + matrix.get(buffer); + buffer.rewind(); + return new LoadMatrixCmd(new Matrix4f(matrix), matrixMode, buffer); + } + + /** + * Create a LoadMatrixCmd from a FloatBuffer (copies the data). + */ + public static LoadMatrixCmd create(FloatBuffer src, int matrixMode) { + final Matrix4f matrix = new Matrix4f().set(src); + src.rewind(); + return create(matrix, matrixMode); + } + + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public void execute() { + // Buffer already populated at construction, just rewind and use + buffer.rewind(); + GLStateManager.glLoadMatrix(buffer); + } + + @Override + public boolean handleOptimization(OptimizationContext ctx) { + if (isModelView()) { + // LoadMatrix sets an absolute value - emit any pending relative transform first + ctx.emitPendingTransform(); + // Emit this command + ctx.emit(this); + // Reset accumulated to identity - subsequent transforms are relative to the loaded matrix + ctx.loadIdentity(); + // Mark that GL is at an absolute value - LoadIdentity must emit to reset + ctx.markAbsoluteMatrix(); + return false; // Already emitted + } + return true; // Non-MODELVIEW, emit as-is + } + + @Override + public @NotNull String toString() { + return String.format("LoadMatrix(%s, %s)", GLDebug.getMatrixModeName(matrixMode), matrix); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/MultMatrixCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/MultMatrixCmd.java new file mode 100644 index 000000000..5f9a0f2e1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/MultMatrixCmd.java @@ -0,0 +1,50 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; + +/** + * Command: glMultMatrix(matrix) + * Multiplies the specified matrix mode by the specified matrix. + */ +@Desugar +public record MultMatrixCmd(Matrix4f matrix, int matrixMode, FloatBuffer buffer) implements AccumulableMatrixCommand { + + /** + * Create a MultMatrixCmd with pre-allocated buffer. + */ + public static MultMatrixCmd create(Matrix4f matrix, int matrixMode) { + final FloatBuffer buffer = BufferUtils.createFloatBuffer(16); + matrix.get(buffer); + buffer.rewind(); + return new MultMatrixCmd(new Matrix4f(matrix), matrixMode, buffer); + } + + @Override + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public void applyTo(Matrix4f target) { + target.mul(matrix); + } + + @Override + public void execute() { + // Buffer already populated at construction, just rewind and use + buffer.rewind(); + GLStateManager.glMultMatrix(buffer); + } + + @Override + public @NotNull String toString() { + return String.format("MultMatrix(%s)", matrix); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/OptimizationContext.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/OptimizationContext.java new file mode 100644 index 000000000..5f957e4d5 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/OptimizationContext.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import org.joml.Matrix4f; + +/** + * Context provided to commands during display list optimization. + * Allows commands to interact with the transform optimizer and draw batcher. + */ +public interface OptimizationContext { + /** Get the accumulated MODELVIEW transform matrix. */ + Matrix4f getAccumulatedTransform(); + + /** Reset accumulated transform to identity. */ + void loadIdentity(); + + /** Push current accumulated transform onto stack. */ + void pushTransform(); + + /** Pop transform from stack, restoring previous accumulated value. */ + void popTransform(); + + /** Emit a MultMatrix command if accumulated differs from last emitted. */ + void emitPendingTransform(); + + /** Emit a MultMatrix command to reach the target transform. */ + void emitTransformTo(Matrix4f target); + + /** Add a command to the output list. */ + void emit(DisplayListCommand cmd); + + /** + * Mark that the GL matrix is at an absolute value (after LoadMatrix). + * Subsequent LoadIdentity commands must be emitted to reset to identity. + */ + void markAbsoluteMatrix(); + + /** + * Check if the GL matrix is at an absolute value and clear the flag. + * @return true if LoadMatrix was called and not yet reset by LoadIdentity + */ + boolean checkAndClearAbsoluteMatrix(); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PopAttribCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PopAttribCmd.java new file mode 100644 index 000000000..9d44797f7 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PopAttribCmd.java @@ -0,0 +1,33 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; + +/** + * Command: glPopAttrib() + * Pops the server attribute stack. + */ +@Desugar +public record PopAttribCmd() implements DisplayListCommand { + @Override + public void execute() { + GLStateManager.glPopAttrib(); + } + + @Override + public boolean needsTransformSync() { + return true; + } + + @Override + public boolean handleOptimization(OptimizationContext ctx) { + ctx.emitPendingTransform(); + return true; + } + + @Override + public @NotNull String toString() { + return "PopAttrib()"; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PopMatrixCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PopMatrixCmd.java new file mode 100644 index 000000000..5314218f8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PopMatrixCmd.java @@ -0,0 +1,36 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL11; + +/** + * Command: glPopMatrix() + * Pops the current matrix from the matrix stack for the specified matrix mode. + */ +@Desugar +public record PopMatrixCmd(int matrixMode) implements DisplayListCommand { + @Override + public void execute() { + GLStateManager.glPopMatrix(); + } + + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public boolean handleOptimization(OptimizationContext ctx) { + if (isModelView()) { + ctx.popTransform(); + } + return true; // Always emit Pop + } + + @Override + public @NotNull String toString() { + return String.format("PopMatrix(%s)", GLDebug.getMatrixModeName(matrixMode)); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PushAttribCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PushAttribCmd.java new file mode 100644 index 000000000..d23c951b4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PushAttribCmd.java @@ -0,0 +1,38 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL11; + +/** + * Command: glPushAttrib(mask) + * Pushes the server attribute stack. + */ +@Desugar +public record PushAttribCmd(int mask) implements DisplayListCommand { + @Override + public void execute() { + GLStateManager.glPushAttrib(mask); + } + + @Override + public boolean needsTransformSync() { + // Only need transform sync if we're pushing transform state. + // GL_TRANSFORM_BIT includes the matrix mode and matrix stacks. + return (mask & GL11.GL_TRANSFORM_BIT) != 0; + } + + @Override + public boolean handleOptimization(OptimizationContext ctx) { + if (needsTransformSync()) { + ctx.emitPendingTransform(); + } + return true; + } + + @Override + public @NotNull String toString() { + return String.format("PushAttrib(0x%08X)", mask); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PushMatrixCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PushMatrixCmd.java new file mode 100644 index 000000000..7c459982a --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/PushMatrixCmd.java @@ -0,0 +1,37 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.opengl.GL11; + +/** + * Command: glPushMatrix() + * Pushes the current matrix onto the matrix stack for the specified matrix mode. + */ +@Desugar +public record PushMatrixCmd(int matrixMode) implements DisplayListCommand { + @Override + public void execute() { + GLStateManager.glPushMatrix(); + } + + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public boolean handleOptimization(OptimizationContext ctx) { + if (isModelView()) { + ctx.emitPendingTransform(); + ctx.pushTransform(); + } + return true; // Always emit Push + } + + @Override + public @NotNull String toString() { + return String.format("PushMatrix(%s)", GLDebug.getMatrixModeName(matrixMode)); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/RotateCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/RotateCmd.java new file mode 100644 index 000000000..8cc2151fa --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/RotateCmd.java @@ -0,0 +1,35 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; + +/** + * Command: glRotatef() or glRotated() + * Stores as doubles for precision. + */ +@Desugar +public record RotateCmd(double angle, double x, double y, double z, int matrixMode) implements AccumulableMatrixCommand { + @Override + public void execute() { + GLStateManager.glRotated(angle, x, y, z); + } + + @Override + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public void applyTo(Matrix4f matrix) { + matrix.rotate((float) Math.toRadians(angle), (float) x, (float) y, (float) z); + } + + @Override + public @NotNull String toString() { + return String.format("Rotate(%.2f deg, %.2f, %.2f, %.2f, %s)", angle, x, y, z, GLDebug.getMatrixModeName(matrixMode)); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/ScaleCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/ScaleCmd.java new file mode 100644 index 000000000..c2e963391 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/ScaleCmd.java @@ -0,0 +1,35 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; + +/** + * Command: glScalef() or glScaled() + * Stores as doubles for precision. + */ +@Desugar +public record ScaleCmd(double x, double y, double z, int matrixMode) implements AccumulableMatrixCommand { + @Override + public void execute() { + GLStateManager.glScaled(x, y, z); + } + + @Override + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public void applyTo(Matrix4f matrix) { + matrix.scale((float) x, (float) y, (float) z); + } + + @Override + public @NotNull String toString() { + return String.format("Scale(%.2f, %.2f, %.2f, %s)", x, y, z, GLDebug.getMatrixModeName(matrixMode)); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TexImage2DCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TexImage2DCmd.java new file mode 100644 index 000000000..8bccfe828 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TexImage2DCmd.java @@ -0,0 +1,117 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** + * Command: glTexImage2D(target, level, internalformat, width, height, border, format, type, pixels) + * Specifies a two-dimensional texture image during display list playback. + */ +@Desugar +public record TexImage2DCmd( + int target, + int level, + int internalformat, + int width, + int height, + int border, + int format, + int type, + @Nullable byte[] pixelData +) implements DisplayListCommand { + + /** + * Create command from IntBuffer + */ + public static TexImage2DCmd fromIntBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable IntBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = (limit - pos) * 4; // 4 bytes per int + data = new byte[size]; + ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + bb.asIntBuffer().put(pixels); + pixels.position(pos); // Restore position + } + return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data); + } + + /** + * Create command from FloatBuffer + */ + public static TexImage2DCmd fromFloatBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable FloatBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = (limit - pos) * 4; // 4 bytes per float + data = new byte[size]; + ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + bb.asFloatBuffer().put(pixels); + pixels.position(pos); // Restore position + } + return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data); + } + + /** + * Create command from DoubleBuffer + */ + public static TexImage2DCmd fromDoubleBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable DoubleBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = (limit - pos) * 8; // 8 bytes per double + data = new byte[size]; + ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + bb.asDoubleBuffer().put(pixels); + pixels.position(pos); // Restore position + } + return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data); + } + + /** + * Create command from ByteBuffer + */ + public static TexImage2DCmd fromByteBuffer(int target, int level, int internalformat, int width, int height, int border, int format, int type, @Nullable ByteBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = limit - pos; + data = new byte[size]; + pixels.get(data); + pixels.position(pos); // Restore position + } + return new TexImage2DCmd(target, level, internalformat, width, height, border, format, type, data); + } + + @Override + public void execute() { + ByteBuffer buffer = null; + if (pixelData != null) { + buffer = ByteBuffer.allocateDirect(pixelData.length).order(ByteOrder.nativeOrder()); + buffer.put(pixelData); + buffer.flip(); + } + GLStateManager.glTexImage2D(target, level, internalformat, width, height, border, format, type, buffer); + } + + @Override + public @NotNull String toString() { + String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null"; + return String.format("TexImage2D(target=%s, level=%d, internalformat=%s, %dx%d, border=%d, format=%s, type=%s, data=%s)", + GLDebug.getTextureTargetName(target), level, GLDebug.getTextureFormatName(internalformat), + width, height, border, GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TexSubImage2DCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TexSubImage2DCmd.java new file mode 100644 index 000000000..479923d9f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TexSubImage2DCmd.java @@ -0,0 +1,117 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +/** + * Command: glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels) + * Updates a portion of an existing two-dimensional texture image during display list playback. + */ +@Desugar +public record TexSubImage2DCmd( + int target, + int level, + int xoffset, + int yoffset, + int width, + int height, + int format, + int type, + @Nullable byte[] pixelData +) implements DisplayListCommand { + + /** + * Create command from IntBuffer + */ + public static TexSubImage2DCmd fromIntBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable IntBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = (limit - pos) * 4; // 4 bytes per int + data = new byte[size]; + ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + bb.asIntBuffer().put(pixels); + pixels.position(pos); // Restore position + } + return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data); + } + + /** + * Create command from FloatBuffer + */ + public static TexSubImage2DCmd fromFloatBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable FloatBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = (limit - pos) * 4; // 4 bytes per float + data = new byte[size]; + ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + bb.asFloatBuffer().put(pixels); + pixels.position(pos); // Restore position + } + return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data); + } + + /** + * Create command from DoubleBuffer + */ + public static TexSubImage2DCmd fromDoubleBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable DoubleBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = (limit - pos) * 8; // 8 bytes per double + data = new byte[size]; + ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.nativeOrder()); + bb.asDoubleBuffer().put(pixels); + pixels.position(pos); // Restore position + } + return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data); + } + + /** + * Create command from ByteBuffer + */ + public static TexSubImage2DCmd fromByteBuffer(int target, int level, int xoffset, int yoffset, int width, int height, int format, int type, @Nullable ByteBuffer pixels) { + byte[] data = null; + if (pixels != null) { + int pos = pixels.position(); + int limit = pixels.limit(); + int size = limit - pos; + data = new byte[size]; + pixels.get(data); + pixels.position(pos); // Restore position + } + return new TexSubImage2DCmd(target, level, xoffset, yoffset, width, height, format, type, data); + } + + @Override + public void execute() { + ByteBuffer buffer = null; + if (pixelData != null) { + buffer = ByteBuffer.allocateDirect(pixelData.length).order(ByteOrder.nativeOrder()); + buffer.put(pixelData); + buffer.flip(); + } + GLStateManager.glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, buffer); + } + + @Override + public @NotNull String toString() { + String dataStr = pixelData != null ? (pixelData.length + " bytes") : "null"; + return String.format("TexSubImage2D(target=%s, level=%d, offset=(%d,%d), %dx%d, format=%s, type=%s, data=%s)", + GLDebug.getTextureTargetName(target), level, xoffset, yoffset, width, height, + GLDebug.getTextureFormatName(format), GLDebug.getDataTypeName(type), dataStr); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TranslateCmd.java b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TranslateCmd.java new file mode 100644 index 000000000..d620a7f1d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/recording/commands/TranslateCmd.java @@ -0,0 +1,43 @@ +package com.gtnewhorizons.angelica.glsm.recording.commands; + +import com.github.bsideup.jabel.Desugar; +import com.gtnewhorizons.angelica.glsm.GLDebug; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.jetbrains.annotations.NotNull; +import org.joml.Matrix4f; +import org.lwjgl.opengl.GL11; + +/** + * Command: glTranslatef() or glTranslated() + * Stores as doubles for precision. + */ +@Desugar +public record TranslateCmd(double x, double y, double z, int matrixMode) implements AccumulableMatrixCommand { + @Override + public void execute() { + // Save current mode + final int savedMode = GLStateManager.getMatrixMode().getMode(); + + // Switch to the mode this was recorded with + GLStateManager.glMatrixMode(matrixMode); + GLStateManager.glTranslated(x, y, z); + + // Restore mode + GLStateManager.glMatrixMode(savedMode); + } + + @Override + public boolean isModelView() { + return matrixMode == GL11.GL_MODELVIEW; + } + + @Override + public void applyTo(Matrix4f matrix) { + matrix.translate((float) x, (float) y, (float) z); + } + + @Override + public @NotNull String toString() { + return String.format("Translate(%.2f, %.2f, %.2f, %s)", x, y, z, GLDebug.getMatrixModeName(matrixMode)); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/BooleanStateStack.java b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/BooleanStateStack.java index 5d72f1967..aca76cfcc 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/BooleanStateStack.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/BooleanStateStack.java @@ -3,13 +3,23 @@ package com.gtnewhorizons.angelica.glsm.stacks; import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.states.BooleanState; -import org.lwjgl.opengl.GL11; +/** + * A stack for boolean GL state with lazy copy-on-write optimization. + *

+ * When using the global attrib depth tracking (from glPushAttrib/glPopAttrib), state is only + * saved when actually modified, dramatically reducing overhead when most states + * don't change (e.g., GL_ENABLE_BIT saves ~270 states but typically only 3-4 change). + */ public class BooleanStateStack extends BooleanState implements IStateStack { - protected final BooleanState[] stack; + protected final boolean[] stack; - protected int pointer; + /** + * The depth at which state has been saved. Compared against GLStateManager.getAttribDepth() + * to determine if we need to save before modification or restore on pop. + */ + protected int savedDepth; public BooleanStateStack(int glCap) { // Most booleans default to false @@ -26,34 +36,83 @@ public class BooleanStateStack extends BooleanState implements IStateStack= stack.length) { + throw new IllegalStateException("Stack overflow size " + (savedDepth + 1) + " reached"); + } + stack[savedDepth++] = enabled; return this; } + @Override public BooleanStateStack pop() { - if(pointer == 0) { + if (savedDepth == 0) { throw new IllegalStateException("Stack underflow"); } + final boolean oldValue = stack[--savedDepth]; + // Call setEnabledDirect to avoid triggering beforeModify during restore + setEnabledDirect(oldValue); + return this; + } - set(stack[--pointer]); - if (GLStateManager.vendorIsAMD() && glCap == GL11.GL_BLEND) { - setEnabled(enabled); + @Override + public boolean isEmpty() { + return savedDepth == 0; + } + + // ==================== Lazy Copy-on-Write Operations ==================== + + @Override + public int pushDepth() { + // No-op: global depth is managed by GLStateManager + return GLStateManager.getAttribDepth(); + } + + /** + * Restore state if it was modified at the current depth. + * Only called by GLStateManager.popState() for states that registered as modified. + */ + @Override + public BooleanStateStack popDepth() { + // We're only called if we were modified, so savedDepth should match + if (savedDepth > 0) { + final boolean oldValue = stack[--savedDepth]; + setEnabledDirect(oldValue); } return this; } - public boolean isEmpty() { - return pointer == 0; + /** + * Called before modifying state. If we haven't saved at the current depth yet, + * save the current value and register with GLStateManager for restoration. + */ + @Override + public void beforeModify() { + final int globalDepth = GLStateManager.getAttribDepth(); + if (savedDepth < globalDepth) { + stack[savedDepth++] = enabled; + GLStateManager.registerModifiedState(this); + } + } + + @Override + public int getDepth() { + return GLStateManager.getAttribDepth(); + } + + @Override + public void setEnabled(boolean enabled) { + beforeModify(); + super.setEnabled(enabled); + } + + private void setEnabledDirect(boolean enabled) { + super.setEnabled(enabled); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/LineStateStack.java b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/LineStateStack.java new file mode 100644 index 000000000..1fb9cbeff --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/LineStateStack.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.glsm.stacks; + +import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.states.LineState; + +/** + * Stack for GL line state (width, stipple factor, stipple pattern). + * Used by GL_LINE_BIT push/pop attrib. + */ +public class LineStateStack extends LineState implements IStateStack { + + protected final LineState[] stack; + protected int pointer; + + public LineStateStack() { + stack = new LineState[GLStateManager.MAX_ATTRIB_STACK_DEPTH]; + for (int i = 0; i < GLStateManager.MAX_ATTRIB_STACK_DEPTH; i++) { + stack[i] = new LineState(); + } + } + + public LineStateStack push() { + if (pointer == stack.length) { + throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached"); + } + stack[pointer++].set(this); + return this; + } + + public LineStateStack pop() { + if (pointer == 0) { + throw new IllegalStateException("Stack underflow"); + } + set(stack[--pointer]); + return this; + } + + public boolean isEmpty() { + return pointer == 0; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/PointStateStack.java b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/PointStateStack.java new file mode 100644 index 000000000..1672eca39 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/PointStateStack.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.glsm.stacks; + +import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.states.PointState; + +/** + * Stack for GL point state (size). + * Used by GL_POINT_BIT push/pop attrib. + */ +public class PointStateStack extends PointState implements IStateStack { + + protected final PointState[] stack; + protected int pointer; + + public PointStateStack() { + stack = new PointState[GLStateManager.MAX_ATTRIB_STACK_DEPTH]; + for (int i = 0; i < GLStateManager.MAX_ATTRIB_STACK_DEPTH; i++) { + stack[i] = new PointState(); + } + } + + public PointStateStack push() { + if (pointer == stack.length) { + throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached"); + } + stack[pointer++].set(this); + return this; + } + + public PointStateStack pop() { + if (pointer == 0) { + throw new IllegalStateException("Stack underflow"); + } + set(stack[--pointer]); + return this; + } + + public boolean isEmpty() { + return pointer == 0; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/PolygonStateStack.java b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/PolygonStateStack.java new file mode 100644 index 000000000..54fe37236 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/PolygonStateStack.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.glsm.stacks; + +import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.states.PolygonState; + +/** + * Stack for GL polygon state (mode, offset, cull face mode, front face). + * Used by GL_POLYGON_BIT push/pop attrib. + */ +public class PolygonStateStack extends PolygonState implements IStateStack { + + protected final PolygonState[] stack; + protected int pointer; + + public PolygonStateStack() { + stack = new PolygonState[GLStateManager.MAX_ATTRIB_STACK_DEPTH]; + for (int i = 0; i < GLStateManager.MAX_ATTRIB_STACK_DEPTH; i++) { + stack[i] = new PolygonState(); + } + } + + public PolygonStateStack push() { + if (pointer == stack.length) { + throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached"); + } + stack[pointer++].set(this); + return this; + } + + public PolygonStateStack pop() { + if (pointer == 0) { + throw new IllegalStateException("Stack underflow"); + } + set(stack[--pointer]); + return this; + } + + public boolean isEmpty() { + return pointer == 0; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/StencilStateStack.java b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/StencilStateStack.java new file mode 100644 index 000000000..3e72464a1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/StencilStateStack.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.glsm.stacks; + +import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.states.StencilState; + +/** + * Stack for GL stencil buffer state. + * Used by GL_STENCIL_BUFFER_BIT push/pop attrib. + */ +public class StencilStateStack extends StencilState implements IStateStack { + + protected final StencilState[] stack; + protected int pointer; + + public StencilStateStack() { + stack = new StencilState[GLStateManager.MAX_ATTRIB_STACK_DEPTH]; + for (int i = 0; i < GLStateManager.MAX_ATTRIB_STACK_DEPTH; i++) { + stack[i] = new StencilState(); + } + } + + public StencilStateStack push() { + if (pointer == stack.length) { + throw new IllegalStateException("Stack overflow size " + (pointer + 1) + " reached"); + } + stack[pointer++].set(this); + return this; + } + + public StencilStateStack pop() { + if (pointer == 0) { + throw new IllegalStateException("Stack underflow"); + } + set(stack[--pointer]); + return this; + } + + public boolean isEmpty() { + return pointer == 0; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/BlendState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BlendState.java index 9713fca44..fc6a88db9 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/states/BlendState.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BlendState.java @@ -21,6 +21,18 @@ public class BlendState implements ISettableState { this.dstAlpha = dstAlpha; } + public void setAll(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { + this.srcRgb = srcRgb; + this.dstRgb = dstRgb; + this.srcAlpha = srcAlpha; + this.dstAlpha = dstAlpha; + } + + public void setSrcDstRgb(int srcRgb, int dstRgb) { + this.srcRgb = srcRgb; + this.dstRgb = dstRgb; + } + @Override public BlendState set(BlendState state) { this.srcRgb = state.srcRgb; diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java index 843f9b78e..64b2ed8ad 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java @@ -22,12 +22,18 @@ public class BooleanState implements ISettableState { } public void setEnabled(boolean enabled) { - if (GLStateManager.shouldBypassCache() || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) { - this.enabled = enabled; - if (enabled) { - GL11.glEnable(this.glCap); - } else { - GL11.glDisable(this.glCap); + final boolean bypass = GLStateManager.shouldBypassCache(); + if (bypass || enabled != this.enabled || (this.glCap == GL11.GL_BLEND && GLStateManager.vendorIsAMD() && GLStateManager.isPoppingAttributes())) { + if (!bypass) { + this.enabled = enabled; + } + // Skip actual GL calls during display list recording (state tracking only) + if (!GLStateManager.isRecordingDisplayList()) { + if (enabled) { + GL11.glEnable(this.glCap); + } else { + GL11.glDisable(this.glCap); + } } } } diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/ClientArrayState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/ClientArrayState.java new file mode 100644 index 000000000..030926d5e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/ClientArrayState.java @@ -0,0 +1,93 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import java.nio.Buffer; +import org.lwjgl.opengl.GL11; + +/** + * Tracks OpenGL client array state (glVertexPointer, glColorPointer, etc.) + * This state is NOT part of the server attribute stack (push/pop attrib), + * but is needed for display list compilation to capture glDrawArrays calls. + * + * When a display list is compiled, glDrawArrays reads from the current + * vertex/color arrays at compile time (not playback time). We need to track + * this state globally so ImmediateModeRecorder can access it. + */ +public class ClientArrayState { + // Vertex array state + private boolean vertexArrayEnabled = false; + private Buffer vertexPointer = null; + private int vertexPointerSize = 4; + private int vertexPointerType = GL11.GL_FLOAT; + private int vertexPointerStride = 0; + + // Color array state + private boolean colorArrayEnabled = false; + private Buffer colorPointer = null; + private int colorPointerSize = 4; + private int colorPointerType = GL11.GL_FLOAT; + private int colorPointerStride = 0; + + // Vertex array methods + public void setVertexPointer(int size, int type, int stride, Buffer pointer) { + this.vertexPointerSize = size; + this.vertexPointerType = type; + this.vertexPointerStride = stride; + this.vertexPointer = pointer; + } + + public boolean isVertexArrayEnabled() { + return vertexArrayEnabled; + } + + public void setVertexArrayEnabled(boolean enabled) { + this.vertexArrayEnabled = enabled; + } + + public Buffer getVertexPointer() { + return vertexPointer; + } + + public int getVertexPointerSize() { + return vertexPointerSize; + } + + public int getVertexPointerType() { + return vertexPointerType; + } + + public int getVertexPointerStride() { + return vertexPointerStride; + } + + // Color array methods + public void setColorPointer(int size, int type, int stride, Buffer pointer) { + this.colorPointerSize = size; + this.colorPointerType = type; + this.colorPointerStride = stride; + this.colorPointer = pointer; + } + + public boolean isColorArrayEnabled() { + return colorArrayEnabled; + } + + public void setColorArrayEnabled(boolean enabled) { + this.colorArrayEnabled = enabled; + } + + public Buffer getColorPointer() { + return colorPointer; + } + + public int getColorPointerSize() { + return colorPointerSize; + } + + public int getColorPointerType() { + return colorPointerType; + } + + public int getColorPointerStride() { + return colorPointerStride; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/ColorMask.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/ColorMask.java index 8fff5a32c..3c09cecef 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/states/ColorMask.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/ColorMask.java @@ -6,6 +6,13 @@ public class ColorMask implements ISettableState { public boolean blue = true; public boolean alpha = true; + public void setAll(boolean red, boolean green, boolean blue, boolean alpha) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + @Override public ColorMask set(ColorMask state) { this.red = state.red; diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/LineState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/LineState.java new file mode 100644 index 000000000..661dcefbb --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/LineState.java @@ -0,0 +1,37 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import lombok.Getter; +import lombok.Setter; + +/** + * Tracks GL line state: width, stipple factor, and stipple pattern. + * Corresponds to GL_LINE_BIT attributes. + */ +@Getter @Setter +public class LineState implements ISettableState { + private float width = 1.0f; + private int stippleFactor = 1; + private short stipplePattern = (short) 0xFFFF; + + @Override + public LineState set(LineState state) { + this.width = state.width; + this.stippleFactor = state.stippleFactor; + this.stipplePattern = state.stipplePattern; + return this; + } + + @Override + public boolean sameAs(Object state) { + if (this == state) return true; + if (!(state instanceof LineState lineState)) return false; + return Float.compare(width, lineState.width) == 0 + && stippleFactor == lineState.stippleFactor + && stipplePattern == lineState.stipplePattern; + } + + @Override + public LineState copy() { + return new LineState().set(this); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/MatrixMode.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/MatrixMode.java index 04c9b4593..138d0f19f 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/states/MatrixMode.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/MatrixMode.java @@ -15,8 +15,11 @@ public class MatrixMode implements ISettableState { return; } - if(this.mode != mode || GLStateManager.shouldBypassCache()) { - this.mode = mode; + final boolean bypass = GLStateManager.shouldBypassCache(); + if (bypass || this.mode != mode) { + if (!bypass) { + this.mode = mode; + } GL11.glMatrixMode(mode); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/PointState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/PointState.java new file mode 100644 index 000000000..e6a31693f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/PointState.java @@ -0,0 +1,31 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import lombok.Getter; +import lombok.Setter; + +/** + * Tracks GL point state: size. + * Corresponds to GL_POINT_BIT attributes. + */ +@Getter @Setter +public class PointState implements ISettableState { + private float size = 1.0f; + + @Override + public PointState set(PointState state) { + this.size = state.size; + return this; + } + + @Override + public boolean sameAs(Object state) { + if (this == state) return true; + if (!(state instanceof PointState pointState)) return false; + return Float.compare(size, pointState.size) == 0; + } + + @Override + public PointState copy() { + return new PointState().set(this); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/PolygonState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/PolygonState.java new file mode 100644 index 000000000..aea5c56e6 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/PolygonState.java @@ -0,0 +1,47 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import lombok.Getter; +import lombok.Setter; +import org.lwjgl.opengl.GL11; + +/** + * Tracks GL polygon state: polygon mode (front/back) and polygon offset. + * Corresponds to GL_POLYGON_BIT attributes (excluding enable bits which are BooleanStateStack). + */ +@Getter @Setter +public class PolygonState implements ISettableState { + private int frontMode = GL11.GL_FILL; + private int backMode = GL11.GL_FILL; + private float offsetFactor = 0.0f; + private float offsetUnits = 0.0f; + private int cullFaceMode = GL11.GL_BACK; + private int frontFace = GL11.GL_CCW; + + @Override + public PolygonState set(PolygonState state) { + this.frontMode = state.frontMode; + this.backMode = state.backMode; + this.offsetFactor = state.offsetFactor; + this.offsetUnits = state.offsetUnits; + this.cullFaceMode = state.cullFaceMode; + this.frontFace = state.frontFace; + return this; + } + + @Override + public boolean sameAs(Object state) { + if (this == state) return true; + if (!(state instanceof PolygonState polygonState)) return false; + return frontMode == polygonState.frontMode + && backMode == polygonState.backMode + && Float.compare(offsetFactor, polygonState.offsetFactor) == 0 + && Float.compare(offsetUnits, polygonState.offsetUnits) == 0 + && cullFaceMode == polygonState.cullFaceMode + && frontFace == polygonState.frontFace; + } + + @Override + public PolygonState copy() { + return new PolygonState().set(this); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/states/StencilState.java b/src/main/java/com/gtnewhorizons/angelica/glsm/states/StencilState.java new file mode 100644 index 000000000..059fb34fe --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/states/StencilState.java @@ -0,0 +1,114 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import lombok.Getter; +import lombok.Setter; +import org.lwjgl.opengl.GL11; + +/** + * Tracks GL stencil buffer state. + * Corresponds to GL_STENCIL_BUFFER_BIT attributes. + * Supports separate front/back state (GL 2.0 glStencilFuncSeparate, glStencilOpSeparate, glStencilMaskSeparate). + */ +@Getter @Setter +public class StencilState implements ISettableState { + // Front face state + private int funcFront = GL11.GL_ALWAYS; + private int refFront = 0; + private int valueMaskFront = 0xFFFFFFFF; + private int failOpFront = GL11.GL_KEEP; + private int zFailOpFront = GL11.GL_KEEP; + private int zPassOpFront = GL11.GL_KEEP; + private int writeMaskFront = 0xFFFFFFFF; + + // Back face state (GL 2.0) + private int funcBack = GL11.GL_ALWAYS; + private int refBack = 0; + private int valueMaskBack = 0xFFFFFFFF; + private int failOpBack = GL11.GL_KEEP; + private int zFailOpBack = GL11.GL_KEEP; + private int zPassOpBack = GL11.GL_KEEP; + private int writeMaskBack = 0xFFFFFFFF; + + // Clear value (shared) + private int clearValue = 0; + + @Override + public StencilState set(StencilState state) { + this.funcFront = state.funcFront; + this.refFront = state.refFront; + this.valueMaskFront = state.valueMaskFront; + this.failOpFront = state.failOpFront; + this.zFailOpFront = state.zFailOpFront; + this.zPassOpFront = state.zPassOpFront; + this.writeMaskFront = state.writeMaskFront; + + this.funcBack = state.funcBack; + this.refBack = state.refBack; + this.valueMaskBack = state.valueMaskBack; + this.failOpBack = state.failOpBack; + this.zFailOpBack = state.zFailOpBack; + this.zPassOpBack = state.zPassOpBack; + this.writeMaskBack = state.writeMaskBack; + + this.clearValue = state.clearValue; + return this; + } + + /** + * Set both front and back state at once (non-separate calls). + */ + public void setFunc(int func, int ref, int mask) { + this.funcFront = func; + this.refFront = ref; + this.valueMaskFront = mask; + this.funcBack = func; + this.refBack = ref; + this.valueMaskBack = mask; + } + + /** + * Set both front and back operations at once (non-separate calls). + */ + public void setOp(int fail, int zFail, int zPass) { + this.failOpFront = fail; + this.zFailOpFront = zFail; + this.zPassOpFront = zPass; + this.failOpBack = fail; + this.zFailOpBack = zFail; + this.zPassOpBack = zPass; + } + + /** + * Set both front and back write mask at once (non-separate calls). + */ + public void setWriteMask(int mask) { + this.writeMaskFront = mask; + this.writeMaskBack = mask; + } + + @Override + public boolean sameAs(Object state) { + if (this == state) return true; + if (!(state instanceof StencilState stencilState)) return false; + return funcFront == stencilState.funcFront + && refFront == stencilState.refFront + && valueMaskFront == stencilState.valueMaskFront + && failOpFront == stencilState.failOpFront + && zFailOpFront == stencilState.zFailOpFront + && zPassOpFront == stencilState.zPassOpFront + && writeMaskFront == stencilState.writeMaskFront + && funcBack == stencilState.funcBack + && refBack == stencilState.refBack + && valueMaskBack == stencilState.valueMaskBack + && failOpBack == stencilState.failOpBack + && zFailOpBack == stencilState.zFailOpBack + && zPassOpBack == stencilState.zPassOpBack + && writeMaskBack == stencilState.writeMaskBack + && clearValue == stencilState.clearValue; + } + + @Override + public StencilState copy() { + return new StencilState().set(this); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfoCache.java b/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfoCache.java index 5d24d09e5..51b4a079c 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfoCache.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfoCache.java @@ -21,7 +21,7 @@ public class TextureInfoCache { } public TextureInfo getInfo(int id) { - if(id < 0 || !GLStateManager.isMainThread()) return null; + if(id < 0 || !GLStateManager.isCachingEnabled()) return null; return cache.computeIfAbsent(id, TextureInfo::new); } @@ -45,7 +45,7 @@ public class TextureInfoCache { } public void onDeleteTexture(int id) { - if(id >= 0 && GLStateManager.isMainThread()) cache.remove(id); + if(id >= 0 && GLStateManager.isCachingEnabled()) cache.remove(id); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTracker.java b/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTracker.java index 64414ce55..40fe20f98 100644 --- a/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTracker.java +++ b/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTracker.java @@ -1,6 +1,5 @@ package com.gtnewhorizons.angelica.glsm.texture; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.RenderSystem; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -49,7 +48,7 @@ public class TextureTracker { if (bindTextureListener != null) { bindTextureListener.run(); } - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { final WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); if (pipeline != null) { pipeline.onBindTexture(id); diff --git a/src/main/java/com/gtnewhorizons/angelica/loading/shared/transformers/AngelicaRedirector.java b/src/main/java/com/gtnewhorizons/angelica/loading/shared/transformers/AngelicaRedirector.java index a6b547858..2f71bf689 100644 --- a/src/main/java/com/gtnewhorizons/angelica/loading/shared/transformers/AngelicaRedirector.java +++ b/src/main/java/com/gtnewhorizons/angelica/loading/shared/transformers/AngelicaRedirector.java @@ -41,6 +41,7 @@ public final class AngelicaRedirector { private static final String Drawable = "org/lwjgl/opengl/Drawable"; private static final String GLStateManager = "com/gtnewhorizons/angelica/glsm/GLStateManager"; private static final String GL11 = "org/lwjgl/opengl/GL11"; + private static final String GL12 = "org/lwjgl/opengl/GL12"; private static final String GL13 = "org/lwjgl/opengl/GL13"; private static final String GL14 = "org/lwjgl/opengl/GL14"; private static final String GL20 = "org/lwjgl/opengl/GL20"; @@ -55,7 +56,7 @@ public final class AngelicaRedirector { "initializeTextures", "func_77474_a" ); - private static final ClassConstantPoolParser cstPoolParser = new ClassConstantPoolParser(GL11, GL13, GL14, GL20, OpenGlHelper, EXTBlendFunc, ARBMultiTexture, Project); + private static final ClassConstantPoolParser cstPoolParser = new ClassConstantPoolParser(GL11, GL12, GL13, GL14, GL20, OpenGlHelper, EXTBlendFunc, ARBMultiTexture, Project); private static final Map> methodRedirects = new HashMap<>(); private static final Map glCapRedirects = new HashMap<>(); @@ -75,9 +76,12 @@ public final class AngelicaRedirector { .add("glBindTexture") .add("glBlendFunc") .add("glCallList") + .add("glCallLists") .add("glClear") .add("glClearColor") .add("glClearDepth") + .add("glClearStencil") + .add("glClipPlane") .add("glColor3b") .add("glColor3d") .add("glColor3f") @@ -88,6 +92,7 @@ public final class AngelicaRedirector { .add("glColor4ub") .add("glColorMask") .add("glColorMaterial") + .add("glDeleteLists") .add("glDeleteTextures") .add("glDepthFunc") .add("glDepthMask") @@ -96,6 +101,7 @@ public final class AngelicaRedirector { .add("glDrawBuffer") .add("glDrawElements") .add("glEdgeFlag") + .add("glEnd") .add("glEndList") .add("glFog") .add("glFogf") @@ -119,6 +125,9 @@ public final class AngelicaRedirector { .add("glLightModel") .add("glLightModelf") .add("glLightModeli") + .add("glLineStipple") + .add("glLineWidth") + .add("glListBase") .add("glLoadIdentity") .add("glLoadMatrix") .add("glLogicOp") @@ -156,16 +165,59 @@ public final class AngelicaRedirector { .add("glTexCoord3f") .add("glTexCoord4d") .add("glTexCoord4f") + .add("glTexImage1D") .add("glTexImage2D") + .add("glTexImage3D") + .add("glTexSubImage1D") + .add("glTexSubImage2D") + .add("glCopyTexImage1D") + .add("glCopyTexImage2D") + .add("glCopyTexSubImage1D") + .add("glCopyTexSubImage2D") + .add("glPixelStoref") + .add("glPixelStorei") .add("glTexParameter") .add("glTexParameterf") .add("glTexParameteri") .add("glTexParameteri") + .add("glCullFace") + .add("glFrontFace") + .add("glHint") + .add("glLineStipple") + .add("glLineWidth") + .add("glPointSize") + .add("glPolygonMode") + .add("glPolygonOffset") + .add("glReadBuffer") + .add("glSampleCoverage") + .add("glScissor") + .add("glStencilFunc") + .add("glStencilFuncSeparate") + .add("glStencilMask") + .add("glStencilMaskSeparate") + .add("glStencilOp") + .add("glStencilOpSeparate") .add("glTranslated") .add("glTranslatef") + .add("glVertex2d") + .add("glVertex2f") + .add("glVertex3d") + .add("glVertex3f") + .add("glVertexPointer") + .add("glColorPointer") + .add("glEnableClientState") + .add("glDisableClientState") .add("glViewport") ); - methodRedirects.put(GL13, RedirectMap.newMap().add("glActiveTexture")); + methodRedirects.put(GL12, RedirectMap.newMap() + .add("glTexImage3D") + .add("glTexSubImage3D") + .add("glCopyTexSubImage3D") + ); + methodRedirects.put(GL13, RedirectMap.newMap() + .add("glActiveTexture") + .add("glSampleCoverage") + ); methodRedirects.put(GL14, RedirectMap.newMap() .add("glBlendFuncSeparate", "tryBlendFuncSeparate") .add("glBlendColor") @@ -173,6 +225,10 @@ public final class AngelicaRedirector { ); methodRedirects.put(GL20, RedirectMap.newMap() .add("glBlendEquationSeparate") + .add("glDrawBuffers") + .add("glStencilFuncSeparate") + .add("glStencilMaskSeparate") + .add("glStencilOpSeparate") .add("glUseProgram") ); methodRedirects.put(OpenGlHelper, RedirectMap.newMap() diff --git a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java index 57bf1489f..aecd85098 100644 --- a/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java +++ b/src/main/java/com/gtnewhorizons/angelica/mixins/Mixins.java @@ -35,19 +35,17 @@ public enum Mixins implements IMixins { , "angelica.optimizations.MixinRendererLivingEntity" , "angelica.MixinFMLClientHandler" , "angelica.bugfixes.MixinRenderGlobal_DestroyBlock" + , "angelica.glsm.MixinSplashProgressCaching" ) ), - ANGELICA_VBO( + + ANGELICA_VBO_CLOUDS( new MixinBuilder() - .setApplyIf(() -> AngelicaConfig.enableVBO) + .setApplyIf(() -> AngelicaConfig.enableVBOClouds) .setPhase(Phase.EARLY) - .addClientMixins( - "angelica.vbo.MixinGLAllocation" - , "angelica.vbo.MixinModelRenderer" - , "angelica.vbo.MixinRenderGlobal" - , "angelica.vbo.MixinWavefrontObject" - ) + .addClientMixins("angelica.vbo.MixinRenderGlobal") ), + ANGELICA_FONT_RENDERER(new MixinBuilder() .setPhase(Phase.EARLY) .setApplyIf(() -> AngelicaConfig.enableFontRenderer) @@ -101,6 +99,10 @@ public enum Mixins implements IMixins { .addClientMixins("angelica.itemrenderer.MixinItemRenderer") .setApplyIf(() -> AngelicaConfig.optimizeInWorldItemRendering)), + ANGELICA_OPTIMIZE_GLALLOCATION(new MixinBuilder("Replace HashMap with fastutil Int2IntMap in GLAllocation") + .setPhase(Phase.EARLY) + .addClientMixins("angelica.optimizations.MixinGLAllocation")), + // Not compatible with the lwjgl debug callbacks, so disable if that's enabled ARCHAIC_SPLASH(new MixinBuilder() .setPhase(Phase.EARLY) @@ -438,16 +440,25 @@ public enum Mixins implements IMixins { "base.MixinMinecraft" )) ), - MCPATCHER_FORGE_RENDERPASS(new MixinBuilder() + MCPATCHER_FORGE_RENDERPASS_BASE(new MixinBuilder() .setPhase(Phase.EARLY) .setApplyIf(() -> NotFineConfig.renderPass) - .addClientMixins(addPrefix("mcpatcherforge.", - "renderpass.MixinEntityRenderer", - "renderpass.MixinRenderBlocks", - "renderpass.MixinRenderGlobal", - "renderpass.MixinWorldRenderer" + .addClientMixins(addPrefix("mcpatcherforge.renderpass.", + "MixinEntityRenderer", + "MixinRenderBlocks", + "MixinWorldRenderer" )) ), + MCPATCHER_FORGE_RENDERPASS_DISPLAYLIST(new MixinBuilder("RenderPass display list allocation increase") + .setPhase(Phase.EARLY) + .setApplyIf(() -> NotFineConfig.renderPass && !AngelicaConfig.enableSodium) + .addClientMixins("mcpatcherforge.renderpass.MixinRenderGlobal_DisplayLists") + ), + MCPATCHER_FORGE_RENDERPASS_FEATURES(new MixinBuilder("RenderPass rendering features") + .setPhase(Phase.EARLY) + .setApplyIf(() -> NotFineConfig.renderPass && !AngelicaConfig.enableSodium) + .addClientMixins("mcpatcherforge.renderpass.MixinRenderGlobal_Features") + ), MCPATCHER_FORGE_CUSTOM_COLORS(new MixinBuilder() .setPhase(Phase.EARLY) .setApplyIf(() -> AngelicaConfig.enableMCPatcherForgeFeatures && MCPatcherForgeConfig.CustomColors.enabled) diff --git a/src/main/java/com/gtnewhorizons/angelica/utils/BufferUtil.java b/src/main/java/com/gtnewhorizons/angelica/utils/BufferUtil.java new file mode 100644 index 000000000..3ff60869d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/utils/BufferUtil.java @@ -0,0 +1,46 @@ +package com.gtnewhorizons.angelica.utils; + +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; + +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress; +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocFloat; +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memCopy; + +/** + * Utility class for buffer operations in display list commands. + */ +public final class BufferUtil { + private BufferUtil() {} // Prevent instantiation + + /** + * Creates a copy of a FloatBuffer. + * The source buffer's position is preserved. + * + * @param src The source buffer to copy + * @return A new FloatBuffer containing a copy of the data + */ + public static FloatBuffer copyBuffer(FloatBuffer src) { + final FloatBuffer dst = BufferUtils.createFloatBuffer(src.remaining()); + final int pos = src.position(); + dst.put(src); + dst.flip(); + src.position(pos); + return dst; + } + + /** + * Creates a copy of a direct FloatBuffer using native memory. + * Caller is responsible for freeing the returned buffer with memFree(). + * + * @param src Direct FloatBuffer to copy (must be direct) + * @return New direct FloatBuffer with copied data + */ + public static FloatBuffer copyDirectBuffer(FloatBuffer src) { + final int count = src.remaining(); + final FloatBuffer dst = memAllocFloat(count); + memCopy(memAddress(src), memAddress(dst), (long) count * Float.BYTES); + return dst; + } +} diff --git a/src/main/java/jss/notfine/gui/NotFineGameOptionPages.java b/src/main/java/jss/notfine/gui/NotFineGameOptionPages.java index 9349ebb22..e09e83db6 100644 --- a/src/main/java/jss/notfine/gui/NotFineGameOptionPages.java +++ b/src/main/java/jss/notfine/gui/NotFineGameOptionPages.java @@ -1,7 +1,7 @@ package jss.notfine.gui; import com.google.common.collect.ImmutableList; -import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.dynamiclights.DynamicLights; import com.gtnewhorizons.angelica.glsm.GLStateManager; import jss.notfine.config.NotFineConfig; import jss.notfine.core.Settings; @@ -148,7 +148,7 @@ public class NotFineGameOptionPages { .build()) .build()); - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { final OptionImpl maxShadowDistanceSlider = OptionImpl.createBuilder(int.class, vanillaOpts) .setName(I18n.format("options.iris.shadowDistance")) .setTooltip(I18n.format("options.iris.shadowDistance.sodium_tooltip")) @@ -190,8 +190,8 @@ public class NotFineGameOptionPages { .add(Settings.MODE_WATER.option) .add(Settings.DOWNFALL_DISTANCE.option) .add(Settings.MODE_VIGNETTE.option) - .add(Settings.DYNAMIC_LIGHTS.option, AngelicaConfig.enableDynamicLights) - .add(Settings.DYNAMIC_LIGHTS_SHADER_FORCE.option, AngelicaConfig.enableDynamicLights) + .add(Settings.DYNAMIC_LIGHTS.option, DynamicLights.configEnabled) + .add(Settings.DYNAMIC_LIGHTS_SHADER_FORCE.option, DynamicLights.configEnabled) .add(Settings.MODE_SHADOWS.option) .add(Settings.VOID_FOG.option) .add(Settings.MODE_DROPPED_ITEMS.option) diff --git a/src/main/java/jss/notfine/render/RenderStars.java b/src/main/java/jss/notfine/render/RenderStars.java index 82a8042fe..476b12cf7 100644 --- a/src/main/java/jss/notfine/render/RenderStars.java +++ b/src/main/java/jss/notfine/render/RenderStars.java @@ -1,10 +1,6 @@ package jss.notfine.render; -import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager; -import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VBOManager; -import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import jss.notfine.core.Settings; import jss.util.RandomXoshiro256StarStar; import net.minecraft.client.renderer.RenderGlobal; @@ -16,18 +12,9 @@ public class RenderStars { //private static final ResourceLocation locationStarsPng = new ResourceLocation("textures/colormap/stars.png"); public static void reloadStarRenderList(RenderGlobal render) { - - if(AngelicaConfig.enableVBO) { - TessellatorManager.startCapturing(); - renderStars(); - VBOManager.registerVBO(render.starGLCallList, TessellatorManager.stopCapturingToVBO(DefaultVertexFormat.POSITION)); - } else { - GL11.glPushMatrix(); - GL11.glNewList(render.starGLCallList, GL11.GL_COMPILE); - renderStars(); - GL11.glEndList(); - GL11.glPopMatrix(); - } + GL11.glNewList(render.starGLCallList, GL11.GL_COMPILE); + renderStars(); + GL11.glEndList(); } public static void renderStars() { diff --git a/src/main/java/me/flashyreese/mods/reeses_sodium_options/client/gui/ReeseSodiumVideoOptionsScreen.java b/src/main/java/me/flashyreese/mods/reeses_sodium_options/client/gui/ReeseSodiumVideoOptionsScreen.java index f9c21e657..98ea90c21 100644 --- a/src/main/java/me/flashyreese/mods/reeses_sodium_options/client/gui/ReeseSodiumVideoOptionsScreen.java +++ b/src/main/java/me/flashyreese/mods/reeses_sodium_options/client/gui/ReeseSodiumVideoOptionsScreen.java @@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.client.gui.FontConfigScreen; import com.gtnewhorizons.angelica.compat.mojang.Element; import com.gtnewhorizons.angelica.config.AngelicaConfig; import jss.notfine.gui.GuiCustomMenu; +import net.coderbot.iris.Iris; import me.flashyreese.mods.reeses_sodium_options.client.gui.frame.AbstractFrame; import me.flashyreese.mods.reeses_sodium_options.client.gui.frame.BasicFrame; import me.flashyreese.mods.reeses_sodium_options.client.gui.frame.components.SearchTextFieldComponent; @@ -104,7 +105,7 @@ public class ReeseSodiumVideoOptionsScreen extends SodiumOptionsGUI { int cumulativeXShift = 0; - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { //int size = this.client.textRenderer.getWidth(new TranslatableText(IrisApi.getInstance().getMainScreenLanguageKey())); final String irisText = I18n.format(IrisApi.getInstance().getMainScreenLanguageKey()); final int size = this.mc.fontRenderer.getStringWidth(irisText); diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptionPages.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptionPages.java index ca763f07b..d09aca580 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptionPages.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptionPages.java @@ -2,6 +2,7 @@ package me.jellysquid.mods.sodium.client.gui; import com.google.common.collect.ImmutableList; import com.gtnewhorizons.angelica.config.AngelicaConfig; +import com.gtnewhorizons.angelica.dynamiclights.DynamicLights; import com.gtnewhorizons.angelica.glsm.GLStateManager; import jss.notfine.core.Settings; import jss.notfine.core.SettingsManager; @@ -52,7 +53,7 @@ public class SodiumGameOptionPages { .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) .build()); - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { final OptionImpl maxShadowDistanceSlider = OptionImpl.createBuilder(int.class, vanillaOpts) .setName(I18n.format("options.iris.shadowDistance")) .setTooltip(I18n.format("options.iris.shadowDistance.sodium_tooltip")) @@ -264,8 +265,8 @@ public class SodiumGameOptionPages { )*/ .add(Settings.MODE_SHADOWS.option) .add(Settings.MODE_VIGNETTE.option) - .add(Settings.DYNAMIC_LIGHTS.option, AngelicaConfig.enableDynamicLights) - .add(Settings.DYNAMIC_LIGHTS_SHADER_FORCE.option, AngelicaConfig.enableDynamicLights) + .add(Settings.DYNAMIC_LIGHTS.option, DynamicLights.configEnabled) + .add(Settings.DYNAMIC_LIGHTS_SHADER_FORCE.option, DynamicLights.configEnabled) .build()); diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java index 033603653..42f5b887f 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java @@ -4,7 +4,6 @@ import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import me.jellysquid.mods.sodium.client.SodiumClientMod; import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality; import net.coderbot.iris.Iris; @@ -105,7 +104,7 @@ public class SodiumGameOptions { } Files.write(this.configPath, GSON.toJson(this).getBytes(StandardCharsets.UTF_8)); - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { try { if (Iris.getIrisConfig() != null) { Iris.getIrisConfig().save(); diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumOptionsGUI.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumOptionsGUI.java index 1fc2f4660..a3c235533 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumOptionsGUI.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumOptionsGUI.java @@ -4,8 +4,8 @@ import com.google.common.collect.ImmutableList; import com.gtnewhorizons.angelica.client.gui.ScrollableGuiScreen; import com.gtnewhorizons.angelica.compat.mojang.Drawable; import com.gtnewhorizons.angelica.compat.mojang.Element; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import jss.notfine.gui.GuiCustomMenu; +import net.coderbot.iris.Iris; import me.jellysquid.mods.sodium.client.gui.options.Option; import me.jellysquid.mods.sodium.client.gui.options.OptionFlag; import me.jellysquid.mods.sodium.client.gui.options.OptionGroup; @@ -61,14 +61,14 @@ public class SodiumOptionsGUI extends ScrollableGuiScreen { this.pages.add(SodiumGameOptionPages.advanced()); this.pages.add(SodiumGameOptionPages.performance()); - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { shaderPacks = new OptionPage(I18n.format("options.iris.shaderPackSelection"), ImmutableList.of()); this.pages.add(shaderPacks); } } public void setPage(OptionPage page) { - if (AngelicaConfig.enableIris && page == shaderPacks) { + if (Iris.enabled && page == shaderPacks) { mc.displayGuiScreen(new ShaderPackScreen(this)); return; } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/flat/FlatLightPipeline.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/flat/FlatLightPipeline.java index 57d9b2787..2fbbdbda7 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/model/light/flat/FlatLightPipeline.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/flat/FlatLightPipeline.java @@ -4,8 +4,8 @@ import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFlags; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import java.util.Arrays; +import net.coderbot.iris.Iris; import me.jellysquid.mods.sodium.client.model.light.LightPipeline; import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; import me.jellysquid.mods.sodium.client.model.light.data.QuadLightData; @@ -45,7 +45,7 @@ public class FlatLightPipeline implements LightPipeline { } Arrays.fill(out.lm, lightmap); - final float brightness = (AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading()) + final float brightness = (Iris.enabled && BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading()) ? 1.0F : this.lightCache.getWorld().getBrightness(face, shade); Arrays.fill(out.br, brightness); } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java index c506db79b..c16b4bbaa 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/model/light/smooth/SmoothLightPipeline.java @@ -5,8 +5,8 @@ import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFlags; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import me.jellysquid.mods.sodium.client.model.light.LightPipeline; +import net.coderbot.iris.Iris; import me.jellysquid.mods.sodium.client.model.light.data.LightDataAccess; import me.jellysquid.mods.sodium.client.model.light.data.QuadLightData; import net.coderbot.iris.block_rendering.BlockRenderingSettings; @@ -226,7 +226,7 @@ public class SmoothLightPipeline implements LightPipeline { } private void applySidedBrightness(QuadLightData out, ModelQuadFacing face, boolean shade) { - if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading()) { + if(Iris.enabled && BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading()) { return; } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/GameRendererContext.java b/src/main/java/me/jellysquid/mods/sodium/client/render/GameRendererContext.java index f280b2218..44244961a 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/GameRendererContext.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/GameRendererContext.java @@ -2,8 +2,8 @@ package me.jellysquid.mods.sodium.client.render; import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack; import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.rendering.RenderingState; +import net.coderbot.iris.Iris; import java.nio.FloatBuffer; import net.coderbot.iris.shadows.ShadowRenderingState; import org.joml.Matrix4f; @@ -18,7 +18,7 @@ public class GameRendererContext { */ public static FloatBuffer getModelViewProjectionMatrix(MatrixStack.Entry matrices, MemoryStack stack) { final FloatBuffer bufModelViewProjection = stack.mallocFloat(16); - final Matrix4f projectionMatrix = (AngelicaConfig.enableIris && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) ? ShadowRenderingState.getShadowOrthoMatrix() : RenderingState.INSTANCE.getProjectionMatrix(); + final Matrix4f projectionMatrix = (Iris.enabled && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) ? ShadowRenderingState.getShadowOrthoMatrix() : RenderingState.INSTANCE.getProjectionMatrix(); final Matrix4f matrix = new Matrix4f(projectionMatrix); matrix.mul(matrices.getModel()); diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java index e74483a61..33d305fed 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java @@ -3,8 +3,8 @@ package me.jellysquid.mods.sodium.client.render; import com.gtnewhorizons.angelica.compat.mojang.Camera; import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.dynamiclights.DynamicLights; +import net.coderbot.iris.Iris; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.rendering.RenderingState; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; @@ -171,7 +171,7 @@ public class SodiumWorldRenderer { */ public void scheduleTerrainUpdate() { if (this.chunkRenderManager != null) { - if(AngelicaConfig.enableIris) iris$ensureStateSwapped(); + if(Iris.enabled) iris$ensureStateSwapped(); this.chunkRenderManager.markDirty(); } } @@ -223,7 +223,7 @@ public class SodiumWorldRenderer { boolean dirty = pos.x != this.lastCameraX || pos.y != this.lastCameraY || pos.z != this.lastCameraZ || pitch != this.lastCameraPitch || yaw != this.lastCameraYaw || fogDistance != this.lastFogDistance || fov != this.lastFov; - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { iris$ensureStateSwapped(); if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { dirty = true; @@ -258,7 +258,7 @@ public class SodiumWorldRenderer { profiler.endSection(); SodiumGameOptions.EntityRenderDistance.setRenderDistanceMult(MathHelper.clamp_double((double) this.client.gameSettings.renderDistanceChunks / 8.0D, 1.0D, 2.5D) * (double) 1.0F * (SettingsManager.entityRenderScaleFactor)); - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { ShadowRenderer.visibleTileEntities.addAll(this.chunkRenderManager.getVisibleTileEntities()); ShadowRenderer.globalTileEntities.addAll(this.globalTileEntities); @@ -279,7 +279,7 @@ public class SodiumWorldRenderer { // or other factors as having clouds disabled. GLStateManager.enableCull(); - if(AngelicaConfig.enableIris) iris$ensureStateSwapped(); + if(Iris.enabled) iris$ensureStateSwapped(); // startDrawing/endDrawing are handled by 1.7 already // pass.startDrawing(); @@ -318,7 +318,7 @@ public class SodiumWorldRenderer { final ChunkVertexType vertexFormat; - if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat()) { + if(Iris.enabled && BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat()) { vertexFormat = IrisModelVertexFormats.MODEL_VERTEX_XHFP; } else if (opts.advanced.useCompactVertexFormat) { vertexFormat = DefaultModelVertexFormats.MODEL_VERTEX_HFP; @@ -358,7 +358,7 @@ public class SodiumWorldRenderer { return; try { - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { final Block block = tileEntity.getWorldObj().getBlock(tileEntity.xCoord, tileEntity.yCoord, tileEntity.zCoord); CapturedRenderingState.INSTANCE.setCurrentBlockEntity(Block.getIdFromBlock(block)); GL20.glVertexAttrib2s(ProgramCreator.MC_ENTITY, (short)-1, (short)-1); @@ -376,7 +376,7 @@ public class SodiumWorldRenderer { } } finally { - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { CapturedRenderingState.INSTANCE.setCurrentBlockEntity(-1); GbufferPrograms.endBlockEntities(); } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java index f083c4fca..65ccab24d 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java @@ -3,8 +3,8 @@ package me.jellysquid.mods.sodium.client.render.chunk; import com.gtnewhorizons.angelica.compat.mojang.Camera; import com.gtnewhorizons.angelica.compat.mojang.ChunkPos; import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue; +import net.coderbot.iris.Iris; import it.unimi.dsi.fastutil.ints.IntIterator; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; @@ -152,7 +152,7 @@ public class ChunkRenderManager implements ChunkSt this.useBlockFaceCulling = SodiumClientMod.options().advanced.useBlockFaceCulling; - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { this.chunkRenderListsSwap = new ChunkRenderList[BlockRenderPass.COUNT]; this.tickableChunksSwap = new ObjectArrayList<>(); this.visibleTileEntitiesSwap = new ObjectArrayList<>(); @@ -252,7 +252,7 @@ public class ChunkRenderManager implements ChunkSt private void addChunk(ChunkRenderContainer render) { boolean canRebuild = render.canRebuild(); - if (AngelicaConfig.enableIris && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { + if (Iris.enabled && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { canRebuild = false; } @@ -307,7 +307,7 @@ public class ChunkRenderManager implements ChunkSt } private int computeVisibleFaces(ChunkRenderContainer render) { - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { // TODO: Enable chunk face culling during the shadow pass if (ShadowRenderingState.areShadowsCurrentlyBeingRendered()) { return(ChunkFaceFlags.ALL); @@ -366,9 +366,9 @@ public class ChunkRenderManager implements ChunkSt } private void reset() { - if(!AngelicaConfig.enableIris || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.rebuildQueue.clear(); - if(!AngelicaConfig.enableIris || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.importantRebuildQueue.clear(); - if(!AngelicaConfig.enableIris || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.sortQueue.clear(); + if(!Iris.enabled || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.rebuildQueue.clear(); + if(!Iris.enabled || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.importantRebuildQueue.clear(); + if(!Iris.enabled || !ShadowRenderingState.areShadowsCurrentlyBeingRendered()) this.sortQueue.clear(); this.visibleTileEntities.clear(); @@ -510,7 +510,7 @@ public class ChunkRenderManager implements ChunkSt final RenderDevice device = RenderDevice.INSTANCE; final CommandList commandList = device.createCommandList(); - if(AngelicaConfig.enableIris) this.backend.iris$begin(matrixStack, pass); + if(Iris.enabled) this.backend.iris$begin(matrixStack, pass); else this.backend.begin(matrixStack); // Ensure multidraw regions are ordered appropriately @@ -534,7 +534,7 @@ public class ChunkRenderManager implements ChunkSt } public void updateChunks() { - if (AngelicaConfig.enableIris && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) return; + if (Iris.enabled && ShadowRenderingState.areShadowsCurrentlyBeingRendered()) return; this.builder.cleanupSectionCache(); Deque>> futures = new ArrayDeque<>(); diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawChunkRenderBackend.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawChunkRenderBackend.java index 5d3cdd04d..98ea6780b 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawChunkRenderBackend.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/backends/multidraw/MultidrawChunkRenderBackend.java @@ -1,8 +1,8 @@ package me.jellysquid.mods.sodium.client.render.chunk.backends.multidraw; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue; +import net.coderbot.iris.Iris; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -196,7 +196,7 @@ public class MultidrawChunkRenderBackend extends ChunkRenderShaderBackend vertexFormat) { - if(AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat()) { + if(Iris.enabled && BlockRenderingSettings.INSTANCE.shouldUseExtendedVertexFormat()) { return new GlVertexAttributeBinding[] { new GlVertexAttributeBinding(ChunkShaderBindingPoints.POSITION, vertexFormat.getAttribute(ChunkMeshAttribute.POSITION)), new GlVertexAttributeBinding(ChunkShaderBindingPoints.COLOR, vertexFormat.getAttribute(ChunkMeshAttribute.COLOR)), diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java index c82395938..52885936d 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildBuffers.java @@ -1,8 +1,8 @@ package me.jellysquid.mods.sodium.client.render.chunk.compile; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import it.unimi.dsi.fastutil.objects.Object2IntMap; +import net.coderbot.iris.Iris; import java.nio.ByteBuffer; import java.util.Map; import lombok.Getter; @@ -59,7 +59,7 @@ public class ChunkBuildBuffers implements ChunkBuildBuffersExt { } } - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { final Object2IntMap blockMatches = BlockRenderingSettings.INSTANCE.getBlockMatches(); if (blockMatches != null) { @@ -76,7 +76,7 @@ public class ChunkBuildBuffers implements ChunkBuildBuffersExt { for (ModelQuadFacing facing : ModelQuadFacing.VALUES) { final ModelVertexSink sink = this.vertexType.createBufferWriter(this.buffersByLayer[i][facing.ordinal()], SodiumClientMod.isDirectMemoryAccessEnabled()); - if (AngelicaConfig.enableIris && sink instanceof ContextAwareVertexWriter) { + if (Iris.enabled && sink instanceof ContextAwareVertexWriter) { ((ContextAwareVertexWriter) sink).iris$setContextHolder(iris$contextHolder); } writers[facing.ordinal()] = new ChunkModelVertexTransformer(sink, this.offset); @@ -156,17 +156,17 @@ public class ChunkBuildBuffers implements ChunkBuildBuffersExt { // Iris Compat public void iris$setLocalPos(int localPosX, int localPosY, int localPosZ) { - if(!AngelicaConfig.enableIris) return; + if(!Iris.enabled) return; this.iris$contextHolder.setLocalPos(localPosX, localPosY, localPosZ); } public void iris$setMaterialId(Block block, short renderType) { - if(!AngelicaConfig.enableIris) return; + if(!Iris.enabled) return; this.iris$contextHolder.set(block, renderType); } public void iris$resetBlockContext() { - if(!AngelicaConfig.enableIris) return; + if(!Iris.enabled) return; this.iris$contextHolder.reset(); } } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkRenderShaderBackend.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkRenderShaderBackend.java index ff25a4134..5df87f8f6 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkRenderShaderBackend.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/shader/ChunkRenderShaderBackend.java @@ -1,7 +1,6 @@ package me.jellysquid.mods.sodium.client.render.chunk.shader; import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.glsm.GLStateManager; import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; import me.jellysquid.mods.sodium.client.gl.compat.FogHelper; @@ -52,7 +51,7 @@ public abstract class ChunkRenderShaderBackend imp private ChunkProgram override; public ChunkRenderShaderBackend(ChunkVertexType vertexType) { - if (AngelicaConfig.enableIris) { + if (Iris.enabled) { irisChunkProgramOverrides = new IrisChunkProgramOverrides(); } @@ -61,7 +60,7 @@ public abstract class ChunkRenderShaderBackend imp } public GlShader loadShaderRedirect(RenderDevice device, ShaderType type, ResourceLocation name, List constants) { - if (AngelicaConfig.enableIris && this.vertexType == IrisModelVertexFormats.MODEL_VERTEX_XHFP) { + if (Iris.enabled && this.vertexType == IrisModelVertexFormats.MODEL_VERTEX_XHFP) { String shader = getShaderSource(ShaderLoader.getShaderPath(name, type)); shader = shader.replace("v_LightCoord = a_LightCoord", "v_LightCoord = (iris_LightmapTextureMatrix * vec4(a_LightCoord, 0, 1)).xy"); @@ -78,7 +77,7 @@ public abstract class ChunkRenderShaderBackend imp } private ChunkProgram createShader(RenderDevice device, ChunkFogMode fogMode, GlVertexFormat vertexFormat) { - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { this.device = device; WorldRenderingPipeline worldRenderingPipeline = Iris.getPipelineManager().getPipelineNullable(); SodiumTerrainPipeline sodiumTerrainPipeline = null; @@ -159,7 +158,7 @@ public abstract class ChunkRenderShaderBackend imp @Override public void begin(MatrixStack matrixStack) { this.activeProgram = this.programs.get(FogHelper.getFogMode()); - if (AngelicaConfig.enableIris && override != null) { + if (Iris.enabled && override != null) { this.activeProgram = override; } this.activeProgram.bind(); @@ -170,7 +169,7 @@ public abstract class ChunkRenderShaderBackend imp public void end(MatrixStack matrixStack) { this.activeProgram.unbind(); this.activeProgram = null; - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { ProgramUniforms.clearActiveUniforms(); ProgramSamplers.clearActiveSamplers(); Iris.getPipelineManager().getPipeline().ifPresent(WorldRenderingPipeline::endSodiumTerrainRendering); @@ -179,7 +178,7 @@ public abstract class ChunkRenderShaderBackend imp @Override public void delete() { - if(AngelicaConfig.enableIris) { + if(Iris.enabled) { irisChunkProgramOverrides.deleteShaders(); } for (ChunkProgram shader : this.programs.values()) { diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderRebuildTask.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderRebuildTask.java index 50cf1f0f7..145a089f2 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderRebuildTask.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/tasks/ChunkRenderRebuildTask.java @@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.compat.mojang.ChunkOcclusionDataBuilder; import com.gtnewhorizons.angelica.compat.toremove.RenderLayer; import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.mixins.interfaces.ITexturesCache; +import net.coderbot.iris.Iris; import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry; import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue; import com.gtnewhorizons.angelica.utils.AnimationsRenderUtils; @@ -217,7 +218,7 @@ public class ChunkRenderRebuildTask extends ChunkR pos.set(baseX + relX, baseY + relY, baseZ + relZ); buffers.setRenderOffset(pos.x - renderOffset.getX(), pos.y - renderOffset.getY(), pos.z - renderOffset.getZ()); - if(AngelicaConfig.enableIris) buffers.iris$setLocalPos(relX, relY, relZ); + if(Iris.enabled) buffers.iris$setLocalPos(relX, relY, relZ); if (rendersOffThread(block)) { // Do regular block rendering @@ -226,14 +227,14 @@ public class ChunkRenderRebuildTask extends ChunkR if (ModStatus.isFluidLoggedLoaded) { if (fluid != null && canRenderInPass(fluid.getBlock(), pass)) { ChunkRenderManager.setWorldRenderPass(pass); - if(AngelicaConfig.enableIris) buffers.iris$setMaterialId(fluid.getBlock(), ExtendedDataHelper.FLUID_RENDER_TYPE); + if(Iris.enabled) buffers.iris$setMaterialId(fluid.getBlock(), ExtendedDataHelper.FLUID_RENDER_TYPE); cache.getBlockRenderer().renderFluidLogged(cache.getWorldSlice(), fluid, renderBlocks, pos, buffers.get(pass), seed); } } if (canRenderInPass(block, pass) && !shouldUseSodiumFluidRendering(block)) { ChunkRenderManager.setWorldRenderPass(pass); - if(AngelicaConfig.enableIris) buffers.iris$setMaterialId(block, ExtendedDataHelper.BLOCK_RENDER_TYPE); + if(Iris.enabled) buffers.iris$setMaterialId(block, ExtendedDataHelper.BLOCK_RENDER_TYPE); if (cache.getBlockRenderer().renderModel(cache.getWorldSlice(), renderBlocks, block, meta, pos, buffers.get(pass), true, seed)) { bounds.addBlock(relX, relY, relZ); @@ -251,7 +252,7 @@ public class ChunkRenderRebuildTask extends ChunkR for (BlockRenderPass pass : BlockRenderPass.VALUES) { if (canRenderInPass(block, pass)) { ChunkRenderManager.setWorldRenderPass(pass); - if(AngelicaConfig.enableIris) buffers.iris$setMaterialId(block, ExtendedDataHelper.FLUID_RENDER_TYPE); + if(Iris.enabled) buffers.iris$setMaterialId(block, ExtendedDataHelper.FLUID_RENDER_TYPE); if (cache.getFluidRenderer().render(slice, cache.getWorldSlice(), block, pos, buffers.get(pass))) { bounds.addBlock(relX, relY, relZ); @@ -260,7 +261,7 @@ public class ChunkRenderRebuildTask extends ChunkR } } - if(AngelicaConfig.enableIris) buffers.iris$resetBlockContext(); + if(Iris.enabled) buffers.iris$resetBlockContext(); if (block.hasTileEntity(meta)) { final TileEntity entity = slice.getTileEntity(pos.x, pos.y, pos.z); @@ -355,14 +356,14 @@ public class ChunkRenderRebuildTask extends ChunkR final int meta = slice.getBlockMetadataRelative(relX + 16, relY + 16, relZ + 16); buffers.setRenderOffset(pos.x - renderOffset.getX(), pos.y - renderOffset.getY(), pos.z - renderOffset.getZ()); - if(AngelicaConfig.enableIris) buffers.iris$setLocalPos(relX, relY, relZ); + if(Iris.enabled) buffers.iris$setLocalPos(relX, relY, relZ); // Do regular block rendering for (BlockRenderPass pass : BlockRenderPass.VALUES) { if (canRenderInPass(block, pass) && !shouldUseSodiumFluidRendering(block)) { ChunkRenderManager.setWorldRenderPass(pass); final long seed = MathUtil.hashPos(pos.x, pos.y, pos.z); - if(AngelicaConfig.enableIris) buffers.iris$setMaterialId(block, ExtendedDataHelper.BLOCK_RENDER_TYPE); + if(Iris.enabled) buffers.iris$setMaterialId(block, ExtendedDataHelper.BLOCK_RENDER_TYPE); if (cache.getBlockRenderer().renderModel(slice.getWorld(), rb, block, meta, pos, buffers.get(pass), true, seed)) { bounds.addBlock(relX, relY, relZ); @@ -370,7 +371,7 @@ public class ChunkRenderRebuildTask extends ChunkR } } - if(AngelicaConfig.enableIris) buffers.iris$resetBlockContext(); + if(Iris.enabled) buffers.iris$resetBlockContext(); } if(rb instanceof ITexturesCache) { diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/BlockRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/BlockRenderer.java index 733d763f2..d2926de90 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/BlockRenderer.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/BlockRenderer.java @@ -9,8 +9,8 @@ import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMuta import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadOrientation; import com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import java.util.List; +import net.coderbot.iris.Iris; import java.util.Random; import me.jellysquid.mods.sodium.client.SodiumClientMod; import me.jellysquid.mods.sodium.client.model.light.LightMode; @@ -97,7 +97,7 @@ public class BlockRenderer { boolean rendered = false; - this.useSeparateAo = AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo(); + this.useSeparateAo = Iris.enabled && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo(); final int emitted = block.getLightValue(world, pos.x, pos.y, pos.z); try { diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/FluidRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/FluidRenderer.java index bc4812aac..2538b1bea 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/FluidRenderer.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/pipeline/FluidRenderer.java @@ -14,8 +14,8 @@ import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing; import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFlags; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import java.util.Arrays; +import net.coderbot.iris.Iris; import me.jellysquid.mods.sodium.client.model.light.LightMode; import me.jellysquid.mods.sodium.client.model.light.LightPipeline; import me.jellysquid.mods.sodium.client.model.light.LightPipelineProvider; @@ -89,7 +89,7 @@ public class FluidRenderer { } public boolean render(IBlockAccess world, WorldSlice slice, Block block, BlockPos pos, ChunkModelBuffers buffers) { - this.useSeparateAo = AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo(); + this.useSeparateAo = Iris.enabled && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo(); int posX = pos.x; int posY = pos.y; diff --git a/src/main/java/net/coderbot/iris/Iris.java b/src/main/java/net/coderbot/iris/Iris.java index 89c914602..f84f05685 100644 --- a/src/main/java/net/coderbot/iris/Iris.java +++ b/src/main/java/net/coderbot/iris/Iris.java @@ -71,6 +71,9 @@ public class Iris { public static final IrisLogging logger = new IrisLogging(MODNAME); + // Cached at class load - config must be loaded before Iris. Do not change at runtime. + public static final boolean enabled = AngelicaConfig.enableIris; + private static Path shaderpacksDirectory; private static ShaderpackDirectoryManager shaderpacksDirectoryManager; @@ -103,7 +106,9 @@ public class Iris { public static Iris INSTANCE = new Iris(); private Iris() { - isDevelopmentEnvironment = (boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment"); + // Guard against null blackboard in test environments + final Object deobfEnv = Launch.blackboard != null ? Launch.blackboard.get("fml.deobfuscatedEnvironment") : null; + isDevelopmentEnvironment = deobfEnv != null && (boolean) deobfEnv; } @SubscribeEvent @@ -663,7 +668,7 @@ public class Iris { } public static void setShaderMaterialOverride(Block block, int meta) { - if (!AngelicaConfig.enableIris) + if (!enabled) return; int blockId = getShaderMaterialOverrideId(block, meta); @@ -673,7 +678,7 @@ public class Iris { } public static void resetShaderMaterialOverride() { - if (!AngelicaConfig.enableIris) + if (!enabled) return; if (TessellatorManager.get() instanceof CapturingTessellator tess) tess.setShaderBlockId(-1); diff --git a/src/main/java/net/coderbot/iris/gl/blending/BlendModeStorage.java b/src/main/java/net/coderbot/iris/gl/blending/BlendModeStorage.java index ed2ba1ac1..5d913e0cf 100644 --- a/src/main/java/net/coderbot/iris/gl/blending/BlendModeStorage.java +++ b/src/main/java/net/coderbot/iris/gl/blending/BlendModeStorage.java @@ -54,10 +54,7 @@ public class BlendModeStorage { } public static void deferBlendFunc(int srcRgb, int dstRgb, int srcAlpha, int dstAlpha) { - originalBlend.setSrcRgb(srcRgb); - originalBlend.setDstRgb(dstRgb); - originalBlend.setSrcAlpha(srcAlpha); - originalBlend.setDstAlpha(dstAlpha); + originalBlend.setAll(srcRgb, dstRgb, srcAlpha, dstAlpha); } public static void restoreBlend() { diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft.java index 7a7966227..c2ffd6b9c 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/MixinMinecraft.java @@ -10,6 +10,7 @@ import net.minecraft.client.settings.GameSettings; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -27,9 +28,25 @@ public abstract class MixinMinecraft { @Shadow public abstract int getLimitFramerate(); + @Shadow(remap = false) + private static int max_texture_size; + @Unique private long angelica$lastFrameTime = 0; + /** + * @author mitchej123 + * @reason Avoid GL_PROXY_TEXTURE_2D which doesn't work with GLSM's texture binding. + * Uses the standard GL_MAX_TEXTURE_SIZE query instead. + */ + @Overwrite + public static int getGLMaximumTextureSize() { + if (max_texture_size == -1) { + max_texture_size = GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE); + } + return max_texture_size; + } + @Inject( method = "runGameLoop", at = @At(value = "INVOKE", target = "Lcpw/mods/fml/common/FMLCommonHandler;onRenderTickEnd(F)V", shift = At.Shift.AFTER, remap = false) @@ -59,7 +76,7 @@ public abstract class MixinMinecraft { private void angelica$trackFrametimes(CallbackInfo ci) { if (AngelicaMod.proxy == null) return; - long time = System.nanoTime(); + final long time = System.nanoTime(); AngelicaMod.proxy.putFrametime(time - angelica$lastFrameTime); angelica$lastFrameTime = time; } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/debug/MixinSplashProgress.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/debug/MixinSplashProgress.java index 2f464aa5c..0d4cf03b2 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/debug/MixinSplashProgress.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/debug/MixinSplashProgress.java @@ -7,17 +7,22 @@ import cpw.mods.fml.client.SplashProgress; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; +/** + * Debug mixin to disable splash screen when LWJGL debug mode is active. + * + * NOTE: This is only needed because LWJGL's debug callback conflicts with the splash thread's + * GL context switching. The splash screen itself works correctly with GLSM caching + * + */ @SuppressWarnings("deprecation") @Mixin(value = SplashProgress.class, remap = false) public class MixinSplashProgress { @WrapOperation(method="Lcpw/mods/fml/client/SplashProgress;start()V", at=@At(value="INVOKE", target="Lcpw/mods/fml/client/SplashProgress;getBool(Ljava/lang/String;Z)Z")) private static boolean angelica$disableSplashProgress(String name, boolean def, Operation original) { - AngelicaTweaker.LOGGER.info("Forcibly disabling splash progress because LWJGL debug has been set"); - // Forcibly disable splash progress until we can figure out why it's not working with our debug callback - if(name.equals("enabled")) + if(name.equals("enabled")) { + AngelicaTweaker.LOGGER.info("Disabling splash screen due to LWJGL debug mode"); return false; - else - return original.call(name, def); + } + return original.call(name, def); } - } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/glsm/MixinSplashProgressCaching.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/glsm/MixinSplashProgressCaching.java new file mode 100644 index 000000000..1e4885b3a --- /dev/null +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/glsm/MixinSplashProgressCaching.java @@ -0,0 +1,55 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.glsm; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import org.lwjgl.opengl.Drawable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Controls GLSM cache tracking based on which GL context is active during splash screen lifecycle. + * + * Two GL contexts exist during splash: + * - SharedDrawable: Isolated context used by Client thread during splash, discarded after + * - DrawableGL: Main Display context that survives into the game (splash thread renders here) + * + * We only want to cache state changes made on DrawableGL, since SharedDrawable's state is irrelevant + * to the main game loop. + * + * GLStateManager.makeCurrent() automatically handles caching based on whether the drawable + * is SharedDrawable or DrawableGL. This mixin just needs to: + * 1. Register the SharedDrawable reference when it's created + * 2. Mark splash complete when finish() is called + */ +@SuppressWarnings("deprecation") +@Mixin(value = cpw.mods.fml.client.SplashProgress.class, remap = false) +public class MixinSplashProgressCaching { + + @Shadow private static Drawable d; + + /** + * Before start() calls d.makeCurrent(), register the SharedDrawable reference. + * This allows GLStateManager.makeCurrent() to distinguish SharedDrawable from DrawableGL. + */ + @Inject(method = "start", + at = @At(value = "INVOKE", + target = "Lorg/lwjgl/opengl/Drawable;makeCurrent()V")) + private static void angelica$captureSharedDrawable(CallbackInfo ci) { + GLStateManager.setSharedDrawable(d); + } + + /** + * After finish() calls Display.getDrawable().makeCurrent() (DrawableGL), mark splash complete. + * This is the final switch to DrawableGL that persists into the main game loop. + * markSplashComplete() enables the fast path that bypasses holder tracking. + */ + @Inject(method = "finish", + at = @At(value = "INVOKE", + target = "Lorg/lwjgl/opengl/Drawable;makeCurrent()V", + shift = At.Shift.AFTER)) + private static void angelica$enableCachingOnFinish(CallbackInfo ci) { + GLStateManager.markSplashComplete(); + } +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/optimizations/MixinGLAllocation.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/optimizations/MixinGLAllocation.java new file mode 100644 index 000000000..3ec9e3376 --- /dev/null +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/optimizations/MixinGLAllocation.java @@ -0,0 +1,66 @@ +package com.gtnewhorizons.angelica.mixins.early.angelica.optimizations; + +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import net.minecraft.client.renderer.GLAllocation; +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; + +/** + * Optimizes GLAllocation display list tracking by replacing HashMap with fastutil Int2IntOpenHashMap. + */ +@Mixin(GLAllocation.class) +public class MixinGLAllocation { + + @Shadow @Final @Mutable + private static Map mapDisplayLists; + + static { + mapDisplayLists = new Int2IntOpenHashMap(); + } + + /** + * @author mitchej123 + * @reason Use primitive put() method to avoid boxing overhead + */ + @Overwrite + public static synchronized int generateDisplayLists(int count) { + final int id = GL11.glGenLists(count); + // Cast to Int2IntMap to use primitive put(int, int) instead of put(Integer, Integer) + ((Int2IntMap) mapDisplayLists).put(id, count); + return id; + } + + /** + * @author mitchej123 + * @reason Use primitive remove() method and add null-safety + */ + @Overwrite + public static synchronized void deleteDisplayLists(int id) { + // Cast to Int2IntMap to use primitive remove(int) which returns 0 if key not found + // This makes it null-safe even if called with unallocated ID (no NPE from unboxing null) + final int count = ((Int2IntMap) mapDisplayLists).remove(id); + if (count > 0) { + GL11.glDeleteLists(id, count); + } + } + + /** + * @author mitchej123 + * @reason Use primitive iteration to avoid boxing + */ + @Overwrite + public static synchronized void deleteTexturesAndDisplayLists() { + // Cast to Int2IntMap to use primitive iteration + for (Int2IntMap.Entry entry : ((Int2IntMap) mapDisplayLists).int2IntEntrySet()) { + GL11.glDeleteLists(entry.getIntKey(), entry.getIntValue()); + } + mapDisplayLists.clear(); + } +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinGLAllocation.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinGLAllocation.java deleted file mode 100644 index ff4261243..000000000 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinGLAllocation.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.angelica.vbo; - -import net.minecraft.client.renderer.GLAllocation; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(GLAllocation.class) -public class MixinGLAllocation { - @Inject(method = "deleteDisplayLists", at = @At("HEAD"), cancellable = true) - private static void deleteDisplayLists(int list, CallbackInfo ci) { - if(list < 0) ci.cancel(); - } -} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinModelRenderer.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinModelRenderer.java deleted file mode 100644 index 9e0b5aaed..000000000 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinModelRenderer.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.angelica.vbo; - - -import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager; -import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VBOManager; -import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; -import net.minecraft.client.model.ModelRenderer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -@Mixin(ModelRenderer.class) -public class MixinModelRenderer { - @Shadow - public int displayList; - - @Redirect(method = "compileDisplayList", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GLAllocation;generateDisplayLists(I)I")) - public int generateDisplayLists(int range) { - return VBOManager.generateDisplayLists(range); - } - - @Redirect(method = "compileDisplayList", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glNewList(II)V", remap = false)) - public void startCapturingVBO(int list, int mode) { - TessellatorManager.startCapturing(); - } - - @Redirect(method = "compileDisplayList", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEndList()V", remap = false)) - public void stopCapturingVBO() { - VBOManager.registerVBO(this.displayList, TessellatorManager.stopCapturingToVBO(DefaultVertexFormat.POSITION_TEXTURE_NORMAL)); - } -} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinRenderGlobal.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinRenderGlobal.java index 918fbcc99..1bc6acb00 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinRenderGlobal.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinRenderGlobal.java @@ -1,84 +1,27 @@ package com.gtnewhorizons.angelica.mixins.early.angelica.vbo; - -import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager; -import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VBOManager; -import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.render.CloudRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.WorldClient; -import net.minecraft.client.renderer.GLAllocation; import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.renderer.Tessellator; import net.minecraftforge.client.IRenderHandler; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; @Mixin(value = RenderGlobal.class) public class MixinRenderGlobal { - @Shadow public int starGLCallList; - @Shadow private int glSkyList; - @Shadow private int glSkyList2; @Shadow public WorldClient theWorld; @Shadow public Minecraft mc; @Shadow private int cloudTickCounter; - @Redirect(method="", at = @At(value="INVOKE", target="Lnet/minecraft/client/renderer/GLAllocation;generateDisplayLists(I)I", ordinal = 0)) - private int generateGLRenderListBaseDisplayLists(int range) { - return AngelicaConfig.enableSodium ? -1 : GLAllocation.generateDisplayLists(range); - } - - @Redirect(method="", at = @At(value="INVOKE", target="Lnet/minecraft/client/renderer/GLAllocation;generateDisplayLists(I)I", ordinal = 2), remap = false) - private int generateDisplayLists(int range) { - return VBOManager.generateDisplayLists(range); - } - - @Redirect(method="", at = @At(value="INVOKE", target="Lorg/lwjgl/opengl/GL11;glNewList(II)V", ordinal = 0), remap = false) - public void startStarsVBO(int list, int mode) { - TessellatorManager.startCapturing(); - } - @Redirect(method="", at = @At(value="INVOKE", target="Lorg/lwjgl/opengl/GL11;glEndList()V", ordinal = 0), remap = false) - public void finishStarsVBO() { - VBOManager.registerVBO(starGLCallList, TessellatorManager.stopCapturingToVBO(DefaultVertexFormat.POSITION)); - } - - @Redirect(method="", at = @At(value="FIELD", target="Lnet/minecraft/client/renderer/Tessellator;instance:Lnet/minecraft/client/renderer/Tessellator;")) - private Tessellator redirectTessellator() { - TessellatorManager.startCapturing(); - return TessellatorManager.get(); - } - - @Redirect(method="", at = @At(value="INVOKE", target="Lorg/lwjgl/opengl/GL11;glNewList(II)V", ordinal = 1), remap = false) - public void startSkyVBO(int list, int mode) { - // Do nothing, we'll be making a VBO instead and already started the capturing above - } - - @Redirect(method="", at = @At(value="INVOKE", target="Lorg/lwjgl/opengl/GL11;glEndList()V", ordinal = 1), remap = false) - public void finishSkyVBO() { - VBOManager.registerVBO(glSkyList, TessellatorManager.stopCapturingToVBO(DefaultVertexFormat.POSITION)); - } - - @Redirect(method="", at = @At(value="INVOKE", target="Lorg/lwjgl/opengl/GL11;glNewList(II)V", ordinal = 2), remap = false) - public void startSky2VBO(int list, int mode) { - TessellatorManager.startCapturing(); - } - - @Redirect(method="", at = @At(value="INVOKE", target="Lorg/lwjgl/opengl/GL11;glEndList()V", ordinal = 2), remap = false) - public void finishSky2VBO() { - VBOManager.registerVBO(glSkyList2, TessellatorManager.stopCapturingToVBO(DefaultVertexFormat.POSITION)); - } - /** * @author mitchej123 * @reason VBO Clouds */ @Overwrite public void renderClouds(float partialTicks) { - IRenderHandler renderer; + final IRenderHandler renderer; if((renderer = theWorld.provider.getCloudRenderer()) != null) { renderer.render(partialTicks, theWorld, mc); return; diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinWavefrontObject.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinWavefrontObject.java deleted file mode 100644 index 8b8717228..000000000 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/vbo/MixinWavefrontObject.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.gtnewhorizons.angelica.mixins.early.angelica.vbo; - -import com.gtnewhorizon.gtnhlib.client.renderer.vbo.IModelCustomExt; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import net.minecraftforge.client.model.obj.WavefrontObject; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; - -@Mixin(value = WavefrontObject.class, remap = false) -public abstract class MixinWavefrontObject { - /** - * @author mitchej123 - * @reason Force all models to use VBOs - */ - @SideOnly(Side.CLIENT) - @Overwrite - public void renderAll() { - ((IModelCustomExt) (Object)this).renderAllVBO(); - } -} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/base/MixinMinecraft.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/base/MixinMinecraft.java index ddbdc8afd..9be8c0d2e 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/base/MixinMinecraft.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/base/MixinMinecraft.java @@ -99,6 +99,7 @@ public abstract class MixinMinecraft { @Redirect( method = "loadScreen()V", + expect = 0, at = @At( value = "INVOKE", target = "Lnet/minecraft/client/renderer/texture/TextureManager;getDynamicTextureLocation(Ljava/lang/String;Lnet/minecraft/client/renderer/texture/DynamicTexture;)Lnet/minecraft/util/ResourceLocation;")) diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/cit/client/renderer/entity/MixinRenderEntityLiving.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/cit/client/renderer/entity/MixinRenderEntityLiving.java index 71ce20b5a..8ee42a2df 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/cit/client/renderer/entity/MixinRenderEntityLiving.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/cit/client/renderer/entity/MixinRenderEntityLiving.java @@ -20,7 +20,7 @@ import org.spongepowered.asm.mixin.Shadow; import com.prupe.mcpatcher.cit.CITUtils; -@Mixin(RendererLivingEntity.class) +@Mixin(value = RendererLivingEntity.class, priority = 500) public abstract class MixinRenderEntityLiving extends Render { @Final diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal_DisplayLists.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal_DisplayLists.java new file mode 100644 index 000000000..0755d9b64 --- /dev/null +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal_DisplayLists.java @@ -0,0 +1,22 @@ +package com.gtnewhorizons.angelica.mixins.early.mcpatcherforge.renderpass; + +import net.minecraft.client.renderer.GLAllocation; +import net.minecraft.client.renderer.RenderGlobal; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * RenderPass display list allocation increase - increases from 3 to 5 display lists per chunk. + * Only the constructor modification - separated for compatibility with Sodium. + */ +@Mixin(RenderGlobal.class) +public abstract class MixinRenderGlobal_DisplayLists { + + @Redirect( + method = "(Lnet/minecraft/client/Minecraft;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GLAllocation;generateDisplayLists(I)I", ordinal = 0)) + private int modifyRenderGlobal(int n) { + return GLAllocation.generateDisplayLists(n / 3 * 5); + } +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal_Features.java similarity index 84% rename from src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal.java rename to src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal_Features.java index f13e972dd..b30722e82 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/mcpatcherforge/renderpass/MixinRenderGlobal_Features.java @@ -1,7 +1,6 @@ package com.gtnewhorizons.angelica.mixins.early.mcpatcherforge.renderpass; import net.minecraft.client.renderer.EntityRenderer; -import net.minecraft.client.renderer.GLAllocation; import net.minecraft.client.renderer.RenderGlobal; import net.minecraft.entity.EntityLivingBase; import net.minecraft.world.IWorldAccess; @@ -16,18 +15,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import com.prupe.mcpatcher.renderpass.RenderPass; import com.prupe.mcpatcher.renderpass.RenderPassMap; +/** + * RenderPass feature hooks - loadRenderers, sortAndRender, renderAllRenderLists. + * Separated from display list allocation for compatibility with Sodium. + */ @Mixin(RenderGlobal.class) -public abstract class MixinRenderGlobal implements IWorldAccess { - - @Redirect( - method = "(Lnet/minecraft/client/Minecraft;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/GLAllocation;generateDisplayLists(I)I", - ordinal = 0)) - private int modifyRenderGlobal(int n) { - return GLAllocation.generateDisplayLists(n / 3 * 5); - } +public abstract class MixinRenderGlobal_Features implements IWorldAccess { @ModifyVariable( method = "loadRenderers()V", @@ -78,5 +71,4 @@ public abstract class MixinRenderGlobal implements IWorldAccess { private void modifyRenderAllRenderLists(EntityRenderer instance, double partialTick) { RenderPass.enableDisableLightmap(instance, partialTick); } - } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderBlocks.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderBlocks.java index 45530c471..b84c0cb8c 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderBlocks.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderBlocks.java @@ -2,8 +2,8 @@ package com.gtnewhorizons.angelica.mixins.early.sodium; import com.gtnewhorizons.angelica.AngelicaMod; import com.gtnewhorizons.angelica.common.BlockError; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.loading.AngelicaTweaker; +import net.coderbot.iris.Iris; import cpw.mods.fml.client.registry.RenderingRegistry; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; @@ -88,7 +88,7 @@ public abstract class MixinRenderBlocks { @Redirect(method = "renderStandardBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isAmbientOcclusionEnabled()Z")) private boolean checkAOEnabled() { if ((this.isRenderingByType && Minecraft.isAmbientOcclusionEnabled() && SodiumClientMod.options().quality.useSodiumAO) || - (AngelicaConfig.enableIris && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo())) { + (Iris.enabled && BlockRenderingSettings.INSTANCE.shouldUseSeparateAo())) { return false; // Force sodium pipeline with Iris or for standard blocks rendered from renderBlockByRenderType when using AO } diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderGlobal.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderGlobal.java index 22fe6ac8c..3013efdc3 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderGlobal.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinRenderGlobal.java @@ -3,7 +3,6 @@ package com.gtnewhorizons.angelica.mixins.early.sodium; import com.gtnewhorizons.angelica.compat.mojang.Camera; import com.gtnewhorizons.angelica.compat.mojang.GameModeUtil; import com.gtnewhorizons.angelica.compat.toremove.MatrixStack; -import com.gtnewhorizons.angelica.config.AngelicaConfig; import com.gtnewhorizons.angelica.mixins.interfaces.IRenderGlobalExt; import com.gtnewhorizons.angelica.rendering.AngelicaRenderQueue; import com.gtnewhorizons.angelica.rendering.RenderingState; @@ -70,6 +69,15 @@ public class MixinRenderGlobal implements IRenderGlobalExt { this.renderer.scheduleTerrainUpdate(); } + @Redirect( + method = "(Lnet/minecraft/client/Minecraft;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GLAllocation;generateDisplayLists(I)I", ordinal = 0)) + private int sodium$skipDisplayListAllocation(int count) { + // Skip allocation when Sodium is enabled - Sodium completely replaces WorldRenderer + // with its own chunk rendering system, so these display lists are never used. + return 0; + } + @Inject(method="", at=@At("RETURN")) private void sodium$initRenderer(Minecraft mc, CallbackInfo ci) { this.renderer = SodiumWorldRenderer.create(mc); @@ -164,7 +172,7 @@ public class MixinRenderGlobal implements IRenderGlobalExt { @Overwrite public int sortAndRender(EntityLivingBase entity, int pass, double partialTicks) { final WorldRenderingPipeline pipeline; - if(!AngelicaConfig.enableIris) { + if(!Iris.enabled) { pipeline = null; } else { pipeline = Iris.getPipelineManager().getPipelineNullable(); diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTileEntity.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTileEntity.java index 188a753df..b5faabf4c 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTileEntity.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/sodium/MixinTileEntity.java @@ -23,7 +23,7 @@ public class MixinTileEntity { @Overwrite public Block getBlockType() { Block block = this.blockType; - if (block == null) { + if (block == null && this.worldObj != null) { this.blockType = block = this.worldObj.getBlock(this.xCoord, this.yCoord, this.zCoord); } @@ -37,7 +37,7 @@ public class MixinTileEntity { @Overwrite public int getBlockMetadata() { int metadata = this.blockMetadata; - if (metadata == -1) { + if (metadata == -1 && this.worldObj != null) { this.blockMetadata = metadata = this.worldObj.getBlockMetadata(this.xCoord, this.yCoord, this.zCoord); } diff --git a/src/test/java/com/gtnewhorizons/angelica/AngelicaExtension.java b/src/test/java/com/gtnewhorizons/angelica/AngelicaExtension.java index 047450ac1..f2422ce29 100644 --- a/src/test/java/com/gtnewhorizons/angelica/AngelicaExtension.java +++ b/src/test/java/com/gtnewhorizons/angelica/AngelicaExtension.java @@ -1,20 +1,27 @@ package com.gtnewhorizons.angelica; +import com.gtnewhorizons.angelica.glsm.GLDebug; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.RenderSystem; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.lwjgl.LWJGLException; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.DisplayMode; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.PixelFormat; +import org.lwjgl.util.glu.GLU; + +import sun.misc.Unsafe; + +import java.lang.reflect.Field; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; -public class AngelicaExtension implements BeforeAllCallback, AfterEachCallback, ExtensionContext.Store.CloseableResource { +public class AngelicaExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, ExtensionContext.Store.CloseableResource { private static boolean started = false; private static DisplayMode displayMode; @@ -22,6 +29,10 @@ public class AngelicaExtension implements BeforeAllCallback, AfterEachCallback, public static String glRenderer; public static String glVersion; + // Expected stack depths (1 = just the base matrix, no pushes) + private static final int EXPECTED_MODELVIEW_STACK_DEPTH = 1; + private static final int EXPECTED_PROJECTION_STACK_DEPTH = 1; + @Override public void beforeAll(ExtensionContext context) throws LWJGLException { if (!started) { @@ -31,12 +42,16 @@ public class AngelicaExtension implements BeforeAllCallback, AfterEachCallback, Display.setDisplayModeAndFullscreen(displayMode); Display.setResizable(false); Display.setFullscreen(false); - final PixelFormat format = new PixelFormat().withDepthBits(24); + final PixelFormat format = new PixelFormat().withDepthBits(24).withStencilBits(8); Display.create(format); + // Set MainThread to the current test thread (the one with the GL context) + setMainThread(Thread.currentThread()); + // Warm-up State Manager features GLStateManager.preInit(); GLStateManager.setRunningSplash(false); // So we don't bypass the cache + GLStateManager.markSplashComplete(); // Enable caching for tests GLStateManager.BYPASS_CACHE = false; // Just to be sure RenderSystem.initRenderer(); context.getRoot().getStore(GLOBAL).put("AngelicaExtension", this); @@ -50,13 +65,75 @@ public class AngelicaExtension implements BeforeAllCallback, AfterEachCallback, } } + private static void setMainThread(Thread thread) { + try { + // Use Unsafe to set final static field - works on all Java versions + Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + Unsafe unsafe = (Unsafe) unsafeField.get(null); + + Field mainThreadField = GLStateManager.class.getDeclaredField("MainThread"); + Object base = unsafe.staticFieldBase(mainThreadField); + long offset = unsafe.staticFieldOffset(mainThreadField); + unsafe.putObject(base, offset, thread); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException("Failed to set MainThread for tests", e); + } + } + @Override public void close() { Display.destroy(); } + @Override + public void beforeEach(ExtensionContext context) throws Exception { + // Clear any pending GL errors from previous tests + while (GL11.glGetError() != GL11.GL_NO_ERROR) { + // drain errors + } + + // Reset matrix stacks to known state by popping until we're at base level + GL11.glMatrixMode(GL11.GL_MODELVIEW); + while (GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH) > EXPECTED_MODELVIEW_STACK_DEPTH) { + GL11.glPopMatrix(); + } + GL11.glLoadIdentity(); + + GL11.glMatrixMode(GL11.GL_PROJECTION); + while (GL11.glGetInteger(GL11.GL_PROJECTION_STACK_DEPTH) > EXPECTED_PROJECTION_STACK_DEPTH) { + GL11.glPopMatrix(); + } + GL11.glLoadIdentity(); + + // Reset to MODELVIEW mode (typical default) + GL11.glMatrixMode(GL11.GL_MODELVIEW); + + // Clear any errors from cleanup + while (GL11.glGetError() != GL11.GL_NO_ERROR) { + // drain errors + } + } + @Override public void afterEach(ExtensionContext context) throws Exception { - assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "GL Error"); + // Check for GL errors first + int error = GL11.glGetError(); + assertEquals(GL11.GL_NO_ERROR, error, + () -> "GL Error: " + GLU.gluErrorString(error)); + + // Check matrix stack depths - unbalanced push/pop is a common bug + int modelviewDepth = GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH); + int projectionDepth = GL11.glGetInteger(GL11.GL_PROJECTION_STACK_DEPTH); + + assertEquals(EXPECTED_MODELVIEW_STACK_DEPTH, modelviewDepth, + "MODELVIEW stack depth mismatch - unbalanced glPushMatrix/glPopMatrix"); + assertEquals(EXPECTED_PROJECTION_STACK_DEPTH, projectionDepth, + "PROJECTION stack depth mismatch - unbalanced glPushMatrix/glPopMatrix"); + + // Check matrix mode is reset to MODELVIEW + int matrixMode = GL11.glGetInteger(GL11.GL_MATRIX_MODE); + assertEquals(GL11.GL_MODELVIEW, matrixMode, + () -> "Matrix mode not reset to MODELVIEW, was: " + GLDebug.getMatrixModeName(matrixMode)); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/DisplayListTestHelper.java b/src/test/java/com/gtnewhorizons/angelica/glsm/DisplayListTestHelper.java new file mode 100644 index 000000000..2e7815082 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/DisplayListTestHelper.java @@ -0,0 +1,208 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuad; +import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadViewMutable; +import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw; +import org.joml.Matrix4f; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper class for building test data for display list batching tests. + * Allows creating quads and AccumulatedDraws without GL context. + */ +public final class DisplayListTestHelper { + + private DisplayListTestHelper() {} + + // ==================== Quad Builders ==================== + + /** + * Create a quad from 4 vertex positions. + * @param vertices 4 vertices, each as [x, y, z] + */ + public static ModelQuad createQuad(float[][] vertices) { + if (vertices.length != 4) { + throw new IllegalArgumentException("Quad requires exactly 4 vertices"); + } + + ModelQuad quad = new ModelQuad(); + for (int i = 0; i < 4; i++) { + quad.setX(i, vertices[i][0]); + quad.setY(i, vertices[i][1]); + quad.setZ(i, vertices[i][2]); + // Set default color (white, fully opaque) + quad.setColor(i, 0xFFFFFFFF); + // Set default UVs + quad.setTexU(i, i == 1 || i == 2 ? 1.0f : 0.0f); + quad.setTexV(i, i >= 2 ? 1.0f : 0.0f); + } + return quad; + } + + /** + * Create a simple unit quad at origin (0,0,0) to (1,1,0). + */ + public static ModelQuad createSimpleQuad() { + return createQuad(new float[][] { + {0, 0, 0}, + {1, 0, 0}, + {1, 1, 0}, + {0, 1, 0} + }); + } + + /** + * Create a quad at a specific position (offset from origin). + */ + public static ModelQuad createQuadAt(float x, float y, float z) { + return createQuad(new float[][] { + {x, y, z}, + {x + 1, y, z}, + {x + 1, y + 1, z}, + {x, y + 1, z} + }); + } + + /** + * Create multiple simple quads. + */ + public static List createQuads(int count) { + List quads = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + quads.add(createSimpleQuad()); + } + return quads; + } + + // ==================== AccumulatedDraw Builders ==================== + + /** + * Create an AccumulatedDraw with specified parameters. + */ + public static AccumulatedDraw createDraw( + List quads, + Matrix4f transform, + CapturingTessellator.Flags flags, + int commandIndex) { + return new AccumulatedDraw(quads, transform, flags, commandIndex); + } + + /** + * Create an AccumulatedDraw with a single quad and identity transform. + */ + public static AccumulatedDraw createSimpleDraw(int commandIndex) { + return createDraw( + Collections.singletonList(createSimpleQuad()), + new Matrix4f(), + positionOnly(), + commandIndex + ); + } + + /** + * Create an AccumulatedDraw with specified transform. + */ + public static AccumulatedDraw createDrawWithTransform(Matrix4f transform, int commandIndex) { + return createDraw( + Collections.singletonList(createSimpleQuad()), + transform, + positionOnly(), + commandIndex + ); + } + + /** + * Create an AccumulatedDraw with specified flags. + */ + public static AccumulatedDraw createDrawWithFlags(CapturingTessellator.Flags flags, int commandIndex) { + return createDraw( + Collections.singletonList(createSimpleQuad()), + new Matrix4f(), + flags, + commandIndex + ); + } + + /** + * Create an AccumulatedDraw with multiple quads. + */ + public static AccumulatedDraw createDrawWithQuads(int quadCount, int commandIndex) { + return createDraw( + createQuads(quadCount), + new Matrix4f(), + positionOnly(), + commandIndex + ); + } + + // ==================== Flag Helpers ==================== + + /** + * Flags for position-only vertices (no color, texture, brightness, normals). + */ + public static CapturingTessellator.Flags positionOnly() { + return new CapturingTessellator.Flags(false, false, false, false); + } + + /** + * Flags for position + color vertices. + */ + public static CapturingTessellator.Flags positionColor() { + return new CapturingTessellator.Flags(false, false, true, false); + } + + /** + * Flags for position + texture vertices. + */ + public static CapturingTessellator.Flags positionTexture() { + return new CapturingTessellator.Flags(true, false, false, false); + } + + /** + * Flags for position + color + texture vertices. + */ + public static CapturingTessellator.Flags positionColorTexture() { + return new CapturingTessellator.Flags(true, false, true, false); + } + + /** + * Flags for full vertex format (all attributes). + */ + public static CapturingTessellator.Flags fullFormat() { + return new CapturingTessellator.Flags(true, true, true, true); + } + + // ==================== Transform Helpers ==================== + + /** + * Create identity transform. + */ + public static Matrix4f identity() { + return new Matrix4f(); + } + + /** + * Create translation transform. + */ + public static Matrix4f translate(float x, float y, float z) { + return new Matrix4f().translate(x, y, z); + } + + /** + * Create rotation transform (around Y axis). + */ + public static Matrix4f rotateY(float degrees) { + return new Matrix4f().rotateY((float) Math.toRadians(degrees)); + } + + /** + * Create scale transform. + */ + public static Matrix4f scale(float s) { + return new Matrix4f().scale(s); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLMS_MatrixStack_UnitTest.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLMS_MatrixStack_UnitTest.java index 38a62df0a..f4f5f715d 100644 --- a/src/test/java/com/gtnewhorizons/angelica/glsm/GLMS_MatrixStack_UnitTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLMS_MatrixStack_UnitTest.java @@ -104,16 +104,6 @@ public class GLMS_MatrixStack_UnitTest { GLStateManager.glPopMatrix(); testMatrix.identity(); validateMatrix(testMatrix, matrix.getEnum, "Popped " + matrix.name + " Matrix"); - } - - @AfterAll - static void cleanup() { - GLStateManager.glMatrixMode(GL11.GL_MODELVIEW); - GLStateManager.glLoadIdentity(); - GLStateManager.glMatrixMode(GL11.GL_PROJECTION); - GLStateManager.glLoadIdentity(); - GLStateManager.glMatrixMode(GL11.GL_TEXTURE); - GLStateManager.glLoadIdentity(); GLStateManager.glMatrixMode(GL11.GL_MODELVIEW); } diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Batching_Test.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Batching_Test.java new file mode 100644 index 000000000..d22e0116a --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Batching_Test.java @@ -0,0 +1,320 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator; +import com.gtnewhorizon.gtnhlib.client.renderer.vbo.VertexBuffer; +import com.gtnewhorizons.angelica.AngelicaExtension; +import com.gtnewhorizons.angelica.glsm.recording.AccumulatedDraw; +import com.gtnewhorizons.angelica.glsm.recording.commands.DisplayListCommand; +import com.gtnewhorizons.angelica.glsm.recording.commands.DrawRangeCmd; +import com.gtnewhorizons.angelica.glsm.recording.commands.MultMatrixCmd; +import com.gtnewhorizons.angelica.glsm.recording.commands.TranslateCmd; +import org.joml.Matrix4f; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for display list format-based batching behavior. + * + *

With format-based batching, VBO allocation depends ONLY on vertex format: + *

    + *
  • All draws with the same format share a single VBO
  • + *
  • Different transforms produce separate DrawRangeCmd but share the VBO
  • + *
  • Different formats produce separate VBOs
  • + *
+ * + *

Tests call {@link DisplayListManager#buildOptimizedDisplayList} directly + * with manually constructed AccumulatedDraws. GL context needed for VBO creation.

+ */ +@ExtendWith(AngelicaExtension.class) +class GLSM_DisplayList_Batching_Test { + + // ==================== Helper Methods ==================== + + private long countDrawRangeCmds(DisplayListCommand[] commands) { + long count = 0; + for (DisplayListCommand cmd : commands) { + if (cmd instanceof DrawRangeCmd) count++; + } + return count; + } + + private long countMultMatrixCmds(DisplayListCommand[] commands) { + long count = 0; + for (DisplayListCommand cmd : commands) { + if (cmd instanceof MultMatrixCmd) count++; + } + return count; + } + + private Set getUniqueVBOs(DisplayListCommand[] commands) { + Set vbos = new HashSet<>(); + for (DisplayListCommand cmd : commands) { + if (cmd instanceof DrawRangeCmd) { + vbos.add(((DrawRangeCmd) cmd).sharedVbo()); + } + } + return vbos; + } + + // ==================== Pure Unit Tests (No GL Context) ==================== + + @Test + void testConsecutiveDrawsWithSameFormatShareSingleVBO() { + // Setup: 3 draws at the same command index, same format + List draws = new ArrayList<>(); + draws.add(createSimpleDraw(0)); + draws.add(createSimpleDraw(0)); + draws.add(createSimpleDraw(0)); + + List commands = new ArrayList<>(); + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList( + commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 1 VBO for all draws (format-based batching) + assertEquals(1, result.ownedVbos().length, + "3 draws with same format should produce 1 shared VBO"); + + // Verify: 1 DrawRangeCmd covering all geometry + long drawCount = countDrawRangeCmds(optimized); + assertEquals(1, drawCount, + "3 consecutive draws with same format and transform should produce 1 DrawRangeCmd"); + } + + @Test + void testDrawsWithDifferentFormatsProduceSeparateVBOs() { + // Setup: 3 draws with different formats + List draws = new ArrayList<>(); + draws.add(createDrawWithFlags(positionOnly(), 0)); + draws.add(createDrawWithFlags(positionTexture(), 0)); + draws.add(createDrawWithFlags(positionColorTexture(), 0)); + + List commands = new ArrayList<>(); + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList(commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 3 VBOs (one per format) + assertEquals(3, result.ownedVbos().length, + "3 draws with different formats should produce 3 VBOs"); + + // Verify: 3 DrawRangeCmds + long drawCount = countDrawRangeCmds(optimized); + assertEquals(3, drawCount, "3 draws with different formats should produce 3 DrawRangeCmds"); + } + + @Test + void testDifferentTransformsSameFormatShareVBO() { + // Setup: draws with different transforms but same format + // With format-based batching, they share 1 VBO but produce separate DrawRangeCmds + List draws = new ArrayList<>(); + draws.add(createDrawWithTransform(new Matrix4f(), 0)); // Identity transform + draws.add(createDrawWithTransform(new Matrix4f().translate(1.0f, 0.0f, 0.0f), 2)); // After translate + draws.add(createDrawWithTransform(new Matrix4f().translate(3.0f, 0.0f, 0.0f), 4)); // After more translates + + List commands = new ArrayList<>(); + commands.add(new TranslateCmd(1.0, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 0 + commands.add(new TranslateCmd(2.0, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 1 + commands.add(new TranslateCmd(3.0, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 2 + commands.add(new TranslateCmd(4.0, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 3 + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList(commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 1 shared VBO (same format) + assertEquals(1, result.ownedVbos().length, "Draws with different transforms but same format should share 1 VBO"); + + // Verify: 3 DrawRangeCmds (one per draw, each with different transform) + long drawCount = countDrawRangeCmds(optimized); + assertEquals(3, drawCount, "3 draws with different transforms should produce 3 DrawRangeCmds"); + + // Verify: All DrawRangeCmds reference the same VBO + Set uniqueVBOs = getUniqueVBOs(optimized); + assertEquals(1, uniqueVBOs.size(), "All DrawRangeCmds should reference the same shared VBO"); + + // Should have MultMatrix commands for the transforms + long matrixCount = countMultMatrixCmds(optimized); + assertTrue(matrixCount >= 2, "Should have MultMatrix commands for each unique transform"); + } + + @Test + void testManyDrawsSameFormatProduceSingleVBO() { + // Setup: 100 draws at same position, same format + List draws = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + draws.add(createSimpleDraw(0)); + } + + List commands = new ArrayList<>(); + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList( + commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 1 shared VBO + assertEquals(1, result.ownedVbos().length, "100 draws with same format should produce 1 shared VBO"); + + // Verify: 1 DrawRangeCmd (all same transform) + long drawCount = countDrawRangeCmds(optimized); + assertEquals(1, drawCount, + "100 consecutive draws with same format and transform should produce 1 DrawRangeCmd"); + } + + @Test + void testDrawsWithMultipleQuadsSameFormat() { + // Setup: draws with varying quad counts should share VBO if same format + List draws = new ArrayList<>(); + draws.add(createDrawWithQuads(1, 0)); + draws.add(createDrawWithQuads(5, 0)); + draws.add(createDrawWithQuads(10, 0)); + + List commands = new ArrayList<>(); + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList(commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 1 shared VBO (total 16 quads) + assertEquals(1, result.ownedVbos().length, "Draws with different quad counts but same format should share 1 VBO"); + + // Verify: 1 DrawRangeCmd (all same transform) + long drawCount = countDrawRangeCmds(optimized); + assertEquals(1, drawCount, "Draws with different quad counts but same format and transform should produce 1 DrawRangeCmd"); + } + + @Test + void testEmptyDrawListProducesNoVBOs() { + // Setup: no draws + List draws = new ArrayList<>(); + List commands = new ArrayList<>(); + commands.add(new TranslateCmd(1.0, 0.0, 0.0, GL11.GL_MODELVIEW)); + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList( + commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: no VBOs + assertEquals(0, result.ownedVbos().length, "No draws should produce no VBOs"); + + // Verify: no DrawRangeCmds + long drawCount = countDrawRangeCmds(optimized); + assertEquals(0, drawCount, "No draws should produce no DrawRangeCmds"); + + // But should still have the collapsed transform + long matrixCount = countMultMatrixCmds(optimized); + assertEquals(1, matrixCount, + "Transform should still be emitted even without draws"); + } + + @Test + void testCancellingTransformsProduceSeparateDrawRangeCmds() { + // This tests the specific bug where transforms cancel out in the command stream + // (e.g., translate(+0.5) then translate(-0.5)) but draws captured in between + // have different transform states that must be preserved. + // + // With format-based batching: 1 shared VBO, 3 DrawRangeCmds + + List draws = new ArrayList<>(); + // Draw 1: captured at identity + draws.add(createDrawWithTransform(new Matrix4f(), 0)); + // Draw 2: captured at translate(0.5, 0, 0) - BEFORE the cancelling translate(-0.5) + draws.add(createDrawWithTransform(new Matrix4f().translate(0.5f, 0.0f, 0.0f), 2)); + // Draw 3: captured back at identity (after translate(-0.5) cancelled out) + draws.add(createDrawWithTransform(new Matrix4f(), 4)); + + List commands = new ArrayList<>(); + commands.add(new TranslateCmd(0.5, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 0: move +0.5 + commands.add(new TranslateCmd(-0.5, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 1: move -0.5 (cancels out) + commands.add(new TranslateCmd(0.5, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 2: move +0.5 + commands.add(new TranslateCmd(-0.5, 0.0, 0.0, GL11.GL_MODELVIEW)); // index 3: move -0.5 (cancels out) + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList( + commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 1 shared VBO (same format) + assertEquals(1, result.ownedVbos().length, "Draws with same format should share 1 VBO regardless of transforms"); + + // Verify: 3 DrawRangeCmds (different transforms require separate draw commands) + long drawCount = countDrawRangeCmds(optimized); + assertEquals(3, drawCount, "Draws with different recorded transforms must produce separate DrawRangeCmds"); + } + + @Test + void testDrawsWithSameTransformProduceSingleDrawRangeCmd() { + // Draws with the same format AND same transform produce a single DrawRangeCmd + Matrix4f commonTransform = new Matrix4f().translate(1.0f, 2.0f, 3.0f); + + List draws = new ArrayList<>(); + draws.add(createDrawWithTransform(new Matrix4f(commonTransform), 0)); + draws.add(createDrawWithTransform(new Matrix4f(commonTransform), 0)); + draws.add(createDrawWithTransform(new Matrix4f(commonTransform), 0)); + + List commands = new ArrayList<>(); + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList(commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 1 shared VBO + assertEquals(1, result.ownedVbos().length, + "Draws with same format should share 1 VBO"); + + // Verify: 1 DrawRangeCmd (same format AND same transform) + long drawCount = countDrawRangeCmds(optimized); + assertEquals(1, drawCount, "Draws with same format and transform should produce 1 DrawRangeCmd"); + + // Should have 1 MultMatrix for the common transform + long matrixCount = countMultMatrixCmds(optimized); + assertEquals(1, matrixCount, + "Should have exactly 1 MultMatrix for the common transform"); + } + + @Test + void testInterleavedFormatsProduceSeparateVBOs() { + // Setup: A A B A A - should produce 2 VBOs (one for A, one for B) + // With format-based batching, all A draws are grouped together and merged + // (since they have same transform), producing 2 DrawRangeCmds total + CapturingTessellator.Flags flagsA = positionOnly(); + CapturingTessellator.Flags flagsB = positionTexture(); + + List draws = new ArrayList<>(); + draws.add(createDrawWithFlags(flagsA, 0)); // A + draws.add(createDrawWithFlags(flagsA, 0)); // A + draws.add(createDrawWithFlags(flagsB, 0)); // B + draws.add(createDrawWithFlags(flagsA, 0)); // A + draws.add(createDrawWithFlags(flagsA, 0)); // A + + List commands = new ArrayList<>(); + + // Execute + OptimizedListResult result = DisplayListManager.buildOptimizedDisplayList( + commands, draws, null, 1); + DisplayListCommand[] optimized = result.commands(); + + // Verify: 2 VBOs (one for format A, one for format B) + assertEquals(2, result.ownedVbos().length, "A A B A A pattern should produce 2 VBOs (one per format)"); + + // Verify: 2 DrawRangeCmds (all A's merged into 1, B is 1) + // With format-based batching, all A draws go into the same FormatBuffer + // and get merged (same transform). Order within same command index doesn't matter. + long drawCount = countDrawRangeCmds(optimized); + assertEquals(2, drawCount, "A A B A A pattern with same transform should produce 2 DrawRangeCmds (A's merged, B separate)"); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_FormatOptimization_Test.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_FormatOptimization_Test.java new file mode 100644 index 000000000..f1417f075 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_FormatOptimization_Test.java @@ -0,0 +1,176 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizons.angelica.AngelicaExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.opengl.GL11; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for display list format optimization. + * Verifies that display lists work correctly with different vertex attribute combinations + * and that optimal formats are selected. + */ +@ExtendWith(AngelicaExtension.class) +class GLSM_DisplayList_FormatOptimization_Test { + + private int displayList = -1; + + @AfterEach + void cleanup() { + if (displayList > 0) { + GLStateManager.glDeleteLists(displayList, 1); + displayList = -1; + } + // Reset GL state to avoid test pollution + GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GLStateManager.glTexCoord2f(0.0f, 0.0f); + GLStateManager.glNormal3f(0.0f, 0.0f, 1.0f); + } + + @Test + void testPositionOnlyFormat() { + // Create a display list with only position data (no color, texture, brightness, or normals) + displayList = GL11.glGenLists(1); + GLStateManager.glNewList(displayList, GL11.GL_COMPILE); + + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex3f(0, 0, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glVertex3f(1, 1, 0); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + + GLStateManager.glEndList(); + + // Call the list to ensure it doesn't crash (verifies compilation succeeded) + GLStateManager.glCallList(displayList); + } + + @Test + void testPositionTextureFormat() { + // Create a display list with position and texture coordinates + displayList = GL11.glGenLists(1); + GLStateManager.glNewList(displayList, GL11.GL_COMPILE); + + GL11.glBegin(GL11.GL_QUADS); + GL11.glTexCoord2f(0, 0); + GL11.glVertex3f(0, 0, 0); + GL11.glTexCoord2f(1, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glTexCoord2f(1, 1); + GL11.glVertex3f(1, 1, 0); + GL11.glTexCoord2f(0, 1); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + + GLStateManager.glEndList(); + + // Call the list to ensure it doesn't crash (verifies compilation succeeded) + GLStateManager.glCallList(displayList); + } + + @Test + void testPositionColorTextureFormat() { + // Create a display list with position, color, and texture + displayList = GL11.glGenLists(1); + GLStateManager.glNewList(displayList, GL11.GL_COMPILE); + + GL11.glBegin(GL11.GL_QUADS); + GL11.glColor4f(1, 1, 1, 1); + GL11.glTexCoord2f(0, 0); + GL11.glVertex3f(0, 0, 0); + GL11.glColor4f(1, 0, 0, 1); + GL11.glTexCoord2f(1, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glColor4f(0, 1, 0, 1); + GL11.glTexCoord2f(1, 1); + GL11.glVertex3f(1, 1, 0); + GL11.glColor4f(0, 0, 1, 1); + GL11.glTexCoord2f(0, 1); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + + GLStateManager.glEndList(); + + // Call the list to ensure it doesn't crash (verifies compilation succeeded) + GLStateManager.glCallList(displayList); + } + + @Test + void testFullFormat() { + // Create a display list with all attributes: position, color, texture, and normal + displayList = GL11.glGenLists(1); + GLStateManager.glNewList(displayList, GL11.GL_COMPILE); + + GL11.glBegin(GL11.GL_QUADS); + GL11.glNormal3f(0, 0, 1); + GL11.glColor4f(1, 1, 1, 1); + GL11.glTexCoord2f(0, 0); + GL11.glVertex3f(0, 0, 0); + GL11.glNormal3f(0, 0, 1); + GL11.glColor4f(1, 0, 0, 1); + GL11.glTexCoord2f(1, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glNormal3f(0, 0, 1); + GL11.glColor4f(0, 1, 0, 1); + GL11.glTexCoord2f(1, 1); + GL11.glVertex3f(1, 1, 0); + GL11.glNormal3f(0, 0, 1); + GL11.glColor4f(0, 0, 1, 1); + GL11.glTexCoord2f(0, 1); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + + GLStateManager.glEndList(); + + // Call the list to ensure it doesn't crash (verifies compilation succeeded) + GLStateManager.glCallList(displayList); + } + + @Test + void testMultipleDrawsInDisplayList() { + // Test a display list with multiple draws with different formats + displayList = GL11.glGenLists(1); + GLStateManager.glNewList(displayList, GL11.GL_COMPILE); + + // First draw: position only + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex3f(0, 0, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glVertex3f(1, 1, 0); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + + // Second draw: position + color + GL11.glBegin(GL11.GL_QUADS); + GL11.glColor4f(1, 0, 0, 1); + GL11.glVertex3f(2, 0, 0); + GL11.glColor4f(0, 1, 0, 1); + GL11.glVertex3f(3, 0, 0); + GL11.glColor4f(0, 0, 1, 1); + GL11.glVertex3f(3, 1, 0); + GL11.glColor4f(1, 1, 0, 1); + GL11.glVertex3f(2, 1, 0); + GL11.glEnd(); + + // Third draw: position + texture + GL11.glBegin(GL11.GL_QUADS); + GL11.glTexCoord2f(0, 0); + GL11.glVertex3f(4, 0, 0); + GL11.glTexCoord2f(1, 0); + GL11.glVertex3f(5, 0, 0); + GL11.glTexCoord2f(1, 1); + GL11.glVertex3f(5, 1, 0); + GL11.glTexCoord2f(0, 1); + GL11.glVertex3f(4, 1, 0); + GL11.glEnd(); + + GLStateManager.glEndList(); + + // Call the list to ensure it doesn't crash (verifies compilation succeeded) + GLStateManager.glCallList(displayList); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Nesting_Test.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Nesting_Test.java new file mode 100644 index 000000000..add6971cf --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Nesting_Test.java @@ -0,0 +1,265 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizons.angelica.AngelicaExtension; +import org.joml.Matrix4f; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for nested display list execution. + * Verifies that child display lists compose correctly with parent transforms. + */ +@ExtendWith(AngelicaExtension.class) +class GLSM_DisplayList_Nesting_Test { + + private int parentList = -1; + private int childList = -1; + + @AfterEach + void cleanup() { + if (parentList > 0) { + GLStateManager.glDeleteLists(parentList, 1); + parentList = -1; + } + if (childList > 0) { + GLStateManager.glDeleteLists(childList, 1); + childList = -1; + } + // Reset GL state + GLStateManager.glLoadIdentity(); + GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + } + + @Test + void testNestedDisplayListWithParentTransform() { + // Create child list: just a simple quad at origin + childList = GL11.glGenLists(1); + GLStateManager.glNewList(childList, GL11.GL_COMPILE); + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex3f(0, 0, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glVertex3f(1, 1, 0); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + GLStateManager.glEndList(); + + // Create parent list: translate then call child + parentList = GL11.glGenLists(1); + GLStateManager.glNewList(parentList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); // Parent transform + GLStateManager.glCallList(childList); // Should render child at (10,0,0) + GLStateManager.glEndList(); + + // Execute parent list + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(parentList); + + // Check final matrix state - should have parent's translate applied + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Expected: translate(10, 0, 0) + Matrix4f expected = new Matrix4f().translate(10.0f, 0.0f, 0.0f); + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + // Compare buffers directly (both are column-major OpenGL format) + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testNestedDisplayListWithBothTransforms() { + // Create child list: translate and draw + childList = GL11.glGenLists(1); + GLStateManager.glNewList(childList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); // Child transform + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex3f(0, 0, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glVertex3f(1, 1, 0); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + GLStateManager.glEndList(); + + // Create parent list: translate then call child + parentList = GL11.glGenLists(1); + GLStateManager.glNewList(parentList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); // Parent transform + GLStateManager.glCallList(childList); // Child adds (1,0,0) to parent's (10,0,0) + GLStateManager.glEndList(); + + // Execute parent list + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(parentList); + + // Check final matrix state - should be translate(10,0,0) * translate(1,0,0) = translate(11,0,0) + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Expected: translate(11, 0, 0) = parent(10) + child(1) + Matrix4f expected = new Matrix4f().translate(11.0f, 0.0f, 0.0f); + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + // Compare buffers directly + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testNestedDisplayListWithPushPop() { + // Create child list with push/pop (shouldn't affect parent's matrix) + childList = GL11.glGenLists(1); + GLStateManager.glNewList(childList, GL11.GL_COMPILE); + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); // Child transform + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex3f(0, 0, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glVertex3f(1, 1, 0); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + GLStateManager.glPopMatrix(); // Restore matrix to parent's state + GLStateManager.glEndList(); + + // Create parent list + parentList = GL11.glGenLists(1); + GLStateManager.glNewList(parentList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); // Parent transform + GLStateManager.glCallList(childList); // Child uses push/pop + GLStateManager.glEndList(); + + // Execute parent list + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(parentList); + + // Check final matrix state - should still be translate(10,0,0) because child did push/pop + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Expected: translate(10, 0, 0) only (child's transform was popped) + Matrix4f expected = new Matrix4f().translate(10.0f, 0.0f, 0.0f); + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + // Compare buffers directly + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testDeepNesting() { + // Create deeply nested lists: grandchild -> child -> parent + int grandchildList = GL11.glGenLists(1); + GLStateManager.glNewList(grandchildList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); // Grandchild: +1 + GL11.glBegin(GL11.GL_QUADS); + GL11.glVertex3f(0, 0, 0); + GL11.glVertex3f(1, 0, 0); + GL11.glVertex3f(1, 1, 0); + GL11.glVertex3f(0, 1, 0); + GL11.glEnd(); + GLStateManager.glEndList(); + + childList = GL11.glGenLists(1); + GLStateManager.glNewList(childList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); // Child: +10 + GLStateManager.glCallList(grandchildList); // Grandchild: +1 + GLStateManager.glEndList(); + + parentList = GL11.glGenLists(1); + GLStateManager.glNewList(parentList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(100.0f, 0.0f, 0.0f); // Parent: +100 + GLStateManager.glCallList(childList); // Child: +10, Grandchild: +1 + GLStateManager.glEndList(); + + // Execute parent list + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(parentList); + + // Check final matrix state - should be 100 + 10 + 1 = 111 + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Expected: translate(111, 0, 0) + Matrix4f expected = new Matrix4f().translate(111.0f, 0.0f, 0.0f); + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + // Compare buffers directly + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch", i)); + } + + GLStateManager.glPopMatrix(); + + // Cleanup grandchild + GLStateManager.glDeleteLists(grandchildList, 1); + } + + @Test + void testMismatchedPushPopInSeparateLists() { + // This test verifies that push and pop matrix operations can be split across separate display lists. + // This is a uncommon but technically valid per the spec. + + // Get initial stack depth + int initialDepth = GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH); + + // Create a display list that only pushes the matrix + int matrixPushList = GL11.glGenLists(1); + GLStateManager.glNewList(matrixPushList, GL11.GL_COMPILE); + GLStateManager.glPushMatrix(); + GLStateManager.glEndList(); + + // Create a display list that only pops the matrix + int matrixPopList = GL11.glGenLists(1); + GLStateManager.glNewList(matrixPopList, GL11.GL_COMPILE); + GLStateManager.glPopMatrix(); + GLStateManager.glEndList(); + + // Verify initial stack depth hasn't changed after compilation + assertEquals(initialDepth, GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH), "Stack depth should be unchanged after compiling display lists"); + + // Call the push list - should increase stack depth + GLStateManager.glCallList(matrixPushList); + int depthAfterPush = GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH); + assertEquals(initialDepth + 1, depthAfterPush, "Stack depth should increase by 1 after calling push list"); + + // Optionally do some operations here that would benefit from the pushed matrix + GLStateManager.glTranslatef(5.0f, 0.0f, 0.0f); + + // Call the pop list - should restore stack depth + GLStateManager.glCallList(matrixPopList); + int depthAfterPop = GL11.glGetInteger(GL11.GL_MODELVIEW_STACK_DEPTH); + assertEquals(initialDepth, depthAfterPop, "Stack depth should return to initial after calling pop list"); + + // Cleanup + GLStateManager.glDeleteLists(matrixPushList, 1); + GLStateManager.glDeleteLists(matrixPopList, 1); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Stress_Test.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Stress_Test.java new file mode 100644 index 000000000..6cde4e27f --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_Stress_Test.java @@ -0,0 +1,320 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizons.angelica.AngelicaExtension; +import org.joml.Matrix4f; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Stress tests for display list system. + * Verifies system stability under high load conditions. + * + *

Note: Tests that require actual geometry capture via Tessellator + * need the full Minecraft environment. These tests focus on matrix + * operations and display list lifecycle which work with raw GL calls.

+ */ +@ExtendWith(AngelicaExtension.class) +class GLSM_DisplayList_Stress_Test { + + private final List testLists = new ArrayList<>(); + + @AfterEach + void cleanup() { + for (int list : testLists) { + if (list > 0) { + GLStateManager.glDeleteLists(list, 1); + } + } + testLists.clear(); + GLStateManager.glLoadIdentity(); + } + + private int createAndTrackList() { + int list = GL11.glGenLists(1); + testLists.add(list); + return list; + } + + @Test + void testManyDisplayListsWithTransforms() { + // Create and compile many display lists with transforms + final int numLists = 500; + + for (int i = 0; i < numLists; i++) { + int list = createAndTrackList(); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + + // Each list has a unique transform + GLStateManager.glTranslatef(i * 0.1f, 0, 0); + + GLStateManager.glEndList(); + + // Verify each list was compiled + assertTrue(DisplayListManager.displayListExists(list), + "Display list " + list + " should exist after compilation"); + } + + // Verify all lists still exist + for (int list : testLists) { + assertTrue(DisplayListManager.displayListExists(list), + "All " + numLists + " display lists should exist"); + } + } + + @Test + void testManyTransformsInSingleList() { + // Create a single display list with many transform operations + final int numTransforms = 1000; + + int list = createAndTrackList(); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + + // Many push/pop pairs with transforms + for (int i = 0; i < numTransforms; i++) { + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(i * 0.001f, 0, 0); + GLStateManager.glRotatef(i * 0.36f, 0, 1, 0); + GLStateManager.glPopMatrix(); + } + + // Final transform that should be the result + GLStateManager.glTranslatef(42.0f, 0, 0); + + GLStateManager.glEndList(); + + // Execute and verify final transform + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(list); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // All push/pop pairs cancel out, only final translate remains + assertEquals(42.0f, actualBuf.get(12), 0.0001f, + "After many push/pop pairs, only final transform should remain"); + + GLStateManager.glPopMatrix(); + } + + @Test + void testRapidCompileDeleteCycle() { + // Rapidly create and delete display lists to test resource management + final int cycles = 100; + + for (int cycle = 0; cycle < cycles; cycle++) { + int list = GL11.glGenLists(1); + + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glTranslatef(cycle, 0, 0); + GLStateManager.glEndList(); + + // Immediately delete + GLStateManager.glDeleteLists(list, 1); + + // Verify it's gone + assertFalse(DisplayListManager.displayListExists(list), + "Display list should be deleted after glDeleteLists"); + } + } + + @Test + void testDeepNestingChain() { + // Create a chain of display lists that call each other + final int chainLength = 50; + int[] chain = new int[chainLength]; + + // Create from innermost to outermost + for (int i = 0; i < chainLength; i++) { + chain[i] = createAndTrackList(); + GLStateManager.glNewList(chain[i], GL11.GL_COMPILE); + + // Each list translates by 1 and calls the previous list (if any) + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); + + if (i > 0) { + GLStateManager.glCallList(chain[i - 1]); + } + + GLStateManager.glEndList(); + } + + // Execute the outermost list + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(chain[chainLength - 1]); + + // Final matrix should have translation = chainLength (each list adds 1) + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + Matrix4f expected = new Matrix4f().translate(chainLength, 0.0f, 0.0f); + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch for chain of %d lists", i, chainLength)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testManyPushPopOperations() { + // Stress test push/pop stack operations + int list = createAndTrackList(); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + + // Go deep into the stack + final int stackDepth = 30; // GL minimum is 32 + for (int i = 0; i < stackDepth; i++) { + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); + } + + // Pop all the way back + for (int i = 0; i < stackDepth; i++) { + GLStateManager.glPopMatrix(); + } + + // Add final known transform + GLStateManager.glTranslatef(99.0f, 0.0f, 0.0f); + + GLStateManager.glEndList(); + + // Execute and verify + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(list); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // All pushes and pops cancel out, only final 99 translate remains + assertEquals(99.0f, actualBuf.get(12), 0.0001f, + "After balanced push/pop, only final transform should remain"); + + GLStateManager.glPopMatrix(); + } + + @Test + void testRapidListSwitching() { + // Create multiple lists and rapidly switch between them + final int numLists = 20; + int[] lists = new int[numLists]; + + // Create all lists with unique transforms + for (int i = 0; i < numLists; i++) { + lists[i] = createAndTrackList(); + GLStateManager.glNewList(lists[i], GL11.GL_COMPILE); + GLStateManager.glTranslatef(i + 1, 0, 0); // List i translates by (i+1) + GLStateManager.glEndList(); + } + + // Rapidly switch between calling different lists + for (int iteration = 0; iteration < 100; iteration++) { + for (int i = 0; i < numLists; i++) { + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(lists[i]); + + // Verify correct transform was applied + FloatBuffer buf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, buf); + assertEquals(i + 1, buf.get(12), 0.0001f, + "List " + i + " should translate by " + (i + 1)); + + GLStateManager.glPopMatrix(); + } + } + } + + @Test + void testRecompileSameId() { + // Test recompiling a display list with the same ID + int list = createAndTrackList(); + + // First compilation + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glTranslatef(5.0f, 0.0f, 0.0f); + GLStateManager.glEndList(); + + // Verify first compilation + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(list); + + FloatBuffer buf1 = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, buf1); + assertEquals(5.0f, buf1.get(12), 0.0001f, "First compilation should translate by 5"); + + GLStateManager.glPopMatrix(); + + // Recompile with different transform + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); + GLStateManager.glEndList(); + + // Verify recompilation + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(list); + + FloatBuffer buf2 = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, buf2); + assertEquals(10.0f, buf2.get(12), 0.0001f, "Recompilation should translate by 10"); + + GLStateManager.glPopMatrix(); + } + + @Test + void testComplexTransformSequence() { + // Test a complex sequence of transforms that could expose numerical issues + int list = createAndTrackList(); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + + // Apply transforms that together should result in a known value + GLStateManager.glTranslatef(100.0f, 0.0f, 0.0f); + GLStateManager.glRotatef(90.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glTranslatef(50.0f, 0.0f, 0.0f); // This goes in Y direction after rotate + GLStateManager.glRotatef(-90.0f, 0.0f, 0.0f, 1.0f); // Cancel the rotation + GLStateManager.glTranslatef(25.0f, 0.0f, 0.0f); + + GLStateManager.glEndList(); + + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(list); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Build expected with JOML + Matrix4f expected = new Matrix4f() + .translate(100.0f, 0.0f, 0.0f) + .rotate((float) Math.toRadians(90.0f), 0.0f, 0.0f, 1.0f) + .translate(50.0f, 0.0f, 0.0f) + .rotate((float) Math.toRadians(-90.0f), 0.0f, 0.0f, 1.0f) + .translate(25.0f, 0.0f, 0.0f); + + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.001f, + String.format("Matrix[%d] mismatch in complex transform", i)); + } + + GLStateManager.glPopMatrix(); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_TransformCollapsing_Test.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_TransformCollapsing_Test.java new file mode 100644 index 000000000..33c4139a0 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_TransformCollapsing_Test.java @@ -0,0 +1,648 @@ +package com.gtnewhorizons.angelica.glsm; + +import com.gtnewhorizons.angelica.AngelicaExtension; +import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList; +import com.gtnewhorizons.angelica.glsm.recording.GLCommand; +import org.joml.Matrix4f; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.FloatBuffer; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for transform collapsing optimization in display lists. + * + *

The optimization collapses consecutive MODELVIEW transforms into single MultMatrix + * commands, emitting them at "barriers" (draws, CallList, exit). These tests verify:

+ *
    + *
  • Consecutive transforms collapse to single MultMatrix
  • + *
  • Matrix state is correct after execution
  • + *
  • Push/Pop maintain proper stack semantics
  • + *
  • Barriers (draws, CallList) correctly flush pending transforms
  • + *
+ */ +@ExtendWith(AngelicaExtension.class) +class GLSM_DisplayList_TransformCollapsing_Test { + + private int testList = -1; + private int nestedList = -1; + + @AfterEach + void cleanup() { + if (testList > 0) { + GLStateManager.glDeleteLists(testList, 1); + testList = -1; + } + if (nestedList > 0) { + GLStateManager.glDeleteLists(nestedList, 1); + nestedList = -1; + } + GLStateManager.glLoadIdentity(); + } + + // ==================== Transform Collapsing Tests ==================== + + @Test + void testConsecutiveTransformsCollapseToSingleMultMatrix() { + // Multiple consecutive transforms should collapse to one MultMatrix in optimized list + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // 5 consecutive transforms + GLStateManager.glTranslatef(1.0f, 2.0f, 3.0f); + GLStateManager.glRotatef(45.0f, 0.0f, 1.0f, 0.0f); + GLStateManager.glScalef(2.0f, 2.0f, 2.0f); + GLStateManager.glTranslatef(-0.5f, -0.5f, -0.5f); + GLStateManager.glRotatef(30.0f, 1.0f, 0.0f, 0.0f); + + GLStateManager.glEndList(); + + // Verify optimization: should have collapsed to 1 MultMatrix + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + assertNotNull(compiled, "Display list should be compiled"); + + Map counts = compiled.getCommandCounts(); + int multMatrixCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0); + int individualTransformCount = counts.getOrDefault(GLCommand.TRANSLATE, 0) + + counts.getOrDefault(GLCommand.ROTATE, 0) + + counts.getOrDefault(GLCommand.SCALE, 0); + + assertEquals(1, multMatrixCount, + "5 consecutive transforms should collapse to 1 MultMatrix"); + assertEquals(0, individualTransformCount, + "No individual transform commands should remain in optimized list"); + + // Verify behavioral correctness: matrix state should match expected + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + Matrix4f expected = new Matrix4f() + .translate(1.0f, 2.0f, 3.0f) + .rotate((float) Math.toRadians(45.0f), 0.0f, 1.0f, 0.0f) + .scale(2.0f, 2.0f, 2.0f) + .translate(-0.5f, -0.5f, -0.5f) + .rotate((float) Math.toRadians(30.0f), 1.0f, 0.0f, 0.0f); + + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch after collapsed transforms", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testPushPopPreservesStackSemantics() { + // Push/Pop should maintain proper stack semantics with collapsed transforms + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); // Outer translate + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(5.0f, 0.0f, 0.0f); // Inner translate (isolated) + GLStateManager.glRotatef(90.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glPopMatrix(); + GLStateManager.glTranslatef(0.0f, 3.0f, 0.0f); // After pop + + GLStateManager.glEndList(); + + // Verify optimization: should have Push, Pop, and collapsed MultMatrix commands + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + Map counts = compiled.getCommandCounts(); + + int pushCount = counts.getOrDefault(GLCommand.PUSH_MATRIX, 0); + int popCount = counts.getOrDefault(GLCommand.POP_MATRIX, 0); + + assertEquals(1, pushCount, "Should have exactly 1 PushMatrix"); + assertEquals(1, popCount, "Should have exactly 1 PopMatrix"); + + // Verify behavioral correctness + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Expected: translate(10,0,0) then translate(0,3,0) = translate(10,3,0) + // The inner push/pop isolates the (5,0,0) translate and rotation + Matrix4f expected = new Matrix4f() + .translate(10.0f, 0.0f, 0.0f) + .translate(0.0f, 3.0f, 0.0f); + + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch - push/pop should isolate inner transforms", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testNestedPushPopWithTransforms() { + // Nested push/pop with transforms at each level + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); // Level 0 + GLStateManager.glPushMatrix(); // Enter level 1 + GLStateManager.glTranslatef(2.0f, 0.0f, 0.0f); + GLStateManager.glPushMatrix(); // Enter level 2 + GLStateManager.glTranslatef(3.0f, 0.0f, 0.0f); + GLStateManager.glPopMatrix(); // Back to level 1 + GLStateManager.glTranslatef(4.0f, 0.0f, 0.0f); // At level 1 + GLStateManager.glPopMatrix(); // Back to level 0 + GLStateManager.glTranslatef(5.0f, 0.0f, 0.0f); // At level 0 + + GLStateManager.glEndList(); + + // Verify behavioral correctness + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Final state: translate(1,0,0) + translate(5,0,0) = translate(6,0,0) + // Level 1 and 2 transforms are isolated by push/pop + Matrix4f expected = new Matrix4f() + .translate(1.0f, 0.0f, 0.0f) + .translate(5.0f, 0.0f, 0.0f); + + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + assertEquals(expectedBuf.get(12), actualBuf.get(12), 0.0001f, + "X translation should be 6.0 (1+5, inner transforms isolated)"); + + GLStateManager.glPopMatrix(); // Balance the push from line 185 + } + + @Test + void testCallListFlushesTransform() { + // Create a nested display list + nestedList = GL11.glGenLists(1); + GLStateManager.glNewList(nestedList, GL11.GL_COMPILE); + GLStateManager.glTranslatef(100.0f, 0.0f, 0.0f); // Large translate to verify it's applied + GLStateManager.glEndList(); + + // Create parent list that transforms then calls nested list + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); // Before CallList + GLStateManager.glRotatef(90.0f, 0.0f, 1.0f, 0.0f); + GLStateManager.glCallList(nestedList); // Barrier - should flush transforms + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); // After CallList + + GLStateManager.glEndList(); + + // Verify optimization: MultMatrix should be emitted before CallList + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + List opcodes = compiled.getCommandOpcodes(); + + // Find positions of MultMatrix and CallList + int multMatrixIndex = opcodes.indexOf(GLCommand.MULT_MATRIX); + int callListIndex = opcodes.indexOf(GLCommand.CALL_LIST); + + assertTrue(multMatrixIndex >= 0, "Should have MultMatrix command"); + assertTrue(callListIndex >= 0, "Should have CallList command"); + assertTrue(multMatrixIndex < callListIndex, + "MultMatrix should be emitted BEFORE CallList (transform flush at barrier)"); + + // Verify behavioral correctness + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Build expected: translate(10,0,0) * rotate(90,Y) * translate(100,0,0) * translate(1,0,0) + // After 90-degree Y rotation, X axis points into -Z, so nested translate goes along -Z + Matrix4f expected = new Matrix4f() + .translate(10.0f, 0.0f, 0.0f) + .rotate((float) Math.toRadians(90.0f), 0.0f, 1.0f, 0.0f) + .translate(100.0f, 0.0f, 0.0f) + .translate(1.0f, 0.0f, 0.0f); + + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch - nested list should see parent's transformed state", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testIdentityTransformNotEmitted() { + // A display list with only identity-equivalent transforms should not emit MultMatrix + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // These cancel out to identity + GLStateManager.glTranslatef(5.0f, 0.0f, 0.0f); + GLStateManager.glTranslatef(-5.0f, 0.0f, 0.0f); + + GLStateManager.glEndList(); + + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + Map counts = compiled.getCommandCounts(); + + int multMatrixCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0); + + assertEquals(0, multMatrixCount, + "Identity-equivalent transforms should not emit MultMatrix"); + } + + @Test + void testTransformBetweenDrawsCreatesSeparateBatches() { + // This test verifies that draws with different transforms are not batched together + // Note: Without actual Tessellator integration, we can only verify command structure + + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // Simulate: Transform A, Draw, Transform B, Draw + // The draws should NOT be in the same batch + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); + // (Would have a draw here in real usage) + GLStateManager.glTranslatef(2.0f, 0.0f, 0.0f); + // (Would have another draw here) + + GLStateManager.glEndList(); + + // Verify the transforms are accumulated correctly + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Total translation should be 3.0 (1+2) + assertEquals(3.0f, actualBuf.get(12), 0.0001f, + "Consecutive translates should accumulate"); + + GLStateManager.glPopMatrix(); + } + + @Test + void testLoadIdentityResetsAccumulation() { + // LoadIdentity should reset accumulated transforms + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + GLStateManager.glTranslatef(100.0f, 100.0f, 100.0f); // Big translate + GLStateManager.glLoadIdentity(); // Reset + GLStateManager.glTranslatef(1.0f, 2.0f, 3.0f); // New translate + + GLStateManager.glEndList(); + + // Verify: final state should only have the second translate + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + assertEquals(1.0f, actualBuf.get(12), 0.0001f, "X should be 1.0 (LoadIdentity reset)"); + assertEquals(2.0f, actualBuf.get(13), 0.0001f, "Y should be 2.0 (LoadIdentity reset)"); + assertEquals(3.0f, actualBuf.get(14), 0.0001f, "Z should be 3.0 (LoadIdentity reset)"); + + GLStateManager.glPopMatrix(); + } + + @Test + void testMultMatrixAccumulation() { + // MultMatrix should be accumulated like other transforms + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); + + // Create a rotation matrix and multiply + Matrix4f rotMatrix = new Matrix4f().rotate((float) Math.toRadians(90.0f), 0.0f, 1.0f, 0.0f); + FloatBuffer rotBuf = BufferUtils.createFloatBuffer(16); + rotMatrix.get(rotBuf); + rotBuf.rewind(); + GLStateManager.glMultMatrix(rotBuf); + + GLStateManager.glTranslatef(2.0f, 0.0f, 0.0f); + + GLStateManager.glEndList(); + + // Verify optimization: should collapse to 1 MultMatrix + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + Map counts = compiled.getCommandCounts(); + + int multMatrixCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0); + + assertEquals(1, multMatrixCount, + "Translate + MultMatrix + Translate should collapse to 1 MultMatrix"); + + // Verify behavioral correctness + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + Matrix4f expected = new Matrix4f() + .translate(1.0f, 0.0f, 0.0f) + .rotate((float) Math.toRadians(90.0f), 0.0f, 1.0f, 0.0f) + .translate(2.0f, 0.0f, 0.0f); + + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] mismatch for MultMatrix accumulation", i)); + } + + GLStateManager.glPopMatrix(); + } + + // ==================== Batcher Flush Ordering Tests ==================== + + @Test + void testDrawsBeforePushMatrixAreEmittedBeforePush() { + // This test verifies that draws occurring before a PushMatrix are flushed + // before the PushMatrix command, not delayed until later. + // This was a bug where the batcher wasn't flushed at PushMatrix. + + // Create a list with: Transform, (implicit draw would go here), PushMatrix, Transform, PopMatrix + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // Outer transform + GLStateManager.glTranslatef(1.0f, 0.0f, 0.0f); + // Note: If there was a draw here, it should be emitted BEFORE PushMatrix + + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(5.0f, 0.0f, 0.0f); // Inner transform + GLStateManager.glPopMatrix(); + + // Final transform after pop + GLStateManager.glTranslatef(2.0f, 0.0f, 0.0f); + + GLStateManager.glEndList(); + + // Verify the command structure + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + assertNotNull(compiled, "Display list should be compiled"); + + List opcodes = compiled.getCommandOpcodes(); + + // Find indices of key commands + int pushIndex = opcodes.indexOf(GLCommand.PUSH_MATRIX); + int popIndex = opcodes.indexOf(GLCommand.POP_MATRIX); + + // Count MultMatrix commands before first PushMatrix + int multMatrixBeforePushCount = 0; + for (int i = 0; i < opcodes.size() && i < pushIndex; i++) { + if (opcodes.get(i) == GLCommand.MULT_MATRIX) { + multMatrixBeforePushCount++; + } + } + + assertTrue(pushIndex >= 0, "Should have PushMatrix command"); + assertTrue(popIndex >= 0, "Should have PopMatrix command"); + assertTrue(pushIndex < popIndex, "PushMatrix should come before PopMatrix"); + + // The outer translate(1,0,0) should be emitted as MultMatrix before PushMatrix + assertTrue(multMatrixBeforePushCount >= 1, + "Transform before PushMatrix should be emitted before the Push"); + + // Verify behavioral correctness: final state should be translate(1,0,0) + translate(2,0,0) = translate(3,0,0) + // The inner translate(5,0,0) is isolated by push/pop + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + assertEquals(3.0f, actualBuf.get(12), 0.0001f, + "X translation should be 3.0 (1+2, inner transform isolated by push/pop)"); + + GLStateManager.glPopMatrix(); + } + + @Test + void testDrawsBeforePopMatrixAreEmittedBeforePop() { + // Verify draws inside a push/pop block are emitted before the pop + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); + // Draws here should be emitted before PopMatrix + GLStateManager.glPopMatrix(); + + GLStateManager.glEndList(); + + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + List opcodes = compiled.getCommandOpcodes(); + + int pushIndex = opcodes.indexOf(GLCommand.PUSH_MATRIX); + int popIndex = opcodes.indexOf(GLCommand.POP_MATRIX); + + assertTrue(pushIndex >= 0, "Should have PushMatrix"); + assertTrue(popIndex >= 0, "Should have PopMatrix"); + assertTrue(pushIndex < popIndex, "Push should come before Pop"); + + // The inner translate should be emitted between push and pop (or collapsed to residual) + // At minimum, the structure should be valid + assertTrue(popIndex > pushIndex, "PopMatrix should follow PushMatrix"); + } + + // ==================== LoadMatrix Tests ==================== + + @Test + void testLoadMatrixSetsAbsoluteTransform() { + // LoadMatrix should set an absolute matrix, not multiply + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // First, apply a large transform + GLStateManager.glTranslatef(100.0f, 200.0f, 300.0f); + + // Then load a specific matrix (should replace, not multiply) + Matrix4f loadedMatrix = new Matrix4f() + .translate(1.0f, 2.0f, 3.0f) + .rotate((float) Math.toRadians(45.0f), 0.0f, 1.0f, 0.0f); + FloatBuffer loadBuf = BufferUtils.createFloatBuffer(16); + loadedMatrix.get(loadBuf); + loadBuf.rewind(); + GLStateManager.glLoadMatrix(loadBuf); + + GLStateManager.glEndList(); + + // Verify the LoadMatrixCmd is present + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + assertNotNull(compiled, "Display list should be compiled"); + + Map counts = compiled.getCommandCounts(); + int loadMatrixCount = counts.getOrDefault(GLCommand.LOAD_MATRIX, 0); + + assertEquals(1, loadMatrixCount, + "Should have exactly 1 LoadMatrix command (can't be absorbed)"); + + // Verify behavioral correctness: matrix should equal the loaded matrix + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + loadedMatrix.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] should match loaded matrix (not multiplied)", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testLoadMatrixResetsRelativeTransform() { + // After LoadMatrix, subsequent transforms should be relative to the loaded matrix + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // Load a rotation matrix + Matrix4f loadedMatrix = new Matrix4f() + .rotate((float) Math.toRadians(90.0f), 0.0f, 1.0f, 0.0f); + FloatBuffer loadBuf = BufferUtils.createFloatBuffer(16); + loadedMatrix.get(loadBuf); + loadBuf.rewind(); + GLStateManager.glLoadMatrix(loadBuf); + + // Then translate - this should be relative to the loaded matrix + GLStateManager.glTranslatef(10.0f, 0.0f, 0.0f); + + GLStateManager.glEndList(); + + // Verify behavioral correctness + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + // Expected: loadedMatrix * translate(10,0,0) + Matrix4f expected = new Matrix4f(loadedMatrix).translate(10.0f, 0.0f, 0.0f); + FloatBuffer expectedBuf = BufferUtils.createFloatBuffer(16); + expected.get(expectedBuf); + + for (int i = 0; i < 16; i++) { + assertEquals(expectedBuf.get(i), actualBuf.get(i), 0.0001f, + String.format("Matrix[%d] - transform after LoadMatrix should be relative to loaded matrix", i)); + } + + GLStateManager.glPopMatrix(); + } + + @Test + void testLoadMatrixThenLoadIdentity() { + // LoadMatrix followed by LoadIdentity should result in identity + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // Load a complex matrix + Matrix4f loadedMatrix = new Matrix4f() + .translate(50.0f, 60.0f, 70.0f) + .rotate((float) Math.toRadians(45.0f), 1.0f, 1.0f, 0.0f); + FloatBuffer loadBuf = BufferUtils.createFloatBuffer(16); + loadedMatrix.get(loadBuf); + loadBuf.rewind(); + GLStateManager.glLoadMatrix(loadBuf); + + // Then reset to identity + GLStateManager.glLoadIdentity(); + + // Then apply a simple translate + GLStateManager.glTranslatef(1.0f, 2.0f, 3.0f); + + GLStateManager.glEndList(); + + // Verify: final state should just be translate(1,2,3) + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + assertEquals(1.0f, actualBuf.get(12), 0.0001f, "X should be 1.0"); + assertEquals(2.0f, actualBuf.get(13), 0.0001f, "Y should be 2.0"); + assertEquals(3.0f, actualBuf.get(14), 0.0001f, "Z should be 3.0"); + + GLStateManager.glPopMatrix(); + } + + @Test + void testMultipleLoadMatrixCalls() { + // Multiple LoadMatrix calls should each be recorded + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + // First load + Matrix4f matrix1 = new Matrix4f().translate(10.0f, 0.0f, 0.0f); + FloatBuffer buf1 = BufferUtils.createFloatBuffer(16); + matrix1.get(buf1); + buf1.rewind(); + GLStateManager.glLoadMatrix(buf1); + + // Second load (replaces first) + Matrix4f matrix2 = new Matrix4f().translate(0.0f, 20.0f, 0.0f); + FloatBuffer buf2 = BufferUtils.createFloatBuffer(16); + matrix2.get(buf2); + buf2.rewind(); + GLStateManager.glLoadMatrix(buf2); + + GLStateManager.glEndList(); + + // Verify: final state should be matrix2 + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + GLStateManager.glCallList(testList); + + FloatBuffer actualBuf = BufferUtils.createFloatBuffer(16); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, actualBuf); + + assertEquals(0.0f, actualBuf.get(12), 0.0001f, "X should be 0.0 (from second LoadMatrix)"); + assertEquals(20.0f, actualBuf.get(13), 0.0001f, "Y should be 20.0 (from second LoadMatrix)"); + + GLStateManager.glPopMatrix(); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_UnitTest.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_UnitTest.java index ea6f7a7a1..1516052ad 100644 --- a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_UnitTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DisplayList_UnitTest.java @@ -1,11 +1,13 @@ package com.gtnewhorizons.angelica.glsm; import com.gtnewhorizons.angelica.AngelicaExtension; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.lwjgl.opengl.GL11; import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState; +import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(AngelicaExtension.class) class GLSM_DisplayList_UnitTest { @@ -43,4 +45,114 @@ class GLSM_DisplayList_UnitTest { // Reset state that we changed during test GLStateManager.glDisable(GL11.GL_LIGHTING); } + + // ==================== CullFace Tests ==================== + + @Test + void testCullFaceInDisplayList() { + // Get initial cull face mode + int initialMode = GL11.glGetInteger(GL11.GL_CULL_FACE_MODE); + + int list = GL11.glGenLists(1); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glCullFace(GL11.GL_FRONT); + GLStateManager.glEndList(); + + // After compile, mode should be unchanged + assertEquals(initialMode, GL11.glGetInteger(GL11.GL_CULL_FACE_MODE), "CullFace mode should be unchanged after GL_COMPILE"); + + // Call the display list + GLStateManager.glCallList(list); + + // Now mode should be GL_FRONT + assertEquals(GL11.GL_FRONT, GL11.glGetInteger(GL11.GL_CULL_FACE_MODE), "CullFace mode should be GL_FRONT after calling display list"); + + // Cleanup + GLStateManager.glCullFace(GL11.GL_BACK); // Reset to default + GLStateManager.glDeleteLists(list, 1); + } + + @Test + void testCullFaceCompileAndExecute() { + // Get initial cull face mode + int initialMode = GL11.glGetInteger(GL11.GL_CULL_FACE_MODE); + + int list = GL11.glGenLists(1); + GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE); + GLStateManager.glCullFace(GL11.GL_FRONT_AND_BACK); + GLStateManager.glEndList(); + + // After compile and execute, mode should be changed + assertEquals(GL11.GL_FRONT_AND_BACK, GL11.glGetInteger(GL11.GL_CULL_FACE_MODE), "CullFace mode should be GL_FRONT_AND_BACK after GL_COMPILE_AND_EXECUTE"); + + // Cleanup + GLStateManager.glCullFace(GL11.GL_BACK); + GLStateManager.glDeleteLists(list, 1); + } + + // ==================== LogicOp Tests ==================== + + @Test + void testLogicOpInDisplayList() { + // Get initial logic op + int initialOp = GL11.glGetInteger(GL11.GL_LOGIC_OP_MODE); + + int list = GL11.glGenLists(1); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glLogicOp(GL11.GL_XOR); + GLStateManager.glEndList(); + + // After compile, op should be unchanged + assertEquals(initialOp, GL11.glGetInteger(GL11.GL_LOGIC_OP_MODE), "LogicOp should be unchanged after GL_COMPILE"); + + // Call the display list + GLStateManager.glCallList(list); + + // Now op should be GL_XOR + assertEquals(GL11.GL_XOR, GL11.glGetInteger(GL11.GL_LOGIC_OP_MODE), "LogicOp should be GL_XOR after calling display list"); + + // Cleanup + GLStateManager.glLogicOp(GL11.GL_COPY); // Reset to default + GLStateManager.glDeleteLists(list, 1); + } + + @Test + void testLogicOpCompileAndExecute() { + int list = GL11.glGenLists(1); + GLStateManager.glNewList(list, GL11.GL_COMPILE_AND_EXECUTE); + GLStateManager.glLogicOp(GL11.GL_INVERT); + GLStateManager.glEndList(); + + // After compile and execute, op should be changed + assertEquals(GL11.GL_INVERT, GL11.glGetInteger(GL11.GL_LOGIC_OP_MODE), "LogicOp should be GL_INVERT after GL_COMPILE_AND_EXECUTE"); + + // Cleanup + GLStateManager.glLogicOp(GL11.GL_COPY); + GLStateManager.glDeleteLists(list, 1); + } + + @Test + void testMultipleStateChangesInDisplayList() { + // Test that multiple state commands work together + int list = GL11.glGenLists(1); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + GLStateManager.glCullFace(GL11.GL_FRONT); + GLStateManager.glLogicOp(GL11.GL_AND); + GLStateManager.glEnable(GL11.GL_LIGHTING); + GLStateManager.glEndList(); + + // Call the list + GLStateManager.glCallList(list); + + // Verify all states changed + assertEquals(GL11.GL_FRONT, GL11.glGetInteger(GL11.GL_CULL_FACE_MODE), "CullFace should be GL_FRONT"); + assertEquals(GL11.GL_AND, GL11.glGetInteger(GL11.GL_LOGIC_OP_MODE), "LogicOp should be GL_AND"); + verifyState(GL11.GL_LIGHTING, true, "GL_LIGHTING should be enabled"); + + // Cleanup + GLStateManager.glCullFace(GL11.GL_BACK); + GLStateManager.glLogicOp(GL11.GL_COPY); + GLStateManager.glDisable(GL11.GL_LIGHTING); + GLStateManager.glDeleteLists(list, 1); + } } diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PushPop_UnitTest.java b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PushPop_UnitTest.java index e25af534c..6381dbae9 100644 --- a/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PushPop_UnitTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PushPop_UnitTest.java @@ -12,6 +12,7 @@ import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL32; import java.nio.FloatBuffer; import java.util.ArrayList; @@ -516,4 +517,356 @@ class GLSM_PushPop_UnitTest { verifyState(GL11.GL_VIEWPORT, new int[] { 0, 0, 800, 600 }, "Viewport - Reset"); verifyState(GL11.GL_DEPTH_RANGE, new float[] { 0.0f, 1.0f }, "Depth Range - Reset"); } + + @Test + void testPushPopLineBit() { + /* + * GL_LINE_BIT + * GL_LINE_SMOOTH enable bit + * GL_LINE_STIPPLE enable bit + * Line stipple pattern and repeat counter + * Line width + */ + verifyState(GL11.GL_LINE_WIDTH, 1.0f, "Line Width - Initial"); + verifyState(GL11.GL_LINE_STIPPLE_PATTERN, (short) 0xFFFF, "Line Stipple Pattern - Initial"); + verifyState(GL11.GL_LINE_STIPPLE_REPEAT, 1, "Line Stipple Repeat - Initial"); + verifyIsEnabled(GL11.GL_LINE_SMOOTH, false, "Line Smooth - Initial"); + verifyIsEnabled(GL11.GL_LINE_STIPPLE, false, "Line Stipple - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_LINE_BIT); + GLStateManager.glLineWidth(2.5f); + GLStateManager.glLineStipple(3, (short) 0xAAAA); + GLStateManager.glEnable(GL11.GL_LINE_SMOOTH); + GLStateManager.glEnable(GL11.GL_LINE_STIPPLE); + + verifyState(GL11.GL_LINE_WIDTH, 2.5f, "Line Width"); + verifyState(GL11.GL_LINE_STIPPLE_PATTERN, (short) 0xAAAA, "Line Stipple Pattern"); + verifyState(GL11.GL_LINE_STIPPLE_REPEAT, 3, "Line Stipple Repeat"); + verifyIsEnabled(GL11.GL_LINE_SMOOTH, true, "Line Smooth"); + verifyIsEnabled(GL11.GL_LINE_STIPPLE, true, "Line Stipple"); + + GLStateManager.glPopAttrib(); + verifyState(GL11.GL_LINE_WIDTH, 1.0f, "Line Width - Reset"); + verifyState(GL11.GL_LINE_STIPPLE_PATTERN, (short) 0xFFFF, "Line Stipple Pattern - Reset"); + verifyState(GL11.GL_LINE_STIPPLE_REPEAT, 1, "Line Stipple Repeat - Reset"); + verifyIsEnabled(GL11.GL_LINE_SMOOTH, false, "Line Smooth - Reset"); + verifyIsEnabled(GL11.GL_LINE_STIPPLE, false, "Line Stipple - Reset"); + } + + @Test + void testPushPopPointBit() { + /* + * GL_POINT_BIT + * GL_POINT_SMOOTH enable bit + * Point size + */ + verifyState(GL11.GL_POINT_SIZE, 1.0f, "Point Size - Initial"); + verifyIsEnabled(GL11.GL_POINT_SMOOTH, false, "Point Smooth - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_POINT_BIT); + GLStateManager.glPointSize(5.0f); + GLStateManager.glEnable(GL11.GL_POINT_SMOOTH); + + verifyState(GL11.GL_POINT_SIZE, 5.0f, "Point Size"); + verifyIsEnabled(GL11.GL_POINT_SMOOTH, true, "Point Smooth"); + + GLStateManager.glPopAttrib(); + verifyState(GL11.GL_POINT_SIZE, 1.0f, "Point Size - Reset"); + verifyIsEnabled(GL11.GL_POINT_SMOOTH, false, "Point Smooth - Reset"); + } + + @Test + void testPushPopPolygonBit() { + /* + * GL_POLYGON_BIT + * GL_CULL_FACE enable bit + * GL_CULL_FACE_MODE setting + * GL_FRONT_FACE setting + * GL_POLYGON_MODE setting + * GL_POLYGON_SMOOTH enable bit + * GL_POLYGON_STIPPLE enable bit + * GL_POLYGON_OFFSET_FILL enable bit + * GL_POLYGON_OFFSET_LINE enable bit + * GL_POLYGON_OFFSET_POINT enable bit + * GL_POLYGON_OFFSET_FACTOR + * GL_POLYGON_OFFSET_UNITS + */ + verifyIsEnabled(GL11.GL_CULL_FACE, false, "Cull Face - Initial"); + verifyState(GL11.GL_CULL_FACE_MODE, GL11.GL_BACK, "Cull Face Mode - Initial"); + verifyState(GL11.GL_FRONT_FACE, GL11.GL_CCW, "Front Face - Initial"); + verifyState(GL11.GL_POLYGON_MODE, new int[] { GL11.GL_FILL, GL11.GL_FILL }, "Polygon Mode - Initial"); + verifyIsEnabled(GL11.GL_POLYGON_SMOOTH, false, "Polygon Smooth - Initial"); + verifyIsEnabled(GL11.GL_POLYGON_STIPPLE, false, "Polygon Stipple - Initial"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_FILL, false, "Polygon Offset Fill - Initial"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_LINE, false, "Polygon Offset Line - Initial"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_POINT, false, "Polygon Offset Point - Initial"); + verifyState(GL11.GL_POLYGON_OFFSET_FACTOR, 0.0f, "Polygon Offset Factor - Initial"); + verifyState(GL11.GL_POLYGON_OFFSET_UNITS, 0.0f, "Polygon Offset Units - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_POLYGON_BIT); + GLStateManager.glEnable(GL11.GL_CULL_FACE); + GLStateManager.glCullFace(GL11.GL_FRONT); + GLStateManager.glFrontFace(GL11.GL_CW); + GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE); + GLStateManager.glEnable(GL11.GL_POLYGON_SMOOTH); + GLStateManager.glEnable(GL11.GL_POLYGON_STIPPLE); + GLStateManager.glEnable(GL11.GL_POLYGON_OFFSET_FILL); + GLStateManager.glEnable(GL11.GL_POLYGON_OFFSET_LINE); + GLStateManager.glEnable(GL11.GL_POLYGON_OFFSET_POINT); + GLStateManager.glPolygonOffset(1.5f, 2.0f); + + verifyIsEnabled(GL11.GL_CULL_FACE, true, "Cull Face"); + verifyState(GL11.GL_CULL_FACE_MODE, GL11.GL_FRONT, "Cull Face Mode"); + verifyState(GL11.GL_FRONT_FACE, GL11.GL_CW, "Front Face"); + verifyState(GL11.GL_POLYGON_MODE, new int[] { GL11.GL_LINE, GL11.GL_LINE }, "Polygon Mode"); + verifyIsEnabled(GL11.GL_POLYGON_SMOOTH, true, "Polygon Smooth"); + verifyIsEnabled(GL11.GL_POLYGON_STIPPLE, true, "Polygon Stipple"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_FILL, true, "Polygon Offset Fill"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_LINE, true, "Polygon Offset Line"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_POINT, true, "Polygon Offset Point"); + verifyState(GL11.GL_POLYGON_OFFSET_FACTOR, 1.5f, "Polygon Offset Factor"); + verifyState(GL11.GL_POLYGON_OFFSET_UNITS, 2.0f, "Polygon Offset Units"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_CULL_FACE, false, "Cull Face - Reset"); + verifyState(GL11.GL_CULL_FACE_MODE, GL11.GL_BACK, "Cull Face Mode - Reset"); + verifyState(GL11.GL_FRONT_FACE, GL11.GL_CCW, "Front Face - Reset"); + verifyState(GL11.GL_POLYGON_MODE, new int[] { GL11.GL_FILL, GL11.GL_FILL }, "Polygon Mode - Reset"); + verifyIsEnabled(GL11.GL_POLYGON_SMOOTH, false, "Polygon Smooth - Reset"); + verifyIsEnabled(GL11.GL_POLYGON_STIPPLE, false, "Polygon Stipple - Reset"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_FILL, false, "Polygon Offset Fill - Reset"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_LINE, false, "Polygon Offset Line - Reset"); + verifyIsEnabled(GL11.GL_POLYGON_OFFSET_POINT, false, "Polygon Offset Point - Reset"); + verifyState(GL11.GL_POLYGON_OFFSET_FACTOR, 0.0f, "Polygon Offset Factor - Reset"); + verifyState(GL11.GL_POLYGON_OFFSET_UNITS, 0.0f, "Polygon Offset Units - Reset"); + } + + @Test + void testPushPopStencilBufferBit() { + /* + * GL_STENCIL_BUFFER_BIT + * GL_STENCIL_TEST enable bit + * Stencil function and reference value + * Stencil value mask + * Stencil fail, pass, and depth buffer pass actions + * Stencil buffer clear value + * Stencil buffer writemask + */ + // The default stencil mask is clamped to the stencil buffer bit depth + // Query the actual initial mask from GL to get the hardware-specific value + final int initialStencilMask = GL11.glGetInteger(GL11.GL_STENCIL_VALUE_MASK); + final int initialWriteMask = GL11.glGetInteger(GL11.GL_STENCIL_WRITEMASK); + + verifyIsEnabled(GL11.GL_STENCIL_TEST, false, "Stencil Test - Initial"); + verifyState(GL11.GL_STENCIL_FUNC, GL11.GL_ALWAYS, "Stencil Func - Initial"); + verifyState(GL11.GL_STENCIL_REF, 0, "Stencil Ref - Initial"); + verifyState(GL11.GL_STENCIL_VALUE_MASK, initialStencilMask, "Stencil Value Mask - Initial"); + verifyState(GL11.GL_STENCIL_FAIL, GL11.GL_KEEP, "Stencil Fail - Initial"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_FAIL, GL11.GL_KEEP, "Stencil Pass Depth Fail - Initial"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_PASS, GL11.GL_KEEP, "Stencil Pass Depth Pass - Initial"); + verifyState(GL11.GL_STENCIL_CLEAR_VALUE, 0, "Stencil Clear Value - Initial"); + verifyState(GL11.GL_STENCIL_WRITEMASK, initialWriteMask, "Stencil Write Mask - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_STENCIL_BUFFER_BIT); + GLStateManager.glEnable(GL11.GL_STENCIL_TEST); + GLStateManager.glStencilFunc(GL11.GL_LESS, 127, 0x7F); + GLStateManager.glStencilOp(GL11.GL_INCR, GL11.GL_DECR, GL11.GL_REPLACE); + GLStateManager.glStencilMask(0x0F); + GLStateManager.glClearStencil(64); + + verifyIsEnabled(GL11.GL_STENCIL_TEST, true, "Stencil Test"); + verifyState(GL11.GL_STENCIL_FUNC, GL11.GL_LESS, "Stencil Func"); + verifyState(GL11.GL_STENCIL_REF, 127, "Stencil Ref"); + verifyState(GL11.GL_STENCIL_VALUE_MASK, 0x7F, "Stencil Value Mask"); + verifyState(GL11.GL_STENCIL_FAIL, GL11.GL_INCR, "Stencil Fail"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_FAIL, GL11.GL_DECR, "Stencil Pass Depth Fail"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_PASS, GL11.GL_REPLACE, "Stencil Pass Depth Pass"); + verifyState(GL11.GL_STENCIL_CLEAR_VALUE, 64, "Stencil Clear Value"); + verifyState(GL11.GL_STENCIL_WRITEMASK, 0x0F, "Stencil Write Mask"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_STENCIL_TEST, false, "Stencil Test - Reset"); + verifyState(GL11.GL_STENCIL_FUNC, GL11.GL_ALWAYS, "Stencil Func - Reset"); + verifyState(GL11.GL_STENCIL_REF, 0, "Stencil Ref - Reset"); + verifyState(GL11.GL_STENCIL_VALUE_MASK, initialStencilMask, "Stencil Value Mask - Reset"); + verifyState(GL11.GL_STENCIL_FAIL, GL11.GL_KEEP, "Stencil Fail - Reset"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_FAIL, GL11.GL_KEEP, "Stencil Pass Depth Fail - Reset"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_PASS, GL11.GL_KEEP, "Stencil Pass Depth Pass - Reset"); + verifyState(GL11.GL_STENCIL_CLEAR_VALUE, 0, "Stencil Clear Value - Reset"); + verifyState(GL11.GL_STENCIL_WRITEMASK, initialWriteMask, "Stencil Write Mask - Reset"); + } + + @Test + void testPushPopStencilBufferBitSeparate() { + /* + * GL_STENCIL_BUFFER_BIT with GL 2.0 separate front/back state + */ + // The default stencil mask is clamped to the stencil buffer bit depth + // Query the actual initial masks from GL to get the hardware-specific values + final int initialFrontMask = GL11.glGetInteger(GL11.GL_STENCIL_VALUE_MASK); + final int initialFrontWriteMask = GL11.glGetInteger(GL11.GL_STENCIL_WRITEMASK); + final int initialBackMask = GL11.glGetInteger(GL20.GL_STENCIL_BACK_VALUE_MASK); + final int initialBackWriteMask = GL11.glGetInteger(GL20.GL_STENCIL_BACK_WRITEMASK); + + // GL 2.0 separate stencil queries + verifyState(GL20.GL_STENCIL_BACK_FUNC, GL11.GL_ALWAYS, "Stencil Back Func - Initial"); + verifyState(GL20.GL_STENCIL_BACK_REF, 0, "Stencil Back Ref - Initial"); + verifyState(GL20.GL_STENCIL_BACK_VALUE_MASK, initialBackMask, "Stencil Back Value Mask - Initial"); + verifyState(GL20.GL_STENCIL_BACK_FAIL, GL11.GL_KEEP, "Stencil Back Fail - Initial"); + verifyState(GL20.GL_STENCIL_BACK_PASS_DEPTH_FAIL, GL11.GL_KEEP, "Stencil Back Pass Depth Fail - Initial"); + verifyState(GL20.GL_STENCIL_BACK_PASS_DEPTH_PASS, GL11.GL_KEEP, "Stencil Back Pass Depth Pass - Initial"); + verifyState(GL20.GL_STENCIL_BACK_WRITEMASK, initialBackWriteMask, "Stencil Back Write Mask - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_STENCIL_BUFFER_BIT); + // Use separate functions to set different front/back state + GLStateManager.glStencilFuncSeparate(GL11.GL_FRONT, GL11.GL_LESS, 10, 0x0A); + GLStateManager.glStencilFuncSeparate(GL11.GL_BACK, GL11.GL_GREATER, 20, 0x0B); + GLStateManager.glStencilOpSeparate(GL11.GL_FRONT, GL11.GL_INCR, GL11.GL_DECR, GL11.GL_ZERO); + GLStateManager.glStencilOpSeparate(GL11.GL_BACK, GL11.GL_KEEP, GL11.GL_INVERT, GL11.GL_REPLACE); + GLStateManager.glStencilMaskSeparate(GL11.GL_FRONT, 0x11); + GLStateManager.glStencilMaskSeparate(GL11.GL_BACK, 0x22); + + // Verify front state + verifyState(GL11.GL_STENCIL_FUNC, GL11.GL_LESS, "Stencil Front Func"); + verifyState(GL11.GL_STENCIL_REF, 10, "Stencil Front Ref"); + verifyState(GL11.GL_STENCIL_VALUE_MASK, 0x0A, "Stencil Front Value Mask"); + verifyState(GL11.GL_STENCIL_FAIL, GL11.GL_INCR, "Stencil Front Fail"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_FAIL, GL11.GL_DECR, "Stencil Front Pass Depth Fail"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_PASS, GL11.GL_ZERO, "Stencil Front Pass Depth Pass"); + verifyState(GL11.GL_STENCIL_WRITEMASK, 0x11, "Stencil Front Write Mask"); + + // Verify back state + verifyState(GL20.GL_STENCIL_BACK_FUNC, GL11.GL_GREATER, "Stencil Back Func"); + verifyState(GL20.GL_STENCIL_BACK_REF, 20, "Stencil Back Ref"); + verifyState(GL20.GL_STENCIL_BACK_VALUE_MASK, 0x0B, "Stencil Back Value Mask"); + verifyState(GL20.GL_STENCIL_BACK_FAIL, GL11.GL_KEEP, "Stencil Back Fail"); + verifyState(GL20.GL_STENCIL_BACK_PASS_DEPTH_FAIL, GL11.GL_INVERT, "Stencil Back Pass Depth Fail"); + verifyState(GL20.GL_STENCIL_BACK_PASS_DEPTH_PASS, GL11.GL_REPLACE, "Stencil Back Pass Depth Pass"); + verifyState(GL20.GL_STENCIL_BACK_WRITEMASK, 0x22, "Stencil Back Write Mask"); + + GLStateManager.glPopAttrib(); + // Verify front state reset + verifyState(GL11.GL_STENCIL_FUNC, GL11.GL_ALWAYS, "Stencil Front Func - Reset"); + verifyState(GL11.GL_STENCIL_REF, 0, "Stencil Front Ref - Reset"); + verifyState(GL11.GL_STENCIL_VALUE_MASK, initialFrontMask, "Stencil Front Value Mask - Reset"); + verifyState(GL11.GL_STENCIL_FAIL, GL11.GL_KEEP, "Stencil Front Fail - Reset"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_FAIL, GL11.GL_KEEP, "Stencil Front Pass Depth Fail - Reset"); + verifyState(GL11.GL_STENCIL_PASS_DEPTH_PASS, GL11.GL_KEEP, "Stencil Front Pass Depth Pass - Reset"); + verifyState(GL11.GL_STENCIL_WRITEMASK, initialFrontWriteMask, "Stencil Front Write Mask - Reset"); + + // Verify back state reset + verifyState(GL20.GL_STENCIL_BACK_FUNC, GL11.GL_ALWAYS, "Stencil Back Func - Reset"); + verifyState(GL20.GL_STENCIL_BACK_REF, 0, "Stencil Back Ref - Reset"); + verifyState(GL20.GL_STENCIL_BACK_VALUE_MASK, initialBackMask, "Stencil Back Value Mask - Reset"); + verifyState(GL20.GL_STENCIL_BACK_FAIL, GL11.GL_KEEP, "Stencil Back Fail - Reset"); + verifyState(GL20.GL_STENCIL_BACK_PASS_DEPTH_FAIL, GL11.GL_KEEP, "Stencil Back Pass Depth Fail - Reset"); + verifyState(GL20.GL_STENCIL_BACK_PASS_DEPTH_PASS, GL11.GL_KEEP, "Stencil Back Pass Depth Pass - Reset"); + verifyState(GL20.GL_STENCIL_BACK_WRITEMASK, initialBackWriteMask, "Stencil Back Write Mask - Reset"); + } + + // ==================== Lazy Copy-on-Write Tests ==================== + + @Test + void testNestedPushPopEnableBit_ModifyAtBothLevels() { + // Test: modify same state at both depth levels + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - Initial"); + verifyIsEnabled(GL11.GL_LIGHTING, false, "Lighting - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.enableBlend(); + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - After first enable"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.disableBlend(); + GLStateManager.enableLighting(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After second push disable"); + verifyIsEnabled(GL11.GL_LIGHTING, true, "Lighting - After enable at depth 2"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - After first pop (should restore to depth 1 value)"); + verifyIsEnabled(GL11.GL_LIGHTING, false, "Lighting - After first pop (should restore to depth 1 value)"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After second pop (should restore to initial)"); + verifyIsEnabled(GL11.GL_LIGHTING, false, "Lighting - After second pop (should restore to initial)"); + } + + @Test + void testNestedPushPopEnableBit_ModifyOnlyAtInnerLevel() { + // Test: modify state only at depth 2, not at depth 1 + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + // Don't modify blend at depth 1 + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.enableBlend(); + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - After enable at depth 2"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After pop to depth 1 (should restore)"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After pop to depth 0 (should still be false)"); + } + + @Test + void testNestedPushPopEnableBit_ModifyOnlyAtOuterLevel() { + // Test: modify state only at depth 1, not at depth 2 + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.enableBlend(); + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - After enable at depth 1"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + // Don't modify blend at depth 2 + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - At depth 2 (unchanged)"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - After pop to depth 1 (should still be true)"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After pop to depth 0 (should restore to initial)"); + } + + @Test + void testMultipleModificationsAtSameDepth() { + // Test: multiple modifications to same state at same depth should only save once + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.enableBlend(); // First modification - should save false + GLStateManager.disableBlend(); // Second modification - should NOT save again + GLStateManager.enableBlend(); // Third modification - should NOT save again + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - After multiple toggles"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After pop (should restore original false)"); + } + + @Test + void testTripleNestedPushPop() { + // Test: three levels of nesting + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.enableBlend(); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.disableBlend(); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.enableBlend(); + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - At depth 3"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After pop to depth 2"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, true, "Blend - After pop to depth 1"); + + GLStateManager.glPopAttrib(); + verifyIsEnabled(GL11.GL_BLEND, false, "Blend - After pop to depth 0"); + } } diff --git a/src/test/java/com/gtnewhorizons/angelica/glsm/recording/RecordingDataStructuresTest.java b/src/test/java/com/gtnewhorizons/angelica/glsm/recording/RecordingDataStructuresTest.java new file mode 100644 index 000000000..1aba9e61d --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/glsm/recording/RecordingDataStructuresTest.java @@ -0,0 +1,267 @@ +package com.gtnewhorizons.angelica.glsm.recording; + +import com.gtnewhorizons.angelica.AngelicaExtension; +import com.gtnewhorizons.angelica.glsm.DisplayListManager; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.recording.commands.DrawCommand; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.opengl.GL11; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for display list recording data structures. + */ +@ExtendWith(AngelicaExtension.class) +class RecordingDataStructuresTest { + + private int testList = -1; + + @AfterEach + void cleanup() { + if (testList > 0) { + GLStateManager.glDeleteLists(testList, 1); + testList = -1; + } + } + + @Test + void testDrawCommandCreation() { + DrawCommand cmd = new DrawCommand(0, 4, GL11.GL_QUADS); + assertEquals(0, cmd.firstVertex()); + assertEquals(4, cmd.vertexCount()); + assertEquals(GL11.GL_QUADS, cmd.primitiveType()); + } + + @Test + void testRecordedGeometryCreation() { + ByteBuffer buffer = ByteBuffer.allocate(16); + DrawCommand cmd1 = new DrawCommand(0, 4, GL11.GL_QUADS); + DrawCommand cmd2 = new DrawCommand(4, 3, GL11.GL_TRIANGLES); + List commands = Arrays.asList(cmd1, cmd2); + + RecordedGeometry geom = new RecordedGeometry(buffer, null, commands, 7); + assertEquals(buffer, geom.buffer()); + assertEquals(commands, geom.drawCommands()); + assertEquals(7, geom.totalVertexCount()); + assertEquals(16, geom.getByteSize()); + } + + @Test + void testCompiledDisplayListCreation() { + // Create a display list with PushMatrix, Scale, PopMatrix + testList = GL11.glGenLists(1); + GLStateManager.glNewList(testList, GL11.GL_COMPILE); + + GLStateManager.glPushMatrix(); + GLStateManager.glScalef(2.0f, 2.0f, 2.0f); + GLStateManager.glPopMatrix(); + + GLStateManager.glEndList(); + + // Verify the compiled display list + CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList); + assertNotNull(compiled, "Display list should be compiled"); + assertNotNull(compiled.getCommandBuffer(), "Command buffer should not be null"); + + // Get command counts to verify structure + Map counts = compiled.getCommandCounts(); + + // Should have PushMatrix and PopMatrix + assertEquals(1, counts.getOrDefault(GLCommand.PUSH_MATRIX, 0), "Should have 1 PushMatrix"); + assertEquals(1, counts.getOrDefault(GLCommand.POP_MATRIX, 0), "Should have 1 PopMatrix"); + + // Scale should be collapsed into a MultMatrix (optimizer collapses transforms) + // Or if in debug mode, might have individual commands - just check we have some transforms + int transformCount = counts.getOrDefault(GLCommand.MULT_MATRIX, 0) + + counts.getOrDefault(GLCommand.SCALE, 0); + assertTrue(transformCount >= 0, "Should have transform commands"); + } + + @Test + void testImmediateModeRecorderCapturesQuads() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + // Record a single quad using immediate mode + recorder.begin(GL11.GL_QUADS); + recorder.setTexCoord(0, 0); + recorder.vertex(0, 0, 0); + recorder.setTexCoord(1, 0); + recorder.vertex(1, 0, 0); + recorder.setTexCoord(1, 1); + recorder.vertex(1, 1, 0); + recorder.setTexCoord(0, 1); + recorder.vertex(0, 1, 0); + + // end() now returns quads immediately (for correct command interleaving) + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertEquals(1, result.quads().size()); + assertTrue(result.flags().hasTexture); // We called setTexCoord + + // After end(), recorder should be empty (quads were returned immediately) + assertFalse(recorder.hasGeometry()); + assertNull(recorder.getQuadsAndClear()); + } + + @Test + void testImmediateModeRecorderCapturesTriangles() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + // Record 2 triangles (6 vertices, should become 2 degenerate quads) + recorder.begin(GL11.GL_TRIANGLES); + recorder.vertex(0, 0, 0); + recorder.vertex(1, 0, 0); + recorder.vertex(0.5f, 1, 0); + recorder.vertex(2, 0, 0); + recorder.vertex(3, 0, 0); + recorder.vertex(2.5f, 1, 0); + + // end() now returns quads immediately + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertEquals(2, result.quads().size()); // 2 triangles = 2 degenerate quads + + // Recorder should be empty after end() + assertFalse(recorder.hasGeometry()); + } + + @Test + void testImmediateModeRecorderCapturesTriangleFan() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + // Record a triangle fan (center + 3 outer vertices = 2 triangles = 2 quads) + recorder.begin(GL11.GL_TRIANGLE_FAN); + recorder.vertex(0, 0, 0); // Center + recorder.vertex(1, 0, 0); + recorder.vertex(1, 1, 0); + recorder.vertex(0, 1, 0); + + // end() now returns quads immediately + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertEquals(2, result.quads().size()); // 2 triangles from fan + + // Recorder should be empty after end() + assertFalse(recorder.hasGeometry()); + } + + @Test + void testImmediateModeRecorderEmptyAfterReset() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + recorder.begin(GL11.GL_QUADS); + recorder.vertex(0, 0, 0); + recorder.vertex(1, 0, 0); + recorder.vertex(1, 1, 0); + recorder.vertex(0, 1, 0); + + // end() returns quads immediately, so recorder is empty after + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertEquals(1, result.quads().size()); + + // Already empty after end(), but reset() should still work + assertFalse(recorder.hasGeometry()); + + recorder.reset(); + + assertFalse(recorder.hasGeometry()); + assertNull(recorder.getQuadsAndClear()); + } + + @Test + void testImmediateModeRecorderNormalAndColor() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + recorder.setNormal(0, 0, 1); // Normal pointing +Z + recorder.begin(GL11.GL_QUADS); + recorder.vertex(0, 0, 0); + recorder.vertex(1, 0, 0); + recorder.vertex(1, 1, 0); + recorder.vertex(0, 1, 0); + + // end() now returns quads immediately + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertTrue(result.flags().hasNormals); + } + + @Test + void testImmediateModeRecorderThrowsOnInvalidState() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + // glEnd without glBegin + assertThrows(IllegalStateException.class, () -> recorder.end()); + + // glVertex without glBegin + assertThrows(IllegalStateException.class, () -> recorder.vertex(0, 0, 0)); + + // Nested glBegin + recorder.begin(GL11.GL_QUADS); + assertThrows(IllegalStateException.class, () -> recorder.begin(GL11.GL_QUADS)); + recorder.end(); + + // getQuadsAndClear while in primitive + recorder.begin(GL11.GL_QUADS); + assertThrows(IllegalStateException.class, () -> recorder.getQuadsAndClear()); + } + + @Test + void testImmediateModeRecorderCapturesLines() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + // Record 2 lines (4 vertices) + recorder.begin(GL11.GL_LINES); + recorder.vertex(0, 0, 0); + recorder.vertex(1, 0, 0); + recorder.vertex(0, 1, 0); + recorder.vertex(1, 1, 0); + + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertTrue(result.quads().isEmpty(), "Lines should not produce quads"); + assertEquals(4, result.lines().size(), "4 vertices for 2 lines"); + } + + @Test + void testImmediateModeRecorderCapturesLineStrip() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + // Record a line strip: 4 vertices = 3 line segments = 6 line vertices + recorder.begin(GL11.GL_LINE_STRIP); + recorder.vertex(0, 0, 0); + recorder.vertex(1, 0, 0); + recorder.vertex(1, 1, 0); + recorder.vertex(0, 1, 0); + + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertTrue(result.quads().isEmpty(), "Line strips should not produce quads"); + assertEquals(6, result.lines().size(), "4 vertices in strip = 3 segments = 6 line vertices"); + } + + @Test + void testImmediateModeRecorderCapturesLineLoop() { + ImmediateModeRecorder recorder = new ImmediateModeRecorder(); + + // Record a line loop: 4 vertices = 4 line segments (closes back to start) = 8 line vertices + recorder.begin(GL11.GL_LINE_LOOP); + recorder.vertex(0, 0, 0); + recorder.vertex(1, 0, 0); + recorder.vertex(1, 1, 0); + recorder.vertex(0, 1, 0); + + ImmediateModeRecorder.Result result = recorder.end(); + assertNotNull(result); + assertTrue(result.quads().isEmpty(), "Line loops should not produce quads"); + assertEquals(8, result.lines().size(), "4 vertices in loop = 4 segments (including closing) = 8 line vertices"); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/util/GLSMUtil.java b/src/test/java/com/gtnewhorizons/angelica/util/GLSMUtil.java index 3993c3e41..f995789e1 100644 --- a/src/test/java/com/gtnewhorizons/angelica/util/GLSMUtil.java +++ b/src/test/java/com/gtnewhorizons/angelica/util/GLSMUtil.java @@ -70,6 +70,14 @@ public class GLSMUtil { ); } + public static void verifyState(int glCap, short expected, String message) { + // For short state (e.g., line stipple pattern), GL returns it as int, we compare as short + assertAll(message, + () -> assertEquals(expected, (short) GL11.glGetInteger(glCap), "GL State Mismatch"), + () -> assertEquals(expected, (short) GLStateManager.glGetInteger(glCap), "GLSM State Mismatch") + ); + } + public static void verifyState(int glCap, float expected) { verifyState(glCap, expected, "Float State Mismatch"); } @@ -253,6 +261,25 @@ public class GLSMUtil { // Reset draw buffer GLStateManager.glDrawBuffer(GLStateManager.DEFAULT_DRAW_BUFFER); + + // Reset line state + GLStateManager.glLineWidth(1.0f); + GLStateManager.glLineStipple(1, (short) 0xFFFF); + + // Reset point state + GLStateManager.glPointSize(1.0f); + + // Reset polygon state + GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL); + GLStateManager.glPolygonOffset(0.0f, 0.0f); + GLStateManager.glCullFace(GL11.GL_BACK); + GLStateManager.glFrontFace(GL11.GL_CCW); + + // Reset stencil state + GLStateManager.glStencilFunc(GL11.GL_ALWAYS, 0, 0xFFFFFFFF); + GLStateManager.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP); + GLStateManager.glStencilMask(0xFFFFFFFF); + GLStateManager.glClearStencil(0); } diff --git a/src/test/java/com/gtnewhorizons/angelica/utils/BufferUtilTest.java b/src/test/java/com/gtnewhorizons/angelica/utils/BufferUtilTest.java new file mode 100644 index 000000000..2ad6aa458 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/utils/BufferUtilTest.java @@ -0,0 +1,62 @@ +package com.gtnewhorizons.angelica.utils; + +import com.gtnewhorizons.angelica.AngelicaExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.nio.FloatBuffer; + +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAllocFloat; +import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for BufferUtil. + */ +@ExtendWith(AngelicaExtension.class) +class BufferUtilTest { + + @Test + void testCopyDirectBufferCopiesData() { + FloatBuffer src = memAllocFloat(4); + src.put(1.0f).put(2.0f).put(3.0f).put(4.0f); + src.flip(); + + FloatBuffer dst = BufferUtil.copyDirectBuffer(src); + assertTrue(dst.isDirect(), "Returned buffer should be direct"); + + try { + assertEquals(4, dst.remaining()); + assertEquals(1.0f, dst.get(0), 0.0001f); + assertEquals(2.0f, dst.get(1), 0.0001f); + assertEquals(3.0f, dst.get(2), 0.0001f); + assertEquals(4.0f, dst.get(3), 0.0001f); + } finally { + memFree(src); + memFree(dst); + } + } + + @Test + void testCopyDirectBufferPreservesSourcePosition() { + FloatBuffer src = memAllocFloat(4); + src.put(1.0f).put(2.0f).put(3.0f).put(4.0f); + src.flip(); + src.position(1); // Start at second element + + int posBefore = src.position(); + FloatBuffer dst = BufferUtil.copyDirectBuffer(src); + + try { + assertEquals(posBefore, src.position(), "Source position should be preserved"); + assertEquals(3, dst.remaining(), "Should copy remaining elements only"); + assertEquals(2.0f, dst.get(0), 0.0001f); + assertEquals(3.0f, dst.get(1), 0.0001f); + assertEquals(4.0f, dst.get(2), 0.0001f); + } finally { + memFree(src); + memFree(dst); + } + } + +} diff --git a/src/test/java/net/minecraft/launchwrapper/Launch.java b/src/test/java/net/minecraft/launchwrapper/Launch.java new file mode 100644 index 000000000..e73f2cf3b --- /dev/null +++ b/src/test/java/net/minecraft/launchwrapper/Launch.java @@ -0,0 +1,15 @@ +package net.minecraft.launchwrapper; + +import java.util.HashMap; +import java.util.Map; + +/** + * Stub for tests - provides blackboard without loading real launchwrapper. + */ +public class Launch { + public static Map blackboard = new HashMap<>(); + + static { + blackboard.put("lwjgl3ify:rfb-booted", Boolean.FALSE); + } +}