diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9a6c2717..f6f6f984 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -128,18 +128,16 @@ below). | Item | Status | Refs | Notes | |---|---|---|---| -| Auto-fire gunshot sound slightly differs from 1964/GEPD | open | D433, #114 | Cadence now matches the N64 rules exactly. The residual is timbre (resampler / release envelope suspected). Needs a precise by-ear description. | -| SFX voice cap and long-session audio decay | partial | D322, #87 | The Windows periodic garble is fixed. Still open: is the 8-voice cap faithful, and the progressive decay that #87 reported (not reproduced here). The owed GitHub housekeeping is in `docs/dev/notes/ISSUE-87-FOLLOWUP.md` (local). | +| Auto-fire gunshot sound slightly differs from 1964/GEPD | decision | D433, #114 | Cadence matches the N64 rules exactly. The residual is subtle differences in the click/empty-magazine sounds between shots during continuous AK47 fire. **Maintainer-owned (2026-09-30):** possibly already fixed; it needs a recorded A/B against 1964/GEPD, which isn't suited to agents. Done together with the WrapFix check below. | | Auto-fire gate at unstable 40–55 fps | partial | D427 | Exact at a steady 60 or 30 fps. At fluctuating rates it can flip between 1× and 2×, because `field_88C` is never rescaled. | | Third-person Bond offset residual | partial | D173, D292 | Mostly fixed. A small drift remains about 1 time in 10. | -| Statue corner flicker at max FOV | partial | D294 | Room-pool residual after the black-ground fix. | +| Statue corner flicker at max FOV | verify | D294 | Room-pool cap raised 2.0 -> 2.5 (2026-09-30; Statue needs about 2.17x). It is unproven: the exhaustion log never fired at spawn. **Owed:** strafe fast past the Statue corners at max FOV and max draw distance. | | Crash on save slots 1/3/4, plus a host BSOD (fltmgr.sys) | open | #94 | Needs a repro. Two maintainer retest requests (v0.3.0, v0.4.0) got no reply. Likely D299/D301 (save-slot crash, fixed) and D344 (BSOD on exit, fixed). Close as stale if there's still no reply, and reproduce locally rather than asking the reporter again. | -| SkipIntro skips the post-mission failure dossier | open | D408, D216 | Only affects the experimental `Game.SkipIntro` option. The audio-break half of D216 is also unrooted. | +| SkipIntro skips the post-mission failure dossier | verify | D408, D216 | Fixed 2026-09-30: the hook is gated to first boot. **Owed:** fail or abort a mission with SkipIntro on and check the dossier shows; also listen for whether D216's audio break is gone. | | 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 | parked | D323 | Distant textures use driver mips instead of Rare's hand-authored chain (heaviest on Statue/Aztec). Parked as a fidelity enhancement. | -| 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. | +| 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. | -| Controller preset parity (Xbox "Jinx" 1.1, 1.2/1.3 selector) | partial | D394 | The gadget category and pad-initiated binding capture are still open. | ## 3. Verification debt (fixed; a live check is owed) @@ -199,7 +197,7 @@ Background research lives in the maintainer's local notes (gitignored): | Restart button in the settings menus | open | — | QoL (2026-09-30): next to Quit to desktop, a "Restart game" action so that restart-required changes (MSAA, etc.) apply in one click. Relaunch the same exe with the same args after an orderly quit (the D344 path, since hard exits have BSOD'd). Offer it wherever a changed row is flagged "restart". | | Front-end PC Options: Previous/Next paging for mouse users | open | — | QoL (2026-09-30): add clickable Previous/Next (page) controls to the main-menu settings screen, so mouse users don't have to rely on wheel scrolling. Today the wheel scroll competes with mouse hover: any small mouse nudge re-selects a row and snaps the scroll back. Fix that interaction too. Hover must not reset the scroll position, and it should only change the selection once the pointer moves onto a different row. Owner: the D357/D443 track (same file, `port/src/frontoptions.c`). | | Outside PRs #107–#109, #122–#124 | open | #122 hot-plugged pads never reopen, #123 persistent crosshair (`gunfire.c`), #124 compact health/armour bars (`bondview2.c`); #107/#108/#109 (italoarruda) | **#120 and #121 merged locally into `release/v0.4.1` on 2026-09-30** (dolent's original commits kept for credit; relabelled D445/D446; the review is in `docs/dev/notes/PR-REVIEW-120-121.md`). Close both on GitHub when the branch is pushed. **Owed live:** kneeling rifle guards (#120), and the 2P watch-menu text (#121, on the MP branch). **Remaining order (proposed; approval owed):** #122 (then rebase split-screen on it; #122/#109/MP all touch `input.c`) → #107 (fold remaining sites into the D441 census, then close) → #108 (ask for a range check; belongs with `macos`) → #109 (after split-screen settles `input.c`) → #123/#124. **Policy:** any PR that adds a *feature* (#109, #123, #124, …) gets a **design review before any work**: compare against other PC ports and remasters (PD port, Nightdive, GE+/XBLA), then approve, or send suggestions back and defer until the submitter revises. **Their finding labels clash with ours:** relabel on merge. `src/game` touches (#120/#123/#124) each need a Rule-2 or ABI-exception check. | -| Gamepad button rebinding UI | open | PR #109, D394 | Keyboard/mouse rebinding has shipped. Review #109 under the outside-PR policy. | +| Gamepad rebinding + modern controller presets | open | PR #109, D394 | QoL (maintainer, 2026-09-30), one item: rebindable controller buttons, plus presets taken from the modern Xbox Series X release's control scheme (the D394 "Jinx" 1.1 layout, with a 1.2/1.3 selector). The remaining D394 gap is the **gadget category**: the camera, the GoldenEye key copier, bombs, and other devices planted on things. Also pad-initiated binding capture. Keyboard/mouse rebinding has shipped. Review #109 under the feature design-review policy first. | | Per-pad tuning (device index, hot-plug seats) | open | — | Needed for split-screen anyway. | | Full-gamepad menu navigation audit, Xbox/PS button glyphs | open | — | Menus are only partly audited for pad-only use. There are no glyphs yet. | | Mute and auto-pause on focus loss; PNG screenshots to `screenshots/` | parked | D231, PR #56 (closed unmerged), `docs/dev/notes/parked/0001-QoL-*.patch` (local) | Deprioritised by the maintainer on 2026-09-25. F12 today is the dev PPM dump. | diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 1e183d72..4fc27ddb 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -29,13 +29,13 @@ Two classes: | `GE_FAKE_DECK` | `port/src/video.c:490` (`videoDetectSteamDeck`) | D283: `=1` forces the Steam Deck path on any host (Windows/Linux desktop) so the once-per-ini Deck preset logic (`Video.DeckPresetApplied`, applied only while `Video.Fullscreen=0` and `Window.Maximized=0`) can be tested; `=0` forces it off on a real Deck (skips DMI + `STEAMOS`). Unset = real detection (DMI `Valve` + `Jupiter`/`Galileo`, then `STEAMOS`). **Writes the ini** (preset + flag) — use a throwaway data dir. | **live** (test hook) | | `GE_OPTIONSOVERLAY` | `port/src/optionsoverlay.c:231` | Force-enable the F10 port-layer options overlay (D184 / PR #10) regardless of ini. `atoi != 0`. | **live** | | `GE_D204` | `port/src/audio.c` (`audioSetNextBuffer`) | **Audio-health monitor (D204).** One line per 5 s of wall clock: `rt=` produced-audio/wall ratio (1.000 healthy; below 1 = the 30 Hz audio retrace is starving and the device is padding with silence), `q=` SDL queue depth / `queueLimit` (pinning at the limit with `drop=` climbing = overproduction, the "eventually no audio" state), `drop=` blocks discarded, `max=` largest block vs its `g_MaxFrameSize*4` allocation. **Deliberately cheap (~30 clock reads/s) — safe to leave on for a full playtest, unlike `GE_MIXERTRACE`.** The instrument of record for D202. | **live** | -| `GE_D204_OLD` | `port/src/audio.c` (`d204OldMode`) | Restores pre-D204 behaviour (`osAiGetLength()` reports the whole SDL queue depth; `queueLimit` back to 8192) so before/after can be A/B'd in **one binary**, without build-to-build variance. Temporary — remove when D204 closes. | **live** (D204 A/B) | +| `GE_D204_OLD` | `port/src/audio.c` (`d204OldMode`) | Restores pre-D204 behaviour (`osAiGetLength()` reports the whole SDL queue depth; `queueLimit` back to 8192) so before/after can be A/B'd in **one binary**, without build-to-build variance. Temporary — remove when D204 closes. | **dead** (D204 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE | | `GE_AUDIOTRACE` | `src/snd.c` (`sndPlaySfx`, `sndDeactivate`), `src/game/propobj.c` (door sounds), `src/libultra/audio/load.c` (`alLoadParam`) | Per-`sndPlaySfx` resolution trace to `audiotrace.log`: `soundIndex` → `ALSound*`/keyMap/wavetable/base/len/book, the returned `ALSoundState*`, every `sndDeactivate`, and wavetable bind events. M-65 additions: `[VOICE+]`/`[VOICE-]` pairing every voice acquire/release with sound/state/flags (NB: written from two threads to one unbuffered FILE* — occasional torn lines, parse defensively), `[SLOTWRITE]` door slot ownership, `[RETRIGGER-POST]`, `[WAVELOOP]` (per-sound `ALADPCMloop` presence), `[DOORSND]` every door sound state change incl. an `ORPHAN-both-slots-busy` verdict. M-66b addition: `[EXPIRE]` — posted whenever the port-side ownerless-infinite-loop expiration is scheduled (sound/state/delay/fade); this line is **permanent** (part of the D202 option-C fix), unlike the rest of the probe set which is removable on close. M-67/M-69 additions: `[WIRE] t=µs filter=… <- table=…` (`src/libultra/audio/load.c`, every wavetable→decoder bind, µs-timestamped) and `[VOICEMAP]` (`src/libultra/audio/synthesizer.c`, full PVoice→decoder/resampler/env-state address map at synth init — this is how offline voicedump slot addresses tie to WIRE filter addresses). M-68 additions: `[EVT] t=µs type=N state=P` + `[STOP-EVT] … deltaUs=N` (event types per `src/snd.h`: 1=PLAY, 2=STOP, 8=VOL, 64=DECAY, 128=END). M-71 additions: `[VOL] t=µs state=… rawVol=… playing=…` (`src/snd.c`, per-VOL-event value before the slot-volume law) and `[DISTVOL] pos=(…) player=(…) dist=… vol=…` (`src/game/propobj.c`, the distance-attenuation funnel `sub_GAME_7F053894` — this is how an SFX's final volume is computed from hit position). NB: lines from the multiple file handles interleave — occasional torn/corrupt lines, parse defensively. `dumppos=` gives the exact byte offset into `audiodump.raw` (quantised to the 2880-byte audio block). Low cost. | **live** (D202/D205) | | `GE_PULLTRACE` | `src/libultra/audio/load.c` (`alAdpcmPull`) | `[PASTEND]` assertion: fires if a voice's `f->sample` exceeds its wave's total samples while `nOver == 0` (i.e. an unstopped voice keeps decoding past the end instead of zero-filling). Rate-limited to 40 lines. M-65: 0 hits — exhausted voices go silent correctly. | **live** (D202) | | `GE_DMEMWIPE` | `port/src/mixer.c` (`aClearBufferImpl`) | Zeroes the whole software DMEM at the frame boundary (detected as `alMainBusPull`'s clear of `AL_MAIN_L_OUT`, addr 1088). Tests whether audio state survives across frames in DMEM. M-65: no-op on output — ruled out. | **live** (D202) | | `GE_NOWET` | `port/src/mixer.c` (`aSetVolumeImpl`) | Mutes the reverb send (`wetamt = 0`), isolating the reverb delay lines as a possible source of persistent signal. M-65: drone unchanged — ruled out (NB `aPoleFilterImpl` is still an unimplemented no-op, D199). | **live** (D202) | | `GE_BANKDUMP` | `port/src/romdata.c` | `BANKSRC` lines: the source ROM-layout bank tree per `soundIndex` (instrument → sound → envelope pointers + `decayTime`). Used to prove sound 203's `decay=-1` is genuine ROM data, not a conversion bug. One-shot at load. | **live** (D202) | -| `GE_FORCEALARM` | `src/game/propobj.c` (`handle_alarm_gas_timer_calldamage`, `#ifdef PORT`) | **D207 diag (temporary).** Forces the in-game alarm klaxon (`alarmActivate`) on a frame counter — on ~2–10 s, off, on again — and emits a per-frame `[D207] f=… active=… ptr_alarm_sfx=… playstate=… locked=…` line to `audiotrace.log`. Repro harness for the "alarm SFX overrides other audio" report when a level's own alarm can't be triggered headlessly. Remove when D207 closes. | **live** (D207) — temporary | +| `GE_FORCEALARM` | `src/game/propobj.c` (`handle_alarm_gas_timer_calldamage`, `#ifdef PORT`) | **D207 diag (temporary).** Forces the in-game alarm klaxon (`alarmActivate`) on a frame counter — on ~2–10 s, off, on again — and emits a per-frame `[D207] f=… active=… ptr_alarm_sfx=… playstate=… locked=…` line to `audiotrace.log`. Repro harness for the "alarm SFX overrides other audio" report when a level's own alarm can't be triggered headlessly. Remove when D207 closes. | **dead** (D207 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE | | `GE_KEYMAPDUMP` | `port/src/romdata.c` | `KEYMAP` + `ENVSRC` lines: raw source bytes of every `ALKeyMap` / `ALEnvelope` in the ROM-layout blob, for byte-wise comparison against runtime values. One-shot at load. | **live** (D202) | | `GE_AUDIODUMP` | `port/src/audio.c` (`audioSetNextBuffer`) | Raw-dumps every mixed s16/stereo/22050 Hz buffer to `audiodump.raw` in the CWD for offline waveform inspection. Length ÷ 88200 = seconds of audio produced. | **live** (D202) | | `GE_VOICEDUMP` | `port/src/mixer.c` (`aEnvMixerImpl`) | Dumps each voice's resampled mono stream (post-resample/pre-envelope, i.e. the env-mixer input) to `voicedump.raw` as records of `[u32 stateAddr][u32 nSamples][u64 us][s16 × n]`. **The µs stamp is when the block was MIXED — records are rendered ahead, so a record's samples END at its stamp** (M-69: treating stamps as start offsets misplaces content by up to one ~33 ms block). `stateAddr` = the env-mixer state address from `[VOICEMAP]`. Used with `scratchpad/exact_match.py` for full-corpus per-voice sample verification. | **live** (D202) | @@ -67,10 +67,10 @@ Two classes: | `GE_D288` | `port/fast3d/gfx_pc.cpp` (`gfx_sp_tri1`, right after the D233 trivial-reject guard; cached `ge_d288`) | M-152 diag for the Silo intro stray triangle: frame-gated to `num_dls` 550–755 (M-155), now **skips `any_behind_camera==1` triangles** (M-155 follow-up — the x/w divide is meaningless for w≤0; that subset was pure false-lead noise) and logs the render-mode word (`oml`) so a live capture can ID the culprit asset against level tables. Logs any triangle whose clip-space NDC bbox covers >50% of the screen or goes non-finite, with each vertex's `w`. Bare `-level_20` re-run: 1965 sane hits in the window, still zero for the target color RGB(83,62,44) — consistent with M-155's working theory that the bare boot's scripted camera never reaches the bug state; **headless probing is at its limit, live capture is next**. | **dead** (D288 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE | | `GE_D245V` | `src/game/sky.c:962` (`skyPortRenderPoly`-adjacent, `#ifdef PORT`) | D245 water-seam diag: per-call min/max of the recomputed S/T texture-coordinate ranges for the water fan vertices (rate-limited), to check whether the seam tracks a tc-shift boundary. Inert unset. | **live** (D245 OPEN) | | `GE_D245_FIXEDSHIFT` | `src/game/sky.c:946` (`#ifdef PORT`) | D245 test override: pins the water tc shift `k` to the given integer (0..SKY_TC_MAX_SHIFT) instead of the computed per-vertex value, to A/B the allowShift=TRUE behavior. Off by default (unset = current behavior). | **inert** (D245 fixed) — since the s16 sky path was removed 2026-09-30 nothing consumes the forced shift; strip candidate | -| `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **live** (D252 OPEN — non-reproducible) | +| `GE_D252` | `port/fast3d/gfx_pc.cpp:733` (`import_texture`, cached `s_d252`) | D252 rainbow-particle diag: one-shot per (address,size) import log of the tile state (tmem/fmt/siz/addr/size/line) for small textures so a suspect particle tile can be checked against its source bytes by hand. Remove once D252 is resolved. | **dead** (D252 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE | | `GE_D252POOL` | `src/game/image.c` (`texLoad`), `src/game/initmttex.c`, `port/src/gimgfixup.c` | **TEMP** (M-201): one line per new texture load (pool main/other, free bytes) + `POOL-FULL` events + stage pool range; `D252SYNC: N slots re-synced, M changed` per stage load (the D252 fix at work: M>0 from the 2nd stage on). Load-time only, no fps cost. | **dead** (D252 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE | | `GE_D245_OLDVTX=1` / `GE_D245_OLDTEX=1` | `src/game/sky.c` (`skyPortRenderPoly`) / `port/fast3d/gfx_pc.cpp` (CI8 import + tri-path dims) | **A/B** (M-201): restore the pre-fix s16 sky/water vertex path / the pre-fix 32x43, 16-wide water texture geometry. | **dead** (D245 fixed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE. The pre-fix s16 sky/water vertex path (`skyPortRenderPoly` tail + `skyPortEmitTileShift`) went with it; the float path is now unconditional | -| `GE_DYNTEXHASH_OFF=1` | `port/fast3d/gfx_pc.cpp` (`import_texture` cache key) | **A/B** (M-201, D341): disable the content-hashed cache key for dyn-pool textures (reverts to the stale address-only key). | **TEMP** (A/B switch) | +| `GE_DYNTEXHASH_OFF=1` | `port/fast3d/gfx_pc.cpp` (`import_texture` cache key) | **A/B** (M-201, D341): disable the content-hashed cache key for dyn-pool textures (reverts to the stale address-only key). | **dead** (D341 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE | | `GE_D207S` | `src/game/gunfire.c` (player shot-sound block, `#ifdef PORT`) | **TEMP** (M-201): one line per player shot-sound decision: `D207S t= hand item rate -> H1|H2|SKIP` (SKIP = both per-hand sound handles still held). Read-only, uncapped, cheap; use instead of `GE_AUDIOTRACE` for live captures (that probe does file I/O on the audio path). | **dead** (D207/D241 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, branch `chore/strip-temp-probes`); tombstone — the generator flags it GONE | | `GE_D306C="f0 f1 [wmax]"` | `port/fast3d/gfx_pc.cpp` (after `zf_note_emit`) | **TEMP** (M-201): depth-state census of every emitted triangle in frames f0..f1 (GL depth mode, geometry_mode, other_mode_l, tile-0 texture, NDC centroid, z/w range, min w); optional `wmax` keeps near geometry only. Cap 60000 lines. | **TEMP** (D306 closed; remove post-release) | | `GE_INTROCAM=` | `src/game/bondview_r.c` (`#ifdef PORT`, intro camera pick) | **TEMP/test-only**: force the level intro's first camera index (Dam truck shot = 5). The PRNG draw is still consumed. Logs `D306: intro camera index` when set (or with `GE_D306C`). | **TEMP** (useful repro tool) | @@ -139,7 +139,7 @@ Two classes: | `GE_D314` | `port/src/optionsoverlay.c:681` (cached `s_d314Enabled`) | D314 F10-flicker probe: logs the overlay's viGetY / visible-row count / scroll / selection each poll. Inert unset; strip candidate once D314 is triaged. | **live** (D314 pending triage) | | `GE_D318R` | `port/src/random.c:69,147` (`#ifdef PORT`) | PRNG golden probe (D284/D318): logs the first 4000 gameplay-PRNG draws with tick + caller return address, and every `randomSeed` set — verifies the bit-exact port of `random.s` and which call sites consume the stream. Inert unset. | **live** (D284 verification) | | `GE_RSEED` | `port/src/random.c:134` (`#ifdef PORT`) | Test-only boot-seed override: `GE_RSEED=` pins `g_randomSeed` at first seed instead of the wall-clock-derived value — for determinism and N64-parity tests. Inert unset. | **live, test-only** | -| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `@`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **live** (D318 verification) | +| `GE_RSEED_LV` | `port/src/d318watchdog.c:232` (`#ifdef PORT`) | One-shot level-start seed log: logs `g_randomSeed` at the first tick after level start (optionally armed for a specific tick via `@`) — ties a saved/loaded game's PRNG state to a level for D318-class repro. Inert unset. | **dead** (D318 closed) — removed 2026-09-30 (ROADMAP §7 strip pass, batch 1, branch `chore/scaffolding-removal-1`); tombstone — the generator flags it GONE | | `GE_D318T` | `port/src/d318watchdog.c:270` (`#ifdef PORT`) | D318 watchdog timeline probe: per-tick log of the Facility ai_22 softlock signature window (c78 list/offset/attack state, c67 health) while the watchdog watches. Inert unset. | **live** (D318 verification) | | `GE_D322` | `port/src/audio.c` (`audioSetNextBuffer`), `src/snd.c` (`sndD322PoolSummary`) | D322 pool-telemetry probe: one line per 5 s of wall clock with the three pool layers a long session can exhaust — `sfx=count/max` (8-voice SFX soft cap), `evtq=n/64` (event queue occupancy; full = silent post drops, M-71), `pv free/lame/alloc` (24 physical synth voices shared by SFX + music; `-1`s before sndInit), plus the D204 pipeline fields. Cheap (~30 reads/s), campaign-safe; read together with `GE_D204=1`. | **live** (D322 capture) | | `GE_D318W` | `port/src/d318watchdog.c` (`d318WatchdogTick`, `#ifdef PORT`) | **The D318/D320 pin detector, ON by default, DETECT-AND-LOG ONLY since 2026-09-30.** Watches the Facility ai_22 / ai_19 and Control ai_9 hold-pose pin signatures (findings D318/D320) and logs `D318W:` lines (first detection, confirmation after 600 ticks, periodic reminders while held). It used to re-seed the attack via `sub_GAME_7F025560`; that recovery was removed because D329 fixed the cause, so a hit now means a D329 regression. `GE_D318W=0` silences it. | **live** (D329 regression detector) | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index e9f85b0b..d65b5682 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -172,7 +172,7 @@ D290,FIXED,"Watch's in-game ""controller"" section shows no control-pad graphic D291,CLOSED,"Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).","CLOSED — duplicate of D306, faithful (M-201, 2026-09-26:" D292,FIXED,Death animations are glitched —,"MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) —" D293,FIXED,"QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest —","FIXED (M-141) — ""Quit to desktop"" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.…" -D294,PARTIAL,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —",PARTIAL (2026-09-29 bookkeeping: +D294,PARTIAL,"Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later… —",PARTIAL -> fix landed pending by-eye check (2026-09-30: D295,FIXED,"Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtes…",FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed: D296,CLOSED,"Control Center, 00 Agent:","CLOSED (2026-09-26, user-approved:" D297,FIXED,v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).,FIXED (M-150) — migration shim landed: @@ -284,7 +284,7 @@ D404,RESOLVED,"""Aim style: Centred (PC)"" (Input.AimMode=1, D333/D337) has no e D405,CLOSED,"fast3d/OpenGL: MSAA above the driver max black-screens; two glBindFramebuffer calls pass an index where a GL name is required (PR #106, italoarruda, 2026-09-25…","CLOSED (2026-09-28). Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations)." D406,RESOLVED,front-end PC Options screen:,"RESOLVED (2026-09-28, port-layer only, Steam Deck-validated):" D407,CLOSED,Tanks cannot be boarded/exited on PC —,CLOSED (2026-09-28 bookkeeping: -D408,OPEN,Game.SkipIntro skips the post-mission failure dossier:,"OPEN (cosmetic, not root-caused past the handoff site)." +D408,FIXED,Game.SkipIntro skips the post-mission failure dossier:,FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed). D415,FIXED,"Shooting a light fixture does not ""kill"" its flickering light (Bunker, Caverns):",FIXED (2026-09-29 bookkeeping: D424,FIXED,WeaponStats.RecoilSpeed/b44[] byte order:,"FIXED in tree (WS_B44, port-guarded); user-verified vs GEPD/1964 2026-09-29." D425,FIXED,Tank run-over death sound loops:,FIXED (2026-09-29 bookkeeping: @@ -301,7 +301,14 @@ D440,FIXED,"""Original N64"" / ""Port defaults"" one-press presets in the F10 ov D441,PARTIAL,64-bit pointer-width sweep (part 1):,"PARTIAL (mechanical widening landed, build/run verified on Bunker and Silo; census/provenance table for the remaining sites still owed)." D442,FIXED,Game.AllUnlocked reworked into a pure RAM/query-time override:,FIXED (build and eep-diff verified; level-select/cheat-menu live check owed). D443,FIXED,"D357 settings-display cleanup (mouse ×, whole-% deadzone and volume steps, FOV in horizontal degrees, MSAA 16×) plus a ""Restart game"" action (orderly D344 quit…",FIXED (port-only; reset probes pass; both-UI visual check and one Restart click owed; Linux relaunch path not compiled). +D454,FIXED,Knife slash crashed on Windows:,FIXED (verified by maintainer live: +D461,FIXED,Item-slot allocation overran into the next stage-bank block:,FIXED (maintainer-verified: +D462,FIXED,vtxstore.c sized its two record tables with the N64 stride 0x14 for a 24-byte struct unk_09B7A0_struct_parent on PC (Vertex* unk00 widened); both are indexed a…,FIXED (build + solo -level_09 and 4P GE_STARTMP=4 smoke runs exit clean; no crash; by-eye owed in solo). D444,FIXED,Front-end PC Options:,FIXED (build-verified; human mouse test owed) +D455,FIXED,Binary-first census of exe-static addresses truncated to 32 bits (D454 class) (2026-09-30).,FIXED/CLEAN (no further REAL site beyond D454) D447,FIXED,Pillarbox output mode (Video.AspectMode = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.,"FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human)" D445,FIXED,Crouched-rifle firing animation (crouched_rifle_firing_animation_groupA) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player cro…,"FIXED — layout only (§D5 linker adjacency, D164 class):" D446,FIXED,Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).,FIXED — port-only (fast3d): +D456,?,"Font-pointer / text-string-pointer slots retyped from s32 to real pointers (ABI/layout exception; front.c, options.c, language.c, bondview*, mpmenu.c, propobj.…", +D453,?,"memp allocator API made 64-bit clean (D441 step 0; ABI/layout exception, §A1).",2026-09-30 +D451,FIXED,Automatic-fire gate at unsteady frame rates (D427 follow-up) and D173 residual.,"FIXED (RULE-2-SIGNOFF, maintainer 2026-09-30:" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 6411686c..3651a262 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -580,7 +580,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D291 | **Dam: texture glitches when the truck drives past (M-141, Steam Deck user playtest).** — full `## D291` entry at file tail | CLOSED — duplicate of D306, faithful (M-201, 2026-09-26: user-captured, Dam intro camera 5 only). Earlier: OPEN — observed, not investigated. Needs a live repro with the truck's exact position/timing and, ideally, a screenshot. | | D292 | **Death animations are glitched — Bond is often sunk halfway into the floor (M-141, Steam Deck user playtest).** — full `## D292` entry at file tail | **MOSTLY FIXED as a side effect of D243 (2026-09-18, user-confirmed alongside D173) — same fix, same outcome: right 9/10 times, small residual offset only when off, no more full sinking/glitching.** See D173 for the user's exact confirmation wording; not separately re-tested, reported together. | | D293 | **QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest — "couldn't see a way to exit the game. Maybe build this into the…** — full `## D293` entry at file tail | FIXED (M-141) — "Quit to desktop" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.… | -| D294 | **Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later…** — full `## D294` entry at file tail | PARTIAL (2026-09-29 bookkeeping: main symptom FIXED — room-model pool; user playtests 2026-09-22/25 no longer reproduce the black ground; the residual corner flicker at max FOV reported 2026-09-22 was never re-checked, tracked in docs/ROADMAP.md §2). Earlier: **FIXED, user playtest confirmed (2026-09-25: not reproduced)** — root cause: room-model POOL exhaustion (authored `-ma220`=220 KB < ~250 KB monument-plaza demand at FovScale 130 + max DD/Lod); `memaAlloc` NULL → silent skip → undrawn room shows the night-sky clear colour. Stochastic (wall-clock seed → load order). Fix: `portRoomPoolScale()` (video.c, identity at N64-faithful settings) applied in `bossMainloop`; verified headless (faithful→exact authored size; user settings→0 failures; `GE_ROOMPOOL=0.25`→exhaustion on demand). Playtest: main patch gone; residual corner flicker traced to full-level residency (487 KB = 2.2× authored) starving the 2× pool; the 3×/linear fix REGRESSED the renderer on the menu-driven path (D156 NaN anim state, not a bank overflow) → reverted to sqrt + 2.0 cap (ma=450560). Residual open; probes in place for next capture. High-DD horizon edge on night levels = setting-inherent, documented only.… | +| D294 | **Statue: ground geometry near the large CCCP/hammer-and-sickle monument briefly renders solid black over a wide area, then self-recovers 10-30 seconds later…** — full `## D294` entry at file tail | PARTIAL -> fix landed pending by-eye check (2026-09-30: pool cap 2.0 -> 2.5, DD/Lod terms powf 0.66, quarter-step round-up; ma 450560 -> 563200 at max settings; see D294 entry tail). Prior: PARTIAL (2026-09-29 bookkeeping: main symptom FIXED — room-model pool; user playtests 2026-09-22/25 no longer reproduce the black ground; the residual corner flicker at max FOV reported 2026-09-22 was never re-checked, tracked in docs/ROADMAP.md §2). Earlier: **FIXED, user playtest confirmed (2026-09-25: not reproduced)** — root cause: room-model POOL exhaustion (authored `-ma220`=220 KB < ~250 KB monument-plaza demand at FovScale 130 + max DD/Lod); `memaAlloc` NULL → silent skip → undrawn room shows the night-sky clear colour. Stochastic (wall-clock seed → load order). Fix: `portRoomPoolScale()` (video.c, identity at N64-faithful settings) applied in `bossMainloop`; verified headless (faithful→exact authored size; user settings→0 failures; `GE_ROOMPOOL=0.25`→exhaustion on demand). Playtest: main patch gone; residual corner flicker traced to full-level residency (487 KB = 2.2× authored) starving the 2× pool; the 3×/linear fix REGRESSED the renderer on the menu-driven path (D156 NaN anim state, not a bank overflow) → reverted to sqrt + 2.0 cap (ma=450560). Residual open; probes in place for next capture. High-DD horizon edge on night levels = setting-inherent, documented only.… | | D295 | **Campaign-halting crash at the end of Control Center on 00 Agent difficulty, then a crash on every subsequent relaunch/resume (GitHub #87, user campaign playtest).** — full `## D295` entry at file tail | FIXED (M-149) — M-148 root-caused + reproduced on HEAD; fix landed: port-side JPN-cache seed (`port/src/main.c`) + `#ifdef PORT` buffer widen in `front.c` (D8 class). Verified: previously-crashing input now clean, 21/21 sweep PASS. Companion save-wipe regression → D297. History — M-148 fully root-caused + **reproduced on current HEAD**: `difficultytext[4]` (`front.c:2616`) overflow — `strcpy("00 Agent")`/`strcat("\n")` spills onto `textpos.p[0]` (y, ≥0x80 in normal geometries) → high-bit byte → `langGetJpnCharPixels` NULL deref (`language.c:320`, `g_JpnCacheCacheItems` NULL outside JPN mode — likewise unallocated on N64 non-JP builds, crash potential is decomp-native). The user's save is a *valid pre-D284* save (all 5 slot CRCs bit-exact under the old PRNG); post-fix builds wipe it at validation, which is why the unmodified artifact no longer triggers it — NOT a fix. No OOB exists (M-147 guess retracted: 007 difficulty has no stored times). Fix options flagged for sign-off (port-side JPN-cache init preferred; `difficultytext` enlarge = D8 class). Side finding: v0.3.0 silently wipes all pre-fix saves on first launch (D284 side effect — own D entry owed). Separate still-open issue found: unconditional `[EVTQ-DROP]` audio-pool-exhaustion logging, `src/libultra/audio/event.c:92`.… | | D296 | **Control Center, 00 Agent: noticeable stutter, user's own read is "maybe from the drone guns" (M-146, same D295 investigation session).** — full `## D296` entry at file tail | CLOSED (2026-09-26, user-approved: stutter seen only under gdb (debug.ps1); D302, fixed two days later, removed a per-triangle uncached getenv costing ~15 fps; reopen if seen on a normal build). Earlier: OPEN — single live report, un-triaged; observed only under a gdb-attached debug-harness launch (`tools_pc/debug.ps1`), not the packaged release artifact — user flagged that harness overhead has caused false stutter before. Needs a plain-exe / real-release repro check before any perf tracing.… | | D297 | **v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).** — full `## D297` entry at file tail | FIXED (M-150) — migration shim landed: `port/src/legacycrc.c` (isolated, deliberately-wrong pre-D284 PRNG, reviewer-verified bit-exact against `0de99b4d^`) + eeprom-shim read-path hook in `libultra.c` (block 4 only; persists pristine+migrated bytes before the read-time-only AllUnlocked patch). Verified: user's #87 artifact → `D297-migrated 5`, all slots OK, idempotent on relaunch; native post-fix save untouched; corrupt slot left to the game's own wipe path; fresh eep untouched; 21/21 sweep PASS. Block-0 header needed no migration (factory seal, verified). | @@ -692,7 +692,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D405 | **fast3d/OpenGL: MSAA above the driver max black-screens; two `glBindFramebuffer` calls pass an index where a GL name is required** (PR #106, italoarruda, 2026-09-25; label rebased from the PR's D323, which collides with our D323 LOD finding) — full `## D405` entry at file tail | **CLOSED (2026-09-28).** Merged from PR #106 (code file only; security-reviewed clean - port-layer GL state fixes, no external input, no allocations). `resolve_msaa_color_buffer` clamps `msaa_level` to `GL_MAX_SAMPLES`; the resolve/copy paths bind `current_framebuffer ? framebuffers[i].fbo : 0` (the default framebuffer is GL name 0). Both Windows (opengl32) and Linux use this backend. Post-merge sweep 21/21 PASS + MSAA=16 boot check render-verified; link ritual clean. | | D406 | **front-end PC Options screen: long sections (INPUT, 17 visible rows) run off the bottom of the 440x330 paper, and rows past the silent 14-row page cap (Crouch mode, Reset to defaults, Bindings…) are unreachable from the front screen** (user report 2026-09-28, v0.4.0 pre-push review) — full `## D406` entry at file tail | RESOLVED (2026-09-28, port-layer only, Steam Deck-validated): in-section pagination in `frontoptions.c` (11 rows/page; the vertical stepper crosses page boundaries both ways; dim 'Page p/N' marker + bottom hint on every page; slider bars start past long labels; F10 overlay untouched). D406b/c/d same-cycle refinements (label/track overlap, hint visibility/wording, hint position). | | 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 | **CLOSED (2026-09-28 bookkeeping: tank board/exit accepted by the user; shipped in v0.4.0).** Earlier: 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)". | +| 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 | FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed). | | D415 | **Shooting a light fixture does not "kill" its flickering light (Bunker, Caverns): the fixture takes the hit but the flicker keeps going (community report, issue #87 comment by the reporter, 2026-09-28, 00 Agent playthrough of v0.4.0).** — full `## D415` entry at file tail | FIXED (2026-09-29 bookkeeping: root-caused and fixed as D430 + D431, user-verified vs GEPD/1964 on Caverns 2026-09-29; #119). Earlier: ROOT-CAUSED as D430 (see there); fix in tree, live check owed. | | D424 | **`WeaponStats.RecoilSpeed`/`b44[]` byte order: no grenade-launcher recoil, shortened recoil on every gun (issue #118).** — full `## D424` entry at file tail | FIXED in tree (`WS_B44`, port-guarded); user-verified vs GEPD/1964 2026-09-29. | | D425 | **Tank run-over death sound loops: `(chrflags << 7) >= 0` is always true on unsigned GCC enum (issue #115).** — full `## D425` entry at file tail | FIXED (2026-09-29 bookkeeping: use-site `(s32)` cast; user-verified vs GEPD/1964 2026-09-29, #115). Earlier: FIXED in tree (use-site `(s32)` cast); live check owed. | @@ -709,10 +709,17 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D441 | **64-bit pointer-width sweep (part 1): 17 files widened `(u32)`/`(s32)` pointer casts to `uintptr_t` under `#ifdef PORT` (ABI/layout exception; ROADMAP §2 "Latent 64-bit ABI hazards").** — full `## D441` entry at file tail | PARTIAL (mechanical widening landed, build/run verified on Bunker and Silo; census/provenance table for the remaining sites still owed). | | D442 | **`Game.AllUnlocked` reworked into a pure RAM/query-time override: #ifdef PORT hooks in `fileGetIsCheatUnlocked` and `fileIsStageUnlockedAtDifficulty` (every stage reports COMPLETED); the EEPROM patch and the D387 merge are removed (RULE-2-SIGNOFF, maintainer opt-in feature).** Full `## D442` entry at file tail | FIXED (build and eep-diff verified; level-select/cheat-menu live check owed). | | D443 | **D357 settings-display cleanup (mouse ×, whole-% deadzone and volume steps, FOV in horizontal degrees, MSAA 16×) plus a "Restart game" action (orderly D344 quit, then relaunch).** Full `## D443` entry at file tail | FIXED (port-only; reset probes pass; both-UI visual check and one Restart click owed; Linux relaunch path not compiled). | +| D454 | **Knife slash crashed on Windows: `var_a0_2` in `gunTickHandState` was `u32`, truncating the exe-data address of `D_80034CA4/E0C` (PE image base 0x140000000) before `gunSample1PTransform` (ABI/layout exception, §A1).** Full `## D454` entry at file tail | FIXED (verified by maintainer live: knife slash no longer crashes). | +| D461 | **Item-slot allocation overran into the next stage-bank block: `alloc_additional_item_slots` used the N64 0x14 stride for a 32-byte `InvItem` (the code comment tags it D458b); `bondinvReinitInv` wrote `p_itemcur[i].type = -1` at the C stride, scribbling 0xffffffff into the first prop model file (TT33 root Child) and crashing 2P respawn / pickups later (ABI/layout, section A1).** Full `## D461` entry at file tail | FIXED (maintainer-verified: ~10 min of 2P on Caves, Bunker and Complex, no crash). | +| D462 | **`vtxstore.c` sized its two record tables with the N64 stride 0x14 for a 24-byte `struct unk_09B7A0_struct_parent` on PC (`Vertex* unk00` widened); both are indexed at the C stride, so each overran its block (up to ~2 KB at 500 entries) into the next stage-bank allocation. Same shape as D461; found by the local-Qwen literal-stride allocation audit (ABI/layout, section A1).** Full `## D462` entry at file tail | FIXED (build + solo `-level_09` and 4P `GE_STARTMP=4` smoke runs exit clean; no crash; by-eye owed in solo). | | D444 | **Front-end PC Options: clickable Previous/Next page controls; hover no longer overrides the wheel/page selection; stale All-unlocked comments refreshed.** — full `## D444` entry at file tail | FIXED (build-verified; human mouse test owed) | +| D455 | **Binary-first census of exe-static addresses truncated to 32 bits (D454 class) (2026-09-30).** — full `## D455` entry at file tail | FIXED/CLEAN (no further REAL site beyond D454) | | D447 | **Pillarbox output mode (`Video.AspectMode` = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.** — full `## D447` entry at file tail | FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human) | | D445 | **Crouched-rifle firing animation (`crouched_rifle_firing_animation_groupA`) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player crouch-fires a rifle (2026-09-28).** — full `## D445` entry at file tail | FIXED — layout only (§D5 linker adjacency, D164 class): `crouched_rifle_firing_animation_group1` has no zero terminator and on N64 falls through into `...groupA` (source-order .data); GCC places groupA first, so groupA[0] kept the raw anim offset 0xB84. `#ifdef PORT` one array {right, left, terminator} + `groupA` alias (chr.c/chr.h); N64 resolution restored. | | D446 | **Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).** — full `## D446` entry at file tail | FIXED — port-only (fast3d): `GL_TEXTURE_MAX_ANISOTROPY` is per-texture-object state that `gfx_opengl_set_sampler_parameters` set only when mipmapped and never reset; a GL texture id freed by `gfx_texture_cache_delete_range` (unordered_map order = texture address order) and reused for a non-mipmapped glyph kept 4x. Now always written (level or 1). Render-only; sim never affected. | +| D456 | **Font-pointer / text-string-pointer slots retyped from `s32` to real pointers (ABI/layout exception; front.c, options.c, language.c, bondview*, mpmenu.c, propobj.c:3909 alias replaced by named field).** `frontPrintText`/`display_aligned_white_text_to_screen` font params, `copy_1st/2ndfonttable`, `setFontTables`, ~26 options.c locals, `g_LangBanks[]` + `langGet` result, `player.ptr_text_*_mp_award`. front.c 186->2, options.c 139->0 int-conversion warnings. FIXED (static + 1 launch; not gameplay-verified). | +| D453 | **memp allocator API made 64-bit clean (D441 step 0; ABI/layout exception, §A1).** `mempCheckMemflagTokens` took the arena start as `s32`, `mempSetBankStarts` carried it in an `s32 mempStart`, and `mempAllocPackedBytesInBank` returned a pointer as `u32`; all truncate/sign-extend an arena at or above 0x80000000. Widened to `uintptr_t`/`void*` under `#ifdef PORT` (N64 arm unchanged). mema.c NOT changed (structural, see entry). | 2026-09-30 | fix/abi-memp-widen | [H] for memp, mema deferred | +| D451 | **Automatic-fire gate at unsteady frame rates (D427 follow-up) and D173 residual.** — full `## D451` entry at file tail | FIXED (**RULE-2-SIGNOFF**, maintainer 2026-09-30: covered by the D427 grant; tick-crossing gate, `gunfire.c` `d427Gate`); headless A/B at FpsCap 45 measured; maintainer hold-fire test at 60/30 fps showed no perceptible difference (expected: identical at steady rates). D173 not the same class (see entry). | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS @@ -11980,6 +11987,8 @@ Superseded history below (the original OPEN investigation, root cause not found **2026-09-25 playtest (user, Windows, one ~46-min session Silo → Caverns, `GE_D322=1 GE_D204=1`, build `7baf4900`):** walking past the CCCP monument did not reproduce the black ground. Confirmed fixed. +**2026-09-30 (fix/d294-room-pool-cap): cap raised 2.0 -> 2.5, residual corner flicker fix landed (port-only, awaiting by-eye check).** The 3x 'regression' above was D336 (uninitialised head-anim placeholder fields), not pool size, so the cap is revisitable. `portRoomPoolScale()` (`port/src/video.c`): final cap 2.0 -> 2.5 (and the `GE_ROOMPOOL` clamp 2.0 -> 2.5); DD/Lod terms changed from `sqrtf` to `powf(x,0.66)` (4^0.66 = 2.497) because with sqrt the max-DD term is exactly 2.0 and a raised cap alone changes nothing; result is rounded UP to a quarter step because `boss.c` truncates `(s64)(scale*4)` (raw 2.497 truncated to 2.25 -> ma=506880, only 4% over Statue's 487 KB set). Statue at FovScale 150 / DD 400 now gets `ma` = 2.5 x 225280 = 563200 (was 450560), i.e. 1.16x the full 27-room set (measured sum of `D104 LOAD used` over 27 rooms = 498752). Worst case `-ma350` row = 875 KB, well inside the STAGE bank (~3.8 MB free per D336; `boss.c` still clamps to bank-left minus 128 KB). Identity at N64-faithful settings unchanged. Headless check (max FOV, DD 400, scripted walk, `GE_D294`/`GE_D104`): no `FAIL alloc` before or after -- the scripted walk cannot reproduce the fast-strafe churn, so the exhaustion counter is NOT falsified either way; maintainer to check Statue corners at max FOV by eye. No `src/game` change. + ## D211 — PC FOV slider — `Video.FovScale` (M-83 QoL wave, port-only, `WIDESCREEN-FOV-PLAN.md` Phase 4). **PC FOV slider — `Video.FovScale` (M-83 QoL wave, port-only, `WIDESCREEN-FOV-PLAN.md` Phase 4).** fast3d applies a multiplicative half-angle scale to every *perspective* projection matrix as it is loaded (`gfx_apply_fov_scale()` in `gfx_pc.cpp gfx_sp_matrix`, on the `G_MTX_PROJECTION | G_MTX_LOAD` path only — GE always reloads its view projection via `guPerspectiveF` at `fr.c:725`, so a relative `G_MTX_MUL` onto an already-scaled matrix cannot compound). Columns 0 and 1 are scaled by the same factor `tan(half_orig)/tan(half_adj)` → H and V FOV widen uniformly, aspect ratio exactly preserved (no stretch), and because the game rewrites fovy every frame for aim-zoom the scale composes with zoom for free. Ortho matrices (`P[3][3]==1`) are left untouched so HUD/menu 2D is unaffected. `Video.FovScale` = percent of the original vertical FOV, 50–150, **default 100 ⇒ `g_fov_scale == 1.0f` ⇒ guarded early-return, zero FP ops on the matrix ⇒ golden dumps byte-identical**. F10 overlay row "FOV scale %". Math checked offline (`s=1.25 → fovy 60°→75°`, aspect held at 1.3333). @@ -12905,6 +12914,8 @@ Verified: build clean (MSYS2 MINGW64/ninja); `tools_pc/verify.sh bunker1` — PA ## D322 — Long-session audio degradation: full campaign on v0.3.0, audio progressively worsens (Silo → Caverns/Cradle: OST inaudible, SFX "come and go"); restart restores it (issue #87, user report, 2026-09-20) +**Status (2026-09-30): CLOSED.** The 8-voice SFX cap is the decomp constant (`src/music.c:76`), so it is faithful and ACCEPTED by the maintainer. The #87 long-session decay was not reproduced in the maintainer's 2026-09-29 playtest ("our bugs are fixed for the most part with audio"). Reopen only with a repro. + ### Report (verbatim, issue #87 comment, v0.3.0 build) > I just did a full run on 00 agent including Aztec and Egyptian in one shot :) It definitely felt better overall but the audio really started to struggle in the final levels again, but i made it through to the end. Interestingly, exiting the game and starting it back up again let the audio go back to normal. Feels like something about long gaming sessions throws off the audio after a while. [...] i think Silo was the first instance i noticed some audio sync issues popping up (possibly from me spamming the KF7). But it wasn't until the end around caverns / cradle that it became very noticeable (like to the point you couldn't hear the background OST and the audio felt like it came and went) @@ -15577,6 +15588,8 @@ included; D281 keeps the shipped mitigation + disclosure, zero-saves root cause ## D408 — `Game.SkipIntro` skips the post-mission failure dossier: failed/aborted missions return straight to the menus with no REPORT screen (user report 2026-09-28, during v0.4.0 wrap-up playtest) +**Status (2026-09-30): FIXED (boot-only gate).** The SkipIntro block in `src/game/lv.c` (`#ifdef PORT`, port feature code) ran on every title-stage load, including the return from a mission, and its `menu_update = MENU_FILE_SELECT` overwrote the `MENU_MISSION_FAILED` route. It is now gated on `is_first_time_on_main_menu`. Verified: build, plus one boot with `SkipIntro=1` that logs `booting to file-select` exactly once. **Owed (maintainer):** fail or abort a mission with SkipIntro on and check the dossier shows. The hook also re-set `maybe_is_in_menu` on every return, so D216's audio-break half may be fixed too; listen for it. + **Symptom:** with `Game.SkipIntro = 1`, entering a solo mission and then either aborting (watch menu Z+A) or dying (KIA) returns the player straight to the front-end menus — the post-mission failure dossier is never shown. On a normal boot that screen is `MENU_MISSION_FAILED` (`front.c` `constructor_menu0C_missionfailed`: "REPORT / Mission status: KILLED IN ACTION" or "ABORTED", with the objective list and next/prev tabs to the mission-complete report). Initial suspicion was the user's save or `Game.AllUnlocked` (D257/D387); **user confirmed 2026-09-28 that it is a SkipIntro quirk** — save state and AllUnlocked are not involved. **Traced so far (handoff site, mechanism not fully pinned):** the stage-end handoff lives in `menu_init()`'s `MENU_RUN_STAGE` case (`front.c:~8909`, called from `lv.c:1322` only once `g_CurrentStageToLoad == LEVELID_TITLE`): non-replay solo stages fall through to `frontChangeMenu(MENU_MISSION_FAILED, 1)` (Cuba → cast display, multi → MP options, ramrom replay → legal screen). Under SkipIntro that handoff evidently never runs on the fail/abort return path — the exact point where the boot shortcut's state diverges (which flag/menu value the skipped intro stages would normally have left behind) is not yet identified. The abort path itself (`options.c:798`: `set_missionstate(MISSION_STATE_0); bossRunTitleStage(); mission_failed_or_aborted = TRUE; deleteCurrentSelectedFolder()`) and the KIA path (`bondview2.c bondviewKillCurrentPlayer`, `g_isBondKIA = 1`) are game logic and behave per the decomp; nothing in `port/` intercepts `MENU_MISSION_FAILED`. @@ -15911,3 +15924,107 @@ About 55-922 pixels differ, a few glyphs only, each by +-1-2 (mostly green). Eve - the free-id order is still address-dependent. The general fix would reset the whole object on reuse, or sort the ids that `delete_range` frees. Neither is needed for this symptom. **Confidence: [H]** for the cause and the fix (A/B reproduced, fixed, bisected to the one GL parameter). + + +## D454 — knife slash crash: keyframe pointer held in `u32` (ABI/layout exception, 2026-09-30) + +**Symptom.** Windows: equipping the knife and slashing crashed at once (EXCEPTION 0xc0000005, fault `0x40169764`, `Rcx=0x40169720`). Found by the read-side 64-bit census (`ABI-CENSUS-READSIDE.md`), confirmed by the maintainer. + +**Cause.** `gunTickHandState` (`src/game/gunfire.c`) picks the keyframe table with `var_a0_2 = D_80034CA4;` (or `D_80034E0C`) and passes it to `gunSample1PTransform(Weapon1PTransformKeyframe *, ...)`. `var_a0_2` was declared `u32`, and the tables are `u32[]` in the exe's `.data`, which on Windows sits above 4 GiB (image base `0x140000000`, ASLR off, CMakeLists.txt ~305). The store truncated the address. Linux is unaffected (image base 0x20000000). Class: §A1 (pointer-width misread from 32→64 widening). + +**Trap.** The declaration exists once per region arm (US, JP, EU). The first fix (`75559906`) patched only the EU arm and the US build still crashed with the identical fault; the disassembly showed `mov ecx,ecx` before the call. All three arms are now patched (`8ba7dbb7`). Lesson: a decomp-style function can repeat a local per region `#if` arm; grep the declaration in every arm and check the disassembly (`mov reg32,reg32` before a pointer argument). + +**Fix.** `#ifdef PORT` declares `Weapon1PTransformKeyframe *var_a0_2` and casts the two table assignments; the `#else` arm is the original. No logic change. + +**Verified.** Maintainer: knife slash no longer crashes (Windows, this build). Disassembly: no `mov ecx,ecx` before any `gunSample1PTransform` call. **Confidence: [H].** + +## D456 - font/text pointer slots held in s32 (ABI/layout exception, 2026-09-30) + +**Source.** Read-side census (ABI-CENSUS-READSIDE.md): 286 of 618 -Wint-conversion warnings were two slot kinds, font pointers (`struct font *` / `struct fontchar *`) and `langGet()` strings, parked in `s32`. Implicit ptr->int narrowing has no cast, so a cast census never sees it. Pointers come from arena memory; any arena >= 2 GiB sign-extends to wild pointers, >= 4 GiB truncates. + +**Change (all `#ifdef PORT`, `#else` = original; no logic change).** +- front.c: `frontPrintText`, `display_aligned_white_text_to_screen` (+ forward decl) font params -> `struct fontchar *` / `struct font *`; `constructor_menu16_nocontrollers` `text` -> `char *`; `frontGetPlayersFavoriteWeaponInHand` returns `u16 *` (was `int`, also in `port/include/pc_protos.h`: the unprototyped `int` decl made mpmenu.c:1596 truncate the pointer). +- bondview.c/.h, bondview_internal.h, bondview2.c: `copy_1stfonttable`/`copy_2ndfonttable`, `setFontTables` params. +- options.c: 26 locals in 9 functions (pFontFile/pFontChars/*2, text locals, sp78/sp7C in draw_current_hand_item_and_ammo). NOTE the `spNN` names are per-function: `sp4C` is text in draw_abort_cancel_confirm but a colour in draw_text_mission_status, and sp50/sp4C are coordinates in draw_text_q_watch_v201_beta; retype by use, not by name. +- language.c: `g_LangBanks` -> `u32 *[45]`; `langGet` result built as `(u8 *)textbank_ptr + off` (was a `u32` accumulator, truncating >= 4 GiB). bondview.h: `player.ptr_text_first/second_mp_award` -> `char *` (live struct, never serialised; adds 4 bytes of pointer width, sizeof is taken from the type in player.c). +- mpmenu.c: `h2` (only receives langGet) -> `char *`; cast at the favourite-weapon assignment. +- propobj.c:3909: `!((s32)obj->model & 0x400)` -> `!(prop->chr->chrflags & CHRFLAG_HIDDEN)`. Prop type is VIEWER/CHR there and `obj`/`chr` share a union slot (bondtypes.h:2342-2344), so identical behaviour today (two layout shifts cancelled), now by name. + +**Left (not a type/cast swap, or out of scope).** mpmenu.c `q` (dual-use: team colour, then `langGet` string; needs a separate local); front.c `ptr_menu_videobuffer` and `modelSetAnimation(animation_table_ptrs1)`; language.c:364/375 `romCopy` p2 (ROM address, tag R); `sp70 = bondinvGetTextbyInvIndex` and the options.c:34 `OS_PHYSICAL_TO_K0` macro warning; JP/EU `dword_CODE_bss_jp...` font arrays (not built on ntsc-final). + +**Verified.** Falsifier held: front.c 186->2 (census predicted ~172 from the two params), options.c 139->0. Full ntsc-final build clean. Disassembly: font params read as QWORD stack slots, no `mov r32,r32` before pointer args; `langGet` adds at 64-bit. One default-boot launch (GE_PCDUMP 200-1400:200, GE_QUITFRAME=1450) ran rc=0, no crash log; frame 200 (legal screen, display_aligned_white_text_to_screen + textRender) renders all text correctly. No baseline-build frame diff was taken (budget); menu/watch/MP text paths not exercised at runtime. Confidence: [H] types/ABI, [M] runtime. + +## D453 — memp allocator API 64-bit clean (D441 step 0, ABI/layout exception, cross-tag §A1) + +**Scope.** `src/memp.c`, `src/memp.h` only. AGENTS.md rule-2 narrow exception (pointer-width; no logic or control-flow change). Every edit is under `#ifdef PORT`; the N64 arm is byte-identical. + +**Sites.** +- `mempCheckMemflagTokens(s32 poolAreaStart, s32 poolAreaSize)` -> `(uintptr_t poolAreaStart, s32 poolAreaSize)`; the pool-0 `start`/`end` are now assigned through explicit `(u8 *)` casts. (memp.h prototype matches.) +- `mempSetBankStarts`: `s32 mempStart` -> `uintptr_t` (was a sign-extending round trip of the `u8*` pool start); the per-bank `start`/`end` assignments cast `(u8 *)`. +- `mempAllocPackedBytesInBank` returned the allocation as `u32` (truncates the pointer); now returns `void *`. It has no callers in the tree today. +- Left `s32` on purpose: `poolAreaSize`, `bankstarts[]`, `mempLen`, `poolSizes` (sizes, not addresses). + +**Falsifier (written before the run).** The change altered behaviour if, on the spawn run, the startup log differs beyond version/config lines, OR a `-level_09` frame-400 `GE_PCDUMP` differs structurally (different geometry/textures/lighting, not just the intro-camera phase), OR the build gains a warning in memp.c. + +**Evidence.** `-Wpointer-to-int-cast -Wint-to-pointer-cast -Wint-conversion` over memp.c: 6 real warnings (lines 46, 47, 133, 137, 139, 290) -> 0 (5 header-only warnings unchanged). Build clean, no new warnings. `-level_09`, GE_QUITFRAME=420, before vs after: startup logs identical (modulo hash/config/timing lines); frame-400 dump same scene/textures/lighting but the intro camera is at a slightly different phase (3072x1728, dmean 12.1 - wall-clock camera motion, not a memp effect; no noise-floor rerun was in budget). **Confidence [M-H] on no behaviour change** (types only; all pool values < 2 GiB here so the value path is identical by construction). + +**Left, not fixed.** +- mema.c: `memaspace.addr` is `s32`, `g_MemaHeapStart` is `s32`, `memaReset` stores `(uintptr_t)heapaddr` into it, `memaAlloc` returns `(void *)` of an `s32`/`u32` addr (mema.c:317, 572), and `end1/end2.addr = 0xffffffff` plus `curr->addr == -1` sentinels. Widening needs `memaspace` layout + sentinel changes, which is more than a type/cast edit. Fine for an arena < 2 GiB; wrong above. Needs its own step (and a decision on the sentinels). +- boss.c:268 `(u32)tlbmanageGetTlbAllocatedBlock() - (u32)start` and `start` type: size diff stays correct only if both ends share the same 4 GiB window; boss.c was out of scope. memp.c callers that store `mempAllocBytesInBank` results into `s32` (bg.c `ptr_bg_data`, bondview_r.c `resolution`) are other census rows. + + +## D455 — census: exe-static addresses narrowed to 32 bits (D454 class) (ABI/layout exception, 2026-09-30) + +**Question.** D454 (knife slash) was an exe-`.data` address (Windows image base `0x140000000`) held in a `u32`. Are there other sites? + +**Method (binary-first).** `objdump -d -M intel` of the built exe, then a forward data-flow scan (`scratch` script, not committed): from every `lea/mov r64,[rip+...]` address load (incl. `.refptr.`), follow the register and its `mov r64,r64` copies for N instructions (following unconditional `jmp`, dropping the register on any overwrite or call), and flag (a) `mov r32,r32` same-register zero-extend, (b) `mov r32b,r32a` narrowing copy, (c) `mov DWORD PTR [..],r32` store. N=14 and N=60. + +**Falsifier (done before fixing).** Run on the PRE-fix binary (release/v0.4.1 @ 48908d18 has no D454 fix): the scan re-detects D454 as 4 hits in `gunTickHandState` (`.refptr.D_80034CA4` / `D_80034E0C`, all joining at `mov ecx,ecx` before `call gunSample1PTransform`; the load precedes a `jmp` join, so the scan has to follow jumps; a naive adjacent-instruction scan misses it). Whole-exe hit counts: 29 (N=14) / 39 (N=60). Rebuilt with the two D454 commits cherry-picked (temp branch, dropped): 25 / 35, 0 in `gunTickHandState`. The remaining hits were triaged one by one. + +**Triage of the remaining hits.** None is a REAL exe-static narrowing. +- OFFSET/ROM (benign): `_*SegmentEnd/RomStart/Start` are `.set` absolute constants in `port/src/romassets_u.s` (ROM addresses, not exe addresses): image_bank.c:251, initanitable.c:281, textrelated.c:123/146, music.c:686/709, romdata.c:268/275/372. `(s32)&ANIM_DATA_*` (title.c, initactorpropstuff.c) are D34 offsets. +- VALUE loads misflagged by the scan (register reloaded via a path the scan does not model, or a float/int value read from the global): bg.c:3232, bondview2.c:6512, chrprop.c:511, front.c:3424 (2 hits), image.c:2375, options.c:2049/4140, player.c:632, propobj.c:5376, spectrum.c:725, frontoptions.c:867, libultra.c:1457, fr.c:458, chr.c D_80030988/D_80030994, gfx_run_dl. +- Comparison-only narrowing (both sides truncated identically): chraction.c:2320/2324, zlib.c:312-441. Benign. +- Already guarded under `PORT`: bg.c:4238/4392, bondview2.c:10449, initplayergaitobject.c:16, model.c:1538 (in the `#else` arm), ramromreplay.c:225, initactorpropstuff.c ANIM_PTR, title.c:568. +- Not in the PC build: tlb_manage.c:134 (excluded; not in CMakeLists.txt). +- UNSURE (not exe-static, stack address): indy_comms.c:35 `(u32)&size`; dead on PC (`indy_ready`). +- Sites in the four files owned by another agent (front.c, options.c, propobj.c, language.c): only the value-load false positives above; no real site found. `language.c` produced no hit. + +**Limits.** Flow-limited (N instructions, no full CFG, no loads through memory: an address stored to a 64-bit slot then re-read as `u32` later is not seen); heap/stack pointers truncation is out of scope (heap is mapped low). Link-time narrowing in static initialisers cannot occur (the linker rejects a truncated reloc). **No code change**: the only real site was D454. **Confidence: [M]** that no other straight-line narrowing remains; the scan is a filter, not a proof. + +## D451 -- D427 follow-up: the auto-fire gate still counted frames, so an unsteady clock (1<->2 ticks/frame) gave the wrong cadence; D173 is not the same class + +**Mechanism (code reading + measurement).** `gunTickHandState` (`gunfire.c` ~2902) does `field_890 += g_ClockTimer` (tick accumulator) and `field_88C += 1` (frame counter) each call; both reset together on state entry. The automatic-fire gate was `88C % AutomaticFiringRate` (N64: one shot per `rate` frames, i.e. 2-3x`rate` ticks at 20-30 fps; the port at 1 tick/frame fired 2-3x too fast = D427). The D427 fix multiplied the modulus by `(g_ClockTimer >= 2 ? 1 : 2)` using the CURRENT frame's clock. That is exact only at a steady 1 or 2 ticks/frame; at mixed pacing the modulus flips between `rate` and `2*rate` frames against a counter that is not rescaled (the D427 "known limit"). +**Measured before (AK47 `GE_STARTWEAPON=8`, `-level_09`, `GE_INPUTSCRIPT=1000:ZHOLD`, `Video.FpsCap=45`, clock mix 1:307 / 2:153 frames):** 79 shots, every gap = 8 ticks (N64 rule at 2 ticks/frame = 6); i.e. the flip produces a systematic slow-down vs the console cadence, not just jitter. +**Fix (`#ifdef PORT`, `d427Gate`):** fire when a multiple of `P = 2*rate` ticks of `field_890` was crossed this call (`t1/P != (t1-clk)/P`), plus the existing `88C == 0` first-frame fire. Stateless, exact on average at any pacing; identical to the D427 result at steady 60 (P=6 ticks) and steady 30. +**Measured after (same run):** 90 shots, gaps 5/6/7 ticks (mean ~6 = 2*rate, quantised by the 1-2 tick frames). Residual +-1 tick jitter is inherent to frame quantisation. The probe log tick sum counts only gate-evaluated calls, so the 5-gaps are partly measurement alignment; the mean is the signal. +**Rule-2 bucket:** (c) game-logic, but the same gate the existing D427 sign-off covers. Quoted wording (D427 entry): "Fix (rule-2, user go-ahead 2026-09-29; `gunfire.c`, `#ifdef PORT`): the automatic-fire gate `88C % AutomaticFiringRate` now uses `rate * (g_ClockTimer >= 2 ? 1 : 2)` ... Only that gate changes." This edit replaces that one expression, same purpose, same gate, N64 branch byte-identical. Maintainer to confirm that wording extends to the tick-based formulation; if not, revert the `gunfire.c` commit. +**Grant (RULE-2-SIGNOFF).** Requested as "accept under the existing D427 sign-off, or treat as a new request"; scope = replace the one expression in the automatic-fire gate (same gate, same intent, identical result at steady 60/30 fps, N64 arm untouched). Maintainer reply 2026-09-30: "Proceed with option 1" (accept under the existing D427 sign-off). Resulting diff: `9d8b31b5` (`d427Gate`, `#ifdef PORT`). Still scoped to this one gate; a different behaviour in `gunfire.c` needs its own request. +**D173 (code analysis only):** NOT the same timing class as far as the evidence shows. Its measured defect is a constant ~324-329 unit rendered-root offset independent of animation and frame rate on the intro puppet (M-172/M-173), with an asset/transform hypothesis; its bulk was fixed by D243's `sizeof(Model)` layout fix and the user reports a ~1/10 small residual offset. No per-tick accumulator feeding a single floor/threshold was found on that path. Whether the residual shares the D13 class (`chrUpdateAnim` -> `modelTickAnim` -> one `modelSetAnimFrame2WithChrStuff` per frame, D329) is untested; the test is a `GE_D318B_CLK=1` vs `=2` A/B of the puppet's rendered offset. Low confidence on that negative. +**Confidence:** mechanism high; fix behaviour medium-high (one 45 fps run, headless); D173 negative low. +**By-eye test (maintainer):** AK47/Uzi held fire with `FpsCap` 60, 45 (or any unsteady load) and 30; empties the magazine in about the same wall time as 1964/GEPD at every setting; no stutter-bursts. +**Probes left in tree:** none (env-gated `GE_D427`/`GE_D427_OLD` probes stripped). + + +## D461 — `p_itemcur` allocated with the N64 stride; overran into the first prop model file (ABI/layout exception, 2026-09-30) + +**Symptom.** 2P (split-screen branch) on Caves: a crash at the first or second respawn, or on a body-armor pickup, in `modelUpdateRelationsQuick` / `modelInitRwData`. The fault address `0xffffffff7022cb4c` repeated across runs: the TT33 pistol model's root Child slot had its high half overwritten with 0xffffffff. + +**Cause.** `inititemslots.c` allocated `equipmaxitems * 0x14` bytes; `InvItem` is 32 bytes on PC (verified by a compile-time `_Static_assert`). `bondinvReinitInv` (`bondinv.c`) indexes `p_itemcur[i]` at the C stride and sets `.type = -1`, so the block was overrun by about 12 bytes per item (roughly 360-420 bytes at equipmaxitems 30-35) into the next MEMPOOL_STAGE allocation, which is the first prop model file loaded (TT33 on Caves). Solo levels run the same code, so this was likely corrupting something there too, silently. Not a converter or PROMOTE bug (all 340 P* models in `pcmodels.bin` were walked clean). + +**Fix.** `#ifdef PORT` allocates `equipmaxitems * sizeof(InvItem)`; the `#else` arm is the original. Commit `0ebda7e8`. No logic change. + +**Process.** The agent that found the corrupted node (analysis, D416-D448 branch) could not find the writer; a local Qwen read-only census of stage-bank allocations named `p_itemcur`, and the lead verified the 0x14 literal, `sizeof(InvItem) == 32`, and the write loop before applying. **Lesson (generalisable):** an allocation that multiplies a count by an N64 literal stride for a struct that grew on PC is invisible to a pointer-cast warning census; grep every `* 0x..` stride next to `mempAlloc*` and compare with `sizeof`. + +**Verified.** Maintainer: no crash in about 10 minutes of 2P on Caves, Bunker and Complex (respawns including rocket-launcher kills, body-armor pickups). **Confidence: [H].** Still worth auditing: the other `mempAlloc*` sites with literal strides. + + +## D462 — `vtxstore` tables allocated with the N64 stride (ABI/layout exception, 2026-09-30) + +**Cause.** `vtxstore.c` set `tmp = 0x14` and allocated `count * tmp` for two tables of `struct unk_09B7A0_struct_parent` (`Vertex* unk00; s32 unk04; s32 unk08; s16 unk0C, unk0E, unk10, unk12`). On PC the pointer widens it to 24 bytes (0x20 once D457(d) turns `unk04` into a pointer; that part is NOT merged). The init loop indexes the tables at the C stride (`...[i].unk0E = -1`), so each block was overrun by about 4 bytes per entry (about 2 KB at the 500-entry table size) into the next MEMPOOL_STAGE allocation. Silent corruption, the same class as D461. The Vertex arrays (`* 0x10`) are fine (Vertex is pinned to 16 bytes). + +**Fix.** `#ifdef PORT` uses `sizeof(struct unk_09B7A0_struct_parent)`; the `#else` arm is the original. Commit `fix/abi-vtxstore-alloc`. No logic change. D457(d) (unmerged, `fix/abi-bg-model-slots`) contained the same sizeof change plus the `unk04` pointer retype; this lands only the allocation part. + +**Found by.** The local-Qwen read-only audit of every `mempAlloc*` size expression (top suspects #1 and #2); the lead verified the literal at `vtxstore.c`, the C-stride indexing, and the D457(d) diff. + +**Verified.** Build clean; solo `-level_09` to frame 420 and a 4-player `GE_STARTMP=4 GE_MPVIRT=3` run to frame 500 both exit rc=0 with no crash log. **Confidence: [H]** on the overrun, [M] on any visible effect (none expected). Remaining audit suspects: `size_item_buffer` weapon model buffers (`initBondDATA.c`), `model.c` rwdata word count vs converted data, `image_bank.c` +0x1000 slack: all UNKNOWN, need a converted-size check. diff --git a/docs/porting-notes.md b/docs/porting-notes.md index f5120510..165ee69a 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -1297,6 +1297,7 @@ GE on N64 renders gameplay at ~20–30 fps, so `g_ClockTimer` (= `speedgraphfram - **Tell:** a state that holds forever at 60 fps, is frame-exact (integer frame, `fa == fb`), and has a per-tick step < 1.0 unit. - **Repro correctly:** force the granularity with `GE_D318B_CLK=N` (TEMP probe, lockstep N ticks/frame) and A/B N=1 vs N=2. Wall-clock runs on a slow host (WSLg renders ~5 fps → mixed clk 1–6) and `GE_DETERM=1` (advances per VI-retrace request, also mixed clk) both give uncalibrated results. - **Where else to look:** any `for/while (numticks)` accumulator followed by a single threshold/floor consumer; D193 (AI locomotion rate) and D243 (cutscene `numticks` bursts) touched the same axis from the other side. +- **Frame-counter gates (D427/D451):** a gate that reads the current `g_ClockTimer` to rescale a frame counter (`88C % (rate*(clk>=2?1:2))`) is only exact at steady pacing. Prefer the tick accumulator already kept beside the counter (`field_890`) and test for a threshold *crossing* (`t/P != (t-clk)/P`): stateless and exact on average at any pacing. ## D15. A texture-rectangle idiom that is sub-1-native-pixel tall relies on the RDP's floor-based fixed-point scanline stepping — now replicated in `gfx_draw_rectangle` (D397, RESOLVED 2026-09-28) @@ -1768,3 +1769,5 @@ A port stub that forces a value to `-1` (D154 `texnum`, D135) can hide latent la PATH, `/tmp`, `mktemp` and `cygpath` can belong to different runtimes (different Windows dirs). Never trust `command -v cmake` for the MSYS2 toolchain either: a pip-installed cmake can shadow it (2026-09-30). + +**A1 cross-tag (D455, 2026-09-30):** an exe-static address (array/global/function in `.data`/`.rodata`/`.text`) held in a `u32`/`s32` truncates on Windows (image base `0x140000000`; Linux 0x20000000 hides it). Binary tell: `mov r64,[rip+.refptr.X]` ... `mov r32,r32` before the pointer is used or passed. The address load may precede a `jmp` join, so an adjacent-instruction scan misses it; follow jumps. A census of the whole exe found only the D454 site; `_*Segment*` linker symbols are ROM constants (`.set` in `romassets_u.s`), not exe addresses, and are benign in `(u32)` casts. diff --git a/port/fast3d/gfx_pc.cpp b/port/fast3d/gfx_pc.cpp index 46449731..c3762e6b 100644 --- a/port/fast3d/gfx_pc.cpp +++ b/port/fast3d/gfx_pc.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -763,32 +762,6 @@ static void import_texture_rgba16(int tile, const LoadedTexture& loaded_texture, const uint32_t width = rdp.texture_tile[tile].line_size_bytes / 2; const uint32_t height = size_bytes / rdp.texture_tile[tile].line_size_bytes; -#ifdef PORT - /* D252 diag (temporary): compare the line_size_bytes-derived width/height - * against the SETTILESIZE-derived rdp.texture_tile[tile].width/height, and - * dump the raw texel data once per distinct (addr,size) pair so a fire - * tile can be checked against the source bytes by hand. Remove once - * D252 is resolved. */ - static int s_d252 = -1; - if (s_d252 < 0) s_d252 = getenv("GE_D252") != NULL; - if (s_d252) { - static std::set> s_d252_seen; - if (s_d252_seen.emplace((const void*)addr, size_bytes).second) { - const auto& t = rdp.texture_tile[tile]; - fprintf(stderr, - "[D252] tile=%d addr=%p size_bytes=%u line_size_bytes=%u " - "computed(w=%u h=%u) settilesize(w=%u h=%u) uls=%u ult=%u lrs=%u lrt=%u " - "masks=%u maskt=%u tmem=%u\n", - tile, (const void*)addr, size_bytes, line_size_bytes, width, height, - t.width, t.height, t.uls, t.ult, t.lrs, t.lrt, t.masks, t.maskt, t.tmem); - char rawpath[256]; - snprintf(rawpath, sizeof(rawpath), "scratch/d252_raw_%p_%u.bin", (const void*)addr, size_bytes); - FILE* rf = fopen(rawpath, "wb"); - if (rf) { fwrite(addr, 1, size_bytes, rf); fclose(rf); } - } - } -#endif - gfx_rapi->upload_texture(tex_upload_buffer, width, height, gen_mipmaps); // DumpTexture(loaded_texture.otr_path, rgba32_buf, width, height); } @@ -1129,10 +1102,8 @@ static void import_texture(int i, int tile, bool importReplacement) { * die_blood_image_routine -- reuse the same addresses frame after frame, * so an address-only key returns a stale GL texture from an earlier frame. * Key those on a content hash (FNV-1a) instead; static textures keep the - * free address key. GE_DYNTEXHASH_OFF=1 disables for A/B. */ + * free address key. */ { - static int dynhash_off = -1; - if (dynhash_off < 0) dynhash_off = getenv("GE_DYNTEXHASH_OFF") != NULL; /* Game code often reaches this memory through OS_K0_TO_PHYSICAL, * which fast3d resolves into the byte-identical KSEG0 mirror at * 0x80000000 (port/src/dram.c V2) -- normalise to the V1 view the @@ -1141,7 +1112,7 @@ static void import_texture(int i, int tile, bool importReplacement) { if ((uintptr_t)v1addr >= 0x80000000UL && (uintptr_t)v1addr < 0x80800000UL) { v1addr -= 0x10000000UL; } - if (!dynhash_off && g_VtxBuffers[0] && v1addr >= g_VtxBuffers[0] && v1addr < g_VtxBuffers[2]) { + if (g_VtxBuffers[0] && v1addr >= g_VtxBuffers[0] && v1addr < g_VtxBuffers[2]) { uint32_t h = 2166136261u; const uint32_t n = loaded_texture.size_bytes; for (uint32_t b = 0; b < n; b++) { diff --git a/port/include/pc_protos.h b/port/include/pc_protos.h index 2bccf078..965aba88 100644 --- a/port/include/pc_protos.h +++ b/port/include/pc_protos.h @@ -211,7 +211,7 @@ void free(); * truncated to int (crashed configGetFrameDump on Linux). */ char * getenv(); void * realloc(); -int frontGetPlayersFavoriteWeaponInHand(); +u16 * frontGetPlayersFavoriteWeaponInHand(); void generate_player_thrown_grenade(); void generate_player_thrown_knife(); void generate_player_thrown_object(); diff --git a/port/src/audio.c b/port/src/audio.c index 7b136049..b1336947 100644 --- a/port/src/audio.c +++ b/port/src/audio.c @@ -72,20 +72,6 @@ static u32 lastBufferBytes = 0; static u32 dropCount = 0; static u32 oversizeCount = 0; -/* D204 diag (temporary): GE_D204_OLD=1 restores the pre-fix behaviour -- - * osAiGetLength() reports the whole SDL queue depth and queueLimit goes back - * to 8192 -- so before/after can be measured in ONE binary, without - * build-to-build variance. Remove once D204 is closed. */ -static int d204OldMode(void) -{ - static int cached = -1; - if (cached < 0) { - cached = getenv("GE_D204_OLD") ? 1 : 0; - if (cached) queueLimit = 8192; - } - return cached; -} - int audioInit(void) { #if defined(_WIN32) && defined(SDL_HINT_AUDIODRIVER) @@ -159,10 +145,6 @@ u32 audioGetAiLengthBytes(void) { u32 queued = dev ? SDL_GetQueuedAudioSize(dev) : 0; - if (d204OldMode()) { - return queued; /* pre-fix behaviour, for A/B measurement only */ - } - /* D204/F5 -- shift the regulator's setpoint off zero. THIS is the part * that measurably changes behaviour. * @@ -202,8 +184,6 @@ void audioSetNextBuffer(const s16 *buf, u32 len) s_audioDumpFile = fopen("audiodump.raw", "wb"); } } - (void)d204OldMode(); /* D204 diag: ensure the queueLimit override applies */ - /* D204 audio-health monitor: GE_D204=1 prints one line every 5 s of wall * clock with everything needed to see the pipeline degrade in real time: * diff --git a/port/src/d318watchdog.c b/port/src/d318watchdog.c index 8b517fbd..dfe50c5e 100644 --- a/port/src/d318watchdog.c +++ b/port/src/d318watchdog.c @@ -351,62 +351,8 @@ void d318WatchdogTick(void) /* derailed state (random aim-variant endframe vs the hold frame H). */ /* ------------------------------------------------------------------ */ -/* Manual hex parse: MinGW's strtoull returns the sign-extended low 32 bits - * for values above LLONG_MAX (verified: "deadbeefcafe0123" -> - * ffffffffcafe0123). Parse by hand; stops at '@' or non-hex. */ -static u64 d318wParseHex(const char *s) -{ - u64 v = 0; - - while (*s && *s != '@') - { - char c = *s++; - - v <<= 4; - if (c >= '0' && c <= '9') { v |= (u64)(c - '0'); } - else if (c >= 'a' && c <= 'f') { v |= (u64)(c - 'a' + 10); } - else if (c >= 'A' && c <= 'F') { v |= (u64)(c - 'A' + 10); } - else { break; } - } - return v; -} - void d318TimelineTick(void) { - /* E-B seed injection: GE_RSEED_LV=[@tick] -- set g_randomSeed to - * the value read from N64 RAM (0x80024460) at a canonical moment, so the - * port's stream can be A/B-compared against the original from that point - * on. Test-only; inert without the env var. The DET line logs the running - * seed every 60 ticks for verifying stream parity afterwards. */ - { - static int s_seedArmed = 1; - static u64 s_seedVal = 0; - static s32 s_seedTick = 0; - - if (s_seedArmed) - { - const char *e = getenv("GE_RSEED_LV"); - - s_seedArmed = 0; - if (e && e[0]) - { - const char *at = strchr(e, '@'); - - s_seedVal = d318wParseHex(e); /* MinGW strtoull mangles >LLONG_MAX */ - s_seedTick = at ? (s32)strtol(at + 1, NULL, 10) : 0; - } - } - if (s_seedVal && (s32)g_GlobalTimer >= s_seedTick) - { - u64 v = s_seedVal; - - g_randomSeed = v; - s_seedVal = 0; - osSyncPrintf("D318T: t=%d RSEED_LV -> %016llx\n", - (int)g_GlobalTimer, (unsigned long long)v); - } - } - static int s_on = -1; /* -1 uncached, 0 off, 1 on (GE_D318T=1) */ static s32 s_obj = -1; /* last logged objectiveregisters1 */ static s32 s_off = -1; /* last logged c78 aioffset */ diff --git a/port/src/frontoptions.c b/port/src/frontoptions.c index b4d6e1a2..0b5546c9 100644 --- a/port/src/frontoptions.c +++ b/port/src/frontoptions.c @@ -53,8 +53,8 @@ extern Gfx *frontSetupMenuBackground(Gfx *DL); extern Gfx *frontAddPreviousTabText(Gfx *DL); extern s32 frontCheckCursorOnPreviousTab(void); extern Gfx *frontDrawCursor(Gfx *DL); -extern Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table, - s32 first_font_table, s32 arg6, s32 view_x, s32 view_y, +extern Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, struct fontchar *second_font_table, + struct font *first_font_table, s32 arg6, s32 view_x, s32 view_y, s32 arg9, s32 arga); extern void load_walletbond(void); extern void disable_all_switches(Model *arg0); /* front.c:967 */ @@ -152,8 +152,8 @@ static s32 measureW(const char *str) static Gfx *ink(Gfx *DL, s32 x, s32 y, const char *str, u32 colour) { - return frontPrintText(DL, &x, &y, (s8 *)str, (s32)(uintptr_t)ptrFontZurichBoldChars, - (s32)(uintptr_t)ptrFontZurichBold, (s32)colour, + return frontPrintText(DL, &x, &y, (s8 *)str, ptrFontZurichBoldChars, + ptrFontZurichBold, (s32)colour, viGetX(), viGetY(), 0, 0); } diff --git a/port/src/video.c b/port/src/video.c index 6870467b..fd78341a 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -403,7 +403,7 @@ f32 portRoomPoolScale(void) /* Test hook: values < 1.0 shrink the pool BELOW the authored size * to stress-test the exhaustion path (D294 verification). */ scale = (f32)atof(ov); - if (scale > 2.0f) { scale = 2.0f; } + if (scale > 2.5f) { scale = 2.5f; } if (scale < 0.25f) { scale = 0.25f; } return scale; } @@ -423,14 +423,21 @@ f32 portRoomPoolScale(void) * -level_22 boots stayed clean headless). Reverted to sqrt + 2.0 cap, * the user-verified working state. Do not re-apply without the capture-bat * repro data (scratch/d294_capture.bat -> D156 hexdump + GE_D294BANK). */ - t = sqrtf(portDrawDistanceMultiplier()); + t = powf(portDrawDistanceMultiplier(), 0.66f); /* D294 2026-09-30: was sqrtf; 4^0.66 = 2.5 */ if (t > scale) { scale = t; } - t = sqrtf(1.0f / portLodDistanceMultiplier()); + t = powf(1.0f / portLodDistanceMultiplier(), 0.66f); if (t > scale) { scale = t; } - if (scale > 2.0f) { scale = 2.0f; } + /* D294 (2026-09-30): cap 2.0 -> 2.5 so Statue's full 27-room set (487 KB + * = 2.17x its 225 KB authored pool) fits at max DD/Lod. The old 3x + * "regression" was D336 (uninitialised head-anim fields), not pool size. + * Worst case -ma350 row -> 875 KB; boss.c still clamps to STAGE bank - 128 KB. */ + if (scale > 2.5f) { scale = 2.5f; } if (scale < 1.0f) { scale = 1.0f; } + /* boss.c truncates scale to quarter steps ((s64)(scale*4)); round UP to a + * quarter here so 4^0.66 = 2.497 lands on 2.5 instead of truncating to 2.25. */ + scale = ceilf(scale * 4.0f - 0.01f) / 4.0f; return scale; } diff --git a/src/game/bg.c b/src/game/bg.c index 9f813262..8e9ad996 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -67,7 +67,7 @@ enum GlobalVisOpcode { }; extern struct unk_portalstruct table_for_portals[PORTMAX]; -extern s32 ptr_bgdata_offsets; +extern bg_addr_t ptr_bgdata_offsets; extern s32 dword_CODE_bss_8007FF88; extern s32 *dword_CODE_bss_8007FF90; extern f32 *dword_CODE_bss_8007FF94; @@ -92,10 +92,10 @@ extern s32 dword_CODE_bss_800815f8; // bss //CODE.bss:8007BF90 -s32 ptr_bg_data; +bg_addr_t ptr_bg_data; /* D457: host pointer (was s32) */ //CODE.bss:8007BF94 -s32 gptr_stan; +bg_addr_t gptr_stan; /* D457: host pointer (was s32) */ /** * address 8007BF98 @@ -879,7 +879,7 @@ void load_bg_file(LEVEL_INDEX levelid) lightFixtureInitTables(); - ptr_bg_data = (s32)header; + ptr_bg_data = (bg_addr_t)header; obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, (u8 *) ptr_bg_data, 0, 0x40); if (((levelid && ptr_bg_data) && levelentry_index)); @@ -889,10 +889,10 @@ void load_bg_file(LEVEL_INDEX levelid) size = (((((u32) ptr_bgdata_room_fileposition_list[1].pPointTableBin) & 0x00ffffff) - 1) | 0xf) + 1; - ptr_bg_data = (s32) mempAllocBytesInBank(size, 4); + ptr_bg_data = (bg_addr_t) mempAllocBytesInBank(size, 4); obLoadBGFileBytesAtOffset(levelinfotable[levelentry_index].bg_seg_filename, (u8 *) ptr_bg_data, 0, size); - gptr_stan = (s32) _fileNameLoadToBank(levelinfotable[levelentry_index].bg_stan_filename, 2, 0, 4); + gptr_stan = (bg_addr_t) _fileNameLoadToBank(levelinfotable[levelentry_index].bg_stan_filename, 2, 0, 4); stanDetermineEOF((struct StanPrefixRecord *) gptr_stan, 0, (u8 *) gptr_stan); stanLoadFile((struct StanPrefixRecord *) gptr_stan); @@ -914,7 +914,7 @@ void load_bg_file(LEVEL_INDEX levelid) if (dword_CODE_bss_8007BF98 == 0) { dword_CODE_bss_8007FF88 = 2; - ptr_bgdata_offsets = (s32)data; + ptr_bgdata_offsets = (bg_addr_t)data; ptr_bgdata_room_fileposition_list = (bg_room_data *) BG_SEG_TO_PTR(data, ((s32 *)ptr_bgdata_offsets)[1]); // Keep this fake goto for matching. @@ -1923,7 +1923,7 @@ char *bgDebPrintROOMID(s32 roomId) bg_queued_portal_entry g_BgPortalQueue[BG_PORTAL_QUEUE_LEN]; bg_portal_data_entry *g_BgPortals; -s32 ptr_bgdata_offsets; +bg_addr_t ptr_bgdata_offsets; s32 dword_CODE_bss_8007FF88; bg_room_data *ptr_bgdata_room_fileposition_list; s32 *dword_CODE_bss_8007FF90; diff --git a/src/game/bg.h b/src/game/bg.h index 71012a97..a8fd646c 100644 --- a/src/game/bg.h +++ b/src/game/bg.h @@ -20,7 +20,16 @@ struct levelentry // cannonical name #define PORTMAX 200 +#ifdef PORT +/* D457: keep the base at pointer width. The N64 form wraps the whole sum mod 2^32 + * (`(u32)base + (off + 0xF1000000)`), i.e. base + (off - 0x0F000000) with a + * signed delta; do the delta in s32 and add it to the full-width base. */ +#define BG_SEG_TO_PTR(base, off) ((void *) ((uintptr_t) (base) + (uintptr_t) (intptr_t) (s32) ((u32) (uintptr_t) (off) + 0xF1000000u))) +typedef u8 *bg_addr_t; +#else #define BG_SEG_TO_PTR(base, off) ((void *) (((u32) (base)) + (((u32) (off)) + 0xF1000000))) +typedef s32 bg_addr_t; +#endif typedef struct RoomVtxBatchBounds { s16 gdlindex; // 0x00 diff --git a/src/game/bondview.c b/src/game/bondview.c index acfea50e..2bd6e42f 100644 --- a/src/game/bondview.c +++ b/src/game/bondview.c @@ -492,10 +492,18 @@ s32 status_bar_text_buffer_index = 0; s32 display_statusbar = 0; #ifdef BUGFIX_R0 //D:800368A0 +#ifdef PORT +struct font *copy_1stfonttable = NULL; +#else s32 copy_1stfonttable = 0; +#endif //D:800368A4 +#ifdef PORT +struct fontchar *copy_2ndfonttable = NULL; +#else s32 copy_2ndfonttable = 0; #endif +#endif //D:800368A8 s32 upper_text_buffer_index = 0; //D:800368AC diff --git a/src/game/bondview.h b/src/game/bondview.h index c545e11d..ffc85ce9 100644 --- a/src/game/bondview.h +++ b/src/game/bondview.h @@ -2360,8 +2360,13 @@ struct player s16 registeredroom; f32 field_2A08; f32 field_2A0C; +#ifdef PORT + char *ptr_text_first_mp_award; + char *ptr_text_second_mp_award; +#else s32 ptr_text_first_mp_award; s32 ptr_text_second_mp_award; +#endif coord3d field_2A18[2]; s32 field_2A30; s32 field_2A34; @@ -2699,9 +2704,17 @@ extern s32 status_bar_text_buffer_index; //D:8003689C extern s32 display_statusbar; //D:800368A0 +#ifdef PORT +extern struct font *copy_1stfonttable; +#else extern s32 copy_1stfonttable; +#endif //D:800368A4 +#ifdef PORT +extern struct fontchar *copy_2ndfonttable; +#else extern s32 copy_2ndfonttable; +#endif //D:800368A8 extern s32 upper_text_buffer_index; //D:800368AC @@ -2818,8 +2831,12 @@ void jp_hudmsgBottomShow(char *string); // VERSION_US #define HUDMESSAGEBOTTOM hudmsgBottomShow void hudmsgBottomShow(char *string); +#ifdef PORT +void setFontTables(struct fontchar *arg0, struct font *arg1); +#else void setFontTables(s32 arg0, s32 arg1); #endif +#endif Gfx * bondviewRenderDebugBondView(Gfx *arg0); s32 bond_pressed_reload_activate(void); diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 9657af32..5c8118b5 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -10239,7 +10239,11 @@ void hudmsgsSetOff(s32 flags) #ifdef VERSION_US +#ifdef PORT +void setFontTables(struct fontchar *arg0, struct font *arg1) +#else void setFontTables(s32 arg0, s32 arg1) +#endif { copy_2ndfonttable = arg0; copy_1stfonttable = arg1; diff --git a/src/game/bondview_internal.h b/src/game/bondview_internal.h index e83a09c8..221414e4 100644 --- a/src/game/bondview_internal.h +++ b/src/game/bondview_internal.h @@ -130,9 +130,17 @@ extern coord3d ZeroCoordSpawnPos; extern s32 status_bar_text_buffer_index; extern s32 display_statusbar; #ifdef BUGFIX_R0 +#ifdef PORT +extern struct font *copy_1stfonttable; +#else extern s32 copy_1stfonttable; +#endif +#ifdef PORT +extern struct fontchar *copy_2ndfonttable; +#else extern s32 copy_2ndfonttable; #endif +#endif extern s32 upper_text_buffer_index; extern s32 display_upper_text_window; extern s32 upper_text_window_timer; diff --git a/src/game/front.c b/src/game/front.c index 1037ab3d..e4d42254 100644 --- a/src/game/front.c +++ b/src/game/front.c @@ -968,14 +968,22 @@ Gfx *constructor_menu19_spectrum(Gfx *DL); void disable_all_switches(Model *arg0); void set_item_visibility_in_objinstance(Model* objinstance, s32 item, s32 mode); void set_cursor_to_stage_solo(LEVEL_SOLO_SEQUENCE level); +#ifdef PORT +Gfx *display_aligned_white_text_to_screen(Gfx *dl, s32 arg1, s32 arg2, s32 halign, s32 valign, u8 *arg5, struct fontchar *arg6, struct font *arg7); +#else Gfx *display_aligned_white_text_to_screen(Gfx *dl, s32 arg1, s32 arg2, s32 halign, s32 valign, u8 *arg5, s32 arg6, s32 arg7); +#endif void setCursorPOSforMode(int mode); void set_cursor_pos_difficulty(int difficulty); // end forward declarations. +#ifdef PORT +Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, struct fontchar *second_font_table, struct font *first_font_table, s32 arg6, s32 view_x, s32 view_y, s32 arg9, s32 arga) +#else Gfx *frontPrintText(Gfx *gdl, s32 *x, s32 *y, s8 *text, s32 second_font_table, s32 first_font_table, s32 arg6, s32 view_x, s32 view_y, s32 arg9, s32 arga) +#endif { if (j_text_trigger != 0) { @@ -1146,7 +1154,11 @@ s32 frontCheckIfCheatIsUnlocked(s32 cheat) } +#ifdef PORT +u16 *frontGetPlayersFavoriteWeaponInHand(int player,int hand) { +#else int frontGetPlayersFavoriteWeaponInHand(int player,int hand) { +#endif return get_ptr_long_watch_text_for_item(array_favweapon[player][hand]); } @@ -1514,7 +1526,11 @@ void interface_menu00_legalscreen(void) /** * Render a string of standard text on the legal screen e.g. "TWYCROSS BOARD OF GAME CLASSIFICATION" */ +#ifdef PORT +Gfx *display_aligned_white_text_to_screen(Gfx *dl, s32 arg1, s32 arg2, s32 halign, s32 valign, u8 *text, struct fontchar *arg6, struct font *arg7) +#else Gfx *display_aligned_white_text_to_screen(Gfx *dl, s32 arg1, s32 arg2, s32 halign, s32 valign, u8 *text, s32 arg6, s32 arg7) +#endif { s32 sp4C; s32 sp48; @@ -7490,7 +7506,11 @@ Gfx *constructor_menu0D_missioncomplete(Gfx *DL) x = 0x37; y = 0xDC; DL = frontPrintText(DL, &x, &y, text, ptrFontZurichBoldChars, ptrFontZurichBold, 0xFF, viGetX(), viGetY(), 0, 0); +#ifdef PORT + strcpy(stagename, (char *)frontGetPlayersFavoriteWeaponInHand(0, 0)); +#else strcpy(stagename, frontGetPlayersFavoriteWeaponInHand(0, 0)); +#endif if ((array_favweapon[0][0] > 0) && (array_favweapon[0][1] == array_favweapon[0][0])) { temp = strlen(stagename)-1; @@ -7832,7 +7852,11 @@ Gfx *constructor_menu16_nocontrollers(Gfx *DL) s32 x2; s32 x; s32 y; +#ifdef PORT + char *text; +#else s32 text; +#endif s32 conConnected = joyGetConnectedControllers(); s32 numContCon = 0; diff --git a/src/game/gunfire.c b/src/game/gunfire.c index 1be1401a..d5b838a4 100644 --- a/src/game/gunfire.c +++ b/src/game/gunfire.c @@ -2717,6 +2717,24 @@ void sub_GAME_7F0649D8(enum GUNHAND hand) /** * Address: 7F064B28 */ +#ifdef PORT +/* D451 (D427 follow-up): automatic-fire gate measured in TICKS, not frames. + * The D427 fix scaled the modulus by (g_ClockTimer >= 2 ? 1 : 2) and kept the + * frame counter field_88C, so at an unsteady frame rate (clock flipping 1<->2) + * the window flips between rate and 2*rate frames and shots fire early/jittery. + * field_890 is the tick accumulator (+= g_ClockTimer per call, reset together + * with field_88C), so "a multiple of P ticks was crossed this call" is stateless + * and exact on average at any pacing: P = 2 ticks * rate = what the console's + * 2-tick frames gave. First frame (88C == 0) still fires, as before. */ +static int d427Gate(struct hand *h, s32 rate) +{ + s32 P = rate * 2; + s32 t1 = (s32) h->field_890; + s32 t0 = t1 - g_ClockTimer; + return (h->field_88C == 0) || (P > 0 && (t1 / P) != (t0 / P)); +} +#endif + void gunTickHandState(enum GUNHAND hand, s32 triggerOn) { #if defined(VERSION_US) @@ -2762,7 +2780,11 @@ void gunTickHandState(enum GUNHAND hand, s32 triggerOn) struct PropRecord *temp_v0_8; Weapon1PTransformKeyframe *sp74; f32 temp_f0_2; +#ifdef PORT + Weapon1PTransformKeyframe *var_a0_2; /* D454: see EU arm */ +#else u32 var_a0_2; +#endif f32 temp_v1_9; struct hand *temp_v1_5; f32 un_f32_num = 0.0f; @@ -2818,7 +2840,11 @@ void gunTickHandState(enum GUNHAND hand, s32 triggerOn) Weapon1PTransformKeyframe *sp74; struct PropRecord *temp_v0_8; f32 temp_f0_2; +#ifdef PORT + Weapon1PTransformKeyframe *var_a0_2; /* D454: see EU arm */ +#else u32 var_a0_2; +#endif f32 temp_v1_9; struct hand *temp_v1_5; f32 un_f32_num = 0.0f; @@ -2873,7 +2899,14 @@ void gunTickHandState(enum GUNHAND hand, s32 triggerOn) Weapon1PTransformKeyframe *sp74; struct PropRecord *temp_v0_8; f32 temp_f0_2; +#ifdef PORT + /* D454: was u32, which truncates the exe-data address of D_80034CA4/E0C + * (PE image base 0x140000000) before gunSample1PTransform; knife slash + * crashed on Windows. Pointer-width ABI fix only. */ + Weapon1PTransformKeyframe *var_a0_2; +#else u32 var_a0_2; +#endif f32 temp_v1_9; struct hand *temp_v1_5; f32 un_f32_num = 0.0f; @@ -3232,7 +3265,7 @@ void gunTickHandState(enum GUNHAND hand, s32 triggerOn) * Only this gate changes: 88C itself and every other 88C==0 * "first frame" gate stay per-frame, and animation time * (field_890) stays tick-scaled. */ - if (((s32) handptr->field_88C % (bondwalkItemGetAutomaticFiringRate(var_s1) * ((g_ClockTimer >= 2) ? 1 : 2))) == 0) + if (d427Gate(handptr, bondwalkItemGetAutomaticFiringRate(var_s1))) #else if (((s32) handptr->field_88C % bondwalkItemGetAutomaticFiringRate(var_s1)) == 0) #endif @@ -4120,11 +4153,19 @@ void gunTickHandState(enum GUNHAND hand, s32 triggerOn) || (handptr->weapon_action_state == GUN_ANIM_STATE_KNIFE_SLASH1_STRIKE) || (handptr->weapon_action_state == GUN_ANIM_STATE_KNIFE_SLASH1_RECOVER)) { +#ifdef PORT + var_a0_2 = (Weapon1PTransformKeyframe *) D_80034CA4; +#else var_a0_2 = D_80034CA4; +#endif } else { +#ifdef PORT + var_a0_2 = (Weapon1PTransformKeyframe *) D_80034E0C; +#else var_a0_2 = D_80034E0C; +#endif } if (gunSample1PTransform(var_a0_2, sp88, &handptr->field_8EC, hand) != 0) @@ -6216,9 +6257,17 @@ Gfx *generate_ammo_total_microcode(Gfx *gdl) s32 rightx; s32 reserveammo; s32 magammo; +#ifdef PORT + uintptr_t imageoffset_r; +#else u32 imageoffset_r; +#endif s32 textwidth_r; +#ifdef PORT + uintptr_t imageoffset_l; +#else u32 imageoffset_l; +#endif s32 textwidth_l; if (g_CurrentPlayer->gunammooff == 0) @@ -6407,7 +6456,11 @@ Gfx *gunDrawWatchAmmoDisplay(Gfx *gdl) s32 ammotype; s32 reserveammo; s32 magammo; +#ifdef PORT + uintptr_t imageoffset; +#else u32 imageoffset; +#endif s32 textwidth; s32 pad; diff --git a/src/game/image_bank.c b/src/game/image_bank.c index f9007415..8b067191 100644 --- a/src/game/image_bank.c +++ b/src/game/image_bank.c @@ -16,7 +16,14 @@ s32 img_bitcount; //8008D0AC s32 dword_CODE_bss_8008D0AC; //8008D0B0; +#ifdef PORT +/* D457: holds (base - 0x02000000) as a pointer-width value; adding an N64 + * segmented image address (0x02000000 + off) yields the real pointer. The N64 + * arm's 32-bit wrap trick (+0xFE000000) is only valid mod 2^32. */ +uintptr_t globalbank_rdram_offset; +#else s32 globalbank_rdram_offset; +#endif //8008D0B4; s32 *pGlobalimagetable; //8008D0B8; @@ -257,7 +264,11 @@ void texReset(void) gimgFixupGlobalimagetable((u8 *)pGlobalimagetable); #endif +#ifdef PORT + globalbank_rdram_offset = (uintptr_t)pGlobalimagetable - 0x02000000u; +#else globalbank_rdram_offset = (u32)pGlobalimagetable + 0xFE000000; +#endif genericimage = (void *) (globalbank_rdram_offset + GIMG_OFF(s_genericimage)); impactimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_impactimages)); explosion_smokeimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_explosion_smokeimages)); @@ -291,23 +302,23 @@ void texReset(void) mpcharselimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_mpcharselimages)); mpstageselimages = (void *) (globalbank_rdram_offset + GIMG_OFF(s_mpstageselimages)); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x000), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x078), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x120), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x1c8), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x270), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x318), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x3c0), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x468), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x510), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x5b8), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x660), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x708), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x7b0), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x858), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x900), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0x9a8), 0); - texLoadFromDisplayList(globalbank_rdram_offset + GIMG_OFF(globalDL_0xa50), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x000)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x078)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x120)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x1c8)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x270)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x318)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x3c0)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x468)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x510)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x5b8)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x660)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x708)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x7b0)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x858)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x900)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0x9a8)), 0); + texLoadFromDisplayList((Gfx *)(globalbank_rdram_offset + GIMG_OFF(globalDL_0xa50)), 0); texLoad(genericimage, 0); diff --git a/src/game/image_bank.h b/src/game/image_bank.h index 85ffa545..c3f891c8 100644 --- a/src/game/image_bank.h +++ b/src/game/image_bank.h @@ -25,7 +25,11 @@ extern struct sImageTableEntry *flareimage5; extern u8* img_curpos; extern s32 img_bitcount; extern s32 *pGlobalimagetable; +#ifdef PORT +extern uintptr_t globalbank_rdram_offset; +#else extern s32 globalbank_rdram_offset; +#endif void texReset(void); u32 texReadBits(s32 bitCount); diff --git a/src/game/inititemslots.c b/src/game/inititemslots.c index fd7c6a59..d590746a 100644 --- a/src/game/inititemslots.c +++ b/src/game/inititemslots.c @@ -16,6 +16,14 @@ void reinit_gunheld_totaltime(void) { void alloc_additional_item_slots(s32 additionalentries) { g_CurrentPlayer->equipmaxitems = additionalentries + 0x1e; +#ifdef PORT + /* D458b: InvItem is 32 bytes on PC (pointer-bearing union and next/prev widened) but the + * N64 stride is 0x14. bondinvReinitInv() indexes p_itemcur[i] at the C stride, so the + * 0x14 allocation was overrun by ~12 bytes per item, scribbling type=-1 into the next + * MEMPOOL_STAGE allocation (the first prop model file: TT33 root Child). ABI/layout fix. */ + g_CurrentPlayer->p_itemcur = mempAllocBytesInBank((g_CurrentPlayer->equipmaxitems * (s32)sizeof(InvItem) + 0xfU | 0xf) ^ 0xf, MEMPOOL_STAGE); +#else g_CurrentPlayer->p_itemcur = mempAllocBytesInBank((g_CurrentPlayer->equipmaxitems * 0x14 + 0xfU | 0xf) ^ 0xf, MEMPOOL_STAGE); +#endif bondinvReinitInv(); } diff --git a/src/game/language.c b/src/game/language.c index cfdac343..daa75ad3 100644 --- a/src/game/language.c +++ b/src/game/language.c @@ -23,7 +23,11 @@ static void langFixupLoadedBank(char *name, void *p) // bss //CODE.bss:8008C640 +#ifdef PORT +u32 *g_LangBanks[45]; +#else s32 g_LangBanks[45]; +#endif //CODE.bss:8008C6F4 @@ -419,7 +423,7 @@ u8 * langGet(s32 slotID) #endif u32 * textbank_ptr = g_LangBanks[slotID >> 10]; /* get the text file bank ID index the text ptr table */ #ifdef PORT - /* D129 cont.: g_LangBanks[] is s32 and only populated for banks the + /* D129 cont.: g_LangBanks[] holds pointer-width entries (u32* under PORT) and is only populated for banks the * current flow has loaded. A bare `-level_XX` boot that reaches the * cast/credits text path (Cuba, bondviewRenderCredits, D76) hits a bank * slot that was never filled -> stale/garbage non-NULL value -> fault on @@ -445,8 +449,13 @@ u8 * langGet(s32 slotID) #endif u32 textslot_offset = textbank_ptr[slotID & 0x03FF]; /* load the textbank ptr table then get the slot's offset */ +#ifdef PORT + /* D456: rebuild the result at pointer width; a u32 here truncates any arena address >= 4 GiB. */ + u8 *output_slot = (u8 *)textbank_ptr + textslot_offset; +#else u32 output_slot = textslot_offset; /* add the text slot offset to the base ptr to get the ptr to text file's slot */ output_slot += (u32)textbank_ptr; +#endif #ifdef DEBUG return (textslot_offset != 0) ? (u8 *)output_slot : "Sorry, string not loaded."; #endif diff --git a/src/game/lv.c b/src/game/lv.c index b6db8a84..8f6e11ef 100644 --- a/src/game/lv.c +++ b/src/game/lv.c @@ -433,7 +433,12 @@ void lvlStageLoad(s32 stage) * so saves[] matches what the legal screen would have * produced. */ extern s32 portSkipIntro; - if (portSkipIntro) { + /* D408: boot only. This block runs on EVERY title-stage load, + * including the return from a mission; forcing file-select + * there overwrote menu_update and skipped the post-mission + * failure dossier. is_first_time_on_main_menu is TRUE only + * until the first pass through here (cleared below). */ + if (portSkipIntro && is_first_time_on_main_menu) { extern void fileValidateSaves(void); fileValidateSaves(); is_first_time_on_main_menu = FALSE; diff --git a/src/game/mpmenu.c b/src/game/mpmenu.c index 1e60719a..bcce9e28 100644 --- a/src/game/mpmenu.c +++ b/src/game/mpmenu.c @@ -1060,7 +1060,11 @@ Gfx *mp_watch_menu_display(Gfx *gdl) s32 textheight; s32 m; s32 h1; +#ifdef PORT + char *h2; +#else s32 h2; +#endif char rankbuffer[4]; s32 two_player_x_offset; char *text; @@ -1406,7 +1410,11 @@ Gfx *mp_watch_menu_display(Gfx *gdl) q = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1C_P)); /* P */ // Must remain a comma expression for matching +#ifdef PORT + h2 = (char *) langGet(getStringID(LMPMENU, MPMENU_STR_1D_KILLS)), /* KILLS */ +#else h2 = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1D_KILLS)), /* KILLS */ +#endif sprintf(rankbuffer, ascii_pnum_KILLS, (char *) q, curplayernum + 1, (char *) h2); /* -> "P KILLS" */ textMeasure(&textheight, &textwidth, rankbuffer, ptrFontBankGothicChars, ptrFontBankGothic, 0); @@ -1476,7 +1484,11 @@ Gfx *mp_watch_menu_display(Gfx *gdl) q = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1C_P)); /* P */ // Must remain a comma expression for matching. +#ifdef PORT + h2 = (char *) langGet(getStringID(LMPMENU, MPMENU_STR_1E_LOSSES)), /* LOSSES */ +#else h2 = (s32) langGet(getStringID(LMPMENU, MPMENU_STR_1E_LOSSES)), /* LOSSES */ +#endif sprintf(rankbuffer, ascii_pnum_LOSSES, (char *) q, curplayernum + 1, (char *) h2); /* -> "P LOSSES" */ textMeasure(&textheight, &textwidth, rankbuffer, ptrFontBankGothicChars, ptrFontBankGothic, 0); @@ -1593,7 +1605,7 @@ Gfx *mp_watch_menu_display(Gfx *gdl) y = (viGetViewTop() - (fav_textheight >> 1)) + (37 + MPMENU_YOFF); viewleft = viGetX(); h1 = viGetY(); gdl = textRender(gdl, &x, &y, text, ptrFontBankGothicChars, ptrFontBankGothic, 0x00ff00b0, viewleft, h1, 0, 0); - text = frontGetPlayersFavoriteWeaponInHand(curplayernum, 0); + text = (char *) frontGetPlayersFavoriteWeaponInHand(curplayernum, 0); textMeasure(&fav_textheight, &fav_textwidth, text, ptrFontBankGothicChars, ptrFontBankGothic, 0); x = ((viGetViewLeft() + fav_x_offset) - (fav_textwidth >> 1)) + 80; x2 = viGetViewTop(); diff --git a/src/game/options.c b/src/game/options.c index 1a8a321e..c4781308 100644 --- a/src/game/options.c +++ b/src/game/options.c @@ -1895,11 +1895,31 @@ Gfx *draw_abort_cancel_confirm(Gfx *gdl) s32 sp64; s32 sp60; +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; +#endif +#ifdef PORT + struct fontchar *pFontChars; +#else s32 pFontChars; +#endif +#ifdef PORT + char *sp54; +#else s32 sp54; +#endif +#ifdef PORT + char *sp50; +#else s32 sp50; +#endif +#ifdef PORT + char *sp4C; +#else s32 sp4C; +#endif pFontFile = ptrFontBankGothic; pFontChars = ptrFontBankGothicChars; @@ -1969,14 +1989,30 @@ Gfx *draw_abort_cancel_confirm(Gfx *gdl) Gfx *draw_text_mission_status(Gfx *gdl) { +#ifdef PORT + char *txtptr_1; +#else s32 txtptr_1; +#endif +#ifdef PORT + char *txtptr_2; +#else s32 txtptr_2; +#endif s32 sp64; s32 sp60; s32 sp5C; s32 sp58; +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; +#endif +#ifdef PORT + struct fontchar *pFontChars; +#else s32 pFontChars; +#endif s32 sp4C; s32 joffset; @@ -2027,13 +2063,25 @@ Gfx *empty_draw_function(Gfx *gdl) { Gfx *draw_text_q_watch_v201_beta(Gfx *gdl) { +#ifdef PORT + char *txtptr; +#else s32 txtptr; +#endif s32 sp50; s32 sp4C; s32 sp48; s32 sp44; +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; +#endif +#ifdef PORT + struct fontchar *pFontChars; +#else s32 pFontChars; +#endif s32 joffset; txtptr = langGet(getStringID(LOPTIONS, OPTION_STR_2B_QWATCHVERSION_LF)); //q watch v2.01 beta @@ -2093,8 +2141,16 @@ Gfx* draw_current_hand_item_and_ammo(Gfx* gdl) { s32 sp88; s32 sp84; s32 sp80; +#ifdef PORT + struct font *sp7C; +#else s32 sp7C; +#endif +#ifdef PORT + struct fontchar *sp78; +#else s32 sp78; +#endif s32 temp_v0; s32 sp70; f32 sp6C; @@ -2369,12 +2425,24 @@ Gfx *draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) s32 i; s32 textheight; s32 textwidth; +#ifdef PORT + struct font *pFontFile2; +#else s32 pFontFile2; +#endif +#ifdef PORT + struct fontchar *pFontChars2; +#else s32 pFontChars2; +#endif char string_builder_allocation[2000]; #if defined(VERSION_JP) || defined(VERSION_EU) +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; +#endif s32 base_y; char formattedString[32]; #endif @@ -2439,9 +2507,17 @@ Gfx *draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) { #if !defined(VERSION_JP) && !defined(VERSION_EU) char formattedString[32]; +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; #endif +#endif +#ifdef PORT + struct fontchar *pFontChars; +#else s32 pFontChars; +#endif s32 x2; s32 y2; char *invItemName; @@ -2495,8 +2571,16 @@ Gfx *unused_draw_watch_inventory_page(Gfx *gdl, Mtx *param_2) { s32 sp5C; s32 sp58; s32 sp54; +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; +#endif +#ifdef PORT + struct fontchar *pFontChars; +#else s32 pFontChars; +#endif u16 *long_name; s32 temp_2; @@ -3227,7 +3311,11 @@ Gfx *sub_GAME_7F0A9AB8(Gfx *gdl) Gfx *display_text_buttons_dual_control(Gfx *gdl) { +#ifdef PORT + char *textptr_aux; +#else s32 textptr_aux; +#endif gdl = microcode_constructor(gdl); @@ -3528,8 +3616,16 @@ Gfx *draw_watch_control_options_page(Gfx *gdl, Mtx *param_2) { s32 sp58; s32 sp54; s32 sp50; +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; +#endif +#ifdef PORT + struct fontchar *pFontChars; +#else s32 pFontChars; +#endif gdl = draw_background_health_and_armor(gdl, param_2, 0); @@ -3830,8 +3926,16 @@ Gfx *draw_watch_game_options_page(Gfx *gdl, Mtx *param_2) { s32 sp4C; s32 sp48; +#ifdef PORT + struct font *pFontFile; +#else s32 pFontFile; +#endif +#ifdef PORT + struct fontchar *pFontChars; +#else s32 pFontChars; +#endif gdl = draw_background_health_and_armor(gdl, param_2, 0); diff --git a/src/game/propobj.c b/src/game/propobj.c index e1cc9463..d9434796 100644 --- a/src/game/propobj.c +++ b/src/game/propobj.c @@ -3906,7 +3906,12 @@ s32 sub_GAME_7F0448A8(struct PropRecord *argProp) if ((prop->type == PROP_TYPE_VIEWER) || (prop->type == PROP_TYPE_CHR)) { temp_v0_2 = prop->obj; +#ifdef PORT + /* D456: prop->obj and prop->chr share a union slot; for VIEWER/CHR props it is a ChrRecord. N64 read ObjectRecord.model @0x14 == ChrRecord.chrflags @0x14; read the named field. */ + if ((prop->chr == NULL) || !(prop->chr->chrflags & CHRFLAG_HIDDEN)) +#else if ((temp_v0_2 == NULL) || !((s32) temp_v0_2->model & 0x400)) +#endif { chrpropGetCollisionBounds(prop, &radius, &height, &arbitratyNumber); @@ -14483,25 +14488,6 @@ Gfx *countdownTimerRender(Gfx *DL) void handle_alarm_gas_timer_calldamage(void) { -#ifdef PORT - /* D207 diag probe (temporary): force + trace the alarm klaxon to - * root-cause "alarm SFX starves all other audio and never recovers". - * GE_FORCEALARM pins alarm_timer so alarmIsActive() stays true. Remove - * once root-caused. */ - if (GE_ENVFLAG("GE_FORCEALARM")) { - static int fa_frames; - ++fa_frames; - if (fa_frames == 120 || fa_frames == 900) alarmActivate(); - if (fa_frames > 120 && fa_frames < 600) alarm_timer = 1; /* sustain on ~2..10s */ - if (fa_frames == 600) alarmDeactivate(); /* explicit off at 10s */ - if (fa_frames > 900) alarm_timer = 1; /* sustain on again */ - geTracePrintf("audiotrace.log", - "[D207] f=%d active=%d ptr_alarm_sfx=%p playstate=%d locked=%d\n", - fa_frames, (int)alarmIsActive(), (void *)ptr_alarm_sfx, - ptr_alarm_sfx ? (int)sndGetPlayingState(ptr_alarm_sfx) : -1, - (int)lvlGetControlsLockedFlag()); - } -#endif if (alarmIsActive() != 0) { if ((ptr_alarm_sfx == 0) && (lvlGetControlsLockedFlag() == 0)) diff --git a/src/game/vtxstore.c b/src/game/vtxstore.c index cde5eed2..6f1ac022 100644 --- a/src/game/vtxstore.c +++ b/src/game/vtxstore.c @@ -105,7 +105,14 @@ void sub_GAME_7F09B820(void) } } +#ifdef PORT + /* D462: struct unk_09B7A0_struct_parent is 24 bytes on x86-64 (Vertex* unk00 widened) but the + * N64 stride 0x14 was used to size both tables, which are indexed at the C stride below + * (heap overrun of ~4 bytes per entry, up to 500 entries). ABI/layout fix, section A1. */ + tmp = sizeof(struct unk_09B7A0_struct_parent); +#else tmp = 0x14; +#endif dword_CODE_bss_8007A0E8 = mempAllocBytesInBank(dword_CODE_bss_8007A0D4 * tmp, MEMPOOL_STAGE); dword_CODE_bss_8007A0E0 = mempAllocBytesInBank(dword_CODE_bss_8007A0D0 * 0x10, MEMPOOL_STAGE); dword_CODE_bss_8007A0EC = mempAllocBytesInBank(dword_CODE_bss_8007A0DC * tmp, MEMPOOL_STAGE); diff --git a/src/memp.c b/src/memp.c index 0fafbeb1..b450f159 100644 --- a/src/memp.c +++ b/src/memp.c @@ -37,14 +37,24 @@ void mempInit(void) const char *tokenFind(s32 arg0, const char *arg1); long int strtol(const char *str, char **endptr, int base); +#ifdef PORT +/* D453: arena start is a host pointer; carry it in uintptr_t, not s32. */ +void mempCheckMemflagTokens(uintptr_t poolAreaStart, s32 poolAreaSize) +#else void mempCheckMemflagTokens(s32 poolAreaStart, s32 poolAreaSize) +#endif { s_mempMVALS poolSizes; //set pool 0 to what boss wants (room_model_buffer) //pool 0 = TotalPoolArea +#ifdef PORT + g_mempPools[MEMPOOL_TOTAL].start = (u8 *)poolAreaStart; + g_mempPools[MEMPOOL_TOTAL].end = (u8 *)(poolAreaStart + poolAreaSize); +#else g_mempPools[MEMPOOL_TOTAL].start = poolAreaStart; g_mempPools[MEMPOOL_TOTAL].end = poolAreaStart + poolAreaSize; +#endif poolSizes = sdefaultmvals; @@ -91,7 +101,11 @@ void mempSetBankStarts(s32 poolSizes[MEMPOOL_COUNT+1]) s32 bankstarts[MEMPOOL_COUNT] = {0}; s32 mempLen; s32 mempRequested; +#ifdef PORT + uintptr_t mempStart; /* D453 */ +#else s32 mempStart; +#endif //set MF, ML, ME first i = 0; @@ -130,13 +144,23 @@ void mempSetBankStarts(s32 poolSizes[MEMPOOL_COUNT+1]) // {0,0,0,0,poolAreaSize - 303104, poolAreaSize - 303104, poolAreaSize} +#ifdef PORT + mempStart = (uintptr_t)g_mempPools[MEMPOOL_TOTAL].start; +#else mempStart = g_mempPools[MEMPOOL_TOTAL].start; +#endif //for each bank 1-7, add new start position for (i = MEMPOOL_TOTAL; i < MEMPOOL_COUNT - 1; i++) { +#ifdef PORT + g_mempPools[i + 1].start = (u8 *)(bankstarts[i] + mempStart); + g_mempPools[i + 1].pos = 0; + g_mempPools[i + 1].end = (u8 *)(bankstarts[i + 1] + mempStart); +#else g_mempPools[i + 1].start = bankstarts[i] + mempStart; g_mempPools[i + 1].pos = 0; g_mempPools[i + 1].end = bankstarts[i + 1] + mempStart; +#endif } /* rel-start size @@ -286,7 +310,11 @@ s32 mempGetBankSizeLeft(u8 bank) { } // Last three bits contains the bank, the rest contains the size. +#ifdef PORT +void *mempAllocPackedBytesInBank(u32 sizeandbank) { /* D453: was u32 */ +#else u32 mempAllocPackedBytesInBank(u32 sizeandbank) { +#endif return mempAllocBytesInBank((sizeandbank >> 3), (sizeandbank & 7)); } diff --git a/src/memp.h b/src/memp.h index 9faf5031..6aa9cf85 100644 --- a/src/memp.h +++ b/src/memp.h @@ -46,13 +46,21 @@ typedef enum MEMP_ADD_ENTRY_RESULT } MEMP_ADD_ENTRY_RESULT; void mempInit(void); +#ifdef PORT +void mempCheckMemflagTokens(uintptr_t bstart, int bsize); +#else void mempCheckMemflagTokens(int bstart,int bsize); +#endif void mempSetBankStarts(s32 banks[8]); void *mempAllocBytesInBank(u32 bytes,u8 bank); MEMP_ADD_ENTRY_RESULT mempAddEntryOfSizeToBank(void *allocation, s32 newsize, u8 poolnum); void nulled_mempLoopAllMemBanks(void); s32 mempGetBankSizeLeft(u8 bank); +#ifdef PORT +void *mempAllocPackedBytesInBank(u32 param_1); +#else u32 mempAllocPackedBytesInBank(u32 param_1); +#endif void mempResetBank(u8 bank); void mempNullNextEntryInBank(u8 bank); diff --git a/tools_pc/README.md b/tools_pc/README.md index 5b01fdd8..f0eef27f 100644 --- a/tools_pc/README.md +++ b/tools_pc/README.md @@ -32,7 +32,7 @@ investigation artifacts. | `pixcount.py` | Count non-black pixels in a PPM dump — "did the scene render anything" as a number. | | `level_sweep.sh` | Bare `-level_XX` boot of all 21 solo levels → PASS / NO-FRAMES / CRASH. Predates `verify.sh sweep`; kept as the battle-tested full-21-level runner until `verify.sh sweep` has done an equivalent soak. | | `playtest.sh` | Launch a level for `docs/dev/LEVEL-PLAYTEST.md` human validation (WS6). | -| `audiodebug.ps1` | Audio counterpart to `debug.ps1` (that one is for crashes; this one is for audio behaviour). Drives the `GE_D204` health monitor + `GE_AUDIOTRACE`/`GE_AUDIODUMP` and prints a pass/fail verdict: real-time ratio, queue starvation, dropped blocks, oversized blocks, plus a soundIndex histogram. `-AB` runs the same binary twice (with and without `GE_D204_OLD`) and prints a before/after table — how D204 was measured. `-Play` for an instrumented interactive playtest (the D202 workflow), `-Soak` for a 5-min stability run, `-SyncData` to mirror `./data` first. | +| `audiodebug.ps1` | Audio counterpart to `debug.ps1` (that one is for crashes; this one is for audio behaviour). Drives the `GE_D204` health monitor + `GE_AUDIOTRACE`/`GE_AUDIODUMP` and prints a pass/fail verdict: real-time ratio, queue starvation, dropped blocks, oversized blocks, plus a soundIndex histogram. `-Play` for an instrumented interactive playtest (the D202 workflow), `-Soak` for a 5-min stability run, `-SyncData` to mirror `./data` first. | | `debug.ps1` / `repro_gdb.sh` / `attach_animgen.sh` | Launch (or attach to) the game under gdb so a crash always leaves a backtrace. | | `bundle-win.sh` / `bundle-linux.sh` | Package a built tree as a distributable archive (exe + licenses + `prepare-assets/`). | | `romverify.c` | One-shot `.z64` integrity check against the repo's ground truths. | diff --git a/tools_pc/audiodebug.ps1 b/tools_pc/audiodebug.ps1 index 1669359e..eef356e4 100644 --- a/tools_pc/audiodebug.ps1 +++ b/tools_pc/audiodebug.ps1 @@ -50,6 +50,7 @@ param( [switch]$VoiceDump, # GE_VOICEDUMP=1 -> voicedump.raw (per-voice, D202/M-67) [switch]$MixerTrace, # GE_MIXERTRACE=1 -- SLOW, see warning below [switch]$Old, # GE_D204_OLD=1: pre-D204 behaviour + # NOTE: GE_D204_OLD was removed from the game; -AB is now a no-op. [switch]$AB, # run twice (old, then new) and compare # Plumbing