docs(render): S2 plan packet - membership ownership map and revised chunks

Read-only trace of every production owner of cell membership (render and
physics), the retail transaction they must collapse into (Contracts A/B),
and the five S2 chunks re-cut on that evidence: the two static floods are
one CellTransit walk with different part inputs; retail uses the whole
part array once and registers every part in every array cell; children
inherit at registration; topology consolidation moves to S3.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-02 20:57:04 +02:00
parent 75873886f3
commit e7ad25a705
2 changed files with 139 additions and 15 deletions

View file

@ -360,24 +360,35 @@ registration transaction. Every consumer borrows it; the App-owned render
index, visual-AABB fallbacks, origin-cell fallbacks, and duplicate cell buckets
are deleted.
**Chunks:**
1. Read-only graph snapshot beside existing owners from the same accepted
publication; equality test against source publication; zero pixel change.
2. Static `AddPartsShadow` parity: ordered membership vector per visual part;
old index retained as comparator only inside this chunk; cut render lookup
after the installed-DAT comparator is zero-mismatch. Fixtures: cathedral
**Chunks (revised 2026-09-02 from the ownership trace in
`s2-membership-ownership-map.md`):**
1. Retail CELLARRAY from the whole part array (per part: physics sphere else
drawing sphere, part box) computed in `ShadowObjectRegistry` at the same
registration call as today's collision flood, retained as a new product
beside `_entityToCells`, plus per-cell part entries (entity, part, GfxObj,
cell frame, clip-planes-required) in registration order. Nothing consumes
it yet; a comparator over installed-DAT fixtures reports retail vs current
collision vs current render cells. Zero pixel change.
2. Render statics cut over to the registry's per-cell part entries; delete
`ResolveStaticRenderCells`, `ComputeStaticRenderCells`, the fingerprint
cache, and App's `FromStaticRenderParts` use. Fixtures: cathedral
ramp/stairs/walls, Facility stairs, a 24 m cell-edge crosser, a
landblock-edge object, a non-colliding decoration, a multi-part Setup whose
parts cross different cells.
3. Dynamic/child parity: `add_shadows_to_cells`, removal, movement update,
child inheritance as one transaction keyed by exact incarnation. Fixtures:
local/remote player, NPC, projectile, spell world object, equipped child,
contained child, indoor portal crossing, outdoor crossing, teardown/
reconnect.
4. Consumer cutover in order: indoor static render lookup, outdoor static
render lookup, dynamic render lookup, physics broadphase projection,
particle owner-cell lookup, point-light owner-cell lookup, directional-shadow
reachability. Delete the superseded builders.
3. Collision cut over: `_entityToCells`/`_cells` from the same CELLARRAY.
Physics suites, placement/collision replay fixtures, connected nine-stop
route.
4. Dynamics and children: `add_shadows_to_cells` recursion at registration and
movement; particles own-cell; delete the render-side parent-chain walk.
Fixtures: local/remote player, NPC, projectile, spell world object,
equipped child, contained child, indoor portal crossing, outdoor crossing,
teardown/reconnect.
5. Consumer cutover and deletion: remaining `WalkProductionWorldData` buckets
become borrowed views, particle owner union reads the same, duplicate
dictionaries and origin-cell/visual-AABB fallbacks are deleted. Lights and
directional shadows stay for S5. Topology triplication (`LoadedCell` /
`WalkCell` / flat asset) is consolidated with S3's `WalkPView` state, not
here.
**Review:** architecture (one authority; retry/recenter/reset safe; no
per-frame rebuild) and retail (crossed cells and insertion order match the

View file

@ -0,0 +1,113 @@
# S2 plan packet — cell-membership ownership map and chunk contracts
Date: 2026-09-02. Campaign OVERHAUL S2 (world graph: one membership owner).
Retail contract: `oh1-construction-landscape-contract.md` Contracts A and B.
Current-state evidence: read-only code trace (file:line) taken 2026-09-02 after
S1 closed at `8c6563ca`.
## 1. Retail truth this slice ports
One transaction per object (`CPhysicsObj::calc_cross_cells_static`
`0x00515160`, then `remove_shadows_from_cells` `0x00511230` +
`add_shadows_to_cells` `0x00514ae0`):
1. **CELLARRAY** is computed from the whole part array. Cylsphere route when
`(state & 0x10000) == 0 && part_array != null && GetNumCylsphere() != 0`;
otherwise the bbox route (`find_bbox_cell_list` `0x00510fc0`): seed the
object's cell, then for every array cell call
`CEnvCell::find_transit_cells` `0x0052cae0`, which per portal and per
VISUAL part uses `gfxobj->physics_sphere` else `drawing_sphere` for the
plane reject, the part bounding box (`CPhysicsPart::GetBoundingBox`) for
`Plane::intersect_box` and `box_intersects_cell`, and
`add_all_outside_cells` once for an `0xFFFFFFFF` portal. Insertion order
is retained; nothing prunes outside cells afterwards.
2. **Two products of that one array, in the same order:** per cell a
`CShadowObj` (collision row, `add_shadow_object`) and, when a part array
exists, `CPartArray::AddPartsShadow` `0x00517e40`, which adds EVERY part to
that cell's `shadow_part_list` with `planes = num_shadow_objects > 1 ?
cell->clip_planes : null` and the cell frame. Per-part visibility is NOT a
membership subset; it is decided at draw time by the portal view test and
the drawn stamp (S3).
3. **Children** receive the root's same CELLARRAY recursively in child-list
order. **Particles** (`state & 0x1000`) register only in their own cell.
4. **Removal** is the inverse transaction at the same owner.
## 2. What acdream does today (trace, 2026-09-02)
| 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 |
| `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) |
| `DirectionalShadowCasterFrame` (`DirectionalShadowCasterFrame.cs:229-233`) | flat outdoor indices, no cell filter | caster list | per frame | shadow pass (S5) |
| Topology: `EnvCellLandblockBuildBuilder.BuildVisibilityCell` vs `WalkCellFactory.FromParsed` vs `FlatEnvCellTopology` | same DAT / pak | three distinct portal graphs (`LoadedCell`, `WalkCell`, flat asset) | per landblock build | render, walk, `CellTransit` |
Findings that shape the chunks:
- The two static floods are the SAME `CellTransit` walk with DIFFERENT part
inputs (BSP-only vs all visual parts). Retail uses all parts once. The
render-side flood already carries retail's inputs; the collision side does
not.
- Render membership is whole-object per cell; retail's is every part per
cell (identity retained per part, with cell frame and clip planes).
- Children are inherited only on the render side, at bucket time; retail
inherits at registration.
- `b3b7d922` removed the indoor-seed stab-list prune inside the shared
`BuildShadowCellSetFromParts`, affecting every caller. Retail has no such
prune in the bbox route (Contract A), so the removal is the retail
direction; it is re-verified, not re-litigated, in chunk 3.
- Topology triplication is real but the walk is its consumer; consolidating
it belongs with S3's `WalkPView` state, not S2. S2 owns membership only.
## 3. Chunks
**Chunk 1 — the retail CELLARRAY and the per-part render product, beside
the existing owners (zero pixel change).** Core `ShadowObjectRegistry` gains
a registration input for the whole part array (per part: GfxObj id, part
frame, physics sphere else drawing sphere, part box) and computes retail's
CELLARRAY from it as a new retained `RetailCellArray(entityId)`; it also
records per-cell part entries `(entityId, partIndex, gfxObjId, cell frame,
clipPlanes-required)` in registration order. Existing `_entityToCells`,
`_cells`, and every consumer are untouched. App (`LandblockPhysicsPublisher`)
and Runtime (`LiveEntityCollisionBuilder`) supply the part array at the same
call. A comparator test over installed-DAT fixtures (Facility stair Setup
`0x02000623` in `0x8A02015F`, cathedral ramp `0x020009A2`, the #334 Neftet
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 3 — collision cut over.** `_entityToCells`/`_cells` are populated
from the retail CELLARRAY (collision shapes into every array cell). Physics
gates: Core physics suites, the placement/collision replay fixtures, and the
connected nine-stop route; `CellTransit`'s prune removal is confirmed
against Contract A.
**Chunk 4 — dynamics and children.** `add_shadows_to_cells` recursion:
children register with the root's CELLARRAY at registration and movement;
particles own-cell only; the render-side parent-chain walk is deleted.
**Chunk 5 — consumer cutover and deletion.** Remaining `WalkProductionWorldData`
buckets become borrowed views; particle owner-cell union reads the same;
duplicate dictionaries and fallbacks are deleted; architecture text updated.
Lights and directional shadows stay as they are (S5).
Owner gate G2 after chunk 5 (plan §9 S2).
## 4. Tests that pin current behavior (must be consciously replaced, not
silently broken)
`WalkProductionWorldDataTests` (bucketing), `Issue177StairDescentCameraFloodTests.
FacilityStairAssembly_RegisterAcross015FTo015EWithoutCollisionRows`, the
`CellTransit*` and `BuildShadowCellSetTests` families, `ShadowObjectRegistry*`
and `ShadowShapeBuilder*`, `LiveEntityCollisionBuilderTests`,
`LandblockPhysicsPublisherTests`, `Issue334*`, the flood replay fixtures, and
the Runtime placement/physics suites (full list in the 2026-09-02 trace).