perf(rendering): retire production entity partition

Route production meshes and attached particles from the exact retained PView frame ranges, while keeping the former WorldEntity partition only for standalone tests and explicit diagnostic/oracle probes. Copy retained source/count facts before arena release so world diagnostics no longer require the old partition. Record the accepted G4 visual gate and open the exact G5 production matrix.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 10:50:57 +02:00
parent 14b48cf553
commit 10ccce3f2d
11 changed files with 452 additions and 125 deletions

View file

@ -1639,9 +1639,13 @@ port in any phase — no separate listing here.
> comparisons have zero mismatches. The first retained-path visual gate exposed
> independent portal scheduling/private-view/ACE-revisit ownership regressions.
> Their correction passed visually while the draw cutover was temporarily
> reverted, which proves the correction but not the retained source.
> `20f9fadb` reapplies the cutover over those fixes; its connected portal/revisit
> re-gate is pending before G5 old-path deletion and production-only profiling.
> reverted. The reapplied retained source subsequently passed the connected
> portal/revisit matrix on 2026-07-25, including stable server objects and
> paperdoll, distant interaction, and correct `/ls` plus spell-recall
> materialization. G4 is complete. G5 cleanup is active: normal production no
> longer builds `InteriorEntityPartition`; mesh and attached-particle ownership
> come from the retained frame product. The exact production-only
> performance/lifecycle matrix remains.
> The historical exact cutover
> rollback remains
> `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`. Slice H is complete:

View file

@ -108,11 +108,13 @@ publication, retirement, upload, warmup, or resource backlog. Capped/uncapped
CPU p99 is 34.7%/30.4% below Slice A and maximum frame allocation is
84.7%/82.2% lower. The user explicitly approved Slices FL, including both
the F/G ECS and Slice J gameplay-owner gates, on 2026-07-24. Slice F and
G0G4 are implemented and G4's automated gate has zero mismatches. The portal
scheduling/private-viewport/ACE-revisit correction passed visually while the
retained draw cutover was temporarily reverted, so `20f9fadb` reapplies the
cutover over those fixes for the pending connected retained-path re-gate. G5
deletion and production-only profiling remain gated on that pass. Slice H is
G0G4 are complete: G4's automated gate has zero mismatches and the user
accepted the corrected retained-path portal/revisit matrix on 2026-07-25.
G5 cleanup is active; normal production now routes meshes and attached
particles from the retained frame product without building the old
`InteriorEntityPartition`. Focused route tests, 3,825 App tests / 3 skips,
and 8,334 solution tests / 5 skips pass. The exact production-only
performance/lifecycle matrix remains before G5 closeout. Slice H is
complete: retained UI/frame work, exact bounded light selection, and
pooled/borrowed/direct network I/O pass 8,292 Release tests / 5 skips and the
seven-checkpoint connected lifecycle/reconnect gate. Slice I is active under

View file

@ -2,8 +2,9 @@
**Date:** 2026-07-24
**Status:** Slices AE complete. Slices FL explicitly approved 2026-07-24;
Slice F is active.
**Status:** Slices AF and H are complete; G0G4 and I0 are complete. Slices
FL were explicitly approved 2026-07-24; G5 production-matrix closeout is
active.
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.

View file

