mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
port(audio): Phase-3 software mixer + K0_TO_PHYS/bnkf pointer fixes (D198-201)
Software RSP-audio-ucode mixer (macro-swap aXxx -> Impl functions, PD-port pattern) makes libaudio output audible on PC. Alongside it, two pointer-width bugs surfaced by exercising the audio path for the first time: - D198: alAdpcmPull/_decodeChunk used K0_TO_PHYS (N64 kseg0 mask) on 64-bit heap pointers; switched to osVirtualToPhysical like every sibling call site. - D201: bnkf.c bank/inst/sound/wavetable relocation offsets were s32, truncating the real 64-bit base pointer for ~50% of allocations; widened to uintptr_t. - D200 (already committed-pending from a prior session): reverb's ALDelay u32 negative indices zero-extended into +4GB offsets on 64-bit; (s32)/(s64) casts match PD ground truth. - D199: documents the mixer's aSetBuffer persistent-DMEM-context design, reconstructed from call-site pairs (no RSP disassembly to check against) since there's no N64 audio ucode source to reference. - D195/D197 (playtest reports referenced by an earlier HANDOFF note but never written into findings.md): backfilled as OPEN with next steps. Verified: ./build-pc.sh ntsc-final links clean; -level_09 runs 600+ frames crash-free with music + SFX audible.
This commit is contained in:
1 parent
6d6609a550
commit
a9cc629de6
15 files changed
+773
-417
No files matched your search
+160
-343
@@ -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 |
|
||||
@@ -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` (`<stdint.h>`) — 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<<mask`, and GE sets `mask = texDimensionToMask(dim) = ceil(log2(dim))` (`src/game/tex.c:361`), so a non-PoT tile (Depot's 65×65 / 96×48 / 56×56 room surfaces) repeats at the **next power of two**, not at its image size the way GL `GL_REPEAT` does → the pattern is squashed/stretched and the seam lands in the wrong place. fast3d never stored `masks`/`maskt` at all (`gfx_dp_set_tile` dropped them) and wrapped purely at the uploaded image dimension. **Fix (`port/fast3d/gfx_pc.cpp`, behind the existing `Video.WrapFix` knob, default OFF):** store `masks`/`maskt` on the tile; in the hoisted per-texunit pre-wrap block in `gfx_sp_tri1` (D74 block — already lifted out of the vertex loop, indexed by texunit `t` not vertex `i`), when the tile is WRAP (no CLAMP/MIRROR bit) and `1<<mask != tex_width`, fold the UV at the N64 period `1<<mask` and clamp the `[dim, 1<<mask)` overflow band (which is a TMEM smear on console, no real texels) to the last texel so it reads as an edge streak instead of a bogus early image restart. `GE_WRAPFIX=0/1` env override added (env wins over the ini, matching `Debug.FrameDump`). **Per-level captures (`-level_09`/`-30`/`-34`/`-20`, WrapFix OFF vs ON, `GE_PCDUMP` 6-frame windows):** no crashes, 6/6 frames each; on settled/comparable frames Silo is ~pixel-identical (phash 0–11), Facility 180–260 pixel-identical, Depot shows small localized texel changes on ceiling/wall cells (dmean 5–9, no structural break); the large per-run deltas are all the D117 intro-camera-pan nondeterminism, not the fix. Default kept **OFF** — no regression, but a headless structural diff can't confirm the Depot ceiling actually looks *better*; needs a human eyeball with `Video.WrapFix=1`. Default-off is byte-identical to golden (all new behaviour is inside `if (g_wrap_fix)`). D74's dead in-vertex-loop wrap block was already reworked/hoisted at M-30; this only adds the mask-period trigger. Confidence: **medium** (mechanism correct; overflow-band handling is an approximation, not exact TMEM-smear emulation; visual win unconfirmed). porting-notes.md §D. | KNOB ADDED, default OFF (`port/fast3d/gfx_pc.cpp`). Needs user visual check on Depot. |
|
||||
| D168 | **`GE_PCDUMP` / F12 PPM captures were vertically flipped — the entire source of the bogus D114/D116 "HUD/text X-mirror" (M-33, developer-confirmed).** `gfx_opengl_dump_bound_fbo` (`port/fast3d/gfx_opengl.cpp`) wrote `glReadPixels` output straight to a P6 PPM. GL framebuffer origin is bottom-left; PPM P6 is top-row-first — so every capture (and the `tools_pc/golden/` set, and every screenshot pasted into the finding log since M-6) was upside-down. On real hardware / an actual screen the game renders correctly (developer confirmed). The successive D114→D116 "shared fast3d mirror" investigations — each of which found *every probed stage clean yet the output "mirrored"* — were reading an inverted capture and pattern-matching upside-down asymmetric content (text, guards, the Nintendo logo) as "mirrored". Fix: emit PPM rows bottom-to-top. `tools_pc/golden/*.png` flipped in place to match (see `tools_pc/golden/README.md`); regenerate from a real run when convenient. **Not runtime-verified this session** (no ROM / toolchain in the migrated tree — see the migration note) — needs a fresh capture to confirm text reads normally. Confidence: **high** (mechanism is unambiguous; developer has hardware confirmation the screen is correct). porting-notes.md §D2. | FIXED (`port/fast3d/gfx_opengl.cpp`); D114 / D116 reclassified as capture-orientation artifacts (below). |
|
||||
@@ -5451,3 +5457,40 @@ byte-identical to the known-good `pccg.bin` (3604378 B) / `pcmodels.bin`;
|
||||
`-level_09` + `-level_20` boot crash-free. The CRLF→LF observation on
|
||||
`filelist.u.csv` / `file_resource_table.inc.c` is real but a red herring for
|
||||
this crash (d69 tolerates it; output unchanged).
|
||||
|
||||
## D200 — `-level_09` segfault: u32 reverb delay indices negate into +4GB offsets on 64-bit (fixed)
|
||||
|
||||
**Symptom.** `-level_09` reliably segfaulted within seconds of audio starting, in
|
||||
`_filterBuffer` (`src/libultrare/audio/reverb.c`) — `d->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).
|
||||
@@ -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_ */
|
||||
@@ -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
|
||||
|
||||
+61
-11
@@ -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 <PR/ultratypes.h>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+19
-23
@@ -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 <SDL.h>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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; }
|
||||
|
||||
|
||||
+419
-14
@@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <PR/abi.h>
|
||||
#include <PR/os.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
+22
-11
@@ -21,19 +21,30 @@
|
||||
#include <libaudio.h>
|
||||
#include <os.h>
|
||||
#include <ultraerror.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* ### 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,8 +57,8 @@ 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;
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "synthInternals.h"
|
||||
#include <os.h>
|
||||
#include <R4300.h>
|
||||
#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)
|
||||
@@ -232,6 +237,7 @@ Acmd *alRaw16Pull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd
|
||||
if (nSam > 0){
|
||||
dramLoc = (f->dma)(f->memin, nbytes, f->dmaState);
|
||||
|
||||
|
||||
/*
|
||||
* Make sure enough is loaded into DMEM to take care
|
||||
* of 8 byte alignment
|
||||
@@ -320,6 +326,7 @@ Acmd *alRaw16Pull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset, Acmd
|
||||
nbytes -= overFlow;
|
||||
dramLoc = (f->dma)(f->memin, nbytes, f->dmaState);
|
||||
|
||||
|
||||
/*
|
||||
* Make sure enough is loaded into DMEM to take care
|
||||
* of 8 byte alignment
|
||||
@@ -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;
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include <libaudio.h>
|
||||
#include "synthInternals.h"
|
||||
#include <os.h>
|
||||
#if defined(__x86_64__)
|
||||
#endif
|
||||
#include "include/assert.h"
|
||||
|
||||
Acmd *alSavePull(void *filter, s16 *outp, s32 outCount, s32 sampleOffset,
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
*====================================================================*/
|
||||
|
||||
#include "synthInternals.h"
|
||||
#if defined(__x86_64__)
|
||||
#endif
|
||||
#include <os_internal.h>
|
||||
#include <ultraerror.h>
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
*====================================================================*/
|
||||
|
||||
#include "synthInternals.h"
|
||||
#if defined(__x86_64__)
|
||||
#endif
|
||||
#include <os_internal.h>
|
||||
#include <ultraerror.h>
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
*====================================================================*/
|
||||
|
||||
#include "synthInternals.h"
|
||||
#if defined(__x86_64__)
|
||||
#endif
|
||||
#include <os_internal.h>
|
||||
#include <ultraerror.h>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user