forked from Ports/goldeneye-pc-port
PC port: phase 2 — D59-D68 milestone: full intro renders; blocker is D69 (BG-file BE headers)
D59/D64: the 'external GL DLL' crash was an unbounded blood-RLE write (sentinel die_blood_image_end lost its N64 section adjacency on PC) clobbering the gun-barrel sub-DL at ptr_logo_and_walletbond_DL+0x200. D60: osPiStartDma DMA-target validation (VirtualQuery-based) in libultra.c. D61/D63: TEMP env-gated ROM-read / VTX-pool / menu probes (strip list in HANDOFF Task 3). D62: OSMesgQueue/OSScMsg PC layout verification. D65/D65b: enum HEADS/BODIES signed sentinels under PORT (PC GCC picks unsigned underlying types; N64 semantics were signed). D66: romCopyAligned + ramrom replay 64-bit pointer width (s32 truncation of .bss targets > 4 GiB). D67: struct image_entry N64 layout reconstruction (dataoffset:24 first, all-u32 bitfields, sizeof==8). D68: Globalimagetable BE->LE fixup (port/src/gimgfixup.c): bswap the IMAGESEG-marked Gfx w1 words in the 17 ROM-copied DLs + sImageTableEntry. index in the 32 tables after texReset's romCopy; marker scan moved to bytes 6..7 under PORT; compiled globalDL shadows synced post-texLoad for explosion.c's g_ExplosionDisplayLists. Result: game boots, plays intro music, and renders the entire ~3.5-minute intro (logo -> gun barrel with Brosnan -> cast screen) at ~59 fps. New blocker D69: load_bg_file faults on first stage load - BG-file headers are N64 big-endian segment-0x0F offsets read LE on PC. Full analysis + strategy in PCPortResearch.md §F/D69; HANDOFF rewritten for the D69 session.
This commit is contained in:
1 parent
2c0c0515dd
commit
8f631462f3
50 files changed
+2526
-376
No files matched your search
@@ -124,3 +124,8 @@ _run.log
|
||||
ge007.crash.log
|
||||
.pi/
|
||||
run_out.log
|
||||
|
||||
# PC port run/scratch logs
|
||||
run_*.log
|
||||
d6*.log
|
||||
ge007.crash.log
|
||||
@@ -48,6 +48,6 @@ Run `/linkcheck` for this sweep. Record new findings in `docs/PCPortResearch.md`
|
||||
- **Compile+link milestone:** all ~235 TUs compile and `ge007.x86_64.exe` links clean (`ninja ge007 -k 0`, 236/236). Findings D7–D17 in `docs/PCPortResearch.md` §11; asset stubs in `port/src/assetstubs.c`, fast3d no-ops in `port/src/gfxstub.c` (delete when real fast3d lands).
|
||||
- **Phase 1 (boot to window):** done — ROM loads/validates/maps at cart base 0x10000000 (`romdata.c`), SDL2 window + GL clear loop (`video.c`), absolute asset symbols from `scripts/gen_romassets.py` → `port/src/romassets_<r>.s` (findings D18–D23).
|
||||
- **Phase 1.5 (boot to first frame):** DONE — boot chain complete AND real frames render (`[NOTE] frame N rendered in … us`). D34–D42 closed the blockers on top of D31–D33: ANIM_DATA lvalues, music seq-table ABI+endianness, music-heap sizing, libaudio bank-tree re-layout, implicit-declaration prototype shim (D38), Globalimagetable rebasing + true segment size (D39), ModelHitEntry pool sizing (D40), cross-thread GL context release (D41), rsp task-settings toggle (D42). ROM dump verified against the No-Intro good dump (byte-identical, header CRCs re-checked — `tools_pc/romverify.c`).
|
||||
- **Phase 2 (rendering):** IN PROGRESS — fast3d integrated + real frames render. **Plan B EXECUTED (D50, committed):** all 512 NTSC model files converted offline by `tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` + manifest.csv (single concatenated RZ image; regenerate per region with the script — sidecars are gitignored), served through the existing load path via a port-layer table patch (`port/src/pcmodels.c`, romdata cart-extension, one-shot `pcmodelsPatchTable()` + poolRemaining reset hook in `load_object_fill_header`); runtime C fixups for language-bank BE offset tables + font re-layout (D50.3/D50.4); legal-screen UB seed (D50.5); **D50.6 fixed the first model-render crash** — `texCopyGdls` copied only w0 of each 16-byte Gfx slot on PC (the union's trailing `long long` is 8 bytes), leaving stale mempool w1s in the compacted GDLs; byte-proven against PlegalpageZ, fixed with a full-slot copy under #ifdef PORT. Model GDLs now execute to completion (PlegalpageZ sub-DLs). **Current blocker D51:** SIGSEGV in `import_texture_i8` via a main-DL G_TEXRECT lazy tile-0 upload (frame 5) — GE opcode space decoded (G_TEXRECT=0xE4/0xE5, non-F3DEX numbering; see §F/D51); investigation recipe in HANDOFF Task 1. Next: D51 → pixel-assert soak (PPM dump + pixcount still unbuilt) → next asset type (Tbg_*_stanZ backgrounds, same offline pattern; PD `preprocess/filebg.c` reference). Recurring sub-task: ABI/layout fixes per the D32 procedure (§H) as more asset types are first touched (PD's `preprocess/` module already solves most of them; prefer the offline pattern for format work — D48; full-struct Gfx copies on PC — D50.6).
|
||||
- **Phase 2 (rendering):** IN PROGRESS — fast3d integrated + real frames render. **Plan B EXECUTED (D50, committed):** all 512 NTSC model files converted offline by `tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` + manifest.csv (single concatenated RZ image; regenerate per region with the script — sidecars are gitignored), served through the existing load path via a port-layer table patch (`port/src/pcmodels.c`, romdata cart-extension, one-shot `pcmodelsPatchTable()` + poolRemaining reset hook in `load_object_fill_header`); runtime C fixups for language-bank BE offset tables + font re-layout (D50.3/D50.4); legal-screen UB seed (D50.5); **D50.6 fixed the first model-render crash** — `texCopyGdls` copied only w0 of each 16-byte Gfx slot on PC (the union's trailing `long long` is 8 bytes), leaving stale mempool w1s in the compacted GDLs; byte-proven against PlegalpageZ, fixed with a full-slot copy under #ifdef PORT. Model GDLs now execute to completion (PlegalpageZ sub-DLs). **D51 RESOLVED** (font `pixeldata` fixup wrote the pointer at the wrong offsets — frame-5 `import_texture_i8` crash gone; GE opcode space decoded along the way, G_TEXRECT=0xE4/0xE5 non-F3DEX numbering), **D52 RESOLVED** (`osGetCount` now advances at the N64 RSP counter rate ≈ 46.5525 ticks/µs — fixed the non-deterministic post-frame-2 hang), **D53 RESOLVED** (model RW-data pool addressing: D53.1 `Model.datas` word-stride → `u32 *` under PORT + casts; D53.2 ModelSlot/AnimModelSlot re-laid on top of the PC struct Model — in-use@0x10 / pool@0x20, slot size = sizeof(Model) = 0xE8 — plus PORT-guarded heap-fallback bumps in model.c), **D54 RESOLVED** (cseq ALCMidiHdr BE header: port-layer `romdataFixupCseq` bswap32 of the first 17 u32s, called from music.c after each decompress), **D54b RESOLVED** (synth param slots sized for ALStartParamAlt = 0x28 in synthesizer.c alSynInit under PORT), **D55 RESOLVED** (RLE folder-menu background BE w/h header: bswap32 of the first word at `(u32 *)&unknown2` at cart-map time in romdataInit, guarded). Build GREEN; game boots, plays intro music, renders through logo/gun-barrel, instantiates Brosnan, and executes the gun-barrel hole DL clean to ENDDL. **D56 RESOLVED** (watch Model/RW-pool raw offsets: field-name Model access + static 0xC8-byte pool under PORT in bondview2.c), **D57 RESOLVED** (pointer-grown rwdata records overflowed N64-sized spare pools — Brosnan PC numRecords 153 > 140: capacities grown in initunk_005520.c + dynamic slot+pool fallback in model.c), **D58 RESOLVED** (gun-barrel DL: vertex-pointer K0 idiom rebuilt via `OS_K0_TO_PHYSICAL(…) | 0x80000000` in title.c + DL reserve 0x100→0x200 because `sizeof(Gfx)` is 16 on PC). **D59–D68 RESOLVED (committed):** the "GL DLL" crash was D64's unbounded blood-RLE write (sentinel section-adjacency lost on PC) clobbering the gun-barrel sub-DL; then D60–D62 (DMA target validation, OSMesgQueue layout), D65/D65b (HEADS/BODIES enum signed sentinels under PORT), D66 (romCopyAligned + ramrom replay 64-bit pointer width), D67 (struct image_entry N64 layout), **D68 (Globalimagetable BE→LE fixup** — `port/src/gimgfixup.c` bswaps IMAGESEG Gfx w1 words + sImageTableEntry.index after texReset's romCopy; compiled globalDL shadows synced for explosion.c). The game now renders the **entire ~3.5-minute intro** (logo → gun barrel w/ Brosnan → cast) at ~59 fps. **Current blocker D69:** `load_bg_file` (bg.c:830) faults on first stage load — BG-file headers/records are N64 big-endian segment-0x0F offsets, read LE on PC; the bg `.seg` + `Tbg_*_stanZ` format work is next (decode from bg.c/stan.c → offline sidecar conversion per the D43 pattern or runtime port-layer fixup; PD `preprocess/filebg.c` is approach-reference only — its BG format differs). Details in PCPortResearch.md §F/D59–D69. Recurring sub-task: ABI/layout fixes per the D32 procedure (§H) as more asset types are first touched (PD's `preprocess/` module already solves most of them; prefer the offline pattern for format work — D48; full-struct Gfx copies on PC — D50.6; audit struct type-puns after pointer-width changes — D53.2; raw byte offsets into pointer-containing structs shift on PC — D56; N64-sized reservations must be re-checked against 16-byte Gfx/Vtx widths — D58).
|
||||
- **Phase 3 (audio + input):** libaudio → SDL — largely copy-and-adapt from PD (`mixer.c` 722 lines vs our 31-line stub; `input.c` SDL_GameController backend; §2.4); decide the ASP strategy (C2: `aspMain*` dummies in `ucode.c`).
|
||||
- **Phase 4 (saves + polish):** file-backed EEPROM — build on PD's `fs.c` (file-backed save dir); PFS/motor already no-op stubs.
|
||||
+148
-302
@@ -1,321 +1,167 @@
|
||||
# Handoff brief — GoldenEye 007 PC port (Phase 2: Plan B executed (D50) — next session diagnoses D51, the main-DL texture-upload crash)
|
||||
# Handoff brief — GoldenEye 007 PC port (Phase 2: Session H — D69 BG/stan stage loading)
|
||||
|
||||
_Paste-ready briefing. Authoritative detail lives in `AGENTS.md` and
|
||||
`docs/PCPortResearch.md` (§F/D43–D51 findings); this is the summary + the
|
||||
immediate tasks._
|
||||
`docs/PCPortResearch.md` (§F/D59–D69 findings, §G status, §H procedure); this
|
||||
is the summary + the immediate tasks._
|
||||
|
||||
## Your job
|
||||
|
||||
- **Session C (next): diagnose + fix D51** — SIGSEGV in `import_texture_i8`
|
||||
via a main-DL G_TEXRECT lazy tile-0 upload (frame 5). Recipe in Task 1.
|
||||
Then build the pixel assert (Task 2), run a soak, commit at each milestone
|
||||
(`PC port: phase 2 — <what>`). The goal: get past frame 5 with actual model
|
||||
geometry on screen, provable by pixel count.
|
||||
- **Session H:** make the first stage (BUNKER1) load and render. The game now
|
||||
plays the entire ~3.5-minute intro at ~59 fps and crashes in
|
||||
`load_bg_file` because BG-file data is N64 big-endian (D69). This is the
|
||||
"next asset type" milestone: reverse-engineer GE's bg `.seg` +
|
||||
`Tbg_*_stanZ` formats from the decompiled consumers, then convert/fix them
|
||||
for PC. Work agentically: decode → implement → build (~5 s) → verify →
|
||||
commit at each sub-milestone (document findings as D6x in §F).
|
||||
|
||||
Work agentically — fix → build (~5 s) → verify → commit at each milestone.
|
||||
## Current state (verified this session)
|
||||
|
||||
- **Build GREEN.** `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`.
|
||||
- **D59–D68 all resolved** (details in PCPortResearch.md §F). Highlights:
|
||||
- **D59/D64** — the old "GL DLL" crash was an unbounded blood-RLE write
|
||||
(sentinel `die_blood_image_end` lost its N64 section adjacency on PC)
|
||||
clobbering the gun-barrel sub-DL. Sentinel now defined as one-past-the-
|
||||
end of the array under PORT.
|
||||
- **D65/D65b** — `enum HEADS`/`BODIES` sentinels: N64 semantics were signed
|
||||
(`HEAD_FIXED == -1`); PC GCC 16 picks unsigned underlying types, deleting
|
||||
the guard branches. Negative literals under PORT restore the bit pattern
|
||||
with signed semantics.
|
||||
- **D66** — `romCopyAligned` + ramrom replay did s32 pointer math; targets
|
||||
in `.bss` > 4 GiB truncated to wild addresses. 64-bit version under PORT,
|
||||
returns `void *`.
|
||||
- **D67** — `struct image_entry` re-laid (all-u32 bitfields, `dataoffset:24`
|
||||
first) so texLoad's raw word read + chrprop.c's 8-byte stride agree;
|
||||
sizeof == 8 on both targets.
|
||||
- **D68** — Globalimagetable BE→LE fixup (`port/src/gimgfixup.c`): bswap the
|
||||
IMAGESEG-marked Gfx w1 words (17 DLs) + `sImageTableEntry.index` (32
|
||||
tables, counts from D39 layout) after texReset's romCopy; marker scan in
|
||||
`texLoadFromDisplayList` moved to bytes 6..7 under PORT; compiled
|
||||
globalDL shadows synced after texLoad (`gimgSyncCompiledGlobalDLs`).
|
||||
- **Runtime:** boots → intro music → full intro (logo, gun barrel w/ Brosnan,
|
||||
cast screen) at ~59 fps → **crash in `load_bg_file` (bg.c:830)** on first
|
||||
stage load. EXCEPTION 0xc0000005, FAULT ADDR ≈ header buffer + 0x500000F.
|
||||
- **Committed at D68** (see git log). TEMP diagnostics from the D51–D69
|
||||
sessions are still in the tree — strip list in Task 3.
|
||||
|
||||
## The blocker, precisely (D69)
|
||||
|
||||
`load_bg_file` (src/game/bg.c:800+) for level BUNKER1:
|
||||
1. `obLoadBGFileBytesAtOffset("bg/bg_sev_all_p.seg", header, 0, 0x40)` —
|
||||
**works**. `&fileentry->hw_address[offset]` == `hw_address + offset`;
|
||||
compiled asset symbols are absolute cart addresses (e.g.
|
||||
`bg_sev_all_p_seg` @ 0x10438660) into the ROM mapped at CART_BASE, and the
|
||||
PI shim memcpys from there. GE_D69 probe confirms: idx=1, rom_size=0x10DF0,
|
||||
hw=0x10438660.
|
||||
2. `((s32 *)ptr_bg_data)[1]` — **the bug**. ROM bytes at +4 are
|
||||
`0F 00 00 14` (BE value 0x0F000014). N64: BG_SEG_TO_PTR folds
|
||||
`off + 0xF1000000` → file offset 0x14. PC reads LE 0x1400000F → pointer
|
||||
~5 MB past the stack buffer → fault reading `[1].pPointTableBin`.
|
||||
3. Same class applies to: every bg_room_data record (more 0x0Fxxxxxx offsets,
|
||||
pPointTableBin at record+0x28 per the crash disasm), the rest of the .seg
|
||||
header/tables, and the whole `Tbg_sev_all_p_stanZ` geometry file
|
||||
(stanDetermineEOF/stanLoadFile in stan.c).
|
||||
|
||||
**Strategy options** (decide after decoding; AGENTS.md prefers offline):
|
||||
- **(a) Offline sidecar conversion** (the D43/Plan-B pattern): a tools_pc
|
||||
script converts all bg/*.seg + Tbg_*_stanZ per region into one image +
|
||||
manifest, served through the existing load path via a port-layer table
|
||||
patch (cf. `port/src/pcmodels.c`). Pros: matches established pattern, no
|
||||
runtime cost, regenerable per region. Cons: must fully decode both formats.
|
||||
- **(b) Runtime port-layer fixup** after each load (hook where the bytes land).
|
||||
Same format knowledge; smaller initial step if only a few fields matter for
|
||||
first-frame render — but stage geometry is deep; likely most fields end up
|
||||
needing conversion anyway.
|
||||
|
||||
**Caveat:** PD's `port/src/preprocess/filebg.c` describes a *different* BG
|
||||
format (zipped multi-section: primary/section1/2/3 headers). GE's .seg files
|
||||
are raw with segment-0x0F internal offsets. Same engine family ≠ identical
|
||||
format — use PD only as an approach reference; validate every field against
|
||||
GE's own consumers (bg.c, stan.c, and the N64 disasm where in doubt).
|
||||
|
||||
## Read first (authoritative, in order)
|
||||
|
||||
1. `AGENTS.md` — non-negotiables, critical files, build, verification ritual.
|
||||
2. `docs/PCPortResearch.md` **§F/D50** (what the last session did, incl. the
|
||||
texCopyGdls byte proof), **D51** (current blocker + GE opcode facts), then
|
||||
D49/D48/D47 (sidecar contract — still ground truth for regeneration).
|
||||
3. The crash-chain code: `port/fast3d/gfx_pc.cpp` (`gfx_run_dl` G_TEXRECT case
|
||||
~line 2457, `import_texture*` ~600–950, `seg_addr` ~2282,
|
||||
`gfx_sp_moveword` ~2290s), `src/game/tex.c` (texCopyGdls + texLoadFromGdl).
|
||||
2. `docs/PCPortResearch.md` **§F/D69** (blocker + strategy), then D68
|
||||
(Globalimagetable fixup — the template for BE data in this segment family),
|
||||
§G status, and the environment reminders at the end of §H.
|
||||
3. The consumers: `src/game/bg.c` (`load_bg_file` ~line 795+, levelinfotable
|
||||
line 183, BG_SEG_TO_PTR in bg.h:23), `src/game/stan.c` (stanLoadFile /
|
||||
stanDetermineEOF), `src/game/ob.c` (obLoadBGFileBytesAtOffset — note the
|
||||
TEMP D69 probe), `assets/obseg/file_resource_table.inc.c` (name→symbol map).
|
||||
4. Reference for the offline pattern: `tools_pc/d43_emit.py` +
|
||||
`port/src/pcmodels.c` + romdata cart-extension plumbing.
|
||||
|
||||
## Non-negotiables (the ones that bite)
|
||||
- N64 build files (`Makefile`, `tools/`, `rsp/`, `ld/`) — never touch.
|
||||
- **Game logic is unmodified.** Hardware deps go in `port/`. Narrow exception:
|
||||
mechanical, semantics-preserving ABI/layout fixes forced by the 32→64-bit
|
||||
transition (embedded pointers → u32 + cast at use; PC-guarded pool sizing /
|
||||
idiom branches; UB-exposure seeds), each documented as a D3x/D50 finding.
|
||||
No behavior changes. The N64 path stays verbatim under `#else`.
|
||||
- Region macros in `CMakeLists.txt` must mirror the N64 Makefile exactly.
|
||||
## Task 1 — decode the formats
|
||||
|
||||
## Current state
|
||||
- **Plan B is EXECUTED (D50, committed):** all 512 NTSC model files converted
|
||||
offline by `tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin`
|
||||
+ `manifest.csv` (single concatenated RZ image, 16-aligned offsets, decimal
|
||||
manifest); port plumbing in `port/src/pcmodels.c` (+ `port/include/pcmodels.h`,
|
||||
romdata.c/h cart-extension); one-shot `pcmodelsPatchTable()` + poolRemaining=0
|
||||
reset hook in `load_object_fill_header` (objecthandler_2.c, #ifdef PORT).
|
||||
Boot log: `[INFO] pcmodels: table patched (512 model entries)`.
|
||||
- **Runtime C fixups landed (D50.3/D50.4):** language-bank BE offset tables
|
||||
(`romdataFixupLangBank`, called from language.c after each load + lazily in
|
||||
the langGetJpnCharPixels paths) and font re-layout
|
||||
(`romdataFontPcSize`/`romdataFixupFont` in textrelated.c). These are
|
||||
load-time C fixups, NOT offline sidecars — small blobs on different load
|
||||
paths; fine to keep runtime.
|
||||
- **Legal-screen UB seed (D50.5):** front.c `constructor_menu00_legalscreen`
|
||||
read an uninitialized pointer (N64 survived by luck); PORT branch seeds it
|
||||
with `legalpage_text_array`. Identical lookat.
|
||||
- **First model-render crash fixed (D50.6):** `texCopyGdls` copied only w0 of
|
||||
each 16-byte Gfx slot on PC (the union's trailing `long long` is 8 bytes),
|
||||
so the compacted GDLs kept stale mempool w1s → garbage G_SETTIMG → OOB in
|
||||
`import_texture_rgba16`. Byte-proven against PlegalpageZ (sidecar w1
|
||||
0x050012C8 vs RAM garbage 0xACAAB819 at file offset 0x2488). Fixed with a
|
||||
full-slot copy under #ifdef PORT. **Model GDLs now execute to completion.**
|
||||
- **Current blocker — D51:** SIGSEGV on frame 5 in `import_texture_i8`
|
||||
(gfx_pc.cpp:782) via `G_TEXRECT` (main DL offset +0x260) → lazy upload of
|
||||
RDP tile 0 with an invalid/stale source. See Task 1. Frames 1–4 render
|
||||
successfully in every run (deterministic).
|
||||
1. Dump BUNKER1's .seg header + first records from the ROM (cart 0x10438660,
|
||||
size 0x10DF0) and map each word to its consumer in bg.c:
|
||||
- word0 = 0; word1 = room-fileposition-list offset (0x14); word2/word3 =
|
||||
more table offsets (0x334 / 0x32C).
|
||||
- Follow `ptr_bgdata_room_fileposition_list[1].pPointTableBin` etc. into
|
||||
bg_room_data (bg.h) and note every offset field + its base
|
||||
(BG_SEG_TO_PTR = base + off + 0xF1000000, i.e. stored as 0x0Fxxxxxx).
|
||||
2. Do the same for `Tbg_sev_all_p_stanZ` (cart 0x1087C3F0, size 0x3DD0):
|
||||
StanPrefixRecord + whatever stanLoadFile walks. Cross-check struct field
|
||||
offsets against the N64 disasm of the consumer functions where the decomp
|
||||
is ambiguous.
|
||||
3. Write the format notes into PCPortResearch.md §F/D69 (or a new §I) as you
|
||||
go — they are the converter spec.
|
||||
|
||||
## Task 1 — D51 diagnosis (the immediate task)
|
||||
## Task 2 — implement + verify
|
||||
|
||||
The failing blit: 32×48 px screen rect (236,92)–(268,140), tile 0, i8 format.
|
||||
G_TEXRECT carries no source address — it uses RDP texture-memory tiles; the
|
||||
fault is in `import_texture(tile=0)` reading `rdp.textures[0]`'s stored source.
|
||||
Work the suspects in order:
|
||||
1. Implement the chosen conversion/fixup; wire it in (sidecar table patch or
|
||||
load hook).
|
||||
2. Verify: stage loads without fault, then renders frames (watch for the next
|
||||
BE field class surfacing at render time — vertex tables, DL pointers,
|
||||
texture refs inside stan data; expect a few iterations, D43 had ~10).
|
||||
3. Once a stage renders: pixel-assert soak (PPM dump + tools_pc/pixcount.py),
|
||||
then move on (next stages use the same files pattern — make sure all 20+
|
||||
levels' bg/stan files are covered, not just BUNKER1).
|
||||
|
||||
1. **Dump live (post-mortem dumps FAIL — 0x70052e80 reads "Cannot access
|
||||
memory" after the crash).** gdb launch mode:
|
||||
```
|
||||
gdb -batch \
|
||||
-ex "handle SIGSEGV stop" \
|
||||
-ex "break import_texture_i8" \
|
||||
-ex run \
|
||||
-ex "print rdp.textures[0]" \
|
||||
-ex "up 4" \
|
||||
-ex "print cmd" -ex "print dListStart" \
|
||||
-ex "x/256wx <dListStart>" \
|
||||
./build-pc/ge007.x86_64.exe
|
||||
```
|
||||
(Adjust `up` depth to land in `gfx_run_dl`; the DL start may not be
|
||||
0x70052e80 every run — it's a mempool bump allocation. Alternatively break
|
||||
at `osSpTaskStartGo` and read `t->t.data_ptr` / `data_size`.)
|
||||
2. **Find the G_SETTIMG (w0 top byte 0xFD) slots in the dumped main DL** and
|
||||
decode each w1 through the seg_addr rules (raw 0x05xxxxxx → seg5+low24;
|
||||
marked LSB → segmentPointers[seg]+addr; unmarked <0x10000000 →
|
||||
segmentPointers[seg]+low24; <0x800000 → +0x80000000; else passthrough).
|
||||
Also dump `segmentPointers[]` at the breakpoint. If a SETTIMG w1 resolves
|
||||
to an OOB/invalid pointer → that's the bug (converter remap miss, or a new
|
||||
address class seg_addr doesn't handle).
|
||||
3. **If no main-DL SETTIMG sets tile 0 before the G_TEXRECT:** the tile is
|
||||
stale/uninitialized — check whether the game expects a tile set by an
|
||||
earlier frame's DL (fast3d `rdp` persistence across frames) or by a sub-DL
|
||||
that never ran.
|
||||
4. **If the source pointer is valid but points at wrong-format/wrong-size
|
||||
data:** the game may point it at a ROM/ramrom image needing a port fixup
|
||||
(D50.3/D50.4 class) — check what asset lives there and how N64 PD handles
|
||||
the analogue.
|
||||
5. **Find who emits the G_TEXRECT** in game code: grep `src/game/` for
|
||||
`gSPTileRect\|G_TEXRECT\|gSPTextureRectangle` (likely image.c / front.c /
|
||||
title.c — legal-screen or logo 2D blit). Reading the emitter tells you
|
||||
which texture it means and where its source should come from.
|
||||
## Task 3 — strip TEMP diagnostics before committing
|
||||
|
||||
Record findings as **D51.x** in §F; fix in `port/` where possible (seg_addr /
|
||||
tile setup), PORT-guarded game-code only if ABI/layout-forced. Rebuild,
|
||||
confirm the crash moves past frame 5, then Task 2.
|
||||
Env-gated probes to remove (grep `GE_D` + `TEMP D`):
|
||||
- ob.c: D69 probe (both obLoadBGFileBytesAtOffset versions) + its stdio/stdlib
|
||||
includes.
|
||||
- image.c / image_bank.c: D66 texLoad probe if still present.
|
||||
- gfx_pc.cpp: D59 seg probes, D63 dram-branch/modelRenderNodeGundl/rspGfxTaskStart.
|
||||
- blood_animation.c: D63 g_GfxMemPos probe.
|
||||
- front.c: D63 menu probes (d63MenuProbe/After + call sites).
|
||||
- rsp.c, model.c, initanitable.c, language.c, title.c, ramromreplay.c,
|
||||
n64stubs.c, libultra.c, romdata.c: leftover TEMP blocks from D51–D66
|
||||
(D51 vi-post logging, D54 music, D56 watch, D57 pool, D60/D61 DMA logging —
|
||||
decide per item whether the guard stays permanent; D60's target validation
|
||||
is a permanent safety net and should stay).
|
||||
- Scratch files at repo root: `run_*.log`, `d62mesg.log`, `all.txt`, `b3.c`,
|
||||
`btest*.c`, `buildtest.txt`, `err.txt`, `gcout.txt`, `preproc.txt`,
|
||||
`vsize.c/.exe`, `scratch-logs/` — delete (add to .gitignore if they recur).
|
||||
|
||||
## Task 2 — pixel assert + soak (build it now — dev-loop item #1)
|
||||
## Standing procedure (per AGENTS.md)
|
||||
|
||||
Env-gated framebuffer PPM dump every N frames in `port/video.c`
|
||||
(e.g. `GE_PCDUMP=1` → `build-pc/frame_%05d.ppm`) + `tools_pc/pixcount.py`
|
||||
(counts non-clear-color pixels; diffs two captures). Milestone: pixel count
|
||||
rises above a small threshold and stays up through a 30 s+ soak. Screenshot
|
||||
diffs against Mupen64Plus (verified No-Intro dump) are the ground truth for
|
||||
"looks right". Commit: `PC port: phase 2 — D51 …` then `… pixel assert +
|
||||
soak`.
|
||||
- N64 build untouched; game **logic** unmodified — narrow exception for
|
||||
pointer-width/ABI-only edits under #ifdef PORT, each documented in
|
||||
PCPortResearch.md §F/D3x style.
|
||||
- Verification ritual after any build-affecting change: undefined symbols,
|
||||
duplicates, syntax (`./build-pc.sh` is the final word).
|
||||
- **Run the game from the repo root** (`cd REPO_ROOT &&
|
||||
./build-pc/ge007.x86_64.exe`) — running from build-pc/ picks up the wrong
|
||||
ROM (baserom.u.z64) and misses the sidecar.
|
||||
- `Gfx.words.w0/w1` are both uintptr_t (16-byte Gfx on PC); ROM-copied N64
|
||||
Gfx DLs stay 8 bytes in memory — walk them at 8-byte stride when scanning.
|
||||
- N64 CPU/RDP data in ROM is big-endian; any raw `*(u32*)` read of ROM-copied
|
||||
buffers needs a bswap under PORT (D68 pattern) or offline conversion.
|
||||
|
||||
## Task 3 — next asset type (after D51 + soak)
|
||||
## Environment reminders
|
||||
|
||||
Tbg_*_stanZ stage backgrounds (different format/path than models). SAME
|
||||
offline pattern (D48): pin the format with Python over ROM ground truth, emit
|
||||
+ validate offline, serve through the existing load path. PD's
|
||||
`port/src/preprocess/filebg.c` is the per-field reference (§2.4) — but same
|
||||
family ≠ identical format; validate every field against GE headers + ROM.
|
||||
|
||||
## Verified facts (sidecar spec — ground truth; do NOT re-derive)
|
||||
|
||||
**Sidecar serving (D50):**
|
||||
- Sidecars live in `data/pcmodels-<region>/` = **one concatenated
|
||||
`pcmodels.bin` + `manifest.csv` (`name,offset,size`, DECIMAL — C parser
|
||||
uses strtol base 10; rows in file_resource_table.inc.c order)**. NOT
|
||||
per-file .bin. Regenerate: `python tools_pc/d43_emit.py [ntsc-final|
|
||||
pal-final|jpn-final|--check-only]` (needs the region ROM in data/).
|
||||
- RZ format = `[0x11 0x72][raw deflate]`. Python decompress for inspection:
|
||||
`zlib.decompress(raw[2:], -15)` after checking the 2-byte header.
|
||||
- pcmodels.c copies the whole blob to `[CART_BASE+romSize, …)` (cart base
|
||||
0x10000000, romdataInit reserves romSize+sidecarTotal;
|
||||
romdataCartAddrValid accepts the extension) and patches
|
||||
`file_resource_table[i].hw_address = cartBase+romSize+off` +
|
||||
`resource_lookup_data_array[i].rom_size = C_pc` per manifest row — one-shot,
|
||||
from `load_object_fill_header` (after obInit has run). The existing
|
||||
romCopy/decompressdata path then serves PC bytes unmodified and sets
|
||||
poolRemaining = D_PC.
|
||||
- **Inspecting a loaded file offline:** find the name in manifest.csv →
|
||||
decompress that slice → it is exactly what lands at the file base in RAM
|
||||
(verified byte-for-byte for PlegalpageZ). GDLs are at the tail of the image
|
||||
in visit order; `x/Nwx <filebase+off>` in gdb compares directly.
|
||||
|
||||
**Format / layout:**
|
||||
- All 512 C*/G*/P*Z model files decompress fine; each file starts directly with
|
||||
the switches array (no in-file header — NS/NT come from the exe
|
||||
`ModelFileHeader` record via `objheader->numSwitches/numtextures`).
|
||||
- PC file layout: `[switches NS×8B][texconfigs NT×12B][root node @ 8NS+12NT]…`;
|
||||
all embedded pointers are vma `0x05xxxxxx` (mask 0xFFFFFF). N64 was
|
||||
`[NS×4][NT×12][root @ 4NS+12NT]`.
|
||||
- **Tiling invariant verified, 0 failures in all 512 files**
|
||||
(`tools_pc/d43_invariants.py`): the image is exactly tiled by {switches,
|
||||
textures, nodes(48B PC), records(PC_REC per opcode), vertex arrays(nv×16B),
|
||||
collision arrays(nc×16B), PointUsage (numVertices s16 entries), GDLs (each
|
||||
extends to the next GDL start or D)}. Records INTERLEAVE with vertex arrays
|
||||
in character bodies — do not assume "all records before bulk".
|
||||
- **ModelNode = 24B N64 / 48B PC** (u16 op + 2 pad + 5 ptrs: Data@4/8,
|
||||
Parent, Next, Prev, Child). Record sizes N64/PC: OP01 0x10/0x18, OP02
|
||||
0x1C/0x28, OP04 0x14/0x28, OP08 0x10/0x18, OP09 0x24/0x30, OP10 0x1C/0x1C,
|
||||
OP12 0x28/0x30, OP13 0x20/0x30, OP15 0x1C/0x1C, OP17 0x20/0x28, OP18
|
||||
0x08/0x10, OP21 0x14/0x14, OP22 0x10/0x20, OP23 0x02/0x02, OP24 0x20/0x40
|
||||
(compiler-verified by `tools_pc/d43_layoutprobe.c`). Opcodes present:
|
||||
1,2,4,8,9,10,12,13,15,18,21,22,23,24.
|
||||
- **Endianness:** f32 fields BE in ROM → bswap; every u16/s16 record scalar
|
||||
BE → bswap16; vertex-array s16s BE → bswap16 (x,y,z,index,s,t); TextureID
|
||||
u32 → bswap32. Padding/reserved zeroed.
|
||||
- **Vertex arrays stay 16B-stride** (GDL gSPVertex hardcodes it). Collision
|
||||
vertices carry LinkedTo@8 = vma of a ModelNode — remapped.
|
||||
- **GDLs:** N64 8B BE word pairs → PC 16B LE slots: `w0=bswap32(rom_w0);
|
||||
w1=bswap32(rom_w1)`; **no LSB set** (fast3d's seg_addr handles unmarked +
|
||||
raw-0x05). Opcode-aware remap: only {G_VTX=0x04, G_SETTIMG=0xFD,
|
||||
G_LOADBLOCK=0xF3} carry addresses in w1; seg-5 low24 remapped via the region
|
||||
map. **0xB1 (G_TRI4) w1 is packed 8-bit vertex indices — NEVER an address.**
|
||||
G_VTX w0 is GBI1-style (`04<<24 | dst<<16 | 16·n`) — bswap only.
|
||||
- **Marker format:** top byte of w0 = 0xC0; type = `w0 & 7`; texnum =
|
||||
`w1 & 0xfff`. texLoadFromGdl expands markers into RDP commands (that's the
|
||||
compaction growth); default case copies the full 16B slot as-is — **the
|
||||
sidecar must contain final correct w1 values** (no runtime remap in
|
||||
texLoadFromGdl).
|
||||
- **PROMOTE contract:** promoted pointers emitted as zero-extended u64 with
|
||||
low32 = VMA `0x05xxxxxx`; `PROMOTE` (`(u32)var + diff`, vma 0x5000000) works
|
||||
unmodified. GDL Primary/Secondary NOT promoted (raw VMAs); BaseAddr=0 (game
|
||||
overwrites with fileramaddr).
|
||||
- **texCopyGdls contract (D50.6):** on PC it must copy the FULL 16-byte slot
|
||||
(`*arg1 = *arg0` under #ifdef PORT) — the N64 `long long` idiom copies only
|
||||
w0 on x86-64 and poisons the compaction scratch. texLoadFromGdl then reads
|
||||
the scratch and writes back at the original GDL offsets; final fileSetSize
|
||||
shrinks the bank chunk.
|
||||
|
||||
**GE opcode space (include/PR/gbi.h, non-F3DEX numbering — verified D51):**
|
||||
- DMA: G_SPNOOP=0, G_MTX=1, G_MOVEMEM=3, G_VTX=4, G_DL=6, G_SPRITE2D_BASE=9.
|
||||
- IMM (G_IMMFIRST=−65): TRI1=0xBF, CULLDL=0xBE, POPMTX=0xBD, MOVEWORD=0xBC,
|
||||
TEXTURE=0xBB, SETOTHERMODE_H=0xBA, SETOTHERMODE_L=0xB9, ENDDL=0xB8,
|
||||
SETGEOMETRYMODE=0xB7, CLEARGEOMETRYMODE=0xB6, LINE3D=0xB5, RDPHALF_1=0xB4,
|
||||
RDPHALF_2=0xB3, TRI2=0xB2, **G_TRI4(GE)=0xB1** (gbi_extension.h).
|
||||
- RDP pass-through: G_SETTIMG=0xFD, G_LOADTXC=0xFE, G_SETCIMG=0xFF,
|
||||
**G_TEXRECT=0xE4 / G_TEXRECTFLIP=0xE5** (GE-specific values — NOT libultra's
|
||||
0x46/0x45), plus VI2D=0xF5 etc.
|
||||
- `gSPSegment(pkt,seg,base)` = `gMoveWd(pkt,G_MW_SEGMENT,seg*4,base)` →
|
||||
w0=(0xBC<<24)|(seg*4<<8)|6, w1=data. fast3d gfx_sp_moveword stores data as-is
|
||||
when ≥0x800000 else +0x80000000.
|
||||
- Main DL is game-built per frame with gSP* macros (16B slots) — NOT from a
|
||||
sidecar. Segment table: seg0=0, seg1=0x7029F800-ish, seg3=render_pos,
|
||||
seg4=runtime vtx buffer, seg5=file base (set per object).
|
||||
|
||||
**Load path / compaction (unchanged by D50 — see D47/D46/D48 for full detail):**
|
||||
- ALL model loads go through `load_object_fill_header()`; dst≠0 →
|
||||
`_fileNameLoadToAddr` (caller's buffer), else `_fileNameLoadToBank` (fresh
|
||||
bank when poolRemaining==0 — the hook resets it every time).
|
||||
- Compaction: texCopyGdls mirrors [G,D) to tail scratch [B−D+G,B);
|
||||
texLoadFromGdl rewrites each GDL in place with marker expansion; final size
|
||||
= D + 16·Σ(K_t−1) worst case (P_est model — the emit pass's cross-checks
|
||||
encode it for every dst!=0 buffer and chain).
|
||||
|
||||
## Helper scripts (tools_pc/, tracked; run from repo root)
|
||||
- `d43_emit.py` — **THE emit pass (D50.1):** converts all 512 files, validates
|
||||
round-trip + tiling + buffer fits, writes pcmodels.bin + manifest.csv.
|
||||
`--check-only` runs validation without writing. First stop for any
|
||||
"sidecar looks wrong" question.
|
||||
- `d43_layoutprobe.c` — compiler probe that re-verifies every PC record/node/
|
||||
header size (run with the exact CMake flags; compare against d43_emit.py's
|
||||
tables after any bondtypes.h change).
|
||||
- `d43_convert.py` — reference layout + validator (D47); foundation of the
|
||||
emit pass.
|
||||
- `d43_invariants.py` / `d43_gdlorder.py` / `d43_sizes*.py` / `d43_chainbound.py`
|
||||
/ `d43_cover.py` / `d43_lutscan.py` / `d43_seg5*.py` / `d43_gdl*.py` — the
|
||||
investigation suite (all pass on NTSC).
|
||||
- Gotcha: MODELFILEHEADER's name is field a[0] of the macro args (NOT the
|
||||
filename); nsnt lookup keys by header symbol (`armourguard`), not file base
|
||||
name (`CarmourguardZ`).
|
||||
|
||||
## Dev-loop speedups (priority order)
|
||||
1. **Frame capture + pixel assert** — NOW (Task 2): env-gated PPM dump every N
|
||||
frames in `port/video.c` + `tools_pc/pixcount.py`. Turns "the scene contains
|
||||
actual model geometry" into an assert and makes the 30 s soak scriptable.
|
||||
2. **`tools_pc/sym.sh`** — tiny wrapper: PC or rel offset → add image base
|
||||
0x140000000 → `addr2line -f -C`. Re-verify the image base with `info
|
||||
address` after any link-layout change.
|
||||
3. Sidecar inspection is now trivial offline (manifest + zlib, see Verified
|
||||
facts) — don't gdb the game for "geometry looks wrong" in a model file.
|
||||
4. D44 automatic backtraces — DONE; `ge007.crash.log` is the first stop.
|
||||
|
||||
## PD port as standing reference (always available)
|
||||
`PD_PORT_CHECKOUT` is the Perfect Dark PC port — same Rare
|
||||
engine family, the **standing reference** for this project (noted in
|
||||
AGENTS.md). Consult it whenever a remaining work item has a PD analogue:
|
||||
- **Phase 2 asset conversion:** `port/src/preprocess/` (~4,100 lines) —
|
||||
per-ROM-segment N64→PC layout converters hooked from their `romdata.c`.
|
||||
`filemodel.c` = D43 near-analogue (PD converts at LOAD time — we convert
|
||||
OFFLINE for speed, same field rules apply); `filebg.c` (expected follow-on),
|
||||
`segaudio.c`, `filelang.c`, `filetiles.c`, `filepads.c`, `filesetup.c`,
|
||||
`gbi.c`, `segfonts.c`.
|
||||
- **Phase 3/4 (copy-and-adapt; our stubs were scaffolded for this):**
|
||||
`port/src/mixer.c` (722 lines vs our 31), `input.c` (1,551 vs 48 —
|
||||
SDL_GameController backend; remap buttons to GE's scheme), `fs.c` (294 vs 81
|
||||
— file-backed save dir).
|
||||
- **Caveats:** copy port-layer files only (their game code was modified for
|
||||
PC); same family ≠ identical format — validate every conversion per-field
|
||||
against GE headers + ROM ground truth. Full audit: §2.4.
|
||||
|
||||
## Standing procedure (you will hit more of these)
|
||||
Every ROM-serialized struct with a pointer field faults the same way once you
|
||||
reach more asset loading. The D32 fix procedure (doc §H): at the fault,
|
||||
`ptype /o <Struct>`; if a pointer field's offset/size diverges from its N64
|
||||
offset comment, change it to `u32` + cast at the use sites; verify the
|
||||
load-time rebase yields valid V1 addresses (< 0x80000000); **also check the
|
||||
ROM bytes' endianness per-field (D33 rule: u32 bswap32 / u16 bswap16 / u8
|
||||
identity) and any fixup loop's pointer stride on x86-64**; for packed struct
|
||||
trees use the D37 re-layout pattern; for address-arithmetic idioms over exe
|
||||
symbols (D39/D42 class) guard a PC branch that reproduces the N64 32-bit value
|
||||
exactly. Rebuild; confirm boot advances. Log each as D5x in §F and note it in
|
||||
AGENTS.md phase status. **For asset FORMAT work, prefer the offline pattern
|
||||
(D48): pin the format with Python over the ROM ground truth, emit + validate
|
||||
offline, serve through the existing load path — runtime C conversion only as
|
||||
fallback.** Also: any code that copies Gfx slots must use full-struct copies on
|
||||
PC (D50.6) — never the `long long` union member.
|
||||
|
||||
## Environment (do not rediscover these)
|
||||
- MSYS2/MinGW tools are in `/c/msys64/mingw64/bin/` — **not** on PATH. Prefix
|
||||
every shell: `export PATH="/c/msys64/mingw64/bin:$PATH"` (build-pc.sh needs
|
||||
it for cmake too).
|
||||
- Build: `./build-pc.sh ntsc-final` (incremental; full rebuild ~5 s on the
|
||||
16-core dev box). ROM at `data/ge007.ntsc-final.z64` (= `baserom.u.z64`,
|
||||
byte-identical to the No-Intro good dump, MD5
|
||||
`70c525880240c1e838b8b1be35666c3b`).
|
||||
- **Sidecars are gitignored (data/).** If `data/pcmodels-ntsc-final/` is
|
||||
missing on a fresh checkout, regenerate: `python tools_pc/d43_emit.py
|
||||
ntsc-final`. The game boots without them (warn + ROM-only mode) but model
|
||||
loads fail.
|
||||
- The ROM is **mapped at cart base 0x10000000** on PC; sidecar image follows
|
||||
at [CART_BASE+romSize, …). All game RAM lives in the V1 view at 0x70xxxxxx
|
||||
(< 4 GiB) — s32 truncation of game-RAM pointers is lossless.
|
||||
- gdb: **launch** mode only (`gdb -batch -ex "handle SIGSEGV stop" -ex run …`
|
||||
— attach fails with error 87). Symbolicate offline with `addr2line -e
|
||||
build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`. Image base
|
||||
0x140000000. **Post-mortem memory dumps of game RAM can fail** ("Cannot
|
||||
access memory") — dump at a live breakpoint instead (hit this with the main
|
||||
DL in D51).
|
||||
- Many init functions use a fake RBP — compute stack offsets from the **entry
|
||||
RSP**, not RBP.
|
||||
- Python on this box: no f-strings with nested quotes (< 3.12) — use `%`
|
||||
formatting.
|
||||
- Untracked scratch at repo root (all.txt, b3.c, btest*.c, buildtest.txt,
|
||||
err.txt, gcout.txt, preproc.txt, vsize.*) + `tools_pc/__pycache__/` are
|
||||
session scratch — do NOT commit them; use explicit pathspecs with git add.
|
||||
|
||||
## Definition of done
|
||||
- **Session C:** D51 diagnosed + fixed (finding in §F, crash moves past frame
|
||||
5), pixel assert built, 30 s+ soak clean with real geometry on screen.
|
||||
Committed + pushed (`PC port: phase 2 — <what>`). After that: Phase 2 makes
|
||||
it look right (fast3d CC/RM correctness vs `rsp/graphics/gmain.s` —
|
||||
GE-specific ground truth, no PD analogue), next asset type (Tbg_*_stanZ
|
||||
backgrounds), and Phase 3 adds sound + input (copy-and-adapt from the PD
|
||||
port).
|
||||
- MSYS2 tools: `export PATH="/c/msys64/mingw64/bin:$PATH"`. Build ~5 s.
|
||||
- Crash log `ge007.crash.log` now includes FAULT ADDR, STACK@RSP window, and
|
||||
module list — first stop for any fault. Symbolicate offline:
|
||||
`addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>`.
|
||||
- gdb is launch-mode only and far too slow for timing-dependent crashes —
|
||||
prefer env-gated probes + the crash log.
|
||||
- Image base 0x140000000; DRAM V1 @ 0x70000000, V2 @ 0x80000000 (8 MB each);
|
||||
CART_BASE = 0x10000000; model sidecar extends the cart region at
|
||||
0x10C00000. `osGetCount` advances at ~46.5525 ticks/µs (D52).
|
||||
- ROM: data/ge007.ntsc-final.z64 (byte-identical to the No-Intro good dump);
|
||||
sidecar: data/pcmodels-ntsc-final/ (gitignored, regenerate with
|
||||
tools_pc/d43_emit.py per region).
|
||||
+516
-65
@@ -1745,84 +1745,510 @@ Fix: `*arg1 = *arg0;` under `#ifdef PORT` (tex.c). Audit: tex.c is the only
|
||||
local 8-byte union); every other Gfx copy is full-struct. Post-fix:
|
||||
PlegalpageZ's sub-DLs execute to completion; crash moves on (D51).
|
||||
|
||||
**D51 (OPEN — current blocker: `import_texture_i8` SIGSEGV via main-DL G_TEXRECT lazy tile upload)**
|
||||
**D51 (RESOLVED — font `pixeldata` fixup wrote the pointer at the wrong offsets; frame-5 `import_texture_i8` SIGSEGV)**
|
||||
|
||||
Post-D50 run: frames 1–4 log, then SIGSEGV on frame 5 in
|
||||
`import_texture_i8` (gfx_pc.cpp:782, `addr[i]`) ← `import_texture(i=0,
|
||||
tile=0)` ← `gfx_sp_tri1(128,129,131,is_rect=true)` ←
|
||||
`gfx_draw_rectangle(236,92,268,140)` ← `gfx_dp_texture_rectangle` ←
|
||||
`G_TEXRECT` case (gfx_run_dl ~line 2472), main DL start 0x70052e80,
|
||||
failing opcode slot at +0x260 (0x700530e0). G_TEXRECT carries no source
|
||||
address — it blits from RDP texture-memory tiles; the fault is the **lazy
|
||||
upload of tile 0** whose stored source pointer is invalid (or the tile was
|
||||
never set up this frame → stale `rdp.textures[0]`). Suspects: (a) a
|
||||
main-DL G_SETTIMG (0xFD) earlier than +0x260 whose w1 seg_addr
|
||||
mis-resolves; (b) tile referenced before setup / across frames; (c) the
|
||||
game points the tile source at a ROM/ramrom image that needs a port fixup
|
||||
(D50.3/D50.4 class). **Investigation not started.** Note: after the crash,
|
||||
0x70052e80 is unreadable from gdb ("Cannot access memory") — dump live at a
|
||||
breakpoint, not post-mortem. GE opcode facts verified this session
|
||||
(include/PR/gbi.h, non-F3DEX numbering): G_IMMFIRST=−65; DMA G_MTX=1 /
|
||||
G_MOVEMEM=3 / G_VTX=4 / G_DL=6; IMM TRI1=0xBF … ENDDL=0xB8;
|
||||
GE extension G_TRI4=0xB1 (gbi_extension.h, 8-bit packed indices — NOT an
|
||||
address carrier); RDP pass-throughs include G_SETTIMG=0xFD,
|
||||
G_SETCIMG=0xFF, **G_TEXRECT=0xE4 / G_TEXRECTFLIP=0xE5** (GE-specific
|
||||
values, not libultra's 0x46/0x45). The main DL is game-built per frame with
|
||||
gSP* macros (16B slots); the segment table is set via
|
||||
The G_TEXRECT tile-0 upload's source was a **font glyph** (I8, loaded via
|
||||
`gDPLoadTextureBlock`). Root cause in `romdataFixupFont`
|
||||
(port/src/romdata.c): it wrote each char's pixel pointer at blob offsets
|
||||
d+20/d+24, but on PC `struct fontchar.pixeldata` is a **u64 at char offset
|
||||
+24** — the low word landed in padding and only half the pointer was
|
||||
written, so glyph sources resolved to stale/invalid addresses → OOB read in
|
||||
`import_texture_i8`. Fix: write the blob-relative offset into d+24 (low)
|
||||
and zero-extend d+28 (high). Verified: glyphs load, SIGSEGV gone. The old
|
||||
D51 hypothesis list (seg_addr / tile staleness) was wrong; its GE opcode
|
||||
facts remain valid reference material (G_IMMFIRST=−65; DMA G_MTX=1 /
|
||||
G_MOVEMEM=3 / G_VTX=4 / G_DL=6; IMM TRI1=0xBF … ENDDL=0xB8; GE extension
|
||||
G_TRI4=0xB1, 8-bit packed indices — NOT an address carrier; RDP
|
||||
pass-throughs G_SETTIMG=0xFD, G_SETCIMG=0xFF, **G_TEXRECT=0xE4 /
|
||||
G_TEXRECTFLIP=0xE5** — GE-specific values, not libultra's 0x46/0x45;
|
||||
width-1 stored in the 12-bit field, fmt I=4, G_TX_LOADTILE=7; main DL is
|
||||
game-built per frame with gSP* macros; segment table via
|
||||
`gMoveWd(G_MW_SEGMENT, seg*4, base)` → w0=(0xBC<<24)|(seg*4<<8)|6,
|
||||
w1=data; fast3d's gfx_sp_moveword stores data as-is when ≥ 0x800000 else
|
||||
+0x80000000.
|
||||
+0x80000000).
|
||||
|
||||
Environment notes for Task 2: standalone gcc needs
|
||||
**D52 (RESOLVED — `osGetCount` tick-rate mismatch → non-deterministic post-frame-2 hang)**
|
||||
|
||||
Plain runs sometimes hung after frame 2 (kernel watchdog: "no frame rendered
|
||||
for 3006180 ms") instead of crashing at frame 5. Root cause:
|
||||
port/src/libultra.c `osGetCount()` returned **microseconds** (1M/s), but GE's
|
||||
pacing assumes the N64 RSP counter rate ≈ **46.5525 ticks/µs**:
|
||||
`MAIN_LOOP_TICK_INTERVAL` = 387,937 ticks (boss.c: NTSC
|
||||
`INTERVAL_INTER_MATH - 2688U`; PAL
|
||||
`frameDelay*(CYCLES_PER_FRAME-6450)-(INTERVAL_INTER_MATH-3225)`), and
|
||||
`waitForNextFrame()` (frametiming.c) waits for
|
||||
`(elapsed+interval)/775875` ticks (NTSC frame = 775,875; PAL 931,050). At
|
||||
1M/s the steady-state 16,667 µs between retraces < 387,937 → bossMainloop's
|
||||
gate never passes → no DL built → hang. Frames 1–2 rendered only because
|
||||
stage loading took >388 ms of real time. Fix: `osGetCount()` returns
|
||||
`(u32)(((uint64_t)sysGetMicroseconds()*465525ull)/10000ull)` (rate derived
|
||||
from GE's own constants: 775875/16666.67µs = 931050/20000µs); wraps every
|
||||
~92 s like the HW counter. `osGetTime()` still returns µs (osSetTimer/
|
||||
OSTime). PD's port has the same µs implementation but lacks GE's cycle-based
|
||||
pacing gate — do not copy it blindly.
|
||||
|
||||
**D53 (RESOLVED: model RW-data pool addressing on PC; frame-5 SIGSEGV in `modelInitRwData`)**
|
||||
|
||||
Post-D51/D52, the frame-5 SIGSEGV moves to `modelInitRwData`
|
||||
(model.c ~6131): first BSP node → `movl $0x0,(%rax)` right after
|
||||
`call modelGetNodeRwData` — writing `visible=FALSE` through NULL. Two
|
||||
compounding PC layout bugs, both in the D32 class:
|
||||
|
||||
- **D53.1 (applied; necessary but not sufficient): `Model.datas` word
|
||||
stride.** `RwDataIndex` values are **4-byte word offsets** into the
|
||||
RW-data pool (`modelCalculateRwDataIndexes` accumulates
|
||||
`len += sizeof(record)/4`; pool = round16(numRecords×4) bytes, allocated
|
||||
in modelmgrInstantiateModel(WithAnim)). On N64 `&data[index]` with
|
||||
`union ModelRwData **data` strides 4 B; on PC it strides 8 B → every
|
||||
non-zero index addresses the wrong record. Fix (3 files, all #ifdef PORT):
|
||||
`Model.datas` → `u32 *datas` (bondtypes.h — layout unchanged: single
|
||||
pointer field); casts in model.c (`modelGetNodeRwData` local + return
|
||||
`(union ModelRwData *)&data[index]`, parent-walk
|
||||
`data=(u32*)tmp->RwDatas`, `modelAttachPart`) and propobj.c:7301/14343.
|
||||
- **D53.2 (root-caused; fix designed, NOT applied): `ModelSlot` /
|
||||
`AnimModelSlot` ↔ `Model` type-pun breaks on PC.** The game puns the slot
|
||||
structs and Model in both directions: `slot.unk08@8` ↔ `Model.obj@8` (the
|
||||
**in-use flag** — `modelInit`'s `objinst->obj = header` marks a slot),
|
||||
`slot.unk10@0x10` ↔ `Model.datas@0x10` (RW pool), `slot.unk02@2` ↔
|
||||
`rwdatalen@2`. On N64 all pointers are 4 B → offsets agree. On PC there is
|
||||
no pack pragma — natural alignment (probe-verified with the exact CMake
|
||||
flags, see vsize.c): **Model = 0xE8 B**: chr@8, **obj@0x10**,
|
||||
render_pos@0x18, **datas@0x20**, scale@0x28, attachedto@0x30, anim@0x40;
|
||||
the slot structs still have unk08@8 / unk10@0x10. (An earlier draft of
|
||||
this section said obj@0xC/datas@0x1C — that assumed packed layout and is
|
||||
wrong; the log evidence below only fits the natural-alignment offsets.)
|
||||
Consequences: (a) `modelInit` writes obj@0x10 — the free check reads
|
||||
unk08@8 (low word of Model.chr, never written) → slots are never marked in
|
||||
use → every model reuses slot 0; (b) the legal-screen logo model takes
|
||||
slot 0, then `update_menu00_legalscreen()` (front.c:1430) →
|
||||
`clear_model_obj(logoinst)` writes obj=NULL at @0x10..0x17 — exactly where
|
||||
`slot.unk10@0x10` lives; the next instantiation passes
|
||||
`modelmgrCanSlotFitRwdata` on stale Model.datas@0x20 (non-NULL) + real
|
||||
unk02=20 ≥ 17, then `rwdata = g_ModelSlots[i].unk10` = NULL →
|
||||
`modelInit(model, header, NULL)` → fault. **Log evidence** (TEMP D51 trace
|
||||
in model.c → d52rw.log):
|
||||
```
|
||||
INST model=0x7020ac48 header=0x140141c40 numRecords=0 rwdata=0x7020b0f8 rwdatalen=20 lvreset=0
|
||||
INST model=0x7020ac48 header=0x140142700 numRecords=17 rwdata=NULL rwdatalen=20 lvreset=0
|
||||
GND obj=0x7020ac48 datas=NULL idx=0 rwdatalen=-1 op=2 data=NULL res=NULL
|
||||
```
|
||||
(same Model address both times = slot reuse; 20 = MODEL_SPARE_RWDATALEN
|
||||
spare-slot pool.)
|
||||
- **D53.2 fix APPLIED and verified.** objecthandler.h under #ifdef PORT:
|
||||
both slot structs re-laid out on top of the PC struct Model — `unk02`@2,
|
||||
in-use marker `unk08`@**0x10** (low word of Model.obj), pool pointer
|
||||
`unk10`@**0x20** (Model.datas), then `char pad28[sizeof(struct Model)-0x28]`
|
||||
so each slot is exactly sizeof(Model) = 0xE8 (a full Model, including
|
||||
animInit's writes up to PC offset 0xE3, fits). Member names kept; N64
|
||||
layout verbatim under #else. model.c heap fallbacks bumped PORT-guarded:
|
||||
0x20 → sizeof(struct Model) (non-animated), 0xC0 → sizeof(struct
|
||||
AnimModelSlot) (animated). Verified with the d52rw.log probe: both INSTs
|
||||
get valid pools (rwdata=0x7020c778, non-NULL), all 17 records initialize,
|
||||
frame-5 modelInitRwData crash gone; game runs to ~frame 102 and past the
|
||||
Nintendo-logo transition. Note: the bump allocator's `pos` drifts with the
|
||||
new sizes → some slots land on 4-byte boundaries; misaligned 8-byte field
|
||||
stores are functionally fine on x86-64 (individual field accesses, no
|
||||
faults) — accepted without a PORT alignment bump in memp.c.
|
||||
|
||||
Size/offset probe: `vsize.c` at repo root (untracked scratch — delete before
|
||||
commit) prints sizeof + field offsets of Model / ModelSlot / AnimModelSlot /
|
||||
struct player; compile with the CMake include order (port/shim first, then .,
|
||||
include, include/PR, src, src/game, src/libultra, port/include) plus
|
||||
`-DPORT=1 -DAVOID_UB=1 -std=c11`. Re-run after any slot-struct edit.
|
||||
|
||||
Environment notes: standalone gcc needs
|
||||
`/c/msys64/mingw64/bin:/c/msys64/usr/bin` on PATH (cc1 fails SILENTLY without
|
||||
it — exit 1, zero diagnostics), and `-std=c11` is required (the CMake flag;
|
||||
under the default gnu23 `typedef s32 bool` in bondtypes.h breaks). Also:
|
||||
`include/stddef.h`'s body is `#if 0`'d — offsetof/size_t are unavailable in
|
||||
game TUs.
|
||||
game TUs (use pointer-difference arithmetic in probes).
|
||||
|
||||
**D54 (RESOLVED: cseq ALCMidiHdr endianness; audio-thread SIGSEGV in
|
||||
`__getTrackByte`)**
|
||||
|
||||
After D53, the first music load (M_INTROSWOOSH, seq 44) crashed the audio
|
||||
thread in `__getTrackByte`. Root cause: a decompressed compact-sequence file
|
||||
starts with `struct ALCMidiHdr` — 16 **big-endian** u32 trackOffset values +
|
||||
a BE u32 division. N64 reads them natively; on an LE host alCSeqNew() does
|
||||
not swap them (track-0 offset 0x44 becomes 0x44000000), builds "valid"
|
||||
curLoc pointers ~1 GB past the buffer, and the first track-byte read faults.
|
||||
The rest of the stream is byte-oriented (varlens, MIDI bytes, BE loop offsets
|
||||
assembled byte-by-byte) so only the 17 header words need fixing. Fix: port-
|
||||
layer `romdataFixupCseq(u8 *blob)` (port/src/romdata.c — bswap32 of the first
|
||||
17 u32s; declared in port/include/romdata.h), called under #ifdef PORT after
|
||||
each of the 3 `decompressdata` calls in musicTrack1Play/2/3 (src/music.c).
|
||||
Verified: __getTrackByte crash gone; full 60 s run (exit=124 timeout) with no
|
||||
audio fault, past the Nintendo-logo transition.
|
||||
|
||||
**D54b (RESOLVED: synthesizer param-slot sizing; audio-thread SIGSEGV in
|
||||
`alLoadParam`)**
|
||||
|
||||
Next audio crash: `alLoadParam` dereferenced a corrupted free-list slot. Root
|
||||
cause: alSynInit's "build the parameter update list" allocates
|
||||
`c->maxUpdates` slots of `sizeof(ALParam)` and the game type-puns those slots
|
||||
as several AL*Param structs. On N64 all of them are 0x1C bytes (one slot
|
||||
each); on x86-64 **ALStartParamAlt is 0x28** (two 8-byte pointers: next +
|
||||
wave) vs ALParam = 0x20 — every start-voice update wrote `wave` into the
|
||||
neighbouring slot, corrupting the free list. Fix in synthesizer.c alSynInit
|
||||
under #ifdef PORT: allocate `maxUpdates * sizeof(ALStartParamAlt)` and stride
|
||||
the init loop by that size (cast each slot to ALParam* for the next-pointer
|
||||
link); N64 verbatim under #else. Cost +8 B/slot × 0x80 slots = +1 KB vs
|
||||
MUSIC_ALLOCATION_BYTES — no alHeapAlloc failure observed. Verified:
|
||||
alLoadParam crash gone; full 60 s run clean.
|
||||
|
||||
**D55 (RESOLVED: RLE folder-menu background header endianness; SIGSEGV in
|
||||
`rle_expand_8bit`)**
|
||||
|
||||
After the audio fixes, the game ran to the gun-barrel intro (~frame 654,
|
||||
~23 s) and crashed in `rle_expand_8bit` (src/game/rle.c:30, the `*dst++`
|
||||
store). Caller: title.c `sub_GAME_7F008DE4` (the initializeGunBarrelIntro path)
|
||||
romCopies the asset at
|
||||
`unknown2` (romassets_<r>.s, NTSC cart 0x102A4D50, size 0x1A580) and RLE-
|
||||
decodes it into a 0x40400-byte buffer. Root cause: the asset is the title
|
||||
folder-menu background; its raw ROM header is **big-endian** `01 B8 01 2B`
|
||||
(w=440, h=299) + 6 pad bytes + a valid RLE stream (decodes to exactly
|
||||
440×299 = 131560 bytes). rle_expand_8bit reads w/h as **LE** u16s (byte-
|
||||
matched N64 code): the raw header gives w=47105, h=11009 → remaining ≈
|
||||
518 MB written into a 256 KB buffer → SIGSEGV. The N64 build embeds this
|
||||
asset into .data via `assets/romfiles2.s` (`.incbin
|
||||
"assets/ge007.u.2A4D50.usedby7F008DE4.bin"`) from an extracted .bin whose
|
||||
header is byte-swapped — title2.c hardcodes 440-wide I8 rows × 299, and this
|
||||
is the only decodable 440×299 RLE stream in the ROM (a full-file 16-bit swap
|
||||
was ruled out: it zeroes the first RLE count at +0xC), so only the 4-byte
|
||||
header differs between raw ROM and the N64 .bin. Fix (port-layer, romdata.c
|
||||
romdataInit, after the cart-base mapping): bswap32 the first word at
|
||||
`(u32 *)&unknown2` in place — the image is a writable VirtualAlloc at CART_
|
||||
BASE — guarded by "only swap if the LE-read w or h > 512" so an already-LE
|
||||
region copy is a no-op. rle.c/title.c untouched (D37/D54 pattern). Verified:
|
||||
RLE crash gone; game renders 600+ frames past the gun-barrel background into
|
||||
the watch intro, where it hits D56.
|
||||
|
||||
**D56 (RESOLVED: watch-intro embedded Model/RW-pool raw offsets into struct
|
||||
player; SIGSEGV in `modelSetScale`)**
|
||||
|
||||
Post-D55, the game renders 600+ frames (~10 s into the gun-barrel/watch
|
||||
intro) then SIGSEGVs in `modelSetScale` (src/game/model.c:778,
|
||||
`objinst->scale = scale`) with a garbage Model* (crash-log return frame was
|
||||
corrupted; caller inferred — see below). Prime suspect, and the only code
|
||||
passing a **raw N64 offset into struct player** as a Model*: `sub_GAME_7F07E7CC`
|
||||
(bondview2.c:3102-3116, called from bondview2.c:3400 whenever the pause/watch
|
||||
transition completes — every watch-menu open, and during the gun-barrel
|
||||
intro):
|
||||
```
|
||||
animInit((Model *)((u8 *)g_CurrentPlayer + 0x230), itemheader, (u32 *)((u8 *)g_CurrentPlayer + 0x2ec));
|
||||
modelSetScale((Model *)((u8 *)g_CurrentPlayer + 0x230), c_item_entries[41].scale * 0.1f);
|
||||
modelSetAnimation((Model *)((u8 *)g_CurrentPlayer + 0x230), …ANIM_DATA_bond_watch…);
|
||||
*(s32 *)((u8 *)g_CurrentPlayer + 0x220) = 0; // = step_in_view_watch_animation
|
||||
```
|
||||
Layout facts (probe-verified where noted):
|
||||
- struct Model (bondtypes.h:1482): 8 pointer fields (chr, obj, render_pos,
|
||||
datas [u32* under PORT, D53.1 — note the in-code comment there still says
|
||||
"D52", flagged for rename in HANDOFF Task 3], attachedto,
|
||||
attachedto_objinst, anim, anim2).
|
||||
sizeof_N64 = **0xBC**; sizeof_PC = **0xE8** (vsize probe: chr@8, obj@0x10,
|
||||
render_pos@0x18, datas@0x20, scale@0x28, attachedto@0x30, anim@0x40 — note
|
||||
the 8-byte pointer alignment padding after the two leading s16s).
|
||||
- N64 struct player: the **watch Model is embedded at +0x230** (its first
|
||||
word is the anonymous s32 `something_with_watch_object_instance`), size
|
||||
0xBC, so it ends exactly at +0x2EC where the **RW-data pool** begins. The
|
||||
pool region runs to `buttons_pressed`@0x3B4 = **0xC8 bytes** of capacity;
|
||||
every field in [0x2EC, 0x3B4) is an anonymous s32 (no pointers → no extra
|
||||
PC shift inside the region).
|
||||
- On PC the embedded Model sits at X = offsetof(struct player,
|
||||
something_with_watch_object_instance) (≥ 0x230 + 5×4: cameratile@0x34,
|
||||
prop@0xA8, bodyModel@0xD4, autoaim_target_y@0x130, autoaim_target_x@0x140
|
||||
are the pointer fields before it) and spans [X, X+0xE8). Remaining capacity
|
||||
before buttons_pressed is only 0x184−0xE8 = **0x9C < 0xC8** → the pool does
|
||||
NOT fit embedded on PC.
|
||||
- modelInit stores the pool pointer in `Model.datas` and ALL rwdata access
|
||||
goes through `modelGetNodeRwData(model, node)` via `model->datas` — so
|
||||
redirecting the pool to separate storage is safe; only sub_GAME_7F07E7CC
|
||||
references +0x2EC directly (whole-tree grep).
|
||||
- The N64 capacity 0xC8 bounds the watch model's real pool size (the game
|
||||
works on N64), so a fixed static buffer of that size is safe.
|
||||
**Applied as designed + verified:** bondview2.c sub_GAME_7F07E7CC under
|
||||
#ifdef PORT takes the Model by field name —
|
||||
`Model *watch = (Model *)&g_CurrentPlayer->something_with_watch_object_instance;`
|
||||
(probe-verified at +0x24C on PC — not 8-aligned; unaligned pointer stores are
|
||||
fine on x86-64 per D53.2) and hosts the pool in `static u8 watchRwPool[0xC8]`
|
||||
(N64 capacity; N64 embeds it at player+0x2EC); animInit/modelSetScale/
|
||||
modelSetAnimation on `watch`; the +0x220 store becomes
|
||||
`g_CurrentPlayer->step_in_view_watch_animation = 0;`. N64 raw-offset path kept
|
||||
verbatim under #else. Verified: the watch path no longer crashes — but the
|
||||
same `modelSetScale` SIGSEGV remained, and an env-gated probe (GE_D56,
|
||||
logging `__builtin_return_address(0)` in modelSetScale) proved the real
|
||||
caller was **not** the watch path: `initializeGunBarrelIntro` (title.c)
|
||||
calling `modelSetScale(NULL, 0.18779343f)` because `setup_chr_instance()` →
|
||||
`modelmgrInstantiateModelWithAnim()` returned NULL for BODY_Brosnan_Tuxedo —
|
||||
see D57.
|
||||
|
||||
**D57 (RESOLVED: pointer-grown rwdata records overflow the N64-sized spare
|
||||
pools; `modelmgrInstantiateModelWithAnim` returns NULL → SIGSEGV in
|
||||
`modelSetScale(NULL, …)`)**
|
||||
|
||||
The Brosnan tuxedo's computed PC `numRecords` is **153** words vs the N64-
|
||||
sized anim spare-pool capacity of **140** (`ANIM_MODEL_SPARE_RWDATALEN =
|
||||
0x8C`). Cause: two rwdata record structs contain pointer fields and grow 8 →
|
||||
16 bytes on x86-64 — `ModelRwData_HeadPlaceholderRecord` (ModelFileHeader* +
|
||||
void*) and `ModelRwData_DisplayList_CollisionRecord` (Vertex* + Gfx*). Since
|
||||
`modelCalculateRwDataLen()` accumulates sizeof(record)/4 per node, every
|
||||
HEAD/DLCOLLISION node adds +2 words vs N64. Fix (two parts, both #ifdef PORT):
|
||||
(1) initunk_005520.c: spare capacities grown with headroom —
|
||||
`MODEL_SPARE_RWDATALEN 0x14→0x38`, `ANIM_MODEL_SPARE_RWDATALEN 0x8C→0xA8`
|
||||
(N64 values kept under #else); (2) model.c: in the non-LvResetting branches
|
||||
of both `modelmgrInstantiateModel()` and `modelmgrInstantiateModelWithAnim()`,
|
||||
a dynamic slot+pool fallback mirroring the existing LvResetting path
|
||||
(`mempAllocBytesInBank(sizeof(struct ModelSlot/AnimModelSlot))` + 16-aligned
|
||||
pool of numRecords words) — the slot is untracked (never reused), acceptable
|
||||
because with the grown capacities it should not trigger. A u32-field approach
|
||||
for the two pointer records was rejected: `ModelFileHeader` pointers there are
|
||||
exe-resident globals (>0x80000000 on PC) and would truncate. Verified:
|
||||
Brosnan gets a dynamic slot, `modelSetScale` succeeds, game proceeds to
|
||||
rendering — where it hits D58.
|
||||
|
||||
**D58 (RESOLVED: gun-barrel DL — K0 vertex-pointer idiom + 16-byte Gfx
|
||||
overflow of the N64-sized reservation; SIGSEGV in `gfx_sp_vertex`, then FATAL
|
||||
"Unknown GBI opcode 0x00")**
|
||||
|
||||
Two distinct PC-layout breaks in the same buffer (initializeGunBarrelIntro,
|
||||
title.c), both found via env-gated probes (GE_D57: per-command VTX/CALL/JMP
|
||||
log + entry-time hexdump of the barrel DL):
|
||||
- **Part A (vertex pointer):** title.c passed `barrelDisplayListPtr +
|
||||
0x80000000` to sub_GAME_7F01BFF8, which embeds it verbatim in each G_VTX
|
||||
w1 (GE's gDma1p writes `(uintptr_t)(v)` — no LSB). On N64 the mempool
|
||||
pointer was physical, so +0x80000000 gave the RSP-visible KSEG0 address;
|
||||
on PC it is a V1 pointer (0x70xxxxxx, dram.c) and +0x80000000 lands at
|
||||
0xF0xxxxxx — unresolvable by fast3d's seg_addr() → SIGSEGV reading the
|
||||
vertex array. Fix: rebuild the exact N64 value —
|
||||
`(Vtx *)(OS_K0_TO_PHYSICAL((void *)barrelDisplayListPtr) | 0x80000000u)`
|
||||
(→ 0x80xxxxxx; seg_addr passes it through to the KSEG0 mirror; segments
|
||||
7/8 are never registered, so the unmarked-segment path is skipped).
|
||||
- **Part B (DL reservation):** on PC `sizeof(Gfx) == 16` — the union's
|
||||
trailing `long long` (gbi.h documents it: "except on 64-bit, where it is
|
||||
exactly 128 bit"), same class as D50.6. Both writers (`gdl++`) and fast3d
|
||||
(`++cmd`) advance by 16, so all game-written DLs are 16-byte-wide — but
|
||||
the barrel-DL reservation `bufferSize -= 0x100` was sized for N64's 8-byte
|
||||
Gfx. sub_GAME_7F01BFF8 emits 31 Gfx (2×VTX + 28×TRI + ENDDL) = 496 B, so
|
||||
slots 16–30 (second TRI batch + ENDDL) overflowed into the RLE region at
|
||||
+0x300, and sub_GAME_7F008DE4's expand then clobbered them with image data.
|
||||
At render the RSP executed VTX/TRI×14/VTX fine, then hit slot 16 = RLE
|
||||
pixels (w0=0x00000001 → opcode 0x00; fast3d has no G_SPNOOP case) → FATAL.
|
||||
Fix: reserve 0x200 under PORT. The 0x200 vertex reserve still fits
|
||||
(30 Vtx × 16 B = 0x1E0). NOTE for future asset work: any other N64-sized
|
||||
reservation for game-written DLs/vertex arrays must be re-checked against
|
||||
the 16-byte Gfx / 16-byte Vtx widths (recurring class, cf. D50.6/D53.2).
|
||||
Verified: barrel DL executes to ENDDL; game proceeds past the gun-barrel
|
||||
hole into model rasterization — where it hits D59.
|
||||
|
||||
**D59 (OPEN — current blocker: SIGSEGV inside an external GL DLL during the
|
||||
first real model rasterization after the gun-barrel hole)**
|
||||
|
||||
Post-D58, the barrel DL runs clean (probe-verified: VTX@+0, TRI×14,
|
||||
VTX@slot15(+0xF0 in 16-byte form), … ENDDL) and the game crashes shortly
|
||||
after with EXCEPTION 0xc0000005 at PC 0x7ff8d42a44d3 — inside a DLL loaded
|
||||
at 0x7ff8d4230000 (offset +0x744d3; almost certainly the OpenGL driver,
|
||||
not yet confirmed). The crash-log backtrace frame #1 (main+0x227cc0) is a
|
||||
BSS symbol (`memoryMesgMB`) — garbage stack, no usable caller. Draw path:
|
||||
gfx_flush() (gfx_pc.cpp:299) → `gfx_rapi->draw_triangles(buf_vbo,
|
||||
buf_vbo_len, buf_vbo_num_tris)` → gfx_opengl.cpp:813-816
|
||||
`glBufferData(GL_ARRAY_BUFFER, sizeof(float)*buf_vbo_len, buf_vbo,
|
||||
GL_STREAM_DRAW); glDrawArrays(GL_TRIANGLES, 0, 3*buf_vbo_num_tris)`, where
|
||||
`buf_vbo` is the static `float buf_vbo[MAX_BUFFERED*(32*3)]`. Hypotheses:
|
||||
(a) buf_vbo overflow while accumulating transformed vertices for the first
|
||||
real model (Brosnan) — check the vertex-append site in gfx_pc.cpp (the
|
||||
transform loop after ~line 1118) for a missing bounds check against
|
||||
MAX_BUFFERED; (b) garbage buf_vbo_len/num_tris; (c) bad texture/shader state
|
||||
on first model draw. Next: env-gated probe logging buf_vbo_len/
|
||||
buf_vbo_num_tris at every gfx_flush + identify the DLL (PowerShell module
|
||||
list during a run, or the GL vendor string in the log).
|
||||
|
||||
**D59 RESOLVED (sessions G–I).** The "external GL DLL" crash was not a
|
||||
driver bug: it was an msvcrt.dll `memcpy` faulting on a wild source — the
|
||||
gun-barrel sub-DL region at `ptr_logo_and_walletbond_DL + 0x200` had been
|
||||
clobbered by the unbounded RLE write of D64 (below). With D64 fixed, the
|
||||
barrel renders and the game advances; no fast3d/vertex-buffer change was
|
||||
needed. The crash handler gained permanent improvements along the way:
|
||||
FAULT ADDR (ExceptionInformation[1]), a 16-qword STACK@RSP window, and a
|
||||
module list in `ge007.crash.log` (crash.c + psapi).
|
||||
|
||||
**D60 RESOLVED — DMA target validation (port layer).**
|
||||
`osPiStartDma` (libultra.c) now validates ROM-read targets: the N64 PI can
|
||||
DMA to any KSEG address, but on PC an unmapped target is a wild memcpy.
|
||||
`dramHostAddrValid()` accepts DRAM V1/V2 and any host-committed region
|
||||
(VirtualQuery), so legitimate `.bss`/`.data` targets (e.g.
|
||||
`ramrom_data_target`) pass while s32-truncated wild addresses are rejected
|
||||
with a logged FATAL instead of a silent crash. Also added the GE_D60
|
||||
sidecar-read tracer and GE_D61 per-ROM-read log (`d61dma.log`).
|
||||
|
||||
**D62 RESOLVED — OSMesgQueue/OSScMsg layout (port layer).** The shim's
|
||||
message-queue bookkeeping had to match the PC struct widths: OSMesgQueue is
|
||||
40 bytes on PC (two OSThread* + 3×s32 + OSMesg*), OSScMsg stays 32 bytes.
|
||||
Scheduler-thread message flow (retrace/pre-NMI/interrupt/cmd queues in
|
||||
`os_scheduler`, g_AudioManager frame/reply queues) verified against those
|
||||
layouts.
|
||||
|
||||
**D63 — TEMP diagnostics (to strip).** GE_D63-gated probes in gfx_pc.cpp /
|
||||
blood_animation.c / front.c / rsp.c tracing the VTX-pool bump pointer
|
||||
(`g_GfxMemPos`), the gun-barrel sub-DL slot word, dram-branch targets and
|
||||
rspGfxTaskStart hand-off. Used to prove D64's clobber path and to rule out
|
||||
VTX-pool overflow; no permanent change.
|
||||
|
||||
**D64 RESOLVED — blood RLE sentinel (src/game/blood_animation.c).**
|
||||
The N64 build places `die_blood_image_end` in the same section directly
|
||||
after `die_blood_image_1[]`; the RLE decoder's guard `bloodImgNxt <
|
||||
&die_blood_image_end` relies on that adjacency (only the address is used).
|
||||
On PC a zero-init symbol lands in `.bss` ~1 MB away, so the guard never
|
||||
fires and the decoder writes unbounded past the array — it clobbered the
|
||||
gun-barrel sub-DL at `ptr_logo_and_walletbond_DL + 0x200`, which is what
|
||||
surfaced as the D59 "GL DLL" crash. Fixed under #ifdef PORT by defining
|
||||
`die_blood_image_end` as one-past-the-end of the array.
|
||||
|
||||
**D65 RESOLVED — `enum HEADS` signed sentinels (src/bondconstants.h).**
|
||||
The N64 toolchain gave this enum a signed underlying type, so
|
||||
`HEAD_FIXED == -1` and `head >= 0` guards were real branches. PC GCC 16
|
||||
picks `unsigned int` for enums whose enumerators are all non-negative
|
||||
(0xFFFFFFFF > INT_MAX), making every `head >= 0` always true and turning
|
||||
`c_item_entries[HEAD_FIXED]` into a wild 64-bit OOB read (SIGSEGV in
|
||||
init_menu18_displaycast). Under PORT the sentinels are now negative
|
||||
literals (`HEAD_FIXED = -1`, `HEAD_RANDOM = -97`) — identical bit pattern,
|
||||
signed semantics restored.
|
||||
|
||||
**D65b RESOLVED — `enum BODIES` signed sentinel (src/bondconstants.h).**
|
||||
Same class as D65: ROM tables store 0xFFFFFFFF in `body` fields and the
|
||||
cast-end check compares `intro_char_table[f].body < 0`; PC's unsigned
|
||||
underlying type deleted the reset branch, so the cast screen rendered the
|
||||
terminator entry and `langGet(0)` dereferenced a NULL bank. Added
|
||||
`BODY_FIXED = -1` under PORT (forces signed underlying type; no existing
|
||||
value changes).
|
||||
|
||||
**D66 RESOLVED — romCopyAligned pointer width (src/ramrom.c/.h) +
|
||||
ramrom replay truncations (src/game/ramromreplay.c).** The N64 build did
|
||||
all of `romCopyAligned` in s32; on PC targets live in `.bss` above 4 GiB,
|
||||
so `(s32)target` truncated (0x1401C6F00 → 0x401C6F00) and the DMA went to a
|
||||
wild address. PORT version uses uintptr_t throughout and returns `void *`
|
||||
callers assign straight to pointers. The ramrom replay path had the same
|
||||
class of `(s32)` truncation on `ramrom_data_target`.
|
||||
|
||||
**D67 RESOLVED — struct image_entry layout (src/game/image.h).** The
|
||||
decompiled field order cannot be right: texLoad() reads
|
||||
`*(s32*)&entry & 0xFFFFFF` as the data offset (dataoffset must occupy bits
|
||||
0-23 of word 0) while chrprop.c indexes entries with an 8-byte stride
|
||||
(sizeof == 8). Under PORT the struct is re-declared with all-u32 bitfields
|
||||
and `dataoffset : 24` first, so GCC packs it to exactly two words on both
|
||||
targets and the raw word read is satisfied. The IMAGE() macro initializer
|
||||
order in image.c is adjusted to match under PORT.
|
||||
|
||||
**D68 RESOLVED — Globalimagetable endianness (port/src/gimgfixup.c +
|
||||
src/game/image_bank.c + src/game/image.c).** The ROM-copied Globalimagetable
|
||||
segment (texReset) is N64 big-endian, but PC code reads its CPU-interpreted
|
||||
u32 fields natively: the IMAGESEG-marked G_SETTIMG w1 words
|
||||
(`IMAGESEG(id) = 0xABCD0000 | id`) and the `sImageTableEntry.index` field of
|
||||
all 32 table arrays. Unfixed, texLoad computed texnum from byte-swapped ids
|
||||
(e.g. 52651 for IMAGE_SMOKE_11 = 2106) → out-of-range offsets → a 925 KB
|
||||
ROM read into the 4000-byte stack compbuffer (FATAL at boot). Fix:
|
||||
`gimgFixupGlobalimagetable()` bswaps exactly those u32s in place after the
|
||||
romCopy (17 Gfx DLs walked op-by-op for the AB CD marker; table entry
|
||||
counts fall out of the D39 symbol layout, 12-byte stride); everything else
|
||||
in the segment is byte-level (opcodes, single-byte fields, raw pixel blocks
|
||||
referenced via 0x02xxxxxx segmented addresses) and untouched. Two
|
||||
consequences handled under PORT: (1) texLoadFromDisplayList's marker scan
|
||||
now checks bytes 6..7 (CD AB — the LE encoding of 0xABCDxxxx) instead of
|
||||
4..5; (2) explosion.c executes the *compiled* globalDL_0xNNN shadows via
|
||||
g_ExplosionDisplayLists[], so `gimgSyncCompiledGlobalDLs()` copies the
|
||||
texLoad()-patched IMAGESEG w1 values from the ROM copy into those arrays
|
||||
(command j of the 8-byte ROM DL maps to Gfx slot j of the 16-byte compiled
|
||||
array; D39 verified them byte-identical). Verified: 137 texLoads with valid
|
||||
in-range ids (2106, 2084, …), real offsets/sizes from g_Textures, and the
|
||||
game runs the full ~3.5-minute intro (logo → gun barrel → cast) at ~59 fps
|
||||
to the first stage load.
|
||||
|
||||
**D69 (OPEN — current blocker: BG-file big-endian headers at stage load)**
|
||||
|
||||
Post-D68 the game plays through the entire intro and crashes in
|
||||
`load_bg_file` (src/game/bg.c:830) when loading the first stage (BUNKER1,
|
||||
"bg/bg_sev_all_p.seg", cart 0x10438660). The header IS loaded correctly —
|
||||
`obLoadBGFileBytesAtOffset` works: `&fileentry->hw_address[offset]`
|
||||
evaluates to `hw_address + offset` (a valid cart address; the compiled
|
||||
absolute asset symbols point into the ROM mapped at the cart base, and the
|
||||
PI shim memcpys from there). The bug is interpretation: BG-file offsets are
|
||||
N64 big-endian u32s in segment-0x0F form. Header word 1 in the ROM is
|
||||
`0F 00 00 14` (BE value 0x0F000014 → file offset 0x14 after
|
||||
BG_SEG_TO_PTR's `+ 0xF1000000` fold); PC reads it LE as 0x1400000F, so
|
||||
`ptr_bgdata_room_fileposition_list = header + 0x1400000F - 0xF000000`
|
||||
lands ~0x5 MB past the stack buffer and `...[1].pPointTableBin` faults
|
||||
(EXCEPTION 0xc0000005, FAULT ADDR ≈ header + 0x500000F). The whole
|
||||
stage-load path (bg .seg headers/room tables + Tbg_*_stanZ geometry files)
|
||||
is riddled with BE u32 fields — the same class as D68 but a far larger
|
||||
format surface. This is the "next asset type" milestone anticipated in
|
||||
AGENTS.md. Strategy options: (a) offline per-region conversion of all bg/*.seg
|
||||
+ Tbg_*_stanZ files into sidecars (the D43/Plan-B pattern; requires fully
|
||||
decoding GE's BG/stan formats from bg.c/stan.c — note PD's
|
||||
preprocess/filebg.c describes a *different*, zipped multi-section format;
|
||||
same family ≠ identical, validate per field); (b) runtime port-layer fixup
|
||||
after each load (same format knowledge, placed in port/). Either way the
|
||||
first task is reverse-engineering the formats: header words 0..3 are
|
||||
pointers (rooms/portals/bgcmds/lights-style tables per the D69 probe:
|
||||
word1=0x14 room-fileposition list), bg_room_data records carry more
|
||||
0x0Fxxxxxx offsets (pPointTableBin at record+0x28, see crash disasm), and
|
||||
stanZ files go through stanDetermineEOF/stanLoadFile. TEMP D69 probe in
|
||||
ob.c (GE_D69) logs name/index/rom_size/hw_address per BG load.
|
||||
|
||||
### G. Phase 2 status (current)
|
||||
|
||||
Done through **D50**: PD fast3d integrated (`port/fast3d/`); GE's real
|
||||
Done through **D68**: PD fast3d integrated (`port/fast3d/`); GE's real
|
||||
`src/sched.c` + pthread kernel; dual-mapped DRAM; ROM mapped at cart base;
|
||||
SDL2 window; full boot chain (D31–D42); **Plan B executed (D50)** — all 512
|
||||
NTSC model files offline-converted to PC-layout RZ sidecars
|
||||
(`tools_pc/d43_emit.py` → `data/pcmodels-ntsc-final/pcmodels.bin` +
|
||||
manifest.csv), served through the existing load path via a port-layer table
|
||||
patch (`port/src/pcmodels.c`, romdata cart-extension, one-shot
|
||||
`pcmodelsPatchTable()` + poolRemaining reset hook in
|
||||
`load_object_fill_header`); runtime C fixups for language-bank BE offset
|
||||
tables (D50.3) and font re-layout (D50.4); legal-screen UB seed (D50.5);
|
||||
`texCopyGdls` w1 partial-copy bug fixed and byte-proven (D50.6). Frames 1–4
|
||||
render; PlegalpageZ model GDLs execute to completion. **Current blocker:
|
||||
D51** — SIGSEGV in `import_texture_i8` via a main-DL G_TEXRECT lazy tile-0
|
||||
upload (frame 5); investigation not started, recipe in docs/HANDOFF.md.
|
||||
After D51: pixel-assert soak (env-gated PPM dump + pixcount still unbuilt),
|
||||
then the next asset type (Tbg_*_stanZ backgrounds) with the same offline
|
||||
pattern.
|
||||
NTSC model files offline-converted to PC-layout RZ sidecars, served through
|
||||
the existing load path via a port-layer table patch; runtime C fixups for
|
||||
language-bank BE offset tables (D50.3) and font re-layout (D50.4);
|
||||
legal-screen UB seed (D50.5); `texCopyGdls` w1 partial-copy bug fixed and
|
||||
byte-proven (D50.6). **D51–D58 resolved** (font pixeldata fixup, osGetCount
|
||||
tick rate, model RW-data pools, cseq BE header, synth param slots, RLE
|
||||
folder-menu background, watch-intro raw offsets, spare-pool capacities,
|
||||
gun-barrel DL idiom + reservation). **D59 resolved** (the "GL DLL" crash
|
||||
was D64's unbounded blood-RLE write clobbering the barrel sub-DL),
|
||||
**D60–D62 resolved** (DMA target validation, OSMesgQueue layout),
|
||||
**D63** TEMP diagnostics, **D64 resolved** (blood RLE sentinel adjacency),
|
||||
**D65/D65b resolved** (HEADS/BODIES enum signed sentinels under PORT),
|
||||
**D66 resolved** (romCopyAligned + ramrom replay 64-bit pointer width),
|
||||
**D67 resolved** (struct image_entry N64 layout reconstruction),
|
||||
**D68 resolved** (Globalimagetable BE→LE fixup for IMAGESEG Gfx words +
|
||||
sImageTableEntry.index; compiled globalDL shadows synced after texLoad).
|
||||
The game now boots, plays the intro music, and renders the **entire ~3.5-
|
||||
minute intro** (Nintendo logo → gun barrel with Brosnan → cast screen) at
|
||||
~59 fps, then crashes in `load_bg_file` on the first stage load —
|
||||
**D69, the current blocker**: BG-file headers are N64 big-endian
|
||||
(segment-0x0F offsets) and PC reads them LE. Details in §F/D59–D69.
|
||||
|
||||
**Committed at D68** (this milestone): D51–D68 fixes + TEMP diagnostics
|
||||
still present (strip list in HANDOFF Task 3); build is GREEN.
|
||||
|
||||
### H. Handoff & plan (current session)
|
||||
|
||||
Full paste-ready brief: **docs/HANDOFF.md** (rewritten this session). Summary:
|
||||
Full paste-ready brief: **docs/HANDOFF.md** (rewritten for the D69 session —
|
||||
BG/stan format work). Immediate task: reverse-engineer GE's bg .seg +
|
||||
Tbg_*_stanZ formats from the decompiled consumers (bg.c, stan.c), then
|
||||
choose offline sidecar conversion (Plan-B pattern, D43) vs runtime port-layer
|
||||
fixup; PD's preprocess/filebg.c is a reference for the *approach* only — its
|
||||
BG format is different (zipped multi-section).
|
||||
Summary:
|
||||
|
||||
**State.** Plan B is EXECUTED (D50): offline sidecars for all 512 NTSC model
|
||||
files + port-layer plumbing + lang/font runtime fixups + legal-screen UB seed
|
||||
+ the texCopyGdls w1 fix (D50.6, byte-proven against PlegalpageZ). Boot runs
|
||||
to the main loop; frames 1–4 render; model GDLs execute (PlegalpageZ
|
||||
sub-DLs complete).
|
||||
**State.** D50–D68 all resolved and verified; committed at the D68
|
||||
milestone. The game boots, plays intro music, and renders the entire
|
||||
~3.5-minute intro (logo → gun barrel with Brosnan → cast screen) at ~59
|
||||
fps. TEMP diagnostics from the D51–D69 sessions are still in the tree
|
||||
(strip list in HANDOFF Task 3). Build is GREEN.
|
||||
|
||||
**Current blocker — D51.** SIGSEGV on frame 5 in `import_texture_i8` via a
|
||||
main-DL G_TEXRECT lazy tile-0 upload (gfx_pc.cpp:782; failing opcode slot at
|
||||
DL offset +0x260). G_TEXRECT blits from RDP texture-memory tiles, so the fault
|
||||
is an invalid/stale tile source — either a mis-resolved main-DL G_SETTIMG,
|
||||
a tile used before setup, or a ROM/ramrom image needing a port fixup.
|
||||
Investigation not started; the gdb recipe (break live at
|
||||
`import_texture_i8`, dump `rdp.textures[0]` + the full main DL — post-mortem
|
||||
dumps of 0x70052e80 fail with "Cannot access memory") is in HANDOFF Task 1.
|
||||
**Current blocker — D69.** `load_bg_file` (src/game/bg.c:830) faults on
|
||||
the first stage load (BUNKER1): BG-file header words are N64 big-endian
|
||||
segment-0x0F offsets; PC reads them LE, so the room-fileposition-list
|
||||
pointer lands ~5 MB past the stack header buffer. Full analysis +
|
||||
strategy options in §F/D69.
|
||||
|
||||
**Next after D51:** build the pixel assert (env-gated PPM dump every N frames
|
||||
in port/video.c + `tools_pc/pixcount.py`) and run a 30 s+ soak; then move to
|
||||
the next asset type (Tbg_*_stanZ backgrounds) with the same offline pattern —
|
||||
PD's `port/src/preprocess/filebg.c` is the per-field reference (§2.4).
|
||||
**After D69:** get a stage to load + render (bg .seg + Tbg_*_stanZ format
|
||||
work — see §H immediate task), then continue the diagnose→fix→verify loop
|
||||
through gameplay; strip TEMP diagnostics at each milestone; pixel-assert
|
||||
soak (PPM dump + tools_pc/pixcount.py) once a stage is stable.
|
||||
|
||||
**D32 repeatable fix procedure** (apply to any ROM-serialized struct that faults
|
||||
on a pointer-field read):
|
||||
@@ -1844,6 +2270,24 @@ on a pointer-field read):
|
||||
guard a PC branch that reproduces the N64 32-bit value exactly (e.g. keep the
|
||||
0x02000000 base) or replace the idiom with an explicit equivalent; N64 line
|
||||
stays verbatim under `#else`.
|
||||
7. If two structs are **type-punned** (cast back and forth, e.g. ModelSlot ↔
|
||||
Model), verify their layouts still agree on PC — pointer-width changes break
|
||||
puns silently: fields the game reaches "through" one struct land at wrong
|
||||
offsets in the other (D53.2). Also check any fixed-size allocation that must
|
||||
contain the grown struct (the 0x20-byte model heap fallback, D53).
|
||||
8. If a hang (not a crash) appears with no thread making progress, suspect a
|
||||
**timing/pacing gate**: GE's loop gates on `osGetCount()` deltas in N64 RSP
|
||||
counter ticks (~46.5525/µs), not µs (D52).
|
||||
9. If the fault is a store through a pointer built from a **raw byte offset
|
||||
into a struct that contains pointers** (`(u8 *)ptr + 0xNNN`, D56): every
|
||||
pointer field before 0xNNN shifts +4 on PC (plus 8-byte alignment padding),
|
||||
so the offset no longer lands on the intended object. Fix under #ifdef PORT
|
||||
via the named field (`&s->field`); if the embedded sub-object has grown
|
||||
(sizeof_PC > sizeof_N64) and its trailing companion storage (e.g. a model
|
||||
RW pool packed right after it in the struct) no longer fits before the next
|
||||
live field, relocate that companion to a static buffer of the N64 capacity —
|
||||
safe when all access goes through a pointer stored in the sub-object at init
|
||||
(modelInit → Model.datas) and only the one site references the raw offset.
|
||||
|
||||
**Non-negotiable #2 refinement (applied to AGENTS.md).** The original "game code
|
||||
compiles unmodified / fix belongs in port/" is too absolute: pointer-width layout
|
||||
@@ -1856,9 +2300,16 @@ following PD ground truth. No logic/behavior changes; each such edit is logged i
|
||||
|
||||
**Environment reminders.** MSYS2 tools in `/c/msys64/mingw64/bin/` (not on PATH —
|
||||
prefix `export PATH=…`). Build: `./build-pc.sh ntsc-final`. gdb **launch** mode
|
||||
only (attach fails, error 87); symbolicate offline with `addr2line -e
|
||||
build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`. Image base 0x140000000.
|
||||
`load_resource`/many init fns use a fake RBP — compute stack offsets from entry
|
||||
RSP. The D30 crash handler writes `ge007.crash.log` with a working Phase-2
|
||||
backtrace (**D44** fixed) — first stop for any fault; frames past the true chain
|
||||
may be stale, sanity-check they fall inside `.text`.
|
||||
only (attach fails, error 87) and it is **far too slow for timing-dependent
|
||||
crashes** (a D56-class crash ~10 s in took >300 s under gdb to reach 2 frames —
|
||||
DBGHELP symbol loading + the D51 stall-heartbeat thread dumps; don't wait on
|
||||
it): prefer env-gated TEMP probes + the built-in crash log, and symbolicate
|
||||
offline with `addr2line -e build-pc/ge007.x86_64.exe -f -C <0x140000000+rel>`.
|
||||
Image base 0x140000000. `load_resource`/many init fns use a fake RBP — compute
|
||||
stack offsets from entry RSP. The D30 crash handler writes `ge007.crash.log`
|
||||
with a working Phase-2 backtrace (**D44** fixed) — first stop for any fault;
|
||||
frames past the true chain may be stale (a corrupted return address outside the
|
||||
module, as in D56, means unwind depth is limited — confirm callers by code-
|
||||
path analysis + behavior). Standalone probe compiles need `-std=c11` (without
|
||||
it `typedef s32 bool` in bondtypes.h breaks under gnu23) and pointer-difference
|
||||
arithmetic instead of offsetof (include/stddef.h is #if 0'd).
|
||||
@@ -5,4 +5,14 @@
|
||||
|
||||
extern struct GfxRenderingAPI gfx_opengl_api;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Frame capture (dev tool, env GE_PCDUMP=1) — see gfx_opengl.cpp. */
|
||||
bool gfx_opengl_pcdump_enabled(void);
|
||||
bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const char* path);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+101
-2
@@ -2320,6 +2320,13 @@ static inline void *seg_addr(uintptr_t w1) {
|
||||
|
||||
uintptr_t clearMtx;
|
||||
|
||||
/* TEMP D63 (strip before commit) */
|
||||
static int s_d63taskcount = 0;
|
||||
static int s_d63ctxdump = 0;
|
||||
extern "C" void d63RecordBranch(const Gfx *from, const Gfx *to);
|
||||
extern "C" void d63DumpTrail(void);
|
||||
extern "C" void d63LogLastBranchReread(void);
|
||||
|
||||
static void gfx_run_dl(Gfx* cmd) {
|
||||
// puts("dl");
|
||||
int dummy = 0;
|
||||
@@ -2357,7 +2364,30 @@ static void gfx_run_dl(Gfx* cmd) {
|
||||
case G_VTX:
|
||||
gfx_sp_vertex(C0(0, 16) / sizeof(Vtx), C0(16, 4), (const Vtx*)seg_addr(cmd->words.w1));
|
||||
break;
|
||||
case G_DL:
|
||||
case G_DL: {
|
||||
/* TEMP D63: record the branch trail for post-mortem */
|
||||
{
|
||||
const Gfx *tgt = (const Gfx *)seg_addr(cmd->words.w1);
|
||||
uintptr_t ta = (uintptr_t)tgt;
|
||||
if (ta >= 0x70000000 && ta < 0xA0000000)
|
||||
{
|
||||
sysLogPrintf(LOG_NOTE, "D63 dram-branch t=%d from=%p to=%p first=(%08x,%08x)",
|
||||
s_d63taskcount++, (const void *)cmd, (const void *)tgt,
|
||||
tgt->words.w0, tgt->words.w1);
|
||||
/* Context: 4 commands before the parent G_DL */
|
||||
if (ta == 0x8012EC38 && s_d63ctxdump < 3)
|
||||
{
|
||||
for (int k = 4; k >= 0; --k)
|
||||
{
|
||||
const Gfx *c = cmd - k;
|
||||
sysLogPrintf(LOG_NOTE, "D63 ctx[%d]=%p (%08x,%08x)",
|
||||
k, (const void *)c, c->words.w0, c->words.w1);
|
||||
}
|
||||
++s_d63ctxdump;
|
||||
}
|
||||
}
|
||||
}
|
||||
d63RecordBranch(cmd, (const Gfx *)seg_addr(cmd->words.w1));
|
||||
if (C0(16, 1) == 0) {
|
||||
// Push return address
|
||||
Gfx* subGFX = (Gfx*)seg_addr(cmd->words.w1);
|
||||
@@ -2370,6 +2400,7 @@ static void gfx_run_dl(Gfx* cmd) {
|
||||
--cmd; // increase after break
|
||||
}
|
||||
break;
|
||||
}
|
||||
case (uint8_t)G_ENDDL:
|
||||
return;
|
||||
case (uint8_t)G_SETGEOMETRYMODE:
|
||||
@@ -2576,9 +2607,28 @@ static void gfx_run_dl(Gfx* cmd) {
|
||||
case G_RDPLOADSYNC:
|
||||
case G_RDPTILESYNC:
|
||||
break;
|
||||
default:
|
||||
default: {
|
||||
/* TEMP D63: dump context around the garbage command */
|
||||
uint32_t *w = (uint32_t *)cmd;
|
||||
sysLogPrintf(LOG_ERROR,
|
||||
"D63 unknown opcode 0x%02x at %p (dlstart=%p); words[-2..+2]:",
|
||||
opcode, (void *)cmd, (void *)dListStart);
|
||||
for (int i = -2; i <= 2; ++i)
|
||||
sysLogPrintf(LOG_ERROR, "D63 [%d] %p = %08x %08x",
|
||||
i, (void *)&w[i], w[i * 2], w[i * 2 + 1]);
|
||||
d63DumpTrail();
|
||||
d63LogLastBranchReread();
|
||||
/* TEMP D63: dump the sub-DL region to see the 0xFF extent. */
|
||||
if (getenv("GE_D63")) {
|
||||
uint32_t *r = (uint32_t *)dListStart;
|
||||
for (int i = 0; i < 128; i += 4) {
|
||||
sysLogPrintf(LOG_ERROR, "D63 region +%03x: %08x %08x %08x %08x",
|
||||
i * 4, r[i], r[i + 1], r[i + 2], r[i + 3]);
|
||||
}
|
||||
}
|
||||
sysFatalError("Unknown GBI opcode 0x%02x at %p.\nw0 %08x\nw1 %08x", opcode, cmd, cmd->words.w0, cmd->words.w1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
++cmd;
|
||||
}
|
||||
@@ -2848,3 +2898,52 @@ extern "C" void gfx_reset_framebuffer(void) {
|
||||
gfx_rapi->start_draw_to_framebuffer(0, (float)gfx_current_dimensions.height / SCREEN_HEIGHT);
|
||||
active_fb = framebuffers.end();
|
||||
}
|
||||
|
||||
/* TEMP D63: G_DL branch trail for post-mortem (strip before commit) */
|
||||
#include <cstdio>
|
||||
#define D63_TRAIL_SIZE 4096
|
||||
static struct { const Gfx *from; const Gfx *to; uintptr_t w1; } s_d63trail[D63_TRAIL_SIZE];
|
||||
static int s_d63count = 0;
|
||||
|
||||
extern "C" void d63RecordBranch(const Gfx *from, const Gfx *to) {
|
||||
int idx = s_d63count % D63_TRAIL_SIZE;
|
||||
s_d63trail[idx].from = from;
|
||||
s_d63trail[idx].to = to;
|
||||
s_d63trail[idx].w1 = from->words.w1;
|
||||
++s_d63count;
|
||||
}
|
||||
|
||||
extern "C" void d63LogLastBranchReread(void) {
|
||||
if (s_d63count > 0) {
|
||||
int li = (s_d63count - 1) % D63_TRAIL_SIZE;
|
||||
const Gfx *lf = s_d63trail[li].from;
|
||||
sysLogPrintf(LOG_ERROR, "D63 last-branch re-read: from=%p now w0=%08x w1=%llx (w1@rec was %llx) to=%p",
|
||||
(const void *)lf, lf->words.w0,
|
||||
(unsigned long long)lf->words.w1,
|
||||
(unsigned long long)s_d63trail[li].w1,
|
||||
(const void *)s_d63trail[li].to);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void d63DumpTrail(void) {
|
||||
int n = 24;
|
||||
if (s_d63count < n) n = s_d63count;
|
||||
sysLogPrintf(LOG_ERROR, "D63 branch trail (last %d of %d):", n, s_d63count);
|
||||
for (int i = s_d63count - n; i < s_d63count; ++i)
|
||||
sysLogPrintf(LOG_ERROR, "D63 [%d] from=%p w1@rec=%llx to=%p",
|
||||
i % D63_TRAIL_SIZE, (const void *)s_d63trail[i % D63_TRAIL_SIZE].from,
|
||||
(unsigned long long)s_d63trail[i % D63_TRAIL_SIZE].w1,
|
||||
(const void *)s_d63trail[i % D63_TRAIL_SIZE].to);
|
||||
/* Also: every branch whose target is in the DRAM views, to reconstruct
|
||||
* the sub-DL chain that led to the fault. */
|
||||
int span = s_d63count < D63_TRAIL_SIZE ? s_d63count : D63_TRAIL_SIZE;
|
||||
sysLogPrintf(LOG_ERROR, "D63 DRAM-targeted branches (last %d):", span);
|
||||
for (int i = s_d63count - span; i < s_d63count; ++i)
|
||||
{
|
||||
int idx = i % D63_TRAIL_SIZE;
|
||||
uintptr_t to = (uintptr_t)s_d63trail[idx].to;
|
||||
if (to >= 0x70000000 && to < 0xA0000000)
|
||||
sysLogPrintf(LOG_ERROR, "D63 [%d] from=%p to=%p",
|
||||
idx, (const void *)s_d63trail[idx].from, (const void *)to);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef GIMG_FIXUP_H
|
||||
#define GIMG_FIXUP_H
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
|
||||
/* D68 (docs/PCPortResearch.md): endianness fixup for the Globalimagetable
|
||||
* segment after texReset()'s romCopy(). The N64 CPU is big-endian, so every
|
||||
* u32 in the ROM copy is BE-encoded; PC consumers read them natively (LE).
|
||||
*
|
||||
* Two kinds of CPU-interpreted u32 live in the segment:
|
||||
* - Gfx w1 words of IMAGESEG-marked G_SETTIMG commands (IMAGESEG(id) =
|
||||
* 0xABCD0000 | id, see bondconstants.h). texLoadFromDisplayList() scans
|
||||
* for the AB CD marker bytes and texLoad() reads the word back as
|
||||
* `*updateword & 0xffff` to get the image id.
|
||||
* - sImageTableEntry.index (first u32 of each 12-byte entry) in the 32
|
||||
* table arrays; read both as a raw word by texLoad() and as a struct
|
||||
* field by texSelect()/explosion.c.
|
||||
*
|
||||
* Everything else in the segment is byte-level (Gfx opcodes, single-byte
|
||||
* table fields, raw pixel blocks referenced via 0x02xxxxxx segmented
|
||||
* addresses) and needs no transform.
|
||||
*
|
||||
* After the fixup the marker bytes sit at offsets 6..7 of each Gfx (LE
|
||||
* encoding of 0xABCDxxxx), so texLoadFromDisplayList() checks them there
|
||||
* under PORT.
|
||||
*/
|
||||
void gimgFixupGlobalimagetable(u8 *base);
|
||||
|
||||
/* Copy the IMAGESEG w1 words patched by texLoad() in the ROM copy back into
|
||||
* the compiled globalDL_0xNNN arrays (oddtextures.o), which explosion.c
|
||||
* executes via g_ExplosionDisplayLists[] on PC. The ROM copy and the
|
||||
* compiled arrays are byte-identical per D39, so command j of the ROM DL
|
||||
* maps to Gfx slot j of the compiled array (16-byte stride on PC). */
|
||||
void gimgSyncCompiledGlobalDLs(u8 *base);
|
||||
|
||||
#endif
|
||||
@@ -39,6 +39,9 @@ void pcmodelsPatchTable(void);
|
||||
|
||||
uint32_t pcmodelsTotalSize(void);
|
||||
|
||||
/* Cart address where the sidecar image lives (0 if absent). */
|
||||
uintptr_t pcmodelsSidecarBase(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -61,6 +61,13 @@ void romdataFixupAnimationData(u8 *blob, u32 blobSize,
|
||||
*/
|
||||
void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize);
|
||||
|
||||
/*
|
||||
* D54 (docs/PCPortResearch.md): decode a decompressed compact-sequence file's
|
||||
* big-endian ALCMidiHdr in place (16 x u32 trackOffset + u32 division;
|
||||
* see romdata.c). Call after decompressdata(), before alCSeqNew().
|
||||
*/
|
||||
void romdataFixupCseq(u8 *blob);
|
||||
|
||||
/*
|
||||
* D50 (docs/PCPortResearch.md): decode a language bank's big-endian offset
|
||||
* table in place ([u32 offsets x N][text]; see romdata.c). blobSize is the
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* PC port shim for bare <R4300.h> includes (some game files include it
|
||||
* without the PR/ prefix, which resolves to include/PR/R4300.h and would
|
||||
* bypass the PHYS_TO_K0 redefinition in port/shim/PR/R4300.h). Same content
|
||||
* as that shim; keep the two in sync.
|
||||
*/
|
||||
#ifndef _PORT_SHIM_R4300_BARE_H_
|
||||
#define _PORT_SHIM_R4300_BARE_H_
|
||||
|
||||
#if defined(PORT)
|
||||
# include "include/PR/R4300.h"
|
||||
|
||||
# undef PHYS_TO_K0
|
||||
# define PHYS_TO_K0(x) ((u32)(x)) /* identity: V1 view is already virtual */
|
||||
|
||||
#else
|
||||
# include <PR/R4300.h>
|
||||
#endif
|
||||
|
||||
#endif /* _PORT_SHIM_R4300_BARE_H_ */
|
||||
@@ -64,6 +64,26 @@ static void crashStackTraceRaw(char *msg, PEXCEPTION_POINTERS exinfo)
|
||||
CRASH_MSG("PC: %p (Rip=%p Rsp=%p Rbp=%p)\n",
|
||||
exinfo->ExceptionRecord->ExceptionAddress,
|
||||
(void *)context.Rip, (void *)context.Rsp, (void *)context.Rbp);
|
||||
CRASH_MSG("REGS: Rax=%p Rcx=%p Rdx=%p Rsi=%p Rdi=%p R8=%p R9=%p R10=%p R11=%p\n",
|
||||
(void *)context.Rax, (void *)context.Rcx, (void *)context.Rdx,
|
||||
(void *)context.Rsi, (void *)context.Rdi, (void *)context.R8,
|
||||
(void *)context.R9, (void *)context.R10, (void *)context.R11);
|
||||
if (exinfo->ExceptionRecord->NumberParameters > 1) {
|
||||
CRASH_MSG("FAULT ADDR: %p\n",
|
||||
(void *)exinfo->ExceptionRecord->ExceptionInformation[1]);
|
||||
}
|
||||
/* Raw stack window around RSP: survives even when the EBP chain is
|
||||
* broken (tail calls, corrupted frames). 16 qwords up from RSP. */
|
||||
{
|
||||
const uint64_t *sp = (const uint64_t *)context.Rsp;
|
||||
CRASH_MSG("STACK@RSP:");
|
||||
for (int i = 0; i < 16; i++) {
|
||||
char cell[24];
|
||||
snprintf(cell, sizeof(cell), " %p", (void *)sp[i]);
|
||||
CRASH_MSG("%s", cell);
|
||||
}
|
||||
CRASH_MSG("\n");
|
||||
}
|
||||
/* FPU state: for STATUS_FLOATING_POINT_* we want to know whether the
|
||||
* exception masks were actually cleared (MxCsr bits 7..10 / x87 CW
|
||||
* mask bits) or this is a stale status bit. */
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
* +0x050000 _bssSegmentEnd -> mempool start (boss.c:217)
|
||||
* ... game heap (mempools / dyn / audio buffers)
|
||||
* +0x2F4400 tlb block -> mempool end (tlbmanageGetTlbAllocatedBlock)
|
||||
* ... free
|
||||
* -0x0002D0 animations_frame_buffer (0x2D0, D59; dram_syms.s absolute sym)
|
||||
* +0x800000 end of region (8 MB, like an 8-MB-RAM N64)
|
||||
*/
|
||||
|
||||
|
||||
@@ -16,3 +16,11 @@
|
||||
|
||||
.global _bssSegmentEnd
|
||||
.set _bssSegmentEnd, 0x70050000
|
||||
|
||||
/* D59: animation-frame scratch buffer (src/game/initanitable.c). The game
|
||||
* addresses it through s32 fields (Model.unk34/38/64/68) and loadAnimationFrame
|
||||
* computes its address with a (u32) cast, so on PC it must live below 4 GiB;
|
||||
* the BSS copy at 0x140xxxxxx truncates to wild 0x40xxxxxx. Parked at the top
|
||||
* of DRAM V1 (above the mempool sentinel at +0x2F4400). */
|
||||
.global animations_frame_buffer
|
||||
.set animations_frame_buffer, 0x707FFD30
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* gimgfixup.c - D68 (docs/PCPortResearch.md): Globalimagetable endianness
|
||||
* fixup for the PC port. See port/include/gimgfixup.h for the rationale.
|
||||
*
|
||||
* The offset tables below must stay in lockstep with the g_pc_gimg_off_*
|
||||
* values in src/game/image_bank.c (D39) and with assets/oddtextures.c, whose
|
||||
* linked layout defines the segment: 17 Gfx display lists followed by 32
|
||||
* sImageTableEntry arrays, total size 0x13F8.
|
||||
*/
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
#include <PR/gbi.h>
|
||||
#include "gimgfixup.h"
|
||||
|
||||
#define GIMG_REGION_SIZE 0x13F8u
|
||||
|
||||
/* Segment offsets of the 17 global display lists (globalDL_0xNNN). */
|
||||
static const u32 s_dl_offs[] = {
|
||||
0x0, 0x78, 0x120, 0x1C8, 0x270, 0x318, 0x3C0, 0x468,
|
||||
0x510, 0x5B8, 0x660, 0x708, 0x7B0, 0x858, 0x900, 0x9A8, 0xA50
|
||||
};
|
||||
|
||||
/* Segment offsets of the 32 sImageTableEntry arrays (12-byte entries). */
|
||||
static const u32 s_tbl_offs[] = {
|
||||
0xAC8, 0xAD4, 0xBC4, 0xC0C, 0xC48, 0xC54, 0xC60, 0xC6C,
|
||||
0xC78, 0xC84, 0xC90, 0xC9C, 0xCA8, 0xCB4, 0xCC0, 0xCCC,
|
||||
0xCD8, 0xCE4, 0xCF0, 0xCFC, 0xD08, 0xD14, 0xD20, 0xD2C,
|
||||
0xD38, 0xD44, 0xD5C, 0xFB4, 0xFD8, 0x1020, 0x102C, 0x132C
|
||||
};
|
||||
|
||||
#define N_DLS ((int)(sizeof(s_dl_offs) / sizeof(s_dl_offs[0])))
|
||||
#define N_TBLS ((int)(sizeof(s_tbl_offs) / sizeof(s_tbl_offs[0])))
|
||||
|
||||
/* Compiled shadows of the ROM-copied display lists (assets/oddtextures.c).
|
||||
* Incomplete-type externs are fine: we only do pointer arithmetic into them. */
|
||||
extern Gfx globalDL_0x000;
|
||||
extern Gfx globalDL_0x078;
|
||||
extern Gfx globalDL_0x120;
|
||||
extern Gfx globalDL_0x1c8;
|
||||
extern Gfx globalDL_0x270;
|
||||
extern Gfx globalDL_0x318;
|
||||
extern Gfx globalDL_0x3c0;
|
||||
extern Gfx globalDL_0x468;
|
||||
extern Gfx globalDL_0x510;
|
||||
extern Gfx globalDL_0x5b8;
|
||||
extern Gfx globalDL_0x660;
|
||||
extern Gfx globalDL_0x708;
|
||||
extern Gfx globalDL_0x7b0;
|
||||
extern Gfx globalDL_0x858;
|
||||
extern Gfx globalDL_0x900;
|
||||
extern Gfx globalDL_0x9a8;
|
||||
extern Gfx globalDL_0xa50;
|
||||
|
||||
static Gfx *const s_dl_syms[] = {
|
||||
&globalDL_0x000, &globalDL_0x078, &globalDL_0x120, &globalDL_0x1c8,
|
||||
&globalDL_0x270, &globalDL_0x318, &globalDL_0x3c0, &globalDL_0x468,
|
||||
&globalDL_0x510, &globalDL_0x5b8, &globalDL_0x660, &globalDL_0x708,
|
||||
&globalDL_0x7b0, &globalDL_0x858, &globalDL_0x900, &globalDL_0x9a8,
|
||||
&globalDL_0xa50
|
||||
};
|
||||
|
||||
void gimgFixupGlobalimagetable(u8 *base)
|
||||
{
|
||||
int i, j;
|
||||
u8 *end = base + GIMG_REGION_SIZE;
|
||||
|
||||
/* 1) Gfx display lists: bswap the w1 word of every IMAGESEG-marked
|
||||
* G_SETTIMG command. In the raw BE bytes the marker sits at
|
||||
* offsets 4..5 (AB CD); after the swap it is at 6..7 (CD AB). */
|
||||
for (i = 0; i < N_DLS; i++)
|
||||
{
|
||||
u8 *p = base + s_dl_offs[i];
|
||||
|
||||
while (p + 8 <= end && p[0] != (u8)G_ENDDL)
|
||||
{
|
||||
if (p[4] == 0xAB && p[5] == 0xCD)
|
||||
{
|
||||
u32 *w1 = (u32 *)(p + 4);
|
||||
*w1 = __builtin_bswap32(*w1);
|
||||
}
|
||||
p += 8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2) sImageTableEntry arrays: bswap the index field (word 0 of each
|
||||
* 12-byte entry). Entry counts fall out of the D39 layout: each
|
||||
* table runs up to the next symbol. */
|
||||
for (i = 0; i < N_TBLS; i++)
|
||||
{
|
||||
u32 start = s_tbl_offs[i];
|
||||
u32 stop = (i + 1 < N_TBLS) ? s_tbl_offs[i + 1] : GIMG_REGION_SIZE;
|
||||
u32 count = (stop - start) / 12u;
|
||||
|
||||
for (j = 0; j < (int)count; j++)
|
||||
{
|
||||
u32 *idx = (u32 *)(base + start + (u32)j * 12u);
|
||||
*idx = __builtin_bswap32(*idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gimgSyncCompiledGlobalDLs(u8 *base)
|
||||
{
|
||||
int i;
|
||||
u8 *end = base + GIMG_REGION_SIZE;
|
||||
|
||||
for (i = 0; i < N_DLS; i++)
|
||||
{
|
||||
u8 *p = base + s_dl_offs[i];
|
||||
Gfx *dst = s_dl_syms[i];
|
||||
int j = 0;
|
||||
|
||||
while (p + 8 <= end && p[0] != (u8)G_ENDDL)
|
||||
{
|
||||
/* Post-fixup marker position: LE encoding of 0xABCDxxxx puts
|
||||
* CD AB at byte offsets 6..7. */
|
||||
if (p[6] == 0xCD && p[7] == 0xAB)
|
||||
{
|
||||
dst[j].words.w1 = *(u32 *)(p + 4);
|
||||
}
|
||||
p += 8;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
+334
-3
@@ -87,7 +87,16 @@ OSTime osGetTime(void)
|
||||
|
||||
u32 osGetCount(void)
|
||||
{
|
||||
return (u32)osGetTime();
|
||||
/* The N64 RSP core counter advances at ~OS_CPU_COUNTER (~46.5 MHz), NOT
|
||||
* microseconds. GE's pacing assumes this rate: waitForNextFrame() in
|
||||
* frametiming.c waits for 775,875 - 387,937 ticks per NTSC frame (931,050
|
||||
* per PAL frame) and bossMainloop gates DL builds on
|
||||
* MAIN_LOOP_TICK_INTERVAL = 387,937 ticks. Feeding it microseconds made
|
||||
* waitForNextFrame block ~388 ms/frame and the gate never pass in steady
|
||||
* state (hang after the first couple of frames). Scale real time to
|
||||
* 46.5525 ticks/us (= 775875/16666.67us = 931050/20000us, both regions)
|
||||
* and wrap like the 32-bit hardware counter. */
|
||||
return (u32)(((uint64_t)sysGetMicroseconds() * 465525ull) / 10000ull);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ */
|
||||
@@ -249,6 +258,7 @@ void portKernelInit(void)
|
||||
{
|
||||
memset(g_pt, 0, sizeof(g_pt));
|
||||
g_pqCount = 0;
|
||||
d63WatchdogStart(); /* TEMP D63 */
|
||||
/* Anchor the heartbeat clock so the first check doesn't compare against
|
||||
* epoch 0 (the host QPC has a large absolute offset). */
|
||||
g_lastFrameUs = sysGetMicroseconds();
|
||||
@@ -379,10 +389,24 @@ void osSetThreadPri(OSThread *t, OSPri prio)
|
||||
/* Message queues */
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
/* D59: the lookup/create below MUST be serialized. Two threads first
|
||||
* touching the same (or different) OSMesgQueue concurrently used to race on
|
||||
* g_pqCount and end up with two PortQueues — or one re-initialized slot —
|
||||
* for a single queue. Sends then signal a cond the receiver never waits on
|
||||
* (lost wakeup: permanent stall after frame 2), and re-initing a live mutex
|
||||
* is UB that can corrupt unrelated state (wild-pointer crashes). Boot-time
|
||||
* osCreateMesgQueue() pre-registers most queues, but first-touches still race
|
||||
* for any queue created later or touched before its create call. */
|
||||
static pthread_mutex_t s_pqLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static PortQueue *portQueueGet(OSMesgQueue *mq)
|
||||
{
|
||||
pthread_mutex_lock(&s_pqLock);
|
||||
for (int i = 0; i < g_pqCount; ++i) {
|
||||
if (g_pq[i].os == mq) return &g_pq[i];
|
||||
if (g_pq[i].os == mq) {
|
||||
pthread_mutex_unlock(&s_pqLock);
|
||||
return &g_pq[i];
|
||||
}
|
||||
}
|
||||
if (g_pqCount >= PORT_MAX_QUEUES) {
|
||||
sysFatalError("portQueueGet: out of port queue slots");
|
||||
@@ -392,6 +416,7 @@ static PortQueue *portQueueGet(OSMesgQueue *mq)
|
||||
pthread_mutex_init(&pq->lock, NULL);
|
||||
pthread_cond_init(&pq->cond, NULL);
|
||||
pq->loggedFirstBlock = 0;
|
||||
pthread_mutex_unlock(&s_pqLock);
|
||||
return pq;
|
||||
}
|
||||
|
||||
@@ -441,9 +466,162 @@ void osEnqueueMesg(OSMesgQueue *mq, OSMesg msg)
|
||||
pthread_mutex_unlock(&pq->lock);
|
||||
}
|
||||
|
||||
/* TEMP D62: full message-flow trace (env GE_D62=1 -> d62mesg.log). */
|
||||
static FILE *s_d62log = NULL;
|
||||
static int s_d62opened = 0;
|
||||
static pthread_mutex_t s_d62lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static void d62log(const char *op, OSMesgQueue *mq, OSMesg msg)
|
||||
{
|
||||
if (!s_d62opened) {
|
||||
s_d62opened = 1;
|
||||
if (getenv("GE_D62"))
|
||||
s_d62log = fopen("d62mesg.log", "w");
|
||||
}
|
||||
if (s_d62log) {
|
||||
pthread_mutex_lock(&s_d62lock);
|
||||
fprintf(s_d62log, "%s mq=%p msg=%p valid=%d/%d first=%d\n",
|
||||
op, (void *)mq, (void *)msg, mq->validCount,
|
||||
mq->msgCount, mq->first);
|
||||
fflush(s_d62log);
|
||||
pthread_mutex_unlock(&s_d62lock);
|
||||
}
|
||||
}
|
||||
|
||||
/* TEMP D63: watch the gun-barrel sub-DL slot (V1+0x12EC38) for clobbering.
|
||||
* Hooked in osSendMesg/osRecvMesg so both game threads are sampled often. */
|
||||
void d63SlotLog(const char *tag)
|
||||
{
|
||||
if (!getenv("GE_D63")) return;
|
||||
u32 cur = *(const u32 *)0x7012EC38;
|
||||
sysLogPrintf(LOG_NOTE, "D63 slot %s: word@0x7012ec38=%08x", tag, cur);
|
||||
}
|
||||
|
||||
/* TEMP D63: log only when the slot value changes (first 8 per tag). */
|
||||
void d63SlotCheck(const char *tag)
|
||||
{
|
||||
if (!getenv("GE_D63")) return;
|
||||
static const char *s_tags[16];
|
||||
static u32 s_vals[16];
|
||||
static int s_n = 0, s_count = 0;
|
||||
u32 cur = *(const u32 *)0x7012EC38;
|
||||
int i;
|
||||
for (i = 0; i < s_n; i++) {
|
||||
if (s_tags[i] == tag) {
|
||||
if (cur != s_vals[i] && s_count < 64) {
|
||||
sysLogPrintf(LOG_NOTE, "D63 check %s: %08x -> %08x", tag, s_vals[i], cur);
|
||||
s_vals[i] = cur;
|
||||
++s_count;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (s_n < 16) { s_tags[s_n] = tag; s_vals[s_n] = cur; ++s_n; }
|
||||
}
|
||||
|
||||
/* TEMP D63: timestamped cross-thread activity ring + watchdog thread. */
|
||||
typedef struct { uint64_t t; unsigned long tid; const char *tag; } D63Act;
|
||||
#define D63_ACT_N 2048
|
||||
static D63Act s_d63act[D63_ACT_N];
|
||||
static volatile long s_d63actIdx = 0;
|
||||
static int s_d63on = -1;
|
||||
|
||||
int d63On(void) { if (s_d63on < 0) s_d63on = getenv("GE_D63") != 0; return s_d63on; }
|
||||
|
||||
unsigned long d63Tid(void)
|
||||
{
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
return GetCurrentThreadId();
|
||||
#else
|
||||
return (unsigned long)pthread_self();
|
||||
#endif
|
||||
}
|
||||
|
||||
void d63Act(const char *tag)
|
||||
{
|
||||
if (!d63On()) return;
|
||||
long i = s_d63actIdx++;
|
||||
s_d63act[i & (D63_ACT_N - 1)] = (D63Act){ sysGetMicroseconds(), d63Tid(), tag };
|
||||
}
|
||||
|
||||
static void d63DumpActivity(uint64_t now)
|
||||
{
|
||||
long newest = s_d63actIdx - 1;
|
||||
int k;
|
||||
for (k = 47; k >= 0; --k) {
|
||||
long i = newest - k;
|
||||
if (i < 0) break;
|
||||
const D63Act *e = &s_d63act[i & (D63_ACT_N - 1)];
|
||||
sysLogPrintf(LOG_NOTE, "D63 act t=%llu d=%lld tid=%lu %s",
|
||||
(unsigned long long)e->t,
|
||||
(long long)(e->t >= now ? (long long)(e->t - now) : -(long long)(now - e->t)),
|
||||
e->tid, e->tag);
|
||||
}
|
||||
}
|
||||
|
||||
static void d63WatchdogThread(void *arg)
|
||||
{
|
||||
const u32 *slot = (const u32 *)0x7012EC38;
|
||||
u32 last = *slot;
|
||||
int changes = 0;
|
||||
for (;;) {
|
||||
u32 cur = *slot;
|
||||
if (cur != last) {
|
||||
uint64_t now = sysGetMicroseconds();
|
||||
if (changes < 8) {
|
||||
sysLogPrintf(LOG_NOTE, "D63 WATCHDOG t=%llu word@0x7012ec38 %08x -> %08x",
|
||||
(unsigned long long)now, last, cur);
|
||||
d63DumpActivity(now);
|
||||
}
|
||||
++changes;
|
||||
last = cur;
|
||||
}
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
Sleep(0); /* yield to same-priority threads */
|
||||
#else
|
||||
sched_yield();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void d63WatchdogStart(void)
|
||||
{
|
||||
if (!d63On()) return;
|
||||
static pthread_t th;
|
||||
pthread_create(&th, NULL,
|
||||
(void *(*)(void *))d63WatchdogThread, NULL);
|
||||
pthread_detach(th);
|
||||
sysLogPrintf(LOG_NOTE, "D63 watchdog started");
|
||||
}
|
||||
|
||||
static void d63WatchGunbarrelSlot(void)
|
||||
{
|
||||
if (!d63On()) return;
|
||||
static u32 s_last = 0;
|
||||
static int s_init = 0;
|
||||
static int s_changes = 0;
|
||||
const u32 *slot = (const u32 *)0x7012EC38;
|
||||
u32 cur = *slot;
|
||||
if (!s_init) { s_init = 1; s_last = cur; return; }
|
||||
if (cur != s_last && s_changes < 64) {
|
||||
sysLogPrintf(LOG_NOTE, "D63 clobber-watch: word@0x7012ec38 %08x -> %08x tid=%lu changes=%d",
|
||||
s_last, cur, d63Tid(), s_changes);
|
||||
s_last = cur;
|
||||
++s_changes;
|
||||
}
|
||||
}
|
||||
|
||||
s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
|
||||
{
|
||||
PortQueue *pq = portQueueGet(mq);
|
||||
d62log("SEND", mq, msg); /* TEMP D62 */
|
||||
d63Act("send"); /* TEMP D63 */
|
||||
d63WatchGunbarrelSlot(); /* TEMP D63 */
|
||||
/* TEMP D51: watch sends to the 32-slot queues (sched cmdQ + client Qs) */
|
||||
if (getenv("GE_D51") && mq->msgCount == 32 && !(g_viRetraceMQ && mq == g_viRetraceMQ)) {
|
||||
void *ra = __builtin_return_address(0);
|
||||
sysLogPrintf(LOG_NOTE, "D51 send32 mq=%p from %p msg=%p valid=%d/%d",
|
||||
(void *)mq, ra, (void *)msg, mq->validCount, mq->msgCount);
|
||||
}
|
||||
pthread_mutex_lock(&pq->lock);
|
||||
while (mq->validCount >= mq->msgCount) {
|
||||
if (flag != OS_MESG_BLOCK) {
|
||||
@@ -459,9 +637,26 @@ s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* TEMP D60: gfxFrameMsgQ receive watch (env GE_D60=1). The main loop only
|
||||
* acts on msg type 1 (retrace) / 4 (RSP done); anything else means the
|
||||
* message object's .type field was clobbered. */
|
||||
extern OSMesgQueue gfxFrameMsgQ; /* src/init.c */
|
||||
static void d60logRecv(OSMesgQueue *mq, OSMesg m) {
|
||||
if (mq != &gfxFrameMsgQ || !getenv("GE_D60")) return;
|
||||
static uint64_t n = 0;
|
||||
const u16 type = *(const u16 *)m; /* OSScMsg.type */
|
||||
if (n < 20 || (n % 300) == 0 || (type != 1 && type != 4))
|
||||
sysLogPrintf(LOG_NOTE, "D60 recv #%llu mq=%p msg=%p type=%u",
|
||||
(unsigned long long)n, (void *)mq, (void *)m, type);
|
||||
++n;
|
||||
}
|
||||
|
||||
s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
|
||||
{
|
||||
PortQueue *pq = portQueueGet(mq);
|
||||
d62log("RECV", mq, 0); /* TEMP D62 */
|
||||
d63Act("recv"); /* TEMP D63 */
|
||||
d63WatchGunbarrelSlot(); /* TEMP D63 */
|
||||
pthread_mutex_lock(&pq->lock);
|
||||
while (mq->validCount == 0) {
|
||||
if (flag != OS_MESG_BLOCK) {
|
||||
@@ -484,6 +679,7 @@ s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
|
||||
mq->first = (mq->first + 1) % mq->msgCount;
|
||||
--mq->validCount;
|
||||
if (msg) *msg = m;
|
||||
d60logRecv(mq, m); /* TEMP D60 */
|
||||
pthread_cond_signal(&pq->cond);
|
||||
pthread_mutex_unlock(&pq->lock);
|
||||
return 0;
|
||||
@@ -531,12 +727,22 @@ void osViSetEvent(OSMesgQueue *mq, OSMesg msg, u32 retraceCount)
|
||||
g_viRetraceMsg = msg;
|
||||
}
|
||||
|
||||
/* TEMP D51: instrument the retrace pacemaker */
|
||||
static uint64_t g_viPostCount = 0;
|
||||
static void portPostVIEvent(void)
|
||||
{
|
||||
if (g_viRetraceMQ) {
|
||||
osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK);
|
||||
s32 r = osSendMesg(g_viRetraceMQ, g_viRetraceMsg, OS_MESG_NOBLOCK);
|
||||
if (++g_viPostCount % 60 == 1 || r != 0) {
|
||||
sysLogPrintf(r != 0 ? LOG_ERROR : LOG_NOTE,
|
||||
"D51 vi post #%llu mq=%p msg=%d valid=%d/%d ret=%d",
|
||||
(unsigned long long)g_viPostCount, (void *)g_viRetraceMQ,
|
||||
(int)g_viRetraceMsg, g_viRetraceMQ->validCount,
|
||||
g_viRetraceMQ->msgCount, r);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* END TEMP D51 */
|
||||
|
||||
void osViSetMode(OSViMode *vm)
|
||||
{
|
||||
@@ -604,11 +810,53 @@ void osAiSetConvert(u32 convert) { (void)convert; }
|
||||
* completion; osPiStartDma() below replicates that, because the game blocks
|
||||
* in romReceiveMesg()/osRecvMesg() after every romCopy().
|
||||
*/
|
||||
/* TEMP D60: identify sidecar model reads (env GE_D60=1). srcPA-base is the
|
||||
* manifest offset; cross-reference data/pcmodels-<region>/manifest.csv for
|
||||
* the file name. */
|
||||
extern uintptr_t pcmodelsSidecarBase(void);
|
||||
extern uint32_t pcmodelsTotalSize(void);
|
||||
static void d60logSidecarRead(u32 srcPA, void *dstVA, u32 size) {
|
||||
if (!getenv("GE_D60")) return;
|
||||
uintptr_t base = pcmodelsSidecarBase();
|
||||
if (base && srcPA >= base && srcPA < base + pcmodelsTotalSize())
|
||||
sysLogPrintf(LOG_NOTE, "D60 sidecar read off=%llu size=0x%X dst=%p",
|
||||
(unsigned long long)(srcPA - base), size, dstVA);
|
||||
}
|
||||
|
||||
/* TEMP D60/D61: a ROM-read DMA target must land in the game DRAM views
|
||||
* (V1/V2) or the current thread's stack (texLoad's compbuffer). Anything
|
||||
* else is unmapped host memory on PC. */
|
||||
static FILE *s_d61log = NULL;
|
||||
static int s_d61opened = 0;
|
||||
|
||||
static int dramHostAddrValid(uintptr_t addr, u32 size)
|
||||
{
|
||||
static const uintptr_t bases[2] = { 0x70000000UL, 0x80000000UL };
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (addr >= bases[i] && addr + size <= bases[i] + 0x00800000UL)
|
||||
return 1;
|
||||
}
|
||||
/* Any other host-committed region is a legitimate DMA target: .bss/.data
|
||||
* buffers (e.g. ramrom_data_target), stack compbuffers, sidecar images.
|
||||
* Truncated wild addresses (0x40xxxxxx from s32 pointer math) are not
|
||||
* committed, so VirtualQuery still catches them. */
|
||||
{
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi)) == sizeof(mbi) &&
|
||||
mbi.State == MEM_COMMIT &&
|
||||
(uintptr_t)mbi.BaseAddress <= addr &&
|
||||
(uintptr_t)mbi.BaseAddress + mbi.RegionSize >= addr + size)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size)
|
||||
{
|
||||
if (size == 0)
|
||||
return;
|
||||
if (direction == OS_READ) {
|
||||
d60logSidecarRead(srcPA, dstVA, size); /* TEMP D60 */
|
||||
if (!romdataCartAddrValid(srcPA, size)) {
|
||||
sysLogPrintf(LOG_WARNING,
|
||||
"osPiStartDma: ROM read out of range "
|
||||
@@ -616,6 +864,50 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size)
|
||||
srcPA, size);
|
||||
return;
|
||||
}
|
||||
/* TEMP D60: validate the DMA target too. The N64 PI happily DMAs to
|
||||
* any KSEG address; on PC an unmapped target is a wild memcpy.
|
||||
* Log the whole call chain (romCopy <- doRomCopy <- osPiStartDma) so
|
||||
* the game-side caller can be symbolicated offline. */
|
||||
/* TEMP D61: log every ROM read (dst/src/size). The last line before
|
||||
* a crash identifies the culprit; dst symbolizes offline via nm. */
|
||||
if (!s_d61opened && getenv("GE_D61")) {
|
||||
s_d61log = fopen("d61dma.log", "w");
|
||||
s_d61opened = 1;
|
||||
}
|
||||
if (s_d61log) {
|
||||
static int d61count = 0;
|
||||
fprintf(s_d61log, "D61 %06d dst=%p src=0x%08X size=0x%X\n",
|
||||
++d61count, dstVA, srcPA, size);
|
||||
fflush(s_d61log);
|
||||
}
|
||||
if (!dramHostAddrValid((uintptr_t)dstVA, size)) {
|
||||
/* No __builtin_return_address here: the caller's frame chain is
|
||||
* not always walkable (it faulted). Log the raw stack window
|
||||
* instead — return addresses are in it and symbolicate offline.
|
||||
* The FATAL below re-dumps RSP/registers via the crash handler. */
|
||||
const uint64_t *sp = (const uint64_t *)__builtin_frame_address(0);
|
||||
char win[1200] = "";
|
||||
char *wp = win;
|
||||
for (int i = 0; i < 32; i++) {
|
||||
wp += snprintf(wp, win + sizeof(win) - (wp - win),
|
||||
" %p", (void *)sp[i]);
|
||||
}
|
||||
{
|
||||
PVOID tlow = NULL, thigh = NULL;
|
||||
GetCurrentThreadStackLimits(&tlow, &thigh);
|
||||
sysLogPrintf(LOG_ERROR,
|
||||
"D60 thread stack: %p..%p dst-in-stack=%d\n",
|
||||
(void *)tlow, (void *)thigh,
|
||||
((uintptr_t)dstVA >= (uintptr_t)tlow &&
|
||||
(uintptr_t)dstVA < (uintptr_t)thigh));
|
||||
}
|
||||
sysLogPrintf(LOG_ERROR,
|
||||
"D60 BAD DMA TARGET dst=%p src=0x%08X size=0x%X "
|
||||
"stack@rbp:%s\n",
|
||||
dstVA, srcPA, size, win);
|
||||
sysFatalError("D60: ROM-read target %p + 0x%X not host-mapped "
|
||||
"(src=0x%08X)", dstVA, size, srcPA);
|
||||
}
|
||||
memcpy(dstVA, (const void *)(uintptr_t)srcPA, size);
|
||||
} else {
|
||||
/* OS_WRITE: the game never writes the cart (saves go to EEPROM via
|
||||
@@ -804,12 +1096,51 @@ void osSpTaskLoad(OSTask *t) { (void)t; /* nothing to load on the host */ }
|
||||
|
||||
void osSpTaskStartGo(OSTask *t)
|
||||
{
|
||||
d63Act(t->t.type == M_AUDTASK ? "audtask" : "spgo"); /* TEMP D63 */
|
||||
if (t->t.type == M_AUDTASK) {
|
||||
/* Phase 3: execute the audio ucode against t->t.data_ptr (an Acmd
|
||||
* list). For now the task simply completes; libaudio's amMain still
|
||||
* runs its per-frame bookkeeping. */
|
||||
} else {
|
||||
/* Graphics task: run the software RSP on the display list. */
|
||||
/* TEMP D63: log every gfx task's DL pointer (env GE_D63=1) */
|
||||
if (getenv("GE_D63")) {
|
||||
unsigned long tid = 0;
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
tid = GetCurrentThreadId();
|
||||
#else
|
||||
tid = (unsigned long)pthread_self();
|
||||
#endif
|
||||
sysLogPrintf(LOG_NOTE, "D63 gfx task data_ptr=%p data_size=%d tid=%lu slot=%08x",
|
||||
(void *)t->t.data_ptr, (int)t->t.data_size, tid,
|
||||
*(const u32 *)0x7012EC38);
|
||||
}
|
||||
#if defined(PORT)
|
||||
/* TEMP D63: find when the fatal G_DL value first appears in the master DL */
|
||||
{
|
||||
static int s_d63seen = 0;
|
||||
if (getenv("GE_D63") && !s_d63seen) {
|
||||
const uint32_t *p = (const uint32_t *)t->t.data_ptr;
|
||||
size_t nwords = t->t.data_size / 4;
|
||||
static int s_d63scancount = 0;
|
||||
if ((s_d63scancount++ % 100) == 0)
|
||||
sysLogPrintf(LOG_NOTE, "D63 scan running: ptr=%p nwords=%zu", (void *)p, nwords);
|
||||
for (size_t i = 1; i + 1 < nwords; i += 2) {
|
||||
if (p[i] == 0x0012EC38u) {
|
||||
s_d63seen = 1;
|
||||
sysLogPrintf(LOG_NOTE, "D63 fatal-value first seen: frame=%d off=0x%zx words:",
|
||||
g_framesRendered + 1, i * 4);
|
||||
for (int k = -3; k <= 3; ++k) {
|
||||
long j = (long)i + 2 * k;
|
||||
if (j >= 0 && j + 1 < (long)nwords)
|
||||
sysLogPrintf(LOG_NOTE, "D63 [%d] %08x %08x", k, p[j], p[j+1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
uint64_t t0 = sysGetMicroseconds();
|
||||
videoStartFrame();
|
||||
gfx_run((Gfx *)t->t.data_ptr);
|
||||
|
||||
@@ -135,6 +135,7 @@ void osSyncPrintf(const char *fmt, ...)
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
fflush(stderr); /* keep probe lines intact across crashes (block-buffered when redirected) */
|
||||
}
|
||||
|
||||
/* --- VI manager (src/libultrare/io/vimgr.c, not built for PC) ----------- */
|
||||
|
||||
@@ -234,3 +234,8 @@ uint32_t pcmodelsTotalSize(void)
|
||||
{
|
||||
return s_loaded ? s_total : 0;
|
||||
}
|
||||
|
||||
uintptr_t pcmodelsSidecarBase(void)
|
||||
{
|
||||
return s_loaded ? (uintptr_t)s_base : 0;
|
||||
}
|
||||
+41
-2
@@ -102,6 +102,8 @@ static int romHeaderValid(const u8 *h, char *err, size_t errsz)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static u32 romdataBswap32(u32 v); /* defined below; used by the D55 fixup */
|
||||
|
||||
int romdataInit(void)
|
||||
{
|
||||
char err[128] = "";
|
||||
@@ -175,6 +177,23 @@ int romdataInit(void)
|
||||
tok, romSize, CART_BASE,
|
||||
sideTotal ? ", + model sidecars" : "");
|
||||
pcmodelsLoadSidecars(CART_BASE, romSize);
|
||||
|
||||
/* D55 (docs/PCPortResearch.md): the RLE folder-menu background at
|
||||
* `unknown2` (romassets_<r>.s) is stored in the ROM with a
|
||||
* big-endian w/h header (01 B8 01 2B = 440x299), but
|
||||
* rle_expand_8bit() reads it little-endian. The N64 build embeds
|
||||
* this asset into .data via assets/romfiles2.s from an extracted
|
||||
* .bin whose header is byte-swapped (title2.c hardcodes 440x299
|
||||
* row rendering, and this is the only decodable 440x299 RLE
|
||||
* stream in the ROM), so make the cart copy match it: swap the
|
||||
* w/h word in place. The guard makes the fixup a no-op if a
|
||||
* region's copy already reads as a plausible LE size. */
|
||||
{
|
||||
extern void *unknown2; /* absolute cart address */
|
||||
u32 *hdr = (u32 *)&unknown2;
|
||||
if ((u16)*hdr > 512 || (u16)(*hdr >> 16) > 512)
|
||||
*hdr = romdataBswap32(*hdr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (at)
|
||||
@@ -372,6 +391,23 @@ void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* D54 (docs/PCPortResearch.md): a decompressed compact-sequence (cseq) file
|
||||
* starts with struct ALCMidiHdr — 16 big-endian u32 trackOffset values plus a
|
||||
* big-endian u32 division. N64 reads them natively; on a little-endian host
|
||||
* alCSeqNew() byte-swaps them (track 0 offset 0x44 becomes 0x44000000),
|
||||
* builds "valid" curLoc pointers ~1 GB past the buffer, and __getTrackByte()
|
||||
* faults. The rest of the stream is byte-oriented (varlens, MIDI bytes, BE
|
||||
* loop offsets assembled byte-by-byte), so only these 17 words need fixing.
|
||||
*/
|
||||
void romdataFixupCseq(u8 *blob)
|
||||
{
|
||||
u32 *hdr = (u32 *)blob;
|
||||
for (u32 i = 0; i < 17; i++) {
|
||||
hdr[i] = romdataBswap32(hdr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* D50 (docs/PCPortResearch.md): a language bank is [u32 offsets x N][text].
|
||||
* Offsets are big-endian and point into the text region; langGet() reads them
|
||||
@@ -464,8 +500,11 @@ void romdataFixupFont(u8 *blob, u32 n64Size)
|
||||
o = romdataBswap32(*(const u32 *)(s + 20));
|
||||
if (o != 0 && pixStart != 0 && o >= pixStart)
|
||||
o = pcPixOff + (o - pixStart);
|
||||
*(u32 *)(d + 20) = o; /* zero-extended relative offset */
|
||||
*(u32 *)(d + 24) = 0;
|
||||
/* D51: on PC pixeldata is a u64 at d+24 (fontchar needs 8-align,
|
||||
* sizeof=32) — the N64 u32-at-d+20 layout does not hold. Write the
|
||||
* offset into the pointer and zero-extend it. */
|
||||
*(u32 *)(d + 24) = o;
|
||||
*(u32 *)(d + 28) = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3022,6 +3022,19 @@ enum CCRMLUT
|
||||
BODY_Male_Pierce_Bond_Tuxedo,
|
||||
BODY_Natalya_Jungle_Fatigues,
|
||||
BODIES_MAX
|
||||
#ifdef PORT
|
||||
/* D65b: same class of bug as D65 (HEADS). ROM tables store 0xFFFFFFFF in
|
||||
* body fields as a "no body" sentinel and the cast-end check compares
|
||||
* `intro_char_table[f].body < 0`. The N64 toolchain gave this enum a
|
||||
* signed underlying type, so the sentinel read as -1. PC GCC 16 picks
|
||||
* unsigned int for enums whose enumerators are all non-negative, which
|
||||
* makes `body < 0` always false and the compiler deletes the reset
|
||||
* branch entirely — the cast screen then renders the terminator entry
|
||||
* (text1 == 0) and langGet(0) dereferences a NULL bank. A negative
|
||||
* enumerator forces the signed underlying type without changing any
|
||||
* existing value or BODIES_MAX. */
|
||||
, BODY_FIXED = -1
|
||||
#endif
|
||||
} BODIES;
|
||||
|
||||
typedef enum GENDER
|
||||
@@ -3082,8 +3095,19 @@ enum CCRMLUT
|
||||
HEAD_COUNT = HEAD_END - HEAD_START, // Total number of heads
|
||||
HEAD_MALE_COUNT = HEAD_F_START - HEAD_START, // Total number of usable randon male heads
|
||||
HEAD_FEMALE_COUNT = HEAD_BOND_START - HEAD_F_START, // Total number of usable randon female heads
|
||||
#ifdef PORT
|
||||
/* D65: the N64 toolchain treated these sentinels as signed (HEAD_FIXED == -1),
|
||||
* so `head >= 0` guards compiled to real branches. PC GCC picks unsigned int
|
||||
* as this enum's underlying type (0xFFFFFFFF > INT_MAX), which makes every
|
||||
* `head >= 0` always true and turns c_item_entries[HEAD_FIXED] into a wild
|
||||
* 64-bit OOB read (SIGSEGV in init_menu18_displaycast). Negative literals
|
||||
* keep the bit pattern identical while restoring the signed semantics. */
|
||||
HEAD_FIXED = -1,
|
||||
HEAD_RANDOM = -97
|
||||
#else
|
||||
HEAD_FIXED = 0xFFFFFFFF,
|
||||
HEAD_RANDOM = 0xFFFFFF9F
|
||||
#endif
|
||||
} HEADS;
|
||||
|
||||
//Canonical name and style "ai_destroyobj 2 : (def->obj == PROP_ELVIS_SAUCER)\n"
|
||||
|
||||
@@ -1493,7 +1493,21 @@ typedef union
|
||||
* List of length model->obj->numMatrices dynamically allocated.
|
||||
*/
|
||||
RenderPosView *render_pos; /*0x0c*/
|
||||
#ifdef PORT
|
||||
/* PC port (D52): `datas` is the model's RW-data pool itself, not an
|
||||
* array of pointers. RwDataIndex values are 4-byte WORD offsets:
|
||||
* modelCalculateRwDataIndexes() accumulates sizeof(record)/4 and
|
||||
* sizes the pool as round16(numRecords*4) bytes (modelmgrInstantiate*
|
||||
*ModelWithAnim). On N64, `union ModelRwData **` strides 4B so
|
||||
* &datas[i] lands on record i; on PC the same type strides 8B and
|
||||
* every non-zero index addresses the wrong record (crash in
|
||||
* modelInitRwData at the first BSP node). u32* keeps the N64 byte
|
||||
* math exactly; use sites cast to the record type. Layout is a
|
||||
* single pointer either way, so struct Model is unchanged. */
|
||||
u32 *datas;
|
||||
#else
|
||||
union ModelRwData **datas; // array of pointers to modeldata structs /*0x10*/
|
||||
#endif
|
||||
|
||||
f32 scale; /*0x14*/
|
||||
struct Model *attachedto; /*0x18*/
|
||||
|
||||
@@ -2337,6 +2337,16 @@ s32 bgLoadRoomPrimaryGdl(s32 roomnum, u8 *dst, s32 allocsize)
|
||||
roominfo->ptr_expanded_mapping_info = dst;
|
||||
roominfo->usize_primary_DL_binary = expanded_size;
|
||||
|
||||
#if defined(PORT)
|
||||
/* TEMP D63: log primary GDL load result (env GE_D63=1) */
|
||||
if (getenv("GE_D63")) {
|
||||
u32 *w = (u32 *)dst;
|
||||
osSyncPrintf("D63 bgLoadRoomPrimaryGdl room=%d dst=%p expanded=%d final=%d w0..3=(%08x,%08x,%08x,%08x)\n",
|
||||
roomnum, (void *)dst, (int)expanded_size, (int)size,
|
||||
w[0], w[1], w[2], w[3]);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Return the uncompressed data size.
|
||||
return expanded_size;
|
||||
}
|
||||
@@ -2663,8 +2673,12 @@ void bgRoomsTickUnload(void)
|
||||
* Ensures the room's bg data is loaded if budget allows, then appends its display list.
|
||||
* Also resets the age of rendered rooms so bgRoomsTickUnload won't unload it.
|
||||
*/
|
||||
static int d63bgprimarycount = 0;
|
||||
|
||||
Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index)
|
||||
{
|
||||
if (getenv("GE_D63") && (d63bgprimarycount++ % 500 == 0))
|
||||
osSyncPrintf("D63 bgRenderRoomPrimary called count=%d room=%d\n", d63bgprimarycount, room_index);
|
||||
if (room_index >= g_MaxNumRooms)
|
||||
{
|
||||
return gdl;
|
||||
@@ -2689,6 +2703,16 @@ Gfx *bgRenderRoomPrimary(Gfx *gdl, s32 room_index)
|
||||
{
|
||||
gdl = applyRoomMatrixToDisplayList(gdl, room_index);
|
||||
|
||||
#if defined(PORT)
|
||||
/* TEMP D63: log primary GDL at render time (env GE_D63=1) */
|
||||
if (getenv("GE_D63")) {
|
||||
u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info;
|
||||
osSyncPrintf("D63 bgRenderRoomPrimary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n",
|
||||
room_index, (void *)g_BgRoomInfo[room_index].ptr_expanded_mapping_info,
|
||||
(int)g_BgRoomInfo[room_index].usize_primary_DL_binary,
|
||||
w[0], w[1], w[2], w[3]);
|
||||
}
|
||||
#endif
|
||||
gSPSegment(gdl++, SPSEGMENT_BG_VTX, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].vertices));
|
||||
gSPDisplayList(gdl++, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].ptr_expanded_mapping_info));
|
||||
|
||||
@@ -2724,6 +2748,16 @@ Gfx *bgRenderRoomSecondary(Gfx *gdl, s32 room_index)
|
||||
{
|
||||
gdl = applyRoomMatrixToDisplayList(gdl, room_index);
|
||||
|
||||
#if defined(PORT)
|
||||
/* TEMP D63: log secondary GDL at render time (env GE_D63=1) */
|
||||
if (getenv("GE_D63")) {
|
||||
u32 *w = (u32 *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info;
|
||||
osSyncPrintf("D63 bgRenderRoomSecondary room=%d ptr=%p usize=%d w0..3=(%08x,%08x,%08x,%08x)\n",
|
||||
room_index, (void *)g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info,
|
||||
(int)g_BgRoomInfo[room_index].usize_secondary_DL_binary,
|
||||
w[0], w[1], w[2], w[3]);
|
||||
}
|
||||
#endif
|
||||
gSPSegment(gdl++, SPSEGMENT_BG_VTX, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].vertices));
|
||||
gSPDisplayList(gdl++, OS_K0_TO_PHYSICAL(g_BgRoomInfo[room_index].ptr_secondary_expanded_mapping_info));
|
||||
|
||||
|
||||
@@ -193,7 +193,19 @@ u8 die_blood_image_1[] = {
|
||||
0xE5, 0xE5, 0xE5, 0xE5
|
||||
};
|
||||
|
||||
#ifdef PORT
|
||||
/* D64: the N64 build places this sentinel in the same section directly after
|
||||
* die_blood_image_1[]; the guard `bloodImgNxt < &die_blood_image_end` and the
|
||||
* completion test rely on that adjacency (the value is never read — only its
|
||||
* address). On PC a zero-init symbol lands in .bss, a different section ~1 MB
|
||||
* away, so the guard never fires and the RLE decoder runs past the array into
|
||||
* adjacent .data, writing unbounded garbage (clobbered the gun-barrel sub-DL
|
||||
* at +0x200 — see docs/PCPortResearch.md §F/D64). Define it as one-past-the-
|
||||
* end of the array instead. Only used in this TU. */
|
||||
#define die_blood_image_end (*(const u8 *)(die_blood_image_1 + sizeof(die_blood_image_1)))
|
||||
#else
|
||||
u8 die_blood_image_end = 0;
|
||||
#endif
|
||||
|
||||
Gfx *clear_framebuffer_black(Gfx *gdl)
|
||||
{
|
||||
@@ -219,6 +231,12 @@ Gfx *sub_GAME_7F01C1A4(Gfx *gdl) {
|
||||
s32 die_blood_image_routine(s32 arg0) {
|
||||
s8 sp37;
|
||||
u8* temp_v0_2;
|
||||
#ifdef PORT
|
||||
/* TEMP D63: catch VTX-pool overflow into the gun-barrel buffer. */
|
||||
extern u8 *g_GfxMemPos;
|
||||
if (getenv("GE_D63"))
|
||||
osSyncPrintf("D63 blood-routine arg=%d mempos=%p\n", arg0, (void *)g_GfxMemPos);
|
||||
#endif
|
||||
|
||||
if (arg0 == 0) {
|
||||
g_CurrentPlayer->bloodImgCur = die_blood_image_1;
|
||||
@@ -228,14 +246,39 @@ s32 die_blood_image_routine(s32 arg0) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
extern void d63SlotCheck(const char *tag); /* TEMP D63 */
|
||||
#endif
|
||||
g_CurrentPlayer->bloodImgIdx = (1 - g_CurrentPlayer->bloodImgIdx);
|
||||
g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx] = dynAllocate(BLOOD_IMG_WIDTH * BLOOD_IMG_HEIGHT);
|
||||
temp_v0_2 = dynAllocate(BLOOD_IMG_WIDTH * BLOOD_IMG_HEIGHT);
|
||||
#ifdef PORT
|
||||
if (getenv("GE_D63"))
|
||||
osSyncPrintf("D63 blood: buf=%p temp=%p\n",
|
||||
(void *)g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx],
|
||||
(void *)temp_v0_2);
|
||||
d63SlotCheck("blood-a-alloc");
|
||||
#endif
|
||||
g_CurrentPlayer->bloodImgNxt = decrypt_bleeding_animation_data(g_CurrentPlayer->bloodImgCur, BLOOD_IMG_WIDTH, BLOOD_IMG_HEIGHT, temp_v0_2, &sp37);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("blood-b-decrypt");
|
||||
#endif
|
||||
bloodImgTranspose(temp_v0_2, BLOOD_IMG_WIDTH, BLOOD_IMG_HEIGHT, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("blood-c-transpose");
|
||||
#endif
|
||||
sub_GAME_7F01D02C(g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx], BLOOD_IMG_WIDTH, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("blood-d-d02c");
|
||||
#endif
|
||||
sub_GAME_7F01CEEC(g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx], BLOOD_IMG_WIDTH, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("blood-e-ceec");
|
||||
#endif
|
||||
sub_GAME_7F01CC94(g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx], BLOOD_IMG_WIDTH * BLOOD_IMG_HEIGHT, g_CurrentPlayer->bloodImgBufPtrArray[g_CurrentPlayer->bloodImgIdx]);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("blood-f-cc94");
|
||||
#endif
|
||||
|
||||
return (g_CurrentPlayer->bloodImgNxt >= &die_blood_image_end);
|
||||
}
|
||||
|
||||
@@ -3109,10 +3109,31 @@ void sub_GAME_7F07E7CC(void) {
|
||||
return_null();
|
||||
}
|
||||
#endif
|
||||
#ifdef PORT
|
||||
/* PC port (D56, docs/PCPortResearch.md): the watch Model is embedded in
|
||||
* struct player at N64 +0x230 and its RW-data pool at +0x2EC. On x86-64
|
||||
* the pointer fields before 0x230 shift the region to +0x24C (probe:
|
||||
* vsize.c, not 8-aligned — unaligned pointer stores are fine on x86-64,
|
||||
* D53.2), and the grown Model (0xE8 vs 0xBC) leaves only 0x9C of the N64
|
||||
* 0xC8-byte pool capacity in-struct — so take the Model by field name and
|
||||
* host the pool in a static buffer of the N64 capacity. All rwdata access
|
||||
* goes through Model.datas (set by modelInit), so nothing else references
|
||||
* the old raw offsets. */
|
||||
{
|
||||
static u8 watchRwPool[0xC8]; /* N64: embedded at player+0x2EC */
|
||||
Model *watch = (Model *)&g_CurrentPlayer->something_with_watch_object_instance;
|
||||
|
||||
animInit(watch, itemheader, (u32 *)watchRwPool);
|
||||
modelSetScale(watch, c_item_entries[41].scale * 0.10000001f);
|
||||
modelSetAnimation(watch, (ModelAnimation *)&ptr_animation_table->data[(s32)&ANIM_DATA_bond_watch], 0, 0.0f, 0.5f * watch_transition_time, 0.0f);
|
||||
g_CurrentPlayer->step_in_view_watch_animation = 0; /* N64: *(s32*)(player+0x220) */
|
||||
}
|
||||
#else
|
||||
animInit((Model *)((u8 *)g_CurrentPlayer + 0x230), itemheader, (u32 *)((u8 *)g_CurrentPlayer + 0x2ec));
|
||||
modelSetScale((Model *)((u8 *)g_CurrentPlayer + 0x230), c_item_entries[41].scale * 0.10000001f);
|
||||
modelSetAnimation((Model *)((u8 *)g_CurrentPlayer + 0x230), (ModelAnimation *)&ptr_animation_table->data[(s32)&ANIM_DATA_bond_watch], 0, 0.0f, 0.5f * watch_transition_time, 0.0f);
|
||||
*(s32 *)((u8 *)g_CurrentPlayer + 0x220) = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -81,6 +81,23 @@ s32 dynGetFreeGfx2(Gfx *gdl) {
|
||||
*/
|
||||
Vtx *dynAllocateVertices(s32 count)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* TEMP D63: catch the gun-barrel sub-DL clobber with caller context. */
|
||||
if (getenv("GE_D63")) {
|
||||
static u32 s_last = 0;
|
||||
static int s_init = 0;
|
||||
static int s_changes = 0;
|
||||
const u32 *slot = (const u32 *)0x7012EC38;
|
||||
u32 cur = *slot;
|
||||
if (!s_init) { s_init = 1; s_last = cur; }
|
||||
else if (cur != s_last && s_changes < 64) {
|
||||
osSyncPrintf("D63 vtx-alloc clobber: word@0x7012ec38 %08x -> %08x caller=%p mempos=%p\n",
|
||||
s_last, cur, __builtin_return_address(0), (void *)g_GfxMemPos);
|
||||
s_last = cur;
|
||||
++s_changes;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
void *ptr = g_GfxMemPos;
|
||||
g_GfxMemPos += count * sizeof(Vtx);
|
||||
return ptr;
|
||||
@@ -104,6 +121,10 @@ Light *dynAllocateLights(s32 count)
|
||||
}
|
||||
|
||||
void *dynAllocate(s32 size) {
|
||||
#ifdef PORT
|
||||
extern void d63Act(const char *tag); /* TEMP D63 */
|
||||
d63Act("dyn");
|
||||
#endif
|
||||
void *ptr = g_GfxMemPos;
|
||||
size = ALIGN16_a(size);
|
||||
g_GfxMemPos += size;
|
||||
@@ -111,6 +132,28 @@ void *dynAllocate(s32 size) {
|
||||
}
|
||||
|
||||
void dynSwapBuffers(void) {
|
||||
#ifdef PORT
|
||||
/* TEMP D63: log buffer bounds + bump position each frame */
|
||||
if (getenv("GE_D63")) {
|
||||
static int d63dyncount = 0;
|
||||
if ((d63dyncount++ % 100) == 0 || g_GfxMemPos > g_VtxBuffers[g_GfxActiveBufferIndex + 1]) {
|
||||
osSyncPrintf("D63 dynSwap #%d active=%d gfx=[%p..%p) vtx=[%p..%p) mempos=%p\n",
|
||||
d63dyncount, (int)g_GfxActiveBufferIndex,
|
||||
(void *)g_GfxBuffers[g_GfxActiveBufferIndex],
|
||||
(void *)g_GfxBuffers[g_GfxActiveBufferIndex + 1],
|
||||
(void *)g_VtxBuffers[g_GfxActiveBufferIndex],
|
||||
(void *)g_VtxBuffers[g_GfxActiveBufferIndex + 1],
|
||||
(void *)g_GfxMemPos);
|
||||
extern MemoryPool g_mempPools[];
|
||||
for (int b = 0; b < 8; b++) {
|
||||
if (g_mempPools[b].start || g_mempPools[b].end)
|
||||
osSyncPrintf("D63 pool[%d] start=%p end=%p pos=%p prevpos=%p\n",
|
||||
b, (void *)g_mempPools[b].start, (void *)g_mempPools[b].end,
|
||||
(void *)g_mempPools[b].pos, (void *)g_mempPools[b].prevpos);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
g_GfxActiveBufferIndex = (g_GfxActiveBufferIndex ^ 1);
|
||||
g_GfxRequestedDisplayList = FALSE;
|
||||
g_GfxMemPos = g_VtxBuffers[g_GfxActiveBufferIndex];
|
||||
|
||||
+97
-1
@@ -1402,16 +1402,39 @@ u32 frontCheckCursorOnNextTab(void)
|
||||
//********************************************************************************************************
|
||||
//LEGAL SCREEN
|
||||
//********************************************************************************************************
|
||||
#ifdef PORT
|
||||
/* TEMP D63: log each intro-menu init that reuses ptr_logo_and_walletbond_DL,
|
||||
* with the word at base+0x200 (the gun-barrel sub-DL slot) before/after. */
|
||||
static void d63MenuProbe(const char *menu)
|
||||
{
|
||||
if (!getenv("GE_D63") || !ptr_logo_and_walletbond_DL) return;
|
||||
u32 before = *(u32 *)(ptr_logo_and_walletbond_DL + 0x200);
|
||||
osSyncPrintf("D63 menu init %s: base=%p word@+0x200 before=%08x\n", menu, (void *)ptr_logo_and_walletbond_DL, before);
|
||||
}
|
||||
static void d63MenuProbeAfter(const char *menu)
|
||||
{
|
||||
if (!getenv("GE_D63") || !ptr_logo_and_walletbond_DL) return;
|
||||
u32 after = *(u32 *)(ptr_logo_and_walletbond_DL + 0x200);
|
||||
osSyncPrintf("D63 menu init %s: word@+0x200 after=%08x\n", menu, after);
|
||||
}
|
||||
#endif
|
||||
|
||||
void init_menu00_legalscreen(void)
|
||||
{
|
||||
s32 padding;
|
||||
struct coord3d pos;
|
||||
|
||||
#ifdef PORT
|
||||
d63MenuProbe("legalscreen");
|
||||
#endif
|
||||
musicTrack1Stop();
|
||||
maybe_is_in_menu = TRUE;
|
||||
g_MenuTimer = 0;
|
||||
pos = legalpage_pos;
|
||||
load_object_fill_header(PitemZ_entries[PROP_LEGALPAGE].header, PitemZ_entries[PROP_LEGALPAGE].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0);
|
||||
#ifdef PORT
|
||||
d63MenuProbeAfter("legalscreen");
|
||||
#endif
|
||||
modelCalculateRwDataLen(PitemZ_entries[PROP_LEGALPAGE].header);
|
||||
|
||||
logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_LEGALPAGE].header);
|
||||
@@ -1612,7 +1635,13 @@ void init_menu01_nintendo(void)
|
||||
|
||||
g_MenuTimer = 0;
|
||||
pos = nintendologo_pos;
|
||||
#ifdef PORT
|
||||
d63MenuProbe("nintendologo");
|
||||
#endif
|
||||
load_object_fill_header(PitemZ_entries[PROP_NINTENDOLOGO].header, PitemZ_entries[PROP_NINTENDOLOGO].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0);
|
||||
#ifdef PORT
|
||||
d63MenuProbeAfter("nintendologo");
|
||||
#endif
|
||||
modelCalculateRwDataLen(PitemZ_entries[PROP_NINTENDOLOGO].header);
|
||||
logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_NINTENDOLOGO].header);
|
||||
modelSetScale(logoinst, 1.0f);
|
||||
@@ -1793,7 +1822,13 @@ Gfx *constructor_menu01_nintendo(Gfx *DL)
|
||||
*/
|
||||
void init_menu02_rarelogo(void)
|
||||
{
|
||||
#ifdef PORT
|
||||
d63MenuProbe("rarelogo");
|
||||
#endif
|
||||
setupRarewareLogoData(ptr_logo_and_walletbond_DL, 0x78000);
|
||||
#ifdef PORT
|
||||
d63MenuProbeAfter("rarelogo");
|
||||
#endif
|
||||
sndPlaySfx(g_musicSfxBufferPtr, RARELOGO_SFX, 0);
|
||||
}
|
||||
|
||||
@@ -1839,9 +1874,22 @@ Gfx * constructor_menu02_rareware(Gfx * DL) {
|
||||
//GUNBARREL
|
||||
//********************************************************************************************************
|
||||
void init_menu03_gunbarrel(void) {
|
||||
#ifdef PORT
|
||||
d63MenuProbe("gunbarrel");
|
||||
extern void d63SlotLog(const char *tag); /* TEMP D63 */
|
||||
#endif
|
||||
initializeGunBarrelIntro(ptr_logo_and_walletbond_DL, 0x78000);
|
||||
#ifdef PORT
|
||||
d63SlotLog("gb:after-init");
|
||||
#endif
|
||||
musicTrack1Play(M_INTRO);
|
||||
#ifdef PORT
|
||||
d63SlotLog("gb:after-music");
|
||||
#endif
|
||||
maybe_is_in_menu = TRUE;
|
||||
#ifdef PORT
|
||||
d63SlotLog("gb:end");
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1883,7 +1931,13 @@ void init_menu04_goldeneyelogo(void)
|
||||
|
||||
g_MenuTimer = 0;
|
||||
pos = goldeneyelogo_pos;
|
||||
#ifdef PORT
|
||||
d63MenuProbe("goldeneyelogo");
|
||||
#endif
|
||||
load_object_fill_header(PitemZ_entries[PROP_GOLDENEYELOGO].header, PitemZ_entries[PROP_GOLDENEYELOGO].filename, ptr_logo_and_walletbond_DL, 0x3c000, 0);
|
||||
#ifdef PORT
|
||||
d63MenuProbeAfter("goldeneyelogo");
|
||||
#endif
|
||||
modelCalculateRwDataLen(PitemZ_entries[PROP_GOLDENEYELOGO].header);
|
||||
logoinst = modelmgrInstantiateModel(PitemZ_entries[PROP_GOLDENEYELOGO].header);
|
||||
#ifdef DEBUG
|
||||
@@ -7758,6 +7812,15 @@ void init_menu18_displaycast(void)
|
||||
randomly_selected_intro_animation = randomGetNext() % ((u32) intro_animation_count);
|
||||
body = intro_char_table[intro_character_index].body;
|
||||
head = intro_char_table[intro_character_index].head;
|
||||
#ifdef PORT
|
||||
/* TEMP D64: dump the live cast-table entry (suspected .data clobber). */
|
||||
if (getenv("GE_D63")) {
|
||||
const u32 *e = (const u32 *)&intro_char_table[intro_character_index];
|
||||
osSyncPrintf("D64 cast-table idx=%d body=%d head=%08x raw=[%08x %08x %08x %08x %08x]\n",
|
||||
(int)intro_character_index, (int)body, (unsigned)head,
|
||||
e[0], e[1], e[2], e[3], e[4]);
|
||||
}
|
||||
#endif
|
||||
cameraPreset = intro_animation_table[randomly_selected_intro_animation].camera_preset;
|
||||
|
||||
if (body == BODY_Special_Operations_Uniform)
|
||||
@@ -7824,6 +7887,15 @@ void init_menu18_displaycast(void)
|
||||
bufferRemaining -= allocSize;
|
||||
bufferPtr += allocSize;
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D64: dump head/body/index state right before the wild access. */
|
||||
if (getenv("GE_D63")) {
|
||||
extern u32 num_male_heads, num_female_heads;
|
||||
osSyncPrintf("D64 pre-citem idx=%d body=%d head=%08x nmh=%u nfh=%u\n",
|
||||
(int)intro_character_index, (int)body, (unsigned)head,
|
||||
num_male_heads, num_female_heads);
|
||||
}
|
||||
#endif
|
||||
if (head >= 0)
|
||||
{
|
||||
headHeader = c_item_entries[head].header;
|
||||
@@ -8009,15 +8081,27 @@ void interface_menu18_displaycast(void)
|
||||
}
|
||||
|
||||
f = intro_character_index;
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D65: trace the cast-end decision. */
|
||||
if (getenv("GE_D63"))
|
||||
osSyncPrintf("D65 cast-end f=%d body=%d\n", (int)f, (int)intro_char_table[f].body);
|
||||
#endif
|
||||
if (intro_char_table[f].body < 0)
|
||||
{
|
||||
intro_character_index = 0;
|
||||
f = 0;
|
||||
#ifdef PORT
|
||||
if (getenv("GE_D63"))
|
||||
osSyncPrintf("D65 cast-end RESET idx=0\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (intro_character_index > 0)
|
||||
{
|
||||
#ifdef PORT
|
||||
if (getenv("GE_D63"))
|
||||
osSyncPrintf("D65 cast-end frontChangeMenu idx=%d\n", (int)intro_character_index);
|
||||
#endif
|
||||
frontChangeMenu(MENU_DISPLAY_CAST, 1);
|
||||
}
|
||||
else if (full_actor_intro != 0)
|
||||
@@ -8280,6 +8364,18 @@ Gfx *constructor_menu18_displaycast(Gfx *DL)
|
||||
|
||||
DL = microcode_constructor(DL);
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D65: dump index state at the crashing langGet. */
|
||||
if (getenv("GE_D63")) {
|
||||
int i65 = (int)intro_character_index;
|
||||
osSyncPrintf("D65 ctor idx=%d full=%d\n", i65, (int)full_actor_intro);
|
||||
if (i65 >= 0 && i65 <= 40) {
|
||||
const u32 *e = (const u32 *)&intro_char_table[i65];
|
||||
osSyncPrintf("D65 ctor raw=[%08x %08x %08x %08x %08x]\n",
|
||||
e[0], e[1], e[2], e[3], e[4]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (full_actor_intro == FALSE)
|
||||
{
|
||||
text = langGet( (u16)intro_char_table[intro_character_index].text1);
|
||||
|
||||
@@ -98,14 +98,25 @@ s32 D_800492E4[] =
|
||||
{
|
||||
0, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
#ifdef PORT
|
||||
/* D67: match the PORT struct image_entry field order (dataoffset first) —
|
||||
* see src/game/image.h. */
|
||||
#define IMAGE(NAME, SZ, HS, HT, F3, F4, F5, F6) \
|
||||
{SZ, HT, HS, F3, F4, F5, F6 },
|
||||
#else
|
||||
#define IMAGE(NAME, SZ, HS, HT, F3, F4, F5, F6) \
|
||||
{HS, HT, SZ, F3, F4, F5, F6 },
|
||||
#endif
|
||||
|
||||
//D:80049300
|
||||
//need way to calculate size at compile time from external data
|
||||
struct image_entry g_Textures[] = {
|
||||
#include <assets/images.def>
|
||||
#ifdef PORT
|
||||
{0xFFFF, HIT_DEFAULT, HIT_DEFAULT, 0, 0, 0, 0}
|
||||
#else
|
||||
{HIT_DEFAULT, HIT_DEFAULT,0xFFFF,0,0,0,0}
|
||||
#endif
|
||||
};
|
||||
#undef IMAGE
|
||||
|
||||
@@ -2321,7 +2332,14 @@ void texLoadFromDisplayList(Gfx *gdl, struct texpool *arg1)
|
||||
while (bytes[0] != (u8)G_ENDDL)
|
||||
{
|
||||
// Look for GBI sequence: fd...... abcd....
|
||||
#ifdef PORT
|
||||
/* D68 (docs/PCPortResearch.md): after gimgFixupGlobalimagetable() the
|
||||
* 0xABCDxxxx word is stored host-order, so the marker bytes sit at
|
||||
* offsets 6..7 of the Gfx. */
|
||||
if (bytes[0] == G_SETTIMG && bytes[6] == 0xcd && bytes[7] == 0xab)
|
||||
#else
|
||||
if (bytes[0] == G_SETTIMG && bytes[4] == 0xab && bytes[5] == 0xcd)
|
||||
#endif
|
||||
{
|
||||
texLoad((u32 *)((s32)bytes + 4), arg1);
|
||||
}
|
||||
@@ -2405,6 +2423,14 @@ void texLoad(s32 *updateword, struct texpool *pool)
|
||||
|
||||
if (TRUE)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* TEMP D66: catch oversized texLoad ROM reads. */
|
||||
if (getenv("GE_D63"))
|
||||
osSyncPrintf("D66 texLoad texnum=%d thisoff=%08x nextoff=%08x size=%08x compbuf=%p\n",
|
||||
g_TexNumToLoad, thisoffset, nextoffset,
|
||||
(unsigned)(((u32)(nextoffset - thisoffset) + 0x1f) >> 4 << 4),
|
||||
(void *)alignedcompbuffer);
|
||||
#endif
|
||||
// Copy the compressed texture to RAM
|
||||
romCopy(alignedcompbuffer,
|
||||
(u32) &_imagesSegmentRomStart + (thisoffset & 0xfffffff8),
|
||||
|
||||
@@ -52,6 +52,30 @@ struct tex {
|
||||
/*0x0c*/ u32 next : 24;
|
||||
};
|
||||
|
||||
#ifdef PORT
|
||||
/* D67 (docs/PCPortResearch.md): N64 layout reconstruction for struct
|
||||
* image_entry. The decompiled field order cannot be right: texLoad() reads
|
||||
* `*(s32*)&entry & 0xFFFFFF` as the data offset (so dataoffset must occupy
|
||||
* bits 0-23 of word 0), while chrprop.c indexes entries with an 8-byte
|
||||
* stride (`(u8*)g_Textures + n*8`, so sizeof == 8). Declaring every field
|
||||
* u32 makes GCC pack the struct into exactly two words on both targets, and
|
||||
* putting dataoffset first satisfies the raw word read. The IMAGE() macro
|
||||
* initializer order is adjusted to match under PORT (see image.c).
|
||||
* NOTE: chrprop.c/propobj.c also read `((u8*)&entry)[0] & 0xf` as a hit
|
||||
* type; with this layout that yields the low nibble of dataoffset, which is
|
||||
* what the N64 binary does too (the .hitSound bitfield is used by the other
|
||||
* hit-sound paths in gunfire.c). */
|
||||
struct image_entry
|
||||
{
|
||||
u32 dataoffset : 24; // runtime: cumulative start offset within the images segment (image_entries_load converts sizes -> offsets)
|
||||
u32 hitTexture : 4; // HitType-Texture
|
||||
u32 hitSound : 4; // HitType-Sound (should be enum HIT_TYPE, but it needs to be unsigned)
|
||||
u32 flag3 : 4; // Detailflag1 used once with value 0x38D2 (S/T offset of detail)
|
||||
u32 flag4 : 4;
|
||||
u32 flag5 : 4; // DetailFlag2 difference from detail image, to large image, in terms of 2 to the power of value. It is subtracted from the small one, and absolute value or something. (Zoinkity)
|
||||
u32 flag6 : 4;
|
||||
};
|
||||
#else
|
||||
struct image_entry
|
||||
{
|
||||
u8 hitSound : 4; // HitType-Sound (should be enum HIT_TYPE, but it needs to be unsigned)
|
||||
@@ -62,6 +86,7 @@ struct image_entry
|
||||
u32 flag5 : 4; // DetailFlag2 difference from detail image, to large image, in terms of 2 to the power of value. It is subtracted from the small one, and absolute value or something. (Zoinkity)
|
||||
u32 flag6 : 4;
|
||||
};
|
||||
#endif
|
||||
|
||||
struct texcacheitem {
|
||||
s16 texturenum;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <ramrom.h>
|
||||
#include <memp.h>
|
||||
#include "image_bank.h"
|
||||
#ifdef PORT
|
||||
#include <gimgfixup.h>
|
||||
#endif
|
||||
|
||||
// bss
|
||||
//8008D0A0
|
||||
@@ -243,6 +246,13 @@ void texReset(void)
|
||||
|
||||
romCopy(pGlobalimagetable, &_GlobalimagetableSegmentRomStart, size);
|
||||
|
||||
#ifdef PORT
|
||||
/* D68 (docs/PCPortResearch.md): the ROM copy is N64 big-endian; convert
|
||||
* the CPU-interpreted u32 fields (IMAGESEG Gfx w1 words and
|
||||
* sImageTableEntry.index) to host order before any code reads them. */
|
||||
gimgFixupGlobalimagetable((u8 *)pGlobalimagetable);
|
||||
#endif
|
||||
|
||||
globalbank_rdram_offset = (u32)pGlobalimagetable + 0xFE000000;
|
||||
genericimage = (void *) (globalbank_rdram_offset + GIMG_OFF(s_genericimage));
|
||||
impactimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_impactimages));
|
||||
@@ -306,4 +316,11 @@ void texReset(void)
|
||||
{
|
||||
texLoad(&scattered_explosions[i], 0);
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D68: explosion.c executes the compiled globalDL_0xNNN shadows via
|
||||
* g_ExplosionDisplayLists[]; copy the IMAGESEG words that texLoad()
|
||||
* patched in the ROM copy over into those arrays. */
|
||||
gimgSyncCompiledGlobalDLs((u8 *)pGlobalimagetable);
|
||||
#endif
|
||||
}
|
||||
@@ -10,7 +10,15 @@
|
||||
//bss
|
||||
|
||||
// Where animation frames are saved. Can possibly hold as much as nine, but the game will ever store four at maximum.
|
||||
#ifdef PORT
|
||||
/* D59: relocated into DRAM by port/src/dram_syms.s (absolute symbol at
|
||||
* 0x707FFD30) because loadAnimationFrame() addresses it through s32 fields
|
||||
* and a (u32) cast — PC BSS (0x140xxxxxx) would truncate to a wild pointer.
|
||||
* N64 build keeps the original BSS array. */
|
||||
extern char animations_frame_buffer[0x2D0];
|
||||
#else
|
||||
char animations_frame_buffer[0x2D0];
|
||||
#endif
|
||||
|
||||
// Msg Queue stuff (unused)
|
||||
OSMesgQueue animMsgQ;
|
||||
|
||||
@@ -8,8 +8,22 @@
|
||||
#define MODEL_SPARE_SLOTS 30
|
||||
#define ANIM_MODEL_SPARE_SLOTS 10
|
||||
|
||||
#ifdef PORT
|
||||
/* PC port (D57, docs/PCPortResearch.md): two rwdata record structs contain
|
||||
* pointer fields — ModelRwData_HeadPlaceholderRecord (ModelFileHeader* +
|
||||
* void*) and ModelRwData_DisplayList_CollisionRecord (Vertex* + Gfx*) — so
|
||||
* they grow 8 -> 16 bytes on x86-64. modelCalculateRwDataLen() accumulates
|
||||
* sizeof(record)/4 per node, so every HEAD/DLCOLLISION node adds +2 words
|
||||
* vs N64, and the largest models (e.g. BODY_Brosnan_Tuxedo: 153 words on PC)
|
||||
* no longer fit the N64-sized spare pools below. Grow the spare capacities
|
||||
* with headroom over the measured max; modelmgrInstantiate*() additionally
|
||||
* falls back to a dynamic slot+pool (D57) if a model still exceeds them. */
|
||||
#define MODEL_SPARE_RWDATALEN 0x38
|
||||
#define ANIM_MODEL_SPARE_RWDATALEN 0xA8
|
||||
#else
|
||||
#define MODEL_SPARE_RWDATALEN 0x14
|
||||
#define ANIM_MODEL_SPARE_RWDATALEN 0x8c
|
||||
#endif
|
||||
|
||||
|
||||
void modelmgrResetSlotCounts(void)
|
||||
|
||||
@@ -404,6 +404,20 @@ void langClearBank(s32 textBank) {
|
||||
u8 * langGet(s32 slotID)
|
||||
{
|
||||
u32 * textbank_ptr = g_LangBanks[slotID >> 10]; /* get the text file bank ID index the text ptr table */
|
||||
#ifdef PORT
|
||||
/* TEMP D65: log NULL-bank derefs (cast screen langGet crash). */
|
||||
if (getenv("GE_D63")) {
|
||||
static int d65first = 1;
|
||||
if (d65first) {
|
||||
d65first = 0;
|
||||
for (int b = 0; b < 45; b++)
|
||||
if (g_LangBanks[b])
|
||||
osSyncPrintf("D65 bank %d = %p\n", b, (void *)g_LangBanks[b]);
|
||||
}
|
||||
if (!textbank_ptr)
|
||||
osSyncPrintf("D65 langGet slotID=0x%08x bank=%d ptr=NULL\n", (unsigned)slotID, slotID >> 10);
|
||||
}
|
||||
#endif
|
||||
u32 textslot_offset = textbank_ptr[slotID & 0x03FF]; /* load the textbank ptr table then get the slot's offset */
|
||||
|
||||
u32 output_slot = textslot_offset; /* add the text slot offset to the base ptr to get the ptr to text file's slot */
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#include <ultra64.h>
|
||||
#ifdef PORT /* TEMP D51 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
#include <memp.h>
|
||||
#include "model.h"
|
||||
#include "../rmon.h" /*<PR/rmon.h>*/
|
||||
@@ -67,7 +71,14 @@ Model *modelmgrInstantiateModel(ModelFileHeader *header)
|
||||
|
||||
if (model == NULL)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* PC port (D53.2): this buffer is used as a struct Model via the
|
||||
* same type-pun as g_ModelSlots; on PC that struct is 0xE8 bytes,
|
||||
* not the N64 0x20 slot size. */
|
||||
model = mempAllocBytesInBank(sizeof(struct Model), MEMPOOL_STAGE);
|
||||
#else
|
||||
model = mempAllocBytesInBank(0x20, MEMPOOL_STAGE);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (header->numRecords > 0)
|
||||
@@ -91,9 +102,34 @@ Model *modelmgrInstantiateModel(ModelFileHeader *header)
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef PORT
|
||||
/* PC port (D57, docs/PCPortResearch.md): pointer-grown rwdata records can
|
||||
* push a model's word count past the (grown) spare-pool capacity; fall
|
||||
* back to a dynamic slot+pool exactly like the LvResetting path above.
|
||||
* The slot is not tracked in g_MaxModelSlots, so it is never reused —
|
||||
* acceptable: with the D57 capacities this should not trigger. */
|
||||
if (model == NULL && header->numRecords > 0)
|
||||
{
|
||||
model = mempAllocBytesInBank(sizeof(struct ModelSlot), MEMPOOL_STAGE);
|
||||
if (model != NULL)
|
||||
{
|
||||
rwdata = mempAllocBytesInBank((((header->numRecords * 4) + 0xf) | 0xf) ^ 0xf, MEMPOOL_STAGE);
|
||||
rwdatalen = header->numRecords;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (model != NULL)
|
||||
{
|
||||
#ifdef PORT /* TEMP D51 */
|
||||
if (getenv("GE_D51")) {
|
||||
static FILE *f = NULL;
|
||||
if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); }
|
||||
fprintf(f, "INST model=%p header=%p numRecords=%d rwdata=%p rwdatalen=%d lvreset=%d\n",
|
||||
(void *)model, (void *)header, header->numRecords,
|
||||
(void *)rwdata, rwdatalen, g_ModelIsLvResetting);
|
||||
}
|
||||
#endif
|
||||
modelInit(model, header, rwdata);
|
||||
((struct ModelSlot *)model)->unk02 = rwdatalen;
|
||||
}
|
||||
@@ -137,7 +173,13 @@ Model *modelmgrInstantiateModelWithAnim(ModelFileHeader *modelFileHeader)
|
||||
|
||||
if (newModel == NULL)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* PC port (D53.2): same pun as g_AnimModelSlots — must hold a full
|
||||
* struct Model (0xE8 on PC), not the N64 0xC0 anim-slot size. */
|
||||
newModel = mempAllocBytesInBank(sizeof(struct AnimModelSlot), MEMPOOL_STAGE);
|
||||
#else
|
||||
newModel = mempAllocBytesInBank(0xc0, MEMPOOL_STAGE);
|
||||
#endif
|
||||
}
|
||||
|
||||
requiredRwdatalen = modelFileHeader->numRecords;
|
||||
@@ -167,10 +209,46 @@ Model *modelmgrInstantiateModelWithAnim(ModelFileHeader *modelFileHeader)
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef PORT
|
||||
/* PC port (D57, docs/PCPortResearch.md): same fallback as
|
||||
* modelmgrInstantiateModel() — a dynamic slot+pool when no spare
|
||||
* fits the (pointer-grown) rwdata word count. */
|
||||
if (newModel == NULL && requiredRwdatalen > 0)
|
||||
{
|
||||
newModel = mempAllocBytesInBank(sizeof(struct AnimModelSlot), MEMPOOL_STAGE);
|
||||
if (newModel != NULL)
|
||||
{
|
||||
rwdatas = mempAllocBytesInBank((((requiredRwdatalen * 4) + 0xf) | 0xf) ^ 0xf, MEMPOOL_STAGE);
|
||||
rwdatalen = requiredRwdatalen;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef PORT /* TEMP D56: log slot-scan outcome incl. failure */
|
||||
if (getenv("GE_D56")) {
|
||||
fprintf(stderr, "[D56] animInstantiate header=%p numRecords=%d maxslots=%d newModel=%p\n",
|
||||
(void *)modelFileHeader, modelFileHeader->numRecords,
|
||||
g_MaxAnimModelSlots, (void *)newModel);
|
||||
if (newModel == NULL) {
|
||||
for (i2 = 0; i2 < g_MaxAnimModelSlots; i2++)
|
||||
fprintf(stderr, "[D56] slot%d unk08=%d unk10=%p unk02=%d\n", i2,
|
||||
g_AnimModelSlots[i2].unk08, g_AnimModelSlots[i2].unk10,
|
||||
g_AnimModelSlots[i2].unk02);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (newModel != NULL)
|
||||
{
|
||||
#ifdef PORT /* TEMP D51 */
|
||||
if (getenv("GE_D51")) {
|
||||
static FILE *f = NULL;
|
||||
if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); }
|
||||
fprintf(f, "INSTA model=%p header=%p numRecords=%d rwdatas=%p rwdatalen=%d lvreset=%d\n",
|
||||
(void *)newModel, (void *)modelFileHeader, modelFileHeader->numRecords,
|
||||
(void *)rwdatas, rwdatalen, g_ModelIsLvResetting);
|
||||
}
|
||||
#endif
|
||||
animInit(newModel, modelFileHeader, rwdatas);
|
||||
newModel->rwdatalen = rwdatalen;
|
||||
}
|
||||
@@ -218,9 +296,16 @@ void sub_GAME_7F06C418(Vew4s32 *src, Vew4s32 *dst) {
|
||||
}
|
||||
|
||||
|
||||
#ifdef PORT
|
||||
/* D59: keep the full 64-bit code address (see model.h). */
|
||||
void set_vtxallocator(void *param_1) {
|
||||
vtxallocator = (struct Vertex *(*)(s32 numvertices))param_1;
|
||||
}
|
||||
#else
|
||||
void set_vtxallocator(s32 param_1) {
|
||||
vtxallocator = param_1;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(LEFTOVERDEBUG)
|
||||
@@ -383,7 +468,12 @@ f32 sub_GAME_7F06C768(Model *objinst)
|
||||
union ModelRwData* modelGetNodeRwData(Model *Objinst, ModelNode *root)
|
||||
{
|
||||
s32 index = 0;
|
||||
#ifdef PORT
|
||||
/* D52: word-indexed pool access — see bondtypes.h struct Model.datas. */
|
||||
u32 *data = Objinst->datas;
|
||||
#else
|
||||
union ModelRwData **data = Objinst->datas;
|
||||
#endif
|
||||
|
||||
switch (root->Opcode & 0xff)
|
||||
{
|
||||
@@ -440,12 +530,29 @@ union ModelRwData* modelGetNodeRwData(Model *Objinst, ModelNode *root)
|
||||
if ((root->Opcode & 0xFF) == MODELNODE_OPCODE_HEAD)
|
||||
{
|
||||
ModelRwData_HeadPlaceholderRecord *tmp = modelGetNodeRwData(Objinst, root);
|
||||
#ifdef PORT
|
||||
data = (u32 *)tmp->RwDatas;
|
||||
#else
|
||||
data = tmp->RwDatas;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D51: trace rwdata pool addressing */
|
||||
if (getenv("GE_D51")) {
|
||||
static FILE *f = NULL;
|
||||
if (!f) { f = fopen("d52rw.log", "a"); setvbuf(f, NULL, _IONBF, 0); }
|
||||
fprintf(f, "GND obj=%p datas=%p idx=%d rwdatalen=%d op=%d data=%p res=%p\n",
|
||||
(void *)Objinst, (void *)data, index, Objinst->rwdatalen,
|
||||
root->Opcode & 0xff, (void *)data,
|
||||
(void *)&data[index]);
|
||||
}
|
||||
return (union ModelRwData *)&data[index];
|
||||
#else
|
||||
return &data[index];
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -718,6 +825,14 @@ void setsubroty(Model *model, f32 angle)
|
||||
|
||||
void modelSetScale(Model *objinst, f32 scale)
|
||||
{
|
||||
#ifdef PORT /* TEMP D56: identify the crashing caller */
|
||||
static int d56count = 0;
|
||||
if (getenv("GE_D56")) {
|
||||
fprintf(stderr, "[D56] modelSetScale #%d objinst=%p scale=%.4f ret=%p\n",
|
||||
d56count++, (void *)objinst, scale,
|
||||
__builtin_return_address(0));
|
||||
}
|
||||
#endif
|
||||
objinst->scale = scale;
|
||||
}
|
||||
|
||||
@@ -4098,6 +4213,13 @@ void modelRenderNodeGundl(ModelRenderData* renderdata, ModelNode* arg1)
|
||||
modelApplyRenderModeType2(renderdata);
|
||||
}
|
||||
|
||||
#if defined(PORT)
|
||||
/* TEMP D63: log Primary/Secondary before emit (env GE_D63=1) */
|
||||
if (getenv("GE_D63") && ((renderdata->flags & 1) && rodata->Primary))
|
||||
osSyncPrintf("D63 modelRenderNodeGundl Primary=%p Secondary=%p BaseAddr=%p ModelType=%d\n",
|
||||
(void *)rodata->Primary, (void *)rodata->Secondary,
|
||||
(void *)rodata->BaseAddr, (int)rodata->ModelType);
|
||||
#endif
|
||||
gSPDisplayList(renderdata->gdl++, rodata->Primary);
|
||||
|
||||
if ((rodata->ModelType == 3) && rodata->Secondary)
|
||||
@@ -6235,7 +6357,12 @@ void modelAttachPart(Model *pmodel, ModelFileHeader *pmodeldef, ModelNode *pnode
|
||||
ModelNode *node;
|
||||
|
||||
rwdata->ModelFileHeader = cmodeldef;
|
||||
#ifdef PORT
|
||||
/* D52: word-indexed pool offset — see bondtypes.h struct Model.datas. */
|
||||
rwdata->RwDatas = (void *)&pmodel->datas[pmodeldef->numRecords];
|
||||
#else
|
||||
rwdata->RwDatas = &pmodel->datas[pmodeldef->numRecords];
|
||||
#endif
|
||||
|
||||
pnode->Child = cmodeldef->RootNode;
|
||||
|
||||
|
||||
@@ -18,7 +18,13 @@ void modelAttachHead(Model *, ModelNode*, ModelFileHeader *);
|
||||
void clear_aircraft_model_obj(Model *objinstance);
|
||||
void modelSetDistanceDisabled(s32 param_1);
|
||||
void modelSetDistanceScale(f32 param_1);
|
||||
#ifdef PORT
|
||||
/* D59: function pointers must not pass through s32 on PC (the caller passes
|
||||
* &get_ptr_allocated_block_for_vertices, a 0x140xxxxxx code address). */
|
||||
void set_vtxallocator(void *param_1);
|
||||
#else
|
||||
void set_vtxallocator(s32 param_1);
|
||||
#endif
|
||||
void modelCalculateScaledRootToOriginDir(Model* model, coord3d* coord);
|
||||
void modelGetScaledRootToOriginDir(Model* model, coord3d* coord);
|
||||
s32 modelFindNodeMtxIndex(ModelNode *node, s32 arg1);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#include <ultra64.h>
|
||||
#ifdef PORT
|
||||
/* TEMP D69: probe below uses fprintf/getenv. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
#include "macro.h"
|
||||
#include "ob.h"
|
||||
#include <deb.h>
|
||||
@@ -142,6 +147,15 @@ void obLoadBGFileBytesAtOffset(u8 *bgname, u8 *target, s32 offset, s32 len)
|
||||
index = fileGetIndex(bgname);
|
||||
fileentry = &file_resource_table[index];
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D69: trace BG-file loads (env GE_D69=1). */
|
||||
if (getenv("GE_D69"))
|
||||
fprintf(stderr, "D69 obLoadBGFile %s idx=%d rom_size=0x%X hw=0x%08X off=0x%X len=0x%X\n",
|
||||
(const char *)bgname, index,
|
||||
(unsigned)resource_lookup_data_array[index].rom_size,
|
||||
(unsigned)(u32)fileentry->hw_address, (unsigned)offset, (unsigned)len);
|
||||
#endif
|
||||
|
||||
if (resource_lookup_data_array[index].rom_size != 0)
|
||||
{
|
||||
//if the size of offset data would exceed file size, loop forever
|
||||
@@ -195,6 +209,15 @@ void obLoadBGFileBytesAtOffset(u8 *bgname, u8 *target, s32 offset, s32 len)
|
||||
index = fileGetIndex(bgname);
|
||||
fileentry = &file_resource_table[index];
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D69: trace BG-file loads (env GE_D69=1). */
|
||||
if (getenv("GE_D69"))
|
||||
fprintf(stderr, "D69 obLoadBGFile %s idx=%d rom_size=0x%X hw=0x%08X off=0x%X len=0x%X\n",
|
||||
(const char *)bgname, index,
|
||||
(unsigned)resource_lookup_data_array[index].rom_size,
|
||||
(unsigned)(u32)fileentry->hw_address, (unsigned)offset, (unsigned)len);
|
||||
#endif
|
||||
|
||||
if (resource_lookup_data_array[index].rom_size != 0)
|
||||
{
|
||||
//if the size of offset data would exceed file size, loop forever
|
||||
|
||||
@@ -16,6 +16,52 @@ struct bondstruct_unk_op07_related {
|
||||
s32 unk0C;
|
||||
};
|
||||
|
||||
#ifdef PORT
|
||||
/* PC port (D53.2): the game type-puns these slot structs with struct Model —
|
||||
* model.c does `(Model *)&g_ModelSlots[i]` / `(Model *)&g_AnimModelSlots[i]`
|
||||
* and reads/writes Model fields through the slot storage. On N64 the punned
|
||||
* pairs line up exactly: slot.unk08@0x08 IS the low word of Model.obj (the
|
||||
* in-use flag — modelInit's `obj = header` marks a slot used, obj=NULL frees
|
||||
* it), slot.unk10@0x10 IS Model.datas (the RW-data pool pointer), and
|
||||
* slot.unk02@0x02 IS Model.rwdatalen.
|
||||
*
|
||||
* On x86-64 natural alignment moves the Model pointers: obj@0x10, datas@0x20
|
||||
* (probe-verified with vsize.c; bondtypes.h has no pack pragma). The old slot
|
||||
* layout kept N64 offsets, so the pun silently diverged:
|
||||
* - clear_model_obj() (`model->obj = NULL`, called every frame by
|
||||
* update_menu00_legalscreen) wrote 8 bytes at @0x10..0x17 — clobbering
|
||||
* slot.unk10 (the pool pointer lived at @0x10 here);
|
||||
* - modelmgrCanSlotFitRwdata() read `(Model*)&slot)->datas` at @0x20
|
||||
* (garbage: old unk14/unk18) while the assignment read unk10@0x10 —
|
||||
* check passed on garbage, assignment got NULL;
|
||||
* - modelInit/animInit write through the whole punned Model (up to PC
|
||||
* offset 0xE3), overflowing the 0x20/0xC0-byte slots.
|
||||
* Result: frame-5 SIGSEGV in modelInitRwData (NULL pool deref).
|
||||
*
|
||||
* Fix: re-lay the slots out on top of the PC struct Model so every punned
|
||||
* pair matches again, and pad each slot to sizeof(struct Model) so a full
|
||||
* Model fits. Member names are kept — only unk02/unk08/unk10 are referenced
|
||||
* anywhere (whole-tree grep). N64 layout verbatim under #else. */
|
||||
struct AnimModelSlot {
|
||||
s16 unk00; /* 0x00 */
|
||||
s16 unk02; /* 0x02 — Model.rwdatalen */
|
||||
char pad04[0x0C]; /* 0x04..0x0F: Model.chr@8 (PC) */
|
||||
s32 unk08; /* 0x10 — low word of Model.obj = in-use flag */
|
||||
char pad14[0x0C]; /* 0x14..0x1F: Model.render_pos@0x18 (PC) */
|
||||
void *unk10; /* 0x20 — Model.datas (RW-data pool pointer) */
|
||||
char pad28[sizeof(struct Model) - 0x28]; /* rest of the punned Model */
|
||||
};
|
||||
|
||||
struct ModelSlot {
|
||||
s16 unk00; /* 0x00 */
|
||||
s16 unk02; /* 0x02 — Model.rwdatalen */
|
||||
char pad04[0x0C]; /* 0x04..0x0F: Model.chr@8 (PC) */
|
||||
s32 unk08; /* 0x10 — low word of Model.obj = in-use flag */
|
||||
char pad14[0x0C]; /* 0x14..0x1F: Model.render_pos@0x18 (PC) */
|
||||
void *unk10; /* 0x20 — Model.datas (RW-data pool pointer) */
|
||||
char pad28[sizeof(struct Model) - 0x28]; /* rest of the punned Model */
|
||||
};
|
||||
#else
|
||||
struct AnimModelSlot {
|
||||
s16 unk00;
|
||||
s16 unk02;
|
||||
@@ -81,6 +127,7 @@ struct ModelSlot {
|
||||
s32 unk18;
|
||||
s32 unk1c;
|
||||
};
|
||||
#endif
|
||||
|
||||
extern struct AnimModelSlot *g_AnimModelSlots;
|
||||
extern struct ModelSlot *g_ModelSlots;
|
||||
|
||||
@@ -7298,7 +7298,12 @@ void sub_GAME_7F04AC20(PropRecord *prop, ModelRenderData *mrData, s32 arg2)
|
||||
|
||||
if (nodedata != NULL)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* D52: word-indexed pool access — see bondtypes.h struct Model.datas. */
|
||||
rwdataSlot = (union ModelRwData **)&obj->model->datas[nodedata->DisplayListCollisions.RwDataIndex];
|
||||
#else
|
||||
rwdataSlot = &obj->model->datas[nodedata->DisplayListCollisions.RwDataIndex];
|
||||
#endif
|
||||
|
||||
if (nodedata->DisplayListCollisions.Vertices != (Vertex *)*rwdataSlot)
|
||||
{
|
||||
@@ -14340,7 +14345,12 @@ void sub_GAME_7F056690(void)
|
||||
}
|
||||
if (sub_GAME_7F04B590(s1->model->obj, t0) != 0)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* D52: word-indexed pool access — see bondtypes.h struct Model.datas. */
|
||||
new_var = (union ModelRwData **)&s3->datas[s0->RwDataIndex];
|
||||
#else
|
||||
new_var = &s3->datas[s0->RwDataIndex];
|
||||
#endif
|
||||
if ((s32)s0->Vertices != (s32)*new_var)
|
||||
{
|
||||
objFreePermanently(s1, 1);
|
||||
|
||||
@@ -195,7 +195,12 @@ void load_ramrom_from_devtool(void)
|
||||
// Address 0x7F0BFE5C NTSC
|
||||
void record_player_input_as_packet(struct contsample *arg0, s32 arg1, s32 arg2)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* D66: 64-bit pointer width — ramrom_data_target lives above 4 GiB in .bss. */
|
||||
uintptr_t temp_t5;
|
||||
#else
|
||||
s32 temp_t5;
|
||||
#endif
|
||||
s32 temp_t1;
|
||||
s32 var_a0;
|
||||
s32 var_a2;
|
||||
@@ -206,7 +211,11 @@ void record_player_input_as_packet(struct contsample *arg0, s32 arg1, s32 arg2)
|
||||
u8 t1;
|
||||
s32 others0;
|
||||
|
||||
#ifdef PORT
|
||||
temp_t5 = ALIGN16_a((uintptr_t)&ramrom_data_target[0x1f8]);
|
||||
#else
|
||||
temp_t5 = ALIGN16_a((s32)&ramrom_data_target[0x1f8]);
|
||||
#endif
|
||||
temp_t1 = ptr_active_demofile->size_cmds;
|
||||
|
||||
var_t2 = 0;
|
||||
@@ -432,7 +441,12 @@ void test_if_recording_demos_this_stage_load(enum LEVELID arg0, enum DIFFICULTY
|
||||
{
|
||||
if (g_ramromRecordFlag != 0)
|
||||
{
|
||||
#ifdef PORT
|
||||
/* D66: 64-bit pointer width — ramrom_data_target lives above 4 GiB in .bss. */
|
||||
ptr_active_demofile = (ramromfilestructure *) ALIGN16_a((uintptr_t)ramrom_data_target);
|
||||
#else
|
||||
ptr_active_demofile = (ramromfilestructure *) ALIGN16_a((s32)ramrom_data_target);
|
||||
#endif
|
||||
dword_CODE_bss_8008C5F8 = 0;
|
||||
ptr_active_demofile->stagenum = arg0;
|
||||
ptr_active_demofile->difficulty = arg1;
|
||||
|
||||
@@ -229,6 +229,24 @@ void rspGfxTaskStart(Gfx *firstGdl, Gfx *gdl, s32 arg2, OSMesg rspReplyMsg)
|
||||
OSScTask *sctask;
|
||||
OSTask *task;
|
||||
|
||||
#ifdef PORT
|
||||
extern void d63Act(const char *tag); /* TEMP D63 */
|
||||
d63Act("rspstart");
|
||||
/* TEMP D63: log task submission (main thread) with tid */
|
||||
if (getenv("GE_D63")) {
|
||||
unsigned long tid = 0;
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
tid = GetCurrentThreadId();
|
||||
#else
|
||||
tid = (unsigned long)pthread_self();
|
||||
#endif
|
||||
osSyncPrintf("D63 rspGfxTaskStart first=%p last=%p size=%d tid=%lu slot=%08x\n",
|
||||
(void *)firstGdl, (void *)gdl,
|
||||
(int)((gdl - firstGdl) * sizeof(Gfx)), tid,
|
||||
*(const u32 *)0x7012EC38);
|
||||
}
|
||||
#endif
|
||||
|
||||
sctask = &((struct GfxInfo_s *)g_gfxTaskSettingsList)->task;
|
||||
task = &sctask->list;
|
||||
|
||||
@@ -265,6 +283,12 @@ void rspGfxTaskStart(Gfx *firstGdl, Gfx *gdl, s32 arg2, OSMesg rspReplyMsg)
|
||||
|
||||
sctask->framebuffer = (void *) ((struct GfxInfo_s *)g_gfxTaskSettingsList)->cfb;
|
||||
|
||||
#if defined(PORT)
|
||||
/* TEMP D63: log the DL range before task submit (env GE_D63=1) */
|
||||
if (getenv("GE_D63"))
|
||||
osSyncPrintf("D63 rspGfxTaskStart firstGdl=%p gdl=%p\n", (void *)firstGdl, (void *)gdl);
|
||||
#endif
|
||||
|
||||
osWritebackDCacheAll();
|
||||
|
||||
/* start graphic task */
|
||||
|
||||
@@ -470,10 +470,37 @@ void initializeGunBarrelIntro(u8 *gfxBuffer, s32 bufferSize)
|
||||
createGunbarrelRenderHole(barrelDisplayListPtr, 0x1E);
|
||||
|
||||
gunbarrelgfxListPointer = (Gfx*)gfxBuffer;
|
||||
#ifdef PORT
|
||||
/* PC port (D58, docs/PCPortResearch.md): sub_GAME_7F01BFF8 emits 31 Gfx
|
||||
* (2x VTX + 28 TRI + ENDDL). On N64 sizeof(Gfx)==8 so the 0x100 reserve
|
||||
* covers it (248 B); on PC the Gfx union's trailing `long long` makes
|
||||
* sizeof(Gfx)==16, so the same DL is 496 B and overflowed the reserve,
|
||||
* with sub_GAME_7F008DE4's RLE expand then clobbering slots 16-30 (the
|
||||
* second TRI batch + ENDDL) — the RSP ran into RLE pixel data (opcode
|
||||
* 0x00, fatal). Reserve 0x200 so the whole DL stays clear of the RLE
|
||||
* region. (The 0x200 vertex reserve above still fits: 30 Vtx x 16 B =
|
||||
* 0x1E0.) */
|
||||
bufferSize -= 0x200;
|
||||
gfxBuffer += 0x200;
|
||||
#else
|
||||
bufferSize -= 0x100;
|
||||
gfxBuffer += 0x100;
|
||||
#endif
|
||||
|
||||
#ifdef PORT
|
||||
/* PC port (D58, docs/PCPortResearch.md): on N64 barrelDisplayListPtr is a
|
||||
* physical DRAM address, so +0x80000000 yields the KSEG0 (RSP-visible)
|
||||
* address embedded in each G_VTX w1 by sub_GAME_7F01BFF8. On PC the same
|
||||
* buffer is a V1 pointer (0x70xxxxxx, s32-safe per dram.c), and
|
||||
* +0x80000000 lands at 0xF0xxxxxx — unresolvable by fast3d's seg_addr()
|
||||
* (SIGSEGV in gfx_sp_vertex reading the vertex array). Rebuild the exact
|
||||
* N64 K0 value from the physical offset; seg_addr() passes it through to
|
||||
* the KSEG0 mirror at 0x80000000 (segments 7/8 are never registered, so
|
||||
* the unmarked-segment path is skipped). */
|
||||
sub_GAME_7F01BFF8(gunbarrelgfxListPointer, (Vtx *)(OS_K0_TO_PHYSICAL((void *)barrelDisplayListPtr) | 0x80000000u), 0x1E);
|
||||
#else
|
||||
sub_GAME_7F01BFF8(gunbarrelgfxListPointer, barrelDisplayListPtr + 0x80000000, 0x1E);
|
||||
#endif
|
||||
sub_GAME_7F008DE4((u8 **)&gfxBuffer, &bufferSize);
|
||||
|
||||
// struct copy
|
||||
@@ -575,7 +602,19 @@ void clearChrGunModelInstances(void)
|
||||
/*
|
||||
* Address: 0x7F009254
|
||||
*/
|
||||
#ifdef PORT
|
||||
extern void d63SlotCheck(const char *tag); /* TEMP D63 */
|
||||
#endif
|
||||
Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) {
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb-render-entry");
|
||||
if (getenv("GE_D63")) {
|
||||
static int n = 0;
|
||||
if ((++n % 200) == 1)
|
||||
osSyncPrintf("D63 gb-render call #%d mode=%d slot=%08x\n",
|
||||
n, (int)gunbarrel_mode, *(const u32 *)0x7012EC38);
|
||||
}
|
||||
#endif
|
||||
D_8002A7D0 = (1 - D_8002A7D0);
|
||||
switch (gunbarrel_mode - 2)
|
||||
{
|
||||
@@ -608,10 +647,19 @@ Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) {
|
||||
gdl = clear_framebuffer_black(gdl++);
|
||||
#endif
|
||||
gdl = insert_sniper_sight_eye_intro(gdl++);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb1-after-sight");
|
||||
#endif
|
||||
gdl = insert_sight_backdrop_eye_intro(gdl++);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb1-after-backdrop");
|
||||
#endif
|
||||
|
||||
if (g_TitleX < 600.0f) {
|
||||
gdl = insert_bond_eye_intro(gdl);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb1-after-bond");
|
||||
#endif
|
||||
}
|
||||
g_TitleX -= XDEC3;
|
||||
if (g_TitleX <= -80.0f) {
|
||||
@@ -627,7 +675,13 @@ Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) {
|
||||
intro_eye_counter--;
|
||||
if (intro_eye_counter < 0) {
|
||||
gunbarrel_mode++;
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb2-before-blood0");
|
||||
#endif
|
||||
die_blood_image_routine(0);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb2-after-blood0");
|
||||
#endif
|
||||
intro_state_blood_animation = 0;
|
||||
intro_eye_counter = 1;
|
||||
}
|
||||
@@ -636,13 +690,25 @@ Gfx *renderGunbarrelEyeIntroSequence (Gfx *gdl) {
|
||||
case 3:
|
||||
intro_eye_counter--;
|
||||
if (intro_eye_counter == 0) {
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb3-before-blood1");
|
||||
#endif
|
||||
intro_state_blood_animation = die_blood_image_routine(1);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb3-after-blood1");
|
||||
#endif
|
||||
intro_eye_counter = 2;
|
||||
}
|
||||
gdl = insert_sniper_sight_eye_intro(gdl);
|
||||
gdl = insert_sight_backdrop_eye_intro(gdl);
|
||||
gdl = insert_bond_eye_intro(gdl);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb3-before-bloodDL");
|
||||
#endif
|
||||
gdl = gunbarrelBloodOverlayDL(gdl);
|
||||
#ifdef PORT
|
||||
d63SlotCheck("gb3-after-bloodDL");
|
||||
#endif
|
||||
if (intro_state_blood_animation != 0) {
|
||||
gunbarrel_mode++;
|
||||
word_CODE_bss_80069584 = 0;
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
#include "seqp.h"
|
||||
#include "cseqp.h"
|
||||
#include "cseq.h"
|
||||
#ifdef PORT
|
||||
/* TEMP D54 probe — delete before commit. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
|
||||
static ALMicroTime __CSPVoiceHandler(void *node);
|
||||
@@ -442,6 +447,16 @@ static void __CSPHandleMIDIMsg(ALCSPlayer *seqp, ALEvent *event)
|
||||
break;
|
||||
|
||||
sound = __lookupSoundQuick((ALSeqPlayer*)seqp, key, vel, chan);
|
||||
#ifdef PORT
|
||||
/* TEMP D54 probe — delete before commit. */
|
||||
if (getenv("GE_D54")) {
|
||||
fprintf(stderr, "D54 NOTEON seqp=%p chan=%d key=%d vel=%d sound=%p wt=%p inst=%p\n",
|
||||
(void *)seqp, chan, key, vel, (void *)sound,
|
||||
sound ? (void *)sound->wavetable : 0,
|
||||
(void *)seqp->chanState[chan].instrument);
|
||||
fflush(stderr);
|
||||
}
|
||||
#endif
|
||||
ALFlagFailIf(!sound, seqp->debugFlags & NO_SOUND_ERR_MASK,
|
||||
ERR_ALSEQP_NO_SOUND);
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
#include "synthInternals.h"
|
||||
#include <os.h>
|
||||
#include <R4300.h>
|
||||
#ifdef PORT
|
||||
/* TEMP D54 probe — delete before commit. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
#ifndef MIN
|
||||
# define MIN(a,b) (((a)<(b))?(a):(b))
|
||||
@@ -359,6 +364,19 @@ alLoadParam(void *filter, s32 paramID, void *param)
|
||||
|
||||
switch (paramID) {
|
||||
case (AL_FILTER_SET_WAVETABLE):
|
||||
#ifdef PORT
|
||||
/* TEMP D54 probe — delete before commit. */
|
||||
if (getenv("GE_D54")) {
|
||||
ALWaveTable *wt = (ALWaveTable *) param;
|
||||
fprintf(stderr, "D54 SET_WAVETABLE wt=%p base=%p len=%d type=%d book=%p\n",
|
||||
(void *)wt,
|
||||
wt ? (void *)wt->base : 0,
|
||||
wt ? wt->len : -1,
|
||||
wt ? wt->type : -1,
|
||||
(wt && wt->type == AL_ADPCM_WAVE) ? (void *)wt->waveInfo.adpcmWave.book : 0);
|
||||
fflush(stderr);
|
||||
}
|
||||
#endif
|
||||
a->table = (ALWaveTable *) param;
|
||||
a->memin = (s32) a->table->base;
|
||||
a->sample = 0;
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
#include "include/assert.h"
|
||||
#include "seqp.h"
|
||||
#include "seq.h"
|
||||
#ifdef PORT
|
||||
/* TEMP D54 probe — delete before commit. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#endif
|
||||
|
||||
|
||||
static ALMicroTime __seqpVoiceHandler(void *node);
|
||||
@@ -458,6 +463,16 @@ void __handleMIDIMsg(ALSeqPlayer *seqp, ALEvent *event)
|
||||
break;
|
||||
|
||||
sound = __lookupSoundQuick(seqp, key, vel, chan);
|
||||
#ifdef PORT
|
||||
/* TEMP D54 probe — delete before commit. */
|
||||
if (getenv("GE_D54")) {
|
||||
fprintf(stderr, "D54 SFXNOTEON seqp=%p chan=%d key=%d vel=%d sound=%p wt=%p inst=%p\n",
|
||||
(void *)seqp, chan, key, vel, (void *)sound,
|
||||
sound ? (void *)sound->wavetable : 0,
|
||||
(void *)seqp->chanState[chan].instrument);
|
||||
fflush(stderr);
|
||||
}
|
||||
#endif
|
||||
ALFlagFailIf(!sound, seqp->debugFlags & NO_SOUND_ERR_MASK,
|
||||
ERR_ALSEQP_NO_SOUND);
|
||||
|
||||
|
||||
@@ -124,6 +124,21 @@ void alSynNew(ALSynth *drvr, ALSynConfig *c)
|
||||
/*
|
||||
* build the parameter update list
|
||||
*/
|
||||
#ifdef PORT
|
||||
/* PC port (D54): the N64 code type-puns these slots as various AL*Param
|
||||
* structs. On x86-64, ALStartParamAlt is 0x28 bytes (two 64-bit pointers:
|
||||
* next + wave), larger than the slot type ALParam (0x20), so every
|
||||
* start-voice update wrote `wave` into the neighbouring slot and
|
||||
* corrupted the free list. Size each slot for the largest param struct,
|
||||
* preserving the N64 invariant that every param type fits one slot. */
|
||||
params = alHeapAlloc(hp, c->maxUpdates, sizeof(ALStartParamAlt));
|
||||
drvr->paramList = 0;
|
||||
for (i = 0; i < c->maxUpdates; i++) {
|
||||
paramPtr = (ALParam *)((u8 *)params + i * sizeof(ALStartParamAlt));
|
||||
paramPtr->next = drvr->paramList;
|
||||
drvr->paramList = paramPtr;
|
||||
}
|
||||
#else
|
||||
params = alHeapAlloc(hp, c->maxUpdates, sizeof(ALParam));
|
||||
drvr->paramList = 0;
|
||||
for (i = 0; i < c->maxUpdates; i++) {
|
||||
@@ -131,6 +146,7 @@ void alSynNew(ALSynth *drvr, ALSynConfig *c)
|
||||
paramPtr->next = drvr->paramList;
|
||||
drvr->paramList = paramPtr;
|
||||
}
|
||||
#endif
|
||||
|
||||
drvr->heap = hp;
|
||||
}
|
||||
|
||||
+20
@@ -209,6 +209,26 @@ void *mempAllocBytesInBank(u32 bytes, u8 poolnum)
|
||||
|
||||
if (needmemallocation);
|
||||
|
||||
#ifdef PORT
|
||||
/* TEMP D63: map allocations around the gun-barrel buffer base 0x7012EA38. */
|
||||
if (getenv("GE_D63")) {
|
||||
u8 *a = allocation;
|
||||
if ((a >= (u8 *)0x70129000 && a < (u8 *)0x7012EA38) ||
|
||||
(a >= (u8 *)0x7012D000 && a < (u8 *)0x7012F500)) {
|
||||
static u32 s_seen[64];
|
||||
static u32 s_sizes[64];
|
||||
int i, n = 0;
|
||||
for (i = 0; i < 64 && s_seen[i]; i++) {
|
||||
if (s_seen[i] == (u32)a && s_sizes[i] == bytes) return allocation;
|
||||
}
|
||||
for (n = 0; n < 64 && s_seen[n]; n++);
|
||||
if (n < 64) { s_seen[n] = (u32)a; s_sizes[n] = bytes; }
|
||||
osSyncPrintf("D63 alloc %p size=%x ra=%p\n", (void *)a, bytes,
|
||||
__builtin_return_address(0));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return allocation;
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -880,6 +880,11 @@ void musicTrack1Play(s32 track)
|
||||
|
||||
romCopy(temp_a0, romAddress, trackSizeBytes);
|
||||
decompressdata(temp_a0, thing.seqData, &hlist);
|
||||
#ifdef PORT
|
||||
/* PC port (D54): the decompressed ALCMidiHdr is big-endian; fix it up
|
||||
* before alCSeqNew reads trackOffset[]/division natively. */
|
||||
romdataFixupCseq(thing.seqData);
|
||||
#endif
|
||||
|
||||
alCSeqNew(&g_musicXTrack1Seq, g_musicXTrack1SeqData);
|
||||
alCSPSetSeq(g_musicXTrack1SeqPlayer, &g_musicXTrack1Seq);
|
||||
@@ -1069,6 +1074,10 @@ void musicTrack2Play(s32 track)
|
||||
|
||||
romCopy(temp_a0, romAddress, trackSizeBytes);
|
||||
decompressdata(temp_a0, thing.seqData, &hlist);
|
||||
#ifdef PORT
|
||||
/* PC port (D54): see track 1 — fix up the big-endian ALCMidiHdr. */
|
||||
romdataFixupCseq(thing.seqData);
|
||||
#endif
|
||||
|
||||
alCSeqNew(&g_musicXTrack2Seq, g_musicXTrack2SeqData);
|
||||
alCSPSetSeq(g_musicXTrack2SeqPlayer, &g_musicXTrack2Seq);
|
||||
@@ -1258,6 +1267,10 @@ void musicTrack3Play(s32 track)
|
||||
|
||||
romCopy(temp_a0, romAddress, trackSizeBytes);
|
||||
decompressdata(temp_a0, thing.seqData, &hlist);
|
||||
#ifdef PORT
|
||||
/* PC port (D54): see track 1 — fix up the big-endian ALCMidiHdr. */
|
||||
romdataFixupCseq(thing.seqData);
|
||||
#endif
|
||||
|
||||
alCSeqNew(&g_musicXTrack3Seq, g_musicXTrack3SeqData);
|
||||
alCSPSetSeq(g_musicXTrack3SeqPlayer, &g_musicXTrack3Seq);
|
||||
|
||||
@@ -62,6 +62,27 @@ void romCopy(void *target, void *source, u32 size)
|
||||
* romCopyAligned
|
||||
* aligns data, does a romCopy(), then returns aligned pointer to target
|
||||
*/
|
||||
#ifdef PORT
|
||||
/* D66: the N64 build did all of this in s32 (every address fit). On PC the
|
||||
* targets live in .bss above 4 GiB, so `(s32)target` truncates (e.g.
|
||||
* 0x1401C6F00 -> 0x401C6F00) and romCopy DMA's into a wild address. Same
|
||||
* class as D53/D56: pointer-width reconciliation only — the arithmetic and
|
||||
* the returned (aligned + offset) value are unchanged, just 64-bit wide.
|
||||
* Callers assign the result straight to pointers, so it returns void* here
|
||||
* instead of s32. */
|
||||
void *romCopyAligned(void *target, void *source, s32 length)
|
||||
{
|
||||
uintptr_t target_u = (uintptr_t)target;
|
||||
uintptr_t source_u = (uintptr_t)source;
|
||||
uintptr_t source_aligned = align_addr_even(source_u);
|
||||
uintptr_t source_offset = source_u - source_aligned;
|
||||
uintptr_t target_aligned = ALIGN16_a(target_u);
|
||||
|
||||
romCopy((void *)target_aligned, (void *)source_aligned,
|
||||
(s32)ALIGN16_a(source_offset + length));
|
||||
return (void *)(target_aligned + source_offset);
|
||||
}
|
||||
#else
|
||||
s32 romCopyAligned(void *target, void *source, s32 length)
|
||||
{
|
||||
s32 target_offset;
|
||||
@@ -76,6 +97,7 @@ s32 romCopyAligned(void *target, void *source, s32 length)
|
||||
romCopy(target_aligned, source_aligned, ALIGN16_a((s32)source_offset + length));
|
||||
return ((s32)target_aligned + target_offset);
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* 68A8 70005CA8
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
|
||||
void romCreateMesgQueue(void);
|
||||
void romCopy(void *target, void *source, u32 size);
|
||||
#ifdef PORT
|
||||
/* D66: 64-bit pointer width on PC (see ramrom.c). */
|
||||
void *romCopyAligned(void *target, void *source, s32 length);
|
||||
#else
|
||||
s32 romCopyAligned(void *target, void *source, s32 length);
|
||||
#endif
|
||||
void romWrite(void *source, void *target, u32 size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,136 @@
|
||||
# D51 live capture: log every import_texture_i8 hit; on SIGSEGV dump the
|
||||
# RDP tile state, segment table, and the main DL around the faulting G_TEXRECT.
|
||||
import gdb
|
||||
import struct
|
||||
|
||||
def log_hit(frame):
|
||||
try:
|
||||
tile = int(frame.read_var("tile"))
|
||||
lt = frame.read_var("loaded_texture")
|
||||
addr = lt["addr"]
|
||||
size = int(lt["size_bytes"])
|
||||
line = int(lt["line_size_bytes"])
|
||||
fline = int(lt["full_image_line_size_bytes"])
|
||||
t2l = gdb.parse_and_eval("rdp.texture_to_load.addr")
|
||||
print("I8HIT tile=%d addr=%s size=%u line=%u full_line=%u to_load=%s" % (
|
||||
tile, addr, size, line, fline, t2l))
|
||||
except Exception as e:
|
||||
print("I8HIT log error: %s" % e)
|
||||
|
||||
def dump_crash():
|
||||
print("=== CRASH STATE ===")
|
||||
frame = gdb.selected_frame()
|
||||
frames = []
|
||||
while frame is not None:
|
||||
frames.append(frame)
|
||||
frame = frame.older()
|
||||
for i, f in enumerate(frames[:12]):
|
||||
fn = f.symtab.filename if f.symtab else "?"
|
||||
print("FRAME %d: %s at %s:%d" % (i, f.name(), fn, f.line()))
|
||||
|
||||
try:
|
||||
f0 = frames[0]
|
||||
lt = f0.read_var("loaded_texture")
|
||||
print("f0: tile=%d size_bytes=%u line=%u full_line=%u addr=%s" % (
|
||||
int(f0.read_var("tile")), int(lt["size_bytes"]),
|
||||
int(lt["line_size_bytes"]), int(lt["full_image_line_size_bytes"]), lt["addr"]))
|
||||
except Exception as e:
|
||||
print("f0 vars: %s" % e)
|
||||
|
||||
try:
|
||||
tt = gdb.parse_and_eval("rdp.texture_tile[0]")
|
||||
print("tile0: fmt=%d siz=%d line_size=%u tmem=%d palette=%d uls=%d ult=%d lrs=%d lrt=%d w=%d h=%d" % (
|
||||
int(tt["fmt"]), int(tt["siz"]), int(tt["line_size_bytes"]), int(tt["tmem"]),
|
||||
int(tt["palette"]), int(tt["uls"]), int(tt["ult"]), int(tt["lrs"]), int(tt["lrt"]),
|
||||
int(tt["width"]), int(tt["height"])))
|
||||
t2l = gdb.parse_and_eval("rdp.texture_to_load")
|
||||
print("to_load: addr=%s siz=%d width=%u" % (t2l["addr"], int(t2l["siz"]), int(t2l["width"])))
|
||||
except Exception as e:
|
||||
print("rdp state: %s" % e)
|
||||
|
||||
# TEMP D51: font char tables
|
||||
for name in ("ptrFontBankGothicChars", "ptrFontZurichBoldChars"):
|
||||
try:
|
||||
ch = gdb.parse_and_eval(name)
|
||||
bad = 0
|
||||
for i in range(94):
|
||||
v = int(ch[i]["pixeldata"])
|
||||
if v >= 0x100000000:
|
||||
print("%s[%d].pixeldata=0x%x (HIGH BYTES SET)" % (name, i, v))
|
||||
bad += 1
|
||||
print("%s: %d/94 entries with high bytes set" % (name, bad))
|
||||
except Exception as e:
|
||||
print(name + ": %s" % e)
|
||||
try:
|
||||
sp = gdb.parse_and_eval("segmentPointers")
|
||||
for i in range(16):
|
||||
v = int(sp[i])
|
||||
if v:
|
||||
print("seg[%d]=0x%x" % (i, v))
|
||||
except Exception as e:
|
||||
print("segs: %s" % e)
|
||||
|
||||
inf = gdb.selected_inferior()
|
||||
for i, f in enumerate(frames):
|
||||
if f.name() == "gfx_run_dl":
|
||||
try:
|
||||
cmd = int(f.read_var("cmd"))
|
||||
dls = int(f.read_var("dListStart"))
|
||||
print("gfx_run_dl frame=%d cmd=0x%x dListStart=0x%x" % (i, cmd, dls))
|
||||
# 8 slots around cmd
|
||||
base = cmd - 64
|
||||
b = bytes(inf.read_memory(base, 128))
|
||||
ws = struct.unpack("<32I", b)
|
||||
for j in range(0, 32, 2):
|
||||
w0, w1 = ws[j], ws[j + 1]
|
||||
op = (w0 >> 24) & 0xff
|
||||
tag = ""
|
||||
if op == 0xe4: tag = " <-- G_TEXRECT"
|
||||
elif op == 0xe5: tag = " <-- G_TEXRECTFLIP"
|
||||
elif op == 0xfd: tag = " [SETTIMG]"
|
||||
elif op == 0xf5: tag = " [SETTILE]"
|
||||
elif op == 0xf2: tag = " [SETTILESIZE]"
|
||||
elif op == 0xf3: tag = " [LOADBLOCK]"
|
||||
elif op == 0xf4: tag = " [LOADSYNC]"
|
||||
elif op == 0xe7: tag = " [PIPESYNC]"
|
||||
elif op == 0xbc: tag = " [MOVEWORD]"
|
||||
elif op == 0x06: tag = " [DL]"
|
||||
elif op == 0xb8: tag = " [ENDDL]"
|
||||
mark = " *" if (base + j * 4) == cmd else ""
|
||||
print(" dl[0x%x] w0=0x%08x w1=0x%08x%s%s" % (base + j * 4, w0, w1, tag, mark))
|
||||
# walk the main DL from its start; log SETTIMGs and find the G_TEXRECT
|
||||
n = 0
|
||||
off = 0
|
||||
while n < 600:
|
||||
b2 = bytes(inf.read_memory(dls + off, 8))
|
||||
w0, w1 = struct.unpack("<II", b2)
|
||||
op = (w0 >> 24) & 0xff
|
||||
if op == 0xe4 or op == 0xe5:
|
||||
print("MAIN-DL G_TEXRECT at dls+0x%x (slot %d) w0=0x%08x w1=0x%08x" % (off, n, w0, w1))
|
||||
# dump the 10 slots before it
|
||||
for k in range(max(0, n - 10), n):
|
||||
o2 = k * 16
|
||||
a, b3 = struct.unpack("<II", bytes(inf.read_memory(dls + o2, 8)))
|
||||
print(" pre[%d] +0x%x w0=0x%08x w1=0x%08x" % (k, o2, a, b3))
|
||||
break
|
||||
if op == 0xb8:
|
||||
print("MAIN-DL ENDDL at dls+0x%x (slot %d), no G_TEXRECT seen" % (off, n))
|
||||
break
|
||||
n += 1
|
||||
off += 16
|
||||
except Exception as e:
|
||||
print("gfx_run_dl dump: %s" % e)
|
||||
break
|
||||
|
||||
def on_stop(signal_name, stop_reason):
|
||||
if stop_reason == "breakpoint-hit":
|
||||
f = gdb.selected_frame()
|
||||
if f.name() == "import_texture_i8":
|
||||
log_hit(f)
|
||||
gdb.execute("continue")
|
||||
elif signal_name == "SIGSEGV":
|
||||
dump_crash()
|
||||
|
||||
gdb.Breakpoint("import_texture_i8")
|
||||
gdb.events.stop.connect(on_stop)
|
||||
print("D51 capture armed")
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""pixcount.py — count non-clear-color pixels in PPM frame dumps (GE_PCDUMP).
|
||||
|
||||
The game clears to black (gfx_opengl_clear_framebuffer: glClearColor 0,0,0),
|
||||
so "non-clear" == any pixel that is not exactly (0,0,0). This turns "the
|
||||
scene contains actual model geometry / glyphs" into a number: a blank frame
|
||||
counts ~0, a rendered legal page counts thousands.
|
||||
|
||||
Usage (run from anywhere; paths relative to CWD):
|
||||
pixcount.py frame.ppm [more.ppm ...] per-file stats (count + bbox)
|
||||
pixcount.py a.ppm b.ppm --diff pixel diff between two captures
|
||||
|
||||
Exit status: 0 if every listed file has at least one non-clear pixel
|
||||
(--diff: 0 if the two files differ at all), 1 otherwise, 2 on usage/IO error.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
# any non-zero byte (channels are stored R,G,B interleaved)
|
||||
_NZ = re.compile(rb"[^\x00]")
|
||||
|
||||
|
||||
def load_ppm(path):
|
||||
"""Parse a binary P6 PPM -> (width, height, bytes)."""
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
if data[:2] != b"P6":
|
||||
raise ValueError("%s: not a binary P6 PPM" % path)
|
||||
pos = 2
|
||||
fields = []
|
||||
while len(fields) < 3:
|
||||
# skip whitespace and '#' comments
|
||||
while pos < len(data) and data[pos:pos + 1].isspace():
|
||||
pos += 1
|
||||
if data[pos:pos + 1] == b"#":
|
||||
while pos < len(data) and data[pos] != 0x0A:
|
||||
pos += 1
|
||||
continue
|
||||
start = pos
|
||||
while pos < len(data) and not data[pos:pos + 1].isspace():
|
||||
pos += 1
|
||||
fields.append(int(data[start:pos]))
|
||||
pos += 1 # single whitespace after maxval
|
||||
w, h, maxval = fields
|
||||
if maxval != 255:
|
||||
raise ValueError("%s: unsupported maxval %d" % (path, maxval))
|
||||
px = data[pos:pos + w * h * 3]
|
||||
if len(px) < w * h * 3:
|
||||
raise ValueError("%s: truncated pixel data (%d of %d bytes)"
|
||||
% (path, len(px), w * h * 3))
|
||||
return w, h, px
|
||||
|
||||
|
||||
def _bbox(indices, w):
|
||||
if not indices:
|
||||
return None
|
||||
minx = min(i % w for i in indices)
|
||||
maxx = max(i % w for i in indices)
|
||||
miny = min(i // w for i in indices)
|
||||
maxy = max(i // w for i in indices)
|
||||
return "(%d,%d)-(%d,%d)" % (minx, miny, maxx, maxy)
|
||||
|
||||
|
||||
def stats(path):
|
||||
w, h, px = load_ppm(path)
|
||||
total = w * h
|
||||
# pixel indices where any channel byte is non-zero (C-speed regex scan)
|
||||
idx = set()
|
||||
for off in (0, 1, 2):
|
||||
ch = px[off::3]
|
||||
for m in _NZ.finditer(ch):
|
||||
idx.add(m.start())
|
||||
pct = 100.0 * len(idx) / total
|
||||
print("%s: %dx%d non-clear %d (%.2f%%) bbox %s"
|
||||
% (path, w, h, len(idx), pct, _bbox(idx, w) or "-"))
|
||||
return len(idx)
|
||||
|
||||
|
||||
def diff(path_a, path_b):
|
||||
wa, ha, pa = load_ppm(path_a)
|
||||
wb, hb, pb = load_ppm(path_b)
|
||||
if (wa, ha) != (wb, hb):
|
||||
print("sizes differ: %dx%d vs %dx%d" % (wa, ha, wb, hb))
|
||||
w, h = min(wa, wb), min(ha, hb)
|
||||
n = w * h * 3
|
||||
pa, pb = pa[:n], pb[:n]
|
||||
# XOR as big ints, then scan the result for non-zero bytes (C-speed)
|
||||
x = (int.from_bytes(pa, "big") ^ int.from_bytes(pb, "big")).to_bytes(n, "big")
|
||||
idx = {m.start() // 3 for m in _NZ.finditer(x)}
|
||||
total = w * h
|
||||
print("diff %s vs %s: %d differing pixels (%.2f%% of %dx%d) bbox %s"
|
||||
% (path_a, path_b, len(idx), 100.0 * len(idx) / total, w, h,
|
||||
_bbox(idx, w) or "-"))
|
||||
return len(idx)
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
do_diff = "--diff" in argv
|
||||
files = [a for a in argv[1:] if a != "--diff"]
|
||||
try:
|
||||
if do_diff:
|
||||
if len(files) != 2:
|
||||
print("--diff needs exactly two files")
|
||||
return 2
|
||||
return 0 if diff(files[0], files[1]) else 1
|
||||
ok = True
|
||||
for f in files:
|
||||
if not stats(f):
|
||||
ok = False
|
||||
return 0 if ok else 1
|
||||
except (IOError, ValueError) as e:
|
||||
print("error: %s" % e)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ppm2bmp.py — convert binary P6 PPM to 24-bit BMP (no dependencies).
|
||||
|
||||
Usage: ppm2bmp.py in.ppm out.bmp [scale] (scale = integer, default 1)
|
||||
Useful for eyeballing GE_PCDUMP frames without an image library.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def load_ppm(path):
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
if data[:2] != b"P6":
|
||||
raise ValueError("%s: not a binary P6 PPM" % path)
|
||||
pos = 2
|
||||
fields = []
|
||||
while len(fields) < 3:
|
||||
while pos < len(data) and data[pos:pos + 1].isspace():
|
||||
pos += 1
|
||||
if data[pos:pos + 1] == b"#":
|
||||
while pos < len(data) and data[pos] != 0x0A:
|
||||
pos += 1
|
||||
continue
|
||||
start = pos
|
||||
while pos < len(data) and not data[pos:pos + 1].isspace():
|
||||
pos += 1
|
||||
fields.append(int(data[start:pos]))
|
||||
w, h, maxval = fields
|
||||
if maxval != 255:
|
||||
raise ValueError("unsupported maxval %d" % maxval)
|
||||
px = data[pos + 1:pos + 1 + w * h * 3]
|
||||
if len(px) < w * h * 3:
|
||||
raise ValueError("truncated pixel data")
|
||||
return w, h, px
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 3:
|
||||
print(__doc__)
|
||||
return 2
|
||||
scale = int(argv[3]) if len(argv) > 3 else 1
|
||||
w, h, px = load_ppm(argv[1])
|
||||
w *= scale
|
||||
h *= scale
|
||||
row_size = (w * 3 + 3) & ~3
|
||||
out = bytearray()
|
||||
out += struct.pack("<2sIHHI", b"BM", 54 + row_size * h, 0, 0, 54)
|
||||
out += struct.pack("<IiiHHIIiiII", 40, w, h, 1, 24, 0, w * h * 3, 2835, 2835, 0, 0)
|
||||
for y in range(h - 1, -1, -1): # BMP is bottom-up
|
||||
src_y = (y // scale) * (w // scale) * 3
|
||||
row = bytearray(row_size)
|
||||
for x in range(w):
|
||||
i = src_y + (x // scale) * 3
|
||||
row[x * 3] = px[i + 2] # B
|
||||
row[x * 3 + 1] = px[i + 1] # G
|
||||
row[x * 3 + 2] = px[i] # R
|
||||
out += row
|
||||
with open(argv[2], "wb") as f:
|
||||
f.write(out)
|
||||
print("%s -> %s (%dx%d)" % (argv[1], argv[2], w, h))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in new issue
Block a user