@ -2,8 +2,8 @@
**Date:** 2026-07-24
**Status:** approved 2026-07-24; Slice F complete, G4 retained-path re-gate
active, G5 prepared but not unblocked
**Status:** approved 2026-07-24; Slice F and G4 complete, G5 retained-route
cleanup active
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
@ -28,8 +28,8 @@ retail-faithful gameplay
| G1 — scene-query replacement | complete | Exact `e346f8bb` passed capped and uncapped nine-stop routes plus uncapped dense Arwic: 29,392 + 29,025 + 1,522 same-frame candidate comparisons, zero mismatches, equal counts/digests at every checkpoint, exact binary/source identity, and graceful shutdown. Evidence: `docs/research/2026-07-25-slice-g1-scene-query-candidate-gate.md`. |
| G2 — packed dispatcher input | complete | Exact `f9829d5f` passed capped and uncapped nine-stop routes plus dense Arwic. Candidate order, packed input, classified output, and selection each completed 16,879 + 17,282 + 770 comparisons with zero mismatch; journal/rejection counts were zero and shutdown graceful. Evidence: `docs/research/2026-07-25-slice-g2-packed-dispatcher-gate.md`. |
| G3 — retained classification/storage | complete | Exact `6a026c5a` passed capped and uncapped nine-stop routes plus dense Arwic with 17,069 + 16,827 + 757 exact comparisons in every channel, zero mismatch, warm cross-frame reuse, and graceful shutdown. Evidence: `docs/research/2026-07-25-slice-g3-retained-classification-gate.md`. |
| G4 — production cutover | re-gate active | Exact cutover `ef1d263337997bb030eadb7b8e71d73dc659907a` makes the retained frame product the production entity source at the existing five retail PView stages. Exact `03b10183` passed capped/uncapped nine-stop and uncapped dense-Arwic automation with 46,599 total product comparisons and zero mismatch. The portal correction was visually accepted while the cutover was temporarily reverted by `2c848d41`; that proves the correction but cannot accept the retained draw source. Commit `20f9fadb` reapplies the cutover over the correction and is the current connected visual re-gate. Evidence: `docs/research/2026-07-25-slice-g4-production-cutover-automated-gate.md` and `docs/research/2026-07-25-portal-regression-closeout.md`. |
| G5 | prepared | Old-route deletion and the production-only reference matrix remain blocked only on the connected `20f9fadb` retained-path re-gate. An allocation-free retained-route particle-owner selector and steady RDP sample are prepared without changing the running gate binary. |
| G4 — production cutover | complete | Exact cutover `ef1d263337997bb030eadb7b8e71d73dc659907a` makes the retained frame product the production entity source at the existing five retail PView stages. Exact `03b10183` passed capped/uncapped nine-stop and uncapped dense-Arwic automation with 46,599 total product comparisons and zero mismatch. After the independent portal/private-view/revisit corrections, the user accepted the connected retained-path visual matrix on 2026-07-25: server objects and paperdoll survive travel, distant use works, `/ls` and spell recalls both materialize through the retail purple haze, and neither character pop nor recall tail remains. Evidence: `docs/research/2026-07-25-slice-g4-production-cutover-automated-gate.md` and `docs/research/2026-07-25-portal-regression-closeout.md`. |
| G5 | cleanup active | Normal production no longer builds `InteriorEntityPartition`: mesh and attached-particle ownership now consume the exact retained route ranges. The legacy partition remains only for standalone tests and explicitly enabled diagnostic/oracle probes. Focused route tests, 3,825 App tests / 3 skips, and 8,334 complete-solution tests / 5 skips pass; the exact production-only performance/lifecycle matrix remains. |
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
non-drawing diagnostic infrastructure and therefore is not a visual-cutover
@ -653,9 +653,11 @@ correction is recorded in
`docs/research/2026-07-25-portal-regression-closeout.md`. The user's subsequent
pass occurred while exact cutover `ef1d2633` was temporarily reverted by
`2c848d41`, so it proves those corrections but not the retained draw source.
`20f9fadb` reapplies the cutover over the accepted corrections. G4 acceptance,
old-route deletion, and the G5 production-only matrix remain gated on the
connected retained-path recheck.
`20f9fadb` reapplies the cutover over the accepted corrections. The user
accepted that connected retained path on 2026-07-25, including stable objects
and paperdoll through travel, distant interaction, and the corrected `/ls` and
spell-recall materialization. G4 is complete. G5 removes the old production
partition route and runs the production-only matrix.
### G5 — Physical performance and lifecycle gate

View file

@ -1,7 +1,8 @@
# Modern runtime Slice I — flat collision assets and zero-allocation physics
**Status:** I0 COMPLETE — retail oracle, representative graph fixtures, and
four-mover allocation baselines fixed; accepted G5 closeout now precedes I1
four-mover allocation baselines fixed; active G5 production-matrix closeout
precedes I1
**Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice I
**Purpose:** remove parsed DAT object graphs and steady collision allocations
without changing one retail collision decision, float, comparison, or traversal

