Try to reduce stutter when loading

This commit is contained in:
mcrcortex
2025-04-25 20:55:36 +10:00
parent 7c27a4d8fd
commit 985e0beafd
6 changed files with 83 additions and 19 deletions

View File

@@ -75,7 +75,9 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
this.geometryUpdateQueue = new MessageQueue<>(this.nodeManager::processGeometryResult); this.geometryUpdateQueue = new MessageQueue<>(this.nodeManager::processGeometryResult);
this.viewportSelector = new ViewportSelector<>(this.sectionRenderer::createViewport); this.viewportSelector = new ViewportSelector<>(this.sectionRenderer::createViewport);
this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool, this.geometryUpdateQueue::push, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets); this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool,
this.geometryUpdateQueue::push, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets,
()->this.geometryUpdateQueue.count()<2000);
router.setCallbacks(this.renderGen::enqueueTask, section -> { router.setCallbacks(this.renderGen::enqueueTask, section -> {
section.acquire(); section.acquire();
@@ -129,8 +131,11 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
DownloadStream.INSTANCE.tick(); DownloadStream.INSTANCE.tick();
this.sectionUpdateQueue.consume(); this.sectionUpdateQueue.consume(128);
this.geometryUpdateQueue.consume();
//Cap the number of consumed sections per frame to 40 + 2% of the queue size, cap of 200
int geoUpdateCap = Math.max(100, Math.min((int)(0.02*this.geometryUpdateQueue.count()), 200));
this.geometryUpdateQueue.consume(geoUpdateCap);
if (this.nodeManager.writeChanges(this.traversal.getNodeBuffer())) {//TODO: maybe move the node buffer out of the traversal class if (this.nodeManager.writeChanges(this.traversal.getNodeBuffer())) {//TODO: maybe move the node buffer out of the traversal class
UploadStream.INSTANCE.commit(); UploadStream.INSTANCE.commit();
} }
@@ -185,12 +190,16 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
this.world.getMapper().setBiomeCallback(null); this.world.getMapper().setBiomeCallback(null);
this.world.getMapper().setStateCallback(null); this.world.getMapper().setStateCallback(null);
//Release all the unprocessed built geometry
this.geometryUpdateQueue.clear(BuiltSection::free);
this.modelService.shutdown(); this.modelService.shutdown();
this.renderGen.shutdown(); this.renderGen.shutdown();
this.viewportSelector.free(); this.viewportSelector.free();
this.sectionRenderer.free(); this.sectionRenderer.free();
this.traversal.free(); this.traversal.free();
this.nodeCleaner.free(); this.nodeCleaner.free();
//Release all the unprocessed built geometry //Release all the unprocessed built geometry
this.geometryUpdateQueue.clear(BuiltSection::free); this.geometryUpdateQueue.clear(BuiltSection::free);
this.sectionUpdateQueue.clear(WorldSection::release);//Release anything thats in the queue this.sectionUpdateQueue.clear(WorldSection::release);//Release anything thats in the queue

View File

@@ -14,6 +14,7 @@ import me.cortex.voxy.common.thread.ServiceSlice;
import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.thread.ServiceThreadPool;
import java.util.List; import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -39,6 +40,10 @@ public class RenderGenerationService {
public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer<BuiltSection> consumer, boolean emitMeshlets) { public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer<BuiltSection> consumer, boolean emitMeshlets) {
this(world, modelBakery, serviceThreadPool, consumer, emitMeshlets, ()->true);
}
public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer<BuiltSection> consumer, boolean emitMeshlets, BooleanSupplier taskLimiter) {
this.emitMeshlets = emitMeshlets; this.emitMeshlets = emitMeshlets;
this.world = world; this.world = world;
this.modelBakery = modelBakery; this.modelBakery = modelBakery;
@@ -50,7 +55,7 @@ public class RenderGenerationService {
return new Pair<>(() -> { return new Pair<>(() -> {
this.processJob(factory); this.processJob(factory);
}, factory::free); }, factory::free);
}); }, taskLimiter);
} }
//NOTE: the biomes are always fully populated/kept up to date //NOTE: the biomes are always fully populated/kept up to date
@@ -166,6 +171,10 @@ public class RenderGenerationService {
this.threads.execute(); this.threads.execute();
return new BuildTask(key); return new BuildTask(key);
}); });
//Prioritize lower detail builds
if (WorldEngine.getLevel(pos) > 2) {
this.taskQueue.getAndMoveToFirst(pos);
}
} }
} }
@@ -189,11 +198,16 @@ public class RenderGenerationService {
public void shutdown() { public void shutdown() {
//Steal and free as much work as possible //Steal and free as much work as possible
while (this.threads.steal()) { while (this.threads.hasJobs()) {
int i = this.threads.drain();
if (i == 0) break;
synchronized (this.taskQueue) { synchronized (this.taskQueue) {
var task = this.taskQueue.removeFirst(); for (int j = 0; j < i; j++) {
if (task.section != null) { var task = this.taskQueue.removeFirst();
task.section.release(); if (task.section != null) {
task.section.release();
}
} }
} }
} }

View File

@@ -139,6 +139,10 @@ public class ServiceSlice extends TrackedObject {
return this.jobCount.availablePermits() != 0; return this.jobCount.availablePermits() != 0;
} }
boolean workConditionMet() {
return this.condition.getAsBoolean();
}
public void blockTillEmpty() { public void blockTillEmpty() {
while (this.activeCount.get() != 0 && this.alive) { while (this.activeCount.get() != 0 && this.alive) {
while (this.jobCount2.get() != 0 && this.alive) { while (this.jobCount2.get() != 0 && this.alive) {
@@ -161,10 +165,23 @@ public class ServiceSlice extends TrackedObject {
if (this.jobCount2.decrementAndGet() < 0) { if (this.jobCount2.decrementAndGet() < 0) {
throw new IllegalStateException("Job count negative!!!"); throw new IllegalStateException("Job count negative!!!");
} }
this.threadPool.steal(this); this.threadPool.steal(this, 1);
return true; return true;
} }
public int drain() {
int count = this.jobCount.drainPermits();
if (count == 0) {
return 0;
}
if (this.jobCount2.addAndGet(-count) < 0) {
throw new IllegalStateException("Job count negative!!!");
}
this.threadPool.steal(this, count);
return count;
}
public boolean isAlive() { public boolean isAlive() {
return this.alive; return this.alive;
} }

View File

@@ -127,9 +127,9 @@ public class ServiceThreadPool {
this.jobCounter.release(1); this.jobCounter.release(1);
} }
void steal(ServiceSlice service) { void steal(ServiceSlice service, int count) {
this.totalJobWeight.addAndGet(-service.weightPerJob); this.totalJobWeight.addAndGet(-(service.weightPerJob*(long)count));
this.jobCounter.acquireUninterruptibly(1); this.jobCounter.acquireUninterruptibly(count);
} }
private void worker(int threadId) { private void worker(int threadId) {
@@ -141,9 +141,17 @@ public class ServiceThreadPool {
break; break;
} }
int attempts = 50; final int ATTEMPT_COUNT = 50;
int attempts = ATTEMPT_COUNT;
outer: outer:
while (true) { while (true) {
if (attempts < ATTEMPT_COUNT-2) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
var ref = this.serviceSlices; var ref = this.serviceSlices;
if (ref.length == 0) { if (ref.length == 0) {
Logger.error("Service worker tried to run but had 0 slices"); Logger.error("Service worker tried to run but had 0 slices");
@@ -152,7 +160,7 @@ public class ServiceThreadPool {
if (attempts-- == 0) { if (attempts-- == 0) {
Logger.warn("Unable to execute service after many attempts, releasing"); Logger.warn("Unable to execute service after many attempts, releasing");
try { try {
Thread.sleep(10); Thread.sleep(100);
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -169,13 +177,20 @@ public class ServiceThreadPool {
break; break;
} }
ServiceSlice service = ref[(int) (clamped % ref.length)]; ServiceSlice service = ref[0];
for (int i = 0; i < ref.length; i++) {
service = ref[(int) ((clamped+i) % ref.length)];
if (service.workConditionMet()) {
break;
}
}
//1 in 64 chance just to pick a service that has a task, in a cycling manor, this is to keep at least one service from overloading all services constantly //1 in 64 chance just to pick a service that has a task, in a cycling manor, this is to keep at least one service from overloading all services constantly
if (((seed>>10)&63) == 0) { if (((seed>>10)&63) == 0) {
for (int i = 0; i < ref.length; i++) { for (int i = 0; i < ref.length; i++) {
int idx = (i+revolvingSelector)%ref.length; int idx = (i+revolvingSelector)%ref.length;
var slice = ref[idx]; var slice = ref[idx];
if (slice.hasJobs()) { if (slice.hasJobs() && slice.workConditionMet()) {
service = slice; service = slice;
revolvingSelector = (idx+1)%ref.length; revolvingSelector = (idx+1)%ref.length;
break; break;
@@ -186,7 +201,7 @@ public class ServiceThreadPool {
long chosenNumber = clamped % weight; long chosenNumber = clamped % weight;
for (var slice : ref) { for (var slice : ref) {
chosenNumber -= ((long) slice.weightPerJob) * slice.jobCount.availablePermits(); chosenNumber -= ((long) slice.weightPerJob) * slice.jobCount.availablePermits();
if (chosenNumber <= 0) { if (chosenNumber <= 0 && slice.workConditionMet()) {
service = slice; service = slice;
break; break;
} }

View File

@@ -1,11 +1,13 @@
package me.cortex.voxy.common.util; package me.cortex.voxy.common.util;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
public class MessageQueue <T> { public class MessageQueue <T> {
private final Consumer<T> consumer; private final Consumer<T> consumer;
private final ConcurrentLinkedDeque<T> queue = new ConcurrentLinkedDeque<>(); private final ConcurrentLinkedDeque<T> queue = new ConcurrentLinkedDeque<>();
private final AtomicInteger count = new AtomicInteger(0);
public MessageQueue(Consumer<T> consumer) { public MessageQueue(Consumer<T> consumer) {
this.consumer = consumer; this.consumer = consumer;
@@ -13,6 +15,7 @@ public class MessageQueue <T> {
public void push(T obj) { public void push(T obj) {
this.queue.add(obj); this.queue.add(obj);
this.count.addAndGet(1);
} }
public int consume() { public int consume() {
@@ -23,10 +26,13 @@ public class MessageQueue <T> {
int i = 0; int i = 0;
while (i < max) { while (i < max) {
var entry = this.queue.poll(); var entry = this.queue.poll();
if (entry == null) return i; if (entry == null) break;
i++; i++;
this.consumer.accept(entry); this.consumer.accept(entry);
} }
if (i != 0) {
this.count.addAndGet(-i);
}
return i; return i;
} }
@@ -36,4 +42,7 @@ public class MessageQueue <T> {
} }
} }
public int count() {
return this.count.get();
}
} }

View File

@@ -25,7 +25,7 @@ public class VoxelIngestService {
private final ConcurrentLinkedDeque<IngestSection> ingestQueue = new ConcurrentLinkedDeque<>(); private final ConcurrentLinkedDeque<IngestSection> ingestQueue = new ConcurrentLinkedDeque<>();
public VoxelIngestService(ServiceThreadPool pool) { public VoxelIngestService(ServiceThreadPool pool) {
this.threads = pool.createServiceNoCleanup("Ingest service", 100, ()-> this::processJob); this.threads = pool.createServiceNoCleanup("Ingest service", 1000, ()-> this::processJob);
} }
private void processJob() { private void processJob() {