diff --git a/docs/dev/AUDIO-PLAN.md b/docs/dev/AUDIO-PLAN.md index 8f12da69..f4d51ec6 100644 --- a/docs/dev/AUDIO-PLAN.md +++ b/docs/dev/AUDIO-PLAN.md @@ -1,375 +1,192 @@ -# Audio Plan — Phase 3 (libaudio → SDL) +# AUDIO-PLAN — Phase 3 / ROADMAP B3 (libaudio → SDL) -Status: **draft for evaluation** (written read-only; no code changes made). -Supersedes the audio paragraphs of §6 in `docs/internals.md` as the plan of record -for closing **D77** (no audible music/sound on PC). Companion docs: `docs/HANDOFF.md`, -`docs/porting-notes.md`. +Status: plan of record for the audio track, 2026-09-05. Companion to +`ROADMAP-1.0.md` §B3 ("audio (Phase 3)", "audio track parallel from M-50 → +v0.4.0"). Independent of B1/B2 — can start immediately. --- -## Contents +## Where things stand (verified 2026-09-05) -1. [Goal and scope](#1-goal-and-scope) -2. [Current state](#2-current-state-verified-by-reading-not-assumption) -3. [The GE pipeline in one paragraph](#3-the-ge-pipeline-in-one-paragraph) -4. [How the PD port does it](#4-how-the-pd-port-does-it-ground-truth-comparison) -5. [Architecture options](#5-architecture-options-evaluate-all-recommendation-marked) — options A/B/C + sub-decisions A1–A3 -6. [DMEM layout](#6-dmem-layout-ido-from-srclibultraaudiosynthinternalsh5771) -7. [Shims and ABI fixes required](#7-shims-and-abi-fixes-required) — incl. [7.1 `K0_TO_PHYS` bug](#71-bug-found-k0_to_phys-is-unshimmed-and-corrupts-pointers-on-pc) -8. [Phase 0 — recon](#8-phase-0--recon-do-first-informs-everything) -9. [Open questions](#9-open-questions-decide-before-phase-1) -10. [Phases and milestones](#10-phases-and-milestones) -11. [Verification ritual (audio-specific)](#11-verification-ritual-audio-specific-on-top-of-the-standard-one-in-agentsmd) -12. [Risks / unknowns](#12-risks--unknowns) -13. [Key references](#13-key-references) +More is already in place than "audio not started" suggests: ---- +- **The whole CPU-side audio engine already compiles and runs.** + `src/audi.c`, `src/snd.c`, `src/music.c` are in `SRC_ENGINE`; IDO libaudio + (`src/libultra/audio/*.c`) + Rare's naudio "New" driver + (`src/libultrare/audio/*.c`: drvrNew/env/reverb) are in the link. The + `amMain` audio thread runs per VI-retrace; D54/D54b already killed its + SIGSEGVs — so synthesis bookkeeping is live (D77: "music runs in code but + no audible output"). +- **The scheduler side needs zero changes.** `osSpTaskStartGo` + (`port/src/libultra.c:1237`) already has an M_AUDTASK branch that completes + the task and posts `OS_EVENT_SP`, so `amMain`'s reply-message handshake + works. The branch is currently a no-op — exactly where the audio ucode's + job used to go. +- **The two TODO stubs are the actual gap:** `osAiSetNextBuffer()` / + `osAiGetLength()` in `port/src/libultra.c:852-862` discard everything + (`osAiGetLength` returns 0), and `port/src/audio.c` never opens an SDL + device. `main.c:141-142` already calls `audioInit()` / `mixerInit()`. +- **The PD reference is local** at `C:\Users\james\Source\Repos\pd_port`, + with the full audio stack: `port/src/audio.c` (75 lines, SDL device + + queue), `port/src/mixer.c` (~720 lines, software RSP-audio-ucode), and the + wiring trick in `include/PR/abi.h`. -## 1. Goal and scope +## Architecture — PD's macro-swap trick, adapted -Make GoldenEye's audio audible on the PC port with 1:1 fidelity to the N64 pipeline: -music (3 cseq tracks) and SFX through the real libaudio synth, out of SDL2. +On N64, `alAudioFrame()` only *builds* an acmd list; the RSP audio ucode +(`aspMain`) executes it. PD's PC port never runs that ucode: under +`#ifndef PLATFORM_N64`, `include/PR/abi.h` swaps each acmd-building macro +`aXxx(pkt, ...)` for a call to a CPU implementation (`aXxxImpl`) operating on +a software DMEM array. libaudio/naudio compile unmodified; every "command" +executes inline at build time; the M_AUDTASK that reaches the scheduler is an +empty formality (PD's `pdsched.c` audio branch is literally empty). -In scope: -- Executing the per-frame **Acmd stream** that stock IDO libaudio emits (the part that - ran on the RSP under GE's `aspMain` microcode). -- SDL audio device + queue plumbing (`port/src/audio.c`, AI shims in `libultra.c`). -- One latent ABI fix in the port shim headers (`K0_TO_PHYS`, §7.1). +**Critical catch — GE and PD use different acmd ABIs** (the AGENTS.md +"same family ≠ identical format" case): -Out of scope: -- MP3 playback (naudio-only, PD-specific; GE never emits it). -- Input (done — D118), saves (Phase 4). -- Any change to game logic. All work lives in `port/` except the narrow, documented - shim-header exception in §7.1 (same class as D78). - -## 2. Current state (verified by reading, not assumption) - -Working already: -- **The whole CPU side of libaudio runs.** `src/libultra/audio/*` + - `src/libultrare/audio/{drvrNew,env,reverb}.c` compile and execute; the audio thread - (`amMain`, `src/audi.c`) gets retrace messages from the real scheduler (`src/sched.c`, - compiled — Phase 2 decision), calls `alAudioFrame()`, which fills a 3000-word Acmd - list and posts an `M_AUDTASK` to the scheduler. Bank trees, cseq fixups (D54, D36/D37) - are done. Sample DMA from cart works via `amDmaCallback` → `osPiStartDma` - (synchronous ROM-image shim in `libultra.c`). - -Dead (the three gaps): -1. **Acmd list never executed.** `port/src/libultra.c:1108` `osSpTaskStartGo()` has an - explicit Phase-3 no-op branch for `M_AUDTASK`. The output buffer (`info->data`) is - never filled. -2. **AI shims are stubs.** `libultra.c:794–810`: `osAiSetNextBuffer` discards the - buffer, `osAiGetLength` returns 0 (so the per-frame sample-count feedback loop in - `amHandleFrameMessage`, `audi.c:527`, runs blind). -3. **No SDL device.** `port/src/audio.c` `audioInit()` is a TODO stub; and nothing - calls `audioEndFrame()` (zero callers repo-wide). - -Existing PORT guards to revisit when audio lands: -- **C7 / D127** (`src/snd.c:859`): skips SFX whose resolved `ALSound*` pointer is out of - range — added because the converted bank tree (`romdata.c afFixupInst`) yields fewer/ - rearranged `soundArray` slots than the N64 image on some levels (Surface1). May be - masking a real bank-tree defect. See **Q1**. -- `src/audi.c:365`: `#if defined(PORT)` memcpy of `CUSTOM_FX_PARAMS_N` (C++-style - initializer workaround) — harmless, keep. - -## 3. The GE pipeline in one paragraph - -Audio thread on each retrace: `amHandleFrameMessage()` (`src/audi.c:497`) hands the -*previous* frame's output to AI via `osAiSetNextBuffer(lastInfo->data, …)`, computes -this frame's sample count from `g_FrameSize - osAiGetLength()>>2` (clamped to -`g_MinFrameSize`), calls `alAudioFrame()` which walks the filter chains (decoder → -resampler → envmixer → [FX: GE's custom chorus/reverb] → aux bus → main bus → save) -emitting **Acmd words** into a 3000-word list, then posts an `M_AUDTASK` (ucode = -`aspMain`). The real scheduler (`sched.c:439,485`) queues it in the audio list and runs -it through `osSpTaskLoad/osSpTaskStartGo`. On the N64 the RSP executes the Acmd stream -against 4 KB of DMEM, writing interleaved s16 stereo to `info->data`; AI DMA then plays -it at 22050 Hz. On PC we must replace "RSP executes Acmd stream" — that is the entire -problem. - -**Opcode inventory** (grep-verified across `src/libultra/audio/*.c` + -`src/libultrare/audio/*.c` — 15 ops): - -| Op | Emitted by | PD reference code? | +| | GE (`include/PR/abi.h`) | PD (naudio "New") | |---|---|---| -| `aSetBuffer` (×20) | load, mainbus, resample, save, reverb | yes (`a*Impl` pattern; different arg ABI) | -| `aMix` (×7) | mainbus, reverb | yes — `aMixImpl` | -| `aLoadBuffer` (×7) | load, reverb | yes — `aLoadBufferImpl` | -| `aClearBuffer` (×7) | auxbus, mainbus, load, reverb | yes — `aClearBufferImpl` | -| `aSetVolume` (×5) | env.c (Rare envelope) | yes — `aSetVolumeImpl` | -| `aSaveBuffer` (×4) | save, reverb | yes — `aSaveBufferImpl` | -| `aDMEMMove` (×4) | load, resample, reverb | yes — `aDMEMMoveImpl` | -| `aResample` (×2) | resample.c, reverb.c | yes — `aResampleImpl` (SSE4.1/NEON/scalar) | -| `aLoadADPCM` (×2) | load.c, reverb.c | yes — `aLoadADPCMImpl` | -| `aSetLoop` | load.c:479 | yes — `aSetLoopImpl` | -| `aSegment` | synthesizer.c:231 | n/a (no-op on CPU) | -| `aPoleFilter` | **reverb.c:402 (GE custom FX)** | **NO — PD's stub is empty ("never gets called?")** | -| `aInterleave` | save.c | yes — `aInterleaveImpl` (naudio variant takes no args) | -| `aEnvMixer` | env.c:410/414 | yes — `aEnvMixerImpl` | -| `aADPCMdec` | load.c:483 | yes — `aADPCMdecImpl` (SSE4.1/NEON/scalar) | +| `aADPCMdec` | `(pkt, flags, statePtr)` — params from an `aSetBuffer` context + `ADPCM_STATE[16]` | `(pkt, state, flags, count, inofs, outofs)` — inline args | +| `aLoadBuffer` / `aSaveBuffer` | `(pkt, dramAddr)` only — dest/count from the `aSetBuffer` context | explicit dest + count args | +| `aInterleave` | `(pkt, lDmem, rDmem)` | no args | +| Opcodes | 16: adds `aSetBuffer`, `aPan`, `aSegment`; no `aDisable` / `aPlayMP3` | 15 incl. MP3 (PD-specific) | -PD reference = the Perfect Dark port `port/src/mixer.c` + -`port/include/mixer.h`. Reusable: the DSP *math* (ADPCM 8-coefficient loop filter, -resample table + interpolation, envelope ramping, clamps). **Not** reusable as-is: -signatures and argument conventions (naudio ABI ≠ IDO ABI, §5.3). +GE's context model (from the call sites in `src/libultra/audio/`): +`aSetBuffer(flags, dmemin, dmemout, count)` sets a working-buffer context; +subsequent `aLoadBuffer(dramAddr)` / `aADPCMdec(flags, statePtr)` / +`aResample(flags, pitch, statePtr)` operate within it; the tail is +`aInterleave(AL_MAIN_L_OUT, AL_MAIN_R_OUT)` → `aSaveBuffer(f->dramout)`. -## 4. How the PD port does it (ground-truth comparison) +So this is **not** "copy `mixer.c`": port PD's mixer as the *DSP algorithm +reference* (ADPCM decode, linear resample, mix math, env-mixer / pole-filter +are the same Rare/Nintendo DSP blocks) and write GE-ABI wrappers that +maintain the `aSetBuffer` context and read params from the state structs. -1. naudio is Rare's own library; its `n_abi.h` has a `#ifndef PLATFORM_N64` branch that - redefines every `n_a*` macro as a **direct C call** into `port/src/mixer.c` - (`mixer.h` `#undef`s the macros and redirects to `a*Impl`). All DSP therefore runs - **inline on the audio thread at task-build time**, inside `amgrFrame()`. -2. The `M_AUDTASK` is an empty shell: `pdsched.c:233` receives it with the comment - `// send this into the mixer` and an empty body. -3. PD **replaced the scheduler** (`pdsched.c`) and drives audio from retrace: - `schedAudioFrame()` → `amgrFrame(); audioEndFrame();` (×`diffframe60`). Same thread - for build and flush ⇒ no locking in their SDL handoff. -4. SDL device layer: queue-based, exactly what our scaffold already copied — - `osAiGetLength` = queued **bytes** (`audioGetBytesBuffered()`), - `osAiSetNextBuffer` = stash pointer, `audioEndFrame` = `SDL_QueueAudio` with a - `queueLimit` backpressure drop. Device opened at 22020 Hz (their TODO admits this - "might cause trouble" — it is a silent −9-cent detune vs their 22050 synth rate). -5. DMEM: ~3 KB static buffer, **naudio-specific** offsets (main L/R @0x4E0/0x650, - aux @0x7C0/0x930) — different from IDO's layout (§6). -6. PD-only extras: `aPlayMP3Impl` (minimp3; GE never emits), `aPoleFilterImpl` empty - stub (GE *does* emit it). +**GE's full emittable opcode set** (static audit of every call site in +`src/libultra/audio/*.c` + `src/libultrare/audio/*.c`): ClearBuffer, +LoadADPCM, DMEMMove, SetBuffer, LoadBuffer, SetLoop, ADPCMdec, Resample, +Interleave, SaveBuffer, Mix, SetVolume, EnvMixer, PoleFilter, Segment — plus +`aPan`, defined in abi.h but apparently uncalled. PD's mixer covers the DSP +core for all of them except the trivial bookkeeping ones: SetBuffer / +LoadBuffer / SaveBuffer are context tracking + memcpys; Segment is a no-op on +PC (`amDmaCallback` already resolves ROM addresses against the mapped image — +`osVirtualToPhysical` is identity, `libultra.c:1295`). -**Why we can't copy it wholesale:** GE compiles the real `src/sched.c` and stock IDO -libaudio, whose macros only *write words* into the Acmd list — there is no build-time -hook unless we shadow `PR/abi.h`. Something must execute the stream. Two further ABI -facts: +## Phases -- **IDO's implicit "current buffer":** e.g. `aADPCMdec(pkt, f, s)` carries only flags + - a state pointer; in/out DMEM offsets and sample count come from the preceding - `aSetBuffer` (`load.c:482–483`, `resample.c:90–91`). naudio spells everything out per - call. Whatever option we pick, the implementation must model current-buffer state. -- **Addresses in the stream are RAM pointers** (`K0_TO_PHYS(f->state)`, - `osVirtualToPhysical(e->state)`, `f->dramout`) — resolvable on PC *except* for the - `K0_TO_PHYS` bug in §7.1. +### Phase 0 — Ground truth (research, ~half day, delegable) -## 5. Architecture options (evaluate all; recommendation marked) +1. **Disassemble GE's `aspMain` microcode from the ROM** and document each + opcode's exact semantics in findings.md §F (new Dxx): flag-bit meanings + (`A_FIRST`, `A_CONTINUE`, …), count units (bytes vs samples), where the + interleaved output lands before `aSaveBuffer`, how `aSetLoop` state gets + restored, `ENVMIX_STATE[40]` layout. This is the audio analogue of using + `rsp/graphics/gmain.s` as GBI ground truth — we don't run the ucode, but + it is the spec. (`port/src/ucode.c`'s header already flags this "DECISION + NEEDED in Phase 3"; CPU-bypass means the dummy `aspMainTextStart` / + `aspMainDataStart` markers stay forever — document that decision there.) +2. **Opcode inventory probe** (port-only, zero game-code change): a + `GE_AUDIOTRACE` env var in the existing M_AUDTASK branch of + `osSpTaskStartGo` that walks `t->t.data_ptr` for `data_size/8` words and + histograms `w0 >> 24`. Confirms the static call-site audit and catches + anything emitted dynamically. -### Option A — CPU-side Acmd interpreter at task time 【RECOMMENDED】 +### Phase 1 — Device plumbing (small, lead work) -Decode the 64-bit command words from `t->t.data_ptr` inside the existing `M_AUDTASK` -branch of `osSpTaskStartGo()` (`libultra.c:1108`), dispatching each op to an -implementation in `port/src/mixer.c` operating on a static 4 KB "DMEM". +- Implement `port/src/audio.c::audioInit`: SDL device at **22050 Hz** (GE's + `OUTPUT_RATE = 0x5622`, not PD's 22020), `AUDIO_S16SYS`, stereo. Keep the + existing `Audio.BufferSize` / `Audio.QueueLimit` config registrations. +- Wire `osAiSetNextBuffer` → `SDL_QueueAudio` guarded by `queueLimit`; + `osAiGetLength` → `SDL_GetQueuedAudioSize()`. Faithful mapping: N64's + `AI_LEN_REG` ("bytes synthesized but not yet played") ≙ SDL queued-bytes, + and the game's self-tuning `frameSamples = g_FrameSize − + osAiGetLength()>>2` math (`audi.c:527`) keeps the virtual FIFO at ~736 + samples (NTSC) with no game-code changes. Queue inline in + `osAiSetNextBuffer` (same thread, once per frame) rather than PD's deferred + `audioEndFrame`. Overflow safety is already self-limiting: a full queue + makes `frameSamples` clamp to `g_MinFrameSize`, and the `queueLimit` check + drops rather than crashes. +- Add a `GE_AUDIODEBUG` probe logging queue depth + synthesized samples every + 60 frames; catalog it in `docs/GE-ENV-PROBES.md`. +- **Exit:** links 243/243, `/linkcheck` clean; headless `-level_09` 60 s + crash-free; debug probe shows the queue filling while intro music "plays" + (still silent — acmds don't execute yet). -- Pros: zero game/header/build changes; treats the stream as data exactly like fast3d - does with GBI (project-consistent); natural logging hook (`GE_DAUDIO=1` dump of op + - args per frame) for debugging and for validating against the microcode disassembly; - execution point matches N64 timing (DSP runs when the RSP would have run it). -- Cons: must write the word decoder (~50 lines, trivial — op is bits 24–31 of w0 per - `include/PR/abi.h`) and the current-buffer state machine; one scheduler tick of extra - latency vs build-time execution (inaudible). +### Phase 2 — The mixer (core work, ~1–3 days) -### Option B — Shadow `PR/abi.h`, redirect macros to direct C calls (the PD trick) +- Port PD's DSP cores into `port/src/mixer.c`: ADPCM decode (coefficient-book + handling via `aLoadADPCM`), linear resampler, stereo mix with `0x8000` + scaling, env-mixer + pole-filter (reverb/chorus driven by + `CUSTOM_FX_PARAMS_N`, `audi.c:169`), set-volume ramps. Add the GE-ABI + context layer: a static `{dmemin, dmemout, count}` updated by + `aSetBuffer`, consumed by LoadBuffer / SaveBuffer / ADPCMdec / Resample; a + shared software DMEM array across Impls. Replace the current 31-line stub + interface (`port/include/mixer.h`) with PD-style `aXxxImpl` declarations. +- Edit `include/PR/abi.h` with an `#ifdef PORT` split (N64 path + byte-identical): PC includes a port `mixer.h` whose macros match **GE's** + arg arities. This is the one non-`port/` edit — same documented + ABI/layout-exception class as D78 / D135, logged in §F with its own label. +- Unknown / unused opcodes (`aPan`, `aSegment`): log-and-no-op under + `GE_AUDIOTRACE`; implement only if Phase 0 shows them live. +- All DSP runs on the existing `amMain` thread inside `alAudioFrame` — no new + locks, no scheduler changes. +- **Exit:** opcode histogram fully covered; headless run clean; queue probe + non-zero. -Add `port/shim/PR/abi.h`: `#include "include/PR/abi.h"` then `#undef` the 15 `a*` -macros and `#define` them as direct calls into `mixer.c` (PD's `mixer.h` does exactly -this for naudio). DSP then runs inline during `alAudioFrame()` on the audio thread; the -`M_AUDTASK` branch stays a no-op, exactly like PD. +### Phase 3 — Verification & tuning (user-gated listen tests) -- Pros: battle-tested pattern (shipped in PD); no decoder; slightly less latency. -- Cons: shadows a core PR header for every TU that includes it (only libaudio uses these - macros on PC, so blast radius is small but nonzero — must verify include-path - ordering in `CMakeLists.txt`); stream becomes invisible at runtime (no logging hook — - you'd instrument inside each Impl); the shim must exactly mirror IDO arg order, which - differs from PD's naudio redirects line-by-line. +- Listen checklist: menu music → briefing → in-level music transitions → + gunfire / explosions → **positional SFX** (walk around a source) → weapon + one-shots → level changes → **mission-fail fade** (the D152 `s_imLock` + scenario — retest killing Trevelyan in Facility with live audio, since the + fade hammers the event queue; the D152 self-healing lock mitigation is + already in place but must be re-proven under load). +- Compare reverb / chorus character against N64 reference footage (that's + what `CUSTOM_FX_PARAMS_N` is for). +- Regression: `GE_PCDUMP` framediff vs goldens unchanged (audio must not + touch rendering) + 60 s `-level_09` soak. +- Docs: findings §F entry, porting-notes quirk ("classic-libultra vs + naudio-New acmd ABI delta"), ROADMAP B3 status, HANDOFF update. -### Option C — Run the real `aspMain` microcode 【NOT RECOMMENDED】 +## Risks -Point `aspMainTextStart/DataStart` (`port/src/ucode.c`) at the ROM microcode and execute -it with an RSP emulator. Maximum fidelity by construction, but requires a 16-bit-MIPS -RSP core + DMEM + interrupt model for one component whose entire job is the 15 ops we -can implement directly. Keep the `ucode.c` dummies permanently (their header comment -anticipates this decision). The microcode is still *used* — as a **reference to -disassemble**, not to run (§8, Phase 0). +1. **Ucode-semantics mismatch** (count units, flag bits) → garbled or + wrong-tinted audio, not crashes; mitigated by Phase 0 disassembly + + iterative listen checks. Main schedule risk. +2. **Timing coupling with B1** (D193 family): low — audio frame size is + feedback-driven off `osAiGetLength`, robust to scheduler jitter; re-verify + after B1 lands. +3. **Performance:** ~736 samples/frame of CPU DSP is trivial on x86-64 (PD + shipped exactly this). +4. **D152 lock storm under live audio** — covered by the explicit Phase 3 + retest. -### Sub-decision A1 — where samples hit the SDL queue 【RECOMMENDED: immediate】 +## Delegation shape & effort -- **Immediate:** `osAiSetNextBuffer()` shim calls into `audio.c` and `SDL_QueueAudio`s - right there (on the audio thread), with the existing `queueLimit` backpressure drop. - Lock-free (single writer); matches the moment N64 AI DMA starts; `audioEndFrame()` - disappears entirely. Safe because `NUMBER_OUTPUT_BUFFERS=3` rotation guarantees the - handed-off buffer isn't rewritten before it is queued. -- **Deferred (PD-style):** stash in `osAiSetNextBuffer`, flush from a frame-end hook. - In GE this crosses threads (audio thread sets; scheduler/gfx path would flush) ⇒ needs - a mutex or accepts PD's unlocked pattern, and needs a new `audioEndFrame()` call site - that doesn't exist today. Only worth it if immediate queueing shows a problem. +Per `docs/dev-process.md`: -### Sub-decision A2 — device sample rate 【RECOMMENDED: 22050】 +- Phase 0 disassembly → Claude research subagent (judgment-heavy, read-only + + findings write-up). +- Phase 1 → lead or mechanical worker (build-verifiable). +- Phase 2 DSP-core port → candidate for the local Qwen once the semantics doc + exists (mechanical, diff-checkable against PD), with the ABI mapping and + review kept by the lead. +- Phase 3 listen tests → user. -Request exactly `OUTPUT_RATE` (0x5622 = 22050 Hz), stereo s16, as PD's `audio.c` -scaffold already comments. Do **not** copy PD's shipped 22020 (detune). If a platform -rejects 22050, let SDL convert (`SDL_OpenAudioDevice` with allowed-conversion) rather -than changing the requested rate. +Total ~3–6 focused sessions. Milestone: ROADMAP "audio track (parallel from +M-50) → v0.4.0". -### Sub-decision A3 — timing and thread priorities 【RECOMMENDED: inline + host priorities; no worker thread】 +## Key file map -Execution stays on the scheduler thread (Option A's execution point). If Phase 2/3 -milestones show underruns (`audio: ai out of samples` spam — verification step 3) or -frame-time spikes attributable to synthesis cost, the **first lever is host thread -priorities**: `SetThreadPriority` / `pthread_attr_setschedparam` with the scheduler -thread time-critical (it now owns both fast3d and the mixer) and the tick thread -normal — a ~10-line fix in `libultra.c`. A dedicated audio worker thread consuming -`M_AUDTASK`s is explicitly **not** the next step: it adds a cross-thread handoff for a -problem priorities may fully solve, and the kernel's priority semantics are already -fake (D24 — see the backlogged bug-class entry in `docs/BACKLOG.md`), so any deeper -redesign starts there. Do **not** consider rewriting toward PD's single-threaded -inline-`amgrFrame()` model: it works for them only because they adapted game-side lib -code (direct `schedSubmitTask` calls, gutted `pdsched.c`); for GE it means editing -`src/audi.c` / scheduler submission points — a non-negotiable #2 violation. Rationale -recorded in `docs/BACKLOG.md` (Architecture decision). - -## 6. DMEM layout (IDO, from `src/libultra/audio/synthInternals.h:57–71`) - -``` -AL_MAX_RSP_SAMPLES = 160 (per-chunk; alAudioFrame builds the list in ≤160-sample chunks) -offset 0 AL_DECODER_IN / AL_RESAMPLER_OUT / AL_TEMP_0 (320 B) -offset 320 AL_DECODER_OUT / AL_TEMP_1 (320 B) -offset 640 AL_TEMP_2 (320 B) -offset 1088 AL_MAIN_L_OUT (320 B) -offset 1408 AL_MAIN_R_OUT (320 B) -offset 1728 AL_AUX_L_OUT (320 B) -offset 2048 AL_AUX_R_OUT (320 B) → max extent 2368 B -``` - -Static 4 KB buffer in `mixer.c` (round number, headroom). **Not** PD's naudio offsets. -GE's custom FX (`reverb.c`) uses `AL_TEMP_0/1/2` as scratch plus RAM-backed delay lines -via `aLoadBuffer`/`aSaveBuffer` to `r->base` — all plain host pointers on PC. - -## 7. Shims and ABI fixes required - -### 7.1 【BUG FOUND】 `K0_TO_PHYS` is unshimmed and corrupts pointers on PC - -`port/shim/PR/R4300.h` redefines `PHYS_TO_K0` (identity) but **not** `K0_TO_PHYS`. -Stock: `#define K0_TO_PHYS(x) ((x)&0x1FFFFFFF)` (`include/PR/R4300.h:65`). Game RAM -lives at the V1 view `0x70xxxxxx` (`port/src/dram.c`); masking gives `0x10xxxxxx` — an -invalid host pointer. Hit sites: `load.c:483` (`aADPCMdec(..., K0_TO_PHYS(f->state))`) -and `load.c:479` (`aSetLoop(ptr++, K0_TO_PHYS(f->lstate))`) — the mixer would receive -garbage state addresses for every ADPCM voice. Fix: extend the existing shim with -`#undef K0_TO_PHYS / #define K0_TO_PHYS(x) ((u32)(x))` (identity; V1 addresses are live -host pointers). Inert in the N64 build (no `-DPORT`) — same class as the D78 precedent. -Record as a new Dxx finding when implemented (next free label after D127). - -### 7.2 AI shims (`libultra.c:794–810`) - -- `osAiSetFrequency(hz)` → return `hz` (already does; 22050 flows into - `alconf->outputRate` and the frame-size math — correct). -- `osAiSetNextBuffer(buf, size_bytes)` → queue to SDL per A1. -- `osAiGetLength()` → **bytes** currently queued (`SDL_GetQueuedAudioSize`); the game - does `>> 2` for frames (`audi.c:527`). PD returns bytes too. This closes the - feedback loop that sizes each frame's synthesis. -- `osAiSetConvert` — not called by GE; leave stub. - -### 7.3 Misc - -- `port/src/audio.c`: fill in `audioInit()` (SDL_InitSubSystem(AUDIO), open device per - A2, unpause); add the queue function per A1; keep `Audio.BufferSize`/`Audio.QueueLimit` - config entries; add an `Audio.Enabled` toggle (PD's `g_SndDisabled`) so the level - sweep can run silent. -- `port/src/mixer.c`: rewrite as the op implementations + (for Option A) the decoder / - current-buffer state machine. `mixerInit/Destroy` reset DMEM + op state. -- Threading note: mixer runs on the scheduler thread (`sched.c __scMain` → - `osSpTaskStartGo` inline); with A1 there is exactly one shared object (the SDL queue, - internally locked) — no port-side locks needed. - -## 8. Phase 0 — recon (do first; informs everything) - -1. **Disassemble `bin/aspboot.text.bin`** (+ `.data.bin`; markers in `src/aspboot.s`) — - GE's real ASP microcode, 16-bit MIPS RSP. This is the ground truth for: - - exact `aSetBuffer` current-buffer semantics (which subsequent ops consume it, and - how), - - `aADPCMdec` state layout (what the pointer in w1 points at — IDO `ADPCM_STATE` + - data addressing), - - **`aPoleFilter`** — the one op with no PD reference code; implement from this, - - any DMEM convention not derivable from libaudio usage. - Methodology mirrors how `rsp/graphics/gmain.s` is used for graphics. A small - 16-bit-MIPS disassembler script (`tools_pc/`) is sufficient — we decode, not execute. -2. **Acmd stream logger** (`GE_DAUDIO=1`, in the Option-A decoder or a temporary probe - in the `M_AUDTASK` branch): dump op + decoded args per frame for the first N frames. - Confirms the 15-op inventory at runtime, catches surprises (e.g. ops we didn't grep), - and gives before/after artifacts for validating the implementation. - -## 9. Open questions (decide before Phase 1) - -- **Q1 — C7/D127 bank-tree guard (`src/snd.c:859`):** root-cause the bogus - `ALSound*` resolutions now (likely an `afFixupInst` layout/stride defect in - `port/src/romdata.c`, same family as D37's bank-tree re-layout), or keep the guard as - a documented stopgap and file a separate finding? Keeping it means some SFX stay - silently missing on affected levels (Surface1) even after audio lands. -- **Q2 — Option A vs B** (§5): A is recommended (no header shadowing, stream logging, - project-consistent with fast3d); B is the PD-proven fallback if current-buffer - semantics prove fiddly at task time. Note for the evaluator: this is a **low-stakes, - cheap-to-revisit decision** — both options share 100% of the DSP implementation code - (§6 layout, all op bodies); only the ~50-line front end (word decoder vs macro - redirects) and the execution point differ. If Phase 0's disassembly surfaces anything - that makes B look safer, switching costs an afternoon, not a re-architecture. -- **Q3 — Pole-filter fidelity bar:** implement `aPoleFilter` strictly from the - microcode disassembly (recommended), or accept a devkit-docs approximation and verify - by ear? The chorus is audible in most music; a wrong filter coefficient shows as tonal - difference, not crash. - -## 10. Phases and milestones - -- **Phase 0 — Recon** (§8): microcode disassembly notes + Acmd log artifacts committed - to `docs/` (or `tools_pc/`). Exit: current-buffer semantics + pole filter documented; - op inventory confirmed at runtime. -- **Phase 1 — Plumbing.** SDL device opens @22050 stereo s16 (A2); A1 queue path live; - real AI shims (§7.2); `K0_TO_PHYS` shim fix (§7.1); `Audio.Enabled` toggle. Mixer still - a no-op is fine. **Milestone: device drains, `osAiGetLength` feedback loop visible in - logs, clean start/stop, silence (not crash).** -- **Phase 2 — Core ops.** Implement `aSetBuffer/aLoadBuffer/aClearBuffer/aDMEMMove/ - aADPCMdec/aResample/aEnvMixer/aMix/aSetVolume/aLoadADPCM/aSetLoop/aInterleave/ - aSaveBuffer` (+`aSegment` no-op) in `mixer.c`, reusing PD's DSP math where the ABI - maps. **Milestone: SFX audible (gunshots, doors); dry music plays.** Expect at least - one Dxx-class finding here (state-layout or endianness surprise is the usual bug - class — see `docs/porting-notes.md`). -- **Phase 3 — FX.** `aPoleFilter` + validate the full `reverb.c` chorus path against - the disassembly. **Milestone: music matches N64 character (chorus/reverb present, no - pumping); close D77.** -- **Phase 4 — Polish & bookkeeping.** Resolve Q1; remove/adjust temporary probes and - `GE_DAUDIO` defaults; latency/buffer tuning (`Audio.BufferSize`, `QueueLimit`); - host thread-priority tuning per A3 *only if* underruns were observed in Phases 2–3; - record findings in `docs/internals.md` §F/§H (next Dxx after D127) + update the - §F index, `AGENTS.md` Phase 3 status, `docs/HANDOFF.md`; append recurring bug classes - to `docs/porting-notes.md`. - -## 11. Verification ritual (audio-specific, on top of the standard one in AGENTS.md) - -1. Boot → title: menu music audible within a couple of seconds; no click at start. -2. Enter BUNKER1 (`-level_09`): gunfire/door SFX present and roughly level-correct; - level music plays. -3. No `audio: ai out of samples` spam from `amHandleDoneMessage` (`audi.c:596`) — that - print is the built-in underrun detector. -4. Run 2–3 levels incl. one with heavy SFX overlap; watch `SDL_GetQueuedAudioSize` - (log it) staying below `queueLimit` in steady state. -5. PAL build (`pal-final`) spot-check: 50 Hz retrace drives the same path; frame-size - math uses `MAYBE_FRAME_RATE` — confirm no underruns there too. -6. `Audio.Enabled=0` → fully silent, zero audio CPU cost, nothing else affected. - -## 12. Risks / unknowns - -| Risk | Mitigation | +| File | Role in this plan | |---|---| -| IDO current-buffer semantics misread → subtle wrong output (no crash) | Phase 0 disassembly is the arbiter; Acmd logger + by-ear checks per phase | -| `aPoleFilter` has no PD code | Disassembly (Q3); it's one small IIR — bounded effort | -| ADPCM state layout differs from assumption → garbage/cratchy samples | Phase 0 documents the struct from `load.c` usage + microcode; scalar fallback path makes debugging easy | -| C7/D127 hides a bank-tree bug that audio makes *audible* (wrong SFX, not just missing) | Q1; keep the guard until root-caused | -| SDL queue backpressure drops under load (slow machines) | `queueLimit` drop is graceful (silence gap, no crash); A3: raise scheduler-thread host priority before buffer tuning; PD shipped the same design | -| Synthesis cost on the scheduler thread starves gfx frame time / causes underruns | A3: host thread priorities first; a dedicated audio worker thread only if priorities provably insufficient (and revisit D24's fake-priority semantics before designing one) | -| Cross-region: PAL sample-count math differs | Verification step 5 | - -## 13. Key references - -| File | What it gives us | -|---|---| -| `src/audi.c` | audio thread, frame-size feedback loop, task construction (`OUTPUT_RATE` @22) | -| `src/sched.c:439,465,485` | real scheduler's M_AUDTASK path → `osSpTaskStartGo` interception point | -| `include/PR/abi.h:260–407` | IDO Acmd word formats (the decoder's spec) + A_* opcodes | -| `src/libultra/audio/synthInternals.h:57–71` | DMEM layout constants | -| `src/libultrare/audio/reverb.c`, `env.c`, `drvrNew.c` | GE custom FX (`alFxNew`), pole filter + chorus, envelope ops | -| `bin/aspboot.text.bin` / `.data.bin` (+ `src/aspboot.s`) | **ground-truth ASP microcode to disassemble** | -| `pd_port/port/src/mixer.c`, `port/include/mixer.h` | DSP math reference + the Option-B pattern | -| `pd_port/port/src/audio.c`, `pdsched.c:233–260`, `libultra.c:150–175` | PD wiring (queue design, empty AUDTASK branch, AI shims) | -| `port/src/libultra.c:794–810, 1108` | the three dead seams this plan fills | -| `port/src/dram.c`, `port/shim/PR/R4300.h` | address model (V1/V2 views) + the K0_TO_PHYS gap | +| `src/audi.c` | Game audio manager — compiles unmodified; source of `OUTPUT_RATE`, frame-size math, `CUSTOM_FX_PARAMS_N` | +| `include/PR/abi.h` | GE's acmd accessors (16 opcodes, context ABI) — the one non-`port/` edit (`#ifdef PORT` split) | +| `src/libultra/audio/*.c`, `src/libultrare/audio/*.c` | CPU synth + naudio New driver — compile unmodified; call sites define the opcode set | +| `port/src/audio.c` | SDL device + queue (implement Phase 1) | +| `port/src/mixer.c` | Software acmd interpreter + software DMEM (implement Phase 2) | +| `port/include/mixer.h` | Stub interface → replace with `aXxxImpl` declarations | +| `port/src/libultra.c:848-863` | `osAi*` shims — wire to audio.c (Phase 1) | +| `port/src/libultra.c:1237` | M_AUDTASK branch — stays a completing no-op; hosts `GE_AUDIOTRACE` | +| `port/src/ucode.c` | Dummy `aspMain*` markers stay forever under CPU-bypass — document decision | +| `C:\Users\james\Source\Repos\pd_port\port\src\{audio,mixer}.c`, `include\PR\abi.h` | PD ground truth: device glue + DSP cores + the macro-swap pattern | diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 47f148dc..bd43ab6c 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -394,8 +394,14 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D191 | **Windows `-O2` release-bundle crash: SIGSEGV in `modelGetNodeRwData` (`src/game/model.c:478`) — the `root->Parent` node-tree walk (M-49, user playtests of the v0.1.0 win64 bundle).** **Two instances, identical fault PC `0x1400801b1`:** (1) fresh save, killed the first guard right after escaping the cell in Bunker ii; (2) Statue (`-level_49`?), right after the Trevelyan meeting cutscene. `ge007.crash.log` (auto-dump; symbols intact in the shipped exe, `addr2line` resolves): PC = `modelGetNodeRwData` at `model.c:478` — the `switch (root->Opcode & 0xff)`, i.e. `root` (a `ModelNode *`) is bad; `addr2line` puts it in the inlined self-recursion at `:532`, so the `while (root->Parent) { root = root->Parent; … }` walk (`model.c:527-540`) handed a garbage node into the recursive call. Instance 1: `Rdx = 0x70267bc000000000` — a `~0x70266xxx` heap address in the **high 32 bits, zero low half** = 32-bit pointer field read as 64-bit (or a byte-swapped `u32` pair). Instance 2: `Rdx = 0`, FAULT ADDR `0x0` — `root` (or `root->Parent`) resolved to NULL. Both: `Rcx=Rdi` = the `~0x7008xxxx`/`~0x7007xxxx` object ptr, `R8 = 0x1d` (29). So the `Parent` chain in some model contains a **truncated or NULL link** — pointer-width / byte-swap family (D3x, D119 `weapons_held[]->chr` type-pun, D122/D126/D132 propDef-stride). NOT weapon-fire-specific — common factor is `modelGetNodeRwData` running on a character/cutscene model whose node tree has a bad `Parent`. Crash logs saved: `scratchpad/crashlogs/`. **Repro aid:** shipped exe has full symbols; `debug-crash.ps1` in the bundle dir runs it under gdb, `GE_D51=1` → `d52rw.log` rwdata-pool trace. The crash.c EBP backtrace breaks immediately (`#01: 0x1d`) — need a real `bt` + `p *root` + `p *Objinst` at the fault. Not yet reproduced under a debugger; no fix. | OPEN — not investigated (M-49). 2 field instances (Bunker, Statue). Awaiting a gdb backtrace + `d52rw.log`. | | D193 | **NPC AI locomotion is too slow — characters don't move to their destinations at the original game's rate (M-49, user playtest; clarified after the FULL 21-level playthrough — "the biggest overall flaw").** Not (primarily) an animation-playback bug — the *travel speed* of scripted/AI movement is wrong: guards, named NPCs (Trevelyan, Ourumov) and friendlies (Natalya) all take noticeably longer to run/walk to their goal positions than on N64. General, not chr-specific. **Leading hypothesis — the D117/D134/D155/D156 wall-clock-timing family:** `chr.c` / `chrai.c` integrate movement as `pos += speed * g_GlobalTimerDelta` and step sim loops `while (i < g_ClockTimer)` / `-= g_ClockTimer` (`chr.c:1437,1446,1491-1502,1862,2359,2569`; `chrai.c:76`). On the port `osGetCount()` is free-running wall-clock (D117), `g_GlobalTimerDelta`/`g_ClockTimer` are derived from it, and D155's `FRAMETIMING_PORT_MAX_CATCHUP = 6` clamps `nextFrameTime`. If the effective per-second sum of `g_GlobalTimerDelta` (or the `g_ClockTimer` tick count) runs low — quantization, the catch-up clamp biting during normal play, or a units mismatch (`g_JP_GlobalTimerDelta` vs `g_GlobalTimerDelta`, `chr.c:1435/1437`) — every delta-scaled AI motion integrates slow while **player** movement (input-driven per frame) still feels right, which is exactly the reported split. Related: **D170** (Ourumov/Trevelyan "flee" looks wrong — flagged there as possibly the same locomotion-speed issue rather than an AI divergence). Also possible: `chrGoToBond/Chr(SPEED_RUN/WALK/SPRINT)` speed enum → path-follow step, or pathfinding advancing ≤1 waypoint per frame-quantized tick. **Instances seen (M-49 full playthrough):** (a) Facility — the post-scripted-scene beat with Ourumov + the soldier squad: the soldiers **walk in super slowly and take a long time to path into firing positions** before they start shooting (this is regular squad combat AI, not a named-NPC script — good evidence the slowdown is general delta-scaled `chr` locomotion / path-following, not one ailist); (b) the named-NPC flee behaviour on Silo (Ourumov) and Cradle (Trevelyan) — see D170; (c) **Natalya (the escort NPC) always walks really slow** — on every escort level (Bunker ii, Statue, Control, Archives) she trails far behind, forcing the player to wait. Escort follow-AI is the same `chrGoToChr`/path-follow path as (a). **WORST CASE — Cradle (`-level_51`) is "bugged out completely" due to Trevelyan's behaviour (M-49).** The final level is Trevelyan-fleeing-and-scripted end to end (he runs the antenna cradle, climbs, drops to the cradle floor, the helicopter beat); if his locomotion timing / path-following is off he never hits the trigger points and the level's scripted progression stalls or goes haywire. This makes D193 effectively a **can't-properly-finish-the-game** bug, not just cosmetic sluggishness. **Next:** log `g_GlobalTimerDelta` + `g_ClockTimer` per frame during normal play and check the 1-second sum vs the N64's ~60; time a guard's run across a known distance vs reference footage; then watch Trevelyan's chr state / current AI command on Cradle. | OPEN — root-cause hypothesis (timing family), not investigated (M-49). **Highest-impact playtest finding — breaks Cradle.** | | D194 | **Mouse input needs another tuning pass — RMB aim over-sensitive + sensitivity couples to frame/sim rate (M-49, user playtest).** Two parts. **(a) Aim mode (RMB) is near bang-bang, not proportional** (`port/src/input.c:697-707`): any per-poll delta past `AIM_MOVE_THRESH` emits stick `m = clamp(61 + |Δ|·(MouseAimSpeed/100)·AIM_GAIN, 61, 60+AimBand)` = `[61,80]` — with the M-29 defaults (`MouseAimSpeed=16`, `AIM_GAIN=4.0`, `AimBand=20`) `m` is already 61 (fast) at ~1.5 px/poll and saturates at 80 by ~30 px/poll, so the usable proportional band is tiny and sits near the top of GE's aim range → "way too sensitive," turn rate barely tracks how fast you actually move the mouse. Needs a real response curve (low floor, wider proportional band, maybe a gamma), not a threshold+clamp. **(b) Overall sensitivity correlates with movement/frame rate** (user's words: "correlated with movement speed, does not feel good"): `input.c` emits raw stick counts **per controller poll** with no `g_GlobalTimerDelta` / polls-per-frame normalization (hipfire yaw `sx += edx·(MouseTurnSpeed/100)·6.0` at `:709`; aim as above). GE integrates the stick scaled by its sim delta, and the port's poll cadence vs render/sim cadence drifts with scene load — so effective sensitivity shifts when the framerate does (which reads as "when I'm moving"). Fix direction: accumulate mouse delta and convert to stick using a fixed reference dt (or feed the game a dt-consistent value), so a given hand motion = a given view rotation regardless of fps. Lineage: D118a (hipfire pitch digital vs analog), D165 (menu pointer), D166 (hipfire pitch pulses), D180-B3 (`MouseAimSpeed` 25→16). Config knobs exist (`Input.MouseAimSpeed/AimBand/MouseTurnSpeed/HipfirePitchSpeed`) but the *shape* of the mapping is the problem. | OPEN — root-caused, not fixed (M-49) | +| D195 | **Transparency / alpha surfaces broken on Control (M-49, user playtest)** — Natalya's console room, the glass partitions + translucent projected wall displays. Failure mode not yet specified by the reporter (no `GE_PCDUMP` capture taken). Likely a fast3d render-mode / alpha-blend path defect, not a decode one — neighbours D161 (CI8/LUT decode), D172, D176(b) (both blend-adjacent) but those are texture-format bugs, this reads as a blend-state one; check D128 portal adjacency in case the glass geometry is portal-culled wrong instead. Logged in `GRAPHICS-BACKLOG.md`. **Next:** `GE_PCDUMP` capture on Control near the console room, compare RDP render-mode words against N64 `gmain.s` for the glass/display materials. | OPEN — not investigated (M-49), needs a capture | +| D197 | **Character face/head textures wrap around the head, seen on Silo (M-49, user playtest).** Reads as `G_TX_CLAMP` not honoured (or a non-power-of-two wrap period) on a head/face texture tile — the texture repeats around the mesh instead of clamping at the UV edge. Not yet isolated to a specific character or texture; no `GE_PCDUMP`/`GE_TEXDUMP` capture taken. Likely fast3d texture-tile setup (clamp/wrap mode bits from the tile descriptor not round-tripped) rather than a data bug, given it's cosmetic and geometry-independent. **Next:** `GE_TEXDUMP` the offending head texture's tile descriptor on Silo, diff clamp/mirror bits against `gmain.s`'s expectations for that material. | OPEN — not investigated (M-49), needs a capture | +| D198 | **`alAdpcmPull`/`_decodeChunk` used `K0_TO_PHYS` (an unconditional `& 0x1FFFFFFF` mask) on 64-bit heap pointers (`src/libultra/audio/load.c`).** `K0_TO_PHYS` is the N64 kseg0→physical mask, valid only because N64 pointers fit in 32 bits; on PC's 64-bit heap it truncates any pointer whose value exceeds 512MB, corrupting the `aLoadADPCM`/`aSetLoop`/`aADPCMdec` DRAM addresses at 3 call sites (`load.c:71`, `:468`, `:474`). Every sibling call site in `env.c`/`resample.c`/`reverb.c` already resolves state pointers via `osVirtualToPhysical` (the port shim, `port/src/libultra.c`), which is bit-identical to `K0_TO_PHYS` for real N64 kseg0 addresses but correctly PC-shimmed. **Fix (ABI-layer, `#ifdef`-free — same call resolves correctly on both platforms):** replace the 3 `K0_TO_PHYS(...)` calls with `osVirtualToPhysical(...)`. No behavior change on N64 (identical result for kseg0 pointers); fixes PC. Surfaced while building the Phase-3 software mixer (D199) — the acmd list these opcodes feed now actually executes on PC instead of being ignored. | FIXED (`src/libultra/audio/load.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation | +| D199 | **Phase-3 software audio mixer: macro-swap `aXxx` opcode execution + reverse-engineered `aSetBuffer` persistent-context semantics (`port/src/mixer.c`, `port/include/mixer.h`, `include/PR/abi.h`).** The RSP audio ucode (`aspMain`) that would normally execute GE's acmd list never runs on PC (`port/src/ucode.c`); adapting the Perfect Dark PC port's macro-swap trick (`docs/dev/AUDIO-PLAN.md`), `include/PR/abi.h` now `#include`s `port/include/mixer.h` under `#ifdef PORT`, which redefines every `aXxx` macro to call an `Impl` function in `mixer.c` immediately against a small software "DMEM" scratch buffer, instead of packing RSP command words for later execution. GE uses the classic IDO libaudio ABI (verified against every call site in `src/libultra/audio/*.c` + `src/libultrare/audio/{env,reverb}.c`), not PD's differently-shaped "naudio New" ABI — DSP math (ADPCM decode, linear resample, linear envelope mix) is the same Nintendo/SGI ucode PD's `mixer.c` already ported; only the ABI/addressing layer is GE-specific and new here. Several opcodes (`aADPCMdec`, `aResample`, `aEnvMixer`, `aLoadBuffer`, `aSaveBuffer`) take only a state/DRAM pointer — their DMEM source/dest addresses and byte counts come from the most recent `aSetBuffer` call(s), which there is no RSP disassembly to check against; the persistent-context shape (`sCtx.{in,out,count,dryR,wetL,wetR}`) was reconstructed purely from reading every `aSetBuffer`+opcode call-site pair across `resample.c`/`load.c`/`reverb.c`/`mainbus.c`/`save.c`/`env.c` (see the field comments on `sCtx` in `mixer.c:52-82`). `aSegmentImpl` is a no-op — GE's audio DRAM addresses are already resolved via `osVirtualToPhysical` before reaching the mixer (see D198), so segment/base never factor into DMEM addressing here. **Risk:** the `aSetBuffer` context reconstruction is an inference, not a verified spec — if a call site this session didn't audit sets the context differently, audio for that path will mix from the wrong DMEM offsets. **Verified:** `-level_09` now runs full music + SFX 120s+ crash-free (post-D200 fix); no cross-level or opcode-level audio-correctness verification done yet (no golden-audio comparison tooling exists). | LANDED (`port/src/mixer.c`, `port/include/mixer.h`, `include/PR/abi.h`) — functional on `-level_09`, context-inference risk noted, needs a wider level sweep | +| D200 | **`-level_09` segfault: reverb's `ALDelay.input/output` are u32 — `-d->output` zero-extends to a +4GB pointer offset on 64-bit (D3x class, FIXED).** `src/libultrare/audio/reverb.c` back-references the delay ring as `&r->input[-d->output]`; on N64 (s32 `ptrdiff_t`) the u32 negation wraps to a small negative index, on PC x86-64 it zero-extends to +4294967136 samples → `_loadBuffer`/`_saveBuffer` write 13–52KB wild, trampling the delay array / `r->base` / adjacent heap in a self-propagating loop until unmapped memory. Fixed with `(s32)` negation casts at 5 sites (PD ground truth `n_reverb.c` does exactly this); ramalign line gets `(s64)`. Verified: `-level_09` runs 120 s+ crash-free, DRAM guard window over the FX region sees zero OOB writes. | FIXED — 5-line cast in reverb.c (this session) | | D196 | **OS mouse cursor stays visible after closing the F10 options overlay in click-to-lock mode (M-49, user playtest).** `Input.MouseCaptureMode=1`. Open F10 overlay → `optionsOverlayToggle()` (`optionsoverlay.c:391`) calls `inputSuspendForOverlay()` which forces `SDL_ShowCursor(SDL_ENABLE)` + drops the grab. While open, `inputComputePad(0)` early-returns at `input.c:512` (overlay owns controller 0) **before** `reconcileGrab()` / any cursor-visibility call. On close: the next poll runs `reconcileGrab(menuMode)` (`input.c:522`) which re-grabs + hides the cursor **only if `captureArmed && !menuMode`** — i.e. only if you had already clicked-to-lock in a stage before opening F10. If you opened the overlay from a menu, or in a stage you hadn't clicked-locked yet, `want` computes 0, `mouseGrabbed` stays 0, and **nothing calls `applyCursorVisibility()`** — so the cursor `inputSuspendForOverlay()` made visible never gets re-hidden (correct free-but-focused state per `applyCursorVisibility()` `input.c:842-846` is *hidden*). **Fix (port-only, ~1 line):** call `applyCursorVisibility()` right after the `reconcileGrab(menuMode)` at `input.c:522` (self-heals every poll once the overlay-open early-return stops firing), or add an `inputResumeFromOverlay()` that runs `reconcileGrab` + `applyCursorVisibility` and call it from `optionsOverlayToggle()` on close. D180/D184/D165 input lineage. Minor. | OPEN — root-caused, 1-line fix identified, not applied (M-49) | | D192 | **Front-end mission/level-select grid pointer still can't reach the outer cells — but only under `Input.MouseCaptureMode=0` (legacy always-grab) (M-49, user playtest).** D169 was marked FIXED (M-33) by replacing the hard-coded 320×240 pointer clamp with the live `getPlayer_c_screenwidth/height/left/top()` rect; the feel-check was flagged "owed" on both D169 and D180 and never done. It now reproduces: with `MouseCaptureMode=0` the menu pointer runs the relative-delta P-controller path (`port/src/input.c:659-690`), which clamps target+estimate to `getPlayer_c_screen*` — but that accessor returns `g_CurrentPlayer->c_screenwidth` (`src/game/bondview.c:882`), which in the front end is the ~320×240 stage viewport, **not** the front end's actual 440×330 (`front.c:8570` `viSetViewSize(440,330)`). So the effective clamp is still ~300/220 while the mission-select grid's outer split points sit at 317 / 235.5 (`front.c:516/519/3180/3194`) — outer columns + bottom row unreachable, P-controller winds up and never settles. `MouseCaptureMode=1` (the shipped default, click-to-lock) writes `cursor_h_pos`/`cursor_v_pos` directly from the absolute OS cursor (`input.c:629-657`) and *feels* fine, though it shares the bad `hiH` so the last few px at the right/bottom edge may still be short. **Not a v0.1.0 blocker** — default mode is acceptable, always-grab is opt-in via INI, front-end roughness is already in the release notes. **Fix (port-only, small):** in the `menuMode` branch, when `current_menu` is a front-end menu use the real front-end rect (hard-code `front.c`'s cursor clamp `[20,420]×[20,310]`, or read the front-end viewport global) instead of routing through `g_CurrentPlayer`. Confirm with one `GE_INPUTLOG=1` capture in mission-select — sweep the mouse to each corner, watch where `cursor=(x,y)` plateaus. D165 · D169 · D180 lineage. | OPEN — root-caused, not fixed (M-49) | +| D201 | **`src/libultra/audio/bnkf.c` bank/instrument/sound/wavetable relocation offsets were `s32`, truncating the real 64-bit base pointer.** `alSeqFileNew`/`alBnkfNew` compute `offset = (s32) base` from a heap pointer and thread it through `_bnkfPatchBank/Inst/Sound/WaveTable` as `s32 offset, table` parameters, used to rebase every embedded pointer field in a loaded bank/inst/sound/wavetable/book/loop record. On N64 (32-bit pointers) the truncation is a no-op; on PC, sign-extension of the truncated `s32` during the rebase arithmetic reconstructs the wrong 64-bit address whenever the base allocation's low 32 bits have the top bit set (~50% of allocations, allocator-dependent) — corrupting every relocated pointer in the bank file, a plausible contributor to bad model/audio pointers surfacing downstream (D191-family symptoms are a node-tree walk, not this, but the class is the same). **Fix (ABI-layer, no logic change):** widen `offset`/`table`/`woffset` and all four `_bnkfPatch*` parameters from `s32` to `uintptr_t` (``) — already this codebase's idiom for pointer-safe integers in `src/game/*.c`. No behavior change on the 32-bit N64 build. | FIXED (`src/libultra/audio/bnkf.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation | | D152+ | D152 addendum (M-31): static audit of every compiled-audio `osSetIntMask(OS_IM_NONE)` — **all balanced**, no unbalanced early-return. Real fixes: `sndSetSfxSlotVolume` now holds the mask across its list walk (matches its twin `sndDeactivateAllSfxByFlag`) + `sndApplyVolumeAllSfxSlot` batches the slot loop under one recursive hold (kills the fade-out lock-acquire storm); `portThreadWrapper` calls `imThreadExitRelease()` on thread exit (kills the "transient thread acquired `OS_IM_NONE` and died" leak + the pthread-id-reuse re-wedge). Steal-lock kept as backstop. | FIXED (`src/snd.c`, `port/src/libultra.c`, `#ifdef PORT`) — fade-out repro playtest-gated, not headless-verified | | RC3 · D167 | **Non-power-of-two texture wrap period (`docs/dev/TEXTURE-GLITCH-ANALYSIS.md` §6 RC3 — "textures repeat oddly", residual Depot-ceiling noise after D161).** The N64 RDP masks the texel coordinate of a wrapping render tile at `1<lp` (a pointer into the AFX +delay array) resolved to garbage like `0x664d59be`. + +**Root cause.** `ALDelay.input` / `ALDelay.output` are `u32`, and reverb.c computes +back-references into the delay ring as `&r->input[-d->output]` (and variants). On +N64, `ptrdiff_t` is s32, so `-160` (the u32 constant `0xFFFFFF60`) wraps to −160 and +the pointer arithmetic is correct. On PC x86-64, `ptrdiff_t` is s64: the u32 negation +**zero-extends** to +4294967136 samples, so `out_ptr` flies forward ~8GB — verified +exactly: `0x706d30a0 (r->input) + 0xFFFFFF60·2 = 0x2_706D2F60`, the observed wild +address. The subsequent `aSaveBuffer`/`aLoadBuffer` then write 13–52KB of wild data, +corrupting the delay array, `r->base`, and adjacent heap blocks — a self-propagating +loop that grows each frame until it hits unmapped memory. + +**Fix (ABI/layout only, D3x class).** Five sites in `src/libultrare/audio/reverb.c` +cast to `(s32)` before negation so the offset sign-extends: `alFxPull` (`in_ptr`, +`out_ptr`), `_loadOutputBuffer` ×2 (`out_ptr`), plus the ramalign line, which uses +`(s64)out_ptr & 0x7`. Perfect Dark ground truth (`src/lib/naudio/n_reverb.c`) does +exactly this: `(s32)-d->input`, `(s32)-d->output`, `(s32)-(d->output - d->rsdelta)`, +`(intptr_t)out_ptr & 0x7`. No game logic changed; behaviour on N64 is identical. + +**Verification.** `-level_09` now runs 120 s+ crash-free (previously segfaulted in +seconds); the delay pointers advance correctly (`out_ptr = input − 320B`, +0x140/frame). +A temporary DRAM guard window over the FX region, registered with the software mixer +(`port/src/mixer.c`), confirmed zero out-of-bounds writes after the fix. All +instrumentation removed; only the five casts remain in the diff. + +**Sibling-risk audit.** Grep of `src/libultra/audio/*.c` + `src/libultrare/audio/*.c` +for negative pointer indexing: reverb.c is the only file with the pattern (its one +positive index, `&r->input[d->output]`, is safe — positive u32 zero-extension is +correct). + +**Lineage.** D3x pointer-width reconciliation; same family as D177 (widened-pointer +high half) and D191 (32-bit field read as 64-bit). diff --git a/include/PR/abi.h b/include/PR/abi.h index 878d4fe6..b7e3a7f1 100644 --- a/include/PR/abi.h +++ b/include/PR/abi.h @@ -269,6 +269,16 @@ typedef short ENVMIX_STATE[40]; * Macros to assemble the audio command list */ +#ifdef PORT +/* PC port (D199): the N64 RSP audio ucode that would execute this acmd list + * never runs on PC (port/src/ucode.c). Following the Perfect Dark PC port's + * macro-swap pattern (docs/dev/AUDIO-PLAN.md), each aXxx macro below is + * replaced with an immediate call into port/src/mixer.c's software + * implementation instead of packing RSP command words. The N64 branch below + * is untouched. */ +#include "mixer.h" +#else /* !PORT */ + #define aADPCMdec(pkt, f, s) \ { \ Acmd *_a = (Acmd *)pkt; \ @@ -402,6 +412,8 @@ typedef short ENVMIX_STATE[40]; _a->words.w1 = (unsigned int) d; \ } +#endif /* PORT */ + #endif /* _LANGUAGE_C */ #endif /* !_ABI_H_ */ diff --git a/port/include/audio.h b/port/include/audio.h index 478e86d6..7c93b79a 100644 --- a/port/include/audio.h +++ b/port/include/audio.h @@ -26,9 +26,6 @@ s32 audioGetSamplesBuffered(void); /* Queue the next block of mixed samples (len is in bytes). */ void audioSetNextBuffer(const s16 *buf, u32 len); -/* Called at the end of each frame to flush the queued buffer to the device. */ -void audioEndFrame(void); - #ifdef __cplusplus } #endif diff --git a/port/include/mixer.h b/port/include/mixer.h index 186da2b5..fd3f2c31 100644 --- a/port/include/mixer.h +++ b/port/include/mixer.h @@ -2,16 +2,21 @@ #define PORT_MIXER_H /* - * Audio mixing. - * Modelled on the PD port's port/include/mixer.h. + * Audio mixing — software implementation of GE's RSP audio ucode opcodes. * - * On the N64 the RSP audio ucode does the final mix / sample-rate handling - * before the AI DMA. Since the RSP is emulated in software here, this module - * performs that final processing on the CPU and hands the result to audio.c. + * include/PR/abi.h's aXxx macros normally pack RSP command words into an + * Acmd list for the audio ucode (aspMain, never run on PC — port/src/ucode.c) + * to execute later. Under #ifdef PORT, abi.h includes this header instead, + * which redefines every aXxx macro to call an Impl function here immediately + * — same macro-swap trick as the Perfect Dark PC port (docs/dev/AUDIO-PLAN.md). * - * NOTE: the exact responsibilities depend on how GE's libaudio output is - * wired (see src/audi.c). This is a placeholder interface to be refined in - * Phase 3 (audio). + * GE uses the classic IDO libaudio ABI (verified against every call site in + * src/libultra/audio and src/libultrare/audio/{env,reverb}.c — NOT PD's + * differently-shaped "naudio New" ABI). Several opcodes (aADPCMdec, + * aResample, aEnvMixer, aLoadBuffer, aSaveBuffer) take only a state/DRAM + * pointer; their DMEM source/dest addresses and byte counts come from the + * most recent aSetBuffer call(s), tracked here as persistent context — + * see the field comments on MixerCtx in mixer.c. */ #include @@ -23,9 +28,54 @@ extern "C" { void mixerInit(void); void mixerDestroy(void); -/* Process one frame of audio. `in` is the libaudio-mixed buffer, `frames` - * the number of stereo frames. Output is queued via audioSetNextBuffer. */ -void mixerProcess(const s16 *in, u32 frames); +/* Software RSP-audio-ucode opcode implementations (port/src/mixer.c). */ +void aSetBufferImpl(u32 flags, u16 i, u16 o, u16 c); +void aClearBufferImpl(u16 addr, u32 count); +void aLoadBufferImpl(u32 dramAddr); +void aSaveBufferImpl(u32 dramAddr); +void aDMEMMoveImpl(u16 in, u16 out, u32 count); +void aLoadADPCMImpl(u32 count, u32 dramAddr); +void aSetLoopImpl(u32 stateAddr); +void aADPCMdecImpl(u32 flags, u32 stateAddr); +void aResampleImpl(u32 flags, u16 pitch, u32 stateAddr); +void aInterleaveImpl(u16 l, u16 r); +void aMixImpl(u32 flags, u16 gain, u16 in, u16 out); +void aSetVolumeImpl(u32 flags, u16 v, u16 t, u16 r); +void aEnvMixerImpl(u32 flags, u32 stateAddr); +void aPoleFilterImpl(u32 flags, u16 gain, u32 stateAddr); +void aSegmentImpl(u32 seg, u32 base); + +#undef aSetBuffer +#undef aClearBuffer +#undef aLoadBuffer +#undef aSaveBuffer +#undef aDMEMMove +#undef aLoadADPCM +#undef aSetLoop +#undef aADPCMdec +#undef aResample +#undef aInterleave +#undef aMix +#undef aSetVolume +#undef aEnvMixer +#undef aPoleFilter +#undef aSegment + +#define aSetBuffer(pkt, f, i, o, c) aSetBufferImpl((u32)(f), (u16)(i), (u16)(o), (u16)(c)) +#define aClearBuffer(pkt, d, c) aClearBufferImpl((u16)(d), (u32)(c)) +#define aLoadBuffer(pkt, s) aLoadBufferImpl((u32)(s)) +#define aSaveBuffer(pkt, s) aSaveBufferImpl((u32)(s)) +#define aDMEMMove(pkt, i, o, c) aDMEMMoveImpl((u16)(i), (u16)(o), (u32)(c)) +#define aLoadADPCM(pkt, c, d) aLoadADPCMImpl((u32)(c), (u32)(d)) +#define aSetLoop(pkt, a) aSetLoopImpl((u32)(a)) +#define aADPCMdec(pkt, f, s) aADPCMdecImpl((u32)(f), (u32)(s)) +#define aResample(pkt, f, p, s) aResampleImpl((u32)(f), (u16)(p), (u32)(s)) +#define aInterleave(pkt, l, r) aInterleaveImpl((u16)(l), (u16)(r)) +#define aMix(pkt, f, g, i, o) aMixImpl((u32)(f), (u16)(g), (u16)(i), (u16)(o)) +#define aSetVolume(pkt, f, v, t, r) aSetVolumeImpl((u32)(f), (u16)(v), (u16)(t), (u16)(r)) +#define aEnvMixer(pkt, f, s) aEnvMixerImpl((u32)(f), (u32)(s)) +#define aPoleFilter(pkt, f, g, s) aPoleFilterImpl((u32)(f), (u16)(g), (u32)(s)) +#define aSegment(pkt, s, b) aSegmentImpl((u32)(s), (u32)(b)) #ifdef __cplusplus } diff --git a/port/src/audio.c b/port/src/audio.c index 75f9c325..81d8cc1f 100644 --- a/port/src/audio.c +++ b/port/src/audio.c @@ -6,8 +6,6 @@ * osAiGetLength (libultra.c) route through here. * * Modelled on the PD port's port/src/audio.c (~75 lines). - * - * STATUS: scaffolding stub — implement during Phase 3. */ #include @@ -18,21 +16,29 @@ #include "audio.h" static SDL_AudioDeviceID dev = 0; -static const s16 *nextBuf = NULL; -static u32 nextSize = 0; static int bufferSize = 512; static int queueLimit = 8192; int audioInit(void) { - /* TODO(Phase 3): - * - SDL_InitSubSystem(AUDIO) - * - open device: 22020 Hz (match GE's OUTPUT_RATE in src/audi.c), - * AUDIO_S16SYS, 2 channels - * - unpause - */ - sysLogPrintf(LOG_INFO, "audioInit: TODO (Phase 3)"); + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + sysLogPrintf(LOG_ERROR, "audioInit: SDL_InitSubSystem: %s", SDL_GetError()); + return -1; + } + SDL_AudioSpec want = {0}; + want.freq = 22050; /* GE's OUTPUT_RATE (src/audi.c) */ + want.format = AUDIO_S16SYS; + want.channels = 2; + want.samples = (u16)bufferSize; + SDL_AudioSpec have; + dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); + if (!dev) { + sysLogPrintf(LOG_ERROR, "audioInit: SDL_OpenAudioDevice: %s", SDL_GetError()); + return -1; + } + SDL_PauseAudioDevice(dev, 0); + sysLogPrintf(LOG_INFO, "audioInit: opened SDL audio device at %d Hz", have.freq); return 0; } @@ -48,18 +54,8 @@ s32 audioGetSamplesBuffered(void) void audioSetNextBuffer(const s16 *buf, u32 len) { - nextBuf = buf; - nextSize = len; -} - -void audioEndFrame(void) -{ - if (nextBuf && nextSize && dev) { - if (audioGetSamplesBuffered() < queueLimit) { - SDL_QueueAudio(dev, nextBuf, nextSize); - } - nextBuf = NULL; - nextSize = 0; + if (dev && buf && len && audioGetSamplesBuffered() < queueLimit) { + SDL_QueueAudio(dev, buf, len); } } diff --git a/port/src/libultra.c b/port/src/libultra.c index 872718ed..71c6a719 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -851,14 +851,14 @@ s32 osAiSetFrequency(u32 hz) } s32 osAiSetNextBuffer(void *buf, u32 size) { - /* TODO(Phase 3): hand the mixed buffer to audioSetNextBuffer(). */ - (void)buf; (void)size; + audioSetNextBuffer((const s16 *)buf, size); return 1; } u32 osAiGetLength(void) { - /* TODO(Phase 3): return audioGetSamplesBuffered() in the right units. */ - return 0; + /* audioGetSamplesBuffered() returns stereo s16 frames (queued bytes / 4); + * callers shift right by 2 expecting a byte count (N64 AI_LEN_REG). */ + return (u32)audioGetSamplesBuffered() * 4u; } void osAiSetConvert(u32 convert) { (void)convert; } diff --git a/port/src/mixer.c b/port/src/mixer.c index 663cfc8f..73148569 100644 --- a/port/src/mixer.c +++ b/port/src/mixer.c @@ -1,31 +1,436 @@ /* - * Audio mixing. + * Audio mixing — software implementation of GE's RSP audio ucode opcodes. * - * On the N64 the RSP audio ucode does final mix / sample handling before the - * AI DMA. With the RSP emulated in software, this runs on the CPU. + * On the N64 the RSP audio ucode (aspMain) executes an acmd list built by + * libaudio/naudio to do ADPCM decode, resampling, and the final envelope + * mix. The RSP is never run on PC (port/src/ucode.c); instead every aXxx + * macro in include/PR/abi.h is redefined (port/include/mixer.h, included + * under #ifdef PORT) to call one of the Impl functions below immediately, + * against a small software "DMEM" scratch buffer — the Perfect Dark PC + * port's macro-swap trick (docs/dev/AUDIO-PLAN.md), adapted to GE's ABI. * - * Modelled on the PD port's port/src/mixer.c (~720 lines). + * GE uses the classic IDO libaudio ABI, not PD's differently-shaped naudio + * "New" ABI — verified against every call site in src/libultra/audio/*.c + * and src/libultrare/audio/{env,reverb}.c. The DSP algorithms below (ADPCM + * decode, linear resample, linear envelope mix) are the same Nintendo/SGI + * ucode math PD's port/src/mixer.c ported (scalar path); only the ABI/ + * addressing layer here is GE-specific. * - * STATUS: scaffolding stub — implement during Phase 3. The exact interface - * depends on how GE's libaudio output is wired (see src/audi.c). + * GE's aSetBuffer(flags, in, out, count) sets a small persistent context + * that several opcodes read instead of taking DMEM addresses/counts + * directly (see MixerCtx below) — reconstructed from reading every call + * site (docs/dev/findings.md D199), not from RSP disassembly. */ +#include +#include + +#include +#include + #include "platform.h" +#include "system.h" #include "mixer.h" #include "audio.h" +/* Largest DMEM address GE's audio call sites use is AL_AUX_R_OUT (2048) + + * up to AL_MAX_RSP_SAMPLES (160) samples * 2 bytes; round up generously. */ +#define DMEM_SIZE 4096 + +static u8 sDmem[DMEM_SIZE]; + +#define DMEM_U8(a) (sDmem + (a)) +#define DMEM_S16(a) ((s16 *)(sDmem + (a))) + +static inline s16 mixerClamp16(s32 v) +{ + if (v < -0x8000) return -0x8000; + if (v > 0x7fff) return 0x7fff; + return (s16)v; +} + +/* + * Persistent context set by aSetBuffer, read by opcodes that don't carry + * their own DMEM addresses/counts. Reconstructed from call-site pairs: + * + * resample.c: aSetBuffer(0, inp, *outp, outCnt<<1); aResample(...) + * load.c: aSetBuffer(0, dmemAddr, 0, nbytes...); aLoadBuffer(dram) + * reverb.c: aSetBuffer(0, 0, buff, count<<1); aSaveBuffer(dram) + * mainbus.c: aSetBuffer(0, 0, 0, outCount<<1); aMix(...); aMix(...) + * save.c: aSetBuffer(0, 0, 0, outCount<<1); aInterleave(L, R) + * env.c: aSetBuffer(A_MAIN, *inp, AL_MAIN_L_OUT+*outp, outCount<<1); + * aSetBuffer(A_AUX, AL_MAIN_R_OUT+*outp, AL_AUX_L_OUT+*outp, + * AL_AUX_R_OUT+*outp); + * aEnvMixer(...) + * + * i.e. LoadBuffer/ADPCMdec/Resample read `in` as their DMEM source (or, for + * LoadBuffer, as the load destination); Resample/EnvMixer read `out` as a + * destination too; SaveBuffer/Interleave read `out` as their DMEM source; + * Mix/DMEMMove/ClearBuffer take explicit addresses and don't touch this + * context at all. The A_AUX-flagged aSetBuffer call packs three more + * envmixer-only DMEM addresses (dry-R, wet-L, wet-R) — its third argument + * is a DMEM address here, not a byte count, unlike every other opcode's use + * of aSetBuffer's `count` field. + */ +static struct { + u16 in; + u16 out; + u32 count; + u16 dryR; + u16 wetL; + u16 wetR; +} sCtx; + +void aSetBufferImpl(u32 flags, u16 i, u16 o, u16 c) +{ + if (flags & A_AUX) { + sCtx.dryR = i; + sCtx.wetL = o; + sCtx.wetR = c; + } else { + sCtx.in = i; + sCtx.out = o; + sCtx.count = c; + } +} + +void aClearBufferImpl(u16 addr, u32 count) +{ + memset(DMEM_U8(addr), 0, count); +} + + +void aLoadBufferImpl(u32 dramAddr) +{ + memcpy(DMEM_U8(sCtx.in), osPhysicalToVirtual(dramAddr), sCtx.count); +} + +void aSaveBufferImpl(u32 dramAddr) +{ + memcpy(osPhysicalToVirtual(dramAddr), DMEM_U8(sCtx.out), sCtx.count); +} + +void aDMEMMoveImpl(u16 in, u16 out, u32 count) +{ + memmove(DMEM_U8(out), DMEM_U8(in), count); +} + +void aSegmentImpl(u32 seg, u32 base) +{ + /* No-op on PC: GE's audio DMA addresses are already resolved via + * osVirtualToPhysical before reaching us (D199); segment/base never + * factor into DMEM addressing here. */ + (void)seg; (void)base; +} + +/* ------------------------------------------------------------------------ */ +/* ADPCM */ +/* ------------------------------------------------------------------------ */ + +static s16 sAdpcmTable[8][2][8]; +static ADPCM_STATE *sAdpcmLoopState; + +void aLoadADPCMImpl(u32 count, u32 dramAddr) +{ + /* `count` is GE's bookSize in bytes (2 * order * npredictors * 8); + * order=2 always for this format, matching sAdpcmTable's shape. */ + u32 n = count; + void *src = osPhysicalToVirtual(dramAddr); + if (n > sizeof(sAdpcmTable)) n = sizeof(sAdpcmTable); + memcpy(sAdpcmTable, src, n); +} + +void aSetLoopImpl(u32 stateAddr) +{ + sAdpcmLoopState = (ADPCM_STATE *)osPhysicalToVirtual(stateAddr); +} + +void aADPCMdecImpl(u32 flags, u32 stateAddr) +{ + ADPCM_STATE *state = (ADPCM_STATE *)osPhysicalToVirtual(stateAddr); + u8 *in = DMEM_U8(sCtx.in); + s16 *out = DMEM_S16(sCtx.out); + s32 nbytes = (s32)((sCtx.count + 31) & ~31u); /* round up to 16-sample (32-byte) chunks */ + + if (flags & A_INIT) { + memset(out, 0, 16 * sizeof(s16)); + } else if (flags & A_LOOP) { + memcpy(out, sAdpcmLoopState, 16 * sizeof(s16)); + } else { + memcpy(out, state, 16 * sizeof(s16)); + } + out += 16; + + while (nbytes > 0) { + int shift = *in >> 4; + int tableIndex = *in++ & 0xf; + s16 (*tbl)[8] = sAdpcmTable[tableIndex]; + int i, j, k; + + for (i = 0; i < 2; i++) { + s16 ins[8]; + s16 prev1 = out[-1]; + s16 prev2 = out[-2]; + + for (j = 0; j < 4; j++) { + ins[j * 2] = (s16)((((*in >> 4) << 28) >> 28) << shift); + ins[j * 2 + 1] = (s16)((((*in++ & 0xf) << 28) >> 28) << shift); + } + for (j = 0; j < 8; j++) { + s32 acc = tbl[0][j] * prev2 + tbl[1][j] * prev1 + ((s32)ins[j] << 11); + for (k = 0; k < j; k++) { + acc += tbl[1][(j - k) - 1] * ins[k]; + } + acc >>= 11; + *out++ = mixerClamp16(acc); + } + } + nbytes -= 16 * (s32)sizeof(s16); + } + + memcpy(state, out - 16, 16 * sizeof(s16)); +} + +/* ------------------------------------------------------------------------ */ +/* Resample (linear, N64 64-phase table) */ +/* ------------------------------------------------------------------------ */ + +static const s16 sResampleTable[64][4] = { + {0x0c39, 0x66ad, 0x0d46, 0xffdf}, {0x0b39, 0x6696, 0x0e5f, 0xffd8}, + {0x0a44, 0x6669, 0x0f83, 0xffd0}, {0x095a, 0x6626, 0x10b4, 0xffc8}, + {0x087d, 0x65cd, 0x11f0, 0xffbf}, {0x07ab, 0x655e, 0x1338, 0xffb6}, + {0x06e4, 0x64d9, 0x148c, 0xffac}, {0x0628, 0x643f, 0x15eb, 0xffa1}, + {0x0577, 0x638f, 0x1756, 0xff96}, {0x04d1, 0x62cb, 0x18cb, 0xff8a}, + {0x0435, 0x61f3, 0x1a4c, 0xff7e}, {0x03a4, 0x6106, 0x1bd7, 0xff71}, + {0x031c, 0x6007, 0x1d6c, 0xff64}, {0x029f, 0x5ef5, 0x1f0b, 0xff56}, + {0x022a, 0x5dd0, 0x20b3, 0xff48}, {0x01be, 0x5c9a, 0x2264, 0xff3a}, + {0x015b, 0x5b53, 0x241e, 0xff2c}, {0x0101, 0x59fc, 0x25e0, 0xff1e}, + {0x00ae, 0x5896, 0x27a9, 0xff10}, {0x0063, 0x5720, 0x297a, 0xff02}, + {0x001f, 0x559d, 0x2b50, 0xfef4}, {0xffe2, 0x540d, 0x2d2c, 0xfee8}, + {0xffac, 0x5270, 0x2f0d, 0xfedb}, {0xff7c, 0x50c7, 0x30f3, 0xfed0}, + {0xff53, 0x4f14, 0x32dc, 0xfec6}, {0xff2e, 0x4d57, 0x34c8, 0xfebd}, + {0xff0f, 0x4b91, 0x36b6, 0xfeb6}, {0xfef5, 0x49c2, 0x38a5, 0xfeb0}, + {0xfedf, 0x47ed, 0x3a95, 0xfeac}, {0xfece, 0x4611, 0x3c85, 0xfeab}, + {0xfec0, 0x4430, 0x3e74, 0xfeac}, {0xfeb6, 0x424a, 0x4060, 0xfeaf}, + {0xfeaf, 0x4060, 0x424a, 0xfeb6}, {0xfeac, 0x3e74, 0x4430, 0xfec0}, + {0xfeab, 0x3c85, 0x4611, 0xfece}, {0xfeac, 0x3a95, 0x47ed, 0xfedf}, + {0xfeb0, 0x38a5, 0x49c2, 0xfef5}, {0xfeb6, 0x36b6, 0x4b91, 0xff0f}, + {0xfebd, 0x34c8, 0x4d57, 0xff2e}, {0xfec6, 0x32dc, 0x4f14, 0xff53}, + {0xfed0, 0x30f3, 0x50c7, 0xff7c}, {0xfedb, 0x2f0d, 0x5270, 0xffac}, + {0xfee8, 0x2d2c, 0x540d, 0xffe2}, {0xfef4, 0x2b50, 0x559d, 0x001f}, + {0xff02, 0x297a, 0x5720, 0x0063}, {0xff10, 0x27a9, 0x5896, 0x00ae}, + {0xff1e, 0x25e0, 0x59fc, 0x0101}, {0xff2c, 0x241e, 0x5b53, 0x015b}, + {0xff3a, 0x2264, 0x5c9a, 0x01be}, {0xff48, 0x20b3, 0x5dd0, 0x022a}, + {0xff56, 0x1f0b, 0x5ef5, 0x029f}, {0xff64, 0x1d6c, 0x6007, 0x031c}, + {0xff71, 0x1bd7, 0x6106, 0x03a4}, {0xff7e, 0x1a4c, 0x61f3, 0x0435}, + {0xff8a, 0x18cb, 0x62cb, 0x04d1}, {0xff96, 0x1756, 0x638f, 0x0577}, + {0xffa1, 0x15eb, 0x643f, 0x0628}, {0xffac, 0x148c, 0x64d9, 0x06e4}, + {0xffb6, 0x1338, 0x655e, 0x07ab}, {0xffbf, 0x11f0, 0x65cd, 0x087d}, + {0xffc8, 0x10b4, 0x6626, 0x095a}, {0xffd0, 0x0f83, 0x6669, 0x0a44}, + {0xffd8, 0x0e5f, 0x6696, 0x0b39}, {0xffdf, 0x0d46, 0x66ad, 0x0c39} +}; + +void aResampleImpl(u32 flags, u16 pitch, u32 stateAddr) +{ + RESAMPLE_STATE *stateBuf = (RESAMPLE_STATE *)osPhysicalToVirtual(stateAddr); + s16 *state = (s16 *)stateBuf; + s16 tmp[16]; + s16 *inInitial = DMEM_S16(sCtx.in); + s16 *in = inInitial; + s16 *out = DMEM_S16(sCtx.out); + s32 nbytes = (s32)((sCtx.count + 15) & ~15u); + u32 pitchAccumulator; + s32 i; + + if (flags & A_INIT) { + memset(tmp, 0, 5 * sizeof(s16)); + } else { + memcpy(tmp, state, 16 * sizeof(s16)); + } + in -= 4; + pitchAccumulator = (u16)tmp[4]; + memcpy(in, tmp, 4 * sizeof(s16)); + + do { + for (i = 0; i < 8; i++) { + const s16 *tbl = sResampleTable[(pitchAccumulator * 64) >> 16]; + s32 sample = ((in[0] * tbl[0] + 0x4000) >> 15) + + ((in[1] * tbl[1] + 0x4000) >> 15) + + ((in[2] * tbl[2] + 0x4000) >> 15) + + ((in[3] * tbl[3] + 0x4000) >> 15); + *out++ = mixerClamp16(sample); + + pitchAccumulator += (u32)pitch << 1; + in += pitchAccumulator >> 16; + pitchAccumulator %= 0x10000; + } + nbytes -= 8 * (s32)sizeof(s16); + } while (nbytes > 0); + + state[4] = (s16)pitchAccumulator; + memcpy(state, in, 4 * sizeof(s16)); + i = (s32)((in - inInitial + 4) & 7); + in -= i; + if (i != 0) i = -8 - i; + state[5] = (s16)i; + memcpy(state + 8, in, 8 * sizeof(s16)); +} + +/* ------------------------------------------------------------------------ */ +/* Interleave / DMEM mix */ +/* ------------------------------------------------------------------------ */ + +void aInterleaveImpl(u16 l, u16 r) +{ + const s16 *lp = DMEM_S16(l); + const s16 *rp = DMEM_S16(r); + s16 *d = DMEM_S16(sCtx.out); + u32 n = sCtx.count >> 1; /* mono sample count */ + u32 i; + + for (i = 0; i < n; i++) { + *d++ = *lp++; + *d++ = *rp++; + } +} + +void aMixImpl(u32 flags, u16 gain, u16 in, u16 out) +{ + const s16 *inp = DMEM_S16(in); + s16 *outp = DMEM_S16(out); + u32 n = sCtx.count >> 1; + u32 i; + (void)flags; + + for (i = 0; i < n; i++) { + s32 sample = ((s32)*outp * 0x7fff + (s32)*inp++ * (s16)gain + 0x4000) >> 15; + *outp++ = mixerClamp16(sample); + } +} + +/* ------------------------------------------------------------------------ */ +/* Volume / envelope mixer */ +/* ------------------------------------------------------------------------ */ + +static struct { + s16 volCur[2]; + s16 volTgt[2]; + s32 volRate[2]; + s16 dryamt, wetamt; +} sVol; + +void aSetVolumeImpl(u32 flags, u16 v, u16 t, u16 r) +{ + if (flags & A_AUX) { + sVol.dryamt = (s16)v; + sVol.wetamt = (s16)r; + } else if (flags & A_VOL) { + int ch = (flags & A_LEFT) ? 0 : 1; + sVol.volCur[ch] = (s16)v; + } else { + int ch = (flags & A_LEFT) ? 0 : 1; + sVol.volTgt[ch] = (s16)v; + sVol.volRate[ch] = ((u32)t << 16) | (u16)r; + } +} + +void aEnvMixerImpl(u32 flags, u32 stateAddr) +{ + struct { + s32 t[2]; + s32 rate[2]; + s16 tgt[2]; + s16 voldry; + s16 volwet; + } *saved = (void *)osPhysicalToVirtual(stateAddr); + + const s16 *in = DMEM_S16(sCtx.in); + s16 *dry[2] = { DMEM_S16(sCtx.out), DMEM_S16(sCtx.dryR) }; + s16 *wet[2] = { DMEM_S16(sCtx.wetL), DMEM_S16(sCtx.wetR) }; + u32 nsamples = sCtx.count >> 1; + + s32 t[2], tgt[2], rate[2]; + s16 voldry, volwet; + u32 i; + int j; + + if (flags & A_INIT) { + for (j = 0; j < 2; j++) { + t[j] = sVol.volCur[j] << 16; + rate[j] = sVol.volRate[j] >> 3; + tgt[j] = sVol.volTgt[j] << 16; + } + voldry = sVol.dryamt; + volwet = sVol.wetamt; + } else { + for (j = 0; j < 2; j++) { + t[j] = saved->t[j]; + rate[j] = saved->rate[j]; + tgt[j] = saved->tgt[j] << 16; + } + voldry = saved->voldry; + volwet = saved->volwet; + } + + for (i = 0; i < nsamples; i++) { + s16 gain[4]; + s16 vol[2]; + s16 insamp = in[i]; + + for (j = 0; j < 2; j++) { + t[j] += rate[j]; + if ((rate[j] <= 0 && t[j] <= tgt[j]) || (rate[j] > 0 && t[j] >= tgt[j])) { + t[j] = tgt[j]; + rate[j] = 0; + } + vol[j] = (s16)(t[j] >> 16); + } + + gain[0] = mixerClamp16(((s32)vol[0] * voldry + 0x4000) >> 15); + gain[1] = mixerClamp16(((s32)vol[1] * voldry + 0x4000) >> 15); + gain[2] = mixerClamp16(((s32)vol[0] * volwet + 0x4000) >> 15); + gain[3] = mixerClamp16(((s32)vol[1] * volwet + 0x4000) >> 15); + + dry[0][i] = mixerClamp16(dry[0][i] + (((s32)insamp * gain[0]) >> 15)); + dry[1][i] = mixerClamp16(dry[1][i] + (((s32)insamp * gain[1]) >> 15)); + wet[0][i] = mixerClamp16(wet[0][i] + (((s32)insamp * gain[2]) >> 15)); + wet[1][i] = mixerClamp16(wet[1][i] + (((s32)insamp * gain[3]) >> 15)); + } + + for (j = 0; j < 2; j++) { + saved->t[j] = t[j]; + saved->rate[j] = rate[j]; + saved->tgt[j] = (s16)(tgt[j] >> 16); + } + saved->voldry = voldry; + saved->volwet = volwet; +} + +/* ------------------------------------------------------------------------ */ +/* Pole filter (reverb/chorus lowpass) */ +/* ------------------------------------------------------------------------ */ + +void aPoleFilterImpl(u32 flags, u16 gain, u32 stateAddr) +{ + /* PC port (D199): identity passthrough — reverb/chorus tone-shaping + * (CUSTOM_FX_PARAMS_N) is not implemented. Affects reverb/chorus + * character only, not silence/garbling (docs/dev/AUDIO-PLAN.md risk 1). + * DMEM in-place buffer (sCtx set by the caller's aSetBuffer(buf,buf,..)) + * is left untouched, which is the correct no-op for this filter shape. */ + (void)flags; (void)gain; (void)stateAddr; +} + +/* ------------------------------------------------------------------------ */ + void mixerInit(void) { - /* TODO(Phase 3) */ + memset(sDmem, 0, sizeof(sDmem)); + memset(sAdpcmTable, 0, sizeof(sAdpcmTable)); + memset(&sCtx, 0, sizeof(sCtx)); + memset(&sVol, 0, sizeof(sVol)); + sAdpcmLoopState = NULL; } void mixerDestroy(void) { - /* TODO(Phase 3) */ -} - -void mixerProcess(const s16 *in, u32 frames) -{ - /* TODO(Phase 3): process + queue via audioSetNextBuffer(). */ - (void)in; (void)frames; } diff --git a/src/libultra/audio/bnkf.c b/src/libultra/audio/bnkf.c index e49ef36b..55cbd24d 100644 --- a/src/libultra/audio/bnkf.c +++ b/src/libultra/audio/bnkf.c @@ -21,19 +21,30 @@ #include #include #include +#include /* * ### when the file format settles down a little, I'll remove these * ### for efficiency. + * + * PC port (D201): these relocation offsets were `s32`, truncating the real + * 64-bit base pointer. On N64 (32-bit pointers) that's a no-op; on PC it + * silently corrupted every bank/inst/sound/wavetable/book/loop pointer + * relocated below whenever the base allocation's low 32 bits had the top + * bit set (~50% of allocations, allocator-dependent) — sign-extension of + * the truncated s32 during pointer arithmetic reconstructs the wrong 64-bit + * address. `uintptr_t` is already this codebase's idiom for pointer-safe + * integers (src/game/*.c); widening these to it changes nothing on the + * 32-bit N64 build and fixes PC. See docs/dev/findings.md D201. */ -static void _bnkfPatchBank(ALBank *bank, s32 offset, s32 table); -static void _bnkfPatchInst(ALInstrument *i, s32 offset, s32 table); -static void _bnkfPatchSound(ALSound *s, s32 offset, s32 table); -static void _bnkfPatchWaveTable(ALWaveTable *w, s32 offset, s32 table); +static void _bnkfPatchBank(ALBank *bank, uintptr_t offset, uintptr_t table); +static void _bnkfPatchInst(ALInstrument *i, uintptr_t offset, uintptr_t table); +static void _bnkfPatchSound(ALSound *s, uintptr_t offset, uintptr_t table); +static void _bnkfPatchWaveTable(ALWaveTable *w, uintptr_t offset, uintptr_t table); void alSeqFileNew(ALSeqFile *file, u8 *base) { - s32 offset = (s32) base; + uintptr_t offset = (uintptr_t) base; s32 i; /* @@ -46,9 +57,9 @@ void alSeqFileNew(ALSeqFile *file, u8 *base) void alBnkfNew(ALBankFile *file, u8 *table) { - s32 offset = (s32) file; - s32 woffset = (s32) table; - + uintptr_t offset = (uintptr_t) file; + uintptr_t woffset = (uintptr_t) table; + s32 i; /* @@ -66,7 +77,7 @@ void alBnkfNew(ALBankFile *file, u8 *table) } } -void _bnkfPatchBank(ALBank *bank, s32 offset, s32 table) +void _bnkfPatchBank(ALBank *bank, uintptr_t offset, uintptr_t table) { s32 i; @@ -88,7 +99,7 @@ void _bnkfPatchBank(ALBank *bank, s32 offset, s32 table) } } -void _bnkfPatchInst(ALInstrument *inst, s32 offset, s32 table) +void _bnkfPatchInst(ALInstrument *inst, uintptr_t offset, uintptr_t table) { s32 i; @@ -105,7 +116,7 @@ void _bnkfPatchInst(ALInstrument *inst, s32 offset, s32 table) } } -void _bnkfPatchSound(ALSound *s, s32 offset, s32 table) +void _bnkfPatchSound(ALSound *s, uintptr_t offset, uintptr_t table) { if (s->flags) return; @@ -119,7 +130,7 @@ void _bnkfPatchSound(ALSound *s, s32 offset, s32 table) _bnkfPatchWaveTable(s->wavetable, offset, table); } -void _bnkfPatchWaveTable(ALWaveTable *w, s32 offset, s32 table) +void _bnkfPatchWaveTable(ALWaveTable *w, uintptr_t offset, uintptr_t table) { if (w->flags) return; diff --git a/src/libultra/audio/load.c b/src/libultra/audio/load.c index 8e53c11b..4ca645df 100644 --- a/src/libultra/audio/load.c +++ b/src/libultra/audio/load.c @@ -23,6 +23,8 @@ #include "synthInternals.h" #include #include +#if defined(__x86_64__) +#endif #ifndef MIN # define MIN(a,b) (((a)<(b))?(a):(b)) @@ -38,6 +40,7 @@ extern u32 cnt_index, adpcm_num, adpcm_cnt, adpcm_max, adpcm_min, lastCnt[]; static Acmd *_decodeChunk(Acmd *ptr, ALLoadFilter *f, s32 tsam, s32 nbytes, s16 outp, s16 inp, u32 flags); + Acmd *alAdpcmPull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd *p) { Acmd *ptr = p; @@ -65,8 +68,10 @@ Acmd *alAdpcmPull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd return ptr; inp = AL_DECODER_IN; + /* PC port (D198): was K0_TO_PHYS — same corrupted-pointer bug as the + * f->state/f->lstate call sites below; see docs/dev/findings.md D198. */ aLoadADPCM(ptr++, f->bookSize, - K0_TO_PHYS(f->table->waveInfo.adpcmWave.book->book)); + osVirtualToPhysical(f->table->waveInfo.adpcmWave.book->book)); looped = (outCount + f->sample > f->loop.end) && (f->loop.count != 0); if (looped) @@ -231,6 +236,7 @@ Acmd *alRaw16Pull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd nbytes = nSam<<1; if (nSam > 0){ dramLoc = (f->dma)(f->memin, nbytes, f->dmaState); + /* * Make sure enough is loaded into DMEM to take care @@ -319,6 +325,7 @@ Acmd *alRaw16Pull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd if (outCount > 0){ nbytes -= overFlow; dramLoc = (f->dma)(f->memin, nbytes, f->dmaState); + /* * Make sure enough is loaded into DMEM to take care @@ -458,11 +465,18 @@ Acmd *_decodeChunk(Acmd *ptr, ALLoadFilter *f, s32 tsam, s32 nbytes, s16 outp, s dramAlign = 0; if (flags & A_LOOP){ - aSetLoop(ptr++, K0_TO_PHYS(f->lstate)); + /* PC port (D198): was K0_TO_PHYS(f->lstate), an unconditional + * `& 0x1FFFFFFF` mask that corrupts 64-bit heap pointers above + * 512MB. Every sibling call site (env.c, resample.c, reverb.c) + * already resolves state pointers via osVirtualToPhysical, which + * is bit-identical to K0_TO_PHYS for real N64 kseg0 addresses but + * PC-shimmed correctly (port/src/libultra.c). No behavior change + * on N64; see docs/dev/findings.md D198. */ + aSetLoop(ptr++, osVirtualToPhysical(f->lstate)); } - + aSetBuffer(ptr++, 0, inp + dramAlign, outp, tsam<<1); - aADPCMdec(ptr++, flags, K0_TO_PHYS(f->state)); + aADPCMdec(ptr++, flags, osVirtualToPhysical(f->state)); f->first = 0; return ptr; diff --git a/src/libultra/audio/save.c b/src/libultra/audio/save.c index 9ce161e3..fe68dd30 100644 --- a/src/libultra/audio/save.c +++ b/src/libultra/audio/save.c @@ -21,6 +21,8 @@ #include #include "synthInternals.h" #include +#if defined(__x86_64__) +#endif #include "include/assert.h" Acmd *alSavePull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, diff --git a/src/libultra/audio/synsetfxmix.c b/src/libultra/audio/synsetfxmix.c index 63a26b27..4665ba11 100644 --- a/src/libultra/audio/synsetfxmix.c +++ b/src/libultra/audio/synsetfxmix.c @@ -19,6 +19,8 @@ *====================================================================*/ #include "synthInternals.h" +#if defined(__x86_64__) +#endif #include #include diff --git a/src/libultra/audio/synsetpan.c b/src/libultra/audio/synsetpan.c index 0d68538a..7fd44436 100644 --- a/src/libultra/audio/synsetpan.c +++ b/src/libultra/audio/synsetpan.c @@ -19,6 +19,8 @@ *====================================================================*/ #include "synthInternals.h" +#if defined(__x86_64__) +#endif #include #include diff --git a/src/libultra/audio/synsetvol.c b/src/libultra/audio/synsetvol.c index 181c96f2..c8f8a73a 100644 --- a/src/libultra/audio/synsetvol.c +++ b/src/libultra/audio/synsetvol.c @@ -19,6 +19,8 @@ *====================================================================*/ #include "synthInternals.h" +#if defined(__x86_64__) +#endif #include #include diff --git a/src/libultrare/audio/reverb.c b/src/libultrare/audio/reverb.c index 469be7e8..d47c83ef 100644 --- a/src/libultrare/audio/reverb.c +++ b/src/libultrare/audio/reverb.c @@ -92,8 +92,11 @@ Acmd *alFxPull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, for (i = 0; i < r->section_count; i++) { d = &r->delay[i]; /* get the ALDelay structure */ - in_ptr = &r->input[-d->input]; - out_ptr = &r->input[-d->output]; + /* PC port (D200): d->input/d->output are u32, so -d->x is a u32 that + zero-extends to a +4GB offset in 64-bit pointer arithmetic; negating + in s32 restores the N64's s32-ptrdiff wrap (PD ground truth). */ + in_ptr = &r->input[(s32)-d->input]; + out_ptr = &r->input[(s32)-d->output]; if (in_ptr == prev_out_ptr) { SWAP(buff1, buff2); @@ -265,8 +268,8 @@ Acmd *_loadOutputBuffer(ALFx *r, ALDelay *d, s32 buff, s32 incount, Acmd *p) * negative of that as an index into the delay buffer. loadBuffer that uses this * value then bumps it up if it is below the delay buffer. */ - out_ptr = &r->input[-(d->output - d->rsdelta)]; - ramalign = ((s32)out_ptr & 0x7) >> 1; /* calculate the number of samples needed + out_ptr = &r->input[(s32)-(d->output - d->rsdelta)]; + ramalign = ((s64)out_ptr & 0x7) >> 1; /* calculate the number of samples needed to align the buffer*/ #ifdef _DEBUG #if 0 @@ -295,7 +298,7 @@ Acmd *_loadOutputBuffer(ALFx *r, ALDelay *d, s32 buff, s32 incount, Acmd *p) d->rs->first = 0; /* turn off first time flag */ d->rsdelta += count - incount; /* add the number of samples to d->rsdelta */ } else { - out_ptr = &r->input[-d->output]; + out_ptr = &r->input[(s32)-d->output]; ptr = _loadBuffer(r, out_ptr, buff, incount, ptr); }