Skip to content

APU Bulk VP to DSP Mix Transfer

github-actions[bot] edited this page Sep 15, 2026 · 1 revision

APU Bulk VP-to-DSP Mix Transfer

Lifecycle: Performance - Unproven · Type: Exact backend bulk-transfer shortcut
Reviewed source: 322986f4c5502b3fa887a9605ac8cd88e9b2c579
Question: Can the complete VP mix matrix be converted and written through one backend operation instead of 1,024 generic DSP writes per APU subframe?

APU/DSP investigation directory · DSP JIT Qualification

Verified starting point

After VP processing produces:

float mixbins[NUM_MIXBINS][NUM_SAMPLES_PER_FRAME];

mcpx_apu_dsp_frame() writes the complete matrix into GP DSP X memory:

for (int mixbin = 0; mixbin < NUM_MIXBINS; mixbin++) {
    uint32_t base = GP_DSP_MIXBUF_BASE +
                    mixbin * NUM_SAMPLES_PER_FRAME;
    for (int sample = 0; sample < NUM_SAMPLES_PER_FRAME; sample++) {
        dsp_write_memory(d->gp.dsp, 'X', base + sample,
                         float_to_24b(mixbins[mixbin][sample]));
    }
}

With 32 mixbins and 32 samples, that is 1,024 interface calls per subframe. At the nominal 1,500-subframe cadence, the source-derived upper estimate is 1,536,000 generic write calls per second when the path is active.

This does not prove 1.536 million expensive calls occur in every title or that they dominate APU time. It establishes a precise counter target.

Source seams

Current flow

flowchart TD
    A[VP produces 32 x 32 float matrix] --> B[Select mixbin and sample]
    B --> C[float_to_24b]
    C --> D[dsp_write_memory generic interface]
    D --> E[Backend dispatch]
    E --> F[Interpret X-space address]
    F --> G[Write one DSP word]
    G --> H{1,024 words complete?}
    H -->|No| B
    H -->|Yes| I[Start/run GP DSP]
Loading

The numerical conversion is necessary. The candidate targets repeated interface and address machinery around it.

Candidate flow

flowchart TD
    A[VP produces 32 x 32 float matrix] --> B[dsp_write_mixblock24]
    B --> C{Active backend}
    C -->|Interpreter| D[Backend validates destination span]
    C -->|JIT| E[Backend resolves owned XRAM alias]
    D --> F[Convert and write contiguous 1,024 words]
    E --> F
    F --> G[Optional exact vectorized conversion experiment]
    G --> H[Start/run GP DSP]
Loading

Proposed interface:

typedef struct DspMixBlockResult {
    size_t words_written;
    bool used_fast_span;
} DspMixBlockResult;

DspMixBlockResult dsp_write_mixblock24(
    DSPState *dsp,
    const float mixbins[NUM_MIXBINS][NUM_SAMPLES_PER_FRAME]);

The first version should keep the conversion scalar and bit-identical:

static void write_mixblock_reference(
    uint32_t *dst,
    const float *src,
    size_t words)
{
    for (size_t i = 0; i < words; i++) {
        dst[i] = float_to_24b(src[i]);
    }
}

SIMD is a later experiment because it can change rounding, saturation, NaN handling, or conversion ordering.

Backend implementations

JIT backend

The JIT owns XRAM and maps the mix-buffer region as an alias. A fast backend method can resolve the known range once and write directly into that C-owned buffer.

The implementation must derive the destination from the backend’s actual region mapping rather than duplicating a fragile hard-coded pointer in gp_ep.c.

Possible internal API:

bool dsp_backend_get_writable_span(
    DSPState *dsp,
    char space,
    uint32_t address,
    size_t requested_words,
    DspWordSpan *span);

The mix transfer can use it only when the returned span covers the complete expected range and has ordinary buffer semantics.

C interpreter

