feat(render): Campaign OVERHAUL S2 chunk 5 + closeout — registry is the only render membership owner

Chunk 5 (consumer cutover): WalkProductionWorldData's per-cell views are
borrowed from ShadowObjectRegistry.GetRetailPartEntriesInCell and resolved
through RenderSceneQuery.TryGetByLocalEntityId; every render-side sweep,
bucket, parent-cell and root-position fallback is deleted (AD-116 for the
one-frame registry→scene window, counted in UnregisteredRenderMembershipCount).
A live entity with visual parts but no collision geometry registers
render-only (LiveEntityCollisionBuilder computes the part array before the
empty-shapes gate).

Closeout fixes found while landing it:
- RefloodOwnerForLandblock forwards the retained part array — a reflood is
  retail's recalc_cross_cells over the SAME CPartArray; without it every owner
  touched by a landblock replacement commit lost its render membership.
- Non-colliding DAT statics register render-only from BOTH publishers
  (LandblockPhysicsPublisher.PublishStaticEntity,
  LandblockPhysicsContentBuilder.RegisterRenderOnlyStatic). The G2 self-gate
  pixel diff caught them vanishing (Facility Hub wall panels): retail floods
  every object regardless of collision (CEnvCell::init_static_objects
  0x0052c350, add_shadows_to_cells 0x00514ae0).
- S2 dual review fix batch (arch + retail lens, lead-verified):
  Suspend clears the retail product (remove_shadows_from_cells 0x00511230 is
  one transaction); AttachChild/DetachChild advance the mutation revision so
  a prepared SetPosition cannot clobber a child's rows; an attached child
  never floods on its own re-registration; RemoveLandblock and the non-rooted
  RetireOwnerFromLandblock prune retail rows (render-only statics end with
  their landblock); a render-only owner's no-cell-array commit republishes at
  its destination cell (AD-117); an empty non-null part array is treated as
  null; per-move closures/LINQ replaced by index loops; EnvCell shells stay
  out of the scene's LocalEntityId index (payload-less records); the index
  predicate compares the id; the dead per-cell scene indices are deleted.

Register: AD-116 (chunk 5), AD-117 (four residual Contract A/B readings).
Evidence: s2-membership-ownership-map.md §8 (chunk 5) and §9 (closeout).

Gates (Release): Core 4,984/4,984; Content 214/214; Runtime 1,884/1,884;
App hermetic lane 6,760/6,760; App InstalledDat lane 217 pass / 1 skip /
2 pre-existing #383 layout-fixture failures; App Windows lane 1/1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 01:01:47 +02:00
parent f30039d3d9
commit c94a1a407e
20 changed files with 2141 additions and 1026 deletions

View file

