Try to reduce stutter when loading
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user