Repository navigation
APU DSP JIT Qualification
Reviewed: November 1, 2026 · Source:
b7140848
Release availability: JIT-default selection is included in v0.8.136-0-g458730bf5373 (September 30, 2026).
State: Default engine selection delivered in #183; compatibility and output qualification remain ongoing subjects.
Scope: Existing interpreter/JIT engines, their selection policy and remaining differential coverage.
APU/DSP investigation directory · MCPX APU & Audio
The fork already contains two DSP backends behind the same DSPOps interface:
- the C interpreter initialized by
dsp_c_init(); - the Rust DSP56300 JIT initialized by
dsp_jit_init().
dsp_init() selects the backend from g_config.audio.use_dsp_jit. dsp_set_engine() can switch an existing GP or EP instance by synchronizing the active backend into canonical DspCoreState, finalizing it, initializing the other backend, and restoring the canonical state.
The existing-engine default has already changed: #183 merged September 29 and sets audio.use_dsp_jit=true. Explicit saved false remains the user's interpreter choice. The normal UI is a Boolean selection, with no automatic per-title compatibility policy. Remaining work is differential compatibility/output qualification and targeted fixes.
| Audio control | Configuration | Current default | Meaning |
|---|---|---|---|
| Quality → DSP JIT engine | audio.use_dsp_jit |
true |
Choose Rust JIT or C interpreter for GP/EP execution |
| Quality → Real-time DSP processing | audio.use_dsp |
false |
Select real-time GP/EP processing behavior and GP/EP monitor output |
These settings select different things. When real-time processing is off, the VP monitor is used and enabled GP/EP cores can still execute bounded 1,000-cycle slices at their frame boundaries. The loops continue toward halt only when their real-time flags are enabled. Turning this setting off does not stop all DSP work.
Configuration defaults, Audio UI, and GP/EP scheduling define the current behavior. Engine preference changes use the existing synchronized engine-switch path below; there is no new restart-only JIT setting.
-
hw/xbox/mcpx/apu/dsp/dsp.cdsp_init()dsp_set_engine()-
dsp_sync_to_vm()/dsp_sync_from_vm() - shared memory/peripheral/backend dispatch
-
hw/xbox/mcpx/apu/dsp/dsp_jit.c- C-owned X/Y/P memory buffers
- mix-buffer alias in X memory
- peripheral callbacks
- interrupt-state mapping
- VM-state synchronization
- cache invalidation
hw/xbox/mcpx/apu/dsp/dsp_c.c-
hw/xbox/mcpx/apu/dsp/gp_ep.c- runtime preference update for GP and EP
- DSP frame execution and monitoring
flowchart TD
A[dsp_init] --> B{use_dsp_jit?}
B -->|false| C[dsp_c_init]
B -->|true| D[dsp_jit_init]
C --> E[DSPOps interpreter]
D --> F[DSPOps Rust JIT]
E --> G[GP / EP frame calls]
F --> G
G --> H[dsp_run / memory / DMA / peripherals]
Both engines expose the same host-facing operations. That reduces integration risk but does not prove behavior parity.
The JIT backend creates C-owned memory buffers and registers them with the Rust engine:
typedef struct JitBackend {
Dsp56300Jit *jit;
uint32_t *xram;
uint32_t *yram;
uint32_t *pram;
} JitBackend;Its X-space map includes ordinary XRAM, a mix-buffer alias, and peripheral callbacks. Y and P use buffer regions. Program writes and explicit invalidation can affect translated-code validity.
flowchart TD
A[Running backend] --> B[dsp_sync_to_vm]
B --> C[Canonical DspCoreState + memories]
C --> D[Finalize old backend]
D --> E[Initialize requested backend]
E --> F[dsp_sync_from_vm]
F --> G[Invalidate translated cache as required]
G --> H[Continue GP/EP execution]
This existing switch path is valuable for qualification because it supplies a natural comparison boundary. It is also a major test target: a backend can appear correct from reset while failing when state is transferred mid-program.
The source already answers several questions:
- The JIT is not a prototype stub; it can reset, step, run, bootstrap, access memory, receive peripherals, synchronize state, and invalidate code.
- GP and EP can both use it.
- VM state is explicitly transferred for save/load and engine changes.
- Legacy 16-bit register representations receive compatibility widening when restored into the JIT.
- Interrupt numbering is translated between the C interpreter model and architectural vector slots.
- Mix-buffer memory is aliased into JIT XRAM rather than represented only through callbacks.
These details mean the investigation should start with differential execution and lifecycle cases, not with another call-graph survey.
The earlier PGR2 JIT observation reported +16.021% guest-display-write cadence for its own workload, build and settings. It supports that bounded observation, not all-title audio equivalence, host-presented FPS, or a universal game gain. See #183 for the default-change decision and Measured Results for the current evidence boundaries. This documentation review did not rerun that campaign against November main.
The following is a proposed comparison interface, not a claim that this exact harness is delivered. A useful harness runs identical initial state and cycle budgets through both engines:
typedef struct DspQualificationResult {
DspCoreState state;
uint64_t output_hash;
uint64_t peripheral_trace_hash;
uint64_t dma_trace_hash;
} DspQualificationResult;
DspQualificationResult run_engine(
const DspInitialState *initial,
bool use_jit,
const DspStimulus *stimulus);Comparison should report the first divergence, not only a final hash:
cycle / instruction boundary
program counter
register or stack element
X/Y/P/mix-buffer address
interrupt state
DMA register/cursor
audio sample index
peripheral callback sequence
For long programs, periodic checkpoints allow a binary search over the divergence window.
- Arithmetic, saturation, rounding, accumulator width, flags, loops, and repeat behavior.
- Parallel instruction forms and address generation.
- Interrupt entry/return and delayed effects.
- Self-modifying P memory and translated-code invalidation.
- X, Y, P, and mix-buffer aliases.
- GP/EP FIFO callbacks.
- DMA peripheral registers and completion behavior.
- Halt/start-frame behavior.
- Cold reset and bootstrap.
- Snapshot save/restore while halted and while active.
- Runtime interpreter → JIT and JIT → interpreter switching.
- Pause/resume, APU reset, and shutdown.
- Repeated title/VM reload without process restart.
- x86-64.
- ARM64.
- Windows, Linux, and macOS where the relevant JIT library/build is available.
- Debug/reference builds for diagnostics and optimized Release builds for timing.
Use three layers:
- Focused DSP programs for instruction and state coverage.
- Captured GP/EP programs and memory from real APU initialization and frames.
- Known audio-problem titles that exercise bootstrapping, effects, DMA, interrupts, snapshots, and long playback.
Synthetic instruction tests cannot establish integration compatibility. Retail playback alone cannot localize a divergence.
GP cycles requested and executed
EP cycles requested and executed
host time in dsp_run by engine
step/run calls
JIT translated blocks or cache entries
JIT cache hits/misses
JIT invalidations by reason
P-memory writes
peripheral reads/writes
DMA starts/descriptors/words
engine switches
sync-to-VM and sync-from-VM time
audio underruns
APU frame utilization
If the Rust library does not expose cache counters, add a thin optional diagnostic interface or count xemu-side invalidation/submission events first.
The same executable is ideal if runtime switching is stable. Separate runs are still needed to avoid warm-cache contamination and engine-switch artifacts.
Interpreter cold start
JIT cold start
Interpreter steady state
JIT steady state
Interpreter → JIT mid-run
JIT → Interpreter mid-run
snapshot saved by each engine and restored by each engine
Report:
- GP/EP host time;
- whole APU frame time;
- vCPU progress;
- whole-emulator CPU use;
- p95/p99 audio-block and frame time;
- underruns;
- JIT cache memory;
- output/state divergence.
Historical speedup claims can guide expectations but cannot qualify the current fork, current Rust library, or current host architecture.
- Large possible gain without inventing a new engine.
- Existing reference implementation remains available.
- Runtime switch and canonical state already exist.
- Differential testing can produce precise, reusable compatibility knowledge.
- A successful result can simplify later APU optimization by making DSP execution cheaper.
- JIT/library behavior may differ by architecture or compiler.
- Peripheral/interrupt edge cases may be rare and title-specific.
- Translation-cache invalidation may be incomplete for P-memory changes.
- State synchronization can hide fields not represented in
DspCoreState. - Runtime switching can perturb cache/timing and is not itself a fair steady-state benchmark.
- A JIT may be faster yet unsuitable as the default because one compatibility gap is severe.
| Case | Required comparison |
|---|---|
| Reset/bootstrap | Registers, PC, stack, memories |
| Fixed cycle budget | First divergent instruction/state |
| GP frame | Mix buffer, DMA, interrupts, output |
| EP frame | FIFO/audio output and cycle state |
| P-memory modification | Correct invalidation and new code execution |
| Peripheral callback program | Identical callback sequence/values |
| DMA activity | Descriptor/cursor/data/status equality |
| Engine switch both directions | No lost or widened state beyond defined conversion |
| Snapshot cross-engine restore | Equivalent resumed state/output |
| Long title playback | No drift, underrun, or compatibility failure |
| x86-64 versus ARM64 | Same guest state/output contract |
The current product policy is JIT on by default, with an explicit interpreter option. Further evidence may justify refining it; none of the following alternatives is an implemented Auto policy:
- JIT is safe and faster as the default.
- JIT is safe under an Auto policy selected by host architecture or title capability.
- JIT is faster but needs a specific compatibility fallback.
- JIT is useful only as an opt-in diagnostic/performance route.
- JIT has no useful whole-emulator benefit for the tested workloads.
- Interpreter remains required until specific divergences are fixed.
The default-selection change is already implemented; do not relabel it as unmerged because broader parity work remains. New engine or policy changes need a documented compatibility envelope, focused output/state/lifecycle checks and representative Release measurements. A future Auto/fallback policy would be a separate implementation and decision.
Do not broaden compatibility claims or introduce an Auto policy based only on a synthetic benchmark or one clean title. Stop performance work and localize correctness first when state, DMA, interrupt, or output divergence appears.
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