From 88bcef32a335fff650fb25380c053afd890bbbf9 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:35:41 -0400 Subject: [PATCH] Performance work D473-D477; scaffolding batches 3-4 Uncached getenv per vertex batch (D473), fast3d micro-optimisations (D474), hitch diagnosis (D475), CI4 palette-bank cache key (D476), spike counters, CI8 palette churn measurement (D477). Dead diagnostic probes removed. Refs: D473 D474 D475 D476 D477 GE_PERFSTAT --- docs/ROADMAP.md | 11 +- docs/dev/GE-ENV-PROBES.md | 16 +-- docs/dev/findings-index.csv | 9 +- docs/dev/findings.md | 64 ++++++++- port/fast3d/gfx_pc.cpp | 150 +++++++++++++++----- port/include/portaddr.h | 14 ++ port/shim/PR/os.h | 3 +- port/src/dram.c | 7 +- port/src/libultra.c | 2 +- port/src/n64stubs.c | 2 +- src/game/bondview2.c | 276 ++---------------------------------- src/game/chr.c | 200 -------------------------- src/game/chraction.c | 24 ---- src/game/chrai.c | 38 ----- src/game/front.c | 84 ----------- src/game/model.c | 127 +---------------- src/game/objecthandler.c | 37 ----- 17 files changed, 229 insertions(+), 835 deletions(-) create mode 100644 port/include/portaddr.h diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 84a69b06..52400af0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -107,7 +107,7 @@ below). | PAL and JP ROMs aren't supported | NTSC-U only | Use an NTSC-U ROM | D258, §1 | | `All unlocked` may already have written fake unlocks into a save from an older build | Save corruption | **Fixed on `release/v0.4.1`**: `All unlocked` is now a RAM-only override (D442) and never writes the save. Fakes an older build already wrote are not repaired; back up `data/ge007.eep`. Delete this row when it ships. | D442, D387, §5a | | `Game.SkipIntro` also skips the post-mission failure dossier | Minor | Leave SkipIntro off | D408, §2 | -| F10 overlay stretches in native widescreen (menus pillarbox correctly) | Cosmetic | — | D335b, §5a | +| F10 overlay stretches in native widescreen (menus pillarbox correctly) | Cosmetic | **Fixed on `release/v0.4.1`** (D472 pillarboxes the overlay card); delete this row when it ships | D335b, D472, §5a | | Rareware logo shows a subtle texture-filtering artifact | Cosmetic | — | D75, §2 | | Steam Deck: a first launch in Desktop Mode skips the Deck preset | Settings | **Fixed on `release/v0.4.1`** (real-Deck check owed); delete this row when it ships. Until then: first launch in Game Mode, or delete `ge007.ini` and relaunch | D283, §4 | | ~30 fps on very low-end GPUs (e.g. Intel HD 400) | Performance | Set `Video.LowEndMode=1` in `ge007.ini` | D339, #92, §4 | @@ -141,7 +141,7 @@ below). | Front-end: Rareware logo filtering, cast-roll models | partial | D75 | Cosmetic. The other front-end logos are fixed. The cast-roll character models have never been verified. | | Authored mip chains not sampled for explicit-LOD textures | post-1.0 | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. **Decided 2026-10-01:** after 1.0, together with the LOD-fraction approximation as one "real RDP LOD" project. | | Approximations in the software RSP | open | `gfx_pc.cpp` `G_CCMUX_LOD_FRACTION`; `Video.WrapFix` (§F row `RC3 · D167`, D74 family) | The LOD-fraction CC input is an eyeballed approximation. The non-power-of-two wrap fix is still opt-in (default off), with unknown visible impact. Both need a comparison against 1964. **2026-09-30 triage** (`docs/dev/notes/FIDELITY-TRIAGE.md`): the LOD-fraction input is bundled with D323 as one "real RDP LOD" project (fix later, large). **WrapFix is maintainer-owned:** a 1964 Depot-ceiling screenshot vs the port with WrapFix on/off decides the default. | -| Latent 64-bit ABI hazards | partial | D441, #107, #108; ~396 `-Wpointer-to-int-cast` sites at the start of the sweep (185 remain in the 17 touched files; many are intentional `(s32)&ANIM_DATA_*` offsets, D34); AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | Part 1 landed (D441): 17 files widened to `uintptr_t` under `#ifdef PORT`, build and Bunker/Silo runs verified. **Still owed:** the full census with per-site provenance (H/R/I/?, table not yet written) plus a **store-to-u32 vs arithmetic-only** tag per site (a `uintptr_t` widening is enough only for arithmetic; sites that store back into a u32 field need `portHostToN64` under the macOS port_addr model), an env-gated high-arena test mode, and a check under an arena above 4 GB. Harmless today because the arena sits low. They would bite on macOS/ARM/ASLR. **#108 concern:** widening the `sndPlaySfx` bound disables the guard on every platform; prefer a range check. **#107 will now conflict** with D441 in `bg.c` (`bgRoomCalcBB`), so rebase or close it during the outside-PR review. A read-only provenance pass over the remaining sites is a good local-Qwen delegate task. **2026-10-01:** D457(c)(d) merged (player matrix pointers, Model/PROMOTE/vtxstore pointer width); #107's two sites fixed (D441); read-side census part 2 (403 `-Wint-conversion`/`-Wint-to-pointer-cast` warnings, triaged by file) in the local note `ABI-CENSUS-D441-PART2.md`; model.c rows need a re-run after D457(d). | +| Latent 64-bit ABI hazards | partial | D441, #107, #108; ~396 `-Wpointer-to-int-cast` sites at the start of the sweep (185 remain in the 17 touched files; many are intentional `(s32)&ANIM_DATA_*` offsets, D34); AUDIT-M6 `struct player`/`struct hand` raw offsets (porting-notes §A1) | Part 1 landed (D441): 17 files widened to `uintptr_t` under `#ifdef PORT`, build and Bunker/Silo runs verified. **Still owed:** the full census with per-site provenance (H/R/I/?, table not yet written) plus a **store-to-u32 vs arithmetic-only** tag per site (a `uintptr_t` widening is enough only for arithmetic; sites that store back into a u32 field need `portHostToN64` under the macOS port_addr model), an env-gated high-arena test mode, and a check under an arena above 4 GB. Harmless today because the arena sits low. They would bite on macOS/ARM/ASLR. **#108 concern:** widening the `sndPlaySfx` bound disables the guard on every platform; prefer a range check. **#107 will now conflict** with D441 in `bg.c` (`bgRoomCalcBB`), so rebase or close it during the outside-PR review. A read-only provenance pass over the remaining sites is a good local-Qwen delegate task. **2026-10-01:** D457(c)(d) merged (player matrix pointers, Model/PROMOTE/vtxstore pointer width); #107's two sites fixed (D441); read-side census part 2 (403 `-Wint-conversion`/`-Wint-to-pointer-cast` warnings, triaged by file) in the local note `ABI-CENSUS-D441-PART2.md`; model.c rows need a re-run after D457(d). **Stage B plan (2026-10-01, maintainer's local design note):** (1) centralise the hard-coded arena address windows: **done 2026-10-02** (`port/include/portaddr.h`: `PORT_DRAM_V1_BASE` / `PORT_DRAM_K0_BASE` / `PORT_DRAM_SIZE`, used by `OS_K0_TO_PHYSICAL`, `dram.c`, `libultra.c` bases, `n64stubs.c`, the fast3d window checks; compile-time constants, no codegen change; menu / Bunker / Statue smoke clean). Left: the three link-time symbols in `port/src/dram_syms.s` (needs a preprocessed `.S` or `--defsym`), and a runtime-derived base for macOS; (2) widen `mema.c` (`memaspace.addr` s32 with `0xffffffff` sentinels, `memaRealloc`/`_memaFree` s32 params; the deferred D453 item); (3) build the high-arena test flavour (`GE_HIGHARENA_BASE`, tier 1 at 0x90000000, tier 2 above 4 GiB) and iterate on the first failures; (4) the heap-backed animation-table slots (`initanitable.c`); converges with #95's port_addr chokepoint. | ## 3. Verification debt (fixed; a live check is owed) @@ -162,7 +162,7 @@ A single campaign pass with these on the checklist would clear most of them. | Settings/F10 batch | D356, D358, D361, D363, D374, D406 | Live accept of each item in the F10 overlay. (D377–D379 residuals were waived.) | | Controller feel | CONTROLLER-INPUT-PLAN Wave A, D204 | Per-stick deadzone/smoothing feel on a pad and on Deck; D204 tempo by ear; `Input.RumbleScale=0` silences rumble. | | Widescreen HUD at 21:9 | D334/D335 | One ultrawide HUD playthrough. | -| Older "owed" notes likely cleared by the 2026-09-28 campaign sign-off | D150, D177, D264, D369, D371, D372 | Tick off in findings (bookkeeping only). | +| Older "owed" notes | D371, D372 | **Bookkeeping done 2026-10-01:** D150, D177, D264 ticked off against the 2026-09-28 campaign sign-off; D369 accepted in use. Still owed: D371 (GEPD key-layout preset + reload/crouch bindings: select the preset in F10 and play a level). (D372 low-end preset: no further effort; low-end work is real optimizations, see §4.) | ## 4. Platform @@ -170,7 +170,8 @@ A single campaign pass with these on the checklist would clear most of them. |---|---|---|---| | macOS (Intel + Apple Silicon) / ARM64 Linux | open | PRs #88, #95; #101 | Outside drafts, parked for `main` until after 1.0. **Address model decided (2026-09-30): port_addr (#95)**, i.e. a single `portN64ToHost`/`PORT_N64PTR` chokepoint with a `PORT_ADDR_STRICT` validator, over the dram-scoped alternative. The `macos` integration branch **exists on origin** (2026-09-30). It was cut from `origin/main` `0e8c2ce2`, and it takes #88 (merged as `c4153157`, using #95's conflict-resolved copy `ae1884cc`), #132 (Linux build fix, `39d4f8d0`: #88's `romdata.c` included `` before `ultra64.h`, and glibc's `errno` macro broke the `errno` fields in `PR/os.h`; #95 needs it on rebase) and #95 (retargeted to it, still open). italoarruda's work also goes there. Nothing merges from it to `main` before the post-1.0 pass. **Open against `macos`:** #131, the fork-friendly self-hosted sweep. It adds `platform` and `runner_labels` inputs, a macOS job running `level_sweep_mac.sh` with a JSON verdict, seed-dir-only ROM staging, and `docs/selfhosted-sweep.md`. It is a scaffold that has not been run on a Mac, and it needs #95 in `macos` first. **When `macos` is next rebased onto `main`** (after the v0.4.1 push), `selfhosted.yml` will conflict with the v0.4.1 ROM-cache fix. Take #131's version, which already includes that fix. The crouched-rifle table fix (`chr.c`), found three times (#95, italoarruda, #120), landed on `release/v0.4.1` via #120 (D445) on 2026-09-30. #120's shape is the correct one (it restores the N64 table length of 2; #95's terminator-only version leaves length 1), so **drop #95's `crouched_rifle` hunk** when `macos` is rebased. #95's finding labels (D29x, then D405–D421) clash with ours, so relabel on merge. The maintainer tests firsthand before merging. Needs the ABI-hazard sweep in §2 (store-to-u32 tagging). | | Platform parity gate (Linux co-equal with Windows) | open | — | **Decided 2026-09-30:** yes. Every released platform carries the same gate: a 21-level sweep plus a campaign pass. Needs the golden baselines on both platforms first (§7). The last known Linux crash (D190) was closed as not-a-bug. | -| Low-end GPUs (~30 fps on Celeron/HD 400) | open | D339, #92 | Needs `GE_PERFSTAT` data from that class of hardware. The D372 low-end preset is the mitigation. | +| Low-end GPUs (~30 fps on Celeron/HD 400) | open | D339, #92 | **Direction (maintainer, 2026-10-01): real optimizations, not a preset.** **2026-10-01 profile** (all-thread sampler, local note `PERF-HOTSPOTS.md`, probes on branch `perf/profile-hotspots`): the top render-thread hotspot was an uncached getenv per vertex batch, fixed as **D473** (Statue display-list CPU about -55%). After it, whole-process CPU is ~1.5-2 ms/frame on a fast machine (~8-10 ms scaled 5x), so the N3060 / HD 400 is probably GPU- or driver-bound (per-draw cost, MSAA, fill). **Next:** one `GE_PERFSTAT=1` run on that class of hardware (compare `gpu=` vs `run=` vs `interval=`), then target what dominates: draw-call/state-change reduction (needs a flush-reason histogram on the target), texture-bind lookups, per-triangle debug hooks, combiner lookup (ranked in the note). The D372 preset gets no further effort. | +| Statue/Cradle hitching (maintainer, 2026-09-30) | open | `GE_PERFSTAT` spike counters (2026-10-01) | **Diagnosed 2026-10-01 (D475):** no sustained load. The low-CPU ~33 ms frames are the game's own frame limiter skipping a retrace after a slightly late gfx task (`boss.c` tick-interval check; a D13-class timing interplay, needs a design look and possibly Rule-2). The CPU-side bursts are CI-palette re-imports (one texture re-imported with 51 palettes in ~8 frames), not first-sight loads; room-texture pre-warm was built and measured, with ~no gain (unmerged branch `perf/prewarm`). **CI8 palette re-imports ruled out as a CPU cost (D477, 2026-10-02):** decode+upload ~0.008 ms/frame, worst frame 0.44 ms; the cache already holds the animated palettes (10 re-misses in 1314); the same texture is drawn with several palettes per frame, so an in-place update path would thrash. GPU palette lookup is not justified by this data. CI4 part done (D476). Frame-limiter design look done 2026-10-02: no independent pacing bug; the skips follow long gfx tasks. **Next (if the hitches still matter):** a per-frame timeline probe (game-thread build + limiter decision, render-thread misses/new textures/shader compiles, per-frame GPU timestamps, swap time) over >=5 runs, PresentMon during real play at a spot that hitches, then a disable-falsifier per suspect (driver-deferred texture allocation, `glGenerateMipmap` under three-point filtering, shader compiles). Needs the maintainer's description of where the hitch shows. | | Steam Deck preset skipped when first launched from Desktop Mode | verify | D283 | Fixed on `release/v0.4.1`: the Deck is detected by DMI hardware id (Valve Jupiter/Galileo; `STEAMOS` kept as a second signal) and the preset applies once per ini, only while Fullscreen/Maximized are untouched. Only the Windows build and a Linux `-fsyntax-only` check ran. **Owed: one run on a real Deck** (Desktop-Mode-created ini, then Game Mode; expect fullscreen 1280x800 and the log line `video: Steam Deck (DMI Valve ...)`). The Known-issues row above goes when this ships. | | Windows 7 | decision | #98 | Recommend won't-fix (toolchain). | @@ -263,7 +264,7 @@ and a stage loader. | Scripted-playthrough harness | open | `GE_INPUTSCRIPT` | Input script + `GE_PCDUMP` + crash-log check, for unattended regression runs. The tool exists; the harness doesn't. | | Golden baselines | open | `tools_pc/verify.sh`, D117 | bunker1 has been noisy since M-84, so the per-patch gate is unreliable. Re-base it, extend to all 21 levels × both platforms, and finish `GE_DETERM`. | | Self-hosted CI runner link failure | open | D244 | Needs a `pacman -Syu` on the runner (maintainer-only access). Release tag builds use GitHub runners, so it doesn't block a release. | -| Remove temporary scaffolding before a release | partial | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | The 2026-09-30 pass removed D207S, D157, D252POOL, D245V, D288, D309, the D318B suite, the D320 repro harness and `scan_op12/13.py`, demoted the D318 watchdog to detect-and-log, and cached the per-frame `getenv` sites. **Left:** dead registry rows (D116, D51, D56, D60-D63, D69*, D71LOG, D85DUMP, D86-D88, D90, D96, D104, D154, D178); closed-finding TEMPs (`GE_D243X2/X4` in `chr.c`'s cutscene path, `GE_D235_*` once D235 closes; **removed 2026-10-01:** `GE_D306C`, `GE_D318T` (batch 2); `GE_DYNTEXHASH_OFF`, `GE_FORCEALARM`, `GE_D204_OLD`, `GE_D252`, `GE_RSEED_LV` were already gone); the now-inert sky tile-capture/shift machinery in `sky.c` (`skyPortCaptureTile`, `skyPortPickShift`, `GE_D245_FIXEDSHIFT`); and the M-183/M-185 clamps, which need a full cutscene re-check first. | +| Remove temporary scaffolding before a release | partial | [`dev/GE-ENV-PROBES.md`](dev/GE-ENV-PROBES.md), D302 | The 2026-09-30 pass removed D207S, D157, D252POOL, D245V, D288, D309, the D318B suite, the D320 repro harness and `scan_op12/13.py`, demoted the D318 watchdog to detect-and-log, and cached the per-frame `getenv` sites. **Left:** dead registry rows (D116, D51, D56, D60-D63, D69*, D71LOG, D85DUMP, D86-D88, D90, D96, D104, D154, D178); closed-finding TEMPs (`GE_D235_*` once D235 closes; **removed 2026-10-01:** `GE_D306C`, `GE_D318T` (batch 2); the whole D243 diagnostic set -- `GE_D243X2`, `GE_D243X4`, `GE_D243M` (+ its `d243mProbeActive`/frame-counter plumbing and all `D243M:` log lines in `bondview2.c`/`chr.c`/`chraction.c`/`chrai.c`/`model.c`/`objecthandler.c`), `GE_D243CAM`, `GE_D243SWIRL`, `GE_D243` (front.c menu trace) (batch 3, `chore/strip-d243-probes`; the shipped fix and the M-183/M-185 clamps are kept); `GE_DYNTEXHASH_OFF`, `GE_FORCEALARM`, `GE_D204_OLD`, `GE_D252`, `GE_RSEED_LV` were already gone); the now-inert sky tile-capture/shift machinery in `sky.c` (`skyPortCaptureTile`, `skyPortPickShift`, `GE_D245_FIXEDSHIFT`); and the M-183/M-185 clamps, which need a full cutscene re-check first. | | Findings log and doc size | partial | `tools_pc/docs_budget.py` | The budget checker exists (per-file and tier-1 token budgets; `--toc FILE`), and everything is within budget. `findings.md` is still about 2 MB (~500k tokens), so agents must keep using the index. Remaining: nothing blocks; trim only if the budget check starts failing. | | Branch and PR housekeeping | open | — | 30 landed local branches pruned 2026-09-30 (tips kept in the notes archive, `prune-branches-2026-09-30.txt`). `docs/m138-security-disclosure` landed on `release/v0.4.1` with a stale-fact pass (SECURITY.md, the security-and-fidelity page, README roadmap bullet). `feat/unlocked-fps-phase1-audit`'s commit (Phase 0 resolved by D248, Phase 1 audit) landed as `docs/unlocked-fps-audit`. **On hold by the maintainer (2026-09-30, do not prune yet):** probe-only leftovers `d243-verify-local`, `investigate/d243-cutscene-race`, `park/d236-probes-m139`. Local `main` and `release/v0.4.1` are deliberately not pushed. **Pushed on 2026-09-30:** the `macos` integration branch; `ci/selfhosted-fork-runners` (PR #131 into `macos`, open); `fix/macos-errno-include` (PR #132, merged into `macos`). Pruned after merging, 2026-09-30: `fix/selfhosted-rom-no-cache`, `docs/macos-ci-tracking` and `fix/macos-errno-include` (local and remote), plus the `review/pr88` and `review/pr95` read-only refs. PR #62 (mouse dt-decouple) stays held until mouse feel is fundamentally better. | | Docs site: Web 1.0 phase 2 | decision | — | Phase 1 shipped. Phase 2 (centred serif page) is plan-only. | diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index a6b4a428..09252636 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -50,13 +50,13 @@ Two classes: | `GE_WRAPFIX` | `port/fast3d/gfx_pc.cpp:3166` (`gfx_set_wrap_fix`) | RC3 test override for the `Video.WrapFix` D74/D167 wrap-block path (`atoi != 0` wins over the ini). | **live** (RC3 not finished) | | `GE_SAVELOG` | `src/game/file.c:46`, `src/game/file2.c:16` (`SAVELOG` macro), `src/boss.c:749`, `src/game/objective_status.c:297,343` | Traces the campaign save/unlock chain (objective completion → `end_of_mission_briefing` → `fileWriteSave` → EEPROM). Used to confirm D157. `g_savelogObjOnce` gates the per-criterion dump to the `bossReturnTitleStage` call. | **live** (strip once a few more level boundaries are playtested) | | `GE_D160` | `src/boss.c:744`, `src/game/bondview2.c:791`, `src/aicommands.def:10055,10181,10236,10242,10247` (all `#ifdef PORT`) | Dam exit-cutscene (D148/D160) diagnostic: trace points in `bossReturnTitleStage`, `bondviewSetCameraMode`, and AI cmds `EndLevel`/`exit_level`/`CameraLookAtBondFromPad`/`CameraSwitch`. **Investigation in progress** — user owes a `GE_D160=1` Dam run. | **live** | -| `GE_D243` | `src/game/front.c` (`menu_init`, cached `s_d243`) | Per-menu-transition trace with a local per-tick frame counter (D243 stale-DL-burst screen identification). Counter is NOT numerically identical to the port's internal frame count — cross-reference by elapsed offset. | **live** (D243) | -| `GE_D243CAM` | `src/game/bondview2.c` (`bondviewFrozenCameraTick`, three `CAMERAMODE_POSEND` branches) | Dumps eye/look-at positions (+theta/verta on the `gBondViewCutscene` branch, +dt/angle/pad on the orbit branch) per call. **Found M-143**: the `gBondViewCutscene` branch fires and its look-at (`field_3C4/8/C`) sweeps through a filter catch-up after the cutscene warp — the shake mechanism. | **live** (D243) | -| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159/M-162/M-163/M-166, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (M-159's frustum hypothesis, REFUTED by M-160) + the live `prop->pos` + `actiontype=`/`anim=` (M-160/M-164: which of `chrTick`'s branches fired, the attached-animation pointer) + `aidx=`/`animframe=` (M-166: the animation reverse-resolved to its `animation_table_ptrs1` index, GE_D193A's technique, + its current playback phase — tests whether the visible chr's animation *phase* at cutscene-entry, not just its identity, is what differs from N64) + `x2active=`/`x2raw=` (M-162/M-163, gated on `GE_D243X2`: whether the decisive save/restore experiment ran this tick, and the raw pre-restore position it overrode — the whole `prop->pos` vector as of M-163, not just Z). Silent during FPS play, silent on the `GE_D243X2` fields unless that env var is also set. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) | -| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. **M-163's live result: NEGATIVE for the visible shake** — `prop->pos` genuinely went smooth (log-verified) but the shake was unchanged, because the camera's look-at filter reads a *different* variable (`field_488.pos`, see `GE_D243X3`) that this experiment never touches. Kept in the tree as a real, useful (if ultimately not the visible-symptom fix) data point — always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode. Do not ship. | **live, test-only** (D243) | +| `GE_D243` | `src/game/front.c` (`menu_init`, cached `s_d243`) | Per-menu-transition trace with a local per-tick frame counter (D243 stale-DL-burst screen identification). Counter is NOT numerically identical to the port's internal frame count — cross-reference by elapsed offset. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE | +| `GE_D243CAM` | `src/game/bondview2.c` (`bondviewFrozenCameraTick`, three `CAMERAMODE_POSEND` branches) | Dumps eye/look-at positions (+theta/verta on the `gBondViewCutscene` branch, +dt/angle/pad on the orbit branch) per call. **Found M-143**: the `gBondViewCutscene` branch fires and its look-at (`field_3C4/8/C`) sweeps through a filter catch-up after the cutscene warp — the shake mechanism. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE | +| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159/M-162/M-163/M-166, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (M-159's frustum hypothesis, REFUTED by M-160) + the live `prop->pos` + `actiontype=`/`anim=` (M-160/M-164: which of `chrTick`'s branches fired, the attached-animation pointer) + `aidx=`/`animframe=` (M-166: the animation reverse-resolved to its `animation_table_ptrs1` index, GE_D193A's technique, + its current playback phase — tests whether the visible chr's animation *phase* at cutscene-entry, not just its identity, is what differs from N64) + `x2active=`/`x2raw=` (M-162/M-163, gated on `GE_D243X2`: whether the decisive save/restore experiment ran this tick, and the raw pre-restore position it overrode — the whole `prop->pos` vector as of M-163, not just Z). Silent during FPS play, silent on the `GE_D243X2` fields unless that env var is also set. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE | +| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. **M-163's live result: NEGATIVE for the visible shake** — `prop->pos` genuinely went smooth (log-verified) but the shake was unchanged, because the camera's look-at filter reads a *different* variable (`field_488.pos`, see `GE_D243X3`) that this experiment never touches. Kept in the tree as a real, useful (if ultimately not the visible-symptom fix) data point — always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode. Do not ship. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE | | `GE_D243X3` | `src/game/bondview2.c` (`playerTick`, the `field_488.pos` write site, gated on `g_CameraMode == CAMERAMODE_POSEND`) | **Not a probe — the experiment that actually confirmed the mechanism, off by default, NOT a fix.** M-167: skips the `field_488.pos.x/y/z` assignment (`bondview2.c:10495-10497`) entirely while POSEND is active, freezing it at whatever value it held the instant suppression engaged. `field_488.pos` — not `prop->pos` — is what the cutscene's leaky-integrator look-at filter (M-143, `field_3B8`/`field_3C4-8-C`) actually reads; it comes from `render_pos` (the model's animated-skeleton readback), an independent data path from `prop->pos`. **Live-confirmed positive: user — "yep camera shake is gone now."** Logs a companion `D243M: f488write frame=... idx=... x3active=... x3skip=... epoch=... f488pos=x,y,z` line (always-on when `GE_D243M=1`, independent of whether `GE_D243X3` itself is set) so the freeze is log-verifiable. **M-169: no longer freezes forever** — re-baselines (`x3skip=0`, write allowed through) exactly on the tick a legitimate shot-change teleport fires (`d243GetTeleportEpoch()`, bumped by `chrai.c`'s `AI_TRYTeleportingChrToPad` right after its real `setsuboffset()` call), addressing the M-168 postmortem's "camera looks the wrong way for later shots" incompleteness — untested live yet, next session's first move. **Superseded: D243 shipped its permanent fix (`4ccab22c`, M-170), which removed the X3 suppression block from `playerTick` — no live site remains.** Kept here as a tombstone; the generator flags it GONE. | **dead** (D243 closed — probe removed) | -| `GE_D243X4` | `src/game/chr.c` (`chrTick`, the `render_pos` snapshot/restore, gated on `d243mProbeActive()`) | **Not a probe — the M-168 experiment applying X3's technique to the actual draw-path consumer, off by default, NOT a fix.** Snapshots `model->render_pos[]` after a 2-tick warm-up once POSEND engages, then holds it steady by copying the cached contents back over each subsequent tick's freshly-computed buffer — `subcalcmatrices()` always still runs first (never skipped; the first two attempts skipped it and crashed on a reused-pointer stale `-1` tombstone, see findings.md D243 M-168). Addresses Bond's own rendered-body duplication (the consumer X3 never touched). Crash-free, stable, but same forever-freeze incompleteness as X3 pre-M-169. **M-169: same teleport-epoch re-baseline as X3** — the warm-up counter now also resets (re-snapshots) whenever `d243GetTeleportEpoch()` changes, not only on the active/inactive edge. Untested live yet. Revert once a real root-cause fix replaces it; do not ship as-is. | **live, test-only** (D243) | -| `GE_D243SWIRL` | `src/game/bondview2.c:10480` (`playerTick`, cached `d243SwirlTarget`) | D243 follow-up probe: `GE_D243SWIRL=` logs a per-tick line for that chr while the swirl/fade camera path is active (tick counter + state), to verify post-fix behavior of the swirl transition. Inert unless set to a non-negative chr number. | **live** (D243 verification) | +| `GE_D243X4` | `src/game/chr.c` (`chrTick`, the `render_pos` snapshot/restore, gated on `d243mProbeActive()`) | **Not a probe — the M-168 experiment applying X3's technique to the actual draw-path consumer, off by default, NOT a fix.** Snapshots `model->render_pos[]` after a 2-tick warm-up once POSEND engages, then holds it steady by copying the cached contents back over each subsequent tick's freshly-computed buffer — `subcalcmatrices()` always still runs first (never skipped; the first two attempts skipped it and crashed on a reused-pointer stale `-1` tombstone, see findings.md D243 M-168). Addresses Bond's own rendered-body duplication (the consumer X3 never touched). Crash-free, stable, but same forever-freeze incompleteness as X3 pre-M-169. **M-169: same teleport-epoch re-baseline as X3** — the warm-up counter now also resets (re-snapshots) whenever `d243GetTeleportEpoch()` changes, not only on the active/inactive edge. Untested live yet. Revert once a real root-cause fix replaces it; do not ship as-is. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE | +| `GE_D243SWIRL` | `src/game/bondview2.c:10480` (`playerTick`, cached `d243SwirlTarget`) | D243 follow-up probe: `GE_D243SWIRL=` logs a per-tick line for that chr while the swirl/fade camera path is active (tick counter + state), to verify post-fix behavior of the swirl transition. Inert unless set to a non-negative chr number. | **dead** (D243 FIXED) — removed 2026-10-01 (scaffolding strip batch 3, branch `chore/strip-d243-probes`); kept as a tombstone — the generator flags it GONE | | `GE_D193` | `src/game/frametiming.c` (`waitForNextFrame`, `#ifdef PORT`) | Per-wall-second line: `render=` rendered frames, `sim=` sim ticks delivered (Σ of the `deltaFrames`/`g_ClockTimer` fed downstream), `clamped=` frames whose catch-up hit `FRAMETIMING_PORT_MAX_CATCHUP`, `maxraw=` largest pre-clamp delta. Healthy steady state = `render=30 sim=60 clamped=0 maxraw=2` (measured M-80, all levels idle). Use to check whether the wall-clock timer under-advances under real AI load (D193). Cheap; safe for a full playtest. | **live** (D193) | | `GE_D193A` | `src/game/chr.c` (`chrTick` → `d193aSample`, plus `d193aMoved` in `chrUpdateAnim`; `#ifdef PORT`) | Per-wall-second per-ticked-chr locomotion telemetry (D193/D209): `chr=` chrnum, `act=` ACT_TYPE, `speed=` world-units travelled/s, `gtd=` g_GlobalTimerDelta, `tier=` `act_gopos.unk59` SPEED tier (0 walk / 1 run / 2 sprint; `-1` when not ACT_GOPOS), `gspd=` `act_gopos.speed` (**a turn rate, not a travel rate** — 0 means running straight), `typ=` PROP_TYPE (3=CHR, 6=VIEWER), `tick=`/`move=`/`frz=` chrTick entries vs `chrUpdateAnim` calls vs freeze-gated (move « tick = the chr is being visibility-culled out of its own locomotion; `move=0` at high `speed` = WAYMODE_MAGIC), `hid=` CHRFLAG_HIDDEN, then the root-motion scalars `scale` / `xlscale` / `animrate` / `playspeed` / `mspeed` (model->speed) / `endframe` / `anim` ptr / `aidx=` that pointer reverse-resolved to its `animation_table_ptrs1` index (40 walking, 41 sprinting, 42 running, 107 walking_unarmed — map via `src/game/initanitable.c`). `GE_D193A_CHR=` restricts to one chrnum. **This probe found D209**: Cradle showed `tier=1` (RUN) with `aidx=40/107` (walk anims) on every sample. | **live** (D193/D209) | | `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) | @@ -84,8 +84,8 @@ Two classes: | `GE_D235_NEXT=` | `src/boss.c` (with `GE_D235_RELOAD`) | **TEMP**: re-enter a *different* stage after the abort (level A → title → level B), used for D336 (Runway → Frigate). `GE_D85TEX=1` now also logs each stage's texture-pool size and address range (`src/game/initmttex.c`). | **TEMP** | | `GE_D320R="tick:action;..."` | `port/src/d320repro.c` (hooked in `src/game/lv.c` per-tick PORT block) | **TEMP** forced-repro harness keyed on `g_GlobalTimer`. Actions: `roster`, `jump=,list=,off=`, `bit=`, `trace=`, `rename=,`, `seed=,` (D320); `explode=,` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|` (game cheat activation) for D252 headless explosion/debris captures. | **dead** (D320 fixed via D329) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE; `port/src/d320repro.c` + its `lv.c` hook deleted | | `GE_AIMLOG=1` (+ `GE_INPUTSCRIPT` tokens `MDX` `MDY` `RHOLD` `LHOLD` `RREL`) | `port/src/input.c` | D337 aim-model harness: scripted sustained mouse deltas (treated as a grabbed mouse) and aim holds, plus a per-poll line with aim state, active device, the game's drawn crosshair x, `crosshair_x/y_pos` and camera angles. Measures range, edge-scroll and jitter headlessly. | **live** | -| `GE_PERFSTAT=1` | `port/fast3d/gfx_pc.cpp` (`gfx_run`/`gfx_end_frame`), `port/fast3d/gfx_opengl.cpp` (start/end_frame), `src/boss.c` (main loop) | #92/D339 perf probe: per-300-frame averages of render phases (pre / display-list / post / swap+pace), tris and draw batches per frame, game-thread busy time per scheduler message, and a GL_TIME_ELAPSED GPU timer (meaningful only when GPU-bound). | **live** | -| `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. | **live** (D75) | +| `GE_PERFSTAT=1` | `port/fast3d/gfx_pc.cpp` (`gfx_run`/`gfx_end_frame`), `port/fast3d/gfx_opengl.cpp` (start/end_frame), `src/boss.c` (main loop) | #92/D339 perf probe: per-300-frame averages of render phases (pre / display-list / post / swap+pace), tris and draw batches per frame, game-thread busy time per scheduler message, and a GL_TIME_ELAPSED GPU timer (meaningful only when GPU-bound). 2026-10-01: also prints `PERFSTAT spikes: max=..ms at frame N (dl= run=) >20ms= >33ms=` per 300-frame window (hitches hide in the averages). | **live** | +| `GE_D75V` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex`, end of the copy/transform loop; cached env check) | D75 (Nintendo logo) per-batch vertex probe: for each `G_VTX` batch in frames `lo..hi`, logs frame#, source pointer, n_vertices, original dest_index, raw ob[0..2] of the first vertex, min/max of the transformed x/y/z/w across the batch, and the MP translation row. Used to prove the transform is correct (M-195) and that every batch encodes v0=0 (slot-overwrite hypothesis). Value form `"lo-hi"` (e.g. `300-900` = the logo screen); unset = off. **D473 (2026-10-01): the cache was broken (re-read on every G_VTX when unset); now a real one-shot cache.** | **live** (D75) | | `GE_D75D` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_vertex` G_VTX case + `gfx_sp_tri1` rejection/emit gates; cached env check) | D75 (Nintendo logo) per-frame draw-path probe (M-196): for frames `lo..hi`, logs one line/frame with the V/T token stream (`seq`), max triangle vertex index, cumulative vertices loaded, and per-triangle accounting — trivial-reject / backface-cull counts, how many tris reach GL, how many of those are on-screen (NDC bbox ⊂ [−1,1]²), invisible-flag count, vertex-color min/max, first oml word, depth z/w range + NaN count, texture-sampler presence (`utex` = GL shader, `cutex` = combiner), combine mode, prim/env color. Used to REFUTE the slot-overwrite hypothesis (DL is streamed, max index 15) and narrow the bug to a texture-modulate material rendering black. M-197 added logo-mode `lg_*` fields (sampled only on tris with the logo combine word — the frame-level `utex`/`cutex` are first-tri-wins and were the source of the "combiner claims 0" false anomaly): `lg_utex`/`lg_tex` + tile-0 descriptor + texture buffer address/id/size for the logo material. Value form `"lo-hi"` (logo window ≈ `244-320` on a bare front-end boot); unset = off. Companion one-shot `D75DCC:` log in `gfx_generate_cc` fires when `combine_mode==0x009ffe4f19ffe4f1` to decode the logo's combine slots. | **live** (D75) | | `GE_PDMOUSEAIM` | `port/src/input.c:487` (`pdMouseAimEnabled`) | Force on/off the PD-style mouse-aim model (D194 lineage); unset = default (on in N64 aim mode unless legacy GEPD aim is selected). | **live** | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index d558d038..6a7eae21 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -58,7 +58,7 @@ D169,FIXED,Front-end mouse pointer can't reach the outer cells of the mission/le D178,FIXED,Pre-mission briefing screen:,FIXED (M-36) — high confidence D175,CLOSED,In-game stutter / brief hang during normal play —,"CLOSED (v0.4.0 campaign sign-off 2026-09-28, user:" D176,FIXED,"Surface exterior renders wrong (-level_36), two independent defects.",FIXED (2026-09-29 bookkeeping: -D177,FIXED,Ladders non-functional —,FIXED (M-36) — high confidence; interactive climb test owed +D177,FIXED,Ladders non-functional —,"FIXED (M-36) — high confidence; interactive climb test owed Owed climb test considered cleared by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01); reopen if seen." D172,FIXED,"Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).",FIXED (port/fast3d/gfx_pc.cpp gfx_lod_tile_offset)… D174,CLOSED,"""No blood effect"" (user QA report).","CLOSED — faithful (2026-09-26, user side-by-side on 1964:" D183,CLOSED,"M-36 Family A (""texture line/pitch shear"") is DISPROVEN for the Surface repro (-level_36).","CLOSED (2026-09-26, user-approved:" @@ -90,7 +90,7 @@ D108-D112,RESOLVED,skeletal models: d43_emit.py put_f32 byte-reversal bug,resolv D113-D116,FIXED,portal BFS ok · matrix chain ok · player raw-offset audit + gunfire.c THROW* · HUD text X-mirror,"D114/D116 CLOSED — NOT A BUG (M-33, see D168):" D117,CLOSED,"Frame nondeterminism root-caused; GE_DETERM attempted, not achieved.",CLOSED (2026-09-28 bookkeeping: D118,RESOLVED,"SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.",resolved (D118a residual) -D150,FIXED,watch OBJECTIVES / BRIEF page crash:,FIXED (src/str.c #ifdef PORT); interactive re-verify pending +D150,FIXED,watch OBJECTIVES / BRIEF page crash:,"FIXED (src/str.c #ifdef PORT); interactive re-verify pending Owed re-verify considered cleared by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01); reopen if seen." D260-D265,CLOSED,"v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → roo…",CLOSED (2026-09-26 bookkeeping: D266-D269,CLOSED,"D236/D265 wall, second pass:",CLOSED (2026-09-26 bookkeeping: D270,CLOSED,Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.,CLOSED (2026-09-26 bookkeeping: @@ -332,4 +332,9 @@ D469,FIXED,Gamepad presets + per-seat controller rebinding (ROADMAP 5a; supersed D470,FIXED,Audio master volume + output-device selection (ROADMAP 5a):,FIXED (live listening test owed) D471,FIXED,Controller-family button names (no glyphs) in the PC menus + player-index LEDs:,FIXED 2026-10-01 (local); headless captures for Xbox/PS/Nintendo; live PS/Nintendo pad check owed D472,FIXED,"F10 overlay polish: multi-pad ownership (audit G1-G3), widescreen pillarbox (D335b), red EXPERIMENTAL rows, unknown-ini-key warning.","FIXED 2026-10-01 (merged; port-only, no src/game)." +D473,FIXED,"Uncached getenv(""GE_D75V"") on every G_VTX:",FIXED 2026-10-01 (local); Statue display-list CPU about -55% in steady windows +D474,FIXED,fast3d micro-optimisations:,FIXED 2026-10-01 (local); output-identical by construction; measured gain on the dev machine within noise +D475,OPEN,Statue/Cradle hitches diagnosed:,OPEN (diagnosed 2026-10-01): +D476,FIXED,CI4 textures keyed on their own 16-entry palette bank instead of the whole 256-entry table (D217 hash):,FIXED 2026-10-02 (local); Statue sweep CI4 cache misses 189 -> 36 +D477,?,CI8 palette re-imports measured and ruled out as a CPU-side hitch cause:,MEASURED 2026-10-02 (local); premise of the D475/D476 follow-up refuted on the dev box; GPU/driver-side cost unmeasured D450,FIXED,"Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).","FIXED — port-only: the render thread's gfx_sdl_handle_events drains the same SDL queue as video.c's host pump but had no case for SDL_CONTROLLERDEVICEADDED/REMOVED, so it usually swallowed them and i…" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 34ed0f75..d5258bd6 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -443,7 +443,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D178 | **Pre-mission briefing screen: objectives blank / missing, briefing pages blank (also the D143 "briefing text blank" side effect) — FIXED (M-36).** — full `## D178` entry at file tail | **FIXED (M-36)** — high confidence | | D175 | **In-game stutter / brief hang during normal play** — full `## D175` entry at file tail | CLOSED (v0.4.0 campaign sign-off 2026-09-28, user: no stutter over the full campaign -- fixed). Earlier: OPEN (user, 2026-09-26: still happens occasionally; keep). Earlier: OPEN — observed, not investigated. **Steam Deck (Linux, 2026-09-27/28): occasional frame-timing hitches at low CPU/GPU (~15–30 %) — a frame-pacing/present profile, not a throughput bottleneck (see entry).** | | D176 | **Surface exterior renders wrong (`-level_36`), two independent defects.** (a) black sky = RDPHALF no-op, root-caused M-37; scroll-speed FIXED (perspective-correct tc in `skyPortRenderPoly`). (b) cliff/rock walls = grey diagonal static — NOT a decode bug (D183: 0/166 loads strided); re-scope from ROM ground truth of the wall texnum (M-37 inconclusive, leaning tiling-density). — full `## D176(a)` entries at file tail; see also GRAPHICS-BACKLOG.md D176 | FIXED (2026-09-29 bookkeeping: (a) sky via D227, human play-test confirmed; (b) Surface perimeter tree/rock wall via D236, closed M-198; GRAPHICS-BACKLOG row closed 2026-09-28). Earlier: **(a) scroll-speed FIXED** (fidelity pass; HIGH confidence on root cause, MODERATE on N64 parity); M-93 playtest + M-98 Defect-2 starburst screenshot in the sections. **(b) open.** | -| D177 | **Ladders non-functional — progression blocker (user QA) — FIXED (M-36): `count`/`rooms` land in the high half of a widened pointer.** — full `## D177` entry at file tail | **FIXED (M-36)** — high confidence; interactive climb test owed | +| D177 | **Ladders non-functional — progression blocker (user QA) — FIXED (M-36): `count`/`rooms` land in the high half of a widened pointer.** — full `## D177` entry at file tail | **FIXED (M-36)** — high confidence; interactive climb test owed Owed climb test considered cleared by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01); reopen if seen. | | D172 | **Bullet-impact / blood / spark particles render magenta or cyan instead of dark red (M-82, ROOT CAUSE FOUND + FIXED).** — full `## D172` entry at file tail | **FIXED (`port/fast3d/gfx_pc.cpp` `gfx_lod_tile_offset`)… | | D174 | **"No blood effect" (user QA report).** — full `## D174` entry at file tail | CLOSED — faithful (2026-09-26, user side-by-side on 1964: blood matches N64). Earlier: Blood renders (user, 2026-09-26); OPEN only for an N64-accuracy comparison on 1964. Earlier: OPEN — observed; first step = BUNKER1 firefight with regenerated sidecars | | D183 | **M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (`-level_36`).** The importers' pitch assumption is real but never fires there; the Surface cliff "grey static" is not a shear. Full evidence + what the walls actually are: §F "D183" below. A defensive, provably-no-op de-stride landed in `import_texture()` anyway (covers the `gfx_dp_load_tile` case the `SUPPORT_CHECK`s assert against), plus `GE_DTEX` STRIDED marker + `GE_TEXRAW` raw-source dump. | CLOSED (2026-09-26, user-approved: disproven hypothesis; the Surface sky issue it served was fixed under D176). Earlier: Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN | @@ -489,7 +489,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D113–D116 | portal BFS ok · matrix chain ok · player raw-offset audit + `gunfire.c` THROW* · HUD text X-mirror | **D114/D116 CLOSED — NOT A BUG (M-33, see D168): the "X-mirror" was an upside-down `GE_PCDUMP` capture misread as mirrored. PPM writer fixed; the game renders correctly on hardware.** | | D117 | **Frame nondeterminism root-caused; `GE_DETERM` attempted, not achieved.** — see §H (D117 M-8 root cause + M-52 addenda) | **CLOSED (2026-09-28 bookkeeping: won't-fix research item — the golden gate compares settled frames and no longer needs determinism; `GE_DETERM` left as-is).** Earlier: OPEN (mode attempted, not achieved) | | D118 | **SDL input layer; M-24 mouse-look rework (mode-aware map, real INI); D118b/c FIXED.** — see §H (D118 + M-24 entries) | resolved (D118a residual) | -| D150 | watch OBJECTIVES / BRIEF page crash: `strcat(buf, langGet(id))` with `langGet` → NULL (unloaded bank) → NULL deref; `str.c` str* builtins elide a plain param NULL check, so guard via asm-laundered `GE_IS_NULL` | FIXED (`src/str.c` `#ifdef PORT`); interactive re-verify pending | +| D150 | watch OBJECTIVES / BRIEF page crash: `strcat(buf, langGet(id))` with `langGet` → NULL (unloaded bank) → NULL deref; `str.c` str* builtins elide a plain param NULL check, so guard via asm-laundered `GE_IS_NULL` | FIXED (`src/str.c` `#ifdef PORT`); interactive re-verify pending Owed re-verify considered cleared by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01); reopen if seen. | | D260–D265 | v0.2.1 hotfix batch: D260 wheel direction swap (fixed) · D261 watch-menu nav auto-repeat (fixed, feel-check owed) · D262 watch item-preview model missing → root-caused to D264 (fixed at Gfx level, visual check pending) · D263 Facility Ourumov/Trevelyan beat — **resolved: not a bug**, proximity/LOS-triggered as on N64; full chain incl. execution shot verified firing on PC (teleport probe + user playtest) · D264 watch preview renderdata.flags zeroed by cross-global template read on PC (ABI/layout class; fixed) · D236/D265 Surface 1 tree "wall": IA16 importer dimensions transposed (fixed); decode attempt reverted after raw-byte proof that byte-pair is the authored image; root cause = `import_texture_ia4` decoding RDP I2:A2 as I3:A1, inverting the tree strip's alpha ramp → opaque repeating silhouettes; IA4 I3:A1→I2:A2 decode fix applied and user re-tested — **still a wall; reverted, parked** with evidence + suspect list in D265 | CLOSED (2026-09-26 bookkeeping: v0.2.1 hotfix batch shipped; D265 is a D236 sub-pass, closed M-198). Earlier: partial — see rows | | D266–D269 | D236/D265 wall, second pass: **the wall is CI8 32×32, not IA** (D269) — band-gated probe finds two draw classes per room stream (OPA oml=0xc4112078 + XLU oml=0xc41049d8), both BILERP/TEXTLOD/2-cycle/**FOG-on**; cards are soft gradient blobs, stripe period ≈ one card per visible room. D266: re-applied the reverted I2:A2 IA4 decode + replaced the hardcoded 0.19 edge quantize with AC-aware handling (census shows zero IA4 draws at boot). D267: IA8 dump infra — the three IA8 images are noise/mast sprites, not the wall. D268: force-alpha A/B proves the blend pipeline works. Ranked suspects now: fog range/application (D218 family) > TEXTLOD/mip sampling > draw density/culling > palette | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: OPEN (the wall itself) — probes live in tree (`GE_D266`/`GE_D266W`/`GE_D267`/`GE_D268_FORCEA`); the D266 decode + D271 portal-culling fixes **landed** (`532092c7`, M-139 R2 extraction) | | D270 | **Backfilled (M-132) — this label was never given its own row; only code comments and the HANDOFF archive documented it.** — full `## D270` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: dead-ended into D271/D272; infrastructure kept in tree | @@ -522,7 +522,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D140 | **Pause-menu crash (M-23 open → M-27 fixed).** — full `## D140` entry at file tail | FIXED (`src/game/bondview.h`, `src/game/bondview2.c`; `#ifdef PORT`). Exposed the next watch-render crash → **D141**. `-level_09`/`-level_20` unregressed (3/3). porting-notes.md §A. | | D118d | **Watch-menu list over-scrolls with keyboard (M-27, user bug report).** `game_options_inventory_navigation` / `sub_GAME_7F0A611C` (`options.c`) have a "slam the stick" fast-scroll: a raw *level* check `joyGetStickY(PLAYER_1) < -0x46` / `>= 0x47` that steps `watch_inventory_cursor_pos` by 1 **every frame** it's held. An N64 stick rarely sustains past ~0x46 on-axis so a human taps it; keyboard W/S (and SDL pads pegged at 0x80) sit there every frame → one keypress skips several items (breaks item selection needed to finish the game). Fix: `#ifdef PORT` macro `GE_WATCH_STICK_FAST{UP,DOWN}` → `0` for the port — stick list-nav then goes only through the latched single-step path (`watch_stick_y_pressed_*`, one step per press, existing game mechanism) and the -0x1e..-0x45 smooth-scroll band; D-pad/C-buttons keep edge-repeat. N64 `#else` = verbatim. | FIXED (`src/game/options.c`, `#ifdef PORT`). Build green, `-level_09` unregressed. **Feel to be confirmed in playtest** (probe couldn't reach the INVENTORY watch page headlessly). D118 input family. Latent sibling: `front.c` menu nav uses `joyGetStick*InRange` level checks the same way. | | D141 | **Watch-menu item-model crash (M-27), exposed by D140.** — full `## D141` entry at file tail | FIXED (`src/game/gunfire.c`, `#ifdef PORT`). Watch page now renders (weapon page verified — mirrored text D114/D116, dark weapon model D75, both parked cosmetic). `-level_09`/`-level_20` unregressed. | -| D150 | **Watch objective/briefing page crash (M-28).** — full `## D150` entry at file tail | FIXED (`src/str.c`, `#ifdef PORT`). Build green (`ntsc-final`); `-level_09` 600+ frames @ 91.67% unregressed. Watch-page repro is interactive — user to re-verify. porting-notes.md §C. | +| D150 | **Watch objective/briefing page crash (M-28).** — full `## D150` entry at file tail | FIXED (`src/str.c`, `#ifdef PORT`). Build green (`ntsc-final`); `-level_09` 600+ frames @ 91.67% unregressed. Watch-page repro is interactive — user to re-verify. porting-notes.md §C. Owed re-verify considered cleared by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01); reopen if seen. | | D152 | **Mission-failed / objective-failed → permanent black screen (M-28, user bug report — LIVE process inspected, not yet fixed).** — full `## D152` entry at file tail | FIXED (user, 2026-09-26: no black screen after mission/objective failure). Earlier: MITIGATED (`port/src/libultra.c`, M-28, `#ifdef PORT`/port-only).… | | D152+ | **D152 addendum — static audit + narrow fix (M-31).** — full `## D152+` entry at file tail | FIXED (`src/snd.c`, `port/src/libultra.c`, `#ifdef PORT`). porting-notes.md §D4. | | D154 | **`bg.c` room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).** — full `## D154` entry at file tail | **CLOSED (2026-09-28 bookkeeping: playtest gate satisfied — full v0.4.0 campaign played with no background-hit/raycast anomalies; ABI re-audited again under D313).** Earlier: **M-30 re-audit + fixes (still playtest-gated for final verify).** Cross-checked the ported G_TRI1 + all 12 G_TRI4 nibble recoveries twice against the N64 `#else` and the raw BE-word bit derivation —… | @@ -566,7 +566,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D260 | **v0.2.1 (user request): mouse-wheel weapon-select direction swapped.** — full `## D260` entry at file tail | FIXED — build green; one-line branch swap, trivially reviewable. | | D261 | **v0.2.1 (user request: "fix watch menu sensitivity for wasd/controller buttons"): holding W/S (or a fully-pressed stick axis) on the watch inventory page…** — full `## D261` entry at file tail | FIXED — feel-check received; W/S sensitivity follow-up logged above. | | D262 | **v0.2.1 (user report): "3D models missing on watch menu screen item select" — the selected-item 3D preview does not appear on the watch inventory page.** — full `## D262` entry at file tail | FIXED (primary cause, D264) — pixel-level confirmation pending user run. | -| D264 | **D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit…** — full `## D264` entry at file tail | FIXED — build green, probes removed; visual confirmation pending. | +| D264 | **D262's root cause (ABI/layout class, cf. D140/D52): the watch item preview's `ModelRenderData` template is read across two adjacent globals that no longer sit…** — full `## D264` entry at file tail | FIXED — build green, probes removed; visual confirmation pending. Covered by the 2026-09-28 v0.4.0 campaign sign-off (ROADMAP §3 bookkeeping, 2026-10-01). | | D263 | **v0.2.1 (user report): Facility — "Ourumov not shooting Trevelyan"; the scripted opening beat never triggers. RESOLVED: not a port bug — the beat is…** — full `## D263` entry at file tail | RESOLVED (not a bug), refined (v0.2.1 further user testing, 2026-09-15). The D318 re-report (2026-09-18) turned out to be a **different** failure mode (derailment race → softlock, see D318 RESOLVED) — D263's proximity-trigger mechanism map stands. | | D265 | **D236 investigation (port-layer, `port/fast3d/gfx_pc.cpp`): the tree cards are IA8/IA4, not IA16 — and `import_texture_ia4` decoded the RDP's I2:A2 texel as…** — full `## D265` entry at file tail | CLOSED (2026-09-26 bookkeeping: sub-pass of D236, which closed in M-198). Earlier: PARKED — reverted to baseline; evidence + suspect list above. | | D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | DEFERRED (user call 2026-09-28: v0.4.0 ships NTSC-only, README-disclosed; the verified repair path is post-v0.4 backlog). Earlier: OPEN — repair path verified, not applied (out of v0.2.0 scope). | @@ -653,7 +653,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D366 | **Input calibration label: call 50 the default, not original (2026-09-27).** — full `## D366` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. | | D367 | **F10 full-width control boxes misaligned with their contents (2026-09-27).** — full `## D367` entry at file tail | **CLOSED (2026-09-28 bookkeeping: superseded by D368).** Earlier: SUPERSEDED by D368 after user playtest: per-value boxes remained confusing. | | D368 | **Simplify F10 click targets: setting name selects, the rest changes (2026-09-27).** — full `## D368` entry at file tail | **CLOSED (2026-09-28 bookkeeping: superseded by D369 (input rule kept)).** Earlier: INPUT RULE KEPT; tinted strips user-rejected, removed in D369. | -| D369 | **Remove F10 row boxes/strips entirely (2026-09-27).** — full `## D369` entry at file tail | FIXED (awaiting live acceptance): no row backgrounds, control rectangles or hover paint; selected setting distinguished by brighter text, slider tracks remain. Input rule from D368 unchanged. | +| D369 | **Remove F10 row boxes/strips entirely (2026-09-27).** — full `## D369` entry at file tail | FIXED (awaiting live acceptance): no row backgrounds, control rectangles or hover paint; selected setting distinguished by brighter text, slider tracks remain. Input rule from D368 unchanged. Accepted in use: the maintainer used the F10 overlay extensively on 2026-10-01 (D469/D470/D472 testing) with no complaint about row styling. | | D370 | **F10 distance/volume drags hurt frame rate (2026-09-27).** — full `## D370` entry at file tail | **CLOSED (2026-09-28).** Shipped in v0.4.0; accepted in the user's final review / campaign playtest. (user: fixed.) | | D371 | **GEPD key-layout preset + reload/crouch bindings, v0.4.0 modern options wave M3 (2026-09-27).** full `## D371` entry at file tail | FIXED (headless-verified; awaiting user live accept): `Input.Layout` (0=FPS, 1=GEPD) + `Input.CrouchMode` (hold/toggle) in F10 INPUT; GEPD preset acts as per-key effective default under `[Input.Bind]` overrides; new IA_RELOAD/IA_CROUCH emit B (unbound by default). MODERN section dropped per D356 Turok-standard rule (rows land in functional INPUT). Boot/ini-round-trip/F10-page verified; in-game feel-check owed. | | D372 | **Low-end perf preset (v0.4.0 modern options wave M5, 2026-09-27).** full `## D372` entry at file tail | FIXED (headless-verified; awaiting user live accept): one-row `Video.LowEndMode` in F10 VIDEO; ON writes FpsCap 30 + MSAA x1 (restart), OFF restores compiled defaults (60/2). Registered key persists through orderly exit. | @@ -740,6 +740,11 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D470 | **Audio master volume + output-device selection (ROADMAP 5a):** `Audio.MasterVolume` (0..100, scratch-copy gain on the final mix, default 100 = byte-identical) and `Audio.Device` (name, default system device; live reopen on the audio-producer thread, fallback on missing/removed device). | FIXED (live listening test owed) | | D471 | **Controller-family button names (no glyphs) in the PC menus + player-index LEDs: PlayStation/Nintendo pads see their own button names in the F10/front-end help lines and the pad-binding column; Xbox/Steam Deck/unknown keep the old text exactly** — full `## D471` entry at file tail | FIXED 2026-10-01 (local); headless captures for Xbox/PS/Nintendo; live PS/Nintendo pad check owed | | D472 | **F10 overlay polish: multi-pad ownership (audit G1-G3), widescreen pillarbox (D335b), red EXPERIMENTAL rows, unknown-ini-key warning.** — full `## D472` entry at file tail | FIXED 2026-10-01 (merged; port-only, no src/game). Headless-verified pillarbox/red rows/warning at 1280x720; multi-pad ownership by code reading only, live check owed. | +| D473 | **Uncached `getenv("GE_D75V")` on every G_VTX: the probe's `d75v_lo == -1` meant both "not read yet" and "disabled", so with the env unset `gfx_sp_vertex` re-ran getenv on every vertex batch (~55-59% of render-thread CPU by sampling; D302/D250 class)** — full `## D473` entry at file tail | FIXED 2026-10-01 (local); Statue display-list CPU about -55% in steady windows | +| D474 | **fast3d micro-optimisations: unchanged `set_tile`/`set_tile_size` no longer dirty the textures (each dirty = cache lookup + `gfx_flush()` batch break); per-triangle/per-vertex debug hooks (D75D/ZF/D303) behind one cached gate** — full `## D474` entry at file tail | FIXED 2026-10-01 (local); output-identical by construction; measured gain on the dev machine within noise | +| D475 | **Statue/Cradle hitches diagnosed: low-CPU ~33 ms frames are the game's own frame limiter skipping a retrace (`boss.c` tick-interval check) after a 13-16 ms gfx task; CPU-side bursts are CI-palette re-imports (D217 keys CI textures on palette content), not first-sight loads. Room-texture pre-warm was built but gives ~no gain (kept on unmerged branch `perf/prewarm`, default off)** — full `## D475` entry at file tail | OPEN (diagnosed 2026-10-01): next levers = palette-as-texture/LRU for CI re-imports; frame-limiter interplay needs a design look | +| D476 | **CI4 textures keyed on their own 16-entry palette bank instead of the whole 256-entry table (D217 hash): unrelated TLUT loads no longer re-import unchanged CI4 textures** — full `## D476` entry at file tail | FIXED 2026-10-02 (local); Statue sweep CI4 cache misses 189 -> 36 | +| D477 | **CI8 palette re-imports measured and ruled out as a CPU-side hitch cause: decode+upload averages ~0.008 ms/frame (worst frame 0.44 ms); the cache already holds the animated palettes (1314 misses, 10 re-misses); one CI8 texture is drawn with several palettes per frame, so an in-place update path would thrash** — full `## D477` entry at file tail | MEASURED 2026-10-02 (local); premise of the D475/D476 follow-up refuted on the dev box; GPU/driver-side cost unmeasured | | D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). | | D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". | | D450 | **Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).** — full `## D450` entry at file tail | FIXED — port-only: the render thread's `gfx_sdl_handle_events` drains the same SDL queue as video.c's host pump but had no case for `SDL_CONTROLLERDEVICEADDED/REMOVED`, so it usually swallowed them and `inputRescanPads` never ran. Both pumps now post a request; the rescan runs at the top of `inputUpdate`, on the thread that reads the pads. | @@ -11718,6 +11723,8 @@ Archived capture: `ge007_d243_live.log`. D173 (level-start floating), D292 (death sinking), and the Cradle spin (also under D148) are cross-referenced as the same "third-person world model transform/lifecycle" family (M-145, and the M-160/M-164/M-165 addendum on D173/D292's own entries). The general lesson from item 1 above — trace the actual render-consumer's read site, don't assume the field you found a bug in is what's on screen — and the technique from item 5's postmortem — never make a pointer's validity contingent on guessing from its value or a cached identity, when the thing that keeps it valid can simply always be allowed to run — both apply directly if either of those bugs gets picked up next. +**2026-10-01: diagnostics stripped (ROADMAP §7 scaffolding strip, batch 3, branch `chore/strip-d243-probes`).** D243 is FIXED (M-189/M-190); the investigation scaffolding is removed, behaviour unchanged. Removed: `GE_D243M` and its plumbing (`d243mEnabled`/`d243mProbeActive`/`d243mGetFrameCounter`, `D243M:` CREATE/REMOVE/tick/draw/f488write/firinganim lines in `bondview2.c`; chrdraw census in both `drawjointlist` variants of `objecthandler.c`; animrestart in `chraction.c`; playanim in `chrai.c`; setanim_caller/setanimspeed/setanimplayspeed/tickstart/postadv/biganim and the clamp log lines in `model.c`; prebefore/onscreen in `chr.c`), `GE_D243X2` (prop->pos save/restore) and `GE_D243X4` (render_pos freeze) in `chr.c`, `GE_D243CAM` (3 sites) and `GE_D243SWIRL` in `bondview2.c`, and the `GE_D243` menu-transition trace + `d243MenuName` in `front.c`. (`GE_D243X3` was already gone.) Kept (the shipped fix and its supports): the `sizeof(Model)` buffer-layout fix in `bondview2.c` (M-189); the unconditional `field_488.pos` write + teleport-epoch filter re-seed (M-190: `d243NotifyTeleport`/`d243GetTeleportEpoch`, the call in `chrai.c` `AI_TRYTeleportingChrToPad`); `gameScriptedCameraActive()` and its two users (`model.c` clamps, `port/src/input.c` mouse-look gate); the M-183/M-185 playspeed/endframe clamps in `model.c` (Rule-2 retention sign-off, M-192) minus their log lines. Left alone: `GE_DEBUGMENU` in `src/boss.c` (a debug-menu enabler whose comment cites D243; not a D243 probe). + ## D244 — Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the… **Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the reinstalled box, run `34707798707`).** `src/game/objective_status.c:162` uses `#pragma weak objectiveGetStatus_WEAK = get_status_of_objective` (a decomp ground-truth alias, referenced 2-arg from `chrai.c:2538` — comment calls it "horrible hack to get ai matching"). The box's `g++`/`ld` reported as `x86_64-w64-mingw32/16.1.0`; a minimal local repro of the identical `#pragma weak` cross-TU function alias **links cleanly** on this session's own machine (`mingw-w64-x86_64-gcc 16.2.0-3`, `binutils 2.47-3`). Not a game-logic bug (the alias is unmodified decomp code, ground truth) and not a CMakeLists/link-flag issue (no `--gc-sections`/LTO in play) — the leading, unverified explanation is the self-hosted box's MSYS2 packages predate a PE/COFF weak-external fix or behavior change versus current MSYS2, consistent with rollout step 2's earlier finding that the box's mingw64 package set was stale/incomplete at initial setup. @@ -16385,3 +16392,50 @@ Whichever loop runs `SDL_PollEvent` first gets the event, and the render loop us **Unknown ini keys.** `config.c` counts keys that `applyKV` rejects during `configLoad` (`configUnknownKeyCount/First`); the F10 root page shows an amber "N UNKNOWN INI KEY(S) - SEE LOG" in the title line. Non-blocking, no behaviour change. Legacy migrated keys (e.g. old `Input.AimStyle`) also count, once, until the ini self-cleans on exit. **Verified (Windows, 1280x720, MouseEnabled=0, `GE_OPTIONSOVERLAY=1`, GE_PCDUMP frame 250, rc=0, no crash log):** root page pillarboxed with the warning (`scratch/f472/root.png`); Gameplay page with red Skip intro / All unlocked rows (`scratch/f472/gp250.png`). **Not verifiable headless:** multi-pad ownership. Maintainer live checks: with 2 pads, P2 Back opens F10, P2 navigates/adjusts/B-backs/Start-closes while P1 Back/Start does nothing; detach the owner mid-menu, the lowest remaining pad takes over; keyboard F10 + pad 1 still drives; solo single-pad unchanged; pillarboxed mouse hover/click lands on the drawn row at 16:9 and 21:9; PS pad in seat 2 shows PS names in the help line when it owns the overlay. + +## D473 — Uncached `getenv("GE_D75V")` per G_VTX (low-end perf, #92 / D339) + +**Found by** the 2026-10-01 CPU profile (in-process all-thread sampler `GE_PERFSAMP`, branch `perf/profile-hotspots`, local note `docs/dev/notes/PERF-HOTSPOTS.md`): ~55-59% of render-thread samples landed in `getenv` called from `gfx_sp_vertex`. The D75 vertex probe (`port/fast3d/gfx_pc.cpp`, `GE_D75V="lo-hi"`) cached its range in `d75v_lo`, but used `-1` both as "uncached" and as "disabled", so with the env unset (always, for players) the `if (d75v_lo < 0)` re-ran `getenv` on every G_VTX (170-520 per frame). Same class as D302/D250 (porting-notes D302); the registry wrongly described it as a cached check. + +**Fix:** a separate `d75v_init` flag; the probe's behaviour when set is unchanged, so output is identical. **Measured** (`GE_PERFSTAT`, Statue `-level_22`, maintainer settings 1600x900 MSAA 8x DD 400, same scripted sweep, RTX 5090): steady windows with matching triangle counts, display-list time 0.99 -> 0.46 ms and 1.31 -> 0.55 ms per frame; total `run` 5.6 -> 1.8-2.0 ms. Profile context: whole-process CPU is only ~1.5-2 ms/frame on this machine after the fix, so on the N3060 / HD 400 (#92) the remaining bottleneck is probably GPU/driver-side; next step there is a `GE_PERFSTAT` run on that hardware (gpu= vs run= vs interval=). Further CPU proposals (texture-bind lookups, redundant state changes, debug hooks per triangle, combiner lookup) are ranked in the local note. + +## D474 — fast3d micro-optimisations from the 2026-10-01 profile (low-end, #92) + +**Changes (`port/fast3d/gfx_pc.cpp`, output-identical by construction):** (1) `gfx_dp_set_tile` compares the post-normalisation tile fields with the stored ones and only sets `rdp.textures_changed[]` when something differs; `gfx_dp_set_tile_size` returns early when uls/ult/lrs/lrt are unchanged. A dirty flag costs a texture-cache lookup and forces `gfx_flush()` (a batch break, i.e. an extra draw call) on the next textured triangle. Texture/TLUT loads and G_TEXTURE still dirty the textures through their own paths, so a real change is never missed. Narrow stored fields compare conservatively (out-of-range args count as changed). (2) A single cached `tri_dbg()` gate (non-empty `GE_D75D`, `GE_ZF` or `GE_D303`, the same semantics as each hook's own lazy init) now guards the per-triangle emit/reject hooks and the per-G_VTX note, which previously each cost a call plus a frame-counter read even when disabled. + +**Measured** (`GE_PERFSTAT`, Statue, the same sweep and settings as D473): display-list time in comparable windows 0.46 -> 0.42 and 0.55 -> 0.54 ms (one window 1.57 -> 0.80), batches 255 -> 254. On this machine the gain is within run-to-run noise: redundant tile setters rarely broke batches in this scene. Kept because it is free and the batch/lookup savings scale with weaker CPUs and per-draw driver cost. Frame 1350 dump checked by eye: normal textured rendering. + +## D475 -- low-CPU spikes explained; texture pre-warm (Video.TexturePrewarm) built, little to gain + +**Game thread (GE_GTPERF).** On Statue (-level_22, 1550-frame view sweep) no scheduler message ever took more than 3 ms of game-thread work. The ~33 ms low-CPU frames (render CPU 1-2 ms) are retraces skipped by the game's own frame limiter (`boss.c` `mainTickElapsed < MAIN_LOOP_TICK_INTERVAL`): every gap line reads "1 skipped by tick-interval, 0 by pendingGfx" and the previous gfx task had run ~13-16 ms (includes the vsync/present wait). A frame that finishes a little late misses the tick threshold and the next DL is built one retrace later. Not a loading/AI/audio cost. + +**Texture bursts (GE_PWMISS).** After frame 150, 894 of 1002 cache misses were repeats of an already-imported address: the same CI8 texture and the same palette address with a different palette *content hash* (51 distinct hashes for one texture within ~8 frames from frame 587). D217 keys CI textures on palette content, so a palette the game rewrites in place (lighting/effect tint) re-decodes and re-uploads the texture every change. Only ~108 misses per 1000 frames were true first-sight. That is the real source of the 60-130-import bursts. + +**Pre-warm (built, default OFF).** `gfx_prewarm_walk()` (port/fast3d/gfx_pc.cpp) is a reduced DL interpreter run on the game thread from `bgRoomCalcBB` (one `#ifdef PORT` hook in src/game/bg.c, while the room is transiently loaded); it executes only texture-state commands and records import jobs (snapshot of tile/loaded-texture/palette state, deduped); the render thread replays them via `import_texture` at the start of the next `gfx_run`. Statue: 27 rooms, 756 jobs (638 dup), +378 cache entries, walk 0.8 ms, replay 49 ms at level load. In-game misses 1419 (off) vs 1271 (on) over 1500 frames; spikes >20 ms 6 vs 6, >33 ms 4 vs 3, i.e. no real gain. Not verified output-identical (no frame diff run; Cradle not run), so `Video.TexturePrewarm` defaults to 0. + +**Next lever.** Stop keying/decoding per palette content: upload CI index + palette as textures and look up in the shader (or cache by palette with a small LRU), so a palette change costs a 1 KB update instead of a full re-import. + +**Lead note (2026-10-01):** the pre-warm code is NOT merged (no measurable gain, never frame-diffed); it stays on branch `perf/prewarm` (`d625dc7e`) with probes `GE_GTPERF` / `GE_PWMISS` for future work. Two real levers remain: (1) CI palette re-import churn (upload index + palette separately and look up in the shader, or cache per palette with an LRU); (2) the frame-limiter skip: a gfx task that finishes slightly late makes `mainTickElapsed < MAIN_LOOP_TICK_INTERVAL` drop the next retrace, a timing interplay between the port's wall-clock `osGetCount` and the VI pacing (porting-notes D13 class); needs a design look before any change (game-loop pacing is game logic, so Rule-2 applies if `boss.c` behaviour changes). + +**Frame-limiter design look (2026-10-02, lead):** `boss.c` builds a frame on a VI retrace only if >= 387,937 counts (~8.3 ms) passed since the previous frame's start (`copy_of_osgetcount_value_1`, set in `updateFrameCounters`). A clock-beat cause (free-running 60.000 Hz port VI vs monitor vsync) was tested and refuted: a static `-level_09` view for 3700 frames with `GE_GTPERF=3` logged 9 skips, all in the first 314 frames (load/intro), none in the remaining ~57 s. So the skip only follows a long previous gfx task, i.e. it is a consequence of the CPU bursts (CI palette re-imports on Statue), not an independent pacing bug. Decision: no pacing change and no Rule-2 item; fixing the palette churn should remove most of these skips. + +## D476 — CI4 texture cache keyed on its own palette bank (palette re-import churn, D475 follow-up) + +**Cause:** D217 keys CI textures on an FNV hash of the whole 256-entry TLUT table (`rdp.palette`). A CI4 texture decodes only its 16-entry bank (`rdp.palette + palette_index * 16`, `import_texture_ci4`), so a TLUT load into any other part of the table changed the key and forced a re-decode and re-upload of an unchanged CI4 texture. +**Fix (`port/fast3d/gfx_pc.cpp`, port-only, output-identical):** for `siz == G_IM_SIZ_4b` the key's palette hash covers only that bank's 32 bytes; CI8 (reads all 256 entries) keeps the whole-table hash. Two keys can only match when the decoded texels would be identical. +**Measured** (`GE_PWMISS` probe build, Statue sweep at the maintainer's settings, misses after frame 150): CI4 189 -> 36; total 1472 -> 1101 (the CI8 count also moved, which is run-to-run variation; its keys are unchanged). +**Not fixed here:** the bulk is CI8 textures in the arena (e.g. `0x701a2920`) whose palettes the game animates (70-155 distinct palettes over the sweep, only 48 eviction re-misses in total), so no cache can absorb them. That needs a GPU palette lookup (index texture + palette texture, with filtering done in the shader) or an in-place `glTexSubImage2D` update path; see ROADMAP §4. + + +## D477 — CI8 palette re-imports measured: not a CPU-side hitch cause (D475/D476 follow-up premise refuted) + +**Question:** D475/D476 named CI8 textures with game-animated palettes as the main remaining source of the Statue/Cradle hitches and proposed a GPU palette lookup or an in-place `glTexSubImage2D` update path. +**Probe:** `GE_CI8PROF=1` on branch `probe/ci8-churn` (TEMP, never merge): per-CI8-bind counters in `import_texture` (binds, misses, distinct palette hashes, re-misses of an already-seen palette, same-frame binds with a different palette) and timers around the miss path (decode vs `upload_texture`), plus the worst CI8 frame per 300-frame window. +**Run:** Statue `-level_22`, maintainer's ini (1600x900, MSAA 8x, `TextureFilter = 2`, DD 400, FOV 135, vsync), `GE_INPUTSCRIPT="200:SLEFT;1000:SNONE;1050:SRIGHT;1800:SNONE;1850:SUP;2100:SLEFT;2400:SNONE"`, `GE_QUITFRAME=2420`, with `GE_PERFSTAT=1`; RTX 5090 dev box; rc=0, ini/eep restored. One run. +**Results:** +- CI8 miss path costs ~0.008 ms/frame on average in the busiest window (frames 300-900, ~1.9 misses/frame); the worst single frame spent 0.44 ms on CI8 imports. Non-CI8 misses are smaller still. CPU-side, this cannot make a gfx task ~15 ms late. +- The cache absorbs the animation: 1314 CI8 misses over 153 textures, only 10 re-misses of a palette already seen (the top texture `0x701a2920`, 32x48: 137 misses, 134 palettes, 3 re-misses). Misses fall to 0 after ~frame 1800. +- The same CI8 texture is bound with different palettes within one frame in most frames (`0x70155ac0`: 1902 of 2400 frames; `0x701a2920`: 8166 same-frame palette switches). An in-place single-texture update path would therefore re-upload every frame; **do not build it**. +- Spikes: 3 frames > 33 ms in 2420 (frames 3, 459, 762), each with render-thread `dl`/`run` 0.2-0.6 ms; none coincides with the worst CI8 frame (577). +**Not measured:** driver-deferred work (first-draw allocation of a new texture, `glGenerateMipmap` forced by the three-point filter on every upload) shows up neither in these CPU timers nor in the averaged GPU timer. The D475 association between the bursts and palette re-imports was temporal, not a measured cost. +**Conclusion:** GPU palette lookup / in-place update are not justified by this data. Next step if the hitches still matter: a per-frame timeline (game-thread build + limiter decision, render-thread misses/new textures/shader compiles, per-frame GPU timestamps, swap time) over >=5 runs, PresentMon for what reaches the screen, and a disable-falsifier per suspect. Low-end hardware may weigh these costs differently (a `GE_PERFSTAT` run there is still owed, ROADMAP §4). diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 4aef52d4..5af1b357 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -26,6 +26,7 @@ #include "gbiex.h" /* GE's G_TRI4 + PD extension opcodes (see header) */ #include "platform.h" +#include "portaddr.h" /* Stage B: arena windows */ #include "gfx_pc.h" #include "gfx_cc.h" @@ -717,7 +718,7 @@ void gfx_texture_cache_delete_range(const uint8_t* start, const uint8_t* end) { static bool gfx_tex_source_is_c_array(const uint8_t* addr) { const uintptr_t a = (uintptr_t)addr; if (a >= 0x10000000u && a < 0x20000000u) return false; // cart map + sidecar - if (a >= 0x70000000u && a < 0x90000000u) return false; // V1 dram + KSEG0 mirror + if (a >= PORT_DRAM_V1_BASE && a < 0x90000000u) return false; // V1 dram + KSEG0 mirror (portaddr.h) return true; // exe image: C-compiled array } @@ -1123,8 +1124,22 @@ static void import_texture(int i, int tile, bool importReplacement) { TextureCacheKey key; if (fmt == G_IM_FMT_CI) { + /* D476: a CI4 texture decodes only its 16-entry bank + * (rdp.palette + palette_index * 16, see import_texture_ci4), so key it + * on that bank's content. The whole-table D217 hash made unrelated + * TLUT loads elsewhere in the table re-import unchanged CI4 textures. + * CI8 reads all 256 entries and keeps the whole-table hash. */ + uint32_t ph = rdp.palette_hash; + if (siz == G_IM_SIZ_4b) { + ph = 2166136261u; + const uint8_t* pb = (const uint8_t*)(rdp.palette + (palette_index & 15) * 16); + for (uint32_t k = 0; k < 16 * sizeof(uint16_t); ++k) { + ph ^= pb[k]; + ph *= 16777619u; + } + } key = { orig_addr, { rdp.palette_addrs[0], rdp.palette_addrs[1] }, fmt, siz, palette_index, - loaded_texture.size_bytes, rdp.palette_hash }; // D217: key on palette content + loaded_texture.size_bytes, ph }; // D217/D476: key on (used) palette content } else { key = { orig_addr, {}, fmt, siz, palette_index, loaded_texture.size_bytes, 0u }; } @@ -1142,8 +1157,8 @@ static void import_texture(int i, int tile, bool importReplacement) { * 0x80000000 (port/src/dram.c V2) -- normalise to the V1 view the * dyn pool pointers use before the range test. */ const uint8_t* v1addr = orig_addr; - if ((uintptr_t)v1addr >= 0x80000000UL && (uintptr_t)v1addr < 0x80800000UL) { - v1addr -= 0x10000000UL; + if ((uintptr_t)v1addr >= PORT_DRAM_K0_BASE && (uintptr_t)v1addr < PORT_DRAM_K0_BASE + PORT_DRAM_SIZE) { + v1addr -= (PORT_DRAM_K0_BASE - PORT_DRAM_V1_BASE); } if (g_VtxBuffers[0] && v1addr >= g_VtxBuffers[0] && v1addr < g_VtxBuffers[2]) { uint32_t h = 2166136261u; @@ -1731,6 +1746,21 @@ static void d303_note_emit(uint32_t f, struct LoadedVertex* const* v_arr) { v_arr[0]->color.r, v_arr[0]->color.g, v_arr[0]->color.b); } +/* D474 (low-end perf): one cached gate for the per-triangle / per-vertex debug + * hooks (D75D, ZF, D303). All three are off for players; without this every + * triangle paid several calls + frame-counter reads just to return. Same + * enable semantics as each hook's own lazy init (non-empty env value). */ +static int s_tri_dbg = -1; +static inline bool tri_dbg(void) { + if (s_tri_dbg < 0) { + const char* a = getenv("GE_D75D"); + const char* b = getenv("GE_ZF"); + const char* c = getenv("GE_D303"); + s_tri_dbg = ((a && *a) || (b && *b) || (c && *c)) ? 1 : 0; + } + return s_tri_dbg != 0; +} + static void d75d_note_tri(uint32_t f, uint32_t maxidx) { d75d_init(); if (d75d_lo < 0 || f < (uint32_t)d75d_lo || f > (uint32_t)d75d_hi) return; @@ -2001,7 +2031,12 @@ static void gfx_sp_vertex(size_t n_vertices, size_t dest_index, const Vtx* verti * Remove once D75 is root-caused. */ { static int d75v_lo = -1, d75v_hi = 0; - if (d75v_lo < 0) { + /* D473: a separate init flag. d75v_lo == -1 also means "disabled", so + * the old `if (d75v_lo < 0)` re-ran getenv on EVERY G_VTX when the env + * was unset -- ~55% of render-thread CPU (D302/D250 class). */ + static bool d75v_init = false; + if (!d75v_init) { + d75v_init = true; const char* v = getenv("GE_D75V"); d75v_lo = 1; d75v_hi = 0x7fffffff; if (!v || sscanf(v, "%d-%d", &d75v_lo, &d75v_hi) != 2) @@ -2112,7 +2147,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo if (vtx1_idx < MAX_VERTICES && vtx2_idx < MAX_VERTICES && vtx3_idx < MAX_VERTICES) { uint32_t d75d_mx = vtx1_idx > vtx2_idx ? (vtx1_idx > vtx3_idx ? vtx1_idx : vtx3_idx) : (vtx2_idx > vtx3_idx ? vtx2_idx : vtx3_idx); - d75d_note_tri(videoGetFrameCount(), d75d_mx); + if (tri_dbg()) d75d_note_tri(videoGetFrameCount(), d75d_mx); } struct LoadedVertex* v1 = &rsp.loaded_vertices[vtx1_idx]; struct LoadedVertex* v2 = &rsp.loaded_vertices[vtx2_idx]; @@ -2141,7 +2176,7 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo bool any_behind_camera = (v1->w < 0) || (v2->w < 0) || (v3->w < 0); if (!any_behind_camera && (v1->clip_rej & v2->clip_rej & v3->clip_rej)) { // The whole triangle lies outside the visible area - d75d_note_rej(videoGetFrameCount(), 0); + if (tri_dbg()) d75d_note_rej(videoGetFrameCount(), 0); return; } } @@ -2167,19 +2202,19 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo switch (rsp.geometry_mode & G_CULL_BOTH) { case G_CULL_FRONT: if (cross <= 0) { - d75d_note_rej(videoGetFrameCount(), 1); + if (tri_dbg()) d75d_note_rej(videoGetFrameCount(), 1); return; } break; case G_CULL_BACK: if (cross >= 0) { - d75d_note_rej(videoGetFrameCount(), 1); + if (tri_dbg()) d75d_note_rej(videoGetFrameCount(), 1); return; } break; case G_CULL_BOTH: // Why is this even an option? - d75d_note_rej(videoGetFrameCount(), 1); + if (tri_dbg()) d75d_note_rej(videoGetFrameCount(), 1); return; } } @@ -2555,10 +2590,12 @@ static void gfx_sp_tri1(uint8_t vtx1_idx, uint8_t vtx2_idx, uint8_t vtx3_idx, bo struct GfxClipParameters clip_parameters = gfx_rapi->get_clip_parameters(); - d75d_note_emit(videoGetFrameCount(), v_arr); // survived all rejection gates -> reaches GL - d303_note_emit(videoGetFrameCount(), v_arr); // TEMP D303 - zf_note_emit(videoGetFrameCount(), v_arr); // TEMP D306/D308 - d75d_note_emit_z(videoGetFrameCount(), v_arr, (used_textures[0] || used_textures[1]) ? 1 : 0, comb->used_textures[0] ? 1 : 0); + if (tri_dbg()) { /* D474: one cached gate for the debug hooks */ + d75d_note_emit(videoGetFrameCount(), v_arr); // survived all rejection gates -> reaches GL + d303_note_emit(videoGetFrameCount(), v_arr); // TEMP D303 + zf_note_emit(videoGetFrameCount(), v_arr); // TEMP D306/D308 + d75d_note_emit_z(videoGetFrameCount(), v_arr, (used_textures[0] || used_textures[1]) ? 1 : 0, comb->used_textures[0] ? 1 : 0); + } for (int i = 0; i < 3; i++) { float z = v_arr[i]->z, w = v_arr[i]->w; if (clip_parameters.z_is_from_0_to_1) { @@ -2976,7 +3013,7 @@ static void gfx_sp_moveword(uint8_t index, uint16_t offset, uintptr_t data) { case G_MW_SEGMENT: // GE registers segment bases as OS_K0_TO_PHYSICAL(ptr); store the // live host pointer so seg_addr() resolves seg+offset correctly. - segmentPointers[(offset >> 2) & 0xff] = (data < 0x800000) ? (data + 0x80000000) : data; + segmentPointers[(offset >> 2) & 0xff] = (data < PORT_DRAM_SIZE) ? (data + PORT_DRAM_K0_BASE) : data; break; } } @@ -3038,29 +3075,44 @@ static void gfx_dp_set_tile(uint8_t fmt, uint32_t siz, uint32_t line, uint32_t t siz = G_IM_SIZ_8b; } - rdp.texture_tile[tile].palette = palette; // palette should set upper 4 bits of color index in 4b mode - rdp.texture_tile[tile].fmt = fmt; - rdp.texture_tile[tile].siz = siz; - rdp.texture_tile[tile].cms = cms; - rdp.texture_tile[tile].cmt = cmt; - rdp.texture_tile[tile].masks = masks; /* RC3 */ - rdp.texture_tile[tile].maskt = maskt; /* RC3 */ - rdp.texture_tile[tile].shifts = shifts; - rdp.texture_tile[tile].shiftt = shiftt; - rdp.texture_tile[tile].line_size_bytes = line * 8; - rdp.texture_tile[tile].tmem = tmem; + /* D474 (low-end perf): a set_tile that changes nothing must not mark the + * textures dirty -- that forces a texture-cache lookup AND a gfx_flush() + * (batch break / extra draw call) on the next triangle. Loads, TLUTs and + * G_TEXTURE still dirty the textures through their own paths. */ + auto& tt = rdp.texture_tile[tile]; + const bool same = tt.palette == palette && tt.fmt == fmt && tt.siz == siz && + tt.cms == cms && tt.cmt == cmt && tt.masks == masks && tt.maskt == maskt && + tt.shifts == shifts && tt.shiftt == shiftt && + tt.line_size_bytes == line * 8 && tt.tmem == tmem; + tt.palette = palette; // palette should set upper 4 bits of color index in 4b mode + tt.fmt = fmt; + tt.siz = siz; + tt.cms = cms; + tt.cmt = cmt; + tt.masks = masks; /* RC3 */ + tt.maskt = maskt; /* RC3 */ + tt.shifts = shifts; + tt.shiftt = shiftt; + tt.line_size_bytes = line * 8; + tt.tmem = tmem; - rdp.textures_changed[0] = true; - rdp.textures_changed[1] = true; + if (!same) { + rdp.textures_changed[0] = true; + rdp.textures_changed[1] = true; + } } static void gfx_dp_set_tile_size(uint8_t tile, uint16_t uls, uint16_t ult, uint16_t lrs, uint16_t lrt) { - rdp.texture_tile[tile].uls = uls; - rdp.texture_tile[tile].ult = ult; - rdp.texture_tile[tile].lrs = lrs; - rdp.texture_tile[tile].lrt = lrt; - rdp.texture_tile[tile].width = (lrs - uls + 4) / 4; - rdp.texture_tile[tile].height = (lrt - ult + 4) / 4; + auto& tt = rdp.texture_tile[tile]; + if (tt.uls == uls && tt.ult == ult && tt.lrs == lrs && tt.lrt == lrt) { + return; /* D474: unchanged tile size -- no re-import, no batch break */ + } + tt.uls = uls; + tt.ult = ult; + tt.lrs = lrs; + tt.lrt = lrt; + tt.width = (lrs - uls + 4) / 4; + tt.height = (lrt - ult + 4) / 4; rdp.textures_changed[0] = true; rdp.textures_changed[1] = true; } @@ -3650,8 +3702,8 @@ static inline void *seg_addr(uintptr_t w1) { // GE passes OS_K0_TO_PHYSICAL(ptr) == ptr - 0x80000000 for RAM that lives // in the reserved N64-DRAM region (port/src/dram.c); map it back. The // region is 8 MB, so any offset below 0x800000 came from there. - if (w1 < 0x800000) { - return (void *)(w1 + 0x80000000); + if (w1 < PORT_DRAM_SIZE) { + return (void *)(w1 + PORT_DRAM_K0_BASE); } // D131: a GBI DL built by game code can reference a COMPILED symbol via // osVirtualToPhysical() (a u32-returning shim), which truncates the @@ -3666,7 +3718,7 @@ static inline void *seg_addr(uintptr_t w1) { { static const uintptr_t mod_hi = ((uintptr_t)(void *)&segmentPointers[0]) & 0xffffffff00000000ULL; - if (mod_hi && w1 >= 0x40000000 && w1 < 0x70000000) { + if (mod_hi && w1 >= 0x40000000 && w1 < PORT_DRAM_V1_BASE) { /* below the arena (portaddr.h) */ return (void *)(mod_hi | w1); } } @@ -3711,7 +3763,7 @@ static void gfx_run_dl(Gfx* cmd) { break; case G_VTX: gfx_sp_vertex(C0(0, 16) / sizeof(Vtx), C0(16, 4), (const Vtx*)seg_addr(cmd->words.w1)); - d75d_note_vtx(videoGetFrameCount(), C0(0, 16) / sizeof(Vtx), C0(16, 4)); + if (tri_dbg()) d75d_note_vtx(videoGetFrameCount(), C0(0, 16) / sizeof(Vtx), C0(16, 4)); break; case G_DL: { if (C0(16, 1) == 0) { @@ -4166,6 +4218,8 @@ static int gfx_perfstat_on(void) { static double s_perf_dl = 0, s_perf_run = 0, s_perf_present = 0, s_perf_interval = 0; static double s_perf_pre = 0, s_perf_post = 0, s_perf_swap = 0; static uint64_t s_perf_frames = 0, s_perf_last_start = 0; +static double s_perf_maxiv = 0, s_perf_maxiv_dl = 0, s_perf_maxiv_run = 0; +static uint64_t s_perf_maxiv_frame = 0, s_perf_over20 = 0, s_perf_over33 = 0; extern "C" void gfx_run(Gfx* commands) { s_hud_scale = 1.0f; /* D226: never carry a HUD scale across frames */ @@ -4233,7 +4287,20 @@ extern "C" void gfx_run(Gfx* commands) { const uint64_t t2 = gfx_perf_now_ns(); s_perf_dl += (double)(perf_t1 - perf_tpre) / f; s_perf_run += (double)(t2 - perf_t0) / f; - if (s_perf_last_start) s_perf_interval += (double)(perf_t0 - s_perf_last_start) / f; + if (s_perf_last_start) { + const double iv = (double)(perf_t0 - s_perf_last_start) / f; + s_perf_interval += iv; + /* Spike stats (hitches hide in 300-frame averages): the worst + * frame's interval and its own dl/run, plus counts over 20/33 ms. */ + if (iv > s_perf_maxiv) { + s_perf_maxiv = iv; + s_perf_maxiv_dl = (double)(perf_t1 - perf_tpre) / f; + s_perf_maxiv_run = (double)(t2 - perf_t0) / f; + s_perf_maxiv_frame = s_perf_frames + 1; + } + if (iv > 20.0) s_perf_over20++; + if (iv > 33.4) s_perf_over33++; + } s_perf_last_start = perf_t0; } } @@ -4255,6 +4322,11 @@ extern "C" void gfx_end_frame(void) { (unsigned long long)s_perf_frames, s_perf_dl / n, s_perf_run / n, s_perf_present / n, s_perf_interval / n, s_perf_interval > 0 ? 1000.0 * n / s_perf_interval : 0.0, (double)s_perf_tris / n, (double)s_perf_batches / n); + fprintf(stderr, "PERFSTAT spikes: max=%.2fms at frame %llu (dl=%.2f run=%.2f) >20ms=%llu >33ms=%llu\n", + s_perf_maxiv, (unsigned long long)s_perf_maxiv_frame, s_perf_maxiv_dl, s_perf_maxiv_run, + (unsigned long long)s_perf_over20, (unsigned long long)s_perf_over33); + s_perf_maxiv = s_perf_maxiv_dl = s_perf_maxiv_run = 0; + s_perf_maxiv_frame = s_perf_over20 = s_perf_over33 = 0; s_perf_dl = s_perf_run = s_perf_present = s_perf_interval = 0; s_perf_tris = s_perf_batches = 0; } diff --git a/port/include/portaddr.h b/port/include/portaddr.h new file mode 100644 index 00000000..b2ba151d --- /dev/null +++ b/port/include/portaddr.h @@ -0,0 +1,14 @@ +#ifndef PORT_PORTADDR_H +#define PORT_PORTADDR_H + +/* Stage B step 1 (STAGEB-ABI-DESIGN, D441 follow-on): the ONE place that + * names the N64-DRAM arena windows. port/src/dram.c maps an 8 MB backing + * store at BOTH views; everything that converts or range-checks an arena + * address uses these instead of a literal, so moving the arena later (the + * high-arena test flavour, macOS/ARM port_addr) is a change in one header. + * Compile-time constants: no codegen change from the old literals. */ +#define PORT_DRAM_V1_BASE 0x70000000UL /* s32-safe "virtual" view (game RAM symbols) */ +#define PORT_DRAM_K0_BASE 0x80000000UL /* KSEG0 mirror of V1 (same pages) */ +#define PORT_DRAM_SIZE 0x00800000UL /* 8 MB, like an 8-MB-RAM N64 */ + +#endif /* PORT_PORTADDR_H */ diff --git a/port/shim/PR/os.h b/port/shim/PR/os.h index 5378e3ec..5b1dcba8 100644 --- a/port/shim/PR/os.h +++ b/port/shim/PR/os.h @@ -26,12 +26,13 @@ */ #ifndef _PORT_SHIM_OS_H_ #define _PORT_SHIM_OS_H_ +#include "portaddr.h" #if defined(PORT) # include "include/PR/os.h" # undef OS_K0_TO_PHYSICAL -# define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - 0x70000000)) +# define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - PORT_DRAM_V1_BASE)) # undef OS_PHYSICAL_TO_K0 # define OS_PHYSICAL_TO_K0(x) ((void *)(x)) diff --git a/port/src/dram.c b/port/src/dram.c index f0a5f4f4..cdadb88e 100644 --- a/port/src/dram.c +++ b/port/src/dram.c @@ -48,6 +48,7 @@ #include "platform.h" #include "system.h" +#include "portaddr.h" #if defined(PLATFORM_WINDOWS) #include @@ -57,9 +58,9 @@ #include #endif -#define DRAM_V1_BASE 0x70000000UL /* s32-safe "virtual" view */ -#define DRAM_K0_BASE 0x80000000UL /* KSEG0 mirror view */ -#define DRAM_SIZE 0x00800000UL /* 8 MB */ +#define DRAM_V1_BASE PORT_DRAM_V1_BASE /* s32-safe "virtual" view (portaddr.h) */ +#define DRAM_K0_BASE PORT_DRAM_K0_BASE /* KSEG0 mirror view (portaddr.h) */ +#define DRAM_SIZE PORT_DRAM_SIZE /* 8 MB (portaddr.h) */ void *dramReserve(void) { diff --git a/port/src/libultra.c b/port/src/libultra.c index b4ab556c..4b0075cd 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -915,7 +915,7 @@ static int s_d61opened = 0; static int dramHostAddrValid(uintptr_t addr, u32 size) { - static const uintptr_t bases[2] = { 0x70000000UL, 0x80000000UL }; + static const uintptr_t bases[2] = { PORT_DRAM_V1_BASE, PORT_DRAM_K0_BASE }; /* portaddr.h */ for (int i = 0; i < 2; i++) { if (addr >= bases[i] && addr + size <= bases[i] + 0x00800000UL) return 1; diff --git a/port/src/n64stubs.c b/port/src/n64stubs.c index dfc01bae..5ed2b01e 100644 --- a/port/src/n64stubs.c +++ b/port/src/n64stubs.c @@ -103,7 +103,7 @@ u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE] * area; the extra goes to MEMPOOL_STAGE (boss.c:218 gives STAGE * everything that isn't the fixed PERMANENT bank). Stays well clear of * animations_frame_buffer. */ - return (u8 (*)[TLB_BLOCK_SIZE])0x70700000; + return (u8 (*)[TLB_BLOCK_SIZE])(PORT_DRAM_V1_BASE + 0x700000UL); /* portaddr.h */ } /* --- K&R libc helpers (IDO provided these; MinGW's libc does not) -------- */ diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 14dd767a..241de776 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -342,79 +342,22 @@ f32 g_MpSwirlDistance; #define ALIGN64_V3(val) (((val) | 0x3f) ^ 0x3f) #ifdef PORT -/* D243 (M-145): model-lifecycle probe for the Dam-abseil "two Bonds + camera - * shake" (M-143/M-145: the gBondViewCutscene POSEND look-at reads - * field_3C4/8/C, a leaky-integrator filter of Bond's real position that is - * never reset when a cutscene warps him; M-145 ties both symptoms to a - * suspected model-remove/recreate window around the CHR_BOND_CINEMA pad - * teleports). GE_D243M=1 logs: - * D243M: CREATE/REMOVE -- every bodyModel assignment/removal (the only two - * sites in this file), with camera mode at the moment it happened; - * D243M: tick -- once per playerTick() call while the third-person model - * exists OR a cutscene camera mode (POSEND/INTRO) is active: index, model - * pointer, g_CameraMode, dword_CODE_bss_80079A18, prop->pos, - * field_488.pos, and the filter fields field_3B8 + field_3C4/8/C; - * D243M: draw -- once per on-screen render_pos submission, so a second - * Bond instance shows up as two draw lines (different idx or model - * pointer) at the same frame number. - * The frame counter increments once per playerTick() call (solo play: once - * per frame); it is NOT necessarily numerically identical to port's internal - * g_framesRendered -- cross-reference by elapsed offset, same convention as - * front.c's GE_D243 trace. D250: getenv cached. Diagnosis only; zero - * behaviour change when unset. - */ -static int d243mEnabled(void) -{ - static int s = -1; - if (s < 0) { s = getenv("GE_D243M") != NULL; } - return s; -} - -static int g_d243mFrameCounter = 0; - -/* M-154: accessors for the D243M: chrdraw census probe in objecthandler.c's - * drawjointlist (which does not include bondview.h). ProbeActive() folds in - * the scripted-camera gate (POSEND/INTRO/SWIRL/FADESWIRL) so callers need no - * camera-mode knowledge; silent during FPS play. */ -int d243mProbeActive(void) -{ - return d243mEnabled() && - ((g_CameraMode == CAMERAMODE_POSEND) || (g_CameraMode == CAMERAMODE_INTRO) || - (g_CameraMode == CAMERAMODE_SWIRL) || (g_CameraMode == CAMERAMODE_FADESWIRL)); -} - -int d243mGetFrameCounter(void) { return g_d243mFrameCounter; } - -/* D243 M-187: the scripted-camera-mode test alone, WITHOUT the GE_D243M - * getenv gate -- originally added for the always-on M-183/M-185 sanity - * clamps in model.c (playspeed/endframe), which must fire for every real - * player regardless of whether the diagnostic env var happens to be set - * (before this, both clamps were mistakenly gated on d243mProbeActive(), - * env-var AND camera-mode, so they only ever fired during a diagnostic - * capture). Renamed from d243mCutsceneActive (M-190): the same "is the - * game in a scripted/no-control camera state" test is also the missing - * gate port/src/input.c's hipDirectCompute() documented but never - * implemented -- see that function's comment and M-190 in findings.md. */ +/* D243 M-187/M-190: the scripted-camera-mode test, used by model.c's + * playspeed/endframe clamps and by port/src/input.c's hipDirectCompute() + * gate (the same "is the game in a scripted/no-control camera state" + * test; see M-190 in findings.md). */ int gameScriptedCameraActive(void) { return (g_CameraMode == CAMERAMODE_POSEND) || (g_CameraMode == CAMERAMODE_INTRO) || (g_CameraMode == CAMERAMODE_SWIRL) || (g_CameraMode == CAMERAMODE_FADESWIRL); } -/* D243 M-169: the "real signal" named as next-step (b) in findings.md's - * D243 CONSOLIDATED NEXT STEPS -- a monotonic epoch counter bumped by - * chrai.c's AI_TRYTeleportingChrToPad case (the exact decomp call, chrai.c - * ~4064-4119, that legitimately re-anchors a chr's model root joint via - * setsuboffset() for a real cutscene shot-change) right after that call. - * chr.c's GE_D243X4 (render_pos freeze) and this file's GE_D243X3 - * (field_488.pos freeze) both poll this to re-baseline their frozen - * snapshot exactly on a legitimate shot-change instead of either freezing - * forever (losing real repositioning) or guessing a position-delta - * threshold (next-step (a), not attempted). Global, not per-chr: sufficient - * for this cutscene's scope (one chr teleported at a time); revisit if a - * future cutscene needs per-chr tracking. Diagnosis/experiment plumbing - * only -- zero cost and zero behaviour change when GE_D243M is unset, since - * nothing reads the epoch unless the X3/X4 experiments are also enabled. */ +/* D243 M-169: a monotonic epoch counter bumped by chrai.c's + * AI_TRYTeleportingChrToPad case right after the decomp call that + * legitimately re-anchors a chr's model root joint via setsuboffset() for a + * real cutscene shot-change. playerTick() polls it to re-seed the camera + * look-at filter exactly on a shot-change (M-190). Global, not per-chr: + * sufficient for this cutscene's scope. */ static u32 g_d243TeleportEpoch = 0; void d243NotifyTeleport(void) { g_d243TeleportEpoch++; } u32 d243GetTeleportEpoch(void) { return g_d243TeleportEpoch; } @@ -684,20 +627,6 @@ void solo_char_load(void) self->chrflags |= CHRFLAG_INIT; setsuboffset((*pp)->bodyModel, &(*pp)->prop->pos); setsubroty(g_CurrentPlayer->bodyModel, yaw); -#ifdef PORT - if (d243mEnabled()) { - osSyncPrintf("D243M: CREATE frame=%d model=%p prop=%p pos=%.1f,%.1f,%.1f cam=%d subcam=%d " - "sizeofModel=%zu\n", - g_d243mFrameCounter, - (void *) g_CurrentPlayer->bodyModel, - (void *) g_CurrentPlayer->prop, - (double) g_CurrentPlayer->prop->pos.f[0], - (double) g_CurrentPlayer->prop->pos.f[1], - (double) g_CurrentPlayer->prop->pos.f[2], - (int) g_CameraMode, (int) dword_CODE_bss_80079A18, - sizeof(Model)); - } -#endif #ifndef VERSION_US self->headnum = head; self->bodynum = body; @@ -750,15 +679,6 @@ void solo_char_load(void) */ void bondviewRemovePlayerBody(void) { -#ifdef PORT - if (d243mEnabled()) { - osSyncPrintf("D243M: REMOVE frame=%d had_model=%d chr=%p cam=%d subcam=%d\n", - g_d243mFrameCounter, - (int)(g_CurrentPlayer->bodyModel != 0), - (void *) g_CurrentPlayer->prop->chr, - (int) g_CameraMode, (int) dword_CODE_bss_80079A18); - } -#endif if ((g_CurrentPlayer->prop->chr) && (getPlayerCount() == 1)) { chrpropCleanupForRemoval(g_CurrentPlayer->prop); @@ -1697,18 +1617,6 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos, arg6->f[1] = g_CameraLookAtBondPad->pos.f[1]; arg6->f[2] = g_CameraLookAtBondPad->pos.f[2]; -#ifdef PORT - /* D243 (M-142): sibling probe to GE_D243CAM below, for the - * look-at-pad branch of CAMERAMODE_POSEND (untested as of - * M-141/M-142 -- the pad-orbit branch below was ruled out with - * zero hits, so the Dam cutscene must be reaching this branch - * or the gBondViewCutscene branch further down instead). */ - if (GE_ENVFLAG("GE_D243CAM")) { - osSyncPrintf("D243CAM lookatpad: pos=%.2f,%.2f,%.2f pos2=%.2f,%.2f,%.2f\n", - (double) pos->f[0], (double) pos->f[1], (double) pos->f[2], - (double) pos2->f[0], (double) pos2->f[1], (double) pos2->f[2]); - } -#endif return; } @@ -1748,21 +1656,6 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos, pos2->f[2] = pos->f[2] - (cosf(gBondViewCutscene->verta) * cosf(gBondViewCutscene->theta)); } -#ifdef PORT - /* D243 (M-142): sibling probe to GE_D243CAM below, for the - * gBondViewCutscene spherical-angle branch of CAMERAMODE_POSEND - * -- the other untested candidate for the Dam abseil cutscene's - * camera path (the pad-orbit branch below was ruled out with - * zero hits in M-141/M-142). theta/verta are the spherical - * angles driving pos2's look-at offset from pos; a jump/jitter - * in either between calls would show up as the reported shake. */ - if (GE_ENVFLAG("GE_D243CAM")) { - osSyncPrintf("D243CAM cutscene: pos=%.2f,%.2f,%.2f pos2=%.2f,%.2f,%.2f theta=%.4f verta=%.4f\n", - (double) pos->f[0], (double) pos->f[1], (double) pos->f[2], - (double) pos2->f[0], (double) pos2->f[1], (double) pos2->f[2], - (double) gBondViewCutscene->theta, (double) gBondViewCutscene->verta); - } -#endif return; } @@ -1790,26 +1683,6 @@ void bondviewFrozenCameraTick(u16 buttons, u16 oldbuttons, struct coord3d *pos, pos->f[1] = setupPad->pos.f[1] + flt_CODE_bss_80079A10 + flt_CODE_bss_80079A0C; pos->f[2] = setupPad->pos.f[2] + (cosf(flt_CODE_bss_80079A00) * flt_CODE_bss_80079A08) + sinf(flt_CODE_bss_80079A00) * 0.0f; -#ifdef PORT - /* D243 (M-142): the abseil/end-of-Dam cutscene's camera is this - * orbit -- a fixed look-at (pos2, above setupPad) with the eye (pos) - * circling it at radius flt_CODE_bss_80079A08, angle accumulated by - * flt_CODE_bss_80079A04 * g_GlobalTimerDelta each tick. User-confirmed - * live (M-142): the visible "violent shake" reproduces even on a run - * where the separate D146 stale-memory burst never fires at all -- - * decouples the two symptoms M-107 had been treating as related. - * This dumps the angle delta and computed eye position every call so - * a live capture can show whether g_GlobalTimerDelta (the D155/D193 - * wall-clock sim-tick count, normally clamped to <=6 but not - * necessarily steady at 1) is jittering frame-to-frame in a way that - * would show up as this exact judder. */ - if (GE_ENVFLAG("GE_D243CAM")) { - osSyncPrintf("D243CAM: dt=%.4f angle=%.4f pos=%.2f,%.2f,%.2f pad=%d\n", - (double) g_GlobalTimerDelta, (double) flt_CODE_bss_80079A00, - (double) pos->f[0], (double) pos->f[1], (double) pos->f[2], - (int) dword_CODE_bss_80079A14); - } -#endif flt_CODE_bss_80079A00 += flt_CODE_bss_80079A04 * g_GlobalTimerDelta; @@ -10720,84 +10593,6 @@ s32 playerTick(PropRecord *prop) index = getPlayerPointerIndex(prop); chr = prop->chr; -#ifdef PORT - /* GE_D243SWIRL= -- diagnostic-only: force CAMERAMODE_SWIRL once, at the - * given tick of this per-player function (called once/frame while playing), - * to make ai_25's AI_IFCameraIsInBondSwirl poll jump into ai_17 (the Dam - * abseil cutscene) without navigating to the ledge (D243). Independent of - * GE_D243M/d243mEnabled() so it works with just this one flag set. No-op, - * zero cost, when unset. */ - { - static int d243SwirlTarget = -2; /* -2 = uncached, -1 = disabled */ - static int d243SwirlTick = 0; - if (d243SwirlTarget == -2) { - const char *e = getenv("GE_D243SWIRL"); - d243SwirlTarget = e ? atoi(e) : -1; - } - if (d243SwirlTarget >= 0) { - d243SwirlTick++; - if (d243SwirlTick == d243SwirlTarget) { - osSyncPrintf("D243SWIRL: forcing CAMERAMODE_SWIRL at tick=%d\n", d243SwirlTick); - bondviewSetCameraMode(CAMERAMODE_SWIRL); - } - } - } - if (d243mEnabled()) { - struct player *pp = g_playerPointers[index]; - int cutsceneCam = ((g_CameraMode == CAMERAMODE_POSEND) || (g_CameraMode == CAMERAMODE_INTRO)); - g_d243mFrameCounter++; - if (cutsceneCam || (pp->bodyModel != NULL)) { - /* M-154: rp = the render_pos pointer itself (a change between - * phases would mean the Model's matrix buffer is being - * reassigned/aliased). The raw pre-view translation used to be - * logged here too, but see M-154 fix 2 below -- it is captured - * by the chrdraw census at draw time instead. */ - RenderPosView *d243mrp = (pp->bodyModel != NULL) ? pp->bodyModel->render_pos : NULL; - /* M-154 fix 2 (second abseil crash, same PC/fault addr): the - * NULL/-1 guard was NOT sufficient -- render_pos is assigned by - * instcalcmatrices() during the draw pass (model.c:2453), i.e. - * AFTER this probe point, so between CREATE and the first draw - * it holds whatever the reused AnimModelSlot last had: observed - * as the 0xFF tombstone (-1) on run 1, and a different bad - * value that passed the sentinel guard on run 2 (fault addr was - * still exactly -1). Sentinel-whack-a-mole is the wrong shape - for a probe: NEVER dereference render_pos at tick time. The raw - * root translation is captured by the D243M chrdraw census in - * drawjointlist instead, where the game itself dereferences - * render_pos moments later (gSPSegment), so validity is - * guaranteed there -- and draw-time is the more accurate sample - * anyway (tick-time would read the previous frame's buffer). - * rp=%p stays: logging the pointer value costs no deref. */ - /* M-154b: propptr/chrptr -- the phase-3 A,A,B,C position cycle - * with smoothly falling y could mean pp->prop itself rotates - * among three PropRecords per frame; a changing pointer proves - * it, a constant one points at a cycling x/z source (script / - * anim table). */ - osSyncPrintf("D243M: tick frame=%d idx=%d model=%p propptr=%p chrptr=%p " - "cam=%d subcam=%d " - "prop=%.1f,%.1f,%.1f f488pos=%.1f,%.1f,%.1f " - "f3B8=%.1f,%.1f,%.1f f3C4=%.1f,%.1f,%.1f " - "rp=%p\n", - g_d243mFrameCounter, index, - (void *) pp->bodyModel, - (void *) pp->prop, (void *) pp->prop->chr, - (int) g_CameraMode, (int) dword_CODE_bss_80079A18, - (double) pp->prop->pos.f[0], - (double) pp->prop->pos.f[1], - (double) pp->prop->pos.f[2], - (double) pp->field_488.pos.f[0], - (double) pp->field_488.pos.f[1], - (double) pp->field_488.pos.f[2], - (double) pp->field_3B8.f[0], - (double) pp->field_3B8.f[1], - (double) pp->field_3B8.f[2], - (double) pp->field_3C4, - (double) pp->field_3C8, - (double) pp->field_3CC, - (void *) d243mrp); - } - } -#endif if (chr != NULL) { @@ -10823,35 +10618,9 @@ s32 playerTick(PropRecord *prop) if (prop->flags & PROPFLAG_ONSCREEN) { -#ifdef PORT - if (d243mEnabled()) { - osSyncPrintf("D243M: draw frame=%d idx=%d model=%p\n", - g_d243mFrameCounter, index, - (void *) g_playerPointers[index]->bodyModel); - } -#endif RenderPosView *rp = g_playerPointers[index]->bodyModel->render_pos; matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *) rp, (Mtxf *) mtx); #ifdef PORT - /* D243 M-167: M-162/M-163 smoothed prop->pos (chr.c) but the - * visible shake was UNCHANGED -- because field_488.pos, which - * THIS site writes and which is what the camera's look-at - * filter (M-143's field_3B8/field_3C4-8-C) actually chases, - * comes from render_pos here -- a completely separate data - * path from prop->pos, driven by the model's own animated - * skeleton (subcalcmatrices/instcalcmatrices, computed inside - * chrTick's chrUpdateAnim -> modelTickAnim/subcalcpos calls, - * upstream of and independent from prop->pos). M-163's live - * capture confirmed this directly: field_488.pos kept cycling - * every tick (228.8/229.3/79.3/-1214.7 repeating) while - * prop->pos in the same capture was smooth. This experiment - * targets the actual variable the filter reads: skip this - * write entirely while active, freezing field_488.pos at - * whatever it was the moment suppression engaged. Gated on - * GE_D243X3 (separate from GE_D243X2 so results aren't - * conflated) + the same POSEND-active gate. THIS IS A TEST, - * NOT A FIX -- revert once it reports (see docs/dev/findings.md - * D243 M-167). */ /* D243 M-170 (superseded by M-190 below): the original "fix" * unconditionally froze field_488.pos during POSEND, only * letting a write through on the tick a shot-change teleport @@ -10909,15 +10678,6 @@ s32 playerTick(PropRecord *prop) g_playerPointers[index]->field_3C8 = fy; g_playerPointers[index]->field_3CC = fz; } - if (d243mEnabled()) { - osSyncPrintf("D243M: f488write frame=%d idx=%d cam=%d reseed=%d epoch=%u f488pos=%.1f,%.1f,%.1f\n", - g_d243mFrameCounter, index, - (int) g_CameraMode, (int) (justTeleported && g_CameraMode == CAMERAMODE_POSEND), - (unsigned) epoch, - (double) g_playerPointers[index]->field_488.pos.x, - (double) g_playerPointers[index]->field_488.pos.y, - (double) g_playerPointers[index]->field_488.pos.z); - } #endif } @@ -11193,22 +10953,6 @@ lean_return_to_centre: angle *= fa->x; local90 = fa->z; frame = fa->y; - -#ifdef PORT - /* D173 M-174: name the exact firing_animation_groups[group][sub] - * entry the puppet's per-tick animation selector lands on, so - * the ~328-unit float (M-172/M-173) can be traced to a specific - * table row instead of guessed from a static read (the group/sub - * selection logic above is too branchy to trust a static guess - * -- already tried and got it wrong once this session). Fires - * whenever this table lookup runs, gated on the existing - * d243mEnabled(); zero cost when GE_D243M is unset. */ - if (d243mEnabled()) { - osSyncPrintf("D243M: firinganim frame=%d idx=%d group=%d sub=%d fa_y=%.1f fa_z=%.1f fa_x=%.2f\n", - g_d243mFrameCounter, index, (int) group, (int) sub, - (double) fa->y, (double) fa->z, (double) fa->x); - } -#endif } cur = ppointers[index]->players_cur_animation; diff --git a/src/game/chr.c b/src/game/chr.c index e6b98a26..adaae032 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -2471,11 +2471,6 @@ s32 chrTick(PropRecord *prop) #endif s32 headVisible; s32 tickamount; -#ifdef PORT - int d243x2Active = 0; /* D243 M-162/M-163, see the CHRFLAG_HIDDEN else block below */ - coord3d d243x2SavedPos = {0}; - coord3d d243x2CycledPos = {0}; -#endif renderdata = D_8002CC6C; chr = prop->chr; @@ -2485,30 +2480,6 @@ s32 chrTick(PropRecord *prop) #ifdef PORT d193aSample(chr, model); - - /* D173 M-173: does a normal (non-player-puppet) chr's prop->pos, read - * BEFORE this tick's position-update dispatch runs, differ from what - * the existing D243M: onscreen probe logs AFTER it (chr.c:~2705, at - * after_position_update:)? For the player's own third-person puppet, - * M-172 (findings.md D173) measured a fixed ~328-unit gap between - * prop->pos and the rendered root that holds across levels and - * animations. This applies the exact same before/after comparison to - * EVERY ticked chr (not just the player), gated on the existing - * d243mProbeActive() (fires during INTRO/SWIRL/POSEND, silent in normal - * FPS play) -- if ordinary guards show the same before/after gap, the - * compensation a normal chr's render is missing is universal (a - * port-layer/render-path gap); if guards show ~zero gap, the puppet's - * animation/model selection itself is what differs. Diagnosis only. */ - { - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive()) - { - osSyncPrintf("D243M: prebefore frame=%d chr=%p pos=%.1f,%.1f,%.1f\n", - d243mGetFrameCounter(), (void *) chr, - (double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2]); - } - } #endif if ((!(chr->chrflags & CHRFLAG_HIDDEN)) || (chr->chrflags & CHRFLAG_00040000)) @@ -2578,47 +2549,6 @@ s32 chrTick(PropRecord *prop) } else { -#ifdef PORT - /* D243 M-162/M-163: corrected decisive experiment. M-161 restored - * prop->pos AFTER this chrTick() call returned -- but render_pos/ - * field_488.pos are computed INSIDE this same call (subcalcmatrices, - * further down this function), from whatever prop->pos is at THAT - * point, so M-161 never actually prevented the stale value from - * being rendered -- its "no change" result was inconclusive, not a - * real falsification (see docs/dev/findings.md D243 M-162). - * - * Save pos right here, before the position-update dispatch below - * runs (chr.c:2541-2617, the ACT_PATROL/ACT_ANIM/ACT_STAND/else - * chain M-160 traced the overwrite to), and restore it at - * after_position_update -- before anything later in this tick - * (including the render-matrix computation) can see the cycled - * value. - * - * M-162 (Z-only) confirmed the mechanism -- Z stopped cycling and - * went smooth -- but the visible glitch was UNCHANGED, because this - * capture's chr (actiontype=3, different from M-160's capture) turned - * out to cycle Y instead of Z, and Y was left untouched. M-163: the - * whole vector, to see if suppressing every axis finally stops the - * visible shake. This DOES also freeze whatever legitimate motion - * exists on any axis (e.g. a real descent) -- acceptable for a - * decisive test, not for a fix. - * - * Gated on GE_D243X2 + the existing d243mProbeActive() cutscene-mode - * gate (POSEND/INTRO/SWIRL/FADESWIRL) -- g_CameraMode was verified - * POSEND for the entire abseil capture (GE_D243M data), so this is - * scoped correctly without needing a new g_CameraMode extern here. - * - * THIS IS A TEST, NOT A FIX. If it confirms the cycling stops: the - * real root cause is still open -- this is decomp-faithful game - * code, so find WHY it emits cycling values on PC before touching - * anything for real (rule #2's diagnosis-before-fix discipline). - * Revert this block once the experiment reports. */ - extern int d243mProbeActive(void); - static int s_d243x2 = -1; - if (s_d243x2 < 0) { s_d243x2 = getenv("GE_D243X2") != NULL; } - d243x2Active = s_d243x2 && d243mProbeActive(); - d243x2SavedPos = prop->pos; -#endif if (((prop->type == PROP_TYPE_VIEWER) && (g_playerPointers[getPlayerPointerIndex(prop)]->cameramode == 1)) || (chr->chrflags & CHRFLAG_CULL_USING_HITBOX)) { headSwitchVisible = 1; @@ -2732,56 +2662,6 @@ s32 chrTick(PropRecord *prop) } after_position_update: -#ifdef PORT - /* D243 M-163: apply the save/restore decided above, BEFORE anything else - * in this tick (including the D243M: onscreen log right below, so it - * reports what will actually be used downstream -- and before the - * render-matrix computation further down this function) can see the - * cycled position. Whole vector this pass (M-162 was Z-only; see the - * comment at the save site for why). */ - { - d243x2CycledPos = prop->pos; - if (d243x2Active) { prop->pos = d243x2SavedPos; } - } - - /* D243 M-159/M-160: does render_pos only refresh on ticks that pass the - * on-screen visibility gate? Log the gate's result + the live prop - * position every chrTick call while a scripted cutscene camera is - * active (reuses bondview2.c's GE_D243M accessors -- gate already folds - * in POSEND/INTRO/SWIRL/FADESWIRL, so this is silent during FPS play). - * If the freeze/teleport boundaries line up with headSwitchVisible - * flipping 0->1, this confirms the mechanism named in findings.md D243 - * M-159. M-162/M-163: actiontype= and anim= (current attached animation - * pointer) added -- M-160 flagged "which of chrTick's branches fires" - * as the open next step toward the actual root cause once the - * suppression experiment reports; x2active=/x2raw= show whether the - * suppression ran this tick and what raw position it overrode, so a - * positive result can be read straight from this log instead of by eye. - * Diagnosis only; zero behaviour change when GE_D243M/GE_D243X2 unset. */ - { - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive()) - { - /* M-166: aidx=/animframe= reuse the GE_D193A reverse-resolve - * technique (d193aAnimIndex, chr.c:67) + model->animframe1 (the - * actual current playback phase, bondtypes.h:1578) -- named per - * M-165/M-166's open question: is the visible chr's animation - * IDENTITY deterministic but its PHASE inherited from whatever - * gameplay preceded the cutscene (not guaranteed identical - * cross-platform)? A live capture with this field can now - * answer that directly instead of needing another round-trip. */ - osSyncPrintf("D243M: onscreen frame=%d chr=%p vis=%d pos=%.1f,%.1f,%.1f " - "actiontype=%d anim=%p aidx=%d animframe=%.2f x2active=%d x2raw=%.1f,%.1f,%.1f\n", - d243mGetFrameCounter(), (void *) chr, (int) headSwitchVisible, - (double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2], - (int) chr->actiontype, (void *) model->anim, - d193aAnimIndex((void *) model->anim), (double) model->animframe1, - (int) d243x2Active, - (double) d243x2CycledPos.f[0], (double) d243x2CycledPos.f[1], (double) d243x2CycledPos.f[2]); - } - } -#endif if (((chr->actiontype != ACT_STAND) || (model->anim2 != NULL)) || (prop->type == PROP_TYPE_VIEWER)) { chr->hidden |= CHRHIDDEN_BACKGROUND_AI; @@ -2856,86 +2736,6 @@ after_position_update: subcalcmatrices(&renderdata, model); -#ifdef PORT - /* D243 M-168 (rewritten after two live crashes -- see findings.md - * D243 M-168 for the full postmortem): same technique as M-167 - * (field_488.pos freeze), aimed at the actual draw-path consumer of - * the cycling skeleton state -- render_pos itself. M-167 only froze - * the camera's copy (field_488.pos) and fixed the shake, but the - * user still saw Bond's own rendered body glitch/duplicate -- - * expected, since the draw call reads render_pos directly - * (objecthandler.c's drawjointlist, via gSPSegment), a path M-167 - * never touched. - * - * The first two attempts tried to SKIP this subcalcmatrices() call - * entirely once a model "looked already validated" -- both crashed, - * because a model POINTER can be reused by the allocator for a - * genuinely new logical model instance (a new CREATE at the same - * freed address), and there is no way to tell "is this the same - * instance" from the pointer value or from render_pos's own content - * (M-154 already learned the render_pos sentinel-guessing lesson - * once; this is the same trap from a different angle). Skipping the - * call left a brand-new instance's render_pos at its fresh, - * uninitialized -1 tombstone, and sub_GAME_7F06C768() (model.c:459, - * called moments later at chr.c:2822 to compute zDepth) dereferenced - * it and crashed at fault address -1. - * - * This version NEVER skips the call -- subcalcmatrices() always - * runs above, unconditionally, so render_pos is always left valid. - * The freeze is purely a CONTENTS overwrite on top of that - * guaranteed-valid buffer: cache the buffer for a couple of ticks - * after the gate first engages (letting the pose settle from - * whatever it was mid-transition), then hold it steady by copying - * the cached contents back over each subsequent tick's freshly - * (and validly) computed buffer. Gated on GE_D243X4 + - * d243mProbeActive(); the active/inactive edge resets the warm-up - * counter so a later, different model under the same gate re-warms - * instead of inheriting a stale snapshot. Capped at 128 matrices - * (generous; GE character models are well under this). THIS IS A - * TEST, NOT A FIX -- revert once it reports. */ - { - extern int d243mProbeActive(void); - extern u32 d243GetTeleportEpoch(void); - static int s_d243x4 = -1; - static int s_d243x4WasActive = 0; - static int s_d243x4WarmTicks = 0; - static RenderPosView s_d243x4Snap[128]; - static s16 s_d243x4Count = 0; - static u32 s_d243x4LastEpoch = 0; - if (s_d243x4 < 0) { s_d243x4 = getenv("GE_D243X4") != NULL; } - { - int active = s_d243x4 && d243mProbeActive(); - u32 epoch = d243GetTeleportEpoch(); - if (active && !s_d243x4WasActive) { s_d243x4WarmTicks = 0; s_d243x4LastEpoch = epoch; } - /* M-169: a legitimate shot-change teleport fired since the - * last snapshot -- re-warm so the freeze re-baselines to the - * post-teleport pose instead of holding the pre-teleport - * one (which produced the "camera looks the wrong way for - * later shots" incompleteness noted in the M-168 postmortem). */ - if (active && (epoch != s_d243x4LastEpoch)) { s_d243x4WarmTicks = 0; s_d243x4LastEpoch = epoch; } - s_d243x4WasActive = active; - if (active && (model->render_pos != NULL) && (model->obj != NULL)) - { - s16 n = model->obj->numMatrices; - if (n > 0 && n <= 128) - { - s32 i; - if (s_d243x4WarmTicks < 2) - { - for (i = 0; i < n; i++) { s_d243x4Snap[i] = model->render_pos[i]; } - s_d243x4Count = n; - s_d243x4WarmTicks++; - } - else if (n == s_d243x4Count) - { - for (i = 0; i < n; i++) { model->render_pos[i] = s_d243x4Snap[i]; } - } - } - } - } - } -#endif - g_ModelJointPositionedFunc = NULL; modelSetDistanceScale(1.0f); diff --git a/src/game/chraction.c b/src/game/chraction.c index 7cbace76..d1e84587 100644 --- a/src/game/chraction.c +++ b/src/game/chraction.c @@ -608,30 +608,6 @@ void chrlvPerformAnimationForActor(ChrRecord *self, s32 animID, s32 startframe, chrStopFiring(self); -#ifdef PORT - /* D243 M-187: log animation-restart entry state before modelSetAnimation() - * touches anything, to test whether unkb0/unkb4/unkac/animrate (which - * modelSetAnimation2, model.c:2720, never resets on a scripted restart -- - * only modelSetAnimPlaySpeed touches them) are already stale/nonzero at - * this moment -- the open question from findings.md's D243 entry (M-178 - * onward). Env-gated via bondview2.c's d243mProbeActive() (folds in the - * POSEND/INTRO/SWIRL/FADESWIRL camera-mode gate already used by the rest - * of the D243M probe family); zero cost/behaviour change when unset. */ - { - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive() && self->model != NULL) - { - osSyncPrintf("D243M: animrestart frame=%d chr=%d animID=%d startframe=%d " - "unkb0=%.3f unkb4=%.3f unkac=%.3f animrate=%.3f playspeed=%.3f\n", - d243mGetFrameCounter(), self->chrnum, (int) animID, (int) startframe, - (double) self->model->unkb0, (double) self->model->unkb4, - (double) self->model->unkac, (double) self->model->animrate, - (double) self->model->playspeed); - } - } -#endif - modelSetAnimation(self->model, (void *)animation_table_ptrs1[animID], (bitfield & ANIM_MIRROR) != 0, startframef, phi_f0, (f32)interpol_time60); if (endframe >= 0) diff --git a/src/game/chrai.c b/src/game/chrai.c index a4e982ff..0faf0a98 100644 --- a/src/game/chrai.c +++ b/src/game/chrai.c @@ -1130,44 +1130,6 @@ void ai(PropDefHeaderRecord *Entityp, PROP_TYPE EntityType) endframe = -1; } -#ifdef PORT - /* D243 M-170: which chr does a scripted guard_play_animation - * actually land on during the Dam abseil (anim_id 0xb100)? - * M-145 (static-only) claimed this targets an empty, - * model-less "false GUARD" driver chr -- but that theory's - * own stated binding rule (ailist ID >= 0x1000) doesn't - * apply to ai_17's registered ID (0x412), and the user's - * live memory of the real N64 cutscene has Bond visibly - * jumping/falling with the camera tracking him, which is - * hard to reconcile with "no model to animate." One-shot - * empirical check: log ChrEntityp's chrnum + whether it - * has a model + whether it IS the visible player chr, - * every time this specific anim_id fires. Env-gated - * (GE_D243M, already cached elsewhere), essentially free - * when unset. Diagnosis only. */ - if (GE_ENVFLAG("GE_D243M")) - { - /* M-170 correction: the first version of this probe - * filtered on anim_id==0xb100 and got ZERO hits - * across a full Dam-abseil capture that otherwise - * clearly exercised ai_17 (3 successful teleports, - * GE_D243X3/X4's epoch counter went 0->3). Rather - * than guess why, log every AI_PlayAnimation call - * unfiltered so the next capture shows what anim_id - * values (if any) actually arrive at this case - * during the cutscene -- confirms/refutes whether - * the filter itself was wrong (byte-order, wrong - * literal) or the case is never reached at all for - * this cutscene's actor. */ - osSyncPrintf("D243M: playanim anim=0x%x chr=%p chrnum=%d model=%p " - "isPlayerChr=%d\n", - (unsigned) anim_id, (void *) ChrEntityp, - ChrEntityp ? (int) ChrEntityp->chrnum : -1, - ChrEntityp ? (void *) ChrEntityp->model : NULL, - (int) (ChrEntityp && g_CurrentPlayer && g_CurrentPlayer->prop - && (void *) ChrEntityp == (void *) g_CurrentPlayer->prop->chr)); - } -#endif if (ChrEntityp) { check_if_able_to_then_perform_animation(ChrEntityp, anim_id, startframe, endframe, ai->BITFIELD, ai->INTERPOL_TIME60); diff --git a/src/game/front.c b/src/game/front.c index e4d42254..14353cef 100644 --- a/src/game/front.c +++ b/src/game/front.c @@ -8686,81 +8686,10 @@ MENU get_currentmenu(void) } -#ifdef PORT -/* D243: fast3d's D146 catch-all ("unknown GBI opcode ... ending DL") fires a - * freed/uninitialized-memory-pattern read burst ~1600-3900 frames *after* - * bossReturnTitleStage()/the Dam abseil cutscene's camera_switch calls, not - * during them (M-109 re-analysis) -- i.e. tens of seconds into whatever - * front-end screen(s) the mission-complete -> briefing/file-select chain - * passes through. This probe does not hook bossReturnTitleStage() directly - * (out of this investigation's file scope, boss.c) -- instead it logs every - * MENU_* state transition this state machine (menu_init(), called once per - * front-end tick from lv.c) makes, tagged with a local frame counter, so a - * GE_D146 burst's frame number can be cross-referenced against whichever - * MENU_* was active at that point. NOTE: this counter is local to front.c's - * per-tick calls (menu_init() runs once per LEVELID_TITLE tick) -- it is not - * necessarily numerically identical to port/src/libultra.c's internal - * g_framesRendered (that counter is `static`, no accessor exists, and it is - * out of this investigation's file scope to add one) -- cross-reference by - * elapsed count/offset from bossReturnTitleStage's own GE_D160 trace line, - * not by assuming exact frame-number equality between the two logs. */ -static int g_d243FrameCounter = 0; - -static const char *d243MenuName(MENU m) -{ - switch (m) { - case MENU_INVALID: return "MENU_INVALID"; - case MENU_LEGAL_SCREEN: return "MENU_LEGAL_SCREEN"; - case MENU_SWITCH_SCREENS: return "MENU_SWITCH_SCREENS"; - case MENU_NINTENDO_LOGO: return "MENU_NINTENDO_LOGO"; - case MENU_RAREWARE_LOGO: return "MENU_RAREWARE_LOGO"; - case MENU_EYE_INTRO: return "MENU_EYE_INTRO"; - case MENU_GOLDENEYE_LOGO: return "MENU_GOLDENEYE_LOGO"; - case MENU_FILE_SELECT: return "MENU_FILE_SELECT"; - case MENU_MODE_SELECT: return "MENU_MODE_SELECT"; - case MENU_MISSION_SELECT: return "MENU_MISSION_SELECT"; - case MENU_DIFFICULTY: return "MENU_DIFFICULTY"; - case MENU_007_OPTIONS: return "MENU_007_OPTIONS"; - case MENU_BRIEFING: return "MENU_BRIEFING"; - case MENU_RUN_STAGE: return "MENU_RUN_STAGE"; - case MENU_MISSION_FAILED: return "MENU_MISSION_FAILED"; - case MENU_MISSION_COMPLETE: return "MENU_MISSION_COMPLETE"; - case MENU_MP_OPTIONS: return "MENU_MP_OPTIONS"; - case MENU_MP_SCENARIO_SELECT: return "MENU_MP_SCENARIO_SELECT"; - case MENU_MP_CHAR_SELECT: return "MENU_MP_CHAR_SELECT"; - case MENU_MP_TEAMS: return "MENU_MP_TEAMS"; - case MENU_MP_HANDICAP: return "MENU_MP_HANDICAP"; - case MENU_MP_CONTROL_STYLE: return "MENU_MP_CONTROL_STYLE"; - case MENU_MP_STAGE_SELECT: return "MENU_MP_STAGE_SELECT"; - case MENU_CHEAT: return "MENU_CHEAT"; - case MENU_NO_CONTROLLERS: return "MENU_NO_CONTROLLERS"; - case MENU_DISPLAY_CAST: return "MENU_DISPLAY_CAST"; - case MENU_SPECTRUM_EMU: return "MENU_SPECTRUM_EMU"; -#ifdef PORT - case MENU_PC_OPTIONS: return "MENU_PC_OPTIONS"; -#endif - default: return "MENU_"; - } -} -#endif - - void menu_init(void) { s32 var_v0; -#ifdef PORT - /* D243: one call per front-end tick (see g_d243FrameCounter comment - * above for how this correlates against the D146/GE_D160 logs). - * D250: menu_init() runs ~once per frame during front-end states -- - * cache the getenv() rather than re-checking it every call. */ - static int s_d243 = -1; - if (s_d243 < 0) s_d243 = getenv("GE_D243") != NULL; - if (s_d243) { - g_d243FrameCounter++; - } -#endif - if (current_menu == MENU_SWITCH_SCREENS) { if (spectrum_related_flag != 0) @@ -8853,12 +8782,6 @@ void menu_init(void) { current_menu = MENU_SWITCH_SCREENS; reset_menutimer(); -#ifdef PORT - if (s_d243) { - fprintf(stderr, "D243: frame=%d -> MENU_SWITCH_SCREENS (pending=%s)\n", - g_d243FrameCounter, d243MenuName(menu_update)); - } -#endif } } @@ -8867,13 +8790,6 @@ void menu_init(void) current_menu = maybe_prev_menu; maybe_prev_menu = MENU_INVALID; -#ifdef PORT - if (s_d243) { - fprintf(stderr, "D243: frame=%d -> %s\n", - g_d243FrameCounter, d243MenuName(current_menu)); - } -#endif - switch(current_menu) { case MENU_LEGAL_SCREEN: init_menu00_legalscreen(); break; case MENU_NINTENDO_LOGO: init_menu01_nintendo(); break; diff --git a/src/game/model.c b/src/game/model.c index 552bbb80..3fd8d76f 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -2876,24 +2876,6 @@ void modelSetAnimationWithMerge(Model *model, ModelAnimation *modelAnimation, s3 void modelSetAnimation(Model *model, ModelAnimation *modelAnimation, s32 flip, f32 startframe, f32 speed, f32 merge) { -#ifdef PORT - /* D173 M-175: identify the actual caller that sets the intro puppet's - * first animation (M-174's firing_animation_groups probe got zero hits, - * ruling that path out -- rather than keep guessing which function it - * is, catch it at the one place all callers funnel through and resolve - * the caller with addr2line against the built exe afterward. Filters - * on g_CurrentPlayer->bodyModel specifically so normal chr/guard - * animation-sets (which fire constantly) don't spam the log. */ - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - extern struct player *g_CurrentPlayer; - if (d243mProbeActive() && g_CurrentPlayer && (model == g_CurrentPlayer->bodyModel)) - { - osSyncPrintf("D243M: setanim_caller frame=%d model=%p startframe=%.1f speed=%.2f merge=%.1f retaddr=%p\n", - d243mGetFrameCounter(), (void *) model, (double) startframe, - (double) speed, (double) merge, __builtin_return_address(0)); - } -#endif modelCopyAnimForMerge(model, merge); modelSetAnimation2(model, modelAnimation, flip, startframe, speed, merge); } @@ -2981,19 +2963,6 @@ void modelSetAnimSpeed(Model *model, f32 anim_speed, f32 startframe) { (double)model->oldspeed, (double)model->newspeed, (double)model->timespeed, (void *)model); } - /* D243 M-180: log all modelSetAnimSpeed calls during scripted camera modes - * to identify if unusual speed values are being set during cutscenes. - * Env-gated on GE_D243M (already cached elsewhere). */ - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive() && model != NULL) { - osSyncPrintf("D243M: setanimspeed frame=%d model=%p speed=%.3f startframe=%.1f " - "timespeed=%.1f oldspeed=%.3f newspeed=%.3f elapsespeed=%.1f\n", - d243mGetFrameCounter(), (void *) model, (double) anim_speed, - (double) startframe, (double) model->timespeed, - (double) model->speed, (double) model->newspeed, - (double) model->elapsespeed); - } #endif if (startframe > 0.0f) { @@ -3037,20 +3006,6 @@ void sub_GAME_7F06FE90(Model *model, f32 arg1, f32 arg2) } void modelSetAnimPlaySpeed(Model *model, f32 animation_rate, f32 startframe) { -#ifdef PORT - /* D243 M-181: log all modelSetAnimPlaySpeed calls during scripted camera - * modes to identify if unusual playspeed values are being set during - * cutscenes. Env-gated on GE_D243M (already cached elsewhere). */ - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive() && model != NULL) { - osSyncPrintf("D243M: setanimplayspeed frame=%d model=%p rate=%.3f startframe=%.1f " - "unkb0=%.1f playspeed=%.3f animrate=%.3f\n", - d243mGetFrameCounter(), (void *) model, (double) animation_rate, - (double) startframe, (double) model->unkb0, - (double) model->playspeed, (double) model->animrate); - } -#endif if (startframe > 0.0f) { model->unkb0 = startframe; model->animrate = animation_rate; @@ -3627,28 +3582,6 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff) f32 frame2; f32 animlast; -#ifdef PORT - /* D243 M-182: log playspeed at the start of every tick during scripted - * camera modes to identify when and how it changes. Env-gated on - * GE_D243M (already cached elsewhere). */ - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive() && model != NULL) - { - /* D243 M-187: numticks added -- this is the D193 multi-tick - * catch-up burst size for this call; >1 here at the moment of a - * scripted restart is the leading PC-specific candidate for why - * unkb0/unkb4 could be stale in a way N64's single-tick-per-frame - * execution would never produce (see findings.md D243). */ - osSyncPrintf("D243M: tickstart frame=%d model=%p numticks=%d playspeed=%.3f animrate=%.3f " - "unkb0=%.1f unkb4=%.1f unkac=%.3f endframe=%.1f\n", - d243mGetFrameCounter(), (void *) model, (int) numticks, - (double) model->playspeed, (double) model->animrate, - (double) model->unkb0, (double) model->unkb4, - (double) model->unkac, (double) model->endframe); - } -#endif - frame = model->animframe1; frame2 = model->animframe2; @@ -3739,11 +3672,10 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff) * stale-nonzero divides by it using a mismatched unkac/animrate * pair from the previous animation, producing huge playspeed * spikes (~388-410, vs. a legitimate max of ~2.0). - * M-187: was mistakenly gated on d243mProbeActive() (requires - * GE_D243M=1), so it never fired for a real player -- only during - * a diagnostic capture. Fixed to gate on the camera-mode test + * M-187: was mistakenly gated on a diagnostic env var, so it + * never fired for a real player. Fixed to gate on the camera-mode test * alone (gameScriptedCameraActive()) so it's actually active by - * default. Logging stays separately gated on d243mProbeActive(). + * default. * M-189 (2026-09-18) found and fixed the ACTUAL root cause (a * stale hardcoded 32-bit sizeof(Model) literal in bondview2.c's * model-carving buffer, user-verified live): this clamp no longer @@ -3756,56 +3688,15 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff) * re-verified clean without it (findings.md D243 / M-192). * Threshold: 10.0 is way above any legitimate playspeed (normal * is ~1.0, max observed in gameplay is ~2.0). */ - extern int d243mProbeActive(void); extern int gameScriptedCameraActive(void); if (gameScriptedCameraActive() && playspeed > 10.0f) { - if (d243mProbeActive()) - { - osSyncPrintf("D243M: clamp playspeed %.3f → 1.0\n", (double) playspeed); - } playspeed = 1.0f; } #endif frame += playspeed * speed; -#ifdef PORT - /* M-184: log frame progression after advancement */ - extern int d243mProbeActive(void); - if (d243mProbeActive() && model != NULL) - { - osSyncPrintf("D243M: postadv frame=%.2f endframe=%.1f\n", - (double) frame, (double) endframe); - } -#endif - -#ifdef PORT - /* D243 M-179: log large per-tick animation advancements to identify - * whether rapid cutscene anim advancement is due to playspeed, speed, - * or numticks. Fires when a single tick advances >5 frames (normal - * is ~0.5-3). Env-gated on GE_D243M (already cached elsewhere). */ - { - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive()) - { - f32 advancement = playspeed * speed; - if (advancement > 5.0f) - { - osSyncPrintf("D243M: biganim frame=%d model=%p adv=%.2f " - "playspeed=%.2f speed=%.2f numticks=%d " - "unkb0=%.1f unk88=%.1f timespeed=%.1f\n", - d243mGetFrameCounter(), (void *) model, - (double) advancement, (double) playspeed, - (double) speed, numticks, - (double) model->unkb0, (double) model->unk88, - (double) model->timespeed); - } - } - } -#endif - if (model->anim2 != NULL) { if (model->unk7c > 0.0f) @@ -3850,18 +3741,12 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff) * release review): approved for retention by the user for * v0.3.0; candidate for removal post-release (findings.md D243 / * M-192). - * M-187: was mistakenly gated on d243mProbeActive() (requires - * GE_D243M=1), so it never fired for a real player. Fixed to - * gate on the camera-mode test alone (gameScriptedCameraActive()). - * Logging stays separately gated on d243mProbeActive(). */ - extern int d243mProbeActive(void); + * M-187: was mistakenly gated on a diagnostic env var, so it + * never fired for a real player. Fixed to + * gate on the camera-mode test alone (gameScriptedCameraActive()). */ extern int gameScriptedCameraActive(void); if (gameScriptedCameraActive() && (endframe < 0.0f || endframe > 1000.0f)) { - if (d243mProbeActive()) - { - osSyncPrintf("D243M: clamp endframe %.1f → 100.0\n", (double) endframe); - } endframe = 100.0f; } #endif diff --git a/src/game/objecthandler.c b/src/game/objecthandler.c index 7834e422..99def1f3 100644 --- a/src/game/objecthandler.c +++ b/src/game/objecthandler.c @@ -887,29 +887,6 @@ void drawjointlist(ModelRenderData *data, ModelHitEntry *entry) Gfx *gdl; s32 opcode; -#ifdef PORT - /* D243 M-154: per-frame model-draw census. One line per drawjointlist - * call (i.e. per CHR render submission) while a scripted camera mode is - * active, logging the Model* pointer and render_pos[0]'s raw translation - * BEFORE the view transform. A second Bond instance shows up as extra - * lines with distinct model pointers at the same frame; a shared/aliased - * matrix buffer shows up as one pointer whose raw position cycles. - * Diagnosis only; silent when GE_D243M is unset or during FPS play. - * M-154 fix 2: render_pos is also rejected at the -1 tombstone value - * (belt-and-braces -- at draw time instcalcmatrices has already run, - * so this should never trigger; if it did, the game's own gSPSegment - * moments later would crash first anyway). */ - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive() && (entry != NULL) && (entry->model != NULL) - && (entry->model->render_pos != NULL) && (entry->model->render_pos != (RenderPosView *) -1)) { - Mtxf *d243mraw = (Mtxf *) entry->model->render_pos; - osSyncPrintf("D243M: chrdraw frame=%d model=%p raw=%.1f,%.1f,%.1f\n", - d243mGetFrameCounter(), (void *) entry->model, - (double) d243mraw->m[3][0], (double) d243mraw->m[3][1], (double) d243mraw->m[3][2]); - } -#endif - if (data->gdl == NULL) { osSyncPrintf("drawjointlist: no gfxlist!\n"); @@ -1037,20 +1014,6 @@ void drawjointlist(ModelRenderData *data, ModelHitEntry *entry) Gfx *gdl; s32 opcode; -#ifdef PORT - /* D243 M-154: per-frame model-draw census (EU variant) -- see the US/JP - * copy above for the full rationale (incl. the M-154 fix 2 -1 guard). */ - extern int d243mProbeActive(void); - extern int d243mGetFrameCounter(void); - if (d243mProbeActive() && (entry != NULL) && (entry->model != NULL) - && (entry->model->render_pos != NULL) && (entry->model->render_pos != (RenderPosView *) -1)) { - Mtxf *d243mraw = (Mtxf *) entry->model->render_pos; - osSyncPrintf("D243M: chrdraw frame=%d model=%p raw=%.1f,%.1f,%.1f\n", - d243mGetFrameCounter(), (void *) entry->model, - (double) d243mraw->m[3][0], (double) d243mraw->m[3][1], (double) d243mraw->m[3][2]); - } -#endif - while (entry != NULL) { root = entry->rootnode;