Compare commits

..
3 Commits
Author SHA1 Message Date
Jason Mitchell 3659b59db9 Adjust vsync and fps limiters, and fix WSR with player models (#2002) 2026-08-09 20:06:53 -07:00
Jason Mitchell 8c31eec525 SDL followup fixes (#2001)
* Deploy fixes
* Narrow transformer exclusions for Legends mods, and mixin priority for CustomNPC
* Journeymap 6 stencil/depth fix
* OpenLTW swizzle workaround
2026-08-09 13:43:46 -07:00
Jason Mitchell 166116d692 More fixes (#1995) 2026-08-08 23:53:06 -07:00
107 changed files with 4719 additions and 588 deletions

No files matched your search

+1 -1
View File
@@ -89,4 +89,4 @@ jobs:
env:
MAVEN_USER: ${{ secrets.MAVEN_USER }}
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
run: ./gradlew :tracy-client:publish
run: ./gradlew -PpublishTracy=true :tracy-client:publish
+1 -1
View File
@@ -3,7 +3,7 @@ name: Release tagged build
on:
push:
tags: [ '*' ]
tags: [ '*', '!tracy-v*' ]
permissions:
contents: write
+13 -4
View File
@@ -1,3 +1,5 @@
import com.modrinth.minotaur.TaskModrinthUpload
import net.darkhax.curseforgegradle.TaskPublishCurseForge
import xyz.wagyourtail.jvmdg.gradle.flags.DowngradeFlags
import xyz.wagyourtail.jvmdg.gradle.task.DowngradeJar
import xyz.wagyourtail.jvmdg.gradle.task.files.DowngradeFiles
@@ -31,11 +33,11 @@ minecraft {
}
// Ensure flyby args are passed to the client
// Forward -Dangelica.* from the gradle invocation to the client
tasks.withType<JavaExec>().matching { it.name.startsWith("runClient") }.configureEach {
for ((key, value) in System.getProperties()) {
val name = key.toString()
if (name.startsWith("angelica.flyby.")) {
if (name.startsWith("angelica.")) {
jvmArgs("-D$name=$value")
}
}
@@ -75,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")
@@ -227,7 +229,6 @@ val glCoreTest by tasks.registering(Test::class) {
}
tasks.test { finalizedBy(glCoreTest) }
tasks.check { dependsOn(glCoreTest) }
tasks.shadowJar {
dependsOn(embedOnly)
@@ -252,3 +253,11 @@ tasks.shadowJar {
relocate("com.gtnewhorizons.angelica.lwjgl3", "com.gtnewhorizons.angelica.lwjgl3")
relocate("com.gtnewhorizons.angelica.sdlgpu", "com.gtnewhorizons.angelica.sdlgpu")
}
tasks.withType<TaskPublishCurseForge>().configureEach {
uploadArtifacts.forEach { it.addGameVersion("Client") }
}
tasks.withType<TaskModrinthUpload>().configureEach {
setDependsOn(dependsOn.filterNot { it == "build" } + "assemble")
}
@@ -16,9 +16,11 @@ public final class SystemProperties {
private static final Logger LOGGER = LogManager.getLogger("SystemProperties");
// Backend selection
public static final boolean USE_SDL_GPU = Boolean.getBoolean("angelica.sdlgpu.enable");
public static final String KEY_USE_SDL_GPU = "angelica.sdlgpu.enable";
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);
@@ -613,6 +613,7 @@ public class DisplayListManager {
// Get relative transform (changes since glNewList, not absolute matrix state)
addAccumulatedDraw(tessellator, tessellator.getVertexFormat() != DefaultVertexFormat.POSITION);
}
transformCallback.clearBaked(tessellator);
tessellator.reset();
}
@@ -1322,6 +1323,16 @@ public class DisplayListManager {
private final Matrix4f transformMatrix = new Matrix4f();
private DirectTessellator bakedOwner;
private int bakedVertices;
void clearBaked(DirectTessellator tessellator) {
if (bakedOwner == tessellator) {
bakedOwner = null;
bakedVertices = 0;
}
}
public void scale(float x, float y, float z) {
transformMatrix.scale(x, y, z);
markDirty();
@@ -1384,10 +1395,26 @@ public class DisplayListManager {
return MatrixHelper.isIdentity(transformMatrix);
}
@Override
public boolean onStartDrawing(CallbackTessellator tessellator, int drawMode) {
bakedOwner = tessellator;
bakedVertices = 0;
return super.onStartDrawing(tessellator, drawMode);
}
@Override
public boolean onDraw(CallbackTessellator tessellator) {
if (!tessellator.isEmpty()) {
if (!isIdentity()) {
final int baked = bakedOwner == tessellator ? bakedVertices : 0;
if (bakedVertices == 0) {
flushMatrix();
} else if (baked != tessellator.vertexCount) {
GLStateManager.warnOnce("dl-mixed-baked-raw", "Display list {}: draw of {} vertices carries {} transformed ones - the rest lose the pending transform", glListId, tessellator.vertexCount, baked);
}
}
addAccumulatedDraw(tessellator, false);
clearBaked(tessellator);
}
return true;
}
@@ -1396,6 +1423,11 @@ public class DisplayListManager {
public void onVertex(CallbackTessellator tessellator, double x, double y, double z) {
tessellator.syncVanillaTessellator();
super.onVertex(tessellator, x, y, z);
if (bakedOwner != tessellator) {
bakedOwner = tessellator;
bakedVertices = 0;
}
bakedVertices++;
}
@Override
@@ -26,10 +26,13 @@ import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntStack;
import org.joml.Matrix4fStack;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL13;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -152,6 +155,8 @@ class GLContextState {
public final BooleanStateStack map2Vertex4State = track(new BooleanStateStack(GL11.GL_MAP2_VERTEX_4, false, true));
public final BooleanStateStack[] clipPlaneStates = new BooleanStateStack[GLStateManager.MAX_CLIP_PLANES];
public final ClipPlaneState clipPlaneState = new ClipPlaneState();
public final FloatBuffer queryScratch = BufferUtils.createFloatBuffer(16);
public final DoubleBuffer queryScratchDouble = BufferUtils.createDoubleBuffer(16);
public final MatrixModeStack matrixMode = new MatrixModeStack();
public final Matrix4fStack modelViewMatrix = new Matrix4fStack(GLStateManager.MAX_MODELVIEW_STACK_DEPTH);
public final Matrix4fStack projectionMatrix = new Matrix4fStack(GLStateManager.MAX_PROJECTION_STACK_DEPTH);
@@ -10,6 +10,8 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.DisplayListManager.RecordMode;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
@@ -528,7 +530,10 @@ public class GLStateManager {
}
final String glVendor = RENDER_BACKEND.getString(GL11.GL_VENDOR);
VENDOR = Vendor.getVendor(glVendor.toLowerCase());
if (glVendor == null) {
LOGGER.error("Backend {} reported no GL_VENDOR (context: {}); treating the vendor as unknown", RENDER_BACKEND.getName(), RENDER_BACKEND.hasContext());
}
VENDOR = Vendor.getVendor(glVendor == null ? "" : glVendor.toLowerCase());
final String os = System.getProperty("os.name").toLowerCase();
windows = os.contains("win");
@@ -549,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);
@@ -1183,12 +1191,11 @@ public class GLStateManager {
};
}
private static final FloatBuffer glGetFloatScratch = BufferUtils.createFloatBuffer(16);
public static void glGetFloat(int pname, float[] params) {
final FloatBuffer scratch = ctx().queryScratch;
final FloatBuffer buf;
if (params.length <= glGetFloatScratch.capacity()) {
buf = glGetFloatScratch;
if (params.length <= scratch.capacity()) {
buf = scratch;
buf.clear();
buf.limit(params.length);
} else {
@@ -1199,6 +1206,72 @@ public class GLStateManager {
buf.get(params);
}
public static double glGetDouble(int pname) {
return switch (pname) {
case GL11.GL_DEPTH_CLEAR_VALUE -> ctx().depthState.getClearValue();
default -> glGetFloat(pname);
};
}
public static void glGetDouble(int pname, DoubleBuffer params) {
final GLContextState glCtx = ctx();
if (!isCachingEnabled()) {
RENDER_BACKEND.getDouble(pname, params);
return;
}
final int pos = params.position();
switch (pname) {
case GL11.GL_DEPTH_CLEAR_VALUE -> params.put(pos, glCtx.depthState.getClearValue());
case GL11.GL_DEPTH_RANGE -> {
params.put(pos, glCtx.viewportState.depthRangeNear);
params.put(pos + 1, glCtx.viewportState.depthRangeFar);
}
case GL11.GL_MODELVIEW_MATRIX, GL11.GL_PROJECTION_MATRIX, GL11.GL_TEXTURE_MATRIX -> widenFromFloat(pname, params, 16);
case GL11.GL_COLOR_CLEAR_VALUE, GL11.GL_CURRENT_COLOR, GL11.GL_CURRENT_TEXTURE_COORDS, GL11.GL_FOG_COLOR,
GL11.GL_LIGHT_MODEL_AMBIENT, GL14.GL_BLEND_COLOR -> widenFromFloat(pname, params, 4);
case GL11.GL_CURRENT_NORMAL -> widenFromFloat(pname, params, 3);
default -> {
if (!HAS_MULTIPLE_SET.contains(pname)) {
params.put(pos, glGetDouble(pname));
} else {
RENDER_BACKEND.getDouble(pname, params);
}
}
}
}
public static void glGetDouble(int pname, double[] params) {
final DoubleBuffer scratch = ctx().queryScratchDouble;
final DoubleBuffer buf;
if (params.length <= scratch.capacity()) {
buf = scratch;
buf.clear();
buf.limit(params.length);
} else {
buf = BufferUtils.createDoubleBuffer(params.length);
}
glGetDouble(pname, buf);
buf.position(0);
buf.get(params);
}
private static void widenFromFloat(int pname, DoubleBuffer params, int count) {
final FloatBuffer scratch = ctx().queryScratch;
scratch.clear();
glGetFloat(pname, scratch);
final int pos = params.position();
for (int i = 0; i < count; i++) {
params.put(pos + i, scratch.get(i));
}
}
public static void glGetClipPlane(int plane, DoubleBuffer equation) {
final int index = plane - GL11.GL_CLIP_PLANE0;
if (index < 0 || index >= MAX_CLIP_PLANES) return;
ctx().clipPlaneState.getEyePlane(index, equation);
}
// GLStateManager Functions
public static void glBlendColor(float red, float green, float blue, float alpha) {
@@ -1897,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);
}
@@ -1915,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);
}
@@ -1933,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);
}
@@ -1951,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);
}
@@ -2258,7 +2335,7 @@ public class GLStateManager {
}
public static void glDrawElements(int mode, ByteBuffer indices) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
@@ -2283,7 +2360,7 @@ public class GLStateManager {
}
public static void glDrawElements(int mode, IntBuffer indices) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
@@ -2308,7 +2385,7 @@ public class GLStateManager {
}
public static void glDrawElements(int mode, ShortBuffer indices) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
@@ -2333,7 +2410,7 @@ public class GLStateManager {
}
public static void glDrawElements(int mode, int count, int type, ByteBuffer indices) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
@@ -2358,7 +2435,7 @@ public class GLStateManager {
}
public static void glDrawElements(int mode, int indices_count, int type, long indices_buffer_offset) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
@@ -2401,22 +2478,22 @@ public class GLStateManager {
}
public static void glMultiDrawElementsIndirect(int mode, int type, long indirect, int drawcount, int stride) {
if (Tracy.ENABLED) drawCalls += drawcount;
drawCalls += drawcount;
RENDER_BACKEND.multiDrawElementsIndirect(mode, type, indirect, drawcount, stride);
}
public static void glMultiDrawElementsBaseVertex(int mode, long pCount, int type, long pIndices, int drawcount, long pBaseVertex) {
if (Tracy.ENABLED) drawCalls += drawcount;
drawCalls += drawcount;
RENDER_BACKEND.multiDrawElementsBaseVertex(mode, pCount, type, pIndices, drawcount, pBaseVertex);
}
public static void glDrawElementsBaseVertex(int mode, int count, int type, long indices, int basevertex) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
RENDER_BACKEND.drawElementsBaseVertex(mode, count, type, indices, basevertex);
}
public static void glDrawElementsInstanced(int mode, int count, int type, long indices, int primcount) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadElementsAsTrianglesInstanced(count, type, indices, primcount);
return;
@@ -2426,7 +2503,7 @@ public class GLStateManager {
}
public static void glDrawArraysInstanced(int mode, int first, int count, int primcount) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
if (mode == GL11.GL_QUADS) {
QuadConverter.drawQuadsAsTrianglesInstanced(first, count, primcount);
} else if (mode == GL11.GL_QUAD_STRIP) {
@@ -2476,7 +2553,7 @@ public class GLStateManager {
}
public static void glDrawArrays(int mode, int first, int count) {
if (Tracy.ENABLED) drawCalls++;
drawCalls++;
CommandRecorder savedRecorder = null;
final RecordMode recordMode = DisplayListManager.getRecordMode();
if (recordMode != RecordMode.NONE) {
@@ -4442,7 +4519,10 @@ public class GLStateManager {
RENDER_BACKEND.readPixels(x, y, width, height, format, type, pixels);
restorePixelPackBuffer();
}
public static void glTexStorage2D(int target, int levels, int internalFormat, int width, int height) { RENDER_BACKEND.texStorage2D(target, levels, internalFormat, width, height); }
public static void glTexStorage2D(int target, int levels, int internalFormat, int width, int height) {
RENDER_BACKEND.texStorage2D(target, levels, internalFormat, width, height);
TextureInfoCache.INSTANCE.onTexStorage2D(target, internalFormat, width, height);
}
public static void glClearTexImage(int texture, int level, int format, int type, ByteBuffer data) { RENDER_BACKEND.clearTexImage(texture, level, format, type); }
public static int glGenSamplers() { return RENDER_BACKEND.genSamplers(); }
@@ -4968,7 +5048,39 @@ public class GLStateManager {
}
public static void setVSyncEnabled(boolean enabled) {
RENDER_BACKEND.setVSyncEnabled(enabled);
setVSyncMode(RENDER_BACKEND.getPreferredVSyncMode(), enabled);
}
public static void setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
RENDER_BACKEND.setVSyncMode(preferred, vsyncEnabled);
}
public static VSyncMode getEffectiveVSyncMode() {
return RENDER_BACKEND.getEffectiveVSyncMode();
}
public static List<VSyncMode> getSelectableVSyncModes() {
return VSyncMode.selectable(RENDER_BACKEND::supportsVSyncMode);
}
public static int getMinRenderAhead() {
return RENDER_BACKEND.getMinRenderAhead();
}
public static int getMaxRenderAhead() {
return RENDER_BACKEND.getMaxRenderAhead();
}
public static int renderAheadLimit(int configured) {
return RenderBackend.clampRenderAhead(configured, RENDER_BACKEND.getMinRenderAhead(), RENDER_BACKEND.getMaxRenderAhead());
}
public static void applyRenderAheadLimit(int configured) {
RENDER_BACKEND.setRenderAheadLimit(renderAheadLimit(configured));
}
public static int getDisplayRefreshRateHz() {
return RENDER_BACKEND.getDisplayRefreshRateHz();
}
public static int glGetError() {
@@ -5051,6 +5163,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
maybeGenerateMipmap(target, level);
}
@@ -5066,6 +5179,7 @@ public class GLStateManager {
suspendPixelUnpackBuffer();
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels);
restorePixelUnpackBuffer();
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
maybeGenerateMipmap(target, level);
}
@@ -5075,6 +5189,7 @@ public class GLStateManager {
throw DisplayListManager.unsupportedInList("glTexSubImage2D with buffer offset");
}
RENDER_BACKEND.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, pixels_buffer_offset);
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
maybeGenerateMipmap(target, level);
}
@@ -5115,6 +5230,7 @@ public class GLStateManager {
throw DisplayListManager.unsupportedInList("glCopyTexSubImage2D");
}
RENDER_BACKEND.copyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height);
TextureInfoCache.INSTANCE.onTexSubImage2D(target, level);
maybeGenerateMipmap(target, level);
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.states.PixelUnpackState;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import java.nio.ByteBuffer;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
public final class LTWWorkaround {
private LTWWorkaround() {}
private static ByteBuffer oneTexel;
public static boolean isLtwVersionString(String glVersion) {
return glVersion != null && glVersion.contains("LTW");
}
public static void onTexImage2D(int target, int level, int format, boolean hasPixels) {
if (target != GL11.GL_TEXTURE_2D || level != 0 || format != GL12.GL_BGRA || hasPixels) return;
if (oneTexel == null) {
oneTexel = BufferUtils.createByteBuffer(4);
}
oneTexel.clear();
GLStateManager.forcePixelUnpackState(PixelUnpackState.DEFAULT);
GLStateManager.suspendPixelUnpackBuffer();
RENDER_BACKEND.texSubImage2D(target, level, 0, 0, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, oneTexel);
GLStateManager.restorePixelUnpackBuffer();
GLStateManager.restorePixelUnpackState(PixelUnpackState.DEFAULT);
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.dsa.DSAARB;
import com.gtnewhorizons.angelica.glsm.dsa.DSAAccess;
@@ -59,6 +60,9 @@ public class RenderSystem {
private static volatile boolean glesDetected;
private static volatile boolean hasClipCullDistance;
private static volatile boolean isLTW;
private static volatile boolean ltwDetected;
// Sampler object state tracking (null if unsupported)
private static int[] samplers;
@@ -403,6 +407,26 @@ public class RenderSystem {
}
}
public static boolean isLTW() {
if (!ltwDetected) detectLTW();
return isLTW;
}
private static synchronized void detectLTW() {
if (ltwDetected) return;
if (SystemProperties.DISABLE_LTW_WORKAROUND) {
ltwDetected = true;
return;
}
try {
final String v = RENDER_BACKEND.getString(GL11.GL_VERSION);
if (v == null) return;
isLTW = LTWWorkaround.isLtwVersionString(v);
ltwDetected = true;
} catch (Throwable ignored) {
}
}
public static boolean supportsSSBO() {
return supportsSSBO;
}
@@ -4,7 +4,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
public final class BackendManager {
@@ -39,8 +38,8 @@ public final class BackendManager {
if (backend.isAvailable() && (best == null || backend.getPriority() > best.getPriority())) {
best = backend;
}
} catch (ServiceConfigurationError | LinkageError e) {
LOGGER.debug("Skipping unavailable backend: {}", e.getMessage());
} catch (Throwable t) {
LOGGER.debug("Skipping unavailable backend: {}", t.getMessage());
}
}
@@ -10,6 +10,7 @@ import org.lwjgl.opengl.ARBDebugOutputCallback;
import org.lwjgl.opengl.ARBDirectStateAccess;
import org.lwjgl.opengl.ARBTimerQuery;
import org.lwjgl.opengl.ContextCapabilities;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.EXTDirectStateAccess;
import org.lwjgl.opengl.EXTShaderImageLoadStore;
import org.lwjgl.opengl.GLContext;
@@ -114,7 +115,15 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
public int getMinGLSLVersion() {return 330;}
@Override
public void setVSyncEnabled(boolean enabled) {Display.setVSyncEnabled(enabled);}
protected void setSwapInterval(boolean vsync) {Display.setVSyncEnabled(vsync);}
@Override
public int getDisplayRefreshRateHz() {
final DisplayMode mode = Display.getDisplayMode();
if (mode != null && mode.getFrequency() > 0) return mode.getFrequency();
final DisplayMode desktop = Display.getDesktopDisplayMode();
return desktop == null ? 0 : desktop.getFrequency();
}
@Override
public boolean isAnisotropicSupported() {
@@ -1371,6 +1380,16 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL11.glGetFloat(pname, params);
}
@Override
public double getDouble(int pname) {
return GL11.glGetDouble(pname);
}
@Override
public void getDouble(int pname, DoubleBuffer params) {
GL11.glGetDouble(pname, params);
}
@Override
public boolean getBoolean(int pname) {
return GL11.glGetBoolean(pname);
@@ -5,8 +5,6 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import org.lwjgl.opengl.GL20;
import me.eigenraven.lwjgl3ify.api.SwapchainInvalidatingChange;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
@@ -20,7 +18,7 @@ import java.util.List;
*/
public abstract class RenderBackend {
public void onPostWindowCreate(long window, boolean debug) {}
public void onPostWindowCreate(long window) {}
public abstract void init();
public abstract void shutdown();
@@ -64,7 +62,47 @@ public abstract class RenderBackend {
public abstract void flush();
public abstract void finish();
public abstract void setVSyncEnabled(boolean enabled);
protected abstract void setSwapInterval(boolean vsync);
private VSyncMode preferredVSyncMode = VSyncMode.AUTO;
private VSyncMode effectiveVSyncMode = VSyncMode.ON;
public final void setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
preferredVSyncMode = preferred == null ? VSyncMode.AUTO : preferred;
effectiveVSyncMode = applyVSyncMode(preferredVSyncMode, vsyncEnabled);
}
protected VSyncMode applyVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
final VSyncMode mode = preferred.resolve(vsyncEnabled).swapIntervalEquivalent();
setSwapInterval(mode.usesVSyncSwapInterval());
return mode;
}
public boolean supportsVSyncMode(VSyncMode mode) { return mode != VSyncMode.MAILBOX; }
public int getMinRenderAhead() { return 0; }
public int getMaxRenderAhead() { return 9; }
public void setRenderAheadLimit(int frames) {}
public static int clampRenderAhead(int value, int min, int max) {
if (value <= 0) return min <= 0 ? 0 : max;
return Math.min(Math.max(value, Math.max(min, 1)), max);
}
public final VSyncMode getPreferredVSyncMode() { return preferredVSyncMode; }
public final VSyncMode getEffectiveVSyncMode() { return effectiveVSyncMode; }
public int getDisplayRefreshRateHz() { return 0; }
public static int refreshHzFrom(int numerator, int denominator, float fallbackHz) {
if (numerator > 0 && denominator > 0) return (numerator + denominator - 1) / denominator;
return fallbackHz > 0.0f ? (int) Math.ceil(fallbackHz) : 0;
}
public String getPresentDebugInfo() { return null; }
public void onFrameBegin() {
if (GLStateManager.takeStateSeedPending()) {
@@ -75,7 +113,7 @@ public abstract class RenderBackend {
public void onFrameEnd() {}
/** Fired by lwjgl3ify Display before a mutation that recreates the swapchain. */
public void onPreSwapchainInvalidatingChange(SwapchainInvalidatingChange change) {}
public void onPreSwapchainInvalidatingChange(Object change) {}
public boolean handleMakeCurrent(Object drawable) { return false; }
public boolean handleReleaseContext(Object drawable) { return false; }
@@ -84,6 +122,11 @@ public abstract class RenderBackend {
public String getTransferDebugInfo() { return null; }
public boolean bindVoxelizationRegion(int ssboBinding, int vertexBufferGlId, long openPass, float x, float y, float z) { return false; }
public long beginVoxelizationBatch(int ssboBinding) { return 0L; }
public void voxelizeRange(long pass, int vertexOffset, int vertexCount) {}
public void endVoxelizationBatch(long pass) {}
public abstract void enable(int cap);
public abstract void enablei(int cap, int index);
public abstract void disable(int cap);
@@ -389,6 +432,8 @@ public abstract class RenderBackend {
public void getInteger(int pname, int[] params) { if (params.length > 0) params[0] = getInteger(pname); }
public abstract float getFloat(int pname);
public abstract void getFloat(int pname, FloatBuffer params);
public double getDouble(int pname) { return getFloat(pname); }
public void getDouble(int pname, DoubleBuffer params) { if (params.remaining() > 0) params.put(params.position(), getDouble(pname)); }
public abstract boolean getBoolean(int pname);
public abstract void getBoolean(int pname, ByteBuffer params);
public abstract String getString(int pname);
@@ -0,0 +1,54 @@
package com.gtnewhorizons.angelica.glsm.backend;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public enum VSyncMode {
AUTO,
ON,
MAILBOX,
OFF;
private static final List<VSyncMode> OFF_ORDER = List.of(OFF, MAILBOX, ON);
private static final List<VSyncMode> MAILBOX_ORDER = List.of(MAILBOX, ON);
private static final List<VSyncMode> ON_ORDER = List.of(ON);
private static final List<VSyncMode> DISPLAY_ORDER = List.of(OFF, ON, MAILBOX);
public boolean blocksOnVBlank() {
return this == ON;
}
public boolean usesVSyncSwapInterval() {
return this == ON || this == MAILBOX;
}
public VSyncMode swapIntervalEquivalent() {
return this == MAILBOX ? ON : this;
}
public VSyncMode resolve(boolean vsyncEnabled) {
return this != AUTO ? this : (vsyncEnabled ? ON : OFF);
}
public List<VSyncMode> fallbackOrder() {
return switch (this) {
case OFF -> OFF_ORDER;
case MAILBOX -> MAILBOX_ORDER;
default -> ON_ORDER;
};
}
public boolean tearFree() {
return this != OFF;
}
public static List<VSyncMode> selectable(Predicate<VSyncMode> supported) {
final List<VSyncMode> modes = new ArrayList<>(DISPLAY_ORDER.size());
for (VSyncMode mode : DISPLAY_ORDER) {
if (supported.test(mode)) modes.add(mode);
}
return modes;
}
}
@@ -21,11 +21,11 @@ public final class EcosystemNarrowRules {
"Reika.DragonAPI.ASM.StructureLootHooks"
)),
new NarrowRule("Xaeros_common", "xaero.common.core", List.of(
new NarrowRule("Xaeros", "xaero.common.core", List.of(
"xaero.common.core.transformer"
)),
new NarrowRule("Xaeros_map", "xaero.map.core", List.of(
new NarrowRule("Xaeros", "xaero.map.core", List.of(
"xaero.map.core.transformer"
)),
@@ -78,6 +78,13 @@ public final class EcosystemNarrowRules {
"alfheim.common.core.asm.hook.fixes.CorporeaInputFix",
"alfheim.common.core.asm.hook.fixes.GodAttributesHooks",
"alfheim.common.core.asm.hook.fixes.RecipeAncientWillsFix"
)),
new NarrowRule("LegendsMod", "com.tihyo.legends.management.asm", List.of(
"com.tihyo.legends.management.asm.LegendsLoadingPlugin",
"com.tihyo.legends.management.asm.AbstractTransformer",
"com.tihyo.legends.management.asm.ASMHelper",
"com.tihyo.legends.management.asm.Transformer"
))
);
@@ -25,7 +25,7 @@ public final class TransformerNarrower {
/**
* A rule that replaces a broad transformer exclusion with narrower ones.
* @param name Human-readable name for logging (e.g. "DragonAPI")
* @param name Blackboard/config key suffix. (e.g. "DragonAPI").
* @param broad The broad exclusion to remove (e.g. "Reika.DragonAPI.ASM")
* @param narrow Narrower exclusions to add in its place
*/
@@ -236,6 +236,9 @@ public class GLSMRedirector {
.add("glFogi")
.add("glFrustum")
.add("glGetBoolean")
.add("glGetClipPlane")
.add("glGetDouble")
.add("glGetDoublev", "glGetDouble")
.add("glGetFloat")
.add("glGetFloatv", "glGetFloat")
.add("glGetInteger")
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager;
import org.joml.Matrix4f;
import org.joml.Vector4f;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
/**
@@ -31,4 +32,10 @@ public class ClipPlaneState {
final float[] p = eyePlanes[index];
buf.put(p[0]).put(p[1]).put(p[2]).put(p[3]);
}
public void getEyePlane(int index, DoubleBuffer buf) {
final float[] p = eyePlanes[index];
final int pos = buf.position();
buf.put(pos, p[0]).put(pos + 1, p[1]).put(pos + 2, p[2]).put(pos + 3, p[3]);
}
}
@@ -36,6 +36,12 @@ public class TextureInfo {
@Getter @Setter protected boolean generateMipmap = false;
@Getter protected int uploadGeneration;
void bumpUploadGeneration() {
uploadGeneration++;
}
protected TextureInfo(int id) {
this.id = id;
}
@@ -53,6 +53,7 @@ public class TextureInfoCache {
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
info.bumpUploadGeneration();
}
}
@@ -65,9 +66,29 @@ public class TextureInfoCache {
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
info.bumpUploadGeneration();
}
}
public void onTexStorage2D(int target, int internalformat, int width, int height) {
if (!isTrackedTarget(target)) return;
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
if (info == null) return;
info.setTarget(target);
info.internalFormat = internalformat;
info.resolvedInternalFormat = isGenericCompressedInternalFormat(internalformat) ? -1 : internalformat;
info.width = width;
info.height = height;
info.bumpUploadGeneration();
}
public void onTexSubImage2D(int target, int level) {
if (!isTrackedTarget(target) || level != 0) return;
final TextureInfo info = getInfo(GLStateManager.getBoundTextureForServerState());
if (info == null) return;
info.bumpUploadGeneration();
}
public static boolean isGenericCompressedInternalFormat(int internalFormat) {
return switch (internalFormat) {
case GL30.GL_COMPRESSED_RED,
@@ -0,0 +1,153 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.client.renderer.Tessellator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@GLCoreTest
class GLSM_DisplayList_RawTessellatorTransform_Test {
private int testList = -1;
@AfterEach
void cleanup() {
if (testList > 0) {
GLStateManager.glDeleteLists(testList, 1);
testList = -1;
}
GLStateManager.glLoadIdentity();
}
private static void rawQuad(float size) {
final Tessellator tess = Tessellator.instance;
final float[][] corners = { { 0.0f, 0.0f }, { size, 0.0f }, { size, size }, { 0.0f, size } };
tess.rawBuffer = new int[4 * 8];
for (int i = 0; i < 4; i++) {
final int base = i * 8;
tess.rawBuffer[base] = Float.floatToRawIntBits(corners[i][0]);
tess.rawBuffer[base + 1] = Float.floatToRawIntBits(corners[i][1]);
tess.rawBuffer[base + 3] = Float.floatToRawIntBits(i == 1 || i == 2 ? 1.0f : 0.0f);
tess.rawBuffer[base + 4] = Float.floatToRawIntBits(i >= 2 ? 1.0f : 0.0f);
}
tess.isDrawing = true;
tess.drawMode = GL11.GL_QUADS;
tess.hasTexture = true;
tess.rawBufferIndex = 4 * 8;
tess.vertexCount = 4;
TessellatorManager.interceptDraw(tess);
}
private static void immediateQuad(float size) {
GLStateManager.glBegin(GL11.GL_QUADS);
GLStateManager.glVertex3f(0.0f, 0.0f, 0.0f);
GLStateManager.glVertex3f(size, 0.0f, 0.0f);
GLStateManager.glVertex3f(size, size, 0.0f);
GLStateManager.glVertex3f(0.0f, size, 0.0f);
GLStateManager.glEnd();
}
@Test
void pendingTransformIsEmittedBeforeARawDraw() {
testList = GLStateManager.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
GLStateManager.glScalef(0.5f, 0.5f, 0.5f);
rawQuad(1.0f);
GLStateManager.glEndList();
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList);
assertNotNull(compiled);
final IntList opcodes = compiled.getCommandOpcodes();
final int scaleIndex = opcodes.indexOf(GLCommand.SCALE);
final int drawIndex = opcodes.indexOf(GLCommand.DRAW_RANGE);
assertTrue(scaleIndex >= 0, "The pending scale must be recorded");
assertTrue(drawIndex >= 0, "The raw draw must be recorded");
assertTrue(scaleIndex < drawIndex, "Scale must be emitted before the draw it applies to");
assertEquals(1, compiled.getCommandCounts().getOrDefault(GLCommand.SCALE, 0), "Scale emitted once, not again by the end-of-list flush");
}
@Test
void pendingTransformIsNotEmittedBeforeABakedDraw() {
testList = GLStateManager.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
GLStateManager.glScalef(0.5f, 0.5f, 0.5f);
immediateQuad(1.0f);
GLStateManager.glEndList();
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList);
assertNotNull(compiled);
final IntList opcodes = compiled.getCommandOpcodes();
final int scaleIndex = opcodes.indexOf(GLCommand.SCALE);
final int drawIndex = opcodes.indexOf(GLCommand.DRAW_RANGE);
assertTrue(drawIndex >= 0, "The immediate mode draw must be recorded");
assertTrue(scaleIndex < 0 || drawIndex < scaleIndex, "Baked geometry must draw before the delta reaches the stream");
}
@Test
void aRawDrawAfterABakedDrawStillGetsTheTransform() {
testList = GLStateManager.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
GLStateManager.glScalef(0.5f, 0.5f, 0.5f);
immediateQuad(1.0f);
rawQuad(1.0f);
GLStateManager.glEndList();
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList);
assertNotNull(compiled);
final IntList opcodes = compiled.getCommandOpcodes();
final int scaleIndex = opcodes.indexOf(GLCommand.SCALE);
final int lastDrawIndex = opcodes.lastIndexOf(GLCommand.DRAW_RANGE);
final int firstDrawIndex = opcodes.indexOf(GLCommand.DRAW_RANGE);
assertTrue(scaleIndex >= 0, "The pending scale must be recorded");
assertTrue(firstDrawIndex < scaleIndex, "The baked draw stays ahead of the transform");
assertTrue(scaleIndex < lastDrawIndex, "The raw draw still gets the transform");
}
@Test
void aRawDrawDoesNotFlushWhileBakedGeometryIsPending() {
testList = GLStateManager.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
GLStateManager.glScalef(0.5f, 0.5f, 0.5f);
GLStateManager.glBegin(GL11.GL_QUADS);
GLStateManager.glVertex3f(0.0f, 0.0f, 0.0f);
GLStateManager.glVertex3f(1.0f, 0.0f, 0.0f);
GLStateManager.glVertex3f(1.0f, 1.0f, 0.0f);
GLStateManager.glVertex3f(0.0f, 1.0f, 0.0f);
rawQuad(1.0f);
GLStateManager.glEnd();
GLStateManager.glEndList();
final CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList);
assertNotNull(compiled);
final IntList opcodes = compiled.getCommandOpcodes();
final int scaleIndex = opcodes.indexOf(GLCommand.SCALE);
final int drawIndex = opcodes.indexOf(GLCommand.DRAW_RANGE);
assertTrue(drawIndex >= 0, "Draws must be recorded");
assertTrue(scaleIndex < 0 || drawIndex < scaleIndex, "No flush while baked geometry is pending");
}
}
@@ -1,7 +1,9 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.glsm.recording.CompiledDisplayList;
import com.gtnewhorizons.angelica.glsm.recording.GLCommand;
import net.minecraft.client.renderer.Tessellator;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.IntList;
import org.joml.Matrix4f;
@@ -837,6 +839,47 @@ class GLSM_DisplayList_TransformCollapsing_Test {
}
}
@Test
void testRawTessellatorDrawPlaysBackWithTheTransform() {
testList = GL11.glGenLists(1);
GLStateManager.glNewList(testList, GL11.GL_COMPILE);
GLStateManager.glScalef(0.5f, 0.5f, 0.5f);
final Tessellator tess = Tessellator.instance;
final float[][] corners = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f } };
tess.rawBuffer = new int[4 * 8];
for (int i = 0; i < 4; i++) {
final int base = i * 8;
tess.rawBuffer[base] = Float.floatToRawIntBits(corners[i][0]);
tess.rawBuffer[base + 1] = Float.floatToRawIntBits(corners[i][1]);
tess.rawBuffer[base + 3] = Float.floatToRawIntBits(i == 1 || i == 2 ? 1.0f : 0.0f);
tess.rawBuffer[base + 4] = Float.floatToRawIntBits(i >= 2 ? 1.0f : 0.0f);
}
tess.isDrawing = true;
tess.drawMode = GL11.GL_QUADS;
tess.hasTexture = true;
tess.rawBufferIndex = 4 * 8;
tess.vertexCount = 4;
TessellatorManager.interceptDraw(tess);
GLStateManager.glEndList();
CompiledDisplayList compiled = DisplayListManager.getDisplayList(testList);
assertNotNull(compiled);
GLStateManager.glPushMatrix();
GLStateManager.glLoadIdentity();
GLStateManager.glCallList(testList);
FloatBuffer matrixBuf = BufferUtils.createFloatBuffer(16);
GLStateManager.glGetFloat(GL11.GL_MODELVIEW_MATRIX, matrixBuf);
assertEquals(0.5f, matrixBuf.get(0), 0.0001f, "Scale X after playback");
assertEquals(0.5f, matrixBuf.get(5), 0.0001f, "Scale Y after playback");
GLStateManager.glPopMatrix();
}
// ==================== Optimizer Push/Pop Reset Tests ====================
@Test
@@ -0,0 +1,71 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import org.junit.jupiter.api.Test;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@GLCoreTest
public class GLSM_TextureUploadGeneration_GLTest {
private static int allocate(int size) {
final int texId = GLStateManager.glGenTextures();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texId);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(size * size * 4));
return texId;
}
@Test
void anUploadLeavesANonZeroGeneration() {
final int texId = allocate(4);
try {
assertNotEquals(0, TextureInfoCache.INSTANCE.getInfo(texId).getUploadGeneration());
} finally {
GLStateManager.glDeleteTextures(texId);
}
}
@Test
void recreatingATextureAtAReusedIdYieldsAFreshInfo() {
final int texId = allocate(4);
try {
final TextureInfo first = TextureInfoCache.INSTANCE.getInfo(texId);
final int firstGeneration = first.getUploadGeneration();
TextureInfoCache.INSTANCE.onDeleteTexture(texId);
final ByteBuffer pixels = BufferUtils.createByteBuffer(4 * 4 * 4);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 4, 4, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
final TextureInfo second = TextureInfoCache.INSTANCE.getInfo(texId);
assertNotSame(first, second, "a deleted id drops its entry, so the replacement must be a distinct instance");
assertEquals(firstGeneration, second.getUploadGeneration(), "generations alias across a delete/recreate cycle, which is exactly why identity has to carry the distinction");
} finally {
GLStateManager.glDeleteTextures(texId);
}
}
@Test
void anInPlaceRewriteBumpsTheGeneration() {
final int texId = allocate(4);
try {
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(texId);
final int afterAlloc = info.getUploadGeneration();
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 2, 2, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(2 * 2 * 4));
assertNotEquals(afterAlloc, info.getUploadGeneration(), "content changed, so anything caching the pixels is stale");
final int afterSub = info.getUploadGeneration();
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 1, 0, 0, 1, 1, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(4));
assertEquals(afterSub, info.getUploadGeneration(), "only level 0 is tracked");
} finally {
GLStateManager.glDeleteTextures(texId);
}
}
}
@@ -21,6 +21,18 @@ class GLVersionParseTest {
assertEquals(-1, RenderSystem.parseMajorMinor("OpenGL ES 3.2"));
}
@Test
void detectsLtwWrapper() {
assertTrue(LTWWorkaround.isLtwVersionString("3.3 OpenLTW (Built on: May 21 2026/12:38:14)"));
assertFalse(LTWWorkaround.isLtwVersionString(null));
assertFalse(LTWWorkaround.isLtwVersionString(""));
assertFalse(LTWWorkaround.isLtwVersionString("4.6.0 NVIDIA 551.61"));
assertFalse(LTWWorkaround.isLtwVersionString("4.6 (Core Profile) Mesa 24.0.3"));
assertFalse(LTWWorkaround.isLtwVersionString("4.1 Metal - 88"));
assertFalse(LTWWorkaround.isLtwVersionString("OpenGL ES 3.2 ANGLE (Vulkan)"));
assertFalse(LTWWorkaround.isLtwVersionString("2.1 gl4es wrapper"));
}
@Test
void isContextValid() {
assertTrue(RenderSystem.isContextValid(4, 4, 46, true));
@@ -0,0 +1,120 @@
package com.gtnewhorizons.angelica.glsm.backend;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BackendPolicyTest {
@Test
void autoFollowsTheVsyncToggle() {
assertEquals(VSyncMode.ON, VSyncMode.AUTO.resolve(true));
assertEquals(VSyncMode.OFF, VSyncMode.AUTO.resolve(false));
}
@Test
void anExplicitModeIgnoresTheVsyncToggle() {
for (VSyncMode mode : new VSyncMode[]{ VSyncMode.ON, VSyncMode.MAILBOX, VSyncMode.OFF }) {
assertEquals(mode, mode.resolve(true));
assertEquals(mode, mode.resolve(false));
}
}
@Test
void onlyOnBlocks() {
assertTrue(VSyncMode.ON.blocksOnVBlank());
assertFalse(VSyncMode.MAILBOX.blocksOnVBlank());
assertFalse(VSyncMode.OFF.blocksOnVBlank());
}
@Test
void mailboxRidesOnSwapIntervalOne() {
assertTrue(VSyncMode.ON.usesVSyncSwapInterval());
assertTrue(VSyncMode.MAILBOX.usesVSyncSwapInterval());
assertFalse(VSyncMode.OFF.usesVSyncSwapInterval());
}
@Test
void swapIntervalBackendsReportWhatTheyActuallyGet() {
assertEquals(VSyncMode.ON, VSyncMode.MAILBOX.swapIntervalEquivalent());
assertEquals(VSyncMode.ON, VSyncMode.ON.swapIntervalEquivalent());
assertEquals(VSyncMode.OFF, VSyncMode.OFF.swapIntervalEquivalent());
}
@Test
void fallbackNeverWidensTearing() {
assertEquals(List.of(VSyncMode.ON), VSyncMode.ON.fallbackOrder());
assertEquals(List.of(VSyncMode.MAILBOX, VSyncMode.ON), VSyncMode.MAILBOX.fallbackOrder());
assertEquals(List.of(VSyncMode.OFF, VSyncMode.MAILBOX, VSyncMode.ON), VSyncMode.OFF.fallbackOrder());
}
@Test
void onlyOffCanTear() {
assertTrue(VSyncMode.ON.tearFree());
assertTrue(VSyncMode.MAILBOX.tearFree());
assertFalse(VSyncMode.OFF.tearFree());
}
@Test
void onlyBackendSupportedModesAreSelectable() {
assertEquals(List.of(VSyncMode.OFF, VSyncMode.ON), VSyncMode.selectable(m -> m != VSyncMode.MAILBOX));
assertEquals(List.of(VSyncMode.OFF, VSyncMode.ON, VSyncMode.MAILBOX), VSyncMode.selectable(m -> true));
}
@Test
void autoIsNeverSelectable() {
assertFalse(VSyncMode.selectable(m -> true).contains(VSyncMode.AUTO));
}
@Test
void refreshPrefersTheExactRational() {
assertEquals(144, RenderBackend.refreshHzFrom(144, 1, 0.0f));
assertEquals(60, RenderBackend.refreshHzFrom(60000, 1001, 0.0f));
}
@Test
void refreshRoundsUp() {
assertEquals(144, RenderBackend.refreshHzFrom(0, 0, 143.98f));
assertEquals(60, RenderBackend.refreshHzFrom(0, 0, 59.94f));
assertEquals(60, RenderBackend.refreshHzFrom(0, 0, 60.0f));
}
@Test
void refreshIsZeroWhenUnknown() {
assertEquals(0, RenderBackend.refreshHzFrom(0, 0, 0.0f));
assertEquals(0, RenderBackend.refreshHzFrom(-1, -1, -1.0f));
assertEquals(0, RenderBackend.refreshHzFrom(144, 0, 0.0f));
}
@Test
void glRenderAheadKeepsZeroAsOff() {
assertEquals(0, RenderBackend.clampRenderAhead(0, 0, 9));
assertEquals(0, RenderBackend.clampRenderAhead(-1, 0, 9));
}
@Test
void glRenderAheadClampsToItsRange() {
assertEquals(1, RenderBackend.clampRenderAhead(1, 0, 9));
assertEquals(3, RenderBackend.clampRenderAhead(3, 0, 9));
assertEquals(9, RenderBackend.clampRenderAhead(9, 0, 9));
assertEquals(9, RenderBackend.clampRenderAhead(20, 0, 9));
}
@Test
void sdlRenderAheadHasNoOffSwitch() {
assertEquals(3, RenderBackend.clampRenderAhead(0, 1, 3));
assertEquals(3, RenderBackend.clampRenderAhead(-1, 1, 3));
}
@Test
void sdlRenderAheadClampsToItsRange() {
assertEquals(1, RenderBackend.clampRenderAhead(1, 1, 3));
assertEquals(2, RenderBackend.clampRenderAhead(2, 1, 3));
assertEquals(3, RenderBackend.clampRenderAhead(3, 1, 3));
assertEquals(3, RenderBackend.clampRenderAhead(6, 1, 3));
}
}
@@ -0,0 +1,92 @@
package com.gtnewhorizons.angelica.glsm.loading;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TransformerNarrowerTest {
private static final String LEGENDS_BROAD = "com.tihyo.legends.management.asm";
private static final String LEGENDS_HOOK = LEGENDS_BROAD + ".ASMClientMethods";
private static Set<String> exceptionsWith(String... entries) {
return new HashSet<>(Set.of(entries));
}
private static boolean isExcluded(Set<String> exceptions, String className) {
return exceptions.stream().anyMatch(className::startsWith);
}
private static int narrow(Set<String> exceptions, Map<String, Object> blackboard) {
return TransformerNarrower.narrow(exceptions, blackboard, "angelica", "Angelica", EcosystemNarrowRules.ALL);
}
@Test
void legendsHooksBecomeTransformableButTransformersDoNot() {
final Set<String> exceptions = exceptionsWith(LEGENDS_BROAD);
assertEquals(1, narrow(exceptions, new HashMap<>()));
assertFalse(isExcluded(exceptions, LEGENDS_HOOK));
assertFalse(isExcluded(exceptions, LEGENDS_BROAD + ".ASMMethods"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".LegendsLoadingPlugin"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".AbstractTransformer"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".ASMHelper"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerRenderPlayer"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerEntityRenderer"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerMethodProcess"));
assertTrue(isExcluded(exceptions, LEGENDS_BROAD + ".TransformerGuiInventory"));
}
@Test
void disabledRuleLeavesBroadExclusionIntact() {
final Set<String> exceptions = exceptionsWith(LEGENDS_BROAD);
final Map<String, Object> blackboard = new HashMap<>();
blackboard.put("angelica.narrow.LegendsMod", Boolean.FALSE);
assertEquals(0, narrow(exceptions, blackboard));
assertTrue(exceptions.contains(LEGENDS_BROAD));
assertTrue(isExcluded(exceptions, LEGENDS_HOOK));
}
@Test
void sharedXaerosKeyGovernsBothExclusions() {
final Set<String> enabled = exceptionsWith("xaero.common.core", "xaero.map.core");
assertEquals(2, narrow(enabled, new HashMap<>()));
assertFalse(isExcluded(enabled, "xaero.common.gui.GuiMap"));
assertFalse(isExcluded(enabled, "xaero.map.gui.GuiMap"));
assertTrue(isExcluded(enabled, "xaero.common.core.transformer.Foo"));
assertTrue(isExcluded(enabled, "xaero.map.core.transformer.Foo"));
final Set<String> disabled = exceptionsWith("xaero.common.core", "xaero.map.core");
final Map<String, Object> blackboard = new HashMap<>();
blackboard.put("angelica.narrow.Xaeros", Boolean.FALSE);
assertEquals(0, narrow(disabled, blackboard));
assertTrue(disabled.contains("xaero.common.core"));
assertTrue(disabled.contains("xaero.map.core"));
}
@Test
void narrowingIsIdempotent() {
final Set<String> exceptions = exceptionsWith(LEGENDS_BROAD);
assertEquals(1, narrow(exceptions, new HashMap<>()));
final Set<String> afterFirst = new HashSet<>(exceptions);
assertEquals(0, narrow(exceptions, new HashMap<>()));
assertEquals(afterFirst, exceptions);
}
@Test
void absentBroadExclusionIsNoOp() {
final Set<String> exceptions = exceptionsWith("some.unrelated.package");
assertEquals(0, narrow(exceptions, new HashMap<>()));
assertEquals(Set.of("some.unrelated.package"), exceptions);
}
}
@@ -32,6 +32,7 @@ import org.lwjgl.opengl.EXTPolygonOffsetClamp;
import org.lwjgl.opengl.EXTShaderImageLoadStore;
import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.opengl.GLDebugMessageCallback;
import org.lwjgl.sdl.SDL_DisplayMode;
import org.lwjgl.sdl.SDL_DropEvent;
import org.lwjgl.sdl.SDL_EventFilter;
import org.lwjgl.sdl.SDLEvents;
@@ -39,6 +40,7 @@ import org.lwjgl.sdl.SDLVideo;
import org.lwjgl.system.JNI;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjglx.opengl.Display;
import java.nio.Buffer;
import java.nio.ByteBuffer;
@@ -169,7 +171,17 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
public int getMinGLSLVersion() {return 330;}
@Override
public void setVSyncEnabled(boolean enabled) {SDLVideo.SDL_GL_SetSwapInterval(enabled ? 1 : 0);}
protected void setSwapInterval(boolean vsync) {SDLVideo.SDL_GL_SetSwapInterval(vsync ? 1 : 0);}
@Override
public int getDisplayRefreshRateHz() {
final long window = Display.getWindow();
final int displayId = window == 0 ? SDLVideo.SDL_GetPrimaryDisplay() : SDLVideo.SDL_GetDisplayForWindow(window);
if (displayId == 0) return 0;
final SDL_DisplayMode mode = SDLVideo.SDL_GetCurrentDisplayMode(displayId);
if (mode == null) return 0;
return refreshHzFrom(mode.refresh_rate_numerator(), mode.refresh_rate_denominator(), mode.refresh_rate());
}
@Override
public boolean isAnisotropicSupported() {
@@ -1504,6 +1516,16 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
GL11C.glGetFloatv(pname, params);
}
@Override
public double getDouble(int pname) {
return GL11C.glGetDouble(pname);
}
@Override
public void getDouble(int pname, DoubleBuffer params) {
GL11C.glGetDoublev(pname, params);
}
@Override
public boolean getBoolean(int pname) {
return GL11C.glGetBoolean(pname);
@@ -2,11 +2,18 @@ package com.gtnewhorizons.angelica.sdlgpu;
import static org.lwjgl.sdl.SDLVideo.SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
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.lwjgl.sdl.SDLProperties;
import org.lwjgl.system.Platform;
import org.lwjglx.Sys;
import org.lwjglx.input.Mouse;
import org.lwjglx.opengl.ContextAttribs;
import org.lwjglx.opengl.Display;
import org.lwjglx.opengl.PixelFormat;
@@ -14,21 +21,67 @@ import org.lwjglx.opengl.PixelFormat;
import me.eigenraven.lwjgl3ify.api.DisplayEvents;
import me.eigenraven.lwjgl3ify.api.DisplayWindowContext;
import me.eigenraven.lwjgl3ify.api.SwapchainInvalidatingChange;
import me.eigenraven.lwjgl3ify.client.MainThreadExec;
import java.nio.ByteBuffer;
import java.util.function.Consumer;
public final class SDLGPUGate {
private static final Logger LOG = LogManager.getLogger("Angelica/SDLGPU");
private SDLGPUGate() {}
private static volatile boolean fatalInit;
private static Device device;
public static void markFatalInit() { fatalInit = true; }
private static volatile Boolean deviceReady;
private static volatile boolean engaged;
private static volatile boolean disarmed;
private static volatile Throwable initFailure;
private static boolean listenersRegistered;
private static ByteBuffer[] windowIcons;
public static boolean isFatalInit() { return fatalInit; }
public static void rememberIcons(ByteBuffer[] icons) {
windowIcons = icons;
}
public static synchronized Device device() {
if (device == null) device = new Device();
return device;
}
private static final boolean SDL_GPU_AVAILABLE = SDLGPUGate.class.getClassLoader().getResource("org/lwjgl/sdl/SDLGPU.class") != null;
public static boolean isSDLGPUAvailable() {
return SDLGPUGate.class.getClassLoader().getResource("org/lwjgl/sdl/SDLGPU.class") != null;
return SDL_GPU_AVAILABLE;
}
public static boolean isDeviceReady() {
final Boolean ready = deviceReady;
return ready != null ? ready : probe();
}
private static synchronized boolean probe() {
if (deviceReady != null) return deviceReady;
if (!SystemProperties.USE_SDL_GPU || !isSDLGPUAvailable()) {
deviceReady = false;
return false;
}
boolean ok;
try {
Sys.initialize();
ok = MainThreadExec.runOnMainThread(device()::createDevice);
} catch (Throwable t) {
LOG.error("SDL GPU device probe failed", t);
ok = false;
}
if (!ok) resetSdlLogging();
deviceReady = ok;
return ok;
}
public static boolean isEngaged() {
return engaged;
}
public static boolean isActive() {
@@ -37,24 +90,88 @@ public final class SDLGPUGate {
return rb instanceof SDLGPURenderBackend;
}
public static Consumer<DisplayWindowContext> sdlGpuPreWindowCreateCallback() {
if (Platform.get() != Platform.MACOSX) return null;
return ctx -> Sys.checkSdl(SDLProperties.SDL_SetBooleanProperty(ctx.props(), SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN, true));
public static void ensureDrawableInstalled() {
if (!isActive()) return;
SDLGPUDisplayBridge.ensureDrawableInstalled();
}
public static Consumer<DisplayWindowContext> sdlGpuPostWindowCreateCallback(boolean debug) {
return ctx -> BackendManager.RENDER_BACKEND.onPostWindowCreate(ctx.window(), debug);
public static void prewarmSpirv(String transformedSource, int glShaderType) {
if (!isActive()) return;
ShaderManager.prewarmSpirv(transformedSource, glShaderType);
}
public static void clearShaderPrewarmCache() {
if (!isActive()) return;
ShaderManager.clearPrewarmCache();
}
public static Consumer<SwapchainInvalidatingChange> sdlGpuPreSwapchainInvalidatingCallback() {
return change -> BackendManager.RENDER_BACKEND.onPreSwapchainInvalidatingChange(change);
}
public static void createSDLGPUDisplay(Object format, Object attribs, boolean debug) {
public static boolean createSDLGPUDisplay(Object format, Object attribs) {
if (!isDeviceReady()) return false;
initFailure = null;
disarmed = false;
registerListeners();
DisplayEvents.setCreateGLContext(false);
final Consumer<DisplayWindowContext> pre = sdlGpuPreWindowCreateCallback(); // null on non-Mac
if (pre != null) DisplayEvents.addPreWindowCreateListener(pre);
DisplayEvents.addPostWindowCreateListener(sdlGpuPostWindowCreateCallback(debug));
Display.create((PixelFormat) format, (ContextAttribs) attribs, /*sharedWindow*/ 0L);
final Throwable failure = initFailure;
if (failure != null) {
initFailure = null;
LOG.error("SDL GPU could not take the window", failure);
return false;
}
return engaged;
}
private static void registerListeners() {
if (listenersRegistered) return;
listenersRegistered = true;
if (Platform.get() == Platform.MACOSX) {
DisplayEvents.addPreWindowCreateListener((DisplayWindowContext ctx) -> {
if (disarmed) return;
Sys.checkSdl(SDLProperties.SDL_SetBooleanProperty(ctx.props(), SDL_PROP_WINDOW_CREATE_METAL_BOOLEAN, true));
});
}
DisplayEvents.addPostWindowCreateListener((DisplayWindowContext ctx) -> {
if (disarmed) return;
try {
device().claimWindow(ctx.window());
engaged = true;
BackendManager.RENDER_BACKEND.onPostWindowCreate(ctx.window());
} catch (Throwable t) {
initFailure = t;
}
});
}
public static void fallBackToGL() {
disarmed = true;
engaged = false;
deviceReady = false;
device().destroyDevice();
resetSdlLogging();
Mouse.setGrabbed(false);
DisplayEvents.setCreateGLContext(true);
if (Display.isCreated()) {
Display.destroy();
}
Display.isCloseRequested();
if (windowIcons != null) {
Display.setIcon(windowIcons);
}
}
private static void resetSdlLogging() {
SDLLog.SDL_ResetLogPriorities();
}
}
@@ -20,7 +20,7 @@ import java.util.Arrays;
/** SDL GPU implementation of {@link LWJGLService} for Celeritas */
public final class SDLGPULWJGLService extends LWJGLService {
@Override public int getPriority() {
if (SystemProperties.USE_SDL_GPU && SDLGPUGate.isSDLGPUAvailable()) {
if (SystemProperties.USE_SDL_GPU && SDLGPUGate.isSDLGPUAvailable() && SDLGPUGate.isEngaged()) {
return 200;
}
return Integer.MIN_VALUE;
@@ -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;
@@ -96,7 +97,6 @@ import org.lwjglx.opengl.Display;
import me.eigenraven.lwjgl3ify.api.DisplayEvents;
import com.gtnewhorizons.angelica.sdlgpu.compat.Lwjgl3GLCapabilitiesShim;
import me.eigenraven.lwjgl3ify.api.GLCapabilitiesOverride;
import me.eigenraven.lwjgl3ify.api.SwapchainInvalidatingChange;
import static org.lwjgl.sdl.SDLGPU.*;
@@ -106,7 +106,7 @@ public class SDLGPURenderBackend extends RenderBackend {
private static final Tracy.ZoneId Z_SDL_INDIRECT_DRAW = Tracy.zoneId("sdlIndirectDraw", Tracy.COLOR_TERRAIN);
private final Device device = new Device();
private final Device device = SDLGPUGate.device();
private final FrameManager frameManager = new FrameManager(device);
private final ResourceManager resourceManager = new ResourceManager(device, frameManager);
private final Image3DClear image3DClear = new Image3DClear();
@@ -213,8 +213,7 @@ public class SDLGPURenderBackend extends RenderBackend {
persistentSync.onPersistentBufferWrite(glId, offset, size);
}
@Override public void onPostWindowCreate(long window, boolean debug) {
device.claimWindow(window, debug);
@Override public void onPostWindowCreate(long window) {
buildCachedStrings();
populateGLCapabilities();
}
@@ -227,6 +226,7 @@ public class SDLGPURenderBackend extends RenderBackend {
LOG.info("SDL-GPU debug markers ENABLED (lwjglDebug={}, angelica.debug.markers={}, captureTool={})", lwjglDebugFlag, SystemProperties.DEBUG_MARKERS, CaptureGate.TOOL_ATTACHED);
}
frameManager.setResourceManager(resourceManager);
device.setLossDiagnostics(resourceManager::describeGpuState);
frameManager.setBeforeEndCopyPassCallback(() -> resourceManager.flushBatchedUploads(frameManager.getCopyPass()));
frameManager.setBeforeSubmitCallback(() -> {
endComputeDispatchBatch();
@@ -234,7 +234,6 @@ public class SDLGPURenderBackend extends RenderBackend {
drainDeferredPersistentRegions(s());
debugLabels.popAutoDebugGroup(s());
});
frameManager.setBeforePendingUploadSubmitCallback(textureOps::drainPendingMipGen);
frameManager.setAfterPresentCallback(() -> {
frameManager.requestFlushOnAllRegisteredFrames();
resourceManager.drainPendingFreeShadows();
@@ -377,8 +376,26 @@ public class SDLGPURenderBackend extends RenderBackend {
LOG.info("Populated GL capabilities for SDL GPU backend");
}
@Override public void setVSyncEnabled(boolean enabled) {
device.setVSyncEnabled(enabled);
@Override protected void setSwapInterval(boolean vsync) {
device.setVSyncMode(VSyncMode.AUTO, vsync);
}
@Override protected VSyncMode applyVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
return device.setVSyncMode(preferred, vsyncEnabled);
}
@Override public boolean supportsVSyncMode(VSyncMode mode) {
return mode == VSyncMode.AUTO || device.supportsVSyncMode(mode);
}
@Override public int getMinRenderAhead() { return Device.MIN_FRAMES_IN_FLIGHT; }
@Override public int getMaxRenderAhead() { return Device.MAX_FRAMES_IN_FLIGHT; }
@Override public void setRenderAheadLimit(int frames) { device.setFramesInFlight(frames); }
@Override public int getDisplayRefreshRateHz() {
return device.getDisplayRefreshRateHz();
}
@Override public void shutdown() {
@@ -434,7 +451,7 @@ public class SDLGPURenderBackend extends RenderBackend {
}
@Override public boolean isAvailable() {
return SystemProperties.USE_SDL_GPU && SDLGPUGate.isSDLGPUAvailable();
return SystemProperties.USE_SDL_GPU && SDLGPUGate.isSDLGPUAvailable() && SDLGPUGate.isEngaged();
}
@Override public String getName() {
@@ -551,9 +568,13 @@ public class SDLGPURenderBackend extends RenderBackend {
}
ResourceManager.setSingleThreadedReads(GLStateManager.isSplashComplete());
st.clearedTexturesThisFrame.clear();
st.clearedStencilTexturesThisFrame.clear();
st.pendingColorTextures.clear();
st.pendingDepthTextures.clear();
st.pendingStencilTextures.clear();
st.pendingSwapchainClear = false;
st.pendingSwapchainDepthClear = false;
st.pendingSwapchainStencilClear = false;
st.uniformBlocks[0].flushedThisFrame = false;
st.uniformBlocks[1].flushedThisFrame = false;
st.fanIndexBufferOffset = 0;
@@ -630,7 +651,7 @@ public class SDLGPURenderBackend extends RenderBackend {
resourceManager.recycleGpuBufferPool(frameManager.getFrameNumber());
}
@Override public void onPreSwapchainInvalidatingChange(SwapchainInvalidatingChange change) {
@Override public void onPreSwapchainInvalidatingChange(Object change) {
endFrameUploadFlush();
if (frameManager.isFrameActive()) {
frameManager.endFrame();
@@ -679,6 +700,54 @@ public class SDLGPURenderBackend extends RenderBackend {
return transferThread != null ? transferThread.getDebugInfo() : null;
}
private static final long PRESENT_STATS_WINDOW_NANOS = 1_000_000_000L;
private static final long PRESENT_STATS_STALE_NANOS = 2_000_000_000L;
private long presentStatsNanos;
private long presentStatsLoopFrames;
private long presentStatsPresentedFrames;
private long presentStatsSkips;
private long presentStatsEmpty;
private long presentStatsAcquireNanos;
private String presentDebugLine;
@Override public String getPresentDebugInfo() {
if (shutdown) return null;
final long now = System.nanoTime();
final long elapsed = now - presentStatsNanos;
if (presentStatsNanos == 0L || elapsed > PRESENT_STATS_STALE_NANOS) {
snapshotPresentStats(now);
presentDebugLine = null;
return null;
}
if (elapsed < PRESENT_STATS_WINDOW_NANOS) return presentDebugLine;
final long loopDelta = frameManager.statLoopFrames() - presentStatsLoopFrames;
final long presentedDelta = frameManager.statPresentedFrames() - presentStatsPresentedFrames;
final long skipDelta = frameManager.statPresentSkips() - presentStatsSkips;
final long emptyDelta = frameManager.statEmptyFrames() - presentStatsEmpty;
final long acquireDelta = frameManager.statAcquireWaitNanos() - presentStatsAcquireNanos;
final double seconds = elapsed / 1.0e9;
presentDebugLine = String.format("Present: %s %dHz | loop %.0f/s shown %.0f/s | skip %.0f/s empty %.0f/s | acq %.2fms | fif %d",
getEffectiveVSyncMode(), device.getDisplayRefreshRateHz(),
loopDelta / seconds, presentedDelta / seconds,
skipDelta / seconds, emptyDelta / seconds,
loopDelta == 0 ? 0.0 : acquireDelta / (double) loopDelta / 1.0e6,
Device.framesInFlight());
snapshotPresentStats(now);
return presentDebugLine;
}
private void snapshotPresentStats(long now) {
presentStatsNanos = now;
presentStatsLoopFrames = frameManager.statLoopFrames();
presentStatsPresentedFrames = frameManager.statPresentedFrames();
presentStatsSkips = frameManager.statPresentSkips();
presentStatsEmpty = frameManager.statEmptyFrames();
presentStatsAcquireNanos = frameManager.statAcquireWaitNanos();
}
@Override public void enable(int cap) { setBoolCap(cap, true); }
@Override public void enablei(int cap, int index) { setBoolCapIndexed(cap, index, true); }
@Override public void disable(int cap) { setBoolCap(cap, false); }
@@ -871,14 +940,15 @@ public class SDLGPURenderBackend extends RenderBackend {
}
@Override public void clearDepth(double depth) { s().depthClearValue = (float) depth; }
@Override public void clearStencil(int s) {}
@Override public void clearStencil(int s) { s().stencilClearValue = s & 0xFF; }
@Override public void clear(int mask) {
final ContextState cs = s();
if (!frameManager.isFrameActive()) return;
final boolean wantColor = (mask & GL11.GL_COLOR_BUFFER_BIT) != 0;
final boolean wantDepth = (mask & GL11.GL_DEPTH_BUFFER_BIT) != 0;
if (!wantColor && !wantDepth) return;
final boolean wantStencil = (mask & GL11.GL_STENCIL_BUFFER_BIT) != 0;
if (!wantColor && !wantDepth && !wantStencil) return;
final ContextState st = cs;
@@ -891,7 +961,15 @@ public class SDLGPURenderBackend extends RenderBackend {
st.pendingSwapchainB = st.clearB;
st.pendingSwapchainA = st.clearA;
}
// swapchain has no depth attachment in this backend; ignore wantDepth
final int swapDepthFormat = resourceManager.getSwapchainDepthStencilFormat();
if (wantDepth && swapDepthFormat != 0) {
st.pendingSwapchainDepthClear = true;
st.pendingSwapchainDepth = st.depthClearValue;
}
if (wantStencil && PixelOps.isDepthStencilFormat(swapDepthFormat)) {
st.pendingSwapchainStencilClear = true;
st.pendingSwapchainStencil = st.stencilClearValue;
}
return;
}
@@ -916,6 +994,10 @@ public class SDLGPURenderBackend extends RenderBackend {
FBOClearTracker.recordPendingDepthClear(st, fbo.depthTexture, cs.depthClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
}
if (wantStencil && fbo.depthTexture != 0 && PixelOps.isDepthStencilFormat(fbo.depthFormat)) {
FBOClearTracker.recordPendingStencilClear(st, fbo.depthTexture, cs.stencilClearValue);
if (passActive && fbo.depthTexture == activeDepth) affectsActivePass = true;
}
if (affectsActivePass) {
frameManager.endRenderPassIfActive();
@@ -1461,14 +1543,12 @@ public class SDLGPURenderBackend extends RenderBackend {
if (framebuffer == 0) {
cs.pipeline.setColorTargetFormats(cachedSwapchainFormatArray);
cs.pipeline.setDrawBuffers(null);
cs.pipeline.hasDepthTarget = false;
cs.pipeline.depthTargetFormat = 0;
cs.pipeline.setDepthTargetFormat(resourceManager.getSwapchainDepthStencilFormat());
} else {
final FboState fbo = resourceManager.getFbo(framebuffer);
if (fbo != null && fbo.getColorTexture() != 0) {
fboClearTracker.updatePipelineCacheColorFormats(cs, fbo);
cs.pipeline.hasDepthTarget = fbo.depthTexture != 0;
cs.pipeline.depthTargetFormat = fbo.depthFormat;
cs.pipeline.setDepthTargetFormat(fbo.depthTexture != 0 ? fbo.depthFormat : 0);
}
}
cs.pipeline.markOutputDirty();
@@ -1505,14 +1585,23 @@ public class SDLGPURenderBackend extends RenderBackend {
fbo.cachedFormatsDirty = true;
fboClearTracker.updatePipelineCacheColorFormats(cs, fbo);
cs.pipeline.markOutputDirty();
} else if (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT) {
} else if (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT || attachment == GL30.GL_STENCIL_ATTACHMENT) {
final boolean stencilOnly = attachment == GL30.GL_STENCIL_ATTACHMENT;
if (texture == 0) {
if (stencilOnly && fbo.depthGlId != 0) return;
fbo.detachDepth();
cs.pipeline.hasDepthTarget = false;
cs.pipeline.depthTargetFormat = 0;
cs.pipeline.markOutputDirty();
return;
}
if (stencilOnly && fbo.depthGlId != 0 && fbo.depthGlId != texture) {
if (!warnedSplitStencilAttachment) {
warnedSplitStencilAttachment = true;
LOG.warn("Separate stencil attachment (tex {}) on an FBO whose depth attachment is tex {}; SDL has no separate stencil aspect, keeping depth", texture, fbo.depthGlId);
}
return;
}
final long handle = resourceManager.ensureTextureUsage(texture, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET);
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture);
fbo.depthTexture = handle;
@@ -1676,22 +1765,67 @@ public class SDLGPURenderBackend extends RenderBackend {
}
@Override public int getFramebufferAttachmentParameteri(int target, int attachment, int pname) {
final FboState fbo = resourceManager.getFbo(s().boundFboId);
final ContextState cs = s();
final int fboId = target == GL30.GL_READ_FRAMEBUFFER ? cs.boundReadFboId : cs.boundFboId;
if (fboId == 0) {
final int colorFormat = cachedSwapchainFormatArray != null ? cachedSwapchainFormatArray[0] : 0;
return defaultFramebufferAttachmentParameteri(attachment, pname, colorFormat, resourceManager.getSwapchainDepthStencilFormat());
}
return fboAttachmentParameteri(resourceManager.getFbo(fboId), attachment, pname);
}
static int fboAttachmentParameteri(FboState fbo, int attachment, int pname) {
if (fbo == null) return 0;
final int colorIdx = attachment - GL30.GL_COLOR_ATTACHMENT0;
final boolean isColor = (colorIdx >= 0 && colorIdx < ContextState.MAX_COLOR_ATTACHMENTS);
final boolean isDepth = (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT);
final boolean isDepth = (attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT || attachment == GL30.GL_STENCIL_ATTACHMENT || attachment == GL11.GL_DEPTH || attachment == GL11.GL_STENCIL);
final boolean colorPresent = isColor && fbo.colorGlIds[colorIdx] != 0;
final boolean depthPresent = isDepth && fbo.depthGlId != 0;
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME) {
return isColor ? fbo.colorGlIds[colorIdx] : isDepth ? fbo.depthGlId : 0;
return colorPresent ? fbo.colorGlIds[colorIdx] : depthPresent ? fbo.depthGlId : 0;
}
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) {
if (isColor && fbo.colorGlIds[colorIdx] != 0) return GL11.GL_TEXTURE;
if (isDepth && fbo.depthGlId != 0) return GL11.GL_TEXTURE;
return GL11.GL_NONE;
return (colorPresent || depthPresent) ? GL11.GL_TEXTURE : GL11.GL_NONE;
}
if (colorPresent) return colorAttachmentSize(fbo.colorFormats[colorIdx], pname);
if (depthPresent) return depthAttachmentSize(fbo.depthFormat, pname);
return 0;
}
static int defaultFramebufferAttachmentParameteri(int attachment, int pname, int colorFormat, int depthFormat) {
final boolean isDepth = attachment == GL11.GL_DEPTH || attachment == GL11.GL_STENCIL || attachment == GL30.GL_DEPTH_ATTACHMENT || attachment == GL30.GL_STENCIL_ATTACHMENT || attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT;
if (isDepth) {
if (depthFormat == 0) {
return pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE ? GL11.GL_NONE : 0;
}
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) return GL30.GL_FRAMEBUFFER_DEFAULT;
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME) return 0;
return depthAttachmentSize(depthFormat, pname);
}
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE) return GL30.GL_FRAMEBUFFER_DEFAULT;
if (pname == GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME) return 0;
return colorAttachmentSize(colorFormat, pname);
}
private static int colorAttachmentSize(int sdlFormat, int pname) {
return switch (pname) {
case GL30.GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE -> PixelOps.colorChannelBits(sdlFormat, 0);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE -> PixelOps.colorChannelBits(sdlFormat, 1);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE -> PixelOps.colorChannelBits(sdlFormat, 2);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE -> PixelOps.colorChannelBits(sdlFormat, 3);
default -> 0;
};
}
private static int depthAttachmentSize(int sdlFormat, int pname) {
return switch (pname) {
case GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE -> PixelOps.depthBits(sdlFormat);
case GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE -> PixelOps.stencilBits(sdlFormat);
default -> 0;
};
}
@Override public int createShader(int type) { return shaderManager.createShader(type); }
@Override public void deleteShader(int shader) { shaderManager.deleteShader(shader); }
@Override public void shaderSource(int shader, CharSequence source) { shaderManager.shaderSource(shader, source); }
@@ -3201,6 +3335,8 @@ public class SDLGPURenderBackend extends RenderBackend {
case GL11.GL_DRAW_BUFFER -> drawBufferEnum(cs.boundFboId);
case GL11.GL_READ_BUFFER -> readBufferEnum(cs.boundReadFboId);
case GL11.GL_TEXTURE_BINDING_2D -> boundTextureOf(cs.activeTextureUnit, cs.boundTextures);
case GL11.GL_DEPTH_BITS -> getFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE);
case GL11.GL_STENCIL_BITS -> getFramebufferAttachmentParameteri(GL30.GL_DRAW_FRAMEBUFFER, GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE);
default -> {
final int limit = deviceLimit(pname);
yield limit >= 0 ? limit : unknownGetInteger(pname);
@@ -3521,7 +3657,7 @@ public class SDLGPURenderBackend extends RenderBackend {
private int voxLocStart = -1;
private int voxLocCount = -1;
public boolean bindVoxelizationRegion(int ssboBinding, int vertexBufferGlId, long openPass, float x, float y, float z) {
@Override public boolean bindVoxelizationRegion(int ssboBinding, int vertexBufferGlId, long openPass, float x, float y, float z) {
if (ssboBinding < 0 || ssboBinding >= ContextState.MAX_INDEXED_BUFFERS || vertexBufferGlId == 0) return false;
final ContextState st = s();
if (st.boundProgram == 0) return false;
@@ -3540,7 +3676,7 @@ public class SDLGPURenderBackend extends RenderBackend {
return names == null ? new String[0] : names.clone();
}
public long beginVoxelizationBatch(int ssboBinding) {
@Override public long beginVoxelizationBatch(int ssboBinding) {
if (ssboBinding < 0 || ssboBinding >= ContextState.MAX_INDEXED_BUFFERS) return 0;
final ContextState st = s();
final int program = st.boundProgram;
@@ -3550,11 +3686,11 @@ public class SDLGPURenderBackend extends RenderBackend {
return voxelizationDispatcher.beginBatch(st);
}
public void voxelizeRange(long pass, int vertexOffset, int vertexCount) {
@Override public void voxelizeRange(long pass, int vertexOffset, int vertexCount) {
voxelizationDispatcher.dispatchRange(pass, voxLocStart, voxLocCount, vertexOffset, vertexCount, s());
}
public void endVoxelizationBatch(long pass) {
@Override public void endVoxelizationBatch(long pass) {
voxelizationDispatcher.endBatch(pass);
}
@@ -3648,6 +3784,7 @@ public class SDLGPURenderBackend extends RenderBackend {
}
private boolean warnedMSAARenderbuffer = false;
private boolean warnedSplitStencilAttachment = false;
@Override
public void renderbufferStorageMultisample(int target, int samples, int internalformat, int width, int height) {
@@ -1,7 +1,8 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
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;
@@ -12,12 +13,14 @@ import org.lwjgl.sdl.SDLStdinc;
import org.lwjgl.sdl.SDL_DisplayMode;
import org.lwjgl.sdl.SDL_LogOutputFunction;
import org.lwjgl.sdl.SDLVersion;
import org.lwjgl.sdl.SDLVulkan;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.system.Platform;
import org.lwjglx.opengl.Display;
import java.nio.IntBuffer;
import java.util.function.Supplier;
import static org.lwjgl.sdl.SDLGPU.*;
import static org.lwjgl.sdl.SDLHints.SDL_HINT_GPU_DRIVER;
@@ -28,23 +31,67 @@ import static org.lwjgl.system.MemoryUtil.memUTF8;
public final class Device {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
public static final int FRAMES_IN_FLIGHT = 3;
public static final int MIN_FRAMES_IN_FLIGHT = 1;
public static final int DEFAULT_FRAMES_IN_FLIGHT = 2;
public static final int MAX_FRAMES_IN_FLIGHT = 3;
private static volatile int framesInFlight = DEFAULT_FRAMES_IN_FLIGHT;
public static int framesInFlight() { return framesInFlight; }
private static final int REQUESTED_SHADER_FORMATS = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXBC | SDL_GPU_SHADERFORMAT_DXIL;
private long device;
private boolean claimed;
private int supportedShaderFormats;
private String driverName;
private String deviceName;
private String driverVersion;
private String driverInfo;
private int presentMode = SDL_GPU_PRESENTMODE_VSYNC;
private VSyncMode vsyncMode = VSyncMode.ON;
private volatile String windowThreadName;
private SDL_LogOutputFunction logCallback; // prevent GC
private volatile boolean lost;
private volatile Supplier<String> lossDiagnostics = () -> "";
public boolean isLost() { return lost; }
public void setLossDiagnostics(Supplier<String> diagnostics) { this.lossDiagnostics = diagnostics; }
public static boolean isDeviceLossError(String sdlError) {
if (sdlError == null) return false;
return sdlError.contains("DEVICE_LOST") || sdlError.contains("DEVICE_REMOVED") || sdlError.contains("DEVICE_RESET");
}
public void reportGpuFailure(String operation) {
if (lost) return;
final String err = SDLError.SDL_GetError();
if (!isDeviceLossError(err)) {
LOG.error("{}: {}", operation, err);
return;
}
lost = true;
LOG.error("GPU DEVICE LOST during {}: {}", operation, err);
LOG.error(" gpu={} driver={} {} sdl={}", deviceName, driverName, driverVersion, SDLVersion.SDL_GetVersion());
for (final String line : lossDiagnostics.get().split("\n")) {
LOG.error(" {}", line);
}
throw new GpuDeviceLostException(operation, err);
}
// Default to vulkan for now until D3D is more tested
static String resolveDriverName(String userHint, boolean windows) {
return userHint.isEmpty() && windows ? "vulkan" : null;
}
public void claimWindow(long window, boolean lwjglDebug) {
static boolean metalNeedsNewerSdl(String driverName, int sdlVersion) {
return "metal".equalsIgnoreCase(driverName) && sdlVersion < SDLVersion.SDL_VERSIONNUM(3, 4, 6);
}
public boolean createDevice() {
if (device != 0) return true;
installLogCallback();
final int ver = SDLVersion.SDL_GetVersion();
@@ -56,17 +103,15 @@ public final class Device {
LOG.info("SDL GPU driver hint set to '{}'", driverHint);
}
final int requestedFormats = SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXBC | SDL_GPU_SHADERFORMAT_DXIL;
final boolean gpuDebug = lwjglDebug || SystemProperties.SDL_GPU_DEBUG;
final boolean gpuDebug = SystemProperties.LWJGL_DEBUG || SystemProperties.SDL_GPU_DEBUG;
final String forcedDriver = resolveDriverName(driverHint, Platform.get() == Platform.WINDOWS);
device = SDL_CreateGPUDevice(requestedFormats, gpuDebug, forcedDriver);
if (device == 0 && forcedDriver != null) {
LOG.warn("Failed to create a '{}' SDL GPU device ({}); falling back to SDL's default backend. Set -D{} to pin one.", forcedDriver, SDLError.SDL_GetError(), SystemProperties.KEY_SDL_GPU_DRIVER);
device = SDL_CreateGPUDevice(requestedFormats, gpuDebug, (CharSequence) null);
device = createGPUDevice(gpuDebug, forcedDriver);
if (device == 0 && VideoDriverRecovery.shouldRetryOnX11(SDL_GetCurrentVideoDriver(), vulkanLoaderAvailable(), System.getenv("DISPLAY"))) {
device = VideoDriverRecovery.retryUnderX11(() -> createGPUDevice(gpuDebug, forcedDriver));
}
if (device == 0) {
throw new RuntimeException("Failed to create SDL GPU device: " + SDLError.SDL_GetError());
logDeviceCreationFailure(REQUESTED_SHADER_FORMATS);
return false;
}
driverName = SDL_GetGPUDeviceDriver(device);
@@ -77,12 +122,29 @@ public final class Device {
driverVersion = SDLProperties.SDL_GetStringProperty(props, SDL_PROP_GPU_DEVICE_DRIVER_VERSION_STRING, "");
driverInfo = SDLProperties.SDL_GetStringProperty(props, SDL_PROP_GPU_DEVICE_DRIVER_INFO_STRING, "");
LOG.info("SDL GPU device created: gpu={}, driver={} {}, formats=0x{}, debug={}, forcedDriver={}", deviceName, driverName, driverVersion, Integer.toHexString(supportedShaderFormats), gpuDebug, forcedDriver);
LOG.info("SDL GPU device created: gpu={}, driver={} {}, video={}, formats=0x{}, debug={}, forcedDriver={}", deviceName, driverName, driverVersion, SDL_GetCurrentVideoDriver(), Integer.toHexString(supportedShaderFormats), gpuDebug, forcedDriver);
if ("metal".equalsIgnoreCase(driverName) && ver < SDLVersion.SDL_VERSIONNUM(3, 4, 6)) {
LOG.fatal("SDL GPU on Metal requires SDL 3.4.6 or newer; exiting");
SDLGPUGate.markFatalInit();
return;
if (metalNeedsNewerSdl(driverName, ver)) {
LOG.error("SDL GPU on Metal requires SDL 3.4.6 or newer (have {}.{}.{}); falling back to OpenGL", SDLVersion.SDL_VERSIONNUM_MAJOR(ver), SDLVersion.SDL_VERSIONNUM_MINOR(ver), SDLVersion.SDL_VERSIONNUM_MICRO(ver));
destroyDevice();
return false;
}
return true;
}
private long createGPUDevice(boolean gpuDebug, String forcedDriver) {
long dev = SDL_CreateGPUDevice(REQUESTED_SHADER_FORMATS, gpuDebug, forcedDriver);
if (dev == 0 && forcedDriver != null) {
LOG.warn("Failed to create a '{}' SDL GPU device ({}); falling back to SDL's default backend. Set -D{} to pin one.", forcedDriver, SDLError.SDL_GetError(), SystemProperties.KEY_SDL_GPU_DRIVER);
dev = SDL_CreateGPUDevice(REQUESTED_SHADER_FORMATS, gpuDebug, (CharSequence) null);
}
return dev;
}
public void claimWindow(long window) {
if (device == 0) {
throw new IllegalStateException("SDL GPU device not created");
}
if (window == 0) {
@@ -92,18 +154,60 @@ public final class Device {
if (!SDL_ClaimWindowForGPUDevice(device, window)) {
throw new RuntimeException("Failed to claim window for SDL GPU device: " + SDLError.SDL_GetError());
}
final int n = FRAMES_IN_FLIGHT;
if (!SDL_SetGPUAllowedFramesInFlight(device, n)) {
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}", n, SDLError.SDL_GetError());
} else {
LOG.info("SDL frames-in-flight set to {}", n);
}
claimed = true;
windowThreadName = Thread.currentThread().getName();
logWindowDiagnostics(window);
LOG.info("SDL GPU device claimed window successfully");
}
private static boolean vulkanLoaderAvailable() {
if (Platform.get() == Platform.MACOSX) return false;
if (!SDLVulkan.SDL_Vulkan_LoadLibrary((CharSequence) null)) return false;
SDLVulkan.SDL_Vulkan_UnloadLibrary();
return true;
}
private static void logDeviceCreationFailure(int requestedFormats) {
final StringBuilder drivers = new StringBuilder();
final int n = SDL_GetNumGPUDrivers();
for (int i = 0; i < n; i++) {
if (i > 0) drivers.append(", ");
drivers.append(SDL_GetGPUDriver(i));
}
final String videoDriver = SDL_GetCurrentVideoDriver();
LOG.error("SDL GPU device creation failed. platform={}, videoDriver={}, requestedShaderFormats=0x{}, compiledGpuDrivers=[{}]", Platform.get(), videoDriver, Integer.toHexString(requestedFormats), drivers);
if (Platform.get() != Platform.MACOSX) {
if (SDLVulkan.SDL_Vulkan_LoadLibrary((CharSequence) null)) {
SDLVulkan.SDL_Vulkan_UnloadLibrary();
LOG.error("A Vulkan loader is present, so no GPU met SDL's requirements");
} else {
LOG.error("No usable Vulkan loader: {}", SDLError.SDL_GetError());
if (!"x11".equalsIgnoreCase(videoDriver)) {
LOG.error("Retrying under SDL_VIDEODRIVER=x11 did not help either.");
}
}
}
LOG.error("Continuing on OpenGL. Set -D{}=false to skip this probe entirely.", SystemProperties.KEY_USE_SDL_GPU);
}
public void destroyDevice() {
if (device == 0) return;
if (claimed) {
final long window = Display.getWindow();
if (window != 0) SDL_ReleaseWindowFromGPUDevice(device, window);
claimed = false;
}
SDL_DestroyGPUDevice(device);
device = 0;
driverName = null;
deviceName = null;
driverVersion = null;
driverInfo = null;
supportedShaderFormats = 0;
}
private void logWindowDiagnostics(long window) {
final long flags = SDL_GetWindowFlags(window);
final boolean hasMetal = (flags & SDL_WINDOW_METAL) != 0;
@@ -146,12 +250,7 @@ public final class Device {
public void shutdown() {
if (device != 0) {
final long window = Display.getWindow();
if (window != 0) {
SDL_ReleaseWindowFromGPUDevice(device, window);
}
SDL_DestroyGPUDevice(device);
device = 0;
destroyDevice();
LOG.info("SDL GPU device destroyed");
}
}
@@ -164,6 +263,10 @@ public final class Device {
return driverName;
}
public String getWindowThreadName() {
return windowThreadName;
}
public String getDeviceName() {
return deviceName;
}
@@ -192,22 +295,56 @@ public final class Device {
return (supportedShaderFormats & SDL_GPU_SHADERFORMAT_DXBC) != 0;
}
public void setVSyncEnabled(boolean enabled) {
final long window = Display.getWindow();
final int mode;
if (enabled) {
mode = SDL_GPU_PRESENTMODE_VSYNC;
} else if (SDL_WindowSupportsGPUPresentMode(device, window, SDL_GPU_PRESENTMODE_MAILBOX)) {
mode = SDL_GPU_PRESENTMODE_MAILBOX;
} else {
mode = SDL_GPU_PRESENTMODE_IMMEDIATE;
}
if (!SDL_SetGPUSwapchainParameters(device, window, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode)) {
LOG.warn("Failed to set present mode {}: {} (keeping prior mode {})", mode, SDLError.SDL_GetError(), presentMode);
public void setFramesInFlight(int frames) {
final int n = Math.min(Math.max(frames, MIN_FRAMES_IN_FLIGHT), MAX_FRAMES_IN_FLIGHT);
if (n == framesInFlight || device == 0 || !claimed) return;
if (!SDL_SetGPUAllowedFramesInFlight(device, n)) {
LOG.warn("SDL_SetGPUAllowedFramesInFlight({}) failed: {}; keeping {}", n, SDLError.SDL_GetError(), framesInFlight);
return;
}
presentMode = mode;
LOG.info("SDL present mode set to {}", mode);
framesInFlight = n;
LOG.info("SDL frames-in-flight set to {}", n);
}
public VSyncMode setVSyncMode(VSyncMode preferred, boolean vsyncEnabled) {
final long window = Display.getWindow();
final VSyncMode requested = preferred.resolve(vsyncEnabled);
for (VSyncMode candidate : requested.fallbackOrder()) {
final int sdlMode = toSdl(candidate);
if (!SDL_WindowSupportsGPUPresentMode(device, window, sdlMode)) continue;
if (!SDL_SetGPUSwapchainParameters(device, window, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, sdlMode)) {
LOG.warn("Failed to set present mode {}: {}", candidate, SDLError.SDL_GetError());
continue;
}
vsyncMode = candidate;
LOG.info("SDL present mode {} (requested {}, preference {}, vsync={}), refresh {}Hz, supported: vsync={} mailbox={} immediate={}",
candidate, requested, preferred, vsyncEnabled, getDisplayRefreshRateHz(),
supportsVSyncMode(VSyncMode.ON), supportsVSyncMode(VSyncMode.MAILBOX), supportsVSyncMode(VSyncMode.OFF));
return candidate;
}
LOG.warn("No usable present mode for request {}; keeping {}", requested, vsyncMode);
return vsyncMode;
}
public boolean supportsVSyncMode(VSyncMode mode) {
return device != 0 && claimed && SDL_WindowSupportsGPUPresentMode(device, Display.getWindow(), toSdl(mode));
}
private static int toSdl(VSyncMode mode) {
return switch (mode) {
case OFF -> SDL_GPU_PRESENTMODE_IMMEDIATE;
case MAILBOX -> SDL_GPU_PRESENTMODE_MAILBOX;
default -> SDL_GPU_PRESENTMODE_VSYNC;
};
}
public int getDisplayRefreshRateHz() {
final long window = Display.getWindow();
final int displayId = window == 0 ? SDL_GetPrimaryDisplay() : SDL_GetDisplayForWindow(window);
if (displayId == 0) return 0;
final SDL_DisplayMode mode = SDL_GetCurrentDisplayMode(displayId);
if (mode == null) return 0;
return RenderBackend.refreshHzFrom(mode.refresh_rate_numerator(), mode.refresh_rate_denominator(), mode.refresh_rate());
}
public int getSwapchainTextureFormat() {
@@ -218,6 +355,7 @@ public final class Device {
private void installLogCallback() {
if (logCallback != null) return;
logCallback = SDL_LogOutputFunction.create((userdata, category, priority, message) -> {
if (lost) return;
final String msg = memUTF8(message);
switch (priority) {
case SDLLog.SDL_LOG_PRIORITY_ERROR, SDLLog.SDL_LOG_PRIORITY_CRITICAL -> LOG.error("[SDL] {}", msg);
@@ -0,0 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
public final class GpuDeviceLostException extends Error {
public GpuDeviceLostException(String operation, String sdlError) {
super("GPU device lost during " + operation + ": " + sdlError
+ ". A lost device cannot be recovered, so the game cannot continue. Please report this at "
+ "https://github.com/GTNewHorizons/Angelica/issues and attach your full log file (logs/fml-client-latest.log)");
}
}
@@ -0,0 +1,58 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDLHints;
import org.lwjgl.sdl.SDLInit;
import java.util.function.LongSupplier;
import static org.lwjgl.sdl.SDLInit.SDL_INIT_VIDEO;
import static org.lwjgl.sdl.SDLVideo.SDL_GetCurrentVideoDriver;
public final class VideoDriverRecovery {
private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU");
private static final String X11 = "x11";
private VideoDriverRecovery() {}
static boolean shouldRetryOnX11(String videoDriver, boolean vulkanLoaderAvailable, String displayEnv) {
if (vulkanLoaderAvailable) return false;
if (!"wayland".equalsIgnoreCase(videoDriver)) return false;
return displayEnv != null && !displayEnv.isEmpty();
}
static long retryUnderX11(LongSupplier createDevice) {
final String original = SDL_GetCurrentVideoDriver();
LOG.warn("SDL GPU is unavailable under the '{}' video driver; retrying under {}", original, X11);
if (!restartVideo(X11)) {
LOG.warn("Could not restart the SDL video subsystem under {}: {}", X11, SDLError.SDL_GetError());
restartVideo(original);
return 0L;
}
final long device = createDevice.getAsLong();
if (device == 0) {
LOG.warn("SDL GPU is unavailable under {} as well; restoring the '{}' video driver", X11, original);
restartVideo(original);
return 0L;
}
LOG.warn("SDL video driver switched to {} so the SDL GPU backend can run. Set SDL_VIDEODRIVER=wayland to keep the native session and run on OpenGL instead.", X11);
return device;
}
private static boolean restartVideo(String driver) {
SDLInit.SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (driver == null || driver.isEmpty()) {
SDLHints.SDL_ResetHint(SDLHints.SDL_HINT_VIDEO_DRIVER);
} else {
SDLHints.SDL_SetHint(SDLHints.SDL_HINT_VIDEO_DRIVER, driver);
}
return SDLInit.SDL_InitSubSystem(SDL_INIT_VIDEO);
}
}
@@ -7,6 +7,7 @@ import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2FloatOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
@@ -92,6 +93,7 @@ public final class ContextState {
public float blendColorR, blendColorG, blendColorB, blendColorA;
public int stencilRef;
public float depthClearValue = 1.0f;
public int stencilClearValue;
public boolean primitiveRestartEnabled;
public int primitiveRestartSentinel;
@@ -292,13 +294,20 @@ public final class ContextState {
public int boundReadFboId;
public final LongOpenHashSet clearedTexturesThisFrame = new LongOpenHashSet();
public final LongOpenHashSet clearedStencilTexturesThisFrame = new LongOpenHashSet();
public final LongOpenHashSet pendingColorTextures = new LongOpenHashSet();
public final Long2ObjectOpenHashMap<float[]> pendingColorValues = new Long2ObjectOpenHashMap<>();
public final LongOpenHashSet pendingDepthTextures = new LongOpenHashSet();
public final Long2FloatOpenHashMap pendingDepthValues = new Long2FloatOpenHashMap();
public final LongOpenHashSet pendingStencilTextures = new LongOpenHashSet();
public final Long2IntOpenHashMap pendingStencilValues = new Long2IntOpenHashMap();
public boolean pendingSwapchainClear;
public float pendingSwapchainR, pendingSwapchainG, pendingSwapchainB, pendingSwapchainA;
public boolean pendingSwapchainDepthClear;
public boolean pendingSwapchainStencilClear;
public float pendingSwapchainDepth;
public int pendingSwapchainStencil;
public final LongArrayList samplerFlushColorHandles = new LongArrayList();
public final IntArrayList samplerFlushColorGlIds = new IntArrayList();
@@ -37,7 +37,6 @@ public final class FrameManager {
private static final Tracy.ZoneId Z_SDL_FENCE_WAIT = Tracy.zoneId("sdlFenceWait", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_SDL_SUBMIT = Tracy.zoneId("sdlSubmit", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_SDL_ACQUIRE_WAIT = Tracy.zoneId("sdlAcquireWait", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_SDL_SWAPCHAIN_WAIT = Tracy.zoneId("sdlSwapchainWait", Tracy.COLOR_SWAP);
private final Device device;
private ResourceManager resourceManager;
@@ -57,6 +56,7 @@ public final class FrameManager {
public long activeLayoutHash;
public long frameNumber;
public boolean clearedThisFrame;
public boolean depthClearedThisFrame;
public boolean swapchainUsedThisFrame;
public boolean swapchainAcquiredThisFrame;
public boolean swapchainUnavailable;
@@ -88,7 +88,6 @@ public final class FrameManager {
public int emptyFramesThisFrame;
public int droppedDrawsThisFrame;
public int computeBatchJoinsThisFrame;
public boolean drawableStarved;
public int clearPassesThisFrame;
public int swapchainPassesThisFrame;
public int fboPassesThisFrame;
@@ -165,13 +164,32 @@ public final class FrameManager {
return tlFrame.get();
}
public FrameState busiestFrame() {
FrameState best = null;
for (final FrameState f : registeredFrames) {
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 MAX_PENDING_UPLOAD_BYTES = 16L * 1024L * 1024L;
public static final int MAX_PENDING_UPLOAD_COMMANDS = 256;
private Runnable beforeEndCopyPass;
private Runnable beforePendingUploadSubmit;
private long statLoopFrames;
private long statPresentedFrames;
private long statPresentSkips;
private long statEmptyFrames;
private long statAcquireWaitNanos;
public long statLoopFrames() { return statLoopFrames; }
public long statPresentedFrames() { return statPresentedFrames; }
public long statPresentSkips() { return statPresentSkips; }
public long statEmptyFrames() { return statEmptyFrames; }
public long statAcquireWaitNanos() { return statAcquireWaitNanos; }
public FrameManager(Device device) {
this.device = device;
@@ -191,7 +209,13 @@ public final class FrameManager {
}
if (!threadLogged) {
threadLogged = true;
LOG.info("First beginFrame() on thread={}; must match the SDL window-creating thread", Thread.currentThread().getName());
final String renderThread = Thread.currentThread().getName();
final String windowThread = device.getWindowThreadName();
if (windowThread != null && !windowThread.equals(renderThread)) {
LOG.warn("Frames are recorded on '{}' but the SDL window was created on '{}'; SDL requires swapchain acquire/wait on the window-creating thread", renderThread, windowThread);
} else {
LOG.info("First beginFrame() on thread={}", renderThread);
}
}
flushPendingUploadCommandBuffer(f);
@@ -202,13 +226,6 @@ public final class FrameManager {
finally { Tracy.endZone(); }
}
if (f.drawableStarved) {
f.drawableStarved = false;
Tracy.beginZone(Z_SDL_SWAPCHAIN_WAIT);
try { SDL_WaitForGPUSwapchain(device.getDevice(), Display.getWindow()); }
finally { Tracy.endZone(); }
}
f.commandBuffer = SDL_AcquireGPUCommandBuffer(device.getDevice());
if (f.commandBuffer == 0) {
throw new RuntimeException("Failed to acquire GPU command buffer: " + SDLError.SDL_GetError());
@@ -226,6 +243,7 @@ public final class FrameManager {
f.currentDepthTarget = 0;
f.frameNumber++;
f.clearedThisFrame = false;
f.depthClearedThisFrame = false;
f.swapchainUsedThisFrame = false;
f.skipPresent = false;
}
@@ -235,14 +253,34 @@ public final class FrameManager {
f.emptyFramesThisFrame++;
}
public void ensureSwapchainRenderPass(float clearR, float clearG, float clearB, float clearA, boolean clear) {
ensureSwapchainRenderPass(frame(), clearR, clearG, clearB, clearA, clear);
static boolean swapchainClearNeedsPassBreak(ContextState st, FrameState f) {
return (st.pendingSwapchainClear || st.pendingSwapchainDepthClear || st.pendingSwapchainStencilClear) && f.renderPass != 0 && f.currentColorTarget == f.swapchainTexture;
}
public void ensureSwapchainRenderPass(FrameState f, float clearR, float clearG, float clearB, float clearA, boolean clear) {
if (!ensureSwapchainAcquired(f)) return; // present-skip; don't bind tex=0
public boolean ensureSwapchainRenderPass(FrameState f, ContextState st) {
final boolean clearColor = st.pendingSwapchainClear;
if (swapchainClearNeedsPassBreak(st, f)) {
endRenderPassIfActive(f);
}
final boolean applied = ensureSwapchainRenderPass(f,
clearColor ? st.pendingSwapchainR : st.clearR,
clearColor ? st.pendingSwapchainG : st.clearG,
clearColor ? st.pendingSwapchainB : st.clearB,
clearColor ? st.pendingSwapchainA : st.clearA,
clearColor,
st.pendingSwapchainDepthClear, st.pendingSwapchainDepthClear ? st.pendingSwapchainDepth : st.depthClearValue,
st.pendingSwapchainStencilClear, st.pendingSwapchainStencilClear ? st.pendingSwapchainStencil : st.stencilClearValue);
if (!applied) return false;
st.pendingSwapchainClear = false;
st.pendingSwapchainDepthClear = false;
st.pendingSwapchainStencilClear = false;
return true;
}
private boolean ensureSwapchainRenderPass(FrameState f, float clearR, float clearG, float clearB, float clearA, boolean clear, boolean clearDepth, float depthValue, boolean clearStencil, int stencilValue) {
if (!ensureSwapchainAcquired(f)) return false; // present-skip; don't bind tex=0
if (f.renderPass != 0 && f.currentColorTarget == f.swapchainTexture) {
return;
return false;
}
endActiveEncoders(f);
@@ -251,6 +289,12 @@ public final class FrameManager {
clear = true;
}
final long depthTexture = resourceManager != null ? resourceManager.getOrCreateSwapchainDepthStencil(f.swapchainWidth, f.swapchainHeight) : 0L;
if (depthTexture != 0 && !f.depthClearedThisFrame) {
clearDepth = true;
clearStencil = true;
}
try (var stack = stackPush()) {
final SDL_GPUColorTargetInfo.Buffer colorTargets = SDL_GPUColorTargetInfo.calloc(1, stack);
final long ctAddr = colorTargets.get(0).address();
@@ -267,19 +311,34 @@ public final class FrameManager {
f.clearedThisFrame = true;
}
SDL_GPUDepthStencilTargetInfo depthTarget = null;
if (depthTexture != 0) {
depthTarget = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long dtAddr = depthTarget.address();
MemoryAccess.putAddress(dtAddr + SDL_GPUDepthStencilTargetInfo.TEXTURE, depthTexture);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putFloat(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, depthValue);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, clearStencil ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putByte(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) stencilValue);
if (clearDepth && clearStencil) f.depthClearedThisFrame = true;
}
if (preRenderPassHook != null) preRenderPassHook.run();
assertNoEncoderActive(f, "SDL_BeginGPURenderPass(swapchain)");
f.renderPass = SDL_BeginGPURenderPass(f.commandBuffer, colorTargets, null);
f.renderPass = SDL_BeginGPURenderPass(f.commandBuffer, colorTargets, depthTarget);
if (f.renderPass == 0) {
throw new RuntimeException("Failed to begin render pass: " + SDLError.SDL_GetError());
}
f.renderPassGeneration++;
f.swapchainPassesThisFrame++;
f.currentColorTarget = f.swapchainTexture;
f.currentDepthTarget = 0;
f.currentDepthTarget = depthTexture;
f.activeLayoutHash = SWAPCHAIN_LAYOUT_HASH;
f.swapchainUsedThisFrame = true;
}
return true;
}
@@ -409,10 +468,6 @@ public final class FrameManager {
this.beforeEndCopyPass = callback;
}
public void setBeforePendingUploadSubmitCallback(Runnable callback) {
this.beforePendingUploadSubmit = callback;
}
public void setBeforeSubmitCallback(Runnable callback) {
this.beforeSubmit = callback;
}
@@ -468,12 +523,13 @@ public final class FrameManager {
try {
if (f.wantFenceOnNextSubmit) {
f.wantFenceOnNextSubmit = false;
if (f.lastAcquiredFence != 0) SDL_ReleaseGPUFence(device.getDevice(), f.lastAcquiredFence);
f.lastAcquiredFence = SDL_SubmitGPUCommandBufferAndAcquireFence(f.commandBuffer);
if (f.lastAcquiredFence == 0) {
LOG.error("Failed to submit+acquireFence GPU command buffer: {}", SDLError.SDL_GetError());
device.reportGpuFailure("submit+acquireFence GPU command buffer");
}
} else if (!SDL_SubmitGPUCommandBuffer(f.commandBuffer)) {
LOG.error("Failed to submit GPU command buffer: {}", SDLError.SDL_GetError());
device.reportGpuFailure("submit GPU command buffer");
}
} finally {
Tracy.endZone();
@@ -482,9 +538,14 @@ public final class FrameManager {
f.commandBuffer = 0;
}
f.drawableStarved = f.presentSkipsThisFrame > 0;
if (Tracy.ENABLED && resourceManager != null) SdlFramePlots.emit(f, resourceManager);
statLoopFrames++;
if (f.swapchainAcquiredThisFrame) statPresentedFrames++;
statPresentSkips += f.presentSkipsThisFrame;
statEmptyFrames += f.emptyFramesThisFrame;
statAcquireWaitNanos += f.acquireWaitNanosThisFrame;
f.lastPlottedRenderPassGen = f.renderPassGeneration;
f.computePassesThisFrame = 0;
f.submitsThisFrame = 0;
@@ -530,12 +591,13 @@ public final class FrameManager {
try {
if (f.wantFenceOnNextSubmit) {
f.wantFenceOnNextSubmit = false;
if (f.lastAcquiredFence != 0) SDL_ReleaseGPUFence(device.getDevice(), f.lastAcquiredFence);
f.lastAcquiredFence = SDL_SubmitGPUCommandBufferAndAcquireFence(f.commandBuffer);
if (f.lastAcquiredFence == 0) {
LOG.error("Mid-frame submit+acquireFence failed: {}", SDLError.SDL_GetError());
device.reportGpuFailure("mid-frame submit+acquireFence");
}
} else if (!SDL_SubmitGPUCommandBuffer(f.commandBuffer)) {
LOG.error("Mid-frame submit failed: {}", SDLError.SDL_GetError());
device.reportGpuFailure("mid-frame submit");
}
} finally {
Tracy.endZone();
@@ -616,7 +678,6 @@ public final class FrameManager {
return;
}
endCopyPassIfActive(f);
if (beforePendingUploadSubmit != null) beforePendingUploadSubmit.run();
if (f.pendingUploadCommandBuffer != 0) {
Tracy.beginZone(Z_SDL_SUBMIT);
final boolean submitted;
@@ -626,7 +687,7 @@ public final class FrameManager {
Tracy.endZone();
}
if (!submitted) {
LOG.error("Failed to submit pending upload command buffer: {}", SDLError.SDL_GetError());
device.reportGpuFailure("submit pending upload command buffer");
}
f.submitsThisFrame++;
f.pendingUploadCommandBuffer = 0;
@@ -800,7 +861,7 @@ public final class FrameManager {
endActiveEncoders(f);
final long t0 = Tracy.ENABLED ? System.nanoTime() : 0L;
final long t0 = System.nanoTime();
final long tex;
final int w;
final int h;
@@ -811,7 +872,7 @@ public final class FrameManager {
final IntBuffer pHeight = stack.ints(0);
Tracy.beginZone(Z_SDL_ACQUIRE_WAIT);
try {
callOk = SDL_AcquireGPUSwapchainTexture(f.commandBuffer, Display.getWindow(), pTexture, pWidth, pHeight);
callOk = SDL_WaitAndAcquireGPUSwapchainTexture(f.commandBuffer, Display.getWindow(), pTexture, pWidth, pHeight);
} finally {
Tracy.endZone();
}
@@ -819,11 +880,11 @@ public final class FrameManager {
w = pWidth.get(0);
h = pHeight.get(0);
} catch (RuntimeException re) {
if (Tracy.ENABLED) f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
f.swapchainUnavailable = true;
throw re;
}
if (Tracy.ENABLED) f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
f.acquireWaitNanosThisFrame += System.nanoTime() - t0;
if (!callOk) {
f.swapchainUnavailable = true;
@@ -97,7 +97,7 @@ final class SdlFramePlots {
final long nowNanos = System.nanoTime();
if (f.lastEndFrameNanos != 0) Tracy.plotInt(PLOT_PRESENT_TO_PRESENT_US, (nowNanos - f.lastEndFrameNanos) / 1000);
f.lastEndFrameNanos = nowNanos;
Tracy.plotInt(PLOT_FRAMES_IN_FLIGHT, Device.FRAMES_IN_FLIGHT);
Tracy.plotInt(PLOT_FRAMES_IN_FLIGHT, Device.framesInFlight());
Tracy.plotInt(PLOT_MIP_GENS, f.mipGensThisFrame);
Tracy.plotInt(PLOT_BLIT_FALLBACKS, f.blitsThisFrame);
Tracy.plotInt(PLOT_PRESENT_SKIPS, f.presentSkipsThisFrame);
@@ -20,6 +20,7 @@ import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.resource.PersistentBufferSync;
import com.gtnewhorizons.angelica.sdlgpu.resource.PersistentMapping;
import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.sampler.SamplerBinder;
import com.gtnewhorizons.angelica.sdlgpu.sampler.StorageBufferBinder;
@@ -108,11 +109,8 @@ public final class PipelineApplier {
}
if (st.boundFboId == 0) {
if (st.pendingSwapchainClear) {
frameManager.ensureSwapchainRenderPass(f, st.pendingSwapchainR, st.pendingSwapchainG, st.pendingSwapchainB, st.pendingSwapchainA, true);
st.pendingSwapchainClear = false;
} else {
frameManager.ensureSwapchainRenderPass(f, st.clearR, st.clearG, st.clearB, st.clearA, false);
if (frameManager.ensureSwapchainRenderPass(f, st)) {
st.pipeline.setDepthTargetFormat(resourceManager.getSwapchainDepthStencilFormat());
}
return;
}
@@ -145,47 +143,50 @@ public final class PipelineApplier {
tex = dummyTex;
}
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.contains(tex);
final boolean firstUse = !pendingClear && tex != dummyTex && !st.clearedTexturesThisFrame.contains(tex);
final boolean defined = tex != dummyTex && resourceManager.isTextureContentDefined(tex);
final boolean firstUse = !pendingClear && tex != dummyTex && !defined;
if (pendingClear || firstUse) proposedClearOps |= 1 << i;
layoutHash = Hashing.fmix64(layoutHash, tex);
layoutHash = Hashing.fmix64(layoutHash, db);
if (pendingClear || firstUse) {
final float[] c = pendingClear ? st.pendingColorValues.get(tex) : null;
layoutHash = foldClearColor(layoutHash, c != null ? c[0] : st.clearR, c != null ? c[1] : st.clearG, c != null ? c[2] : st.clearB, c != null ? c[3] : st.clearA);
}
}
final boolean proposedDepthClear;
final boolean proposedStencilClear;
final boolean depthHasStencil = fbo.depthTexture != 0 && PixelOps.isDepthStencilFormat(fbo.depthFormat);
if (fbo.depthTexture != 0) {
final boolean pendingClear = st.pendingDepthTextures.contains(fbo.depthTexture);
final boolean firstUse = !pendingClear && !st.clearedTexturesThisFrame.contains(fbo.depthTexture);
proposedDepthClear = pendingClear || firstUse;
final boolean pendingStencil = depthHasStencil && st.pendingStencilTextures.contains(fbo.depthTexture);
if (depthHasStencil) {
final boolean firstStencilUse = !pendingStencil && !st.clearedStencilTexturesThisFrame.contains(fbo.depthTexture);
proposedStencilClear = pendingStencil || firstStencilUse;
} else {
proposedStencilClear = false;
}
layoutHash = Hashing.fmix64(layoutHash, fbo.depthTexture);
if (proposedDepthClear) {
layoutHash = foldClearDepth(layoutHash, pendingClear ? st.pendingDepthValues.get(fbo.depthTexture) : st.depthClearValue);
}
if (proposedStencilClear) {
layoutHash = foldClearStencil(layoutHash, pendingStencil ? st.pendingStencilValues.get(fbo.depthTexture) : st.stencilClearValue);
}
} else {
proposedDepthClear = false;
proposedStencilClear = false;
}
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearR));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearG));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearB));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.clearA));
layoutHash = Hashing.fmix64(layoutHash, Float.floatToRawIntBits(st.depthClearValue));
frameManager.endRenderPassIfActive(f);
final boolean reuse = fbo.cachedTargetsValid && fbo.cachedTargetsLayoutHash == layoutHash && fbo.cachedTargetsCount == totalTargets && fbo.cachedClearOpFlags == proposedClearOps && fbo.cachedDepthClearLast == proposedDepthClear;
final boolean reuse = fbo.cachedTargetsValid && fbo.cachedTargetsLayoutHash == layoutHash && fbo.cachedTargetsCount == totalTargets && fbo.cachedClearOpFlags == proposedClearOps && fbo.cachedDepthClearLast == proposedDepthClear && fbo.cachedStencilClearLast == proposedStencilClear;
SDL_GPUColorTargetInfo.Buffer colorTargets = null;
SDL_GPUDepthStencilTargetInfo depthTarget = null;
if (reuse) {
for (int i = 0; i < totalTargets; i++) {
final int db = fbo.drawBuffers[i];
final long tex = (db >= 0 && db < ContextState.MAX_COLOR_ATTACHMENTS)
? fbo.colorTextures[db] : 0L;
if (tex != 0) {
st.pendingColorTextures.remove(tex);
if ((proposedClearOps & (1 << i)) != 0) st.clearedTexturesThisFrame.add(tex);
}
}
if (fbo.depthTexture != 0) {
st.pendingDepthTextures.remove(fbo.depthTexture);
if (proposedDepthClear) st.clearedTexturesThisFrame.add(fbo.depthTexture);
}
if (totalTargets > 0) colorTargets = fbo.cachedColorTargets.position(0).limit(totalTargets);
if (fbo.depthTexture != 0) depthTarget = fbo.cachedDepthTarget;
} else {
@@ -199,9 +200,8 @@ public final class PipelineApplier {
} else {
tex = dummyTex;
}
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.remove(tex);
final boolean firstUse = !pendingClear && tex != dummyTex && !st.clearedTexturesThisFrame.contains(tex);
final boolean clearThis = pendingClear || firstUse;
final boolean pendingClear = tex != dummyTex && st.pendingColorTextures.contains(tex);
final boolean clearThis = (proposedClearOps & (1 << i)) != 0;
final long ctAddr = colorTargets.address() + (long) i * SDL_GPUColorTargetInfo.SIZEOF;
MemoryAccess.putAddress(ctAddr + SDL_GPUColorTargetInfo.TEXTURE, tex);
MemoryAccess.putInt(ctAddr + SDL_GPUColorTargetInfo.LOAD_OP, clearThis ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
@@ -220,36 +220,79 @@ public final class PipelineApplier {
MemoryAccess.putFloat(ccAddr + SDL_FColor.B, st.clearB);
MemoryAccess.putFloat(ccAddr + SDL_FColor.A, st.clearA);
}
if (tex != dummyTex) st.clearedTexturesThisFrame.add(tex);
}
}
}
if (fbo.depthTexture != 0) {
final boolean pendingClear = st.pendingDepthTextures.remove(fbo.depthTexture);
final boolean firstUse = !pendingClear && !st.clearedTexturesThisFrame.contains(fbo.depthTexture);
final boolean clearDepth = pendingClear || firstUse;
final boolean pendingClear = st.pendingDepthTextures.contains(fbo.depthTexture);
depthTarget = fbo.cachedDepthTarget;
final long dtAddr = depthTarget.address();
MemoryAccess.putAddress(dtAddr + SDL_GPUDepthStencilTargetInfo.TEXTURE, fbo.depthTexture);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, proposedDepthClear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
if (clearDepth) {
if (proposedDepthClear) {
final float dv = pendingClear ? st.pendingDepthValues.get(fbo.depthTexture) : st.depthClearValue;
MemoryAccess.putFloat(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, dv);
st.clearedTexturesThisFrame.add(fbo.depthTexture);
}
if (depthHasStencil) {
final boolean pendingStencil = st.pendingStencilTextures.contains(fbo.depthTexture);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, proposedStencilClear ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
if (proposedStencilClear) {
final int sv = pendingStencil ? st.pendingStencilValues.get(fbo.depthTexture) : st.stencilClearValue;
MemoryAccess.putByte(dtAddr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) sv);
}
} else {
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(dtAddr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
}
}
fbo.cachedTargetsLayoutHash = layoutHash;
fbo.cachedTargetsCount = totalTargets;
fbo.cachedClearOpFlags = proposedClearOps;
fbo.cachedDepthClearLast = proposedDepthClear;
fbo.cachedStencilClearLast = proposedStencilClear;
fbo.cachedTargetsValid = true;
}
consumeClears(resourceManager, st, fbo, dummyTex, proposedClearOps, proposedDepthClear, proposedStencilClear, depthHasStencil);
frameManager.beginRenderPass(colorTargets, depthTarget);
frameManager.setActiveLayoutHash(fbo.structuralLayoutHash);
}
static void consumeClears(ResourceManager rm, ContextState st, FboState fbo, long dummyTex, int clearOps, boolean depthClear, boolean stencilClear, boolean depthHasStencil) {
for (int i = 0, n = fbo.drawBuffers.length; i < n; i++) {
final int db = fbo.drawBuffers[i];
final long tex = (db >= 0 && db < ContextState.MAX_COLOR_ATTACHMENTS) ? fbo.colorTextures[db] : 0L;
if (tex == 0 || tex == dummyTex) continue;
st.pendingColorTextures.remove(tex);
if ((clearOps & (1 << i)) != 0) rm.markTextureContentDefined(tex);
}
if (fbo.depthTexture == 0) return;
st.pendingDepthTextures.remove(fbo.depthTexture);
if (depthClear) st.clearedTexturesThisFrame.add(fbo.depthTexture);
if (depthHasStencil) {
st.pendingStencilTextures.remove(fbo.depthTexture);
if (stencilClear) st.clearedStencilTexturesThisFrame.add(fbo.depthTexture);
}
}
static long foldClearColor(long h, float r, float g, float b, float a) {
h = Hashing.fmix64(h, Float.floatToRawIntBits(r));
h = Hashing.fmix64(h, Float.floatToRawIntBits(g));
h = Hashing.fmix64(h, Float.floatToRawIntBits(b));
return Hashing.fmix64(h, Float.floatToRawIntBits(a));
}
static long foldClearDepth(long h, float depth) {
return Hashing.fmix64(h, Float.floatToRawIntBits(depth));
}
static long foldClearStencil(long h, int stencil) {
return Hashing.fmix64(h, stencil);
}
private static final int RING_BLOCK_BYTES = ContextState.MAX_VERTEX_ATTRIBS * 16;
private static final int RING_CHUNK_BYTES = ResourceManager.ATTRIB_RING_CHUNK_BLOCKS * RING_BLOCK_BYTES;
@@ -86,6 +86,17 @@ public final class PipelineCache {
public int stencilCompareMask = 0xFF;
public int stencilWriteMask = 0xFF;
private static final LongOpenHashSet loggedStencilMaskLost = new LongOpenHashSet();
private void warnOnceOnStencilMaskLost() {
if (!stencilTestEnabled || effectiveStencilTestEnabled()) return;
if (stencilFrontCompareOp == SDL_GPU_COMPAREOP_ALWAYS && stencilBackCompareOp == SDL_GPU_COMPAREOP_ALWAYS && stencilFrontPassOp == SDL_GPU_STENCILOP_KEEP && stencilBackPassOp == SDL_GPU_STENCILOP_KEEP)
return;
if (!loggedStencilMaskLost.add(Hashing.packHiLo(programId, debugFboId))) return;
LOG.warn("Stencil mask dropped: bound target has no stencil aspect. fbo={} depthFormat={} program={} frontCmp={} frontPass={} writeMask=0x{}",
debugFboId, textureFormatName(depthTargetFormat), programId, stencilFrontCompareOp, stencilFrontPassOp, Integer.toHexString(stencilWriteMask));
}
public boolean cullEnabled = false;
public int cullFaceMode = SDL_GPU_CULLMODE_BACK;
public int frontFace = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
@@ -141,6 +152,14 @@ public final class PipelineCache {
this.drawBuffers = drawBuffers;
}
public boolean setDepthTargetFormat(int sdlFormat) {
if (depthTargetFormat == sdlFormat && hasDepthTarget == (sdlFormat != 0)) return false;
hasDepthTarget = sdlFormat != 0;
depthTargetFormat = sdlFormat;
markOutputDirty();
return true;
}
private int attachmentForSlot(int slot) {
final int[] db = drawBuffers;
if (db == null || slot < 0 || slot >= db.length) return 0;
@@ -340,7 +359,7 @@ public final class PipelineCache {
private void warnOnceOnUnconsumedFragOutput(int numColorTargets) {
if (maxFragOutputLocation < numColorTargets) return;
if (!loggedUnconsumedFragOutput.add(Hashing.packHiLo(programId, numColorTargets))) return;
LOG.warn("[PipelineCache] program {} declares fragment output location {} but framebuffer {} ({} colour attachment(s)) has {} draw buffer(s) {}; writes past location {} are discarded", programId, maxFragOutputLocation, debugFboId, debugFboAttachments, numColorTargets, Arrays.toString(drawBuffers), numColorTargets - 1);
LOG.warn("[PipelineCache] program {} declares fragment output location {} but framebuffer {} ({} color attachment(s)) has {} draw buffer(s) {}; writes past location {} are discarded", programId, maxFragOutputLocation, debugFboId, debugFboAttachments, numColorTargets, Arrays.toString(drawBuffers), numColorTargets - 1);
}
private long createPipeline(PipelineStore store, ContextState cs) {
@@ -360,6 +379,7 @@ public final class PipelineCache {
try (var stack = stackPush()) {
final int numColorTargets = colorTargetFormats().length;
warnOnceOnUnconsumedFragOutput(numColorTargets);
warnOnceOnStencilMaskLost();
final SDL_GPUColorTargetDescription.Buffer colorDesc = SDL_GPUColorTargetDescription.calloc(numColorTargets, stack);
for (int i = 0; i < numColorTargets; i++) {
final var desc = colorDesc.get(i);
@@ -49,8 +49,19 @@ public final class FBOClearTracker {
st.clearedTexturesThisFrame.remove(tex);
}
public static void recordPendingStencilClear(ContextState st, long tex, int value) {
st.pendingStencilValues.put(tex, value);
if (st.pendingStencilTextures.add(tex)) {
st.pendingMutationGen++;
}
st.clearedStencilTexturesThisFrame.remove(tex);
}
public static boolean fboHasPendingClear(ContextState st, FboState fbo) {
if (fbo.depthTexture != 0 && st.pendingDepthTextures.contains(fbo.depthTexture)) return true;
if (fbo.depthTexture != 0
&& (st.pendingDepthTextures.contains(fbo.depthTexture) || st.pendingStencilTextures.contains(fbo.depthTexture))) {
return true;
}
if (st.pendingColorTextures.isEmpty()) return false;
for (int db : fbo.drawBuffers) {
if (db < 0 || db >= ContextState.MAX_COLOR_ATTACHMENTS) continue;
@@ -67,7 +78,7 @@ public final class FBOClearTracker {
}
public void flushPendingClearsForBoundSamplers(ContextState st) {
if (st.pendingColorTextures.isEmpty() && st.pendingDepthTextures.isEmpty()) return;
if (st.pendingColorTextures.isEmpty() && st.pendingDepthTextures.isEmpty() && st.pendingStencilTextures.isEmpty()) return;
if (st.lastFlushedSamplerBindGen == st.samplerBindGen && st.lastFlushedProgram == st.boundProgram && st.lastFlushedPendingMutationGen == st.pendingMutationGen) {
return;
@@ -115,7 +126,7 @@ public final class FBOClearTracker {
if (st.pendingColorTextures.remove(handle)) {
colorHandles.add(handle);
colorGlIds.add(glTexId);
} else if (st.pendingDepthTextures.remove(handle)) {
} else if (st.pendingDepthTextures.contains(handle) || st.pendingStencilTextures.contains(handle)) {
depthHandles.add(handle);
depthGlIds.add(glTexId);
}
@@ -169,6 +180,7 @@ public final class FBOClearTracker {
MemoryAccess.putFloat(ccAddr + SDL_FColor.B, color[2]);
MemoryAccess.putFloat(ccAddr + SDL_FColor.A, color[3]);
st.clearedTexturesThisFrame.add(handle);
resourceManager.markTextureContentDefined(handle);
consumed[idx] = true;
}
frameManager.noteClearPass();
@@ -180,19 +192,38 @@ public final class FBOClearTracker {
final int dn = depthHandles.size();
for (int i = 0; i < dn; i++) {
final long handle = depthHandles.getLong(i);
final float val = st.pendingDepthValues.get(handle);
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, val);
frameManager.noteClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive();
st.clearedTexturesThisFrame.add(handle);
emitDepthStencilClearPass(st, handle, st.pendingDepthTextures.remove(handle), st.pendingStencilTextures.remove(handle), false);
}
}
private void emitDepthStencilClearPass(ContextState st, long handle, boolean clearDepth, boolean clearStencil, boolean materialized) {
if (!clearDepth && !clearStencil) return;
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, clearDepth ? SDL_GPU_LOADOP_CLEAR : SDL_GPU_LOADOP_LOAD);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
if (clearDepth) {
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, st.pendingDepthValues.get(handle));
}
if (clearStencil) {
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STENCIL_STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putByte(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_STENCIL, (byte) st.pendingStencilValues.get(handle));
}
frameManager.noteClearPass();
if (materialized) frameManager.noteMaterializedClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive();
}
if (clearDepth) {
st.pendingDepthValues.remove(handle);
st.clearedTexturesThisFrame.add(handle);
}
if (clearStencil) {
st.pendingStencilValues.remove(handle);
st.clearedStencilTexturesThisFrame.add(handle);
}
}
@@ -206,28 +237,18 @@ public final class FBOClearTracker {
if (depth) st.pendingDepthValues.remove(handle);
else st.pendingColorValues.remove(handle);
st.clearedTexturesThisFrame.add(handle);
resourceManager.markTextureContentDefined(handle);
st.pendingMutationGen++;
return true;
}
public void materializePendingClearForTexture(ContextState st, long handle) {
if (handle == 0) return;
if (st.pendingDepthTextures.remove(handle)) {
final float val = st.pendingDepthValues.remove(handle);
final boolean depthPending = st.pendingDepthTextures.remove(handle);
final boolean stencilPending = st.pendingStencilTextures.remove(handle);
if (depthPending || stencilPending) {
frameManager.endRenderPassIfActive();
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUDepthStencilTargetInfo dt = SDL_GPUDepthStencilTargetInfo.calloc(stack);
final long addr = dt.address();
MemoryAccess.putAddress(addr + SDL_GPUDepthStencilTargetInfo.TEXTURE, handle);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.LOAD_OP, SDL_GPU_LOADOP_CLEAR);
MemoryAccess.putInt(addr + SDL_GPUDepthStencilTargetInfo.STORE_OP, SDL_GPU_STOREOP_STORE);
MemoryAccess.putFloat(addr + SDL_GPUDepthStencilTargetInfo.CLEAR_DEPTH, val);
frameManager.noteClearPass();
frameManager.noteMaterializedClearPass();
frameManager.beginRenderPass(null, dt);
frameManager.endRenderPassIfActive();
st.clearedTexturesThisFrame.add(handle);
}
emitDepthStencilClearPass(st, handle, depthPending, stencilPending, true);
st.pendingMutationGen++;
return;
}
@@ -250,6 +271,7 @@ public final class FBOClearTracker {
frameManager.beginRenderPass(targets, null);
frameManager.endRenderPassIfActive();
st.clearedTexturesThisFrame.add(handle);
resourceManager.markTextureContentDefined(handle);
}
st.pendingMutationGen++;
}
@@ -262,7 +284,10 @@ public final class FBOClearTracker {
st.pendingColorValues.remove(handle);
st.pendingDepthTextures.remove(handle);
st.pendingDepthValues.remove(handle);
st.pendingStencilTextures.remove(handle);
st.pendingStencilValues.remove(handle);
st.clearedTexturesThisFrame.remove(handle);
st.clearedStencilTexturesThisFrame.remove(handle);
}
public boolean fbosHaveSameAttachments(ContextState st, int a, int b) {
@@ -35,6 +35,7 @@ public final class FboState {
public int cachedTargetsCount = -1;
public int cachedClearOpFlags;
public boolean cachedDepthClearLast;
public boolean cachedStencilClearLast;
public boolean cachedTargetsValid;
public long getColorTexture() { return colorTextures[0]; }
@@ -34,6 +34,85 @@ public final class PixelOps {
return sdlFormat == SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT || sdlFormat == SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
}
public static int depthBits(int sdlFormat) {
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_D16_UNORM -> 16;
case SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT -> 24;
case SDL_GPU_TEXTUREFORMAT_D32_FLOAT, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT -> 32;
default -> 0;
};
}
public static int stencilBits(int sdlFormat) {
return isDepthStencilFormat(sdlFormat) ? 8 : 0;
}
public static int colorChannelBits(int sdlFormat, int channel) {
if (channel < 0 || channel > 3) return 0;
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_A8_UNORM -> channel == 3 ? 8 : 0;
case SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM -> channel == 3 ? 2 : 10;
case SDL_GPU_TEXTUREFORMAT_R11G11B10_UFLOAT -> switch (channel) {
case 0, 1 -> 11;
case 2 -> 10;
default -> 0;
};
case SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM -> switch (channel) {
case 0, 2 -> 5;
case 1 -> 6;
default -> 0;
};
case SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM -> channel == 3 ? 1 : 5;
case SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM -> 4;
default -> channel < sdlFormatColorComponents(sdlFormat) ? uniformChannelBits(sdlFormat) : 0;
};
}
private static int uniformChannelBits(int sdlFormat) {
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_R8_UNORM, SDL_GPU_TEXTUREFORMAT_R8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8_UINT, SDL_GPU_TEXTUREFORMAT_R8_INT,
SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, SDL_GPU_TEXTUREFORMAT_R8G8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8G8_UINT, SDL_GPU_TEXTUREFORMAT_R8G8_INT,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM_SRGB, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UINT,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_INT, SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM,
SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM_SRGB -> 8;
case SDL_GPU_TEXTUREFORMAT_R16_UNORM, SDL_GPU_TEXTUREFORMAT_R16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16_UINT,
SDL_GPU_TEXTUREFORMAT_R16_INT, SDL_GPU_TEXTUREFORMAT_R16G16_UNORM,
SDL_GPU_TEXTUREFORMAT_R16G16_SNORM, SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT,
SDL_GPU_TEXTUREFORMAT_R16G16_UINT, SDL_GPU_TEXTUREFORMAT_R16G16_INT,
SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UNORM, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16G16B16A16_UINT,
SDL_GPU_TEXTUREFORMAT_R16G16B16A16_INT -> 16;
case SDL_GPU_TEXTUREFORMAT_R32_FLOAT, SDL_GPU_TEXTUREFORMAT_R32_UINT,
SDL_GPU_TEXTUREFORMAT_R32_INT, SDL_GPU_TEXTUREFORMAT_R32G32_FLOAT,
SDL_GPU_TEXTUREFORMAT_R32G32_UINT, SDL_GPU_TEXTUREFORMAT_R32G32_INT,
SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT, SDL_GPU_TEXTUREFORMAT_R32G32B32A32_UINT,
SDL_GPU_TEXTUREFORMAT_R32G32B32A32_INT -> 32;
default -> 0;
};
}
private static int sdlFormatColorComponents(int sdlFormat) {
return switch (sdlFormat) {
case SDL_GPU_TEXTUREFORMAT_R8_UNORM, SDL_GPU_TEXTUREFORMAT_R8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8_UINT, SDL_GPU_TEXTUREFORMAT_R8_INT,
SDL_GPU_TEXTUREFORMAT_R16_UNORM, SDL_GPU_TEXTUREFORMAT_R16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16_UINT,
SDL_GPU_TEXTUREFORMAT_R16_INT, SDL_GPU_TEXTUREFORMAT_R32_FLOAT,
SDL_GPU_TEXTUREFORMAT_R32_UINT, SDL_GPU_TEXTUREFORMAT_R32_INT -> 1;
case SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, SDL_GPU_TEXTUREFORMAT_R8G8_SNORM,
SDL_GPU_TEXTUREFORMAT_R8G8_UINT, SDL_GPU_TEXTUREFORMAT_R8G8_INT,
SDL_GPU_TEXTUREFORMAT_R16G16_UNORM, SDL_GPU_TEXTUREFORMAT_R16G16_SNORM,
SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT, SDL_GPU_TEXTUREFORMAT_R16G16_UINT,
SDL_GPU_TEXTUREFORMAT_R16G16_INT, SDL_GPU_TEXTUREFORMAT_R32G32_FLOAT,
SDL_GPU_TEXTUREFORMAT_R32G32_UINT, SDL_GPU_TEXTUREFORMAT_R32G32_INT -> 2;
default -> 4;
};
}
public static int glPixelSize(int format, int type) {
final int packed = GLTypes.packedTexelBytes(type);
if (packed != 0) return packed;
@@ -127,6 +127,10 @@ public final class ResourceManager {
private int preferredD24 = SDL_GPU_TEXTUREFORMAT_D24_UNORM;
private int preferredD24S8 = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
private volatile int swapchainDepthStencilFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
private long swapchainDepthStencil;
private int swapchainDepthStencilWidth;
private int swapchainDepthStencilHeight;
private static final Supplier<ContextState.VAOState> VAO_FACTORY = ContextState.VAOState::new;
private static final Supplier<TextureSamplerState> TEX_SAMPLER_FACTORY = TextureSamplerState::new;
@@ -154,6 +158,21 @@ public final class ResourceManager {
preferredD24S8 = SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
LOG.info("D24_UNORM_S8_UINT not supported for depth+sampler, using D32_FLOAT_S8_UINT");
}
swapchainDepthStencilFormat = preferredD24S8;
if (!SDL_GPUTextureSupportsFormat(dev, swapchainDepthStencilFormat, texType, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET)) {
final int alt = swapchainDepthStencilFormat == SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT ? SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT : SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
if (SDL_GPUTextureSupportsFormat(dev, alt, texType, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET)) {
swapchainDepthStencilFormat = alt;
} else {
swapchainDepthStencilFormat = 0;
LOG.warn("No depth+stencil format usable as a render target; default framebuffer will have no depth or stencil");
}
}
}
public int getSwapchainDepthStencilFormat() {
return swapchainDepthStencilFormat;
}
public int genTexture() {
@@ -189,6 +208,20 @@ public final class ResourceManager {
return SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
}
public String describeGpuState() {
final FrameManager.FrameState f = frameManager.busiestFrame();
final StringBuilder sb = new StringBuilder(256);
sb.append("frame=").append(f.frameNumber)
.append(" submitsThisFrame=").append(f.submitsThisFrame)
.append(" mipGensThisFrame=").append(f.mipGensThisFrame)
.append(" pendingUpload=").append(f.pendingUploadBytes >> 10).append("KiB/").append(f.pendingUploadCommands).append("cmds")
.append('\n');
sb.append("arena capacity=").append(f.arena.capacity() >> 20).append("MiB used=").append(f.arena.usedBytes() >> 10)
.append("KiB copies=").append(f.arena.copyCount())
.append(" overflowFlushes=").append(f.arenaOverflowFlushesThisFrame);
return sb.toString();
}
private long createTextureCore(int glId, int glTarget, int sdlFormat, int glFormat, int width, int height, int depth, int levels, int usage) {
usage = reconcileUsageAndWarn(glId, usage);
final int sdlType = mapTextureType(glTarget);
@@ -221,7 +254,7 @@ public final class ResourceManager {
final long handle = SDL_CreateGPUTexture(device.getDevice(), ci);
if (props != 0) SDLProperties.SDL_DestroyProperties(props);
if (handle == 0) {
LOG.error("Failed to create texture {}x{} fmt=0x{} sdlFmt=0x{} usage=0x{}: {}", width, height, Integer.toHexString(glFormat), Integer.toHexString(sdlFormat), Integer.toHexString(usage), SDLError.SDL_GetError());
device.reportGpuFailure("createTexture " + width + "x" + height + " fmt=0x" + Integer.toHexString(glFormat) + " sdlFmt=0x" + Integer.toHexString(sdlFormat) + " usage=0x" + Integer.toHexString(usage));
return 0;
}
wLock.lock();
@@ -278,7 +311,7 @@ public final class ResourceManager {
} finally {
wLock.unlock();
}
releaseTextureHandle(handle);
releaseTextureHandleAfterPendingUploads(handle);
}
public void releaseTextureHandleForRealloc(int glId) {
@@ -290,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();
@@ -305,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;
@@ -324,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 {
@@ -503,7 +587,7 @@ public final class ResourceManager {
final GpuBucket b = gpuBufferPool.get(Hashing.packHiLo(usage, bucket));
if (b == null || b.handles.isEmpty()) return 0;
for (int i = 0, n = b.handles.size(); i < n; i++) {
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.FRAMES_IN_FLIGHT) {
if (gpuPoolCurrentFrame - b.frames.getLong(i) >= Device.framesInFlight()) {
final long handle = b.handles.getLong(i);
final int last = n - 1;
if (i != last) {
@@ -606,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;
@@ -1291,7 +1376,7 @@ public final class ResourceManager {
for (int i = 0; i < n; i += 2) {
final long handle = batchSegmentPending.getLong(i);
final long stamp = batchSegmentPending.getLong(i + 1);
if (currentFrame - stamp >= Device.FRAMES_IN_FLIGHT) {
if (currentFrame - stamp >= Device.framesInFlight()) {
batchSegmentFree.enqueue(handle);
} else {
batchSegmentPending.set(w, handle);
@@ -1311,7 +1396,7 @@ public final class ResourceManager {
final long handle = SDL_CreateGPUTransferBuffer(device.getDevice(), ci);
if (handle == 0) {
LOG.error("Failed to create transfer buffer size={}: {}", size, SDLError.SDL_GetError());
device.reportGpuFailure("createTransferBuffer size=" + size);
} else {
synchronized (xferPoolLock) { liveTransferBufferHandles.add(handle); }
}
@@ -1413,7 +1498,7 @@ public final class ResourceManager {
f.arenaCommandBuffer = SDL_AcquireGPUCommandBuffer(device.getDevice());
if (f.arenaCommandBuffer == 0) {
LOG.error("Failed to acquire arena upload command buffer: {}", SDLError.SDL_GetError());
f.arenaMapped = null;
unmapArena(f);
return false;
}
f.arenaCopyPass = SDL_BeginGPUCopyPass(f.arenaCommandBuffer);
@@ -1421,12 +1506,18 @@ public final class ResourceManager {
LOG.error("Failed to begin arena copy pass: {}", SDLError.SDL_GetError());
SDL_SubmitGPUCommandBuffer(f.arenaCommandBuffer);
f.arenaCommandBuffer = 0;
f.arenaMapped = null;
unmapArena(f);
return false;
}
return true;
}
private void unmapArena(FrameManager.FrameState f) {
if (f.arenaMapped == null) return;
SDL_UnmapGPUTransferBuffer(device.getDevice(), f.arenaXfer);
f.arenaMapped = null;
}
public void flushUploadArena() {
flushUploadArena(frameManager.frame());
}
@@ -1442,7 +1533,7 @@ public final class ResourceManager {
}
if (f.arenaCommandBuffer != 0) {
if (!SDL_SubmitGPUCommandBuffer(f.arenaCommandBuffer)) {
LOG.error("Failed to submit arena upload command buffer: {}", SDLError.SDL_GetError());
device.reportGpuFailure("submit arena upload command buffer");
}
f.arenaCommandBuffer = 0;
if (Tracy.ENABLED) f.arenaSubmitsThisFrame++;
@@ -1540,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);
@@ -1566,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();
}
@@ -1596,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);
@@ -1629,6 +1733,7 @@ public final class ResourceManager {
SDL_UploadToGPUTexture(copyPass, src, dst, false);
}
markTextureContentDefined(gpuTexture);
frameManager.recordUploadCommands(size, 1);
returnTransferBuffer(xfer, size);
@@ -1650,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;
@@ -1979,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();
@@ -2094,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;
@@ -2213,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;
@@ -207,15 +207,13 @@ public final class TextureOps {
SDL_GenerateMipmapsForGPUTexture(uploadCb, handle);
}
if (!SDL_SubmitGPUCommandBuffer(uploadCb)) {
device.reportGpuFailure("submit mipmap generation CB");
}
if (f.commandBuffer == uploadCb) {
if (!SDL_SubmitGPUCommandBuffer(uploadCb)) {
LOG.error("drainPendingMipGen: failed to submit frame CB: {}", SDLError.SDL_GetError());
}
f.commandBuffer = 0;
} else {
if (!SDL_SubmitGPUCommandBuffer(uploadCb)) {
LOG.error("drainPendingMipGen: failed to submit pending-upload CB: {}", SDLError.SDL_GetError());
}
f.pendingUploadCommandBuffer = 0;
f.pendingUploadBytes = 0;
f.pendingUploadCommands = 0;
@@ -17,6 +17,7 @@ import java.util.concurrent.locks.LockSupport;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.profiling.TracyBackend;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.device.GpuDeviceLostException;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.lwjgl.system.MemoryUtil;
@@ -33,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();
@@ -213,34 +215,59 @@ 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");
while (!shutdown) {
final long workStart = Tracy.ENABLED ? System.nanoTime() : 0L;
// Drain all pending uploads
DeferredUpload upload = queue.poll();
if (upload != null) {
do {
processOne(upload);
upload = queue.poll();
} while (upload != null);
}
if (shouldFlush(openCB != 0, flushRequestedSeq, submittedSeq, openBytes, openCount)) {
flushOpenCB();
}
if (Tracy.ENABLED) pendingActiveNanos += System.nanoTime() - workStart;
if (queue.isEmpty() && !shutdown) {
parked = true;
if (queue.isEmpty() && !shutdown && flushRequestedSeq <= submittedSeq) {
LockSupport.parkNanos(1_000_000L); // 1ms
try {
while (!shutdown) {
final long workStart = Tracy.ENABLED ? System.nanoTime() : 0L;
// Drain all pending uploads
DeferredUpload upload = queue.poll();
if (upload != null) {
do {
processOne(upload);
upload = queue.poll();
} while (upload != null);
}
parked = false;
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;
if (queue.isEmpty() && !shutdown) {
parked = true;
if (queue.isEmpty() && !shutdown && flushRequestedSeq <= submittedSeq) {
LockSupport.parkNanos(1_000_000L); // 1ms
}
parked = false;
}
}
if (openCB != 0) flushOpenCB();
} catch (GpuDeviceLostException e) {
LOG.error("Transfer thread stopping: {}", e.getMessage());
} finally {
synchronized (submittedLock) {
submittedSeq = Long.MAX_VALUE;
submittedLock.notifyAll();
}
}
if (openCB != 0) flushOpenCB();
drainAllPendingFreesOnShutdown();
srcLoc.free();
dstRegion.free();
@@ -305,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);
@@ -323,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);
@@ -334,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);
@@ -355,15 +406,10 @@ public final class TransferThread {
SDL_EndGPUCopyPass(openCopyPass);
if (!SDL_SubmitGPUCommandBuffer(openCB)) {
LOG.error("Failed to submit transfer command buffer: {}", SDLError.SDL_GetError());
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));
@@ -3,6 +3,8 @@ package com.gtnewhorizons.angelica.sdlgpu.resource;
public final class UploadArena {
public static final int INITIAL_CAPACITY = 16 * 1024 * 1024;
public static final int MAX_CAPACITY = 64 * 1024 * 1024;
private static final int ALIGNMENT = 16;
private int capacity = INITIAL_CAPACITY;
@@ -23,8 +25,11 @@ public final class UploadArena {
return size >= 0 && size <= capacity;
}
public void requestGrow(int minSize) {
while (growTo < minSize) growTo <<= 1;
public void requestGrow(int minSize) {
if (minSize <= growTo) return;
long next = growTo;
while (next < minSize && next < MAX_CAPACITY) next <<= 1;
growTo = (int) Math.min(next, MAX_CAPACITY);
}
public boolean applyGrow() {
@@ -0,0 +1,92 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
import static com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend.defaultFramebufferAttachmentParameteri;
import static com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend.fboAttachmentParameteri;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
class FramebufferAttachmentQueryTest {
private static final int COLOR = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
private static int defaultFb(int attachment, int pname, int depthFormat) {
return defaultFramebufferAttachmentParameteri(attachment, pname, COLOR, depthFormat);
}
@Test
void defaultFramebufferReportsEightStencilBits() {
assertEquals(8, defaultFb(GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(8, defaultFb(GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
}
@Test
void defaultFramebufferReportsDepthAndColorSizes() {
assertEquals(24, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(32, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
assertEquals(8, defaultFb(GL11.GL_BACK_LEFT, GL30.GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(8, defaultFb(GL11.GL_BACK_LEFT, GL30.GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE, SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
}
@Test
void defaultFramebufferWithoutDepthReportsNone() {
assertEquals(0, defaultFb(GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, 0));
assertEquals(0, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, 0));
assertEquals(GL11.GL_NONE, defaultFb(GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, 0));
assertEquals(GL30.GL_FRAMEBUFFER_DEFAULT, defaultFb(GL11.GL_BACK_LEFT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, 0));
}
@Test
void packedDepthStencilAttachmentReportsBothAspects() {
final FboState fbo = new FboState();
fbo.depthGlId = 42;
fbo.depthTexture = 0xDDDDL;
fbo.depthFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
for (int attachment : new int[]{ GL30.GL_DEPTH_ATTACHMENT, GL30.GL_STENCIL_ATTACHMENT, GL30.GL_DEPTH_STENCIL_ATTACHMENT, GL11.GL_STENCIL }) {
assertEquals(8, fboAttachmentParameteri(fbo, attachment, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE), "attachment " + attachment);
}
assertEquals(24, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE));
assertEquals(42, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME));
assertEquals(GL11.GL_TEXTURE, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE));
}
@Test
void depthOnlyAttachmentReportsNoStencil() {
final FboState fbo = new FboState();
fbo.depthGlId = 7;
fbo.depthTexture = 0xDDDDL;
fbo.depthFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM;
assertEquals(0, fboAttachmentParameteri(fbo, GL11.GL_STENCIL, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE));
assertEquals(24, fboAttachmentParameteri(fbo, GL11.GL_DEPTH, GL30.GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE));
}
@Test
void missingAttachmentReportsNone() {
final FboState fbo = new FboState();
assertEquals(GL11.GL_NONE, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE));
assertEquals(0, fboAttachmentParameteri(fbo, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE));
assertEquals(0, fboAttachmentParameteri(null, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE));
}
@Test
void colorAttachmentReportsChannelSizes() {
final FboState fbo = new FboState();
fbo.colorGlIds[1] = 11;
fbo.colorTextures[1] = 0xCCCCL;
fbo.colorFormats[1] = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
assertEquals(8, fboAttachmentParameteri(fbo, GL30.GL_COLOR_ATTACHMENT0 + 1, GL30.GL_FRAMEBUFFER_ATTACHMENT_GREEN_SIZE));
assertEquals(11, fboAttachmentParameteri(fbo, GL30.GL_COLOR_ATTACHMENT0 + 1, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME));
assertEquals(GL11.GL_NONE, fboAttachmentParameteri(fbo, GL30.GL_COLOR_ATTACHMENT0, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE));
}
}
@@ -0,0 +1,50 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDLInit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.lwjgl.sdl.SDLInit.SDL_INIT_VIDEO;
class DeviceLifecycleGpuTest {
private Device device;
@BeforeAll
static void initSdl() {
assumeTrue(SDLInit.SDL_Init(SDL_INIT_VIDEO), () -> "SDL_Init failed: " + SDLError.SDL_GetError());
}
@AfterEach
void tearDown() {
if (device != null) device.destroyDevice();
}
@Test
void createsWithoutAWindowAndIsMemoized() {
device = new Device();
assumeTrue(device.createDevice(), "no SDL GPU device on this machine");
final long handle = device.getDevice();
assertNotEquals(0L, handle);
assertTrue(device.createDevice(), "second createDevice should be a no-op");
assertEquals(handle, device.getDevice(), "createDevice must not replace a live device");
}
@Test
void destroyIsIdempotentOnAnUnclaimedDevice() {
device = new Device();
assumeTrue(device.createDevice(), "no SDL GPU device on this machine");
device.destroyDevice();
assertEquals(0L, device.getDevice());
device.destroyDevice();
assertEquals(0L, device.getDevice());
}
}
@@ -2,9 +2,13 @@ package com.gtnewhorizons.angelica.sdlgpu.device;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.device.Device.metalNeedsNewerSdl;
import static com.gtnewhorizons.angelica.sdlgpu.device.Device.resolveDriverName;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLVersion.SDL_VERSIONNUM;
class DriverSelectionTest {
@@ -24,4 +28,17 @@ class DriverSelectionTest {
assertNull(resolveDriverName("", false));
assertNull(resolveDriverName("direct3d12", false));
}
@Test
void metalBelow346FallsBack() {
assertTrue(metalNeedsNewerSdl("metal", SDL_VERSIONNUM(3, 4, 5)));
assertFalse(metalNeedsNewerSdl("metal", SDL_VERSIONNUM(3, 4, 6)));
assertFalse(metalNeedsNewerSdl("metal", SDL_VERSIONNUM(3, 5, 0)));
}
@Test
void otherDriversIgnoreTheMetalFloor() {
assertFalse(metalNeedsNewerSdl("vulkan", SDL_VERSIONNUM(3, 4, 5)));
assertFalse(metalNeedsNewerSdl("direct3d12", SDL_VERSIONNUM(3, 0, 0)));
}
}
@@ -0,0 +1,34 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.device.VideoDriverRecovery.shouldRetryOnX11;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class VideoDriverRecoveryTest {
@Test
void waylandWithoutVulkanSurfaceRetries() {
assertTrue(shouldRetryOnX11("wayland", false, ":0"));
}
@Test
void waylandWithVulkanStays() {
assertFalse(shouldRetryOnX11("wayland", true, ":0"));
}
@Test
void noXWaylandMeansNoRetry() {
assertFalse(shouldRetryOnX11("wayland", false, null));
assertFalse(shouldRetryOnX11("wayland", false, ""));
}
@Test
void otherVideoDriversAreLeftAlone() {
assertFalse(shouldRetryOnX11("x11", false, ":0"));
assertFalse(shouldRetryOnX11("cocoa", false, ":0"));
assertFalse(shouldRetryOnX11("windows", false, ":0"));
assertFalse(shouldRetryOnX11(null, false, ":0"));
}
}
@@ -0,0 +1,63 @@
package com.gtnewhorizons.angelica.sdlgpu.frame;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.swapchainClearNeedsPassBreak;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SwapchainClearPassBreakTest {
private static final long SWAPCHAIN = 0x5A1CL;
private static FrameManager.FrameState activeSwapchainPass() {
final FrameManager.FrameState f = new FrameManager.FrameState();
f.swapchainTexture = SWAPCHAIN;
f.renderPass = 0xBEEFL;
f.currentColorTarget = SWAPCHAIN;
return f;
}
@Test
void aPendingColorClearBreaksTheActivePass() {
final ContextState st = new ContextState();
st.pendingSwapchainClear = true;
assertTrue(swapchainClearNeedsPassBreak(st, activeSwapchainPass()), "otherwise the clear is deferred to the next pass and wipes what this one drew");
}
@Test
void aPendingDepthOrStencilClearBreaksTheActivePass() {
final ContextState depth = new ContextState();
depth.pendingSwapchainDepthClear = true;
assertTrue(swapchainClearNeedsPassBreak(depth, activeSwapchainPass()));
final ContextState stencil = new ContextState();
stencil.pendingSwapchainStencilClear = true;
assertTrue(swapchainClearNeedsPassBreak(stencil, activeSwapchainPass()));
}
@Test
void nothingPendingLeavesThePassAlone() {
assertFalse(swapchainClearNeedsPassBreak(new ContextState(), activeSwapchainPass()));
}
@Test
void aClearWithNoActivePassNeedsNoBreak() {
final ContextState st = new ContextState();
st.pendingSwapchainClear = true;
final FrameManager.FrameState f = activeSwapchainPass();
f.renderPass = 0;
assertFalse(swapchainClearNeedsPassBreak(st, f));
}
@Test
void aPassBoundToAnFboIsNotTheSwapchainPass() {
final ContextState st = new ContextState();
st.pendingSwapchainClear = true;
final FrameManager.FrameState f = activeSwapchainPass();
f.currentColorTarget = 0xF80L;
assertFalse(swapchainClearNeedsPassBreak(st, f));
}
}
@@ -0,0 +1,112 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.consumeClears;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
class ClearConsumptionTest {
private static final long COLOR = 0xC010L;
private static final long DEPTH = 0xD00DL;
private static final long DUMMY = 0xD04DL;
private static FboState fbo(int depthFormat) {
final FboState fbo = new FboState();
fbo.colorTextures[0] = COLOR;
fbo.colorGlIds[0] = 3;
if (depthFormat != 0) {
fbo.depthTexture = DEPTH;
fbo.depthGlId = 5;
fbo.depthFormat = depthFormat;
}
fbo.recomputeTargets();
return fbo;
}
private static ResourceManager rm() {
return SdlTestRig.resourceManager();
}
@Test
void aClearedColorTargetBecomesContentDefined() {
final ResourceManager rm = rm();
final ContextState st = new ContextState();
final FboState fbo = fbo(0);
FBOClearTracker.recordPendingColorClear(st, COLOR, 0f, 0f, 0f, 1f);
consumeClears(rm, st, fbo, DUMMY, 0b1, false, false, false);
assertFalse(st.pendingColorTextures.contains(COLOR), "the pass applied the clear, so it is no longer pending");
assertTrue(rm.isTextureContentDefined(COLOR), "a later frame must not clear it again");
}
@Test
void anUnclearedColorTargetIsNotMarkedDefined() {
final ResourceManager rm = rm();
final ContextState st = new ContextState();
consumeClears(rm, st, fbo(0), DUMMY, 0, false, false, false);
assertFalse(rm.isTextureContentDefined(COLOR));
}
@Test
void theDummyTargetIsNeverTracked() {
final ResourceManager rm = rm();
final ContextState st = new ContextState();
final FboState fbo = fbo(0);
fbo.colorTextures[0] = DUMMY;
fbo.recomputeTargets();
consumeClears(rm, st, fbo, DUMMY, 0b1, false, false, false);
assertFalse(rm.isTextureContentDefined(DUMMY), "the shared stand-in target has no per-FBO content");
}
@Test
void depthAndStencilAreConsumedIndependently() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
consumeClears(rm(), st, fbo(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT), DUMMY, 0, true, true, true);
assertFalse(st.pendingDepthTextures.contains(DEPTH));
assertFalse(st.pendingStencilTextures.contains(DEPTH));
assertTrue(st.clearedTexturesThisFrame.contains(DEPTH));
assertTrue(st.clearedStencilTexturesThisFrame.contains(DEPTH));
}
@Test
void aDepthOnlyTargetLeavesStencilStateAlone() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
consumeClears(rm(), st, fbo(SDL_GPU_TEXTUREFORMAT_D24_UNORM), DUMMY, 0, true, false, false);
assertFalse(st.pendingDepthTextures.contains(DEPTH));
assertTrue(st.pendingStencilTextures.contains(DEPTH), "the format has no stencil aspect to clear");
assertFalse(st.clearedStencilTexturesThisFrame.contains(DEPTH));
}
@Test
void anUnclearedDepthTargetIsStillConsumed() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
consumeClears(rm(), st, fbo(SDL_GPU_TEXTUREFORMAT_D24_UNORM), DUMMY, 0, false, false, false);
assertFalse(st.pendingDepthTextures.contains(DEPTH));
assertFalse(st.clearedTexturesThisFrame.contains(DEPTH), "no clear was baked, so nothing was cleared");
}
}
@@ -0,0 +1,44 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.foldClearColor;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.foldClearDepth;
import static com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier.foldClearStencil;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class ClearValueLayoutHashTest {
private static final long SEED = 0xA5A5A5A5A5A5A5A5L;
@Test
void depthZeroAndDepthOneDoNotCollide() {
assertNotEquals(foldClearDepth(SEED, 0.0f), foldClearDepth(SEED, 1.0f), "the minimap mask clear must not reuse the world's depth-1 clear target");
}
@Test
void depthFoldIsStableForTheSameValue() {
assertEquals(foldClearDepth(SEED, 0.0f), foldClearDepth(SEED, 0.0f));
}
@Test
void negativeZeroIsDistinguishedFromZero() {
assertNotEquals(foldClearDepth(SEED, 0.0f), foldClearDepth(SEED, -0.0f), "raw bits, not numeric equality, so the cached value always matches what was baked");
}
@Test
void everyColorChannelParticipates() {
final long base = foldClearColor(SEED, 0f, 0f, 0f, 1f);
assertNotEquals(base, foldClearColor(SEED, 1f, 0f, 0f, 1f), "red");
assertNotEquals(base, foldClearColor(SEED, 0f, 1f, 0f, 1f), "green");
assertNotEquals(base, foldClearColor(SEED, 0f, 0f, 1f, 1f), "blue");
assertNotEquals(base, foldClearColor(SEED, 0f, 0f, 0f, 0f), "alpha");
}
@Test
void stencilValuesDoNotCollide() {
assertNotEquals(foldClearStencil(SEED, 0), foldClearStencil(SEED, 1));
assertEquals(foldClearStencil(SEED, 0xFF), foldClearStencil(SEED, 0xFF));
}
}
@@ -0,0 +1,232 @@
package com.gtnewhorizons.angelica.sdlgpu.pipeline;
import com.gtnewhorizons.angelica.glsm.ffp.VAOManager;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDL_GPUColorTargetInfo;
import org.lwjgl.sdl.SDL_GPUDepthStencilTargetInfo;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.*;
class StencilMaskGpuTest {
private static SdlTestRig rig;
private static ShaderManager sm;
private static final int SIZE = 64;
private static final int MASK = 32;
private static PipelineStore store;
private static long colorTexture;
private static int colorFormat;
private static long depthTexture;
private static int depthFormat;
private static long maskPipeline;
private static long fillPipeline;
private static final String VERTEX = String.join("\n",
"#version 460 core",
"void main() {",
" vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);",
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);",
"}",
"");
private static final String MASK_FRAGMENT = String.join("\n",
"#version 460 core",
"layout(location = 0) out vec4 fragColor;",
"void main() {",
" if (gl_FragCoord.x >= " + MASK + ".0 || gl_FragCoord.y >= " + MASK + ".0) discard;",
" fragColor = vec4(0.0, 0.0, 1.0, 1.0);",
"}",
"");
private static final String FILL_FRAGMENT = String.join("\n",
"#version 460 core",
"layout(location = 0) out vec4 fragColor;",
"void main() {",
" fragColor = vec4(1.0, 0.0, 0.0, 1.0);",
"}",
"");
@BeforeAll
static void setUp() throws Exception {
rig = SdlTestRig.acquireRealDevice();
rig.resourceManager.cachePreferredDepthFormats();
sm = new ShaderManager(rig.device);
VAOManager.init(0);
store = new PipelineStore(rig.device);
colorTexture = rig.resourceManager.createTexture(9500, GL11.GL_TEXTURE_2D, GL11.GL_RGBA8, SIZE, SIZE, 1, 1);
assertTrue(colorTexture != 0, "color target creation failed");
colorFormat = rig.resourceManager.getTextureMeta(9500).sdlFormat();
PipelineCache.setSwapchainFormats(new int[]{ colorFormat });
rig.resourceManager.createTexture(9501, GL11.GL_TEXTURE_2D, GL30.GL_DEPTH24_STENCIL8, SIZE, SIZE, 1, 1);
depthTexture = rig.resourceManager.ensureTextureUsage(9501, SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET);
assertTrue(depthTexture != 0, "depth+stencil target creation failed");
depthFormat = rig.resourceManager.getTextureMeta(9501).sdlFormat();
assertEquals(8, PixelOps.stencilBits(depthFormat), "test needs a packed depth+stencil format");
maskPipeline = createPipeline(MASK_FRAGMENT, true);
fillPipeline = createPipeline(FILL_FRAGMENT, false);
}
@AfterAll
static void tearDown() {
SdlTestRig.releaseRealDevice();
}
private static long createPipeline(String fragmentSource, boolean writesMask) {
final int vs = sm.createShader(GL20.GL_VERTEX_SHADER);
sm.shaderSource(vs, VERTEX);
sm.compileShader(vs);
final int fs = sm.createShader(GL20.GL_FRAGMENT_SHADER);
sm.shaderSource(fs, fragmentSource);
sm.compileShader(fs);
final int prog = sm.createProgram();
sm.attachShader(prog, vs);
sm.attachShader(prog, fs);
sm.linkProgram(prog);
final ShaderManager.ProgramObject po = sm.getProgram(prog);
assertTrue(po != null && po.linked, "link failed: " + (po == null ? "no program" : po.infoLog));
po.sdlVertexShader = sm.createSDLShader(po.vertexSpirv, SDL_GPU_SHADERSTAGE_VERTEX, po.vertexResources.numSamplers(), po.vertexResources.numUBOs(), po.vertexResources.numStorageBuffers(), po.vertexResources.numStorageTextures());
po.sdlFragmentShader = sm.createSDLShader(po.fragmentSpirv, SDL_GPU_SHADERSTAGE_FRAGMENT, po.fragmentResources.numSamplers(), po.fragmentResources.numUBOs(), po.fragmentResources.numStorageBuffers(), po.fragmentResources.numStorageTextures());
assertTrue(po.sdlVertexShader != 0 && po.sdlFragmentShader != 0, "SDL shader creation failed");
final ContextState cs = new ContextState();
final PipelineCache cache = cs.pipeline;
cache.vertexShader = po.sdlVertexShader;
cache.fragmentShader = po.sdlFragmentShader;
cache.programId = prog;
cache.maxFragOutputLocation = po.maxFragOutputLocation;
cache.shaderInputMask = po.vertexInputMask;
cache.shaderInputVecSize = po.vertexInputVecSize;
cache.shaderInputBaseType = po.vertexInputBaseType;
cache.shaderInputName = po.vertexInputName;
cache.setColorTargetFormats(new int[]{ colorFormat });
cache.setDrawBuffers(new int[]{ 0 });
cache.hasDepthTarget = true;
cache.depthTargetFormat = depthFormat;
cache.depthTestEnabled = false;
cache.depthWriteEnabled = false;
cache.stencilTestEnabled = true;
cache.stencilCompareMask = 0xFF;
cache.stencilWriteMask = writesMask ? 0xFF : 0x00;
final int compareOp = writesMask ? SDL_GPU_COMPAREOP_ALWAYS : SDL_GPU_COMPAREOP_EQUAL;
final int passOp = writesMask ? SDL_GPU_STENCILOP_REPLACE : SDL_GPU_STENCILOP_KEEP;
cache.stencilFrontCompareOp = compareOp;
cache.stencilBackCompareOp = compareOp;
cache.stencilFrontPassOp = passOp;
cache.stencilBackPassOp = passOp;
cache.markShaderDirty();
cache.markOutputDirty();
assertTrue(cache.effectiveStencilTestEnabled(), "stencil must survive the depth-format gate");
final long pipeline = cache.getOrCreatePipeline(store, cs);
assertTrue(pipeline != 0, "pipeline creation failed: " + SDLError.SDL_GetError());
return pipeline;
}
private static void render(int secondPassStencilLoad) {
try (MemoryStack stack = MemoryStack.stackPush()) {
final long cb = SDL_AcquireGPUCommandBuffer(rig.device.getDevice());
assertTrue(cb != 0, "no command buffer");
final SDL_GPUColorTargetInfo.Buffer color = SDL_GPUColorTargetInfo.calloc(1, stack);
color.get(0).texture(colorTexture).load_op(SDL_GPU_LOADOP_CLEAR).store_op(SDL_GPU_STOREOP_STORE);
final SDL_GPUDepthStencilTargetInfo ds = SDL_GPUDepthStencilTargetInfo.calloc(stack)
.texture(depthTexture)
.load_op(SDL_GPU_LOADOP_CLEAR).store_op(SDL_GPU_STOREOP_STORE).clear_depth(1.0f)
.stencil_load_op(SDL_GPU_LOADOP_CLEAR).stencil_store_op(SDL_GPU_STOREOP_STORE).clear_stencil((byte) 0);
long pass = SDL_BeginGPURenderPass(cb, color, ds);
assertTrue(pass != 0, "no render pass");
SDL_SetGPUStencilReference(pass, (byte) 1);
SDL_BindGPUGraphicsPipeline(pass, maskPipeline);
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
if (secondPassStencilLoad < 0) {
SDL_BindGPUGraphicsPipeline(pass, fillPipeline);
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
}
SDL_EndGPURenderPass(pass);
if (secondPassStencilLoad >= 0) {
color.get(0).load_op(SDL_GPU_LOADOP_LOAD);
ds.load_op(SDL_GPU_LOADOP_LOAD).stencil_load_op(secondPassStencilLoad).clear_stencil((byte) 0);
pass = SDL_BeginGPURenderPass(cb, color, ds);
assertTrue(pass != 0, "no second render pass");
SDL_SetGPUStencilReference(pass, (byte) 1);
SDL_BindGPUGraphicsPipeline(pass, fillPipeline);
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
assertTrue(SDL_SubmitGPUCommandBuffer(cb), "draw submit failed");
SDL_WaitForGPUIdle(rig.device.getDevice());
}
}
private static int[] readRedPixels() {
final ByteBuffer pixels = MemoryUtil.memAlloc(SIZE * SIZE * 4);
try {
final long cb = SDL_AcquireGPUCommandBuffer(rig.device.getDevice());
assertTrue(cb != 0, "no readback command buffer");
rig.resourceManager.downloadFromTexture(cb, colorTexture, 0, 0, SIZE, SIZE, 0, pixels);
int count = 0, maxX = -1, minX = SIZE;
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
final int i = (y * SIZE + x) * 4;
final boolean red = (pixels.get(i) & 0xFF) > 200 && (pixels.get(i + 1) & 0xFF) < 64;
if (red) {
count++;
if (x > maxX) maxX = x;
if (x < minX) minX = x;
}
}
}
return new int[]{ count, minX, maxX };
} finally {
MemoryUtil.memFree(pixels);
}
}
@Test
void fillIsClippedToTheStencilMask() {
render(-1);
final int[] r = readRedPixels();
assertEquals(MASK * MASK, r[0], "fill must cover exactly the masked region, not the whole target");
assertEquals(0, r[1], "masked region starts at x=0");
assertEquals(MASK - 1, r[2], "masked region must not bleed past the mask");
}
@Test
void maskSurvivesAPassBoundaryWhenStencilIsLoaded() {
render(SDL_GPU_LOADOP_LOAD);
final int[] r = readRedPixels();
assertEquals(MASK * MASK, r[0], "a stored stencil buffer must still clip the next pass");
assertEquals(MASK - 1, r[2]);
}
@Test
void clearingStencilWipesTheMask() {
render(SDL_GPU_LOADOP_CLEAR);
assertEquals(0, readRedPixels()[0], "a cleared stencil buffer must clip everything");
}
}
@@ -0,0 +1,42 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ColorContentLifetimeTest {
private static final long TEX = 0xC010125L;
@Test
void freshTextureHasUndefinedContents() {
final ResourceManager rm = SdlTestRig.resourceManager();
assertFalse(rm.isTextureContentDefined(TEX), "never written, so the first pass must clear");
}
@Test
void definedContentsSurviveAcrossFrames() {
final ResourceManager rm = SdlTestRig.resourceManager();
rm.markTextureContentDefined(TEX);
assertTrue(rm.isTextureContentDefined(TEX));
assertTrue(rm.isTextureContentDefined(TEX), "contents outlive the frame that produced them");
}
@Test
void releaseResetsContentsBecauseSdlCanRecycleThePointer() {
final ResourceManager rm = SdlTestRig.resourceManager();
rm.markTextureContentDefined(TEX);
rm.markTextureContentUndefined(TEX);
assertFalse(rm.isTextureContentDefined(TEX), "a recycled handle must not inherit the previous texture's defined flag");
}
@Test
void theZeroHandleIsNeverDefined() {
final ResourceManager rm = SdlTestRig.resourceManager();
rm.markTextureContentDefined(0L);
assertFalse(rm.isTextureContentDefined(0L));
}
}
@@ -0,0 +1,64 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.*;
class DepthStencilBitsTest {
@Test
void depthBitsPerFormat() {
assertEquals(16, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D16_UNORM));
assertEquals(24, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM));
assertEquals(24, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(32, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT));
assertEquals(32, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
assertEquals(0, PixelOps.depthBits(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM));
}
@Test
void onlyPackedFormatsCarryStencil() {
assertEquals(8, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertEquals(8, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D32_FLOAT));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_D16_UNORM));
assertEquals(0, PixelOps.stencilBits(SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM));
}
@Test
void stencilBitsAgreesWithIsDepthStencilFormat() {
final int[] formats = {
SDL_GPU_TEXTUREFORMAT_D16_UNORM, SDL_GPU_TEXTUREFORMAT_D24_UNORM, SDL_GPU_TEXTUREFORMAT_D32_FLOAT,
SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT, SDL_GPU_TEXTUREFORMAT_D32_FLOAT_S8_UINT,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
};
for (int f : formats) {
assertEquals(PixelOps.isDepthStencilFormat(f), PixelOps.stencilBits(f) != 0, "format " + f);
}
}
@Test
void colorChannelBitsForCommonFormats() {
for (int fmt : new int[]{ SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM }) {
for (int c = 0; c < 4; c++) assertEquals(8, PixelOps.colorChannelBits(fmt, c), "channel " + c);
}
assertEquals(16, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT, 3));
assertEquals(8, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8_UNORM, 0));
assertEquals(0, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8_UNORM, 1));
assertEquals(8, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, 1));
assertEquals(0, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8_UNORM, 2));
assertEquals(10, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM, 0));
assertEquals(2, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM, 3));
assertEquals(0, PixelOps.colorChannelBits(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT, 0));
}
@Test
void depthFormatsAreNotColorFormats() {
assertTrue(PixelOps.isDepthFormat(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT));
assertFalse(PixelOps.isDepthFormat(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM));
}
}
@@ -35,6 +35,18 @@ class FullCoverageClearDiscardTest {
assertTrue(st.clearedTexturesThisFrame.contains(HANDLE), "the overwrite counts as the clear");
}
@Test
void fullTargetColorBlitDefinesTheContents() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingColorClear(st, HANDLE, 1f, 0f, 0f, 1f);
final SdlTestRig rig = SdlTestRig.create();
final FBOClearTracker tracker = new FBOClearTracker(rig.frameManager, rig.resourceManager, null);
assertTrue(tracker.discardPendingClearIfFullyCovered(st, HANDLE, 0, 0, 0, W, H, meta()));
assertTrue(rig.resourceManager.isTextureContentDefined(HANDLE), "the overwrite defines the contents, so a later frame must not clear it back");
}
@Test
void fullTargetDepthBlitDropsThePendingClear() {
final ContextState st = new ContextState();
@@ -0,0 +1,68 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps.colorChannelBits;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_A8_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_BC1_RGBA_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R11G11B10_UFLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8_UNORM;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
class PixelFormatBitsTest {
private static int[] rgba(int sdlFormat) {
return new int[]{
colorChannelBits(sdlFormat, 0),
colorChannelBits(sdlFormat, 1),
colorChannelBits(sdlFormat, 2),
colorChannelBits(sdlFormat, 3),
};
}
private static void assertRgba(int sdlFormat, int r, int g, int b, int a) {
assertEquals(r, colorChannelBits(sdlFormat, 0), "red");
assertEquals(g, colorChannelBits(sdlFormat, 1), "green");
assertEquals(b, colorChannelBits(sdlFormat, 2), "blue");
assertEquals(a, colorChannelBits(sdlFormat, 3), "alpha");
}
@Test
void uniformWidthFormats() {
assertRgba(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, 8, 8, 8, 8);
assertRgba(SDL_GPU_TEXTUREFORMAT_R8_UNORM, 8, 0, 0, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_R16G16_FLOAT, 16, 16, 0, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_R32G32B32A32_FLOAT, 32, 32, 32, 32);
}
@Test
void packedFormatsReportTheirRealChannelWidths() {
assertRgba(SDL_GPU_TEXTUREFORMAT_A8_UNORM, 0, 0, 0, 8);
assertRgba(SDL_GPU_TEXTUREFORMAT_R10G10B10A2_UNORM, 10, 10, 10, 2);
assertRgba(SDL_GPU_TEXTUREFORMAT_R11G11B10_UFLOAT, 11, 11, 10, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_B5G6R5_UNORM, 5, 6, 5, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_B5G5R5A1_UNORM, 5, 5, 5, 1);
assertRgba(SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM, 4, 4, 4, 4);
}
@Test
void depthAndCompressedFormatsHaveNoColorChannels() {
assertRgba(SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT, 0, 0, 0, 0);
assertRgba(SDL_GPU_TEXTUREFORMAT_BC1_RGBA_UNORM, 0, 0, 0, 0);
}
@Test
void outOfRangeChannelsAreZero() {
assertEquals(0, colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, -1));
assertEquals(0, colorChannelBits(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, 4));
assertEquals(4, rgba(SDL_GPU_TEXTUREFORMAT_B4G4R4A4_UNORM).length);
}
}
@@ -0,0 +1,122 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.lwjgl.sdl.SDLGPU.SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
class StencilClearTrackingTest {
private static final long DEPTH = 0xD00DL;
private static FBOClearTracker tracker() {
final SdlTestRig rig = SdlTestRig.create();
return new FBOClearTracker(rig.frameManager, rig.resourceManager, null);
}
private static FboState packedDepthStencilFbo() {
final FboState fbo = new FboState();
fbo.depthTexture = DEPTH;
fbo.depthGlId = 5;
fbo.depthFormat = SDL_GPU_TEXTUREFORMAT_D24_UNORM_S8_UINT;
fbo.recomputeTargets();
return fbo;
}
@Test
void pendingStencilClearIsTrackedSeparatelyFromDepth() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertTrue(st.pendingStencilTextures.contains(DEPTH));
assertFalse(st.pendingDepthTextures.contains(DEPTH), "a stencil clear must not imply a depth clear");
}
@Test
void stencilClearSurvivesADepthClearOnTheSameHandle() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 3);
FBOClearTracker.recordPendingDepthClear(st, DEPTH, 1f);
assertTrue(st.pendingStencilTextures.contains(DEPTH), "depth and stencil share a texture but not a clear");
assertEquals(3, st.pendingStencilValues.get(DEPTH));
}
@Test
void aPendingStencilClearAloneBlocksTargetReuse() {
final ContextState st = new ContextState();
final FboState fbo = packedDepthStencilFbo();
assertFalse(FBOClearTracker.fboHasPendingClear(st, fbo));
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertTrue(FBOClearTracker.fboHasPendingClear(st, fbo), "the pass must be rebuilt to apply the stencil clear");
}
@Test
void recordingAStencilClearRetractsTheClearedThisFrameMark() {
final ContextState st = new ContextState();
st.clearedStencilTexturesThisFrame.add(DEPTH);
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertFalse(st.clearedStencilTexturesThisFrame.contains(DEPTH), "a second clear in one frame must not be swallowed");
}
@Test
void recordingBumpsMutationGen() {
final ContextState st = new ContextState();
FBOClearTracker.snapshotFlushGenerations(st);
final int before = st.pendingMutationGen;
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
assertEquals(before + 1, st.pendingMutationGen);
}
@Test
void scrubDropsStencilStateWithTheTexture() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 7);
st.clearedStencilTexturesThisFrame.add(DEPTH);
final SdlTestRig rig = SdlTestRig.create();
final int glId = rig.resourceManager.genTexture();
SdlReflect.putTextureHandle(rig.resourceManager, glId, DEPTH);
new FBOClearTracker(rig.frameManager, rig.resourceManager, null).scrubPendingClearsForTexture(st, glId);
assertFalse(st.pendingStencilTextures.contains(DEPTH));
assertFalse(st.pendingStencilValues.containsKey(DEPTH));
assertFalse(st.clearedStencilTexturesThisFrame.contains(DEPTH));
}
@Test
void stencilClearValueIsMaskedToEightBits() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0xFF);
assertEquals(0xFF, st.pendingStencilValues.get(DEPTH));
assertEquals((byte) 0xFF, (byte) st.pendingStencilValues.get(DEPTH));
}
@Test
void aStencilOnlyClearStillReachesTheSamplerFlush() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
final int gen = st.pendingMutationGen;
tracker().flushPendingClearsForBoundSamplers(st);
assertEquals(gen, st.lastFlushedPendingMutationGen, "the early-out must not skip a texture whose only pending clear is stencil");
}
@Test
void trackerIsUnusedForFbosWithoutADepthAttachment() {
final ContextState st = new ContextState();
FBOClearTracker.recordPendingStencilClear(st, DEPTH, 0);
final FboState colorOnly = new FboState();
colorOnly.colorTextures[0] = 0xAAAAL;
colorOnly.recomputeTargets();
assertFalse(FBOClearTracker.fboHasPendingClear(st, colorOnly));
}
}
@@ -0,0 +1,112 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TextureReleaseUploadOrderTest {
private static final long HANDLE = 0xBEEFL;
private static List<Long> deferredReleases(ResourceManager rm) {
return Reflect.get(rm, "deferredTextureReleases");
}
private static void setUploadSeqs(ResourceManager rm, long lastUpload) {
Reflect.setDeclared(ResourceManager.class, rm, "lastTextureUploadSeq", lastUpload);
}
@Test
void releaseIsImmediateWhenNoTransferThreadIsAttached() {
final ResourceManager rm = SdlTestRig.resourceManager();
setUploadSeqs(rm, 50L);
assertFalse(rm.shouldDeferTextureRelease(), "with no transfer thread there is nothing to outlive");
}
@Test
void releaseIsDeferredWhileTextureUploadsAreStillQueued() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
final TransferThread tt = new TransferThread(rig.device, rm);
rm.setTransferThread(tt);
setUploadSeqs(rm, 50L);
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 10L);
rm.trackTextureHandle(HANDLE);
final int glId = rm.genTexture();
SdlReflect.putTextureHandle(rm, glId, HANDLE);
rm.deleteTexture(glId);
assertEquals(List.of(HANDLE), deferredReleases(rm), "the free must wait for the queued upload");
}
@Test
void releaseIsImmediateOnceTheTransferThreadHasCaughtUp() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
final TransferThread tt = new TransferThread(rig.device, rm);
rm.setTransferThread(tt);
setUploadSeqs(rm, 50L);
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 49L);
assertTrue(rm.shouldDeferTextureRelease(), "one upload still unsubmitted is enough to defer");
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 50L);
assertFalse(rm.shouldDeferTextureRelease(), "nothing is in flight, so no need to defer");
}
@Test
void aQueuedInlineBatchAlsoDefersTheRelease() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
rig.frameManager.setResourceManager(rm);
assertFalse(rm.shouldDeferTextureRelease(), "nothing queued yet");
rig.frameManager.frame().pendingTexHandles.add(HANDLE);
assertTrue(rm.hasBatchedUploads());
assertTrue(rm.shouldDeferTextureRelease(), "a queued batch entry must outlive the release");
rig.frameManager.frame().pendingTexHandles.clear();
assertFalse(rm.shouldDeferTextureRelease());
}
@Test
void releasingATextureBlanksItsQueuedBatchEntries() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
rig.frameManager.setResourceManager(rm);
final var handles = rig.frameManager.frame().pendingTexHandles;
handles.add(HANDLE);
handles.add(0xFEEDL);
handles.add(HANDLE);
rm.dropBatchedUploadsTargeting(HANDLE);
assertEquals(0L, handles.getLong(0), "every entry for the dying texture is blanked");
assertEquals(0xFEEDL, handles.getLong(1), "other destinations are untouched");
assertEquals(0L, handles.getLong(2));
}
@Test
void reallocReleaseObeysTheSameRule() {
final SdlTestRig rig = SdlTestRig.create();
final ResourceManager rm = rig.resourceManager;
final TransferThread tt = new TransferThread(rig.device, rm);
rm.setTransferThread(tt);
setUploadSeqs(rm, 7L);
Reflect.setDeclared(TransferThread.class, tt, "submittedSeq", 0L);
rm.trackTextureHandle(HANDLE);
final int glId = rm.genTexture();
SdlReflect.putTextureHandle(rm, glId, HANDLE);
rm.releaseTextureHandleForRealloc(glId);
assertEquals(List.of(HANDLE), deferredReleases(rm), "glTexImage2D re-specification frees the old handle on the same path as a delete");
}
}
@@ -0,0 +1,61 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SdlTestRig;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.sdlgpu.resource.TransferThread.shouldPublishRetired;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TransferRetireSeqTest {
private static TransferThread idleThread() {
final SdlTestRig rig = SdlTestRig.create();
final TransferThread tt = new TransferThread(rig.device, rig.resourceManager);
tt.shutdown();
try {
tt.getThread().join(5_000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AssertionError("interrupted while stopping the transfer thread", e);
}
assertFalse(tt.getThread().isAlive(), "the worker must be stopped first: its loop zeroes openHighestSeq after publishing, so a live worker races this test");
return tt;
}
@Test
void anOpenCommandBufferIsNeverPublishedEarly() {assertFalse(shouldPublishRetired(true, 50L, 10L),
"those copies are recorded but not submitted; a waiter must not be released");
}
@Test
void retiredSeqsArePublishedWhenNothingIsOpen() {
assertTrue(shouldPublishRetired(false, 50L, 10L));
}
@Test
void anAlreadyPublishedSeqIsNotRepublished() {
assertFalse(shouldPublishRetired(false, 10L, 10L));
assertFalse(shouldPublishRetired(false, 0L, 10L));
}
@Test
void aDroppedTextureUploadStillRetiresItsSeq() {
final TransferThread tt = idleThread();
Reflect.invoke(tt, "recordTextureUpload",
new Class<?>[]{ long.class, long.class, int.class, int.class, int.class, int.class, int.class, long.class, long.class },
0L, 0xBEEFL, 0, 0, 16, 16, 0, 1024L, 42L);
assertEquals(42L, (long) Reflect.get(tt, "openHighestSeq"), "a dropped upload must still advance the watermark or awaitSubmittedUpTo blocks forever");
}
@Test
void aDroppedBufferUploadStillRetiresItsSeq() {
final TransferThread tt = idleThread();
Reflect.invoke(tt, "recordBufferUpload", new Class<?>[]{ long.class, long.class, long.class, long.class, long.class, boolean.class }, 0L, 256L, 0xF00L, 0L, 7L, false);
assertEquals(7L, (long) Reflect.get(tt, "openHighestSeq"));
}
}
@@ -64,6 +64,26 @@ class UploadArenaTest {
assertEquals(UploadArena.INITIAL_CAPACITY * 4, a.capacity());
}
@Test
void requestGrowClampsAtMaxCapacityInsteadOfSpinning() {
final UploadArena a = new UploadArena();
a.requestGrow(Integer.MAX_VALUE);
assertTrue(a.applyGrow());
assertEquals(UploadArena.MAX_CAPACITY, a.capacity());
a.requestGrow(Integer.MAX_VALUE);
assertFalse(a.applyGrow());
assertEquals(UploadArena.MAX_CAPACITY, a.capacity());
}
@Test
void requestGrowIsIdempotentBelowCurrentTarget() {
final UploadArena a = new UploadArena();
a.requestGrow(UploadArena.INITIAL_CAPACITY * 4);
a.requestGrow(1);
assertTrue(a.applyGrow());
assertEquals(UploadArena.INITIAL_CAPACITY * 4, a.capacity());
}
@Test
void overflowThenGrowRoundTrip() {
final UploadArena a = new UploadArena();
@@ -158,7 +158,7 @@ public class BatchingFontRenderer {
private int blendSrcRGB = GL11.GL_SRC_ALPHA;
private int blendDstRGB = GL11.GL_ONE_MINUS_SRC_ALPHA;
private static final class DeferredTextSegment {
private static final class TextSegment {
final Matrix4f mvp = new Matrix4f();
BatchingFontRenderer owner;
int cmdStart;
@@ -172,10 +172,17 @@ public class BatchingFontRenderer {
private static int deferredIdxPos;
private int deferredCmdWatermark;
private static final ObjectArrayList<DeferredTextSegment> deferredSegments = ObjectArrayList.wrap(new DeferredTextSegment[16], 0);
private static final ObjectArrayList<DeferredTextSegment> deferredSegmentPool = ObjectArrayList.wrap(new DeferredTextSegment[16], 0);
private static final ObjectArrayList<TextSegment> deferredSegments = ObjectArrayList.wrap(new TextSegment[16], 0);
private static final ObjectArrayList<TextSegment> deferredSegmentPool = ObjectArrayList.wrap(new TextSegment[16], 0);
private boolean deferCurrentBatch;
private final Matrix4f batchProj = new Matrix4f();
private final Matrix4f batchModelView = new Matrix4f();
private boolean batchMatrixValid;
private final ObjectArrayList<TextSegment> batchSegments = ObjectArrayList.wrap(new TextSegment[16], 0);
private int batchSealedEnd;
private static BatchingFontRenderer arenaOwner;
private void allocateBuffers() {
populateEBO(rawCapacity / VERTEX_SIZE);
@@ -298,8 +305,9 @@ public class BatchingFontRenderer {
}
private void pushDrawCmd(int startIdx, int idxCount, ResourceLocation texture, boolean isUnicode) {
// Never coalesce into a command below the watermark - it belongs to a sealed deferred segment.
if (batchCommands.size() > deferredCmdWatermark) {
arenaOwner = this;
// Never coalesce into a command below the watermark - it belongs to a sealed segment.
if (batchCommands.size() > batchSealedEnd) {
final FontDrawCmd lastCmd = batchCommands.get(batchCommands.size() - 1);
final int prevEndVtx = lastCmd.startVtx + lastCmd.idxCount;
if (prevEndVtx == startIdx && lastCmd.texture == texture) {
@@ -370,9 +378,55 @@ public class BatchingFontRenderer {
* allow for easier optimizing of blocks of font rendering code.
*/
public void beginBatch() {
if (arenaOwner != null && arenaOwner != this) {
arenaOwner.flushBatch();
}
if (batchDepth > 0) {
if (batchCommands.size() == batchSealedEnd) {
captureBatchMatrix();
} else if (!batchProj.equals(GLStateManager.getProjectionMatrix(), 0.0f) || !batchModelView.equals(GLStateManager.getModelViewMatrix(), 0.0f)) {
sealBatchSegment();
captureBatchMatrix();
}
}
batchDepth++;
}
private void captureBatchMatrix() {
batchProj.set(GLStateManager.getProjectionMatrix());
batchModelView.set(GLStateManager.getModelViewMatrix());
batchMatrixValid = true;
}
private void resolveMvp(Matrix4f dest) {
if (batchMatrixValid) {
batchProj.mul(batchModelView, dest);
} else {
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), dest);
}
}
private void sealBatchSegment() {
final int end = batchCommands.size();
if (end == batchSealedEnd) return;
final TextSegment segment = deferredSegmentPool.isEmpty() ? new TextSegment() : deferredSegmentPool.pop();
segment.owner = this;
segment.cmdStart = batchSealedEnd;
segment.cmdEnd = end;
resolveMvp(segment.mvp);
batchSegments.add(segment);
batchSealedEnd = end;
}
private void recycleBatchSegments() {
if (batchSegments.isEmpty()) return;
for (int i = 0, n = batchSegments.size(); i < n; i++) {
batchSegments.get(i).owner = null;
}
deferredSegmentPool.addAll(batchSegments);
batchSegments.clear();
}
public void endBatch() {
if (batchDepth <= 0) {
batchDepth = 0;
@@ -404,7 +458,8 @@ public class BatchingFontRenderer {
}
private void deferBatch() {
if (batchCommands.size() == deferredCmdWatermark || vertexDataPos == deferredVertexPos) {
sealBatchSegment();
if (batchSegments.isEmpty()) {
truncateBatchToWatermark();
return;
}
@@ -412,15 +467,13 @@ public class BatchingFontRenderer {
flushBatch();
return;
}
final DeferredTextSegment segment = deferredSegmentPool.isEmpty() ? new DeferredTextSegment() : deferredSegmentPool.pop();
segment.owner = this;
segment.cmdStart = deferredCmdWatermark;
segment.cmdEnd = batchCommands.size();
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), segment.mvp);
deferredSegments.add(segment);
deferredSegments.addAll(batchSegments);
batchSegments.clear();
deferredCmdWatermark = batchCommands.size();
batchSealedEnd = deferredCmdWatermark;
deferredVertexPos = vertexDataPos;
deferredIdxPos = idxWriterIndex;
batchMatrixValid = false;
}
public static void flushDeferredText() {
@@ -443,7 +496,7 @@ public class BatchingFontRenderer {
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
for (int i = 0, n = deferredSegments.size(); i < n; i++) {
final DeferredTextSegment segment = deferredSegments.get(i);
final TextSegment segment = deferredSegments.get(i);
mvpBuf.clear();
segment.mvp.get(mvpBuf);
GLStateManager.glUniformMatrix4(segment.owner.mvpMatrixLocation, false, mvpBuf);
@@ -459,8 +512,11 @@ public class BatchingFontRenderer {
public static void discardDeferredText() {
if (deferredSegments.isEmpty()) return;
for (int i = 0, n = deferredSegments.size(); i < n; i++) {
final DeferredTextSegment segment = deferredSegments.get(i);
final TextSegment segment = deferredSegments.get(i);
segment.owner.clearDeferredCommands();
segment.owner.recycleBatchSegments();
segment.owner.batchSealedEnd = 0;
segment.owner.batchMatrixValid = false;
segment.owner = null;
}
deferredSegmentPool.addAll(deferredSegments);
@@ -469,6 +525,7 @@ public class BatchingFontRenderer {
deferredIdxPos = 0;
vertexDataPos = 0;
idxWriterIndex = 0;
arenaOwner = null;
}
private void clearDeferredCommands() {
@@ -477,6 +534,12 @@ public class BatchingFontRenderer {
batchCommandPool.add(batchCommands.get(i));
}
batchCommands.removeElements(0, deferredCmdWatermark);
for (int i = 0, n = batchSegments.size(); i < n; i++) {
final TextSegment segment = batchSegments.get(i);
segment.cmdStart -= deferredCmdWatermark;
segment.cmdEnd -= deferredCmdWatermark;
}
batchSealedEnd = Math.max(0, batchSealedEnd - deferredCmdWatermark);
deferredCmdWatermark = 0;
}
@@ -495,7 +558,8 @@ public class BatchingFontRenderer {
private void flushBatchInner() {
final int cmdCount = batchCommands.size();
if (vertexDataPos == deferredVertexPos || cmdCount == deferredCmdWatermark) {
final int segmentCount = batchSegments.size();
if (segmentCount == 0 && (vertexDataPos == deferredVertexPos || cmdCount == deferredCmdWatermark)) {
truncateBatchToWatermark();
return;
}
@@ -504,7 +568,12 @@ public class BatchingFontRenderer {
final int prevProgram = GLStateManager.glGetInteger(GL20.GL_CURRENT_PROGRAM);
Arrays.sort(batchCommands.elements(), deferredCmdWatermark, cmdCount, FontDrawCmd.DRAW_ORDER_COMPARATOR);
final FontDrawCmd[] cmds = batchCommands.elements();
for (int i = 0; i < segmentCount; i++) {
final TextSegment segment = batchSegments.get(i);
Arrays.sort(cmds, segment.cmdStart, segment.cmdEnd, FontDrawCmd.DRAW_ORDER_COMPARATOR);
}
Arrays.sort(cmds, batchSealedEnd, cmdCount, FontDrawCmd.DRAW_ORDER_COMPARATOR);
final boolean isTextureEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_TEXTURE_2D);
final boolean isBlendEnabledBefore = GLStateManager.glIsEnabled(GL11.GL_BLEND);
@@ -513,16 +582,25 @@ public class BatchingFontRenderer {
final boolean depthMaskBefore = GLStateManager.getDepthState().isEnabled();
setupFontDrawState();
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
GLStateManager.getProjectionMatrix().mul(GLStateManager.getModelViewMatrix(), scratchMvp);
scratchMvp.get(mvpBuf);
GLStateManager.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
}
flushLastTexture = DUMMY_RESOURCE_LOCATION;
flushTextureChanged = false;
drawCommands(batchCommands.elements(), deferredCmdWatermark, cmdCount, this);
try (MemoryStack stack = stackPush()) {
final FloatBuffer mvpBuf = stack.mallocFloat(16);
for (int i = 0; i < segmentCount; i++) {
final TextSegment segment = batchSegments.get(i);
mvpBuf.clear();
segment.mvp.get(mvpBuf);
GLStateManager.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
drawCommands(cmds, segment.cmdStart, segment.cmdEnd, this);
}
if (batchSealedEnd < cmdCount) {
mvpBuf.clear();
resolveMvp(scratchMvp);
scratchMvp.get(mvpBuf);
GLStateManager.glUniformMatrix4(mvpMatrixLocation, false, mvpBuf);
drawCommands(cmds, batchSealedEnd, cmdCount, this);
}
}
restoreFontDrawState(prevProgram, isTextureEnabledBefore, isBlendEnabledBefore, boundTextureBefore);
restoreDepth(depthTestBefore, depthMaskBefore);
@@ -622,12 +700,18 @@ public class BatchingFontRenderer {
}
private void truncateBatchToWatermark() {
recycleBatchSegments();
for (int i = batchCommands.size() - 1; i >= deferredCmdWatermark; i--) {
batchCommandPool.add(batchCommands.get(i));
}
batchCommands.size(deferredCmdWatermark);
batchSealedEnd = deferredCmdWatermark;
batchMatrixValid = false;
vertexDataPos = deferredVertexPos;
idxWriterIndex = deferredIdxPos;
if (arenaOwner == this) {
arenaOwner = null;
}
}
// === Actual text mesh generation
@@ -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.")
@@ -7,7 +7,7 @@ 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.ShaderWorkSubmitter;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import net.coderbot.iris.Iris;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
@@ -59,10 +59,10 @@ public class IrisGLSMBridge {
};
GLSMHooks.postTransformProcessor = (src, shaderType) -> {
if (Iris.ShaderTransformExecutor.isOnWorker()) {
ShaderManager.prewarmSpirv(src, shaderType.id);
SDLGPUGate.prewarmSpirv(src, shaderType.id);
} else {
Iris.ShaderTransformExecutor.submitTracked(() -> {
ShaderManager.prewarmSpirv(src, shaderType.id);
SDLGPUGate.prewarmSpirv(src, shaderType.id);
});
}
};
@@ -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) {
@@ -91,7 +91,10 @@ public enum Mixins implements IMixins {
ANGELICA_SDL_GPU_DISPLAY(new MixinBuilder("SDL-GPU-aware Display.create path")
.setPhase(Phase.EARLY)
.setApplyIf(() -> SystemProperties.USE_SDL_GPU && SDLGPUGate.isSDLGPUAvailable())
.addClientMixins("sdlgpu.MixinForgeHooksClient_SDLGPUDisplay")
.addClientMixins(
"sdlgpu.MixinForgeHooksClient_SDLGPUDisplay",
"sdlgpu.MixinMinecraft_SDLGPUIcons"
)
),
ANGELICA_SDL_GPU_SHADOW_VOXEL_PREPASS(new MixinBuilder("Voxelization compute pre-pass before shadow raster (SDL_GPU)")
@@ -168,7 +171,10 @@ public enum Mixins implements IMixins {
.addClientMixins(
"angelica.entity.MixinModelRenderer",
"angelica.entity.MixinRenderGlobal_EntityBatch",
"angelica.entity.MixinTextureManager"
"angelica.entity.MixinRenderManager_BatchEligibility",
"angelica.entity.MixinRender_BatchEligibility",
"angelica.entity.MixinTextureManager",
"angelica.tesr.MixinTileEntitySpecialRenderer_BatchEligibility"
)
),
@@ -197,9 +203,9 @@ public enum Mixins implements IMixins {
)
),
ANGELICA_TESR_PROVIDER_DISPATCH(new MixinBuilder("Route TesrMeshProvider renderers through the batched mesh cache")
ANGELICA_TESR_PROVIDER_DISPATCH(new MixinBuilder("Route TesrMeshProvider renderers through the batched mesh cache, and observe renderers that mix model parts with their own draws")
.setPhase(Phase.EARLY)
.setApplyIf(() -> AngelicaConfig.enableTESRProviderDispatch)
.setApplyIf(() -> AngelicaConfig.enableTESRProviderDispatch || AngelicaConfig.enableEntityBatching)
.addClientMixins(
"angelica.tesr.MixinTileEntityRendererDispatcher"
)
@@ -0,0 +1,8 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
public interface IBatchEligibility {
byte angelica$batchState();
void angelica$setBatchState(byte state);
}
@@ -0,0 +1,8 @@
package com.gtnewhorizons.angelica.mixins.interfaces;
import net.minecraft.client.model.TexturedQuad;
public interface ModelBoxQuads {
TexturedQuad[] angelica$getQuadList();
}
@@ -106,6 +106,10 @@ public final class ClientProxy extends CommonProxy {
return CONFIG;
}
public static void applyVSyncMode(boolean vsyncEnabled) {
GLStateManager.setVSyncMode(options().advanced.vsyncMode, vsyncEnabled);
}
@Override
public void preInit(FMLPreInitializationEvent event) {
ModStatus.preInit();
@@ -0,0 +1,116 @@
package com.gtnewhorizons.angelica.rendering;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
public final class FramePacer {
private static final int MIN_PLAUSIBLE_REFRESH_HZ = 20;
private static final int MAX_PLAUSIBLE_REFRESH_HZ = 1000;
private static final long REFRESH_POLL_NANOS = 1_000_000_000L;
private static long lastFrameTime;
private static long fpsLimitOverhead;
private static long lastRefreshPollNanos;
private static int cachedRefreshHz;
private static boolean warnedUnknownRefresh;
private static VSyncMode lastVSyncMode = VSyncMode.ON;
private static int lastCapHz;
private static int lastRefreshHz;
private FramePacer() {}
public static int plausibleRefreshHz(int hz) {
return hz >= MIN_PLAUSIBLE_REFRESH_HZ && hz <= MAX_PLAUSIBLE_REFRESH_HZ ? hz : 0;
}
public static int pacingCeilingHz(VSyncMode mode, int capHz, int refreshHz) {
if (mode.blocksOnVBlank()) return 0;
final int cap = Math.max(capHz, 0);
if (mode != VSyncMode.MAILBOX) return cap;
final int refresh = plausibleRefreshHz(refreshHz);
if (cap == 0) return refresh;
return refresh == 0 ? cap : Math.min(cap, refresh);
}
public static long sleepNanosFor(long targetNanos, long lastWorkNanos, long overheadNanos) {
return targetNanos - lastWorkNanos - overheadNanos;
}
public static int updateCeiling(int capHz) {
lastVSyncMode = GLStateManager.getEffectiveVSyncMode();
lastCapHz = Math.max(capHz, 0);
lastRefreshHz = refreshHz();
return pacingCeilingHz(lastVSyncMode, lastCapHz, lastRefreshHz);
}
public static long pace(int ceilingHz) {
if (ceilingHz > 0) {
final long time = System.nanoTime();
final long targetNanos = 1_000_000_000L / ceilingHz;
final long sleepNanos = sleepNanosFor(targetNanos, time - lastFrameTime, fpsLimitOverhead);
if (sleepNanos > 0) {
try {
Thread.sleep(sleepNanos / 1_000_000, (int) sleepNanos % 1_000_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
long overhead = System.nanoTime() - time - sleepNanos;
if (overhead < 0 || overhead > targetNanos / 2) overhead = 0;
fpsLimitOverhead = overhead;
}
}
final long time = System.nanoTime();
final long period = time - lastFrameTime;
lastFrameTime = time;
return period;
}
private static int refreshHz() {
final long now = System.nanoTime();
if (lastRefreshPollNanos == 0L || now - lastRefreshPollNanos >= REFRESH_POLL_NANOS) {
lastRefreshPollNanos = now;
cachedRefreshHz = GLStateManager.getDisplayRefreshRateHz();
}
final int hz = plausibleRefreshHz(cachedRefreshHz);
if (hz == 0 && lastVSyncMode == VSyncMode.MAILBOX && !warnedUnknownRefresh) {
warnedUnknownRefresh = true;
AngelicaMod.LOGGER.warn("Display refresh rate reported as {}Hz; Mailbox will not be paced to the refresh rate. The Max Framerate setting still applies.", cachedRefreshHz);
}
return hz;
}
public static String debugIndicator(VSyncMode mode, int refreshHz, int capHz) {
final boolean vsync = mode.blocksOnVBlank();
final boolean mailbox = mode == VSyncMode.MAILBOX;
final boolean showCap = capHz > 0 && !vsync;
if (!vsync && !mailbox && !showCap) return null;
final StringBuilder sb = new StringBuilder(" [");
if (vsync || mailbox) {
sb.append(vsync ? "vsync" : "mailbox");
if (refreshHz > 0) sb.append(' ').append(refreshHz);
if (showCap) sb.append(", ");
}
if (showCap) sb.append("cap ").append(capHz);
return sb.append(']').toString();
}
public static String debugIndicator() {
return debugIndicator(lastVSyncMode, lastRefreshHz, lastCapHz);
}
public static VSyncMode lastVSyncMode() { return lastVSyncMode; }
public static int lastCapHz() { return lastCapHz; }
public static int lastRefreshHz() { return lastRefreshHz; }
}
@@ -5,66 +5,103 @@ import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFlags;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.QuadConverter;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.glsm.states.ColorMask;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfo;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.mixins.interfaces.ModelBoxQuads;
import com.gtnewhorizons.angelica.rendering.tesr.PassRebindGate;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.gl.blending.DepthColorStorage;
import net.coderbot.iris.gl.image.GlImage;
import net.coderbot.iris.pipeline.DeferredWorldRenderingPipeline;
import net.coderbot.iris.uniforms.CapturedRenderingState;
import net.minecraft.client.model.ModelBiped;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.model.PositionTextureVertex;
import net.minecraft.client.model.TexturedQuad;
import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.RendererLivingEntity;
import net.minecraft.entity.player.EntityPlayer;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
/**
* Skins in 1.7.10 are 64x32 while modern has 64x64 textures. We have to address it here.
* Feeds the shaderpack's world-space player reflection, which expects the modern 12-box player model:
* 288 vertices over a 64x64 skin layout. 1.7.10 has 7 boxes and a 64x32 layout, so both are synthesized here.
*/
public final class PlayerReflectionCapture {
public static final int VERTEX_COUNT = 288;
public static final int FLOATS_PER_VERTEX = 5; // x, y, z, u, v
public static final int PART_COUNT = 12;
public static final int VERTICES_PER_PART = VERTEX_COUNT / PART_COUNT;
private static final int RAW_INTS_PER_VERTEX = 8;
private static final int ATLAS_LAYOUT = 64;
private static final int VERTEX_FLAGS = VertexFlags.convertToFlags(true, false, true, false);
private static final Tracy.ZoneId Z_PLAYER_REFLECTION_FLUSH = Tracy.zoneId("playerReflectionFlush", Tracy.COLOR_IRIS);
private static boolean active;
private static boolean drawnThisCapture;
private static boolean captured;
@Getter
private static Object target;
@Getter
private static EntityPlayer targetEntity;
private static boolean pending;
private static final Matrix4f pendingModelView = new Matrix4f();
private static int pendingSkin;
private static int pendingEntityId;
private static int vbo;
private static int vao;
private static int[] rawData;
private static float[] vertexScratch;
private static int skin64Tex;
private static int skin64Size;
private static boolean skin64Valid;
private static ByteBuffer skinPixels;
private static int lastCapturedSkin;
private static int lastUploadedAtlas;
private static int skinCopyWidth = 64;
private static int skinCopyHeight = 32;
private static TextureInfo lastCapturedInfo;
private static int lastCapturedGeneration = -1;
private static TextureInfo lastUploadedAtlasInfo;
private static int lastUploadedAtlasGeneration = -1;
private static int skinCopyWidth;
private static int skinCopyHeight;
private static ByteBuffer repackBuffer;
private static long repackAddress;
private static int repackCapacity;
private static final ModelRenderer[] partOrder = new ModelRenderer[PART_COUNT];
private static final Matrix4f partMatrix = new Matrix4f();
private static final Vector3f partVertex = new Vector3f();
private static final Matrix4f savedModelView = new Matrix4f();
private PlayerReflectionCapture() {}
public static void begin(EntityPlayer player) {
active = false;
drawnThisCapture = false;
captured = false;
pending = false;
target = null;
targetEntity = null;
if (player == null) return;
@@ -79,24 +116,122 @@ public final class PlayerReflectionCapture {
public static void end() {
active = false;
pending = false;
target = null;
targetEntity = null;
}
public static boolean shouldCapture() {
return active && !drawnThisCapture;
return active && !captured;
}
/**
* Emit the 288-vertex player batch VBO.
*/
public static void drawPlayerCapture(float[] xyzuv) {
if (drawnThisCapture) return;
drawnThisCapture = true;
public static void emitAndSubmit(ModelBiped model, float scale) {
captured = true;
final float[] data = vertexScratch();
emitModel(model, scale, data);
pendingModelView.set(GLStateManager.getModelViewMatrix());
pendingSkin = GLStateManager.getBoundTextureForServerState(0);
pendingEntityId = CapturedRenderingState.INSTANCE.getCurrentRenderedEntity();
pending = true;
}
public static ModelRenderer[] partOrder(ModelBiped model, ModelRenderer[] out) {
out[0] = model.bipedHead;
out[1] = model.bipedHeadwear;
out[2] = model.bipedRightArm;
out[3] = model.bipedRightArm;
out[4] = model.bipedLeftLeg;
out[5] = model.bipedLeftLeg;
out[6] = model.bipedLeftArm;
out[7] = model.bipedLeftArm;
out[8] = model.bipedRightLeg;
out[9] = model.bipedRightLeg;
out[10] = model.bipedBody;
out[11] = model.bipedBody;
return out;
}
private static void emitModel(ModelBiped model, float scale, float[] out) {
final ModelRenderer[] parts = partOrder(model, partOrder);
int o = 0;
for (int i = 0; i < PART_COUNT; i++) {
o = emitBox(parts[i], quadsOf(parts[i]), out, o, scale);
}
}
private static TexturedQuad[] quadsOf(ModelRenderer part) {
if (part == null || part.cubeList == null || part.cubeList.isEmpty()) return null;
final ModelBox box = part.cubeList.getFirst();
return ((ModelBoxQuads) box).angelica$getQuadList();
}
public static int emitBox(ModelRenderer part, TexturedQuad[] quads, float[] out, int o, float scale) {
if (part == null || quads == null || part.isHidden || !part.showModel || part.textureWidth > ATLAS_LAYOUT || part.textureHeight > ATLAS_LAYOUT) {
final int end = o + VERTICES_PER_PART * FLOATS_PER_VERTEX;
Arrays.fill(out, o, end, 0.0f);
return end;
}
final Matrix4f m = partMatrix
.rotationZYX(part.rotateAngleZ, part.rotateAngleY, part.rotateAngleX)
.setTranslation(
part.offsetX + part.rotationPointX * scale,
part.offsetY + part.rotationPointY * scale,
part.offsetZ + part.rotationPointZ * scale);
final float uScale = part.textureWidth / ATLAS_LAYOUT;
final float vScale = part.textureHeight / ATLAS_LAYOUT;
final Vector3f v = partVertex;
for (int q = 0; q < 6; q++) {
final PositionTextureVertex[] corners = quads[q].vertexPositions;
for (int c = 0; c < 4; c++) {
final PositionTextureVertex corner = corners[c];
v.set((float) corner.vector3D.xCoord * scale,
(float) corner.vector3D.yCoord * scale,
(float) corner.vector3D.zCoord * scale);
m.transformPosition(v);
out[o++] = v.x;
out[o++] = v.y;
out[o++] = v.z;
out[o++] = corner.texturePositionX * uScale;
out[o++] = corner.texturePositionY * vScale;
}
}
return o;
}
public static void flush() {
if (!pending) return;
pending = false;
if (!Iris.enabled) return;
final float[] xyzuv = vertexScratch;
if (xyzuv == null || xyzuv.length < VERTEX_COUNT * FLOATS_PER_VERTEX) return;
// Build the Tessellator layout
if (rawData == null) rawData = new int[VERTEX_COUNT * RAW_INTS_PER_VERTEX];
PassRebindGate.rebindIfDirty();
if (!(Iris.getPipelineManager().getPipelineNullable() instanceof DeferredWorldRenderingPipeline pipeline)) return;
if (GLStateManager.getActiveProgram() != pipeline.getActivePassProgramId()) return;
if (Tracy.ENABLED) Tracy.beginZone(Z_PLAYER_REFLECTION_FLUSH);
try {
flushInner(xyzuv);
} finally {
if (Tracy.ENABLED) Tracy.endZone();
}
}
private static void flushInner(float[] xyzuv) {
if (rawData == null) {
rawData = new int[VERTEX_COUNT * RAW_INTS_PER_VERTEX];
for (int i = 0; i < VERTEX_COUNT; i++) {
final int d = i * RAW_INTS_PER_VERTEX;
rawData[d + 5] = 0xFFFFFFFF; // color
rawData[d + 6] = 0x00008000; // normal
rawData[d + 7] = 0x00F000F0; // light
}
}
final int[] raw = rawData;
for (int i = 0; i < VERTEX_COUNT; i++) {
final int s = i * FLOATS_PER_VERTEX;
@@ -106,12 +241,8 @@ public final class PlayerReflectionCapture {
raw[d + 2] = Float.floatToRawIntBits(xyzuv[s + 2]);
raw[d + 3] = Float.floatToRawIntBits(xyzuv[s + 3]);
raw[d + 4] = Float.floatToRawIntBits(xyzuv[s + 4]);
raw[d + 5] = 0xFFFFFFFF; // color
raw[d + 6] = 0x00008000; // normal
raw[d + 7] = 0x00F000F0; // light
}
// Pack into our own native buffer using the player model's vertex format
final VertexFormat format = DefaultVertexFormat.ALL_FORMATS[VERTEX_FLAGS];
ensureRepackCapacity(VERTEX_COUNT * format.getVertexSize());
@@ -119,25 +250,33 @@ public final class PlayerReflectionCapture {
repackBuffer.position(0);
repackBuffer.limit((int) (writePtr - repackAddress));
// Upload to our dedicated VBO at offset 0
ensureVAO(format);
GLStateManager.glBindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glBufferData(GL15.GL_ARRAY_BUFFER, repackBuffer, GL15.GL_STREAM_DRAW);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
// Bind a 64x64 copy of the skin as `tex`
final int activeSkin = GLStateManager.getBoundTextureForServerState();
final boolean haveSkin = activeSkin != 0 && activeSkin != skin64Tex;
if (haveSkin) {
ensureSkin64Texture();
final int prevUnit = GL13.GL_TEXTURE0 + GLStateManager.getActiveTextureUnitForServerState();
GLStateManager.glActiveTexture(GL13.GL_TEXTURE0);
final int prevTexture = GLStateManager.getBoundTextureForServerState();
if (pendingSkin > 0 && pendingSkin != skin64Tex) {
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(pendingSkin);
final int skinGeneration = info.getUploadGeneration();
final boolean skinChanged = info != lastCapturedInfo || skinGeneration != lastCapturedGeneration;
final GlImage atlas = GlImage.BY_NAME.get("playerAtlas_img");
final int atlasId = atlas != null ? atlas.getId() : 0;
final boolean skinChanged = activeSkin != lastCapturedSkin;
final TextureInfo atlasInfo = atlasId > 0 ? TextureInfoCache.INSTANCE.getInfo(atlasId) : null;
final int atlasGeneration = atlasInfo != null ? atlasInfo.getUploadGeneration() : 0;
final boolean atlasChanged = atlasInfo != lastUploadedAtlasInfo || atlasGeneration != lastUploadedAtlasGeneration;
if (skinChanged) {
final TextureInfo info = TextureInfoCache.INSTANCE.getInfo(activeSkin);
lastCapturedInfo = info;
lastCapturedGeneration = skinGeneration;
skinCopyWidth = 0;
skinCopyHeight = 0;
final int srcW = info.getWidth();
final int srcH = info.getHeight();
if (srcW > 0 && srcH > 0) {
@@ -145,48 +284,72 @@ public final class PlayerReflectionCapture {
if (skinPixels == null || skinPixels.capacity() < required) {
skinPixels = ByteBuffer.allocateDirect(required).order(ByteOrder.nativeOrder());
}
// The 1.7.10 model UVs only address the top 64x32 region.
skinCopyWidth = Math.min(64, srcW);
skinCopyHeight = Math.min(32, srcH);
skinPixels.clear();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, activeSkin);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, pendingSkin);
GLStateManager.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
lastCapturedSkin = activeSkin;
skinCopyWidth = srcW;
skinCopyHeight = srcH;
}
}
if (skinPixels != null && (skinChanged || atlasId != lastUploadedAtlas)) {
skinPixels.rewind();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, skinCopyWidth, skinCopyHeight, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
if (skinCopyWidth > 0 && (skinChanged || atlasChanged)) {
ensureSkin64Texture(Math.max(skinCopyWidth, skinCopyHeight));
if (atlas != null) {
final int prevRowLength = GLStateManager.glGetInteger(GL11.GL_UNPACK_ROW_LENGTH);
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, skinCopyWidth);
try {
skinPixels.rewind();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, atlasId);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, skinCopyWidth, skinCopyHeight, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
skin64Valid = true;
if (atlasInfo != null) {
final int dstW = Math.min(atlasInfo.getWidth(), skinCopyWidth);
final int dstH = Math.min(atlasInfo.getHeight(), skinCopyHeight);
if (dstW > 0 && dstH > 0) {
skinPixels.rewind();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, atlasId);
GLStateManager.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, dstW, dstH, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, skinPixels);
}
}
} finally {
GLStateManager.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, prevRowLength);
}
lastUploadedAtlas = atlasId;
lastUploadedAtlasInfo = atlasInfo;
lastUploadedAtlasGeneration = atlasInfo != null ? atlasInfo.getUploadGeneration() : 0;
}
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
if (skin64Valid) GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
}
final boolean maskable = !DepthColorStorage.isDepthColorLocked();
final ColorMask prevColor = GLStateManager.getColorMask();
final boolean pr = prevColor.red, pg = prevColor.green, pb = prevColor.blue, pa = prevColor.alpha;
final boolean prevDepthWrite = GLStateManager.getDepthState().isEnabled();
GLStateManager.glColorMask(false, false, false, false);
GLStateManager.glDepthMask(false);
QuadConverter.drawQuadsAsTriangles(0, VERTEX_COUNT);
GLStateManager.glDepthMask(prevDepthWrite);
GLStateManager.glColorMask(pr, pg, pb, pa);
final int prevEntityId = CapturedRenderingState.INSTANCE.getCurrentRenderedEntity();
savedModelView.set(GLStateManager.getModelViewMatrix());
if (haveSkin) {
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, activeSkin);
CapturedRenderingState.INSTANCE.setCurrentEntity(pendingEntityId);
GLStateManager.setModelViewMatrix(pendingModelView);
if (maskable) {
GLStateManager.glColorMask(false, false, false, false);
GLStateManager.glDepthMask(false);
}
try {
QuadConverter.drawQuadsAsTriangles(0, VERTEX_COUNT);
} finally {
if (maskable) {
GLStateManager.glDepthMask(prevDepthWrite);
GLStateManager.glColorMask(pr, pg, pb, pa);
}
GLStateManager.setModelViewMatrix(savedModelView);
CapturedRenderingState.INSTANCE.setCurrentEntity(prevEntityId);
GLStateManager.glBindVertexArray(0);
GLStateManager.glBindVertexArray(0);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, prevTexture);
GLStateManager.glActiveTexture(prevUnit);
}
}
public static float[] vertexScratch() {
@@ -205,16 +368,16 @@ public final class PlayerReflectionCapture {
GLStateManager.glBindVertexArray(0);
}
private static void ensureSkin64Texture() {
if (skin64Tex != 0) return;
final int prev = GLStateManager.getBoundTextureForServerState();
skin64Tex = GLStateManager.glGenTextures();
private static void ensureSkin64Texture(int size) {
if (skin64Tex != 0 && skin64Size == size) return;
if (skin64Tex == 0) skin64Tex = GLStateManager.glGenTextures();
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, skin64Tex);
final ByteBuffer zero = ByteBuffer.allocateDirect(64 * 64 * 4);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, 64, 64, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, zero);
final ByteBuffer zero = ByteBuffer.allocateDirect(size * size * 4);
GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, zero);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, prev);
skin64Size = size;
skin64Valid = false;
}
private static void ensureRepackCapacity(int requiredBytes) {
@@ -78,6 +78,10 @@ public class CeleritasDebugScreenHandler {
if (transferInfo != null) {
lines.add(transferInfo);
}
final String presentInfo = RENDER_BACKEND.getPresentDebugInfo();
if (presentInfo != null) {
lines.add(presentInfo);
}
if (SystemProperties.DEBUG_F3_DETAIL) {
lines.addAll(TesrBatchRenderer.INSTANCE.getDebugDetailStrings());
lines.add(ModelPartBatcher.INSTANCE.getDebugString());
@@ -0,0 +1,58 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
/** Excludes renderers that mix batched model parts with their own draws, which reorders their geometry. */
public final class BatchEligibility {
public static final byte UNKNOWN = 0;
public static final byte SAFE = 1;
public static final byte DENIED = 2;
private static int depth;
private static boolean allowed;
private static long drawsAtStart;
private static long expectedDraws;
private static int parts;
private BatchEligibility() {}
public static boolean begin(byte state, long drawCount) {
if (depth++ > 0) return allowed;
drawsAtStart = drawCount;
expectedDraws = 0L;
parts = 0;
allowed = state == SAFE;
return allowed;
}
public static byte end(byte state, long drawCount) {
if (--depth > 0) return state;
allowed = false;
if (parts == 0) return state;
final long foreign = (drawCount - drawsAtStart) - expectedDraws;
if (foreign > 0L) return DENIED;
return state == UNKNOWN ? SAFE : state;
}
public static boolean batchingAllowed() {
return allowed;
}
public static void onPartQueued() {
parts++;
}
public static void onPartFallback(long drawsBefore, long drawsNow) {
parts++;
if (!allowed) {
expectedDraws += drawsNow - drawsBefore;
}
}
public static void onStateChange(Object renderer, byte state) {
if (state != DENIED) return;
final String name = renderer.getClass().getName();
GLStateManager.warnOnce("tesr-mixed:" + name, "{} draws its own geometry alongside model parts - excluding it from model part batching", name);
}
}
@@ -49,7 +49,8 @@ public final class ModelPartBatcher {
TEXTURE("tesr.bail.texture"),
TEMPLATE("tesr.bail.template"),
HURT_FLASH("tesr.bail.hurtFlash"),
FOREIGN_PROGRAM("tesr.bail.foreignProgram");
FOREIGN_PROGRAM("tesr.bail.foreignProgram"),
MIXED_RENDERER("tesr.bail.mixedRenderer");
public static final BailReason[] VALUES = values();
public final String plotName;
@@ -204,6 +205,10 @@ public final class ModelPartBatcher {
r.bail(BailReason.INACTIVE);
return false;
}
if (!BatchEligibility.batchingAllowed()) {
r.bail(BailReason.MIXED_RENDERER);
return false;
}
if (DisplayListManager.isRecording()) {
r.bail(BailReason.RECORDING);
return false;
@@ -212,6 +217,7 @@ public final class ModelPartBatcher {
r.liveFallbacks++;
return false;
}
BatchEligibility.onPartQueued();
return true;
}
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.rendering.voxelization;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor;
import org.embeddedt.embeddium.impl.gl.attribute.GlVertexFormat;
import org.embeddedt.embeddium.impl.gl.buffer.GlBuffer;
@@ -26,8 +26,8 @@ public final class SdlShadowVoxelizationSink implements ShadowVoxelizer.Sink {
private float pendingY;
private float pendingZ;
private static SDLGPURenderBackend sdl() {
return (SDLGPURenderBackend) BackendManager.RENDER_BACKEND;
private static RenderBackend sdl() {
return BackendManager.RENDER_BACKEND;
}
@Override
@@ -9,8 +9,7 @@ public class RenderPassAPI {
public static RenderPassAPI instance = new RenderPassAPI();
private static final String[] NAMES = new String[] { "solid", "cutout_mipped", "cutout", "translucent", "backface",
"overlay" };
private static final String[] NAMES = new String[] { "solid", "cutout_mipped", "cutout", "translucent", "backface", "overlay" };
public static final int SOLID_RENDER_PASS = 0;
public static final int CUTOUT_MIPPED_RENDER_PASS = 1;
@@ -3,10 +3,10 @@ package jss.notfine.gui;
import com.google.common.collect.ImmutableList;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import jss.notfine.config.NotFineConfig;
import jss.notfine.core.Settings;
import jss.notfine.core.SettingsManager;
import me.jellysquid.mods.sodium.client.gui.FrameRateOptions;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptionPages;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import me.jellysquid.mods.sodium.client.gui.options.OptionFlag;
@@ -43,6 +43,7 @@ public class NotFineGameOptionPages {
public static OptionPage general() {
List<OptionGroup> groups = new ArrayList<>();
final FrameRateOptions frameRate = FrameRateOptions.create(vanillaOpts, sodiumOpts);
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(GraphicsMode.class, vanillaOpts)
.setName(I18n.format("options.graphics"))
@@ -63,16 +64,7 @@ public class NotFineGameOptionPages {
.setImpact(OptionImpact.HIGH)
.setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD)
.build())
.add(OptionImpl.createBuilder(boolean.class, vanillaOpts)
.setName(I18n.format("options.vsync"))
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> {
opts.enableVsync = value;
GLStateManager.setVSyncEnabled(opts.enableVsync);
}, opts -> opts.enableVsync)
.setImpact(OptionImpact.VARIES)
.build())
.add(frameRate.vsync())
.add(OptionImpl.createBuilder(boolean.class, vanillaOpts)
.setName(I18n.format("options.fullscreen"))
.setTooltip(I18n.format("sodium.options.fullscreen.tooltip"))
@@ -90,12 +82,7 @@ public class NotFineGameOptionPages {
.build());
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.framerateLimit"))
.setTooltip(I18n.format("sodium.options.fps_limit.tooltip"))
.setControl(option -> new SliderControl(option, 5, 260, 1, ControlValueFormatter.fpsLimit()))
.setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate)
.build())
.add(frameRate.maxFramerate())
.build());
final OptionImpl<GameSettings, Integer> mipmapLevels = OptionImpl.createBuilder(int.class, vanillaOpts)
@@ -7,19 +7,20 @@ import net.minecraft.client.Minecraft;
public class CyclingControlElement<T extends Enum<T>> extends NotFineControlElement<T> {
private final T[] allowedValues;
private final String[] names;
private int currentIndex = 0;
public CyclingControlElement(Option<T> option, Dim2i dim, T[] allowedValues, String[] names) {
super(option, dim);
this.allowedValues = allowedValues;
this.names = names;
}
private int currentIndex() {
for(int i = 0; i < allowedValues.length; ++i) {
if(allowedValues[i] == option.getValue()) {
currentIndex = i;
break;
return i;
}
}
return 0;
}
@Override
@@ -31,8 +32,7 @@ public class CyclingControlElement<T extends Enum<T>> extends NotFineControlElem
@Override
public boolean mousePressed(Minecraft mc, int mouseX, int mouseY) {
if(super.mousePressed(mc, mouseX, mouseY)) {
currentIndex = (currentIndex + 1) % allowedValues.length;
option.setValue(allowedValues[currentIndex]);
option.setValue(allowedValues[(currentIndex() + 1) % allowedValues.length]);
onOptionValueChanged();
return true;
} else {
@@ -0,0 +1,52 @@
package me.jellysquid.mods.sodium.client.gui;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import me.jellysquid.mods.sodium.client.gui.options.Option;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpact;
import me.jellysquid.mods.sodium.client.gui.options.OptionImpl;
import me.jellysquid.mods.sodium.client.gui.options.control.ControlValueFormatter;
import me.jellysquid.mods.sodium.client.gui.options.control.CyclingControl;
import me.jellysquid.mods.sodium.client.gui.options.control.SliderControl;
import me.jellysquid.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage;
import me.jellysquid.mods.sodium.client.gui.options.storage.SodiumOptionsStorage;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.I18n;
import net.minecraft.client.settings.GameSettings;
public record FrameRateOptions(Option<VSyncMode> vsync, Option<Integer> maxFramerate) {
public static FrameRateOptions create(MinecraftOptionsStorage vanillaOpts, SodiumOptionsStorage sodiumOpts) {
final OptionImpl<SodiumGameOptions, VSyncMode> vsync = OptionImpl.createBuilder(VSyncMode.class, sodiumOpts)
.setName(I18n.format("options.vsync"))
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
.setControl(option -> new CyclingControl<>(option, VSyncMode.class,
GLStateManager.getSelectableVSyncModes().toArray(new VSyncMode[0]),
new String[]{
I18n.format("sodium.options.vsync_mode.auto"),
I18n.format("sodium.options.vsync_mode.on"),
I18n.format("sodium.options.vsync_mode.mailbox"),
I18n.format("sodium.options.vsync_mode.off")}))
.setBinding((opts, value) -> {
opts.advanced.vsyncMode = value;
final GameSettings settings = Minecraft.getMinecraft().gameSettings;
settings.enableVsync = value.tearFree();
settings.saveOptions();
GLStateManager.setVSyncMode(value, settings.enableVsync);
}, opts -> opts.advanced.vsyncMode.resolve(Minecraft.getMinecraft().gameSettings.enableVsync))
.setImpact(OptionImpact.VARIES)
.build();
final OptionImpl<GameSettings, Integer> maxFramerate = OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.framerateLimit"))
.setTooltip(I18n.format("sodium.options.fps_limit.tooltip"))
.setControl(option -> new SliderControl(option, 5, 260, 5, ControlValueFormatter.fpsLimit()))
.setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate)
.build();
maxFramerate.iris$dynamicallyEnable(() -> !vsync.getValue().blocksOnVBlank());
return new FrameRateOptions(vsync, maxFramerate);
}
}
@@ -105,6 +105,7 @@ public class SodiumGameOptionPages {
groups.add(firstGroupBuilder.build());
int maxGuiScale = Math.max(3, Math.min(Minecraft.getMinecraft().displayWidth / 320, Minecraft.getMinecraft().displayHeight / 240));
final FrameRateOptions frameRate = FrameRateOptions.create(vanillaOpts, sodiumOpts);
groups.add(OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.guiScale"))
@@ -138,22 +139,8 @@ public class SodiumGameOptionPages {
}
}, (opts) -> opts.fullScreen)
.build())
.add(OptionImpl.createBuilder(boolean.class, vanillaOpts)
.setName(I18n.format("options.vsync"))
.setTooltip(I18n.format("sodium.options.v_sync.tooltip"))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> {
opts.enableVsync = value;
GLStateManager.setVSyncEnabled(opts.enableVsync);
}, opts -> opts.enableVsync)
.setImpact(OptionImpact.VARIES)
.build())
.add(OptionImpl.createBuilder(int.class, vanillaOpts)
.setName(I18n.format("options.framerateLimit"))
.setTooltip(I18n.format("sodium.options.fps_limit.tooltip"))
.setControl(option -> new SliderControl(option, 5, 260, 5, ControlValueFormatter.fpsLimit()))
.setBinding((opts, value) -> opts.limitFramerate = value, opts -> opts.limitFramerate)
.build())
.add(frameRate.vsync())
.add(frameRate.maxFramerate())
.build());
groups.add(OptionGroup.createBuilder()
@@ -521,9 +508,12 @@ public class SodiumGameOptionPages {
.add(OptionImpl.createBuilder(int.class, sodiumOpts)
.setName(I18n.format("sodium.options.cpu_render_ahead_limit.name"))
.setTooltip(I18n.format("sodium.options.cpu_render_ahead_limit.tooltip"))
.setControl(o -> new SliderControl(o, 0, 9, 1, ControlValueFormatter.quantity("sodium.options.cpu_render_ahead_limit.value")))
.setControl(o -> new SliderControl(o, GLStateManager.getMinRenderAhead(), GLStateManager.getMaxRenderAhead(), 1, ControlValueFormatter.quantity("sodium.options.cpu_render_ahead_limit.value")))
.setImpact(OptionImpact.MEDIUM)
.setBinding((opts, value) -> opts.performance.cpuRenderAheadLimit = value, opts -> opts.performance.cpuRenderAheadLimit)
.setBinding((opts, value) -> {
opts.performance.cpuRenderAheadLimit = value;
GLStateManager.applyRenderAheadLimit(value);
}, opts -> GLStateManager.renderAheadLimit(opts.performance.cpuRenderAheadLimit))
.setEnabled(GLStateManager.capabilities != null && GLStateManager.capabilities.OpenGL32)
.build())
@@ -6,6 +6,7 @@ import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.streaming.StreamingUploader;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import me.jellysquid.mods.sodium.client.gui.options.named.GraphicsQuality;
@@ -203,6 +204,7 @@ public class SodiumGameOptions {
public boolean ignoreDriverBlacklist = false;
public StreamingUploader.UploadStrategy streamingUploadStrategy = StreamingUploader.UploadStrategy.MAP_BUFFER_RANGE;
public boolean enableDeferredBatching = true;
public VSyncMode vsyncMode = VSyncMode.AUTO;
}
public static class PerformanceSettings {
@@ -27,6 +27,17 @@ public class CyclingControl<T extends Enum<T>> implements Control<T> {
this.names = names;
}
public CyclingControl(Option<T> option, Class<T> enumType, T[] allowedValues, String[] names) {
final T[] universe = enumType.getEnumConstants();
Validate.isTrue(universe.length == names.length, "Mismatch between universe length and names array length");
Validate.notEmpty(allowedValues, "There must be at least one allowed value");
this.option = option;
this.allowedValues = allowedValues;
this.names = names;
}
public CyclingControl(Option<T> option, Class<T> enumType, T[] allowedValues) {
final T[] universe = enumType.getEnumConstants();
@@ -27,21 +27,21 @@ public class SodiumControlElementFactory implements ControlElementFactory {
private static class CyclingControlElement<T extends Enum<T>> extends SodiumControlElement<T> {
private final T[] allowedValues;
private final String[] names;
private int currentIndex;
public CyclingControlElement(Option<T> option, Dim2i dim, T[] allowedValues, String[] names) {
super(option, dim);
this.allowedValues = allowedValues;
this.names = names;
this.currentIndex = 0;
}
for (int i = 0; i < allowedValues.length; i++) {
if (allowedValues[i] == option.getValue()) {
this.currentIndex = i;
break;
private int currentIndex() {
for (int i = 0; i < this.allowedValues.length; i++) {
if (this.allowedValues[i] == this.option.getValue()) {
return i;
}
}
return 0;
}
@Override
@@ -59,16 +59,14 @@ public class SodiumControlElementFactory implements ControlElementFactory {
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if(AngelicaConfig.enableReesesSodiumOptions) {
if (this.option.isAvailable() && this.dim.containsCursor(mouseX, mouseY) && (button == 0 || button == 1)) {
this.currentIndex = Math.floorMod(this.currentIndex + (button == 0 ? 1 : -1), this.allowedValues.length);
this.option.setValue(this.allowedValues[this.currentIndex]);
this.option.setValue(this.allowedValues[Math.floorMod(this.currentIndex() + (button == 0 ? 1 : -1), this.allowedValues.length)]);
this.playClickSound();
return true;
}
} else {
if (this.option.isAvailable() && button == 0 && this.dim.containsCursor(mouseX, mouseY)) {
this.currentIndex = (this.currentIndex + 1) % this.allowedValues.length;
this.option.setValue(this.allowedValues[this.currentIndex]);
this.option.setValue(this.allowedValues[(this.currentIndex() + 1) % this.allowedValues.length]);
this.playClickSound();
return true;
+5 -3
View File
@@ -4,12 +4,14 @@ import com.google.common.base.Throwables;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizons.angelica.Tags;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess;
import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.rendering.StateAwareTessellator;
import com.gtnewhorizons.angelica.rendering.celeritas.api.IrisShaderProviderHolder;
import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import cpw.mods.fml.client.registry.ClientRegistry;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.InputEvent;
@@ -309,9 +311,9 @@ public class Iris {
// Clear transformation caches - no longer needed after loading
TransformPatcher.clearCache();
ShaderTransformer.clearCache();
SpirvCompiler.clearCache();
GlslVulkanPreprocess.clearCache();
ShaderManager.clearPrewarmCache();
if (RenderSystem.isGLES() || BackendManager.RENDER_BACKEND.isSDLGPU()) SpirvCompiler.clearCache();
SDLGPUGate.clearShaderPrewarmCache();
} else {
// Still active (or in-flight), schedule another check.
final long remainingSeconds = Math.max(1, IDLE_TIMEOUT_SECONDS - idleSeconds + 1);
@@ -11,7 +11,6 @@ import org.embeddedt.embeddium.impl.gl.shader.uniform.GlUniformMatrix4f;
import org.joml.Matrix3f;
import org.joml.Matrix4fc;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
@@ -547,11 +547,11 @@ public class ShadowRenderer {
profiler.endStartSection("iris_shadow_build_geometry");
setupEntityShadowState(modelView, cameraX, cameraY, cameraZ);
if (playerReflectionCaptureEnabled) {
PlayerReflectionCapture.begin(player);
}
ModelPartBatcher.INSTANCE.begin(ModelPartBatcher.Mode.ENTITIES, true);
try {
if (playerReflectionCaptureEnabled) {
PlayerReflectionCapture.begin(player);
}
for (Entity entity : renderedEntitiesList) {
final long start = Tracy.ENABLED ? System.nanoTime() : 0L;
try {
@@ -629,7 +629,7 @@ public class ShadowRenderer {
// Saved RenderManager position for shadow pass
private double savedRenderPosX, savedRenderPosY, savedRenderPosZ;
/** Flushes shadow-pass batched model parts while GL_POLYGON_OFFSET_FILL is still enabled. */
/** Flushes deferred shadow-pass geometry while GL_POLYGON_OFFSET_FILL is still enabled. */
private static void flushShadowModelParts() {
if (Tracy.ENABLED) Tracy.beginZone(Z_SHADOW_MODEL_PARTS);
final boolean alphaEnabled = GLStateManager.getAlphaTest().isEnabled();
@@ -637,6 +637,7 @@ public class ShadowRenderer {
final float alphaRef = GLStateManager.getAlphaState().getReference();
try {
ModelPartBatcher.INSTANCE.flush();
PlayerReflectionCapture.flush();
} finally {
if (alphaEnabled) GLStateManager.enableAlphaTest();
else GLStateManager.disableAlphaTest();
@@ -712,14 +713,11 @@ public class ShadowRenderer {
if (playerReflectionCaptureEnabled) {
PlayerReflectionCapture.begin(player);
}
try {
RenderManager.instance.renderEntitySimple(player, tickDelta);
} finally {
PlayerReflectionCapture.end();
}
RenderManager.instance.renderEntitySimple(player, tickDelta);
shadowEntities++;
} finally {
flushShadowModelParts();
PlayerReflectionCapture.end();
teardownEntityShadowState();
}
@@ -24,8 +24,8 @@ sodium.options.clouds.name=Clouds
sodium.options.clouds.tooltip=Controls whether or not clouds will be visible.
sodium.options.gui_scale.tooltip=Sets the maximum scale factor to be used for the user interface. If 'auto' is used, then the largest scale factor will always be used.
sodium.options.fullscreen.tooltip=If enabled, the game will display in full-screen (if supported).
sodium.options.v_sync.tooltip=If enabled, the game's frame rate will be synchronized to the monitor's refresh rate, making for a generally smoother experience at the expense of overall input latency. This setting might reduce performance if your system is too slow.
sodium.options.fps_limit.tooltip=Limits the maximum FPS (frames per second). This can help reduce battery usage and general system load when multi-tasking. If V-Sync is enabled, this option will be ignored unless it is lower than your display's refresh rate.
sodium.options.v_sync.tooltip=Synchronizes frames to your monitor to stop tearing. §lOff§r shows each frame the moment it is ready: lowest latency, but the image can tear, and Max Framerate applies. §lOn§r waits for the monitor, which removes tearing and hands it the frame rate, so §lMax Framerate has no effect§r. §lMailbox§r removes tearing without waiting, keeping most of the latency of Off, and Max Framerate still applies. Mailbox needs the SDL-GPU backend on Vulkan or D3D12.
sodium.options.fps_limit.tooltip=Limits the maximum FPS (frames per second). This can help reduce battery usage and general system load when multi-tasking. §lHas no effect while VSync is On§r, because the monitor sets the frame rate instead; use Mailbox to combine a limit with tear-free output.
sodium.options.view_bobbing.tooltip=If enabled, the player's view will sway and bob when moving around. Players who experience motion sickness while playing can benefit from disabling this.
sodium.options.attack_indicator.tooltip=Controls where the Attack Indicator is displayed on screen.
sodium.options.autosave_indicator.tooltip=If enabled, an indicator will be shown when the game is saving the world to disk.
@@ -75,7 +75,7 @@ sodium.options.animate_only_visible_textures.tooltip=If enabled, only animated t
sodium.options.translucency_sorting.name=Translucency Sorting
sodium.options.translucency_sorting.tooltip=If enabled, translucent blocks will be sorted correctly from back to front to provide better rendering, especially when multiple translucent blocks are in view and layered. This is a bit of an expensive operation.
sodium.options.cpu_render_ahead_limit.name=CPU Render-Ahead Limit
sodium.options.cpu_render_ahead_limit.tooltip=Specifies the maximum number of frames the CPU can be waiting on the GPU to finish rendering. Very low or high values may create frame rate instability.
sodium.options.cpu_render_ahead_limit.tooltip=How many frames the CPU may run ahead of the GPU. Lower values cut input latency, higher values smooth out uneven frame times. Very low or high values may create frame rate instability. The SDL-GPU backend limits this to 3.
sodium.options.cpu_render_ahead_limit.value=%s frame(s)
sodium.options.allow_direct_memory_access.name=Allow Direct Memory Access
sodium.options.allow_direct_memory_access.tooltip=If enabled, some critical code paths will be allowed to use direct memory access for performance. This often greatly reduces CPU overhead for chunk and entity rendering, but can make it harder to diagnose some bugs and crashes. You should only disable this if you've been asked to or otherwise know what you're doing.
@@ -261,6 +261,10 @@ options.angelica.texteffects.drop_shadow.tooltip=Per-segment drop shadow via %s,
options.angelica.texteffects.ampersand_conversion=Ampersand Conversion
options.angelica.texteffects.ampersand_conversion.tooltip=Convert %s prefix codes to formatting codes at render time
sodium.options.vsync_mode.auto=Auto
sodium.options.vsync_mode.on=On
sodium.options.vsync_mode.mailbox=Mailbox
sodium.options.vsync_mode.off=Off
sodium.options.multidraw_mode.name=Multi-Draw Mode
sodium.options.multidraw_mode.tooltip=Controls how draw commands are dispatched for terrain rendering. §lDirect§r uses glMultiDrawElementsBaseVertex (default, GL 3.2+). §lIndirect§r uses GPU indirect rendering (GL 4.3+), may reduce CPU overhead on some hardware, and is required for GPU / Hi-Z culling. §lIndividual§r issues individual draw calls.
sodium.options.multidraw_mode.direct=Direct
@@ -6,6 +6,7 @@ import com.gtnewhorizons.angelica.glsm.ffp.ShaderManager;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.mixins.interfaces.IGameSettingsExt;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.GameSettings;
@@ -36,12 +37,6 @@ public abstract class MixinMinecraft {
@Shadow(remap = false)
private static int max_texture_size;
@Unique
private static long angelica$lastFrameTime = 0;
@Unique
private static long angelica$fpsLimitOverhead = 0;
@Unique
private final RenderAheadManager celeritas$renderAheadManager = new RenderAheadManager();
@@ -66,48 +61,23 @@ public abstract class MixinMinecraft {
GLStateManager.glEnable(GL11.GL_LIGHTING);
}
@Inject(
method = "func_147120_f",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;update()V", remap = false)
)
@Inject(method = "func_147120_f", at = @At("RETURN"))
private void angelica$limitFPS(CallbackInfo ci) {
if (AngelicaMod.proxy == null) return;
if (isFramerateLimitBelowMax() && !gameSettings.enableVsync) {
final long time = System.nanoTime();
final long lastWorkTime = time - angelica$lastFrameTime;
final long targetNanos = (long) (1.0 / getLimitFramerate() * 1_000_000_000L);
// Account for overhead, so the average FPS remains stable.
final long sleepNanos = targetNanos - lastWorkTime - angelica$fpsLimitOverhead;
if (sleepNanos > 0) {
try {
Thread.sleep(sleepNanos / 1_000_000, (int) sleepNanos % 1_000_000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// Record overhead, capping it to prevent outsized spikes from affecting framerates for too long.
// In testing, spikes were followed by 1-frame dips, which I think is acceptable.
long overhead = System.nanoTime() - time - sleepNanos;
if (overhead < 0 || overhead > targetNanos / 2) overhead = 0;
angelica$fpsLimitOverhead = overhead;
}
final long time = System.nanoTime();
AngelicaMod.proxy.putFrametime(time - angelica$lastFrameTime);
angelica$lastFrameTime = time;
final int capHz = isFramerateLimitBelowMax() ? getLimitFramerate() : 0;
AngelicaMod.proxy.putFrametime(FramePacer.pace(FramePacer.updateCeiling(capHz)));
}
@Redirect(method = {"startGame", "toggleFullscreen"}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setVSyncEnabled(Z)V", remap = false))
private void angelica$redirectVSync(boolean sync) {
GLStateManager.setVSyncEnabled(sync);
ClientProxy.applyVSyncMode(sync);
}
@Inject(method = "startGame", at = @At("RETURN"))
private void angelica$markSplashCompleteOnStartGame(CallbackInfo ci) {
GLStateManager.markSplashComplete("startGame");
GLStateManager.applyRenderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit);
}
@WrapWithCondition(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;sync(I)V", remap = false))
@@ -122,7 +92,7 @@ public abstract class MixinMinecraft {
@Inject(method = "runTick", at = @At("HEAD"))
private void celeritas$renderAheadStartFrame(CallbackInfo ci) {
final int limit = ClientProxy.options().performance.cpuRenderAheadLimit;
final int limit = GLStateManager.renderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit);
if (limit > 0) {
celeritas$renderAheadManager.startFrame(limit);
}
@@ -130,7 +100,7 @@ public abstract class MixinMinecraft {
@Inject(method = "runTick", at = @At("RETURN"))
private void celeritas$renderAheadEndFrame(CallbackInfo ci) {
if (ClientProxy.options().performance.cpuRenderAheadLimit > 0) {
if (GLStateManager.renderAheadLimit(ClientProxy.options().performance.cpuRenderAheadLimit) > 0) {
celeritas$renderAheadManager.endFrame();
}
}
@@ -1,7 +1,7 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.debug;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.GameSettings;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
@@ -15,19 +15,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public class MixinMinecraft_FPSCap {
@Shadow public String debug;
@Shadow public GameSettings gameSettings;
@Inject(method = "runGameLoop", at = @At(value = "FIELD", target = "Lnet/minecraft/client/Minecraft;debug:Ljava/lang/String;", shift = At.Shift.AFTER, ordinal = 0))
private void angelica$appendFPSCapInfo(CallbackInfo ci) {
final int limit = this.gameSettings.limitFramerate;
String indicator = null;
if (this.gameSettings.enableVsync) {
indicator = " [vsync]";
} else if (limit < 260) {
indicator = " [cap " + limit + "]";
}
if (indicator != null) {
this.debug = this.debug.replace(" fps,", " fps" + indicator + ",");
}
final String indicator = FramePacer.debugIndicator();
if (indicator == null) return;
this.debug = this.debug.replace(" fps,", " fps" + indicator + ",");
}
}
@@ -1,19 +1,22 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.entity;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility;
import com.gtnewhorizons.angelica.rendering.tesr.ModelPartBatcher;
import net.minecraft.client.model.ModelRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ModelRenderer.class)
@Mixin(value = ModelRenderer.class, priority = 1500)
public abstract class MixinModelRenderer {
@Redirect(method = {"render", "renderWithRotation"}, at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallList(I)V", remap = false))
private void angelica$batchPart(int list, float scale) {
if (!ModelPartBatcher.partDraw((ModelRenderer) (Object) this, scale, null)) {
final long drawsBefore = GLStateManager.drawCalls;
GLStateManager.glCallList(list);
BatchEligibility.onPartFallback(drawsBefore, GLStateManager.drawCalls);
}
}
}
@@ -0,0 +1,31 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.entity;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.IBatchEligibility;
import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility;
import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(RenderManager.class)
public class MixinRenderManager_BatchEligibility {
@Redirect(method = "func_147939_a", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/Render;doRender(Lnet/minecraft/entity/Entity;DDDFF)V"))
private void angelica$observeDoRender(Render render, Entity entity, double x, double y, double z, float yaw, float partialTicks) {
final IBatchEligibility holder = (IBatchEligibility) render;
final byte state = holder.angelica$batchState();
BatchEligibility.begin(state, GLStateManager.drawCalls);
try {
render.doRender(entity, x, y, z, yaw, partialTicks);
} finally {
final byte next = BatchEligibility.end(state, GLStateManager.drawCalls);
if (next != state) {
holder.angelica$setBatchState(next);
BatchEligibility.onStateChange(render, next);
}
}
}
}
@@ -0,0 +1,23 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.entity;
import com.gtnewhorizons.angelica.mixins.interfaces.IBatchEligibility;
import net.minecraft.client.renderer.entity.Render;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(Render.class)
public abstract class MixinRender_BatchEligibility implements IBatchEligibility {
@Unique
private byte angelica$batchState;
@Override
public byte angelica$batchState() {
return angelica$batchState;
}
@Override
public void angelica$setBatchState(byte state) {
angelica$batchState = state;
}
}
@@ -8,11 +8,12 @@ import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer;
import com.gtnewhorizons.angelica.compat.DriverCompatabilityCheck;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.render.SelectionBoxRenderer;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUDisplayBridge;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import org.embeddedt.embeddium.impl.gl.debug.GLDebug;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.OpenGlHelper;
@@ -27,7 +28,7 @@ public class MixinInitGLStateManager {
@Inject(method = "initializeTextures", at = @At("HEAD"))
private static void angelica$installSDLDrawable(CallbackInfo ci) {
SDLGPUDisplayBridge.ensureDrawableInstalled();
SDLGPUGate.ensureDrawableInstalled();
}
@Inject(method = "initializeTextures", at = @At("RETURN"))
@@ -1,5 +1,9 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.tesr;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.mixins.interfaces.IBatchEligibility;
import com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility;
import com.gtnewhorizons.angelica.rendering.tesr.TesrProviderDispatch;
import net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
@@ -13,8 +17,23 @@ public class MixinTileEntityRendererDispatcher {
@Redirect(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/tileentity/TileEntitySpecialRenderer;renderTileEntityAt(Lnet/minecraft/tileentity/TileEntity;DDDF)V"))
private void angelica$dispatchTesrProvider(TileEntitySpecialRenderer renderer, TileEntity te, double x, double y, double z, float partialTicks) {
if (!TesrProviderDispatch.tryRender(renderer, te, x, y, z)) {
if (AngelicaConfig.enableTESRProviderDispatch && TesrProviderDispatch.tryRender(renderer, te, x, y, z)) return;
if (!AngelicaConfig.enableEntityBatching) {
renderer.renderTileEntityAt(te, x, y, z, partialTicks);
return;
}
final IBatchEligibility holder = (IBatchEligibility) renderer;
final byte state = holder.angelica$batchState();
BatchEligibility.begin(state, GLStateManager.drawCalls);
try {
renderer.renderTileEntityAt(te, x, y, z, partialTicks);
} finally {
final byte next = BatchEligibility.end(state, GLStateManager.drawCalls);
if (next != state) {
holder.angelica$setBatchState(next);
BatchEligibility.onStateChange(renderer, next);
}
}
}
}
@@ -0,0 +1,23 @@
package com.gtnewhorizons.angelica.mixins.early.angelica.tesr;
import com.gtnewhorizons.angelica.mixins.interfaces.IBatchEligibility;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(TileEntitySpecialRenderer.class)
public abstract class MixinTileEntitySpecialRenderer_BatchEligibility implements IBatchEligibility {
@Unique
private byte angelica$batchState;
@Override
public byte angelica$batchState() {
return angelica$batchState;
}
@Override
public void angelica$setBatchState(byte state) {
angelica$batchState = state;
}
}
@@ -1,8 +1,6 @@
package com.gtnewhorizons.angelica.mixins.early.sdlgpu;
import com.gtnewhorizons.angelica.AngelicaMod;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import cpw.mods.fml.common.FMLCommonHandler;
import net.minecraftforge.client.ForgeHooksClient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -26,20 +24,22 @@ public abstract class MixinForgeHooksClient_SDLGPUDisplay {
@Inject(method = "createDisplay", at = @At("HEAD"), cancellable = true)
private static void angelicaSdl$createDisplay(CallbackInfo ci) throws LWJGLException {
if (!SDLGPUGate.isDeviceReady()) {
LOGGER.warn("No usable SDL GPU device; creating an OpenGL window instead");
return;
}
ImageIO.setUseCache(false);
stencilBits = 8;
final PixelFormat format = new PixelFormat().withDepthBits(24).withStencilBits(stencilBits);
try {
SDLGPUGate.createSDLGPUDisplay(format, null, AngelicaMod.lwjglDebug);
if (SDLGPUGate.isFatalInit()) {
FMLCommonHandler.instance().exitJava(1, false);
}
if (SDLGPUGate.createSDLGPUDisplay(format, null)) {
LOGGER.info("Created SDL GPU window");
ci.cancel();
} catch (RuntimeException e) {
LOGGER.error("FATAL: Failed to create SDL GPU window: {}", e.getMessage());
throw new LWJGLException("Failed to create SDL GPU window", e);
return;
}
LOGGER.warn("SDL GPU could not take the window; falling back to OpenGL");
SDLGPUGate.fallBackToGL();
}
}
@@ -0,0 +1,19 @@
package com.gtnewhorizons.angelica.mixins.early.sdlgpu;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import java.nio.ByteBuffer;
@Mixin(Minecraft.class)
public abstract class MixinMinecraft_SDLGPUIcons {
@ModifyArg(method = "startGame", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/Display;setIcon([Ljava/nio/ByteBuffer;)I", remap = false), index = 0)
private ByteBuffer[] angelicaSdl$rememberIcons(ByteBuffer[] icons) {
SDLGPUGate.rememberIcons(icons);
return icons;
}
}
@@ -1,13 +1,15 @@
package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.gtnewhorizons.angelica.mixins.interfaces.ModelBoxQuads;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.TexturedQuad;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ModelBox.class)
public interface AccessorModelBox {
public interface AccessorModelBox extends ModelBoxQuads {
@Override
@Accessor("quadList")
TexturedQuad[] angelica$getQuadList();
}
@@ -3,29 +3,18 @@ package com.gtnewhorizons.angelica.mixins.early.shaders;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.rendering.PlayerReflectionCapture;
import net.minecraft.client.model.ModelBiped;
import net.minecraft.client.model.ModelBox;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.model.PositionTextureVertex;
import net.minecraft.client.model.TexturedQuad;
import net.minecraft.entity.Entity;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.Unique;
import java.util.Arrays;
@Mixin(ModelBiped.class)
public abstract class MixinModelBiped {
@Unique private static final Tracy.ZoneId Z_PLAYER_REFLECTION_CAPTURE = Tracy.zoneId("playerReflectionCapture", Tracy.COLOR_IRIS);
@Unique private static final Matrix4f angelica$mat = new Matrix4f();
@Unique private static final Vector3f angelica$vec = new Vector3f();
@Inject(method = "render(Lnet/minecraft/entity/Entity;FFFFFF)V", at = @At("RETURN"))
private void angelica$capturePlayerReflection(Entity entity, float limbSwing, float limbSwingAmount,
float ageInTicks, float netHeadYaw, float headPitch, float scale, CallbackInfo ci) {
@@ -34,65 +23,11 @@ public abstract class MixinModelBiped {
if ((Object) this != PlayerReflectionCapture.getTarget()) return;
if (entity != PlayerReflectionCapture.getTargetEntity()) return;
final ModelBiped model = (ModelBiped) (Object) this;
final float[] data = PlayerReflectionCapture.vertexScratch();
if (Tracy.ENABLED) Tracy.beginZone(Z_PLAYER_REFLECTION_CAPTURE);
try {
// Six 48-vertex blocks = body parts, each base box + overlay box
int o = 0;
o = angelica$emitBox(model.bipedHead, data, o, scale);
o = angelica$emitBox(model.bipedHeadwear, data, o, scale);
o = angelica$emitBox(model.bipedRightArm, data, o, scale);
o = angelica$emitBox(model.bipedRightArm, data, o, scale);
o = angelica$emitBox(model.bipedLeftLeg, data, o, scale);
o = angelica$emitBox(model.bipedLeftLeg, data, o, scale);
o = angelica$emitBox(model.bipedLeftArm, data, o, scale);
o = angelica$emitBox(model.bipedLeftArm, data, o, scale);
o = angelica$emitBox(model.bipedRightLeg, data, o, scale);
o = angelica$emitBox(model.bipedRightLeg, data, o, scale);
o = angelica$emitBox(model.bipedBody, data, o, scale);
angelica$emitBox(model.bipedBody, data, o, scale);
PlayerReflectionCapture.drawPlayerCapture(data);
PlayerReflectionCapture.emitAndSubmit((ModelBiped) (Object) this, scale);
} finally {
if (Tracy.ENABLED) Tracy.endZone();
}
}
private static int angelica$emitBox(ModelRenderer part, float[] out, int o, float scale) {
if (part == null || part.cubeList == null || part.cubeList.isEmpty()) {
final int end = o + 24 * PlayerReflectionCapture.FLOATS_PER_VERTEX;
Arrays.fill(out, o, end, 0.0f);
return end;
}
final Matrix4f m = angelica$mat.identity();
m.translate(part.offsetX, part.offsetY, part.offsetZ);
m.translate(part.rotationPointX * scale, part.rotationPointY * scale, part.rotationPointZ * scale);
if (part.rotateAngleZ != 0.0f) m.rotateZ(part.rotateAngleZ);
if (part.rotateAngleY != 0.0f) m.rotateY(part.rotateAngleY);
if (part.rotateAngleX != 0.0f) m.rotateX(part.rotateAngleX);
final ModelBox box = part.cubeList.getFirst();
final TexturedQuad[] quads = ((AccessorModelBox) (Object) box).angelica$getQuadList();
final Vector3f v = angelica$vec;
for (int q = 0; q < 6; q++) {
final PositionTextureVertex[] corners = quads[q].vertexPositions;
for (int c = 0; c < 4; c++) {
final PositionTextureVertex corner = corners[c];
v.set((float) corner.vector3D.xCoord * scale,
(float) corner.vector3D.yCoord * scale,
(float) corner.vector3D.zCoord * scale);
m.transformPosition(v);
out[o++] = v.x;
out[o++] = v.y;
out[o++] = v.z;
out[o++] = corner.texturePositionX;
out[o++] = corner.texturePositionY * 0.5f;
}
}
return o;
}
}
@@ -84,11 +84,11 @@ class Lwjgl3AwareBoundaryTest {
for (JavaMethod method : renderBackend.getMethods()) {
final List<JavaClass> types = new ArrayList<>(method.getRawParameterTypes());
types.add(method.getRawReturnType());
if (types.stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type)) {
if (types.stream().anyMatch(Lwjgl3AwareBoundaryTest::isRemapUnsafeType)) {
violations.add(method.getFullName());
}
}
assertTrue(violations.isEmpty(), "RenderBackend methods with an org.lwjgl type in their signature:\n" + String.join("\n", violations));
assertTrue(violations.isEmpty(), "RenderBackend methods with an org.lwjgl/org.lwjglx/lwjgl3ify type in their signature:\n" + String.join("\n", violations));
}
private static boolean isAware(JavaClass c) {
@@ -103,6 +103,11 @@ class Lwjgl3AwareBoundaryTest {
return type.getName().startsWith("org.lwjgl.");
}
private static boolean isRemapUnsafeType(JavaClass type) {
final String name = type.getName();
return isLwjgl3Type(type) || name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.");
}
private static boolean mentionsLwjgl3Type(AccessTarget target) {
return descriptorTypes(target).stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type);
}
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more