acdream/docs/research/2026-06-02-retail-render-pipeline-full-reference.md
Erik 21bf97ed35 docs(render): REOPEN the render half — full retail-faithful redesign dossier (handoff + huge plan + 3 research docs)
The Phase W indoor seal did NOT land. The 2026-06-02 visual gate proved the interior render is fundamentally broken (#78: transparent walls, outdoor terrain + scenery entities bleeding in, grey floors, no outside-looking-in). Stage 4 (sky-through-door clip) was real but a top layer on a base that never sealed.

DECISIVE EVIDENCE (committed in the handoff): the PVS computes correctly AND the cell shells render correctly (opaque, textured, complete — the [shell] probe shows zero NOSNAP / zero missing-texture). The failure is the SEAL + three inconsistent gates — concretely the WbDrawDispatcher.cs:1756 ParentCellId==null -> return true bypass draws outdoor scenery indoors, and the indoor path draws the outdoor world then gates it instead of running ONLY DrawInside. Retail, when inside, runs ONE PView flood: visibility IS the cull; the landscape enters only through clipped exit portals + a conditional depth-only clear.

Dossier (per the user's mandate: NO shortcuts/bandaids, port from retail, redesign the whole pipeline if needed, brainstorm first):
- Master handoff (root cause + retail target + reusable-vs-redesign + apparatus + do-not-repeat + copy-paste pickup prompt).
- Huge staged redesign plan R0(brainstorm)->R1(one visibility authority, kill the bleed)->R2(indoor=DrawInside-only)->R3(the seal, DrawCells port)->R4(per-cell object/particle clip)->R5(outside-looking-in)->R6(dungeons)->R7(polish/conformance). Each ends at a user visual gate.
- 3 research docs: full retail render pipeline reference (705 lines, decomp-verified), acdream pipeline inventory + failure map, reference cross-check (WB two-pipe is the wrong model).

#78 promoted to the redesign. The 5 remaining Core test failures are pre-existing physics/collision bugs, none render-related.

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

810 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Retail AC — the FULL render pipeline (outdoor + indoor + dungeon + seal), implementable reference
> **Purpose.** The single, exhaustive, port-ready reference for how the **retail** AC client
> (Sept 2013 EoR build, PDB-named) renders the world: outdoor landscape, building interiors,
> dungeons, portals, the "seal" (drawing the outside through a doorway with no blue hole,
> capped ceilings, no wall-bleed), and object/particle clipping. This is the foundation of the
> Phase W / Phase U unified-render redesign. **The code is modern; the behavior is retail —
> port this faithfully, with no shortcuts.**
>
> **Sources.** Consolidates the four 2026-06-02 decomp studies
> (`-opus48-a`, `-opus48-b`, `-sonnet46`, `-codex`) and the approved design spec
> (`docs/superpowers/specs/2026-06-02-phase-w-transition-membership-and-pview-render-design.md`),
> then **deepens the DRAW/seal pipeline** with the actual pseudo-C read this session from
> `docs/research/named-retail/acclient_2013_pseudo_c.txt` and verbatim structs from
> `docs/research/named-retail/acclient.h`. Citations are `Class::method @ 0xADDR (pc:LINE)` for
> decomp and `acclient.h:LINE` for structs. Membership/transition material (Section A) is
> deliberately summarized — it is fully covered in the four studies and is *not* the render
> redesign's spine; the render pipeline (Sections 27) is the new, deep material.
---
## 1. Executive summary — the ONE retail pipeline (≤12 bullets)
1. **One cell graph, one membership answer, render obeys it.** Physics tracks the player's cell
as `SPHEREPATH::curr_cell` carried *through* the collision sweep; the camera tracks its own
`viewer_cell` via a second (spring-arm) transition. Both resolve through the **same**
`CObjCell::GetVisible(objcell_id)` graph. There is **no** separate render cell system, **no**
static "re-derive the cell from XYZ", and **no** "underground" boolean.
2. **Top-level decision is binary, per frame** (`SmartBox::RenderNormalMode @ 0x00453aa0`,
pc:92635). `is_player_outside`/viewer-cell test → **outdoor** path = `LScape::draw` (full
terrain+sky+buildings); **EnvCell** path = `RenderDevice::DrawInside(viewer_cell)`. It is
*not* "landscape then inside" at the top level — when inside, **only** `DrawInside` runs.
3. **`DrawInside` (`PView`) is one portal-flood traversal** (`PView::DrawInside @ 0x005a5860`
`ConstructView @ 0x005a57b0``DrawCells @ 0x005a4840`). It produces an ordered
`cell_draw_list` (visible EnvCells) plus per-cell screen **clip regions** (`portal_view`) and
one accumulated `outside_view` (the outdoors seen through exit portals).
4. **The landscape is pulled INTO the indoor traversal through exit portals.** A portal with
`other_cell_id == 0xffffffff` is an *exit portal*. `ClipPortals` (`@ 0x005a5520`, pc:433662)
copies its screen clip region into `this->outside_view` (gated by `draw_landscape`). This is
the seam that makes the outside visible through a doorway.
5. **The seal sequence in `DrawCells`** (pc:432715+), when `outside_view.view_count > 0`:
(a) `LScape::draw(lscape)` with `Render::PortalList = this` → terrain+sky+rain+exterior,
**clipped to the doorway**; (b) a **conditional Z-only clear** `Clear(4, …)` (flag 4 = Z
buffer, NOT color; conditional on `portalsDrawnCount`); (c) per-cell exit-portal **stencil**
via `DrawPortalPolyInternal`; (d) per-cell **`DrawEnvCell`** (the closed interior geometry);
(e) per-cell **`DrawObjCellForDummies`** with `PortalList` set to that cell's view (objects).
6. **There is no blue clear-color hole, by construction.** The only clear in the indoor path is
`Clear(4, …)` = depth only, and even that is conditional. The doorway shows real terrain/sky
because the landscape was drawn first, clipped to the exit-portal region.
7. **Ceilings/walls/floors are sealed by the dat geometry.** Each EnvCell's `drawing_bsp` is a
*closed* box (floor + walls + ceiling) with holes only where `CCellPortal`s exist. There is no
"cap the ceiling" step; `DrawCells` draws each visible cell's `drawing_bsp`. Portal openings
are masked, not filled.
8. **Visibility IS the cull.** Only cells reached by the portal BFS are in `cell_draw_list`; only
their objects (`object_list`, drawn per-cell with `PortalList` set) are drawn. An object/
particle in a non-visible cell is never iterated → no wall bleed-through. Membership comes from
the *same* physics `enter_cell`/`leave_cell` graph.
9. **Outside-looking-in is the mirror image, same machinery.** While drawing the landscape, when
the camera can see a building's door, `PView::DrawPortal @ 0x005a5ab0` runs
`ConstructView(CBldPortal, polygon, …) @ 0x005a59a0` — a viewer-vs-portal-plane side test, a
`GetClip`, then it **recurses `ConstructView(interior_cell, other_portal_id)`** and `DrawCells`
the interior through the door's clip region. Outside↔inside is one recursive portal-clipped
traversal over the shared graph.
10. **Dungeons are emergent, not flagged.** A dungeon is an all-EnvCell landblock (terrain heights
0, ≥1 EnvCell, no buildings) where every cell has `seen_outside == 0` and no exit portals. So
`outside_view.view_count` stays 0, `LScape::draw` is never reached, and there is no terrain/
sky — automatically. Same `DrawInside` path as a cottage interior.
11. **Terrain/sky/landscape state keys off `seen_outside`.** `CellManager::ChangePosition @
0x004559b0` keeps the landscape loaded + sunlight/outdoor-ambient live iff the current cell is
a landcell **or** `CObjCell::seen_outside` is set; otherwise it `release_all`s the landscape
and uses flat indoor ambient. `CEnvCell::grab_visible_cells @ 0x0052e220` loads the landscape
iff `seen_outside`.
12. **BFS convergence is watermark-bounded, not capped.** `ConstructView` uses a per-cell
`portal_view_type.update_count` watermark + a `cell_todo_list` worklist; a cell can be
re-processed only for genuinely new view slices (`update_count → view_count`). This is the
retail replacement for acdream's fixed `MaxReprocessPerCell` cap (issue #102) and the right
fix for dungeon PVS blowup (#95).
---
## 2. Render-decision tree — outdoor / indoor / both
### 2.1 The single per-frame gate: `SmartBox::RenderNormalMode @ 0x00453aa0` (pc:92635)
Verbatim control flow (pc:92642-92684), with the decompiler-garbled predicate named:
```c
if (render_device->m_bOpenScene != 0) {
// edi_2 == SmartBox::is_player_outside(this)-equivalent: viewer is in an OUTDOOR landcell
// ebx_1 == "outside is relevant" = (viewer outside) || (viewer_cell->seen_outside != 0)
if (edi_2 != 0 || this->viewer_cell->seen_outside != 0) ebx_1 = 1; else ebx_1 = 0;
// FOV / view-distance setup (omitted) ...
if (edi_2 == 0) { // ── VIEWER IS INSIDE AN ENVCELL ──
if (ebx_1 != 0) { // cell can see outside:
uint eax = Position::get_outside_cell_id(&this->viewer); // pc:92669
LScape::update_viewpoint(this->lscape, eax); // pre-position terrain
}
Render::update_viewpoint(&this->viewer);
render_device->vtable->DrawInside(render_device, this->viewer_cell); // pc:92675 PView
} else { // ── VIEWER IS OUTSIDE (landcell) ──
LScape::update_viewpoint(this->lscape, this->viewer.objcell_id); // pc:92679
Render::update_viewpoint(&this->viewer);
Render::set_default_view();
Render::useSunlightSet(1);
LScape::draw(this->lscape); // pc:92683 full outdoor render
}
}
D3DPolyRender::FlushAlphaList(0);
// ... target bounding-box callback + rendering callback ...
```
**Key facts:**
- **Exactly two branches**, chosen by `edi_2` = "is the viewer cell an outdoor landcell?" When
outside, the *only* draw is `LScape::draw`. When inside, the *only* draw is `DrawInside` — the
landscape, if shown, is drawn **inside** `DrawInside`/`DrawCells` (Section 4), *not* here.
- **`ebx_1` (= outside-relevant)** controls whether the landscape's *viewpoint* is updated before
the indoor draw, so that if a doorway shows terrain it is centered on the right landblock. The
*actual* terrain draw-through-door decision is `outside_view.view_count > 0` inside `DrawCells`.
- The predicate maps to **`SmartBox::is_player_outside @ 0x00451e80` (pc:90996)**:
```c
int is_player_outside(SmartBox* this) {
if (this->player == 0) return 0;
uint lowWord = (uint16_t)this->player->m_position.objcell_id; // pc:91004
return lowWord < 0x100; // (decompiler garbles the compare; semantics: outdoor iff < 0x100)
}
```
Outdoor landcell ids are `0x0001..0x0040`; EnvCell ids are `>= 0x0100`. This low-word test is
the *type* discriminator used everywhere (`find_cell_list` pc:308753; `GetVisible` pc:308209).
### 2.2 The decision matrix (port this exactly)
| Viewer cell | `seen_outside` | Top-level draw | Terrain/sky? |
|---|---|---|---|
| Outdoor landcell (`id&0xFFFF < 0x100`) | n/a | `LScape::draw` (full) | Yes (always). Buildings recurse via `CBldPortal`/`DrawPortal`. |
| EnvCell (cottage/inn interior) | **1** | `DrawInside` | **Only through exit portals** (`outside_view>0` → `LScape::draw` clipped to doorway). |
| EnvCell (dungeon / sealed room) | **0** | `DrawInside` | **No** (no exit portal reachable → `outside_view==0` → `LScape::draw` never called). |
**There is no "both at once" at the top level.** "Both" happens *only inside* the indoor path:
`DrawCells` draws the landscape (through exit portals) **then** the interior cells, in one pass.
### 2.3 `RenderDeviceD3D::DrawInside @ 0x0059f0d0` (pc:427843) — thin forwarder
```c
return PView::DrawInside(RenderDeviceD3D::indoor_pview, arg2); // pc:427847
```
The render device owns two persistent `PView` instances: `indoor_pview` (rooted at the viewer
cell) and `outdoor_pview` (used by `DrawPortal` for outside-looking-in, Section 6.2).
---
## 3. PView traversal — step by step
`PView` ("portal view") is the heart of indoor rendering: a breadth-first (worklist) portal flood
over the shared `CEnvCell` graph that yields one ordered visible-cell list + per-cell clip regions.
### 3.1 Entry: `PView::DrawInside @ 0x005a5860` (pc:433793)
```c
Render::object_scale = 1; /* object_scale_vec = (1,1,1) */
CEnvCell::curr_view_push(viewer_cell); // pc:433800 push a view slot
PView::add_views(this, viewer_cell->num_stabs, viewer_cell->stab_list); // pc:433801 seed PVS from stab list
Frame::cache(&identityFrame); Render::positionPush(3, &identityFrame);
Render::copy_view(viewer_cell->portal_view[num_view-1], nullptr, 4); // pc:433814 seed root view = full screen
ConstructView(this, viewer_cell, 0xffff); // pc:433817 build visible set
PView::DrawCells(this, …); // pc:433819 draw it
Render::framePop();
PView::remove_views(this, viewer_cell->num_stabs, viewer_cell->stab_list);
viewer_cell->num_view -= 1; // pop the view slot
```
`add_views`/`remove_views` (`@ 0x005a5210`) push/pop a per-cell `portal_view_type` slot for every
cell in the root's stab list, so the BFS has somewhere to accumulate clip regions for those cells.
### 3.2 The BFS: `PView::ConstructView(CEnvCell*, ushort) @ 0x005a57b0` (pc:433750)
Verbatim:
```c
this->outside_view.view_count = 0; // reset the outdoor-through-portal accumulator
PView::master_timestamp += 1; // new traversal stamp (cycle guard)
this->cell_todo_num = 0; this->cell_draw_num = 0;
PView::InitCell(this, root, portalId); // compute root's per-portal in/seen flags + clip
PView::InsCellTodoList(this, root, 0); // push root onto the worklist
while (true) {
if (cell_todo_num <= 0) return;
cell = cell_todo_list[--cell_todo_num]->cell; // POP (LIFO: index num-1)
if (cell == 0) return;
// append to the visible draw list (grow by 0x1e if needed):
cell_draw_list[cell_draw_num++] = cell; // pc:433783
cell->portal_view[cell->num_view - 1]->cell_view_done = 1; // pc:433784
if (PView::ClipPortals(this, cell, 0) != 0) // pc:433786 clip this cell's portals
PView::AddViewToPortals(this, cell); // pc:433787 enqueue visible neighbours
}
```
**Outputs:** `cell_draw_list[0..cell_draw_num]` (visible EnvCells in pop order), each cell's
`portal_view[...]->view` (accumulated screen clip polys), and `this->outside_view` (exit-portal
clip regions). Note it is technically a LIFO worklist (a stack), but converges to the same visible
set; "BFS" is used loosely in the studies.
### 3.3 `PView::InitCell @ 0x005a4b70` (pc:432896) — per-portal sidedness + clip init
For a cell's current view slot (`portal_view[num_view-1]`):
- Early-out if `esi[0xe] == 0` (no active view — `update_count`/`view_count` empty). pc:432903.
- `Render::positionPush(3, &cell->pos)` to enter cell-local space.
- For each portal: compute the portal polygon's plane vs the **viewer viewpoint**
(`Render::FrameCurrent->viewer.viewpoint`), set per-portal `seen`/`inflag`
(`portal_info { int seen; int inflag; }`, acclient.h:32459) according to `portal_side` — i.e.
which portals face the viewer and can be seen through. Compute `max_indist` and set
`update_count = view_count` (the watermark — see §5.4). This is "which of my portals are live
for this view slice."
### 3.4 `PView::AddToCell @ 0x005a4d90` (pc:433050) — incremental view-slice merge
When a cell is reached **again** through a *different* portal (a second clip region), `AddToCell`
processes only the **new** view slices, from the cell's `update_count` watermark up to its current
`view_count` (pc:433056-433080). This is how a far cell seen through two doorways accumulates two
clip regions without re-walking the slices it already has — the watermark guarantees each slice is
expanded once.
### 3.5 `PView::ClipPortals @ 0x005a5520` (pc:433572) — clip portals + feed `outside_view`
Two phases. **Phase 1 (pc:433583-433620):** for each portal with `seen != 0 && inflag != 1`,
resolve `other_cell_ptr` (via `CEnvCell::GetVisible(other_cell_id)`, cached into the portal); set a
local `var_c = 1` if the portal has *any* destination — a loaded neighbour, **or** the exit
sentinel `other_cell_id == 0xffffffff`, or a resolvable neighbour. If `var_c == 0` (no portal goes
anywhere) the function returns 0 → the cell contributes no neighbours.
**Phase 2 (pc:433622-…):** for each live view slice `i` of this cell, `Render::set_view`, then per
portal compute its screen clip via `GetClip` (`@ 0x005a4320`, projects the portal poly to screen,
runs `polyClipFinish`, honors `Sidedness`). The decisive branch on the portal's destination
(pc:433651-433701):
```c
GetClip(this, portal.portal_side, portal.portal, &clip_view, &clipNonEmpty, 1);
if (clipNonEmpty != 0) {
if (portal.other_cell_id == 0xffffffff) { // pc:433662 ── EXIT PORTAL ──
if (this->draw_landscape != 0) { // pc:433664 indoor PView built with draw_landscape=1
if (cliplandscape != 0)
Render::copy_view(this /*->outside_view*/, &clip_view, clipNonEmpty); // pc:433674
else /* draw_landscape */
Render::copy_view(this /*->outside_view*/, nullptr, 0);
}
} else if (portal.other_cell_ptr != 0) { // pc:433687 ── INTERIOR PORTAL ──
// OtherPortalClip intersects clip with the neighbour's own portal opening, then
// copies the resulting clip region into the neighbour cell's portal_view (set_view/copy_view)
OtherPortalClip(this, portal, &clip_view, &clipNonEmpty); // pc:433692
// → records the clipped view onto edi_1 (the neighbour's portal_view at +0x134/+0x138)
}
}
```
**This is the load-bearing seam.** The exit-portal branch (`0xffffffff`) is the *only* place the
outdoors enters the indoor traversal: it accumulates the doorway's screen region into
`this->outside_view`. The interior branch propagates the clipped region into the neighbour cell so
that cell is later drawn only through the intersection of all portals it was seen through.
### 3.6 `PView::AddViewToPortals @ 0x005a52d0` (pc:433446) — enqueue neighbours
For each portal of the cell whose `other_cell_ptr` exists and is active:
```c
neighbour = portal.other_cell_ptr;
if (neighbour not yet inited this traversal) {
InitCell(this, neighbour, portal.other_portal_id); // pc:433480 set up neighbour's view slot
InsCellTodoList(this, neighbour, portal.max_indist); // pc:433485 enqueue it
if (portal.flag >= 0) SetOtherSeen(this, cell, portalIdx); // pc:433490 mark exit-seen / matching portal
} else {
AddToCell(this, neighbour, portal.other_portal_id); // pc:433494 merge a new clip slice
}
```
`SetOtherSeen @ 0x005a4e30` records the reciprocal portal as seen (prevents re-entering the cell
you just came from with a redundant slice). `InsCellTodoList @ 0x005a4f50` pushes onto the worklist
(growing it as needed).
### 3.7 The exterior→interior sibling: `ConstructView(CBldPortal*, CPolygon*, int, int) @ 0x005a59a0` (pc:433827)
Used by `DrawPortal` (Section 6.2) when the camera is **outside** and can see a building door:
```c
// side-test the building portal polygon vs the viewer viewpoint:
d = dot(FrameCurrent->viewer.viewpoint, portal->plane.N) + portal->plane.d;
side = (d < 0.0002) ? NEGATIVE : (|d|<eps ? IN_PLANE : POSITIVE); // pc:433832-433849
if (side matches the portal's required sidedness) {
GetClip(this, side, portal, &clip_view, &arg3, arg4); // pc:433856 clip to the door opening
if (clip non-empty) {
interior = CEnvCell::GetVisible(bldPortal->other_cell_id); // the room behind the door
if (arg5 != 2) DrawPortalPolyInternal(portal, …); // stencil the doorway
ConstructView(this, interior, bldPortal->other_portal_id); // pc:433879 RECURSE into the interior
}
}
```
So **the same `ConstructView`** builds the interior visible set whether entered from inside (the
`CEnvCell` overload) or from outside through a building portal (the `CBldPortal` overload). This is
why outside↔inside is seamless.
---
## 4. Seal mechanics — `PView::DrawCells @ 0x005a4840` (pc:432709), the exact sequence
This is the function the prior studies under-covered. Read this section against the verbatim
pseudo-C; it has **three sequential per-cell loops** plus the landscape-through-door block.
### 4.1 The landscape-through-door block + conditional Z-clear (pc:432715-432732)
```c
if (this->outside_view.view_count > 0) { // pc:432715 an exit portal was visible
Render::useSunlightSet(1);
Render::PortalList = this; // pc:432718 tell LScape to clip to outside_view
LScape::draw(this->lscape); // pc:432719 DRAW terrain+sky+rain+exterior, CLIPPED
D3DPolyRender::FlushAlphaList(0);
render_device->m_nFrameStamp += 1;
// ── CONDITIONAL Z-ONLY CLEAR ──
bool nothingDrawn;
if (forceClear == 0) {
nothingDrawn = (D3DPolyRender::portalsDrawnCount == 0); // pc:432727
D3DPolyRender::portalsDrawnCount = 0;
}
if (forceClear != 0 || !nothingDrawn)
render_device->vtable->Clear(4, 0x820fc0, 1.0f); // pc:432732 flag 4 == Z-BUFFER ONLY
... (loops below run inside this `if`) ...
}
```
**Critical seal facts:**
- **`Clear(4, …)`** — the `4` is the **depth/Z buffer bit only**, NOT color. There is *no*
`Clear(color)` anywhere in this path. The "blue hole" acdream sees is the *absence* of the
`LScape::draw` step (the outdoors is never injected), not a stray blue clear.
- **The clear is conditional** on `portalsDrawnCount` (and `forceClear`). It re-establishes a clean
depth field for the interior cells after the landscape (which is at far depth) was drawn through
the doorway, so interior geometry composites correctly over it. Color is preserved → the terrain
pixels in the doorway survive.
- **`Render::PortalList = this`** before `LScape::draw` is what clips the *entire landscape draw*
to the union of exit-portal screen regions in `outside_view`. Outside the doorway region, the
landscape contributes nothing.
- **`0x820fc0`** is the clear-region/rect parameter (the area to clear), not a color.
### 4.2 Loop 1 — exit-portal stencil masks (pc:432737-432808)
Iterates `cell_draw_list` from `cell_draw_num-1` down to 1 (reverse). For each cell whose
`structure->drawing_bsp != 0`:
```c
SetCurrentMaterial(render_device, nullptr, 0);
Render::SetSurfaceArray(cell->surfaces);
Render::object_scale = 1; Render::positionPush(3, &cell->pos);
viewCount = (cell->num_view != 0) ? cell->portal_view[num_view-1]->view_count : -1; // -1 == "one default view"
for (view = 0; view < |viewCount|; view++) { // pc:432772
CEnvCell::setup_view(cell, view); // pc:432774 select this clip slice
for (j = 0; j < cell->num_portals; j++) {
if (cell->portals[j].other_cell_id == 0xffffffff) // pc:432785 EXIT portal
D3DPolyRender::DrawPortalPolyInternal(cell->portals[j].portal, 0); // pc:432786 STENCIL the opening
}
}
Render::framePop();
```
This stencils each exit-portal polygon (per clip slice) so the interior geometry drawn next is
masked to leave the doorway showing the already-drawn landscape. (`setup_view` re-binds the
per-slice clip region so multi-doorway cells are handled correctly.)
### 4.3 Loop 2 — draw the closed interior geometry (pc:432815-432866)
Runs **unconditionally** (outside the `outside_view>0` block — i.e., for dungeons too). Reverse
iterate `cell_draw_list`; for each cell with `drawing_bsp != 0`:
```c
SetCurrentMaterial(...); Render::SetSurfaceArray(cell->surfaces);
Render::object_scale = 1; Render::positionPush(3, &cell->pos);
viewCount = (cell->num_view != 0) ? cell->portal_view[num_view-1]->view_count : -1;
for (view = 0; view < |viewCount|; view++) {
CEnvCell::setup_view(cell, view); // pc:432852 select clip slice
render_device->vtable->DrawEnvCell(cell); // pc:432853 DRAW floor+walls+CEILING (drawing_bsp)
}
Render::framePop();
```
**`DrawEnvCell` draws the cell's full closed mesh** (floor, four walls, ceiling) from
`structure->drawing_bsp`, back-face-culled and clipped to the cell's accumulated `portal_view`
slices. The ceiling is sealed *because it is part of the cell mesh* — there is no separate cap step.
Portal openings are not filled (Loop 1 stenciled them).
### 4.4 Loop 3 — per-cell objects/particles, clipped (pc:432870-432882)
```c
for (cell = cell_draw_num-1; cell >= 0; cell--) {
eax = cell_draw_list[cell];
// PortalList = the cell's CURRENT portal_view slot (its accumulated clip region):
Render::PortalList = *(eax->portal_view.data[eax->num_view - 1] ... ); // pc:432877
render_device->vtable->DrawObjCellForDummies(eax); // pc:432878 draw this cell's objects
}
Render::object_scale = 1; Render::useSunlightSet(1); // restore
```
**`DrawObjCellForDummies(cell)`** draws the objects registered in that cell's `object_list`
(the same list physics `enter_cell`/`leave_cell` maintains), with `Render::PortalList` set to the
cell's clip region — so objects (and their attached particles) are clipped to the portal opening(s)
through which their cell is visible. **An object in a cell not in `cell_draw_list` is never drawn**
→ no bleed-through. An object straddling a portal is clipped to the opening.
### 4.5 Why the seal holds — the four guarantees
1. **No blue hole:** outdoors is drawn first (`LScape::draw`, clipped to `outside_view`); the only
clear is Z-only and conditional; color survives in the doorway.
2. **Sealed ceiling/walls:** each visible cell's `drawing_bsp` is a closed box; `DrawEnvCell` draws
it whole; portal holes are stencil-masked, not filled.
3. **No outdoor bleed-in:** the landscape only paints through exit-portal clip regions; if
`outside_view.view_count == 0` (dungeon), `LScape::draw` is never called at all.
4. **No object/particle bleed:** objects are drawn per-cell, clipped to that cell's `PortalList`,
only for cells in `cell_draw_list`.
---
## 5. Data structures (annotated, verbatim from `acclient.h`)
### 5.1 The cell hierarchy
```c
// acclient.h:30915 — the base cell. ONE graph for physics AND render.
struct CObjCell : SerializeUsingPackDBObj, CPartCell {
LandDefs::WaterType water_type;
Position pos; // cell-to-world frame
unsigned int num_objects;
DArray<CPhysicsObj*> object_list; // RENDER + physics: objects in this cell (Loop 3)
unsigned int num_lights; DArray<LIGHTOBJ const*> light_list;
unsigned int num_shadow_objects;
DArray<CShadowObj*> shadow_object_list; // PHYSICS: collision objects registered here
unsigned int restriction_obj;
ClipPlaneList **clip_planes;
unsigned int num_stabs;
unsigned int *stab_list; // STATIC visibility set (PVS): cell ids this cell can see
int seen_outside; // boolean: this interior can reach the outdoors ★
LongNIValHash<GlobalVoyeurInfo>* voyeur_table;
CLandBlock *myLandBlock_;
};
// acclient.h:31880 — adds a building pointer
struct CSortCell : CObjCell { CBuildingObj* building; };
// acclient.h:31886 — outdoor surface cell of a landblock (8×8 grid → ids 0x01..0x40)
struct CLandCell : CSortCell { CPolygon** polygons; BoundingType in_view; };
// acclient.h:32072 — interior cell (building room OR dungeon room; id >= 0x100)
struct CEnvCell : CObjCell {
unsigned int num_surfaces; CSurface** surfaces; // material/surface array (RENDER)
CCellStruct *structure; // geometry + 3 BSP trees (see 5.3)
CEnvironment *env;
unsigned int num_portals; CCellPortal* portals; // portal graph edges
unsigned int num_static_objects;
IDClass* static_object_ids; Frame* static_object_frames; CPhysicsObj** static_objects;
RGBColor *light_array; int incell_timestamp;
MeshBuffer *constructed_mesh; int use_built_mesh; unsigned int m_current_render_frame_num;
unsigned int num_view; // RENDER: # of active per-portal view slots
DArray<portal_view_type*> portal_view; // RENDER: per-portal clip state (see 5.4) ★
};
```
`seen_outside` is the dat flag `EnvCellFlags.SeenOutside = 0x1` (ACViewer
`ACE.Entity/Enum/EnvCellFlags.cs:7`). **Confirmed verbatim** at acclient.h:30929.
### 5.2 The portals
```c
// acclient.h:32300 — interior↔interior (room↔room) AND interior→exterior
struct CCellPortal {
unsigned int other_cell_id; // 0xFFFFFFFF (low 0xFFFF) ⇒ EXIT PORTAL (opens to the outdoors) ★
CEnvCell *other_cell_ptr; // cached resolved neighbour (or null until GetVisible)
CPolygon *portal; // the portal polygon (its plane = the doorway plane)
int portal_side; // which half-space is "inside" this cell
int other_portal_id; // index of the matching portal in the neighbour
int exact_match;
};
// acclient.h:32094 — outdoor landblock → building interior (the door from the street)
struct CBldPortal {
int portal_side;
unsigned int other_cell_id; // the interior EnvCell this building portal leads into
int other_portal_id;
int exact_match;
unsigned int num_stabs; unsigned int* stab_list; // PVS of interior cells visible through this door
float sidedness;
};
```
Held by `CBuildingObj : CPhysicsObj { num_portals; CBldPortal** portals; num_leaves;
CPartCell** leaf_cells; ... }` (acclient.h:31908), which a `CLandCell`/`CSortCell` references.
### 5.3 Per-cell geometry: `CCellStruct` (acclient.h:32275) — THREE BSP trees
```c
struct CCellStruct {
... vertex_array; num_portals; CPolygon** portals; surface_strips; polygons;
BSPTree* drawing_bsp; // RENDER — back-face order the closed cell mesh (DrawEnvCell)
... physics_polygons;
BSPTree* physics_bsp; // PHYSICS — collide the sphere against cell walls/floor
BSPTree* cell_bsp; // CONTAINMENT — point/sphere-in-cell tests (point_in_cell, membership)
};
```
A dungeon cell and a building-interior cell use the **same** struct; only their portal topology and
`seen_outside` differ. **The closed mesh (with ceiling) is authored in the dat** — there is no
ceiling-cap code path.
### 5.4 The render view state — `portal_view_type` (acclient.h:32346) + the watermark
```c
struct portal_view_type { // one slot per active view of a CEnvCell (CEnvCell.portal_view[])
DArray<portal_info> portal; // per-portal { int seen; int inflag; } (acclient.h:32459)
view_type view; // the screen-clip geometry (poly+vertex)
float max_indist;
unsigned int view_count; // how many view slices this cell currently has
int cell_view_done;
int view_timestamp;
int update_count; // ★ WATERMARK: slices [update_count..view_count) are "new"
};
struct view_type { unsigned vertex_count_total; DArray<view_poly> poly; DArray<view_vertex> vertex; };
struct view_poly { int vertex_count; int vertex_index; float xmin, xmax, ymin, ymax; }; // 2D screen clip rect/poly
```
**The `update_count` watermark — how it bounds the BFS without a fixed cap (issue #102):**
When a cell is *first* reached, `InitCell` sets `update_count = view_count` (the slices it has).
When the cell is reached *again* through another portal, `AddViewToPortals → AddToCell` only
processes slices from `update_count` up to the (now larger) `view_count` — i.e., the genuinely new
clip regions — then advances `update_count`. A cell therefore expands each distinct view slice
**exactly once**; the traversal terminates when no portal produces a new slice. `master_timestamp`
(bumped per `ConstructView`) + `view_timestamp`/`cell_view_done` are the per-traversal cycle guards.
**This is the retail replacement for acdream's `PortalVisibilityBuilder.MaxReprocessPerCell = 4`**
and the correct fix for dungeon PVS blowup (#95): the watermark naturally converges; a fixed cap
either truncates (missing cells) or never converges (blowup).
### 5.5 The traversal owner — `PView` (acclient.h:45934)
```c
struct PView {
portal_view_type outside_view; // ★ accumulated clip region of the outdoors seen thru exit portals
int draw_landscape; // 1 for indoor_pview ⇒ exit portals feed outside_view (ClipPortals)
CBldPortal **outdoor_portal_list; // building doors the camera can see (used by DrawPortal)
DArray<CEnvCell*> cell_draw_list; // ★ ordered visible cells (the BFS output)
unsigned int cell_draw_num;
DArray<CellListType*> cell_todo_list; // the worklist
unsigned int cell_todo_num;
LScape *lscape; // the landscape to draw through doorways
};
```
### 5.6 The physics-membership structs (summary — full detail in the studies)
```c
// acclient.h:32625 — the per-transition working state
struct SPHEREPATH {
... CObjCell* begin_cell; Position* begin_pos; Position* end_pos;
CObjCell* curr_cell; Position curr_pos; // ★ ACCEPTED membership (the answer)
... CObjCell* check_cell; Position check_pos; // candidate this sub-step
SPHEREPATH::InsertType insert_type;
CObjCell* backup_cell; ...
int hits_interior_cell; int cell_array_valid; ...
};
// acclient.h:31574 — the COLLISION candidate set (distinct from curr_cell)
struct CELLARRAY { int added_outside; int do_not_load_cells; unsigned int num_cells;
DArray<CELLINFO> cells; }; // CELLINFO = { uint cell_id; CObjCell* cell; }
```
---
## 6. Dungeons, outside-looking-in, object/particle clipping
### 6.1 Dungeons — emergent, no flag
- **Data:** a dungeon landblock has terrain heights all 0, ≥1 EnvCell, and **no buildings** (ACE's
`IsDungeon` heuristic, `references/ACE/.../Landblock.cs:575` — a *server* heuristic; the **client
needs no such flag**). Its EnvCells have `seen_outside == 0` and **no exit portals**.
- **Movement:** identical to a building interior — `transition` advances `curr_cell` across
`CCellPortal`s; `find_transit_cells`' exit-portal flag (`var_44`) never fires (no exit portals),
so `add_all_outside_cells` is never called → outdoor cells never enter the candidate set.
- **Loading:** `CEnvCell::grab_visible_cells @ 0x0052e220` (pc:311878) — adds self + every
`stab_list` cell to the visible table, then `if (seen_outside == 0) return;` (**pc:311893** —
dungeon stops here, never touches the landscape); a `seen_outside` cell tail-calls
`LScape::grab_visible_cells`. **This is the exact load-time decision** "stream the outdoor world
or not."
- **Render:** `DrawInside` runs; `ConstructView` floods the dungeon cells; **no exit portal is ever
reached**, so `outside_view.view_count` stays 0; `DrawCells`' landscape block (§4.1) is skipped;
Loops 2+3 still draw the closed cells + their objects. Result: sealed dungeon, no terrain, no sky
— by construction, same path as a cottage.
### 6.2 Outside-looking-in — a building interior seen through its door from the street
Driven by the **outdoor** render (`LScape::draw`), which calls `PView::DrawPortal @ 0x005a5ab0`
(pc:433895) for each building door the camera can see:
```c
Render::m_pRenderer->polyListFinishInternal(); ACRender::backup_curr_state();
bldPortal = this->outdoor_portal_list[arg2->portal_index]; // the building's door
portal = arg2->portal; // its polygon
PView::add_views(this, bldPortal->num_stabs, bldPortal->stab_list); // seed interior PVS
result = ConstructView(this, bldPortal, portal, arg3, arg4); // pc:433910 build interior visible set thru door
if (result == 0) { // door not actually visible
if (arg4 == 3) DrawPortalPolyInternal(portal, 0); // just stencil the closed door
ACRender::restore_curr_state();
} else {
if (arg4 != 1) PView::DrawCells(this, …); // pc:433924 DRAW the interior through the door
ACRender::restore_curr_state();
Render::positionPush(3, CBuildingObj::curr_pos); Render::obj_view_set();
}
PView::remove_views(this, bldPortal->num_stabs, bldPortal->stab_list);
```
So the **same `ConstructView` + `DrawCells`** that render an interior from inside also render it
from outside through the door — entered via the `CBldPortal` overload of `ConstructView`
(§3.7) which side-tests the door plane, clips to the opening, and recurses into the room. The
`outdoor_pview` instance is used here (vs `indoor_pview` for `DrawInside`). This is the path
acdream is missing ("outside-looking-in shows no interior" residual, spec §1a).
### 6.3 Object / particle clipping to the visible cell set
- **Membership:** objects live in a cell's `object_list` (and collision objects in
`shadow_object_list`), maintained by physics `enter_cell @ 0x00510ed0` / `leave_cell @
0x00510f50` — the **same** graph render reads. A particle attached to an object inherits the
object's cell.
- **Draw + clip (indoor):** Loop 3 of `DrawCells` (§4.4) iterates **only** `cell_draw_list`, draws
each cell's `object_list` via `DrawObjCellForDummies`, with `Render::PortalList` set to that
cell's clip region. Non-visible cells' objects are never iterated → no bleed.
- **Child-cell resolution for entities:** `CEnvCell::find_visible_child_cell @ 0x0052dc50`
(pc:311397) — given a point, returns the exact child cell containing it:
```c
if (point_in_cell(this, p)) return this; // pc:311402
if (arg3 == 0) { // search via portals:
for (portal in this->portals) {
n = CCellPortal::GetOtherCell(portal);
if (n && n->point_in_cell(p)) return n; // pc:311424
}
return 0;
} else { // search via stab_list (PVS):
for (id in this->stab_list) {
c = CEnvCell::visible_cell_table[id];
if (c && c->point_in_cell(p)) return c; // pc:311456
}
}
```
Used for the 3rd-person camera offset (which child cell is the eye in?) and for placing
entities/particles into the right cell **via the graph/BSP, never an AABB**. This is the
retail-faithful replacement for acdream's `CellVisibility.FindCameraCell` AABB + grace-frame
resolver.
---
## 7. PORTING CHECKLIST — the spine of the redesign
Ordered list of every behavior a faithful C# port must implement to get a fully-sealed
outdoor+indoor+dungeon render with **no bleed, no flaps, no transparent walls, no blue hole**.
Cross-referenced to the Phase W staged plan; each behavior cites its retail anchor.
### CL-A. Membership foundation (prerequisite — physics owns the cell)
*(Detail in the four studies + spec §1/§1a; summarized here because render roots on it.)*
- [ ] **A1.** `ResolveWithTransition` returns the swept `sp.CurCellId` (mirror
`SetPositionInternal @ 0x00515330` reading `sphere_path.curr_cell`), **not** a static
`ResolveCellId(origin,…)`. Demote `ResolveCellId` to seed-only (spawn/teleport/server-set).
- [ ] **A2.** Replace `FindEnvCollisions`' early static re-derive (`TransitionTypes.cs:1947→1949`)
with a retail directed exit-portal crossing (`CEnvCell::find_transit_cells @ 0x0052c820`
exit-portal path) — become outdoor by crossing the doorway polygon, not by re-resolving XYZ.
- [ ] **A3.** Port the `find_cell_list` interior-wins pick (`@ 0x0052b4e0`, pc:308814-308819) +
`do_not_load_cells` prune (pc:308829-308867) into `CellTransit`; re-gate `FindTransitCellsSphere`'s
unconditional `exitOutside=true` (`CellTransit.cs:95-123`).
- [ ] **A4.** Commit-on-difference: write `CellGraph.CurrCell` once, fire a "cell changed" event
only when it differs (mirror `change_cell @ 0x00513390`, the `if (this->cell != curr_cell)` guard).
### CL-B. Render root unification (Stage 3)
- [ ] **B1.** Port the **single per-frame decision** (`RenderNormalMode @ 0x00453aa0`): viewer cell
is an outdoor landcell (`id&0xFFFF < 0x100`, `is_player_outside @ 0x00451e80`) → outdoor path;
else → indoor `DrawInside`. Remove the `ACDREAM_A8_INDOOR_BRANCH` two-pipe split.
- [ ] **B2.** Root render visibility at the **physics `CellGraph.CurrCell`** (the U.4c flap fix),
not an independent AABB `FindCameraCell`. Delete the 3-frame grace-frame hack.
- [ ] **B3.** 3rd-person camera offset resolves its child cell via
`CEnvCell::find_visible_child_cell @ 0x0052dc50` (graph/BSP), rooted at the player cell — never
an AABB reclassification. (Eye drives projection; player cell drives the visibility root.)
- [ ] **B4.** Port the **landscape keep/release policy** (`CellManager::ChangePosition @
0x004559b0`, pc:94601-94682): keep landscape loaded + sunlight/outdoor-ambient live iff the
current cell is a landcell OR `seen_outside`; else `LScape::release_all` + flat indoor ambient.
- [ ] **B5.** Port `CEnvCell::grab_visible_cells @ 0x0052e220` (pc:311878): add self + `stab_list`
to the visible set; load the landscape **iff `seen_outside`** (the dungeon gate, pc:311893).
- [ ] **B6.** Pre-position the through-door terrain via `Position::get_outside_cell_id @
0x004527b0` + `LScape::update_viewpoint` before the indoor draw (when `seen_outside`).
### CL-C. PView traversal (Stage 4, the big one)
- [ ] **C1.** Port the BFS `PView::ConstructView(CEnvCell*) @ 0x005a57b0`: reset `outside_view`,
bump `master_timestamp`, `InitCell(root)`, push root, then pop-and-expand into `cell_draw_list`
via `ClipPortals` + `AddViewToPortals` until the worklist drains.
- [ ] **C2.** Port `PView::InitCell @ 0x005a4b70`: per-portal sidedness vs the **viewer viewpoint**
→ `seen`/`inflag`; compute `max_indist`; set `update_count = view_count`.
- [ ] **C3.** Port `PView::ClipPortals @ 0x005a5520` with **both** branches: exit portal
(`other_cell_id == 0xffffffff`) → `copy_view` into `outside_view` (gated by `draw_landscape`);
interior portal → `OtherPortalClip` → propagate clipped region into the neighbour's `portal_view`.
- [ ] **C4.** Port `PView::AddViewToPortals @ 0x005a52d0`: enqueue uninited neighbours
(`InitCell` + `InsCellTodoList` + `SetOtherSeen`); merge a new slice into already-inited
neighbours (`AddToCell @ 0x005a4d90`).
- [ ] **C5.** Implement the **`update_count` watermark** convergence (per-cell, slices
`[update_count..view_count)` processed once) — **delete the fixed `MaxReprocessPerCell` cap**
(closes #102; correct dungeon-PVS fix for #95).
- [ ] **C6.** Port `PView::GetClip @ 0x005a4320` (project portal poly → screen, `polyClipFinish`,
honor `Sidedness`) producing 2D `view_poly` clip rects/polys.
### CL-D. Seal mechanics in `DrawCells` (Stage 4)
- [ ] **D1.** When `outside_view.view_count > 0`: set `PortalList = this`, `LScape::draw(lscape)`
(terrain+sky+rain **clipped to the doorway**) FIRST (`DrawCells @ 0x005a4840`, pc:432715-432719).
- [ ] **D2.** Implement the **conditional Z-only clear** (`Clear(4, …)`, pc:432731-432732): depth
bit only, NOT color; conditional on `portalsDrawnCount`/`forceClear`. **Never `Clear(color)` in
the indoor path** — that is the blue-hole bug.
- [ ] **D3.** **Loop 1** — per visible cell, per view slice (`setup_view`), `DrawPortalPolyInternal`
every exit portal (`other_cell_id == 0xffffffff`) to stencil the openings (pc:432785-432786).
- [ ] **D4.** **Loop 2** — per visible cell, per view slice, `DrawEnvCell` the closed
`structure->drawing_bsp` (floor+walls+**ceiling**). Runs for dungeons too (unconditional). No
ceiling-cap step — the mesh is closed (pc:432852-432853).
- [ ] **D5.** **Loop 3** — per visible cell, set `PortalList` to the cell's clip region, then
`DrawObjCellForDummies` (the cell's `object_list`) — objects/particles clipped to the doorway
(pc:432876-432878).
- [ ] **D6.** Renderer self-contained GL state: the indoor pass must SET every GL state it depends
on (view-proj, BLEND, DepthMask, Cull, FrontFace, A2C) — never inherit (memory
`render-self-contained-gl-state`; `EnvCellRenderer` hit this 3× in U.4).
### CL-E. Outside-looking-in (Stage 4/5)
- [ ] **E1.** Port `PView::DrawPortal @ 0x005a5ab0` on the **outdoor** path: for each visible
building door (`outdoor_portal_list`), `add_views(stab_list)`,
`ConstructView(CBldPortal,polygon)`, then `DrawCells` the interior through the door's clip; if
the door isn't actually visible, just `DrawPortalPolyInternal` (stencil the closed door).
- [ ] **E2.** Port the `ConstructView(CBldPortal*) @ 0x005a59a0` exterior→interior recursion:
side-test the door plane vs the viewer, `GetClip` to the opening, `GetVisible(other_cell_id)`,
optional `DrawPortalPolyInternal`, then **recurse `ConstructView(interior, other_portal_id)`**.
- [ ] **E3.** Use a **separate `outdoor_pview`** instance for E1/E2 (vs `indoor_pview` for
`DrawInside`), matching `RenderDeviceD3D::indoor_pview`/`outdoor_pview_1`.
### CL-F. Entity / particle cell clipping (Stage 5)
- [ ] **F1.** Entities/particles are placed into cells via the **physics graph** (`object_list`),
resolved with `find_visible_child_cell @ 0x0052dc50` — never an AABB.
- [ ] **F2.** Entities/particles draw **only** for cells in `cell_draw_list`, clipped to the cell's
`PortalList` (Loop 3). Non-visible-cell entities are not drawn → no NPC/door/smoke bleed-through.
- [ ] **F3.** An entity straddling a portal is clipped to the portal opening (inherits the cell's
clip region), not drawn full-screen.
### CL-G. Conformance / acceptance gates
- [ ] **G1.** Cottage (`seen_outside`): flicker gone (CL-A); interior sealed; sky/rain visible
through the door; **no blue hole**; no transparent walls; no bleed-through.
- [ ] **G2.** Dungeon (`seen_outside == 0` everywhere): sealed; **no terrain/sky**;
`outside_view.view_count == 0`; traversal converges (watermark) without blowup (#95).
- [ ] **G3.** Outside-looking-in: standing in the street facing an open cottage door, the interior
(walls/floor/objects) renders through the doorway (CL-E).
- [ ] **G4.** Headless asserts: doorway `outside_view` non-empty + `LScape::draw` invoked;
sealed-cellar `outside_view` empty + `LScape::draw` NOT invoked; PVS root id == physics
`CurrCell.Id` every frame; a cell receiving two clip slices is processed once per slice.
---
## Appendix — primary decomp address index (all read/verified this session)
```
Top-level / landscape policy
SmartBox::RenderNormalMode 0x00453aa0 pc:92635 (binary decision; DrawInside vs LScape::draw)
SmartBox::is_player_outside 0x00451e80 pc:90996 (low-word objcell_id < 0x100)
Position::get_outside_cell_id 0x004527b0 pc:91552 (interior pos → outdoor landcell id)
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)
RenderDeviceD3D::DrawInside 0x0059f0d0 pc:427843 (→ PView::DrawInside(indoor_pview))
PView traversal + seal
PView::DrawInside 0x005a5860 pc:433793
PView::ConstructView(CEnvCell) 0x005a57b0 pc:433750 (the BFS worklist)
PView::ConstructView(CBldPortal) 0x005a59a0 pc:433827 (exterior→interior recursion)
PView::InitCell 0x005a4b70 pc:432896 (per-portal sidedness; update_count=view_count)
PView::AddToCell 0x005a4d90 pc:433050 (incremental new-slice merge)
PView::AddViewToPortals 0x005a52d0 pc:433446 (enqueue neighbours / SetOtherSeen)
PView::ClipPortals 0x005a5520 pc:433572 (exit→outside_view @433662; interior→OtherPortalClip)
PView::OtherPortalClip 0x005a5400 pc:433524
PView::GetClip 0x005a4320 pc:432344 (portal poly → screen clip)
PView::SetOtherSeen 0x005a4e30 pc:433089
PView::InsCellTodoList 0x005a4f50 pc:433183
PView::add_views / remove_views 0x005a5210 pc:433382
PView::DrawCells 0x005a4840 pc:432709 (LScape-thru-door + Z-clear + 3 loops)
PView::DrawPortal 0x005a5ab0 pc:433895 (outside-looking-in entry)
LScape::draw 0x00506330 (terrain+sky; clipped via Render::PortalList)
CEnvCell::find_visible_child_cell 0x0052dc50 pc:311397 (point → child cell via portals/stab_list)
Membership (summary; see studies)
CTransition::validate_transition 0x0050aa70 pc:272547
CTransition::check_other_cells 0x0050ae50 pc:272717
CObjCell::find_cell_list 0x0052b4e0 pc:308742
CPhysicsObj::SetPositionInternal 0x00515330 pc:283399
CPhysicsObj::change_cell 0x00513390 pc:281192
CEnvCell::find_transit_cells 0x0052c820 pc:309968
CLandCell::add_all_outside_cells 0x00533630 pc:317499
CEnvCell::check_building_transit 0x0052c5d0 pc:309827
CObjCell::GetVisible 0x0052ad40 pc:308209 (≥0x100→CEnvCell::GetVisible else CLandCell)
CEnvCell::GetVisible 0x0052dc10 pc:311378
Structs (acclient.h)
CObjCell 30915 (object_list 30920; shadow_object_list 30924; num_stabs/stab_list 30927-28; seen_outside 30929)
CEnvCell 32072 (structure 32076; portals 32079; surfaces 32075; num_view/portal_view 32089-90)
CLandCell 31886 | CSortCell 31880 | CBuildingObj 31908
CCellPortal 32300 | CBldPortal 32094 | CCellStruct 32275 (drawing_bsp/physics_bsp/cell_bsp)
portal_view_type 32346 (view_count, cell_view_done, view_timestamp, update_count) | view_type 32338
view_poly 32465 | portal_info 32459 (seen, inflag) | PView 45934 (outside_view, draw_landscape,
outdoor_portal_list, cell_draw_list, cell_todo_list, lscape)
SPHEREPATH 32625 | CELLARRAY 31574 | CELLINFO 31925
Reference cross-checks
ACE Transition.cs:984 / PhysicsObj.cs:1171 / ObjCell.cs:335 / EnvCell.cs:311 / CellArray.cs:17
ACE Landblock.cs:575 (IsDungeon, server heuristic — client needs no flag)
ACViewer EnvCellFlags.cs:7 (SeenOutside=0x1); Buffer.cs (brute-force draw, NO PView — divergent)
WorldBuilder PortalService / VisibilityManager.RenderInsideOut (flat stencil — Silk.NET base, NOT the algorithm)
```
> **Divergence note (do not copy the references' render model).** ACViewer brute-force draws all
> loaded EnvCells with a `DungeonMode` cull toggle; WorldBuilder uses a flat `RenderInsideOut`
> stencil pass. **Neither implements retail's portal-clipped `PView` / landscape-through-door.**
> For Sections 27 the decomp is the sole authority and it wins. WorldBuilder's classes are a
> useful Silk.NET *implementation* base (buffer management, shader plumbing) but the *algorithm*
> is `PView` as documented above.