From 6d86bf071ff52760bc9d190c9a75e5c6e2449d00 Mon Sep 17 00:00:00 2001 From: BonsUnleashed Date: Tue, 29 Sep 2026 04:34:49 +0200 Subject: [PATCH] Reduce allocation in upload classification, preparation and queue bookkeeping Before drawing newly built chunks, the renderer organizes data and creates temporary lists, even when some lists will be empty. These changes reduce that preparation and create fewer temporary objects. The same drawing data still reaches the graphics card; an overall frame-rate gain has not been established. Source port of a change shipped in Bons and Furious 1.0.15. --- .../sodium/client/gl/arena/GlBufferArena.java | 16 +++--- .../render/chunk/RenderSectionManager.java | 19 ++++--- .../chunk/region/RenderRegionManager.java | 54 ++++++++++++++++--- 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferArena.java b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferArena.java index 124519452..a89e78cc4 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferArena.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/gl/arena/GlBufferArena.java @@ -11,8 +11,6 @@ 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; @@ -268,13 +266,13 @@ public GlBuffer getBufferObject() { return this.arenaBuffer; } - public boolean upload(CommandList commandList, Stream stream) { + public boolean upload(CommandList commandList, Collection uploads) { // 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)); + List queue = new LinkedList<>(uploads); // Try to upload all of the data into free segments first this.tryUploads(commandList, queue); @@ -282,9 +280,13 @@ public boolean upload(CommandList commandList, Stream stream) { // 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 remainingElements = (int)(queue.stream() - .mapToLong(upload -> upload.getDataBuffer().getLength()) - .sum() / this.stride); + long remainingBytes = 0; + + for (PendingUpload upload : queue) { + remainingBytes += upload.getDataBuffer().getLength(); + } + + int remainingElements = (int)(remainingBytes / 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 diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/RenderSectionManager.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/RenderSectionManager.java index eddb4bbee..80e2e8c1b 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -403,7 +403,7 @@ public void uploadChunks() { this.needsUpdate = true; } - private void processChunkBuildResults(ArrayList results) { + private void processChunkBuildResults(List results) { var filtered = filterChunkBuildResults(results); this.regions.uploadMeshes(RenderDevice.INSTANCE.createCommandList(), filtered); @@ -447,7 +447,7 @@ private void updateSectionInfo(RenderSection render, BuiltSectionInfo info) { } } - private static List filterChunkBuildResults(ArrayList outputs) { + private static List filterChunkBuildResults(List outputs) { var map = new Reference2ReferenceLinkedOpenHashMap(); for (var output : outputs) { @@ -466,12 +466,19 @@ private static List filterChunkBuildResults(ArrayList(map.values()); } - private ArrayList collectChunkBuildResults() { - ArrayList results = new ArrayList<>(); - ChunkJobResult result; + private List collectChunkBuildResults() { + ChunkJobResult result = this.buildResults.poll(); - while ((result = this.buildResults.poll()) != null) { + // Avoid allocating a list on frames where no jobs have finished + if (result == null) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(); + + while (result != null) { results.add(result.unwrap()); + result = this.buildResults.poll(); } return results; diff --git a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/RenderRegionManager.java b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/RenderRegionManager.java index 4ab96cb73..b3ce6d7d4 100644 --- a/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/RenderRegionManager.java +++ b/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/region/RenderRegionManager.java @@ -50,11 +50,35 @@ public void update() { public void uploadMeshes(CommandList commandList, Collection results) { for (var entry : this.createMeshUploadQueues(results)) { - this.uploadMeshes(commandList, entry.getKey(), entry.getValue().stream().filter(o -> !o.isIndexOnlyUpload()).toList()); - this.uploadResorts(commandList, entry.getKey(), entry.getValue().stream().filter(ChunkBuildOutput::isIndexOnlyUpload).toList()); + this.uploadMeshes(commandList, entry.getKey(), filterOutputs(entry.getValue(), false)); + this.uploadResorts(commandList, entry.getKey(), filterOutputs(entry.getValue(), true)); } } + // Selects the outputs whose index-only flag matches. Either group is often empty, so the list is only + // allocated once an output matches. + private static List filterOutputs(List outputs, boolean indexOnly) { + List filtered = null; + + for (ChunkBuildOutput output : outputs) { + if (output.isIndexOnlyUpload() != indexOnly) { + continue; + } + + if (filtered == null) { + filtered = new ArrayList<>(); + } + + filtered.add(output); + } + + if (filtered == null) { + return Collections.emptyList(); + } + + return filtered; + } + private void uploadMeshes(CommandList commandList, RenderRegion region, Collection results) { var uploads = new ArrayList(); @@ -83,11 +107,20 @@ private void uploadMeshes(CommandList commandList, RenderRegion region, Collecti var resources = region.createResources(commandList); var geometryArena = resources.getGeometryArena(); - boolean bufferChanged = geometryArena.upload(commandList, uploads.stream() - .map(upload -> upload.vertexUpload)); + var vertexUploads = new ArrayList(uploads.size()); + var indexUploads = new ArrayList(); - bufferChanged |= resources.getIndexArena().upload(commandList, uploads.stream() - .map(upload -> upload.indexUpload).filter(Objects::nonNull)); + for (PendingSectionUpload upload : uploads) { + vertexUploads.add(upload.vertexUpload); + + if (upload.indexUpload != null) { + indexUploads.add(upload.indexUpload); + } + } + + boolean bufferChanged = geometryArena.upload(commandList, vertexUploads); + + bufferChanged |= resources.getIndexArena().upload(commandList, indexUploads); // 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 @@ -133,8 +166,13 @@ private void uploadResorts(CommandList commandList, RenderRegion region, Collect var resources = region.createResources(commandList); - boolean bufferChanged = resources.getIndexArena().upload(commandList, uploads.stream() - .map(upload -> upload.indexUpload).filter(Objects::nonNull)); + var indexUploads = new ArrayList(uploads.size()); + + for (PendingResortUpload upload : uploads) { + indexUploads.add(upload.indexUpload); + } + + boolean bufferChanged = resources.getIndexArena().upload(commandList, indexUploads); // 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