More fixes (#1995)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-08-08 23:53:06 -07:00
1 parent 4b21bc77b6
commit 166116d692
50 files changed
+1460 -195

No files matched your search

+7 -2
View File
@@ -1,3 +1,4 @@
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 +32,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")
}
}
@@ -252,3 +253,7 @@ 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") }
}
@@ -16,7 +16,8 @@ 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");
@@ -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);
@@ -528,7 +528,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");
@@ -1183,12 +1186,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 +1201,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) {
@@ -2258,7 +2326,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 +2351,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 +2376,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 +2401,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 +2426,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 +2469,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 +2494,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 +2544,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) {
@@ -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());
}
}
@@ -1371,6 +1371,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();
@@ -75,7 +73,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 +82,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 +392,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);
@@ -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]);
}
}
@@ -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
@@ -1504,6 +1504,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;
@@ -96,7 +96,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 +105,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 +212,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 +225,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 +233,6 @@ public class SDLGPURenderBackend extends RenderBackend {
drainDeferredPersistentRegions(s());
debugLabels.popAutoDebugGroup(s());
});
frameManager.setBeforePendingUploadSubmitCallback(textureOps::drainPendingMipGen);
frameManager.setAfterPresentCallback(() -> {
frameManager.requestFlushOnAllRegisteredFrames();
resourceManager.drainPendingFreeShadows();
@@ -434,7 +432,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() {
@@ -630,7 +628,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();
@@ -3521,7 +3519,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 +3538,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 +3548,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);
}
@@ -1,7 +1,6 @@
package com.gtnewhorizons.angelica.sdlgpu.device;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPUGate;
import com.gtnewhorizons.angelica.sdlgpu.util.DebugMessageRelay;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -12,12 +11,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;
@@ -30,7 +31,10 @@ public final class Device {
public static final int FRAMES_IN_FLIGHT = 3;
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;
@@ -39,12 +43,46 @@ public final class Device {
private int presentMode = SDL_GPU_PRESENTMODE_VSYNC;
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 +94,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 +113,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,6 +145,7 @@ public final class Device {
if (!SDL_ClaimWindowForGPUDevice(device, window)) {
throw new RuntimeException("Failed to claim window for SDL GPU device: " + SDLError.SDL_GetError());
}
claimed = true;
final int n = FRAMES_IN_FLIGHT;
if (!SDL_SetGPUAllowedFramesInFlight(device, n)) {
@@ -104,6 +158,53 @@ public final class Device {
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 +247,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");
}
}
@@ -218,6 +314,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);
}
}
@@ -165,13 +165,20 @@ 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;
public FrameManager(Device device) {
this.device = device;
@@ -409,10 +416,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 +471,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();
@@ -530,12 +534,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 +621,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 +630,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;
@@ -189,6 +189,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 +235,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();
@@ -1311,7 +1325,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 +1427,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 +1435,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 +1462,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++;
@@ -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;
@@ -215,32 +216,41 @@ public final class TransferThread {
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();
}
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();
@@ -355,7 +365,7 @@ 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) {
@@ -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,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"));
}
}
@@ -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,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);
});
}
};
@@ -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,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;
+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;
@@ -1,6 +1,7 @@
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;
@@ -13,7 +14,9 @@ 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;
}
}
@@ -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);
}
@@ -1,16 +1,22 @@
package com.gtnewhorizons.angelica.arch;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaAccess;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.ReferencedClassObject;
import com.tngtech.archunit.core.domain.JavaStaticInitializer;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.lang.ArchRule;
import org.junit.jupiter.api.Test;
import java.util.Set;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Enforces the :sdl-gpu module boundary
@@ -19,35 +25,73 @@ class SDLGPUEncapsulationTest {
private static final String SDLGPU_ROOT = "com.gtnewhorizons.angelica.sdlgpu";
private static final Set<String> PUBLIC_ENTRIES = Set.of(
SDLGPU_ROOT + ".SDLGPURenderBackend", // ServiceLoader
SDLGPU_ROOT + ".SDLGPULWJGLService", // ServiceLoader
SDLGPU_ROOT + ".SDLGPUGate", // Mixins, MixinInitGLStateManager
SDLGPU_ROOT + ".SDLGPUDisplayBridge", // MixinForgeHooksClient_SDLGPUDisplay
SDLGPU_ROOT + ".device.SDLDrawable", // MixinForgeHooksClient_SDLGPUDisplay
SDLGPU_ROOT + ".shader.ShaderManager" // Iris, IrisGLSMBridge
);
private static final String GATE = SDLGPU_ROOT + ".SDLGPUGate";
private static final DescribedPredicate<JavaClass> SDLGPU_INTERNAL =
new DescribedPredicate<>("sdlgpu internals (anything under " + SDLGPU_ROOT + " not on the allow-list)") {
private static final String LWJGL3_AWARE = "me.eigenraven.lwjgl3ify.api.Lwjgl3Aware";
private static final DescribedPredicate<JavaClass> SDLGPU_BEHIND_THE_GATE =
new DescribedPredicate<>("anything under " + SDLGPU_ROOT + " other than SDLGPUGate") {
@Override
public boolean test(JavaClass c) {
final String name = c.getName();
return name.startsWith(SDLGPU_ROOT + ".") && !PUBLIC_ENTRIES.contains(name);
return name.startsWith(SDLGPU_ROOT + ".") && !GATE.equals(name);
}
};
private static final DescribedPredicate<JavaClass> LWJGL3_ONLY_TYPE =
new DescribedPredicate<>("types that only exist under lwjgl3ify/LWJGL3") {
@Override
public boolean test(JavaClass c) {
final String name = c.getName();
if (LWJGL3_AWARE.equals(name)) return false;
return name.startsWith("org.lwjglx.") || name.startsWith("me.eigenraven.lwjgl3ify.") || name.startsWith("org.lwjgl.sdl.");
}
};
private static final JavaClasses CLASSES = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPaths("build/classes/java/main", "build/classes/java/mixin");
.importPaths("build/classes/java/main", "build/classes/java/mixin", "glsm/build/classes/java/main");
private static final ArchRule NO_SDLGPU_INTERNALS_FROM_OUTSIDE = noClasses()
private static final JavaClasses SDLGPU_CLASSES = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPaths("sdl-gpu/build/classes/java/main");
private static final ArchRule SDLGPU_REACHED_ONLY_THROUGH_THE_GATE = noClasses()
.that().resideOutsideOfPackage(SDLGPU_ROOT + "..")
.should().dependOnClassesThat(SDLGPU_INTERNAL)
.because("only the allow-listed classes may be referenced by name from outside :sdl-gpu");
.should().dependOnClassesThat(SDLGPU_BEHIND_THE_GATE)
.because("SDLGPUGate is the only :sdl-gpu class safe to load without LWJGL3");
private static final ArchRule NO_LWJGL3_ONLY_TYPES = noClasses()
.that().resideOutsideOfPackage(SDLGPU_ROOT + "..")
.should().dependOnClassesThat(LWJGL3_ONLY_TYPE)
.because("main, mixin and glsm all load on an LWJGL2 launch, where these types are absent");
@Test
void noSdlGpuInternalsFromOutside() {
NO_SDLGPU_INTERNALS_FROM_OUTSIDE.check(CLASSES);
void sdlGpuIsReachedOnlyThroughTheGate() {
SDLGPU_REACHED_ONLY_THROUGH_THE_GATE.check(CLASSES);
}
@Test
void noLwjgl3OnlyTypesOutsideSdlGpu() {
NO_LWJGL3_ONLY_TYPES.check(CLASSES);
}
@Test
void gateStaticInitIsLwjgl3Free() {
final Optional<JavaStaticInitializer> clinit = SDLGPU_CLASSES.get(GATE).getStaticInitializer();
assertTrue(clinit.isPresent(), GATE + " has no static initializer to check; the importer path is likely wrong");
final List<String> violations = new ArrayList<>();
for (JavaAccess<?> access : clinit.get().getAccessesFromSelf()) {
if (isLwjgl3(access.getTargetOwner())) violations.add(" " + access.getTarget().getFullName());
}
for (ReferencedClassObject ref : clinit.get().getReferencedClassObjects()) {
if (isLwjgl3(ref.getValue())) violations.add(" " + ref.getValue().getName() + ".class");
}
assertTrue(violations.isEmpty(), GATE + " static init resolves LWJGL3 types:\n" + String.join("\n", violations));
}
private static boolean isLwjgl3(JavaClass c) {
return LWJGL3_ONLY_TYPE.test(c) || c.getName().startsWith("org.lwjgl.");
}
}
@@ -0,0 +1,48 @@
package com.gtnewhorizons.angelica.client.font;
import org.joml.Matrix4f;
import org.joml.Matrix4fStack;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class FontBatchMatrixTest {
private static Matrix4f overlayOrtho() {
return new Matrix4f().ortho(0, 854, 480, 0, 1000, 3000).translate(0, 0, -2000);
}
@Test
void anUnchangedMatrixDoesNotSegment() {
final Matrix4f cached = overlayOrtho();
assertTrue(cached.equals(new Matrix4f(cached), 0.0f));
}
@Test
void aScaleSegments() {
final Matrix4f cached = overlayOrtho();
assertFalse(cached.equals(new Matrix4f(cached).scale(0.5f), 0.0f));
}
@Test
void aDepthOnlyTranslateSegments() {
final Matrix4f cached = overlayOrtho();
assertFalse(cached.equals(new Matrix4f(cached).translate(0, 0, 50), 0.0f));
}
@Test
void aStackIsComparedByValue() {
final Matrix4f cached = overlayOrtho();
final Matrix4fStack live = new Matrix4fStack(4);
live.set(cached);
live.pushMatrix();
assertTrue(cached.equals(live, 0.0f));
live.scale(0.5f);
assertFalse(cached.equals(live, 0.0f));
live.popMatrix();
assertTrue(cached.equals(live, 0.0f));
}
}
@@ -0,0 +1,108 @@
package com.gtnewhorizons.angelica.rendering.tesr;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility.DENIED;
import static com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility.SAFE;
import static com.gtnewhorizons.angelica.rendering.tesr.BatchEligibility.UNKNOWN;
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 BatchEligibilityTest {
private long drawCalls;
@BeforeEach
void reset() {
drawCalls = 0L;
}
private void draw(int count) {
drawCalls += count;
}
private void unbatchedPart(int draws) {
final long before = drawCalls;
draw(draws);
BatchEligibility.onPartFallback(before, drawCalls);
}
@Test
void aRendererDrawingOnlyModelPartsIsPromoted() {
assertFalse(BatchEligibility.begin(UNKNOWN, drawCalls), "unproven renderers run unbatched");
unbatchedPart(1);
unbatchedPart(1);
assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls));
}
@Test
void aRendererMixingItsOwnDrawsIsDenied() {
BatchEligibility.begin(UNKNOWN, drawCalls);
unbatchedPart(1);
draw(1);
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls));
}
@Test
void aPromotedRendererIsDemotedWhenItStartsMixing() {
assertTrue(BatchEligibility.begin(SAFE, drawCalls), "proven renderers batch");
BatchEligibility.onPartQueued();
draw(1);
assertEquals(DENIED, BatchEligibility.end(SAFE, drawCalls));
}
@Test
void aBatchedPartFallingBackCountsAsMixing() {
BatchEligibility.begin(SAFE, drawCalls);
BatchEligibility.onPartQueued();
unbatchedPart(1);
assertEquals(DENIED, BatchEligibility.end(SAFE, drawCalls));
}
@Test
void aDeniedRendererIsNeverPromoted() {
assertFalse(BatchEligibility.begin(DENIED, drawCalls));
unbatchedPart(1);
assertEquals(DENIED, BatchEligibility.end(DENIED, drawCalls));
}
@Test
void aDispatchWithoutPartsProvesNothing() {
BatchEligibility.begin(UNKNOWN, drawCalls);
draw(3);
assertEquals(UNKNOWN, BatchEligibility.end(UNKNOWN, drawCalls));
}
@Test
void aNestedDispatchDoesNotObserveSeparately() {
BatchEligibility.begin(UNKNOWN, drawCalls);
unbatchedPart(1);
BatchEligibility.begin(UNKNOWN, drawCalls);
draw(1);
assertEquals(UNKNOWN, BatchEligibility.end(UNKNOWN, drawCalls), "the inner dispatch leaves the verdict to the outer one");
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls), "the nested draw still counts against the outer renderer");
}
@Test
void statePersistsNothingBetweenDispatches() {
BatchEligibility.begin(UNKNOWN, drawCalls);
unbatchedPart(1);
draw(1);
assertEquals(DENIED, BatchEligibility.end(UNKNOWN, drawCalls));
BatchEligibility.begin(UNKNOWN, drawCalls);
unbatchedPart(1);
assertEquals(SAFE, BatchEligibility.end(UNKNOWN, drawCalls), "the previous dispatch's foreign draws must not leak");
}
@Test
void batchingIsDisallowedOutsideADispatch() {
BatchEligibility.begin(SAFE, drawCalls);
BatchEligibility.end(SAFE, drawCalls);
assertFalse(BatchEligibility.batchingAllowed());
}
}