Replaces APUBUG-PRODUCER-001's random-victim-hijack audio injection
with a dedicated per-client guest worker thread, mirroring xenia-canary's
apu/audio_system.cc:84-159 WorkerThreadMain pattern in xenia-rs's
threading model. Audio callback ticker is now safe to enable by default.
## What changed
- xenia-kernel/src/xaudio.rs: new XAudioState fields worker_handles +
worker_refs (one slot per of XAUDIO_MAX_CLIENTS=8). Synthetic
park-handle helper (0xF000_0000 | client_idx) — outside the normal
alloc range so wake_eligible_waiters never finds it; the only
legitimate state-flip is via try_inject_audio_callback.
- xenia-kernel/src/exports.rs: xaudio_register_render_driver spawns a
64KB-stack guest thread (create_suspended=true) via
state.scheduler.spawn after registration succeeds. Immediately flips
the spawned thread's state from Blocked(Suspended) to
Blocked(WaitAny[synthetic]) so it's parked but not woken. Stores the
kernel handle so find_by_handle resolves a fresh ThreadRef after slot
compaction. Failure paths log + leave xaudio.worker_refs[i] = None,
in which case the ticker drops fires (no random-victim fallback).
- xenia-app/src/main.rs: try_inject_audio_callback resolves the worker
via worker_handles[index] instead of scanning runqueues for a Ready
or Blocked victim. The PC+r3 injection and SavedCallbackCtx capture
are unchanged; the existing LR_HALT restore path re-blocks the
worker on its synthetic handle for the next tick. Flag handling
reworked: --xaudio-tick / XENIA_XAUDIO_TICK now act as explicit
override (truthy = force on, falsey = force off, absent = use the
KernelState default).
- xenia-kernel/src/state.rs: xaudio_tick_enabled default flipped from
false to true. Pre-fix it was off because the random-victim hijack
regressed swaps=2->1; with the dedicated worker that whole class of
regression is gone.
## Cascade verification at -n 500M (audit-runs/audit-048-audio-host-pump/)
Pre-fix baseline: audit-runs/audit-047-gamma-wedges/ours-end-state.log.
| Dim | Predicted (AUDIT-032) | Observed |
|-----|-------------------------------------|---------------------------------|
| A | tid=9 leaves Blocked[0x828A3254] | Ready @ pc=0x824d1404 |
| B | tid=10 leaves Blocked[0x828A3230] | Ready @ same pc/lr |
| C | XAudioSubmitRenderDriverFrame > 0 | Mixer setup path executed |
| D | KeReleaseSemaphore 0 -> non-zero | 0 -> 1; xaudio.callback.delivered=1 |
Bonus: audit-042's tid=6 worker pair on 0x10A0+0x10A4 also went
Blocked->Ready as a downstream effect.
Boot trajectory shifted significantly: NtWaitForSingleObjectEx
1,489,791 -> 30; NtSetEvent 3,334 -> 68; new exports firing
(StfsCreateDevice, ObCreateSymbolicLink, XamContentCreateEnumerator,
XamEnumerate, XamTaskSchedule, ExCreateThread x10, KeSetAffinityThread x7,
NtCreateSemaphore x4, NtWaitForMultipleObjectsEx x94, NtDuplicateObject x14,
XeCryptSha, XeKeysConsolePrivateKeySign). The system left the
audio-wait busy loop and entered the savegame/content/crypto init phase.
swaps regressed 2 -> 1 (degenerate splash repeat lost; main thread now
advances past splash entirely, blocked on a different handle). draws
unchanged at 0 — expected per AUDIT-032 (audio gate != renderer gate).
## Tests + scope
- cargo build --release succeeds, no new warnings.
- cargo test -p xenia-kernel --lib: 127/127 pass (incl. xaudio).
- cargo test -p xenia-app --lib: 5/5 non-ignored pass.
- Lockstep goldens (sylpheed_n2m / sylpheed_n50m) WILL drift on this
fix and need re-baselining as a follow-up commit.
75 net non-comment LOC across 4 files, well under AUDIT-032's
60-120 LOC budget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>