Fix scissor clears wiping the whole screen on SDL-GPU (#2138)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-09-26 13:54:10 -07:00
1 parent b76284048a
commit 44c930f4ab
17 files changed
+740 -171

No files matched your search

@@ -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()
@@ -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");
@@ -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;
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
@@ -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<ScissorStateStack> {
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;
}
}
@@ -0,0 +1,54 @@
package com.gtnewhorizons.angelica.glsm.states;
import java.nio.IntBuffer;
public class ScissorState implements ISettableState<ScissorState> {
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);
}
}
@@ -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 {
@@ -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),
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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();
}
}
@@ -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();
@@ -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");
}
}