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>
63 KiB
FW1 flood decomp-read appendix - raw extraction reports (2026-08-30)
Second read round: the interior-flood and view-support functions. Same method as the first appendix (BN pseudo-C, Ghidra-arbitrated where flagged).
Report 1 - Flood bookkeeping (InitCell / InsCellTodoList / GetVisible / curr_view_push)
PView::InitCell @0x005a4b70
Summary: Initializes the cell's TOP portal_view slot (portal_view.data[num_view-1]) for the flood: stamps view_timestamp = master_timestamp, clears cell_view_done, grows the per-portal portal_info array to num_portals, classifies every portal as in-view (inflag) or rejected via a cell-local viewpoint-vs-portal-plane side test, computes max_indist = max SQUARED viewpoint distance to any in-view portal vertex, and marks every rejected portal seen=1 so the flood never traverses it. The entry portal (real index; 0xffff sentinel never matches) is forced inflag=1 + seen=1 instead of side-tested.
int PView::InitCell(CEnvCell* cell, uint16 entry_portal_idx) // Ghidra: returns int; BN said void
{
slot = cell->portal_view.data[cell->num_view - 1]; // TOP recursion slot
if (slot->view_count == 0) return 0; // no views on this cell -> nothing to do
Render::positionPush(3, &cell->pos); // install CELL frame: viewpoint below is CELL-LOCAL
slot->cell_view_done = 0;
slot->view_timestamp = PView::master_timestamp;
if (slot->portal.sizeOf < cell->num_portals)
DArray<portal_info>::grow(&slot->portal, cell->num_portals); // exact-size; NEW entries UNINITIALIZED
float max_d2 = 0.0f;
int any_rejected = /*UNINITIALIZED stack dword — see gotchas*/;
for (i = 0; i < cell->num_portals; i++) { // CCellPortal stride 0x18
CPolygon* poly = cell->portals[i].portal;
if (i == entry_portal_idx && slot->portal.data[i].inflag == 0) {
// entered-through portal: forced visible + consumed (0xffff seed sentinel never hits this)
slot->portal.data[i].inflag = 1;
slot->portal.data[i].seen = 1;
} else {
slot->portal.data[i].seen = 0;
// side of cell-local viewpoint vs portal plane (CPolygon.plane @ +0x20)
float d = plane.N.x*vp.x + plane.N.y*vp.y + plane.N.z*vp.z + plane.d;
int side; // 0=POSITIVE, 1=NEGATIVE
if (d > F_EPSILON) side = 0; // F_EPSILON = 0.000199999995f
else if (d < -F_EPSILON) side = 1;
else { slot->portal.data[i].inflag = 0; any_rejected = 1; goto vertex_scan; } // IN_PLANE: always reject
if (side != cell->portals[i].portal_side)
slot->portal.data[i].inflag = 1; // viewer on the see-through side
else { slot->portal.data[i].inflag = 0; any_rejected = 1; } // viewer on the portal's own side
}
vertex_scan:
if (slot->portal.data[i].inflag == 1 && poly->num_pts > 0) // num_pts = byte @ +0xe
for each CVertex* v in poly->vertices[0..num_pts) { // vertices = CVertex** @ +0
d2 = (vp.x-v.x)^2 + (vp.y-v.y)^2 + (vp.z-v.z)^2; // SQUARED, cell-local, no sqrt
if (max_d2 < d2) max_d2 = d2;
}
}
slot->max_indist = max_d2; // max squared distance to any in-view portal vertex
if (any_rejected != 0 && slot->view_count > 0)
for (v = 0; v < slot->view_count; v++) {
Render::set_view(&slot->view, v); // installs global active view; the check below does NOT read it
for (j = 0; j < cell->num_portals; j++)
if (portal[j].inflag == 0 && portal[j].seen == 0)
portal[j].seen = 1; // rejected portals become 'consumed': flood never walks them
}
slot->update_count = slot->view_count;
Render::positionPop();
return 1;
}
Gotchas: BN body @0x005a4b70 is UNUSABLE: it scrambled the x87 side-test control flow AND elided the squared-distance math (showed only the z subtraction). This model is Ghidra-verified (127.0.0.1:8081). Confirmed semantics: side==portal_side rejects, IN_PLANE (|d|<=0.000199999995f) always rejects — matches the FW doc's sidedness table. Real retail quirks: (1) any_rejected (local_4) is NEVER initialized — if no portal is rejected it reads stack garbage; the effect is benign (the fixup inner body no-ops when nothing was rejected; only side effect is redundant set_view churn), so a port should init it to 0 with identical observable behavior. (2) The entry-portal branch is guarded by the STALE inflag (inflag==0) — on a freshly grown portal array inflag is heap garbage (DArray::grow does NOT zero new entries); the caller (AddViewToPortals) presumably establishes it — verify that contract when porting FW3. (3) set_view inside the fixup loop installs each view globally but nothing in the loop consults it, and the LAST view stays installed on exit — both decompilers agree; purpose unclear (possibly vestigial). (4) positionPush(3, cell->pos) means the plane test and max_indist run in CELL-LOCAL coordinates. (5) 0xffff sentinel: ushort zero-extended vs uint loop index — never matches, so the seed cell side-tests every portal. (6) max_indist is a SQUARED distance — todo-list keys fed from it are squared; comparisons stay consistent. Ghidra return type is int (0 = early-out on view_count==0, 1 = did work); BN said void __stdcall.
PView::InsCellTodoList @0x005a4f50
Summary: Sorted insert into the flood todo list. Keeps cell_todo_list ordered NON-INCREASING by float distance from index 0: index 0 = FARTHEST, END = NEAREST. The new entry sinks toward index 0 past every entry with dist <= its own and stops under the first STRICTLY greater one. Since the flood pops from the END, pop order is SMALLEST-key-first (nearest-first traversal); ties pop FIFO (older equal entries first). 8-byte {cell,dist} nodes are lazily allocated once and recycled across floods; grow is batched +30 with explicit zero-fill of new slots.
void PView::InsCellTodoList(CEnvCell* cell, float dist)
{
n = this->cell_todo_num;
if (n >= cell_todo_list.sizeOf) {
DArray<CellListType*>::grow(&cell_todo_list, n + 0x1e); // +30; grow sets sizeOf to EXACTLY n+30
for (k = this->cell_todo_num; k < cell_todo_list.sizeOf; k++)
cell_todo_list.data[k] = 0; // zero-fill ALL new slots (lazy-node contract)
}
if (cell_todo_list.data[n] == 0)
cell_todo_list.data[n] = new CellListType{cell=0, dist=0}; // 8 bytes; alloc-once, recycled, never freed here
node = cell_todo_list.data[n];
node->cell = cell; node->dist = dist;
pos = n;
while (pos > 0) {
prev = cell_todo_list.data[pos - 1];
if (dist < prev->dist) break; // STRICT less stops the sink [GHIDRA polarity — BN was INVERTED]
cell_todo_list.data[pos] = prev; // shift the <=-dist entry toward the END
pos--;
}
cell_todo_list.data[pos] = node;
this->cell_todo_num++;
}
Gotchas: LOAD-BEARING BN INVERSION: BN's x87 flag mush (test ah,0x5) read as shift-while-LESS, which would produce an ascending list and a farthest-first pop. Ghidra (authoritative, verified live) shows if (dist < prev->dist) break — shift while dist >= prev->dist — descending list, END = nearest, pop-from-END = NEAREST-first flood. This reconciles exactly with DrawCells walking cell_draw_list from the end for far-to-near: cells append to the draw list in pop order (near->far), so end-first walk = far-to-near. Ties: equal-dist predecessors get shifted (>= shifts), so the newcomer lands closer to index 0 and pops AFTER existing equals — FIFO among ties (seed at dist 0 always pops first). No dedup: the same cell can be enqueued multiple times; dedup is the caller's job (ConstructView/AddViewToPortals). Nodes are pointer-recycled and permuted by the shift, never freed; the explicit zero-fill after grow is what makes the data[n]==0 lazy-alloc test sound (DArray::grow itself does NOT zero and sets sizeOf to exactly the requested count — the +30 batching is caller-side).
CEnvCell::GetVisible @0x0052dc10
Summary: Static resident-cell registry lookup: walks the bucket chain of CEnvCell::visible_cell_table (intrusive hash keyed by cell id, bucket = (uint64)cell_id % numBuckets) and returns the CEnvCell* whose node key matches, or null if the cell is not currently loaded/visible. Node layout {key @+0, next @+4, CEnvCell* value @+8}.
static CEnvCell* CEnvCell::GetVisible(uint32 cell_id)
{
for (node = visible_cell_table.m_intrusiveTable.m_buckets[(uint64)cell_id % m_numBuckets];
node != 0; node = node->next) // next @ +4
if (node->key == cell_id) // key @ +0
return node->value; // CEnvCell* @ +8
return 0;
}
Gotchas: Trivial and unambiguous; both decompilers agree. BN's COMBINE(0, arg1) is just unsigned zero-extension of the 32-bit key for the modulo. The redundant post-match null re-check in both decomps (BN 'if (i != 0)' / Ghidra's second null test) is decompiler noise from the shared return path — one lookup, no side effects.
CEnvCell::curr_view_push @0x005a5090
Summary: Pushes one view-recursion level onto the cell: ensures portal_view has a slot at index num_view (exact-size grow to num_view+1, nulling just the ONE new slot), lazily allocates a 0x48-byte portal_view_type on first use (three empty DArrays with blocksize 0x80: portal_info list, view.poly, view.vertex; view_timestamp=0), then unconditionally resets EXACTLY three counters — view_count=0, update_count=0, view_timestamp=0 — and increments num_view. Slots are recycled across frames (never freed here); their DArrays keep capacity.
void CEnvCell::curr_view_push()
{
if (num_view >= portal_view.sizeOf) {
DArray<portal_view_type*>::grow(&portal_view, num_view + 1); // grow sets sizeOf EXACTLY num_view+1
portal_view.data[num_view] = 0; // null only the single new slot
}
if (portal_view.data[num_view] == 0) { // lazy first-use alloc
s = operator new(0x48);
if (s) {
s->portal = {data=0, blocksize=0x80, next_available=0, sizeOf=0}; // DArray<portal_info>
s->view.poly = {data=0, blocksize=0x80, next_available=0, sizeOf=0};
s->view.vertex = {data=0, blocksize=0x80, next_available=0, sizeOf=0};
s->view_timestamp = 0;
// NOT initialized: view.vertex_count_total, max_indist, view_count, cell_view_done, update_count
}
portal_view.data[num_view] = s;
}
slot = portal_view.data[num_view];
slot->view_count = 0;
slot->update_count = 0;
slot->view_timestamp = 0;
num_view += 1;
}
Gotchas: VERIFIES the earlier read: num_view++ with 0x48-byte lazy slot alloc + counter resets — with precision: exactly view_count/update_count/view_timestamp are reset on EVERY push (fresh or recycled); cell_view_done and max_indist are NOT reset here and stay stale (heap garbage on a brand-new slot) until PView::InitCell writes them — InitCell always runs before they are consumed, but a port must preserve that ordering or zero them harmlessly. The single-slot null after grow is sound only because this DArray grow(n) sets sizeOf to EXACTLY n (verified @0x005a45d0: copies old, sizeOf=arg; blocksize unused by grow; grow with arg<=sizeOf delegates to shrink) — no hidden capacity slack, so no garbage slots. portal_view_type layout confirmed in acclient.h: {DArray<portal_info> portal @0; view_type view @0x10 (vertex_count_total, poly@0x14, vertex@0x24); max_indist @0x34; view_count @0x38; cell_view_done @0x3c; view_timestamp @0x40; update_count @0x44}; DArray = {data, blocksize, next_available, sizeOf}.
Report notes: All four bodies cross-checked against live Ghidra MCP (http://127.0.0.1:8081, patchmem.gpr) — mandatory here, because BN got two of them materially wrong: (1) InsCellTodoList's insertion comparison was polarity-INVERTED in BN (would have modeled a farthest-first pop); Ghidra's strict dist < prev->dist break gives a descending-from-index-0 list whose END-pop is NEAREST-first, which is what makes the draw list come out near->far and DrawCells' end-first walk far-to-near — consistent with FW doc section 5. (2) InitCell's BN body scrambled the plane-side branches and elided the dx^2+dy^2+dz^2 accumulation entirely (showed a bare z subtraction). Ghidra-confirmed model: side 0/1 vs portal_side rejects on equality, IN_PLANE always rejects (matches the doc's section 7 sidedness table), max_indist = max SQUARED cell-local distance to in-view portal vertices, and rejected portals get seen=1 in a fixup pass whose per-view set_view calls are side-effect-only. Two genuine retail quirks worth register-awareness if ported observably: InitCell's any_rejected flag is an uninitialized stack read (benign in effect), and the entry-portal force-visible branch keys off STALE inflag whose state is a caller contract (read PView::AddViewToPortals before relying on it in FW3). Struct authorities verified in acclient.h: portal_info {seen, inflag}; portal_view_type (0x48 bytes, field offsets in the curr_view_push gotchas); PView {outside_view, draw_landscape, outdoor_portal_list, cell_draw_list, cell_draw_num, cell_todo_list, cell_todo_num, lscape}; CellListType nodes are 8-byte {CEnvCell* cell, float dist}; CPolygon {vertices@0, num_pts byte@0xe, plane@0x20}. Sources: docs/research/named-retail/acclient_2013_pseudo_c.txt lines 311378-311393, 432896-433045, 433183-433243, 433279-433320; DArray grow/shrink @0x005a45d0 region; struct defs in docs/research/named-retail/acclient.h (portal_info @32458, portal_view_type @32346, view_type @32338, PView @45934, CPolygon @31855).
Report 2 - Flood propagation (ClipPortals / AddViewToPortals)
PView::ClipPortals @0x005a5520
Summary: Per popped flood cell: pass 1 scans the cell's top portal_view flags (seen && inflag!=1) and lazily resolves neighbor pointers via CEnvCell::GetVisible; returns 0 if no portal is live. Pass 2, in the cell's frame, installs each view in [start_view, view_count) and clips every live portal polygon (GetClip, do_clip=1). Survivors are appended to the neighbor's top portal_view (double-clipped via OtherPortalClip when exact_match==0 && other_portal_id>=0), or into this->outside_view for portals leading outdoors. Returns 1 when any portal was live.
int PView::ClipPortals(CEnvCell* cell, uint start_view /*first view index to process; 0 from ConstructView@005a5845, top->update_count from AdjustCellView@005a5796*/)
{
portal_view_type* top = cell->portal_view.data[cell->num_view - 1]; // cell's current (top) view slot
Render::PortalList = top; // GLOBAL set unconditionally, before any early-out
int any_live = 0;
if ((int)cell->num_portals <= 0) return 0; // signed
// ---- pass 1: which portals are live; resolve+cache neighbor pointers ----
for (int j = 0; j < (int)cell->num_portals; j++) { // CCellPortal stride 0x18
portal_info* pi = &top->portal.data[j]; // {seen@+0, inflag@+4}
if (pi->seen != 0 && pi->inflag != 1) {
CCellPortal* p = &cell->portals[j];
if (p->other_cell_ptr == null && p->other_cell_id != 0xFFFFFFFF) {
p->other_cell_ptr = CEnvCell::GetVisible(p->other_cell_id); // cached into the portal record
if (p->other_cell_ptr == null) continue; // neighbor not visible/loaded => not live
}
any_live = 1; // live: ptr already set, OR id==0xFFFFFFFF (outdoors), OR GetVisible succeeded
}
}
if (!any_live) return 0;
// ---- pass 2: clip every live portal against each view in the window ----
Render::positionPush(3, &cell->pos); // enter the cell's frame
for (uint i = start_view; (int)i < (int)top->view_count; i++) { // signed compare
Render::set_view(&top->view, i); // install view i (CPU globals only)
for (int j = 0; j < (int)cell->num_portals; j++) {
portal_info* pi = &top->portal.data[j];
if (pi->seen == 0 || pi->inflag == 1) continue;
CCellPortal* p = &cell->portals[j];
uint n;
PView::GetClip(this, (Sidedness)p->portal_side, p->portal, clip_view /*global out buffer*/, &n, 1 /*do_clip*/);
if (n == 0) continue; // portal fully clipped away in this view
if (p->other_cell_id == 0xFFFFFFFF) { // ---- portal leads OUTDOORS ----
if (this->draw_landscape != 0) {
if (cliplandscape != 0)
Render::copy_view(&this->outside_view, clip_view, n); // exit view = the clipped portal shape
else
Render::copy_view(&this->outside_view, null, 0); // null-src marker view (see gotchas)
}
} else if (p->other_cell_ptr != null) { // ---- portal into a resolved neighbor ----
if (p->exact_match == 0 && (int)p->other_portal_id >= 0) { // far poly differs => double clip
if (PView::OtherPortalClip(this, p, clip_view, &n) == 0) {
Render::set_view(&top->view, i); // restore after far-frame excursion; nothing survived
continue;
}
Render::set_view(&top->view, i); // restore; clip_view/n now the doubly-clipped poly
}
CEnvCell* nb = p->other_cell_ptr;
if (nb->num_view != 0) // neighbor must have a pushed view slot (add_views/stab_list)
Render::copy_view(nb->portal_view.data[nb->num_view - 1], clip_view, n); // append to NEIGHBOR top view
}
// else: id != 0xFFFFFFFF and neighbor unresolved => clipped view silently dropped
}
}
Render::positionPop();
return 1; // 1 = some portal was live, even if the view window [start_view, view_count) was empty
}
Gotchas: BN's body is type-mushed garbage here (esi_2->vtable stands for BOTH top->portal.data and the view_count loop bound; m_timeStamp = top->view @+0x10) — this model is Ghidra-arbitrated (127.0.0.1:8081, matches doc §9 practice). Render::PortalList is set BEFORE the early-outs, so the global still points at this cell's top view even on return 0. The cliplandscape==0 arm calls copy_view(&outside_view, null, 0) — doc §6 documents copy_view(dest,null,4)=full-viewport quad; whether count 0 vs 4 matters must be verified in Render::copy_view's body before porting that arm. Loop/window compares are SIGNED ints. Ghidra names the pop positionPop, BN framePop — same paired call. draw_landscape==0 discards exit-portal views entirely (outside_view never raised).
PView::OtherPortalClip @0x005a5400
Summary: The double-clip helper for non-exact_match portals (the task's ~0x005a5495 temp_view + copy_view site). Snapshots the near-clipped polygon into a lazily-initialized function-static temp_view (view 0), enters the FAR cell's frame, installs that snapshot as the active view, and re-clips against the far cell's own matching portal polygon (portals[other_portal_id]) with INVERTED sidedness (pass 1 iff far portal_side==0). Writes the result back through clip_view/count; returns count!=0.
int PView::OtherPortalClip(CCellPortal* p, Vec2Dscreen** clip_view, int* n)
{
static portal_view_type temp_view; // guard-bit lazy init ($S225): all 3 DArrays {data=0,sizeOf=0,next_available=0,blocksize=0x80}, view_timestamp=0, atexit dtor
temp_view.view_count = 0; // reset EVERY call
if (Render::copy_view(&temp_view, clip_view, *n) == 0) // snapshot the near-clipped poly as temp view 0
return 0; // rejected (<3 survivors per copy_view rules)
int opid = p->other_portal_id;
CCellPortal* far = &p->other_cell_ptr->portals[opid]; // far cell's OWN portal entry
Render::positionPush(3, &p->other_cell_ptr->pos); // enter the FAR cell's frame
Render::set_view(&temp_view.view, 0); // install the snapshot as the active view
PView::GetClip(this, (Sidedness)(far->portal_side == 0 ? 1 : 0), // INVERTED vs the far cell's declared side
far->portal, clip_view, n, 1 /*do_clip*/); // result written back through clip_view/n
Render::positionPop();
return *n != 0;
}
Gotchas: The sidedness inversion is a literal == 0 test, NOT a 0<->1 swap: a far portal_side of 2 (IN_PLANE) would pass POSITIVE(0) — in-data values should only be 0/1, but don't port it as XOR. temp_view is one shared static (single-threaded renderer; not reentrant). BN's int80_t return / result from copy_view is FPU-tracking mush — Ghidra confirms the return is exactly (*n != 0), with an early return 0 when the snapshot copy_view fails. Only called when exact_match==0 && other_portal_id>=0, so far indexing is safe.
PView::AddViewToPortals @0x005a52d0
Summary: Runs after ClipPortals(cell,...) returned 1: schedules neighbors that just received views. For each portal with a resolved neighbor, live source flags (seen && inflag!=1), and a nonempty neighbor top view: first touch (update_count==0) => InitCell + InsCellTodoList keyed by the neighbor top view's max_indist (+0x34); duplicate reach with NEW views (update_count!=view_count) => AddToCell, plus FixCellList re-place/re-clip when the neighbor was already marked cell_view_done, then update_count=view_count; no new views => skip entirely. Finally SetOtherSeen marks the neighbor's back-portal when other_portal_id>=0.
void PView::AddViewToPortals(CEnvCell* cell)
{
for (uint j = 0; j < cell->num_portals; j++) { // unsigned compare; CCellPortal stride 0x18
CCellPortal* p = &cell->portals[j];
CEnvCell* nb = p->other_cell_ptr; // null for outdoor portals + unresolved neighbors
portal_info* pi = &cell->portal_view.data[cell->num_view - 1]->portal.data[j]; // SOURCE cell top-view flags (re-read each iteration)
if (nb == null || pi->inflag == 1 || pi->seen == 0 || nb->num_view == 0)
continue;
portal_view_type* nbtop = nb->portal_view.data[nb->num_view - 1]; // neighbor's top view slot
if (nbtop->view_count == 0) continue; // ClipPortals copied nothing in => nothing to schedule
if (nbtop->update_count == 0) {
// ---- first touch this flood: schedule the neighbor ----
if (PView::InitCell(this, nb, (uint16)p->other_portal_id) != 0) // ZERO-EXTENDED 16-bit read of the id
PView::InsCellTodoList(this, nb, nbtop->max_indist); // todo distance key = neighbor top view's max_indist (float @+0x34)
// update_count NOT set here (InitCell's business); fall through to SetOtherSeen
} else if (nbtop->update_count != nbtop->view_count) {
// ---- duplicate reach (2nd+ portal into nb) WITH new views since last processed ----
PView::AddToCell(this, nb, (uint16)p->other_portal_id);
if (nbtop->cell_view_done != 0) // nb already popped+processed by ConstructView
PView::FixCellList(this, nb, cell); // = AdjustCellPlace(nb, cell) + AdjustCellView(nb);
// AdjustCellView@005a5770 re-runs ClipPortals(nb, nbtop->update_count)
// then AddViewToPortals(nb) => recursive incremental re-flood
nbtop->update_count = nbtop->view_count; // fresh re-read AFTER FixCellList returns (both arms)
} else {
continue; // update_count == view_count: nothing new; NO SetOtherSeen either (goto loop end)
}
if ((int)p->other_portal_id >= 0) // FULL-WIDTH SIGNED test (sentinel -1 skips)
PView::SetOtherSeen(this, cell, j);
// SetOtherSeen@005a4e30 (verified): q = &nb->portals[other_portal_id];
// if (q->other_cell_ptr == null) q->other_cell_ptr = cell; // backlink fill
// bp = &nbtop->portal.data[other_portal_id]; if (bp->inflag != 0) bp->seen = 1; // mark back-portal seen
}
}
Gotchas: TASK CORRECTION: outside_view is raised in ClipPortals, not here — outdoor portals keep other_cell_ptr null forever (GetVisible is never called for the 0xFFFFFFFF sentinel), so the nb==null gate skips them. The InitCell/AddToCell argument is a zero-extended 16-bit read of other_portal_id (both decompilers agree): id -1 => 0xFFFF, which IS ConstructView's no-through seed sentinel — coherent, but note InitCell/AddToCell DO run for such portals while SetOtherSeen (full signed int test) is skipped. In the InitCell branch, SetOtherSeen fires even when InitCell returned 0. update_count = view_count is a fresh read AFTER the FixCellList recursion returns — a portal cycle that adds views to nb during that recursion gets marked consumed (retail behavior; do not 'fix'). BN's loop-pointer prints (arg2 = &arg2->m_pNext, uint16 reads) are byte-offset-accumulator artifacts; Ghidra's _padding_ expressions are equally mushed — the field decode above is offset-verified against acclient.h (other_cell_ptr +4, other_portal_id +0x10, stride 0x18; portal_view_type max_indist +0x34, view_count +0x38, cell_view_done +0x3C, update_count +0x44).
Report notes: Extraction sources: docs/research/named-retail/acclient_2013_pseudo_c.txt lines 433446-433737 (BN), cross-arbitrated function-by-function against live Ghidra MCP (127.0.0.1:8081) — the BN body of ClipPortals is type-confused throughout and MUST NOT be used directly; Ghidra's decomp was clean and is the authority for all three bodies, with struct offsets independently verified against docs/research/named-retail/acclient.h (portal_view_type /* 3415 /, CCellPortal / 3406 /, portal_info / 3407 /, CEnvCell / 3405 /, PView / 4912 / with outside_view at offset 0). Caller contract (completes doc §5): ConstructView@0x005a57b0 pops a todo cell, appends it to cell_draw_list (grow +30), sets top->cell_view_done=1, then ClipPortals(cell, 0) and — only on return 1 — AddViewToPortals(cell). The second ClipPortals call site is AdjustCellView@0x005a5770 (reached via FixCellList@0x005a5250 = AdjustCellPlace + AdjustCellView), which passes start_view = top->update_count, i.e. arg3 is the first-unprocessed-view index and the update_count/view_count pair is a consumed-views watermark: that is the whole duplicate-reach mechanism — a cell reached through a second portal only re-clips the view window [update_count, view_count). clip_view is a global Vec2Dscreen* scratch buffer shared by GetClip; Render::PortalList is a global that GetClip/DrawCells consume. The seen/inflag ORIGIN (who first sets them per view) is not in these bodies — it belongs to InitCell/AddToCell/GetClip extraction; here they are only read (gate: seen!=0 && inflag!=1) and seen is set by SetOtherSeen on the neighbor's back-portal, gated on that back-portal's inflag!=0. Open verify-before-port items: Render::copy_view(dest, null, 0) semantics in the cliplandscape==0 arm (doc §6 only documents count=4), and the far-side sidedness ==0 test if any data ever carries portal_side 2.
Report 3 - View-clip support (copy_view / polyClipFinish / xformStart)
Render::copy_view @0x0054dfc0
Summary: Appends ONE polygon view slot to a portal_view_type: perspective-divides homogeneous screen points in place, marks survivors by dropping ~1px duplicates and collinear points (three closing wrap checks included), rejects <3 survivors (returns 0, view_count untouched), caps at 31, stores pt list + closing duplicate, computes screen bounds, then per-edge WORLD planes from unprojected rays (live path: ScreenToViewTransform; N=cross(ray[k+1],ray[k]) normalized unless degenerate; d=-dot(N,viewer_world_space.viewpoint)). pts==null builds the full-viewport root quad. Returns 1 on success.
int copy_view(portal_view_type* dest, Vec2Dscreen** pts, uint npts) // Ghidra-verified; returns int (BN showed void)
vc = dest->view_count
vbase = (vc == 0) ? 0 : dest->view.vertex_count_total // FIRST view resets the vertex pool
if (vc >= view.poly.sizeOf) DArray<view_poly>::grow(&view.poly, vc + 0x10)
if (pts == null) { // ---- full-viewport root quad path
n = 4; view.vertex_count_total = vbase + 5
if (vbase+5 >= view.vertex.sizeOf) DArray<view_vertex>::grow(&view.vertex, vbase + 0x15)
poly[vc] = { vertex_count: 4, vertex_index: vbase }
v = &view.vertex.data[vbase] // W/H = render_device->m_viewportWidth/Height (uint->float)
v[0].pt=(0,H); v[1].pt=(W,H); v[2].pt=(W,0); v[3].pt=(0,0); v[4].pt=(0,H) // closing dup
goto bounds
}
// ---- survivor marking
keep[0]=1; n=1; last=0; stl=0 /*second-to-last kept corner*/; second=0 /*idx of 2nd kept (local_220)*/
for (i = 0; i < npts; i++) {
p = pts[i]
if (p->w != 1.0) { p->x /= p->w; p->y /= p->w; p->w = 1.0 } // perspective divide IN PLACE (mutates caller's points)
if (i == 0) continue
distinct = |pts[i].x - pts[last].x| > 1.0 || |pts[i].y - pts[last].y| > 1.0 // strict >, vs LAST KEPT
keep[i] = distinct
if (!distinct) continue // ~1px duplicate dropped; last unchanged
if (this is only the 2nd kept point) { n++; second = i }
else {
pp = pts[stl]; prev = pts[last]; cur = pts[i]
span = max(|pp.x - cur.x|, |pp.y - cur.y|) // Chebyshev chord length
cross = (pp.x - prev.x)*(prev.y - cur.y) - (pp.y - prev.y)*(prev.x - cur.x)
if (|cross| >= span) { n++; stl = last } // prev is a genuine corner (~1px deviation test)
else { keep[last] = 0; if (second == last) second = i } // prev collinear: un-keep it; n unchanged (prev out, cur in)
}
last = i
}
// ---- closing wrap checks against pts[0]
first = pts[0]
distinct = |first.x - pts[last].x| > 1.0 || |first.y - pts[last].y| > 1.0
keep[last] = distinct
if (!distinct) { n--; last = stl } // last duplicates first: drop it
else {
span = max(|pts[stl].x - first.x|, |pts[stl].y - first.y|)
cross = (pts[stl].x - pts[last].x)*(pts[last].y - first.y) - (pts[last].x - first.x)*(pts[stl].y - pts[last].y)
if (|cross| < span) { keep[last] = 0; n--; last = stl } // last collinear on stl->first
}
stl = last
if (second > 0) { // is point 0 itself collinear between stl and second?
span = max(|pts[stl].x - pts[second].x|, |pts[stl].y - pts[second].y|)
cross = (first.y - pts[second].y)*(pts[stl].x - first.x) - (first.x - pts[second].x)*(pts[stl].y - first.y)
if (|cross| < span) { n--; keep[0] = 0 }
}
if (n < 3) return 0 // REJECT: nothing written, view_count NOT bumped
if (n > 0x1f) n = 0x1f // cap 31
view.vertex_count_total = vbase + n + 1
if (>= view.vertex.sizeOf) DArray<view_vertex>::grow(&view.vertex, vbase + n + 0x11)
poly[vc] = { vertex_count: n, vertex_index: vbase }
v = &view.vertex.data[vbase]; j = 0
for (i = 0; i < npts; i++)
if (keep[i]) { v[j].pt.x = fabs(pts[i].x); v[j].pt.y = fabs(pts[i].y); j++ } // REAL fabs (Ghidra-confirmed)
v[n].pt = v[0].pt // closing duplicate vertex (its plane slot never written)
bounds: // over v[0..n-1] only
xmin=xmax=v[n-1].pt.x; ymin=ymax=v[n-1].pt.y
for (k = n-2; k >= 0; k--) {
if (v[k].x < xmin) xmin=v[k].x; else if (v[k].x > xmax) xmax=v[k].x
if (v[k].y < ymin) ymin=v[k].y; else if (v[k].y > ymax) ymax=v[k].y
}
poly[vc].{xmin,xmax,ymin,ymax} = ...
// ---- per-edge world planes via unprojected rays (stack array ray[0..n], stride 0xc)
if (newmethod == 1) // newmethod statically = 1: this is the LIVE path
for (k = n-1; k >= 0; k--) PrimD3DRender::ScreenToViewTransform(&ray[k], v[k].pt.x, v[k].pt.y) // world ray dir @0x0059aa40
else // DEAD legacy software fork
for (k = n-1; k >= 0; k--) {
u = v[k].pt.x * Render::xinvscale - Render::tx
w = v[k].pt.y * Render::yinvscale - Render::ty
ray[k] = Render::Xaxis*u + Render::Yaxis*Render::vdst - Render::Zaxis*w
}
ray[n] = ray[0] // closing duplicate ray
for (k = n-1; k >= 0; k--) {
N = cross(ray[k+1], ray[k]) // NOTE ORDER: next x current
if (|N.x| >= F_EPSILON || |N.y| >= F_EPSILON || |N.z| >= F_EPSILON) // F_EPSILON = 0.000199999995f
N *= 1.0f / sqrt(N.x^2 + N.y^2 + N.z^2) // else left tiny/unnormalized (degenerate edge, no reject)
v[k].plane.N = N // view_vertex = {Vec2D pt; Plane plane} stride 0x18
v[k].plane.d = -dot(N, Render::viewer_world_space.viewpoint) // edge k = screen verts k -> k+1
}
dest->view_count += 1
return 1
Gotchas: BN body is heavy x87-flag mush; every branch here is Ghidra-cross-checked (8081/patchmem). (1) MUTATES the caller's points (in-place perspective divide). (2) The vertex copy applies REAL fabs to x,y — harmless post-clip (coords in-viewport up to rounding) but real. (3) Cap-31 quirk: if >31 survive, the copy loop still writes ALL survivors, then the closing dup overwrites index 31 — corrupt polygon; unreachable from the <=32-vert callers but not guarded. (4) Plane N = cross(ray[k+1], ray[k]) — the fw-walk doc §6 summary says cross(ray_i, ray_i+1), i.e. NEGATED; decomp order is next-x-current. (5) The corner-keep boundary is |cross| >= span per Ghidra (consistent at all 3 sites); BN rendered strictness ambiguously. (6) newmethod is statically 1 with no other write found — ScreenToViewTransform is the live unproject; the Xaxis/vdst fork is dead. (7) Grow checks are '>= sizeOf' with +0x10 slack. (8) Returns int 0/1 (BN showed void); reject leaves dest completely untouched.
ACRender::polyClipFinish @0x006b6d00
Summary: Sutherland-Hodgman clip of a homogeneous screen polygon (array of Vec2Dscreen pointers) against the INSTALLED view: first the w >= cdstW plane (cdstW = 0.000199999995f), then each edge of Render::portal_vertex[0..portal_npnts-1] whose inmask bit is clear (all 3 retail callers pass inmask=0 — clip every edge). Each pass scans input in REVERSE; pointer lists ping-pong between static tempPtPBuf and pts_out; a final reverse-copy restores the ORIGINAL winding. Any stage dropping below 3 vertices returns without writing *npts_out (callers pre-zero it).
void polyClipFinish(Vec2Dscreen** pts_in, int npts_in, Vec2Dscreen** pts_out, int* npts_out, int inmask)
// Installed view: Render::portal_vertex (view_vertex[] {pt, plane}, stride 0x18), Render::portal_npnts.
// Statics: tempPtBuf = Vec2Dscreen pool for intersections (stride 0x10), tempPtPBuf @0x009053d0 = pointer list.
mask = inmask << (0x1e - portal_npnts) // aligns mask bit `npnts` at bit 30; per-edge <<1 exposes it at sign bit
parity = 0 // pass parity; dstsel = { [0]: pts_out, [1]: tempPtPBuf }
freept = tempPtBuf
cur_pts = pts_in; cur_n = npts_in
// ---- pass 0: w-clip, only if some w < cdstW (scan last-to-first)
if (exists i: pts_in[i]->w < cdstW) { // cdstW = 0.000199999995f (set @0x007247d5)
parity = 1; out = base = tempPtPBuf
prev = pts_in[0]; sprev = prev->w - cdstW; inprev = (sprev >= 0)
for (i = npts_in-1; i >= 0; i--) { // REVERSE traversal; wrap pair (pts[0], pts[n-1]) first
cur = pts_in[i]; s = cur->w - cdstW; incur = (s >= 0) // INSIDE = w >= cdstW
if (inprev != incur) {
t = sprev / (sprev - s) // lands exactly on w = cdstW
*freept = prev + (cur - prev)*t // ALL FOUR components x,y,z,w lerped
*out++ = freept++
}
if (incur) *out++ = cur
prev = cur; sprev = s; inprev = incur
}
cur_n = out - tempPtPBuf
if (cur_n < 3) return // *npts_out NOT written
cur_pts = tempPtPBuf
}
// ---- edge passes, edges iterated LAST-to-FIRST as pairs (a,b):
// (v[0], v[npnts-1]), (v[npnts-1], v[npnts-2]), ..., (v[1], v[0])
a = portal_vertex[0]
for (b = &portal_vertex[npnts-1]; b >= &portal_vertex[0]; a = b, b--) {
mask <<= 1
if (mask < 0 /*sign bit set*/) continue // inmask bit SET = poly already inside this edge: skip
parity ^= 1; out = base = dstsel[parity]
ex = b->pt.x - a->pt.x; ey = b->pt.y - a->pt.y // screen-space edge direction
side(p) = (p->x - a->pt.x * p->w) * ey - (p->y - a->pt.y * p->w) * ex // homogeneous 2D cross; w>0 preserves sign
prev = cur_pts[0]; s0 = side(prev); sprev = s0; inprev = (s0 <= 0) // INSIDE = side <= 0 (Ghidra-verified)
for (i = cur_n-1; i >= 0; i--) { // REVERSE traversal again
cur = cur_pts[i]
s = (i != 0) ? side(cur) : s0 // final pair (prev=cur_pts[1], cur=cur_pts[0]) reuses point 0's side
incur = (s <= 0)
if (inprev != incur) {
t = sprev / (sprev - s)
*freept = prev + (cur - prev)*t // 4-component homogeneous lerp
*out++ = freept++
}
if (incur) *out++ = cur
prev = cur; sprev = s; inprev = incur
}
cur_n = out - base
if (cur_n < 3) return // *npts_out NOT written
cur_pts = base // just-written buffer becomes next pass's input
}
*npts_out = cur_n
if (parity != 0) // final data sits in tempPtPBuf, not pts_out
for (p = out-1; p >= base; p--) *pts_out++ = *p // reverse-copy into pts_out
// NET ORDERING: each pass reverses order (reverse scan); (#passes + conditional copy) is always even,
// so pts_out ends with the ORIGINAL input winding.
Gotchas: Ghidra-verified against BN's flag mush. (1) The w-plane is w >= cdstW = 0.000199999995f, NOT w >= 0 — BN's 'vs 0f' is fsub-cdstW rendering. (2) Edge INSIDE = side <= 0 (verified sensible: interior of the root full-viewport quad (0,H),(W,H),(W,0),(0,0) yields negative sides); BN's literal rendering inverts this — trust Ghidra. (3) Early <3 returns NEVER write *npts_out — all 3 callers (GetClip @0x005a43b2/0x005a4414, DrawPortalPolyInternal @0x0059bdb0) pre-zero it; a port must preserve or document that contract. (4) inmask bit mapping: iteration k tests original bit npnts-k+1; bit npnts = wrap edge (v[0],v[npnts-1]) pair, bit 1 = (v[1],v[0]); bit 0 (the CY-plane bit of set_view's npnts+1-bit in-mask) is NEVER tested here — but all callers pass inmask=0 anyway, so every edge always clips and the skip path is dead in practice. (5) Zero-pass case (all bits set + no w<cdstW) would set *npts_out=npts_in with pts_out never filled — unreachable with inmask=0. (6) Static tempPtBuf/tempPtPBuf have NO bounds checks; callers assume <=32 verts (GetClip's reversal stack array is exactly 32 pointers). (7) Not reentrant (static buffers).
PrimD3DRender::xformStart @0x0059b990
Summary: Transforms one object-space Vector3 into a slot of the 100-entry static Vec2Dscreen ring buffer and returns the slot. toScreen!=0: object->clip via GetObjectMatrix()WorldToViewViewToClip, then viewport-scales WITHOUT perspective divide — x=(bw/2)(x_clip+w), y=(bh/2)(w-y_clip) (y flipped, origin top-left), z=raw clip z, w=raw clip w. toScreen==0: object->view; raw view-space x,y stored, z/w from the affine transform (w=1). The divide happens downstream (copy_view divides; polyClipFinish clips pre-divide homogeneously).
Vec2Dscreen* xformStart(Vector3* p_obj, int toScreen)
M_ov = D3DXMatrixMultiply(Render::GetObjectMatrix(), render_device->m_GState.WorldToViewMatrix) // object->view (row-vector conv.)
M_oc = D3DXMatrixMultiply(M_ov, render_device->m_GState.ViewToClipMatrix) // object->clip
M = toScreen ? M_oc : M_ov // BOTH products computed unconditionally, EVERY call
v4 = D3DXVec3Transform((p_obj,1), M) // homogeneous (x,y,z,w)
slot = &tmpScreenBuffer[pt_to_use] // static ring @0x00871360, stride 0x10
pt_to_use += 1; if (pt_to_use == 100) pt_to_use = 0
if (toScreen == 0) { slot->x = v4.x; slot->y = v4.y } // raw VIEW-space x,y
else {
slot->x = v4.x*Render::bw*0.5 + v4.w*Render::bw*0.5 // = (bw/2)*(x_clip + w): viewport-scaled, NO divide
slot->y = v4.w*Render::bh*0.5 - v4.y*Render::bh*0.5 // = (bh/2)*(w - y_clip): y-flip, 0 = top
}
slot->z = v4.z // raw clip z (or view z); never viewport-mapped here
slot->w = v4.w // raw clip w (view path: w = 1, affine)
return slot
Gotchas: (1) NO perspective divide — output is homogeneous 'screen * w' coordinates; dividing x,y by w yields pixels in [0,bw]x[0,bh]. copy_view performs the divide (its w != 1.0 check is exactly this contract); polyClipFinish consumes them undivided via the (p.x - a.x*p.w) homogeneous side test and clips w at cdstW. (2) Recomputes BOTH matrix concatenations per vertex, even the unused one — a per-vertex cost quirk, do not 'optimize into' different rounding without noting it. (3) 100-slot static ring: >100 outstanding returned pointers alias (callers hold <=32). (4) Uses Render::bw/bh for scaling while copy_view's root quad uses render_device->m_viewportWidth/Height — kept equal by the device, but two different globals. (5) BN's '0x63 == pre-increment' check is the same as Ghidra's '==100 post-increment' wrap.
Report notes: All three bodies extracted from docs/research/named-retail/acclient_2013_pseudo_c.txt and cross-checked function-by-function against the live Ghidra MCP (http://127.0.0.1:8081, patchmem 2013 build) — the BN text for copy_view and polyClipFinish is severe x87-flag/stack mush and would have yielded at least two inverted branch polarities (the edge inside test and the corner-keep test) plus a wrong w-clip plane (0 instead of cdstW) if read literally. Key contract facts for FW3/FW4: (a) view_vertex = {Vec2D pt; Plane plane} stride 0x18 (acclient.h line 32483), view_poly = {vertex_count, vertex_index, xmin, xmax, ymin, ymax} stride 0x18, portal_view_type per acclient.h line 32346; copy_view appends into poly slot [view_count] and vertex pool [vertex_count_total], resetting the pool when view_count==0. (b) The full chain: xformStart(v,1) produces homogeneous viewport coords (no divide) -> PView::GetClip @0x005a4320 pre-zeroes npts_out, reverses the pointer order for Sidedness != POSITIVE, sets Render::PolyCurrent=null/PolyCurrentMod=1.0/PolyCurrentPos=1, and calls polyClipFinish(screen, n, out, npts_out, 0) — inmask is ALWAYS 0 from all three call sites, so the edge-skip fast path is dead; do_clip==0 skips clipping entirely (copy/reverse only). (c) polyClipFinish output preserves input winding (reverse-scan passes + the conditional final reverse-copy always cancel). (d) copy_view then divides, dedups/collinear-prunes at ~1px (corner survives iff |2D cross| >= Chebyshev span), rejects <3, caps 31, and builds per-edge world planes N = normalize(cross(ray[k+1], ray[k])), d = -dot(N, Render::viewer_world_space.viewpoint) — note the cross ORDER is next-x-current, the fw-walk doc's §6 one-liner has it reversed (doc fix candidate, affects plane sign conventions for any consumer like viewconeCheck). (e) The live unproject is ScreenToViewTransform @0x0059aa40 (newmethod statically 1, initializer at data 0x0081efe8, no other writes found): ndc_u = ((2sx/vpW)-1)/ViewToClip[0][0], ndc_v = ((2sy/vpH)-1)(-1/ViewToClip[1][1]), ray = urow0 + vrow1 + row2 of inverse(WorldToView), with out.y taking matrix column 2 and out.z column 1 (D3D y-up -> AC z-up wiring); un-normalized direction. (f) Constants: cdstW = F_EPSILON = 0.000199999995f (0x3951B717); dedup threshold 1.0 px; vertex cap 0x1f; poly grow +0x10; vertex grow +0x10 slack. Ghidra decomp copies saved in scratchpad (copyview_ghidra.c, polyclip_ghidra.c, xformstart_ghidra.c) if the parent wants the raw text.
Report 4 - Landscape visibility (get_clip_height / block_check / set_default_view / update_viewpoint)
Render::get_pt_limit @0x0054b840
Summary: Defines the ViewIntervalType cell encoding. Classifies the vertical column at (x,y) against ONE clip plane, returning a signed scalar: outside_val (1001f) = column wholly outside; inside_val (0f) = wholly inside; positive h = inside only BELOW height h (plane normal points down, N.z < -eps); negative -h = inside only ABOVE height h (normal points up, N.z > eps). Vertical planes (|N.z| <= eps) collapse to all-in/all-out via Plane::which_side at z=0.
// F_EPSILON = 0.000199999995f; sky_height = 1000f; inside_val = 0f; outside_val = 1001f (static init $E106: 1000+1)
float get_pt_limit(float x, float y, Plane* p) {
if (p->N.z > F_EPSILON) { // inside half-space is z >= h
h = -((x*p->N.x + y*p->N.y + p->d) / p->N.z);
if (h >= sky_height) return outside_val; // need z >= 1000: nothing
if (h > 0) return -h; // inside above h
return inside_val; // h <= 0: whole column inside
} else if (p->N.z < -F_EPSILON) { // inside half-space is z <= h
h = -((x*p->N.x + y*p->N.y + p->d) / p->N.z);
if (h <= 0) return outside_val; // nothing above ground inside
if (h >= sky_height) return inside_val; // everything below sky inside
return h; // inside below h
} else { // vertical plane: no z dependence
if (Plane::which_side(p, Vector3(x, y, 0f), F_EPSILON) == NEGATIVE)
return outside_val;
return inside_val; // POSITIVE or ON_PLANE
}
}
Gotchas: BN body is pure x87-flag mush (unusable); this is the Ghidra decomp. Note the ROLE of eps: sidedness of N.z picks which half-space is 'inside' — inside is always the POSITIVE side of the plane (dot(N,p)+d >= 0), consistent with copy_view's plane winding and the vertical-plane which_side test. Vertical-plane test point is (x, y, 0) — z hardwired to 0. Boundary: h exactly 0 in the up-normal arm returns inside_val (encoding is continuous at 0).
Render::get_clip_height @0x0054cff0
Summary: Fills one ViewIntervalType (struct { float bound[32]; } — 0x80 bytes, acclient.h #4994) for the grid corner at world (x,y): bound[0] = column vs the CY near plane (Render::viewer_world_space.CY, installed by update_viewpoint); bound[1..portal_npnts] = column vs each edge plane of the ACTIVE view (installed by set_view: portal_vertex[i-1].plane). Called per landblock-grid corner by LScape::draw_check_blocks and per cell-grid corner by LScape::landcell_check.
void get_clip_height(float x, float y, ViewIntervalType* out) {
out->bound[0] = get_pt_limit(x, y, &Render::viewer_world_space.CY);
for (i = 1; i <= Render::portal_npnts; i++) // npnts = active view's edge count (4 for default view)
out->bound[i] = get_pt_limit(x, y, &Render::portal_vertex[i-1].plane);
}
Gotchas: BN's plane pointer '(edi_1 + portal_vertex) - 0x10' decodes as view_vertex stride 0x18 (Vec2Dscreen pt @0 + Plane @+8): iteration 1 hits vertex[0].plane. Loop bound is INCLUSIVE (i <= portal_npnts), so npnts+1 floats are written — matches set_view's portal_inmask = (1 << (npnts+1)) - 1 (edges + CY). Coordinates are viewer-block-relative 'world' space (the landscape's viewer-rebased frame), same space the CY/edge planes were built in.
Render::corner_plane_check @0x0054b930
Summary: Tests one corner's per-plane interval encoding against the block's z-range [min_z, max_z]. Returns BoundingType: OUTSIDE=0, PARTIALLY_INSIDE=1, ENTIRELY_INSIDE=2. Decodes the get_pt_limit scalar: sentinels first, then the above-h (negative) and below-h (positive) interval arms.
BoundingType corner_plane_check(float bound, float min_z, float max_z) {
if (bound == outside_val) return OUTSIDE; // 1001f
if (bound != inside_val) { // != 0f
if (bound <= 0f) { // inside is z >= h, h = -bound
h = -bound;
if (h > min_z) { // block bottom below clip height
if (max_z <= h) return OUTSIDE; // whole block below h
return PARTIALLY_INSIDE;
} // h <= min_z: block wholly above h
} else if (bound < max_z) { // inside is z <= h, h = bound; block top above h
if (bound <= min_z) return OUTSIDE; // whole block above h
return PARTIALLY_INSIDE;
} // h >= max_z: block wholly below h
}
return ENTIRELY_INSIDE;
}
Gotchas: BN body is x87-flag mush; polarity decoded from Ghidra's '(a < b) == (a == b)' idiom = a > b. Equality edges: touching the clip height on the OUT side counts OUTSIDE (max_z == h / min_z == h both reject); touching on the IN side counts ENTIRELY_INSIDE. Arg order min_z-then-max_z is established by the double swap through block_check (see its gotcha).
Render::block_plane_check @0x0054d060
Summary: Combines the four corner classifications of one plane for a block: OUTSIDE only when ALL four corners are OUTSIDE; ENTIRELY_INSIDE only when ALL four are ENTIRELY_INSIDE; anything mixed is PARTIALLY_INSIDE. The first corner's class picks which unanimity test runs.
BoundingType block_plane_check(float b1, float b2, float b3, float b4, float min_z, float max_z) {
c1 = corner_plane_check(b1, min_z, max_z);
c2 = corner_plane_check(b2, min_z, max_z);
c3 = corner_plane_check(b3, min_z, max_z);
c4 = corner_plane_check(b4, min_z, max_z);
if (c1 == OUTSIDE) {
if (c2 == OUTSIDE && c3 == OUTSIDE && c4 == OUTSIDE) return OUTSIDE;
} else if (c1 == ENTIRELY_INSIDE && c2 == ENTIRELY_INSIDE && c3 == ENTIRELY_INSIDE && c4 == ENTIRELY_INSIDE) {
return ENTIRELY_INSIDE;
}
return PARTIALLY_INSIDE;
}
Gotchas: This is conservative per plane: 3-of-4 corners outside one plane still yields PARTIAL (the block may straddle the plane). Cross-plane rejection happens in block_check, which ANDs OUTSIDE across planes — a block fully outside ANY single plane is culled.
Render::block_check @0x0054dc50
Summary: The exact block-vs-view interval test. Takes the block's four corner ViewIntervalTypes (west[y], west[y+1], east[y], east[y+1]) plus the block's max_zval/min_zval. Tests plane 0 (CY) first with early-out, then each of the active view's portal_npnts edge planes: any plane reporting all-corners-OUTSIDE culls the block; any PARTIAL demotes the running result; otherwise the plane-0 result (possibly ENTIRELY_INSIDE) survives.
// caller passes (..., blk->max_zval, blk->min_zval) — note arg5=max, arg6=min
BoundingType block_check(ViewIntervalType* i00, ViewIntervalType* i01,
ViewIntervalType* i10, ViewIntervalType* i11,
float max_zval, float min_zval) {
result = block_plane_check(i00->bound[0], i01->bound[0], i10->bound[0], i11->bound[0],
min_zval, max_zval); // CY plane first (args swapped back to min,max)
if (result == OUTSIDE) return OUTSIDE;
for (k = 1; k <= Render::portal_npnts; k++) {
r = block_plane_check(i00->bound[k], i01->bound[k], i10->bound[k], i11->bound[k],
min_zval, max_zval);
if (r == OUTSIDE) return OUTSIDE; // fully outside ONE plane => culled
if (r == PARTIALLY_INSIDE) result = PARTIALLY_INSIDE; // sticky demotion
}
return result; // ENTIRELY_INSIDE only if every plane said so
}
Gotchas: Argument-order trap: block_check receives (max_zval, min_zval) but forwards (arg6, arg5) = (min, max) to block_plane_check, which forwards positionally to corner_plane_check — net effect corner_plane_check(bound, min_z, max_z). Ghidra confirms the swap. BN's pointer-difference loop ('eax_5 = arg1 - arg3' etc.) is just base-relative addressing of bound[k] across the four structs — Ghidra's version shown here is the honest shape. Loop is 1..portal_npnts inclusive (bound[0] handled before the loop).
Render::update_viewpoint (Position const*) @0x0054cdd0
Summary: Installs the viewer for the frame: copies the pose into Render::viewer_pos, splits the rotation into Xaxis/Yaxis/Zaxis (right/forward/up rows of m_fl2gv), builds viewer_world_space = { viewpoint = eye origin, CY = near plane with N = Yaxis (forward), d = -dot(origin, Yaxis) - znear }, then pushes the D3D world-to-view matrix (view Z = forward, translation = world origin in viewer-local coords), refreshes lights, and recomputes selection_ray when check_selection is armed. This is ALL the state get_clip_height/viewconeCheck read besides set_view's.
void update_viewpoint(Position* pos) {
viewer_pos.objcell_id = pos->objcell_id;
viewer_pos.frame = pos->frame;
Xaxis = m_fl2gv[0..2]; Yaxis = m_fl2gv[3..5]; Zaxis = m_fl2gv[6..8]; // right, FORWARD, up
viewer_world_space.CY.d = -(origin.x*m[3] + origin.y*m[4] + origin.z*m[5]) - znear;
viewer_world_space.viewpoint = viewer_pos.frame.m_fOrigin;
viewer_world_space.CY.N = Yaxis; // near plane: dot(fwd,p) >= dot(fwd,eye)+znear
o = Frame::globaltolocal(&viewer_pos.frame, (0,0,0)); // world origin in view space
M = 4x4 { rows built from Xaxis / Zaxis / Yaxis, translation o, last row (0,0,0,1) }; // D3D: view Z = forward
RenderDeviceD3D::SetWorldToViewMatrix(render_device, &M);
m_pRenderer->UpdateLightsInternal(...); // vtbl +0x28
if (check_selection) selection_ray = *pick_ray(&o, selection_x, selection_y);
}
Gotchas: The Frame overload @0x0054dc20 wraps into a stack Position{vftable, objcell_id = 0, frame} and tail-calls this — BN's 'var_48 = 0x796910' is Position's vftable pointer, NOT a cell id; objcell_id is 0. The exact element ordering of the D3D Matrix4 is ambiguous in both decompilers (Ghidra shows _21 = Xaxis.z, _31 = Xaxis.y — transposed-looking stack writes); semantics (world-to-view, forward mapped to view Z) are solid but element layout should be re-derived at port time if it becomes load-bearing. CY does NOT depend on the active view — it is rebuilt only here; set_default_view/set_view never touch it.
Render::set_default_view @0x0054ef50
Summary: Installs the outdoor full-screen view. One-time-inits the static Render::window (a portal_view_type; portal/poly/vertex DArrays zeroed, blocksize 0x80, atexit dtor), resets view_count and vertex_count_total to 0, appends the full-viewport quad via copy_view(&window, null, 0), sets Render::PortalList = &window, and activates it with set_view(&window.view, 0) — after which portal_npnts = 4, portal_inmask = 0x1F (4 edge planes + CY), portal_vertex points at the quad's verts, and xmin/xmax/ymin/ymax hold the full screen bounds.
void set_default_view() {
if (!($S273 & 1)) { // one-time static init
$S273 |= 1;
window.portal = window.view.poly = window.view.vertex = empty DArray (blocksize 0x80);
window.view_timestamp = 0;
atexit($E274);
}
window.view_count = 0;
window.view.vertex_count_total = 0;
copy_view(&window, /*screen pts*/ null, /*npts*/ 0); // null source => full-viewport quad:
// appends poly {vertex_count=4, vertex_index=base}, verts (0,H),(W,H),(W,0),(0,0) + wrap copy of v0
// (W/H = render_device->m_viewportWidth/Height), computes xmin/xmax/ymin/ymax,
// builds 4 WORLD-space edge planes from the corner rays:
// ray = Xaxis*sx + Yaxis*vdst - Zaxis*sy (sx = x*xinvscale - tx, sy = y*yinvscale - ty)
// N = normalize(cross(ray_i, ray_i+1)), d = -dot(N, viewer_world_space.viewpoint)
// then window.view_count += 1 (=> 1)
Render::PortalList = &window;
set_view(&window.view, 0); // portal_npnts=4, portal_inmask=(1<<5)-1=0x1F,
// portal_vertex=&vertex[poly[0].vertex_index], screen bounds
}
Gotchas: Third copy_view arg here is 0 (source-point count, unused for the null source) — PView::DrawInside's root uses copy_view(top_view, null, 4); the null branch is identical, the count is ignored. copy_view has TWO ray paths gated on the global 'newmethod': ==1 uses PrimD3DRender::ScreenToViewTransform per point; else the inline xinvscale/yinvscale/tx/ty/vdst formula shown. Edge-plane normalization is skipped when every cross component < F_EPSILON (degenerate edge left unnormalized). The window's DArrays are grow-only and reused across frames; only the counters reset. copy_view returns 0 (view rejected, view_count NOT bumped) when <3 distinct screen points survive dedup — impossible for the null-source quad.
LScape::draw_check_blocks @0x00505f80
Summary: The per-frame landscape visibility pass (called by LScape::update_viewpoint @0x005062d0/0x0050634b). Clears every block's and land cell's in_view, (re)allocates the rolling corner-interval buffer, then FOR EACH VIEW in Render::PortalList (1 outdoor default view, or N portal exit views): installs the view with set_view, computes ViewIntervalType per landblock-grid corner at block_length (192 m) spacing in viewer-block-relative coords, and runs block_check per block; non-OUTSIDE blocks get in_view set and are refined per-cell by landcell_check @0x005050a0 (same machinery at 24 m).
void LScape::draw_check_blocks() {
// 1) clear
for (y,x over mid_width x mid_width) if (blk = land_blocks[mid_width*y + x]) {
blk->in_view = OUTSIDE; // +0xfc
for (i < side_cell_count^2) blk->lcell[i].in_view = 0; // CLandCell stride 0x108, field +0x104
}
// 2) rolling corner buffer: 2 rows x (mid_width+1) ViewIntervalType (0x80 each)
if (block_interval && block_int_size != mid_width) delete[] block_interval, block_interval = null;
if (!block_interval) { block_interval = new[( mid_width+1) * 0x100]; block_int_size = mid_width; }
// 3) per active view
nviews = Render::PortalList ? Render::PortalList->view_count : 0;
v = 0; last = false;
do {
if (nviews == 0) last = true;
else { Render::set_view(&Render::PortalList->view, v); v++; if (v == nviews) last = true; }
// seed west column (grid x index 0) into row 0
for (i = 0; i <= mid_width; i++)
get_clip_height((0 - viewer_b_xoff) * block_length, // block_length = 24*8 = 192f
(i - viewer_b_yoff) * block_length,
&block_interval[i]);
for (bx = 0; bx < mid_width; bx++) {
// east column (grid x = bx+1) into row (bx-1)&1 == (bx+1)&1
for (by = 0; by <= mid_width; by++)
get_clip_height((bx + 1 - viewer_b_xoff) * block_length,
(by - viewer_b_yoff) * block_length,
&block_interval[((bx-1) & 1) * (mid_width+1) + by]);
for (by = 0; by < mid_width; by++) {
if (!(blk = land_blocks[mid_width*bx + by])) continue;
west = &block_interval[(bx & 1) * (mid_width+1) + by]; // x = bx corners
east = &block_interval[((bx-1) & 1) * (mid_width+1) + by]; // x = bx+1 corners
bt = Render::block_check(west, west+1, east, east+1, // +1 struct = y+1 corner (+0x80)
blk->max_zval, blk->min_zval);
if (bt != OUTSIDE) { blk->in_view = bt; LScape::landcell_check(this, blk); }
}
}
} while (!last);
}
Gotchas: BN ELIDED the grid-spacing constant to '0f' — the real multiplier is the global ::block_length = 192.0f (static init: 24*8; the 24f seen at 0x00505187/0x00505206 belongs to landcell_check's 24 m cell grid, a different function). in_view is only WRITTEN when the result is non-OUTSIDE, so across the multi-view loop a block visible in ANY exit view stays marked (values can also be overwritten ENTIRELY<->PARTIALLY by later views; landcell_check reruns per view). When PortalList is null the body still runs ONCE against whatever set_view state is stale — unreachable in practice (set_default_view always precedes the outdoor walk). Ghidra renders side_cell_count/lcell as 'padding' — real names from CLandBlockStruct (acclient.h #3393): side_cell_count @+0x48, lcell @+0x90; the +0x104/0x108 constants mean CLandCell is 0x108 bytes with in_view last.
Report notes: All addresses from docs/research/named-retail/acclient_2013_pseudo_c.txt; every x87-ambiguous body cross-checked against the live Ghidra MCP (patchmem, port 8081) — get_pt_limit, corner_plane_check, block_check, set_default_view, update_viewpoint, draw_check_blocks, and copy_view's null branch were all fetched and agree with BN's control flow where BN was readable.
THE MODEL (how this completes FW sections 5-6): a ViewIntervalType is NOT a z-interval — it is a per-grid-corner VECTOR of 32 per-plane column classifications (bound[0] = the CY near plane from Render::viewer_world_space, bound[1..portal_npnts] = the active view's edge planes). get_pt_limit encodes each plane's intersection with the vertical column at (x,y) as a signed height: 0f = column wholly inside that plane's positive half-space, 1001f = wholly outside, +h = inside below h, -h = inside above h (sky_height = 1000f caps both). block_check then tests a landblock's z-slab [min_zval, max_zval] against the four corner columns, plane by plane: fully-outside-any-plane culls; per-plane mixing demotes to PARTIALLY_INSIDE; ENTIRELY_INSIDE requires unanimity on every plane. This is the landscape analog of viewconeCheck (which does the same CY+edge-planes test for a sphere) and consumes exactly the state installed by update_viewpoint (viewpoint, X/Y/Z axes, CY) + set_view (npnts, vertex planes, inmask).
Outdoor default view: set_default_view appends ONE full-viewport 4-vertex quad view to the static Render::window (screen corners (0,H),(W,H),(W,0),(0,0), 4 world-space edge planes built from corner eye rays with d = -dot(N, eye)), points PortalList at it, and activates it — so the outdoor walk always runs draw_check_blocks with exactly one view of 4 edge planes + CY (portal_inmask 0x1F). Landscape-through-portals reuses the identical loop with PortalList = &outside_view and view_count = N clipped exit views (FW section 5's 'ov' arm).
Constants for the port: F_EPSILON = 0.000199999995f; sky_height = 1000f; inside_val = 0f; outside_val = 1001f; block_length = 192f (24*8); ViewIntervalType = float[32] (0x80 bytes); BoundingType OUTSIDE=0 / PARTIALLY_INSIDE=1 / ENTIRELY_INSIDE=2; view_vertex stride 0x18 (Vec2Dscreen pt + Plane).
Key traps recorded in per-function gotchas: (1) BN elided block_length to '0f' in draw_check_blocks — 24f in the neighborhood belongs to landcell_check, not this function; porting 24 here would shrink the visible landscape 8x. (2) block_check receives (max_zval, min_zval) but corner_plane_check consumes (min_z, max_z) via a double positional swap. (3) equality edges: touching a clip height on the out side is OUTSIDE, on the in side is ENTIRELY_INSIDE. (4) update_viewpoint(Frame) wraps with objcell_id = 0 (BN's 0x796910 is Position's vftable). (5) the D3D world-to-view Matrix4 element order is decompiler-ambiguous (flagged, not guessed) — semantics are right/up/forward rows with view Z = forward; not load-bearing for the CPU walk.
Not extracted (out of scope, already covered or adjacent): copy_view's non-null source path and set_view are as FW section 6 describes (verified in passing); LScape::landcell_check @0x005050a0 is the per-cell (24 m) refinement using the same get_clip_height/interval machinery plus get_block_orient — it is the natural next extraction if FW needs cell-level in_view.