diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 517121c6..28494162 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1395,6 +1395,10 @@ if(BUILD_TESTING) target_include_directories(cdrom_irq_mask_test PRIVATE src) add_test(NAME cdrom_irq_mask_test COMMAND cdrom_irq_mask_test) + add_executable(cdrom_sector_fifo_test tests/test_cdrom_sector_fifo.c) + 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_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) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index 5a33b389..44caebef 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -8,6 +8,8 @@ extern "C" { #endif void cdrom_init(const char* cue_path); +uint32_t cdrom_snapshot_bytes(void); +int cdrom_snapshot_validate(const uint8_t *p, uint32_t len); /* Did cdrom_init() actually mount an image? 0 = the drive is empty. * diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index 9bf4abdd..f458066c 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -78,6 +78,7 @@ extern int spu_snapshot_len_ok(uint32_t len); extern uint8_t* spu_get_ram_ptr(void); extern uint32_t spu_get_ram_bytes(void); extern uint32_t cdrom_snapshot_bytes(void); +extern int cdrom_snapshot_validate(const uint8_t* p, uint32_t len); extern void cdrom_snapshot_write(uint8_t* p); extern int cdrom_snapshot_read(const uint8_t* p, uint32_t len); extern uint32_t dma_snapshot_bytes(void); @@ -861,7 +862,7 @@ static int validate_section(uint32_t tag, const uint8_t* p, uint32_t len) { case BS_SEC_VRAM: return len == VRAM_SIZE; case BS_SEC_SPU: return spu_snapshot_len_ok(len); case BS_SEC_SPURAM: return len == spu_get_ram_bytes(); - case BS_SEC_CDROM: return len == cdrom_snapshot_bytes(); + case BS_SEC_CDROM: return cdrom_snapshot_validate(p, len); case BS_SEC_DMA: return len == dma_snapshot_bytes(); case BS_SEC_SIO: return sio_snapshot_validate(p, len); case BS_SEC_MDEC: return mdec_snapshot_validate(p, len); diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index a0ed6d2d..b54bec74 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -153,35 +153,33 @@ static int response_count; #define FALLBACK_SECTOR_HEADER_SIZE 12 #define FALLBACK_WHOLE_SECTOR_SIZE (FALLBACK_SECTOR_HEADER_SIZE + SECTOR_SIZE) #define SECTOR_BUFFER_SIZE WHOLE_SECTOR_SIZE -/* ---- sector buffer ring ------------------------------------------------ - * Hardware has eight rotating sector buffers. One buffer means a sector - * arriving mid-drain clobbers the FIFO under the guest, and that is - * measured, not assumed: at the artifact screen psx-runtime drains sector - * 125113 into BOTH 0x0E2718 and 0x0DCF18 in one frame, while DuckStation - * drains 125112 then 125113 into the same two buffers. Sector 125112 -- the - * effect's colour palette -- is read here ([D], not lost) and then - * overwritten by 125113 before the guest takes it, so raw file bytes end up - * rendered as vertex colours. - * - * THE READ POINTER IS THE PART THAT MATTERS. An earlier attempt latched - * read = write when an INT1 was PRESENTED, and regressed badly (70 lost - * sectors, every Setloc retried): this drive free-runs, so by presentation - * time `write` has advanced past the sector that INT1 was raised for, and - * the guest was handed a sector seven ahead of the one it asked for. Each - * INT1 therefore records the slot it announces AT RAISE TIME, and - * presentation moves the read pointer to exactly that slot -- never to - * "newest". */ +#define CDROM_REQUEST_BFRD 0x80u +/* The decoder rotates through eight sector buffers. IRQ presentation selects + * the announced slot; BFRD copies that slot into an independent data FIFO. + * Keeping these separate prevents a later IRQ from replacing an active drain. */ #define CDROM_NUM_SECTOR_BUFFERS 8 typedef struct { uint8_t data[SECTOR_BUFFER_SIZE]; int size; int pos; } CdSectorBuf; static CdSectorBuf s_sector_ring[CDROM_NUM_SECTOR_BUFFERS]; -static int s_ring_read; /* slot the guest is draining */ +static int s_ring_read; /* slot announced by the current data-ready IRQ */ static int s_ring_write; /* slot most recently filled */ +static CdSectorBuf s_data_fifo; /* BFRD-latched bytes; IRQs cannot replace them */ static uint64_t s_ring_dropped; /* unread slot overwritten: guest a ring behind */ -#define RB_ (s_sector_ring[s_ring_read]) +#define RB_ (s_data_fifo) static int rb_available(void) { return RB_.size > 0 && RB_.pos < RB_.size; } +/* An explicit BFRD request may reload the retained announced sector after + * software clears the FIFO. Automatic refills consume each new slot once. */ +static void load_data_fifo(int requested) { + CdSectorBuf *sector = &s_sector_ring[s_ring_read]; + if (!(request_reg & CDROM_REQUEST_BFRD) || rb_available() || + sector->size <= 0 || (!requested && sector->pos >= sector->size)) return; + s_data_fifo = *sector; + s_data_fifo.pos = 0; + sector->pos = sector->size; +} + /* Regression tripwire. The ring once stranded a sector in a slot nothing * could reach, starving over a million drains; this stays as an O(1) check * so that can never come back silently. Non-zero means a drain found its @@ -275,7 +273,6 @@ static uint8_t cd_muted; #define CDROM_SKIP_NONE 0 #define CDROM_SKIP_XA_AUDIO_REALTIME 1 -#define CDROM_REQUEST_BFRD 0x80u typedef struct CDROMSectorDelivery { uint8_t raw_mode; @@ -1549,6 +1546,7 @@ static void advance_msf(int* m, int* s, int* f) { } static void clear_sector_buffer(void) { + memset(&s_data_fifo, 0, sizeof(s_data_fifo)); for (int i = 0; i < CDROM_NUM_SECTOR_BUFFERS; i++) { s_sector_ring[i].size = 0; s_sector_ring[i].pos = 0; @@ -1558,14 +1556,9 @@ static void clear_sector_buffer(void) { request_reg &= (uint8_t)~CDROM_REQUEST_BFRD; } -/* One-deep asynchronous data-ready notification, mirroring Beetle - * PS_CDC::SetAIP/CheckAIP (cdc.cpp:829,816): a data sector that comes due - * while the guest still has an unacked controller INT does NOT stop disc - * time — the sector buffer is overwritten on schedule (hardware clobbers - * the FIFO the same way) and its INT1 pends here until the ack clears - * irq_flag. If ANOTHER data sector lands while one is still pending, the - * old notification is lost exactly like Beetle's "Previous notification - * skipped" warning (counted, traced 'P'). */ +/* A sector can arrive while the preceding controller INT is unacknowledged. + * Disc time advances and its notification waits here. A later arrival can + * replace that pending notification, but cannot overwrite the latched FIFO. */ static uint8_t pending_dataready; /* 0/1: INT1 awaiting presentation */ static uint8_t pending_dataready_stat; /* stat_reg snapshot at pend time */ static int pending_dataready_slot; /* ring slot THIS INT1 announces */ @@ -1893,39 +1886,27 @@ static int deliver_read_sector(void) { response_push(stat_reg); /* Delivered immediately: this INT1 announces the slot just filled. */ s_ring_read = s_ring_write; + load_data_fifo(0); set_irq(CDIRQ_DATA_READY); fire_cdrom_irq(); s_dataready_fires++; return 1; } -/* Both callers advance the read pointer, and that is DELIBERATE. - * - * Splitting them -- so the "guest has not acked" path parked the sector - * without advancing -- looked obviously correct (why redirect a drain the - * guest is still working through?) and regressed hard: int1_lost went 0 -> 70 - * and the game retried every Setloc. Without the advance, sectors pile up - * behind an unacked INT, each new one replaces the pended notification, and - * the guest is never told. The reasoning was sound and the measurement said - * otherwise; it was committed without a verification run, which is how it - * survived long enough to confuse three later experiments. - * - * Do not re-split this without a cd_verify run showing int1_lost stays 0. */ -static int deliver_read_sector_without_irq(void) { +/* 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 + * here substitutes the following file's sector for the requested final one. + * present_pending_dataready selects the pending slot when its INT1 is exposed. + * Keep DMA continuation separate; delaying that refill previously caused + * starvation and lost notifications in multi-sector transfers. */ +static int deliver_read_sector_without_irq(int dma_continuation) { int delivered = read_sector_at(read_min, read_sec, read_sect); advance_msf(&read_min, &read_sec, &read_sect); - if (delivered) { - /* Seamless refill for an in-flight multi-sector DMA: no INT1 is - * raised because this is a CONTINUATION of the drain already in - * progress, not a new notification. The read pointer must therefore - * follow it -- with one buffer this refill landed in the very buffer - * being drained, and the ring reproduces that only by advancing. - * - * Measured: leaving the pointer behind starved 1,128,960 drains and - * stranded exactly 70 refills -- the same 70 that showed up as - * int1_lost, with every Setloc retried. This one line is the whole - * difference between the ring regressing and the ring working. */ + if (delivered && dma_continuation) { + /* Multi-sector DMA needs a refill without a second INT1. */ s_ring_read = s_ring_write; + load_data_fifo(0); } return delivered; } @@ -2551,6 +2532,7 @@ static void process_pending(uint32_t cycles) { if (delivered) { response_push(stat_reg); s_ring_read = s_ring_write; + load_data_fifo(0); set_irq(CDIRQ_DATA_READY); fire_cdrom_irq(); } @@ -2745,13 +2727,13 @@ static void process_read_stream(uint32_t cycles) { * asserted: refill the buffer so the DMA keeps draining, no new * INT (the historical shape; games start the DMA inside the * data-ready callback before acking). */ - if (deliver_read_sector_without_irq()) + if (deliver_read_sector_without_irq(1)) cd_timing_flag(timing_seq, CDT_DATA | CDT_DMA); } else { /* Guest hasn't acked the previous INT yet. Read the sector on * schedule (XA audio + buffer overwrite happen inside), and * pend its data-ready INT1 one deep. */ - int delivered = deliver_read_sector_without_irq(); + int delivered = deliver_read_sector_without_irq(0); if (delivered) { cd_timing_flag(timing_seq, CDT_DATA | CDT_PENDED); if (pending_dataready) { @@ -2793,6 +2775,7 @@ static void present_pending_dataready(void) { response_clear(); response_push(pending_dataready_stat); s_ring_read = pending_dataready_slot; /* the slot this INT1 announced */ + load_data_fifo(0); set_irq(CDIRQ_DATA_READY); fire_cdrom_irq(); cd_timing_arm_irq(timing_seq); @@ -2803,6 +2786,7 @@ void cdrom_init(const char* cue_path) { memset(param_fifo, 0, sizeof(param_fifo)); memset(response_fifo, 0, sizeof(response_fifo)); memset(s_sector_ring, 0, sizeof(s_sector_ring)); + memset(&s_data_fifo, 0, sizeof(s_data_fifo)); s_ring_read = 0; s_ring_write = 0; memset(last_sector_buffer, 0, sizeof(last_sector_buffer)); @@ -2990,6 +2974,9 @@ void cdrom_write(uint32_t addr, uint32_t value) { request_reg = val; if (!(request_reg & CDROM_REQUEST_BFRD)) { RB_.pos = 0; + RB_.size = 0; + } else { + load_data_fifo(1); } } else if (index_reg == 1) { /* Controller IRQ acknowledge. irq_flag is a single numeric response @@ -3115,6 +3102,9 @@ uint32_t cdrom_dma_read(void) { } int cdrom_dma_ready(void) { + /* A decoder refill can arrive before the previous FIFO is fully drained. + * Preserve those bytes, then latch the announced continuation on demand. */ + if (dma_cdrom_transfer_active()) load_data_fifo(0); if (request_reg & CDROM_REQUEST_BFRD) ring_note_starved(); return (request_reg & CDROM_REQUEST_BFRD) && rb_available() && (RB_.pos + 4 <= RB_.size); @@ -3334,7 +3324,7 @@ int cdrom_savestate_boost_vblanks_remaining(void) { /* LE field wire — PendingCmd/QueuedCmd have host padding. */ #include "pst_wire.h" -static int cdrom_snap_emit(PstW *w) { +static int cdrom_snap_emit(PstW *w, int extended) { #define W8(f) do { if (!pst_w_u8(w, (uint8_t)(f))) return 0; } while (0) #define WI(f) do { if (!pst_w_i32(w, (int32_t)(f))) return 0; } while (0) #define WU(f) do { if (!pst_w_u32(w, (uint32_t)(f))) return 0; } while (0) @@ -3345,20 +3335,10 @@ static int cdrom_snap_emit(PstW *w) { WU(cdrom_intc_latched_generation); WI(irq_present_rem_cycles()); WB(param_fifo); WI(param_count); WB(response_fifo); WI(response_read); WI(response_count); - /* WIRE FORMAT IS UNCHANGED, deliberately: only the slot the guest is - * draining goes out, in the exact layout the single-buffer version used - * (bytes, read position, available flag, size). - * - * Serialising all eight slots grew the section, so every previously saved - * state failed its size check -- and boot_state applies sections as it - * goes with no rollback, so RAM/CPU/GPU were already overwritten by the - * time the CD section was rejected. The load reported failure and left a - * half-restored machine that ran to the frame and froze. - * - * The other slots hold sectors the drive has read ahead; dropping them - * costs a re-read the game already knows how to ask for, which is what - * the single-buffer version effectively did across a load anyway. */ - WB(s_sector_ring[s_ring_read].data); + /* Keep the legacy single-buffer prefix readable by the new loader. + * New states append the decoder ring and notification indices, so a + * snapshot between BFRD and DMA retains both the FIFO and pending data. */ + WB(s_data_fifo.data); WI(RB_.pos); WI(rb_available()); WI(RB_.size); @@ -3384,6 +3364,16 @@ static int cdrom_snap_emit(PstW *w) { W8(pending.cmd); WI(pending.pending); WI(pending_rem_cycles()); WI(pending.phase); W8(queued_cmd.cmd); WB(queued_cmd.params); WI(queued_cmd.param_count); WI(queued_cmd.pending); W8(pending_dataready); W8(pending_dataready_stat); + if (extended) { + WU(0x32464443u); /* CDF2 */ + WI(s_ring_read); WI(s_ring_write); WI(pending_dataready_slot); + WI(pending_present_due ? cycles_until_due(pending_present_due) + 1 : 0); + for (int slot = 0; slot < CDROM_NUM_SECTOR_BUFFERS; slot++) { + WB(s_sector_ring[slot].data); + WI(s_sector_ring[slot].pos); WI(s_sector_ring[slot].size); + } + } + #undef W8 #undef WI #undef WU @@ -3392,7 +3382,7 @@ static int cdrom_snap_emit(PstW *w) { return 1; } -static int cdrom_snap_parse(PstR *r) { +static int cdrom_snap_parse(PstR *r, int extended) { uint8_t b; int32_t i; uint32_t u; @@ -3409,26 +3399,20 @@ static int cdrom_snap_parse(PstR *r) { RU(cdrom_intc_latched_generation); RI(present_rem); RB(param_fifo); RI(param_count); RB(response_fifo); RI(response_read); RI(response_count); - /* Restore into slot 0 and start the ring clean. See the emitter: the wire - * format is the single-buffer one, so states written before the ring - * existed still load. */ - RB(s_sector_ring[0].data); - RI(s_sector_ring[0].pos); - { int avail; RI(avail); (void)avail; } /* derived from pos < size now */ - RI(s_sector_ring[0].size); - for (int slot = 1; slot < CDROM_NUM_SECTOR_BUFFERS; slot++) { - s_sector_ring[slot].size = 0; - s_sector_ring[slot].pos = 0; - } - s_ring_read = 0; - s_ring_write = 0; - pending_dataready_slot = 0; + RB(s_data_fifo.data); + RI(s_data_fifo.pos); + { int avail; RI(avail); (void)avail; } + RI(s_data_fifo.size); + if (s_data_fifo.size < 0 || s_data_fifo.size > SECTOR_BUFFER_SIZE) + s_data_fifo.size = 0; + if (s_data_fifo.pos < 0 || s_data_fifo.pos > s_data_fifo.size) + s_data_fifo.pos = 0; + memset(s_sector_ring, 0, sizeof(s_sector_ring)); + /* Legacy states stored only one sector. Reconstruct that announced + * sector; new states restore the exact ring in the extension below. */ + s_sector_ring[0] = s_data_fifo; + s_ring_read = s_ring_write = pending_dataready_slot = 0; pending_present_due = 0; - /* A size off the wire indexes this buffer: clamp before anything uses it. */ - if (s_sector_ring[0].size < 0 || s_sector_ring[0].size > SECTOR_BUFFER_SIZE) - s_sector_ring[0].size = 0; - if (s_sector_ring[0].pos < 0 || s_sector_ring[0].pos > s_sector_ring[0].size) - s_sector_ring[0].pos = 0; RB(last_sector_buffer); RI(last_sector_lba); RI(last_sector_size); RB(last_valid_subq); RI(last_valid_subq_available); @@ -3448,6 +3432,17 @@ static int cdrom_snap_parse(PstR *r) { R8(pending.cmd); RI(pending.pending); RI(pending_rem); RI(pending.phase); R8(queued_cmd.cmd); RB(queued_cmd.params); RI(queued_cmd.param_count); RI(queued_cmd.pending); R8(pending_dataready); R8(pending_dataready_stat); + if (extended) { + RU(u); /* extension was validated before mutating controller state */ + RI(s_ring_read); RI(s_ring_write); RI(pending_dataready_slot); + RI(i); + pending_present_due = i > 0 ? psx_cycle_count + (uint32_t)(i - 1) : 0; + for (int slot = 0; slot < CDROM_NUM_SECTOR_BUFFERS; slot++) { + RB(s_sector_ring[slot].data); + RI(s_sector_ring[slot].pos); RI(s_sector_ring[slot].size); + } + } + #undef R8 #undef RI #undef RU @@ -3461,23 +3456,55 @@ static int cdrom_snap_parse(PstR *r) { return 1; } +static uint32_t cdrom_snapshot_legacy_bytes(void) { + PstW w; + pst_w_init(&w, NULL, 0); + (void)cdrom_snap_emit(&w, 0); + return (uint32_t)w.written; +} + +int cdrom_snapshot_validate(const uint8_t *p, uint32_t len) { + const uint32_t legacy = cdrom_snapshot_legacy_bytes(); + if (!p) return 0; + if (len == legacy) return 1; + if (len != cdrom_snapshot_bytes()) return 0; + PstR r; + uint32_t magic; + int32_t index, pos, size, delay; + pst_r_init(&r, p + legacy, len - legacy); + if (!pst_r_u32(&r, &magic) || magic != 0x32464443u) return 0; + for (int n = 0; n < 3; n++) { + if (!pst_r_i32(&r, &index) || index < 0 || index >= CDROM_NUM_SECTOR_BUFFERS) + return 0; + } + if (!pst_r_i32(&r, &delay) || delay < 0 || + delay > CDROM_PEND_PRESENT_DELAY + 1) return 0; + for (int slot = 0; slot < CDROM_NUM_SECTOR_BUFFERS; slot++) { + if (!pst_r_bytes(&r, NULL, SECTOR_BUFFER_SIZE) || + !pst_r_i32(&r, &pos) || !pst_r_i32(&r, &size) || + size < 0 || size > SECTOR_BUFFER_SIZE || pos < 0 || pos > size) + return 0; + } + return r.p == r.end; +} + uint32_t cdrom_snapshot_bytes(void) { PstW w; pst_w_init(&w, NULL, 0); - (void)cdrom_snap_emit(&w); + (void)cdrom_snap_emit(&w, 1); return (uint32_t)w.written; } void cdrom_snapshot_write(uint8_t *p) { PstW w; uint32_t n = cdrom_snapshot_bytes(); pst_w_init(&w, p, n); - (void)cdrom_snap_emit(&w); + (void)cdrom_snap_emit(&w, 1); } int cdrom_snapshot_read(const uint8_t *p, uint32_t len) { PstR r; - if (len != cdrom_snapshot_bytes()) return 0; + if (!cdrom_snapshot_validate(p, len)) return 0; pst_r_init(&r, p, len); - if (!cdrom_snap_parse(&r)) + if (!cdrom_snap_parse(&r, len != cdrom_snapshot_legacy_bytes())) return 0; /* Absolute host deadlines are not on the wire — rebuild from restored * relative read_delay (psx_cycle_count is resynced by the load caller). */ diff --git a/runtime/tests/test_cdrom_sector_fifo.c b/runtime/tests/test_cdrom_sector_fifo.c new file mode 100644 index 00000000..05a1e821 --- /dev/null +++ b/runtime/tests/test_cdrom_sector_fifo.c @@ -0,0 +1,182 @@ +/* Synthetic-sector integration tests for the actual CD controller. */ +#include "../src/cdrom.c" + +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); exit(1); } } while (0) +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; +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) { + (void)h; memset(out, 0, size); out[15]=2; out[18]=out[22]=8; + 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; + psx_cycle_count = 1000000; + cdrom_init("synthetic"); + mode_reg = 0x80; + reading = 1; + read_min = 0; read_sec = 3; read_sect = 25; /* LBA 100 */ +} +static void sector(void) { + psx_cycle_count += 225792; + read_delay = 0; + process_read_stream(0); +} +static void ack(void) { + cdrom_write(0x1f801800, 1); + cdrom_write(0x1f801803, 7); + cdrom_write(0x1f801800, 0); +} +static void request(void) { + cdrom_write(0x1f801800, 0); + cdrom_write(0x1f801803, 0); + cdrom_write(0x1f801803, 0x80); +} +static void drain(unsigned lba, int words) { + for (int n = 0; n < words; n++) { + CHECK(cdrom_dma_ready()); + CHECK(cdrom_dma_read() == lba * 0x01010101u); + } +} +static void delayed_notification(void) { + setup(); sector(); sector(); /* 100 announced, 101 pending */ + CHECK(pending_dataready && last_sector_lba == 101); + ack(); request(); + psx_cycle_count += CDROM_PEND_PRESENT_DELAY; + cdrom_advance(0); /* 101 becomes visible between BFRD and DMA */ + CHECK(irq_flag == CDIRQ_DATA_READY && !pending_dataready); + drain(100, 512); + CHECK(!cdrom_dma_ready()); + ack(); request(); + drain(101, 512); + CHECK(s_int1_lost == 0); +} +static void dma_continuation(void) { + setup(); sector(); request(); drain(100, 512); + test_dma_active = 1; + sector(); drain(101, 512); + sector(); drain(102, 512); + CHECK(!pending_dataready); + CHECK(s_int1_lost == 0); + test_dma_active = 0; +} +static void dma_arrival_mid_drain(void) { + setup(); sector(); request(); + test_dma_active = 1; + drain(100, 17); + sector(); /* Decoder advances while the old FIFO still has unread bytes. */ + drain(100, 495); + drain(101, 512); + CHECK(!cdrom_dma_ready()); + CHECK(!pending_dataready && s_int1_lost == 0); + test_dma_active = 0; +} +static void request_rearms_retained_sector(void) { + setup(); sector(); request(); drain(100, 512); ack(); + sector(); /* BFRD remains asserted and preloads 101. */ + request(); /* BIOS clears BFRD before requesting that same sector. */ + drain(101, 512); + CHECK(!cdrom_dma_ready()); +} +static void snapshot_pending_deadline(void) { + setup(); sector(); request(); sector(); ack(); + psx_cycle_count += 123; + const uint32_t size = cdrom_snapshot_bytes(); + uint8_t *wire = malloc(size); + CHECK(wire); + cdrom_snapshot_write(wire); + setup(); + psx_cycle_count = 0; /* Restore at a different cycle origin. */ + CHECK(cdrom_snapshot_read(wire, size)); + CHECK(pending_present_due == CDROM_PEND_PRESENT_DELAY - 123); + psx_cycle_count = pending_present_due - 1; + cdrom_advance(0); + CHECK(irq_flag == 0 && pending_dataready); + psx_cycle_count++; + cdrom_advance(0); + CHECK(irq_flag == CDIRQ_DATA_READY && !pending_dataready); + drain(100, 512); + request(); drain(101, 512); + free(wire); +} +static void snapshot_between_request_and_dma(void) { + setup(); sector(); request(); drain(100, 11); sector(); + const uint32_t size = cdrom_snapshot_bytes(); + uint8_t *wire = malloc(size), *after = malloc(size); + CHECK(wire && after); + cdrom_snapshot_write(wire); + CHECK(cdrom_snapshot_validate(wire, size)); + setup(); + CHECK(cdrom_snapshot_read(wire, size)); + ack(); + psx_cycle_count += CDROM_PEND_PRESENT_DELAY; + cdrom_advance(0); + drain(100, 501); + ack(); request(); drain(101, 512); + + /* Malformed ring indices must be rejected before controller mutation. */ + cdrom_snapshot_write(after); + const uint32_t legacy = cdrom_snapshot_legacy_bytes(); + wire[legacy + 4] = 0xff; + CHECK(!cdrom_snapshot_validate(wire, size)); + CHECK(!cdrom_snapshot_read(wire, size)); + cdrom_snapshot_write(wire); + CHECK(!memcmp(wire, after, size)); + + CHECK(!cdrom_snapshot_validate(NULL, size)); + CHECK(!cdrom_snapshot_validate(after, size - 1)); + memcpy(wire, after, size); + memset(wire + legacy + 16, 0x7f, 4); /* Impossible pending delay. */ + CHECK(!cdrom_snapshot_read(wire, size)); + cdrom_snapshot_write(wire); + CHECK(!memcmp(wire, after, size)); + + /* Old states retain their original wire prefix and readable payload. */ + setup(); sector(); request(); drain(100, 7); + PstW writer; + pst_w_init(&writer, wire, size); + CHECK(cdrom_snap_emit(&writer, 0)); + CHECK(writer.written == legacy); + setup(); + CHECK(cdrom_snapshot_validate(wire, legacy)); + CHECK(cdrom_snapshot_read(wire, legacy)); + drain(100, 505); + free(wire); free(after); +} +int main(void) { + delayed_notification(); + dma_continuation(); + dma_arrival_mid_drain(); + request_rearms_retained_sector(); + snapshot_between_request_and_dma(); + snapshot_pending_deadline(); + puts("PASS: CD FIFO latching, pending IRQ, DMA continuation, and state compatibility"); + return 0; +} + +void spu_cd_audio_reset(void) {} +void spu_cd_audio_push(const int16_t* stereo,int frames) {(void)stereo;(void)frames;} +void* iso_open(const char* p) {(void)p;return (void*)1;} +void iso_close(void* h) {(void)h;} +int iso_has_subq_replacements(void* h) {(void)h;return 0;} +int iso_read_subq(void* h,uint32_t lba,uint8_t* b,int n,int* v) {(void)h;(void)lba;(void)b;(void)n;*v=0;return 0;} +int iso_read_cdda_sector(void* h,uint32_t lba,uint8_t* b,int n) {(void)h;(void)lba;memset(b,0,n);return 0;} +uint32_t iso_cdda_track_start_lba(void* h,int track) {(void)h;(void)track;return 0;} +uint32_t iso_cdda_track_pregap_lba(void* h,int track) {(void)h;(void)track;return 0;} +int iso_cdda_track_count(void* h) {(void)h;return 1;} +uint32_t iso_cdda_sector_count(void* h) {(void)h;return 1000;} +int iso_cdda_track_is_audio(void* h,int track) {(void)h;(void)track;return 0;} +uint32_t iso_sector_count(void* h) {(void)h;return 1000;} +int iso_track_count(void* h) {(void)h;return 1;} +void event_ring_record_aux(uint16_t k,uint8_t d,uint32_t a) {(void)k;(void)d;(void)a;} +void event_ring_record(uint16_t k,uint8_t d) {(void)k;(void)d;} +void savestate_set_disc_scope(int n) {(void)n;} +int psx_netplay_cd_bisect_active(void) {return 0;} +uint32_t psx_netplay_sim_tick(void) {return 0;} +uint32_t interrupts_get_cycles_since_vblank(void) {return 0;} +int psx_netplay_is_resimulating(void) {return 0;} +void audio_trace_event(uint16_t k,uint32_t a,uint32_t b) {(void)k;(void)a;(void)b;}