From 985e0beafdd2b45cb46ad9fb3a0b78a8b1f4f2a2 Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:55:36 +1000 Subject: [PATCH] Try to reduce stutter when loading --- .../client/core/rendering/RenderService.java | 15 +++++++-- .../building/RenderGenerationService.java | 24 +++++++++++--- .../voxy/common/thread/ServiceSlice.java | 19 +++++++++++- .../voxy/common/thread/ServiceThreadPool.java | 31 ++++++++++++++----- .../cortex/voxy/common/util/MessageQueue.java | 11 ++++++- .../world/service/VoxelIngestService.java | 2 +- 6 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java index 17351982..fa196d74 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java @@ -75,7 +75,9 @@ public class RenderService, J extends Vi this.geometryUpdateQueue = new MessageQueue<>(this.nodeManager::processGeometryResult); 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 -> { section.acquire(); @@ -129,8 +131,11 @@ public class RenderService, J extends Vi DownloadStream.INSTANCE.tick(); - this.sectionUpdateQueue.consume(); - this.geometryUpdateQueue.consume(); + this.sectionUpdateQueue.consume(128); + + //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 UploadStream.INSTANCE.commit(); } @@ -185,12 +190,16 @@ public class RenderService, J extends Vi this.world.getMapper().setBiomeCallback(null); this.world.getMapper().setStateCallback(null); + //Release all the unprocessed built geometry + this.geometryUpdateQueue.clear(BuiltSection::free); + this.modelService.shutdown(); this.renderGen.shutdown(); this.viewportSelector.free(); this.sectionRenderer.free(); this.traversal.free(); this.nodeCleaner.free(); + //Release all the unprocessed built geometry this.geometryUpdateQueue.clear(BuiltSection::free); this.sectionUpdateQueue.clear(WorldSection::release);//Release anything thats in the queue diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java index b20146c7..6fd71a2a 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java @@ -14,6 +14,7 @@ import me.cortex.voxy.common.thread.ServiceSlice; import me.cortex.voxy.common.thread.ServiceThreadPool; import java.util.List; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; @@ -39,6 +40,10 @@ public class RenderGenerationService { public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer consumer, boolean emitMeshlets) { + this(world, modelBakery, serviceThreadPool, consumer, emitMeshlets, ()->true); + } + + public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer consumer, boolean emitMeshlets, BooleanSupplier taskLimiter) { this.emitMeshlets = emitMeshlets; this.world = world; this.modelBakery = modelBakery; @@ -50,7 +55,7 @@ public class RenderGenerationService { return new Pair<>(() -> { this.processJob(factory); }, factory::free); - }); + }, taskLimiter); } //NOTE: the biomes are always fully populated/kept up to date @@ -166,6 +171,10 @@ public class RenderGenerationService { this.threads.execute(); 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() { //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) { - var task = this.taskQueue.removeFirst(); - if (task.section != null) { - task.section.release(); + for (int j = 0; j < i; j++) { + var task = this.taskQueue.removeFirst(); + if (task.section != null) { + task.section.release(); + } } } } diff --git a/src/main/java/me/cortex/voxy/common/thread/ServiceSlice.java b/src/main/java/me/cortex/voxy/common/thread/ServiceSlice.java index b32a1c82..5e3b4171 100644 --- a/src/main/java/me/cortex/voxy/common/thread/ServiceSlice.java +++ b/src/main/java/me/cortex/voxy/common/thread/ServiceSlice.java @@ -139,6 +139,10 @@ public class ServiceSlice extends TrackedObject { return this.jobCount.availablePermits() != 0; } + boolean workConditionMet() { + return this.condition.getAsBoolean(); + } + public void blockTillEmpty() { while (this.activeCount.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) { throw new IllegalStateException("Job count negative!!!"); } - this.threadPool.steal(this); + this.threadPool.steal(this, 1); 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() { return this.alive; } diff --git a/src/main/java/me/cortex/voxy/common/thread/ServiceThreadPool.java b/src/main/java/me/cortex/voxy/common/thread/ServiceThreadPool.java index aadc3ed4..c0099d88 100644 --- a/src/main/java/me/cortex/voxy/common/thread/ServiceThreadPool.java +++ b/src/main/java/me/cortex/voxy/common/thread/ServiceThreadPool.java @@ -127,9 +127,9 @@ public class ServiceThreadPool { this.jobCounter.release(1); } - void steal(ServiceSlice service) { - this.totalJobWeight.addAndGet(-service.weightPerJob); - this.jobCounter.acquireUninterruptibly(1); + void steal(ServiceSlice service, int count) { + this.totalJobWeight.addAndGet(-(service.weightPerJob*(long)count)); + this.jobCounter.acquireUninterruptibly(count); } private void worker(int threadId) { @@ -141,9 +141,17 @@ public class ServiceThreadPool { break; } - int attempts = 50; + final int ATTEMPT_COUNT = 50; + int attempts = ATTEMPT_COUNT; outer: while (true) { + if (attempts < ATTEMPT_COUNT-2) { + try { + Thread.sleep(20); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } var ref = this.serviceSlices; if (ref.length == 0) { Logger.error("Service worker tried to run but had 0 slices"); @@ -152,7 +160,7 @@ public class ServiceThreadPool { if (attempts-- == 0) { Logger.warn("Unable to execute service after many attempts, releasing"); try { - Thread.sleep(10); + Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } @@ -169,13 +177,20 @@ public class ServiceThreadPool { 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 if (((seed>>10)&63) == 0) { for (int i = 0; i < ref.length; i++) { int idx = (i+revolvingSelector)%ref.length; var slice = ref[idx]; - if (slice.hasJobs()) { + if (slice.hasJobs() && slice.workConditionMet()) { service = slice; revolvingSelector = (idx+1)%ref.length; break; @@ -186,7 +201,7 @@ public class ServiceThreadPool { long chosenNumber = clamped % weight; for (var slice : ref) { chosenNumber -= ((long) slice.weightPerJob) * slice.jobCount.availablePermits(); - if (chosenNumber <= 0) { + if (chosenNumber <= 0 && slice.workConditionMet()) { service = slice; break; } diff --git a/src/main/java/me/cortex/voxy/common/util/MessageQueue.java b/src/main/java/me/cortex/voxy/common/util/MessageQueue.java index f0d044b8..a2b18bee 100644 --- a/src/main/java/me/cortex/voxy/common/util/MessageQueue.java +++ b/src/main/java/me/cortex/voxy/common/util/MessageQueue.java @@ -1,11 +1,13 @@ package me.cortex.voxy.common.util; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; public class MessageQueue { private final Consumer consumer; private final ConcurrentLinkedDeque queue = new ConcurrentLinkedDeque<>(); + private final AtomicInteger count = new AtomicInteger(0); public MessageQueue(Consumer consumer) { this.consumer = consumer; @@ -13,6 +15,7 @@ public class MessageQueue { public void push(T obj) { this.queue.add(obj); + this.count.addAndGet(1); } public int consume() { @@ -23,10 +26,13 @@ public class MessageQueue { int i = 0; while (i < max) { var entry = this.queue.poll(); - if (entry == null) return i; + if (entry == null) break; i++; this.consumer.accept(entry); } + if (i != 0) { + this.count.addAndGet(-i); + } return i; } @@ -36,4 +42,7 @@ public class MessageQueue { } } + public int count() { + return this.count.get(); + } } diff --git a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java index 5d4d97c7..6b0e6aae 100644 --- a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java +++ b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java @@ -25,7 +25,7 @@ public class VoxelIngestService { private final ConcurrentLinkedDeque ingestQueue = new ConcurrentLinkedDeque<>(); 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() {