Merge pull request #657 from RetroPortingToolKit/work/final-bout-cd-sector

fix(cdrom): preserve requested sector bytes across pending interrupts
This commit is contained in:
Matt Stanley
2026-10-14 11:20:09 -07:00
committed by GitHub
5 changed files with 311 additions and 95 deletions
+4
View File
@@ -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)
+2
View File
@@ -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.
*
+2 -1
View File
@@ -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);
+121 -94
View File
@@ -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). */
+182
View File
@@ -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;}