mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-14 17:24:43 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffe54f61b9 | ||
|
|
c86e1de9f6 | ||
|
|
6af401c834 | ||
|
|
b17bb42c49 | ||
|
|
3659b59db9 | ||
|
|
8c31eec525 |
No files matched your search
@@ -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
|
||||
@@ -3,7 +3,7 @@ name: Release tagged build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ '*' ]
|
||||
tags: [ '*', '!tracy-v*' ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -40,5 +40,6 @@ addon.late.local.gradle
|
||||
addon.late.local.gradle.kts
|
||||
local/
|
||||
tracy-client/src/main/resources/natives/
|
||||
tracy-client/angelica/
|
||||
dist/tracy-natives/
|
||||
*.tracy
|
||||
+6
-2
@@ -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")
|
||||
}
|
||||
@@ -202,6 +202,7 @@ afterEvaluate {
|
||||
|
||||
glsmTestFixtures(testFixtures(project(":glsm")) as ModuleDependency) { isTransitive = false }
|
||||
testCompileOnly(testFixtures(project(":glsm")))
|
||||
testImplementation("mega:fluidlogged-mc1.7.10:0.1.2")
|
||||
|
||||
testRuntimeOnly(libs.fastutil)
|
||||
testRuntimeOnly(libs.joml)
|
||||
|
||||
@@ -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);
|
||||
@@ -34,6 +35,8 @@ public final class SystemProperties {
|
||||
public static final boolean SDL_ENCODER_ASSERTIONS_FATAL = ENCODER_ASSERTIONS == SdlAssertionMode.FATAL;
|
||||
public static final boolean SDL_VERIFY_STATE_SYNC = Boolean.getBoolean("angelica.sdlgpu.verifyStateSync");
|
||||
public static final boolean SDL_VERIFY_PER_FRAME_UNIFORM_BLOCK = Boolean.getBoolean("angelica.sdlgpu.verifyPerFrameUniformBlock");
|
||||
public static final int SDL_FRAMES_IN_FLIGHT = Integer.getInteger("angelica.sdlgpu.framesInFlight", 2);
|
||||
public static final boolean DISABLE_SDL_PRESENTER_THREAD = Boolean.getBoolean("angelica.sdlgpu.disablePresenterThread");
|
||||
|
||||
// Tracy
|
||||
public static final boolean TRACY = Boolean.getBoolean("angelica.tracy");
|
||||
|
||||
@@ -10,9 +10,11 @@ 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.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.VertexKey;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredAlphaHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredDepthColorHandler;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3049,9 +3058,15 @@ public class GLStateManager {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (light >= VertexKey.FFP_LIGHT_COUNT && (warnedFfpLightMask & (1 << light)) == 0 && ShaderManager.getInstance().isEnabled()) {
|
||||
warnedFfpLightMask |= (1 << light);
|
||||
LOGGER.warn("GL_LIGHT{} enabled, but FFP shader emulation implements only GL_LIGHT0..{}", light, VertexKey.FFP_LIGHT_COUNT - 1, new Throwable("call site"));
|
||||
}
|
||||
ctx().lightStates[light].enable();
|
||||
}
|
||||
|
||||
private static int warnedFfpLightMask = 0;
|
||||
|
||||
public static void enableColorMaterial() {
|
||||
final RecordMode mode = DisplayListManager.getRecordMode();
|
||||
if (mode != RecordMode.NONE) {
|
||||
@@ -3374,14 +3389,11 @@ public class GLStateManager {
|
||||
}
|
||||
|
||||
public static boolean isCurrent() {
|
||||
if (BackendManager.RENDER_BACKEND == null) {
|
||||
return Thread.currentThread() == MainThread;
|
||||
}
|
||||
return BackendManager.RENDER_BACKEND.isCurrent();
|
||||
return RENDER_BACKEND.isCurrent();
|
||||
}
|
||||
|
||||
public static void swapBuffers() throws LWJGLException {
|
||||
if (BackendManager.RENDER_BACKEND != null && BackendManager.RENDER_BACKEND.handleSwapBuffers()) {
|
||||
if (RENDER_BACKEND.handleSwapBuffers()) {
|
||||
return;
|
||||
}
|
||||
Display.swapBuffers();
|
||||
@@ -3397,7 +3409,14 @@ public class GLStateManager {
|
||||
if (processMessages) Display.processMessages();
|
||||
return;
|
||||
}
|
||||
if (!RENDER_BACKEND.wantsDisplayUpdateGateTiming()) {
|
||||
Display.update(processMessages);
|
||||
return;
|
||||
}
|
||||
final long start = System.nanoTime();
|
||||
Display.update(processMessages);
|
||||
final long end = System.nanoTime();
|
||||
RENDER_BACKEND.recordFrameGate(end - start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4510,7 +4529,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(); }
|
||||
@@ -5039,6 +5061,54 @@ public class GLStateManager {
|
||||
RENDER_BACKEND.setVSyncEnabled(enabled);
|
||||
}
|
||||
|
||||
public static void setVSyncMode(VSyncMode mode) {
|
||||
RENDER_BACKEND.setVSyncMode(mode);
|
||||
}
|
||||
|
||||
public static void setVSyncPreference(VSyncMode mode) {
|
||||
RENDER_BACKEND.setVSyncPreference(mode);
|
||||
}
|
||||
|
||||
public static boolean supportsVSyncMode(VSyncMode mode) {
|
||||
return RENDER_BACKEND.supportsVSyncMode(mode);
|
||||
}
|
||||
|
||||
public static VSyncMode preferredTearFreeMode() {
|
||||
return RENDER_BACKEND.preferredTearFreeMode();
|
||||
}
|
||||
|
||||
public static String getRenderBackendName() {
|
||||
return RENDER_BACKEND.getName();
|
||||
}
|
||||
|
||||
public static VSyncMode getEffectiveVSyncMode() {
|
||||
return RENDER_BACKEND.getEffectiveVSyncMode();
|
||||
}
|
||||
|
||||
public static int getDisplayRefreshRateHz() {
|
||||
return RENDER_BACKEND.getDisplayRefreshRateHz();
|
||||
}
|
||||
|
||||
public static boolean gateAnchorsNextFrameStart() {
|
||||
return RENDER_BACKEND.gateAnchorsNextFrameStart();
|
||||
}
|
||||
|
||||
public static long lastFrameGateNanos() {
|
||||
return RENDER_BACKEND.lastFrameGateNanos();
|
||||
}
|
||||
|
||||
public static long lastFrameGateEndNanos() {
|
||||
return RENDER_BACKEND.lastFrameGateEndNanos();
|
||||
}
|
||||
|
||||
public static boolean hasSwapchainBackpressure() {
|
||||
return RENDER_BACKEND.hasSwapchainBackpressure();
|
||||
}
|
||||
|
||||
public static void pumpDisplayMessages() {
|
||||
Display.processMessages();
|
||||
}
|
||||
|
||||
public static int glGetError() {
|
||||
if (!RENDER_BACKEND.hasContext()) return 0;
|
||||
if (initConfig.isNoErrorChecks()) return 0;
|
||||
@@ -5119,6 +5189,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 +5205,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 +5215,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 +5256,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import org.antlr.v4.runtime.BufferedTokenStream;
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.CommonToken;
|
||||
import org.antlr.v4.runtime.CommonTokenStream;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.WritableToken;
|
||||
import org.antlr.v4.runtime.tree.ParseTree;
|
||||
import org.antlr.v4.runtime.tree.TerminalNode;
|
||||
import org.taumc.glsl.grammar.GLSLLexer;
|
||||
@@ -88,6 +90,23 @@ public class GlslTransformUtils {
|
||||
return source;
|
||||
}
|
||||
|
||||
public static void restoreReservedWordsInTree(ParseTree tree) {
|
||||
if (tree instanceof TerminalNode terminal) {
|
||||
final Token token = terminal.getSymbol();
|
||||
final String text = token.getText();
|
||||
if (text != null && text.startsWith(RENAMED_PREFIX) && token instanceof WritableToken writable) {
|
||||
final String rest = text.substring(RENAMED_PREFIX.length());
|
||||
if (rest.startsWith("texture") || rest.startsWith("sample")) {
|
||||
writable.setText(rest);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < tree.getChildCount(); i++) {
|
||||
restoreReservedWordsInTree(tree.getChild(i));
|
||||
}
|
||||
}
|
||||
|
||||
public record QuietParse(GLSLParser.Translation_unitContext full, GLSLPreParser.Translation_unitContext pre) {}
|
||||
|
||||
public static GLSLParser.Translation_unitContext parseFullQuiet(String source) {
|
||||
@@ -162,21 +181,32 @@ public class GlslTransformUtils {
|
||||
public static String getFormattedShader(ParseTree tree, String header) {
|
||||
final StringBuilder sb = new StringBuilder(header + "\n");
|
||||
final String[] tabHolder = {""};
|
||||
getFormattedShader(tree, sb, tabHolder);
|
||||
getFormattedShader(tree, sb, tabHolder, false);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void getFormattedShader(ParseTree tree, StringBuilder stringBuilder, String[] tabHolder) {
|
||||
if (tree instanceof TerminalNode) {
|
||||
public static String getFormattedShaderRebased(ParseTree tree, String header) {
|
||||
final StringBuilder sb = new StringBuilder(header + "\n");
|
||||
final String[] tabHolder = {""};
|
||||
getFormattedShader(tree, sb, tabHolder, true);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void getFormattedShader(ParseTree tree, StringBuilder stringBuilder, String[] tabHolder, boolean rebaseOffsets) {
|
||||
if (tree instanceof TerminalNode terminal) {
|
||||
final String text = tree.getText();
|
||||
if (text.equals("<EOF>")) {
|
||||
return;
|
||||
}
|
||||
if (text.equals("#")) {
|
||||
stringBuilder.append("\n#");
|
||||
rebaseAt(terminal, stringBuilder.length() - 1, text, rebaseOffsets);
|
||||
return;
|
||||
}
|
||||
stringBuilder.append(text);
|
||||
if (!text.equals("}")) {
|
||||
rebaseAt(terminal, stringBuilder.length() - text.length(), text, rebaseOffsets);
|
||||
}
|
||||
if (text.equals("{")) {
|
||||
stringBuilder.append(" \n\t");
|
||||
tabHolder[0] = "\t";
|
||||
@@ -186,6 +216,7 @@ public class GlslTransformUtils {
|
||||
if (stringBuilder.length() >= 2) {
|
||||
stringBuilder.deleteCharAt(stringBuilder.length() - 2);
|
||||
}
|
||||
rebaseAt(terminal, stringBuilder.length() - 1, text, rebaseOffsets);
|
||||
tabHolder[0] = "";
|
||||
stringBuilder.append(" \n");
|
||||
} else {
|
||||
@@ -193,8 +224,17 @@ public class GlslTransformUtils {
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < tree.getChildCount(); ++i) {
|
||||
getFormattedShader(tree.getChild(i), stringBuilder, tabHolder);
|
||||
getFormattedShader(tree.getChild(i), stringBuilder, tabHolder, rebaseOffsets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void rebaseAt(TerminalNode terminal, int startIndex, String text, boolean rebaseOffsets) {
|
||||
if (!rebaseOffsets) return;
|
||||
if (terminal.getSymbol() instanceof CommonToken common) {
|
||||
common.setText(text);
|
||||
common.setStartIndex(startIndex);
|
||||
common.setStopIndex(startIndex + text.length() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -8,6 +9,7 @@ import com.gtnewhorizons.angelica.glsm.dsa.DSAEXT;
|
||||
import com.gtnewhorizons.angelica.glsm.dsa.DSAUnsupported;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
|
||||
import com.mitchej123.lwjgl.LWJGLServiceProvider;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
@@ -59,6 +61,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;
|
||||
|
||||
@@ -107,6 +112,13 @@ public class RenderSystem {
|
||||
|
||||
BackendManager.init();
|
||||
|
||||
try {
|
||||
// Force init on client thread
|
||||
GLStateManager.LOGGER.info("Celeritas LWJGL service: {}", LWJGLServiceProvider.LWJGL.getClass().getName());
|
||||
} catch (Throwable t) {
|
||||
GLStateManager.LOGGER.debug("Could not resolve the celeritas LWJGL service: {}", t.toString());
|
||||
}
|
||||
|
||||
supportsCompute = supportsCompute();
|
||||
supportsTesselation = GLStateManager.capabilities.GL_ARB_tessellation_shader || GLStateManager.capabilities.OpenGL40;
|
||||
|
||||
@@ -403,6 +415,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;
|
||||
}
|
||||
|
||||
+14
-1
@@ -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,19 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
|
||||
public int getMinGLSLVersion() {return 330;}
|
||||
|
||||
@Override
|
||||
public void setVSyncEnabled(boolean enabled) {Display.setVSyncEnabled(enabled);}
|
||||
protected VSyncMode applyVSyncMode(VSyncMode preferred) {
|
||||
final boolean enabled = preferred.tearFree();
|
||||
Display.setVSyncEnabled(enabled);
|
||||
return enabled ? VSyncMode.ON : VSyncMode.OFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int queryDisplayRefreshRateHz() {
|
||||
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() {
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.nio.IntBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Abstract rendering backend.
|
||||
@@ -62,7 +63,77 @@ public abstract class RenderBackend {
|
||||
public abstract void flush();
|
||||
public abstract void finish();
|
||||
|
||||
public abstract void setVSyncEnabled(boolean enabled);
|
||||
private VSyncMode effectiveVSyncMode = VSyncMode.ON;
|
||||
private VSyncMode preferredTearFreeMode;
|
||||
|
||||
public final void setVSyncEnabled(boolean enabled) {
|
||||
setVSyncMode(enabled ? preferredTearFreeMode() : VSyncMode.OFF);
|
||||
}
|
||||
|
||||
public final void setVSyncMode(VSyncMode preferred) {
|
||||
setVSyncPreference(preferred);
|
||||
publishVSyncMode(applyVSyncMode(preferred));
|
||||
}
|
||||
|
||||
public final void setVSyncPreference(VSyncMode preferred) {
|
||||
if (preferred.tearFree()) preferredTearFreeMode = preferred;
|
||||
}
|
||||
|
||||
public final VSyncMode preferredTearFreeMode() {
|
||||
return resolveTearFreeMode(preferredTearFreeMode, this::supportsVSyncMode);
|
||||
}
|
||||
|
||||
static VSyncMode resolveTearFreeMode(VSyncMode preferred, Predicate<VSyncMode> supported) {
|
||||
if (preferred != null && preferred.tearFree() && supported.test(preferred)) return preferred;
|
||||
return supported.test(VSyncMode.MAILBOX) ? VSyncMode.MAILBOX : VSyncMode.ON;
|
||||
}
|
||||
|
||||
protected abstract VSyncMode applyVSyncMode(VSyncMode preferred);
|
||||
|
||||
public boolean supportsVSyncMode(VSyncMode mode) { return mode != VSyncMode.MAILBOX; }
|
||||
|
||||
protected final void publishVSyncMode(VSyncMode mode) { effectiveVSyncMode = mode; }
|
||||
|
||||
public final VSyncMode getEffectiveVSyncMode() { return effectiveVSyncMode; }
|
||||
|
||||
private static final long REFRESH_POLL_NANOS = 1_000_000_000L;
|
||||
|
||||
private long refreshPollNanos;
|
||||
private int cachedRefreshHz;
|
||||
|
||||
public final int getDisplayRefreshRateHz() {
|
||||
final long now = System.nanoTime();
|
||||
if (refreshPollNanos == 0L || now - refreshPollNanos >= REFRESH_POLL_NANOS) {
|
||||
refreshPollNanos = now;
|
||||
cachedRefreshHz = queryDisplayRefreshRateHz();
|
||||
}
|
||||
return cachedRefreshHz;
|
||||
}
|
||||
|
||||
protected int queryDisplayRefreshRateHz() { 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 boolean hasSwapchainBackpressure() { return false; }
|
||||
|
||||
private long frameGateNanos;
|
||||
private long frameGateEndNanos;
|
||||
|
||||
public boolean gateAnchorsNextFrameStart() { return false; }
|
||||
|
||||
public boolean wantsDisplayUpdateGateTiming() { return true; }
|
||||
|
||||
public void recordFrameGate(long durationNanos, long endNanos) {
|
||||
frameGateNanos = durationNanos;
|
||||
frameGateEndNanos = endNanos;
|
||||
}
|
||||
|
||||
public long lastFrameGateNanos() { return frameGateNanos; }
|
||||
|
||||
public long lastFrameGateEndNanos() { return frameGateEndNanos; }
|
||||
|
||||
public void onFrameBegin() {
|
||||
if (GLStateManager.takeStateSeedPending()) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.gtnewhorizons.angelica.glsm.backend;
|
||||
|
||||
public enum VSyncMode {
|
||||
OFF,
|
||||
ON,
|
||||
MAILBOX;
|
||||
|
||||
public boolean tearFree() {
|
||||
return this != OFF;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.states.LightState;
|
||||
import com.gtnewhorizons.angelica.glsm.states.MaterialState;
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
public final class FFPVertexLighting {
|
||||
|
||||
private static final Vector3f scratchNormal = new Vector3f();
|
||||
private static final Vector3f scratchLightVec = new Vector3f();
|
||||
private static final Vector4f scratchEyePos = new Vector4f();
|
||||
private static final Matrix3f scratchNormalMat = new Matrix3f();
|
||||
private static final Vector3f scratchVec = new Vector3f();
|
||||
|
||||
private FFPVertexLighting() {}
|
||||
|
||||
public static boolean modulatesVertexColor(Vector3f destFactor) {
|
||||
destFactor.set(1.0f);
|
||||
|
||||
if (!GLStateManager.getLightingState().isEnabled()) return false;
|
||||
if (!GLStateManager.getColorMaterial().isEnabled()) return false;
|
||||
if (VertexKey.encodeColorMaterialMode(GLStateManager.getColorMaterialParameter().getValue()) != VertexKey.CM_AMBIENT_AND_DIFFUSE) return false;
|
||||
|
||||
final MaterialState material = GLStateManager.getFrontMaterial();
|
||||
if (material.emission.x != 0.0f || material.emission.y != 0.0f || material.emission.z != 0.0f)
|
||||
return false;
|
||||
|
||||
final boolean materialHasSpecular = material.shininess > 0.0f && (material.specular.x != 0.0f || material.specular.y != 0.0f || material.specular.z != 0.0f);
|
||||
|
||||
final Vector4f lightModelAmbient = GLStateManager.getLightModel().ambient;
|
||||
float r = lightModelAmbient.x;
|
||||
float g = lightModelAmbient.y;
|
||||
float b = lightModelAmbient.z;
|
||||
|
||||
final var lightEnables = GLStateManager.getLightStates();
|
||||
final var lightData = GLStateManager.getLightDataStates();
|
||||
boolean normalReady = false;
|
||||
|
||||
for (int i = 0; i < VertexKey.FFP_LIGHT_COUNT; i++) {
|
||||
if (!lightEnables[i].isEnabled()) continue;
|
||||
final LightState light = lightData[i];
|
||||
if (materialHasSpecular && (light.specular.x != 0.0f || light.specular.y != 0.0f || light.specular.z != 0.0f))
|
||||
return false;
|
||||
if (!normalReady) {
|
||||
transformCurrentNormal();
|
||||
normalReady = true;
|
||||
}
|
||||
final float nDotVP = Math.max(scratchNormal.dot(lightDirection(light)), 0.0f);
|
||||
r += light.ambient.x + nDotVP * light.diffuse.x;
|
||||
g += light.ambient.y + nDotVP * light.diffuse.y;
|
||||
b += light.ambient.z + nDotVP * light.diffuse.z;
|
||||
}
|
||||
|
||||
if (r == 1.0f && g == 1.0f && b == 1.0f) return false;
|
||||
if (!Float.isFinite(r + g + b)) return false;
|
||||
destFactor.set(r, g, b);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void transformCurrentNormal() {
|
||||
GLStateManager.getModelViewMatrix().normal(scratchNormalMat);
|
||||
scratchNormal.set(ShaderManager.getCurrentNormal());
|
||||
scratchNormalMat.transform(scratchNormal);
|
||||
if (GLStateManager.getNormalizeState().isEnabled()) {
|
||||
scratchNormal.normalize();
|
||||
} else if (GLStateManager.getRescaleNormalState().isEnabled()) {
|
||||
scratchNormal.mul(Uniforms.rescaleFactor(scratchNormalMat, scratchVec));
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3f lightDirection(LightState light) {
|
||||
if (light.position.w == 0.0f) {
|
||||
scratchLightVec.set(light.position.x, light.position.y, light.position.z);
|
||||
} else {
|
||||
scratchEyePos.set(0.0f, 0.0f, 0.0f, 1.0f).mul(GLStateManager.getModelViewMatrix());
|
||||
scratchLightVec.set(light.position.x - scratchEyePos.x, light.position.y - scratchEyePos.y, light.position.z - scratchEyePos.z);
|
||||
}
|
||||
return scratchLightVec.normalize();
|
||||
}
|
||||
}
|
||||
@@ -232,8 +232,12 @@ public class Uniforms {
|
||||
private void stageNormalMatrix() {
|
||||
GLStateManager.getModelViewMatrix().normal(normalMatrix);
|
||||
putMat3(FFPUniformBlock.NORMAL_MATRIX, normalMatrix);
|
||||
final float scale = 1.0f / normalMatrix.getColumn(0, tempVec3).length();
|
||||
putFloat(FFPUniformBlock.NORMAL_SCALE, scale);
|
||||
putFloat(FFPUniformBlock.NORMAL_SCALE, rescaleFactor(normalMatrix, tempVec3));
|
||||
}
|
||||
|
||||
static float rescaleFactor(Matrix3f normalMatrix, Vector3f scratch) {
|
||||
final float f = normalMatrix.getColumn(2, scratch).lengthSquared();
|
||||
return f > 1.0e-12f ? 1.0f / (float) Math.sqrt(f) : 1.0f;
|
||||
}
|
||||
|
||||
private void stageTextureMatrices() {
|
||||
|
||||
@@ -20,38 +20,36 @@ public final class VertexKey {
|
||||
|
||||
public static final int MAX_UNITS = 4;
|
||||
|
||||
public static final int FFP_LIGHT_COUNT = 2;
|
||||
|
||||
private final long packed;
|
||||
|
||||
private static final int BIT_LIGHTING = 0;
|
||||
private static final int BIT_LIGHT0 = 1;
|
||||
private static final int BIT_LIGHT1 = 2;
|
||||
private static final int BIT_LIGHT0_DIR = 3;
|
||||
private static final int BIT_LIGHT1_DIR = 4;
|
||||
private static final int BIT_COLOR_MATERIAL = 5;
|
||||
private static final int BIT_SEPARATE_SPECULAR = 6;
|
||||
private static final int BIT_FOG = 7;
|
||||
private static final int BIT_FOG_DIST_MODE = 8; // 2 bits: 8-9
|
||||
private static final int BIT_NORMALIZE = 10;
|
||||
private static final int BIT_RESCALE_NORMAL = 11;
|
||||
// Per-unit texcoord enable: bits 12-15 (unit 0..3). Unit 1 doubles as the lightmap flag.
|
||||
private static final int BIT_UNIT_TEX_BASE = 12;
|
||||
private static final int BIT_HAS_VERTEX_COLOR = 16;
|
||||
private static final int BIT_HAS_VERTEX_NORMAL = 17;
|
||||
private static final int BIT_HAS_VERTEX_TEX = 18;
|
||||
private static final int BIT_HAS_VERTEX_LIGHTMAP = 19;
|
||||
private static final int BIT_COLOR_MAT_MODE = 20; // 3 bits: 20-22
|
||||
// TexGen modes (unit 0 only, for now): 3 bits each (0=NONE, 1=OBJ_LINEAR, 2=EYE_LINEAR).
|
||||
private static final int BIT_TEXGEN_S = 23; // 3 bits: 23-25
|
||||
private static final int BIT_TEXGEN_T = 26; // 3 bits: 26-28
|
||||
private static final int BIT_TEXGEN_R = 29; // 3 bits: 29-31
|
||||
private static final int BIT_TEXGEN_Q = 32; // 3 bits: 32-34
|
||||
private static final int BIT_CLIP_PLANES = 35;
|
||||
private static final int BIT_WIDE_LINE = 36;
|
||||
// Per-unit texmat enable: bits 37-40 (unit 0..3). Unit 1 is the lightmap matrix.
|
||||
private static final int BIT_UNIT_TEXMAT_BASE = 37;
|
||||
private static final int BIT_UNIT23_UV_FROM_UNIT0 = 41;
|
||||
private static final int BIT_LINE_STIPPLE = 42;
|
||||
private static final int BIT_INSTANCED = 44;
|
||||
private static final int BIT_LIGHT_BASE = BIT_LIGHTING + 1;
|
||||
private static final int BIT_LIGHT_DIR_BASE = BIT_LIGHT_BASE + FFP_LIGHT_COUNT;
|
||||
private static final int BIT_COLOR_MATERIAL = BIT_LIGHT_DIR_BASE + FFP_LIGHT_COUNT;
|
||||
private static final int BIT_SEPARATE_SPECULAR = BIT_COLOR_MATERIAL + 1;
|
||||
private static final int BIT_FOG = BIT_SEPARATE_SPECULAR + 1;
|
||||
private static final int BIT_FOG_DIST_MODE = BIT_FOG + 1;
|
||||
private static final int BIT_NORMALIZE = BIT_FOG_DIST_MODE + 2;
|
||||
private static final int BIT_RESCALE_NORMAL = BIT_NORMALIZE + 1;
|
||||
private static final int BIT_UNIT_TEX_BASE = BIT_RESCALE_NORMAL + 1;
|
||||
private static final int BIT_HAS_VERTEX_COLOR = BIT_UNIT_TEX_BASE + MAX_UNITS;
|
||||
private static final int BIT_HAS_VERTEX_NORMAL = BIT_HAS_VERTEX_COLOR + 1;
|
||||
private static final int BIT_HAS_VERTEX_TEX = BIT_HAS_VERTEX_NORMAL + 1;
|
||||
private static final int BIT_HAS_VERTEX_LIGHTMAP = BIT_HAS_VERTEX_TEX + 1;
|
||||
private static final int BIT_COLOR_MAT_MODE = BIT_HAS_VERTEX_LIGHTMAP + 1;
|
||||
// TexGen modes are unit 0 only, for now.
|
||||
private static final int BIT_TEXGEN_S = BIT_COLOR_MAT_MODE + 3;
|
||||
private static final int BIT_TEXGEN_T = BIT_TEXGEN_S + 3;
|
||||
private static final int BIT_TEXGEN_R = BIT_TEXGEN_T + 3;
|
||||
private static final int BIT_TEXGEN_Q = BIT_TEXGEN_R + 3;
|
||||
private static final int BIT_CLIP_PLANES = BIT_TEXGEN_Q + 3;
|
||||
private static final int BIT_WIDE_LINE = BIT_CLIP_PLANES + 1;
|
||||
private static final int BIT_UNIT_TEXMAT_BASE = BIT_WIDE_LINE + 1;
|
||||
private static final int BIT_UNIT23_UV_FROM_UNIT0 = BIT_UNIT_TEXMAT_BASE + MAX_UNITS;
|
||||
private static final int BIT_LINE_STIPPLE = BIT_UNIT23_UV_FROM_UNIT0 + 1;
|
||||
private static final int BIT_INSTANCED = BIT_LINE_STIPPLE + 1;
|
||||
|
||||
public static final int TG_NONE = 0;
|
||||
public static final int TG_OBJ_LINEAR = 1;
|
||||
@@ -70,10 +68,8 @@ public final class VertexKey {
|
||||
public long pack() { return packed; }
|
||||
|
||||
public boolean lightingEnabled() { return bit(BIT_LIGHTING); }
|
||||
public boolean light0Enabled() { return bit(BIT_LIGHT0); }
|
||||
public boolean light1Enabled() { return bit(BIT_LIGHT1); }
|
||||
public boolean light0Directional() { return bit(BIT_LIGHT0_DIR); }
|
||||
public boolean light1Directional() { return bit(BIT_LIGHT1_DIR); }
|
||||
public boolean lightEnabled(int light) { return bit(BIT_LIGHT_BASE + light); }
|
||||
public boolean lightDirectional(int light) { return bit(BIT_LIGHT_DIR_BASE + light); }
|
||||
public boolean colorMaterialEnabled() { return bit(BIT_COLOR_MATERIAL); }
|
||||
public boolean separateSpecular() { return bit(BIT_SEPARATE_SPECULAR); }
|
||||
public boolean fogEnabled() { return bit(BIT_FOG); }
|
||||
@@ -136,17 +132,12 @@ public final class VertexKey {
|
||||
if (lighting) {
|
||||
bits |= (1L << BIT_LIGHTING);
|
||||
|
||||
if (GLStateManager.getLightStates()[0].isEnabled()) {
|
||||
bits |= (1L << BIT_LIGHT0);
|
||||
for (int i = 0; i < FFP_LIGHT_COUNT; i++) {
|
||||
if (!GLStateManager.getLightStates()[i].isEnabled()) continue;
|
||||
bits |= (1L << (BIT_LIGHT_BASE + i));
|
||||
// position.w == 0 means directional
|
||||
if (GLStateManager.getLightDataStates()[0].position.w == 0.0f) {
|
||||
bits |= (1L << BIT_LIGHT0_DIR);
|
||||
}
|
||||
}
|
||||
if (GLStateManager.getLightStates()[1].isEnabled()) {
|
||||
bits |= (1L << BIT_LIGHT1);
|
||||
if (GLStateManager.getLightDataStates()[1].position.w == 0.0f) {
|
||||
bits |= (1L << BIT_LIGHT1_DIR);
|
||||
if (GLStateManager.getLightDataStates()[i].position.w == 0.0f) {
|
||||
bits |= (1L << (BIT_LIGHT_DIR_BASE + i));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +256,7 @@ public final class VertexKey {
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("FFPVertexKey[0x%011X: lit=%b l0=%b l1=%b cm=%b fog=%b tex=%d%d%d%d texmat=%d%d%d%d col=%b nrm=%b vtex=%b vlm=%b tg=%d/%d/%d/%d clip=%b wline=%b inst=%b]",
|
||||
packed, lightingEnabled(), light0Enabled(), light1Enabled(),
|
||||
packed, lightingEnabled(), lightEnabled(0), lightEnabled(1),
|
||||
colorMaterialEnabled(), fogEnabled(),
|
||||
unitTexCoordEnabled(0)?1:0, unitTexCoordEnabled(1)?1:0, unitTexCoordEnabled(2)?1:0, unitTexCoordEnabled(3)?1:0,
|
||||
unitTexMatEnabled(0)?1:0, unitTexMatEnabled(1)?1:0, unitTexMatEnabled(2)?1:0, unitTexMatEnabled(3)?1:0,
|
||||
|
||||
@@ -182,11 +182,10 @@ public final class VertexShaderGenerator {
|
||||
}
|
||||
|
||||
// Per-light accumulation
|
||||
if (key.light0Enabled()) {
|
||||
emitLightContribution(sb, key, 0);
|
||||
}
|
||||
if (key.light1Enabled()) {
|
||||
emitLightContribution(sb, key, 1);
|
||||
for (int i = 0; i < VertexKey.FFP_LIGHT_COUNT; i++) {
|
||||
if (key.lightEnabled(i)) {
|
||||
emitLightContribution(sb, key, i);
|
||||
}
|
||||
}
|
||||
|
||||
sb.append(" v_Color = vec4(clamp(color0, 0.0, 1.0), outAlpha);\n");
|
||||
@@ -198,7 +197,7 @@ public final class VertexShaderGenerator {
|
||||
|
||||
private static void emitLightContribution(StringBuilder sb, VertexKey key, int lightIndex) {
|
||||
final String li = String.valueOf(lightIndex);
|
||||
final boolean directional = (lightIndex == 0) ? key.light0Directional() : key.light1Directional();
|
||||
final boolean directional = key.lightDirectional(lightIndex);
|
||||
final String posUniform = "u_Light" + li + "Position";
|
||||
|
||||
sb.append(" { // Light ").append(li).append('\n');
|
||||
|
||||
+5
@@ -1,8 +1,13 @@
|
||||
package com.gtnewhorizons.angelica.glsm.hooks;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ShaderTransformPostProcessor {
|
||||
void onTransformed(String transformedSource, ShaderType shaderType);
|
||||
|
||||
default void onTransformed(String transformedSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, ShaderType shaderType) {
|
||||
onTransformed(transformedSource, shaderType);
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -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
|
||||
*/
|
||||
|
||||
@@ -162,6 +162,7 @@ public class GLSMRedirector {
|
||||
private static final String EXTGpuShader4 = "org/lwjgl/opengl/EXTGpuShader4";
|
||||
private static final String EXTGPUShader4 = "org/lwjgl/opengl/EXTGPUShader4";
|
||||
private static final String ARBTimerQuery = "org/lwjgl/opengl/ARBTimerQuery";
|
||||
private static final String EXTTimerQuery = "org/lwjgl/opengl/EXTTimerQuery";
|
||||
|
||||
// Redirect VAO related calls from NHLib
|
||||
private static final String UniversalVAO = "com/gtnewhorizon/gtnhlib/client/opengl/UniversalVAO";
|
||||
@@ -632,6 +633,10 @@ public class GLSMRedirector {
|
||||
.add("glQueryCounter")
|
||||
.add("glGetQueryObjectui64")
|
||||
);
|
||||
methodRedirects.put(EXTTimerQuery, RedirectMap.newMap()
|
||||
.add("glGetQueryObjectuEXT", "glGetQueryObjectui64")
|
||||
.add("glGetQueryObjectui64EXT", "glGetQueryObjectui64")
|
||||
);
|
||||
|
||||
glDescRedirects.put("org/lwjgl/opengl/GL32C glFenceSync(II)J", "glFenceSync");
|
||||
glDescRedirects.put("org/lwjgl/opengl/GL32C glClientWaitSync(JIJ)I", "glClientWaitSync");
|
||||
|
||||
@@ -100,7 +100,7 @@ public final class GlslVulkanPreprocess {
|
||||
return out;
|
||||
}
|
||||
|
||||
public record Metadata(Set<String> boolUniforms, Set<String> explicitVsInputs) {}
|
||||
public record Metadata(Set<String> boolUniforms, Set<String> explicitVsInputs, boolean needsSamplerless) {}
|
||||
|
||||
public static Metadata collectEdits(String source, GLSLParser.Translation_unitContext root, int glShaderType, String debugName, boolean separateReadOnlyImages, List<Edit> edits) {
|
||||
final boolean isVertex = glShaderType == GL20.GL_VERTEX_SHADER;
|
||||
@@ -216,7 +216,7 @@ public final class GlslVulkanPreprocess {
|
||||
}
|
||||
}, root);
|
||||
|
||||
if (needsSamplerless[0]) {
|
||||
if (needsSamplerless[0] && source != null) {
|
||||
final int at = versionDirectiveEnd(source);
|
||||
if (at >= 0) edits.add(new Edit(at, at - 1, "\n" + SAMPLERLESS_EXTENSION));
|
||||
}
|
||||
@@ -234,7 +234,7 @@ public final class GlslVulkanPreprocess {
|
||||
}
|
||||
}
|
||||
|
||||
return new Metadata(bools, explicitInputs);
|
||||
return new Metadata(bools, explicitInputs, needsSamplerless[0]);
|
||||
}
|
||||
|
||||
public static void clearCache() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+71
@@ -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,67 @@
|
||||
package com.gtnewhorizons.angelica.glsm;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
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 ReservedWordRestoreTest {
|
||||
|
||||
private static void assertTreeRestoreMatchesStringRestore(String label, String renamedSource) {
|
||||
final GLSLParser.Translation_unitContext treeA = GlslTransformUtils.parseFullQuiet(renamedSource);
|
||||
final String legacy = GlslTransformUtils.restoreReservedWords(GlslTransformUtils.getFormattedShader(treeA, ""));
|
||||
|
||||
final GLSLParser.Translation_unitContext treeB = GlslTransformUtils.parseFullQuiet(renamedSource);
|
||||
GlslTransformUtils.restoreReservedWordsInTree(treeB);
|
||||
final String inTree = GlslTransformUtils.getFormattedShader(treeB, "");
|
||||
|
||||
assertEquals(legacy, inTree, "in-tree restore diverged from string restore for " + label);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allThreeRenamesRestoreIdentically() {
|
||||
final String src = "float angelica_renamed_sample = 1.0;\n"
|
||||
+ "uniform sampler2D angelica_renamed_sampler;\n"
|
||||
+ "vec4 angelica_renamed_texture = vec4(0.0);\n"
|
||||
+ "void main() {\n"
|
||||
+ " vec4 c = angelica_renamed_texture + vec4(angelica_renamed_sample);\n"
|
||||
+ "}\n";
|
||||
assertTreeRestoreMatchesStringRestore("all-three", src);
|
||||
}
|
||||
|
||||
@Test
|
||||
void renamedNewStaysRenamed() {
|
||||
final String src = "float angelica_renamed_new = 2.0;\n"
|
||||
+ "void main() { float x = angelica_renamed_new; }\n";
|
||||
assertTreeRestoreMatchesStringRestore("new-identifier", src);
|
||||
|
||||
final GLSLParser.Translation_unitContext tree = GlslTransformUtils.parseFullQuiet(src);
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
final String out = GlslTransformUtils.getFormattedShader(tree, "");
|
||||
assertTrue(out.contains("angelica_renamed_new"), "new is reserved in every GLSL version and must stay renamed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void renamedTextureFunctionCallRestores() {
|
||||
final String src = "uniform sampler2D tex;\n"
|
||||
+ "void main() {\n"
|
||||
+ " vec4 c = angelica_renamed_texture2D(tex, vec2(0.5));\n"
|
||||
+ "}\n";
|
||||
assertTreeRestoreMatchesStringRestore("texture2D-call", src);
|
||||
|
||||
final GLSLParser.Translation_unitContext tree = GlslTransformUtils.parseFullQuiet(src);
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
final String out = GlslTransformUtils.getFormattedShader(tree, "");
|
||||
assertTrue(out.contains("texture2D"), "suffixed rename must strip only the prefix");
|
||||
assertFalse(out.contains("angelica_renamed_"), "no renamed remnants expected");
|
||||
}
|
||||
|
||||
@Test
|
||||
void untouchedSourcePrintsIdentically() {
|
||||
final String src = "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(1.0); }\n";
|
||||
assertTreeRestoreMatchesStringRestore("no-renames", src);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.gtnewhorizons.angelica.glsm.backend;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
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 onlyImmediateCanTear() {
|
||||
assertTrue(VSyncMode.ON.tearFree());
|
||||
assertTrue(VSyncMode.MAILBOX.tearFree());
|
||||
assertFalse(VSyncMode.OFF.tearFree());
|
||||
}
|
||||
|
||||
private static final Predicate<VSyncMode> NO_MAILBOX = mode -> mode != VSyncMode.MAILBOX;
|
||||
private static final Predicate<VSyncMode> EVERYTHING = mode -> true;
|
||||
|
||||
@Test
|
||||
void mailboxIsTakenWhenTheBackendOffersIt() {
|
||||
assertEquals(VSyncMode.MAILBOX, RenderBackend.resolveTearFreeMode(null, EVERYTHING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void vsyncIsTheFallbackWhenMailboxIsUnsupported() {
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(null, NO_MAILBOX));
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(VSyncMode.MAILBOX, NO_MAILBOX));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anExplicitSupportedPreferenceWins() {
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(VSyncMode.ON, EVERYTHING));
|
||||
assertEquals(VSyncMode.MAILBOX, RenderBackend.resolveTearFreeMode(VSyncMode.MAILBOX, EVERYTHING));
|
||||
}
|
||||
|
||||
@Test
|
||||
void offIsNeverATearFreePreference() {
|
||||
assertEquals(VSyncMode.MAILBOX, RenderBackend.resolveTearFreeMode(VSyncMode.OFF, EVERYTHING));
|
||||
assertEquals(VSyncMode.ON, RenderBackend.resolveTearFreeMode(VSyncMode.OFF, NO_MAILBOX));
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
|
||||
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import org.joml.Vector3f;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags.COLOR_BIT;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@GLCoreTest
|
||||
class FFPVertexLightingGLTest {
|
||||
|
||||
private static final int POSITION_LOC = VertexFormatElement.Usage.POSITION.getAttributeLocation();
|
||||
private static final int COLOR_LOC = VertexFormatElement.Usage.COLOR.getAttributeLocation();
|
||||
private static final int STRIDE = 16;
|
||||
|
||||
private static final int VERTEX_COLOR = 0xFF808080;
|
||||
private static final float VERTEX_CHANNEL = 0x80 / 255.0f;
|
||||
|
||||
private static final Vector3f IDENTITY_FACTOR = new Vector3f(1.0f, 1.0f, 1.0f);
|
||||
|
||||
private final Vector3f factor = new Vector3f();
|
||||
|
||||
private int vao;
|
||||
private int vbo;
|
||||
|
||||
@BeforeEach
|
||||
void setUpState() {
|
||||
GLStateManager.glMatrixMode(GL11.GL_PROJECTION);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glViewport(0, 0, 800, 600);
|
||||
GLStateManager.disableDepthTest();
|
||||
GLStateManager.disableCull();
|
||||
GLStateManager.disableBlend();
|
||||
GLStateManager.disableTexture();
|
||||
GLStateManager.glNormal3f(0.0f, 0.0f, 1.0f);
|
||||
standardItemLighting();
|
||||
|
||||
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_EMISSION, floats(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_SPECULAR, floats(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
GLStateManager.glMaterialf(GL11.GL_FRONT_AND_BACK, GL11.GL_SHININESS, 0.0f);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
GLStateManager.glDisable(GL11.GL_NORMALIZE);
|
||||
GLStateManager.glDisable(GL12.GL_RESCALE_NORMAL);
|
||||
GLStateManager.glMatrixMode(GL11.GL_MODELVIEW);
|
||||
GLStateManager.glLoadIdentity();
|
||||
GLStateManager.glDisable(GL11.GL_LIGHTING);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHT0);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHT1);
|
||||
GLStateManager.glDisable(GL11.GL_LIGHT2);
|
||||
GLStateManager.glDisable(GL11.GL_COLOR_MATERIAL);
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
if (sm.isActive()) sm.deactivate();
|
||||
sm.disable();
|
||||
GLStateManager.glBindVertexArray(0);
|
||||
if (vbo != 0) {
|
||||
GLStateManager.glDeleteBuffers(vbo);
|
||||
vbo = 0;
|
||||
}
|
||||
if (vao != 0) {
|
||||
GLStateManager.glDeleteVertexArrays(vao);
|
||||
vao = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void standardItemLighting() {
|
||||
GLStateManager.glEnable(GL11.GL_LIGHTING);
|
||||
GLStateManager.glEnable(GL11.GL_LIGHT0);
|
||||
GLStateManager.glEnable(GL11.GL_LIGHT1);
|
||||
GLStateManager.glEnable(GL11.GL_COLOR_MATERIAL);
|
||||
GLStateManager.glColorMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_AMBIENT_AND_DIFFUSE);
|
||||
GLStateManager.glLightModel(GL11.GL_LIGHT_MODEL_AMBIENT, floats(0.4f, 0.4f, 0.4f, 1.0f));
|
||||
light(GL11.GL_LIGHT0, 0.2f, 1.0f, -0.7f);
|
||||
light(GL11.GL_LIGHT1, -0.2f, 1.0f, 0.7f);
|
||||
}
|
||||
|
||||
private static void light(int light, float x, float y, float z) {
|
||||
GLStateManager.glLight(light, GL11.GL_POSITION, floats(x, y, z, 0.0f));
|
||||
GLStateManager.glLight(light, GL11.GL_DIFFUSE, floats(0.6f, 0.6f, 0.6f, 1.0f));
|
||||
GLStateManager.glLight(light, GL11.GL_AMBIENT, floats(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
GLStateManager.glLight(light, GL11.GL_SPECULAR, floats(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
private static String gateState() {
|
||||
final var material = GLStateManager.getFrontMaterial();
|
||||
return "lighting=" + GLStateManager.getLightingState().isEnabled()
|
||||
+ " colorMaterial=" + GLStateManager.getColorMaterial().isEnabled()
|
||||
+ " cmParam=0x" + Integer.toHexString(GLStateManager.getColorMaterialParameter().getValue())
|
||||
+ " emission=" + material.emission
|
||||
+ " specular=" + material.specular
|
||||
+ " shininess=" + material.shininess
|
||||
+ " lmAmbient=" + GLStateManager.getLightModel().ambient
|
||||
+ " light0=" + GLStateManager.getLightStates()[0].isEnabled()
|
||||
+ " light1=" + GLStateManager.getLightStates()[1].isEnabled();
|
||||
}
|
||||
|
||||
private static FloatBuffer floats(float... values) {
|
||||
final FloatBuffer buf = BufferUtils.createFloatBuffer(values.length);
|
||||
buf.put(values).flip();
|
||||
return buf;
|
||||
}
|
||||
|
||||
private void buildTriangle() {
|
||||
vao = GLStateManager.glGenVertexArrays();
|
||||
GLStateManager.glBindVertexArray(vao);
|
||||
|
||||
final ByteBuffer data = BufferUtils.createByteBuffer(3 * STRIDE);
|
||||
putVertex(data, -0.9f, -0.9f);
|
||||
putVertex(data, 0.9f, -0.9f);
|
||||
putVertex(data, 0.0f, 0.9f);
|
||||
data.flip();
|
||||
|
||||
vbo = GLStateManager.glGenBuffers();
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
|
||||
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW);
|
||||
GLStateManager.glEnableVertexAttribArray(POSITION_LOC);
|
||||
GLStateManager.glVertexAttribPointer(POSITION_LOC, 3, GL11.GL_FLOAT, false, STRIDE, 0L);
|
||||
GLStateManager.glEnableVertexAttribArray(COLOR_LOC);
|
||||
GLStateManager.glVertexAttribPointer(COLOR_LOC, 4, GL11.GL_UNSIGNED_BYTE, true, STRIDE, 12L);
|
||||
VAOManager.setCurrentVertexFlags(COLOR_BIT);
|
||||
}
|
||||
|
||||
private static void putVertex(ByteBuffer buf, float x, float y) {
|
||||
final int base = buf.position();
|
||||
buf.putFloat(base, x).putFloat(base + 4, y).putFloat(base + 8, 0.0f);
|
||||
buf.putInt(base + 12, VERTEX_COLOR);
|
||||
buf.position(base + STRIDE);
|
||||
}
|
||||
|
||||
private float[] drawAndReadCenter() {
|
||||
buildTriangle();
|
||||
final ShaderManager sm = ShaderManager.getInstance();
|
||||
sm.enable();
|
||||
sm.activate();
|
||||
|
||||
GLStateManager.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT);
|
||||
GLStateManager.glDrawArrays(GL11.GL_TRIANGLES, 0, 3);
|
||||
assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "FFP draw must not raise a GL error");
|
||||
|
||||
final ByteBuffer pixel = BufferUtils.createByteBuffer(4);
|
||||
GL11.glReadPixels(400, 200, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixel);
|
||||
return new float[] { (pixel.get(0) & 0xFF) / 255.0f, (pixel.get(1) & 0xFF) / 255.0f, (pixel.get(2) & 0xFF) / 255.0f };
|
||||
}
|
||||
|
||||
private void assertSolverMatchesShader(String context) {
|
||||
assertTrue(FFPVertexLighting.modulatesVertexColor(factor), context + ": expected a representable factor");
|
||||
final float[] actual = drawAndReadCenter();
|
||||
final float[] expected = { Math.min(1.0f, VERTEX_CHANNEL * factor.x), Math.min(1.0f, VERTEX_CHANNEL * factor.y), Math.min(1.0f, VERTEX_CHANNEL * factor.z) };
|
||||
for (int c = 0; c < 3; c++) {
|
||||
assertEquals(expected[c], actual[c], 2.0f / 255.0f, context + ": channel " + c + " solver=" + expected[c] + " shader=" + actual[c] + " factor=" + factor);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void faceOnNormalMatchesGeneratedShader() {
|
||||
GLStateManager.glNormal3f(0.0f, 0.0f, 1.0f);
|
||||
assertSolverMatchesShader("face-on normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
void signNormalMatchesGeneratedShader() {
|
||||
GLStateManager.glNormal3f(0.0f, 0.0f, -0.010416667f);
|
||||
assertSolverMatchesShader("sign normal");
|
||||
assertTrue(factor.x < 0.45f, "a 0.0104-long normal must stay at the 0.4 light model ambient floor rather than pick up 0.6 per light, got " + factor.x);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tiltedNormalMatchesGeneratedShader() {
|
||||
GLStateManager.glNormal3f(0.3f, 0.8f, 0.5f);
|
||||
assertSolverMatchesShader("tilted normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rescaleNormalMatchesGeneratedShader() {
|
||||
GLStateManager.glEnable(GL12.GL_RESCALE_NORMAL);
|
||||
GLStateManager.glScalef(1.0f, 2.0f, 1.0f);
|
||||
GLStateManager.glNormal3f(0.0f, 1.0f, 0.0f);
|
||||
assertSolverMatchesShader("rescale normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizeMatchesGeneratedShader() {
|
||||
GLStateManager.glEnable(GL11.GL_NORMALIZE);
|
||||
GLStateManager.glScalef(1.0f, 2.0f, 1.0f);
|
||||
GLStateManager.glNormal3f(0.0f, 1.0f, 0.0f);
|
||||
assertSolverMatchesShader("normalize");
|
||||
assertTrue(factor.x > 1.2f, "normalize restores the unit normal, which must pick up more diffuse than rescale's 0.5-long one, got " + factor.x);
|
||||
}
|
||||
|
||||
@Test
|
||||
void materialEmissionIsNotRepresentable() {
|
||||
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_EMISSION, floats(0.1f, 0.1f, 0.1f, 1.0f));
|
||||
assertFalse(FFPVertexLighting.modulatesVertexColor(factor), "emission is additive, so the lit colour is not vertexColor * factor");
|
||||
assertEquals(IDENTITY_FACTOR, factor, "a rejected solve must leave an identity factor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void specularContributionIsNotRepresentable() {
|
||||
GLStateManager.glMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_SPECULAR, floats(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
GLStateManager.glMaterialf(GL11.GL_FRONT_AND_BACK, GL11.GL_SHININESS, 16.0f);
|
||||
GLStateManager.glLight(GL11.GL_LIGHT0, GL11.GL_SPECULAR, floats(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
assertFalse(FFPVertexLighting.modulatesVertexColor(factor), "specular is additive, so the lit colour is not vertexColor * factor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonAmbientAndDiffuseColorMaterialIsNotRepresentable() {
|
||||
GLStateManager.glColorMaterial(GL11.GL_FRONT_AND_BACK, GL11.GL_EMISSION);
|
||||
assertFalse(FFPVertexLighting.modulatesVertexColor(factor), "only AMBIENT_AND_DIFFUSE makes the lit colour scale with the vertex colour");
|
||||
}
|
||||
|
||||
@Test
|
||||
void lightsBeyondTheFfpCountAreIgnored() {
|
||||
assertTrue(FFPVertexLighting.modulatesVertexColor(factor), FFPVertexLightingGLTest::gateState);
|
||||
final Vector3f twoLights = new Vector3f(factor);
|
||||
|
||||
light(GL11.GL_LIGHT2, 0.0f, 0.0f, 1.0f);
|
||||
GLStateManager.glEnable(GL11.GL_LIGHT2);
|
||||
assertTrue(FFPVertexLighting.modulatesVertexColor(factor), FFPVertexLightingGLTest::gateState);
|
||||
|
||||
assertEquals(twoLights, factor, "VertexShaderGenerator emits only GL_LIGHT0/GL_LIGHT1, so text must not see GL_LIGHT2 either");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.gtnewhorizons.angelica.glsm.ffp;
|
||||
|
||||
import org.joml.Matrix3f;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class UniformsRescaleTest {
|
||||
|
||||
private static float rescaleFactorFor(float sx, float sy, float sz) {
|
||||
final Matrix3f normalMatrix = new Matrix3f();
|
||||
new Matrix4f().rotateY(0.7f).scale(sx, sy, sz).normal(normalMatrix);
|
||||
return Uniforms.rescaleFactor(normalMatrix, new Vector3f());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rescaleFactorUsesTheInverseModelviewBottomRow() {
|
||||
assertEquals(8.0f, rescaleFactorFor(4.0f, 2.0f, 8.0f), 1.0e-5f, "wrong normal matrix column");
|
||||
}
|
||||
|
||||
@Test
|
||||
void uniformScaleIsRowIndependent() {
|
||||
assertEquals(2.0f, rescaleFactorFor(2.0f, 2.0f, 2.0f), 1.0e-5f);
|
||||
}
|
||||
|
||||
@Test
|
||||
void singularMatrixFallsBackToUnitScale() {
|
||||
assertEquals(1.0f, rescaleFactorFor(1.0f, 1.0f, 0.0f), 1.0e-5f, "a degenerate normal matrix must not divide by zero");
|
||||
}
|
||||
}
|
||||
+92
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -331,6 +331,9 @@ class GLSMRedirectorTest {
|
||||
void phase2NameKeyedGapsRedirect() {
|
||||
assertRedirected("org/lwjgl/opengl/GL33C", "glQueryCounter", "(II)V", "glQueryCounter");
|
||||
assertRedirected("org/lwjgl/opengl/GL33C", "glGetQueryObjectui64", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/ARBTimerQuery", "glGetQueryObjectui64", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/EXTTimerQuery", "glGetQueryObjectuEXT", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/EXTTimerQuery", "glGetQueryObjectui64EXT", "(II)J", "glGetQueryObjectui64");
|
||||
assertRedirected("org/lwjgl/opengl/GL30C", "glBindFragDataLocation", "(IILjava/lang/CharSequence;)V", "glBindFragDataLocation");
|
||||
assertRedirected("org/lwjgl/opengl/GL11C", "glGetIntegerv", "(I[I)V", "glGetInteger");
|
||||
assertRedirected("org/lwjgl/opengl/GL20C", "glUniform4fv", "(I[F)V", "glUniform4");
|
||||
|
||||
@@ -9,7 +9,7 @@ asmCompile = "5.0.3"
|
||||
lwjgl3 = "3.4.2-20260602.093430-9"
|
||||
lwjgl3ify = "3.0.31"
|
||||
|
||||
celeritas = "2.5.9-GTNH"
|
||||
celeritas = "2.5.10-GTNH"
|
||||
gtnhlib = "0.11.36"
|
||||
eventbus = "7.0-beta.15"
|
||||
glslTransformLib = "0.2.0-33.g3ec381e"
|
||||
|
||||
+18
-1
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.GLESCaps;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.DebugMessageHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -32,6 +33,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 +41,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 +172,21 @@ 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 VSyncMode applyVSyncMode(VSyncMode preferred) {
|
||||
final boolean enabled = preferred.tearFree();
|
||||
SDLVideo.SDL_GL_SetSwapInterval(enabled ? 1 : 0);
|
||||
return enabled ? VSyncMode.ON : VSyncMode.OFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int queryDisplayRefreshRateHz() {
|
||||
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() {
|
||||
|
||||
@@ -59,6 +59,7 @@ dependencies {
|
||||
api(project(":glsm"))
|
||||
|
||||
compileOnly(libs.lwjgl3ify) { artifact { classifier = "dev" }; isTransitive = false }
|
||||
compileOnly(libs.retrofuturabootstrap) { isTransitive = false }
|
||||
compileOnly(libs.celeritas.common) { isTransitive = false }
|
||||
|
||||
// GL enum constants (compileOnly -- no runtime GL dependency)
|
||||
@@ -76,6 +77,7 @@ dependencies {
|
||||
testImplementation(platform(libs.junit.bom))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testImplementation(testFixtures(project(":glsm")))
|
||||
testImplementation(libs.celeritas.common) { isTransitive = false }
|
||||
testImplementation(libs.lwjgl3.opengl)
|
||||
// GL.<clinit> loads liblwjgl_opengl.so. Test scope only: a root-level runtimeOnly collides with lwjgl3Bindings.
|
||||
testRuntimeOnly(libs.lwjgl3.opengl) { artifact { classifier = lwjglNatives } }
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.sdl.SDLLog;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
import org.lwjgl.sdl.SDLProperties;
|
||||
import org.lwjgl.system.Platform;
|
||||
import org.lwjglx.Sys;
|
||||
@@ -100,6 +101,11 @@ public final class SDLGPUGate {
|
||||
ShaderManager.prewarmSpirv(transformedSource, glShaderType);
|
||||
}
|
||||
|
||||
public static void prewarmSpirv(String transformedSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType) {
|
||||
if (!isActive()) return;
|
||||
ShaderManager.prewarmSpirv(transformedSource, bodyTree, headerLen, glShaderType);
|
||||
}
|
||||
|
||||
public static void clearShaderPrewarmCache() {
|
||||
if (!isActive()) return;
|
||||
ShaderManager.clearPrewarmCache();
|
||||
@@ -152,6 +158,9 @@ public final class SDLGPUGate {
|
||||
}
|
||||
|
||||
public static void fallBackToGL() {
|
||||
if (SDLGPULWJGLService.isOfferedAsAvailable()) {
|
||||
throw new IllegalStateException("Celeritas already selected the SDL GPU LWJGL service; falling back to OpenGL would leave it bound to a dead backend");
|
||||
}
|
||||
disarmed = true;
|
||||
engaged = false;
|
||||
deviceReady = false;
|
||||
|
||||
@@ -19,11 +19,17 @@ import java.util.Arrays;
|
||||
|
||||
/** SDL GPU implementation of {@link LWJGLService} for Celeritas */
|
||||
public final class SDLGPULWJGLService extends LWJGLService {
|
||||
|
||||
private static volatile boolean offeredAsAvailable;
|
||||
|
||||
public static boolean isOfferedAsAvailable() { return offeredAsAvailable; }
|
||||
|
||||
@Override public int getPriority() {
|
||||
if (SystemProperties.USE_SDL_GPU && SDLGPUGate.isSDLGPUAvailable() && SDLGPUGate.isEngaged()) {
|
||||
return 200;
|
||||
if (!SystemProperties.USE_SDL_GPU || !SDLGPUGate.isSDLGPUAvailable() || !SDLGPUGate.isEngaged()) {
|
||||
return PRIORITY_UNAVAILABLE;
|
||||
}
|
||||
return Integer.MIN_VALUE;
|
||||
offeredAsAvailable = true;
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override public boolean isOpenGLVersionSupported(int major, int minor) {
|
||||
@@ -148,22 +154,30 @@ public final class SDLGPULWJGLService extends LWJGLService {
|
||||
}
|
||||
@Override public void glDeleteSync(long sync) { BackendManager.RENDER_BACKEND.deleteSync(sync); }
|
||||
|
||||
private long[] queryTimestamps = new long[64];
|
||||
private long[] queryElapsed = new long[64];
|
||||
private int activeQueryId = -1;
|
||||
private long activeQueryStart;
|
||||
|
||||
private void ensureQueryCapacity(int id) {
|
||||
if (id >= queryTimestamps.length) {
|
||||
queryTimestamps = Arrays.copyOf(queryTimestamps, Math.max(id + 1, queryTimestamps.length * 2));
|
||||
if (id >= queryElapsed.length) {
|
||||
queryElapsed = Arrays.copyOf(queryElapsed, Math.max(id + 1, queryElapsed.length * 2));
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int glGenQueries() { return BackendManager.RENDER_BACKEND.genQueries(); }
|
||||
@Override public void glDeleteQueries(int query) { }
|
||||
@Override public void glQueryCounter(int id, int target) {
|
||||
@Override public void glBeginQuery(int target, int id) {
|
||||
ensureQueryCapacity(id);
|
||||
queryTimestamps[id] = System.nanoTime();
|
||||
activeQueryId = id;
|
||||
activeQueryStart = System.nanoTime();
|
||||
}
|
||||
@Override public void glEndQuery(int target) {
|
||||
if (activeQueryId < 0) return;
|
||||
queryElapsed[activeQueryId] = System.nanoTime() - activeQueryStart;
|
||||
activeQueryId = -1;
|
||||
}
|
||||
@Override public long glGetQueryObjectui64(int id, int pname) {
|
||||
if (id >= 0 && id < queryTimestamps.length) return queryTimestamps[id];
|
||||
if (id >= 0 && id < queryElapsed.length) return queryElapsed[id];
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -17,6 +18,8 @@ import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FenceTracker;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.Presenter;
|
||||
import com.gtnewhorizons.retrofuturabootstrap.MainStartOnFirstThread;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.pipeline.DrawDispatch;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineCache;
|
||||
@@ -40,9 +43,10 @@ import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageBufferBinder;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageTextureBinder;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.splash.SplashDispatcher;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.splash.SplashOffscreenTarget;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.OffscreenTarget;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.DebugLabels;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.DebugMessageRelay;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.ThreadRegistry;
|
||||
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
@@ -79,8 +83,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.lwjgl.sdl.SDLGPU;
|
||||
@@ -107,6 +111,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
private final Device device = SDLGPUGate.device();
|
||||
private final FrameManager frameManager = new FrameManager(device);
|
||||
private Presenter presenter;
|
||||
private final ResourceManager resourceManager = new ResourceManager(device, frameManager);
|
||||
private final Image3DClear image3DClear = new Image3DClear();
|
||||
private final ShaderManager shaderManager = new ShaderManager(device);
|
||||
@@ -150,33 +155,30 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
private volatile boolean shutdown;
|
||||
|
||||
private SplashOffscreenTarget splashTarget;
|
||||
private OffscreenTarget splashTarget;
|
||||
|
||||
private static final CopyOnWriteArrayList<ContextState> registeredStates = new CopyOnWriteArrayList<>();
|
||||
private static final ThreadLocal<ContextState> tlState = ThreadLocal.withInitial(() -> {
|
||||
private static final ThreadRegistry<ContextState> registeredStates = new ThreadRegistry<>(new ContextState[0]);
|
||||
private static final ThreadLocal<ContextState> tlState = new ThreadLocal<>();
|
||||
|
||||
private static ContextState s() {
|
||||
final ContextState sole = registeredStates.sole();
|
||||
if (sole != null && sole.owner == Thread.currentThread()) return sole;
|
||||
final ContextState st = tlState.get();
|
||||
return st != null ? st : registerState();
|
||||
}
|
||||
|
||||
private static ContextState registerState() {
|
||||
final ContextState st = new ContextState();
|
||||
for (int i = 0; i < ContextState.MAX_VERTEX_ATTRIBS; i++) st.attribDefaults[i * 4 + 3] = 1.0f;
|
||||
st.attribDefaults[4] = 1.0f; st.attribDefaults[5] = 1.0f;
|
||||
st.attribDefaults[6] = 1.0f;
|
||||
tlState.set(st);
|
||||
registeredStates.add(st);
|
||||
refreshSoleState();
|
||||
return st;
|
||||
});
|
||||
|
||||
private static volatile ContextState soleState;
|
||||
|
||||
private static void refreshSoleState() {
|
||||
soleState = registeredStates.size() == 1 ? registeredStates.get(0) : null;
|
||||
}
|
||||
|
||||
private static ContextState s() {
|
||||
final ContextState st = soleState;
|
||||
if (st != null && st.owner == Thread.currentThread()) return st;
|
||||
return tlState.get();
|
||||
}
|
||||
|
||||
private void markAllPipelineInputDirty() {
|
||||
for (ContextState st : registeredStates) st.pipeline.markInputDirty();
|
||||
for (ContextState st : registeredStates.snapshot()) st.pipeline.markInputDirty();
|
||||
}
|
||||
|
||||
public static final int CTR_SLOT_WRITES = 0;
|
||||
@@ -188,7 +190,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
|
||||
public static void takeContextCounters(int[] out) {
|
||||
Arrays.fill(out, 0);
|
||||
for (ContextState st : registeredStates) {
|
||||
for (ContextState st : registeredStates.snapshot()) {
|
||||
out[CTR_SLOT_WRITES] += st.slotWrites;
|
||||
out[CTR_SLOT_WRITES_ELIDED] += st.slotWritesElided;
|
||||
out[CTR_SSBO_BINDS] += st.ssboBinds;
|
||||
@@ -203,7 +205,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
private void releaseUniformStagingAllThreads(ShaderManager.ProgramObject prog) {
|
||||
for (ContextState st : registeredStates) st.releaseUniformStaging(prog);
|
||||
for (ContextState st : registeredStates.snapshot()) st.releaseUniformStaging(prog);
|
||||
}
|
||||
|
||||
|
||||
@@ -289,13 +291,19 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
resourceManager.setBufferLivenessListener(this::markAllPipelineInputDirty);
|
||||
shaderManager.setUniformReleaseListener(this::releaseUniformStagingAllThreads);
|
||||
fenceTracker.setUnresolvedFenceFlush(this::midFrameFenceFlush);
|
||||
frameManager.setMainThread(GLStateManager.getMainThread());
|
||||
if (!SystemProperties.DISABLE_SDL_PRESENTER_THREAD) {
|
||||
presenter = new Presenter(frameManager, MainStartOnFirstThread.instance());
|
||||
frameManager.setPresenter(presenter);
|
||||
LOG.info("SDL presenter thread enabled: presents run on '{}'", device.getWindowThread() != null ? device.getWindowThread().getName() : "window thread");
|
||||
}
|
||||
|
||||
final int splashW = Math.max(1, Display.getWidth());
|
||||
final int splashH = Math.max(1, Display.getHeight());
|
||||
final int[] maxDesktop = device.getMaxDesktopSizePixels();
|
||||
final int splashTexW = Math.max(splashW, maxDesktop[0]);
|
||||
final int splashTexH = Math.max(splashH, maxDesktop[1]);
|
||||
splashTarget = new SplashOffscreenTarget();
|
||||
splashTarget = new OffscreenTarget();
|
||||
splashTarget.create(device, resourceManager, splashTexW, splashTexH, device.getSwapchainTextureFormat());
|
||||
SplashDispatcher.seedDrawnSize(splashW, splashH);
|
||||
LOG.info("Splash offscreen target {}x{} (window {}x{}, max desktop {}x{})", splashTexW, splashTexH, splashW, splashH, maxDesktop[0], maxDesktop[1]);
|
||||
@@ -375,13 +383,50 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
LOG.info("Populated GL capabilities for SDL GPU backend");
|
||||
}
|
||||
|
||||
@Override public void setVSyncEnabled(boolean enabled) {
|
||||
device.setVSyncEnabled(enabled);
|
||||
private final AtomicReference<VSyncMode> pendingVSync = new AtomicReference<>();
|
||||
|
||||
@Override protected VSyncMode applyVSyncMode(VSyncMode preferred) {
|
||||
pendingVSync.set(preferred);
|
||||
return device.chooseVSyncMode(preferred);
|
||||
}
|
||||
|
||||
@Override public boolean supportsVSyncMode(VSyncMode mode) {
|
||||
return device.supportsVSyncMode(mode);
|
||||
}
|
||||
|
||||
private void applyPendingVSync() {
|
||||
final VSyncMode preferred = pendingVSync.getAndSet(null);
|
||||
if (preferred != null) {
|
||||
if (presenter != null) presenter.drain();
|
||||
publishVSyncMode(device.applyVSync(preferred));
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected int queryDisplayRefreshRateHz() {
|
||||
return device.getDisplayRefreshRateHz();
|
||||
}
|
||||
|
||||
@Override public boolean gateAnchorsNextFrameStart() { return true; }
|
||||
|
||||
@Override public boolean wantsDisplayUpdateGateTiming() { return false; }
|
||||
|
||||
@Override public long lastFrameGateNanos() {
|
||||
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateNanos;
|
||||
return frameManager.lastFrameGateNanos();
|
||||
}
|
||||
|
||||
@Override public long lastFrameGateEndNanos() {
|
||||
if (presenter != null && presenter.isEngaged()) return frameManager.windowFrame().lastFrameGateEndNanos;
|
||||
return frameManager.lastFrameGateEndNanos();
|
||||
}
|
||||
|
||||
@Override public boolean hasSwapchainBackpressure() { return true; }
|
||||
|
||||
@Override public void shutdown() {
|
||||
if (shutdown) return;
|
||||
shutdown = true;
|
||||
if (presenter != null) presenter.drain();
|
||||
frameManager.destroyFinalTarget();
|
||||
if (splashTarget != null) {
|
||||
splashTarget.destroy(resourceManager);
|
||||
splashTarget = null;
|
||||
@@ -404,7 +449,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
fenceTracker.dispose();
|
||||
frameManager.releaseAllRegisteredFrames();
|
||||
resourceManager.shutdownTexSamplerStates();
|
||||
for (ContextState st : registeredStates) {
|
||||
for (ContextState st : registeredStates.snapshot()) {
|
||||
if (st.fanIndexBuffer != 0) {
|
||||
resourceManager.releaseBufferHandle(st.fanIndexBuffer);
|
||||
st.fanIndexBuffer = 0;
|
||||
@@ -422,7 +467,6 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
clearMappedState(st);
|
||||
}
|
||||
registeredStates.clear();
|
||||
soleState = null;
|
||||
|
||||
drawDispatch.clearWarnedState();
|
||||
pipelineStore.shutdown();
|
||||
@@ -461,13 +505,16 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
@Override public void onRenderThreadReleased(Thread t) {
|
||||
if (shutdown || t == GLStateManager.getMainThread()) return;
|
||||
if (frameManager.isFrameActive()) {
|
||||
frameManager.endFrame();
|
||||
}
|
||||
frameManager.releaseThreadState();
|
||||
registeredStates.remove(tlState.get());
|
||||
tlState.remove();
|
||||
refreshSoleState();
|
||||
final ContextState st = tlState.get();
|
||||
if (st != null) {
|
||||
tlState.remove();
|
||||
registeredStates.remove(st);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean handleMakeCurrent(Object drawable) {
|
||||
@@ -481,12 +528,11 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
}
|
||||
|
||||
@Override public boolean handleReleaseContext(Object drawable) {
|
||||
if (isSDLManagedDrawable(drawable)) {
|
||||
Lwjgl3GLCapabilitiesShim.clearOnCurrentThread();
|
||||
onRenderThreadReleased(Thread.currentThread());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (!isSDLManagedDrawable(drawable)) return false;
|
||||
if (Thread.currentThread() == GLStateManager.getMainThread()) return true;
|
||||
Lwjgl3GLCapabilitiesShim.clearOnCurrentThread();
|
||||
onRenderThreadReleased(Thread.currentThread());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isSDLManagedDrawable(Object drawable) {
|
||||
@@ -509,33 +555,19 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void dispatchSplashBlit(SplashOffscreenTarget target) {
|
||||
public void dispatchSplashBlit(OffscreenTarget target) {
|
||||
if (shutdown || target == null) return;
|
||||
onFrameBegin();
|
||||
final FrameManager.FrameState f = frameManager.frame();
|
||||
if (!frameManager.ensureSwapchainAcquired(f)) {
|
||||
onFrameEnd();
|
||||
return;
|
||||
}
|
||||
frameManager.endCopyPassIfActive();
|
||||
frameManager.endRenderPassIfActive();
|
||||
final long drawnSize = SplashDispatcher.getDrawnSize();
|
||||
final int srcW = Math.max(1, Math.min((int) (drawnSize >> 32), target.width()));
|
||||
final int srcH = Math.max(1, Math.min((int) drawnSize, target.height()));
|
||||
try (var stack = MemoryStack.stackPush()) {
|
||||
final var info = SDL_GPUBlitInfo.calloc(stack);
|
||||
info.source(s -> s.texture(target.colorTexture()).x(0).y(0).w(srcW).h(srcH));
|
||||
info.destination(d -> d.texture(f.swapchainTexture).x(0).y(0).w(f.swapchainWidth).h(f.swapchainHeight));
|
||||
info.filter(SDL_GPU_FILTER_LINEAR);
|
||||
info.flip_mode(SDLSurface.SDL_FLIP_VERTICAL);
|
||||
SDL_BlitGPUTexture(f.commandBuffer, info);
|
||||
}
|
||||
f.swapchainUsedThisFrame = true;
|
||||
onFrameEnd();
|
||||
final FrameState f = frameManager.frame();
|
||||
frameManager.presentBlit(f, target.colorTexture(), srcW, srcH, SDLSurface.SDL_FLIP_VERTICAL);
|
||||
f.presentedThisFrame = false;
|
||||
}
|
||||
|
||||
@Override public void onFrameBegin() {
|
||||
if (shutdown) return;
|
||||
if (GLStateManager.isSplashComplete()) applyPendingVSync();
|
||||
frameManager.beginFrame();
|
||||
beginFrameInit();
|
||||
}
|
||||
@@ -549,9 +581,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;
|
||||
@@ -613,22 +649,24 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
endFrameUploadFlushNoWait();
|
||||
|
||||
final FrameState f = frameManager.frame();
|
||||
if (f.frameActive && !f.swapchainUsedThisFrame) {
|
||||
if (f.frameActive && !f.fbo0UsedThisFrame) {
|
||||
if (!emptyFrameWarned) {
|
||||
emptyFrameWarned = true;
|
||||
LOG.info("Frame {} ended without drawing to the swapchain; skipping present (further occurrences counted in sdl.emptyFrames)", f.frameNumber);
|
||||
LOG.info("Frame {} ended without drawing to FBO 0; presenting the previous contents (further occurrences counted in sdl.emptyFrames)", f.frameNumber);
|
||||
}
|
||||
frameManager.markFrameEmpty(f);
|
||||
}
|
||||
|
||||
awaitUploadFlush();
|
||||
frameManager.endFrame();
|
||||
frameManager.presentFinalTarget();
|
||||
fenceTracker.resolvePendingFences();
|
||||
resourceManager.flushDeferredReleases();
|
||||
resourceManager.recycleGpuBufferPool(frameManager.getFrameNumber());
|
||||
}
|
||||
|
||||
@Override public void onPreSwapchainInvalidatingChange(Object change) {
|
||||
if (presenter != null) presenter.drain();
|
||||
endFrameUploadFlush();
|
||||
if (frameManager.isFrameActive()) {
|
||||
frameManager.endFrame();
|
||||
@@ -869,18 +907,19 @@ 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;
|
||||
|
||||
// Swapchain path: just record pending state; ensureSwapchainRenderPass consumes it lazily.
|
||||
// FBO0 path: just record pending state; ensureFbo0RenderPass consumes it lazily.
|
||||
if (st.boundFboId == 0) {
|
||||
if (wantColor) {
|
||||
st.pendingSwapchainClear = true;
|
||||
@@ -889,7 +928,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 +961,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 +1510,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 +1552,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;
|
||||
@@ -1619,7 +1677,7 @@ public class SDLGPURenderBackend extends RenderBackend {
|
||||
final long texHandle;
|
||||
final int srcSdlFormat;
|
||||
if (cs.boundReadFboId == 0) {
|
||||
texHandle = frameManager.getSwapchainTexture();
|
||||
texHandle = frameManager.getFbo0Texture();
|
||||
srcSdlFormat = frameManager.getSwapchainFormat();
|
||||
} else {
|
||||
final FboState fbo = resourceManager.getFbo(cs.boundReadFboId);
|
||||
@@ -1674,22 +1732,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 +3302,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 +3751,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;
|
||||
@@ -18,6 +20,7 @@ import org.lwjgl.system.Platform;
|
||||
import org.lwjglx.opengl.Display;
|
||||
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.lwjgl.sdl.SDLGPU.*;
|
||||
@@ -29,7 +32,11 @@ 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 MAX_FRAMES_IN_FLIGHT = 3;
|
||||
|
||||
private static final int FRAMES_IN_FLIGHT = Math.min(Math.max(SystemProperties.SDL_FRAMES_IN_FLIGHT, 1), MAX_FRAMES_IN_FLIGHT);
|
||||
|
||||
public static int framesInFlight() { return FRAMES_IN_FLIGHT; }
|
||||
|
||||
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 +47,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 Thread windowThread;
|
||||
private SDL_LogOutputFunction logCallback; // prevent GC
|
||||
|
||||
private volatile boolean lost;
|
||||
@@ -146,12 +154,12 @@ public final class Device {
|
||||
throw new RuntimeException("Failed to claim window for SDL GPU device: " + SDLError.SDL_GetError());
|
||||
}
|
||||
claimed = true;
|
||||
windowThread = Thread.currentThread();
|
||||
|
||||
final int n = FRAMES_IN_FLIGHT;
|
||||
if (!SDL_SetGPUAllowedFramesInFlight(device, n)) {
|
||||
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}", n, SDLError.SDL_GetError());
|
||||
if (!SDL_SetGPUAllowedFramesInFlight(device, FRAMES_IN_FLIGHT)) {
|
||||
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}", FRAMES_IN_FLIGHT, SDLError.SDL_GetError());
|
||||
} else {
|
||||
LOG.info("SDL frames-in-flight set to {}", n);
|
||||
LOG.info("SDL frames-in-flight set to {}", FRAMES_IN_FLIGHT);
|
||||
}
|
||||
|
||||
logWindowDiagnostics(window);
|
||||
@@ -260,6 +268,10 @@ public final class Device {
|
||||
return driverName;
|
||||
}
|
||||
|
||||
public Thread getWindowThread() {
|
||||
return windowThread;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
return deviceName;
|
||||
}
|
||||
@@ -288,22 +300,89 @@ public final class Device {
|
||||
return (supportedShaderFormats & SDL_GPU_SHADERFORMAT_DXBC) != 0;
|
||||
}
|
||||
|
||||
public void setVSyncEnabled(boolean enabled) {
|
||||
private static final List<VSyncMode> ON_ORDER = List.of(VSyncMode.ON);
|
||||
private static final List<VSyncMode> OFF_ORDER = List.of(VSyncMode.OFF, VSyncMode.ON);
|
||||
private static final List<VSyncMode> MAILBOX_ORDER = List.of(VSyncMode.MAILBOX, VSyncMode.ON);
|
||||
|
||||
private static List<VSyncMode> presentModeOrder(VSyncMode preferred) {
|
||||
return switch (preferred) {
|
||||
case MAILBOX -> MAILBOX_ORDER;
|
||||
case OFF -> OFF_ORDER;
|
||||
default -> ON_ORDER;
|
||||
};
|
||||
}
|
||||
|
||||
public VSyncMode chooseVSyncMode(VSyncMode preferred) {
|
||||
for (VSyncMode candidate : presentModeOrder(preferred)) {
|
||||
if (supportsVSyncMode(candidate)) return candidate;
|
||||
}
|
||||
return vsyncMode;
|
||||
}
|
||||
|
||||
public VSyncMode applyVSync(VSyncMode preferred) {
|
||||
if (device == 0 || !claimed) return vsyncMode;
|
||||
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;
|
||||
final List<VSyncMode> order = presentModeOrder(preferred);
|
||||
for (VSyncMode candidate : order) {
|
||||
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;
|
||||
}
|
||||
if (candidate != order.get(0)) {
|
||||
LOG.warn("Present mode {} is not supported by this window; using {} instead", order.get(0), candidate);
|
||||
}
|
||||
vsyncMode = candidate;
|
||||
LOG.info("SDL present mode {} (preferred={}), refresh {}Hz, {}, video={}, framesInFlight={}, supported: vsync={} immediate={} mailbox={}",
|
||||
candidate, preferred, getDisplayRefreshRateHz(),
|
||||
(SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? "fullscreen" : "windowed",
|
||||
SDL_GetCurrentVideoDriver(), FRAMES_IN_FLIGHT,
|
||||
supportsVSyncMode(VSyncMode.ON), supportsVSyncMode(VSyncMode.OFF), supportsVSyncMode(VSyncMode.MAILBOX));
|
||||
return candidate;
|
||||
}
|
||||
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);
|
||||
return;
|
||||
LOG.warn("No usable present mode for preference {}; keeping {}", preferred, vsyncMode);
|
||||
return vsyncMode;
|
||||
}
|
||||
|
||||
public boolean supportsVSyncMode(VSyncMode mode) {
|
||||
return device != 0 && claimed && SDL_WindowSupportsGPUPresentMode(device, Display.getWindow(), toSdl(mode));
|
||||
}
|
||||
|
||||
public long getClaimedWindow() {
|
||||
return device != 0 && claimed ? Display.getWindow() : 0L;
|
||||
}
|
||||
|
||||
public boolean wasWindowResized() {
|
||||
return getClaimedWindow() != 0L && Display.wasResized();
|
||||
}
|
||||
|
||||
public long getWindowSizeInPixels() {
|
||||
final long window = getClaimedWindow();
|
||||
if (window == 0) return 0L;
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
final IntBuffer w = stack.mallocInt(1);
|
||||
final IntBuffer h = stack.mallocInt(1);
|
||||
SDL_GetWindowSizeInPixels(window, w, h);
|
||||
return ((long) w.get(0) << 32) | (h.get(0) & 0xFFFFFFFFL);
|
||||
}
|
||||
presentMode = mode;
|
||||
LOG.info("SDL present mode set to {}", 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();
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.MemoryAccess;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.ThreadRegistry;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.UploadArena;
|
||||
@@ -16,16 +17,19 @@ import org.lwjgl.PointerBuffer;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.lwjgl.sdl.SDLError;
|
||||
import org.lwjgl.sdl.SDL_FColor;
|
||||
import org.lwjgl.sdl.SDL_GPUBlitInfo;
|
||||
import org.lwjgl.sdl.SDL_GPUBlitRegion;
|
||||
import org.lwjgl.sdl.SDL_GPUColorTargetInfo;
|
||||
import org.lwjgl.sdl.SDL_GPUDepthStencilTargetInfo;
|
||||
import org.lwjglx.opengl.Display;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.lwjgl.sdl.SDLGPU.*;
|
||||
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
|
||||
import static org.lwjgl.system.MemoryStack.*;
|
||||
|
||||
/**
|
||||
@@ -37,7 +41,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;
|
||||
@@ -48,19 +51,17 @@ public final class FrameManager {
|
||||
public long renderPass;
|
||||
public long copyPass;
|
||||
public boolean computePassOpen;
|
||||
public long swapchainTexture;
|
||||
public int swapchainWidth;
|
||||
public int swapchainHeight;
|
||||
public boolean frameActive;
|
||||
public long currentColorTarget;
|
||||
public long currentDepthTarget;
|
||||
public long activeLayoutHash;
|
||||
public long frameNumber;
|
||||
public boolean clearedThisFrame;
|
||||
public boolean swapchainUsedThisFrame;
|
||||
public boolean swapchainAcquiredThisFrame;
|
||||
public boolean depthClearedThisFrame;
|
||||
public boolean fbo0UsedThisFrame;
|
||||
public boolean presentedThisFrame;
|
||||
public boolean swapchainUnavailable;
|
||||
public boolean skipPresent;
|
||||
public boolean windowThreadChecked;
|
||||
public long renderPassGeneration;
|
||||
|
||||
public boolean wantFenceOnNextSubmit;
|
||||
@@ -82,13 +83,17 @@ public final class FrameManager {
|
||||
public int batchBreakSamplerThisFrame;
|
||||
public int stateAppliesThisFrame;
|
||||
public long acquireWaitNanosThisFrame;
|
||||
public volatile long lastFrameGateNanos;
|
||||
public volatile long lastFrameGateEndNanos;
|
||||
public int midFrameSubmitsThisFrame;
|
||||
public int mipGensThisFrame;
|
||||
public int mipGenSubmitsThisFrame;
|
||||
public int uploadFlushSubmitsThisFrame;
|
||||
public int blitsThisFrame;
|
||||
public int presentSkipsThisFrame;
|
||||
public int emptyFramesThisFrame;
|
||||
public int droppedDrawsThisFrame;
|
||||
public int computeBatchJoinsThisFrame;
|
||||
public boolean drawableStarved;
|
||||
public int clearPassesThisFrame;
|
||||
public int swapchainPassesThisFrame;
|
||||
public int fboPassesThisFrame;
|
||||
@@ -144,42 +149,53 @@ public final class FrameManager {
|
||||
|
||||
private volatile boolean globalSyncAtNextBeginFrame;
|
||||
|
||||
private final CopyOnWriteArrayList<FrameState> registeredFrames = new CopyOnWriteArrayList<>();
|
||||
private final ThreadRegistry<FrameState> registeredFrames = new ThreadRegistry<>(new FrameState[0]);
|
||||
|
||||
private volatile FrameState soleFrame;
|
||||
|
||||
private final ThreadLocal<FrameState> tlFrame = ThreadLocal.withInitial(() -> {
|
||||
final FrameState f = new FrameState();
|
||||
registeredFrames.add(f);
|
||||
refreshSoleFrame();
|
||||
return f;
|
||||
});
|
||||
|
||||
private void refreshSoleFrame() {
|
||||
soleFrame = registeredFrames.size() == 1 ? registeredFrames.get(0) : null;
|
||||
}
|
||||
private final ThreadLocal<FrameState> tlFrame = new ThreadLocal<>();
|
||||
|
||||
public FrameState frame() {
|
||||
final FrameState f = soleFrame;
|
||||
if (f != null && f.owner == Thread.currentThread()) return f;
|
||||
return tlFrame.get();
|
||||
final FrameState sole = registeredFrames.sole();
|
||||
if (sole != null && sole.owner == Thread.currentThread()) return sole;
|
||||
final FrameState f = tlFrame.get();
|
||||
return f != null ? f : registerFrame();
|
||||
}
|
||||
|
||||
private FrameState registerFrame() {
|
||||
final FrameState f = new FrameState();
|
||||
tlFrame.set(f);
|
||||
registeredFrames.add(f);
|
||||
return f;
|
||||
}
|
||||
|
||||
public int registeredFrameCount() {
|
||||
return registeredFrames.size();
|
||||
}
|
||||
|
||||
public FrameState busiestFrame() {
|
||||
final FrameState[] frames = registeredFrames.snapshot();
|
||||
FrameState best = null;
|
||||
for (final FrameState f : registeredFrames) {
|
||||
for (final FrameState f : frames) {
|
||||
if (best == null || f.frameNumber > best.frameNumber) best = f;
|
||||
}
|
||||
return best != null ? best : frame();
|
||||
}
|
||||
|
||||
public static final long SWAPCHAIN_LAYOUT_HASH = 0x5CA1AB1E5CA1AB1EL;
|
||||
public static final long FBO0_LAYOUT_HASH = 0x5CA1AB1E5CA1AB1EL;
|
||||
|
||||
private final OffscreenTarget finalTarget = new OffscreenTarget();
|
||||
|
||||
public OffscreenTarget finalTarget() { return finalTarget; }
|
||||
|
||||
public void destroyFinalTarget() {
|
||||
if (resourceManager != null) finalTarget.destroy(resourceManager);
|
||||
}
|
||||
|
||||
public static final long MAX_PENDING_UPLOAD_BYTES = 16L * 1024L * 1024L;
|
||||
public static final int MAX_PENDING_UPLOAD_COMMANDS = 256;
|
||||
|
||||
private Runnable beforeEndCopyPass;
|
||||
|
||||
|
||||
public FrameManager(Device device) {
|
||||
this.device = device;
|
||||
}
|
||||
@@ -188,18 +204,12 @@ public final class FrameManager {
|
||||
this.resourceManager = resourceManager;
|
||||
}
|
||||
|
||||
private static volatile boolean threadLogged;
|
||||
|
||||
public void beginFrame() {
|
||||
final FrameState f = frame();
|
||||
if (f.frameActive) {
|
||||
LOG.warn("beginFrame() called while frame already active on thread {}", Thread.currentThread().getName());
|
||||
return;
|
||||
}
|
||||
if (!threadLogged) {
|
||||
threadLogged = true;
|
||||
LOG.info("First beginFrame() on thread={}; must match the SDL window-creating thread", Thread.currentThread().getName());
|
||||
}
|
||||
|
||||
flushPendingUploadCommandBuffer(f);
|
||||
if (globalSyncAtNextBeginFrame) {
|
||||
@@ -209,23 +219,15 @@ 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());
|
||||
}
|
||||
|
||||
f.swapchainTexture = 0;
|
||||
f.swapchainWidth = 0;
|
||||
f.swapchainHeight = 0;
|
||||
f.swapchainAcquiredThisFrame = false;
|
||||
f.swapchainUnavailable = false;
|
||||
f.presentedThisFrame = false;
|
||||
|
||||
maintainFinalTarget();
|
||||
|
||||
f.frameActive = true;
|
||||
f.renderPass = 0;
|
||||
@@ -233,23 +235,72 @@ public final class FrameManager {
|
||||
f.currentDepthTarget = 0;
|
||||
f.frameNumber++;
|
||||
f.clearedThisFrame = false;
|
||||
f.swapchainUsedThisFrame = false;
|
||||
f.skipPresent = false;
|
||||
f.depthClearedThisFrame = false;
|
||||
f.fbo0UsedThisFrame = false;
|
||||
}
|
||||
|
||||
private static final long FINAL_TARGET_SIZE_POLL_NANOS = 1_000_000_000L;
|
||||
|
||||
private long finalTargetPollNanos;
|
||||
private volatile Thread mainThread;
|
||||
|
||||
public void setMainThread(Thread thread) {
|
||||
this.mainThread = thread;
|
||||
}
|
||||
|
||||
// Resizing releases a texture an in-flight present may still reference, so drain before ensureSize.
|
||||
private void maintainFinalTarget() {
|
||||
if (resourceManager == null) return;
|
||||
final Thread owner = mainThread;
|
||||
if (owner != null && Thread.currentThread() != owner) return;
|
||||
|
||||
final long now = System.nanoTime();
|
||||
final boolean poll = finalTargetPollNanos == 0L || now - finalTargetPollNanos >= FINAL_TARGET_SIZE_POLL_NANOS;
|
||||
if (!poll && !device.wasWindowResized() && finalTarget.isReady()) return;
|
||||
if (poll) finalTargetPollNanos = now;
|
||||
|
||||
final long size = device.getWindowSizeInPixels();
|
||||
if (size == 0L) return;
|
||||
final int width = (int) (size >> 32);
|
||||
final int height = (int) size;
|
||||
if (finalTarget.isReady() && width == finalTarget.width() && height == finalTarget.height()) return;
|
||||
|
||||
if (presenter != null) presenter.drain();
|
||||
finalTarget.ensureSize(device, resourceManager, width, height);
|
||||
}
|
||||
|
||||
public void markFrameEmpty(FrameState f) {
|
||||
f.skipPresent = true;
|
||||
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.activeLayoutHash == FBO0_LAYOUT_HASH;
|
||||
}
|
||||
|
||||
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
|
||||
if (f.renderPass != 0 && f.currentColorTarget == f.swapchainTexture) {
|
||||
return;
|
||||
public boolean ensureFbo0RenderPass(FrameState f, ContextState st) {
|
||||
final boolean clearColor = st.pendingSwapchainClear;
|
||||
if (swapchainClearNeedsPassBreak(st, f)) {
|
||||
endRenderPassIfActive(f);
|
||||
}
|
||||
final boolean applied = ensureFbo0RenderPass(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 ensureFbo0RenderPass(FrameState f, float clearR, float clearG, float clearB, float clearA, boolean clear, boolean clearDepth, float depthValue, boolean clearStencil, int stencilValue) {
|
||||
if (!finalTarget.isReady() || f.commandBuffer == 0) return false;
|
||||
if (f.renderPass != 0 && f.activeLayoutHash == FBO0_LAYOUT_HASH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
endActiveEncoders(f);
|
||||
@@ -258,10 +309,16 @@ public final class FrameManager {
|
||||
clear = true;
|
||||
}
|
||||
|
||||
final long depthTexture = resourceManager != null ? resourceManager.getOrCreateSwapchainDepthStencil(finalTarget.width(), finalTarget.height()) : 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();
|
||||
MemoryAccess.putAddress(ctAddr + SDL_GPUColorTargetInfo.TEXTURE, f.swapchainTexture);
|
||||
MemoryAccess.putAddress(ctAddr + SDL_GPUColorTargetInfo.TEXTURE, finalTarget.colorTexture());
|
||||
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.LOAD_OP, clear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
|
||||
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
|
||||
|
||||
@@ -274,19 +331,35 @@ 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.activeLayoutHash = SWAPCHAIN_LAYOUT_HASH;
|
||||
f.swapchainUsedThisFrame = true;
|
||||
f.currentColorTarget = finalTarget.colorTexture();
|
||||
f.currentDepthTarget = depthTexture;
|
||||
f.activeLayoutHash = FBO0_LAYOUT_HASH;
|
||||
f.fbo0UsedThisFrame = true;
|
||||
finalTarget.markContent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -336,6 +409,8 @@ public final class FrameManager {
|
||||
|
||||
public void noteMipGen() { frame().mipGensThisFrame++; }
|
||||
|
||||
public void noteMipGenSubmit() { frame().mipGenSubmitsThisFrame++; }
|
||||
|
||||
public void noteBlit() { frame().blitsThisFrame++; }
|
||||
|
||||
public void noteClearPass() { frame().clearPassesThisFrame++; }
|
||||
@@ -425,17 +500,14 @@ public final class FrameManager {
|
||||
}
|
||||
|
||||
public void requestFlushOnAllRegisteredFrames() {
|
||||
final int n = registeredFrames.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
registeredFrames.get(i).flushRequested = true;
|
||||
for (final FrameState f : registeredFrames.snapshot()) {
|
||||
f.flushRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void requestSyncFlushOnAllRegisteredFrames() {
|
||||
globalSyncAtNextBeginFrame = true;
|
||||
final int n = registeredFrames.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
final FrameState f = registeredFrames.get(i);
|
||||
for (final FrameState f : registeredFrames.snapshot()) {
|
||||
f.flushRequested = true;
|
||||
f.syncOnNextFlush = true;
|
||||
}
|
||||
@@ -462,10 +534,6 @@ public final class FrameManager {
|
||||
endCopyPassIfActive(f);
|
||||
endRenderPassIfActive(f);
|
||||
|
||||
if (!f.swapchainAcquiredThisFrame && !f.swapchainUnavailable && !f.skipPresent && f.commandBuffer != 0) {
|
||||
ensureSwapchainAcquired(f);
|
||||
}
|
||||
|
||||
if (f.commandBuffer != 0) {
|
||||
Tracy.beginZone(Z_SDL_SUBMIT);
|
||||
try {
|
||||
@@ -486,9 +554,14 @@ public final class FrameManager {
|
||||
f.commandBuffer = 0;
|
||||
}
|
||||
|
||||
f.drawableStarved = f.presentSkipsThisFrame > 0;
|
||||
drainWindowPresentCounters(f);
|
||||
|
||||
if (Tracy.ENABLED && resourceManager != null) {
|
||||
SdlFramePlots.emit(f, resourceManager);
|
||||
}
|
||||
|
||||
f.midFrameSubmitsThisFrame = 0;
|
||||
|
||||
if (Tracy.ENABLED && resourceManager != null) SdlFramePlots.emit(f, resourceManager);
|
||||
f.lastPlottedRenderPassGen = f.renderPassGeneration;
|
||||
f.computePassesThisFrame = 0;
|
||||
f.submitsThisFrame = 0;
|
||||
@@ -506,6 +579,8 @@ public final class FrameManager {
|
||||
f.stateAppliesThisFrame = 0;
|
||||
f.acquireWaitNanosThisFrame = 0;
|
||||
f.mipGensThisFrame = 0;
|
||||
f.mipGenSubmitsThisFrame = 0;
|
||||
f.uploadFlushSubmitsThisFrame = 0;
|
||||
f.blitsThisFrame = 0;
|
||||
f.clearPassesThisFrame = 0;
|
||||
f.swapchainPassesThisFrame = 0;
|
||||
@@ -520,9 +595,6 @@ public final class FrameManager {
|
||||
f.computeBatchJoinsThisFrame = 0;
|
||||
|
||||
f.frameActive = false;
|
||||
f.swapchainTexture = 0;
|
||||
|
||||
if (afterPresent != null) afterPresent.run();
|
||||
}
|
||||
|
||||
public void submitMidFrame() {
|
||||
@@ -546,6 +618,7 @@ public final class FrameManager {
|
||||
Tracy.endZone();
|
||||
}
|
||||
f.submitsThisFrame++;
|
||||
f.midFrameSubmitsThisFrame++;
|
||||
f.commandBuffer = 0;
|
||||
if (f.frameActive) {
|
||||
reacquireCommandBuffer();
|
||||
@@ -557,6 +630,7 @@ public final class FrameManager {
|
||||
if (f.frameActive) {
|
||||
endFrame();
|
||||
}
|
||||
presentFinalTarget();
|
||||
beginFrame();
|
||||
}
|
||||
|
||||
@@ -633,6 +707,7 @@ public final class FrameManager {
|
||||
device.reportGpuFailure("submit pending upload command buffer");
|
||||
}
|
||||
f.submitsThisFrame++;
|
||||
f.uploadFlushSubmitsThisFrame++;
|
||||
f.pendingUploadCommandBuffer = 0;
|
||||
f.pendingUploadBytes = 0;
|
||||
f.pendingUploadCommands = 0;
|
||||
@@ -667,22 +742,16 @@ public final class FrameManager {
|
||||
|
||||
public long getActiveLayoutHash(FrameState f) { return f.activeLayoutHash; }
|
||||
|
||||
public long getSwapchainTexture() {
|
||||
final FrameState f = frame();
|
||||
ensureSwapchainAcquired(f);
|
||||
return f.swapchainTexture;
|
||||
public long getFbo0Texture() {
|
||||
return finalTarget.colorTexture();
|
||||
}
|
||||
|
||||
public int getSwapchainWidth() {
|
||||
final FrameState f = frame();
|
||||
ensureSwapchainAcquired(f);
|
||||
return f.swapchainWidth;
|
||||
public int getFbo0Width() {
|
||||
return finalTarget.width();
|
||||
}
|
||||
|
||||
public int getSwapchainHeight() {
|
||||
final FrameState f = frame();
|
||||
ensureSwapchainAcquired(f);
|
||||
return f.swapchainHeight;
|
||||
public int getFbo0Height() {
|
||||
return finalTarget.height();
|
||||
}
|
||||
|
||||
public int getSwapchainFormat() {
|
||||
@@ -717,8 +786,8 @@ public final class FrameManager {
|
||||
return frame().frameNumber;
|
||||
}
|
||||
|
||||
public boolean isSwapchainUsedThisFrame() {
|
||||
return frame().swapchainUsedThisFrame;
|
||||
public boolean isFbo0UsedThisFrame() {
|
||||
return frame().fbo0UsedThisFrame;
|
||||
}
|
||||
|
||||
public void reacquireCommandBuffer() {
|
||||
@@ -731,24 +800,18 @@ public final class FrameManager {
|
||||
f.currentDepthTarget = 0;
|
||||
|
||||
if (f.frameActive) {
|
||||
final boolean wasAcquired = f.swapchainAcquiredThisFrame;
|
||||
f.swapchainAcquiredThisFrame = false;
|
||||
f.swapchainTexture = 0;
|
||||
if (wasAcquired && !ensureSwapchainAcquired(f)) {
|
||||
LOG.warn("Failed to re-acquire swapchain after readback: {}", SDLError.SDL_GetError());
|
||||
}
|
||||
f.clearedThisFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void releaseThreadState() {
|
||||
final FrameState f = frame();
|
||||
final FrameState f = tlFrame.get();
|
||||
if (f == null) return;
|
||||
if (resourceManager != null) resourceManager.flushUploadArena(f);
|
||||
flushPendingUploadCommandBuffer(f);
|
||||
releaseFrameStateResources(f);
|
||||
registeredFrames.remove(f);
|
||||
tlFrame.remove();
|
||||
refreshSoleFrame();
|
||||
registeredFrames.remove(f);
|
||||
}
|
||||
|
||||
private void releaseFrameStateResources(FrameState f) {
|
||||
@@ -786,25 +849,100 @@ public final class FrameManager {
|
||||
}
|
||||
|
||||
public void releaseAllRegisteredFrames() {
|
||||
for (FrameState f : registeredFrames) {
|
||||
for (final FrameState f : registeredFrames.snapshot()) {
|
||||
releaseFrameStateResources(f);
|
||||
}
|
||||
registeredFrames.clear();
|
||||
soleFrame = null;
|
||||
}
|
||||
|
||||
public boolean ensureSwapchainAcquired() {
|
||||
return ensureSwapchainAcquired(frame());
|
||||
public long lastFrameGateNanos() {
|
||||
return frame().lastFrameGateNanos;
|
||||
}
|
||||
|
||||
public boolean ensureSwapchainAcquired(FrameState f) {
|
||||
if (f.swapchainAcquiredThisFrame) return true;
|
||||
if (f.swapchainUnavailable) return false;
|
||||
if (!f.frameActive || f.commandBuffer == 0) return false;
|
||||
public long lastFrameGateEndNanos() {
|
||||
return frame().lastFrameGateEndNanos;
|
||||
}
|
||||
|
||||
endActiveEncoders(f);
|
||||
private Presenter presenter;
|
||||
|
||||
final long t0 = Tracy.ENABLED ? System.nanoTime() : 0L;
|
||||
public void setPresenter(Presenter presenter) {
|
||||
this.presenter = presenter;
|
||||
}
|
||||
|
||||
private final FrameState windowFrame = new FrameState();
|
||||
|
||||
public FrameState windowFrame() {
|
||||
return windowFrame;
|
||||
}
|
||||
|
||||
public void presentFinalTarget() {
|
||||
if (!finalTarget.hasContent()) return;
|
||||
final long texture = finalTarget.colorTexture();
|
||||
final int width = finalTarget.width();
|
||||
final int height = finalTarget.height();
|
||||
final Presenter p = presenter;
|
||||
if (p != null && p.isEngaged()) {
|
||||
final FrameState f = frame();
|
||||
if (f.presentedThisFrame) return;
|
||||
f.presentedThisFrame = true;
|
||||
p.requestPresent(texture, width, height);
|
||||
} else {
|
||||
presentBlit(frame(), texture, width, height, SDL_FLIP_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
private final AtomicLong windowAcquireWaitNanos = new AtomicLong();
|
||||
private final AtomicInteger windowPresentSkips = new AtomicInteger();
|
||||
|
||||
private void drainWindowPresentCounters(FrameState f) {
|
||||
if (f == windowFrame) return;
|
||||
final long acquire = windowAcquireWaitNanos.getAndSet(0L);
|
||||
if (acquire != 0L) {
|
||||
f.acquireWaitNanosThisFrame += acquire;
|
||||
f.lastFrameGateNanos = windowFrame.lastFrameGateNanos;
|
||||
f.lastFrameGateEndNanos = windowFrame.lastFrameGateEndNanos;
|
||||
}
|
||||
f.presentSkipsThisFrame += windowPresentSkips.getAndSet(0);
|
||||
}
|
||||
|
||||
private void noteAcquireWait(FrameState f, long waitNanos) {
|
||||
if (f == windowFrame) windowAcquireWaitNanos.addAndGet(waitNanos);
|
||||
else f.acquireWaitNanosThisFrame += waitNanos;
|
||||
}
|
||||
|
||||
private void notePresentSkip(FrameState f) {
|
||||
if (f == windowFrame) windowPresentSkips.incrementAndGet();
|
||||
else f.presentSkipsThisFrame++;
|
||||
}
|
||||
|
||||
public void presentOnWindowThread(long srcTexture, int srcW, int srcH) {
|
||||
presentBlit(windowFrame, srcTexture, srcW, srcH, SDL_FLIP_NONE);
|
||||
windowFrame.presentedThisFrame = false;
|
||||
windowFrame.swapchainUnavailable = false;
|
||||
}
|
||||
|
||||
public boolean presentBlit(FrameState f, long srcTexture, int srcW, int srcH, int flipMode) {
|
||||
if (srcTexture == 0 || srcW <= 0 || srcH <= 0) return false;
|
||||
if (f.presentedThisFrame || f.swapchainUnavailable) return false;
|
||||
|
||||
if (!f.windowThreadChecked) {
|
||||
f.windowThreadChecked = true;
|
||||
final Thread self = Thread.currentThread();
|
||||
final Thread windowThread = device.getWindowThread();
|
||||
if (windowThread != null && windowThread != self) {
|
||||
LOG.warn("Swapchain acquired on '{}' but the SDL window was created on '{}'; SDL requires acquire on the window-creating thread", self.getName(), windowThread.getName());
|
||||
} else {
|
||||
LOG.info("First swapchain acquire on thread={}", self.getName());
|
||||
}
|
||||
}
|
||||
|
||||
final long window = device.getClaimedWindow();
|
||||
if (window == 0) return false;
|
||||
|
||||
final long cb = SDL_AcquireGPUCommandBuffer(device.getDevice());
|
||||
if (cb == 0) return false;
|
||||
|
||||
final long t0 = System.nanoTime();
|
||||
final long tex;
|
||||
final int w;
|
||||
final int h;
|
||||
@@ -815,7 +953,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(cb, window, pTexture, pWidth, pHeight);
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
@@ -823,25 +961,43 @@ public final class FrameManager {
|
||||
w = pWidth.get(0);
|
||||
h = pHeight.get(0);
|
||||
} catch (RuntimeException re) {
|
||||
if (Tracy.ENABLED) f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
|
||||
SDL_CancelGPUCommandBuffer(cb);
|
||||
f.swapchainUnavailable = true;
|
||||
throw re;
|
||||
}
|
||||
if (Tracy.ENABLED) f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
|
||||
final long tEnd = System.nanoTime();
|
||||
f.lastFrameGateNanos = tEnd - t0;
|
||||
f.lastFrameGateEndNanos = tEnd;
|
||||
noteAcquireWait(f, tEnd - t0);
|
||||
|
||||
if (!callOk) {
|
||||
if (!callOk || tex == 0) {
|
||||
SDL_CancelGPUCommandBuffer(cb);
|
||||
f.swapchainUnavailable = true;
|
||||
notePresentSkip(f);
|
||||
return false;
|
||||
}
|
||||
if (tex == 0) {
|
||||
f.swapchainUnavailable = true;
|
||||
f.presentSkipsThisFrame++;
|
||||
|
||||
try (MemoryStack stack = stackPush()) {
|
||||
final long info = stack.ncalloc(SDL_GPUBlitInfo.ALIGNOF, 1, SDL_GPUBlitInfo.SIZEOF);
|
||||
writeBlitRegion(info + SDL_GPUBlitInfo.SOURCE, srcTexture, srcW, srcH);
|
||||
writeBlitRegion(info + SDL_GPUBlitInfo.DESTINATION, tex, w, h);
|
||||
MemoryAccess.putInt(info + SDL_GPUBlitInfo.FILTER, SDL_GPU_FILTER_LINEAR);
|
||||
MemoryAccess.putInt(info + SDL_GPUBlitInfo.FLIP_MODE, flipMode);
|
||||
nSDL_BlitGPUTexture(cb, info);
|
||||
}
|
||||
|
||||
if (!SDL_SubmitGPUCommandBuffer(cb)) {
|
||||
device.reportGpuFailure("submit present blit");
|
||||
return false;
|
||||
}
|
||||
f.swapchainTexture = tex;
|
||||
f.swapchainWidth = w;
|
||||
f.swapchainHeight = h;
|
||||
f.swapchainAcquiredThisFrame = true;
|
||||
f.presentedThisFrame = true;
|
||||
if (afterPresent != null) afterPresent.run();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void writeBlitRegion(long region, long texture, int w, int h) {
|
||||
MemoryAccess.putAddress(region + SDL_GPUBlitRegion.TEXTURE, texture);
|
||||
MemoryAccess.putInt(region + SDL_GPUBlitRegion.W, w);
|
||||
MemoryAccess.putInt(region + SDL_GPUBlitRegion.H, h);
|
||||
}
|
||||
}
|
||||
+14
-3
@@ -1,7 +1,6 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.splash;
|
||||
package com.gtnewhorizons.angelica.sdlgpu.frame;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
@@ -9,13 +8,14 @@ import org.lwjgl.opengl.GL11;
|
||||
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
|
||||
public final class SplashOffscreenTarget {
|
||||
public final class OffscreenTarget {
|
||||
|
||||
private int fboGlId;
|
||||
private int colorTexGlId;
|
||||
private long colorTexHandle;
|
||||
private int width;
|
||||
private int height;
|
||||
private boolean hasContent;
|
||||
|
||||
public void create(Device device, ResourceManager rm, int width, int height, int swapchainSdlFormat) {
|
||||
this.width = width;
|
||||
@@ -37,10 +37,20 @@ public final class SplashOffscreenTarget {
|
||||
fbo.cachedFormatsDirty = true;
|
||||
}
|
||||
|
||||
public void ensureSize(Device device, ResourceManager rm, int width, int height) {
|
||||
if (width <= 0 || height <= 0) return;
|
||||
if (colorTexHandle != 0 && width == this.width && height == this.height) return;
|
||||
destroy(rm);
|
||||
create(device, rm, width, height, device.getSwapchainTextureFormat());
|
||||
}
|
||||
|
||||
public int fboId() { return fboGlId; }
|
||||
public long colorTexture() { return colorTexHandle; }
|
||||
public int width() { return width; }
|
||||
public int height() { return height; }
|
||||
public boolean isReady() { return colorTexHandle != 0; }
|
||||
public boolean hasContent() { return hasContent; }
|
||||
public void markContent() { hasContent = true; }
|
||||
|
||||
public boolean isFor(ContextState st) {
|
||||
return fboGlId != 0 && st.boundFboId == fboGlId;
|
||||
@@ -56,5 +66,6 @@ public final class SplashOffscreenTarget {
|
||||
colorTexGlId = 0;
|
||||
colorTexHandle = 0;
|
||||
}
|
||||
hasContent = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.frame;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
public final class Presenter {
|
||||
|
||||
private final FrameManager frameManager;
|
||||
private final Executor windowThreadExecutor;
|
||||
private final Semaphore pending = new Semaphore(1);
|
||||
|
||||
public Presenter(FrameManager frameManager, Executor windowThreadExecutor) {
|
||||
this.frameManager = frameManager;
|
||||
this.windowThreadExecutor = windowThreadExecutor;
|
||||
}
|
||||
|
||||
public boolean isEngaged() {
|
||||
return windowThreadExecutor != null && GLStateManager.isSplashComplete();
|
||||
}
|
||||
|
||||
public void requestPresent(long srcTexture, int srcW, int srcH) {
|
||||
pending.acquireUninterruptibly();
|
||||
boolean submitted = false;
|
||||
try {
|
||||
windowThreadExecutor.execute(() -> {
|
||||
try {
|
||||
frameManager.presentOnWindowThread(srcTexture, srcW, srcH);
|
||||
} finally {
|
||||
pending.release();
|
||||
}
|
||||
});
|
||||
submitted = true;
|
||||
} finally {
|
||||
if (!submitted) pending.release();
|
||||
}
|
||||
}
|
||||
|
||||
public void drain() {
|
||||
pending.acquireUninterruptibly();
|
||||
pending.release();
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ final class SdlFramePlots {
|
||||
private static final long PLOT_PRESENT_TO_PRESENT_US = Tracy.plotHandle("sdl.presentToPresentUs");
|
||||
private static final long PLOT_FRAMES_IN_FLIGHT = Tracy.plotHandle("sdl.framesInFlight");
|
||||
private static final long PLOT_MIP_GENS = Tracy.plotHandle("sdl.mipGens");
|
||||
private static final long PLOT_MIP_GEN_SUBMITS = Tracy.plotHandle("sdl.mipGenSubmits");
|
||||
private static final long PLOT_UPLOAD_FLUSH_SUBMITS = Tracy.plotHandle("sdl.uploadFlushSubmits");
|
||||
private static final long PLOT_BLIT_FALLBACKS = Tracy.plotHandle("sdl.blitFallbacks");
|
||||
private static final long PLOT_PRESENT_SKIPS = Tracy.plotHandle("sdl.presentSkips");
|
||||
private static final long PLOT_EMPTY_FRAMES = Tracy.plotHandle("sdl.emptyFrames");
|
||||
@@ -54,6 +56,8 @@ final class SdlFramePlots {
|
||||
private static final long PLOT_BUFFER_POOL_HITS = Tracy.plotHandle("sdl.bufferPoolHits");
|
||||
private static final long PLOT_PIPELINE_KEY_RECOMPUTES = Tracy.plotHandle("sdl.pipelineKeyRecomputes");
|
||||
private static final long PLOT_PIPELINE_FAST_PATH_HITS = Tracy.plotHandle("sdl.pipelineFastPathHits");
|
||||
private static final long PLOT_MID_FRAME_SUBMITS = Tracy.plotHandle("sdl.midFrameSubmits");
|
||||
private static final long PLOT_GATE_US = Tracy.plotHandle("sdl.gateUs");
|
||||
|
||||
private static final int[] contextCounters = new int[SDLGPURenderBackend.CTR_COUNT];
|
||||
|
||||
@@ -97,11 +101,15 @@ 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_MIP_GEN_SUBMITS, f.mipGenSubmitsThisFrame);
|
||||
Tracy.plotInt(PLOT_UPLOAD_FLUSH_SUBMITS, f.uploadFlushSubmitsThisFrame);
|
||||
Tracy.plotInt(PLOT_BLIT_FALLBACKS, f.blitsThisFrame);
|
||||
Tracy.plotInt(PLOT_PRESENT_SKIPS, f.presentSkipsThisFrame);
|
||||
Tracy.plotInt(PLOT_EMPTY_FRAMES, f.emptyFramesThisFrame);
|
||||
Tracy.plotInt(PLOT_DROPPED_DRAWS, f.droppedDrawsThisFrame);
|
||||
Tracy.plotInt(PLOT_MID_FRAME_SUBMITS, f.midFrameSubmitsThisFrame);
|
||||
Tracy.plotInt(PLOT_GATE_US, f.lastFrameGateNanos / 1000);
|
||||
}
|
||||
}
|
||||
+78
-35
@@ -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.ensureFbo0RenderPass(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;
|
||||
|
||||
|
||||
+21
-1
@@ -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);
|
||||
|
||||
+55
-30
@@ -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]; }
|
||||
@@ -63,7 +64,7 @@ public final class FboState {
|
||||
h = Hashing.fmix64(h, db);
|
||||
}
|
||||
h = Hashing.fmix64(h, depthTexture);
|
||||
if (h == 0L || h == FrameManager.SWAPCHAIN_LAYOUT_HASH) h = 0xD1CE5EEDL;
|
||||
if (h == 0L || h == FrameManager.FBO0_LAYOUT_HASH) h = 0xD1CE5EEDL;
|
||||
structuralLayoutHash = h;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+128
-5
@@ -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.MAX_FRAMES_IN_FLIGHT) {
|
||||
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.MAX_FRAMES_IN_FLIGHT) {
|
||||
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;
|
||||
|
||||
@@ -101,19 +101,11 @@ public final class TextureOps {
|
||||
|
||||
public void readbackTexture(long texHandle, int x, int y, int w, int h, int level, ByteBuffer output) {
|
||||
if (texHandle == 0 || output == null) return;
|
||||
frameManager.endCopyPassIfActive();
|
||||
frameManager.endRenderPassIfActive();
|
||||
frameManager.submitMidFrame();
|
||||
|
||||
if (texHandle == frameManager.getSwapchainTexture()) {
|
||||
final long cb = frameManager.getCommandBuffer();
|
||||
if (cb == 0) return;
|
||||
resourceManager.downloadFromTexture(cb, texHandle, x, y, w, h, level, output);
|
||||
frameManager.reacquireCommandBuffer();
|
||||
} else {
|
||||
final long cb = SDL_AcquireGPUCommandBuffer(device.getDevice());
|
||||
if (cb == 0) return;
|
||||
resourceManager.downloadFromTexture(cb, texHandle, x, y, w, h, level, output);
|
||||
}
|
||||
final long cb = SDL_AcquireGPUCommandBuffer(device.getDevice());
|
||||
if (cb == 0) return;
|
||||
resourceManager.downloadFromTexture(cb, texHandle, x, y, w, h, level, output);
|
||||
}
|
||||
|
||||
public void copyTexSubImageImpl(ContextState st, int destGlId, int level, int xoffset, int yoffset, int x, int y, int width, int height) {
|
||||
@@ -210,6 +202,7 @@ public final class TextureOps {
|
||||
if (!SDL_SubmitGPUCommandBuffer(uploadCb)) {
|
||||
device.reportGpuFailure("submit mipmap generation CB");
|
||||
}
|
||||
frameManager.noteMipGenSubmit();
|
||||
|
||||
if (f.commandBuffer == uploadCb) {
|
||||
f.commandBuffer = 0;
|
||||
|
||||
+45
-9
@@ -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));
|
||||
|
||||
+15
-11
@@ -31,11 +31,6 @@ public final class PerFrameBlockInjector {
|
||||
private PerFrameBlockInjector() {}
|
||||
|
||||
public static String inject(String source, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass) {
|
||||
final PerFrameUniformBlock[] blocks = new PerFrameUniformBlock[ShaderManager.BLOCK_COUNT];
|
||||
blocks[ShaderManager.BLOCK_PER_FRAME] = normalize(perFrame);
|
||||
blocks[ShaderManager.BLOCK_PER_PASS] = normalize(perPass);
|
||||
if (blocks[0] == null && blocks[1] == null) return source;
|
||||
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
@@ -43,8 +38,19 @@ public final class PerFrameBlockInjector {
|
||||
return source;
|
||||
}
|
||||
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
collectEdits(root, perFrame, perPass, edits);
|
||||
return edits.isEmpty() ? source : applyEdits(source, edits);
|
||||
}
|
||||
|
||||
public static void collectEdits(GLSLParser.Translation_unitContext root, PerFrameUniformBlock perFrame, PerFrameUniformBlock perPass, List<Edit> out) {
|
||||
final PerFrameUniformBlock[] blocks = new PerFrameUniformBlock[ShaderManager.BLOCK_COUNT];
|
||||
blocks[ShaderManager.BLOCK_PER_FRAME] = normalize(perFrame);
|
||||
blocks[ShaderManager.BLOCK_PER_PASS] = normalize(perPass);
|
||||
if (blocks[0] == null && blocks[1] == null) return;
|
||||
|
||||
final int insertAt = firstDeclarationStart(root);
|
||||
if (insertAt < 0) return source;
|
||||
if (insertAt < 0) return;
|
||||
|
||||
final Map<String, String> memberTypes = new LinkedHashMap<>();
|
||||
final Object2IntOpenHashMap<String> memberBlock = new Object2IntOpenHashMap<>();
|
||||
@@ -71,18 +77,16 @@ public final class PerFrameBlockInjector {
|
||||
if (referenced.contains(m.name())) { injected[b] = true; anyInjected = true; break; }
|
||||
}
|
||||
}
|
||||
if (!anyInjected) return source;
|
||||
if (!anyInjected) return;
|
||||
|
||||
final List<Edit> edits = new ArrayList<>(stripEdits.size() + 2);
|
||||
final StringBuilder blockDecls = new StringBuilder(128);
|
||||
for (int b = 0; b < blocks.length; b++) {
|
||||
if (injected[b]) appendBlockText(blockDecls, b, blocks[b].members(), shadowed);
|
||||
}
|
||||
edits.add(new Edit(insertAt, insertAt - 1, blockDecls.toString()));
|
||||
out.add(new Edit(insertAt, insertAt - 1, blockDecls.toString()));
|
||||
for (int i = 0; i < stripEdits.size(); i++) {
|
||||
if (injected[stripBlocks.getInt(i)]) edits.add(stripEdits.get(i));
|
||||
if (injected[stripBlocks.getInt(i)]) out.add(stripEdits.get(i));
|
||||
}
|
||||
return applyEdits(source, edits);
|
||||
}
|
||||
|
||||
private static PerFrameUniformBlock normalize(PerFrameUniformBlock block) {
|
||||
|
||||
@@ -188,19 +188,46 @@ public final class ShaderManager {
|
||||
return new PrewarmTransformResult(transformedSource, Set.of());
|
||||
}
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
PerFrameBlockInjector.collectEdits(root, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock, edits);
|
||||
}
|
||||
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(transformedSource, root, glShaderType, "prewarm", true, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
ClipZRemap.collectEdits(root, edits);
|
||||
}
|
||||
SamplerStripper.collectEdits(root, transformedSource, edits);
|
||||
String s = edits.isEmpty() ? transformedSource : GlslVulkanPreprocess.applyEdits(transformedSource, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
s = PerFrameBlockInjector.inject(s, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
final String s = edits.isEmpty() ? transformedSource : GlslVulkanPreprocess.applyEdits(transformedSource, edits);
|
||||
return new PrewarmTransformResult(s, meta.boolUniforms());
|
||||
}
|
||||
|
||||
public static PrewarmTransformResult applyPrewarmTransformsFull(String finalSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType) {
|
||||
final String header = finalSource.substring(0, headerLen);
|
||||
final String body = finalSource.substring(headerLen);
|
||||
final List<Edit> edits = new ArrayList<>();
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
PerFrameBlockInjector.collectEdits(bodyTree, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock, edits);
|
||||
}
|
||||
final GlslVulkanPreprocess.Metadata meta = GlslVulkanPreprocess.collectEdits(null, bodyTree, glShaderType, "prewarm", true, edits);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER) {
|
||||
ClipZRemap.collectEdits(bodyTree, edits);
|
||||
}
|
||||
SamplerStripper.collectEdits(bodyTree, body, edits);
|
||||
String outHeader = header;
|
||||
if (meta.needsSamplerless()) {
|
||||
final int nl = header.indexOf('\n');
|
||||
outHeader = header.substring(0, nl) + "\n" + GlslVulkanPreprocess.SAMPLERLESS_EXTENSION + header.substring(nl);
|
||||
}
|
||||
if (edits.isEmpty() && outHeader == header) {
|
||||
return new PrewarmTransformResult(finalSource, meta.boolUniforms());
|
||||
}
|
||||
return new PrewarmTransformResult(outHeader + GlslVulkanPreprocess.applyEdits(body, edits), meta.boolUniforms());
|
||||
}
|
||||
|
||||
public static void prewarmSpirv(String transformedSource, int glShaderType) {
|
||||
prewarmSpirv(transformedSource, null, 0, glShaderType);
|
||||
}
|
||||
|
||||
public static void prewarmSpirv(String transformedSource, GLSLParser.Translation_unitContext bodyTree, int headerLen, int glShaderType) {
|
||||
if (glShaderType != GL20.GL_VERTEX_SHADER && glShaderType != GL20.GL_FRAGMENT_SHADER
|
||||
&& glShaderType != GL43.GL_COMPUTE_SHADER && glShaderType != GL32.GL_GEOMETRY_SHADER) {
|
||||
return;
|
||||
@@ -211,7 +238,9 @@ public final class ShaderManager {
|
||||
if (PREWARM_CACHE.containsKey(key)) return;
|
||||
}
|
||||
|
||||
final PrewarmTransformResult pre = applyPrewarmTransformsFull(transformedSource, glShaderType);
|
||||
final PrewarmTransformResult pre = bodyTree != null
|
||||
? applyPrewarmTransformsFull(transformedSource, bodyTree, headerLen, glShaderType)
|
||||
: applyPrewarmTransformsFull(transformedSource, glShaderType);
|
||||
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(pre.source(), shaderKindFor(glShaderType), "prewarm", SpirvCompiler.Options.vulkanForced460Core());
|
||||
if (r.spirv() == null) return;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
|
||||
import org.antlr.v4.runtime.Token;
|
||||
import org.antlr.v4.runtime.tree.ParseTreeWalker;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
@@ -18,11 +19,29 @@ public final class UscaledRetype {
|
||||
|
||||
public record Attrib(String name, int location, int declVecSize, int boundVecSize, boolean signed) {}
|
||||
|
||||
private record RetypeKey(String source, List<Attrib> attribs) {}
|
||||
|
||||
private static final int CACHE_MAX = 64;
|
||||
private static final Object2ObjectLinkedOpenHashMap<RetypeKey, String> CACHE = new Object2ObjectLinkedOpenHashMap<>();
|
||||
|
||||
private UscaledRetype() {}
|
||||
|
||||
public static String retype(String source, List<Attrib> attribs) {
|
||||
if (attribs.isEmpty()) return source;
|
||||
|
||||
final RetypeKey key = new RetypeKey(source, List.copyOf(attribs));
|
||||
synchronized (CACHE) {
|
||||
if (CACHE.containsKey(key)) return CACHE.getAndMoveToFirst(key);
|
||||
}
|
||||
final String result = retypeUncached(source, attribs);
|
||||
synchronized (CACHE) {
|
||||
CACHE.putAndMoveToFirst(key, result);
|
||||
while (CACHE.size() > CACHE_MAX) CACHE.removeLast();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String retypeUncached(String source, List<Attrib> attribs) {
|
||||
final GLSLParser.Translation_unitContext root;
|
||||
try {
|
||||
root = GlslTransformUtils.parseFullQuiet(source);
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.splash;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.OffscreenTarget;
|
||||
|
||||
public final class SplashDispatcher {
|
||||
|
||||
@@ -32,7 +33,7 @@ public final class SplashDispatcher {
|
||||
return (long) w << 32 | (h & 0xffffffffL);
|
||||
}
|
||||
|
||||
public static void tryDispatch(SDLGPURenderBackend backend, SplashOffscreenTarget target) {
|
||||
public static void tryDispatch(SDLGPURenderBackend backend, OffscreenTarget target) {
|
||||
if (!frameReady || target == null) return;
|
||||
final long now = System.nanoTime();
|
||||
if (now - lastBlitNanos < MIN_INTERVAL_NS) return;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ThreadRegistry<T> {
|
||||
|
||||
private final T[] empty;
|
||||
private final Object lock = new Object();
|
||||
|
||||
private volatile T[] entries;
|
||||
private volatile T sole;
|
||||
|
||||
public ThreadRegistry(T[] empty) {
|
||||
this.empty = empty;
|
||||
this.entries = empty;
|
||||
}
|
||||
|
||||
public T sole() {
|
||||
return sole;
|
||||
}
|
||||
|
||||
public T[] snapshot() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
public void add(T entry) {
|
||||
synchronized (lock) {
|
||||
final T[] prev = entries;
|
||||
final T[] next = Arrays.copyOf(prev, prev.length + 1);
|
||||
next[prev.length] = entry;
|
||||
publish(next);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(T entry) {
|
||||
synchronized (lock) {
|
||||
final T[] prev = entries;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < prev.length; i++) {
|
||||
if (prev[i] == entry) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) return false;
|
||||
final T[] next = Arrays.copyOf(prev, prev.length - 1);
|
||||
System.arraycopy(prev, idx + 1, next, idx, prev.length - idx - 1);
|
||||
publish(next);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (lock) {
|
||||
publish(empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(T[] next) {
|
||||
entries = next;
|
||||
sole = next.length == 1 ? next[0] : null;
|
||||
}
|
||||
}
|
||||
+92
@@ -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));
|
||||
}
|
||||
}
|
||||
+5
-12
@@ -7,7 +7,6 @@ import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL42;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -15,17 +14,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ImageUnitBindingStateTest {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ContextState state() throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("tlState");
|
||||
f.setAccessible(true);
|
||||
return ((ThreadLocal<ContextState>) f.get(null)).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindImageTextureRecordsTheFullTuple() throws Exception {
|
||||
void bindImageTextureRecordsTheFullTuple() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.bindImageTexture(2, 77, 3, true, 5, GL15.GL_READ_WRITE, GL30.GL_R32UI);
|
||||
|
||||
@@ -38,7 +31,7 @@ class ImageUnitBindingStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void getIntegerIndexedAnswersImageBindingsFromState() throws Exception {
|
||||
void getIntegerIndexedAnswersImageBindingsFromState() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
|
||||
backend.bindImageTexture(1, 42, 2, false, 4, GL15.GL_READ_ONLY, GL30.GL_RGBA32UI);
|
||||
@@ -52,9 +45,9 @@ class ImageUnitBindingStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void outOfRangeUnitIsIgnored() throws Exception {
|
||||
void outOfRangeUnitIsIgnored() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.bindImageTexture(0, 11, 0, true, 0, GL15.GL_READ_ONLY, GL30.GL_R32UI);
|
||||
backend.bindImageTexture(ContextState.MAX_IMAGE_UNITS, 99, 0, true, 0, GL15.GL_READ_ONLY, GL30.GL_R32UI);
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu;
|
||||
|
||||
import com.mitchej123.lwjgl.LWJGLService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class SDLGPULWJGLServiceGatingTest {
|
||||
|
||||
@Test
|
||||
void anUnengagedSdlDeclinesAndNeverArmsTheFallbackTripwire() {
|
||||
assertFalse(SDLGPUGate.isEngaged(), "test JVM must not have an engaged SDL GPU device");
|
||||
assertEquals(LWJGLService.PRIORITY_UNAVAILABLE, new SDLGPULWJGLService().getPriority());
|
||||
assertFalse(SDLGPULWJGLService.isOfferedAsAvailable(), "declining must leave fallBackToGL() usable");
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
|
||||
import org.lwjgl.sdl.SDLError;
|
||||
@@ -57,6 +59,10 @@ public final class SdlTestRig {
|
||||
return create().frameManager;
|
||||
}
|
||||
|
||||
public static ContextState contextState() {
|
||||
return Reflect.invokeStatic(SDLGPURenderBackend.class, "s", new Class<?>[0]);
|
||||
}
|
||||
|
||||
private static long realSdlDevice;
|
||||
|
||||
private static final int SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXBC | SDL_GPU_SHADERFORMAT_DXIL;
|
||||
|
||||
+1
-8
@@ -48,13 +48,6 @@ class UniformNegativeLocationTest {
|
||||
st.releaseUniformStaging(prog);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ContextState threadState() throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("tlState");
|
||||
f.setAccessible(true);
|
||||
return ((ThreadLocal<ContextState>) f.get(null)).get();
|
||||
}
|
||||
|
||||
private static void clearApplier(SDLGPURenderBackend backend) throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("pipelineApplier");
|
||||
f.setAccessible(true);
|
||||
@@ -66,7 +59,7 @@ class UniformNegativeLocationTest {
|
||||
void scalarAndVectorEntryPointsNeverReachTheApplier() throws Exception {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
clearApplier(backend);
|
||||
threadState().boundProgramObj = null;
|
||||
SdlTestRig.contextState().boundProgramObj = null;
|
||||
|
||||
final IntBuffer iv = MemoryUtil.memAllocInt(4);
|
||||
try {
|
||||
|
||||
+2
-2
@@ -6,13 +6,13 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.util.ThreadRegistry;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class ContextCounterAggregationTest {
|
||||
|
||||
private List<ContextState> registered;
|
||||
private ThreadRegistry<ContextState> registered;
|
||||
private ContextState a;
|
||||
private ContextState b;
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.frame;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Timeout(10)
|
||||
class PresenterTest {
|
||||
|
||||
private static final class DeferredExecutor implements Executor {
|
||||
final Deque<Runnable> tasks = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
tasks.add(command);
|
||||
}
|
||||
|
||||
void runNext() {
|
||||
tasks.removeFirst().run();
|
||||
}
|
||||
}
|
||||
|
||||
private static Presenter presenter(DeferredExecutor executor) {
|
||||
return new Presenter(new FrameManager(new Device()), executor);
|
||||
}
|
||||
|
||||
private static void awaitWaiting(Thread t) throws InterruptedException {
|
||||
while (t.getState() != Thread.State.WAITING && t.getState() != Thread.State.TIMED_WAITING) {
|
||||
assertTrue(t.isAlive(), "thread died instead of blocking");
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void aSecondRequestBlocksUntilTheFirstPresentRuns() throws Exception {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final Presenter p = presenter(executor);
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
assertEquals(1, executor.tasks.size());
|
||||
|
||||
final Thread second = new Thread(() -> p.requestPresent(2L, 10, 10));
|
||||
second.start();
|
||||
awaitWaiting(second);
|
||||
assertEquals(1, executor.tasks.size(), "second present must not enqueue while the first is in flight");
|
||||
|
||||
executor.runNext();
|
||||
second.join();
|
||||
assertEquals(1, executor.tasks.size(), "second present enqueues only after the first released the slot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void drainBlocksUntilThePendingPresentCompletes() throws Exception {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final Presenter p = presenter(executor);
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
|
||||
final Thread drainer = new Thread(p::drain);
|
||||
drainer.start();
|
||||
awaitWaiting(drainer);
|
||||
|
||||
executor.runNext();
|
||||
drainer.join();
|
||||
}
|
||||
|
||||
@Test
|
||||
void anInlineExecutorReleasesTheSlotImmediately() {
|
||||
final Presenter p = new Presenter(new FrameManager(new Device()), Runnable::run);
|
||||
p.requestPresent(1L, 10, 10);
|
||||
p.requestPresent(2L, 10, 10);
|
||||
p.drain();
|
||||
}
|
||||
|
||||
@Test
|
||||
void presentingDoesNotEnterTheFrameRegistry() throws Exception {
|
||||
final DeferredExecutor executor = new DeferredExecutor();
|
||||
final FrameManager fm = new FrameManager(new Device());
|
||||
final Presenter p = new Presenter(fm, executor);
|
||||
fm.frame();
|
||||
final int before = fm.registeredFrameCount();
|
||||
|
||||
p.requestPresent(1L, 10, 10);
|
||||
|
||||
final Thread windowThread = new Thread(executor::runNext, "window-thread");
|
||||
windowThread.start();
|
||||
windowThread.join();
|
||||
|
||||
assertEquals(before, fm.registeredFrameCount(), "the window thread must stay out of the frame registry");
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
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 FrameManager.FrameState activeFbo0Pass() {
|
||||
final FrameManager.FrameState f = new FrameManager.FrameState();
|
||||
f.renderPass = 0xBEEFL;
|
||||
f.activeLayoutHash = FrameManager.FBO0_LAYOUT_HASH;
|
||||
return f;
|
||||
}
|
||||
|
||||
@Test
|
||||
void aPendingColorClearBreaksTheActivePass() {
|
||||
final ContextState st = new ContextState();
|
||||
st.pendingSwapchainClear = true;
|
||||
assertTrue(swapchainClearNeedsPassBreak(st, activeFbo0Pass()), "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, activeFbo0Pass()));
|
||||
|
||||
final ContextState stencil = new ContextState();
|
||||
stencil.pendingSwapchainStencilClear = true;
|
||||
assertTrue(swapchainClearNeedsPassBreak(stencil, activeFbo0Pass()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nothingPendingLeavesThePassAlone() {
|
||||
assertFalse(swapchainClearNeedsPassBreak(new ContextState(), activeFbo0Pass()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aClearWithNoActivePassNeedsNoBreak() {
|
||||
final ContextState st = new ContextState();
|
||||
st.pendingSwapchainClear = true;
|
||||
|
||||
final FrameManager.FrameState f = activeFbo0Pass();
|
||||
f.renderPass = 0;
|
||||
assertFalse(swapchainClearNeedsPassBreak(st, f));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aPassBoundToAnFboIsNotTheFbo0Pass() {
|
||||
final ContextState st = new ContextState();
|
||||
st.pendingSwapchainClear = true;
|
||||
|
||||
final FrameManager.FrameState f = activeFbo0Pass();
|
||||
f.activeLayoutHash = 0xF80L;
|
||||
assertFalse(swapchainClearNeedsPassBreak(st, f));
|
||||
}
|
||||
}
|
||||
+112
@@ -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");
|
||||
}
|
||||
}
|
||||
+44
@@ -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));
|
||||
}
|
||||
}
|
||||
+232
@@ -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");
|
||||
}
|
||||
}
|
||||
+42
@@ -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));
|
||||
}
|
||||
}
|
||||
+64
@@ -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));
|
||||
}
|
||||
}
|
||||
+12
@@ -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();
|
||||
|
||||
+68
@@ -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);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.resource;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
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.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||
import static org.lwjgl.system.MemoryUtil.memAlloc;
|
||||
import static org.lwjgl.system.MemoryUtil.memFree;
|
||||
|
||||
@Timeout(60)
|
||||
class ReadbackSubmitOrderGpuTest {
|
||||
|
||||
private static final int SIZE = 4;
|
||||
|
||||
@AfterAll
|
||||
static void releaseDevice() {
|
||||
SdlTestRig.releaseRealDevice();
|
||||
}
|
||||
|
||||
@Test
|
||||
void aReadbackSubmitsTheFramesWritesFirst() throws Exception {
|
||||
final SdlTestRig rig = SdlTestRig.acquireRealDevice();
|
||||
final ResourceManager rm = rig.resourceManager;
|
||||
final FrameManager fm = rig.frameManager;
|
||||
final TextureOps textureOps = new TextureOps(rig.device, fm, rm, null);
|
||||
|
||||
final int glId = rm.genTexture();
|
||||
final long texture = rm.createTexture(glId, GL11.GL_TEXTURE_2D, GL11.GL_RGBA8, SIZE, SIZE, 1, 1, SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET);
|
||||
assertNotEquals(0L, texture, "texture creation failed");
|
||||
|
||||
fm.beginFrame();
|
||||
final FrameManager.FrameState f = fm.frame();
|
||||
final long frameCb = f.commandBuffer;
|
||||
assertNotEquals(0L, frameCb, "no frame command buffer to order against");
|
||||
assertEquals(0, f.midFrameSubmitsThisFrame);
|
||||
|
||||
final ByteBuffer out = memAlloc(SIZE * SIZE * 4);
|
||||
try {
|
||||
textureOps.readbackTexture(texture, 0, 0, SIZE, SIZE, 0, out);
|
||||
} finally {
|
||||
memFree(out);
|
||||
}
|
||||
|
||||
assertEquals(1, f.midFrameSubmitsThisFrame, "the frame command buffer must be submitted before the download buffer, or the readback sees stale contents");
|
||||
assertNotEquals(frameCb, f.commandBuffer, "the frame must continue on a freshly acquired command buffer");
|
||||
|
||||
fm.endFrame();
|
||||
}
|
||||
}
|
||||
+122
@@ -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));
|
||||
}
|
||||
}
|
||||
+112
@@ -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");
|
||||
}
|
||||
}
|
||||
+61
@@ -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"));
|
||||
}
|
||||
}
|
||||
+9
-15
@@ -1,29 +1,23 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.sampler;
|
||||
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
class SamplerBindGenerationTest {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ContextState state() throws Exception {
|
||||
final Field f = SDLGPURenderBackend.class.getDeclaredField("tlState");
|
||||
f.setAccessible(true);
|
||||
return ((ThreadLocal<ContextState>) f.get(null)).get();
|
||||
}
|
||||
|
||||
@Test
|
||||
void switchingActiveUnitDoesNotBumpTheGeneration() throws Exception {
|
||||
void switchingActiveUnitDoesNotBumpTheGeneration() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE0);
|
||||
final int before = st.samplerBindGen;
|
||||
@@ -37,9 +31,9 @@ class SamplerBindGenerationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void aRealBindChangeBumpsTheGeneration() throws Exception {
|
||||
void aRealBindChangeBumpsTheGeneration() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE0);
|
||||
backend.bindTexture(GL11.GL_TEXTURE_2D, 0);
|
||||
@@ -50,9 +44,9 @@ class SamplerBindGenerationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rebindingTheSameTextureDoesNotBumpTheGeneration() throws Exception {
|
||||
void rebindingTheSameTextureDoesNotBumpTheGeneration() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE0);
|
||||
backend.bindTexture(GL11.GL_TEXTURE_2D, 99);
|
||||
@@ -63,9 +57,9 @@ class SamplerBindGenerationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindingsAreHeldPerUnitSoTheActiveUnitIsNotSamplerState() throws Exception {
|
||||
void bindingsAreHeldPerUnitSoTheActiveUnitIsNotSamplerState() {
|
||||
final SDLGPURenderBackend backend = new SDLGPURenderBackend();
|
||||
final ContextState st = state();
|
||||
final ContextState st = SdlTestRig.contextState();
|
||||
|
||||
backend.activeTexture(GL13.GL_TEXTURE2);
|
||||
backend.bindTexture(GL11.GL_TEXTURE_2D, 55);
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
|
||||
import com.gtnewhorizons.angelica.glsm.testutil.TestPaths;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.sampler.SamplerLookup;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.util.shaderc.Shaderc;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.lwjgl.system.MemoryUtil.memFree;
|
||||
|
||||
class FontFilterVertexSamplerTest {
|
||||
|
||||
private static final String FONT_FILTER_VSH = TestPaths.readString("src/main/resources/assets/angelica/shaders/fontFilter.vsh");
|
||||
|
||||
private static String runSdlPipeline(String raw) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(raw, GL20.GL_VERTEX_SHADER, "fontFilter.vsh", true);
|
||||
String src = pre != null ? pre.rewrittenSource() : raw;
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
return src;
|
||||
}
|
||||
|
||||
private static ShaderManager.StageReflection reflect(String source) {
|
||||
final SpirvCompiler.Result r = SpirvCompiler.compile(source, Shaderc.shaderc_vertex_shader,
|
||||
"fontFilter.vsh", SpirvCompiler.Options.vulkanForced460Core());
|
||||
assertNotNull(r.spirv(), () -> "fontFilter.vsh failed to compile for Vulkan: " + r.error());
|
||||
final ByteBuffer spirv = r.spirv();
|
||||
try {
|
||||
return ShaderManager.reflectStage(spirv, true);
|
||||
} finally {
|
||||
memFree(spirv);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void samplerSurvivesStrippingAndReflectsOnTheVertexStage() {
|
||||
final String transformed = runSdlPipeline(FONT_FILTER_VSH);
|
||||
assertTrue(transformed.contains("uniform sampler2D lightmap"), "a reference inside a runtime if must keep the declaration; only preprocessor #if hides it");
|
||||
|
||||
final List<String> samplers = reflect(transformed).samplerNames();
|
||||
assertEquals(List.of("lightmap"), samplers, "the vertex stage must expose exactly the lightmap sampler");
|
||||
}
|
||||
|
||||
@Test
|
||||
void looseVec3UniformReflectsWithAStd140Offset() {
|
||||
final List<ShaderManager.UboMember> members = reflect(runSdlPipeline(FONT_FILTER_VSH)).uboMembers();
|
||||
|
||||
final ShaderManager.UboMember lightmap = members.stream()
|
||||
.filter(m -> m.name().endsWith("u_Lightmap"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("u_Lightmap missing from the default uniform block: " + members));
|
||||
|
||||
assertEquals(3, lightmap.vectorSize(), "u_Lightmap must reflect as a 3-wide vector");
|
||||
assertEquals(1, lightmap.columns(), "u_Lightmap is a vector, not a matrix");
|
||||
assertEquals(0, lightmap.arrayStride(), "a scalar member takes Std140Writer's contiguous path");
|
||||
assertEquals(0, lightmap.offset() % 16, "std140 aligns a vec3 to 16 bytes");
|
||||
}
|
||||
|
||||
@Test
|
||||
void samplerNamedLightmapResolvesToTextureUnitOne() {
|
||||
final List<String> samplers = reflect(runSdlPipeline(FONT_FILTER_VSH)).samplerNames();
|
||||
final Object2IntOpenHashMap<String> noExplicitUnits = new Object2IntOpenHashMap<>();
|
||||
noExplicitUnits.defaultReturnValue(-1);
|
||||
|
||||
assertEquals(1, SamplerLookup.getSamplerTextureUnit(samplers, noExplicitUnits, 0), "even without a glUniform1i the SDL fallback must put a sampler named 'lightmap' on unit 1");
|
||||
}
|
||||
}
|
||||
+107
@@ -1,15 +1,28 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL32;
|
||||
import org.lwjgl.opengl.GL43;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class PrewarmSingleParseEquivalenceTest {
|
||||
|
||||
@AfterEach
|
||||
void clearBlocks() {
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GLSMHooks.perPassUniformBlock = null;
|
||||
}
|
||||
|
||||
private static String legacy(String source, int glShaderType) {
|
||||
final GlslVulkanPreprocess.Result pre = GlslVulkanPreprocess.run(source, glShaderType, "test", true);
|
||||
String src = pre != null ? pre.rewrittenSource() : source;
|
||||
@@ -17,6 +30,9 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
src = ClipZRemap.injectGLToVulkanClipZ(src);
|
||||
}
|
||||
src = SamplerStripper.stripUnused(src);
|
||||
if (glShaderType == GL20.GL_VERTEX_SHADER || glShaderType == GL20.GL_FRAGMENT_SHADER) {
|
||||
src = PerFrameBlockInjector.inject(src, GLSMHooks.perFrameUniformBlock, GLSMHooks.perPassUniformBlock);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
@@ -28,6 +44,14 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
assertEquals(legacyOut, parseOnceOut, "single-parse vs chained pipeline diverged for " + label);
|
||||
}
|
||||
|
||||
private static void assertEquivalentIgnoringWhitespace(String label, String source, int glShaderType) {
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String legacyOut = legacy(source, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final String parseOnceOut = ShaderManager.applyPrewarmTransforms(source, glShaderType);
|
||||
assertEquals(legacyOut.replaceAll("\\s+", " "), parseOnceOut.replaceAll("\\s+", " "), "single-parse vs chained pipeline diverged beyond whitespace for " + label);
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalFragment_noTransformsNeeded() {
|
||||
final String src = "#version 460 core\n"
|
||||
@@ -135,4 +159,87 @@ class PrewarmSingleParseEquivalenceTest {
|
||||
final String src = "this is not valid GLSL @@@@\n";
|
||||
assertEquivalent("unparseable", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
private static final PerFrameUniformBlock PER_FRAME = new PerFrameUniformBlock(List.of(
|
||||
new Member("cameraPosition", UniformType.VEC3),
|
||||
new Member("frameTimeCounter", UniformType.FLOAT)));
|
||||
|
||||
private static final PerFrameUniformBlock PER_PASS = new PerFrameUniformBlock(List.of(
|
||||
new Member("alphaTestRef", UniformType.FLOAT)));
|
||||
|
||||
@Test
|
||||
void perFrameBlock_replaceableUniformStrippedAndInjected() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
GLSMHooks.perPassUniformBlock = PER_PASS;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform float frameTimeCounter;\n"
|
||||
+ "uniform float alphaTestRef;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " if (fragColor.a < alphaTestRef) discard;\n"
|
||||
+ " fragColor = vec4(frameTimeCounter);\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-replaceable", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_firstDeclIsUnlocatedVsInput() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "in vec3 a_Position;\n"
|
||||
+ "layout(location = 5) in vec2 a_TexCoord;\n"
|
||||
+ "uniform vec3 cameraPosition;\n"
|
||||
+ "void main() {\n"
|
||||
+ " gl_Position = vec4(a_Position - cameraPosition, 1.0);\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-unlocated-first-in", src, GL20.GL_VERTEX_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_memberUniformWithInitializer() {
|
||||
GLSMHooks.perPassUniformBlock = PER_PASS;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform float alphaTestRef = 0.1;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " if (fragColor.a < alphaTestRef) discard;\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-initializer", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_firstDeclIsStrippedSampler() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform sampler2D u_Unused;\n"
|
||||
+ "uniform float frameTimeCounter;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " fragColor = vec4(frameTimeCounter);\n"
|
||||
+ "}\n";
|
||||
assertEquivalentIgnoringWhitespace("pfb-stripped-first-sampler", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_shadowedMemberKeepsLocalDeclaration() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "uniform vec4 frameTimeCounter;\n"
|
||||
+ "uniform vec3 cameraPosition;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() {\n"
|
||||
+ " fragColor = frameTimeCounter + vec4(cameraPosition, 0.0);\n"
|
||||
+ "}\n";
|
||||
assertEquivalent("pfb-shadowed", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlock_noReferencesLeavesSourceUntouched() {
|
||||
GLSMHooks.perFrameUniformBlock = PER_FRAME;
|
||||
GLSMHooks.perPassUniformBlock = PER_PASS;
|
||||
final String src = "#version 460 core\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(1.0); }\n";
|
||||
assertEquivalent("pfb-unreferenced", src, GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.shader;
|
||||
|
||||
import com.gtnewhorizons.angelica.glsm.GlslTransformUtils;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.PerFrameUniformBlock.Member;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.UniformType;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL32;
|
||||
import org.taumc.glsl.ShaderParser;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class PrewarmTreeHandoffEquivalenceTest {
|
||||
|
||||
private static final String HEADER = "#version 460 core\n";
|
||||
|
||||
@AfterEach
|
||||
void clearBlocks() {
|
||||
GLSMHooks.perFrameUniformBlock = null;
|
||||
GLSMHooks.perPassUniformBlock = null;
|
||||
}
|
||||
|
||||
private static void assertHandoffEquivalent(String label, String body, int glShaderType) {
|
||||
final GLSLParser.Translation_unitContext tree = ShaderParser.parseShader(body).full();
|
||||
GlslTransformUtils.restoreReservedWordsInTree(tree);
|
||||
final String printedBody = GlslTransformUtils.getFormattedShaderRebased(tree, "");
|
||||
final String finalSource = HEADER + printedBody;
|
||||
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final ShaderManager.PrewarmTransformResult reparse = ShaderManager.applyPrewarmTransformsFull(finalSource, glShaderType);
|
||||
GlslVulkanPreprocess.clearCache();
|
||||
final ShaderManager.PrewarmTransformResult treeFed = ShaderManager.applyPrewarmTransformsFull(finalSource, tree, HEADER.length(), glShaderType);
|
||||
|
||||
assertEquals(reparse.source(), treeFed.source(), "tree handoff diverged from re-parse for " + label);
|
||||
assertEquals(reparse.boolUniforms(), treeFed.boolUniforms(), "bool uniforms diverged for " + label);
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalFragment() {
|
||||
assertHandoffEquivalent("minimal-fragment",
|
||||
"layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(1.0); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unusedSamplerStripped() {
|
||||
assertHandoffEquivalent("unused-sampler",
|
||||
"uniform sampler2D u_Unused;\n"
|
||||
+ "uniform sampler2D u_Used;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = texture(u_Used, vec2(0.5)); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void perFrameBlockInjection() {
|
||||
GLSMHooks.perFrameUniformBlock = new PerFrameUniformBlock(List.of(
|
||||
new Member("cameraPosition", UniformType.VEC3),
|
||||
new Member("frameTimeCounter", UniformType.FLOAT)));
|
||||
assertHandoffEquivalent("per-frame-block",
|
||||
"uniform float frameTimeCounter;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(frameTimeCounter); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void vertexClipZAndUnlocatedInputs() {
|
||||
GLSMHooks.perFrameUniformBlock = new PerFrameUniformBlock(List.of(
|
||||
new Member("cameraPosition", UniformType.VEC3)));
|
||||
assertHandoffEquivalent("vertex-clipz-unlocated",
|
||||
"layout(location = 0) in vec3 a_Position;\n"
|
||||
+ "in vec2 a_TexCoord;\n"
|
||||
+ "uniform vec3 cameraPosition;\n"
|
||||
+ "out vec2 v_TexCoord;\n"
|
||||
+ "void main() {\n"
|
||||
+ " v_TexCoord = a_TexCoord;\n"
|
||||
+ " gl_Position = vec4(a_Position - cameraPosition, 1.0);\n"
|
||||
+ "}\n",
|
||||
GL20.GL_VERTEX_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void boolUniformMetadata() {
|
||||
assertHandoffEquivalent("bool-uniform",
|
||||
"uniform bool u_Enabled;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = vec4(u_Enabled ? 1.0 : 0.0); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void texelFetchPath() {
|
||||
assertHandoffEquivalent("texelfetch",
|
||||
"uniform sampler2D u_Data;\n"
|
||||
+ "layout(location = 0) out vec4 fragColor;\n"
|
||||
+ "void main() { fragColor = texelFetch(u_Data, ivec2(0), 0); }\n",
|
||||
GL20.GL_FRAGMENT_SHADER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void geometryStage() {
|
||||
assertHandoffEquivalent("geometry",
|
||||
"layout(triangles) in;\n"
|
||||
+ "layout(triangle_strip, max_vertices = 3) out;\n"
|
||||
+ "void main() {\n"
|
||||
+ " for (int i = 0; i < 3; i++) {\n"
|
||||
+ " gl_Position = gl_in[i].gl_Position;\n"
|
||||
+ " EmitVertex();\n"
|
||||
+ " }\n"
|
||||
+ " EndPrimitive();\n"
|
||||
+ "}\n",
|
||||
GL32.GL_GEOMETRY_SHADER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.gtnewhorizons.angelica.sdlgpu.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
class ThreadRegistryTest {
|
||||
|
||||
private static ThreadRegistry<Object> registry() {
|
||||
return new ThreadRegistry<>(new Object[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void soleTracksTheSingleEntry() {
|
||||
final ThreadRegistry<Object> r = registry();
|
||||
final Object a = new Object();
|
||||
final Object b = new Object();
|
||||
|
||||
assertNull(r.sole(), "empty registry has no sole entry");
|
||||
r.add(a);
|
||||
assertSame(a, r.sole());
|
||||
r.add(b);
|
||||
assertNull(r.sole(), "two entries means no sole entry");
|
||||
r.remove(b);
|
||||
assertSame(a, r.sole());
|
||||
r.remove(a);
|
||||
assertNull(r.sole());
|
||||
assertEquals(0, r.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeOfAnAbsentEntryLeavesTheRegistryAlone() {
|
||||
final ThreadRegistry<Object> r = registry();
|
||||
final Object a = new Object();
|
||||
r.add(a);
|
||||
final Object[] before = r.snapshot();
|
||||
|
||||
assertFalse(r.remove(new Object()));
|
||||
assertSame(before, r.snapshot(), "a no-op remove must not republish");
|
||||
assertSame(a, r.sole());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,19 +4,14 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer;
|
||||
import com.gtnewhorizon.gtnhlib.util.font.GlyphReplacements;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.GRADIENT_PAYLOAD;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LENGTH;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_PAYLOAD;
|
||||
|
||||
import com.gtnewhorizons.angelica.config.AngelicaConfig;
|
||||
import com.gtnewhorizons.angelica.config.FontConfig;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.RenderSystem;
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.FFPVertexLighting;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.PersistentStreamingBuffer;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
|
||||
@@ -29,20 +24,31 @@ import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.embeddedt.embeddium.impl.render.shader.ShaderLoader;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryStack.stackPush;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutByte;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutFloat;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutShort;
|
||||
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.GRADIENT_PAYLOAD;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_LENGTH;
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.SECTION_X_PAYLOAD;
|
||||
|
||||
/**
|
||||
* A batching replacement for {@code FontRenderer}
|
||||
@@ -67,6 +73,8 @@ public class BatchingFontRenderer {
|
||||
private final int AAStrength;
|
||||
private final int alphaTestRefLocation;
|
||||
private final int mvpMatrixLocation;
|
||||
private final int lightmapLocation;
|
||||
private final int lightmapSamplerLocation;
|
||||
private final int fontShaderId;
|
||||
|
||||
final boolean isSGA;
|
||||
@@ -111,6 +119,8 @@ public class BatchingFontRenderer {
|
||||
AAStrength = GLStateManager.glGetUniformLocation(fontShaderId, "strength");
|
||||
alphaTestRefLocation = GLStateManager.glGetUniformLocation(fontShaderId, "alphaTestRef");
|
||||
mvpMatrixLocation = GLStateManager.glGetUniformLocation(fontShaderId, "u_MVPMatrix");
|
||||
lightmapLocation = GLStateManager.glGetUniformLocation(fontShaderId, "u_Lightmap");
|
||||
lightmapSamplerLocation = GLStateManager.glGetUniformLocation(fontShaderId, "lightmap");
|
||||
if (ebo == null) {
|
||||
ebo = new IndexBuffer();
|
||||
vbo = GLStateManager.glGenBuffers();
|
||||
@@ -128,6 +138,9 @@ public class BatchingFontRenderer {
|
||||
// [v, v, t, t, c, c, c, c, tb, tb, tb, tb]
|
||||
// v, t and tb are floats, c is bytes; 36 bytes total
|
||||
private static final int VERTEX_SIZE = 36;
|
||||
|
||||
/** {@code OpenGlHelper.lightmapTexUnit} */
|
||||
private static final int LIGHTMAP_TEX_UNIT = 1;
|
||||
private static int rawCapacity = INITIAL_BATCH_SIZE * VERTEX_SIZE;
|
||||
private static ByteBuffer vertexData = memAlloc(rawCapacity);
|
||||
private static long vertexDataAddress = memAddress0(vertexData);
|
||||
@@ -138,6 +151,7 @@ public class BatchingFontRenderer {
|
||||
private static int fontVAO = 0;
|
||||
private static int vbo;
|
||||
private static IndexBuffer ebo;
|
||||
private static int lightmapSamplerProgram;
|
||||
|
||||
private static final int RING_CAPACITY = 2 * 1024 * 1024;
|
||||
private static PersistentStreamingBuffer streamRing;
|
||||
@@ -158,11 +172,58 @@ public class BatchingFontRenderer {
|
||||
private int blendSrcRGB = GL11.GL_SRC_ALPHA;
|
||||
private int blendDstRGB = GL11.GL_ONE_MINUS_SRC_ALPHA;
|
||||
|
||||
private float lightmapU = 0.0f;
|
||||
private float lightmapV = 0.0f;
|
||||
private boolean lightmapActive = false;
|
||||
private int lightmapTextureId = 0;
|
||||
|
||||
private final Vector3f lightingFactor = new Vector3f(1.0f, 1.0f, 1.0f);
|
||||
private boolean lightingFactorActive = false;
|
||||
|
||||
private static final Vector4f scratchLightmapUv = new Vector4f();
|
||||
|
||||
private int applyLighting(int argb) {
|
||||
if (!lightingFactorActive) return argb;
|
||||
final int red = Math.max(0, Math.min(255, (int) (((argb >> 16) & 0xFF) * lightingFactor.x + 0.5f)));
|
||||
final int green = Math.max(0, Math.min(255, (int) (((argb >> 8) & 0xFF) * lightingFactor.y + 0.5f)));
|
||||
final int blue = Math.max(0, Math.min(255, (int) ((argb & 0xFF) * lightingFactor.z + 0.5f)));
|
||||
return (argb & 0xFF000000) | (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
private void captureLightmapState() {
|
||||
final int unitBinding = GLStateManager.getTextures().getTextureUnitBindings(LIGHTMAP_TEX_UNIT).getBinding();
|
||||
|
||||
final boolean active = GLStateManager.getProjectionMatrix().m33() == 0.0f && unitBinding != 0;
|
||||
|
||||
float u = 0.0f;
|
||||
float v = 0.0f;
|
||||
int texture = 0;
|
||||
if (active) {
|
||||
scratchLightmapUv.set(GLSMConfig.lastBrightnessX, GLSMConfig.lastBrightnessY, 0.0f, 1.0f).mul(GLStateManager.getTextures().getTextureUnitMatrix(LIGHTMAP_TEX_UNIT));
|
||||
u = scratchLightmapUv.x;
|
||||
v = scratchLightmapUv.y;
|
||||
texture = unitBinding;
|
||||
}
|
||||
|
||||
if (active == lightmapActive && u == lightmapU && v == lightmapV && texture == lightmapTextureId) {
|
||||
return;
|
||||
}
|
||||
sealBatchSegment();
|
||||
lightmapActive = active;
|
||||
lightmapU = u;
|
||||
lightmapV = v;
|
||||
lightmapTextureId = texture;
|
||||
}
|
||||
|
||||
private static final class TextSegment {
|
||||
final Matrix4f mvp = new Matrix4f();
|
||||
BatchingFontRenderer owner;
|
||||
int cmdStart;
|
||||
int cmdEnd;
|
||||
float lightmapU;
|
||||
float lightmapV;
|
||||
boolean lightmapActive;
|
||||
int lightmapTexture;
|
||||
}
|
||||
|
||||
// 16-bit EBO index range
|
||||
@@ -183,7 +244,6 @@ public class BatchingFontRenderer {
|
||||
private int batchSealedEnd;
|
||||
private static BatchingFontRenderer arenaOwner;
|
||||
|
||||
|
||||
private void allocateBuffers() {
|
||||
populateEBO(rawCapacity / VERTEX_SIZE);
|
||||
}
|
||||
@@ -255,6 +315,7 @@ public class BatchingFontRenderer {
|
||||
|
||||
private void pushVtx(float x, float y, int rgba, float u, float v, float uMin, float uMax, float vMin, float vMax) {
|
||||
final long ptr = vertexDataAddress + vertexDataPos;
|
||||
rgba = applyLighting(rgba);
|
||||
|
||||
// v, v
|
||||
memPutFloat(ptr, x);
|
||||
@@ -414,6 +475,10 @@ public class BatchingFontRenderer {
|
||||
segment.cmdStart = batchSealedEnd;
|
||||
segment.cmdEnd = end;
|
||||
resolveMvp(segment.mvp);
|
||||
segment.lightmapActive = lightmapActive;
|
||||
segment.lightmapU = lightmapU;
|
||||
segment.lightmapV = lightmapV;
|
||||
segment.lightmapTexture = lightmapTextureId;
|
||||
batchSegments.add(segment);
|
||||
batchSealedEnd = end;
|
||||
}
|
||||
@@ -485,6 +550,7 @@ public class BatchingFontRenderer {
|
||||
final boolean isTextureEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_TEXTURE_2D);
|
||||
final boolean isBlendEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_BLEND);
|
||||
final int boundTextureBefore = GLStateManager.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
|
||||
final int lightmapBindingBefore = boundLightmapTexture();
|
||||
|
||||
final boolean depthTestBefore = GLStateManager.getDepthTest().isEnabled();
|
||||
final boolean depthMaskBefore = GLStateManager.getDepthState().isEnabled();
|
||||
@@ -493,6 +559,7 @@ public class BatchingFontRenderer {
|
||||
GLStateManager.glDepthMask(false);
|
||||
flushLastTexture = DUMMY_RESOURCE_LOCATION;
|
||||
flushTextureChanged = false;
|
||||
resetFlushLightmap();
|
||||
try (MemoryStack stack = stackPush()) {
|
||||
final FloatBuffer mvpBuf = stack.mallocFloat(16);
|
||||
for (int i = 0, n = deferredSegments.size(); i < n; i++) {
|
||||
@@ -500,10 +567,12 @@ public class BatchingFontRenderer {
|
||||
mvpBuf.clear();
|
||||
segment.mvp.get(mvpBuf);
|
||||
GLStateManager.glUniformMatrix4(segment.owner.mvpMatrixLocation, false, mvpBuf);
|
||||
uploadLightmap(segment.owner.lightmapLocation, segment.lightmapActive, segment.lightmapU,
|
||||
segment.lightmapV, segment.lightmapTexture);
|
||||
drawCommands(segment.owner.batchCommands.elements(), segment.cmdStart, segment.cmdEnd, segment.owner);
|
||||
}
|
||||
}
|
||||
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore);
|
||||
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore, lightmapBindingBefore);
|
||||
restoreDepth(depthTestBefore, depthMaskBefore);
|
||||
|
||||
discardDeferredText();
|
||||
@@ -517,6 +586,7 @@ public class BatchingFontRenderer {
|
||||
segment.owner.recycleBatchSegments();
|
||||
segment.owner.batchSealedEnd = 0;
|
||||
segment.owner.batchMatrixValid = false;
|
||||
segment.owner.resetLightmapState();
|
||||
segment.owner = null;
|
||||
}
|
||||
deferredSegmentPool.addAll(deferredSegments);
|
||||
@@ -578,12 +648,14 @@ public class BatchingFontRenderer {
|
||||
final boolean isTextureEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_TEXTURE_2D);
|
||||
final boolean isBlendEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_BLEND);
|
||||
final int boundTextureBefore = GLStateManager.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
|
||||
final int lightmapBindingBefore = boundLightmapTexture();
|
||||
final boolean depthTestBefore = GLStateManager.getDepthTest().isEnabled();
|
||||
final boolean depthMaskBefore = GLStateManager.getDepthState().isEnabled();
|
||||
|
||||
setupFontDrawState();
|
||||
flushLastTexture = DUMMY_RESOURCE_LOCATION;
|
||||
flushTextureChanged = false;
|
||||
resetFlushLightmap();
|
||||
try (MemoryStack stack = stackPush()) {
|
||||
final FloatBuffer mvpBuf = stack.mallocFloat(16);
|
||||
for (int i = 0; i < segmentCount; i++) {
|
||||
@@ -591,6 +663,7 @@ public class BatchingFontRenderer {
|
||||
mvpBuf.clear();
|
||||
segment.mvp.get(mvpBuf);
|
||||
GLStateManager.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
|
||||
uploadLightmap(lightmapLocation, segment.lightmapActive, segment.lightmapU, segment.lightmapV, segment.lightmapTexture);
|
||||
drawCommands(cmds, segment.cmdStart, segment.cmdEnd, this);
|
||||
}
|
||||
if (batchSealedEnd < cmdCount) {
|
||||
@@ -598,10 +671,11 @@ public class BatchingFontRenderer {
|
||||
resolveMvp(scratchMvp);
|
||||
scratchMvp.get(mvpBuf);
|
||||
GLStateManager.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
|
||||
uploadLightmap(lightmapLocation, lightmapActive, lightmapU, lightmapV, lightmapTextureId);
|
||||
drawCommands(cmds, batchSealedEnd, cmdCount, this);
|
||||
}
|
||||
}
|
||||
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore);
|
||||
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore, lightmapBindingBefore);
|
||||
restoreDepth(depthTestBefore, depthMaskBefore);
|
||||
|
||||
truncateBatchToWatermark();
|
||||
@@ -624,6 +698,10 @@ public class BatchingFontRenderer {
|
||||
GLStateManager.glUniform1f(AAStrength, FontConfig.fontAAStrength / 120.f);
|
||||
}
|
||||
GLStateManager.glUniform1f(alphaTestRefLocation, GLStateManager.getAlphaState().getReference());
|
||||
if (lightmapSamplerProgram != fontShaderId) {
|
||||
GLStateManager.glUniform1i(lightmapSamplerLocation, LIGHTMAP_TEX_UNIT);
|
||||
lightmapSamplerProgram = fontShaderId;
|
||||
}
|
||||
|
||||
if (fontVAO == 0) {
|
||||
fontVAO = GLStateManager.glGenVertexArrays();
|
||||
@@ -654,6 +732,38 @@ public class BatchingFontRenderer {
|
||||
private static ResourceLocation flushLastTexture;
|
||||
private static boolean flushTextureChanged;
|
||||
|
||||
private static float flushLastLightmapU;
|
||||
private static float flushLastLightmapV;
|
||||
private static boolean flushLastLightmapActive;
|
||||
|
||||
private static void resetFlushLightmap() {
|
||||
flushLastLightmapU = Float.NaN;
|
||||
flushLastLightmapV = Float.NaN;
|
||||
flushLastLightmapActive = false;
|
||||
}
|
||||
|
||||
private static int boundLightmapTexture() {
|
||||
return GLStateManager.getTextures().getTextureUnitBindings(LIGHTMAP_TEX_UNIT).getBinding();
|
||||
}
|
||||
|
||||
private static void bindLightmapTexture(int texture) {
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + LIGHTMAP_TEX_UNIT);
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture);
|
||||
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
|
||||
}
|
||||
|
||||
private static void uploadLightmap(int uniformLocation, boolean active, float u, float v, int texture) {
|
||||
if (u != flushLastLightmapU || v != flushLastLightmapV || active != flushLastLightmapActive) {
|
||||
GLStateManager.glUniform3f(uniformLocation, u, v, active ? 1.0f : 0.0f);
|
||||
flushLastLightmapU = u;
|
||||
flushLastLightmapV = v;
|
||||
flushLastLightmapActive = active;
|
||||
}
|
||||
if (active && texture != 0 && boundLightmapTexture() != texture) {
|
||||
bindLightmapTexture(texture);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawCommands(FontDrawCmd[] cmdsData, int from, int to, BatchingFontRenderer owner) {
|
||||
for (int i = from; i < to; i++) {
|
||||
final FontDrawCmd cmd = cmdsData[i];
|
||||
@@ -673,7 +783,7 @@ public class BatchingFontRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreFontDrawState(int prevProgram, boolean textureEnabledBefore, boolean blendEnabledBefore, int boundTextureBefore) {
|
||||
private static void restoreFontDrawState(int prevProgram, boolean textureEnabledBefore, boolean blendEnabledBefore, int boundTextureBefore, int lightmapBindingBefore) {
|
||||
GLStateManager.glUseProgram(prevProgram);
|
||||
|
||||
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
|
||||
@@ -688,6 +798,9 @@ public class BatchingFontRenderer {
|
||||
if (flushTextureChanged) {
|
||||
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, boundTextureBefore);
|
||||
}
|
||||
if (boundLightmapTexture() != lightmapBindingBefore) {
|
||||
bindLightmapTexture(lightmapBindingBefore);
|
||||
}
|
||||
}
|
||||
|
||||
private static void restoreDepth(boolean testBefore, boolean maskBefore) {
|
||||
@@ -709,11 +822,19 @@ public class BatchingFontRenderer {
|
||||
batchMatrixValid = false;
|
||||
vertexDataPos = deferredVertexPos;
|
||||
idxWriterIndex = deferredIdxPos;
|
||||
resetLightmapState();
|
||||
if (arenaOwner == this) {
|
||||
arenaOwner = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetLightmapState() {
|
||||
lightmapActive = false;
|
||||
lightmapU = 0.0f;
|
||||
lightmapV = 0.0f;
|
||||
lightmapTextureId = 0;
|
||||
}
|
||||
|
||||
// === Actual text mesh generation
|
||||
|
||||
public static boolean charInRange(char what, char fromInclusive, char toInclusive) {
|
||||
@@ -808,6 +929,8 @@ public class BatchingFontRenderer {
|
||||
FontProviderMC.get(this.isSGA).locationFontTexture = this.locationFontTexture;
|
||||
|
||||
this.beginBatch();
|
||||
this.captureLightmapState();
|
||||
this.lightingFactorActive = FFPVertexLighting.modulatesVertexColor(this.lightingFactor);
|
||||
float curX = anchorX;
|
||||
try {
|
||||
final int totalStringLength = string.length();
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.gtnewhorizons.angelica.helpers;
|
||||
|
||||
import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR;
|
||||
|
||||
import net.minecraft.entity.EntityLiving;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
|
||||
public final class RendererLivingEntityHelper {
|
||||
|
||||
private RendererLivingEntityHelper() {}
|
||||
|
||||
public static String getUpsideDownName(EntityLivingBase entity) {
|
||||
if (entity instanceof EntityLiving living) {
|
||||
return living.hasCustomNameTag() ? entity.getCommandSenderName() : "";
|
||||
}
|
||||
|
||||
if (entity instanceof EntityPlayer player) {
|
||||
return player.getHideCape() ? "" : entity.getCommandSenderName();
|
||||
}
|
||||
|
||||
return entity.getCommandSenderName();
|
||||
}
|
||||
|
||||
public static String stripFormattingCodes(String entityName) {
|
||||
if (entityName == null || entityName.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return entityName.indexOf(FORMATTING_CHAR) < 0
|
||||
? entityName
|
||||
: EnumChatFormatting.getTextWithoutFormattingCodes(entityName);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import com.gtnewhorizons.angelica.glsm.hooks.DeferredBlendHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.DeferredDepthColorHandler;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ShaderTransformPostProcessor;
|
||||
import com.gtnewhorizons.angelica.glsm.shader.ShaderType;
|
||||
import org.taumc.glsl.grammar.GLSLParser;
|
||||
import com.gtnewhorizons.angelica.glsm.hooks.ShaderWorkSubmitter;
|
||||
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
|
||||
import net.coderbot.iris.Iris;
|
||||
@@ -48,6 +51,27 @@ public class IrisGLSMBridge {
|
||||
StateUpdateNotifiers.colorModulatorNotifier = listener -> colorModulatorListener = listener;
|
||||
}
|
||||
|
||||
public static void installPostTransformHook() {
|
||||
if (GLSMHooks.postTransformProcessor != null) return;
|
||||
GLSMHooks.postTransformProcessor = new ShaderTransformPostProcessor() {
|
||||
@Override
|
||||
public void onTransformed(String src, ShaderType shaderType) {
|
||||
onTransformed(src, null, 0, shaderType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransformed(String src, GLSLParser.Translation_unitContext bodyTree, int headerLen, ShaderType shaderType) {
|
||||
if (Iris.ShaderTransformExecutor.isOnWorker()) {
|
||||
SDLGPUGate.prewarmSpirv(src, bodyTree, headerLen, shaderType.id);
|
||||
} else {
|
||||
Iris.ShaderTransformExecutor.submitTracked(() -> {
|
||||
SDLGPUGate.prewarmSpirv(src, bodyTree, headerLen, shaderType.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
GLSMConfig.expandVertexFormats = Iris.enabled;
|
||||
IrisSamplers.initRenderer();
|
||||
@@ -57,15 +81,7 @@ public class IrisGLSMBridge {
|
||||
return Iris.ShaderTransformExecutor.submitTracked(work);
|
||||
}
|
||||
};
|
||||
GLSMHooks.postTransformProcessor = (src, shaderType) -> {
|
||||
if (Iris.ShaderTransformExecutor.isOnWorker()) {
|
||||
SDLGPUGate.prewarmSpirv(src, shaderType.id);
|
||||
} else {
|
||||
Iris.ShaderTransformExecutor.submitTracked(() -> {
|
||||
SDLGPUGate.prewarmSpirv(src, shaderType.id);
|
||||
});
|
||||
}
|
||||
};
|
||||
installPostTransformHook();
|
||||
GLSMHooks.blendHandler = new DeferredBlendHandler() {
|
||||
@Override
|
||||
public boolean isBlendLocked() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -70,6 +70,12 @@ public enum Mixins implements IMixins {
|
||||
)
|
||||
),
|
||||
|
||||
STARMINER_RENDERER_LIVING_ENTITY_OPTIMIZATION(new MixinBuilder()
|
||||
.setPhase(Phase.LATE)
|
||||
.addRequiredMod(TargetedMod.STARMINER)
|
||||
.addClientMixins("client.starminer.MixinTransformClientHelper")
|
||||
),
|
||||
|
||||
ANGELICA_ENTITY_OVERLAYS(new MixinBuilder()
|
||||
.setPhase(Phase.EARLY)
|
||||
.setApplyIf(() -> AngelicaConfig.entityOverlayFixes)
|
||||
@@ -377,6 +383,10 @@ public enum Mixins implements IMixins {
|
||||
, "celeritas.biome_blending.MixinBlockLiquid"
|
||||
, "celeritas.threading.MixinForgeHooksClient"
|
||||
, "celeritas.terrain.MixinChunk"
|
||||
, "celeritas.terrain.MixinWorldClient_WorkerAccess"
|
||||
, "celeritas.terrain.MixinWorld_WorkerMutationGuard"
|
||||
, "celeritas.terrain.MixinTileEntity_AwaitingDescriptor"
|
||||
, "celeritas.terrain.MixinNetHandlerPlayClient_DescriptorRepair"
|
||||
, "celeritas.terrain.MixinRenderRegion"
|
||||
, "celeritas.terrain.MixinSectionRenderDataStorage"
|
||||
, "celeritas.terrain.MixinDefaultChunkRenderer"
|
||||
|
||||
@@ -24,6 +24,7 @@ public enum TargetedMod implements ITargetMod {
|
||||
IC2("ic2.core.coremod.IC2core", "IC2"),
|
||||
NATURA(null, "Natura"),
|
||||
SECURITYCRAFT(null, "securitycraft"),
|
||||
STARMINER("jp.mc.ancientred.starminer.core.SMPlugin", "modJ_StarMiner"),
|
||||
SUPERNOVA("com.mitchej123.supernova.core.SupernovaCore", "supernova"),
|
||||
THAUMCRAFT(null, "Thaumcraft"),
|
||||
TINKERS_CONSTRUCT(null, "TConstruct"),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.mixins.interfaces;
|
||||
|
||||
public interface IAwaitingDescriptorTE {
|
||||
|
||||
boolean angelica$isAwaitingDescriptor();
|
||||
|
||||
void angelica$setAwaitingDescriptor(boolean awaiting);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.gtnewhorizons.angelica.mixins.interfaces;
|
||||
|
||||
import net.minecraft.client.model.TexturedQuad;
|
||||
|
||||
public interface ModelBoxQuads {
|
||||
|
||||
TexturedQuad[] angelica$getQuadList();
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.proxy;
|
||||
import com.gtnewhorizons.angelica.rendering.culling.GpuCulling;
|
||||
import static com.gtnewhorizons.angelica.AngelicaMod.MOD_ID;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -36,6 +37,7 @@ import com.google.common.base.Objects;
|
||||
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager;
|
||||
import com.gtnewhorizons.angelica.commands.AngelicaCommand;
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.common.BlockError;
|
||||
import com.gtnewhorizons.angelica.compat.ModStatus;
|
||||
import com.gtnewhorizons.angelica.compat.bettercrashes.BetterCrashesCompat;
|
||||
@@ -52,6 +54,7 @@ import com.gtnewhorizons.angelica.debug.TPSGraph;
|
||||
import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
|
||||
import com.gtnewhorizons.angelica.dynamiclights.config.EntityLightConfig;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.hudcaching.HUDCaching;
|
||||
import com.gtnewhorizons.angelica.iris.IrisGLSMBridge;
|
||||
import com.gtnewhorizons.angelica.loading.AngelicaClientTweaker;
|
||||
@@ -59,8 +62,10 @@ import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
|
||||
import com.gtnewhorizons.angelica.render.CloudRenderer;
|
||||
import com.gtnewhorizons.angelica.render.EmissiveTextureAutoloader;
|
||||
import com.gtnewhorizons.angelica.rendering.AngelicaBlockSafetyRegistry;
|
||||
import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasDebugScreenHandler;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasSetup;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.threading.ChunkTaskRegistry;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.threading.DefaultChunkTaskProvider;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.threading.ThreadedChunkTaskProvider;
|
||||
@@ -83,6 +88,7 @@ import jss.notfine.gui.GuiCustomMenu;
|
||||
import jss.notfine.gui.NotFineGameOptionPages;
|
||||
import jss.notfine.gui.options.named.FOVMode;
|
||||
import me.flashyreese.mods.reeses_sodium_options.client.gui.ReeseSodiumVideoOptionsScreen;
|
||||
import me.jellysquid.mods.sodium.client.gui.FrameRateOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
|
||||
import me.jellysquid.mods.sodium.client.gui.SodiumOptionsGUI;
|
||||
import net.coderbot.iris.Iris;
|
||||
@@ -106,6 +112,17 @@ public final class ClientProxy extends CommonProxy {
|
||||
return CONFIG;
|
||||
}
|
||||
|
||||
public static void toggleVSync(boolean enabled) {
|
||||
final SodiumGameOptions opts = options();
|
||||
opts.advanced.vsyncMode = enabled ? GLStateManager.preferredTearFreeMode() : VSyncMode.OFF;
|
||||
try {
|
||||
opts.writeChanges();
|
||||
} catch (IOException e) {
|
||||
AngelicaMod.LOGGER.warn("Could not persist the vsync mode", e);
|
||||
}
|
||||
GLStateManager.setVSyncMode(opts.advanced.vsyncMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preInit(FMLPreInitializationEvent event) {
|
||||
ModStatus.preInit();
|
||||
@@ -117,6 +134,9 @@ public final class ClientProxy extends CommonProxy {
|
||||
MinecraftForge.EVENT_BUS.register(new AngelicaTesrMeshCache.ReloadListener());
|
||||
ModelRegistry.registerModid(MOD_ID);
|
||||
blockError = new BlockError();
|
||||
if (AngelicaConfig.enableIris) {
|
||||
Iris.warmupShaderTransforms();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,6 +147,16 @@ public final class ClientProxy extends CommonProxy {
|
||||
HUDCaching.init();
|
||||
}
|
||||
CeleritasSetup.ensureInitialized();
|
||||
final SodiumGameOptions opts = options();
|
||||
if (opts.advanced.vsyncMode == null) {
|
||||
opts.advanced.vsyncMode = FrameRateOptions.defaultMode();
|
||||
}
|
||||
Minecraft.getMinecraft().gameSettings.enableVsync = opts.advanced.vsyncMode.tearFree();
|
||||
GLStateManager.setVSyncPreference(opts.advanced.vsyncMode);
|
||||
FramePacer.setIdleWork(deadlineNanos -> {
|
||||
final CeleritasWorldRenderer renderer = CeleritasWorldRenderer.getInstanceOrNull();
|
||||
if (renderer != null) renderer.runFrameIdleWork(deadlineNanos);
|
||||
});
|
||||
MinecraftForge.EVENT_BUS.register(CeleritasDebugScreenHandler.INSTANCE);
|
||||
if (AngelicaConfig.enableIris) {
|
||||
IrisGLSMBridge.register();
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import com.gtnewhorizons.angelica.AngelicaMod;
|
||||
import com.gtnewhorizons.angelica.glsm.GLStateManager;
|
||||
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
|
||||
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
|
||||
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
public final class FramePacer {
|
||||
|
||||
public interface IdleWork {
|
||||
void run(long deadlineNanos);
|
||||
}
|
||||
|
||||
private static final int MIN_PLAUSIBLE_REFRESH_HZ = 20;
|
||||
private static final int MAX_PLAUSIBLE_REFRESH_HZ = 1000;
|
||||
private static final long IDLE_WORK_MIN_BUDGET_NANOS = 500_000L;
|
||||
private static final long GATE_BLOCK_MIN_NANOS = 100_000L;
|
||||
private static final int GATE_STREAK_FRAMES = 60;
|
||||
static final int GATE_SETTLE_FRAMES = 120;
|
||||
static final int PACING_HEADROOM_PERCENT = 5;
|
||||
|
||||
private static final Tracy.ZoneId Z_PACER_WAIT = Tracy.zoneId("pacerBackpressure", Tracy.COLOR_SWAP);
|
||||
private static final Tracy.ZoneId Z_PACER_IDLE = Tracy.zoneId("pacerIdleWork", Tracy.COLOR_CLIENT);
|
||||
private static final Tracy.ZoneId Z_PACER_SLEEP = Tracy.zoneId("pacerSleep", Tracy.COLOR_SWAP);
|
||||
private static final long P_FRAME_GATE_US = Tracy.plotHandle("frameGateUs");
|
||||
|
||||
private static long deadline;
|
||||
private static long lastFrameTime;
|
||||
|
||||
private static boolean warnedUncapped;
|
||||
|
||||
private static VSyncMode lastVSyncMode = VSyncMode.ON;
|
||||
private static int lastCapHz;
|
||||
private static int lastRefreshHz;
|
||||
private static int lastCeilingHz;
|
||||
|
||||
private static IdleWork idleWork;
|
||||
private static PacerSleeper sleeper = newSleeper();
|
||||
private static int gateStreak;
|
||||
private static Boolean settledBlocking;
|
||||
private static boolean candidateBlocking;
|
||||
private static int candidateFrames;
|
||||
private static Boolean loggedBlocking;
|
||||
private static VSyncMode loggedMode;
|
||||
private static int loggedRefreshHz;
|
||||
private static int loggedCapHz;
|
||||
|
||||
private static boolean gateBlocking;
|
||||
private static long lastSeenGateEnd;
|
||||
|
||||
private FramePacer() {}
|
||||
|
||||
private static PacerSleeper newSleeper() {
|
||||
return new PacerSleeper(System::nanoTime, LockSupport::parkNanos, true);
|
||||
}
|
||||
|
||||
public static void setIdleWork(IdleWork work) {
|
||||
idleWork = work;
|
||||
}
|
||||
|
||||
public static int plausibleRefreshHz(int hz) {
|
||||
return hz >= MIN_PLAUSIBLE_REFRESH_HZ && hz <= MAX_PLAUSIBLE_REFRESH_HZ ? hz : 0;
|
||||
}
|
||||
|
||||
public static int withHeadroom(int hz, int percent) {
|
||||
if (hz <= 0) return 0;
|
||||
return (int) Math.max(hz + 1L, Math.round(hz * (100 + percent) / 100.0));
|
||||
}
|
||||
|
||||
public static int pacingCeilingHz(VSyncMode mode, int capHz, int refreshHz, boolean gateActive) {
|
||||
final int cap = Math.max(capHz, 0);
|
||||
final int backstop = backstopHz(mode, refreshHz, gateActive);
|
||||
if (cap == 0) return backstop;
|
||||
if (backstop == 0) return cap;
|
||||
return Math.min(cap, backstop);
|
||||
}
|
||||
|
||||
static int backstopHz(VSyncMode mode, int refreshHz, boolean gateActive) {
|
||||
if (!mode.tearFree()) return 0;
|
||||
final int refresh = plausibleRefreshHz(refreshHz);
|
||||
if (mode == VSyncMode.ON && gateActive) return withHeadroom(refresh, PACING_HEADROOM_PERCENT);
|
||||
return refresh;
|
||||
}
|
||||
|
||||
static boolean observeGate(long gateNanos, long thresholdNanos) {
|
||||
if (gateNanos > thresholdNanos) gateStreak = GATE_STREAK_FRAMES;
|
||||
else if (gateStreak > 0) gateStreak--;
|
||||
return gateStreak > 0;
|
||||
}
|
||||
|
||||
static boolean gateActive() {
|
||||
return gateStreak > 0;
|
||||
}
|
||||
|
||||
static Boolean settleGateBlocking(boolean observed) {
|
||||
if (settledBlocking != null && observed == settledBlocking) {
|
||||
candidateFrames = 0;
|
||||
} else if (candidateFrames == 0 || observed != candidateBlocking) {
|
||||
candidateBlocking = observed;
|
||||
candidateFrames = 1;
|
||||
} else if (++candidateFrames >= GATE_SETTLE_FRAMES) {
|
||||
settledBlocking = observed;
|
||||
candidateFrames = 0;
|
||||
}
|
||||
return settledBlocking;
|
||||
}
|
||||
|
||||
static boolean vsyncNotHonoured(VSyncMode mode, boolean gateBlocking) {
|
||||
return mode == VSyncMode.ON && !gateBlocking;
|
||||
}
|
||||
|
||||
static boolean pacingStatusChanged(boolean blocking, VSyncMode mode, int refreshHz, int capHz) {
|
||||
final Boolean settled = settleGateBlocking(blocking);
|
||||
if (settled == null) return false;
|
||||
if (settled.equals(loggedBlocking) && mode == loggedMode && refreshHz == loggedRefreshHz && capHz == loggedCapHz) {
|
||||
return false;
|
||||
}
|
||||
loggedBlocking = settled;
|
||||
loggedMode = mode;
|
||||
loggedRefreshHz = refreshHz;
|
||||
loggedCapHz = capHz;
|
||||
return true;
|
||||
}
|
||||
|
||||
static String pacingLine(VSyncMode mode, int refreshHz, int capHz, boolean gateBlocking) {
|
||||
final StringBuilder sb = new StringBuilder("Frame pacing: ");
|
||||
sb.append(switch (mode) {
|
||||
case OFF -> "vsync off";
|
||||
case MAILBOX -> "mailbox";
|
||||
case ON -> "vsync";
|
||||
});
|
||||
if (mode.tearFree() && refreshHz > 0) sb.append(' ').append(refreshHz).append("Hz");
|
||||
if (capHz > 0) sb.append(", cap ").append(capHz);
|
||||
if (vsyncNotHonoured(mode, gateBlocking)) sb.append(", driver not blocking, paced by wall clock");
|
||||
else if (gateBlocking) sb.append(", hardware paced");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static long gateBlockThresholdNanos(long targetNanos) {
|
||||
return Math.max(GATE_BLOCK_MIN_NANOS, targetNanos / 32);
|
||||
}
|
||||
|
||||
public static long nextDeadline(long previousDeadline, long now, long targetNanos) {
|
||||
if (previousDeadline == 0L || now - previousDeadline > 2 * targetNanos) return now + targetNanos;
|
||||
return previousDeadline + targetNanos;
|
||||
}
|
||||
|
||||
public static long endFrame(int capHz, Runnable renderAheadWait) {
|
||||
lastVSyncMode = GLStateManager.getEffectiveVSyncMode();
|
||||
lastCapHz = Math.max(capHz, 0);
|
||||
lastRefreshHz = refreshHz();
|
||||
return pace(pacingCeilingHz(lastVSyncMode, lastCapHz, lastRefreshHz, gateActive()), renderAheadWait);
|
||||
}
|
||||
|
||||
public static long pace(int ceilingHz, Runnable renderAheadWait) {
|
||||
if (ceilingHz != lastCeilingHz) {
|
||||
lastCeilingHz = ceilingHz;
|
||||
deadline = 0L;
|
||||
}
|
||||
|
||||
long now = System.nanoTime();
|
||||
long phaseEnd;
|
||||
|
||||
final long targetNanos = ceilingHz > 0 ? 1_000_000_000L / ceilingHz : 0L;
|
||||
final long thresholdNanos = gateBlockThresholdNanos(targetNanos);
|
||||
long frameGateNanos = 0L;
|
||||
|
||||
if (ceilingHz > 0) {
|
||||
frameGateNanos = GLStateManager.lastFrameGateNanos();
|
||||
final long gateEnd = GLStateManager.lastFrameGateEndNanos();
|
||||
Tracy.plotInt(P_FRAME_GATE_US, frameGateNanos / 1000);
|
||||
|
||||
if (gateEnd != lastSeenGateEnd) {
|
||||
lastSeenGateEnd = gateEnd;
|
||||
if (frameGateNanos > thresholdNanos && GLStateManager.gateAnchorsNextFrameStart()) {
|
||||
deadline = gateEnd;
|
||||
}
|
||||
}
|
||||
deadline = nextDeadline(deadline, now, targetNanos);
|
||||
|
||||
final IdleWork work = idleWork;
|
||||
if (work != null && deadline - now > IDLE_WORK_MIN_BUDGET_NANOS) {
|
||||
Tracy.beginZone(Z_PACER_IDLE);
|
||||
try {
|
||||
work.run(deadline);
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
phaseEnd = System.nanoTime();
|
||||
now = phaseEnd;
|
||||
}
|
||||
|
||||
if (now < deadline) {
|
||||
final long preSleep = now;
|
||||
Tracy.beginZone(Z_PACER_SLEEP);
|
||||
try {
|
||||
now = sleeper.sleepUntil(deadline, now);
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deadline = 0L;
|
||||
}
|
||||
|
||||
Tracy.beginZone(Z_PACER_WAIT);
|
||||
try {
|
||||
if (renderAheadWait != null) renderAheadWait.run();
|
||||
} finally {
|
||||
Tracy.endZone();
|
||||
}
|
||||
phaseEnd = System.nanoTime();
|
||||
final long waitNanos = phaseEnd - now;
|
||||
now = phaseEnd;
|
||||
if (ceilingHz > 0 && waitNanos > thresholdNanos) deadline = now;
|
||||
gateBlocking = observeGate(ceilingHz > 0 ? Math.max(frameGateNanos, waitNanos) : 0L, thresholdNanos);
|
||||
|
||||
GLStateManager.pumpDisplayMessages();
|
||||
|
||||
if (pacingStatusChanged(gateBlocking, lastVSyncMode, lastRefreshHz, lastCapHz)) {
|
||||
final boolean blocking = settledBlocking;
|
||||
final String line = pacingLine(lastVSyncMode, lastRefreshHz, lastCapHz, blocking);
|
||||
if (vsyncNotHonoured(lastVSyncMode, blocking)) AngelicaMod.LOGGER.warn(line);
|
||||
else AngelicaMod.LOGGER.info(line);
|
||||
}
|
||||
|
||||
final long period = lastFrameTime == 0L ? 0L : now - lastFrameTime;
|
||||
lastFrameTime = now;
|
||||
return period;
|
||||
}
|
||||
|
||||
private static int refreshHz() {
|
||||
final int reported = GLStateManager.getDisplayRefreshRateHz();
|
||||
final int hz = plausibleRefreshHz(reported);
|
||||
|
||||
if (hz == 0 && lastCapHz == 0 && lastVSyncMode.tearFree() && !warnedUncapped) {
|
||||
warnedUncapped = true;
|
||||
AngelicaMod.LOGGER.warn("Display refresh rate reported as {}Hz, so the frame rate cannot be bounded if the driver ignores vsync. Set Max Framerate to bound it.", reported);
|
||||
}
|
||||
return hz;
|
||||
}
|
||||
|
||||
public static String debugIndicator(VSyncMode mode, int refreshHz, int capHz, boolean gateActive) {
|
||||
final int cap = Math.max(capHz, 0);
|
||||
final boolean showCap = cap > 0 && cap == pacingCeilingHz(mode, cap, refreshHz, gateActive);
|
||||
if (!mode.tearFree() && !showCap) return null;
|
||||
|
||||
final StringBuilder sb = new StringBuilder(" [");
|
||||
if (mode.tearFree()) {
|
||||
sb.append(mode == VSyncMode.MAILBOX ? "mailbox" : "vsync");
|
||||
final int refresh = plausibleRefreshHz(refreshHz);
|
||||
if (refresh > 0) sb.append(' ').append(refresh);
|
||||
if (showCap) sb.append(", ");
|
||||
}
|
||||
if (showCap) sb.append("cap ").append(cap);
|
||||
return sb.append(']').toString();
|
||||
}
|
||||
|
||||
public static String debugIndicator() {
|
||||
return debugIndicator(lastVSyncMode, lastRefreshHz, lastCapHz, gateActive());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import java.util.function.LongConsumer;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
final class PacerSleeper {
|
||||
|
||||
private static final long SPIN_FLOOR_NANOS = 100_000L;
|
||||
private static final long MIN_PARK_NANOS = 1_000_000L;
|
||||
private static final long MAX_OVERSHOOT_NANOS = 4_000_000L;
|
||||
private static final int SLOTS = 10;
|
||||
|
||||
private final LongSupplier clock;
|
||||
private final LongConsumer parker;
|
||||
private final boolean spinWait;
|
||||
private final long[] overshoots = new long[SLOTS];
|
||||
private long overshootSum;
|
||||
private int offset;
|
||||
|
||||
PacerSleeper(LongSupplier clock, LongConsumer parker, boolean spinWait) {
|
||||
this.clock = clock;
|
||||
this.parker = parker;
|
||||
this.spinWait = spinWait;
|
||||
}
|
||||
|
||||
long sleepUntil(long deadlineNanos, long now) {
|
||||
while (true) {
|
||||
final long remaining = deadlineNanos - now;
|
||||
if (remaining <= SPIN_FLOOR_NANOS) break;
|
||||
if (Thread.currentThread().isInterrupted()) return clock.getAsLong();
|
||||
|
||||
long request = remaining - avgOvershootNanos() - SPIN_FLOOR_NANOS;
|
||||
if (request <= 0L) {
|
||||
if (remaining <= MIN_PARK_NANOS) break;
|
||||
request = MIN_PARK_NANOS;
|
||||
}
|
||||
|
||||
final long before = now;
|
||||
parker.accept(request);
|
||||
now = clock.getAsLong();
|
||||
record(Math.min(Math.max(0L, now - before - request), MAX_OVERSHOOT_NANOS));
|
||||
}
|
||||
|
||||
if (spinWait) {
|
||||
while (now < deadlineNanos) {
|
||||
Thread.yield();
|
||||
now = clock.getAsLong();
|
||||
}
|
||||
}
|
||||
return now;
|
||||
}
|
||||
|
||||
long avgOvershootNanos() {
|
||||
return overshootSum / SLOTS;
|
||||
}
|
||||
|
||||
private void record(long nanos) {
|
||||
overshootSum += nanos - overshoots[offset];
|
||||
overshoots[offset] = nanos;
|
||||
offset = (offset + 1) % SLOTS;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
|
||||
import net.minecraft.tileentity.TileEntity;
|
||||
|
||||
public class RenderThreadContext {
|
||||
|
||||
@@ -15,10 +15,13 @@ public class RenderThreadContext {
|
||||
currentWorldSlice.remove();
|
||||
}
|
||||
|
||||
public static TileEntity getSnapshotTE(int x, int y, int z) {
|
||||
final WorldSlice slice = currentWorldSlice.get();
|
||||
if (slice == null) return null;
|
||||
return slice.getTileEntity(x, y, z);
|
||||
public static WorldSlice get() {
|
||||
return currentWorldSlice.get();
|
||||
}
|
||||
|
||||
public static WorldSlice workerSlice() {
|
||||
if (TessellatorManager.isOnMainThread()) return null;
|
||||
return currentWorldSlice.get();
|
||||
}
|
||||
|
||||
public static boolean hasWorldSlice() {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.gtnewhorizons.angelica.rendering;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.CodeSource;
|
||||
import java.util.Arrays;
|
||||
|
||||
import net.minecraft.block.Block;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.gtnewhorizons.angelica.mixins.interfaces.IRenderingRegistryExt;
|
||||
|
||||
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler;
|
||||
import cpw.mods.fml.client.registry.RenderingRegistry;
|
||||
import cpw.mods.fml.common.Loader;
|
||||
import cpw.mods.fml.common.ModContainer;
|
||||
import cpw.mods.fml.common.registry.GameRegistry;
|
||||
import cpw.mods.fml.common.registry.GameRegistry.UniqueIdentifier;
|
||||
|
||||
public final class WorkerWorldAccess {
|
||||
|
||||
private static final String REPORT_URL = "https://github.com/GTNewHorizons/angelica/issues";
|
||||
private static final int INITIAL_SLOTS = 64;
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("Angelica");
|
||||
|
||||
private static final LoggedRenderTypeErrors READS = new LoggedRenderTypeErrors();
|
||||
private static final LoggedRenderTypeErrors WRITES = new LoggedRenderTypeErrors();
|
||||
|
||||
private WorkerWorldAccess() {}
|
||||
|
||||
public static void readOutsideSlice(String worldMethod, int x, int y, int z, Block renderingBlock) {
|
||||
if (!READS.shouldLog(renderingBlock)) return;
|
||||
LOGGER.warn(describe(worldMethod, "read outside the chunk slice", x, y, z, renderingBlock), new Throwable());
|
||||
}
|
||||
|
||||
public static void blockedWrite(String worldMethod, int x, int y, int z, Block renderingBlock) {
|
||||
if (!WRITES.shouldLog(renderingBlock)) return;
|
||||
LOGGER.error(describe(worldMethod, "ignored, it would corrupt world data", x, y, z, renderingBlock), new Throwable());
|
||||
}
|
||||
|
||||
private static String describe(String worldMethod, String outcome, int x, int y, int z, Block block) {
|
||||
final StringBuilder sb = new StringBuilder(224);
|
||||
sb.append("Off-thread World.").append(worldMethod).append(' ').append(outcome).append(" at ").append(x).append(',').append(y).append(',').append(z);
|
||||
if (block != null) {
|
||||
sb.append(". Rendering ").append(nameOf(block));
|
||||
final ISimpleBlockRenderingHandler isbrh = isbrhOf(block);
|
||||
if (isbrh != null) {
|
||||
sb.append(" via ").append(isbrh.getClass().getName());
|
||||
final String owner = modIdOf(isbrh.getClass());
|
||||
if (owner != null) sb.append(" from ").append(owner);
|
||||
}
|
||||
}
|
||||
return sb.append(". Please report with this trace: ").append(REPORT_URL).toString();
|
||||
}
|
||||
|
||||
private static String nameOf(Block block) {
|
||||
try {
|
||||
final UniqueIdentifier id = GameRegistry.findUniqueIdentifierFor(block);
|
||||
if (id != null) return id.modId + ':' + id.name;
|
||||
} catch (Throwable ignored) {}
|
||||
return block.getClass().getName();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static ISimpleBlockRenderingHandler isbrhOf(Block block) {
|
||||
try {
|
||||
return ((IRenderingRegistryExt) RenderingRegistry.instance()).getISBRH(block.getRenderType());
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String modIdOf(Class<?> clazz) {
|
||||
try {
|
||||
final CodeSource source = clazz.getProtectionDomain() == null ? null : clazz.getProtectionDomain().getCodeSource();
|
||||
if (source == null || source.getLocation() == null) return null;
|
||||
final File jar = new File(jarFileUrl(source.getLocation()).toURI());
|
||||
for (ModContainer mod : Loader.instance().getModList()) {
|
||||
if (jar.equals(mod.getSource())) return mod.getModId();
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static URL jarFileUrl(URL location) throws MalformedURLException {
|
||||
if (!"jar".equals(location.getProtocol())) return location;
|
||||
final String nested = location.getPath();
|
||||
final int separator = nested.indexOf("!/");
|
||||
return new URL(separator < 0 ? nested : nested.substring(0, separator));
|
||||
}
|
||||
|
||||
private static final class LoggedRenderTypeErrors {
|
||||
|
||||
private volatile boolean[] logged = new boolean[INITIAL_SLOTS];
|
||||
|
||||
boolean shouldLog(Block block) {
|
||||
final int renderType = block == null ? -1 : block.getRenderType();
|
||||
final int slot = renderType < 0 ? 0 : renderType + 1;
|
||||
final boolean[] current = this.logged;
|
||||
return (slot >= current.length || !current[slot]) && markLogged(slot);
|
||||
}
|
||||
|
||||
private synchronized boolean markLogged(int slot) {
|
||||
boolean[] current = this.logged;
|
||||
if (slot < current.length) {
|
||||
if (current[slot]) return false;
|
||||
current[slot] = true;
|
||||
return true;
|
||||
}
|
||||
current = Arrays.copyOf(current, Math.max(slot + 1, current.length * 2));
|
||||
current[slot] = true;
|
||||
this.logged = current;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -9,6 +9,7 @@ import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProvider;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.iris.BlockRenderContext;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.iris.ContextAwareChunkVertexEncoder;
|
||||
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
|
||||
import com.prupe.mcpatcher.mal.block.RenderBlocksUtils;
|
||||
import it.unimi.dsi.fastutil.floats.FloatArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
|
||||
@@ -111,6 +112,8 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
|
||||
final BlockPos blockPos = new BlockPos(minX, minY, minZ);
|
||||
final IBlockAccess region = getBlockAccess();
|
||||
final WorldSlice reportingSlice = region instanceof WorldSlice slice ? slice : null;
|
||||
if (reportingSlice != null) reportingSlice.setRenderingBlock(null);
|
||||
final SmoothBiomeColorCache biomeColorCache = getBiomeColorCache();
|
||||
|
||||
onEnterExecute();
|
||||
@@ -155,6 +158,8 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask<C
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reportingSlice != null) reportingSlice.setRenderingBlock(block);
|
||||
|
||||
final int meta = region.getBlockMetadata(x, y, z);
|
||||
|
||||
if (block.hasTileEntity(meta)) {
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACK
|
||||
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
|
||||
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
|
||||
import com.gtnewhorizons.angelica.glsm.states.FogState;
|
||||
import com.gtnewhorizons.angelica.rendering.FramePacer;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
|
||||
import com.gtnewhorizons.angelica.rendering.tesr.TesrBatchRenderer;
|
||||
import cpw.mods.fml.common.eventhandler.EventPriority;
|
||||
|
||||
+29
@@ -21,6 +21,7 @@ import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.embeddedt.embeddium.impl.gl.device.CommandList;
|
||||
import org.embeddedt.embeddium.impl.gl.device.RenderDevice;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.ChunkRenderMatrices;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.RenderSection;
|
||||
import org.embeddedt.embeddium.impl.render.chunk.data.MinecraftBuiltRenderSectionData;
|
||||
@@ -114,6 +115,34 @@ public class CeleritasWorldRenderer extends SimpleWorldRenderer<WorldClient, Ang
|
||||
return this.renderSectionManager;
|
||||
}
|
||||
|
||||
private long asyncEwmaNanos;
|
||||
private long uploadEwmaNanos;
|
||||
|
||||
private static long blendEwma(long ewmaNanos, long tookNanos) {
|
||||
return ewmaNanos == 0L ? tookNanos : (ewmaNanos * 3 + tookNanos) / 4;
|
||||
}
|
||||
|
||||
public void runFrameIdleWork(long deadlineNanos) {
|
||||
final AngelicaRenderSectionManager rsm = this.renderSectionManager;
|
||||
if (rsm == null || this.world == null) return;
|
||||
RenderDevice.enterManagedCode();
|
||||
try {
|
||||
long start = System.nanoTime();
|
||||
if (start + asyncEwmaNanos > deadlineNanos) return;
|
||||
rsm.runAsyncTasks();
|
||||
long now = System.nanoTime();
|
||||
asyncEwmaNanos = blendEwma(asyncEwmaNanos, now - start);
|
||||
|
||||
start = now;
|
||||
if (start + uploadEwmaNanos > deadlineNanos) return;
|
||||
rsm.uploadChunks();
|
||||
now = System.nanoTime();
|
||||
uploadEwmaNanos = blendEwma(uploadEwmaNanos, now - start);
|
||||
} finally {
|
||||
RenderDevice.exitManagedCode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return this.world != null;
|
||||
|
||||
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user