diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 55a11308..0038c896 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -70,10 +70,12 @@ public sealed class RetailPViewRenderer // apparatus (plan Task 5). Zero-alloc: GetTimestamp/GetElapsedTime + a direct- // call local fn (struct closure over _ts, never a delegate). long _ts = System.Diagnostics.Stopwatch.GetTimestamp(); + FrameProfiler.MarkGpu("_start"); // GPU timestamp baseline (=2; CPU-only phases diff ~0) void Phase(string n) { if (FrameProfiler.CpuPhaseEnabled) FrameProfiler.AddCpuPhase(n, System.Diagnostics.Stopwatch.GetElapsedTime(_ts).TotalMilliseconds); + FrameProfiler.MarkGpu(n); _ts = System.Diagnostics.Stopwatch.GetTimestamp(); } @@ -859,35 +861,23 @@ public sealed class RetailPViewRenderer DrawEntityBucket(ctx, _allCellStatics, _cellObjCells); } - // Loop 2: per-cell particles, AFTER the batched statics are in the depth - // buffer. T3 (BR-5): particles gate through the SAME viewcone as their - // owners — the callback receives the cone-surviving entity set, so an - // emitter attached to a culled static no longer draws through the wall - // (the candle-flames-through-walls fix). Re-cull per cell to rebuild the - // owner set; the sphere tests are negligible next to the draw submission - // just removed. Consumed synchronously within this iteration (scratch reuse). - for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--) - { - uint cellId = pvFrame.OrderedVisibleCells[i]; - if (!drawableCells.Contains(cellId)) - continue; - - if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0) - continue; - - _cellStaticScratch.Clear(); - foreach (var e in bucket) - { - EntitySphere(e, out var c, out float r); - if (viewcone.SphereVisibleInCell(cellId, c, r)) - _cellStaticScratch.Add(e); - } - if (_cellStaticScratch.Count == 0) - continue; - - foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) - ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, _cellStaticScratch)); - } + // Cell-particle pass — consolidated across ALL visible cells into ONE + // draw. Was per-cell, and each call re-walked the ENTIRE live particle set + // (DrawRetailPViewCellParticles → ParticleRenderer.Draw enumerates every + // live emitter), i.e. O(cells × particles) — the dense-town cellobjects + // sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION + // (= _allCellStatics, already accumulated above for the batched draw) draws + // EXACTLY the same emitters: the callback gates on owner id (the cone- + // surviving set), the renderer sorts globally back-to-front, and the per- + // cell slice was never used for clipping (the scissor gate was deleted in + // T3 — DrawRetailPViewCellParticles disables clip distances). Runs after + // the batched static draw so emitters depth-test against the statics now in + // the buffer (the statics-before-particles order). cellId/slice are unused + // by the particle pass — pass NoClipSlice + the union owner list. This also + // drops the per-cell BuildDrawList allocations (N → 1). + if (_allCellStatics.Count > 0) + ctx.DrawCellParticles?.Invoke( + new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics)); } // T3 scratch lists (render thread only; cleared per use).