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) {