acdream/docs/plans/2026-06-11-building-render-port-plan.md
Erik af5d424df0 docs: T5 comprehensive gate verdict - PARTIAL PASS; #108/#109/#97 closed, #117-#120 filed
The single comprehensive visual gate (2026-06-11, user-driven):

CONFIRMED (user axioms): doors block both ways incl. off-center (#99
visual pass), cellar descent/ascent clean + #108 grass-sweep GONE, inn
2nd floor clean (#97 CLOSED), interiors stable through doorways incl.
edge-on, #109 far-door oscillation GONE, formerly-popping stairs now
stable at all ranges. The entire T6/BR-7 collision port passed 100%.

REMAINING (render, filed at mechanism level - no live whack-a-mole):
- #117 aperture-shaped see-through: doors/interiors visible through
  terrain hills and through nearer buildings (the far-Z punch erases
  occluder depth at aperture pixels). Decomp direction:
  DrawPortalPolyInternal depth state vs draw order.
- #118 character clipped + vanishes momentarily on house exit
  (viewer-indoor/player-outdoor transition frames; local-player
  partition / aperture-clip suspects).
- #119 old-tower stairs partially invisible + extraneous water barrel
  (pre-existing); T5-log lead: [up-null] 0x010002B4 + 0x010008A8
  cached EMPTY render data, permanently invisible.
- #120 [pv-ERROR] in-place propagation tripwire at depth 128 on the
  cottage interior cells (T2 convergence invariant break, self-detected
  during the gate) - investigate FIRST.

Rain-indoors not verifiable (clear weather).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 15:46:30 +02:00

22 KiB
Raw Permalink Blame History

The holistic building-render port plan (Phase B) — one drawing discipline

**EXECUTION STATUS (2026-06-11, post-BR-7): BR-2…BR-7 are ALL CODE-COMPLETE on the branch — the render arc as the fused tasks T1T4 (T1 579c8b0 frame order; T2 cf8a2c3/529dfcf/88f3ce1 flood fidelity, two retail constants refuted by the conformance gate and kept at documented tolerances; T3 a6aec8c viewconeCheck; T4 4a307d3 one-gate deletions), and BR-7 (T6, collision A6.P4) as 6ec4cde (signed OtherPortalId gate) + abf36e2 (BuildShadowCellSet flood) + dbfbf85 (per-cell architecture: flood registration, building channel, per-cell query, b3ce505 DELETED — closes #99) + ca4b482 (straddle-only outside-add, A6.P5 widening + #90 stickiness removed). Of the 4 #99-era Core reds, 3 flipped green as designed (door apparatus + tick-13558 + tick-22760's blocking invariant); the 4th (BSPStepUp D4) + 22760's lateral-slide delta proved to be a SEPARATE pre-existing slide-response family — filed #116, D4 skipped with the reference (probes show the cell-set layer innocent). Suites: Core 1416/0/2skip, App 225, UI 420, Net 294.

T5 EXECUTED 2026-06-11 (the single comprehensive user gate) — PARTIAL PASS. Confirmed by the user: doors block both ways incl. off-center (#99 visual), cellar descent/ascent clean + #108 grass-sweep GONE, inn 2nd floor clean (#97 closed), interiors stable through doorways incl. edge-on, #109 far-door oscillation GONE, formerly-popping stairs now STABLE at all ranges (the distance-pop class is dead). Remaining — four filed render artifacts: #117 aperture-shaped see-through (doors/interiors through terrain hills + through nearer buildings — the punch erases occluder depth), #118 character clipped+vanishes for a moment on house exit, #119 old-tower stairs partially invisible + extraneous barrel (pre-existing; [up-null] permanently-invisible mesh lead in the T5 log), #120 [pv-ERROR] in-place-propagation convergence tripwire at depth 128 on the cottage cells (self-detected T2 invariant break — investigate first). Rain-indoors not verifiable (clear weather). NEXT: fix #120 → #117 → #118 → #119 at the mechanism level, then a focused re-gate on just those spots.**

Status: APPROVED + AMENDED (2026-06-11). EXECUTION DIRECTIVE CHANGED BY THE USER: "I don't care if it is non-playable… I want everything ported, then we test." The per-phase playability constraint and per-phase user visual gates are DROPPED. BR-2 through BR-6 execute as ONE continuous port (the fused render discipline), with build + unit/conformance tests green at every commit (engineering hygiene, not gates), and ONE comprehensive visual test pass at the end. Rationale: the first BR-2 attempt failed precisely because the phase slicing cut retail's frame order in half (the punch shipped without entities-drawn-last and erased characters in apertures — reverted 88be519); the installment-must-be-a-complete-retail-behavior rule replaces the playability rule. BR-7 (collision) runs as an independent track; BR-8b (lighting) still wants the verification resume first. 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 surface gate — RESOLVED AS ALREADY-EQUIVALENT (2026-06-11, execution day 1)

Premise falsified before implementation (the BR-1 pre-check, ReplicateProductionEmission_OnPortalFills): acdream already suppresses every portal fill — all four extraction paths skip Stippling.NoPos positive sides (ObjectMeshManager.PrepareGfxObjMeshData:1046, PrepareCellStructMeshData:1394, CellMesh.Build:44, GfxObjMesh.Build:71), and the Holtburg fills have no negative surface. The planned "draw-time surface gate" has nothing to gate.

What shipped instead — the equivalence pin (StipplingSurfaceEquivalenceTests): 2,607 polys across 13 building models + 13 environments, zero violations both directionsNoPos ⇔ untextured surface. Our build-time skip is therefore proven equivalent to retail's draw-time skipNoTexture rule on this content; the portal-poly-suppression-criterion divergence closes as equivalent-with-proof. The pin fails loudly if future content breaks the invariant (the cue to implement the draw-time gate then).

Consequences (the honest part):

  • The #113 phantom residual is NOT GfxObj fills — it cannot be, they never reach a vertex buffer. The "root cause #2" attribution from the e46d3d9 session is corrected; the e46d3d9 user-gate observations (filter removed phantom/doors) were confounded — the filter was a provable mesh no-op on both shells and door parts.
  • The phantom's plausible true sites are cell-side: flood-admitted stair CELLS drawn with a pass-all slice when the assembler hands them no slot (RetailPViewRenderer.cs:71 draws ALL visible cells; NoClipSlice default), and/or stair-cell STATICS drawn unclipped + un-viewcone'd by design (object-lists-skip-portal-view-gate, confirmed). BR-2's first task is a 10-minute probe at the hall bisect spot pinning which — the closure moves to BR-2/BR-3 (shells) and BR-5 (statics).
  • Closes: the portal-poly-suppression-criterion divergence (as proven-equivalent); #113's closure moves to BR-2/BR-3/BR-5.
  • Shipped: the pre-check + equivalence pin tests; no production code (none needed).

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.

  • First task (from BR-1's falsification): the 10-minute probe at the hall bisect spot — when the phantom is visible, log per stair cell (0x100..0x106) whether it drew with a real clip slot or the pass-all NoClipSlice, and whether its statics drew — pinning the phantom's true draw site (shells → fixed here/BR-3; statics → BR-5).
  • 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; the #113 phantom residual if the probe pins it on pass-all shell slices.
  • Acceptance: cellar↔main-floor walk shows no grass sweep (user gate); phantom-spot check at the hall (user gate, replaces the old BR-1 acceptance); 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) — CODE-COMPLETE 2026-06-11 (6ec4cde+abf36e2+dbfbf85+ca4b482; visual confirmation rides T5)

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. Explicitly out of scope — tracked follow-ups (NOT covered by BR-1…BR-8)

Completing BR-1 through BR-8 lands the building/interior drawing discipline and the collision rearchitecture. It does not cover the items below. They are named here so the boundary of what the campaign delivers is written down, not assumed — each becomes its own roadmap item or issue, none blocks BR-1…BR-8.

  • FU-1 — Transparency / draw-sorting (→ BR-9 candidate). Retail's DrawSortCell + AlphaList deferral (decompiled in 2026-06-11-holistic-map/wf1-gfxobj-draw.md) governs water surfaces, translucent windows, and alpha-blend ordering. The area's map never completed (agent hit the token limit), so there are no divergences yet — scope it before promoting to BR-9. Severity: medium; user-visible as wrong window/water compositing.
  • FU-2 — Dungeon visibility scaling (#95). The 8 phases are Holtburg-building-shaped. Dungeons share the EnvCell/portal discipline so they benefit automatically, and BR-4's tighter flood admission (no-distance-constant + screen-clip rejection + cell-loaded gate) plausibly shrinks #95's 135-cells/frame blowup — but #95 is a disconnected-landblock seeding problem that BR-4 is not guaranteed to fix. Re-measure #95 after BR-4/BR-6 land; if still blown, it needs its own phase. Do not assume the building port closes it.
  • FU-3 — Distance LOD / degrades (= BR-8c, optional). Per-part degrade selection beyond humanoids; far models stay base-detail until picked up.
  • FU-4 — Picking refinements (4 low-severity divergences, wf2-picking-selection.md). Defer; file as issues if/when the port changes what is clickable (e.g. building shells, baked fills).
  • FU-5 — The ~30 open questions live in the comparison doc §6 (2026-06-11-building-render-acdream-vs-retail-comparison.md). The load-bearing ones are referenced inline in the phases that consume them (e.g. LScape::draw clip behavior for BR-2/BR-3, the near-W constant, DrawPortal mode-3 seal-on-failure for unstreamed interiors); the rest are pinned during implementation, not before.
  • FU-6 — Verification top-up. ~36/76 divergences remain UNVERIFIED (the overnight resume was stopped to preserve budget; both runs are resumable by ID — see comparison §7). Run a cheap resume before BR-8b lighting scoping (the one phase that leans on unverified rows) and before promoting FU-1 to BR-9.

5. Sequencing summary

BR-1 (surface gate)          — ✅ RESOLVED as already-equivalent (pin shipped,
                               no production code; #113 closure moved to
                               BR-2/3/5 — see BR-1 section)
BR-2 (depth punch/seal)      — FIRST implementation phase; opens with the
                               phantom-site probe; 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;
                               closes the phantom if it is statics-side
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.

6. 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.