acdream/docs/plans/2026-06-11-building-render-port-plan.md
Erik 5e2f99d08e docs: Phase A comparison + Phase B port plan (holistic building-render investigation)
Deliverable 1: docs/research/2026-06-11-building-render-acdream-vs-retail-
comparison.md - the acdream-vs-retail architecture comparison synthesized
from two ultracode mapping fan-outs (11/12 areas, ~90 agents, every retail
claim Ghidra/pc-cited, every acdream claim file:line, 40/76 divergences
adversarially verified so far; raw per-area evidence committed under
docs/research/2026-06-11-holistic-map/).

Headline findings: (1) retail flattens GfxObjs/cells at load exactly like
us (ConstructMesh + RemoveNonPortalNodes) - the MDI pipeline survives;
(2) the phantom/door mechanism is the skipNoTexture draw-time surface gate
(dat-confirmed); (3) retail never geometrically clips world geometry -
aperture exactness is a DEPTH discipline (punch maxZ1 / seal maxZ2 / gated
clear + far-to-near whole-mesh draws) - reframes #114; (4) flood admission
is already faithful, the trigger/depth/multi-view/cone-culling layers are
missing; (5) #115 root cause verified (boom damping severed from the
published collided viewer); collision A6.P4 design verified with
corrections (signed other_portal_id >= 0 gate).

Deliverable 2: docs/plans/2026-06-11-building-render-port-plan.md - the
phased port plan (BR-1 surface gate, BR-2 depth punch/seal, BR-3 delete
the shell chop, BR-4 draw-driven floods, BR-5 viewconeCheck, BR-6 one
gate, BR-7 collision A6.P4, BR-8 camera/lighting/LOD) with per-phase
acceptance criteria, bug closures, keep-list, and a playable-after-every-
phase migration order. AWAITING USER APPROVAL - no implementation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 05:54:12 +02:00

