From 403317fd29706ecab3eff359b0084b8fe97eabeb Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Tue, 6 Aug 2024 21:50:05 +1000 Subject: [PATCH] Swap to a single ServiceThreadPool workload --- src/main/java/me/cortex/voxy/client/Voxy.java | 1 - .../cortex/voxy/client/config/VoxyConfig.java | 15 +- .../config/VoxyConfigScreenFactory.java | 28 ++-- .../me/cortex/voxy/client/core/VoxelCore.java | 13 +- .../client/core/rendering/RenderService.java | 5 +- .../building/RenderGenerationService.java | 151 +++++++----------- .../client/saver/ContextSelectionSystem.java | 5 +- .../cortex/voxy/common/world/WorldEngine.java | 9 +- .../world/service/SectionSavingService.java | 94 ++++------- .../world/service/ServiceThreadPool.java | 72 --------- .../world/service/VoxelIngestService.java | 124 +++++--------- .../common/world/thread/ServiceSlice.java | 102 ++++++++++++ .../world/thread/ServiceThreadPool.java | 146 +++++++++++++++++ .../resources/assets/voxy/lang/en_us.json | 2 +- 14 files changed, 409 insertions(+), 358 deletions(-) delete mode 100644 src/main/java/me/cortex/voxy/common/world/service/ServiceThreadPool.java create mode 100644 src/main/java/me/cortex/voxy/common/world/thread/ServiceSlice.java create mode 100644 src/main/java/me/cortex/voxy/common/world/thread/ServiceThreadPool.java diff --git a/src/main/java/me/cortex/voxy/client/Voxy.java b/src/main/java/me/cortex/voxy/client/Voxy.java index 75c66b22..14f5c6a6 100644 --- a/src/main/java/me/cortex/voxy/client/Voxy.java +++ b/src/main/java/me/cortex/voxy/client/Voxy.java @@ -23,7 +23,6 @@ public class Voxy implements ClientModInitializer { @Override public void onInitializeClient() { - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { dispatcher.register(WorldImportCommand.register()); }); diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java index f70afefc..3523f5ad 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java @@ -24,15 +24,10 @@ public class VoxyConfig { public boolean enabled = true; public boolean ingestEnabled = true; - public int qualityScale = 12; - public int maxSections = 200_000; - public int renderDistance = 128; - public int geometryBufferSize = (1<<30)/8; - public int ingestThreads = 2; - public int savingThreads = 4; - public int renderThreads = 5; - public boolean useMeshShaderIfPossible = true; + public int renderDistance = 128;//Unused at the present + public int serviceThreads = Math.max(Runtime.getRuntime().availableProcessors()/2, 1); public String defaultSaveConfig; + public int renderQuality = 256;//Smaller is higher quality public static VoxyConfig loadOrCreate() { @@ -69,8 +64,4 @@ public class VoxyConfig { .getConfigDir() .resolve("voxy-config.json"); } - - public boolean useMeshShaders() { - return this.useMeshShaderIfPossible && Capabilities.INSTANCE.meshShaders; - } } diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java index 9be36c7d..31d4ceae 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java @@ -46,8 +46,6 @@ public class VoxyConfigScreenFactory implements ModMenuApi { } private static void addGeneralCategory(ConfigBuilder builder, VoxyConfig config) { - - ConfigCategory category = builder.getOrCreateCategory(Text.translatable("voxy.config.general")); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); @@ -76,12 +74,13 @@ public class VoxyConfigScreenFactory implements ModMenuApi { .setDefaultValue(DEFAULT.ingestEnabled) .build()); - category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.quality"), config.qualityScale, 8, 32) + category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.quality"), config.renderQuality, 32, 512) .setTooltip(Text.translatable("voxy.config.general.quality.tooltip")) - .setSaveConsumer(val -> config.qualityScale = val) - .setDefaultValue(DEFAULT.qualityScale) + .setSaveConsumer(val -> config.renderQuality = val) + .setDefaultValue(DEFAULT.renderQuality) .build()); + /* category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.geometryBuffer"), config.geometryBufferSize, (1<<27)/8, ((1<<31)-1)/8) .setTooltip(Text.translatable("voxy.config.general.geometryBuffer.tooltip")) .setSaveConsumer(val -> config.geometryBufferSize = val) @@ -93,32 +92,22 @@ public class VoxyConfigScreenFactory implements ModMenuApi { .setSaveConsumer(val -> config.maxSections = val) .setDefaultValue(DEFAULT.maxSections) .build()); + */ category.addEntry(entryBuilder.startIntField(Text.translatable("voxy.config.general.renderDistance"), config.renderDistance) .setTooltip(Text.translatable("voxy.config.general.renderDistance.tooltip")) .setSaveConsumer(val -> config.renderDistance = val) .setDefaultValue(DEFAULT.renderDistance) .build()); - - category.addEntry(entryBuilder.startBooleanToggle(Text.translatable("voxy.config.general.nvmesh"), config.useMeshShaderIfPossible) - .setTooltip(Text.translatable("voxy.config.general.nvmesh.tooltip")) - .setSaveConsumer(val -> config.useMeshShaderIfPossible = val) - .setDefaultValue(DEFAULT.useMeshShaderIfPossible) - .build()); - - //category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.compression"), config.savingCompressionLevel, 1, 21) - // .setTooltip(Text.translatable("voxy.config.general.compression.tooltip")) - // .setSaveConsumer(val -> config.savingCompressionLevel = val) - // .setDefaultValue(DEFAULT.savingCompressionLevel) - // .build()); } private static void addThreadsCategory(ConfigBuilder builder, VoxyConfig config) { ConfigCategory category = builder.getOrCreateCategory(Text.translatable("voxy.config.threads")); ConfigEntryBuilder entryBuilder = builder.entryBuilder(); - category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.threads.ingest"), config.ingestThreads, 1, Runtime.getRuntime().availableProcessors()) - .setTooltip(Text.translatable("voxy.config.ingest.tooltip")) + /* + category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.threads.service"), config.serviceThreads, 1, Runtime.getRuntime().availableProcessors()) + .setTooltip(Text.translatable("voxy.config.threads.tooltip")) .setSaveConsumer(val -> config.ingestThreads = val) .setDefaultValue(DEFAULT.ingestThreads) .build()); @@ -134,6 +123,7 @@ public class VoxyConfigScreenFactory implements ModMenuApi { .setSaveConsumer(val -> config.renderThreads = val) .setDefaultValue(DEFAULT.renderThreads) .build()); + */ } private static void addStorageCategory(ConfigBuilder builder, VoxyConfig config) { diff --git a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java index 73b5b904..7e1b9f8e 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java @@ -2,6 +2,7 @@ package me.cortex.voxy.client.core; import com.mojang.blaze3d.systems.RenderSystem; import me.cortex.voxy.client.Voxy; +import me.cortex.voxy.client.config.VoxyConfig; import me.cortex.voxy.client.core.rendering.*; import me.cortex.voxy.client.core.rendering.post.PostProcessing; import me.cortex.voxy.client.core.rendering.util.DownloadStream; @@ -9,6 +10,7 @@ import me.cortex.voxy.client.core.util.IrisUtil; import me.cortex.voxy.client.saver.ContextSelectionSystem; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.client.importers.WorldImporter; +import me.cortex.voxy.common.world.thread.ServiceThreadPool; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.hud.ClientBossBar; import net.minecraft.client.render.Camera; @@ -54,20 +56,21 @@ public class VoxelCore { private final RenderService renderer; private final PostProcessing postProcessing; - - //private final Thread shutdownThread = new Thread(this::shutdown); + private final ServiceThreadPool serviceThreadPool; private WorldImporter importer; public VoxelCore(ContextSelectionSystem.Selection worldSelection) { - this.world = worldSelection.createEngine(); var cfg = worldSelection.getConfig(); + this.serviceThreadPool = new ServiceThreadPool(VoxyConfig.CONFIG.serviceThreads); + + this.world = worldSelection.createEngine(this.serviceThreadPool); System.out.println("Initializing voxy core"); //Trigger the shared index buffer loading SharedIndexBuffer.INSTANCE.id(); Capabilities.init();//Ensure clinit is called - this.renderer = new RenderService(this.world); + this.renderer = new RenderService(this.world, this.serviceThreadPool); System.out.println("Using " + this.renderer.getClass().getSimpleName()); this.postProcessing = new PostProcessing(); @@ -183,6 +186,8 @@ public class VoxelCore { if (this.postProcessing!=null){try {this.postProcessing.shutdown();} catch (Exception e) {e.printStackTrace();}} System.out.println("Shutting down world engine"); try {this.world.shutdown();} catch (Exception e) {e.printStackTrace();} + System.out.println("Shutting down service thread pool"); + this.serviceThreadPool.shutdown(); System.out.println("Voxel core shut down"); } 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 c1effbcb..04885b28 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 @@ -15,6 +15,7 @@ import me.cortex.voxy.client.core.rendering.util.DownloadStream; import me.cortex.voxy.client.core.rendering.util.UploadStream; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldSection; +import me.cortex.voxy.common.world.thread.ServiceThreadPool; import net.minecraft.client.render.Camera; import java.util.Arrays; @@ -40,7 +41,7 @@ public class RenderService, J extends Vi private final ConcurrentLinkedDeque sectionBuildResultQueue = new ConcurrentLinkedDeque<>(); - public RenderService(WorldEngine world) { + public RenderService(WorldEngine world, ServiceThreadPool serviceThreadPool) { this.modelService = new ModelBakerySubsystem(world.getMapper()); //Max sections: ~500k @@ -53,7 +54,7 @@ public class RenderService, J extends Vi this.nodeManager = new HierarchicalNodeManager(1<<21, this.sectionRenderer.getGeometryManager(), positionFilterForwarder); this.viewportSelector = new ViewportSelector<>(this.sectionRenderer::createViewport); - this.renderGen = new RenderGenerationService(world, this.modelService, VoxyConfig.CONFIG.renderThreads, this.sectionBuildResultQueue::add, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets); + this.renderGen = new RenderGenerationService(world, this.modelService, serviceThreadPool, this.sectionBuildResultQueue::add, this.sectionRenderer.getGeometryManager() instanceof IUsesMeshlets); positionFilterForwarder.setCallback(this.renderGen::enqueueTask); this.traversal = new HierarchicalOcclusionTraverser(this.nodeManager, 512); 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 117ecc27..af56205b 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 @@ -7,6 +7,8 @@ import me.cortex.voxy.client.core.model.ModelBakerySubsystem; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldSection; import me.cortex.voxy.common.world.other.Mapper; +import me.cortex.voxy.common.world.thread.ServiceSlice; +import me.cortex.voxy.common.world.thread.ServiceThreadPool; import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; @@ -20,31 +22,30 @@ public class RenderGenerationService { public interface TaskChecker {boolean check(int lvl, int x, int y, int z);} private record BuildTask(long position, Supplier sectionSupplier, boolean[] hasDoneModelRequest) {} - private volatile boolean running = true; - private final Thread[] workers; - private final Long2ObjectLinkedOpenHashMap taskQueue = new Long2ObjectLinkedOpenHashMap<>(); - private final Semaphore taskCounter = new Semaphore(0); private final WorldEngine world; private final ModelBakerySubsystem modelBakery; private final Consumer resultConsumer; private final BuiltSectionMeshCache meshCache = new BuiltSectionMeshCache(); private final boolean emitMeshlets; - public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, int workers, Consumer consumer, boolean emitMeshlets) { + private final ServiceSlice threads; + + + public RenderGenerationService(WorldEngine world, ModelBakerySubsystem modelBakery, ServiceThreadPool serviceThreadPool, Consumer consumer, boolean emitMeshlets) { this.emitMeshlets = emitMeshlets; this.world = world; this.modelBakery = modelBakery; this.resultConsumer = consumer; - this.workers = new Thread[workers]; - for (int i = 0; i < workers; i++) { - this.workers[i] = new Thread(this::renderWorker); - this.workers[i].setPriority(3); - this.workers[i].setDaemon(true); - this.workers[i].setName("Render generation service #" + i); - this.workers[i].start(); - } + + this.threads = serviceThreadPool.createService("Section mesh generation service", 100, ()->{ + //Thread local instance of the factory + var factory = new RenderDataFactory(this.world, this.modelBakery.factory, this.emitMeshlets); + return () -> { + this.processJob(factory); + }; + }); } //NOTE: the biomes are always fully populated/kept up to date @@ -65,64 +66,53 @@ public class RenderGenerationService { } //TODO: add a generated render data cache - private void renderWorker() { - //Thread local instance of the factory - var factory = new RenderDataFactory(this.world, this.modelBakery.factory, this.emitMeshlets); - while (this.running) { - this.taskCounter.acquireUninterruptibly(); - if (!this.running) break; - try { - BuildTask task; - synchronized (this.taskQueue) { - task = this.taskQueue.removeFirst(); - } - var section = task.sectionSupplier.get(); - if (section == null) { - this.resultConsumer.accept(new BuiltSection(task.position)); - continue; - } - section.assertNotFree(); - BuiltSection mesh = null; + private void processJob(RenderDataFactory factory) { + BuildTask task; + synchronized (this.taskQueue) { + task = this.taskQueue.removeFirst(); + } + var section = task.sectionSupplier.get(); + if (section == null) { + this.resultConsumer.accept(new BuiltSection(task.position)); + return; + } + section.assertNotFree(); + BuiltSection mesh = null; + try { + mesh = factory.generateMesh(section); + } catch (IdNotYetComputedException e) { + if (!this.modelBakery.factory.hasModelForBlockId(e.id)) { + this.modelBakery.requestBlockBake(e.id); + } + if (task.hasDoneModelRequest[0]) { try { - mesh = factory.generateMesh(section); - } catch (IdNotYetComputedException e) { - if (!this.modelBakery.factory.hasModelForBlockId(e.id)) { - this.modelBakery.requestBlockBake(e.id); - } - if (task.hasDoneModelRequest[0]) { - try { - Thread.sleep(10); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - } else { - //The reason for the extra id parameter is that we explicitly add/check against the exception id due to e.g. requesting accross a chunk boarder wont be captured in the request - this.computeAndRequestRequiredModels(section, e.id); - } - //We need to reinsert the build task into the queue - //System.err.println("Render task failed to complete due to un-computed client id"); - synchronized (this.taskQueue) { - var queuedTask = this.taskQueue.computeIfAbsent(section.key, (a)->task); - queuedTask.hasDoneModelRequest[0] = true;//Mark (or remark) the section as having chunks requested - - if (queuedTask == task) {//use the == not .equal to see if we need to release a permit - this.taskCounter.release();//Since we put in queue, release permit - } - } + Thread.sleep(10); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); } + } else { + //The reason for the extra id parameter is that we explicitly add/check against the exception id due to e.g. requesting accross a chunk boarder wont be captured in the request + this.computeAndRequestRequiredModels(section, e.id); + } + //We need to reinsert the build task into the queue + //System.err.println("Render task failed to complete due to un-computed client id"); + synchronized (this.taskQueue) { + var queuedTask = this.taskQueue.computeIfAbsent(section.key, (a)->task); + queuedTask.hasDoneModelRequest[0] = true;//Mark (or remark) the section as having chunks requested - //TODO: if the section was _not_ built, maybe dont release it, or release it with the hint - section.release(); - if (mesh != null) { - //TODO: if the mesh is null, need to clear the cache at that point - this.resultConsumer.accept(mesh.clone()); - if (!this.meshCache.putMesh(mesh)) { - mesh.free(); - } + if (queuedTask == task) {//use the == not .equal to see if we need to release a permit + this.threads.execute();//Since we put in queue, release permit } - } catch (Exception e) { - e.printStackTrace(); - MinecraftClient.getInstance().executeSync(()->MinecraftClient.getInstance().player.sendMessage(Text.literal("Voxy render service had an exception while executing please check logs and report error"))); + } + } + + //TODO: if the section was _not_ built, maybe dont release it, or release it with the hint + section.release(); + if (mesh != null) { + //TODO: if the mesh is null, need to clear the cache at that point + this.resultConsumer.accept(mesh.clone()); + if (!this.meshCache.putMesh(mesh)) { + mesh.free(); } } } @@ -169,7 +159,7 @@ public class RenderGenerationService { } synchronized (this.taskQueue) { this.taskQueue.computeIfAbsent(ikey, key->{ - this.taskCounter.release(); + this.threads.execute(); return new BuildTask(ikey, ()->{ if (checker.check(WorldEngine.getLevel(ikey), WorldEngine.getX(ikey), WorldEngine.getY(ikey), WorldEngine.getZ(ikey))) { return this.world.acquireIfExists(WorldEngine.getLevel(ikey), WorldEngine.getX(ikey), WorldEngine.getY(ikey), WorldEngine.getZ(ikey)); @@ -196,6 +186,7 @@ public class RenderGenerationService { this.meshCache.clearMesh(WorldEngine.getWorldSectionId(lvl, x, y, z)); } + /* public void removeTask(int lvl, int x, int y, int z) { synchronized (this.taskQueue) { if (this.taskQueue.remove(WorldEngine.getWorldSectionId(lvl, x, y, z)) != null) { @@ -203,32 +194,14 @@ public class RenderGenerationService { } } } + */ public int getTaskCount() { - return this.taskCounter.availablePermits(); + return this.threads.getJobCount(); } public void shutdown() { - boolean anyAlive = false; - for (var worker : this.workers) { - anyAlive |= worker.isAlive(); - } - - if (!anyAlive) { - System.err.println("Render gen workers already dead on shutdown! this is very very bad, check log for errors from this thread"); - return; - } - - //Since this is just render data, dont care about any tasks needing to finish - this.running = false; - this.taskCounter.release(1000); - - //Wait for thread to join - try { - for (var worker : this.workers) { - worker.join(); - } - } catch (InterruptedException e) {throw new RuntimeException(e);} + this.threads.shutdown(); //Cleanup any remaining data while (!this.taskQueue.isEmpty()) { diff --git a/src/main/java/me/cortex/voxy/client/saver/ContextSelectionSystem.java b/src/main/java/me/cortex/voxy/client/saver/ContextSelectionSystem.java index 91c4caf0..e5dd2699 100644 --- a/src/main/java/me/cortex/voxy/client/saver/ContextSelectionSystem.java +++ b/src/main/java/me/cortex/voxy/client/saver/ContextSelectionSystem.java @@ -9,6 +9,7 @@ import me.cortex.voxy.common.storage.config.StorageConfig; import me.cortex.voxy.common.storage.other.CompressionStorageAdaptor; import me.cortex.voxy.common.storage.rocksdb.RocksDBStorageBackend; import me.cortex.voxy.common.world.WorldEngine; +import me.cortex.voxy.common.world.thread.ServiceThreadPool; import net.minecraft.client.MinecraftClient; import net.minecraft.client.world.ClientWorld; import net.minecraft.util.WorldSavePath; @@ -96,8 +97,8 @@ public class ContextSelectionSystem { return this.config.storageConfig.build(ctx); } - public WorldEngine createEngine() { - return new WorldEngine(this.createStorageBackend(), VoxyConfig.CONFIG.ingestThreads, VoxyConfig.CONFIG.savingThreads, 5); + public WorldEngine createEngine(ServiceThreadPool serviceThreadPool) { + return new WorldEngine(this.createStorageBackend(), serviceThreadPool, 5); } //Saves the config for the world selection or something, need to figure out how to make it work with dimensional configs maybe? diff --git a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java index 6ca3a7bd..5abc55d5 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java @@ -1,11 +1,11 @@ package me.cortex.voxy.common.world; -import me.cortex.voxy.common.storage.StorageCompressor; import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.world.other.Mapper; import me.cortex.voxy.common.world.service.SectionSavingService; import me.cortex.voxy.common.world.service.VoxelIngestService; import me.cortex.voxy.common.storage.StorageBackend; +import me.cortex.voxy.common.world.thread.ServiceThreadPool; import org.lwjgl.system.MemoryUtil; import java.util.Arrays; @@ -22,22 +22,21 @@ public class WorldEngine { private Consumer dirtyCallback; private final int maxMipLevels; - public void setDirtyCallback(Consumer tracker) { this.dirtyCallback = tracker; } public Mapper getMapper() {return this.mapper;} - public WorldEngine(StorageBackend storageBackend, int ingestWorkers, int savingServiceWorkers, int maxMipLayers) { + public WorldEngine(StorageBackend storageBackend, ServiceThreadPool serviceThreadPool, int maxMipLayers) { this.maxMipLevels = maxMipLayers; this.storage = storageBackend; this.mapper = new Mapper(this.storage); //4 cache size bits means that the section tracker has 16 separate maps that it uses this.sectionTracker = new ActiveSectionTracker(3, this::unsafeLoadSection); - this.savingService = new SectionSavingService(this, savingServiceWorkers); - this.ingestService = new VoxelIngestService(this, ingestWorkers); + this.savingService = new SectionSavingService(this, serviceThreadPool); + this.ingestService = new VoxelIngestService(this, serviceThreadPool); } private int unsafeLoadSection(WorldSection into) { diff --git a/src/main/java/me/cortex/voxy/common/world/service/SectionSavingService.java b/src/main/java/me/cortex/voxy/common/world/service/SectionSavingService.java index 1415e920..05c58523 100644 --- a/src/main/java/me/cortex/voxy/common/world/service/SectionSavingService.java +++ b/src/main/java/me/cortex/voxy/common/world/service/SectionSavingService.java @@ -1,58 +1,42 @@ package me.cortex.voxy.common.world.service; -import me.cortex.voxy.common.storage.StorageCompressor; import me.cortex.voxy.common.world.SaveLoadSystem; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldSection; +import me.cortex.voxy.common.world.thread.ServiceSlice; +import me.cortex.voxy.common.world.thread.ServiceThreadPool; import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; import org.lwjgl.system.MemoryUtil; import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Semaphore; //TODO: add an option for having synced saving, that is when call enqueueSave, that will instead, instantly // save to the db, this can be useful for just reducing the amount of thread pools in total // might have some issues with threading if the same section is saved from multiple threads? public class SectionSavingService { - private volatile boolean running = true; - private final Thread[] workers; - + private final ServiceSlice threads; private final ConcurrentLinkedDeque saveQueue = new ConcurrentLinkedDeque<>(); - private final Semaphore saveCounter = new Semaphore(0); - private final WorldEngine world; - - public SectionSavingService(WorldEngine worldEngine, int workers) { - this.workers = new Thread[workers]; - for (int i = 0; i < workers; i++) { - var worker = new Thread(this::saveWorker); - worker.setDaemon(false); - worker.setName("Saving service #" + i); - worker.start(); - this.workers[i] = worker; - } + public SectionSavingService(WorldEngine worldEngine, ServiceThreadPool threadPool) { this.world = worldEngine; + this.threads = threadPool.createService("Section saving service", 100, () -> this::processJob); } - private void saveWorker() { - while (running) { - this.saveCounter.acquireUninterruptibly(); - if (!this.running) break; - var section = this.saveQueue.pop(); - section.assertNotFree(); - try { - section.inSaveQueue.set(false); - var saveData = SaveLoadSystem.serialize(section); - this.world.storage.setSectionData(section.key, saveData); - MemoryUtil.memFree(saveData); - } catch (Exception e) { - e.printStackTrace(); - MinecraftClient.getInstance().executeSync(()->MinecraftClient.getInstance().player.sendMessage(Text.literal("Voxy saver had an exception while executing please check logs and report error"))); - } - section.release(); + private void processJob() { + var section = this.saveQueue.pop(); + section.assertNotFree(); + try { + section.inSaveQueue.set(false); + var saveData = SaveLoadSystem.serialize(section); + this.world.storage.setSectionData(section.key, saveData); + MemoryUtil.memFree(saveData); + } catch (Exception e) { + e.printStackTrace(); + MinecraftClient.getInstance().executeSync(()->MinecraftClient.getInstance().player.sendMessage(Text.literal("Voxy saver had an exception while executing please check logs and report error"))); } + section.release(); } public void enqueueSave(WorldSection section) { @@ -61,47 +45,25 @@ public class SectionSavingService { //Acquire the section for use section.acquire(); this.saveQueue.add(section); - this.saveCounter.release(); + this.threads.execute(); } } public void shutdown() { - boolean anyAlive = false; - boolean allAlive = true; - for (var worker : this.workers) { - anyAlive |= worker.isAlive(); - allAlive &= worker.isAlive(); - } - - if (!anyAlive) { - System.err.println("Section saving workers already dead on shutdown! this is very very bad, check log for errors from this thread"); - return; - } - if (!allAlive) { - System.err.println("Some section saving works have died, please check log and report errors."); - } - - - int i = 0; - //Wait for all the saving to finish - while (this.saveCounter.availablePermits() != 0) { - try {Thread.sleep(500);} catch (InterruptedException e) {break;} - if (i++%10 == 0) { - System.out.println("Section saving shutdown has " + this.saveCounter.availablePermits() + " tasks remaining"); + if (this.threads.getJobCount() != 0) { + System.err.println("Voxy section saving still in progress, estimated " + this.threads.getJobCount() + " sections remaining."); + while (this.threads.getJobCount() != 0) { + Thread.onSpinWait(); } } - //Shutdown - this.running = false; - this.saveCounter.release(1000); - //Wait for threads to join - try { - for (var worker : this.workers) { - worker.join(); - } - } catch (InterruptedException e) {throw new RuntimeException(e);} + this.threads.shutdown(); + //Manually save any remaining entries + while (!this.saveQueue.isEmpty()) { + this.processJob(); + } } public int getTaskCount() { - return this.saveCounter.availablePermits(); + return this.threads.getJobCount(); } } diff --git a/src/main/java/me/cortex/voxy/common/world/service/ServiceThreadPool.java b/src/main/java/me/cortex/voxy/common/world/service/ServiceThreadPool.java deleted file mode 100644 index 66cf38e3..00000000 --- a/src/main/java/me/cortex/voxy/common/world/service/ServiceThreadPool.java +++ /dev/null @@ -1,72 +0,0 @@ -package me.cortex.voxy.common.world.service; - -import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.common.world.WorldSection; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.Text; - -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Semaphore; - -//TODO: -//FIXME: -// FINISHME: -// Use this instead of seperate thread pools, use a single shared pool where tasks are submitted to and worked on - -public class ServiceThreadPool { - private volatile boolean running = true; - private final Thread[] workers; - private final Semaphore jobCounter = new Semaphore(0); - //TODO: have a wrapper to specify extra information about the job for debugging - private final ConcurrentLinkedDeque jobQueue = new ConcurrentLinkedDeque<>(); - - - public ServiceThreadPool(int workers) { - this.workers = new Thread[workers]; - for (int i = 0; i < workers; i++) { - var worker = new Thread(this::worker); - worker.setDaemon(false); - worker.setName("Service worker #" + i); - worker.start(); - this.workers[i] = worker; - } - } - - private void worker() { - while (true) { - this.jobCounter.acquireUninterruptibly(); - if (!this.running) { - break; - } - var job = this.jobQueue.pop(); - try { - job.run(); - } catch (Exception e) { - e.printStackTrace(); - MinecraftClient.getInstance().executeSync(()-> - MinecraftClient.getInstance().player.sendMessage( - Text.literal( - "Voxy ingester had an exception while executing service job please check logs and report error"))); - } - } - } - - - public void shutdown() { - //Wait for the tasks to finish - while (this.jobCounter.availablePermits() != 0) { - Thread.onSpinWait(); - } - - //Shutdown - this.running = false; - this.jobCounter.release(1000); - - //Wait for thread to join - try { - for (var worker : this.workers) { - worker.join(); - } - } catch (InterruptedException e) {throw new RuntimeException(e);} - } -} 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 de918d2c..8375ef8d 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 @@ -4,6 +4,8 @@ import it.unimi.dsi.fastutil.Pair; import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.voxelization.WorldConversionFactory; import me.cortex.voxy.common.world.WorldEngine; +import me.cortex.voxy.common.world.thread.ServiceSlice; +import me.cortex.voxy.common.world.thread.ServiceThreadPool; import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; import net.minecraft.util.math.ChunkSectionPos; @@ -18,69 +20,48 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Semaphore; public class VoxelIngestService { - private volatile boolean running = true; - private final Thread[] workers; - + private final ServiceSlice threads; private final ConcurrentLinkedDeque ingestQueue = new ConcurrentLinkedDeque<>(); - private final Semaphore ingestCounter = new Semaphore(0); - private final ConcurrentHashMap> captureLightMap = new ConcurrentHashMap<>(1000,0.75f, 7); private final WorldEngine world; - public VoxelIngestService(WorldEngine world, int workers) { + public VoxelIngestService(WorldEngine world, ServiceThreadPool pool) { this.world = world; - - this.workers = new Thread[workers]; - for (int i = 0; i < workers; i++) { - var worker = new Thread(this::ingestWorker); - worker.setDaemon(false); - worker.setName("Ingest service #" + i); - worker.start(); - this.workers[i] = worker; - } + this.threads = pool.createService("Ingest service", 100, ()-> this::processJob); } - private void ingestWorker() { - while (this.running) { - this.ingestCounter.acquireUninterruptibly(); - if (!this.running) break; - try { - var chunk = this.ingestQueue.pop(); - int i = chunk.getBottomSectionCoord() - 1; - for (var section : chunk.getSectionArray()) { - i++; - var lighting = this.captureLightMap.remove(ChunkSectionPos.from(chunk.getPos(), i).asLong()); - if (section.isEmpty()) { - //TODO: add local cache so that it doesnt constantly create new sections - this.world.insertUpdate(VoxelizedSection.createEmpty().setPosition(chunk.getPos().x, i, chunk.getPos().z)); - } else { - VoxelizedSection csec = WorldConversionFactory.convert( - VoxelizedSection.createEmpty().setPosition(chunk.getPos().x, i, chunk.getPos().z), - this.world.getMapper(), - section.getBlockStateContainer(), - section.getBiomeContainer(), - (x, y, z, state) -> { - if (lighting == null || ((lighting.first() != null && lighting.first().isUninitialized())&&(lighting.second()!=null&&lighting.second().isUninitialized()))) { - return (byte) 0x0f; - } else { - //Lighting is a piece of shit cause its done per face - int block = lighting.first()!=null?Math.min(15,lighting.first().get(x, y, z)):0; - int sky = lighting.second()!=null?Math.min(15,lighting.second().get(x, y, z)):0; - if (block { + if (lighting == null || ((lighting.first() != null && lighting.first().isUninitialized())&&(lighting.second()!=null&&lighting.second().isUninitialized()))) { + return (byte) 0x0f; + } else { + //Lighting is a piece of shit cause its done per face + int block = lighting.first()!=null?Math.min(15,lighting.first().get(x, y, z)):0; + int sky = lighting.second()!=null?Math.min(15,lighting.second().get(x, y, z)):0; + if (blockMinecraftClient.getInstance().player.sendMessage(Text.literal("Voxy ingester had an exception while executing please check logs and report error"))); + sky = 15-sky;//This is cause sky light is inverted which saves memory when saving empty sections + return (byte) (sky|(block<<4)); + } + } + ); + WorldConversionFactory.mipSection(csec, this.world.getMapper()); + this.world.insertUpdate(csec); } } } @@ -118,41 +99,14 @@ public class VoxelIngestService { public void enqueueIngest(WorldChunk chunk) { fetchLightingData(this.captureLightMap, chunk); this.ingestQueue.add(chunk); - this.ingestCounter.release(); + this.threads.execute(); } public int getTaskCount() { - return this.ingestCounter.availablePermits(); + return this.threads.getJobCount(); } public void shutdown() { - boolean anyAlive = false; - boolean allAlive = true; - for (var worker : this.workers) { - anyAlive |= worker.isAlive(); - allAlive &= worker.isAlive(); - } - - if (!anyAlive) { - System.err.println("Ingest workers already dead on shutdown! this is very very bad, check log for errors from this thread"); - return; - } - if (!allAlive) { - System.err.println("Some ingest workers already dead on shutdown! this is very very bad, check log for errors from this thread"); - } - - //Wait for the ingest to finish - while (this.ingestCounter.availablePermits() != 0) { - Thread.onSpinWait(); - } - //Shutdown - this.running = false; - this.ingestCounter.release(1000); - //Wait for thread to join - try { - for (var worker : this.workers) { - worker.join(); - } - } catch (InterruptedException e) {throw new RuntimeException(e);} + this.threads.shutdown(); } } diff --git a/src/main/java/me/cortex/voxy/common/world/thread/ServiceSlice.java b/src/main/java/me/cortex/voxy/common/world/thread/ServiceSlice.java new file mode 100644 index 00000000..f040c366 --- /dev/null +++ b/src/main/java/me/cortex/voxy/common/world/thread/ServiceSlice.java @@ -0,0 +1,102 @@ +package me.cortex.voxy.common.world.thread; + +import me.cortex.voxy.common.util.TrackedObject; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + +public class ServiceSlice extends TrackedObject { + private final String name; + final int weightPerJob; + private volatile boolean alive = true; + private final ServiceThreadPool threadPool; + private final Supplier workerGenerator; + final Semaphore jobCount = new Semaphore(0); + private final Runnable[] runningCtxs; + private final AtomicInteger activeCount = new AtomicInteger(); + + ServiceSlice(ServiceThreadPool threadPool, Supplier workerGenerator, String name, int weightPerJob) { + this.threadPool = threadPool; + this.runningCtxs = new Runnable[threadPool.getThreadCount()]; + this.workerGenerator = workerGenerator; + this.name = name; + this.weightPerJob = weightPerJob; + } + + boolean doRun(int threadIndex) { + //Run this thread once if possible + if (!this.jobCount.tryAcquire()) { + return false; + } + + if (!this.alive) { + return true;//Return true because we have "consumed" the job (needed to keep weight tracking correct) + } + + this.activeCount.incrementAndGet(); + + //Check that we are still alive + if (!this.alive) { + if (this.activeCount.decrementAndGet() < 0) { + throw new IllegalStateException("Alive count negative!"); + } + return true; + } + + //If the running context is null, create and set it + var ctx = this.runningCtxs[threadIndex]; + if (ctx == null) { + ctx = this.workerGenerator.get(); + this.runningCtxs[threadIndex] = ctx; + } + + //Run the job + try { + ctx.run(); + } catch (Exception e) { + System.err.println("Unexpected error occurred while executing a service job, expect things to break badly"); + e.printStackTrace(); + MinecraftClient.getInstance().execute(()->MinecraftClient.getInstance().player.sendMessage(Text.literal("A voxy service had an exception while executing please check logs and report error"))); + } finally { + if (this.activeCount.decrementAndGet() < 0) { + throw new IllegalStateException("Alive count negative!"); + } + } + return true; + } + + //Tells the system that a single instance of this service needs executing + public void execute() { + if (!this.alive) { + throw new IllegalStateException("Tried to do work on a dead service"); + } + this.threadPool.execute(this); + } + + public void shutdown() { + this.alive = false; + + //Wait till all is finished + while (this.activeCount.get() != 0) { + Thread.onSpinWait(); + } + + //Tell parent to remove + this.threadPool.removeService(this); + + super.free0(); + } + + @Override + public void free() { + this.shutdown(); + } + + public int getJobCount() { + return this.jobCount.availablePermits(); + } +} diff --git a/src/main/java/me/cortex/voxy/common/world/thread/ServiceThreadPool.java b/src/main/java/me/cortex/voxy/common/world/thread/ServiceThreadPool.java new file mode 100644 index 00000000..f9578298 --- /dev/null +++ b/src/main/java/me/cortex/voxy/common/world/thread/ServiceThreadPool.java @@ -0,0 +1,146 @@ +package me.cortex.voxy.common.world.thread; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; + + +//TODO: could also probably replace all of this with just VirtualThreads and a Executors.newThreadPerTaskExecutor with a fixed thread pool +// it is probably better anyway +public class ServiceThreadPool { + private volatile boolean running = true; + private final Thread[] workers; + private final Semaphore jobCounter = new Semaphore(0); + + private volatile ServiceSlice[] serviceSlices = new ServiceSlice[0]; + private final AtomicLong totalJobWeight = new AtomicLong(); + + public ServiceThreadPool(int workers) { + this.workers = new Thread[workers]; + for (int i = 0; i < workers; i++) { + int threadId = i; + var worker = new Thread(()->this.worker(threadId)); + worker.setDaemon(false); + worker.setName("Service worker #" + i); + worker.start(); + worker.setUncaughtExceptionHandler(this::handleUncaughtException); + this.workers[i] = worker; + } + } + + public synchronized ServiceSlice createService(String name, int weight, Supplier workGenerator) { + var current = this.serviceSlices; + var newList = new ServiceSlice[current.length + 1]; + System.arraycopy(current, 0, newList, 0, current.length); + var service = new ServiceSlice(this, workGenerator, name, weight); + newList[current.length] = service; + this.serviceSlices = newList; + return service; + } + + synchronized void removeService(ServiceSlice service) { + this.removeServiceFromArray(service); + this.totalJobWeight.addAndGet(-((long) service.weightPerJob) * service.jobCount.availablePermits()); + } + + private synchronized void removeServiceFromArray(ServiceSlice service) { + var lst = this.serviceSlices; + int idx; + for (idx = 0; idx < lst.length; idx++) { + if (lst[idx] == service) { + break; + } + } + if (idx == lst.length) { + throw new IllegalStateException("Service not in service list"); + } + + //Remove the slice from the array and set it back + + if (lst.length-1 == 0) { + this.serviceSlices = new ServiceSlice[0]; + return; + } + + ServiceSlice[] newArr = new ServiceSlice[lst.length-1]; + System.arraycopy(lst, 0, newArr, 0, idx); + if (lst.length-1 != idx) { + //Need to do a second copy + System.arraycopy(lst, idx+1, newArr, idx, newArr.length-idx); + } + this.serviceSlices = newArr; + } + + void execute(ServiceSlice service) { + this.totalJobWeight.addAndGet(service.weightPerJob); + this.jobCounter.release(1); + } + + private void worker(int threadId) { + long seed = 1234342; + while (true) { + seed = (seed ^ seed >>> 30) * -4658895280553007687L; + seed = (seed ^ seed >>> 27) * -7723592293110705685L; + long clamped = seed&((1L<<63)-1); + this.jobCounter.acquireUninterruptibly(); + if (!this.running) { + break; + } + + while (true) { + var ref = this.serviceSlices; + long chosenNumber = clamped % this.totalJobWeight.get(); + ServiceSlice service = ref[(int) (clamped % ref.length)]; + for (var slice : ref) { + chosenNumber -= ((long) slice.weightPerJob) * slice.jobCount.availablePermits(); + if (chosenNumber <= 0) { + service = slice; + } + } + + //Run the job + if (!service.doRun(threadId)) { + //Didnt consume the job, find a new job + continue; + } + //Consumed a job from the service, decrease weight by the amount + if (this.totalJobWeight.addAndGet(-service.weightPerJob)<0) { + throw new IllegalStateException("Total job weight is negative"); + } + break; + } + } + } + + private void handleUncaughtException(Thread thread, Throwable throwable) { + System.err.println("Service worker thread has exploded unexpectedly! this is really not good very very bad."); + throwable.printStackTrace(); + } + + public void shutdown() { + if (this.serviceSlices.length != 0) { + throw new IllegalStateException("All service slices must be shutdown before thread pool can exit"); + } + + //Wait for the tasks to finish + while (this.jobCounter.availablePermits() != 0) { + Thread.onSpinWait(); + } + + //Shutdown + this.running = false; + this.jobCounter.release(1000); + + //Wait for thread to join + try { + for (var worker : this.workers) { + worker.join(); + } + } catch (InterruptedException e) {throw new RuntimeException(e);} + } + + public int getThreadCount() { + return this.workers.length; + } +} diff --git a/src/main/resources/assets/voxy/lang/en_us.json b/src/main/resources/assets/voxy/lang/en_us.json index 5de6eec3..935c2142 100644 --- a/src/main/resources/assets/voxy/lang/en_us.json +++ b/src/main/resources/assets/voxy/lang/en_us.json @@ -10,7 +10,7 @@ "voxy.config.general.ingest": "Chunk Ingest", "voxy.config.general.ingest.tooltip": "Enables or disables voxies ability to convert new chunks into LoDs", "voxy.config.general.quality": "LoD Quality", - "voxy.config.general.quality.tooltip": "How far each LoD ring lasts before its downgraded to a lower detail level", + "voxy.config.general.quality.tooltip": "How big of an area a section should be on screen before it subdivides (pixels^2)", "voxy.config.general.geometryBuffer": "Geometry Buffer Quads", "voxy.config.general.geometryBuffer.tooltip": "How many quads the geometry buffer can hold", "voxy.config.general.maxSections": "Max Sections",