mirror of
https://github.com/GTNewHorizons/Angelica.git
synced 2026-11-15 09:44:52 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b46b4f602 | ||
|
|
8e099e9529 | ||
|
|
bbbdc12b2a | ||
|
|
08aa37712f |
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() {
|
||||
|
||||
+8
-2
@@ -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
|
||||
|
||||
+192
-1
@@ -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");
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
#
|
||||
|
||||
+5
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -514,7 +514,6 @@ public enum Mixins implements IMixins {
|
||||
, "celeritas.terrain.MixinShaderLoader"
|
||||
, "celeritas.terrain.MixinGlUniformMatrix4f"
|
||||
, "celeritas.terrain.MixinGlUniformMatrix3f"
|
||||
, "celeritas.terrain.MixinDefaultChunkRenderer"
|
||||
)
|
||||
),
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+6
-1
@@ -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;
|
||||
|
||||
+4
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
+20
-2
@@ -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) {
|
||||
|
||||
+24
-1
@@ -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++;
|
||||
|
||||
+1
-10
@@ -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();
|
||||
}
|
||||
|
||||
-20
@@ -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;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-4
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user