perf(pview): consolidate per-cell cell-particle pass into one union draw
DrawCellObjectLists drew cell particles PER visible cell, and each call (DrawRetailPViewCellParticles -> ParticleRenderer.Draw) re-walked the ENTIRE live particle set to filter by owner id — O(cells x particles). Measured via [CPU-PHASE] at dense Arwic this was the cellobjects sink (~5.4 ms CPU; the phase's GPU share is 0.01 ms — pure CPU). gpu is 0.5 ms; the dense town is ~96% CPU-bound (the earlier "6.7 ms GPU" was a moving/streaming transient). Static owners are disjoint per cell (InteriorEntityPartition.ByCell partitions by ParentCellId), so the UNION of survivors (= _allCellStatics, already accumulated in loop 1 for the batched entity draw) draws EXACTLY the same emitters in ONE pass: the callback gates on owner id, the renderer sorts globally back-to-front, and the per-cell slice was never used for clipping (scissor gate deleted in T3; DisableClipDistances). Runs after the batched static draw so emitters still depth-test against same-cell statics. Deletes the loop-2 re-cull and collapses the per-cell BuildDrawList allocations N -> 1 (also eases the GC spikes). Adversarial review (3 angles) confirmed emitter-set equivalence, no double- draw, equal-or-better compositing, and no lost per-cell side effect — and found the OLD code DOUBLE-DREW additive particles for multi-view-polygon cells (one DrawCellParticles per slice, same owner set each time; the #121 over-bright class). Consolidation draws each emitter once, fixing that latent bug. Pixels identical-or-better; draw-mechanism speed only. Build + full suite green (pview replay tests incl. HouseExitWalkReplay/TowerAscent/Issue130 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
290e731ce3
commit
9f51a4db18
1 changed files with 19 additions and 29 deletions
|
|
@ -70,10 +70,12 @@ public sealed class RetailPViewRenderer
|
||||||
// apparatus (plan Task 5). Zero-alloc: GetTimestamp/GetElapsedTime + a direct-
|
// apparatus (plan Task 5). Zero-alloc: GetTimestamp/GetElapsedTime + a direct-
|
||||||
// call local fn (struct closure over _ts, never a delegate).
|
// call local fn (struct closure over _ts, never a delegate).
|
||||||
long _ts = System.Diagnostics.Stopwatch.GetTimestamp();
|
long _ts = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
|
FrameProfiler.MarkGpu("_start"); // GPU timestamp baseline (=2; CPU-only phases diff ~0)
|
||||||
void Phase(string n)
|
void Phase(string n)
|
||||||
{
|
{
|
||||||
if (FrameProfiler.CpuPhaseEnabled)
|
if (FrameProfiler.CpuPhaseEnabled)
|
||||||
FrameProfiler.AddCpuPhase(n, System.Diagnostics.Stopwatch.GetElapsedTime(_ts).TotalMilliseconds);
|
FrameProfiler.AddCpuPhase(n, System.Diagnostics.Stopwatch.GetElapsedTime(_ts).TotalMilliseconds);
|
||||||
|
FrameProfiler.MarkGpu(n);
|
||||||
_ts = System.Diagnostics.Stopwatch.GetTimestamp();
|
_ts = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -859,35 +861,23 @@ public sealed class RetailPViewRenderer
|
||||||
DrawEntityBucket(ctx, _allCellStatics, _cellObjCells);
|
DrawEntityBucket(ctx, _allCellStatics, _cellObjCells);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop 2: per-cell particles, AFTER the batched statics are in the depth
|
// Cell-particle pass — consolidated across ALL visible cells into ONE
|
||||||
// buffer. T3 (BR-5): particles gate through the SAME viewcone as their
|
// draw. Was per-cell, and each call re-walked the ENTIRE live particle set
|
||||||
// owners — the callback receives the cone-surviving entity set, so an
|
// (DrawRetailPViewCellParticles → ParticleRenderer.Draw enumerates every
|
||||||
// emitter attached to a culled static no longer draws through the wall
|
// live emitter), i.e. O(cells × particles) — the dense-town cellobjects
|
||||||
// (the candle-flames-through-walls fix). Re-cull per cell to rebuild the
|
// sink (~5 ms at Arwic). Static owners are disjoint per cell, so the UNION
|
||||||
// owner set; the sphere tests are negligible next to the draw submission
|
// (= _allCellStatics, already accumulated above for the batched draw) draws
|
||||||
// just removed. Consumed synchronously within this iteration (scratch reuse).
|
// EXACTLY the same emitters: the callback gates on owner id (the cone-
|
||||||
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
// 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
|
||||||
uint cellId = pvFrame.OrderedVisibleCells[i];
|
// T3 — DrawRetailPViewCellParticles disables clip distances). Runs after
|
||||||
if (!drawableCells.Contains(cellId))
|
// the batched static draw so emitters depth-test against the statics now in
|
||||||
continue;
|
// the buffer (the statics-before-particles order). cellId/slice are unused
|
||||||
|
// by the particle pass — pass NoClipSlice + the union owner list. This also
|
||||||
if (!partition.ByCell.TryGetValue(cellId, out var bucket) || bucket.Count == 0)
|
// drops the per-cell BuildDrawList allocations (N → 1).
|
||||||
continue;
|
if (_allCellStatics.Count > 0)
|
||||||
|
ctx.DrawCellParticles?.Invoke(
|
||||||
_cellStaticScratch.Clear();
|
new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics));
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// T3 scratch lists (render thread only; cleared per use).
|
// T3 scratch lists (render thread only; cleared per use).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue