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>
This commit is contained in:
Erik 2026-06-02 19:18:59 +02:00
parent 21bf97ed35
commit 7aca79f8eb
2 changed files with 336 additions and 0 deletions

View file

@ -0,0 +1,329 @@
# 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
```