From 158cd780d64f643c6b0a46a3add13de9a4e5b825 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 22 Nov 2024 20:42:30 +0100 Subject: [PATCH] refactor job effort estimation, add category-based meshing task size estimation, limit upload size based on previous mesh task result size or an estimate of it, the limit behavior changes depending on which type of upload buffer is used --- .../arena/staging/FallbackStagingBuffer.java | 7 ++ .../gl/arena/staging/MappedStagingBuffer.java | 7 ++ .../gl/arena/staging/StagingBuffer.java | 2 + .../client/render/chunk/RenderSection.java | 11 +-- .../render/chunk/RenderSectionManager.java | 57 ++++++++----- .../chunk/compile/BuilderTaskOutput.java | 11 +++ .../chunk/compile/ChunkBuildOutput.java | 21 +++-- .../render/chunk/compile/ChunkSortOutput.java | 5 ++ .../estimation/CategoryFactorEstimator.java | 81 +++++++++++++++++++ .../estimation/JobDurationEstimator.java | 14 ++++ .../chunk/compile/estimation/JobEffort.java | 22 +++++ .../compile/estimation/MeshResultSize.java | 49 +++++++++++ .../estimation/MeshTaskSizeEstimator.java | 25 ++++++ .../chunk/compile/executor/ChunkBuilder.java | 11 --- .../chunk/compile/executor/ChunkJob.java | 2 + .../compile/executor/ChunkJobResult.java | 1 + .../chunk/compile/executor/ChunkJobTyped.java | 8 +- .../chunk/compile/executor/JobEffort.java | 7 -- .../compile/executor/JobEffortEstimator.java | 67 --------------- .../tasks/ChunkBuilderMeshingTask.java | 5 +- .../tasks/ChunkBuilderSortingTask.java | 3 +- .../chunk/compile/tasks/ChunkBuilderTask.java | 15 +++- .../render/chunk/region/RenderRegion.java | 11 +-- 23 files changed, 310 insertions(+), 132 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java delete mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java delete mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java index cfe9aae5a8..0230e6f82b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java @@ -8,6 +8,8 @@ import java.nio.ByteBuffer; public class FallbackStagingBuffer implements StagingBuffer { + private static final float BYTES_PER_NANO_LIMIT = 8_000_000.0f / (1_000_000_000.0f / 60.0f); // MB per frame at 60fps + private final GlMutableBuffer fallbackBufferObject; public FallbackStagingBuffer(CommandList commandList) { @@ -39,4 +41,9 @@ public void flip() { public String toString() { return "Fallback"; } + + @Override + public long getUploadSizeLimit(long frameDuration) { + return (long) (frameDuration * BYTES_PER_NANO_LIMIT); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java index 5252861cad..a7b6223204 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java @@ -15,6 +15,8 @@ import java.util.List; public class MappedStagingBuffer implements StagingBuffer { + private static final float UPLOAD_LIMIT_MARGIN = 0.8f; + private static final EnumBitField STORAGE_FLAGS = EnumBitField.of(GlBufferStorageFlags.PERSISTENT, GlBufferStorageFlags.CLIENT_STORAGE, GlBufferStorageFlags.MAP_WRITE); @@ -156,6 +158,11 @@ public void flip() { } } + @Override + public long getUploadSizeLimit(long frameDuration) { + return (long) (this.capacity * UPLOAD_LIMIT_MARGIN); + } + private static final class CopyCommand { private final GlBuffer buffer; private final long readOffset; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java index d3087e8df5..0fe71170d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java @@ -13,4 +13,6 @@ public interface StagingBuffer { void delete(CommandList commandList); void flip(); + + long getUploadSizeLimit(long frameDuration); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index 07f9cabb1e..ec98ce8cd9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirectionSet; @@ -46,7 +47,7 @@ public class RenderSection { // Pending Update State @Nullable private CancellationToken taskCancellationToken = null; - private long lastMeshingTaskEffort = 1; + private long lastMeshResultSize = MeshResultSize.NO_DATA; @Nullable private ChunkUpdateType pendingUpdateType; @@ -150,12 +151,12 @@ private void clearRenderState() { this.visibilityData = VisibilityEncoding.NULL; } - public void setLastMeshingTaskEffort(long effort) { - this.lastMeshingTaskEffort = effort; + public void setLastMeshResultSize(long size) { + this.lastMeshResultSize = size; } - public long getLastMeshingTaskEffort() { - return this.lastMeshingTaskEffort; + public long getLastMeshResultSize() { + return this.lastMeshResultSize; } /** diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index bcb6b5fd98..72181c5c09 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -10,10 +10,10 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobCollector; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.JobEffortEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.*; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; @@ -71,7 +71,8 @@ public class RenderSectionManager { private final Long2ReferenceMap sectionByPosition = new Long2ReferenceOpenHashMap<>(); private final ConcurrentLinkedDeque> buildResults = new ConcurrentLinkedDeque<>(); - private final JobEffortEstimator jobEffortEstimator = new JobEffortEstimator(); + private final JobDurationEstimator jobDurationEstimator = new JobDurationEstimator(); + private final MeshTaskSizeEstimator meshTaskSizeEstimator = new MeshTaskSizeEstimator(); private ChunkJobCollector lastBlockingCollector; private long thisFrameBlockingTasks; private long nextFrameBlockingTasks; @@ -573,7 +574,10 @@ private boolean processChunkBuildResults(ArrayList results) { TranslucentData oldData = result.render.getTranslucentData(); if (result instanceof ChunkBuildOutput chunkBuildOutput) { this.updateSectionInfo(result.render, chunkBuildOutput.info); - result.render.setLastMeshingTaskEffort(chunkBuildOutput.getEffort()); + + var resultSize = chunkBuildOutput.getResultSize(); + result.render.setLastMeshResultSize(resultSize); + this.meshTaskSizeEstimator.addBatchEntry(MeshResultSize.forSection(result.render, resultSize)); touchedSectionInfo = true; @@ -600,6 +604,8 @@ private boolean processChunkBuildResults(ArrayList results) { result.render.setLastUploadFrame(result.submitTime); } + this.meshTaskSizeEstimator.flushNewData(); + return touchedSectionInfo; } @@ -642,11 +648,11 @@ private ArrayList collectChunkBuildResults() { results.add(result.unwrap()); var jobEffort = result.getJobEffort(); if (jobEffort != null) { - this.jobEffortEstimator.addJobEffort(jobEffort); + this.jobDurationEstimator.addBatchEntry(jobEffort); } } - this.jobEffortEstimator.flushNewData(); + this.jobDurationEstimator.flushNewData(); return results; } @@ -670,21 +676,22 @@ public void updateChunks(boolean updateImmediately) { if (updateImmediately) { // for a perfect frame where everything is finished use the last frame's blocking collector // and add all tasks to it so that they're waited on - this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); + this.submitSectionTasks(Long.MAX_VALUE, thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); thisFrameBlockingCollector.awaitCompletion(this.builder); } else { var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); var remainingDuration = this.builder.getTotalRemainingDuration(this.averageFrameDuration); + var remainingUploadSize = this.regions.getStagingBuffer().getUploadSizeLimit(this.averageFrameDuration); var deferredCollector = new ChunkJobCollector(remainingDuration, this.buildResults::add); // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. // otherwise submit with the collector that the next frame is blocking on. if (SodiumClientMod.options().performance.getSortBehavior().getDeferMode() == DeferMode.ZERO_FRAMES) { - this.submitSectionTasks(thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(remainingUploadSize, thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); } else { - this.submitSectionTasks(nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(remainingUploadSize, nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); } this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); @@ -701,6 +708,7 @@ public void updateChunks(boolean updateImmediately) { } private void submitSectionTasks( + long remainingUploadSize, ChunkJobCollector importantCollector, ChunkJobCollector semiImportantCollector, ChunkJobCollector deferredCollector) { @@ -711,11 +719,15 @@ private void submitSectionTasks( case ALWAYS -> deferredCollector; }; - submitSectionTasks(collector, deferMode); + // don't limit on size for zero frame defer (needs to be done, no matter the limit) + remainingUploadSize = submitSectionTasks(remainingUploadSize, deferMode != DeferMode.ZERO_FRAMES, collector, deferMode); + if (remainingUploadSize <= 0) { + break; + } } } - private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode) { + private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, ChunkJobCollector collector, DeferMode deferMode) { LongHeapPriorityQueue frustumQueue = null; LongHeapPriorityQueue globalQueue = null; float frustumPriorityBias = 0; @@ -743,7 +755,8 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode long frustumItem = 0; long globalItem = 0; - while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && collector.hasBudgetRemaining()) { + while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && + collector.hasBudgetRemaining() && (!limitOnSize || remainingUploadSize > 0)) { // get the first item from the non-empty queues and see which one has higher priority. // if the priority is not infinity, then the item priority was fetched the last iteration and doesn't need updating. if (!frustumQueue.isEmpty() && Float.isInfinite(frustumPriority)) { @@ -780,10 +793,9 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode continue; } - int frame = this.frame; ChunkBuilderTask task; if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { - task = this.createSortTask(section, frame); + task = this.createSortTask(section, this.frame); if (task == null) { // when a sort task is null it means the render section has no dynamic data and @@ -791,7 +803,7 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode continue; } } else { - task = this.createRebuildTask(section, frame); + task = this.createRebuildTask(section, this.frame); if (task == null) { // if the section is empty or doesn't exist submit this null-task to set the @@ -804,7 +816,7 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode // rebuild that must have happened in the meantime includes new non-dynamic // index data. var result = ChunkJobResult.successfully(new ChunkBuildOutput( - section, frame, NoData.forEmptySection(section.getPosition()), + section, this.frame, NoData.forEmptySection(section.getPosition()), BuiltSectionInfo.EMPTY, Collections.emptyMap())); this.buildResults.add(result); @@ -815,13 +827,16 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode if (task != null) { var job = this.builder.scheduleTask(task, type.isImportant(), collector::onJobFinished); collector.addSubmittedJob(job); + remainingUploadSize -= job.getEstimatedSize(); section.setTaskCancellationToken(job); } - section.setLastSubmittedFrame(frame); + section.setLastSubmittedFrame(this.frame); section.clearPendingUpdate(); } + + return remainingUploadSize; } public @Nullable ChunkBuilderMeshingTask createRebuildTask(RenderSection render, int frame) { @@ -832,14 +847,14 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode } var task = new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context); - task.estimateDurationWith(this.jobEffortEstimator); + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator); return task; } public ChunkBuilderSortingTask createSortTask(RenderSection render, int frame) { var task = ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); if (task != null) { - task.estimateDurationWith(this.jobEffortEstimator); + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator); } return task; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java index 102f93727a..89fcc25d5f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java @@ -1,10 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; public abstract class BuilderTaskOutput { public final RenderSection render; public final int submitTime; + private long resultSize = MeshResultSize.NO_DATA; public BuilderTaskOutput(RenderSection render, int buildTime) { this.render = render; @@ -13,4 +15,13 @@ public BuilderTaskOutput(RenderSection render, int buildTime) { public void destroy() { } + + protected abstract long calculateResultSize(); + + public long getResultSize() { + if (this.resultSize == MeshResultSize.NO_DATA) { + this.resultSize = this.calculateResultSize(); + } + return this.resultSize; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java index 442d3669e1..20f075681d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java @@ -32,14 +32,6 @@ public BuiltSectionMeshParts getMesh(TerrainRenderPass pass) { return this.meshes.get(pass); } - public long getEffort() { - long size = 0; - for (var data : this.meshes.values()) { - size += data.getVertexData().getLength(); - } - return 1 + (size >> 8); // make sure the number isn't huge - } - @Override public void destroy() { super.destroy(); @@ -48,4 +40,17 @@ public void destroy() { data.getVertexData().free(); } } + + private long getMeshSize() { + long size = 0; + for (var data : this.meshes.values()) { + size += data.getVertexData().getLength(); + } + return size; + } + + @Override + public long calculateResultSize() { + return super.calculateResultSize() + this.getMeshSize(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java index 52236a161e..e782dafb8e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java @@ -54,4 +54,9 @@ public void destroy() { this.indexBuffer.free(); } } + + @Override + protected long calculateResultSize() { + return this.indexBuffer == null ? 0 : this.indexBuffer.getLength(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java new file mode 100644 index 0000000000..d207f95f62 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java @@ -0,0 +1,81 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import it.unimi.dsi.fastutil.objects.Reference2FloatMap; +import it.unimi.dsi.fastutil.objects.Reference2FloatOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; + +public class CategoryFactorEstimator { + private final Reference2FloatMap aPerB = new Reference2FloatOpenHashMap<>(); + private final Reference2ReferenceMap newData = new Reference2ReferenceOpenHashMap<>(); + private final float newDataFactor; + private final long initialAEstimate; + + public CategoryFactorEstimator(float newDataFactor, long initialAEstimate) { + this.newDataFactor = newDataFactor; + this.initialAEstimate = initialAEstimate; + } + + private static class BatchDataAggregation { + private long aSum; + private long bSum; + + public void addDataPoint(long a, long b) { + this.aSum += a; + this.bSum += b; + } + + public void reset() { + this.aSum = 0; + this.bSum = 0; + } + + public float getAPerBFactor() { + return (float) this.aSum / this.bSum; + } + } + + public interface BatchEntry { + C getCategory(); + + long getA(); + + long getB(); + } + + public void addBatchEntry(BatchEntry batchEntry) { + var category = batchEntry.getCategory(); + if (this.newData.containsKey(category)) { + this.newData.get(category).addDataPoint(batchEntry.getA(), batchEntry.getB()); + } else { + var batchData = new BatchDataAggregation(); + batchData.addDataPoint(batchEntry.getA(), batchEntry.getB()); + this.newData.put(category, batchData); + } + } + + public void flushNewData() { + this.newData.forEach((category, frameData) -> { + var newFactor = frameData.getAPerBFactor(); + if (Float.isNaN(newFactor)) { + return; + } + if (this.aPerB.containsKey(category)) { + var oldFactor = this.aPerB.getFloat(category); + var newValue = oldFactor * (1 - this.newDataFactor) + newFactor * this.newDataFactor; + this.aPerB.put(category, newValue); + } else { + this.aPerB.put(category, newFactor); + } + frameData.reset(); + }); + } + + public long estimateAWithB(C category, long b) { + if (this.aPerB.containsKey(category)) { + return (long) (this.aPerB.getFloat(category) * b); + } else { + return this.initialAEstimate; + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java new file mode 100644 index 0000000000..d2cb6c85a7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java @@ -0,0 +1,14 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public class JobDurationEstimator extends CategoryFactorEstimator> { + public static final float NEW_DATA_FACTOR = 0.01f; + private static final long INITIAL_JOB_DURATION_ESTIMATE = 5_000_000L; + + public JobDurationEstimator() { + super(NEW_DATA_FACTOR, INITIAL_JOB_DURATION_ESTIMATE); + } + + public long estimateJobDuration(Class jobType, long effort) { + return this.estimateAWithB(jobType, effort); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java new file mode 100644 index 0000000000..0802e57ccf --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java @@ -0,0 +1,22 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public record JobEffort(Class category, long duration, long effort) implements CategoryFactorEstimator.BatchEntry> { + public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { + return new JobEffort(effortType,System.nanoTime() - start, effort); + } + + @Override + public Class getCategory() { + return this.category; + } + + @Override + public long getA() { + return this.duration; + } + + @Override + public long getB() { + return this.effort; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java new file mode 100644 index 0000000000..1552630efe --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java @@ -0,0 +1,49 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + +public record MeshResultSize(SectionCategory category, long resultSize) implements CategoryFactorEstimator.BatchEntry { + public static long NO_DATA = -1; + + public enum SectionCategory { + LOW, + UNDERGROUND, + WATER_LEVEL, + SURFACE, + HIGH; + + public static SectionCategory forSection(RenderSection section) { + var sectionY = section.getChunkY(); + if (sectionY < 0) { + return LOW; + } else if (sectionY < 3) { + return UNDERGROUND; + } else if (sectionY == 3) { + return WATER_LEVEL; + } else if (sectionY < 7) { + return SURFACE; + } else { + return HIGH; + } + } + } + + public static MeshResultSize forSection(RenderSection section, long resultSize) { + return new MeshResultSize(SectionCategory.forSection(section), resultSize); + } + + @Override + public SectionCategory getCategory() { + return this.category; + } + + @Override + public long getA() { + return this.resultSize; + } + + @Override + public long getB() { + return 1; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java new file mode 100644 index 0000000000..21f10787ca --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java @@ -0,0 +1,25 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; + +public class MeshTaskSizeEstimator extends CategoryFactorEstimator { + public static final float NEW_DATA_FACTOR = 0.02f; + + public MeshTaskSizeEstimator() { + super(NEW_DATA_FACTOR, RenderRegion.SECTION_BUFFER_ESTIMATE); + } + + public long estimateSize(RenderSection section) { + var lastResultSize = section.getLastMeshResultSize(); + if (lastResultSize != MeshResultSize.NO_DATA) { + return lastResultSize; + } + return this.estimateAWithB(MeshResultSize.SectionCategory.forSection(section), 1); + } + + @Override + public void flushNewData() { + super.flushNewData(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java index 5a230efda5..4b894728c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java @@ -20,17 +20,6 @@ import java.util.function.Consumer; public class ChunkBuilder { - /** - * The low and high efforts given to the sorting and meshing tasks, - * respectively. This split into two separate effort categories means more - * sorting tasks, which are faster, can be scheduled compared to mesh tasks. - * These values need to capture that there's a limit to how much data can be - * uploaded per frame. Since sort tasks generate index data, which is smaller - * per quad and (on average) per section, more of their results can be uploaded - * in one frame. This number should essentially be a conservative estimate of - * min((mesh task upload size) / (sort task upload size), (mesh task time) / - * (sort task time)). - */ static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); private final ChunkJobQueue queue = new ChunkJobQueue(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java index 458ed3a369..65dd30a5fb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java @@ -8,5 +8,7 @@ public interface ChunkJob extends CancellationToken { boolean isStarted(); + long getEstimatedSize(); + long getEstimatedDuration(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java index 07d9659d12..2a1b98ac54 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.minecraft.ReportedException; public class ChunkJobResult { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java index 607d5bc2c8..97f877fba4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java @@ -2,6 +2,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; import java.util.function.Consumer; @@ -50,7 +51,7 @@ public void execute(ChunkBuildContext context) { return; } - result = ChunkJobResult.successfully(output, JobEffort.untilNowWithEffort(this.task.getClass(), start, this.task.getEffort())); + result = ChunkJobResult.successfully(output, JobEffort.untilNowWithEffort(this.task.getClass(), start, output.getResultSize())); } catch (Throwable throwable) { result = ChunkJobResult.exceptionally(throwable); ChunkBuilder.LOGGER.error("Chunk build failed", throwable); @@ -68,6 +69,11 @@ public boolean isStarted() { return this.started; } + @Override + public long getEstimatedSize() { + return this.task.getEstimatedSize(); + } + @Override public long getEstimatedDuration() { return this.task.getEstimatedDuration(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java deleted file mode 100644 index 978c8c9cf9..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; - -public record JobEffort(Class category, long duration, long effort) { - public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { - return new JobEffort(effortType,System.nanoTime() - start, effort); - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java deleted file mode 100644 index 3151c49ec2..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; - -import it.unimi.dsi.fastutil.objects.*; - -// TODO: deal with maximum number of uploads per frame -// TODO: implement per-thread pending upload size limit with a simple semaphore? also see discussion about more complicated allocation scheme with small and large threads: https://discord.com/channels/602796788608401408/651120262129123330/1294158402859171870 -public class JobEffortEstimator { - public static final float NEW_DATA_FACTOR = 0.01f; - - Reference2FloatMap> durationPerEffort = new Reference2FloatArrayMap<>(); - Reference2ReferenceMap, FrameDataAggregation> newData = new Reference2ReferenceArrayMap<>(); - - private static class FrameDataAggregation { - private long durationSum; - private long effortSum; - - public void addDataPoint(long duration, long effort) { - this.durationSum += duration; - this.effortSum += effort; - } - - public void reset() { - this.durationSum = 0; - this.effortSum = 0; - } - - public float getEffortFactor() { - return (float) this.durationSum / this.effortSum; - } - } - - public void addJobEffort(JobEffort jobEffort) { - var category = jobEffort.category(); - if (this.newData.containsKey(category)) { - this.newData.get(category).addDataPoint(jobEffort.duration(), jobEffort.effort()); - } else { - var frameData = new FrameDataAggregation(); - frameData.addDataPoint(jobEffort.duration(), jobEffort.effort()); - this.newData.put(category, frameData); - } - } - - public void flushNewData() { - this.newData.forEach((category, frameData) -> { - var newFactor = frameData.getEffortFactor(); - if (Float.isNaN(newFactor)) { - return; - } - if (this.durationPerEffort.containsKey(category)) { - var oldFactor = this.durationPerEffort.getFloat(category); - var newValue = oldFactor * (1 - NEW_DATA_FACTOR) + newFactor * NEW_DATA_FACTOR; - this.durationPerEffort.put(category, newValue); - } else { - this.durationPerEffort.put(category, newFactor); - } - frameData.reset(); - }); - } - - public long estimateJobDuration(Class category, long effort) { - if (this.durationPerEffort.containsKey(category)) { - return (long) (this.durationPerEffort.getFloat(category) * effort); - } else { - return 10_000_000L; // 10ms as initial guess - } - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java index 8f6ae90436..24b50360d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java @@ -7,6 +7,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderCache; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; @@ -227,7 +228,7 @@ private ReportedException fillCrashInfo(CrashReport report, LevelSlice slice, Bl } @Override - public long getEffort() { - return this.render.getLastMeshingTaskEffort(); + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { + return estimator.estimateSize(this.render); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java index 82380ed692..f0fc6ca110 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicSorter; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; @@ -42,7 +43,7 @@ public static ChunkBuilderSortingTask createTask(RenderSection render, int frame } @Override - public long getEffort() { + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { return this.sorter.getQuadCount(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java index 622ee43ef7..d30a23a0ee 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.JobEffortEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import org.joml.Vector3dc; import org.joml.Vector3f; import org.joml.Vector3fc; @@ -27,6 +28,7 @@ public abstract class ChunkBuilderTask impleme protected final Vector3dc absoluteCameraPos; protected final Vector3fc cameraPos; + private long estimatedSize; private long estimatedDuration; /** @@ -57,10 +59,15 @@ public ChunkBuilderTask(RenderSection render, int time, Vector3dc absoluteCamera */ public abstract OUTPUT execute(ChunkBuildContext context, CancellationToken cancellationToken); - public abstract long getEffort(); + public abstract long estimateTaskSizeWith(MeshTaskSizeEstimator estimator); - public void estimateDurationWith(JobEffortEstimator estimator) { - this.estimatedDuration = estimator.estimateJobDuration(this.getClass(), this.getEffort()); + public void calculateEstimations(JobDurationEstimator jobEstimator, MeshTaskSizeEstimator sizeEstimator) { + this.estimatedSize = this.estimateTaskSizeWith(sizeEstimator); + this.estimatedDuration = jobEstimator.estimateJobDuration(this.getClass(), this.estimatedSize); + } + + public long getEstimatedSize() { + return this.estimatedSize; } public long getEstimatedDuration() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java index 88cdddbc50..8149845e74 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java @@ -25,6 +25,10 @@ import java.util.Map; public class RenderRegion { + public static final int SECTION_VERTEX_COUNT_ESTIMATE = 756; + public static final int SECTION_INDEX_COUNT_ESTIMATE = (SECTION_VERTEX_COUNT_ESTIMATE / 4) * 6; + public static final int SECTION_BUFFER_ESTIMATE = SECTION_VERTEX_COUNT_ESTIMATE * ChunkMeshFormats.COMPACT.getVertexFormat().getStride() + SECTION_INDEX_COUNT_ESTIMATE * Integer.BYTES; + public static final int REGION_WIDTH = 8; public static final int REGION_HEIGHT = 4; public static final int REGION_LENGTH = 8; @@ -285,11 +289,8 @@ public static class DeviceResources { public DeviceResources(CommandList commandList, StagingBuffer stagingBuffer) { int stride = ChunkMeshFormats.COMPACT.getVertexFormat().getStride(); - // the magic number 756 for the initial size is arbitrary, it was made up. - var initialVertices = 756; - this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * initialVertices, stride, stagingBuffer); - var initialIndices = (initialVertices / 4) * 6; - this.indexArena = new GlBufferArena(commandList, REGION_SIZE * initialIndices, Integer.BYTES, stagingBuffer); + this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_VERTEX_COUNT_ESTIMATE, stride, stagingBuffer); + this.indexArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_INDEX_COUNT_ESTIMATE, Integer.BYTES, stagingBuffer); } public void updateTessellation(CommandList commandList, GlTessellation tessellation) {