diff --git a/docs/LEVEL-STATUS.md b/docs/LEVEL-STATUS.md index 3c8096d8..8df3c26b 100644 --- a/docs/LEVEL-STATUS.md +++ b/docs/LEVEL-STATUS.md @@ -27,19 +27,20 @@ Build = `f2beae4b` + `tools_pc/d88_propdefs.py` (D123) + `port/src/gimgfixup.c` | Runway | 35 | CRASH | `import_texture_i8` gfx_pc.cpp:821 (fault 0x721b8ee8) — same as Facility | **C2** | | Facility | 34 | CRASH | `import_texture_i8` gfx_pc.cpp:821 (fault 0x72181ee8) — model-GDL relocation align bug, D124, NOT fixed | **C2** | | Jungle | 37 | CRASH | `gfx_sp_matrix` gfx_pc.cpp:1046 (fault 0x401c68e0) — C2 texture crash fixed (D124), now explosion-DL `G_MTX` (D75/matrix family) | **C2m** | -| Aztec | 28 | **PASS** 91.0% | — (C3 fixed, D125) | — | -| Bunker2 | 27 | CRASH | `door7F054FB4` propobj.c:13536 (`var_s1->openPosition`, `var_s1`=0xffff.. from `linkedDoor`) | **C3r** | -| Depot | 30 | CRASH | `sub_GAME_7F00324C` prop.c:902 (`sp4C->room` after `walkTilesBetweenPoints`) | **C4** | +| Aztec | 28 | **PASS** 90.8% | — (C3 fixed, D125) | — | +| Bunker2 | 27 | **PASS** 91.5% | — (C3r fixed, D126) | — | +| Depot | 30 | **PASS** 79.8% | — (C4 fixed, D126) | — | | Control | 23 | CRASH | `sub_GAME_7F0BA2D4` bg.c:5723 (`portal_pts->numPoints`, via chrprop.c:62) | **C5** | -| Surface2 | 43 | CRASH | `modelLoad` loadobjectmodel.c:393 (`PitemZ_entries[modelid].header->RootNode`) | **C6** | +| Surface2 | 43 | **PASS** 70.4% | — (C6 fixed, D126) | — | | Surface1 | 36 | CRASH | `sndSetupSound` snd.c:653 (fault 0x56220001…, ascii-ish) | **C7** | -**13 / 21 PASS.** D123 cleared C1 on all 6 (Dam/Frigate/Statue/Cradle/Streets -PASS; Runway falls through to C2). D124 cleared Jungle's texture crash but -it now hits an explosion-DL matrix crash. D125 (`d88_emit.py:374` 8-byte -slice / 4-byte literal → boundpad-name blob drift) cleared C3-Aztec; -Bunker2 falls through to a separate DOOR-tail `linkedDoor` layout bug -(C3r). 8 crashes remain in 6 classes. +**16 / 21 PASS.** D123 cleared C1 on all 6. D124 cleared Jungle's texture +crash (now hits an explosion-DL matrix crash). D125 cleared C3-Aztec. +**D126** (objective sub-record `->next` pointer widens 4→8B → 8-byte store +clobbers the next propdef record's header → walk desync → wrong command +indices) cleared **C3r Bunker2 + C4 Depot + C6 Surface2** in one fix +(`d88_propdefs.py` types 30/32/33/35 → 24B/6w). 5 crashes remain in 4 +classes: C2 Runway+Facility, C2m Jungle, C5 Control, C7 Surface1. Notes: Streets passed this run but the C1 agent saw a `gfx_sp_matrix` crash — likely the same explosion-DL `G_MTX` (C2m) as Jungle, timing- @@ -162,20 +163,16 @@ id from a propDef record). Fault addr 0x0. Files: `tools_pc/d88_propdefs.py`, crash on load, not silence. May be dodge-able with a narrow guard until audio lands. Files: `src/snd.c`, `port/src/` audio stubs. -## Next (8 crashes, 6 classes — priority order) +## Next (5 crashes, 4 classes — priority order) 1. **C2** Runway + Facility — model-GDL relocation align bug in `objecthandler_2.c`/`texLoadFromGdl` (D80/D82/D83 area). Shared infra, - 2 levels + likely helps Jungle/Streets. Biggest win. -2. **C3r** Bunker2 — DOOR-tail `linkedDoorOffset`/`linkedDoor` layout in - `d88_propdefs.py` vs compiled PC `DoorRecord` (Aztec cleared by D125); - fold into the converter write-audit. Brief `docs/BRIEF-C3-C6-prop-model.md`. -3. **C2m** Jungle (+ Streets intermittent) — explosion-DL `G_MTX` - (`gfx_sp_matrix`), D75/matrix family. -4. **C6** Surface2 — `PitemZ_entries` modelid (D122/D123 continuation). -5. **C5** Control — BG portal `offset_portal` resolve. -6. **C4** Depot — BG tile/room table. -7. **C7** Surface1 — `sndSetupSound`; parked-audio, may just need a narrow - load guard. + 2 levels + likely helps Jungle. Biggest win. +2. **C2m** Jungle — explosion-DL `G_MTX` (`gfx_sp_matrix`), D75/matrix + family. +3. **C5** Control — BG portal `g_BgPortals[].offset_portal` unresolved/BE, + `bg.c:5723` via `chrprop.c:62`. BG sidecar (`d69_emit.py`). +4. **C7** Surface1 — `sndSetupSound` `snd.c:653`; parked-audio, may just + need a narrow load guard until Phase 3. Re-run `tools_pc/level_sweep.sh` after each class fix; keep `-level_09`/`-level_20` green (`tools_pc/framediff.py`). diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md index 8eed3c6f..5254191b 100644 --- a/docs/PCPortResearch.md +++ b/docs/PCPortResearch.md @@ -819,6 +819,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D122 | per-level prop/item model-load crash (Dam/Facility/Runway `modelLoad`/`modelInitRwData`): `d88_propdefs.py` had no handler for 6 ObjectRecord-derived propDef types (47/39/40/45/13/20) → generic arm half-swapped the `[s16 obj][s16 pad]` word → OOB `PitemZ_entries[]` | converter fixed; residual chr/fast3d crashes on those levels are separate | | D123 | crash class C1: `chrIsNotDeadOrShot` NULL deref on 6 levels (Dam/Runway/Frigate/Statue/Streets/Cradle). D122's `OBJ_TAIL_DESC` zeroed the widened `VehichleRecord/AircraftRecord.ailist` slot (w32), but `prop.c:1764/1786` reads a pre-populated int AI-list id there → `ailistFindById(0)` → `GAILIST_AIM_AT_BOND` → `ai()` runs a CHR aim list with `ChrEntityp==NULL` | converter fixed (`OBJ_ID_WORDS`); C1 cleared on all 6, residual crashes are fast3d (C2) | | D125 | crash classes C3+C6 (Aztec/Bunker2/Surface2): **root cause found (M-16)** — `tools_pc/d88_emit.py:374` assigns a 4-byte literal to an 8-byte slice (`out[dst_o+0x30:dst_o+0x38] = b"\x00…"`, the stan zero-fill in `emit_pad`); every pad/boundpad record silently shrinks the output bytearray by 4 B, and once the tail falls below the boundpad plink string blob, later verbatim leaf writes hit Python out-of-range slice semantics (insert at current end, not relocated offset) → boundpad names drift/truncate (Aztec pad33 `p138d2`→`8d2`) → `stanPackId()` reject → `getposstan()` NULL stan → `setupDoor` leaves door `model=NULL` → crash `propobj.c:13601`. M-14's "propDefs zeros in RAM" was a misread; sidecar propdefs were always correct (M-15) and post-load RAM matches the sidecar byte-for-byte (M-16) | **resolved (M-16b)** — line 374 → 8 NULs; all 21 sidecars regen'd; **Aztec `-level_28` now PASSES** (was C3 CRASH). Bunker2 falls through to a separate DOOR-tail `linkedDoor` layout bug (C3 residual) | +| D126 | crash classes C3r/C4/C6 (Bunker2 `-level_27` `door7F054FB4` propobj.c:13523, Depot `-level_30` prop.c:902, Surface2 `-level_43` loadobjectmodel.c:393): the objective sub-records `criteria_picture` (30), `criteria_roomentered` (32), `criteria_deposit` (33), `setup_objective_text` (35) each end in a `T *next` list pointer that the setup walk (`set_parent_cur_obj_*` / `setup_briefing_text_entry_parent`) writes unconditionally. On PC that pointer widens 4→8B and lands 8-aligned at offset 16 → struct is 24B/6w (N64 16/20). `d88_propdefs.py` emitted them at N64 size via the generic arm → the runtime 8-byte `->next` write clobbered the *next* record's header → propdef walk desynced, command indices drifted ~100, `linkedDoorOffset + arg2` resolved to the wrong record → door `linkedDoor` chain walked into garbage. | resolved — `d88_propdefs.py` PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler; `loadobjectmodel.c` sizepropdef PORT returns 6. Bunker2/Depot/Surface2 now PASS (13→16/21) | | D124 | crash class C2: fast3d bad texture pointer. **Jungle** (`0xabcd0824`): `gimgSyncCompiledGlobalDLs()` slot-detect keyed on the post-fixup marker, which `texLoad()` had already overwritten → compiled `globalDL_0xNNN` explosion DLs kept link-time `IMAGESEG` words → latent on every level, tripped by the first explosion-DL draw. **Facility** (`0x72181ee8`): separate — model/prop GDL from the `texLoadFromGdl()`/`sub_GAME_7F0762E0` relocation path writes non-16-aligned `dst` (N64 8B vs PC 16B `Gfx` stride mix in `objecthandler_2.c`), open D80/D82/D83 area | Jungle fixed (`port/src/gimgfixup.c`); Facility diagnosed, not fixed | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS @@ -4355,6 +4356,39 @@ has linked double-doors, Aztec's are singletons so it dodged it. Folded into the converter write-audit (C3 residual). Sweep: 12 → 13/21. All M-16 probes reverted, scratch files deleted, `d125_check.py` kept. +**D126 (session M-17) — objective-subrecord `->next` pointer growth desyncs +the propdef walk (C3r Bunker2 + C4 Depot + C6 Surface2).** The Bunker2 +`door7F054FB4` crash was NOT a DOOR-tail layout bug (the M-16b hypothesis). +Instrumenting the `proplvreset2` first-loop walk showed `pdefIndex` drifting +~100 ahead of the offline `convert_stream` record index; a byte dump proved +the RAM propDefs matched the sidecar exactly *at load* but had record N+1's +header zeroed *by the time the walk reached it*. Cause: four objective +sub-record types — `criteria_picture` (30), `criteria_roomentered` (32), +`criteria_deposit` (33), `setup_objective_text` (35) — are +`{ s32 x N; T *next; }`. `set_parent_cur_obj_photograph/enter_room/ +deposited_in_room` and `setup_briefing_text_entry_parent` (`objective.c`) +write `arg0->next` unconditionally while the setup walk visits each record, +building a runtime linked list. On N64 `next` is a 4-byte word at the end of +a 16/20-byte record — no spill. On x86-64 the pointer widens to 8B and the +compiler 8-aligns it at offset 16, so the struct is 24B; the 8-byte +`->next` store lands at record+16..24, clobbering the *following* record's +`PropDefHeaderRecord` (type byte at +0x13). Walk desyncs → command indices +drift → `setupDoor`'s `setupGetPtrToCommandByIndex(linkedDoorOffset + arg2)` +returns the wrong record → `door->linkedDoor` chain walks into non-door +memory → crash. Depot (`prop.c:902` tile-walk room) and Surface2 +(`PitemZ_entries[modelid]`, the C6 "D122 continuation") were the same +desync landing on different downstream derefs. Fix: +`tools_pc/d88_propdefs.py` `PROPDEF_PC_BYTES[30/32/33/35] = 24` + a typed +handler emitting the leading s32 words then an 8-byte zero `next`; +`src/game/loadobjectmodel.c` `sizepropdef` PORT returns 6 for those types +(N64 `#else` kept). Regen'd all 21 sidecars, `d125_check.py` 21/21 MATCH. +**Bunker2 `-level_27`, Depot `-level_30`, Surface2 `-level_43` now PASS** +(91.5% / 79.8% / 70.4%); Bunker1/Silo/Aztec/Archives/Egypt/Train +unregressed. Sweep 13 → 16/21. Generalisable quirk appended to +`PORT-LEARNINGS.md` §A. Remaining crashes: C2 Runway/Facility (model-GDL +align), C2m Jungle (`G_MTX`), C5 Control (BG portal), C7 Surface1 +(`sndSetupSound`). + **D124-Facility addendum (session M-14 — partial, NOT fixed, out of time).** Re-instrumented with a `GE_C2GDL` probe in `sub_GAME_7F0762E0` (`objecthandler_2.c`) + a bad-`G_SETTIMG` catcher in fast3d diff --git a/docs/PORT-LEARNINGS.md b/docs/PORT-LEARNINGS.md index a804696c..68f93a29 100644 --- a/docs/PORT-LEARNINGS.md +++ b/docs/PORT-LEARNINGS.md @@ -26,6 +26,19 @@ state. N64 via act-union@0x2C+84; act union moves to ~0x38 on PC). - **Open landmine:** raw hardcoded-offset accessors into `struct player` / `struct hand` — see `docs/AUDIT-M6-player-offsets.md`. +- **D126 corollary — a trailing runtime list pointer in a ROM-serialized + record.** If a fixed-size ROM record ends with a `T *next` (or `*child`, + `*parent`) that game code *writes* while walking the setup stream + (linked-list build), that field is 4B/end-of-record on N64 but widens to + 8B and 8-aligns on PC — the store spills past the N64 record size into + the *next* record and silently corrupts a polymorphic walk downstream + (type byte clobbered → wrong strides → wrong command indices → wrong + pointer resolution, crashing far away). Offline converter (`d*_emit.py`) + MUST emit these at the real PC `sizeof` with the pointer slot widened + + the matching `#ifdef PORT` `sizepropdef`/stride. Instances: `d88_propdefs.py` + types 30/32/33/35 (`criteria_*` / `setup_objective_text` `->next`), + 22 (`TagObjectRecord.NextTag`, already handled). Check every record type + the walk can see for a `set_parent_*` / `*_entry_parent` writer. ## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64 diff --git a/src/game/loadobjectmodel.c b/src/game/loadobjectmodel.c index 45566951..05b4b41e 100644 --- a/src/game/loadobjectmodel.c +++ b/src/game/loadobjectmodel.c @@ -66,11 +66,18 @@ s32 sizepropdef(PropDefHeaderRecord *pdef) case PROPDEF_OBJECTIVE_DEPOSIT_OBJECT: return 2; case PROPDEF_GUARD_ATTRIBUTE: case PROPDEF_OBJECTIVE_COPY_ITEM: return 3; - case PROPDEF_OBJECTIVE_START: + case PROPDEF_OBJECTIVE_START: return 4; + /* D126: criteria_picture / criteria_roomentered / criteria_deposit / + * setup_objective_text each end in a T*next list pointer that the setup + * walk (set_parent_cur_obj_* / setup_briefing_text_entry_parent) writes + * unconditionally. The pointer widens 4->8B and lands 8-aligned at + * offset 16, so the PC struct is 24B / 6 words (N64: 16/20). Converter + * emits the matching stride (tools_pc/d88_propdefs.py PROPDEF_PC_BYTES). + * N64 counts were: PHOTOGRAPH/ENTER_ROOM/WATCH_MENU 4, DEPOSIT_IN_ROOM 5. */ case PROPDEF_OBJECTIVE_PHOTOGRAPH: case PROPDEF_OBJECTIVE_ENTER_ROOM: - case PROPDEF_WATCH_MENU_OBJECTIVE_TEXT: return 4; - case PROPDEF_OBJECTIVE_DEPOSIT_OBJECT_IN_ROOM: return 5; + case PROPDEF_WATCH_MENU_OBJECTIVE_TEXT: + case PROPDEF_OBJECTIVE_DEPOSIT_OBJECT_IN_ROOM: return 6; case PROPDEF_LINK: case PROPDEF_SWITCH: case PROPDEF_TAG: diff --git a/tools_pc/d88_propdefs.py b/tools_pc/d88_propdefs.py index 2d5bc9c8..17b16779 100644 --- a/tools_pc/d88_propdefs.py +++ b/tools_pc/d88_propdefs.py @@ -138,12 +138,12 @@ PROPDEF_PC_BYTES = { 23: 16, # OBJECTIVE_START (4w, no serialized pointer) 24: 4, # OBJECTIVE_END 25: 8, 26: 8, 27: 8, 28: 8, 29: 8, # OBJ_* (2w) - 30: 16, # OBJ_PHOTOGRAPH (4w serialized, lastprop* not serialized) + 30: 24, # OBJ_PHOTOGRAPH (D126: criteria_picture, trailing ->next widens) 31: 4, # OBJ_NULL - 32: 16, # OBJ_ENTER_ROOM - 33: 20, # OBJ_DEPOSIT_IN_ROOM + 32: 24, # OBJ_ENTER_ROOM (D126: criteria_roomentered, trailing ->next widens) + 33: 24, # OBJ_DEPOSIT_IN_ROOM (D126: criteria_deposit, trailing ->next widens) 34: 12, # OBJ_COPY_ITEM (3w) - 35: 16, # WATCH_MENU_OBJ_TEXT (4w) + 35: 24, # WATCH_MENU_OBJ_TEXT (D126: setup_objective_text, trailing ->next widens) 37: 48, # RenameObjectRecord 38: 32, # LockDoorRecord 39: 208, # VehichleRecord (D122: 144 prefix + widened tail) @@ -335,6 +335,26 @@ def convert_record(src, so, type_byte): assert pc <= pcb, (type_byte, pc, pcb) return bytes(out) + if type_byte in (30, 32, 33, 35): + # D126: objective sub-records with a trailing runtime linked-list pointer + # 30 criteria_picture / 32 criteria_roomentered / + # 33 criteria_deposit / 35 setup_objective_text + # all share the shape `{ s32 x N; T *next; }`. The `next` word is a + # runtime list pointer -- set_parent_cur_obj_* / setup_briefing_text_ + # entry_parent overwrite it unconditionally during the setup walk -- so + # its ROM value is don't-care. On PC the pointer widens 4->8B and lands + # 8-aligned at offset 16 (compiler pads the 3-s32 variants), making the + # struct 24B / 6 words (was 16/20 on N64). Without the grow, that 8-byte + # write clobbers the next record's header -> propdef walk desyncs, command + # indices drift, linked-door resolution lands on the wrong record + # (Bunker2 -level_27 crash in door7F054FB4). Emit the leading s32 words + # then 8 zero bytes for `next`. + out[0:4] = _hdr_word(src[so:so + 4]) + for i in range(1, n64w - 1): + out[4 * i:4 * i + 4] = _bswap32(src[so + 4 * i: so + 4 * i + 4]) + # next* @ offset 16 already zero (bytes 12..15 = alignment pad, also zero) + return bytes(out) + # ---- generic: header-only records (objectives, END, DOOR_SCALE, ...) ---- # No serialized pointers. header word special, rest plain bswap32. out[0:4] = _hdr_word(src[so:so + 4])