Compare commits

...
2 Commits
Author SHA1 Message Date
Jason Mitchell 3659b59db9 Adjust vsync and fps limiters, and fix WSR with player models (#2002) 2026-08-09 20:06:53 -07:00
Jason Mitchell 8c31eec525 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
2026-08-09 13:43:46 -07:00
69 changed files with 3259 additions and 393 deletions

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);
@@ -10,6 +10,8 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.DisplayListManager.RecordMode;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
@@ -552,6 +554,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 +1970,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 +1989,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 +2008,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 +2027,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);
}
@@ -4510,7 +4519,10 @@ public class GLStateManager {
RENDER_BACKEND.readPixels(x, y, width, height, format, type, pixels);
restorePixelPackBuffer();
}
public static void glTexStorage2D(int target, int levels, int internalFormat, int width, int height) { RENDER_BACKEND.texStorage2D(target, levels, internalFormat, width, height); }
public static void glTexStorage2D(int target, int levels, int internalFormat, int width, int height) {
RENDER_BACKEND.texStorage2D(target, levels, internalFormat, width, height);
TextureInfoCache.INSTANCE.onTexStorage2D(target, internalFormat, width, height);
}
public static void glClearTexImage(int texture, int level, int format, int type, ByteBuffer data) { RENDER_BACKEND.clearTexImage(texture, level, format, type); }
public static int glGenSamplers() { return RENDER_BACKEND.genSamplers(); }
@@ -5036,7 +5048,39 @@ public class GLStateManager {
}
public static void setVSyncEnabled(boolean enabled) {
RENDER_BACKEND.setVSyncEnabled(enabled);
setVSyncMode(RENDER_BACKEND.getPreferredVSyncMode(), enabled);
}
public static void setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
RENDER_BACKEND.setVSyncMode(preferred, vsyncEnabled);
}
public static VSyncMode getEffectiveVSyncMode() {
return RENDER_BACKEND.getEffectiveVSyncMode();
}
public static List<VSyncMode> getSelectableVSyncModes() {
return VSyncMode.selectable(RENDER_BACKEND::supportsVSyncMode);
}
public static int getMinRenderAhead() {
return RENDER_BACKEND.getMinRenderAhead();
}
public static int getMaxRenderAhead() {
return RENDER_BACKEND.getMaxRenderAhead();
}
public static int renderAheadLimit(int configured) {
return RenderBackend.clampRenderAhead(configured, RENDER_BACKEND.getMinRenderAhead(), RENDER_BACKEND.getMaxRenderAhead());
}
public static void applyRenderAheadLimit(int configured) {
RENDER_BACKEND.setRenderAheadLimit(renderAheadLimit(configured));
}
public static int getDisplayRefreshRateHz() {
return RENDER_BACKEND.getDisplayRefreshRateHz();
}
public static int glGetError() {
@@ -5119,6 +5163,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
maybeGenerateMipmap(target, level);
}
@@ -5134,6 +5179,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
maybeGenerateMipmap(target, level);
}
@@ -5143,6 +5189,7 @@ public class GLStateManager {
throw DisplayListManager.unsupportedInList("glTexSubImage2D with buffer offset");
}
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
maybeGenerateMipmap(target, level);
}
@@ -5183,6 +5230,7 @@ public class GLStateManager {
throw DisplayListManager.unsupportedInList("glCopyTexSubImage2D");
}
RENDER_BACKEND.copyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
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;
}
@@ -10,6 +10,7 @@ import org.lwjgl.opengl.ARBDebugOutputCallback;
import org.lwjgl.opengl.ARBDirectStateAccess;
import org.lwjgl.opengl.ARBTimerQuery;
import org.lwjgl.opengl.ContextCapabilities;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.EXTDirectStateAccess;
import org.lwjgl.opengl.EXTShaderImageLoadStore;
import org.lwjgl.opengl.GLContext;
@@ -114,7 +115,15 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
public int getMinGLSLVersion() {return 330;}
@Override
public void setVSyncEnabled(boolean enabled) {Display.setVSyncEnabled(enabled);}
protected void setSwapInterval(boolean vsync) {Display.setVSyncEnabled(vsync);}
@Override
public int getDisplayRefreshRateHz() {
final DisplayMode mode = Display.getDisplayMode();
if (mode != null && mode.getFrequency() > 0) return mode.getFrequency();
final DisplayMode desktop = Display.getDesktopDisplayMode();
return desktop == null ? 0 : desktop.getFrequency();
}
@Override
public boolean isAnisotropicSupported() {
@@ -62,7 +62,47 @@ public abstract class RenderBackend {
public abstract void flush();
public abstract void finish();
public abstract void setVSyncEnabled(boolean enabled);
protected abstract void setSwapInterval(boolean vsync);
private VSyncMode preferredVSyncMode = VSyncMode.AUTO;
private VSyncMode effectiveVSyncMode = VSyncMode.ON;
public final void setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
preferredVSyncMode = preferred == null ? VSyncMode.AUTO : preferred;
effectiveVSyncMode = applyVSyncMode(preferredVSyncMode, vsyncEnabled);
}
protected VSyncMode applyVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
final VSyncMode mode = preferred.resolve(vsyncEnabled).swapIntervalEquivalent();
setSwapInterval(mode.usesVSyncSwapInterval());
return mode;
}
public boolean supportsVSyncMode(VSyncMode mode) { return mode != VSyncMode.MAILBOX; }
public int getMinRenderAhead() { return 0; }
public int getMaxRenderAhead() { return 9; }
public void setRenderAheadLimit(int frames) {}
public static int clampRenderAhead(int value, int min, int max) {
if (value <= 0) return min <= 0 ? 0 : max;
return Math.min(Math.max(value, Math.max(min, 1)), max);
}
public final VSyncMode getPreferredVSyncMode() { return preferredVSyncMode; }
public final VSyncMode getEffectiveVSyncMode() { return effectiveVSyncMode; }
public int getDisplayRefreshRateHz() { return 0; }
public static int refreshHzFrom(int numerator, int denominator, float fallbackHz) {
if (numerator > 0 && denominator > 0) return (numerator + denominator - 1) / denominator;
return fallbackHz > 0.0f ? (int) Math.ceil(fallbackHz) : 0;
}
public String getPresentDebugInfo() { return null; }
public void onFrameBegin() {
if (GLStateManager.takeStateSeedPending()) {
@@ -0,0 +1,54 @@
package com.gtnewhorizons.angelica.glsm.backend;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public enum VSyncMode {
AUTO,
ON,
MAILBOX,
OFF;
private static final List<VSyncMode> OFF_ORDER = List.of(OFF, MAILBOX, ON);
private static final List<VSyncMode> MAILBOX_ORDER = List.of(MAILBOX, ON);
private static final List<VSyncMode> ON_ORDER = List.of(ON);
private static final List<VSyncMode> DISPLAY_ORDER = List.of(OFF, ON, MAILBOX);
public boolean blocksOnVBlank() {
return this == ON;
}
public boolean usesVSyncSwapInterval() {
return this == ON || this == MAILBOX;
}
public VSyncMode swapIntervalEquivalent() {
return this == MAILBOX ? ON : this;
}
public VSyncMode resolve(boolean vsyncEnabled) {
return this != AUTO ? this : (vsyncEnabled ? ON : OFF);
}
public List<VSyncMode> fallbackOrder() {
return switch (this) {
case OFF -> OFF_ORDER;
case MAILBOX -> MAILBOX_ORDER;
default -> ON_ORDER;
};
}
public boolean tearFree() {
return this != OFF;
}
public static List<VSyncMode> selectable(Predicate<VSyncMode> supported) {
final List<VSyncMode> modes = new ArrayList<>(DISPLAY_ORDER.size());
for (VSyncMode mode : DISPLAY_ORDER) {
if (supported.test(mode)) modes.add(mode);
}
return modes;
}
}
@@ -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
*/
@@ -36,6 +36,12 @@ public class TextureInfo {
@Getter @Setter protected boolean generateMipmap = false;
@Getter protected int uploadGeneration;
void bumpUploadGeneration() {
uploadGeneration++;
}
protected TextureInfo(int id) {
this.id = id;
}
@@ -53,6 +53,7 @@ public class TextureInfoCache {
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
info.bumpUploadGeneration();
}
}
@@ -65,9 +66,29 @@ public class TextureInfoCache {
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
info.bumpUploadGeneration();
}
}
public void onTexStorage2D(int target, int internalformat, int width, int height) {
if (!isTrackedTarget(target)) return;
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
if (info == null) return;
info.setTarget(target);
info.internalFormat = internalformat;
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
info.bumpUploadGeneration();
}
public void onTexSubImage2D(int target, int level) {
if (!isTrackedTarget(target) || level != 0) return;
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
if (info == null) return;
info.bumpUploadGeneration();
}
public static boolean isGenericCompressedInternalFormat(int internalFormat) {
return switch (internalFormat) {
case GL30.GL_COMPRESSED_RED,
@@ -0,0 +1,71 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@GLCoreTest
public class GLSM_TextureUploadGeneration_GLTest {
private static int allocate(int size) {
final int texId = GLStateManager.glGenTextures();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texId);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(size * size * 4));
return texId;
}
@Test
void anUploadLeavesANonZeroGeneration() {
final int texId = allocate(4);
try {
assertNotEquals(0, TextureInfoCache.INSTANCE.getInfo(texId).getUploadGeneration());
} finally {
GLStateManager.glDeleteTextures(texId);
}
}
@Test
void recreatingATextureAtAReusedIdYieldsAFreshInfo() {
final int texId = allocate(4);
try {
final TextureInfo first = TextureInfoCache.INSTANCE.getInfo(texId);
final int firstGeneration = first.getUploadGeneration();
TextureInfoCache.INSTANCE.onDeleteTexture(texId);
final ByteBuffer pixels = BufferUtils.createByteBuffer(4 * 4 * 4);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 4, 4, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
final TextureInfo second = TextureInfoCache.INSTANCE.getInfo(texId);
assertNotSame(first, second, "a deleted id drops its entry, so the replacement must be a distinct instance");
assertEquals(firstGeneration, second.getUploadGeneration(), "generations alias across a delete/recreate cycle, which is exactly why identity has to carry the distinction");
} finally {
GLStateManager.glDeleteTextures(texId);
}
}
@Test
void anInPlaceRewriteBumpsTheGeneration() {
final int texId = allocate(4);
try {
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(texId);
final int afterAlloc = info.getUploadGeneration();
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(2 * 2 * 4));
assertNotEquals(afterAlloc, info.getUploadGeneration(), "content changed, so anything caching the pixels is stale");
final int afterSub = info.getUploadGeneration();
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 1, 0, 0, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(4));
assertEquals(afterSub, info.getUploadGeneration(), "only level 0 is tracked");
} finally {
GLStateManager.glDeleteTextures(texId);
}
}
}
@@ -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,120 @@
package com.gtnewhorizons.angelica.glsm.backend;
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 BackendPolicyTest {
@Test
void autoFollowsTheVsyncToggle() {
assertEquals(VSyncMode.ON, VSyncMode.AUTO.resolve(true));
assertEquals(VSyncMode.OFF, VSyncMode.AUTO.resolve(false));
}
@Test
void anExplicitModeIgnoresTheVsyncToggle() {
for (VSyncMode mode : new VSyncMode[]{ VSyncMode.ON, VSyncMode.MAILBOX, VSyncMode.OFF }) {
assertEquals(mode, mode.resolve(true));
assertEquals(mode, mode.resolve(false));
}
}
@Test
void onlyOnBlocks() {
assertTrue(VSyncMode.ON.blocksOnVBlank());
assertFalse(VSyncMode.MAILBOX.blocksOnVBlank());
assertFalse(VSyncMode.OFF.blocksOnVBlank());
}
@Test
void mailboxRidesOnSwapIntervalOne() {
assertTrue(VSyncMode.ON.usesVSyncSwapInterval());
assertTrue(VSyncMode.MAILBOX.usesVSyncSwapInterval());
assertFalse(VSyncMode.OFF.usesVSyncSwapInterval());
}
@Test
void swapIntervalBackendsReportWhatTheyActuallyGet() {
assertEquals(VSyncMode.ON, VSyncMode.MAILBOX.swapIntervalEquivalent());
assertEquals(VSyncMode.ON, VSyncMode.ON.swapIntervalEquivalent());
assertEquals(VSyncMode.OFF, VSyncMode.OFF.swapIntervalEquivalent());
}
@Test
void fallbackNeverWidensTearing() {
assertEquals(List.of(VSyncMode.ON), VSyncMode.ON.fallbackOrder());
assertEquals(List.of(VSyncMode.MAILBOX, VSyncMode.ON), VSyncMode.MAILBOX.fallbackOrder());
assertEquals(List.of(VSyncMode.OFF, VSyncMode.MAILBOX, VSyncMode.ON), VSyncMode.OFF.fallbackOrder());
}
@Test
void onlyOffCanTear() {
assertTrue(VSyncMode.ON.tearFree());
assertTrue(VSyncMode.MAILBOX.tearFree());
assertFalse(VSyncMode.OFF.tearFree());
}
@Test
void onlyBackendSupportedModesAreSelectable() {
assertEquals(List.of(VSyncMode.OFF, VSyncMode.ON), VSyncMode.selectable(m -> m != VSyncMode.MAILBOX));
assertEquals(List.of(VSyncMode.OFF, VSyncMode.ON, VSyncMode.MAILBOX), VSyncMode.selectable(m -> true));
}
@Test
void autoIsNeverSelectable() {
assertFalse(VSyncMode.selectable(m -> true).contains(VSyncMode.AUTO));
}
@Test
void refreshPrefersTheExactRational() {
assertEquals(144, RenderBackend.refreshHzFrom(144, 1, 0.0f));
assertEquals(60, RenderBackend.refreshHzFrom(60000, 1001, 0.0f));
}
@Test
void refreshRoundsUp() {
assertEquals(144, RenderBackend.refreshHzFrom(0, 0, 143.98f));
assertEquals(60, RenderBackend.refreshHzFrom(0, 0, 59.94f));
assertEquals(60, RenderBackend.refreshHzFrom(0, 0, 60.0f));
}
@Test
void refreshIsZeroWhenUnknown() {
assertEquals(0, RenderBackend.refreshHzFrom(0, 0, 0.0f));
assertEquals(0, RenderBackend.refreshHzFrom(-1, -1, -1.0f));
assertEquals(0, RenderBackend.refreshHzFrom(144, 0, 0.0f));
}
@Test
void glRenderAheadKeepsZeroAsOff() {
assertEquals(0, RenderBackend.clampRenderAhead(0, 0, 9));
assertEquals(0, RenderBackend.clampRenderAhead(-1, 0, 9));
}
@Test
void glRenderAheadClampsToItsRange() {
assertEquals(1, RenderBackend.clampRenderAhead(1, 0, 9));
assertEquals(3, RenderBackend.clampRenderAhead(3, 0, 9));
assertEquals(9, RenderBackend.clampRenderAhead(9, 0, 9));
assertEquals(9, RenderBackend.clampRenderAhead(20, 0, 9));
}
@Test
void sdlRenderAheadHasNoOffSwitch() {
assertEquals(3, RenderBackend.clampRenderAhead(0, 1, 3));
assertEquals(3, RenderBackend.clampRenderAhead(-1, 1, 3));
}
@Test
void sdlRenderAheadClampsToItsRange() {
assertEquals(1, RenderBackend.clampRenderAhead(1, 1, 3));
assertEquals(2, RenderBackend.clampRenderAhead(2, 1, 3));
assertEquals(3, RenderBackend.clampRenderAhead(3, 1, 3));
assertEquals(3, RenderBackend.clampRenderAhead(6, 1, 3));
}
}
@@ -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);
}
}
@@ -32,6 +32,7 @@ import org.lwjgl.opengl.EXTPolygonOffsetClamp;
import org.lwjgl.opengl.EXTShaderImageLoadStore;
import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.opengl.GLDebugMessageCallback;
import org.lwjgl.sdl.SDL_DisplayMode;
import org.lwjgl.sdl.SDL_DropEvent;
import org.lwjgl.sdl.SDL_EventFilter;
import org.lwjgl.sdl.SDLEvents;
@@ -39,6 +40,7 @@ import org.lwjgl.sdl.SDLVideo;
import org.lwjgl.system.JNI;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjglx.opengl.Display;
import java.nio.Buffer;
import java.nio.ByteBuffer;
@@ -169,7 +171,17 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
public int getMinGLSLVersion() {return 330;}
@Override
public void setVSyncEnabled(boolean enabled) {SDLVideo.SDL_GL_SetSwapInterval(enabled ? 1 : 0);}
protected void setSwapInterval(boolean vsync) {SDLVideo.SDL_GL_SetSwapInterval(vsync ? 1 : 0);}
@Override
public int getDisplayRefreshRateHz() {
final long window = Display.getWindow();
final int displayId = window == 0 ? SDLVideo.SDL_GetPrimaryDisplay() : SDLVideo.SDL_GetDisplayForWindow(window);
if (displayId == 0) return 0;
final SDL_DisplayMode mode = SDLVideo.SDL_GetCurrentDisplayMode(displayId);
if (mode == null) return 0;
return refreshHzFrom(mode.refresh_rate_numerator(), mode.refresh_rate_denominator(), mode.refresh_rate());
}
@Override
public boolean isAnisotropicSupported() {
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.glsm.CaptureGate;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
@@ -375,8 +376,26 @@ public class SDLGPURenderBackend extends RenderBackend {
LOG.info("Populated GL capabilities for SDL GPU backend");
}
@Override public void setVSyncEnabled(boolean enabled) {
device.setVSyncEnabled(enabled);
@Override protected void setSwapInterval(boolean vsync) {
device.setVSyncMode(VSyncMode.AUTO, vsync);
}
@Override protected VSyncMode applyVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
return device.setVSyncMode(preferred, vsyncEnabled);
}
@Override public boolean supportsVSyncMode(VSyncMode mode) {
return mode == VSyncMode.AUTO || device.supportsVSyncMode(mode);
}
@Override public int getMinRenderAhead() { return Device.MIN_FRAMES_IN_FLIGHT; }
@Override public int getMaxRenderAhead() { return Device.MAX_FRAMES_IN_FLIGHT; }
@Override public void setRenderAheadLimit(int frames) { device.setFramesInFlight(frames); }
@Override public int getDisplayRefreshRateHz() {
return device.getDisplayRefreshRateHz();
}
@Override public void shutdown() {
@@ -549,9 +568,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;
@@ -677,6 +700,54 @@ public class SDLGPURenderBackend extends RenderBackend {
return transferThread != null ? transferThread.getDebugInfo() : null;
}
private static final long PRESENT_STATS_WINDOW_NANOS = 1_000_000_000L;
private static final long PRESENT_STATS_STALE_NANOS = 2_000_000_000L;
private long presentStatsNanos;
private long presentStatsLoopFrames;
private long presentStatsPresentedFrames;
private long presentStatsSkips;
private long presentStatsEmpty;
private long presentStatsAcquireNanos;
private String presentDebugLine;
@Override public String getPresentDebugInfo() {
if (shutdown) return null;
final long now = System.nanoTime();
final long elapsed = now - presentStatsNanos;
if (presentStatsNanos == 0L || elapsed > PRESENT_STATS_STALE_NANOS) {
snapshotPresentStats(now);
presentDebugLine = null;
return null;
}
if (elapsed < PRESENT_STATS_WINDOW_NANOS) return presentDebugLine;
final long loopDelta = frameManager.statLoopFrames() - presentStatsLoopFrames;
final long presentedDelta = frameManager.statPresentedFrames() - presentStatsPresentedFrames;
final long skipDelta = frameManager.statPresentSkips() - presentStatsSkips;
final long emptyDelta = frameManager.statEmptyFrames() - presentStatsEmpty;
final long acquireDelta = frameManager.statAcquireWaitNanos() - presentStatsAcquireNanos;
final double seconds = elapsed / 1.0e9;
presentDebugLine = String.format("Present: %s %dHz | loop %.0f/s shown %.0f/s | skip %.0f/s empty %.0f/s | acq %.2fms | fif %d",
getEffectiveVSyncMode(), device.getDisplayRefreshRateHz(),
loopDelta / seconds, presentedDelta / seconds,
skipDelta / seconds, emptyDelta / seconds,
loopDelta == 0 ? 0.0 : acquireDelta / (double) loopDelta / 1.0e6,
Device.framesInFlight());
snapshotPresentStats(now);
return presentDebugLine;
}
private void snapshotPresentStats(long now) {
presentStatsNanos = now;
presentStatsLoopFrames = frameManager.statLoopFrames();
presentStatsPresentedFrames = frameManager.statPresentedFrames();
presentStatsSkips = frameManager.statPresentSkips();
presentStatsEmpty = frameManager.statEmptyFrames();
presentStatsAcquireNanos = frameManager.statAcquireWaitNanos();
}
@Override public void enable(int cap) { setBoolCap(cap, true); }
@Override public void enablei(int cap, int index) { setBoolCapIndexed(cap, index, true); }
@Override public void disable(int cap) { setBoolCap(cap, false); }
@@ -869,14 +940,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 +961,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 +994,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 +1543,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 +1585,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 +1765,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 +3335,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 +3784,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) {
@@ -1,6 +1,8 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.sdlgpu.util.DebugMessageRelay;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -29,7 +31,13 @@ import static org.lwjgl.system.MemoryUtil.memUTF8;
public final class Device {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
public static final int FRAMES_IN_FLIGHT = 3;
public static final int MIN_FRAMES_IN_FLIGHT = 1;
public static final int DEFAULT_FRAMES_IN_FLIGHT = 2;
public static final int MAX_FRAMES_IN_FLIGHT = 3;
private static volatile int framesInFlight = DEFAULT_FRAMES_IN_FLIGHT;
public static int framesInFlight() { return framesInFlight; }
private static final int REQUESTED_SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXBC | SDL_GPU_SHADERFORMAT_DXIL;
@@ -40,7 +48,8 @@ public final class Device {
private String deviceName;
private String driverVersion;
private String driverInfo;
private int presentMode = SDL_GPU_PRESENTMODE_VSYNC;
private VSyncMode vsyncMode = VSyncMode.ON;
private volatile String windowThreadName;
private SDL_LogOutputFunction logCallback; // prevent GC
private volatile boolean lost;
@@ -146,13 +155,7 @@ public final class Device {
throw new RuntimeException("Failed to claim window for SDL GPU device: " + SDLError.SDL_GetError());
}
claimed = true;
final int n = FRAMES_IN_FLIGHT;
if (!SDL_SetGPUAllowedFramesInFlight(device, n)) {
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}", n, SDLError.SDL_GetError());
} else {
LOG.info("SDL frames-in-flight set to {}", n);
}
windowThreadName = Thread.currentThread().getName();
logWindowDiagnostics(window);
LOG.info("SDL GPU device claimed window successfully");
@@ -260,6 +263,10 @@ public final class Device {
return driverName;
}
public String getWindowThreadName() {
return windowThreadName;
}
public String getDeviceName() {
return deviceName;
}
@@ -288,22 +295,56 @@ public final class Device {
return (supportedShaderFormats & SDL_GPU_SHADERFORMAT_DXBC) != 0;
}
public void setVSyncEnabled(boolean enabled) {
final long window = Display.getWindow();
final int mode;
if (enabled) {
mode = SDL_GPU_PRESENTMODE_VSYNC;
} else if (SDL_WindowSupportsGPUPresentMode(device, window, SDL_GPU_PRESENTMODE_MAILBOX)) {
mode = SDL_GPU_PRESENTMODE_MAILBOX;
} else {
mode = SDL_GPU_PRESENTMODE_IMMEDIATE;
}
if (!SDL_SetGPUSwapchainParameters(device, window, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode)) {
LOG.warn("Failed to set present mode {}: {} (keeping prior mode {})", mode, SDLError.SDL_GetError(), presentMode);
public void setFramesInFlight(int frames) {
final int n = Math.min(Math.max(frames, MIN_FRAMES_IN_FLIGHT), MAX_FRAMES_IN_FLIGHT);
if (n == framesInFlight || device == 0 || !claimed) return;
if (!SDL_SetGPUAllowedFramesInFlight(device, n)) {
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}; keeping {}", n, SDLError.SDL_GetError(), framesInFlight);
return;
}
presentMode = mode;
LOG.info("SDL present mode set to {}", mode);
framesInFlight = n;
LOG.info("SDL frames-in-flight set to {}", n);
}
public VSyncMode setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
final long window = Display.getWindow();
final VSyncMode requested = preferred.resolve(vsyncEnabled);
for (VSyncMode candidate : requested.fallbackOrder()) {
final int sdlMode = toSdl(candidate);
if (!SDL_WindowSupportsGPUPresentMode(device, window, sdlMode)) continue;
if (!SDL_SetGPUSwapchainParameters(device, window, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, sdlMode)) {
LOG.warn("Failed to set present mode {}: {}", candidate, SDLError.SDL_GetError());
continue;
}
vsyncMode = candidate;
LOG.info("SDL present mode {} (requested {}, preference {}, vsync={}), refresh {}Hz, supported: vsync={} mailbox={} immediate={}",
candidate, requested, preferred, vsyncEnabled, getDisplayRefreshRateHz(),
supportsVSyncMode(VSyncMode.ON), supportsVSyncMode(VSyncMode.MAILBOX), supportsVSyncMode(VSyncMode.OFF));
return candidate;
}
LOG.warn("No usable present mode for request {}; keeping {}", requested, vsyncMode);
return vsyncMode;
}
public boolean supportsVSyncMode(VSyncMode mode) {
return device != 0 && claimed && SDL_WindowSupportsGPUPresentMode(device, Display.getWindow(), toSdl(mode));
}
private static int toSdl(VSyncMode mode) {
return switch (mode) {
case OFF -> SDL_GPU_PRESENTMODE_IMMEDIATE;
case MAILBOX -> SDL_GPU_PRESENTMODE_MAILBOX;
default -> SDL_GPU_PRESENTMODE_VSYNC;
};
}
public int getDisplayRefreshRateHz() {
final long window = Display.getWindow();
final int displayId = window == 0 ? SDL_GetPrimaryDisplay() : SDL_GetDisplayForWindow(window);
if (displayId == 0) return 0;
final SDL_DisplayMode mode = SDL_GetCurrentDisplayMode(displayId);
if (mode == null) return 0;
return RenderBackend.refreshHzFrom(mode.refresh_rate_numerator(), mode.refresh_rate_denominator(), mode.refresh_rate());
}
public int getSwapchainTextureFormat() {
@@ -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();
@@ -37,7 +37,6 @@ public final class FrameManager {
private static final Tracy.ZoneId Z_SDL_FENCE_WAIT = Tracy.zoneId("sdlFenceWait", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_SDL_SUBMIT = Tracy.zoneId("sdlSubmit", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_SDL_ACQUIRE_WAIT = Tracy.zoneId("sdlAcquireWait", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_SDL_SWAPCHAIN_WAIT = Tracy.zoneId("sdlSwapchainWait", Tracy.COLOR_SWAP);
private final Device device;
private ResourceManager resourceManager;
@@ -57,6 +56,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;
@@ -88,7 +88,6 @@ public final class FrameManager {
public int emptyFramesThisFrame;
public int droppedDrawsThisFrame;
public int computeBatchJoinsThisFrame;
public boolean drawableStarved;
public int clearPassesThisFrame;
public int swapchainPassesThisFrame;
public int fboPassesThisFrame;
@@ -180,6 +179,18 @@ public final class FrameManager {
private Runnable beforeEndCopyPass;
private long statLoopFrames;
private long statPresentedFrames;
private long statPresentSkips;
private long statEmptyFrames;
private long statAcquireWaitNanos;
public long statLoopFrames() { return statLoopFrames; }
public long statPresentedFrames() { return statPresentedFrames; }
public long statPresentSkips() { return statPresentSkips; }
public long statEmptyFrames() { return statEmptyFrames; }
public long statAcquireWaitNanos() { return statAcquireWaitNanos; }
public FrameManager(Device device) {
this.device = device;
}
@@ -198,7 +209,13 @@ public final class FrameManager {
}
if (!threadLogged) {
threadLogged = true;
LOG.info("First beginFrame() on thread={}; must match the SDL window-creating thread", Thread.currentThread().getName());
final String renderThread = Thread.currentThread().getName();
final String windowThread = device.getWindowThreadName();
if (windowThread != null && !windowThread.equals(renderThread)) {
LOG.warn("Frames are recorded on '{}' but the SDL window was created on '{}'; SDL requires swapchain acquire/wait on the window-creating thread", renderThread, windowThread);
} else {
LOG.info("First beginFrame() on thread={}", renderThread);
}
}
flushPendingUploadCommandBuffer(f);
@@ -209,13 +226,6 @@ public final class FrameManager {
finally { Tracy.endZone(); }
}
if (f.drawableStarved) {
f.drawableStarved = false;
Tracy.beginZone(Z_SDL_SWAPCHAIN_WAIT);
try { SDL_WaitForGPUSwapchain(device.getDevice(), Display.getWindow()); }
finally { Tracy.endZone(); }
}
f.commandBuffer = SDL_AcquireGPUCommandBuffer(device.getDevice());
if (f.commandBuffer == 0) {
throw new RuntimeException("Failed to acquire GPU command buffer: " + SDLError.SDL_GetError());
@@ -233,6 +243,7 @@ public final class FrameManager {
f.currentDepthTarget = 0;
f.frameNumber++;
f.clearedThisFrame = false;
f.depthClearedThisFrame = false;
f.swapchainUsedThisFrame = false;
f.skipPresent = false;
}
@@ -242,14 +253,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 +289,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 +311,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;
}
@@ -486,9 +538,14 @@ public final class FrameManager {
f.commandBuffer = 0;
}
f.drawableStarved = f.presentSkipsThisFrame > 0;
if (Tracy.ENABLED && resourceManager != null) SdlFramePlots.emit(f, resourceManager);
statLoopFrames++;
if (f.swapchainAcquiredThisFrame) statPresentedFrames++;
statPresentSkips += f.presentSkipsThisFrame;
statEmptyFrames += f.emptyFramesThisFrame;
statAcquireWaitNanos += f.acquireWaitNanosThisFrame;
f.lastPlottedRenderPassGen = f.renderPassGeneration;
f.computePassesThisFrame = 0;
f.submitsThisFrame = 0;
@@ -804,7 +861,7 @@ public final class FrameManager {
endActiveEncoders(f);
final long t0 = Tracy.ENABLED ? System.nanoTime() : 0L;
final long t0 = System.nanoTime();
final long tex;
final int w;
final int h;
@@ -815,7 +872,7 @@ public final class FrameManager {
final IntBuffer pHeight = stack.ints(0);
Tracy.beginZone(Z_SDL_ACQUIRE_WAIT);
try {
callOk = SDL_AcquireGPUSwapchainTexture(f.commandBuffer, Display.getWindow(), pTexture, pWidth, pHeight);
callOk = SDL_WaitAndAcquireGPUSwapchainTexture(f.commandBuffer, Display.getWindow(), pTexture, pWidth, pHeight);
} finally {
Tracy.endZone();
}
@@ -823,11 +880,11 @@ public final class FrameManager {
w = pWidth.get(0);
h = pHeight.get(0);
} catch (RuntimeException re) {
if (Tracy.ENABLED) f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
f.swapchainUnavailable = true;
throw re;
}
if (Tracy.ENABLED) f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
if (!callOk) {
f.swapchainUnavailable = true;
@@ -97,7 +97,7 @@ final class SdlFramePlots {
final long nowNanos = System.nanoTime();
if (f.lastEndFrameNanos != 0) Tracy.plotInt(PLOT_PRESENT_TO_PRESENT_US, (nowNanos - f.lastEndFrameNanos) / 1000);
f.lastEndFrameNanos = nowNanos;
Tracy.plotInt(PLOT_FRAMES_IN_FLIGHT, Device.FRAMES_IN_FLIGHT);
Tracy.plotInt(PLOT_FRAMES_IN_FLIGHT, Device.framesInFlight());
Tracy.plotInt(PLOT_MIP_GENS, f.mipGensThisFrame);
Tracy.plotInt(PLOT_BLIT_FALLBACKS, f.blitsThisFrame);
Tracy.plotInt(PLOT_PRESENT_SKIPS, f.presentSkipsThisFrame);
@@ -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 {
@@ -517,7 +587,7 @@ public final class ResourceManager {
final GpuBucket b = gpuBufferPool.get(Hashing.packHiLo(usage, bucket));
if (b == null || b.handles.isEmpty()) return 0;
for (int i = 0, n = b.handles.size(); i < n; i++) {
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.FRAMES_IN_FLIGHT) {
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.framesInFlight()) {
final long handle = b.handles.getLong(i);
final int last = n - 1;
if (i != last) {
@@ -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;
@@ -1305,7 +1376,7 @@ public final class ResourceManager {
for (int i = 0; i < n; i += 2) {
final long handle = batchSegmentPending.getLong(i);
final long stamp = batchSegmentPending.getLong(i + 1);
if (currentFrame - stamp >= Device.FRAMES_IN_FLIGHT) {
if (currentFrame - stamp >= Device.framesInFlight()) {
batchSegmentFree.enqueue(handle);
} else {
batchSegmentPending.set(w, handle);
@@ -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,8 +2086,36 @@ 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_BLOCKS = 2048 * Device.MAX_FRAMES_IN_FLIGHT;
public static final int ATTRIB_RING_CHUNK_BLOCKS = 64;
private static final int ATTRIB_RING_CHUNKS = ATTRIB_RING_BLOCKS / ATTRIB_RING_CHUNK_BLOCKS;
private final AtomicLong attribRingCounter = new AtomicLong();
@@ -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,61 @@
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 idleThread() {
final SdlTestRig rig = SdlTestRig.create();
final TransferThread tt = new TransferThread(rig.device, rig.resourceManager);
tt.shutdown();
try {
tt.getThread().join(5_000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AssertionError("interrupted while stopping the transfer thread", e);
}
assertFalse(tt.getThread().isAlive(), "the worker must be stopped first: its loop zeroes openHighestSeq after publishing, so a live worker races this test");
return tt;
}
@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 = idleThread();
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");
}
@Test
void aDroppedBufferUploadStillRetiresItsSeq() {
final TransferThread tt = idleThread();
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"));
}
}
@@ -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) {
@@ -0,0 +1,8 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
import net.minecraft.client.model.TexturedQuad;
public interface ModelBoxQuads {
TexturedQuad[] angelica$getQuadList();
}
@@ -106,6 +106,10 @@ public final class ClientProxy extends CommonProxy {
return CONFIG;
}
public static void applyVSyncMode(boolean vsyncEnabled) {
GLStateManager.setVSyncMode(options().advanced.vsyncMode, vsyncEnabled);
}
@Override
public void preInit(FMLPreInitializationEvent event) {
ModStatus.preInit();
@@ -0,0 +1,116 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
public final class FramePacer {
private static final int MIN_PLAUSIBLE_REFRESH_HZ = 20;
private static final int MAX_PLAUSIBLE_REFRESH_HZ = 1000;
private static final long REFRESH_POLL_NANOS = 1_000_000_000L;
private static long lastFrameTime;
private static long fpsLimitOverhead;
private static long lastRefreshPollNanos;
private static int cachedRefreshHz;
private static boolean warnedUnknownRefresh;
private static VSyncMode lastVSyncMode = VSyncMode.ON;
private static int lastCapHz;
private static int lastRefreshHz;
private FramePacer() {}
public static int plausibleRefreshHz(int hz) {
return hz >= MIN_PLAUSIBLE_REFRESH_HZ && hz <= MAX_PLAUSIBLE_REFRESH_HZ ? hz : 0;
}
public static int pacingCeilingHz(VSyncMode mode, int capHz, int refreshHz) {
if (mode.blocksOnVBlank()) return 0;
final int cap = Math.max(capHz, 0);
if (mode != VSyncMode.MAILBOX) return cap;
final int refresh = plausibleRefreshHz(refreshHz);
if (cap == 0) return refresh;
return refresh == 0 ? cap : Math.min(cap, refresh);
}
public static long sleepNanosFor(long targetNanos, long lastWorkNanos, long overheadNanos) {
return targetNanos - lastWorkNanos - overheadNanos;
}
public static int updateCeiling(int capHz) {
lastVSyncMode = GLStateManager.getEffectiveVSyncMode();
lastCapHz = Math.max(capHz, 0);
lastRefreshHz = refreshHz();
return pacingCeilingHz(lastVSyncMode, lastCapHz, lastRefreshHz);
}
public static long pace(int ceilingHz) {
if (ceilingHz > 0) {
final long time = System.nanoTime();
final long targetNanos = 1_000_000_000L / ceilingHz;
final long sleepNanos = sleepNanosFor(targetNanos, time - lastFrameTime, fpsLimitOverhead);
if (sleepNanos > 0) {
try {
Thread.sleep(sleepNanos / 1_000_000, (int) sleepNanos % 1_000_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long overhead = System.nanoTime() - time - sleepNanos;
if (overhead < 0 || overhead > targetNanos / 2) overhead = 0;
fpsLimitOverhead = overhead;
}
}
final long time = System.nanoTime();
final long period = time - lastFrameTime;
lastFrameTime = time;
return period;
}
private static int refreshHz() {
final long now = System.nanoTime();
if (lastRefreshPollNanos == 0L || now - lastRefreshPollNanos >= REFRESH_POLL_NANOS) {
lastRefreshPollNanos = now;
cachedRefreshHz = GLStateManager.getDisplayRefreshRateHz();
}
final int hz = plausibleRefreshHz(cachedRefreshHz);
if (hz == 0 && lastVSyncMode == VSyncMode.MAILBOX && !warnedUnknownRefresh) {
warnedUnknownRefresh = true;
AngelicaMod.LOGGER.warn("Display refresh rate reported as {}Hz; Mailbox will not be paced to the refresh rate. The Max Framerate setting still applies.", cachedRefreshHz);
}
return hz;
}
public static String debugIndicator(VSyncMode mode, int refreshHz, int capHz) {
final boolean vsync = mode.blocksOnVBlank();
final boolean mailbox = mode == VSyncMode.MAILBOX;
final boolean showCap = capHz > 0 && !vsync;
if (!vsync && !mailbox && !showCap) return null;
final StringBuilder sb = new StringBuilder(" [");
if (vsync || mailbox) {
sb.append(vsync ? "vsync" : "mailbox");
if (refreshHz > 0) sb.append(' ').append(refreshHz);
if (showCap) sb.append(", ");
}
if (showCap) sb.append("cap ").append(capHz);
return sb.append(']').toString();
}
public static String debugIndicator() {
return debugIndicator(lastVSyncMode, lastRefreshHz, lastCapHz);
}
public static VSyncMode lastVSyncMode() { return lastVSyncMode; }
public static int lastCapHz() { return lastCapHz; }
public static int lastRefreshHz() { return lastRefreshHz; }
}
@@ -5,66 +5,103 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.states.ColorMask;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.mixins.interfaces.ModelBoxQuads;
import com.gtnewhorizons.angelica.rendering.tesr.PassRebindGate;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.DepthColorStorage;
import net.coderbot.iris.gl.image.GlImage;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.minecraft.client.model.ModelBiped;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.model.PositionTextureVertex;
import net.minecraft.client.model.TexturedQuad;
import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.RendererLivingEntity;
import net.minecraft.entity.player.EntityPlayer;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Skins in 1.7.10 are 64x32 while modern has 64x64 textures. We have to address it here.
* Feeds the shaderpack's world-space player reflection, which expects the modern 12-box player model:
* 288 vertices over a 64x64 skin layout. 1.7.10 has 7 boxes and a 64x32 layout, so both are synthesized here.
*/
public final class PlayerReflectionCapture {
public static final int VERTEX_COUNT = 288;
public static final int FLOATS_PER_VERTEX = 5; // x, y, z, u, v
public static final int PART_COUNT = 12;
public static final int VERTICES_PER_PART = VERTEX_COUNT / PART_COUNT;
private static final int RAW_INTS_PER_VERTEX = 8;
private static final int ATLAS_LAYOUT = 64;
private static final int VERTEX_FLAGS = VertexFlags.convertToFlags(true, false, true, false);
private static final Tracy.ZoneId Z_PLAYER_REFLECTION_FLUSH = Tracy.zoneId("playerReflectionFlush", Tracy.COLOR_IRIS);
private static boolean active;
private static boolean drawnThisCapture;
private static boolean captured;
@Getter
private static Object target;
@Getter
private static EntityPlayer targetEntity;
private static boolean pending;
private static final Matrix4f pendingModelView = new Matrix4f();
private static int pendingSkin;
private static int pendingEntityId;
private static int vbo;
private static int vao;
private static int[] rawData;
private static float[] vertexScratch;
private static int skin64Tex;
private static int skin64Size;
private static boolean skin64Valid;
private static ByteBuffer skinPixels;
private static int lastCapturedSkin;
private static int lastUploadedAtlas;
private static int skinCopyWidth = 64;
private static int skinCopyHeight = 32;
private static TextureInfo lastCapturedInfo;
private static int lastCapturedGeneration = -1;
private static TextureInfo lastUploadedAtlasInfo;
private static int lastUploadedAtlasGeneration = -1;
private static int skinCopyWidth;
private static int skinCopyHeight;
private static ByteBuffer repackBuffer;
private static long repackAddress;
private static int repackCapacity;
private static final ModelRenderer[] partOrder = new ModelRenderer[PART_COUNT];
private static final Matrix4f partMatrix = new Matrix4f();
private static final Vector3f partVertex = new Vector3f();
private static final Matrix4f savedModelView = new Matrix4f();
private PlayerReflectionCapture() {}
public static void begin(EntityPlayer player) {
active = false;
drawnThisCapture = false;
captured = false;
pending = false;
target = null;
targetEntity = null;
if (player == null) return;
@@ -79,24 +116,122 @@ public final class PlayerReflectionCapture {
public static void end() {
active = false;
pending = false;
target = null;
targetEntity = null;
}
public static boolean shouldCapture() {
return active && !drawnThisCapture;
return active && !captured;
}
/**
* Emit the 288-vertex player batch VBO.
*/
public static void drawPlayerCapture(float[] xyzuv) {
if (drawnThisCapture) return;
drawnThisCapture = true;
public static void emitAndSubmit(ModelBiped model, float scale) {
captured = true;
final float[] data = vertexScratch();
emitModel(model, scale, data);
pendingModelView.set(GLStateManager.getModelViewMatrix());
pendingSkin = GLStateManager.getBoundTextureForServerState(0);
pendingEntityId = CapturedRenderingState.INSTANCE.getCurrentRenderedEntity();
pending = true;
}
public static ModelRenderer[] partOrder(ModelBiped model, ModelRenderer[] out) {
out[0] = model.bipedHead;
out[1] = model.bipedHeadwear;
out[2] = model.bipedRightArm;
out[3] = model.bipedRightArm;
out[4] = model.bipedLeftLeg;
out[5] = model.bipedLeftLeg;
out[6] = model.bipedLeftArm;
out[7] = model.bipedLeftArm;
out[8] = model.bipedRightLeg;
out[9] = model.bipedRightLeg;
out[10] = model.bipedBody;
out[11] = model.bipedBody;
return out;
}
private static void emitModel(ModelBiped model, float scale, float[] out) {
final ModelRenderer[] parts = partOrder(model, partOrder);
int o = 0;
for (int i = 0; i < PART_COUNT; i++) {
o = emitBox(parts[i], quadsOf(parts[i]), out, o, scale);
}
}
private static TexturedQuad[] quadsOf(ModelRenderer part) {
if (part == null || part.cubeList == null || part.cubeList.isEmpty()) return null;
final ModelBox box = part.cubeList.getFirst();
return ((ModelBoxQuads) box).angelica$getQuadList();
}
public static int emitBox(ModelRenderer part, TexturedQuad[] quads, float[] out, int o, float scale) {
if (part == null || quads == null || part.isHidden || !part.showModel || part.textureWidth > ATLAS_LAYOUT || part.textureHeight > ATLAS_LAYOUT) {
final int end = o + VERTICES_PER_PART * FLOATS_PER_VERTEX;
Arrays.fill(out, o, end, 0.0f);
return end;
}
final Matrix4f m = partMatrix
.rotationZYX(part.rotateAngleZ, part.rotateAngleY, part.rotateAngleX)
.setTranslation(
part.offsetX + part.rotationPointX * scale,
part.offsetY + part.rotationPointY * scale,
part.offsetZ + part.rotationPointZ * scale);
final float uScale = part.textureWidth / ATLAS_LAYOUT;
final float vScale = part.textureHeight / ATLAS_LAYOUT;
final Vector3f v = partVertex;
for (int q = 0; q < 6; q++) {
final PositionTextureVertex[] corners = quads[q].vertexPositions;
for (int c = 0; c < 4; c++) {
final PositionTextureVertex corner = corners[c];
v.set((float) corner.vector3D.xCoord * scale,
(float) corner.vector3D.yCoord * scale,
(float) corner.vector3D.zCoord * scale);
m.transformPosition(v);
out[o++] = v.x;
out[o++] = v.y;
out[o++] = v.z;
out[o++] = corner.texturePositionX * uScale;
out[o++] = corner.texturePositionY * vScale;
}
}
return o;
}
public static void flush() {
if (!pending) return;
pending = false;
if (!Iris.enabled) return;
final float[] xyzuv = vertexScratch;
if (xyzuv == null || xyzuv.length < VERTEX_COUNT * FLOATS_PER_VERTEX) return;
// Build the Tessellator layout
if (rawData == null) rawData = new int[VERTEX_COUNT * RAW_INTS_PER_VERTEX];
PassRebindGate.rebindIfDirty();
if (!(Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline pipeline)) return;
if (GLStateManager.getActiveProgram() != pipeline.getActivePassProgramId()) return;
if (Tracy.ENABLED) Tracy.beginZone(Z_PLAYER_REFLECTION_FLUSH);
try {
flushInner(xyzuv);
} finally {
if (Tracy.ENABLED) Tracy.endZone();
}
}
private static void flushInner(float[] xyzuv) {
if (rawData == null) {
rawData = new int[VERTEX_COUNT * RAW_INTS_PER_VERTEX];
for (int i = 0; i < VERTEX_COUNT; i++) {
final int d = i * RAW_INTS_PER_VERTEX;
rawData[d + 5] = 0xFFFFFFFF; // color
rawData[d + 6] = 0x00008000; // normal
rawData[d + 7] = 0x00F000F0; // light
}
}
final int[] raw = rawData;
for (int i = 0; i < VERTEX_COUNT; i++) {
final int s = i * FLOATS_PER_VERTEX;
@@ -106,12 +241,8 @@ public final class PlayerReflectionCapture {
raw[d + 2] = Float.floatToRawIntBits(xyzuv[s + 2]);
raw[d + 3] = Float.floatToRawIntBits(xyzuv[s + 3]);
raw[d + 4] = Float.floatToRawIntBits(xyzuv[s + 4]);
raw[d + 5] = 0xFFFFFFFF; // color
raw[d + 6] = 0x00008000; // normal
raw[d + 7] = 0x00F000F0; // light
}
// Pack into our own native buffer using the player model's vertex format
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[VERTEX_FLAGS];
ensureRepackCapacity(VERTEX_COUNT * format.getVertexSize());
@@ -119,25 +250,33 @@ public final class PlayerReflectionCapture {
repackBuffer.position(0);
repackBuffer.limit((int) (writePtr - repackAddress));
// Upload to our dedicated VBO at offset 0
ensureVAO(format);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, repackBuffer, GL15.GL_STREAM_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
// Bind a 64x64 copy of the skin as `tex`
final int activeSkin = GLStateManager.getBoundTextureForServerState();
final boolean haveSkin = activeSkin != 0 && activeSkin != skin64Tex;
if (haveSkin) {
ensureSkin64Texture();
final int prevUnit = GL13.GL_TEXTURE0 + GLStateManager.getActiveTextureUnitForServerState();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final int prevTexture = GLStateManager.getBoundTextureForServerState();
if (pendingSkin > 0 && pendingSkin != skin64Tex) {
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(pendingSkin);
final int skinGeneration = info.getUploadGeneration();
final boolean skinChanged = info != lastCapturedInfo || skinGeneration != lastCapturedGeneration;
final GlImage atlas = GlImage.BY_NAME.get("playerAtlas_img");
final int atlasId = atlas != null ? atlas.getId() : 0;
final boolean skinChanged = activeSkin != lastCapturedSkin;
final TextureInfo atlasInfo = atlasId > 0 ? TextureInfoCache.INSTANCE.getInfo(atlasId) : null;
final int atlasGeneration = atlasInfo != null ? atlasInfo.getUploadGeneration() : 0;
final boolean atlasChanged = atlasInfo != lastUploadedAtlasInfo || atlasGeneration != lastUploadedAtlasGeneration;
if (skinChanged) {
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(activeSkin);
lastCapturedInfo = info;
lastCapturedGeneration = skinGeneration;
skinCopyWidth = 0;
skinCopyHeight = 0;
final int srcW = info.getWidth();
final int srcH = info.getHeight();
if (srcW > 0 && srcH > 0) {
@@ -145,48 +284,72 @@ public final class PlayerReflectionCapture {
if (skinPixels == null || skinPixels.capacity() < required) {
skinPixels = ByteBuffer.allocateDirect(required).order(ByteOrder.nativeOrder());
}
// The 1.7.10 model UVs only address the top 64x32 region.
skinCopyWidth = Math.min(64, srcW);
skinCopyHeight = Math.min(32, srcH);
skinPixels.clear();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, activeSkin);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, pendingSkin);
GLStateManager.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
lastCapturedSkin = activeSkin;
skinCopyWidth = srcW;
skinCopyHeight = srcH;
}
}
if (skinPixels != null && (skinChanged || atlasId != lastUploadedAtlas)) {
skinPixels.rewind();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, skinCopyWidth, skinCopyHeight, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
if (skinCopyWidth > 0 && (skinChanged || atlasChanged)) {
ensureSkin64Texture(Math.max(skinCopyWidth, skinCopyHeight));
if (atlas != null) {
final int prevRowLength = GLStateManager.glGetInteger(GL11.GL_UNPACK_ROW_LENGTH);
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, skinCopyWidth);
try {
skinPixels.rewind();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, atlasId);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, skinCopyWidth, skinCopyHeight, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
skin64Valid = true;
if (atlasInfo != null) {
final int dstW = Math.min(atlasInfo.getWidth(), skinCopyWidth);
final int dstH = Math.min(atlasInfo.getHeight(), skinCopyHeight);
if (dstW > 0 && dstH > 0) {
skinPixels.rewind();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, atlasId);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, dstW, dstH, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
}
}
} finally {
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, prevRowLength);
}
lastUploadedAtlas = atlasId;
lastUploadedAtlasInfo = atlasInfo;
lastUploadedAtlasGeneration = atlasInfo != null ? atlasInfo.getUploadGeneration() : 0;
}
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
if (skin64Valid) GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
}
final boolean maskable = !DepthColorStorage.isDepthColorLocked();
final ColorMask prevColor = GLStateManager.getColorMask();
final boolean pr = prevColor.red, pg = prevColor.green, pb = prevColor.blue, pa = prevColor.alpha;
final boolean prevDepthWrite = GLStateManager.getDepthState().isEnabled();
GLStateManager.glColorMask(false, false, false, false);
GLStateManager.glDepthMask(false);
QuadConverter.drawQuadsAsTriangles(0, VERTEX_COUNT);
GLStateManager.glDepthMask(prevDepthWrite);
GLStateManager.glColorMask(pr, pg, pb, pa);
final int prevEntityId = CapturedRenderingState.INSTANCE.getCurrentRenderedEntity();
savedModelView.set(GLStateManager.getModelViewMatrix());
if (haveSkin) {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, activeSkin);
CapturedRenderingState.INSTANCE.setCurrentEntity(pendingEntityId);
GLStateManager.setModelViewMatrix(pendingModelView);
if (maskable) {
GLStateManager.glColorMask(false, false, false, false);
GLStateManager.glDepthMask(false);
}
try {
QuadConverter.drawQuadsAsTriangles(0, VERTEX_COUNT);
} finally {
if (maskable) {
GLStateManager.glDepthMask(prevDepthWrite);
GLStateManager.glColorMask(pr, pg, pb, pa);
}
GLStateManager.setModelViewMatrix(savedModelView);
CapturedRenderingState.INSTANCE.setCurrentEntity(prevEntityId);
GLStateManager.glBindVertexArray(0);
GLStateManager.glBindVertexArray(0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, prevTexture);
GLStateManager.glActiveTexture(prevUnit);
}
}
public static float[] vertexScratch() {
@@ -205,16 +368,16 @@ public final class PlayerReflectionCapture {
GLStateManager.glBindVertexArray(0);
}
private static void ensureSkin64Texture() {
if (skin64Tex != 0) return;
final int prev = GLStateManager.getBoundTextureForServerState();
skin64Tex = GLStateManager.glGenTextures();
private static void ensureSkin64Texture(int size) {
if (skin64Tex != 0 && skin64Size == size) return;
if (skin64Tex == 0) skin64Tex = GLStateManager.glGenTextures();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
final ByteBuffer zero = ByteBuffer.allocateDirect(64 * 64 * 4);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 64, 64, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, zero);
final ByteBuffer zero = ByteBuffer.allocateDirect(size * size * 4);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, zero);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, prev);
skin64Size = size;
skin64Valid = false;
}
private static void ensureRepackCapacity(int requiredBytes) {
@@ -78,6 +78,10 @@ public class CeleritasDebugScreenHandler {
if (transferInfo != null) {
lines.add(transferInfo);
}
final String presentInfo = RENDER_BACKEND.getPresentDebugInfo();
if (presentInfo != null) {
lines.add(presentInfo);
}
if (SystemProperties.DEBUG_F3_DETAIL) {
lines.addAll(TesrBatchRenderer.INSTANCE.getDebugDetailStrings());
lines.add(ModelPartBatcher.INSTANCE.getDebugString());
@@ -3,10 +3,10 @@ package jss.notfine.gui;
import com.google.common.collect.ImmutableList;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import jss.notfine.config.NotFineConfig;
import jss.notfine.core.Settings;
import jss.notfine.core.SettingsManager;
import me.jellysquid.mods.sodium.client.gui.FrameRateOptions;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptionPages;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.options.OptionFlag;
@@ -43,6 +43,7 @@ public class NotFineGameOptionPages {
public static OptionPage general() {
List<OptionGroup> groups = new ArrayList<>();
final FrameRateOptions frameRate = FrameRateOptions.create(vanillaOpts, sodiumOpts);
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(GraphicsMode.class, vanillaOpts)
.setName(I18n.format("options.graphics"))
@@ -63,16 +64,7 @@ public class NotFineGameOptionPages {
.setImpact(OptionImpact.HIGH)
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
.build())
.add(OptionImpl.createBuilder(boolean.class, vanillaOpts)
.setName(I18n.format("options.vsync"))
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> {
opts.enableVsync = value;
GLStateManager.setVSyncEnabled(opts.enableVsync);
}, opts -> opts.enableVsync)
.setImpact(OptionImpact.VARIES)
.build())
.add(frameRate.vsync())
.add(OptionImpl.createBuilder(boolean.class, vanillaOpts)
.setName(I18n.format("options.fullscreen"))
.setTooltip(I18n.format("sodium.options.fullscreen.tooltip"))
@@ -90,12 +82,7 @@ public class NotFineGameOptionPages {
.build());
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.framerateLimit"))
.setTooltip(I18n.format("sodium.options.fps_limit.tooltip"))
.setControl(option -> new SliderControl(option, 5, 260, 1, ControlValueFormatter.fpsLimit()))
.setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate)
.build())
.add(frameRate.maxFramerate())
.build());
final OptionImpl<GameSettings, Integer> mipmapLevels = OptionImpl.createBuilder(int.class, vanillaOpts)
@@ -7,19 +7,20 @@ import net.minecraft.client.Minecraft;
public class CyclingControlElement<T extends Enum<T>> extends NotFineControlElement<T> {
private final T[] allowedValues;
private final String[] names;
private int currentIndex = 0;
public CyclingControlElement(Option<T> option, Dim2i dim, T[] allowedValues, String[] names) {
super(option, dim);
this.allowedValues = allowedValues;
this.names = names;
}
private int currentIndex() {
for(int i = 0; i < allowedValues.length; ++i) {
if(allowedValues[i] == option.getValue()) {
currentIndex = i;
break;
return i;
}
}
return 0;
}
@Override
@@ -31,8 +32,7 @@ public class CyclingControlElement<T extends Enum<T>> extends NotFineControlElem
@Override
public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) {
if(super.mousePressed(mc, mouseX, mouseY)) {
currentIndex = (currentIndex + 1) % allowedValues.length;
option.setValue(allowedValues[currentIndex]);
option.setValue(allowedValues[(currentIndex() + 1) % allowedValues.length]);
onOptionValueChanged();
return true;
} else {
@@ -0,0 +1,52 @@
package me.jellysquid.mods.sodium.client.gui;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import me.jellysquid.mods.sodium.client.gui.options.Option;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpact;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpl;
import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter;
import me.jellysquid.mods.sodium.client.gui.options.control.CyclingControl;
import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl;
import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage;
import me.jellysquid.mods.sodium.client.gui.options.storage.SodiumOptionsStorage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.I18n;
import net.minecraft.client.settings.GameSettings;
public record FrameRateOptions(Option<VSyncMode> vsync, Option<Integer> maxFramerate) {
public static FrameRateOptions create(MinecraftOptionsStorage vanillaOpts, SodiumOptionsStorage sodiumOpts) {
final OptionImpl<SodiumGameOptions, VSyncMode> vsync = OptionImpl.createBuilder(VSyncMode.class, sodiumOpts)
.setName(I18n.format("options.vsync"))
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
.setControl(option -> new CyclingControl<>(option, VSyncMode.class,
GLStateManager.getSelectableVSyncModes().toArray(new VSyncMode[0]),
new String[]{
I18n.format("sodium.options.vsync_mode.auto"),
I18n.format("sodium.options.vsync_mode.on"),
I18n.format("sodium.options.vsync_mode.mailbox"),
I18n.format("sodium.options.vsync_mode.off")}))
.setBinding((opts, value) -> {
opts.advanced.vsyncMode = value;
final GameSettings settings = Minecraft.getMinecraft().gameSettings;
settings.enableVsync = value.tearFree();
settings.saveOptions();
GLStateManager.setVSyncMode(value, settings.enableVsync);
}, opts -> opts.advanced.vsyncMode.resolve(Minecraft.getMinecraft().gameSettings.enableVsync))
.setImpact(OptionImpact.VARIES)
.build();
final OptionImpl<GameSettings, Integer> maxFramerate = OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.framerateLimit"))
.setTooltip(I18n.format("sodium.options.fps_limit.tooltip"))
.setControl(option -> new SliderControl(option, 5, 260, 5, ControlValueFormatter.fpsLimit()))
.setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate)
.build();
maxFramerate.iris$dynamicallyEnable(() -> !vsync.getValue().blocksOnVBlank());
return new FrameRateOptions(vsync, maxFramerate);
}
}
@@ -105,6 +105,7 @@ public class SodiumGameOptionPages {
groups.add(firstGroupBuilder.build());
int maxGuiScale = Math.max(3, Math.min(Minecraft.getMinecraft().displayWidth / 320, Minecraft.getMinecraft().displayHeight / 240));
final FrameRateOptions frameRate = FrameRateOptions.create(vanillaOpts, sodiumOpts);
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.guiScale"))
@@ -138,22 +139,8 @@ public class SodiumGameOptionPages {
}
}, (opts) -> opts.fullScreen)
.build())
.add(OptionImpl.createBuilder(boolean.class, vanillaOpts)
.setName(I18n.format("options.vsync"))
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> {
opts.enableVsync = value;
GLStateManager.setVSyncEnabled(opts.enableVsync);
}, opts -> opts.enableVsync)
.setImpact(OptionImpact.VARIES)
.build())
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.framerateLimit"))
.setTooltip(I18n.format("sodium.options.fps_limit.tooltip"))
.setControl(option -> new SliderControl(option, 5, 260, 5, ControlValueFormatter.fpsLimit()))
.setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate)
.build())
.add(frameRate.vsync())
.add(frameRate.maxFramerate())
.build());
groups.add(OptionGroup.createBuilder()
@@ -521,9 +508,12 @@ public class SodiumGameOptionPages {
.add(OptionImpl.createBuilder(int.class, sodiumOpts)
.setName(I18n.format("sodium.options.cpu_render_ahead_limit.name"))
.setTooltip(I18n.format("sodium.options.cpu_render_ahead_limit.tooltip"))
.setControl(o -> new SliderControl(o, 0, 9, 1, ControlValueFormatter.quantity("sodium.options.cpu_render_ahead_limit.value")))
.setControl(o -> new SliderControl(o, GLStateManager.getMinRenderAhead(), GLStateManager.getMaxRenderAhead(), 1, ControlValueFormatter.quantity("sodium.options.cpu_render_ahead_limit.value")))
.setImpact(OptionImpact.MEDIUM)
.setBinding((opts, value) -> opts.performance.cpuRenderAheadLimit = value, opts -> opts.performance.cpuRenderAheadLimit)
.setBinding((opts, value) -> {
opts.performance.cpuRenderAheadLimit = value;
GLStateManager.applyRenderAheadLimit(value);
}, opts -> GLStateManager.renderAheadLimit(opts.performance.cpuRenderAheadLimit))
.setEnabled(GLStateManager.capabilities != null && GLStateManager.capabilities.OpenGL32)
.build())
@@ -6,6 +6,7 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality;
@@ -203,6 +204,7 @@ public class SodiumGameOptions {
public boolean ignoreDriverBlacklist = false;
public StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.MAP_BUFFER_RANGE;
public boolean enableDeferredBatching = true;
public VSyncMode vsyncMode = VSyncMode.AUTO;
}
public static class PerformanceSettings {
@@ -27,6 +27,17 @@ public class CyclingControl<T extends Enum<T>> implements Control<T> {
this.names = names;
}
public CyclingControl(Option<T> option, Class<T> enumType, T[] allowedValues, String[] names) {
final T[] universe = enumType.getEnumConstants();
Validate.isTrue(universe.length == names.length, "Mismatch between universe length and names array length");
Validate.notEmpty(allowedValues, "There must be at least one allowed value");
this.option = option;
this.allowedValues = allowedValues;
this.names = names;
}
public CyclingControl(Option<T> option, Class<T> enumType, T[] allowedValues) {
final T[] universe = enumType.getEnumConstants();
@@ -27,21 +27,21 @@ public class SodiumControlElementFactory implements ControlElementFactory {
private static class CyclingControlElement<T extends Enum<T>> extends SodiumControlElement<T> {
private final T[] allowedValues;
private final String[] names;
private int currentIndex;
public CyclingControlElement(Option<T> option, Dim2i dim, T[] allowedValues, String[] names) {
super(option, dim);
this.allowedValues = allowedValues;
this.names = names;
this.currentIndex = 0;
}
for (int i = 0; i < allowedValues.length; i++) {
if (allowedValues[i] == option.getValue()) {
this.currentIndex = i;
break;
private int currentIndex() {
for (int i = 0; i < this.allowedValues.length; i++) {
if (this.allowedValues[i] == this.option.getValue()) {
return i;
}
}
return 0;
}
@Override
@@ -59,16 +59,14 @@ public class SodiumControlElementFactory implements ControlElementFactory {
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if(AngelicaConfig.enableReesesSodiumOptions) {
if (this.option.isAvailable() && this.dim.containsCursor(mouseX, mouseY) && (button == 0 || button == 1)) {
this.currentIndex = Math.floorMod(this.currentIndex + (button == 0 ? 1 : -1), this.allowedValues.length);
this.option.setValue(this.allowedValues[this.currentIndex]);
this.option.setValue(this.allowedValues[Math.floorMod(this.currentIndex() + (button == 0 ? 1 : -1), this.allowedValues.length)]);
this.playClickSound();
return true;
}
} else {
if (this.option.isAvailable() && button == 0 && this.dim.containsCursor(mouseX, mouseY)) {
this.currentIndex = (this.currentIndex + 1) % this.allowedValues.length;
this.option.setValue(this.allowedValues[this.currentIndex]);
this.option.setValue(this.allowedValues[(this.currentIndex() + 1) % this.allowedValues.length]);
this.playClickSound();
return true;
@@ -547,11 +547,11 @@ public class ShadowRenderer {
profiler.endStartSection("iris_shadow_build_geometry");
setupEntityShadowState(modelView, cameraX, cameraY, cameraZ);
if (playerReflectionCaptureEnabled) {
PlayerReflectionCapture.begin(player);
}
ModelPartBatcher.INSTANCE.begin(ModelPartBatcher.Mode.ENTITIES, true);
try {
if (playerReflectionCaptureEnabled) {
PlayerReflectionCapture.begin(player);
}
for (Entity entity : renderedEntitiesList) {
final long start = Tracy.ENABLED ? System.nanoTime() : 0L;
try {
@@ -629,7 +629,7 @@ public class ShadowRenderer {
// Saved RenderManager position for shadow pass
private double savedRenderPosX, savedRenderPosY, savedRenderPosZ;
/** Flushes shadow-pass batched model parts while GL_POLYGON_OFFSET_FILL is still enabled. */
/** Flushes deferred shadow-pass geometry while GL_POLYGON_OFFSET_FILL is still enabled. */
private static void flushShadowModelParts() {
if (Tracy.ENABLED) Tracy.beginZone(Z_SHADOW_MODEL_PARTS);
final boolean alphaEnabled = GLStateManager.getAlphaTest().isEnabled();
@@ -637,6 +637,7 @@ public class ShadowRenderer {
final float alphaRef = GLStateManager.getAlphaState().getReference();
try {
ModelPartBatcher.INSTANCE.flush();
PlayerReflectionCapture.flush();
} finally {
if (alphaEnabled) GLStateManager.enableAlphaTest();
else GLStateManager.disableAlphaTest();
@@ -712,14 +713,11 @@ public class ShadowRenderer {
if (playerReflectionCaptureEnabled) {
PlayerReflectionCapture.begin(player);
}
try {
RenderManager.instance.renderEntitySimple(player, tickDelta);
} finally {
PlayerReflectionCapture.end();
}
RenderManager.instance.renderEntitySimple(player, tickDelta);
shadowEntities++;
} finally {
flushShadowModelParts();
PlayerReflectionCapture.end();
teardownEntityShadowState();
}
@@ -24,8 +24,8 @@ sodium.options.clouds.name=Clouds
sodium.options.clouds.tooltip=Controls whether or not clouds will be visible.
sodium.options.gui_scale.tooltip=Sets the maximum scale factor to be used for the user interface. If 'auto' is used, then the largest scale factor will always be used.
sodium.options.fullscreen.tooltip=If enabled, the game will display in full-screen (if supported).
sodium.options.v_sync.tooltip=If enabled, the game's frame rate will be synchronized to the monitor's refresh rate, making for a generally smoother experience at the expense of overall input latency. This setting might reduce performance if your system is too slow.
sodium.options.fps_limit.tooltip=Limits the maximum FPS (frames per second). This can help reduce battery usage and general system load when multi-tasking. If V-Sync is enabled, this option will be ignored unless it is lower than your display's refresh rate.
sodium.options.v_sync.tooltip=Synchronizes frames to your monitor to stop tearing. §lOff§r shows each frame the moment it is ready: lowest latency, but the image can tear, and Max Framerate applies. §lOn§r waits for the monitor, which removes tearing and hands it the frame rate, so §lMax Framerate has no effect§r. §lMailbox§r removes tearing without waiting, keeping most of the latency of Off, and Max Framerate still applies. Mailbox needs the SDL-GPU backend on Vulkan or D3D12.
sodium.options.fps_limit.tooltip=Limits the maximum FPS (frames per second). This can help reduce battery usage and general system load when multi-tasking. §lHas no effect while VSync is On§r, because the monitor sets the frame rate instead; use Mailbox to combine a limit with tear-free output.
sodium.options.view_bobbing.tooltip=If enabled, the player's view will sway and bob when moving around. Players who experience motion sickness while playing can benefit from disabling this.
sodium.options.attack_indicator.tooltip=Controls where the Attack Indicator is displayed on screen.
sodium.options.autosave_indicator.tooltip=If enabled, an indicator will be shown when the game is saving the world to disk.
@@ -75,7 +75,7 @@ sodium.options.animate_only_visible_textures.tooltip=If enabled, only animated t
sodium.options.translucency_sorting.name=Translucency Sorting
sodium.options.translucency_sorting.tooltip=If enabled, translucent blocks will be sorted correctly from back to front to provide better rendering, especially when multiple translucent blocks are in view and layered. This is a bit of an expensive operation.
sodium.options.cpu_render_ahead_limit.name=CPU Render-Ahead Limit
sodium.options.cpu_render_ahead_limit.tooltip=Specifies the maximum number of frames the CPU can be waiting on the GPU to finish rendering. Very low or high values may create frame rate instability.
sodium.options.cpu_render_ahead_limit.tooltip=How many frames the CPU may run ahead of the GPU. Lower values cut input latency, higher values smooth out uneven frame times. Very low or high values may create frame rate instability. The SDL-GPU backend limits this to 3.
sodium.options.cpu_render_ahead_limit.value=%s frame(s)
sodium.options.allow_direct_memory_access.name=Allow Direct Memory Access
sodium.options.allow_direct_memory_access.tooltip=If enabled, some critical code paths will be allowed to use direct memory access for performance. This often greatly reduces CPU overhead for chunk and entity rendering, but can make it harder to diagnose some bugs and crashes. You should only disable this if you've been asked to or otherwise know what you're doing.
@@ -261,6 +261,10 @@ options.angelica.texteffects.drop_shadow.tooltip=Per-segment drop shadow via %s,
options.angelica.texteffects.ampersand_conversion=Ampersand Conversion
options.angelica.texteffects.ampersand_conversion.tooltip=Convert %s prefix codes to formatting codes at render time
sodium.options.vsync_mode.auto=Auto
sodium.options.vsync_mode.on=On
sodium.options.vsync_mode.mailbox=Mailbox
sodium.options.vsync_mode.off=Off
sodium.options.multidraw_mode.name=Multi-Draw Mode
sodium.options.multidraw_mode.tooltip=Controls how draw commands are dispatched for terrain rendering. §lDirect§r uses glMultiDrawElementsBaseVertex (default, GL 3.2+). §lIndirect§r uses GPU indirect rendering (GL 4.3+), may reduce CPU overhead on some hardware, and is required for GPU / Hi-Z culling. §lIndividual§r issues individual draw calls.
sodium.options.multidraw_mode.direct=Direct
@@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.GameSettings;
@@ -36,12 +37,6 @@ public abstract class MixinMinecraft {
@Shadow(remap = false)
private static int max_texture_size;
@Unique
private static long angelica$lastFrameTime = 0;
@Unique
private static long angelica$fpsLimitOverhead = 0;
@Unique
private final RenderAheadManager celeritas$renderAheadManager = new RenderAheadManager();
@@ -66,48 +61,23 @@ public abstract class MixinMinecraft {
GLStateManager.glEnable(GL11.GL_LIGHTING);
}
@Inject(
method = "func_147120_f",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;update()V", remap = false)
)
@Inject(method = "func_147120_f", at = @At("RETURN"))
private void angelica$limitFPS(CallbackInfo ci) {
if (AngelicaMod.proxy == null) return;
if (isFramerateLimitBelowMax() && !gameSettings.enableVsync) {
final long time = System.nanoTime();
final long lastWorkTime = time - angelica$lastFrameTime;
final long targetNanos = (long) (1.0 / getLimitFramerate() * 1_000_000_000L);
// Account for overhead, so the average FPS remains stable.
final long sleepNanos = targetNanos - lastWorkTime - angelica$fpsLimitOverhead;
if (sleepNanos > 0) {
try {
Thread.sleep(sleepNanos / 1_000_000, (int) sleepNanos % 1_000_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// Record overhead, capping it to prevent outsized spikes from affecting framerates for too long.
// In testing, spikes were followed by 1-frame dips, which I think is acceptable.
long overhead = System.nanoTime() - time - sleepNanos;
if (overhead < 0 || overhead > targetNanos / 2) overhead = 0;
angelica$fpsLimitOverhead = overhead;
}
final long time = System.nanoTime();
AngelicaMod.proxy.putFrametime(time - angelica$lastFrameTime);
angelica$lastFrameTime = time;
final int capHz = isFramerateLimitBelowMax() ? getLimitFramerate() : 0;
AngelicaMod.proxy.putFrametime(FramePacer.pace(FramePacer.updateCeiling(capHz)));
}
@Redirect(method = {"startGame", "toggleFullscreen"}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setVSyncEnabled(Z)V", remap = false))
private void angelica$redirectVSync(boolean sync) {
GLStateManager.setVSyncEnabled(sync);
ClientProxy.applyVSyncMode(sync);
}
@Inject(method = "startGame", at = @At("RETURN"))
private void angelica$markSplashCompleteOnStartGame(CallbackInfo ci) {
GLStateManager.markSplashComplete("startGame");
GLStateManager.applyRenderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit);
}
@WrapWithCondition(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;sync(I)V", remap = false))
@@ -122,7 +92,7 @@ public abstract class MixinMinecraft {
@Inject(method = "runTick", at = @At("HEAD"))
private void celeritas$renderAheadStartFrame(CallbackInfo ci) {
final int limit = ClientProxy.options().performance.cpuRenderAheadLimit;
final int limit = GLStateManager.renderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit);
if (limit > 0) {
celeritas$renderAheadManager.startFrame(limit);
}
@@ -130,7 +100,7 @@ public abstract class MixinMinecraft {
@Inject(method = "runTick", at = @At("RETURN"))
private void celeritas$renderAheadEndFrame(CallbackInfo ci) {
if (ClientProxy.options().performance.cpuRenderAheadLimit > 0) {
if (GLStateManager.renderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit) > 0) {
celeritas$renderAheadManager.endFrame();
}
}
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.debug;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.GameSettings;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
@@ -15,19 +15,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class MixinMinecraft_FPSCap {
@Shadow public String debug;
@Shadow public GameSettings gameSettings;
@Inject(method = "runGameLoop", at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;debug:Ljava/lang/String;", shift = At.Shift.AFTER, ordinal = 0))
private void angelica$appendFPSCapInfo(CallbackInfo ci) {
final int limit = this.gameSettings.limitFramerate;
String indicator = null;
if (this.gameSettings.enableVsync) {
indicator = " [vsync]";
} else if (limit < 260) {
indicator = " [cap " + limit + "]";
}
if (indicator != null) {
this.debug = this.debug.replace(" fps,", " fps" + indicator + ",");
}
final String indicator = FramePacer.debugIndicator();
if (indicator == null) return;
this.debug = this.debug.replace(" fps,", " fps" + indicator + ",");
}
}
@@ -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,13 +1,15 @@
package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.gtnewhorizons.angelica.mixins.interfaces.ModelBoxQuads;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.TexturedQuad;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ModelBox.class)
public interface AccessorModelBox {
public interface AccessorModelBox extends ModelBoxQuads {
@Override
@Accessor("quadList")
TexturedQuad[] angelica$getQuadList();
}
@@ -3,29 +3,18 @@ package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.rendering.PlayerReflectionCapture;
import net.minecraft.client.model.ModelBiped;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.model.PositionTextureVertex;
import net.minecraft.client.model.TexturedQuad;
import net.minecraft.entity.Entity;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.Unique;
import java.util.Arrays;
@Mixin(ModelBiped.class)
public abstract class MixinModelBiped {
@Unique private static final Tracy.ZoneId Z_PLAYER_REFLECTION_CAPTURE = Tracy.zoneId("playerReflectionCapture", Tracy.COLOR_IRIS);
@Unique private static final Matrix4f angelica$mat = new Matrix4f();
@Unique private static final Vector3f angelica$vec = new Vector3f();
@Inject(method = "render(Lnet/minecraft/entity/Entity;FFFFFF)V", at = @At("RETURN"))
private void angelica$capturePlayerReflection(Entity entity, float limbSwing, float limbSwingAmount,
float ageInTicks, float netHeadYaw, float headPitch, float scale, CallbackInfo ci) {
@@ -34,65 +23,11 @@ public abstract class MixinModelBiped {
if ((Object) this != PlayerReflectionCapture.getTarget()) return;
if (entity != PlayerReflectionCapture.getTargetEntity()) return;
final ModelBiped model = (ModelBiped) (Object) this;
final float[] data = PlayerReflectionCapture.vertexScratch();
if (Tracy.ENABLED) Tracy.beginZone(Z_PLAYER_REFLECTION_CAPTURE);
try {
// Six 48-vertex blocks = body parts, each base box + overlay box
int o = 0;
o = angelica$emitBox(model.bipedHead, data, o, scale);
o = angelica$emitBox(model.bipedHeadwear, data, o, scale);
o = angelica$emitBox(model.bipedRightArm, data, o, scale);
o = angelica$emitBox(model.bipedRightArm, data, o, scale);
o = angelica$emitBox(model.bipedLeftLeg, data, o, scale);
o = angelica$emitBox(model.bipedLeftLeg, data, o, scale);
o = angelica$emitBox(model.bipedLeftArm, data, o, scale);
o = angelica$emitBox(model.bipedLeftArm, data, o, scale);
o = angelica$emitBox(model.bipedRightLeg, data, o, scale);
o = angelica$emitBox(model.bipedRightLeg, data, o, scale);
o = angelica$emitBox(model.bipedBody, data, o, scale);
angelica$emitBox(model.bipedBody, data, o, scale);
PlayerReflectionCapture.drawPlayerCapture(data);
PlayerReflectionCapture.emitAndSubmit((ModelBiped) (Object) this, scale);
} finally {
if (Tracy.ENABLED) Tracy.endZone();
}
}
private static int angelica$emitBox(ModelRenderer part, float[] out, int o, float scale) {
if (part == null || part.cubeList == null || part.cubeList.isEmpty()) {
final int end = o + 24 * PlayerReflectionCapture.FLOATS_PER_VERTEX;
Arrays.fill(out, o, end, 0.0f);
return end;
}
final Matrix4f m = angelica$mat.identity();
m.translate(part.offsetX, part.offsetY, part.offsetZ);
m.translate(part.rotationPointX * scale, part.rotationPointY * scale, part.rotationPointZ * scale);
if (part.rotateAngleZ != 0.0f) m.rotateZ(part.rotateAngleZ);
if (part.rotateAngleY != 0.0f) m.rotateY(part.rotateAngleY);
if (part.rotateAngleX != 0.0f) m.rotateX(part.rotateAngleX);
final ModelBox box = part.cubeList.getFirst();
final TexturedQuad[] quads = ((AccessorModelBox) (Object) box).angelica$getQuadList();
final Vector3f v = angelica$vec;
for (int q = 0; q < 6; q++) {
final PositionTextureVertex[] corners = quads[q].vertexPositions;
for (int c = 0; c < 4; c++) {
final PositionTextureVertex corner = corners[c];
v.set((float) corner.vector3D.xCoord * scale,
(float) corner.vector3D.yCoord * scale,
(float) corner.vector3D.zCoord * scale);
m.transformPosition(v);
out[o++] = v.x;
out[o++] = v.y;
out[o++] = v.z;
out[o++] = corner.texturePositionX;
out[o++] = corner.texturePositionY * 0.5f;
}
}
return o;
}
}
@@ -0,0 +1,97 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class FramePacerTest {
@Test
void vsyncNeverPaces() {
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 0, 0));
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 60, 144));
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.ON, 260, 60));
}
@Test
void vsyncOffHonorsOnlyTheCap() {
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.OFF, 215, 0));
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.OFF, 215, 144));
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.OFF, 0, 144));
}
@Test
void mailboxTakesTheLowerOfCapAndRefresh() {
assertEquals(60, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 60, 143));
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 143));
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 143, 143));
}
@Test
void mailboxTreatsZeroAsNoLimitFromThatSource() {
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 143));
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 0));
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 0));
}
@Test
void nonPositiveCapIsUncapped() {
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.OFF, -1, 0));
assertEquals(143, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, -1, 143));
}
@Test
void implausibleRefreshIsIgnored() {
assertEquals(0, FramePacer.plausibleRefreshHz(0));
assertEquals(0, FramePacer.plausibleRefreshHz(-1));
assertEquals(0, FramePacer.plausibleRefreshHz(19));
assertEquals(0, FramePacer.plausibleRefreshHz(1001));
assertEquals(20, FramePacer.plausibleRefreshHz(20));
assertEquals(1000, FramePacer.plausibleRefreshHz(1000));
}
@Test
void implausibleRefreshLeavesMailboxOnTheCapAlone() {
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 19));
assertEquals(215, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 215, 1001));
assertEquals(0, FramePacer.pacingCeilingHz(VSyncMode.MAILBOX, 0, 1001));
}
@Test
void sleepDeductsWorkAndOverhead() {
assertEquals(10_000L, FramePacer.sleepNanosFor(16_000L, 5_000L, 1_000L));
}
@Test
void anOverrunAsksForNoSleep() {
assertEquals(-5_000L, FramePacer.sleepNanosFor(16_000L, 21_000L, 0L));
}
@Test
void indicatorNamesWhatIsHoldingTheRateDown() {
assertEquals(" [vsync 144]", FramePacer.debugIndicator(VSyncMode.ON, 144, 0));
assertEquals(" [mailbox 144]", FramePacer.debugIndicator(VSyncMode.MAILBOX, 144, 0));
assertEquals(" [mailbox 144, cap 60]", FramePacer.debugIndicator(VSyncMode.MAILBOX, 144, 60));
assertEquals(" [cap 60]", FramePacer.debugIndicator(VSyncMode.OFF, 144, 60));
}
@Test
void indicatorOmitsACapThatDoesNothing() {
assertEquals(" [vsync 144]", FramePacer.debugIndicator(VSyncMode.ON, 144, 60));
assertEquals(" [vsync]", FramePacer.debugIndicator(VSyncMode.ON, 0, 60));
}
@Test
void indicatorStillReportsVsyncWithoutARefreshRate() {
assertEquals(" [vsync]", FramePacer.debugIndicator(VSyncMode.ON, 0, 0));
assertEquals(" [mailbox, cap 60]", FramePacer.debugIndicator(VSyncMode.MAILBOX, 0, 60));
}
@Test
void indicatorIsAbsentWhenNothingLimits() {
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 144, 0));
assertNull(FramePacer.debugIndicator(VSyncMode.OFF, 0, 0));
}
}
@@ -0,0 +1,180 @@
package com.gtnewhorizons.angelica.rendering;
import net.minecraft.client.model.ModelBiped;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.model.TexturedQuad;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PlayerReflectionLayoutTest {
static final int FLOATS = PlayerReflectionCapture.FLOATS_PER_VERTEX;
static final int PART_VERTS = PlayerReflectionCapture.VERTICES_PER_PART;
static final float MODEL_SCALE = 0.0625f;
private static Field quadListField;
static TexturedQuad[] quadsOf(ModelRenderer part) {
if (part == null || part.cubeList == null || part.cubeList.isEmpty()) return null;
try {
if (quadListField == null) {
quadListField = ModelBox.class.getDeclaredField("quadList");
quadListField.setAccessible(true);
}
return (TexturedQuad[]) quadListField.get(part.cubeList.getFirst());
} catch (ReflectiveOperationException e) {
throw new AssertionError("ModelBox.quadList is no longer reachable", e);
}
}
static float[] emit(ModelBiped model, float scale) {
final float[] out = new float[PlayerReflectionCapture.VERTEX_COUNT * FLOATS];
final ModelRenderer[] parts = PlayerReflectionCapture.partOrder(model, new ModelRenderer[PlayerReflectionCapture.PART_COUNT]);
int o = 0;
for (ModelRenderer part : parts) {
o = PlayerReflectionCapture.emitBox(part, quadsOf(part), out, o, scale);
}
assertEquals(out.length, o, "emit did not fill exactly 288 vertices");
return out;
}
private static float min(float[] v, int block, int component) {
float m = Float.POSITIVE_INFINITY;
for (int i = 0; i < PART_VERTS; i++) m = Math.min(m, v[(block * PART_VERTS + i) * FLOATS + component]);
return m;
}
private static float max(float[] v, int block, int component) {
float m = Float.NEGATIVE_INFINITY;
for (int i = 0; i < PART_VERTS; i++) m = Math.max(m, v[(block * PART_VERTS + i) * FLOATS + component]);
return m;
}
private static void assertTexelRange(float[] v, int block, int u0, int u1, int v0, int v1, String part) {
assertEquals(u0, Math.round(min(v, block, 3) * 64f), part + ": u start");
assertEquals(u1, Math.round(max(v, block, 3) * 64f), part + ": u end");
assertEquals(v0, Math.round(min(v, block, 4) * 64f), part + ": v start");
assertEquals(v1, Math.round(max(v, block, 4) * 64f), part + ": v end");
}
@Test
void partOrderMatchesTheShaderpackVertexIdPartitioning() {
final ModelBiped model = new ModelBiped(0.0f);
final ModelRenderer[] parts = PlayerReflectionCapture.partOrder(model, new ModelRenderer[PlayerReflectionCapture.PART_COUNT]);
assertSame(model.bipedHead, parts[0], "block 0 must be the head");
assertSame(model.bipedHeadwear, parts[1], "block 1 must stand in for the hat overlay");
assertSame(model.bipedRightArm, parts[2], "block 2 must be the right arm");
assertSame(model.bipedRightArm, parts[3], "block 3 must stand in for the right sleeve");
assertSame(model.bipedLeftLeg, parts[4], "block 4 must be the left leg");
assertSame(model.bipedLeftLeg, parts[5], "block 5 must stand in for the left trouser leg");
assertSame(model.bipedLeftArm, parts[6], "block 6 must be the left arm");
assertSame(model.bipedLeftArm, parts[7], "block 7 must stand in for the left sleeve");
assertSame(model.bipedRightLeg, parts[8], "block 8 must be the right leg");
assertSame(model.bipedRightLeg, parts[9], "block 9 must stand in for the right trouser leg");
assertSame(model.bipedBody, parts[10], "block 10 must be the body");
assertSame(model.bipedBody, parts[11], "block 11 must stand in for the jacket");
}
@Test
void skinTexelRangesLandWhereTheRayTracerSamples() {
final float[] v = emit(new ModelBiped(0.0f), MODEL_SCALE);
assertTexelRange(v, 0, 0, 32, 0, 16, "head");
assertTexelRange(v, 1, 32, 64, 0, 16, "hat");
assertTexelRange(v, 2, 40, 56, 16, 32, "right arm");
assertTexelRange(v, 4, 0, 16, 16, 32, "left leg");
assertTexelRange(v, 6, 40, 56, 16, 32, "left arm");
assertTexelRange(v, 8, 0, 16, 16, 32, "right leg");
assertTexelRange(v, 10, 16, 40, 16, 32, "body");
}
@Test
void everyQuadIsAParallelogramInV0V1V2Order() {
final float[] v = emit(new ModelBiped(0.0f), MODEL_SCALE);
for (int q = 0; q < PlayerReflectionCapture.VERTEX_COUNT / 4; q++) {
final int b = q * 4 * FLOATS;
final float[] v0 = { v[b], v[b + 1], v[b + 2] };
final float[] v1 = { v[b + FLOATS], v[b + FLOATS + 1], v[b + FLOATS + 2] };
final float[] v2 = { v[b + 2 * FLOATS], v[b + 2 * FLOATS + 1], v[b + 2 * FLOATS + 2] };
final float[] v3 = { v[b + 3 * FLOATS], v[b + 3 * FLOATS + 1], v[b + 3 * FLOATS + 2] };
for (int c = 0; c < 3; c++) {
assertEquals(v0[c] + v2[c] - v1[c], v3[c], 1e-4f, "quad " + q + " component " + c + ": the ray tracer reconstructs the 4th corner as v0 + v2 - v1");
}
float dot = 0.0f;
for (int c = 0; c < 3; c++) dot += (v1[c] - v0[c]) * (v2[c] - v1[c]);
assertEquals(0.0f, dot, 1e-6f, "quad " + q + ": edges v0->v1 and v1->v2 must be perpendicular");
}
}
@Test
void boxExtentsMatchTheVanillaModel() {
final float[] v = emit(new ModelBiped(0.0f), MODEL_SCALE);
assertEquals(-0.25f, min(v, 0, 0), 1e-5f, "head min x");
assertEquals(0.25f, max(v, 0, 0), 1e-5f, "head max x");
assertEquals(-0.5f, min(v, 0, 1), 1e-5f, "head min y");
assertEquals(0.0f, max(v, 0, 1), 1e-5f, "head max y");
assertEquals(-0.25f, min(v, 10, 0), 1e-5f, "body min x");
assertEquals(0.25f, max(v, 10, 0), 1e-5f, "body max x");
assertEquals(0.0f, min(v, 10, 1), 1e-5f, "body min y");
assertEquals(0.75f, max(v, 10, 1), 1e-5f, "body max y");
assertEquals(-0.125f, min(v, 10, 2), 1e-5f, "body min z");
assertEquals(0.125f, max(v, 10, 2), 1e-5f, "body max z");
}
@Test
void hatIsLargerThanTheHeadSoAnOpaqueOverlayWins() {
final float[] v = emit(new ModelBiped(0.0f), MODEL_SCALE);
assertTrue(max(v, 1, 0) > max(v, 0, 0), "the hat box must enclose the head box");
assertEquals(0.5f + 2 * 0.5f * MODEL_SCALE, max(v, 1, 0) - min(v, 1, 0), 1e-5f, "hat width");
}
@Test
void hiddenPartsEmitDegenerateQuads() {
final ModelBiped model = new ModelBiped(0.0f);
model.bipedHeadwear.showModel = false;
model.bipedRightArm.isHidden = true;
final float[] v = emit(model, MODEL_SCALE);
final float[] zeros = new float[PART_VERTS * FLOATS];
assertArrayEquals(zeros, java.util.Arrays.copyOfRange(v, PART_VERTS * FLOATS, 2 * PART_VERTS * FLOATS), "a hidden hat must not be ray-traced");
assertArrayEquals(zeros, java.util.Arrays.copyOfRange(v, 2 * PART_VERTS * FLOATS, 3 * PART_VERTS * FLOATS), "a hidden right arm must not be ray-traced");
}
@Test
void aPartWhoseLayoutOverflowsTheAtlasIsDropped() {
final ModelBiped model = new ModelBiped(0.0f);
model.bipedHead.setTextureSize(128, 128);
final float[] v = emit(model, MODEL_SCALE);
assertArrayEquals(new float[PART_VERTS * FLOATS], java.util.Arrays.copyOfRange(v, 0, PART_VERTS * FLOATS), "a >64 layout cannot be expressed in the 64x64 atlas, so the head block must be degenerate rather than emit UVs > 1");
assertTexelRange(v, 10, 16, 40, 16, 32, "body is unaffected by the head's layout");
}
@Test
void headUvsAreUnaffectedByHeadRotation() {
final ModelBiped model = new ModelBiped(0.0f);
model.bipedHead.rotateAngleY = 0.9f;
model.bipedHead.rotateAngleX = -0.4f;
final float[] v = emit(model, MODEL_SCALE);
assertTexelRange(v, 0, 0, 32, 0, 16, "rotated head");
assertTrue(max(v, 0, 0) - min(v, 0, 0) > 0.5f, "a yawed head must present a wider silhouette than 8 units");
}
}
@@ -0,0 +1,186 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import net.minecraft.client.model.ModelBiped;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL42;
import org.lwjgl.opengl.GL43;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import static com.gtnewhorizons.angelica.rendering.PlayerReflectionLayoutTest.MODEL_SCALE;
import static com.gtnewhorizons.angelica.rendering.PlayerReflectionLayoutTest.emit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@GLCoreTest
class PlayerReflectionSsboMappingTest {
private static final int SSBO_ENTRIES = 216;
private static final int POSITION_STRIDE = 16;
private static final int DATA_OFFSET = SSBO_ENTRIES * POSITION_STRIDE;
private static final int DATA_STRIDE = 8;
private static final int SSBO_BYTES = DATA_OFFSET + SSBO_ENTRIES * DATA_STRIDE;
private static final int BINDING = 3;
private static final int VISITED_BINDING = 4;
private static final int VISITED_BYTES = PlayerReflectionCapture.VERTEX_COUNT * 4;
private static final String VS = "#version 430 core\n"
+ "layout(location = 0) in vec3 a_Pos;\n"
+ "layout(location = 1) in vec2 a_Uv;\n"
+ "layout(std430, binding = " + BINDING + ") buffer playerVerticesBuffer {\n"
+ " vec3 vertexPositions[" + SSBO_ENTRIES + "];\n"
+ " vec2 vertexData[" + SSBO_ENTRIES + "];\n"
+ "} ssbo;\n"
+ "layout(std430, binding = " + VISITED_BINDING + ") buffer visitedBuffer {\n"
+ " uint visited[" + PlayerReflectionCapture.VERTEX_COUNT + "];\n"
+ "};\n"
+ "void main() {\n"
+ " visited[gl_VertexID] = 1u;\n"
+ " if (gl_VertexID % 4 != 3) {\n"
+ " int i = gl_VertexID - gl_VertexID / 4;\n"
+ " ssbo.vertexPositions[i] = a_Pos;\n"
+ " ssbo.vertexData[i] = a_Uv;\n"
+ " }\n"
+ " gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n"
+ "}\n";
private static final String FS = "#version 430 core\n"
+ "out vec4 fragColor;\n"
+ "void main() { fragColor = vec4(1.0); }\n";
private int program;
private int vao;
private int vbo;
private int ssbo;
private int visitedSsbo;
@AfterEach
void cleanup() {
GLStateManager.glUseProgram(0);
GLStateManager.glBindVertexArray(0);
if (program != 0) GLStateManager.glDeleteProgram(program);
if (vbo != 0) GLStateManager.glDeleteBuffers(vbo);
if (ssbo != 0) GLStateManager.glDeleteBuffers(ssbo);
if (visitedSsbo != 0) GLStateManager.glDeleteBuffers(visitedSsbo);
if (vao != 0) GLStateManager.glDeleteVertexArrays(vao);
program = vao = vbo = ssbo = visitedSsbo = 0;
}
private static int compile(int type, String source) {
final int shader = GL20.glCreateShader(type);
GL20.glShaderSource(shader, source);
GL20.glCompileShader(shader);
assertEquals(GL11.GL_TRUE, GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS), "shader failed to compile: " + GL20.glGetShaderInfoLog(shader, 4096));
return shader;
}
private void buildProgram() {
final int vs = compile(GL20.GL_VERTEX_SHADER, VS);
final int fs = compile(GL20.GL_FRAGMENT_SHADER, FS);
program = GL20.glCreateProgram();
GL20.glAttachShader(program, vs);
GL20.glAttachShader(program, fs);
GL20.glLinkProgram(program);
assertEquals(GL11.GL_TRUE, GL20.glGetProgrami(program, GL20.GL_LINK_STATUS), "program failed to link: " + GL20.glGetProgramInfoLog(program, 4096));
GL20.glDeleteShader(vs);
GL20.glDeleteShader(fs);
}
private void uploadVertices(float[] xyzuv) {
final FloatBuffer verts = BufferUtils.createFloatBuffer(xyzuv.length);
verts.put(xyzuv).flip();
vao = GLStateManager.glGenVertexArrays();
vbo = GLStateManager.glGenBuffers();
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, verts, GL15.GL_STATIC_DRAW);
final int stride = PlayerReflectionLayoutTest.FLOATS * 4;
GL20.glVertexAttribPointer(0, 3, GL11.GL_FLOAT, false, stride, 0L);
GL20.glEnableVertexAttribArray(0);
GL20.glVertexAttribPointer(1, 2, GL11.GL_FLOAT, false, stride, 12L);
GL20.glEnableVertexAttribArray(1);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
}
private void allocateSsbo() {
ssbo = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, ssbo);
GLStateManager.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, ByteBuffer.allocateDirect(SSBO_BYTES).order(ByteOrder.nativeOrder()), GL15.GL_DYNAMIC_DRAW);
GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, BINDING, ssbo);
visitedSsbo = GLStateManager.glGenBuffers();
GLStateManager.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, visitedSsbo);
GLStateManager.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, ByteBuffer.allocateDirect(VISITED_BYTES).order(ByteOrder.nativeOrder()), GL15.GL_DYNAMIC_DRAW);
GL30.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, VISITED_BINDING, visitedSsbo);
GLStateManager.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, 0);
}
private ByteBuffer readback(int buffer, int bytes) {
GL42.glMemoryBarrier(GL43.GL_SHADER_STORAGE_BARRIER_BIT | GL42.GL_BUFFER_UPDATE_BARRIER_BIT);
final ByteBuffer out = BufferUtils.createByteBuffer(bytes);
GLStateManager.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, buffer);
GLStateManager.glGetBufferSubData(GL43.GL_SHADER_STORAGE_BUFFER, 0L, out);
GLStateManager.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, 0);
return out;
}
@Test
void quadIndexPatternProducesTheVertexIdsThePackIndexesBy() {
assumeTrue(RenderSystem.supportsSSBO(), "SSBOs unsupported");
final float[] xyzuv = emit(new ModelBiped(0.0f), MODEL_SCALE);
buildProgram();
uploadVertices(xyzuv);
allocateSsbo();
GLStateManager.glUseProgram(program);
GLStateManager.glBindVertexArray(vao);
QuadConverter.drawQuadsAsTriangles(0, PlayerReflectionCapture.VERTEX_COUNT);
final ByteBuffer ssboData = readback(ssbo, SSBO_BYTES);
final int floats = PlayerReflectionLayoutTest.FLOATS;
for (int i = 0; i < SSBO_ENTRIES; i++) {
final int vertexId = i + i / 3;
final int src = vertexId * floats;
assertEquals(xyzuv[src], ssboData.getFloat(i * POSITION_STRIDE), 1e-6f, "entry " + i + " x came from a vertex other than " + vertexId);
assertEquals(xyzuv[src + 1], ssboData.getFloat(i * POSITION_STRIDE + 4), 1e-6f, "entry " + i + " y came from a vertex other than " + vertexId);
assertEquals(xyzuv[src + 2], ssboData.getFloat(i * POSITION_STRIDE + 8), 1e-6f, "entry " + i + " z came from a vertex other than " + vertexId);
assertEquals(xyzuv[src + 3], ssboData.getFloat(DATA_OFFSET + i * DATA_STRIDE), 1e-6f, "entry " + i + " u came from a vertex other than " + vertexId);
assertEquals(xyzuv[src + 4], ssboData.getFloat(DATA_OFFSET + i * DATA_STRIDE + 4), 1e-6f, "entry " + i + " v came from a vertex other than " + vertexId);
}
}
@Test
void everyQuadCornerRunsTheVertexShader() {
assumeTrue(RenderSystem.supportsSSBO(), "SSBOs unsupported");
final float[] xyzuv = emit(new ModelBiped(0.0f), MODEL_SCALE);
buildProgram();
uploadVertices(xyzuv);
allocateSsbo();
GLStateManager.glUseProgram(program);
GLStateManager.glBindVertexArray(vao);
QuadConverter.drawQuadsAsTriangles(0, PlayerReflectionCapture.VERTEX_COUNT);
final ByteBuffer visitedData = readback(visitedSsbo, VISITED_BYTES);
for (int vertexId = 0; vertexId < PlayerReflectionCapture.VERTEX_COUNT; vertexId++) {
assertEquals(1, visitedData.getInt(vertexId * 4), "vertex " + vertexId + " (corner " + (vertexId % 4) + " of quad " + (vertexId / 4) + ") never ran; the shared quad EBO must visit all four corners of every quad");
}
}
}
+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/")