acdream/docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md
Erik 7aca79f8eb docs(render): Phase R0 — lock the render-redesign design spec (brainstorm outcome)
Resolves the plan §3 open questions with the user this session:
- object/entity/particle draw = LITERAL PER-CELL LOOP (retail DrawCells),
  not a global MDI batch with per-instance clip. Fidelity > perf > blast-radius.
- sequencing = HOLISTIC: build the per-cell DrawInside directly; no intermediate
  global-pass gate-fix. First visual gate = sealed cottage interior, no bleed.
- terrain in the seal = FAITHFUL: drawn only through the exit-portal clip, never
  as a floor under the interior. Inventory's 'relax Skip' suggestion REJECTED as a
  non-retail workaround; grey-floor = a sealing bug (verify cell mesh in R1).
- WB mesh pipeline KEPT (per-cell draws from the global buffers, batched within a
  cell); two-camera invariant preserved (eye projects, player cell roots visibility).

Phases (holistic): R1 unified per-cell DrawInside (the core) -> R2 outside-looking-in
(DrawPortal) -> R3 dungeons -> R4 polish+cleanup. Each ends GREEN + a user visual gate.
Retail anchors cited throughout (RenderNormalMode 0x453aa0, DrawCells 0x5a4840, etc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 19:18:59 +02:00

329 lines
24 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.

# Render Pipeline Redesign — Locked Design Spec (R0 outcome, 2026-06-02)
> **This is the design locked in the R0 brainstorm.** It supersedes the open questions in
> [`docs/superpowers/plans/2026-06-02-render-pipeline-redesign-plan.md`](2026-06-02-render-pipeline-redesign-plan.md) §3
> with concrete decisions, and is the input `writing-plans` turns into the implementation plan.
>
> **Read first (context, in order):**
> 1. [`docs/research/2026-06-02-render-pipeline-redesign-handoff.md`](../../research/2026-06-02-render-pipeline-redesign-handoff.md) — proven root cause (§2), the three-gate failure (§3), the retail target (§5).
> 2. [`docs/research/2026-06-02-retail-render-pipeline-full-reference.md`](../../research/2026-06-02-retail-render-pipeline-full-reference.md) — the retail PView pipeline + the `DrawCells` seal mechanics (the algorithm being ported).
> 3. [`docs/research/2026-06-02-acdream-render-pipeline-inventory-and-failures.md`](../../research/2026-06-02-acdream-render-pipeline-inventory-and-failures.md) — the concrete bugs (the `WbDrawDispatcher.cs:1756` bypass; the parallel BFS; the terrain Skip model).
> 4. [`docs/research/2026-06-02-render-reference-crosscheck.md`](../../research/2026-06-02-render-reference-crosscheck.md) — why WB's two-pipe stencil is the wrong model (do NOT reintroduce).
---
## 0. Mandate (user, 2026-06-02 — non-negotiable)
FULLY WORKING outdoor + indoor + dungeon rendering. No flaps, no missing textures, no transparent
walls, no terrain leaking into cellars, no entity/particle bleed-through, outside-looking-in works.
**No shortcuts, no bandaids, no quick fixes** — take the architecturally-correct path even if slower;
redesign the whole pipeline if needed; **port from retail** (the oracle is `docs/research/named-retail/`);
do more research mid-session rather than guess.
---
## 1. Decisions locked in the R0 brainstorm
These are the resolutions to the plan §3 open questions, decided with the user this session. They are
binding for the implementation plan.
1. **Object / entity / particle draw = LITERAL PER-CELL LOOP.** A faithful port of retail
`PView::DrawCells`' per-cell loops (iterate `cell_draw_list` closest-first; draw each visible cell's
shell + objects + particles, clipped to that cell's portal-derived region). **NOT** the alternative
of keeping one global `glMultiDrawElementsIndirect` pass with per-instance clip slots. The user's
stated priority order: **retail-structural fidelity > perf > minimal blast-radius.**
2. **Sequencing = HOLISTIC.** Build the per-cell `DrawInside` **directly**. Do **not** ship an
intermediate "fix the gate on the existing global pass" step. The first phase is the full per-cell
`DrawInside`; the first visual gate is the **sealed cottage interior with no bleed**. (Verification
with the probes + the user's eyes still happens at natural checkpoints *within* the per-cell build —
that is evidence discipline on the real architecture, not the rejected throwaway intermediate.)
3. **Terrain in the seal = FAITHFUL.** Terrain (and outdoor scenery, and sky/weather) is drawn **only
through the exit-portal clip region**, never as a floor under the interior. The inventory doc's
"REDESIGN item #4" suggestion — *relax `TerrainClipMode.Skip` for `seen_outside=true` cottages so the
cellar floor stops going grey* — is **REJECTED as a non-retail workaround.** The "grey floor / grey
world in cellar" symptom is a **sealing bug** (the closed cell mesh is not covering those pixels: a
missing or back-facing floor polygon, or the GL clear color showing through a gap). The faithful fix
is to make the cell mesh seal; verified by the `[shell]` probe + a dat dump of the cellar EnvCell mesh.
4. **WB mesh pipeline KEPT; orchestration restructured.** The global VAO/VBO/IBO, GfxObj decode, and
texture cache stay. What changes is that draws are issued **per visible cell** from those global
buffers (batched *within* a cell where the geometry allows), not one global batch across all entities.
"Keep the WB mesh pipeline, restructure the orchestration" still holds.
5. **Two-camera invariant PRESERVED.** The 3rd-person eye drives the **projection** (`envCellViewProj`);
the **player's physics cell** (`CellGraph.CurrCell`) roots **visibility** and the portal-side test.
This is the U.4c flap fix and it is kept verbatim.
---
## 2. The one model (the inversion)
There is **one** top-level decision per frame — a faithful port of retail
`SmartBox::RenderNormalMode @ 0x453aa0` (pc:92635):
```
RenderWorld(viewer):
if clipRoot == null: # viewer in an outdoor LandCell (retail is_player_outside: id&0xFFFF < 0x100)
DrawOutside() # today's outdoor path: terrain + scenery entities + sky/weather.
# building INTERIORS reached via DrawPortal (R2 — outside-looking-in).
else: # viewer in an EnvCell (clipRoot != null)
DrawInside(clipRoot) # ONE PView flood — nothing else. The outdoor world is NOT drawn;
# it enters ONLY through clipped exit portals, inside DrawInside.
```
`DrawInside` is a faithful port of `PView::DrawInside @ 0x5a5860``ConstructView @ 0x5a57b0`
`DrawCells @ 0x5a4840`:
```
DrawInside(cell):
frame = PortalVisibilityBuilder.Build(cell, playerPos, eyeViewProj) # KEEP — the PVS is correct
if frame.OutsideView non-empty: # an exit portal is in view
DrawLandscapeClippedTo(frame.OutsideView) # terrain + outdoor scenery entities + sky, thru the doorway
ConditionalDepthOnlyClear(frame.OutsideView)# Z only — never color → no blue hole
for cellId in frame.OrderedVisibleCells: # the per-cell loop (retail DrawCells loops 1-3)
clip = frame.CellViews[cellId]
DrawCellShell(cellId, clip) # EnvCellRenderer.Render(pass, {cellId}) — closed mesh
DrawCellObjects(cellId, clip) # ONLY this cell's entities, clipped to its region
DrawCellParticles(cellId, clip) # ONLY this cell's particles — solves #104 for free
```
**Visibility *is* the cull.** No global entity pass; no second visibility computation. The bleed cannot
happen by construction — the outdoor world is never iterated when inside (except the clipped doorway).
This is the inversion of the current bug: acdream draws the outdoor world and *then* gates a few cell
shells on top through three inconsistent gates (handoff §3); retail never visits the outdoor world when
inside.
---
## 3. Keep / Build / Remove
### KEEP (proven correct — handoff §4, inventory §5)
- `PortalVisibilityBuilder` — the PView BFS. Produces `OutsideView`, `CellViews` (per-cell NDC clip),
`OrderedVisibleCells` (closest-first), `CrossBuildingViews`. The single visibility authority.
- `ClipFrame` / `ClipFrameAssembler` / `ClipPlaneSet` / `PortalView` — the clip-plane machinery and the
per-cell NDC→GPU slot packing. (`ClipFrameAssembly` already exposes `CellIdToSlot`, `OutdoorSlot`,
`OutdoorVisible`, `TerrainMode`, `HasOutsideView`, `OutsideViewNdcAabb` — everything the seal needs.)
- `EnvCellRenderer` mesh/MDI/texture path, including `Render(pass, filter)` (a single-cell filter set
drives one cell — the hook the per-cell loop uses).
- `TerrainModernRenderer` (the renderer; only its caller's clip mode changes).
- The WB mesh pipeline (global VAO/VBO, GfxObj decode, `TextureCache`).
- The membership fix `59f3a13` (`[cell-transit]` confirms correct cell tracking — do NOT reopen).
- The Stage-4 sky NDC-clip + the conditional doorway Z-clear (`ce2edad`/`b595cfb`).
- All diagnostic probes (`ACDREAM_PROBE_CELL` / `_VIS` / `_SHELL` / `_FLAP`).
### BUILD (the redesign)
- A new **`DrawInside` orchestrator** (the §2 per-cell loop) that replaces the current
"global terrain → global shells → global entity pass" structure in `GameWindow.OnRender`
(~72507610).
- The **binary top-level decision** (`clipRoot == null``DrawOutside`; else → `DrawInside` only).
- A **per-cell entity dispatch** issued from the global buffers, batched within a cell.
- A **per-cell particle draw** (each cell's particles in its clip scope).
- The **landscape-through-the-door** composition (terrain + outdoor scenery + sky clipped to `OutsideView`)
+ the conditional Z-only clear.
- (R2) the **outside-looking-in `DrawPortal`** path (separate outdoor pview).
### REMOVE (dead / divergent — scheduled in the final polish phase so it can't destabilize earlier work)
- The dormant WB-two-pipe scaffolding: `Building`, `BuildingLoader`, `ExitPortalPolygons` stencil-marking,
the occlusion-query state (`QueryId`/`WasVisible`), and the `IsShellScopedSet` / `BuildingShellAnchorCellId`
anchor machinery (`IsShellScopedSet` already returns `false` — U.1 deleted the live two-pipe).
- `CellVisibility` as a **rendering gate** — its `VisibleCellIds` set retires as the entity gate. Its
`CameraCell` / root-selection role (the indoor/outdoor root decision) **stays**.
- The `CullMode.Landblock → None` double-sided stopgap (`EnvCellRenderer.cs:~1216`) — replace with the
correct per-polygon winding once the seal is verified.
---
## 4. The seal mechanics (`PView::DrawCells @ 0x5a4840` port)
Two parts: the landscape-through-the-door block, then the per-cell loop. (Full retail verbatim:
research doc A §4.)
### 4.1 Landscape through the door (only when `OutsideView` is non-empty)
- Draw the "landscape" clipped to the doorway's screen silhouette (`OutsideView`). Retail does this with
one `LScape::draw` (`PortalList = this`). In acdream the landscape is split across three renderers, so
this one step becomes:
- **terrain** (`TerrainModernRenderer`, gated by the clip-plane UBO in Planes mode, or `glScissor` to
the `OutsideView` AABB in Scissor mode),
- **outdoor scenery entities** (`ParentCellId == null`, clipped to `OutsideView` / `OutdoorSlot`),
- **sky / weather** (`SkyRenderer`, `gl_ClipDistance` against the same UBO — the Stage-4 path, kept).
- Then a **conditional depth-only clear** scissored to the `OutsideView` AABB
(`Clear(4, …)` — flag 4 = depth buffer only, retail pc:432731). Color is preserved (terrain painted
through the door stays); depth resets so interior geometry composites without z-fighting at the doorway
edge. **There is NEVER a color clear in the indoor path** — that is structurally why there is no blue hole.
### 4.2 The per-cell loop over `OrderedVisibleCells` (closest-first; each cell clipped to `CellViews[cellId]`)
- **Shell** (retail DrawCells Loop 2 — `DrawEnvCell`): `EnvCellRenderer.Render(pass, {cellId})` draws the
cell's **closed** mesh (floor + walls + ceiling) from the cell's dat geometry. The ceiling and floor seal
*because they are authored in the dat mesh* — there is **no "cap the ceiling" step**. Doorways are genuine
holes in the mesh, so the landscape from §4.1 shows through them. (Retail Loop 1 stencils the exit-portal
openings; acdream's mesh-hole + the §4.1 Z-clear achieve the same composition — **R1 verifies** whether an
explicit portal stencil is needed or the mesh-hole + Z-clear suffice.)
- **Objects** (Loop 3 — `DrawObjCellForDummies`): only this cell's entities (`object_list` equivalent),
clipped to the cell's region. Live-dynamic entities (player/NPCs/items, `serverGuid != 0`) render
unclipped (depth only) per retail.
- **Particles:** only this cell's particles, in the cell's clip scope (closes #104 — no per-instance slot
needed because the draw is already per-cell).
- **Self-contained GL state:** each per-cell draw SETS every GL state it depends on (view-proj, blend,
depth-mask, cull, front-face, A2C) — never inherited (memory `render-self-contained-gl-state`; this bit
`EnvCellRenderer` three times in U.4).
### 4.3 Why the seal holds (the four guarantees — retail doc A §4.5)
1. **No blue hole:** outdoors is drawn first (clipped to `OutsideView`); the only clear is Z-only +
conditional; color survives in the doorway.
2. **Sealed ceiling/walls/floor:** each visible cell's mesh is a closed box; portal holes are the only
openings.
3. **No outdoor bleed-in:** the landscape paints only through exit-portal clip regions; if `OutsideView`
is empty (dungeon, or facing away from the door), no terrain/sky is drawn at all.
4. **No object/particle bleed:** objects/particles are drawn per-cell, only for cells in
`OrderedVisibleCells`, clipped to the cell's region.
---
## 5. The per-cell loop & batching (how the mesh pipeline is preserved)
The user chose the literal per-cell loop over global MDI; this section pins down how that coexists with the
KEPT mesh pipeline so the plan does not regress perf catastrophically or break the working mesh path.
- **Geometry storage is unchanged.** All cell shells and entity meshes live in the global VAO/VBO/IBO; each
batch references its slice via `BaseVertex` / `FirstIndex`. The per-cell loop issues draws against those
slices — it does not re-upload or re-pack geometry per cell.
- **Batched within a cell.** Each cell's draw still groups by mesh/material and issues an MDI call for that
cell's object set (per-cell MDI), rather than a draw call per object. So we lose *cross-cell* batching
(the retail-faithful cost the user accepted) but keep *within-cell* batching. A cottage interior is a
handful of visible cells, so the per-cell call count is small.
- **Clip is the cell's region, applied per-cell.** Because the loop is per-cell, the cell's clip region is
bound once before that cell's draws (clip-plane UBO / `gl_ClipDistance`), not per-instance. This is what
lets particles (no instanceID) be clipped — the #104 win.
- **`EnvCellRenderer`** is driven with a single-cell filter (`Render(pass, {cellId})`) per loop iteration,
or a small per-cell render entry is added if the snapshot model needs it (decided in the plan; the
existing filter path is the starting point).
- **Entity dispatch** becomes per-cell: instead of `WbDrawDispatcher.Draw(... visibleCellIds ...)` walking
all entities once, the orchestrator asks for each visible cell's entities (entities whose `ParentCellId`
== cellId) and issues that cell's batched draw. The outdoor-scenery entities (`ParentCellId == null`) are
drawn in the §4.1 landscape-through-door step, clipped to `OutsideView`, NOT in any interior cell's loop.
**Perf note (risk, not a blocker):** per-cell dispatch raises draw-call count vs the single global MDI. The
N.6 baseline showed CPU dominates GPU by 3050× and the GPU sits at ~3.6% of frame budget, so there is
headroom; a cottage is a few cells. If a complex interior or dungeon regresses, within-cell batching +
the closest-first order (early-Z) are the mitigations. Measure at the R1 gate; do not pre-optimize.
---
## 6. Component-level design
| Concern | Component | Change |
|---|---|---|
| Top-level decision | `GameWindow.OnRender` | New binary branch: `clipRoot == null``DrawOutside` (existing); else → `DrawInside` only. Remove the "global outdoor draw then shells/entities on top" structure. |
| Visibility | `PortalVisibilityBuilder` | **Keep.** Sole authority. Root at the player cell, project from the eye (§1.5 invariant). |
| Clip packing | `ClipFrameAssembler` / `ClipFrame` | **Keep.** Per-cell clip slots + `OutsideView` already produced; the per-cell loop consumes `CellViews` directly and/or the assembled slots. |
| Indoor orchestration | **new** `DrawInside` orchestrator (App layer) | The §2 per-cell loop. Owns: landscape-through-door, conditional Z-clear, the per-cell shell/object/particle draws. Lives in `AcDream.App.Rendering` (not `GameWindow.cs` per Code Structure Rule 1). |
| Cell shells | `EnvCellRenderer` | Driven per-cell (`Render(pass, {cellId})`). Keep the mesh/MDI/texture path. Self-contained GL state. |
| Entities | `WbDrawDispatcher` | Restructured: per-cell entity draw (entities by `ParentCellId == cellId`); outdoor scenery drawn in the landscape-through-door step. Delete the `:1756` `ParentCellId==null → return true` bypass and the dead `IsShellScopedSet` branch. |
| Particles | `ParticleRenderer` | Per-cell draw in the cell's clip scope (give particles a cell via the owning entity's `ParentCellId`). Closes #104. |
| Terrain | `TerrainModernRenderer` | Drawn only in the landscape-through-door step (Planes/Scissor clip). When `OutsideView` empty → not drawn (faithful Skip). |
| Sky/weather | `SkyRenderer` | Drawn in the landscape-through-door step, `gl_ClipDistance` clip (Stage-4, kept). |
| Root selection | `CellVisibility` | Keep `CameraCell` / root selection. Retire `VisibleCellIds` as a render gate. |
| Outside-looking-in | **new** `DrawPortal` path (R2) | Separate outdoor pview; `ConstructView(CBldPortal)` recursion; `DrawCells` the interior through the door's clip. |
---
## 7. Phase plan (holistic) + visual gates + retail anchors
> Each phase ends GREEN (build + `dotnet test`) **and** at a **user visual gate**. A seal is verified on
> screen (probes + the user's eyes), never off the test suite — the lesson that produced this redesign
> (handoff §9). Phases are NOT batched past a gate.
### R1 — Unified per-cell `DrawInside` (the core) — THE make-or-break phase
**Retail anchors:** `RenderNormalMode @ 0x453aa0` (binary decision), `PView::DrawInside @ 0x5a5860`,
`ConstructView @ 0x5a57b0`, `DrawCells @ 0x5a4840` (the seal + the three per-cell loops), fact 8
(visibility is the cull).
- Build the binary top-level decision (indoor → `DrawInside` only; the global outdoor pass is not issued).
- Build the per-cell loop: landscape-through-door (terrain + outdoor scenery + sky, clipped to `OutsideView`)
→ conditional Z-only clear → per-cell closed shells → per-cell objects → per-cell particles.
- Delete the `WbDrawDispatcher.cs:1756` bypass (outdoor scenery now enters only via the landscape-through-door
step). Retire the `CellVisibility.VisibleCellIds` render gate.
- **Verify the cell mesh seals** (the grey-floor investigation): `[shell]` probe + a dat dump of the cellar
EnvCell mesh; confirm floor + walls + ceiling are present and front-facing; decide stencil-vs-mesh-hole for
the doorway (§4.2).
- Internal checkpoints (probes + eyes, on the real per-cell architecture): (a) sealed shells + landscape-thru-door;
(b) per-cell objects (no entity bleed); (c) per-cell particles (no smoke bleed).
- **Gate (visual):** Holtburg cottage + cellar — sealed interior (opaque walls, solid floor, ceiling),
sky/rain through the door only, **no blue hole, no terrain under the floor, no grey floor**, no
entity/scenery/particle bleed.
### R2 — Outside-looking-in (`DrawPortal`)
**Retail anchors:** `PView::DrawPortal @ 0x5a5ab0`, `ConstructView(CBldPortal) @ 0x5a59a0`, fact 9.
- On the outdoor path, for each visible building door, run a separate `outdoor_pview`:
`ConstructView(CBldPortal)` (side-test the door plane, clip to the opening) → recurse into the interior →
`DrawCells` the interior through the door's clip. Same machinery as `DrawInside`.
- **Gate (visual):** standing in the street facing the cottage door/window, the **sealed interior** renders
through the opening — not transparent walls.
### R3 — Dungeons
**Retail anchors:** fact 10 (emergent — all-EnvCell, `seen_outside == 0`, no exit portals), the
`update_count` watermark (fact 12 / #95), `grab_visible_cells @ 0x52e220` (landscape iff `seen_outside`).
- Validate the all-EnvCell path on a real dungeon: `OutsideView` stays empty → no terrain/sky by
construction; sealed cells; the watermark BFS converges (confirm #95 closed — no FPS collapse).
- **Gate (visual):** a real dungeon is sealed, no terrain/sky, no FPS collapse from the BFS.
### R4 — Polish + cleanup + conformance
- Resolve the `CullMode.Landblock → None` double-sided stopgap (the real per-polygon winding).
- Remove the dormant WB-two-pipe scaffolding (`Building`, `BuildingLoader`, stencil/occlusion, anchor machinery).
- Headless conformance tests (G1G4 below). Confirm no missing textures across dungeons.
- Update the roadmap; flip the M1.5 milestone; memory notes.
- **Gate (visual):** cottage + dungeon + outside-looking-in, all sealed and seamless.
---
## 8. Risks
- **R1 is a large phase** (the holistic choice). Mitigation: build behind internal probe/eyes checkpoints on
the real per-cell architecture; keep the outdoor path working throughout (the 99% case is unaffected — the
binary branch leaves `DrawOutside` intact).
- **Per-cell draw-call count** vs the single global MDI (§5). Mitigation: within-cell batching + closest-first
early-Z; measure at the R1 gate; the N.6 baseline shows large CPU/GPU headroom.
- **The grey-floor cause is unconfirmed** — it may be a missing dat floor polygon, a winding/cull issue, or a
clear-color gap. Mitigation: evidence-first in R1 (probe + dat dump) before any fix; do **not** relax the
faithful terrain Skip (decision §1.3).
- **Do not reintroduce the abandoned approaches** (handoff §9): no stencil two-pipe, no `isInside` gate, no
AABB grace-frame for the visibility root.
## 9. No-shortcuts rules (enforced on every task — plan §5)
1. A fast-but-wrong path is rejected for the retail-faithful path; the tradeoff is noted in the commit.
2. No suppression flags, grace periods, or `if (problem) return early` guards at a symptom site.
3. Every AC-specific behavior cites a retail decomp anchor (address + pseudo-C line).
4. Mid-session research over guessing — if the retail behavior is unclear, read the decomp / attach cdb.
5. Each phase ends GREEN (build + tests) AND at a user visual gate. The seal is verified on screen.
## 10. Conformance / acceptance gates (headless asserts — retail doc A §7 CL-G)
- **G1 (cottage, `seen_outside`):** interior sealed; sky/rain through the door; no blue hole; no transparent
walls; no bleed. `OutsideView` non-empty ⇒ the landscape-through-door step runs.
- **G2 (dungeon, `seen_outside == 0`):** sealed; no terrain/sky; `OutsideView` empty ⇒ landscape step NOT run;
BFS converges (watermark) without blowup (#95).
- **G3 (outside-looking-in):** facing an open cottage door from the street, the interior renders through the
doorway (R2).
- **G4 (invariants):** PVS root id == physics `CurrCell.Id` every frame; a cell receiving two clip slices is
processed once per slice (watermark); eye drives projection, player cell roots visibility.
## 11. Decomp anchor index (verified in the research docs this session)
```
SmartBox::RenderNormalMode 0x00453aa0 pc:92635 binary decision (DrawInside vs LScape::draw)
SmartBox::is_player_outside 0x00451e80 pc:90996 low-word objcell_id < 0x100
CellManager::ChangePosition 0x004559b0 pc:94601 keep/release landscape on seen_outside
CEnvCell::grab_visible_cells 0x0052e220 pc:311878 self+stab; landscape iff seen_outside (@311893)
PView::DrawInside 0x005a5860 pc:433793
PView::ConstructView(CEnvCell) 0x005a57b0 pc:433750 the BFS worklist
PView::ConstructView(CBldPortal) 0x005a59a0 pc:433827 exterior→interior recursion (outside-looking-in)
PView::InitCell 0x005a4b70 pc:432896 per-portal sidedness; update_count = view_count
PView::ClipPortals 0x005a5520 pc:433572 exit portal → outside_view (@433662); interior → OtherPortalClip
PView::AddViewToPortals 0x005a52d0 pc:433446 enqueue neighbours / SetOtherSeen
PView::DrawCells 0x005a4840 pc:432709 LScape-thru-door + Z-clear + 3 per-cell loops
PView::DrawPortal 0x005a5ab0 pc:433895 outside-looking-in entry
CEnvCell::find_visible_child_cell 0x0052dc50 pc:311397 point → child cell via portals/stab_list (camera child)
LScape::draw 0x00506330 terrain+sky; clipped via Render::PortalList
portal_view_type.update_count acclient.h:32346 watermark — BFS convergence (#95/#102)
CCellStruct drawing_bsp acclient.h:32275 closed cell mesh (floor+walls+ceiling); no cap step
CCellPortal.other_cell_id acclient.h:32300 0xFFFFFFFF (low 0xFFFF) ⇒ EXIT PORTAL
```