feat(render): S2 chunk 2 - render statics borrow the registry's retail cell array

WalkProductionWorldData no longer floods; its indoor and outdoor static
sweeps read ShadowObjectRegistry.TryGetRetailCellArray (retail's
calc_cross_cells_static @0x00515160 -> AddPartsShadow @0x00517e40
CELLARRAY, computed once at registration). The App-owned render flood
(ResolveStaticRenderCells, its fingerprint cache, the primitive-Setup
special case) and Core's ComputeStaticRenderCells are deleted. The one
remaining fallback, an entity the physics publisher has not registered yet
while the projection journal already published it, buckets to the authored
parent cell and is counted per frame (UnregisteredStaticRenderFallbackCount)
for chunk 5 to judge on the connected route.

The Facility stair pin now registers at the projection's own entity id
(the old pure-function test never carried identity) and reads the retail
array; the installed-DAT comparator compares retail against collision.

Gates (run in the implementer's isolated worktree at identical content):
Release build 0/0; Core Physics 2,202/2,202; App hermetic 6,760/6,760 (the
two added WalkProductionWorldData tests); installed-DAT walk/flood/stair
family 24/24; Runtime 1,884/1,884.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-02 22:31:46 +02:00
parent 2b40c0bb38
commit 707d2803a4
10 changed files with 392 additions and 227 deletions

View file

@ -176,21 +176,33 @@ Likewise `PView::DrawCells` calls `DrawEnvCell` once per admitted cell and
draws its complete constructed shell. Portal polygons are not GPU mesh clips;
pixel occlusion comes from the ordered walk and depth buffer.
**Retail static render-shadow cell membership (2026-08-31).** Indoor
statics are not rendered solely from their authored parent EnvCell. Retail's
**Retail static render-shadow cell membership (2026-08-31; ownership moved
to `ShadowObjectRegistry` by Campaign OVERHAUL S2 chunk 2).** Indoor statics
are not rendered solely from their authored parent EnvCell. Retail's
`CEnvCell::init_static_objects` creates a `CPhysicsObj` for every static,
then `calc_cross_cells_static` + `CPartArray::AddPartsShadow` places every
visual part in each crossed cell's `shadow_part_list`, including decorative
GfxObjs that have no collision BSP. `WalkProductionWorldData` therefore owns
a distinct render-only per-cell index built from each MeshRef's prepared
visual AABB and the existing exact static part transit walk. This must not be
collapsed into `ShadowObjectRegistry`'s collision rows: the two retail lists
share transit math but have different membership. 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 regression: its authored parent is
`0x8A02015F`, while its visual parts also register in `0x8A02015E` and the
adjoining vertical cells.
GfxObjs that have no collision BSP. `ShadowObjectRegistry` owns this product
as a retained per-entity retail CELLARRAY (`TryGetRetailCellArray`) plus
per-cell part entries (`GetRetailPartEntriesInCell`), computed once at the
SAME registration transaction as the collision flood
(`RegisterMultiPart`/`Register`'s optional `partArray:` parameter) — not
rebuilt per frame. `WalkProductionWorldData`'s indoor and outdoor static
sweeps BORROW that array whole per record; they no longer recompute
membership from each MeshRef's prepared visual AABB (the deleted
`ResolveStaticRenderCells` / `ShadowObjectRegistry.ComputeStaticRenderCells`
pair). The retail CELLARRAY remains distinct from `ShadowObjectRegistry`'s
own collision rows (`GetOwnerCells`/`_entityToCells`) — the two retail lists
share transit math but have different membership (collision cutover to the
same CELLARRAY is S2 chunk 3) — until then, 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`.
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
regression: its authored parent is `0x8A02015F`, while its visual parts also
register in `0x8A02015E` and the adjoining vertical cells.
Moving multipart objects follow the sibling retail route rather than their
authored/feet cell alone: `CPhysicsObj::add_shadows_to_cells` installs every

View file

@ -37,7 +37,7 @@ One transaction per object (`CPhysicsObj::calc_cross_cells_static`
| Owner | Inputs | Output | Runs | Consumers |
|---|---|---|---|---|
| `LandblockPhysicsPublisher.PublishStaticEntity``ShadowObjectRegistry.RegisterMultiPart` (`LandblockPhysicsPublisher.cs:930-1079`, `ShadowObjectRegistry.cs:493-584`) | BSP parts only (`ShadowShapeBuilder.FromLandblockBspParts`) or Setup cyl/sphere fallback; `CellTransit.BuildShadowCellSetFromParts` | `_entityToCells[id]` (ordered, the CELLARRAY analog) + `_cells[cell]` `ShadowEntry` rows | once per static at publication | physics broadphase `TransitionTypes.FindObjCollisionsInCell` (`TransitionTypes.cs:3755-3779`); outdoor static render lookup reads it verbatim |
| `WalkProductionWorldData.ResolveStaticRenderCells``ShadowObjectRegistry.ComputeStaticRenderCells` (`WalkProductionWorldData.cs:285-366`, `ShadowObjectRegistry.cs:371-394`) | ALL visual parts (`ShadowShapeBuilder.FromStaticRenderParts`: physics sphere else visual-AABB circumsphere; exact visual box); same `BuildShadowCellSetFromParts` | per-object cell list, cached by fingerprint; whole `RenderProjectionRecord` bucketed into every cell | every `BeginFrame` sweep of `RenderSceneIndex.IndoorCellStatic` | `WalkFrameDriver.GetCellStatics`; `RetailPViewRenderer` particle owner union |
| **DELETED (chunk 2).** Was `WalkProductionWorldData.ResolveStaticRenderCells``ShadowObjectRegistry.ComputeStaticRenderCells`. Now: `WalkProductionWorldData.ResolveIndoorStaticRenderCells`/`ResolveOutdoorStaticRenderCells``ShadowObjectRegistry.TryGetRetailCellArray` (`WalkProductionWorldData.cs`, `ShadowObjectRegistry.cs:634-663`) | none (borrowed) | chunk 1's retained per-entity retail CELLARRAY, computed once at registration | every `BeginFrame` sweep of `RenderSceneIndex.IndoorCellStatic`/`OutdoorStatic` (a borrow, not a recompute) | `WalkFrameDriver.GetCellStatics`; `RetailPViewRenderer` particle owner union; unregistered fallback (indoor: authored `ParentCellId`; outdoor: `GetOwnerCells`) counted by `WalkProductionWorldData.UnregisteredStaticRenderFallbackCount` |
| `LiveEntityCollisionBuilder.Register` / `ShadowObjectRegistry.UpdatePosition` / `CommitSetPosition` (`LiveEntityCollisionBuilder.cs:149-280`, `ShadowObjectRegistry.cs:881-982`) | `ShadowShapeBuilder.FromSetup` (BSP parts, else cylspheres, else spheres) | `_entityToCells` + `_cells` | spawn and every accepted move/appearance change | physics broadphase; dynamic render lookup reads it verbatim |
| `WalkProductionWorldData.ResolveDynamicRenderCells` (`WalkProductionWorldData.cs:247-283`) | `GetOwnerCells`; for an `EquippedChild` with no cells, a 64-hop parent-chain walk | whole-record buckets | every `BeginFrame` | `WalkFrameDriver.GetCellDynamics` |
| `LightManager.SelectForCell` (`LightManager.cs:594-650`) | flat `_all` list, sphere overlap | per-cell light subset | per cell drawn | lighting (S5 owns; retail selects from the current EnvCell + stab list) |
@ -78,12 +78,25 @@ formation, an outdoor landblock-edge static, a multi-part Setup) reports the
three answers side by side: retail CELLARRAY, current collision cells,
current render cells. Differences are evidence for chunks 2/3, not failures.
**Chunk 2 — render statics cut over.** `WalkProductionWorldData` indoor and
outdoor static buckets are filled from the registry's per-cell part entries
(grouped back to records for the existing draw path; per-part draw
admission is S3). `ResolveStaticRenderCells`, `ComputeStaticRenderCells`,
the fingerprint cache, and App's use of `FromStaticRenderParts` are deleted.
The Facility stair pin moves to the registry.
**Chunk 2 — render statics cut over (landed 2026-09-02).** `WalkProductionWorldData`
indoor and outdoor static buckets are filled whole-record-per-cell from the
registry's retail CELLARRAY (`TryGetRetailCellArray`), not from its per-cell
part entries: per-part draw admission is S3's job, and today's draw path
still consumes whole `RenderProjectionRecord`s, so grouping
`GetRetailPartEntriesInCell` back into records here would be a no-op
detour. `ResolveStaticRenderCells`, `ComputeStaticRenderCells`, the
fingerprint cache (`_staticRenderCellCache` + its revision/generation
invalidation), the `primitiveSetup` special case, and
`WalkProductionWorldData`'s use of `ShadowShapeBuilder.FromStaticRenderParts`
are deleted. An entity with no retail array yet (the streaming-window race
between the static-projection journal and the physics publisher — AD-40's
residency reasoning) falls back to a conservative single-cell membership
(indoor: authored `ParentCellId`; outdoor: today's `GetOwnerCells`), counted
by the new `WalkProductionWorldData.UnregisteredStaticRenderFallbackCount`
for chunk 5 to weigh against the connected-route count. The Facility stair
pin (`Issue177StairDescentCameraFloodTests.FacilityStairAssembly_*`) moved to
registering through the production `RegisterMultiPart(..., partArray:)` +
`TryGetRetailCellArray` recipe.
**Chunk 3 — collision cut over.** `_entityToCells`/`_cells` are populated
from the retail CELLARRAY (collision shapes into every array cell). Physics