From b446968e66c80338572442943e2fcc4cf25d7e16 Mon Sep 17 00:00:00 2001 From: Jason Mitchell Date: Sun, 18 Oct 2026 23:59:55 -0700 Subject: [PATCH] Instance animated TESRs and let mods register shaders for batched TESRs (#2275) --- dependencies.gradle.kts | 2 +- .../angelica/glsm/CompatUniformManager.java | 44 +- .../angelica/glsm/ffp/FfpGlsl.java | 32 + .../angelica/glsm/ffp/FragmentKey.java | 20 +- .../glsm/ffp/FragmentShaderGenerator.java | 15 +- .../angelica/glsm/ffp/Uniforms.java | 21 +- .../glsm/ffp/VertexShaderGenerator.java | 15 +- .../angelica/glsm/states/FogState.java | 29 +- scripts/flyby-scenes/opencomputers-close.txt | 58 ++ scripts/flyby-scenes/opencomputers-perf.txt | 67 ++ .../flyby-scenes/opencomputers-textfade.txt | 39 + scripts/flyby-smoke.sh | 19 +- .../angelica/sdlgpu/shader/ClipZRemap.java | 7 +- .../angelica/sdlgpu/shader/ShaderManager.java | 16 +- .../PrewarmTreeHandoffEquivalenceTest.java | 15 +- .../ShaderSourceTransformCacheTest.java | 29 + .../sdlgpu/shader/ShaderTransformChain.java | 1 + .../angelica/api/tesr/TesrMaterial.java | 9 +- .../angelica/api/tesr/TesrShader.java | 16 +- .../angelica/api/tesr/TesrShaderUniforms.java | 11 + .../angelica/api/tesr/TesrShaders.java | 34 +- .../angelica/api/tesr/package-info.java | 2 +- .../angelica/compat/mojang/RenderLayer.java | 50 +- .../compat/thaumcraft/PortalDrawer.java | 31 +- .../debug/flyby/FlybyCommandSender.java | 4 + .../angelica/debug/flyby/FlybyRunner.java | 55 +- .../angelica/debug/flyby/FlybyScene.java | 196 ++++- .../angelica/profiling/TracyFramePlots.java | 13 +- .../rendering/tesr/AngelicaTesrMeshCache.java | 60 +- .../angelica/rendering/tesr/DrawState.java | 10 +- .../tesr/InstancedTemplateRenderer.java | 14 + .../rendering/tesr/ModelPartBatcher.java | 10 +- .../rendering/tesr/RetainedTesrGroups.java | 685 +++++++++++++++--- .../rendering/tesr/TesrAnchorMath.java | 57 +- .../rendering/tesr/TesrBatchRenderer.java | 69 +- .../rendering/tesr/TesrProviderDispatch.java | 15 - .../rendering/tesr/TesrShaderPrelude.java | 171 +++++ .../rendering/tesr/TesrShaderPrograms.java | 245 +++++++ .../MixinTileEntityRendererDispatcher.java | 36 +- .../api/tesr/TesrMaterialInternTest.java | 33 +- .../angelica/api/tesr/TesrShadersTest.java | 18 +- .../angelica/debug/flyby/FlybySceneTest.java | 8 +- .../tesr/AngelicaTesrMeshCacheTest.java | 99 ++- .../rendering/tesr/BatchEligibilityTest.java | 52 +- .../rendering/tesr/BatchStateGuardGLTest.java | 258 +++---- .../rendering/tesr/DrawStateTest.java | 42 +- .../rendering/tesr/EntityMaterialsTest.java | 185 ++--- .../tesr/RenderLayerInterningTest.java | 118 +-- .../tesr/RetainedTesrGroupsGLTest.java | 584 +++++++++++---- .../tesr/RetainedTesrGroupsTest.java | 584 ++++++++------- .../tesr/RetainedTesrTestSupport.java | 39 +- .../tesr/StateSetOverrideGLTest.java | 167 +---- .../rendering/tesr/TesrAnchorMathTest.java | 64 +- .../tesr/TesrBatchRendererScopeTest.java | 33 + .../rendering/tesr/TesrLayerKeyTest.java | 54 +- .../rendering/tesr/TesrLayerStateTest.java | 494 ++++++------- .../tesr/TesrProviderDispatchTest.java | 11 - .../rendering/tesr/TesrShaderPreludeTest.java | 50 ++ .../tesr/TesrShaderProgramsGLTest.java | 358 +++++++++ .../rendering/tesr/VertexTransformTest.java | 78 +- 60 files changed, 3916 insertions(+), 1635 deletions(-) create mode 100644 glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FfpGlsl.java create mode 100644 scripts/flyby-scenes/opencomputers-close.txt create mode 100644 scripts/flyby-scenes/opencomputers-perf.txt create mode 100644 scripts/flyby-scenes/opencomputers-textfade.txt create mode 100644 src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaderUniforms.java create mode 100644 src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrelude.java create mode 100644 src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrograms.java create mode 100644 src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRendererScopeTest.java create mode 100644 src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPreludeTest.java create mode 100644 src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderProgramsGLTest.java diff --git a/dependencies.gradle.kts b/dependencies.gradle.kts index 2c480a7bf..25d122c99 100644 --- a/dependencies.gradle.kts +++ b/dependencies.gradle.kts @@ -64,7 +64,7 @@ DependencyHandlerScope.of(dependencies).apply { runtimeOnlyNonPublishable("com.github.GTNewHorizons:BuildCraft:7.1.63:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:Botania:1.13.32-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:LogisticsPipes:1.5.32-GTNH:dev") - runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.55-GTNH:dev") + runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenComputers:1.12.64-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:Mobs-Info:0.5.18-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:IguanaTweaksTConstruct:2.7.10:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:OpenBlocks:1.12.18-GTNH:dev") diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/CompatUniformManager.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/CompatUniformManager.java index 5ea005916..e2d51f88e 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/CompatUniformManager.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/CompatUniformManager.java @@ -1,5 +1,7 @@ package com.gtnewhorizons.angelica.glsm; +import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey; +import com.gtnewhorizons.angelica.glsm.ffp.Uniforms; import com.gtnewhorizons.angelica.glsm.stacks.FogStateStack; import com.gtnewhorizons.angelica.glsm.stacks.LightStateStack; import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState; @@ -66,7 +68,13 @@ public class CompatUniformManager { static final int LOC_IRIS_MODELVIEW_INVERSE = LOC_MAT_BASE + MAT_FIELDS; // 48 static final int LOC_IRIS_PROJECTION_INVERSE = LOC_IRIS_MODELVIEW_INVERSE + 1; // 49 - static final int LOC_COUNT = LOC_IRIS_PROJECTION_INVERSE + 1; // 50 + static final int LOC_FOG_MODE = LOC_IRIS_PROJECTION_INVERSE + 1; // 50 + static final int LOC_FOG_PARAMS = LOC_FOG_MODE + 1; // 51 + static final int LOC_FOG_DISTANCE_MODE = LOC_FOG_PARAMS + 1; // 52 + + static final int LOC_MODELVIEW_PROJECTION = LOC_FOG_DISTANCE_MODE + 1; // 53 + + static final int LOC_COUNT = LOC_MODELVIEW_PROJECTION + 1; // 54 private static final String[] LIGHT_FIELD_NAMES = { "ambient", "diffuse", "specular", "position", "halfVector", @@ -84,6 +92,7 @@ public class CompatUniformManager { UNIFORM_NAMES[LOC_MODELVIEW] = "angelica_ModelViewMatrix"; UNIFORM_NAMES[LOC_MODELVIEW_INVERSE] = "angelica_ModelViewMatrixInverse"; UNIFORM_NAMES[LOC_PROJECTION] = "angelica_ProjectionMatrix"; + UNIFORM_NAMES[LOC_MODELVIEW_PROJECTION] = "angelica_ModelViewProjectionMatrix"; UNIFORM_NAMES[LOC_PROJECTION_INVERSE] = "angelica_ProjectionMatrixInverse"; UNIFORM_NAMES[LOC_NORMAL] = "angelica_NormalMatrix"; UNIFORM_NAMES[LOC_LIGHTMAP_TEXTURE_MATRIX] = "angelica_LightmapTextureMatrix"; @@ -98,6 +107,9 @@ public class CompatUniformManager { UNIFORM_NAMES[LOC_IRIS_TEXTURE_MATRIX] = "iris_TextureMatrix"; UNIFORM_NAMES[LOC_IRIS_MODELVIEW_INVERSE] = "iris_ModelViewMatrixInverse"; UNIFORM_NAMES[LOC_IRIS_PROJECTION_INVERSE] = "iris_ProjectionMatrixInverse"; + UNIFORM_NAMES[LOC_FOG_MODE] = "angelica_FogMode"; + UNIFORM_NAMES[LOC_FOG_PARAMS] = "angelica_FogParams"; + UNIFORM_NAMES[LOC_FOG_DISTANCE_MODE] = "angelica_FogDistanceMode"; UNIFORM_NAMES[LOC_ALPHA_TEST_REF] = "angelica_currentAlphaTest"; UNIFORM_NAMES[LOC_SCENE_COLOR] = "angelica_SceneColor"; UNIFORM_NAMES[LOC_CLIP_PLANES] = "angelica_ClipPlane[0]"; @@ -120,7 +132,14 @@ public class CompatUniformManager { GLContextState owner; int lastMvGen = -1, lastMvLinearGen = -1, lastProjGen = -1, lastTexMatGen = -1; int lastFragmentGen = -1, lastLightingGen = -1, lastClipPlaneGen = -1; - ProgramUniforms(int[] locs) { this.locs = locs; } + int lastFogMode = -1, lastFogDistanceMode = -1; + final boolean lights; + ProgramUniforms(int[] locs) { + this.locs = locs; + boolean any = false; + for (int i = LOC_LIGHT_BASE; i < LOC_MAT_BASE; i++) any |= locs[i] != -1; + this.lights = any; + } } // Reusable NIO buffers for upload @@ -186,6 +205,7 @@ public class CompatUniformManager { pu.owner = glCtx; pu.lastMvGen = pu.lastMvLinearGen = pu.lastProjGen = pu.lastTexMatGen = -1; pu.lastFragmentGen = pu.lastLightingGen = pu.lastClipPlaneGen = -1; + pu.lastFogMode = pu.lastFogDistanceMode = -1; } // Matrix uniforms — skip if this program's storage already holds the current generation @@ -212,12 +232,15 @@ public class CompatUniformManager { uploadFragmentUniforms(locs); } + pu.lastFogMode = uploadIfChanged(locs[LOC_FOG_MODE], FragmentKey.activeFogMode(glCtx), pu.lastFogMode); + pu.lastFogDistanceMode = uploadIfChanged(locs[LOC_FOG_DISTANCE_MODE], glCtx.fogState.getFogDistanceMode(), pu.lastFogDistanceMode); + // Lighting-derived uniforms (scene color, light sources, material) final int litGen = glCtx.lightingGeneration; if (litGen != pu.lastLightingGen) { pu.lastLightingGen = litGen; if (locs[LOC_SCENE_COLOR] != -1) uploadSceneColor(locs); - uploadLightSources(locs); + if (pu.lights) uploadLightSources(locs); uploadMaterial(locs); } @@ -262,6 +285,11 @@ public class CompatUniformManager { } } + if ((mvChanged || projChanged) && locs[LOC_MODELVIEW_PROJECTION] != -1) { + Uniforms.modelViewProjection(proj, mv, scratchMatrix).get(mat4Buf); + RENDER_BACKEND.uniformMatrix4(locs[LOC_MODELVIEW_PROJECTION], false, mat4Buf); + } + if (mvLinearChanged) { // Normal Matrix (inverse-transpose of upper-left 3x3 of ModelView) if (locs[LOC_NORMAL] != -1 || locs[LOC_IRIS_NORMAL] != -1) { @@ -324,6 +352,11 @@ public class CompatUniformManager { } } + private static int uploadIfChanged(int location, int value, int last) { + if (location != -1 && value != last) RENDER_BACKEND.uniform1i(location, value); + return value; + } + private static void uploadFragmentUniforms(int[] locs) { // Fog uniforms FogStateStack fog = GLStateManager.getFogState(); @@ -336,6 +369,9 @@ public class CompatUniformManager { if (locs[LOC_FOG_END] != -1) { RENDER_BACKEND.uniform1f(locs[LOC_FOG_END], fog.getEnd()); } + if (locs[LOC_FOG_PARAMS] != -1) { + RENDER_BACKEND.uniform4f(locs[LOC_FOG_PARAMS], fog.linearScale(), fog.linearBias(), fog.expScale(), fog.exp2Scale()); + } if (locs[LOC_FOG_COLOR] != -1) { vec4Buf.clear(); vec4Buf.put((float) fog.getFogColor().x).put((float) fog.getFogColor().y).put((float) fog.getFogColor().z).put(fog.getFogAlpha()); @@ -372,8 +408,6 @@ public class CompatUniformManager { } private static void uploadLightSources(int[] locs) { - // Early exit: if the first light field isn't present, no lighting uniforms exist - if (locs[LOC_LIGHT_BASE] == -1) return; final LightStateStack[] lights = GLStateManager.getLightDataStates(); for (int li = 0; li < 2; li++) { final int base = LOC_LIGHT_BASE + li * LIGHT_FIELDS; diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FfpGlsl.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FfpGlsl.java new file mode 100644 index 000000000..d3ae275b8 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FfpGlsl.java @@ -0,0 +1,32 @@ +package com.gtnewhorizons.angelica.glsm.ffp; + +import com.gtnewhorizons.angelica.glsm.states.FogState; + +public final class FfpGlsl { + + private FfpGlsl() {} + + // Mesa ffvertex_prog.c build_fog(): FDM_EYE_RADIAL/EYE_PLANE/EYE_PLANE_ABS + public static String fogDistance(int mode, String eye) { + return switch (mode) { + case FogState.DISTANCE_EYE_RADIAL -> "length(" + eye + ".xyz)"; + case FogState.DISTANCE_EYE_PLANE -> eye + ".z"; + default -> "abs(" + eye + ".z)"; + }; + } + + public static void appendFogFactor(StringBuilder sb, String indent, int fogMode, String coord, String params) { + switch (fogMode) { + case FragmentKey.FOG_LINEAR -> sb.append(indent).append("f = ").append(coord).append(" * ").append(params).append(".x + ").append(params).append(".y;\n"); + case FragmentKey.FOG_EXP -> sb.append(indent).append("f = exp2(-(").append(coord).append(" * ").append(params).append(".z));\n"); + case FragmentKey.FOG_EXP2 -> { + sb.append(indent).append("float fogTmp = ").append(coord).append(" * ").append(params).append(".w;\n"); + sb.append(indent).append("f = exp2(-(fogTmp * fogTmp));\n"); + } + } + } + + public static String quantizeAlpha(String alpha) { + return "round(" + alpha + " * 255.0)"; + } +} diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentKey.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentKey.java index 2c7d6f81e..2a6163f73 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentKey.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentKey.java @@ -65,6 +65,16 @@ public final class FragmentKey { public static final int FOG_EXP = 2; public static final int FOG_EXP2 = 3; + public static int activeFogMode(GLContextState glCtx) { + if (!glCtx.fogMode.isEnabled()) return FOG_NONE; + return switch (glCtx.fogState.getFogMode()) { + case GL11.GL_LINEAR -> FOG_LINEAR; + case GL11.GL_EXP -> FOG_EXP; + case GL11.GL_EXP2 -> FOG_EXP2; + default -> FOG_NONE; + }; + } + private static final int GLOBAL_BITS = 14; private static final int BIT_COMBINED_GLINT = 61; private static final int BIT_GLINT_REPLACE_ALPHA = 62; @@ -117,15 +127,7 @@ public final class FragmentKey { if (CombinedGlint.replacesAlpha()) global |= 1L << BIT_GLINT_REPLACE_ALPHA; // Fog - if (glCtx.fogMode.isEnabled()) { - final int fogMode = switch (glCtx.fogState.getFogMode()) { - case GL11.GL_LINEAR -> FOG_LINEAR; - case GL11.GL_EXP -> FOG_EXP; - case GL11.GL_EXP2 -> FOG_EXP2; - default -> FOG_NONE; - }; - global |= (long) fogMode; - } + global |= activeFogMode(glCtx); // Alpha test if (glCtx.alphaTest.isEnabled()) { diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentShaderGenerator.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentShaderGenerator.java index cb9350183..8b13d8776 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentShaderGenerator.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/FragmentShaderGenerator.java @@ -305,7 +305,7 @@ public final class FragmentShaderGenerator { final int func = FragmentKey.decodeAlphaFunc(key.alphaTestFunc()); - sb.append(" float qa = round(color.a * 255.0);\n"); + sb.append(" float qa = ").append(FfpGlsl.quantizeAlpha("color.a")).append(";\n"); switch (func) { case GL11.GL_NEVER -> sb.append(" discard;\n"); @@ -335,18 +335,7 @@ public final class FragmentShaderGenerator { sb.append(" // Fog\n"); sb.append(" float f;\n"); - - switch (fogMode) { - case FragmentKey.FOG_LINEAR -> - sb.append(" f = v_FogCoord * u_FogParams.x + u_FogParams.y;\n"); - case FragmentKey.FOG_EXP -> - sb.append(" f = exp2(-(v_FogCoord * u_FogParams.z));\n"); - case FragmentKey.FOG_EXP2 -> { - sb.append(" float fogTmp = v_FogCoord * u_FogParams.w;\n"); - sb.append(" f = exp2(-(fogTmp * fogTmp));\n"); - } - } - + FfpGlsl.appendFogFactor(sb, " ", fogMode, "v_FogCoord", "u_FogParams"); sb.append(" f = clamp(f, 0.0, 1.0);\n"); sb.append(" color.rgb = mix(u_FogColor.rgb, color.rgb, f);\n"); } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/Uniforms.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/Uniforms.java index 93d65d2e9..3b541a4f7 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/Uniforms.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/Uniforms.java @@ -80,9 +80,6 @@ public class Uniforms { private final Vector3f tempVec3 = new Vector3f(); private final FloatBuffer clipPlaneBuf = memAllocFloat(32); // 8 planes * vec4 - private static final double LN2 = java.lang.Math.log(2.0); - private static final double SQRT_LN2 = java.lang.Math.sqrt(LN2); - public void upload(GLContextState glCtx) { boolean dirty = false; contentChanged = false; @@ -233,8 +230,11 @@ public class Uniforms { if (projChanged) { putMat4(FFPUniformBlock.PROJECTION_MATRIX, proj); } - proj.mul(mv, mvpMatrix); - putMat4(FFPUniformBlock.MVP_MATRIX, mvpMatrix); + putMat4(FFPUniformBlock.MVP_MATRIX, modelViewProjection(proj, mv, mvpMatrix)); + } + + public static Matrix4f modelViewProjection(Matrix4f proj, Matrix4f mv, Matrix4f dest) { + return proj.mul(mv, dest); } private void stageNormalMatrix(GLContextState glCtx) { @@ -343,17 +343,8 @@ public class Uniforms { final var secondary = glCtx.secondaryColor; putVec3(FFPUniformBlock.SECONDARY_COLOR, secondary.getRed(), secondary.getGreen(), secondary.getBlue()); - // Mesa STATE_FOG_PARAMS_OPTIMIZED final FogState fog = glCtx.fogState; - final float start = fog.getStart(); - final float end = fog.getEnd(); - final float density = fog.getDensity(); - final float range = end - start; - putVec4(FFPUniformBlock.FOG_PARAMS, - range != 0.0f ? -1.0f / range : 0.0f, - range != 0.0f ? end / range : 1.0f, - (float) (density / LN2), - (float) (density / SQRT_LN2)); + putVec4(FFPUniformBlock.FOG_PARAMS, fog.linearScale(), fog.linearBias(), fog.expScale(), fog.exp2Scale()); putVec4(FFPUniformBlock.FOG_COLOR, (float) fog.getFogColor().x, (float) fog.getFogColor().y, (float) fog.getFogColor().z, fog.getFogAlpha()); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexShaderGenerator.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexShaderGenerator.java index a1e4d2495..59a430612 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexShaderGenerator.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexShaderGenerator.java @@ -100,9 +100,9 @@ public final class VertexShaderGenerator { private static void emitOutputs(StringBuilder sb, VertexKey key) { sb.append("// Outputs\n"); sb.append("out vec4 v_Color;\n"); + sb.append("invariant gl_Position;\n"); if (key.instancing().hasInstanceHead()) { sb.append("out vec4 v_Overlay;\n"); - sb.append("invariant gl_Position;\n"); } if (key.lineStipple()) { sb.append("flat out vec2 v_LineStart;\n"); @@ -414,20 +414,11 @@ public final class VertexShaderGenerator { private static void emitFogDistance(StringBuilder sb, VertexKey key) { sb.append(" // Fog distance\n"); - final String own = fogDistance(key, "eyePos"); - final String fog = key.lineQuads() ? acrossLine(own, fogDistance(key, "lineEyeOther")) : own; + final String own = FfpGlsl.fogDistance(key.fogDistanceMode(), "eyePos"); + final String fog = key.lineQuads() ? acrossLine(own, FfpGlsl.fogDistance(key.fogDistanceMode(), "lineEyeOther")) : own; sb.append(" v_FogCoord = ").append(fog).append(";\n"); } - // Mesa ffvertex_prog.c build_fog(): FDM_EYE_RADIAL/EYE_PLANE/EYE_PLANE_ABS - private static String fogDistance(VertexKey key, String eye) { - return switch (key.fogDistanceMode()) { - case 0 -> "length(" + eye + ".xyz)"; // FDM_EYE_RADIAL: Euclidean distance - case 1 -> eye + ".z"; // FDM_EYE_PLANE: raw Z (can be negative, clamped by fog factor) - default -> "abs(" + eye + ".z)"; // FDM_EYE_PLANE_ABS: absolute Z - }; - } - private static void emitClipDistances(StringBuilder sb, VertexKey key) { sb.append(" // Clip distances\n"); for (int i = 0; i < 8; i++) { diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/FogState.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/FogState.java index 5ed3c0332..f033d3052 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/FogState.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/FogState.java @@ -9,6 +9,13 @@ import java.nio.FloatBuffer; @Getter @Setter public class FogState implements ISettableState { + public static final int DISTANCE_EYE_RADIAL = 0; + public static final int DISTANCE_EYE_PLANE = 1; + public static final int DISTANCE_EYE_PLANE_ABS = 2; + + private static final double LN2 = Math.log(2.0); + private static final double SQRT_LN2 = Math.sqrt(LN2); + protected int fogMode = GL11.GL_EXP; protected final Vector3d fogColor = new Vector3d(0.0F, 0.0F, 0.0F); protected float fogAlpha = 1.0F; @@ -16,8 +23,26 @@ public class FogState implements ISettableState { protected float density = 1.0F; protected float start; protected float end = 1.0F; - /** Fog distance computation mode: 0=EYE_RADIAL, 1=EYE_PLANE, 2=EYE_PLANE_ABS */ - protected int fogDistanceMode = 2; + protected int fogDistanceMode = DISTANCE_EYE_PLANE_ABS; + + // Mesa STATE_FOG_PARAMS_OPTIMIZED + public float linearScale() { + final float range = end - start; + return range != 0.0f ? -1.0f / range : 0.0f; + } + + public float linearBias() { + final float range = end - start; + return range != 0.0f ? end / range : 1.0f; + } + + public float expScale() { + return (float) (density / LN2); + } + + public float exp2Scale() { + return (float) (density / SQRT_LN2); + } @Override public FogState set(FogState state) { diff --git a/scripts/flyby-scenes/opencomputers-close.txt b/scripts/flyby-scenes/opencomputers-close.txt new file mode 100644 index 000000000..c4fb0f1fc --- /dev/null +++ b/scripts/flyby-scenes/opencomputers-close.txt @@ -0,0 +1,58 @@ +# OpenComputers TESR scene, close ring. ~ is the flight start; ground-standing level is ~-2. Yaw 0 faces south (+z). +# Use with FLYBY_ORIGIN=x,z,-21 SMOKE_PITCH=30. + +# setup +gamerule doMobSpawning false +gamerule doFireTick false +difficulty 1 + +# northwest corner: test ISBRH +setblock ~-7 ~-2 ~-7 angelica:test_isbrh + +# south: geolyzers +setblock ~-1 ~-2 ~4 OpenComputers:geolyzer +setblock ~0 ~-2 ~4 OpenComputers:geolyzer +setblock ~1 ~-2 ~4 OpenComputers:geolyzer +setblock ~0 ~-2 ~5 OpenComputers:geolyzer + +# southwest: power distributor, assembler, capacitor +setblock ~-3 ~-2 ~3 OpenComputers:powerDistributor +setblock ~-3 ~-1 ~3 OpenComputers:assembler +setblock ~-4 ~-2 ~3 OpenComputers:capacitor + +# west: relay, transposer, adapter +setblock ~-4 ~-2 ~0 OpenComputers:relay +setblock ~-4 ~-2 ~1 OpenComputers:transposer +setblock ~-4 ~-1 ~1 OpenComputers:adapter + +# northwest: powered chargers +setblock ~-3 ~-2 ~-3 OpenComputers:charger +setblock ~-3 ~-2 ~-4 minecraft:redstone_block +setblock ~-4 ~-2 ~-2 OpenComputers:charger +setblock ~-4 ~-2 ~-3 minecraft:redstone_block + +# north: microcontrollers, raids +setblock ~-1 ~-2 ~-4 OpenComputers:microcontroller +setblock ~0 ~-2 ~-4 OpenComputers:microcontroller +setblock ~1 ~-2 ~-4 OpenComputers:raid +setblock ~-1 ~-1 ~-4 OpenComputers:microcontroller +setblock ~0 ~-1 ~-4 OpenComputers:raid +setblock ~1 ~-1 ~-4 OpenComputers:raid + +# northeast: cases, disk drive +setblock ~3 ~-2 ~-3 OpenComputers:case1 +setblock ~4 ~-2 ~-3 OpenComputers:case2 +setblock ~3 ~-1 ~-3 OpenComputers:case3 +setblock ~4 ~-1 ~-3 OpenComputers:diskDrive + +# east: rack, screens +setblock ~4 ~-2 ~-2 OpenComputers:rack +setblock ~4 ~-2 ~-1 OpenComputers:screen2 +setblock ~4 ~-1 ~-1 OpenComputers:screen2 +setblock ~4 ~-2 ~1 OpenComputers:screen1 + +# southeast: hologram projectors, disassembler, net splitter +setblock ~2 ~-2 ~3 OpenComputers:hologram1 +setblock ~3 ~-2 ~2 OpenComputers:hologram2 +setblock ~3 ~-2 ~3 OpenComputers:disassembler +setblock ~3 ~-1 ~3 OpenComputers:netSplitter diff --git a/scripts/flyby-scenes/opencomputers-perf.txt b/scripts/flyby-scenes/opencomputers-perf.txt new file mode 100644 index 000000000..2c5035c0c --- /dev/null +++ b/scripts/flyby-scenes/opencomputers-perf.txt @@ -0,0 +1,67 @@ +# OpenComputers TESR perf scene: running machines and a 12x12 overlay grid. ~ is the flight start; ground-standing level is ~-2. Yaw 0 faces south (+z). +# Use with FLYBY_ORIGIN=x,z,-21 SMOKE_PITCH=20 SMOKE_WARMUP_TICKS=400. + +define BUFFER_3X2 {width:24,height:8,depth:1,buffer:["ANGELICA TESR FLYBY TEST","RED GREEN BLUE YELLOW ok","0123456789ABCDEF01234567","========================","abcdefghijklmnopqrstuvwx","The quick brown fox jump","+-*/=<>#%&!?.+-*/=<>#%&!","screen1 3x2 16 color pal"],color:[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,3599,3599,3599,2063,1295,1295,1295,1295,1295,2063,783,783,783,783,2063,1039,1039,1039,1039,1039,1039,2063,2063,2063,7,263,519,775,1031,1287,1543,1807,2055,2311,2567,2823,3079,3335,3591,3847,7,263,519,775,1031,1287,1543,1807,3840,3840,3841,3842,3842,3843,3844,3844,3845,3846,3846,3847,3848,3848,3849,3850,3850,3851,3852,3852,3853,3854,3854,3855,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266]} +define BUFFER_1X1 {width:8,height:4,depth:0,buffer:["ONE BIT.","01234567","abcdefgh","+-*/=<>#"],color:[256,256,256,256,256,256,256,256,1,1,1,1,1,1,1,1,256,256,256,256,256,256,256,256,256,1,256,1,256,1,256,1]} +define EEPROM {id:"OpenComputers:eeprom",Count:1b,Damage:0s,tag:{oc;data:{oc;eeprom#:"local c,k=component,computer local function f(t) return (c.list(t)()) end local h,m,r,g=f('hologram'),f('modem'),f('robot'),f('debug') local function s(t) if g then pcall(c.invoke,g,'runCommand','say flyby-oc '..t) end end s('boot hologram='..tostring(h)..' modem='..tostring(m)..' robot='..tostring(r)) if h then s('hologram '..tostring(pcall(function() c.invoke(h,'clear') local n=1 if c.invoke(h,'maxDepth')==2 then n=3 end for x=1,48 do for z=1,48 do local a,b=x-24.5,z-24.5 local d=math.sqrt(a*a+b*b) if d<23 then c.invoke(h,'fill',x,z,1,math.floor(math.sqrt(529-d*d))+1,math.floor(d/4)%n+1) end end end end))) end while true do if m then pcall(c.invoke,m,'broadcast',1,'flyby') end if r then pcall(c.invoke,r,'turn',true) end k.pullSignal(0.5) end",oc;label:"flyby",oc;readonly:1b}}} + +# setup +gamerule doMobSpawning false +gamerule doFireTick false +difficulty 1 + +# held screen: up-facing screens draw the up indicator +clear @p +give @p OpenComputers:screen1 1 + +# south: test ISBRH +setblock ~0 ~-2 ~5 angelica:test_isbrh + +# southwest: up-facing screen on a creative case +setblock ~-3 ~-2 ~4 OpenComputers:caseCreative +tile ~-3 ~-2 ~3 OpenComputers:screen1 0 {oc;pitch:1,buffer:${BUFFER_1X1}} + +# north: creative case, 3x2 16-color screen, 1x1 mono screen (every wall block carries the buffer, the merge keeps the origin's) +setblock ~2 ~-2 ~-5 OpenComputers:caseCreative +setblock ~-1 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~0 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~1 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~-1 ~-1 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~0 ~-1 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~1 ~-1 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~3 ~-2 ~-5 OpenComputers:screen1 0 replace {buffer:${BUFFER_1X1}} + +# west: powered case, distributor, capacitor, chargers +setblock ~-4 ~-2 ~0 OpenComputers:caseCreative +setblock ~-4 ~-1 ~0 OpenComputers:powerDistributor +setblock ~-4 ~-2 ~1 OpenComputers:capacitor +setblock ~-4 ~-2 ~-1 OpenComputers:charger +setblock ~-5 ~-2 ~-1 minecraft:redstone_block +setblock ~-4 ~-2 ~2 OpenComputers:charger +setblock ~-5 ~-2 ~2 minecraft:redstone_block + +# east: running computer, tier 2 projector on top, relay +tile ~5 ~-2 ~0 OpenComputers:caseCreative 0 {oc;tier:3b,oc;yaw:4,oc;items:[{slot:0b,item:{id:"OpenComputers:item",Count:1b,Damage:11s}},{slot:1b,item:{id:"OpenComputers:item",Count:1b,Damage:73s}},{slot:3b,item:{id:"OpenComputers:item",Count:1b,Damage:39s}},{slot:8b,item:{id:"OpenComputers:item",Count:1b,Damage:43s}},{slot:9b,item:${EEPROM}}]} +tile ~5 ~-1 ~0 OpenComputers:hologram2 0 {oc;tier:1b,oc;scale:1.0d} +setblock ~5 ~-2 ~1 OpenComputers:relay +after 30 activate ~5 ~-2 ~0 sneak + +# far southeast: same computer, projector at scale 4 in the hologram fade band +tile ~40 ~-2 ~40 OpenComputers:caseCreative 0 {oc;tier:3b,oc;yaw:4,oc;items:[{slot:1b,item:{id:"OpenComputers:item",Count:1b,Damage:73s}},{slot:3b,item:{id:"OpenComputers:item",Count:1b,Damage:39s}},{slot:8b,item:{id:"OpenComputers:item",Count:1b,Damage:43s}},{slot:9b,item:${EEPROM}}]} +tile ~40 ~-1 ~40 OpenComputers:hologram2 0 {oc;tier:1b,oc;scale:4.0d} +after 30 activate ~40 ~-2 ~40 sneak + +# southeast: empty tier 1 and 2 projectors +setblock ~3 ~-3 ~2 OpenComputers:caseCreative +setblock ~3 ~-2 ~2 OpenComputers:hologram1 +setblock ~3 ~-3 ~4 OpenComputers:caseCreative +setblock ~3 ~-2 ~4 OpenComputers:hologram2 + +# northwest: turning robot +tile ~-4 ~-2 ~-4 OpenComputers:robot 0 {display:{Name:"Flyby"},oc;tier:3,oc;components:[{id:"OpenComputers:item",Count:1b,Damage:73s},{id:"OpenComputers:item",Count:1b,Damage:43s},{id:"OpenComputers:item",Count:1b,Damage:39s},${EEPROM}]} +after 30 activate ~-4 ~-2 ~-4 sneak + +# south: 12x12 overlay grid, one 12x4 band per block +fill ~-6 ~-2 ~9 ~5 ~-2 ~12 OpenComputers:geolyzer +fill ~-6 ~-2 ~13 ~5 ~-2 ~16 OpenComputers:microcontroller +fill ~-6 ~-2 ~17 ~5 ~-2 ~20 OpenComputers:raid diff --git a/scripts/flyby-scenes/opencomputers-textfade.txt b/scripts/flyby-scenes/opencomputers-textfade.txt new file mode 100644 index 000000000..9414e9ddb --- /dev/null +++ b/scripts/flyby-scenes/opencomputers-textfade.txt @@ -0,0 +1,39 @@ +# OpenComputers screen text fade scene, for a shader pack run. ~ is the flight start; ground-standing level is ~-2. Yaw 0 faces south (+z). +# Use with FLYBY_ORIGIN=x,z,-21 SMOKE_PITCH=15 SMOKE_WARMUP_TICKS=300. Shot 4 (north) holds all three walls. + +define BUFFER_3X2 {width:24,height:8,depth:1,buffer:["ANGELICA TESR FLYBY TEST","RED GREEN BLUE YELLOW ok","0123456789ABCDEF01234567","========================","abcdefghijklmnopqrstuvwx","The quick brown fox jump","+-*/=<>#%&!?.+-*/=<>#%&!","screen1 3x2 16 color pal"],color:[11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,3599,3599,3599,2063,1295,1295,1295,1295,1295,2063,783,783,783,783,2063,1039,1039,1039,1039,1039,1039,2063,2063,2063,7,263,519,775,1031,1287,1543,1807,2055,2311,2567,2823,3079,3335,3591,3847,7,263,519,775,1031,1287,1543,1807,3840,3840,3841,3842,3842,3843,3844,3844,3845,3846,3846,3847,3848,3848,3849,3850,3850,3851,3852,3852,3853,3854,3854,3855,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1039,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,1295,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266,266]} + +# setup +gamerule doMobSpawning false +gamerule doFireTick false +difficulty 1 + +# south: test ISBRH +setblock ~0 ~-2 ~5 angelica:test_isbrh + +# north, near: full text +setblock ~-2 ~-2 ~-7 OpenComputers:caseCreative +setblock ~-5 ~-2 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~-4 ~-2 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~-3 ~-2 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~-5 ~-1 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~-4 ~-1 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~-3 ~-1 ~-7 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} + +# north, mid: fading text +setblock ~2 ~-2 ~-26 OpenComputers:caseCreative +setblock ~-1 ~-2 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~0 ~-2 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~1 ~-2 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~-1 ~-1 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~0 ~-1 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~1 ~-1 ~-26 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} + +# north, far: no text +setblock ~10 ~-2 ~-32 OpenComputers:caseCreative +setblock ~7 ~-2 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~8 ~-2 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~9 ~-2 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~7 ~-1 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~8 ~-1 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} +setblock ~9 ~-1 ~-32 OpenComputers:screen1 0 replace {buffer:${BUFFER_3X2}} diff --git a/scripts/flyby-smoke.sh b/scripts/flyby-smoke.sh index 6c25109bd..7064b399e 100755 --- a/scripts/flyby-smoke.sh +++ b/scripts/flyby-smoke.sh @@ -9,7 +9,11 @@ # QUICKPLAY_WORLD - save to load (default: Flat World - No Mods). # FLYBY_ORIGIN - x,z[,yaw] flight start (default: 1509.5,-924.5,0). # SMOKE_TIMEOUT - seconds before the client is killed (default: 600). +# SMOKE_SCENE - scene command file (default: scripts/flyby-scenes/surround-smoke.txt). # SMOKE_PACK - shaderpack name for the pack variant (default: ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1). +# SMOKE_DEBUG_HUD - true forces F3 on; false keeps the saved setting (default: true). +# SMOKE_PITCH - camera pitch in degrees, positive looks down (default: 15). +# SMOKE_WARMUP_TICKS - ticks at the first pose before the pan (default: 100). # # Runs from this script's own checkout. Restores run/client/config/shaders.properties and angelica-options.json on exit. @@ -28,8 +32,15 @@ QUICKPLAY_WORLD="${QUICKPLAY_WORLD:-Flat World - No Mods}" FLYBY_ORIGIN="${FLYBY_ORIGIN:-1509.5,-924.5,0}" SMOKE_TIMEOUT="${SMOKE_TIMEOUT:-600}" SMOKE_PACK="${SMOKE_PACK:-ComplementaryUnbound_r5.7.1 + EuphoriaPatches_1.8.1}" +SMOKE_DEBUG_HUD="${SMOKE_DEBUG_HUD:-true}" +SMOKE_PITCH="${SMOKE_PITCH:-15}" +SMOKE_WARMUP_TICKS="${SMOKE_WARMUP_TICKS:-100}" -SCENE="$ROOT/scripts/flyby-scenes/surround-smoke.txt" +SCENE="${SMOKE_SCENE:-$ROOT/scripts/flyby-scenes/surround-smoke.txt}" +case "$SCENE" in + /*) ;; + *) SCENE="$ROOT/$SCENE" ;; +esac RUN="$ROOT/run/client" SHADERS="$RUN/config/shaders.properties" OUT="$ROOT/tmp/smoke/$NAME" @@ -146,13 +157,13 @@ PY ./gradlew runClient25 --console=plain \ -Dangelica.flyby.route=pan \ -Dangelica.flyby.length=360 \ - -Dangelica.flyby.warmupTicks=100 \ + -Dangelica.flyby.warmupTicks="$SMOKE_WARMUP_TICKS" \ -Dangelica.flyby.exitWhenDone=true \ -Dangelica.flyby.commands="$SCENE" \ -Dangelica.flyby.origin="$FLYBY_ORIGIN" \ - -Dangelica.flyby.pitch=15 \ + -Dangelica.flyby.pitch="$SMOKE_PITCH" \ -Dangelica.flyby.screenshots=8 \ - -Dangelica.flyby.debugHud=true \ + -Dangelica.flyby.debugHud="$SMOKE_DEBUG_HUD" \ -Dangelica.flyby.crashTest=isbrh \ -Dangelica.debug.testBlocks=true \ -DquickPlaySingleplayer="$QUICKPLAY_WORLD" \ diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ClipZRemap.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ClipZRemap.java index 03727c4a2..be64a8fea 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ClipZRemap.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ClipZRemap.java @@ -11,16 +11,17 @@ public final class ClipZRemap { private ClipZRemap() {} - public static void collectEdits(GLSLParser.Translation_unitContext root, List edits) { + public static boolean collectEdits(GLSLParser.Translation_unitContext root, List edits) { for (GLSLParser.External_declarationContext external : root.external_declaration()) { final GLSLParser.Function_definitionContext function = external.function_definition(); if (function == null || function.function_prototype().IDENTIFIER() == null) continue; if (!"main".equals(function.function_prototype().IDENTIFIER().getText())) continue; final GLSLParser.Compound_statement_no_new_scopeContext body = function.compound_statement_no_new_scope(); - if (body == null || body.RIGHT_BRACE() == null) return; + if (body == null || body.RIGHT_BRACE() == null) return false; final int idx = body.RIGHT_BRACE().getSymbol().getStartIndex(); edits.add(new Edit(idx, idx - 1, INJECTION_STD)); - return; + return true; } + return false; } } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java index 34a37ac17..14bcc76bc 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java @@ -296,7 +296,7 @@ public final class ShaderManager { return shaderKindFor(obj.type); } - private static int shaderKindFor(int glShaderType) { + static int shaderKindFor(int glShaderType) { if (glShaderType == GL20.GL_VERTEX_SHADER) return Shaderc.shaderc_vertex_shader; if (glShaderType == GL32.GL_GEOMETRY_SHADER) return Shaderc.shaderc_geometry_shader; if (glShaderType == GL43.GL_COMPUTE_SHADER) return Shaderc.shaderc_compute_shader; @@ -311,14 +311,20 @@ public final class ShaderManager { } private static PrewarmTransformResult transformOrNull(String transformedSource, int glShaderType, String debugName, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) { + final String source = preprocessed(transformedSource, glShaderType); final GLSLParser.Translation_unitContext root; try { - root = GlslTransformUtils.parseFullQuiet(transformedSource); + root = GlslTransformUtils.parseFullQuiet(source); } catch (Exception e) { LOG.warn("glsl-transformation-lib parse failed for '{}': {}", debugName, e.getMessage()); return null; } - return transform(transformedSource, 0, root, glShaderType, debugName, perFrame, perPass); + return transform(source, 0, root, glShaderType, debugName, perFrame, perPass); + } + + static String preprocessed(String source, int glShaderType) { + final String preprocessed = SpirvCompiler.preprocess(source, shaderKindFor(glShaderType), "transform", SpirvCompiler.Options.vulkanForced460Core()); + return preprocessed != null ? preprocessed : source; } public static PrewarmTransformResult applyPrewarmTransformsFull(String finalSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) { @@ -333,8 +339,8 @@ public final class ShaderManager { PerFrameBlockInjector.collectEdits(index, perFrame, perPass, edits); } final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(index, glShaderType, debugName, true, edits); - if (glShaderType == GL20.GL_VERTEX_SHADER) { - ClipZRemap.collectEdits(tree, edits); + if (glShaderType == GL20.GL_VERTEX_SHADER && !ClipZRemap.collectEdits(tree, edits)) { + LOG.warn("No main() found in vertex shader '{}'; its clip-space depth stays in OpenGL's -1..1 range", debugName); } final List textureDeclarations = TextureDeclarations.collect(index); final Set strippedSamplers = SamplerStripper.collectEdits(index, textureDeclarations, edits); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/PrewarmTreeHandoffEquivalenceTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/PrewarmTreeHandoffEquivalenceTest.java index 2d2a08606..8c4a1117e 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/PrewarmTreeHandoffEquivalenceTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/PrewarmTreeHandoffEquivalenceTest.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks; import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock; import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member; import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess; +import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler; import com.gtnewhorizons.angelica.glsm.shader.UniformType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; @@ -15,10 +16,13 @@ import org.lwjgl.opengl.GL32; import org.taumc.glsl.ShaderParser; import org.taumc.glsl.grammar.GLSLParser; +import java.nio.ByteBuffer; import java.util.List; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; class PrewarmTreeHandoffEquivalenceTest { @@ -104,7 +108,16 @@ class PrewarmTreeHandoffEquivalenceTest { GlslVulkanPreprocess.clearCache(); final ShaderManager.PrewarmTransformResult treeFed = ShaderManager.applyPrewarmTransformsFull(finalSource, tree, HEADER.length(), glShaderType, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock); - assertEquals(reparse.source(), treeFed.source(), "tree handoff diverged from re-parse for " + label); + assertArrayEquals(spirv(reparse.source(), glShaderType, label), spirv(treeFed.source(), glShaderType, label), "tree handoff compiled differently from re-parse for " + label); assertEquals(reparse.boolUniforms(), treeFed.boolUniforms(), "bool uniforms diverged for " + label); } + + private static byte[] spirv(String source, int glShaderType, String label) { + final SpirvCompiler.Result result = SpirvCompiler.compile(source, ShaderManager.shaderKindFor(glShaderType), label, SpirvCompiler.Options.vulkanForced460Core(), false); + assertNotNull(result.spirv(), () -> label + " failed to compile: " + result.error() + "\n" + source); + final ByteBuffer words = result.spirv().duplicate(); + final byte[] bytes = new byte[words.remaining()]; + words.get(bytes); + return bytes; + } } diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderSourceTransformCacheTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderSourceTransformCacheTest.java index 884046f60..ea8729ae7 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderSourceTransformCacheTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderSourceTransformCacheTest.java @@ -49,6 +49,23 @@ class ShaderSourceTransformCacheTest { } """; + private static final String CONDITIONAL_MAIN_VERT = """ + #version 330 core + #define OC_CORE + #ifdef OC_CORE + layout(location = 0) in vec3 a_Position; + uniform mat4 u_MVP; + void main() { + gl_Position = u_MVP * vec4(a_Position, 1.0); + } + #else + uniform mat4 u_ModelMatrix; + void main() { + gl_Position = gl_ModelViewProjectionMatrix * u_ModelMatrix * gl_Vertex; + } + #endif + """; + private static final String CLIP_Z = "gl_Position.z = gl_Position.z * 0.5 + gl_Position.w * 0.5;"; @BeforeEach @@ -93,6 +110,18 @@ class ShaderSourceTransformCacheTest { assertTrue(sm.getShaderSource(v).contains(CLIP_Z)); } + @Test + void mainInsideConditionalBlocksIsPreprocessedAndCarriesClipZOnce() { + final ShaderManager sm = new ShaderManager(null); + final int v = sm.createShader(GL20.GL_VERTEX_SHADER); + sm.shaderSource(v, CONDITIONAL_MAIN_VERT); + + final String out = sm.getShaderSource(v); + assertEquals(out.indexOf(CLIP_Z), out.lastIndexOf(CLIP_Z), out); + assertTrue(out.contains(CLIP_Z), out); + assertFalse(out.contains("gl_ModelViewProjectionMatrix"), out); + } + @Test void blockChangeMissesTheCache() { GLSMHooks.perFrameUniformBlock = new PerFrameUniformBlock(List.of(new Member("frameTimeCounter", UniformType.FLOAT))); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderTransformChain.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderTransformChain.java index d4aa55234..3e919317d 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderTransformChain.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderTransformChain.java @@ -18,6 +18,7 @@ final class ShaderTransformChain { private ShaderTransformChain() {} static String run(String source, int glShaderType) { + source = ShaderManager.preprocessed(source, glShaderType); final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(source, glShaderType, "test", true); String src = pre != null ? pre.rewrittenSource() : source; if (glShaderType == GL20.GL_VERTEX_SHADER) { diff --git a/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterial.java b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterial.java index b9c035779..7ca17dbca 100644 --- a/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterial.java +++ b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterial.java @@ -97,7 +97,7 @@ public final class TesrMaterial { return this; } - /** Depth prepass: writes depth only, no color. Mutually exclusive with noDepthWrite */ + /** Depth prepass: writes depth only, no color. Mutually exclusive with noDepthWrite and depthEqual */ public Builder depthOnly() { this.depthOnly = true; return this; @@ -109,13 +109,13 @@ public final class TesrMaterial { return this; } - /** Discards pixels with alpha at or below threshold - hard-edged cutout, no blending. ALPHA_TEST GREATER */ + /** Discards pixels with alpha at or below threshold. ALPHA_TEST GREATER */ public Builder cutout(float threshold) { this.cutoutAlpha = threshold; return this; } - /** Draws only where geometry already exists at the same depth; for decal/overlay passes. GL_EQUAL depth */ + /** GL_EQUAL depth for decal/overlay passes; matches only depth writers in the same tile-entity render or renderCached call */ public Builder depthEqual() { this.depthEqual = true; return this; @@ -165,6 +165,9 @@ public final class TesrMaterial { if (special != SpecialRender.NONE && shader != null) { throw new IllegalStateException("special() and shader() are mutually exclusive"); } + if (depthOnly && (noDepthWrite || depthEqual)) { + throw new IllegalStateException("depthOnly() excludes noDepthWrite() and depthEqual()"); + } return intern(new TesrMaterial(hasColor, red, green, blue, alpha, hasLightmap, lightmapX, lightmapY, noCull, unlit, noDepthWrite, depthOnly, stream, cutoutAlpha, depthEqual, unfilteredAtlas, transparency, special, shader)); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShader.java b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShader.java index 7f5117add..cbd8c7ac7 100644 --- a/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShader.java +++ b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShader.java @@ -1,17 +1,23 @@ package com.gtnewhorizons.angelica.api.tesr; import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.RequiredArgsConstructor; import lombok.experimental.Accessors; +import net.minecraft.util.ResourceLocation; -/** A registered mod-owned GL program; see {@link TesrShaders#register} */ +import javax.annotation.Nullable; + +/** A registered mod shader; see {@link TesrShaders#register} */ @Getter @Accessors(fluent = true) -@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +@AllArgsConstructor(access = AccessLevel.PACKAGE) public final class TesrShader { private final String name; - private final Runnable bind; - private final Runnable release; + @Nullable private final Runnable bind; + @Nullable private final Runnable release; + @Nullable private final ResourceLocation vertex; + @Nullable private final ResourceLocation fragment; + @Nullable private final TesrShaderUniforms uniforms; } diff --git a/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaderUniforms.java b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaderUniforms.java new file mode 100644 index 000000000..08ca78705 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaderUniforms.java @@ -0,0 +1,11 @@ +package com.gtnewhorizons.angelica.api.tesr; + +/** Uniforms of a source-registered {@link TesrShader}; render thread, program in use, no draws */ +public interface TesrShaderUniforms { + + /** After each variant link (first draw, resource reload): resolve locations, set constants */ + default void angelica$linked(int program, boolean instanced) {} + + /** Per layer bind: upload per-draw values; Angelica's own uniforms are not uploaded yet */ + void angelica$bound(int program, boolean instanced); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaders.java b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaders.java index bfae672ed..8910531f3 100644 --- a/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaders.java +++ b/src/main/java/com/gtnewhorizons/angelica/api/tesr/TesrShaders.java @@ -1,5 +1,8 @@ package com.gtnewhorizons.angelica.api.tesr; +import net.minecraft.util.ResourceLocation; + +import javax.annotation.Nullable; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -9,17 +12,36 @@ public final class TesrShaders { private TesrShaders() {} - /** Registers a GL program under a unique name and returns its handle, wraps draws of any {@link TesrMaterial} referencing it */ + /** Registers a GL program under a unique name and returns its handle, wraps draws of any {@link TesrMaterial} referencing it */ public static TesrShader register(String name, Runnable bind, Runnable release) { + checkName(name); + Objects.requireNonNull(bind, "bind"); + Objects.requireNonNull(release, "release"); + return add(new TesrShader(name, bind, release, null, null, null)); + } + + public static TesrShader register(String name, ResourceLocation vertex, ResourceLocation fragment) { + return register(name, vertex, fragment, null); + } + + /** Registers GLSL bodies (no {@code #version}) wrapped in Angelica's prelude, compiled at the first draw that binds them */ + public static TesrShader register(String name, ResourceLocation vertex, ResourceLocation fragment, @Nullable TesrShaderUniforms uniforms) { + checkName(name); + Objects.requireNonNull(vertex, "vertex"); + Objects.requireNonNull(fragment, "fragment"); + return add(new TesrShader(name, null, null, vertex, fragment, uniforms)); + } + + private static void checkName(String name) { final int colon = name.indexOf(':'); if (colon <= 0 || colon == name.length() - 1 || name.indexOf(':', colon + 1) >= 0) { throw new IllegalArgumentException("TESR shader name must be namespace:path, got '" + name + "'"); } - Objects.requireNonNull(bind, "bind"); - Objects.requireNonNull(release, "release"); - final TesrShader shader = new TesrShader(name, bind, release); - if (REGISTRY.putIfAbsent(name, shader) != null) { - throw new IllegalStateException("TESR shader '" + name + "' already registered"); + } + + private static TesrShader add(TesrShader shader) { + if (REGISTRY.putIfAbsent(shader.name(), shader) != null) { + throw new IllegalStateException("TESR shader '" + shader.name() + "' already registered"); } return shader; } diff --git a/src/main/java/com/gtnewhorizons/angelica/api/tesr/package-info.java b/src/main/java/com/gtnewhorizons/angelica/api/tesr/package-info.java index 7f01acc09..39aeb50d5 100644 --- a/src/main/java/com/gtnewhorizons/angelica/api/tesr/package-info.java +++ b/src/main/java/com/gtnewhorizons/angelica/api/tesr/package-info.java @@ -1,5 +1,5 @@ /** TESR mesh cache/batch API (Render thread only) */ -@API(owner = "angelica", provides = "angelica|tesr", apiVersion = "2") +@API(owner = "angelica", provides = "angelica|tesr", apiVersion = "3") package com.gtnewhorizons.angelica.api.tesr; import cpw.mods.fml.common.API; diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/RenderLayer.java b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/RenderLayer.java index 6f3076a69..7a5553cfa 100644 --- a/src/main/java/com/gtnewhorizons/angelica/compat/mojang/RenderLayer.java +++ b/src/main/java/com/gtnewhorizons/angelica/compat/mojang/RenderLayer.java @@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.api.tesr.TesrShader; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.hooks.TextureUtilHooks; import com.gtnewhorizons.angelica.rendering.tesr.DrawState; +import com.gtnewhorizons.angelica.rendering.tesr.TesrShaderPrograms; import com.gtnewhorizons.angelica.shadercompat.ShaderGlint; import it.unimi.dsi.fastutil.Hash; import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet; @@ -46,7 +47,7 @@ public final class RenderLayer implements BlendingStateHolder { private static final Hook BEACON_BEAM_HOOK = new Hook( () -> GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.BEACON_BEAM), GbufferPrograms::teardownSpecialRenderCondition); - private static final Reference2ObjectOpenHashMap SHADER_HOOKS = new Reference2ObjectOpenHashMap<>(); + private static final Reference2ObjectOpenHashMap> SHADER_HOOKS = new Reference2ObjectOpenHashMap<>(); private static final Reference2ObjectOpenHashMap PASS_HOOKS = new Reference2ObjectOpenHashMap<>(); private String name; @@ -100,7 +101,7 @@ public final class RenderLayer implements BlendingStateHolder { private static RenderLayer build(ResourceLocation texture, TesrMaterial material, PassOverride pass, float offsetFactor, float offsetUnits, int glintSlot, int cull, boolean lit, boolean lightmap, boolean noPass, boolean afterGlint) { final DrawState state = DrawState.forMaterial(material, cull, lit, lightmap, offsetFactor, offsetUnits); - final Hook hook = hookFor(material, noPass ? PassOverride.NONE : pass, glintSlot); + final Hook hook = hookFor(material, noPass ? PassOverride.NONE : pass, glintSlot, state.isLightmap()); final TransparencyType transparency = afterGlint ? TransparencyType.AFTER_GLINT : transparencyFor(state.getBlend()); final RenderLayer candidate = new RenderLayer(null, BatchVertexFormats.POSITION_COLOR_TEXTURE_LIGHTF_NORMAL, GL11.GL_QUADS, TESR_BUFFER_SIZE, state, texture, material.isUnfilteredAtlas(), transparency, hook, @@ -129,10 +130,6 @@ public final class RenderLayer implements BlendingStateHolder { } } - public static int cacheSize() { - return CACHE.size(); - } - private static TransparencyType transparencyFor(int blend) { return switch (blend) { case DrawState.OPAQUE -> TransparencyType.OPAQUE; @@ -141,7 +138,7 @@ public final class RenderLayer implements BlendingStateHolder { }; } - private static Hook hookFor(TesrMaterial material, PassOverride pass, int glintSlot) { + private static Hook hookFor(TesrMaterial material, PassOverride pass, int glintSlot, boolean lightmap) { switch (material.special()) { case GLINT -> { return glintHook(glintSlot); @@ -152,7 +149,7 @@ public final class RenderLayer implements BlendingStateHolder { case NONE -> { final TesrShader shader = material.shader(); if (shader != null) { - return shaderHook(shader); + return shaderHook(shader, pass, lightmap); } } } @@ -171,11 +168,40 @@ public final class RenderLayer implements BlendingStateHolder { return hook; } - private static Hook shaderHook(TesrShader shader) { - Hook hook = SHADER_HOOKS.get(shader); + private static Hook shaderHook(TesrShader shader, PassOverride pass, boolean lightmap) { + Reference2ObjectOpenHashMap byPass = SHADER_HOOKS.get(shader); + if (byPass == null) { + byPass = new Reference2ObjectOpenHashMap<>(); + SHADER_HOOKS.put(shader, byPass); + } + Hook[] byLightmap = byPass.get(pass); + if (byLightmap == null) { + byLightmap = new Hook[2]; + byPass.put(pass, byLightmap); + } + final int slot = lightmap ? 1 : 0; + Hook hook = byLightmap[slot]; if (hook == null) { - hook = new Hook(shader.bind(), shader.release()); - SHADER_HOOKS.put(shader, hook); + final Hook packHook = pass == PassOverride.NONE ? null : passHook(pass); + final TesrShaderPrograms.Entry entry = TesrShaderPrograms.entry(shader); + final boolean[] packDrew = new boolean[1]; + final Runnable bind = shader.bind() != null ? shader.bind() : () -> TesrShaderPrograms.bindBaked(entry, lightmap); + final Runnable release = shader.release() != null ? shader.release() : () -> TesrShaderPrograms.release(entry); + hook = new Hook(() -> { + packDrew[0] = TesrShaderPrograms.packActive(); + if (!packDrew[0]) { + bind.run(); + } else if (packHook != null) { + packHook.begin.run(); + } + }, () -> { + if (!packDrew[0]) { + release.run(); + } else if (packHook != null) { + packHook.end.run(); + } + }); + byLightmap[slot] = hook; } return hook; } diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/thaumcraft/PortalDrawer.java b/src/main/java/com/gtnewhorizons/angelica/compat/thaumcraft/PortalDrawer.java index 30e9a740d..9d44d1552 100644 --- a/src/main/java/com/gtnewhorizons/angelica/compat/thaumcraft/PortalDrawer.java +++ b/src/main/java/com/gtnewhorizons/angelica/compat/thaumcraft/PortalDrawer.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.client.rendering.GlUniformFloat3Array; import com.gtnewhorizons.angelica.client.rendering.GlUniformFloat4Array; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.StateSet; +import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey; import com.gtnewhorizons.angelica.glsm.states.FogState; import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray; import net.coderbot.iris.gl.framebuffer.GlFramebuffer; @@ -51,8 +52,6 @@ final class PortalDrawer { private static final int LIGHTMAP_UNIT = 1; private static final int FIELD_UNIT = 2; private static final int FACE_BYTES = FLOATS_PER_FACE * Float.BYTES; - private static final double LN2 = Math.log(2.0); - private static final double SQRT_LN2 = Math.sqrt(LN2); private static final float[] LAYER_DEPTH = new float[LAYERS]; private static final float[] LAYER_SCALE = new float[LAYERS]; private static final float[] LAYER_ANGLE = new float[LAYERS]; @@ -105,26 +104,12 @@ final class PortalDrawer { faces[i] = 0.0F; } - private static int fogMode() { - if (!GLStateManager.getFogMode().isEnabled()) return 0; - return switch (GLStateManager.getFogState().getFogMode()) { - case GL11.GL_LINEAR -> 1; - case GL11.GL_EXP -> 2; - case GL11.GL_EXP2 -> 3; - default -> 0; - }; - } - private static void uploadFog(Uniforms uniforms, int fogMode) { final FogState fog = GLStateManager.getFogState(); - final float start = fog.getStart(); - final float end = fog.getEnd(); - final float density = fog.getDensity(); - final float range = end - start; - FOG_PARAMS[0] = range != 0.0f ? -1.0f / range : 0.0f; - FOG_PARAMS[1] = range != 0.0f ? end / range : 1.0f; - FOG_PARAMS[2] = (float) (density / LN2); - FOG_PARAMS[3] = (float) (density / SQRT_LN2); + FOG_PARAMS[0] = fog.linearScale(); + FOG_PARAMS[1] = fog.linearBias(); + FOG_PARAMS[2] = fog.expScale(); + FOG_PARAMS[3] = fog.exp2Scale(); uniforms.fogParams.set(FOG_PARAMS); uniforms.fogColor.set((float) fog.getFogColor().x, (float) fog.getFogColor().y, (float) fog.getFogColor().z); uniforms.fogMode.setInt(fogMode); @@ -187,9 +172,9 @@ final class PortalDrawer { } void drawDirect(float[] faces, int count, int tunnelTexture, int fieldTexture, Frame frame, boolean fogged) { - final int fogMode = fogged ? fogMode() : 0; + final int fogMode = fogged ? FragmentKey.activeFogMode(GLStateManager.ctx()) : FragmentKey.FOG_NONE; final GlProgram program; - if (fogMode == 0) { + if (fogMode == FragmentKey.FOG_NONE) { if (direct == null) direct = buildProgram(false, false); program = direct; } else { @@ -276,7 +261,7 @@ final class PortalDrawer { uniforms.lightmapUv.set(frame.lightmapU, frame.lightmapV); uniforms.lightmapEnabled.setFloat(frame.lightmapEnabled ? 1.0F : 0.0F); if (uniforms.atlasSize != null) uniforms.atlasSize.setFloat(ATLAS_SIZE); - if (fogMode != 0) uploadFog(uniforms, fogMode); + if (fogMode != FragmentKey.FOG_NONE) uploadFog(uniforms, fogMode); GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + FIELD_UNIT); GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, fieldTexture); diff --git a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyCommandSender.java b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyCommandSender.java index 6e7ede2b4..3949accf9 100644 --- a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyCommandSender.java +++ b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyCommandSender.java @@ -22,6 +22,10 @@ final class FlybyCommandSender implements ICommandSender { this.anchor = anchor; } + EntityPlayerMP player() { + return this.player; + } + @Override public String getCommandSenderName() { return NAME; diff --git a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java index 18f304cd7..0297aa955 100644 --- a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java +++ b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyRunner.java @@ -32,6 +32,7 @@ import net.minecraft.world.World; import net.minecraft.world.WorldServer; import net.minecraft.world.chunk.Chunk; import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.storage.ThreadedFileIOBase; import net.minecraftforge.common.DimensionManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -39,7 +40,9 @@ import org.lwjgl.opengl.Display; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; @@ -83,6 +86,9 @@ public final class FlybyRunner { private String[] sceneCommands = NO_COMMANDS; private volatile boolean sceneClearRequested; private int sceneSpawnTotal; + private final List sceneDeferred = new ArrayList<>(); + private FlybyCommandSender sceneSender; + private int sceneTick; private boolean pauseOnLostFocusSaved; private boolean pauseOnLostFocusOverridden; private boolean vsyncOverridden; @@ -623,7 +629,7 @@ public final class FlybyRunner { public void onServerTick(TickEvent.ServerTickEvent event) { if (event.phase != TickEvent.Phase.END) return; final boolean clear = this.sceneClearRequested; - if (!clear && this.pendingRequest.get() == null && this.pendingDimensionPlayer.get() == null) return; + if (!clear && this.pendingRequest.get() == null && this.pendingDimensionPlayer.get() == null && this.sceneDeferred.isEmpty()) return; final MinecraftServer server = MinecraftServer.getServer(); if (server == null) return; @@ -645,6 +651,8 @@ public final class FlybyRunner { this.clearScene(server); } + this.runDeferred(server); + final FlybyRequest request = this.pendingRequest.getAndSet(null); if (request == null) return; @@ -669,6 +677,12 @@ public final class FlybyRunner { } } server.getConfigurationManager().saveAllPlayerData(); + // Queued chunk NBT aliases the live block arrays: without this drain the scene placed this tick reaches disk. + try { + ThreadedFileIOBase.threadedIOInstance.waitForFinish(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } worldChangesDiscarded = true; LOGGER.warn("Flyby: world saving is off until you leave this world"); @@ -755,14 +769,22 @@ public final class FlybyRunner { this.clearScene(server); final ChunkCoordinates anchor = new ChunkCoordinates(MathHelper.floor_double(request.originX()), MathHelper.floor_double(feetY + 0.5D), MathHelper.floor_double(request.originZ())); - final FlybyCommandSender sender = new FlybyCommandSender(player, anchor); + this.sceneSender = new FlybyCommandSender(player, anchor); + this.sceneTick = 0; for (String raw : this.sceneCommands) { final String line = FlybyScene.expand(raw, anchor.posX, anchor.posY, anchor.posZ); - if (server.getCommandManager().executeCommand(sender, line) == 0) { + if (!line.startsWith("after ")) { + this.runSceneLine(server, line); + continue; + } + final FlybyScene.Deferred deferred = FlybyScene.deferred(line); + if (deferred == null) { LOGGER.warn("flyby scene command did not execute: {}", line); + } else { + this.sceneDeferred.add(deferred); } } - LOGGER.info("Flyby scene: ran {} commands", this.sceneCommands.length); + LOGGER.info("Flyby scene: ran {} commands, {} deferred", this.sceneCommands.length - this.sceneDeferred.size(), this.sceneDeferred.size()); if (Tracy.ENABLED) Tracy.message("flyby scene " + this.sceneCommands.length + " commands"); int total = 0; for (WorldServer world : server.worldServers) { @@ -771,7 +793,32 @@ public final class FlybyRunner { this.sceneSpawnTotal = total; } + private void runSceneLine(MinecraftServer server, String line) { + if (!FlybyScene.run(server, this.sceneSender, line)) { + LOGGER.warn("flyby scene command did not execute: {}", line); + } + } + + private void runDeferred(MinecraftServer server) { + if (this.sceneDeferred.isEmpty()) return; + + this.sceneTick++; + int i = 0; + while (i < this.sceneDeferred.size()) { + final FlybyScene.Deferred deferred = this.sceneDeferred.get(i); + if (deferred.ticks() > this.sceneTick) { + i++; + continue; + } + this.sceneDeferred.remove(i); + LOGGER.info("Flyby scene: tick {} {}", this.sceneTick, deferred.line().length() > 80 ? deferred.line().substring(0, 80) : deferred.line()); + this.runSceneLine(server, deferred.line()); + } + } + private void clearScene(MinecraftServer server) { + this.sceneDeferred.clear(); + this.sceneSender = null; int remaining = 0; int cleared = 0; for (WorldServer world : server.worldServers) { diff --git a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyScene.java b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyScene.java index 0d8814383..0e8a5e07a 100644 --- a/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyScene.java +++ b/src/main/java/com/gtnewhorizons/angelica/debug/flyby/FlybyScene.java @@ -1,17 +1,38 @@ package com.gtnewhorizons.angelica.debug.flyby; +import net.minecraft.block.Block; +import net.minecraft.command.CommandBase; import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.JsonToNBT; +import net.minecraft.nbt.NBTBase; +import net.minecraft.nbt.NBTException; +import net.minecraft.nbt.NBTTagByteArray; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagShort; +import net.minecraft.nbt.NBTTagString; +import net.minecraft.server.MinecraftServer; import net.minecraft.tileentity.TileEntity; import net.minecraft.tileentity.TileEntityDispenser; +import net.minecraft.util.ChunkCoordinates; +import net.minecraft.util.MathHelper; +import net.minecraft.world.World; import net.minecraft.world.WorldServer; +import net.minecraftforge.common.util.Constants; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -20,6 +41,7 @@ final class FlybyScene { private static final Logger LOGGER = LogManager.getLogger("Angelica/Flyby"); private static final String[] NO_COMMANDS = new String[0]; private static final Pattern COORD_TOKEN = Pattern.compile("\\$([xyz])([+-]\\d+)?"); + private static final Pattern DEFINE_TOKEN = Pattern.compile("\\$\\{(\\w+)}"); static final String MARKER = "flyby"; @@ -33,13 +55,23 @@ final class FlybyScene { } final String[] commands = new String[lines.size()]; + final Map defines = new HashMap<>(); int n = 0; for (int i = 0; i < lines.size(); i++) { String line = lines.get(i).trim(); if (line.isEmpty() || line.charAt(0) == '#') continue; if (line.charAt(0) == '/') line = line.substring(1).trim(); if (line.isEmpty()) continue; - commands[n++] = line; + if (line.startsWith("define ")) { + final String[] parts = line.split("\\s+", 3); + if (parts.length == 3) { + defines.put(parts[1], parts[2]); + } else { + LOGGER.warn("Flyby scene '{}': malformed define '{}'", path, line); + } + continue; + } + commands[n++] = substitute(line, defines); } if (n == commands.length) return commands; final String[] trimmed = new String[n]; @@ -47,6 +79,19 @@ final class FlybyScene { return trimmed; } + private static String substitute(String line, Map defines) { + if (line.indexOf("${") < 0) return line; + final Matcher matcher = DEFINE_TOKEN.matcher(line); + final StringBuilder sb = new StringBuilder(line.length() + 256); + while (matcher.find()) { + final String value = defines.get(matcher.group(1)); + if (value == null) LOGGER.warn("Flyby scene: undefined '{}'", matcher.group()); + matcher.appendReplacement(sb, Matcher.quoteReplacement(value != null ? value : matcher.group())); + } + matcher.appendTail(sb); + return sb.toString(); + } + static String expand(String line, int x, int y, int z) { if (line.indexOf('$') < 0) return line; final Matcher matcher = COORD_TOKEN.matcher(line); @@ -64,6 +109,153 @@ final class FlybyScene { return sb.toString(); } + static Deferred deferred(String line) { + final String[] parts = line.split("\\s+", 3); + if (parts.length < 3) return null; + try { + final int ticks = Integer.parseInt(parts[1]); + return ticks > 0 ? new Deferred(ticks, parts[2]) : null; + } catch (NumberFormatException e) { + return null; + } + } + + static boolean run(MinecraftServer server, FlybyCommandSender sender, String line) { + try { + if (line.startsWith("tile ")) return placeTile(sender, line); + if (line.startsWith("activate ")) return activate(sender, line); + if (line.startsWith("fill ")) return fill(sender, line); + } catch (NBTException | RuntimeException e) { + LOGGER.warn("Flyby scene: '{}' failed", line, e); + return false; + } + return server.getCommandManager().executeCommand(sender, line) != 0; + } + + private static boolean placeTile(FlybyCommandSender sender, String line) throws NBTException { + final int brace = line.indexOf('{'); + if (brace < 0) return false; + final String[] args = line.substring(0, brace).trim().split("\\s+"); + if (args.length < 5 || args.length > 6 || !Block.blockRegistry.containsKey(args[4])) return false; + + final ChunkCoordinates pos = position(sender, args, 1); + final int x = pos.posX, y = pos.posY, z = pos.posZ; + final Block block = Block.getBlockFromName(args[4]); + final int meta = args.length == 6 ? Integer.parseInt(args[5]) : 0; + final World world = sender.getEntityWorld(); + if (!world.blockExists(x, y, z) || !block.hasTileEntity(meta)) return false; + if (!(JsonToNBT.func_150315_a(line.substring(brace)) instanceof NBTTagCompound parsed)) return false; + + final NBTTagCompound tag = decode(parsed); + tag.setInteger("x", x); + tag.setInteger("y", y); + tag.setInteger("z", z); + + final TileEntity tile = block.createTileEntity(world, meta); + if (tile == null) return false; + tile.readFromNBT(tag); + world.setBlock(x, y, z, block, meta, 3); + world.setTileEntity(x, y, z, tile); + return world.getTileEntity(x, y, z) == tile; + } + + private static boolean activate(FlybyCommandSender sender, String line) { + final String[] args = line.split("\\s+"); + final boolean sneak = args.length == 5 && "sneak".equals(args[4]); + if (args.length < 4 || args.length > 5 || (args.length == 5 && !sneak)) return false; + + final ChunkCoordinates pos = position(sender, args, 1); + final int x = pos.posX, y = pos.posY, z = pos.posZ; + final EntityPlayerMP player = sender.player(); + final World world = player.worldObj; + if (!world.blockExists(x, y, z)) return false; + + final double playerX = player.posX; + final double playerY = player.posY; + final double playerZ = player.posZ; + final boolean sneaking = player.isSneaking(); + final ItemStack[] hotbar = player.inventory.mainInventory; + final int slot = player.inventory.currentItem; + final ItemStack held = hotbar[slot]; + player.posX = x + 0.5D; + player.posY = y + 1.0D; + player.posZ = z + 0.5D; + player.setSneaking(sneak); + hotbar[slot] = null; + try { + return world.getBlock(x, y, z).onBlockActivated(world, x, y, z, player, 1, 0.5F, 1.0F, 0.5F); + } finally { + player.posX = playerX; + player.posY = playerY; + player.posZ = playerZ; + player.setSneaking(sneaking); + hotbar[slot] = held; + } + } + + private static boolean fill(FlybyCommandSender sender, String line) { + final String[] args = line.split("\\s+"); + if (args.length < 8 || args.length > 9 || !Block.blockRegistry.containsKey(args[7])) return false; + + final ChunkCoordinates from = position(sender, args, 1); + final ChunkCoordinates to = position(sender, args, 4); + final Block block = Block.getBlockFromName(args[7]); + final int meta = args.length == 9 ? Integer.parseInt(args[8]) : 0; + final World world = sender.getEntityWorld(); + for (int x = Math.min(from.posX, to.posX); x <= Math.max(from.posX, to.posX); x++) { + for (int y = Math.min(from.posY, to.posY); y <= Math.max(from.posY, to.posY); y++) { + for (int z = Math.min(from.posZ, to.posZ); z <= Math.max(from.posZ, to.posZ); z++) { + if (!world.blockExists(x, y, z)) return false; + world.setBlock(x, y, z, block, meta, 3); + } + } + } + return true; + } + + private static ChunkCoordinates position(FlybyCommandSender sender, String[] args, int first) { + return new ChunkCoordinates(coordinate(sender, 'x', args[first]), coordinate(sender, 'y', args[first + 1]), coordinate(sender, 'z', args[first + 2])); + } + + private static int coordinate(FlybyCommandSender sender, char axis, String token) { + final ChunkCoordinates anchor = sender.getPlayerCoordinates(); + final int base = switch (axis) { + case 'x' -> anchor.posX; + case 'y' -> anchor.posY; + default -> anchor.posZ; + }; + return MathHelper.floor_double(CommandBase.func_110666_a(sender, base, token)); + } + + private static NBTTagCompound decode(NBTTagCompound source) { + final NBTTagCompound decoded = new NBTTagCompound(); + final boolean itemStack = source.hasKey("Count"); + for (String key : source.func_150296_c()) { + String name = key.replace(';', ':'); + NBTBase value = source.getTag(key); + if (value instanceof NBTTagCompound compound) { + value = decode(compound); + } else if (value instanceof NBTTagList list && list.func_150303_d() == Constants.NBT.TAG_COMPOUND) { + final NBTTagList entries = new NBTTagList(); + for (int i = 0; i < list.tagCount(); i++) { + entries.appendTag(decode(list.getCompoundTagAt(i))); + } + value = entries; + } else if (value instanceof NBTTagString string) { + final String text = string.func_150285_a_(); + if (name.endsWith("#")) { + name = name.substring(0, name.length() - 1); + value = new NBTTagByteArray(text.getBytes(StandardCharsets.UTF_8)); + } else if (itemStack && "id".equals(name)) { + if (!Item.itemRegistry.containsKey(text)) throw new IllegalArgumentException("Unknown item " + text); + value = new NBTTagShort((short) Item.getIdFromItem((Item) Item.itemRegistry.getObject(text))); + } + } + decoded.setTag(name, value); + } + return decoded; + } + static int count(WorldServer world) { final List entities = world.loadedEntityList; int total = 0; @@ -107,5 +299,7 @@ final class FlybyScene { return tileEntity instanceof TileEntityDispenser dispenser && !dispenser.isInvalid() && dispenser.getClass() == TileEntityDispenser.class && MARKER.equals(dispenser.getInventoryName()); } + record Deferred(int ticks, String line) {} + private FlybyScene() {} } diff --git a/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java b/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java index 894a8e00a..bd9e2fef1 100644 --- a/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java +++ b/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java @@ -19,6 +19,7 @@ import com.gtnewhorizons.angelica.rendering.particles.ParticleInstancer; import com.gtnewhorizons.angelica.rendering.tesr.AngelicaTesrMeshCache; import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher; import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer; +import com.gtnewhorizons.angelica.rendering.tesr.TesrShaderPrograms; import net.coderbot.iris.pipeline.ShadowRenderer; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.WorldRenderer; @@ -76,7 +77,7 @@ public final class TracyFramePlots { private static final long P_FFP_PROGRAMS = Tracy.plotHandle("ffp.programs"); private static final PlotDelta P_FFP_VARIANT_SWITCHES = new PlotDelta("ffp.variantSwitches"); - private static final PlotDelta P_TESR_REBUILDS = new PlotDelta("tesr.rebuilds"); + private static final PlotDelta P_TESR_REBAKES = new PlotDelta("tesr.rebakes"); private static final PlotDelta P_TESR_RETAINED_DRAWS = new PlotDelta("tesr.retainedDraws"); private static final PlotDelta P_TESR_INSTANCED_DRAWS = new PlotDelta("tesr.instancedDraws"); private static final PlotDelta P_TESR_INSTANCED_INSTANCES = new PlotDelta("tesr.instancedInstances"); @@ -88,6 +89,10 @@ public final class TracyFramePlots { private static final long P_TESR_BUFFER_SOURCE_BYTES = Tracy.plotHandle("tesr.bufferSourceBytes", TracyBackend.PLOT_FORMAT_MEMORY); private static final PlotDelta P_TESR_CACHE_HITS = new PlotDelta("tesr.cacheHits"); private static final PlotDelta P_TESR_CACHE_MISSES = new PlotDelta("tesr.cacheMisses"); + private static final PlotDelta P_TESR_SHADER_BAKED_BINDS = new PlotDelta("tesr.shaderBakedBinds"); + private static final PlotDelta P_TESR_SHADER_INSTANCED_BINDS = new PlotDelta("tesr.shaderInstancedBinds"); + private static final PlotDelta P_TESR_SHADER_FALLBACKS = new PlotDelta("tesr.shaderFallbacks"); + private static final PlotDelta P_TESR_SHADER_LINK_FAILURES = new PlotDelta("tesr.shaderLinkFailures"); private static final PlotDelta P_TESR_MODEL_PARTS = new PlotDelta("tesr.modelParts"); private static final PlotDelta P_ARMOR_BASE_REUSES = new PlotDelta("glint.armorBaseReuses"); private static final PlotDelta P_GLINT_PASS_REUSES = new PlotDelta("glint.passReuses"); @@ -227,7 +232,7 @@ public final class TracyFramePlots { final TesrBatchRenderer tesr = TesrBatchRenderer.INSTANCE; final ModelPartBatcher batcher = ModelPartBatcher.INSTANCE; - P_TESR_REBUILDS.plot(tesr.statRebuilds()); + P_TESR_REBAKES.plot(tesr.statRebakes()); P_TESR_RETAINED_DRAWS.plot(tesr.statRetainedDraws()); P_TESR_INSTANCED_DRAWS.plot(tesr.statInstancedDraws() + batcher.statInstancedDraws()); P_TESR_INSTANCED_INSTANCES.plot(tesr.statInstancedInstances() + batcher.statInstancedInstances()); @@ -241,6 +246,10 @@ public final class TracyFramePlots { } P_TESR_CACHE_HITS.plot(AngelicaTesrMeshCache.cacheHits); P_TESR_CACHE_MISSES.plot(AngelicaTesrMeshCache.cacheMisses); + P_TESR_SHADER_BAKED_BINDS.plot(TesrShaderPrograms.bakedBinds); + P_TESR_SHADER_INSTANCED_BINDS.plot(TesrShaderPrograms.instancedBinds); + P_TESR_SHADER_FALLBACKS.plot(TesrShaderPrograms.fallbacks); + P_TESR_SHADER_LINK_FAILURES.plot(TesrShaderPrograms.linkFailures); P_TESR_MODEL_PARTS.plot(batcher.statParts()); P_ARMOR_BASE_REUSES.plot(batcher.statArmorBaseReuses()); P_GLINT_PASS_REUSES.plot(batcher.statGlintPassReuses()); diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCache.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCache.java index 14cb91214..fb29d9072 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCache.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCache.java @@ -33,6 +33,7 @@ public final class AngelicaTesrMeshCache { public static final long LRU_TIMEOUT_MS = 60_000L; + private static final BatchRendererSink SINK = new BatchRendererSink(); public static final AngelicaTesrMeshCache INSTANCE = new AngelicaTesrMeshCache(new GtnhMeshBackend(), System::currentTimeMillis); private static final Logger LOG = LogManager.getLogger("AngelicaTesrMeshCache"); @@ -53,7 +54,35 @@ public final class AngelicaTesrMeshCache { } public interface ReplaySink { + void beginScope(); + + void endScope(); + void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material); + + void retire(TemplateBuffer template); + } + + private static final class BatchRendererSink implements ReplaySink { + @Override + public void beginScope() { + TesrBatchRenderer.INSTANCE.beginScope(); + } + + @Override + public void endScope() { + TesrBatchRenderer.INSTANCE.endScope(); + } + + @Override + public void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material) { + TesrBatchRenderer.INSTANCE.queue(template, texture, material); + } + + @Override + public void retire(TemplateBuffer template) { + TesrBatchRenderer.INSTANCE.retire(template); + } } private static final class Bucket { @@ -77,7 +106,7 @@ public final class AngelicaTesrMeshCache { private static final Tracy.ZoneId Z_TESR_CAPTURE = Tracy.zoneId("tesrCapture", Tracy.COLOR_CLIENT); AngelicaTesrMeshCache(MeshBackend backend, LongSupplier clock) { - this(backend, clock, (template, texture, material) -> TesrBatchRenderer.INSTANCE.queue(template, texture, material)); + this(backend, clock, SINK); } AngelicaTesrMeshCache(MeshBackend backend, LongSupplier clock, ReplaySink replaySink) { @@ -113,10 +142,18 @@ public final class AngelicaTesrMeshCache { public void invalidate(Object key) { final Entry entry = entries.remove(key); if (entry != null) { - entry.buckets.clear(); + retireBuckets(entry); } } + private void retireBuckets(Entry entry) { + final List buckets = entry.buckets; + for (int i = 0, n = buckets.size(); i < n; i++) { + replaySink.retire(buckets.get(i).template); + } + buckets.clear(); + } + private Entry capture(Object key, Entry existing, TesrMeshBuilder builder) { if (Tracy.ENABLED) Tracy.beginZone(Z_TESR_CAPTURE); try { @@ -132,7 +169,7 @@ public final class AngelicaTesrMeshCache { entry = new Entry(); entries.put(key, entry); } else { - entry.buckets.clear(); + retireBuckets(entry); } final CaptureSink sink = new CaptureSink(entry); builder.angelica$build(sink); @@ -181,9 +218,14 @@ public final class AngelicaTesrMeshCache { private void replay(Entry entry) { final List buckets = entry.buckets; - for (int i = 0, n = buckets.size(); i < n; i++) { - final Bucket b = buckets.get(i); - replaySink.queue(b.template, b.texture, b.material); + replaySink.beginScope(); + try { + for (int i = 0, n = buckets.size(); i < n; i++) { + final Bucket b = buckets.get(i); + replaySink.queue(b.template, b.texture, b.material); + } + } finally { + replaySink.endScope(); } } @@ -194,6 +236,7 @@ public final class AngelicaTesrMeshCache { DroppedItemInstancer.clear(); BopParticleDescriptors.invalidate(); ShaderGlint.invalidate(); + TesrShaderPrograms.invalidateAll(); } } @@ -202,6 +245,9 @@ public final class AngelicaTesrMeshCache { } public void clear() { + for (final Entry entry : entries.values()) { + retireBuckets(entry); + } entries.clear(); } @@ -215,7 +261,7 @@ public final class AngelicaTesrMeshCache { while (it.hasNext()) { final Entry entry = it.next().getValue(); if (entry.lastUsedMs + LRU_TIMEOUT_MS < nowMs) { - entry.buckets.clear(); + retireBuckets(entry); it.remove(); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/DrawState.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/DrawState.java index d9ab38eab..eac5c9f5b 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/DrawState.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/DrawState.java @@ -25,7 +25,7 @@ public final class DrawState { public static final int ADDITIVE_ALPHA = 3; public static final int GLINT = 4; - private static final int LIGHTMAP_UNIT = 1; + static final int LIGHTMAP_UNIT = 1; private static final ObjectOpenCustomHashSet CACHE = new ObjectOpenCustomHashSet<>(HashStrategy.INSTANCE); @@ -33,10 +33,6 @@ public final class DrawState { CACHE.clear(); } - static int internedStateCount() { - return CACHE.size(); - } - @Getter private final int cull; @Getter private final int blend; @Getter private final boolean depthEqual; @@ -114,6 +110,10 @@ public final class DrawState { return cull != DISABLED && polygon.getFrontFace() == GL11.GL_CW ? cull | FRONT_FACE_CLOCKWISE : cull; } + public static int liveCull(TesrMaterial material) { + return material.isNoCull() ? DISABLED : liveCull(); + } + public static boolean liveLit(TesrMaterial material) { return GLStateManager.getLightingState().isEnabled() && !material.isUnlit(); } diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/InstancedTemplateRenderer.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/InstancedTemplateRenderer.java index aa97bea95..7d1e23e40 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/InstancedTemplateRenderer.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/InstancedTemplateRenderer.java @@ -56,6 +56,7 @@ final class InstancedTemplateRenderer { private final Reference2ObjectOpenHashMap meshes = new Reference2ObjectOpenHashMap<>(); private final ObjectArrayList bucketPool = new ObjectArrayList<>(); private final ObjectArrayList liveBuckets = new ObjectArrayList<>(); + private final ObjectArrayList retired = new ObjectArrayList<>(); private final InstanceRing ring; private ByteBuffer staging; @@ -314,6 +315,18 @@ final class InstancedTemplateRenderer { ring.postDraw(); } + void retire(TemplateBuffer template) { + retired.add(template); + } + + void releaseRetired() { + for (int i = 0, n = retired.size(); i < n; i++) { + final TemplateMesh mesh = meshes.remove(retired.get(i)); + if (mesh != null) delete(mesh); + } + retired.clear(); + } + void sweep(long now) { if (meshes.isEmpty()) return; final ObjectIterator it = meshes.values().iterator(); @@ -330,6 +343,7 @@ final class InstancedTemplateRenderer { delete(mesh); } meshes.clear(); + retired.clear(); liveBuckets.clear(); bucketPool.clear(); staging = null; diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/ModelPartBatcher.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/ModelPartBatcher.java index 274267a79..c4b41d079 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/ModelPartBatcher.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/ModelPartBatcher.java @@ -462,7 +462,7 @@ public final class ModelPartBatcher { if (!canQueueGlint() || GLStateManager.getActiveTextureUnit() != 0 || !activeGroups.canCopyInstances(EntityMaterials.ITEM_GLINT) || !MatrixHelper.isIdentity(GLStateManager.getTextures().getTextureUnitMatrix(0))) return false; final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL); final PolygonState polygon = GLStateManager.getPolygonState(); - final int cullCode = EntityMaterials.ITEM_GLINT.isNoCull() ? DrawState.DISABLED : DrawState.liveCull(); + final int cullCode = DrawState.liveCull(EntityMaterials.ITEM_GLINT); final RenderLayer layer = layerFor(HeldItemGlint.texture(), EntityMaterials.ITEM_GLINT, PassOverride.NONE, offset ? polygon.getOffsetFactor() : 0, offset ? polygon.getOffsetUnits() : 0, ShaderGlint.NO_TINT, cullCode, false, DrawState.liveLightmap()); activeGroups.queueGlintLayers(template, layer, EntityMaterials.ITEM_GLINT, currentModelView(), GLSMConfig.packedLastBrightness(), VANILLA_GLINT_COLOR, currentOverlay(), currentEntityInfo(0), HeldItemGlint.firstMatrix(), HeldItemGlint.secondMatrix()); @@ -506,7 +506,7 @@ public final class ModelPartBatcher { private void setArmorGlint(ResourceLocation texture, int glintSlot, boolean lit, Matrix4f textureMatrix, int color) { final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL); final PolygonState polygon = GLStateManager.getPolygonState(); - final int cullCode = EntityMaterials.GLINT.isNoCull() ? DrawState.DISABLED : DrawState.liveCull(); + final int cullCode = DrawState.liveCull(EntityMaterials.GLINT); armorGlintLayer = layerFor(texture, EntityMaterials.GLINT, PassOverride.NONE, offset ? polygon.getOffsetFactor() : 0, offset ? polygon.getOffsetUnits() : 0, glintSlot, cullCode, lit, DrawState.liveLightmap()); armorGlintTexture.set(textureMatrix); armorGlintColor = color; @@ -630,7 +630,7 @@ public final class ModelPartBatcher { final float offsetFactor = offset ? polygon.getOffsetFactor() : 0.0f; final float offsetUnits = offset ? polygon.getOffsetUnits() : 0.0f; final int glintSlot = material.special() == TesrMaterial.SpecialRender.GLINT ? lastBoundGlintSlot : ShaderGlint.NO_TINT; - final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull(); + final int cullCode = DrawState.liveCull(material); final boolean lit = DrawState.liveLit(material); final EntityTextureAtlas.Slot atlasSlot = texMatrix == null && cubesInsideTexture ? atlasSlotFor(material) : null; final RenderLayer layer = layerFor(atlasSlot != null ? Objects.requireNonNull(atlasSlot.page).location : texture, material, pass, offsetFactor, offsetUnits, glintSlot, cullCode, lit, DrawState.liveLightmap(), afterGlint); @@ -641,7 +641,7 @@ public final class ModelPartBatcher { final int overlayABGR = currentOverlay(); final long entityInfo = currentEntityInfo(CapturedRenderingState.INSTANCE.getCurrentRenderedItem()); final int packedLight = GLSMConfig.packedLastBrightness(); - activeGroups.queue(template, cubes, scale, layer, material, currentModelView(), packedLight, colorABGR, overlayABGR, entityInfo, entityId, texMatrix, atlasSlot); + activeGroups.queue(0L, template, cubes, scale, layer, material, currentModelView(), packedLight, colorABGR, overlayABGR, entityInfo, entityId, texMatrix, atlasSlot); parts++; return true; } @@ -698,7 +698,7 @@ public final class ModelPartBatcher { final PassOverride pass = shaderPipeline != null ? PassOverride.capture() : PassOverride.NONE; final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL); final PolygonState polygon = GLStateManager.getPolygonState(); - final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull(); + final int cullCode = DrawState.liveCull(material); final RenderLayer layer = layerFor(lastBoundLocation, material, pass, offset ? polygon.getOffsetFactor() : 0.0f, offset ? polygon.getOffsetUnits() : 0.0f, ShaderGlint.NO_TINT, cullCode, DrawState.liveLit(material), DrawState.liveLightmap()); // Flames carry entity_flame, which some packs may voxelize as a light source in the shadow pass diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroups.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroups.java index b3dcc0a7c..75ab1c2dd 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroups.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroups.java @@ -31,13 +31,16 @@ import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0; final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { - static final int PROMOTE_AFTER_REBUILDS = 3; - static final int DEMOTE_AFTER_STABLE_FRAMES = 4; + static final int PROMOTE_AFTER_REBAKES = 3; + static final long PROMOTE_WINDOW_MS = 500; + static final int PROMOTE_MAX_TEMPLATES = 8; + static final long STABILITY_PROBE_MS = 250; + static final int DEMOTE_AFTER_STABLE_PROBES = 2; static final long IDLE_RESET_FRAMES = 100; static final long GROUP_TTL_MS = AngelicaTesrMeshCache.LRU_TIMEOUT_MS; static final int MERGE_SCAN = 4; - private static final Tracy.ZoneId Z_TESR_REBUILD = Tracy.zoneId("tesrRebuild", Tracy.COLOR_CLIENT); + private static final Tracy.ZoneId Z_TESR_REBAKE = Tracy.zoneId("tesrRebake", Tracy.COLOR_CLIENT); private final AngelicaBufferSource source; private final LongSupplier clock; @@ -52,11 +55,12 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { private final Matrix4f savedTexMatrix = new Matrix4f(); private boolean texApplied; private final float[] matScratch = new float[16]; + private final TemplateBuffer[] budgetSeen = new TemplateBuffer[PROMOTE_MAX_TEMPLATES]; private double camX, camY, camZ; private long frameMark; GlintCapture glintCapture; - long rebuilds; + long rebakes; long retainedDraws; long streamPromotions; long streamedInstances; @@ -184,6 +188,171 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { } } + static final class Snapshot { + final ObjectArrayList templates = new ObjectArrayList<>(); + final IntArrayList lights = new IntArrayList(); + final IntArrayList colors = new IntArrayList(); + float[] matrices = new float[16]; + float[] texMatrices = new float[TesrAnchorMath.TEX_MATRIX_TERMS]; + int size; + long hashSum; + + boolean matches(int i, TemplateBuffer template, float[] matrix, int offset, int light, int color, Matrix4f texMatrix) { + return i < size && templates.get(i) == template && lights.getInt(i) == light && colors.getInt(i) == color + && TesrAnchorMath.sameTransform(matrices, i * 16, matrix, offset) + && TesrAnchorMath.sameTexMatrix(texMatrices, i * TesrAnchorMath.TEX_MATRIX_TERMS, texMatrix); + } + + private void ensure(int instances) { + if (instances * 16 <= matrices.length) return; + matrices = Arrays.copyOf(matrices, Math.max(instances * 16, matrices.length << 1)); + texMatrices = Arrays.copyOf(texMatrices, matrices.length / 16 * TesrAnchorMath.TEX_MATRIX_TERMS); + } + + boolean exchange(int i, TemplateBuffer template, float[] matrix, int light, int color, Matrix4f texMatrix) { + final boolean same = matches(i, template, matrix, 0, light, color, texMatrix); + ensure(i + 1); + System.arraycopy(matrix, 0, matrices, i * 16, 16); + if (i < templates.size()) { + templates.set(i, template); + lights.set(i, light); + colors.set(i, color); + } else { + templates.add(template); + lights.add(light); + colors.add(color); + } + TesrAnchorMath.storeTexMatrix(texMatrices, i * TesrAnchorMath.TEX_MATRIX_TERMS, texMatrix); + return same; + } + + void reset() { + templates.clear(); + lights.clear(); + colors.clear(); + size = 0; + } + + boolean sameTemplates(InstanceColumns cols) { + if (cols.size != size) return false; + for (int i = 0; i < size; i++) { + if (templates.get(i) != cols.templates.get(i)) return false; + } + return true; + } + + void copyFrom(InstanceColumns cols, ObjectArrayList instTexMatrices, long hashSum) { + final int n = cols.size; + ensure(n); + System.arraycopy(cols.matrices, 0, matrices, 0, n * 16); + templates.clear(); + templates.addAll(cols.templates); + lights.clear(); + lights.addElements(0, cols.lights.elements(), 0, n); + colors.clear(); + colors.addElements(0, cols.colors.elements(), 0, n); + for (int i = 0; i < n; i++) { + TesrAnchorMath.storeTexMatrix(texMatrices, i * TesrAnchorMath.TEX_MATRIX_TERMS, instTexMatrices.get(i)); + } + size = n; + this.hashSum = hashSum; + } + } + + static final class Probe { + final Snapshot snapshot = new Snapshot(); + boolean probing; + int count; + long hashSum; + boolean ordered; + int lastCount = -1; + long lastHashSum; + int stableProbes; + long lastMs; + + void start(long nowMs) { + probing = true; + lastMs = nowMs; + count = 0; + hashSum = 0; + ordered = true; + } + + void record(TemplateBuffer template, float[] matrix, int light, int color, Matrix4f texMatrix, long hash) { + ordered &= snapshot.exchange(count++, template, matrix, light, color, texMatrix); + hashSum += hash; + } + + boolean finish() { + final boolean stable = count == lastCount && (ordered || hashSum == lastHashSum); + lastCount = count; + lastHashSum = hashSum; + snapshot.size = count; + stableProbes = stable ? stableProbes + 1 : 0; + return stableProbes >= DEMOTE_AFTER_STABLE_PROBES; + } + + void reset() { + stableProbes = 0; + lastCount = -1; + snapshot.reset(); + probing = false; + } + } + + static final class Anchor { + long x, y, z; + + void set(Anchor other) { + x = other.x; + y = other.y; + z = other.z; + } + + boolean sameAs(Anchor other) { + return x == other.x && y == other.y && z == other.z; + } + + boolean stale(double camX, double camY, double camZ) { + return TesrAnchorMath.shouldReanchor(camX, camY, camZ, x, y, z); + } + + void moveTo(double camX, double camY, double camZ) { + x = TesrAnchorMath.anchorCoord(camX); + y = TesrAnchorMath.anchorCoord(camY); + z = TesrAnchorMath.anchorCoord(camZ); + } + } + + static final class DepthEqualSet { + final ObjectArrayList members = new ObjectArrayList<>(); + int depthEqualMembers; + int streamMembers; + boolean instanced; + long effectiveFrame = -1; + long checkedFrame = -1; + final Anchor anchor = new Anchor(); + long anchorFrame = -1; + + void add(Group group) { + members.add(group); + if (group.material.isDepthEqual()) depthEqualMembers++; + if (group.stream) streamMembers++; + group.depthEqualSet = this; + } + + void remove(Group group) { + members.remove(group); + if (group.material.isDepthEqual()) depthEqualMembers--; + if (group.stream) streamMembers--; + } + + void clearPath() { + instanced = false; + effectiveFrame = -1; + } + } + static final class Group { final RenderLayer layer; final TesrMaterial material; @@ -191,9 +360,11 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { final boolean opaque; final boolean stream; final boolean mergeRuns; + final boolean couplable; + long coupledScope; Class renderableClass; - int rebuildsWindow; - long anchorX, anchorY, anchorZ; + int rebakesWindow; + final Anchor anchor = new Anchor(); boolean anchored; long frameMark = -1; long lastUsedMs; @@ -208,16 +379,18 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { private int texMatrixPoolUsed; private final ObjectArrayList runPool = new ObjectArrayList<>(); private int runPoolUsed; - long hashAcc; + final Snapshot snapshot = new Snapshot(); + boolean dirty; + DepthEqualSet depthEqualSet; + final TesrShaderPrograms.Entry shaderEntry; - long builtHash; int builtCount = -1; int builtVertexCount; - boolean streaming; - int consecutiveRebuilds; - long frameHash; - long prevFrameHash; - int stableFrames; + boolean streamPath; + long promoteWindowStartMs; + int promoteWindowRebakes; + boolean queuedThisFrame; + Probe probe; final MeshBuffer mesh = new MeshBuffer(); ByteBuffer scratch; int meshBytes; @@ -362,12 +535,17 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { this.opaque = material.transparency() == TesrMaterial.Transparency.OPAQUE; this.stream = material.isStream(); this.mergeRuns = material.isDepthEqual() && material.isNoDepthWrite(); - this.streaming = stream; + this.streamPath = stream; + this.couplable = material.isDepthEqual() || !material.isNoDepthWrite(); + this.shaderEntry = TesrShaderPrograms.entryFor(material); } } private final Reference2ObjectOpenHashMap>> groups = new Reference2ObjectOpenHashMap<>(); private final Reference2ObjectOpenHashMap> byLayer = new Reference2ObjectOpenHashMap<>(); + private long openScope; + private DepthEqualSet openDepthEqualSet; + private final ObjectArrayList openWriters = new ObjectArrayList<>(); private static final int RECENT_GROUPS = 4; private final Group[] recentGroups = new Group[RECENT_GROUPS]; private int recentGroupNext; @@ -438,22 +616,27 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { private final DrawStateKey lookupState = new DrawStateKey(); private Group groupFor(RenderLayer layer, TesrMaterial material, int id) { - final boolean perInstance = instanceableMaterial(material); - final boolean entityKind = !perInstance && source.effectiveIdKind() == AngelicaBufferSource.GroupIdKind.ENTITY; - final CapturedRenderingState captured = CapturedRenderingState.INSTANCE; - final int entity = perInstance ? 0 : entityKind ? id : captured.getCurrentRenderedEntity(); - final int block = perInstance ? 0 : entityKind ? captured.getCurrentRenderedBlockEntity() : id; - final int item = perInstance ? 0 : captured.getCurrentRenderedItem(); - final int color = perInstance ? 0 : AngelicaBufferSource.packEntityColor(captured.getCurrentEntityColor()); Group group = null; + boolean keyed = false; + boolean perInstance = false; for (int i = 0; i < RECENT_GROUPS; i++) { final Group recent = recentGroups[i]; - if (recent != null && recent.layer == layer && recent.material == material && recent.state.matches(entity, block, item, color)) { + if (recent == null || recent.layer != layer || recent.material != material) continue; + if (!keyed) { + keyed = true; + perInstance = recent.stream && canInstance(recent); + keyLookup(perInstance, id); + } + if (recent.state.equals(lookupState)) { group = recent; break; } } if (group == null) { + if (!keyed) { + perInstance = streamsInstanced(material); + keyLookup(perInstance, id); + } Reference2ObjectOpenHashMap> byMaterial = groups.get(layer); if (byMaterial == null) { byMaterial = new Reference2ObjectOpenHashMap<>(); @@ -464,9 +647,9 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { byState = new Object2ObjectOpenHashMap<>(); byMaterial.put(material, byState); } - group = byState.get(lookupState.set(entity, block, item, color)); + group = byState.get(lookupState); if (group == null) { - final DrawStateKey key = new DrawStateKey().set(entity, block, item, color); + final DrawStateKey key = new DrawStateKey().set(lookupState.entity, lookupState.block, lookupState.item, lookupState.color); group = new Group(layer, material, perInstance ? 0 : id, key); group.lastUsedMs = nowMs; byState.put(key, group); @@ -481,6 +664,17 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { return group; } + private void keyLookup(boolean perInstance, int id) { + if (perInstance) { + lookupState.set(0, 0, 0, 0); + return; + } + final CapturedRenderingState captured = CapturedRenderingState.INSTANCE; + final boolean entityKind = source.effectiveIdKind() == AngelicaBufferSource.GroupIdKind.ENTITY; + lookupState.set(entityKind ? id : captured.getCurrentRenderedEntity(), entityKind ? captured.getCurrentRenderedBlockEntity() : id, + captured.getCurrentRenderedItem(), AngelicaBufferSource.packEntityColor(captured.getCurrentEntityColor())); + } + void forceStreaming(RenderLayer layer, TesrMaterial material, int blockEntityId) { promote(groupFor(layer, material, blockEntityId)); } @@ -491,21 +685,27 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { streamedInstances++; } - void queue(TemplateBuffer template, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix) { - queue(template, null, 1.0f, layer, material, currentMV, packedLight, colorABGR, overlayABGR, entityInfo, id, texMatrix, null); + void queue(long scope, TemplateBuffer template, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix) { + queue(scope, template, null, 1.0f, layer, material, currentMV, packedLight, colorABGR, overlayABGR, entityInfo, id, texMatrix, null); } boolean instancesCubes(TesrMaterial material) { - return instanceableMaterial(material) && glintCapture == null; + return streamsInstanced(material) && material.shader() == null && glintCapture == null; } - void queue(TemplateBuffer template, CubeParams[] cubes, float scale, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix, EntityTextureAtlas.Slot atlasSlot) { + void queue(long scope, TemplateBuffer template, CubeParams[] cubes, float scale, RenderLayer layer, TesrMaterial material, Matrix4f currentMV, int packedLight, int colorABGR, int overlayABGR, long entityInfo, int id, Matrix4f texMatrix, EntityTextureAtlas.Slot atlasSlot) { final Group group = groupFor(layer, material, id); touch(group, layer); - if (group.stream) { - final boolean instanceable = instanceable(group); + if (group.couplable) linkDepthScope(group, scope); + group.queuedThisFrame = true; + if (group.streamPath) { + final boolean instancedPath = drawsInstanced(group); + final boolean segments = !instancedPath && template.drawMode == layer.getDrawMode(); + if (group.probe != null && group.probe.probing && (instancedPath || segments)) { + probe(group, template, currentMV, packedLight, colorABGR, texMatrix); + } final boolean capturingBase = glintCapture != null && glintCapture.capturesBase(); - if (instanceable && cubes != null) { + if (instancedPath && cubes != null && material.shader() == null) { final TexRun run = group.openOrExtendRun(group.cubeColumns, texMatrix, false, capturingBase && group.opaque); group.cubeColumns.addCubes(deferred != null ? template : null, cubes, scale, currentMV, packedLight, colorABGR, overlayABGR, entityInfo, atlasSlot); run.last.end = group.cubeColumns.size; @@ -514,7 +714,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { if (glintCapture != null) glintCapture.record(group, group.cubeColumns, run, group.cubeColumns.size - 1, group.cubeColumns.size, cubes.length); return; } - if (instanceable) { + if (instancedPath) { final TexRun run = group.openOrExtendRun(group.templateColumns, texMatrix, false, capturingBase && group.opaque); group.templateColumns.add(template, currentMV, packedLight, colorABGR, overlayABGR, entityInfo); run.last.end = group.templateColumns.size; @@ -523,32 +723,168 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { if (glintCapture != null) glintCapture.record(group, group.templateColumns, run, group.templateColumns.size - 1, group.templateColumns.size, 1); return; } - if (glintCapture != null) glintCapture.invalidate(); - if (template.drawMode == layer.getDrawMode()) { + if (group.stream && glintCapture != null) glintCapture.invalidate(); + if (segments) { source.getBuffer(layer, id).addTemplateInstance(template, currentMV, scratchVec, colorABGR, packedLight, texMatrix); streamedInstances++; return; } + if (!group.stream) { + final DepthEqualSet set = activeDepthEqualSet(group); + if (set != null) { + switchSetNextFrame(set, false); + } else { + resetPromotion(group); + } + } } - scratchMat.set(baseMVInv).mul(currentMV); - TesrAnchorMath.toAnchorRelative(scratchMat, camX, camY, camZ, group.anchorX, group.anchorY, group.anchorZ); - long hash = TesrAnchorMath.instanceHash(System.identityHashCode(template), scratchMat, packedLight, colorABGR); - if (texMatrix != null) { - hash += TesrAnchorMath.texMatrixHash(texMatrix); + toAnchorRelative(group, currentMV); + final Matrix4f bakedTexMatrix = atlasSlot != null ? atlasSlot.uvMatrix : texMatrix; + final InstanceColumns cols = group.templateColumns; + final int index = cols.size; + cols.add(template, scratchMat, packedLight, colorABGR, overlayABGR, entityInfo); + group.addTexMatrix(bakedTexMatrix); + if (!group.dirty && !group.snapshot.matches(index, template, cols.matrices, index * 16, packedLight, colorABGR, bakedTexMatrix)) { + group.dirty = true; } - if (group.streaming && template.drawMode == layer.getDrawMode()) { - group.frameHash += hash; - source.getBuffer(layer, id).addTemplateInstance(template, currentMV, scratchVec, colorABGR, packedLight, texMatrix); - streamedInstances++; + } + + private void linkDepthScope(Group group, long scope) { + if (scope == 0) return; + if (scope != openScope) { + openScope = scope; + openDepthEqualSet = null; + openWriters.clear(); + } + if (group.coupledScope == scope) return; + group.coupledScope = scope; + if (openDepthEqualSet == null && !group.material.isDepthEqual()) { + openWriters.add(group); return; } - group.hashAcc += hash; - group.templateColumns.add(template, scratchMat, packedLight, colorABGR, overlayABGR, entityInfo); - group.addTexMatrix(texMatrix); + openDepthEqualSet = join(openDepthEqualSet, group); + for (int i = 0, n = openWriters.size(); i < n; i++) { + openDepthEqualSet = join(openDepthEqualSet, openWriters.get(i)); + } + openWriters.clear(); + if (!group.queuedThisFrame) adopt(group, openDepthEqualSet.instanced); + } + + private DepthEqualSet join(DepthEqualSet set, Group group) { + final DepthEqualSet own = group.depthEqualSet; + if (own == null) { + if (set == null) { + set = new DepthEqualSet(); + set.anchor.set(group.anchor); + } + set.add(group); + joinAnchor(set, group); + settle(set, set.instanced || promoted(group)); + return set; + } + if (set == null || set == own) return own; + final DepthEqualSet survivor = set.members.size() >= own.members.size() ? set : own; + final DepthEqualSet absorbed = survivor == set ? own : set; + final boolean instanced = wantsInstanced(survivor) || wantsInstanced(absorbed); + for (int i = 0, n = absorbed.members.size(); i < n; i++) { + final Group member = absorbed.members.get(i); + survivor.add(member); + joinAnchor(survivor, member); + } + settle(survivor, instanced); + absorbed.members.clear(); + absorbed.depthEqualMembers = 0; + absorbed.streamMembers = 0; + absorbed.clearPath(); + if (openDepthEqualSet == absorbed) openDepthEqualSet = survivor; + return survivor; + } + + private void joinAnchor(DepthEqualSet set, Group group) { + if (group.frameMark != frameMark) return; + if (!group.queuedThisFrame) { + adoptAnchor(group, set); + } else if (set.anchorFrame < 0) { + set.anchor.set(group.anchor); + set.anchorFrame = frameMark; + for (int i = 0, n = set.members.size(); i < n; i++) { + final Group member = set.members.get(i); + if (member.frameMark == frameMark && !member.queuedThisFrame) adoptAnchor(member, set); + } + } + } + + private static void adoptAnchor(Group group, DepthEqualSet set) { + if (group.anchored && group.anchor.sameAs(set.anchor)) return; + group.anchor.set(set.anchor); + group.anchored = true; + group.builtCount = -1; + } + + private static boolean promoted(Group group) { + return group.streamPath && !group.stream; + } + + private static boolean wantsInstanced(DepthEqualSet set) { + if (set.depthEqualMembers > 0) return set.instanced; + for (int i = 0, n = set.members.size(); i < n; i++) { + if (promoted(set.members.get(i))) return true; + } + return false; + } + + private void settle(DepthEqualSet set, boolean wantInstanced) { + switchSetNextFrame(set, (wantInstanced || set.streamMembers > 0) && allCanInstance(set)); + } + + private void refresh(DepthEqualSet set) { + if (set.checkedFrame == frameMark) return; + set.checkedFrame = frameMark; + if (set.anchorFrame != frameMark && set.anchor.stale(camX, camY, camZ)) set.anchor.moveTo(camX, camY, camZ); + set.anchorFrame = frameMark; + if ((set.streamMembers > 0 || set.instanced) && set.instanced != allCanInstance(set)) { + set.instanced = !set.instanced; + set.effectiveFrame = frameMark - 1; + } + } + + private void adopt(Group group, boolean instanced) { + if (group.streamPath == instanced) return; + if (instanced) { + promote(group); + } else { + group.streamPath = false; + clearPromotionTracking(group); + } + } + + private void toAnchorRelative(Group group, Matrix4f currentMV) { + scratchMat.set(baseMVInv).mul(currentMV); + TesrAnchorMath.toAnchorRelative(scratchMat, camX, camY, camZ, group.anchor.x, group.anchor.y, group.anchor.z); + } + + private void probe(Group group, TemplateBuffer template, Matrix4f currentMV, int packedLight, int colorABGR, Matrix4f texMatrix) { + toAnchorRelative(group, currentMV); + scratchMat.get(matScratch); + group.probe.record(template, matScratch, packedLight, colorABGR, texMatrix, instanceHash(template, scratchMat, packedLight, colorABGR, texMatrix)); + } + + private static long instanceHash(TemplateBuffer template, Matrix4f anchorRelative, int packedLight, int colorABGR, Matrix4f texMatrix) { + return TesrAnchorMath.instanceHash(System.identityHashCode(template), anchorRelative, packedLight, colorABGR) ^ TesrAnchorMath.texMatrixHash(texMatrix); + } + + private long hashSum(Group group) { + final InstanceColumns cols = group.templateColumns; + long sum = 0; + for (int i = 0, n = cols.size; i < n; i++) { + scratchMat.set(cols.matrices, i * 16); + sum += instanceHash(cols.templates.get(i), scratchMat, cols.lights.getInt(i), cols.colors.getInt(i), group.instTexMatrices.get(i)); + } + return sum; } boolean canCopyInstances(TesrMaterial material) { - return material.isStream() && instanceableMaterial(material); + return material.isStream() && streamsInstanced(material); } void copyInstances(InstanceColumns src, int start, int end, RenderLayer layer, TesrMaterial material, int packedLight, int colorABGR, int overlayABGR, Matrix4f texMatrix) { @@ -620,33 +956,40 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { private void touch(Group group, RenderLayer layer) { if (group.frameMark != frameMark) { + final DepthEqualSet set = activeDepthEqualSet(group); + if (set != null) refresh(set); if (frameMark - group.frameMark > IDLE_RESET_FRAMES) { - group.streaming = group.stream; - group.consecutiveRebuilds = 0; - group.stableFrames = 0; - group.builtCount = -1; - } else if (group.streaming && group.opaque && !group.stream) { - if (group.frameHash == group.prevFrameHash) { - if (++group.stableFrames >= DEMOTE_AFTER_STABLE_FRAMES) { - group.streaming = false; - group.consecutiveRebuilds = 0; - group.stableFrames = 0; - group.builtCount = -1; + resetPromotion(group); + } else if (promoted(group)) { + final boolean pinned = set != null && set.streamMembers > 0; + final Probe probe = group.probe; + if (probe.probing && probe.finish()) { + if (set == null) { + resetPromotion(group); + } else if (!pinned && set.instanced && allStable(set)) { + switchSetNextFrame(set, false); } - } else { - group.stableFrames = 0; } - group.prevFrameHash = group.frameHash; - group.frameHash = 0; + if (group.streamPath && !pinned && nowMs - probe.lastMs >= STABILITY_PROBE_MS) { + probe.start(nowMs); + } else { + probe.probing = false; + } } + if (set != null) { + if (frameMark > set.effectiveFrame) adopt(group, set.instanced); + } else if (group.stream && !group.streamPath) { + resetPromotion(group); + } + group.queuedThisFrame = false; group.frameMark = frameMark; group.lastUsedMs = nowMs; - group.hashAcc = 0; + group.dirty = false; group.clearInstances(); - if (!group.anchored || TesrAnchorMath.shouldReanchor(camX, camY, camZ, group.anchorX, group.anchorY, group.anchorZ)) { - group.anchorX = TesrAnchorMath.anchorCoord(camX); - group.anchorY = TesrAnchorMath.anchorCoord(camY); - group.anchorZ = TesrAnchorMath.anchorCoord(camZ); + if (set != null) { + adoptAnchor(group, set); + } else if (!group.anchored || group.anchor.stale(camX, camY, camZ)) { + group.anchor.moveTo(camX, camY, camZ); group.anchored = true; group.builtCount = -1; } @@ -654,6 +997,43 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { } } + private void switchSetNextFrame(DepthEqualSet set, boolean instanced) { + if (set.instanced == instanced) return; + set.instanced = instanced; + set.effectiveFrame = frameMark; + } + + private static DepthEqualSet activeDepthEqualSet(Group group) { + final DepthEqualSet set = group.depthEqualSet; + return set != null && set.depthEqualMembers > 0 ? set : null; + } + + private boolean allStable(DepthEqualSet set) { + for (int i = 0, n = set.members.size(); i < n; i++) { + final Group member = set.members.get(i); + if (member.streamPath && frameMark - member.frameMark <= IDLE_RESET_FRAMES && (member.probe == null || member.probe.stableProbes < DEMOTE_AFTER_STABLE_PROBES)) return false; + } + return true; + } + + private boolean allCanInstance(DepthEqualSet set) { + for (int i = 0, n = set.members.size(); i < n; i++) { + if (!canInstance(set.members.get(i))) return false; + } + return true; + } + + private static void resetPromotion(Group group) { + group.streamPath = group.stream; + clearPromotionTracking(group); + } + + private static void clearPromotionTracking(Group group) { + group.promoteWindowRebakes = 0; + if (group.probe != null) group.probe.reset(); + group.builtCount = -1; + } + @Override public boolean hasDraws(RenderLayer layer) { final ObjectArrayList list = byLayer.get(layer); @@ -671,7 +1051,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { if (list == null) return; boolean matrixAvailable = true; boolean cubeAvailable = true; - if (deferred != null && anyInstanceable(list)) { + if (deferred != null && anyQueuedInstanced(list)) { deferred.rebindCurrentPass(); matrixAvailable = deferred.hasInstancedVariant(Instancing.TEMPLATE); cubeAvailable = deferred.hasInstancedVariant(Instancing.CUBE); @@ -679,8 +1059,19 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { final int need = drawUninstanced(list, matrixAvailable, cubeAvailable); restoreMV(); if ((need & NEED_MATRIX) != 0) { - if (deferred != null) deferred.bindInstancedVariant(Instancing.TEMPLATE); - drawMatrixVariant(list, cubeAvailable); + boolean bound = true; + if (deferred != null) { + deferred.bindInstancedVariant(Instancing.TEMPLATE); + } else { + final TesrShaderPrograms.Entry layerShader = list.get(0).shaderEntry; + bound = layerShader == null || TesrShaderPrograms.bindInstanced(layerShader); + } + if (bound) { + drawMatrixVariant(list, cubeAvailable); + } else { + drawMatrixCpu(list, cubeAvailable); + restoreMV(); + } } if ((need & NEED_CUBE) != 0) { if (deferred != null) deferred.bindInstancedVariant(Instancing.CUBE); @@ -698,10 +1089,14 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { return g.frameMark == frameMark && (g.templateColumns.hasInstances() || g.cubeColumns.hasInstances()); } - private boolean anyInstanceable(ObjectArrayList list) { + private static boolean queuedInstanced(Group g) { + return !g.templateColumns.runs.isEmpty() || !g.cubeColumns.runs.isEmpty(); + } + + private boolean anyQueuedInstanced(ObjectArrayList list) { for (int i = 0, n = list.size(); i < n; i++) { final Group group = list.get(i); - if (live(group) && instanceable(group)) return true; + if (live(group) && queuedInstanced(group)) return true; } return false; } @@ -711,7 +1106,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { for (int i = 0, n = list.size(); i < n; i++) { final Group group = list.get(i); if (!live(group)) continue; - if (!instanceable(group)) { + if (!queuedInstanced(group)) { drawRetained(group); continue; } @@ -738,7 +1133,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { private void drawMatrixVariant(ObjectArrayList list, boolean cubeAvailable) { for (int i = 0, n = list.size(); i < n; i++) { final Group group = list.get(i); - if (!live(group) || !instanceable(group)) continue; + if (!live(group) || !queuedInstanced(group)) continue; if (group.templateColumns.hasInstances()) { final boolean fixedFunction = deferred == null && GLStateManager.getActiveProgram() == 0; drawTemplateRuns(group.templateColumns, group.material.special() == TesrMaterial.SpecialRender.GLINT && fixedFunction, group.material == EntityMaterials.ITEM_GLINT); @@ -750,38 +1145,106 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { restoreTexMatrix(); } + private void drawMatrixCpu(ObjectArrayList list, boolean cubeAvailable) { + for (int i = 0, n = list.size(); i < n; i++) { + final Group group = list.get(i); + if (!live(group) || !queuedInstanced(group)) continue; + if (group.templateColumns.hasInstances()) drawInstancesCpu(group, group.templateColumns); + if (!cubeAvailable && group.cubeColumns.hasInstances()) drawInstancesCpu(group, group.cubeColumns); + } + } + private void drawCubeVariant(ObjectArrayList list) { for (int i = 0, n = list.size(); i < n; i++) { final Group group = list.get(i); - if (!live(group) || !instanceable(group) || !group.cubeColumns.hasInstances()) continue; + if (!live(group) || !queuedInstanced(group) || !group.cubeColumns.hasInstances()) continue; drawCubeInstances(group); } restoreTexMatrix(); } - private boolean instanceable(Group group) { - return instanceableMaterial(group.material); + private boolean drawsInstanced(Group group) { + return group.streamPath && canInstance(group); } - private boolean instanceableMaterial(TesrMaterial material) { - return material.isStream() && instancedActive && (deferred == null || material.shader() == null); + private boolean streamsInstanced(TesrMaterial material) { + return material.isStream() && instancedActive && instancedShaderReady(TesrShaderPrograms.entryFor(material)); + } + + private boolean canInstance(Group group) { + return instancedActive && instancedShaderReady(group.shaderEntry); + } + + private boolean instancedShaderReady(TesrShaderPrograms.Entry entry) { + return entry == null || deferred != null || TesrShaderPrograms.instancedAvailable(entry); } private void drawRetained(Group group) { - if (group.builtCount != group.templateColumns.size || group.builtHash != group.hashAcc || !group.mesh.isUploaded()) { - if (group.mesh.isUploaded()) { - group.consecutiveRebuilds++; + final InstanceColumns cols = group.templateColumns; + final Snapshot snapshot = group.snapshot; + boolean instanceChanged = false; + if (group.builtCount < 0 || !group.mesh.isUploaded()) { + rebake(group, hashSum(group)); + } else if (group.dirty || group.builtCount != cols.size) { + final long sum = hashSum(group); + final boolean sameShape = snapshot.sameTemplates(cols); + final boolean reorderedOnly = sameShape && sum == snapshot.hashSum && group.material.transparency() != TesrMaterial.Transparency.TRANSLUCENT; + if (reorderedOnly) { + snapshot.copyFrom(cols, group.instTexMatrices, sum); + } else { + instanceChanged = sameShape && sum != snapshot.hashSum; + rebake(group, sum); } - rebuild(group); - } else { - group.consecutiveRebuilds = 0; } draw(group); - if (group.consecutiveRebuilds >= PROMOTE_AFTER_REBUILDS && !group.streaming && group.opaque) { - promote(group); + if (instanceChanged && !group.stream && !group.streamPath && rebakesTooOften(group) && withinTemplateBudget(cols.templates)) { + final DepthEqualSet set = activeDepthEqualSet(group); + if (set != null) { + if (set.streamMembers == 0 && allCanInstance(set)) switchSetNextFrame(set, true); + } else if (canInstance(group) || group.opaque && templatesMatchLayerDrawMode(group)) { + promote(group); + } } } + private boolean withinTemplateBudget(ObjectArrayList templates) { + final TemplateBuffer[] seen = budgetSeen; + int distinct = 0; + boolean within = true; + outer: + for (int i = 0, n = templates.size(); i < n; i++) { + final TemplateBuffer template = templates.get(i); + for (int j = 0; j < distinct; j++) { + if (seen[j] == template) continue outer; + } + if (distinct == PROMOTE_MAX_TEMPLATES) { + within = false; + break; + } + seen[distinct++] = template; + } + Arrays.fill(seen, 0, distinct, null); + return within; + } + + private static boolean templatesMatchLayerDrawMode(Group group) { + final ObjectArrayList templates = group.templateColumns.templates; + final int drawMode = group.layer.getDrawMode(); + for (int i = 0, n = templates.size(); i < n; i++) { + if (templates.get(i).drawMode != drawMode) return false; + } + return true; + } + + private boolean rebakesTooOften(Group group) { + if (group.promoteWindowRebakes == 0 || nowMs - group.promoteWindowStartMs > PROMOTE_WINDOW_MS) { + group.promoteWindowStartMs = nowMs; + group.promoteWindowRebakes = 1; + return false; + } + return ++group.promoteWindowRebakes >= PROMOTE_AFTER_REBAKES; + } + private void drawTemplateRuns(InstanceColumns cols, boolean combineGlint, boolean itemStencil) { for (int r = 0, n = cols.runs.size(); r < n; r++) { final TexRun run = cols.runs.get(r); @@ -920,8 +1383,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { final long base = memAddress0(fallbackScratch); long ptr = base; for (int i = start; i < end; i++) { - System.arraycopy(data.matrices, i * 16, matScratch, 0, 16); - scratchMat.set(matScratch); + scratchMat.set(data.matrices, i * 16); final int color = head.source != null ? head.color : data.colors.getInt(i); final EntityTextureAtlas.Slot slot = data.atlasSlot(head, i); ptr = VertexTransform.writeInstance(ptr, format, templates.get(i), scratchMat, scratchVec, color, data.lights.getInt(i), slot != null ? slot.uvMatrix : texMatrix); @@ -940,13 +1402,11 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { } private void promote(Group group) { - group.streaming = true; - streamPromotions++; - group.hashAcc = 0; - group.frameHash = 0; - group.prevFrameHash = 0; - group.stableFrames = 0; - group.builtCount = -1; + group.streamPath = true; + if (!group.stream) streamPromotions++; + if (group.probe == null) group.probe = new Probe(); + clearPromotionTracking(group); + group.probe.lastMs = nowMs; group.clearInstances(); group.mesh.delete(); group.meshBytes = 0; @@ -960,6 +1420,11 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { if (now - group.lastUsedMs <= GROUP_TTL_MS) continue; group.mesh.delete(); list.remove(i); + final DepthEqualSet set = group.depthEqualSet; + if (set != null) { + set.remove(group); + if (group.material.isDepthEqual() && set.depthEqualMembers == 0) set.clearPath(); + } final Reference2ObjectOpenHashMap> byMaterial = groups.get(group.layer); if (byMaterial != null) { final Object2ObjectOpenHashMap byId = byMaterial.get(group.material); @@ -979,6 +1444,9 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { } byLayer.clear(); groups.clear(); + openScope = 0; + openDepthEqualSet = null; + openWriters.clear(); Arrays.fill(recentGroups, null); fallbackMesh.delete(); fallbackScratch = null; @@ -1000,7 +1468,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { int count = 0; for (final ObjectArrayList list : byLayer.values()) { for (int i = 0, n = list.size(); i < n; i++) { - if (list.get(i).streaming) count++; + if (list.get(i).streamPath) count++; } } return count; @@ -1018,25 +1486,25 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { return bytes; } - private void rebuild(Group group) { - rebuilds++; - group.rebuildsWindow++; + private void rebake(Group group, long hashSum) { + rebakes++; + group.rebakesWindow++; if (Tracy.ENABLED) { - Tracy.beginZone(Z_TESR_REBUILD); + Tracy.beginZone(Z_TESR_REBAKE); Tracy.zoneText(attribution(group)); } try { - rebuildInner(group); + rebakeInner(group, hashSum); } finally { if (Tracy.ENABLED) Tracy.endZone(); } } - void collectRebuilders(ObjectArrayList out) { + void collectRebakers(ObjectArrayList out) { for (final ObjectArrayList list : byLayer.values()) { for (int i = 0, n = list.size(); i < n; i++) { final Group group = list.get(i); - if (group.rebuildsWindow > 0) out.add(group); + if (group.rebakesWindow > 0) out.add(group); } } } @@ -1057,7 +1525,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { return sb.toString(); } - private void rebuildInner(Group group) { + private void rebakeInner(Group group, long hashSum) { final VertexFormat format = group.layer.getVertexFormat(); final InstanceColumns cols = group.templateColumns; int totalVerts = 0; @@ -1070,16 +1538,15 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { final long base = memAddress0(group.scratch); long ptr = base; for (int i = 0, n = cols.size; i < n; i++) { - System.arraycopy(cols.matrices, i * 16, matScratch, 0, 16); - scratchMat.set(matScratch); + scratchMat.set(cols.matrices, i * 16); ptr = VertexTransform.writeInstance(ptr, format, cols.templates.get(i), scratchMat, scratchVec, cols.colors.getInt(i), cols.lights.getInt(i), group.instTexMatrices.get(i)); } + group.snapshot.copyFrom(cols, group.instTexMatrices, hashSum); group.scratch.position((int) (ptr - base)); group.scratch.flip(); group.builtVertexCount = totalVerts; group.mesh.upload(format, group.layer.getDrawMode(), group.scratch, group.builtVertexCount, true); group.meshBytes = bytes; - group.builtHash = group.hashAcc; group.builtCount = cols.size; } @@ -1087,7 +1554,7 @@ final class RetainedTesrGroups implements AngelicaBufferSource.LayerDrawHook { retainedDraws++; source.applyIdsAndRebind(group.state.entity, group.state.block, group.state.item); AngelicaBufferSource.setEntityColor(group.entityColor); - drawMV.set(baseMV).translate((float) (group.anchorX - camX), (float) (group.anchorY - camY), (float) (group.anchorZ - camZ)); + drawMV.set(baseMV).translate((float) (group.anchor.x - camX), (float) (group.anchor.y - camY), (float) (group.anchor.z - camZ)); saveMV(); GLStateManager.setModelViewMatrix(drawMV); group.mesh.render(); diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMath.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMath.java index d3b8721e5..5d0447655 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMath.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMath.java @@ -6,7 +6,10 @@ public final class TesrAnchorMath { public static final int ANCHOR_GRID = 16; public static final double REANCHOR_DISTANCE = 512.0; - private static final float HASH_QUANT = 4096f; + public static final float TRANSFORM_TOLERANCE = 1f / 2048; + public static final int TEX_MATRIX_TERMS = 7; + private static final int TEX_PRESENT = TEX_MATRIX_TERMS - 1; + private static final float HASH_QUANT = 1f / TRANSFORM_TOLERANCE; private TesrAnchorMath() {} @@ -27,6 +30,48 @@ public final class TesrAnchorMath { m.m32(m.m32() + (float) (camZ - anchorZ)); } + public static boolean close(float a, float b) { + return Math.abs(a - b) <= TRANSFORM_TOLERANCE || Float.floatToRawIntBits(a) == Float.floatToRawIntBits(b); + } + + public static boolean sameTransform(float[] a, int aOffset, float[] b, int bOffset) { + for (int column = 0; column < 16; column += 4) { + for (int row = 0; row < 3; row++) { + if (!close(a[aOffset + column + row], b[bOffset + column + row])) return false; + } + } + return true; + } + + public static void storeTexMatrix(float[] dst, int offset, Matrix4f t) { + if (t == null) { + for (int i = 0; i < TEX_MATRIX_TERMS; i++) dst[offset + i] = 0f; + return; + } + for (int i = 0; i < TEX_PRESENT; i++) dst[offset + i] = texTerm(t, i); + dst[offset + TEX_PRESENT] = 1f; + } + + public static boolean sameTexMatrix(float[] stored, int offset, Matrix4f t) { + final boolean storedPresent = stored[offset + TEX_PRESENT] != 0f; + if (t == null || !storedPresent) return t == null && !storedPresent; + for (int i = 0; i < TEX_PRESENT; i++) { + if (!close(stored[offset + i], texTerm(t, i))) return false; + } + return true; + } + + private static float texTerm(Matrix4f t, int i) { + return switch (i) { + case 0 -> t.m00(); + case 1 -> t.m01(); + case 2 -> t.m10(); + case 3 -> t.m11(); + case 4 -> t.m30(); + default -> t.m31(); + }; + } + public static long instanceHash(int templateIdentity, Matrix4f mA, int packedLight, int colorABGR) { long h = templateIdentity * 0x9E3779B97F4A7C15L; h = mix(h, quantize(mA.m00())); @@ -47,13 +92,9 @@ public final class TesrAnchorMath { } public static long texMatrixHash(Matrix4f t) { - long h = 0x2545F4914F6CDD1DL; - h = mix(h, quantize(t.m00())); - h = mix(h, quantize(t.m01())); - h = mix(h, quantize(t.m10())); - h = mix(h, quantize(t.m11())); - h = mix(h, quantize(t.m30())); - h = mix(h, quantize(t.m31())); + if (t == null) return 0L; + long h = 0x9E3779B97F4A7C15L; + for (int i = 0; i < TEX_PRESENT; i++) h = mix(h, quantize(texTerm(t, i))); return avalanche(h); } diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRenderer.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRenderer.java index 7f595ca39..26485de05 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRenderer.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRenderer.java @@ -68,6 +68,9 @@ public final class TesrBatchRenderer { final InstanceRing instanceRing = new InstanceRing(); final InstancedTemplateRenderer instancedRenderer = new InstancedTemplateRenderer(instanceRing); private int activePass = -1; + private long scopeCounter; + private long currentScope; + private int scopeDepth; private long lastSweepMs; private boolean deferredFlushPending; private RetainedTesrGroups pendingDeferredHook; @@ -82,6 +85,14 @@ public final class TesrBatchRenderer { } } + public void beginScope() { + if (scopeDepth++ == 0) currentScope = ++scopeCounter; + } + + public void endScope() { + if (--scopeDepth == 0) currentScope = 0; + } + public void beginPass(int passKey, Matrix4f baseMV, double camX, double camY, double camZ) { BatchStateFallback.install(); if (deferredFlushPending) { @@ -90,8 +101,10 @@ public final class TesrBatchRenderer { activePass = passKey; bufferSource.capturePassLighting(); BatchDrawDefaults.capturePassFramebuffer(); - retained[passKey].beginPass(baseMV, camX, camY, camZ, instancedCapable() ? instancedRenderer : null, deferredPipeline()); + final DeferredWorldRenderingPipeline deferred = deferredPipeline(); + retained[passKey].beginPass(baseMV, camX, camY, camZ, instancedCapable() ? instancedRenderer : null, deferred); if (passKey == PASS_MAIN_0) { + instancedRenderer.releaseRetired(); final long now = System.currentTimeMillis(); if (now - lastSweepMs >= SWEEP_INTERVAL_MS) { lastSweepMs = now; @@ -166,6 +179,10 @@ public final class TesrBatchRenderer { return hasPendingGeometry() && bufferSource.hasPendingLayers(); } + void retire(TemplateBuffer template) { + instancedRenderer.retire(template); + } + public void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material) { modelView.set(GLStateManager.getModelViewMatrix()); final int packedLight = currentPackedLight(material); @@ -174,7 +191,7 @@ public final class TesrBatchRenderer { final boolean offset = GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL); final PolygonState polygon = GLStateManager.getPolygonState(); final PassOverride pass = PassOverride.capture(); - final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull(); + final int cullCode = DrawState.liveCull(material); final boolean lit = DrawState.liveLit(material); final RenderLayer layer = layerFor(texture, material, pass, offset ? polygon.getOffsetFactor() : 0.0f, offset ? polygon.getOffsetUnits() : 0.0f, cullCode, lit, DrawState.liveLightmap()); @@ -182,7 +199,7 @@ public final class TesrBatchRenderer { ? CapturedRenderingState.INSTANCE.getCurrentRenderedEntity() : CapturedRenderingState.INSTANCE.getCurrentRenderedBlockEntity(); final long entityInfo = InstancedAttribs.packEntityInfo(CapturedRenderingState.INSTANCE.getCurrentRenderedEntity(), CapturedRenderingState.INSTANCE.getCurrentRenderedBlockEntity(), CapturedRenderingState.INSTANCE.getCurrentRenderedItem()); - retained[activePass].queue(template, layer, material, modelView, packedLight, colorABGR, + retained[activePass].queue(currentScope, template, layer, material, modelView, packedLight, colorABGR, AngelicaBufferSource.packEntityColor(CapturedRenderingState.INSTANCE.getCurrentEntityColor()), entityInfo, blockEntityId, captureTextureMatrix()); } else { drawImmediate(template, texture, material, packedLight, colorABGR); @@ -313,7 +330,7 @@ public final class TesrBatchRenderer { } private RenderLayer noPassLayerFor(ResourceLocation texture, TesrMaterial material) { - final int cullCode = material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull(); + final int cullCode = DrawState.liveCull(material); final boolean lit = DrawState.liveLit(material); final boolean lightmap = DrawState.liveLightmap(); if (texture == lastImmediateTexture && material == lastImmediateMaterial && cullCode == lastImmediateCull && lit == lastImmediateLit @@ -460,15 +477,15 @@ public final class TesrBatchRenderer { } } - private long lastRebuilds, lastRetainedDraws, lastStreamed, lastInstInstances; + private long lastRebakes, lastRetainedDraws, lastStreamed, lastInstInstances; private long lastSummaryNanos; public String getDebugSummaryLine() { - long rebuilds = 0, retainedDraws = 0, streamed = 0, instInstances = 0, retainedBytes = 0; + long rebakes = 0, retainedDraws = 0, streamed = 0, instInstances = 0, retainedBytes = 0; int groupCount = 0, streamingCount = 0; for (int i = 0; i < PASS_COUNT; i++) { final RetainedTesrGroups r = retained[i]; - rebuilds += r.rebuilds; + rebakes += r.rebakes; retainedDraws += r.retainedDraws; streamed += r.streamedInstances; instInstances += r.instancedInstances; @@ -480,13 +497,13 @@ public final class TesrBatchRenderer { final long elapsed = now - lastSummaryNanos; final String line = String.format("TESR: %d grp (%d strm), reb %d/s, drw %d/s, inst %d/s, stream %d/s, mem %.1fMB", groupCount, streamingCount, - perSecond(rebuilds - lastRebuilds, elapsed), + perSecond(rebakes - lastRebakes, elapsed), perSecond(retainedDraws - lastRetainedDraws, elapsed), perSecond(instInstances - lastInstInstances, elapsed), perSecond(streamed - lastStreamed, elapsed), (retainedBytes + bufferSource.allocatedBytes() + instancedRenderer.meshBytes()) / 1048576.0); lastSummaryNanos = now; - lastRebuilds = rebuilds; + lastRebakes = rebakes; lastRetainedDraws = retainedDraws; lastStreamed = streamed; lastInstInstances = instInstances; @@ -506,37 +523,37 @@ public final class TesrBatchRenderer { retainedBytes / 1048576.0, bufferSource.allocatedBytes() / 1048576.0, instancedRenderer.meshCount(), instancedRenderer.meshBytes() / 1048576.0, AngelicaTesrMeshCache.INSTANCE.size()); - final String attribution = rebuildAttributionLine(); + final String attribution = rebakeAttributionLine(); return attribution == null ? Arrays.asList(mem) : Arrays.asList(mem, attribution); } - private final ObjectArrayList rebuilders = new ObjectArrayList<>(); + private final ObjectArrayList rebakers = new ObjectArrayList<>(); - private String rebuildAttributionLine() { - rebuilders.clear(); + private String rebakeAttributionLine() { + rebakers.clear(); for (int i = 0; i < PASS_COUNT; i++) { - retained[i].collectRebuilders(rebuilders); + retained[i].collectRebakers(rebakers); } - if (rebuilders.isEmpty()) return null; - rebuilders.sort((a, b) -> Integer.compare(b.rebuildsWindow, a.rebuildsWindow)); + if (rebakers.isEmpty()) return null; + rebakers.sort((a, b) -> Integer.compare(b.rebakesWindow, a.rebakesWindow)); final StringBuilder sb = new StringBuilder("TESR reb:"); - final int shown = Math.min(3, rebuilders.size()); + final int shown = Math.min(3, rebakers.size()); for (int i = 0; i < shown; i++) { - final RetainedTesrGroups.Group group = rebuilders.get(i); + final RetainedTesrGroups.Group group = rebakers.get(i); if (i > 0) sb.append(','); - sb.append(' ').append(group.rebuildsWindow).append("x ").append(RetainedTesrGroups.attribution(group)).append(' ').append(group.builtVertexCount).append('v'); + sb.append(' ').append(group.rebakesWindow).append("x ").append(RetainedTesrGroups.attribution(group)).append(' ').append(group.builtVertexCount).append('v'); } - if (rebuilders.size() > shown) { - sb.append(" +").append(rebuilders.size() - shown); + if (rebakers.size() > shown) { + sb.append(" +").append(rebakers.size() - shown); } - for (int i = 0, n = rebuilders.size(); i < n; i++) { - rebuilders.get(i).rebuildsWindow = 0; + for (int i = 0, n = rebakers.size(); i < n; i++) { + rebakers.get(i).rebakesWindow = 0; } - rebuilders.clear(); + rebakers.clear(); return sb.toString(); } - public long statRebuilds() { return sumStat(0); } + public long statRebakes() { return sumStat(0); } public long statRetainedDraws() { return sumStat(1); } public long statInstancedDraws() { return sumStat(2); } public long statInstancedInstances() { return sumStat(3); } @@ -548,7 +565,7 @@ public final class TesrBatchRenderer { for (int i = 0; i < PASS_COUNT; i++) { final RetainedTesrGroups r = retained[i]; v += switch (which) { - case 0 -> r.rebuilds; + case 0 -> r.rebakes; case 1 -> r.retainedDraws; case 2 -> r.instancedDraws; case 3 -> r.instancedInstances; diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatch.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatch.java index 1b380442e..d761889f4 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatch.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatch.java @@ -2,28 +2,13 @@ package com.gtnewhorizons.angelica.rendering.tesr; import com.gtnewhorizons.angelica.api.tesr.TesrMeshProvider; import com.gtnewhorizons.angelica.glsm.GLStateManager; -import it.unimi.dsi.fastutil.ints.Int2IntMap; -import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; -import net.coderbot.iris.block_rendering.BlockRenderingSettings; import net.coderbot.iris.uniforms.CapturedRenderingState; -import net.minecraft.block.Block; import net.minecraft.tileentity.TileEntity; public final class TesrProviderDispatch { private TesrProviderDispatch() {} - public static int resolveBlockEntityId(TileEntity te) { - if (te == null) return 0; - final Block block = te.getBlockType(); - if (block == null) return 0; - final Reference2ObjectMap matches = BlockRenderingSettings.INSTANCE.getBlockMetaMatches(); - if (matches == null) return 0; - final Int2IntMap metaMap = matches.get(block); - if (metaMap == null) return 0; - return Math.max(0, metaMap.get(blockMetadata(te))); - } - /** * Item renderers often hand a world-less dummy TE to the dispatcher, and some mods override getBlockMetadata() * to read the world unconditionally (e.g. EFR shulker boxes), so don't call it without a world. diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrelude.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrelude.java new file mode 100644 index 000000000..f56f6f08e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrelude.java @@ -0,0 +1,171 @@ +package com.gtnewhorizons.angelica.rendering.tesr; + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage; +import com.gtnewhorizons.angelica.glsm.ffp.FfpGlsl; +import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey; +import com.gtnewhorizons.angelica.glsm.ffp.InstancedGlslHelpers; +import com.gtnewhorizons.angelica.glsm.ffp.Instancing; +import com.gtnewhorizons.angelica.glsm.states.FogState; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +final class TesrShaderPrelude { + + static final String LAYER_SAMPLER = "angelica_LayerTexture"; + static final String LIGHTMAP_SAMPLER = "angelica_LightmapTexture"; + + private static final Logger LOG = LogManager.getLogger("AngelicaTesrShaders"); + private static final String VERSION_DIRECTIVE = "#version"; + private static final ObjectOpenHashSet WARNED_VERSION = new ObjectOpenHashSet<>(); + + private static final String VERTEX_BAKED = buildVertex(false); + private static final String VERTEX_INSTANCED = buildVertex(true); + private static final String FRAGMENT = buildFragment(); + + private TesrShaderPrelude() {} + + static String vertex(boolean instanced) { + return instanced ? VERTEX_INSTANCED : VERTEX_BAKED; + } + + static String fragment() { + return FRAGMENT; + } + + private static String buildVertex(boolean instanced) { + final StringBuilder sb = new StringBuilder(2048); + sb.append("#version 330 core\n\n"); + sb.append("#define angelica_out out\n"); + sb.append("invariant gl_Position;\n\n"); + + sb.append("// Vertex attributes\n"); + attribute(sb, Usage.POSITION, "vec3", "angelica_Position"); + attribute(sb, Usage.COLOR, "vec4", "angelica_VertexColor"); + attribute(sb, Usage.PRIMARY_UV, "vec2", "angelica_TexCoord"); + attribute(sb, Usage.NORMAL, "vec3", "angelica_Normal"); + if (instanced) { + for (String d : InstancedGlslHelpers.attributeDecls("angelica_", Instancing.TEMPLATE)) { + sb.append(d).append('\n'); + } + } else { + attribute(sb, Usage.SECONDARY_UV, "vec2", "angelica_VertexLightmap"); + } + sb.append('\n'); + + sb.append("// Uniforms\n"); + if (instanced) { + sb.append("uniform mat4 iris_TextureMatrix;\n"); + } else { + sb.append("uniform mat4 angelica_ModelViewMatrix;\n"); + sb.append("uniform mat4 angelica_ModelViewProjectionMatrix;\n"); + sb.append("uniform mat3 angelica_NormalMatrix;\n"); + } + sb.append("uniform mat4 angelica_ProjectionMatrix;\n"); + sb.append("uniform mat4 angelica_LightmapTextureMatrix;\n"); + sb.append("uniform int angelica_FogDistanceMode;\n"); + sb.append("struct angelica_LightSourceParameters { vec4 diffuse; vec4 position; };\n"); + sb.append("uniform angelica_LightSourceParameters angelica_LightSource[2];\n\n"); + + sb.append("// Accessors\n"); + if (instanced) { + sb.append("mat4 angelica_modelView() { return ") + .append(InstancedGlslHelpers.mat4FromRows("angelica_InstRow0", "angelica_InstRow1", "angelica_InstRow2")).append("; }\n"); + sb.append("vec4 angelica_eyePosition() { return angelica_modelView() * vec4(angelica_Position, 1.0); }\n"); + sb.append("vec4 angelica_clipPosition() { return angelica_ProjectionMatrix * angelica_eyePosition(); }\n"); + sb.append("vec3 angelica_eyeNormal() { return normalize(mat3(angelica_modelView()) * angelica_Normal); }\n"); + sb.append("vec4 angelica_color() { return angelica_VertexColor * angelica_InstColor; }\n"); + sb.append("vec2 angelica_uv() { return (iris_TextureMatrix * vec4(angelica_TexCoord, 0.0, 1.0)).xy; }\n"); + sb.append("vec2 angelica_lightmapUv() { return (angelica_LightmapTextureMatrix * vec4(angelica_InstLightmap, 0.0, 1.0)).xy; }\n"); + } else { + sb.append("mat4 angelica_modelView() { return angelica_ModelViewMatrix; }\n"); + sb.append("vec4 angelica_eyePosition() { return angelica_ModelViewMatrix * vec4(angelica_Position, 1.0); }\n"); + sb.append("vec4 angelica_clipPosition() { return angelica_ModelViewProjectionMatrix * vec4(angelica_Position, 1.0); }\n"); + sb.append("vec3 angelica_eyeNormal() { return normalize(angelica_NormalMatrix * angelica_Normal); }\n"); + sb.append("vec4 angelica_color() { return angelica_VertexColor; }\n"); + sb.append("vec2 angelica_uv() { return angelica_TexCoord; }\n"); + sb.append("vec2 angelica_lightmapUv() { return (angelica_LightmapTextureMatrix * vec4(angelica_VertexLightmap, 0.0, 1.0)).xy; }\n"); + } + sb.append("float angelica_fogCoord() {\n"); + sb.append(" vec4 eye = angelica_eyePosition();\n"); + for (final int mode : new int[] { FogState.DISTANCE_EYE_RADIAL, FogState.DISTANCE_EYE_PLANE }) { + sb.append(" if (angelica_FogDistanceMode == ").append(mode).append(") return ").append(FfpGlsl.fogDistance(mode, "eye")).append(";\n"); + } + sb.append(" return ").append(FfpGlsl.fogDistance(FogState.DISTANCE_EYE_PLANE_ABS, "eye")).append(";\n"); + sb.append("}\n"); + sb.append("vec3 angelica_lightDirection(int i) { return angelica_LightSource[i].position.xyz; }\n"); + sb.append("vec3 angelica_lightDiffuse(int i) { return angelica_LightSource[i].diffuse.rgb; }\n\n"); + return sb.toString(); + } + + private static String buildFragment() { + final StringBuilder sb = new StringBuilder(1024); + sb.append("#version 330 core\n\n"); + sb.append("#define angelica_in in\n\n"); + + sb.append("// Outputs\n"); + sb.append("layout(location = 0) out vec4 angelica_FragColor;\n\n"); + + sb.append("// Uniforms\n"); + sb.append("uniform sampler2D ").append(LAYER_SAMPLER).append(";\n"); + sb.append("uniform sampler2D ").append(LIGHTMAP_SAMPLER).append(";\n"); + sb.append("uniform int angelica_FogMode;\n"); + sb.append("uniform vec4 angelica_FogParams;\n"); + sb.append("uniform vec4 angelica_FogColor;\n"); + sb.append("uniform float angelica_currentAlphaTest;\n\n"); + + sb.append("// Helpers\n"); + sb.append("vec4 angelica_sampleTexture(vec2 uv) { return texture(").append(LAYER_SAMPLER).append(", uv); }\n"); + sb.append("vec4 angelica_sampleLightmap(vec2 uv) { return texture(").append(LIGHTMAP_SAMPLER).append(", uv); }\n"); + sb.append("vec3 angelica_fog(vec3 color, float fogCoord) {\n"); + sb.append(" float f;\n"); + fogBranch(sb, "if", FragmentKey.FOG_LINEAR); + fogBranch(sb, "} else if", FragmentKey.FOG_EXP); + fogBranch(sb, "} else if", FragmentKey.FOG_EXP2); + sb.append(" } else {\n"); + sb.append(" return color;\n"); + sb.append(" }\n"); + sb.append(" return mix(angelica_FogColor.rgb, color, clamp(f, 0.0, 1.0));\n"); + sb.append("}\n"); + sb.append("bool angelica_alphaTest(float a) { return ").append(FfpGlsl.quantizeAlpha("a")) + .append(" > angelica_currentAlphaTest * 255.0; }\n\n"); + return sb.toString(); + } + + private static void fogBranch(StringBuilder sb, String keyword, int fogMode) { + sb.append(" ").append(keyword).append(" (angelica_FogMode == ").append(fogMode).append(") {\n"); + FfpGlsl.appendFogFactor(sb, " ", fogMode, "fogCoord", "angelica_FogParams"); + } + + static String stripVersion(String shaderName, String body) { + final int start = skipCommentsAndWhitespace(body); + if (!body.startsWith(VERSION_DIRECTIVE, start)) return body; + if (WARNED_VERSION.add(shaderName)) { + LOG.warn("TESR shader {} declares #version; Angelica supplies it, the line is ignored", shaderName); + } + final int end = body.indexOf('\n', start); + return end < 0 ? "" : body.substring(end + 1); + } + + private static int skipCommentsAndWhitespace(String s) { + int i = 0; + while (i < s.length()) { + if (Character.isWhitespace(s.charAt(i))) { + i++; + } else if (s.startsWith("//", i)) { + final int eol = s.indexOf('\n', i); + i = eol < 0 ? s.length() : eol + 1; + } else if (s.startsWith("/*", i)) { + final int close = s.indexOf("*/", i + 2); + i = close < 0 ? s.length() : close + 2; + } else { + break; + } + } + return i; + } + + private static void attribute(StringBuilder sb, Usage usage, String type, String name) { + sb.append("layout(location = ").append(usage.getAttributeLocation()).append(") in ").append(type).append(' ').append(name).append(";\n"); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrograms.java b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrograms.java new file mode 100644 index 000000000..219e15722 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPrograms.java @@ -0,0 +1,245 @@ +package com.gtnewhorizons.angelica.rendering.tesr; + +import com.gtnewhorizons.angelica.api.tesr.TesrMaterial; +import com.gtnewhorizons.angelica.api.tesr.TesrShader; +import com.gtnewhorizons.angelica.api.tesr.TesrShaderUniforms; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.mixins.interfaces.EntityRendererAccessor; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.coderbot.iris.Iris; +import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline; +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public final class TesrShaderPrograms { + + private static final Logger LOG = LogManager.getLogger("AngelicaTesrShaders"); + + private static final int UNKNOWN = 0; + private static final int READY = 1; + private static final int FAILED = 2; + + private static final Reference2ObjectOpenHashMap ENTRIES = new Reference2ObjectOpenHashMap<>(); + + public static long bakedBinds; + public static long instancedBinds; + public static long fallbacks; + public static long linkFailures; + + private TesrShaderPrograms() {} + + static final class Variant { + final boolean instanced; + final String label; + int state; + int program; + + Variant(boolean instanced) { + this.instanced = instanced; + this.label = instanced ? "instanced" : "baked"; + } + } + + public static final class Entry { + final TesrShader shader; + final Variant baked = new Variant(false); + final Variant instanced = new Variant(true); + private boolean programBound; + private int savedProgram; + private boolean wantsLightmap; + private boolean lightmapBound; + private int savedLightmapBinding; + + Entry(TesrShader shader) { + this.shader = shader; + } + } + + public static boolean packActive() { + return Iris.enabled && Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline drp && drp.shouldOverrideShaders(); + } + + public static Entry entry(TesrShader shader) { + Entry entry = ENTRIES.get(shader); + if (entry == null) { + entry = new Entry(shader); + ENTRIES.put(shader, entry); + } + return entry; + } + + static Entry entryFor(TesrMaterial material) { + final TesrShader shader = material.shader(); + return shader == null ? null : entry(shader); + } + + public static boolean bindBaked(Entry entry, boolean lightmap) { + entry.wantsLightmap = lightmap; + if (!use(entry, entry.baked)) return false; + bakedBinds++; + return true; + } + + static boolean bindInstanced(Entry entry) { + if (!use(entry, entry.instanced)) return false; + instancedBinds++; + return true; + } + + public static void release(Entry entry) { + if (entry.lightmapBound) { + entry.lightmapBound = false; + bindLightmapUnit(entry.savedLightmapBinding); + } + if (entry.programBound) { + entry.programBound = false; + GLStateManager.glUseProgram(entry.savedProgram); + } + } + + static boolean instancedAvailable(Entry entry) { + return entry.instanced.state == READY; + } + + static void invalidateAll() { + for (final Entry entry : ENTRIES.values()) { + reset(entry.baked); + reset(entry.instanced); + } + } + + private static void reset(Variant variant) { + if (variant.program != 0) GLStateManager.glDeleteProgram(variant.program); + variant.program = 0; + variant.state = UNKNOWN; + } + + private static boolean use(Entry entry, Variant variant) { + ensureLinked(entry); + if (variant.state != READY) { + fallbacks++; + return false; + } + if (!entry.programBound) { + entry.programBound = true; + entry.savedProgram = GLStateManager.getActiveProgram(); + } + GLStateManager.glUseProgram(variant.program); + if (entry.wantsLightmap && !entry.lightmapBound) { + entry.lightmapBound = true; + entry.savedLightmapBinding = GLStateManager.getBoundTextureForServerState(DrawState.LIGHTMAP_UNIT); + bindLightmapUnit(((EntityRendererAccessor) Minecraft.getMinecraft().entityRenderer).getLightmapTexture().getGlTextureId()); + } + final TesrShaderUniforms uniforms = entry.shader.uniforms(); + if (uniforms != null) uniforms.angelica$bound(variant.program, variant.instanced); + return true; + } + + private static void bindLightmapUnit(int texture) { + final int active = GLStateManager.getActiveTextureUnit(); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + DrawState.LIGHTMAP_UNIT); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + active); + } + + private static void ensureLinked(Entry entry) { + final boolean baked = entry.baked.state == UNKNOWN; + final boolean instanced = entry.instanced.state == UNKNOWN; + if (!baked && !instanced) return; + if (GLStateManager.isRecordingDisplayList()) return; + final TesrShader shader = entry.shader; + final String vertexBody; + final String fragmentBody; + try { + vertexBody = read(shader.vertex()); + fragmentBody = read(shader.fragment()); + } catch (IOException | RuntimeException e) { + if (baked) fail(entry, entry.baked, "could not be read", e.toString()); + if (instanced) fail(entry, entry.instanced, "could not be read", e.toString()); + return; + } + if (baked) link(entry, false, vertexBody, fragmentBody); + if (instanced) link(entry, true, vertexBody, fragmentBody); + } + + private static String read(ResourceLocation location) throws IOException { + try (InputStream stream = Minecraft.getMinecraft().getResourceManager().getResource(location).getInputStream()) { + return IOUtils.toString(stream, StandardCharsets.UTF_8); + } + } + + static void link(Entry entry, boolean instanced, String vertexBody, String fragmentBody) { + final Variant variant = instanced ? entry.instanced : entry.baked; + reset(variant); + final String name = entry.shader.name(); + int vertex = 0; + int fragment = 0; + int program = 0; + try { + vertex = compile(entry, variant, GL20.GL_VERTEX_SHADER, TesrShaderPrelude.vertex(instanced) + TesrShaderPrelude.stripVersion(name, vertexBody)); + if (vertex == 0) return; + fragment = compile(entry, variant, GL20.GL_FRAGMENT_SHADER, TesrShaderPrelude.fragment() + TesrShaderPrelude.stripVersion(name, fragmentBody)); + if (fragment == 0) return; + program = GLStateManager.glCreateProgram(); + GLStateManager.glAttachShader(program, vertex); + GLStateManager.glAttachShader(program, fragment); + GLStateManager.glLinkProgram(program); + if (GLStateManager.glGetProgrami(program, GL20.GL_LINK_STATUS) != GL11.GL_TRUE) { + fail(entry, variant, "failed to link", GLStateManager.glGetProgramInfoLog(program)); + return; + } + final int previous = GLStateManager.getActiveProgram(); + GLStateManager.glUseProgram(program); + try { + setSampler(program, TesrShaderPrelude.LAYER_SAMPLER, 0); + setSampler(program, TesrShaderPrelude.LIGHTMAP_SAMPLER, DrawState.LIGHTMAP_UNIT); + final TesrShaderUniforms uniforms = entry.shader.uniforms(); + if (uniforms != null) uniforms.angelica$linked(program, instanced); + } finally { + GLStateManager.glUseProgram(previous); + } + variant.program = program; + variant.state = READY; + LOG.info("TESR shader {} ({} variant) linked: program {}", name, variant.label, program); + program = 0; + } catch (RuntimeException e) { + fail(entry, variant, "failed to build", e.toString()); + } finally { + if (vertex != 0) GLStateManager.glDeleteShader(vertex); + if (fragment != 0) GLStateManager.glDeleteShader(fragment); + if (program != 0) GLStateManager.glDeleteProgram(program); + } + } + + private static int compile(Entry entry, Variant variant, int type, String source) { + final int shader = GLStateManager.glCreateShader(type); + GLStateManager.glShaderSource(shader, source); + GLStateManager.glCompileShader(shader); + if (GLStateManager.glGetShaderi(shader, GL20.GL_COMPILE_STATUS) == GL11.GL_TRUE) return shader; + fail(entry, variant, type == GL20.GL_VERTEX_SHADER ? "vertex stage failed to compile" : "fragment stage failed to compile", GLStateManager.glGetShaderInfoLog(shader)); + GLStateManager.glDeleteShader(shader); + return 0; + } + + private static void setSampler(int program, String name, int unit) { + final int location = GLStateManager.glGetUniformLocation(program, name); + if (location >= 0) GLStateManager.glUniform1i(location, unit); + } + + private static void fail(Entry entry, Variant variant, String what, String detail) { + variant.state = FAILED; + linkFailures++; + LOG.warn("TESR shader {} ({} variant) {}; variant disabled until the next resource reload: {}", + entry.shader.name(), variant.label, what, detail); + } +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/tesr/MixinTileEntityRendererDispatcher.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/tesr/MixinTileEntityRendererDispatcher.java index e677a733e..4ceaf212a 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/tesr/MixinTileEntityRendererDispatcher.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/angelica/tesr/MixinTileEntityRendererDispatcher.java @@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.mixins.interfaces.IBatchEligibility; import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility; import com.gtnewhorizons.angelica.rendering.tesr.TesrProviderDispatch; +import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer; import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher; import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; import net.minecraft.tileentity.TileEntity; @@ -17,23 +18,28 @@ public class MixinTileEntityRendererDispatcher { @Redirect(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer;renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V")) private void angelica$dispatchTesrProvider(TileEntitySpecialRenderer renderer, TileEntity te, double x, double y, double z, float partialTicks) { - if (AngelicaConfig.enableTESRProviderDispatch && TesrProviderDispatch.tryRender(renderer, te, x, y, z)) return; - if (!AngelicaConfig.enableEntityBatching) { - renderer.renderTileEntityAt(te, x, y, z, partialTicks); - return; - } - - final IBatchEligibility holder = (IBatchEligibility) renderer; - final byte state = holder.angelica$batchState(); - BatchEligibility.begin(state, GLStateManager.drawCalls); + TesrBatchRenderer.INSTANCE.beginScope(); try { - renderer.renderTileEntityAt(te, x, y, z, partialTicks); - } finally { - final byte next = BatchEligibility.end(state, GLStateManager.drawCalls); - if (next != state) { - holder.angelica$setBatchState(next); - BatchEligibility.onStateChange(renderer, next); + if (AngelicaConfig.enableTESRProviderDispatch && TesrProviderDispatch.tryRender(renderer, te, x, y, z)) return; + if (!AngelicaConfig.enableEntityBatching) { + renderer.renderTileEntityAt(te, x, y, z, partialTicks); + return; } + + final IBatchEligibility holder = (IBatchEligibility) renderer; + final byte state = holder.angelica$batchState(); + BatchEligibility.begin(state, GLStateManager.drawCalls); + try { + renderer.renderTileEntityAt(te, x, y, z, partialTicks); + } finally { + final byte next = BatchEligibility.end(state, GLStateManager.drawCalls); + if (next != state) { + holder.angelica$setBatchState(next); + BatchEligibility.onStateChange(renderer, next); + } + } + } finally { + TesrBatchRenderer.INSTANCE.endScope(); } } } diff --git a/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterialInternTest.java b/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterialInternTest.java index 62fa100af..8202f9a10 100644 --- a/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterialInternTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrMaterialInternTest.java @@ -1,29 +1,32 @@ package com.gtnewhorizons.angelica.api.tesr; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; class TesrMaterialInternTest { - @Test - void equalSettingsBuildSameInstance() { - final TesrMaterial a = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).translucent().stream().build(); - final TesrMaterial b = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).translucent().stream().build(); - assertSame(a, b); + private static final TesrShader SPECIAL_EXCLUDED = TesrShaders.register("test:invalid_special", () -> {}, () -> {}); + + static Stream invalidCombinations() { + return Stream.of( + Arguments.of("depthOnly + noDepthWrite", (Executable) () -> TesrMaterial.builder().depthOnly().noDepthWrite().build()), + Arguments.of("depthOnly + depthEqual", (Executable) () -> TesrMaterial.builder().depthOnly().depthEqual().build()), + Arguments.of("special + shader", (Executable) () -> TesrMaterial.builder().special(TesrMaterial.SpecialRender.GLINT).shader(SPECIAL_EXCLUDED).build())); } - @Test - void differingSettingsBuildDistinctInstances() { - final TesrMaterial a = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).translucent().build(); - final TesrMaterial b = TesrMaterial.builder().color(1f, 0.5f, 0.25f, 1f).additive().build(); - assertNotSame(a, b); - } - - @Test - void defaultsFoldToCurrentState() { - assertSame(TesrMaterial.CURRENT_STATE, TesrMaterial.builder().build()); + @ParameterizedTest(name = "{0}") + @MethodSource("invalidCombinations") + void invalidCombinationsThrow(String name, Executable build) { + assertThrows(IllegalStateException.class, build); } @Test diff --git a/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrShadersTest.java b/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrShadersTest.java index 878a15a40..451a33efb 100644 --- a/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrShadersTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/api/tesr/TesrShadersTest.java @@ -1,25 +1,14 @@ package com.gtnewhorizons.angelica.api.tesr; +import net.minecraft.util.ResourceLocation; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; class TesrShadersTest { private static final Runnable NOOP = () -> {}; - @Test - void registerReturnsHandleWithAccessors() { - final Runnable bind = () -> {}; - final Runnable release = () -> {}; - final TesrShader shader = TesrShaders.register("test:accessors", bind, release); - assertEquals("test:accessors", shader.name()); - assertSame(bind, shader.bind()); - assertSame(release, shader.release()); - } - @Test void duplicateNameThrows() { TesrShaders.register("test:duplicate", NOOP, NOOP); @@ -35,8 +24,11 @@ class TesrShadersTest { } @Test - void nullRunnablesThrow() { + void nullArgumentsThrow() { + final ResourceLocation location = new ResourceLocation("test", "shaders/a.vert"); assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_bind", null, NOOP)); assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_release", NOOP, null)); + assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_vertex", null, location)); + assertThrows(NullPointerException.class, () -> TesrShaders.register("test:null_fragment", location, null)); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/debug/flyby/FlybySceneTest.java b/src/test/java/com/gtnewhorizons/angelica/debug/flyby/FlybySceneTest.java index f021f6f0c..a6add7c11 100644 --- a/src/test/java/com/gtnewhorizons/angelica/debug/flyby/FlybySceneTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/debug/flyby/FlybySceneTest.java @@ -37,7 +37,7 @@ class FlybySceneTest { Arguments.of("trims surrounding whitespace", " time set day \n", new String[] { "time set day" }), Arguments.of("handles CRLF line endings", "time set day\r\nweather clear\r\n", new String[] { "time set day", "weather clear" }), Arguments.of("drops a bare slash line", "time set day\n/\nweather clear\n", new String[] { "time set day", "weather clear" }), - Arguments.of("preserves order", "one\ntwo\nthree\n", new String[] { "one", "two", "three" })); + Arguments.of("substitutes defines and drops them", "define BUF {buffer:[\"a$b\"]}\nsetblock ~ ~ ~ stone 0 replace ${BUF}\nsay ${MISSING}\n", new String[] { "setblock ~ ~ ~ stone 0 replace {buffer:[\"a$b\"]}", "say ${MISSING}" })); } @ParameterizedTest(name = "{0}") @@ -53,12 +53,6 @@ class FlybySceneTest { assertSame(plain, FlybyScene.expand(plain, 100, 64, -50)); } - @Test - void missingFileReturnsEmptyArray() { - final String path = tempDir.resolve("does-not-exist.txt").toString(); - assertEquals(0, FlybyScene.load(path).length); - } - @Test void shippedScenesTagEverythingTheyPlace() { for (String path : new String[] { "scripts/flyby-scenes/entities.txt", "scripts/flyby-scenes/entities-x8.txt" }) { diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCacheTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCacheTest.java index 8386a90f5..6779133b1 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCacheTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/AngelicaTesrMeshCacheTest.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.rendering.tesr; +import com.gtnewhorizons.angelica.api.tesr.TesrMeshProvider; import com.gtnewhorizons.angelica.api.tesr.TesrMeshSink; import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat; @@ -7,10 +8,14 @@ import com.gtnewhorizons.angelica.api.tesr.TesrMaterial; import com.gtnewhorizons.angelica.rendering.tesr.AngelicaTesrMeshCache.MeshBackend; import com.gtnewhorizons.angelica.rendering.tesr.TemplateBuffer; import net.minecraft.client.renderer.Tessellator; +import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ResourceLocation; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.LongSupplier; @@ -31,17 +36,6 @@ class AngelicaTesrMeshCacheTest { private final AngelicaTesrMeshCache cache = new AngelicaTesrMeshCache(backend, clock, replay); private final Object key = new Object(); - @Test - void firstFrameCapturesAndReplays() { - cache.renderCached(key, sink -> sink.angelica$bucket(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, TEX_A)); - - assertEquals(1, backend.beginCaptures); - assertTrue(cache.hasEntry(key)); - assertEquals(1, cache.version(key)); - assertEquals(1, replay.calls.size()); - assertSame(TEX_A, replay.calls.get(0).texture); - } - @Test void cleanFrameReplaysWithoutRecapture() { cache.renderCached(key, this::oneBucket); @@ -53,24 +47,69 @@ class AngelicaTesrMeshCacheTest { } @Test - void invalidateForcesRecapture() { - cache.renderCached(key, this::oneBucket); - cache.invalidate(key); - assertFalse(cache.hasEntry(key)); + void replayIsWrappedInOneScope() { + cache.renderCached(key, sink -> { + sink.angelica$bucket(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, TEX_A); + sink.angelica$bucket(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, TEX_B); + }); - cache.renderCached(key, this::oneBucket); - assertEquals(2, backend.beginCaptures); + assertEquals(List.of("begin", "queue", "queue", "end"), replay.events); + } + + enum RetireTrigger { INVALIDATE, DIRTY_RECAPTURE, EVICT, CLEAR } + + @ParameterizedTest + @EnumSource(RetireTrigger.class) + void droppedTemplatesAreRetiredOnce(RetireTrigger trigger) { + final TesrMeshProvider provider = new TesrMeshProvider() { + @Override + public Object angelica$meshKey(TileEntity te) { + return key; + } + + @Override + public void angelica$build(TesrMeshSink sink, TileEntity te) { + oneBucket(sink); + } + }; + cache.renderCached(key, false, provider, null); + final TemplateBuffer captured = replay.calls.get(0).template; + assertTrue(replay.retired.isEmpty()); + + switch (trigger) { + case INVALIDATE -> cache.invalidate(key); + case DIRTY_RECAPTURE -> cache.renderCached(key, true, provider, null); + case EVICT -> { + now[0] = 1_000L + AngelicaTesrMeshCache.LRU_TIMEOUT_MS + 1; + cache.tick(); + } + case CLEAR -> cache.clear(); + } + assertEquals(List.of(captured), replay.retired); + + if (trigger != RetireTrigger.DIRTY_RECAPTURE) { + assertFalse(cache.hasEntry(key)); + cache.renderCached(key, false, provider, null); + assertEquals(2, backend.beginCaptures); + } + + cache.clear(); + assertEquals(1, Collections.frequency(replay.retired, captured), "a dropped template must not be retired again"); } @Test void lruEvicts() { + final long timeout = AngelicaTesrMeshCache.LRU_TIMEOUT_MS; cache.renderCached(key, this::oneBucket); - now[0] = 1_000L + AngelicaTesrMeshCache.LRU_TIMEOUT_MS; // boundary -> retained - cache.tick(); - assertTrue(cache.hasEntry(key)); + now[0] = 1_000L + timeout; + cache.renderCached(key, this::oneBucket); - now[0] = 1_000L + AngelicaTesrMeshCache.LRU_TIMEOUT_MS + 1; // past timeout -> evicted + now[0] = 1_000L + timeout + 1; + cache.tick(); + assertTrue(cache.hasEntry(key), "a hit refreshes the entry"); + + now[0] = 1_000L + 2 * timeout + 1; cache.tick(); assertFalse(cache.hasEntry(key)); } @@ -113,10 +152,28 @@ class AngelicaTesrMeshCacheTest { private static final class RecordingReplay implements AngelicaTesrMeshCache.ReplaySink { final List calls = new ArrayList<>(); + final List events = new ArrayList<>(); + final List retired = new ArrayList<>(); + + @Override + public void beginScope() { + events.add("begin"); + } + + @Override + public void endScope() { + events.add("end"); + } @Override public void queue(TemplateBuffer template, ResourceLocation texture, TesrMaterial material) { calls.add(new Recorded(template, texture, material)); + events.add("queue"); + } + + @Override + public void retire(TemplateBuffer template) { + retired.add(template); } } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchEligibilityTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchEligibilityTest.java index ef7b167e5..673a5022a 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchEligibilityTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchEligibilityTest.java @@ -4,6 +4,12 @@ import com.gtnewhorizons.angelica.glsm.testutil.Reflect; import com.gtnewhorizons.angelica.rendering.items.DroppedItemInstancer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.Consumer; +import java.util.stream.Stream; import static com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility.DENIED; import static com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility.SAFE; @@ -46,35 +52,18 @@ class BatchEligibilityTest { BatchEligibility.onPartFallback(before, drawCalls); } - @Test - void aRendererDrawingOnlyModelPartsIsPromoted() { - assertFalse(BatchEligibility.begin(UNKNOWN, drawCalls), "unproven renderers run unbatched"); - unbatchedPart(1); - unbatchedPart(1); - assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls)); + static Stream demotions() { + return Stream.of( + Arguments.of("a raw draw", (Consumer) t -> t.draw(1)), + Arguments.of("a fallback part", (Consumer) t -> t.unbatchedPart(1))); } - @Test - void aRendererMixingItsOwnDrawsIsDenied() { - BatchEligibility.begin(UNKNOWN, drawCalls); - unbatchedPart(1); - draw(1); - assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls)); - } - - @Test - void aPromotedRendererIsDemotedWhenItStartsMixing() { + @ParameterizedTest(name = "{0}") + @MethodSource("demotions") + void aPromotedRendererIsDemotedWhenItStartsMixing(String name, Consumer mixing) { assertTrue(BatchEligibility.begin(SAFE, drawCalls), "proven renderers batch"); BatchEligibility.onPartQueued(); - draw(1); - assertEquals(DENIED, BatchEligibility.end(SAFE, drawCalls)); - } - - @Test - void aBatchedPartFallingBackCountsAsMixing() { - BatchEligibility.begin(SAFE, drawCalls); - BatchEligibility.onPartQueued(); - unbatchedPart(1); + mixing.accept(this); assertEquals(DENIED, BatchEligibility.end(SAFE, drawCalls)); } @@ -111,7 +100,7 @@ class BatchEligibilityTest { draw(1); assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls)); - BatchEligibility.begin(UNKNOWN, drawCalls); + assertFalse(BatchEligibility.begin(UNKNOWN, drawCalls), "unproven renderers run unbatched"); unbatchedPart(1); assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls), "the previous dispatch's foreign draws must not leak"); } @@ -213,10 +202,8 @@ class BatchEligibilityTest { BatchEligibility.beginExpectedDraws(drawCalls); unbatchedPart(2); BatchEligibility.endExpectedDraws(drawCalls); - draw(3); - assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls)); - final long foreign = Reflect.getStatic(BatchEligibility.class, "foreignDraws"); - assertEquals(3, foreign, "only the 3 unbracketed draws must count as foreign"); + draw(1); + assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls), "only the unbracketed draw is foreign"); } @Test @@ -230,10 +217,7 @@ class BatchEligibilityTest { BatchEligibility.endExpectedDraws(drawCalls); draw(1); BatchEligibility.endExpectedDraws(drawCalls); - draw(2); - assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls)); - final long foreign = Reflect.getStatic(BatchEligibility.class, "foreignDraws"); - assertEquals(2, foreign, "the inner bracket must not reset the outer span"); + assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls), "the inner bracket must not reset the outer span"); } @Test diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchStateGuardGLTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchStateGuardGLTest.java index 713b04002..bf31092e3 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchStateGuardGLTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/BatchStateGuardGLTest.java @@ -8,12 +8,16 @@ import com.gtnewhorizons.angelica.iris.IrisDisplayListState; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL30; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; @@ -46,55 +50,141 @@ class BatchStateGuardGLTest { BatchStateGuard.flush = flushes::incrementAndGet; } - @Test - void projectionMutationFlushesBeforeOverwritingTheMatrixAndDoesNotReenter() { + private static final Runnable NOTHING = () -> { }; + + private void assertFlushes(Runnable setup, Runnable atFlush, Runnable action, int expected, Runnable cleanup) { + final int drawBuffer = GLStateManager.ctx().drawBuffer.getValue(); + setup.run(); BatchStateGuard.flush = () -> { + atFlush.run(); flushes.incrementAndGet(); - assertEquals(1f, GLStateManager.getProjectionMatrix().m00()); - // A flushing renderer is allowed to set up its own draw state. - GLStateManager.glScissor(0, 0, 8, 8); }; - GLStateManager.glMatrixMode(GL11.GL_PROJECTION); - GLStateManager.glScalef(2, 3, 1); - assertEquals(1, flushes.get()); - assertEquals(2f, GLStateManager.getProjectionMatrix().m00()); + try { + action.run(); + assertEquals(expected, flushes.get()); + } finally { + BatchStateGuard.flush = null; + cleanup.run(); + GLStateManager.glDrawBuffer(drawBuffer); + } } - @Test - void commonCapturedStateAndManagedPassSetupDoNotDrainBatches() { + static Stream flushCases() { + return Stream.of( + Arguments.of("fog", (Runnable) () -> GLStateManager.glFogf(GL11.GL_FOG_START, 17), + (Runnable) () -> assertEquals(17f, GLStateManager.getFogState().getStart()), + (Runnable) () -> { + GLStateManager.glFogf(GL11.GL_FOG_START, 29); + assertEquals(29f, GLStateManager.getFogState().getStart()); + }, 1, NOTHING), + Arguments.of("stencil", (Runnable) () -> GLStateManager.glDisable(GL11.GL_STENCIL_TEST), + (Runnable) () -> assertFalse(GLStateManager.glIsEnabled(GL11.GL_STENCIL_TEST)), + (Runnable) () -> GLStateManager.glEnable(GL11.GL_STENCIL_TEST), 1, NOTHING), + Arguments.of("polygon mode", NOTHING, + (Runnable) () -> assertEquals(GL11.GL_FILL, GLStateManager.getPolygonState().getFrontMode()), + (Runnable) () -> { + GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE); + GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE); + }, 1, NOTHING), + Arguments.of("depth range", NOTHING, + (Runnable) () -> assertEquals(1.0, GLStateManager.getViewportState().depthRangeFar), + (Runnable) () -> { + GLStateManager.glDepthRange(0.0, 0.5); + GLStateManager.glDepthRange(0.0, 0.5); + }, 1, NOTHING), + Arguments.of("logic op and color sum", NOTHING, NOTHING, + (Runnable) () -> { + GLStateManager.glEnable(GL11.GL_COLOR_LOGIC_OP); + GLStateManager.glLogicOp(GL11.GL_XOR); + GLStateManager.glEnable(GL14.GL_COLOR_SUM); + }, 3, NOTHING), + Arguments.of("sample and discard state", NOTHING, NOTHING, + (Runnable) () -> { + GLStateManager.glEnable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE); + GLStateManager.glDisable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE); + GLStateManager.glEnable(GL30.GL_RASTERIZER_DISCARD); + GLStateManager.glDisable(GL30.GL_RASTERIZER_DISCARD); + GLStateManager.glSampleCoverage(0.5f, false); + }, 5, (Runnable) () -> GLStateManager.glSampleCoverage(1.0f, false)), + Arguments.of("draw buffer", NOTHING, NOTHING, + (Runnable) () -> { + GLStateManager.glDrawBuffer(GL11.GL_NONE); + GLStateManager.glDrawBuffer(GL11.GL_NONE); + }, 1, NOTHING), + Arguments.of("draw buffers", NOTHING, NOTHING, (Runnable) () -> GLStateManager.glDrawBuffers(GL11.GL_NONE), 1, NOTHING), + Arguments.of("projection matrix", (Runnable) () -> GLStateManager.glMatrixMode(GL11.GL_PROJECTION), + (Runnable) () -> { + assertEquals(1f, GLStateManager.getProjectionMatrix().m00()); + GLStateManager.glScissor(0, 0, 8, 8); + }, + (Runnable) () -> { + GLStateManager.glScalef(2, 3, 1); + assertEquals(2f, GLStateManager.getProjectionMatrix().m00()); + }, 1, NOTHING), + Arguments.of("texture matrix unit 2", (Runnable) () -> { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); + GLStateManager.glMatrixMode(GL11.GL_TEXTURE); + GLStateManager.glPushMatrix(); + GLStateManager.glLoadIdentity(); + }, + (Runnable) () -> assertEquals(0f, GLStateManager.getTextures().getTextureUnitMatrix(2).m30()), + (Runnable) () -> GLStateManager.glTranslatef(0.5f, 0, 0), 1, + (Runnable) () -> { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); + GLStateManager.glPopMatrix(); + }), + Arguments.of("texture matrix unit 0 stays instanced", (Runnable) () -> { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + GLStateManager.glMatrixMode(GL11.GL_TEXTURE); + }, NOTHING, + (Runnable) () -> { + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(0.5f, 0, 0); + GLStateManager.glPopMatrix(); + }, 0, NOTHING)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("flushCases") + void changesFlushBeforeMutatingAndOnlyWhenChanged(String name, Runnable setup, Runnable atFlush, Runnable action, int expected, Runnable cleanup) { + assertFlushes(setup, atFlush, action, expected, cleanup); + } + + static Stream undrainedCases() { + return Stream.of( + Arguments.of("matrices color and blend", (Runnable) () -> { + GLStateManager.glMatrixMode(GL11.GL_MODELVIEW); + GLStateManager.glPushMatrix(); + GLStateManager.glTranslatef(1, 2, 3); + GLStateManager.glPopMatrix(); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + GLStateManager.glColor4f(0.5f, 1, 1, 1); + GLStateManager.enableBlend(); + GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + }), + Arguments.of("managed pass setup", (Runnable) () -> IrisDisplayListState.runProgramTransition(() -> { + GLStateManager.glFogf(GL11.GL_FOG_START, 123); + GLStateManager.glViewport(0, 0, 8, 8); + })), + Arguments.of("lighting", (Runnable) () -> { + GLStateManager.glEnable(GL11.GL_LIGHT0); + GLStateManager.glDisable(GL11.GL_LIGHT0); + GLStateManager.glEnable(GL11.GL_COLOR_MATERIAL); + GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL); + GLStateManager.glEnable(GL11.GL_NORMALIZE); + GLStateManager.glLightf(GL11.GL_LIGHT0, GL11.GL_CONSTANT_ATTENUATION, 2.0f); + })); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("undrainedCases") + void commonCapturedStateAndManagedPassSetupDoNotDrainBatches(String name, Runnable action) { countFlushes(); - GLStateManager.glMatrixMode(GL11.GL_MODELVIEW); - GLStateManager.glPushMatrix(); - GLStateManager.glTranslatef(1, 2, 3); - GLStateManager.glPopMatrix(); - GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); - GLStateManager.glColor4f(0.5f, 1, 1, 1); - GLStateManager.enableBlend(); - GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - IrisDisplayListState.runProgramTransition(() -> { - GLStateManager.glFogf(GL11.GL_FOG_START, 123); - GLStateManager.glViewport(0, 0, 8, 8); - }); + action.run(); assertEquals(0, flushes.get()); assertFalse(BatchStateGuard.isSuspended()); } - @Test - void fogAndStencilFlushBeforeChangingTheCachedState() { - GLStateManager.glFogf(GL11.GL_FOG_START, 17); - BatchStateGuard.flush = () -> assertEquals(17f, GLStateManager.getFogState().getStart()); - GLStateManager.glFogf(GL11.GL_FOG_START, 29); - assertEquals(29f, GLStateManager.getFogState().getStart()); - BatchStateGuard.flush = null; - GLStateManager.glDisable(GL11.GL_STENCIL_TEST); - BatchStateGuard.flush = () -> { - assertFalse(GLStateManager.glIsEnabled(GL11.GL_STENCIL_TEST)); - flushes.incrementAndGet(); - }; - GLStateManager.glEnable(GL11.GL_STENCIL_TEST); - assertEquals(1, flushes.get()); - } - @Test void extraTextureBindingsAndAttributeRestoresCannotOvertakeQueuedDraws() { GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); @@ -174,58 +264,6 @@ class BatchStateGuardGLTest { } } - @Test - void rasterStateNotStoredWithLayersFlushesBeforeChanging() { - BatchStateGuard.flush = () -> { - assertEquals(GL11.GL_FILL, GLStateManager.getPolygonState().getFrontMode()); - flushes.incrementAndGet(); - }; - GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE); - GLStateManager.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE); - assertEquals(1, flushes.get()); - - flushes.set(0); - BatchStateGuard.flush = () -> { - assertEquals(1.0, GLStateManager.getViewportState().depthRangeFar); - flushes.incrementAndGet(); - }; - GLStateManager.glDepthRange(0.0, 0.5); - GLStateManager.glDepthRange(0.0, 0.5); - assertEquals(1, flushes.get()); - - flushes.set(0); - countFlushes(); - GLStateManager.glEnable(GL11.GL_COLOR_LOGIC_OP); - GLStateManager.glLogicOp(GL11.GL_XOR); - GLStateManager.glEnable(GL14.GL_COLOR_SUM); - assertEquals(3, flushes.get()); - } - - @Test - void sampleDrawBufferAndDiscardStateFlushesBeforeChanging() { - countFlushes(); - final int drawBuffer = GLStateManager.ctx().drawBuffer.getValue(); - try { - GLStateManager.glEnable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE); - GLStateManager.glDisable(GL13.GL_SAMPLE_ALPHA_TO_COVERAGE); - GLStateManager.glEnable(GL30.GL_RASTERIZER_DISCARD); - GLStateManager.glDisable(GL30.GL_RASTERIZER_DISCARD); - GLStateManager.glSampleCoverage(0.5f, false); - assertEquals(5, flushes.get()); - - flushes.set(0); - GLStateManager.glDrawBuffer(GL11.GL_NONE); - GLStateManager.glDrawBuffer(GL11.GL_NONE); - assertEquals(1, flushes.get(), "unchanged draw buffer does not flush"); - GLStateManager.glDrawBuffers(GL11.GL_NONE); - assertEquals(2, flushes.get()); - } finally { - BatchStateGuard.flush = null; - GLStateManager.glSampleCoverage(1.0f, false); - GLStateManager.glDrawBuffer(drawBuffer); - } - } - @Test void secondaryColorFlushesOnlyWhileColorSumIsOn() { GLStateManager.glSecondaryColor3f(0f, 0f, 0f); @@ -243,42 +281,4 @@ class BatchStateGuardGLTest { GLStateManager.glSecondaryColor3f(0f, 0f, 0f); } } - - @Test - void lightingChangesDoNotDrainBatches() { - countFlushes(); - GLStateManager.glEnable(GL11.GL_LIGHT0); - GLStateManager.glDisable(GL11.GL_LIGHT0); - GLStateManager.glEnable(GL11.GL_COLOR_MATERIAL); - GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL); - GLStateManager.glEnable(GL11.GL_NORMALIZE); - GLStateManager.glLightf(GL11.GL_LIGHT0, GL11.GL_CONSTANT_ATTENUATION, 2.0f); - assertEquals(0, flushes.get(), "batches draw with the lighting captured when their pass began"); - } - - @Test - void textureMatrixChangesDrainOtherUnitsButKeepUnitZeroInstanced() { - GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); - GLStateManager.glMatrixMode(GL11.GL_TEXTURE); - GLStateManager.glPushMatrix(); - GLStateManager.glLoadIdentity(); - try { - BatchStateGuard.flush = () -> { - assertEquals(0f, GLStateManager.getTextures().getTextureUnitMatrix(2).m30()); - flushes.incrementAndGet(); - }; - GLStateManager.glTranslatef(0.5f, 0, 0); - assertEquals(1, flushes.get()); - countFlushes(); - GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); - GLStateManager.glPushMatrix(); - GLStateManager.glTranslatef(0.5f, 0, 0); - GLStateManager.glPopMatrix(); - assertEquals(1, flushes.get(), "the base texture matrix is already stored in batch runs"); - } finally { - BatchStateGuard.flush = null; - GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); - GLStateManager.glPopMatrix(); - } - } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/DrawStateTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/DrawStateTest.java index 761473b00..06bfec802 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/DrawStateTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/DrawStateTest.java @@ -9,8 +9,6 @@ import org.lwjgl.opengl.GL11; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.params.provider.Arguments.arguments; class DrawStateTest { @@ -20,9 +18,7 @@ class DrawStateTest { arguments(true, GL11.GL_BACK, DrawState.CULL_BACK), arguments(true, GL11.GL_FRONT, DrawState.CULL_FRONT), arguments(true, GL11.GL_FRONT_AND_BACK, DrawState.CULL_BOTH), - arguments(false, GL11.GL_BACK, DrawState.DISABLED), - arguments(false, GL11.GL_FRONT, DrawState.DISABLED), - arguments(false, GL11.GL_FRONT_AND_BACK, DrawState.DISABLED) + arguments(false, GL11.GL_BACK, DrawState.DISABLED) ); } @@ -32,42 +28,6 @@ class DrawStateTest { assertEquals(expected, DrawState.packCull(enabled, cullFaceMode)); } - @Test - void equalValuesInternToOneInstance() { - assertSame(DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f), DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f)); - assertNotSame(DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f), DrawState.of(DrawState.CULL_FRONT, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f)); - assertNotSame(DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f), DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, false, 0f, 0f)); - } - - @Test - void lifecycleResetBoundsInterningAndReinternsIdentity() { - DrawState.clearInterning(); - final DrawState before = DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f); - DrawState.of(DrawState.CULL_FRONT, DrawState.TRANSLUCENT, false, false, true, 0f, false, true, 1f, 1f); - assertEquals(2, DrawState.internedStateCount()); - - DrawState.clearInterning(); - DrawState.clearInterning(); - assertEquals(0, DrawState.internedStateCount()); - - final DrawState after = DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f); - assertNotSame(before, after); - assertSame(after, DrawState.of(DrawState.CULL_BACK, DrawState.OPAQUE, false, true, true, 0f, true, true, 0f, 0f)); - } - - @Test - void noCullMaterialNormalizesToDisabled() { - final TesrMaterial noCull = TesrMaterial.builder().noCull().build(); - assertSame(DrawState.forMaterial(noCull, DrawState.CULL_FRONT, true, true, 0f, 0f), DrawState.forMaterial(noCull, DrawState.CULL_BACK, true, true, 0f, 0f)); - assertEquals(DrawState.DISABLED, DrawState.forMaterial(noCull, DrawState.CULL_FRONT, true, true, 0f, 0f).getCull()); - } - - @Test - void unlitMaterialNormalizesToUnlit() { - final TesrMaterial unlit = TesrMaterial.builder().unlit().build(); - assertSame(DrawState.forMaterial(unlit, DrawState.CULL_BACK, true, true, 0f, 0f), DrawState.forMaterial(unlit, DrawState.CULL_BACK, false, true, 0f, 0f)); - } - @Test void transparencyMapsToBlendMode() { assertEquals(DrawState.OPAQUE, DrawState.blendFor(TesrMaterial.Transparency.OPAQUE)); diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/EntityMaterialsTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/EntityMaterialsTest.java index ffcd838b4..7c29f9f93 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/EntityMaterialsTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/EntityMaterialsTest.java @@ -1,10 +1,14 @@ package com.gtnewhorizons.angelica.rendering.tesr; -import org.junit.jupiter.api.Test; +import com.gtnewhorizons.angelica.api.tesr.TesrMaterial; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.lwjgl.opengl.GL11; +import java.util.stream.Stream; + import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -13,120 +17,91 @@ class EntityMaterialsTest { private static final int SA = GL11.GL_SRC_ALPHA; private static final int OMSA = GL11.GL_ONE_MINUS_SRC_ALPHA; - @Test - void mainModelStateMapsToCutout() { - assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); + private static Arguments row(String name, TesrMaterial expected, boolean textured, boolean blend, Boolean texAnimated, int srcRgb, int dstRgb, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) { + return Arguments.of(name, expected, textured, blend, texAnimated, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask); } - @Test - void shadowPassHalfAlphaRefMapsToCutoutHalf() { - assertSame(EntityMaterials.CUTOUT_HALF, EntityMaterials.fromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true)); + static Stream fromStateCases() { + return Stream.of( + row("main model state maps to cutout", EntityMaterials.CUTOUT, true, false, null, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + row("shadow pass half alpha ref maps to cutout half", EntityMaterials.CUTOUT_HALF, true, false, null, SA, OMSA, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), + row("no alpha no blend maps to solid", EntityMaterials.SOLID, true, false, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + row("invisibility maps to translucent", EntityMaterials.TRANSLUCENT, true, true, null, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, false), + row("invisibility with depth write maps to translucent depth write", EntityMaterials.TRANSLUCENT_DEPTH_WRITE, true, true, null, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, true), + row("eyes map to additive", EntityMaterials.ADDITIVE, true, true, null, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + row("eyes without depth write map to additive no depth write", EntityMaterials.ADDITIVE_NO_DEPTH_WRITE, true, true, null, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false), + row("additive keeps captured alpha test", EntityMaterials.ADDITIVE_CUTOUT, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + row("additive cutout without depth write", EntityMaterials.ADDITIVE_CUTOUT_NO_DEPTH_WRITE, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false), + row("additive alpha keeps captured alpha test", EntityMaterials.ADDITIVE_ALPHA_CUTOUT, true, true, null, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + row("additive alpha cutout without depth write", EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, true, true, null, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false), + row("animated additive keeps captured alpha test", EntityMaterials.ADDITIVE_CUTOUT, true, true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + row("animated additive alpha cutout without depth write", EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false), + row("additive with a nonstandard alpha ref stays live", null, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), + row("additive with a non-GREATER alpha func stays live", null, true, true, null, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true), + row("animated additive with a nonstandard alpha ref stays live", null, true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), + row("hurt overlay maps to overlay", EntityMaterials.OVERLAY, false, true, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true), + row("glint maps to glint when texture animated", EntityMaterials.GLINT, true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false), + row("an alpha-blended animated decal such as a CIT armor overlay is not glint", null, true, true, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false), + row("glint blend without texture animation stays live", null, true, true, false, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false), + row("glint blend with depth write is not vanilla glint", null, true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, true), + row("animated additive maps", EntityMaterials.ADDITIVE, true, true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + row("animated additive alpha without depth write maps", EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, true, true, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false), + row("animated standard-alpha scrolling translucent stays live", null, true, true, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + row("animated without blend stays live", null, true, false, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + row("additive alpha maps without animation", EntityMaterials.ADDITIVE_ALPHA, true, true, false, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + row("additive alpha without animation or depth write", EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, true, true, false, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false), + row("glint blend at EQUAL without animation stays live", null, true, true, null, GL11.GL_SRC_COLOR, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, false), + row("nonstandard alpha ref stays live", null, true, false, null, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true), + row("texture off without overlay signature stays live", null, false, false, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + row("textured EQUAL-depth re-render stays live", null, true, true, null, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true)); } - @Test - void noAlphaNoBlendMapsToSolid() { - assertSame(EntityMaterials.SOLID, EntityMaterials.fromState(true, false, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true)); + @ParameterizedTest(name = "{0}") + @MethodSource("fromStateCases") + void fromState(String name, TesrMaterial expected, boolean textured, boolean blend, Boolean texAnimated, int srcRgb, int dstRgb, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) { + final TesrMaterial actual = texAnimated == null + ? EntityMaterials.fromState(textured, blend, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask) + : EntityMaterials.fromState(textured, texAnimated, blend, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask); + assertSame(expected, actual, name); } - @Test - void invisibilityMapsToTranslucent() { - assertSame(EntityMaterials.TRANSLUCENT, EntityMaterials.fromState(true, true, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, false)); - assertSame(EntityMaterials.TRANSLUCENT_DEPTH_WRITE, EntityMaterials.fromState(true, true, SA, OMSA, true, GL11.GL_GREATER, 0.0039f, GL11.GL_LEQUAL, true)); + static Stream itemFromStateCases() { + return Stream.of( + Arguments.of("dropped item cutout", EntityMaterials.DROPPED_ITEM_CUTOUT, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + Arguments.of("held block cutout", EntityMaterials.HELD_BLOCK_CUTOUT, false, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + Arguments.of("dropped item translucent", EntityMaterials.DROPPED_ITEM_TRANSLUCENT, true, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + Arguments.of("held block translucent", EntityMaterials.HELD_BLOCK_TRANSLUCENT, false, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + Arguments.of("nonstandard alpha ref must stay live", null, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true), + Arguments.of("non-GREATER alpha func must stay live", null, true, false, SA, OMSA, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true), + Arguments.of("alpha test disabled must stay live", null, true, false, SA, OMSA, false, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + Arguments.of("depth mask off must stay live", null, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false), + Arguments.of("non-LEQUAL/LESS depth func must stay live", null, true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_ALWAYS, true), + Arguments.of("additive blend has no item material", null, true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); } - @Test - void eyesMapToAdditive() { - assertSame(EntityMaterials.ADDITIVE, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.ADDITIVE_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false)); + @ParameterizedTest(name = "{0}") + @MethodSource("itemFromStateCases") + void itemFromState(String name, TesrMaterial expected, boolean unfilteredAtlas, boolean blend, int srcRgb, int dstRgb, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) { + assertSame(expected, EntityMaterials.itemFromState(unfilteredAtlas, blend, srcRgb, dstRgb, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask), name); } - @Test - void additiveKeepsCapturedAlphaTest() { - assertSame(EntityMaterials.ADDITIVE_CUTOUT, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.ADDITIVE_CUTOUT_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false)); - assertSame(EntityMaterials.ADDITIVE_ALPHA_CUTOUT, EntityMaterials.fromState(true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false)); + static Stream depthOnlyFromStateCases() { + return Stream.of( + Arguments.of("color masked off with alpha test maps to depth-only cutout", EntityMaterials.DEPTH_ONLY_CUTOUT, true, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), + Arguments.of("color masked off maps to depth-only", EntityMaterials.DEPTH_ONLY, true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), + Arguments.of("nonstandard alpha ref must stay live", null, true, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), + Arguments.of("no color and no depth writes must stay live", null, true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false), + Arguments.of("non-LEQUAL/LESS depth func must stay live", null, true, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true), + Arguments.of("texture off must stay live", null, false, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true)); } - @Test - void texAnimatedAdditiveKeepsCapturedAlphaTest() { - assertSame(EntityMaterials.ADDITIVE_CUTOUT, EntityMaterials.fromState(true, true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.ADDITIVE_ALPHA_CUTOUT_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false)); - } - - @Test - void additiveWithUnrepresentableAlphaTestStaysLive() { - assertNull(EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), () -> "additive with a nonstandard alpha ref must stay live, not silently drop the test"); - assertNull(EntityMaterials.fromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true), () -> "additive with a non-GREATER alpha func must stay live"); - assertNull(EntityMaterials.fromState(true, true, true, SA, GL11.GL_ONE, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), () -> "animated additive with a nonstandard alpha ref must stay live"); - } - - @Test - void hurtOverlayMapsToOverlay() { - assertSame(EntityMaterials.OVERLAY, EntityMaterials.fromState(false, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true)); - } - - @Test - void glintStateMapsToGlintOnlyWhenTexAnimated() { - assertSame(EntityMaterials.GLINT, EntityMaterials.fromState(true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false)); - assertNull(EntityMaterials.fromState(true, true, true, GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false), () -> "an alpha-blended animated decal such as a CIT armor overlay is not glint"); - assertNull(EntityMaterials.fromState(true, false, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, false), () -> "glint blend without texture animation must stay live"); - assertNull(EntityMaterials.fromState(true, true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_EQUAL, true), () -> "glint blend with depth write is not vanilla glint"); - } - - @Test - void texAnimatedAdditiveVariantsMap() { - assertSame(EntityMaterials.ADDITIVE, EntityMaterials.fromState(true, true, true, GL11.GL_ONE, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, EntityMaterials.fromState(true, true, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false)); - assertNull(EntityMaterials.fromState(true, true, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), () -> "animated standard-alpha (scrolling translucent) must stay live"); - assertNull(EntityMaterials.fromState(true, true, false, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), () -> "animated without blend must stay live"); - } - - @Test - void additiveAlphaMapsWithoutAnimation() { - assertSame(EntityMaterials.ADDITIVE_ALPHA, EntityMaterials.fromState(true, false, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.ADDITIVE_ALPHA_NO_DEPTH_WRITE, EntityMaterials.fromState(true, false, true, SA, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false)); - } - - @Test - void itemStateCutoutVsTranslucent() { - assertSame(EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.HELD_BLOCK_CUTOUT, EntityMaterials.itemFromState(false, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.DROPPED_ITEM_TRANSLUCENT, EntityMaterials.itemFromState(true, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.HELD_BLOCK_TRANSLUCENT, EntityMaterials.itemFromState(false, true, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - } - - @Test - void itemStateRejectsWrongAlphaRefOrFunc() { - assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true), "nonstandard alpha ref must stay live"); - assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_LESS, 0.1f, GL11.GL_LEQUAL, true), "non-GREATER alpha func must stay live"); - assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, false, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), "alpha test disabled must stay live"); - assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, false), "depth mask off must stay live"); - assertNull(EntityMaterials.itemFromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.1f, GL11.GL_ALWAYS, true), "non-LEQUAL/LESS depth func must stay live"); - assertNull(EntityMaterials.itemFromState(true, true, GL11.GL_ONE, GL11.GL_ONE, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true), "additive blend has no item material"); - } - - @Test - void colorMaskedOffMapsToDepthOnly() { - assertSame(EntityMaterials.DEPTH_ONLY_CUTOUT, EntityMaterials.depthOnlyFromState(true, true, GL11.GL_GREATER, 0.1f, GL11.GL_LEQUAL, true)); - assertSame(EntityMaterials.DEPTH_ONLY, EntityMaterials.depthOnlyFromState(true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true)); - assertFalse(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isColorWrite(), "depth-only must replay with color writes off"); - assertTrue(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isDepthWrite(), "depth-only must replay with depth writes on"); - } - - @Test - void unrepresentableDepthOnlyStaysLive() { - assertNull(EntityMaterials.depthOnlyFromState(true, true, GL11.GL_GREATER, 0.5f, GL11.GL_LEQUAL, true), "nonstandard alpha ref must stay live"); - assertNull(EntityMaterials.depthOnlyFromState(true, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, false), "no color and no depth writes must stay live"); - assertNull(EntityMaterials.depthOnlyFromState(true, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true), "non-LEQUAL/LESS depth func must stay live"); - assertNull(EntityMaterials.depthOnlyFromState(false, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), "texture off must stay live"); - } - - @Test - void unrecognizedStatesFallBackToLive() { - assertNull(EntityMaterials.fromState(true, true, GL11.GL_SRC_COLOR, GL11.GL_ONE, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, false), () -> "glint blend at EQUAL must stay live"); - assertNull(EntityMaterials.fromState(true, false, SA, OMSA, true, GL11.GL_GREATER, 0.3f, GL11.GL_LEQUAL, true), () -> "nonstandard alpha ref must stay live"); - assertNull(EntityMaterials.fromState(false, false, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_LEQUAL, true), () -> "texture off without overlay signature must stay live"); - assertNull(EntityMaterials.fromState(true, true, SA, OMSA, false, GL11.GL_ALWAYS, 0f, GL11.GL_EQUAL, true), () -> "textured EQUAL-depth re-render must stay live"); + @ParameterizedTest(name = "{0}") + @MethodSource("depthOnlyFromStateCases") + void depthOnlyFromState(String name, TesrMaterial expected, boolean textured, boolean alphaTest, int alphaFunc, float alphaRef, int depthFunc, boolean depthMask) { + assertSame(expected, EntityMaterials.depthOnlyFromState(textured, alphaTest, alphaFunc, alphaRef, depthFunc, depthMask), name); + if (expected == EntityMaterials.DEPTH_ONLY) { + assertFalse(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isColorWrite(), "depth-only must replay with color writes off"); + assertTrue(DrawState.forMaterial(EntityMaterials.DEPTH_ONLY, DrawState.CULL_BACK, true, true, 0f, 0f).isDepthWrite(), "depth-only must replay with depth writes on"); + } } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RenderLayerInterningTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RenderLayerInterningTest.java index a8813271f..d380e4c5f 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RenderLayerInterningTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RenderLayerInterningTest.java @@ -5,73 +5,91 @@ import com.gtnewhorizons.angelica.compat.mojang.RenderLayer; import com.gtnewhorizons.angelica.shadercompat.ShaderGlint; import net.coderbot.iris.layer.PassOverride; import net.minecraft.util.ResourceLocation; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; class RenderLayerInterningTest { private static final ResourceLocation TEX = new ResourceLocation("angelica", "textures/render_layer_interning_test.png"); - @Test - void noCullMaterialInternsToOneLayerRegardlessOfCullCode() { + private static RenderLayer layer(TesrMaterial material, int cull, boolean lit, boolean lightmap) { + return RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, cull, lit, lightmap); + } + + private static RenderLayer glint(int slot) { + return RenderLayer.tesr(null, EntityMaterials.GLINT, PassOverride.NONE, 0f, 0f, slot, DrawState.CULL_BACK, true, true); + } + + static Stream normalizeCases() { final TesrMaterial noCull = TesrMaterial.builder().noCull().build(); - final RenderLayer back = RenderLayer.tesr(TEX, noCull, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true); - final RenderLayer front = RenderLayer.tesr(TEX, noCull, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_FRONT, true, true); - assertSame(back, front); - assertEquals(DrawState.DISABLED, back.getState().getCull()); - } - - @Test - void unlitMaterialRequestedLitYieldsUnlitState() { final TesrMaterial unlit = TesrMaterial.builder().unlit().build(); - final RenderLayer layer = RenderLayer.tesr(TEX, unlit, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true); - assertFalse(layer.getState().isLit()); + return Stream.of( + arguments("noCull", + (Supplier) () -> layer(noCull, DrawState.CULL_BACK, true, true), + (Supplier) () -> layer(noCull, DrawState.CULL_FRONT, true, true), + (BiConsumer) (a, b) -> assertEquals(DrawState.DISABLED, a.getState().getCull())), + arguments("unlit", + (Supplier) () -> layer(unlit, DrawState.CULL_BACK, true, true), + (Supplier) () -> layer(unlit, DrawState.CULL_BACK, false, true), + (BiConsumer) (a, b) -> assertFalse(a.getState().isLit())) + ); } - @Test - void litMaterialRequestedUnlitYieldsUnlitStateAndDistinctLayer() { + @ParameterizedTest(name = "{0}") + @MethodSource("normalizeCases") + void normalizesToOneLayer(String name, Supplier requestA, Supplier requestB, BiConsumer stateCheck) { + final RenderLayer a = requestA.get(); + final RenderLayer b = requestB.get(); + assertSame(a, b); + stateCheck.accept(a, b); + } + + static Stream splitCases() { final TesrMaterial lit = TesrMaterial.builder().build(); - final RenderLayer unlitRequest = RenderLayer.tesr(TEX, lit, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, false, true); - final RenderLayer litRequest = RenderLayer.tesr(TEX, lit, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true); - assertFalse(unlitRequest.getState().isLit()); - assertTrue(litRequest.getState().isLit()); - assertNotSame(unlitRequest, litRequest); + return Stream.of( + arguments("lit material requested unlit", + (Supplier) () -> layer(lit, DrawState.CULL_BACK, false, true), + (Supplier) () -> layer(lit, DrawState.CULL_BACK, true, true), + (BiConsumer) (a, b) -> { + assertFalse(a.getState().isLit()); + assertTrue(b.getState().isLit()); + }), + arguments("lightmap", + (Supplier) () -> layer(lit, DrawState.CULL_BACK, true, true), + (Supplier) () -> layer(lit, DrawState.CULL_BACK, true, false), + (BiConsumer) (a, b) -> { + assertTrue(a.getState().isLightmap()); + assertFalse(b.getState().isLightmap()); + }), + arguments("glint tint slots 0 and 1", + (Supplier) () -> glint(1), + (Supplier) () -> glint(0), + (BiConsumer) (a, b) -> { }), + arguments("glint untinted and tinted", + (Supplier) () -> glint(ShaderGlint.NO_TINT), + (Supplier) () -> glint(0), + (BiConsumer) (a, b) -> { }) + ); } - @Test - void lightmapIsPartOfLayerIdentity() { - final TesrMaterial material = TesrMaterial.builder().build(); - final RenderLayer withLightmap = RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true); - final RenderLayer withoutLightmap = RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, false); - assertTrue(withLightmap.getState().isLightmap()); - assertFalse(withoutLightmap.getState().isLightmap()); - assertNotSame(withLightmap, withoutLightmap); - } - - @Test - void noPassIsPartOfStructuredInterningIdentity() { - final TesrMaterial material = TesrMaterial.builder().build(); - final RenderLayer regular = RenderLayer.tesr(TEX, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true); - final RenderLayer noPass = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true); - - assertNotSame(regular, noPass); - } - - @Test - void cacheResetAllowsFreshCanonicalIdentity() { - final TesrMaterial material = TesrMaterial.builder().build(); - final RenderLayer before = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true); - RenderLayer.clearInterningAndHooks(); - final RenderLayer first = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true); - final RenderLayer second = RenderLayer.tesrNoPass(TEX, material, DrawState.CULL_BACK, true, true); - - assertNotSame(before, first); - assertSame(first, second); - assertTrue(RenderLayer.cacheSize() > 0); + @ParameterizedTest(name = "{0}") + @MethodSource("splitCases") + void fieldSplitsLayerIdentity(String name, Supplier requestA, Supplier requestB, BiConsumer stateCheck) { + final RenderLayer a = requestA.get(); + final RenderLayer b = requestB.get(); + assertNotSame(a, b); + assertSame(a, requestA.get(), "the same request must reuse the layer"); + stateCheck.accept(a, b); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsGLTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsGLTest.java index fc47c51ed..eafde3b96 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsGLTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsGLTest.java @@ -4,6 +4,7 @@ import com.gtnewhorizon.gtnhlib.client.renderer.MatrixHelper; import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat; import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat; import com.gtnewhorizons.angelica.api.tesr.TesrMaterial; +import com.gtnewhorizons.angelica.api.tesr.TesrShader; import com.gtnewhorizons.angelica.compat.mojang.RenderLayer; import com.gtnewhorizons.angelica.glsm.GLCoreTest; import com.gtnewhorizons.angelica.glsm.GLStateManager; @@ -17,6 +18,7 @@ import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard; import com.gtnewhorizons.angelica.glsm.states.BlendState; import com.gtnewhorizons.angelica.glsm.testutil.Reflect; import com.gtnewhorizons.angelica.rendering.GlintClock; +import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrGroups.Group; import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrGroups.InstanceColumns; import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrGroups.TexRun; import com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.CountingPipeline; @@ -26,12 +28,17 @@ import net.coderbot.iris.layer.PassOverride; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.ItemRenderer; import net.minecraft.util.ResourceLocation; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; import org.joml.Matrix4f; import org.joml.Matrix4fc; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; @@ -40,7 +47,11 @@ import org.lwjgl.opengl.GL15; import java.nio.ByteBuffer; import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Stream; +import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.SCOPE; import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.COLOR_INDEX; import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.VERTEX_SIZE; import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.X_INDEX; @@ -63,6 +74,7 @@ class RetainedTesrGroupsGLTest { private static boolean probeRegistered; private static Runnable onBufferSetup; + private final long[] clock = {1_000L}; private AngelicaBufferSource source; private InstanceRing ring; private InstancedTemplateRenderer instanced; @@ -110,7 +122,7 @@ class RetainedTesrGroupsGLTest { source = new AngelicaBufferSource(); ring = new InstanceRing(); instanced = new InstancedTemplateRenderer(ring); - groups = new RetainedTesrGroups(source); + groups = new RetainedTesrGroups(source, () -> clock[0]); layer = RetainedTesrTestSupport.testLayer(); } @@ -124,6 +136,7 @@ class RetainedTesrGroupsGLTest { groups.clear(); instanced.clear(); ring.delete(); + TesrShaderPrograms.invalidateAll(); final ShaderManager ffp = ShaderManager.getInstance(); if (ffp.isActive()) ffp.deactivate(); ShaderManager.disable(); @@ -190,11 +203,193 @@ class RetainedTesrGroupsGLTest { groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline); for (int id = 1; id <= 3; id++) { final Matrix4f mv = new Matrix4f().translation(id, 0f, 0f); - groups.queue(template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null); - groups.queue(template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null); + groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null); + groups.queue(SCOPE, template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null); } } + private static final TesrMaterial BAKED_ADDITIVE = TesrMaterial.builder().additiveAlpha().build(); + private static final TesrMaterial BAKED_TRANSLUCENT = TesrMaterial.builder().translucent().build(); + + private void bakedFrame(TesrMaterial material, TemplateBuffer template, Matrix4f base, boolean instancing, Matrix4f... models) { + final Matrix4f mv = new Matrix4f(); + groups.beginPass(base, 0, 0, 0, instancing ? instanced : null); + for (final Matrix4f model : models) { + groups.queue(SCOPE, template, layer, material, mv.set(base).mul(model), 0, -1, 0, 0L, 5, null); + } + groups.drawLayer(layer); + instanced.endFrame(); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + + @Test + void staticGroupBakesOnceWhileTheCameraRotates() { + final TemplateBuffer template = capturedQuad(0.1); + final Matrix4f[] models = new Matrix4f[256]; + for (int i = 0; i < models.length; i++) { + models[i] = new Matrix4f().translation((i % 29) * 3.3f - 48f, (i % 7) * 1.7f - 6f, (i % 31) * 3.1f - 48f); + } + for (int frame = 0; frame < 120; frame++) { + clock[0] += 1; + bakedFrame(BAKED_ADDITIVE, template, new Matrix4f().rotationX(0.1f).rotateY(0.002f * frame), true, models); + } + assertEquals(1, groups.rebakes, "rotation alone must not re-bake a static group"); + assertEquals(0, groups.streamPromotions); + } + + static Stream nonOpaqueMaterials() { + return Stream.of(Arguments.of("additive", BAKED_ADDITIVE), Arguments.of("translucent", BAKED_TRANSLUCENT)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("nonOpaqueMaterials") + void nonOpaqueGroupsThatKeepRebakingPromoteToInstancing(String name, TesrMaterial material) { + final TemplateBuffer template = capturedQuad(0.1); + for (int frame = 0; frame < 30; frame++) { + clock[0] += 1; + bakedFrame(material, template, new Matrix4f(), true, new Matrix4f().translation((frame / 3) * 0.03f, 0f, 0f)); + } + assertEquals(1, groups.streamPromotions, "a group re-baking every third frame must promote"); + assertEquals(1 + RetainedTesrGroups.PROMOTE_AFTER_REBAKES, groups.rebakes, "the first bake plus the re-bakes that triggered promotion, none after"); + assertTrue(groups.instancedInstances > 0, "a promoted group must draw instanced"); + assertEquals(0, groups.streamedInstances, "promotion with instancing available must not CPU-stream"); + } + + @Test + void slowRebakesOutsideTheWindowDoNotPromote() { + final TemplateBuffer template = capturedQuad(0.1); + for (int frame = 0; frame < 8; frame++) { + clock[0] += RetainedTesrGroups.PROMOTE_WINDOW_MS + 1; + bakedFrame(BAKED_ADDITIVE, template, new Matrix4f(), true, new Matrix4f().translation(frame * 0.01f, 0f, 0f)); + } + assertEquals(0, groups.streamPromotions); + assertEquals(8, groups.rebakes); + } + + static Stream reorderRebakes() { + return Stream.of(Arguments.of("additive keeps its mesh", BAKED_ADDITIVE, 1), Arguments.of("translucent re-bakes every reorder", BAKED_TRANSLUCENT, 8)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("reorderRebakes") + void reorderRebakesOnlyUnderAlphaBlending(String name, TesrMaterial material, int rebakes) { + final TemplateBuffer template = capturedQuad(0.1); + final Matrix4f left = new Matrix4f().translation(-0.3f, 0f, 0f); + final Matrix4f right = new Matrix4f().translation(0.3f, 0f, 0f); + for (int frame = 0; frame < 8; frame++) { + clock[0] += 1; + bakedFrame(material, template, new Matrix4f(), true, frame % 2 == 0 ? left : right, frame % 2 == 0 ? right : left); + } + assertEquals(rebakes, groups.rebakes, "draw order matters only under alpha blending"); + assertEquals(0, groups.streamPromotions, "a reorder is not an instance change"); + } + + @ParameterizedTest(name = "count changes {0}") + @ValueSource(booleans = { true, false }) + void countOrTemplateChangeDoesNotCountTowardPromotion(boolean changeCount) { + final TemplateBuffer[] templates = {capturedQuad(0.1), capturedQuad(0.2)}; + final Matrix4f left = new Matrix4f().translation(-0.3f, 0f, 0f); + final Matrix4f right = new Matrix4f().translation(0.3f, 0f, 0f); + for (int frame = 0; frame < 8; frame++) { + clock[0] += 1; + final boolean odd = frame % 2 == 1; + if (changeCount && odd) { + bakedFrame(BAKED_ADDITIVE, templates[0], new Matrix4f(), true, left); + } else { + bakedFrame(BAKED_ADDITIVE, templates[odd ? 1 : 0], new Matrix4f(), true, left, right); + } + } + assertEquals(8, groups.rebakes, "sanity: every frame re-bakes"); + assertEquals(0, groups.streamPromotions, "instances entering view or swapping meshes are not volatile transforms"); + } + + @ParameterizedTest(name = "{0} distinct templates") + @ValueSource(ints = { RetainedTesrGroups.PROMOTE_MAX_TEMPLATES, RetainedTesrGroups.PROMOTE_MAX_TEMPLATES + 1 }) + void templateBudgetGatesPromotion(int count) { + final TemplateBuffer[] templates = new TemplateBuffer[count]; + for (int i = 0; i < count; i++) templates[i] = capturedQuad(0.01); + for (int frame = 0; frame < 8; frame++) { + clock[0] += 1; + groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); + for (int i = 0; i < count; i++) { + groups.queue(SCOPE, templates[i], layer, BAKED_ADDITIVE, new Matrix4f().translation(frame * 0.01f, i * 0.05f, 0f), 0, -1, 0, 0L, 5, null); + } + groups.drawLayer(layer); + instanced.endFrame(); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + assertEquals(count <= RetainedTesrGroups.PROMOTE_MAX_TEMPLATES ? 1 : 0, groups.streamPromotions); + } + + @Test + void templateRetiredBeforeFirstDrawIsFreed() { + final TemplateBuffer template = template(TRIANGLE, COLOR_ABGR, GL11.GL_TRIANGLES); + instanced.retire(template); + drawSingleInstance(instanced, template, new Matrix4f(), COLOR_ABGR); + instanced.releaseRetired(); + assertEquals(0, instanced.meshCount(), "a retire that precedes the mesh's first build must still free it"); + } + + static Stream materialsWithoutInstancing() { + return Stream.of(Arguments.of("opaque", TesrMaterial.CURRENT_STATE, true), Arguments.of("additive", BAKED_ADDITIVE, false)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("materialsWithoutInstancing") + void withoutInstancingOnlyOpaqueGroupsPromote(String name, TesrMaterial material, boolean promotes) { + final TemplateBuffer template = RetainedTesrTestSupport.template(); + for (int frame = 0; frame < 12; frame++) { + clock[0] += 1; + bakedFrame(material, template, new Matrix4f(), false, new Matrix4f().translation(frame * 0.01f, 0f, 0f)); + } + assertEquals(promotes ? 1 : 0, groups.streamPromotions); + assertEquals(promotes, groups.streamedInstances > 0, "only a promoted group CPU-streams"); + } + + @Test + void depthEqualOverlayAndItsDepthWriterAlwaysShareAPath() { + final TemplateBuffer template = capturedQuad(0.1); + final Matrix4f fixed = new Matrix4f().translation(0.2f, 0f, 0f); + boolean sawPromoted = false, sawDemotedAgain = false; + for (int frame = 0; frame < 40; frame++) { + final boolean animating = frame < 12; + clock[0] += animating ? 1 : RetainedTesrGroups.STABILITY_PROBE_MS; + groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, fixed, 0, -1, 0, 0L, 5, null); + groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.DEPTH_EQUAL_OVERLAY, fixed, 0, animating ? 0xFF000000 | frame : -1, 0, 0L, 5, null); + final ObjectArrayList pair = RetainedTesrTestSupport.groupsOn(groups, layer); + assertEquals(2, pair.size()); + assertEquals(pair.get(0).streamPath, pair.get(1).streamPath, "base and overlay diverged at frame " + frame); + if (pair.get(0).streamPath) sawPromoted = true; + else if (sawPromoted) sawDemotedAgain = true; + groups.drawLayer(layer); + instanced.endFrame(); + } + assertTrue(sawPromoted, "the animating overlay must pull its static depth writer onto the instanced path"); + assertTrue(sawDemotedAgain, "once the overlay settles the pair must return to baked together"); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + + @Test + void coupledPairWithoutInstancingStaysBaked() { + final TemplateBuffer template = RetainedTesrTestSupport.template(); + for (int frame = 0; frame < 12; frame++) { + clock[0] += 1; + groups.beginPass(new Matrix4f(), 0, 0, 0); + final Matrix4f pose = new Matrix4f().translation(frame * 0.01f, 0f, 0f); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, pose, 0, -1, 0, 0L, 5, null); + groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.DEPTH_EQUAL_OVERLAY, pose, 0, -1, 0, 0L, 5, null); + final ObjectArrayList pair = RetainedTesrTestSupport.groupsOn(groups, layer); + assertEquals(2, pair.size(), "base and overlay must form two groups at frame " + frame); + assertEquals(pair.get(0).streamPath, pair.get(1).streamPath, "pair diverged at frame " + frame); + assertFalse(pair.get(0).streamPath, "frame " + frame); + groups.drawLayer(layer); + } + assertEquals(0, groups.streamPromotions, "a depth-coupled group must not promote on its own when its set cannot instance"); + source.discard(); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + @Test void cpuFallbackRestoresEveryIdAndTintBeforeDrawingAndRestoresTheCaller() { final var state = net.coderbot.iris.uniforms.CapturedRenderingState.INSTANCE; @@ -202,7 +397,7 @@ class RetainedTesrGroupsGLTest { state.pushCurrentBlockEntity(); state.pushCurrentEntityColor(); try { - final var draws = new java.util.ArrayList(); + final var draws = new ArrayList(); onBufferSetup = () -> draws.add(new long[] { state.getCurrentRenderedEntity(), state.getCurrentRenderedBlockEntity(), state.getCurrentRenderedItem(), AngelicaBufferSource.packEntityColor(state.getCurrentEntityColor()) @@ -212,7 +407,7 @@ class RetainedTesrGroupsGLTest { for (int i = 0; i < 3; i++) { final long info = com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs.packEntityInfo(45010 + i, 45020 + i, 45030 + i); final int color = AngelicaBufferSource.packAbgr(i / 2f, 0, 0, 1); - groups.queue(template, layer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, -1, color, info, 45020 + i, null); + groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, -1, color, info, 45020 + i, null); } state.setCurrentEntityAndItem(81, 82); state.setCurrentBlockEntity(83); @@ -239,36 +434,25 @@ class RetainedTesrGroupsGLTest { } } - @Test - void bothVariantsBindOncePerLayer() { - final CountingPipeline pipeline = new CountingPipeline(true, true); - queueThreeEntities(pipeline); - - groups.drawLayer(layer); - - assertEquals(1, pipeline.matrixBinds, "one matrix variant bind for the whole layer"); - assertEquals(1, pipeline.cubeBinds, "one cube variant bind for the whole layer"); - assertEquals(2, pipeline.rebinds, "one resolve, one release"); - assertEquals(3, groups.instancedInstances); - assertEquals(3, groups.cubeInstances); - assertEquals(2, groups.instancedDraws, "the three ids collapse into one group: one template-run draw, one cube-run draw"); - assertEquals(0, groups.streamedInstances); + static Stream variantBinds() { + return Stream.of(Arguments.of(true, 1, 1, 3, 3), Arguments.of(false, 1, 0, 6, 0)); } - @Test - void cubePartsRideTheMatrixVariantWhenTheCubeVariantIsMissing() { - final CountingPipeline pipeline = new CountingPipeline(true, false); + @ParameterizedTest(name = "cube variant {0}") + @MethodSource("variantBinds") + void bothVariantsBindOncePerLayer(boolean cubeVariant, int matrixBinds, int cubeBinds, int instancedInstances, int cubeInstances) { + final CountingPipeline pipeline = new CountingPipeline(true, cubeVariant); queueThreeEntities(pipeline); groups.drawLayer(layer); - assertEquals(1, pipeline.matrixBinds); - assertEquals(0, pipeline.cubeBinds); + assertEquals(matrixBinds, pipeline.matrixBinds, "one matrix variant bind for the whole layer"); + assertEquals(cubeBinds, pipeline.cubeBinds, "one cube variant bind for the whole layer, none without it"); assertEquals(2, pipeline.rebinds, "one resolve, one release"); - assertEquals(6, groups.instancedInstances, "cube parts draw as their own templates"); - assertEquals(0, groups.cubeInstances); + assertEquals(instancedInstances, groups.instancedInstances, "without a cube variant, cube parts draw as their own templates"); + assertEquals(cubeInstances, groups.cubeInstances); + assertEquals(2, groups.instancedDraws, "the three ids collapse into one group: one template-run draw, one cube-run draw"); assertEquals(0, groups.streamedInstances); - assertEquals(2, groups.instancedDraws, "one template-run draw, one cube-as-template-run draw"); } @Test @@ -282,8 +466,8 @@ class RetainedTesrGroupsGLTest { groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline); for (int id = 1; id <= 3; id++) { final Matrix4f mv = new Matrix4f().translation(id, 0f, 0f); - groups.queue(template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]); - groups.queue(template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null); + groups.queue(SCOPE, template, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]); + groups.queue(SCOPE, template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, RetainedTesrTestSupport.STREAM, mv, 0, -1, 0, 0L, id, null, null); } groups.drawLayer(layer); @@ -301,9 +485,9 @@ class RetainedTesrGroupsGLTest { final TemplateBuffer second = RetainedTesrTestSupport.template(); groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline); - groups.queue(first, layer, EntityMaterials.GLINT, new Matrix4f().translation(1f, 0f, 0f), 0, -1, 0, 0L, 1, null); - groups.queue(second, layer, EntityMaterials.GLINT, new Matrix4f().translation(2f, 0f, 0f), 0, -1, 0, 0L, 2, null); - groups.queue(first, layer, EntityMaterials.GLINT, new Matrix4f().translation(3f, 0f, 0f), 0, -1, 0, 0L, 3, null); + groups.queue(SCOPE, first, layer, EntityMaterials.GLINT, new Matrix4f().translation(1f, 0f, 0f), 0, -1, 0, 0L, 1, null); + groups.queue(SCOPE, second, layer, EntityMaterials.GLINT, new Matrix4f().translation(2f, 0f, 0f), 0, -1, 0, 0L, 2, null); + groups.queue(SCOPE, first, layer, EntityMaterials.GLINT, new Matrix4f().translation(3f, 0f, 0f), 0, -1, 0, 0L, 3, null); groups.drawLayer(layer); @@ -322,16 +506,15 @@ class RetainedTesrGroupsGLTest { groups.beginPass(new Matrix4f(), 0, 0, 0, instanced, pipeline); for (int id = 1; id <= 3; id++) { final Matrix4f mv = new Matrix4f().translation(id, 0f, 0f); - groups.queue(template, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]); - groups.queue(template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1], null); + groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1]); + groups.queue(SCOPE, template, RetainedTesrTestSupport.cubes(1), 0.0625f, layer, EntityMaterials.GLINT, mv, 0, -1, 0, 0L, id, texMatrices[id - 1], null); } - final RetainedTesrGroups.Group group = RetainedTesrTestSupport.onlyGroup(groups); + final Group group = RetainedTesrTestSupport.onlyGroup(groups); assertEquals(2, group.templateColumns.runs.size(), "sanity: template runs A,B,A merge into two heads"); assertNotNull(group.templateColumns.runs.get(0).next, "sanity: the template A head must carry a second segment"); assertEquals(2, group.cubeColumns.runs.size(), "sanity: cube runs A,B,A merge into two heads"); assertNotNull(group.cubeColumns.runs.get(0).next, "sanity: the cube A head must carry a second segment"); - groups.drawLayer(layer); assertEquals(0, pipeline.matrixBinds); @@ -360,7 +543,7 @@ class RetainedTesrGroupsGLTest { assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced draw must not raise a GL error"); final int[] atTranslation = FfpFixture.readPixel(tx, ty); - assertTrue(atTranslation[0] > 200 && atTranslation[1] < 50 && atTranslation[2] < 50, "translated instance must render red at (" + tx + "," + ty + "), got " + atTranslation[0] + "," + atTranslation[1] + "," + atTranslation[2]); + assertRed(atTranslation, "translated instance must render red at (" + tx + "," + ty + ")"); final int[] atOrigin = FfpFixture.readPixel(originX, originY); assertTrue(atOrigin[0] < 50 && atOrigin[1] < 50 && atOrigin[2] < 50, "the untranslated origin must stay background, got " + atOrigin[0] + "," + atOrigin[1] + "," + atOrigin[2]); @@ -370,7 +553,7 @@ class RetainedTesrGroupsGLTest { assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "instanced draw must not raise a GL error"); final int[] identityAtOrigin = FfpFixture.readPixel(originX, originY); - assertTrue(identityAtOrigin[0] > 200 && identityAtOrigin[1] < 50 && identityAtOrigin[2] < 50, "an identity instance must render red at the origin, got " + identityAtOrigin[0] + "," + identityAtOrigin[1] + "," + identityAtOrigin[2]); + assertRed(identityAtOrigin, "an identity instance must render red at the origin"); } private void queueTriangleOnClearedTarget() { @@ -379,9 +562,13 @@ class RetainedTesrGroupsGLTest { GLStateManager.disableTexture(); GLStateManager.glDisable(GL11.GL_SCISSOR_TEST); FfpFixture.clear(); - groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); - groups.queue(template(TRIANGLE, COLOR_ABGR, GL11.GL_TRIANGLES), layer, RetainedTesrTestSupport.STREAM, - new Matrix4f(), 0, COLOR_ABGR, 0, 0L, 0, null); + queueTriangle(groups, new Matrix4f()); + } + + private void queueTriangle(RetainedTesrGroups target, Matrix4f mv) { + target.beginPass(new Matrix4f(), 0, 0, 0, instanced); + target.queue(SCOPE, template(TRIANGLE, COLOR_ABGR, GL11.GL_TRIANGLES), layer, RetainedTesrTestSupport.STREAM, + mv, 0, COLOR_ABGR, 0, 0L, 0, null); } /** Runs body with the named fields of a batcher singleton swapped and the state fallback installed. */ @@ -416,12 +603,23 @@ class RetainedTesrGroupsGLTest { void heldEntityTranslucentsSurviveUncapturedStateChange() { GLStateManager.glScissor(0, 0, 0, 0); queueTriangleOnClearedTarget(); - final ModelPartBatcher models = ModelPartBatcher.INSTANCE; - withBatcherFields(models, () -> { - GLStateManager.glEnable(GL11.GL_SCISSOR_TEST); - assertTrue(models.hasHeldEntities(), "held entity translucents wait for the post-deferred flush"); - assertTrue(FfpFixture.readPixel(400, 300)[0] < 50, "held entity geometry must not draw before deferred"); - }, "entitySource", source, "entityGroups", groups, "entityTranslucentsHeld", true); + final AngelicaBufferSource blockEntitySource = new AngelicaBufferSource(); + final RetainedTesrGroups blockEntityGroups = new RetainedTesrGroups(blockEntitySource, () -> clock[0]); + try { + queueTriangle(blockEntityGroups, new Matrix4f().translation(0.5f, 0f, 0f)); + final ModelPartBatcher models = ModelPartBatcher.INSTANCE; + withBatcherFields(models, () -> { + assertTrue(models.hasQueuedGeometry(), "sanity: the block-entity pass has queued geometry to drain"); + GLStateManager.glEnable(GL11.GL_SCISSOR_TEST); + assertTrue(FfpFixture.readPixel(600, 300)[0] > 200, "the active block-entity batch must draw before the zero-area scissor takes effect"); + assertTrue(FfpFixture.readPixel(400, 300)[0] < 50, "held entity geometry must not draw before deferred"); + assertTrue(models.hasHeldEntities(), "held entity translucents wait for the post-deferred flush"); + }, "entitySource", source, "entityGroups", groups, "entityTranslucentsHeld", true, + "activeSource", blockEntitySource, "activeGroups", blockEntityGroups, "active", true); + } finally { + blockEntitySource.discard(); + blockEntityGroups.clear(); + } } @Test @@ -637,10 +835,10 @@ class RetainedTesrGroupsGLTest { setGlintBlendState(); bindGlint(); groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); - groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixA); - groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixB); - groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixA); - groups.queue(glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixB); + groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixA); + groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE1, 0, -1, 0, 0L, 1, glintMatrixB); + groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixA); + groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, templateE2, 0, -1, 0, 0L, 2, glintMatrixB); groups.drawLayer(layer); instanced.endFrame(); assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "the merged candidate draw must not raise a GL error"); @@ -668,10 +866,10 @@ class RetainedTesrGroupsGLTest { setGlintBlendState(); bindGlint(); groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); - groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixA, null); - groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixB, null); - groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixA, null); - groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixB, null); + groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixA, null); + groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE1, 0, -1, 0, 0L, 1, glintMatrixB, null); + groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixA, null); + groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, cubeE2, 0, -1, 0, 0L, 2, glintMatrixB, null); groups.drawLayer(layer); instanced.endFrame(); assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "the merged candidate draw must not raise a GL error"); @@ -682,75 +880,53 @@ class RetainedTesrGroupsGLTest { return "0x" + Integer.toHexString(pixel); } - @Test - void templateGlintRunsMatchSequentialDrawsUnderAlphaBlend() { + @ParameterizedTest(name = "cubes {0}") + @ValueSource(booleans = { false, true }) + void glintRunsMatchSequentialDrawsUnderAlphaBlend(boolean cubes) { glintState(); - final int[] base = drawBaseTemplates(); - final int[] reference = templateReference(false); + final int[] base = cubes ? drawBaseCubes() : drawBaseTemplates(); + final int[] reference = cubes ? cubeReference() : templateReference(false); FfpFixture.assertOverlayFootprint(base, reference, BACKGROUND, "glint"); - final int[] swapped = templateReference(true); - int differing = 0; - for (int i = 0; i < reference.length; i++) { - if (reference[i] != swapped[i]) differing++; + if (!cubes) { + final int[] swapped = templateReference(true); + int differing = 0; + for (int i = 0; i < reference.length; i++) { + if (reference[i] != swapped[i]) differing++; + } + assertTrue(differing > 0, "swapping e1's k0/k1 draw order under alpha blending must change the rendered " + "pixels, otherwise the parity tests could not detect a reversed emitter order"); } - assertTrue(differing > 0, "swapping e1's k0/k1 draw order under alpha blending must change the rendered " + "pixels, otherwise the parity tests could not detect a reversed emitter order"); - CubeParityFixture.assertPixelParity(reference, templateCandidate(), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe); + CubeParityFixture.assertPixelParity(reference, cubes ? cubeCandidate() : templateCandidate(), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe); } - @Test - void cubeGlintRunsMatchSequentialDrawsUnderAlphaBlend() { + @ParameterizedTest(name = "cubes {0}") + @ValueSource(booleans = { false, true }) + void replayedGlintMatchesIndependentPasses(boolean cubes) { glintState(); - final int[] base = drawBaseCubes(); - final int[] reference = cubeReference(); - FfpFixture.assertOverlayFootprint(base, reference, BACKGROUND, "glint"); - CubeParityFixture.assertPixelParity(reference, cubeCandidate(), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe); - } - - @Test - void replayedGlintTemplatesMatchIndependentPasses() { - glintState(); - final int[] reference = templateReference(false); - drawBaseTemplates(); + final int[] reference = cubes ? cubeReference() : templateReference(false); + if (cubes) drawBaseCubes(); + else drawBaseTemplates(); setGlintBlendState(); groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); final GlintCapture capture = new GlintCapture(); - for (Matrix4f pose : new Matrix4f[] {templateE1, templateE2}) { + for (Matrix4f pose : cubes ? new Matrix4f[] {cubeE1, cubeE2} : new Matrix4f[] {templateE1, templateE2}) { applyGlintTexMatrix(glintMatrixA); assertTrue(capture.begin(groups)); - groups.queue(glintTemplate, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA); - capture.end(); - applyGlintTexMatrix(glintMatrixB); - GLStateManager.glColor4f(0.5f, 1, 1, 1); - assertFalse(capture.replay(groups), "changed layer color must retain the original render call"); - GLStateManager.glColor4f(1, 1, 1, 1); - GLStateManager.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE); - assertFalse(capture.replay(groups), "changed layer blending must retain the original render call"); - setGlintBlendState(); - assertTrue(capture.replay(groups)); - assertFalse(capture.replay(groups), "a captured pass is replayed only once"); - } - groups.drawLayer(layer); - instanced.endFrame(); - CubeParityFixture.assertPixelParity(reference, FfpFixture.readRegion(SIZE), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe); - } - - @Test - void replayedGlintCubesMatchIndependentPasses() { - glintState(); - final int[] reference = cubeReference(); - drawBaseCubes(); - setGlintBlendState(); - groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); - final GlintCapture capture = new GlintCapture(); - for (Matrix4f pose : new Matrix4f[] {cubeE1, cubeE2}) { - applyGlintTexMatrix(glintMatrixA); - assertTrue(capture.begin(groups)); - groups.queue(null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA, null); + if (cubes) groups.queue(SCOPE, null, glintCubes, CUBE_SCALE, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA, null); + else groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.GLINT, pose, 0, -1, 0, 0L, 0, glintMatrixA); capture.end(); applyGlintTexMatrix(glintMatrixB); + if (!cubes) { + GLStateManager.glColor4f(0.5f, 1, 1, 1); + assertFalse(capture.replay(groups), "changed layer color must retain the original render call"); + GLStateManager.glColor4f(1, 1, 1, 1); + GLStateManager.glBlendFunc(GL11.GL_ONE, GL11.GL_ONE); + assertFalse(capture.replay(groups), "changed layer blending must retain the original render call"); + setGlintBlendState(); + } assertTrue(capture.replay(groups)); + if (!cubes) assertFalse(capture.replay(groups), "a captured pass is replayed only once"); } groups.drawLayer(layer); instanced.endFrame(); @@ -773,7 +949,7 @@ class RetainedTesrGroupsGLTest { final GlintCapture base = new GlintCapture(); assertTrue(base.begin(groups)); base.allowBase(); - groups.queue(glintTemplate, layer, EntityMaterials.CUTOUT, templateE1, 0, 0xFF123456, 0, 0L, 0, null); + groups.queue(SCOPE, glintTemplate, layer, EntityMaterials.CUTOUT, templateE1, 0, 0xFF123456, 0, 0L, 0, null); base.end(); setGlintBlendState(); GLStateManager.glBlendFunc(GL11.GL_SRC_COLOR, GL11.GL_ONE); @@ -822,7 +998,6 @@ class RetainedTesrGroupsGLTest { GLStateManager.enableCull(); GLStateManager.glCullFace(GL11.GL_FRONT); final int cullCode = DrawState.liveCull(); - assertEquals(DrawState.CULL_FRONT, cullCode, "queue-time glCullFace(GL_FRONT) must pack as CULL_FRONT"); final int center = SIZE / 2 * SIZE + SIZE / 2; FfpFixture.clear(); @@ -835,23 +1010,13 @@ class RetainedTesrGroupsGLTest { final RenderLayer culled = RenderLayer.tesr(null, RetainedTesrTestSupport.STREAM, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, cullCode, false, true); FfpFixture.clear(); groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); - groups.queue(quad, culled, RetainedTesrTestSupport.STREAM, mirrored, 0, COLOR_ABGR, 0, 0L, 1, null); + groups.queue(SCOPE, quad, culled, RetainedTesrTestSupport.STREAM, mirrored, 0, COLOR_ABGR, 0, 0L, 1, null); GLStateManager.glCullFace(GL11.GL_BACK); - final boolean cullEnabledBefore = GLStateManager.getCullState().isEnabled(); - final int cullFaceBefore = GLStateManager.getPolygonState().getCullFaceMode(); - final boolean glCullEnabledBefore = GL11.glIsEnabled(GL11.GL_CULL_FACE); - final int glCullFaceBefore = GL11.glGetInteger(GL11.GL_CULL_FACE_MODE); - source.endBatch(groups); instanced.endFrame(); assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "the batched draw must not raise a GL error"); - assertEquals(cullEnabledBefore, GLStateManager.getCullState().isEnabled(), "the flush must leave the cached cull enable where it found it"); - assertEquals(cullFaceBefore, GLStateManager.getPolygonState().getCullFaceMode(), "the flush must leave the cached cull face mode where it found it"); - assertEquals(glCullEnabledBefore, GL11.glIsEnabled(GL11.GL_CULL_FACE), "the flush must leave the real cull enable where it found it"); - assertEquals(glCullFaceBefore, GL11.glGetInteger(GL11.GL_CULL_FACE_MODE), "the flush must leave the real cull face mode where it found it"); - CubeParityFixture.assertPixelParity(reference, FfpFixture.readRegion(SIZE), SIZE, BACKGROUND, RetainedTesrGroupsGLTest::describe); } @@ -890,10 +1055,10 @@ class RetainedTesrGroupsGLTest { final TemplateBuffer quad = capturedQuad(0.5); final TesrMaterial opposite = TesrMaterial.builder().build(); - final RenderLayer opposingLayer = RenderLayer.tesr(null, opposite, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, true); + final RenderLayer opposingLayer = RetainedTesrTestSupport.layerFor(opposite); groups.beginPass(new Matrix4f(), 0, 0, 0, instanced); - groups.queue(quad, opposingLayer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, COLOR_ABGR, 0, 0L, 1, null); + groups.queue(SCOPE, quad, opposingLayer, RetainedTesrTestSupport.STREAM, new Matrix4f(), 0, COLOR_ABGR, 0, 0L, 1, null); source.endBatch(groups); instanced.endFrame(); @@ -917,4 +1082,177 @@ class RetainedTesrGroupsGLTest { assertEquals(offsetFactorBefore, GLStateManager.getPolygonState().getOffsetFactor(), 0.0001f, "cache polygon offset factor must be restored"); assertEquals(glOffsetFactorBefore, GL11.glGetFloat(GL11.GL_POLYGON_OFFSET_FACTOR), 0.0001f, "driver polygon offset factor must be restored"); } + + private static final int[][] SHADER_PROBES = { { 200, 300 }, { 600, 300 }, { 400, 300 }, { 200, 500 } }; + + private static void assertRed(int[] pixel, String what) { + assertTrue(pixel[0] > 200 && pixel[1] < 50 && pixel[2] < 50, what + ", got " + Arrays.toString(pixel)); + } + + private static RenderLayer noLightmapLayer(TesrMaterial material) { + return RenderLayer.tesr(null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, false); + } + + private RenderLayer queueShaderPair(TesrMaterial material, boolean instancing) { + final RenderLayer shaderLayer = noLightmapLayer(material); + final TemplateBuffer template = capturedQuad(0.1); + FfpFixture.clear(); + groups.beginPass(new Matrix4f(), 0, 0, 0, instancing ? instanced : null); + groups.queue(SCOPE, template, shaderLayer, material, templateE1, 0, COLOR_ABGR, 0, 0L, 5, null); + groups.queue(SCOPE, template, shaderLayer, material, templateE2, 0, COLOR_ABGR, 0, 0L, 5, null); + return shaderLayer; + } + + private int[][] drawShaderLayer(RenderLayer shaderLayer) { + shaderLayer.startDrawing(); + groups.drawLayer(shaderLayer); + shaderLayer.endDrawing(); + instanced.endFrame(); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "shader draw must not raise a GL error"); + final int[][] pixels = new int[SHADER_PROBES.length][]; + for (int i = 0; i < pixels.length; i++) { + pixels[i] = FfpFixture.readPixel(SHADER_PROBES[i][0], SHADER_PROBES[i][1]); + } + return pixels; + } + + @Test + void shaderLayerBindsTheInstancedProgramOnceAndMatchesTheRetainedDraw() { + final TesrShader shader = RetainedTesrTestSupport.linkedSource("angelica:test_gl_source_shader").shader; + GLStateManager.disableBlend(); + GLStateManager.disableAlphaTest(); + + long bakedBinds = TesrShaderPrograms.bakedBinds; + long instancedBinds = TesrShaderPrograms.instancedBinds; + final int[][] instancedPixels = drawShaderLayer(queueShaderPair(TesrMaterial.builder().stream().shader(shader).build(), true)); + assertEquals(1, TesrShaderPrograms.bakedBinds - bakedBinds, "the layer hook binds the baked variant once"); + assertEquals(1, TesrShaderPrograms.instancedBinds - instancedBinds, "the instanced variant binds once for the layer"); + assertEquals(0, GLStateManager.getActiveProgram(), "release must restore the caller's program"); + assertEquals(1, groups.instancedDraws, "both instances share one template run"); + assertEquals(2, groups.instancedInstances); + + bakedBinds = TesrShaderPrograms.bakedBinds; + instancedBinds = TesrShaderPrograms.instancedBinds; + final int[][] retainedPixels = drawShaderLayer(queueShaderPair(TesrMaterial.builder().shader(shader).build(), false)); + assertEquals(1, TesrShaderPrograms.bakedBinds - bakedBinds); + assertEquals(0, TesrShaderPrograms.instancedBinds - instancedBinds, "nothing instanced, so no instanced bind"); + assertEquals(1, groups.instancedDraws, "the baked draw is not an instanced draw"); + + for (int i = 0; i < SHADER_PROBES.length; i++) { + assertArrayEquals(retainedPixels[i], instancedPixels[i], "probe " + i + " differs between the baked and instanced variants"); + } + assertRed(instancedPixels[0], "instance color must reach the left quad"); + assertRed(instancedPixels[1], "instance color must reach the right quad"); + assertTrue(instancedPixels[2][0] < 50 && instancedPixels[3][0] < 50, "probes off the quads must stay background"); + } + + private void drawDepthEqualPair(TesrShader shader, boolean instancing) { + drawDepthEqualPair(shader, instancing, null, new double[3]); + } + + private Group[] drawDepthEqualPair(TesrShader shader, boolean instancing, double[] baseOnlyCam, double[] cam) { + final TesrMaterial.Builder baseBuilder = TesrMaterial.builder(); + final TesrMaterial.Builder overlayBuilder = TesrMaterial.builder().depthEqual().noDepthWrite(); + if (shader != null) overlayBuilder.shader(shader); + final TesrMaterial baseMaterial = (instancing ? baseBuilder.stream() : baseBuilder).build(); + final TesrMaterial overlayMaterial = (instancing ? overlayBuilder.stream() : overlayBuilder).build(); + final RenderLayer baseLayer = noLightmapLayer(baseMaterial); + final RenderLayer overlayLayer = noLightmapLayer(overlayMaterial); + final TemplateBuffer template = capturedQuad(0.6); + final String path = instancing ? "instanced" : "baked"; + + final Matrix4f savedProjection = new Matrix4f(GLStateManager.getProjectionMatrix()); + GLStateManager.glViewport(0, 0, SIZE, SIZE); + GLStateManager.setProjectionMatrix(new Matrix4f().setPerspective(1.2217f, 1.3713f, 0.0517f, 173.31f)); + GLStateManager.disableTexture(); + GLStateManager.glDepthMask(true); + FfpFixture.clear(); + try { + final Matrix4f view = new Matrix4f().translation(0.0713f, -0.0291f, -2.3177f).rotateX(0.413f); + final Matrix4f pose = new Matrix4f(view).rotateY(0.917f).rotateZ(0.2311f).translate(0.0137f, 0.0219f, 0.0071f); + if (baseOnlyCam != null) { + groups.beginPass(view, baseOnlyCam[0], baseOnlyCam[1], baseOnlyCam[2]); + groups.queue(SCOPE, template, baseLayer, baseMaterial, pose, 0, 0xFF00FF00, 0, 0L, 5, null); + baseLayer.startDrawing(); + groups.drawLayer(baseLayer); + baseLayer.endDrawing(); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + FfpFixture.clear(); + } + final long instancedBefore = groups.instancedDraws; + groups.beginPass(view, cam[0], cam[1], cam[2], instancing ? instanced : null); + groups.queue(SCOPE + 1, template, baseLayer, baseMaterial, pose, 0, 0xFF00FF00, 0, 0L, 5, null); + groups.queue(SCOPE + 1, template, overlayLayer, overlayMaterial, pose, 0, COLOR_ABGR, 0, 0L, 5, null); + baseLayer.startDrawing(); + groups.drawLayer(baseLayer); + baseLayer.endDrawing(); + final int[] base = FfpFixture.readRegion(SIZE); + overlayLayer.startDrawing(); + assertEquals(GL11.GL_EQUAL, GL11.glGetInteger(GL11.GL_DEPTH_FUNC)); + groups.drawLayer(overlayLayer); + overlayLayer.endDrawing(); + instanced.endFrame(); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "depth-equal pair must not raise a GL error"); + assertEquals(instancing ? 2 : 0, groups.instancedDraws - instancedBefore, path + ": base and overlay must share the path"); + + final int[] overlaid = FfpFixture.readRegion(SIZE); + int covered = 0; + int missed = 0; + for (int i = 0; i < base.length; i++) { + if (base[i] == BACKGROUND) { + assertEquals(BACKGROUND, overlaid[i], path + ": overlay drew outside the base at pixel " + i); + continue; + } + assertEquals(0xFF00FF00, base[i], path + ": base pixel " + i); + covered++; + if (overlaid[i] != COLOR_ABGR) missed++; + } + assertTrue(covered > SIZE * SIZE / 20, path + ": base must cover a usable area, covered " + covered); + assertEquals(0, missed, path + ": depth-equal overlay missed " + missed + " of " + covered + " base pixels"); + return new Group[] { RetainedTesrTestSupport.groupsOn(groups, baseLayer).get(0), RetainedTesrTestSupport.groupsOn(groups, overlayLayer).get(0) }; + } finally { + GLStateManager.setProjectionMatrix(savedProjection); + } + } + + @Test + void depthEqualSetMembersBakeAgainstOneAnchor() { + GLStateManager.disableBlend(); + GLStateManager.disableAlphaTest(); + final double[] first = { 1000.3, 70.6, -2000.7 }; + final double[] moved = { 1300.4, 75.8, -1750.4 }; + final Group[] pair = drawDepthEqualPair(null, false, first, moved); + assertEquals(TesrAnchorMath.anchorCoord(first[0]), pair[0].anchor.x, "sanity: the move stays inside the re-anchor distance, so the base keeps its first anchor"); + assertTrue(pair[0].anchor.sameAs(pair[1].anchor), "an overlay joining later must bake against its writer's anchor"); + assertFalse(pair[0].streamPath || pair[1].streamPath, "sanity: both members are on the baked path"); + } + + @Test + void failedInstancedRelinkDrawsQueuedInstancesOnTheCpuPath() { + final TesrShaderPrograms.Entry entry = RetainedTesrTestSupport.linkedSource("angelica:test_gl_relink_failure_shader"); + GLStateManager.disableBlend(); + GLStateManager.disableAlphaTest(); + final RenderLayer shaderLayer = queueShaderPair(TesrMaterial.builder().stream().shader(entry.shader).build(), true); + assertEquals(1, RetainedTesrTestSupport.onlyGroup(groups).templateColumns.runs.size(), "sanity: both parts queued as instances"); + + TesrShaderPrograms.invalidateAll(); + TesrShaderPrograms.link(entry, false, RetainedTesrTestSupport.COLOR_VERTEX, RetainedTesrTestSupport.COLOR_FRAGMENT); + TesrShaderPrograms.link(entry, true, RetainedTesrTestSupport.BAKED_ONLY_VERTEX, RetainedTesrTestSupport.COLOR_FRAGMENT); + assertFalse(TesrShaderPrograms.instancedAvailable(entry), "sanity: the relinked instanced variant failed"); + + final int[][] pixels = drawShaderLayer(shaderLayer); + assertEquals(0, groups.instancedDraws, "no instanced program, so no instanced draw"); + assertEquals(2, groups.streamedInstances, "both queued instances must draw through the CPU fallback"); + assertRed(pixels[0], "left instance must reach the framebuffer"); + assertRed(pixels[1], "right instance must reach the framebuffer"); + } + + @Test + void shaderOverlayDepthEqualsItsEmulatedBaseOnBothPaths() { + final TesrShader shader = RetainedTesrTestSupport.linkedSource("angelica:test_gl_depth_parity_shader").shader; + GLStateManager.disableBlend(); + GLStateManager.disableAlphaTest(); + drawDepthEqualPair(shader, false); + drawDepthEqualPair(shader, true); + } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsTest.java index 88218cb95..df4995ab0 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrGroupsTest.java @@ -1,12 +1,11 @@ package com.gtnewhorizons.angelica.rendering.tesr; import com.gtnewhorizons.angelica.api.tesr.TesrMaterial; +import com.gtnewhorizons.angelica.api.tesr.TesrShader; +import com.gtnewhorizons.angelica.api.tesr.TesrShaders; import com.gtnewhorizons.angelica.compat.mojang.RenderLayer; -import com.gtnewhorizons.angelica.glsm.ffp.CubeParams; import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs; -import com.gtnewhorizons.angelica.glsm.testutil.Reflect; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; import net.coderbot.batchedentityrendering.impl.AngelicaBufferSource; import net.coderbot.batchedentityrendering.impl.BufferSegment; import net.coderbot.batchedentityrendering.impl.BufferSourceProbe; @@ -15,13 +14,24 @@ import net.minecraft.util.ResourceLocation; import org.joml.Matrix4f; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.lwjgl.opengl.GL11; import java.util.List; +import java.util.function.IntFunction; +import java.util.stream.Stream; import static com.gtnewhorizon.gtnhlib.client.renderer.cel.util.ModelQuadUtil.VERTEX_SIZE; +import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.DEPTH_EQUAL_OVERLAY; +import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.SCOPE; import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.STREAM; +import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.byLayer; import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.cubes; +import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.groupsOn; +import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.layerFor; import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.onlyGroup; import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.template; import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.testLayer; @@ -34,15 +44,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class RetainedTesrGroupsTest { + private static final TesrShader NOOP_SHADER = TesrShaders.register("angelica:test_groups_plain", () -> {}, () -> {}); + private final InstanceRing ring = new InstanceRing(); private AngelicaBufferSource source; private RetainedTesrGroups groups; private RenderLayer layer; + private final long[] clock = {1_000L}; @BeforeEach void freshGroups() { source = new AngelicaBufferSource(); - groups = new RetainedTesrGroups(source); + groups = new RetainedTesrGroups(source, () -> clock[0]); layer = testLayer(); } @@ -56,52 +69,6 @@ class RetainedTesrGroupsTest { } } - @Test - void retainedGroupsKeepDifferentItemIdsSeparateAcrossFrames() { - int previous = CapturedRenderingState.INSTANCE.getCurrentRenderedItem(); - try { - final TemplateBuffer template = template(); - RetainedTesrGroups.Group first = null; - for (int frame = 0; frame < 2; frame++) { - groups.beginPass(new Matrix4f(), 0, 0, 0); - for (int item : new int[] {42, 43, 42}) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(item); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null); - } - final Reference2ObjectOpenHashMap> byLayer = Reflect.get(groups, "byLayer"); - final ObjectArrayList entries = byLayer.get(layer); - assertEquals(2, entries.size()); - assertEquals(42, entries.get(0).itemId); - assertEquals(2, entries.get(0).templateColumns.size); - assertEquals(43, entries.get(1).itemId); - assertEquals(1, entries.get(1).templateColumns.size); - if (first != null) assertSame(first, entries.get(0), "stable groups must be reused across frames"); - first = entries.get(0); - } - } finally { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(previous); - } - } - - @Test - void instancedGroupsKeepItemIdsPerInstanceWithoutSplitting() { - int previous = CapturedRenderingState.INSTANCE.getCurrentRenderedItem(); - try { - groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(true, true)); - for (int item : new int[] {42, 43}) { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(item); - long info = InstancedAttribs.packEntityInfo(-1, 5, item); - groups.queue(template(), layer, STREAM, new Matrix4f(), 0, -1, 0, info, 5, null); - } - final RetainedTesrGroups.Group group = onlyGroup(groups); - assertEquals(2, group.templateColumns.size); - assertEquals(42, RetainedTesrGroups.itemFromInfo(group.templateColumns.infos.getLong(0))); - assertEquals(43, RetainedTesrGroups.itemFromInfo(group.templateColumns.infos.getLong(1))); - } finally { - CapturedRenderingState.INSTANCE.setCurrentRenderedItem(previous); - } - } - @Test void retainedGroupsSeparateTheFullIdTupleAndTintAndReuseStableKeys() { final var state = CapturedRenderingState.INSTANCE; @@ -112,16 +79,18 @@ class RetainedTesrGroupsTest { for (int frame = 0; frame < 2; frame++) { groups.beginPass(new Matrix4f(), 0, 0, 0); for (int entity : new int[] {11, 12}) { - state.setCurrentEntityAndItem(entity, 13); - for (int block : new int[] {21, 22}) { - state.setCurrentBlockEntity(block); - for (float red : new float[] {0, 1}) { - state.setCurrentEntityColor(red, 0, 0, 0.5f); - groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, block, null); + for (int item : new int[] {13, 14}) { + state.setCurrentEntityAndItem(entity, item); + for (int block : new int[] {21, 22}) { + state.setCurrentBlockEntity(block); + for (float red : new float[] {0, 1}) { + state.setCurrentEntityColor(red, 0, 0, 0.5f); + groups.queue(SCOPE, template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, block, null); + } } } } - assertEquals(8, groups.groupCount(), "only distinct ID/tint combinations allocate groups, including across frames"); + assertEquals(16, groups.groupCount(), "only distinct ID/tint combinations allocate groups, including across frames"); } } finally { state.popCurrentEntityColor(); @@ -144,7 +113,7 @@ class RetainedTesrGroupsTest { state.setCurrentEntityColor(i / 100f, 0, 0, 1); final long info = InstancedAttribs.packEntityInfo(i, i + 100, i + 200); final int overlay = AngelicaBufferSource.packEntityColor(state.getCurrentEntityColor()); - groups.queue(template(), layer, STREAM, new Matrix4f(), 0, -1, overlay, info, i + 100, null); + groups.queue(SCOPE, template(), layer, STREAM, new Matrix4f(), 0, -1, overlay, info, i + 100, null); } final var group = onlyGroup(groups); assertEquals(100, group.templateColumns.size); @@ -160,11 +129,31 @@ class RetainedTesrGroupsTest { } } - @Test - void retainedGroupAccumulatesInsteadOfStreaming() { + @FunctionalInterface + private interface GroupSetup { + void apply(RetainedTesrGroups groups, RenderLayer layer); + } - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null); + private static void idleGap(RetainedTesrGroups groups) { + for (long i = 0; i <= RetainedTesrGroups.IDLE_RESET_FRAMES + 1; i++) { + groups.beginPass(new Matrix4f(), 0, 0, 0); + } + } + + static Stream retainedDraws() { + return Stream.of( + Arguments.of("fresh group", (GroupSetup) (g, l) -> g.beginPass(new Matrix4f(), 0, 0, 0)), + Arguments.of("promoted group after an idle gap", (GroupSetup) (g, l) -> { + g.forceStreaming(l, TesrMaterial.CURRENT_STATE, 5); + idleGap(g); + })); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("retainedDraws") + void retainedGroupDrawsWithoutSegments(String name, GroupSetup setup) { + setup.apply(groups, layer); + groups.queue(SCOPE, template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null); assertTrue(groups.hasDraws(layer)); assertEquals(0, groups.streamedInstances); @@ -173,156 +162,291 @@ class RetainedTesrGroupsTest { assertNull(BufferSourceProbe.segmentsFor(source, layer), "retained instances must not produce dynamic segments"); } - @Test - void streamingGroupStreamsIntoDynamicSegments() { + private static final TesrMaterial TRANSLUCENT_STREAM = TesrMaterial.builder().translucent().stream().build(); - groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5); - assertEquals(1, groups.streamPromotions); + private static void queueAt(RetainedTesrGroups groups, RenderLayer layer, TesrMaterial material, float x, float y, float z) { + groups.queue(SCOPE, template(), layer, material, new Matrix4f().translation(x, y, z), 0, -1, 0, 0L, 5, null); + } - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, 0L, 5, null); - groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f().translation(4f, 5f, 6f), 0, -1, 0, 0L, 5, null); + static Stream streamingDraws() { + return Stream.of( + Arguments.of("forced promotion", (GroupSetup) (g, l) -> { + g.forceStreaming(l, TesrMaterial.CURRENT_STATE, 5); + g.beginPass(new Matrix4f(), 0, 0, 0); + queueAt(g, l, TesrMaterial.CURRENT_STATE, 1f, 2f, 3f); + queueAt(g, l, TesrMaterial.CURRENT_STATE, 4f, 5f, 6f); + }, 2, 1), + Arguments.of("stream material from the first queue", (GroupSetup) (g, l) -> { + g.beginPass(new Matrix4f(), 0, 0, 0); + queueAt(g, l, TRANSLUCENT_STREAM, 1f, 2f, 3f); + }, 1, 0), + Arguments.of("stream material after an idle gap", (GroupSetup) (g, l) -> { + g.beginPass(new Matrix4f(), 0, 0, 0); + queueAt(g, l, TRANSLUCENT_STREAM, 0f, 0f, 0f); + idleGap(g); + queueAt(g, l, TRANSLUCENT_STREAM, 0f, 0f, 0f); + }, 2, 0), + Arguments.of("cube parts without an instanced renderer", (GroupSetup) (g, l) -> { + g.beginPass(new Matrix4f(), 0, 0, 0); + g.queue(SCOPE, template(), cubes(2), 0.0625f, l, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null); + }, 1, 0)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("streamingDraws") + void streamingGroupStreamsIntoDynamicSegments(String name, GroupSetup setup, int instances, int promotions) { + setup.apply(groups, layer); assertFalse(groups.hasDraws(layer), "volatile group must not report retained draws"); - assertEquals(2, groups.streamedInstances); + assertEquals(instances, groups.streamedInstances); + assertEquals(promotions, groups.streamPromotions); + assertEquals(0, cubeInstances(groups)); BufferSourceProbe.prepare(source); final List segments = BufferSourceProbe.segmentsFor(source, layer); assertEquals(1, segments.size()); - assertEquals(8, segments.get(0).getVertexCount(), "both instances stream into one segment"); + assertEquals(4 * instances, segments.get(0).getVertexCount()); assertEquals(5, segments.get(0).getBlockEntityId()); } + private void probedFrame(TemplateBuffer template, TesrMaterial material, Matrix4f... instances) { + clock[0] += RetainedTesrGroups.STABILITY_PROBE_MS; + groups.beginPass(new Matrix4f(), 0, 0, 0); + for (final Matrix4f mv : instances) groups.queue(SCOPE, template, layer, material, mv, 0, -1, 0, 0L, 5, null); + } + + private static IntFunction perFrame(IntFunction instances) { + return instances; + } + + static Stream probeSequences() { + final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f); + final Matrix4f other = new Matrix4f().translation(4f, 5f, 6f); + final float edge = 1f + 0.5f * TesrAnchorMath.TRANSFORM_TOLERANCE; + final float jitter = TesrAnchorMath.TRANSFORM_TOLERANCE / 8f; + final Matrix4f below = new Matrix4f().translation(edge - jitter, 2f, 3f); + final Matrix4f above = new Matrix4f().translation(edge + jitter, 2f, 3f); + return Stream.of( + Arguments.of("stable opaque", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { fixed }), true), + Arguments.of("reordered", TesrMaterial.CURRENT_STATE, perFrame(i -> i % 2 == 0 ? new Matrix4f[] { fixed, other } : new Matrix4f[] { other, fixed }), true), + Arguments.of("jitter inside tolerance", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { i % 2 == 0 ? below : above }), true), + Arguments.of("moving", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { new Matrix4f().translation(i, 0f, 0f) }), false), + Arguments.of("instance count changes", TesrMaterial.CURRENT_STATE, perFrame(i -> i % 2 == 0 ? new Matrix4f[] { fixed, fixed } : new Matrix4f[] { fixed }), false), + Arguments.of("rotating", TesrMaterial.CURRENT_STATE, perFrame(i -> new Matrix4f[] { new Matrix4f().rotateY(0.002f * i) }), false), + Arguments.of("stream material", STREAM, perFrame(i -> new Matrix4f[] { fixed }), false)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("probeSequences") + void probedInstancesDecideDemotion(String name, TesrMaterial material, IntFunction instances, boolean demotes) { + final TemplateBuffer template = template(); + if (!material.isStream()) groups.forceStreaming(layer, material, 5); + int frames = 0; + while (frames < 8 && !groups.hasDraws(layer)) { + probedFrame(template, material, instances.apply(frames++)); + } + assertEquals(demotes, groups.hasDraws(layer)); + if (demotes) assertTrue(frames > RetainedTesrGroups.DEMOTE_AFTER_STABLE_PROBES + 1, "the first probe has nothing to match, took " + frames); + } + @Test - void stableTransformsDemoteStreamingGroup() { + void promotedGroupOnlyProbesOncePerInterval() { final TemplateBuffer template = template(); final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f); groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5); - long streamedFrames = 0; - for (int frame = 0; frame < RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4; frame++) { + for (int frame = 0; frame < 64; frame++) { + clock[0] += 1; groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, fixed, 0, -1, 0, 0L, 5, null); - if (groups.hasDraws(layer)) break; - streamedFrames++; - } - - assertTrue(groups.hasDraws(layer), "identical frames must demote the group back to retained"); - assertTrue(streamedFrames >= RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES, "demotion must wait for the stability window, streamed " + streamedFrames); - } - - @Test - void movingTransformsStayStreaming() { - final TemplateBuffer template = template(); - - groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5); - for (int frame = 0; frame < RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4; frame++) { - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f().translation(frame, 0f, 0f), 0, -1, 0, 0L, 5, null); - assertFalse(groups.hasDraws(layer), "moving group must keep streaming at frame " + frame); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, fixed, 0, -1, 0, 0L, 5, null); + assertFalse(groups.hasDraws(layer), "64 ms cannot hold enough probes to demote, frame " + frame); } } - @Test - void idleGapResetsVolatility() { - - groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5); - for (long i = 0; i <= RetainedTesrGroups.IDLE_RESET_FRAMES + 1; i++) { - groups.beginPass(new Matrix4f(), 0, 0, 0); - } - groups.queue(template(), layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 5, null); - - assertTrue(groups.hasDraws(layer), "idle reset must demote the group back to retained"); - assertEquals(0, groups.streamedInstances); + private void queueBlock(long scope, TesrMaterial material, int id) { + groups.queue(scope, template(), layer, material, new Matrix4f(), 0, -1, 0, 0L, id, null); } @Test - void translucentStreamingGroupNeverDemotes() { - final TesrMaterial translucent = TesrMaterial.builder().translucent().build(); - final TemplateBuffer template = template(); - final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f); - - groups.forceStreaming(layer, translucent, 5); - final int frames = RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4; - for (int frame = 0; frame < frames; frame++) { - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, translucent, fixed, 0, -1, 0, 0L, 5, null); - assertFalse(groups.hasDraws(layer), "translucent group must keep streaming at frame " + frame); - } - assertEquals(frames, groups.streamedInstances); - } - - @Test - void streamMaterialStreamsFromFirstQueue() { - final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build(); - + void depthEqualOverlayCouplesOnlyWithinItsOwnNonZeroRenderScope() { groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template(), layer, stream, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, 0L, 5, null); + queueBlock(SCOPE, TesrMaterial.CURRENT_STATE, 5); + queueBlock(SCOPE, DEPTH_EQUAL_OVERLAY, 5); + queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6); + queueBlock(0L, TesrMaterial.CURRENT_STATE, 7); + queueBlock(0L, DEPTH_EQUAL_OVERLAY, 7); - assertFalse(groups.hasDraws(layer), "stream group must not report retained draws"); - assertEquals(1, groups.streamedInstances); - assertEquals(0, groups.streamPromotions, "streaming from birth is not a promotion"); + final ObjectArrayList entries = groupsOn(groups, layer); + assertEquals(5, entries.size()); + assertNotNull(entries.get(0).depthEqualSet, "the id-5 writer must belong to a path set"); + assertSame(entries.get(0).depthEqualSet, entries.get(1).depthEqualSet, "the overlay must couple with the writer from its own scope"); + assertNull(entries.get(2).depthEqualSet, "a writer from another scope must not be coupled"); + assertNull(entries.get(3).depthEqualSet, "scope 0 never couples"); + assertNull(entries.get(4).depthEqualSet, "scope 0 never couples"); + } - BufferSourceProbe.prepare(source); - final List segments = BufferSourceProbe.segmentsFor(source, layer); - assertEquals(1, segments.size()); - assertEquals(5, segments.get(0).getBlockEntityId()); + private static final TesrMaterial STREAM_OVERLAY = TesrMaterial.builder().additiveAlpha().depthEqual().noDepthWrite().stream().build(); + private static final TesrMaterial BAKED_PLAIN_SHADER = TesrMaterial.builder().shader(NOOP_SHADER).build(); + + private void instancedPair(InstancedTemplateRenderer renderer) { + groups.forceStreaming(layer, TesrMaterial.CURRENT_STATE, 5); + groups.beginPass(new Matrix4f(), 0, 0, 0, renderer); + queueBlock(SCOPE, TesrMaterial.CURRENT_STATE, 5); + queueBlock(SCOPE, DEPTH_EQUAL_OVERLAY, 5); } @Test - void streamGroupNeverDemotesUnderStableTransforms() { - for (final TesrMaterial material : new TesrMaterial[] { - TesrMaterial.builder().stream().build(), - TesrMaterial.builder().translucent().stream().build()}) { - freshGroups(); - final TemplateBuffer template = template(); - final Matrix4f fixed = new Matrix4f().translation(1f, 2f, 3f); + void promotedGroupJoiningAFreshDepthEqualSetKeepsItsPath() { + final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring); + instancedPair(renderer); + groups.beginPass(new Matrix4f(), 0, 0, 0, renderer); + queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 5); + queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 5); - final int frames = RetainedTesrGroups.DEMOTE_AFTER_STABLE_FRAMES + 4; - for (int frame = 0; frame < frames; frame++) { - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, material, fixed, 0, -1, 0, 0L, 5, null); - assertFalse(groups.hasDraws(layer), material.transparency() + " stream group must keep streaming at frame " + frame); - } - assertEquals(frames, groups.streamedInstances); + final ObjectArrayList pair = groupsOn(groups, layer); + assertTrue(pair.get(0).streamPath, "joining a fresh set must not demote the promoted writer"); + assertTrue(pair.get(1).streamPath, "the overlay must follow its promoted writer"); + } + + @Test + void bakedGroupAdoptsAnInstancedDepthEqualSetBeforeItsFirstInstance() { + final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring); + instancedPair(renderer); + groups.beginPass(new Matrix4f(), 0, 0, 0, renderer); + queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 5); + queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6); + + final RetainedTesrGroups.Group joined = groupsOn(groups, layer).get(2); + assertEquals(1, joined.templateColumns.runs.size(), "its first instance must already take the set's instanced path"); + } + + @Test + void memberThatCannotInstanceDemotesTheDepthEqualSet() { + final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring); + instancedPair(renderer); + groups.beginPass(new Matrix4f(), 0, 0, 0, renderer); + queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 5); + queueBlock(SCOPE + 1, BAKED_PLAIN_SHADER, 5); + + assertFalse(groupsOn(groups, layer).get(0).depthEqualSet.instanced, "a member whose shader has no instanced variant forces the set to baked"); + } + + @Test + void atlasPartPulledOffTheInstancedPathBakesItsSlotUvs() { + final EntityTextureAtlas.Slot slot = new EntityTextureAtlas.Slot(new EntityTextureAtlas.Page(new ResourceLocation("angelica", "test_page"), 0), 0, 0, 4, 2); + final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring); + for (int frame = 0; frame < 2; frame++) { + groups.beginPass(new Matrix4f(), 0, 0, 0, renderer); + groups.queue(SCOPE + frame, template(), cubes(1), 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, slot); + queueBlock(SCOPE + frame, DEPTH_EQUAL_OVERLAY, 5); + queueBlock(SCOPE + frame, BAKED_PLAIN_SHADER, 5); } + + final RetainedTesrGroups.Group atlas = groupsOn(groups, layer).get(0); + assertFalse(atlas.streamPath, "sanity: a member that cannot instance must pull the atlas writer off the instanced path"); + assertEquals(1, atlas.instTexMatrices.size(), "sanity: the part must take the retained branch"); + assertEquals(slot.uvMatrix, atlas.instTexMatrices.get(0), "a baked atlas part must sample its slot, not the whole atlas page"); } @Test - void idleGapKeepsStreamGroupStreaming() { - final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build(); + void mergedDepthEqualSetsKeepTheInstancedPath() { + final InstancedTemplateRenderer renderer = new InstancedTemplateRenderer(ring); + instancedPair(renderer); + queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6); + queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 7); + queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 6); + groups.beginPass(new Matrix4f(), 0, 0, 0, renderer); + queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 5); + queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 6); + final ObjectArrayList entries = groupsOn(groups, layer); + assertSame(entries.get(1).depthEqualSet, entries.get(4).depthEqualSet, "both overlays in one scope must merge their sets"); + assertTrue(entries.get(4).depthEqualSet.instanced, "the larger baked set must take the instanced path of the one it absorbs"); + } + + @Test + void mergedDepthEqualSetsShareTheSurvivorsAnchor() { groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, null); - for (long i = 0; i <= RetainedTesrGroups.IDLE_RESET_FRAMES + 1; i++) { - groups.beginPass(new Matrix4f(), 0, 0, 0); - } - groups.queue(template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, null); + queueBlock(SCOPE, TesrMaterial.CURRENT_STATE, 5); + queueBlock(SCOPE, DEPTH_EQUAL_OVERLAY, 5); + groups.beginPass(new Matrix4f(), 304, 0, 0); + queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 6); + queueBlock(SCOPE + 1, TesrMaterial.CURRENT_STATE, 7); + queueBlock(SCOPE + 1, DEPTH_EQUAL_OVERLAY, 6); + final ObjectArrayList entries = groupsOn(groups, layer); + assertEquals(0, entries.get(0).anchor.x, "sanity: the first set anchored at the origin"); + assertEquals(304, entries.get(4).anchor.x, "sanity: the second set anchored at the moved camera"); - assertFalse(groups.hasDraws(layer), "idle reset must not move a stream group to retained"); - assertEquals(2, groups.streamedInstances); + groups.beginPass(new Matrix4f(), 304, 0, 0); + queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 5); + queueBlock(SCOPE + 2, DEPTH_EQUAL_OVERLAY, 6); + assertSame(entries.get(1).depthEqualSet, entries.get(4).depthEqualSet, "sanity: both overlays in one scope merge their sets"); + assertEquals(0, entries.get(1).anchor.x, "a member that already queued this frame keeps its anchor until the next frame"); + + groups.beginPass(new Matrix4f(), 304, 0, 0); + queueBlock(SCOPE + 3, TesrMaterial.CURRENT_STATE, 5); + queueBlock(SCOPE + 3, DEPTH_EQUAL_OVERLAY, 5); + assertEquals(304, entries.get(0).anchor.x, "an absorbed writer must take the larger set's anchor"); + assertEquals(304, entries.get(1).anchor.x, "an absorbed overlay must take the larger set's anchor"); } - @Test - void streamGroupRoutesToInstanceArraysWhenInstancedCapable() { - final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build(); + @ParameterizedTest(name = "instancing {0}") + @ValueSource(booleans = { true, false }) + void streamOverlayPinsDepthEqualSetInstancedOrBakesWithItsWriter(boolean available) { + final InstancedTemplateRenderer renderer = available ? new InstancedTemplateRenderer(ring) : null; + for (int frame = 0; frame < 2; frame++) { + groups.beginPass(new Matrix4f(), 0, 0, 0, renderer); + queueBlock(SCOPE + frame, TesrMaterial.CURRENT_STATE, 5); + queueBlock(SCOPE + frame, STREAM_OVERLAY, 5); + } - groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template(), layer, stream, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, 0L, 5, null); + final ObjectArrayList pair = groupsOn(groups, layer); + assertEquals(available, pair.get(0).streamPath, "the writer must take the pinned path"); + assertEquals(available, pair.get(1).streamPath, "the stream overlay must share its writer's path"); + } - assertTrue(groups.hasDraws(layer), "instanced-capable stream group accumulates for the hook draw"); + @ParameterizedTest(name = "stream material {0}") + @ValueSource(booleans = { false, true }) + void streamingGroupRoutesToInstanceArraysWhenInstancedCapable(boolean streamMaterial) { + final TesrMaterial material = streamMaterial ? TesrMaterial.builder().translucent().stream().build() : TesrMaterial.builder().additiveAlpha().build(); + final long info = InstancedAttribs.packEntityInfo(7, 5, 9); + + if (!streamMaterial) groups.forceStreaming(layer, material, 5); + groups.beginPass(new Matrix4f(), 100.5, 64.25, -100.75, new InstancedTemplateRenderer(ring)); + groups.queue(SCOPE, template(), layer, material, new Matrix4f().translation(1f, 2f, 3f), 0, -1, 0, info, 5, null); + + assertTrue(groups.hasDraws(layer), "a streaming group accumulates for the instanced hook draw"); assertEquals(0, groups.streamedInstances, "no segment streaming when instanced"); + final RetainedTesrGroups.Group group = onlyGroup(groups); + assertEquals(1, group.templateColumns.runs.size()); + assertEquals(info, group.templateColumns.infos.getLong(0)); + assertEquals(3f, group.templateColumns.matrices[14], "instanced columns hold the caller's modelview, not the anchor-relative one"); BufferSourceProbe.prepare(source); assertNull(BufferSourceProbe.segmentsFor(source, layer)); } + @ParameterizedTest(name = "stream material {0}") + @ValueSource(booleans = { false, true }) + void drawModeMismatchFallsBackToRetained(boolean streamMaterial) { + final TesrMaterial material = streamMaterial ? TesrMaterial.builder().translucent().stream().build() : TesrMaterial.CURRENT_STATE; + final TemplateBuffer triangles = new TemplateBuffer(new int[3 * VERTEX_SIZE], 3, GL11.GL_TRIANGLES); + + if (!streamMaterial) groups.forceStreaming(layer, material, 5); + groups.beginPass(new Matrix4f(), 0, 0, 0); + groups.queue(SCOPE, triangles, layer, material, new Matrix4f(), 0, -1, 0, 0L, 5, null); + + assertTrue(groups.hasDraws(layer), "a template the layer cannot segment-stream falls back to retained accumulation"); + assertEquals(0, groups.streamedInstances); + assertEquals(streamMaterial, onlyGroup(groups).streamPath, "a promoted group returns to baked, a stream group keeps streaming"); + } + @Test void streamGroupTexMatrixOpensARun() { final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build(); final Matrix4f texMatrix = new Matrix4f().translation(0.5f, 0.5f, 0f); groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, texMatrix); + groups.queue(SCOPE, template(), layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, texMatrix); texMatrix.translation(9f, 9f, 9f); assertTrue(groups.hasDraws(layer), "a texture matrix now opens an instanced run instead of falling back to streaming"); @@ -332,39 +456,24 @@ class RetainedTesrGroupsTest { assertEquals(new Matrix4f().translation(0.5f, 0.5f, 0f), group.templateColumns.runs.get(0).matrix, "the run holds a copy, not the caller's matrix"); } - @Test - void streamGroupDrawModeMismatchFallsBackToRetained() { - final TesrMaterial stream = TesrMaterial.builder().translucent().stream().build(); - final int[] data = new int[3 * VERTEX_SIZE]; - final TemplateBuffer triangles = new TemplateBuffer(data, 3, GL11.GL_TRIANGLES); - - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(triangles, layer, stream, new Matrix4f(), 0, -1, 0, 0L, 5, null); - - assertTrue(groups.hasDraws(layer), "mismatched drawMode must fall back to retained accumulation"); - assertEquals(0, groups.streamedInstances); - } - @Test void sweepEvictsIdleGroupsAndKeepsLive() { - final long[] clock = {1_000L}; - groups = new RetainedTesrGroups(source, () -> clock[0]); final TemplateBuffer template = template(); groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null); assertEquals(2, groups.groupCount()); clock[0] += RetainedTesrGroups.GROUP_TTL_MS + 1; groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); groups.sweep(clock[0]); assertEquals(1, groups.groupCount(), "idle group must be evicted, live group kept"); assertTrue(groups.hasDraws(layer), "live group still draws this frame"); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null); assertEquals(2, groups.groupCount(), "evicted id must be re-creatable"); } @@ -373,23 +482,22 @@ class RetainedTesrGroupsTest { final TemplateBuffer template = template(); groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 2, null); groups.clear(); assertEquals(0, groups.groupCount()); assertFalse(groups.hasDraws(layer)); groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); assertEquals(1, groups.groupCount()); assertTrue(groups.hasDraws(layer)); } private static int cubeInstances(RetainedTesrGroups groups) { - final Reference2ObjectOpenHashMap> byLayer = Reflect.get(groups, "byLayer"); int total = 0; - for (final ObjectArrayList list : byLayer.values()) { + for (final ObjectArrayList list : byLayer(groups).values()) { for (int i = 0; i < list.size(); i++) { final ObjectArrayList runs = list.get(i).cubeColumns.runs; for (int r = 0; r < runs.size(); r++) total += runs.get(r).instances; @@ -399,57 +507,21 @@ class RetainedTesrGroupsTest { } @Test - void cubePartsRouteToPerPartRecordsThatKeepTheirTemplate() { + void cubePartsKeepPerPartRecordsAndTheirTemplateWithoutAccumulatingAcrossFrames() { final TemplateBuffer template = template(); - final Matrix4f mv = new Matrix4f().translation(1f, 2f, 3f); - groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template, cubes(3), 0.0625f, layer, STREAM, mv, 7, -1, 0, 0L, 5, null, null); - groups.queue(template, cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 7, -1, 0, 0L, 5, null, null); + for (int frame = 0; frame < 2; frame++) { + groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(false, false)); + groups.queue(SCOPE, template, cubes(3), 0.0625f, layer, STREAM, new Matrix4f().translation(1f, 2f, 3f), 7, -1, 0, 0L, 5, null, null); + groups.queue(SCOPE, template, cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 7, -1, 0, 0L, 5, null, null); + } - assertTrue(groups.hasDraws(layer)); - assertEquals(0, groups.streamedInstances); final RetainedTesrGroups.Group group = onlyGroup(groups); - assertEquals(0, group.templateColumns.size, "cube parts stay out of the template columns"); assertEquals(2, group.cubeColumns.size, "one record per part, not per cube"); - assertEquals(5, cubeInstances(groups)); - assertTrue(group.cubeColumns.templates.isEmpty(), "the FFP cube path has no fallback that needs the template"); - - final RetainedTesrGroups piped = new RetainedTesrGroups(source); - piped.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(false, false)); - piped.queue(template, cubes(3), 0.0625f, layer, STREAM, mv, 7, -1, 0, 0L, 5, null, null); - piped.queue(template, cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 7, -1, 0, 0L, 5, null, null); - - final RetainedTesrGroups.Group pipedGroup = onlyGroup(piped); - assertEquals(2, pipedGroup.cubeColumns.templates.size(), "a pipeline can fall back to the per-part template"); - assertSame(template, pipedGroup.cubeColumns.templates.get(0)); - } - - @Test - void cubePartsFallBackToTemplatesWithoutAnInstancedRenderer() { - - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template(), cubes(2), 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null); - - assertEquals(0, cubeInstances(groups)); - assertEquals(1, groups.streamedInstances); - } - - @Test - void cubeInstancesClearBetweenFrames() { - final CubeParams[] params = cubes(2); - - groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template(), params, 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null); - groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template(), params, 0.0625f, layer, STREAM, new Matrix4f(), 0, -1, 0, 0L, 5, null, null); - - assertEquals(2, cubeInstances(groups), "the previous frame's instances must not accumulate"); - } - - @Test - void groupIdOnlySplitsPerEntityWhenAPackIsLoaded() { - assertEquals(ModelPartBatcher.groupId(true, true, -1, 9), RetainedTesrGroups.entityFromInfo(InstancedAttribs.packEntityInfo(-1, 9, 0))); + assertEquals(5, cubeInstances(groups), "the previous frame's instances must not accumulate"); + assertEquals(0, group.templateColumns.size, "cube parts stay out of the template columns"); + assertEquals(2, group.cubeColumns.templates.size(), "a pipeline can fall back to the per-part template"); + assertSame(template, group.cubeColumns.templates.get(0)); } @Test @@ -459,10 +531,10 @@ class RetainedTesrGroupsTest { final Matrix4f matrixB = new Matrix4f().translation(0f, 0.5f, 0f); groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA); - groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 2, matrixA); - groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 3, matrixB); - groups.queue(template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 4, matrixA); + groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA); + groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 2, matrixA); + groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 3, matrixB); + groups.queue(SCOPE, template, layer, EntityMaterials.OVERLAY, new Matrix4f(), 0, -1, 0, 0L, 4, matrixA); final RetainedTesrGroups.Group group = onlyGroup(groups); assertEquals(3, group.templateColumns.runs.size(), "OVERLAY is depth-equal but writes depth, so it must not merge like GLINT"); @@ -475,15 +547,15 @@ class RetainedTesrGroupsTest { final Matrix4f matrixB = new Matrix4f().translation(0f, 0.5f, 0f); groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA); - groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 2, matrixB); - groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 3, matrixA); + groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA); + groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 2, matrixB); + groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 3, matrixA); final RetainedTesrGroups.Group frame1 = onlyGroup(groups); assertEquals(2, frame1.templateColumns.runs.size()); assertNotNull(frame1.templateColumns.runs.get(0).next, "sanity: frame 1 must build a chain"); groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring)); - groups.queue(template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA); + groups.queue(SCOPE, template, layer, EntityMaterials.GLINT, new Matrix4f(), 0, -1, 0, 0L, 1, matrixA); final RetainedTesrGroups.Group frame2 = onlyGroup(groups); assertEquals(1, frame2.templateColumns.runs.size(), "only one distinct matrix was queued this frame"); @@ -494,12 +566,10 @@ class RetainedTesrGroupsTest { @Test void sweptGroupIsNotResurrectedByTheMemo() { - final long[] clock = {1_000L}; - groups = new RetainedTesrGroups(source, () -> clock[0]); final TemplateBuffer template = template(); groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); assertEquals(1, groups.groupCount()); clock[0] += RetainedTesrGroups.GROUP_TTL_MS + 1; @@ -507,25 +577,35 @@ class RetainedTesrGroupsTest { assertEquals(0, groups.groupCount(), "the idle group must be evicted"); groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); + groups.queue(SCOPE, template, layer, TesrMaterial.CURRENT_STATE, new Matrix4f(), 0, -1, 0, 0L, 1, null); assertEquals(1, groups.groupCount(), "a fresh group must be created, not the evicted one resurrected by the last-group memo"); assertTrue(groups.hasDraws(layer), "the new group must be reachable through the layer map for drawing"); } + private static final TesrMaterial PLAIN_SHADER = TesrMaterial.builder().stream().shader(NOOP_SHADER).build(); + + private int instancedAfterQueue(TesrMaterial material, TesrInstancingPipeline pipeline) { + final RetainedTesrGroups fresh = new RetainedTesrGroups(source); + fresh.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), pipeline); + fresh.queue(SCOPE, template(), layerFor(material), material, new Matrix4f(), 0, -1, 0, 0L, 5, null); + final RetainedTesrGroups.Group group = onlyGroup(fresh); + return group.templateColumns.runs.isEmpty() ? 0 : group.templateColumns.size; + } + @Test - void alternatingMaterialsRouteToTheirOwnGroups() { - final TemplateBuffer template = template(); - final TesrMaterial materialA = TesrMaterial.builder().stream().build(); - final TesrMaterial materialB = TesrMaterial.builder().translucent().stream().build(); + void runnableShaderMaterialIsInstancedOnlyUnderAPack() { + assertEquals(0, instancedAfterQueue(PLAIN_SHADER, null), "its program cannot read the per-instance attributes"); + assertEquals(1, instancedAfterQueue(PLAIN_SHADER, new RetainedTesrTestSupport.CountingPipeline(true, true)), "the pack draws it with its own instanced variant"); - groups.beginPass(new Matrix4f(), 0, 0, 0); - groups.queue(template, layer, materialA, new Matrix4f(), 0, -1, 0, 0L, 5, null); - groups.queue(template, layer, materialB, new Matrix4f(), 0, -1, 0, 0L, 5, null); - groups.queue(template, layer, materialA, new Matrix4f(), 0, -1, 0, 0L, 5, null); - groups.queue(template, layer, materialB, new Matrix4f(), 0, -1, 0, 0L, 5, null); + final RenderLayer shaderLayer = layerFor(PLAIN_SHADER); + groups.beginPass(new Matrix4f(), 0, 0, 0, new InstancedTemplateRenderer(ring), new RetainedTesrTestSupport.CountingPipeline(true, true)); + groups.queue(SCOPE, template(), cubes(3), 0.0625f, shaderLayer, PLAIN_SHADER, new Matrix4f(), 0, -1, 0, 0L, 5, null, null); - assertEquals(2, groups.groupCount(), "the last-group memo must compare material, not just layer and id, on every queue call"); + final RetainedTesrGroups.Group group = onlyGroup(groups); + assertEquals(0, cubeInstances(groups), "a shader material never takes the cube path"); + assertEquals(1, group.templateColumns.size); + assertEquals(1, group.templateColumns.runs.size()); } @Test diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrTestSupport.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrTestSupport.java index b8cf56f9c..859fdd4e6 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrTestSupport.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/RetainedTesrTestSupport.java @@ -1,6 +1,7 @@ package com.gtnewhorizons.angelica.rendering.tesr; import com.gtnewhorizons.angelica.api.tesr.TesrMaterial; +import com.gtnewhorizons.angelica.api.tesr.TesrShaders; import com.gtnewhorizons.angelica.compat.mojang.RenderLayer; import com.gtnewhorizons.angelica.glsm.ffp.CubeParams; import com.gtnewhorizons.angelica.glsm.ffp.Instancing; @@ -22,10 +23,38 @@ final class RetainedTesrTestSupport { private RetainedTesrTestSupport() {} + static final long SCOPE = 1L; + static final TesrMaterial STREAM = TesrMaterial.builder().stream().build(); + static final TesrMaterial DEPTH_EQUAL_OVERLAY = TesrMaterial.builder().additiveAlpha().depthEqual().noDepthWrite().build(); + + static final String COLOR_VERTEX = "angelica_out vec4 v_Color;\nvoid main() {\n gl_Position = angelica_clipPosition();\n v_Color = angelica_color();\n}\n"; + static final String BAKED_ONLY_VERTEX = "angelica_out vec4 v_Color;\nvoid main() {\n gl_Position = angelica_ProjectionMatrix * (angelica_ModelViewMatrix * vec4(angelica_Position, 1.0));\n v_Color = angelica_color();\n}\n"; + static final String COLOR_FRAGMENT = "angelica_in vec4 v_Color;\nvoid main() { angelica_FragColor = v_Color; }\n"; + + static TesrShaderPrograms.Entry linkedSource(String name) { + final ResourceLocation unread = new ResourceLocation("angelica", "test/unread"); + final TesrShaderPrograms.Entry entry = TesrShaderPrograms.entry(TesrShaders.register(name, unread, unread)); + TesrShaderPrograms.link(entry, false, COLOR_VERTEX, COLOR_FRAGMENT); + TesrShaderPrograms.link(entry, true, COLOR_VERTEX, COLOR_FRAGMENT); + return entry; + } + static RenderLayer testLayer() { - return RenderLayer.tesr(null, TesrMaterial.CURRENT_STATE, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, true); + return layerFor(TesrMaterial.CURRENT_STATE); + } + + static RenderLayer layerFor(TesrMaterial material) { + return RenderLayer.tesr(null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.DISABLED, false, true); + } + + static Reference2ObjectOpenHashMap> byLayer(RetainedTesrGroups groups) { + return Reflect.get(groups, "byLayer"); + } + + static ObjectArrayList groupsOn(RetainedTesrGroups groups, RenderLayer layer) { + return byLayer(groups).get(layer); } static TemplateBuffer template() { @@ -48,21 +77,17 @@ final class RetainedTesrTestSupport { } static RetainedTesrGroups.Group onlyGroup(RetainedTesrGroups groups) { - final Reference2ObjectOpenHashMap> byLayer = Reflect.get(groups, "byLayer"); + final Reference2ObjectOpenHashMap> byLayer = byLayer(groups); assertEquals(1, byLayer.size()); final ObjectArrayList list = byLayer.values().iterator().next(); assertEquals(1, list.size()); return list.get(0); } - static int liveCull(TesrMaterial material) { - return material.isNoCull() ? DrawState.DISABLED : DrawState.liveCull(); - } - static RenderLayer capturedLayer(TesrMaterial material) { return Reflect.invoke(ModelPartBatcher.INSTANCE, "layerFor", new Class[] { ResourceLocation.class, TesrMaterial.class, PassOverride.class, float.class, float.class, int.class, int.class, boolean.class, boolean.class }, - null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap()); + null, material, PassOverride.NONE, 0f, 0f, ShaderGlint.NO_TINT, DrawState.liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap()); } static final class CountingPipeline implements TesrInstancingPipeline { diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/StateSetOverrideGLTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/StateSetOverrideGLTest.java index 2115608c7..5d3ba9251 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/StateSetOverrideGLTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/StateSetOverrideGLTest.java @@ -94,16 +94,25 @@ class StateSetOverrideGLTest { AlphaTestStorage.overrideAlphaTest(new AlphaTest(AlphaTestFunction.LEQUAL, 0.25f)); } - @Test - void blendBracketUnderOverrideRestoresVanillaShadowAndLeavesLiveStateUntouched() { + static Stream blendFuncSetters() { + return Stream.of( + Arguments.of("glBlendFunc", (Runnable) () -> GLStateManager.glBlendFunc(GL11.GL_DST_COLOR, GL11.GL_ZERO)), + Arguments.of("tryBlendFuncSeparate", (Runnable) () -> GLStateManager.tryBlendFuncSeparate(GL11.GL_DST_COLOR, GL11.GL_ZERO, GL11.GL_DST_COLOR, GL11.GL_ZERO))); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("blendFuncSetters") + void blendBracketUnderOverrideRestoresVanillaShadowAndLeavesLiveStateUntouched(String name, Runnable setBlendFunc) { overrideVanillaBlend(); final boolean liveEnabledBefore = GLStateManager.getBlendMode().isEnabled(); final BlendState liveBefore = new BlendState().set(GLStateManager.getBlendState()); final int d = GLStateManager.pushState(StateSet.BLEND); + final boolean vanillaEnabledBefore = BlendModeStorage.ENABLE_LAYER.getVanilla(); GLStateManager.disableBlend(); - GLStateManager.tryBlendFuncSeparate(GL11.GL_DST_COLOR, GL11.GL_ZERO, GL11.GL_DST_COLOR, GL11.GL_ZERO); + assertNotEquals(vanillaEnabledBefore, BlendModeStorage.ENABLE_LAYER.getVanilla(), "the defer branch changes the vanilla shadow immediately"); + setBlendFunc.run(); GLStateManager.popStateTo(d); assertTrue(BlendModeStorage.ENABLE_LAYER.getVanilla(), "vanilla shadow enable restored to the pre-bracket value"); @@ -132,7 +141,18 @@ class StateSetOverrideGLTest { final int d = GLStateManager.pushState(StateSet.ALPHA); GLStateManager.disableAlphaTest(); GLStateManager.glAlphaFunc(GL11.GL_LESS, 0.1f); + final int[] calls = { 0 }; + final AlphaState shadow = new AlphaState(); + listener = () -> { + calls[0]++; + assertEquals(GL11.GL_LEQUAL, GLStateManager.getAlphaState().getFunction()); + assertEquals(0.25f, GLStateManager.getAlphaState().getReference()); + assertTrue(GLStateManager.isEffectiveAlphaTestEnabled()); + assertEquals(GL11.GL_GREATER, GLStateManager.getEffectiveAlphaState(shadow).getFunction()); + assertEquals(0.5f, shadow.getReference()); + }; GLStateManager.popStateTo(d); + assertEquals(1, calls[0]); assertTrue(AlphaTestStorage.ENABLE_LAYER.getVanilla(), "vanilla shadow enable restored to the pre-bracket value"); final AlphaState vanillaAfter = new AlphaState(); @@ -158,7 +178,7 @@ class StateSetOverrideGLTest { final ColorMask liveColorBefore = new ColorMask().set(GLStateManager.getColorMask()); final int d = GLStateManager.pushState(StateSet.MASKS); - GLStateManager.glDepthMask(true); + GLStateManager.glDepthMask(false); GLStateManager.glColorMask(true, true, true, true); GLStateManager.popStateTo(d); @@ -241,118 +261,6 @@ class StateSetOverrideGLTest { assertEquals(GL11.GL_GREATER, GLStateManager.getAlphaState().getFunction(), "once released, the bracket pop restores the live cache to the vanilla baseline captured at push time through the vanilla-layer-aware slot, proving the pop actually acted rather than the assertion coincidentally already holding"); } - @Test - void glDepthMaskDeferBranchInsideBracketRestoredAtPop() { - VanillaLayerRig.installDepthColor(); - - GLStateManager.glDepthMask(true); - DepthColorStorage.disableDepthColor(); - - final int d = GLStateManager.pushState(StateSet.MASKS); - final boolean liveBefore = GLStateManager.getDepthState().isEnabled(); - - GLStateManager.glDepthMask(false); - - GLStateManager.popStateTo(d); - - assertEquals(liveBefore, GLStateManager.getDepthState().isEnabled(), "GLSM's own depth-mask cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was"); - final DepthState vanillaAfter = new DepthState(); - DepthColorStorage.DEPTH_LAYER.readVanilla(vanillaAfter); - assertTrue(vanillaAfter.isEnabled(), "the bracket pop reverts the vanilla depth-mask shadow to the pre-bracket baseline, undoing the defer branch's write"); - } - - @Test - void glColorMaskDeferBranchInsideBracketRestoredAtPop() { - VanillaLayerRig.installDepthColor(); - - GLStateManager.glColorMask(true, true, true, true); - DepthColorStorage.disableDepthColor(); - - final int d = GLStateManager.pushState(StateSet.MASKS); - final ColorMask liveBefore = new ColorMask().set(GLStateManager.getColorMask()); - - GLStateManager.glColorMask(false, false, true, true); - - GLStateManager.popStateTo(d); - - assertEquals(liveBefore.red, GLStateManager.getColorMask().red, "GLSM's own color-mask cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was"); - assertEquals(liveBefore.green, GLStateManager.getColorMask().green, "GLSM's own color-mask cache green channel is untouched by the defer branch"); - final ColorMask vanillaAfter = new ColorMask(); - DepthColorStorage.COLOR_LAYER.readVanilla(vanillaAfter); - assertTrue(vanillaAfter.red && vanillaAfter.green, "the bracket pop reverts the vanilla color-mask shadow to the pre-bracket baseline, undoing the defer branch's write"); - } - - @Test - void glAlphaFuncDeferBranchInsideBracketRestoredAtPop() { - overrideVanillaAlpha(); - - final int d = GLStateManager.pushState(StateSet.ALPHA); - final int liveFuncBefore = GLStateManager.getAlphaState().getFunction(); - - GLStateManager.glAlphaFunc(GL11.GL_LESS, 0.1f); - - GLStateManager.popStateTo(d); - - assertEquals(liveFuncBefore, GLStateManager.getAlphaState().getFunction(), "GLSM's own alpha-func cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was"); - final AlphaState vanillaAfter = new AlphaState(); - AlphaTestStorage.FUNC_LAYER.readVanilla(vanillaAfter); - assertEquals(GL11.GL_GREATER, vanillaAfter.getFunction(), "the bracket pop reverts the vanilla alpha-func shadow to the pre-bracket baseline, undoing the defer branch's write"); - assertEquals(0.5f, vanillaAfter.getReference(), 0.0001f, "the bracket pop reverts the vanilla alpha-ref shadow to the pre-bracket baseline"); - } - - static Stream blendFuncSetters() { - return Stream.of( - Arguments.of("glBlendFunc", (Runnable) () -> GLStateManager.glBlendFunc(GL11.GL_DST_COLOR, GL11.GL_ZERO)), - Arguments.of("tryBlendFuncSeparate", (Runnable) () -> GLStateManager.tryBlendFuncSeparate(GL11.GL_DST_COLOR, GL11.GL_ZERO, GL11.GL_DST_COLOR, GL11.GL_ZERO))); - } - - @ParameterizedTest(name = "{0}") - @MethodSource("blendFuncSetters") - void blendFuncDeferBranchInsideBracketRestoredAtPop(String name, Runnable setBlendFunc) { - overrideVanillaBlend(); - - final int d = GLStateManager.pushState(StateSet.BLEND); - final int liveSrcBefore = GLStateManager.getBlendState().getSrcRgb(); - - setBlendFunc.run(); - - GLStateManager.popStateTo(d); - - assertEquals(liveSrcBefore, GLStateManager.getBlendState().getSrcRgb(), "GLSM's own blend-func cache is untouched by the defer branch, so the bracket pop leaves it exactly as it was"); - final BlendState vanillaAfter = new BlendState(); - BlendModeStorage.FUNC_LAYER.readVanilla(vanillaAfter); - assertEquals(GL11.GL_SRC_ALPHA, vanillaAfter.getSrcRgb(), "the bracket pop reverts the vanilla blend-func shadow to the pre-bracket baseline, undoing the defer branch's write"); - } - - @Test - void enableBlendDeferBranchInsideBracketRestoredAtPop() { - overrideVanillaBlend(); - - final int d = GLStateManager.pushState(StateSet.BLEND); - final boolean vanillaBefore = BlendModeStorage.ENABLE_LAYER.getVanilla(); - - GLStateManager.disableBlend(); - assertNotEquals(vanillaBefore, BlendModeStorage.ENABLE_LAYER.getVanilla(), "the defer branch changes the vanilla shadow immediately"); - - GLStateManager.popStateTo(d); - - assertEquals(vanillaBefore, BlendModeStorage.ENABLE_LAYER.getVanilla(), "the boolean defer branch is tracked by beforeModify, so the bracket pop reverts the vanilla shadow to the pre-bracket baseline"); - } - - @Test - void enableAlphaTestDeferBranchInsideBracketRestoredAtPop() { - overrideVanillaAlpha(); - - final int d = GLStateManager.pushState(StateSet.ALPHA); - final boolean vanillaBefore = AlphaTestStorage.ENABLE_LAYER.getVanilla(); - - GLStateManager.disableAlphaTest(); - - GLStateManager.popStateTo(d); - - assertEquals(vanillaBefore, AlphaTestStorage.ENABLE_LAYER.getVanilla(), "the boolean defer branch is tracked by beforeModify, so the bracket pop reverts the vanilla shadow to the pre-bracket baseline"); - } - @Test void nonMemberBracketUnderOverrideCarriesToParent() { overrideVanillaBlend(); @@ -418,33 +326,6 @@ class StateSetOverrideGLTest { assertEquals(1, calls[0]); } - @Test - void lockedOverrideRestoresEffectiveStateBeforeNotification() { - GLStateManager.enableAlphaTest(); - GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.5f); - VanillaLayerRig.installAlpha(); - AlphaTestStorage.overrideAlphaTest(new AlphaTest(AlphaTestFunction.LEQUAL, 0.75f)); - final int depth = GLStateManager.pushState(StateSet.ALPHA); - GLStateManager.getAlphaState().beforeModify(); - final AlphaState shadow = new AlphaState(); - shadow.setFunction(GL11.GL_LESS); - shadow.setReference(0.25f); - AlphaTestStorage.FUNC_LAYER.writeVanilla(shadow); - GLStateManager.getAlphaTest().beforeModify(); - AlphaTestStorage.ENABLE_LAYER.setVanilla(false); - final int[] calls = { 0 }; - listener = () -> { - calls[0]++; - assertEquals(GL11.GL_LEQUAL, GLStateManager.getAlphaState().getFunction()); - assertEquals(0.75f, GLStateManager.getAlphaState().getReference()); - assertTrue(GLStateManager.isEffectiveAlphaTestEnabled()); - assertEquals(GL11.GL_GREATER, GLStateManager.getEffectiveAlphaState(shadow).getFunction()); - assertEquals(0.5f, shadow.getReference()); - }; - GLStateManager.popStateTo(depth); - assertEquals(1, calls[0]); - } - @Test void dynamicUniformsUpdateWithoutRebindingAndAfterProgramRestoration() { GLStateManager.enableAlphaTest(); diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMathTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMathTest.java index 82ebe2c5b..88069a3c7 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMathTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrAnchorMathTest.java @@ -2,6 +2,11 @@ package com.gtnewhorizons.angelica.rendering.tesr; import org.joml.Matrix4f; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -22,14 +27,6 @@ class TesrAnchorMathTest { return m; } - @Test - void anchorCoordRoundsDownToGrid() { - assertEquals(96, TesrAnchorMath.anchorCoord(100.5)); - assertEquals(112, TesrAnchorMath.anchorCoord(112.0)); - assertEquals(-16, TesrAnchorMath.anchorCoord(-0.5)); - assertEquals(-32, TesrAnchorMath.anchorCoord(-16.5)); - } - @Test void shouldReanchorTriggersOnlyBeyondThreshold() { assertFalse(TesrAnchorMath.shouldReanchor(100, 0, 0, 0, 0, 0)); @@ -48,33 +45,37 @@ class TesrAnchorMathTest { assertEquals(TE_X - ax, mA.m30(), 1e-2f); assertEquals(TE_Y - ay, mA.m31(), 1e-2f); assertEquals(TE_Z - az, mA.m32(), 1e-2f); + + final Matrix4f moved = anchorRelative(camX + 137.37, camY + 41.03, camZ - 250.91, ax, ay, az); + assertEquals(TE_X - ax, moved.m30(), 1e-2f); + assertEquals(TE_Y - ay, moved.m31(), 1e-2f); + assertEquals(TE_Z - az, moved.m32(), 1e-2f); } - @Test - void hashIsInvariantUnderCameraMotion() { - final double camAX = TE_X - 20.0, camAY = TE_Y - 3.0, camAZ = TE_Z + 8.0; - final long ax = TesrAnchorMath.anchorCoord(camAX); - final long ay = TesrAnchorMath.anchorCoord(camAY); - final long az = TesrAnchorMath.anchorCoord(camAZ); - - final Matrix4f mA = anchorRelative(camAX, camAY, camAZ, ax, ay, az); - final long h1 = TesrAnchorMath.instanceHash(42, mA, 0x00F000F0, -1); - - final double camBX = camAX + 137.37, camBY = camAY + 41.03, camBZ = camAZ - 250.91; - final Matrix4f mB = anchorRelative(camBX, camBY, camBZ, ax, ay, az); - final long h2 = TesrAnchorMath.instanceHash(42, mB, 0x00F000F0, -1); - - assertEquals(h1, h2, "camera-only motion must not change the rebuild hash"); + static Stream texMatrices() { + final Matrix4f scroll = new Matrix4f().m00(0.9f).m01(0.1f).m10(-0.2f).m11(0.8f).m30(0.25f).m31(0.5f); + final Matrix4f zeros = new Matrix4f().zero(); + return Stream.of( + Arguments.of("absent vs absent", null, null, true), + Arguments.of("absent vs zeros", null, zeros, false), + Arguments.of("scroll vs scroll", scroll, scroll, true), + Arguments.of("scroll vs absent", scroll, null, false), + Arguments.of("m00 off", scroll, new Matrix4f(scroll).m00(0.91f), false), + Arguments.of("m01 off", scroll, new Matrix4f(scroll).m01(0.11f), false), + Arguments.of("m10 off", scroll, new Matrix4f(scroll).m10(-0.21f), false), + Arguments.of("m11 off", scroll, new Matrix4f(scroll).m11(0.81f), false), + Arguments.of("m30 off", scroll, new Matrix4f(scroll).m30(0.26f), false), + Arguments.of("m31 off", scroll, new Matrix4f(scroll).m31(0.51f), false), + Arguments.of("zeros vs zeros", zeros, zeros, true), + Arguments.of("zeros vs absent", zeros, null, false)); } - @Test - void reanchorRoundTripKeepsWorldPosition() { - final double camX = TE_X - 20.0, camY = TE_Y, camZ = TE_Z + 20.0; - final long ax1 = TesrAnchorMath.anchorCoord(camX); - final long ax2 = ax1 + 480; - final Matrix4f m1 = anchorRelative(camX, camY, camZ, ax1, 0, 0); - final Matrix4f m2 = anchorRelative(camX, camY, camZ, ax2, 0, 0); - assertEquals(ax1 + m1.m30(), ax2 + m2.m30(), 1e-2); + @ParameterizedTest(name = "{0}") + @MethodSource("texMatrices") + void texMatrixCompareSeparatesAbsentFromPresent(String name, Matrix4f stored, Matrix4f query, boolean same) { + final float[] terms = new float[2 * TesrAnchorMath.TEX_MATRIX_TERMS]; + TesrAnchorMath.storeTexMatrix(terms, TesrAnchorMath.TEX_MATRIX_TERMS, stored); + assertEquals(same, TesrAnchorMath.sameTexMatrix(terms, TesrAnchorMath.TEX_MATRIX_TERMS, query)); } @Test @@ -86,5 +87,6 @@ class TesrAnchorMathTest { assertNotEquals(base, TesrAnchorMath.instanceHash(42, mA, 0x00F000F0, 0xFF0000FF), "color"); final Matrix4f moved = new Matrix4f(mA).translate(1f, 0f, 0f); assertNotEquals(base, TesrAnchorMath.instanceHash(42, moved, 0x00F000F0, -1), "transform"); + assertNotEquals(TesrAnchorMath.texMatrixHash(null), TesrAnchorMath.texMatrixHash(new Matrix4f().translation(0.5f, 0f, 0f)), "tex matrix"); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRendererScopeTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRendererScopeTest.java new file mode 100644 index 000000000..9b1f17eed --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrBatchRendererScopeTest.java @@ -0,0 +1,33 @@ +package com.gtnewhorizons.angelica.rendering.tesr; + +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class TesrBatchRendererScopeTest { + + private final TesrBatchRenderer renderer = TesrBatchRenderer.INSTANCE; + + private long current() { + return Reflect.get(renderer, "currentScope"); + } + + @Test + void nestedBeginKeepsOuterIdOutermostEndReturnsToZeroAndIdsAreUnique() { + renderer.beginScope(); + final long outer = current(); + assertNotEquals(0L, outer); + renderer.beginScope(); + assertEquals(outer, current()); + renderer.endScope(); + assertEquals(outer, current()); + renderer.endScope(); + assertEquals(0L, current()); + renderer.beginScope(); + final long second = current(); + renderer.endScope(); + assertNotEquals(outer, second); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerKeyTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerKeyTest.java index d4c9e5b98..3782378f7 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerKeyTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerKeyTest.java @@ -24,13 +24,13 @@ class TesrLayerKeyTest { @Test void scratchProbeMatchesCopiedKey() { - final TesrBatchRenderer.LayerKey scratch = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); + final TesrBatchRenderer.LayerKey scratch = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.TRANSLUCENT, true, false, true, false, 0.1f, true, TesrMaterial.SpecialRender.GLINT, SHADER, false, EYES, 1.0f, 2.0f, DrawState.CULL_FRONT, true, false); final TesrBatchRenderer.LayerKey stored = scratch.copy(); assertNotSame(scratch, stored); assertEquals(scratch, stored); assertEquals(scratch.hashCode(), stored.hashCode()); - scratch.set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0.1f, false, NONE, SHADER, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); + scratch.set(TEX, TesrMaterial.Transparency.TRANSLUCENT, true, false, true, false, 0.1f, true, TesrMaterial.SpecialRender.GLINT, SHADER, false, EYES, 1.0f, 2.0f, DrawState.CULL_FRONT, true, false); assertEquals(stored, scratch, "re-set scratch still matches stored copy"); } @@ -60,50 +60,30 @@ class TesrLayerKeyTest { other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, false); assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, true, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); + assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, true, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); + assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, true, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); + assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, true, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); + assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, EYES, 0.0f, 0.0f, DrawState.DISABLED, false, true); + assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, TRANSLUCENT, 0.0f, 0.0f, DrawState.DISABLED, false, true); + assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, -1.0f, -1.0f, DrawState.DISABLED, false, true); + assertNotEquals(base, other); + other.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); assertEquals(base, other); } - @Test - void passSeparatesOtherwiseIdenticalKeys() { - final TesrBatchRenderer.LayerKey plain = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); - final TesrBatchRenderer.LayerKey eyes = plain.copy(); - eyes.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, EYES, 0.0f, 0.0f, DrawState.DISABLED, false, true); - assertNotEquals(plain, eyes); - - final TesrBatchRenderer.LayerKey translucent = plain.copy(); - translucent.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, TRANSLUCENT, 0.0f, 0.0f, DrawState.DISABLED, false, true); - assertNotEquals(plain, translucent); - assertNotEquals(eyes, translucent); - - assertEquals(eyes, eyes.copy()); - assertEquals(eyes.hashCode(), eyes.copy().hashCode()); - - // Polygon offset is the other thing a batch outlives -- the coplanar eye overlay needs it at emit time. - final TesrBatchRenderer.LayerKey offset = plain.copy(); - offset.set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, NO_PASS, -1.0f, -1.0f, DrawState.DISABLED, false, true); - assertNotEquals(plain, offset); - assertEquals(offset, offset.copy()); - assertEquals(offset.hashCode(), offset.copy().hashCode()); - } - - @Test - void nullTextureAndShaderHash() { - final TesrBatchRenderer.LayerKey key = new TesrBatchRenderer.LayerKey().set(null, null, false, false, false, false, 0f, false, null, null, false, NO_PASS, 0.0f, 0.0f, DrawState.DISABLED, false, true); - assertEquals(key, key.copy()); - assertEquals(key.hashCode(), key.copy().hashCode()); - } - @Test void passOverridesAreInterned() { assertSame(EYES, PassOverride.of(SpecialCondition.ENTITY_EYES, null, null)); assertSame(TRANSLUCENT, PassOverride.of(null, Boolean.TRUE, null)); assertSame(PassOverride.NONE, PassOverride.of(null, null, null)); assertNotSame(PassOverride.of(null, Boolean.TRUE, null), PassOverride.of(null, Boolean.FALSE, null)); - - final TesrBatchRenderer.LayerKey a = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, PassOverride.of(SpecialCondition.GLINT, Boolean.FALSE, null), 0.0f, 0.0f, DrawState.DISABLED, false, true); - final TesrBatchRenderer.LayerKey b = new TesrBatchRenderer.LayerKey().set(TEX, TesrMaterial.Transparency.OPAQUE, false, false, false, false, 0f, false, NONE, null, false, PassOverride.of(SpecialCondition.GLINT, Boolean.FALSE, null), 0.0f, 0.0f, DrawState.DISABLED, false, true); - assertEquals(a, b); - assertEquals(a.hashCode(), b.hashCode()); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerStateTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerStateTest.java index 36004816c..cc0b88256 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerStateTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrLayerStateTest.java @@ -15,28 +15,35 @@ import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache; import com.gtnewhorizons.angelica.shadercompat.ShaderGlint; import net.coderbot.batchedentityrendering.impl.AngelicaBufferSource; import net.coderbot.batchedentityrendering.impl.TransparencyType; +import net.coderbot.iris.Iris; import net.coderbot.iris.gl.blending.AlphaTestStorage; import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition; import net.coderbot.iris.gl.blending.DepthColorStorage; import net.coderbot.iris.layer.GbufferPrograms; import net.coderbot.iris.layer.PassOverride; +import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline; +import net.coderbot.iris.pipeline.PipelineManager; import net.coderbot.iris.pipeline.WorldRenderingPhase; +import net.coderbot.iris.pipeline.WorldRenderingPipeline; import net.minecraft.util.ResourceLocation; import org.joml.Matrix4f; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL30; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; -import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.capturedLayer; -import static com.gtnewhorizons.angelica.rendering.tesr.RetainedTesrTestSupport.liveCull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; @@ -44,6 +51,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @GLCoreTest class TesrLayerStateTest { @@ -69,6 +78,7 @@ class TesrLayerStateTest { VanillaLayerRig.clear(); ModelPartBatcher.INSTANCE.clear(); + TesrShaderPrograms.invalidateAll(); GLStateManager.glActiveTexture(GL13.GL_TEXTURE1); GLStateManager.glMatrixMode(GL11.GL_TEXTURE); GLStateManager.glLoadIdentity(); @@ -118,33 +128,6 @@ class TesrLayerStateTest { assertEquals(function, GL11.glGetInteger(GL11.GL_DEPTH_FUNC), "driver depth function"); } - @Test - void entityBatchRestoresDepthBeforeBlockEntities() { - final AngelicaBufferSource source = new AngelicaBufferSource(); - final RenderLayer entityLayer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT); - final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() { - @Override - public boolean hasDraws(RenderLayer layer) { - return layer == entityLayer; - } - - @Override - public void drawLayer(RenderLayer layer) { - assertTrue(GL11.glIsEnabled(GL11.GL_DEPTH_TEST), "entity draw depth test"); - } - }; - try { - source.declareUse(entityLayer); - source.endBatchWithType(TransparencyType.OPAQUE, hook); - source.endBatch(hook); - assertTrue(GLStateManager.getDepthTest().isEnabled(), "cached depth before block entities"); - assertTrue(GL11.glIsEnabled(GL11.GL_DEPTH_TEST), "driver depth before block entities"); - } finally { - source.discard(); - source.freeBuffers(); - } - } - @Test void afterGlintLayerDrawsAfterEveryGlintLayer() { final AngelicaBufferSource source = new AngelicaBufferSource(); @@ -178,11 +161,82 @@ class TesrLayerStateTest { } } - @Test - void batchDrawDoesNotInheritTheInterruptedRenderersTextureMatrixOrIrisOverride() { + record BatchScenario(Runnable during, Runnable after, Runnable cleanup) {} + + static Stream batchDrawCases() { + return Stream.of( + Arguments.of("texture matrix and iris override", (Supplier) () -> { + GLStateManager.setTextureMatrix(0, new Matrix4f().translation(0.25f, 0f, 0f)); + GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.GLINT); + GbufferPrograms.pushOverridePhase(WorldRenderingPhase.TERRAIN_CUTOUT); + return new BatchScenario( + () -> { + assertTrue(MatrixHelper.isIdentity(GLStateManager.getTextures().getTextureUnitMatrix(0)), "unit 0 texture matrix during batch draw"); + assertNull(GbufferPrograms.getSpecialCondition(), "special condition during batch draw"); + assertNull(GbufferPrograms.getOverridePhase(), "override phase during batch draw"); + }, + () -> { + assertEquals(0.25f, GLStateManager.getTextures().getTextureUnitMatrix(0).m30(), "caller texture matrix restored"); + assertEquals(SpecialCondition.GLINT, GbufferPrograms.getSpecialCondition(), "caller special condition restored"); + assertEquals(WorldRenderingPhase.TERRAIN_CUTOUT, GbufferPrograms.getOverridePhase(), "caller override phase restored"); + }, + () -> { + GbufferPrograms.popOverridePhase(); + GbufferPrograms.teardownSpecialRenderCondition(); + GLStateManager.setTextureMatrix(0, new Matrix4f()); + }); + }), + Arguments.of("foreign program and framebuffer", (Supplier) () -> { + final int program = GlTestSupport.program(GlTestSupport.TRIVIAL_VS, GlTestSupport.TRIVIAL_FS); + final int framebuffer = GLStateManager.glGenFramebuffers(); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + BatchDrawDefaults.capturePassFramebuffer(); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer); + GLStateManager.glUseProgram(program); + return new BatchScenario( + () -> { + assertEquals(0, GLStateManager.getActiveProgram(), "program during batch draw"); + assertEquals(0, GLStateManager.getDrawFramebuffer(), "framebuffer during batch draw"); + }, + () -> { + assertEquals(program, GLStateManager.getActiveProgram(), "caller program restored"); + assertEquals(framebuffer, GLStateManager.getDrawFramebuffer(), "caller framebuffer restored"); + }, + () -> { + GLStateManager.glUseProgram(0); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + GLStateManager.glDeleteFramebuffers(framebuffer); + GLStateManager.glDeleteProgram(program); + }); + }), + Arguments.of("filter from before an item renderer's blur window", (Supplier) () -> { + final int texture = GLStateManager.glGenTextures(); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + // What TextureUtil.func_152777_a(false, false, 1) does around an item render. + TextureUtilHooks.captureBoundFilterTexture(); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + return new BatchScenario( + () -> assertEquals(GL11.GL_LINEAR, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "filter from before the window"), + () -> assertEquals(GL11.GL_NEAREST, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "window filter restored"), + () -> { + TextureUtilHooks.clearSavedFilterTexture(); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + GLStateManager.glDeleteTextures(texture); + }); + })); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("batchDrawCases") + void batchDrawDoesNotInheritTheInterruptedRenderersState(String name, Supplier setup) { final AngelicaBufferSource source = new AngelicaBufferSource(); final RenderLayer layer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT); final int[] draws = { 0 }; + final BatchScenario[] scenario = { null }; final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() { @Override public boolean hasDraws(RenderLayer candidate) { @@ -192,25 +246,17 @@ class TesrLayerStateTest { @Override public void drawLayer(RenderLayer drawn) { draws[0]++; - assertTrue(MatrixHelper.isIdentity(GLStateManager.getTextures().getTextureUnitMatrix(0)), "unit 0 texture matrix during batch draw"); - assertNull(GbufferPrograms.getSpecialCondition(), "special condition during batch draw"); - assertNull(GbufferPrograms.getOverridePhase(), "override phase during batch draw"); + scenario[0].during().run(); } }; - GLStateManager.setTextureMatrix(0, new Matrix4f().translation(0.25f, 0f, 0f)); - GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.GLINT); - GbufferPrograms.pushOverridePhase(WorldRenderingPhase.TERRAIN_CUTOUT); try { + scenario[0] = setup.get(); source.declareUse(layer); source.endBatch(hook); assertEquals(1, draws[0]); - assertEquals(0.25f, GLStateManager.getTextures().getTextureUnitMatrix(0).m30(), "caller texture matrix restored"); - assertEquals(SpecialCondition.GLINT, GbufferPrograms.getSpecialCondition(), "caller special condition restored"); - assertEquals(WorldRenderingPhase.TERRAIN_CUTOUT, GbufferPrograms.getOverridePhase(), "caller override phase restored"); + scenario[0].after().run(); } finally { - GbufferPrograms.popOverridePhase(); - GbufferPrograms.teardownSpecialRenderCondition(); - GLStateManager.setTextureMatrix(0, new Matrix4f()); + if (scenario[0] != null) scenario[0].cleanup().run(); source.discard(); source.freeBuffers(); } @@ -238,86 +284,6 @@ class TesrLayerStateTest { } } - @Test - void batchDrawSwapsOutAForeignProgramAndFramebuffer() { - final int program = GlTestSupport.program(GlTestSupport.TRIVIAL_VS, GlTestSupport.TRIVIAL_FS); - final int framebuffer = GLStateManager.glGenFramebuffers(); - final AngelicaBufferSource source = new AngelicaBufferSource(); - final RenderLayer layer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT); - final int[] draws = { 0 }; - final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() { - @Override - public boolean hasDraws(RenderLayer candidate) { - return candidate == layer; - } - - @Override - public void drawLayer(RenderLayer drawn) { - draws[0]++; - assertEquals(0, GLStateManager.getActiveProgram(), "program during batch draw"); - assertEquals(0, GLStateManager.getDrawFramebuffer(), "framebuffer during batch draw"); - } - }; - try { - GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); - BatchDrawDefaults.capturePassFramebuffer(); - GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer); - GLStateManager.glUseProgram(program); - source.declareUse(layer); - source.endBatch(hook); - assertEquals(1, draws[0]); - assertEquals(program, GLStateManager.getActiveProgram(), "caller program restored"); - assertEquals(framebuffer, GLStateManager.getDrawFramebuffer(), "caller framebuffer restored"); - } finally { - GLStateManager.glUseProgram(0); - GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); - GLStateManager.glDeleteFramebuffers(framebuffer); - GLStateManager.glDeleteProgram(program); - source.discard(); - source.freeBuffers(); - } - } - - @Test - void batchDrawUsesTheFilterFromBeforeAnItemRenderersBlurWindow() { - final int texture = GLStateManager.glGenTextures(); - final AngelicaBufferSource source = new AngelicaBufferSource(); - final RenderLayer layer = layer(EntityMaterials.DROPPED_ITEM_CUTOUT); - final int[] draws = { 0 }; - final AngelicaBufferSource.LayerDrawHook hook = new AngelicaBufferSource.LayerDrawHook() { - @Override - public boolean hasDraws(RenderLayer candidate) { - return candidate == layer; - } - - @Override - public void drawLayer(RenderLayer drawn) { - draws[0]++; - assertEquals(GL11.GL_LINEAR, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "filter from before the window"); - } - }; - try { - GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); - GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); - GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); - // What TextureUtil.func_152777_a(false, false, 1) does around an item render. - TextureUtilHooks.captureBoundFilterTexture(); - GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); - GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); - source.declareUse(layer); - source.endBatch(hook); - assertEquals(1, draws[0]); - assertEquals(GL11.GL_NEAREST, TextureInfoCache.INSTANCE.getInfo(texture).getMinFilter(), "window filter restored"); - } finally { - TextureUtilHooks.clearSavedFilterTexture(); - GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); - GLStateManager.glDeleteTextures(texture); - source.discard(); - source.freeBuffers(); - } - } - private static RenderLayer glint(int slot) { return RenderLayer.tesr(null, EntityMaterials.GLINT, PassOverride.NONE, 0f, 0f, slot, DrawState.CULL_BACK, true, true); } @@ -341,10 +307,50 @@ class TesrLayerStateTest { assertSame(teardown, thrown.getSuppressed()[0]); } + @Test + void shaderLayerDefersToThePassHookWhileAPackIsActive() { + final List calls = new ArrayList<>(); + final TesrMaterial material = TesrMaterial.builder() + .shader(TesrShaders.register("angelica:test_pack_defers", () -> calls.add("bind"), () -> calls.add("release"))) + .build(); + final PassOverride pass = PassOverride.of(SpecialCondition.BEACON_BEAM, null, null); + final RenderLayer layer = RenderLayer.tesr(null, material, pass, 0f, 0f, ShaderGlint.NO_TINT, DrawState.CULL_BACK, true, true); + withPack(true, drawing -> { + layer.startDrawing(); + assertSame(SpecialCondition.BEACON_BEAM, GbufferPrograms.getSpecialCondition(), "the pass hook must run in the shader's place"); + drawing[0] = false; + layer.endDrawing(); + assertNull(GbufferPrograms.getSpecialCondition(), "end must undo what begin ran, not re-read the pack state"); + assertTrue(calls.isEmpty(), "a pack draws the material; the mod program is never bound: " + calls); + + layer.startDrawing(); + drawing[0] = true; + layer.endDrawing(); + assertEquals(List.of("bind", "release"), calls, "the same interned layer binds the shader once the pack stops drawing the world"); + }); + } + + private static void withPack(boolean initiallyDrawing, Consumer body) { + final PipelineManager manager = Iris.getPipelineManager(); + final WorldRenderingPipeline previous = manager.getPipelineNullable(); + final boolean enabled = Iris.enabled; + final boolean[] drawing = { initiallyDrawing }; + final DeferredWorldRenderingPipeline pipeline = mock(DeferredWorldRenderingPipeline.class); + when(pipeline.shouldOverrideShaders()).thenAnswer(inv -> drawing[0]); + Reflect.set(manager, "pipeline", pipeline); + Reflect.setStaticFinal(Iris.class, "enabled", boolean.class, true); + try { + body.accept(drawing); + } finally { + Reflect.setStaticFinal(Iris.class, "enabled", boolean.class, enabled); + Reflect.set(manager, "pipeline", previous); + } + } + private static RenderLayer queuedLayer(TesrMaterial material) { return Reflect.invoke(TesrBatchRenderer.INSTANCE, "layerFor", new Class[] { ResourceLocation.class, TesrMaterial.class, PassOverride.class, float.class, float.class, int.class, boolean.class, boolean.class }, - null, material, PassOverride.NONE, 0f, 0f, liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap()); + null, material, PassOverride.NONE, 0f, 0f, DrawState.liveCull(material), DrawState.liveLit(material), DrawState.liveLightmap()); } private static void drawInBatch(RenderLayer layer, boolean culled) { @@ -355,22 +361,31 @@ class TesrLayerStateTest { GLStateManager.popStateTo(depth); } - @Test - void queuedCullingSplitsBothLayerCachesAndRestoresTheDrawCaller() { - for (TesrMaterial material : new TesrMaterial[] { EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.GLINT }) { + static Stream layerCaches() { + return Stream.of( + Arguments.of("ModelPartBatcher", (Function) RetainedTesrTestSupport::capturedLayer, + new TesrMaterial[] { EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.GLINT }), + Arguments.of("TesrBatchRenderer", (Function) TesrLayerStateTest::queuedLayer, + new TesrMaterial[] { TesrMaterial.CURRENT_STATE })); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("layerCaches") + void queuedCullingSplitsLayerCachesAndRestoresTheDrawCaller(String name, Function layerFor, TesrMaterial[] materials) { + for (TesrMaterial material : materials) { GLStateManager.disableCull(); - final RenderLayer twoSided = capturedLayer(material); + final RenderLayer twoSided = layerFor.apply(material); GLStateManager.enableCull(); - final RenderLayer culled = capturedLayer(material); + final RenderLayer culled = layerFor.apply(material); assertNotSame(twoSided, culled, "the last-layer shortcut must include culling"); GLStateManager.disableCull(); - assertSame(twoSided, capturedLayer(material), "the layer map must retain both states"); + assertSame(twoSided, layerFor.apply(material), "the layer map must retain both states"); } GLStateManager.disableCull(); - final RenderLayer twoSided = capturedLayer(EntityMaterials.DROPPED_ITEM_CUTOUT); + final RenderLayer twoSided = layerFor.apply(materials[0]); GLStateManager.enableCull(); - final RenderLayer culled = capturedLayer(EntityMaterials.DROPPED_ITEM_CUTOUT); + final RenderLayer culled = layerFor.apply(materials[0]); twoSided.startDrawing(); assertFalse(GL11.glIsEnabled(GL11.GL_CULL_FACE)); assertTrue(GL11.glIsEnabled(GL11.GL_DEPTH_TEST)); @@ -384,143 +399,108 @@ class TesrLayerStateTest { GLStateManager.disableCull(); drawInBatch(culled, true); assertFalse(GL11.glIsEnabled(GL11.GL_CULL_FACE)); - } - @Test - void queuedTemplateCullingSurvivesCacheHitsAndRestoresTheCaller() { - final TesrMaterial material = TesrMaterial.CURRENT_STATE; - GLStateManager.disableCull(); - final RenderLayer twoSided = queuedLayer(material); GLStateManager.enableCull(); - final RenderLayer culled = queuedLayer(material); - assertNotSame(twoSided, culled); - GLStateManager.disableCull(); - assertSame(twoSided, queuedLayer(material)); - drawInBatch(culled, true); - assertFalse(GL11.glIsEnabled(GL11.GL_CULL_FACE)); - GLStateManager.enableCull(); - drawInBatch(twoSided, false); - assertTrue(GL11.glIsEnabled(GL11.GL_CULL_FACE)); - - final RenderLayer forcedTwoSided = queuedLayer(TesrMaterial.builder().noCull().build()); - drawInBatch(forcedTwoSided, false); + drawInBatch(layerFor.apply(TesrMaterial.builder().noCull().build()), false); assertTrue(GL11.glIsEnabled(GL11.GL_CULL_FACE)); } - @Test - void glintTintSlotsSplitLayers() { - assertSame(glint(0), glint(0), "the same slot must reuse the layer"); - assertNotSame(glint(1), glint(0), "distinct slots must not share a layer"); - assertNotSame(glint(ShaderGlint.NO_TINT), glint(0), "an untinted glint must not share a tinted layer"); + static Stream axisCases() { + return Stream.of( + Arguments.of("current state over a dirty caller", (Runnable) () -> { + GLStateManager.disableCull(); + GLStateManager.glCullFace(GL11.GL_FRONT); + GLStateManager.disableLighting(); + GLStateManager.glColorMask(false, false, false, false); + }, TesrMaterial.CURRENT_STATE, (Runnable) () -> { + assertFalse(GLStateManager.getAlphaTest().isEnabled(), "no cutout"); + assertTrue(GLStateManager.getDepthTest().isEnabled(), "depth test"); + assertEquals(GL11.GL_LEQUAL, GLStateManager.getDepthState().getFunc()); + assertTrue(GLStateManager.getDepthState().isEnabled(), "depth write"); + assertFalse(GLStateManager.getBlendMode().isEnabled(), "opaque"); + assertTrue(GLStateManager.getCullState().isEnabled(), "cull pinned on"); + assertEquals(GL11.GL_BACK, GLStateManager.getPolygonState().getCullFaceMode(), "cull face pinned to back"); + assertTrue(GLStateManager.getLightingState().isEnabled(), "lit"); + assertTrue(GLStateManager.getColorMask().red, "color mask pinned on"); + assertFalse(GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL), "no polygon offset"); + }), + Arguments.of("every material toggle", (Runnable) () -> { }, + TesrMaterial.builder().noCull().unlit().noDepthWrite().cutout(0.5f).translucent().depthEqual().build(), (Runnable) () -> { + assertFalse(GLStateManager.getCullState().isEnabled(), "noCull"); + assertFalse(GLStateManager.getLightingState().isEnabled(), "unlit"); + assertFalse(GLStateManager.getDepthState().isEnabled(), "noDepthWrite"); + assertTrue(GLStateManager.getAlphaTest().isEnabled(), "cutout"); + assertEquals(0.5f, GLStateManager.getAlphaState().getReference(), "cutout threshold"); + assertTrue(GLStateManager.getBlendMode().isEnabled(), "translucent"); + assertEquals(GL11.GL_SRC_ALPHA, GLStateManager.getBlendState().getSrcRgb()); + assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, GLStateManager.getBlendState().getDstRgb()); + assertEquals(GL11.GL_EQUAL, GLStateManager.getDepthState().getFunc(), "depthEqual"); + })); } - @ParameterizedTest(name = "fog={0}") - @ValueSource(booleans = { false, true }) - void drawingLeavesUndeclaredPhasesToTheSurroundingContext(boolean fog) { - if (fog) GLStateManager.enableFog(); - final RenderLayer layer = layer(TesrMaterial.CURRENT_STATE); - layer.startDrawing(); - assertEquals(fog, GLStateManager.getFogMode().isEnabled(), "fog after startDrawing"); - layer.endDrawing(); - assertEquals(fog, GLStateManager.getFogMode().isEnabled(), "fog after endDrawing"); + @ParameterizedTest(name = "{0}") + @MethodSource("axisCases") + void everyAxisAppliesAbsolutely(String name, Runnable callerState, TesrMaterial material, Runnable asserts) { + callerState.run(); + layer(material).startDrawing(); + asserts.run(); } - @Test - void everyAxisAppliesAbsolutely() { - GLStateManager.disableCull(); - GLStateManager.glCullFace(GL11.GL_FRONT); - GLStateManager.disableLighting(); - GLStateManager.glColorMask(false, false, false, false); + static Stream effectiveStateCases() { + return Stream.of( + Arguments.of("alpha test", (Runnable) () -> { + GLStateManager.getAlphaTest().setVanillaLayer(AlphaTestStorage.ENABLE_LAYER); + GLStateManager.getAlphaState().setVanillaLayer(AlphaTestStorage.FUNC_LAYER); - layer(TesrMaterial.CURRENT_STATE).startDrawing(); + GLStateManager.disableBlend(); + GLStateManager.glEnable(GL11.GL_TEXTURE_2D); + GLStateManager.enableAlphaTest(); + GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f); + GLStateManager.glDepthFunc(GL11.GL_LEQUAL); + GLStateManager.glDepthMask(true); - assertFalse(GLStateManager.getAlphaTest().isEnabled(), "no cutout"); - assertTrue(GLStateManager.getDepthTest().isEnabled(), "depth test"); - assertEquals(GL11.GL_LEQUAL, GLStateManager.getDepthState().getFunc()); - assertTrue(GLStateManager.getDepthState().isEnabled(), "depth write"); - assertFalse(GLStateManager.getBlendMode().isEnabled(), "opaque"); - assertTrue(GLStateManager.getCullState().isEnabled(), "cull pinned on"); - assertEquals(GL11.GL_BACK, GLStateManager.getPolygonState().getCullFaceMode(), "cull face pinned to back"); - assertTrue(GLStateManager.getLightingState().isEnabled(), "lit"); - assertTrue(GLStateManager.getColorMask().red, "color mask pinned on"); - assertFalse(GLStateManager.glIsEnabled(GL11.GL_POLYGON_OFFSET_FILL), "no polygon offset"); + AlphaTestStorage.overrideAlphaTest(null); + + assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw alpha test is disabled by the pack override; a raw read would give SOLID"); + assertSame(EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.itemFromCurrentState(true)); + }), + Arguments.of("depth mask", (Runnable) () -> { + GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER); + GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER); + + GLStateManager.glEnable(GL11.GL_TEXTURE_2D); + GLStateManager.enableBlend(); + GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GLStateManager.glDepthFunc(GL11.GL_LEQUAL); + GLStateManager.glDepthMask(true); + + DepthColorStorage.disableDepthColor(); + + assertSame(EntityMaterials.TRANSLUCENT_DEPTH_WRITE, EntityMaterials.fromCurrentState(true), "raw depth mask held off by another pass; the effective vanilla mask must pick the depth-write variant"); + }), + Arguments.of("color mask", (Runnable) () -> { + GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER); + GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER); + + GLStateManager.glEnable(GL11.GL_TEXTURE_2D); + GLStateManager.disableBlend(); + GLStateManager.enableAlphaTest(); + GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f); + GLStateManager.glDepthFunc(GL11.GL_LEQUAL); + GLStateManager.glDepthMask(true); + + DepthColorStorage.disableDepthColor(); + assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw color mask held off by another pass is not a depth-only draw"); + + DepthColorStorage.deferColorMask(false, false, false, false); + assertSame(EntityMaterials.DEPTH_ONLY_CUTOUT, EntityMaterials.fromCurrentState(true), "vanilla color mask off while held must pick depth-only"); + })); } - @Test - void everyMaterialToggleApplies() { - final TesrMaterial material = TesrMaterial.builder().noCull().unlit().noDepthWrite().cutout(0.5f).translucent().depthEqual().build(); - final RenderLayer layer = layer(material); - layer.startDrawing(); - - assertFalse(GLStateManager.getCullState().isEnabled(), "noCull"); - assertFalse(GLStateManager.getLightingState().isEnabled(), "unlit"); - assertFalse(GLStateManager.getDepthState().isEnabled(), "noDepthWrite"); - assertTrue(GLStateManager.getAlphaTest().isEnabled(), "cutout"); - assertEquals(0.5f, GLStateManager.getAlphaState().getReference(), "cutout threshold"); - assertTrue(GLStateManager.getBlendMode().isEnabled(), "translucent"); - assertEquals(GL11.GL_SRC_ALPHA, GLStateManager.getBlendState().getSrcRgb()); - assertEquals(GL11.GL_ONE_MINUS_SRC_ALPHA, GLStateManager.getBlendState().getDstRgb()); - assertEquals(GL11.GL_EQUAL, GLStateManager.getDepthState().getFunc(), "depthEqual"); - - layer.endDrawing(); - - assertTrue(GLStateManager.getBlendMode().isEnabled(), "endDrawing must not reset blend"); - assertFalse(GLStateManager.getDepthState().isEnabled(), "endDrawing must not reset the depth mask"); - assertFalse(GLStateManager.getLightingState().isEnabled(), "endDrawing must not reset lighting"); - assertTrue(GLStateManager.getAlphaTest().isEnabled(), "endDrawing must not reset the alpha test"); - } - - @Test - void effectiveAlphaStateSurvivesAPackOverridingTheRawAlphaTest() { - GLStateManager.getAlphaTest().setVanillaLayer(AlphaTestStorage.ENABLE_LAYER); - GLStateManager.getAlphaState().setVanillaLayer(AlphaTestStorage.FUNC_LAYER); - - GLStateManager.disableBlend(); - GLStateManager.glEnable(GL11.GL_TEXTURE_2D); - GLStateManager.enableAlphaTest(); - GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f); - GLStateManager.glDepthFunc(GL11.GL_LEQUAL); - GLStateManager.glDepthMask(true); - - AlphaTestStorage.overrideAlphaTest(null); - - assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw alpha test is disabled by the pack override; a raw read would give SOLID"); - assertSame(EntityMaterials.DROPPED_ITEM_CUTOUT, EntityMaterials.itemFromCurrentState(true)); - } - - @Test - void effectiveDepthMaskSurvivesAnotherPassHoldingItOff() { - GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER); - GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER); - - GLStateManager.glEnable(GL11.GL_TEXTURE_2D); - GLStateManager.enableBlend(); - GLStateManager.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GLStateManager.glDepthFunc(GL11.GL_LEQUAL); - GLStateManager.glDepthMask(true); - - DepthColorStorage.disableDepthColor(); - - assertSame(EntityMaterials.TRANSLUCENT_DEPTH_WRITE, EntityMaterials.fromCurrentState(true), "raw depth mask held off by another pass; the effective vanilla mask must pick the depth-write variant"); - } - - @Test - void effectiveColorMaskPicksDepthOnlyWhileAnotherPassHoldsIt() { - GLStateManager.getDepthState().setVanillaLayer(DepthColorStorage.DEPTH_LAYER); - GLStateManager.getColorMask().setVanillaLayer(DepthColorStorage.COLOR_LAYER); - - GLStateManager.glEnable(GL11.GL_TEXTURE_2D); - GLStateManager.disableBlend(); - GLStateManager.enableAlphaTest(); - GLStateManager.glAlphaFunc(GL11.GL_GREATER, 0.1f); - GLStateManager.glDepthFunc(GL11.GL_LEQUAL); - GLStateManager.glDepthMask(true); - - DepthColorStorage.disableDepthColor(); - assertSame(EntityMaterials.CUTOUT, EntityMaterials.fromCurrentState(true), "raw color mask held off by another pass is not a depth-only draw"); - - DepthColorStorage.deferColorMask(false, false, false, false); - assertSame(EntityMaterials.DEPTH_ONLY_CUTOUT, EntityMaterials.fromCurrentState(true), "vanilla color mask off while held must pick depth-only"); + @ParameterizedTest(name = "{0}") + @MethodSource("effectiveStateCases") + void effectiveStateSurvivesAnotherPassOverridingTheRawState(String name, Runnable body) { + body.run(); } private static boolean unitTexIdentity() { diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatchTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatchTest.java index fadcf89e2..46320f922 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatchTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrProviderDispatchTest.java @@ -5,7 +5,6 @@ import com.gtnewhorizons.angelica.api.tesr.TesrMeshSink; import net.minecraft.tileentity.TileEntity; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; class TesrProviderDispatchTest { @@ -17,18 +16,8 @@ class TesrProviderDispatchTest { @Override public void angelica$build(TesrMeshSink sink, TileEntity te) {} } - @Test - void nonProviderIsNotHandled() { - assertFalse(TesrProviderDispatch.tryRender(new Object(), null, 0, 0, 0)); - } - @Test void nullMeshKeyFallsBackToNormalRender() { assertFalse(TesrProviderDispatch.tryRender(new FakeProvider(null), null, 0, 0, 0)); } - - @Test - void nullTileEntityResolvesToZeroBlockEntityId() { - assertEquals(0, TesrProviderDispatch.resolveBlockEntityId(null)); - } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPreludeTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPreludeTest.java new file mode 100644 index 000000000..8d5e59792 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderPreludeTest.java @@ -0,0 +1,50 @@ +package com.gtnewhorizons.angelica.rendering.tesr; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TesrShaderPreludeTest { + + private static final Pattern GLOBAL = Pattern.compile("^(?:layout\\([^)]*\\) )?(?:in|out|uniform|struct|[a-z0-9]+) (?:[A-Za-z0-9_]+ )?([A-Za-z_][A-Za-z0-9_]*)[;(\\[ ]", Pattern.MULTILINE); + + @Test + void everyGlobalIsPrefixedAndMainIsLeftToTheBody() { + for (final String source : new String[] { TesrShaderPrelude.vertex(false), TesrShaderPrelude.vertex(true), TesrShaderPrelude.fragment() }) { + final Matcher matcher = GLOBAL.matcher(source); + int globals = 0; + while (matcher.find()) { + globals++; + final String name = matcher.group(1); + assertTrue(name.startsWith("angelica_") || name.startsWith("iris_") || name.equals("gl_Position"), "unprefixed global " + name); + } + assertTrue(globals >= 10, "the scan must see the declarations, saw " + globals); + assertFalse(source.contains("main")); + } + } + + static Stream versionedBodies() { + return Stream.of( + Arguments.of("leading directive", "#version 120\nvoid main() {}\n", "void main() {}\n"), + Arguments.of("after whitespace", "\n #version 330 core\nvoid main() {}\n", "void main() {}\n"), + Arguments.of("after a line comment", "// header\n#version 330 core\nvoid main() {}\n", "void main() {}\n"), + Arguments.of("after a block comment", "/* a\n b */ #version 330 core\nvoid main() {}\n", "void main() {}\n"), + Arguments.of("directive only", "#version 330 core", ""), + Arguments.of("no directive", "void main() {}\n", "void main() {}\n")); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("versionedBodies") + void stripVersionDropsOnlyALeadingDirective(String name, String body, String expected) { + assertEquals(expected, TesrShaderPrelude.stripVersion("test:strip", body)); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderProgramsGLTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderProgramsGLTest.java new file mode 100644 index 000000000..40b591902 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/TesrShaderProgramsGLTest.java @@ -0,0 +1,358 @@ +package com.gtnewhorizons.angelica.rendering.tesr; + +import com.gtnewhorizons.angelica.api.tesr.TesrShader; +import com.gtnewhorizons.angelica.api.tesr.TesrShaderUniforms; +import com.gtnewhorizons.angelica.api.tesr.TesrShaders; +import com.gtnewhorizons.angelica.glsm.CompatUniformManager; +import com.gtnewhorizons.angelica.glsm.GLCoreTest; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture; +import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import com.gtnewhorizons.angelica.mixins.interfaces.EntityRendererAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.EntityRenderer; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.resources.IResource; +import net.minecraft.client.resources.IResourceManager; +import net.minecraft.util.ResourceLocation; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL20; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +@GLCoreTest +class TesrShaderProgramsGLTest { + + private static final String LIT_VERTEX = """ + angelica_out vec2 v_UV; angelica_out vec2 v_Light; angelica_out vec4 v_Color; angelica_out float v_Depth; + void main() { gl_Position = angelica_clipPosition(); vec3 normal = angelica_eyeNormal(); float d0 = max(dot(normal, normalize(angelica_lightDirection(0))), 0.0); float d1 = max(dot(normal, normalize(angelica_lightDirection(1))), 0.0); vec3 light = min(vec3(0.4) + angelica_lightDiffuse(0) * d0 + angelica_lightDiffuse(1) * d1, 1.0); vec4 color = angelica_color(); v_Color = vec4(color.rgb * light, color.a); v_UV = angelica_uv(); v_Light = angelica_lightmapUv(); v_Depth = angelica_fogCoord(); } + """; + private static final String LIT_FRAGMENT = """ + angelica_in vec2 v_UV; angelica_in vec2 v_Light; angelica_in vec4 v_Color; angelica_in float v_Depth; + void main() { vec4 color = angelica_sampleTexture(v_UV) * v_Color; if (!angelica_alphaTest(color.a)) discard; color.rgb *= angelica_sampleLightmap(v_Light).rgb; angelica_FragColor = vec4(angelica_fog(color.rgb, v_Depth), color.a); } + """; + private static final String UNLIT_VERTEX = """ + angelica_out vec2 v_UV; angelica_out vec4 v_Color; angelica_out float v_Depth; + void main() { gl_Position = angelica_clipPosition(); v_Color = angelica_color(); v_UV = angelica_uv(); v_Depth = angelica_fogCoord(); } + """; + private static final String UNLIT_FRAGMENT = """ + angelica_in vec2 v_UV; angelica_in vec4 v_Color; angelica_in float v_Depth; + void main() { vec4 color = angelica_sampleTexture(v_UV) * v_Color; if (!angelica_alphaTest(color.a)) discard; angelica_FragColor = vec4(angelica_fog(color.rgb, v_Depth), color.a); } + """; + private static final String BROKEN_FRAGMENT = "void main() { angelica_FragColor = missing; }\n"; + + private static int names; + + private final Map sources = new HashMap<>(); + private int reads; + private Minecraft savedMinecraft; + private int lightmapTexture; + private int otherTexture; + + @BeforeEach + void installResources() { + ShaderManager.enable(); + ShaderManager.getInstance().activate(); + lightmapTexture = FfpFixture.solidTexture(255, 255, 255, 255); + otherTexture = FfpFixture.solidTexture(0, 0, 0, 255); + + final IResourceManager manager = new IResourceManager() { + @Override + public Set getResourceDomains() { + return Set.of("test"); + } + + @Override + public IResource getResource(ResourceLocation location) throws IOException { + reads++; + final String body = sources.get(location); + if (body == null) throw new FileNotFoundException(location.toString()); + final IResource resource = mock(IResource.class); + when(resource.getInputStream()).thenReturn(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + return resource; + } + + @Override + public List getAllResources(ResourceLocation location) throws IOException { + return List.of(getResource(location)); + } + }; + final DynamicTexture lightmap = mock(DynamicTexture.class); + when(lightmap.getGlTextureId()).thenReturn(lightmapTexture); + final EntityRenderer entityRenderer = mock(EntityRenderer.class, withSettings().extraInterfaces(EntityRendererAccessor.class)); + when(((EntityRendererAccessor) entityRenderer).getLightmapTexture()).thenReturn(lightmap); + final Minecraft minecraft = mock(Minecraft.class); + when(minecraft.getResourceManager()).thenReturn(manager); + minecraft.entityRenderer = entityRenderer; + savedMinecraft = Reflect.getStatic(Minecraft.class, "theMinecraft"); + Reflect.setStatic(Minecraft.class, "theMinecraft", minecraft); + } + + @AfterEach + void cleanup() { + GLStateManager.glUseProgram(0); + TesrShaderPrograms.invalidateAll(); + Reflect.setStatic(Minecraft.class, "theMinecraft", savedMinecraft); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE1); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + GLStateManager.glDeleteTextures(lightmapTexture); + GLStateManager.glDeleteTextures(otherTexture); + GLStateManager.disableFog(); + final ShaderManager ffp = ShaderManager.getInstance(); + if (ffp.isActive()) ffp.deactivate(); + ShaderManager.disable(); + } + + private TesrShaderPrograms.Entry entry(String vertexBody, String fragmentBody, TesrShaderUniforms uniforms) { + final int id = names++; + final ResourceLocation vertex = new ResourceLocation("test", "shaders/" + id + ".vert"); + final ResourceLocation fragment = new ResourceLocation("test", "shaders/" + id + ".frag"); + sources.put(vertex, vertexBody); + sources.put(fragment, fragmentBody); + final TesrShader shader = TesrShaders.register("test:programs_" + id, vertex, fragment, uniforms); + return TesrShaderPrograms.entry(shader); + } + + private static int uniformInt(int program, String name) { + final int location = GL20.glGetUniformLocation(program, name); + assertNotEquals(-1, location, name + " must be active"); + final IntBuffer value = BufferUtils.createIntBuffer(1); + GL20.glGetUniform(program, location, value); + return value.get(0); + } + + private static void refresh(int program) { + assertTrue(CompatUniformManager.refreshCompatUniforms(program, GLStateManager.ctx()), "a linked variant must be registered for Angelica's uniforms"); + } + + @Test + void variantsLinkLazilyOnFirstBind() { + final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null); + assertEquals(0, reads, "registration and entry resolution read nothing"); + assertFalse(TesrShaderPrograms.instancedAvailable(entry), "an unlinked variant reads as unavailable"); + assertEquals(0, reads, "the queue-time availability read must not link"); + assertEquals(0, entry.baked.program); + assertEquals(0, entry.instanced.program); + + assertTrue(TesrShaderPrograms.bindBaked(entry, false)); + assertEquals(2, reads, "one read of each stage serves both variants"); + assertNotEquals(0, entry.baked.program); + assertNotEquals(0, entry.instanced.program, "the first layer bind links the instanced variant too"); + assertTrue(TesrShaderPrograms.instancedAvailable(entry)); + assertEquals(entry.baked.program, GLStateManager.getActiveProgram()); + + assertTrue(TesrShaderPrograms.bindInstanced(entry)); + assertEquals(entry.instanced.program, GLStateManager.getActiveProgram()); + TesrShaderPrograms.release(entry); + assertEquals(0, GLStateManager.getActiveProgram(), "release restores the program bound before the layer"); + + assertTrue(TesrShaderPrograms.bindBaked(entry, false)); + TesrShaderPrograms.release(entry); + assertEquals(2, reads, "a ready variant is never read or linked again"); + } + + @Test + void linkWaitsWhileADisplayListRecords() { + final TesrShaderPrograms.Entry entry = entry(UNLIT_VERTEX, UNLIT_FRAGMENT, null); + final long failures = TesrShaderPrograms.linkFailures; + final int list = GLStateManager.glGenLists(1); + GLStateManager.glNewList(list, GL11.GL_COMPILE); + try { + assertFalse(TesrShaderPrograms.bindBaked(entry, false), "a recording list would capture the link's program switches"); + TesrShaderPrograms.release(entry); + } finally { + GLStateManager.glEndList(); + GLStateManager.glDeleteLists(list, 1); + } + assertEquals(0, reads); + assertEquals(failures, TesrShaderPrograms.linkFailures, "the variants stay unlinked, not failed"); + + assertTrue(TesrShaderPrograms.bindBaked(entry, false), "the next bind outside a list links"); + TesrShaderPrograms.release(entry); + } + + @ParameterizedTest(name = "missing resource {0}") + @ValueSource(booleans = { false, true }) + void failedVariantsReportOnceAndAreNotRetried(boolean missingResource) { + final TesrShaderPrograms.Entry entry = entry(UNLIT_VERTEX, missingResource ? UNLIT_FRAGMENT : BROKEN_FRAGMENT, null); + if (missingResource) sources.clear(); + final long failures = TesrShaderPrograms.linkFailures; + final long fallbacks = TesrShaderPrograms.fallbacks; + + assertFalse(TesrShaderPrograms.bindBaked(entry, false)); + assertEquals(0, GLStateManager.getActiveProgram(), "a failed bind leaves the emulation bound"); + TesrShaderPrograms.release(entry); + final int readsAfterFirstBind = reads; + assertFalse(TesrShaderPrograms.bindBaked(entry, false)); + TesrShaderPrograms.release(entry); + + assertEquals(0, entry.baked.program); + assertEquals(2, TesrShaderPrograms.linkFailures - failures, "each variant reports its failure once, not per bind"); + assertEquals(2, TesrShaderPrograms.fallbacks - fallbacks); + assertEquals(readsAfterFirstBind, reads, "a failed variant is not retried"); + assertEquals(0, GLStateManager.getActiveProgram()); + } + + @Test + void instancedFailureDisablesInstancingOnly() { + final TesrShaderPrograms.Entry entry = entry(RetainedTesrTestSupport.BAKED_ONLY_VERTEX, RetainedTesrTestSupport.COLOR_FRAGMENT, null); + final long failures = TesrShaderPrograms.linkFailures; + + assertTrue(TesrShaderPrograms.bindBaked(entry, false), "the baked variant still draws"); + assertEquals(entry.baked.program, GLStateManager.getActiveProgram()); + assertFalse(TesrShaderPrograms.instancedAvailable(entry), "the body names a uniform only the baked variant declares"); + assertEquals(1, TesrShaderPrograms.linkFailures - failures); + assertFalse(TesrShaderPrograms.bindInstanced(entry)); + assertEquals(entry.baked.program, GLStateManager.getActiveProgram(), "a failed instanced bind leaves the baked variant bound"); + TesrShaderPrograms.release(entry); + assertEquals(1, TesrShaderPrograms.linkFailures - failures, "a failed variant is not retried"); + } + + @Test + void invalidateAllRelinks() { + final int[] linked = new int[2]; + final int[] bound = new int[2]; + final TesrShaderUniforms uniforms = new TesrShaderUniforms() { + @Override + public void angelica$linked(int program, boolean instanced) { + assertEquals(program, GLStateManager.getActiveProgram(), "the variant is bound while locations resolve"); + linked[instanced ? 1 : 0]++; + } + + @Override + public void angelica$bound(int program, boolean instanced) { + bound[instanced ? 1 : 0]++; + } + }; + final TesrShaderPrograms.Entry entry = entry(UNLIT_VERTEX, BROKEN_FRAGMENT, uniforms); + assertFalse(TesrShaderPrograms.bindBaked(entry, false)); + TesrShaderPrograms.release(entry); + assertEquals(0, linked[0]); + + sources.put(entry.shader.fragment(), UNLIT_FRAGMENT); + TesrShaderPrograms.invalidateAll(); + assertTrue(TesrShaderPrograms.bindBaked(entry, false), "a reload retries a failed variant"); + TesrShaderPrograms.release(entry); + final int first = entry.baked.program; + assertTrue(GL20.glIsProgram(first)); + + TesrShaderPrograms.invalidateAll(); + assertEquals(0, entry.baked.program); + assertTrue(!GL20.glIsProgram(first) || GL20.glGetProgrami(first, GL20.GL_DELETE_STATUS) == GL11.GL_TRUE, "the stale program is deleted, or flagged while the driver still has it current"); + assertTrue(TesrShaderPrograms.bindBaked(entry, false)); + TesrShaderPrograms.release(entry); + assertNotEquals(0, entry.baked.program); + assertEquals(2, linked[0], "linked runs once per linked variant"); + assertEquals(2, linked[1], "each relink of the baked variant links the instanced one with it"); + assertEquals(2, bound[0], "bound runs once per successful bind"); + assertEquals(0, bound[1]); + } + + @Test + void lightmapBoundOnUnitOneAndRestored() { + final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE1); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, otherTexture); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + + assertTrue(TesrShaderPrograms.bindBaked(entry, false)); + assertEquals(otherTexture, GLStateManager.getBoundTextureForServerState(1), "a layer without the lightmap flag leaves unit 1 alone"); + TesrShaderPrograms.release(entry); + + assertTrue(TesrShaderPrograms.bindBaked(entry, true)); + assertEquals(lightmapTexture, GLStateManager.getBoundTextureForServerState(1)); + assertEquals(0, GLStateManager.getActiveTextureUnit(), "the bind must not move the active unit"); + assertEquals(1, uniformInt(entry.baked.program, TesrShaderPrelude.LIGHTMAP_SAMPLER)); + assertTrue(TesrShaderPrograms.bindInstanced(entry)); + assertEquals(lightmapTexture, GLStateManager.getBoundTextureForServerState(1)); + TesrShaderPrograms.release(entry); + assertEquals(otherTexture, GLStateManager.getBoundTextureForServerState(1), "release restores the caller's unit-1 texture"); + assertEquals(0, GLStateManager.getActiveTextureUnit()); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE1); + assertEquals(otherTexture, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "the driver binding is restored too"); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + } + + @Test + void fogUniformsFollowFogState() { + final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null); + assertTrue(TesrShaderPrograms.bindBaked(entry, false)); + final int program = entry.baked.program; + + GLStateManager.disableFog(); + GLStateManager.glFogi(GL11.GL_FOG_MODE, GL11.GL_LINEAR); + refresh(program); + assertEquals(0, uniformInt(program, "angelica_FogMode"), "disabled fog is mode 0 whatever the fog function"); + GLStateManager.enableFog(); + refresh(program); + assertEquals(1, uniformInt(program, "angelica_FogMode")); + GLStateManager.glFogi(GL11.GL_FOG_MODE, GL11.GL_EXP); + refresh(program); + assertEquals(2, uniformInt(program, "angelica_FogMode")); + GLStateManager.glFogi(GL11.GL_FOG_MODE, GL11.GL_EXP2); + refresh(program); + assertEquals(3, uniformInt(program, "angelica_FogMode")); + GLStateManager.disableFog(); + refresh(program); + assertEquals(0, uniformInt(program, "angelica_FogMode")); + + GLStateManager.glFogf(GL11.GL_FOG_START, 2f); + GLStateManager.glFogf(GL11.GL_FOG_END, 10f); + refresh(program); + final FloatBuffer params = BufferUtils.createFloatBuffer(4); + GL20.glGetUniform(program, GL20.glGetUniformLocation(program, "angelica_FogParams"), params); + assertEquals(-1f / 8f, params.get(0), 1e-6f, "a fog range change re-uploads the params"); + TesrShaderPrograms.release(entry); + GLStateManager.glFogf(GL11.GL_FOG_START, 0f); + GLStateManager.glFogf(GL11.GL_FOG_END, 1f); + } + + @Test + void lightUniformsReachAProgramWithoutAnAmbientField() { + final TesrShaderPrograms.Entry entry = entry(LIT_VERTEX, LIT_FRAGMENT, null); + assertTrue(TesrShaderPrograms.bindBaked(entry, false)); + final int program = entry.baked.program; + final FloatBuffer diffuse = BufferUtils.createFloatBuffer(4); + diffuse.put(0.25f).put(0.5f).put(0.75f).put(1f).flip(); + GLStateManager.glLight(GL11.GL_LIGHT1, GL11.GL_DIFFUSE, diffuse); + refresh(program); + + final int location = GL20.glGetUniformLocation(program, "angelica_LightSource[1].diffuse"); + assertNotEquals(-1, location); + final FloatBuffer uploaded = BufferUtils.createFloatBuffer(4); + GL20.glGetUniform(program, location, uploaded); + assertEquals(0.5f, uploaded.get(1), 1e-6f); + assertEquals(0.75f, uploaded.get(2), 1e-6f); + TesrShaderPrograms.release(entry); + diffuse.clear(); + diffuse.put(0f).put(0f).put(0f).put(1f).flip(); + GLStateManager.glLight(GL11.GL_LIGHT1, GL11.GL_DIFFUSE, diffuse); + } +} diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/VertexTransformTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/VertexTransformTest.java index 749d02577..6e934773e 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/VertexTransformTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/tesr/VertexTransformTest.java @@ -7,8 +7,13 @@ import net.coderbot.batchedentityrendering.impl.BatchVertexFormats; import org.joml.Matrix4f; import org.joml.Vector3f; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.lwjgl.opengl.GL11; import java.nio.ByteBuffer; +import java.util.stream.Stream; import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0; import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetFloat; @@ -51,35 +56,20 @@ class VertexTransformTest { @Test void writeInstanceTranslatesPositionSetsColorLightKeepsUv() { final TemplateBuffer template = template(oneVertexUp()); - final Matrix4f mv = new Matrix4f().translation(10f, -2f, 3f); + final Matrix4f mv = new Matrix4f().translation(10f, -2f, 3f).rotateX((float) Math.toRadians(90)); final ByteBuffer dest = ByteBuffer.allocateDirect(DEST.getVertexSize()); final long addr = memAddress0(dest); VertexTransform.writeInstance(addr, DEST, template, mv, new Vector3f(), 0xFF8040C0, 0x00F000F0, null); assertEquals(10f, memGetFloat(addr + OFF_POS), 1e-6f); - assertEquals(-1f, memGetFloat(addr + OFF_POS + 4), 1e-6f); - assertEquals(3f, memGetFloat(addr + OFF_POS + 8), 1e-6f); + assertEquals(-2f, memGetFloat(addr + OFF_POS + 4), 1e-6f); + assertEquals(4f, memGetFloat(addr + OFF_POS + 8), 1e-6f); assertEquals(0xFF8040C0, memGetInt(addr + OFF_COLOR)); assertEquals(0.25f, memGetFloat(addr + OFF_TEX), 0f); assertEquals(0.75f, memGetFloat(addr + OFF_TEX + 4), 0f); assertEquals(240f, memGetFloat(addr + OFF_LIGHT), 0f); // 0x00F0 assertEquals(240f, memGetFloat(addr + OFF_LIGHT + 4), 0f); // 0x00F0 - final int n = memGetInt(addr + OFF_NORMAL); - assertEquals(0f, NormI8.unpackX(n), NORM_EPS); - assertEquals(1f, NormI8.unpackY(n), NORM_EPS); - assertEquals(0f, NormI8.unpackZ(n), NORM_EPS); - } - - @Test - void writeInstanceRotatesNormal() { - final TemplateBuffer template = template(oneVertexUp()); - final Matrix4f mv = new Matrix4f().rotationX((float) Math.toRadians(90)); - final ByteBuffer dest = ByteBuffer.allocateDirect(DEST.getVertexSize()); - final long addr = memAddress0(dest); - - VertexTransform.writeInstance(addr, DEST, template, mv, new Vector3f(), 0, 0, null); - final int n = memGetInt(addr + OFF_NORMAL); assertEquals(0f, NormI8.unpackX(n), NORM_EPS); assertEquals(0f, NormI8.unpackY(n), NORM_EPS); @@ -101,14 +91,6 @@ class VertexTransformTest { assertEquals(0xFF0080FF, memGetInt(addr + OFF_COLOR)); } - @Test - void mulColorIsExactForWhiteAndRounds() { - assertEquals(0x12345678, VertexTransform.mulColor(0x12345678, 0xFFFFFFFF)); - assertEquals(0x12345678, VertexTransform.mulColor(0xFFFFFFFF, 0x12345678)); - assertEquals(0x80, VertexTransform.mulColor(0xFF, 0x80)); - assertEquals(0x00, VertexTransform.mulColor(0x7F, 0x00)); - } - @Test void writeInstanceAppliesTextureMatrixToUvThenRestores() { final TemplateBuffer template = template(oneVertexUp()); @@ -125,25 +107,37 @@ class VertexTransformTest { assertEquals(0.75f, memGetFloat(addr + OFF_TEX + 4), 0f); } - @Test - void decodeReadsPositionUvNormalByUsage() { + static Stream decodeCases() { + return Stream.of( + Arguments.of("explicit normal", GL11.GL_POINTS, new float[][] {{5f, 6f, 7f, 0.1f, 0.2f}}, NormI8.pack(1f, 0f, 0f), 1f, 0f, 0f), + Arguments.of("quad seeds its face normal", GL11.GL_QUADS, new float[][] { + {0f, 0f, 0f, 0f, 0f}, {1f, 0f, 0f, 1f, 0f}, {1f, 1f, 0f, 1f, 1f}, {0f, 1f, 0f, 0f, 1f}}, 0, 0f, 0f, 1f)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("decodeCases") + void decodeReadsPositionUvNormalByUsage(String name, int drawMode, float[][] vertices, int packedNormal, float nx, float ny, float nz) { final VertexFormat src = DefaultVertexFormat.POSITION_TEXTURE_NORMAL; - final ByteBuffer buf = ByteBuffer.allocateDirect(src.getVertexSize()); + final ByteBuffer buf = ByteBuffer.allocateDirect(src.getVertexSize() * vertices.length); final long addr = memAddress0(buf); - memPutFloat(addr, 5f); - memPutFloat(addr + 4, 6f); - memPutFloat(addr + 8, 7f); - memPutFloat(addr + 12, 0.1f); - memPutFloat(addr + 16, 0.2f); - memPutInt(addr + 20, NormI8.pack(1f, 0f, 0f)); + for (int i = 0; i < vertices.length; i++) { + final long v = addr + (long) i * src.getVertexSize(); + for (int f = 0; f < 5; f++) memPutFloat(v + 4L * f, vertices[i][f]); + memPutInt(v + 20, packedNormal); + } - final int[] out = VertexTransform.decode(addr, src, 1); + final int[] out = VertexTransform.decode(addr, src, vertices.length, drawMode); - assertEquals(5f, Float.intBitsToFloat(out[X_INDEX]), 0f); - assertEquals(6f, Float.intBitsToFloat(out[Y_INDEX]), 0f); - assertEquals(7f, Float.intBitsToFloat(out[Z_INDEX]), 0f); - assertEquals(0.1f, Float.intBitsToFloat(out[TEX_X_INDEX]), 0f); - assertEquals(0.2f, Float.intBitsToFloat(out[TEX_Y_INDEX]), 0f); - assertEquals(1f, NormI8.unpackX(out[NORMAL_INDEX]), NORM_EPS); + for (int i = 0; i < vertices.length; i++) { + final int base = i * VERTEX_SIZE; + assertEquals(vertices[i][0], Float.intBitsToFloat(out[base | X_INDEX]), 0f); + assertEquals(vertices[i][1], Float.intBitsToFloat(out[base | Y_INDEX]), 0f); + assertEquals(vertices[i][2], Float.intBitsToFloat(out[base | Z_INDEX]), 0f); + assertEquals(vertices[i][3], Float.intBitsToFloat(out[base | TEX_X_INDEX]), 0f); + assertEquals(vertices[i][4], Float.intBitsToFloat(out[base | TEX_Y_INDEX]), 0f); + assertEquals(nx, NormI8.unpackX(out[base | NORMAL_INDEX]), NORM_EPS); + assertEquals(ny, NormI8.unpackY(out[base | NORMAL_INDEX]), NORM_EPS); + assertEquals(nz, NormI8.unpackZ(out[base | NORMAL_INDEX]), NORM_EPS); + } } }