Compare commits

...
5 Commits
43 changed files with 1040 additions and 227 deletions

No files matched your search

@@ -77,6 +77,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");
@@ -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();
@@ -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;
@@ -7,6 +7,8 @@ public final class DebugCounters {
public static final LongAdder TRANSFER_WAIT_NANOS = new LongAdder();
public static final LongAdder PRESENTS = new LongAdder();
public static final LongAdder EMPTY_ACQUIRES = new LongAdder();
public static final LongAdder PBR_ANIM_UPDATES = new LongAdder();
public static final LongAdder PBR_ANIM_DRY_RUNS = new LongAdder();
private DebugCounters() {}
}
@@ -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
@@ -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
@@ -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 -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"
+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).
#
@@ -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) {
@@ -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;
@@ -138,6 +147,8 @@ public final class FlybyRunner {
private long transferWaitStartNs;
private long presentsStart;
private long emptyAcquiresStart;
private long pbrAnimUpdatesStart;
private long pbrAnimDryRunsStart;
private long allocClientStart;
private long allocProcessStart;
private int frameCount;
@@ -187,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);
@@ -249,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;
}
@@ -557,6 +585,8 @@ public final class FlybyRunner {
this.transferWaitStartNs = DebugCounters.TRANSFER_WAIT_NANOS.sum();
this.presentsStart = DebugCounters.PRESENTS.sum();
this.emptyAcquiresStart = DebugCounters.EMPTY_ACQUIRES.sum();
this.pbrAnimUpdatesStart = DebugCounters.PBR_ANIM_UPDATES.sum();
this.pbrAnimDryRunsStart = DebugCounters.PBR_ANIM_DRY_RUNS.sum();
this.allocClientStart = AllocBean.allocatedBytes();
this.allocProcessStart = AllocBean.totalAllocatedBytes();
this.frameCount = 0;
@@ -888,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();
}
@@ -944,6 +1011,9 @@ public final class FlybyRunner {
sb.append(String.format(", presents %d, empty acquires %d",
DebugCounters.PRESENTS.sum() - this.presentsStart,
DebugCounters.EMPTY_ACQUIRES.sum() - this.emptyAcquiresStart));
sb.append(String.format(", pbr anim updates %d, dry runs %d",
DebugCounters.PBR_ANIM_UPDATES.sum() - this.pbrAnimUpdatesStart,
DebugCounters.PBR_ANIM_DRY_RUNS.sum() - this.pbrAnimDryRunsStart));
}
if (this.framesSeen > this.frameCount) sb.append(" (frame samples truncated)");
for (int i = 0; i < this.phaseCount; i++) {
@@ -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;
}
@@ -34,9 +34,6 @@ public final class PositionEntries {
}
}
private WorldProvider cachedProvider;
private String dimensionName = "";
@Override
public void display(DebugScreenDisplayer displayer, DebugContext context) {
final EntityClientPlayerMP player = context.player();
@@ -52,10 +49,7 @@ public final class PositionEntries {
facing.value(DIRECTION_NAMES[heading]).text(" (").text(TOWARDS[heading]).text(") (");
facing.value(MathHelper.wrapAngleTo180_float(player.rotationYaw), 1).text(SEPARATOR).value(MathHelper.wrapAngleTo180_float(player.rotationPitch), 1).text(')');
final WorldProvider provider = context.world().provider;
if (provider != cachedProvider) {
cachedProvider = provider;
dimensionName = provider.getDimensionName();
}
final String dimensionName = provider.getDimensionName();
displayer.fact(DebugGroup.POSITION, "Dimension").value(dimensionName).text(" (").value(provider.dimensionId).text(')');
}
}
@@ -514,7 +514,6 @@ public enum Mixins implements IMixins {
, "celeritas.terrain.MixinShaderLoader"
, "celeritas.terrain.MixinGlUniformMatrix4f"
, "celeritas.terrain.MixinGlUniformMatrix3f"
, "celeritas.terrain.MixinDefaultChunkRenderer"
)
),
@@ -5,5 +5,5 @@ public interface IPatchedTextureAtlasSprite {
void angelica$markNeedsAnimationUpdate();
boolean angelica$needsAnimationUpdate();
void angelica$unmarkNeedsAnimationUpdate();
void angelica$updateAnimationsDryRun();
boolean angelica$tickAnimation(boolean onlyVisible);
}
@@ -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;
@@ -2,10 +2,15 @@ package net.coderbot.iris.texture.pbr;
import com.gtnewhorizons.angelica.compat.mojang.AutoClosableAbstractTexture;
import com.gtnewhorizons.angelica.config.AngelicaConfig;
import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters;
import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite;
import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import com.gtnewhorizons.angelica.rendering.celeritas.SpriteExtension;
import com.gtnewhorizons.angelica.utils.SpritePadding;
import lombok.Getter;
import net.coderbot.iris.Iris;
import net.coderbot.iris.texture.pbr.loader.AtlasPBRLoader;
import net.coderbot.iris.texture.util.TextureExporter;
import net.coderbot.iris.texture.util.TextureManipulationUtil;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
@@ -20,10 +25,10 @@ import net.minecraft.util.ResourceLocation;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class PBRAtlasTexture extends AutoClosableAbstractTexture {
protected final TextureMap texMap;
@@ -31,7 +36,8 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture {
protected final PBRType type;
protected final ResourceLocation id;
protected final Map<ResourceLocation, TextureAtlasSprite> sprites = new HashMap<>();
protected final Set<TextureAtlasSprite> animatedSprites = new HashSet<>();
protected final List<IPatchedTextureAtlasSprite> animatedSprites = new ArrayList<>();
protected final List<TextureAtlasSprite> alwaysAnimatedSprites = new ArrayList<>();
public PBRAtlasTexture(TextureMap textureMap, PBRType type) {
this.texMap = textureMap;
@@ -44,10 +50,15 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture {
return id;
}
public void addSprite(TextureAtlasSprite sprite) {
public void addSprite(AtlasPBRLoader.PBRTextureAtlasSprite sprite) {
sprites.put(texMap.completeResourceLocation(new ResourceLocation(sprite.getIconName()), 0), sprite);
if (sprite.hasAnimationMetadata()) {
animatedSprites.add(sprite);
if (((ISpriteExt) sprite).isAnimation()) {
// Without the animation mixins, or for a base outside listAnimatedSprites, nothing consumes base visibility
if (sprite instanceof IPatchedTextureAtlasSprite patched && sprite.getBaseSprite().hasAnimationMetadata()) {
animatedSprites.add(patched);
} else {
alwaysAnimatedSprites.add(sprite);
}
}
}
@@ -59,6 +70,7 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture {
public void clear() {
sprites.clear();
animatedSprites.clear();
alwaysAnimatedSprites.clear();
}
public void upload(int atlasWidth, int atlasHeight, int mipLevel, float anisotropicFiltering) {
@@ -78,7 +90,7 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture {
}
}
if (!animatedSprites.isEmpty()) {
if (!animatedSprites.isEmpty() || !alwaysAnimatedSprites.isEmpty()) {
final PBRAtlasHolder pbrHolder = ((TextureAtlasExtension) texMap).getOrCreatePBRHolder();
switch (type) {
case NORMAL:
@@ -128,10 +140,17 @@ public class PBRAtlasTexture extends AutoClosableAbstractTexture {
}
public void cycleAnimationFrames() {
final boolean onlyVisible = ClientProxy.options().performance.animateOnlyVisibleTextures;
bind();
for (TextureAtlasSprite sprite : animatedSprites) {
sprite.updateAnimation();
int updated = alwaysAnimatedSprites.size();
for (int i = 0; i < animatedSprites.size(); i++) {
if (animatedSprites.get(i).angelica$tickAnimation(onlyVisible)) updated++;
}
for (int i = 0; i < alwaysAnimatedSprites.size(); i++) {
alwaysAnimatedSprites.get(i).updateAnimation();
}
DebugCounters.PBR_ANIM_UPDATES.add(updated);
DebugCounters.PBR_ANIM_DRY_RUNS.add(animatedSprites.size() + alwaysAnimatedSprites.size() - updated);
}
@Override
@@ -58,8 +58,8 @@ public class AtlasPBRLoader implements PBRTextureLoader<TextureMap> {
PBRAtlasTexture specularAtlas = null;
for (TextureAtlasSprite sprite : (Collection<TextureAtlasSprite>) texMap.mapUploadedSprites.values()) {
if (!(sprite.getIconName().equals("missingno"))) {
TextureAtlasSprite normalSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.NORMAL);
TextureAtlasSprite specularSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.SPECULAR);
PBRTextureAtlasSprite normalSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.NORMAL);
PBRTextureAtlasSprite specularSprite = createPBRSprite(sprite, resourceManager, texMap, atlasWidth, atlasHeight, mipLevel, PBRType.SPECULAR);
if (normalSprite != null) {
if (normalAtlas == null) {
normalAtlas = new PBRAtlasTexture(texMap, PBRType.NORMAL);
@@ -115,7 +115,7 @@ public class AtlasPBRLoader implements PBRTextureLoader<TextureMap> {
}
@Nullable
protected TextureAtlasSprite createPBRSprite(TextureAtlasSprite sprite, IResourceManager resourceManager, TextureMap texMap, int atlasWidth, int atlasHeight, int mipLevel, PBRType pbrType) {
protected PBRTextureAtlasSprite createPBRSprite(TextureAtlasSprite sprite, IResourceManager resourceManager, TextureMap texMap, int atlasWidth, int atlasHeight, int mipLevel, PBRType pbrType) {
final ResourceLocation spriteName = new ResourceLocation(sprite.getIconName());
final ResourceLocation imageLocation = texMap.completeResourceLocation(spriteName, 0);
final ResourceLocation pbrImageLocation = pbrType.appendToFileLocation(imageLocation);
@@ -124,7 +124,7 @@ public class AtlasPBRLoader implements PBRTextureLoader<TextureMap> {
return null;
}
TextureAtlasSprite pbrSprite = null;
PBRTextureAtlasSprite pbrSprite = null;
try {
// This is no longer closable. Not sure about this.
@@ -174,7 +174,7 @@ public class AtlasPBRLoader implements PBRTextureLoader<TextureMap> {
final int gutter = ((SpriteExtension) sprite).angelica$getGutterWidth();
pbrSprite = new PBRTextureAtlasSprite(pbrSpriteInfo, animationMetadata, atlasWidth, atlasHeight,
sprite.getOriginX(), sprite.getOriginY(), gutter, nativeImage, false, mipLevel);
sprite.getOriginX(), sprite.getOriginY(), gutter, nativeImage, false, mipLevel, sprite);
syncAnimation(sprite, pbrSprite);
} catch (FileNotFoundException e) {
//
@@ -234,14 +234,21 @@ public class AtlasPBRLoader implements PBRTextureLoader<TextureMap> {
}
public static class PBRTextureAtlasSprite extends TextureAtlasSprite implements CustomMipmapGenerator.Provider {
protected final TextureAtlasSprite baseSprite;
// This feels super janky
protected PBRTextureAtlasSprite(TextureAtlasSpriteInfo info, AnimationMetadataSection animationMetaDataSection, int atlasWidth, int atlasHeight, int x, int y, int gutter, NativeImage nativeImage, boolean useAnisotropicFiltering, int miplevel) {
protected PBRTextureAtlasSprite(TextureAtlasSpriteInfo info, AnimationMetadataSection animationMetaDataSection, int atlasWidth, int atlasHeight, int x, int y, int gutter, NativeImage nativeImage, boolean useAnisotropicFiltering, int miplevel, TextureAtlasSprite baseSprite) {
super(info.name().toString());
this.baseSprite = baseSprite;
((SpriteExtension) this).angelica$setGutterWidth(gutter);
super.loadSprite(Objects.requireNonNull(getMipmapGenerator(info, atlasWidth, atlasHeight)).generateMipLevels(nativeImage, miplevel), animationMetaDataSection, useAnisotropicFiltering);
super.initSprite(atlasWidth, atlasHeight, x - gutter, y - gutter, false);
}
public TextureAtlasSprite getBaseSprite() {
return baseSprite;
}
@Override
public CustomMipmapGenerator getMipmapGenerator(TextureAtlasSpriteInfo info, int atlasWidth, int atlasHeight) {
if (info instanceof PBRTextureAtlasSpriteInfo pbrInfo) {
@@ -2,6 +2,8 @@ package com.gtnewhorizons.angelica.mixins.early.angelica.animation;
import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite;
import com.gtnewhorizons.angelica.utils.AnimationsRenderUtils;
import net.coderbot.iris.texture.pbr.PBRSpriteHolder;
import net.coderbot.iris.texture.pbr.TextureAtlasSpriteExtension;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.data.AnimationMetadataSection;
import org.spongepowered.asm.mixin.Mixin;
@@ -18,6 +20,7 @@ public abstract class MixinTextureAtlasSprite implements IPatchedTextureAtlasSpr
@Shadow protected int frameCounter;
@Shadow private AnimationMetadataSection animationMetadata;
@Shadow protected List<?> framesTextureData;
@Shadow public abstract void updateAnimation();
@Unique private boolean angelica$isActive = false;
@@ -31,6 +34,16 @@ public abstract class MixinTextureAtlasSprite implements IPatchedTextureAtlasSpr
public boolean angelica$needsAnimationUpdate() {
if (this.angelica$isActive) {
this.angelica$isActive = false;
// PBR sprites are never UV-sampled; they inherit visibility when the base sprite's mark is consumed
if (this instanceof TextureAtlasSpriteExtension extension) {
final PBRSpriteHolder pbrHolder = extension.getPBRHolder();
if (pbrHolder != null) {
final TextureAtlasSprite normalSprite = pbrHolder.getNormalSprite();
final TextureAtlasSprite specularSprite = pbrHolder.getSpecularSprite();
if (normalSprite != null) ((IPatchedTextureAtlasSprite) normalSprite).angelica$markNeedsAnimationUpdate();
if (specularSprite != null) ((IPatchedTextureAtlasSprite) specularSprite).angelica$markNeedsAnimationUpdate();
}
}
return true;
}
return false;
@@ -42,7 +55,17 @@ public abstract class MixinTextureAtlasSprite implements IPatchedTextureAtlasSpr
}
@Override
public void angelica$updateAnimationsDryRun() {
public boolean angelica$tickAnimation(boolean onlyVisible) {
if (!onlyVisible || angelica$needsAnimationUpdate()) {
updateAnimation();
return true;
}
angelica$advanceFramesDryRun();
return false;
}
@Unique
private void angelica$advanceFramesDryRun() {
if (animationMetadata == null || framesTextureData == null) return;
tickCounter++;
@@ -36,16 +36,7 @@ public abstract class MixinTextureMap extends AbstractTexture {
final int size = listAnimatedSprites.size();
for (int i = 0; i < size; i++) {
final TextureAtlasSprite sprite = listAnimatedSprites.get(i);
final IPatchedTextureAtlasSprite patched = (IPatchedTextureAtlasSprite) sprite;
// needsAnimationUpdate() is one-shot: returns true if marked, then auto-resets
if (!renderVisible || patched.angelica$needsAnimationUpdate()) {
sprite.updateAnimation();
} else {
// Keep frame counters in sync for sprites not being updated
patched.angelica$updateAnimationsDryRun();
}
((IPatchedTextureAtlasSprite) listAnimatedSprites.get(i)).angelica$tickAnimation(renderVisible);
}
Minecraft.getMinecraft().mcProfiler.endSection();
}
@@ -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;
}
}
@@ -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));
}
}
}
@@ -0,0 +1,89 @@
package net.coderbot.iris.texture.pbr;
import com.gtnewhorizons.angelica.glsm.GLCoreTest;
import com.gtnewhorizons.angelica.glsm.GLSMTextures;
import com.gtnewhorizons.angelica.glsm.profiling.DebugCounters;
import com.gtnewhorizons.angelica.glsm.testutil.Reflect;
import com.gtnewhorizons.angelica.mixins.interfaces.IPatchedTextureAtlasSprite;
import com.gtnewhorizons.angelica.mixins.interfaces.ISpriteExt;
import com.gtnewhorizons.angelica.proxy.ClientProxy;
import me.jellysquid.mods.sodium.client.gui.SodiumGameOptions;
import net.coderbot.iris.texture.pbr.loader.AtlasPBRLoader;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
@GLCoreTest
class PBRAtlasAnimationTest {
static Stream<Arguments> cases() {
return Stream.of(
Arguments.of("visible tracked sprite updates", true, true, true, true, true, 1, 0, 1, 0),
Arguments.of("hidden tracked sprite dry-runs", true, true, true, true, false, 1, 0, 0, 1),
Arguments.of("only-visible off reaches the gate", true, true, true, false, true, 1, 0, 1, 0),
Arguments.of("base without animation metadata is always animated", true, false, true, true, false, 0, 1, 1, 0),
Arguments.of("sprite without the animation mixin is always animated", false, true, true, true, false, 0, 1, 1, 0),
Arguments.of("non-animated pbr sprite is never touched", true, true, false, true, false, 0, 0, 0, 0)
);
}
@ParameterizedTest(name = "{0}")
@MethodSource("cases")
void cycleRoutesAnimationAndCountsUpdates(String name, boolean patchedSprite, boolean baseAnimated, boolean pbrAnimated,
boolean onlyVisible, boolean tickReturns, int expectedTicks, int expectedUpdateCalls,
long expectedUpdates, long expectedDryRuns) {
final SodiumGameOptions previousOptions = Reflect.getStatic(ClientProxy.class, "CONFIG");
final SodiumGameOptions options = new SodiumGameOptions();
options.performance.animateOnlyVisibleTextures = onlyVisible;
final TextureMap textureMap = mock(TextureMap.class, withSettings().extraInterfaces(TextureAtlasExtension.class));
when(textureMap.completeResourceLocation(any(), anyInt())).thenAnswer(invocation -> invocation.getArgument(0));
final TextureAtlasSprite base = mock(TextureAtlasSprite.class);
when(base.hasAnimationMetadata()).thenReturn(baseAnimated);
final AtlasPBRLoader.PBRTextureAtlasSprite sprite = mock(AtlasPBRLoader.PBRTextureAtlasSprite.class, patchedSprite
? withSettings().extraInterfaces(IPatchedTextureAtlasSprite.class, ISpriteExt.class)
: withSettings().extraInterfaces(ISpriteExt.class));
when(sprite.getIconName()).thenReturn("minecraft:stone_n");
when(sprite.getBaseSprite()).thenReturn(base);
when(((ISpriteExt) sprite).isAnimation()).thenReturn(pbrAnimated);
if (patchedSprite) {
when(((IPatchedTextureAtlasSprite) sprite).angelica$tickAnimation(anyBoolean())).thenReturn(tickReturns);
}
Reflect.setStatic(ClientProxy.class, "CONFIG", options);
final PBRAtlasTexture atlas = new PBRAtlasTexture(textureMap, PBRType.NORMAL);
final int texture = GLSMTextures.glGenTextures();
Reflect.set(atlas, "glTextureId", texture);
try {
atlas.addSprite(sprite);
final long updatesBefore = DebugCounters.PBR_ANIM_UPDATES.sum();
final long dryRunsBefore = DebugCounters.PBR_ANIM_DRY_RUNS.sum();
atlas.cycleAnimationFrames();
if (patchedSprite) {
verify((IPatchedTextureAtlasSprite) sprite, times(expectedTicks)).angelica$tickAnimation(anyBoolean());
verify((IPatchedTextureAtlasSprite) sprite, times(expectedTicks)).angelica$tickAnimation(onlyVisible);
}
verify(sprite, times(expectedUpdateCalls)).updateAnimation();
assertEquals(expectedUpdates, DebugCounters.PBR_ANIM_UPDATES.sum() - updatesBefore);
assertEquals(expectedDryRuns, DebugCounters.PBR_ANIM_DRY_RUNS.sum() - dryRunsBefore);
} finally {
GLSMTextures.glDeleteTextures(texture);
Reflect.setStatic(ClientProxy.class, "CONFIG", previousOptions);
}
}
}
@@ -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) {