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>
810 lines
48 KiB
Markdown
810 lines
48 KiB
Markdown
# 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 2–7) 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 2–7 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.
|