research(render) Campaign FW0: the frame-walk pseudocode model + decomp appendix

The distilled port-ready model for FW1: camera-cell rooting, the
invisible-panel primitive (punch far-Z / seal own-depth, byte-verified
constants), the far-to-near landscape walk, the building two-pass portal
machinery with its push/pop asymmetry, the interior flood + DrawCells
passes, the view machinery, and the constants/struct anchor table. The
panel question is resolved: retail DOES draw depth-only portal-polygon
panels via DrawPortalPolyInternal - AD-117 re-invented a real mechanism
at the wrong site. FW0 is complete: oracle fixtures, replay helper,
decomp model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 09:28:19 +02:00
parent 9f4c0f95e3
commit a6885aa2f0
2 changed files with 1491 additions and 0 deletions

View file

@ -0,0 +1,273 @@
# 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_i, ray_i+1))`, `d = dot(N, viewpoint)`).
`copy_view(dest, null, 4)` = the full-viewport root quad.
- `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.