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

15 KiB
Raw Blame History

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 (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/. 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::DrawDrawNoBlit @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, ClipPortalsAddViewToPortals (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_pushed).

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.