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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user