Repository navigation
MCPX APU and 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.
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.
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.
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]
The host audio stream is an implementation backend; it is not guest-visible MCPX state.
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
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.
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.
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
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.
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.
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.
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_ISTSclears selected interrupt bits, recomputes IRQ state, and wakes the APU condition variable; - writing
NV_PAPU_FECTLorNV_PAPU_SECTLupdates engine state and wakes the worker; - ordinary register writes below
0x20000store 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 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
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.
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
Changing that order can create lock inversion or make snapshot/reset paths deadlock.
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
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.
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.
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.
| 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 |
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]
| 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 |
| 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.
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.
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.
DSP tooling guide and navigation updated November 2, 2026 for experimental PR #384, source 8f51d237. Capture/replay usage and limits.
The earlier wiki-wide review was published November 1, 2026. Emulator source: b7140848. Reviewed wiki content: 1d2b671. Current status · Release availability · Publication process.
- Home
- Current status
- Stable versus main
- Implemented mechanisms
- Controls and defaults
- Measured results and limits
- Complete change register
- System
- CPU and vCPU
- Memory and DMA
- Graphics / NV2A
- APU and audio
- Timing and timers
- Platform and I/O
- Hardware observations
- Fidelity boundaries
- Methodology
- PGR2 routes
- Debugging lag / hot paths
- Tools and commands
- DSP capture, replay and fuzzing
- HLE readiness and public XISO proof
- Concurrent DSP shadow and OpenAL proof
- DSP HLE operation inventory
- DSP HLE fix examples and testing
- Development
- Upstream shortlist
- Wiki maintenance
- Research / retired work
- Optimization catalog
- Hot-path shortcuts
- Researched experiments
- Unproven proposal directory
- PR lessons
- September26 snapshot
Repository · PRs · Issues