diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java index 0d3e2f522..b664deba0 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuildContext.java @@ -44,6 +44,7 @@ import java.util.List; public class AngelicaChunkBuildContext extends ChunkBuildContext { public static final int NUM_PASSES = 2; + public static final int INTS_PER_VERTEX = 8; // Yoinked from Celeritas private static final int[] REVERSED_QUAD_ORDER = { 0, 3, 2, 1 }; @@ -121,7 +122,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { } @SuppressWarnings("unchecked") - public void copyRawBuffer(int[] rawBuffer, int vertexCount, int[] vertexStates, + public void copyRawBuffer(int[] rawBuffer, int vertexOffset, int vertexCount, int[] vertexStates, int[] shaderOverrideBlockIds, ChunkBuildBuffers buffers, Material material, boolean isShaderPackOverride, boolean blockAllowsSmoothLighting) { @@ -144,7 +145,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { final boolean shouldApplyDiffuse = !BlockRenderingSettings.INSTANCE.shouldDisableDirectionalShading(); final ChunkColorWriter colorEncoder = separateAo ? ChunkColorWriter.SEPARATE_AO : ChunkColorWriter.EMBEDDIUM; - int ptr = 0; + int ptr = vertexOffset * INTS_PER_VERTEX; final int numQuads = vertexCount / 4; final int blockX = blockRenderContext.localPosX; @@ -156,7 +157,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { final short originalBlockId = blockRenderContext.blockId; - int stateIdx = 0; + int stateIdx = vertexOffset; materialSelector.reset(material, isShaderPackOverride, useRenderPassOptimization); this.fluidQuadLightFace = null; @@ -250,7 +251,7 @@ public class AngelicaChunkBuildContext extends ChunkBuildContext { final Material correctMaterial = materialSelector.select(facing, sprite, hasVertexTransparency); final var builder = buffers.get(correctMaterial); - final int shaderOverrideBlockId = shaderOverrideBlockIds[quadIdx * 4]; + final int shaderOverrideBlockId = shaderOverrideBlockIds[vertexOffset + quadIdx * 4]; blockRenderContext.blockId = shaderOverrideBlockId == StateAwareTessellator.UNMAPPED_SHADER_BLOCK_ID ? -1 : shaderOverrideBlockId != -1 ? (short) shaderOverrideBlockId : originalBlockId; diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuilderMeshingTask.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuilderMeshingTask.java index 1005c5b7f..4c5ebecc2 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuilderMeshingTask.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/AngelicaChunkBuilderMeshingTask.java @@ -1,7 +1,9 @@ package com.gtnewhorizons.angelica.rendering.celeritas; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; +import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager; import com.gtnewhorizons.angelica.glsm.profiling.Tracy; +import com.gtnewhorizons.angelica.rendering.RenderThreadContext; import com.gtnewhorizons.angelica.rendering.StateAwareTessellator; import com.gtnewhorizons.angelica.mixins.interfaces.OverridesGetDistanceFrom; import com.gtnewhorizons.angelica.rendering.TileEntityRenderBoundsRegistry; @@ -254,6 +256,8 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask meshes; - try { - meshes = BuiltSectionMeshParts.groupFromBuildBuffers(buffers, - (float) (camera.x - minX), (float) (camera.y - minY), (float) (camera.z - minZ)); - } finally { - if (Tracy.ENABLED) Tracy.endZone(); - } - - if (!meshes.isEmpty()) { - renderData.hasBlockGeometry = true; - if (Tracy.ENABLED) { - long meshBytes = 0; - for (BuiltSectionMeshParts parts : meshes.values()) { - if (parts.vertexBuffer() != null) meshBytes += parts.vertexBuffer().getLength(); - } - Tracy.zoneValue(meshBytes); - } - } - SmoothBiomeColorCache.clearActiveCache(); tessellator.setTranslation(0, 0, 0); - CeleritasDebug.incrementChunkUpdateCounter(); - - return new ChunkBuildOutput(this.render, renderData, meshes, this.buildTime); + return finalizeSection(buffers, renderData, minX, minY, minZ); } catch (ReportedException ex) { throw fillCrashInfo(ex.getCrashReport(), region, blockPos); @@ -374,7 +361,7 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask>> 6] & (1L << bit)) != 0; } - ChunkJobResult completeDeferred(DeferredSectionMesh m, AngelicaChunkBuildContext ctx) { + ChunkJobResult captureDeferred(DeferredSectionMesh m) { final long start = System.nanoTime(); final int minX = this.render.getOriginX(); @@ -384,7 +371,10 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask blocks = m.blocks; + final DeferredSectionMesh.DeferredCapture capture = m.capture; + final NbtConditionalIdMap teMap = m.teMap; if (Tracy.ENABLED) { Tracy.beginZone(Z_MESH_DEFERRED_MAIN); @@ -392,51 +382,44 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask 0) { + capture.add(i * AngelicaChunkBuildContext.NUM_PASSES + pass, tessellator.rawBuffer, tessellator.vertexCount, stateTessellator.angelica$getVertexStates(), stateTessellator.angelica$getShaderOverrideBlockIds(), teBlockId); + } + tessellator.reset(); + tessellator.isDrawing = false; } - renderBlock(deferred.block(), deferred.meta(), deferred.x(), deferred.y(), deferred.z(), deferred.pass(), - tessellator, renderBlocks, m.buffers, ctx, blockRenderContext, minX, minY, minZ, - deferred.materialOverride(), deferred.isShaderPackOverride(), m.teMap, m.currentTick); } - if (Tracy.ENABLED) Tracy.beginZone(Z_MESH_FINALIZE); - final Reference2ReferenceMap meshes; - try { - meshes = BuiltSectionMeshParts.groupFromBuildBuffers(m.buffers, (float) (camera.x - minX), (float) (camera.y - minY), (float) (camera.z - minZ)); - } finally { - if (Tracy.ENABLED) Tracy.endZone(); - } - - if (!meshes.isEmpty()) { - m.renderData.hasBlockGeometry = true; - } - - CeleritasDebug.incrementChunkUpdateCounter(); - - return new ChunkJobResult.Success<>(new ChunkBuildOutput(this.render, m.renderData, meshes, this.buildTime), m.workerNanos + (System.nanoTime() - start)); + m.mainNanos = System.nanoTime() - start; + return null; } catch (ReportedException ex) { return new ChunkJobResult.Failure<>(fillCrashInfo(ex.getCrashReport(), slice, blockPos)); } catch (Throwable ex) { return new ChunkJobResult.Failure<>(fillCrashInfo(CrashReport.makeCrashReport(ex, "Encountered exception while building deferred chunk meshes"), slice, blockPos)); } finally { - ((StateAwareTessellator)tessellator).angelica$setCeleritasMeshing(false); + stateTessellator.angelica$setCeleritasMeshing(false); tessellator.setTranslation(0, 0, 0); SmoothBiomeColorCache.clearActiveCache(); RenderPassHelper.resetWorldRenderPass(); @@ -444,22 +427,105 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask teMap, long currentTick) { + ChunkBuildOutput finishDeferred(DeferredSectionMesh m, AngelicaChunkBuildContext ctx) { + final int minX = this.render.getOriginX(); + final int minY = this.render.getOriginY(); + final int minZ = this.render.getOriginZ(); - final var blockMaterial = block.getMaterial(); - final boolean isFluid = blockMaterial == net.minecraft.block.material.Material.water || blockMaterial == net.minecraft.block.material.Material.lava; + final WorldSlice slice = m.slice; + final BlockPos blockPos = new BlockPos(minX, minY, minZ); + final ChunkBuildBuffers buffers = m.buffers; + final List blocks = m.blocks; + final DeferredSectionMesh.DeferredCapture capture = m.capture; + final boolean bindSlice = !TessellatorManager.isOnMainThread(); - // Use material override if provided, otherwise derive from pass - // Lava is opaque - use solid to skip unnecessary alpha testing - final Material passMaterial; - if (materialOverride != null) { - passMaterial = materialOverride; - } else if (blockMaterial == net.minecraft.block.material.Material.lava) { - passMaterial = AngelicaRenderPassConfiguration.SOLID_MATERIAL; - } else { - passMaterial = buffers.getRenderPassConfiguration().getMaterialForRenderType(BlockRenderLayer.fromVanillaPass(pass)); + try { + ctx.setupLightPipeline(slice, minX, minY, minZ); + ctx.setupDynamicLights(minX, minY, minZ); + if (bindSlice) RenderThreadContext.set(slice); + slice.setRenderingBlock(null); + + final BlockRenderContext blockRenderContext = ctx.getBlockRenderContext(); + final int[] raw = capture.raw.elements(); + final int[] vertexStates = capture.vertexStates.elements(); + final int[] shaderBlockIds = capture.shaderBlockIds.elements(); + + for (int i = 0, n = capture.blockCount(); i < n; i++) { + final int entry = capture.blockIndex.getInt(i); + final DeferredBlock deferred = blocks.get(entry / AngelicaChunkBuildContext.NUM_PASSES); + final Block block = deferred.block(); + final int x = deferred.x(), y = deferred.y(), z = deferred.z(); + blockPos.set(x, y, z); + slice.setRenderingBlock(block); + final Material passMaterial = resolvePassMaterial(block, entry % AngelicaChunkBuildContext.NUM_PASSES, buffers, deferred.materialOverride()); + final ContextAwareChunkVertexEncoder contextEncoder = prepareEncode(block, deferred.meta(), x, y, z, slice, buffers, blockRenderContext, passMaterial, capture.teBlockIds.getInt(i)); + encodeTessellated(raw, capture.blockStart.getInt(i), capture.blockVertexCount.getInt(i), vertexStates, shaderBlockIds, block, buffers, ctx, passMaterial, deferred.isShaderPackOverride(), contextEncoder); + } + + return finalizeSection(buffers, m.renderData, minX, minY, minZ); + } catch (ReportedException ex) { + throw fillCrashInfo(ex.getCrashReport(), slice, blockPos); + } catch (Throwable ex) { + throw fillCrashInfo(CrashReport.makeCrashReport(ex, "Encountered exception while building deferred chunk meshes"), slice, blockPos); + } finally { + if (bindSlice) RenderThreadContext.clear(); + } + } + + private ChunkBuildOutput finalizeSection(ChunkBuildBuffers buffers, AngelicaBuiltRenderSectionData renderData, int minX, int minY, int minZ) { + if (Tracy.ENABLED) Tracy.beginZone(Z_MESH_FINALIZE); + final Reference2ReferenceMap meshes; + try { + meshes = BuiltSectionMeshParts.groupFromBuildBuffers(buffers, (float) (camera.x - minX), (float) (camera.y - minY), (float) (camera.z - minZ)); + } finally { + if (Tracy.ENABLED) Tracy.endZone(); } + if (!meshes.isEmpty()) { + renderData.hasBlockGeometry = true; + if (Tracy.ENABLED) { + long meshBytes = 0; + for (BuiltSectionMeshParts parts : meshes.values()) { + if (parts.vertexBuffer() != null) meshBytes += parts.vertexBuffer().getLength(); + } + Tracy.zoneValue(meshBytes); + } + } + + CeleritasDebug.incrementChunkUpdateCounter(); + + return new ChunkBuildOutput(this.render, renderData, meshes, this.buildTime); + } + + protected void renderBlock(Block block, int metadata, int x, int y, int z, int pass, Tessellator tessellator, RenderBlocks renderBlocks, ChunkBuildBuffers buffers, AngelicaChunkBuildContext buildContext, BlockRenderContext blockRenderContext, int originX, int originY, int originZ, Material materialOverride, boolean isShaderPackOverride, NbtConditionalIdMap teMap, long currentTick) { + final Material passMaterial = resolvePassMaterial(block, pass, buffers, materialOverride); + final int teBlockId = resolveTeBlockId(teMap, renderBlocks.blockAccess, block, metadata, x, y, z, buffers, pass, materialOverride, currentTick); + final ContextAwareChunkVertexEncoder contextEncoder = prepareEncode(block, metadata, x, y, z, renderBlocks.blockAccess, buffers, blockRenderContext, passMaterial, teBlockId); + drawBlock(block, x, y, z, pass, tessellator, renderBlocks); + encodeTessellated(tessellator.rawBuffer, 0, tessellator.vertexCount, ((StateAwareTessellator) tessellator).angelica$getVertexStates(), + ((StateAwareTessellator) tessellator).angelica$getShaderOverrideBlockIds(), block, buffers, buildContext, passMaterial, isShaderPackOverride, contextEncoder); + tessellator.reset(); + tessellator.isDrawing = false; + } + + private static Material resolvePassMaterial(Block block, int pass, ChunkBuildBuffers buffers, Material materialOverride) { + // Use material override if provided, otherwise derive from pass + // Lava is opaque - use solid to skip unnecessary alpha testing + if (materialOverride != null) { + return materialOverride; + } else if (block.getMaterial() == net.minecraft.block.material.Material.lava) { + return AngelicaRenderPassConfiguration.SOLID_MATERIAL; + } else { + return buffers.getRenderPassConfiguration().getMaterialForRenderType(BlockRenderLayer.fromVanillaPass(pass)); + } + } + + private static boolean isFluid(Block block) { + final var blockMaterial = block.getMaterial(); + return blockMaterial == net.minecraft.block.material.Material.water || blockMaterial == net.minecraft.block.material.Material.lava; + } + + private static ContextAwareChunkVertexEncoder prepareEncode(Block block, int metadata, int x, int y, int z, IBlockAccess blockAccess, ChunkBuildBuffers buffers, BlockRenderContext blockRenderContext, Material passMaterial, int teBlockId) { final var encoder = buffers.get(passMaterial).getEncoder(); final ContextAwareChunkVertexEncoder contextEncoder = (encoder instanceof ContextAwareChunkVertexEncoder) ? (ContextAwareChunkVertexEncoder) encoder : null; @@ -469,43 +535,49 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask teMap, IBlockAccess blockAccess, Block block, int metadata, int x, int y, int z, ChunkBuildBuffers buffers, int pass, Material materialOverride, long currentTick) { + // Check for TileEntity NBT-conditional shader block ID override + if (teMap == null || !teMap.hasConditions(block) || isFluid(block)) return -1; + if (!(buffers.get(resolvePassMaterial(block, pass, buffers, materialOverride)).getEncoder() instanceof ContextAwareChunkVertexEncoder)) return -1; + final long packedPos = BlockRenderingSettings.packBlockPos(x, y, z); + int teBlockId = BlockRenderingSettings.getCachedTeNbtId(packedPos, currentTick); + + if (teBlockId == BlockRenderingSettings.CACHE_MISS) { + final TileEntity te = blockAccess.getTileEntity(x, y, z); + if (te != null) { + final NBTTagCompound teNbt = new NBTTagCompound(); + te.writeToNBT(teNbt); + teBlockId = teMap.resolve(block, teNbt, metadata); + } else { + teBlockId = -1; + } + BlockRenderingSettings.cacheTeNbtId(packedPos, teBlockId, currentTick); + } + return teBlockId; + } + + private void drawBlock(Block block, int x, int y, int z, int pass, Tessellator tessellator, RenderBlocks renderBlocks) { setRenderPass(pass); // Trigger side effects from canRenderInPass (some ISBRHs like BuildCraft set global state in this method that gets read elsewhere renderWorldBlock) block.canRenderInPass(pass); @@ -518,15 +590,13 @@ public abstract class AngelicaChunkBuilderMeshingTask extends ChunkBuilderTask new AngelicaChunkBuildContext(configuration, world), () -> new WorldSlice(world), AngelicaRenderQueue::submit, ((RenderSectionManagerAccessor) this).angelica$getBuildResults()); + this.deferredScheduler = new DeferredMeshScheduler(configuration, getBuilder(), () -> new WorldSlice(world), AngelicaRenderQueue::submit, ((RenderSectionManagerAccessor) this).angelica$getBuildResults()); } public static AngelicaRenderSectionManager create(ChunkVertexType vertexType, WorldClient world, int renderDistance, CommandList commandList) { @@ -225,6 +226,7 @@ public class AngelicaRenderSectionManager extends RenderSectionManager { this.deferredScheduler.markDestroyed(); super.destroy(); this.deferredScheduler.runImportant(); + this.deferredScheduler.releaseOrphans(); } @Override @@ -350,5 +352,6 @@ public class AngelicaRenderSectionManager extends RenderSectionManager { Tracy.plotInt(P_MESH_DEVICE_ALLOCATED, mem.deviceAllocated + mem.indexAllocated); Tracy.plotInt(P_DRAW_REGION_BUFFERS, mem.bufferCount); Tracy.plotInt(P_MESH_DEFERRED_PENDING, this.deferredScheduler.pendingCount()); + Tracy.plotInt(P_MESH_DEFERRED_FINISHING, this.deferredScheduler.finishingCount()); } } diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredBlock.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredBlock.java index 4524aacce..60c2c48a6 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredBlock.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredBlock.java @@ -3,4 +3,8 @@ package com.gtnewhorizons.angelica.rendering.celeritas; import net.minecraft.block.Block; import org.embeddedt.embeddium.impl.render.chunk.terrain.material.Material; -public record DeferredBlock(int x, int y, int z, Block block, int meta, int pass, Material materialOverride, boolean isShaderPackOverride) {} +public record DeferredBlock(int x, int y, int z, Block block, int meta, int passMask, Material materialOverride) { + public boolean isShaderPackOverride() { + return materialOverride != null; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredFinishTask.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredFinishTask.java new file mode 100644 index 000000000..0b64e4e80 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredFinishTask.java @@ -0,0 +1,42 @@ +package com.gtnewhorizons.angelica.rendering.celeritas; + +import com.gtnewhorizons.angelica.glsm.profiling.Tracy; +import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext; +import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput; +import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobCollector; +import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult; +import org.embeddedt.embeddium.impl.render.chunk.compile.tasks.ChunkBuilderTask; +import org.embeddedt.embeddium.impl.util.task.CancellationToken; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public final class DeferredFinishTask extends ChunkBuilderTask implements Consumer> { + private static final Tracy.ZoneId Z_MESH_DEFERRED_FINISH = Tracy.zoneId("meshDeferredFinish"); + + private final DeferredSectionMesh mesh; + private final DeferredMeshScheduler scheduler; + private final @Nullable ChunkJobCollector collector; + + public DeferredFinishTask(DeferredSectionMesh mesh, DeferredMeshScheduler scheduler, @Nullable ChunkJobCollector collector) { + this.mesh = mesh; + this.scheduler = scheduler; + this.collector = collector; + } + + @Override + public ChunkBuildOutput execute(ChunkBuildContext context, CancellationToken cancellationToken) { + if (mesh.isObsolete()) return null; + if (Tracy.ENABLED) Tracy.beginZone(Z_MESH_DEFERRED_FINISH); + try { + return mesh.finish(context); + } finally { + if (Tracy.ENABLED) Tracy.endZone(); + } + } + + @Override + public void accept(@Nullable ChunkJobResult result) { + scheduler.finished(mesh, result, collector); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshScheduler.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshScheduler.java index d78d13d5d..4d89cfa88 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshScheduler.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshScheduler.java @@ -2,37 +2,56 @@ package com.gtnewhorizons.angelica.rendering.celeritas; import org.embeddedt.embeddium.impl.render.chunk.RenderPassConfiguration; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers; +import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkTaskOutput; +import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkBuilder; +import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobCollector; import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult; +import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobTyped; +import com.gtnewhorizons.angelica.rendering.celeritas.DeferredSectionMesh.DeferredCapture; import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice; +import org.jetbrains.annotations.Nullable; +import java.util.Comparator; import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Supplier; public class DeferredMeshScheduler { - private final Supplier mainContextFactory; + /** Must stay below every distance priority: queued finishes hold pooled buffers and slices. */ + private static final long FINISH_PRIORITY = -1L; + + private static final Comparator NEAREST_FIRST = Comparator.comparingLong(m -> m.priority); + + private final ChunkBuilder builder; private final Consumer mainThreadQueue; private final Queue> results; + private final Consumer> resultSink; private final ObjectPool buffers; private final ObjectPool slices; + private final ObjectPool captures; private final ConcurrentLinkedQueue important = new ConcurrentLinkedQueue<>(); - private final ConcurrentLinkedQueue normal = new ConcurrentLinkedQueue<>(); + private final PriorityBlockingQueue normal = new PriorityBlockingQueue<>(11, NEAREST_FIRST); + private final Set inFlight = ConcurrentHashMap.newKeySet(); private final Runnable runNext = this::runNext; private final AtomicInteger pending = new AtomicInteger(); - private AngelicaChunkBuildContext mainContext; private boolean destroyed; - public DeferredMeshScheduler(RenderPassConfiguration config, Supplier mainContextFactory, Supplier sliceFactory, Consumer mainThreadQueue, Queue> results) { - this.mainContextFactory = mainContextFactory; + public DeferredMeshScheduler(RenderPassConfiguration config, ChunkBuilder builder, Supplier sliceFactory, Consumer mainThreadQueue, Queue> results) { + this.builder = builder; this.mainThreadQueue = mainThreadQueue; this.results = results; - this.buffers = new ObjectPool<>(() -> new ChunkBuildBuffers(config)); - this.slices = new ObjectPool<>(sliceFactory); + this.resultSink = results::add; + this.buffers = new ObjectPool<>(() -> new ChunkBuildBuffers(config), Integer.MAX_VALUE); + this.slices = new ObjectPool<>(sliceFactory, Integer.MAX_VALUE); + this.captures = new ObjectPool<>(DeferredCapture::new, Math.max(1, builder.getTotalThreadCount()) * 2); } public ChunkBuildBuffers acquireBuffers() { @@ -59,17 +78,30 @@ public class DeferredMeshScheduler { } public void runImportant() { + if (important.isEmpty()) return; + final ChunkJobCollector collector = new ChunkJobCollector(Integer.MAX_VALUE, resultSink); DeferredSectionMesh m; while ((m = important.poll()) != null) { - run(m); + run(m, collector); } + collector.awaitCompletion(builder); } public void markDestroyed() { destroyed = true; - if (mainContext != null) { - mainContext.cleanup(); - mainContext = null; + } + + /** Call after the builder shut down: its queued jobs were dropped without reaching their consumers. */ + public void releaseOrphans() { + DeferredSectionMesh m; + while ((m = important.poll()) != null) { + release(m); + } + while ((m = normal.poll()) != null) { + release(m); + } + for (final DeferredSectionMesh orphan : inFlight) { + if (inFlight.remove(orphan)) release(orphan); } } @@ -77,29 +109,67 @@ public class DeferredMeshScheduler { return pending.get(); } - private AngelicaChunkBuildContext mainContext() { - if (mainContext == null) { - mainContext = mainContextFactory.get(); + public int finishingCount() { + return inFlight.size(); + } + + void finished(DeferredSectionMesh m, @Nullable ChunkJobResult result, @Nullable ChunkJobCollector collector) { + final long nanos = m.workerNanos + m.mainNanos; + ChunkJobResult out = result instanceof ChunkJobResult.Success success ? new ChunkJobResult.Success<>(success.output(), nanos + success.executionTimeNanos()) : result; + if (inFlight.remove(m)) { + // Runs as a celeritas job consumer on a worker, where an escaping throwable kills the worker thread. + try { + release(m); + } catch (Throwable t) { + out = new ChunkJobResult.Failure<>(t); + } + } + if (collector != null) { + collector.onJobFinished(out); + } else if (out != null) { + results.add(out); } - return mainContext; } private void runNext() { final DeferredSectionMesh m = normal.poll(); if (m != null) { - run(m); + run(m, null); } } - private void run(DeferredSectionMesh m) { + private void run(DeferredSectionMesh m, @Nullable ChunkJobCollector collector) { + boolean scheduled = false; try { if (destroyed || m.isObsolete()) return; - results.add(m.complete(mainContext())); + m.capture = captures.acquire(); + final ChunkJobResult failure = m.capture(); + if (failure != null) { + results.add(failure); + return; + } + final DeferredFinishTask task = new DeferredFinishTask(m, this, collector); + inFlight.add(m); + final ChunkJobTyped job = builder.scheduleTask(task, m.important, FINISH_PRIORITY, task); + scheduled = true; + if (collector != null) collector.addSubmittedJob(job); } finally { - releaseBuffers(m.buffers); - m.slice.releaseContext(); - slices.release(m.slice); - pending.decrementAndGet(); + if (!scheduled) { + inFlight.remove(m); + release(m); + } } } + + private void release(DeferredSectionMesh m) { + releaseBuffers(m.buffers); + m.slice.releaseContext(); + slices.release(m.slice); + if (m.capture != null) { + m.capture.clear(); + captures.release(m.capture); + m.capture = null; + } + pending.decrementAndGet(); + } } diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMesh.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMesh.java index 457d58871..0e30edcc4 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMesh.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMesh.java @@ -1,9 +1,11 @@ package com.gtnewhorizons.angelica.rendering.celeritas; import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice; +import it.unimi.dsi.fastutil.ints.IntArrayList; import net.coderbot.iris.block_rendering.NbtConditionalIdMap; import net.minecraft.block.Block; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers; +import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput; import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult; import org.embeddedt.embeddium.impl.util.task.CancellationToken; @@ -20,9 +22,12 @@ public class DeferredSectionMesh { public final NbtConditionalIdMap teMap; public final long currentTick; public final boolean important; + public final long priority; public final long workerNanos; + public DeferredCapture capture; + public long mainNanos; - public DeferredSectionMesh(AngelicaChunkBuilderMeshingTask task, CancellationToken token, ChunkBuildBuffers buffers, WorldSlice slice, AngelicaBuiltRenderSectionData renderData, List blocks, NbtConditionalIdMap teMap, long currentTick, boolean important, long workerNanos) { + public DeferredSectionMesh(AngelicaChunkBuilderMeshingTask task, CancellationToken token, ChunkBuildBuffers buffers, WorldSlice slice, AngelicaBuiltRenderSectionData renderData, List blocks, NbtConditionalIdMap teMap, long currentTick, boolean important, long priority, long workerNanos) { this.task = task; this.token = token; this.buffers = buffers; @@ -32,6 +37,7 @@ public class DeferredSectionMesh { this.teMap = teMap; this.currentTick = currentTick; this.important = important; + this.priority = priority; this.workerNanos = workerNanos; } @@ -39,7 +45,57 @@ public class DeferredSectionMesh { return token.isCancelled() || task.render.isDisposed() || task.render.getLastBuiltFrame() > task.buildTime; } - public ChunkJobResult complete(AngelicaChunkBuildContext ctx) { - return task.completeDeferred(this, ctx); + /** Main thread only; returns a failure or null. */ + public ChunkJobResult capture() { + return task.captureDeferred(this); + } + + public ChunkBuildOutput finish(ChunkBuildContext context) { + return task.finishDeferred(this, (AngelicaChunkBuildContext) context); + } + + public static final class DeferredCapture { + public static final int VERTEX_TRIM = 1 << 17; + + public final IntArrayList raw = new IntArrayList(); + public final IntArrayList vertexStates = new IntArrayList(); + public final IntArrayList shaderBlockIds = new IntArrayList(); + public final IntArrayList blockIndex = new IntArrayList(); + public final IntArrayList blockStart = new IntArrayList(); + public final IntArrayList blockVertexCount = new IntArrayList(); + public final IntArrayList teBlockIds = new IntArrayList(); + + public void add(int blockIdx, int[] rawBuffer, int vertexCount, int[] states, int[] shaderIds, int teBlockId) { + blockIndex.add(blockIdx); + blockStart.add(vertexStates.size()); + blockVertexCount.add(vertexCount); + teBlockIds.add(teBlockId); + raw.addElements(raw.size(), rawBuffer, 0, vertexCount * AngelicaChunkBuildContext.INTS_PER_VERTEX); + vertexStates.addElements(vertexStates.size(), states, 0, vertexCount); + shaderBlockIds.addElements(shaderBlockIds.size(), shaderIds, 0, vertexCount); + } + + public int blockCount() { + return blockIndex.size(); + } + + public int vertexCount() { + return vertexStates.size(); + } + + public void clear() { + reset(raw, VERTEX_TRIM * AngelicaChunkBuildContext.INTS_PER_VERTEX); + reset(vertexStates, VERTEX_TRIM); + reset(shaderBlockIds, VERTEX_TRIM); + reset(blockIndex, VERTEX_TRIM); + reset(blockStart, VERTEX_TRIM); + reset(blockVertexCount, VERTEX_TRIM); + reset(teBlockIds, VERTEX_TRIM); + } + + private static void reset(IntArrayList list, int cap) { + list.clear(); + if (list.elements().length > cap) list.trim(cap); + } } } diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPool.java b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPool.java index 50d39045e..e8ecd6251 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPool.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPool.java @@ -1,22 +1,32 @@ package com.gtnewhorizons.angelica.rendering.celeritas; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; final class ObjectPool { - private final ConcurrentLinkedQueue free = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedDeque free = new ConcurrentLinkedDeque<>(); + private final AtomicInteger freeCount = new AtomicInteger(); private final Supplier factory; + private final int maxFree; - ObjectPool(Supplier factory) { + ObjectPool(Supplier factory, int maxFree) { this.factory = factory; + this.maxFree = maxFree; } T acquire() { - final T t = free.poll(); - return t != null ? t : factory.get(); + final T t = free.pollFirst(); + if (t == null) return factory.get(); + freeCount.decrementAndGet(); + return t; } void release(T t) { - free.add(t); + if (freeCount.incrementAndGet() > maxFree) { + freeCount.decrementAndGet(); + return; + } + free.offerFirst(t); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshSchedulerTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshSchedulerTest.java index bca05b75e..6f0768a49 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshSchedulerTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredMeshSchedulerTest.java @@ -3,8 +3,10 @@ package com.gtnewhorizons.angelica.rendering.celeritas; import com.gtnewhorizons.angelica.rendering.celeritas.world.WorldSlice; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildBuffers; +import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildContext; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkBuildOutput; import org.embeddedt.embeddium.impl.render.chunk.compile.ChunkTaskOutput; +import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkBuilder; import org.embeddedt.embeddium.impl.render.chunk.compile.executor.ChunkJobResult; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -13,160 +15,206 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.function.Supplier; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; class DeferredMeshSchedulerTest { - private static DeferredSectionMesh mesh(ChunkBuildBuffers buffers, WorldSlice slice, boolean important, boolean obsolete, ChunkJobResult result) { - return new DeferredSectionMesh(null, null, buffers, slice, null, null, null, 0L, important, 0L) { + private static final int NO_WORKERS = -1; + + private record Row(String label, boolean important, boolean obsoleteAtCapture, boolean obsoleteAtFinish, boolean captureFails, boolean finishThrows, boolean destroyed, int expectedResults, Class expectedClass) { + @Override + public String toString() { + return label; + } + } + + private static final class Probe { + final AtomicInteger obsoleteChecks = new AtomicInteger(); + final AtomicReference finishThread = new AtomicReference<>(); + final List captureOrder = new ArrayList<>(); + final List blocksOnCapture = new ArrayList<>(); + } + + private static DeferredSectionMesh mesh(Probe probe, ChunkBuildBuffers buffers, WorldSlice slice, boolean important, long priority, boolean obsoleteAtCapture, boolean obsoleteAtFinish, boolean captureFails, boolean finishThrows) { + return new DeferredSectionMesh(null, null, buffers, slice, null, null, null, 0L, important, priority, 0L) { @Override public boolean isObsolete() { - return obsolete; + return probe.obsoleteChecks.getAndIncrement() == 0 ? obsoleteAtCapture : obsoleteAtFinish; } @Override - public ChunkJobResult complete(AngelicaChunkBuildContext ctx) { - return result; + public ChunkJobResult capture() { + probe.captureOrder.add(priority); + probe.blocksOnCapture.add(capture.blockCount()); + capture.add(0, new int[8 * 4], 4, new int[4], new int[4], 1); + return captureFails ? new ChunkJobResult.Failure<>(new RuntimeException("capture")) : null; + } + + @Override + public ChunkBuildOutput finish(ChunkBuildContext context) { + probe.finishThread.set(Thread.currentThread()); + if (finishThrows) throw new IllegalStateException("finish"); + return Mockito.mock(ChunkBuildOutput.class); } }; } - private static ChunkJobResult success() { - return new ChunkJobResult.Success<>(null, 0L); - } - - private static ChunkJobResult failure() { - return new ChunkJobResult.Failure<>(new RuntimeException("x")); - } - private static final class Harness { final ArrayDeque queue = new ArrayDeque<>(); final Queue> results = new ConcurrentLinkedDeque<>(); - final DeferredMeshScheduler scheduler = new DeferredMeshScheduler(null, () -> null, () -> Mockito.mock(WorldSlice.class), queue::add, results); + final Probe probe = new Probe(); + final AtomicInteger slicesCreated = new AtomicInteger(); + final ChunkBuilder builder; + final DeferredMeshScheduler scheduler; + + Harness(int threads) { + builder = new ChunkBuilder(ChunkBuilder.ManagedBlocker.NONE, () -> new ChunkBuildContext(null), threads); + scheduler = new DeferredMeshScheduler(null, builder, () -> { + slicesCreated.incrementAndGet(); + return Mockito.mock(WorldSlice.class); + }, queue::add, results); + } + + DeferredSectionMesh submit(boolean important, long priority) { + final DeferredSectionMesh m = mesh(probe, scheduler.acquireBuffers(), scheduler.acquireSlice(), important, priority, false, false, false, false); + scheduler.submit(m); + return m; + } } - @Test - void importantItemRunsOnRunImportant() { - Harness h = new Harness(); - - ChunkBuildBuffers buffers = h.scheduler.acquireBuffers(); - DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), true, false, success()); - h.scheduler.submit(m); - - assertEquals(0, h.queue.size()); - assertEquals(1, h.scheduler.pendingCount()); - - h.scheduler.runImportant(); - - assertEquals(1, h.results.size()); - assertTrue(h.results.peek() instanceof ChunkJobResult.Success); - assertEquals(0, h.scheduler.pendingCount()); - assertSame(buffers, h.scheduler.acquireBuffers()); + private static Stream rows() { + return Stream.of( + Arguments.of(new Row("normal", false, false, false, false, false, false, 1, ChunkJobResult.Success.class)), + Arguments.of(new Row("important", true, false, false, false, false, false, 1, ChunkJobResult.Success.class)), + Arguments.of(new Row("capture fails", false, false, false, true, false, false, 1, ChunkJobResult.Failure.class)), + Arguments.of(new Row("normal finish throws", false, false, false, false, true, false, 1, ChunkJobResult.Failure.class)), + Arguments.of(new Row("important finish throws", true, false, false, false, true, false, 1, ChunkJobResult.Failure.class)), + Arguments.of(new Row("obsolete at capture", false, true, false, false, false, false, 0, null)), + Arguments.of(new Row("normal obsolete at finish", false, false, true, false, false, false, 0, null)), + Arguments.of(new Row("important obsolete at finish", true, false, true, false, false, false, 0, null)), + Arguments.of(new Row("normal destroyed", false, false, false, false, false, true, 0, null)), + Arguments.of(new Row("important destroyed", true, false, false, false, false, true, 0, null))); } @ParameterizedTest(name = "{0}") - @MethodSource("normalResults") - void normalItemRunsThroughMainThreadQueue(String label, Supplier> result, Class expected) { - Harness h = new Harness(); + @MethodSource("rows") + void submittedMeshFinishesAndReleasesEverything(Row row) { + Harness h = new Harness(NO_WORKERS); ChunkBuildBuffers buffers = h.scheduler.acquireBuffers(); - DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, false, result.get()); + WorldSlice slice = h.scheduler.acquireSlice(); + DeferredSectionMesh m = mesh(h.probe, buffers, slice, row.important, 0L, row.obsoleteAtCapture, row.obsoleteAtFinish, row.captureFails, row.finishThrows); h.scheduler.submit(m); + if (row.destroyed) h.scheduler.markDestroyed(); - assertEquals(1, h.queue.size()); + assertEquals(row.important ? 0 : 1, h.queue.size()); + if (row.important) { + h.scheduler.runImportant(); + } else { + h.queue.poll().run(); + assertEquals(row.captureFails ? 1 : 0, h.results.size()); + h.builder.tick(); + } + + assertEquals(row.expectedResults, h.results.size()); + if (row.expectedClass != null) assertTrue(row.expectedClass.isInstance(h.results.peek())); + assertEquals(0, h.scheduler.pendingCount()); + assertEquals(0, h.scheduler.finishingCount()); + assertNull(m.capture); + assertSame(buffers, h.scheduler.acquireBuffers()); + assertSame(slice, h.scheduler.acquireSlice()); + Mockito.verify(slice, Mockito.times(1)).releaseContext(); + boolean finishRan = !row.captureFails && !row.obsoleteAtCapture && !row.destroyed && !row.obsoleteAtFinish; + assertEquals(finishRan ? Thread.currentThread() : null, h.probe.finishThread.get()); + } + + @Test + void finishRunsOnWorkerThread() throws InterruptedException { + Harness h = new Harness(1); + try { + h.submit(false, 0L); + h.queue.poll().run(); + + long deadline = System.nanoTime() + 10_000_000_000L; + while (h.results.isEmpty() && System.nanoTime() < deadline) Thread.sleep(5); + + assertTrue(h.results.peek() instanceof ChunkJobResult.Success); + assertTrue(h.probe.finishThread.get() instanceof ChunkBuilder.WorkerThread); + assertEquals(0, h.scheduler.pendingCount()); + } finally { + h.builder.shutdown(); + } + } + + @Test + void normalQueueDrainsNearestFirst() { + Harness h = new Harness(NO_WORKERS); + + for (long p : new long[] {30L, 10L, 20L}) h.submit(false, p); + while (!h.queue.isEmpty()) { + h.queue.poll().run(); + h.builder.tick(); + } + + assertEquals(List.of(10L, 20L, 30L), h.probe.captureOrder); + assertEquals(List.of(0, 0, 0), h.probe.blocksOnCapture); + } + + @Test + void slicesSurviveBurstsBeyondWorkerBound() { + Harness h = new Harness(NO_WORKERS); + + for (int i = 0; i < 5; i++) h.submit(false, i); + while (!h.queue.isEmpty()) h.queue.poll().run(); + h.builder.tick(); + for (int i = 0; i < 5; i++) h.scheduler.acquireSlice(); + + assertEquals(5, h.slicesCreated.get()); + } + + @Test + void importantReleaseFailureSurfacesAndReturns() { + Harness h = new Harness(NO_WORKERS); + + DeferredSectionMesh m = h.submit(true, 0L); + Mockito.doThrow(new IllegalStateException("release")).when(m.slice).releaseContext(); h.scheduler.runImportant(); - assertEquals(0, h.results.size()); - - h.queue.poll().run(); assertEquals(1, h.results.size()); - assertTrue(expected.isInstance(h.results.peek())); - assertEquals(0, h.scheduler.pendingCount()); - } - - private static Stream normalResults() { - return Stream.of( - Arguments.of("success", (Supplier>) DeferredMeshSchedulerTest::success, ChunkJobResult.Success.class), - Arguments.of("failure is recorded and counters still decrement", (Supplier>) DeferredMeshSchedulerTest::failure, ChunkJobResult.Failure.class)); + assertTrue(h.results.peek() instanceof ChunkJobResult.Failure); + assertEquals(0, h.scheduler.finishingCount()); } @Test - void importantItemAfterDestroyReleasesBuffers() { - Harness h = new Harness(); + void releaseOrphansReleasesEachMeshOnce() { + Harness h = new Harness(NO_WORKERS); - ChunkBuildBuffers buffers = h.scheduler.acquireBuffers(); - WorldSlice slice = h.scheduler.acquireSlice(); - DeferredSectionMesh m = mesh(buffers, slice, true, false, success()); - h.scheduler.submit(m); - h.scheduler.markDestroyed(); - h.scheduler.runImportant(); + DeferredSectionMesh queued = h.submit(false, 2L); + DeferredSectionMesh inFlight = h.submit(false, 1L); + h.queue.poll().run(); + assertEquals(1, h.scheduler.finishingCount()); + h.builder.shutdown(); + h.scheduler.releaseOrphans(); + h.scheduler.releaseOrphans(); + + assertEquals(0, h.scheduler.pendingCount()); + assertEquals(0, h.scheduler.finishingCount()); assertEquals(0, h.results.size()); - assertEquals(0, h.scheduler.pendingCount()); - assertSame(buffers, h.scheduler.acquireBuffers()); - assertSame(slice, h.scheduler.acquireSlice()); - Mockito.verify(slice).releaseContext(); - } - - @Test - void markDestroyedDiscardsQueuedItemAndReturnsBuffersToPool() { - Harness h = new Harness(); - - ChunkBuildBuffers buffers = h.scheduler.acquireBuffers(); - DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, false, success()); - h.scheduler.submit(m); - h.scheduler.markDestroyed(); - h.queue.poll().run(); - - assertEquals(0, h.results.size()); - assertEquals(0, h.scheduler.pendingCount()); - assertSame(buffers, h.scheduler.acquireBuffers()); - } - - @Test - void obsoleteItemIsDiscarded() { - Harness h = new Harness(); - - ChunkBuildBuffers buffers = h.scheduler.acquireBuffers(); - DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, true, success()); - h.scheduler.submit(m); - h.queue.poll().run(); - - assertEquals(0, h.results.size()); - assertEquals(0, h.scheduler.pendingCount()); - } - - @Test - void acquireBuffersAfterCompletedRunReturnsTheSameInstance() { - Harness h = new Harness(); - - ChunkBuildBuffers buffers = h.scheduler.acquireBuffers(); - DeferredSectionMesh m = mesh(buffers, h.scheduler.acquireSlice(), false, false, success()); - h.scheduler.submit(m); - h.queue.poll().run(); - - assertSame(buffers, h.scheduler.acquireBuffers()); - } - - @Test - void runReturnsSliceToPool() { - Harness h = new Harness(); - - ChunkBuildBuffers buffers = h.scheduler.acquireBuffers(); - WorldSlice slice = h.scheduler.acquireSlice(); - DeferredSectionMesh m = mesh(buffers, slice, false, false, success()); - h.scheduler.submit(m); - h.queue.poll().run(); - - assertSame(slice, h.scheduler.acquireSlice()); - Mockito.verify(slice).releaseContext(); + Mockito.verify(queued.slice, Mockito.times(1)).releaseContext(); + Mockito.verify(inFlight.slice, Mockito.times(1)).releaseContext(); + assertNull(inFlight.capture); } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMeshTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMeshTest.java index 76aede482..59b915c8f 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMeshTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/DeferredSectionMeshTest.java @@ -8,10 +8,15 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import com.gtnewhorizons.angelica.rendering.celeritas.DeferredSectionMesh.DeferredCapture; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import org.junit.jupiter.api.Test; + import java.util.function.Consumer; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class DeferredSectionMeshTest { @@ -26,7 +31,7 @@ class DeferredSectionMeshTest { @Override public void setCancelled() {} }; - return new DeferredSectionMesh(task, token, null, null, null, null, null, 0L, false, 0L); + return new DeferredSectionMesh(task, token, null, null, null, null, null, 0L, false, 0L, 0L); } static Stream sections() { @@ -45,4 +50,63 @@ class DeferredSectionMeshTest { setup.accept(section); assertEquals(expected, mesh(section, 5, cancelled).isObsolete()); } + + @Test + void captureCopiesBlocks() { + final int[] counts = {4, 12}; + final DeferredCapture capture = new DeferredCapture(); + final IntArrayList expectedRaw = new IntArrayList(); + final IntArrayList expectedStates = new IntArrayList(); + final IntArrayList expectedShader = new IntArrayList(); + final IntArrayList expectedStart = new IntArrayList(); + int total = 0; + for (int b = 0; b < counts.length; b++) { + final int n = counts[b]; + final int[] raw = new int[n * AngelicaChunkBuildContext.INTS_PER_VERTEX + 5]; + final int[] states = new int[n + 3]; + final int[] shader = new int[n + 3]; + for (int i = 0; i < raw.length; i++) raw[i] = b * 100000 + i; + for (int i = 0; i < states.length; i++) { + states[i] = b * 1000 + 500000 + i; + shader[i] = b * 1000 + 900000 + i; + } + capture.add(b, raw, n, states, shader, 7000 + b); + expectedStart.add(total); + for (int i = 0; i < n * AngelicaChunkBuildContext.INTS_PER_VERTEX; i++) expectedRaw.add(raw[i]); + for (int i = 0; i < n; i++) { + expectedStates.add(states[i]); + expectedShader.add(shader[i]); + } + total += n; + } + + assertEquals(counts.length, capture.blockCount()); + assertEquals(total, capture.vertexCount()); + assertEquals(expectedStart, capture.blockStart); + assertEquals(expectedRaw, capture.raw); + assertEquals(expectedStates, capture.vertexStates); + assertEquals(expectedShader, capture.shaderBlockIds); + for (int b = 0; b < counts.length; b++) { + assertEquals(b, capture.blockIndex.getInt(b)); + assertEquals(counts[b], capture.blockVertexCount.getInt(b)); + assertEquals(7000 + b, capture.teBlockIds.getInt(b)); + } + } + + @Test + void clearEmptiesAndTrimsOversizedLists() { + final DeferredCapture capture = new DeferredCapture(); + final int vertices = DeferredCapture.VERTEX_TRIM + 1000; + capture.add(0, new int[vertices * AngelicaChunkBuildContext.INTS_PER_VERTEX], vertices, new int[vertices], new int[vertices], 1); + assertTrue(capture.vertexStates.elements().length > DeferredCapture.VERTEX_TRIM); + + capture.clear(); + + assertEquals(0, capture.raw.size()); + assertTrue(capture.raw.elements().length <= DeferredCapture.VERTEX_TRIM * AngelicaChunkBuildContext.INTS_PER_VERTEX); + for (IntArrayList list : new IntArrayList[] {capture.vertexStates, capture.shaderBlockIds}) { + assertEquals(0, list.size()); + assertTrue(list.elements().length <= DeferredCapture.VERTEX_TRIM); + } + } } diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPoolTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPoolTest.java index d4ea47559..36d2f4348 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPoolTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/celeritas/ObjectPoolTest.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.rendering.celeritas; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -9,6 +10,7 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; class ObjectPoolTest { @@ -20,7 +22,7 @@ class ObjectPoolTest { ObjectPool pool = new ObjectPool<>(() -> { calls.incrementAndGet(); return new Object(); - }); + }, n); Object[] acquired = new Object[n]; for (int i = 0; i < n; i++) { @@ -36,4 +38,25 @@ class ObjectPoolTest { for (Object o : acquired) assertTrue(reacquired.contains(o)); assertEquals(n, calls.get()); } + + @Test + void releaseIsLifoAndDropsBeyondMaxFree() { + AtomicInteger calls = new AtomicInteger(); + ObjectPool pool = new ObjectPool<>(() -> { + calls.incrementAndGet(); + return new Object(); + }, 2); + + Object a = pool.acquire(); + Object b = pool.acquire(); + Object c = pool.acquire(); + pool.release(a); + pool.release(b); + pool.release(c); + + assertSame(b, pool.acquire()); + assertSame(a, pool.acquire()); + pool.acquire(); + assertEquals(4, calls.get()); + } }