# Phase XAudio-Resume — ESCALATION (case IV) **Date**: 2026-05-19 **Outcome**: Resume mechanism is correctly implemented. The 60% missing event volume is gated on a DOWNSTREAM application-level spin-poll, not on the resume itself. No engine change landed. ## Canary's resume mechanism (Step 1+2) For each suspended XAudio worker (`entry_pc=0x824D2878` aff=16 → tid=14; `entry_pc=0x824D2940` aff=32 → tid=15), canary tid=6 (main) emits an identical 6-call sequence immediately after `ExCreateThread`: ``` canary tid=6 idx=106750..106766 (host_ns 1726.0..1726.2 ms) 106750 import.call ExCreateThread 106751 kernel.call ExCreateThread 106752 handle.create (raw_handle 0x???????? — tid=14 handle) 106753 thread.create (entry_pc=0x824d2878, suspended=true) 106754 kernel.return ExCreateThread 106755 import.call ObReferenceObjectByHandle 106756 kernel.call ObReferenceObjectByHandle 106757 kernel.return ObReferenceObjectByHandle 106758 import.call KeSetBasePriorityThread 106759 kernel.call KeSetBasePriorityThread 106760 kernel.return KeSetBasePriorityThread 106761 import.call KeResumeThread ← RESUME (xboxkrnl ord 146) 106762 kernel.call KeResumeThread 106763 kernel.return KeResumeThread 106764 import.call ObDereferenceObject 106765 kernel.call ObDereferenceObject 106766 kernel.return ObDereferenceObject ``` Block repeats verbatim at idx 106767-106783 for `entry_pc=0x824D2940`. Containing function is `XAudioRegisterRenderDriverClient` (visible at idx 106817). ## Ours's behavior at the matched site (Step 3) Cold ours (-n 500M, --halt-on-deadlock, fresh cache wipe), checked against `/tmp/ours-xaudio.jsonl` (121,569 events captured before halt): ``` ours tid=1 idx=106756..106786 (host_ns 1626 ms — boot is ~100 ms ahead of canary) 106756 import.call ExCreateThread 106757 kernel.call ExCreateThread 106758 handle.create 106759 thread.create (entry_pc=0x824d2878, suspended=true) ← matches canary 106760 kernel.return ExCreateThread ... 106767 import.call KeResumeThread ← RESUME fires 106768 kernel.call KeResumeThread 106769 kernel.return KeResumeThread ... 106776 thread.create (entry_pc=0x824d2940, suspended=true) ← matches canary ... 106784 import.call KeResumeThread ← second RESUME fires 106785 kernel.call KeResumeThread 106786 kernel.return KeResumeThread ``` ours's per-tid first-events (cold) for the spawned children: ``` tid=9 (=canary tid=14, entry 0x824d2878): 77 events, idx 0..76 identical to canary tid=14 tid=10 (=canary tid=15, entry 0x824d2940): 17 events, idx 0..16 identical to canary tid=15 ``` Ours's tid=9 / tid=10 EXECUTE the canary-matching XAudio init sequence: `KeWaitForSingleObject (with immediate signal)` → spinlock/IRQL cycle → `XAudioGetVoiceCategoryVolumeChangeMask` → `KeReleaseSemaphore` → more IRQL cycles. Then halt. Halt-on-deadlock diagnostic shows tids 9 and 10 in state **Ready** at `pc=0x824d1404 lr=0x824d22b4` — they are NOT blocked on a missing kernel API, they are inside a guest-side spin-poll loop: ``` 0x824d1400: beqlr cr6 ; return if poll succeeded 0x824d1404: cmpd cr6, r10, r11 ; r10 vs r11 0x824d1408: beq cr6, 0x824D1420 ; ok-branch 0x824d140c: mr r31, r31 ; nop (yield hint) 0x824d1410: ld r11, 0(r4) ; reload [r4] 0x824d1414: cmpdi cr6, r11, 0 0x824d1418: bne cr6, 0x824D1404 ; if nonzero, loop 0x824d141c: blr ``` `r4 = r31+356` (caller pushes `addi r4, r31, 356` at 0x824d22a8). The threads are spin-polling guest memory at `[r31+356]` waiting for it to reach 0. ## Classification: case IV (not I / II / III) The plan's original classification anticipated: - (I) ours doesn't reach the spawn LR ← refuted: spawn fires at idx 106756/106773 - (II) ours reaches spawn but no resume ← refuted: KeResumeThread fires at idx 106768/106785 - (III) ours's NtResumeThread is misimplemented ← refuted: `resume_ref()` correctly clears `Blocked(BlockReason::Suspended)` → `Ready`; halt diagnostic confirms post-resume Ready state and identical first-77/17 events to canary **Actual classification (IV)**: Resume succeeds; XAudio threads start running and execute their init sequence verbatim against canary; then enter a guest-side application spin-poll on `[r31+356]` that never resolves in ours. The producer of the 0-write to that location is part of canary's audio/GPU host bridge chain that AUDIT-048 only partially restored (cascades A/B/D landed; cascade C — XAudioSubmitRenderDriverFrame — remained `0` per that audit's own assessment). ## Why the 60% volume claim doesn't follow from a resume-only fix Phase NonMatch's "60% missing event volume" attribution to XAudio assumed the threads simply weren't running. They ARE running — they emit identical first events, get scheduled, and reach the spin loop. The volume bottleneck is the post-init *steady-state pump*: canary's 6.15 M tid=14 events come from 26,126 repeated iterations of the `XAudioGetVoiceCategoryVolumeChangeMask` / `KeReleaseSemaphore` / IRQL-cycle loop, each iteration gated on the host bridge clearing the `[r31+356]` flag. With the flag stuck non-zero in ours, the loop never re-enters; only the single first iteration (idx 0-76) ever executes. No quantum of resume-side change is going to unstick this. ## Out-of-scope for this session Per session authorization, fixing the host-bridge memory-write that clears `[r31+356]` requires touching xenia-apu/xenia-gpu host code, which is explicitly forbidden ("the host bridge is separate"). Therefore no engine change lands in this session. ## Progression metric (re-validation gate, baseline-only) Not re-measured for a change — there was no change. Pre-existing baseline remains the C+23+absorber state (`23cf4c4cbf61a577caa4118ab2308ba6` / `ba5b5e07…` depending on Phase D stage). swaps and draws unchanged. Per-chain matched-prefixes from MEMORY.md remain: - main tid=6→1: 105,046 (with Phase D D-extension absorber) - sister chains 11/32/4/41/16: preserved ## Recommended next attack target The remaining XAudio gate is **AUDIT-048 cascade C**: producer of the `[r31+356]=0` write. This is the part of the audio host-bridge chain that did NOT land in AUDIT-048. It likely involves: 1. `XAudioSubmitRenderDriverFrame` host-side callback firing the buffer-complete event with a side effect that decrements/clears a counter at offset 356 of the XAudio client struct. 2. `KeReleaseSemaphore` on a paired semaphore that produces the host-side buffer-complete notification. A targeted re-attack would: 1. Read xenia-canary's `apu/audio_system.cc` + `apu/xma_decoder.cc` to find the host-side write that clears `r31+356` (likely an XAUDIO_CLIENT_STATE struct field). 2. Mirror it in xenia-rs's `xaudio.rs` / audio worker context. 3. Re-validate the cold cycle. swaps may move 1→2 if the audio pump reaches the renderer fence; draws likely remain 0 (audio ≠ renderer per AUDIT-048). That work is the AUDIT-048-cascade-C completion task, NOT the resume gate. It's the natural sister of the deferred sub_825070F0 main-gate Path P. ## Per-chain delta (no change this session) | chain | pre | post | delta | |------:|----:|-----:|------:| | tid=6→1 main | 105,046 | 105,046 | 0 | | tid=11→11 | preserved | preserved | 0 | | tid=14→9 XAudio | 41 | 41 | 0 | | tid=15→10 XAudio | 16 | 16 | 0 | | tid=4→4 | preserved | preserved | 0 | | tid=16→16 | preserved | preserved | 0 | ## Artifacts - `tid6_window.json` — canary tid=6 events idx 106700..108200 around the XAudio spawn burst - `tid14_first.json` / `tid15_first.json` — canary tid=14/15 first 120 events - `extract_window.py` — extraction script - `escalation.md` — this file