Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor job effort estimation, add category-based meshing task size …
Browse files Browse the repository at this point in the history
…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
douira committed Nov 22, 2024
1 parent 1ea07c3 commit 94885b5
Showing 23 changed files with 310 additions and 132 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<GlBufferStorageFlags> 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;
Original file line number Diff line number Diff line change
@@ -13,4 +13,6 @@ public interface StagingBuffer {
void delete(CommandList commandList);

void flip();

long getUploadSizeLimit(long frameDuration);
}
Original file line number Diff line number Diff line change
@@ -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;
}

/**
Original file line number Diff line number Diff line change
@@ -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<RenderSection> sectionByPosition = new Long2ReferenceOpenHashMap<>();

private final ConcurrentLinkedDeque<ChunkJobResult<? extends BuilderTaskOutput>> 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<BuilderTaskOutput> 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<BuilderTaskOutput> results) {
result.render.setLastUploadFrame(result.submitTime);
}

this.meshTaskSizeEstimator.flushNewData();

return touchedSectionInfo;
}

@@ -642,11 +648,11 @@ private ArrayList<BuilderTaskOutput> 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,18 +793,17 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode
continue;
}

int frame = this.frame;
ChunkBuilderTask<? extends BuilderTaskOutput> 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
// doesn't need to be sorted. Nothing needs to be done.
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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -54,4 +54,9 @@ public void destroy() {
this.indexBuffer.free();
}
}

@Override
protected long calculateResultSize() {
return this.indexBuffer == null ? 0 : this.indexBuffer.getLength();
}
}
Loading

0 comments on commit 94885b5

Please sign in to comment.