feat(audio): APUBUG-PRODUCER-001 — XAudio register driver client + opt-in callback ticker

Replace the three XAudio kernel-export stubs (Register/Unregister/SubmitFrame)
with canary-faithful implementations and add a periodic buffer-complete
callback ticker reusing the existing SavedCallbackCtx injection machinery.

Canary parity:
- xboxkrnl_audio.cc:56-93 — read callback_ptr[0..1], wrap callback_arg in a
  4-byte big-endian guest heap buffer (`wrapped_callback_arg`), write
  `0x4155_xxxx` to *driver_ptr.
- audio_system.cc:139-141 — guest callback receives r3 = wrapped pointer,
  not raw callback_arg.
- audio_driver.h:21-24 — frame rate 256 samples / 48 kHz ≈ 5.33 ms.

Implementation:
- New `crates/xenia-kernel/src/xaudio.rs` — `XAudioClient`, `XAudioState`
  (8-slot table, pending FIFO, dual-mode ticker), `XAUDIO_INSTR_PERIOD =
  48_000` (lockstep) and `XAUDIO_PERIOD = 5.333 ms` (--parallel), same
  pattern as KRNBUG-D08 v-sync.
- `try_inject_audio_callback` in xenia-app mirrors `try_inject_graphics_interrupt`,
  shares `interrupts.saved` slot for mutex with graphics callbacks.

