Merge Tron Bonne resident-sector adapter and validation

This commit is contained in:
Matthew Stanley
2026-10-25 12:55:31 -07:00
10 changed files with 438 additions and 21 deletions
+35
View File
@@ -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.
+8
View File
@@ -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)
+6
View File
@@ -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
+12
View File
@@ -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`.
+72 -4
View File
@@ -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
+5
View File
@@ -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, &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) {
+27 -10
View File
@@ -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 (idx<SPU_VOICE_COUNT?(non_mask & (1u << idx)):
if (!audible) {
/* Silent hardware slots still clock below, without interpolation. */
} else if (idx<SPU_VOICE_COUNT?(non_mask & (1u << idx)):
(private_modes[idx-SPU_VOICE_COUNT]&1u)) {
raw_s = (int16_t)noise_lfsr;
} else {
@@ -953,7 +967,7 @@ static int16_t voice_next_sample(int idx) {
* advances. The optional shadow keeps its own cubic, block-clamped window;
* the canonical hardware path above independently uses the preceding
* block's tail for Gaussian interpolation. */
if (s_shadow_tap_on && s_shadow_tap_frame < SPU_SHADOW_TAP_FRAMES) {
if (audible && s_shadow_tap_on && s_shadow_tap_frame < SPU_SHADOW_TAP_FRAMES) {
SpuShadowVoiceTap *t =
&s_shadow_tap[s_shadow_tap_frame].voice[idx];
int si = v->sample_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];
+142
View File
@@ -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;
}
+13 -7
View File
@@ -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;
+118
View File
@@ -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 <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#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;
}