View file

@ -2,10 +2,13 @@
## Status
Work in progress. The retained-scene cutover is running at `20f9fadb`, but G5
old-route deletion remains blocked on its connected portal/revisit visual
re-gate. This note records production-only evidence gathered without enabling
the referee or lifecycle automation.
Work in progress. The retained-scene connected visual re-gate passed on
2026-07-25. Normal production now consumes retained frame-product ranges for
both mesh submission and attached-particle ownership and does not build
`InteriorEntityPartition`. The legacy partition is retained only for
standalone tests and explicitly enabled diagnostic/oracle probes. This note
records production-only evidence gathered without enabling the referee or
lifecycle automation; the exact post-removal route matrix is still pending.
## First steady RDP sample
@ -36,8 +39,14 @@ mandatory after the visual re-gate.
`RenderFrameRouteOwnerSelector` derives attached particle owner IDs from the
exact retained PView route ranges. It supports exact-route replace/union and
all-range exclusion without allocation after warmup. This removes the final
particle-routing reason for normal production to build
`InteriorEntityPartition`; it is not wired into the running visual-gate binary.
Four focused tests and the 3,821-pass / 3-skip App Release suite are green
through an isolated output path.
all-range exclusion without allocation after warmup. It now supplies the
landscape-static, outside-dynamic, look-in, cell-static, and dynamic-last
particle passes. `RetailPViewRenderer` builds the former partition only when
the retained frame product is unavailable in a standalone test or when an
explicit legacy comparison/probe is enabled. The production frame result
copies retained source and route counts before releasing its arena, so
diagnostics do not require the partition either.
The focused route suite, 3,825 App tests / 3 skips, Release solution build,
and 8,334 complete-solution tests / 5 skips are green. The production-only
reference matrix below remains the G5 closeout gate.

View file

