acdream/docs/research/2026-08-30-fw-walk-pseudocode.md
Erik 368c480bc2 feat(render) Campaign FW1: flood decomp appendix + the visibility math port
The second decomp round (flood bookkeeping/propagation, view-clip
support, landscape visibility) is archived Ghidra-arbitrated - it caught
a load-bearing BN inversion (InsCellTodoList pops NEAREST-first, which
is what makes the draw list far-to-near) and three more traps (the 192m
elided constant, the min/max double positional swap, the copy_view
cross order - the walk doc section 6 is corrected). WalkVisibilityMath
ports get_pt_limit @0x0054b840, get_clip_height @0x0054cff0,
corner/block_plane_check @0x0054b930/@0x0054d060, block_check
@0x0054dc50, and viewconeCheck @0x0054c250 with retail boundary
semantics (strict cull, inclusive partial, touch-out=Outside /
touch-in=EntirelyInside) under 23 focused tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 09:44:49 +02:00

277 lines
15 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.

# FW0 — the retail frame walk, port-ready pseudocode model (2026-08-30)
The distilled model FW1 ports from. Sources: the five decomp reads in
[`2026-08-30-fw-walk-pseudocode-appendix.md`](2026-08-30-fw-walk-pseudocode-appendix.md)
(BN pseudo-C cross-checked against the Ghidra patchmem decomp wherever
x87-flag mush demanded it), reconciled against the live traces in
[`2026-08-30-fw-walk-oracle/`](2026-08-30-fw-walk-oracle/README.md).
Every function below carries its named symbol + address; the appendix has
the full extractions with per-function gotchas.
## 1. The frame root — `SmartBox::RenderNormalMode` @0x00453aa0
Per frame (via `SmartBox::Draw``DrawNoBlit` @0x00454c20, which first
runs `update_viewer` and skips rendering entirely when `viewer_cell` is
null):
```
if ((viewer.objcell_id & 0xFFFF) < 0x100) { // CAMERA cell is outdoor
LScape::update_viewpoint(lscape, viewer.objcell_id);
Render::update_viewpoint(&viewer);
Render::set_default_view(); // full-screen view
Render::useSunlightSet(1);
LScape::draw(lscape); // → §3
} else { // camera in an EnvCell
if (viewer_cell->seen_outside)
LScape::update_viewpoint(lscape, Position::get_outside_cell_id(&viewer));
Render::update_viewpoint(&viewer);
render_device->DrawInside(viewer_cell); // → PView::DrawInside, §5
}
D3DPolyRender::FlushAlphaList(0f);
```
`SmartBox::update_viewer` @0x00453ce0 resolves the camera pose and cell:
a `CTransition` sphere sweep (the global `viewer_sphere`) from a pivot on
the player (CameraManager pivot part + offset) toward the camera's sought
position; `t->sphere_path.curr_cell` becomes `viewer_cell`. Fallbacks:
`AdjustPosition` on the raw sought position, else camera at the player
with `viewer_cell = null` (frame skipped). **The frame roots at the
CAMERA's cell** — proven live by the porch-cam trace.
## 2. The invisible-panel primitive — `D3DPolyRender::DrawPortalPolyInternal` @0x0059bc90
The single primitive behind both "punch" and "seal": draws one portal
polygon as an untextured CULL_NONE triangle fan, **depth-test ALWAYS,
z-write ON, color alpha 0** (invisible under SRCALPHA/INVSRCALPHA).
Byte-verified in the raw pseudo-C: `SetDepthBufferMode(DEPTHTEST_ALWAYS,
(mode>>2)&1)` @0059be02; forced Z `0.999998987f` @0059bf4b.
- `flag=1` (mode global `maxZ1`=7): every vertex Z forced to
**0.999998987 (far plane)** — the **PUNCH**: re-opens the aperture's
depth so look-in content can draw into it.
- `flag=0` (mode `maxZ2`=6): real projected Z — the **SEAL**: writes the
portal surface's own depth at the aperture. Only seals increment
`portalsDrawnCount` (the deferred z-clear trigger).
- Trivial reject: polygon wholly outside a ±12.0 local-XY box (the
24 m cell); the reject-loop polarity is an FP-status idiom (`test
ah,0x44`) BN could not lift — **re-verify polarity at FW1 against the
binary bytes** before porting.
- Debug: raising the mode's alpha bit (via the maxZ1/maxZ2 globals,
.data @0x820e18/0x820e14) renders the panels visibly in cycling colors.
- Goes through `DrawPrimitiveUP`, gated by the registry bool
`s_bAllowDrawPrimitiveUP` (default 1).
- Restores NO state — callers rely on backup/restore_curr_state.
**This resolves the campaign's panel question:** the "seal panels" are
not mesh subsets (those authored Translucency-1.0 subsets stay
never-drawn); they are the buildings'/cells' PORTAL POLYGONS drawn
depth-only by this primitive. AD-117's stamps unknowingly re-invented a
real retail mechanism at the wrong site with the wrong geometry.
## 3. The outdoor walk — `LScape::draw` @0x00506330
```
GameSky::Draw(sky, 0) // dome
LScape::draw_check_blocks(this) // per-view visibility union (below)
for i = mid_width² - 1 .. 0: // block_draw_list built near-to-far →
blk = block_draw_list[i] // backwards walk = FAR-TO-NEAR
if (blk && blk->in_view) render_device->DrawBlock(blk)
if (weather) GameSky::Draw(sky, 1)
```
- Grid: `mid_width`=11 × 11 blocks (`mid_radius`=5); block side 192 m
(FPU-elided in BN — pinned by the explicit 24 m cell pitch).
- `get_block_order` @0x00504c50: viewer's block first, then concentric
rings (8 quadrant-symmetric slots per step from static coefficient
tables); rebuild only when the viewer crosses a LANDBLOCK boundary.
- Block frames are **viewer-relative**: origin
`((rowviewer_b_xoff)·192, (colviewer_b_yoff)·192, 0)` — the
landscape renders in viewer-block-local space for float precision.
- `draw_check_blocks` @0x00505f80: clears every block/cell `in_view`,
then FOR EACH VIEW of `Render::PortalList` (outdoor: the default view;
interior: the exit views) unions visibility via clip-height interval
grids (`get_clip_height` corners → `block_check` vs max/min_zval);
`landcell_check` @0x005050a0 refines per-cell (non-8×8 far blocks:
all cells PARTIAL; ENTIRELY_INSIDE blocks: all cells 2; else per-cell
corner grids at 24 m pitch). A cell marked by an earlier view is never
downgraded.
- `CLandBlock::calc_sq_draw_order` @0x0052f4a0: per-block cell order,
closest cell (picked by the 9-case compass direction switch) written
LAST, rings filled backwards → forward iteration = far-to-near.
Early-out compares only `closest.x/y`, not `dir` (keep this quirk).
Per block, `RenderDeviceD3D::DrawBlock` @0x005a17c0: pass 1 updates
in-view cells' shadow objects + insertion-sorts shadow parts by depth;
pass 2 per cell (far-to-near): terrain (`DrawLandCell` — detail texture
only on full 8×8 blocks) then `DrawSortCell` @0x0059f140:
```
if (cell->building) render_device->DrawBuilding(cell->building) // §4
render_device->DrawObjCell(cell) // scenery/statics/dynamics via sorted shadow parts → CPhysicsPart::Draw(part, 0)
```
Mid-walk alpha pressure valve: `FlushAlphaList(flush)` with the global
`flush`=0.75 (flushes only above ~2250 queued alpha meshes).
## 4. Buildings — `RenderDeviceD3D::DrawBuilding` @0x0059f2a0
```
outdoor_pview->outdoor_portal_list = b->portals; // ALWAYS
CPhysicsPart::UpdateViewerDistance(part0); // degrade level
if (part0->gfxobj[deg_level] == 0) return; // degraded out → whole building skipped
CBuildingObj::curr_pos = &b->m_position; // frame-restore anchor for the portal pass
set building detail-surface state; FlushAlphaList(0f);
CPhysicsPart::Draw(part0, 1); // PORTAL pass
ObjBuildingOrBuildingPart = 1;
CPhysicsPart::Draw(part0, 0); // SHELL pass (normal mesh; per-frame dedupe applies)
ObjBuildingOrBuildingPart = 0;
```
The portal pass, through `DrawMesh` @0x005a0860 (per portal view,
`building_view`-gated; portal-flagged draws run even when viewconeCheck
says OUTSIDE) → `DrawMeshInternal` @0x0059f360, walks the building's
drawing-BSP **twice** (`BSPTREE::build_draw_portals_only` — portal nodes
visited far-to-near, plane epsilon 0.0002):
- **Pass 1 — punch.** Each in_portal → `PView::DrawPortal` @0x005a5ab0
`ConstructView(CBldPortal)` @0x005a59a0: viewer sidedness must match
the authored `portal_side` (IN_PLANE within ±0.0002 fails BOTH gates);
`GetClip` must leave ≥3 points; the destination EnvCell must be
currently Visible (`CEnvCell::GetVisible` — not loaded ⇒ silently no
punch, no fallback). On success: `DrawPortalPolyInternal(poly, 1)`
the far-Z punch — and the clipped view is pushed onto the destination
cell's top `portal_view` via `Render::copy_view`.
- **Pass 2 — look-in.** Same walk/gates; on success recurses
`ConstructView(CEnvCell)` (the §5 flood seeded through
`other_portal_id`) and calls `PView::DrawCells` — the trace's
`DC ov=0` punches: interior cells + objects drawn into the punched
aperture (no landscape, no clear, no sealing on this path).
- Push/pop asymmetry (MUST preserve): `ConstructView(CBldPortal)` pops
the building frame only on SUCCESS (before recursing); on failure the
frame stays pushed and `DrawPortal` compensates by re-pushing
`CBuildingObj::curr_pos` only on success.
- `DrawCells` bumps `m_nFrameStamp`, re-arming the per-frame part dedupe
so objects already drawn outdoors can draw again inside a look-in.
- DEAD in 2013 (do not port): `DrawBuildingLeaf` + `curr_leaf_cells`
(pre-EnvCell interior model); `PView::DrawPortal`'s pass==3
seal-on-fail branch (pass only ever ∈ {1,2}).
## 5. Interior frames — `PView::DrawInside` @0x005a5860
```
CEnvCell::curr_view_push(cell); // num_view++ (0x48-byte lazy slot, counters reset)
PView::add_views(this, cell->num_stabs, cell->stab_list); // push a view slot on every visible stab cell
Render::positionPush(3, identity Position stamped with cell DID);
Render::copy_view(cell->top_view, null, 4); // root view = full viewport quad
PView::ConstructView(this, cell, 0xffff); // the flood (below)
PView::DrawCells(this, 0); // dead arg
Render::positionPop(); remove_views(...); cell->num_view -= 1;
```
`ConstructView(CEnvCell)` @0x005a57b0 — the flood: zero
`outside_view.view_count`, bump `master_timestamp`, reset todo/draw
lists; seed `InitCell(cell, 0xffff)` + `InsCellTodoList(cell, 0)`;
then pop-from-END: append cell to `cell_draw_list` (grow +30), mark top
view `cell_view_done`, `ClipPortals``AddViewToPortals` (clips each
portal against the cell's current view and enqueues neighbors with their
clipped views; portals leading outdoors raise `outside_view`).
`PView::DrawCells` @0x005a4840 — the draw, all passes far-to-near
(`cell_draw_list` walked from the end):
```
if (outside_view.view_count > 0) { // 'ov' in the traces (field, not the dead arg)
Render::PortalList = &outside_view;
LScape::draw(lscape); // landscape THROUGH the exit views (§3)
FlushAlphaList(0f); m_nFrameStamp += 1;
if (forceClear || portalsDrawnCount != 0) // count reset only on the !forceClear arm
Clear(flag 4 /*z-buffer*/, black, z=1.0); // deferred clear triggered by earlier SEALS
for cells far-to-near, per view of the cell's top portal_view:
for each portal with other_cell_id == 0xFFFFFFFF: // exits to the landscape
DrawPortalPolyInternal(portal.poly, 0); // SEAL @ own depth (verified @0059a49b7)
}
useSunlightSet(0); restore_all_lighting();
for cells far-to-near, per view: setup_view; DrawEnvCell(cell); // BSP geometry
for cells far-to-near: PortalList = cell top view; DrawObjCellForDummies(cell); // objects
```
Degenerate guard NOT to port: `num_view==0` would loop 65535 views —
unreachable (every listed cell was `curr_view_push`ed).
## 6. View machinery
- `Render::set_view` @0x0054d0e0 — pure CPU global install: the active
view's edge count, in-mask (`npnts+1` bits: edges + the CY plane),
vertex/plane base, screen bounds. No GPU state.
- `Render::copy_view` @0x0054dfc0 — appends ONE view: perspective-divided
screen points (drop near-duplicates/collinear within ~1 px; <3
survivors reject; cap 31), bounds, and per-edge WORLD-space planes
(`N = normalize(cross(ray[k+1], ray[k]))` NEXT × CURRENT, corrected
2026-08-30 by the flood-read appendix against Ghidra; an earlier
revision of this line had the operands reversed
`d = dot(N, viewpoint)`). `copy_view(dest, null, n)` = the
full-viewport root quad (count ignored). Full extraction:
`2026-08-30-fw-flood-pseudocode-appendix.md`.
- `PView::GetClip` @0x005a4320 projects the portal polygon
(`xformStart`), reverses winding for NEGATIVE side, and (do_clip)
clips via `ACRender::polyClipFinish` against the installed view. 32
verts assumed. The only CPU polygon clip in the frame.
- `Render::viewconeCheck` @0x0054c250 sphere vs the CY plane + the
active view's edge planes, in viewer-block space. Cull strict
(`d < r`); partial inclusive (`d ≤ r`); publishes
`local_object_center/radius` ALWAYS (even when OUTSIDE). BN's version
of this body is unusable (inverted flags) the model here is
Ghidra-verified.
## 7. Constants and anchors
| Item | Value |
|---|---|
| Outdoor cell test | `(objcell_id & 0xFFFF) < 0x100` |
| Plane/side epsilon `F_EPSILON` | 0.000199999995f (0x3951B717) |
| Punch far-Z | 0.999998987f |
| Panel depth mode | test ALWAYS, write ON (mode bit2), alpha 0 |
| Panel local-XY reject box | ±12.0 |
| Landscape grid | mid_width 11, mid_radius 5, block 192 m, cell 24 m |
| Alpha pressure valve | global `flush` = 0.75 (~2250 meshes) |
| copy_view vertex cap | 31 (0x1F) |
| Flood list growth | +30 (0x1E) |
| BoundingType | 0 OUTSIDE, 1 PARTIALLY_INSIDE, 2 ENTIRELY_INSIDE |
| Sidedness | 0 POSITIVE, 1 NEGATIVE, 2 IN_PLANE (always rejects) |
| Exit-portal sentinel | `other_cell_id == 0xFFFFFFFF` (cell), `0xFFFF` (no-through seed) |
Struct anchors (acclient.h): `PView` {outside_view@0 (view_count@+0x38),
draw_landscape, outdoor_portal_list, cell_draw_list@+0x50,
cell_draw_num@+0x60, cell_todo_list, cell_todo_num, lscape@+0x78};
`CEnvCell.m_DID`@+0x28, `num_view`@+0x134, `portal_view`@+0x138;
`CCellPortal` stride 0x18 {other_cell_id, other_cell_ptr, portal@+8,
portal_side, other_portal_id, exact_match}; `CBldPortal` {portal_side,
other_cell_id, other_portal_id, exact_match, num_stabs, stab_list};
`CLandBlock.in_view`@+0xFC, `CLandCell` stride 0x108 (in_view@+0x104).
## 8. What the traces + code jointly prove about #456
Outdoor frames draw EVERYTHING distance-eligible the far building
first (far-to-near) and correctness is pure depth: nearer terrain,
shells, punches, and look-in cell geometry bury the vista. Interior
frames never roster the far building (exit-view cone culls it) and SEAL
their exit apertures at own depth after drawing the landscape through
them, so nothing drawn later can leak into the aperture. There is no
hiding mechanism to invent; FW3/FW4 reproducing this walk resolves #456
wholesale.
## 9. Verification status
Byte-verified against the raw pseudo-C by hand: the seal loop
(`other_cell_id==0xffffffff → DrawPortalPolyInternal(poly,0)` @0059a49af),
the deferred z-clear (@005a48a9), `DEPTHTEST_ALWAYS` + z-write-from-mode
(@0059be02), punch Z 0.999998987f (@0059bf4b), the fan submit (@0059bf84).
Ghidra-arbitrated where BN mush was flagged: the rooting condition, the
`viewconeCheck` comparisons, the ConstructView sidedness decode, the
DrawCells dead argument. **Re-verify at FW1 before relying on:** the ±12
reject polarity in `DrawPortalPolyInternal` (FP-status idiom), the
`DrawBlock` alpha-flush comparison polarity, and `InsCellTodoList`'s
insert position (LIFO pop order depends on it port it from its body
@0x005a4f50, not from assumption). Resolved non-issue: the trace's `ov`
is the FIELD `outside_view.view_count` (probe read PView+0x38), not
`DrawCells`' dead argument reports 2 and 5 agree once combined.