mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-08 12:35:01 +00:00
Add Tracy frame images (#2326)
This commit is contained in:
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);
|
||||
|
||||
+7
@@ -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;
|
||||
}
|
||||
+62
-45
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
-49
@@ -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;
|
||||
|
||||
+215
@@ -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++) {
|
||||
|
||||
+15
-12
@@ -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
|
||||
|
||||
+3
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user