diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java index bc5c44ad..981ea088 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gui/SodiumGameOptions.java @@ -32,6 +32,16 @@ public class SodiumGameOptions implements SpeedrunConfig { public final SpeedrunSettings speedrun = new SpeedrunSettings(); public static class AdvancedSettings implements SpeedrunConfigStorage { + @Config.Numbers.Whole.Bounds(min = 0, max = 64) + public int maxChunkThreads; + @Config.Numbers.Whole.Bounds(min = 0, max = 64) + public int targetChunkThreads; + @Config.Numbers.Whole.Bounds(min = 0, max = 64) + public int initialChunkThreads; + @Config.Numbers.Whole.Bounds(min = 1, max = 2000) + public int quickThreadCreationInterval = 100; + @Config.Numbers.Whole.Bounds(min = 1, max = 10000) + public int slowThreadCreationInterval = 1000; public ChunkRendererBackendOption chunkRendererBackend = ChunkRendererBackendOption.BEST; public boolean useChunkFaceCulling = true; public boolean useCompactVertexFormat = true; diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java b/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java index 334b3929..cb3935ad 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/SodiumWorldRenderer.java @@ -426,4 +426,8 @@ public ChunkRenderBackend getChunkRenderer() { public boolean getUseEntityCulling() { return this.useEntityCulling; } + + public int getRenderDistance() { + return this.renderDistance; + } } diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java index 37eb65bc..12c3d757 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/ChunkRenderManager.java @@ -462,6 +462,8 @@ public void updateChunks() { if (!futures.isEmpty()) { this.backend.upload(new FutureDequeDrain<>(futures)); } + + this.builder.createMoreThreads(); } public void markDirty() { diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java index 069ebce5..c98e94c7 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java @@ -1,6 +1,8 @@ package me.jellysquid.mods.sodium.client.render.chunk.compile; +import me.jellysquid.mods.sodium.client.SodiumClientMod; import me.jellysquid.mods.sodium.client.gl.attribute.GlVertexFormat; +import me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer; import me.jellysquid.mods.sodium.client.render.chunk.ChunkGraphicsState; import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderBackend; import me.jellysquid.mods.sodium.client.render.chunk.ChunkRenderContainer; @@ -17,6 +19,7 @@ import me.jellysquid.mods.sodium.common.util.pool.ObjectPool; import net.minecraft.client.MinecraftClient; import net.minecraft.client.util.math.Vector3d; +import net.minecraft.util.math.MathHelper; import net.minecraft.client.world.ClientWorld; import net.minecraft.util.math.ChunkSectionPos; import net.minecraft.world.World; @@ -54,23 +57,90 @@ public class ChunkBuilder { private BiomeCacheManager biomeCacheManager; private BlockRenderPassManager renderPassManager; - private final int limitThreads; + // This is the initial number of threads we spin up upon ChunkBuilder creation. + // Default: 2-4 depending on render distance. *Always* less than hardLimitThreads. + private final int initialThreads; + // This is the number of threads we want to 'quickly' get up to. This is calculated + // by getOptimalThreadCount, but can also be configured by the user. Always less than hardLimitThreads. + private final int targetThreads; + // This is the number of threads we are allowed to create in total. + // This is defaulted to targetThreads, but maxes out at 64. Testing would be required to determine what + // actual count is optimal for a specific user and use case. + private final int hardLimitThreads; + // This is the initial time when this builder is created. We use this to create more threads. + private long lastThreadAddition; + // This is the user-configurable time delta to create a new thread, up until targetThreads. + private final long quickThreadCreationInterval; + // This is the user-configurable time delta to create a new thread, up until hardLimitThreads. + private final long slowThreadCreationInterval; + private final GlVertexFormat format; private final ChunkRenderBackend backend; public ChunkBuilder(GlVertexFormat format, ChunkRenderBackend backend) { this.format = format; this.backend = backend; - this.limitThreads = getOptimalThreadCount(); + + // User-configurable options for chunk threads. + int desiredTargetThreads = SodiumClientMod.options().advanced.targetChunkThreads; + int desiredInitialThreads = SodiumClientMod.options().advanced.initialChunkThreads; + + // These are bounded by the options configuration. Both are measured in milliseconds. + this.quickThreadCreationInterval = SodiumClientMod.options().advanced.quickThreadCreationInterval; + this.slowThreadCreationInterval = SodiumClientMod.options().advanced.slowThreadCreationInterval; + + // Our hard limit of threads. Cap user config at 64, prefer desiredMaxThreads, otherwise use logical core count. + this.hardLimitThreads = getMaxThreadCount(); + // Our targeted number of threads. + this.targetThreads = Math.min(desiredTargetThreads == 0 ? getDefaultTargetThreads() : desiredTargetThreads, this.hardLimitThreads); + // Our initial threads. A bit of a silly calculation for this one. + this.initialThreads = Math.min(desiredInitialThreads == 0 ? getDefaultInitialThreads() : desiredInitialThreads, this.targetThreads); this.pool = new ObjectPool<>(this.getSchedulingBudget(), WorldSlice::new); } + private static int getDefaultTargetThreads() { + return MathHelper.clamp(Math.max(getLogicalCoreCount() / 3, getLogicalCoreCount() - 6), 1, 10); + } + + private static int getDefaultInitialThreads() { + return (SodiumWorldRenderer.getInstance().getRenderDistance() / 10) + 2; + } + + private static int getLogicalCoreCount() { + return Runtime.getRuntime().availableProcessors(); + } + + // Split out this function so that SeedQueue can inject into this. + private static int getMaxThreadCount() { + int desiredMaxThreads = SodiumClientMod.options().advanced.maxChunkThreads; + return desiredMaxThreads == 0 ? getLogicalCoreCount() : desiredMaxThreads; + } + /** * Returns the remaining number of build tasks which should be scheduled this frame. If an attempt is made to * spawn more tasks than the budget allows, it will block until resources become available. */ public int getSchedulingBudget() { - return Math.max(0, (this.limitThreads * TASK_QUEUE_LIMIT_PER_WORKER) - this.buildQueue.size()); + return Math.max(0, (Math.max(this.threads.size(), this.targetThreads) * TASK_QUEUE_LIMIT_PER_WORKER) - this.buildQueue.size()); + } + + public void createWorker(MinecraftClient client) { + ChunkBuildBuffers buffers = new ChunkBuildBuffers(this.format, this.renderPassManager); + ChunkRenderContext pipeline = new ChunkRenderContext(client); + + WorkerRunnable worker = new WorkerRunnable(buffers, pipeline); + + Thread thread = new Thread(worker, "Chunk Render Task Executor #" + this.threads.size() + 1); + thread.setPriority(Math.max(0, Thread.NORM_PRIORITY - 2)); + thread.start(); + + this.threads.add(thread); + + // Helper debug message. Prints at most once per reload, so shouldn't noticeably increase log spam. + if (this.threads.size() == this.hardLimitThreads) { + LOGGER.info("Reached maximum Sodium builder threads of {}", this.hardLimitThreads); + } + this.lastThreadAddition = System.currentTimeMillis(); } /** @@ -87,21 +157,32 @@ public void startWorkers() { } MinecraftClient client = MinecraftClient.getInstance(); + for (int i = 0; i < this.initialThreads; i++) { + this.createWorker(client); + } - for (int i = 0; i < this.limitThreads; i++) { - ChunkBuildBuffers buffers = new ChunkBuildBuffers(this.format, this.renderPassManager); - ChunkRenderContext pipeline = new ChunkRenderContext(client); - - WorkerRunnable worker = new WorkerRunnable(buffers, pipeline); - - Thread thread = new Thread(worker, "Chunk Render Task Executor #" + i); - thread.setPriority(Math.max(0, Thread.NORM_PRIORITY - 2)); - thread.start(); + LOGGER.info("Started {} worker threads", this.threads.size()); + } - this.threads.add(thread); + /** + * Spawns workers if we have thread space. + */ + public void createMoreThreads() { + if (this.threads.size() >= this.hardLimitThreads) { + return; } - LOGGER.info("Started {} worker threads", this.threads.size()); + long timeDelta = System.currentTimeMillis() - this.lastThreadAddition; + if (this.threads.size() < this.targetThreads) { + // Check if enough time has elapsed for us to create a target thread. + if (timeDelta > this.quickThreadCreationInterval) { + this.createWorker(MinecraftClient.getInstance()); + } + } + // Check if enough time has elapsed for us to create a target thread. + else if (timeDelta > this.slowThreadCreationInterval) { + this.createWorker(MinecraftClient.getInstance()); + } } /** @@ -221,14 +302,6 @@ public void init(ClientWorld world, BlockRenderPassManager renderPassManager) { this.startWorkers(); } - /** - * Returns the "optimal" number of threads to be used for chunk build tasks. This is always at least one thread, - * but can be up to the number of available processor threads on the system. - */ - private static int getOptimalThreadCount() { - return Math.max(1, Runtime.getRuntime().availableProcessors()); - } - /** * Creates a {@link WorldSlice} around the given chunk section. If the chunk section is empty, null is returned. * @param pos The position of the chunk section diff --git a/src/main/java/me/jellysquid/mods/sodium/mixin/features/chunk_rendering/MixinWorldRenderer.java b/src/main/java/me/jellysquid/mods/sodium/mixin/features/chunk_rendering/MixinWorldRenderer.java index e579ae44..e73bd3b8 100644 --- a/src/main/java/me/jellysquid/mods/sodium/mixin/features/chunk_rendering/MixinWorldRenderer.java +++ b/src/main/java/me/jellysquid/mods/sodium/mixin/features/chunk_rendering/MixinWorldRenderer.java @@ -16,6 +16,7 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -38,6 +39,12 @@ public SodiumWorldRenderer getSodiumWorldRenderer() { return renderer; } + @ModifyArg(method = "", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/objects/ObjectArrayList;(I)V")) + private int nullifyVisibleChunksList(int capacity) { + // Sodium doesn't use this list, so we prevent the initial capacity of 69696 to be allocated + return 0; + } + @Redirect(method = "reload", at = @At(value = "FIELD", target = "Lnet/minecraft/client/options/GameOptions;viewDistance:I", ordinal = 1)) private int nullifyBuiltChunkStorage(GameOptions options) { // Do not allow any resources to be allocated diff --git a/src/main/resources/assets/sodiummac/lang/en_us.json b/src/main/resources/assets/sodiummac/lang/en_us.json index 407c5cae..9a7928ab 100644 --- a/src/main/resources/assets/sodiummac/lang/en_us.json +++ b/src/main/resources/assets/sodiummac/lang/en_us.json @@ -4,6 +4,21 @@ "speedrunapi.config.sodiummac.category.speedrun": "Speedrun", "speedrunapi.config.sodiummac.option.quality:enableVignette": "Vignette", "speedrunapi.config.sodiummac.option.quality:enableVignette.description": "If enabled, a vignette effect will be rendered on the player's view. This is very unlikely to make a difference to frame rates unless you are fill-rate limited.", + "speedrunapi.config.sodium.option.advanced:initialChunkThreads": "Initial Chunk Threads", + "speedrunapi.config.sodium.option.advanced:initialChunkThreads.description": "How many chunk building threads to create instantly when reloading (lower = less F3 + F lag). Cannot be more than Target Chunk Threads.", + "speedrunapi.config.sodium.option.advanced:initialChunkThreads.value.0": "Auto", + "speedrunapi.config.sodium.option.advanced:targetChunkThreads": "Target Chunk Threads", + "speedrunapi.config.sodium.option.advanced:targetChunkThreads.description": "How many chunk building threads to create quickly after reloading (ideal: Number of hardware threads). Cannot be more than Maximum Chunk Threads.", + "speedrunapi.config.sodium.option.advanced:targetChunkThreads.value.0": "Auto", + "speedrunapi.config.sodium.option.advanced:maxChunkThreads": "Maximum Chunk Threads", + "speedrunapi.config.sodium.option.advanced:maxChunkThreads.description": "How many chunk building threads to create in total (may help long-term chunk loading)", + "speedrunapi.config.sodium.option.advanced:maxChunkThreads.value.0": "Auto", + "speedrunapi.config.sodium.option.advanced:quickThreadCreationInterval": "Fast Thread Creation Interval", + "speedrunapi.config.sodium.option.advanced:quickThreadCreationInterval.description": "Interval between each chunk builder thread being created, up to targetChunkThreads.", + "speedrunapi.config.sodium.option.advanced:quickThreadCreationInterval.value": "%sms", + "speedrunapi.config.sodium.option.advanced:slowThreadCreationInterval": "Slow Thread Creation Interval", + "speedrunapi.config.sodium.option.advanced:slowThreadCreationInterval.description": "Interval between each chunk builder thread being created, up to maxChunkThreads.", + "speedrunapi.config.sodium.option.advanced:slowThreadCreationInterval.value": "%sms", "speedrunapi.config.sodiummac.option.advanced:chunkRendererBackend": "Chunk Renderer", "speedrunapi.config.sodiummac.option.advanced:chunkRendererBackend.description": "Modern versions of OpenGL provide features which can be used to greatly reduce driver overhead when rendering chunks. You should use the latest feature set allowed by Sodium for optimal performance. If you're experiencing chunk rendering issues or driver crashes, try using the older (and possibly more stable) feature sets.", "speedrunapi.config.sodiummac.option.advanced:chunkRendererBackend.value.GL43": "Multidraw (GL 4.3)",