mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Merge Tron Bonne resident-sector adapter and validation
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+27
-10
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user