Skip to content

Commit bf404f0

Browse files
committed
Add adaptive traversal and upload-pressure backpressure controls
1 parent 9f77b40 commit bf404f0

5 files changed

Lines changed: 128 additions & 8 deletions

File tree

src/main/java/me/cortex/voxy/client/core/rendering/hierachical/AsyncNodeManager.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ public class AsyncNodeManager {
7171
private final GeometryCache geometryCache = new GeometryCache(1L<<32);
7272

7373
private final AtomicInteger workCounter = new AtomicInteger();
74+
private static final boolean ENABLE_UPLOAD_BACKPRESSURE_DEFERRAL =
75+
System.getProperty("voxy.asyncNodeUploadBackpressureDeferral", "true").equalsIgnoreCase("true");
76+
private static final int UPLOAD_BACKPRESSURE_LOG_COOLDOWN_FRAMES =
77+
Math.max(1, Integer.getInteger("voxy.asyncNodeUploadBackpressureLogCooldownFrames", 120));
7478

7579
@SuppressWarnings("FieldMayBeFinal")
7680
private volatile SyncResults results = null, resultCache1 = new SyncResults(), resultCache2 = new SyncResults();
@@ -82,6 +86,7 @@ public class AsyncNodeManager {
8286
private final IntOpenHashSet cleanerIdResetClear = new IntOpenHashSet();//Tells the cleaner if it needs to clear the id to 0, or reset the id to the current frame
8387

8488
private boolean needsWaitForSync = false;
89+
private int uploadBackpressureLogCooldown;
8590

8691
public AsyncNodeManager(int maxNodeCount, IGeometryData geometryData, RenderGenerationService renderService) {
8792
//Note the current implmentation of ISectionWatcher is threadsafe
@@ -496,6 +501,20 @@ public void tick(GlBuffer nodeBuffer, NodeCleaner cleaner) {//TODO: dont pass no
496501
return;
497502
}
498503

504+
if (ENABLE_UPLOAD_BACKPRESSURE_DEFERRAL) {
505+
long estimatedUploadBytes = this.estimateRenderThreadUploadBytes(results);
506+
if (UploadStream.INSTANCE.shouldDefer(estimatedUploadBytes)) {
507+
if (this.uploadBackpressureLogCooldown-- <= 0) {
508+
this.uploadBackpressureLogCooldown = UPLOAD_BACKPRESSURE_LOG_COOLDOWN_FRAMES;
509+
Logger.info("[AsyncNodeManager] Deferring sync due to upload pressure; estBytes=" + estimatedUploadBytes);
510+
}
511+
if (!RESULT_HANDLE.compareAndSet(this, null, results)) {
512+
throw new IllegalStateException("Failed to requeue sync results under upload pressure");
513+
}
514+
return;
515+
}
516+
}
517+
499518
//top level node add/remove
500519
if (!results.tlnDelta.isEmpty()) {
501520
var iter = results.tlnDelta.intIterator();
@@ -583,6 +602,24 @@ public void tick(GlBuffer nodeBuffer, NodeCleaner cleaner) {//TODO: dont pass no
583602
}
584603
}
585604

605+
private long estimateRenderThreadUploadBytes(SyncResults results) {
606+
long bytes = 0L;
607+
if (!results.geometryUpload.dataUploadPoints.isEmpty()) {
608+
int copies = results.geometryUpload.dataUploadPoints.size();
609+
int scratchSize = (int) results.geometryUpload.arena.getSize() * 8;
610+
bytes += (long) scratchSize + (long) copies * 16L;
611+
}
612+
if (!results.scatterWriteLocationMap.isEmpty()) {
613+
int count = results.scatterWriteLocationMap.size();
614+
int chunks = (count + 3) / 4;
615+
bytes += (long) chunks * 80L + 16L;
616+
}
617+
if (!results.cleanerOperations.isEmpty()) {
618+
bytes += (long) results.cleanerOperations.size() * 4L + 16L;
619+
}
620+
return bytes;
621+
}
622+
586623

587624
public void setTLNAddRemoveCallbacks(IntConsumer add, IntConsumer remove) {
588625
this.tlnAddCallback = add;

src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,20 @@ public class HierarchicalOcclusionTraverser {
8989
Float.parseFloat(System.getProperty("voxy.requestFrustumDirectionalMaxExtra", "64.0"));
9090
private static final int TRAVERSAL_INTERVAL_FRAMES =
9191
Math.max(1, Integer.parseInt(System.getProperty("voxy.traversalIntervalFrames", "1")));
92+
private static final boolean ENABLE_ADAPTIVE_TRAVERSAL_INTERVAL =
93+
System.getProperty("voxy.adaptiveTraversalInterval", "true").equalsIgnoreCase("true");
94+
private static final int TRAVERSAL_MAX_INTERVAL_FRAMES =
95+
Math.max(TRAVERSAL_INTERVAL_FRAMES, Integer.parseInt(System.getProperty("voxy.traversalIntervalMaxFrames", "3")));
96+
private static final double TRAVERSAL_FORCE_MOTION_BLOCKS =
97+
Double.parseDouble(System.getProperty("voxy.traversalForceMotionBlocks", "1.25"));
98+
private static final double TRAVERSAL_FORCE_ROTATION_DEGREES =
99+
Double.parseDouble(System.getProperty("voxy.traversalForceRotationDegrees", "1.0"));
100+
private static final int TRAVERSAL_FORCE_BACKLOG_HIGH =
101+
Math.max(1, Integer.parseInt(System.getProperty("voxy.traversalForceBacklogHigh", "1500")));
102+
private static final int TRAVERSAL_BACKLOG_LOW =
103+
Math.max(0, Integer.parseInt(System.getProperty("voxy.traversalBacklogLow", "300")));
104+
private static final int HIZ_DISABLE_FROM_LOD =
105+
Math.max(0, Integer.parseInt(System.getProperty("voxy.hizDisableFromLod", String.valueOf(MAX_ITERATIONS + 1))));
92106
private double lastCamX = Double.NaN, lastCamY = Double.NaN, lastCamZ = Double.NaN;
93107
private float lastNearPlaneX = Float.NaN, lastNearPlaneY = Float.NaN, lastNearPlaneZ = Float.NaN;
94108
private double lastRequestBudget = Double.NaN;
@@ -115,6 +129,7 @@ public class HierarchicalOcclusionTraverser {
115129
.define("MAX_ITERATIONS", MAX_ITERATIONS)
116130
.define("LOCAL_SIZE_BITS", LOCAL_WORK_SIZE_BITS)
117131
.define("MAX_REQUEST_QUEUE_SIZE", MAX_REQUEST_QUEUE_SIZE)
132+
.define("HIZ_DISABLE_FROM_LOD", HIZ_DISABLE_FROM_LOD)
118133

119134
.define("HIZ_BINDING", 0)
120135

@@ -362,8 +377,9 @@ private void bindings(Viewport<?> viewport) {
362377
}
363378

364379
public void doTraversal(Viewport<?> viewport) {
380+
int traversalInterval = this.computeTraversalInterval(viewport);
365381
this.traversalFrameCounter++;
366-
if (TRAVERSAL_INTERVAL_FRAMES > 1 && (this.traversalFrameCounter % TRAVERSAL_INTERVAL_FRAMES) != 0) {
382+
if (traversalInterval > 1 && (this.traversalFrameCounter % traversalInterval) != 0) {
367383
return;
368384
}
369385
this.uploadUniform(viewport);
@@ -402,6 +418,48 @@ public void doTraversal(Viewport<?> viewport) {
402418
glBindTextureUnit(0, 0);
403419
}
404420

421+
private int computeTraversalInterval(Viewport<?> viewport) {
422+
if (!ENABLE_ADAPTIVE_TRAVERSAL_INTERVAL) {
423+
return TRAVERSAL_INTERVAL_FRAMES;
424+
}
425+
int backlog = this.meshGen.getTaskCount();
426+
if (backlog >= TRAVERSAL_FORCE_BACKLOG_HIGH) {
427+
return 1;
428+
}
429+
if (!Double.isNaN(this.lastCamX)) {
430+
double dx = viewport.cameraX - this.lastCamX;
431+
double dy = viewport.cameraY - this.lastCamY;
432+
double dz = viewport.cameraZ - this.lastCamZ;
433+
double motion = Math.sqrt(dx * dx + dy * dy + dz * dz);
434+
if (motion >= TRAVERSAL_FORCE_MOTION_BLOCKS) {
435+
return 1;
436+
}
437+
if (!Float.isNaN(this.lastNearPlaneX)) {
438+
float nearPlaneX = viewport.frustumPlanes[4].x;
439+
float nearPlaneY = viewport.frustumPlanes[4].y;
440+
float nearPlaneZ = viewport.frustumPlanes[4].z;
441+
double dot = nearPlaneX * this.lastNearPlaneX
442+
+ nearPlaneY * this.lastNearPlaneY
443+
+ nearPlaneZ * this.lastNearPlaneZ;
444+
dot = Math.max(-1.0, Math.min(1.0, dot));
445+
double rotationDegrees = Math.toDegrees(Math.acos(dot));
446+
if (rotationDegrees >= TRAVERSAL_FORCE_ROTATION_DEGREES) {
447+
return 1;
448+
}
449+
}
450+
}
451+
452+
if (backlog <= TRAVERSAL_BACKLOG_LOW) {
453+
return TRAVERSAL_MAX_INTERVAL_FRAMES;
454+
}
455+
int span = Math.max(1, TRAVERSAL_FORCE_BACKLOG_HIGH - TRAVERSAL_BACKLOG_LOW);
456+
double normalized = 1.0 - ((double) (backlog - TRAVERSAL_BACKLOG_LOW) / span);
457+
normalized = Math.max(0.0, Math.min(1.0, normalized));
458+
int adaptive = TRAVERSAL_INTERVAL_FRAMES
459+
+ (int) Math.round(normalized * (TRAVERSAL_MAX_INTERVAL_FRAMES - TRAVERSAL_INTERVAL_FRAMES));
460+
return Math.max(1, Math.min(TRAVERSAL_MAX_INTERVAL_FRAMES, adaptive));
461+
}
462+
405463
private void traverseInternal() {
406464
{
407465
//Fix mesa bug

src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeCleaner.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public class NodeCleaner {
7979
Integer.parseInt(System.getProperty("voxy.nodeCleanerInterval", "1"));
8080
private static final long CLEAN_REMAINING_GEOMETRY_THRESHOLD_BYTES =
8181
Long.parseLong(System.getProperty("voxy.nodeCleanerMinRemainingBytes", "100000000"));
82+
private static final boolean DEFER_ON_UPLOAD_PRESSURE =
83+
System.getProperty("voxy.nodeCleanerDeferOnUploadPressure", "true").equalsIgnoreCase("true");
8284

8385
private int frameCounter = 0;
8486
private int deferCleanFrames = 0;
@@ -220,6 +222,9 @@ private boolean shouldCleanGeometry() {
220222
public void updateIds(IntOpenHashSet collection) {
221223
if (!collection.isEmpty()) {
222224
int count = collection.size();
225+
if (DEFER_ON_UPLOAD_PRESSURE && UploadStream.INSTANCE.shouldDefer(count * 4L + 16L)) {
226+
return;
227+
}
223228
long addr = UploadStream.INSTANCE.rawUploadAddress(count * 4 + 16);//TODO ensure alignment, create method todo alignment things
224229
addr = (addr+15)&~15L;//Align to 16 bytes
225230

src/main/java/me/cortex/voxy/client/core/rendering/util/UploadStream.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public class UploadStream {
3232
private static final boolean USE_COHERENT = false;
3333
private static final int STREAM_FULL_ATTEMPTS =
3434
Math.max(1, Integer.getInteger("voxy.uploadStreamFullAttempts", 4));
35+
private static final boolean ALLOW_FORCED_FINISH =
36+
System.getProperty("voxy.uploadStreamAllowFinish", "false").equalsIgnoreCase("true");
37+
private static final double DEFER_USAGE_THRESHOLD =
38+
Math.max(0.0, Math.min(1.0, Double.parseDouble(System.getProperty("voxy.uploadStreamDeferUsageThreshold", "0.90"))));
3539
private static final boolean LOG_UPLOAD_PRESSURE =
3640
System.getProperty("voxy.logUploadPressure", "true").equalsIgnoreCase("true");
3741
private static final int LOG_UPLOAD_PRESSURE_EVERY =
@@ -99,12 +103,14 @@ public long rawUploadAddress(int size) {
99103
this.maybeLogUploadPressure();
100104
}
101105

102-
int attempts = STREAM_FULL_ATTEMPTS;
103-
while (--attempts != 0 && this.caddr == SIZE_LIMIT) {
104-
this.streamFullForcedFinishCalls++;
105-
glFinish();
106-
this.tick(false);
107-
this.caddr = this.allocationArena.alloc((int) size);
106+
if (ALLOW_FORCED_FINISH) {
107+
int attempts = STREAM_FULL_ATTEMPTS;
108+
while (--attempts != 0 && this.caddr == SIZE_LIMIT) {
109+
this.streamFullForcedFinishCalls++;
110+
glFinish();
111+
this.tick(false);
112+
this.caddr = this.allocationArena.alloc((int) size);
113+
}
108114
}
109115
if (this.caddr == SIZE_LIMIT) {
110116
this.streamFullHardFailures++;
@@ -185,6 +191,19 @@ public int getRawBufferId() {
185191
return this.uploadBuffer.id;
186192
}
187193

194+
public boolean shouldDefer(long incomingBytes) {
195+
if (incomingBytes <= 0) {
196+
return false;
197+
}
198+
long limit = this.allocationArena.getLimit();
199+
if (limit <= 0) {
200+
return false;
201+
}
202+
long used = this.allocationArena.getSize();
203+
long free = Math.max(0L, limit - used);
204+
return free < incomingBytes || ((double) used / (double) limit) >= DEFER_USAGE_THRESHOLD;
205+
}
206+
188207
private void maybeLogUploadPressure() {
189208
if (!LOG_UPLOAD_PRESSURE) {
190209
return;

src/main/resources/assets/voxy/shaders/lod/hierarchical/traversal_dev.comp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ void traverse(in UnpackedNode node) {
128128
#ifdef DISABLE_TRAVERSAL_VISIBILITY_CULLING
129129
if (false) {
130130
#else
131-
if (outsideFrustum() || isCulledByHiz()) {
131+
bool hizCulled = node.lodLevel < HIZ_DISABLE_FROM_LOD && isCulledByHiz();
132+
if (outsideFrustum() || hizCulled) {
132133
#endif
133134
//printf("culled");
134135
} else {

0 commit comments

Comments
 (0)