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
This commit is contained in:
James Dansereau committed 2026-10-06 18:35:41 -04:00
1 parent b735190fd9
commit 88bcef32a3
17 files changed
+229 -835

No files matched your search

+6 -5
View File
@@ -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 `<errno.h>` 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. |
+8 -8
View File
@@ -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=<chrnum>` 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=<chrnum>` 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=<n>` 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=<stage id>` | `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=<chr>,list=<hexid>,off=<n>`, `bit=<hex>`, `trace=<chr>`, `rename=<chr>,<num>`, `seed=<hi>,<lo>` (D320); `explode=<type>,<dist>` (explosionCreate ahead of Bond) and `cheat=gl|rl|inv|<id>` (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<n>` `MDY<n>` `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** |
+7 -2
View File
@@ -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…"
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
58 D178
59 D175
60 D176
61 D177
62 D172
63 D174
64 D183
90 D113-D116
91 D117
92 D118
93 D150
94 D260-D265
95 D266-D269
96 D270
332 D470
333 D471
334 D472
335 D473
336 D474
337 D475
338 D476
339 D477
340 D450
+59 -5
View File
@@ -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).
+111 -39
View File
@@ -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;
}
+14
View File
@@ -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 */
+2 -1
View File
@@ -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))
+4 -3
View File
@@ -48,6 +48,7 @@
#include "platform.h"
#include "system.h"
#include "portaddr.h"
#if defined(PLATFORM_WINDOWS)
#include <windows.h>
@@ -57,9 +58,9 @@
#include <unistd.h>
#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)
{
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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) -------- */
+10 -266
View File
@@ -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=<tick> -- 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;
-200
View File
@@ -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);
-24
View File
@@ -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)
-38
View File
@@ -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);
-84
View File
@@ -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_<unknown>";
}
}
#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;
+6 -121
View File
@@ -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
-37
View File
@@ -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;