Skip to content

Commit bcd0c67

Browse files
committed
perf(glsm): upload only the vertex range a client-array draw actually reads
Root cause of the F3/GUI FPS collapse (140+ -> 15 fps with the debug overlay open, sluggish inventory/video-settings screens): 2f54db7 widened captured client pointers to the whole underlying allocation to stay correct when HBM-CE mutates the BufferBuilder limit after setting a pointer. uploadClientArraysToVBO then re-uploaded that entire allocation (>4MB observed, ~1.1ms per upload on PCIe) on EVERY draw issued while a mod leaves client-state attribs enabled. Text/rect-heavy GUI frames issue hundreds of draws, multiplying this into ~57ms/frame. glDrawArrays (and the QUADS path through QuadConverter, whose shared EBO references exactly [first, first+count)) knows the drawn vertex range, so per-attrib uploads are now clamped to the bytes that range can read via VertexAttribState.computeUploadLength; indexed draws with an unknown max index keep the full-allocation upload. Escape hatch: -Dactinium.glsmFullClientArrayUpload=true. Also adds clientArray.stackSample caller attribution to the periodic GLSM perf report to identify which mod drives large uploads.
1 parent f499c63 commit bcd0c67

4 files changed

Lines changed: 165 additions & 8 deletions

File tree

glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ public class GLStateManager {
126126

127127
public static final Logger LOGGER = LogManager.getLogger("GLSM");
128128
private static final boolean DEBUG_DRAW_LOGS = Boolean.getBoolean("actinium.glsm.verboseDrawLogs");
129+
/** Escape hatch: -Dactinium.glsmFullClientArrayUpload=true restores whole-allocation uploads per draw. */
130+
private static final boolean FULL_CLIENT_ARRAY_UPLOAD = Boolean.getBoolean("actinium.glsmFullClientArrayUpload");
129131

130132
// Thread Checking - must be early in static init order so isMainThread() works for state initialization
131133
@Getter private static final Thread MainThread = Thread.currentThread();
@@ -434,6 +436,7 @@ public static void registerModifiedState(IStateStack<?> stack) {
434436
private static int clientArraysVBO = 0;
435437
private static int clientArraysVBOCapacity = 0;
436438
private static final int[] clientArraysVBOOffsets = new int[VertexAttribState.MAX_ATTRIBS];
439+
private static final int[] clientArraysVBOUploadLengths = new int[VertexAttribState.MAX_ATTRIBS];
437440
private static int boundPixelUnpackBuffer;
438441
private static int boundPixelPackBuffer;
439442
private static final Int2IntOpenHashMap vaoEboMap = new Int2IntOpenHashMap();
@@ -2517,7 +2520,7 @@ public static void glDrawElementsInstanced(int mode, int count, int type, long i
25172520
public static void glDrawArraysInstanced(int mode, int first, int count, int primcount) {
25182521
prepareWideLineEmulation(mode);
25192522
preDrawFFP();
2520-
prepareClientArrays();
2523+
prepareClientArrays(first, count);
25212524
recordGpuCommand(GpuCommandType.DRAW_ARRAYS, mode, count);
25222525
if (mode == GL11.GL_QUADS) {
25232526
QuadConverter.drawQuadsAsTrianglesInstanced(first, count, primcount);
@@ -2548,30 +2551,42 @@ private static void preDrawFFP() {
25482551
}
25492552

25502553
private static void prepareClientArrays() {
2554+
prepareClientArrays(0, -1);
2555+
}
2556+
2557+
private static void prepareClientArrays(int first, int count) {
25512558
final boolean perfDebugEnabled = GLSMPerfDebug.isEnabled();
25522559
final long perfStart = perfDebugEnabled ? GLSMPerfDebug.begin(GLSMPerfDebug.Stage.GL_PREPARE_CLIENT_ARRAYS) : 0L;
25532560
if (ShaderManager.getInstance().isEnabled() && VertexAttribState.hasAnyClientSideEnabledAttrib()) {
2554-
uploadClientArraysToVBO();
2561+
uploadClientArraysToVBO(first, count);
25552562
}
25562563
if (perfDebugEnabled) {
25572564
GLSMPerfDebug.end(GLSMPerfDebug.Stage.GL_PREPARE_CLIENT_ARRAYS, perfStart);
25582565
}
25592566
}
25602567

25612568
/**
2562-
* If any enabled vertex attribute uses a client-side pointer (no VBO), upload all such
2563-
* attribs into a shared stream VBO so the draw succeeds under core profile.
2569+
* If any enabled vertex attribute uses a client-side pointer (no VBO), upload such attribs
2570+
* into a shared stream VBO so the draw succeeds under core profile. When the draw's vertex
2571+
* range [first, first + count) is known (count >= 0), each attrib is narrowed to the bytes
2572+
* that range can actually read; the captured client pointer spans the whole underlying
2573+
* allocation, which can be megabytes larger than one draw needs. Indexed draws with an
2574+
* unknown maximum index pass count = -1 and keep the full-allocation upload.
25642575
*/
2565-
private static void uploadClientArraysToVBO() {
2576+
private static void uploadClientArraysToVBO(int first, int count) {
25662577
final boolean perfDebugEnabled = GLSMPerfDebug.isEnabled();
25672578
final long perfStart = perfDebugEnabled ? GLSMPerfDebug.begin(GLSMPerfDebug.Stage.GL_CLIENT_ARRAY_UPLOAD) : 0L;
2579+
final boolean fullUpload = FULL_CLIENT_ARRAY_UPLOAD || count < 0;
25682580
int totalBytes = 0;
25692581
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
25702582
clientArraysVBOOffsets[i] = -1;
25712583
final VertexAttribState.Attrib a = VertexAttribState.get(i);
25722584
if (!a.enabled || a.clientPointer == null) continue;
2585+
final int uploadLength = fullUpload ? a.clientPointer.remaining()
2586+
: VertexAttribState.computeUploadLength(a, first, count);
2587+
clientArraysVBOUploadLengths[i] = uploadLength;
25732588
clientArraysVBOOffsets[i] = totalBytes;
2574-
totalBytes += a.clientPointer.remaining();
2589+
totalBytes += uploadLength;
25752590
}
25762591
if (totalBytes == 0) {
25772592
if (perfDebugEnabled) {
@@ -2600,13 +2615,16 @@ private static void uploadClientArraysToVBO() {
26002615
for (int i = 0; i < VertexAttribState.MAX_ATTRIBS; i++) {
26012616
if (clientArraysVBOOffsets[i] < 0) continue;
26022617
final VertexAttribState.Attrib a = VertexAttribState.get(i);
2603-
RENDER_BACKEND.bufferSubData(GL15.GL_ARRAY_BUFFER, clientArraysVBOOffsets[i], a.clientPointer.duplicate());
2618+
final ByteBuffer slice = a.clientPointer.duplicate();
2619+
slice.limit(slice.position() + clientArraysVBOUploadLengths[i]);
2620+
RENDER_BACKEND.bufferSubData(GL15.GL_ARRAY_BUFFER, clientArraysVBOOffsets[i], slice);
26042621
RENDER_BACKEND.vertexAttribPointer(i, a.size, a.type, a.normalized, a.stride, (long) clientArraysVBOOffsets[i]);
26052622
}
26062623

26072624
glBindBuffer(GL15.GL_ARRAY_BUFFER, savedVBO);
26082625
if (perfDebugEnabled) {
26092626
GLSMPerfDebug.end(GLSMPerfDebug.Stage.GL_CLIENT_ARRAY_UPLOAD, perfStart);
2627+
GLSMPerfDebug.countClientArrayUpload(totalBytes);
26102628
}
26112629
}
26122630

@@ -2628,7 +2646,7 @@ public static void glDrawArrays(int mode, int first, int count) {
26282646
}
26292647
prepareWideLineEmulation(mode);
26302648
preDrawFFP();
2631-
prepareClientArrays();
2649+
prepareClientArrays(first, count);
26322650
if (DEBUG_DRAW_LOGS) {
26332651
GLSMDebug.logDrawArrays("draw-arrays", mode, first, count);
26342652
}

glsm/src/main/java/com/gtnewhorizons/angelica/glsm/debug/GLSMPerfDebug.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public final class GLSMPerfDebug {
1212
private static final long REPORT_INTERVAL_NS = 1_000_000_000L;
1313
private static final int SAMPLE_MASK = 255;
1414
private static final int MAX_BUFFERBUILDER_SOURCE_LINES = 12;
15+
private static final int MAX_CLIENT_ARRAY_STACK_LINES = 12;
1516
private static final String ENABLED_OVERRIDE = System.getProperty("actinium.glsmPerfDebug");
1617
private static volatile boolean enabled = resolveEnabled(ENABLED_OVERRIDE, false);
1718

@@ -71,6 +72,7 @@ public enum Source {
7172
private static final int[] sourceCounts = new int[Source.values().length];
7273
private static final Map<String, Integer> bufferBuilderSourceCounts = new HashMap<>();
7374
private static final Map<String, Integer> bufferBuilderStackSamples = new HashMap<>();
75+
private static final Map<String, Integer> clientArrayStackSamples = new HashMap<>();
7476
private static long fenceReclaimedBytes;
7577
private static int fenceReclaimedRegions;
7678
private static int fenceQueuePeak;
@@ -110,6 +112,7 @@ private static void resetStats() {
110112
Arrays.fill(sourceCounts, 0);
111113
bufferBuilderSourceCounts.clear();
112114
bufferBuilderStackSamples.clear();
115+
clientArrayStackSamples.clear();
113116
fenceReclaimedBytes = 0L;
114117
fenceReclaimedRegions = 0;
115118
fenceQueuePeak = 0;
@@ -150,6 +153,20 @@ public static void countBufferBuilder(String source, int drawMode, int vertexCou
150153
}
151154
}
152155

156+
/**
157+
* Attributes a client-array VBO upload to its draw-call origin. Sampled at the same cadence
158+
* as the stage timer; the key carries the first non-glsm caller and a power-of-two bucket of
159+
* the uploaded byte total, so the periodic report shows who drives large uploads.
160+
*/
161+
public static void countClientArrayUpload(int totalBytes) {
162+
if (!isEnabled()) return;
163+
final int observed = observedCounts[Stage.GL_CLIENT_ARRAY_UPLOAD.ordinal()];
164+
if ((observed & SAMPLE_MASK) == 0) {
165+
final String stackKey = findClientArrayCaller() + "/bytes~" + bucketVertexCount(totalBytes);
166+
clientArrayStackSamples.put(stackKey, clientArrayStackSamples.getOrDefault(stackKey, 0) + 1);
167+
}
168+
}
169+
153170
public static void recordFenceReclaim(int bytes) {
154171
fenceReclaimedRegions++;
155172
fenceReclaimedBytes += bytes;
@@ -228,6 +245,7 @@ private static void report(long now) {
228245
}
229246
appendTopEntries(sb, "bufferbuilder.source", bufferBuilderSourceCounts, MAX_BUFFERBUILDER_SOURCE_LINES);
230247
appendTopEntries(sb, "bufferbuilder.stackSample", bufferBuilderStackSamples, MAX_BUFFERBUILDER_SOURCE_LINES);
248+
appendTopEntries(sb, "clientArray.stackSample", clientArrayStackSamples, MAX_CLIENT_ARRAY_STACK_LINES);
231249
if (fenceReclaimedRegions != 0 || fenceQueuePeak != 0) {
232250
sb.append(" stream.fence[")
233251
.append("reclaimedRegions=").append(fenceReclaimedRegions)
@@ -298,6 +316,28 @@ private static boolean isBufferBuilderBridge(String className, String methodName
298316
&& (methodName.equals("draw") || methodName.startsWith("handler$"));
299317
}
300318

319+
/**
320+
* Like {@link #findBufferBuilderCaller()} but additionally skips all glsm frames: the
321+
* client-array upload is triggered from GLStateManager draw entry points, so the first
322+
* interesting frame is the Minecraft/mod code that issued the draw.
323+
*/
324+
private static String findClientArrayCaller() {
325+
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
326+
for (StackTraceElement element : stack) {
327+
String className = element.getClassName();
328+
if (className.startsWith("com.gtnewhorizons.angelica.glsm.")
329+
|| className.startsWith("com.dhj.actinium.render.")
330+
|| className.startsWith("com.dhj.actinium.mixin.vintage.core.")
331+
|| className.startsWith("org.taumc.celeritas.impl.render.")
332+
|| className.startsWith("org.taumc.celeritas.mixin.core.")
333+
|| className.equals("java.lang.Thread")) {
334+
continue;
335+
}
336+
return shortenClassName(className) + "#" + element.getMethodName();
337+
}
338+
return "unknown";
339+
}
340+
301341
private static String shortenClassName(String className) {
302342
int index = className.lastIndexOf('.');
303343
return index >= 0 ? className.substring(index + 1) : className;

glsm/src/main/java/com/gtnewhorizons/angelica/glsm/states/VertexAttribState.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@ public static Attrib get(int index) {
101101
return current[index];
102102
}
103103

104+
/**
105+
* Computes how many leading bytes of {@code a.clientPointer} a draw over vertices
106+
* [first, first + count) can actually read. The captured pointer deliberately spans the
107+
* whole underlying allocation (mods like HBM-CE mutate the Java limit after setting the
108+
* pointer), so the Java limit is never consulted; only the draw's first/count may narrow
109+
* the range. The last read byte of the final vertex is (first + count - 1) * stride +
110+
* vertexSize - 1, which stays correct even for stride smaller than the vertex size.
111+
* Negative strides read backwards from the pointer, so they fall back to the full
112+
* captured range. The result is always clamped to the captured allocation.
113+
*/
114+
public static int computeUploadLength(Attrib a, int first, int count) {
115+
final int remaining = a.clientPointer.remaining();
116+
if (count <= 0) return 0;
117+
if (a.stride < 0) return remaining;
118+
final int stride = a.stride > 0 ? a.stride : a.size * a.typeSizeBytes();
119+
final long lastByteExclusive = (long) (first + count - 1) * stride + (long) a.size * a.typeSizeBytes();
120+
return (int) Math.min(remaining, lastByteExclusive);
121+
}
122+
104123
/**
105124
* Captures the native pointer address without treating the Java buffer limit as its GL
106125
* allocation boundary. HBM reuses a BufferBuilder allocation and changes its limit between
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.gtnewhorizons.angelica.glsm.states;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.lwjgl.opengl.GL11;
5+
6+
import java.nio.ByteBuffer;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
class VertexAttribUploadLengthTest {
11+
private static VertexAttribState.Attrib attrib(int size, int type, int stride, int bufferBytes) {
12+
final VertexAttribState.Attrib a = new VertexAttribState.Attrib();
13+
a.size = size;
14+
a.type = type;
15+
a.stride = stride;
16+
a.clientPointer = ByteBuffer.allocate(bufferBytes);
17+
return a;
18+
}
19+
20+
@Test
21+
void tightlyPackedFloatPosition() {
22+
// stride=0 means tightly packed: 3 floats = 12 bytes per vertex; [0, 100) reads 1200 bytes.
23+
assertEquals(1200, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, 0, 4096), 0, 100));
24+
}
25+
26+
@Test
27+
void explicitStrideCoversWholeVertices() {
28+
// stride=32 with a 12-byte vertex: last vertex of [0, 100) ends at 99*32+12 = 3180.
29+
assertEquals(3180, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, 32, 8192), 0, 100));
30+
}
31+
32+
@Test
33+
void nonZeroFirstStillUploadsFromAllocationStart() {
34+
// QUADS-as-triangles style range [16, 20): bytes up to (16+4-1)*12+12 = 240 are needed.
35+
assertEquals(240, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, 0, 4096), 16, 4));
36+
}
37+
38+
@Test
39+
void hugeAllocationShrinksToDrawRange() {
40+
// The HBM-CE case: a 4.2MB reused allocation, a 4-vertex GUI draw reads only 48 bytes.
41+
assertEquals(48, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, 0, 4_200_768), 0, 4));
42+
}
43+
44+
@Test
45+
void clampedToCapturedAllocation() {
46+
assertEquals(1024, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, 0, 1024), 0, 10000));
47+
}
48+
49+
@Test
50+
void respectsBufferPosition() {
51+
final VertexAttribState.Attrib a = attrib(3, GL11.GL_FLOAT, 0, 4096);
52+
a.clientPointer.position(1024);
53+
assertEquals(1200, VertexAttribState.computeUploadLength(a, 0, 100));
54+
assertEquals(3072, VertexAttribState.computeUploadLength(a, 0, 1000));
55+
}
56+
57+
@Test
58+
void zeroCountUploadsNothing() {
59+
assertEquals(0, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, 0, 4096), 0, 0));
60+
}
61+
62+
@Test
63+
void byteAndShortTypes() {
64+
// RGBA color as 4 unsigned bytes: 4 bytes per vertex.
65+
assertEquals(400, VertexAttribState.computeUploadLength(attrib(4, GL11.GL_UNSIGNED_BYTE, 0, 4096), 0, 100));
66+
// 2 shorts = 4-byte vertex, stride=8, range [2, 12): (2+10-1)*8+4 = 92.
67+
assertEquals(92, VertexAttribState.computeUploadLength(attrib(2, GL11.GL_SHORT, 8, 4096), 2, 10));
68+
}
69+
70+
@Test
71+
void narrowStrideKeepsLastVertexCovered() {
72+
// stride=8 < 12-byte vertex: the last vertex's tail must still be uploaded.
73+
assertEquals(11 * 8 + 12, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, 8, 4096), 0, 12));
74+
}
75+
76+
@Test
77+
void negativeStrideFallsBackToFullAllocation() {
78+
assertEquals(4096, VertexAttribState.computeUploadLength(attrib(3, GL11.GL_FLOAT, -1, 4096), 0, 4));
79+
}
80+
}

0 commit comments

Comments
 (0)