271 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# The holistic building-render port plan (Phase B) — one drawing discipline
**Status: AWAITING USER APPROVAL — no implementation until approved.**
Companion to the Phase A comparison:
[`docs/research/2026-06-11-building-render-acdream-vs-retail-comparison.md`](../research/2026-06-11-building-render-acdream-vs-retail-comparison.md)
(evidence appendices in
[`docs/research/2026-06-11-holistic-map/`](../research/2026-06-11-holistic-map/)).
Mandate: *"one solution that works every time I walk to a new landblock and
walk into a dungeon"* (2026-06-11).
---
## 0. The invariant (what "one drawing discipline" means, retail-cited)
Every phase below moves us toward — and no phase may move us away from — this
frame shape, which is retail's (Ghidra-cited in the comparison doc §2):
1. **Geometry is flattened at load** into surface-batched meshes (we already
do this). World geometry is **never geometrically clipped at draw time**.
2. **Untextured (solid) surface batches never draw** on building shells and
cell meshes (`skipNoTexture`); they do draw on plain objects.
3. **Portal polygons are not wall geometry.** They exist per frame only as
(a) flood admission tests (`ConstructView`: eye-side ε=0.0002 → clip vs
current view → cell loaded) and (b) **invisible depth writes** — far-Z
*punch* before an interior draws through an aperture; true-depth *seal*
on portals to the outside after the landscape draws.
4. **Cells draw whole, far→near, once** (frame stamp); the z-buffer plus the
punches/seals produce pixel-exact apertures.
5. **Objects and particles are culled per portal view** (sphere vs the view's
edge planes — `viewconeCheck`), never clipped, never scissored.
6. **One visibility computation feeds everything** — the PView flood. No
second BFS, no parallel gate, no distance constants in admission.
## 1. Keep-list (the code worth saving — explicitly not touched/rewritten)
- **Mesh pipeline**: `ObjectMeshManager` flatten + global VAO + bindless MDI
(`WbDrawDispatcher`) — retail-faithful architecture, confirmed by the
`ConstructMesh`/`RemoveNonPortalNodes` finding.
- **The flood port**: `PortalVisibilityBuilder` (homogeneous clipper, side
tests, reciprocal clip, exact-match skip) + conformance gates
(`CornerFloodReplayTests`, `Issue113MeetingHallFloodTests`) — BR-4 adjusts
constants/heuristics, it does not rewrite the clipper.
- **Membership** (P1 9/9 golden) + **straddle gate** (`414c3de`) +
**camera collision sweep** (verbatim `update_viewer`) + **znear=0.1** +
**#105 texture flush** + **two-tier streaming** + spawn/snap validation
(#107/#111/#112).
- Diagnostics/probes and the dat dump harness.
The M0 freeze list is superseded *for rendering only* by the 2026-06-11
mandate; nothing outside building/interior render + interior collision is in
scope.
## 2. Phases
Ordering rule: each phase lands green (build + full suites + named visual
gate) and the client stays playable after every phase. Conformance pins come
from the dat harness + the flood replay harnesses; retail constants are cited
inline when ported.
### BR-1 — The draw-time surface gate (kills the phantom class)
**What:** classify every mesh batch at decode by surface texturedness
(`Surface.Type & (Base1Image|Base1ClipMap)`); at draw, skip untextured
batches for **building-shell entities and cell meshes only** (plain objects
keep drawing them — retail's bypass). Align the cell-side build-time
`NoPos`/`NoNeg` drop with this rule: run a dat-wide sweep (all CellStructs +
all building models in the populated landblocks) proving
`portal-fill ⇔ untextured`; keep the cheaper build-time drop only where the
sweep proves equivalence, otherwise move to the draw-time gate. GfxObj-side:
fills stay in the mesh but never draw (matches retail exactly).
- **Closes:** #113 phantom staircase class (hall ramp, cottage "flying
stairs", every building's baked fills) — without touching doors (entities).
- **Acceptance:** `DumpPortalFillSurfaceTypes`-derived conformance sweep
green; hall + hill-cottage phantom gone and doors/windows intact at
Holtburg (user gate); all suites green.
- **Risk note:** apertures whose flood fails become true holes
(retail-identical); per-building floods + DrawPortal look-in already cover
the visible cases. If a hole shows at an unflooded aperture, that is BR-2/
BR-4 evidence, not a BR-1 regression.
- **Size:** ~2 commits (batch metadata plumb + draw gate; sweep test).
### BR-2 — Aperture depth machinery (punch / seal / clear)
**What:** port the invisible depth writes:
(a) wire `DrawExitPortalMasks` (today an unwired no-op) as a depth-only draw
of each outside-leading portal polygon, software-clipped to its view slice
(the `ClipToRegion` math already exists), at the portal's **true projected
depth** (retail `maxZ2`) — after the landscape slices, indoor roots;
(b) add the **far-Z punch** (retail `maxZ1`) on building-aperture flood
success on the outdoor + look-in paths, before the interior cells draw;
(c) replace the per-slice scissored `ClearDepthSlice` AABB clear with
retail's discipline: one full depth clear between the outside stage and the
interior stage, gated on whether any seal was drawn (`portalsDrawnCount`);
(d) on the look-in path, draw interior-through-aperture **before** the shell
mesh (retail `DrawBuilding` order) so the shell's depth closes everything
outside the punch.
- **Closes:** #108 (outdoor terrain sweeping across the upstairs door — the
missing true-depth seal is the confirmed `missing-portal-depth-fence`
divergence); the outdoor-root depth-discipline gap; part of #109.
- **Acceptance:** cellar↔main-floor walk shows no grass sweep (user gate);
new harness fact: seal depth = portal plane depth inside the clipped
aperture polygon (GL readback test or probe assertion); suites green.
- **Size:** ~3 commits (~80 lines of GL + clipper reuse per the area
estimate, plus the clear re-shape and order swap).
### BR-3 — Retire the geometric shell chop; whole-shell far→near draws
**What:** remove `gl_ClipDistance` as the *enforcement* mechanism for cell
shells (both the outdoor-scoped enable from `927fd8f`/`9ce335e` and the
never-enabled indoor half — i.e. #114 closes by *deleting* the chop, not
perfecting it). Shells draw whole, far→near per `OrderedVisibleCells`
(already the order), drawn-once. Clip regions remain for admission, punch
shapes, and (BR-5) object culling. The landscape-through-aperture pass keeps
its per-slice plane clip for now (open Q: `LScape::draw` internals) — revisit
after BR-2 proves the seal protects terrain.
- **Closes:** #114 (chopped stairs / vanished candle area / barrel-through-
wall were artifacts of clipping geometry retail never clips) — jointly
with BR-2. Removes the 8-plane budget + slot-0 PASS-ALL as load-bearing
for shells.
- **Acceptance:** meeting-hall interior + multi-room cottages render
unchopped from indoor and outdoor eyes (user gate vs the #114 screenshot
set); phantom stays gone (BR-1 unaffected); flood replay gates green.
- **Order constraint:** must not land before BR-2 (the depth fence replaces
the chop's job at apertures).
- **Size:** ~2 commits (mostly deletions + the draw-order assertion).
### BR-4 — Shell-draw-driven floods + flood fidelity
**What:** make the building's own draw the flood trigger, retail-shaped:
pair the shell GfxObj's `PortalRef.PortalIndex` with its `BuildInfo.Portals`
entry (the `outdoor_portal_list` correspondence) and, when a shell survives
the cull for a view slice, run each aperture through the ported
`ConstructView(CBldPortal)` chain under that slice. Then remove the
non-retail machinery the trigger replaces: the 48 m seed constant, the
Chebyshev≤1 candidate gather, the `EyeInsidePortalOpening` full-view rescue;
adopt retail constants (ε=0.0002; in-plane rejects for building portals);
add the 1-px screen-space vertex dedup to `ClipToRegion` output (retail's
fixpoint floor) and switch late view growth to in-place propagation
(`AddToCell`/`FixCellList`/`AdjustCellView` shape), removing the
`MaxReprocessPerCell=16` cap; make `MergeBuildingFrame` union views instead
of first-wins and retire single-slot consumers (`CellIdToSlot[0]`); bind
nested floods to their originating slot (the `building_view` latch).
- **Closes:** #109 (binary 48 m pop + first-wins view loss + missing punch
are its named mechanisms); the flood-stability family (edge-on doorway
residuals); enables interior-visible-through-window parity.
- **Acceptance:** flood replay harnesses extended: (a) building flood
triggers with no distance constant — admission matches the
clip-survival rule across an eye sweep; (b) two-aperture cell holds two
views; (c) growth propagates without the cap on a portal-dense fixture;
#109 spot user gate; suites green.
- **Size:** ~45 commits (trigger + pairing; constants/dedup; growth
in-place; merge union; deletions).
### BR-5 — Per-view object + particle culling (viewconeCheck)
**What:** port `Render::viewconeCheck`: per view slice, lift the per-edge
eye planes (each NDC edge + the eye defines a plane — the `view_vertex.plane`
analog) and sphere-test every entity and emitter against the slice before
draw; route particles through the same gate and the same clip/punch
discipline (delete the `BeginDoorwayScissor` AABB path); fix the
outdoor-root unattached-emitter drop; gate the weather pass on
`is_player_outside` (player cell, not viewer root).
- **Closes:** particles-through-walls (candle flames in other buildings);
rain-indoors-through-doorways; the neighbour-room object over-inclusion
half of the old #114 report.
- **Acceptance:** flame-through-wall spot at Holtburg (user gate); a
conformance fact pinning sphere-vs-slice culling on a fixture; no
regression in entity draw counts outdoors (perf probe within noise).
- **Size:** ~3 commits.
### BR-6 — One gate: consolidate visibility + delete legacy paths
**What:** make the PView flood the only visibility computation:
remove the per-frame ACME BFS (`CellVisibility.ComputeVisibilityFromRoot`)
by folding its remaining consumers (lighting indoor flag etc.) onto PView/
membership outputs; delete or quarantine the confirmed legacy remnants
(`InteriorRenderer`, `IndoorDrawPlan` consumers of the old path, the
`clipRoot==null` second render branch, the dormant exit-mask wiring once
BR-2 rewires it, duplicate frustum implementation); one frustum, one
center/radius window.
- **Closes:** the `dual-live-visibility-computations` inconsistency class
(the one-gate rule, `feedback_render_one_gate`); removes the surface area
where two gates disagree (future flap-class bugs).
- **Acceptance:** gate-audit re-run shows ONE visibility computation per
frame; every deletion verified by a launch + the visual gate set; suites
green.
- **Size:** ~3 commits, mostly deletions (each independently revertable).
### BR-7 — Interior collision: per-cell shadow lists (A6.P4, verified)
**What:** ship the A6.P4 architecture with the investigation's corrections:
registration builds the cell set by sphere-overlap portal flood (not an XY
grid; crosses landblocks), per-cell `shadow_object_list` iteration on the
query side (`CheckOtherCells` runs env AND shadow objects per other cell),
buildings dispatch through a per-LandCell building channel
(`CSortCell.building` shape), `OtherPortalId` widened to signed with the
`>= 0` gate (sign-extension Ghidra-proven). Then remove the `b3ce505`
stopgap, the A6.P5 `hasExitPortal` widening, and the #90 stickiness
workaround.
- **Closes:** #99 (doors block from both sides), very likely #97; retires
three flagged workarounds.
- **Acceptance:** A6.P4 spec acceptance (doors block both ways at Holtburg
inn + cottages; #98 cellar ascent stays fixed — `CellarUp` harness green);
capture/replay comparison on the door apparatus; suites green.
- **Size:** the A6.P4 spec's estimate stands (~5 commits); independent of
BR-2..BR-5 — may run in parallel with them.
### BR-8 — Feel tier: camera, lighting, LOD (post-discipline polish)
- **BR-8a Camera (#115, verified root cause; can land any time):** damp the
sought eye FROM the published collided viewer each frame (retail
`PlayerPhysicsUpdatedCallback` shape) and apply the computed player fade
over the 0.45→0.20 m band. Acceptance: cramped-interior turn feel (user
gate). ~12 commits.
- **BR-8b Lighting (pending verifier confirmation):** interior sun mask
(never sun-light interiors), static cell-light burn-in (all lights, not
8-nearest), viewer light, per-object light selection, surface
luminosity/diffuse. Acceptance: side-by-side interior look vs retail
screenshots. Phase-sized; spec before code.
- **BR-8c LOD + dedup (low):** per-part degrade selection beyond humanoids;
frame-stamp draw dedup. Optional per-cell interleave for draw-order parity
is explicitly NOT planned (z-buffer makes it unnecessary; revisit only on
evidence).
- **Picking refinements** (all-low area): defer; file as issues when the
port changes what is clickable.
## 3. What this plan deliberately does NOT do
- No per-frame BSP traversal of ordinary geometry (retail doesn't either).
- No rewrite of the mesh/MDI pipeline, the flood clipper, membership, or
streaming (keep-list).
- No `leaf_cells`/`CPartCell` port (path dormant in the 2013 binary — needs
runtime proof first).
- No transparency-sorting work yet — that area's map is still re-running;
fold its findings in as a BR-9 candidate after review (the AlphaList
deferral machinery is already decompiled in the Area 1 file).
## 4. Sequencing summary
```
BR-1 (surface gate) — first; standalone visual win, lowest risk
BR-2 (depth punch/seal) — second; enables BR-3
BR-3 (delete shell chop) — closes #114 with BR-2
BR-4 (draw-driven floods) — closes #109; flood fidelity
BR-5 (viewconeCheck) — particles/objects through the same gate
BR-6 (one gate + deletions) — consolidation after the discipline is in
BR-7 (collision A6.P4) — independent track; may interleave with BR-2..5
BR-8 (camera/lighting/LOD) — feel tier; BR-8a may land early
```
Every phase: `dotnet build` + full suites green, conformance pins added with
retail citations, named user visual gate, roadmap/ISSUES updated in the same
session, and the render digest updated when a phase closes one of the named
bugs.
## 5. Approval asks
1. Approve the plan shape + ordering (BR-1 → BR-8, BR-7 parallel-capable).
2. Approve the deletions implied by BR-3/BR-6 (shell-chop enforcement,
ACME BFS visibility, legacy render branches) — all on the strength of the
cited evidence that retail has no counterpart.
3. Note the verification caveat: ~36/76 divergences still carry UNVERIFIED
(resume in flight); BR-1..BR-3's load-bearing claims are either verified
or dat-confirmed locally, so approval need not wait on the rest.