Compare commits

..
3 Commits
35 changed files with 874 additions and 193 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;
@@ -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;
@@ -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"
)
),
@@ -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;
@@ -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));
}
}
}
@@ -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) {