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:
parent
9f4c0f95e3
commit
a6885aa2f0
2 changed files with 1491 additions and 0 deletions
1218
docs/research/2026-08-30-fw-walk-pseudocode-appendix.md
Normal file
1218
docs/research/2026-08-30-fw-walk-pseudocode-appendix.md
Normal file
File diff suppressed because it is too large
Load diff
273
docs/research/2026-08-30-fw-walk-pseudocode.md
Normal file
273
docs/research/2026-08-30-fw-walk-pseudocode.md
Normal 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
|
||||
`((row−viewer_b_xoff)·192, (col−viewer_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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue