docs(render): rescope Campaign OVERHAUL to five production slices

v2 replaces the twelve-stage plan. OH1's evidence-grammar stage is parked
on quarantine/oh1-evidence-grammar-2026-09-02; the committed research
contracts stay binding. Stage mapping: OH2 -> S1, OH3 -> S2, OH4+OH5 -> S3,
OH6+OH7 -> S4, OH8-OH11 -> S5. Adds the working model (lead in the loop,
bounded chunks, time-box), the single retail capture session before S3,
and per-slice minimal evidence products. Carries the InitCell inflag
prose correction in the flood appendix and marks the T3 handoff superseded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-02 17:50:25 +02:00
parent 5d907ae9ad
commit 13fc7d8349
3 changed files with 1077 additions and 1444 deletions

View file

@ -6,7 +6,18 @@ Second read round: the interior-flood and view-support functions. Same method as
### 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.
**Corrected summary (2026-09-02, x87 branch re-arbitrated):** 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`, and classifies every portal by
the cell-local viewpoint/plane side test. The names are deceptive:
`ClipPortals` considers exactly `seen != 0 && inflag != 1`, so `inflag == 0`
is the traversable candidate state and `inflag == 1` is excluded. The entered-
through portal is forced to `inflag=1, seen=1` and is therefore excluded as the
backlink. `max_indist` is the maximum SQUARED viewpoint distance over the
`inflag == 1` portal vertices—the excluded/incoming/back-facing set—not the
traversable set. The earlier prose labels in this report inverted this meaning;
the assignments themselves were correct.
```c
int PView::InitCell(CEnvCell* cell, uint16 entry_portal_idx) // Ghidra: returns int; BN said void
@ -21,12 +32,12 @@ int PView::InitCell(CEnvCell* cell, uint16 entry_portal_idx) // Ghidra: returns
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*/;
int any_candidate = /*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)
// entered-through backlink: forced excluded + consumed (0xffff seed sentinel never hits this)
slot->portal.data[i].inflag = 1;
slot->portal.data[i].seen = 1;
} else {
@ -36,10 +47,10 @@ int PView::InitCell(CEnvCell* cell, uint16 entry_portal_idx) // Ghidra: returns
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
else { slot->portal.data[i].inflag = 0; any_candidate = 1; goto vertex_scan; } // IN_PLANE: traversable candidate
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
slot->portal.data[i].inflag = 1; // excluded by ClipPortals
else { slot->portal.data[i].inflag = 0; any_candidate = 1; } // traversable candidate
}
vertex_scan:
if (slot->portal.data[i].inflag == 1 && poly->num_pts > 0) // num_pts = byte @ +0xe
@ -48,14 +59,14 @@ vertex_scan:
if (max_d2 < d2) max_d2 = d2;
}
}
slot->max_indist = max_d2; // max squared distance to any in-view portal vertex
slot->max_indist = max_d2; // max squared distance over inflag==1 (excluded) portal vertices
if (any_rejected != 0 && slot->view_count > 0)
if (any_candidate != 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
portal[j].seen = 1; // makes inflag==0 portals eligible for ClipPortals
}
slot->update_count = slot->view_count;
@ -64,7 +75,29 @@ vertex_scan:
}
```
**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.
**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). The x87 branch was re-arbitrated against retail bytes
`005A4C48..005A4C9D` on 2026-09-02. Confirmed semantics:
`side==portal_side` and IN_PLANE (`|d|<=0.000199999995f`) produce
`inflag=0`, which is the traversable candidate state because `ClipPortals`
later requires `seen!=0 && inflag!=1`; side mismatch produces the excluded
`inflag=1` state. Real retail quirks: (1) `any_candidate` (`local_4`) is NEVER
initialized—if no candidate exists it reads stack garbage; the effect is
benign because the fixup inner body no-ops, leaving only redundant `set_view`
churn, so a port may initialize it to zero with identical observable behavior.
(2) The entry-portal branch is guarded by stale `inflag`; it forces the
entered-through backlink to the excluded/seen state. On a freshly grown portal
array `inflag` is heap garbage (`DArray::grow` does not zero new entries), so
the caller contract remains significant. (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. (4) `positionPush(3, cell->pos)` means the plane
test and `max_indist` run in CELL-LOCAL coordinates. (5) The `0xffff` seed
sentinel never matches, so the seed cell side-tests every portal. (6)
`max_indist` is a SQUARED distance over `inflag==1` portal vertices; todo-list
comparisons stay internally consistent even though that set is excluded from
traversal. Ghidra return type is int (0 = early-out on `view_count==0`, 1 =
did work); BN said void `__stdcall`.
### PView::InsCellTodoList @0x005a4f50
@ -147,7 +180,25 @@ void CEnvCell::curr_view_push()
**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 notes (corrected 2026-09-02):** All four bodies were
cross-checked against the live Ghidra project; the load-bearing branches were
then re-arbitrated against the retail bytes because the earlier prose assigned
the intuitive but wrong meaning to `inflag`. (1) `InsCellTodoList` uses strict
`dist < prev->dist` as its break, giving a list descending from index zero and
nearest-first END pops, with FIFO ties. (2) `InitCell` writes `inflag=0` for
`side==portal_side` and IN_PLANE, then its fixup writes `seen=1`; those rows
are precisely the candidates later admitted by `ClipPortals`'s
`seen!=0 && inflag!=1` gate. Side mismatch and the entered-through backlink
use `inflag=1` and are excluded. `max_indist` is the maximum squared
cell-local distance over the `inflag==1` portal vertices, not over the
traversable candidates. Two genuine retail quirks remain relevant:
`local_4` is an uninitialized stack read, and the entry-portal branch keys off
stale `inflag` state established by the caller/allocation history. Struct
authorities remain `portal_info {seen,inflag}`, `portal_view_type` (0x48
bytes), `PView`, `CellListType {cell,float dist}`, and `CPolygon`. Sources:
named-retail lines 311378-311393, 432896-433045, 433183-433243, and
433279-433320; retail bytes `005A4C48..005A4C9D`; DArray grow/shrink near
`0x005A45D0`; and the named retail header definitions.
## Report 2 - Flood propagation (ClipPortals / AddViewToPortals)