From 16f72066a8c32cbf0a657044b7a221d7bb67a6e4 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sat, 30 May 2026 11:32:46 -0500 Subject: [PATCH] Pool BufferQuad objects --- .../render/bufferBuilding/BufferQuad.java | 30 ++--- .../bufferBuilding/LodBufferContainer.java | 8 +- .../render/bufferBuilding/LodQuadBuilder.java | 119 +++++++++++++++--- .../render/QuadTree/LodRenderSection.java | 39 +++--- 4 files changed, 146 insertions(+), 50 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java index 4faf71b36..44d679ba8 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/BufferQuad.java @@ -41,28 +41,28 @@ public final class BufferQuad public static final int MAX_QUAD_WIDTH_FOR_EARTH_CURVATURE = LodUtil.CHUNK_WIDTH; - public final short x; - public final short y; - public final short z; + public short x; + public short y; + public short z; public short widthEastWest; /** This is both North/South and Up/Down since the merging logic is the same either way */ public short widthNorthSouthOrHeight; - public final int color; + public int color; /** used by the Iris shader mod to determine how each LOD should be rendered */ - public final byte irisBlockMaterialId; + public byte irisBlockMaterialId; - public final byte skyLight; - public final byte blockLight; - public final EDhDirection direction; + public byte skyLight; + public byte blockLight; + public EDhDirection direction; public boolean hasError = false; // Pre-computed sort keys to avoid recomputing on every comparison // Slight increase in memory for reduction in cpu usage - public final long sortKeyEastWest; - public final long sortKeyNorthSouth; + public long sortKeyEastWest; + public long sortKeyNorthSouth; @@ -71,15 +71,17 @@ public final class BufferQuad //=============// //region - public BufferQuad( - short x, short y, short z, short widthEastWest, short widthNorthSouthOrHeight, - int color, byte irisBlockMaterialId, byte skylight, byte blockLight, - EDhDirection direction) + public BufferQuad() {} + + public void set(short x, short y, short z, short widthEastWest, short widthNorthSouthOrHeight, + int color, byte irisBlockMaterialId, byte skylight, byte blockLight, + EDhDirection direction) { if (widthEastWest == 0 || widthNorthSouthOrHeight == 0) { throw new IllegalArgumentException("Size 0 quad!"); } + if (widthEastWest < 0 || widthNorthSouthOrHeight < 0) { throw new IllegalArgumentException("Negative sized quad!"); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodBufferContainer.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodBufferContainer.java index c3bf2124b..6b1acd33e 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodBufferContainer.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodBufferContainer.java @@ -89,7 +89,9 @@ public class LodBufferContainer implements AutoCloseable /** Should be run on a DH thread. */ public static CompletableFuture tryMakeAndUploadBuffersAsync( long pos, IDhClientLevel clientLevel, - LodQuadBuilder builder) + ArrayList opaqueBuffers, + ArrayList transparentBuffers + ) { // new upload needed CompletableFuture future = new CompletableFuture<>(); @@ -107,10 +109,6 @@ public class LodBufferContainer implements AutoCloseable DhSectionPos.getMinCornerBlockZ(pos)); LodBufferContainer bufferContainer = new LodBufferContainer(pos, minCornerBlockPos); - // create CPU vertex buffers - ArrayList opaqueBuffers = builder.makeOpaqueVertexBuffers(); - ArrayList transparentBuffers = builder.makeTransparentVertexBuffers(); - // update arrays to contain buffers bufferContainer.vboOpaqueWrappers = resizeWrapperArray(bufferContainer.vboOpaqueWrappers, opaqueBuffers.size()); bufferContainer.vboTransparentWrappers = resizeWrapperArray(bufferContainer.vboTransparentWrappers, transparentBuffers.size()); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java index 67622a5f3..ec56619cb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/bufferBuilding/LodQuadBuilder.java @@ -40,21 +40,13 @@ import org.lwjgl.system.MemoryUtil; * * Note: the magic number 6 you see throughout this method represents the number of sides on a cube. */ -public class LodQuadBuilder +public class LodQuadBuilder implements AutoCloseable { private static final DhLogger LOGGER = new DhLoggerBuilder().build(); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); - @SuppressWarnings("unchecked") - private final ArrayList[] opaqueQuads = (ArrayList[]) new ArrayList[6]; - @SuppressWarnings("unchecked") - private final ArrayList[] transparentQuads = (ArrayList[]) new ArrayList[6]; - - private final boolean doTransparency; - private final IClientLevelWrapper clientLevelWrapper; - - private final EDhApiDebugRendering debugRenderingMode; - private final EDhApiGrassSideRendering grassSideRenderingMode; + /** ThreadLocal is the simplest way to allow each LOD loading thread to have their own builder */ + private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(LodQuadBuilder::new); /** the number of bytes for a single vertex */ public static final int BYTES_PER_VERTEX = 16; @@ -111,6 +103,26 @@ public class LodQuadBuilder }; //endregion + + + @SuppressWarnings("unchecked") + private final ArrayList[] opaqueQuads = (ArrayList[]) new ArrayList[6]; + @SuppressWarnings("unchecked") + private final ArrayList[] transparentQuads = (ArrayList[]) new ArrayList[6]; + + /** + * Caching the BufferQuad objects reduces overhead slightly.
+ * Caching is handled per builder (vs globally in {@link BufferQuad} itself) + * to prevent concurrency overhead. + */ + private final ArrayList bufferQuadCacheList = new ArrayList<>(); + + private boolean doTransparency; + private IClientLevelWrapper clientLevelWrapper; + + private EDhApiDebugRendering debugRenderingMode; + private EDhApiGrassSideRendering grassSideRenderingMode; + private int premergeCount = 0; @@ -120,20 +132,31 @@ public class LodQuadBuilder //=============// //region - public LodQuadBuilder(boolean doTransparency, IClientLevelWrapper clientLevelWrapper) + private LodQuadBuilder() { - this.doTransparency = doTransparency; for (int i = 0; i < 6; i++) { this.opaqueQuads[i] = new ArrayList<>(); this.transparentQuads[i] = new ArrayList<>(); } + } + + public static LodQuadBuilder getBuilder(boolean doTransparency, IClientLevelWrapper clientLevelWrapper) + { + LodQuadBuilder builder = THREAD_LOCAL.get(); + builder.set(doTransparency, clientLevelWrapper); + return builder; + } + private void set(boolean doTransparency, IClientLevelWrapper clientLevelWrapper) + { + this.doTransparency = doTransparency; this.clientLevelWrapper = clientLevelWrapper; this.debugRenderingMode = Config.Client.Advanced.Debugging.debugRenderingColors.get(); this.grassSideRenderingMode = Config.Client.Advanced.Graphics.Quality.grassSideRendering.get(); + this.premergeCount = 0; } //endregion @@ -167,7 +190,8 @@ public class LodQuadBuilder quadList = this.opaqueQuads[dir.ordinal()]; } - BufferQuad quad = new BufferQuad(x, y, z, width, height, color, irisBlockMaterialId, skyLight, blockLight, dir); + BufferQuad quad = this.getOrCreateBufferQuad(); + quad.set(x, y, z, width, height, color, irisBlockMaterialId, skyLight, blockLight, dir); if (!quadList.isEmpty() && ( quadList.get(quadList.size() - 1).tryMerge(quad, BufferMergeDirectionEnum.EastWest) @@ -189,7 +213,8 @@ public class LodQuadBuilder ? this.transparentQuads[EDhDirection.UP.ordinal()] : this.opaqueQuads[EDhDirection.UP.ordinal()]; - BufferQuad quad = new BufferQuad(minX, maxY, minZ, blockWidth, blockWidth, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP); + BufferQuad quad = this.getOrCreateBufferQuad(); + quad.set(minX, maxY, minZ, blockWidth, blockWidth, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.UP); quadList.add(quad); } @@ -199,7 +224,8 @@ public class LodQuadBuilder ? this.transparentQuads[EDhDirection.DOWN.ordinal()] : this.opaqueQuads[EDhDirection.DOWN.ordinal()]; - BufferQuad quad = new BufferQuad(x, y, z, blockWidth, blockWidth, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN); + BufferQuad quad = this.getOrCreateBufferQuad(); + quad.set(x, y, z, blockWidth, blockWidth, color, irisBlockMaterialId, skylight, blocklight, EDhDirection.DOWN); quadArray.add(quad); } @@ -517,4 +543,65 @@ public class LodQuadBuilder + //=====================// + // buffer quad pooling // + //=====================// + //region + + private BufferQuad getOrCreateBufferQuad() + { + // start from the back of the list so we don't have + // to move the array around + int index = bufferQuadCacheList.size() - 1; + if (index < 0) + { + // cache empty, create a new object + return new BufferQuad(); + } + + BufferQuad quad = bufferQuadCacheList.remove(index); + if (quad != null) // shouldn't happen, but just in case + { + return quad; + } + + return new BufferQuad(); + } + + private static void returnQuadsToCache(ArrayList quadCache, ArrayList[] quadsToReturn) + { + for (int i = 0; i < quadsToReturn.length; i++) + { + // manual add and loop to reduce GC pressure due to addAll() doing unnecessary + // array copies + for (int j = 0; j < quadsToReturn[i].size(); j++) + { + quadCache.add(quadsToReturn[i].get(j)); + } + + quadsToReturn[i].clear(); + } + } + + //endregion + + + + //================// + // base overrides // + //================// + //region + + // can be used/closed multiple times + @Override + public void close() + { + returnQuadsToCache(this.bufferQuadCacheList, this.opaqueQuads); + returnQuadsToCache(this.bufferQuadCacheList, this.transparentQuads); + } + + //endregion + + + } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java index 89995b45c..dbc5816c3 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java @@ -47,6 +47,8 @@ import org.jetbrains.annotations.Nullable; import javax.annotation.WillNotClose; import java.awt.*; +import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; @@ -171,20 +173,26 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable try { // build LOD data on a DH thread - LodQuadBuilder lodQuadBuilder = this.getAndBuildRenderData(); - if (lodQuadBuilder == null) + try (LodQuadBuilder lodQuadBuilder = this.getAndBuildRenderData()) { - future.complete(null); - return; - } - - // uploading will primarily happen on the render thread - this.uploadToGpuAsync(future, lodQuadBuilder) - .thenRun(() -> + if (lodQuadBuilder == null) { - // the future is passed in separately (IE not using the local var) to prevent any possible race condition null pointers future.complete(null); - }); + return; + } + + // create CPU vertex buffers + ArrayList opaqueBuffers = lodQuadBuilder.makeOpaqueVertexBuffers(); + ArrayList transparentBuffers = lodQuadBuilder.makeTransparentVertexBuffers(); + + // uploading will primarily happen on the render thread + this.uploadToGpuAsync(future, opaqueBuffers, transparentBuffers) + .thenRun(() -> + { + // the future is passed in separately (IE not using the local var) to prevent any possible race condition null pointers + future.complete(null); + }); + } } catch (Exception e) { @@ -225,7 +233,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get() == EDhApiTransparency.COMPLETE; - LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.clientLevel.getClientLevelWrapper()); + LodQuadBuilder lodQuadBuilder = LodQuadBuilder.getBuilder(enableTransparency, this.clientLevel.getClientLevelWrapper()); // get the adjacent positions @@ -303,10 +311,11 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable private synchronized CompletableFuture uploadToGpuAsync( - CompletableFuture parentFuture, - LodQuadBuilder lodQuadBuilder) + CompletableFuture parentFuture, + ArrayList opaqueBuffers, + ArrayList transparentBuffers) { - CompletableFuture uploadFuture = LodBufferContainer.tryMakeAndUploadBuffersAsync(this.pos, this.clientLevel, lodQuadBuilder); + CompletableFuture uploadFuture = LodBufferContainer.tryMakeAndUploadBuffersAsync(this.pos, this.clientLevel, opaqueBuffers, transparentBuffers); uploadFuture.whenComplete((bufferContainer, e) -> { try