From 734f0eb985f16e08f464aef713fbefa983a2c67d Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:59:14 +0900 Subject: [PATCH 1/8] migrate tlas/entity lifetime fix --- .../comfyfluffy/caustica/rt/RtComposite.java | 22 +- .../comfyfluffy/caustica/rt/RtFrameStats.java | 6 +- .../caustica/rt/RtGpuExecutor.java | 8 + .../caustica/rt/accel/RtAccel.java | 39 ++- .../caustica/rt/entity/RtEntities.java | 254 +++++++++++------- 5 files changed, 221 insertions(+), 108 deletions(-) diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java index df737537..6ef2cd14 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java @@ -761,6 +761,12 @@ private void updateMotion() { private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColor) { long dstImage = vkImage(nativeColor); var encoder = (VulkanCommandEncoder) ((CommandEncoderAccessor) RenderSystem.getDevice().createCommandEncoder()).caustica$getBackend(); + RtGpuExecutor gpuExecutor = ctx.gpuExecutor(); + // Reserve this frame's graphics-use value up front: prepareTlas/entity resource reuse below need + // it to gate their ring slots on actual GPU completion instead of assuming frame age is enough. + long graphicsUse = gpuExecutor.beginGraphicsTerrainUse(encoder); + pendingTerrainGraphicsUse = graphicsUse; + RtEntities.FrameEntities frameEntities = null; VkCommandBuffer cmd = encoder.allocateAndBeginTransientCommandBuffer(); RtDebugLabels.name(ctx, VK10.VK_OBJECT_TYPE_COMMAND_BUFFER, cmd.address(), "composite command buffer"); int debugView = debugView(); @@ -829,6 +835,7 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo // feeds the hit shader entity path (per-prim normal/tint) and motion vectors. RtEntities.FrameEntities fe = RtEntities.INSTANCE.beginFrame(ctx, terrain.staticInstances(), terrain.blockX, terrain.blockY, terrain.blockZ, camX, camY, camZ, frameProjection, frameViewRotation); + frameEntities = fe; // Block-breaking overlay: resolves each destroy-stage RenderType's texture into the // SAME bindless entity-texture array (destroy_stage_N.png is a standalone Sampler0 texture, // not a block-atlas sprite — see ModelBakery.BREAKING_LOCATIONS/DESTROY_TYPES), so any newly @@ -878,8 +885,9 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo // Upload any entity textures registered this frame into the bindless set before the trace. RtEntityTextures.INSTANCE.uploadPending(active, atlasSampler(ctx)); // Build the entity BLAS this frame, then the TLAS that references them (+ the already-built - // terrain BLAS), then the trace — each separated by a barrier. The frame TLAS is retired - // KEEP_FRAMES later (entity meshes/BLAS are retired by RtEntities on the same horizon). + // terrain BLAS), then the trace — each separated by a barrier. The frame TLAS slot (and entity + // meshes/BLAS retired by RtEntities) is reused once this frame's graphics-use value has actually + // completed on the GPU, not merely after KEEP_FRAMES have elapsed on the CPU. if (!fe.blas().isEmpty()) { try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("entity.blasRecord")) { RtAccel.recordBlasBuilds(ctx, cmd, fe.blas()); @@ -888,8 +896,10 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo } RtAccel.PreparedTlas frameTlas; try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("frame.prepareTlas")) { - frameTlas = RtAccel.prepareTlas(ctx, fe.baseInstances(), fe.dynamicInstances(), tlasRing); + frameTlas = RtAccel.prepareTlas(ctx, fe.baseInstances(), fe.dynamicInstances(), tlasRing, + graphicsUse); } + RtAccel.markTlasUsed(frameTlas, graphicsUse); active.setTlas(frameTlas.accel.handle); currentTlasHandle = frameTlas.accel.handle; try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("frame.recordTlas")) { @@ -961,10 +971,10 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo if (VK10.vkEndCommandBuffer(cmd) != VK10.VK_SUCCESS) { throw new IllegalStateException("vkEndCommandBuffer(rt composite) failed"); } - RtGpuExecutor gpuExecutor = ctx.gpuExecutor(); - long graphicsUse = gpuExecutor.beginGraphicsTerrainUse(encoder); encoder.execute(cmd); // deferred into the frame's submission — correct for per-frame work - pendingTerrainGraphicsUse = graphicsUse; + // Do not attach a merely reserved token: failed recording may never signal it. Once execute succeeds, + // every owner in this frame's manifest is protected through the final overlay consumer. + RtEntities.INSTANCE.markGraphicsUse(frameEntities, graphicsUse); } /** diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtFrameStats.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtFrameStats.java index de610f08..5310114a 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtFrameStats.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtFrameStats.java @@ -57,6 +57,7 @@ public final class RtFrameStats { "entity.capture.append.alloc", "entity.capture.append.copy", "entity.capture.append.blas", + "entity.uploadFlush", "entity.blockEntities", "entity.particles", "entity.blasRecord", @@ -79,7 +80,10 @@ public final class RtFrameStats { "entitySpecializedCuboids", "entityGenericCuboids", "entityParityChecks", "entityVmaBufferCreates", "entityGeometryBufferReuses", "entityScratchBufferReuses", "entityUploadBytes", "entityMotionUploadBytes", - "entityPackedBytes", "entityPackedPaddingBytes", "entityRetainedGeometryBytes"}, + "entityPackedBytes", "entityPackedPaddingBytes", "entityRetainedGeometryBytes", + "entityFrameListsWaits", "entityTableWaits", "entitySlotWaits", + "entityGraphicsWaitNanos", "entityMotionFlushes", "entityTableFlushes", + "entityBlockEntityRetirements", "entitySlotRetirements", "entityTableRetirements"}, true); private static final List GC_BEANS = ManagementFactory.getGarbageCollectorMXBeans(); diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java index e7046380..3fdb356c 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java @@ -130,6 +130,14 @@ public long completedGraphicsValue() { return queryTimeline(graphicsTimeline); } + /** Wait for a graphics-use reservation before reusing completion-owned frame resources. */ + public void waitForGraphicsValue(long graphicsValue) { + checkExecutorFailure(); + if (graphicsValue != 0L) { + waitTimeline(graphicsTimeline, graphicsValue); + } + } + /** Latest recorded graphics submission that can reference the currently published terrain state. */ public long latestGraphicsUseValue() { return latestGraphicsUseValue.get(); diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java b/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java index 68d7a906..7b66757a 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java @@ -926,13 +926,16 @@ public static final class PreparedTlas { private final RtBuffer scratch; private final int instanceCount; private final String label; + private final TlasRing.Slot ringSlot; - private PreparedTlas(RtAccel accel, RtBuffer instanceBuffer, RtBuffer scratch, int instanceCount, String label) { + private PreparedTlas(RtAccel accel, RtBuffer instanceBuffer, RtBuffer scratch, int instanceCount, + String label, TlasRing.Slot ringSlot) { this.accel = accel; this.instanceBuffer = instanceBuffer; this.scratch = scratch; this.instanceCount = instanceCount; this.label = label; + this.ringSlot = ringSlot; } } @@ -940,10 +943,9 @@ private PreparedTlas(RtAccel accel, RtBuffer instanceBuffer, RtBuffer scratch, i * Reusable per-frame TLAS resources. Allocating the instance buffer + AS backing + scratch fresh every * frame (and defer-destroying them 4 frames later) occasionally hit VMA's slow path — a fresh * VkDeviceMemory block allocation + map — observed as rare 20–50ms prepareTlas spikes. The ring keeps - * {@value #RING} slots, each sized for a capacity instance count, and rebuilds the same AS in place: a - * slot is reused every {@value #RING} frames (the established frames-in-flight horizon), so its - * previous build/trace is off all queues before the instance buffer is rewritten. A slot is recreated - * only when the instance count outgrows its capacity. + * {@value #RING} slots, each sized for a capacity instance count, and rebuilds the same AS in place. + * Reuse is guarded by the graphics-use timeline rather than frame age, so startup backlog cannot race + * an older build/trace. A slot is recreated only when the instance count outgrows its capacity. */ public static final class TlasRing { private static final int RING = 4; // = the frames-in-flight KEEP_FRAMES horizon @@ -957,6 +959,7 @@ private static final class Slot { RtBuffer instanceBuffer; RtBuffer scratch; int capacity; + long lastGraphicsUse; void destroy() { accel.destroy(); @@ -981,19 +984,25 @@ public void destroy() { * rebuilt in place — BUILD mode overwrites). Do NOT call {@link PreparedTlas#destroyAll} on the * result: the ring owns the resources. */ - public static PreparedTlas prepareTlas(RtContext ctx, List instances, TlasRing ring) { - return prepareTlas(ctx, instances, List.of(), ring); + public static PreparedTlas prepareTlas(RtContext ctx, List instances, TlasRing ring, + long graphicsUse) { + return prepareTlas(ctx, instances, List.of(), ring, graphicsUse); } /** Pack terrain and dynamic instances as two contiguous ranges without a composite-list get per item. */ public static PreparedTlas prepareTlas(RtContext ctx, List baseInstances, - List dynamicInstances, TlasRing ring) { + List dynamicInstances, TlasRing ring, long graphicsUse) { int baseCount = baseInstances.size(); int count = Math.addExact(baseCount, dynamicInstances.size()); TlasRing.Slot slot = ring.slots[ring.cursor]; + // Frame age is not GPU completion. Startup can leave more than RING submissions in flight; wait + // before rewriting this instance buffer, rebuilding its AS, or destroying it during a resize. + if (slot != null) { + ctx.gpuExecutor().waitForGraphicsValue(slot.lastGraphicsUse); + } if (slot == null || count > slot.capacity) { - // Outgrown (or first use). The slot's previous use is RING frames behind — off all queues by - // the same convention the old per-frame deferred free relied on — so immediate destroy is safe. + // Outgrown (or first use). The slot's previous use is confirmed off all queues by the wait + // above, so immediate destroy is safe. if (slot != null) { slot.destroy(); } @@ -1007,8 +1016,16 @@ public static PreparedTlas prepareTlas(RtContext ctx, List baseInstanc if (count > 0) { slot.instanceBuffer.flush(0L, (long) count * VkAccelerationStructureInstanceKHR.SIZEOF); } + slot.lastGraphicsUse = graphicsUse; return new PreparedTlas(slot.accel, slot.instanceBuffer, slot.scratch, count, - "frame TLAS " + count + " instances"); + "frame TLAS " + count + " instances", slot); + } + + /** Extend a retained TLAS slot's lifetime when a frame traces it without rebuilding it this call. */ + public static void markTlasUsed(PreparedTlas tlas, long graphicsUse) { + if (tlas.ringSlot != null) { + tlas.ringSlot.lastGraphicsUse = Math.max(tlas.ringSlot.lastGraphicsUse, graphicsUse); + } } // Wrap the mapped Vulkan array in LWJGL structs so its generated accessors own the native ABI/bitfields. diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java index 8c710e75..53fbba34 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java @@ -42,6 +42,7 @@ import it.unimi.dsi.fastutil.floats.FloatArrayList; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.longs.LongArrayList; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; @@ -137,8 +138,8 @@ private static int beBuildsPerFrame() { // EntityGeom: four addresses + rigid displacement + three geometry triangle bases + padding = 64 B. private static final int TABLE_ENTRY_BYTES = 64; - // Ring of fixed-size geometry tables: each frame fills the next slot so the GPU read of this frame's - // trace never races a later frame's host write. > frames-in-flight (mirrors RtPipeline RING). + // Ring of fixed-size geometry tables. Reuse is guarded by the graphics-use timeline; ring depth avoids + // routine waits but is not treated as proof of GPU completion. private static final int TABLE_RING = 6; // Frames a superseded cache or per-frame entity resource must outlive before it is freed. private static final int KEEP_FRAMES = 4; @@ -155,8 +156,8 @@ private static int beBuildsPerFrame() { // Well below a texel (1/16 block) and DLSS-RR jitter; float pose math noise is ~1e-5. private static final float RIGID_FIT_EPS = 2.0e-3f; - // Per-entity ring depth: a slot is reused every REFIT_RING frames, so it must be off all queues by - // then. = KEEP_FRAMES (the established frames-in-flight-safe horizon). Each slot holds one persistent AS. + // Per-entity ring depth. Each slot holds one persistent AS; the graphics-use timeline, not frame age, + // gates mapped writes, refits, rebuilds, and destruction when the cursor wraps. private static final int REFIT_RING = KEEP_FRAMES; // Force a periodic full rebuild of a slot's AS to bound BVH-quality degradation from repeated refits // (an entity that deforms a lot would otherwise refit the same BVH topology forever). Per-slot count. @@ -206,7 +207,7 @@ void set(float cx, float cy, float cz, int rbx, int rby, int rbz) { } } - private RtBuffer[] tableRing; + private TableSlot[] tableRing; private int tableCapacity; private int tableSlot; @@ -264,8 +265,6 @@ private static final class EntityPrev { float anchorX, anchorY, anchorZ; } - // Per-frame entity GPU resources awaiting a frames-in-flight-safe free. - private final List deferred = new ArrayList<>(); private long retainedGeometryBytes; // Persistent per-entity acceleration structures, keyed by entity id, for refit. @@ -302,6 +301,7 @@ private static final class BeEntry { long meshHash; // hash of the captured mesh — rebuild only when it changes long lastSeen; // last frame this BE was in the scan window — for eviction float[] prevVerts; // block-local verts at this build, for the per-vertex MV diff + long lastGraphicsUse; } /** One persistent updatable AS in an entity's ring: its own backing buffer + the topology it @@ -320,6 +320,7 @@ private static final class EntitySlot { int indexCount; long updateScratchSize; int updatesSinceBuild; + long lastGraphicsUse; } /** A per-entity ring of {@link EntitySlot}s, cycled one-per-frame so a refit never writes an AS still @@ -335,6 +336,7 @@ private static final class EntityAccel { // reuses the AS via the TLAS instance transform instead of re-uploading + refitting. Reuse frames // only READ the AS, so referencing the last-written ring slot while it is in flight is safe. RtAccel refAccel; + EntitySlot refSlot; float[] refVerts; int refVertCount = -1; int refIdxCount = -1; @@ -346,7 +348,19 @@ private static final class EntityAccel { /** This frame's terrain and dynamic instance segments, entity BLAS builds, and geometry-table address. */ public record FrameEntities(List baseInstances, List dynamicInstances, - List blas, long geomTableAddr) { + List blas, long geomTableAddr, FrameUse use) { + } + + private record FrameUse(FrameLists lists, TableSlot table) { + } + + private static final class TableSlot { + final RtBuffer buffer; + long lastGraphicsUse; + + TableSlot(RtBuffer buffer) { + this.buffer = buffer; + } } /** One glowing entity's body mesh (rebased-space positions, copied out of {@link #capture} before the @@ -361,9 +375,6 @@ public record GlowEntity(float[] verts, int[] idx, int color) { public record NameTagEntity(Component text, float x, float y, float z) { } - private record Deferred(long freeFrame, Runnable free) { - } - private record Motion(long dispAddr, float rigidX, float rigidY, float rigidZ) { } @@ -382,16 +393,13 @@ MotionSlice set(RtBuffer buffer, long offset, long size) { this.size = size; return this; } - - void flush() { - buffer.flush(offset, size); - } } - /** Host-visible storage pages owned by one frames-in-flight slot and reused when that slot retires. */ + /** Host-visible storage pages owned by one frame-list slot and reused after its graphics token completes. */ private static final class MotionArena { private final ArrayList pages = new ArrayList<>(); private final IntArrayList lastUsedCycles = new IntArrayList(); + private final LongArrayList dirtyEnds = new LongArrayList(); private final MotionSlice slice = new MotionSlice(); private int pageIndex; private long offset; @@ -403,10 +411,14 @@ void reset() { if (cycle - lastUsedCycles.getInt(i) >= MOTION_UNUSED_RETIRE_CYCLES) { pages.remove(i).destroy(); lastUsedCycles.removeInt(i); + dirtyEnds.removeLong(i); } } pageIndex = 0; offset = 0L; + for (int i = 0; i < dirtyEnds.size(); i++) { + dirtyEnds.set(i, 0L); + } } MotionSlice allocate(RtContext ctx, long bytes) { @@ -418,6 +430,7 @@ MotionSlice allocate(RtContext ctx, long bytes) { org.lwjgl.vulkan.VK10.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true, "entity motion arena")); lastUsedCycles.add(cycle); + dirtyEnds.add(0L); RtFrameStats.FRAME.count("vmaBufferCreates", 1); RtFrameStats.FRAME.count("entityVmaBufferCreates", 1); } @@ -426,6 +439,7 @@ MotionSlice allocate(RtContext ctx, long bytes) { if (size <= page.size - aligned) { lastUsedCycles.set(pageIndex, cycle); offset = Math.addExact(aligned, size); + dirtyEnds.set(pageIndex, Math.max(dirtyEnds.getLong(pageIndex), offset)); return slice.set(page, aligned, size); } pageIndex++; @@ -433,12 +447,25 @@ MotionSlice allocate(RtContext ctx, long bytes) { } } + /** Publish all sequential displacement writes with one VMA flush per used page. */ + void flushWrites() { + for (int i = 0; i < dirtyEnds.size(); i++) { + long bytes = dirtyEnds.getLong(i); + if (bytes == 0L) { + continue; + } + pages.get(i).flush(0L, bytes); + RtFrameStats.FRAME.count("entityMotionFlushes", 1); + } + } + void destroy() { for (RtBuffer page : pages) { page.destroy(); } pages.clear(); lastUsedCycles.clear(); + dirtyEnds.clear(); } } @@ -506,6 +533,9 @@ private static final class FrameLists { final ArrayList refitScratch = new ArrayList<>(entityListCapacity()); final ArrayList buffers = new ArrayList<>(TRANSIENT_BUFFER_LIST_CAPACITY); final MotionArena motion = new MotionArena(); + final ArrayList usedEntitySlots = new ArrayList<>(entityListCapacity()); + final ArrayList usedBlockEntities = new ArrayList<>(); + long lastGraphicsUse; void reset() { instances.clear(); @@ -513,6 +543,8 @@ void reset() { pooledBlas.clear(); refitScratch.clear(); buffers.clear(); + usedEntitySlots.clear(); + usedBlockEntities.clear(); motion.reset(); } @@ -531,6 +563,8 @@ void releaseDeferred() { pooledBlas.clear(); refitScratch.clear(); buffers.clear(); + usedEntitySlots.clear(); + usedBlockEntities.clear(); } void destroyPersistent() { @@ -550,11 +584,15 @@ private final class FrameBuild { MotionArena motion; // suballocated entity/BE/particle displacement uploads long tableBase; long geomTableAddr; + TableSlot table; int count; // geometry-table entries / TLAS instances int logicalCount; // ordinary entities + block entities + individual particles - FrameBuild(List base) { + long completedGraphicsUse; + + FrameBuild(List base, long completedGraphicsUse) { this.base = base; + this.completedGraphicsUse = completedGraphicsUse; } boolean full() { @@ -571,19 +609,18 @@ boolean full() { */ public FrameEntities beginFrame(RtContext ctx, List base, int rbx, int rby, int rbz, double camX, double camY, double camZ, Matrix4f projection, Matrix4f viewRotation) { - processDeferred(); if (!enabled()) { - return new FrameEntities(base, List.of(), List.of(), 0L); + return new FrameEntities(base, List.of(), List.of(), 0L, null); } Minecraft mc = Minecraft.getInstance(); ClientLevel level = mc.level; if (level == null) { - return new FrameEntities(base, List.of(), List.of(), 0L); + return new FrameEntities(base, List.of(), List.of(), 0L, null); } float partial = mc.getDeltaTracker().getGameTimeDeltaPartialTick(false); setCamera(camX, camY, camZ, projection, viewRotation); - FrameBuild build = new FrameBuild(base); + FrameBuild build = new FrameBuild(base, ctx.gpuExecutor().completedGraphicsValue()); try { try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("entity.capture")) { captureEntities(ctx, build, mc, level, partial, rbx, rby, rbz); @@ -601,23 +638,38 @@ public FrameEntities beginFrame(RtContext ctx, List base, int shutdown(); throw t; } - evictStaleAccels(); - evictStaleBes(); + evictStaleAccels(ctx); + evictStaleBes(ctx); RtFrameStats.FRAME.count("entityRetainedGeometryBytes", retainedGeometryBytes); if (build.instances == null) { - return new FrameEntities(base, List.of(), List.of(), 0L); + return new FrameEntities(base, List.of(), List.of(), 0L, null); + } + try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("entity.uploadFlush")) { + build.motion.flushWrites(); + if (build.count > 0) { + build.table.buffer.flush(0L, (long) build.count * TABLE_ENTRY_BYTES); + RtFrameStats.FRAME.count("entityTableFlushes", 1); + } + } + return new FrameEntities(base, build.instances, build.blas, build.geomTableAddr, + new FrameUse(build.lists, build.table)); + } + + /** Associate every resource returned for a successfully enqueued frame with its graphics completion. */ + public void markGraphicsUse(FrameEntities frame, long graphicsUse) { + if (frame == null || frame.use == null || graphicsUse == 0L) { + return; + } + FrameLists lists = frame.use.lists; + lists.lastGraphicsUse = Math.max(lists.lastGraphicsUse, graphicsUse); + frame.use.table.lastGraphicsUse = Math.max(frame.use.table.lastGraphicsUse, graphicsUse); + for (EntitySlot slot : lists.usedEntitySlots) { + slot.lastGraphicsUse = Math.max(slot.lastGraphicsUse, graphicsUse); + } + for (BeEntry entry : lists.usedBlockEntities) { + entry.lastGraphicsUse = Math.max(entry.lastGraphicsUse, graphicsUse); } - // Retire this frame's transient meshes + scratch + pooled-BUILD BLAS once it is no longer in flight - // (their build + the trace that reads them must complete first). Refit AS persist in entityAccels. - long freeAt = RtComposite.frameCounter() + KEEP_FRAMES; - FrameLists listsForFree = build.lists; - deferred.add(new Deferred(freeAt, () -> { - // The deferred horizon guarantees these are off all queues, so destroying them now is safe. - listsForFree.releaseDeferred(); - })); - tableRing[tableSlot].flush(0L, (long) build.count * TABLE_ENTRY_BYTES); - return new FrameEntities(base, build.instances, build.blas, build.geomTableAddr); } /** Capture animated entities (mobs, items, falling blocks) with per-object motion-vector displacement. */ @@ -843,7 +895,6 @@ private Motion uploadVertexMotion(RtContext ctx, FrameBuild build, FloatArrayLis MemoryUtil.memPutFloat(out + 12, 0f); out += 16; } - disp.flush(); RtFrameStats.FRAME.count("entityMotionUploadBytes", bytes); return new Motion(disp.deviceAddress, 0f, 0f, 0f); } @@ -1120,7 +1171,7 @@ private void updateBlockEntity(RtContext ctx, FrameBuild build, BlockEntityRende BeEntry rebuilt = buildBe(ctx, build, be, hash); rebuilt.lastSeen = now; if (entry != null) { - deferDestroyBe(entry); // retire the superseded geometry off-queue + retireBe(ctx, entry); } beCache.put(key, rebuilt); entry = rebuilt; @@ -1178,6 +1229,7 @@ private BeEntry buildBe(RtContext ctx, FrameBuild build, BlockEntity be, long ha e.meshHash = hash; // Retain this build's block-local verts so the next rebuild can diff against them for the MV. e.prevVerts = java.util.Arrays.copyOf(capture.verts.elements(), capture.verts.size()); + build.lists.usedBlockEntities.add(e); return e; } @@ -1222,21 +1274,25 @@ private void emitBe(RtContext ctx, FrameBuild build, BeEntry e, float[] disp, in build.instances.add(new RtAccel.Instance(xform, e.accel.deviceAddress, ENTITY_BIT | (build.count & 0x7FFFFF), 0xFF, RtAccel.SBT_ENTITY_OFFSET)); build.count++; + build.lists.usedBlockEntities.add(e); build.logicalCount++; RtFrameStats.FRAME.count("blockEntitiesCaptured", 1); } - /** Retire a cached block entity's persistent AS + mesh buffers once off all in-flight queues. */ - private void deferDestroyBe(BeEntry e) { - long freeAt = RtComposite.frameCounter() + KEEP_FRAMES; - deferred.add(new Deferred(freeAt, () -> { - RtAccel.destroyEntityAccel(e.accel, e.backing); - e.geometry.destroy(); - })); + /** Retire a cached block entity's persistent AS + mesh buffers once its exact last graphics use completes. */ + private static void retireBe(RtContext ctx, BeEntry e) { + RtAccel accel = e.accel; + RtBuffer backing = e.backing; + RtBuffer geometry = e.geometry; + ctx.gpuExecutor().enqueueDestroyAfterGraphics(e.lastGraphicsUse, () -> { + RtAccel.destroyEntityAccel(accel, backing); + geometry.destroy(); + }); + RtFrameStats.FRAME.count("entityBlockEntityRetirements", 1); } /** Drop cached block entities not seen (in window) within the last KEEP_FRAMES frames — unloaded/out of view. */ - private void evictStaleBes() { + private void evictStaleBes(RtContext ctx) { if (beCache.isEmpty()) { return; } @@ -1247,7 +1303,7 @@ private void evictStaleBes() { if (now - e.lastSeen < KEEP_FRAMES) { continue; } - deferDestroyBe(e); + retireBe(ctx, e); it.remove(); } } @@ -1275,6 +1331,8 @@ private void beginBuildIfNeeded(RtContext ctx, FrameBuild build) { return; } FrameLists lists = frameLists[(int) (RtComposite.frameCounter() % frameLists.length)]; + awaitGraphicsUse(ctx, build, lists.lastGraphicsUse, "entityFrameListsWaits"); + lists.releaseDeferred(); lists.reset(); build.lists = lists; build.instances = lists.instances; @@ -1285,8 +1343,21 @@ private void beginBuildIfNeeded(RtContext ctx, FrameBuild build) { build.motion = lists.motion; ensureResources(ctx); tableSlot = (tableSlot + 1) % TABLE_RING; - build.tableBase = tableRing[tableSlot].mapped; - build.geomTableAddr = tableRing[tableSlot].deviceAddress; + build.table = tableRing[tableSlot]; + awaitGraphicsUse(ctx, build, build.table.lastGraphicsUse, "entityTableWaits"); + build.tableBase = build.table.buffer.mapped; + build.geomTableAddr = build.table.buffer.deviceAddress; + } + + private static void awaitGraphicsUse(RtContext ctx, FrameBuild build, long lastUse, String counter) { + if (lastUse <= build.completedGraphicsUse) { + return; + } + long started = System.nanoTime(); + ctx.gpuExecutor().waitForGraphicsValue(lastUse); + build.completedGraphicsUse = lastUse; + RtFrameStats.FRAME.count(counter, 1); + RtFrameStats.FRAME.count("entityGraphicsWaitNanos", System.nanoTime() - started); } /** @@ -1346,6 +1417,10 @@ private boolean appendRigidReuse(RtContext ctx, FrameBuild build, Motion motion, } beginBuildIfNeeded(ctx, build); ea.lastSeen = RtComposite.frameCounter(); + if (ea.refSlot == null) { + throw new IllegalStateException("Rigid entity reuse lost its owning slot"); + } + build.lists.usedEntitySlots.add(ea.refSlot); writeTableEntry(build, ea.refPrimAddr, ea.refIndexAddr, ea.refUvAddr, motion.dispAddr, motion.rigidX, motion.rigidY, motion.rigidZ, ea.refBucketTris); build.instances.add(new RtAccel.Instance(placeTransform(localTransform, placeX, placeY, placeZ), @@ -1524,7 +1599,8 @@ private void appendPackedEntity(RtContext ctx, FrameBuild build, Motion motion, RtBuffer geometry; long allocStart = RtFrameStats.FRAME.startStage(); try { - slot = selectEntityBuildSlot(entityId); + slot = selectEntityBuildSlot(ctx, build, entityId); + build.lists.usedEntitySlots.add(slot); long required = Math.addExact(layout.totalBytes, EntityGeometryLayout.REGION_ALIGNMENT - 1L); geometry = slot.geometry; if (geometry == null || geometry.size < required) { @@ -1585,6 +1661,7 @@ private void appendPackedEntity(RtContext ctx, FrameBuild build, Motion motion, EntityAccel ea = slot.owner; clearRefGeometry(ea); ea.refAccel = accel; + ea.refSlot = slot; ea.refIndexAddr = indexAddr; ea.refUvAddr = uvAddr; ea.refPrimAddr = primAddr; @@ -1603,6 +1680,7 @@ private void appendPackedEntity(RtContext ctx, FrameBuild build, Motion motion, /** Clear the latest rigid-reuse view; the backing remains owned by its retired ring slot. */ private void clearRefGeometry(EntityAccel ea) { ea.refAccel = null; + ea.refSlot = null; ea.refIndexAddr = 0L; ea.refUvAddr = 0L; ea.refPrimAddr = 0L; @@ -1622,7 +1700,6 @@ private long uploadDisp(RtContext ctx, FrameBuild build, float[] disp) { beginBuildIfNeeded(ctx, build); MotionSlice slice = build.motion.allocate(ctx, (long) disp.length * Float.BYTES); MemoryUtil.memFloatBuffer(slice.mapped, disp.length).put(disp, 0, disp.length); - slice.flush(); return slice.deviceAddress; } @@ -1635,7 +1712,6 @@ private long uploadDisp(RtContext ctx, FrameBuild build, FloatArrayList disp) { beginBuildIfNeeded(ctx, build); MotionSlice slice = build.motion.allocate(ctx, (long) size * Float.BYTES); MemoryUtil.memFloatBuffer(slice.mapped, size).put(disp.elements(), 0, size); - slice.flush(); return slice.deviceAddress; } @@ -1660,11 +1736,8 @@ private void writeTableEntry(FrameBuild build, long primAddr, long idxAddr, long MemoryUtil.memPutInt(entry + 60, 0); } - /** - * Select the next per-entity slot. One entity contributes at most one changed capture per frame, so a - * wrapped slot is at least {@link #REFIT_RING} frames old and off all queues. - */ - private EntitySlot selectEntityBuildSlot(int entityId) { + /** Select the next per-entity slot, waiting on its exact last graphics use before mutable reuse. */ + private EntitySlot selectEntityBuildSlot(RtContext ctx, FrameBuild build, int entityId) { EntityAccel ea = entityAccels.get(entityId); if (ea == null) { ea = new EntityAccel(); @@ -1678,6 +1751,8 @@ private EntitySlot selectEntityBuildSlot(int entityId) { slot = new EntitySlot(); slot.owner = ea; ea.ring[s] = slot; + } else { + awaitGraphicsUse(ctx, build, slot.lastGraphicsUse, "entitySlotWaits"); } return slot; } @@ -1783,7 +1858,7 @@ private static void rememberIndexTopology(EntitySlot slot, IntArrayList indices) } /** Drop persistent AS for entities not captured within the last KEEP_FRAMES frames (off all queues). */ - private void evictStaleAccels() { + private void evictStaleAccels(RtContext ctx) { if (entityAccels.isEmpty()) { return; } @@ -1796,7 +1871,7 @@ private void evictStaleAccels() { } for (EntitySlot slot : ea.ring) { if (slot != null) { - destroyEntitySlot(slot); + retireEntitySlot(ctx, slot); } } clearRefGeometry(ea); @@ -1823,6 +1898,28 @@ private void destroyEntitySlot(EntitySlot slot) { slot.indexCount = 0; } + /** Detach a stale slot immediately and destroy its GPU owners after their exact last use completes. */ + private void retireEntitySlot(RtContext ctx, EntitySlot slot) { + RtAccel accel = slot.accel; + RtBuffer backing = slot.backing; + RtBuffer geometry = slot.geometry; + RtBuffer scratch = slot.refitScratch; + long geometryBytes = geometry == null ? 0L : geometry.size; + retainedGeometryBytes = Math.subtractExact(retainedGeometryBytes, geometryBytes); + slot.accel = null; + slot.backing = null; + slot.geometry = null; + slot.refitScratch = null; + slot.indices = null; + slot.indexCount = 0; + ctx.gpuExecutor().enqueueDestroyAfterGraphics(slot.lastGraphicsUse, () -> { + if (accel != null) RtAccel.destroyEntityAccel(accel, backing); + if (geometry != null) geometry.destroy(); + if (scratch != null) scratch.destroy(); + }); + RtFrameStats.FRAME.count("entitySlotRetirements", 1); + } + private void setCamera(double camX, double camY, double camZ, Matrix4f projection, Matrix4f viewRotation) { if (cameraState == null) { cameraState = new CameraRenderState(); @@ -1846,20 +1943,18 @@ private void ensureResources(RtContext ctx) { return; } if (tableRing != null) { - RtBuffer[] oldRing = tableRing; - deferred.add(new Deferred(RtComposite.frameCounter() + KEEP_FRAMES, () -> { - for (RtBuffer b : oldRing) { - b.destroy(); - } - })); + for (TableSlot old : tableRing) { + ctx.gpuExecutor().enqueueDestroyAfterGraphics(old.lastGraphicsUse, old.buffer::destroy); + RtFrameStats.FRAME.count("entityTableRetirements", 1); + } tableRing = null; tableCapacity = 0; } int storage = org.lwjgl.vulkan.VK10.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; - tableRing = new RtBuffer[TABLE_RING]; + tableRing = new TableSlot[TABLE_RING]; for (int i = 0; i < TABLE_RING; i++) { - tableRing[i] = ctx.createBuffer((long) requiredCapacity * TABLE_ENTRY_BYTES, storage, true, - "entity geometry table ring " + i); + tableRing[i] = new TableSlot(ctx.createBuffer((long) requiredCapacity * TABLE_ENTRY_BYTES, + storage, true, "entity geometry table ring " + i)); } tableCapacity = requiredCapacity; } @@ -1869,29 +1964,8 @@ public void onResourceReload() { collector.clearCaches(); } - private void processDeferred() { - if (deferred.isEmpty()) { - return; - } - long now = RtComposite.frameCounter(); - Iterator it = deferred.iterator(); - while (it.hasNext()) { - Deferred d = it.next(); - if (d.freeFrame() <= now) { - d.free().run(); - it.remove(); - } - } - } - - /** Free the geometry-table ring + any outstanding per-frame entity resources (teardown; GPU idle). */ + /** Free the geometry-table ring and entity resources (teardown; caller has idled the device). */ public void shutdown() { - // Drain outstanding deferred releases first (they destroy buffers/AS), then destroy the persistent - // per-entity AS. Runs after waitIdle, so immediate destruction is safe. - for (Deferred d : deferred) { - d.free().run(); - } - deferred.clear(); for (FrameLists lists : frameLists) { lists.releaseDeferred(); lists.destroyPersistent(); @@ -1911,8 +1985,8 @@ public void shutdown() { } beCache.clear(); if (tableRing != null) { - for (RtBuffer b : tableRing) { - b.destroy(); + for (TableSlot slot : tableRing) { + slot.buffer.destroy(); } tableRing = null; tableCapacity = 0; From 8d954d6503c1850d17beb624690ff86af64c5b69 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:52:07 +0900 Subject: [PATCH 2/8] cleanup --- .../comfyfluffy/caustica/rt/RtComposite.java | 14 +++------- .../caustica/rt/accel/RtAccel.java | 19 ++++--------- .../caustica/rt/entity/RtEntities.java | 27 +++++++------------ 3 files changed, 18 insertions(+), 42 deletions(-) diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java index 6ef2cd14..4933195e 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java @@ -95,11 +95,6 @@ public static boolean enabled() { // Hot addresses/frameIndex and raygen's debugView avoid unnecessary global-memory dereferences; // WorldPushConstantsData is generated from the same Slang module and owns this second ABI as well. private static final int GUIDE_COUNT = 6; // RR guide buffers bound at world-pipeline bindings 3..8 - // Frames a retired per-frame TLAS must outlive before it's freed (> frames-in-flight); matches - // RtTerrain's deferred-free horizon. The frame TLAS is built + traced this frame, then freed once - // the composite frame counter has advanced this far past it (so no in-flight frame still reads it). - private static final int KEEP_FRAMES = 4; - private static int debugView() { return CausticaConfig.Rt.Composite.DEBUG_VIEW.value(); } @@ -762,8 +757,7 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo long dstImage = vkImage(nativeColor); var encoder = (VulkanCommandEncoder) ((CommandEncoderAccessor) RenderSystem.getDevice().createCommandEncoder()).caustica$getBackend(); RtGpuExecutor gpuExecutor = ctx.gpuExecutor(); - // Reserve this frame's graphics-use value up front: prepareTlas/entity resource reuse below need - // it to gate their ring slots on actual GPU completion instead of assuming frame age is enough. + // Reserve the graphics-use value that guards this frame's reusable TLAS and entity resources. long graphicsUse = gpuExecutor.beginGraphicsTerrainUse(encoder); pendingTerrainGraphicsUse = graphicsUse; RtEntities.FrameEntities frameEntities = null; @@ -884,10 +878,8 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo pushBuf.flush(0L, WORLD_PUSH_SIZE); // Upload any entity textures registered this frame into the bindless set before the trace. RtEntityTextures.INSTANCE.uploadPending(active, atlasSampler(ctx)); - // Build the entity BLAS this frame, then the TLAS that references them (+ the already-built - // terrain BLAS), then the trace — each separated by a barrier. The frame TLAS slot (and entity - // meshes/BLAS retired by RtEntities) is reused once this frame's graphics-use value has actually - // completed on the GPU, not merely after KEEP_FRAMES have elapsed on the CPU. + // Build the entity BLAS, the TLAS that references it and the terrain BLAS, then the trace. + // Barriers separate each stage; the graphics-use timeline guards resource reuse. if (!fe.blas().isEmpty()) { try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("entity.blasRecord")) { RtAccel.recordBlasBuilds(ctx, cmd, fe.blas()); diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java b/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java index 7b66757a..5f12a075 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java @@ -940,15 +940,12 @@ private PreparedTlas(RtAccel accel, RtBuffer instanceBuffer, RtBuffer scratch, i } /** - * Reusable per-frame TLAS resources. Allocating the instance buffer + AS backing + scratch fresh every - * frame (and defer-destroying them 4 frames later) occasionally hit VMA's slow path — a fresh - * VkDeviceMemory block allocation + map — observed as rare 20–50ms prepareTlas spikes. The ring keeps - * {@value #RING} slots, each sized for a capacity instance count, and rebuilds the same AS in place. - * Reuse is guarded by the graphics-use timeline rather than frame age, so startup backlog cannot race - * an older build/trace. A slot is recreated only when the instance count outgrows its capacity. + * Owns {@value #RING} reusable per-frame TLAS slots. Each slot contains a capacity-sized instance + * buffer, acceleration structure, and scratch buffer. Graphics timeline completion guards reuse; + * instance-count growth recreates the selected slot with a larger capacity. */ public static final class TlasRing { - private static final int RING = 4; // = the frames-in-flight KEEP_FRAMES horizon + private static final int RING = 4; // depth avoids routine reuse waits private static final float GROWTH = 1.25f; // capacity headroom on (re)size private static final int MIN_CAPACITY = 1024; private final Slot[] slots = new Slot[RING]; @@ -984,19 +981,13 @@ public void destroy() { * rebuilt in place — BUILD mode overwrites). Do NOT call {@link PreparedTlas#destroyAll} on the * result: the ring owns the resources. */ - public static PreparedTlas prepareTlas(RtContext ctx, List instances, TlasRing ring, - long graphicsUse) { - return prepareTlas(ctx, instances, List.of(), ring, graphicsUse); - } - /** Pack terrain and dynamic instances as two contiguous ranges without a composite-list get per item. */ public static PreparedTlas prepareTlas(RtContext ctx, List baseInstances, List dynamicInstances, TlasRing ring, long graphicsUse) { int baseCount = baseInstances.size(); int count = Math.addExact(baseCount, dynamicInstances.size()); TlasRing.Slot slot = ring.slots[ring.cursor]; - // Frame age is not GPU completion. Startup can leave more than RING submissions in flight; wait - // before rewriting this instance buffer, rebuilding its AS, or destroying it during a resize. + // Complete the slot's prior graphics use before rewriting, rebuilding, or resizing it. if (slot != null) { ctx.gpuExecutor().waitForGraphicsValue(slot.lastGraphicsUse); } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java index 53fbba34..414380bb 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java @@ -138,10 +138,10 @@ private static int beBuildsPerFrame() { // EntityGeom: four addresses + rigid displacement + three geometry triangle bases + padding = 64 B. private static final int TABLE_ENTRY_BYTES = 64; - // Ring of fixed-size geometry tables. Reuse is guarded by the graphics-use timeline; ring depth avoids - // routine waits but is not treated as proof of GPU completion. + // Fixed-size geometry-table ring. Timeline completion guards host writes; ring depth avoids routine waits. private static final int TABLE_RING = 6; - // Frames a superseded cache or per-frame entity resource must outlive before it is freed. + // Stale-cache eviction horizon and default reusable-resource ring depth. + // Graphics timeline completion guards GPU reuse and destruction. private static final int KEEP_FRAMES = 4; private static final int FRAME_LIST_RING = KEEP_FRAMES; // Refit (UPDATE-mode) BLAS: persistent per-entity AS, refit in place each frame (cheap) while @@ -156,8 +156,8 @@ private static int beBuildsPerFrame() { // Well below a texel (1/16 block) and DLSS-RR jitter; float pose math noise is ~1e-5. private static final float RIGID_FIT_EPS = 2.0e-3f; - // Per-entity ring depth. Each slot holds one persistent AS; the graphics-use timeline, not frame age, - // gates mapped writes, refits, rebuilds, and destruction when the cursor wraps. + // Each per-entity ring slot owns one persistent AS. Timeline completion guards cursor reuse, + // mapped writes, refits, rebuilds, and destruction. private static final int REFIT_RING = KEEP_FRAMES; // Force a periodic full rebuild of a slot's AS to bound BVH-quality degradation from repeated refits // (an entity that deforms a lot would otherwise refit the same BVH topology forever). Per-slot count. @@ -333,8 +333,8 @@ private static final class EntityAccel { long lastSeen; // Rigid-reuse reference (refAccel == null → no reusable build yet). refVerts are the exact // positions the AS was last built/refit from; a frame whose capture is a rigid transform of them - // reuses the AS via the TLAS instance transform instead of re-uploading + refitting. Reuse frames - // only READ the AS, so referencing the last-written ring slot while it is in flight is safe. + // reuses the AS through the TLAS instance transform. Reuse frames only READ the AS, so referencing + // the last-written ring slot while it is in flight is safe. RtAccel refAccel; EntitySlot refSlot; float[] refVerts; @@ -379,18 +379,12 @@ private record Motion(long dispAddr, float rigidX, float rigidY, float rigidZ) { } private static final class MotionSlice { - RtBuffer buffer; - long offset; long mapped; long deviceAddress; - long size; - MotionSlice set(RtBuffer buffer, long offset, long size) { - this.buffer = buffer; - this.offset = offset; + MotionSlice set(RtBuffer buffer, long offset) { this.mapped = buffer.mapped + offset; this.deviceAddress = buffer.deviceAddress + offset; - this.size = size; return this; } } @@ -440,7 +434,7 @@ MotionSlice allocate(RtContext ctx, long bytes) { lastUsedCycles.set(pageIndex, cycle); offset = Math.addExact(aligned, size); dirtyEnds.set(pageIndex, Math.max(dirtyEnds.getLong(pageIndex), offset)); - return slice.set(page, aligned, size); + return slice.set(page, aligned); } pageIndex++; offset = 0L; @@ -1229,7 +1223,6 @@ private BeEntry buildBe(RtContext ctx, FrameBuild build, BlockEntity be, long ha e.meshHash = hash; // Retain this build's block-local verts so the next rebuild can diff against them for the MV. e.prevVerts = java.util.Arrays.copyOf(capture.verts.elements(), capture.verts.size()); - build.lists.usedBlockEntities.add(e); return e; } @@ -1857,7 +1850,7 @@ private static void rememberIndexTopology(EntitySlot slot, IntArrayList indices) slot.indexCount = count; } - /** Drop persistent AS for entities not captured within the last KEEP_FRAMES frames (off all queues). */ + /** Retire persistent AS for entities not captured within the last KEEP_FRAMES frames. */ private void evictStaleAccels(RtContext ctx) { if (entityAccels.isEmpty()) { return; From f2a5827ac713fa545fcbd358751e029c11bbf716 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:17:35 +0900 Subject: [PATCH 3/8] fix shader ub --- shaders/world/shadow.rmiss.slang | 11 +++++++---- shaders/world/world.rahit.slang | 24 ++++++++++++------------ shaders/world/world.rgen.slang | 28 +++++++++++++++++++++++----- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/shaders/world/shadow.rmiss.slang b/shaders/world/shadow.rmiss.slang index 12f5a9e4..8ac82c41 100644 --- a/shaders/world/shadow.rmiss.slang +++ b/shaders/world/shadow.rmiss.slang @@ -1,8 +1,11 @@ // Shadow / sky-visibility miss (SBT miss index 1). Note this shader does not actually RUN in the // current pipeline: visibility() uses a hit object purely as the traversal result and never invokes -// the miss shader — it fills the SBT slot. Kept behavior-identical to the GLSL original in case a -// future caller does invoke it (marks the ray escaped without touching the accumulated transmittance). +// the miss shader — it only fills the SBT slot. +// Radiance and shadow rays share the exact Payload ABI, as Vulkan requires for every stage reachable by +// a trace — see world.rgen.slang's shadowPayload. +import world_common; + [shader("miss")] -void main(inout float4 shadowVis) { - shadowVis.a = 1.0; +void main(inout Payload payload) { + payload.hitT = 1.0; } diff --git a/shaders/world/world.rahit.slang b/shaders/world/world.rahit.slang index 099d8d33..731ee7b6 100644 --- a/shaders/world/world.rahit.slang +++ b/shaders/world/world.rahit.slang @@ -8,9 +8,9 @@ // closest-hit. Opaque entity geometry bypasses this shader; every alpha/transmissive material shares // one non-opaque geometry and routes here through its SBT record. // -// NOTE on the payload: this module declares the SHADOW payload (float4 shadowVis). Radiance rays carry -// the big Payload struct but only reach the cutout paths here, which never touch the payload — the -// water/translucent branches that write shadowVis run only from shadow SBT records. +// Radiance and shadow rays share the exact Payload ABI, as Vulkan requires for every stage reachable by +// a trace. Shadow traversal uses albedo.rgb as accumulated transmittance and hitT as the nearest-water +// crossing; radiance cutout paths do not touch either field. import world_common; [[vk::push_constant]] WorldPushConstants pc; @@ -48,7 +48,7 @@ float alphaDitherThreshold(uint salt) { } [shader("anyhit")] -void main(inout float4 shadowVis, in BuiltInTriangleIntersectionAttributes attr) { +void main(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr) { uint pid = PrimitiveIndex(); float2 attribs = attr.barycentrics; float3 bary = float3(1.0 - attribs.x - attribs.y, attribs.x, attribs.y); @@ -89,14 +89,14 @@ void main(inout float4 shadowVis, in BuiltInTriangleIntersectionAttributes attr) if (instanceKind == ENTITY_BIT && shadowRay && materialHeader.model == MATERIAL_GLASS) { float3 tint = lerp(float3(1.0, 1.0, 1.0), srgbToLinear(texel.rgb) * epr.tint.rgb, texel.a); - shadowVis.rgb *= tint * clamp(materialHeader.params.w, 0.0, 1.0); + payload.albedo *= tint * clamp(materialHeader.params.w, 0.0, 1.0); IgnoreHit(); } if (instanceKind == ENTITY_BIT && shadowRay && materialHeader.model == MATERIAL_WATER) { float3 tint = srgbToLinear(texel.rgb) * epr.tint.rgb; - shadowVis.rgb *= lerp(float3(1.0, 1.0, 1.0), tint, WATER_SHADOW_TINT) + payload.albedo *= lerp(float3(1.0, 1.0, 1.0), tint, WATER_SHADOW_TINT) * clamp(materialHeader.params.w, 0.0, 1.0); - shadowVis.a = shadowVis.a < 0.0 ? RayTCurrent() : min(shadowVis.a, RayTCurrent()); + payload.hitT = payload.hitT < 0.0 ? RayTCurrent() : min(payload.hitT, RayTCurrent()); IgnoreHit(); } return; @@ -110,11 +110,11 @@ void main(inout float4 shadowVis, in BuiltInTriangleIntersectionAttributes attr) // the biome water color, then keep walking so submerged terrain is lit by colored transmission. if (bucket == BUCKET_WATER) { TerrainPrim pr = ConstPtr(sec.primAddr)[tri]; - shadowVis.rgb *= lerp(float3(1.0, 1.0, 1.0), pr.tint.rgb, WATER_SHADOW_TINT); - // Record the NEAREST water crossing (any-hit order is arbitrary) in the otherwise-unused alpha + payload.albedo *= lerp(float3(1.0, 1.0, 1.0), pr.tint.rgb, WATER_SHADOW_TINT); + // Record the NEAREST water crossing (any-hit order is arbitrary) in the shadow payload's hitT // lane. For an underwater shading point this is the exit point of its sun shadow ray, where // world.rgen evaluates the wave-refraction caustic. visibility() seeds the -1 sentinel. - shadowVis.a = shadowVis.a < 0.0 ? RayTCurrent() : min(shadowVis.a, RayTCurrent()); + payload.hitT = payload.hitT < 0.0 ? RayTCurrent() : min(payload.hitT, RayTCurrent()); IgnoreHit(); } @@ -126,7 +126,7 @@ void main(inout float4 shadowVis, in BuiltInTriangleIntersectionAttributes attr) // Modeled as Beer-Lambert absorption (matching the water medium in world.rgen): a per-channel // extinction derived from how dark the average is, scaled by the average alpha (how much of the // sprite is glass-colorant vs. see-through frame), so saturated panes darken transmitted light - // non-linearly. Multiplying into shadowVis.rgb compounds correctly across stacked panes. + // non-linearly. Multiplying into payload.albedo compounds correctly across stacked panes. if (bucket == BUCKET_TRANSLUCENT) { TerrainPrim pr = ConstPtr(sec.primAddr)[tri]; MaterialHeader materialHeader = ConstPtr(pc.materialTableAddr)[pr.materialId]; @@ -136,7 +136,7 @@ void main(inout float4 shadowVis, in BuiltInTriangleIntersectionAttributes attr) // The neutral floor is a flat per-hit dimming, NOT scaled by alpha: vanilla clear glass has a low // natural alpha, so folding it into the alpha-scaled term crushed it to near-zero for exactly the // white/clear-glass case it's meant to cover. - shadowVis.rgb *= exp(-colorExtinction * materialHeader.average.a - TRANSLUCENT_NEUTRAL_EXTINCTION); + payload.albedo *= exp(-colorExtinction * materialHeader.average.a - TRANSLUCENT_NEUTRAL_EXTINCTION); IgnoreHit(); } diff --git a/shaders/world/world.rgen.slang b/shaders/world/world.rgen.slang index 30df3ef6..0edc30ad 100644 --- a/shaders/world/world.rgen.slang +++ b/shaders/world/world.rgen.slang @@ -40,7 +40,10 @@ static WorldPush worldPush; // Ray payloads. Module-level statics so the helper functions (visibility / refractedGuideHit / // specularReflectionMotion / tracePath) can share them, mirroring the GLSL rayPayloadEXT globals. +// Vulkan requires every shader stage reached by one trace to use an identical payload structure, so +// shadow rays use Payload too even though their any-hit path only needs albedo.rgb + hitT. static Payload payload; +static Payload shadowPayload; static float4 shadowVis; // rgb = transmittance; a = nearest water-crossing t (-1 = none) // First-hit (primary-visibility) guide attributes, captured at bounce 0 of tracePath. The primary ray @@ -307,7 +310,12 @@ uint pcg(inout uint s) { uint w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u; return (w >> 22u) ^ w; } -float rndf(inout uint s) { return float(pcg(s)) * (1.0 / 4294967296.0); } +float rndf(inout uint s) { + // Convert the high 24 bits, which are exactly representable as float. Converting all 32 bits first + // lets the top 128 uint values round to 2^32, incorrectly returning 1.0 and breaking `< probability` + // tests (most seriously the F == 1 total-internal-reflection branch). + return float(pcg(s) >> 8u) * (1.0 / 16777216.0); +} float3 primaryRayDir(float2 ndc) { float4 nearH = mul(worldPush.invViewProj, float4(ndc.x, ndc.y, 1.0, 1.0)); @@ -704,14 +712,24 @@ void traceRadianceReordered(uint cullMask, float3 ro, float tmin, float3 rd, flo } float3 visibility(float3 origin, float3 dir, float tmax) { - shadowVis = float4(1.0, 1.0, 1.0, -1.0); // a = water-crossing sentinel, filled by the water any-hit + shadowPayload.albedo = float3(1.0, 1.0, 1.0); + shadowPayload.hitT = -1.0; // water-crossing sentinel, filled by the water any-hit + shadowPayload.normal = float3(0.0, 0.0, 0.0); + shadowPayload.motionPrev = float3(0.0, 0.0, 0.0); + shadowPayload.f0 = float3(0.0, 0.0, 0.0); + shadowPayload.flags = 0u; + shadowPayload.roughMetal = 0u; + shadowPayload.emissionSss = 0u; + shadowPayload.iorTransmission = 0u; + shadowPayload.rayCone = 0u; // Shadow SBT records run any-hit only for cutout/translucent/water. Cutout alpha-tests; translucent - // and water tint shadowVis.rgb and pass through. Solid blocks terminate traversal. There is no closest - // or miss shader worth executing, so use a hit object only as the traversal result. + // and water tint shadowPayload.albedo and pass through. Solid blocks terminate traversal. There is no + // closest or miss shader worth executing, so use a hit object only as the traversal result. HitObject hObj = HitObject::TraceRay(topLevelAS, RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH | RAY_FLAG_SKIP_CLOSEST_HIT_SHADER, CULL_SECONDARY, SBT_SHADOW, SBT_STRIDE_BUCKET, 0u, - makeRay(origin, RAY_TMIN, dir, tmax), shadowVis); + makeRay(origin, RAY_TMIN, dir, tmax), shadowPayload); + shadowVis = float4(shadowPayload.albedo, shadowPayload.hitT); return hObj.IsMiss() ? shadowVis.rgb : float3(0.0, 0.0, 0.0); } From ad0fbf352a2d00d3d5fc08c8c8380b6db22097f2 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:56:46 +0900 Subject: [PATCH 4/8] Unify graphics resource lifetime tracking --- .../caustica/mixin/GameRendererMixin.java | 6 +- .../comfyfluffy/caustica/rt/RtComposite.java | 25 +++-- .../caustica/rt/RtGpuExecutor.java | 94 ++++++++++++++----- .../caustica/rt/accel/RtAccel.java | 23 ++--- .../caustica/rt/entity/RtEntities.java | 52 +++++----- .../rt/terrain/RtLightGridManager.java | 13 +-- .../caustica/rt/terrain/RtSectionTable.java | 2 +- .../caustica/rt/terrain/RtTerrain.java | 21 +++-- 8 files changed, 139 insertions(+), 97 deletions(-) diff --git a/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java b/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java index 1b87da3e..db44f82e 100644 --- a/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java +++ b/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java @@ -163,9 +163,9 @@ public abstract class GameRendererMixin { try { RtWorldOverlay.INSTANCE.compositeIntoUiOverlay(this.mainRenderTarget); } finally { - // The block-outline ray query consumes this frame's TLAS. Signal terrain retirement only after - // its transient command buffer has been placed later in the same graphics submission. - RtComposite.INSTANCE.finishTerrainGraphicsUse(); + // The block-outline ray query consumes this frame's TLAS. Signal the shared RT frame token only + // after its transient command buffer has been placed later in the same graphics submission. + RtComposite.INSTANCE.finishGraphicsUse(); } } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java index 4933195e..1ac7b019 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java @@ -287,7 +287,7 @@ public static long frameCounter() { // makes the TLAS build's writes visible without an extra semaphore, matching every other overlay // feature's reliance on in-order queue execution for this frame's world content. private volatile long currentTlasHandle; - private long pendingTerrainGraphicsUse; + private RtGpuExecutor.GraphicsUse pendingGraphicsUse; private RtComposite() { } @@ -380,27 +380,27 @@ public Matrix4fc currentViewProjection() { * runs instead. */ public void beginFrame() { - if (pendingTerrainGraphicsUse != 0L) { - throw new IllegalStateException("Previous RT terrain graphics use was never completed"); + if (pendingGraphicsUse != null) { + throw new IllegalStateException("Previous RT graphics use was never completed"); } RtFrameStats.FRAME.beginIfInactive(); hdrWrittenThisFrame = false; } - /** Record terrain retirement completion after the frame's final TLAS consumer (world overlay). */ - public void finishTerrainGraphicsUse() { - long graphicsUse = pendingTerrainGraphicsUse; - if (graphicsUse == 0L) { + /** Signal this RT frame's shared completion token after its final TLAS consumer (world overlay). */ + public void finishGraphicsUse() { + RtGpuExecutor.GraphicsUse graphicsUse = pendingGraphicsUse; + if (graphicsUse == null) { return; } RtContext ctx = RtContext.currentOrNull(); if (ctx == null) { - throw new IllegalStateException("RT context disappeared before terrain graphics use completed"); + throw new IllegalStateException("RT context disappeared before graphics use completed"); } var encoder = (VulkanCommandEncoder) ((CommandEncoderAccessor) RenderSystem.getDevice() .createCommandEncoder()).caustica$getBackend(); - ctx.gpuExecutor().endGraphicsTerrainUse(encoder, graphicsUse); - pendingTerrainGraphicsUse = 0L; + ctx.gpuExecutor().endGraphicsUse(encoder, graphicsUse); + pendingGraphicsUse = null; } public void endFrame() { @@ -758,8 +758,8 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo var encoder = (VulkanCommandEncoder) ((CommandEncoderAccessor) RenderSystem.getDevice().createCommandEncoder()).caustica$getBackend(); RtGpuExecutor gpuExecutor = ctx.gpuExecutor(); // Reserve the graphics-use value that guards this frame's reusable TLAS and entity resources. - long graphicsUse = gpuExecutor.beginGraphicsTerrainUse(encoder); - pendingTerrainGraphicsUse = graphicsUse; + RtGpuExecutor.GraphicsUse graphicsUse = gpuExecutor.beginGraphicsUse(encoder); + pendingGraphicsUse = graphicsUse; RtEntities.FrameEntities frameEntities = null; VkCommandBuffer cmd = encoder.allocateAndBeginTransientCommandBuffer(); RtDebugLabels.name(ctx, VK10.VK_OBJECT_TYPE_COMMAND_BUFFER, cmd.address(), "composite command buffer"); @@ -891,7 +891,6 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo frameTlas = RtAccel.prepareTlas(ctx, fe.baseInstances(), fe.dynamicInstances(), tlasRing, graphicsUse); } - RtAccel.markTlasUsed(frameTlas, graphicsUse); active.setTlas(frameTlas.accel.handle); currentTlasHandle = frameTlas.accel.handle; try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("frame.recordTlas")) { diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java index 3fdb356c..82932543 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java @@ -98,13 +98,13 @@ public synchronized Build submit(BooleanSupplier cancelled, Consumer it = destroyJobs.iterator(); while (it.hasNext()) { @@ -498,6 +502,48 @@ public long value() { } } + /** Immutable reservation for one graphics frame's completion on the shared RT graphics timeline. */ + public static final class GraphicsUse { + private final long value; + + private GraphicsUse(long value) { + this.value = value; + } + } + + /** Mutable last-use owner embedded in reusable or asynchronously retired GPU resource slots. */ + public static final class TrackedGraphicsUse { + private long value; + + public void mark(GraphicsUse graphicsUse) { + value = Math.max(value, graphicsUse.value); + } + } + + /** + * Reuses one timeline query while awaiting several tracked owners. Timeline values are monotonic, so + * completing a newer value also proves every older value complete. + */ + public final class GraphicsUseWaiter { + private long completedValue; + + private GraphicsUseWaiter(long completedValue) { + this.completedValue = completedValue; + } + + /** Return true only when this call had to issue a host wait. */ + public boolean await(TrackedGraphicsUse trackedUse) { + long requiredValue = trackedUse.value; + if (requiredValue <= completedValue) { + return false; + } + checkExecutorFailure(); + waitTimeline(graphicsTimeline, requiredValue); + completedValue = requiredValue; + return true; + } + } + private record Job(BooleanSupplier cancelled, Consumer record, Runnable afterSuccess, BiConsumer finished, Build build) { } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java b/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java index 5f12a075..c7c03cf6 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/accel/RtAccel.java @@ -26,6 +26,8 @@ import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtDebugLabels; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor.GraphicsUse; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor.TrackedGraphicsUse; import java.util.List; @@ -926,16 +928,14 @@ public static final class PreparedTlas { private final RtBuffer scratch; private final int instanceCount; private final String label; - private final TlasRing.Slot ringSlot; private PreparedTlas(RtAccel accel, RtBuffer instanceBuffer, RtBuffer scratch, int instanceCount, - String label, TlasRing.Slot ringSlot) { + String label) { this.accel = accel; this.instanceBuffer = instanceBuffer; this.scratch = scratch; this.instanceCount = instanceCount; this.label = label; - this.ringSlot = ringSlot; } } @@ -956,7 +956,7 @@ private static final class Slot { RtBuffer instanceBuffer; RtBuffer scratch; int capacity; - long lastGraphicsUse; + final TrackedGraphicsUse graphicsUse = new TrackedGraphicsUse(); void destroy() { accel.destroy(); @@ -983,13 +983,13 @@ public void destroy() { */ /** Pack terrain and dynamic instances as two contiguous ranges without a composite-list get per item. */ public static PreparedTlas prepareTlas(RtContext ctx, List baseInstances, - List dynamicInstances, TlasRing ring, long graphicsUse) { + List dynamicInstances, TlasRing ring, GraphicsUse graphicsUse) { int baseCount = baseInstances.size(); int count = Math.addExact(baseCount, dynamicInstances.size()); TlasRing.Slot slot = ring.slots[ring.cursor]; // Complete the slot's prior graphics use before rewriting, rebuilding, or resizing it. if (slot != null) { - ctx.gpuExecutor().waitForGraphicsValue(slot.lastGraphicsUse); + ctx.gpuExecutor().graphicsUseWaiter().await(slot.graphicsUse); } if (slot == null || count > slot.capacity) { // Outgrown (or first use). The slot's previous use is confirmed off all queues by the wait @@ -1007,16 +1007,9 @@ public static PreparedTlas prepareTlas(RtContext ctx, List baseInstanc if (count > 0) { slot.instanceBuffer.flush(0L, (long) count * VkAccelerationStructureInstanceKHR.SIZEOF); } - slot.lastGraphicsUse = graphicsUse; + slot.graphicsUse.mark(graphicsUse); return new PreparedTlas(slot.accel, slot.instanceBuffer, slot.scratch, count, - "frame TLAS " + count + " instances", slot); - } - - /** Extend a retained TLAS slot's lifetime when a frame traces it without rebuilding it this call. */ - public static void markTlasUsed(PreparedTlas tlas, long graphicsUse) { - if (tlas.ringSlot != null) { - tlas.ringSlot.lastGraphicsUse = Math.max(tlas.ringSlot.lastGraphicsUse, graphicsUse); - } + "frame TLAS " + count + " instances"); } // Wrap the mapped Vulkan array in LWJGL structs so its generated accessors own the native ABI/bitfields. diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java index 414380bb..15c0a399 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java @@ -35,6 +35,10 @@ import dev.comfyfluffy.caustica.rt.RtComposite; import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtFrameStats; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor.GraphicsUse; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor.GraphicsUseWaiter; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor.TrackedGraphicsUse; import dev.comfyfluffy.caustica.rt.accel.RtAccel; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; import dev.comfyfluffy.caustica.rt.pipeline.RtPipeline; @@ -301,7 +305,7 @@ private static final class BeEntry { long meshHash; // hash of the captured mesh — rebuild only when it changes long lastSeen; // last frame this BE was in the scan window — for eviction float[] prevVerts; // block-local verts at this build, for the per-vertex MV diff - long lastGraphicsUse; + final TrackedGraphicsUse graphicsUse = new TrackedGraphicsUse(); } /** One persistent updatable AS in an entity's ring: its own backing buffer + the topology it @@ -320,7 +324,7 @@ private static final class EntitySlot { int indexCount; long updateScratchSize; int updatesSinceBuild; - long lastGraphicsUse; + final TrackedGraphicsUse graphicsUse = new TrackedGraphicsUse(); } /** A per-entity ring of {@link EntitySlot}s, cycled one-per-frame so a refit never writes an AS still @@ -356,7 +360,7 @@ private record FrameUse(FrameLists lists, TableSlot table) { private static final class TableSlot { final RtBuffer buffer; - long lastGraphicsUse; + final TrackedGraphicsUse graphicsUse = new TrackedGraphicsUse(); TableSlot(RtBuffer buffer) { this.buffer = buffer; @@ -529,7 +533,7 @@ private static final class FrameLists { final MotionArena motion = new MotionArena(); final ArrayList usedEntitySlots = new ArrayList<>(entityListCapacity()); final ArrayList usedBlockEntities = new ArrayList<>(); - long lastGraphicsUse; + final TrackedGraphicsUse graphicsUse = new TrackedGraphicsUse(); void reset() { instances.clear(); @@ -582,11 +586,11 @@ private final class FrameBuild { int count; // geometry-table entries / TLAS instances int logicalCount; // ordinary entities + block entities + individual particles - long completedGraphicsUse; + final GraphicsUseWaiter graphicsUseWaiter; - FrameBuild(List base, long completedGraphicsUse) { + FrameBuild(List base, RtGpuExecutor gpuExecutor) { this.base = base; - this.completedGraphicsUse = completedGraphicsUse; + this.graphicsUseWaiter = gpuExecutor.graphicsUseWaiter(); } boolean full() { @@ -614,7 +618,7 @@ public FrameEntities beginFrame(RtContext ctx, List base, int float partial = mc.getDeltaTracker().getGameTimeDeltaPartialTick(false); setCamera(camX, camY, camZ, projection, viewRotation); - FrameBuild build = new FrameBuild(base, ctx.gpuExecutor().completedGraphicsValue()); + FrameBuild build = new FrameBuild(base, ctx.gpuExecutor()); try { try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("entity.capture")) { captureEntities(ctx, build, mc, level, partial, rbx, rby, rbz); @@ -651,18 +655,18 @@ public FrameEntities beginFrame(RtContext ctx, List base, int } /** Associate every resource returned for a successfully enqueued frame with its graphics completion. */ - public void markGraphicsUse(FrameEntities frame, long graphicsUse) { - if (frame == null || frame.use == null || graphicsUse == 0L) { + public void markGraphicsUse(FrameEntities frame, GraphicsUse graphicsUse) { + if (frame == null || frame.use == null) { return; } FrameLists lists = frame.use.lists; - lists.lastGraphicsUse = Math.max(lists.lastGraphicsUse, graphicsUse); - frame.use.table.lastGraphicsUse = Math.max(frame.use.table.lastGraphicsUse, graphicsUse); + lists.graphicsUse.mark(graphicsUse); + frame.use.table.graphicsUse.mark(graphicsUse); for (EntitySlot slot : lists.usedEntitySlots) { - slot.lastGraphicsUse = Math.max(slot.lastGraphicsUse, graphicsUse); + slot.graphicsUse.mark(graphicsUse); } for (BeEntry entry : lists.usedBlockEntities) { - entry.lastGraphicsUse = Math.max(entry.lastGraphicsUse, graphicsUse); + entry.graphicsUse.mark(graphicsUse); } } @@ -1277,7 +1281,7 @@ private static void retireBe(RtContext ctx, BeEntry e) { RtAccel accel = e.accel; RtBuffer backing = e.backing; RtBuffer geometry = e.geometry; - ctx.gpuExecutor().enqueueDestroyAfterGraphics(e.lastGraphicsUse, () -> { + ctx.gpuExecutor().retireAfterGraphics(e.graphicsUse, () -> { RtAccel.destroyEntityAccel(accel, backing); geometry.destroy(); }); @@ -1324,7 +1328,7 @@ private void beginBuildIfNeeded(RtContext ctx, FrameBuild build) { return; } FrameLists lists = frameLists[(int) (RtComposite.frameCounter() % frameLists.length)]; - awaitGraphicsUse(ctx, build, lists.lastGraphicsUse, "entityFrameListsWaits"); + awaitGraphicsUse(build, lists.graphicsUse, "entityFrameListsWaits"); lists.releaseDeferred(); lists.reset(); build.lists = lists; @@ -1337,18 +1341,16 @@ private void beginBuildIfNeeded(RtContext ctx, FrameBuild build) { ensureResources(ctx); tableSlot = (tableSlot + 1) % TABLE_RING; build.table = tableRing[tableSlot]; - awaitGraphicsUse(ctx, build, build.table.lastGraphicsUse, "entityTableWaits"); + awaitGraphicsUse(build, build.table.graphicsUse, "entityTableWaits"); build.tableBase = build.table.buffer.mapped; build.geomTableAddr = build.table.buffer.deviceAddress; } - private static void awaitGraphicsUse(RtContext ctx, FrameBuild build, long lastUse, String counter) { - if (lastUse <= build.completedGraphicsUse) { + private static void awaitGraphicsUse(FrameBuild build, TrackedGraphicsUse graphicsUse, String counter) { + long started = System.nanoTime(); + if (!build.graphicsUseWaiter.await(graphicsUse)) { return; } - long started = System.nanoTime(); - ctx.gpuExecutor().waitForGraphicsValue(lastUse); - build.completedGraphicsUse = lastUse; RtFrameStats.FRAME.count(counter, 1); RtFrameStats.FRAME.count("entityGraphicsWaitNanos", System.nanoTime() - started); } @@ -1745,7 +1747,7 @@ private EntitySlot selectEntityBuildSlot(RtContext ctx, FrameBuild build, int en slot.owner = ea; ea.ring[s] = slot; } else { - awaitGraphicsUse(ctx, build, slot.lastGraphicsUse, "entitySlotWaits"); + awaitGraphicsUse(build, slot.graphicsUse, "entitySlotWaits"); } return slot; } @@ -1905,7 +1907,7 @@ private void retireEntitySlot(RtContext ctx, EntitySlot slot) { slot.refitScratch = null; slot.indices = null; slot.indexCount = 0; - ctx.gpuExecutor().enqueueDestroyAfterGraphics(slot.lastGraphicsUse, () -> { + ctx.gpuExecutor().retireAfterGraphics(slot.graphicsUse, () -> { if (accel != null) RtAccel.destroyEntityAccel(accel, backing); if (geometry != null) geometry.destroy(); if (scratch != null) scratch.destroy(); @@ -1937,7 +1939,7 @@ private void ensureResources(RtContext ctx) { } if (tableRing != null) { for (TableSlot old : tableRing) { - ctx.gpuExecutor().enqueueDestroyAfterGraphics(old.lastGraphicsUse, old.buffer::destroy); + ctx.gpuExecutor().retireAfterGraphics(old.graphicsUse, old.buffer::destroy); RtFrameStats.FRAME.count("entityTableRetirements", 1); } tableRing = null; diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtLightGridManager.java b/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtLightGridManager.java index 7af7a1c5..c807a2d9 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtLightGridManager.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtLightGridManager.java @@ -4,6 +4,7 @@ import dev.comfyfluffy.caustica.CausticaMod; import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtGpuExecutor; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor.GraphicsUse; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; import net.minecraft.client.Minecraft; import org.lwjgl.system.MemoryStack; @@ -103,7 +104,7 @@ void publishReady(RtContext ctx) { } /** World-reset path only. Normal light changes intentionally retain the published generation. */ - void invalidate(RtContext ctx, long lastGraphicsUse) { + void invalidate(RtContext ctx, GraphicsUse lastGraphicsUse) { cancelPending(); PublishedState old = published; published = PublishedState.EMPTY; @@ -262,11 +263,11 @@ private void publish(RtContext ctx, Uploaded uploaded) { PublishedState old = published; // The executor's host-side timeline wait only proves that the transfer completed. It does not // establish device-memory visibility from the async queue to the graphics queue. Publish the - // exact upload build so beginGraphicsTerrainUse() attaches the required semaphore dependency + // exact upload build so beginGraphicsUse() attaches the required semaphore dependency // before any shader can dereference this generation's buffer device addresses. ctx.gpuExecutor().markPublished(uploaded.build); published = next; - old.retire(ctx, ctx.gpuExecutor().latestGraphicsUseValue()); + old.retire(ctx, ctx.gpuExecutor().latestGraphicsUse()); if (CausticaConfig.Rt.Lights.DUMP.value()) dumpNearbyLights(uploaded.data); @@ -312,7 +313,7 @@ private void publishEmpty(RtContext ctx, long requestId) { if (!isLatest(requestId)) return; PublishedState old = published; published = PublishedState.empty(requestId); - old.retire(ctx, ctx.gpuExecutor().latestGraphicsUseValue()); + old.retire(ctx, ctx.gpuExecutor().latestGraphicsUse()); } private void discardCompletions() { @@ -373,9 +374,9 @@ private long address(long offset) { return arena != null ? arena.deviceAddress + offset : 0L; } - private void retire(RtContext ctx, long lastGraphicsUse) { + private void retire(RtContext ctx, GraphicsUse lastGraphicsUse) { if (arena != null) { - ctx.gpuExecutor().enqueueDestroyAfterGraphics(lastGraphicsUse, arena::destroy); + ctx.gpuExecutor().retireAfterGraphics(lastGraphicsUse, arena::destroy); } } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtSectionTable.java b/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtSectionTable.java index 0d440a76..a3f6176a 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtSectionTable.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtSectionTable.java @@ -114,7 +114,7 @@ private Generation acquireGeneration(RtContext ctx, int minCapacity) { if (generation.capacity >= minCapacity) { return generation; } - ctx.gpuExecutor().enqueueDestroyUnpublished(generation.buffer::destroy); + ctx.gpuExecutor().retireUnpublished(generation.buffer::destroy); } int storage = org.lwjgl.vulkan.VK10.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; RtBuffer buffer = ctx.createBuffer((long) minCapacity * SECTION_ENTRY_BYTES, storage, true, diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtTerrain.java b/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtTerrain.java index 75bdeb4d..2b91fb0d 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtTerrain.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/terrain/RtTerrain.java @@ -12,6 +12,7 @@ import dev.comfyfluffy.caustica.rt.RtDeviceBringup; import dev.comfyfluffy.caustica.rt.RtFrameStats; import dev.comfyfluffy.caustica.rt.RtGpuExecutor; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor.GraphicsUse; import dev.comfyfluffy.caustica.rt.accel.RtAccel; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; import dev.comfyfluffy.caustica.rt.material.RtMaterialRegistry; @@ -1406,7 +1407,7 @@ private void destroyPreparedSection(PreparedSection ps) { if (ctx == null) { RtSectionBuilder.destroy(ps); } else { - ctx.gpuExecutor().enqueueDestroyUnpublished(() -> RtSectionBuilder.destroy(ps)); + ctx.gpuExecutor().retireUnpublished(() -> RtSectionBuilder.destroy(ps)); } } @@ -1471,7 +1472,7 @@ private boolean shouldRebase(int rbx, int rby, int rbz) { private void applyBuildChanges(RtContext ctx, List prepared, List removed, boolean rebase, int rbx, int rby, int rbz) { - long lastGraphicsUse = ctx.gpuExecutor().latestGraphicsUseValue(); + GraphicsUse lastGraphicsUse = ctx.gpuExecutor().latestGraphicsUse(); int baseX = rebase ? rbx : blockX; int baseY = rebase ? rby : blockY; int baseZ = rebase ? rbz : blockZ; @@ -1506,7 +1507,7 @@ private void applyBuildChanges(RtContext ctx, List prepared, Li if (!desired.contains(ps.key())) { // Left the window while its batched BLAS build was in flight (window sync keeps running // during builds). Never published — retire the fresh, unreferenced geometry. - ctx.gpuExecutor().enqueueDestroyUnpublished(g::destroy); + ctx.gpuExecutor().retireUnpublished(g::destroy); continue; } SectionGeom prev = resident.get(ps.key()); @@ -1631,14 +1632,14 @@ private void ensureEmptyTableReady(RtContext ctx) { } /** Queue old GPU resources until the last graphics submission that could reference them completes. */ - private void retire(RtContext ctx, long lastGraphicsUse, List removed) { + private void retire(RtContext ctx, GraphicsUse lastGraphicsUse, List removed) { for (SectionGeom g : removed) { - ctx.gpuExecutor().enqueueDestroyAfterGraphics(lastGraphicsUse, g::destroy); + ctx.gpuExecutor().retireAfterGraphics(lastGraphicsUse, g::destroy); } } - private void retireGeneration(RtContext ctx, long lastGraphicsUse, Generation generation) { - ctx.gpuExecutor().enqueueDestroyAfterGraphics(lastGraphicsUse, + private void retireGeneration(RtContext ctx, GraphicsUse lastGraphicsUse, Generation generation) { + ctx.gpuExecutor().retireAfterGraphics(lastGraphicsUse, () -> table.recycleGeneration(generation)); } @@ -1763,7 +1764,7 @@ private void clearAsync(RtContext ctx) { inFlightDirtyGroup.clear(); cancelAllDirtyGroups(); - long lastGraphicsUse = ctx.gpuExecutor().latestGraphicsUseValue(); + GraphicsUse lastGraphicsUse = ctx.gpuExecutor().latestGraphicsUse(); Generation oldGeneration = table.detachGeneration(); Set oldGeometry = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -1817,11 +1818,11 @@ private void clearAsync(RtContext ctx) { lightGrid.invalidate(ctx, lastGraphicsUse); if (!oldGeometry.isEmpty()) { ArrayList retirement = new ArrayList<>(oldGeometry); - ctx.gpuExecutor().enqueueDestroyAfterGraphics(lastGraphicsUse, + ctx.gpuExecutor().retireAfterGraphics(lastGraphicsUse, () -> destroyDetachedGeometry(retirement)); } if (!oldPrepared.isEmpty()) { - ctx.gpuExecutor().enqueueDestroyUnpublished(() -> destroyDetachedPrepared(oldPrepared)); + ctx.gpuExecutor().retireUnpublished(() -> destroyDetachedPrepared(oldPrepared)); } // Keep the RT seam alive as an empty world while the new desired window begins filling. From 4666a849b3c3d1e1a1bceb24a5d715c3c71793a4 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:01:34 +0900 Subject: [PATCH 5/8] Localize shadow visibility payload state --- shaders/world/shadow.rmiss.slang | 2 +- shaders/world/world.rgen.slang | 55 ++++++++++++++++++++------------ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/shaders/world/shadow.rmiss.slang b/shaders/world/shadow.rmiss.slang index 8ac82c41..1b338c15 100644 --- a/shaders/world/shadow.rmiss.slang +++ b/shaders/world/shadow.rmiss.slang @@ -2,7 +2,7 @@ // current pipeline: visibility() uses a hit object purely as the traversal result and never invokes // the miss shader — it only fills the SBT slot. // Radiance and shadow rays share the exact Payload ABI, as Vulkan requires for every stage reachable by -// a trace — see world.rgen.slang's shadowPayload. +// a trace — see world.rgen.slang's visibility(). import world_common; [shader("miss")] diff --git a/shaders/world/world.rgen.slang b/shaders/world/world.rgen.slang index 0edc30ad..274c0a2d 100644 --- a/shaders/world/world.rgen.slang +++ b/shaders/world/world.rgen.slang @@ -38,13 +38,14 @@ import world_common; // `worldPush` global). Layout constants generated from this module's SPIR-V — see world_common.WorldPush. static WorldPush worldPush; -// Ray payloads. Module-level statics so the helper functions (visibility / refractedGuideHit / -// specularReflectionMotion / tracePath) can share them, mirroring the GLSL rayPayloadEXT globals. -// Vulkan requires every shader stage reached by one trace to use an identical payload structure, so -// shadow rays use Payload too even though their any-hit path only needs albedo.rgb + hitT. +// The radiance payload remains module-level so tracePath and its guide helpers can share it across +// HitObject trace/invoke calls, mirroring the GLSL rayPayloadEXT global. static Payload payload; -static Payload shadowPayload; -static float4 shadowVis; // rgb = transmittance; a = nearest water-crossing t (-1 = none) + +struct VisibilityResult { + float3 transmittance; + float waterHitT; +}; // First-hit (primary-visibility) guide attributes, captured at bounce 0 of tracePath. The primary ray // is deterministic (no AA jitter yet), so every SPP sample's bounce 0 yields identical values. @@ -182,9 +183,9 @@ float3 applyWaterWaves(float3 nGeo, float2 worldXZ, float t) { // ---- Water caustics. Sunlight refracting through the waved surface converges/diverges before it // reaches an underwater receiver; the analytic wave field makes the true focusing factor computable // instead of faked with a scrolling texture. Where the shadow ray of an underwater NEE vertex crossed -// water (recorded by world.rahit in shadowVis.a), evaluate the horizontal landing position of the -// refracted sun ray as a function of surface position and finite-difference it: the caustic intensity is -// the inverse Jacobian determinant of that surface→floor mapping (area compression = brightening, real +// water (returned by visibility() as VisibilityResult.waterHitT), evaluate the horizontal landing +// position of the refracted sun ray as a function of surface position and finite-difference it. The +// caustic intensity is the inverse Jacobian determinant of that surface→floor mapping (area compression = brightening, real // fold caustics where det → 0). Because this uses the SAME wave field as the visible surface normals, // the caustic pattern stays in sync with the ripples, and the per-sample sun-quad jitter (sampleSquare) // shifts the pattern per sample → caustics physically blur with depth under DLSS-RR accumulation. @@ -676,7 +677,8 @@ float3 shadeReservoir(Reservoir s, float3 hitPos, float3 n, float3 v, float3 rd, float3 toL = s.pos - origin; float dist = length(toL); // Stop just short of the sample point so the ray doesn't self-occlude on the emitter's own face. - vis = visibility(origin, toL / dist, dist * 0.999); + VisibilityResult shadow = visibility(origin, toL / dist, dist * 0.999); + vis = shadow.transmittance; return contrib * vis * s.W; } @@ -711,7 +713,8 @@ void traceRadianceReordered(uint cullMask, float3 ro, float tmin, float3 rd, flo HitObject::Invoke(topLevelAS, hObj, payload); } -float3 visibility(float3 origin, float3 dir, float tmax) { +Payload makeShadowPayload() { + Payload shadowPayload; shadowPayload.albedo = float3(1.0, 1.0, 1.0); shadowPayload.hitT = -1.0; // water-crossing sentinel, filled by the water any-hit shadowPayload.normal = float3(0.0, 0.0, 0.0); @@ -722,6 +725,13 @@ float3 visibility(float3 origin, float3 dir, float tmax) { shadowPayload.emissionSss = 0u; shadowPayload.iorTransmission = 0u; shadowPayload.rayCone = 0u; + return shadowPayload; +} + +VisibilityResult visibility(float3 origin, float3 dir, float tmax) { + // Vulkan requires an identical payload structure for every stage reachable by this trace. The shadow + // path uses only albedo as accumulated transmittance and hitT as the nearest-water crossing. + Payload shadowPayload = makeShadowPayload(); // Shadow SBT records run any-hit only for cutout/translucent/water. Cutout alpha-tests; translucent // and water tint shadowPayload.albedo and pass through. Solid blocks terminate traversal. There is no // closest or miss shader worth executing, so use a hit object only as the traversal result. @@ -729,8 +739,10 @@ float3 visibility(float3 origin, float3 dir, float tmax) { RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH | RAY_FLAG_SKIP_CLOSEST_HIT_SHADER, CULL_SECONDARY, SBT_SHADOW, SBT_STRIDE_BUCKET, 0u, makeRay(origin, RAY_TMIN, dir, tmax), shadowPayload); - shadowVis = float4(shadowPayload.albedo, shadowPayload.hitT); - return hObj.IsMiss() ? shadowVis.rgb : float3(0.0, 0.0, 0.0); + VisibilityResult result; + result.transmittance = hObj.IsMiss() ? shadowPayload.albedo : float3(0.0, 0.0, 0.0); + result.waterHitT = shadowPayload.hitT; + return result; } float2 projectPrevNdc(float3 worldPos) { @@ -989,7 +1001,7 @@ float3 tracePath(float3 ro, float3 rd, float primaryConeSpread, uint2 pix, uint float ndl = abs(signedNdl); if (ndl > 0.0) { float3 shadowOrigin = hitPos + (signedNdl >= 0.0 ? n : -n) * SURF_BIAS; - float3 vis = visibility(shadowOrigin, lightDir, 10000.0); + float3 vis = visibility(shadowOrigin, lightDir, 10000.0).transmittance; if (max(vis.r, max(vis.g, vis.b)) > 0.0) { L += throughput * albedo * INV_PI * worldPush.lightRadiance.xyz * ndl * vis; } @@ -1132,12 +1144,13 @@ float3 tracePath(float3 ro, float3 rd, float primaryConeSpread, uint2 pix, uint } float ndl = max(0.0, dot(n, lightDir)); if (ndl > 0.0) { - float3 vis = visibility(p, lightDir, 10000.0); + VisibilityResult shadow = visibility(p, lightDir, 10000.0); + float3 vis = shadow.transmittance; // Underwater receiver whose shadow ray crossed a water surface: scale the direct light by // the wave-refraction caustic at the exit point. Focusing scales the incident irradiance, // so it applies to the whole NEE term (diffuse + specular). - if (inWater && waterWaves && shadowVis.a > 0.0) { - vis *= waterCaustic(p + lightDir * shadowVis.a, lightDir, shadowVis.a); + if (inWater && waterWaves && shadow.waterHitT > 0.0) { + vis *= waterCaustic(p + lightDir * shadow.waterHitT, lightDir, shadow.waterHitT); } if (max(vis.r, max(vis.g, vis.b)) > 0.0) { float3 brdf = diffAlb * INV_PI; // Lambertian diffuse (f = albedo/PI) @@ -1177,11 +1190,13 @@ float3 tracePath(float3 ro, float3 rd, float primaryConeSpread, uint2 pix, uint if (sss > 0.0 && bounce <= MAX_SSS_BOUNCE) { float backNdl = max(0.0, dot(-n, lightDir)); if (backNdl > 0.0) { - float3 visB = visibility(hitPos - n * SURF_BIAS, lightDir, 10000.0); + VisibilityResult shadowBack = visibility(hitPos - n * SURF_BIAS, lightDir, 10000.0); + float3 visB = shadowBack.transmittance; // Same caustic as the front-face NEE — underwater kelp/seagrass transmission should // flicker with the same light bands as the floor around it. - if (inWater && waterWaves && shadowVis.a > 0.0) { - visB *= waterCaustic(hitPos + lightDir * shadowVis.a, lightDir, shadowVis.a); + if (inWater && waterWaves && shadowBack.waterHitT > 0.0) { + visB *= waterCaustic(hitPos + lightDir * shadowBack.waterHitT, + lightDir, shadowBack.waterHitT); } if (max(visB.r, max(visB.g, visB.b)) > 0.0) { float cosT = dot(lightDir, rd); From ccdcd86fe706d2b56d12177aad42f1e7b55dcd10 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:28:50 +0900 Subject: [PATCH 6/8] Assert graphics lifetime render-thread ownership --- .../dev/comfyfluffy/caustica/rt/RtGpuExecutor.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java index 82932543..ccfa9432 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtGpuExecutor.java @@ -1,5 +1,6 @@ package dev.comfyfluffy.caustica.rt; +import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vulkan.VulkanCommandEncoder; import com.mojang.blaze3d.vulkan.VulkanQueue; import org.lwjgl.PointerBuffer; @@ -100,11 +101,13 @@ public synchronized Build submit(BooleanSupplier cancelled, Consumer Date: Sat, 25 Jul 2026 01:35:53 +0900 Subject: [PATCH 7/8] Retire overlay scratch on graphics completion --- .../caustica/mixin/GameRendererMixin.java | 3 +- .../comfyfluffy/caustica/rt/RtComposite.java | 6 +++ .../rt/overlay/RtOverlayFramePool.java | 40 +++++-------------- .../caustica/rt/overlay/RtWorldOverlay.java | 21 +++++----- 4 files changed, 28 insertions(+), 42 deletions(-) diff --git a/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java b/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java index db44f82e..15a7593b 100644 --- a/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java +++ b/src/main/java/dev/comfyfluffy/caustica/mixin/GameRendererMixin.java @@ -161,7 +161,8 @@ public abstract class GameRendererMixin { // Fold RT world overlays into the shared transparent UI image before hand/screen effects and the GUI // add their own layers. RtUiOverlay then performs the single final blend to SDR/HDR. try { - RtWorldOverlay.INSTANCE.compositeIntoUiOverlay(this.mainRenderTarget); + RtWorldOverlay.INSTANCE.compositeIntoUiOverlay( + this.mainRenderTarget, RtComposite.INSTANCE.currentGraphicsUse()); } finally { // The block-outline ray query consumes this frame's TLAS. Signal the shared RT frame token only // after its transient command buffer has been placed later in the same graphics submission. diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java index 1ac7b019..cd724fff 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java @@ -387,6 +387,12 @@ public void beginFrame() { hdrWrittenThisFrame = false; } + /** This frame's completion token, valid until {@link #finishGraphicsUse()} signals it. */ + public RtGpuExecutor.GraphicsUse currentGraphicsUse() { + RenderSystem.assertOnRenderThread(); + return pendingGraphicsUse; + } + /** Signal this RT frame's shared completion token after its final TLAS consumer (world overlay). */ public void finishGraphicsUse() { RtGpuExecutor.GraphicsUse graphicsUse = pendingGraphicsUse; diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFramePool.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFramePool.java index 0f3bc711..f549f2fc 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFramePool.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFramePool.java @@ -3,41 +3,23 @@ import org.lwjgl.vulkan.VK10; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import dev.comfyfluffy.caustica.rt.RtContext; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; /** * Per-frame host-visible vertex/index scratch for overlay passes, shared by every {@link RtOverlayFeature}. - * Buffers acquired during a frame are queued at {@link #endFrame} and destroyed only {@value #KEEP_FRAMES} - * frames later (the same frames-in-flight-safe deferred-release convention {@code RtEntities} uses), so a - * buffer is never destroyed while a prior frame's GPU reads are still in flight. + * Buffers acquired during a frame retire against that frame's exact graphics completion token, so a buffer + * is never destroyed while the GPU can still read it. */ public final class RtOverlayFramePool { - private static final int KEEP_FRAMES = 4; // Vulkan requires buffer size > 0; a few zero-length overlay draws could otherwise reach acquire() with // bytes == 0. private static final long MIN_SIZE = 256; private final List acquiredThisFrame = new ArrayList<>(); - private final List deferred = new ArrayList<>(); - - private record Deferred(long freeFrame, RtBuffer buffer) { - } - - /** Destroy buffers whose in-flight window has passed. Call once at the start of the overlay frame. */ - public void beginFrame(long frameCounter) { - Iterator it = deferred.iterator(); - while (it.hasNext()) { - Deferred d = it.next(); - if (d.freeFrame <= frameCounter) { - d.buffer.destroy(); - it.remove(); - } - } - } /** A host-visible vertex buffer of at least {@code bytes}, valid for this frame only. */ public RtBuffer acquireVertex(RtContext ctx, long bytes, String label) { @@ -55,23 +37,21 @@ private RtBuffer acquire(RtContext ctx, long bytes, int usage, String label) { return b; } - /** Queue everything acquired this frame for destruction once it is safely out of flight. */ - public void endFrame(long frameCounter) { - for (RtBuffer b : acquiredThisFrame) { - deferred.add(new Deferred(frameCounter + KEEP_FRAMES, b)); + /** Retire everything acquired this frame once its overlay commands have completed. */ + public void endFrame(RtContext ctx, RtGpuExecutor.GraphicsUse graphicsUse) { + if (acquiredThisFrame.isEmpty()) { + return; } + List retired = List.copyOf(acquiredThisFrame); + ctx.gpuExecutor().retireAfterGraphics(graphicsUse, () -> retired.forEach(RtBuffer::destroy)); acquiredThisFrame.clear(); } - /** Immediate teardown; only valid once the device is idle (mirrors {@code RtComposite.destroy}). */ + /** Immediate teardown of unpublished buffers; queued buffers are owned by the GPU executor. */ public void destroy() { for (RtBuffer b : acquiredThisFrame) { b.destroy(); } acquiredThisFrame.clear(); - for (Deferred d : deferred) { - d.buffer.destroy(); - } - deferred.clear(); } } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java index 759d0bba..3c7ea473 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java @@ -25,6 +25,7 @@ import dev.comfyfluffy.caustica.rt.RtComposite; import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtDebugLabels; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; import dev.comfyfluffy.caustica.rt.RtUiOverlay; import dev.comfyfluffy.caustica.rt.accel.RtImage; @@ -75,17 +76,15 @@ private RtWorldOverlay() { * target. Called after the RT world composite and before the vanilla hand/screen-effects/GUI path can draw * more UI layers into that same target. */ - public void compositeIntoUiOverlay(RenderTarget main) { - long frame = RtComposite.frameCounter(); - framePool.beginFrame(frame); + public void compositeIntoUiOverlay(RenderTarget main, RtGpuExecutor.GraphicsUse graphicsUse) { + if (graphicsUse == null || failed || main == null || main.getColorTexture() == null || !RtUiOverlay.enabled()) { + return; + } + RtContext ctx = RtContext.currentOrNull(); + if (ctx == null) { + return; + } try { - if (failed || main == null || main.getColorTexture() == null || !RtUiOverlay.enabled()) { - return; - } - RtContext ctx = RtContext.currentOrNull(); - if (ctx == null) { - return; - } List ready = new ArrayList<>(features.size()); for (RtOverlayFeature f : features) { if (f.prepare(ctx, framePool, main.width, main.height)) { @@ -106,7 +105,7 @@ public void compositeIntoUiOverlay(RenderTarget main) { failed = true; CausticaMod.LOGGER.error("World overlay failed; disabling for this session", t); } finally { - framePool.endFrame(frame); + framePool.endFrame(ctx, graphicsUse); } } From 0b48fd5d3bce425975ca9af3f11c1f05df847b24 Mon Sep 17 00:00:00 2001 From: ComfyFluffy <24245520+ComfyFluffy@users.noreply.github.com> Date: Sat, 25 Jul 2026 01:46:18 +0900 Subject: [PATCH 8/8] Guard reusable graphics rings by timeline --- .../comfyfluffy/caustica/rt/RtComposite.java | 35 +++++++++++++------ .../caustica/rt/entity/RtEntities.java | 6 ++-- .../rt/overlay/RtBlockOutlineFeature.java | 6 ++-- .../rt/overlay/RtGlowOutlineFeature.java | 4 ++- .../caustica/rt/overlay/RtNameTagFeature.java | 4 ++- .../caustica/rt/overlay/RtOverlayFeature.java | 5 ++- .../rt/overlay/RtOverlayPipelines.java | 17 ++++++--- .../caustica/rt/overlay/RtWorldOverlay.java | 2 +- .../caustica/rt/pipeline/RtPipeline.java | 24 +++++++------ 9 files changed, 69 insertions(+), 34 deletions(-) diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java index cd724fff..2f8fb54f 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/RtComposite.java @@ -149,7 +149,7 @@ private static float celestialAxisZ() { return sunNoonY(); } - // Monotonic per-composite frame counter, used by RtTerrain to time frames-in-flight-safe frees. + // Monotonic per-composite frame counter used for cache eviction, shader sampling, and diagnostics. private static volatile long frameCounter; public static long frameCounter() { @@ -173,9 +173,9 @@ public static long frameCounter() { private boolean materialEpochTraceGate; // World push data lives in a host-visible BDA ring; only the slot address and a small hot subset are // pushed inline (the full generated structure exceeds NVIDIA's 256-byte push-constant ceiling). - // One slot per in-flight frame, cycled per frame so an in-flight slot is never overwritten. + // Exact graphics completion guards host writes; ring depth only avoids routine waits. private static final int PUSH_RING = 6; - private RtBuffer[] pushRing; + private PushSlot[] pushRing; private int pushSlot; private RtDisplayPipeline displayPipeline; private RtImage output; @@ -200,6 +200,15 @@ public static long frameCounter() { // Step C.2: composites the combined UI overlay over hdrDisplayImage at paper white, just before present. private RtHdrCompositePipeline hdrCompositePipeline; private long hdrUiSampler; + + private static final class PushSlot { + final RtBuffer buffer; + final RtGpuExecutor.TrackedGraphicsUse graphicsUse = new RtGpuExecutor.TrackedGraphicsUse(); + + PushSlot(RtBuffer buffer) { + this.buffer = buffer; + } + } // Menu/non-RT present: converts the SDR main target (sRGB) to PQ-encoded at paper white so menus, // the title panorama and the loading screen present correctly to the PQ swapchain instead of being // raw-copied (misdisplayed). Lazily created; the image is sized to the swapchain. @@ -515,10 +524,10 @@ private RtPipeline ensureWorld(RtContext ctx) { WorldPushConstantsData.BYTE_SIZE, true, GUIDE_COUNT, bindlessTextureCapacity, true); // Per-frame world data lives in this BDA ring; the pipeline pushes its address and hot fields. if (pushRing == null) { - pushRing = new RtBuffer[PUSH_RING]; + pushRing = new PushSlot[PUSH_RING]; for (int i = 0; i < PUSH_RING; i++) { - pushRing[i] = ctx.createBuffer(WORLD_PUSH_SIZE, - VK10.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true, "rt world push " + i); + pushRing[i] = new PushSlot(ctx.createBuffer(WORLD_PUSH_SIZE, + VK10.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, true, "rt world push " + i)); } } if (output != null) { @@ -765,6 +774,7 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo RtGpuExecutor gpuExecutor = ctx.gpuExecutor(); // Reserve the graphics-use value that guards this frame's reusable TLAS and entity resources. RtGpuExecutor.GraphicsUse graphicsUse = gpuExecutor.beginGraphicsUse(encoder); + RtGpuExecutor.GraphicsUseWaiter graphicsUseWaiter = gpuExecutor.graphicsUseWaiter(); pendingGraphicsUse = graphicsUse; RtEntities.FrameEntities frameEntities = null; VkCommandBuffer cmd = encoder.allocateAndBeginTransientCommandBuffer(); @@ -787,7 +797,10 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo // Select the next BDA ring slot; the generated WorldPushData serializer fills it once all // frame-derived values (including entity addresses and block-breaking entries) are known. pushSlot = (pushSlot + 1) % PUSH_RING; - RtBuffer pushBuf = pushRing[pushSlot]; + PushSlot selectedPushSlot = pushRing[pushSlot]; + graphicsUseWaiter.await(selectedPushSlot.graphicsUse); + selectedPushSlot.graphicsUse.mark(graphicsUse); + RtBuffer pushBuf = selectedPushSlot.buffer; ByteBuffer push = MemoryUtil.memByteBuffer(pushBuf.mapped, WORLD_PUSH_SIZE); frameInvViewProj.set(frameProjection).mul(frameViewRotation).invert(); // flags: PBR BRDF (bit 1, always on) + camera-in-water (so the path tracer starts in the water @@ -897,7 +910,7 @@ private void recordFrame(RtContext ctx, RtPipeline active, GpuTexture nativeColo frameTlas = RtAccel.prepareTlas(ctx, fe.baseInstances(), fe.dynamicInstances(), tlasRing, graphicsUse); } - active.setTlas(frameTlas.accel.handle); + active.setTlas(frameTlas.accel.handle, graphicsUse, graphicsUseWaiter); currentTlasHandle = frameTlas.accel.handle; try (RtFrameStats.Scope ignored = RtFrameStats.FRAME.stage("frame.recordTlas")) { RtAccel.recordTlasBuild(ctx, cmd, frameTlas); @@ -1237,9 +1250,9 @@ public void destroy() { materialEpochTraceGate = false; RtMaterialRegistry.INSTANCE.destroy(); if (pushRing != null) { - for (RtBuffer b : pushRing) { - if (b != null) { - b.destroy(); + for (PushSlot slot : pushRing) { + if (slot != null) { + slot.buffer.destroy(); } } pushRing = null; diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java index 15c0a399..727f5263 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/entity/RtEntities.java @@ -71,8 +71,8 @@ * *

Per-frame cost is real (per-entity capture + buffer uploads + a BLAS build); capped by {@code * -Dcaustica.rt.maxEntities}. Changed-entity geometry and refit scratch reuse the existing per-entity - * frames-in-flight ring; motion uploads suballocate from a per-frame-slot arena. A generic size-bucketed - * recycling free-list was tried and measured slower per-call than trusting VMA's own allocator. + * graphics-timeline-guarded ring; motion uploads suballocate from a guarded per-frame-slot arena. A generic + * size-bucketed recycling free-list was tried and measured slower per-call than trusting VMA's own allocator. */ public final class RtEntities { public static final RtEntities INSTANCE = new RtEntities(); @@ -1792,7 +1792,7 @@ && sameIndexTopology(slot, indices) slot.updatesSinceBuild++; return slot.accel; } - // (Re)build: the selected ring slot is already past the in-flight horizon, so replace its old AS. + // (Re)build: the selected ring slot's exact prior graphics use has completed, so replace its old AS. if (slot.accel != null) { RtAccel.destroyEntityAccel(slot.accel, slot.backing); slot.accel = null; diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtBlockOutlineFeature.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtBlockOutlineFeature.java index 207eefef..eedb1987 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtBlockOutlineFeature.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtBlockOutlineFeature.java @@ -29,6 +29,7 @@ import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtDebugLabels; import dev.comfyfluffy.caustica.rt.RtDeviceBringup; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; import dev.comfyfluffy.caustica.rt.accel.RtImage; import dev.comfyfluffy.caustica.rt.entity.RtEntities; @@ -89,7 +90,8 @@ final class RtBlockOutlineFeature implements RtOverlayFeature { private long boundSet; @Override - public boolean prepare(RtContext ctx, RtOverlayFramePool pool, int width, int height) { + public boolean prepare(RtContext ctx, RtOverlayFramePool pool, RtGpuExecutor.GraphicsUse graphicsUse, + int width, int height) { if (!CausticaConfig.Rt.Overlay.BLOCK_OUTLINE_ENABLED.value()) { return false; } @@ -145,7 +147,7 @@ public boolean prepare(RtContext ctx, RtOverlayFramePool pool, int width, int he vbo.flush(0L, (long) data.length * Float.BYTES); viewProj.set(RtComposite.INSTANCE.currentViewProjection()); - boundSet = accelSet.bind(ctx, tlas); + boundSet = accelSet.bind(ctx, tlas, graphicsUse); return true; } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtGlowOutlineFeature.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtGlowOutlineFeature.java index 86f38b76..ca8c51c5 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtGlowOutlineFeature.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtGlowOutlineFeature.java @@ -14,6 +14,7 @@ import dev.comfyfluffy.caustica.rt.RtComposite; import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtDebugLabels; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; import dev.comfyfluffy.caustica.rt.accel.RtImage; import dev.comfyfluffy.caustica.rt.entity.RtEntities; @@ -54,7 +55,8 @@ final class RtGlowOutlineFeature implements RtOverlayFeature { private int drawCount; @Override - public boolean prepare(RtContext ctx, RtOverlayFramePool pool, int width, int height) { + public boolean prepare(RtContext ctx, RtOverlayFramePool pool, RtGpuExecutor.GraphicsUse graphicsUse, + int width, int height) { if (!RtEntities.glowEnabled()) { return false; } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtNameTagFeature.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtNameTagFeature.java index 64da14ee..cf7a337f 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtNameTagFeature.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtNameTagFeature.java @@ -30,6 +30,7 @@ import dev.comfyfluffy.caustica.rt.RtComposite; import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtDebugLabels; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; import dev.comfyfluffy.caustica.rt.entity.RtEntities; @@ -95,7 +96,8 @@ int vertexCount() { } @Override - public boolean prepare(RtContext ctx, RtOverlayFramePool pool, int width, int height) { + public boolean prepare(RtContext ctx, RtOverlayFramePool pool, RtGpuExecutor.GraphicsUse graphicsUse, + int width, int height) { if (!RtEntities.nameTagsEnabled()) { return false; } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFeature.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFeature.java index 806381a2..7ef1b6da 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFeature.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayFeature.java @@ -3,6 +3,7 @@ import org.lwjgl.vulkan.VkCommandBuffer; import dev.comfyfluffy.caustica.rt.RtContext; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; /** * One world-space overlay effect (glow outline today; block outline, nametags, leash planned) rendered by @@ -14,9 +15,11 @@ public interface RtOverlayFeature { /** * Gather this frame's CPU-side data, lazily create GPU resources, and upload vertex scratch via * {@code pool}. Runs before any command recording; return false to skip {@link #record} this frame. + * {@code graphicsUse} is the exact completion token for resources referenced by the recorded commands. * {@code width}/{@code height} are the composite target's (display-res) extent. */ - boolean prepare(RtContext ctx, RtOverlayFramePool pool, int width, int height); + boolean prepare(RtContext ctx, RtOverlayFramePool pool, RtGpuExecutor.GraphicsUse graphicsUse, + int width, int height); /** * Record this feature's passes. {@code targetView} is {@link RtWorldOverlay}'s shared, mod-owned world- diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayPipelines.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayPipelines.java index 7b865563..28bbbfcd 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayPipelines.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtOverlayPipelines.java @@ -38,6 +38,7 @@ import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtDebugLabels; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; import static dev.comfyfluffy.caustica.rt.RtContext.check; @@ -544,25 +545,32 @@ public static SampledImageSetPool sampledImageSetPool(RtContext ctx, int stageFl * {@link SampledImageSet}) is required because the TLAS handle changes most frames ({@code RtAccel * .TlasRing} cycles it every frame even when it doesn't grow) — rewriting a single set's binding while * an earlier frame's command buffer referencing that same set may still be executing on the GPU is the - * same "descriptor set update while in use" hazard {@code RtPipeline.setTlas} already guards against - * with its own 4-slot ring. + * same "descriptor set update while in use" hazard {@code RtPipeline.setTlas} guards against. Exact + * graphics completion protects both rings; their depths only avoid routine host waits. */ public static final class AccelStructureSet { private static final int RING = 4; public final long layout; private final long pool; private final long[] sets; + private final RtGpuExecutor.TrackedGraphicsUse[] uses; private int current = -1; private AccelStructureSet(long layout, long pool, long[] sets) { this.layout = layout; this.pool = pool; this.sets = sets; + this.uses = new RtGpuExecutor.TrackedGraphicsUse[sets.length]; + for (int i = 0; i < uses.length; i++) { + uses[i] = new RtGpuExecutor.TrackedGraphicsUse(); + } } - /** Advance to the next ring slot, write {@code tlas} into it, and return the set to bind this frame. */ - public long bind(RtContext ctx, long tlas) { + /** Wait for the next ring slot's prior use, write {@code tlas}, and return the set for this frame. */ + public long bind(RtContext ctx, long tlas, RtGpuExecutor.GraphicsUse graphicsUse) { current = (current + 1) % RING; + RtGpuExecutor.TrackedGraphicsUse slotUse = uses[current]; + ctx.gpuExecutor().graphicsUseWaiter().await(slotUse); long set = sets[current]; try (MemoryStack stack = MemoryStack.stackPush()) { VkWriteDescriptorSetAccelerationStructureKHR asWrite = VkWriteDescriptorSetAccelerationStructureKHR.calloc(stack) @@ -573,6 +581,7 @@ public long bind(RtContext ctx, long tlas) { .descriptorCount(1).descriptorType(KHRAccelerationStructure.VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR); VK10.vkUpdateDescriptorSets(ctx.vk(), write, null); } + slotUse.mark(graphicsUse); return set; } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java index 3c7ea473..a0e112e3 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/overlay/RtWorldOverlay.java @@ -87,7 +87,7 @@ public void compositeIntoUiOverlay(RenderTarget main, RtGpuExecutor.GraphicsUse try { List ready = new ArrayList<>(features.size()); for (RtOverlayFeature f : features) { - if (f.prepare(ctx, framePool, main.width, main.height)) { + if (f.prepare(ctx, framePool, graphicsUse, main.width, main.height)) { ready.add(f); } } diff --git a/src/main/java/dev/comfyfluffy/caustica/rt/pipeline/RtPipeline.java b/src/main/java/dev/comfyfluffy/caustica/rt/pipeline/RtPipeline.java index 6f4c3bcb..a03fb52a 100644 --- a/src/main/java/dev/comfyfluffy/caustica/rt/pipeline/RtPipeline.java +++ b/src/main/java/dev/comfyfluffy/caustica/rt/pipeline/RtPipeline.java @@ -31,6 +31,7 @@ import dev.comfyfluffy.caustica.rt.RtContext; import dev.comfyfluffy.caustica.rt.RtDebugLabels; import dev.comfyfluffy.caustica.rt.RtDeviceBringup; +import dev.comfyfluffy.caustica.rt.RtGpuExecutor; import dev.comfyfluffy.caustica.rt.accel.RtAccel; import dev.comfyfluffy.caustica.rt.accel.RtBuffer; @@ -67,17 +68,15 @@ public final class RtPipeline { private static final int MATERIAL_SURFACE0_BINDING = 1; private static final int MATERIAL_NORMAL_AO_BINDING = 2; private static final int MATERIAL_SURFACE1_BINDING = 3; - // A ring of descriptor sets: setTlas writes the next slot (long-unused) rather than mutating the - // slot in-flight frames are still reading, so the TLAS can be swapped without a device drain. - // The TLAS is rebuilt + rebound every frame (dynamic content), so a slot is reused every RING - // frames; RING must exceed the max frames-in-flight (vanilla MC ≤ 3) for the reused slot to be off - // all queues. 6 gives margin and matches the KEEP_FRAMES-style horizon used for resource frees. + // A ring of descriptor sets: setTlas waits for the selected slot's exact prior graphics use before + // rewriting it. Ring depth is only a performance choice that avoids routine host waits. private static final int RING = 6; private final RtContext ctx; private final long descriptorSetLayout; private final long descriptorPool; private final long[] descriptorSets; + private final RtGpuExecutor.TrackedGraphicsUse[] descriptorSetUses; private int currentSet; private final long pipelineLayout; private final long pipeline; @@ -103,6 +102,10 @@ private RtPipeline(RtContext ctx, long dsl, long pool, long[] sets, long layout, this.descriptorSetLayout = dsl; this.descriptorPool = pool; this.descriptorSets = sets; + this.descriptorSetUses = new RtGpuExecutor.TrackedGraphicsUse[sets.length]; + for (int i = 0; i < descriptorSetUses.length; i++) { + descriptorSetUses[i] = new RtGpuExecutor.TrackedGraphicsUse(); + } this.currentSet = 0; this.pipelineLayout = layout; this.pipeline = pipeline; @@ -362,12 +365,12 @@ private static boolean hitGroupUsesAnyHit(int relativeHitGroup) { return entityBucket == RtAccel.ENTITY_BUCKET_ANY_HIT; } - /** - * Bind a new TLAS into the next ring slot (which in-flight frames are no longer reading, since - * swaps are many frames apart) and make it current, so the binding can change without a drain. - */ - public void setTlas(long tlas) { + /** Bind a new TLAS after the selected descriptor slot's exact prior graphics use completes. */ + public void setTlas(long tlas, RtGpuExecutor.GraphicsUse graphicsUse, + RtGpuExecutor.GraphicsUseWaiter graphicsUseWaiter) { currentSet = (currentSet + 1) % RING; + RtGpuExecutor.TrackedGraphicsUse slotUse = descriptorSetUses[currentSet]; + graphicsUseWaiter.await(slotUse); try (MemoryStack stack = MemoryStack.stackPush()) { VkWriteDescriptorSetAccelerationStructureKHR asWrite = VkWriteDescriptorSetAccelerationStructureKHR.calloc(stack) .sType(VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET_ACCELERATION_STRUCTURE_KHR).pAccelerationStructures(stack.longs(tlas)); @@ -376,6 +379,7 @@ public void setTlas(long tlas) { .descriptorCount(1).descriptorType(VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR); VK10.vkUpdateDescriptorSets(ctx.vk(), write, null); } + slotUse.mark(graphicsUse); } /** Write the storage image into every ring slot (set once at init / on resize, when idle). */