From 9493d630a0510701ace2b8ba09bb5f4e27808d22 Mon Sep 17 00:00:00 2001 From: Jason Mitchell Date: Sat, 7 Nov 2026 13:42:32 -0800 Subject: [PATCH] Emulate clip distances on devices without them (#2348) --- .../angelica/config/SystemProperties.java | 1 + .../angelica/glsm/ClipPlaneBooleanState.java | 11 + .../angelica/glsm/CompatUniformManager.java | 10 +- .../gtnewhorizons/angelica/glsm/GLESCaps.java | 5 +- .../angelica/glsm/GLSMEnable.java | 2 - .../angelica/glsm/GLSMRasterizer.java | 8 + .../angelica/glsm/RenderSystem.java | 13 +- .../angelica/glsm/ffp/ClipDistanceGlsl.java | 17 ++ .../angelica/glsm/ffp/FragmentKey.java | 4 + .../glsm/ffp/FragmentShaderGenerator.java | 6 + .../angelica/glsm/ffp/Uniforms.java | 5 +- .../angelica/glsm/ffp/VertexKey.java | 6 +- .../glsm/ffp/VertexShaderGenerator.java | 14 +- .../angelica/glsm/hooks/GLSMHooks.java | 2 + .../hooks/events/ClipPlanesInUseEvent.java | 5 + .../angelica/glsm/redirect/GLSMOwners.java | 32 +-- .../angelica/glsm/states/BooleanState.java | 8 +- .../angelica/glsm/states/ClipPlaneState.java | 14 +- .../angelica/glsm/GLESCapsTest.java | 21 +- .../angelica/glsm/ffp/ClipPlaneGLTest.java | 124 +++++++++ .../ffp/FFPUniformBlockSpirvLayoutTest.java | 25 ++ .../glsm/ffp/FfpUniformBlockDedupTest.java | 18 +- .../glsm/testutil/ClipTestSupport.java | 37 +++ .../lwjgl3/Lwjgl3GLRenderBackend.java | 2 +- .../angelica/lwjgl3/GlesSmokeTest.java | 35 ++- scripts/flyby-scenes/clip-planes.txt | 8 + .../angelica/sdlgpu/SDLGPURenderBackend.java | 96 ++++++- .../angelica/sdlgpu/device/Device.java | 45 +++- .../angelica/sdlgpu/frame/ContextState.java | 2 + .../sdlgpu/pipeline/PipelineApplier.java | 98 ++++++- .../angelica/sdlgpu/shader/ShaderManager.java | 95 ++++++- .../angelica/sdlgpu/shader/UniformNames.java | 25 ++ .../sdlgpu/device/DeviceLifecycleGpuTest.java | 10 + .../angelica/sdlgpu/glsm/GlsmSdlFfpTest.java | 25 ++ .../sdlgpu/glsm/GlsmSdlUniformArrayTest.java | 246 ++++++++++++++++++ .../angelica/common/BlockClipPlaneTest.java | 34 +++ .../common/TileEntityClipPlaneTest.java | 11 + .../angelica/iris/IrisGLSMBridge.java | 12 +- .../angelica/proxy/ClientProxy.java | 4 + .../angelica/proxy/CommonProxy.java | 4 + .../rendering/ClipPlaneTestRenderer.java | 41 +++ .../DeferredWorldRenderingPipeline.java | 240 +++++++++++------ .../pipeline/transform/CommonTransformer.java | 34 ++- .../pipeline/transform/ShaderTransformer.java | 2 +- .../pipeline/transform/TransformPatcher.java | 10 +- .../parameter/AttributeParameters.java | 11 +- .../AttributeTransformerLinkGLTest.java | 2 +- .../AttributeTransformerInstancedTest.java | 52 ++++ .../ShaderTransformerDiskCacheTest.java | 21 ++ 49 files changed, 1359 insertions(+), 194 deletions(-) create mode 100644 glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/ClipDistanceGlsl.java create mode 100644 glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/events/ClipPlanesInUseEvent.java create mode 100644 glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/ClipPlaneGLTest.java create mode 100644 glsm/src/testFixtures/java/com/gtnewhorizons/angelica/glsm/testutil/ClipTestSupport.java create mode 100644 scripts/flyby-scenes/clip-planes.txt create mode 100644 sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/UniformNames.java create mode 100644 sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlUniformArrayTest.java create mode 100644 src/main/java/com/gtnewhorizons/angelica/common/BlockClipPlaneTest.java create mode 100644 src/main/java/com/gtnewhorizons/angelica/common/TileEntityClipPlaneTest.java create mode 100644 src/main/java/com/gtnewhorizons/angelica/rendering/ClipPlaneTestRenderer.java diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/config/SystemProperties.java b/glsm/src/main/java/com/gtnewhorizons/angelica/config/SystemProperties.java index 519c35e51..c9a933741 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/config/SystemProperties.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/config/SystemProperties.java @@ -21,6 +21,7 @@ public final class SystemProperties { public static final String GL_PROFILE = System.getProperty("angelica.glProfile", ""); public static final boolean DISABLE_NVIDIA_WORKAROUND = Boolean.getBoolean("angelica.disableNvidiaWorkaround"); public static final boolean DISABLE_LTW_WORKAROUND = Boolean.getBoolean("angelica.disableLtwWorkaround"); + public static final boolean EMULATE_CLIP_DISTANCES = Boolean.getBoolean("angelica.emulateClipDistances"); // Redirector public static final UnmappedGLMode UNMAPPED_GL = parseEnum("angelica.unmappedGL", UnmappedGLMode.WARN, UnmappedGLMode.WARN, UnmappedGLMode.class); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ClipPlaneBooleanState.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ClipPlaneBooleanState.java index cc40d7d80..604b06b66 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ClipPlaneBooleanState.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ClipPlaneBooleanState.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.glsm; +import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks; import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack; public final class ClipPlaneBooleanState extends BooleanStateStack { @@ -13,12 +14,22 @@ public final class ClipPlaneBooleanState extends BooleanStateStack { this.index = index; } + @Override + protected boolean forwardsToBackend() { + return !RenderSystem.isClipDistanceEmulated(); + } + @Override protected void onEnabledChanged() { + final int before = owner.clipPlaneEnabledMask; if (isEnabled()) { owner.clipPlaneEnabledMask |= (1 << index); } else { owner.clipPlaneEnabledMask &= ~(1 << index); } + owner.clipPlaneGeneration++; + if ((before == 0) != (owner.clipPlaneEnabledMask == 0) && RenderSystem.isClipDistanceEmulated() && GLSMHooks.CLIP_PLANES_IN_USE.hasListeners()) { + GLSMHooks.CLIP_PLANES_IN_USE.post(GLSMHooks.clipPlanesInUseEvent); + } } } 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 e5e539f62..20e11fc8b 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/CompatUniformManager.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/CompatUniformManager.java @@ -249,7 +249,7 @@ public class CompatUniformManager { final int cpGen = glCtx.clipPlaneGeneration; if (cpGen != pu.lastClipPlaneGen) { pu.lastClipPlaneGen = cpGen; - uploadClipPlanes(locs); + uploadClipPlanes(locs, glCtx.clipPlaneEnabledMask); } } @@ -468,16 +468,14 @@ public class CompatUniformManager { RENDER_BACKEND.uniform1f(locs[LOC_MAT_BASE + MF_SHININESS], mat.shininess); } - private static void uploadClipPlanes(int[] locs) { + private static void uploadClipPlanes(int[] locs, int enabledMask) { if (locs[LOC_CLIP_PLANES_ENABLED] != -1) { - RENDER_BACKEND.uniform1i(locs[LOC_CLIP_PLANES_ENABLED], GLSMRasterizer.anyClipPlaneEnabled() ? 1 : 0); + RENDER_BACKEND.uniform1i(locs[LOC_CLIP_PLANES_ENABLED], enabledMask != 0 ? 1 : 0); } if (locs[LOC_CLIP_PLANES] != -1) { final ClipPlaneState cps = GLSMRasterizer.getClipPlaneState(); clipPlaneBuf.clear(); - for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) { - cps.putEyePlane(i, clipPlaneBuf); - } + cps.putEffectivePlanes(enabledMask, clipPlaneBuf); clipPlaneBuf.flip(); RENDER_BACKEND.uniform4(locs[LOC_CLIP_PLANES], clipPlaneBuf); } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLESCaps.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLESCaps.java index 7f6edaaea..f507c0343 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLESCaps.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLESCaps.java @@ -11,11 +11,8 @@ public final class GLESCaps { private GLESCaps() {} - public static boolean isCapAllowed(int cap, boolean isGLES, boolean hasClipCullDistance) { + public static boolean isCapAllowed(int cap, boolean isGLES) { if (!isGLES) return true; - if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + GLStateManager.MAX_CLIP_PLANES) { - return hasClipCullDistance; - } return switch (cap) { case GL11.GL_LINE_SMOOTH, GL11.GL_POLYGON_SMOOTH, diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMEnable.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMEnable.java index 6ecb0c3e1..f2d5ef400 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMEnable.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMEnable.java @@ -30,7 +30,6 @@ public final class GLSMEnable { // Handle clip planes dynamically (supports up to MAX_CLIP_PLANES) if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) { glCtx.clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].enable(); - glCtx.clipPlaneGeneration++; return; } @@ -119,7 +118,6 @@ public final class GLSMEnable { // Handle clip planes dynamically (supports up to MAX_CLIP_PLANES) if (cap >= GL11.GL_CLIP_PLANE0 && cap < GL11.GL_CLIP_PLANE0 + MAX_CLIP_PLANES) { glCtx.clipPlaneStates[cap - GL11.GL_CLIP_PLANE0].disable(); - glCtx.clipPlaneGeneration++; return; } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java index b6ffe8038..78a993845 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java @@ -509,6 +509,14 @@ public final class GLSMRasterizer { return glCtx.clipPlaneEnabledMask != 0; } + public static boolean clipEmulationActive() { + return RenderSystem.isClipDistanceEmulated() && anyClipPlaneEnabled(); + } + + public static boolean clipEmulationActive(GLContextState glCtx) { + return RenderSystem.isClipDistanceEmulated() && anyClipPlaneEnabled(glCtx); + } + public static void glSampleCoverage(float value, boolean invert) { if (DisplayListManager.isRecording()) { throw DisplayListManager.unsupportedInList("glSampleCoverage"); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java index 208af9cf7..b433d1d54 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java @@ -63,7 +63,7 @@ public class RenderSystem { private static volatile boolean isGLES; private static volatile int glesVersion; private static volatile boolean glesDetected; - private static volatile boolean hasClipCullDistance; + private static boolean clipDistanceEmulated; private static volatile boolean isLTW; private static volatile boolean ltwDetected; @@ -132,7 +132,12 @@ public class RenderSystem { supportsClearTexture = GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_clear_texture; supportsStencilTexturing = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_stencil_texturing; supportsTextureStorage = isGLES ? glesVersion >= 300 : GLStateManager.capabilities.OpenGL42 || GLStateManager.capabilities.GL_ARB_texture_storage; - hasClipCullDistance = !isGLES || hasExtension("GL_EXT_clip_cull_distance"); + final boolean esClipExt = !isGLES || hasExtension("GL_EXT_clip_cull_distance"); + final int maxClipDistances = esClipExt ? RENDER_BACKEND.getInteger(GL30.GL_MAX_CLIP_DISTANCES) : 0; + clipDistanceEmulated = SystemProperties.EMULATE_CLIP_DISTANCES || maxClipDistances < GLStateManager.MAX_CLIP_PLANES; + if (clipDistanceEmulated) { + GLStateManager.LOGGER.info("Clip distances emulated (GL_MAX_CLIP_DISTANCES={}, forced={})", maxClipDistances, SystemProperties.EMULATE_CLIP_DISTANCES); + } // Cache maximum image units if (supportsImageLoadStore) { @@ -434,8 +439,8 @@ public class RenderSystem { return supportsImageLoadStore; } - public static boolean hasClipCullDistance() { - return hasClipCullDistance; + public static boolean isClipDistanceEmulated() { + return clipDistanceEmulated; } private static boolean hasExtension(String name) { diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/ClipDistanceGlsl.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/ClipDistanceGlsl.java new file mode 100644 index 000000000..61568e1f4 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/ClipDistanceGlsl.java @@ -0,0 +1,17 @@ +package com.gtnewhorizons.angelica.glsm.ffp; + +public final class ClipDistanceGlsl { + + public static final String VS_OUT_DIST0 = "out vec4 angelica_ClipDist0;"; + public static final String VS_OUT_DIST1 = "out vec4 angelica_ClipDist1;"; + public static final String FS_IN_DIST0 = "in vec4 angelica_ClipDist0;"; + public static final String FS_IN_DIST1 = "in vec4 angelica_ClipDist1;"; + public static final String FS_DISCARD = "if (any(lessThan(angelica_ClipDist0, vec4(0.0))) || any(lessThan(angelica_ClipDist1, vec4(0.0)))) discard;\n"; + + private ClipDistanceGlsl() {} + + public static void appendVsAssign(StringBuilder sb, String[] distances) { + sb.append(" angelica_ClipDist0 = vec4(").append(distances[0]).append(", ").append(distances[1]).append(", ").append(distances[2]).append(", ").append(distances[3]).append(");\n"); + sb.append(" angelica_ClipDist1 = vec4(").append(distances[4]).append(", ").append(distances[5]).append(", ").append(distances[6]).append(", ").append(distances[7]).append(");\n"); + } +} 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 ba6f5ca0d..82bf93bab 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 @@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.ffp; import java.util.Arrays; import com.gtnewhorizons.angelica.glsm.GLContextState; +import com.gtnewhorizons.angelica.glsm.GLSMRasterizer; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.states.TexEnvState; import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray; @@ -79,6 +80,7 @@ public final class FragmentKey { private static final int GLOBAL_BITS = 14; private static final int BIT_COMBINED_GLINT = 61; private static final int BIT_GLINT_REPLACE_ALPHA = 62; + private static final int BIT_CLIP_DISCARD = 63; private static final int BIT_FOG_MODE = 0; // 2 bits private static final int BIT_ALPHA_TEST = 2; // 1 bit private static final int BIT_ALPHA_FUNC = 3; // 3 bits @@ -126,6 +128,7 @@ public final class FragmentKey { long global = 0; if (CombinedGlint.isActive()) global |= 1L << BIT_COMBINED_GLINT; if (CombinedGlint.replacesAlpha()) global |= 1L << BIT_GLINT_REPLACE_ALPHA; + if (GLSMRasterizer.clipEmulationActive(glCtx)) global |= 1L << BIT_CLIP_DISCARD; // Fog global |= activeFogMode(glCtx); @@ -259,6 +262,7 @@ public final class FragmentKey { public int fogMode() { return (int) (packed[0] & 0x3); } public boolean combinedGlint() { return (packed[0] & (1L << BIT_COMBINED_GLINT)) != 0; } + public boolean clipDiscard() { return (packed[0] & (1L << BIT_CLIP_DISCARD)) != 0; } public boolean glintReplaceAlpha() { return (packed[0] & (1L << BIT_GLINT_REPLACE_ALPHA)) != 0; } public int textureSupersamplingSamples() { return 1 << (int) ((packed[0] >> BIT_TEXTURE_SUPERSAMPLING) & 0x7); } public boolean alphaTestEnabled() { return ((packed[0] >> BIT_ALPHA_TEST) & 1) != 0; } 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 6fe4e51b2..7540aafdb 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 @@ -34,6 +34,9 @@ public final class FragmentShaderGenerator { } sb.append("void main() {\n"); + if (key.clipDiscard()) { + sb.append(ClipDistanceGlsl.FS_DISCARD); + } emitLineStipple(sb, key); emitTextureSampling(sb, key); emitTexEnvChain(sb, key); @@ -49,6 +52,9 @@ public final class FragmentShaderGenerator { private static void emitInputs(StringBuilder sb, FragmentKey key) { sb.append("// Inputs from vertex shader\n"); sb.append("in vec4 v_Color;\n"); + if (key.clipDiscard()) { + sb.append(ClipDistanceGlsl.FS_IN_DIST0).append('\n').append(ClipDistanceGlsl.FS_IN_DIST1).append('\n'); + } if (key.overlayInstanced()) { sb.append("in vec4 v_Overlay;\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 c50225812..6e2748e94 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 @@ -2,7 +2,6 @@ package com.gtnewhorizons.angelica.glsm.ffp; import com.gtnewhorizons.angelica.glsm.GLContextState; import com.gtnewhorizons.angelica.glsm.GLSMBuffers; -import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.states.ClipPlaneState; import com.gtnewhorizons.angelica.glsm.states.FogState; import com.gtnewhorizons.angelica.glsm.states.LightState; @@ -330,9 +329,7 @@ public class Uniforms { private void stageClipPlanes(GLContextState glCtx) { final ClipPlaneState cps = glCtx.clipPlaneState; clipPlaneBuf.clear(); - for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) { - cps.putEyePlane(i, clipPlaneBuf); - } + cps.putEffectivePlanes(glCtx.clipPlaneEnabledMask, clipPlaneBuf); final long src = memAddress0(clipPlaneBuf); for (int i = 0; i < 32 * 4; i += 4) { putInt(FFPUniformBlock.CLIP_PLANES + i, memGetInt(src + i)); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexKey.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexKey.java index 6d72ef2ce..0c10347bd 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexKey.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/ffp/VertexKey.java @@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.GLContextState; import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs; import com.gtnewhorizons.angelica.glsm.GLSMRasterizer; import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.RenderSystem; import com.gtnewhorizons.angelica.glsm.states.TexGenState; import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray; import org.joml.Matrix4f; @@ -53,6 +54,7 @@ public final class VertexKey { private static final int BIT_UNIT23_UV_FROM_UNIT0 = BIT_UNIT_TEXMAT_BASE + MAX_UNITS; private static final int BIT_LINE_STIPPLE = BIT_UNIT23_UV_FROM_UNIT0 + 1; private static final int BIT_INSTANCING = BIT_LINE_STIPPLE + 1; + private static final int BIT_CLIP_EMULATED = BIT_INSTANCING + 3; private static final long INSTANCING_MASK = 0x7L; static { @@ -105,6 +107,7 @@ public final class VertexKey { public int texGenModeQ() { return (int)((packed >> BIT_TEXGEN_Q) & 0x7); } public boolean texGenEnabled() { return texGenModeS() != TG_NONE || texGenModeT() != TG_NONE || texGenModeR() != TG_NONE || texGenModeQ() != TG_NONE; } public boolean clipPlanesEnabled() { return bit(BIT_CLIP_PLANES); } + public boolean clipEmulated() { return bit(BIT_CLIP_EMULATED); } public boolean lineStipple() { return bit(BIT_LINE_STIPPLE); } public boolean lineQuads() { return bit(BIT_LINE_QUADS); } public Instancing instancing() { return Instancing.VALUES[(int) ((packed >> BIT_INSTANCING) & INSTANCING_MASK)]; } @@ -238,6 +241,7 @@ public final class VertexKey { if (GLSMRasterizer.anyClipPlaneEnabled(glCtx)) { bits |= (1L << BIT_CLIP_PLANES); + if (RenderSystem.isClipDistanceEmulated()) bits |= 1L << BIT_CLIP_EMULATED; } if (glCtx.lineStippleActive) { @@ -279,6 +283,6 @@ public final class VertexKey { @Override public String toString() { - return String.format("FFPVertexKey[0x%012X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b lquads=%b inst=%s]", packed,lightingEnabled(), lightEnabled(0), lightEnabled(1), colorMaterialEnabled(), fogEnabled(), unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0, unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0, hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(), texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), lineQuads(), instancing()); + return String.format("FFPVertexKey[0x%012X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b clipEmu=%b lquads=%b inst=%s]", packed,lightingEnabled(), lightEnabled(0), lightEnabled(1), colorMaterialEnabled(), fogEnabled(), unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0, unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0, hasVertexColor(), hasVertexNormal(), hasVertexTexCoord(), hasVertexLightmap(), texGenModeS(), texGenModeT(), texGenModeR(), texGenModeQ(), clipPlanesEnabled(), clipEmulated(), lineQuads(), instancing()); } } 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 d96ecf935..286271930 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 @@ -111,6 +111,9 @@ public final class VertexShaderGenerator { if (key.lineStipple()) { sb.append("flat out vec2 v_LineStart;\n"); } + if (key.clipEmulated()) { + sb.append(ClipDistanceGlsl.VS_OUT_DIST0).append('\n').append(ClipDistanceGlsl.VS_OUT_DIST1).append('\n'); + } if (key.separateSpecular()) { sb.append("out vec3 v_SpecularColor;\n"); } @@ -398,10 +401,19 @@ public final class VertexShaderGenerator { private static void emitClipDistances(StringBuilder sb, VertexKey key) { sb.append(" // Clip distances\n"); + final boolean emulated = key.clipEmulated(); + final String[] distances = emulated ? new String[8] : null; for (int i = 0; i < 8; i++) { final String own = "dot(u_ClipPlane[" + i + "], eyePos)"; final String distance = key.lineQuads() ? acrossLine(own, "dot(u_ClipPlane[" + i + "], lineEyeOther)") : own; - sb.append(" gl_ClipDistance[").append(i).append("] = ").append(distance).append(";\n"); + if (emulated) { + distances[i] = distance; + } else { + sb.append(" gl_ClipDistance[").append(i).append("] = ").append(distance).append(";\n"); + } + } + if (emulated) { + ClipDistanceGlsl.appendVsAssign(sb, distances); } } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java index 94689f0d3..10d2eb0e3 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/GLSMHooks.java @@ -31,6 +31,7 @@ public final class GLSMHooks { public static final EventBus PROGRAM_DELETE = EventBus.create(ProgramDeleteEvent.class); public static final EventBus BLEND_FUNC_CHANGE = EventBus.create(BlendFuncChangeEvent.class); public static final EventBus FOG_STATE_CHANGE = EventBus.create(FogStateChangeEvent.class); + public static final EventBus CLIP_PLANES_IN_USE = EventBus.create(ClipPlanesInUseEvent.class); public static final EventBus LIGHTMAP_COORDS = EventBus.create(LightmapCoordsEvent.class); public static final EventBus ALPHA_STATE_CHANGE = EventBus.create(AlphaStateChangeEvent.class); public static final EventBus SHADER_COLOR_CHANGE = EventBus.create(ShaderColorChangeEvent.class); @@ -46,6 +47,7 @@ public final class GLSMHooks { public static final ProgramDeleteEvent programDeleteEvent = new ProgramDeleteEvent(); public static final BlendFuncChangeEvent blendFuncChangeEvent = new BlendFuncChangeEvent(); public static final FogStateChangeEvent fogStateChangeEvent = new FogStateChangeEvent(); + public static final ClipPlanesInUseEvent clipPlanesInUseEvent = new ClipPlanesInUseEvent(); public static final LightmapCoordsEvent lightmapCoordsEvent = new LightmapCoordsEvent(); public static final AlphaStateChangeEvent alphaStateChangeEvent = new AlphaStateChangeEvent(); public static final ShaderColorChangeEvent shaderColorChangeEvent = new ShaderColorChangeEvent(); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/events/ClipPlanesInUseEvent.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/events/ClipPlanesInUseEvent.java new file mode 100644 index 000000000..f4cd2ba40 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/events/ClipPlanesInUseEvent.java @@ -0,0 +1,5 @@ +package com.gtnewhorizons.angelica.glsm.hooks.events; + +import net.minecraftforge.eventbus.api.event.MutableEvent; + +public final class ClipPlanesInUseEvent extends MutableEvent {} diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMOwners.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMOwners.java index 8201d07ae..fee2456a0 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMOwners.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMOwners.java @@ -106,22 +106,22 @@ final class GLSMOwners { "glGetIntegeri", "glGetIntegeri_v", "glGetQueryObjecti", "glGetQueryObjectui", "glGetQueryObjectui64", "glGetString", "glGetStringi", "glIsQuery", "glQueryCounter"}, {"com/gtnewhorizons/angelica/glsm/GLSMRasterizer", - "anyClipPlaneEnabled", "disableCull", "disableScissorTest", "enableCull", "enableScissorTest", - "getAutoNormalState", "getClipPlaneState", "getClipPlaneStates", "getCullState", "getDitherState", - "getEdgeFlagState", "getHintState", "getLineSmoothState", "getLineState", "getLineStippleState", - "getMap1Color4State", "getMap1IndexState", "getMap1NormalState", "getMap1TextureCoord1State", - "getMap1TextureCoord2State", "getMap1TextureCoord3State", "getMap1TextureCoord4State", - "getMap1Vertex3State", "getMap1Vertex4State", "getMap2Color4State", "getMap2IndexState", - "getMap2NormalState", "getMap2TextureCoord1State", "getMap2TextureCoord2State", "getMap2TextureCoord3State", - "getMap2TextureCoord4State", "getMap2Vertex3State", "getMap2Vertex4State", "getMultisampleState", - "getPointSmoothState", "getPointSpriteState", "getPointState", "getPolygonOffsetFillState", - "getPolygonOffsetLineState", "getPolygonOffsetPointState", "getPolygonSmoothState", "getPolygonState", - "getPolygonStippleState", "getSampleAlphaToCoverageState", "getSampleAlphaToOneState", - "getSampleCoverageState", "getScissorState", "getScissorTest", "getShadeModelState", "getViewportState", - "glClipPlane", "glCullFace", "glFrontFace", "glGetClipPlane", "glHint", "glLineStipple", "glLineWidth", - "glPointParameter", "glPointParameterf", "glPointParameteri", "glPointSize", "glPolygonMode", - "glPolygonOffset", "glPolygonOffsetClamp", "glPolygonStipple", "glSampleCoverage", "glScissor", - "glShadeModel", "glViewport"}, + "anyClipPlaneEnabled", "clipEmulationActive", "disableCull", "disableScissorTest", "enableCull", + "enableScissorTest", "getAutoNormalState", "getClipPlaneState", "getClipPlaneStates", "getCullState", + "getDitherState", "getEdgeFlagState", "getHintState", "getLineSmoothState", "getLineState", + "getLineStippleState", "getMap1Color4State", "getMap1IndexState", "getMap1NormalState", + "getMap1TextureCoord1State", "getMap1TextureCoord2State", "getMap1TextureCoord3State", + "getMap1TextureCoord4State", "getMap1Vertex3State", "getMap1Vertex4State", "getMap2Color4State", + "getMap2IndexState", "getMap2NormalState", "getMap2TextureCoord1State", "getMap2TextureCoord2State", + "getMap2TextureCoord3State", "getMap2TextureCoord4State", "getMap2Vertex3State", "getMap2Vertex4State", + "getMultisampleState", "getPointSmoothState", "getPointSpriteState", "getPointState", + "getPolygonOffsetFillState", "getPolygonOffsetLineState", "getPolygonOffsetPointState", + "getPolygonSmoothState", "getPolygonState", "getPolygonStippleState", "getSampleAlphaToCoverageState", + "getSampleAlphaToOneState", "getSampleCoverageState", "getScissorState", "getScissorTest", + "getShadeModelState", "getViewportState", "glClipPlane", "glCullFace", "glFrontFace", "glGetClipPlane", + "glHint", "glLineStipple", "glLineWidth", "glPointParameter", "glPointParameterf", "glPointParameteri", + "glPointSize", "glPolygonMode", "glPolygonOffset", "glPolygonOffsetClamp", "glPolygonStipple", + "glSampleCoverage", "glScissor", "glShadeModel", "glViewport"}, {"com/gtnewhorizons/angelica/glsm/GLSMTexEnvGen", "glGetTexEnv", "glGetTexEnvf", "glGetTexEnvi", "glGetTexGen", "glGetTexGend", "glGetTexGenf", "glGetTexGeni", "glTexEnv", "glTexEnvf", "glTexEnvi", "glTexGen", "glTexGend", "glTexGenf", "glTexGeni"}, diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java index 669488c99..2015ca421 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/BooleanState.java @@ -23,8 +23,12 @@ public class BooleanState implements ISettableState { this.ffpStateOnly = ffpStateOnly; } + protected boolean forwardsToBackend() { + return !ffpStateOnly; + } + public void applyToBackend() { - if (ffpStateOnly) return; + if (!forwardsToBackend()) return; if (enabled) { RENDER_BACKEND.enable(glCap); } else { @@ -51,7 +55,7 @@ public class BooleanState implements ISettableState { stateUnknown = false; this.enabled = enabled; onEnabledChanged(); - if (!ffpStateOnly) { + if (forwardsToBackend()) { if (enabled) { RENDER_BACKEND.enable(this.glCap); } else { diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/ClipPlaneState.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/ClipPlaneState.java index 27c91d2ef..dda6a5c42 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/ClipPlaneState.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/ClipPlaneState.java @@ -27,10 +27,16 @@ public class ClipPlaneState { dest[3] = tempPlane.w; } - /** Writes the 4-component eye-space plane equation into {@code buf} at its current position. */ - public void putEyePlane(int index, FloatBuffer buf) { - final float[] p = eyePlanes[index]; - buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]); + // Vulkan and clip emulation have no per-distance enable: a disabled plane must read as distance 0. + public void putEffectivePlanes(int enabledMask, FloatBuffer buf) { + for (int i = 0; i < eyePlanes.length; i++) { + if ((enabledMask & (1 << i)) != 0) { + final float[] p = eyePlanes[i]; + buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]); + } else { + buf.put(0f).put(0f).put(0f).put(0f); + } + } } public void getEyePlane(int index, DoubleBuffer buf) { diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLESCapsTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLESCapsTest.java index c3b875e39..d48aa824e 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLESCapsTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLESCapsTest.java @@ -40,7 +40,7 @@ class GLESCapsTest { }; private static boolean onGles(int cap) { - return GLESCaps.isCapAllowed(cap, true, true); + return GLESCaps.isCapAllowed(cap, true); } @Test @@ -60,25 +60,10 @@ class GLESCapsTest { @Test void desktopContextAllowsEveryCap() { for (int cap : DESKTOP_ONLY) { - assertTrue(GLESCaps.isCapAllowed(cap, false, false), () -> "cap 0x" + Integer.toHexString(cap) + " is legal on desktop core and the gate must never suppress it there"); + assertTrue(GLESCaps.isCapAllowed(cap, false), () -> "cap 0x" + Integer.toHexString(cap) + " is legal on desktop core and the gate must never suppress it there"); } for (int cap : VALID_ON_GLES) { - assertTrue(GLESCaps.isCapAllowed(cap, false, false)); + assertTrue(GLESCaps.isCapAllowed(cap, false)); } } - - @Test - void clipPlanesFollowClipCullDistance() { - for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) { - final int cap = GL11.GL_CLIP_PLANE0 + i; - assertTrue(GLESCaps.isCapAllowed(cap, true, true), () -> "clip plane " + cap + " works where EXT_clip_cull_distance is present and must not be suppressed"); - assertFalse(GLESCaps.isCapAllowed(cap, true, false), () -> "clip plane " + cap + " is invalid on GLES without EXT_clip_cull_distance"); - } - } - - @Test - void capsOutsideTheClipPlaneRangeAreUnaffectedByClipCullDistance() { - assertTrue(GLESCaps.isCapAllowed(GL11.GL_BLEND, true, false)); - assertTrue(GLESCaps.isCapAllowed(GL11.GL_CLIP_PLANE0 + GLStateManager.MAX_CLIP_PLANES, true, false)); - } } diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/ClipPlaneGLTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/ClipPlaneGLTest.java new file mode 100644 index 000000000..6872d09b4 --- /dev/null +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/ClipPlaneGLTest.java @@ -0,0 +1,124 @@ +package com.gtnewhorizons.angelica.glsm.ffp; + +import com.gtnewhorizons.angelica.glsm.GLCoreTest; +import com.gtnewhorizons.angelica.glsm.GLSMAttribStack; +import com.gtnewhorizons.angelica.glsm.GLSMBuffers; +import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs; +import com.gtnewhorizons.angelica.glsm.GLSMDraws; +import com.gtnewhorizons.angelica.glsm.GLSMEnable; +import com.gtnewhorizons.angelica.glsm.GLSMMatrices; +import com.gtnewhorizons.angelica.glsm.GLSMRasterizer; +import com.gtnewhorizons.angelica.glsm.GLSMVertexInput; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; + +import java.nio.FloatBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@GLCoreTest +class ClipPlaneGLTest { + + private static final int SIZE = 64; + private static final int BACKGROUND = 0xFF000000; + private static final int QUAD = 0xFF0000FF; + + @BeforeEach + void setUp() { + FfpFixture.resetFfpState(); + FfpFixture.activateFfp(); + GLSMRasterizer.glViewport(0, 0, SIZE, SIZE); + GLSMMatrices.glMatrixMode(GL11.GL_PROJECTION); + GLSMMatrices.glLoadIdentity(); + GLSMMatrices.glMatrixMode(GL11.GL_MODELVIEW); + GLSMMatrices.glLoadIdentity(); + GLSMEnable.glDisable(GL11.GL_DEPTH_TEST); + } + + @AfterEach + void tearDown() { + for (int i = 0; i < GLStateManager.MAX_CLIP_PLANES; i++) { + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0 + i); + } + FfpFixture.resetFfpState(); + } + + @ParameterizedTest(name = "emulated={0}") + @ValueSource(booleans = {false, true}) + void disabledPlaneDoesNotClip(boolean emulated) { + try (var ignored = ClipTestSupport.emulation(emulated)) { + ClipTestSupport.setPlane(1, 0, 0, 0, -1); + GLSMEnable.glEnable(GL11.GL_CLIP_PLANE1); + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1); + ClipTestSupport.setPlane(0, 1, 0, 0, 0); + GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0); + assertRightHalfOnly(drawQuad(), "plane1 disabled, plane0 enabled"); + } + } + + @ParameterizedTest(name = "emulated={0}") + @ValueSource(booleans = {false, true}) + void attribPopRestoresClipping(boolean emulated) { + try (var ignored = ClipTestSupport.emulation(emulated)) { + ClipTestSupport.setPlane(0, 1, 0, 0, 0); + GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0); + assertDriverClipPlane0(!emulated, "after glEnable"); + GLSMAttribStack.glPushAttrib(GL11.GL_ENABLE_BIT); + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0); + final int[] unclipped = drawQuad(); + GLSMAttribStack.glPopAttrib(); + assertDriverClipPlane0(!emulated, "after glPopAttrib"); + GLSMAttribStack.replayStateToBackend(); + assertDriverClipPlane0(!emulated, "after replayStateToBackend"); + final int[] restored = drawQuad(); + + for (int i = 0; i < unclipped.length; i++) { + final int px = i; + assertEquals(QUAD, unclipped[i], () -> "plane disabled inside the pushed scope, pixel " + px % SIZE + "," + px / SIZE); + } + assertRightHalfOnly(restored, "after glPopAttrib"); + } + } + + private static void assertDriverClipPlane0(boolean expected, String when) { + assertEquals(expected, GL11.glIsEnabled(GL11.GL_CLIP_PLANE0), () -> when + ": clip plane reaches the driver only when clip distances are native"); + } + + private static void assertRightHalfOnly(int[] pixels, String what) { + for (int i = 0; i < pixels.length; i++) { + final int x = i % SIZE, y = i / SIZE; + assertEquals(x < SIZE / 2 ? BACKGROUND : QUAD, pixels[i], () -> what + ": first mismatch at pixel " + x + "," + y); + } + } + + private static int[] drawQuad() { + final int vao = GLSMVertexInput.glGenVertexArrays(); + GLSMVertexInput.glBindVertexArray(vao); + final int vbo = GLSMBuffers.glGenBuffers(); + try { + GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo); + final FloatBuffer positions = BufferUtils.createFloatBuffer(18); + positions.put(new float[] { -1, -1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, 1, 1, 0, -1, 1, 0 }).flip(); + GLSMBuffers.glBufferData(GL15.GL_ARRAY_BUFFER, positions, GL15.GL_STATIC_DRAW); + FfpFixture.attrib(0, 3, GL11.GL_FLOAT, false, 12, 0); + GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + VAOManager.setCurrentVertexFlags(0); + + FfpFixture.clear(); + GLSMCurrentAttribs.glColor4f(1.0f, 0.0f, 0.0f, 1.0f); + GLSMDraws.glDrawArrays(GL11.GL_TRIANGLES, 0, 6); + GLSMVertexInput.glBindVertexArray(0); + return FfpFixture.readRegion(SIZE); + } finally { + GLSMBuffers.glDeleteBuffers(vbo); + GLSMVertexInput.glDeleteVertexArrays(vao); + } + } +} diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FFPUniformBlockSpirvLayoutTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FFPUniformBlockSpirvLayoutTest.java index 89fa0effc..c555200a5 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FFPUniformBlockSpirvLayoutTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FFPUniformBlockSpirvLayoutTest.java @@ -7,6 +7,7 @@ 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.PointerBuffer; import org.lwjgl.system.MemoryUtil; import org.lwjgl.util.shaderc.Shaderc; @@ -55,6 +56,30 @@ class FFPUniformBlockSpirvLayoutTest { MemoryUtil.memFree(result.spirv()); } + @ParameterizedTest(name = "emulated={0}") + @ValueSource(booleans = {false, true}) + void clipDistanceCapabilityFollowsEmulation(boolean emulated) { + long vbits = bit("BIT_CLIP_PLANES"); + if (emulated) vbits |= bit("BIT_CLIP_EMULATED"); + final String vs = VertexShaderGenerator.generate(VertexKey.fromPacked(vbits)); + assertEquals(!emulated, declaresClipDistance(vs, Shaderc.shaderc_vertex_shader), "vertex ClipDistance capability"); + } + + private static boolean declaresClipDistance(String glsl, int shaderKind) { + final SpirvCompiler.Result result = SpirvCompiler.compile(glsl, shaderKind, "clip-emulation-test", SpirvCompiler.Options.vulkanForced460Core()); + assertNotNull(result.spirv(), () -> "SPIR-V compile failed: " + result.error() + "\n" + glsl); + try { + final IntBuffer words = result.spirv().asIntBuffer(); + for (int i = 5; i < words.limit(); i += Math.max(1, words.get(i) >>> 16)) { + final int word = words.get(i); + if ((word & 0xFFFF) == 17 && words.get(i + 1) == 32) return true; + } + return false; + } finally { + MemoryUtil.memFree(result.spirv()); + } + } + private static VertexKey cubeKey(boolean lightmapUnit, boolean unit0TextureMatrix, boolean texGen) { long bits = bit("BIT_HAS_VERTEX_COLOR") | bit("BIT_HAS_VERTEX_NORMAL") | bit("BIT_HAS_VERTEX_TEX") | bit("BIT_UNIT_TEX_BASE"); if (lightmapUnit) bits |= 1L << (position("BIT_UNIT_TEX_BASE") + 1); diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FfpUniformBlockDedupTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FfpUniformBlockDedupTest.java index e1553bc3f..1f3d7f845 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FfpUniformBlockDedupTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/ffp/FfpUniformBlockDedupTest.java @@ -3,21 +3,21 @@ package com.gtnewhorizons.angelica.glsm.ffp; import com.gtnewhorizons.angelica.glsm.GLCoreTest; import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs; import com.gtnewhorizons.angelica.glsm.GLSMDepthStencilAlpha; +import com.gtnewhorizons.angelica.glsm.GLSMEnable; import com.gtnewhorizons.angelica.glsm.GLSMFog; import com.gtnewhorizons.angelica.glsm.GLSMLighting; import com.gtnewhorizons.angelica.glsm.GLSMMatrices; import com.gtnewhorizons.angelica.glsm.GLSMRasterizer; import com.gtnewhorizons.angelica.glsm.GLSMTexEnvGen; import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; -import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL30; import org.lwjgl.opengl.GL31; -import java.nio.DoubleBuffer; import java.util.ArrayList; import java.util.List; @@ -32,12 +32,6 @@ class FfpUniformBlockDedupTest { return GL30.glGetInteger(GL31.GL_UNIFORM_BUFFER_START, FFPUniformBlock.BINDING_POINT); } - private static DoubleBuffer doubles(double... values) { - final DoubleBuffer buf = BufferUtils.createDoubleBuffer(values.length); - buf.put(values).flip(); - return buf; - } - private static void resetState() { GLSMMatrices.glMatrixMode(GL11.GL_TEXTURE); GLSMMatrices.glLoadIdentity(); @@ -58,7 +52,8 @@ class FfpUniformBlockDedupTest { GLSMLighting.glLight(GL11.GL_LIGHT0, GL11.GL_DIFFUSE, floats(1.0f, 1.0f, 1.0f, 1.0f)); GLSMLighting.glMaterialf(GL11.GL_FRONT, GL11.GL_SHININESS, 0.0f); GLSMTexEnvGen.glTexGen(GL11.GL_S, GL11.GL_OBJECT_PLANE, floats(1.0f, 0.0f, 0.0f, 0.0f)); - GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, doubles(0.0, 0.0, 0.0, 0.0)); + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0); + ClipTestSupport.setPlane(0, 0.0, 0.0, 0.0, 0.0); GLSMRasterizer.glLineStipple(1, (short) 0xFFFF); GLSMRasterizer.glViewport(0, 0, 854, 480); } @@ -149,7 +144,10 @@ class FfpUniformBlockDedupTest { addCategory(tests, "currentTexCoord", () -> GLSMCurrentAttribs.glTexCoord2f(0.125f, 0.375f)); addCategory(tests, "lightmapCoord", () -> GLStateManager.ctx().lastBrightnessX = 96.0f); addCategory(tests, "texGenPlane", () -> GLSMTexEnvGen.glTexGen(GL11.GL_S, GL11.GL_OBJECT_PLANE, floats(0.5f, 0.0f, 0.0f, 0.25f))); - addCategory(tests, "clipPlane", () -> GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, doubles(0.0, 1.0, 0.0, 7.0))); + addCategory(tests, "clipPlane", () -> { + GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0); + ClipTestSupport.setPlane(0, 0.0, 1.0, 0.0, 7.0); + }); addCategory(tests, "alphaRef", () -> GLSMDepthStencilAlpha.glAlphaFunc(GL11.GL_GREATER, 0.35f)); addCategory(tests, "fogColor", () -> GLSMFog.glFog(GL11.GL_FOG_COLOR, floats(0.1f, 0.2f, 0.3f, 0.4f))); addCategory(tests, "fogDensity", () -> GLSMFog.glFogf(GL11.GL_FOG_DENSITY, 0.75f)); diff --git a/glsm/src/testFixtures/java/com/gtnewhorizons/angelica/glsm/testutil/ClipTestSupport.java b/glsm/src/testFixtures/java/com/gtnewhorizons/angelica/glsm/testutil/ClipTestSupport.java new file mode 100644 index 000000000..c542f9287 --- /dev/null +++ b/glsm/src/testFixtures/java/com/gtnewhorizons/angelica/glsm/testutil/ClipTestSupport.java @@ -0,0 +1,37 @@ +package com.gtnewhorizons.angelica.glsm.testutil; + +import com.gtnewhorizons.angelica.glsm.GLSMRasterizer; +import com.gtnewhorizons.angelica.glsm.RenderSystem; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.DoubleBuffer; + +public final class ClipTestSupport { + + private ClipTestSupport() {} + + public static void setPlane(int index, double a, double b, double c, double d) { + final DoubleBuffer eq = BufferUtils.createDoubleBuffer(4); + eq.put(a).put(b).put(c).put(d).flip(); + GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0 + index, eq); + } + + public static Emulation emulation(boolean emulated) { + return new Emulation(emulated); + } + + public static final class Emulation implements AutoCloseable { + private final boolean previous; + + private Emulation(boolean emulated) { + previous = Reflect.getStatic(RenderSystem.class, "clipDistanceEmulated"); + Reflect.setStatic(RenderSystem.class, "clipDistanceEmulated", emulated); + } + + @Override + public void close() { + Reflect.setStatic(RenderSystem.class, "clipDistanceEmulated", previous); + } + } +} diff --git a/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java b/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java index db48af785..b1999ebc6 100644 --- a/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java +++ b/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java @@ -288,7 +288,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { public void finish() {GL11C.glFinish();} private static boolean capAllowed(int cap) { - return GLESCaps.isCapAllowed(cap, RenderSystem.isGLES(), RenderSystem.hasClipCullDistance()); + return GLESCaps.isCapAllowed(cap, RenderSystem.isGLES()); } @Override diff --git a/lwjgl3-backend/src/test/java/com/gtnewhorizons/angelica/lwjgl3/GlesSmokeTest.java b/lwjgl3-backend/src/test/java/com/gtnewhorizons/angelica/lwjgl3/GlesSmokeTest.java index 2b79f23b2..c74c2b057 100644 --- a/lwjgl3-backend/src/test/java/com/gtnewhorizons/angelica/lwjgl3/GlesSmokeTest.java +++ b/lwjgl3-backend/src/test/java/com/gtnewhorizons/angelica/lwjgl3/GlesSmokeTest.java @@ -12,11 +12,14 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.RenderSystem; import com.gtnewhorizons.angelica.glsm.backend.BackendManager; import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig; +import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL11C; import org.lwjgl.opengl.GL12C; @@ -153,6 +156,32 @@ class GlesSmokeTest { assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); } + @ParameterizedTest(name = "emulated={0}") + @ValueSource(booleans = {false, true}) + void clipPlaneKeepsOnlyTheEnabledHalfSpace(boolean emulated) { + try (var ignored = ClipTestSupport.emulation(emulated)) { + try { + bindClearedTarget(); + ClipTestSupport.setPlane(1, 0.0, 0.0, 0.0, -1.0); + ClipTestSupport.setPlane(0, 1.0, 0.0, 0.0, 0.0); + GLSMEnable.glEnable(GL30C.GL_CLIP_DISTANCE0); + GLSMDraws.glBegin(GL11C.GL_QUADS); + GLSMCurrentAttribs.glColor4f(0.0f, 1.0f, 0.0f, 1.0f); + GLSMDraws.glVertex3f(-1.0f, -1.0f, 0.0f); + GLSMDraws.glVertex3f(1.0f, -1.0f, 0.0f); + GLSMDraws.glVertex3f(1.0f, 1.0f, 0.0f); + GLSMDraws.glVertex3f(-1.0f, 1.0f, 0.0f); + GLSMDraws.glEnd(); + + assertEquals(0xFF000000, readPixel(SIZE / 4, SIZE / 2), "x < 0 must be clipped by plane 0"); + assertEquals(0xFF00FF00, readPixel(3 * SIZE / 4, SIZE / 2), "x >= 0 must survive; plane 1 set but not enabled must not clip"); + assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); + } finally { + GLSMEnable.glDisable(GL30C.GL_CLIP_DISTANCE0); + } + } + } + @Test void proxyTextureProbeFindsTheMaxTextureSize() { final int maxSize = Integer.highestOneBit(GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE)); @@ -173,9 +202,13 @@ class GlesSmokeTest { } private static int readCenterPixel() { + return readPixel(SIZE / 2, SIZE / 2); + } + + private static int readPixel(int x, int y) { final ByteBuffer pixel = MemoryUtil.memAlloc(4); try { - GLSMFramebuffer.glReadPixels(SIZE / 2, SIZE / 2, 1, 1, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, pixel); + GLSMFramebuffer.glReadPixels(x, y, 1, 1, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, pixel); return (pixel.get(3) & 0xFF) << 24 | (pixel.get(0) & 0xFF) << 16 | (pixel.get(1) & 0xFF) << 8 | pixel.get(2) & 0xFF; } finally { MemoryUtil.memFree(pixel); diff --git a/scripts/flyby-scenes/clip-planes.txt b/scripts/flyby-scenes/clip-planes.txt new file mode 100644 index 000000000..031ba3445 --- /dev/null +++ b/scripts/flyby-scenes/clip-planes.txt @@ -0,0 +1,8 @@ +# Clip-plane scene for -Dangelica.debug.testBlocks=true. ~ is the flight start, yaw 0 faces south (+z). +# Each angelica:test_clip_plane TESR keeps only the lower half of a gold cube (plane 0) and leaves a reject-all plane 1 disabled. + +gamerule doMobSpawning false +gamerule doDaylightCycle false +time set 6000 + +fill ~-2 ~-1 ~4 ~2 ~0 ~4 angelica:test_clip_plane diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java index 55d36ad12..ceefa04d4 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java @@ -114,6 +114,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import org.lwjgl.util.spvc.Spvc; import org.lwjgl.sdl.SDLGPU; import org.lwjgl.sdl.SDL_GPUBlitInfo; import org.lwjgl.sdl.SDLTimer; @@ -2547,17 +2548,23 @@ public class SDLGPURenderBackend extends RenderBackend { if (length != null && length.remaining() > 0) length.put(0, 0); return; } - if (s == null) return; - final byte[] bytes = s.getBytes(StandardCharsets.UTF_8); - final int n = Math.min(bytes.length, out.remaining()); - out.put(bytes, 0, n); + final byte[] bytes = (s == null ? "" : s).getBytes(StandardCharsets.UTF_8); + final int pos = out.position(); + final int max = out.remaining(); + final int n = Math.max(0, Math.min(bytes.length, max - 1)); + for (int i = 0; i < n; i++) out.put(pos + i, bytes[i]); + if (max > 0) out.put(pos + n, (byte) 0); if (length != null && length.remaining() > 0) length.put(0, n); } + private static int glStringLength(String s) { + return s == null || s.isEmpty() ? 0 : s.getBytes(StandardCharsets.UTF_8).length + 1; + } @Override public int getShaderi(int shader, int pname) { return switch (pname) { case GL20.GL_COMPILE_STATUS -> shaderManager.isCompiled(shader) ? 1 : 0; case GL20.GL_SHADER_TYPE -> shaderManager.getShaderType(shader); - case GL20.GL_INFO_LOG_LENGTH -> shaderManager.getShaderInfoLog(shader).length(); + case GL20.GL_INFO_LOG_LENGTH -> glStringLength(shaderManager.getShaderInfoLog(shader)); + case GL20.GL_SHADER_SOURCE_LENGTH -> glStringLength(shaderManager.getShaderSource(shader)); default -> 0; }; } @@ -2565,7 +2572,7 @@ public class SDLGPURenderBackend extends RenderBackend { return switch (pname) { case GL20.GL_LINK_STATUS -> shaderManager.isLinked(program) ? 1 : 0; case GL20.GL_VALIDATE_STATUS -> shaderManager.isLinked(program) ? 1 : 0; - case GL20.GL_INFO_LOG_LENGTH -> shaderManager.getProgramInfoLog(program).length(); + case GL20.GL_INFO_LOG_LENGTH -> glStringLength(shaderManager.getProgramInfoLog(program)); case GL20.GL_ATTACHED_SHADERS -> shaderManager.getAttachedShaderCount(program); case GL20.GL_ACTIVE_UNIFORMS -> { final var entries = getOrBuildUniformEntries(program); @@ -2605,6 +2612,7 @@ public class SDLGPURenderBackend extends RenderBackend { final int loc = e.getValue(); int glType; int arrayLen = 1; + boolean array = false; if (prog.allSamplerNames.contains(name)) { glType = GL20.GL_SAMPLER_2D; } else { @@ -2613,6 +2621,7 @@ public class SDLGPURenderBackend extends RenderBackend { if (umi != null) { glType = FormatMap.mapMemberInfoToGlType(umi); arrayLen = Math.max(1, umi.arrayLen()); + array = umi.arrayStride() > 0; } else { glType = GL20.GL_FLOAT_VEC4; } @@ -2620,7 +2629,7 @@ public class SDLGPURenderBackend extends RenderBackend { glType = FormatMap.remapUnsignedIntToBool(glType); } } - result.add(new UniformEntry(name, glType, arrayLen)); + result.add(new UniformEntry(array ? name + "[0]" : name, glType, arrayLen)); } result.sort(Comparator.comparing(UniformEntry::name)); final List immutable = Collections.unmodifiableList(result); @@ -2679,6 +2688,78 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public int getUniformLocation(int program, ByteBuffer name) { return shaderManager.getUniformLocation(program, MemoryUtil.memASCII(name)); } + @Override public void getUniformfv(int program, int location, FloatBuffer params) { + final Readback rb = readback(program, location); + if (rb == null) return; + final int pos = params.position(); + final int count = Math.min(rb.count(), params.remaining()); + for (int i = 0; i < count; i++) { + final float raw = rb.raw(i); + final int bits = Float.floatToRawIntBits(raw); + params.put(pos + i, switch (rb.type()) { + case FLOAT -> raw; + case INT -> (float) bits; + case UINT -> (float) (bits & 0xFFFFFFFFL); + case BOOL -> bits != 0 ? 1f : 0f; + case OPAQUE -> rb.opaqueValue(); + }); + } + } + @Override public void getUniformiv(int program, int location, IntBuffer params) { + final Readback rb = readback(program, location); + if (rb == null) return; + final int pos = params.position(); + final int count = Math.min(rb.count(), params.remaining()); + for (int i = 0; i < count; i++) { + final float raw = rb.raw(i); + final int bits = Float.floatToRawIntBits(raw); + params.put(pos + i, switch (rb.type()) { + case FLOAT -> roundHalfAwayFromZero(raw); + case BOOL -> bits != 0 ? 1 : 0; + case INT, UINT -> bits; + case OPAQUE -> rb.opaqueValue(); + }); + } + } + private enum ReadType { FLOAT, INT, UINT, BOOL, OPAQUE } + private record Readback(float[] storage, int start, int count, ReadType type, int opaqueValue) { + float raw(int i) { + return storage != null && start + i < storage.length ? storage[start + i] : 0f; + } + } + private Readback readback(int program, int location) { + final ShaderManager.ProgramObject prog = shaderManager.getProgram(program); + if (prog == null || !prog.linked || location < 0 || location >= prog.uniformSlotCount) return null; + final int kind = prog.locationKind[location]; + if (kind != ShaderManager.ProgramObject.LOCATION_KIND_PLAIN) { + return new Readback(null, 0, 1, ReadType.OPAQUE, opaqueUnit(prog, location, kind)); + } + final ShaderManager.UniformMemberInfo info = prog.memberInfo(location); + if (info == null) return null; + final int perElement = info.vectorSize() * info.columns(); + final int index = prog.arrayIndexBySlot[location]; + final int base = location - index; + final float[] storage = s().uniformStaging(prog).uniformDataBySlot[base]; + return new Readback(storage, index * perElement, perElement, readType(prog, base, info), 0); + } + private static ReadType readType(ShaderManager.ProgramObject prog, int base, ShaderManager.UniformMemberInfo info) { + if (info.baseType() == Spvc.SPVC_BASETYPE_FP32) return ReadType.FLOAT; + if (info.baseType() != Spvc.SPVC_BASETYPE_UINT32) return ReadType.INT; + final String name = prog.locationName[base]; + return name != null && prog.boolUniforms.contains(name) ? ReadType.BOOL : ReadType.UINT; + } + private static int roundHalfAwayFromZero(float f) { + final double r = Math.floor(Math.abs((double) f) + 0.5); + return (int) (f < 0 ? -r : r); + } + private static int opaqueUnit(ShaderManager.ProgramObject prog, int location, int kind) { + final String name = prog.locationName[location]; + if (name == null) return 0; + final int unit = kind == ShaderManager.ProgramObject.LOCATION_KIND_SAMPLER + ? prog.samplerTextureUnits.getInt(name) + : prog.imageTextureUnits.getInt(name); + return Math.max(unit, 0); + } @Override public void getShaderSource(int shader, IntBuffer length, ByteBuffer source) { writeUtf8(shaderManager.getShaderSource(shader), length, source); } @@ -4131,6 +4212,7 @@ public class SDLGPURenderBackend extends RenderBackend { case GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -> 32; case GL20.GL_MAX_VERTEX_ATTRIBS -> 16; case GL20.GL_MAX_DRAW_BUFFERS -> 8; + case GL30.GL_MAX_CLIP_DISTANCES -> device.hasClipDistance() ? GLStateManager.MAX_CLIP_PLANES : 0; case GL30.GL_MAX_COLOR_ATTACHMENTS -> 8; case GL15.GL_ARRAY_BUFFER_BINDING -> cs.boundArrayBuffer; case GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING -> cs.currentVao.elementBuffer; diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java index 5f8f73317..af36559fb 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/device/Device.java @@ -45,6 +45,7 @@ public final class Device { private long device; private boolean claimed; + private boolean clipDistance = true; private int supportedShaderFormats; private String driverName; private boolean fencePollEnabled; @@ -116,6 +117,10 @@ public final class Device { } public boolean createDevice() { + return createDevice(!SystemProperties.EMULATE_CLIP_DISTANCES); + } + + boolean createDevice(boolean requestClipDistance) { if (device != 0) return true; installLogCallback(); @@ -130,9 +135,9 @@ public final class Device { } final boolean gpuDebug = SystemProperties.LWJGL_DEBUG || SystemProperties.SDL_GPU_DEBUG; - device = createGPUDevice(gpuDebug, candidates); + device = createGPUDevice(gpuDebug, candidates, requestClipDistance); if (device == 0 && VideoDriverRecovery.shouldRetryOnX11(SDL_GetCurrentVideoDriver(), vulkanLoaderAvailable(), System.getenv("DISPLAY"))) { - device = VideoDriverRecovery.retryUnderX11(() -> createGPUDevice(gpuDebug, candidates)); + device = VideoDriverRecovery.retryUnderX11(() -> createGPUDevice(gpuDebug, candidates, requestClipDistance)); } if (device == 0) { logDeviceCreationFailure(REQUESTED_SHADER_FORMATS); @@ -206,13 +211,22 @@ public final class Device { } } - private long createGPUDevice(boolean gpuDebug, String[] candidates) { + private long createGPUDevice(boolean gpuDebug, String[] candidates, boolean requestClipDistance) { final boolean macos = Platform.get() == Platform.MACOSX; for (final String driver : candidates) { final boolean macVulkan = macos && isVulkan(driver); if (macVulkan) useMoltenVk(); - final long dev = SDL_CreateGPUDevice(REQUESTED_SHADER_FORMATS, gpuDebug, driver); - if (dev != 0) return dev; + long dev = createWithProperties(gpuDebug, driver, requestClipDistance); + boolean clip = requestClipDistance; + if (dev == 0 && clip && isVulkan(driver)) { + LOG.info("SDL GPU Vulkan device creation failed with shaderClipDistance ({}); the device may lack it, retrying without it, clip distances will be emulated", SDLError.SDL_GetError()); + dev = createWithProperties(gpuDebug, driver, false); + clip = false; + } + if (dev != 0) { + clipDistance = clip; + return dev; + } LOG.warn("Failed to create a '{}' SDL GPU device: {}", driver, SDLError.SDL_GetError()); if (macVulkan && MoltenVK.locate() == null) { LOG.warn("Vulkan on macOS needs MoltenVK. Add {} to the mods folder or install MoltenVK.", MoltenVK.JAR_NAME); @@ -221,6 +235,22 @@ public final class Device { return 0; } + private static long createWithProperties(boolean gpuDebug, String driver, boolean clipDistance) { + final int props = SDLProperties.SDL_CreateProperties(); + try { + SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_SPIRV_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_SPIRV) != 0); + SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXBC_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_DXBC) != 0); + SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_DXIL_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_DXIL) != 0); + SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_SHADERS_MSL_BOOLEAN, (REQUESTED_SHADER_FORMATS & SDL_GPU_SHADERFORMAT_MSL) != 0); + SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_DEBUGMODE_BOOLEAN, gpuDebug); + SDLProperties.SDL_SetStringProperty(props, SDL_PROP_GPU_DEVICE_CREATE_NAME_STRING, driver); + SDLProperties.SDL_SetBooleanProperty(props, SDL_PROP_GPU_DEVICE_CREATE_FEATURE_CLIP_DISTANCE_BOOLEAN, clipDistance); + return SDL_CreateGPUDeviceWithProperties(props); + } finally { + SDLProperties.SDL_DestroyProperties(props); + } + } + public void claimWindow(long window) { if (device == 0) { throw new IllegalStateException("SDL GPU device not created"); @@ -298,6 +328,7 @@ public final class Device { driverVersion = null; driverInfo = null; supportedShaderFormats = 0; + clipDistance = true; } private void logWindowDiagnostics(long window) { @@ -351,6 +382,10 @@ public final class Device { return device; } + public boolean hasClipDistance() { + return clipDistance; + } + public String getDriverName() { return driverName; } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java index 124a87967..e5accb628 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java @@ -11,6 +11,7 @@ import com.gtnewhorizons.angelica.sdlgpu.shader.LogicOpVariant; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager; import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging; import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.longs.Long2FloatOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; @@ -183,6 +184,7 @@ public final class ContextState { private final Reference2ObjectOpenHashMap uniformStagingByProgram = new Reference2ObjectOpenHashMap<>(); private ShaderManager.ProgramObject stagingProg; private UniformStaging currentUniformStaging; + public final Int2ObjectOpenHashMap uniformArrayScratch = new Int2ObjectOpenHashMap<>(); public UniformStaging uniformStaging(ShaderManager.ProgramObject prog) { if (prog == stagingProg) return currentUniformStaging; diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PipelineApplier.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PipelineApplier.java index e83653fee..cf53cc80d 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PipelineApplier.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/PipelineApplier.java @@ -754,16 +754,32 @@ public final class PipelineApplier { public float[] reuseOrAlloc(ContextState st, int location, int length) { final ShaderManager.ProgramObject prog = st.boundProgramObj; if (prog == null || location < 0 || location >= prog.uniformSlotCount) return null; + if (prog.floatLimitBySlot[location] < 0) return arrayScratch(st, length); final float[] existing = st.uniformStaging(prog).uniformDataBySlot[location]; if (existing != null && existing.length == length) return existing; return new float[length]; } + private static float[] arrayScratch(ContextState st, int length) { + float[] scratch = st.uniformArrayScratch.get(length); + if (scratch == null) { + scratch = new float[length]; + st.uniformArrayScratch.put(length, scratch); + } + return scratch; + } + public void putUniformFv(ContextState st, int location, FloatBuffer value) { final ShaderManager.ProgramObject prog = st.boundProgramObj; if (prog == null || location < 0 || location >= prog.uniformSlotCount) return; final int n = value.remaining(); final int pos = value.position(); + if (prog.floatLimitBySlot[location] < 0) { + final float[] v = arrayScratch(st, n); + value.get(pos, v); + putUniform(st, location, v); + return; + } final float[] existing = st.uniformStaging(prog).uniformDataBySlot[location]; if (existing != null && existing.length == n) { if (contentEquals(existing, value, pos, n)) { @@ -824,6 +840,7 @@ public final class PipelineApplier { if (Tracy.ENABLED) frameManager.noteUniformBlockFlush(b, size); } + private static final long ARRAY_HASH_SEED = 0x9E3779B97F4A7C15L; private static final boolean VERIFY_PER_FRAME_BLOCK = SystemProperties.SDL_VERIFY_PER_FRAME_UNIFORM_BLOCK; private final HashSet perFrameBlockDriftWarned = new HashSet<>(); @@ -837,6 +854,12 @@ public final class PipelineApplier { public void putUniform(ContextState st, int location, float[] data) { final ShaderManager.ProgramObject prog = st.boundProgramObj; if (prog == null || location < 0 || location >= prog.uniformSlotCount) return; + final int limit = prog.floatLimitBySlot[location]; + if (limit < 0) { + putArrayElement(st, prog, st.uniformStaging(prog), location - prog.arrayIndexBySlot[location], location, data); + return; + } + if (data.length > limit) return; final UniformStaging us = st.uniformStaging(prog); final int len = data.length; final long valueKey; @@ -884,6 +907,62 @@ public final class PipelineApplier { } return; } + writeStages(st, prog, us, location, data); + } + + private void putArrayElement(ContextState st, ShaderManager.ProgramObject prog, UniformStaging us, int base, int location, float[] data) { + final ShaderManager.UniformMemberInfo baseInfo = prog.memberInfo(base); + if (baseInfo == null) return; + final int len = prog.arrayLenBySlot[location]; + final int index = prog.arrayIndexBySlot[location]; + final int count = Math.min(elementCount(baseInfo, data), len - index); + if (count <= 0) return; + final int floatsPerElement = baseInfo.vectorSize() * baseInfo.columns(); + if (Tracy.ENABLED) st.slotWrites++; + final int start = index * floatsPerElement; + final int floats = count * floatsPerElement; + float[] storage = us.uniformDataBySlot[base]; + final boolean fresh = storage == null; + long storageHash; + if (fresh) { + storage = us.uniformDataBySlot[base] = new float[len * floatsPerElement]; + storageHash = ARRAY_HASH_SEED; + } else { + storageHash = us.uniformValueHashBySlot[base]; + } + boolean changed = fresh; + for (int i = 0; i < floats; i++) { + final int oldBits = Float.floatToRawIntBits(storage[start + i]); + final int newBits = Float.floatToRawIntBits(data[i]); + if (oldBits == newBits) continue; + storageHash ^= arrayHashTerm(start + i, oldBits) ^ arrayHashTerm(start + i, newBits); + storage[start + i] = data[i]; + changed = true; + } + if (!changed) { + if (Tracy.ENABLED) st.slotWritesElided++; + return; + } + us.uniformValueHashBySlot[base] = storageHash; + for (int b = 0; b < ShaderManager.BLOCK_COUNT; b++) { + final ShaderManager.UniformMemberInfo info = prog.blockInfoBySlot[b][location]; + if (info == null) continue; + final ContextState.UniformBlockState block = st.uniformBlocks[b]; + if (prog.blockSize[b] > block.size) block.size = prog.blockSize[b]; + final FloatBuffer blockFb = block.staging(prog.blockSize[b]); + final int hashSlot = block.hashSlot(prog.blockInfoBySlot[b][base].offset()); + if (hashSlot >= 0) block.setHash(hashSlot, storageHash); + writeMember(st, prog, blockFb, prog.blockSize[b], info, data, location, false); + block.dirty = true; + if (VERIFY_PER_FRAME_BLOCK && b == ShaderManager.BLOCK_PER_FRAME && block.flushedThisFrame) { + notePerFrameBlockDrift(prog, base); + } + return; + } + writeStages(st, prog, us, location, data); + } + + private void writeStages(ContextState st, ShaderManager.ProgramObject prog, UniformStaging us, int location, float[] data) { final ShaderManager.UniformMemberInfo vs = prog.vsInfoBySlot[location]; if (vs != null && us.vsUniformFb != null) { writeMember(st, prog, us.vsUniformFb, prog.vertexUboSize, vs, data, location, true); @@ -896,6 +975,10 @@ public final class PipelineApplier { } } + private static long arrayHashTerm(int index, int bits) { + return bits == 0 ? 0L : Hashing.fmix64(ARRAY_HASH_SEED, Hashing.packHiLo(index, bits)); + } + public void storeMatrix(ContextState st, int location, boolean transpose, FloatBuffer value, int size) { if (location < 0) return; final int n = value.remaining(); @@ -914,16 +997,19 @@ public final class PipelineApplier { putUniform(st, location, out); } - private void writeMember(ContextState st, ShaderManager.ProgramObject prog, FloatBuffer ubo, int uboSize, ShaderManager.UniformMemberInfo info, float[] data, int loc, boolean isVs) { + private static int elementCount(ShaderManager.UniformMemberInfo info, float[] data) { final int floatsPerElement = info.vectorSize() * info.columns(); - if (floatsPerElement <= 0) return; - final int srcCount = data.length / floatsPerElement; - if (srcCount <= 0) return; - final int dataBytes = srcCount * floatsPerElement * 4; + return floatsPerElement <= 0 ? 0 : data.length / floatsPerElement; + } + + private void writeMember(ContextState st, ShaderManager.ProgramObject prog, FloatBuffer ubo, int uboSize, ShaderManager.UniformMemberInfo info, float[] data, int loc, boolean isVs) { + final int count = Math.min(elementCount(info, data), info.arrayLen()); + if (count <= 0) return; + final int dataBytes = count * info.vectorSize() * info.columns() * 4; final int avail = uboSize - info.offset(); if (dataBytes > avail) { throw new IllegalStateException("Uniform overflows UBO: prog=" + st.boundProgram + " loc=" + loc + " name='" + prog.locationToName.getOrDefault(loc, "?") + "' dataBytes=" + dataBytes + " availFromOffset=" + avail + " stage=" + (isVs ? "VS" : "FS")); } - Std140Writer.write(ubo, info, data, srcCount); + Std140Writer.write(ubo, info, data, count); } } 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 14bcc76bc..95ab1b32b 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 @@ -777,7 +777,7 @@ public final class ShaderManager { else prog.fragmentUboSize = refl.uboSize(); } for (UboMember m : refl.uboMembers()) { - final int loc = ensureLocation(prog, m.name()); + final int loc = ensureLocation(prog, m.name(), m.arrayLen()); final UniformMemberInfo info = new UniformMemberInfo( m.offset(), m.size(), m.arrayStride(), isVertex, m.vectorSize(), m.columns(), m.baseType(), m.arrayLen() @@ -795,7 +795,7 @@ public final class ShaderManager { prog.blockSize[b] = Math.max(prog.blockSize[b], block.size()); prog.blockBinding[b] = block.binding(); for (UboMember m : block.members()) { - final int loc = ensureLocation(prog, m.name()); + final int loc = ensureLocation(prog, m.name(), m.arrayLen()); if (prog.blockMemberInfo[b].containsKey(loc)) continue; prog.blockMemberInfo[b].put(loc, new UniformMemberInfo( m.offset(), m.size(), m.arrayStride(), false, @@ -809,14 +809,34 @@ public final class ShaderManager { } private static int ensureLocation(ProgramObject prog, String name) { + return ensureLocation(prog, name, 1); + } + + private static int ensureLocation(ProgramObject prog, String name, int arrayLen) { final int existing = prog.nameToLocation.getInt(name); - if (existing != -1) return existing; - final int loc = prog.nextUniformLocation++; + if (existing != -1) { + final int registered = Math.max(1, prog.arrayLenByLocation.get(existing)); + if (registered == arrayLen) return existing; + retireLocations(prog, existing, registered); + } + final int loc = prog.nextUniformLocation; + prog.nextUniformLocation += arrayLen; prog.nameToLocation.put(name, loc); prog.locationToName.put(loc, name); + for (int i = 1; i < arrayLen; i++) prog.locationToName.put(loc + i, name + "[" + i + "]"); + if (arrayLen > 1) prog.arrayLenByLocation.put(loc, arrayLen); return loc; } + private static void retireLocations(ProgramObject prog, int base, int len) { + prog.arrayLenByLocation.remove(base); + for (int i = 0; i < len; i++) prog.locationToName.remove(base + i); + prog.locationToMemberInfo.remove(base); + prog.vertexMemberInfo.remove(base); + prog.fragmentMemberInfo.remove(base); + for (int b = 0; b < BLOCK_COUNT; b++) prog.blockMemberInfo[b].remove(base); + } + private static void applyVaryingMatch(ByteBuffer fragmentSpirv, StageReflection vs, StageReflection fs) { if (vs.vsOutputs().isEmpty() || fs.fsInputs().isEmpty() || fragmentSpirv == null) return; final HashMap vsOutLocs = new HashMap<>(vs.vsOutputs().size() * 2); @@ -1268,7 +1288,13 @@ public final class ShaderManager { public int getUniformLocation(int program, String name) { final ProgramObject prog = programObjects.get(program); if (prog == null || !prog.linked) return -1; - return prog.nameToLocation.getInt(name); + final int exact = prog.nameToLocation.getInt(name); + if (exact != -1) return exact; + final int index = UniformNames.arrayIndex(name); + if (index < 0) return -1; + final int base = prog.nameToLocation.getInt(name.substring(0, name.lastIndexOf('['))); + if (base < 0 || base >= prog.uniformSlotCount || index >= prog.arrayLenBySlot[base]) return -1; + return base + index; } /** @@ -1645,7 +1671,12 @@ public final class ShaderManager { public record UniformMemberInfo( int offset, int size, int arrayStride, boolean isVertex, int vectorSize, int columns, int baseType, int arrayLen - ) {} + ) { + public UniformMemberInfo element(int i) { + return new UniformMemberInfo(offset + i * arrayStride, size - i * arrayStride, arrayStride, isVertex, + vectorSize, columns, baseType, arrayLen - i); + } + } public record UboMember( String name, int offset, int size, int arrayStride, @@ -2069,11 +2100,24 @@ public final class ShaderManager { return m != null ? m : blockMemberInfo[BLOCK_PER_PASS].get(loc); } + public UniformMemberInfo memberInfo(int slot) { + for (int b = 0; b < BLOCK_COUNT; b++) { + final UniformMemberInfo info = blockInfoBySlot[b][slot]; + if (info != null) return info; + } + final UniformMemberInfo vs = vsInfoBySlot[slot]; + return vs != null ? vs : fsInfoBySlot[slot]; + } + private static final UniformMemberInfo[] EMPTY_INFO = new UniformMemberInfo[0]; public UniformMemberInfo[] vsInfoBySlot; public UniformMemberInfo[] fsInfoBySlot; public int uniformSlotCount; + public final Int2IntOpenHashMap arrayLenByLocation = new Int2IntOpenHashMap(); + public int[] arrayIndexBySlot = EMPTY_UNITS; + public int[] arrayLenBySlot = EMPTY_UNITS; + public int[] floatLimitBySlot = EMPTY_UNITS; public final int[] vertexInputBaseType = new int[16]; public final int[] vertexInputVecSize = new int[16]; public final String[] vertexInputName = new String[16]; @@ -2200,24 +2244,51 @@ public final class ShaderManager { locationKind[loc] = LOCATION_KIND_PLAIN; } } + arrayIndexBySlot = new int[n]; + arrayLenBySlot = new int[n]; + Arrays.fill(arrayLenBySlot, 1); + for (var it = arrayLenByLocation.int2IntEntrySet().fastIterator(); it.hasNext(); ) { + final var e = it.next(); + final int base = e.getIntKey(); + final int len = e.getIntValue(); + for (int i = 0; i < len; i++) { + arrayLenBySlot[base + i] = len; + arrayIndexBySlot[base + i] = i; + } + } for (int b = 0; b < BLOCK_COUNT; b++) { blockInfoBySlot[b] = new UniformMemberInfo[n]; for (var it = blockMemberInfo[b].int2ObjectEntrySet().fastIterator(); it.hasNext(); ) { final var e = it.next(); - final int k = e.getIntKey(); - if (k >= 0 && k < n) blockInfoBySlot[b][k] = e.getValue(); + fillSlots(blockInfoBySlot[b], e.getIntKey(), e.getValue()); } } for (var it = vertexMemberInfo.int2ObjectEntrySet().fastIterator(); it.hasNext(); ) { final var e = it.next(); - final int k = e.getIntKey(); - if (k >= 0 && k < n) vsInfoBySlot[k] = e.getValue(); + fillSlots(vsInfoBySlot, e.getIntKey(), e.getValue()); } for (var it = fragmentMemberInfo.int2ObjectEntrySet().fastIterator(); it.hasNext(); ) { final var e = it.next(); - final int k = e.getIntKey(); - if (k >= 0 && k < n) fsInfoBySlot[k] = e.getValue(); + fillSlots(fsInfoBySlot, e.getIntKey(), e.getValue()); } + floatLimitBySlot = new int[n]; + for (int slot = 0; slot < n; slot++) { + if (arrayLenBySlot[slot] > 1) { + floatLimitBySlot[slot] = -1; + continue; + } + final UniformMemberInfo info = memberInfo(slot); + final int floats = info == null || info.arrayStride() > 0 ? 0 : info.vectorSize() * info.columns(); + floatLimitBySlot[slot] = floats > 0 ? floats : Integer.MAX_VALUE; + } + } + + private void fillSlots(UniformMemberInfo[] bySlot, int base, UniformMemberInfo info) { + final int n = bySlot.length; + if (base < 0 || base >= n) return; + bySlot[base] = info; + final int len = arrayLenBySlot[base]; + for (int i = 1; i < len; i++) bySlot[base + i] = info.element(i); } } } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/UniformNames.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/UniformNames.java new file mode 100644 index 000000000..2038b5f86 --- /dev/null +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/UniformNames.java @@ -0,0 +1,25 @@ +package com.gtnewhorizons.angelica.sdlgpu.shader; + +public final class UniformNames { + + private UniformNames() {} + + public static int arrayIndex(String name) { + final int len = name.length(); + if (len == 0 || name.charAt(len - 1) != ']') return -1; + int i = len - 1; + while (i > 0 && isDigit(name.charAt(i - 1))) i--; + if (i == 0 || name.charAt(i - 1) != '[') return -1; + if (name.charAt(i) == '0' && i + 1 != len - 1) return -1; + long index = 0; + for (int c = i; c < len - 1; c++) { + index = index * 10 + (name.charAt(c) - '0'); + if (index > Integer.MAX_VALUE) return Integer.MAX_VALUE; + } + return (int) index; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } +} diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java index 109cdb8ef..6d8bc74ca 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/device/DeviceLifecycleGpuTest.java @@ -2,11 +2,13 @@ package com.gtnewhorizons.angelica.sdlgpu.device; import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; 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; @@ -46,4 +48,12 @@ class DeviceLifecycleGpuTest { device.destroyDevice(); assertEquals(0L, device.getDevice()); } + + @Test + void createsVulkanDeviceWithoutClipDistance() { + device = new Device(); + assertTrue(device.createDevice(false), "no SDL GPU device on this machine"); + Assumptions.assumeTrue(device.isVulkan(), "test needs the vulkan driver, got " + device.getDriverName()); + assertFalse(device.hasClipDistance()); + } } diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFfpTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFfpTest.java index 37cfb04c9..c312fcdd8 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFfpTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFfpTest.java @@ -10,6 +10,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.CubeParityFixture; import com.gtnewhorizons.angelica.glsm.ffp.FfpFixture; import com.gtnewhorizons.angelica.glsm.ffp.Instancing; import com.gtnewhorizons.angelica.glsm.ffp.ParticleParityFixture; +import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; @@ -127,6 +128,30 @@ class GlsmSdlFfpTest { assertTexelEverywhere("two units, glCallLists"); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void disabledClipPlaneDoesNotClip(boolean emulated) { + try (var ignored = ClipTestSupport.emulation(emulated)) { + bindBaseTexture(); + try { + ClipTestSupport.setPlane(1, 0.0, 0.0, 0.0, -1.0); + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1); + ClipTestSupport.setPlane(0, 1.0, 0.0, 0.0, 0.0); + GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0); + quadUnit0Only(); + + final int[] pixels = GlsmSdlHeadlessRig.readTarget(); + final int left = pixelAt(pixels, SIZE, SIZE / 4, SIZE / 2); + final int right = pixelAt(pixels, SIZE, 3 * SIZE / 4, SIZE / 2); + assertTrue(left != TEXEL, () -> "left quarter must be clipped by enabled plane 0: " + describe(left)); + assertEquals(TEXEL, right, () -> "right quarter must survive; disabled plane 1 must not clip: " + describe(right)); + } finally { + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0); + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1); + } + } + } + @Test void litTexturedQuadIsShaded() { bindBaseTexture(); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlUniformArrayTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlUniformArrayTest.java new file mode 100644 index 000000000..3a73a116e --- /dev/null +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlUniformArrayTest.java @@ -0,0 +1,246 @@ +package com.gtnewhorizons.angelica.sdlgpu.glsm; + +import com.gtnewhorizons.angelica.glsm.GLSMDraws; +import com.gtnewhorizons.angelica.glsm.GLSMPrograms; +import com.gtnewhorizons.angelica.glsm.GLSMVertexInput; +import com.gtnewhorizons.angelica.glsm.GlTestSupport; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@Tag("glsm-sdl") +@ExtendWith(GlsmSdlFrameExtension.class) +class GlsmSdlUniformArrayTest { + + private static final int COUNT = 8; + private static final int[][] A = { + {10, 20, 30}, {40, 50, 60}, {70, 80, 90}, {100, 110, 120}, + {130, 140, 150}, {160, 170, 180}, {190, 200, 210}, {220, 230, 240} + }; + private static final int[] X = {250, 5, 15}; + private static final int[][] B = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {11, 12, 13}}; + + private int program; + private int vao; + private int arr; + private int single; + private int pick; + + @BeforeEach + void setUp() { + program = GlTestSupport.program(GlsmSdlHeadlessRig.FULLSCREEN_VERTEX, """ + #version 330 core + uniform vec4 arr[8]; + uniform vec4 single; + uniform ivec4 iarr[2]; + uniform vec4 one[1]; + uniform int pick; + out vec4 color; + void main() { color = pick == -2 ? vec4(iarr[1]) / 255.0 : pick == -3 ? one[0] : pick < 0 ? single : arr[pick]; } + """); + vao = GLSMVertexInput.glGenVertexArrays(); + GLSMVertexInput.glBindVertexArray(vao); + GLSMPrograms.glUseProgram(program); + arr = loc("arr"); + single = loc("single"); + pick = loc("pick"); + } + + @AfterEach + void tearDown() { + GLSMPrograms.glUseProgram(0); + GLSMVertexInput.glBindVertexArray(0); + GLSMVertexInput.glDeleteVertexArrays(vao); + GLSMPrograms.glDeleteProgram(program); + } + + private int loc(String name) { + return GLSMPrograms.glGetUniformLocation(program, name); + } + + private static int argb(int[] rgb) { + return 0xFF000000 | (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + } + + private static float[] vec(int[] rgb) { + return new float[] {rgb[0] / 255f, rgb[1] / 255f, rgb[2] / 255f, 1f}; + } + + private static FloatBuffer pack(int[]... colors) { + final FloatBuffer buf = MemoryUtil.memAllocFloat(colors.length * 4); + for (int[] c : colors) buf.put(vec(c)); + buf.flip(); + return buf; + } + + private void writeAt(int location, int[]... colors) { + final FloatBuffer buf = pack(colors); + try { + GLSMPrograms.glUniform4(location, buf); + } finally { + MemoryUtil.memFree(buf); + } + } + + private void writeElement(int location, int[] rgb) { + final float[] v = vec(rgb); + GLSMPrograms.glUniform4f(location, v[0], v[1], v[2], v[3]); + } + + private void writeAll() { + writeAt(arr, A); + } + + private int drawPick(int index) { + GlsmSdlHeadlessRig.beginFrame(); + GlsmSdlHeadlessRig.bindTarget(); + GLSMPrograms.glUniform1i(pick, index); + GLSMDraws.glDrawArrays(GL11.GL_TRIANGLES, 0, 3); + return GlsmSdlHeadlessRig.readTarget(GlsmSdlHeadlessRig.SIZE / 2, GlsmSdlHeadlessRig.SIZE / 2, 1, 1)[0]; + } + + private void assertPick(int index, int[] expected, String label) { + final int got = drawPick(index); + assertEquals(Integer.toHexString(argb(expected)), Integer.toHexString(got), label + ": " + GlsmSdlHeadlessRig.describe(got)); + } + + private float[] readUniform(int location) { + final FloatBuffer buf = MemoryUtil.memAllocFloat(4); + try { + GLSMPrograms.glGetUniform(program, location, buf); + final float[] out = new float[4]; + buf.get(out); + return out; + } finally { + MemoryUtil.memFree(buf); + } + } + + @Test + void arrayElementLocations() { + assertNotEquals(-1, arr, "arr"); + assertNotEquals(-1, single, "single"); + assertEquals(arr, loc("arr[0]"), "arr[0]"); + assertEquals(arr + 3, loc("arr[3]"), "arr[3]"); + assertEquals(arr + 7, loc("arr[7]"), "arr[7]"); + assertEquals(-1, loc("arr[8]"), "arr[8]"); + assertEquals(-1, loc("arr[03]"), "arr[03]"); + assertEquals(-1, loc("arr[-1]"), "arr[-1]"); + assertEquals(arr, loc("arr[]"), "arr[] is index 0, as Mesa parses it"); + assertEquals(single, loc("single[0]"), "single[0]"); + assertEquals(-1, loc("single[1]"), "single[1]"); + } + + @Test + void elementWriteLandsInTheArray() { + writeAll(); + final int element3 = loc("arr[3]"); + writeElement(element3, X); + + assertPick(3, X, "pick=3 shows the element write"); + assertPick(2, A[2], "pick=2 keeps the bulk write"); + assertArrayEquals(vec(X), readUniform(element3), 0f, "glGetUniform arr[3]"); + assertArrayEquals(vec(A[2]), readUniform(loc("arr[2]")), 0f, "glGetUniform arr[2]"); + } + + @Test + void elementWriteClampsToTheArrayEnd() { + writeAll(); + writeAt(loc("arr[6]"), B); + + assertPick(6, B[0], "pick=6"); + assertPick(7, B[1], "pick=7"); + assertPick(5, A[5], "pick=5"); + } + + @Test + void countBeyondTheDeclarationFollowsMesa() { + writeElement(single, A[0]); + writeAt(single, B); + + assertPick(-1, A[0], "count 2 on a non-array is ignored"); + assertArrayEquals(vec(A[0]), readUniform(single), 0f, "glGetUniform single keeps its value"); + + writeAt(loc("one"), B); + assertPick(-3, B[0], "count 2 on a size-1 array clamps"); + } + + @Test + void fullWriteAfterElementWriteIsNotElided() { + writeAll(); + writeElement(loc("arr[3]"), X); + writeAll(); + + assertPick(3, A[3], "pick=3 after the repeated full write"); + } + + @Test + void intElementReadsBackAsInts() { + final int element1 = loc("iarr[1]"); + assertEquals(loc("iarr") + 1, element1, "iarr[1]"); + GLSMPrograms.glUniform4i(loc("iarr"), 1, 2, 3, 4); + GLSMPrograms.glUniform4i(element1, X[0], X[1], X[2], 255); + + assertPick(-2, X, "pick=-2 shows iarr[1]"); + assertArrayEquals(new int[] {X[0], X[1], X[2], 255}, readIntUniform(element1), "glGetUniformiv iarr[1]"); + assertArrayEquals(new int[] {1, 2, 3, 4}, readIntUniform(loc("iarr")), "glGetUniformiv iarr[0]"); + } + + private int[] readIntUniform(int location) { + final IntBuffer buf = MemoryUtil.memAllocInt(4); + try { + GLSMPrograms.glGetUniform(program, location, buf); + final int[] out = new int[4]; + buf.get(out); + return out; + } finally { + MemoryUtil.memFree(buf); + } + } + + @Test + void activeUniformReportsArrayNameWithIndexZero() { + final Map sizes = activeUniformSizes(); + assertEquals(COUNT, sizes.getOrDefault("arr[0]", -1), "size of arr[0] in " + sizes); + assertEquals(1, sizes.getOrDefault("single", -1), "size of single in " + sizes); + assertEquals(1, sizes.getOrDefault("one[0]", -1), "size of one[0] in " + sizes); + } + + private Map activeUniformSizes() { + final int active = GLSMPrograms.glGetProgrami(program, GL20.GL_ACTIVE_UNIFORMS); + final Map sizes = new HashMap<>(); + final IntBuffer length = MemoryUtil.memAllocInt(1); + final IntBuffer size = MemoryUtil.memAllocInt(1); + final IntBuffer type = MemoryUtil.memAllocInt(1); + final ByteBuffer name = MemoryUtil.memAlloc(128); + try { + for (int i = 0; i < active; i++) { + GLSMPrograms.glGetActiveUniform(program, i, length, size, type, name); + final byte[] bytes = new byte[length.get(0)]; + for (int c = 0; c < bytes.length; c++) bytes[c] = name.get(c); + sizes.put(new String(bytes, java.nio.charset.StandardCharsets.US_ASCII), size.get(0)); + } + } finally { + MemoryUtil.memFree(name); + MemoryUtil.memFree(type); + MemoryUtil.memFree(size); + MemoryUtil.memFree(length); + } + return sizes; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/common/BlockClipPlaneTest.java b/src/main/java/com/gtnewhorizons/angelica/common/BlockClipPlaneTest.java new file mode 100644 index 000000000..b70df2c94 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/common/BlockClipPlaneTest.java @@ -0,0 +1,34 @@ +package com.gtnewhorizons.angelica.common; + +import net.minecraft.block.BlockContainer; +import net.minecraft.block.material.Material; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; + +public class BlockClipPlaneTest extends BlockContainer { + + public BlockClipPlaneTest() { + super(Material.iron); + setHardness(0.7f); + } + + @Override + public TileEntity createNewTileEntity(World world, int meta) { + return new TileEntityClipPlaneTest(); + } + + @Override + public boolean isOpaqueCube() { + return false; + } + + @Override + public boolean renderAsNormalBlock() { + return false; + } + + @Override + public int getRenderType() { + return -1; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/common/TileEntityClipPlaneTest.java b/src/main/java/com/gtnewhorizons/angelica/common/TileEntityClipPlaneTest.java new file mode 100644 index 000000000..93eb2e6fc --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/common/TileEntityClipPlaneTest.java @@ -0,0 +1,11 @@ +package com.gtnewhorizons.angelica.common; + +import net.minecraft.tileentity.TileEntity; + +public class TileEntityClipPlaneTest extends TileEntity { + + @Override + public boolean canUpdate() { + return false; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/iris/IrisGLSMBridge.java b/src/main/java/com/gtnewhorizons/angelica/iris/IrisGLSMBridge.java index 3ccd30a5e..c3e7f6d24 100644 --- a/src/main/java/com/gtnewhorizons/angelica/iris/IrisGLSMBridge.java +++ b/src/main/java/com/gtnewhorizons/angelica/iris/IrisGLSMBridge.java @@ -274,6 +274,13 @@ public class IrisGLSMBridge { refreshBlendCondition(); }); + GLSMHooks.CLIP_PLANES_IN_USE.addListener(event -> { + if (!Iris.enabled) return; + if (Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline drp) { + drp.onClipPlanesInUseChanged(); + } + }); + GLSMHooks.FOREIGN_DRAW_END.addListener(event -> { if (!Iris.enabled) return; if (programRestoreDeferred) { @@ -336,12 +343,13 @@ public class IrisGLSMBridge { WorldRenderingPipeline pipeline = Iris.getPipelineManager().getPipelineNullable(); if (pipeline instanceof DeferredWorldRenderingPipeline drp) { DeferredWorldRenderingPipeline.Pass activePass = drp.getActivePassProgram(); - if (activePass != null && activePass.getProgram() != null && activePass.getProgram().getProgramId() == event.newProgram) { + final Program bound = activePass != null ? activePass.boundProgram() : null; + if (bound != null && bound.getProgramId() == event.newProgram) { final int frame = SystemTimeUniforms.COUNTER.getAsInt(); if (programLastUpdatedFrame.get(event.newProgram) != frame || !ProgramUniforms.isActiveProgramBound() || ProgramUniforms.activeHasDeferredUploads()) { - activePass.getProgram().getUniforms().update(); + bound.getUniforms().update(); programLastUpdatedFrame.put(event.newProgram, frame); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/proxy/ClientProxy.java b/src/main/java/com/gtnewhorizons/angelica/proxy/ClientProxy.java index e2fe06c58..7e4756edc 100644 --- a/src/main/java/com/gtnewhorizons/angelica/proxy/ClientProxy.java +++ b/src/main/java/com/gtnewhorizons/angelica/proxy/ClientProxy.java @@ -39,6 +39,7 @@ import com.gtnewhorizons.angelica.commands.AngelicaCommand; import com.gtnewhorizons.angelica.AngelicaMod; import com.gtnewhorizons.angelica.common.BlockError; import com.gtnewhorizons.angelica.common.BlockIsbrhTest; +import com.gtnewhorizons.angelica.common.TileEntityClipPlaneTest; import com.gtnewhorizons.angelica.compat.ModStatus; import com.gtnewhorizons.angelica.compat.bettercrashes.BetterCrashesCompat; import com.gtnewhorizons.angelica.config.AngelicaConfig; @@ -70,6 +71,7 @@ import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt; import com.gtnewhorizons.angelica.render.CloudRenderer; import com.gtnewhorizons.angelica.render.EmissiveTextureAutoloader; import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry; +import com.gtnewhorizons.angelica.rendering.ClipPlaneTestRenderer; import com.gtnewhorizons.angelica.rendering.FpsReducer; import com.gtnewhorizons.angelica.rendering.FramePacer; import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer; @@ -86,6 +88,7 @@ import com.gtnewhorizons.angelica.utils.AngelicaJar; import com.gtnewhorizons.angelica.utils.AnimationMode; import com.gtnewhorizons.angelica.utils.ManagedEnum; import com.gtnewhorizons.angelica.zoom.Zoom; +import cpw.mods.fml.client.registry.ClientRegistry; import cpw.mods.fml.client.registry.RenderingRegistry; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.event.FMLInitializationEvent; @@ -146,6 +149,7 @@ public final class ClientProxy extends CommonProxy { if (AngelicaConfig.enableTestBlocks) { BlockIsbrhTest.renderId = RenderingRegistry.getNextAvailableRenderId(); RenderingRegistry.registerBlockHandler(BlockIsbrhTest.renderId, new IsbrhTestRenderer()); + ClientRegistry.bindTileEntitySpecialRenderer(TileEntityClipPlaneTest.class, new ClipPlaneTestRenderer()); } AngelicaJar.configureShaderDiskCache(event.getSourceFile()); if (AngelicaConfig.enableIris) { diff --git a/src/main/java/com/gtnewhorizons/angelica/proxy/CommonProxy.java b/src/main/java/com/gtnewhorizons/angelica/proxy/CommonProxy.java index 179a38c97..b4910fc15 100644 --- a/src/main/java/com/gtnewhorizons/angelica/proxy/CommonProxy.java +++ b/src/main/java/com/gtnewhorizons/angelica/proxy/CommonProxy.java @@ -1,7 +1,9 @@ package com.gtnewhorizons.angelica.proxy; +import com.gtnewhorizons.angelica.common.BlockClipPlaneTest; import com.gtnewhorizons.angelica.common.BlockIsbrhTest; import com.gtnewhorizons.angelica.common.BlockTest; +import com.gtnewhorizons.angelica.common.TileEntityClipPlaneTest; import com.gtnewhorizons.angelica.config.AngelicaConfig; import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.event.FMLPostInitializationEvent; @@ -14,6 +16,8 @@ public class CommonProxy { if (AngelicaConfig.enableTestBlocks) { GameRegistry.registerBlock(new BlockTest(), "test_block"); GameRegistry.registerBlock(new BlockIsbrhTest(), "test_isbrh"); + GameRegistry.registerBlock(new BlockClipPlaneTest(), "test_clip_plane"); + GameRegistry.registerTileEntity(TileEntityClipPlaneTest.class, "angelica:test_clip_plane"); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/ClipPlaneTestRenderer.java b/src/main/java/com/gtnewhorizons/angelica/rendering/ClipPlaneTestRenderer.java new file mode 100644 index 000000000..8c864498f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/ClipPlaneTestRenderer.java @@ -0,0 +1,41 @@ +package com.gtnewhorizons.angelica.rendering; + +import com.gtnewhorizons.angelica.glsm.GLSMEnable; +import com.gtnewhorizons.angelica.glsm.GLSMMatrices; +import com.gtnewhorizons.angelica.glsm.GLSMRasterizer; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; +import net.minecraft.init.Blocks; +import net.minecraft.tileentity.TileEntity; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; + +import java.nio.DoubleBuffer; + +public class ClipPlaneTestRenderer extends TileEntitySpecialRenderer { + + private static final DoubleBuffer KEEP_LOWER_HALF = plane(0.0, -1.0, 0.0, 0.5); + private static final DoubleBuffer REJECT_ALL = plane(0.0, 0.0, 0.0, -1.0); + + private static DoubleBuffer plane(double a, double b, double c, double d) { + final DoubleBuffer buf = BufferUtils.createDoubleBuffer(4); + buf.put(a).put(b).put(c).put(d).flip(); + return buf; + } + + @Override + public void renderTileEntityAt(TileEntity te, double x, double y, double z, float partialTicks) { + GLSMMatrices.glPushMatrix(); + GLSMMatrices.glTranslated(x, y, z); + GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE1, REJECT_ALL); + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE1); + GLSMRasterizer.glClipPlane(GL11.GL_CLIP_PLANE0, KEEP_LOWER_HALF); + GLSMEnable.glEnable(GL11.GL_CLIP_PLANE0); + GLSMMatrices.glTranslated(0.5, 0.5, 0.5); + bindTexture(TextureMap.locationBlocksTexture); + RenderBlocks.getInstance().renderBlockAsItem(Blocks.gold_block, 0, 1.0f); + GLSMEnable.glDisable(GL11.GL_CLIP_PLANE0); + GLSMMatrices.glPopMatrix(); + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java index 27a79db99..283fa2cc0 100644 --- a/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java +++ b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java @@ -29,6 +29,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.Instancing; import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard; import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks; import com.gtnewhorizons.angelica.glsm.hooks.PendingProgramSelection; +import com.gtnewhorizons.angelica.glsm.profiling.Tracy; import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache; import com.gtnewhorizons.angelica.rendering.RenderingState; import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility; @@ -141,6 +142,7 @@ import java.util.OptionalInt; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; import java.util.function.IntFunction; import java.util.function.Supplier; @@ -148,6 +150,8 @@ import java.util.function.Supplier; * Encapsulates the compiled shader program objects for the currently loaded shaderpack. */ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, RenderTargetStateListener, TesrInstancingPipeline, PendingProgramSelection { + private static final Tracy.ZoneId Z_CLIP_VARIANT = Tracy.zoneId("irisClipVariant", Tracy.COLOR_IRIS); + private final RenderTargets renderTargets; @Nullable @@ -202,7 +206,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R private final EnumMap, Map>> instancedAttributeTransforms = new EnumMap<>(Instancing.class); private final boolean[] supportsInstancing = initialInstancingSupport(); private final Map, Map> wideLineTransforms = new HashMap<>(); - private final Map wideLinePrograms = new HashMap<>(); + private final Map wideLinePrograms = new HashMap<>(); private final boolean packWidensLines; private final ParityFlipState parityState = new ParityFlipState(AngelicaConfig.shaderParityFlip); private final Supplier> flippedGbuffers; @@ -965,7 +969,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R public int getActivePassProgramId() { if (current == null) return -1; - final Program p = current.getProgram(); + final Program p = current.boundProgram(); return p != null ? p.getProgramId() : -1; } @@ -1044,6 +1048,29 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R } } + private boolean clipRebindPending; + + public void onClipPlanesInUseChanged() { + if (current == null) return; + clipRebindPending = true; + GLSMHooks.pendingProgramSelection = this; + } + + private void resolveClipRebind() { + if (!clipRebindPending) return; + if (DisplayListManager.isRecording()) { + GLSMHooks.pendingProgramSelection = this; + return; + } + clipRebindPending = false; + final Pass pass = current; + if (pass == null) return; + final Program live = pass.liveProgram; + if (live != null && GLSMPrograms.getActiveProgram() == live.getProgramId() && pass.select(activeInstancing, GLSMRasterizer.clipEmulationActive()) != live) { + pass.rebindProgram(); + } + } + private void clearPendingSelection() { if (GLSMHooks.pendingProgramSelection == this) { GLSMHooks.pendingProgramSelection = null; @@ -1053,11 +1080,10 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R private void matchPass() { clearPendingSelection(); - if (!shouldOverrideShaders()) { - return; + if (shouldOverrideShaders()) { + matchPass(getCondition(getPhase())); } - - matchPass(getCondition(getPhase())); + resolveClipRebind(); } private void matchPass(RenderCondition condition) { @@ -1139,7 +1165,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R } return; } - final Program program = pass.getProgram(); + final Program program = pass.select(Instancing.NONE, GLSMRasterizer.clipEmulationActive()); final GlFramebuffer expected = isBeforeTranslucent ? pass.framebufferBeforeTranslucents : pass.framebufferAfterTranslucents; if (program != null && GLSMPrograms.getActiveProgram() == program.getProgramId() && GLSMFramebuffer.getDrawFramebuffer() == expected.getId()) { return; @@ -1199,7 +1225,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R if (shared != null) { final Pass pass = createPassInner(shared.program(), source.getDirectives(), shadow, id); this.customUniforms.mapPassLike(shared.firstPass(), pass); - pass.setInstancingKey(source.getName(), availability, shadow); + pass.setVariantSource(source, id, availability, shadow); return pass; } @@ -1224,7 +1250,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R final Pass pass = createPassInner(program, source.getDirectives(), shadow, id); this.customUniforms.mapholderToPass(builder, pass); - pass.setInstancingKey(source.getName(), availability, shadow); + pass.setVariantSource(source, id, availability, shadow); if (shareKey != null) { builtPrograms.put(shareKey, new BuiltProgram(program, pass)); } @@ -1432,18 +1458,24 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R private final List bufferBlendOverrides; private final boolean shadowViewport; - private String instancingSourceName; - private InputAvailability instancingAvailability; - private boolean instancingShadow; - private final Program[] instancedVariants = new Program[Instancing.VALUES.length]; - private final boolean[] instancedVariantAttempted = new boolean[Instancing.VALUES.length]; + private ProgramSource variantSource; + private ProgramId variantId; + private InputAvailability variantAvailability; + private boolean variantShadow; + private final Program[][] variants = new Program[Instancing.VALUES.length][2]; + private final boolean[][] variantAttempted = new boolean[Instancing.VALUES.length][2]; @Nullable - private WideLineProgram wideLineProgram; - private boolean wideLineProgramAttempted; + private Program boundProgram; + @Nullable + private Program liveProgram; + private final WideLineProgram[] wideLineVariants = new WideLineProgram[2]; + private final boolean[] wideLineProgramAttempted = new boolean[2]; private Pass(@Nullable Program program, GlFramebuffer framebufferBeforeTranslucents, GlFramebuffer framebufferAfterTranslucents, @Nullable AlphaTestOverride alphaTestOverride, @Nullable BlendModeOverride blendModeOverride, @Nullable List bufferBlendOverrides, boolean shadowViewport) { this.program = program; + this.variants[Instancing.NONE.ordinal()][0] = program; + this.variantAttempted[Instancing.NONE.ordinal()][0] = true; this.framebufferBeforeTranslucents = framebufferBeforeTranslucents; this.framebufferAfterTranslucents = framebufferAfterTranslucents; this.alphaTestOverride = alphaTestOverride; @@ -1457,15 +1489,22 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R } void rebindProgram() { - if (program != null) program.use(); final boolean suspended = GLStateManager.suspendExitCapture(); try { - DeferredWorldRenderingPipeline.this.customUniforms.push(this); + bindSelected(); } finally { GLStateManager.resumeExitCapture(suspended); } } + private void bindSelected() { + final Program selected = select(activeInstancing, GLSMRasterizer.clipEmulationActive()); + liveProgram = selected; + if (activeInstancing == Instancing.NONE) boundProgram = selected; + if (selected != null) selected.use(); + DeferredWorldRenderingPipeline.this.customUniforms.push(selected == null || selected == program ? this : selected); + } + private void useNow() { final boolean suspended = GLStateManager.suspendExitCapture(); try { @@ -1511,12 +1550,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R bufferBlendOverrides.forEach(BufferBlendOverride::apply); } - if (activeInstancing != Instancing.NONE && program != null) { - DeferredWorldRenderingPipeline.this.bindInstancedVariant(activeInstancing); - } else { - if (program != null) program.use(); - DeferredWorldRenderingPipeline.this.customUniforms.push(this); - } + bindSelected(); } public void stopUsing() { @@ -1541,62 +1575,113 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R return program; } - void setInstancingKey(String sourceName, InputAvailability availability, boolean shadow) { - this.instancingSourceName = sourceName; - this.instancingAvailability = availability; - this.instancingShadow = shadow; + @Nullable + public Program boundProgram() { + return boundProgram != null ? boundProgram : program; + } + + void setVariantSource(ProgramSource source, ProgramId id, InputAvailability availability, boolean shadow) { + this.variantSource = source; + this.variantId = id; + this.variantAvailability = availability; + this.variantShadow = shadow; } @Nullable - Program instancedVariant(Instancing kind) { + Program select(Instancing kind, boolean clipped) { final int i = kind.ordinal(); - if (!instancedVariantAttempted[i]) { - instancedVariantAttempted[i] = true; - if (instancingSourceName != null && supportsInstancing[i]) { - instancedVariants[i] = sharedInstancedVariant(instancingSourceName, instancingAvailability, instancingShadow, kind); + if (!variantAttempted[i][0]) { + variantAttempted[i][0] = true; + if (variantSource != null && supportsInstancing[i]) { + variants[i][0] = sharedVariant(new InstancedVariantKey(variantSource.getName(), variantAvailability, variantShadow, kind, false), + () -> buildInstancedVariant(variantSource.getName(), variantAvailability, variantShadow, kind)); } } - return instancedVariants[i]; + final Program unclipped = variants[i][0]; + if (!clipped || unclipped == null) return unclipped; + if (!variantAttempted[i][1]) { + variantAttempted[i][1] = true; + variants[i][1] = sharedVariant(new InstancedVariantKey(variantSource.getName(), variantAvailability, variantShadow, kind, true), + () -> buildClipVariant(variantSource, variantId, variantAvailability, variantShadow, kind)); + } + return variants[i][1] != null ? variants[i][1] : unclipped; } @Nullable - WideLineProgram wideLineProgram() { - if (!wideLineProgramAttempted) { - wideLineProgramAttempted = true; - if (instancingSourceName != null) { - wideLineProgram = sharedWideLineProgram(instancingSourceName, instancingAvailability, instancingShadow); + WideLineProgram wideLineProgram(boolean clipped) { + final int c = clipped ? 1 : 0; + if (!wideLineProgramAttempted[c]) { + wideLineProgramAttempted[c] = true; + if (variantSource != null && (!clipped || wideLineProgram(false) != null)) { + wideLineVariants[c] = sharedWideLineProgram(variantSource, variantAvailability, variantShadow, clipped); } } - return wideLineProgram; + return wideLineVariants[c] != null || !clipped ? wideLineVariants[c] : wideLineVariants[0]; } public void destroy() { if (this.program != null) { this.program.destroy(); } - for (int i = 0; i < instancedVariants.length; i++) { - if (instancedVariants[i] != null) { - instancedVariants[i].destroy(); - instancedVariants[i] = null; + for (int i = 0; i < variants.length; i++) { + for (int c = 0; c < 2; c++) { + if ((i != Instancing.NONE.ordinal() || c != 0) && variants[i][c] != null) { + variants[i][c].destroy(); + variants[i][c] = null; + } } } } } - private record InstancedVariantKey(String sourceName, InputAvailability availability, boolean shadow, Instancing kind) {} + private record InstancedVariantKey(String sourceName, InputAvailability availability, boolean shadow, Instancing kind, boolean clipped) {} // Passes sharing a program also share its instanced variants private final Map instancedVariantPrograms = new HashMap<>(); @Nullable - private Program sharedInstancedVariant(String sourceName, InputAvailability availability, boolean shadow, Instancing kind) { - final InstancedVariantKey key = new InstancedVariantKey(sourceName, availability, shadow, kind); + private Program sharedVariant(InstancedVariantKey key, Supplier build) { if (instancedVariantPrograms.containsKey(key)) return instancedVariantPrograms.get(key); - final Program variant = buildInstancedVariant(sourceName, availability, shadow, kind); + final Program variant = build.get(); instancedVariantPrograms.put(key, variant); return variant; } + @Nullable + private Program buildClipVariant(ProgramSource source, ProgramId id, InputAvailability availability, boolean shadow, Instancing kind) { + final String name = source.getName() + kind.variantSuffix + "_clipped"; + final boolean glint = GlintScrollInjector.shouldInject(id, source); + return buildClipped(source, name, (vertex, fragment) -> { + final Map transformed = TransformPatcher.patchAttributesInstanced(vertex, null, null, null, fragment, availability, glint, kind, true); + return transformed != null ? linkVariant(name, transformed, availability, shadow) : null; + }); + } + + @Nullable + private static T buildClipped(ProgramSource source, String name, BiFunction build) { + if (source.getGeometrySource().isPresent() || source.getTessControlSource().isPresent() || source.getTessEvalSource().isPresent()) { + return null; + } + if (Tracy.ENABLED) Tracy.beginZone(Z_CLIP_VARIANT); + try { + return build.apply(source.getVertexSource().orElse(null), source.getFragmentSource().orElse(null)); + } catch (Exception e) { + Iris.logger.warn("Clip plane variant failed for {}; clip planes stay unclipped there", name, e); + return null; + } finally { + if (Tracy.ENABLED) Tracy.endZone(); + } + } + + private Program linkVariant(String name, Map transformed, InputAvailability availability, boolean shadow) { + final ProgramBuilder builder = beginBuilder(name, transformed); + wireGbufferProgram(builder, availability, shadow); + final Program variant = builder.build(); + vertexWriteReplays.attach(name, variant, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow)); + this.customUniforms.mapholderToPass(builder, variant); + return variant; + } + @Nullable private Program buildInstancedVariant(String sourceName, InputAvailability availability, boolean shadow, Instancing kind) { final Map, Map> transforms = instancedAttributeTransforms.get(kind); @@ -1605,12 +1690,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R return null; } try { - final ProgramBuilder builder = beginBuilder(sourceName + kind.variantSuffix, transformed); - wireGbufferProgram(builder, availability, shadow); - final Program variant = builder.build(); - vertexWriteReplays.attach(sourceName + kind.variantSuffix, variant, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow)); - this.customUniforms.mapholderToPass(builder, variant); - return variant; + return linkVariant(sourceName + kind.variantSuffix, transformed, availability, shadow); } catch (Exception e) { Iris.logger.warn("{} instanced variant link failed for {}; disabling it", kind, sourceName, e); supportsInstancing[kind.ordinal()] = false; @@ -1641,50 +1721,52 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R @Override public boolean hasInstancedVariant(Instancing kind) { - return current != null && current.instancedVariant(kind) != null; + return current != null && current.select(kind, false) != null; } public boolean hasInstancedVariant(RenderCondition condition, InputAvailability availability, Instancing kind) { final Pass pass = table.match(condition, availability); - return pass != null && pass.instancedVariant(kind) != null; + return pass != null && pass.select(kind, false) != null; } @Override public void bindInstancedVariant(Instancing kind) { activeInstancing = kind; - final Program variant = current.instancedVariant(kind); - variant.use(); - this.customUniforms.push(variant); + current.bindSelected(); } private record WideLineProgram(Program program, int lineWidthLocation, int screenSizeLocation) {} + private record WideLineKey(String sourceName, InputAvailability availability, boolean shadow, boolean clipped) {} + @Nullable - private WideLineProgram sharedWideLineProgram(String sourceName, InputAvailability availability, boolean shadow) { - final SharedProgramKey key = new SharedProgramKey(sourceName, availability, shadow); + private WideLineProgram sharedWideLineProgram(ProgramSource source, InputAvailability availability, boolean shadow, boolean clipped) { + final WideLineKey key = new WideLineKey(source.getName(), availability, shadow, clipped); if (wideLinePrograms.containsKey(key)) return wideLinePrograms.get(key); - final WideLineProgram built = buildWideLineProgram(sourceName, availability, shadow); + final WideLineProgram built = clipped ? buildClippedWideLineProgram(source, availability, shadow) + : buildWideLineProgram(source.getName() + "_widelines", wideLineTransforms.get(Pair.of(source.getName(), availability)), availability, shadow); wideLinePrograms.put(key, built); return built; } @Nullable - private WideLineProgram buildWideLineProgram(String sourceName, InputAvailability availability, boolean shadow) { - final Map transformed = wideLineTransforms.get(Pair.of(sourceName, availability)); + private WideLineProgram buildClippedWideLineProgram(ProgramSource source, InputAvailability availability, boolean shadow) { + final String name = source.getName() + "_widelines_clipped"; + return buildClipped(source, name, (vertex, fragment) -> + buildWideLineProgram(name, TransformPatcher.patchAttributesWideLines(vertex, null, null, null, fragment, availability, true), availability, shadow)); + } + + @Nullable + private WideLineProgram buildWideLineProgram(String name, @Nullable Map transformed, InputAvailability availability, boolean shadow) { if (transformed == null) { return null; } - final String name = sourceName + "_widelines"; try { - final ProgramBuilder builder = beginBuilder(name, transformed); - wireGbufferProgram(builder, availability, shadow); - final Program program = builder.build(); - vertexWriteReplays.attach(name, program, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow)); - this.customUniforms.mapholderToPass(builder, program); + final Program program = linkVariant(name, transformed, availability, shadow); final int id = program.getProgramId(); return new WideLineProgram(program, GLSMPrograms.glGetUniformLocation(id, "iris_LineWidth"), GLSMPrograms.glGetUniformLocation(id, "iris_ScreenSize")); } catch (Exception e) { - Iris.logger.warn("Wide line program link failed for {}; lines there stay 1 px wide", sourceName, e); + Iris.logger.warn("Wide line program link failed for {}; lines there stay 1 px wide", name, e); return null; } } @@ -1698,8 +1780,8 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R } GLSMHooks.resolvePendingProgram(); final Pass pass = current; - if (pass == null || pass.getProgram() == null || activeInstancing != Instancing.NONE - || GLSMPrograms.getActiveProgram() != pass.getProgram().getProgramId()) { + final Program bound = pass != null ? pass.boundProgram() : null; + if (bound == null || activeInstancing != Instancing.NONE || GLSMPrograms.getActiveProgram() != bound.getProgramId()) { return false; } @@ -1709,7 +1791,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R return false; } final boolean needsWidening = shadow || !packWidensLines; - final WideLineProgram wide = needsWidening ? linePass.wideLineProgram() : null; + final WideLineProgram wide = needsWidening ? linePass.wideLineProgram(GLSMRasterizer.clipEmulationActive()) : null; if (needsWidening && wide == null) { return false; } @@ -1742,11 +1824,11 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R } public void compileWideLinePrograms() { - table.match(RenderCondition.LINES, LINE_INPUTS).wideLineProgram(); + table.match(RenderCondition.LINES, LINE_INPUTS).wideLineProgram(false); final Pass shadowPass = table.match(RenderCondition.SHADOW, SHADOW_INPUTS); - if (shadowPass != null) shadowPass.wideLineProgram(); + if (shadowPass != null) shadowPass.wideLineProgram(false); final Pass shadowWaterPass = table.match(RenderCondition.SHADOW_TRANSLUCENT, SHADOW_INPUTS); - if (shadowWaterPass != null) shadowWaterPass.wideLineProgram(); + if (shadowWaterPass != null) shadowWaterPass.wideLineProgram(false); } @Override @@ -1851,7 +1933,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R table.forEach(pass -> { if (pass == null || !visited.add(pass)) return; for (Instancing kind : Instancing.VALUES) { - if (kind != Instancing.NONE) pass.instancedVariant(kind); + if (kind != Instancing.NONE) pass.select(kind, false); } }); } @@ -2667,7 +2749,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R source.getTessControlSource().orElse(null), source.getTessEvalSource().orElse(null), source.getFragmentSource().orElse(null), - inputs))); + inputs, false))); } private static final ProgramId[] INSTANCED_PROGRAM_IDS = { diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/CommonTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/CommonTransformer.java index 1606af637..688a95f86 100644 --- a/src/main/java/net/coderbot/iris/pipeline/transform/CommonTransformer.java +++ b/src/main/java/net/coderbot/iris/pipeline/transform/CommonTransformer.java @@ -1,7 +1,10 @@ package net.coderbot.iris.pipeline.transform; import com.gtnewhorizons.angelica.glsm.GlslTransformUtils; +import com.gtnewhorizons.angelica.glsm.RenderSystem; +import com.gtnewhorizons.angelica.glsm.ffp.ClipDistanceGlsl; import com.gtnewhorizons.angelica.glsm.shader.ShaderType; +import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters; import net.coderbot.iris.pipeline.transform.parameter.Parameters; import org.taumc.glsl.Transformer; import org.taumc.glsl.grammar.GLSLParser; @@ -123,7 +126,9 @@ public class CommonTransformer { }); } - if (parameters.patch == Patch.ATTRIBUTES && parameters.type == ShaderType.VERTEX) { + if (parameters.patch == Patch.ATTRIBUTES && RenderSystem.isClipDistanceEmulated()) { + injectClipDiscard(root, (AttributeParameters) parameters); + } else if (parameters.patch == Patch.ATTRIBUTES && parameters.type == ShaderType.VERTEX) { root.injectVariable("uniform bool angelica_ClipPlanesEnabled;"); root.injectVariable("uniform vec4 angelica_ClipPlane[8];"); root.appendMain( @@ -144,4 +149,31 @@ public class CommonTransformer { ); } } + + private static final String CLIP_VS_BLOCK; + static { + final String[] distances = new String[8]; + for (int i = 0; i < distances.length; i++) { + distances[i] = "dot(angelica_ClipPlane[" + i + "], _cp_ep)"; + } + final StringBuilder sb = new StringBuilder(256).append("{ vec4 _cp_ep = iris_ModelViewMatrix * iris_Vertex;\n"); + ClipDistanceGlsl.appendVsAssign(sb, distances); + CLIP_VS_BLOCK = sb.append('}').toString(); + } + + private static void injectClipDiscard(Transformer root, AttributeParameters parameters) { + if (!parameters.clipDiscard) { + return; + } + if (parameters.type == ShaderType.VERTEX) { + root.injectVariable("uniform vec4 angelica_ClipPlane[8];"); + root.injectVariable(ClipDistanceGlsl.VS_OUT_DIST0); + root.injectVariable(ClipDistanceGlsl.VS_OUT_DIST1); + root.appendMain(CLIP_VS_BLOCK); + } else if (parameters.type == ShaderType.FRAGMENT) { + root.injectVariable(ClipDistanceGlsl.FS_IN_DIST0); + root.injectVariable(ClipDistanceGlsl.FS_IN_DIST1); + root.prependMain(ClipDistanceGlsl.FS_DISCARD); + } + } } diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java index 12d33c115..6f54535f9 100644 --- a/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java +++ b/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java @@ -238,7 +238,7 @@ public class ShaderTransformer { final ShaderDiskCache.Key k = ShaderDiskCache.key("iris-transform").str(patch.name()); parameters.appendDiskKey(k); k.i(RenderSystem.getMaxGlslVersion()).b(RenderSystem.supportsSSBO()).b(RenderSystem.supportsImageLoadStore()) - .b(RenderSystem.isGLES()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).b(BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()).str(RwImageStoreExtractor.atomicCustomImagesKey()) + .b(RenderSystem.isGLES()).b(RenderSystem.isClipDistanceEmulated()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).b(BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()).str(RwImageStoreExtractor.atomicCustomImagesKey()) .i(maxSupportedHoistVersion); if (enabledRequirements == null) { k.i(-1); diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/TransformPatcher.java b/src/main/java/net/coderbot/iris/pipeline/transform/TransformPatcher.java index f0114afe0..a18ff5834 100644 --- a/src/main/java/net/coderbot/iris/pipeline/transform/TransformPatcher.java +++ b/src/main/java/net/coderbot/iris/pipeline/transform/TransformPatcher.java @@ -143,11 +143,15 @@ public class TransformPatcher { } public static Map patchAttributesInstanced(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs, boolean scrollGlint, Instancing instancing) { - return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, scrollGlint, instancing)); + return patchAttributesInstanced(vertex, geometry, tessControl, tessEval, fragment, inputs, scrollGlint, instancing, false); } - public static Map patchAttributesWideLines(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs) { - return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, false, Instancing.NONE, true)); + public static Map patchAttributesInstanced(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs, boolean scrollGlint, Instancing instancing, boolean clipDiscard) { + return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, scrollGlint, instancing, false, clipDiscard)); + } + + public static Map patchAttributesWideLines(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs, boolean clipDiscard) { + return transform(vertex, geometry, tessControl, tessEval, fragment, new AttributeParameters(Patch.ATTRIBUTES, geometry != null, tessControl != null, inputs, false, Instancing.NONE, true, clipDiscard)); } public static Map patchAttributes(String vertex, String geometry, String tessControl, String tessEval, String fragment, InputAvailability inputs) { diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/parameter/AttributeParameters.java b/src/main/java/net/coderbot/iris/pipeline/transform/parameter/AttributeParameters.java index be359161d..02733d514 100644 --- a/src/main/java/net/coderbot/iris/pipeline/transform/parameter/AttributeParameters.java +++ b/src/main/java/net/coderbot/iris/pipeline/transform/parameter/AttributeParameters.java @@ -13,13 +13,14 @@ public class AttributeParameters extends Parameters { public final boolean scrollGlint; public final Instancing instancing; public final boolean wideLines; + public final boolean clipDiscard; // WARNING: adding new fields requires updating hashCode and equals methods! public AttributeParameters(Patch patch, boolean hasGeometry, boolean hasTesselation, InputAvailability inputs, boolean scrollGlint, Instancing instancing) { - this(patch, hasGeometry, hasTesselation, inputs, scrollGlint, instancing, false); + this(patch, hasGeometry, hasTesselation, inputs, scrollGlint, instancing, false, false); } - public AttributeParameters(Patch patch, boolean hasGeometry, boolean hasTesselation, InputAvailability inputs, boolean scrollGlint, Instancing instancing, boolean wideLines) { + public AttributeParameters(Patch patch, boolean hasGeometry, boolean hasTesselation, InputAvailability inputs, boolean scrollGlint, Instancing instancing, boolean wideLines, boolean clipDiscard) { super(patch, null); this.hasGeometry = hasGeometry; this.hasTesselation = hasTesselation; @@ -27,6 +28,7 @@ public class AttributeParameters extends Parameters { this.scrollGlint = scrollGlint; this.instancing = instancing; this.wideLines = wideLines; + this.clipDiscard = clipDiscard; } @Override @@ -37,7 +39,7 @@ public class AttributeParameters extends Parameters { @Override public void appendDiskKey(ShaderDiskCache.Key k) { super.appendDiskKey(k); - k.b(hasGeometry).b(hasTesselation).str(inputs == null ? null : inputs.name()).b(scrollGlint).str(instancing.name()).b(wideLines); + k.b(hasGeometry).b(hasTesselation).str(inputs == null ? null : inputs.name()).b(scrollGlint).str(instancing.name()).b(wideLines).b(clipDiscard); } @Override @@ -49,6 +51,7 @@ public class AttributeParameters extends Parameters { result = prime * result + (scrollGlint ? 1231 : 1237); result = prime * result + instancing.ordinal(); result = prime * result + (wideLines ? 1231 : 1237); + result = prime * result + (clipDiscard ? 1231 : 1237); result = prime * result + ((inputs == null) ? 0 : inputs.hashCode()); return result; } @@ -72,6 +75,8 @@ public class AttributeParameters extends Parameters { return false; if (wideLines != other.wideLines) return false; + if (clipDiscard != other.clipDiscard) + return false; if (inputs == null) { return other.inputs == null; } else return inputs.equals(other.inputs); diff --git a/src/test/java/net/coderbot/iris/pipeline/AttributeTransformerLinkGLTest.java b/src/test/java/net/coderbot/iris/pipeline/AttributeTransformerLinkGLTest.java index 5c4b162eb..5c1fa9efd 100644 --- a/src/test/java/net/coderbot/iris/pipeline/AttributeTransformerLinkGLTest.java +++ b/src/test/java/net/coderbot/iris/pipeline/AttributeTransformerLinkGLTest.java @@ -99,7 +99,7 @@ class AttributeTransformerLinkGLTest { @Test void wideLineVariantLinksWithLineUniformsLive() { - final Map result = TransformPatcher.patchAttributesWideLines(COMPAT_VERTEX, null, null, null, COMPAT_FRAGMENT, InputAvailability.NONE); + final Map result = TransformPatcher.patchAttributesWideLines(COMPAT_VERTEX, null, null, null, COMPAT_FRAGMENT, InputAvailability.NONE, false); final int program = link("wideLines", result); try { assertNotEquals(-1, GLSMPrograms.glGetUniformLocation(program, "iris_LineWidth"), () -> result.get(PatchShaderType.VERTEX)); diff --git a/src/test/java/net/coderbot/iris/pipeline/transform/AttributeTransformerInstancedTest.java b/src/test/java/net/coderbot/iris/pipeline/transform/AttributeTransformerInstancedTest.java index 15c96f68b..ec59943bf 100644 --- a/src/test/java/net/coderbot/iris/pipeline/transform/AttributeTransformerInstancedTest.java +++ b/src/test/java/net/coderbot/iris/pipeline/transform/AttributeTransformerInstancedTest.java @@ -1,11 +1,13 @@ package net.coderbot.iris.pipeline.transform; import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement; +import com.gtnewhorizons.angelica.glsm.ffp.ClipDistanceGlsl; import com.gtnewhorizons.angelica.glsm.ffp.CubeInstancedAttribs; import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs; import com.gtnewhorizons.angelica.glsm.ffp.Instancing; import com.gtnewhorizons.angelica.glsm.ffp.ParticleInstancedAttribs; import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler; +import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport; import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters; import org.junit.jupiter.api.AfterAll; @@ -467,4 +469,54 @@ class AttributeTransformerInstancedTest { assertNotNull(entityResult); assertHas(entityResult.get(PatchShaderType.VERTEX), "layout(location = " + ImmediateExtendedAttribHandler.LOC_ENTITY_IDS + ") in vec4 iris_Entity;"); } + + private static final String[] CLIP_NAMES = {"gl_ClipDistance", "angelica_ClipDist", "angelica_ClipPlane"}; + + private static Map clipStages(boolean emulated, boolean clipDiscard) { + try (var ignored = ClipTestSupport.emulation(emulated)) { + final Map result = TransformPatcher.patchAttributesInstanced(VERTEX, null, null, null, FRAGMENT, TEX_LM, false, Instancing.NONE, clipDiscard); + assertNotNull(result); + for (String stage : result.values()) { + if (stage != null) ShaderParser.parseShader(stage); + } + return result; + } + } + + @Test + void nativeClipDistancesWrittenByTheVertexStage() { + final Map result = clipStages(false, false); + assertHas(result.get(PatchShaderType.VERTEX), "gl_ClipDistance[7] = dot(angelica_ClipPlane[7], _cp_ep);"); + assertLacks(result.get(PatchShaderType.VERTEX), "angelica_ClipDist0"); + assertLacks(result.get(PatchShaderType.FRAGMENT), "angelica_ClipDist0"); + } + + @Test + void emulatedUnclippedVariantCarriesNoClipCode() { + final Map result = clipStages(true, false); + for (String stage : result.values()) { + if (stage == null) continue; + for (String name : CLIP_NAMES) assertLacks(stage, name); + } + } + + @Test + void emulatedClippedVariantDiscardsFirstInFragmentMain() { + final Map result = clipStages(true, true); + final String v = result.get(PatchShaderType.VERTEX); + assertHas(v, "uniform vec4 angelica_ClipPlane[8];"); + assertHas(v, "out vec4 angelica_ClipDist0;"); + assertHas(v, "out vec4 angelica_ClipDist1;"); + assertHas(v, "angelica_ClipDist0 = vec4(dot(angelica_ClipPlane[0], _cp_ep), dot(angelica_ClipPlane[1], _cp_ep), dot(angelica_ClipPlane[2], _cp_ep), dot(angelica_ClipPlane[3], _cp_ep));"); + assertHas(v, "angelica_ClipDist1 = vec4(dot(angelica_ClipPlane[4], _cp_ep), dot(angelica_ClipPlane[5], _cp_ep), dot(angelica_ClipPlane[6], _cp_ep), dot(angelica_ClipPlane[7], _cp_ep));"); + assertLacks(v, "gl_ClipDistance"); + + final String f = result.get(PatchShaderType.FRAGMENT); + assertHas(f, "in vec4 angelica_ClipDist0;"); + assertHas(f, "in vec4 angelica_ClipDist1;"); + final String body = squash(f); + final int mainIdx = body.indexOf("voidmain(){"); + assertTrue(mainIdx >= 0, f); + assertTrue(body.startsWith(squash(ClipDistanceGlsl.FS_DISCARD), mainIdx + "voidmain(){".length()), "discard is not the first statement of main:\n" + f); + } } diff --git a/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerDiskCacheTest.java b/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerDiskCacheTest.java index 6713a456b..6bcb00f60 100644 --- a/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerDiskCacheTest.java +++ b/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerDiskCacheTest.java @@ -1,11 +1,15 @@ package net.coderbot.iris.pipeline.transform; import com.gtnewhorizons.angelica.glsm.RenderSystem; +import com.gtnewhorizons.angelica.glsm.ffp.Instancing; import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache; +import com.gtnewhorizons.angelica.glsm.testutil.ClipTestSupport; import com.gtnewhorizons.angelica.glsm.testutil.Reflect; import com.gtnewhorizons.angelica.glsm.texture.TextureType; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; import net.coderbot.iris.helpers.Tri; +import net.coderbot.iris.pipeline.transform.parameter.AttributeParameters; import net.coderbot.iris.pipeline.transform.parameter.ComputeParameters; import net.coderbot.iris.pipeline.transform.parameter.TextureStageParameters; import net.coderbot.iris.shaderpack.texture.TextureStage; @@ -97,6 +101,23 @@ class ShaderTransformerDiskCacheTest { assertNotEquals(before, after); } + @Test + void keyChangesWithClipEmulationAndClipDiscard() { + final InputAvailability avail = InputAvailability.of(true, true); + final AttributeParameters unclipped = new AttributeParameters(Patch.ATTRIBUTES, false, false, avail, false, Instancing.NONE, false, false); + final AttributeParameters clipped = new AttributeParameters(Patch.ATTRIBUTES, false, false, avail, false, Instancing.NONE, false, true); + final String nativeKey; + try (var ignored = ClipTestSupport.emulation(false)) { + nativeKey = ShaderTransformer.diskKey(Patch.ATTRIBUTES, compositeInputs(), unclipped).hex(); + } + try (var ignored = ClipTestSupport.emulation(true)) { + final String emulatedKey = ShaderTransformer.diskKey(Patch.ATTRIBUTES, compositeInputs(), unclipped).hex(); + final String clippedKey = ShaderTransformer.diskKey(Patch.ATTRIBUTES, compositeInputs(), clipped).hex(); + assertNotEquals(nativeKey, emulatedKey); + assertNotEquals(emulatedKey, clippedKey); + } + } + private static Object2ObjectOpenHashMap, String> textureMap(int capacity) { final Object2ObjectOpenHashMap, String> m = new Object2ObjectOpenHashMap<>(capacity); m.put(new Tri<>("colortex0", TextureType.TEXTURE_2D, TextureStage.COMPOSITE_AND_FINAL), "a.png");