@ -193,19 +193,47 @@ per-entity retail CELLARRAY (`TryGetRetailCellArray`) plus per-cell part
entries (`GetRetailPartEntriesInCell`) — not two independent floods, and not
rebuilt per frame. A caller that supplies no part array (only test/legacy
call sites remain) keeps the pre-chunk-3 collision-only dispatch untouched.
`WalkProductionWorldData`'s indoor and outdoor static sweeps BORROW the
retail array whole per record; they no longer recompute membership from each
MeshRef's prepared visual AABB (the deleted `ResolveStaticRenderCells` /
`ShadowObjectRegistry.ComputeStaticRenderCells` pair). A structural
consequence: an object mixing a colliding BSP part with a decorative
non-BSP part now gets a collision row for its BSP part in every cell the
**Consumer cutover to a borrowed per-cell view (S2 chunk 5, 2026-09-03).**
`WalkProductionWorldData`'s three per-frame scene sweeps (indoor static,
outdoor static, dynamic) and their five per-cell bucket dictionaries are
deleted. `GetCellStatics`/`GetCellDynamics`/`GetOutdoorStatics`/
`GetOutdoorDynamics` instead read
`ShadowObjectRegistry.GetRetailPartEntriesInCell` for the QUERIED cell
directly — already in retail CELLARRAY-then-part-array insertion order —
collapse the entries to their distinct owning entity ids (an entity's own
run of entries for one cell is always contiguous, since the registry
removes-then-re-adds a whole entity's rows atomically), and resolve each id
back to its `RenderProjectionRecord` through
`RenderSceneQuery.TryGetByLocalEntityId` — a new presentation-side index
`ArchRenderScene` maintains alongside its existing per-cell/per-class
indices, keyed by the SAME `RenderSourceMetadata.LocalEntityId` the
registry is keyed by (App-only presentation state, never a second
membership source). Statics vs. dynamics is the record's own
`ProjectionClass` (`LiveDynamicRoot`/`EquippedChild` are dynamic; every
other class, including `ActiveAnimatedStatic`, is static — the same split
`ArchRenderScene`'s own internal indexing already used); building shells
are excluded from both — they draw at their own building's shell turn
(`_shellsByAnchor`, still filled by the ONE surviving narrow sweep, since
buildings are the landcell building channel, out of S2's scope), never at
the cell's ordinary object-list turn. A structural consequence carried
over from chunk 3: an object mixing a colliding BSP part with a decorative
non-BSP part gets a collision row for its BSP part in every cell the
combined CELLARRAY reaches, including cells the BSP part alone would not
have crossed — membership is decided by the whole-part-array flood, not per
part; the BSP polygon test still decides actual contact at query time. A
record whose registry entry has no retail array yet (a streaming-window race
between the static-projection journal and the physics publisher) falls back
to its authored parent cell alone, counted by
`WalkProductionWorldData.UnregisteredStaticRenderFallbackCount`.
have crossed — membership is decided by the whole-part-array flood, not
per part; the BSP polygon test still decides actual contact at query time.
An entity the registry HAS flooded into a cell but whose projected record
the presentation journal has not applied yet this frame (the transient
race between the physics publisher and the projection journal — AD-116)
contributes to NO cell for that frame; there is no fallback to an authored
parent cell or a root-position cell any more (both deleted this chunk),
matching retail's own rule that an object not yet in a cell is not drawn.
Every distinct entity id this happens for in one frame is counted once in
`WalkProductionWorldData.UnregisteredRenderMembershipCount` (renamed from
`UnregisteredStaticRenderFallbackCount`, which under the pre-chunk-5 model
also covered the separate dynamic-record fallback) and reported by one
print-only `[walk-membership]` line — gated on
`RenderingDiagnostics.ProbeFacilityStairsEnabled`, the walk family's
existing diagnostic flag — at the start of the next frame when nonzero.
A primitive-only Setup may reuse its already-authored cylsphere collision
cell set; BSP-bearing and pure-visual statics use the all-visual-part box
walk. The Facility Hub stair Setup `0x02000623` is the installed-DAT
@ -217,13 +245,15 @@ Moving multipart objects follow the sibling retail route rather than their
authored/feet cell alone: `CPhysicsObj::add_shadows_to_cells` installs every
`CPartArray` part in every cell of the object's retained `CELLARRAY`, and
`CPhysicsPart::Draw` stamps each part only after that part passes a cell's
portal test. `WalkProductionWorldData.ResolveDynamicRenderCells` therefore
reads `ShadowObjectRegistry.TryGetRetailCellArray` directly for every dynamic
record — the SAME borrow-not-recompute recipe the static sweeps already use
— falling back to `GetOwnerCells` (counted by the shared
`UnregisteredStaticRenderFallbackCount`) only when the registry has not
registered an array yet, while the walk classifier owns a separate
projection+part drawn-pass stamp. Retail advances that stamp after
portal test. `WalkProductionWorldData.GetCellDynamics`/`GetOutdoorDynamics`
therefore read the SAME per-cell `GetRetailPartEntriesInCell` borrowed view
the static getters use (S2 chunk 5), filtered to the dynamic
`ProjectionClass`es — not a per-record `TryGetRetailCellArray` lookup with
its own fallback any more; an entity the registry has flooded into a cell
but the presentation scene cannot resolve yet contributes to no cell there,
counted by the shared `UnregisteredRenderMembershipCount` (AD-116), while
the walk classifier owns a separate projection+part drawn-pass stamp.
Retail advances that stamp after
`LScape::draw` + `FlushAlphaList` and before the interior depth clear
(`PView::DrawCells` @0x005A4886), so a part may draw once in the landscape
pass and once again in the post-clear interior-cell pass; acdream re-arms the
@ -260,9 +290,11 @@ moment an attached projection's `WorldEntity` is registered against its
accepted parent; `CommitProjectionRemoval` (the withdrawal/unparent/teardown
funnel) calls `DetachChild`. A nested attachment (a child of a child)
resolves to the ultimate root by walking the existing attach chain at the
registry, bounded and cycle-safe — `WalkProductionWorldData` no longer walks
an accepted-parent chain of its own; it reads the child's own id straight off
`TryGetRetailCellArray`, identically to any other dynamic record.
registry, bounded and cycle-safe — `WalkProductionWorldData` never walks an
accepted-parent chain of its own; a child's part entries surface through the
SAME per-cell borrowed view (S2 chunk 5) every other dynamic record uses,
resolved to its own projection via `RenderSceneQuery.TryGetByLocalEntityId`,
identically to any other dynamic record.
**Slice I3 prepared collision extension (2026-07-25).** At its introduction,
the package remained format 1 and retained mesh type values 13; bake-tool 4