Skip to content

APU DSP JIT Qualification

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

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

Existing engines and delivered default

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.

Engine selection versus real-time processing

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.

Source seams

Verified current architecture

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]
Loading

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.

Engine-switch flow

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]
Loading

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.

Starting evidence

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.

Historical performance evidence

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.

Remaining differential harness design

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.

Qualification dimensions

Instruction and numerical behavior

  • 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.

Memory and peripherals

  • X, Y, P, and mix-buffer aliases.
  • GP/EP FIFO callbacks.
  • DMA peripheral registers and completion behavior.
  • Halt/start-frame behavior.

Lifecycle

  • 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.

Hosts

  • 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.

Workloads

Use three layers:

  1. Focused DSP programs for instruction and state coverage.
  2. Captured GP/EP programs and memory from real APU initialization and frames.
  3. 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.

Required counters

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.

Performance campaign

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.

Advantages

  • 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.

Risks and costs

  • 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.

Test matrix

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

Conclusions for further qualification

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.

Acceptance rule

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.

Rejection rule

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.

Related

Clone this wiki locally