perf(pview): batch per-cell cell-object draws into one cross-cell draw
DrawCellObjectLists called WbDrawDispatcher.Draw once per visible cell (each
orphaning 6 SSBOs + full state setup) — the top CPU-submission sink at dense
Arwic (cellobjects ~3.5ms/frame, measured via [CPU-PHASE]; the frame is
~96% CPU-bound, GPU only 0.5ms). Apply the shipped cells-shell batching
pattern to cell OBJECTS: collapse N per-cell draws into ONE cross-cell draw.
Two-loop structure preserves the statics-before-particles depth order:
loop 1 — per-cell viewcone cull, accumulate all survivors + the union of
cell ids;
one batched DrawEntityBucket for every cell's statics;
loop 2 — per-cell DrawCellParticles, after the statics own the depth buffer.
Correctness: same survivor set (union visibleCellIds gate is equivalent to the
per-cell {cellId} filter); transparency composites equal-or-better (the
dispatcher sorts opaque front-to-back + transparent back-to-front globally,
WbDrawDispatcher.cs:1469-1470); particles still occlude against same-cell
statics (loop 2 runs after the batched draw); dynamics-last + shells + seals
unchanged. Also drops the per-cell new[]{entry} alloc (~50-100/frame) to one.
Pixels identical — draw-mechanism speed only (render-perf is not faithfulness-
gated per the project steer). Spec: docs/superpowers/specs/2026-06-23-cellobject-
draw-batching-design.md. Build + full test suite green (App pview replay tests
incl. HouseExitWalkReplay/Issue130DoorwayStrip pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6491798edf
commit
290e731ce3
1 changed files with 68 additions and 17 deletions
|
|
@ -806,6 +806,66 @@ public sealed class RetailPViewRenderer
|
|||
// statics-through-walls fix: a static whose sphere is outside every
|
||||
// view of its cell no longer paints through the wall (the cottage
|
||||
// phantom staircase's draw path).
|
||||
// Dense-town FPS iteration-1 (spec 2026-06-23-cellobject-draw-batching):
|
||||
// the per-cell DrawEntityBucket calls below were the top CPU sink at Arwic
|
||||
// (cellobjects ~3.5 ms/frame; each WbDrawDispatcher.Draw orphans 6 SSBOs +
|
||||
// full state setup). Collapse them into ONE cross-cell batched draw — the
|
||||
// shipped cells-shell batching pattern applied to cell OBJECTS. Two loops
|
||||
// preserve the statics-before-particles depth order: loop 1 culls +
|
||||
// accumulates every cell's survivors and draws them once; loop 2 runs the
|
||||
// per-cell particle passes AFTER the statics own the depth buffer (particles
|
||||
// depth-test but write no depth). The dispatcher sorts opaque front-to-back
|
||||
// and transparent back-to-front by group distance (WbDrawDispatcher.cs:
|
||||
// 1469-1470), so cross-cell batching composites correctly — equal-or-better
|
||||
// than the old per-cell-bucketed order. visibleCellIds = the union of cells,
|
||||
// so the dispatcher admits exactly the same survivor set.
|
||||
|
||||
// Loop 1: per-cell viewcone cull → accumulate survivors + the union of cells.
|
||||
_allCellStatics.Clear();
|
||||
_cellObjCells.Clear();
|
||||
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;
|
||||
|
||||
int survivorsBefore = _allCellStatics.Count;
|
||||
foreach (var e in bucket)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (viewcone.SphereVisibleInCell(cellId, c, r))
|
||||
_allCellStatics.Add(e);
|
||||
}
|
||||
int survivors = _allCellStatics.Count - survivorsBefore;
|
||||
if (survivors > 0)
|
||||
_cellObjCells.Add(cellId);
|
||||
|
||||
// BR-2 phantom-site probe (T3-updated): post-viewcone survivors.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled)
|
||||
EmitPhantomObjsProbe(cellId, survivors);
|
||||
}
|
||||
|
||||
// ONE batched static-object draw for every visible cell (was N per-cell
|
||||
// WbDrawDispatcher.Draw calls). T1: per-cell STATIC lists only — dynamics
|
||||
// draw in DrawDynamicsLast. T3 (BR-5): each static was sphere-tested against
|
||||
// ITS cell's views above (the statics-through-walls fix is preserved by the
|
||||
// cull; only the draw is batched).
|
||||
if (_allCellStatics.Count > 0)
|
||||
{
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
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];
|
||||
|
|
@ -822,24 +882,9 @@ public sealed class RetailPViewRenderer
|
|||
if (viewcone.SphereVisibleInCell(cellId, c, r))
|
||||
_cellStaticScratch.Add(e);
|
||||
}
|
||||
if (_cellStaticScratch.Count == 0)
|
||||
continue;
|
||||
|
||||
// BR-2 phantom-site probe (T3-updated): post-viewcone survivors.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled)
|
||||
EmitPhantomObjsProbe(cellId, _cellStaticScratch.Count);
|
||||
|
||||
if (_cellStaticScratch.Count > 0)
|
||||
{
|
||||
_oneCell.Clear();
|
||||
_oneCell.Add(cellId);
|
||||
UseIndoorMembershipOnlyRouting();
|
||||
DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
|
||||
}
|
||||
|
||||
// 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). Consumed
|
||||
// synchronously within this iteration (scratch list reuse).
|
||||
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||
ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, _cellStaticScratch));
|
||||
}
|
||||
|
|
@ -855,6 +900,12 @@ public sealed class RetailPViewRenderer
|
|||
// #121: cone-surviving dynamics whose emitters draw in the dynamics
|
||||
// particle pass (survivors minus outside-stage). Cleared per use.
|
||||
private readonly List<WorldEntity> _dynamicsParticleScratch = new();
|
||||
// Dense-town FPS iteration-1 (cellobject batching): all visible cells'
|
||||
// viewcone-surviving statics accumulated for ONE batched DrawEntityBucket,
|
||||
// plus the union of their cell ids for the dispatcher's visibleCellIds gate.
|
||||
// Cleared at the top of DrawCellObjectLists.
|
||||
private readonly List<WorldEntity> _allCellStatics = new();
|
||||
private readonly HashSet<uint> _cellObjCells = new();
|
||||
|
||||
/// <summary>
|
||||
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue