diff --git a/docs/LOAD_TIME_ZERO.md b/docs/LOAD_TIME_ZERO.md index f52c477d..ff07c197 100644 --- a/docs/LOAD_TIME_ZERO.md +++ b/docs/LOAD_TIME_ZERO.md @@ -386,3 +386,38 @@ rejected on paper. `spike/load-shards` (TombaRecomp). Recreate worktrees before E4. - `cdrom_bursts` ring (`cdrom.c:311-360`) already measures load durations — E0's window boundary source. + + +## Incremental resident sectors + +`psx_mod_resident_cd_sector(lba, user_data)` admits exactly one verified data +sector through the actual controller FIFO and IRQ path. It keeps ReadN active, +advances the drive cursor by one sector, and restores ordinary timing for the +next sector. The original guest Ready callback, header/payload DMA, resource +consumers and final Pause acknowledgement still run. It never writes guest +RAM, calls a guest callback or retains a host pointer/transfer cursor. + +The title must prove its stock file/span, original code and live callback, +and that the next destination is free of pending consumers. A zero ring +occupancy byte alone is insufficient when an SDK queue retains its source +pointer. Check both the software GPU queue and active GPU/SPU DMA before each +admission. The shared helper checks CD ownership only; it does not know the +title's upload queues or grant a whole-file transfer. + +Admission requires the original ReadN ACK and response to be fully consumed, +mode 0x80 or 0xA0, the exact next drive LBA, and no pending command, IRQ, CD DMA +or unread payload. Mode 0xA0 may retain only the already-consumed sector's ECC +tail. The helper witnesses the real raw sector's mode, submode, duplicate +subheader and BCD address and compares every user byte with resident data. +XA, FMV, Form2, realtime, changed bytes and busy ownership decline without +controller mutation. This first version still reads raw sector metadata from +the mounted image for verification; it does not claim removal of all host I/O. + +No extra snapshot fields are needed: every accepted effect lives in the +ordinary controller buffers, cursor, status and IRQ state. The real-controller +`cdrom_resident_sector_test` covers both modes, repeated one-sector admission, +24 current-format snapshot restores, subsequent original-stream delivery and +real Pause completion, plus 33 byte-for-byte atomic decline cases. Existing +FIFO and IRQ-mask tests also pass. These tests qualify the helper's controller +contract, not a title adapter or load-time improvement. Tron consumer integration +and measured runtime outcomes remain pending under beads-eio.3.412/eio.27.3. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 68b1877c..7264acdd 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1660,6 +1660,10 @@ if(BUILD_TESTING) target_include_directories(cdrom_sector_fifo_test PRIVATE include src) add_test(NAME cdrom_sector_fifo_test COMMAND cdrom_sector_fifo_test) + add_executable(cdrom_resident_sector_test tests/test_cdrom_resident_sector.c) + target_include_directories(cdrom_resident_sector_test PRIVATE include src) + add_test(NAME cdrom_resident_sector_test COMMAND cdrom_resident_sector_test) + add_executable(cdrom_lid_state_test tests/test_cdrom_lid_state.c) target_include_directories(cdrom_lid_state_test PRIVATE src) add_test(NAME cdrom_lid_state_test COMMAND cdrom_lid_state_test) @@ -2104,6 +2108,10 @@ if(BUILD_TESTING) add_test(NAME spu_end_without_repeat_test COMMAND spu_end_without_repeat_test) + add_executable(spu_silent_fetch_test tests/test_spu_silent_fetch.c) + target_include_directories(spu_silent_fetch_test PRIVATE include) + add_test(NAME spu_silent_fetch_test COMMAND spu_silent_fetch_test) + add_executable(spu_pitch_zero_test tests/test_spu_pitch_zero.c) target_include_directories(spu_pitch_zero_test PRIVATE include) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index 2af3301e..9b7eb879 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -43,6 +43,12 @@ int cdrom_get_setloc_lba(void); int cdrom_resident_complete_read(uint32_t lba, uint32_t sectors, const uint8_t* last_user_data, int (*commit)(void*), void* context); +/* Admit one verified resident data sector into the real ReadN FIFO/IRQ path. + * Requires an ACKed stream at lba, no pending controller/DMA/payload owner, + * and matching pure-data raw bytes. Never stops the stream or calls a guest + * callback. The title must separately prove its destination/upload ownership. */ +int cdrom_resident_feed_sector(uint32_t lba, const uint8_t* user_data); + /* 'instant' per-frame sector-IRQ budget (step 3). Clamped to [1, 4096]; * the per-sector period additionally floors at CDROM_MIN_DELAY. Writers: * game.toml [runtime] instant_max_per_frame, the cdrom_instant_rate TCP diff --git a/runtime/include/mod_resident.h b/runtime/include/mod_resident.h index c3f72fab..00a78604 100644 --- a/runtime/include/mod_resident.h +++ b/runtime/include/mod_resident.h @@ -56,6 +56,18 @@ typedef struct PSXResidentFile { int psx_mod_resident_cd_read(uint32_t address, const void* data, uint32_t lba, uint32_t sectors); +/* Deliver exactly one stock data sector through the actual controller FIFO, + * IRQ and original guest Ready/DMA path. The ReadN stream remains active and + * its next sector keeps ordinary timing. Busy/changed/media sectors decline + * without mutation. No host pointer or transfer cursor is retained; ordinary + * controller snapshots contain every accepted effect. + * + * Trusted title code must prove the file/span/code/callback and a free next + * destination, including queued GPU source pointers and active GPU/SPU DMA. + * The controller checks CD ownership only. Do not use for XA/FMV or to bypass + * original consumers, publish completion, or replace a final Pause ack. */ +int psx_mod_resident_cd_sector(uint32_t lba, const void* user_data); + typedef struct PSXResidentSink PSXResidentSink; /* Store one derived blob (decoded asset, table, ...) for source file `file`. diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index dd2f7ab8..81fa3736 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -1434,7 +1434,8 @@ static int maybe_deliver_xa_audio(const uint8_t* raw_data, int lba, return 1; } -static int read_sector_at(int min, int sec, int sect) { +static int read_sector_at_with_raw(int min, int sec, int sect, + const uint8_t* supplied_raw) { int lba = msf_to_lba(min, sec, sect); uint8_t user_data[SECTOR_SIZE]; uint8_t raw_data[RAW_SECTOR_SIZE]; @@ -1443,7 +1444,12 @@ static int read_sector_at(int min, int sec, int sect) { const uint8_t *history_bytes = user_data; int history_size = SECTOR_SIZE; - if (iso_handle) { + if (supplied_raw) { + memcpy(raw_data, supplied_raw, sizeof raw_data); + memcpy(user_data, raw_data + RAW_USER_DATA_OFFSET, sizeof user_data); + have_raw = 1; + if (subq_replacements_active) update_last_valid_subq((uint32_t)lba); + } else if (iso_handle) { have_raw = iso_read_raw_sector(iso_handle, lba, raw_data, RAW_SECTOR_SIZE); if (have_raw) { memcpy(user_data, raw_data + RAW_USER_DATA_OFFSET, SECTOR_SIZE); @@ -1540,6 +1546,10 @@ static int read_sector_at(int min, int sec, int sect) { return delivery.data_delivered ? 1 : 0; } +static int read_sector_at(int min, int sec, int sect) { + return read_sector_at_with_raw(min, sec, sect, NULL); +} + static void advance_msf(int* m, int* s, int* f) { (*f)++; if (*f >= 75) { *f = 0; (*s)++; } @@ -1877,8 +1887,9 @@ static int data_fifo_ready(void) { static uint64_t s_dataready_fires; /* INT1 (data-ready) raised per streamed sector — FMV dispatch probe */ uint64_t cdrom_get_dataready_fires(void) { return s_dataready_fires; } -static int deliver_read_sector(void) { - int delivered = read_sector_at(read_min, read_sec, read_sect); +static int deliver_read_sector_with_raw(const uint8_t* supplied_raw) { + int delivered = read_sector_at_with_raw(read_min, read_sec, read_sect, + supplied_raw); advance_msf(&read_min, &read_sec, &read_sect); if (xa_data_end_pending) { xa_data_end_pending = 0; @@ -1900,6 +1911,10 @@ static int deliver_read_sector(void) { return 1; } +static int deliver_read_sector(void) { + return deliver_read_sector_with_raw(NULL); +} + /* A continuing DMA needs the newly filled slot immediately. A sector whose * INT1 is merely pending must leave the previously announced slot readable: * Final Bout drains that slot after acknowledging its IRQ. Redirecting it @@ -3346,6 +3361,59 @@ int cdrom_resident_complete_read(uint32_t lba, uint32_t sectors, return 1; } +int cdrom_resident_feed_sector(uint32_t lba, const uint8_t* user_data) { + uint8_t raw[RAW_SECTOR_SIZE]; + uint64_t timing_seq; + int min, sec, frame; + /* The title owns the verified file, callback and destination consumer. + * This helper owns one real controller delivery, never guest RAM or a + * host-side continuation. A declined call leaves the controller intact. */ + if (!user_data || !has_disc() || lba >= iso_sector_count(iso_handle) || + lba > 0x7FFFFF00u || !reading || read_cmd != 0x06 || + (mode_reg != 0x80u && mode_reg != 0xA0u) || + xa_stream_active || xa_data_end_pending || cdda_playing || + cdda_data_end_pending || setloc_pending || + msf_to_lba(read_min, read_sec, read_sect) != (int)lba || + irq_flag || pending.pending || queued_cmd.pending || + pending_dataready || pending_present_due || param_count || + response_read < response_count || dma_cdrom_transfer_active()) + return 0; + /* PsyQ reads 12 header bytes and 2048 user bytes, retaining 280 ECC + * bytes in whole-sector mode. That tail is not another owner's payload. + * A partial header/payload or any other unread FIFO must not be replaced. */ + if (rb_available() && + !(mode_reg == 0xA0u && RB_.size == WHOLE_SECTOR_SIZE && + RB_.pos == 12 + SECTOR_SIZE && last_sector_mode == mode_reg && + last_sector_have_raw && last_sector_lba >= 0 && + (uint32_t)last_sector_lba + 1u == lba)) + return 0; + if (!iso_read_raw_sector(iso_handle, lba, raw, sizeof raw) || + raw[15] != CDROM_SECTOR_MODE2 || + memcmp(raw + 16, raw + 20, 4) || !(raw[18] & 0x08u) || + (raw[18] & 0x66u) || + memcmp(raw + RAW_USER_DATA_OFFSET, user_data, SECTOR_SIZE)) + return 0; + lba_to_msf((int)lba, 150, &min, &sec, &frame); + if (raw[12] != bin_to_bcd(min) || raw[13] != bin_to_bcd(sec) || + raw[14] != bin_to_bcd(frame)) + return 0; + + /* Keep the stock header/subheader/ECC and supply the verified resident + * payload. The ordinary FIFO, status response, IRQ presentation and DMA + * paths handle the sector; the guest still executes its original Ready. + * There is no saved host pointer or multi-sector grant to invalidate. */ + memcpy(raw + RAW_USER_DATA_OFFSET, user_data, SECTOR_SIZE); + timing_seq = cd_timing_begin_sector((int)lba); + (void)deliver_read_sector_with_raw(raw); + cd_timing_flag(timing_seq, CDT_DATA); + cd_timing_arm_irq(timing_seq); + s_accel_block_accum = 0; + read_delay = sector_delay_cycles(); + s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay; + trace_cdrom('H', lba, 1, 0); + return 1; +} + /* Savestate post-load: authentic CD second-response delays (ReadTOC ~30M * cycles, Init ~1.1M, far seeks, etc.) leave the restored frame on screen * for up to ~1s+ of wall time. Clamp those timers so the next cdrom_advance diff --git a/runtime/src/mod_guest_services.c b/runtime/src/mod_guest_services.c index 4e1d8542..5dbd78f9 100644 --- a/runtime/src/mod_guest_services.c +++ b/runtime/src/mod_guest_services.c @@ -34,6 +34,11 @@ int psx_mod_resident_cd_read(uint32_t address, const void* data, (const uint8_t*)data + copy.bytes - 2048u, resident_cd_copy, ©); } +int psx_mod_resident_cd_sector(uint32_t lba, const void* user_data) { + if (psx_mod_host_output_scope()) return 0; + return cdrom_resident_feed_sector(lba, (const uint8_t*)user_data); +} + uint32_t psx_mod_call_guest(struct CPUState* cpu, uint32_t function, uint32_t return_address, uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3) { diff --git a/runtime/src/spu.c b/runtime/src/spu.c index df885392..be5e4755 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -890,9 +890,21 @@ typedef char spu_shadow_voice_count_check[ const void* spu_shadow_tap_buffer(void) { return s_shadow_tap; } int spu_shadow_tap_count(void) { return s_shadow_tap_frame; } +/* Hardware ADPCM fetches continue after Release reaches zero. Audible state + * only gates the envelope/output; pitch and block/IRQ/ENDX state keep running. + * PSX-SPX, SPU Interrupt / Voice Interrupt: + * https://psx-spx.consoledev.net/soundprocessingunitspu/#voice-interrupt + * Private host voices have an explicit lifetime and are not hardware slots. */ +static int voice_needs_clock(int idx) { + const SpuVoice *v = &voices[idx]; + return v->active || (idx < SPU_VOICE_COUNT && + ((voice_reg(idx, 2) & 0x3FFFu) || v->sample_idx >= SPU_BLOCK_SAMPLES)); +} + static int16_t voice_next_sample(int idx) { SpuVoice *v = &voices[idx]; - if (!v->active) return 0; + if (!voice_needs_clock(idx)) return 0; + const int audible = v->active; if (v->sample_idx >= SPU_BLOCK_SAMPLES) { if (v->flags & 0x01u) { @@ -901,7 +913,7 @@ static int16_t voice_next_sample(int idx) { * (CurAddr = LoopAddr) does this unconditionally. */ v->cur_addr = v->repeat_addr & (SPU_RAM_SIZE - 1u); if (v->flags & 0x02u) { - spu_event_record(SPU_EV_END_LOOP, idx, v->repeat_addr); + if (audible) spu_event_record(SPU_EV_END_LOOP, idx, v->repeat_addr); } else { /* END without REPEAT: hardware forces the envelope to ZERO * and enters Release (Beetle spu.cpp:341-352 — "Force @@ -918,7 +930,7 @@ static int16_t voice_next_sample(int idx) { * parked there at full envelope; a handful of ice-block * hits in the X4 attract demo railed the whole mix at * +32767 for ~35 s ("static" / music cut-out). */ - spu_event_record(SPU_EV_END_STOP, idx, v->cur_addr); + if (audible) spu_event_record(SPU_EV_END_STOP, idx, v->cur_addr); v->adsr_phase = ADSR_RELEASE; v->adsr_divider = 0; v->env_level = 0; @@ -931,10 +943,12 @@ static int16_t voice_next_sample(int idx) { * INSTEAD of its interpolated ADPCM sample. Everything else — block * decoding/address advance, ENDX latching, pitch stepping, the ADSR * envelope and the volume stages — behaves exactly as for ADPCM. */ - int16_t raw_s; + int16_t raw_s = 0; uint32_t non_mask = (uint32_t)spu_regs[reg_index(0x1F801D94u)] | ((uint32_t)spu_regs[reg_index(0x1F801D96u)] << 16); - if (idxsample_idx; @@ -971,8 +985,9 @@ static int16_t voice_next_sample(int idx) { } /* Step envelope once per output sample (44.1 kHz). */ - adsr_run(idx, v); - /* Deactivate once Release fully decays to silence. */ + if (audible) adsr_run(idx, v); + /* Retire audible output once Release fully decays. Hardware fetches keep + * running through voice_needs_clock; private voices stop completely. */ if (v->adsr_phase == ADSR_RELEASE && v->env_level == 0) { v->active = 0; } @@ -1140,7 +1155,7 @@ static void spu_render_block(int16_t* out_stereo, int frames) { int mix_voices=SPU_VOICE_COUNT+(private_enabled?SPU_PRIVATE_VOICES:0); if (enabled) { for (int v = 0; v < mix_voices; v++) { - if (voices[v].active) { any_voice = 1; break; } + if (voice_needs_clock(v)) { any_voice = 1; break; } } } @@ -1157,7 +1172,9 @@ static void spu_render_block(int16_t* out_stereo, int frames) { /* MotK intro FMV (and other XA-only scenes): active_mask stays 0 while * CD/XA plays. Keep capture / noise / reverb / main sweeps (issue #103 * fidelity — FMV is exactly the CD-with-reverb case) but skip the - * 24-voice walk and per-voice sweep steps. Shadow tap stays on the + * 24-voice walk and per-voice sweep steps only when no hardware slot needs + * pitch-driven fetching. Silent released slots can still trigger IRQs. + * Shadow tap stays on the * general path. */ if (enabled && !any_voice && !s_shadow_tap_on) { static int16_t s_voice_silence[2048 * 2]; diff --git a/runtime/tests/test_cdrom_resident_sector.c b/runtime/tests/test_cdrom_resident_sector.c new file mode 100644 index 00000000..47314bdb --- /dev/null +++ b/runtime/tests/test_cdrom_resident_sector.c @@ -0,0 +1,142 @@ +/* Actual controller integration: original FIFO/IRQ/DMA/snapshots, no guest + * callback or controller-state stubs. The backing disc alone is synthetic. */ +#define main fifo_baseline_main +#include "test_cdrom_sector_fifo.c" +#undef main + +static uint8_t resident_data[SECTOR_SIZE]; + +static void setup_resident(uint8_t mode) { + setup(); + mode_reg = mode; + reading = 0; + exec_command(0x06); + CHECK(reading && irq_flag == CDIRQ_ACK); + CHECK(cdrom_read(0x1F801801) == stat_reg); + ack(); + memset(resident_data, 100, sizeof resident_data); +} + +static void consume_resident(unsigned lba, uint8_t mode) { + int m, s, f; + CHECK(irq_flag == CDIRQ_DATA_READY); + CHECK(cdrom_read(0x1F801801) == stat_reg); + ack(); request(); + if (mode == 0xA0) { + lba_to_msf((int)lba, 150, &m, &s, &f); + CHECK(cdrom_dma_read() == (uint32_t)(bin_to_bcd(m) | + bin_to_bcd(s) << 8 | bin_to_bcd(f) << 16 | 2u << 24)); + CHECK(cdrom_dma_read() == 0x00080709u); + CHECK(cdrom_dma_read() == 0x00080709u); + } + drain(lba, 512); +} + +static void original_continuation_and_pause(void) { + for (unsigned k = 0; k < 2; ++k) { + uint8_t mode = k ? 0xA0 : 0x80; + setup_resident(mode); + const uint64_t fires = s_dataready_fires; + CHECK(cdrom_resident_feed_sector(100, resident_data)); + CHECK(reading && read_cmd == 0x06 && last_sector_lba == 100); + CHECK(msf_to_lba(read_min, read_sec, read_sect) == 101); + CHECK(s_dataready_fires == fires + 1); + CHECK(read_delay == sector_delay_cycles()); + CHECK(!cdrom_resident_feed_sector(101, resident_data)); + consume_resident(100, mode); + /* No host transfer state is required to continue through real timing. */ + sector(); + consume_resident(101, mode); + CHECK(msf_to_lba(read_min, read_sec, read_sect) == 102); + exec_command(0x09); + CHECK(!reading && irq_flag == CDIRQ_ACK && pending.pending); + (void)cdrom_read(0x1F801801); ack(); + psx_cycle_count += 10000000; + process_pending(0); + CHECK(irq_flag == CDIRQ_COMPLETE && !pending.pending); + } +} + +static void repeated_one_sector_and_snapshot(void) { + for (unsigned k = 0; k < 2; ++k) { + uint8_t mode = k ? 0xA0 : 0x80; + setup_resident(mode); + for (unsigned lba = 100; lba < 112; ++lba) { + memset(resident_data, lba, sizeof resident_data); + CHECK(cdrom_resident_feed_sector(lba, resident_data)); + const uint32_t size = cdrom_snapshot_bytes(); + uint8_t* wire = malloc(size); CHECK(wire); + cdrom_snapshot_write(wire); + consume_resident(lba, mode); + CHECK(cdrom_snapshot_read(wire, size)); + consume_resident(lba, mode); + free(wire); + CHECK(reading && msf_to_lba(read_min, read_sec, read_sect) == (int)lba + 1); + } + } +} + +static void decline_without_mutation(void) { + for (unsigned n = 0; n < 33; ++n) { + setup_resident(0xA0); + uint32_t lba = 100; + const uint8_t* data = resident_data; + switch (n) { + case 0: data = NULL; break; + case 1: lba = 99; break; + case 2: reading = 0; break; + case 3: read_cmd = 0x1B; break; + case 4: mode_reg = 0xE0; break; + case 5: xa_stream_active = 1; break; + case 6: cdda_playing = 1; break; + case 7: setloc_pending = 1; break; + case 8: irq_flag = CDIRQ_ACK; break; + case 9: pending.pending = 1; break; + case 10: queued_cmd.pending = 1; break; + case 11: pending_dataready = 1; break; + case 12: pending_present_due = psx_cycle_count + 10; break; + case 13: param_count = 1; break; + case 14: response_read = 0; response_count = 1; break; + case 15: test_dma_active = 1; break; + case 16: RB_.size = SECTOR_SIZE; RB_.pos = 4; break; + case 17: test_raw_fail = 1; break; + case 18: resident_data[42] ^= 1; break; + case 19: test_raw_bad_header = 1; break; + case 20: test_raw_bad_copy = 1; break; + case 21: test_raw_mode = 1; break; + case 22: test_submode = 0x0A; break; /* video */ + case 23: test_submode = 0x0C; break; /* audio */ + case 24: test_submode = 0x28; break; /* Form2 */ + case 25: test_submode = 0x48; break; /* realtime */ + case 26: test_submode = 0; break; + case 27: lba = 1000; read_sec = 15; read_sect = 25; break; + case 28: mode_reg = 0; break; + case 29: + case 30: + RB_.size = WHOLE_SECTOR_SIZE; RB_.pos = 12 + SECTOR_SIZE; + last_sector_mode = mode_reg; last_sector_have_raw = 1; + last_sector_lba = n == 29 ? 98 : 99; + if (n == 30) --RB_.pos; + break; + case 31: xa_data_end_pending = 1; break; + case 32: cdda_data_end_pending = 1; break; + } + const uint32_t size = cdrom_snapshot_bytes(); + uint8_t *before = malloc(size), *after = malloc(size); + CHECK(before && after); + cdrom_snapshot_write(before); + CHECK(!cdrom_resident_feed_sector(lba, data)); + cdrom_snapshot_write(after); + CHECK(!memcmp(before, after, size)); + free(before); free(after); + } +} + +int main(void) { + CHECK(fifo_baseline_main() == 0); + original_continuation_and_pause(); + repeated_one_sector_and_snapshot(); + decline_without_mutation(); + puts("PASS: resident single-sector FIFO/IRQ, 24 snapshot restores, original continuation/Pause, 33 atomic declines"); + return 0; +} diff --git a/runtime/tests/test_cdrom_sector_fifo.c b/runtime/tests/test_cdrom_sector_fifo.c index 28146182..70f954b3 100644 --- a/runtime/tests/test_cdrom_sector_fifo.c +++ b/runtime/tests/test_cdrom_sector_fifo.c @@ -5,24 +5,30 @@ uint32_t i_stat, g_debug_current_func_addr, g_debug_last_store_pc; uint64_t s_frame_count, psx_cycle_count; static int test_dma_active; -static int test_raw_fail, test_copy_result = 1, test_copy_calls; -static uint8_t test_submode = 8; +static int test_raw_fail, test_raw_bad_header, test_raw_bad_copy; +static int test_copy_result = 1, test_copy_calls; +static uint8_t test_raw_mode = 2, test_submode = 8; int dma_cdrom_transfer_active(void) { return test_dma_active; } void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)detail; i_stat |= bit; } int iso_read_raw_sector(void* h, uint32_t lba, uint8_t* out, int size) { - int m,s,f; + int m, s, f; (void)h; if (test_raw_fail) return 0; - memset(out, 0, size); lba_to_msf((int)lba,150,&m,&s,&f); - out[12]=bin_to_bcd(m);out[13]=bin_to_bcd(s);out[14]=bin_to_bcd(f); - out[15]=2;out[16]=out[20]=9;out[17]=out[21]=7; + memset(out, 0, size); + lba_to_msf((int)lba, 150, &m, &s, &f); + out[12]=bin_to_bcd(m); out[13]=bin_to_bcd(s); + out[14]=bin_to_bcd(f) ^ (test_raw_bad_header ? 1u : 0u); + out[15]=test_raw_mode; out[16]=out[20]=9; out[17]=out[21]=7; out[18]=out[22]=test_submode; + if (test_raw_bad_copy) out[20]=1; memset(out+24, (unsigned char)lba, 2048); return 1; } int iso_read_sector(void* h,uint32_t lba,uint8_t* out,int size) {(void)h;memset(out,(unsigned char)lba,size);return 1;} static void setup(void) { test_dma_active = 0; - test_raw_fail=0;test_submode=8;test_copy_result=1;test_copy_calls=0; + test_raw_fail = test_raw_bad_header = test_raw_bad_copy = 0; + test_raw_mode = 2; test_submode = 8; + test_copy_result = 1; test_copy_calls = 0; psx_cycle_count = 1000000; cdrom_init("synthetic"); setloc_pending = 0; diff --git a/runtime/tests/test_spu_silent_fetch.c b/runtime/tests/test_spu_silent_fetch.c new file mode 100644 index 00000000..5cbf2c5a --- /dev/null +++ b/runtime/tests/test_spu_silent_fetch.c @@ -0,0 +1,118 @@ +/* Released hardware slots still fetch ADPCM and can deliver stream IRQs. + * Includes spu.c to verify both the mixer fast path and exact decoder state. */ +#include +#include +#include + +#include "../src/spu.c" + +/* Dependencies owned by the surrounding runtime are inert in this unit test. */ +uint64_t s_frame_count; + +uint64_t psx_get_cycle_count(void) { return 0; } + +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; +} + + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "line %d: %s\n", __LINE__, #x); exit(1); } } while (0) + +static void stage(unsigned index, uint16_t pitch, int active) { + SpuVoice *v = &voices[index]; + memset(v, 0, sizeof *v); + v->active = active; + v->env_level = active ? 0x6000u : 0; + v->adsr_phase = active ? ADSR_SUSTAIN : ADSR_RELEASE; + v->cur_addr = 0x2000; + v->repeat_addr = 0x2000; + v->sample_idx = SPU_BLOCK_SAMPLES; + spu_regs[index * 8u + 2u] = pitch; + for (uint32_t a = 0x2000; a < 0x2100; a += 16) { + spu_ram[a] = 0; + spu_ram[a + 1] = 0; + memset(spu_ram + a + 2, 0x11, 14); + } + spu_regs[reg_index(0x1F801DAA)] = 0xC040; + spu_regs[reg_index(0x1F801DA4)] = 0x2040 / 8; +} +static void silent_irq(int with_playing_voice) { + spu_init(); + stage(18, 0x0800, 0); + if (with_playing_voice) { + stage(0, 0x1000, 1); + spu_regs[0] = spu_regs[1] = 0x3FFF; + spu_regs[reg_index(0x1F801D80)] = 0x3FFF; + spu_regs[reg_index(0x1F801D82)] = 0x3FFF; + } + int16_t pcm[512 * 2]; + spu_render_block(pcm, 512); + CHECK(irq_flag && voices[18].cur_addr > 0x2040); + CHECK(!voices[18].active && !voices[18].env_level); + unsigned nonzero = 0; + for (unsigned i = 0; i < sizeof pcm / sizeof *pcm; ++i) nonzero += pcm[i] != 0; + CHECK(with_playing_voice ? nonzero > 0 : nonzero == 0); + SpuEvent events[64]; + uint32_t count = spu_event_get(events, 64), irq = 0; + for (uint32_t i = 0; i < count; ++i) + if (events[i].kind == SPU_EV_IRQ && events[i].addr == 0x2040) ++irq; + CHECK(irq == 1); +} +int main(void) { + silent_irq(0); /* exercises the previously all-silent fast path */ + silent_irq(1); /* a simultaneous audible slot must not hide the issue */ + spu_init(); stage(18, 0x0800, 1); + voices[18].env_level = 1; voices[18].adsr_phase = ADSR_RELEASE; + int16_t released[512 * 2]; spu_render_block(released, 512); + CHECK(irq_flag && !voices[18].active && !voices[18].env_level); + spu_init(); stage(18, 0, 0); + int16_t pcm[512 * 2]; spu_render_block(pcm, 512); + CHECK(!irq_flag && voices[18].cur_addr == 0x2010); + CHECK(voices[18].sample_idx == 0); /* zero pitch still holds */ + spu_init(); stage(18, 0x1000, 0); + spu_ram[0x2041] = 1; /* silent END still updates ENDX */ + spu_render_block(pcm, 512); + CHECK(endx_latch & (1u << 18)); + CHECK(!voices[18].active && !voices[18].env_level); + /* A private voice is explicitly stopped after release; it is not an + * emulated hardware fetch slot and must not dereference an expired bank. */ + voices[SPU_VOICE_COUNT].active = 0; + voices[SPU_VOICE_COUNT].cur_addr = 0x1234; + voices[SPU_VOICE_COUNT].sample_idx = SPU_BLOCK_SAMPLES; + CHECK(!voice_next_sample(SPU_VOICE_COUNT)); + CHECK(voices[SPU_VOICE_COUNT].cur_addr == 0x1234); + spu_init(); stage(18, 0x1000, 0); + spu_regs[reg_index(0x1F801DAA)] = 0xC000; /* silent clock runs with IRQ off */ + spu_render_block(pcm, 80); + CHECK(voices[18].cur_addr > 0x2000 && !irq_flag); + uint32_t next = voices[18].cur_addr; + spu_regs[reg_index(0x1F801DA4)] = (uint16_t)(next / 8); + spu_regs[reg_index(0x1F801DAA)] = 0xC040; + spu_render_block(pcm, 100); + CHECK(irq_flag); /* enabling IRQ sees the continuously advanced position */ + puts("spu_silent_fetch_test: pass"); + return 0; +}