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

15 KiB
Raw Blame History

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 (evidence appendices in docs/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.