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:
parent
5d907ae9ad
commit
13fc7d8349
3 changed files with 1077 additions and 1444 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue