Skip to content

MCPX APU and Audio

OpenAI Codex edited this page Nov 2, 2026 · 8 revisions

MCPX APU & Audio

Reviewed: November 1, 2026 · Source: b7140848
Release availability: Architecture and JIT/mono defaults in v0.8.136-0-g458730bf5373 (September 30, 2026); later main-only paths are identified below.

The MCPX Audio Processing Unit is not emulated as a single synchronous device callback. xemu exposes PCI/MMIO register state to the guest, keeps guest-memory-backed voice/DSP state, and runs audio-frame processing on a dedicated host thread that is paced partly by the host audio queue.

This page separates the guest hardware contract from xemu's threading, shortcuts, and host-audio pacing.

Current controls and delivery

For developer testing, see DSP capture, replay and differential testing. The November 2 experimental PR #384 adds opt-in GP/EP recording and isolated production replay/fuzzing; it is not merged or released. Its emulator-baseline results and incomplete external-context contract are documented separately from the current controls below.

Control Current default / behavior Release boundary
Audio → Quality → DSP JIT engine audio.use_dsp_jit=true; explicit saved false selects interpreter Default changed by #183, included in stable
Audio → Quality → Real-time DSP processing audio.use_dsp=false; enabled GP/EP can still run bounded slices when off Separate scheduling/monitor control, not an engine switch
Audio → Advanced → Voice processing workers audio.vp.num_workers=0: Auto resolves logical CPU count to 1–4; explicit 1–16; restart required Auto-four #200 is main only; stable's Auto cap is sixteen

The JIT setting selects an existing engine; it does not create the engine or supply per-title Auto compatibility selection. The real-time setting changes GP/EP processing and monitor behavior. These defaults do not mean all DSP execution stops when real-time processing is off. Settings source · Audio UI · Detailed JIT qualification.

Hardware-facing model

For the November 2 executable development proof, see HLE readiness and public DSP XISO: exact GP and scratch-DMA outputs, 100 capture replays, isolated native arithmetic and negative controls. This is experimental branch work, separate from the current audio defaults.

The concurrent shadow and OpenAL showcase adds live-frame/private-worker comparison with per-route coverage, fallback and injected mismatch controls. OpenAL Soft loopback matched the three synthetic gains on Deck and cloud; real effect/state and game qualification remain open. DSP still owns all live outputs.

The APU is created as PCI device mcpx-apu and receives the Xbox RAM MemoryRegion. The device exposes a 0x80000 MMIO BAR with subregions for voice processing and the two DSP cores.

Current layout in mcpx_apu_realize():

Range within BAR Model
base register area below 0x20000 APU global/front-end registers
0x20000..0x2ffff Voice processor (vp)
0x30000..0x3ffff GP DSP
0x50000..0x5ffff EP DSP

The device also sets PCI interrupt pin 1 and raises/deasserts its PCI IRQ from APU interrupt state.

End-to-end path

flowchart TD
    A[Guest CPU] --> B[APU MMIO writes]
    A --> C[Guest audio buffers / voice tables in RAM]
    B --> D[Atomic APU register state]
    C --> E[APU RAM pointer / DMA helpers]
    D --> F[mcpx.apu_thread]
    E --> F
    F --> G[Voice processor frame]
    G --> H[GP / EP DSP processing]
    H --> I[Mixbins]
    I --> J[APU monitor]
    J --> K[SDL audio stream]
    K --> L[Host audio device]
    F --> M[APU IRQ state]
    M --> N[PCI IRQ → guest]
Loading

The host audio stream is an implementation backend; it is not guest-visible MCPX state.

Dedicated APU thread

mcpx_apu_realize() creates a joinable host thread named mcpx.apu_thread. The frame thread owns the recurring audio-processing loop under d->lock.

stateDiagram-v2
    [*] --> Paused
    Paused --> CheckState: resume / VM running
    CheckState --> Paused: pause_requested
    CheckState --> Waiting: XCNT off or FE trapped/halted
    CheckState --> Frame: engine enabled
    Waiting --> CheckState: condition/timed wake
    Frame --> Throttle
    Throttle --> CheckState: next frame
    CheckState --> [*]: exiting
Loading

The thread can temporarily release its local lock and acquire the QEMU Big Lock only when it needs to publish IRQ state. This separation matters when profiling lock contention: ordinary audio frame work and guest interrupt publication are not the same critical section.

Frame processing

A normal sound-engine frame runs roughly:

update DSP preference
    ↓
begin debug frame
    ↓
allocate/clear frame mixbins
    ↓
voice processing
    ↓
DSP processing
    ↓
monitor / host-audio output
    ↓
advance EP frame counter

NV_PAPU_XGSCNT is synthesized from ep_frame_div * NUM_SAMPLES_PER_FRAME; it is not incremented by simulating an individual physical clock edge for every sample.

Host-audio pacing

The APU uses the SDL audio stream's queued-byte count as a pacing signal. Every eighth EP frame the throttle path checks the host stream and may sleep on a condition variable.

flowchart TD
    A[Every 8th EP frame] --> B[Read SDL queued bytes]
    B --> C{Queue >= high watermark?}
    C -->|Yes| D[Timed condition wait until queue drains]
    C -->|No| E{Queue <= low watermark?}
    E -->|Yes| F[Allow pacing to advance slightly]
    E -->|No| G[Normal real-time target]
    D --> H[Re-read queue]
    H --> C
    F --> I[Process frame]
    G --> I
Loading

The secondary pacing path uses QEMU_CLOCK_REALTIME and next_frame_time_us. It also nudges the next target slightly depending on whether queued audio is above or below the midpoint between low/high watermarks.

Why this matters

On physical hardware the audio engine and DAC path have hardware clocks and buffers. In xemu, the host SDL queue can influence how long the APU thread waits. A profiler can therefore show audio time that is really host-output backpressure, not guest DSP execution.

Performance work should separately measure:

  • frame computation time;
  • queued host-audio latency;
  • throttle/backoff time;
  • guest IRQ work;
  • synchronization with VM pause/save/reset.

Guest RAM fast path

mcpx_apu_init() stores both the MemoryRegion *ram and a direct host pointer obtained through memory_region_get_ram_ptr().

guest RAM MemoryRegion
    ↓
memory_region_get_ram_ptr()
    ↓
d->ram_ptr
    ↓
APU processing code

That is an important fast-path boundary. Direct RAM access avoids repeated generic address-space dispatch where the device already knows it is operating on Xbox RAM. Any future attempt to generalize/remap APU memory must account for this cached host-pointer assumption.

MMIO semantics and atomic state

Reads and writes to the main APU register range use atomic operations for shared state because the guest/vCPU side and APU worker can interact with the same registers.

Examples:

  • writing NV_PAPU_ISTS clears selected interrupt bits, recomputes IRQ state, and wakes the APU condition variable;
  • writing NV_PAPU_FECTL or NV_PAPU_SECTL updates engine state and wakes the worker;
  • ordinary register writes below 0x20000 store into the register array.

This is not cycle-accurate bus arbitration. Correctness is established through atomic visibility, locks, condition variables and guest-visible register/IRQ semantics.

The FEMEMDATA “magic write” shortcut

The current source explicitly calls one path a “magic write.” When the guest writes NV_PAPU_FEMEMDATA, xemu immediately writes the value to guest physical memory at the address held in NV_PAPU_FEMEMADDR:

Guest writes FEMEMDATA
    ↓
mcpx_apu_write()
    ↓
stl_le_phys(address_space_memory, FEMEMADDR, value)
    ↓
Guest memory changed immediately

The source comment says hardware is expected to perform this as part of notify-related completion, but xemu does it immediately.

flowchart LR
    A[Expected hardware completion sequence] --> B[Notify/event machinery]
    B --> C[Hardware writes result]

    D[xemu FEMEMDATA write] --> E[Direct stl_le_phys]
    E --> C
Loading

This removes unknown hardware latency/state transitions. It is both a fidelity gap and a performance shortcut: software sees the required memory result without xemu reproducing the full hardware engine that would have produced it.

IRQ path

update_irq() combines the interrupt-enable register with pending APU status. When an enabled pending source exists it sets the global interrupt status and asserts the PCI IRQ; otherwise it clears/deasserts it.

The APU frame thread does not hold the BQL while performing normal frame work. When it needs to update the PCI IRQ it temporarily transitions through the BQL:

sequenceDiagram
    participant A as APU thread
    participant L as APU mutex
    participant B as BQL
    participant P as PCI IRQ

    A->>L: Hold local lock
    A->>L: Detect set_irq
    A->>L: Release
    A->>B: Acquire
    A->>P: update_irq()
    A->>B: Release
    A->>L: Reacquire
Loading

Changing that order can create lock inversion or make snapshot/reset paths deadlock.

Pause, reset and snapshot lifecycle

The APU is intentionally quiesced before VM state is saved or restored. This protects DSP arrays and program RAM from being processed while partially serialized/restored.

sequenceDiagram
    participant VM as VM state change
    participant B as BQL
    participant A as APU thread
    participant D as DSP state

    VM->>B: Running under BQL
    VM->>B: Release BQL
    VM->>A: Set pause_requested + signal
    A-->>VM: is_idle
    VM->>D: sync_to_vm / restore-safe state
    VM->>B: Reacquire BQL
Loading

The source documents why the BQL is released before waiting for the APU idle state: the APU thread itself can need the BQL to publish an IRQ, so waiting while retaining it creates a lock inversion.

On restore, mcpx_apu_pre_load() leaves the APU paused until the normal running-state callback synchronizes restored DSP state back to the active backend.

Reset limitation

mcpx_apu_reset_locked() clears the APU register array and resets voice processing, but currently contains:

FIXME: Reset DSP state

Instead, it invalidates the GP and EP DSP opcache. This means reset semantics deserve targeted correctness tests; a game that relies on exact DSP internal reset state can expose behavior not covered by ordinary audio playback.

DSP opcache as a fast path

The DSP implementation maintains translated/cached operation state. Reset invalidates that opcache so old decoded/translated DSP work is not reused after a state transition.

The same principle as CPU TCG applies:

stable executable DSP state
    → reuse cached decoded/translated work

state reset / program change
    → invalidate before reuse

Fast reuse is correct only while the cached representation still corresponds to guest-visible DSP program/state.

Failure and performance surfaces

Area Why it matters
Host queue throttle Host audio backend can dominate waits
Direct RAM pointer Fast but assumes stable RAM backing/mapping
FEMEMDATA immediate write Hardware completion timing/state machine omitted
IRQ BQL bridge Lock-order changes can deadlock or perturb latency
DSP reset FIXME Reset may not match all physical internal state
Frame debug/telemetry Diagnostic instrumentation can alter timing
VM-state quiesce Save/load can block waiting for APU to become idle

Profiling flow

flowchart TD
    A[Audio hitch / APU hot thread] --> B{Where is time?}
    B -->|Frame compute| C[VP / DSP / mix processing]
    B -->|Wait| D[SDL queue / realtime throttle]
    B -->|Lock| E[APU mutex / BQL transition]
    B -->|Guest interaction| F[MMIO / IRQ / RAM access]
    C --> G[Optimize compute with DSP correctness oracle]
    D --> H[Separate host-device backpressure from emulation cost]
    E --> I[Preserve lock ordering and lifecycle]
    F --> J[Preserve register/DMA/IRQ semantics]
Loading

Source map

Source Responsibility
hw/xbox/mcpx/apu/apu.c PCI/MMIO device, frame thread, throttle, IRQ, VM lifecycle
Voice processor (vp.c) Fetch, resample, workers and lifecycle
DSP dispatch (dsp.c) Interpreter/JIT interface and state transfer
hw/xbox/mcpx/apu/monitor.c Host audio monitoring/output integration
hw/xbox/mcpx/apu/apu_regs.h Guest-visible register definitions
hw/xbox/xbox.c APU device creation and RAM attachment

Delivered and partial audio hot paths

Mechanism Current state Availability / evidence limit
Mono resampling #182: one-channel sinc interpolation then output expansion In stable; no new toggle
ADPCM decoded reuse #187: owned per-voice block, reuse only after current full-byte/configuration match Main only; no repeatable game gain established; some PGR2 tails worsened
Sample mappings #190/#275: live SGE descriptor and bounded ADPCM payload mappings Main only; larger spans/conversion kernels remain research; #310 retains adverse Deck Morrowind result
Local format preparation #191: CFG_FMT read/decode once per sample-fetch callback Main only; persistent controls, filter coefficients and mutable histories are not delivered caches
Worker policy/reliability #200 Auto-four, #201 exit unlock, #305 predicate recheck after wakes #201 in stable; #200/#305 main only; targeted wake/reduction redesign remains research
Attenuation lookup #193 measured and closed unmerged Current main retains powf; small favorable cadence/CPU with worse p99

These change host work organization, not claims about physical MCPX internal caching. Source presence is separate from demonstrated whole-game benefit. See Current Change Register and Measured Results.

Approximate interpolation research

Linear Audio Resampling evaluates replacing the current bandlimited sinc converter with libsamplerate's cheaper linear converter as an explicit audio-quality tradeoff. It is separate from Mono Voice Resampling, which already removes duplicated interpolation channel work while retaining sinc quality. #189 remains open/unmerged/HOLD; Auto-four workers do not depend on it.

Performance investigations

The source-backed APU/DSP performance work is organized under APU / DSP Performance Investigations. It separates existing-engine qualification, worker/memory/backend optimizations, lock measurement, HRTF, destructive GP/EP experiments, and secondary-device instrumentation.

The overview links to a standalone technical reference for every investigation and preserves the known source seams, call-rate estimates, counters, negative controls, and promotion criteria.

Related pages

Clone this wiki locally