mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
174 lines
6.9 KiB
C
174 lines
6.9 KiB
C
/*
|
|
* Guest-clocked SPU (beads-eio.3.321): every SPU access advances the SPU to
|
|
* the current guest cycle, so a game that keys a voice on and then polls its
|
|
* envelope (ENVX) sees it rise without any host audio pump. Crash Bandicoot's
|
|
* AudioUpdate keys a voice off when it reads "key on, envelope 0"; with the
|
|
* SPU advanced only at the per-frame pump, its spin sound was cut the frame
|
|
* it started.
|
|
*
|
|
* The test includes spu.c so it can drive the guest clock directly.
|
|
* Build/run: ctest -R spu_guest_clock_test
|
|
*/
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
|
|
#include "../src/spu.c"
|
|
|
|
uint64_t s_frame_count;
|
|
static uint64_t g_cycle;
|
|
uint64_t psx_get_cycle_count(void) { return g_cycle; }
|
|
void audio_trace_pcm(int tap, const int16_t *stereo, int frames) { (void)tap; (void)stereo; (void)frames; }
|
|
void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) { (void)kind; (void)a; (void)b; }
|
|
void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; }
|
|
uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t len) { (void)data; (void)len; return crc; }
|
|
bool spu_shadow_enabled(void) { return false; }
|
|
void spu_shadow_reset(void) {}
|
|
void spu_shadow_process(int16_t *canon, int frames) { (void)canon; (void)frames; }
|
|
|
|
static int failures;
|
|
#define CHECK(condition, message) \
|
|
do { \
|
|
if (!(condition)) { fprintf(stderr, "FAIL: %s\n", message); failures++; } \
|
|
else printf("ok: %s\n", message); \
|
|
} while (0)
|
|
|
|
#define VOICE0(off) (0x1F801C00u + (off))
|
|
static int s_suppress;
|
|
static int suppressed(void) { return s_suppress; }
|
|
|
|
static void stage_voice0(void) {
|
|
spu_init();
|
|
g_cycle = 0;
|
|
for (uint32_t b = 0; b < 8u; b++) { /* nonzero ADPCM, last block loops */
|
|
uint8_t *blk = spu_ram + 0x1000u + b * 16u;
|
|
blk[0] = 0x00;
|
|
blk[1] = b == 7u ? 0x03 : 0x00;
|
|
memset(blk + 2, 0x77, 14);
|
|
}
|
|
spu_write(0x1F801DAAu, 0x8000u); /* SPU on */
|
|
spu_write(VOICE0(0x0), 0x3FFFu); /* volume L/R */
|
|
spu_write(VOICE0(0x2), 0x3FFFu);
|
|
spu_write(VOICE0(0x4), 0x1000u); /* pitch 1.0 */
|
|
spu_write(VOICE0(0x6), 0x1000u >> 3); /* start address */
|
|
spu_write(VOICE0(0x8), 0x000Fu); /* fastest linear attack, SL max */
|
|
spu_write(VOICE0(0xA), 0x0000u);
|
|
spu_write(VOICE0(0xE), 0x1070u >> 3); /* repeat address */
|
|
}
|
|
|
|
static void test_envelope_rises_between_accesses(void) {
|
|
stage_voice0();
|
|
g_cycle = 768u * 10u + 100u;
|
|
spu_write(0x1F801D88u, 0x0001u); /* KEYON voice 0 */
|
|
CHECK(spu_output_available() == 10u, "the write first renders the 10 samples owed");
|
|
CHECK(spu_read(VOICE0(0xC)) == 0u, "ENVX is 0 within the KEYON sample");
|
|
g_cycle += 768u * 3u;
|
|
const uint32_t env = spu_read(VOICE0(0xC));
|
|
CHECK(env > 0u, "ENVX rises three samples after KEYON with no host pump");
|
|
CHECK(spu_output_available() == 13u, "the read rendered exactly the 3 owed samples");
|
|
int16_t out[16 * 2];
|
|
CHECK(spu_output_pop(out, 16u) == 13u && spu_output_available() == 0u,
|
|
"the host pump drains what the guest clock produced");
|
|
}
|
|
|
|
static void test_snapshot_keeps_clock_phase(void) {
|
|
stage_voice0();
|
|
g_cycle = 768u * 20u + 400u;
|
|
spu_catch_up();
|
|
const uint32_t n = spu_snapshot_bytes();
|
|
uint8_t *snap = (uint8_t *)malloc(n);
|
|
spu_snapshot_write(snap);
|
|
CHECK(spu_snapshot_len_ok(n) && spu_snapshot_len_ok(n - 4u) && !spu_snapshot_len_ok(n - 1u),
|
|
"current and pre-clock snapshot lengths validate, others do not");
|
|
|
|
g_cycle = 768u * 5000u + 400u; /* restored guest clock, same phase */
|
|
CHECK(spu_snapshot_read(snap, n) == 1, "snapshot with clock phase loads");
|
|
spu_catch_up();
|
|
CHECK(spu_output_available() == 0u, "no samples owed right after restore");
|
|
g_cycle += 367u;
|
|
spu_catch_up();
|
|
CHECK(spu_output_available() == 0u, "one cycle short of the next sample boundary");
|
|
g_cycle += 1u;
|
|
spu_catch_up();
|
|
CHECK(spu_output_available() == 1u, "phase carried: next sample lands on the boundary");
|
|
|
|
g_cycle = 768u * 9000u + 123u;
|
|
CHECK(spu_snapshot_read(snap, n - 4u) == 1, "pre-clock snapshot loads (phase 0)");
|
|
spu_catch_up();
|
|
g_cycle += 767u;
|
|
spu_catch_up();
|
|
CHECK(spu_output_available() == 0u, "pre-clock snapshot anchors the clock at load");
|
|
free(snap);
|
|
}
|
|
|
|
static void test_gate_and_rewind(void) {
|
|
stage_voice0();
|
|
spu_set_output_gate(suppressed);
|
|
g_cycle = 768u * 4u;
|
|
spu_write(0x1F801D88u, 0x0001u);
|
|
spu_output_flush();
|
|
s_suppress = 1;
|
|
g_cycle += 768u * 6u;
|
|
const uint32_t env = spu_read(VOICE0(0xC));
|
|
CHECK(env > 0u && spu_output_available() == 0u,
|
|
"suppressed output still advances SPU state");
|
|
s_suppress = 0;
|
|
spu_set_output_gate(NULL);
|
|
const uint64_t clock = s_clock;
|
|
g_cycle = 768u * 2u; /* guest clock rewound (state load) */
|
|
spu_catch_up();
|
|
CHECK(s_clock == g_cycle && s_clock < clock && spu_output_available() == 0u,
|
|
"a rewound guest clock re-anchors without rendering");
|
|
}
|
|
|
|
static uint32_t pcm_frames;
|
|
static void pcm_overlay_test(int16_t* stereo, uint32_t frames) {
|
|
pcm_frames += frames;
|
|
for (uint32_t i = 0; i < frames * 2u; ++i) stereo[i] = 1234;
|
|
}
|
|
|
|
static void test_pcm_overlay_uses_guest_clock(void) {
|
|
g_cycle = 0;
|
|
spu_init();
|
|
spu_set_pcm_overlay(pcm_overlay_test);
|
|
spu_catch_up();
|
|
g_cycle += 768u * 3u;
|
|
spu_catch_up();
|
|
CHECK(pcm_frames == 3u && spu_output_available() == 3u,
|
|
"PCM overlay advances with guest samples while the native SPU is off");
|
|
int16_t output[8];
|
|
CHECK(spu_output_pop(output, 4u) == 3u && output[0] == 1234 &&
|
|
output[5] == 1234 && pcm_frames == 3u,
|
|
"host output consumption neither skips nor advances mod playback");
|
|
spu_set_output_gate(suppressed);
|
|
s_suppress = 1;
|
|
g_cycle += 768u * 2u;
|
|
spu_catch_up();
|
|
CHECK(pcm_frames == 5u && spu_output_available() == 0u,
|
|
"suppressed output still advances deterministic mod PCM state");
|
|
s_suppress = 0;
|
|
spu_set_output_gate(NULL);
|
|
spu_write(0x1F801DAAu, 0x8000u);
|
|
g_cycle += 768u * 4u;
|
|
spu_catch_up();
|
|
CHECK(pcm_frames == 9u && spu_output_available() == 4u,
|
|
"native voice-free fast path advances the same mod PCM clock");
|
|
spu_set_pcm_overlay(NULL);
|
|
spu_output_flush();
|
|
g_cycle += 768u;
|
|
spu_catch_up();
|
|
CHECK(pcm_frames == 9u && spu_output_pop(output, 4u) == 1u && output[0] == 0,
|
|
"unbinding restores native silence without advancing mod playback");
|
|
}
|
|
|
|
int main(void) {
|
|
test_envelope_rises_between_accesses();
|
|
test_snapshot_keeps_clock_phase();
|
|
test_gate_and_rewind();
|
|
test_pcm_overlay_uses_guest_clock();
|
|
if (failures) { fprintf(stderr, "%d failure(s)\n", failures); return 1; }
|
|
puts("spu_guest_clock_test: ok");
|
|
return 0;
|
|
}
|