Skip to content

Hot Path Audio ADPCM Block Reuse

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

ADPCM Decoded-Block Reuse

Reviewed: November 1, 2026 · Source: b7140848
State: Implemented in #187; repeatable game speedup not established.
Release availability: Main only at this review; absent from v0.8.136-0-g458730bf5373 (September 30, 2026).

The voice processor retains one owned encoded/decoded ADPCM block per voice across sample-fetch callbacks. Every new block fetch still obtains current encoded guest bytes. A hit requires matching channel count, encoded byte count and every encoded byte; a matching address or logical block number alone is never sufficient.

Before and after

Before #187, voice_get_samples() could reuse decoded samples only within the current call. Its local decoded storage disappeared on return.

flowchart LR
    A[Callback fetches encoded block] --> B[Decode into local storage]
    B --> C[Copy requested samples]
    C --> D[Return and discard decoded block]
    D --> E[Later callback fetches and decodes again]
Loading

The current path separates callback-local source mappings from per-voice decoded ownership:

flowchart TD
    A[Need next ADPCM block] --> B[Read current encoded bytes]
    B --> C{Size and channel layout fit cache?}
    C -->|No| D[Invalidate and reject invalid block]
    C -->|Yes| E{Valid cache / channels / size / all bytes match?}
    E -->|Yes| F[Use retained decoded prefix]
    E -->|No| G[Decode into owned per-voice storage]
    G --> H{Valid decoded frame count?}
    H -->|No| D
    H -->|Yes| I[Save encoded bytes / metadata / count]
    I --> F
    F --> J[Bounds-check requested frame and emit samples]
    J --> K[Advance existing loop / position / completion state]
Loading

Exact identity and bounds

MCPXADPCMBlockCache stores a validity flag, channel count, encoded size, decoded frame count, up to 72 encoded bytes and 130 interleaved int16_t values. The decoded count is frames per channel; only that prefix is valid. mcpx_apu_adpcm_fits_cache() validates layout and capacity before decoding or comparing. Invalid headers or out-of-range decoded positions do not expose an old cached block.

Conceptually, mcpx_apu_adpcm_decode_cached() does this:

validate encoded size and channel count
if cache.valid and channels match and size matches and all bytes match:
    return owned decoded prefix
otherwise:
    decode current bytes into owned storage
    validate decoded count
    save current encoded bytes, metadata and count
    return owned decoded prefix

Address is not part of the current content identity: independently decodable blocks with the same bytes and layout produce the same result even at different addresses. The cache contains copied bytes and samples, not a borrowed pointer into callback-local buffers or guest memory.

Lifetime and invalidation

voice_reset_filters() invalidates the cache when VOICE_ON starts a new stream. VP reset invalidates every voice cache; snapshot pre-load quiesces the APU and invokes that reset. Unsupported/non-ADPCM input and invalid decode paths also clear validity. A different byte sequence, channel count or size causes a miss. Loop, position, notification and completion logic stays in voice_get_samples().

This cache must not be confused with the sample-mapping caches: mappings live for one callback and reread current guest values, while decoded samples can survive callbacks only after fresh-byte validation.

Evidence and limitations

#187 merged October 3. Its final native observations did not establish a repeatable game-performance gain, and some PGR2 tail metrics worsened. The merge retained the mechanism without claiming a speedup. Do not convert fewer decoder calls or correctness passes into a game-FPS percentage; those results also do not qualify November main as a fresh candidate.

Useful regression cases are repeated callbacks within one block; in-place guest rewrites at the same address; changed channels/size; invalid/truncated headers; every emitted-frame bound; loops and stream/SGE boundaries; voice reset/reuse; and snapshot restoration. Compare exact decoded samples and downstream output. For timing, include hit/miss ratios, decoded blocks per output frame, byte-comparison cost, worker CPU and game cadence/tails under matched settings.

Separate decoder research

The 89-step-index × 16-nibble transition table is a different experiment: it accelerates a decode that still happens, whereas a block-cache hit skips that decode. #195 is closed/unmerged research and is not part of this delivered cache. Retain predictor update, clipping and sample order if revisiting it.

Source and related work

Clone this wiki locally