mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
6e6a0b3ed8
Closes the four gaps named in issue #103 and lands the separable, verified parts of PR #102. See docs/internal/SPU_FIDELITY_103.md for the per-piece disposition and the oracle-verification queue. SPU DSP (issue #103), clean-room from hardware documentation: - Reverb: the documented 22050 Hz engine — vLIN/vRIN input, same-side and crossed different-side IIR reflections, 4-tap comb early echo, both all-pass filters, vLOUT/vROUT output, and the work-area address advance. Gated on SPUCNT bit 7 for WRITES only; reads, IRQ checks, address advance and output always run, as on hardware. Addressing is confined to [mBASE, 0x80000). Per-voice sends from EON, CD send from SPUCNT bits 0+2. - Noise: 16-bit LFSR with the documented parity feedback, clocked at the SPUCNT bits 8-13 rate; NON voices substitute it for their ADPCM sample. - Volume sweeps: all 24 voice L/R plus main L/R. Bit 15 selects sweep vs direct; sweep is a live envelope on the same rate machinery as ADSR, and a guest read of a sweeping register returns the live level. - Capture buffers: CD L/R and voice 1/3 output written to SPU RAM, so a game that parks the IRQ address there still gets its interrupt. - SPU IRQ (I_STAT bit 9): checked at every SPU RAM access class — FIFO, DMA both directions, ADPCM block fetch, capture writes, reverb work-area accesses — gated on SPUCNT bit 6, mirrored in SPUSTAT bit 6, acked by clearing SPUCNT bit 6. The CD-only fast path in spu_render was removed rather than extended: the SPU now has per-frame work that must run in every path, and an FMV (XA audio with zero active voices) is exactly the CD-with-reverb case. This is a clean-room implementation from the psx-spx/nocash register map and documented algorithm. Beetle's source was deliberately not consulted: it is GPL-2.0-or-later and this project is PolyForm Noncommercial. PR #16 was parked for exactly that reason and PR #13's reverb was separately wrong (it gated on bit 15 of dAPF1, an address offset, instead of SPUCNT bit 7). The one place this knowingly deviates from hardware is the 22.05 -> 44.1 kHz reconstruction filter, which the documentation does not specify; it is isolated in a single function and flagged for oracle comparison. CAUSE.IP2 (from PR #102): IP2 is combinational on R3000A — it mirrors the interrupt line and falls the instant the guest acks I_STAT or masks the source. It was being OR'd in at delivery and never cleared, in TWO places (the compiled path and psx_interpreter.c, the latter missed by PR #102), leaving a phantom pending interrupt that can spin a kernel dispatcher looping on CAUSE.IP & SR.IM. psx_irq_refresh_cause_ip2() is now the sole writer of bit 10, refreshed at raise, I_STAT ack, I_MASK write, HLE context restore and power-on. Verified against the Beetle oracle's irq.cpp/cpu.cpp rather than asserted, and covered by test_cause_ip2_combinational. Mid-dispatch audio pump (from PR #102), with a fix: routed through a gate that mirrors sdl_audio_update's mute/sink decision. Pumping unconditionally from the VBlank edge, as submitted, would have pushed real audio during every turbo-load hard mute and defeated the freeze-in-place mute model. SPU RAM DMA readback (from PR #102): DMA4 in the SPU->RAM direction wrote literal zeros; SPU RAM is readable memory and titles carry state through it across an Exec boundary. Also fixes an unrelated build race found on the way: tools/embed_spirv.py wrote its intermediate SPIR-V to <source>.spv, so the two runtime targets that embed the same shaders raced on one path and one deleted the file the other was reading. Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
172 lines
7.1 KiB
C
172 lines
7.1 KiB
C
#ifndef PSXRECOMP_SPU_H
|
|
#define PSXRECOMP_SPU_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
void spu_init(void);
|
|
void spu_render(int16_t* out_stereo, int frames);
|
|
|
|
typedef struct SpuDebugInfo {
|
|
uint32_t ctrl;
|
|
uint32_t active_mask;
|
|
int16_t main_l;
|
|
int16_t main_r;
|
|
int16_t cd_l;
|
|
int16_t cd_r;
|
|
uint32_t key_on_count;
|
|
uint64_t render_frames;
|
|
uint64_t nonzero_frames;
|
|
int32_t last_peak;
|
|
int32_t peak;
|
|
uint32_t cd_frames;
|
|
uint64_t cd_push_frames;
|
|
uint64_t cd_overflow_frames;
|
|
uint64_t cd_underflow_frames;
|
|
} SpuDebugInfo;
|
|
|
|
void spu_debug_info(SpuDebugInfo* out);
|
|
|
|
/* ---- Per-voice register snapshot. Mirrors the fields the Beetle oracle
|
|
* exposes via PS_SPU::GetRegister(GSREG_V0_*) so both backends produce a
|
|
* structurally-identical spu_voices payload that diff tooling can compare. */
|
|
typedef struct SpuVoiceState {
|
|
int active; /* our internal "is voicing" flag (no ADSR yet) */
|
|
uint16_t vol_ctrl_l; /* raw voice reg 0 */
|
|
uint16_t vol_ctrl_r; /* raw voice reg 1 */
|
|
uint16_t pitch; /* raw voice reg 2 */
|
|
uint16_t start_lo; /* raw voice reg 3 (start_addr >> 3) */
|
|
uint16_t adsr_lo; /* raw voice reg 5 */
|
|
uint16_t adsr_hi; /* raw voice reg 6 */
|
|
uint16_t loop_lo; /* raw voice reg 7 (loop_addr >> 3) */
|
|
uint32_t cur_addr; /* current decode pointer (byte addr in SPU RAM) */
|
|
uint32_t repeat_addr; /* effective loop point (byte addr in SPU RAM) */
|
|
uint8_t last_flags; /* flag byte from most recently decoded block */
|
|
uint8_t sample_idx; /* position inside the current 28-sample block */
|
|
uint16_t phase; /* sub-sample phase counter (0..0x1000) */
|
|
uint16_t env_level; /* live ADSR envelope level (0..0x7FFF) */
|
|
uint8_t adsr_phase; /* 0=attack 1=decay 2=sustain 3=release */
|
|
int16_t vol_cur_l; /* live effective L volume (sweep-aware, full int16) */
|
|
int16_t vol_cur_r; /* live effective R volume (sweep-aware, full int16) */
|
|
} SpuVoiceState;
|
|
|
|
typedef struct SpuGlobalState {
|
|
uint16_t ctrl; /* SPUCONT (0x1F801DAA) */
|
|
uint16_t main_vol_l; /* raw 0x1F801D80 */
|
|
uint16_t main_vol_r; /* raw 0x1F801D82 */
|
|
uint32_t kon_latch; /* last 24-bit KEYON value */
|
|
uint32_t koff_latch; /* last 24-bit KEYOFF value */
|
|
uint32_t pmon; /* pitch mod (0x1F801D90/92) */
|
|
uint32_t non; /* noise mode (0x1F801D94/96) */
|
|
uint32_t eon; /* reverb mode (0x1F801D98/9A) */
|
|
uint32_t endx; /* end-block reached latch (0x1F801D9C/9E) */
|
|
uint32_t active_mask; /* recomp-local "still voicing" mask */
|
|
|
|
/* ---- SPU DSP fidelity state (issue #103: IRQ/reverb/noise/sweeps) ---- */
|
|
uint16_t irq_flag; /* SPU IRQ latch (mirrors SPUSTAT bit 6) */
|
|
uint16_t reverb_on; /* SPUCNT bit 7 (reverb work-area write enable) */
|
|
uint32_t irq_addr; /* IRQ address as a BYTE address (reg 0x1F801DA4 << 3) */
|
|
uint32_t reverb_mbase; /* reverb work area start, byte address (mBASE << 3) */
|
|
uint32_t reverb_cur; /* current reverb buffer address (byte, advances @22050Hz) */
|
|
uint32_t capture_pos; /* capture-buffer write offset (0..0x3FE, wraps at 0x400) */
|
|
uint16_t noise_lfsr; /* live noise shift register value */
|
|
uint16_t noise_pad; /* reserved / alignment */
|
|
uint32_t sweep_l_mask; /* voices whose LEFT volume register is in sweep mode */
|
|
uint32_t sweep_r_mask; /* voices whose RIGHT volume register is in sweep mode */
|
|
uint32_t sweep_main; /* bit0 = main L in sweep mode, bit1 = main R */
|
|
} SpuGlobalState;
|
|
|
|
void spu_get_voice_state(int voice, SpuVoiceState* out);
|
|
void spu_get_global_state(SpuGlobalState* out);
|
|
/* Debug peek into SPU sample RAM (spu_ram TCP command). */
|
|
uint32_t spu_ram_peek(uint32_t addr, uint8_t *out, uint32_t len);
|
|
|
|
/* ---- Always-on per-voice event ring. Records KEYON, KEYOFF, voice
|
|
* end-flag-stop and end-flag-loop events with a frame timestamp. Allocated
|
|
* at spu_init; never armed/disarmed. Query the window of interest. */
|
|
typedef enum {
|
|
SPU_EV_KEYON = 1,
|
|
SPU_EV_KEYOFF = 2,
|
|
SPU_EV_END_STOP = 3, /* loop_end without repeat → voice silenced */
|
|
SPU_EV_END_LOOP = 4, /* loop_end with repeat → cur_addr=repeat_addr */
|
|
SPU_EV_IRQ = 5 /* SPU RAM IRQ-address hit → I_STAT bit 9 raised;
|
|
voice=0xFF (not voice-attributable), addr=byte
|
|
address that matched */
|
|
} SpuEventKind;
|
|
|
|
typedef struct SpuEvent {
|
|
uint64_t seq;
|
|
uint32_t frame;
|
|
uint8_t kind;
|
|
uint8_t voice;
|
|
uint16_t pitch;
|
|
uint32_t addr; /* start_addr for KEYON; cur_addr for KEYOFF/END_*; repeat_addr for END_LOOP */
|
|
uint16_t adsr_lo;
|
|
uint16_t adsr_hi;
|
|
uint16_t vol_l;
|
|
uint16_t vol_r;
|
|
} SpuEvent;
|
|
|
|
uint64_t spu_event_total(void);
|
|
uint32_t spu_event_get(SpuEvent* out, uint32_t max_count); /* most recent up to max_count */
|
|
void spu_event_reset(void);
|
|
|
|
/* MMIO read/write (0x1F801C00-0x1F801FFF) */
|
|
uint32_t spu_read(uint32_t addr);
|
|
void spu_write(uint32_t addr, uint32_t value);
|
|
|
|
/* DMA channel 4 interface. spu_dma_read reads one 32-bit word from SPU RAM
|
|
* at the current transfer address, advances the address by 4, and runs the
|
|
* SPU IRQ-address check (SPU RAM -> CPU direction, DICR direction bit 0). */
|
|
void spu_dma_write(uint32_t word);
|
|
uint32_t spu_dma_read(void);
|
|
int spu_dma_ready(void);
|
|
|
|
/* CD-ROM XA/CDDA input path. Samples are stereo 44.1 kHz PCM entering the
|
|
* SPU CD input bus; SPU control bit 0 and CD volume registers gate output. */
|
|
void spu_cd_audio_push(const int16_t* stereo, int frames);
|
|
void spu_cd_audio_reset(void);
|
|
|
|
/* Get pointer to SPU RAM for direct access (512KB) */
|
|
const uint8_t* spu_get_ram(void);
|
|
|
|
/* ---- Verified-enhancement shadow tap (consumed by spu_shadow.c) ---------
|
|
*
|
|
* Present-time, opt-in float SPU re-render. When the shadow is enabled,
|
|
* spu_render fills a per-output-frame, per-voice tap with the exact inputs the
|
|
* canon used (the 4 decoded samples bracketing the sub-sample phase, the
|
|
* fractional phase, envelope level, and per-voice + main volumes). The shadow
|
|
* re-interpolates these in float. The layout MUST match spu.c's internal
|
|
* SpuShadowVoiceTap / SpuShadowFrameTap. */
|
|
#define SPU_SHADOW_MAX_VOICES 24
|
|
|
|
typedef struct {
|
|
int16_t s[4]; /* decoded samples sample_idx-1 .. +2 (block-edge clamped) */
|
|
float frac; /* fractional sub-sample phase in [0,1) */
|
|
uint16_t env; /* env_level (0..0x7FFF) */
|
|
int16_t vol_l; /* per-voice L volume, 1.14 scale (effective int16 >> 1) */
|
|
int16_t vol_r; /* per-voice R volume, same scale */
|
|
uint8_t active;
|
|
} SpuShadowVoiceTapPub;
|
|
|
|
typedef struct {
|
|
SpuShadowVoiceTapPub voice[SPU_SHADOW_MAX_VOICES];
|
|
int16_t main_l;
|
|
int16_t main_r;
|
|
int enabled;
|
|
} SpuShadowFrameTapPub;
|
|
|
|
/* Pointer to the tap array (SpuShadowFrameTapPub[]) and the number of valid
|
|
* frames filled by the most recent spu_render block. */
|
|
const void* spu_shadow_tap_buffer(void);
|
|
int spu_shadow_tap_count(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PSXRECOMP_SPU_H */
|