Repository navigation
APU DSP DMA Block Transfers
Lifecycle: Performance - Unproven · Type: Backend memory-span and conversion restructuring
Reviewed source:322986f4c5502b3fa887a9605ac8cd88e9b2c579
Question: Can DSP DMA decode and transfer blocks through bounded memory spans instead of word-level backend calls and an intermediate scratch buffer?
APU/DSP investigation directory · Bulk VP-to-DSP Transfer
The DSP DMA engine walks descriptors stored in DSP X/Y/P memory. For each descriptor it reads seven fields through the backend memory callback, decodes source/destination spaces and sample formats, then moves words one at a time into or out of a reusable intermediate byte buffer. The guest-facing scratch/FIFO side already transfers chunks and can split at circular or guest-page boundaries.
The source contains an explicit FIXME: Remove this intermediate buffer. The stronger opportunity is not merely replacing malloc(). It is separating ordinary contiguous DSP memory from P-memory and peripheral/callback regions so the engine can convert/copy a bounded block at once.
-
hw/xbox/mcpx/apu/dsp/dsp_dma.cdsp_dma_run()- descriptor decode
- interleave/deinterleave loops
- static scratch buffer
- delayed RUNNING status behavior
hw/xbox/mcpx/apu/dsp/dsp_dma.hhw/xbox/mcpx/apu/dsp/dsp.chw/xbox/mcpx/apu/dsp/dsp_jit.chw/xbox/mcpx/apu/dsp/dsp_c.c-
hw/xbox/mcpx/apu/dsp/gp_ep.c- page-sized guest scatter/gather helpers
flowchart TD
A[DMA CONTROL start] --> B[Read descriptor words through DSP backend]
B --> C[Decode space, direction, format, count, FIFO/scratch target]
C --> D[Resize/reuse intermediate scratch buffer]
D --> E{DSP to guest/peripheral?}
E -->|Yes| F[Read each DSP word through mem_read]
F --> G[Convert/interleave into scratch bytes]
G --> H[Chunked FIFO/scratch scatter-gather copy]
E -->|No| I[Chunked FIFO/scratch read into scratch bytes]
I --> J[Convert each item]
J --> K[Write each DSP word through mem_write]
H --> L[Write back cursor and next descriptor]
K --> L
Program-memory and peripheral ranges cannot be treated like ordinary X/Y RAM.
flowchart TD
A[Decode descriptor once] --> B[Resolve DSP memory span]
B --> C[Resolve guest/FIFO/scratch span]
C --> D[Choose bounded chunk]
D --> E{Ordinary buffer span?}
E -->|No| F[Reference callback/word route]
E -->|Yes| G[Convert/copy block]
G --> H{More descriptor data?}
F --> H
H -->|Yes| B
H -->|No| I[Commit cursors, writeback, EOL/status]
Potential API:
typedef struct DspMemorySpan {
uint32_t *words;
size_t word_count;
char space;
uint32_t first_word;
bool writable;
bool requires_code_invalidation;
} DspMemorySpan;
bool dsp_get_memory_span(
DSPState *dsp,
char space,
uint32_t word_address,
size_t requested_words,
bool write,
DspMemorySpan *span);The function must return only a contiguous ordinary-memory span. It must stop before:
X/Y/P region end
mix-buffer alias boundary
peripheral callback range
program-memory invalidation boundary
circular guest/scratch wrap
guest SGE page boundary
descriptor end
interleave layout boundary when no bulk kernel exists
Record descriptor count, words, formats, spaces, directions, interleave use, circular wraps, backend calls, scratch bytes, and transfer time. This determines whether the path is frequent and which format/direction deserves the first prototype.
Read the seven descriptor fields into a local struct once:
typedef struct DspDmaDescriptor {
uint32_t next;
uint32_t control;
uint32_t count;
uint32_t dsp_offset;
uint32_t scratch_offset;
uint32_t scratch_base;
uint32_t scratch_size;
} DspDmaDescriptor;This improves clarity and instrumentation but is not itself expected to produce a major gain.
Start with the simplest exact subset:
ordinary X or Y RAM
non-interleaved
16-, 24-, or 32-bit format with an exact converter
no P-memory write
no peripheral range
no circular DSP-side addressing
Use the current guest-side chunk helper and replace only DSP word callback repetition.
Interleaved DSP→guest transfers currently read planar channels and write interleaved bytes. Add a bounded kernel after exact scalar span handling is proven. Keep per-format scalar reference functions for comparison.
P-memory writes can invalidate JIT translations. A bulk span may be used only with an explicit invalidation contract covering the written program range. One invalidation per block may be correct and cheaper than one per word, but that must be proven against both engines.
Direct conversion can target a guest/scratch span or a small fixed chunk buffer. Some FIFO callback interfaces may still require a contiguous byte buffer. “Remove the buffer” should not become “duplicate conversion into every backend.”
Reference 16-bit DSP→guest:
for (size_t i = 0; i < words; i++) {
dst16[i] = (uint16_t)(src_words[i] >> 8);
}Reference 24-bit/32-bit path:
for (size_t i = 0; i < words; i++) {
dst32[i] = src_words[i] & item_mask;
}The existing code uses host casts into the scratch byte array. A cleanup should define endianness and alignment explicitly rather than silently preserving undefined or host-specific assumptions.
The transfer executes synchronously when DMA CONTROL is written, yet DMA_CONTROL_RUNNING can remain set until it has been read more than twice. Immediate STOPPED publication could remove guest DSP polling but changes observable timing.
Treat it as a separate destructive experiment:
if (policy == DMA_COMPLETION_IMMEDIATE_EXPERIMENT) {
control &= ~DMA_CONTROL_RUNNING;
control |= DMA_CONTROL_STOPPED;
} else {
preserve_current_read_count_completion_model();
}Do not combine this timing change with block-transfer performance testing.
- Per-word backend callback/function-pointer/FFI crossings.
- Repeated ordinary-memory range interpretation.
- Intermediate full-transfer scratch copies for admitted directions.
- Per-word program-cache invalidation if a correct block invalidation is implemented.
- Some descriptor-field callback overhead if contiguous descriptor spans are later proven.
- Descriptor decode and linked-list traversal.
- Exact format conversion, masks, interleave order, and byte layout.
- FIFO and scratch cursor updates.
- Circular wrap behavior.
- SGE/page splitting and guest dirty marking.
- EOL, RUNNING/STOPPED/FROZEN, writeback, and interrupt state.
- Peripheral callbacks.
- JIT code invalidation for P-memory writes.
- Reference route for unsupported spans.
- Source contains a direct cleanup marker.
- Guest-side transfers are already chunked, reducing the project scope.
- JIT and interpreter both own ordinary memory arrays that may support spans.
- Can begin with a narrow exact subset.
- Counters clearly separate call reduction from numerical conversion.
- DMA may be too infrequent to matter.
- Span exposure weakens backend encapsulation.
- Program-memory invalidation mistakes execute stale DSP code.
- Interleave and circular cases can hide off-by-one errors.
- Endianness/alignment assumptions can differ across x86-64 and ARM64.
- Immediate completion experiments can change guest behavior even if copied data is identical.
DMA starts
DMA descriptors processed
words and bytes transferred by format/direction/space
backend mem_read calls
backend mem_write calls
span requests / hits / average words
interleaved descriptors and channels
circular wraps
P-memory writes and invalidations
scratch-buffer allocations/growth/copies
FIFO/scratch callback calls and bytes
DMA host time
DMA status reads while RUNNING
| Scenario | Required result |
|---|---|
| X/Y non-interleaved formats | Byte/word exactness |
| 16/24/32-bit edge values | Exact masks/shifts/endianness |
| Multi-channel interleave | Same channel/sample ordering |
| Circular scratch wrap | Same final cursor and bytes |
| Guest page/SGE split | Same bytes and dirty marking |
| FIFO source/destination | Same cursor and callback behavior |
| P-memory write | Correct translated-code invalidation |
| Descriptor chain and EOL | Same traversal/status/interrupts |
| Freeze/unfreeze/stop | Same control semantics |
| Interpreter/JIT/engine switch | Same state and transfer output |
| Snapshot/reset | No stale span or static-buffer state |
| x86-64/ARM64 | No alignment/endianness divergence |
Move to Researched when counters show substantial word-level backend activity, the admitted block paths pass exact comparisons on both engines and hosts, and Release measurements improve DSP/APU or whole-emulator tails.
Keep only a cleanup or abandon the large rewrite when transfers are rare, when most descriptors hit callback/P-memory/interleave cases, or when span management costs approach the current loop cost.
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