diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java index fba390e65..05ca1b7d2 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/Feature.java @@ -302,7 +302,7 @@ public class Feature { )); attribToFeatures.put(GL11.GL_SCISSOR_BIT, ImmutableSet.of( GLStateManager.getScissorTest() - // Scissor box + , GLStateManager.getScissorState() )); attribToFeatures.put(GL11.GL_STENCIL_BUFFER_BIT, ImmutableSet.of( GLStateManager.getStencilTest() diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/FeedbackManager.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/FeedbackManager.java index 993008900..21a06c436 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/FeedbackManager.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/FeedbackManager.java @@ -48,6 +48,10 @@ public class FeedbackManager { return renderMode == GL11.GL_FEEDBACK; } + public static int getRenderMode() { + return renderMode; + } + public static void glFeedbackBuffer(int type, FloatBuffer buffer) { if (renderMode == GL11.GL_FEEDBACK) { GLStateManager.LOGGER.error("glFeedbackBuffer called while already in feedback mode"); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java index 1dfbfee41..d210f6ca7 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java @@ -23,6 +23,7 @@ import com.gtnewhorizons.angelica.glsm.stacks.MaterialStateStack; import com.gtnewhorizons.angelica.glsm.stacks.MatrixModeStack; import com.gtnewhorizons.angelica.glsm.stacks.PointStateStack; import com.gtnewhorizons.angelica.glsm.stacks.PolygonStateStack; +import com.gtnewhorizons.angelica.glsm.stacks.ScissorStateStack; import com.gtnewhorizons.angelica.glsm.stacks.StackIdAllocator; import com.gtnewhorizons.angelica.glsm.stacks.StencilStateStack; import com.gtnewhorizons.angelica.glsm.stacks.ViewPortStateStack; @@ -41,6 +42,7 @@ import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; +import org.lwjgl.opengl.GL30; import java.nio.DoubleBuffer; import java.nio.FloatBuffer; @@ -125,6 +127,7 @@ public class GLContextState { public final BlendStateStack blendState = member(new BlendStateStack(StackIdAllocator.nextId())); public final BooleanStateStack blendMode = track(new BooleanStateStack(GL11.GL_BLEND)); public final BooleanStateStack scissorTest = track(new BooleanStateStack(GL11.GL_SCISSOR_TEST)); + public final BooleanStateStack rasterizerDiscard = track(new BooleanStateStack(GL30.GL_RASTERIZER_DISCARD)); public final DepthStateStack depthState = member(new DepthStateStack(StackIdAllocator.nextId())); public final BooleanStateStack depthTest = track(new BooleanStateStack(GL11.GL_DEPTH_TEST)); public final FogStateStack fogState = member(new FogStateStack(StackIdAllocator.nextId())); @@ -200,6 +203,7 @@ public class GLContextState { public final MaterialStateStack frontMaterial = member(new MaterialStateStack(GL11.GL_FRONT, StackIdAllocator.nextId())); public final MaterialStateStack backMaterial = member(new MaterialStateStack(GL11.GL_BACK, StackIdAllocator.nextId())); public final ViewPortStateStack viewportState = member(new ViewPortStateStack(StackIdAllocator.nextId())); + public final ScissorStateStack scissorState = member(new ScissorStateStack(StackIdAllocator.nextId())); public int activeProgram = 0; public final IntegerStateStack programStack = member(new IntegerStateStack(0, StackIdAllocator.nextId())); public int listBase = 0; diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java index fbd82a137..7cccc1cba 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java @@ -49,6 +49,7 @@ import com.gtnewhorizons.angelica.glsm.stacks.MaterialStateStack; import com.gtnewhorizons.angelica.glsm.stacks.MatrixModeStack; import com.gtnewhorizons.angelica.glsm.stacks.PointStateStack; import com.gtnewhorizons.angelica.glsm.stacks.PolygonStateStack; +import com.gtnewhorizons.angelica.glsm.stacks.ScissorStateStack; import com.gtnewhorizons.angelica.glsm.stacks.StackIdAllocator; import com.gtnewhorizons.angelica.glsm.stacks.StencilStateStack; import com.gtnewhorizons.angelica.glsm.stacks.TextureBindingStack; @@ -585,6 +586,7 @@ public class GLStateManager { // Initialize viewport state from display dimensions. // After Display.create(), viewport is (0, 0, width, height) mod(ctx().viewportState).setViewPort(0, 0, displayWidth, displayHeight); + mod(ctx().scissorState).setScissor(0, 0, displayWidth, displayHeight); } final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR); @@ -762,6 +764,7 @@ public class GLStateManager { case GL13.GL_SAMPLE_ALPHA_TO_COVERAGE -> glCtx.sampleAlphaToCoverageState.enable(); case GL13.GL_SAMPLE_ALPHA_TO_ONE -> glCtx.sampleAlphaToOneState.enable(); case GL13.GL_SAMPLE_COVERAGE -> glCtx.sampleCoverageState.enable(); + case GL30.GL_RASTERIZER_DISCARD -> glCtx.rasterizerDiscard.enable(); case GL11.GL_SCISSOR_TEST -> enableScissorTest(); case GL11.GL_STENCIL_TEST -> glCtx.stencilTest.enable(); case GL11.GL_TEXTURE_1D -> glCtx.textures.getTexture1DStates(glCtx.activeTextureUnit.getValue()).enable(); @@ -844,6 +847,7 @@ public class GLStateManager { case GL13.GL_SAMPLE_ALPHA_TO_COVERAGE -> glCtx.sampleAlphaToCoverageState.disable(); case GL13.GL_SAMPLE_ALPHA_TO_ONE -> glCtx.sampleAlphaToOneState.disable(); case GL13.GL_SAMPLE_COVERAGE -> glCtx.sampleCoverageState.disable(); + case GL30.GL_RASTERIZER_DISCARD -> glCtx.rasterizerDiscard.disable(); case GL11.GL_SCISSOR_TEST -> disableScissorTest(); case GL11.GL_STENCIL_TEST -> glCtx.stencilTest.disable(); case GL11.GL_TEXTURE_1D -> glCtx.textures.getTexture1DStates(glCtx.activeTextureUnit.getValue()).disable(); @@ -920,6 +924,7 @@ public class GLStateManager { case GL13.GL_SAMPLE_ALPHA_TO_COVERAGE -> glCtx.sampleAlphaToCoverageState.isEnabled(); case GL13.GL_SAMPLE_ALPHA_TO_ONE -> glCtx.sampleAlphaToOneState.isEnabled(); case GL13.GL_SAMPLE_COVERAGE -> glCtx.sampleCoverageState.isEnabled(); + case GL30.GL_RASTERIZER_DISCARD -> glCtx.rasterizerDiscard.isEnabled(); case GL11.GL_SCISSOR_TEST -> glCtx.scissorTest.isEnabled(); case GL11.GL_STENCIL_TEST -> glCtx.stencilTest.isEnabled(); case GL11.GL_TEXTURE_1D -> glCtx.textures.getTexture1DStates(glCtx.activeTextureUnit.getValue()).isEnabled(); @@ -997,6 +1002,7 @@ public class GLStateManager { case GL13.GL_SAMPLE_ALPHA_TO_COVERAGE -> glCtx.sampleAlphaToCoverageState.isEnabled(); case GL13.GL_SAMPLE_ALPHA_TO_ONE -> glCtx.sampleAlphaToOneState.isEnabled(); case GL13.GL_SAMPLE_COVERAGE -> glCtx.sampleCoverageState.isEnabled(); + case GL30.GL_RASTERIZER_DISCARD -> glCtx.rasterizerDiscard.isEnabled(); case GL11.GL_SCISSOR_TEST -> glCtx.scissorTest.isEnabled(); case GL11.GL_STENCIL_TEST -> glCtx.stencilTest.isEnabled(); case GL11.GL_TEXTURE_1D -> glCtx.textures.getTexture1DStates(glCtx.activeTextureUnit.getValue()).isEnabled(); @@ -1125,6 +1131,7 @@ public class GLStateManager { switch (pname) { case GL11.GL_VIEWPORT -> ctx().viewportState.get(params); + case GL11.GL_SCISSOR_BOX -> ctx().scissorState.get(params); case GL11.GL_POLYGON_MODE -> { final PolygonState polygon = ctx().polygonState; final int pos = params.position(); @@ -1950,6 +1957,7 @@ public class GLStateManager { return; } } + depth = Math.min(1.0, Math.max(0.0, depth)); mod(glCtx.depthState); if (isCachingEnabled()) glCtx.depthState.setClearValue(depth); RENDER_BACKEND.clearDepth(depth); @@ -3954,6 +3962,7 @@ public class GLStateManager { RENDER_BACKEND.viewport(glCtx.viewportState.x, glCtx.viewportState.y, glCtx.viewportState.width, glCtx.viewportState.height); RENDER_BACKEND.depthRange(glCtx.viewportState.depthRangeNear, glCtx.viewportState.depthRangeFar); + RENDER_BACKEND.scissor(glCtx.scissorState.x, glCtx.scissorState.y, glCtx.scissorState.width, glCtx.scissorState.height); RENDER_BACKEND.lineWidth(Math.clamp(glCtx.lineState.getWidth(), lineWidthMin, lineWidthMax)); RENDER_BACKEND.pointSize(glCtx.pointState.getSize()); @@ -4064,6 +4073,11 @@ public class GLStateManager { RENDER_BACKEND.depthRange(glCtx.viewportState.depthRangeNear, glCtx.viewportState.depthRangeFar); attribBackendCalls += 2; } + if ((restore & StateSet.R_SCISSOR) != 0 && (changed & StateSet.R_SCISSOR) != 0) { + attribValueRestores++; + RENDER_BACKEND.scissor(glCtx.scissorState.x, glCtx.scissorState.y, glCtx.scissorState.width, glCtx.scissorState.height); + attribBackendCalls++; + } if ((restore & StateSet.R_LINE) != 0 && (changed & StateSet.R_LINE) != 0) { attribValueRestores++; RENDER_BACKEND.lineWidth(Math.clamp(glCtx.lineState.getWidth(), lineWidthMin, lineWidthMax)); @@ -4152,6 +4166,9 @@ public class GLStateManager { return; } } + if (ctx().rasterizerDiscard.isEnabled() || FeedbackManager.getRenderMode() != GL11.GL_RENDER) { + return; + } if (firstClearPending && Thread.currentThread() == MainThread && getDrawFramebuffer() == 0) { firstClearPending = false; final long start = System.nanoTime(); @@ -5945,6 +5962,9 @@ public class GLStateManager { } } RENDER_BACKEND.scissor(x, y, width, height); + if (isCachingEnabled()) { + mod(ctx().scissorState).setScissor(x, y, width, height); + } } public static void glStencilFunc(int func, int ref, int mask) { @@ -6860,6 +6880,13 @@ public class GLStateManager { ctx().viewportState.get(params); } } + case GL11.GL_SCISSOR_BOX -> { + if (params.length < 4) { + RENDER_BACKEND.getInteger(pname, params); + } else { + ctx().scissorState.get(params); + } + } case GL11.GL_POLYGON_MODE -> { final PolygonState polygon = ctx().polygonState; params[0] = polygon.getFrontMode(); @@ -8187,6 +8214,10 @@ public class GLStateManager { return ctx().viewportState; } + public static ScissorStateStack getScissorState() { + return ctx().scissorState; + } + public static int getActiveProgram() { return ctx().activeProgram; } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/StateSet.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/StateSet.java index 7457b4ba8..bc8509651 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/StateSet.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/StateSet.java @@ -23,6 +23,7 @@ public final class StateSet { public static final int R_TEXTURE = 1 << 12; public static final int R_ACTIVE_UNIT = 1 << 13; public static final int R_PROGRAM = 1 << 14; + public static final int R_SCISSOR = 1 << 15; public static final int B_FRAG_COLORBUF = 1; public static final int B_FRAG_TEXTURE = 1 << 1; @@ -205,6 +206,9 @@ public final class StateSet { if ((mask & GL11.GL_VIEWPORT_BIT) != 0) { restore |= R_VIEWPORT; } + if ((mask & GL11.GL_SCISSOR_BIT) != 0) { + restore |= R_SCISSOR; + } if ((mask & GL11.GL_LINE_BIT) != 0) { restore |= R_LINE; } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/CowDispatch.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/CowDispatch.java index 0c4b21d8f..cb1e8bb28 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/CowDispatch.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/CowDispatch.java @@ -40,6 +40,7 @@ public final class CowDispatch { public static final byte LINE = 20; public static final byte TEXTURE_BINDING = 21; public static final byte TEX_ENV = 22; + public static final byte SCISSOR = 23; public static byte kindOf(CowStateStack s) { final Class c = s.getClass(); @@ -66,6 +67,7 @@ public final class CowDispatch { if (c == LineStateStack.class) return LINE; if (c == TextureBindingStack.class) return TEXTURE_BINDING; if (c == TexEnvState.class) return TEX_ENV; + if (c == ScissorStateStack.class) return SCISSOR; throw new IllegalStateException("Unknown CowStateStack class: " + c.getName()); } @@ -94,6 +96,7 @@ public final class CowDispatch { case LINE -> ((LineStateStack) s).topSlotChanged(); case TEXTURE_BINDING -> ((TextureBindingStack) s).topSlotChanged(); case TEX_ENV -> ((TexEnvState) s).topSlotChanged(); + case SCISSOR -> ((ScissorStateStack) s).topSlotChanged(); default -> throw new IllegalStateException("Unknown kind: " + kind); }; } @@ -123,6 +126,7 @@ public final class CowDispatch { case LINE -> ((LineStateStack) s).restoreSlot(slot); case TEXTURE_BINDING -> ((TextureBindingStack) s).restoreSlot(slot); case TEX_ENV -> ((TexEnvState) s).restoreSlot(slot); + case SCISSOR -> ((ScissorStateStack) s).restoreSlot(slot); default -> throw new IllegalStateException("Unknown kind: " + kind); } } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/ScissorStateStack.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/ScissorStateStack.java new file mode 100644 index 000000000..1f4493837 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/stacks/ScissorStateStack.java @@ -0,0 +1,44 @@ +package com.gtnewhorizons.angelica.glsm.stacks; + +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.StateSet; +import com.gtnewhorizons.angelica.glsm.states.ScissorState; + +public final class ScissorStateStack extends ScissorState implements CowStateStack { + + protected final ScissorState[] stack; + private final CowDepths cow = new CowDepths(StateSet.R_SCISSOR); + + public ScissorStateStack(int id) { + cow.id = id; + stack = new ScissorState[GLStateManager.MAX_ATTRIB_STACK_DEPTH]; + for (int i = 0; i < GLStateManager.MAX_ATTRIB_STACK_DEPTH; i++) { + stack[i] = new ScissorState(); + } + } + + @Override + public CowDepths cowDepths() { + return cow; + } + + @Override + public void captureSlot(int s) { + stack[s].set(this); + } + + @Override + public void restoreSlot(int s) { + set(stack[s]); + } + + @Override + public boolean topSlotChanged() { + return !cow.isEmpty() && !sameAs(stack[cow.top()]); + } + + @Override + public int stackId() { + return cow.id; + } +} diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/ScissorState.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/ScissorState.java new file mode 100644 index 000000000..46be26324 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/ScissorState.java @@ -0,0 +1,54 @@ +package com.gtnewhorizons.angelica.glsm.states; + +import java.nio.IntBuffer; + +public class ScissorState implements ISettableState { + public int x; + public int y; + public int width; + public int height; + + public void setScissor(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public void get(IntBuffer params) { + params.put(0, x); + params.put(1, y); + params.put(2, width); + params.put(3, height); + } + + public void get(int[] params) { + params[0] = x; + params[1] = y; + params[2] = width; + params[3] = height; + } + + @Override + public ScissorState set(ScissorState state) { + this.x = state.x; + this.y = state.y; + this.width = state.width; + this.height = state.height; + + return this; + } + + @Override + public boolean sameAs(Object state) { + if (this == state) return true; + if (!(state instanceof ScissorState scissorState)) return false; + return x == scissorState.x && y == scissorState.y && width == scissorState.width && height == scissorState.height; + } + + @Override + public ScissorState copy() { + return new ScissorState().set(this); + } + +} diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PopAttribMaskFidelity_GLTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PopAttribMaskFidelity_GLTest.java index 121261397..dc2fbb550 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PopAttribMaskFidelity_GLTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_PopAttribMaskFidelity_GLTest.java @@ -3,8 +3,10 @@ package com.gtnewhorizons.angelica.glsm; import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack; import org.junit.jupiter.api.Test; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyIsEnabled; +import static com.gtnewhorizons.angelica.util.GLSMUtil.verifyState; import static org.junit.jupiter.api.Assertions.assertEquals; @GLCoreTest @@ -170,6 +172,66 @@ public class GLSM_PopAttribMaskFidelity_GLTest { } } + @Test + void scissorBoxFollowsScissorBitOnly() { + final int[] boxA = { 10, 20, 100, 200 }; + final int[] boxB = { 30, 40, 50, 60 }; + try { + GLStateManager.glScissor(boxA[0], boxA[1], boxA[2], boxA[3]); + GLStateManager.disableScissorTest(); + verifyState(GL11.GL_SCISSOR_BOX, boxA, "Scissor box - Initial"); + verifyIsEnabled(GL11.GL_SCISSOR_TEST, false, "Scissor - Initial"); + + GLStateManager.glPushAttrib(GL11.GL_SCISSOR_BIT); + GLStateManager.enableScissorTest(); + GLStateManager.glScissor(boxB[0], boxB[1], boxB[2], boxB[3]); + GLStateManager.glPopAttrib(); + + verifyState(GL11.GL_SCISSOR_BOX, boxA, "Scissor box - GL_SCISSOR_BIT member, must restore"); + verifyIsEnabled(GL11.GL_SCISSOR_TEST, false, "Scissor - GL_SCISSOR_BIT member, must restore"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.enableScissorTest(); + GLStateManager.glScissor(boxB[0], boxB[1], boxB[2], boxB[3]); + GLStateManager.glPopAttrib(); + + verifyState(GL11.GL_SCISSOR_BOX, boxB, "Scissor box - Not a GL_ENABLE_BIT member, must stay changed"); + verifyIsEnabled(GL11.GL_SCISSOR_TEST, false, "Scissor - GL_ENABLE_BIT member, must restore"); + } finally { + GLStateManager.disableScissorTest(); + GLStateManager.glScissor(0, 0, 800, 600); + } + } + + @Test + void rasterizerDiscardIsNotAnEnableBitMember() { + try { + verifyIsEnabled(GL30.GL_RASTERIZER_DISCARD, false, "RasterizerDiscard - Initial"); + + GLStateManager.glEnable(GL30.GL_RASTERIZER_DISCARD); + verifyIsEnabled(GL30.GL_RASTERIZER_DISCARD, true, "RasterizerDiscard - After enable"); + + GLStateManager.glPushAttrib(GL11.GL_ENABLE_BIT); + GLStateManager.glDisable(GL30.GL_RASTERIZER_DISCARD); + verifyIsEnabled(GL30.GL_RASTERIZER_DISCARD, false, "RasterizerDiscard - After disable inside GL_ENABLE_BIT"); + GLStateManager.glPopAttrib(); + + verifyIsEnabled(GL30.GL_RASTERIZER_DISCARD, false, "RasterizerDiscard - Not a GL_ENABLE_BIT member, must stay disabled"); + } finally { + GLStateManager.glDisable(GL30.GL_RASTERIZER_DISCARD); + } + } + + @Test + void clearDepthIsClamped() { + try { + GLStateManager.glClearDepth(2.0); + verifyState(GL11.GL_DEPTH_CLEAR_VALUE, 1.0f, "Depth clear value - glClearDepth(2.0) must clamp to 1.0"); + } finally { + GLStateManager.glClearDepth(1.0); + } + } + @Test void colorBufferBitPopDoesNotRestoreClipPlaneButMaskStaysFaithful() { try { diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_ValueStackCow_GLTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_ValueStackCow_GLTest.java index e90a06185..1658b09b5 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_ValueStackCow_GLTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_ValueStackCow_GLTest.java @@ -45,7 +45,7 @@ public class GLSM_ValueStackCow_GLTest { "glAlphaFunc", "glBlendFunc", "tryBlendFuncSeparate", "glBlendEquation", "glBlendEquationSeparate", "glBlendColor", "glColorMask", "glClearColor", "glDrawBuffer", "glLogicOp", "glDepthFunc", "glDepthMask", "glClearDepth", "glStencilFunc", "glStencilOp", - "glStencilMask", "glClearStencil", "glViewport", "glDepthRange", "glLineWidth", + "glStencilMask", "glClearStencil", "glViewport", "glScissor", "glDepthRange", "glLineWidth", "glLineStipple", "glPointSize", "glPolygonMode", "glCullFace", "glFrontFace", "glPolygonOffset", "fogColor", "glFogf", "glFogi", "glLightf", "glLighti", "glLight", "glLightModelf", "glLightModeli", "glLightModel", @@ -74,6 +74,7 @@ public class GLSM_ValueStackCow_GLTest { () -> GLStateManager.glStencilMask(0x0F), () -> GLStateManager.glClearStencil(9), () -> GLStateManager.glViewport(1, 2, 3, 4), + () -> GLStateManager.glScissor(1, 2, 3, 4), () -> GLStateManager.glDepthRange(0.2, 0.8), () -> GLStateManager.glLineWidth(3.0f), () -> GLStateManager.glLineStipple(3, (short) 0x00FF), 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 90551b962..0055505cd 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 @@ -162,6 +162,7 @@ public class SDLGPURenderBackend extends RenderBackend { private boolean droppedSsboWriteWarned; private boolean depthClearSuppressedWarned; private boolean stencilClearSuppressedWarned; + private boolean partialClearFallbackWarned; private final DeferredCopyOp deferredCopyOp = new DeferredCopyOp(); private TransferThread transferThread; @@ -814,6 +815,7 @@ public class SDLGPURenderBackend extends RenderBackend { st.scissorDirty = true; } case GL31.GL_PRIMITIVE_RESTART -> cs.primitiveRestartEnabled = on; + case GL30.GL_RASTERIZER_DISCARD -> cs.rasterizerDiscard = on; } } @@ -954,28 +956,78 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void clearStencil(int s) { s().stencilClearValue = s & 0xFF; } @Override public void clear(int mask) { - final ContextState cs = s(); + final ContextState st = s(); if (!frameManager.isFrameActive()) return; - final boolean wantColor = (mask & GL11.GL_COLOR_BUFFER_BIT) != 0; + boolean wantColor = (mask & GL11.GL_COLOR_BUFFER_BIT) != 0; boolean wantDepth = (mask & GL11.GL_DEPTH_BUFFER_BIT) != 0; boolean wantStencil = (mask & GL11.GL_STENCIL_BUFFER_BIT) != 0; - if (wantDepth && !cs.pipeline.depthWriteEnabled) { + if (wantDepth && !st.pipeline.depthWriteEnabled) { wantDepth = false; if (!depthClearSuppressedWarned) { depthClearSuppressedWarned = true; LOG.warn("clear: GL_DEPTH_BUFFER_BIT requested with glDepthMask(false); clear suppressed per GL semantics"); } } - if (wantStencil && cs.pipeline.stencilWriteMask == 0) { + if (wantStencil && st.pipeline.stencilWriteMask == 0) { wantStencil = false; if (!stencilClearSuppressedWarned) { stencilClearSuppressedWarned = true; LOG.warn("clear: GL_STENCIL_BUFFER_BIT requested with a zero glStencilMask; clear suppressed per GL semantics"); } } + if (wantColor && (st.pipeline.colorWriteMask & 0xF) == 0) { + wantColor = false; + } if (!wantColor && !wantDepth && !wantStencil) return; - final ContextState st = cs; + final FboState fbo; + if (st.boundFboId == 0) { + fbo = null; + } else { + fbo = resourceManager.getFbo(st.boundFboId); + if (fbo == null) return; + if (fbo.targetsDirty) fbo.recomputeTargets(); + if (fbo.cachedFormatsDirty && fbo.hasAnyColor) fboClearTracker.updatePipelineCacheColorFormats(st, fbo); + } + + final AttachmentClear.Target target = fbo != null ? attachmentClear.forFbo(fbo) : attachmentClear.forFbo0(cachedSwapchainFormatArray, frameManager.getFbo0Width(), frameManager.getFbo0Height()); + + int skipSlots = 0; + boolean colorPartial = false; + if (wantColor) { + final int colorWriteMask = st.pipeline.colorWriteMask; + final int[] formats = target.colorFormats; + final int slotCount = formats != null ? formats.length : 0; + boolean anyLive = false; + for (int i = 0; i < slotCount; i++) { + final boolean present; + if (fbo != null) { + final int db = target.drawBuffers[i]; + present = db >= 0 && db < ContextState.MAX_COLOR_ATTACHMENTS && fbo.colorTextures[db] != 0; + } else { + present = i == 0; + } + if (!present) continue; + final int fm = PixelOps.colorComponentMask(formats[i]); + final int m = colorWriteMask & fm; + if (m == 0) { + skipSlots |= 1 << i; + } else { + anyLive = true; + if (m != fm) colorPartial = true; + } + } + wantColor = anyLive; + } + + if (target.width > 0 && target.height > 0) { + final int coverage = attachmentClear.coverage(st, target); + if (coverage == AttachmentClear.EMPTY) return; + final boolean partial = coverage == AttachmentClear.PARTIAL || colorPartial || (wantStencil && (st.pipeline.stencilWriteMask & 0xFF) != 0xFF); + if (partial && !(wantColor && attachmentClear.hasIntegerColorTarget(target)) && clearPartial(st, fbo, target, wantColor, wantDepth, wantStencil)) { + return; + } + } // FBO0 path: just record pending state; ensureFbo0RenderPass consumes it lazily. if (st.boundFboId == 0) { @@ -998,15 +1050,9 @@ public class SDLGPURenderBackend extends RenderBackend { return; } - final FboState fbo = resourceManager.getFbo(st.boundFboId); - if (fbo == null) return; - - if (!wantColor && (wantDepth || wantStencil)) { - final FrameState f = frameManager.frame(); - if (attachmentClear.eligible(st, f, fbo, wantDepth, wantStencil) - && attachmentClear.clearInPass(st, f, fbo, wantDepth, wantStencil)) { - return; - } + final FrameState f = frameManager.frame(); + if (!wantColor && !SystemProperties.SDL_DISABLE_IN_PASS_CLEAR && f.renderPass != 0 && f.activeLayoutHash == fbo.structuralLayoutHash && !FBOClearTracker.fboHasPendingClear(st, fbo) && clearPartial(st, fbo, target, false, wantDepth, wantStencil)) { + return; } boolean affectsActivePass = false; @@ -1015,7 +1061,10 @@ public class SDLGPURenderBackend extends RenderBackend { final long activeDepth = passActive ? frameManager.getCurrentDepthTarget() : 0; if (wantColor) { - for (int db : fbo.drawBuffers) { + final int[] drawBuffers = fbo.drawBuffers; + for (int i = 0; i < drawBuffers.length; i++) { + if ((skipSlots & (1 << i)) != 0) continue; + final int db = drawBuffers[i]; if (db < 0 || db >= ContextState.MAX_COLOR_ATTACHMENTS) continue; final long tex = fbo.colorTextures[db]; if (tex == 0) continue; @@ -1024,11 +1073,11 @@ public class SDLGPURenderBackend extends RenderBackend { } } if (wantDepth && fbo.depthTexture != 0) { - FBOClearTracker.recordPendingDepthClear(st, fbo.depthTexture, cs.depthClearValue); + FBOClearTracker.recordPendingDepthClear(st, fbo.depthTexture, st.depthClearValue); if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true; } if (wantStencil && fbo.depthTexture != 0 && PixelOps.isDepthStencilFormat(fbo.depthFormat)) { - FBOClearTracker.recordPendingStencilClear(st, fbo.depthTexture, cs.stencilClearValue); + FBOClearTracker.recordPendingStencilClear(st, fbo.depthTexture, st.stencilClearValue); if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true; } @@ -1037,6 +1086,33 @@ public class SDLGPURenderBackend extends RenderBackend { } } + private boolean clearPartial(ContextState st, FboState fbo, AttachmentClear.Target t, boolean color, boolean depth, boolean stencil) { + final FrameState f = frameManager.frame(); + pipelineApplier.ensureRenderPass(st, f); + final long expectedLayout = fbo == null ? FrameManager.FBO0_LAYOUT_HASH : fbo.structuralLayoutHash; + if (f.renderPass == 0 || f.activeLayoutHash != expectedLayout) { + return warnPartialClearFallback(); + } + if (fbo == null) { + t.depthTexture = f.currentDepthTarget; + t.depthFormat = t.depthTexture != 0 ? resourceManager.getSwapchainDepthStencilFormat() : 0; + } + color &= t.colorFormats != null && (fbo == null || fbo.hasAnyColor); + depth &= t.depthTexture != 0; + stencil &= PixelOps.isDepthStencilFormat(t.depthFormat); + if (!color && !depth && !stencil) return true; + if (attachmentClear.clearInPass(st, f, t, color, depth, stencil)) return true; + return warnPartialClearFallback(); + } + + private boolean warnPartialClearFallback() { + if (!partialClearFallbackWarned) { + partialClearFallbackWarned = true; + LOG.error("clear: in-pass partial clear unavailable; falling back to the legacy clear path"); + } + return false; + } + @Override public void lineWidth(float width) { /* no-op */ } @Override public void pointSize(float size) { /* no-op */ } @Override public void logicOp(int opcode) { /* no-op */ } @@ -1051,9 +1127,10 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void drawArrays(int mode, int first, int count) { if (count <= 0) return; + final ContextState st = s(); + if (st.rasterizerDiscard) return; final FrameState f = frameManager.frame(); if (!f.frameActive) { f.droppedDrawsThisFrame++; return; } - final ContextState st = s(); if (mode == GL11.GL_TRIANGLE_FAN && count >= 3) { if (!drawDispatch.drawTriangleFanAsTriangleList(st, first, count)) { f.droppedDrawsThisFrame++; return; } if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "arraysFan", mode, count, 0, first, 0); @@ -1096,6 +1173,7 @@ public class SDLGPURenderBackend extends RenderBackend { } private long prepareIndexedDrawBind(ContextState st, int mode, int type, String opName) { + if (st.rasterizerDiscard) return 0; if (!drawDispatch.prepareIndexedDraw(st, mode, type)) return 0; final int ebo = st.currentVao.elementBuffer; final long eboHandle = resourceManager.getBufferHandle(ebo); @@ -1868,6 +1946,7 @@ public class SDLGPURenderBackend extends RenderBackend { srcWidth = meta != null ? meta.width() : fbo.width; srcHeight = meta != null ? meta.height() : fbo.height; srcY = y; + if (frameManager.isFrameActive()) fboClearTracker.materializePendingClearForTexture(cs, texHandle); } if (texHandle == 0 || srcWidth <= 0 || srcHeight <= 0) return; @@ -3747,6 +3826,7 @@ public class SDLGPURenderBackend extends RenderBackend { case GL11.GL_SCISSOR_TEST -> cs.scissorEnabled; case GL11.GL_STENCIL_TEST -> cs.pipeline.stencilTestEnabled; case GL11.GL_DEPTH_WRITEMASK -> cs.pipeline.depthWriteEnabled; + case GL30.GL_RASTERIZER_DISCARD -> cs.rasterizerDiscard; default -> false; }; } @@ -4024,8 +4104,9 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void drawArraysInstanced(int mode, int first, int count, int primcount) { if (count <= 0 || primcount <= 0) return; - if (!frameManager.isFrameActive()) return; final ContextState st = s(); + if (st.rasterizerDiscard) return; + if (!frameManager.isFrameActive()) return; drawDispatch.setPrimitiveTypeForDraw(st, FormatMap.mapPrimitiveType(mode)); pipelineApplier.ensureRenderPass(st); if (!frameManager.isRenderPassActive()) return; 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 4bb5e559a..d232da60d 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 @@ -113,6 +113,7 @@ public final class ContextState { public float viewportDepthFar = 1.0f; public int scissorX, scissorY, scissorW, scissorH; public boolean scissorEnabled; + public boolean rasterizerDiscard; public int activeTextureUnit; public int boundProgram; public int autoPushedProgram; diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClear.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClear.java index e2391ee6b..58195172a 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClear.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClear.java @@ -1,10 +1,9 @@ package com.gtnewhorizons.angelica.sdlgpu.pipeline; -import com.gtnewhorizons.angelica.config.SystemProperties; +import com.gtnewhorizons.angelica.glsm.GlslTransformUtils; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState; -import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker; import com.gtnewhorizons.angelica.sdlgpu.resource.FboState; import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager; @@ -23,7 +22,10 @@ import org.lwjgl.sdl.SDL_GPURasterizerState; import org.lwjgl.sdl.SDL_GPUVertexInputState; import org.lwjgl.sdl.SDL_GPUViewport; import org.lwjgl.sdl.SDL_Rect; +import org.lwjgl.system.MemoryStack; +import org.taumc.glsl.Transformer; +import java.nio.ByteBuffer; import java.util.Arrays; import java.util.function.LongSupplier; @@ -38,15 +40,33 @@ public final class AttachmentClear { private static final String FRAGMENT_SOURCE = "angelica:sdlgpu/clear_attachment.fsh"; private static final long KEY_SEED = 0x5B1D2A7C4E9F3061L; private static final int CACHE_SLOTS = 4; + private static final int MAX_COLOR_TARGETS = ContextState.MAX_COLOR_ATTACHMENTS; + + public static final int EMPTY = 0; + public static final int PARTIAL = 1; + public static final int FULL = 2; + + public static final class Target { + public int[] colorFormats; + public int[] drawBuffers; + public int depthFormat; + public long depthTexture; + public int width; + public int height; + public boolean flipY; + } private final FrameManager frameManager; private final PipelineStore pipelineStore; private final ShaderManager shaderManager; - private int program; + private final Target fboTarget = new Target(); + private final Target fbo0Target = new Target(); + + private int vertexShaderId; private long sdlVertexShader; - private long sdlFragmentShader; - private boolean failed; + private final long[] sdlFragmentShaders = new long[MAX_COLOR_TARGETS + 1]; + private final boolean[] fragmentFailed = new boolean[MAX_COLOR_TARGETS + 1]; private final long[] cacheKeys = new long[CACHE_SLOTS]; private final long[] cachePipelines = new long[CACHE_SLOTS]; @@ -55,12 +75,12 @@ public final class AttachmentClear { private final int[] scissorRect = new int[4]; private final Builder builder = new Builder(); - private int[] buildColorFormats; - private int buildColorCount; - private int buildDepthFormat; + private Target buildTarget; + private int buildColorTargets; private boolean buildDepthFlag; private boolean buildStencilFlag; - private int buildWriteMask; + private int buildStencilMask; + private int buildColorMask; public AttachmentClear(FrameManager frameManager, PipelineStore pipelineStore, ShaderManager shaderManager) { this.frameManager = frameManager; @@ -72,40 +92,69 @@ public final class AttachmentClear { @Override public long getAsLong() { return createPipeline(); } } - public boolean eligible(ContextState st, FrameState f, FboState fbo, boolean wantDepth, boolean wantStencil) { - if (SystemProperties.SDL_DISABLE_IN_PASS_CLEAR) return false; - if (st.boundFboId == 0) return false; - if (f.renderPass == 0) return false; - if (fbo.depthTexture == 0) return false; - if (f.currentDepthTarget != fbo.depthTexture) return false; - if (f.activeLayoutHash != fbo.structuralLayoutHash || f.currentColorTarget != fbo.primaryTarget) return false; - if (FBOClearTracker.fboHasPendingClear(st, fbo)) return false; - if (wantStencil && !PixelOps.isDepthStencilFormat(fbo.depthFormat)) return false; - if (fbo.width <= 0 || fbo.height <= 0) return false; - return computeScissor(st, fbo); + public Target forFbo(FboState fbo) { + final Target t = fboTarget; + t.colorFormats = fbo.cachedColorFormats; + t.drawBuffers = fbo.drawBuffers; + t.depthFormat = fbo.depthTexture != 0 ? fbo.depthFormat : 0; + t.depthTexture = fbo.depthTexture; + t.width = fbo.width; + t.height = fbo.height; + t.flipY = false; + return t; } - private boolean computeScissor(ContextState st, FboState fbo) { + public Target forFbo0(int[] swapchainFormats, int w, int h) { + final Target t = fbo0Target; + t.colorFormats = swapchainFormats; + t.drawBuffers = null; + t.depthFormat = 0; + t.depthTexture = 0; + t.width = w; + t.height = h; + t.flipY = true; + return t; + } + + static boolean enabled(Target t, int i) { + return t.drawBuffers == null || t.drawBuffers[i] >= 0; + } + + public boolean hasIntegerColorTarget(Target t) { + final int[] formats = t.colorFormats; + if (formats == null) return false; + for (int i = 0; i < formats.length; i++) { + if (enabled(t, i) && PixelOps.isSdlFormatInteger(formats[i])) return true; + } + return false; + } + + public int coverage(ContextState st, Target t) { final int sx, sy, sw, sh; if (st.scissorEnabled) { sx = st.scissorX; - sy = st.scissorY; + sy = t.flipY ? t.height - st.scissorY - st.scissorH : st.scissorY; sw = st.scissorW; sh = st.scissorH; } else { sx = 0; sy = 0; - sw = fbo.width; - sh = fbo.height; + sw = t.width; + sh = t.height; } - return ScissorClamp.clamp(sx, sy, sw, sh, fbo.width, fbo.height, scissorRect); + if (!ScissorClamp.clamp(sx, sy, sw, sh, t.width, t.height, scissorRect)) return EMPTY; + if (scissorRect[ScissorClamp.X] == 0 && scissorRect[ScissorClamp.Y] == 0 + && scissorRect[ScissorClamp.W] == t.width && scissorRect[ScissorClamp.H] == t.height) return FULL; + return PARTIAL; } - public boolean clearInPass(ContextState st, FrameState f, FboState fbo, boolean depth, boolean stencil) { - final int writeMask = stencil ? (st.pipeline.stencilWriteMask & 0xFF) : 0; - final long pipeline = getOrCreatePipeline(fbo, depth, stencil, writeMask); + public boolean clearInPass(ContextState st, FrameState f, Target t, boolean color, boolean depth, boolean stencil) { + final int colorTargets = color ? t.colorFormats.length : 0; + final int stencilMask = stencil ? (st.pipeline.stencilWriteMask & 0xFF) : 0; + final int colorMask = color ? st.pipeline.colorWriteMask : 0; + final long pipeline = getOrCreatePipeline(t, colorTargets, depth, stencil, stencilMask, colorMask); if (pipeline == 0) return false; - if (!computeScissor(st, fbo)) return false; + if (coverage(st, t) == EMPTY) return false; final long rp = f.renderPass; SDL_BindGPUGraphicsPipeline(rp, pipeline); @@ -114,8 +163,8 @@ public final class AttachmentClear { final long vpAddr = st.cachedViewport.address(); MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.X, 0f); MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.Y, 0f); - MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.W, fbo.width); - MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.H, fbo.height); + MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.W, t.width); + MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.H, t.height); MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.MIN_DEPTH, clearDepth); MemoryAccess.putFloat(vpAddr + SDL_GPUViewport.MAX_DEPTH, clearDepth); SDL_SetGPUViewport(rp, st.cachedViewport); @@ -131,6 +180,18 @@ public final class AttachmentClear { SDL_SetGPUStencilReference(rp, (byte) st.stencilClearValue); } + if (colorTargets > 0) { + try (MemoryStack stack = stackPush()) { + final ByteBuffer clearColor = stack.malloc(16); + clearColor.putFloat(0, st.clearR); + clearColor.putFloat(4, st.clearG); + clearColor.putFloat(8, st.clearB); + clearColor.putFloat(12, st.clearA); + SDL_PushGPUFragmentUniformData(frameManager.getCommandBuffer(f), 0, clearColor); + } + st.lastPushedProgramFs = -1; + } + SDL_DrawGPUPrimitives(rp, 3, 1, 0, 0); st.lastBoundPipeline = 0; @@ -139,34 +200,40 @@ public final class AttachmentClear { st.lastAppliedStencilRef = Integer.MIN_VALUE; st.lastAppliedVboBindCb = 0; - if (depth) st.clearedTexturesThisFrame.add(fbo.depthTexture); - if (stencil) st.clearedStencilTexturesThisFrame.add(fbo.depthTexture); + if (t.depthTexture != 0) { + if (depth) st.clearedTexturesThisFrame.add(t.depthTexture); + if (stencil) st.clearedStencilTexturesThisFrame.add(t.depthTexture); + } frameManager.noteInPassClear(); return true; } - long getOrCreatePipeline(FboState fbo, boolean depthFlag, boolean stencilFlag, int writeMask) { - if (!ensureShaders()) return 0; + long getOrCreatePipeline(Target t, int colorTargets, boolean depth, boolean stencil, int stencilMask, int colorMask) { + if (!ensureShaders(colorTargets)) return 0; - final int[] formats = fbo.cachedColorFormats; + final int[] formats = t.colorFormats; final int count = formats != null ? formats.length : 0; long key = KEY_SEED; key = Hashing.fmix64(key, count); - for (int i = 0; i < count; i++) key = Hashing.fmix64(key, formats[i]); - key = Hashing.fmix64(key, fbo.depthFormat); - key = Hashing.fmix64(key, (depthFlag ? 1 : 0) | ((stencilFlag ? 1 : 0) << 1) | (writeMask << 8)); + for (int i = 0; i < count; i++) { + key = Hashing.fmix64(key, formats[i]); + key = Hashing.fmix64(key, enabled(t, i) ? 1 : 0); + } + key = Hashing.fmix64(key, t.depthFormat); + key = Hashing.fmix64(key, colorTargets); + key = Hashing.fmix64(key, (depth ? 1 : 0) | ((stencil ? 1 : 0) << 1) | (stencilMask << 8) | (colorMask << 16)); for (int i = 0; i < CACHE_SLOTS; i++) { if (cacheKeys[i] == key && cachePipelines[i] != 0) return cachePipelines[i]; } - buildColorFormats = formats; - buildColorCount = count; - buildDepthFormat = fbo.depthFormat; - buildDepthFlag = depthFlag; - buildStencilFlag = stencilFlag; - buildWriteMask = writeMask; + buildTarget = t; + buildColorTargets = colorTargets; + buildDepthFlag = depth; + buildStencilFlag = stencil; + buildStencilMask = stencilMask; + buildColorMask = colorMask; final long pipeline = pipelineStore.getOrBuild(key, builder); if (pipeline != 0) { cacheKeys[cacheNext] = key; @@ -178,24 +245,27 @@ public final class AttachmentClear { private long createPipeline() { try (var stack = stackPush()) { - final int numColorTargets = buildColorCount; + final Target t = buildTarget; + final int numColorTargets = t.colorFormats != null ? t.colorFormats.length : 0; final SDL_GPUColorTargetDescription.Buffer colorDesc = SDL_GPUColorTargetDescription.calloc(numColorTargets, stack); for (int i = 0; i < numColorTargets; i++) { final var desc = colorDesc.get(i); - desc.format(buildColorFormats[i]); + desc.format(t.colorFormats[i]); + final int mask = (buildColorTargets > 0 && enabled(t, i)) ? buildColorMask : 0; desc.blend_state() .enable_blend(false) - .color_write_mask((byte) 0) + .color_write_mask((byte) mask) .enable_color_write_mask(true); } + final boolean hasDepthTarget = t.depthFormat != 0; final SDL_GPUDepthStencilState depthStencil = SDL_GPUDepthStencilState.calloc(stack) .enable_depth_test(buildDepthFlag) .enable_depth_write(buildDepthFlag) .compare_op(SDL_GPU_COMPAREOP_ALWAYS) .enable_stencil_test(buildStencilFlag) .compare_mask((byte) 0xFF) - .write_mask((byte) buildWriteMask); + .write_mask((byte) buildStencilMask); if (buildStencilFlag) { depthStencil.front_stencil_state() .fail_op(SDL_GPU_STENCILOP_REPLACE) @@ -220,14 +290,14 @@ public final class AttachmentClear { final SDL_GPUGraphicsPipelineTargetInfo targetInfo = SDL_GPUGraphicsPipelineTargetInfo.calloc(stack) .num_color_targets(numColorTargets) .color_target_descriptions(colorDesc) - .depth_stencil_format(buildDepthFormat) - .has_depth_stencil_target(true); + .depth_stencil_format(t.depthFormat) + .has_depth_stencil_target(hasDepthTarget); final SDL_GPUVertexInputState vertexInput = SDL_GPUVertexInputState.calloc(stack); final SDL_GPUGraphicsPipelineCreateInfo ci = SDL_GPUGraphicsPipelineCreateInfo.calloc(stack) .vertex_shader(sdlVertexShader) - .fragment_shader(sdlFragmentShader) + .fragment_shader(sdlFragmentShaders[buildColorTargets]) .primitive_type(SDL_GPU_PRIMITIVETYPE_TRIANGLELIST) .rasterizer_state(rasterizer) .multisample_state(multisample) @@ -244,48 +314,78 @@ public final class AttachmentClear { } } - private boolean ensureShaders() { - if (program != 0) return true; - if (failed) return false; - failed = true; + private boolean ensureShaders(int colorTargets) { + if (colorTargets < 0 || colorTargets > MAX_COLOR_TARGETS) return false; + if (sdlFragmentShaders[colorTargets] != 0) return true; + if (fragmentFailed[colorTargets]) return false; + fragmentFailed[colorTargets] = true; + + if (vertexShaderId == 0) { + vertexShaderId = shaderManager.createShader(GL20.GL_VERTEX_SHADER); + shaderManager.shaderSource(vertexShaderId, ShaderLoader.getShaderSource(VERTEX_SOURCE)); + shaderManager.compileShader(vertexShaderId); + } + + final String baseSource = ShaderLoader.getShaderSource(FRAGMENT_SOURCE); + final String fragmentSource = colorTargets == 0 ? baseSource : withColorOutputs(baseSource, colorTargets); - final int vs = shaderManager.createShader(GL20.GL_VERTEX_SHADER); - shaderManager.shaderSource(vs, ShaderLoader.getShaderSource(VERTEX_SOURCE)); - shaderManager.compileShader(vs); final int fs = shaderManager.createShader(GL20.GL_FRAGMENT_SHADER); - shaderManager.shaderSource(fs, ShaderLoader.getShaderSource(FRAGMENT_SOURCE)); + shaderManager.shaderSource(fs, fragmentSource); shaderManager.compileShader(fs); final int prog = shaderManager.createProgram(); - shaderManager.attachShader(prog, vs); + shaderManager.attachShader(prog, vertexShaderId); shaderManager.attachShader(prog, fs); shaderManager.linkProgram(prog); final ShaderManager.ProgramObject po = shaderManager.getProgram(prog); if (po == null || !po.linked) { - LOG.error("AttachmentClear: clear_attachment program failed to link: {}", po == null ? "no program object" : po.infoLog); + LOG.error("AttachmentClear: clear_attachment program (COLOR_TARGETS={}) failed to link: {}", colorTargets, po == null ? "no program object" : po.infoLog); + return false; + } + if (colorTargets > 0 && po.fragmentUboSize != 16) { + LOG.error("AttachmentClear: clear_attachment fragment UBO size {} != 16 for COLOR_TARGETS={}", po.fragmentUboSize, colorTargets); return false; } - po.sdlVertexShader = shaderManager.createSDLShader(po.vertexSpirv, SDL_GPU_SHADERSTAGE_VERTEX, po.vertexResources.numSamplers(), po.vertexResources.numUBOs(), po.vertexResources.numStorageBuffers(), po.vertexResources.numStorageTextures()); - po.sdlFragmentShader = shaderManager.createSDLShader(po.fragmentSpirv, SDL_GPU_SHADERSTAGE_FRAGMENT, po.fragmentResources.numSamplers(), po.fragmentResources.numUBOs(), po.fragmentResources.numStorageBuffers(), po.fragmentResources.numStorageTextures()); - if (po.sdlVertexShader == 0 || po.sdlFragmentShader == 0) { - LOG.error("AttachmentClear: clear_attachment SDL shader creation failed"); + if (sdlVertexShader == 0) { + sdlVertexShader = shaderManager.createSDLShader(po.vertexSpirv, SDL_GPU_SHADERSTAGE_VERTEX, po.vertexResources.numSamplers(), po.vertexResources.numUBOs(), po.vertexResources.numStorageBuffers(), po.vertexResources.numStorageTextures()); + if (sdlVertexShader == 0) { + LOG.error("AttachmentClear: clear_attachment vertex SDL shader creation failed"); + return false; + } + } + + final long fragmentShader = shaderManager.createSDLShader(po.fragmentSpirv, SDL_GPU_SHADERSTAGE_FRAGMENT, po.fragmentResources.numSamplers(), po.fragmentResources.numUBOs(), po.fragmentResources.numStorageBuffers(), po.fragmentResources.numStorageTextures()); + if (fragmentShader == 0) { + LOG.error("AttachmentClear: clear_attachment fragment SDL shader creation failed (COLOR_TARGETS={})", colorTargets); return false; } - program = prog; - sdlVertexShader = po.sdlVertexShader; - sdlFragmentShader = po.sdlFragmentShader; - failed = false; + sdlFragmentShaders[colorTargets] = fragmentShader; + fragmentFailed[colorTargets] = false; return true; } + private static String withColorOutputs(String source, int count) { + final GlslTransformUtils.QuietParse parsed = GlslTransformUtils.parseBothQuiet(source); + final Transformer transformer = new Transformer(parsed.full()); + transformer.injectVariable("uniform vec4 clearColor;"); + for (int i = 0; i < count; i++) { + transformer.injectVariable("layout(location = " + i + ") out vec4 angelica_ClearColor" + i + ";"); + transformer.appendMain("angelica_ClearColor" + i + " = clearColor;"); + } + final String header = GlslTransformUtils.getFormattedShader(parsed.pre(), "").trim(); + final StringBuilder out = new StringBuilder(); + transformer.mutateTree(tree -> out.append(GlslTransformUtils.getFormattedShader(tree, header))); + return out.toString(); + } + public void shutdown() { - program = 0; + vertexShaderId = 0; sdlVertexShader = 0; - sdlFragmentShader = 0; - failed = false; + Arrays.fill(sdlFragmentShaders, 0L); + Arrays.fill(fragmentFailed, false); Arrays.fill(cacheKeys, 0L); Arrays.fill(cachePipelines, 0L); cacheNext = 0; diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java index a0da4b849..cba815a2f 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java @@ -74,6 +74,14 @@ public final class PixelOps { }; } + public static int colorComponentMask(int sdlFormat) { + int mask = 0; + for (int c = 0; c < 4; c++) { + if (colorChannelBits(sdlFormat, c) > 0) mask |= 1 << c; + } + return mask; + } + private static int uniformChannelBits(int sdlFormat) { return switch (sdlFormat) { case SDL_GPU_TEXTUREFORMAT_R8_UNORM, SDL_GPU_TEXTUREFORMAT_R8_SNORM, @@ -174,6 +182,16 @@ public final class PixelOps { }; } + public static boolean isSdlFormatInteger(int sdlFormat) { + if (isSdlFormatUnsignedInteger(sdlFormat)) return true; + return switch (sdlFormat) { + case SDL_GPU_TEXTUREFORMAT_R8_INT, SDL_GPU_TEXTUREFORMAT_R8G8_INT, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_INT, + SDL_GPU_TEXTUREFORMAT_R16_INT, SDL_GPU_TEXTUREFORMAT_R16G16_INT, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_INT, + SDL_GPU_TEXTUREFORMAT_R32_INT, SDL_GPU_TEXTUREFORMAT_R32G32_INT, SDL_GPU_TEXTUREFORMAT_R32G32B32A32_INT -> true; + default -> false; + }; + } + public static ByteBuffer expandToRGBA(int format, ByteBuffer src, int width, int height) { if (src == null) return null; final int pixelCount = width * height; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlAttachmentClearTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlAttachmentClearTest.java index 38ff2dbe4..17b8f3360 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlAttachmentClearTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlAttachmentClearTest.java @@ -10,9 +10,14 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; import sun.misc.Unsafe; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -67,6 +72,12 @@ class GlsmSdlAttachmentClearTest { return r > 200 && g < 64; } + private static boolean isGreen(int argb) { + final int r = (argb >> 16) & 0xFF; + final int g = (argb >> 8) & 0xFF; + return g > 200 && r < 64; + } + private static boolean depthEquals(float depthValue) { GLStateManager.glEnable(GL11.GL_DEPTH_TEST); GLStateManager.glDepthFunc(GL11.GL_EQUAL); @@ -358,4 +369,205 @@ class GlsmSdlAttachmentClearTest { assertTrue(Math.abs(g - 102) <= 2, "green must be untouched: " + g); assertTrue(Math.abs(b - 153) <= 2, "blue must be untouched: " + b); } + + @Test + void scissoredColorAndDepthClearStaysInTheRect() { + GlsmSdlHeadlessRig.bindTarget(); + GLStateManager.glDepthMask(true); + GLStateManager.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClearDepth(0.9); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + GlsmSdlHeadlessRig.solidQuad(0.0f, 0.0f, 1.0f); + + final FrameManager.FrameState f = frame(); + final long passBefore = f.renderPass; + assertTrue(passBefore != 0, "expected an active render pass before the scissored clear"); + final int inPassBefore = f.inPassClearsThisFrame; + + GLStateManager.glEnable(GL11.GL_SCISSOR_TEST); + GLStateManager.glScissor(0, 0, SIZE / 2, SIZE); + GLStateManager.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClearDepth(0.0); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT); + GLStateManager.glDisable(GL11.GL_SCISSOR_TEST); + + assertEquals(passBefore, f.renderPass, "the scissored clear must not break the active render pass"); + assertEquals(inPassBefore + 1, f.inPassClearsThisFrame, "the scissored clear must take the in-pass path"); + + final int probeSize = 4; + final int leftProbeX = SIZE / 2 - probeSize * 2; + final int rightProbeX = SIZE / 2 + probeSize; + + GLStateManager.glEnable(GL11.GL_DEPTH_TEST); + GLStateManager.glDepthFunc(GL11.GL_EQUAL); + GLStateManager.glDepthMask(false); + + GLStateManager.glEnable(GL11.GL_SCISSOR_TEST); + GLStateManager.glScissor(leftProbeX, 0, probeSize, SIZE); + drawFullQuadAtZ(0.0f * 2.0f - 1.0f, 0.0f, 1.0f, 0.0f); + GLStateManager.glScissor(rightProbeX, 0, probeSize, SIZE); + drawFullQuadAtZ(0.9f * 2.0f - 1.0f, 0.0f, 1.0f, 0.0f); + GLStateManager.glDisable(GL11.GL_SCISSOR_TEST); + GLStateManager.glDisable(GL11.GL_DEPTH_TEST); + + final int[] pixels = GlsmSdlHeadlessRig.readTarget(); + assertTrue(isRed(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 8, SIZE / 2)), "inside the scissor rect must be the clear color"); + assertFalse(isRed(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE - SIZE / 8, SIZE / 2)), "outside the scissor rect must keep the prior blue quad"); + assertTrue(isGreen(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, leftProbeX + probeSize / 2, SIZE / 2)), "inside the scissor rect the depth must have been cleared to 0"); + assertTrue(isGreen(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, rightProbeX + probeSize / 2, SIZE / 2)), "outside the scissor rect the prior depth must be untouched"); + } + + @Test + void scissoredClearAfterPendingFullClear() { + GlsmSdlHeadlessRig.bindTarget(); + GLStateManager.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + + GLStateManager.glEnable(GL11.GL_SCISSOR_TEST); + GLStateManager.glScissor(0, 0, SIZE / 2, SIZE); + GLStateManager.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + + GLStateManager.glScissor(0, 0, 0, SIZE); + GLStateManager.glClearColor(0.0f, 0.0f, 1.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GLStateManager.glDisable(GL11.GL_SCISSOR_TEST); + + final int[] pixels = GlsmSdlHeadlessRig.readTarget(); + assertTrue(isGreen(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 4, SIZE / 2)), "the scissored clear must have painted the left half green"); + assertTrue(isRed(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 2 + SIZE / 4, SIZE / 2)), "the empty-scissor clear must not have touched the right half"); + } + + @Test + void colorMaskIsHonoredByClear() { + GlsmSdlHeadlessRig.bindTarget(); + GlsmSdlHeadlessRig.solidQuad(0.2f, 0.4f, 0.6f); + + GLStateManager.glColorMask(true, false, false, true); + GLStateManager.glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GLStateManager.glColorMask(true, true, true, true); + + final int[] pixels = GlsmSdlHeadlessRig.readTarget(); + final int argb = GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 2, SIZE / 2); + final int r = (argb >> 16) & 0xFF; + final int g = (argb >> 8) & 0xFF; + final int b = argb & 0xFF; + assertEquals(255, r, "red must be written: colorMask allowed it"); + assertTrue(Math.abs(g - 102) <= 2, "green must be untouched by the masked clear: " + g); + assertTrue(Math.abs(b - 153) <= 2, "blue must be untouched by the masked clear: " + b); + } + + @Test + void scissoredClearWritesEveryDrawBuffer() { + final int texA = createColorTexture(); + final int texB = createColorTexture(); + + final int mrtFbo = GLStateManager.glGenFramebuffers(); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, mrtFbo); + GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texA, 0); + GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT1, GL11.GL_TEXTURE_2D, texB, 0); + GLStateManager.glViewport(0, 0, SIZE, SIZE); + GLStateManager.glDrawBuffers(IntBuffer.wrap(new int[]{ GL30.GL_COLOR_ATTACHMENT0, GL30.GL_COLOR_ATTACHMENT1 })); + + GLStateManager.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + + GLStateManager.glEnable(GL11.GL_SCISSOR_TEST); + GLStateManager.glScissor(0, 0, SIZE / 2, SIZE); + GLStateManager.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GLStateManager.glDisable(GL11.GL_SCISSOR_TEST); + + final int readFbo = GLStateManager.glGenFramebuffers(); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, readFbo); + + assertLeftHalfRed(texA, "texture A"); + assertLeftHalfRed(texB, "texture B"); + + GLStateManager.glDeleteFramebuffers(readFbo); + GLStateManager.glDeleteFramebuffers(mrtFbo); + GLStateManager.glDeleteTextures(texA); + GLStateManager.glDeleteTextures(texB); + + GlsmSdlHeadlessRig.bindTarget(); + } + + private static int createColorTexture() { + return createColorTexture(GL11.GL_RGBA8, GL11.GL_RGBA); + } + + private static int createColorTexture(int internalFormat, int format) { + final int tex = GLStateManager.glGenTextures(); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, internalFormat, SIZE, SIZE, 0, format, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); + return tex; + } + + private static void assertLeftHalfRed(int texture, String label) { + GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 0); + final int[] pixels = GlsmSdlHeadlessRig.readTarget(); + assertTrue(isRed(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 4, SIZE / 2)), label + " left half must be cleared red"); + assertFalse(isRed(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 2 + SIZE / 4, SIZE / 2)), label + " right half must keep the base clear"); + } + + @Test + void rasterizerDiscardSkipsClearsAndDraws() { + GlsmSdlHeadlessRig.bindTarget(); + GlsmSdlHeadlessRig.solidQuad(1.0f, 0.0f, 0.0f); + + GLStateManager.glEnable(GL30.GL_RASTERIZER_DISCARD); + GLStateManager.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GlsmSdlHeadlessRig.solidQuad(0.0f, 0.0f, 1.0f); + GLStateManager.glDisable(GL30.GL_RASTERIZER_DISCARD); + + final int[] pixels = GlsmSdlHeadlessRig.readTarget(); + assertTrue(isRed(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 2, SIZE / 2)), "rasterizer discard must skip both the clear and the draw"); + } + + @Test + void clearIsIgnoredOutsideRenderMode() { + GlsmSdlHeadlessRig.bindTarget(); + GlsmSdlHeadlessRig.solidQuad(1.0f, 0.0f, 0.0f); + + final FloatBuffer feedbackBuffer = BufferUtils.createFloatBuffer(16); + GLStateManager.glFeedbackBuffer(GL11.GL_3D, feedbackBuffer); + GLStateManager.glRenderMode(GL11.GL_FEEDBACK); + GLStateManager.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GLStateManager.glRenderMode(GL11.GL_RENDER); + + final int[] pixels = GlsmSdlHeadlessRig.readTarget(); + assertTrue(isRed(GlsmSdlHeadlessRig.pixelAt(pixels, SIZE, SIZE / 2, SIZE / 2)), "a clear issued outside GL_RENDER mode must be ignored"); + } + + @Test + void colorMaskCoveringEveryPresentChannelIsAFullClear() { + final int tex = createColorTexture(GL30.GL_R8, GL11.GL_RED); + final int fbo = GLStateManager.glGenFramebuffers(); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, tex, 0); + GLStateManager.glViewport(0, 0, SIZE, SIZE); + + GLStateManager.glClearColor(0.2f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GlsmSdlHeadlessRig.solidQuad(0.4f, 0.0f, 0.0f); + + final FrameManager.FrameState f = frame(); + final int inPassBefore = f.inPassClearsThisFrame; + + GLStateManager.glColorMask(true, false, false, false); + GLStateManager.glClearColor(0.6f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GLStateManager.glColorMask(true, true, true, true); + + assertEquals(inPassBefore, f.inPassClearsThisFrame, "masking every present channel on a single-red-channel target must take the load-op path, not in-pass"); + + GLStateManager.glDeleteFramebuffers(fbo); + GLStateManager.glDeleteTextures(tex); + GlsmSdlHeadlessRig.bindTarget(); + } } diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFbo0ViewportTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFbo0ViewportTest.java index 445249c3e..0d50ef56f 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFbo0ViewportTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlFbo0ViewportTest.java @@ -95,6 +95,19 @@ class GlsmSdlFbo0ViewportTest { GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(0, FBO0_H - VIEW, VIEW, VIEW), BLUE, "above the viewport"); } + @Test + void scissoredClearFlipsYOnFbo0() { + bindFbo0WithFullViewport(); + GLStateManager.glEnable(GL11.GL_SCISSOR_TEST); + GLStateManager.glScissor(0, 0, FBO0_W, VIEW); + GLStateManager.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); + GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT); + GLStateManager.glDisable(GL11.GL_SCISSOR_TEST); + + GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(0, 0, FBO0_W, VIEW), RED, "GL scissor y=0 is the bottom rows of the FBO0 texture"); + GlsmSdlHeadlessRig.assertUniform(GlsmSdlHeadlessRig.readTarget(0, FBO0_H - VIEW, FBO0_W, VIEW), BLUE, "the top rows must stay untouched by the flipped scissor"); + } + @Test void scissorLargerThanFbo0StillCoversTheWholeTarget() { bindFbo0WithFullViewport(); diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java index a5b11d66d..dad12ec08 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/AttachmentClearTest.java @@ -2,9 +2,6 @@ package com.gtnewhorizons.angelica.sdlgpu.pipeline; import com.gtnewhorizons.angelica.glsm.ffp.VAOManager; import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig; -import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; -import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; -import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker; import com.gtnewhorizons.angelica.sdlgpu.resource.FboState; import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps; import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager; @@ -15,10 +12,8 @@ import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL30; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D32_FLOAT; import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET; class AttachmentClearTest { @@ -76,92 +71,33 @@ class AttachmentClearTest { return fbo; } - @Test - void eligibleTruthTable() { - final FboState fbo = makeFbo(); - final ContextState baseline = new ContextState(); - baseline.boundFboId = 1; - final FrameManager.FrameState f = new FrameManager.FrameState(); - f.renderPass = 0xBEEFL; - f.currentDepthTarget = fbo.depthTexture; - f.currentColorTarget = fbo.primaryTarget; - f.activeLayoutHash = fbo.structuralLayoutHash; - assertTrue(attachmentClear.eligible(baseline, f, fbo, true, false), "baseline row must be eligible"); - - final ContextState noFbo = new ContextState(); - noFbo.boundFboId = 0; - assertFalse(attachmentClear.eligible(noFbo, f, fbo, true, false), "boundFboId==0 must be ineligible"); - - final FrameManager.FrameState noPass = new FrameManager.FrameState(); - noPass.currentDepthTarget = fbo.depthTexture; - noPass.currentColorTarget = fbo.primaryTarget; - noPass.activeLayoutHash = fbo.structuralLayoutHash; - assertFalse(attachmentClear.eligible(baseline, noPass, fbo, true, false), "renderPass==0 must be ineligible"); - - final FrameManager.FrameState wrongDepth = new FrameManager.FrameState(); - wrongDepth.renderPass = 0xBEEFL; - wrongDepth.currentDepthTarget = fbo.depthTexture + 1; - wrongDepth.currentColorTarget = fbo.primaryTarget; - wrongDepth.activeLayoutHash = fbo.structuralLayoutHash; - assertFalse(attachmentClear.eligible(baseline, wrongDepth, fbo, true, false), "depth target mismatch must be ineligible"); - - final FrameManager.FrameState wrongLayout = new FrameManager.FrameState(); - wrongLayout.renderPass = 0xBEEFL; - wrongLayout.currentDepthTarget = fbo.depthTexture; - wrongLayout.currentColorTarget = fbo.primaryTarget; - wrongLayout.activeLayoutHash = fbo.structuralLayoutHash ^ 1L; - assertFalse(attachmentClear.eligible(baseline, wrongLayout, fbo, true, false), "layout hash mismatch must be ineligible"); - - final FrameManager.FrameState wrongColor = new FrameManager.FrameState(); - wrongColor.renderPass = 0xBEEFL; - wrongColor.currentDepthTarget = fbo.depthTexture; - wrongColor.currentColorTarget = fbo.primaryTarget + 1; - wrongColor.activeLayoutHash = fbo.structuralLayoutHash; - assertFalse(attachmentClear.eligible(baseline, wrongColor, fbo, true, false), "color target mismatch must be ineligible"); - - final ContextState pending = new ContextState(); - pending.boundFboId = 1; - FBOClearTracker.recordPendingDepthClear(pending, fbo.depthTexture, 1.0f); - assertFalse(attachmentClear.eligible(pending, f, fbo, true, false), "a pending deferred clear must be ineligible"); - - final FboState depthOnlyFormat = makeFbo(); - depthOnlyFormat.depthFormat = SDL_GPU_TEXTUREFORMAT_D32_FLOAT; - assertFalse(attachmentClear.eligible(baseline, f, depthOnlyFormat, false, true), "stencil clear on a non depth-stencil format must be ineligible"); - assertTrue(attachmentClear.eligible(baseline, f, depthOnlyFormat, true, false), "depth clear on a depth-only format stays eligible"); - - final FboState zeroSize = makeFbo(); - zeroSize.width = 0; - assertFalse(attachmentClear.eligible(baseline, f, zeroSize, true, false), "zero-size target must be ineligible"); - - final ContextState scissoredEmpty = new ContextState(); - scissoredEmpty.boundFboId = 1; - scissoredEmpty.scissorEnabled = true; - scissoredEmpty.scissorX = SIZE + 10; - scissoredEmpty.scissorY = 0; - scissoredEmpty.scissorW = 4; - scissoredEmpty.scissorH = 4; - assertFalse(attachmentClear.eligible(scissoredEmpty, f, fbo, true, false), "a scissor clamped to empty must be ineligible"); - } - @Test void onePipelinePerLayoutModeAndMask() { final FboState fbo = makeFbo(); - final long p1 = attachmentClear.getOrCreatePipeline(fbo, true, false, 0); - final long p1Again = attachmentClear.getOrCreatePipeline(fbo, true, false, 0); + final long p1 = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(fbo), 0, true, false, 0, 0); + final long p1Again = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(fbo), 0, true, false, 0, 0); assertTrue(p1 != 0, "pipeline build must succeed"); assertEquals(p1, p1Again, "the same (layout, mode, mask) must reuse the cached pipeline"); - final long p2 = attachmentClear.getOrCreatePipeline(fbo, false, true, 0xFF); + final long p2 = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(fbo), 0, false, true, 0xFF, 0); assertNotEquals(p1, p2, "a different clear mode must build a different pipeline"); - final long p3 = attachmentClear.getOrCreatePipeline(fbo, false, true, 0x0F); + final long p3 = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(fbo), 0, false, true, 0x0F, 0); assertNotEquals(p2, p3, "a different stencil write mask must build a different pipeline"); final FboState depthOnlyLayout = makeFbo(); depthOnlyLayout.drawBuffers = new int[0]; depthOnlyLayout.cachedColorFormats = new int[0]; - final long p4 = attachmentClear.getOrCreatePipeline(depthOnlyLayout, true, false, 0); + final long p4 = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(depthOnlyLayout), 0, true, false, 0, 0); assertTrue(p4 != 0, "a depth-only layout pipeline must still build"); assertNotEquals(p1, p4, "a different color-target layout must build a different pipeline"); + + final long pc1 = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(fbo), 1, false, false, 0, 0xF); + final long pc1Again = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(fbo), 1, false, false, 0, 0xF); + assertTrue(pc1 != 0, "color pipeline build must succeed"); + assertEquals(pc1, pc1Again, "the same color mask must reuse the cached pipeline"); + + final long pc2 = attachmentClear.getOrCreatePipeline(attachmentClear.forFbo(fbo), 1, false, false, 0, 0x9); + assertNotEquals(pc1, pc2, "a different color write mask must build a different pipeline"); } }