# Phase U — Unified retail-faithful render pipeline (design spec) **Status:** design approved 2026-05-30 (brainstorm). Supersedes the abandoned A8/A8.F two-pipe approach (issue #103). **Milestone:** M1.5 — "Indoor world feels right." **Decision context:** [`docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md`](../../research/2026-05-30-unified-render-pipeline-decision-and-handoff.md) --- ## 1. The decision (recap) acdream inherited a **two-pipe** render structure from WorldBuilder: a normal outdoor draw plus a separate flat `RenderInsideOut` stencil pass toggled on `cameraInsideBuilding`. That split is the root cause of every indoor/outdoor seam bug — the "flap", missing / transparent walls, terrain bleeding into interiors — and it **cannot** be made seamless (you cannot hand off two pipes cleanly at a doorway). The A8.F effort tried to graft retail's recursive portal clip *on top of* the two-pipe stencil and failed its visual gate (#103): the CPU clip math was unit-test-correct, but the integration (one global NDC mask gating ALL outdoor geometry, with an else-branch that floods terrain when the mask is empty) is inherently fragile. **Retail has no such split.** It renders through one portal-visibility traversal (`PView`): from whatever cell the camera occupies, walk the portal graph, build a screen-space convex clip region per opening, and draw every visible cell — indoor *and* outdoor — gated by that region. The camera being indoors vs outdoors selects only *which cell is the root*; the draw machinery is identical. Seamless **by construction**. **Phase U builds that.** Modern code, retail behavior. ### Settled design decisions (brainstorm 2026-05-30) | # | Decision | Choice | |---|----------|--------| | Scope | First spec covers how much of `PView` | **Full Phase U in one spec** (indoor BFS + outdoor-camera building peering + dungeon-scale fixpoint termination + distance-priority ordering + reciprocal `OtherPortalClip`). Implemented in staged tasks U.1–U.6 with review/visual checkpoints. | | Clip gate | How a modern GPU enforces "draw only inside this region" | **Hardware clip planes (`gl_ClipDistance`)** for the exact gate + **scissor AABB** as a cheap broad-phase pre-reject. Mirrors retail's two-level structure (it stores both a per-edge `Plane` and an `xmin/xmax/ymin/ymax` box). Structurally immune to the #103 global-mask-flood. Stencil-per-cell is the documented fallback if clip planes prove insufficient. | | Terrain | How terrain + outdoor scenery fit | **Separate path, gated (retail-faithful).** `TerrainModernRenderer` stays as-is; when indoors, the whole terrain + outdoor-scenery batch is gated to the `OutsideView` clip planes (empty ⇒ no terrain). Terrain never enters the cell graph. Diverges intentionally from the handoff's "terrain as cells" sketch, which is *not* what retail does. | | Salvage | Reuse vs fresh-port the A8.F pieces | **Reuse the clip math, rewrite the builder, delete the stencil + orchestration.** Keep `PortalView` / `ScreenPolygonClip` / `PortalProjection` (pure, GL-free, ~36 passing tests). Rework `PortalVisibilityBuilder` (wrong termination, wrong queue, no `OtherPortalClip`, #103 under-production). Delete `IndoorCellStencilPipeline`, `portal_stencil.*`, and the GameWindow two-pipe orchestration. | --- ## 2. Goal & non-goals **Goal.** One render path. The camera's current cell is the root of a per-frame portal-visibility traversal that yields *(ordered visible cells, per-cell screen-space clip region, an `OutsideView` region for the outdoors)*. A single draw pass renders all visible geometry (indoor cells, indoor statics, server entities, terrain, scenery) gated by clip planes derived from those regions. No `cameraInsideBuilding` branch. No `RenderInsideOut` stencil pass. No outdoor-vs-indoor toggle in the draw orchestration — only root selection. **Non-goals (this phase).** - Re-porting the WB mesh/dat pipeline. `ObjectMeshManager` / `WbMeshAdapter` / `WbDrawDispatcher` / `TerrainModernRenderer` / `LandblockMesh` / `DatCollection` / texture decode all stay. Phase U is **visibility + draw orchestration**, not mesh extraction. - The 2026-05-30 camera-collision + physics work (`PhysicsCameraCollisionProbe`, `RetailChaseCamera`, the viewer/sight 30-step bypass). Kept verbatim. - Per-instance highlight / selection blink, alpha-blend mode subpasses, and other rendering-quality items not on the seam-fix critical path. --- ## 3. The retail oracle The thing we port. All line numbers are in `docs/research/named-retail/acclient_2013_pseudo_c.txt`; struct lines in `docs/research/named-retail/acclient.h`. ### 3.1 Top-level dispatch — `SmartBox::RenderNormalMode` (~92649) Branches on `viewer_cell->seen_outside`: - **Indoors** (`seen_outside == 0`, camera in a pure-indoor cell): `RenderDevice::DrawInside(viewer_cell)` → `PView::DrawInside(indoor_pview, viewer_cell)`. The BFS is rooted at the camera's `CEnvCell`. - **Outdoors**: `LScape::draw(lscape)` draws terrain + landblock geometry (including building shells). Building interiors are peered into *from within that path* (§3.4). `seen_outside` is set on a `CEnvCell` in `CEnvCell::find_cell_list` (~311044); it means "this interior cell has at least one open portal to the exterior" (cellar windows, inn doorways). It keeps the landscape loaded and sunlight injected for hybrid cells. ### 3.2 The clip region is a screen-space convex polygon — NOT scissor/stencil Retail stores a cell's clip region as `view_type` (acclient.h:32338): a set of `view_poly` (32465) entries, each a convex polygon of `view_vertex` (32483). **Each `view_vertex` carries a `Plane`** for Sutherland-Hodgman edge clipping, plus the polygon's `xmin/xmax/ymin/ymax` box for fast rejection. Retail clips **every polygon in software** against the active region (`Render::set_view` 343750 installs it; the rasterizer's clipper reads it). We cannot do per-polygon software clipping on a modern GPU — the per-edge `Plane` is exactly what maps to `gl_ClipDistance` (§5.2). ### 3.3 Indoor BFS — `PView::ConstructView(CEnvCell*, uint16_t)` (433750) 1. Reset `outside_view.view_count = 0`; bump `master_timestamp` (the frame counter that drives the fixpoint). 2. `PView::InitCell` (432896) — classify the root cell's front-facing portals. 3. `PView::InsCellTodoList` (433183) — seed a **distance-sorted priority queue** (`cell_todo_list`). 4. **Closest-first BFS:** dequeue nearest cell → append to `cell_draw_list` → `PView::ClipPortals` (433572): for each `seen && !inflag` portal, project + intersect with the current clip region (`PView::GetClip` 432344) and, when non-empty, propagate into the neighbour's `portal_view_type` via `Render::copy_view` (344784). `PView::OtherPortalClip` (433524) additionally clips against the neighbour's matching (reciprocal) portal. `PView::AddViewToPortals` (433446) enqueues grown neighbours; `update_count` vs `master_timestamp` is the convergence watermark. A portal with `other_cell_id == 0xFFFF` accumulates into `PView::outside_view` (the landscape window). 5. `PView::DrawCells` (432709) consumes `cell_draw_list`: draws landscape through `outside_view` (if non-empty), then each cell's geometry gated by its stored region (`CEnvCell::setup_view` → `set_view`). ### 3.4 Outdoor → building-interior peering `outdoor_pview` (a second `PView` instance, landscape-draw OFF) handles seeing *into* a building from outside. `RenderDeviceD3D::DrawBuilding` primes `outdoor_pview->outdoor_portal_list = building->portals` before drawing the shell. The shell's BSP traversal hits portal nodes (`BSPPORTAL::portal_draw_portals_only` ~326881), which call `RenderDeviceD3D::DrawPortal` (427852) → `PView::ConstructView(CBldPortal*, CPolygon*, …)` (433827, the **recursive** overload). That tests viewer sidedness against the building portal, clips the opening, resolves the destination `CEnvCell`, and **recurses into the CEnvCell BFS** (§3.3) rooted at the destination cell. `CBldPortal` (acclient.h:32094) fields: `portal_side`, `other_cell_id`, `other_portal_id`, `exact_match`, `num_stabs`, `stab_list`. **Seamlessness:** both roots draw the same geometry from opposite sides of the same portal — outdoor root peers *in* through the door; indoor root peers *out* through it (`0xFFFF → outside_view → landscape`). Same clip machinery, same convergence. The threshold crossing only swaps which cell is the root. ### 3.5 Terrain is its own path `RenderDeviceD3D::DrawBlock` (430027) / `DrawLandCell` is the landblock render path, visibility-tested in `LScape::draw_check_blocks` against the portal-derived clip polygon (`Render::PortalList = pview`). Terrain is **never** enrolled into the cell graph; it is gated by the single `outside_view` region when indoors and drawn normally when outdoors. --- ## 4. Architecture ``` camera pos ─► CellVisibility.FindCameraCell(pos) │ cell → INDOOR root null → OUTDOOR root ▼ PortalViewBuilder.Build(root, camPos, viewProj, lookup) [GL-free, CPU] • closest-first BFS through portals (distance-priority queue) • per-cell convex NDC clip region (ScreenPolygonClip + OtherPortalClip) • timestamp/update-count fixpoint termination • 0xFFFF exit portals → OutsideView ▼ ClipPlaneSet.From(region) [GL-free, CPU] • each NDC edge (a→b) → clip-space plane (nx,ny,0,d) • merge near-collinear; cap at 8; AABB-scissor fallback when over budget ▼ upload: per-cell plane SSBO (binding=2, keyed by CellId); terrain uniform planes ▼ ONE draw pass (depth buffer on): sky (ungated, no depth write) terrain + scenery (gated → OutsideView planes; outdoor root = ungated) indoor cells EnvCellRenderer.Render(visibleCells) (gated per-cell, SSBO) entities WbDrawDispatcher.Draw (gated per-cell, SSBO) particles / weather (ungated) ▼ ACDREAM_PROBE_VIS [vis] line on cell change ``` Two invariant properties: - **No two-pipe seam.** Indoor vs outdoor changes only the BFS root, not the draw pass. - **Per-cell gating at draw time, never a global mask.** Each draw carries its own region's planes (looked up by `CellId`); an empty region = zero planes pass = nothing drawn. The #103 flood-on-empty failure mode is structurally impossible. --- ## 5. Components Each is independently testable; interfaces are the contract. ### 5.1 `PortalViewBuilder` — GL-free visibility core (reworked) Reworks the existing `PortalVisibilityBuilder`. Reuses `PortalView` (`ViewPolygon`/`CellView`), `ScreenPolygonClip`, `PortalProjection` unchanged. ```csharp public sealed class PortalViewFrame { public IReadOnlyList OrderedVisibleCells { get; } // closest-first public IReadOnlyDictionary CellClipRegions { get; } public CellView OutsideView { get; } // 0xFFFF exits, clipped } public static class PortalViewBuilder { // Indoor root. public static PortalViewFrame Build( LoadedCell rootCell, Vector3 cameraPos, Matrix4x4 viewProj, Func lookup); // Outdoor-peering root (U.5): root at a building-exterior portal. public static PortalViewFrame BuildFromBuildingPortal( BuildingExteriorPortal portal, Vector3 cameraPos, Matrix4x4 viewProj, Func lookup); } ``` Changes from the #103 builder: - **Distance-priority queue** (retail `InsCellTodoList` 433183) replacing the plain FIFO → correct front-to-back order + early-out. - **Timestamp/update-count fixpoint** (retail `master_timestamp` + `update_count`, `AddViewToPortals` 433446) replacing the `MaxReprocessPerCell = 4` hard cap → converges on cyclic dungeon graphs (closes the #102 fast-follow, relates #95). A cell re-enqueues only when its accumulated region genuinely grows, tracked by a real watermark (the current near-no-op grow-guard is removed). - **Reciprocal `OtherPortalClip`** (retail 433524) — also clip the portal against the neighbour's matching portal polygon → prevents over-inclusion through skewed openings. - Emits per-cell `CellView`s + `OutsideView` (the existing builder already does the `OutsideView`; this generalises to all cells). `LoadedCell` already supplies everything needed (`CellVisibility.cs:24`): `CellId`, `WorldTransform`, `InverseWorldTransform`, `LocalBoundsMin/Max`, `Portals` (`CellPortalInfo{ OtherCellId, PolygonId, Flags }`; `OtherCellId == 0xFFFF` = exit), `ClipPlanes` (`PortalClipPlane{ Normal, D, InsideSide }`), `PortalPolygons` (`List`), `BuildingId`. ### 5.2 `ClipPlaneSet` — edge → clip-plane extractor (GL-free) ```csharp public readonly struct ClipPlaneSet { public int Count { get; } // 0..8 public ReadOnlySpan Planes { get; } // clip-space (nx,ny,0,d) public bool UseScissorFallback { get; } // true ⇒ region exceeded 8 edges; use AABB public Vector4 ScissorAabb { get; } // NDC xmin,ymin,xmax,ymax public static ClipPlaneSet From(CellView region); } ``` Each NDC edge from `(ax,ay)` to `(bx,by)` becomes the half-space "inside the edge". With `NDC = clip.xy / clip.w` (and `w > 0` for visible geometry), the screen-space line `nx·x + ny·y + d = 0` lifts to a clip-space plane via multiply-through-by-`w`: ``` gl_ClipDistance = nx·clip.x + ny·clip.y + d·clip.w // ≥ 0 ⇒ inside ``` so no un-projection is needed — the 2D edge directly yields a `vec4(nx, ny, 0, d)` applied to `gl_Position`. Sign convention is fixed by the builder's CCW winding (`EnsureCcw`). The 8-plane cap (GL guarantees `GL_MAX_CLIP_DISTANCES ≥ 8`) is handled by: (1) merge near-collinear edges (retail `copy_view` dedups within ~1px); (2) if still > 8, set `UseScissorFallback` and gate by the AABB box instead — conservative (slight corner over-draw, never hides geometry). ### 5.3 GPU gate — shaders + buffers - **`mesh_modern.vert`** (indoor cells, indoor statics, server entities all share it): new SSBO at `binding=2`: `struct CellClip { uint count; uint _pad0,_pad1,_pad2; vec4 planes[8]; } buffer ClipBuf { CellClip cells[]; };` indexed by a per-cell slot derived from `InstanceData.CellId`. The shader writes `gl_ClipDistance[i] = dot4(planes[i], gl_Position)` for `i < count`. **Does not break MDI batching** — `gl_ClipDistance` is a per-vertex built-in, not per-draw state, so the single `glMultiDrawElementsIndirect` per group is preserved. Existing bindings (`binding=0` instance mat4, `binding=1` batch tuple) are untouched. - **`terrain_modern.vert`**: a small uniform block `{ int count; vec4 planes[8]; }` for the `OutsideView`; writes `gl_ClipDistance`. `count = 0` when outdoors (ungated). - **CPU**: `glEnable(GL_CLIP_DISTANCE0 + i)` for `i < maxActiveCount` (and disable the rest); upload the per-cell SSBO + terrain uniforms each frame after the builder runs. An "outdoor / no-clip" sentinel slot (count 0) for entities outside any gated cell. A per-frame **CellId → SSBO slot** map is built alongside `CellClipRegions` (visible cells get slots 0..N; everything else maps to the no-clip sentinel). ### 5.4 Draw orchestrator — the GameWindow restructure Replaces the deleted two-pipe branch (GameWindow.cs ~7342–7715) with the §4 sequence. Wires in the **currently-orphaned** `EnvCellRenderer.Render(visibleCells)` (today only the two-pipe methods call it — outdoors you see shells + floating furniture but no cell walls). Keeps every audited retail-faithful fix (§7). The default outdoor draw — today `_wbDrawDispatcher.Draw(set: EntitySet.All)` at GameWindow.cs:7704–7715 — becomes the outdoor-root case of the unified sequence. ### 5.5 `ACDREAM_PROBE_VIS` — runtime visibility probe One `[vis]` line per frame on cell change: root cell id, `OrderedVisibleCells` count + ids, `OutsideView` poly count + extracted plane count, per-cell plane counts, and any 8-plane-cap scissor fallbacks. Owned by `PhysicsDiagnostics`-style diagnostic owner (per Code Structure Rule 5), runtime-toggleable via the DebugPanel. **This is the apparatus #103 lacked at runtime** — built in U.2, used to validate the builder against live frames before any GL work. --- ## 6. Data flow, two roots, error handling ### 6.1 Per-frame (see §4 diagram) Opaque correctness rides the depth buffer; the BFS front-to-back order is for early-Z and alpha sorting, not a correctness crutch (retail needed draw-order because its software clip path had no per-pixel depth test; we have one). Clip planes do exactly one job: keep each thing inside its portal-framed NDC region so nothing bleeds across an opening. ### 6.2 Indoor root (dominant, ships first — U.4) Camera in a `CEnvCell`. BFS from that cell; `0xFFFF` exits union into `OutsideView`; terrain gated to `OutsideView`. Fixes the cellar/inn bleed. ### 6.3 Outdoor-peering root (U.5) Camera outdoors. Terrain + shells draw normally. To peer into a building, root the builder at the building's camera-facing exterior portal and recurse into its cells (retail `outdoor_pview` / `DrawBuilding` / `DrawPortal` / `ConstructView(CBldPortal)`). **Dependency to confirm during U.5:** the render-side `BuildingExteriorPortal` (polygon + destination cell id + side). We carry `BldPortalInfo` on the physics side (`BuildingPhysics` / `CheckBuildingTransit`); U.5 surfaces it to the render layer (or reads the same dat structure). If the data is not readily available, U.5 is the place to add it — the spec flags it as the one open data dependency. ### 6.4 Error handling / fallbacks - **8-plane cap:** merge near-collinear edges; else AABB scissor fallback (conservative). Logged. - **Empty-region semantics (the #103 inversion):** empty `CellView` ⇒ cell not visible ⇒ not drawn; empty `OutsideView` ⇒ no outdoors visible ⇒ terrain not drawn. The correct reading of empty. - **Dungeon termination:** the timestamp/update-count fixpoint (§5.1) guarantees convergence on cyclic graphs. - **#103 under-production — tracked, not inherited:** the builder does not ship until `ACDREAM_PROBE_VIS` shows a non-empty, *narrowing* `OutsideView` at the cellar window on a live capture. Suspects to verify in U.2: exit-portal (`0xFFFF`) polygons actually populated in `PortalPolygons` at cell hydration; portal-side test not over-culling. - **Degenerate portals:** `< 3` verts after near-plane clip ⇒ skip (already in `PortalProjection`). - **Camera-cell miss:** existing 3-frame grace + brute-force fallback in `FindCameraCell`; worst case briefly renders as outdoor root (no crash, no flood). --- ## 7. Delete / keep audit (Task 1, surgical) **Delete (two-pipe machinery only):** | Target | Location | |--------|----------| | `IndoorCellStencilPipeline.cs` (792 LOC) + `portal_stencil.vert/.frag` | `src/AcDream.App/Rendering/` | | `RenderInsideOutAcdream` | `GameWindow.cs:11007–11319` | | `RenderOutsideInAcdream` | `GameWindow.cs:213–325` | | A8-perf instrumentation (`_a8Perf*`, `MaybeFlushA8Perf`, probe emit methods) | `GameWindow.cs:7134–7177`, `11321–11609` | | `cameraInsideBuilding` / `a8IndoorBranchEnabled` / `ACDREAM_A8_INDOOR_BRANCH` branch | `GameWindow.cs:7342–7523`, `7613–7715`, `7825–7831` | | `_indoorStencilPipeline` field / ctor / dispose | `GameWindow.cs:172`, `1941–1944`, `11690` | | `PortalVisibilityBuilder.Build` call site | `GameWindow.cs:11040` (replaced by the new orchestration) | **Rework, don't delete:** `PortalVisibilityBuilder.cs` → `PortalViewBuilder` (§5.1). **Keep (referenced beyond the two-pipe path):** `EnvCellRenderer.cs`, `Building.cs`, `BuildingLoader.cs`, `BuildingRegistry.cs`, `CellVisibility.cs`, and the clip-math trio `PortalView.cs` / `ScreenPolygonClip.cs` / `PortalProjection.cs` (+ their ~36 tests). **Keep these real bug-fixes (NOT visibility machinery) through the delete:** | Commit | Fix | |--------|-----| | `9559726` | `EnvCellRenderer` pool aliasing (list clear, `PostPreparePoolIndex` cursor, nested `isSetup`) | | `0fc6003` | `BuildingId` stamping + `GetCellsForLandblock` (cell lookup by landblock) | | `5dc4140` | `EnvCellRenderer` SSBO stride (64B mat4), FrontFace(CW)+per-batch CullMode via MDI, `EntitySet` partition | | `d5deeb3`, `0940d79`, `9ee42d4` | `EnvCellRenderer` GL-state correctness (cull state, cache invalidation at `Render` entry) | | `aae5300` + camera family | `PhysicsCameraCollisionProbe` / `RetailChaseCamera` spring-arm | Note: the `EntitySet` values introduced specifically for the two-pipe split (`IndoorPass`, `OutdoorScenery`, `BuildingShells`) get re-evaluated against the unified draw sequence in U.4 — the partition mechanism stays; some specific draw-call sites collapse. --- ## 8. Testing strategy - **Unit (GL-free):** `PortalViewBuilder` on synthetic graphs — a cottage chain (visible set, ordering, `OutsideView` narrowing) and a cyclic dungeon hub (fixpoint termination, no duplicate-accumulation blowup, `visibleCells` bounded). `ClipPlaneSet` on hand-worked polygons (edge→plane sign, 8-cap merge, scissor fallback). Reuse the existing ~36 clip-math tests. - **The real gate is visual + the runtime probe** — unit tests on synthetic data did **not** catch #103. We assert "`OutsideView` non-empty and narrowing at the cellar window" on a live `ACDREAM_PROBE_VIS` capture before claiming the builder works, then: - Cottage → cellar → out the door: no flap, walls solid, no terrain bleed, seamless threshold from any camera angle/zoom. - Holtburg Inn: no outdoor stabs/terrain through floor/walls (closes #78). - Dungeon via Town Network portal: `visibleCells` ~4–15, no foreign-dungeon geometry (closes/relates #95). - Zero regression to the outdoor default (today's working game). --- ## 9. Implementation staging The spec covers all of Phase U; the plan (writing-plans) details each task. Build + test green at every stage. | Stage | Deliverable | Gate | |-------|-------------|------| | **U.1** | Delete two-pipe surgically (§7); keep all audited fixes. Default path visibly unchanged. | Build/test green; "deck cleared" | | **U.2** | GL-free core: `PortalViewBuilder` (priority queue, fixpoint, `OtherPortalClip`) + `ClipPlaneSet` + `ACDREAM_PROBE_VIS`. Fully unit-tested. No GL. | Tests + probe validates live `OutsideView` | | **U.3** | GPU gate: clip-plane SSBO + `gl_ClipDistance` in `mesh_modern.vert`; terrain uniform planes in `terrain_modern.vert`; upload + slot-map infra. | Build green; clip visibly works on a test draw | | **U.4** | Indoor-root orchestration: wire `EnvCellRenderer.Render` + gated terrain into the unified pass. Cellar/inn bug fix. | **Visual gate #1** | | **U.5** | Outdoor-peering root: `BuildingExteriorPortal` root; seamless threshold from outside. (Confirms the §6.3 data dependency.) | **Visual gate #2** | | **U.6** | Dungeon-scale validation; close/relate #95 + #102; confirm `visibleCells` sane + perf. | **Visual gate #3** | --- ## 10. Risks - **Outdoor-peering data dependency (§6.3).** Medium. Render-side building-exterior-portal geometry may need surfacing from the physics-side `BldPortalInfo`. Isolated to U.5; the indoor bug fix (U.4) does not depend on it. - **#103 under-production recurrence.** Medium, mitigated. The reworked builder + the runtime probe + the empty-region inversion attack the exact failure; the probe makes recurrence falsifiable on live frames before GL work. - **8-plane cap on deep portal chains.** Low. M1.5 scenes are short chains; merge + scissor fallback covers the rest. Revisit only if a dungeon shows corner over-draw. - **MDI batching vs per-cell gating.** Low — resolved by the per-vertex `gl_ClipDistance` + CellId-indexed SSBO (no per-cell draws, batching preserved). --- ## 11. Reference index - **Decision / handoff:** [`docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md`](../../research/2026-05-30-unified-render-pipeline-decision-and-handoff.md) - **#103 failure detail:** [`docs/research/2026-05-29-a8f-visual-gate-failure-handoff.md`](../../research/2026-05-29-a8f-visual-gate-failure-handoff.md) - **Why WB can't express per-portal clipping:** project memory `indoor-portal-visibility-wb-vs-retail` - **Retail decomp:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` — `SmartBox::RenderNormalMode` 92649; `PView::ConstructView` 433750 (CEnvCell) / 433827 (CBldPortal); `ClipPortals` 433572; `GetClip` 432344; `DrawCells` 432709; `InitCell` 432896; `AddViewToPortals` 433446; `InsCellTodoList` 433183; `OtherPortalClip` 433524; `copy_view` 344784; `set_view` 343750; `DrawInside` 433793; `DrawPortal` 427852; `DrawBlock` 430027; `find_visible_child_cell` 311397; `GetVisible` 311378; `grab_visible_cells` 311878. Structs (acclient.h): `PView` 45934; `portal_view_type` 32346; `view_type` 32338; `view_poly` 32465; `view_vertex` 32483; `CCellPortal` 32300; `CBldPortal` 32094. - **acdream anchors:** `CellVisibility.cs` (`LoadedCell` :24, `FindCameraCell` :301, `PointInCell` :367, `GetVisibleCells` :426, `ComputeVisibility` :272); `EnvCellRenderer.cs` (MDI draw :876/:1058, `RegisterCell` :246); `TerrainModernRenderer.cs` (`Draw` :191, MDI :263); shaders `mesh_modern.vert`, `terrain_modern.vert`; clip-math `PortalView.cs` / `ScreenPolygonClip.cs` / `PortalProjection.cs`. - **Related issues:** #103 (this supersedes the A8.F arc), #78 (inn through-floor bleed — U.4), #95 (dungeon portal-graph blowup — U.6), #102 (builder fixpoint fast-follow — U.2/U.6).