From 761b9cf7487d9a988b535fd1091dd8c2bbce5207 Mon Sep 17 00:00:00 2001 From: roleg Date: Sun, 14 Jun 2026 04:43:08 +0300 Subject: [PATCH] Add configurable LOD thread limits for load, ingest, mesh, and save Expose per-task CPU parallelism controls in Sodium settings so users can cap how many cores Voxy uses for each LOD pipeline stage instead of saturating all available threads. --- .../voxy/client/VoxyClientInstance.java | 11 ++++ .../cortex/voxy/client/config/VoxyConfig.java | 24 +++++++ .../client/config/VoxyConfigPageSodium.java | 64 ++++++++++++++++++- .../voxy/client/core/VoxyRenderSystem.java | 6 ++ .../building/RenderGenerationService.java | 5 ++ .../mixin/minecraft/MixinLevelRenderer.java | 1 + .../me/cortex/voxy/common/thread/Service.java | 41 +++++++++++- .../voxy/common/thread/ServiceManager.java | 4 +- .../common/world/ActiveSectionTracker.java | 7 +- .../voxy/common/world/SectionLoadLimiter.java | 34 ++++++++++ .../world/service/SectionSavingService.java | 5 ++ .../world/service/VoxelIngestService.java | 5 ++ .../cortex/voxy/commonImpl/VoxyInstance.java | 7 ++ .../resources/assets/voxy/lang/en_us.json | 14 +++- .../resources/assets/voxy/lang/ru_ru.json | 48 ++++++++++++++ 15 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 src/main/java/me/cortex/voxy/common/world/SectionLoadLimiter.java create mode 100644 src/main/resources/assets/voxy/lang/ru_ru.json diff --git a/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java b/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java index d141e3036..65125cf8e 100644 --- a/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java +++ b/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java @@ -2,6 +2,7 @@ import me.cortex.voxy.client.compat.FlashbackCompat; import me.cortex.voxy.client.config.VoxyConfig; +import me.cortex.voxy.client.core.IGetVoxyRenderSystem; import me.cortex.voxy.client.mixin.sodium.AccessorSodiumWorldRenderer; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.StorageConfigUtil; @@ -38,6 +39,16 @@ public VoxyClientInstance() { this.basePath = path; this.storageConfig = StorageConfigUtil.getCreateStorageConfig(Config.class, c->c.version==1&&c.sectionStorageConfig!=null, ()->DEFAULT_STORAGE_CONFIG, path).sectionStorageConfig; this.updateDedicatedThreads(); + this.updateLodThreadLimits(); + } + + public void updateLodThreadLimits() { + var config = VoxyConfig.CONFIG; + super.updateLodThreadLimits(config.lodIngestThreads, config.lodSaveThreads, config.lodLoadThreads); + var renderSystem = IGetVoxyRenderSystem.getNullable(); + if (renderSystem != null) { + renderSystem.updateLodThreadLimits(); + } } @Override 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 74194ef29..06637f96b 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java @@ -29,6 +29,10 @@ public class VoxyConfig implements OptionStorage { public boolean ingestEnabled = true; public int sectionRenderDistance = 16; public int serviceThreads = (int) Math.max(CpuLayout.getCoreCount()/1.5, 1); + public int lodLoadThreads = Math.max(1, CpuLayout.getCoreCount() / 4); + public int lodIngestThreads = Math.max(1, CpuLayout.getCoreCount() / 4); + public int lodMeshThreads = Math.max(1, CpuLayout.getCoreCount() / 4); + public int lodSaveThreads = 1; public float subDivisionSize = 64; public boolean useEnvironmentalFog = true; public boolean dontUseSodiumBuilderThreads = false; @@ -40,6 +44,7 @@ private static VoxyConfig loadOrCreate() { try (FileReader reader = new FileReader(path.toFile())) { var conf = GSON.fromJson(reader, VoxyConfig.class); if (conf != null) { + conf.applyDefaults(); conf.save(); return conf; } else { @@ -88,4 +93,23 @@ private static Path getConfigPath() { public boolean isRenderingEnabled() { return VoxyCommon.isAvailable() && this.enabled && this.enableRendering; } + + private void applyDefaults() { + int cores = CpuLayout.getCoreCount(); + if (this.serviceThreads <= 0) { + this.serviceThreads = (int) Math.max(cores / 1.5, 1); + } + if (this.lodLoadThreads <= 0) { + this.lodLoadThreads = Math.max(1, cores / 4); + } + if (this.lodIngestThreads <= 0) { + this.lodIngestThreads = Math.max(1, cores / 4); + } + if (this.lodMeshThreads <= 0) { + this.lodMeshThreads = Math.max(1, cores / 4); + } + if (this.lodSaveThreads <= 0) { + this.lodSaveThreads = 1; + } + } } diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfigPageSodium.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfigPageSodium.java index 258ca70be..a78909d1b 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfigPageSodium.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfigPageSodium.java @@ -1,8 +1,8 @@ package me.cortex.voxy.client.config; import com.google.common.collect.ImmutableList; -import me.cortex.voxy.client.RenderStatistics; import me.cortex.voxy.client.VoxyClientInstance; +import me.cortex.voxy.client.RenderStatistics; import me.cortex.voxy.client.core.IGetVoxyRenderSystem; import me.cortex.voxy.common.util.cpu.CpuLayout; import me.cortex.voxy.commonImpl.VoxyCommon; @@ -19,6 +19,21 @@ private VoxyConfigPageSodium(){} public static OptionPage voxyOptionPage = null; + private static int maxThreadSlider() { + if (CpuLayout.CORES != null) { + return CpuLayout.CORES.length; + } + return Runtime.getRuntime().availableProcessors(); + } + + private static void applyLodThreadLimits() { + var instance = VoxyCommon.getInstance(); + if (instance instanceof VoxyClientInstance clientInstance) { + clientInstance.updateLodThreadLimits(); + } + VoxyConfig.CONFIG.save(); + } + public static OptionPage page() { List groups = new ArrayList<>(); VoxyConfig storage = VoxyConfig.CONFIG; @@ -57,8 +72,7 @@ public static OptionPage page() { .setName(Component.translatable("voxy.config.general.serviceThreads")) .setTooltip(Component.translatable("voxy.config.general.serviceThreads.tooltip")) .setControl(opt->new SliderControl(opt, 1, - CpuLayout.CORES.length, //Just do core size as max - //Runtime.getRuntime().availableProcessors(),//Note: this is threads not cores, the default value is half the core count, is fine as this should technically be the limit but CpuLayout.CORES.length is more realistic + maxThreadSlider(), 1, v->Component.literal(Integer.toString(v)))) .setBinding((s, v)->{ s.serviceThreads = v; @@ -93,6 +107,50 @@ public static OptionPage page() { ).build() ); + groups.add(OptionGroup.createBuilder() + .add(OptionImpl.createBuilder(int.class, storage) + .setName(Component.translatable("voxy.config.lod.loadThreads")) + .setTooltip(Component.translatable("voxy.config.lod.loadThreads.tooltip")) + .setControl(opt->new SliderControl(opt, 1, maxThreadSlider(), 1, v->Component.literal(Integer.toString(v)))) + .setBinding((s, v)->{ + s.lodLoadThreads = v; + applyLodThreadLimits(); + }, s -> s.lodLoadThreads) + .setImpact(OptionImpact.HIGH) + .build() + ).add(OptionImpl.createBuilder(int.class, storage) + .setName(Component.translatable("voxy.config.lod.ingestThreads")) + .setTooltip(Component.translatable("voxy.config.lod.ingestThreads.tooltip")) + .setControl(opt->new SliderControl(opt, 1, maxThreadSlider(), 1, v->Component.literal(Integer.toString(v)))) + .setBinding((s, v)->{ + s.lodIngestThreads = v; + applyLodThreadLimits(); + }, s -> s.lodIngestThreads) + .setImpact(OptionImpact.HIGH) + .build() + ).add(OptionImpl.createBuilder(int.class, storage) + .setName(Component.translatable("voxy.config.lod.meshThreads")) + .setTooltip(Component.translatable("voxy.config.lod.meshThreads.tooltip")) + .setControl(opt->new SliderControl(opt, 1, maxThreadSlider(), 1, v->Component.literal(Integer.toString(v)))) + .setBinding((s, v)->{ + s.lodMeshThreads = v; + applyLodThreadLimits(); + }, s -> s.lodMeshThreads) + .setImpact(OptionImpact.HIGH) + .build() + ).add(OptionImpl.createBuilder(int.class, storage) + .setName(Component.translatable("voxy.config.lod.saveThreads")) + .setTooltip(Component.translatable("voxy.config.lod.saveThreads.tooltip")) + .setControl(opt->new SliderControl(opt, 1, maxThreadSlider(), 1, v->Component.literal(Integer.toString(v)))) + .setBinding((s, v)->{ + s.lodSaveThreads = v; + applyLodThreadLimits(); + }, s -> s.lodSaveThreads) + .setImpact(OptionImpact.MEDIUM) + .build() + ).build() + ); + groups.add(OptionGroup.createBuilder() .add(OptionImpl.createBuilder(boolean.class, storage) .setName(Component.translatable("voxy.config.general.rendering")) diff --git a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java index 8d4b536e7..023e124a2 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxyRenderSystem.java @@ -143,6 +143,8 @@ public VoxyRenderSystem(WorldEngine world, ServiceManager sm) { this.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance); } + this.updateLodThreadLimits(); + this.chunkBoundRenderer = new ChunkBoundRenderer(this.pipeline); Logger.info("Voxy render system created with " + geometryCapacity + " geometry capacity, using pipeline '" + this.pipeline.getClass().getSimpleName() + "' with renderer '" + sectionRenderer.getClass().getSimpleName() + "'"); @@ -163,6 +165,10 @@ public VoxyRenderSystem(WorldEngine world, ServiceManager sm) { } + public void updateLodThreadLimits() { + this.renderGen.applyThreadLimit(VoxyConfig.CONFIG.lodMeshThreads); + } + public Viewport setupViewport(ChunkRenderMatrices matrices, FogParameters fogParameters, double cameraX, double cameraY, double cameraZ) { var viewport = this.getViewport(); if (viewport == null) { 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 01407b8b7..71d5d900b 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 @@ -366,4 +366,9 @@ public void addDebugData(List debug) { public int getTaskCount() { return this.taskQueueCount.get(); } + + public void applyThreadLimit(int threads) { + this.service.setMaxConcurrent(threads); + this.service.setWeight(Math.max(1, threads)); + } } diff --git a/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java index c4fac2814..502caf43c 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java +++ b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java @@ -92,5 +92,6 @@ public void createRenderer() { } } instance.updateDedicatedThreads(); + instance.updateLodThreadLimits(); } } diff --git a/src/main/java/me/cortex/voxy/common/thread/Service.java b/src/main/java/me/cortex/voxy/common/thread/Service.java index e8cc2cb9b..fd876af20 100644 --- a/src/main/java/me/cortex/voxy/common/thread/Service.java +++ b/src/main/java/me/cortex/voxy/common/thread/Service.java @@ -4,17 +4,20 @@ import me.cortex.voxy.common.util.Pair; import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; import java.util.function.Supplier; public class Service { private final PerThreadContextExecutor executor; private final ServiceManager sm; - final long weight; + volatile long weight; final String name; final BooleanSupplier limiter; private final Semaphore tasks = new Semaphore(0); + private final AtomicInteger activeJobs = new AtomicInteger(); + private volatile int maxConcurrent = Integer.MAX_VALUE; private volatile boolean isLive = true; private volatile boolean isStopping = false; @@ -27,6 +30,29 @@ public class Service { this.executor = new PerThreadContextExecutor(ctxSupplier, e->sm.handleException(this, e)); } + public void setMaxConcurrent(int max) { + this.maxConcurrent = Math.max(1, max); + } + + public void setWeight(long weight) { + this.weight = Math.max(1, weight); + } + + public int getActiveJobs() { + return this.activeJobs.get(); + } + + public int getMaxConcurrent() { + return this.maxConcurrent; + } + + boolean canAcceptJobs() { + if (this.activeJobs.get() >= this.maxConcurrent) { + return false; + } + return this.limiter == null || this.limiter.getAsBoolean(); + } + public void execute() { if (this.isStopping) { Logger.error("Tried executing on a dead service"); @@ -44,8 +70,17 @@ boolean runJob() { //Failed to get the job, probably due to a race condition return false; } - if (!this.executor.run()) {//Run the job - throw new IllegalStateException("Executor failed to run"); + if (!this.canAcceptJobs()) { + this.tasks.release(); + return false; + } + this.activeJobs.incrementAndGet(); + try { + if (!this.executor.run()) {//Run the job + throw new IllegalStateException("Executor failed to run"); + } + } finally { + this.activeJobs.decrementAndGet(); } return true; } diff --git a/src/main/java/me/cortex/voxy/common/thread/ServiceManager.java b/src/main/java/me/cortex/voxy/common/thread/ServiceManager.java index 454d8c70f..fc9573e2e 100644 --- a/src/main/java/me/cortex/voxy/common/thread/ServiceManager.java +++ b/src/main/java/me/cortex/voxy/common/thread/ServiceManager.java @@ -83,7 +83,7 @@ private int runAJob0() {//Executes a single job on the current thread continue outer;//We need to refetch the array and start over } boolean sc = c--<=0; - if (service.limiter!=null && !service.limiter.getAsBoolean()) { + if (!service.canAcceptJobs()) { skipMsk |= 1L<= maxConcurrent) { + Thread.onSpinWait(); + Thread.yield(); + } + active.incrementAndGet(); + } + + public static void release() { + active.decrementAndGet(); + } +} 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 5c0bc66e3..6602b2561 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 @@ -88,4 +88,9 @@ public void shutdown() { public int getTaskCount() { return this.service.numJobs(); } + + public void applyThreadLimit(int threads) { + this.service.setMaxConcurrent(threads); + this.service.setWeight(Math.max(1, threads)); + } } 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 840033e18..efbafda52 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 @@ -175,6 +175,11 @@ public int getTaskCount() { return this.service.numJobs(); } + public void applyThreadLimit(int threads) { + this.service.setMaxConcurrent(threads); + this.service.setWeight(Math.max(1, threads)); + } + public void shutdown() { this.service.shutdown(); } diff --git a/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java b/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java index fa5ecbb0b..7e2a82366 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java +++ b/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java @@ -5,6 +5,7 @@ import me.cortex.voxy.common.thread.ServiceManager; import me.cortex.voxy.common.thread.UnifiedServiceThreadPool; import me.cortex.voxy.common.util.MemoryBuffer; +import me.cortex.voxy.common.world.SectionLoadLimiter; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.service.SectionSavingService; import me.cortex.voxy.common.world.service.VoxelIngestService; @@ -68,6 +69,12 @@ public void updateDedicatedThreads() { this.setNumThreads(3); } + public void updateLodThreadLimits(int ingestThreads, int saveThreads, int loadThreads) { + this.ingestService.applyThreadLimit(ingestThreads); + this.savingService.applyThreadLimit(saveThreads); + SectionLoadLimiter.setMaxConcurrent(loadThreads); + } + protected ImportManager createImportManager() { return new ImportManager(); } diff --git a/src/main/resources/assets/voxy/lang/en_us.json b/src/main/resources/assets/voxy/lang/en_us.json index 17b440b64..36a4479de 100644 --- a/src/main/resources/assets/voxy/lang/en_us.json +++ b/src/main/resources/assets/voxy/lang/en_us.json @@ -8,7 +8,19 @@ "voxy.config.general.enabled.tooltip": "Fully enables or disables voxy", "voxy.config.general.serviceThreads": "Service threads", - "voxy.config.general.serviceThreads.tooltip": "Number of threads the ServiceThreadPool can use", + "voxy.config.general.serviceThreads.tooltip": "Total number of worker threads in the Voxy thread pool. Use LOD thread settings below to limit how many threads each task type can use at once.", + + "voxy.config.lod.loadThreads": "LOD load threads", + "voxy.config.lod.loadThreads.tooltip": "Maximum number of LOD sections that can be loaded from storage at the same time", + + "voxy.config.lod.ingestThreads": "LOD ingest threads", + "voxy.config.lod.ingestThreads.tooltip": "Maximum number of parallel chunk-to-LOD conversion jobs", + + "voxy.config.lod.meshThreads": "LOD mesh threads", + "voxy.config.lod.meshThreads.tooltip": "Maximum number of parallel LOD mesh generation jobs for rendering", + + "voxy.config.lod.saveThreads": "LOD save threads", + "voxy.config.lod.saveThreads.tooltip": "Maximum number of parallel LOD section save jobs to storage", "voxy.config.general.useSodiumBuilder": "Use sodium threads", "voxy.config.general.useSodiumBuilder.tooltip": "Uses sodium builder threads as part of voxys thread pool, can reduce stuttering and lag when moving quickly at high render distance", diff --git a/src/main/resources/assets/voxy/lang/ru_ru.json b/src/main/resources/assets/voxy/lang/ru_ru.json new file mode 100644 index 000000000..e016fafa5 --- /dev/null +++ b/src/main/resources/assets/voxy/lang/ru_ru.json @@ -0,0 +1,48 @@ +{ + "voxy.config.title": "Voxy", + + "voxy.config.general": "Общие", + "voxy.config.rendering": "Рендеринг", + + "voxy.config.general.enabled": "Включить Voxy", + "voxy.config.general.enabled.tooltip": "Полностью включает или отключает Voxy", + + "voxy.config.general.serviceThreads": "Рабочие потоки", + "voxy.config.general.serviceThreads.tooltip": "Общее количество рабочих потоков пула Voxy. Ниже можно ограничить, сколько потоков одновременно использует каждый тип задач LOD.", + + "voxy.config.lod.loadThreads": "Потоки загрузки LOD", + "voxy.config.lod.loadThreads.tooltip": "Максимальное число одновременных загрузок секций LOD из хранилища", + + "voxy.config.lod.ingestThreads": "Потоки инжеста LOD", + "voxy.config.lod.ingestThreads.tooltip": "Максимальное число параллельных задач конвертации чанков в LOD", + + "voxy.config.lod.meshThreads": "Потоки мешинга LOD", + "voxy.config.lod.meshThreads.tooltip": "Максимальное число параллельных задач генерации мешей LOD для рендеринга", + + "voxy.config.lod.saveThreads": "Потоки сохранения LOD", + "voxy.config.lod.saveThreads.tooltip": "Максимальное число параллельных задач сохранения секций LOD в хранилище", + + "voxy.config.general.useSodiumBuilder": "Использовать потоки Sodium", + "voxy.config.general.useSodiumBuilder.tooltip": "Использует потоки сборщика Sodium как часть пула потоков Voxy — может уменьшить фризы при быстром движении на большой дистанции прорисовки", + + "voxy.config.general.ingest": "Инжест чанков", + "voxy.config.general.ingest.tooltip": "Включает или отключает конвертацию новых чанков в LOD", + + "voxy.config.general.rendering": "Рендеринг Voxy", + "voxy.config.general.rendering.tooltip": "Включает или отключает рендеринг Voxy", + + "voxy.config.general.subDivisionSize": "Размер подразделения (пикс²)", + "voxy.config.general.subDivisionSize.tooltip": "Максимальный размер AABB в экранном пространстве (в пикселях²) перед переходом к более мелким LOD (меньше — выше качество)", + + "voxy.config.general.renderDistance": "Дистанция прорисовки", + "voxy.config.general.renderDistance.tooltip": "Дистанция прорисовки Voxy в чанках", + + "voxy.config.general.environmental_fog": "Включить средовой туман", + "voxy.config.general.environmental_fog.tooltip": "Включает или отключает рендеринг средового тумана Voxy", + + "voxy.config.general.render_fog": "Включить туман рендера", + "voxy.config.general.render_fog.tooltip": "Включает или отключает эффект тумана рендера", + + "voxy.config.general.render_statistics": "Статистика рендера", + "voxy.config.general.render_statistics.tooltip": "Показывать статистику рендера в меню F3 — полезно для отладки" +}