@ -64,6 +64,12 @@ internal sealed class RetailPViewParticleClassifications
foreach (WorldEntity owner in owners)
_outdoor.Add(owner.Id);
}
public void ReplaceOutdoor(IReadOnlySet<uint> ownerIds)
{
_outdoor.Clear();
_outdoor.UnionWith(ownerIds);
}
}
/// <summary>
@ -452,7 +458,7 @@ internal sealed class RetailPViewPassExecutor :
animatedEntityIds: frame.AnimatedEntityIds);
}
_particleClassifications.ReplaceOutdoor(context.ParticleOwners);
_particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds);
_diagnostics.EmitOutStageParticles(
RenderingDiagnostics.ProbeOutStageEnabled,
@ -539,15 +545,14 @@ internal sealed class RetailPViewPassExecutor :
{
if (_particles is null
|| _particleRenderer is null
|| context.CellEntities.Count == 0)
|| context.ParticleOwnerIds.Count == 0)
{
return;
}
HashSet<uint> visible = _particleClassifications.Visible;
visible.Clear();
foreach (WorldEntity entity in context.CellEntities)
visible.Add(entity.Id);
visible.UnionWith(context.ParticleOwnerIds);
if (visible.Count == 0)
return;
@ -562,15 +567,14 @@ internal sealed class RetailPViewPassExecutor :
public void DrawDynamicsParticles(
RetailPViewFrameInput frame,
IReadOnlyList<WorldEntity> survivors)
IReadOnlySet<uint> ownerIds)
{
if (_particles is null || _particleRenderer is null || survivors.Count == 0)
if (_particles is null || _particleRenderer is null || ownerIds.Count == 0)
return;
HashSet<uint> dynamics = _particleClassifications.Dynamics;
dynamics.Clear();
foreach (WorldEntity entity in survivors)
dynamics.Add(entity.Id);
dynamics.UnionWith(ownerIds);
if (dynamics.Count == 0)
return;

View file

@ -25,6 +25,8 @@ public sealed class RetailPViewRenderer
private static readonly ClipViewSlice NoClipSlice =
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
private static readonly ClipViewSlice[] NoClipSlices = { NoClipSlice };
private static readonly IReadOnlySet<uint> NoParticleOwners =
new HashSet<uint>();
private readonly HashSet<uint> _oneCell = new(1);
// Shell-batch scratch: all of a pass's cells collected for ONE batched
@ -50,7 +52,9 @@ public sealed class RetailPViewRenderer
// #131/#132: the late landscape phase's scene-particle owner survivors
// (statics + outside-stage dynamics passing the slice cone).
private readonly List<WorldEntity> _lateParticleOwnerScratch = new();
private readonly HashSet<uint> _lateParticleOwnerScratch = new();
private readonly HashSet<uint> _cellParticleOwnerScratch = new();
private readonly HashSet<uint> _dynamicParticleOwnerScratch = new();
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
@ -58,6 +62,8 @@ public sealed class RetailPViewRenderer
// call. See InteriorEntityPartition.Partition(Result, ...) — clears in
// place and reuses each cell's list across frames when the cell stays
// visible.
// Slice G4: this is now a diagnostic/fallback oracle only. Normal
// production consumes the retained RenderFrameView routes directly.
private readonly InteriorEntityPartition.Result _partitionResult = new();
// MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across
@ -174,20 +180,6 @@ public sealed class RetailPViewRenderer
passes.PrepareCellBatches(ctx, prepareCells);
InteriorEntityPartition.Partition(
_partitionResult,
prepareCells,
ctx.LandblockEntries,
_partitionObserver);
var partition = _partitionResult;
RetailPViewFrameResult result = _frameResultScratch.Reset(
pvFrame,
clipAssembly,
drawableCells,
partition);
passes.EmitDiagnostics(ctx, result);
// T1 (fused BR-2/3): retail's frame order — static world, then the
// aperture depth writes, then interior cells WHOLE far→near, then
// per-cell statics, then ALL dynamics last (retail draws objects after
@ -218,6 +210,51 @@ public sealed class RetailPViewRenderer
_candidateObserver?.BeginPViewFrame();
try
{
if (_sceneFrameProduct is not null)
{
frameView = _sceneFrameProduct.BuildAndBorrow(
pvFrame,
clipAssembly,
viewcone,
_lookInFrames,
drawableCells,
ctx.Cells,
ctx.AnimatedEntityIds,
ctx.RootCell.IsOutdoorNode);
frameViewBorrowed = true;
frameEntityPasses!.BeginEntityFrame(in frameView);
entityFrameOpen = true;
}
// The retained scene product is the production object source.
// Rebuild the former WorldEntity partition only for the standalone
// fallback and explicitly enabled comparison/probe paths.
InteriorEntityPartition.Result? partition = null;
if (_sceneFrameProduct is null || LegacyPartitionDiagnosticsEnabled)
{
InteriorEntityPartition.Partition(
_partitionResult,
prepareCells,
ctx.LandblockEntries,
_partitionObserver);
partition = _partitionResult;
}
RenderFrameDiagnosticCounts counts = frameViewBorrowed
? frameView.DiagnosticCounts
: LegacyDiagnosticCounts(partition!);
RenderProjectionCounts sourceCounts = frameViewBorrowed
? frameView.SourceDigest.Counts
: LegacySourceCounts(partition!);
RetailPViewFrameResult result = _frameResultScratch.Reset(
pvFrame,
clipAssembly,
drawableCells,
counts,
sourceCounts,
partition);
passes.EmitDiagnostics(ctx, result);
// #118: stage assignment for dynamics under an INTERIOR root. Retail
// draws the OUTSIDE world's objects inside the landscape stage —
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
@ -235,7 +272,7 @@ public sealed class RetailPViewRenderer
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
// BR-2 punch-after-dynamics lesson, reverted 88be519).
_outsideStageDynamics.Clear();
if (!ctx.RootCell.IsOutdoorNode)
if (partition is not null && !ctx.RootCell.IsOutdoorNode)
{
foreach (var e in partition.Dynamics)
{
@ -245,22 +282,6 @@ public sealed class RetailPViewRenderer
}
}
if (_sceneFrameProduct is not null)
{
frameView = _sceneFrameProduct.BuildAndBorrow(
pvFrame,
clipAssembly,
viewcone,
_lookInFrames,
drawableCells,
ctx.Cells,
ctx.AnimatedEntityIds,
ctx.RootCell.IsOutdoorNode);
frameViewBorrowed = true;
frameEntityPasses!.BeginEntityFrame(in frameView);
entityFrameOpen = true;
}
DrawLandscapeThroughOutsideView(
ctx,
passes,
@ -441,7 +462,7 @@ public sealed class RetailPViewRenderer
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
@ -466,7 +487,7 @@ public sealed class RetailPViewRenderer
0,
new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY),
cps.PlaneArray),
Array.Empty<WorldEntity>()));
NoParticleOwners));
}
}
@ -494,8 +515,11 @@ public sealed class RetailPViewRenderer
passes.DrawTransparentCellShells(_oneCell);
_cellStaticScratch.Clear();
if (partition.ByCell.TryGetValue(cellId, out var bucket))
if (partition is not null
&& partition.ByCell.TryGetValue(cellId, out var bucket))
{
_cellStaticScratch.AddRange(bucket);
}
// #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the
// Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE
@ -510,11 +534,31 @@ public sealed class RetailPViewRenderer
// cell is absent from the main cone), and their emitters ride
// the DrawCellParticles call below, not DrawDynamicsParticles
// (which only sees dynamics-last cone survivors).
foreach (var e in partition.Dynamics)
if (e.ParentCellId == cellId)
_cellStaticScratch.Add(e);
if (partition is not null)
{
foreach (var e in partition.Dynamics)
if (e.ParentCellId == cellId)
_cellStaticScratch.Add(e);
}
if (_cellStaticScratch.Count > 0)
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LookInObject,
i,
cellId);
}
else
{
ReplaceOwnerIds(
_cellParticleOwnerScratch,
_cellStaticScratch);
}
if (frameEntityPasses is not null
|| _cellStaticScratch.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LookInObject,
@ -536,7 +580,7 @@ public sealed class RetailPViewRenderer
// nested DrawCells draws objects WITH their emitters.
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
cellId, slice, _cellStaticScratch));
cellId, slice, _cellParticleOwnerScratch));
}
}
}
@ -546,7 +590,7 @@ public sealed class RetailPViewRenderer
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
@ -580,11 +624,19 @@ public sealed class RetailPViewRenderer
passes.EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex);
_outdoorStaticScratch.Clear();
foreach (var e in partition.OutdoorStatic)
if (partition is not null)
{
EntitySphere(e, out var c, out float r);
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
_outdoorStaticScratch.Add(e);
foreach (var e in partition.OutdoorStatic)
{
EntitySphere(e, out var c, out float r);
if (viewcone.SphereVisibleInOutsideSlice(
probeSliceIndex,
c,
r))
{
_outdoorStaticScratch.Add(e);
}
}
}
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LandscapeOutdoorStatic,
@ -636,21 +688,27 @@ public sealed class RetailPViewRenderer
_outdoorStaticScratch.Clear(); // late: dynamics survivors
_lateParticleOwnerScratch.Clear(); // late: statics + dynamics survivors
foreach (var e in partition.OutdoorStatic)
if (partition is not null)
{
EntitySphere(e, out var c, out float r);
bool ownerPass = viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r);
if (ownerPass)
_lateParticleOwnerScratch.Add(e);
foreach (var e in partition.OutdoorStatic)
{
EntitySphere(e, out var c, out float r);
bool ownerPass = viewcone.SphereVisibleInOutsideSlice(
probeSliceIndex,
c,
r);
if (ownerPass)
_lateParticleOwnerScratch.Add(e.Id);
// #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids
// double as an ENTITY-id watchlist here — one line per watched
// outdoor-static owner per CHANGE of its cone verdict.
passes.EmitOutStageOwner(
e,
c,
r,
probeSliceIndex,
ownerPass);
passes.EmitOutStageOwner(
e,
c,
r,
probeSliceIndex,
ownerPass);
}
}
foreach (var e in _outsideStageDynamics)
{
@ -658,9 +716,24 @@ public sealed class RetailPViewRenderer
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
{
_outdoorStaticScratch.Add(e);
_lateParticleOwnerScratch.Add(e);
_lateParticleOwnerScratch.Add(e.Id);
}
}
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_lateParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
probeSliceIndex,
0);
RenderFrameRouteOwnerSelector.Union(
_lateParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
probeSliceIndex,
0);
}
passes.EmitOutStageRouting(
probeSliceIndex,
_outsideStageDynamics,
@ -735,7 +808,10 @@ public sealed class RetailPViewRenderer
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
passes.DrawExitPortalMask(
ctx,
new RetailPViewCellSliceContext(cellId, slice, Array.Empty<WorldEntity>()));
new RetailPViewCellSliceContext(
cellId,
slice,
NoParticleOwners));
}
}
@ -795,13 +871,51 @@ public sealed class RetailPViewRenderer
private void DrawDynamicsLast(
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone,
bool rootIsOutdoor,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (partition.Dynamics.Count == 0)
if (partition is null)
{
RenderFrameRouteOwnerSelector.Replace(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.DynamicLast,
0,
0);
passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.DynamicLast,
0,
0,
Array.Empty<WorldEntity>(),
visibleCellIds: null);
// An owner routed through any pre-clear outside slice already had
// its alpha particles drawn there. Meshes may be submitted in both
// stages, but particles must be emitted exactly once.
RenderFrameRouteOwnerSelector.ExceptRoute(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
if (_dynamicParticleOwnerScratch.Count > 0)
{
passes.DrawDynamicsParticles(
ctx,
_dynamicParticleOwnerScratch);
}
return;
}
if (partition.Dynamics.Count == 0
&& frameEntityPasses is null)
return;
_dynamicsScratch.Clear();
@ -837,14 +951,18 @@ public sealed class RetailPViewRenderer
_dynamicsScratch.Add(e);
}
if (_dynamicsScratch.Count == 0)
if (_dynamicsScratch.Count == 0
&& frameEntityPasses is null)
return;
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.DynamicLast,
0,
0,
_dynamicsScratch);
if (_dynamicsScratch.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.DynamicLast,
0,
0,
_dynamicsScratch);
}
passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy(
ctx,
@ -867,12 +985,28 @@ public sealed class RetailPViewRenderer
// portals went invisible. Outside-stage dynamics are excluded here:
// their emitters already drew in the landscape slice (alpha-blended
// particles must not double-draw, unlike the depth-idempotent meshes).
_dynamicsParticleScratch.Clear();
foreach (var e in _dynamicsScratch)
if (!_outsideStageDynamics.Contains(e))
_dynamicsParticleScratch.Add(e);
if (_dynamicsParticleScratch.Count > 0)
passes.DrawDynamicsParticles(ctx, _dynamicsParticleScratch);
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.DynamicLast,
0,
0);
RenderFrameRouteOwnerSelector.ExceptRoute(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
}
else
{
_dynamicParticleOwnerScratch.Clear();
foreach (var e in _dynamicsScratch)
if (!_outsideStageDynamics.Contains(e))
_dynamicParticleOwnerScratch.Add(e.Id);
}
if (_dynamicParticleOwnerScratch.Count > 0)
passes.DrawDynamicsParticles(ctx, _dynamicParticleOwnerScratch);
}
private void DrawCellObjectLists(
@ -881,11 +1015,40 @@ public sealed class RetailPViewRenderer
PortalVisibilityFrame pvFrame,
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (partition is null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.CellStatic,
0,
0);
passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.CellStatic,
0,
0,
Array.Empty<WorldEntity>(),
visibleCellIds: null);
passes.DrawCellParticles(
ctx,
new RetailPViewCellSliceContext(
0u,
NoClipSlice,
_cellParticleOwnerScratch));
return;
}
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
// the shells (retail DrawCells epilogue: PortalList = cell's views →
@ -940,7 +1103,8 @@ public sealed class RetailPViewRenderer
// 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)
if (frameEntityPasses is not null
|| _allCellStatics.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.CellStatic,
@ -974,10 +1138,31 @@ public sealed class RetailPViewRenderer
// 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)
if (frameEntityPasses is not null
|| _allCellStatics.Count > 0)
{
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.CellStatic,
0,
0);
}
else
{
ReplaceOwnerIds(
_cellParticleOwnerScratch,
_allCellStatics);
}
passes.DrawCellParticles(
ctx,
new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics));
new RetailPViewCellSliceContext(
0u,
NoClipSlice,
_cellParticleOwnerScratch));
}
}
private static void DrawEntityRouteOrLegacy(
@ -1016,9 +1201,6 @@ public sealed class RetailPViewRenderer
// #118: dynamics assigned to the OUTSIDE stage this frame (interior roots
// only) — outdoor-classified + exit-portal straddlers. Cleared per frame.
private readonly List<WorldEntity> _outsideStageDynamics = new();
// #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.
@ -1026,6 +1208,70 @@ public sealed class RetailPViewRenderer
private readonly List<WorldEntity> _allCellStatics = new();
private readonly HashSet<uint> _cellObjCells = new();
private bool LegacyPartitionDiagnosticsEnabled =>
_partitionObserver is not null
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled
|| AcDream.App.Streaming.EntityVanishProbe.Enabled;
internal static RenderFrameDiagnosticCounts LegacyDiagnosticCounts(
InteriorEntityPartition.Result partition)
{
int cellStaticCount = 0;
foreach (List<WorldEntity> bucket in partition.ByCell.Values)
cellStaticCount = checked(cellStaticCount + bucket.Count);
return new RenderFrameDiagnosticCounts(
partition.OutdoorStatic.Count,
cellStaticCount,
partition.Dynamics.Count,
TransformCount: 0,
OpaqueClassificationCount: 0,
AlphaClassificationCount: 0,
LightSetCount: 0,
SelectionPartCount: 0,
RouteCandidateCount:
checked(
partition.OutdoorStatic.Count
+ cellStaticCount
+ partition.Dynamics.Count),
EntityCandidateCount: 0,
MeshPartCount: 0);
}
internal static RenderProjectionCounts LegacySourceCounts(
InteriorEntityPartition.Result partition)
{
int cellStaticCount = 0;
foreach (List<WorldEntity> bucket in partition.ByCell.Values)
cellStaticCount = checked(cellStaticCount + bucket.Count);
int total = checked(
partition.OutdoorStatic.Count
+ cellStaticCount
+ partition.Dynamics.Count);
return new RenderProjectionCounts(
total,
partition.OutdoorStatic.Count,
cellStaticCount,
partition.Dynamics.Count,
ActiveAnimatedStatic: 0,
EquippedChild: 0);
}
private static void ReplaceOwnerIds(
HashSet<uint> destination,
IReadOnlyList<WorldEntity> entities)
{
destination.Clear();
for (int index = 0; index < entities.Count; index++)
{
uint localEntityId = entities[index].Id;
if (localEntityId != 0)
destination.Add(localEntityId);
}
}
/// <summary>
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
/// in the OUTSIDE (landscape) stage — before the gated depth clear and the
@ -1153,7 +1399,7 @@ public interface IRetailPViewPassExecutor
void DrawUnattachedSceneParticles(RetailPViewFrameInput frame);
void FlushLandscapeAlpha();
void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors);
void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlySet<uint> ownerIds);
void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result);
}
@ -1428,20 +1674,42 @@ public sealed class RetailPViewFrameResult
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
public HashSet<uint> DrawableCells { get; private set; } = null!;
public InteriorEntityPartition.Result Partition { get; private set; } = null!;
internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; }
internal RenderProjectionCounts SourceCounts { get; private set; }
internal InteriorEntityPartition.Result? DiagnosticPartition
{ get; private set; }
internal RetailPViewFrameResult Reset(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
InteriorEntityPartition.Result partition)
RenderFrameDiagnosticCounts diagnosticCounts,
RenderProjectionCounts sourceCounts,
InteriorEntityPartition.Result? diagnosticPartition)
{
PortalFrame = portalFrame;
ClipAssembly = clipAssembly;
DrawableCells = drawableCells;
Partition = partition;
DiagnosticCounts = diagnosticCounts;
SourceCounts = sourceCounts;
DiagnosticPartition = diagnosticPartition;
return this;
}
internal RetailPViewFrameResult Reset(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
InteriorEntityPartition.Result diagnosticPartition) =>
Reset(
portalFrame,
clipAssembly,
drawableCells,
RetailPViewRenderer.LegacyDiagnosticCounts(
diagnosticPartition),
RetailPViewRenderer.LegacySourceCounts(
diagnosticPartition),
diagnosticPartition);
}
public readonly record struct RetailPViewLandscapeSliceContext(
@ -1457,7 +1725,7 @@ public readonly record struct RetailPViewLandscapeSliceContext(
public readonly record struct RetailPViewLandscapeLateSliceContext(
ClipViewSlice Slice,
IReadOnlyList<WorldEntity> Dynamics,
IReadOnlyList<WorldEntity> ParticleOwners)
IReadOnlySet<uint> ParticleOwnerIds)
{
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
}
@ -1465,4 +1733,4 @@ public readonly record struct RetailPViewLandscapeLateSliceContext(
public readonly record struct RetailPViewCellSliceContext(
uint CellId,
ClipViewSlice Slice,
IReadOnlyList<WorldEntity> CellEntities);
IReadOnlySet<uint> ParticleOwnerIds);

View file

@ -157,7 +157,7 @@ internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
pviewResult?.PortalFrame,
pviewResult?.ClipAssembly,
pviewResult?.DrawableCells,
pviewResult?.Partition,
pviewResult?.DiagnosticPartition,
exteriorPortalFrame: null,
exteriorClipAssembly: null,
exteriorDrawableCells: null,

View file

@ -1,3 +1,4 @@
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
@ -218,11 +219,15 @@ internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
// outdoor roots whose PView executor does not issue an interior
// depth clear.
depthClear = hasOutsideSlice;
if (pviewResult.Partition.ByCell.Count > 0 || hasOutsideSlice)
RenderProjectionCounts sourceCounts =
pviewResult.SourceCounts;
if (sourceCounts.IndoorCellStatic > 0 || hasOutsideSlice)
sceneParticles = "pviewScoped";
outdoorSceneryDrawn = pviewResult.Partition.OutdoorStatic.Count > 0
outdoorSceneryDrawn =
sourceCounts.OutdoorStatic > 0
&& hasOutsideSlice;
liveDynamicDrawnCount = pviewResult.Partition.Dynamics.Count;
liveDynamicDrawnCount =
sourceCounts.LiveDynamicRoot;
}
else
{

View file

@ -310,6 +310,34 @@ public sealed class RetailPViewPassExecutorTests
"passes.DrawCellParticles(ctx");
}
[Fact]
public void Production_uses_retained_routes_without_rebuilding_legacy_partition()
{
string source = RendererSource();
string drawInside = MethodBody(
source,
"public RetailPViewFrameResult DrawInside(");
Assert.Contains(
"if (_sceneFrameProduct is null || LegacyPartitionDiagnosticsEnabled)",
drawInside,
StringComparison.Ordinal);
AssertAppearsInOrder(
drawInside,
"_sceneFrameProduct.BuildAndBorrow(",
"if (_sceneFrameProduct is null || LegacyPartitionDiagnosticsEnabled)",
"InteriorEntityPartition.Partition(");
Assert.Contains(
"RenderFrameRouteOwnerSelector.Replace(",
source,
StringComparison.Ordinal);
Assert.Contains(
"RenderFrameRouteOwnerSelector.ExceptRoute(",
source,
StringComparison.Ordinal);
}
[Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{
@ -698,7 +726,10 @@ public sealed class RetailPViewPassExecutorTests
public void DrawUnattachedSceneParticles(RetailPViewFrameInput frame) => Operations.Add("unattached-particles");
public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha");
public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles");
public void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors) => Operations.Add("dynamics-particles");
public void DrawDynamicsParticles(
RetailPViewFrameInput frame,
IReadOnlySet<uint> ownerIds) =>
Operations.Add("dynamics-particles");
public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics");
public void BeginEntityFrame(in RenderFrameView view) =>