One PView-faithful portal-visibility pass replacing the abandoned two-pipe (inside/outside) split (#103). Settled in brainstorm 2026-05-30: - Full Phase U in one spec (indoor BFS + outdoor building-peering + dungeon fixpoint + distance-priority ordering + reciprocal OtherPortalClip). - Per-cell gate = hardware clip planes (gl_ClipDistance) + scissor pre-check (retail's two-level model); structurally immune to the #103 global-mask flood. - Terrain stays its own path, gated to OutsideView (retail-faithful; NOT the handoff's "terrain as cells" sketch). - Salvage = reuse the clip math (PortalView/ScreenPolygonClip/PortalProjection, ~36 tests), rework the builder (PortalViewBuilder), delete the stencil pipeline + GameWindow two-pipe orchestration. Audited keep-list preserves the real EnvCellRenderer / BuildingId / camera-collision fixes. Staged U.1-U.6 with three visual gates. Retail anchors + acdream file:line injection points catalogued in the spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
25 KiB
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
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'sCEnvCell. - 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)
- Reset
outside_view.view_count = 0; bumpmaster_timestamp(the frame counter that drives the fixpoint). PView::InitCell(432896) — classify the root cell's front-facing portals.PView::InsCellTodoList(433183) — seed a distance-sorted priority queue (cell_todo_list).- Closest-first BFS: dequeue nearest cell → append to
cell_draw_list→PView::ClipPortals(433572): for eachseen && !inflagportal, project + intersect with the current clip region (PView::GetClip432344) and, when non-empty, propagate into the neighbour'sportal_view_typeviaRender::copy_view(344784).PView::OtherPortalClip(433524) additionally clips against the neighbour's matching (reciprocal) portal.PView::AddViewToPortals(433446) enqueues grown neighbours;update_countvsmaster_timestampis the convergence watermark. A portal withother_cell_id == 0xFFFFaccumulates intoPView::outside_view(the landscape window). PView::DrawCells(432709) consumescell_draw_list: draws landscape throughoutside_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.
public sealed class PortalViewFrame
{
public IReadOnlyList<uint> OrderedVisibleCells { get; } // closest-first
public IReadOnlyDictionary<uint, CellView> 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<uint, LoadedCell?> lookup);
// Outdoor-peering root (U.5): root at a building-exterior portal.
public static PortalViewFrame BuildFromBuildingPortal(
BuildingExteriorPortal portal, Vector3 cameraPos, Matrix4x4 viewProj,
Func<uint, LoadedCell?> lookup);
}
Changes from the #103 builder:
- Distance-priority queue (retail
InsCellTodoList433183) replacing the plain FIFO → correct front-to-back order + early-out. - Timestamp/update-count fixpoint (retail
master_timestamp+update_count,AddViewToPortals433446) replacing theMaxReprocessPerCell = 4hard 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
CellViews +OutsideView(the existing builder already does theOutsideView; 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<Vector3[]>),
BuildingId.
5.2 ClipPlaneSet — edge → clip-plane extractor (GL-free)
public readonly struct ClipPlaneSet
{
public int Count { get; } // 0..8
public ReadOnlySpan<Vector4> 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 atbinding=2:struct CellClip { uint count; uint _pad0,_pad1,_pad2; vec4 planes[8]; } buffer ClipBuf { CellClip cells[]; };indexed by a per-cell slot derived fromInstanceData.CellId. The shader writesgl_ClipDistance[i] = dot4(planes[i], gl_Position)fori < count. Does not break MDI batching —gl_ClipDistanceis a per-vertex built-in, not per-draw state, so the singleglMultiDrawElementsIndirectper group is preserved. Existing bindings (binding=0instance mat4,binding=1batch tuple) are untouched.terrain_modern.vert: a small uniform block{ int count; vec4 planes[8]; }for theOutsideView; writesgl_ClipDistance.count = 0when outdoors (ungated).- CPU:
glEnable(GL_CLIP_DISTANCE0 + i)fori < 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; emptyOutsideView⇒ 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_VISshows a non-empty, narrowingOutsideViewat the cellar window on a live capture. Suspects to verify in U.2: exit-portal (0xFFFF) polygons actually populated inPortalPolygonsat cell hydration; portal-side test not over-culling. - Degenerate portals:
< 3verts after near-plane clip ⇒ skip (already inPortalProjection). - 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):
PortalViewBuilderon synthetic graphs — a cottage chain (visible set, ordering,OutsideViewnarrowing) and a cyclic dungeon hub (fixpoint termination, no duplicate-accumulation blowup,visibleCellsbounded).ClipPlaneSeton 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 "
OutsideViewnon-empty and narrowing at the cellar window" on a liveACDREAM_PROBE_VIScapture 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 - #103 failure detail:
docs/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::RenderNormalMode92649;PView::ConstructView433750 (CEnvCell) / 433827 (CBldPortal);ClipPortals433572;GetClip432344;DrawCells432709;InitCell432896;AddViewToPortals433446;InsCellTodoList433183;OtherPortalClip433524;copy_view344784;set_view343750;DrawInside433793;DrawPortal427852;DrawBlock430027;find_visible_child_cell311397;GetVisible311378;grab_visible_cells311878. Structs (acclient.h):PView45934;portal_view_type32346;view_type32338;view_poly32465;view_vertex32483;CCellPortal32300;CBldPortal32094. - 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); shadersmesh_modern.vert,terrain_modern.vert; clip-mathPortalView.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).