Add Tracy frame images (#2326)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-11-01 15:09:13 -08:00
1 parent 5da38c9180
commit e46eb46343
32 files changed
+1092 -146

No files matched your search

@@ -45,6 +45,8 @@ public final class SystemProperties {
public static final Boolean TRACY_FINE_ZONES_OVERRIDE = parseBooleanOverride("angelica.tracy.fineZones");
public static final String TRACY_DIR = System.getProperty("angelica.tracy.dir", "angelica" + File.separator + "natives" + File.separator + "tracy");
public static final Integer TRACY_MAX_SRC_LOCS_OVERRIDE = Integer.getInteger("angelica.tracy.maxSrcLocs");
public static final Boolean TRACY_FRAME_IMAGES_OVERRIDE = parseBooleanOverride("angelica.tracy.frameImages");
public static final Integer TRACY_FRAME_IMAGE_RATE_OVERRIDE = Integer.getInteger("angelica.tracy.frameImageRate");
// Profiling
public static final String PROFILE_OPTS = System.getProperty("angelica.profile.opts", "event=wall,interval=5ms,alloc=512k,lock=10ms");
@@ -25,11 +25,13 @@ public final class CaptureGate {
public static final boolean FORCED = SystemProperties.DEBUG_MARKERS || SystemProperties.LWJGL_DEBUG;
public static final boolean TOOL_ATTACHED = detectCaptureTool();
public static boolean markersThisFrame = true;
public static boolean tracyConnected;
private CaptureGate() {}
public static void refresh() {
markersThisFrame = gateValue(FORCED, TOOL_ATTACHED, Tracy.isConnected());
tracyConnected = Tracy.isConnected();
markersThisFrame = gateValue(FORCED, TOOL_ATTACHED, tracyConnected);
}
static boolean gateValue(boolean forced, boolean toolAttached, boolean tracyConnected) {
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.DisplayListManager.RecordMode;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.stacks.IntegerStateStack;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;
@@ -326,6 +327,11 @@ public final class GLSMFramebuffer {
RENDER_BACKEND.readPixels(x, y, width, height, format, GLSMTextures.remapPixelTypeForGLES(format, type), pixels);
}
public static ScreenCapture createScreenCapture(int width, int height) {
rejectExitCapture();
return RENDER_BACKEND.createScreenCapture(width, height);
}
public static int getDrawFramebufferGeneration() {
return ctx().drawFramebufferGeneration;
}
@@ -1211,6 +1211,14 @@ public final class GLSMTextures {
public static void restorePixelUnpackState(PixelStoreState applied) {
PixelStoreState.applyDiff(applied, ctx().pixelUnpackState, false);
}
public static void forcePixelPackState(PixelStoreState target) {
PixelStoreState.applyDiff(ctx().pixelPackState, target, true);
}
public static void restorePixelPackState(PixelStoreState applied) {
PixelStoreState.applyDiff(applied, ctx().pixelPackState, true);
}
public static boolean glIsTexture(int texture) { rejectExitCapture(); return !GLSMTextureLifetime.isDeferredDelete(texture) && RENDER_BACKEND.isTexture(texture); }
public static boolean glIsSampler(int sampler) { rejectExitCapture(); return RENDER_BACKEND.isSampler(sampler); }
@@ -0,0 +1,106 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
import com.gtnewhorizons.angelica.glsm.states.PixelStoreState;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
public final class GLScreenCapture extends ScreenCapture {
private final int[] textures = new int[RING];
private final int[] framebuffers = new int[RING];
private final int[] buffers = new int[RING];
public GLScreenCapture(int width, int height) {
super(width, height);
}
@Override
protected boolean start(int slot, int srcFramebuffer, int srcWidth, int srcHeight) {
final int width = width();
final int height = height();
final int previousRead = GLSMFramebuffer.getReadFramebuffer();
final int previousDraw = GLSMFramebuffer.getDrawFramebuffer();
final int previousPack = GLStateManager.ctx().boundPixelPackBuffer;
final boolean scissor = GLSMRasterizer.getScissorTest().isEnabled();
BatchStateGuard.suspend();
try {
if (framebuffers[slot] == 0) create(slot);
if (scissor) GLSMEnable.glDisable(GL11.GL_SCISSOR_TEST);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, srcFramebuffer);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, framebuffers[slot]);
GLSMFramebuffer.glBlitFramebuffer(0, 0, srcWidth, srcHeight, 0, 0, width, height, GL11.GL_COLOR_BUFFER_BIT, GL11.GL_LINEAR);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, framebuffers[slot]);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
GLSMTextures.forcePixelPackState(PixelStoreState.DEFAULT);
GLSMFramebuffer.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, 0L);
return true;
} finally {
GLSMTextures.restorePixelPackState(PixelStoreState.DEFAULT);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, previousPack);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, previousRead);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, previousDraw);
if (scissor) GLSMEnable.glEnable(GL11.GL_SCISSOR_TEST);
BatchStateGuard.resume();
}
}
private void create(int slot) {
final int previousTexture = GLSMTextures.getBoundTextureForServerState();
final int texture;
try {
texture = GLSMTextures.glGenTextures();
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
GLSMTextures.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
GLSMTextures.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, width(), height(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
} finally {
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, previousTexture);
}
textures[slot] = texture;
framebuffers[slot] = GLSMFramebuffer.glGenFramebuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, framebuffers[slot]);
GLSMFramebuffer.glFramebufferTexture2D(GL30.GL_DRAW_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 0);
buffers[slot] = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
GLSMBuffers.glBufferData(GL21.GL_PIXEL_PACK_BUFFER, (long) width() * height() * 4, GL15.GL_STREAM_READ);
}
@Override
protected long map(int slot) {
final int previousPack = GLStateManager.ctx().boundPixelPackBuffer;
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
try {
return GLSMBuffers.glMapBufferRangeAddress(GL21.GL_PIXEL_PACK_BUFFER, 0L, (long) width() * height() * 4, GL30.GL_MAP_READ_BIT);
} finally {
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, previousPack);
}
}
@Override
protected void unmap(int slot) {
final int previousPack = GLStateManager.ctx().boundPixelPackBuffer;
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, buffers[slot]);
GLSMBuffers.glUnmapBuffer(GL21.GL_PIXEL_PACK_BUFFER);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, previousPack);
}
@Override
protected void deleteSlot(int slot) {
if (buffers[slot] != 0) GLSMBuffers.glDeleteBuffers(buffers[slot]);
if (framebuffers[slot] != 0) GLSMFramebuffer.glDeleteFramebuffers(framebuffers[slot]);
if (textures[slot] != 0) GLSMTextures.glDeleteTextures(textures[slot]);
buffers[slot] = 0;
framebuffers[slot] = 0;
textures[slot] = 0;
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.gtnewhorizons.angelica.glsm.GLScreenCapture;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import org.lwjgl.BufferUtils;
@@ -659,6 +660,11 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend {
GL11.glReadBuffer(mode);
}
@Override
public ScreenCapture createScreenCapture(int width, int height) {
return new GLScreenCapture(width, height);
}
@Override
public void readPixels(int x, int y, int width, int height, int format, int type, ByteBuffer pixels) {
GL11.glReadPixels(x, y, width, height, format, type, pixels);
@@ -479,6 +479,7 @@ public abstract class RenderBackend {
public abstract void readPixels(int x, int y, int width, int height, int format, int type, FloatBuffer pixels);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, IntBuffer pixels);
public abstract void readPixels(int x, int y, int width, int height, int format, int type, long pixelBufferOffset);
public abstract ScreenCapture createScreenCapture(int width, int height);
public abstract void getTexImage(int target, int level, int format, int type, ByteBuffer pixels);
public abstract void getTexImage(int target, int level, int format, int type, IntBuffer pixels);
public abstract void getTexImage(int target, int level, int format, int type, long pixelBufferOffset);
@@ -0,0 +1,102 @@
package com.gtnewhorizons.angelica.glsm.backend;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import org.lwjgl.opengl.GL32;
/** Async downscaled RGBA8 copy of a framebuffer's color. */
public abstract class ScreenCapture {
public static final int RING = RenderBackend.MAX_FRAMES_IN_FLIGHT + 2;
private final int width;
private final int height;
private final long[] tags = new long[RING];
private final long[] syncs = new long[RING];
private int head;
private int tail;
private long mapped;
private boolean deleted;
protected ScreenCapture(int width, int height) {
if (width <= 0 || height <= 0) throw new IllegalArgumentException("capture size " + width + "x" + height);
this.width = width;
this.height = height;
}
public final int width() { return width; }
public final int height() { return height; }
public final boolean idle() { return head == tail; }
public final boolean begin(int srcFramebuffer, int srcWidth, int srcHeight, long tag) {
if (deleted || head - tail == RING || srcWidth <= 0 || srcHeight <= 0) return false;
final int slot = head % RING;
if (!start(slot, srcFramebuffer, srcWidth, srcHeight)) return false;
final long sync = GLSMBuffers.glFenceSync(GL32.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
if (sync == 0L) return false;
syncs[slot] = sync;
tags[slot] = tag;
head++;
return true;
}
public final boolean poll() {
if (mapped != 0L) throw new IllegalStateException("poll() before release() of the previous capture");
while (tail != head) {
final int slot = tail % RING;
final int status = GLSMBuffers.glClientWaitSync(syncs[slot], 0, 0L);
if (status == GL32.GL_TIMEOUT_EXPIRED) return false;
dropSync(slot);
if (status != GL32.GL_WAIT_FAILED) {
mapped = map(slot);
if (mapped != 0L) return true;
}
tail++;
}
return false;
}
public final long address() {
if (mapped == 0L) throw new IllegalStateException("no polled capture");
return mapped;
}
public final long tag() {
if (mapped == 0L) throw new IllegalStateException("no polled capture");
return tags[tail % RING];
}
public final void release() {
if (mapped == 0L) throw new IllegalStateException("no polled capture");
mapped = 0L;
unmap(tail % RING);
tail++;
}
public final void delete() {
if (deleted) return;
if (mapped != 0L) release();
deleted = true;
for (int slot = 0; slot < RING; slot++) {
dropSync(slot);
deleteSlot(slot);
}
head = tail = 0;
}
private void dropSync(int slot) {
if (syncs[slot] == 0L) return;
GLSMBuffers.glDeleteSync(syncs[slot]);
syncs[slot] = 0L;
}
/** Returns false when nothing was recorded; the slot then stays free. */
protected abstract boolean start(int slot, int srcFramebuffer, int srcWidth, int srcHeight);
protected abstract long map(int slot);
protected abstract void unmap(int slot);
/** Called for every slot, including ones never started. */
protected abstract void deleteSlot(int slot);
}
@@ -152,11 +152,11 @@ public final class Tracy {
return backend;
}
private static final String ABI_SENTINEL = "sectionEnter";
private static final String ABI_SENTINEL = "frameImage";
private static boolean implementsCurrentAbi(TracyBackend backend) {
try {
backend.getClass().getMethod(ABI_SENTINEL, int.class, String.class);
backend.getClass().getMethod(ABI_SENTINEL, long.class, int.class, int.class, int.class, boolean.class);
return true;
} catch (NoSuchMethodException e) {
LOGGER.warn("Tracy backend {} was built against an older TracyBackend interface; profiling disabled", backend.getClass().getName());
@@ -337,6 +337,12 @@ public final class Tracy {
if (ptr != 0) BACKEND.frameMark(ptr);
}
/** Copies {@code rgba} before returning; width and height must be multiples of 4. */
public static void frameImage(long rgba, int width, int height, int offset, boolean flip) {
if (!ENABLED || SHUTTING_DOWN) return;
BACKEND.frameImage(rgba, width, height, offset, flip);
}
public static void plotInt(String name, long value) {
plotInt(name, value, TracyBackend.PLOT_FORMAT_NUMBER);
}
@@ -32,6 +32,7 @@ public interface TracyBackend {
void frameMark();
void frameMark(long namePtr);
void frameImage(long rgba, int width, int height, int offset, boolean flip);
long internFrameName(String name);
long internPlotName(String name, int format);
@@ -59,7 +59,7 @@ final class GLSMOwners {
"disableFog", "enableFog", "fogColor", "getFogColor", "getFogMode", "getFogState", "glFog", "glFogf",
"glFogi"},
{"com/gtnewhorizons/angelica/glsm/GLSMFramebuffer",
"attachTextureForReading", "framebufferCompletenessIsMeaningful", "getDrawBuffer", "getDrawFramebuffer",
"attachTextureForReading", "createScreenCapture", "framebufferCompletenessIsMeaningful", "getDrawBuffer", "getDrawFramebuffer",
"getDrawFramebufferGeneration", "getReadFramebuffer", "glBindFramebuffer", "glBindRenderbuffer",
"glBlitFramebuffer", "glCheckFramebufferStatus", "glClear", "glDeleteFramebuffers", "glDeleteRenderbuffers",
"glDrawBuffer", "glDrawBuffers", "glFramebufferRenderbuffer", "glFramebufferTexture",
@@ -129,7 +129,7 @@ final class GLSMOwners {
"deferredTexturesAwaitingRelease", "onFrameEnd"},
{"com/gtnewhorizons/angelica/glsm/GLSMTextures",
"abandonTextureStaging", "beginTextureStaging", "commitTextureStaging", "disableTexture", "enableTexture",
"forcePixelUnpackState", "getActiveTextureUnit", "getActiveTextureUnitForServerState",
"forcePixelPackState", "forcePixelUnpackState", "getActiveTextureUnit", "getActiveTextureUnitForServerState",
"getActiveTextureUnitStack", "getBoundTextureForServerState", "getClientActiveTextureUnit",
"getImageUnitBinding", "getMaxBoundTextureUnit", "getSamplerBinding", "getSamplerUnitCount", "getTextures",
"glActiveTexture", "glActiveTextureARB", "glBindImageTexture", "glBindImageTextures",
@@ -142,7 +142,7 @@ final class GLSMOwners {
"glTexImage1D", "glTexImage2D", "glTexImage3D", "glTexParameter", "glTexParameterf", "glTexParameteri",
"glTexStorage2D", "glTexSubImage1D", "glTexSubImage2D", "glTexSubImage3D", "lookupTexParameteri",
"markTextureAsAtlas", "remapTexClamp", "remapTexClampBuffer", "remapTexImageForGLES",
"restorePixelUnpackState", "setActiveTexture", "setFilter", "setTextureSupersamplingSamples",
"restorePixelPackState", "restorePixelUnpackState", "setActiveTexture", "setFilter", "setTextureSupersamplingSamples",
"trackMaxBoundTextureUnit", "trimTextureStaging", "unbindAllSamplers", "updateTexParameterfCache",
"updateTexParameteriCache"},
{"com/gtnewhorizons/angelica/glsm/GLSMVertexInput",
@@ -0,0 +1,125 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL21;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@GLCoreTest
class GLScreenCapture_GLTest {
private static final int SRC_W = 64;
private static final int SRC_H = 32;
private static final int DST_W = 16;
private static final int DST_H = 8;
private record Target(int texture, int framebuffer) {
void delete() {
GLSMFramebuffer.glDeleteFramebuffers(framebuffer);
GLSMTextures.glDeleteTextures(texture);
}
}
private static Target quadrantTarget() {
final int texture = GLSMTextures.glGenTextures();
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, texture);
GLSMTextures.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, SRC_W, SRC_H, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
GLSMTextures.glBindTexture(GL11.GL_TEXTURE_2D, 0);
final int framebuffer = GLSMFramebuffer.glGenFramebuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GLSMFramebuffer.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, texture, 0);
GlTestSupport.fillQuadrants(SRC_W, SRC_H);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
return new Target(texture, framebuffer);
}
@Test
void downscalesFramebufferQuadrantsBottomUp() {
final Target target = quadrantTarget();
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(DST_W, DST_H);
try {
assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, 42L));
GL11.glFinish();
assertTrue(capture.poll());
assertEquals(42L, capture.tag());
GlTestSupport.assertQuadrantsBottomUp(capture.address(), DST_W, DST_H);
capture.release();
} finally {
capture.delete();
target.delete();
}
}
@Test
void ringRefusesWhenFullAndReturnsInBeginOrder() {
final Target target = quadrantTarget();
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(DST_W, DST_H);
try {
for (int i = 0; i < ScreenCapture.RING; i++) assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, i), "capture " + i + " was not recorded");
assertFalse(capture.begin(target.framebuffer(), SRC_W, SRC_H, 99L), "a full ring must refuse a new capture");
GL11.glFinish();
for (int i = 0; i < ScreenCapture.RING; i++) {
assertTrue(capture.poll(), "capture " + i + " not ready after glFinish");
assertEquals(i, capture.tag(), "captures must come back in begin order");
capture.release();
}
assertFalse(capture.poll());
assertTrue(capture.idle());
assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, 100L), "a drained ring must accept captures again");
} finally {
capture.delete();
target.delete();
}
}
@Test
void restoresBindingsAndPixelStore() {
final Target target = quadrantTarget();
final int readFbo = GLSMFramebuffer.glGenFramebuffers();
final int drawFbo = GLSMFramebuffer.glGenFramebuffers();
final int packBuffer = GLSMBuffers.glGenBuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, readFbo);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, drawFbo);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, packBuffer);
GLSMBuffers.glBufferData(GL21.GL_PIXEL_PACK_BUFFER, 16L, GL15.GL_STREAM_READ);
GLSMTextures.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
GLSMEnable.glEnable(GL11.GL_SCISSOR_TEST);
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(DST_W, DST_H);
try {
assertTrue(capture.begin(target.framebuffer(), SRC_W, SRC_H, 1L));
GL11.glFinish();
assertTrue(capture.poll());
capture.release();
assertEquals(readFbo, GLSMFramebuffer.getReadFramebuffer());
assertEquals(drawFbo, GLSMFramebuffer.getDrawFramebuffer());
assertEquals(packBuffer, GLStateManager.ctx().boundPixelPackBuffer);
assertEquals(1, GLStateManager.ctx().pixelPackState.alignment());
assertTrue(GLSMRasterizer.getScissorTest().isEnabled());
assertEquals(readFbo, GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING));
assertEquals(drawFbo, GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING));
assertEquals(packBuffer, GL11.glGetInteger(GL21.GL_PIXEL_PACK_BUFFER_BINDING));
assertEquals(1, GL11.glGetInteger(GL11.GL_PACK_ALIGNMENT));
assertTrue(GL11.glIsEnabled(GL11.GL_SCISSOR_TEST));
} finally {
capture.delete();
GLSMEnable.glDisable(GL11.GL_SCISSOR_TEST);
GLSMTextures.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 4);
GLSMBuffers.glBindBuffer(GL21.GL_PIXEL_PACK_BUFFER, 0);
GLSMBuffers.glDeleteBuffers(packBuffer);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLSMFramebuffer.glDeleteFramebuffers(readFbo);
GLSMFramebuffer.glDeleteFramebuffers(drawFbo);
target.delete();
}
}
}
@@ -1,5 +1,6 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
@@ -22,6 +23,35 @@ public final class GlTestSupport {
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
}
public static void fillQuadrants(int width, int height) {
GLSMRasterizer.glViewport(0, 0, width, height);
GLSMEnable.glEnable(GL11.GL_SCISSOR_TEST);
clearRect(0, 0, width / 2, height / 2, 1f, 0f, 0f);
clearRect(width / 2, 0, width / 2, height / 2, 0f, 1f, 0f);
clearRect(0, height / 2, width / 2, height / 2, 0f, 0f, 1f);
clearRect(width / 2, height / 2, width / 2, height / 2, 1f, 1f, 1f);
GLSMEnable.glDisable(GL11.GL_SCISSOR_TEST);
}
public static void assertQuadrantsBottomUp(long rgba, int width, int height) {
assertRgba(rgba, width, width / 4, height / 4, 0xFF0000FF, "bottom-left");
assertRgba(rgba, width, width * 3 / 4, height / 4, 0x00FF00FF, "bottom-right");
assertRgba(rgba, width, width / 4, height * 3 / 4, 0x0000FFFF, "top-left");
assertRgba(rgba, width, width * 3 / 4, height * 3 / 4, 0xFFFFFFFF, "top-right");
}
private static void clearRect(int x, int y, int width, int height, float r, float g, float b) {
GLSMRasterizer.glScissor(x, y, width, height);
GLSMBlend.glClearColor(r, g, b, 1f);
GLSMFramebuffer.glClear(GL11.GL_COLOR_BUFFER_BIT);
}
private static void assertRgba(long rgba, int width, int x, int y, int expected, String where) {
final long at = rgba + ((long) y * width + x) * 4;
final int actual = (MemoryUtilities.memGetByte(at) & 0xFF) << 24 | (MemoryUtilities.memGetByte(at + 1) & 0xFF) << 16 | (MemoryUtilities.memGetByte(at + 2) & 0xFF) << 8 | (MemoryUtilities.memGetByte(at + 3) & 0xFF);
assertEquals(Integer.toHexString(expected), Integer.toHexString(actual), where + " quadrant at row " + y + " (rows bottom-up)");
}
public static int compile(int type, String source) {
final int shader = GLSMPrograms.glCreateShader(type);
GLSMPrograms.glShaderSource(shader, source);
@@ -6,7 +6,9 @@ import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.DebugMessageHandler;
import com.gtnewhorizons.angelica.glsm.backend.MainThreadPump;
import com.gtnewhorizons.retrofuturabootstrap.MainStartOnFirstThread;
import com.gtnewhorizons.angelica.glsm.GLScreenCapture;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware;
import me.eigenraven.lwjgl3ify.client.MainThreadExec;
@@ -792,6 +794,11 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend {
GL11C.glReadBuffer(mode);
}
@Override
public ScreenCapture createScreenCapture(int width, int height) {
return new GLScreenCapture(width, height);
}
@Override
public void readPixels(int x, int y, int width, int height, int format, int type, ByteBuffer pixels) {
GL11C.glReadPixels(x, y, width, height, format, type, pixels);
@@ -18,6 +18,7 @@ import me.eigenraven.lwjgl3ify.client.MainThreadExec;
import org.lwjglx.Lwjgl3ifyEventLoop;
import com.gtnewhorizons.angelica.glsm.backend.VSyncMode;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig;
import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache;
import com.gtnewhorizons.angelica.glsm.texture.TextureStaging;
@@ -42,6 +43,7 @@ import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineStore;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.VertexAttribs;
import com.gtnewhorizons.angelica.sdlgpu.resource.BufferParams;
import com.gtnewhorizons.angelica.sdlgpu.resource.ByteRegionCopy;
import com.gtnewhorizons.angelica.sdlgpu.resource.ColorSource;
import com.gtnewhorizons.angelica.sdlgpu.resource.CopyRectClip;
import com.gtnewhorizons.angelica.sdlgpu.resource.FBOClearTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.FboState;
@@ -168,7 +170,7 @@ public class SDLGPURenderBackend extends RenderBackend {
new VoxelizationDispatcher(computeBinder);
private final VertexAttribs vertexAttribs = new VertexAttribs();
private final TextureOps textureOps = new TextureOps(device, frameManager, resourceManager, fboClearTracker);
private final ReadbackShadows readbackShadows = new ReadbackShadows(device);
private final ReadbackShadows readbackShadows = new ReadbackShadows(device, resourceManager, fenceTracker);
private final PersistentBufferSync persistentSync = new PersistentBufferSync(frameManager, resourceManager,
new PersistentBufferSync.UploadSink() {
@Override public void enqueue(TransferThread.DeferredUpload upload) { enqueueUpload(upload); }
@@ -1378,8 +1380,7 @@ public class SDLGPURenderBackend extends RenderBackend {
}
resourceManager.dropArrayShadow(dstGlId);
resourceManager.dropEboShadow(dstGlId);
resourceManager.markBufferContentsDefined(dstGlId);
readbackShadows.invalidate(dstGlId, writeOffset);
onBufferContentsWritten(dstGlId);
final ByteBuffer dstUboShadow = resourceManager.getUboShadow(dstGlId);
if (dstUboShadow != null) {
@@ -1460,9 +1461,8 @@ public class SDLGPURenderBackend extends RenderBackend {
}
final long cp = frameManager.ensureCopyPass();
if (cp == 0) return;
if (resourceManager.getBufferGlUsage(dstGlId) == GL15.GL_STREAM_READ
&& readbackShadows.scheduleDownload(cp, srcHandle, readOffset, dstGlId, writeOffset, size)) {
return;
if (resourceManager.getBufferGlUsage(dstGlId) == GL15.GL_STREAM_READ && !isBoundAsSsbo(s(), dstGlId)) {
readbackShadows.scheduleDownload(cp, srcHandle, readOffset, dstGlId, writeOffset, size);
}
try (var stack = MemoryStack.stackPush()) {
final SDL_GPUBufferLocation src = SDL_GPUBufferLocation.calloc(stack).buffer(srcHandle).offset((int) readOffset);
@@ -2195,6 +2195,32 @@ public class SDLGPURenderBackend extends RenderBackend {
if (capacity < needed) throw new IllegalArgumentException("readback of " + width + "x" + height + " format 0x" + Integer.toHexString(format) + " type 0x" + Integer.toHexString(type) + " needs " + needed + " bytes, destination has " + capacity);
}
@Override public ScreenCapture createScreenCapture(int width, int height) {
return new SDLScreenCapture(this, device, frameManager, resourceManager, textureOps, width, height);
}
ColorSource colorSourceForRead(int fboId) {
final FboState fbo = fboId != 0 ? resourceManager.getFbo(fboId) : null;
if (fbo == null) return null;
final ContextState cs = s();
final ColorSource source = resolveColorAttachment(fbo, cs.readColor);
if (source.texture == 0L || source.width <= 0 || source.height <= 0) return null;
fboClearTracker.materializePendingClearForRead(cs, frameManager.frame(), source.texture);
return source;
}
private ColorSource resolveColorAttachment(FboState fbo, ColorSource out) {
final int colorGlId = fbo.colorGlIds[fbo.readBufferIndex];
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(colorGlId);
out.texture = resourceManager.getTextureHandle(colorGlId);
out.level = fbo.colorLevels[fbo.readBufferIndex];
out.sdlFormat = meta != null ? meta.sdlFormat() : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
out.glBaseFormat = meta != null ? PixelPack.glBaseFormat(meta.glFormat()) : GL11.GL_RGBA;
out.width = meta != null ? PixelOps.mipLevelSize(meta.width(), out.level) : fbo.width;
out.height = meta != null ? PixelOps.mipLevelSize(meta.height(), out.level) : fbo.height;
return out;
}
private void readPixelsInto(int x, int y, int width, int height, int format, int type, long dst, long capacity, int pbo, long pboOffset) {
PixelPack.validate(format, type);
if (width <= 0 || height <= 0) return;
@@ -2224,14 +2250,13 @@ public class SDLGPURenderBackend extends RenderBackend {
final FboState fbo = resourceManager.getFbo(cs.boundReadFboId);
if (fbo == null) return;
if (kind == READBACK_COLOR) {
final int colorGlId = fbo.colorGlIds[fbo.readBufferIndex];
final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(colorGlId);
colorTex = resourceManager.getTextureHandle(colorGlId);
colorFormat = meta != null ? meta.sdlFormat() : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
if (meta != null) colorBase = PixelPack.glBaseFormat(meta.glFormat());
srcLevel = fbo.colorLevels[fbo.readBufferIndex];
srcWidth = meta != null ? PixelOps.mipLevelSize(meta.width(), srcLevel) : fbo.width;
srcHeight = meta != null ? PixelOps.mipLevelSize(meta.height(), srcLevel) : fbo.height;
final ColorSource source = resolveColorAttachment(fbo, cs.readColor);
colorTex = source.texture;
colorFormat = source.sdlFormat;
colorBase = source.glBaseFormat;
srcLevel = source.level;
srcWidth = source.width;
srcHeight = source.height;
} else {
if (fbo.depthLevel != 0) throw new UnsupportedOperationException("glReadPixels of a depth/stencil attachment at level " + fbo.depthLevel + " is not supported on SDL-GPU");
depthTex = fbo.depthTexture;
@@ -2348,7 +2373,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} finally {
region.clear();
}
resourceManager.markBufferContentsDefined(pbo);
onBufferContentsWritten(pbo);
flushAndSubmitMidFrame();
}
@@ -2955,7 +2980,7 @@ public class SDLGPURenderBackend extends RenderBackend {
case GL31.GL_COPY_WRITE_BUFFER -> st.boundCopyWriteBuffer = buffer;
case GL43.GL_SHADER_STORAGE_BUFFER -> {
st.boundSSBO = buffer;
resourceManager.markBufferContentsDefined(buffer);
onBufferContentsWritten(buffer);
}
}
}
@@ -3044,8 +3069,8 @@ public class SDLGPURenderBackend extends RenderBackend {
private void respecifyStore(ContextState cs, int target, int glId, long size, int usage) {
freePlainMappingFor(glId, true);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL31.GL_UNIFORM_BUFFER) {
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (resourceManager.getBufferSize(glId) == size) return;
if (size > 0) {
resourceManager.recordBufferSizeOnly(glId, size);
@@ -3060,6 +3085,7 @@ public class SDLGPURenderBackend extends RenderBackend {
readbackShadows.release(glId);
resourceManager.deleteBuffer(glId);
resourceManager.createBuffer(glId, sdlUsage, size);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL15.GL_ARRAY_BUFFER) cs.bumpAttribStateGen();
}
@@ -3099,8 +3125,8 @@ public class SDLGPURenderBackend extends RenderBackend {
private void respecifyStore(ContextState cs, int target, int glId, ByteBuffer data, int usage) {
final long dataSize = data.remaining();
freePlainMappingFor(glId, true);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL31.GL_UNIFORM_BUFFER) {
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (dataSize > 0) {
resourceManager.recordBufferSizeOnly(glId, dataSize);
final ByteBuffer shadow = resourceManager.getOrAllocUboShadow(glId, (int) dataSize);
@@ -3116,6 +3142,7 @@ public class SDLGPURenderBackend extends RenderBackend {
readbackShadows.release(glId);
resourceManager.deleteBuffer(glId);
final long handle = resourceManager.createBuffer(glId, sdlUsage, dataSize);
resourceManager.recordBufferGlParams(glId, usage, BufferParams.MUTABLE_STORE);
if (target == GL15.GL_ARRAY_BUFFER) cs.bumpAttribStateGen();
if (target == GL15.GL_ELEMENT_ARRAY_BUFFER && dataSize > 0
&& (cs.primitiveRestartEnabled || resourceManager.hasEboShadow(glId))) {
@@ -3130,7 +3157,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, 0, false);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
}
@@ -3220,7 +3247,7 @@ public class SDLGPURenderBackend extends RenderBackend {
FrameManager.warnDroppedOutsideFrame(droppedSsboWriteWarned, "bufferSubData(GL_SHADER_STORAGE_BUFFER)");
droppedSsboWriteWarned = true;
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
return;
}
final boolean cycle = target == GL40.GL_DRAW_INDIRECT_BUFFER;
@@ -3229,7 +3256,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, offset, cycle);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
}
@@ -3294,7 +3321,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), staging, gpuHandle, m.offset, false);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
final ByteBuffer old = resourceManager.putPboStaging(glId, staging);
if (old != null) MemoryUtil.memFree(old);
@@ -3308,7 +3335,7 @@ public class SDLGPURenderBackend extends RenderBackend {
shadow.position((int) m.offset).limit((int) (m.offset + m.length));
shadow.put(staging);
shadow.position(0).limit(shadow.capacity());
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
cs.uboRangeGen++;
}
MemoryUtil.memFree(staging);
@@ -3353,7 +3380,7 @@ public class SDLGPURenderBackend extends RenderBackend {
} else if (frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), staging, gpuHandle, m.offset, false);
}
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
}
MemoryUtil.memFree(staging);
@@ -3408,6 +3435,10 @@ public class SDLGPURenderBackend extends RenderBackend {
MemoryUtil.memSet(staging, 0);
return;
}
if (readbackShadows.serve(glId, offset, staging, len)) {
staging.position(0).limit(len);
return;
}
final ByteBuffer direct = switch (target) {
case GL30.GL_PIXEL_UNPACK_BUFFER -> resourceManager.getPboStaging(glId);
@@ -3524,6 +3555,20 @@ public class SDLGPURenderBackend extends RenderBackend {
downloadBufferBlocking(handle, (int) offset, rem, data);
}
private void onBufferContentsWritten(int glId) {
resourceManager.markBufferContentsDefined(glId);
readbackShadows.invalidateAll(glId);
}
// An SSBO-bound buffer can be written by any later GPU pass, so it is never shadowed.
private static boolean isBoundAsSsbo(ContextState st, int glId) {
if (st.boundSSBO == glId) return true;
for (int i = 0; i < ContextState.MAX_INDEXED_BUFFERS; i++) {
if (st.boundSsboByIndex[i] == glId) return true;
}
return false;
}
private boolean readUboStore(int glId, long offset, ByteBuffer dst, int len) {
final ByteBuffer shadow = resourceManager.getUboShadow(glId);
if (shadow == null) return false;
@@ -4042,6 +4087,7 @@ public class SDLGPURenderBackend extends RenderBackend {
if (refuseImmutable(buffer, "glNamedBufferData on immutable storage")) return;
final int sdlUsage = FormatMap.mapBufferUsage(GL15.GL_ARRAY_BUFFER);
freePlainMappingFor(buffer, true);
readbackShadows.release(buffer);
resourceManager.deleteBuffer(buffer);
resourceManager.createBuffer(buffer, sdlUsage, size);
resourceManager.recordBufferGlParams(buffer, usage, BufferParams.MUTABLE_STORE);
@@ -4051,12 +4097,13 @@ public class SDLGPURenderBackend extends RenderBackend {
if (refuseImmutable(buffer, "glNamedBufferData on immutable storage")) return;
final int sdlUsage = FormatMap.mapBufferUsage(GL15.GL_ARRAY_BUFFER);
freePlainMappingFor(buffer, true);
readbackShadows.release(buffer);
resourceManager.deleteBuffer(buffer);
final long handle = resourceManager.createBuffer(buffer, sdlUsage, data.remaining());
resourceManager.recordBufferGlParams(buffer, usage, BufferParams.MUTABLE_STORE);
if (handle != 0 && frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, 0, false);
resourceManager.markBufferContentsDefined(buffer);
onBufferContentsWritten(buffer);
}
}
@Override public void namedBufferData(int buffer, FloatBuffer data, int usage) {
@@ -4067,7 +4114,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final long handle = resourceManager.getBufferHandle(buffer);
if (handle != 0 && frameManager.getCommandBuffer() != 0) {
resourceManager.uploadToBuffer(frameManager.ensureCopyPass(), data, handle, offset, false);
resourceManager.markBufferContentsDefined(buffer);
onBufferContentsWritten(buffer);
}
}
@Override public void copyTextureSubImage2D(int texture, int target, int level, int xoffset, int yoffset, int x, int y, int width, int height) {
@@ -4331,7 +4378,7 @@ public class SDLGPURenderBackend extends RenderBackend {
final int chunkBytes = (int) Math.min(size, defer ? CLEAR_CHUNK_BYTES : FILL_CHUNK_BYTES);
final int alignedChunk = (chunkBytes / elemBytes) * elemBytes;
if (alignedChunk == 0) return;
resourceManager.markBufferContentsDefined(glId);
onBufferContentsWritten(glId);
final boolean zero = data == null || isAllZero(data, elemBytes);
final ByteBuffer chunk = zero ? MemoryUtil.memCalloc(alignedChunk) : MemoryUtil.memAlloc(alignedChunk);
try {
@@ -0,0 +1,74 @@
package com.gtnewhorizons.angelica.sdlgpu;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager;
import com.gtnewhorizons.angelica.sdlgpu.resource.ColorSource;
import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager;
import com.gtnewhorizons.angelica.sdlgpu.resource.TextureOps;
import org.lwjgl.opengl.GL11;
import static org.lwjgl.sdl.SDLGPU.*;
import static org.lwjgl.sdl.SDLSurface.SDL_FLIP_NONE;
final class SDLScreenCapture extends ScreenCapture {
private final SDLGPURenderBackend backend;
private final Device device;
private final FrameManager frameManager;
private final ResourceManager resourceManager;
private final TextureOps textureOps;
private final long[] textures = new long[RING];
private final long[] transferBuffers = new long[RING];
SDLScreenCapture(SDLGPURenderBackend backend, Device device, FrameManager frameManager, ResourceManager resourceManager, TextureOps textureOps, int width, int height) {
super(width, height);
this.backend = backend;
this.device = device;
this.frameManager = frameManager;
this.resourceManager = resourceManager;
this.textureOps = textureOps;
}
@Override
protected boolean start(int slot, int srcFramebuffer, int srcWidth, int srcHeight) {
if (frameManager.getCommandBuffer() == 0) return false;
final ColorSource source = backend.colorSourceForRead(srcFramebuffer);
if (source == null) return false;
final int w = width();
final int h = height();
if (textures[slot] == 0) {
textures[slot] = resourceManager.createScratchTexture(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, w, h, SDL_GPU_TEXTUREUSAGE_COLOR_TARGET);
}
if (transferBuffers[slot] == 0) {
transferBuffers[slot] = resourceManager.createTransferBuffer((long) w * h * 4, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (transferBuffers[slot] == 0) return false;
}
textureOps.blitTexture(source.texture, source.level, 0, 0, Math.min(srcWidth, source.width), Math.min(srcHeight, source.height), textures[slot], 0, 0, 0, w, h, GL11.GL_LINEAR, SDL_FLIP_NONE);
final long copyPass = frameManager.ensureCopyPass();
if (copyPass == 0) return false;
resourceManager.recordTextureDownload(copyPass, textures[slot], 0, 0, 0, 0, w, h, 1, transferBuffers[slot]);
return true;
}
@Override
protected long map(int slot) {
return nSDL_MapGPUTransferBuffer(device.getDevice(), transferBuffers[slot], false);
}
@Override
protected void unmap(int slot) {
resourceManager.unmapTransferBuffer(transferBuffers[slot]);
}
@Override
protected void deleteSlot(int slot) {
resourceManager.releaseTextureDeferred(textures[slot]);
resourceManager.releaseTransferBuffer(transferBuffers[slot]);
textures[slot] = 0L;
transferBuffers[slot] = 0L;
}
}
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.sdlgpu.frame;
import com.gtnewhorizons.angelica.glsm.GLContextState;
import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend;
import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineCache;
import com.gtnewhorizons.angelica.sdlgpu.resource.ColorSource;
import com.gtnewhorizons.angelica.sdlgpu.resource.MappedRange;
import com.gtnewhorizons.angelica.sdlgpu.resource.PackState;
import com.gtnewhorizons.angelica.sdlgpu.shader.LogicOpVariant;
@@ -64,6 +65,7 @@ public final class ContextState {
public final PixelStoreState pixelStore = new PixelStoreState();
public final PackState packState = new PackState();
public final ColorSource readColor = new ColorSource();
public static final class ProxyTextureState {
public int target;
@@ -64,16 +64,20 @@ public final class FenceTracker {
if (pendingFenceSyncs.isEmpty()) return;
final long fence = frameManager.frame().lastAcquiredFence;
frameManager.frame().lastAcquiredFence = 0;
boolean fenceTaken = false;
for (int i = 0; i < pendingFenceSyncs.size(); i++) {
final long id = pendingFenceSyncs.getLong(i);
if (!fenceMap.containsKey(id)) continue;
if (fence != 0) {
fenceMap.put(id, fence);
fenceRefcounts.addTo(fence, 1);
fenceTaken = true;
} else {
fenceMap.remove(id);
}
}
pendingFenceSyncs.clear();
if (fence != 0 && !fenceTaken && device.getDevice() != 0) device.fenceReleaser().release(fence);
}
public int clientWaitSync(long sync, int flags, long timeout) {
@@ -0,0 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
public final class ColorSource {
public long texture;
public int level;
public int width;
public int height;
public int sdlFormat;
public int glBaseFormat;
}
@@ -1,16 +1,16 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.sdlgpu.device.Device;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import com.gtnewhorizons.angelica.sdlgpu.frame.FenceTracker;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.lwjgl.sdl.SDLError;
import org.lwjgl.sdl.SDL_GPUBufferRegion;
import org.lwjgl.sdl.SDL_GPUTransferBufferCreateInfo;
import org.lwjgl.sdl.SDL_GPUTransferBufferLocation;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.opengl.GL32;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
@@ -25,25 +25,27 @@ public final class ReadbackShadows {
private static final class Slot {
long transferBuffer;
int capacity;
int validLen;
boolean valid;
long sync;
}
private final Device device;
private final Long2ObjectOpenHashMap<Slot> slots = new Long2ObjectOpenHashMap<>();
private final ResourceManager resourceManager;
private final FenceTracker fenceTracker;
private final Int2ObjectOpenHashMap<Long2ObjectOpenHashMap<Slot>> byBuffer = new Int2ObjectOpenHashMap<>();
public ReadbackShadows(Device device) {
public ReadbackShadows(Device device, ResourceManager resourceManager, FenceTracker fenceTracker) {
this.device = device;
this.resourceManager = resourceManager;
this.fenceTracker = fenceTracker;
}
private static long key(int glId, long offset) {
return ((long) glId << 32) | (offset & 0xFFFFFFFFL);
}
public boolean scheduleDownload(long copyPass, long srcHandle, long srcOffset, int dstGlId, long dstOffset, long size) {
if (copyPass == 0 || srcHandle == 0 || size <= 0 || size > Integer.MAX_VALUE) return false;
public void scheduleDownload(long copyPass, long srcHandle, long srcOffset, int dstGlId, long dstOffset, long size) {
if (copyPass == 0 || srcHandle == 0 || size <= 0 || size > Integer.MAX_VALUE) return;
final Slot slot = ensureSlot(dstGlId, dstOffset, (int) size);
if (slot == null) return false;
if (slot == null) return;
try (MemoryStack stack = stackPush()) {
final SDL_GPUBufferRegion src = SDL_GPUBufferRegion.calloc(stack)
@@ -55,18 +57,32 @@ public final class ReadbackShadows {
.offset(0);
SDL_DownloadFromGPUBuffer(copyPass, src, dst);
}
dropSync(slot);
slot.sync = fenceTracker.fenceSync();
slot.validLen = (int) size;
slot.valid = true;
return true;
}
public void invalidate(int glId, long offset) {
final Slot slot = slots.get(key(glId, offset));
if (slot != null) slot.valid = false;
public void invalidateAll(int glId) {
if (glId == 0) return;
final Long2ObjectOpenHashMap<Slot> slots = byBuffer.get(glId);
if (slots == null) return;
for (Slot slot : slots.values()) {
slot.valid = false;
dropSync(slot);
}
}
public boolean serve(int glId, long offset, ByteBuffer out, int len) {
final Slot slot = slots.get(key(glId, offset));
if (slot == null || !slot.valid || slot.transferBuffer == 0 || slot.capacity < len) return false;
final Long2ObjectOpenHashMap<Slot> slots = byBuffer.get(glId);
final Slot slot = slots != null ? slots.get(offset) : null;
if (slot == null || !slot.valid || slot.transferBuffer == 0 || slot.validLen < len) return false;
if (slot.sync != 0) {
final int wait = fenceTracker.clientWaitSync(slot.sync, GL32.GL_SYNC_FLUSH_COMMANDS_BIT, Long.MAX_VALUE);
if (wait != GL32.GL_ALREADY_SIGNALED && wait != GL32.GL_CONDITION_SATISFIED) return false;
dropSync(slot);
}
final long mapped = nSDL_MapGPUTransferBuffer(device.getDevice(), slot.transferBuffer, false);
if (mapped == 0) {
@@ -80,62 +96,63 @@ public final class ReadbackShadows {
}
public boolean hasSlotsFor(int glId) {
for (long k : slots.keySet()) {
if ((int) (k >>> 32) == glId) return true;
}
return false;
return byBuffer.containsKey(glId);
}
public void release(int glId) {
if (slots.isEmpty()) return;
final ObjectIterator<Long2ObjectMap.Entry<Slot>> it = slots.long2ObjectEntrySet().fastIterator();
while (it.hasNext()) {
final Long2ObjectMap.Entry<Slot> entry = it.next();
if ((int) (entry.getLongKey() >>> 32) == glId) {
releaseSlot(entry.getValue());
it.remove();
}
}
final Long2ObjectOpenHashMap<Slot> slots = byBuffer.remove(glId);
if (slots == null) return;
for (Slot slot : slots.values()) releaseSlot(slot);
}
public void dispose() {
for (Slot slot : slots.values()) releaseSlot(slot);
slots.clear();
for (Long2ObjectOpenHashMap<Slot> slots : byBuffer.values()) {
for (Slot slot : slots.values()) releaseSlot(slot);
}
byBuffer.clear();
}
private Slot ensureSlot(int glId, long offset, int size) {
final long k = key(glId, offset);
Slot slot = slots.get(k);
Long2ObjectOpenHashMap<Slot> slots = byBuffer.get(glId);
Slot slot = slots != null ? slots.get(offset) : null;
if (slot != null && slot.capacity >= size) return slot;
if (slot == null) {
if (slots == null) {
slots = new Long2ObjectOpenHashMap<>();
byBuffer.put(glId, slots);
}
slot = new Slot();
slots.put(k, slot);
slots.put(offset, slot);
} else {
releaseSlot(slot);
}
try (MemoryStack stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo ci = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD)
.size(size);
slot.transferBuffer = SDL_CreateGPUTransferBuffer(device.getDevice(), ci);
}
slot.transferBuffer = resourceManager.createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (slot.transferBuffer == 0) {
LOG.error("Failed to create readback transfer buffer size={}: {}", size, SDLError.SDL_GetError());
slots.remove(k);
slots.remove(offset);
if (slots.isEmpty()) byBuffer.remove(glId);
return null;
}
slot.capacity = size;
slot.validLen = 0;
slot.valid = false;
return slot;
}
private void dropSync(Slot slot) {
if (slot.sync == 0) return;
fenceTracker.deleteSync(slot.sync);
slot.sync = 0;
}
private void releaseSlot(Slot slot) {
dropSync(slot);
if (slot.transferBuffer != 0 && device.getDevice() != 0) {
SDL_ReleaseGPUTransferBuffer(device.getDevice(), slot.transferBuffer);
resourceManager.releaseTransferBuffer(slot.transferBuffer);
}
slot.transferBuffer = 0;
slot.capacity = 0;
slot.validLen = 0;
}
}
@@ -1572,9 +1572,13 @@ public final class ResourceManager {
}
public long createTransferBuffer(long size) {
return createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD);
}
public long createTransferBuffer(long size, int usage) {
try (var stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo ci = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD)
.usage(usage)
.size((int) size);
final long handle = SDL_CreateGPUTransferBuffer(device.getDevice(), ci);
@@ -2067,23 +2071,42 @@ public final class ResourceManager {
public void downloadFromTexture(long commandBuffer, long gpuTexture, int x, int y, int z, int w, int h, int d, int level, ByteBuffer output) {
final int size = output.remaining();
final long xfer = createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (xfer == 0) return;
final long copyPass = SDL_BeginGPUCopyPass(commandBuffer);
if (copyPass == 0) {
LOG.error("Failed to begin copy pass for texture download: {}", SDLError.SDL_GetError());
releaseTransferBuffer(xfer);
return;
}
recordTextureDownload(copyPass, gpuTexture, level, x, y, z, w, h, d, xfer);
SDL_EndGPUCopyPass(copyPass);
final long fence = Submits.submitAndAcquireFence(commandBuffer);
if (fence == 0) {
LOG.error("Failed to submit download command buffer: {}", SDLError.SDL_GetError());
releaseTransferBuffer(xfer);
return;
}
try {
FenceWait.await(device, fence, FenceWait.FOREVER);
} finally {
device.fenceReleaser().release(fence);
}
final long mappedPtr = nSDL_MapGPUTransferBuffer(device.getDevice(), xfer, false);
if (mappedPtr != 0) {
final ByteBuffer mapped = MemoryUtil.memByteBuffer(mappedPtr, size);
output.put(mapped);
}
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
}
public void recordTextureDownload(long copyPass, long gpuTexture, int level, int x, int y, int z, int w, int h, int d, long xfer) {
try (var stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo xferCi = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD)
.size(size);
final long xfer = SDL_CreateGPUTransferBuffer(device.getDevice(), xferCi);
if (xfer == 0) {
LOG.error("Failed to create download transfer buffer: {}", SDLError.SDL_GetError());
return;
}
final long copyPass = SDL_BeginGPUCopyPass(commandBuffer);
if (copyPass == 0) {
LOG.error("Failed to begin copy pass for texture download: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
return;
}
final SDL_GPUTextureRegion src = SDL_GPUTextureRegion.calloc(stack)
.texture(gpuTexture)
.mip_level(level)
@@ -2095,45 +2118,18 @@ public final class ResourceManager {
.offset(0);
SDL_DownloadFromGPUTexture(copyPass, src, dst);
SDL_EndGPUCopyPass(copyPass);
final long fence = Submits.submitAndAcquireFence(commandBuffer);
if (fence == 0) {
LOG.error("Failed to submit download command buffer: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
return;
}
try {
FenceWait.await(device, fence, FenceWait.FOREVER);
} finally {
device.fenceReleaser().release(fence);
}
final long mappedPtr = nSDL_MapGPUTransferBuffer(device.getDevice(), xfer, false);
if (mappedPtr != 0) {
final ByteBuffer mapped = MemoryUtil.memByteBuffer(mappedPtr, size);
output.put(mapped);
}
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
}
}
public void downloadFromBuffer(long commandBuffer, long gpuBuffer, int offset, int size, ByteBuffer output) {
try (var stack = stackPush()) {
final SDL_GPUTransferBufferCreateInfo xferCi = SDL_GPUTransferBufferCreateInfo.calloc(stack)
.usage(SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD)
.size(size);
final long xfer = SDL_CreateGPUTransferBuffer(device.getDevice(), xferCi);
if (xfer == 0) {
LOG.error("Failed to create download transfer buffer for buffer readback: {}", SDLError.SDL_GetError());
return;
}
final long xfer = createTransferBuffer(size, SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD);
if (xfer == 0) return;
final long copyPass = SDL_BeginGPUCopyPass(commandBuffer);
if (copyPass == 0) {
LOG.error("Failed to begin copy pass for buffer download: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
return;
}
@@ -2152,7 +2148,7 @@ public final class ResourceManager {
final long fence = Submits.submitAndAcquireFence(commandBuffer);
if (fence == 0) {
LOG.error("Failed to submit buffer download command buffer: {}", SDLError.SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
return;
}
try {
@@ -2167,7 +2163,7 @@ public final class ResourceManager {
output.put(mapped);
}
SDL_UnmapGPUTransferBuffer(device.getDevice(), xfer);
SDL_ReleaseGPUTransferBuffer(device.getDevice(), xfer);
releaseTransferBuffer(xfer);
}
}
private long defaultSampler;
@@ -3,6 +3,15 @@ package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMFramebuffer;
import com.gtnewhorizons.angelica.glsm.GLSMTextures;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.frame.FenceTracker;
import com.gtnewhorizons.angelica.sdlgpu.resource.ReadbackShadows;
import com.gtnewhorizons.angelica.sdlgpu.resource.SdlReflect;
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -10,11 +19,19 @@ import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL31;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL43;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.LockSupport;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
@@ -26,6 +43,14 @@ class GlsmSdlReadbackTest {
private static final int B = 0x56;
private static final int A = 0xFF;
private static final int SRC_W = 64;
private static final int SRC_H = 32;
private static final int CAPTURE_W = 16;
private static final int CAPTURE_H = 8;
private static final int COPY_BYTES = 64;
private record StreamReadCopy(int src, int dst, ByteBuffer data) {}
@Test
void getTexImageSeesDeferredTextureUpload() {
final int texture = GLSMTextures.glGenTextures();
@@ -84,6 +109,196 @@ class GlsmSdlReadbackTest {
assertEquals(0xFF, pixel.get(3) & 0xFF);
}
@Test
void screenCaptureSeesUserFramebuffer() {
final int texture = GlsmSdlHeadlessRig.newNearestTexture();
GLSMTextures.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, SRC_W, SRC_H, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null);
final int framebuffer = GlsmSdlHeadlessRig.fboWithColor(texture, 0);
GlTestSupport.fillQuadrants(SRC_W, SRC_H);
final ScreenCapture capture = GLSMFramebuffer.createScreenCapture(CAPTURE_W, CAPTURE_H);
try {
assertTrue(capture.begin(framebuffer, SRC_W, SRC_H, 7L), "capture was not recorded");
GlsmSdlHeadlessRig.endFrame();
GlsmSdlHeadlessRig.beginFrame();
awaitPoll(capture);
assertEquals(7L, capture.tag());
GlTestSupport.assertQuadrantsBottomUp(capture.address(), CAPTURE_W, CAPTURE_H);
capture.release();
final long texture0 = Reflect.<long[]>get(capture, "textures")[0];
final long transferBuffer0 = Reflect.<long[]>get(capture, "transferBuffers")[0];
assertNotEquals(0L, texture0, "slot 0 was never created");
assertNotEquals(0L, transferBuffer0, "slot 0 was never created");
capture.delete();
GlsmSdlHeadlessRig.endFrame();
GlsmSdlHeadlessRig.beginFrame();
assertNotLive(texture0, "liveTextureHandles");
assertNotLive(transferBuffer0, "liveTransferBufferHandles");
} finally {
capture.delete();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLSMFramebuffer.glDeleteFramebuffers(framebuffer);
GLSMTextures.glDeleteTextures(texture);
}
}
private static void assertNotLive(long handle, String field) {
final LongOpenHashSet live = Reflect.get(GlsmSdlHeadlessRig.resourceManager(), field);
assertFalse(live.contains(handle), field + " still holds capture handle " + handle + " after delete");
}
@Test
void deleteSyncBeforeSubmitIsNotResurrected() {
final long sync = GLSMBuffers.glFenceSync(GL32.GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
GLSMBuffers.glDeleteSync(sync);
GlsmSdlHeadlessRig.endFrame();
GlsmSdlHeadlessRig.beginFrame();
final FenceTracker fences = Reflect.get(BackendManager.RENDER_BACKEND, "fenceTracker");
final Long2LongOpenHashMap fenceMap = Reflect.get(fences, "fenceMap");
assertFalse(fenceMap.containsKey(sync), "a sync deleted before its submit must not be bound to the frame fence afterwards");
}
private static void awaitPoll(ScreenCapture capture) {
final long deadline = System.nanoTime() + 5_000_000_000L;
while (!capture.poll()) {
assertTrue(System.nanoTime() < deadline, "capture did not complete within 5 s");
LockSupport.parkNanos(1_000_000L);
}
}
@Test
void streamReadCopyServesWithoutAppFence() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
assertTrue(hasShadow(copy.dst()), "copy into a GL_STREAM_READ buffer must take the shadow path");
final ReadbackShadows shadows = Reflect.get(BackendManager.RENDER_BACKEND, "readbackShadows");
assertNotEquals(0L, SdlReflect.readbackSlotSync(shadows, copy.dst(), 0), "the shadow download must carry a fence");
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(copy.data(), read, "glGetBufferSubData right after the copy");
assertEquals(0L, SdlReflect.readbackSlotSync(shadows, copy.dst(), 0), "the read must be served from the shadow, which consumes its fence");
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadShadowSkippedWhileBoundAsSsbo() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(true);
assertFalse(hasShadow(copy.dst()), "a destination bound as an SSBO can be written by the GPU, so it must not be shadowed");
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(copy.data(), read, "glGetBufferSubData of an SSBO-bound copy destination");
GLSMBuffers.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, 3, 0);
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadCopyVisibleToMapAndGpu() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
final ByteBuffer mapped = GLSMBuffers.glMapBufferRange(GL31.GL_COPY_WRITE_BUFFER, 0, COPY_BYTES, GL30.GL_MAP_READ_BIT);
assertNotNull(mapped, "glMapBufferRange returned null");
assertBytes(copy.data(), mapped, "glMapBufferRange READ of the copy destination");
GLSMBuffers.glUnmapBuffer(GL31.GL_COPY_WRITE_BUFFER);
final int third = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL31.GL_COPY_READ_BUFFER, copy.dst());
GLSMBuffers.glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, third);
GLSMBuffers.glBufferData(GL31.GL_COPY_WRITE_BUFFER, COPY_BYTES, GL15.GL_DYNAMIC_DRAW);
GLSMBuffers.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, 0, 0, COPY_BYTES);
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(copy.data(), read, "GPU copy out of the copy destination");
GLSMBuffers.glDeleteBuffers(third);
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadShadowDroppedOnSubData() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
final ByteBuffer other = BufferUtils.createByteBuffer(COPY_BYTES);
for (int i = 0; i < COPY_BYTES; i++) other.put(i, (byte) (0xA0 ^ i));
GLSMBuffers.glBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, other);
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
assertBytes(other, read, "glGetBufferSubData after glBufferSubData over the copy");
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
@Test
void streamReadShadowServesOnlyDownloadedBytes() {
GlsmSdlHeadlessRig.endFrame();
final StreamReadCopy copy = streamReadCopy(false);
final int half = COPY_BYTES / 2;
final int quarter = COPY_BYTES / 4;
final ByteBuffer tail = BufferUtils.createByteBuffer(half);
for (int i = 0; i < half; i++) tail.put(i, (byte) (0x50 + i));
GLSMBuffers.glBufferSubData(GL31.GL_COPY_WRITE_BUFFER, half, tail);
final ByteBuffer head = BufferUtils.createByteBuffer(quarter);
for (int i = 0; i < quarter; i++) head.put(i, (byte) (0xC0 + i));
final int src2 = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, src2);
GLSMBuffers.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, head, GL15.GL_DYNAMIC_DRAW);
GLSMBuffers.glBindBuffer(GL31.GL_COPY_READ_BUFFER, src2);
GLSMBuffers.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, 0, 0, quarter);
final ByteBuffer read = BufferUtils.createByteBuffer(COPY_BYTES);
GLSMBuffers.glGetBufferSubData(GL31.GL_COPY_WRITE_BUFFER, 0, read);
for (int i = 0; i < COPY_BYTES; i++) {
final byte expected = i < quarter ? head.get(i) : i < half ? copy.data().get(i) : tail.get(i - half);
assertEquals(expected, read.get(i), "byte " + i + " after a shorter copy reused a larger shadow");
}
GLSMBuffers.glDeleteBuffers(src2);
GLSMBuffers.glDeleteBuffers(copy.src());
GLSMBuffers.glDeleteBuffers(copy.dst());
}
private static StreamReadCopy streamReadCopy(boolean dstBoundAsSsbo) {
final ByteBuffer data = BufferUtils.createByteBuffer(COPY_BYTES);
for (int i = 0; i < COPY_BYTES; i++) data.put(i, (byte) (i * 7 + 3));
final int src = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL43.GL_SHADER_STORAGE_BUFFER, src);
GLSMBuffers.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, data, GL15.GL_DYNAMIC_DRAW);
final int dst = GLSMBuffers.glGenBuffers();
GLSMBuffers.glBindBuffer(GL31.GL_COPY_READ_BUFFER, src);
GLSMBuffers.glBindBuffer(GL31.GL_COPY_WRITE_BUFFER, dst);
GLSMBuffers.glBufferData(GL31.GL_COPY_WRITE_BUFFER, COPY_BYTES, GL15.GL_STREAM_READ);
if (dstBoundAsSsbo) GLSMBuffers.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, 3, dst);
GLSMBuffers.glCopyBufferSubData(GL31.GL_COPY_READ_BUFFER, GL31.GL_COPY_WRITE_BUFFER, 0, 0, COPY_BYTES);
return new StreamReadCopy(src, dst, data);
}
private static boolean hasShadow(int glId) {
final ReadbackShadows shadows = Reflect.get(BackendManager.RENDER_BACKEND, "readbackShadows");
return shadows.hasSlotsFor(glId);
}
private static void assertBytes(ByteBuffer expected, ByteBuffer actual, String what) {
for (int i = 0; i < COPY_BYTES; i++) {
assertEquals(expected.get(i), actual.get(i), what + ": byte " + i);
}
}
private static ByteBuffer solidTexture(int width, int height) {
final ByteBuffer pixels = BufferUtils.createByteBuffer(width * height * 4);
for (int i = 0; i < width * height; i++) {
@@ -1,12 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.sdlgpu.SdlAsserts;
import org.junit.jupiter.api.Test;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -19,7 +17,7 @@ class ReadbackShadowsTest {
private static final int SLOT_STRIDE = 64;
private static ReadbackShadows shadows() {
return new ReadbackShadows(null);
return new ReadbackShadows(null, null, null);
}
private static ByteBuffer out(int bytes) {
@@ -36,15 +34,24 @@ class ReadbackShadowsTest {
}
@Test
void invalidateTouchesOnlyItsOwnSlot() {
void invalidateAllTouchesOnlyItsBuffer() {
final ReadbackShadows shadows = shadows();
SdlReflect.putReadbackSlot(shadows, 7, 0, SENTINEL_HANDLE, 64, true);
SdlReflect.putReadbackSlot(shadows, 7, 64, SENTINEL_HANDLE, 64, true);
SdlReflect.putReadbackSlot(shadows, 8, 0, SENTINEL_HANDLE, 64, true);
shadows.invalidate(7, 0);
shadows.invalidateAll(7);
assertFalse(shadows.serve(7, 0, out(64), 64), "the invalidated slot is still being served");
assertTrue(SdlReflect.readbackSlotValid(shadows, 7, 64), "invalidating one ring slot must not disturb the others");
assertFalse(shadows.serve(7, 0, out(64), 64), "an invalidated slot is still being served");
assertFalse(SdlReflect.readbackSlotValid(shadows, 7, 64), "every slot of the written buffer must be invalidated");
assertTrue(SdlReflect.readbackSlotValid(shadows, 8, 0), "a write to one buffer must not invalidate another's slots");
}
@Test
void invalidateAllOfAnUnshadowedBufferDoesNotAllocate() {
final ReadbackShadows shadows = shadows();
SdlReflect.putReadbackSlot(shadows, 8, 0, SENTINEL_HANDLE, 64, true);
SdlAsserts.assertAllocationFree(() -> shadows.invalidateAll(7), "buffer writes while another buffer is shadowed");
}
@Test
@@ -60,10 +67,6 @@ class ReadbackShadowsTest {
assertTrue(shadows.hasSlotsFor(8), "deleting one buffer must not drop another's slots");
}
private static int slotCount(ReadbackShadows shadows) {
return Reflect.<Map<Long, Object>>get(shadows, "slots").size();
}
private static ReadbackShadows ringShadows(int... glIds) {
final ReadbackShadows shadows = shadows();
for (int glId : glIds) {
@@ -81,7 +84,7 @@ class ReadbackShadowsTest {
shadows.release(2);
assertFalse(shadows.hasSlotsFor(2));
assertEquals(2 * SLOTS_PER_BUFFER, slotCount(shadows));
assertEquals(2 * SLOTS_PER_BUFFER, SdlReflect.readbackSlotCount(shadows));
for (int glId = 1; glId <= 3; glId += 2) {
for (int i = 0; i < SLOTS_PER_BUFFER; i++) {
assertTrue(SdlReflect.readbackSlotValid(shadows, glId, (long) i * SLOT_STRIDE));
@@ -1,9 +1,10 @@
package com.gtnewhorizons.angelica.sdlgpu.resource;
import java.util.Map;
import java.util.Queue;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState;
public final class SdlReflect {
@@ -30,20 +31,27 @@ public final class SdlReflect {
final Object slot = Reflect.construct(slotClass);
Reflect.setDeclared(slotClass, slot, "transferBuffer", handle);
Reflect.setDeclared(slotClass, slot, "capacity", capacity);
Reflect.setDeclared(slotClass, slot, "validLen", capacity);
Reflect.setDeclared(slotClass, slot, "valid", valid);
readbackSlots(shadows).put(readbackKey(glId, offset), slot);
readbackSlots(shadows).computeIfAbsent(glId, k -> new Long2ObjectOpenHashMap<>()).put(offset, slot);
}
static boolean readbackSlotValid(ReadbackShadows shadows, int glId, long offset) {
return Reflect.get(readbackSlots(shadows).get(readbackKey(glId, offset)), "valid");
return Reflect.get(readbackSlots(shadows).get(glId).get(offset), "valid");
}
private static long readbackKey(int glId, long offset) {
return (((long) glId) << 32) | (offset & 0xFFFFFFFFL);
public static long readbackSlotSync(ReadbackShadows shadows, int glId, long offset) {
return Reflect.get(readbackSlots(shadows).get(glId).get(offset), "sync");
}
private static Map<Long, Object> readbackSlots(ReadbackShadows shadows) {
return Reflect.get(shadows, "slots");
static int readbackSlotCount(ReadbackShadows shadows) {
int count = 0;
for (Long2ObjectOpenHashMap<Object> slots : readbackSlots(shadows).values()) count += slots.size();
return count;
}
private static Int2ObjectOpenHashMap<Long2ObjectOpenHashMap<Object>> readbackSlots(ReadbackShadows shadows) {
return Reflect.get(shadows, "byBuffer");
}
static void recordBuffer(ResourceManager rm, int glId, long handle, long size, int usage) {
@@ -99,6 +99,22 @@ public class TracyOptionPages {
.build())
.add(new ActionOption(I18n.format("options.angelica.tracy.captureAction"), I18n.format("options.angelica.tracy.captureAction.tooltip"),
captureActionLabel, TracyCaptureNotifier.INSTANCE::toggle, () -> Tracy.ENABLED))
.build(),
OptionGroup.createBuilder()
.add(OptionImpl.createBuilder(boolean.class, angelicaOpts)
.setName(I18n.format("options.angelica.tracy.frameImages"))
.setTooltip(overrideTooltip("options.angelica.tracy.frameImages.tooltip", "angelica.tracy.frameImages", SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE != null))
.setControl(TickBoxControl::new)
.setBinding((opts, value) -> AngelicaConfig.tracyFrameImages = value, opts -> AngelicaConfig.tracyFrameImages)
.setEnabled(Tracy.ENABLED && SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE == null)
.build())
.add(OptionImpl.createBuilder(int.class, angelicaOpts)
.setName(I18n.format("options.angelica.tracy.frameImageRate"))
.setTooltip(overrideTooltip("options.angelica.tracy.frameImageRate.tooltip", "angelica.tracy.frameImageRate", SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE != null))
.setControl(o -> new SliderControl(o, 0, 60, 5, ControlValueFormatter.quantityOrDisabled("options.angelica.tracy.frameImageRate.value", "options.angelica.tracy.frameImageRate.everyFrame")))
.setBinding((opts, value) -> AngelicaConfig.tracyFrameImageRate = value, opts -> AngelicaConfig.tracyFrameImageRate)
.setEnabled(Tracy.ENABLED && SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE == null)
.build())
.build()));
}
@@ -556,6 +556,15 @@ public class AngelicaConfig {
@Config.RangeInt(min = 0, max = 3600)
public static int tracyCaptureSeconds;
@Config.Comment("If enabled, attaches downscaled screenshots to Tracy frames. Overridden by -Dangelica.tracy.frameImages")
@Config.DefaultBoolean(false)
public static boolean tracyFrameImages;
@Config.Comment("Tracy frame images captured per second. 0 = every frame. Overridden by -Dangelica.tracy.frameImageRate")
@Config.DefaultInt(10)
@Config.RangeInt(min = 0, max = 60)
public static int tracyFrameImageRate;
public static void applyGpuCullingMode() {
GpuCulling.setMode(gpuCullingMode == null ? GpuCullingMode.CPU_ONLY : gpuCullingMode);
}
@@ -0,0 +1,104 @@
package com.gtnewhorizons.angelica.profiling;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.glsm.CaptureGate;
import com.gtnewhorizons.angelica.glsm.GLSMFramebuffer;
import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import net.minecraft.client.Minecraft;
import net.minecraft.client.shader.Framebuffer;
public final class TracyFrameImages {
private static final int MAX_WIDTH = 320;
private static final int MAX_HEIGHT = 180;
private static final Tracy.ZoneId Z_CAPTURE = Tracy.zoneId("frameImageCapture", Tracy.COLOR_SWAP);
private static final Tracy.ZoneId Z_EMIT = Tracy.zoneId("frameImageEmit", Tracy.COLOR_SWAP);
private static final boolean HAS_ENABLED_OVERRIDE = SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE != null;
private static final boolean ENABLED_OVERRIDE = HAS_ENABLED_OVERRIDE && SystemProperties.TRACY_FRAME_IMAGES_OVERRIDE;
private static final boolean HAS_RATE_OVERRIDE = SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE != null;
private static final int RATE_OVERRIDE = HAS_RATE_OVERRIDE ? Math.max(0, SystemProperties.TRACY_FRAME_IMAGE_RATE_OVERRIDE) : 0;
private static ScreenCapture capture;
private static long frameIndex;
private static long lastCaptureNs;
private static int srcW;
private static int srcH;
private TracyFrameImages() {}
public static void onPostSwap(Minecraft mc) {
if (!enabled() || !CaptureGate.tracyConnected) {
drop();
return;
}
emitReady();
final long now = System.nanoTime();
final int rate = rate();
if (rate != 0 && now - lastCaptureNs < 1_000_000_000L / rate) return;
final Framebuffer source = mc.getFramebuffer();
final int w = source.framebufferWidth;
final int h = source.framebufferHeight;
if (w <= 0 || h <= 0) return;
if (capture == null || w != srcW || h != srcH) {
srcW = w;
srcH = h;
final long size = fitCaptureSize(w, h);
final int captureW = (int) (size >>> 32);
final int captureH = (int) size;
if (capture == null || capture.width() != captureW || capture.height() != captureH) {
if (capture != null) capture.delete();
capture = GLSMFramebuffer.createScreenCapture(captureW, captureH);
}
return;
}
Tracy.beginZone(Z_CAPTURE);
if (capture.begin(source.framebufferObject, w, h, frameIndex)) lastCaptureNs = now;
Tracy.endZone();
}
static long fitCaptureSize(int srcWidth, int srcHeight) {
final float aspect = (float) srcWidth / srcHeight;
int width = srcWidth;
int height = srcHeight;
if (width > MAX_WIDTH) {
width = MAX_WIDTH;
height = (int) (MAX_WIDTH / aspect);
}
if (height > MAX_HEIGHT) {
width = (int) (MAX_HEIGHT * aspect);
height = MAX_HEIGHT;
}
return ((long) Math.max(4, width & ~3) << 32) | Math.max(4, height & ~3);
}
public static void onFrameMark() {
frameIndex++;
}
private static void emitReady() {
if (capture == null || capture.idle()) return;
Tracy.beginZone(Z_EMIT);
while (capture.poll()) {
final long lag = frameIndex - capture.tag();
if (lag <= 255) Tracy.frameImage(capture.address(), capture.width(), capture.height(), (int) lag, true);
capture.release();
}
Tracy.endZone();
}
private static void drop() {
if (capture == null) return;
capture.delete();
capture = null;
srcW = srcH = 0;
}
private static boolean enabled() {
return HAS_ENABLED_OVERRIDE ? ENABLED_OVERRIDE : AngelicaConfig.tracyFrameImages;
}
private static int rate() {
return HAS_RATE_OVERRIDE ? RATE_OVERRIDE : AngelicaConfig.tracyFrameImageRate;
}
}
@@ -381,6 +381,12 @@ options.angelica.tracy.captureAction.tooltip=Starts or stops a Tracy capture. Sa
options.angelica.tracy.captureAction.start=Start capture
options.angelica.tracy.captureAction.stop=Stop capture
options.angelica.tracy.captureAction.saving=Saving...
options.angelica.tracy.frameImages=Frame Images
options.angelica.tracy.frameImages.tooltip=Attach small screenshots to Tracy frames for playback in Tracy Profiler. Only captured while a profiler is connected.
options.angelica.tracy.frameImageRate=Frame Image Rate
options.angelica.tracy.frameImageRate.tooltip=Frame images captured per second while connected.
options.angelica.tracy.frameImageRate.value=%s/s
options.angelica.tracy.frameImageRate.everyFrame=Every frame
pack.iris.select.title=Select
pack.iris.configure.title=Configure
@@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.mixins.early.angelica.tracy;
import com.gtnewhorizons.angelica.glsm.profiling.Tracy;
import com.gtnewhorizons.angelica.profiling.GpuFrameLagMeter;
import com.gtnewhorizons.angelica.profiling.TracyFrameImages;
import com.gtnewhorizons.angelica.profiling.TracyFramePlots;
import com.gtnewhorizons.angelica.profiling.TracyUiSections;
import net.minecraft.client.Minecraft;
@@ -21,6 +22,7 @@ public class MixinMinecraft_Tracy {
TracyFramePlots.onFrame((Minecraft) (Object) this);
GpuFrameLagMeter.onFrame();
Tracy.frameMark();
TracyFrameImages.onFrameMark();
TracyUiSections.poll(((Minecraft) (Object) this).currentScreen);
Tracy.gpuCollect();
}
@@ -43,6 +45,7 @@ public class MixinMinecraft_Tracy {
@Inject(method = "runGameLoop", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;func_147120_f()V", shift = At.Shift.AFTER))
private void angelica$tracySwapEnd(CallbackInfo ci) {
Tracy.endZone();
TracyFrameImages.onPostSwap((Minecraft) (Object) this);
}
// runGameLoop opens a second "root" section after the render phase; renaming it keeps
@@ -0,0 +1,23 @@
package com.gtnewhorizons.angelica.profiling;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TracyFrameImagesTest {
@ParameterizedTest
@CsvSource({
"1920, 1080, 320, 180",
"1920, 400, 320, 64",
"800, 1200, 120, 180",
"300, 170, 300, 168",
"2, 2, 4, 4",
})
void captureSizeFitsInsideTheTracyBoxKeepingAspect(int srcWidth, int srcHeight, int width, int height) {
final long size = TracyFrameImages.fitCaptureSize(srcWidth, srcHeight);
assertEquals(width, (int) (size >>> 32), "width for " + srcWidth + "x" + srcHeight);
assertEquals(height, (int) size, "height for " + srcWidth + "x" + srcHeight);
}
}
@@ -215,6 +215,11 @@ public final class TracyClientBackend implements TracyBackend {
JNI.invokePV(NULL, sym.frameMark);
}
@Override
public void frameImage(long rgba, int width, int height, int offset, boolean flip) {
JNI.invokePV(rgba, width, height, offset, flip ? 1 : 0, sym.frameImage);
}
@Override
public long internFrameName(String name) {
return memAddress(MemoryUtil.memUTF8(name));
@@ -21,6 +21,7 @@ final class TracyLibrary {
final long zoneText;
final long zoneValue;
final long frameMark;
final long frameImage;
final long plot;
final long plotInt;
final long plotConfig;
@@ -58,6 +59,7 @@ final class TracyLibrary {
zoneText = req(lib, "ang_zone_text");
zoneValue = req(lib, "ang_zone_value");
frameMark = req(lib, "___tracy_emit_frame_mark");
frameImage = req(lib, "___tracy_emit_frame_image");
plot = req(lib, "ang_plot");
plotInt = req(lib, "___tracy_emit_plot_int");
plotConfig = req(lib, "___tracy_emit_plot_config");