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
|
// statics-through-walls fix: a static whose sphere is outside every
|
||||||
// view of its cell no longer paints through the wall (the cottage
|
// view of its cell no longer paints through the wall (the cottage
|
||||||
// phantom staircase's draw path).
|
// 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--)
|
for (int i = pvFrame.OrderedVisibleCells.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
uint cellId = pvFrame.OrderedVisibleCells[i];
|
uint cellId = pvFrame.OrderedVisibleCells[i];
|
||||||
|
|
@ -822,24 +882,9 @@ public sealed class RetailPViewRenderer
|
||||||
if (viewcone.SphereVisibleInCell(cellId, c, r))
|
if (viewcone.SphereVisibleInCell(cellId, c, r))
|
||||||
_cellStaticScratch.Add(e);
|
_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))
|
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
|
||||||
ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext(cellId, slice, _cellStaticScratch));
|
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
|
// #121: cone-surviving dynamics whose emitters draw in the dynamics
|
||||||
// particle pass (survivors minus outside-stage). Cleared per use.
|
// particle pass (survivors minus outside-stage). Cleared per use.
|
||||||
private readonly List<WorldEntity> _dynamicsParticleScratch = new();
|
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>
|
/// <summary>
|
||||||
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
|
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue