Compare commits

..
7 Commits
68 changed files with 2495 additions and 406 deletions

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");
@@ -77,6 +79,8 @@ public final class SystemProperties {
public static final Boolean FLYBY_FPS_GRAPH = parseBooleanOverride("angelica.flyby.fpsGraph");
public static final boolean FLYBY_DEBUG_DUMP = Boolean.getBoolean("angelica.flyby.debugDump");
public static final String FLYBY_CRASH_TEST = System.getProperty("angelica.flyby.crashTest", "");
public static final boolean FLYBY_HEAP_CHECK = Boolean.getBoolean("angelica.flyby.heapCheck");
public static final int FLYBY_VISITS = Integer.getInteger("angelica.flyby.visits", 1);
// Debug
public static final boolean LWJGL_DEBUG = Boolean.getBoolean("org.lwjgl.util.Debug");
@@ -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,7 +1,7 @@
package com.gtnewhorizons.angelica.glsm;
/*
* Ported from Mesa's util_idalloc (src/util/u_idalloc.c).
* Ported from Mesa's util_idalloc and util_idalloc_sparse (src/util/u_idalloc.c).
*
* Original copyright and license:
*
@@ -32,17 +32,15 @@ package com.gtnewhorizons.angelica.glsm;
*/
/**
* Bitmap ID allocator for display list IDs.
*
* <p>Bitmap-based: each bit in the {@code data} array represents one ID.
* Bit set = allocated, bit clear = free. ID 0 is reserved (never returned).
* Sparse bitmap ID allocator for display list IDs; ID 0 is reserved (never returned).
*/
public class DisplayListIDAllocator {
private int[] data;
private int numElements; // number of uint32 words allocated
private int numSetElements; // highest word index with any bit set + 1
private int lowestFreeIdx; // optimization: skip fully-allocated words
private static final int SEGMENT_SHIFT = 22;
private static final int SEGMENT_MASK = (1 << SEGMENT_SHIFT) - 1;
private static final int WORDS_PER_SEGMENT = (1 << SEGMENT_SHIFT) / 32;
private final Segment[] segments = new Segment[512];
public DisplayListIDAllocator() {
this(32); // 1024 IDs initially
@@ -50,138 +48,191 @@ public class DisplayListIDAllocator {
public DisplayListIDAllocator(int initialWords) {
if (initialWords < 1) initialWords = 1;
this.data = new int[initialWords];
this.numElements = initialWords;
this.numSetElements = 0;
this.lowestFreeIdx = 0;
final Segment first = new Segment(initialWords);
// Reserve ID 0 (never returned), matching Mesa's skip_zero pattern
data[0] = 1;
numSetElements = 1;
first.data[0] = 1;
first.numSetElements = 1;
segments[0] = first;
}
private Segment segment(int index) {
Segment s = segments[index];
if (s == null) {
s = new Segment(1);
segments[index] = s;
}
return s;
}
public synchronized int allocRange(int count) {
if (count <= 0) throw new IllegalArgumentException("count must be > 0");
if (count == 1) return allocSingle();
return allocMultiple(count);
}
private int allocSingle() {
for (int i = lowestFreeIdx; i < numElements; i++) {
if (data[i] == 0xFFFFFFFF) continue;
int bit = Integer.numberOfTrailingZeros(~data[i]);
data[i] |= 1 << bit;
lowestFreeIdx = i;
numSetElements = Math.max(numSetElements, i + 1);
return i * 32 + bit;
}
// No slots available, grow. Record old end, then mark first bit there.
int oldNumElements = numElements;
resize(Math.max(numElements, 1) * 2);
lowestFreeIdx = oldNumElements;
data[oldNumElements] |= 1;
numSetElements = Math.max(numSetElements, oldNumElements + 1);
return oldNumElements * 32;
}
private int allocMultiple(int num) {
int numWords = divRoundUp(num, 32);
int base = findFreeBlock(lowestFreeIdx);
boolean found = false;
while (!found) {
int i;
for (i = base; i < numElements && i - base < numWords && data[i] == 0; i++)
;
if (i - base == numWords) {
found = true;
} else if (i == numElements) {
// Hit end — base is valid: data[base..end] are zero, resize extends the run into newly zeroed words.
resize(numElements * 2 + numWords);
found = true;
} else {
// Non-zero word at i, skip past it
base = i + 1;
if (count == 1) {
for (int i = 0; i < segments.length; i++) {
final Segment s = segment(i);
if (s.lowestFreeIdx < WORDS_PER_SEGMENT) return (i << SEGMENT_SHIFT) | s.allocSingle();
}
throw new IllegalStateException("all display list IDs are in use");
}
// Mark bits as used
int fullWords = num / 32;
int remainder = num % 32;
for (int i = base; i < base + fullWords; i++) {
data[i] = 0xFFFFFFFF;
final int numWords = Segment.divRoundUp(count, 32);
for (int i = 0; i < segments.length; i++) {
final Segment s = segment(i);
if (s.lowestFreeIdx + numWords > WORDS_PER_SEGMENT) continue;
final int base = s.allocMultiple(count);
if (base + count <= 1 << SEGMENT_SHIFT) return (i << SEGMENT_SHIFT) | base;
for (int j = 0; j < count; j++) s.free(base + j);
}
if (remainder != 0) {
data[base + fullWords] |= (1 << remainder) - 1;
}
if (lowestFreeIdx == base) {
lowestFreeIdx = base + num / 32;
}
numSetElements = Math.max(numSetElements, base + numWords);
return base * 32;
throw new IllegalStateException("no free consecutive range of " + count + " display list IDs");
}
private int findFreeBlock(int start) {
for (int i = start; i < numElements; i++) {
if (data[i] == 0) return i;
}
return numElements;
public synchronized void reserve(int id) {
if (id <= 0) return;
segment(id >>> SEGMENT_SHIFT).reserve(id & SEGMENT_MASK);
}
public synchronized void free(int id) {
if (id <= 0) return; // ID 0 is reserved
int idx = id / 32;
if (idx >= numElements) return;
lowestFreeIdx = Math.min(idx, lowestFreeIdx);
data[idx] &= ~(1 << (id % 32));
if (numSetElements == idx + 1) {
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
final Segment s = segments[id >>> SEGMENT_SHIFT];
if (s != null) s.free(id & SEGMENT_MASK);
}
public synchronized void freeRange(int base, int count) {
if (count <= 0 || base <= 0) return;
int startIdx = base / 32;
if (startIdx >= numElements) return;
int end = base + count;
for (int id = base; id < end; id++) {
int idx = id / 32;
if (idx >= numElements) break;
data[idx] &= ~(1 << (id % 32));
}
lowestFreeIdx = Math.min(startIdx, lowestFreeIdx);
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
final long last = Math.min((long) base + count - 1, Integer.MAX_VALUE);
long id = base;
while (id <= last) {
final int index = (int) (id >>> SEGMENT_SHIFT);
final Segment s = segments[index];
if (s == null || (id & SEGMENT_MASK) >= (long) s.numElements * 32) {
id = (long) (index + 1) << SEGMENT_SHIFT;
continue;
}
s.free((int) id & SEGMENT_MASK);
id++;
}
}
public synchronized boolean isAllocated(int id) {
if (id <= 0) return false;
int idx = id / 32;
if (idx >= numSetElements) return false;
return (data[idx] & (1 << (id % 32))) != 0;
final Segment s = segments[id >>> SEGMENT_SHIFT];
return s != null && s.isAllocated(id & SEGMENT_MASK);
}
private void resize(int newNumElements) {
if (newNumElements <= numElements) return;
int[] newData = new int[newNumElements];
System.arraycopy(data, 0, newData, 0, numElements);
data = newData;
numElements = newNumElements;
}
private static final class Segment {
private int[] data;
private int numElements; // number of uint32 words allocated
private int numSetElements; // highest word index with any bit set + 1
private int lowestFreeIdx; // optimization: skip fully-allocated words
private static int divRoundUp(int a, int b) {
return (a + b - 1) / b;
Segment(int initialWords) {
this.data = new int[initialWords];
this.numElements = initialWords;
}
int allocSingle() {
for (int i = lowestFreeIdx; i < numElements; i++) {
if (data[i] == 0xFFFFFFFF) continue;
int bit = Integer.numberOfTrailingZeros(~data[i]);
data[i] |= 1 << bit;
lowestFreeIdx = i;
numSetElements = Math.max(numSetElements, i + 1);
return i * 32 + bit;
}
// No slots available, grow. Record old end, then mark first bit there.
int oldNumElements = numElements;
resize(Math.max(numElements, 1) * 2);
lowestFreeIdx = oldNumElements;
data[oldNumElements] |= 1;
numSetElements = Math.max(numSetElements, oldNumElements + 1);
return oldNumElements * 32;
}
int allocMultiple(int num) {
int numWords = divRoundUp(num, 32);
int base = findFreeBlock(lowestFreeIdx);
boolean found = false;
while (!found) {
int i;
for (i = base; i < numElements && i - base < numWords && data[i] == 0; i++)
;
if (i - base == numWords) {
found = true;
} else if (i == numElements) {
// Hit end — base is valid: data[base..end] are zero, resize extends the run into newly zeroed words.
resize(numElements * 2 + numWords);
found = true;
} else {
// Non-zero word at i, skip past it
base = i + 1;
}
}
// Mark bits as used
int fullWords = num / 32;
int remainder = num % 32;
for (int i = base; i < base + fullWords; i++) {
data[i] = 0xFFFFFFFF;
}
if (remainder != 0) {
data[base + fullWords] |= (1 << remainder) - 1;
}
if (lowestFreeIdx == base) {
lowestFreeIdx = base + num / 32;
}
numSetElements = Math.max(numSetElements, base + numWords);
return base * 32;
}
int findFreeBlock(int start) {
for (int i = start; i < numElements; i++) {
if (data[i] == 0) return i;
}
return numElements;
}
void free(int id) {
int idx = id / 32;
if (idx >= numElements) return;
lowestFreeIdx = Math.min(idx, lowestFreeIdx);
data[idx] &= ~(1 << (id % 32));
if (numSetElements == idx + 1) {
while (numSetElements > 0 && data[numSetElements - 1] == 0) {
numSetElements--;
}
}
}
boolean isAllocated(int id) {
int idx = id / 32;
if (idx >= numSetElements) return false;
return (data[idx] & (1 << (id % 32))) != 0;
}
void reserve(int id) {
final int idx = id / 32;
if (idx >= numElements) resize((idx + 1) * 2);
data[idx] |= 1 << (id % 32);
numSetElements = Math.max(numSetElements, idx + 1);
}
private void resize(int newNumElements) {
if (newNumElements <= numElements) return;
int[] newData = new int[newNumElements];
System.arraycopy(data, 0, newData, 0, numElements);
data = newData;
numElements = newNumElements;
}
private static int divRoundUp(int a, int b) {
return (a + b - 1) / b;
}
}
}
@@ -1046,9 +1046,12 @@ public class DisplayListManager {
}
final boolean locked = GLStateManager.acquireDrawLock();
try {
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
synchronized (GLSMDraws.displayListIdAllocator) {
final CompiledDisplayList previous = displayListCache.put(glListId, compiled);
if (previous != null && previous != CompiledDisplayList.EMPTY && previous != compiled) {
previous.delete();
}
GLSMDraws.displayListIdAllocator.reserve(glListId);
}
} finally {
if (locked) GLStateManager.releaseDrawLock();
@@ -110,7 +110,7 @@ public final class GLSMAttribStack {
if (!vaos.vaoMap.containsKey(saved.boundVAO)) return;
GLSMVertexInput.bindVertexArrayInternal(glCtx, saved.boundVAO);
glCtx.clientActiveTextureUnit = saved.clientActiveTextureUnit;
if (vaos.isBufferLive(saved.boundVBO, saved)) {
if (vaos.isSavedVboLive(saved)) {
final int changed = vaos.restoreClientArrays(saved);
for (int pointers = changed & 0xFFFF; pointers != 0; pointers &= pointers - 1) {
GLSMVertexInput.pointAttrib(glCtx, Integer.numberOfTrailingZeros(pointers));
@@ -120,7 +120,7 @@ public final class GLSMAttribStack {
}
if (glCtx.boundVBO != saved.boundVBO) GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, saved.boundVBO);
}
if (vaos.boundEBO != saved.boundEBO && vaos.isBufferLive(saved.boundEBO, saved)) GLSMBuffers.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, saved.boundEBO);
if (vaos.boundEBO != saved.boundEBO && vaos.isSavedEboLive(saved)) GLSMBuffers.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, saved.boundEBO);
}
public static void pushState(int mask) {
@@ -64,9 +64,12 @@ public final class GLSMBuffers {
}
}
public static long bufferDeletes;
private static void invalidateDeletedBuffer(int buffer) {
final GLContextState glCtx = ctx();
if (buffer == 0) return;
bufferDeletes++;
final boolean locked = acquireDrawLock();
try {
FfpExtendedAttribs.onDeleteBuffer(glCtx, buffer);
@@ -87,7 +90,7 @@ public final class GLSMBuffers {
}
writeMappedBuffers.remove(buffer);
glCtx.vaos.onDeleteBuffer(buffer);
glCtx.vaos.onDeleteBuffer(buffer, glCtx.clientAttribSavedArrays);
} finally {
if (locked) releaseDrawLock();
}
@@ -481,7 +481,7 @@ public final class GLSMDraws {
DisplayListManager.glEndList();
}
private static final DisplayListIDAllocator displayListIdAllocator = new DisplayListIDAllocator();
static final DisplayListIDAllocator displayListIdAllocator = new DisplayListIDAllocator();
public static int glGenLists(int range) {
rejectExitCapture();
@@ -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);
}
@@ -123,15 +123,63 @@ public final class VAOManager {
private int clientSideEnabledCount = 0;
private int arrayVersion = 0;
private int versionClock = 0;
private static final int DELETED_NAME_PRUNE_THRESHOLD = 4096;
private static final int DEAD_EPOCH = -1;
private final int pruneThreshold;
private int bufferEpoch = 0;
private final Int2IntOpenHashMap bufferDeletedAt = new Int2IntOpenHashMap();
public VAOManager() {
this(DELETED_NAME_PRUNE_THRESHOLD);
}
VAOManager(int pruneThreshold) {
this.pruneThreshold = pruneThreshold;
}
private void touch() {
arrayVersion = ++versionClock;
}
private boolean isBufferLive(int buffer, int sinceEpoch) {
return bufferDeletedAt.get(buffer) <= sinceEpoch;
return buffer == 0 || bufferDeletedAt.get(buffer) <= sinceEpoch;
}
private int retiredEpoch(int buffer, int epoch, boolean rebase) {
if (rebase) return epoch == DEAD_EPOCH ? DEAD_EPOCH : 0;
return isBufferLive(buffer, epoch) ? epoch : DEAD_EPOCH;
}
private void retire(@Nullable Attrib[] slots, boolean rebase) {
if (slots == null) return;
for (final Attrib a : slots) {
if (a != null) a.epoch = retiredEpoch(a.vboId, a.epoch, rebase);
}
}
private void retireAll(ClientArrays[] savedStack, boolean rebase) {
for (final VAOData data : vaoMap.values()) {
retire(data.attribs, rebase);
retire(data.generic, rebase);
retire(data.conventional, rebase);
}
retire(currentAttribs, rebase);
retire(currentGeneric, rebase);
retire(currentConventional, rebase);
for (final ClientArrays saved : savedStack) {
if (saved == null) continue;
saved.vboEpoch = retiredEpoch(saved.boundVBO, saved.vboEpoch, rebase);
saved.eboEpoch = retiredEpoch(saved.boundEBO, saved.eboEpoch, rebase);
retire(saved.generic, rebase);
retire(saved.conventional, rebase);
}
}
private void pruneDeletedNames(ClientArrays[] savedStack) {
retireAll(savedStack, false);
bufferDeletedAt.clear();
retireAll(savedStack, true);
bufferEpoch = 0;
}
public boolean isBufferLive(Attrib a) {
@@ -206,9 +254,10 @@ public final class VAOManager {
return detached;
}
public void onDeleteBuffer(int buffer) {
public void onDeleteBuffer(int buffer, ClientArrays[] savedStack) {
if (buffer == 0) return;
bufferDeletedAt.put(buffer, ++bufferEpoch);
if (bufferEpoch >= pruneThreshold) pruneDeletedNames(savedStack);
if (currentAttribs != null) {
boolean detached = detach(currentGeneric != null ? currentGeneric : currentAttribs, buffer);
if (currentConventional != null) detached |= detach(currentConventional, buffer);
@@ -447,7 +496,8 @@ public final class VAOManager {
public int boundEBO;
public int clientActiveTextureUnit;
int arrayVersion;
int bufferEpoch;
int vboEpoch;
int eboEpoch;
int vertexFlags;
int genericEnabled;
int conventionalEnabled;
@@ -463,13 +513,18 @@ public final class VAOManager {
saved[i].copyPointer(source);
}
public boolean isBufferLive(int buffer, ClientArrays saved) {
return isBufferLive(buffer, saved.bufferEpoch);
public boolean isSavedVboLive(ClientArrays saved) {
return isBufferLive(saved.boundVBO, saved.vboEpoch);
}
public boolean isSavedEboLive(ClientArrays saved) {
return isBufferLive(saved.boundEBO, saved.eboEpoch);
}
public void saveClientArrays(ClientArrays out) {
out.arrayVersion = arrayVersion;
out.bufferEpoch = bufferEpoch;
out.vboEpoch = bufferEpoch;
out.eboEpoch = bufferEpoch;
out.vertexFlags = currentVertexFlags;
out.genericEnabled = genericEnabled;
out.conventionalEnabled = conventionalEnabled;
@@ -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);
@@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm.recording;
import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.hooks.ImmediateExtendedAttribHandler;
import com.gtnewhorizons.angelica.glsm.streaming.LineQuads;
import org.lwjgl.opengl.GL11;
@@ -99,26 +100,31 @@ public final class DisplayListVBO {
public void render() {
if (vao == null) return;
if (lineQuads != null && GLSMDraws.ffpWidensLineQuads()) {
renderLineQuads();
return;
}
vao.bind();
if (pendingExtVbo != 0) {
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, pendingExtVbo);
ImmediateExtendedAttribHandler.setupExtAttribPointers(0L, ImmediateExtendedAttribHandler.EXT_STRIDE);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
pendingExtVbo = 0;
}
if (count <= 0) { vao.unbind(); return; }
VAOManager.setCurrentVertexFlags(vertexFlags);
FfpExtendedAttribs.beginInternalDraw();
final int prevVAO = GLSMVertexInput.getBoundVAO();
try {
vao.draw(drawMode, start, count);
if (lineQuads != null && GLSMDraws.ffpWidensLineQuads()) {
renderLineQuads();
return;
}
vao.bind();
if (pendingExtVbo != 0) {
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, pendingExtVbo);
ImmediateExtendedAttribHandler.setupExtAttribPointers(0L, ImmediateExtendedAttribHandler.EXT_STRIDE);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
pendingExtVbo = 0;
}
if (count <= 0) { vao.unbind(); return; }
VAOManager.setCurrentVertexFlags(vertexFlags);
FfpExtendedAttribs.beginInternalDraw();
try {
vao.draw(drawMode, start, count);
} finally {
FfpExtendedAttribs.endInternalDraw();
}
vao.unbind();
} finally {
FfpExtendedAttribs.endInternalDraw();
GLSMVertexInput.glBindVertexArray(prevVAO);
}
vao.unbind();
}
private void renderLineQuads() {
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.glsm.recording.commands;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
public final class BatchedIndexedDrawCmd implements DisplayListCommand {
private IndexedDrawBatch batch;
@@ -25,8 +26,13 @@ public final class BatchedIndexedDrawCmd implements DisplayListCommand {
@Override
public void execute() {
if (batch == null) return;
batch.bind();
GLSMDraws.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
final int prevVAO = GLSMVertexInput.getBoundVAO();
try {
batch.bind();
GLSMDraws.glDrawElements(drawMode, indexCount, indexType, indexByteOffset);
} finally {
GLSMVertexInput.glBindVertexArray(prevVAO);
}
}
@Override
@@ -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",
@@ -158,6 +158,8 @@ final class GLSMOwners {
static final String[][] FIELDS = {
{"com/gtnewhorizons/angelica/glsm/GLSMAttribStack",
"attribBackendCalls", "attribDiscards", "attribPushes", "attribSlotsSaved", "attribValueRestores"},
{"com/gtnewhorizons/angelica/glsm/GLSMBuffers",
"bufferDeletes"},
{"com/gtnewhorizons/angelica/glsm/GLSMDraws",
"drawCalls"},
{"com/gtnewhorizons/angelica/glsm/GLSMFramebuffer",
@@ -241,4 +241,28 @@ class DisplayListIdAllocatorTest {
int base2 = alloc.allocRange(64);
assertEquals(base1, base2, "Freed range should be reused");
}
@Test
void reserveRoutesByIdWithoutDisturbingLowIds() {
final DisplayListIDAllocator alloc = new DisplayListIDAllocator();
alloc.reserve(-5);
alloc.reserve(0);
alloc.reserve(Integer.MAX_VALUE);
assertTrue(alloc.isAllocated(Integer.MAX_VALUE));
assertFalse(alloc.isAllocated(Integer.MAX_VALUE & ((1 << 22) - 1)));
assertEquals(1, alloc.allocRange(1));
alloc.free(Integer.MAX_VALUE);
assertFalse(alloc.isAllocated(Integer.MAX_VALUE));
}
@Test
void rangeAllocationDoesNotSpanSegments() {
final DisplayListIDAllocator alloc = new DisplayListIDAllocator();
assertEquals(32, alloc.allocRange((1 << 22) - 64));
assertEquals(1 << 22, alloc.allocRange(64));
assertFalse(alloc.isAllocated((1 << 22) - 32));
assertTrue(alloc.isAllocated(1 << 22));
alloc.freeRange((1 << 22) - 1, 2);
assertFalse(alloc.isAllocated(1 << 22));
}
}
@@ -487,29 +487,39 @@ public class GLSM_ClientArrays_GLTest {
}
@Test
void popKeepsCurrentArraysWhenTheSavedArrayBufferWasDeleted() {
void popKeepsCurrentStateWhenTheSavedBuffersWereDeleted() {
final int vbo2 = GLSMBuffers.glGenBuffers();
final int ebo = GLSMBuffers.glGenBuffers();
final int ebo2 = GLSMBuffers.glGenBuffers();
try {
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLSMBuffers.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo);
GLSMTextures.glClientActiveTexture(GL13.GL_TEXTURE0);
GLSMVertexInput.glVertexPointer(3, GL11.GL_FLOAT, 12, 0L);
GLSMAttribStack.glPushClientAttrib(GL11.GL_CLIENT_VERTEX_ARRAY_BIT);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo2);
GLSMBuffers.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, ebo2);
GLSMVertexInput.glVertexPointer(2, GL11.GL_SHORT, 4, 0L);
GLSMTextures.glClientActiveTexture(GL13.GL_TEXTURE1);
GLSMBuffers.glDeleteBuffers(vbo);
GLSMBuffers.glDeleteBuffers(ebo);
GLSMAttribStack.glPopClientAttrib();
assertEquals(2, GLSMQueries.glGetInteger(GL11.GL_VERTEX_ARRAY_SIZE), "array state is not restored once its array buffer is gone");
assertEquals(vbo2, GLSMQueries.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING));
assertEquals(vbo2, GL11.glGetInteger(GL15.GL_ARRAY_BUFFER_BINDING));
assertEquals(ebo2, GLSMQueries.glGetInteger(GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING), "a deleted element buffer is not rebound by the pop");
assertEquals(ebo2, GL11.glGetInteger(GL15.GL_ELEMENT_ARRAY_BUFFER_BINDING));
assertEquals(GL13.GL_TEXTURE0, GLSMQueries.glGetInteger(GL13.GL_CLIENT_ACTIVE_TEXTURE));
} finally {
drainClientAttribStack();
GLSMTextures.glClientActiveTexture(GL13.GL_TEXTURE0);
GLSMBuffers.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
GLSMBuffers.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
GLSMBuffers.glDeleteBuffers(vbo2);
GLSMBuffers.glDeleteBuffers(ebo2);
GLSMBuffers.glDeleteBuffers(ebo);
}
}
@@ -521,6 +531,12 @@ public class GLSM_ClientArrays_GLTest {
GLSMDraws.glNewList(list, GL11.GL_COMPILE);
GLSMAttribStack.glPushClientAttrib(GL11.GL_CLIENT_VERTEX_ARRAY_BIT);
GLSMAttribStack.glPopClientAttrib();
GLSMDraws.glBegin(GL11.GL_QUADS);
GLSMDraws.glVertex2f(-1f, -1f);
GLSMDraws.glVertex2f(1f, -1f);
GLSMDraws.glVertex2f(1f, 1f);
GLSMDraws.glVertex2f(-1f, 1f);
GLSMDraws.glEnd();
GLSMDraws.glEndList();
GLSMVertexInput.glBindVertexArray(other);
@@ -14,6 +14,7 @@ import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.assertModelv
import static com.gtnewhorizons.angelica.glsm.DisplayListTestHelper.modelviewAfterCall;
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.assertTrue;
/**
@@ -209,6 +210,25 @@ class GLSM_DisplayList_Stress_Test {
assertEquals(10.0f, modelviewAfterCall(list).get(12), 0.0001f, "Recompilation should translate by 10");
}
@Test
void compiledNameOutsideGeneratedRangeIsNotReissued() {
final int base = GLSMDraws.glGenLists(1);
int extra = base + 1;
while (GLSMDraws.glIsList(extra)) extra++;
testLists.add(base);
testLists.add(extra);
GLSMDraws.glNewList(extra, GL11.GL_COMPILE);
GLSMMatrices.glTranslatef(5.0f, 0.0f, 0.0f);
GLSMDraws.glEndList();
final int next = GLSMDraws.glGenLists(1);
testLists.add(next);
assertNotEquals(extra, next, "glGenLists must not reissue a name installed by glEndList");
GLSMDraws.glDeleteLists(extra, 1);
assertFalse(GLSMDraws.glIsList(extra), "glDeleteLists must release a reserved name");
}
@Test
void testComplexTransformSequence() {
// Test a complex sequence of transforms that could expose numerical issues
@@ -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,10 +1,14 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import java.util.Random;
@@ -15,6 +19,7 @@ public class VAOManagerEboIndexTest {
private static final int DEFAULT_VAO = 0;
private static final int NO_VAO = -1;
private static final VAOManager.ClientArrays[] NO_SAVED = new VAOManager.ClientArrays[0];
private final VAOManager vaos = new VAOManager();
private final Int2IntOpenHashMap expectedEbo = new Int2IntOpenHashMap();
@@ -62,7 +67,7 @@ public class VAOManagerEboIndexTest {
}
private void deleteBuffer(int buffer) {
vaos.onDeleteBuffer(buffer);
vaos.onDeleteBuffer(buffer, NO_SAVED);
for (int vao : expectedEbo.keySet().toIntArray()) {
if (expectedEbo.get(vao) == buffer) expectedEbo.put(vao, 0);
}
@@ -126,4 +131,190 @@ public class VAOManagerEboIndexTest {
}
assertTrue(sharedEboDeletes > 20, "op stream never freed a multi-owner EBO (hits: " + sharedEboDeletes + ")");
}
private static final int PRUNE_INTERVAL = 4;
private static final int OPS = 40_000;
private static final int SLOTS = 4;
private static final int[] CONVS = {VAOManager.CONV_POSITION, VAOManager.CONV_COLOR, VAOManager.CONV_SECONDARY_COLOR, VAOManager.CONV_TEX0};
private static final String[] CURRENT_FIELDS = {"currentAttribs", "currentGeneric", "currentConventional"};
private static final int STACK_DEPTH = 16;
private static final int MAX_VAOS = 8;
private static final int MAX_LIVE_BUFFERS = 24;
private final VAOManager pruned = new VAOManager(PRUNE_INTERVAL);
private final VAOManager oracle = new VAOManager(OPS + 1);
private final VAOManager[] pair = {pruned, oracle};
private final VAOManager.ClientArrays[][] stacks = new VAOManager.ClientArrays[pair.length][STACK_DEPTH];
private int deadSlotChecks;
private static int pick(Random random, IntArrayList list) {
return list.getInt(random.nextInt(list.size()));
}
private int staleName(Random random, IntArrayList vaoIds, IntLinkedOpenHashSet freeNames) {
final VAOManager.VAOData data = oracle.vaoMap.get(pick(random, vaoIds));
final VAOManager.Attrib[] slots = random.nextBoolean() ? data.attribs : data.conventional;
if (slots == null) return 0;
final VAOManager.Attrib a = slots[random.nextInt(slots.length)];
return a != null && a.vboId != 0 && freeNames.remove(a.vboId) ? a.vboId : 0;
}
private void bindBoth(int vao) {
if (vao == currentVao) return;
for (VAOManager m : pair) m.onBindVertexArrayPre(vao);
currentVao = vao;
}
private static void restore(VAOManager m, VAOManager.ClientArrays saved) {
if (m.isSavedVboLive(saved)) m.restoreClientArrays(saved);
if (m.boundEBO != saved.boundEBO && m.isSavedEboLive(saved)) m.onBindEBO(saved.boundEBO);
}
private static String where(int op, String kind, int id, int slot) {
return "op " + op + ": " + kind + " " + id + " slot " + slot;
}
private void assertSameSlot(int op, String kind, int id, int slot, VAOManager.Attrib p, VAOManager.Attrib o, boolean countDead) {
assertEquals(o == null, p == null, () -> where(op, kind, id, slot) + " presence");
if (o == null) return;
assertEquals(o.vboId, p.vboId, () -> where(op, kind, id, slot) + " vbo");
final boolean live = oracle.isBufferLive(o);
assertEquals(live, pruned.isBufferLive(p), () -> where(op, kind, id, slot) + " liveness of buffer " + o.vboId);
if (countDead && !live && o.vboId != 0) deadSlotChecks++;
}
private void assertSameSlots(int op, String kind, int id, VAOManager.Attrib[] p, VAOManager.Attrib[] o, boolean countDead) {
assertEquals(o == null, p == null, () -> where(op, kind, id, -1) + " array presence");
if (o == null) return;
for (int i = 0; i < o.length; i++) assertSameSlot(op, kind, id, i, p[i], o[i], countDead);
}
private void assertSameLiveness(int op, int sp) {
assertEquals(oracle.vaoMap.keySet(), pruned.vaoMap.keySet(), () -> "op " + op + ": vao sets differ");
for (var entry : oracle.vaoMap.int2ObjectEntrySet()) {
final int vao = entry.getIntKey();
final VAOManager.VAOData o = entry.getValue();
final VAOManager.VAOData p = pruned.vaoMap.get(vao);
final boolean countDead = vao != currentVao;
assertSameSlots(op, "vao attribs", vao, p.attribs, o.attribs, countDead);
assertSameSlots(op, "vao generic", vao, p.generic, o.generic, countDead);
assertSameSlots(op, "vao conventional", vao, p.conventional, o.conventional, countDead);
}
for (String field : CURRENT_FIELDS) {
assertSameSlots(op, field, currentVao, Reflect.get(pruned, field), Reflect.get(oracle, field), false);
}
for (int s = 0; s < sp; s++) {
final VAOManager.ClientArrays p = stacks[0][s];
final VAOManager.ClientArrays o = stacks[1][s];
final int depth = s;
assertEquals(oracle.isSavedVboLive(o), pruned.isSavedVboLive(p), () -> "op " + op + ": saved " + depth + " vbo liveness");
assertEquals(oracle.isSavedEboLive(o), pruned.isSavedEboLive(p), () -> "op " + op + ": saved " + depth + " ebo liveness");
for (int i = 0; i < VAOManager.MAX_ATTRIBS; i++) {
if ((o.genericPresent >> i & 1) != 0) assertSameSlot(op, "saved generic", s, i, p.generic[i], o.generic[i], true);
}
for (int i = 0; i < VAOManager.CONV_COUNT; i++) {
if ((o.conventionalPresent >> i & 1) != 0) assertSameSlot(op, "saved conventional", s, i, p.conventional[i], o.conventional[i], true);
}
}
}
@Test
void prunedDeletedNamesAnswerLivenessLikeAnUnprunedReference() {
final Random random = new Random(0x5EED);
for (VAOManager m : pair) m.init(DEFAULT_VAO);
for (VAOManager.ClientArrays[] stack : stacks) {
for (int s = 0; s < STACK_DEPTH; s++) stack[s] = new VAOManager.ClientArrays();
}
final IntArrayList vaoIds = IntArrayList.of(DEFAULT_VAO);
final IntArrayList liveBuffers = new IntArrayList();
final IntLinkedOpenHashSet freeNames = new IntLinkedOpenHashSet();
final IntOpenHashSet everDeleted = new IntOpenHashSet();
int sp = 0;
int nextVao = 1;
int nextBuffer = 1;
int prunes = 0;
int reusedDeletes = 0;
for (int op = 0; op < OPS; op++) {
final int epochBefore = Reflect.get(pruned, "bufferEpoch");
switch (random.nextInt(11)) {
case 0, 1 -> {
if (liveBuffers.size() >= MAX_LIVE_BUFFERS) break;
final int stale = random.nextInt(3) == 0 ? staleName(random, vaoIds, freeNames) : 0;
liveBuffers.add(stale != 0 ? stale : !freeNames.isEmpty() && random.nextInt(3) == 0 ? freeNames.removeLastInt() : nextBuffer++);
}
case 2, 3 -> {
if (liveBuffers.isEmpty()) break;
final int buffer = liveBuffers.removeInt(random.nextInt(liveBuffers.size()));
if (!everDeleted.add(buffer)) reusedDeletes++;
for (int m = 0; m < pair.length; m++) {
if (pair[m].boundEBO == buffer) pair[m].onBindEBO(0);
pair[m].onDeleteBuffer(buffer, stacks[m]);
}
freeNames.add(buffer);
}
case 4 -> {
final int index = random.nextInt(SLOTS);
final int buffer = liveBuffers.isEmpty() || random.nextInt(8) == 0 ? 0 : pick(random, liveBuffers);
for (VAOManager m : pair) m.setAttribute(index, 4, GL11.GL_FLOAT, false, 0, 0L, buffer);
}
case 5 -> {
final int conv = CONVS[random.nextInt(CONVS.length)];
final int buffer = liveBuffers.isEmpty() || random.nextInt(8) == 0 ? 0 : pick(random, liveBuffers);
for (VAOManager m : pair) m.setConventional(conv, 4, GL11.GL_FLOAT, false, 0, 0L, buffer);
}
case 6 -> {
final int vao;
if (vaoIds.size() < MAX_VAOS && random.nextBoolean()) {
vao = nextVao++;
vaoIds.add(vao);
} else {
vao = pick(random, vaoIds);
}
bindBoth(vao);
}
case 7 -> {
final int ebo = liveBuffers.isEmpty() || random.nextInt(4) == 0 ? 0 : pick(random, liveBuffers);
for (VAOManager m : pair) m.onBindEBO(ebo);
}
case 8 -> {
if (sp == STACK_DEPTH) break;
final int vbo = liveBuffers.isEmpty() || random.nextInt(4) == 0 ? 0 : pick(random, liveBuffers);
for (int m = 0; m < pair.length; m++) {
final VAOManager.ClientArrays saved = stacks[m][sp];
saved.boundVAO = currentVao;
saved.boundVBO = vbo;
saved.boundEBO = pair[m].boundEBO;
pair[m].saveClientArrays(saved);
}
sp++;
}
case 9 -> {
if (sp == 0) break;
sp--;
final int vao = stacks[1][sp].boundVAO;
if (!oracle.vaoMap.containsKey(vao)) break;
bindBoth(vao);
for (int m = 0; m < pair.length; m++) restore(pair[m], stacks[m][sp]);
}
case 10 -> {
if (vaoIds.size() == 1) break;
final int vao = vaoIds.removeInt(1 + random.nextInt(vaoIds.size() - 1));
for (VAOManager m : pair) m.onDeleteVertexArray(vao);
if (vao == currentVao) {
currentVao = NO_VAO;
bindBoth(DEFAULT_VAO);
}
}
}
final int epoch = Reflect.get(pruned, "bufferEpoch");
final int at = op;
assertTrue(epoch < PRUNE_INTERVAL, () -> "op " + at + ": epoch " + epoch + " escaped the prune interval");
if (epoch < epochBefore) prunes++;
assertSameLiveness(op, sp);
}
assertTrue(prunes >= 800, "op stream pruned only " + prunes + " times");
assertTrue(deadSlotChecks >= 400_000, "op stream compared only " + deadSlotChecks + " dead slots");
assertTrue(reusedDeletes >= 1_000, "op stream deleted only " + reusedDeletes + " reused names");
}
}
@@ -13,6 +13,7 @@ import com.gtnewhorizons.angelica.glsm.recording.support.DisplayListTestFixture;
import org.junit.jupiter.api.Test;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL30;
import java.nio.ByteBuffer;
import java.util.List;
@@ -112,7 +113,7 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
}
@Test
void execute_doesNotRestoreVAO_backToBackDraws() {
void execute_restoresRecordedVao_backToBackDraws() {
final int vbo = defaultQuadVbo();
final int ebo = uploadEbo(GL11.GL_UNSIGNED_SHORT, 0, 1, 2, 3);
final int vao = setupPosColorVao(vbo);
@@ -137,7 +138,8 @@ class IndexedDrawReplayTest extends DisplayListTestFixture {
assertSame(batchOf(cmds.getFirst()), batchOf(cmds.get(1)));
final int sharedVAO = vaoOf(batchOf(cmds.getFirst()));
assertNotEquals(0, sharedVAO);
assertEquals(sharedVAO, GLSMVertexInput.getBoundVAO());
assertEquals(vao, GLSMVertexInput.getBoundVAO());
assertEquals(vao, GL11.glGetInteger(GL30.GL_VERTEX_ARRAY_BINDING));
GLSMVertexInput.glBindVertexArray(0);
}
@@ -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);
+1 -1
View File
@@ -12,7 +12,7 @@ lwjgl3 = "3.4.2"
lwjgl3Release = "3.4.2"
lwjgl3ify = "3.0.31"
celeritas = "2.5.13-GTNH"
celeritas = "2.5.14-GTNH"
gtnhlib = "0.11.36"
eventbus = "7.0-beta.15"
glslTransformLib = "0.2.0-33.g3ec381e"
@@ -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);
+3 -1
View File
@@ -14,9 +14,11 @@ PLOTS = [
"items.bail.ineligible", "items.bail.material", "items.bail.isbrh", "items.bail.blockState", "items.bail.notAllowed", "items.bail.template", "items.bail.queue",
"particles.direct", "particles.captured", "particles.spilled", "particles.undecodable", "particles.draws", "sdl.renderPasses", "sdl.submits", "sdl.gpuWaitUs",
"sdl.acquireWaitUs",
"glsm.bufferDeletes", "draw.batchRebuilds.translucent", "draw.batchRebuilds", "terrain.regionsDrawn",
"sdl.bufferCreates", "sdl.bufferDeletes", "sdl.bufferPoolHits",
"weather.strength", "weather.quads", "weather.drawCalls", "weather.micros",
]
ZONES = ["entityModelParts", "tesrBatch", "entityLayerLoop", "entityDeferred", "tesrOpaque", "particlePass", "cl:weather", "streamingEndFrame"]
ZONES = ["entityModelParts", "tesrBatch", "entityLayerLoop", "entityDeferred", "tesrOpaque", "particlePass", "cl:weather", "streamingEndFrame", "chunkAssembleRegion"]
CLOCK = "gl.drawCalls"
+4 -2
View File
@@ -20,7 +20,8 @@ BAD_LINE = re.compile(
r"Debug screen entry \S+ failed and is disabled|needs the batching font renderer|"
r"Flyby debugEntries:|Flyby: debug entry list is not available|Flyby: unknown debugProfile|"
r"Debug screen compat entry for \S+ failed to register|"
r"Failed to (?:load|save|set aside the unreadable) debug screen profile|toggle listeners kept changing"
r"Failed to (?:load|save|set aside the unreadable) debug screen profile|toggle listeners kept changing|"
r"Flyby heap check: (?:FAIL|SKIPPED)"
)
OVERLAY_RE = re.compile(r"Flyby debug overlay shot=(\d+) mode=(\w+)(?:.* renderers=(\S*))?")
LINE_RE = re.compile(r"Flyby debug line shot=(\d+) id=(\S+) text=(.*)")
@@ -125,7 +126,7 @@ def check(variant_dir, exit_status, new_crashes, opts=None):
else:
bad = [l for l in lines if BAD_LINE.search(l)]
if bad:
reasons.append(str(len(bad)) + " mixin/injection/scene failure line(s)")
reasons.append(str(len(bad)) + " mixin/injection/scene/heap-check failure line(s)")
details.extend([" " + l.strip()[:200] for l in bad[:3]])
offending = []
@@ -209,6 +210,7 @@ def self_test():
("mixin-error", GOOD_LOG + ["[10:00:05] [main/ERROR]: InvalidInjectionException: bad"], {}, 1),
("small-shot", GOOD_LOG, {"shot_size": 100}, 1),
("bad-entries", GOOD_LOG + ["[10:00:06] [Client thread/ERROR]: Flyby debugEntries: unknown entry id 'test:nope'"], {}, 1),
("heap-check-fail", GOOD_LOG + ["[10:00:06] [Client thread/WARN] [Angelica/Flyby/]: Flyby heap check: FAIL WorldClient=1"], {}, 1),
]
for name, log, kw, want in cases:
d = make_variant(root, name, log, **kw)
+1
View File
@@ -15,6 +15,7 @@
# SMOKE_PITCH - camera pitch in degrees, positive looks down (default: 15).
# SMOKE_WARMUP_TICKS - ticks at the first pose before the pan (default: 100).
# SMOKE_ARGS - extra -D flags for the client, word-split (default: none).
# World-leak check: "-Dangelica.flyby.visits=3 -Dangelica.flyby.heapCheck=true" (FAIL/SKIPPED fails the variant).
# SMOKE_CHECK_ARGS - extra flyby-smoke-check.py flags, word-split (default: none).
# SMOKE_FULLSCREEN - 0 keeps options.txt's window mode (default: 1, fullscreen for the run).
#
@@ -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;
}
}
@@ -807,6 +807,7 @@ public final class ResourceManager {
final ByteBuffer droppedPboStaging;
final ByteBuffer droppedPlainStaging;
final PersistentMapping droppedPm;
final boolean hadEboShadow;
wLock.lock();
try {
final long handle = bufferHandles.remove(glId);
@@ -820,7 +821,7 @@ public final class ResourceManager {
droppedPlainStaging = removePlainMappingLocked(glId);
droppedPboStaging = pboStagingData.remove(glId);
deleteUboShadow(glId);
deleteEboShadow(glId);
hadEboShadow = deleteEboShadow(glId);
deleteArrayShadow(glId);
if (handle != 0) {
returnBufferToPool(handle, usage, size);
@@ -829,6 +830,7 @@ public final class ResourceManager {
} finally {
wLock.unlock();
}
if (hadEboShadow) invalidateSplitCacheFor(glId);
if (droppedPboStaging != null) {
memFree(droppedPboStaging);
}
@@ -1277,11 +1279,12 @@ public final class ResourceManager {
try { return eboShadowVersions.get(bufferGlId); } finally { rLock.unlock(); }
}
private void deleteEboShadow(int bufferGlId) {
private boolean deleteEboShadow(int bufferGlId) {
final ByteBuffer existing = eboShadows.remove(bufferGlId);
if (existing != null) memFree(existing);
eboShadowVersions.remove(bufferGlId);
eboShadowWanted.remove(bufferGlId);
return existing != null;
}
public boolean canShadowArrayBuffer(int size) {
@@ -1569,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);
@@ -2064,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)
@@ -2092,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;
}
@@ -2149,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 {
@@ -2164,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()));
}
@@ -15,6 +15,7 @@ import com.gtnewhorizons.angelica.glsm.GLSMVertexInput;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.StateSet;
import com.gtnewhorizons.angelica.glsm.ffp.FragmentKey;
import com.gtnewhorizons.angelica.glsm.hooks.BatchStateGuard;
import com.gtnewhorizons.angelica.glsm.states.FogState;
import com.gtnewhorizons.angelica.glsm.states.TextureUnitArray;
import net.coderbot.iris.gl.framebuffer.GlFramebuffer;
@@ -181,6 +182,7 @@ final class PortalDrawer {
}
void drawDirect(float[] faces, int count, int tunnelTexture, int fieldTexture, Frame frame, boolean fogged) {
BatchStateGuard.beforeChange();
final int fogMode = fogged ? FragmentKey.activeFogMode(GLStateManager.ctx()) : FragmentKey.FOG_NONE;
final GlProgram<Uniforms> program;
if (fogMode == FragmentKey.FOG_NONE) {
@@ -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,133 @@
package com.gtnewhorizons.angelica.debug.flyby;
import com.gtnewhorizons.angelica.config.SystemProperties;
import com.gtnewhorizons.angelica.debug.profiling.AsprofRecorder;
import com.gtnewhorizons.angelica.rendering.celeritas.AngelicaRenderSectionManager;
import com.gtnewhorizons.angelica.rendering.celeritas.DeferredMeshScheduler;
import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice;
import com.gtnewhorizons.angelica.rendering.celeritas.world.cloned.ClonedChunkSection;
import com.sun.management.HotSpotDiagnosticMXBean;
import it.unimi.dsi.fastutil.objects.Object2LongMap;
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
import net.minecraft.client.entity.EntityClientPlayerMP;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.world.chunk.Chunk;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.embeddium.impl.render.chunk.RenderSection;
import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers;
import javax.management.ObjectName;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
final class FlybyHeapCheck {
private static final Logger LOGGER = LogManager.getLogger("Angelica/Flyby");
private static final int TOP_GROWERS = 10;
private static final int MIN_REPORTS_FOR_GROWTH = 3;
private final List<Object2LongOpenHashMap<String>> history = new ArrayList<>();
private final Object2LongOpenHashMap<String> maxRetained = new Object2LongOpenHashMap<>();
private String skipReason;
private static Class<?>[] mustBeZero() {
return new Class<?>[] {AngelicaRenderSectionManager.class, WorldSlice.class, ClonedChunkSection.class, DeferredMeshScheduler.class, RenderSection.class, ChunkBuildBuffers.class, WorldClient.class};
}
private static Class<?>[] watched() {
return new Class<?>[] {Chunk.class, EntityClientPlayerMP.class};
}
void report(String label) {
final Object2LongOpenHashMap<String> counts;
try {
// Objects with a pending finalize() (AE2 part monitors) count as live and keep their world until finalized.
System.gc();
System.runFinalization();
counts = liveHistogram();
} catch (Exception e) {
LOGGER.warn("Flyby heap check: histogram failed", e);
skip("histogram failed: " + e);
return;
}
final StringBuilder sb = new StringBuilder("Flyby heap ").append(label).append(':');
for (Class<?> type : mustBeZero()) {
final long count = counts.getLong(type.getName());
if (count > maxRetained.getLong(type.getName())) maxRetained.put(type.getName(), count);
sb.append(' ').append(type.getSimpleName()).append('=').append(count);
}
sb.append(" | watched");
for (Class<?> type : watched()) {
sb.append(' ').append(type.getSimpleName()).append('=').append(counts.getLong(type.getName()));
}
LOGGER.info(sb);
history.add(counts);
}
void skip(String reason) {
if (skipReason == null) skipReason = reason;
}
void finish() {
if (skipReason != null) {
LOGGER.warn("Flyby heap check: SKIPPED ({})", skipReason);
return;
}
if (history.size() >= MIN_REPORTS_FOR_GROWTH) logGrewEveryVisit();
final StringBuilder retained = new StringBuilder();
for (Class<?> type : mustBeZero()) {
final long max = maxRetained.getLong(type.getName());
if (max > 0) retained.append(' ').append(type.getSimpleName()).append('=').append(max);
}
if (retained.length() == 0) {
LOGGER.info("Flyby heap check: PASS");
return;
}
LOGGER.warn("Flyby heap check: FAIL{}", retained);
dump();
}
private void logGrewEveryVisit() {
final Object2LongOpenHashMap<String> first = history.get(0);
final Object2LongOpenHashMap<String> last = history.get(history.size() - 1);
final List<Object2LongMap.Entry<String>> grew = new ArrayList<>();
for (Object2LongMap.Entry<String> e : last.object2LongEntrySet()) {
if (roseAtEveryReport(e.getKey())) grew.add(e);
}
grew.sort((a, b) -> Long.compare(b.getLongValue() - first.getLong(b.getKey()), a.getLongValue() - first.getLong(a.getKey())));
for (int i = 0; i < Math.min(TOP_GROWERS, grew.size()); i++) {
final Object2LongMap.Entry<String> e = grew.get(i);
LOGGER.info("Flyby heap: grew at every visit: {} {} -> {}", e.getKey(), first.getLong(e.getKey()), e.getLongValue());
}
}
private boolean roseAtEveryReport(String className) {
for (int i = 1; i < history.size(); i++) {
if (history.get(i).getLong(className) <= history.get(i - 1).getLong(className)) return false;
}
return true;
}
private static void dump() {
final File file = new File(SystemProperties.PROFILE_DIR, AsprofRecorder.fileName("leak", "hprof", System.currentTimeMillis())).getAbsoluteFile();
try {
file.getParentFile().mkdirs();
ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class).dumpHeap(file.getPath(), true);
LOGGER.info("Flyby heap: dumped {}", file.getPath());
} catch (Exception e) {
LOGGER.warn("Flyby heap dump to {} failed", file.getPath(), e);
}
}
private static Object2LongOpenHashMap<String> liveHistogram() throws Exception {
final String histogram = (String) ManagementFactory.getPlatformMBeanServer().invoke(new ObjectName("com.sun.management:type=DiagnosticCommand"), "gcClassHistogram", new Object[] {null}, new String[] {String[].class.getName()});
final Object2LongOpenHashMap<String> counts = new Object2LongOpenHashMap<>();
for (String line : histogram.split("\n")) {
final String[] cols = line.trim().split("\\s+");
if (cols.length >= 4 && cols[0].endsWith(":")) counts.put(cols[3], Long.parseLong(cols[1]));
}
return counts;
}
}
@@ -13,15 +13,19 @@ import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters;
import com.gtnewhorizons.angelica.rendering.FpsReducer;
import com.gtnewhorizons.angelica.rendering.FramePacer;
import com.gtnewhorizons.angelica.rendering.IsbrhTestRenderer;
import com.gtnewhorizons.angelica.rendering.celeritas.CeleritasWorldRenderer;
import com.sun.management.OperatingSystemMXBean;
import com.sun.management.ThreadMXBean;
import cpw.mods.fml.client.FMLClientHandler;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import net.minecraft.client.Minecraft;
import net.minecraft.client.entity.EntityClientPlayerMP;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.GuiSelectWorld;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.integrated.IntegratedServer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.ChunkCoordinates;
@@ -60,12 +64,17 @@ public final class FlybyRunner {
public static final FlybyRunner INSTANCE = new FlybyRunner();
private enum State { IDLE, WAITING, PREPARING, WARMUP, RUNNING, SETTLE, EXITING, DONE }
private enum State { IDLE, WAITING, PREPARING, WARMUP, RUNNING, SETTLE, EXITING, MENU, DONE }
private static final int EXIT_TICKS = 20;
private static final int SETTLE_TICKS = 40;
private static final int MENU_SETTLE_TICKS = 20;
private final FlybyHeapCheck heapCheck = new FlybyHeapCheck();
private State state = State.IDLE;
private int visit;
private String rejoinFolder;
private String rejoinName;
private FlybyRoute route;
private int warmupTicks;
private int runLength;
@@ -189,6 +198,14 @@ public final class FlybyRunner {
this.waitForFocus = SystemProperties.FLYBY_WAIT_FOR_FOCUS;
this.exitWhenDone = SystemProperties.FLYBY_EXIT_WHEN_DONE;
this.jfr = SystemProperties.FLYBY_JFR;
if (this.jfr && SystemProperties.FLYBY_VISITS > 1) {
LOGGER.error("Flyby: angelica.flyby.jfr records one file per run and cannot cover {} visits; exiting instead of profiling only the first", SystemProperties.FLYBY_VISITS);
FMLCommonHandler.instance().exitJava(1, false);
}
if (!this.exitWhenDone && (SystemProperties.FLYBY_VISITS > 1 || SystemProperties.FLYBY_HEAP_CHECK)) {
LOGGER.error("Flyby: angelica.flyby.visits and angelica.flyby.heapCheck leave and rejoin the world, which needs angelica.flyby.exitWhenDone=true; exiting instead of ignoring them");
FMLCommonHandler.instance().exitJava(1, false);
}
if (!SystemProperties.FLYBY_COMMANDS.isEmpty()) {
this.sceneCommands = FlybyScene.load(SystemProperties.FLYBY_COMMANDS);
LOGGER.info("Flyby scene '{}': {} commands", SystemProperties.FLYBY_COMMANDS, this.sceneCommands.length);
@@ -251,7 +268,16 @@ public final class FlybyRunner {
if (this.state == State.EXITING) {
if (++this.tick >= EXIT_TICKS) {
this.state = State.DONE;
this.exitGame(mc);
this.leave(mc);
}
return;
}
if (this.state == State.MENU) {
if (++this.tick >= MENU_SETTLE_TICKS) {
this.state = State.DONE;
this.checkHeapAtMenu();
this.afterMenu(mc);
}
return;
}
@@ -892,14 +918,51 @@ public final class FlybyRunner {
this.state = this.exitWhenDone ? State.EXITING : State.DONE;
}
private void exitGame(Minecraft mc) {
private void leave(Minecraft mc) {
this.stopRecording(mc);
LOGGER.info("Flyby complete, shutting down.");
this.rejoinFolder = null;
this.rejoinName = null;
if (++this.visit < SystemProperties.FLYBY_VISITS) {
final IntegratedServer server = mc.getIntegratedServer();
if (server != null) {
this.rejoinFolder = server.getFolderName();
this.rejoinName = server.getWorldName();
LOGGER.info("Flyby: visit {} of {} done, rejoining '{}'", this.visit, SystemProperties.FLYBY_VISITS, this.rejoinFolder);
} else {
LOGGER.error("Flyby: no singleplayer world to revisit, shutting down");
}
}
if (this.rejoinFolder == null) LOGGER.info("Flyby complete, shutting down.");
if (mc.theWorld != null) {
mc.theWorld.sendQuittingDisconnectingPacket();
mc.loadWorld(null);
worldChangesDiscarded = false;
mc.displayGuiScreen(new GuiMainMenu());
}
if (SystemProperties.FLYBY_HEAP_CHECK) {
this.state = State.MENU;
this.tick = 0;
} else {
this.afterMenu(mc);
}
}
private void checkHeapAtMenu() {
final CeleritasWorldRenderer renderer = CeleritasWorldRenderer.getInstanceOrNull();
if (renderer != null && renderer.isActive()) this.heapCheck.skip("renderer still loaded: no unload hook");
else this.heapCheck.report("after visit " + this.visit);
}
private void afterMenu(Minecraft mc) {
if (this.rejoinFolder != null) {
FMLClientHandler.instance().tryLoadExistingWorld(new GuiSelectWorld(new GuiMainMenu()), this.rejoinFolder, this.rejoinName);
if (mc.getIntegratedServer() != null) {
this.startFromProperties();
return;
}
LOGGER.error("Flyby: could not rejoin '{}', shutting down", this.rejoinFolder);
}
if (SystemProperties.FLYBY_HEAP_CHECK) this.heapCheck.finish();
mc.shutdown();
}
@@ -121,7 +121,7 @@ public final class AsprofRecorder {
return opts.isEmpty() ? "start,jfr,file=" + file : "start,jfr," + opts + ",file=" + file;
}
static String fileName(String tag, String ext, long nowMillis) {
public static String fileName(String tag, String ext, long nowMillis) {
final SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.ROOT);
return "angelica-" + tag + "-" + format.format(new Date(nowMillis)) + "." + ext;
}
@@ -514,7 +514,6 @@ public enum Mixins implements IMixins {
, "celeritas.terrain.MixinShaderLoader"
, "celeritas.terrain.MixinGlUniformMatrix4f"
, "celeritas.terrain.MixinGlUniformMatrix3f"
, "celeritas.terrain.MixinDefaultChunkRenderer"
)
),
@@ -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;
}
}
@@ -4,6 +4,7 @@ import com.gtnewhorizons.angelica.dynamiclights.DynamicLights;
import com.gtnewhorizons.angelica.glsm.CaptureGate;
import com.gtnewhorizons.angelica.glsm.DisplayListManager;
import com.gtnewhorizons.angelica.glsm.GLSMAttribStack;
import com.gtnewhorizons.angelica.glsm.GLSMBuffers;
import com.gtnewhorizons.angelica.glsm.GLSMDraws;
import com.gtnewhorizons.angelica.glsm.GLSMPrograms;
import com.gtnewhorizons.angelica.glsm.GLSMTextureLifetime;
@@ -40,6 +41,7 @@ public final class TracyFramePlots {
private static final PlotDelta P_GL_TEX_BIND_MISSES = new PlotDelta("gl.texBindMisses");
private static final PlotDelta P_GL_PROGRAM_SWITCHES = new PlotDelta("gl.programSwitches");
private static final long P_GL_TEX_AWAITING_RELEASE = Tracy.plotHandle("gl.texAwaitingRelease");
private static final PlotDelta P_GLSM_BUFFER_DELETES = new PlotDelta("glsm.bufferDeletes");
private static final PlotDelta P_GLSM_BRACKETS = new PlotDelta("glsm.brackets");
private static final PlotDelta P_GLSM_COW_SLOTS = new PlotDelta("glsm.cowSlots");
private static final PlotDelta P_GLSM_POP_RESTORES = new PlotDelta("glsm.popRestores");
@@ -62,6 +64,7 @@ public final class TracyFramePlots {
private static final long P_VOX_RANGES = Tracy.plotHandle("voxelization.ranges");
private static final long P_VOX_REGIONS = Tracy.plotHandle("voxelization.regions");
private static final long P_DRAW_BATCH_REBUILDS = Tracy.plotHandle("draw.batchRebuilds");
private static final long P_DRAW_BATCH_REBUILDS_TRANSLUCENT = Tracy.plotHandle("draw.batchRebuilds.translucent");
private static final PlotDelta P_DRAW_INDEX_BUFFER_GROWTHS = new PlotDelta("draw.indexBufferGrowths");
private static final long P_CULL_COMMANDS_SUBMITTED = Tracy.plotHandle("cull.commandsSubmitted");
@@ -197,6 +200,7 @@ public final class TracyFramePlots {
P_GL_TEX_BIND_MISSES.plot(GLSMTextures.texBindMisses);
P_GL_PROGRAM_SWITCHES.plot(GLSMPrograms.programSwitches);
Tracy.plotInt(P_GL_TEX_AWAITING_RELEASE, GLSMTextureLifetime.deferredTexturesAwaitingRelease());
P_GLSM_BUFFER_DELETES.plot(GLSMBuffers.bufferDeletes);
P_GLSM_BRACKETS.plot(GLSMAttribStack.attribPushes);
P_GLSM_COW_SLOTS.plot(GLSMAttribStack.attribSlotsSaved);
P_GLSM_POP_RESTORES.plot(GLSMAttribStack.attribValueRestores);
@@ -214,6 +218,7 @@ public final class TracyFramePlots {
Tracy.plotInt(P_TERRAIN_DRAW_COMMANDS, TerrainDrawStats.takeCommandsSubmitted());
Tracy.plotInt(P_TERRAIN_REGIONS_DRAWN, TerrainDrawStats.takeRegionsDrawn());
Tracy.plotInt(P_DRAW_BATCH_REBUILDS, TerrainDrawStats.takeBatchRebuilds());
Tracy.plotInt(P_DRAW_BATCH_REBUILDS_TRANSLUCENT, TerrainDrawStats.takeBatchRebuildsTranslucent());
Tracy.plotInt(P_VOX_ENCODERS, SdlShadowVoxelizationSink.takeEncoders());
Tracy.plotInt(P_VOX_RANGES, SdlShadowVoxelizationSink.takeRanges());
Tracy.plotInt(P_VOX_REGIONS, SdlShadowVoxelizationSink.takeRegions());
@@ -57,6 +57,10 @@ public class AngelicaRenderQueue {
LockSupport.unpark(GLStateManager.getMainThread());
}
public static void remove(Runnable runnable) {
TASKS.removeIf(t -> t == runnable);
}
public static int processTasks(int max) {
int tasksRun = 0;
while(tasksRun < max) {
@@ -305,6 +305,11 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
}
}
@Override
protected SectionRenderDataStorage.BatchCacheParams batchCacheParams(boolean useBlockFaceCulling) {
return SectionRenderDataStorage.BatchCacheParams.of(useBlockFaceCulling, ShadowRenderingState.areShadowsCurrentlyBeingRendered() ? 1 : 0);
}
@Override
protected @Nullable CachedBatch getRegionBatch(CommandList commandList, RenderRegion region,
SectionRenderDataStorage storage, ChunkRenderList renderList,
@@ -327,7 +332,7 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
try {
final CachedBatch cached = super.getRegionBatch(commandList, region, storage, renderList, occlusionCamera,
renderPass, useBlockFaceCulling, cacheParams);
if (cached != before) TerrainDrawStats.recordRebuild();
if (cached != before) TerrainDrawStats.recordRebuild(renderPass.isSorted());
final MultiDrawBatch batch = cached != null ? cached.getBatch() : null;
if (batch != null && !batch.isEmpty()) TerrainDrawStats.recordBatch(batch.size());
return cached;
@@ -56,6 +56,7 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
private static final long P_MESH_DEVICE_ALLOCATED = Tracy.plotHandle("mesh.deviceAllocated", TracyBackend.PLOT_FORMAT_MEMORY);
private static final long P_MESH_DEFERRED_PENDING = Tracy.plotHandle("mesh.deferredPending");
private static final long P_MESH_DEFERRED_FINISHING = Tracy.plotHandle("mesh.deferredFinishing");
private static final long P_MESH_DEFERRED_FREE_SLICES = Tracy.plotHandle("mesh.deferredFreeSlices");
private static final Tracy.ZoneId Z_BIOME_REBUILDS = Tracy.zoneId("biomeRebuilds", Tracy.COLOR_TERRAIN);
private static final Tracy.ZoneId Z_GRAPH_SEARCH = Tracy.zoneId("graphSearch", Tracy.COLOR_TERRAIN);
@@ -73,7 +74,7 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
this.world = world;
this.sectionCache = new ClonedChunkSectionCache(world);
this.taskProvider = taskProvider;
this.deferredScheduler = new DeferredMeshScheduler(configuration, getBuilder(), () -> new WorldSlice(world), AngelicaRenderQueue::submit, ((RenderSectionManagerAccessor) this).angelica$getBuildResults());
this.deferredScheduler = new DeferredMeshScheduler(configuration, getBuilder(), () -> new WorldSlice(world), AngelicaRenderQueue::submit, AngelicaRenderQueue::remove, ((RenderSectionManagerAccessor) this).angelica$getBuildResults());
}
public static AngelicaRenderSectionManager create(ChunkVertexType vertexType, WorldClient world, int renderDistance, CommandList commandList) {
@@ -211,6 +212,7 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
@Override
public void updateChunks(boolean updateImmediately) {
this.sectionCache.cleanup();
this.deferredScheduler.trimPools();
this.flushBiomeRebuilds();
if (Tracy.ENABLED) Tracy.beginZone(Z_GRAPH_SEARCH);
try {
@@ -353,5 +355,6 @@ public class AngelicaRenderSectionManager extends RenderSectionManager {
Tracy.plotInt(P_DRAW_REGION_BUFFERS, mem.bufferCount);
Tracy.plotInt(P_MESH_DEFERRED_PENDING, this.deferredScheduler.pendingCount());
Tracy.plotInt(P_MESH_DEFERRED_FINISHING, this.deferredScheduler.finishingCount());
Tracy.plotInt(P_MESH_DEFERRED_FREE_SLICES, this.deferredScheduler.freeSliceCount());
}
}
@@ -28,9 +28,12 @@ public class DeferredMeshScheduler {
private static final long FINISH_PRIORITY = -1L;
private static final Comparator<DeferredSectionMesh> NEAREST_FIRST = Comparator.comparingLong(m -> m.priority);
private static final int CAPTURES_PER_WORKER = 2;
private static final int MIN_FREE_PER_WORKER = 2;
private final ChunkBuilder builder;
private final Consumer<Runnable> mainThreadQueue;
private final Consumer<Runnable> mainThreadUnqueue;
private final Queue<ChunkJobResult<? extends ChunkTaskOutput>> results;
private final Consumer<ChunkJobResult<? extends ChunkTaskOutput>> resultSink;
private final ObjectPool<ChunkBuildBuffers> buffers;
@@ -41,17 +44,21 @@ public class DeferredMeshScheduler {
private final Set<DeferredSectionMesh> inFlight = ConcurrentHashMap.newKeySet();
private final Runnable runNext = this::runNext;
private final AtomicInteger pending = new AtomicInteger();
private final int minFree;
private boolean destroyed;
public DeferredMeshScheduler(RenderPassConfiguration<?> config, ChunkBuilder builder, Supplier<WorldSlice> sliceFactory, Consumer<Runnable> mainThreadQueue, Queue<ChunkJobResult<? extends ChunkTaskOutput>> results) {
public DeferredMeshScheduler(RenderPassConfiguration<?> config, ChunkBuilder builder, Supplier<WorldSlice> sliceFactory, Consumer<Runnable> mainThreadQueue, Consumer<Runnable> mainThreadUnqueue, Queue<ChunkJobResult<? extends ChunkTaskOutput>> results) {
this.builder = builder;
this.mainThreadQueue = mainThreadQueue;
this.mainThreadUnqueue = mainThreadUnqueue;
this.results = results;
this.resultSink = results::add;
final int workers = Math.max(1, builder.getTotalThreadCount());
this.minFree = workers * MIN_FREE_PER_WORKER;
this.buffers = new ObjectPool<>(() -> new ChunkBuildBuffers(config), Integer.MAX_VALUE);
this.slices = new ObjectPool<>(sliceFactory, Integer.MAX_VALUE);
this.captures = new ObjectPool<>(DeferredCapture::new, Math.max(1, builder.getTotalThreadCount()) * 2);
this.captures = new ObjectPool<>(DeferredCapture::new, workers * CAPTURES_PER_WORKER);
}
public ChunkBuildBuffers acquireBuffers() {
@@ -93,6 +100,7 @@ public class DeferredMeshScheduler {
/** Call after the builder shut down: its queued jobs were dropped without reaching their consumers. */
public void releaseOrphans() {
mainThreadUnqueue.accept(runNext);
DeferredSectionMesh m;
while ((m = important.poll()) != null) {
release(m);
@@ -113,6 +121,16 @@ public class DeferredMeshScheduler {
return inFlight.size();
}
public int freeSliceCount() {
return slices.freeCount();
}
public void trimPools() {
final long now = System.nanoTime();
slices.trim(now, minFree);
buffers.trim(now, minFree);
}
void finished(DeferredSectionMesh m, @Nullable ChunkJobResult<ChunkBuildOutput> result, @Nullable ChunkJobCollector collector) {
final long nanos = m.workerNanos + m.mainNanos;
ChunkJobResult<ChunkBuildOutput> out = result instanceof ChunkJobResult.Success<ChunkBuildOutput> success ? new ChunkJobResult.Success<>(success.output(), nanos + success.executionTimeNanos()) : result;
@@ -1,21 +1,31 @@
package com.gtnewhorizons.angelica.rendering.celeritas;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
final class ObjectPool<T> {
static final long TRIM_WINDOW_NANOS = TimeUnit.SECONDS.toNanos(30);
private final ConcurrentLinkedDeque<T> free = new ConcurrentLinkedDeque<>();
private final AtomicInteger freeCount = new AtomicInteger();
private final AtomicInteger inUse = new AtomicInteger();
private final Supplier<T> factory;
private final int maxFree;
private boolean started;
private long windowStart;
private int windowPeak;
private int previousPeak;
ObjectPool(Supplier<T> factory, int maxFree) {
this.factory = factory;
this.maxFree = maxFree;
}
T acquire() {
inUse.incrementAndGet();
final T t = free.pollFirst();
if (t == null) return factory.get();
freeCount.decrementAndGet();
@@ -23,10 +33,34 @@ final class ObjectPool<T> {
}
void release(T t) {
inUse.decrementAndGet();
if (freeCount.incrementAndGet() > maxFree) {
freeCount.decrementAndGet();
return;
}
free.offerFirst(t);
}
int freeCount() {
return freeCount.get();
}
void trim(long nowNanos, int minFree) {
final int used = inUse.get();
if (used > windowPeak) windowPeak = used;
if (!started) {
started = true;
windowStart = nowNanos;
return;
}
if (nowNanos - windowStart < TRIM_WINDOW_NANOS) return;
final int peak = Math.max(windowPeak, previousPeak);
previousPeak = windowPeak;
windowPeak = used;
windowStart = nowNanos;
final int excess = freeCount.get() - Math.max(minFree, peak + (peak >> 1) - used);
for (int i = (excess + 1) >> 1; i > 0 && free.pollLast() != null; i--) {
freeCount.decrementAndGet();
}
}
}
@@ -8,6 +8,7 @@ public final class TerrainDrawStats {
private static int commandsSubmitted;
private static int regionsDrawn;
private static int batchRebuilds;
private static int batchRebuildsTranslucent;
private static int cullCommandsSubmitted;
private static int cullRegionsDrawn;
@@ -35,8 +36,9 @@ public final class TerrainDrawStats {
regionsDrawn++;
}
public static void recordRebuild() {
public static void recordRebuild(boolean translucent) {
batchRebuilds++;
if (translucent) batchRebuildsTranslucent++;
}
public static void recordCullRegion(int commandCount) {
@@ -76,6 +78,12 @@ public final class TerrainDrawStats {
return n;
}
public static int takeBatchRebuildsTranslucent() {
final int n = batchRebuildsTranslucent;
batchRebuildsTranslucent = 0;
return n;
}
public static int takeCommandsSubmitted() {
final int n = commandsSubmitted;
commandsSubmitted = 0;
@@ -34,7 +34,8 @@ public final class BatchDrawDefaults {
}
public void apply() {
if (passFramebuffer >= 0 && TesrBatchRenderer.deferredPipeline() == null) {
final boolean pack = TesrBatchRenderer.deferredPipeline() != null;
if (passFramebuffer >= 0 && !pack) {
final int framebuffer = GLSMFramebuffer.getDrawFramebuffer();
if (framebuffer != passFramebuffer) {
callerFramebuffer = framebuffer;
@@ -42,7 +43,7 @@ public final class BatchDrawDefaults {
}
}
final int program = GLSMPrograms.getActiveProgram();
if (program != 0 && !Program.isManagedBind(program) && !DepthColorStorage.isOwnedProgram(program)) {
if (program != 0 && !Program.isManagedBind(program) && !(pack && DepthColorStorage.isOwnedProgram(program))) {
callerProgram = program;
GLSMPrograms.glUseProgram(0);
}
@@ -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
@@ -1,7 +1,10 @@
options.button.shader=Shadery...
options.chunks=%s chunków
options.entityShadows=Cienie bytów
options.entityShadows=Cienie mobów
options.droppedItems=%s przedmiotów
options.droppedItems.max=Bez limitu
sodium.option_impact.low=Niski
sodium.option_impact.medium=Średni
sodium.option_impact.high=Wysoki
@@ -10,56 +13,107 @@ sodium.option_impact.varies=Różny
sodium.options.pages.quality=Jakość
sodium.options.pages.performance=Wydajność
sodium.options.pages.advanced=Zaawansowane
sodium.options.view_distance.tooltip=Zasięg renderowania kontroluje, jak daleko otoczenie będzie renderowane. Krótszy dystans oznacza, że mniej otoczenia będzie renderowane, zwiększając klatki na sekundę.
sodium.options.simulation_distance.tooltip=Zasięg symulacji kontroluje jak daleko otoczenie i byty będą wczytywane i symulowane. Krótsze dystansy mogą zmniejszyć obciążenie wewnętrznego serwera i poprawić klatki na sekundę.
sodium.options.brightness.tooltip=Kontroluje jasność (gammę) gry.
sodium.options.pages.appearance=Wygląd
sodium.options.pages.text=Efekty tekstu
sodium.options.pages.fps_reducer=Ogranicznik FPS
sodium.options.view_distance.tooltip=Zasięg renderowania określa, jak daleko otoczenie będzie renderowane. Krótszy dystans oznacza, że mniej otoczenia będzie renderowane, zwiększając klatki na sekundę.
sodium.options.simulation_distance.tooltip=Zasięg symulacji określa, jak daleko otoczenie i byty będą wczytywane i symulowane. Krótsze dystansy mogą zmniejszyć obciążenie wewnętrznego serwera i poprawić klatki na sekundę.
sodium.options.cc.vertical_view_distance.name=Pionowy zasięg widzenia
sodium.options.cc.vertical_view_distance.tooltip=Pionowy zasięg widzenia określa pionowy zakres, w którym sześciany będą ładowane, wyśrodkowany względem gracza. Krótszy dystans oznacza, że mniej otoczenia będzie renderowane, zwiększając klatki na sekundę.
sodium.options.brightness.tooltip=Określa jasność (gammę) gry.
sodium.options.clouds.name=Chmury
sodium.options.clouds.tooltip=Kontroluje, czy chmury będą lub nie będą widoczne.
sodium.options.gui_scale.tooltip=Ustawia maksymalne skalowanie użyte dla interfejsu użytkownika. Jeśli opcja "auto" jest użyta, wtedy największa skala interfejsu zawsze będzie użyta.
sodium.options.clouds.tooltip=Określa widoczność chmur.
sodium.options.gui_scale.tooltip=Ustawia maksymalną skalę interfejsu użytkownika. Jeśli opcja "auto" jest użyta, zawsze stosowany będzie największy współczynnik skalowania.
sodium.options.fullscreen.tooltip=Jeśli włączone, gra będzie wyświetlana w trybie pełnoekranowym (Jeśli opcja jest wspierana).
sodium.options.v_sync.tooltip=Jeśli włączone, klatki na sekundę będą synchronizowane z częstotliwością odświeżania monitora, co zapewni płynniejsze wrażenia kosztem gorszego opóźnienia wejścia. To ustawienie może zmniejszyć wydajność, jeśli twój sprzęt jest zbyt słaby.
sodium.options.fps_limit.tooltip=Ogranicza maksymalną liczbę klatek na sekundę. Ta opcja może pomóc zmniejszyć zużycie baterii i obciążenie systemu, gdy robisz wiele rzeczy naraz. Jeśli VSync jest włączony, limit klatek będzie zignorowany, jeśli jest mniejszy od częstotliwości odświeżania twojego monitora.
sodium.options.view_bobbing.tooltip=Jeśli włączone, pole widzenia gracza będzie się bujać i podskakiwać. Gracze, którzy posiadają chorobę lokomocyjną lub tym podobne mogą mieć korzyści z wyłączenia tej opcji.
sodium.options.attack_indicator.tooltip=Kontroluje, gdzie ikona ataku jest wyświetlana na ekranie.
sodium.options.autosave_indicator.tooltip=Jeśli włączone, ikona zapisywania będzie pokazana, gdy gra zapisuje świat na dysku.
sodium.options.graphics_quality.tooltip=Domyślna jakość grafiki kontroluje kilka starych opcji i jest potrzebna do kompatybilności modów. Jeśli opcje poniżej są ustawione na "Domyślna", wtedy użyją one tej opcji.
sodium.options.clouds_quality.tooltip=Kontroluje jakość renderowania chmur na niebie.
sodium.options.weather_quality.tooltip=Kontroluje jakość efektów deszczu i śniegu.
sodium.options.v_sync.tooltip=Wył. prezentuje każdą klatkę natychmiastowo: Najniższe opóźnienie, ale występuje rozrwyanie obrazu. Wł. czeka na vblank i kolejkuje nową klatkę: Brak rozrwyania obrazu, większe opóźnienie. Mailbox czeka na vblank ale zamienia zakolejkowaną klatkę: Brak rozrwyania obrazu, opóźnienie zbliżone do trybu Wył.
sodium.options.vsync_mode.off=Wył.
sodium.options.vsync_mode.on=Wł.
sodium.options.vsync_mode.mailbox=Mailbox
sodium.options.fps_limit.tooltip=Ogranicza maksymalną liczbę klatek na sekundę. Ta opcja może pomóc zmniejszyć zużycie baterii i obciążenie systemu, gdy robisz wiele rzeczy naraz. Działa w każdym trybie VSync.
sodium.options.view_bobbing.tooltip=Jeśli włączone, pole widzenia gracza będzie kołysać się i podskakiwać podczas poruszania się. Wyłączenie tej opcji może korzystnie wpłynąć na graczy z chorobą lokomocyjną.
sodium.options.attack_indicator.tooltip=Określa, gdzie ikona ataku jest wyświetlana na ekranie.
sodium.options.autosave_indicator.tooltip=Jeśli włączone, ikona zapisywania będzie widoczna, gdy gra zapisuje świat na dysku.
sodium.options.graphics_quality.tooltip=Domyślna jakość grafiki określa kilka starszych opcji i jest niezbędna do zachowania kompatybilności z modami. Jeśli opcje poniżej są ustawione na "Domyślna", będą używać tej opcji.
sodium.options.clouds_quality.tooltip=Określa jakość renderowania chmur na niebie.
sodium.options.weather_quality.tooltip=Określa jakość efektów deszczu i śniegu.
sodium.options.leaves_quality.name=Jakość liści
sodium.options.leaves_quality.tooltip=Kontroluje jakość liści.
sodium.options.leaves_quality.tooltip=Określa jakość liści.
sodium.options.grass_quality.name=Jakość trawy
sodium.options.grass_quality.tooltip=Kontroluje jakość stron bloków trawy.
sodium.options.grass_quality.tooltip=Określa jakość stron bloków trawy.
sodium.options.celeritas_smooth_lighting.name=Gładkie oświetlenie Celeritas
sodium.options.celeritas_smooth_lighting.tooltip=Jeśli włączone, system oświetlenia Celeritas zostanie użyty do gładkiego oświetlenia. Działa tylko wtedy, kiedy gładkie oświetlenie jest włączone.
sodium.options.texture_filtering.name=Filtrowanie tekstur
sodium.options.texture_filtering.tooltip=Określa sposób próbkowania tekstur bloków w oddali.
sodium.options.texture_filtering.rgss=RGSS
sodium.options.texture_filtering.anisotropic=Anizotropowe
sodium.options.texture_filtering.rgss_anisotropic=RGSS i anizotropowe
sodium.options.texel_sampling.name=Wygładzanie tekseli
sodium.options.texel_sampling.tooltip=Określa sposób rysowania granicy między dwoma tekselami tekstury bloku. "Najbliższe" jest ostre, "Liniowe" wygładza.
sodium.options.texel_sampling.linear=Liniowe
sodium.options.texel_sampling.nearest=Najbliższe
sodium.options.anisotropic_filtering.name=Filtrowanie anizotropowe
sodium.options.anisotropic_filtering.tooltip=Uwydatnia tekstury oglądane pod ostrym kątem.
sodium.options.gui_block_supersampling.name=Supersampling bloków w GUI
sodium.options.gui_block_supersampling.tooltip=Określa liczbę próbek tekstur na piksel dla trójwymiarowych ikon bloków w GUI.
sodium.options.particle_quality.name=Jakość cząstek
sodium.options.particle_quality.tooltip=Kontroluje maksymalną ilość cząstek, które mogą być widoczne na ekranie w tym samym czasie.
sodium.options.smooth_lighting.tooltip=Kontroluje, czy bloki będą gładko oświetlone i cieniowane. Ta opcja może troszkę podwyższyć ilość czasu, jaką zajmuje załadowanie lub zaktualizowanie chunku, lecz nie ma wpływu na klatki na sekundę.This slightly increases the amount of time needed to re-build a chunk, but doesn't affect frame rates.
sodium.options.particle_quality.tooltip=Określa maksymalną ilość cząstek, które mogą być widoczne na ekranie w tym samym czasie.
sodium.options.smooth_lighting.tooltip=Określa, czy bloki będą gładko oświetlone i cieniowane. Ta opcja może troszkę podwyższyć ilość czasu, jaką zajmuje załadowanie lub zaktualizowanie chunku, lecz nie ma wpływu na klatki na sekundę.
sodium.options.biome_blend.value=%s blok(ów)
sodium.options.biome_blend.tooltip=Kontroluje zasięg, w którym biomy będą próbkowane dla kolorowania bloków. Wyższe wartoście znacznie zwiększy czas ładowania chunków, w zamian za znikome poprawki w jakości.
sodium.options.entity_distance.tooltip=Kontroluje, jak daleko od gracza będą renderowane byty. Wyższe wartości zwiększają zasięg renderowania, kosztem klatek na sekundę.
options.biomeBlendRadius=Zasięg przenikania biomów
sodium.options.biome_blend_mode.name=Przenikanie bloków biomów
sodium.options.biome_blend_mode.tooltip="Szybkie" stosuje jeden odcień biomu na każdy blok. "Dokładne" miesza odcień między trawą, liśćmi i powierzchnią wody używając zasięgu mieszania biomów.
sodium.options.biome_blend.tooltip=Określa zasięg, w którym biomy będą próbkowane dla kolorowania trawy, liści, wody i nieba.
sodium.options.entity_distance.tooltip=Określa, jak daleko od gracza będą renderowane byty. Wyższe wartości zwiększają zasięg renderowania, kosztem klatek na sekundę.
sodium.options.entity_shadows.tooltip=Jeśli włączone, podstawowe cienie będą renderowane pod mobami i innymi bytami.
sodium.options.vignette.name=Winieta
sodium.options.vignette.tooltip=Jeśli włączone, efekt winiety będzie renderowany na widoku gracza. To raczej nie wpłynie na klatki na sekundę, chyba że jesteś ograniczany przez szybkość wypełniania.
sodium.options.mipmap_levels.tooltip=Kontroluje ilość mipmap, które będą użyte do tekstur modeli bloków. Wyższe wartości skutkują lepszym renderowaniem bloków w oddali, lecz mogą spowodować pogorszenie wydajności przy dużej ilości animowanych tekstur.
sodium.options.mipmap_levels.tooltip=Określa ilość mipmap, które będą użyte do tekstur modeli bloków. Wyższe wartości skutkują lepszą jakością bloków w oddali, lecz mogą spowodować pogorszenie wydajności przy dużej ilości animowanych tekstur.
sodium.options.mipmap_generation.name=Generowanie mipmap
sodium.options.mipmap_generation.tooltip=Wybiera, jak mniejsze warianty tekstur są generowane.
sodium.options.mipmap_generation.vanilla_1_7_10=Minecraft 1.7.10
sodium.options.mipmap_generation.celeritas=Celeritas
sodium.options.mipmap_generation.modern_vanilla=Minecraft 26.2
sodium.options.use_vertex_objects.name=Obiekty układu wierzchołków
sodium.options.use_vertex_objects.tooltip=Pomaga poprawić wydajność poprzez przenoszenie informacji o tym, jak dane wierzchołków powinny być renderowane do sterownika, pozwalając na lepszą optymalizację dla wielokrotnego renderowania tych samych obiektów. Generalnie, nie ma potrzebyy wyłączania tego, chyba że używasz niekompatybilnych modów.
sodium.options.use_block_face_culling.name=Chowanie ścian bloków
sodium.options.use_block_face_culling.tooltip=Jeśli włączone, tylko ściany bloków, które odwrócone są w stronę kamery, będą renderowane. To może wyeliminować dużą liczbę ścian bloków wcześnie w procesie renderowania, oszczędzając przepustowość pamięci i czas na GPU. Niektóre paczki zasobów mogą mieć problem z tą opcją, możesz wtedy spróbować ją wyłączyć jeśli widzisz dziury w blokach.
sodium.options.use_vertex_objects.tooltip=Pomaga poprawić wydajność poprzez przenoszenie informacji o tym, jak dane wierzchołków powinny być renderowane do sterownika, pozwalając na lepszą optymalizację dla wielokrotnego renderowania tych samych obiektów. Generalnie, nie ma potrzeby wyłączania tego, chyba że używasz niekompatybilnych modów.
sodium.options.use_block_face_culling.name=Culling ścian bloków
sodium.options.use_block_face_culling.tooltip=Jeśli włączone, tylko ściany bloków które odwrócone są w stronę kamery, będą renderowane. To może wyeliminować dużą liczbę ścian bloków wcześnie w procesie renderowania, oszczędzając przepustowość pamięci i czas na GPU. Niektóre paczki zasobów mogą mieć problem z tą opcją, możesz wtedy spróbować ją wyłączyć jeśli widzisz dziury w blokach.
sodium.options.use_compact_vertex_format.name=Kompaktowy format wierzchołków
sodium.options.use_compact_vertex_format.tooltip=Jeśli włączone, bardziej kompaktowy format wierzchołków będzie użyty do renderowania chunków. To może znacznie zmniejszyć zużycie pamięci grafiki i wymagań przepustowości, szczególnie dla zintegrowanych układów graficznych, ale może powodować z-fighting z niektórymi paczkami zasobów przez to jak zmniejsza precyzję pozycji i atrybutuów współrzędnych tekstur.
sodium.options.use_fog_occlusion.name=Okluzja mgły
sodium.options.use_fog_occlusion.tooltip=Jeśli włączone, chunki, które uważane są za w pełni ukryte przez mgłę, nie będą renderowane, zwiększając wydajność. Efekt poprawy może być większy, gdy efekt mgły jest cięższy (np. pod wodą), ale może skutkować w niechcianych artefaktach graficznych pomiędzy niebem a mgłą w niektórych przypadkach.
sodium.options.use_entity_culling.name=Chowanie bytów
sodium.options.use_fog_occlusion.tooltip=Jeśli włączone, chunki, które uważane są za w pełni ukryte przez mgłę, nie będą renderowane, zwiększając wydajność. Poprawa wydajności może być większa, gdy efekt mgły jest większy (np. pod wodą), ale może skutkować w niechcianych artefaktach graficznych pomiędzy niebem a mgłą w niektórych przypadkach.
sodium.options.use_entity_culling.name=Culling bytów
sodium.options.use_entity_culling.tooltip=Jeśli włączone, byty, które nie są w żadnym widocznym chunku, będą zignorowane podczas renderowaniu.To może zwiększyć wydajność, pomijając renderowanie bytów znajdujących się pod ziemią lub za ścianami.
sodium.options.use_particle_culling.name=Chowanie cząstek
sodium.options.use_particle_culling.tooltip=Jeśli włączone, tylko cząstki, które uważane są za widoczne, będą renderowane. To może skutkować znaczną poprawą klatek na sekundę, gdy wiele cząstek jest w polu widzenia.
sodium.options.section_gated_tesr_culling.name=Bramkowany sekcyjnie culling TESR
sodium.options.section_gated_tesr_culling.tooltip=Jeśli włączone, byty-bloki są chowane algorytmem frustum po jednej sekcji na raz, zamist po jednym bytcie na raz.
sodium.options.use_particle_culling.name=Culling cząstek
sodium.options.use_particle_culling.tooltip=Jeśli włączone, tylko cząstki, które uważane są za widoczne, będą renderowane. To może skutkować znaczną poprawą klatek na sekundę, gdy wiele cząstek znajduje się w polu widzenia.
sodium.options.animate_only_visible_textures.name=Animuj tylko widoczne tekstury
sodium.options.animate_only_visible_textures.tooltip=Jeśli włączone, tylko animowane tekstury, które uważane są za widoczne, będą aktualizowane. To może pomóc z wydajnością na niektórych systemach, szczególnie z cięższymi paczkami zasobów. Jeśli zauważyłeś, że niektóre tekstury nie są animowane, spróbuj wyłączyć tę opcję. If enabled, only animated textures determined to be visible will be updated.
sodium.options.animate_only_visible_textures.tooltip=Jeśli włączone, tylko animowane tekstury, które uważane są za widoczne, będą aktualizowane. To może pomóc z wydajnością na niektórych systemach, szczególnie z cięższymi paczkami zasobów. Jeśli zauważyłeś, że niektóre tekstury nie są animowane, spróbuj wyłączyć tę opcję.
sodium.options.translucency_sorting.name=Sortowanie przezroczystości
sodium.options.translucency_sorting.tooltip=Jeśli włączone, przezroczyste bloki będą sortowane poprawnie od tyłu do przodu aby zapewnić lepsze renderowanie, szczególnie jeśli wiele przezroczystych bloków jest widocznych i ułożonych na sobie. Jest to trochę kosztowna operacja.
sodium.options.translucency_sorting.tooltip=Jeśli włączone, przezroczysty teren (woda, kolorowe szkło, lód) i przezroczyste byty-bloki będą sortowane od tyłu do przodu, aby nakładające się warstwy były poprawnie wyświetlone. Wyłączenie tej opcji zwiększa wdyajność w sytuacjach z dużą ilością przezroczystych bloków, ale powoduje artefakty. Zmienienie tej opcji przeładowuje chunki.
sodium.options.cpu_render_ahead_limit.name=Limit renderowania klatek z wyprzedzeniem przez CPU
sodium.options.cpu_render_ahead_limit.tooltip=Określa maksymalną liczbę klatek na które CPU może czekać, aby zostały wyrenderowane przez GPU. Bardzo niskie lub wysokie wartości mogą powodować niestabilność klatek.
sodium.options.cpu_render_ahead_limit.tooltip=Określa maksymalną liczbę klatek na które CPU może czekać, aby zostały wyrenderowane przez GPU. Bardzo niskie lub wysokie wartości mogą powodować niestabilność klatek. 0 wyłącza tą opcję; Backend SDL-GPU ignoruje ją i stosuje własne rozwiązanie.
sodium.options.cpu_render_ahead_limit.value=%s klat(ki/ek)
sodium.options.fps_reducer_enabled.name=Ogranicz klatki na sekundę w tle
sodium.options.fps_reducer_enabled.tooltip=Zmniejsza liczbę klatek na sekundę i głośność, gdy gra działa w tle.
sodium.options.unfocused_fps_limit.name=Klatki na sekundę na drugim planie
sodium.options.unfocused_fps_limit.tooltip=Ograniczenie liczby klatek na sekundę, gdy inne okno jest na pierwszym planie.
sodium.options.unfocused_volume.name=Głośność na drugim planie
sodium.options.unfocused_volume.tooltip=Ograniczenie głośności, gdy inne okno jest na pierwszym planie.
sodium.options.minimized_volume.name=Głośność zminimalizowana
sodium.options.minimized_volume.tooltip=Ograniczenie głośności, gdy gra jest zminimalizowana.
sodium.options.idle_timeout.name=Limit czasu bezczynności
sodium.options.idle_timeout.tooltip=Liczba minut bezczynności przed zastosowaniem limitów bezczynności.
sodium.options.idle_timeout.value=%s minut
sodium.options.idle_timeout.off=Wył.
sodium.options.idle_fps_limit.name=Liczba klatek na sekundę w stanie bezczynności
sodium.options.idle_fps_limit.tooltip=Ograniczenie liczby klatek na sekundę w stanie bezczynności.
sodium.options.idle_volume.name=Głośność w stanie bezczynności
sodium.options.idle_volume.tooltip=Głośność w stanie bezczynności.
sodium.options.limit_menu_fps.name=Ograniczenie liczby klatek na sekundę w menu
sodium.options.limit_menu_fps.tooltip=Ogranicza liczbę klatek na sekundę do 30, gdy żaden świat nie jest załadowany.
sodium.options.allow_direct_memory_access.name=Zezwól na bezpośredni dostęp do pamięci
sodium.options.allow_direct_memory_access.tooltip=Jeśli włączone, niektóre krytyczne ścieżki kodu będą miały pozwolenie na użycie bezpośredniego dostępu do pamięci dla wydajności. To często znacznie zmniejsza obciążenie CPU do renderowania chunków i bytów, lecz może utrudnić diagnozę błędów i awarii. Powinieneś to wyłączyć tylko wtedy, jeśli zostałeś o to zapytany lub wiesz co robisz.
sodium.options.allow_direct_memory_access.tooltip=Jeśli włączone, niektóre krytyczne ścieżki kodu będą miały pozwolenie na użycie bezpośredniego dostępu do pamięci dla zwiększenia wydajności. To często znacznie zmniejsza obciążenie CPU podczas renderowania chunków i bytów, lecz może utrudnić diagnozę błędów i awarii. Powinieneś to wyłączyć tylko wtedy, jeśli zostałeś o to zapytany lub wiesz co robisz.
sodium.options.ignore_driver_blacklist.name=Ignoruj czarną listę sterowników
sodium.options.ignore_driver_blacklist.tooltip=Jeśli włączone, znane problemy z kompatybilnością z twoim systemem/sterownikiem będą zignorowane, pozwalając na włączenie opcji, które mogą powodować problemy z grą. Nie powinieneś dotykać tej opcji, chyba że dokładnie wiesz co robisz. Po zmienieniu tej opcji, musisz zapisać, zamknąć, a potem ponownie otworzyć ekran ustawień.
sodium.options.enable_memory_tracing.name=Włącz śledzenie pamięci
@@ -74,22 +128,49 @@ sodium.options.chunk_memory_allocator.swap=Swap
sodium.options.chunk_update_threads.name=Wątki aktualizacji chunków
sodium.options.chunk_update_threads.tooltip=Określa liczbę wątków użytych do budowania chunków. Używanie większej liczby wątków może przyspieszyć wczytywanie chunków i ich aktualizowanie, ale może negatywnie wpłynąć na klatki.
sodium.options.always_defer_chunk_updates.name=Zawsze opóźniaj aktualizacje chunków
sodium.options.always_defer_chunk_updates.tooltip=Jeśli włączone, renderowanie nigdy nie będzie czekać aż aktualizacje chunków się zakończą, nawet gdy są one ważne. To może znacznie poprawić wydajność klatek w niektórych przypadkach, ale może skutkować widocznym opóźnieniem na świecie.
sodium.options.always_defer_chunk_updates.tooltip=Jeśli włączone, renderowanie nigdy nie będzie czekać aż aktualizacje chunków się zakończą, nawet gdy są one ważne. To może znacznie poprawić wydajność klatek w niektórych przypadkach, ale może skutkować widocznym opóźnieniem w świecie.
sodium.options.use_no_error_context.name=Brak kontekstu błędu
sodium.options.use_no_error_context.tooltip=Jeśli włączone, kontekst OpenGL zostanie stworzony z wyłączonym sprawdzaniem błędów. To może minimalnie zwiększyć wydajność, ale również zwiększa ryzyko awarii gry zamiast radzenia sobie z błędami OpenGL. Powinieneś wyłączyć tą opcję, jeśli napotykasz nagłe i niewyjaśnione awarie.
sodium.options.async_occlusion_mode.name=Asynchroniczna okluzja
sodium.options.async_occlusion_mode.tooltip=Określa, który przebieg renderowania używa asynchronicznej okluzji otoczenia. "Tylko cienie" (domyślnie) używa asynchronicznej okluzji tylko dla przebiegu cienia, zachowując synchroniczność głównej listy renderowania. "Wszystko" uruchamia wszystkie przebiegi w trybie asynchronicznym, co może spowodować opóźnienie widocznośći jednej klatki podczas szybkich ruchów kamery. "Żadna" całkowicie wyłącza asynchroniczną okluzję.
sodium.options.async_occlusion_mode.only_shadow=Tylko cienie
sodium.options.async_occlusion_mode.everything=Wszystko
sodium.options.use_occlusion_culling.name=Culling okluzją
sodium.options.use_occlusion_culling.tooltip=Jeśli włączone, chunki ukryte za innymi chunkami nie będą renderowane. Wyłączenie tej opcji znacznie zmniejszy wydajność, ale może być przydatne przy debugowaniu.
sodium.options.threads.value=%s wątek(ów)
sodium.options.default=Domyślne
sodium.options.none=Żadne
sodium.options.buttons.undo=Cofnij
sodium.options.buttons.apply=Zastosuj
sodium.options.buttons.donate=Kup kawę dla JellySquid!
sodium.options.anaglyph.tooltip=Jeśli włączone, świat będzie renderowany dwukrotnie z czerwonymi/cyjanowymi maskami koloru.
sodium.options.show_cape.tooltip=Jeśli włączone, twoja peleryna będzie widoczna.
rso.search_bar_empty=Wyszukaj opcję...
options.dynamic_fov=Dynamiczne pole widzenia
options.dynamic_fov.tooltip=Jeśli włączone, pole widzenia gry będzie zmieniać się na podstawie szybkości gracza.
options.framerate=%s FPS
iris.shaders.toggled=Przełączono shadery na %s!
iris.shaders.disabled=Shadery wyłączone!
iris.shaders.reloaded=Shadery przeładowane!
iris.shaders.reloaded.failure=Nie udało się przeładować shaderów! Powód: %s
iris.shaders.toggled.failure=Nie udało się przełączyć shaderów! Powód: %s
key.category.iris.keybinds=Skróty klawiszowe Iris
iris.keybind.reload=Przeładuj shadery
iris.keybind.toggleShaders=Przełącz shadery
iris.keybind.shaderPackSelection=Ekran wyboru shaderów
key.categories.angelica=Angelica
angelica.keybind.zoom=Zoom
angelica.keybind.fps_reducer_force=Wymuś ogranicznik FPS
angelica.keybind.fps_reducer_disable=Przełącz ogranicznik FPS
angelica.keybind.tracyCapture=Zacznij/Zatrzymaj przechwytywanie Tracy
angelica.fps_reducer.hud.forced=Ogranicznik FPS: wymuszony
angelica.fps_reducer.hud.disabled=Ogranicznik FPS: wyłączony
options.iris.apply=Zastosuj
options.iris.refresh=Odśwież
options.iris.openShaderPackFolder=Otwórz folder paczek shaderów...
options.iris.openShaderPackFolder=Otwórz folder z shaderami...
options.iris.shaderPackSettings=Ustawnienia paczki shaderów...
options.iris.shaderPackList=Lista paczek shaderów...
options.iris.refreshShaderPacks=Odśwież paczki shaderów
@@ -100,13 +181,21 @@ options.iris.shaderPackSelection.addedPacks=Dodano %s paczki shaderów
options.iris.shaderPackSelection.failedAdd=Pliki nie były prawidłowymi paczkami shaderów
options.iris.shaderPackSelection.failedAddSingle="%s" nie jest prawidłową paczką shaderów
options.iris.shaderPackSelection.copyError=Nie można było dodać paczki shaderów "%s"
options.iris.shaderPackSelection.copyErrorAlreadyExists="%s" już istnieje w twoim folderze Shader Packs!
options.iris.shaderPackSelection.copyErrorAlreadyExists="%s" jest już w twoim folderze paczek shaderów!
options.iris.shaderPackOptions.noOptions="%s" nie ma konfigurowalnych ustawień
options.iris.shaderPackOptions.tooManyFiles=Nie można zaimportować wielu plików ustawień shadera!
options.iris.shaderPackOptions.failedImport=Nie udało się zaimportować plik ustawień shadera "%s"
options.iris.shaderPackOptions.failedImport=Nie udało się zaimportować pliku ustawień shadera "%s"
options.iris.shaderPackOptions.importedSettings=Zaimportowano ustawienia shadera z "%s"
options.iris.shaders.disabled=Shadery: Wyłączone
options.iris.shaders.enabled=Shadery: Włączone
options.iris.shaders.nonePresent=Shadery: Brak paczek shaderów
options.iris.saveCompiledShaders.on=Zapisz skompilowane shadery: Wł.
options.iris.saveCompiledShaders.off=Zapisz skompilowane shadery: Wył.
options.iris.saveCompiledShaders.tooltip=Zapisuje skompilowane programy shaderów Iris na dysku, aby restartowanie i wchodzenie do nowych wymiarów ładowało je zamiast kompilowania na miejscu. Gdy ta opcja jest włączona, każda zmiana ustawień shadera kompiluje je dla każdego wymiaru na raz, przez co stosowanie ich dłużej trwa. Wyłączenie tej opcji usuwa zapisane programy.
options.iris.saveCompiledShaders.unsupported=Twój sterownik graficzny nie może zapisać skompilowanych programów, więc ta opcja tylko przygotowuje shadery każdego wymiaru wcześniej.
options.iris.saveCompiledShaders.progress=Kompilowanie shaderów
options.iris.saveCompiledShaders.progressStep=Folder shaderów %s z %s: %s
options.iris.saveCompiledShaders.baseFolder=(domyślne shadery)
options.iris.back=Powrót
options.iris.reset=Resetuj
options.iris.reset.tooltip=Zresetować WSZYSTKIE opcje i zastosować?
@@ -118,13 +207,334 @@ options.iris.setToDefault=Ustawić opcję na domyślną wartość?
options.iris.profile=Profil
options.iris.profile.custom=Niestandardowy
options.iris.shadowDistance=Maksymalny zasięg cieni
options.iris.shadowDistance.sodium_tooltip=Maksymalny zasięg renderowania cieni
options.iris.shadowDistance.enabled=Pozwala zmienić maksymalny zasięg renderowania cieni. Teren i byty poza tym dystansem nie będą rzucać cieni. Zmniejszenie zasięgu renderowania cieni może znacznie zwiększyć wydajność.
options.iris.shadowDistance.disabled=Twoja obecna paczka shaderów ustawiła już zasięg renderowania cieni; nie możesz go zmienić.
options.iris.gui.hide=Ukryj GUI
options.iris.gui.show=Pokaż GUI
options.iris.search.button=Wyszukaj
options.iris.clear.button=Wyczyść
options.iris.search.tooltip=Ustawienia szukania shaderów (Ctrl + F)
options.iris.clear.tooltip=Wyjdź z trybu szukania shaderów (Esc)
options.dynamic_lights=Dynamiczne światła
options.dynamic_lights.tooltip=Włącza dynamiczne światła. Tryby: OFF. - wyłączone, FASTEST - opóźnienie 500ms, FAST - opóźnienie 250ms, FANCY - brak opóźnienia. Wyłączone, jeśli shadery są w użyciu.
options.dynamic_lights.tooltip=Włącza dynamiczne światła. Tryby: OFF - wyłączone, FASTEST - opóźnienie 500ms, FAST - 250ms delay, FANCY - opóźnienie 50ms, REALTIME - brak opóźnienia.
options.dynamic_lights_shader_force=Dynamiczne światła - Wymuszone z shaderami
options.dynamic_lights_shader_force.tooltip=Włączone - Dynamiczne światła są widoczne nawet jeśli shader jest aktywny. Wyłączone - dynamiczne światła są wyłączone gdy shader jest aktywny.
options.dynamic_lights_frustum_culling=Algorytm chowania frustum
options.dynamic_lights_frustum_culling.tooltip=Przebudowuje chunki jedynie dla widocznych źródeł światła. Zmniejsza ilość pracy GPU dla świateł znajdujących się poza widokiem.
options.dynamic_lights_adaptive_ticking=Adaptacyjne ticki
options.dynamic_lights_adaptive_ticking.tooltip=Odświeża odległe i niewidoczne źródła światła z mniejszą częstotliwością. Działa razem z globalnym ustawieniem trybu.
options.dynamic_lights_slow_dist=Zasięg powolnego odświeżania.
options.dynamic_lights_slow_dist.tooltip=Zasięg w blokach gdzie światła są odświeżane z częstotliwością 1/5. Zalecane: 32 bloki (~2 chunki).
options.dynamic_lights_slower_dist=Zasięg wolniejszego odświeżania.
options.dynamic_lights_slower_dist.tooltip=Zasięg w blokach gdzie światła są odświeżane z częstotliwością 1/10. Zalecane: 64 bloki (~4 chunki).
options.dynamic_lights_background_dist=Zasięg odświeżania w tle.
options.dynamic_lights_background_dist.tooltip=Zasięg w blokach gdzie światła są odświeżane z częstotliwością 1/20 (raz na sekundę). Również używane do świateł poza widokiem.
options.dynamic_lights_cull_timeout=Limit czasu chowania (ticki)
options.dynamic_lights_cull_timeout.tooltip=Maksymalna ilość ticków ukryty chunk może czekać zanim zostanie zmuszony do przebudowania. Mniejsza = większa responsywność ale więcej pracy dla GPU.
options.dynamiclights.page=Dynamiczne światła
options.dynamiclights.entities=Konfiguruj byty...
options.dynamiclights.entities.tooltip=Wybierz, które typy bytów mogą emitować dynamiczne światła.
options.dynamiclights.entities.title=Dynamiczne światła - byty
options.dynamiclights.entities.info=Przełącz które typy bytów mogą emitować dynamiczne światła. Niektóre wpisy służą zapewnieniu zgodności z modami.
options.dynamiclights.entities.search=Wyszukaj (@mod, $on, $off)
options.dynamiclights.entities.enableall=Włącz wszystkie
options.dynamiclights.entities.disableall=Wyłącz wszystkie
options.dynamiclights.entities.enableshown=Włącz widoczne
options.dynamiclights.entities.disableshown=Wyłącz widoczne
options.angelica.search.no_results=Brak pasujących opcji
options.angelica.fontconfig=Ustawienia czcionki...
options.angelica.fontconfig.title=Ustawienia czcionki
options.angelica.fontconfig.currentfonts=Główna czcionka: §e%s§r Zastępcza czcionka: §b%s
options.angelica.fontconfig.searchprompt=§7§oWyszukaj czcionki...
options.angelica.fontconfig.testareaprompt=§7§oObszar testowy...
options.angelica.fontconfig.testareainfo=Wpisz poniżej tekst, aby sprawdzić, jak się wyświetla. Do formatowania używaj znaku &.
options.angelica.fontconfig.testareaexample=&d&lHerobrine'a&r &ntutaj &7&m&oNIE&r było
options.angelica.fontconfig.font_selection_info=Kliknij lewym przyciskiem myszy pozycję na liście, aby ustawić czcionkę główną. Kliknij prawym przyciskiem myszy, aby ustawić czcionkę zastępczą. Kolejność wyświetlania: czcionka główna, czcionka zastępcza, czcionka Unicode.
options.angelica.fontconfig.disable_custom_font=Wyłącz czcionkę niestandardową
options.angelica.fontconfig.enable_custom_font=Włącz czcionkę niestandardową
options.angelica.fontconfig.reset_values=Zresetuj wartości
options.angelica.fontconfig.no_fonts_warning=Niestandardowe czicionki nie są dostępne na twoim systemie. Sprawdź logi, aby uzyskać więcej informacji.
options.angelica.fontconfig.font_quality=Jakość czcionki: %s
options.angelica.fontconfig.shadow_offset=Przesunięcie cienia: %s
options.angelica.fontconfig.shadow_copies=Kopie cienia: %s
options.angelica.fontconfig.bold_copies=Pogrubione kopie: %s
options.angelica.fontconfig.glyph_scale=Skala znaków: %s
options.angelica.fontconfig.glyph_aspect=Aspekt znaków: %s
options.angelica.fontconfig.whitespace_scale=Rozmiar odstępu: %s
options.angelica.fontconfig.glyph_spacing=Skalowanie znaków: %s
options.angelica.fontconfig.font_aa_strength=Poziom wygładzania czcionki: %s
options.angelica.fontconfig.aamode.aa_none=Wygładzanie czcionki: OFF
options.angelica.fontconfig.aamode.aa_4x=Wygładzanie czcionki: 4x MSAA
options.angelica.fontconfig.aamode.aa_16x=Wygładzanie czcionki: 16x MSAA
options.angelica.fontconfig.font_brightness=Jasność czcionki: %s
options.angelica.fontconfig.custom_font_scale=Niestandardowa skala czcionki: %s
options.angelica.fontconfig.shadow_offset_unicode=Przesunięcie cienia Unicode: %s
options.angelica.fontconfig.font_quality.tooltip=Jakość w której niestandardowe czcionki są renderowane. Zmniejsza rozpikselowanie kosztem wydajnośći i pamięci.
options.angelica.fontconfig.shadow_offset.tooltip=Zasięg w którym cienie czcionek są renderowane. Również wpływa na renderowanie §lpogrubionego§r tekstu.
options.angelica.fontconfig.shadow_copies.tooltip=Liczba cienie do renderowania za tekstem, każdy przesunięty aby uzyskać fałszywy efekt trójwymiaru.
options.angelica.fontconfig.bold_copies.tooltip=Liczba kopii tekstu rysowanych podczas renderowania pogrubionego tekstu. Może zwiększyć jakość pogrubionego tekstu cieńszych czcionek.
options.angelica.fontconfig.glyph_scale.tooltip=Skala każdego pojedynczego znaku. Ekstremalne wartości mogą sprawić, że gra będzie niegrywalna.
options.angelica.fontconfig.glyph_aspect.tooltip=Proporcje każdego pojedynczego znaku.
options.angelica.fontconfig.whitespace_scale.tooltip=Współczynnik skalowania znaków pustych, takich jak spacje.
options.angelica.fontconfig.glyph_spacing.tooltip=Odstęp między poszczególnymi znakami.
options.angelica.fontconfig.font_aa_strength.tooltip=Odstępy między próbkami stosowane w wygładzaniu czcionek. Wysokie wartości mogą powodować, że czcionki będą rozmyte, natomiast niskie wartości mogą powodować efekt aliasingu. Sprawdź, jakie ustawienia najlepiej sprawdzają się w Twoim przypadku.
options.angelica.fontconfig.aamode.tooltip=Tryb wygładzania czcionek. Może znacznie poprawić wygląd i czytelność tekstu, który jest skalowany lub oglądany pod kątem, kosztem wydajności.
options.angelica.fontconfig.font_brightness.tooltip=Wpływa na grubość czcionek z wygładzaniem krawędzi. Może poprawić czytelność niektórych tekstów. Wartość 0,5 pozostawia współczynnik alfa bez zmian. Skrajne wartości mogą sprawić, że tekst będzie wyglądał na postrzępiony.
options.angelica.fontconfig.custom_font_scale.tooltip=Współczynnik skalowania, który wpływa na rozmiar niestandardowych czcionek. Należy go dostosować, jeśli tekst wyświetlany czcionką, którą wybrałeś, wydaje się zbyt mały lub zbyt duży w porównaniu z tekstem wyświetlanym bez użycia czcionek niestandardowych.
options.angelica.fontconfig.shadow_offset_unicode.tooltip=Współczynnik przesunięcia cienia stosowany wyłącznie do czcionki Unicode. Ustaw tę wartość na 1,0x, jeśli na przykład korzystasz z paczki tekstur, która zastępuje fragmenty czcionki Unicode przypominające MC.
options.angelica.hudCaching=Buforowanie HUD
options.angelica.hudCaching.tooltip=Zapisuje HUD do bufora klatek i ponownie wykorzystuje go w kolejnych klatkach, aby zmniejszyć obciążenie związane z renderowaniem. Wymaga włączenia funkcji buforowania HUD podczas uruchamiania. [Semi-stabilne]
options.angelica.droppedItemLimit=Limit renderowania upuszczonych przedmiotów
options.angelica.droppedItemLimit.tooltip=Maksymalna liczba upuszczonych przedmiotów, które zostaną wyświetlone. Niższe wartości mogą poprawić wydajność.
options.angelica.mobSpawnerRenderDistance=Odległość renderowania spawnera mobów
options.angelica.mobSpawnerRenderDistance.tooltip=Odległość renderowania dla wirującego moba wewnątrz spawnera mobów. Ustaw na 0, aby wyłączyć renderowanie.
options.angelica.shadowGraphAngleDelta=Próg odbudowy wykresu cienia.
options.angelica.shadowGraphAngleDelta.tooltip=O ile ciało niebieskie może się przemieścić, zanim mapa cieni zostanie zaktualizowana. Należy pamiętać, że ta opcja jest wyłączona, gdy pakiet shaderów korzysta z funkcji wokselizacji.
options.angelica.shadowGraphAngleDelta.always=Zawsze
options.angelica.shadowGraphHorizonScale=Zmniejszenie horyzontu wykresu cieni
options.angelica.shadowGraphHorizonScale.tooltip=Reguluje częstotliwość odświeżania mapy cieni, gdy ciało niebieskie znajduje się blisko horyzontu. Częstotliwość ma przebieg sinusoidalny.
options.angelica.shadowGraphHorizonScale.off=Wył.
options.angelica.itemdisplaylistcount=Limit pamięci podręcznej renderera elementów
options.angelica.itemdisplaylistcount.tooltip=Górna granica liczby siatek przedmiotów, które mają być buforowane w zoptymalizowanym rendererze przedmiotów, obejmująca VBO oraz szablony przedmiotów upuszczanych i trzymanych. Wyższe wartości znacznie poprawią wydajność, ale mogą spowodować większe zużycie pamięci i pamięci VRAM.
options.angelica.threadedChunkBuilding=Wielowątkowe budowanie chunków
options.angelica.threadedChunkBuilding.tooltip=Jeśli włączone, tworzenie siatki chunków będzie odbywać się równolegle na wielu wątkach, co znacznie przyspieszy proces budowania chunków. Wyłącz tę opcję, jeśli wystąpią problemy z renderowaniem z modami wykorzystującymi niestandardowe renderery bloków. Wymaga ponownego załadowania renderera.
options.angelica.parallelAtlasDecode=Równoległe dekodowanie tekstur
options.angelica.parallelAtlasDecode.tooltip=Dekoduje tekstury bloków i obiektów na kilku wątkach. Szybsze uruchamianie i ponowne ładowanie zasobów. Wyłącz tę opcję, jeśli brakuje tekstur lub są one uszkodzone.
options.angelica.parallelAtlasMipmaps=Równoległe mipmapy
options.angelica.parallelAtlasMipmaps.tooltip=Generuje mipmapy atlasu tekstur na kilku wątkach. Szybsze uruchamianie i ponowne ładowanie zasobów. Wyłącz tę opcję, jeśli brakuje tekstur lub są one uszkodzone.
options.angelica.batchedAtlasUpload=Zbiorcze przesyłanie atlasu
options.angelica.batchedAtlasUpload.tooltip=Przesyłaj atlas tekstur dużymi partiami, a nie po jednej teksturze na raz.
options.angelica.workerThreads=Wątki
options.angelica.workerThreads.tooltip=Wątki wykorzystywane do ładowania tekstur podczas uruchamiania oraz ładowania pakietów shaderów. Domyślnie wybierana jest liczba oparta na liczbie rdzeni procesora.
options.angelica.atlasPacks=Paczki ładujace tekstury...
options.angelica.atlasPacks.tooltip=Wybiera które paczki zasobów mogą zostać odczytane na wielu wątkach podczas ładowania tekstur.
options.angelica.atlasPacks.title=Ładowanie tekstur paczek zasobów
options.angelica.atlasPacks.info=Zaznaczone paczki ładują tekstury równolegle. Zmiany zostaną zastosowane przy następnym odświeżeniu zasobów (F3+T).
options.angelica.atlasPacks.builtin=wbudowany
options.angelica.gpuCullingMode=Tryb chowania GPU
options.angelica.gpuCullingMode.tooltip=Wybiera przebieg chowania chunków. Opcja „Tylko procesor” wykorzystuje emiter procesora. Opcja „Compute” wykonuje frustum culling na procesorze graficznym. Wymaga GL 4.3 / ARB_compute_shader + SSBO + Indirect Multi-Draw.
options.angelica.gpuCullingMode.cpu_only=Tylko procesor
options.angelica.gpuCullingMode.compute=Compute
options.angelica.overriddenBy=Nadpisane przez -D%s
options.angelica.status.restart=Uruchom ponownie aby zastosować
options.angelica.renderer.page=Silnik renderowania
options.angelica.renderer.link=Silnik renderowania
options.angelica.renderer.link.tooltip=Backend graficzny którego używa gra. Kliknij aby otworzyć stronę silnika renderującego.
options.angelica.renderer.backend=Backend
options.angelica.renderer.backend.tooltip=API graficzne używane do renderowania gry. OpenGL jest domyślne. SDL GPU renderuje przez Vulkan, Metal lub Direct3D 12 i wraca do OpenGL jeśli żadne API nie może zostać uruchomione. Zmiana zacznie obowiązywać po ponownym uruchomieniu gry.
options.angelica.renderer.backend.experimental=SDL GPU jest eksperymentalne: oczekuj crashy i błędów renderowania.
options.angelica.renderer.backend.unavailable=SDL GPU jest niedostępne: %s
options.angelica.renderer.backend.opengl=OpenGL
options.angelica.renderer.backend.sdl_gpu=SDL GPU (Eksperymentalne)
options.angelica.renderer.driver=Sterownik SDL GPU
options.angelica.renderer.driver.tooltip=APU graficzne używane przez backend SDL GPU. Jeśli wybrany sterownik nie może zostać uruchomiony, SDL GPU spróbuje użyć swojego domyślnego, a później OpenGL. Ma Zmiana zacznie obowiązywać po ponownym uruchomieniu gry.
options.angelica.renderer.driver.tooltip.macos=Automatyczny używa API Metal. Vulkan wymaga MoltenVK: włóż angelica-moltenvk.jar do folderu mods lub zainstaluj MoltenVK. Metal jest użyty jeśli Vulkan nie może zostać uruchomiony.
options.angelica.renderer.driver.tooltip.windows=Automatyczny używa API Vulkan.
options.angelica.renderer.driver.tooltip.d3d12=API Direct3D 12 jest bardzo eksperymentalne i prawdopobonie działa gorzej niżVulkan.
options.angelica.renderer.driver.tooltip.other=Automatyczny używa API Vulkan.
options.angelica.renderer.driver.auto=Automatyczny
options.angelica.renderer.driver.vulkan=Vulkan
options.angelica.renderer.driver.metal=Metal
options.angelica.renderer.driver.d3d12=Direct3D 12
options.angelica.renderer.glVersion=Wersja OpenGL
options.angelica.renderer.glVersion.tooltip=Wersja kontekstu OpenGL do zażądania. Automatyczna używa najwyższej jaką wspiera twój sprzęt. Stosowana kiedy gra używa API OpenGL, również jeśli SDL GPU nie może zostać uruchomione. Zmiana zacznie obowiązywać po ponownym uruchomieniu gry.
options.angelica.renderer.glVersion.auto=Automatyczna
options.angelica.renderer.status=Stan
options.angelica.renderer.status.tooltip=Czy działający backend jest zgodny z powyższymi ustawieniami.
options.angelica.renderer.status.active=Aktywny
options.angelica.renderer.status.forced=Wymuszony przez flagę JVM
options.angelica.renderer.status.failed=SDL GPU nie mogło zostać uruchomione, sprawdź logi
options.angelica.renderer.status.reset=Zresetowano do OpenGL po nieudanym starcie
options.angelica.renderer.active=Używane API
options.angelica.renderer.active.tooltip=Backend którego gra używa do renderowania.
options.angelica.renderer.moltenvk=MoltenVK
options.angelica.renderer.moltenvk.tooltip=Gdzie znaleziono MoltenVK. Sterownik SDL GPU Vulkan potrzebuje go na macOS.
options.angelica.renderer.moltenvk.env=SDL_VULKAN_LIBRARY
options.angelica.renderer.moltenvk.jar=angelica-moltenvk.jar
options.angelica.renderer.moltenvk.system=Systemowa instalacja
options.angelica.renderer.moltenvk.none=Nie znaleziony
options.angelica.tracy.page=Profiler Tracy
options.angelica.tracy.enable=Włącz Tracy
options.angelica.tracy.enable.tooltip=Włącz moduł profilujący Tracy. Wymaga obecności pliku angelica-tracy.jar w ścieżce klasy. Zmiana zacznie obowiązywać po ponownym uruchomieniu gry.
options.angelica.tracy.allowRemote=Zezwól na zdalne połączenia
options.angelica.tracy.allowRemote.tooltip=Zezwól na połączenia Tracy z innych komputerów, a nie tylko z localhost. Zmiana zacznie obowiązywać po ponownym uruchomieniu gry.
options.angelica.tracy.fineZones=Dokładne strefy
options.angelica.tracy.fineZones.tooltip=Włącza strefy Tracy dla znacznie większej liczby punktów połączeń. Więcej szczegółów, większe obciążenie. Zmiana wchodzi w życie po ponownym uruchomieniu gry.
options.angelica.tracy.maxSrcLocs=Maksymalna liczba lokalizacji źródeł
options.angelica.tracy.maxSrcLocs.tooltip=Maksymalna liczba różnych stref Tracy i punktów wywołania komunikatów, które mają zostać wstępnie przydzielone. Zmiana wchodzi w życie po ponownym uruchomieniu gry.
options.angelica.tracy.status=Stan
options.angelica.tracy.status.tooltip=Czy Tracy jest obecnie uruchomione i czy ustawienia są zgodne z powyższymi.
options.angelica.tracy.status.running=Uruchomione
options.angelica.tracy.status.runningForced=Uruchomione (wymuszone przez flagę JVM)
options.angelica.tracy.status.failed=Nie udało się uruchomić, sprawdź log
options.angelica.tracy.status.off=Wył.
options.angelica.tracy.captureLength=Długość ujęcia
options.angelica.tracy.captureLength.tooltip=Domyślna długość ujęcia Tracy rozpoczętego z tej strony lub /angelica tracy start. Ujęcia są zapisywane w katalogu angelica/profiles i otwierane za pomocą Tracy Profiler 0.14.
options.angelica.tracy.captureAction=Ujęcie
options.angelica.tracy.captureAction.tooltip=Uruchamia lub zatrzymuje rejestrację ujęcie w Tracy. Plik jest zapisywany w katalogu angelica/profiles i otwierany za pomocą Tracy Profiler 0.14.
options.angelica.tracy.captureAction.start=Zacznij ujęcie
options.angelica.tracy.captureAction.stop=Zatrzymaj ujęcie
options.angelica.tracy.captureAction.saving=Zapisywanie...
pack.iris.select.title=Wybierz
pack.iris.configure.title=Konfiguruj
pack.iris.list.label=+ Przeciągnij i upuść paczki shaderów, aby je dodać
pack.iris.list.label.lwjgl2=Zainstaluj lwjgl3ify aby móc przeciągać i upuszczać paczki shaderów
label.iris.true=Wł.
label.iris.false=Wył.
sodium.options.use_render_pass_optimization.name=Optymalizacja przebiegu renderowania
sodium.options.use_render_pass_optimization.tooltip=Analizuje przezroczystość sprite'ów, aby automatycznie obniżać poziom złożoności operacji renderowania (np. z półprzezroczystego na wycięty, z wyciętego na nieprzezroczysty), gdy sprite nie wymaga bardziej zasobochłonnej operacji. Zmniejsza liczbę wywołań rysowania i nadmiernego rysowania.
options.angelica.aggressiveChunkLoading=Agresywne ładowanie chunków
options.angelica.aggressiveChunkLoading.tooltip=Renderuje chunki natychmiast po załadowaniu, nawet jeśli sąsiednie chunki nie są jeszcze gotowe. Poprawia odczuwalną szybkość ładowania na granicach odległości renderowania. Chunki odbudowują się automatycznie po załadowaniu sąsiednich chunków. Przydatne w przypadku serwerów o małej odległości renderowania.
angelica.debug.verbose.on=Szczegółowe F3: widoczne
angelica.debug.verbose.off=Szczegółowe F3: ukryte
angelica.debug.group.rendering=Renderowanie
angelica.debug.group.position=Pozycja
angelica.debug.group.looking_at_block=Oglądany blok
angelica.debug.group.memory=Pamięć
angelica.debug.group.system_specs=Specyfikacja systemu
angelica.debug.group.forge=Forge
angelica.debug.group.angelica=Angelica
angelica.debug.group.shaders=Shadery
angelica.debug.group.other=Inne
angelica.debug.group.help=Pomoc
angelica.debug.options.title=Opcje debugowania
angelica.debug.options.warning=Te opcje służą wyłącznie do celów testowych. Mogą one spowolnić działanie komputera, spowodować awarię gry lub zjeść twojego kamiennego zwierzaka.
angelica.debug.options.search=Wyszukaj...
angelica.debug.options.category.text=Tekst na ekranie debugowania
angelica.debug.options.category.renderer=Renderery debugowania
angelica.debug.options.category.layout=Układ
angelica.debug.options.profile.default=Domyślny profil
angelica.debug.options.profile.performance=Profil wydajności
angelica.debug.options.failed.tooltip=Ten wpis spowodował błąd i jest wyłączony do czasu ponownego uruchomienia. Sprawdź dziennik.
angelica.debug.options.flat_notice=Klasyczny układ F3 jest włączony (ekran zgrupowany jest wyłączony lub renderer czcionek Angelica jest wyłączony): obowiązują wyłącznie te wpisy.
angelica.debug.options.text_size=Rozmiar tekstu F3
angelica.debug.options.text_size.match=Dopasuj do GUI
angelica.debug.options.text_size.auto=Automatycznie
angelica.debug.options.text_size.tooltip=Dopasuj do GUI: taki sam rozmiar jak skala interfejsu użytkownika. Automatycznie: połowa największego rozmiaru, który mieści się w oknie. Nigdy większy niż skala GUI.
angelica.debug.options.category.panels=Panele
angelica.debug.options.panels.tooltip=Użyj grup=%s w regułach linii F3 (konfiguracja Angelica compat), aby przenieść linię do tego panelu.
angelica.debug.options.status.overlay=W nakładce
angelica.debug.options.status.always=Zawsze
angelica.debug.entry.minecraft.game_version=Wersja gry
angelica.debug.entry.minecraft.fps=FPS
angelica.debug.entry.minecraft.tps=Czas ticku serwera
angelica.debug.entry.minecraft.player_position=Pozycja gracza
angelica.debug.entry.minecraft.player_section_position=Wybierz pozycję
angelica.debug.entry.minecraft.player_speed=Prędkość gracza
angelica.debug.entry.minecraft.biome=Biom
angelica.debug.entry.minecraft.light_levels=Poziomy światła
angelica.debug.entry.minecraft.heightmap=Mapa wysokości
angelica.debug.entry.minecraft.chunk_render_stats=Statystyki renderowania chunków
angelica.debug.entry.minecraft.entity_render_stats=Statystyki renderowania bytów
angelica.debug.entry.minecraft.particle_render_stats=Statystyki cząstek
angelica.debug.entry.minecraft.chunk_source_stats=Statystyki pamięci podręcznej chunków
angelica.debug.entry.minecraft.post_effect=Efekty postprocesu
angelica.debug.entry.minecraft.memory=Pamięć
angelica.debug.entry.minecraft.system_specs=Specyfikacja systemu
angelica.debug.entry.minecraft.looking_at_block=Oglądany blok
angelica.debug.entry.forge.brandings=Wersje Forge i modów
angelica.debug.entry.angelica.renderer=Renderer Angelica
angelica.debug.entry.angelica.renderer_verbose=Szczegółowe informacje o rendererze
angelica.debug.entry.angelica.shaders=Paczka shaderów
angelica.debug.entry.angelica.dynamic_lights=Dynamiczne światła
angelica.debug.entry.angelica.fog=Mgła
angelica.debug.entry.angelica.animations=Animacje tekstur
angelica.debug.entry.angelica.other_left=Inne mody (lewo)
angelica.debug.entry.angelica.other_right=Inne mody (prawo)
angelica.debug.entry.angelica.help=Pomoc dotycząca klawiszy
angelica.debug.entry.minecraft.3d_crosshair=Celownik 3D
angelica.debug.entry.minecraft.entity_hitboxes=Hitboxy bytów
angelica.debug.entry.minecraft.chunk_borders=Granice chunków
angelica.debug.entry.minecraft.chunk_section_visibility=Widoczność sekcji
angelica.debug.entry.minecraft.visualize_block_light_levels=Poziomy światła bloków
angelica.debug.entry.minecraft.visualize_sky_light_levels=Poziomy światła nieba
angelica.debug.entry.minecraft.visualize_heightmap=Znaczniki mapy wysokości
angelica.debug.entry.minecraft.visualize_collision_boxes=Prostokąty kolizji
angelica.debug.entry.minecraft.visualize_solid_faces=Powierzchnie pełne
angelica.debug.entry.minecraft.visualize_water_levels=Poziomy wody
angelica.debug.entry.minecraft.visualize_entity_supporting_block=Blok wspierający byt
angelica.debug.entry.angelica.chunk_minimap=Minimapa chunków
angelica.debug.entry.minecraft.game_version.tooltip=Wiersz z wersją Minecrafta u góry ekranu debugowania
angelica.debug.entry.minecraft.fps.tooltip=Liczba klatek na sekundę, aktualne ograniczenie liczby klatek oraz aktualizacje chunków – wyświetlane w lewym górnym rogu
angelica.debug.entry.minecraft.tps.tooltip=Ticki zintegrowanego serwera w milisekundach; tylko w trybie jednoosobowym
angelica.debug.entry.minecraft.player_position.tooltip=Twoja dokładna pozycja, blok, chunk, kierunek orientacji i wymiar
angelica.debug.entry.minecraft.player_section_position.tooltip=Twoja pozycja w bloku względem sekcji 16x16x16, w której się znajdujesz
angelica.debug.entry.minecraft.player_speed.tooltip=Twoja prędkość w blokach na tick, prędkość chodzenia i latania oraz to, czy znajdujesz się na ziemi
angelica.debug.entry.minecraft.biome.tooltip=Biom w miejscu, w którym się znajdujesz, wraz z modem z którego jest
angelica.debug.entry.minecraft.light_levels.tooltip=Poziom światła w Twojej pozycji, w podziale na światło nieba i światło bloku
angelica.debug.entry.minecraft.heightmap.tooltip=Wysokość górnego bloku w Twojej pozycji oraz górnej wypełnionej części Twojego chunku
angelica.debug.entry.minecraft.chunk_render_stats.tooltip=Widoczne i całkowite sekcje renderowania, zasięg renderowania oraz liczba animowanych sprite’ów
angelica.debug.entry.minecraft.entity_render_stats.tooltip=Ile obiektów zostało wyrenderowanych, ukrytych lub odrzuconych w tej klatce spośród tych, które zostały załadowane
angelica.debug.entry.minecraft.particle_render_stats.tooltip=Liczba cząstek z menedżera cząstek Vanilli.
angelica.debug.entry.minecraft.chunk_source_stats.tooltip=Statystyki dotyczące pamięci podręcznej chunków klienta z dostawcy świata
angelica.debug.entry.minecraft.post_effect.tooltip=Nazwa aktywnej grupy post-procesowej Vanilli
angelica.debug.entry.minecraft.memory.tooltip=Wykorzystanie sterty Javy, przydzielona sterta, tempo alokacji oraz pamięć bufora bezpośredniego
angelica.debug.entry.minecraft.system_specs.tooltip=Wersja Javy, liczba rdzeni procesora, rozdzielczość ekranu, układ graficzny i wersja OpenGL
angelica.debug.entry.minecraft.looking_at_block.tooltip=Współrzędne, nazwa w rejestrze i metadane bloku znajdującego się pod celownikiem
angelica.debug.entry.forge.brandings.tooltip=MCP, Wersje FML i Forge oraz liczba załadowanych i aktywnych modów
angelica.debug.entry.angelica.renderer.tooltip=Wersja Angelica, backend renderowania i rozmiary bufora terenu
angelica.debug.entry.angelica.renderer_verbose.tooltip=Adds detailed renderer timings to the Angelica panel, like F3+V
angelica.debug.entry.angelica.shaders.tooltip=Aktywna paczka shaderów i szczegóły dotyczące jego potoku renderowania; pokazuje off kiedy shadery są wyłączone
angelica.debug.entry.angelica.dynamic_lights.tooltip=Liczba dynamicznych źródeł światła, aktualizacji w tej klatce i oczekujące przebudowy chunków
angelica.debug.entry.angelica.fog.tooltip=Stan mgły: włączony lub wyłączony, tryb, odległość początkowa i końcowa, gęstość oraz kolor
angelica.debug.entry.angelica.animations.tooltip=Tryb animacji tekstur; ukryty, chyba że włączono przyspieszenie animacji
angelica.debug.entry.angelica.other_left.tooltip=Wiersze dodawane przez inne modyfikacje po lewej stronie ekranu debugowania
angelica.debug.entry.angelica.other_right.tooltip=Wiersze dodawane przez inne modyfikacje po prawej stronie ekranu debugowania
angelica.debug.entry.angelica.help.tooltip=Przypomnienie skrótów klawiszowych F3, wyświetlane przy otwartym ekranie debugowania
angelica.debug.entry.minecraft.3d_crosshair.tooltip=Zastępuje celownik kolorowymi liniami osi X, Y i Z, które dostosowują się do kierunku patrzenia
angelica.debug.entry.minecraft.entity_hitboxes.tooltip=Pokazuje hitboxy bytów, tak jak F3+B
angelica.debug.entry.minecraft.chunk_borders.tooltip=Wskazuje chunk i sekcję, w której się znajdujesz, tak jak F3+G
angelica.debug.entry.minecraft.chunk_section_visibility.tooltip=Oznacza pobliskie sekcje, których ściany nie stykają się ze sobą, co powoduje schowanie terenu znajdującego się za nimi
angelica.debug.entry.minecraft.visualize_block_light_levels.tooltip=Rysuje wartość jasności na każdym pobliskim bloku, który nie jest całkowicie ciemny
angelica.debug.entry.minecraft.visualize_sky_light_levels.tooltip=Rysuje wartość światła nieba na każdym pobliskim bloku, który nie ma pełnego oświetlenia nieba
angelica.debug.entry.minecraft.visualize_heightmap.tooltip=Oznacza górny blok każdej kolumny w sąsiednich chunkach
angelica.debug.entry.minecraft.visualize_collision_boxes.tooltip=Wskazuje zderzające się obiekty w promieniu kilku bloków od Ciebie
angelica.debug.entry.minecraft.visualize_solid_faces.tooltip=Barwi powierzchnie bloków pełnych, które są widoczne dla sąsiedniego, nieprzezroczystego bloku znajdującego się w pobliżu
angelica.debug.entry.minecraft.visualize_water_levels.tooltip=Pokazuje pobliskie zbiorniki wodne wraz z ich poziomem przepływu w skali od 1 do 8
angelica.debug.entry.minecraft.visualize_entity_supporting_block.tooltip=Pokazuje kontury bloków, na których stoją poszczególne pobliskie byty; twój jest czerwony, a pozostałe zielone
angelica.debug.entry.angelica.chunk_minimap.tooltip=Mała mapa stanów renderowania chunków w rogu
angelica.debug.chunk_borders.on=Granice chunków: Widoczne
angelica.debug.chunk_borders.off=Granice chunków: ukryte
angelica.debug.help.options=F3 + F6 = Edytuj opcje debugowania
angelica.debug.help.hitboxes=F3 + B = Pokaż hitboxy
angelica.debug.help.chunk_borders=F3 + G = Pokaż granice chunków
angelica.debug.help.verbose=F3 + V = Szczegółowe informacje o rendererze
angelica.debug.help.graphs=Alt + F3 = Wykresy FPS i TPS
options.angelica.texteffects.rgb_colors=Kolory RGB
options.angelica.texteffects.rgb_colors.tooltip=Włącz kolory szesnastkowe za pomocą składni %s
options.angelica.texteffects.gradients=Gradienty
options.angelica.texteffects.gradients.tooltip=Płynne przejścia kolorów przez
options.angelica.texteffects.rainbow=Tęcza
options.angelica.texteffects.rainbow.tooltip=Cykliczne kolory tęczy przez
options.angelica.texteffects.wave_text=Tekst falowy
options.angelica.texteffects.wave_text.tooltip=Animowany, podskakujący tekst przez
options.angelica.texteffects.wave_amplitude=Amplituda
options.angelica.texteffects.wave_amplitude.tooltip=Odległość, na jaką odbija się tekst falisty (domyślnie 2,0)
options.angelica.texteffects.dinnerbone_text=Tekst Dinnerbone
options.angelica.texteffects.dinnerbone_text.tooltip=Tekst odwrócony do góry nogami przez
options.angelica.texteffects.drop_shadow=Rzucany cień
options.angelica.texteffects.drop_shadow.tooltip=Cień rzucany dla poszczególnych segmentów za pomocą %s, cień kolorowy za pomocą
options.angelica.texteffects.ampersand_conversion=Konwersja znaku „&”
options.angelica.texteffects.ampersand_conversion.tooltip=Konwertuj kody prefiksowe %s na kody formatujące w momencie renderowania
sodium.options.multidraw_mode.name=Tryb Multi-Draw
sodium.options.multidraw_mode.tooltip=Określa sposób wysyłania poleceń rysowania podczas renderowania terenu. §lDirect§r używa glMultiDrawElementsBaseVertex (domyślna, GL 3.2+). §lIndirect§r renderowanie pośrednie za pomocą procesora graficznego (GL 4.3+), co może zmniejszyć obciążenie procesora na niektórych urządzeniach, i jest wymagane do chowania GPU / Hi-Z. §lIndywidualny§r wysyła pojedyncze wywołania rysowania.
sodium.options.multidraw_mode.direct=Bezpośredni
sodium.options.multidraw_mode.indirect=Pośredni
sodium.options.multidraw_mode.individual=Indywidualny
sodium.options.upload_method.name=Metoda przesyłania strumieniowego
sodium.options.upload_method.tooltip=Zmienia sposób przesyłania danych do procesora graficznego. Niektóre opcje mogą poprawić wydajność w przypadku niektórych sterowników.
sodium.options.upload_method.buffer_data=BufferData
sodium.options.upload_method.buffer_subdata=BufferSubData
sodium.options.upload_method.map_buffer=MapBufferRange
sodium.options.enable_deferred_batching.name=Dozowanie cząstek
sodium.options.enable_deferred_batching.tooltip=Renderuje cząstki jako instancjonowane czworokąty lub, jeśli instancjonowanie nie jest dostępne, jako połączone obiekty. Wyłącz tę opcję, jeśli cząstki są renderowane nieprawidłowo.
@@ -7,33 +7,77 @@ options.graphics.ultra=Ultra
options.off=Wył.
options.on=Wł.
options.cloud_height=Wysokość chmur
options.cloud_height.tooltip=Wysokość, na której chmury są renderowane w światach które same tego nie ustawiają. Światy, które ustawiają własną wartość, zachowują ją, przesuniętą o różnicę między tym ustawieniem a wartością 128.
options.cloud_scale=Skala chmur
options.cloud_scale.tooltip=Skala chmur
options.gui_background=Tło
options.gui_background.tooltip=Tło
options.downfall_distance=Efekty pogody
options.downfall_distance.tooltip=Efekty pogody
options.dynamic_fov=Dynamiczne pole widzenia
options.dynamic_fov.tooltip=Jeśli włączone, pole widzenia gry będzie zmieniać się na podstawie szybkości gracza.
options.dynamic_fov.tooltip=W zależności od trybu, pole widzenia w grze będzie się zmieniać w zależności od prędkości gracza lub w wyniku określonych interakcji z modami (w tym powiększania za pomocą narzędzi lub celowników broni).
options.horizon=Horyzont
options.horizon.tooltip=Horyzont
options.terrain_fog=Mgła terenowa
options.terrain_fog.tooltip=Mgła terenowa (aka Mgła odległości widzenia)
options.fog_near_distance=Odległość mgły
options.fog_near_distance.tooltip=Odległość od gracza, przy której zacznie się pojawiać mgła ograniczająca widoczność (przezroczystość 0%, a następnie liniowy wzrost aż do odległości renderowania).
options.bobview_mode=Tryb animacji chodzenia
options.bobview_mode.tooltip=Zmień zachowanie animacji chodzenia tak, aby kołysała się tylko ręka lub kamera.
options.bobview.hand=Tylko ręka
options.bobview.camera=Tylko kamera
options.fov.all=Włączone
options.fov.mods=Tylko mody
options.fov.none=Wyłączone
options.mode_cloud_translucency=Przezroczystość chmur
options.mode_cloud_translucency.tooltip=Przezroczystość chmur
options.mode_clouds=Chmury
options.mode_clouds.tooltip=Chmury
options.mode_dropped_items=Upuszczone przedmioty
options.mode_dropped_items.tooltip=Jakość renderowania upuszczonych przedmiotów
options.mode_glint_inv=Efekt zaklęć w GUI
options.mode_glint_inv.tooltip=Efekt zaklęć w GUI
options.mode_glint_world=Efekt zaklęć na świecie
options.mode_glint_world.tooltip=Efekt zaklęć na świecie
options.mode_gui_background=Tła menu
options.mode_gui_background.tooltip=Tła menu
options.mode_leaves=Liście
options.mode_leaves.tooltip=Liście
options.mode_light_flicker=Migotanie światła
options.mode_light_flicker.tooltip=Migotanie światła
options.mode_shadows=Cienie bytów
options.mode_shadows.tooltip=Cienie bytów
options.mode_sky=Niebo
options.mode_sky.tooltip=Renderuj niebo
options.mode_sun_moon=Słońce i księżyc
options.mode_sun_moon.tooltip=Renderuj słońce i księżyc
options.mode_stars=Gwiazdy
options.mode_stars.tooltip=Renderuj gwiazdy
options.mode_water=Woda
options.mode_water.tooltip=Woda
options.mode_vignette=Winieta
options.mode_vignette.tooltip=Winieta
options.particles_enc_table=Cząstki zaklęć
options.particles_enc_table.tooltip=Cząstki zaklęć
options.particles_void=Cząstki próżni
options.particles_void.tooltip=Cząstki próżni
options.render_distance_clouds=Odległość renderowania chmur
options.render_distance_clouds.tooltip=Odległość renderowania chmur
options.title.detail=Ustawienia detali
options.title.other=Inne ustawienia
options.title.particle=Ustawienia cząstek
options.title.sky=Ustawienia atmosfery
options.title.video=Ustawienia graficzne
options.total_stars=Gęstość gwiazd
options.total_stars.tooltip=Gęstość gwiazd
options.void_fog=Mgła próżni
options.void_fog.tooltip=Mgła próżni
options.hurt_shake=Drganie ekranu przy obrażeniach
options.hurt_shake.tooltip=O ile mocno przechyla się kamera, gdy otrzymasz obrażenia.
options.background.random=Losowe
options.zoom_smooth=Płynne powiększanie
options.zoom_smooth.tooltip=Płynnie dostosowuje kamerę do wybranego poziomu powiększenia, zamiast przechodzić do niego natychmiastowo.
options.zoom_smooth_speed=Prędkość płynnego powiększania
options.zoom_smooth_speed.tooltip=Jak szybko funkcja płynnego powiększania osiąga docelowy poziom powiększenia. Im wyższa wartość, tym szybsze działanie; im niższa, tym bardziej płynne. Nie ma wpływu, gdy funkcja płynnego powiększania jest wyłączona.
options.zoom_cinematic=Kinowe powiększenie
options.zoom_cinematic.tooltip=Czy włączyć płynny ruch kamery kinowej. „Wyłączone” - standardowy ruch kamery w grze Minecraft, w którym kamera ustawia się w pozycji myszy. „Włączone” - nieprzerwany ruch po początkowym ruchu myszą, w którym kamera nadal się przesuwa po zatrzymaniu ruchu myszy.
@@ -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
@@ -1,20 +0,0 @@
package com.gtnewhorizons.angelica.mixins.early.celeritas.terrain;
import org.embeddedt.embeddium.impl.render.chunk.DefaultChunkRenderer;
import org.embeddedt.embeddium.impl.render.chunk.data.SectionRenderDataStorage;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = DefaultChunkRenderer.class, remap = false)
public abstract class MixinDefaultChunkRenderer {
@Unique private static final SectionRenderDataStorage.BatchCacheParams angelica$CULLED = new SectionRenderDataStorage.BatchCacheParams(true);
@Unique private static final SectionRenderDataStorage.BatchCacheParams angelica$UNCULLED = new SectionRenderDataStorage.BatchCacheParams(false);
@Redirect(method = "render", at = @At(value = "NEW", target = "org/embeddedt/embeddium/impl/render/chunk/data/SectionRenderDataStorage$BatchCacheParams"))
private SectionRenderDataStorage.BatchCacheParams angelica$sharedParams(boolean cull) {
return cull ? angelica$CULLED : angelica$UNCULLED;
}
}
@@ -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);
}
}
@@ -83,7 +83,7 @@ class DeferredMeshSchedulerTest {
scheduler = new DeferredMeshScheduler(null, builder, () -> {
slicesCreated.incrementAndGet();
return Mockito.mock(WorldSlice.class);
}, queue::add, results);
}, queue::add, r -> queue.removeIf(t -> t == r), results);
}
DeferredSectionMesh submit(boolean important, long priority) {
@@ -213,6 +213,7 @@ class DeferredMeshSchedulerTest {
assertEquals(0, h.scheduler.pendingCount());
assertEquals(0, h.scheduler.finishingCount());
assertEquals(0, h.results.size());
assertTrue(h.queue.isEmpty(), "queued runNext keeps the destroyed scheduler and its pools reachable");
Mockito.verify(queued.slice, Mockito.times(1)).releaseContext();
Mockito.verify(inFlight.slice, Mockito.times(1)).releaseContext();
assertNull(inFlight.capture);
@@ -59,4 +59,27 @@ class ObjectPoolTest {
pool.acquire();
assertEquals(4, calls.get());
}
@Test
void trimKeepsRecentPeakThenHalvesExcess() {
final long w = ObjectPool.TRIM_WINDOW_NANOS;
final int floor = 4;
ObjectPool<Object> pool = new ObjectPool<>(Object::new, Integer.MAX_VALUE);
Object[] burst = new Object[100];
for (int i = 0; i < burst.length; i++) burst[i] = pool.acquire();
pool.trim(0, floor);
for (int i = 1; i < burst.length; i++) pool.release(burst[i]);
for (long t : new long[] {w / 2, w, 2 * w}) {
pool.trim(t, floor);
assertEquals(99, pool.freeCount(), "peak from the last two windows still holds at t=" + t);
}
int[] expected = {51, 27, 15, 9, 6, 5, 4, 4};
for (int i = 0; i < expected.length; i++) {
pool.trim((3 + i) * w, floor);
assertEquals(expected[i], pool.freeCount(), "window " + (3 + i));
}
}
}
@@ -226,32 +226,8 @@ class TesrLayerStateTest {
GLSMMatrices.setTextureMatrix(0, new Matrix4f());
});
}),
Arguments.of("foreign program and framebuffer", (Supplier<BatchScenario>) () -> {
final int program = GlTestSupport.program(GlTestSupport.TRIVIAL_VS, GlTestSupport.TRIVIAL_FS);
final int framebuffer = GLSMFramebuffer.glGenFramebuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
BatchDrawDefaults.capturePassFramebuffer();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GLSMPrograms.glUseProgram(program);
return new BatchScenario(
() -> {
assertEquals(0, GLSMPrograms.getActiveProgram(), "program during batch draw");
assertEquals(0, GLSMFramebuffer.getDrawFramebuffer(), "framebuffer during batch draw");
assertEquals(0, GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING), "driver draw framebuffer during batch draw");
},
() -> {
assertEquals(program, GLSMPrograms.getActiveProgram(), "caller program restored");
assertEquals(framebuffer, GLSMFramebuffer.getDrawFramebuffer(), "caller framebuffer restored");
assertEquals(program, GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM), "driver caller program restored");
assertEquals(framebuffer, GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING), "driver caller framebuffer restored");
},
() -> {
GLSMPrograms.glUseProgram(0);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLSMFramebuffer.glDeleteFramebuffers(framebuffer);
GLSMPrograms.glDeleteProgram(program);
});
}),
Arguments.of("foreign program and framebuffer", (Supplier<BatchScenario>) () -> foreignProgram(false)),
Arguments.of("angelica-owned program and framebuffer", (Supplier<BatchScenario>) () -> foreignProgram(true)),
Arguments.of("filter from before an item renderer's blur window", (Supplier<BatchScenario>) () -> {
final int texture = GLSMTextures.glGenTextures();
GLSMTextures.glActiveTexture(GL13.GL_TEXTURE0);
@@ -281,6 +257,35 @@ class TesrLayerStateTest {
}));
}
private static BatchScenario foreignProgram(boolean owned) {
final int program = GlTestSupport.program(GlTestSupport.TRIVIAL_VS, GlTestSupport.TRIVIAL_FS);
if (owned) DepthColorStorage.registerOwnedProgram(program);
final int framebuffer = GLSMFramebuffer.glGenFramebuffers();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
BatchDrawDefaults.capturePassFramebuffer();
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, framebuffer);
GLSMPrograms.glUseProgram(program);
return new BatchScenario(
() -> {
assertEquals(0, GLSMPrograms.getActiveProgram(), "program during batch draw");
assertEquals(0, GLSMFramebuffer.getDrawFramebuffer(), "framebuffer during batch draw");
assertEquals(0, GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING), "driver draw framebuffer during batch draw");
},
() -> {
assertEquals(program, GLSMPrograms.getActiveProgram(), "caller program restored");
assertEquals(framebuffer, GLSMFramebuffer.getDrawFramebuffer(), "caller framebuffer restored");
assertEquals(program, GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM), "driver caller program restored");
assertEquals(framebuffer, GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING), "driver caller framebuffer restored");
},
() -> {
GLSMPrograms.glUseProgram(0);
DepthColorStorage.unregisterOwnedProgram(program);
GLSMFramebuffer.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0);
GLSMFramebuffer.glDeleteFramebuffers(framebuffer);
GLSMPrograms.glDeleteProgram(program);
});
}
@ParameterizedTest(name = "{0}")
@MethodSource("batchDrawCases")
void batchDrawDoesNotInheritTheInterruptedRenderersState(String name, Supplier<BatchScenario> setup) {
@@ -18,8 +18,10 @@ import org.lwjgl.system.libffi.FFICIF;
import org.lwjgl.system.libffi.FFIType;
import org.lwjgl.system.libffi.LibFFI;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.NULL;
@@ -138,11 +140,13 @@ public final class TracyClientBackend implements TracyBackend {
private static final class SlotArena {
private final long base;
private final int strideShift;
private final AtomicReference<WeakReference<Thread>> owner;
private int depth;
SlotArena(int stride) {
SlotArena(int stride, Thread owner) {
this.strideShift = Integer.numberOfTrailingZeros(stride);
this.base = nmemAllocChecked((long) stride * ZoneStack.MAX_DEPTH);
this.owner = new AtomicReference<>(new WeakReference<>(owner));
}
long top() {
@@ -155,11 +159,23 @@ public final class TracyClientBackend implements TracyBackend {
}
}
private final ThreadLocal<SlotArena> slots = ThreadLocal.withInitial(() -> {
final SlotArena arena = new SlotArena(ctxSize);
private final ThreadLocal<SlotArena> slots = ThreadLocal.withInitial(this::claimArena);
/** Arenas outlive their threads (chunk workers are recreated per world), so reuse a dead thread's arena; owners are weak because a dead thread still holds its Runnable and through it the old world. */
private SlotArena claimArena() {
final Thread self = Thread.currentThread();
for (SlotArena arena : ARENAS) {
final WeakReference<Thread> ref = arena.owner.get();
final Thread owner = ref.get();
if ((owner == null || !owner.isAlive()) && arena.owner.compareAndSet(ref, new WeakReference<>(self))) {
arena.depth = 0;
return arena;
}
}
final SlotArena arena = new SlotArena(ctxSize, self);
ARENAS.add(arena);
return arena;
});
}
@Override
public long beginZone(long srcLoc) {
@@ -199,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");