acdream/docs/superpowers/specs/2026-05-30-phase-u-unified-render-pipeline-design.md
Erik 8601137330 docs(render): Phase U — unified retail-faithful render pipeline design spec
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>
2026-05-30 15:38:09 +02:00

25 KiB
Raw Blame History

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.1U.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_listPView::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_viewset_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 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 CellViews + 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<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 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 batchinggl_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 ~73427715) 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:77047715 — 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:1100711319
RenderOutsideInAcdream GameWindow.cs:213325
A8-perf instrumentation (_a8Perf*, MaybeFlushA8Perf, probe emit methods) GameWindow.cs:71347177, 1132111609
cameraInsideBuilding / a8IndoorBranchEnabled / ACDREAM_A8_INDOOR_BRANCH branch GameWindow.cs:73427523, 76137715, 78257831
_indoorStencilPipeline field / ctor / dispose GameWindow.cs:172, 19411944, 11690
PortalVisibilityBuilder.Build call site GameWindow.cs:11040 (replaced by the new orchestration)

Rework, don't delete: PortalVisibilityBuilder.csPortalViewBuilder (§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 ~415, 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.txtSmartBox::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).