From cd1ed57449c90a014d8090f9195c3521725ad94e Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Wed, 2 Sep 2026 10:56:03 -0400 Subject: [PATCH] =?UTF-8?q?port:=20D178=20=E2=80=94=20byte-swap=20the=20ra?= =?UTF-8?q?w=20briefing=20segment=20(blank=20objectives=20/=20briefing=20t?= =?UTF-8?q?ext)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Family B of the M-36 triage. The Ubrief*Z segment (assets/obseg/brief/) is a raw 48-byte ROM image of struct BriefStruct { u16 brief[4]; struct { u16 textid; u16 enabled_difficulty; } objective[10]; }, loaded by front.c load_briefing_text_for_stage() via _fileNameLoadToAddr() with no converter and no BE->LE fixup. On the LE host all 24 u16 read byte-swapped: - textid 0x2C04 (getStringID(LDAM,4)) -> 0x042C -> langGet() resolves an unloaded bank -> NULL -> blank objective/briefing text. This is the whole of the D143 "briefing text blank" residual (the bank loaded fine; the ids into it were swapped). - enabled_difficulty 0x0001 -> 0x0100 = 256, so `selected_difficulty >= enabled_difficulty` is always false -> difficulty-gated objectives vanish (on Agent only the one AGENT-difficulty line survived -> lone "a." bullet). Fix: romdataFixupBriefing() in port/src/romdata.c, called from a #ifdef PORT block in load_briefing_text_for_stage() right after the load — same shape as langFixupLoadedBank(). BE-serialized-struct reconciliation (AGENTS.md #2), semantics-preserving, N64 build untouched. GE_D178=1 dumps raw + fixed words. Verified: Dam briefing renders all four objectives at 00 Agent with correct difficulty gating and "007: James Bond" header; -level_09 framediff 3/3; GE_STARTMENU=7/=13 crash-free; build links clean 240/240. Investigation + patch: subagent (worktree). Suspect 2 (Gfx-width temp_s0 arithmetic) was ruled out — benign 512-byte gap, langLoadToBank records the bank base itself. Follow-ups noted in findings.md: annotate D143 as superseded; latent 64->32 pointer truncation at front.c:6569 / :2202. Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT --- docs/dev/findings.md | 2 +- docs/porting-notes.md | 19 +++++++++++++++++++ port/include/romdata.h | 10 ++++++++++ port/src/romdata.c | 22 ++++++++++++++++++++++ src/game/front.c | 28 ++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 35706176..9d01cfc4 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -390,7 +390,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | RC3 · D167 | **Non-power-of-two texture wrap period (`docs/dev/TEXTURE-GLITCH-ANALYSIS.md` §6 RC3 — "textures repeat oddly", residual Depot-ceiling noise after D161).** The N64 RDP masks the texel coordinate of a wrapping render tile at `1<= enabled_difficulty` (`front.c:6729`) is false for every difficulty-gated objective — on Agent only the one `DIFFICULTY_AGENT`(0) objective survived the filter, which is why a single bare "a." bullet printed. **Fix:** `romdataFixupBriefing()` in `port/src/romdata.c` (+ `port/include/romdata.h`), called from a `#ifdef PORT` block in `load_briefing_text_for_stage()` right after the load — the same shape as `langFixupLoadedBank()` in `language.c` (BE-serialized-struct reconciliation, semantics-preserving, no game logic touched). `GE_D178=1` prints the raw and fixed words. **Verified:** Dam briefing on Agent now shows "a. Bungee jump from platform"; on `GE_STARTMENU_DIFF=3` (00 Agent) all four (Neutralize all alarms / Install covert modem / Intercept data backup / Bungee jump from platform), correct difficulty gating. `-level_09` framediff 3/3; `GE_STARTMENU=7`/`=13` crash-free. | **FIXED (M-36)** — high confidence | | D175 | **In-game stutter / brief hang during normal play** (user QA report): opening a door on Runway (`-level_35`, mission 3) and also observed on Surface. Self-recovered; no backtrace captured. Likely one of the known transient-hang classes (D155 catch-up spiral / D156 anim NaN loop / D134 task-done event / D147-D152 audio-lock steal) or a benign new-room texture-import frame spike on door open. See `GRAPHICS-BACKLOG.md` D175 for the gdb pattern-match sheet. | Observed, not investigated | | D176 | **Skybox broken on Surface** (user QA report; which of `-level_36`/`-level_43` unconfirmed). Symptom unspecified — missing/black sky, bad transform (D75 family), or sky-tile texture import. See `GRAPHICS-BACKLOG.md` D176. | Observed, not investigated | | D177 | **Ladders don't work** (user QA report): climbing is non-functional and **blocks progression on some levels**. FUNCTIONAL bug, not cosmetic — ranks above the graphics backlog. Untested by every harness so far (level sweep is boot-only; no scripted ladder input). Suspects: player ladder state machine in `bondview2.c`/chr physics (struct-offset family D100/D140), or the climb input mapping (A-button hold?) not reaching the game on the PC input path. **Next:** repro on a level with a mandatory ladder, check whether the player's ladder state ever changes (probe) vs the input never registering (`GE_INPUTLOG`). | Observed, NOT investigated — high priority (progression blocker) | diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 0e4aeaa1..48db794a 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -325,6 +325,25 @@ through a converter or a runtime bswap fixup reads scrambled. emit the value (`_bswap32` into the low 4 bytes, LE). Decide per field, not per type. +- **Audit for ROM segments loaded raw with no fixup at all** (D178). Every + `_fileNameLoadToAddr()` / `_fileNameLoadToBank()` call site loads a raw + big-endian ROM image; the ones with a `struct` overlay need a BE→LE pass + and there is nothing in the load path that supplies one — the fixups are + bolted on per call site (`langFixupLoadedBank()` in `language.c`, + `romdataFixup*()` in `port/src/romdata.c`). The briefing segment + (`Ubrief*Z` = `u16 brief[4]` + 10 × `{u16 textid; u16 difficulty}`) had + none for a year, and its failure mode is silent and *split*: a swapped + string id (`0x2C04` → `0x042C`) still passes `!= 0`, so the loop body + runs and `langGet()` quietly returns NULL → **blank text, not a crash** + (that was the whole of the D143 "briefing/objective text is blank" + residual); and a swapped small enum (`0x0001` → `0x0100 = 256`) turns a + `>=` difficulty gate into an always-false filter, so lines silently + *vanish* rather than render wrong. When a screen renders its own + chrome/headers correctly but the file-driven rows are empty, suspect the + raw file, not the renderer: dump the first words of the loaded blob and + look for a byte-mirrored constant you can recognise from the asset `.c` + source in `assets/obseg/`. + ## C2. Port-layer / SDL shims - `#include ` in a port `.c`/`.h` that also sees `` diff --git a/port/include/romdata.h b/port/include/romdata.h index 7cd41be5..3c4e75a0 100644 --- a/port/include/romdata.h +++ b/port/include/romdata.h @@ -75,6 +75,16 @@ void romdataFixupCseq(u8 *blob); */ void romdataFixupLangBank(u8 *blob, u32 blobSize); +/* + * D178 (docs/dev/findings.md): byte-swap a briefing segment (Ubrief*Z) in + * place. `struct BriefStruct` is 24 big-endian u16 (u16 brief[4] + + * 10 x { u16 textid; u16 enabled_difficulty }) loaded raw from ROM by + * front.c load_briefing_text_for_stage(); without this every string id and + * difficulty gate is byte-swapped -> blank briefing / objective text. + */ +#define ROMDATA_BRIEFING_U16S 24 +void romdataFixupBriefing(u8 *blob); + /* * D50 (docs/internals.md): re-lay out a font segment (struct font: * kerning[169] + chars[94] + glyph pixel data) from the N64 ROM layout to diff --git a/port/src/romdata.c b/port/src/romdata.c index c35fc09a..dac3561a 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -437,6 +437,28 @@ void romdataFixupLangBank(u8 *blob, u32 blobSize) } } +/* + * D178 (docs/dev/findings.md): a briefing segment (Ubrief*Z, assets/obseg/ + * brief/) serializes `struct BriefStruct` = { u16 brief[4]; + * { u16 textid; u16 enabled_difficulty; } objective[10] } — 24 big-endian + * u16 in ROM, loaded raw by load_briefing_text_for_stage() with no + * converter. On a little-endian host every field is byte-swapped: + * textid = getStringID(LDAM, 4) = 0x2C04 reads as 0x042C -> langGet() + * resolves bank 1 slot 44 (never loaded) -> NULL -> blank objective text, + * and enabled_difficulty 0x0001 (Secret Agent) reads as 0x0100 = 256 so + * `selected_difficulty >= enabled_difficulty` filters the line out entirely. + * Same for brief[0..3] (the Overview / M / Q / Moneypenny pages, D143). + * Fixed size (48 bytes), so a straight u16 swap of the whole struct. + */ +void romdataFixupBriefing(u8 *blob) +{ + u32 i; + for (i = 0; i < ROMDATA_BRIEFING_U16S; i++) { + u16 v = *(const u16 *)(blob + 2 * i); + *(u16 *)(blob + 2 * i) = (u16)((v >> 8) | (v << 8)); + } +} + /* * D50 (docs/internals.md): font segments (Zurich Bold, Bank Gothic) * serialize `struct font` in the N64 layout — big-endian scalars, 4-byte diff --git a/src/game/front.c b/src/game/front.c index 8cb83172..32871eb9 100644 --- a/src/game/front.c +++ b/src/game/front.c @@ -38,6 +38,10 @@ #include "assets/obseg/text/LtitleE.h" #include "matrixmath.h" #include "bg.h" +#ifdef PORT +#include +#include "romdata.h" /* D178: briefing-segment byte-order fixup */ +#endif #include "chrai.h" #include "title.h" #include @@ -6572,6 +6576,30 @@ void load_briefing_text_for_stage(void) argg = 0x200; ptrbriefingdata = _fileNameLoadToAddr(mission_folder_setup_entries[briefingpage].briefing_name_ptr, FILELOADMETHOD_DEFAULT, (u8 *) temp_s0, argg); +#ifdef PORT + /* D178: the Ubrief*Z segment is a raw ROM image of `struct BriefStruct` + * (24 big-endian u16, no converter). On a little-endian host every + * string id and difficulty gate reads byte-swapped -> langGet() misses + * and the objective/briefing text renders blank (D143). Decode in + * place, mirroring langFixupLoadedBank() in language.c. */ + if (ptrbriefingdata) { + if (getenv("GE_D178")) { + const u16 *w = (const u16 *)ptrbriefingdata; + fprintf(stderr, "D178 %s raw brief=%04x,%04x,%04x,%04x obj0=%04x/%04x obj3=%04x/%04x seldiff=%d\n", + (const char *)mission_folder_setup_entries[briefingpage].briefing_name_ptr, + w[0], w[1], w[2], w[3], w[4], w[5], w[10], w[11], + (int)selected_difficulty); + } + romdataFixupBriefing((u8 *)ptrbriefingdata); + if (getenv("GE_D178")) { + const u16 *w = (const u16 *)ptrbriefingdata; + fprintf(stderr, "D178 %s fix brief=%04x,%04x,%04x,%04x obj0=%04x/%04x obj3=%04x/%04x\n", + (const char *)mission_folder_setup_entries[briefingpage].briefing_name_ptr, + w[0], w[1], w[2], w[3], w[4], w[5], w[10], w[11]); + } + } +#endif + // what is this temp_s0 += argg / 8;