The interpreter may expose its XRAM/mixbuffer directly through a private backend function or implement a backend-specific bulk callback. The public APU code should not inspect interpreter internals.

Generic fallback

If a backend cannot offer the span, use the current word-at-a-time loop. The fallback makes the new interface safe for debugging, future engines, and unusual memory maps.

Exactness contract

The candidate is exact only if it preserves:

  • one float_to_24b() result for each matrix element;
  • mixbin-major, sample-minor layout;
  • GP_DSP_MIXBUF_BASE alias behavior;
  • visibility before the GP DSP begins consuming the block;
  • guest reads of the GP mix-buffer window;
  • interpreter and JIT engine switching;
  • snapshot state;
  • P-memory/cache behavior unaffected by an X-space bulk write.

The bulk call must not defer conversion past GP execution or keep a pointer to the stack-local mixbins array.

Possible vectorized follow-on

After scalar bulk transfer proves interface savings, compare an exact or declared-equivalent vectorized converter:

load float lanes
apply scale/round/saturate matching float_to_24b
pack to 32-bit words with upper byte clear
store contiguous words

Required negative values include:

  • ±0;
  • values just below/above saturation;
  • halfway rounding cases;
  • subnormals;
  • NaN and infinities if reachable;
  • random full-range floats.

No SIMD path should be merged merely because ordinary music samples match.

What may disappear

  • 1,024 calls through dsp_write_memory() per active subframe.
  • 1,024 backend function-pointer dispatches.
  • Repeated space/address decoding.
  • Repeated memory-region lookup or Rust FFI entry.
  • Per-word bounds/range interpretation that can be proven once for the whole mix block.

What remains mandatory

  • 1,024 numerical conversions and stores unless a later exact vector path changes their organization.
  • GP DSP execution.
  • Correct mix-buffer memory visibility.
  • Guest MMIO/memory reads.
  • Engine synchronization and snapshot behavior.
  • Generic fallback.

Required counters

APU subframes
VP→DSP mix transfers
mix words requested
reference dsp_write_memory calls
bulk calls
bulk words written
bulk fast-span hits
bulk fallback calls{reason}
float_to_24b scalar conversions
bulk transfer host time
GP dsp_run host time
APU frame time

The baseline should verify the expected ratio:

reference write calls / active mix transfer ≈ 1,024

If the count is materially lower due to path conditions, document the actual behavior before implementation.

Test matrix

Scenario Required result
Random 32 × 32 matrix Bit-identical DSP words
Saturation/rounding edge vectors Exact float_to_24b() parity
Interpreter backend Correct memory/output
JIT backend Correct alias memory/output
GP mix-buffer guest reads Same words at same addresses
GP program reads during frame Complete block visible before run
Runtime engine switch No layout or state loss
GP disabled/enabled transition No stale deferred block
Snapshot save/load Mix-buffer state preserved
Reset/bootstrap Bulk state cannot survive reset
Long playback No drift or underrun

Advantages

  • Very narrow, measurable source seam.
  • Strong call-count hypothesis.
  • Existing JIT memory ownership makes a direct span plausible.
  • Exact scalar implementation can isolate interface cost before SIMD.
  • Backend fallback preserves compatibility.

Costs and risks

  • The generic calls may inline or be cheaper than expected in Release.
  • Rust FFI cost may differ from interpreter dispatch cost.
  • Direct memory exposure can violate backend encapsulation.
  • Incorrect alias/address assumptions corrupt DSP state.
  • Simultaneously adding SIMD would hide whether the gain came from dispatch or conversion.

Acceptance rule

Move to Researched when counters confirm a high generic-call rate, scalar bulk output is bit-identical across both engines and lifecycle tests, and Release measurements show a meaningful reduction in APU/frame or backend time.

Rejection rule

Do not continue to SIMD or broader span APIs if bulk scalar transfer removes calls but does not affect local or whole-emulator performance. Preserve the evidence as a negative result.

Related

Clone this wiki locally