SDL followup fixes (#2001)

* Deploy fixes
* Narrow transformer exclusions for Legends mods, and mixin priority for CustomNPC
* Journeymap 6 stencil/depth fix
* OpenLTW swizzle workaround
This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-08-09 13:43:46 -07:00
1 parent 166116d692
commit 8c31eec525
38 files changed
+1771 -107

No files matched your search

+1 -1
View File
@@ -89,4 +89,4 @@ jobs:
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
run: ./gradlew :tracy-client:publish
run: ./gradlew -PpublishTracy=true :tracy-client:publish
+1 -1
View File
@@ -3,7 +3,7 @@ name: Release tagged build
on:
push:
tags: [ '*' ]
tags: [ '*', '!tracy-v*' ]
permissions:
contents: write
+6 -2
View File
@@ -1,3 +1,4 @@
import com.modrinth.minotaur.TaskModrinthUpload
import net.darkhax.curseforgegradle.TaskPublishCurseForge
import xyz.wagyourtail.jvmdg.gradle.flags.DowngradeFlags
import xyz.wagyourtail.jvmdg.gradle.task.DowngradeJar
@@ -76,7 +77,7 @@ val isMacOs = org.gradle.internal.os.OperatingSystem.current().isMacOsX
val isMacOsArm64 = isMacOs && System.getProperty("os.arch") == "aarch64"
tasks.withType<JavaExec>().configureEach {
if (name.startsWith("runClient")) {
if (name.startsWith("runClient") && name != "runClient") {
if (isMacOs) {
// SDL3 / Cocoa / Metal must initialize on the JVM main thread.
jvmArgs("-XstartOnFirstThread")
@@ -228,7 +229,6 @@ val glCoreTest by tasks.registering(Test::class) {
}
tasks.test { finalizedBy(glCoreTest) }
tasks.check { dependsOn(glCoreTest) }
tasks.shadowJar {
dependsOn(embedOnly)
@@ -257,3 +257,7 @@ tasks.shadowJar {
tasks.withType<TaskPublishCurseForge>().configureEach {
uploadArtifacts.forEach { it.addGameVersion("Client") }
}
tasks.withType<TaskModrinthUpload>().configureEach {
setDependsOn(dependsOn.filterNot { it == "build" } + "assemble")
}
@@ -20,6 +20,7 @@ public final class SystemProperties {
public static final boolean USE_SDL_GPU = Boolean.getBoolean(KEY_USE_SDL_GPU);
public static final String GL_PROFILE = System.getProperty("angelica.glProfile", "");
public static final boolean DISABLE_NVIDIA_WORKAROUND = Boolean.getBoolean("angelica.disableNvidiaWorkaround");
public static final boolean DISABLE_LTW_WORKAROUND = Boolean.getBoolean("angelica.disableLtwWorkaround");
// Redirector
public static final UnmappedGLMode UNMAPPED_GL = parseEnum("angelica.unmappedGL", UnmappedGLMode.WARN, UnmappedGLMode.WARN, UnmappedGLMode.class);
@@ -552,6 +552,9 @@ public class GLStateManager {
// GL_STENCIL_BITS was removed in core profile; query via default FBO attachment
final int stencilBits = RENDER_BACKEND.getFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE);
glCtx.stencilBitMask = stencilBits >= 32 ? 0xFFFFFFFF : (1 << stencilBits) - 1;
if (glCtx.stencilBitMask == 0) {
LOGGER.warn("Default framebuffer reports 0 stencil bits on backend {}; all stencil masks clamp to 0", RENDER_BACKEND.getClass().getSimpleName());
}
// Initialize stencil masks from computed bit mask
glCtx.stencilState.setValueMaskFront(glCtx.stencilBitMask);
@@ -1965,6 +1968,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
if (mode == RecordMode.NONE && RenderSystem.isLTW()) LTWWorkaround.onTexImage2D(target, level, format, pixels != null);
maybeGenerateMipmap(target, level);
}
@@ -1983,6 +1987,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
if (mode == RecordMode.NONE && RenderSystem.isLTW()) LTWWorkaround.onTexImage2D(target, level, format, pixels != null);
maybeGenerateMipmap(target, level);
}
@@ -2001,6 +2006,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
if (mode == RecordMode.NONE && RenderSystem.isLTW()) LTWWorkaround.onTexImage2D(target, level, format, pixels != null);
maybeGenerateMipmap(target, level);
}
@@ -2019,6 +2025,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);
restorePixelUnpackBuffer();
if (mode == RecordMode.NONE && RenderSystem.isLTW()) LTWWorkaround.onTexImage2D(target, level, format, pixels != null);
maybeGenerateMipmap(target, level);
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.states.PixelUnpackState;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import java.nio.ByteBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
public final class LTWWorkaround {
private LTWWorkaround() {}
private static ByteBuffer oneTexel;
public static boolean isLtwVersionString(String glVersion) {
return glVersion != null && glVersion.contains("LTW");
}
public static void onTexImage2D(int target, int level, int format, boolean hasPixels) {
if (target != GL11.GL_TEXTURE_2D || level != 0 || format != GL12.GL_BGRA || hasPixels) return;
if (oneTexel == null) {
oneTexel = BufferUtils.createByteBuffer(4);
}
oneTexel.clear();
GLStateManager.forcePixelUnpackState(PixelUnpackState.DEFAULT);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texSubImage2D(target, level, 0, 0, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, oneTexel);
GLStateManager.restorePixelUnpackBuffer();
GLStateManager.restorePixelUnpackState(PixelUnpackState.DEFAULT);
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSAAccess;
@@ -59,6 +60,9 @@ public class RenderSystem {
private static volatile boolean glesDetected;
private static volatile boolean hasClipCullDistance;
private static volatile boolean isLTW;
private static volatile boolean ltwDetected;
// Sampler object state tracking (null if unsupported)
private static int[] samplers;
@@ -403,6 +407,26 @@ public class RenderSystem {
}
}
public static boolean isLTW() {
if (!ltwDetected) detectLTW();
return isLTW;
}
private static synchronized void detectLTW() {
if (ltwDetected) return;
if (SystemProperties.DISABLE_LTW_WORKAROUND) {
ltwDetected = true;
return;
}
try {
final String v = RENDER_BACKEND.getString(GL11.GL_VERSION);
if (v == null) return;
isLTW = LTWWorkaround.isLtwVersionString(v);
ltwDetected = true;
} catch (Throwable ignored) {
}
}
public static boolean supportsSSBO() {
return supportsSSBO;
}
@@ -21,11 +21,11 @@ public final class EcosystemNarrowRules {
"Reika.DragonAPI.ASM.StructureLootHooks"
)),
new NarrowRule("Xaeros_common", "xaero.common.core", List.of(
new NarrowRule("Xaeros", "xaero.common.core", List.of(
"xaero.common.core.transformer"
)),
new NarrowRule("Xaeros_map", "xaero.map.core", List.of(
new NarrowRule("Xaeros", "xaero.map.core", List.of(
"xaero.map.core.transformer"
)),
@@ -78,6 +78,13 @@ public final class EcosystemNarrowRules {
"alfheim.common.core.asm.hook.fixes.CorporeaInputFix",
"alfheim.common.core.asm.hook.fixes.GodAttributesHooks",
"alfheim.common.core.asm.hook.fixes.RecipeAncientWillsFix"
)),
new NarrowRule("LegendsMod", "com.tihyo.legends.management.asm", List.of(
"com.tihyo.legends.management.asm.LegendsLoadingPlugin",
"com.tihyo.legends.management.asm.AbstractTransformer",
"com.tihyo.legends.management.asm.ASMHelper",
"com.tihyo.legends.management.asm.Transformer"
))
);
@@ -25,7 +25,7 @@ public final class TransformerNarrower {
/**
* A rule that replaces a broad transformer exclusion with narrower ones.
* @param name Human-readable name for logging (e.g. "DragonAPI")
* @param name Blackboard/config key suffix. (e.g. "DragonAPI").
* @param broad The broad exclusion to remove (e.g. "Reika.DragonAPI.ASM")
* @param narrow Narrower exclusions to add in its place
*/
@@ -21,6 +21,18 @@ class GLVersionParseTest {
assertEquals(-1, RenderSystem.parseMajorMinor("OpenGL ES 3.2"));
}
@Test
void detectsLtwWrapper() {
assertTrue(LTWWorkaround.isLtwVersionString("3.3 OpenLTW (Built on: May 21 2026/12:38:14)"));
assertFalse(LTWWorkaround.isLtwVersionString(null));
assertFalse(LTWWorkaround.isLtwVersionString(""));
assertFalse(LTWWorkaround.isLtwVersionString("4.6.0 NVIDIA 551.61"));
assertFalse(LTWWorkaround.isLtwVersionString("4.6 (Core Profile) Mesa 24.0.3"));
assertFalse(LTWWorkaround.isLtwVersionString("4.1 Metal - 88"));
assertFalse(LTWWorkaround.isLtwVersionString("OpenGL ES 3.2 ANGLE (Vulkan)"));
assertFalse(LTWWorkaround.isLtwVersionString("2.1 gl4es wrapper"));
}
@Test
void isContextValid() {
assertTrue(RenderSystem.isContextValid(4, 4, 46, true));
@@ -0,0 +1,92 @@
package com.gtnewhorizons.angelica.glsm.loading;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TransformerNarrowerTest {
private static final String LEGENDS_BROAD = "com.tihyo.legends.management.asm";
private static final String LEGENDS_HOOK = LEGENDS_BROAD + ".ASMClientMethods";
private static Set<String> exceptionsWith(String... entries) {
return new HashSet<>(Set.of(entries));
}
private static boolean isExcluded(Set<String> exceptions, String className) {
return exceptions.stream().anyMatch(className::startsWith);
}
private static int narrow(Set<String> exceptions, Map<String, Object> blackboard) {
return TransformerNarrower.narrow(exceptions, blackboard, "angelica", "Angelica", EcosystemNarrowRules.ALL);
}
@Test
void legendsHooksBecomeTransformableButTransformersDoNot() {
final Set<String> exceptions = exceptionsWith(LEGENDS_BROAD);
assertEquals(1, narrow(exceptions, new HashMap<>()));
assertFalse(isExcluded(exceptions, LEGENDS_HOOK));
assertFalse(isExcluded(exceptions, LEGENDS_BROAD + ".ASMMethods"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".LegendsLoadingPlugin"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".AbstractTransformer"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".ASMHelper"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerRenderPlayer"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerEntityRenderer"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerMethodProcess"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerGuiInventory"));
}
@Test
void disabledRuleLeavesBroadExclusionIntact() {
final Set<String> exceptions = exceptionsWith(LEGENDS_BROAD);
final Map<String, Object> blackboard = new HashMap<>();
blackboard.put("angelica.narrow.LegendsMod", Boolean.FALSE);
assertEquals(0, narrow(exceptions, blackboard));
assertTrue(exceptions.contains(LEGENDS_BROAD));
assertTrue(isExcluded(exceptions, LEGENDS_HOOK));
}
@Test
void sharedXaerosKeyGovernsBothExclusions() {
final Set<String> enabled = exceptionsWith("xaero.common.core", "xaero.map.core");
assertEquals(2, narrow(enabled, new HashMap<>()));
assertFalse(isExcluded(enabled, "xaero.common.gui.GuiMap"));
assertFalse(isExcluded(enabled, "xaero.map.gui.GuiMap"));
assertTrue(isExcluded(enabled, "xaero.common.core.transformer.Foo"));
assertTrue(isExcluded(enabled, "xaero.map.core.transformer.Foo"));
final Set<String> disabled = exceptionsWith("xaero.common.core", "xaero.map.core");
final Map<String, Object> blackboard = new HashMap<>();
blackboard.put("angelica.narrow.Xaeros", Boolean.FALSE);
assertEquals(0, narrow(disabled, blackboard));
assertTrue(disabled.contains("xaero.common.core"));
assertTrue(disabled.contains("xaero.map.core"));
}
@Test
void narrowingIsIdempotent() {
final Set<String> exceptions = exceptionsWith(LEGENDS_BROAD);
assertEquals(1, narrow(exceptions, new HashMap<>()));
final Set<String> afterFirst = new HashSet<>(exceptions);
assertEquals(0, narrow(exceptions, new HashMap<>()));
assertEquals(afterFirst, exceptions);
}
@Test
void absentBroadExclusionIsNoOp() {
final Set<String> exceptions = exceptionsWith("some.unrelated.package");
assertEquals(0, narrow(exceptions, new HashMap<>()));
assertEquals(Set.of("some.unrelated.package"), exceptions);
}
}
@@ -549,9 +549,13 @@ public class SDLGPURenderBackend extends RenderBackend {
}
ResourceManager.setSingleThreadedReads(GLStateManager.isSplashComplete());
st.clearedTexturesThisFrame.clear();
st.clearedStencilTexturesThisFrame.clear();
st.pendingColorTextures.clear();
st.pendingDepthTextures.clear();
st.pendingStencilTextures.clear();
st.pendingSwapchainClear = false;
st.pendingSwapchainDepthClear = false;
st.pendingSwapchainStencilClear = false;
st.uniformBlocks[0].flushedThisFrame = false;
st.uniformBlocks[1].flushedThisFrame = false;
st.fanIndexBufferOffset = 0;
@@ -869,14 +873,15 @@ public class SDLGPURenderBackend extends RenderBackend {
}
@Override public void clearDepth(double depth) { s().depthClearValue = (float) depth; }
@Override public void clearStencil(int s) {}
@Override public void clearStencil(int s) { s().stencilClearValue = s & 0xFF; }
@Override public void clear(int mask) {
final ContextState cs = s();
if (!frameManager.isFrameActive()) return;
final boolean wantColor = (mask & GL11.GL_COLOR_BUFFER_BIT) != 0;
final boolean wantDepth = (mask & GL11.GL_DEPTH_BUFFER_BIT) != 0;
if (!wantColor && !wantDepth) return;
final boolean wantStencil = (mask & GL11.GL_STENCIL_BUFFER_BIT) != 0;
if (!wantColor && !wantDepth && !wantStencil) return;
final ContextState st = cs;
@@ -889,7 +894,15 @@ public class SDLGPURenderBackend extends RenderBackend {
st.pendingSwapchainB = st.clearB;
st.pendingSwapchainA = st.clearA;
}
// swapchain has no depth attachment in this backend; ignore wantDepth
final int swapDepthFormat = resourceManager.getSwapchainDepthStencilFormat();
if (wantDepth && swapDepthFormat != 0) {
st.pendingSwapchainDepthClear = true;
st.pendingSwapchainDepth = st.depthClearValue;
}
if (wantStencil && PixelOps.isDepthStencilFormat(swapDepthFormat)) {
st.pendingSwapchainStencilClear = true;
st.pendingSwapchainStencil = st.stencilClearValue;
}
return;
}
@@ -914,6 +927,10 @@ public class SDLGPURenderBackend extends RenderBackend {
FBOClearTracker.recordPendingDepthClear(st, fbo.depthTexture, cs.depthClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
}
if (wantStencil && fbo.depthTexture != 0 && PixelOps.isDepthStencilFormat(fbo.depthFormat)) {
FBOClearTracker.recordPendingStencilClear(st, fbo.depthTexture, cs.stencilClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
}
if (affectsActivePass) {
frameManager.endRenderPassIfActive();
@@ -1459,14 +1476,12 @@ public class SDLGPURenderBackend extends RenderBackend {
if (framebuffer == 0) {
cs.pipeline.setColorTargetFormats(cachedSwapchainFormatArray);
cs.pipeline.setDrawBuffers(null);
cs.pipeline.hasDepthTarget = false;
cs.pipeline.depthTargetFormat = 0;
cs.pipeline.setDepthTargetFormat(resourceManager.getSwapchainDepthStencilFormat());
} else {
final FboState fbo = resourceManager.getFbo(framebuffer);
if (fbo != null && fbo.getColorTexture() != 0) {
fboClearTracker.updatePipelineCacheColorFormats(cs, fbo);
cs.pipeline.hasDepthTarget = fbo.depthTexture != 0;
cs.pipeline.depthTargetFormat = fbo.depthFormat;
cs.pipeline.setDepthTargetFormat(fbo.depthTexture != 0 ? fbo.depthFormat : 0);
}
}
cs.pipeline.markOutputDirty();
@@ -1503,14 +1518,23 @@ public class SDLGPURenderBackend extends RenderBackend {
fbo.cachedFormatsDirty = true;
fboClearTracker.updatePipelineCacheColorFormats(cs, fbo);
cs.pipeline.markOutputDirty();
} else if (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT) {
} else if (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT || attachment == GL30.GL_STENCIL_ATTACHMENT) {
final boolean stencilOnly = attachment == GL30.GL_STENCIL_ATTACHMENT;
if (texture == 0) {
if (stencilOnly && fbo.depthGlId != 0) return;
fbo.detachDepth();
cs.pipeline.hasDepthTarget = false;
cs.pipeline.depthTargetFormat = 0;
cs.pipeline.markOutputDirty();
return;
}
if (stencilOnly && fbo.depthGlId != 0 && fbo.depthGlId != texture) {
if (!warnedSplitStencilAttachment) {
warnedSplitStencilAttachment = true;
LOG.warn("Separate stencil attachment (tex {}) on an FBO whose depth attachment is tex {}; SDL has no separate stencil aspect, keeping depth", texture, fbo.depthGlId);
}
return;
}
final long handle = resourceManager.ensureTextureUsage(texture, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET);
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
fbo.depthTexture = handle;
@@ -1674,22 +1698,67 @@ public class SDLGPURenderBackend extends RenderBackend {
}
@Override public int getFramebufferAttachmentParameteri(int target, int attachment, int pname) {
final FboState fbo = resourceManager.getFbo(s().boundFboId);
final ContextState cs = s();
final int fboId = target == GL30.GL_READ_FRAMEBUFFER ? cs.boundReadFboId : cs.boundFboId;
if (fboId == 0) {
final int colorFormat = cachedSwapchainFormatArray != null ? cachedSwapchainFormatArray[0] : 0;
return defaultFramebufferAttachmentParameteri(attachment, pname, colorFormat, resourceManager.getSwapchainDepthStencilFormat());
}
return fboAttachmentParameteri(resourceManager.getFbo(fboId), attachment, pname);
}
static int fboAttachmentParameteri(FboState fbo, int attachment, int pname) {
if (fbo == null) return 0;
final int colorIdx = attachment - GL30.GL_COLOR_ATTACHMENT0;
final boolean isColor = (colorIdx >= 0 && colorIdx < ContextState.MAX_COLOR_ATTACHMENTS);
final boolean isDepth = (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT);
final boolean isDepth = (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT || attachment == GL30.GL_STENCIL_ATTACHMENT || attachment == GL11.GL_DEPTH || attachment == GL11.GL_STENCIL);
final boolean colorPresent = isColor && fbo.colorGlIds[colorIdx] != 0;
final boolean depthPresent = isDepth && fbo.depthGlId != 0;
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME) {
return isColor ? fbo.colorGlIds[colorIdx] : isDepth ? fbo.depthGlId : 0;
return colorPresent ? fbo.colorGlIds[colorIdx] : depthPresent ? fbo.depthGlId : 0;
}
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) {
if (isColor && fbo.colorGlIds[colorIdx] != 0) return GL11.GL_TEXTURE;
if (isDepth && fbo.depthGlId != 0) return GL11.GL_TEXTURE;
return GL11.GL_NONE;
return (colorPresent || depthPresent) ? GL11.GL_TEXTURE : GL11.GL_NONE;
}
if (colorPresent) return colorAttachmentSize(fbo.colorFormats[colorIdx], pname);
if (depthPresent) return depthAttachmentSize(fbo.depthFormat, pname);
return 0;
}
static int defaultFramebufferAttachmentParameteri(int attachment, int pname, int colorFormat, int depthFormat) {
final boolean isDepth = attachment == GL11.GL_DEPTH || attachment == GL11.GL_STENCIL || attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_STENCIL_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT;
if (isDepth) {
if (depthFormat == 0) {
return pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE ? GL11.GL_NONE : 0;
}
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) return GL30.GL_FRAMEBUFFER_DEFAULT;
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME) return 0;
return depthAttachmentSize(depthFormat, pname);
}
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) return GL30.GL_FRAMEBUFFER_DEFAULT;
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME) return 0;
return colorAttachmentSize(colorFormat, pname);
}
private static int colorAttachmentSize(int sdlFormat, int pname) {
return switch (pname) {
case GL30.GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE -> PixelOps.colorChannelBits(sdlFormat, 0);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE -> PixelOps.colorChannelBits(sdlFormat, 1);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE -> PixelOps.colorChannelBits(sdlFormat, 2);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE -> PixelOps.colorChannelBits(sdlFormat, 3);
default -> 0;
};
}
private static int depthAttachmentSize(int sdlFormat, int pname) {
return switch (pname) {
case GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE -> PixelOps.depthBits(sdlFormat);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE -> PixelOps.stencilBits(sdlFormat);
default -> 0;
};
}
@Override public int createShader(int type) { return shaderManager.createShader(type); }
@Override public void deleteShader(int shader) { shaderManager.deleteShader(shader); }
@Override public void shaderSource(int shader, CharSequence source) { shaderManager.shaderSource(shader, source); }
@@ -3199,6 +3268,8 @@ public class SDLGPURenderBackend extends RenderBackend {
case GL11.GL_DRAW_BUFFER -> drawBufferEnum(cs.boundFboId);
case GL11.GL_READ_BUFFER -> readBufferEnum(cs.boundReadFboId);
case GL11.GL_TEXTURE_BINDING_2D -> boundTextureOf(cs.activeTextureUnit, cs.boundTextures);
case GL11.GL_DEPTH_BITS -> getFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE);
case GL11.GL_STENCIL_BITS -> getFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE);
default -> {
final int limit = deviceLimit(pname);
yield limit >= 0 ? limit : unknownGetInteger(pname);
@@ -3646,6 +3717,7 @@ public class SDLGPURenderBackend extends RenderBackend {
}
private boolean warnedMSAARenderbuffer = false;
private boolean warnedSplitStencilAttachment = false;
@Override
public void renderbufferStorageMultisample(int target, int samples, int internalformat, int width, int height) {
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2FloatOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
@@ -92,6 +93,7 @@ public final class ContextState {
public float blendColorR, blendColorG, blendColorB, blendColorA;
public int stencilRef;
public float depthClearValue = 1.0f;
public int stencilClearValue;
public boolean primitiveRestartEnabled;
public int primitiveRestartSentinel;
@@ -292,13 +294,20 @@ public final class ContextState {
public int boundReadFboId;
public final LongOpenHashSet clearedTexturesThisFrame = new LongOpenHashSet();
public final LongOpenHashSet clearedStencilTexturesThisFrame = new LongOpenHashSet();
public final LongOpenHashSet pendingColorTextures = new LongOpenHashSet();
public final Long2ObjectOpenHashMap<float[]> pendingColorValues = new Long2ObjectOpenHashMap<>();
public final LongOpenHashSet pendingDepthTextures = new LongOpenHashSet();
public final Long2FloatOpenHashMap pendingDepthValues = new Long2FloatOpenHashMap();
public final LongOpenHashSet pendingStencilTextures = new LongOpenHashSet();
public final Long2IntOpenHashMap pendingStencilValues = new Long2IntOpenHashMap();
public boolean pendingSwapchainClear;
public float pendingSwapchainR, pendingSwapchainG, pendingSwapchainB, pendingSwapchainA;
public boolean pendingSwapchainDepthClear;
public boolean pendingSwapchainStencilClear;
public float pendingSwapchainDepth;
public int pendingSwapchainStencil;
public final LongArrayList samplerFlushColorHandles = new LongArrayList();
public final IntArrayList samplerFlushColorGlIds = new IntArrayList();
@@ -57,6 +57,7 @@ public final class FrameManager {
public long activeLayoutHash;
public long frameNumber;
public boolean clearedThisFrame;
public boolean depthClearedThisFrame;
public boolean swapchainUsedThisFrame;
public boolean swapchainAcquiredThisFrame;
public boolean swapchainUnavailable;
@@ -233,6 +234,7 @@ public final class FrameManager {
f.currentDepthTarget = 0;
f.frameNumber++;
f.clearedThisFrame = false;
f.depthClearedThisFrame = false;
f.swapchainUsedThisFrame = false;
f.skipPresent = false;
}
@@ -242,14 +244,34 @@ public final class FrameManager {
f.emptyFramesThisFrame++;
}
public void ensureSwapchainRenderPass(float clearR, float clearG, float clearB, float clearA, boolean clear) {
ensureSwapchainRenderPass(frame(), clearR, clearG, clearB, clearA, clear);
static boolean swapchainClearNeedsPassBreak(ContextState st, FrameState f) {
return (st.pendingSwapchainClear || st.pendingSwapchainDepthClear || st.pendingSwapchainStencilClear) && f.renderPass != 0 && f.currentColorTarget == f.swapchainTexture;
}
public void ensureSwapchainRenderPass(FrameState f, float clearR, float clearG, float clearB, float clearA, boolean clear) {
if (!ensureSwapchainAcquired(f)) return; // present-skip; don't bind tex=0
public boolean ensureSwapchainRenderPass(FrameState f, ContextState st) {
final boolean clearColor = st.pendingSwapchainClear;
if (swapchainClearNeedsPassBreak(st, f)) {
endRenderPassIfActive(f);
}
final boolean applied = ensureSwapchainRenderPass(f,
clearColor ? st.pendingSwapchainR : st.clearR,
clearColor ? st.pendingSwapchainG : st.clearG,
clearColor ? st.pendingSwapchainB : st.clearB,
clearColor ? st.pendingSwapchainA : st.clearA,
clearColor,
st.pendingSwapchainDepthClear, st.pendingSwapchainDepthClear ? st.pendingSwapchainDepth : st.depthClearValue,
st.pendingSwapchainStencilClear, st.pendingSwapchainStencilClear ? st.pendingSwapchainStencil : st.stencilClearValue);
if (!applied) return false;
st.pendingSwapchainClear = false;
st.pendingSwapchainDepthClear = false;
st.pendingSwapchainStencilClear = false;
return true;
}
private boolean ensureSwapchainRenderPass(FrameState f, float clearR, float clearG, float clearB, float clearA, boolean clear, boolean clearDepth, float depthValue, boolean clearStencil, int stencilValue) {
if (!ensureSwapchainAcquired(f)) return false; // present-skip; don't bind tex=0
if (f.renderPass != 0 && f.currentColorTarget == f.swapchainTexture) {
return;
return false;
}
endActiveEncoders(f);
@@ -258,6 +280,12 @@ public final class FrameManager {
clear = true;
}
final long depthTexture = resourceManager != null ? resourceManager.getOrCreateSwapchainDepthStencil(f.swapchainWidth, f.swapchainHeight) : 0L;
if (depthTexture != 0 && !f.depthClearedThisFrame) {
clearDepth = true;
clearStencil = true;
}
try (var stack = stackPush()) {
final SDL_GPUColorTargetInfo.Buffer colorTargets = SDL_GPUColorTargetInfo.calloc(1, stack);
final long ctAddr = colorTargets.get(0).address();
@@ -274,19 +302,34 @@ public final class FrameManager {
f.clearedThisFrame = true;
}
SDL_GPUDepthStencilTargetInfo depthTarget = null;
if (depthTexture != 0) {
depthTarget = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long dtAddr = depthTarget.address();
MemoryAccess.putAddress(dtAddr + SDL_GPUDepthStencilTargetInfo.TEXTURE, depthTexture);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putFloat(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, depthValue);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, clearStencil ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putByte(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) stencilValue);
if (clearDepth && clearStencil) f.depthClearedThisFrame = true;
}
if (preRenderPassHook != null) preRenderPassHook.run();
assertNoEncoderActive(f, "SDL_BeginGPURenderPass(swapchain)");
f.renderPass = SDL_BeginGPURenderPass(f.commandBuffer, colorTargets, null);
f.renderPass = SDL_BeginGPURenderPass(f.commandBuffer, colorTargets, depthTarget);
if (f.renderPass == 0) {
throw new RuntimeException("Failed to begin render pass: " + SDLError.SDL_GetError());
}
f.renderPassGeneration++;
f.swapchainPassesThisFrame++;
f.currentColorTarget = f.swapchainTexture;
f.currentDepthTarget = 0;
f.currentDepthTarget = depthTexture;
f.activeLayoutHash = SWAPCHAIN_LAYOUT_HASH;
f.swapchainUsedThisFrame = true;
}
return true;
}
@@ -20,6 +20,7 @@ import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.resource.PersistentBufferSync;
import com.gtnewhorizons.angelica.sdlgpu.resource.PersistentMapping;
import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.sampler.SamplerBinder;
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageBufferBinder;
@@ -108,11 +109,8 @@ public final class PipelineApplier {
}
if (st.boundFboId == 0) {
if (st.pendingSwapchainClear) {
frameManager.ensureSwapchainRenderPass(f, st.pendingSwapchainR, st.pendingSwapchainG, st.pendingSwapchainB, st.pendingSwapchainA, true);
st.pendingSwapchainClear = false;
} else {
frameManager.ensureSwapchainRenderPass(f, st.clearR, st.clearG, st.clearB, st.clearA, false);
if (frameManager.ensureSwapchainRenderPass(f, st)) {
st.pipeline.setDepthTargetFormat(resourceManager.getSwapchainDepthStencilFormat());
}
return;
}
@@ -145,47 +143,50 @@ public final class PipelineApplier {
tex = dummyTex;
}
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.contains(tex);
final boolean firstUse = !pendingClear && tex != dummyTex && !st.clearedTexturesThisFrame.contains(tex);
final boolean defined = tex != dummyTex && resourceManager.isTextureContentDefined(tex);
final boolean firstUse = !pendingClear && tex != dummyTex && !defined;
if (pendingClear || firstUse) proposedClearOps |= 1 << i;
layoutHash = Hashing.fmix64(layoutHash, tex);
layoutHash = Hashing.fmix64(layoutHash, db);
if (pendingClear || firstUse) {
final float[] c = pendingClear ? st.pendingColorValues.get(tex) : null;
layoutHash = foldClearColor(layoutHash, c != null ? c[0] : st.clearR, c != null ? c[1] : st.clearG, c != null ? c[2] : st.clearB, c != null ? c[3] : st.clearA);
}
}
final boolean proposedDepthClear;
final boolean proposedStencilClear;
final boolean depthHasStencil = fbo.depthTexture != 0 && PixelOps.isDepthStencilFormat(fbo.depthFormat);
if (fbo.depthTexture != 0) {
final boolean pendingClear = st.pendingDepthTextures.contains(fbo.depthTexture);
final boolean firstUse = !pendingClear && !st.clearedTexturesThisFrame.contains(fbo.depthTexture);
proposedDepthClear = pendingClear || firstUse;
final boolean pendingStencil = depthHasStencil && st.pendingStencilTextures.contains(fbo.depthTexture);
if (depthHasStencil) {
final boolean firstStencilUse = !pendingStencil && !st.clearedStencilTexturesThisFrame.contains(fbo.depthTexture);
proposedStencilClear = pendingStencil || firstStencilUse;
} else {
proposedStencilClear = false;
}
layoutHash = Hashing.fmix64(layoutHash, fbo.depthTexture);
if (proposedDepthClear) {
layoutHash = foldClearDepth(layoutHash, pendingClear ? st.pendingDepthValues.get(fbo.depthTexture) : st.depthClearValue);
}
if (proposedStencilClear) {
layoutHash = foldClearStencil(layoutHash, pendingStencil ? st.pendingStencilValues.get(fbo.depthTexture) : st.stencilClearValue);
}
} else {
proposedDepthClear = false;
proposedStencilClear = false;
}
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearR));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearG));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearB));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearA));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.depthClearValue));
frameManager.endRenderPassIfActive(f);
final boolean reuse = fbo.cachedTargetsValid && fbo.cachedTargetsLayoutHash == layoutHash && fbo.cachedTargetsCount == totalTargets && fbo.cachedClearOpFlags == proposedClearOps && fbo.cachedDepthClearLast == proposedDepthClear;
final boolean reuse = fbo.cachedTargetsValid && fbo.cachedTargetsLayoutHash == layoutHash && fbo.cachedTargetsCount == totalTargets && fbo.cachedClearOpFlags == proposedClearOps && fbo.cachedDepthClearLast == proposedDepthClear && fbo.cachedStencilClearLast == proposedStencilClear;
SDL_GPUColorTargetInfo.Buffer colorTargets = null;
SDL_GPUDepthStencilTargetInfo depthTarget = null;
if (reuse) {
for (int i = 0; i < totalTargets; i++) {
final int db = fbo.drawBuffers[i];
final long tex = (db >= 0 && db < ContextState.MAX_COLOR_ATTACHMENTS)
? fbo.colorTextures[db] : 0L;
if (tex != 0) {
st.pendingColorTextures.remove(tex);
if ((proposedClearOps & (1 << i)) != 0) st.clearedTexturesThisFrame.add(tex);
}
}
if (fbo.depthTexture != 0) {
st.pendingDepthTextures.remove(fbo.depthTexture);
if (proposedDepthClear) st.clearedTexturesThisFrame.add(fbo.depthTexture);
}
if (totalTargets > 0) colorTargets = fbo.cachedColorTargets.position(0).limit(totalTargets);
if (fbo.depthTexture != 0) depthTarget = fbo.cachedDepthTarget;
} else {
@@ -199,9 +200,8 @@ public final class PipelineApplier {
} else {
tex = dummyTex;
}
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.remove(tex);
final boolean firstUse = !pendingClear && tex != dummyTex && !st.clearedTexturesThisFrame.contains(tex);
final boolean clearThis = pendingClear || firstUse;
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.contains(tex);
final boolean clearThis = (proposedClearOps & (1 << i)) != 0;
final long ctAddr = colorTargets.address() + (long) i * SDL_GPUColorTargetInfo.SIZEOF;
MemoryAccess.putAddress(ctAddr + SDL_GPUColorTargetInfo.TEXTURE, tex);
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.LOAD_OP, clearThis ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
@@ -220,36 +220,79 @@ public final class PipelineApplier {
MemoryAccess.putFloat(ccAddr + SDL_FColor.B, st.clearB);
MemoryAccess.putFloat(ccAddr + SDL_FColor.A, st.clearA);
}
if (tex != dummyTex) st.clearedTexturesThisFrame.add(tex);
}
}
}
if (fbo.depthTexture != 0) {
final boolean pendingClear = st.pendingDepthTextures.remove(fbo.depthTexture);
final boolean firstUse = !pendingClear && !st.clearedTexturesThisFrame.contains(fbo.depthTexture);
final boolean clearDepth = pendingClear || firstUse;
final boolean pendingClear = st.pendingDepthTextures.contains(fbo.depthTexture);
depthTarget = fbo.cachedDepthTarget;
final long dtAddr = depthTarget.address();
MemoryAccess.putAddress(dtAddr + SDL_GPUDepthStencilTargetInfo.TEXTURE, fbo.depthTexture);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, proposedDepthClear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
if (clearDepth) {
if (proposedDepthClear) {
final float dv = pendingClear ? st.pendingDepthValues.get(fbo.depthTexture) : st.depthClearValue;
MemoryAccess.putFloat(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, dv);
st.clearedTexturesThisFrame.add(fbo.depthTexture);
}
if (depthHasStencil) {
final boolean pendingStencil = st.pendingStencilTextures.contains(fbo.depthTexture);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, proposedStencilClear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
if (proposedStencilClear) {
final int sv = pendingStencil ? st.pendingStencilValues.get(fbo.depthTexture) : st.stencilClearValue;
MemoryAccess.putByte(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) sv);
}
} else {
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
}
}
fbo.cachedTargetsLayoutHash = layoutHash;
fbo.cachedTargetsCount = totalTargets;
fbo.cachedClearOpFlags = proposedClearOps;
fbo.cachedDepthClearLast = proposedDepthClear;
fbo.cachedStencilClearLast = proposedStencilClear;
fbo.cachedTargetsValid = true;
}
consumeClears(resourceManager, st, fbo, dummyTex, proposedClearOps, proposedDepthClear, proposedStencilClear, depthHasStencil);
frameManager.beginRenderPass(colorTargets, depthTarget);
frameManager.setActiveLayoutHash(fbo.structuralLayoutHash);
}
static void consumeClears(ResourceManager rm, ContextState st, FboState fbo, long dummyTex, int clearOps, boolean depthClear, boolean stencilClear, boolean depthHasStencil) {
for (int i = 0, n = fbo.drawBuffers.length; i < n; i++) {
final int db = fbo.drawBuffers[i];
final long tex = (db >= 0 && db < ContextState.MAX_COLOR_ATTACHMENTS) ? fbo.colorTextures[db] : 0L;
if (tex == 0 || tex == dummyTex) continue;
st.pendingColorTextures.remove(tex);
if ((clearOps & (1 << i)) != 0) rm.markTextureContentDefined(tex);
}
if (fbo.depthTexture == 0) return;
st.pendingDepthTextures.remove(fbo.depthTexture);
if (depthClear) st.clearedTexturesThisFrame.add(fbo.depthTexture);
if (depthHasStencil) {
st.pendingStencilTextures.remove(fbo.depthTexture);
if (stencilClear) st.clearedStencilTexturesThisFrame.add(fbo.depthTexture);
}
}
static long foldClearColor(long h, float r, float g, float b, float a) {
h = Hashing.fmix64(h, Float.floatToRawIntBits(r));
h = Hashing.fmix64(h, Float.floatToRawIntBits(g));
h = Hashing.fmix64(h, Float.floatToRawIntBits(b));
return Hashing.fmix64(h, Float.floatToRawIntBits(a));
}
static long foldClearDepth(long h, float depth) {
return Hashing.fmix64(h, Float.floatToRawIntBits(depth));
}
static long foldClearStencil(long h, int stencil) {
return Hashing.fmix64(h, stencil);
}
private static final int RING_BLOCK_BYTES = ContextState.MAX_VERTEX_ATTRIBS * 16;
private static final int RING_CHUNK_BYTES = ResourceManager.ATTRIB_RING_CHUNK_BLOCKS * RING_BLOCK_BYTES;
@@ -86,6 +86,17 @@ public final class PipelineCache {
public int stencilCompareMask = 0xFF;
public int stencilWriteMask = 0xFF;
private static final LongOpenHashSet loggedStencilMaskLost = new LongOpenHashSet();
private void warnOnceOnStencilMaskLost() {
if (!stencilTestEnabled || effectiveStencilTestEnabled()) return;
if (stencilFrontCompareOp == SDL_GPU_COMPAREOP_ALWAYS && stencilBackCompareOp == SDL_GPU_COMPAREOP_ALWAYS && stencilFrontPassOp == SDL_GPU_STENCILOP_KEEP && stencilBackPassOp == SDL_GPU_STENCILOP_KEEP)
return;
if (!loggedStencilMaskLost.add(Hashing.packHiLo(programId, debugFboId))) return;
LOG.warn("Stencil mask dropped: bound target has no stencil aspect. fbo={} depthFormat={} program={} frontCmp={} frontPass={} writeMask=0x{}",
debugFboId, textureFormatName(depthTargetFormat), programId, stencilFrontCompareOp, stencilFrontPassOp, Integer.toHexString(stencilWriteMask));
}
public boolean cullEnabled = false;
public int cullFaceMode = SDL_GPU_CULLMODE_BACK;
public int frontFace = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
@@ -141,6 +152,14 @@ public final class PipelineCache {
this.drawBuffers = drawBuffers;
}
public boolean setDepthTargetFormat(int sdlFormat) {
if (depthTargetFormat == sdlFormat && hasDepthTarget == (sdlFormat != 0)) return false;
hasDepthTarget = sdlFormat != 0;
depthTargetFormat = sdlFormat;
markOutputDirty();
return true;
}
private int attachmentForSlot(int slot) {
final int[] db = drawBuffers;
if (db == null || slot < 0 || slot >= db.length) return 0;
@@ -340,7 +359,7 @@ public final class PipelineCache {
private void warnOnceOnUnconsumedFragOutput(int numColorTargets) {
if (maxFragOutputLocation < numColorTargets) return;
if (!loggedUnconsumedFragOutput.add(Hashing.packHiLo(programId, numColorTargets))) return;
LOG.warn("[PipelineCache] program {} declares fragment output location {} but framebuffer {} ({} colour attachment(s)) has {} draw buffer(s) {}; writes past location {} are discarded", programId, maxFragOutputLocation, debugFboId, debugFboAttachments, numColorTargets, Arrays.toString(drawBuffers), numColorTargets - 1);
LOG.warn("[PipelineCache] program {} declares fragment output location {} but framebuffer {} ({} color attachment(s)) has {} draw buffer(s) {}; writes past location {} are discarded", programId, maxFragOutputLocation, debugFboId, debugFboAttachments, numColorTargets, Arrays.toString(drawBuffers), numColorTargets - 1);
}
private long createPipeline(PipelineStore store, ContextState cs) {
@@ -360,6 +379,7 @@ public final class PipelineCache {
try (var stack = stackPush()) {
final int numColorTargets = colorTargetFormats().length;
warnOnceOnUnconsumedFragOutput(numColorTargets);
warnOnceOnStencilMaskLost();
final SDL_GPUColorTargetDescription.Buffer colorDesc = SDL_GPUColorTargetDescription.calloc(numColorTargets, stack);
for (int i = 0; i < numColorTargets; i++) {
final var desc = colorDesc.get(i);
@@ -49,8 +49,19 @@ public final class FBOClearTracker {
st.clearedTexturesThisFrame.remove(tex);
}
public static void recordPendingStencilClear(ContextState st, long tex, int value) {
st.pendingStencilValues.put(tex, value);
if (st.pendingStencilTextures.add(tex)) {
st.pendingMutationGen++;
}
st.clearedStencilTexturesThisFrame.remove(tex);
}
public static boolean fboHasPendingClear(ContextState st, FboState fbo) {
if (fbo.depthTexture != 0 && st.pendingDepthTextures.contains(fbo.depthTexture)) return true;
if (fbo.depthTexture != 0
&& (st.pendingDepthTextures.contains(fbo.depthTexture) || st.pendingStencilTextures.contains(fbo.depthTexture))) {
return true;
}
if (st.pendingColorTextures.isEmpty()) return false;
for (int db : fbo.drawBuffers) {
if (db < 0 || db >= ContextState.MAX_COLOR_ATTACHMENTS) continue;
@@ -67,7 +78,7 @@ public final class FBOClearTracker {
}
public void flushPendingClearsForBoundSamplers(ContextState st) {
if (st.pendingColorTextures.isEmpty() && st.pendingDepthTextures.isEmpty()) return;
if (st.pendingColorTextures.isEmpty() && st.pendingDepthTextures.isEmpty() && st.pendingStencilTextures.isEmpty()) return;
if (st.lastFlushedSamplerBindGen == st.samplerBindGen && st.lastFlushedProgram == st.boundProgram && st.lastFlushedPendingMutationGen == st.pendingMutationGen) {
return;
@@ -115,7 +126,7 @@ public final class FBOClearTracker {
if (st.pendingColorTextures.remove(handle)) {
colorHandles.add(handle);
colorGlIds.add(glTexId);
} else if (st.pendingDepthTextures.remove(handle)) {
} else if (st.pendingDepthTextures.contains(handle) || st.pendingStencilTextures.contains(handle)) {
depthHandles.add(handle);
depthGlIds.add(glTexId);
}
@@ -169,6 +180,7 @@ public final class FBOClearTracker {
MemoryAccess.putFloat(ccAddr + SDL_FColor.B, color[2]);
MemoryAccess.putFloat(ccAddr + SDL_FColor.A, color[3]);
st.clearedTexturesThisFrame.add(handle);
resourceManager.markTextureContentDefined(handle);
consumed[idx] = true;
}
frameManager.noteClearPass();
@@ -180,19 +192,38 @@ public final class FBOClearTracker {
final int dn = depthHandles.size();
for (int i = 0; i < dn; i++) {
final long handle = depthHandles.getLong(i);
final float val = st.pendingDepthValues.get(handle);
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, val);
frameManager.noteClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive();
st.clearedTexturesThisFrame.add(handle);
emitDepthStencilClearPass(st, handle, st.pendingDepthTextures.remove(handle), st.pendingStencilTextures.remove(handle), false);
}
}
private void emitDepthStencilClearPass(ContextState st, long handle, boolean clearDepth, boolean clearStencil, boolean materialized) {
if (!clearDepth && !clearStencil) return;
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
if (clearDepth) {
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, st.pendingDepthValues.get(handle));
}
if (clearStencil) {
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putByte(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) st.pendingStencilValues.get(handle));
}
frameManager.noteClearPass();
if (materialized) frameManager.noteMaterializedClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive();
}
if (clearDepth) {
st.pendingDepthValues.remove(handle);
st.clearedTexturesThisFrame.add(handle);
}
if (clearStencil) {
st.pendingStencilValues.remove(handle);
st.clearedStencilTexturesThisFrame.add(handle);
}
}
@@ -206,28 +237,18 @@ public final class FBOClearTracker {
if (depth) st.pendingDepthValues.remove(handle);
else st.pendingColorValues.remove(handle);
st.clearedTexturesThisFrame.add(handle);
resourceManager.markTextureContentDefined(handle);
st.pendingMutationGen++;
return true;
}
public void materializePendingClearForTexture(ContextState st, long handle) {
if (handle == 0) return;
if (st.pendingDepthTextures.remove(handle)) {
final float val = st.pendingDepthValues.remove(handle);
final boolean depthPending = st.pendingDepthTextures.remove(handle);
final boolean stencilPending = st.pendingStencilTextures.remove(handle);
if (depthPending || stencilPending) {
frameManager.endRenderPassIfActive();
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, val);
frameManager.noteClearPass();
frameManager.noteMaterializedClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive();
st.clearedTexturesThisFrame.add(handle);
}
emitDepthStencilClearPass(st, handle, depthPending, stencilPending, true);
st.pendingMutationGen++;
return;
}
@@ -250,6 +271,7 @@ public final class FBOClearTracker {
frameManager.beginRenderPass(targets, null);
frameManager.endRenderPassIfActive();
st.clearedTexturesThisFrame.add(handle);
resourceManager.markTextureContentDefined(handle);
}
st.pendingMutationGen++;
}
@@ -262,7 +284,10 @@ public final class FBOClearTracker {
st.pendingColorValues.remove(handle);
st.pendingDepthTextures.remove(handle);
st.pendingDepthValues.remove(handle);
st.pendingStencilTextures.remove(handle);
st.pendingStencilValues.remove(handle);
st.clearedTexturesThisFrame.remove(handle);
st.clearedStencilTexturesThisFrame.remove(handle);
}
public boolean fbosHaveSameAttachments(ContextState st, int a, int b) {
@@ -35,6 +35,7 @@ public final class FboState {
public int cachedTargetsCount = -1;
public int cachedClearOpFlags;
public boolean cachedDepthClearLast;
public boolean cachedStencilClearLast;
public boolean cachedTargetsValid;
public long getColorTexture() { return colorTextures[0]; }
@@ -34,6 +34,85 @@ public final class PixelOps {
return sdlFormat == SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT || sdlFormat == SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
}
public static int depthBits(int sdlFormat) {
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_D16_UNORM -> 16;
case SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT -> 24;
case SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT -> 32;
default -> 0;
};
}
public static int stencilBits(int sdlFormat) {
return isDepthStencilFormat(sdlFormat) ? 8 : 0;
}
public static int colorChannelBits(int sdlFormat, int channel) {
if (channel < 0 || channel > 3) return 0;
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_A8_UNORM -> channel == 3 ? 8 : 0;
case SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM -> channel == 3 ? 2 : 10;
case SDL_GPU_TEXTUREFORMAT_R11G11B10_UFLOAT -> switch (channel) {
case 0, 1 -> 11;
case 2 -> 10;
default -> 0;
};
case SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM -> switch (channel) {
case 0, 2 -> 5;
case 1 -> 6;
default -> 0;
};
case SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM -> channel == 3 ? 1 : 5;
case SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM -> 4;
default -> channel < sdlFormatColorComponents(sdlFormat) ? uniformChannelBits(sdlFormat) : 0;
};
}
private static int uniformChannelBits(int sdlFormat) {
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_R8_UNORM, SDL_GPU_TEXTUREFORMAT_R8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8_UINT, SDL_GPU_TEXTUREFORMAT_R8_INT,
SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, SDL_GPU_TEXTUREFORMAT_R8G8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8G8_UINT, SDL_GPU_TEXTUREFORMAT_R8G8_INT,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM_SRGB, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UINT,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_INT, SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM,
SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM_SRGB -> 8;
case SDL_GPU_TEXTUREFORMAT_R16_UNORM, SDL_GPU_TEXTUREFORMAT_R16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16_UINT,
SDL_GPU_TEXTUREFORMAT_R16_INT, SDL_GPU_TEXTUREFORMAT_R16G16_UNORM,
SDL_GPU_TEXTUREFORMAT_R16G16_SNORM, SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT,
SDL_GPU_TEXTUREFORMAT_R16G16_UINT, SDL_GPU_TEXTUREFORMAT_R16G16_INT,
SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UNORM, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UINT,
SDL_GPU_TEXTUREFORMAT_R16G16B16A16_INT -> 16;
case SDL_GPU_TEXTUREFORMAT_R32_FLOAT, SDL_GPU_TEXTUREFORMAT_R32_UINT,
SDL_GPU_TEXTUREFORMAT_R32_INT, SDL_GPU_TEXTUREFORMAT_R32G32_FLOAT,
SDL_GPU_TEXTUREFORMAT_R32G32_UINT, SDL_GPU_TEXTUREFORMAT_R32G32_INT,
SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT, SDL_GPU_TEXTUREFORMAT_R32G32B32A32_UINT,
SDL_GPU_TEXTUREFORMAT_R32G32B32A32_INT -> 32;
default -> 0;
};
}
private static int sdlFormatColorComponents(int sdlFormat) {
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_R8_UNORM, SDL_GPU_TEXTUREFORMAT_R8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8_UINT, SDL_GPU_TEXTUREFORMAT_R8_INT,
SDL_GPU_TEXTUREFORMAT_R16_UNORM, SDL_GPU_TEXTUREFORMAT_R16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16_UINT,
SDL_GPU_TEXTUREFORMAT_R16_INT, SDL_GPU_TEXTUREFORMAT_R32_FLOAT,
SDL_GPU_TEXTUREFORMAT_R32_UINT, SDL_GPU_TEXTUREFORMAT_R32_INT -> 1;
case SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, SDL_GPU_TEXTUREFORMAT_R8G8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8G8_UINT, SDL_GPU_TEXTUREFORMAT_R8G8_INT,
SDL_GPU_TEXTUREFORMAT_R16G16_UNORM, SDL_GPU_TEXTUREFORMAT_R16G16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16G16_UINT,
SDL_GPU_TEXTUREFORMAT_R16G16_INT, SDL_GPU_TEXTUREFORMAT_R32G32_FLOAT,
SDL_GPU_TEXTUREFORMAT_R32G32_UINT, SDL_GPU_TEXTUREFORMAT_R32G32_INT -> 2;
default -> 4;
};
}
public static int glPixelSize(int format, int type) {
final int packed = GLTypes.packedTexelBytes(type);
if (packed != 0) return packed;
@@ -127,6 +127,10 @@ public final class ResourceManager {
private int preferredD24 = SDL_GPU_TEXTUREFORMAT_D24_UNORM;
private int preferredD24S8 = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
private volatile int swapchainDepthStencilFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
private long swapchainDepthStencil;
private int swapchainDepthStencilWidth;
private int swapchainDepthStencilHeight;
private static final Supplier<ContextState.VAOState> VAO_FACTORY = ContextState.VAOState::new;
private static final Supplier<TextureSamplerState> TEX_SAMPLER_FACTORY = TextureSamplerState::new;
@@ -154,6 +158,21 @@ public final class ResourceManager {
preferredD24S8 = SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
LOG.info("D24_UNORM_S8_UINT not supported for depth+sampler, using D32_FLOAT_S8_UINT");
}
swapchainDepthStencilFormat = preferredD24S8;
if (!SDL_GPUTextureSupportsFormat(dev, swapchainDepthStencilFormat, texType, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET)) {
final int alt = swapchainDepthStencilFormat == SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT ? SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT : SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
if (SDL_GPUTextureSupportsFormat(dev, alt, texType, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET)) {
swapchainDepthStencilFormat = alt;
} else {
swapchainDepthStencilFormat = 0;
LOG.warn("No depth+stencil format usable as a render target; default framebuffer will have no depth or stencil");
}
}
}
public int getSwapchainDepthStencilFormat() {
return swapchainDepthStencilFormat;
}
public int genTexture() {
@@ -292,7 +311,7 @@ public final class ResourceManager {
} finally {
wLock.unlock();
}
releaseTextureHandle(handle);
releaseTextureHandleAfterPendingUploads(handle);
}
public void releaseTextureHandleForRealloc(int glId) {
@@ -304,9 +323,24 @@ public final class ResourceManager {
} finally {
wLock.unlock();
}
releaseTextureHandleAfterPendingUploads(handle);
}
private void releaseTextureHandleAfterPendingUploads(long handle) {
if (handle == 0) return;
if (shouldDeferTextureRelease()) {
releaseTextureDeferred(handle);
return;
}
releaseTextureHandle(handle);
}
boolean shouldDeferTextureRelease() {
final TransferThread tt = transferThread;
if (tt != null && tt.getSubmittedSeq() < lastTextureUploadSeq) return true;
return hasBatchedUploads();
}
public void trackBufferHandle(long handle) {
if (handle == 0) return;
wLock.lock();
@@ -319,6 +353,40 @@ public final class ResourceManager {
try { liveTextureHandles.add(handle); } finally { wLock.unlock(); }
}
private final LongOpenHashSet definedContentTextures = new LongOpenHashSet();
public boolean isTextureContentDefined(long handle) {
if (handle == 0) return false;
if (fastRead()) return definedContentTextures.contains(handle);
rLock.lock();
try { return definedContentTextures.contains(handle); } finally { rLock.unlock(); }
}
public void markTextureContentDefined(long handle) {
if (handle == 0) return;
wLock.lock();
try { definedContentTextures.add(handle); } finally { wLock.unlock(); }
}
void markTextureContentsDefined(LongArrayList handles) {
if (handles.isEmpty()) return;
wLock.lock();
try {
for (int i = 0, n = handles.size(); i < n; i++) {
final long handle = handles.getLong(i);
if (handle != 0) definedContentTextures.add(handle);
}
} finally {
wLock.unlock();
}
}
public void markTextureContentUndefined(long handle) {
if (handle == 0) return;
wLock.lock();
try { definedContentTextures.remove(handle); } finally { wLock.unlock(); }
}
public void releaseBufferHandle(long handle) {
if (handle == 0) return;
final boolean live;
@@ -338,6 +406,8 @@ public final class ResourceManager {
public void releaseTextureHandle(long handle) {
if (handle == 0) return;
dropBatchedUploadsTargeting(handle);
markTextureContentUndefined(handle);
final boolean live;
wLock.lock();
try {
@@ -620,6 +690,7 @@ public final class ResourceManager {
private final LongArrayList deferredBufferReleases = new LongArrayList();
private final LongArrayList deferredTextureReleases = new LongArrayList();
private volatile long lastTextureUploadSeq;
public void deleteBuffer(int glId) {
final ByteBuffer droppedPboStaging;
@@ -1560,12 +1631,16 @@ public final class ResourceManager {
f.batchMapped = null;
}
markTextureContentsDefined(f.pendingTexHandles);
try (var stack = stackPush()) {
final SDL_GPUTextureTransferInfo src = SDL_GPUTextureTransferInfo.calloc(stack);
final SDL_GPUTextureRegion dst = SDL_GPUTextureRegion.calloc(stack);
for (int i = 0; i < n; i++) {
final long texture = f.pendingTexHandles.getLong(i);
if (texture == 0) continue;
src.transfer_buffer(f.batchXfer).offset(f.pendingTexOffsets.getInt(i));
dst.texture(f.pendingTexHandles.getLong(i)).mip_level(f.pendingTexLevels.getInt(i))
dst.texture(texture).mip_level(f.pendingTexLevels.getInt(i))
.x(f.pendingTexX.getInt(i)).y(f.pendingTexY.getInt(i)).z(0)
.w(f.pendingTexW.getInt(i)).h(f.pendingTexH.getInt(i)).d(1);
SDL_UploadToGPUTexture(copyPass, src, dst, false);
@@ -1586,6 +1661,14 @@ public final class ResourceManager {
frameManager.recordUploadCommands(totalBytes, n);
}
void dropBatchedUploadsTargeting(long handle) {
final LongArrayList handles = frameManager.frame().pendingTexHandles;
if (handles.isEmpty()) return;
for (int i = 0, n = handles.size(); i < n; i++) {
if (handles.getLong(i) == handle) handles.set(i, 0L);
}
}
public boolean hasBatchedUploads() {
return !frameManager.frame().pendingTexHandles.isEmpty();
}
@@ -1616,6 +1699,7 @@ public final class ResourceManager {
.w(w).h(h).d(d);
SDL_UploadToGPUTexture(copyPass, src, dst, false);
}
markTextureContentDefined(gpuTexture);
frameManager.recordUploadCommands(size, 1);
returnTransferBuffer(xfer, size);
@@ -1649,6 +1733,7 @@ public final class ResourceManager {
SDL_UploadToGPUTexture(copyPass, src, dst, false);
}
markTextureContentDefined(gpuTexture);
frameManager.recordUploadCommands(size, 1);
returnTransferBuffer(xfer, size);
@@ -1670,6 +1755,8 @@ public final class ResourceManager {
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
final long seq = TransferThread.nextSeq();
st.frameHighestEnqueuedSeq = seq;
lastTextureUploadSeq = seq;
markTextureContentDefined(texHandle);
tt.enqueue(TransferThread.TextureRegionUpload.acquire(xfer, texHandle, x, y, w, h, level, size, seq));
tt.wake();
return true;
@@ -1999,6 +2086,34 @@ public final class ResourceManager {
return dummyColorTarget;
}
public long getOrCreateSwapchainDepthStencil(int width, int height) {
if (swapchainDepthStencilFormat == 0 || width <= 0 || height <= 0) return 0;
if (swapchainDepthStencil != 0 && swapchainDepthStencilWidth == width && swapchainDepthStencilHeight == height) {
return swapchainDepthStencil;
}
if (swapchainDepthStencil != 0) {
releaseTextureDeferred(swapchainDepthStencil);
swapchainDepthStencil = 0;
}
try (var stack = stackPush()) {
final SDL_GPUTextureCreateInfo ci = SDL_GPUTextureCreateInfo.calloc(stack)
.type(SDL_GPU_TEXTURETYPE_2D)
.format(swapchainDepthStencilFormat)
.usage(SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET)
.width(width).height(height).layer_count_or_depth(1).num_levels(1);
swapchainDepthStencil = SDL_CreateGPUTexture(device.getDevice(), ci);
}
if (swapchainDepthStencil == 0) {
swapchainDepthStencilFormat = 0;
LOG.warn("Failed to create the default framebuffer depth+stencil target ({}x{}); it will have neither", width, height);
return 0;
}
trackTextureHandle(swapchainDepthStencil);
swapchainDepthStencilWidth = width;
swapchainDepthStencilHeight = height;
return swapchainDepthStencil;
}
private static final int DUMMY_VBO_SIZE = 16 * 16;
public static final int ATTRIB_RING_BLOCKS = 2048 * Device.FRAMES_IN_FLIGHT;
public static final int ATTRIB_RING_CHUNK_BLOCKS = 64;
@@ -2114,6 +2229,8 @@ public final class ResourceManager {
case GL14.GL_DEPTH_COMPONENT32, GL30.GL_DEPTH_COMPONENT32F -> SDL_GPU_TEXTUREFORMAT_D32_FLOAT;
case GL30.GL_DEPTH24_STENCIL8 -> preferredD24S8;
case GL30.GL_DEPTH32F_STENCIL8 -> SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
case GL11.GL_STENCIL_INDEX, GL30.GL_STENCIL_INDEX1, GL30.GL_STENCIL_INDEX4,
GL30.GL_STENCIL_INDEX8, GL30.GL_STENCIL_INDEX16 -> preferredD24S8;
case GL30.GL_R8UI -> SDL_GPU_TEXTUREFORMAT_R8_UINT;
case GL30.GL_R16UI -> SDL_GPU_TEXTUREFORMAT_R16_UINT;
case GL30.GL_R32UI -> SDL_GPU_TEXTUREFORMAT_R32_UINT;
@@ -2233,6 +2350,12 @@ public final class ResourceManager {
releaseTextureHandle(dummyColorTarget);
dummyColorTarget = 0;
}
if (swapchainDepthStencil != 0) {
releaseTextureHandle(swapchainDepthStencil);
swapchainDepthStencil = 0;
swapchainDepthStencilWidth = 0;
swapchainDepthStencilHeight = 0;
}
if (fallbackStorageTexture3D != 0) {
releaseTextureHandle(fallbackStorageTexture3D);
fallbackStorageTexture3D = 0;
@@ -34,6 +34,7 @@ public final class TransferThread {
private static final Tracy.ZoneId Z_SDL_UPLOAD = Tracy.zoneId("sdlUpload", Tracy.COLOR_WORKER);
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU-Transfer");
private boolean warnedNullTextureUpload;
private static final AtomicLong SEQ = new AtomicLong();
@@ -214,6 +215,19 @@ public final class TransferThread {
return flushRequestedSeq > submittedSeq || openBytes >= FLUSH_COALESCE_BYTES || openCount >= FLUSH_COALESCE_COMMANDS;
}
static boolean shouldPublishRetired(boolean hasOpenCB, long openHighestSeq, long submittedSeq) {
return !hasOpenCB && openHighestSeq > submittedSeq;
}
private void publishSubmittedSeq() {
if (openHighestSeq <= submittedSeq) return;
synchronized (submittedLock) {
submittedSeq = openHighestSeq;
submittedLock.notifyAll();
}
drainDuePendingFrees();
}
private void run() {
LOG.info("Transfer thread started");
try {
@@ -230,6 +244,9 @@ public final class TransferThread {
if (shouldFlush(openCB != 0, flushRequestedSeq, submittedSeq, openBytes, openCount)) {
flushOpenCB();
} else if (shouldPublishRetired(openCB != 0, openHighestSeq, submittedSeq)) {
publishSubmittedSeq();
openHighestSeq = 0;
}
if (Tracy.ENABLED) pendingActiveNanos += System.nanoTime() - workStart;
@@ -315,10 +332,18 @@ public final class TransferThread {
return true;
}
private void retireSeq(long seq) {
openHighestSeq = Math.max(openHighestSeq, seq);
}
private void recordBufferUpload(long transferBuffer, long size, long dstGpuBuffer, long dstOffset, long seq, boolean cycle) {
if (transferBuffer == 0) return;
if (transferBuffer == 0) {
retireSeq(seq);
return;
}
if (!ensureOpenCB()) {
resourceManager.returnTransferBufferThreadSafe(transferBuffer, size);
retireSeq(seq);
return;
}
srcLoc.transfer_buffer(transferBuffer).offset(0);
@@ -333,7 +358,10 @@ public final class TransferThread {
}
private void recordGpuCopy(long srcHandle, long dstHandle, long readOffset, long writeOffset, long size, long seq) {
if (!ensureOpenCB()) return;
if (!ensureOpenCB()) {
retireSeq(seq);
return;
}
srcBufLoc.buffer(srcHandle).offset((int) readOffset);
dstBufLoc.buffer(dstHandle).offset((int) writeOffset);
SDL_CopyGPUBufferToBuffer(openCopyPass, srcBufLoc, dstBufLoc, (int) size, false);
@@ -344,9 +372,22 @@ public final class TransferThread {
}
private void recordTextureUpload(long transferBuffer, long texHandle, int x, int y, int w, int h, int level, long size, long seq) {
if (transferBuffer == 0) return;
if (transferBuffer == 0) {
retireSeq(seq);
return;
}
if (texHandle == 0) {
if (!warnedNullTextureUpload) {
warnedNullTextureUpload = true;
LOG.warn("Deferred texture upload with no destination texture; dropped (level={} {}x{} at {},{})", level, w, h, x, y);
}
resourceManager.returnTransferBufferThreadSafe(transferBuffer, size);
retireSeq(seq);
return;
}
if (!ensureOpenCB()) {
resourceManager.returnTransferBufferThreadSafe(transferBuffer, size);
retireSeq(seq);
return;
}
texXferInfo.transfer_buffer(transferBuffer).offset(0);
@@ -368,12 +409,7 @@ public final class TransferThread {
device.reportGpuFailure("submit transfer command buffer");
}
synchronized (submittedLock) {
submittedSeq = openHighestSeq;
submittedLock.notifyAll();
}
drainDuePendingFrees();
publishSubmittedSeq();
for (int i = 0, n = pendingReturnHandles.size(); i < n; i++) {
resourceManager.returnTransferBufferThreadSafe(pendingReturnHandles.getLong(i), pendingReturnSizes.getLong(i));
@@ -0,0 +1,92 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import static com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend.defaultFramebufferAttachmentParameteri;
import static com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend.fboAttachmentParameteri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
class FramebufferAttachmentQueryTest {
private static final int COLOR = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
private static int defaultFb(int attachment, int pname, int depthFormat) {
return defaultFramebufferAttachmentParameteri(attachment, pname, COLOR, depthFormat);
}
@Test
void defaultFramebufferReportsEightStencilBits() {
assertEquals(8, defaultFb(GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(8, defaultFb(GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
}
@Test
void defaultFramebufferReportsDepthAndColorSizes() {
assertEquals(24, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(32, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
assertEquals(8, defaultFb(GL11.GL_BACK_LEFT, GL30.GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(8, defaultFb(GL11.GL_BACK_LEFT, GL30.GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
}
@Test
void defaultFramebufferWithoutDepthReportsNone() {
assertEquals(0, defaultFb(GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, 0));
assertEquals(0, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, 0));
assertEquals(GL11.GL_NONE, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, 0));
assertEquals(GL30.GL_FRAMEBUFFER_DEFAULT, defaultFb(GL11.GL_BACK_LEFT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, 0));
}
@Test
void packedDepthStencilAttachmentReportsBothAspects() {
final FboState fbo = new FboState();
fbo.depthGlId = 42;
fbo.depthTexture = 0xDDDDL;
fbo.depthFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
for (int attachment : new int[]{ GL30.GL_DEPTH_ATTACHMENT, GL30.GL_STENCIL_ATTACHMENT, GL30.GL_DEPTH_STENCIL_ATTACHMENT, GL11.GL_STENCIL }) {
assertEquals(8, fboAttachmentParameteri(fbo, attachment, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE), "attachment " + attachment);
}
assertEquals(24, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE));
assertEquals(42, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME));
assertEquals(GL11.GL_TEXTURE, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE));
}
@Test
void depthOnlyAttachmentReportsNoStencil() {
final FboState fbo = new FboState();
fbo.depthGlId = 7;
fbo.depthTexture = 0xDDDDL;
fbo.depthFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM;
assertEquals(0, fboAttachmentParameteri(fbo, GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE));
assertEquals(24, fboAttachmentParameteri(fbo, GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE));
}
@Test
void missingAttachmentReportsNone() {
final FboState fbo = new FboState();
assertEquals(GL11.GL_NONE, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE));
assertEquals(0, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE));
assertEquals(0, fboAttachmentParameteri(null, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE));
}
@Test
void colorAttachmentReportsChannelSizes() {
final FboState fbo = new FboState();
fbo.colorGlIds[1] = 11;
fbo.colorTextures[1] = 0xCCCCL;
fbo.colorFormats[1] = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
assertEquals(8, fboAttachmentParameteri(fbo, GL30.GL_COLOR_ATTACHMENT0 + 1, GL30.GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE));
assertEquals(11, fboAttachmentParameteri(fbo, GL30.GL_COLOR_ATTACHMENT0 + 1, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME));
assertEquals(GL11.GL_NONE, fboAttachmentParameteri(fbo, GL30.GL_COLOR_ATTACHMENT0, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE));
}
}
@@ -0,0 +1,63 @@
package com.gtnewhorizons.angelica.sdlgpu.frame;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.swapchainClearNeedsPassBreak;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SwapchainClearPassBreakTest {
private static final long SWAPCHAIN = 0x5A1CL;
private static FrameManager.FrameState activeSwapchainPass() {
final FrameManager.FrameState f = new FrameManager.FrameState();
f.swapchainTexture = SWAPCHAIN;
f.renderPass = 0xBEEFL;
f.currentColorTarget = SWAPCHAIN;
return f;
}
@Test
void aPendingColorClearBreaksTheActivePass() {
final ContextState st = new ContextState();
st.pendingSwapchainClear = true;
assertTrue(swapchainClearNeedsPassBreak(st, activeSwapchainPass()), "otherwise the clear is deferred to the next pass and wipes what this one drew");
}
@Test
void aPendingDepthOrStencilClearBreaksTheActivePass() {
final ContextState depth = new ContextState();
depth.pendingSwapchainDepthClear = true;
assertTrue(swapchainClearNeedsPassBreak(depth, activeSwapchainPass()));
final ContextState stencil = new ContextState();
stencil.pendingSwapchainStencilClear = true;
assertTrue(swapchainClearNeedsPassBreak(stencil, activeSwapchainPass()));
}
@Test
void nothingPendingLeavesThePassAlone() {
assertFalse(swapchainClearNeedsPassBreak(new ContextState(), activeSwapchainPass()));
}
@Test
void aClearWithNoActivePassNeedsNoBreak() {
final ContextState st = new ContextState();
st.pendingSwapchainClear = true;
final FrameManager.FrameState f = activeSwapchainPass();
f.renderPass = 0;
assertFalse(swapchainClearNeedsPassBreak(st, f));
}
@Test
void aPassBoundToAnFboIsNotTheSwapchainPass() {
final ContextState st = new ContextState();
st.pendingSwapchainClear = true;
final FrameManager.FrameState f = activeSwapchainPass();
f.currentColorTarget = 0xF80L;
assertFalse(swapchainClearNeedsPassBreak(st, f));
}
}
@@ -0,0 +1,112 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.consumeClears;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
class ClearConsumptionTest {
private static final long COLOR = 0xC010L;
private static final long DEPTH = 0xD00DL;
private static final long DUMMY = 0xD04DL;
private static FboState fbo(int depthFormat) {
final FboState fbo = new FboState();
fbo.colorTextures[0] = COLOR;
fbo.colorGlIds[0] = 3;
if (depthFormat != 0) {
fbo.depthTexture = DEPTH;
fbo.depthGlId = 5;
fbo.depthFormat = depthFormat;
}
fbo.recomputeTargets();
return fbo;
}
private static ResourceManager rm() {
return SdlTestRig.resourceManager();
}
@Test
void aClearedColorTargetBecomesContentDefined() {
final ResourceManager rm = rm();
final ContextState st = new ContextState();
final FboState fbo = fbo(0);
FBOClearTracker.recordPendingColorClear(st, COLOR, 0f, 0f, 0f, 1f);
consumeClears(rm, st, fbo, DUMMY, 0b1, false, false, false);
assertFalse(st.pendingColorTextures.contains(COLOR), "the pass applied the clear, so it is no longer pending");
assertTrue(rm.isTextureContentDefined(COLOR), "a later frame must not clear it again");
}
@Test
void anUnclearedColorTargetIsNotMarkedDefined() {
final ResourceManager rm = rm();
final ContextState st = new ContextState();
consumeClears(rm, st, fbo(0), DUMMY, 0, false, false, false);
assertFalse(rm.isTextureContentDefined(COLOR));
}
@Test
void theDummyTargetIsNeverTracked() {
final ResourceManager rm = rm();
final ContextState st = new ContextState();
final FboState fbo = fbo(0);
fbo.colorTextures[0] = DUMMY;
fbo.recomputeTargets();
consumeClears(rm, st, fbo, DUMMY, 0b1, false, false, false);
assertFalse(rm.isTextureContentDefined(DUMMY), "the shared stand-in target has no per-FBO content");
}
@Test
void depthAndStencilAreConsumedIndependently() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
consumeClears(rm(), st, fbo(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT), DUMMY, 0, true, true, true);
assertFalse(st.pendingDepthTextures.contains(DEPTH));
assertFalse(st.pendingStencilTextures.contains(DEPTH));
assertTrue(st.clearedTexturesThisFrame.contains(DEPTH));
assertTrue(st.clearedStencilTexturesThisFrame.contains(DEPTH));
}
@Test
void aDepthOnlyTargetLeavesStencilStateAlone() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
consumeClears(rm(), st, fbo(SDL_GPU_TEXTUREFORMAT_D24_UNORM), DUMMY, 0, true, false, false);
assertFalse(st.pendingDepthTextures.contains(DEPTH));
assertTrue(st.pendingStencilTextures.contains(DEPTH), "the format has no stencil aspect to clear");
assertFalse(st.clearedStencilTexturesThisFrame.contains(DEPTH));
}
@Test
void anUnclearedDepthTargetIsStillConsumed() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
consumeClears(rm(), st, fbo(SDL_GPU_TEXTUREFORMAT_D24_UNORM), DUMMY, 0, false, false, false);
assertFalse(st.pendingDepthTextures.contains(DEPTH));
assertFalse(st.clearedTexturesThisFrame.contains(DEPTH), "no clear was baked, so nothing was cleared");
}
}
@@ -0,0 +1,44 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.foldClearColor;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.foldClearDepth;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.foldClearStencil;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class ClearValueLayoutHashTest {
private static final long SEED = 0xA5A5A5A5A5A5A5A5L;
@Test
void depthZeroAndDepthOneDoNotCollide() {
assertNotEquals(foldClearDepth(SEED, 0.0f), foldClearDepth(SEED, 1.0f), "the minimap mask clear must not reuse the world's depth-1 clear target");
}
@Test
void depthFoldIsStableForTheSameValue() {
assertEquals(foldClearDepth(SEED, 0.0f), foldClearDepth(SEED, 0.0f));
}
@Test
void negativeZeroIsDistinguishedFromZero() {
assertNotEquals(foldClearDepth(SEED, 0.0f), foldClearDepth(SEED, -0.0f), "raw bits, not numeric equality, so the cached value always matches what was baked");
}
@Test
void everyColorChannelParticipates() {
final long base = foldClearColor(SEED, 0f, 0f, 0f, 1f);
assertNotEquals(base, foldClearColor(SEED, 1f, 0f, 0f, 1f), "red");
assertNotEquals(base, foldClearColor(SEED, 0f, 1f, 0f, 1f), "green");
assertNotEquals(base, foldClearColor(SEED, 0f, 0f, 1f, 1f), "blue");
assertNotEquals(base, foldClearColor(SEED, 0f, 0f, 0f, 0f), "alpha");
}
@Test
void stencilValuesDoNotCollide() {
assertNotEquals(foldClearStencil(SEED, 0), foldClearStencil(SEED, 1));
assertEquals(foldClearStencil(SEED, 0xFF), foldClearStencil(SEED, 0xFF));
}
}
@@ -0,0 +1,232 @@
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.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDL_GPUColorTargetInfo;
import org.lwjgl.sdl.SDL_GPUDepthStencilTargetInfo;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.*;
class StencilMaskGpuTest {
private static SdlTestRig rig;
private static ShaderManager sm;
private static final int SIZE = 64;
private static final int MASK = 32;
private static PipelineStore store;
private static long colorTexture;
private static int colorFormat;
private static long depthTexture;
private static int depthFormat;
private static long maskPipeline;
private static long fillPipeline;
private static final String VERTEX = String.join("\n",
"#version 460 core",
"void main() {",
" vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);",
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);",
"}",
"");
private static final String MASK_FRAGMENT = String.join("\n",
"#version 460 core",
"layout(location = 0) out vec4 fragColor;",
"void main() {",
" if (gl_FragCoord.x >= " + MASK + ".0 || gl_FragCoord.y >= " + MASK + ".0) discard;",
" fragColor = vec4(0.0, 0.0, 1.0, 1.0);",
"}",
"");
private static final String FILL_FRAGMENT = String.join("\n",
"#version 460 core",
"layout(location = 0) out vec4 fragColor;",
"void main() {",
" fragColor = vec4(1.0, 0.0, 0.0, 1.0);",
"}",
"");
@BeforeAll
static void setUp() throws Exception {
rig = SdlTestRig.acquireRealDevice();
rig.resourceManager.cachePreferredDepthFormats();
sm = new ShaderManager(rig.device);
VAOManager.init(0);
store = new PipelineStore(rig.device);
colorTexture = rig.resourceManager.createTexture(9500, GL11.GL_TEXTURE_2D, GL11.GL_RGBA8, SIZE, SIZE, 1, 1);
assertTrue(colorTexture != 0, "color target creation failed");
colorFormat = rig.resourceManager.getTextureMeta(9500).sdlFormat();
PipelineCache.setSwapchainFormats(new int[]{ colorFormat });
rig.resourceManager.createTexture(9501, GL11.GL_TEXTURE_2D, GL30.GL_DEPTH24_STENCIL8, SIZE, SIZE, 1, 1);
depthTexture = rig.resourceManager.ensureTextureUsage(9501, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET);
assertTrue(depthTexture != 0, "depth+stencil target creation failed");
depthFormat = rig.resourceManager.getTextureMeta(9501).sdlFormat();
assertEquals(8, PixelOps.stencilBits(depthFormat), "test needs a packed depth+stencil format");
maskPipeline = createPipeline(MASK_FRAGMENT, true);
fillPipeline = createPipeline(FILL_FRAGMENT, false);
}
@AfterAll
static void tearDown() {
SdlTestRig.releaseRealDevice();
}
private static long createPipeline(String fragmentSource, boolean writesMask) {
final int vs = sm.createShader(GL20.GL_VERTEX_SHADER);
sm.shaderSource(vs, VERTEX);
sm.compileShader(vs);
final int fs = sm.createShader(GL20.GL_FRAGMENT_SHADER);
sm.shaderSource(fs, fragmentSource);
sm.compileShader(fs);
final int prog = sm.createProgram();
sm.attachShader(prog, vs);
sm.attachShader(prog, fs);
sm.linkProgram(prog);
final ShaderManager.ProgramObject po = sm.getProgram(prog);
assertTrue(po != null && po.linked, "link failed: " + (po == null ? "no program" : po.infoLog));
po.sdlVertexShader = sm.createSDLShader(po.vertexSpirv, SDL_GPU_SHADERSTAGE_VERTEX, po.vertexResources.numSamplers(), po.vertexResources.numUBOs(), po.vertexResources.numStorageBuffers(), po.vertexResources.numStorageTextures());
po.sdlFragmentShader = sm.createSDLShader(po.fragmentSpirv, SDL_GPU_SHADERSTAGE_FRAGMENT, po.fragmentResources.numSamplers(), po.fragmentResources.numUBOs(), po.fragmentResources.numStorageBuffers(), po.fragmentResources.numStorageTextures());
assertTrue(po.sdlVertexShader != 0 && po.sdlFragmentShader != 0, "SDL shader creation failed");
final ContextState cs = new ContextState();
final PipelineCache cache = cs.pipeline;
cache.vertexShader = po.sdlVertexShader;
cache.fragmentShader = po.sdlFragmentShader;
cache.programId = prog;
cache.maxFragOutputLocation = po.maxFragOutputLocation;
cache.shaderInputMask = po.vertexInputMask;
cache.shaderInputVecSize = po.vertexInputVecSize;
cache.shaderInputBaseType = po.vertexInputBaseType;
cache.shaderInputName = po.vertexInputName;
cache.setColorTargetFormats(new int[]{ colorFormat });
cache.setDrawBuffers(new int[]{ 0 });
cache.hasDepthTarget = true;
cache.depthTargetFormat = depthFormat;
cache.depthTestEnabled = false;
cache.depthWriteEnabled = false;
cache.stencilTestEnabled = true;
cache.stencilCompareMask = 0xFF;
cache.stencilWriteMask = writesMask ? 0xFF : 0x00;
final int compareOp = writesMask ? SDL_GPU_COMPAREOP_ALWAYS : SDL_GPU_COMPAREOP_EQUAL;
final int passOp = writesMask ? SDL_GPU_STENCILOP_REPLACE : SDL_GPU_STENCILOP_KEEP;
cache.stencilFrontCompareOp = compareOp;
cache.stencilBackCompareOp = compareOp;
cache.stencilFrontPassOp = passOp;
cache.stencilBackPassOp = passOp;
cache.markShaderDirty();
cache.markOutputDirty();
assertTrue(cache.effectiveStencilTestEnabled(), "stencil must survive the depth-format gate");
final long pipeline = cache.getOrCreatePipeline(store, cs);
assertTrue(pipeline != 0, "pipeline creation failed: " + SDLError.SDL_GetError());
return pipeline;
}
private static void render(int secondPassStencilLoad) {
try (MemoryStack stack = MemoryStack.stackPush()) {
final long cb = SDL_AcquireGPUCommandBuffer(rig.device.getDevice());
assertTrue(cb != 0, "no command buffer");
final SDL_GPUColorTargetInfo.Buffer color = SDL_GPUColorTargetInfo.calloc(1, stack);
color.get(0).texture(colorTexture).load_op(SDL_GPU_LOADOP_CLEAR).store_op(SDL_GPU_STOREOP_STORE);
final SDL_GPUDepthStencilTargetInfo ds = SDL_GPUDepthStencilTargetInfo.calloc(stack)
.texture(depthTexture)
.load_op(SDL_GPU_LOADOP_CLEAR).store_op(SDL_GPU_STOREOP_STORE).clear_depth(1.0f)
.stencil_load_op(SDL_GPU_LOADOP_CLEAR).stencil_store_op(SDL_GPU_STOREOP_STORE).clear_stencil((byte) 0);
long pass = SDL_BeginGPURenderPass(cb, color, ds);
assertTrue(pass != 0, "no render pass");
SDL_SetGPUStencilReference(pass, (byte) 1);
SDL_BindGPUGraphicsPipeline(pass, maskPipeline);
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
if (secondPassStencilLoad < 0) {
SDL_BindGPUGraphicsPipeline(pass, fillPipeline);
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
}
SDL_EndGPURenderPass(pass);
if (secondPassStencilLoad >= 0) {
color.get(0).load_op(SDL_GPU_LOADOP_LOAD);
ds.load_op(SDL_GPU_LOADOP_LOAD).stencil_load_op(secondPassStencilLoad).clear_stencil((byte) 0);
pass = SDL_BeginGPURenderPass(cb, color, ds);
assertTrue(pass != 0, "no second render pass");
SDL_SetGPUStencilReference(pass, (byte) 1);
SDL_BindGPUGraphicsPipeline(pass, fillPipeline);
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
assertTrue(SDL_SubmitGPUCommandBuffer(cb), "draw submit failed");
SDL_WaitForGPUIdle(rig.device.getDevice());
}
}
private static int[] readRedPixels() {
final ByteBuffer pixels = MemoryUtil.memAlloc(SIZE * SIZE * 4);
try {
final long cb = SDL_AcquireGPUCommandBuffer(rig.device.getDevice());
assertTrue(cb != 0, "no readback command buffer");
rig.resourceManager.downloadFromTexture(cb, colorTexture, 0, 0, SIZE, SIZE, 0, pixels);
int count = 0, maxX = -1, minX = SIZE;
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
final int i = (y * SIZE + x) * 4;
final boolean red = (pixels.get(i) & 0xFF) > 200 && (pixels.get(i + 1) & 0xFF) < 64;
if (red) {
count++;
if (x > maxX) maxX = x;
if (x < minX) minX = x;
}
}
}
return new int[]{ count, minX, maxX };
} finally {
MemoryUtil.memFree(pixels);
}
}
@Test
void fillIsClippedToTheStencilMask() {
render(-1);
final int[] r = readRedPixels();
assertEquals(MASK * MASK, r[0], "fill must cover exactly the masked region, not the whole target");
assertEquals(0, r[1], "masked region starts at x=0");
assertEquals(MASK - 1, r[2], "masked region must not bleed past the mask");
}
@Test
void maskSurvivesAPassBoundaryWhenStencilIsLoaded() {
render(SDL_GPU_LOADOP_LOAD);
final int[] r = readRedPixels();
assertEquals(MASK * MASK, r[0], "a stored stencil buffer must still clip the next pass");
assertEquals(MASK - 1, r[2]);
}
@Test
void clearingStencilWipesTheMask() {
render(SDL_GPU_LOADOP_CLEAR);
assertEquals(0, readRedPixels()[0], "a cleared stencil buffer must clip everything");
}
}
@@ -0,0 +1,42 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ColorContentLifetimeTest {
private static final long TEX = 0xC010125L;
@Test
void freshTextureHasUndefinedContents() {
final ResourceManager rm = SdlTestRig.resourceManager();
assertFalse(rm.isTextureContentDefined(TEX), "never written, so the first pass must clear");
}
@Test
void definedContentsSurviveAcrossFrames() {
final ResourceManager rm = SdlTestRig.resourceManager();
rm.markTextureContentDefined(TEX);
assertTrue(rm.isTextureContentDefined(TEX));
assertTrue(rm.isTextureContentDefined(TEX), "contents outlive the frame that produced them");
}
@Test
void releaseResetsContentsBecauseSdlCanRecycleThePointer() {
final ResourceManager rm = SdlTestRig.resourceManager();
rm.markTextureContentDefined(TEX);
rm.markTextureContentUndefined(TEX);
assertFalse(rm.isTextureContentDefined(TEX), "a recycled handle must not inherit the previous texture's defined flag");
}
@Test
void theZeroHandleIsNeverDefined() {
final ResourceManager rm = SdlTestRig.resourceManager();
rm.markTextureContentDefined(0L);
assertFalse(rm.isTextureContentDefined(0L));
}
}
@@ -0,0 +1,64 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.*;
class DepthStencilBitsTest {
@Test
void depthBitsPerFormat() {
assertEquals(16, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D16_UNORM));
assertEquals(24, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM));
assertEquals(24, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(32, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT));
assertEquals(32, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
assertEquals(0, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM));
}
@Test
void onlyPackedFormatsCarryStencil() {
assertEquals(8, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(8, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D16_UNORM));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM));
}
@Test
void stencilBitsAgreesWithIsDepthStencilFormat() {
final int[] formats = {
SDL_GPU_TEXTUREFORMAT_D16_UNORM, SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D32_FLOAT,
SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
};
for (int f : formats) {
assertEquals(PixelOps.isDepthStencilFormat(f), PixelOps.stencilBits(f) != 0, "format " + f);
}
}
@Test
void colorChannelBitsForCommonFormats() {
for (int fmt : new int[]{ SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM }) {
for (int c = 0; c < 4; c++) assertEquals(8, PixelOps.colorChannelBits(fmt, c), "channel " + c);
}
assertEquals(16, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT, 3));
assertEquals(8, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8_UNORM, 0));
assertEquals(0, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8_UNORM, 1));
assertEquals(8, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, 1));
assertEquals(0, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, 2));
assertEquals(10, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM, 0));
assertEquals(2, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM, 3));
assertEquals(0, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT, 0));
}
@Test
void depthFormatsAreNotColorFormats() {
assertTrue(PixelOps.isDepthFormat(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertFalse(PixelOps.isDepthFormat(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM));
}
}
@@ -35,6 +35,18 @@ class FullCoverageClearDiscardTest {
assertTrue(st.clearedTexturesThisFrame.contains(HANDLE), "the overwrite counts as the clear");
}
@Test
void fullTargetColorBlitDefinesTheContents() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingColorClear(st, HANDLE, 1f, 0f, 0f, 1f);
final SdlTestRig rig = SdlTestRig.create();
final FBOClearTracker tracker = new FBOClearTracker(rig.frameManager, rig.resourceManager, null);
assertTrue(tracker.discardPendingClearIfFullyCovered(st, HANDLE, 0, 0, 0, W, H, meta()));
assertTrue(rig.resourceManager.isTextureContentDefined(HANDLE), "the overwrite defines the contents, so a later frame must not clear it back");
}
@Test
void fullTargetDepthBlitDropsThePendingClear() {
final ContextState st = new ContextState();
@@ -0,0 +1,68 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps.colorChannelBits;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_A8_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_BC1_RGBA_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R11G11B10_UFLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
class PixelFormatBitsTest {
private static int[] rgba(int sdlFormat) {
return new int[]{
colorChannelBits(sdlFormat, 0),
colorChannelBits(sdlFormat, 1),
colorChannelBits(sdlFormat, 2),
colorChannelBits(sdlFormat, 3),
};
}
private static void assertRgba(int sdlFormat, int r, int g, int b, int a) {
assertEquals(r, colorChannelBits(sdlFormat, 0), "red");
assertEquals(g, colorChannelBits(sdlFormat, 1), "green");
assertEquals(b, colorChannelBits(sdlFormat, 2), "blue");
assertEquals(a, colorChannelBits(sdlFormat, 3), "alpha");
}
@Test
void uniformWidthFormats() {
assertRgba(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, 8, 8, 8, 8);
assertRgba(SDL_GPU_TEXTUREFORMAT_R8_UNORM, 8, 0, 0, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT, 16, 16, 0, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT, 32, 32, 32, 32);
}
@Test
void packedFormatsReportTheirRealChannelWidths() {
assertRgba(SDL_GPU_TEXTUREFORMAT_A8_UNORM, 0, 0, 0, 8);
assertRgba(SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM, 10, 10, 10, 2);
assertRgba(SDL_GPU_TEXTUREFORMAT_R11G11B10_UFLOAT, 11, 11, 10, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM, 5, 6, 5, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM, 5, 5, 5, 1);
assertRgba(SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM, 4, 4, 4, 4);
}
@Test
void depthAndCompressedFormatsHaveNoColorChannels() {
assertRgba(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT, 0, 0, 0, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_BC1_RGBA_UNORM, 0, 0, 0, 0);
}
@Test
void outOfRangeChannelsAreZero() {
assertEquals(0, colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, -1));
assertEquals(0, colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, 4));
assertEquals(4, rgba(SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM).length);
}
}
@@ -0,0 +1,122 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
class StencilClearTrackingTest {
private static final long DEPTH = 0xD00DL;
private static FBOClearTracker tracker() {
final SdlTestRig rig = SdlTestRig.create();
return new FBOClearTracker(rig.frameManager, rig.resourceManager, null);
}
private static FboState packedDepthStencilFbo() {
final FboState fbo = new FboState();
fbo.depthTexture = DEPTH;
fbo.depthGlId = 5;
fbo.depthFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
fbo.recomputeTargets();
return fbo;
}
@Test
void pendingStencilClearIsTrackedSeparatelyFromDepth() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertTrue(st.pendingStencilTextures.contains(DEPTH));
assertFalse(st.pendingDepthTextures.contains(DEPTH), "a stencil clear must not imply a depth clear");
}
@Test
void stencilClearSurvivesADepthClearOnTheSameHandle() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 3);
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
assertTrue(st.pendingStencilTextures.contains(DEPTH), "depth and stencil share a texture but not a clear");
assertEquals(3, st.pendingStencilValues.get(DEPTH));
}
@Test
void aPendingStencilClearAloneBlocksTargetReuse() {
final ContextState st = new ContextState();
final FboState fbo = packedDepthStencilFbo();
assertFalse(FBOClearTracker.fboHasPendingClear(st, fbo));
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertTrue(FBOClearTracker.fboHasPendingClear(st, fbo), "the pass must be rebuilt to apply the stencil clear");
}
@Test
void recordingAStencilClearRetractsTheClearedThisFrameMark() {
final ContextState st = new ContextState();
st.clearedStencilTexturesThisFrame.add(DEPTH);
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertFalse(st.clearedStencilTexturesThisFrame.contains(DEPTH), "a second clear in one frame must not be swallowed");
}
@Test
void recordingBumpsMutationGen() {
final ContextState st = new ContextState();
FBOClearTracker.snapshotFlushGenerations(st);
final int before = st.pendingMutationGen;
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertEquals(before + 1, st.pendingMutationGen);
}
@Test
void scrubDropsStencilStateWithTheTexture() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 7);
st.clearedStencilTexturesThisFrame.add(DEPTH);
final SdlTestRig rig = SdlTestRig.create();
final int glId = rig.resourceManager.genTexture();
SdlReflect.putTextureHandle(rig.resourceManager, glId, DEPTH);
new FBOClearTracker(rig.frameManager, rig.resourceManager, null).scrubPendingClearsForTexture(st, glId);
assertFalse(st.pendingStencilTextures.contains(DEPTH));
assertFalse(st.pendingStencilValues.containsKey(DEPTH));
assertFalse(st.clearedStencilTexturesThisFrame.contains(DEPTH));
}
@Test
void stencilClearValueIsMaskedToEightBits() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0xFF);
assertEquals(0xFF, st.pendingStencilValues.get(DEPTH));
assertEquals((byte) 0xFF, (byte) st.pendingStencilValues.get(DEPTH));
}
@Test
void aStencilOnlyClearStillReachesTheSamplerFlush() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
final int gen = st.pendingMutationGen;
tracker().flushPendingClearsForBoundSamplers(st);
assertEquals(gen, st.lastFlushedPendingMutationGen, "the early-out must not skip a texture whose only pending clear is stencil");
}
@Test
void trackerIsUnusedForFbosWithoutADepthAttachment() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
final FboState colorOnly = new FboState();
colorOnly.colorTextures[0] = 0xAAAAL;
colorOnly.recomputeTargets();
assertFalse(FBOClearTracker.fboHasPendingClear(st, colorOnly));
}
}
@@ -0,0 +1,112 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TextureReleaseUploadOrderTest {
private static final long HANDLE = 0xBEEFL;
private static List<Long> deferredReleases(ResourceManager rm) {
return Reflect.get(rm, "deferredTextureReleases");
}
private static void setUploadSeqs(ResourceManager rm, long lastUpload) {
Reflect.setDeclared(ResourceManager.class, rm, "lastTextureUploadSeq", lastUpload);
}
@Test
void releaseIsImmediateWhenNoTransferThreadIsAttached() {
final ResourceManager rm = SdlTestRig.resourceManager();
setUploadSeqs(rm, 50L);
assertFalse(rm.shouldDeferTextureRelease(), "with no transfer thread there is nothing to outlive");
}
@Test
void releaseIsDeferredWhileTextureUploadsAreStillQueued() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
final TransferThread tt = new TransferThread(rig.device, rm);
rm.setTransferThread(tt);
setUploadSeqs(rm, 50L);
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 10L);
rm.trackTextureHandle(HANDLE);
final int glId = rm.genTexture();
SdlReflect.putTextureHandle(rm, glId, HANDLE);
rm.deleteTexture(glId);
assertEquals(List.of(HANDLE), deferredReleases(rm), "the free must wait for the queued upload");
}
@Test
void releaseIsImmediateOnceTheTransferThreadHasCaughtUp() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
final TransferThread tt = new TransferThread(rig.device, rm);
rm.setTransferThread(tt);
setUploadSeqs(rm, 50L);
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 49L);
assertTrue(rm.shouldDeferTextureRelease(), "one upload still unsubmitted is enough to defer");
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 50L);
assertFalse(rm.shouldDeferTextureRelease(), "nothing is in flight, so no need to defer");
}
@Test
void aQueuedInlineBatchAlsoDefersTheRelease() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
rig.frameManager.setResourceManager(rm);
assertFalse(rm.shouldDeferTextureRelease(), "nothing queued yet");
rig.frameManager.frame().pendingTexHandles.add(HANDLE);
assertTrue(rm.hasBatchedUploads());
assertTrue(rm.shouldDeferTextureRelease(), "a queued batch entry must outlive the release");
rig.frameManager.frame().pendingTexHandles.clear();
assertFalse(rm.shouldDeferTextureRelease());
}
@Test
void releasingATextureBlanksItsQueuedBatchEntries() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
rig.frameManager.setResourceManager(rm);
final var handles = rig.frameManager.frame().pendingTexHandles;
handles.add(HANDLE);
handles.add(0xFEEDL);
handles.add(HANDLE);
rm.dropBatchedUploadsTargeting(HANDLE);
assertEquals(0L, handles.getLong(0), "every entry for the dying texture is blanked");
assertEquals(0xFEEDL, handles.getLong(1), "other destinations are untouched");
assertEquals(0L, handles.getLong(2));
}
@Test
void reallocReleaseObeysTheSameRule() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
final TransferThread tt = new TransferThread(rig.device, rm);
rm.setTransferThread(tt);
setUploadSeqs(rm, 7L);
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 0L);
rm.trackTextureHandle(HANDLE);
final int glId = rm.genTexture();
SdlReflect.putTextureHandle(rm, glId, HANDLE);
rm.releaseTextureHandleForRealloc(glId);
assertEquals(List.of(HANDLE), deferredReleases(rm), "glTexImage2D re-specification frees the old handle on the same path as a delete");
}
}
@@ -0,0 +1,54 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.resource.TransferThread.shouldPublishRetired;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TransferRetireSeqTest {
private static TransferThread thread() {
final SdlTestRig rig = SdlTestRig.create();
return new TransferThread(rig.device, rig.resourceManager);
}
@Test
void anOpenCommandBufferIsNeverPublishedEarly() {assertFalse(shouldPublishRetired(true, 50L, 10L),
"those copies are recorded but not submitted; a waiter must not be released");
}
@Test
void retiredSeqsArePublishedWhenNothingIsOpen() {
assertTrue(shouldPublishRetired(false, 50L, 10L));
}
@Test
void anAlreadyPublishedSeqIsNotRepublished() {
assertFalse(shouldPublishRetired(false, 10L, 10L));
assertFalse(shouldPublishRetired(false, 0L, 10L));
}
@Test
void aDroppedTextureUploadStillRetiresItsSeq() {
final TransferThread tt = thread();
Reflect.invoke(tt, "recordTextureUpload",
new Class<?>[]{ long.class, long.class, int.class, int.class, int.class, int.class, int.class, long.class, long.class },
0L, 0xBEEFL, 0, 0, 16, 16, 0, 1024L, 42L);
assertEquals(42L, (long) Reflect.get(tt, "openHighestSeq"), "a dropped upload must still advance the watermark or awaitSubmittedUpTo blocks forever");
tt.shutdown();
}
@Test
void aDroppedBufferUploadStillRetiresItsSeq() {
final TransferThread tt = thread();
Reflect.invoke(tt, "recordBufferUpload", new Class<?>[]{ long.class, long.class, long.class, long.class, long.class, boolean.class }, 0L, 256L, 0xF00L, 0L, 7L, false);
assertEquals(7L, (long) Reflect.get(tt, "openHighestSeq"));
tt.shutdown();
}
}
@@ -7,6 +7,7 @@ import org.lwjgl.opengl.GL11;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.LTWWorkaround;
/**
* Checks for known problematic driver configurations and applies workarounds. Adapted from Embeddium/Sodium's LateDriverScanner.
@@ -28,6 +29,14 @@ public class DriverCompatabilityCheck {
LOGGER.info("OpenGL Renderer: {}", renderer);
LOGGER.info("OpenGL Version: {}", version);
if (LTWWorkaround.isLtwVersionString(version)) {
if (SystemProperties.DISABLE_LTW_WORKAROUND) {
LOGGER.info("OpenLTW workaround disabled via -Dangelica.disableLtwWorkaround=true");
} else {
LOGGER.info("OpenLTW wrapper detected; enabling BGRA texture swizzle workaround");
}
}
if (vendor != null && vendor.contains("NVIDIA")) {
if (SystemProperties.DISABLE_NVIDIA_WORKAROUND) {
LOGGER.info("NVIDIA workaround disabled via -Dangelica.disableNvidiaWorkaround=true");
@@ -385,6 +385,11 @@ public class AngelicaConfig {
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public boolean narrowFoamFix;
@Config.Comment("Narrow Legends Mod transformer exclusions to allow GL redirection")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public boolean narrowLegendsMod;
}
@Config.Comment("Renders chunks before neighbors are ready. Improves loading at render distance edges, useful for low render distance servers.")
@@ -201,6 +201,7 @@ public final class AngelicaClientTweaker implements IFMLLoadingPlugin, IEarlyMix
narrowTransformerConfig("Ears", AngelicaConfig.transformerCompat.narrowEars);
narrowTransformerConfig("FiskHeroes", AngelicaConfig.transformerCompat.narrowFiskHeroes);
narrowTransformerConfig("FoamFix", AngelicaConfig.transformerCompat.narrowFoamFix);
narrowTransformerConfig("LegendsMod", AngelicaConfig.transformerCompat.narrowLegendsMod);
tweaks.add("com.gtnewhorizons.angelica.loading.fml.tweakers.IncompatibleModsDisablerTweaker");
if (AngelicaConfig.enableHudCaching) {
@@ -8,7 +8,7 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ModelRenderer.class)
@Mixin(value = ModelRenderer.class, priority = 1500)
public abstract class MixinModelRenderer {
@Redirect(method = {"render", "renderWithRotation"}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", remap = false))
+1 -1
View File
@@ -138,7 +138,7 @@ publishing {
}
}
repositories {
if (System.getenv("MAVEN_USER") != null) {
if (hasProperty("publishTracy") && System.getenv("MAVEN_USER") != null) {
maven {
name = "GTNHMaven"
url = uri(rootProject.findProperty("mavenPublishUrl")?.toString() ?: "https://nexus.gtnewhorizons.com/repository/releases/")