Gating: ticker + injector run only when `--xaudio-tick` /
`XENIA_XAUDIO_TICK=1`. Default off because Sylpheed's audio callback
enters an infinite `KeWaitForSingleObject` loop on first invocation
(canary's host worker thread provides the buffer-completion fence we
don't model), which hijacks a guest HW thread and regresses
`swaps=2 → 1`. Default-off preserves the lockstep `sylpheed_n*m.json`
goldens exactly.

Producer hunt outcome (FALSIFIED for parked handles 0x1004/0x100c/0x15e4):
at `-n 500M --xaudio-tick` all 3 handles still show
`signal_attempts=0 (primary=0, ghost=0)`. Audio callback is not the
missing producer. Next candidate per audit-findings.md is Timer DPC
delivery (KeSetTimer / KeInsertQueueDpc).

Tests: 562 → 576 green (10 in `xaudio.rs`, 4 in `exports.rs`).
Lockstep `--stable-digest -n 100M` default-off: instructions=100000002,
swaps=2 (matches pre-change baseline byte-for-byte).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-03 19:50:22 +02:00
parent 38f78c88a8
commit 07068e7616
6 changed files with 706 additions and 6 deletions

View File

@@ -4021,3 +4021,99 @@ known signal source on Xbox 360 audio engines; the stub may be
hiding the producer for one of the 3 handles. If that lead is also
falsified, escalate to file I/O completion (`signal_io_completion_event`
already real but possibly mis-routed) or Timer DPC delivery.
### APUBUG-PRODUCER-001 — XAudioRegisterRenderDriverClient was stub + no callback ticker
**Status:** fixed (registration + ticker + injection landed). Hypothesis
falsified for handles `0x1004` / `0x100c` / `0x15e4`.
**Site:** `crates/xenia-kernel/src/exports.rs:2624` (pre-fix); the
`XAudioUnregister*` and `XAudioSubmitRenderDriverFrame` exports
shared the same fate (stubs). New module: `crates/xenia-kernel/src/xaudio.rs`.
**Canary parity:**
- `xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_audio.cc:56-93`
(the three exports — register reads `callback_ptr[0..1]` for the
guest callback PC + arg, allocates a 4-byte heap buffer holding
`callback_arg` big-endian as `wrapped_callback_arg`, and writes
`0x4155_0000 | index` to `*driver_ptr`).
- `xenia-canary/src/xenia/apu/audio_system.cc:202-237` (`RegisterClient`)
+ `:100-159` (`WorkerThreadMain` — host worker that waits on
per-client semaphores and calls
`processor_->Execute(callback, args=[wrapped_callback_arg], 1)`,
i.e. r3 = wrapped pointer).
- `xenia-canary/src/xenia/apu/xaudio2/xaudio2_audio_driver.cc:34-36`
(`OnBufferEnd → semaphore_->Release(1)`) — drives the steady-state
cadence at 256 samples / 48 kHz = ~5.33 ms.
**Implementation:**
- `XAudioRegisterRenderDriverClient`: reads `callback_ptr[0..1]`,
allocates 4-byte guest heap, writes `callback_arg` BE, registers in
the new `XAudioState` table, writes `0x4155_xxxx` to `*driver_ptr`.
- `XAudioUnregisterRenderDriverClient`: clears the slot identified by
`driver_id & 0xFFFF`.
- `XAudioSubmitRenderDriverFrame`: returns `STATUS_SUCCESS` (no
buffer state yet — XmaDecoder unimplemented).
- `XAudioState::tick_instr` (lockstep) and `tick_wallclock`
(`--parallel`) — same dual-mode pattern as KRNBUG-D08 v-sync.
`XAUDIO_INSTR_PERIOD = 48_000` and `XAUDIO_PERIOD = 5.333 ms`
approximate canary's frame rate.
- `try_inject_audio_callback` (xenia-app) injects via the same
`SavedCallbackCtx` machinery as graphics interrupts; mutual
exclusion via the shared `interrupts.saved` slot. r3 is set to
`wrapped_callback_arg` per canary `processor_->Execute`.
**Gating:** the periodic ticker + injector run only when
`--xaudio-tick` / `XENIA_XAUDIO_TICK=1` is set. Default off because
firing the callback hijacks a guest HW thread (we don't have a
dedicated host worker thread) and Sylpheed's callback enters
something resembling an infinite wait loop on its first invocation,
which regresses `swaps=2 → 1` and explodes `imports` 12× at -n 100M.
Default-off preserves all existing lockstep goldens
(`sylpheed_n50m.json` etc.).
**Verification:**
- Workspace tests: 562 → 576 green (10 in `xaudio.rs` + 4 in
`exports.rs`).
- `--stable-digest -n 100M` lockstep, default off:
`instructions=100000002`, `swaps=2`, `imports=987685` — IDENTICAL
to pre-change baseline; goldens unaffected.
- `--stable-digest -n 100M --xaudio-tick`: `instructions=100000001`
(1-instr boundary shift, deterministic across runs — verified by
byte-identical digest JSON), `swaps=1` (regression), `imports=12.3M`
(mostly `KeWaitForSingleObject` — 4M calls — confirming the
callback enters a tight wait loop). 1 audio callback fires
(`xaudio.callback.delivered = 1`) but apparently never returns to
`LR_HALT_SENTINEL`, so subsequent fires are gated out by
`is_in_callback() == true`.
- `--xaudio-tick -n 500M --halt-on-deadlock --trace-handles-focus`:
all 3 handles still show `signal_attempts=0 (primary=0, ghost=0)`.
**Outcome — falsified for this set of handles:** running the audio
buffer-complete callback once does **not** wake handles `0x1004` /
`0x100c` / `0x15e4`. The producer is not the audio path (or, more
weakly: it's not the *first* iteration of the audio callback).
**Side effects worth noting for the next session:**
1. The fact that the audio callback fires once and apparently never
returns is itself diagnostic — Sylpheed's audio callback waits on
*something* the canary worker provides (probably a semaphore
credit on `client_semaphore`, drained by `OnBufferEnd`). Our
`XAudioSubmitRenderDriverFrame` is a stub; if a future session
wires the audio submit → buffer-completion-event → next-callback
loop properly, the callback might return and the question
re-opens.
2. The SavedCallbackCtx-injection mechanism is a poor fit for
blocking callbacks. Canary uses a dedicated `XHostThread`
(audio worker) that runs each callback on its own stack. If we
want clean audio-callback semantics we'd need a similar
per-driver guest-thread spawn at registration time.
### Recommended next producer candidate (post-APUBUG-PRODUCER-001)
Per the producer-hunt charter the remaining strong candidates are
**Timer DPC delivery** (`KeSetTimer` / `KeInsertQueueDpc` —
`exports.rs` has stubs/partials) and **file I/O completion event
routing**. Timer DPC is the next-strongest because the parked
handles are explicit `Event/Manual`s with no current waker, and
Xbox 360 timer-driven DPCs are a common signal source.