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:
parent
f30039d3d9
commit
c94a1a407e
20 changed files with 2141 additions and 1026 deletions
|
|
@ -37,11 +37,11 @@ One transaction per object (`CPhysicsObj::calc_cross_cells_static`
|
|||
| Owner | Inputs | Output | Runs | Consumers |
|
||||
|---|---|---|---|---|
|
||||
| **Chunk 3 (2026-09-02).** `LandblockPhysicsPublisher.PublishStaticEntity` → `ShadowObjectRegistry.RegisterMultiPart` (`LandblockPhysicsPublisher.cs:930-1079`, `ShadowObjectRegistry.cs`, `RegisterMultiPart`/`Register`/`ComputeContractACellArray`/`PublishRetailCellArray`) | the WHOLE visual part array (`ShadowShapeBuilder.FromStaticRenderParts`, chunk 1b) drives ONE Contract A flood (cylsphere route from the collision shapes' cylinders, else bbox route over the part array); a caller with no part array (every unit-test fixture, `partArray ?? shapes`) keeps the pre-chunk-3 legacy dispatch byte-for-byte | `_entityToCells[id]`/`_cells[cell]` (collision) AND `_retailCellArrays`/`_retailPartEntriesByCell` (render) from the SAME array — the second (redundant) flood chunk 1 left running is deleted | once per static at publication | physics broadphase `TransitionTypes.FindObjCollisionsInCell` (`TransitionTypes.cs:3755-3779`); `WalkProductionWorldData` (below) |
|
||||
| **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, now the SAME array chunk 3 also uses for collision | 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` |
|
||||
| **END STATE (chunk 5, 2026-09-03).** `WalkProductionWorldData.GetCellStatics`/`GetCellDynamics`/`GetOutdoorStatics`/`GetOutdoorDynamics` → `ShadowObjectRegistry.GetRetailPartEntriesInCell(cellId)` → distinct entity ids in entry order → `RenderSceneQuery.TryGetByLocalEntityId` (`WalkProductionWorldData.cs` `ResolveCellView`; `RenderSceneContracts.cs`/`ArchRenderScene.cs` new `TryGetByLocalEntityId`/`_byLocalEntityId`). Superseded, in order: `ResolveStaticRenderCells`/`ComputeStaticRenderCells` (chunk 2 deleted these) → `ResolveIndoorStaticRenderCells`/`ResolveOutdoorStaticRenderCells`/`ResolveDynamicRenderCells` + the five per-cell bucket dictionaries + the three per-frame scene sweeps (chunk 5 deletes ALL of these) | none (borrowed, per QUERIED cell, on demand — no per-frame sweep of the scene at all any more) | one cell's retail `RetailPartEntry` rows, filtered to the record's own `ProjectionClass` (static vs. dynamic) and excluding `IsBuildingShell` | every `Get*` call at a walk turn (lazy, cached per cell per frame in the SAME `_cellCache`/`_cellDynamicCache`/`_outdoorMaterialized`/`_outdoorDynamicsMaterialized` dictionaries chunk 2 introduced) | `WalkFrameDriver.GetCellStatics`/`GetCellDynamics`/`GetOutdoorStatics`/`GetOutdoorDynamics`; `RetailPViewRenderer` particle owner union (`GetCellStatics`, unchanged call); unregistered fallback is now "contributes to no cell" (no authored-parent or root-position substitute), counted by the renamed `WalkProductionWorldData.UnregisteredRenderMembershipCount` (once per DISTINCT entity per frame, not per cell visit) — AD-116 |
|
||||
| **Chunk 4 (2026-09-02).** `LiveEntityCollisionBuilder.Register` / `ShadowObjectRegistry.UpdatePosition` / `CommitSetPosition` (`LiveEntityCollisionBuilder.cs:149-280`, `ShadowObjectRegistry.cs`) | `ShadowShapeBuilder.FromSetup` (collision dispatch) + `FromSetupRenderParts` (chunk 1b's part array) → the same ONE Contract A flood as above at registration; movement (`UpdatePosition`/`CommitSetPosition`'s `Recalculate` action) forwards to the same registration entry points, so it inherits the fix. **RESOLVED (chunk 4):** `CommitSetPosition`'s `None`/`Preserve`/`Replace` actions (`RefreshPositionRows`/`ReplacePositionRows`) now publish the retail render product from the SAME exact collision cell list the move just used (`PublishRetailProductFromExactCells`) — the independent Contract A recompute chunk 1/3 ran on every move (`RecomputeRetailCellArrayIfPresent`) is deleted. Mirrors retail `SetPositionInternal`'s `add_shadows_to_cells(this, &arg2->cell_array)` (pc:283536-283537), which consumes the transition's own array for both products in one call; the `num_cells > 0` keep-when-empty gate (pc:283540) now covers both products identically | `_entityToCells` + `_cells`, and (when a part array is retained) `_retailCellArrays`/`_retailPartEntriesByCell`, both from the SAME exact cell list on every move | spawn and every accepted move/appearance change | physics broadphase; `WalkProductionWorldData` dynamic render lookup (now a direct `TryGetRetailCellArray` read, see below) |
|
||||
| **Chunk 4 (2026-09-02).** The staged `ShadowObjectRegistry.TryPrepareSetPosition`/`TryApplySetPosition` pipeline | `PreparedShadowOwnerState` carries `RetailPartArray`/`RetailCellIds`/`RetailRoute`/`RetailRows`; `TryCaptureOwnerState`/`InstallOwnerState` seed them so the staging registry's own `CommitSetPosition` call inherits chunk 4's one-array-two-products move path for free; `PrepareRetailPartEntryReplacements` diffs the staged retail rows the same way `PrepareCellReplacements` diffs collision rows; `TryApplySetPosition` additionally calls `RepublishAttachedChildren` on the LIVE registry after publishing, since the staging registry carries none of the live attach-chain state | `_retailPartEntriesByCell` publishes alongside `_cells` on `TryApplySetPosition`; every attached child of the moved root republishes too | every live entity moving through the transactional SetPosition publication tail (Runtime) | same as direct `CommitSetPosition`, now consistent with it |
|
||||
| **Chunk 4 (2026-09-02).** `ShadowObjectRegistry.AttachChild`/`DetachChild` (`ShadowObjectRegistry.cs`) — retail's `add_shadows_to_cells` child-inheritance recursion (Contract B) | `EquippedChildRenderController.TryRealize` calls `AttachChild(childEntityId, rootEntityId, childPartArray)` at the exact moment the attached projection's `WorldEntity` is registered against its accepted parent, using `ShadowShapeBuilder.FromSetupRenderParts` over the SAME `PhysicsDataCache` resolvers `LiveEntityCollisionBuilder` uses; `CommitProjectionRemoval` (the single withdrawal/unparent/teardown funnel) calls `DetachChild`. A nested attachment (child of a child) resolves to the ultimate root by walking the existing chain, bounded and cycle-safe | new `_childParent`/`_parentChildren`/`_childPartArrays` dictionaries (own `CollisionWorldState` fields, reset with everything else); PART ENTRIES only for the child — never a collision row, since acdream's attached projections own no independent collision shapes | attach/detach, and every time the root's array republishes (registration, move, staged apply, `ReplaceMultiPartPayload`) | `WalkProductionWorldData.ResolveDynamicRenderCells` (a plain `TryGetRetailCellArray` read on the CHILD's own id — no parent walk) |
|
||||
| **RESOLVED (chunk 4).** `WalkProductionWorldData.ResolveDynamicRenderCells` (`WalkProductionWorldData.cs`) | `ShadowObjectRegistry.TryGetRetailCellArray` directly, for every dynamic record including equipped children (their array is published at attach time by the registry, above); unregistered fallback is `GetOwnerCells`, counted by the SAME `UnregisteredStaticRenderFallbackCount` the static buckets use — the render-side 64-hop parent-chain walk and its `FindParentLocalId` plumbing into this class are deleted | whole-record buckets | every `BeginFrame` | `WalkFrameDriver.GetCellDynamics` |
|
||||
| **DELETED (chunk 5).** Was `WalkProductionWorldData.ResolveDynamicRenderCells` (a per-record `TryGetRetailCellArray` read). Dynamics now share the SAME `ResolveCellView` end-state row above as statics — no record-keyed lookup, no separate dynamic resolver, no separate fallback | — | — | — | superseded by the END STATE row above |
|
||||
| `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` |
|
||||
|
|
@ -117,10 +117,11 @@ movement; particles own-cell only (still no production path reaches this
|
|||
registry — chunk 3's finding stands, deferred); the render-side parent-chain
|
||||
walk is deleted. See §7.
|
||||
|
||||
**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).
|
||||
**Chunk 5 — consumer cutover and deletion (landed 2026-09-03).** 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). See §8 for the evidence.
|
||||
|
||||
Owner gate G2 after chunk 5 (plan §9 S2).
|
||||
|
||||
|
|
@ -479,3 +480,245 @@ nesting, and cycle rejection). App suite (hermetic lanes): 6,761/6,761 (was
|
|||
parent-chain-walk test replaced by two direct-registry tests). Runtime suite:
|
||||
1,884/1,884 (unchanged — chunk 4 touched no Runtime source). Full solution
|
||||
Release build: 0 warnings, 0 errors.
|
||||
|
||||
## 8. Chunk 5 evidence (2026-09-03)
|
||||
|
||||
**A — borrowed per-cell views.** `WalkProductionWorldData.BeginFrame`'s three
|
||||
per-frame scene sweeps (indoor static, outdoor static, dynamic) and their
|
||||
scratch arrays (`_indoorSweepScratch`, `_dynamicSweepScratch`), the five
|
||||
per-cell bucket dictionaries (`_indoorByCell`, `_indoorDynamicsByCell`,
|
||||
`_outdoorByCell`, `_outdoorDynamicsByCell`), `BucketIndoorRecord`/
|
||||
`BucketOutdoorRecord`/`BucketDynamicRecord`, and
|
||||
`ResolveIndoorStaticRenderCells`/`ResolveOutdoorStaticRenderCells`/
|
||||
`ResolveDynamicRenderCells` are all deleted. `GetCellStatics`/
|
||||
`GetCellDynamics`/`GetOutdoorStatics`/`GetOutdoorDynamics` share one private
|
||||
resolver, `ResolveCellView(cellId, dynamic)`: read
|
||||
`ShadowObjectRegistry.GetRetailPartEntriesInCell(cellId)` (already in retail
|
||||
CELLARRAY-then-part-array insertion order), collapse ADJACENT entries with
|
||||
the same `EntityId` to their distinct owning entity ids (safe because
|
||||
`PublishRetailPartEntries` always removes-then-re-adds a whole entity's rows
|
||||
atomically — one entity's own run for a cell is never interleaved with
|
||||
another's), resolve each id to its `RenderProjectionRecord` via the new
|
||||
`RenderSceneQuery.TryGetByLocalEntityId`, filter to the record's own
|
||||
static-vs-dynamic `ProjectionClass` (`LiveDynamicRoot`/`EquippedChild` are
|
||||
dynamic — the SAME split `ArchRenderScene.IsDynamic` already used
|
||||
privately), and exclude `IsBuildingShell`. Indoor vs. outdoor needs no
|
||||
dispatch of its own: the CALLER already queries the right id space (an
|
||||
indoor `GetCellStatics(cellId)` call and an outdoor `GetOutdoorStatics(cellId)`
|
||||
call each pass a cell id from their own id space), and the registry has
|
||||
already published the entity into every crossed cell under its true id
|
||||
either way. The per-frame result caches (`_cellCache`,
|
||||
`_cellDynamicCache`, `_outdoorMaterialized`, `_outdoorDynamicsMaterialized`)
|
||||
and the arena (`_arena`/`AppendToArena`) are UNCHANGED — chunk 5 adds one new
|
||||
grow-on-demand scratch buffer, `_cellViewScratch` (starts at 64 records),
|
||||
that `ResolveCellView` fills before a single `AppendToArena` call per cell,
|
||||
matching the existing `_sweepScratch` growth shape. Building shells keep
|
||||
their own narrow survivor sweep (`_shellsByAnchor`, `GetBuildingShellStatics`)
|
||||
exactly as before — out of S2's scope (the landcell building channel).
|
||||
|
||||
The scene-side half of this borrow is new: `RenderSceneQuery` gains
|
||||
`TryGetByLocalEntityId(uint localEntityId, out RenderProjectionRecord)`
|
||||
(`RenderSceneContracts.cs`'s `IRenderSceneQuerySource` interface + the query
|
||||
struct forwarder), implemented in `ArchRenderScene` by a new
|
||||
`Dictionary<uint, RenderProjectionId> _byLocalEntityId` maintained at the
|
||||
SAME `AddToIndices`/`RemoveFromIndices` insertion points as the existing
|
||||
per-cell (`_cellStatics`/`_cellDynamics`) and per-class indices — purely
|
||||
presentation-side derived state, cleared by `ClearIndices` alongside
|
||||
everything else, never a second membership source. The one other
|
||||
`IRenderSceneQuerySource` implementation in the tree, the
|
||||
`DirectionalShadowCasterFrameTests.QuerySource` test fake, gained a matching
|
||||
linear-scan implementation so the interface stays satisfiable outside
|
||||
production.
|
||||
|
||||
**B — effect-object render-only registration.** `LiveEntityCollisionBuilder.
|
||||
Build` (Runtime) now computes `renderParts` (`ShadowShapeBuilder.
|
||||
FromSetupRenderParts`) BEFORE its early-return gate, and returns a non-null
|
||||
`LiveEntityCollisionRegistration` whenever EITHER `shapes` (collision) or
|
||||
`renderParts` is non-empty (or `retainEmptyPayload` is set) — previously an
|
||||
empty `shapes` list alone (a Setup with visual parts but no CylSphere/
|
||||
Sphere/physics-BSP part) returned `null` unconditionally, silently dropping
|
||||
`renderParts` too and never reaching the registry at all. The materializer
|
||||
call site (`DatLiveEntityProjectionMaterializer.cs:751-763`) needed NO
|
||||
change: it already calls `LiveEntityCollisionBuilder.Register(_shadows,
|
||||
collision)` whenever `Build` returns non-null, and `Register` already
|
||||
forwards `registration.RenderParts` as `RegisterMultiPart`'s `partArray:`
|
||||
argument.
|
||||
|
||||
`ShadowObjectRegistry.RegisterMultiPart`'s `shapes.Count == 0` branch no
|
||||
longer unconditionally deregisters: when a non-empty `partArray` is also
|
||||
supplied, it calls the new private `RegisterRenderOnly`, which floods via
|
||||
`ComputeContractACellArray` with an EMPTY `collisionShapes` argument (so
|
||||
Contract A's cylsphere test is always false and the bbox route always wins —
|
||||
"route per Contract A with an empty collision dispatch"), publishes the
|
||||
resulting CELLARRAY/`RetailPartEntry` rows exactly like any other
|
||||
registration, and retains `_entityShapes[entityId] = []` (not absent) so
|
||||
`UpdatePosition`'s `IsMultiPart && _entityShapes.TryGetValue` dispatch and
|
||||
`ReplacePositionRows`'s per-shape loop both take the correct "multi-part,
|
||||
zero shapes" branch on a later move instead of falling back to the
|
||||
single-shape `Register` path or synthesizing a bogus zero-radius collision
|
||||
entry. `ReplaceMultiPartPayload`'s own "not yet registered" early-return
|
||||
guard gained the same `partArray`-aware condition, and `ReplacePositionRows`
|
||||
gained a `wroteCollisionEntries` guard so a render-only entity moving
|
||||
through the direct (non-staged) `CommitSetPosition` path never gets a ghost
|
||||
`_entityToCells` entry with zero backing `ShadowEntry` rows. NO collision row
|
||||
is EVER published for a render-only entity — `GetOwnerCells` stays empty
|
||||
across register/move/deregister — matching Contract B exactly (an object
|
||||
contributes to `shadow_object_list` only when it has a part TO shadow
|
||||
there).
|
||||
|
||||
One adjacent gap was found and left OPEN, not fixed: `ShadowObjectRegistry.
|
||||
RefloodOwnerForLandblock` (the STATIC/offline `LandblockPhysicsContentBuilder`
|
||||
streaming-reflood path, `AcDream.Content`) calls `RegisterMultiPart`/
|
||||
`Register` without forwarding the entity's retained `partArray` at all —
|
||||
since `RegisterMultiPart`'s body always `DeregisterCore`s (which clears the
|
||||
retail array) before re-flooding, ANY reflooded entity with a retained retail
|
||||
part array silently loses its render product on reflood, and a genuinely
|
||||
render-only entity would be deregistered outright (its `shapes.Count == 0`
|
||||
with no forwarded `partArray` hits the `Deregister` branch). This predates
|
||||
chunk 5 (it exists since chunk 1b/3 threaded `partArray:` through every OTHER
|
||||
call site) and is out of chunk 5's live-entity-materializer scope — no
|
||||
production live-entity path reaches `RefloodOwnerForLandblock` (only the
|
||||
offline static content builder does) — but it is a real gap for
|
||||
landblock-edge STATICS reflooded near a streaming boundary and should be
|
||||
scoped as its own small follow-up.
|
||||
|
||||
**C — the one remaining fallback.** Under the borrowed-view model the
|
||||
former TWO fallback directions collapse to ONE. "Scene has a record, registry
|
||||
never registered it at all" is not observable any more — `ResolveCellView`
|
||||
only ever iterates registry entries, so such an entity is simply never
|
||||
visited (zero code, matching "an object not yet in a cell is not drawn"
|
||||
exactly, no counter). "Registry HAS flooded the entity into a cell, but the
|
||||
scene's projected record isn't resolvable yet" (the presentation journal
|
||||
lagging the physics publisher within the same frame) is the one real,
|
||||
observable race: `ResolveCellView`'s `!_scene.TryGetByLocalEntityId(...)`
|
||||
branch counts the entity ONCE per frame (via a per-frame `HashSet<uint>
|
||||
_unregisteredEntitiesThisFrame`, so an entity crossing several cells or
|
||||
queried through both the static/dynamic getter pair is not double-counted)
|
||||
into the renamed `WalkProductionWorldData.UnregisteredRenderMembershipCount`,
|
||||
and contributes to NO cell — no authored-parent or root-position substitute
|
||||
survives. `BeginFrame` prints one `[walk-membership] unregistered=N
|
||||
tupleLandblock=0x…` line, gated on the existing walk-family flag
|
||||
`RenderingDiagnostics.ProbeFacilityStairsEnabled` (reused, no new env var),
|
||||
reporting the PRIOR frame's final count at the start of the next
|
||||
`BeginFrame` — the only point a "frame is done" signal exists in this
|
||||
class's lazy, on-demand `Get*` model. AD-116 files this deviation citing
|
||||
AD-49's residency-race reasoning (the register's OTHER residency-timing row;
|
||||
the ownership-map's original chunk-2 citation of "AD-40" was already stale
|
||||
against the current register — AD-40 there is the unrelated fsf transient-bit
|
||||
row — AD-49 is the one that actually argues residency-race safety).
|
||||
|
||||
**D — `GetOwnerCells` / `TryGetRetailCellArray` App-layer consumer audit.**
|
||||
`grep -rn "\.GetOwnerCells(" src/AcDream.App/ src/AcDream.Runtime/
|
||||
src/AcDream.Content/` after chunk 5 finds exactly ONE call site:
|
||||
`WalkProductionWorldData.EmitFacilityShadowProbe` (unchanged this chunk) —
|
||||
a diagnostic probe, gated on `ProbeFacilityStairsEnabled`, that PRINTS the
|
||||
collision-owner cell set for the authored Facility/cathedral stair
|
||||
SourceIds and the local player for comparison; it never feeds admission or
|
||||
membership. `grep -rn "\.TryGetRetailCellArray(\|\.GetRetailPartEntriesInCell("
|
||||
src/AcDream.App/` finds every call site inside `WalkProductionWorldData.cs`
|
||||
itself (`ResolveCellView`, `StaticBucketContains`) — nothing else in App
|
||||
reconstructs or reads registry membership. `RetailPViewRenderer`'s particle
|
||||
owner union calls `GetCellStatics` (the borrowed view), unchanged.
|
||||
|
||||
**Test evidence.** Core `ShadowObjectRegistryRetailCellArrayTests`: 27/27
|
||||
(was 22; +5 new fixtures covering item B's render-only registration,
|
||||
keep-when-empty, deregister, and move). App `LiveEntityCollisionBuilderTests`:
|
||||
13/13 (was 12; +1 covering `Build`/`Register` producing a render-only
|
||||
registration end to end against a real registry). App
|
||||
`WalkProductionWorldDataTests`: 11/11 (was 12 pre-chunk-5 — 8 bucket-primitive
|
||||
tests and 2 `ResolveDynamicRenderCells` tests deleted with their methods, 9
|
||||
new tests added against the real production path — a real bare
|
||||
`ShadowObjectRegistry` + real `ArchRenderScene`, never a hand-fed bucket
|
||||
dictionary — covering registry-driven resolution, the one remaining
|
||||
fallback + its per-entity-not-per-cell counting, static/dynamic split,
|
||||
building-shell exclusion, outdoor parity, per-frame caching, and
|
||||
`StaticBucketContains`). Net App suite delta: 0 (6,761/6,761 both before and
|
||||
after, since the file-level and materializer-level deltas cancel exactly).
|
||||
`Issue177StairDescentCameraFloodTests.FacilityStairAssembly_
|
||||
RegisterAcross015FTo015E` (Lane=InstalledDat) required only the
|
||||
`UnregisteredStaticRenderFallbackCount` → `UnregisteredRenderMembershipCount`
|
||||
rename — its own registry/scene wiring already matches the chunk-5 model
|
||||
byte-for-byte since it looks records up by `LocalEntityId`, not by any
|
||||
formulaic id.
|
||||
|
||||
**Automated gate.** Full Release solution build: 0 warnings, 0 errors. Core
|
||||
`Lane!=Timing&Lane!=Linux`: 4,975/4,975 (4,970 + 5 new). App hermetic lanes:
|
||||
6,761/6,761 (unchanged net). App `Lane=InstalledDat`: 147 passed / 97 skipped
|
||||
/ 1 failed — the one failure,
|
||||
`TowerAscentReplayTests.TowerAscent_StaircaseStaysConeVisible_EveryStep`,
|
||||
is PRE-EXISTING and unrelated: the test file has no reference to
|
||||
`WalkProductionWorldData` or `ShadowObjectRegistry` at all, its last commit
|
||||
predates Campaign OVERHAUL entirely, and `docs/ISSUES.md`'s #119-residual
|
||||
entry already documents this exact pin as needing re-validation against the
|
||||
production resolver before it can be trusted ("its band came from the
|
||||
harness's AABB root model, not the production sweep") — an S3-scope
|
||||
viewcone/cone-cull concern, not S2 membership. Runtime
|
||||
`Lane!=Timing&Lane!=Linux`: 1,884/1,884 (unchanged — chunk 5 touched no
|
||||
Runtime test-visible behavior beyond `LiveEntityCollisionBuilder.Build`'s
|
||||
early-return condition, already covered from the App side).
|
||||
|
||||
**Allocation note.** `ResolveCellView` performs no per-call heap allocation
|
||||
after warmup: `GetRetailPartEntriesInCell` returns an existing `List<T>`
|
||||
cast to `IReadOnlyList<T>` (no copy), the entity-id dedup loop is a plain
|
||||
`for` over that list with two `uint` locals, `_cellViewScratch` is a
|
||||
grow-on-demand array reused every call (doubles only when a cell's
|
||||
membership exceeds its current capacity), and `AppendToArena` copies into
|
||||
the existing per-frame arena (`_arena`), also grow-on-demand. The one
|
||||
per-frame allocation-shaped addition is `_unregisteredEntitiesThisFrame`
|
||||
(a `HashSet<uint>`, cleared not reallocated every `BeginFrame`, same
|
||||
grow-once-then-stable shape as every other per-frame dictionary/set in this
|
||||
class) — its `.Add(uint)` calls are non-boxing value-type operations.
|
||||
|
||||
## 9. S2 closeout: the reflood gap, the G2 self-gate finding, and the review fix round (2026-09-03)
|
||||
|
||||
**Reflood (lead, landing chunk 5).** `RefloodOwnerForLandblock` re-registered
|
||||
from `_entityShapes` only, so every owner reflooded by a landblock replacement
|
||||
commit (`PhysicsEngine.ApplyCommittedOwnerReplacement`) or by the Content
|
||||
builder's post-publication reflood lost its retail product — with chunk 5's
|
||||
consumer reading nothing else, it vanished. Fixed by forwarding the retained
|
||||
`_entityRetailPartArrays` entry exactly as `UpdatePosition` already did
|
||||
(retail: a reflood is `recalc_cross_cells` over the SAME `CPartArray`). Pins:
|
||||
`RefloodLandblock_KeepsTheRetailProductOfACollisionOwnerWithAPartArray`,
|
||||
`RefloodLandblock_KeepsARenderOnlyOwner` (both fail without the fix).
|
||||
|
||||
**G2 self-gate finding (pixel diff, not eyes).** Frame `02-facility-stairs.png`
|
||||
differed from the chunk-4 run in 23% of pixels; the chunk-4/baseline frames
|
||||
show the wall panels beside the corridor lit by its purple lights (left-wall
|
||||
mean RGB 65,17,21 baseline / 78,20,25 chunk 4) while the chunk-5 frame shows
|
||||
plain grey stone (29,26,23) with the drudge still pink-lit. Not lighting: the
|
||||
panels are DAT statics with no collision geometry, and `PublishStaticEntity`
|
||||
registered a static only under `entityBspCount > 0` / `setupShapes.Count > 0`
|
||||
— they had only ever been drawn through the parent-cell fallback chunk 5
|
||||
deleted. `UnregisteredRenderMembershipCount` stayed 0 because it counts the
|
||||
registry→scene direction only. Retail floods every object regardless of
|
||||
collision (`CEnvCell::init_static_objects` 0x0052c350 → `add_obj_to_cell`;
|
||||
`add_shadows_to_cells` 0x00514ae0 adds the shadow row unconditionally and
|
||||
gates only `AddPartsShadow` on `part_array != 0`). Fix: both publishers
|
||||
register a non-colliding static render-only (`RegisterMultiPart` with empty
|
||||
shapes + the part array → `RegisterRenderOnly`). Pins:
|
||||
`CompletePublication_NonCollidingStaticRegistersRenderOnly` (App),
|
||||
`PublishStaticCollision_NonCollidingGfxObjStatic_RegistersRenderOnly`
|
||||
(Content).
|
||||
|
||||
**Review fix round (arch lens + retail lens, both Opus, every finding
|
||||
lead-verified against the source before any edit).**
|
||||
|
||||
| # | Finding (verified) | Fix | Pin |
|
||||
|---|---|---|---|
|
||||
| A1/R2 | `Suspend` removed collision rows only; a suspended remote kept render membership at its last cells | `Suspend` clears the retail rows + republishes children; part array/route retained; the un-suspending `.None` commit republishes from the retained cells | `Suspend_ClearsTheRetailProduct_AndTheUnsuspendingMoveRepublishesIt` |
|
||||
| A2 | `AttachChild`/`DetachChild` never advanced `_mutationRevision`, so a prepared SetPosition's wholesale cell-list install could drop an attached child's rows | both advance the revision | `AttachChild_AndDetachChild_AdvanceTheMutationRevision` |
|
||||
| A3 | a re-registration of an attached child (appearance update) flooded it independently, overriding the inherited root array | `PublishRetailCellArray` short-circuits for an attached child: refresh `_childPartArrays`, `PublishChildEntries` | `RegisterMultiPart_OfAnAttachedChild_KeepsInheritingTheRootsCells` |
|
||||
| A4/R4 | `RemoveLandblock` and the non-rooted `RetireOwnerFromLandblock` branch left every retail row behind (unbounded growth, stale membership); render-only statics were unreachable by the static-retirement loop | `RemoveRetailProductForPrefix` (rows for the prefix's cells, per-owner cell pruning, static/render-only owners end with the landblock); the retire branch prunes retail cells first and handles render-only owners | `RemoveLandblock_ClearsTheRetailProduct_IncludingRenderOnlyStatics`, `RetireOwnerFromLandblock_PrunesTheRetailRowsOfANonRootedOwner` |
|
||||
| R3 | `RefreshPositionRows` (`.None`/keep-when-empty) republished from collision cells only → a render-only owner could never move | terminal branch republishes a render-only owner at its destination seed cell (AD-117 item 1) | `CommitSetPosition_None_MovesARenderOnlyOwnerToItsDestinationCell` |
|
||||
| A9 | `ReplaceMultiPartPayload` with a non-null EMPTY part array left a cell array with no rows and a permanent no-op on later moves | `partArray is { Count: > 0 }` | `ReplaceMultiPartPayload_WithAnEmptyPartArray_KeepsThePriorRows` |
|
||||
| A7/A8 | `RemoveAll` closure per cell per move; LINQ `Where().ToArray()` in the staged capture | `RemoveOwnerPartRows` + `CollectOwnerRows`/`CollectOwnerPartRows` | existing suites |
|
||||
| A5 | EnvCell shell projections carry their CELL id as `LocalEntityId`; dungeon cell ids alias the `0x8…` scenery namespace in `_byLocalEntityId` | shells are not indexed | `TryGetByLocalEntityId_IgnoresEnvCellShellRecords` |
|
||||
| A6 | `IndexMembershipEquals` omitted `LocalEntityId`, so an in-place id rebind left the index stale | compares the id | `Update_ThatRebindsTheLocalEntityId_MovesTheIndex` |
|
||||
| A10 | `_cellStatics`/`_cellDynamics` + `GetCellCount`/`CopyCellTo`/`GetCellStaticCount`/`CopyCell*To` had zero production callers | deleted | `ArchRenderSceneTests` updated |
|
||||
| A11/A12, R5–R7 | live list handed out; probe-flag coupling; unloaded-neighbour rows; circumsphere cheap reject; particle branch | documented (AD-117 items 2–4; S5 tail) | — |
|
||||
|
||||
Verified-correct list from both reviews (kept for S5): Contract A dispatch,
|
||||
`find_bbox_cell_list` shape, `AddPartsShadow` order/clip-plane rule,
|
||||
movement from the transition's array, keep-when-empty, child inheritance
|
||||
order, `Deregister` symmetry, cross-generation adoption seeding the part
|
||||
array, ordering determinism, zero-alloc `ResolveCellView`, layering.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue