feat(render): R-A2 — per-building floods (the flap fix)

Replace the outdoor root's single unified reverse-portal flood (whose root-level
portal-side test oscillated as the chase eye grazed a doorway — the measured
flood 2<->6) with retail's per-building floods.

- OutdoorCellNode.Build(uint): portal-less land root; floods only itself ->
  full-screen OutsideView -> terrain (PortalVisibilityBuilder IsOutdoorNode seed).
- PortalVisibilityBuilder.ConstructViewBuilding: per-building flood seeded at a
  building's own finite entrance (retail ConstructView(CBldPortal) 0x5a59a0 via
  DrawPortal 0x5a5ab0 / portal_draw_portals_only 0x53d870). Entrance-bounded ->
  consistent ~2-cell depth (measured retail cell_draw_num, handoff OPTION-A 3.4).
- RetailPViewRenderer.DrawInside: when the root is the outdoor node, group nearby
  cells by BuildingId and merge each per-building flood into the frame before
  assembly; existing shells/object-list draw path unchanged. 48 m seed cutoff.
- GameWindow: pass flat NearbyBuildingCells only on outdoor-node frames.

Tests: +3 PortalVisibilityRobustnessTests (per-building touches ~2 cells, membership
stable under the measured 36 um eye jitter). UnifiedFloodTests retired (its subject,
the unified flood from the outdoor node, is removed); surviving full-screen-OutsideView
coverage moved to OutdoorCellNodeTests. App Rendering 207/207, Core movement 14/14.

Conformance-verified sound; the grazing-doorway flap is the visual acceptance test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-08 18:44:43 +02:00
parent 7fe98098f5
commit c62663d7cb
8 changed files with 251 additions and 198 deletions

View file

@ -128,6 +128,19 @@ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
## Task R-A2: Per-building floods — the flap fix (remove D3, finish D2)
**AS-BUILT (2026-06-08, conformance-green, pending visual gate):** `OutdoorCellNode.Build(uint)` is now
portal-less (reverse portals removed → the land root floods only itself → full-screen OutsideView for
terrain). `PortalVisibilityBuilder.ConstructViewBuilding` is the per-building contract (thin wrapper over
`BuildFromExterior`). `RetailPViewRenderer.DrawInside` groups the nearby building cells by `BuildingId`
(owned by the render layer — a reused dict, keeps GameWindow thin) and merges each small per-building
flood into the frame before assembly (`MergeNearbyBuildingFloods` / `MergeBuildingFrame`; 48 m seed
cutoff); the existing draw path (assemble → shells → object lists) is unchanged. `GameWindow` passes the
flat `NearbyBuildingCells` only on outdoor-node frames. `UnifiedFloodTests` retired (its subject — the
unified flood from the outdoor node — is removed); its surviving full-screen-OutsideView coverage moved
to `OutdoorCellNodeTests`. Conformance + render suites green (App Rendering 207, Core movement 14,
incl. +3 `PortalVisibilityRobustnessTests`). The detailed steps below are the original design rationale;
this note is the as-built. **Visual gate (grazing doorway) is the acceptance test for "flap gone."**
**Intent:** Replace the single unified flood from the outdoor land root (which reaches buildings through reverse portals gated by a root-level portal-side knife-edge → the oscillation) with retail's **per-building** floods: for each building near the camera, run a small `ConstructView` seeded at that building's entrance portal, touching ≈2 cells. The land-cell root then floods **nothing** into buildings — it is a pure terrain root (full-screen `OutsideView`). This makes building membership robust to the eye's ~36 µm jitter → the flap dies.
**Retail oracle:** `BSPPORTAL::portal_draw_portals_only` (`0x53d870`, decomp:326881) → `DrawPortal` (`0x5a5ab0`, decomp:433895) → `ConstructView(CBldPortal*, …)` (`0x5a59a0`, decomp:433827): viewpoint side-test vs the building portal plane (0.0002 epsilon), `GetClip`, `CEnvCell::GetVisible(other_cell_id)`, `copy_view`, recurse into the building's cells. acdream's `BuildFromExterior` (`PortalVisibilityBuilder.cs:373`) already implements this shape (seed from an exit portal, flood inward); R-A2 calls it **per building** instead of once over all candidates, and removes the root-level building reverse-portals.