From 7fd731ecd3a9589cc557d2f9799ce1df8082f208 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 9 Nov 2025 17:52:05 +0100 Subject: [PATCH 1/2] Improved buffer size estimation leading to far fewer buffer allocations and much less resizing (#3313) --- .../sodium/client/gl/arena/GlBufferArena.java | 176 ++++++++++++++---- .../chunk/data/SectionRenderDataStorage.java | 4 +- .../render/chunk/region/RenderRegion.java | 6 +- .../chunk/region/RenderRegionManager.java | 12 +- 4 files changed, 147 insertions(+), 51 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/GlBufferArena.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/GlBufferArena.java index 0820ae0a9f..9e38fd2f60 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/GlBufferArena.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/GlBufferArena.java @@ -11,15 +11,21 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; public class GlBufferArena { static final boolean CHECK_ASSERTIONS = false; - private static final GlBufferUsage BUFFER_USAGE = GlBufferUsage.STATIC_DRAW; + // how many segments we require to be present before we calculate an average size + public static final int MIN_SEGMENTS_FOR_AVG = 16; + // growth factor to use when we have too few segments present + public static final float FEW_SEGMENTS_GROWTH_FACTOR = 1.5f; + // factor to use when we are allocating with an expected size + public static final float EXPECTED_SIZE_TARGET_FACTOR = 1.5f; + // how much bigger than requested a buffer can be to be considered for reuse + public static final float MAX_BUFFER_REUSE_SIZE_FACTOR = 1.4f; - private final int resizeIncrement; + private static final GlBufferUsage BUFFER_USAGE = GlBufferUsage.STATIC_DRAW; private final StagingBuffer stagingBuffer; private GlMutableBuffer arenaBuffer; @@ -28,20 +34,23 @@ public class GlBufferArena { private long capacity; private long used; + private int segmentCount; private final int stride; + private static final GlMutableBuffer[] freeBuffers = new GlMutableBuffer[8]; + private static int freeBufferCount = 0; + public GlBufferArena(CommandList commands, int initialCapacity, int stride, StagingBuffer stagingBuffer) { this.capacity = initialCapacity; - this.resizeIncrement = initialCapacity / 16; this.stride = stride; - this.head = new GlBufferSegment(this, 0, initialCapacity); + this.head = new GlBufferSegment(this, 0, this.capacity); this.head.setFree(true); - this.arenaBuffer = commands.createMutableBuffer(); - commands.allocateStorage(this.arenaBuffer, this.capacity * stride, BUFFER_USAGE); + this.arenaBuffer = getBufferOfSizeAtLeast(commands, this.capacity * stride); + this.capacity = this.arenaBuffer.getSize() / stride; this.stagingBuffer = stagingBuffer; } @@ -117,6 +126,57 @@ private List buildTransferList(List u return pendingCopies; } + private static GlMutableBuffer getBufferOfSizeAtLeast(CommandList commandList, long size) { + GlMutableBuffer buffer = null; + + if (freeBufferCount > 0) { + // get any buffer of at least the requested size but at most MAX_BUFFER_REUSE_SIZE_FACTOR larger + long maxAcceptableSize = (long) (size * MAX_BUFFER_REUSE_SIZE_FACTOR); + + // iterate buffers to get the smallest acceptable one + int candidateIndex = -1; + for (int i = 0; i < freeBuffers.length; i++) { + GlMutableBuffer freeBuffer = freeBuffers[i]; + if (freeBuffer != null) { + long testSize = freeBuffer.getSize(); + if (testSize >= size && testSize <= maxAcceptableSize && + (buffer == null || testSize < buffer.getSize())) { + candidateIndex = i; + buffer = freeBuffer; + } + } + } + if (buffer != null) { + freeBuffers[candidateIndex] = null; + freeBufferCount--; + } + } + + if (buffer == null) { + buffer = commandList.createMutableBuffer(); + commandList.allocateStorage(buffer, size, BUFFER_USAGE); + } + return buffer; + } + + private static void releaseBufferForReuse(CommandList commandList, GlMutableBuffer buffer) { + // find an empty slot if there is one + if (freeBufferCount < freeBuffers.length) { + for (int i = 0; i < freeBuffers.length; i++) { + if (freeBuffers[i] == null) { + freeBuffers[i] = buffer; + freeBufferCount++; + return; + } + } + } + + // evict randomly if no empty slot available + int evictIndex = (int) (Math.random() * freeBuffers.length); + commandList.deleteBuffer(freeBuffers[evictIndex]); + freeBuffers[evictIndex] = buffer; + } + private void transferSegments(CommandList commandList, Collection list, long capacity) { long bufferSize = capacity * this.stride; if (bufferSize >= (1L << 32)) { @@ -124,9 +184,7 @@ private void transferSegments(CommandList commandList, Collection getUsedSegments() { @@ -166,6 +226,11 @@ public long getDeviceAllocatedMemory() { return this.capacity * this.stride; } + private void updateUsed(long deltaUsed) { + this.used += deltaUsed; + this.segmentCount += Long.signum(deltaUsed); + } + private GlBufferSegment alloc(int size) { GlBufferSegment a = this.findFree(size); @@ -195,7 +260,7 @@ private GlBufferSegment alloc(int size) { result = b; } - this.used += result.getLength(); + this.updateUsed(result.getLength()); this.checkAssertions(); return result; @@ -229,7 +294,7 @@ public void free(GlBufferSegment entry) { entry.setFree(true); - this.used -= entry.getLength(); + this.updateUsed(-entry.getLength()); GlBufferSegment next = entry.getNext(); @@ -258,32 +323,29 @@ public GlBuffer getBufferObject() { return this.arenaBuffer; } - public boolean upload(CommandList commandList, Stream stream) { + public boolean upload(CommandList commandList, Stream stream, float regionFillFractionInv) { // Record the buffer object before we start any work // If the arena needs to re-allocate a buffer, this will allow us to check and return an appropriate flag GlBuffer buffer = this.arenaBuffer; // A linked list is used as we'll be randomly removing elements and want O(1) performance - List queue = stream.collect(Collectors.toCollection(LinkedList::new)); + long totalUploadSize = 0; + List queue = new LinkedList<>(); + for (var upload : (Iterable) stream::iterator) { + totalUploadSize += upload.getDataBuffer().getLength(); + queue.add(upload); + } - // Try to upload all the data into free segments first - this.tryUploads(commandList, queue); + // Try to upload all the data into free segments first, + // but only attempt this if there is enough free space assuming no fragmentation + if (totalUploadSize < (this.capacity - this.used) * this.stride) { + this.tryUploads(commandList, queue); + } // If we weren't able to upload some buffers, they will have been left behind in the queue if (!queue.isEmpty()) { - // Calculate the amount of memory needed for the remaining uploads - int remainingUploadSize = queue.stream() - .mapToInt(upload -> upload.getDataBuffer().getLength()) - .sum(); - - // Convert size to elements by dividing by the stride. - // This doesn't need a ceil since the upload buffers will be at least as big as required and have the same stride. - long remainingElements = remainingUploadSize / this.stride; - - // Ask the arena to grow to accommodate the remaining uploads - // This will force a re-allocation and compaction, which will leave us a continuous free segment - // for the remaining uploads - this.ensureCapacity(commandList, remainingElements); + // resize to the new estimated capacity + this.resize(commandList, estimateNewCapacity(regionFillFractionInv, queue)); // Try again to upload any buffers that failed last time this.tryUploads(commandList, queue); @@ -297,6 +359,48 @@ public boolean upload(CommandList commandList, Stream stream) { return this.arenaBuffer != buffer; } + private long estimateNewCapacity(float regionFillFractionInv, List queue) { + // Calculate the amount of memory needed for the remaining uploads + long requiredTotalSize = getRequiredTotalSize(queue); + + int newSegmentCount = this.segmentCount + queue.size(); + + // the base estimation is to use a growth factor applied to the new required size + long newCapacity; + + // use average segment size if we have enough segments to make it an accurate value + if (newSegmentCount >= MIN_SEGMENTS_FOR_AVG) { + // find the average segment size after the remaining uploads are allocated + long averageNewSegmentSize = (requiredTotalSize / newSegmentCount) + 1; // +1 to round up + + // use the average segment size to determine a new capacity, with some overshoot applied for safety + var expectedSegmentCount = newSegmentCount * regionFillFractionInv; + newCapacity = (long) (averageNewSegmentSize * expectedSegmentCount * EXPECTED_SIZE_TARGET_FACTOR); + } else { + newCapacity = (long) (requiredTotalSize * FEW_SEGMENTS_GROWTH_FACTOR); + } + return newCapacity; + } + + private long getRequiredTotalSize(List queue) { + long remainingUploadSize = 0; + for (var upload : queue) { + remainingUploadSize += upload.getDataBuffer().getLength(); + } + + // Convert size to elements by dividing by the stride. + // This doesn't need a ceil since the upload buffers will be at least as big as required and have the same stride. + long remainingElements = remainingUploadSize / this.stride; + + // Ask the arena to grow to accommodate the remaining uploads + // This will force a re-allocation and compaction, which will leave us a continuous free segment + // for the remaining uploads + + // Re-sizing the arena results in a compaction, so any free space in the arena will be + // made into one contiguous segment, joined with the new segment of free space we're asking for + return remainingElements + this.used; + } + private void tryUploads(CommandList commandList, List queue) { queue.removeIf(upload -> this.tryUpload(commandList, upload)); this.stagingBuffer.flush(commandList); @@ -322,16 +426,6 @@ private boolean tryUpload(CommandList commandList, PendingUpload upload) { return true; } - public void ensureCapacity(CommandList commandList, long elementCount) { - // Re-sizing the arena results in a compaction, so any free space in the arena will be - // made into one contiguous segment, joined with the new segment of free space we're asking for - // We calculate the number of free elements in our arena and then subtract that from the total requested - long elementsNeeded = elementCount - (this.capacity - this.used); - - // Try to allocate some extra buffer space unless this is an unusually large allocation - this.resize(commandList, Math.max(this.capacity + this.resizeIncrement, this.capacity + elementsNeeded)); - } - private void checkAssertions() { if (CHECK_ASSERTIONS) { this.checkAssertions0(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java index 17ae86a7fb..68c01fc829 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/SectionRenderDataStorage.java @@ -145,7 +145,7 @@ public boolean needsSharedIndexUpdate() { * @param arena The buffer arena to allocate the new buffer from * @return true if the arena resized itself */ - public boolean updateSharedIndexData(CommandList commandList, GlBufferArena arena) { + public boolean updateSharedIndexData(CommandList commandList, GlBufferArena arena, float regionFillFractionInv) { // assumes this.needsSharedIndexUpdate is true when this is called this.needsSharedIndexUpdate = false; @@ -177,7 +177,7 @@ public boolean updateSharedIndexData(CommandList commandList, GlBufferArena aren // create and upload a new shared index buffer var buffer = SharedQuadIndexBuffer.createIndexBuffer(SharedQuadIndexBuffer.IndexType.INTEGER, this.sharedIndexCapacity); var pendingUpload = new PendingUpload(buffer); - var bufferChanged = arena.upload(commandList, Stream.of(pendingUpload)); + var bufferChanged = arena.upload(commandList, Stream.of(pendingUpload), regionFillFractionInv); this.sharedIndexAllocation = pendingUpload.getResult(); buffer.free(); 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 ce484e6f86..12da495bd1 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 @@ -23,7 +23,7 @@ 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_INDEX_COUNT_ESTIMATE = (SECTION_VERTEX_COUNT_ESTIMATE / DefaultTerrainRenderPasses.ALL.length / 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; @@ -219,6 +219,10 @@ public void removeSection(RenderSection section) { this.sections[sectionIndex] = null; this.sectionCount--; } + + public float getFillFractionInv() { + return (float) RenderRegion.REGION_SIZE / (float) this.sectionCount; + } public RenderSection getSection(int id) { return this.sections[id]; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java index 781147de9c..7bc890f67a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java @@ -22,10 +22,7 @@ import net.minecraft.util.profiling.ProfilerFiller; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; +import java.util.*; public class RenderRegionManager { private final Long2ReferenceOpenHashMap regions = new Long2ReferenceOpenHashMap<>(); @@ -136,13 +133,14 @@ private void uploadResults(CommandList commandList, RenderRegion region, Collect } var resources = region.createResources(commandList); + var regionFillFractionInv = region.getFillFractionInv(); profiler.push("upload_vertices"); if (!uploads.isEmpty()) { var arena = resources.getGeometryArena(); boolean bufferChanged = arena.upload(commandList, uploads.stream() - .map(upload -> upload.vertexUpload)); + .map(upload -> upload.vertexUpload), regionFillFractionInv); // If any of the buffers changed, the tessellation will need to be updated // Once invalidated the tessellation will be re-created on the next attempted use @@ -165,7 +163,7 @@ private void uploadResults(CommandList commandList, RenderRegion region, Collect if (!indexUploads.isEmpty()) { var arena = resources.getIndexArena(); indexBufferChanged = arena.upload(commandList, indexUploads.stream() - .map(upload -> upload.indexBufferUpload)); + .map(upload -> upload.indexBufferUpload), regionFillFractionInv); for (PendingSectionIndexBufferUpload upload : indexUploads) { var storage = region.createStorage(DefaultTerrainRenderPasses.TRANSLUCENT); @@ -174,7 +172,7 @@ private void uploadResults(CommandList commandList, RenderRegion region, Collect } if (needsSharedIndexUpdate) { - indexBufferChanged |= translucentStorage.updateSharedIndexData(commandList, resources.getIndexArena()); + indexBufferChanged |= translucentStorage.updateSharedIndexData(commandList, resources.getIndexArena(), regionFillFractionInv); } if (indexBufferChanged) { From 91e2386ad3c5ebfe20a397e728882b50f170a1d9 Mon Sep 17 00:00:00 2001 From: IMS212 Date: Sun, 9 Nov 2025 08:54:16 -0800 Subject: [PATCH 2/2] Update version to 0.7.3 --- buildSrc/src/main/kotlin/BuildConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt index e2fab18355..c9a8c49e8b 100644 --- a/buildSrc/src/main/kotlin/BuildConfig.kt +++ b/buildSrc/src/main/kotlin/BuildConfig.kt @@ -10,7 +10,7 @@ object BuildConfig { val PARCHMENT_VERSION: String? = null // https://semver.org/ - var MOD_VERSION: String = "0.7.2" + var MOD_VERSION: String = "0.7.3" fun createVersionString(project: Project): String { val builder = StringBuilder()