diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index cea4eb16..d8bc5da3 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -51,6 +51,22 @@ void cdrom_warm_route_stats_json(char* out, int cap); * Diagnostics only: recording never changes CD scheduling or delivery. */ void cdrom_timing_reset(void); void cdrom_timing_stats_json(char* out, int cap); +int cdrom_get_delivered_lba(void); + +/* Per-record view of the same ring, for localising a single lost/skipped + * sector rather than summarising thousands. */ +typedef struct CdTimingPub { + uint64_t seq; + uint64_t due_cycle; + uint64_t buffer_cycle; + uint64_t irq_arm_cycle; + uint64_t intc_cycle; + uint32_t frame; + int32_t lba; + uint8_t flags; /* CDT_* bits, mirrored in the JSON as named fields */ +} CdTimingPub; +uint64_t cdrom_timing_total(void); +int cdrom_timing_record(uint64_t seq, CdTimingPub* out); void debug_force_cd_reinsert(void); /* FMV auto-skip detection: cdrom_xa_stream_active() lets the frontend detect * that streaming XA (FMV/CDDA) is in progress. The skip itself is done by the @@ -178,6 +194,17 @@ typedef struct CDROMDebugState { /* One-deep pended data-ready INT1 accounting (Beetle SetAIP analog). */ uint64_t int1_pended; uint64_t int1_lost; + /* Accelerated-read flow control: holds where a faster-than-hardware + * sector was deferred rather than allowed to clobber an unconsumed one. + * Nonzero is healthy (the guest was busy and the enhancement waited); + * int1_lost rising while the speed divisor != 1 is the regression. */ + /* Sector-ring tripwires: starved = a drain found its slot exhausted + * while the writer had moved on (must stay 0); dropped = an unread slot + * was overwritten because the guest fell a full ring behind. */ + uint64_t ring_starved; + uint64_t ring_dropped; + uint64_t accel_consumer_waits; + uint64_t accel_consumer_wait_cycles; uint8_t int1_pending_now; } CDROMDebugState; @@ -229,6 +256,10 @@ typedef struct CDROMTraceEntry { typedef struct CDROMCommandHistoryEntry { uint64_t seq; + /* Guest cycle at which this command was issued/executed. Frame numbers + * cannot separate "the guest had not asked yet" from "the command was + * queued behind an unacked INT" -- both look like one frame. */ + uint64_t cycle; uint32_t frame; uint32_t func; uint32_t pc; diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 6e62504d..3b0ea8fe 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -146,10 +146,45 @@ 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 -static uint8_t sector_buffer[SECTOR_BUFFER_SIZE]; -static int sector_read_pos; -static int sector_available; -static int 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_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_write; /* slot most recently filled */ +static uint64_t s_ring_dropped; /* unread slot overwritten: guest a ring behind */ + + +#define RB_ (s_sector_ring[s_ring_read]) +static int rb_available(void) { return RB_.size > 0 && RB_.pos < RB_.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 + * slot exhausted while the writer had already moved on. */ +static uint64_t s_ring_starved; + +static void ring_note_starved(void) { + if (!rb_available() && s_ring_read != s_ring_write) s_ring_starved++; +} + static uint8_t last_sector_buffer[SECTOR_BUFFER_SIZE]; static int last_sector_lba; static int last_sector_size; @@ -170,6 +205,7 @@ static uint64_t command_history_seq; static uint8_t seek_min, seek_sec, seek_sect; static int s_setloc_lba = -1; /* LBA captured at SetLoc time */ static int setloc_seek_far; +static int setloc_pending; /* a Setloc has been issued and not consumed */ /* Read state */ static int reading; @@ -285,6 +321,13 @@ void cdrom_notify_game_started(void) { int cdrom_get_setloc_lba(void) { return s_setloc_lba; } +/* The sector most recently DELIVERED into the data buffer -- as opposed to + * the game's last SetLoc. During a streaming read with continuations these + * differ, and attributing a DMA to the SetLoc stamp alone is how a transfer + * carrying sector 125113's bytes was read as "the game asked for 125113" + * when the open question was precisely WHICH sector's data it drained. */ +int cdrom_get_delivered_lba(void) { return last_sector_lba; } + /* Frontend XA-stream probe (FMV auto-skip / turbo-load gating in main.cpp). */ int cdrom_xa_stream_active(void) { return xa_stream_active; } @@ -723,9 +766,9 @@ static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width e->param_count = (uint8_t)param_count; e->response_read = (uint8_t)response_read; e->response_count = (uint8_t)response_count; - e->sector_available = (uint8_t)sector_available; - e->sector_read_pos = sector_read_pos; - e->sector_size = sector_size; + e->sector_available = (uint8_t)rb_available(); + e->sector_read_pos = RB_.pos; + e->sector_size = RB_.size; e->pending_cmd = pending.cmd; e->pending_pending = (uint8_t)pending.pending; e->pending_delay = pending_rem_cycles(); @@ -740,6 +783,7 @@ static void record_command_history(uint8_t kind, uint8_t cmd, &command_history[command_history_seq % CDROM_COMMAND_HISTORY_CAP]; memset(e, 0, sizeof(*e)); e->seq = command_history_seq++; + e->cycle = psx_cycle_count; e->frame = (uint32_t)s_frame_count; e->func = g_debug_current_func_addr; e->pc = g_debug_last_store_pc; @@ -971,6 +1015,22 @@ void cdrom_timing_reset(void) { s_cd_probe_stop_count = s_cd_probe_stop_cycles = 0; } +uint64_t cdrom_timing_total(void) { return s_cd_timing_total; } + +int cdrom_timing_record(uint64_t seq, CdTimingPub *out) { + CdTimingRecord *r = cd_timing_lookup(seq); + if (!r || !out) return 0; + out->seq = r->seq; + out->due_cycle = r->due_cycle; + out->buffer_cycle = r->buffer_cycle; + out->irq_arm_cycle = r->irq_arm_cycle; + out->intc_cycle = r->intc_cycle; + out->frame = r->frame; + out->lba = r->lba; + out->flags = r->flags; + return 1; +} + void cdrom_timing_stats_json(char *out, int cap) { if (!out || cap <= 0) return; uint64_t start = s_cd_timing_reset_seq; @@ -1346,36 +1406,36 @@ static int read_sector_at(int min, int sec, int sect) { delivery.skip_reason = CDROM_SKIP_XA_AUDIO_REALTIME; } - memset(sector_buffer, 0, sizeof(sector_buffer)); - if (delivery.data_delivered && (mode_reg & 0x20)) { - if (have_raw) { - memcpy(sector_buffer, raw_data + WHOLE_SECTOR_OFFSET, WHOLE_SECTOR_SIZE); - sector_size = WHOLE_SECTOR_SIZE; - history_bytes = sector_buffer; - history_size = sector_size; + CdSectorBuf *wb = NULL; + if (delivery.data_delivered) { + int wi = (s_ring_write + 1) % CDROM_NUM_SECTOR_BUFFERS; + wb = &s_sector_ring[wi]; + if (wb->size > 0 && wb->pos < wb->size) s_ring_dropped++; + memset(wb->data, 0, sizeof(wb->data)); + if (mode_reg & 0x20) { + if (have_raw) { + memcpy(wb->data, raw_data + WHOLE_SECTOR_OFFSET, WHOLE_SECTOR_SIZE); + wb->size = WHOLE_SECTOR_SIZE; + } else { + wb->data[0] = bin_to_bcd(min); + wb->data[1] = bin_to_bcd(sec); + wb->data[2] = bin_to_bcd(sect); + wb->data[3] = 0x02; /* Mode 2 sector. */ + memcpy(wb->data + FALLBACK_SECTOR_HEADER_SIZE, user_data, SECTOR_SIZE); + wb->size = FALLBACK_WHOLE_SECTOR_SIZE; + } } else { - sector_buffer[0] = bin_to_bcd(min); - sector_buffer[1] = bin_to_bcd(sec); - sector_buffer[2] = bin_to_bcd(sect); - sector_buffer[3] = 0x02; /* Mode 2 sector. */ - memcpy(sector_buffer + FALLBACK_SECTOR_HEADER_SIZE, user_data, SECTOR_SIZE); - sector_size = FALLBACK_WHOLE_SECTOR_SIZE; - history_bytes = sector_buffer; - history_size = sector_size; + memcpy(wb->data, user_data, SECTOR_SIZE); + wb->size = SECTOR_SIZE; } - } else if (delivery.data_delivered) { - memcpy(sector_buffer, user_data, SECTOR_SIZE); - sector_size = SECTOR_SIZE; - history_bytes = sector_buffer; - history_size = sector_size; - } else { - sector_size = 0; + wb->pos = 0; + s_ring_write = wi; + history_bytes = wb->data; + history_size = wb->size; } - sector_read_pos = 0; - sector_available = delivery.data_delivered ? 1 : 0; if (delivery.data_delivered) { - memcpy(last_sector_buffer, sector_buffer, (size_t)sector_size); + memcpy(last_sector_buffer, wb->data, (size_t)wb->size); burst_note_sector(); if (s_warm_route_active) s_warm_route_sectors++; } else { @@ -1387,7 +1447,7 @@ static int read_sector_at(int min, int sec, int sect) { } } last_sector_lba = lba; - last_sector_size = delivery.data_delivered ? sector_size : history_size; + last_sector_size = delivery.data_delivered ? wb->size : history_size; last_sector_frame = (uint32_t)s_frame_count; last_sector_mode = mode_reg; last_sector_have_raw = (uint8_t)(have_raw ? 1 : 0); @@ -1417,9 +1477,12 @@ static void advance_msf(int* m, int* s, int* f) { } static void clear_sector_buffer(void) { - sector_read_pos = 0; - sector_size = 0; - sector_available = 0; + for (int i = 0; i < CDROM_NUM_SECTOR_BUFFERS; i++) { + s_sector_ring[i].size = 0; + s_sector_ring[i].pos = 0; + } + s_ring_read = 0; + s_ring_write = 0; request_reg &= (uint8_t)~CDROM_REQUEST_BFRD; } @@ -1433,6 +1496,24 @@ static void clear_sector_buffer(void) { * skipped" warning (counted, traced 'P'). */ 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 */ +/* Absolute cycle at which a pended data-ready may be PRESENTED, or 0. + * + * Presenting it synchronously inside the guest's ack write is what loses the + * sector. The ISR clears the interrupt and only then sets up its DMA; if the + * next INT1's response FIFO and read slot are installed in between, the + * transfer drains the WRONG sector. Measured: the guest acks sector 110's + * INT1, 111 is presented instantly, and 111 lands in the buffer meant for 110 + * -- which is why psx-runtime fills 109,111,111,113,113 where DuckStation + * fills 109,110,111,112,113. + * + * DuckStation guards the same window (QueueDeliverAsyncInterrupt): after an + * ack, diff since the last interrupt is 0, so it always schedules rather than + * delivering inline, "give it enough time to read the response out ... the + * real console does something similar anyway, the INT1 task won't run + * immediately after the INT3 is cleared." */ +#define CDROM_PEND_PRESENT_DELAY 500 +static uint64_t pending_present_due; static uint64_t s_int1_pended; /* INT1s that had to wait for ack */ static uint64_t s_int1_lost; /* pended INT1s replaced unseen */ @@ -1441,9 +1522,41 @@ static uint64_t s_int1_lost; /* pended INT1s replaced unseen */ static void cdrom_clear_pending_dataready(void) { pending_dataready = 0; pending_dataready_stat = 0; + pending_present_due = 0; s_cd_timing_pending_seq = UINT64_MAX; } +/* A Read issued while the drive is ALREADY streaming the very sector the + * pending Setloc names is not a new read -- it is the game saying "keep + * going". DuckStation's Read handler does exactly this (cdrom.cpp: "Ignoring + * read command with pending/same setloc, already reading"), and only calls + * BeginReading -- the thing that clears the sector buffers and re-arms the + * read-start latency -- when the request actually moves the drive. + * + * Restarting instead costs two things, both measured on the palette load: + * + * 1. The un-drained sector is DESTROYED. start_read_stream clears the whole + * ring, so sector 125110 -- delivered, announced, and not yet taken by + * the guest -- was wiped when the guest issued Setloc(125111)+ReadN + * ~10,800 cycles later. Its buffer then received 125111 instead, and the + * effect's palette (125112) was lost the same way. + * 2. Every request pays initial_read_delay_cycles() again: 451,584 of the + * 456,000-cycle gap before each individually requested sector. That is + * why this load takes 13 guest frames here and 2 on DuckStation. + * + * The drive is left running and the command is ACKed, exactly as when it is + * accepted -- the guest sees the same INT3, just without the restart. */ +static int read_continues_current_stream(void) { + if (!reading) return 0; + /* Next sector the stream will deliver. */ + int next_lba = msf_to_lba(read_min, read_sec, read_sect); + if (setloc_pending && s_setloc_lba != next_lba) return 0; + setloc_pending = 0; + response_push(stat_reg); + set_irq(CDIRQ_ACK); + return 1; +} + static void start_read_stream(uint8_t cmd) { cdda_playing = 0; cdda_track = 0; @@ -1458,6 +1571,7 @@ static void start_read_stream(uint8_t cmd) { xa_reset_decode(); spu_cd_audio_reset(); } + setloc_pending = 0; read_min = seek_min; read_sec = seek_sec; read_sect = seek_sect; @@ -1675,9 +1789,8 @@ static void process_cdda_stream(uint32_t cycles) { } static int data_fifo_ready(void) { - return (request_reg & CDROM_REQUEST_BFRD) && - sector_available && - sector_read_pos < sector_size; + if (request_reg & CDROM_REQUEST_BFRD) ring_note_starved(); + return (request_reg & CDROM_REQUEST_BFRD) && rb_available(); } static uint64_t s_dataready_fires; /* INT1 (data-ready) raised per streamed sector — FMV dispatch probe */ @@ -1689,15 +1802,42 @@ static int deliver_read_sector(void) { if (!delivered) return 0; response_clear(); response_push(stat_reg); + /* Delivered immediately: this INT1 announces the slot just filled. */ + s_ring_read = s_ring_write; 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) { 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. */ + s_ring_read = s_ring_write; + } return delivered; } @@ -1893,6 +2033,7 @@ static void exec_command(uint8_t cmd) { seek_sec = bcd_to_bin(param_fifo[1]); seek_sect = bcd_to_bin(param_fifo[2]); s_setloc_lba = msf_to_lba(seek_min, seek_sec, seek_sect); + setloc_pending = 1; setloc_seek_far = (abs(current_lba - s_setloc_lba) > 16) ? 1 : 0; warm_route_on_setloc(s_setloc_lba); } @@ -1906,6 +2047,7 @@ static void exec_command(uint8_t cmd) { set_irq(CDIRQ_ERROR); break; } + if (read_continues_current_stream()) break; /* ACKed inside */ start_read_stream(cmd); response_push(stat_reg); set_irq(CDIRQ_ACK); @@ -2216,6 +2358,7 @@ static void exec_command(uint8_t cmd) { set_irq(CDIRQ_ERROR); break; } + if (read_continues_current_stream()) break; /* ACKed inside */ start_read_stream(cmd); response_push(stat_reg); set_irq(CDIRQ_ACK); @@ -2292,6 +2435,7 @@ static void process_pending(uint32_t cycles) { advance_msf(&read_min, &read_sec, &read_sect); if (delivered) { response_push(stat_reg); + s_ring_read = s_ring_write; set_irq(CDIRQ_DATA_READY); fire_cdrom_irq(); } @@ -2329,6 +2473,7 @@ static void process_pending(uint32_t cycles) { case 0x16: /* SeekP complete */ stat_reg &= ~CDSTAT_SEEK; setloc_seek_far = 0; + setloc_pending = 0; response_push(stat_reg); set_irq(CDIRQ_COMPLETE); fire_cdrom_irq(); @@ -2391,6 +2536,53 @@ static int warm_route_consumer_blocked(void) { return 0; } +/* ---- Flow control for ACCELERATED data reads (speed divisor / instant) ---- + * + * At authentic cadence the guest is guaranteed a full sector period + * (~6.6 ms at 2x) to ack each data-ready INT1 before the next sector can + * clobber the buffer; hardware losing a sector there means the game really + * was too slow, and the one-deep pend-then-lose below is Beetle-faithful. + * Accelerated pacing destroys that guarantee: sectors arrive in a fraction + * of the period, so a guest that is merely BUSY (not slow) gets sectors + * overwritten that real hardware would have delivered. That is a silent + * stream corruption manufactured by the enhancement -- Legend of Mana's + * land-creation palette (one sector, LBA 125112, of a multi-sector scene + * load) was skipped exactly this way and the raw file bytes that landed in + * its place were rendered as vertex colours. + * + * Rule: run the disc as fast as configured, but never overwrite a sector + * the guest has not consumed while running FASTER than hardware. The wait + * is capped at the authentic sector period: if the guest still has not + * acked after the time real hardware would have given it, fall through to + * the faithful clobber semantics. Accelerated mode is therefore never + * slower than hardware and never lossier than hardware; loads stay + * near-instant while the guest keeps up and degrade toward guest speed + * when it does not. + * + * XA/streaming modes are excluded by the same gates as apply_read_speed: + * those paths run at authentic cadence anyway, and pausing fictional disc + * time is only sound when the cadence IS fictional. */ +static uint64_t s_accel_consumer_waits; +static uint64_t s_accel_consumer_wait_cycles; +static int s_accel_block_accum; + +static int authentic_sector_period(void) { + return (mode_reg & 0x80) ? (CDROM_SINGLE_SPEED_SECTOR_CYCLES / 2) + : CDROM_SINGLE_SPEED_SECTOR_CYCLES; +} + +static int accelerated_read_active(void) { + if (xa_stream_active || (mode_reg & 0x68u)) return 0; + return g_disc_speed_divisor != 1; /* 0 = instant, >1 = divided */ +} + +static int accelerated_consumer_blocked(void) { + if (!accelerated_read_active()) return 0; + if (pending_dataready) return 1; + if (irq_flag != 0 && !dma_cdrom_transfer_active()) return 1; + return 0; +} + static void process_read_stream(uint32_t cycles) { if (!reading) return; @@ -2400,6 +2592,18 @@ static void process_read_stream(uint32_t cycles) { return; } + if (accelerated_consumer_blocked() && + s_accel_block_accum < authentic_sector_period()) { + /* Hold the sector back rather than clobber it: see the flow-control + * comment above. The accumulator caps the hold at the authentic + * sector period, after which delivery proceeds with hardware + * semantics. */ + s_accel_block_accum += (int)cycles; + s_accel_consumer_waits++; + s_accel_consumer_wait_cycles += cycles; + return; + } + /* Disc time NEVER pauses while the drive reads (Beetle cdc.cpp * HandleSectorRead: the sector pipeline advances on cycle deadlines * regardless of INT ack state). XA-ADPCM realtime audio flows to the @@ -2413,8 +2617,8 @@ static void process_read_stream(uint32_t cycles) { uint64_t timing_seq = cd_timing_begin_sector( msf_to_lba(read_min, read_sec, read_sect)); if (irq_flag == 0) { - if (sector_available) { - trace_cdrom('O', 0, (uint32_t)sector_read_pos, 0); + if (rb_available()) { + trace_cdrom('O', 0, (uint32_t)RB_.pos, 0); } if (deliver_read_sector()) { cd_timing_flag(timing_seq, CDT_DATA); @@ -2441,10 +2645,16 @@ static void process_read_stream(uint32_t cycles) { } pending_dataready = 1; pending_dataready_stat = stat_reg; + /* Capture the slot NOW. By presentation time the drive will + * have moved on -- latching "newest" there is what handed the + * guest a sector seven ahead of the one it asked for. */ + pending_dataready_slot = s_ring_write; s_cd_timing_pending_seq = timing_seq; s_int1_pended++; } } + s_accel_block_accum = 0; /* the pipeline advanced; the next hold + * starts a fresh authentic-period budget */ read_delay += sector_delay_cycles(); /* Clamp pathological underflow to one sector period: never replay a * catch-up burst of missed sectors, never leave a huge negative debt. */ @@ -2460,11 +2670,13 @@ static void process_read_stream(uint32_t cycles) { * register clears). Called from the irq_flag ack write. */ static void present_pending_dataready(void) { if (!pending_dataready || irq_flag != 0) return; + pending_present_due = 0; uint64_t timing_seq = s_cd_timing_pending_seq; pending_dataready = 0; s_cd_timing_pending_seq = UINT64_MAX; response_clear(); response_push(pending_dataready_stat); + s_ring_read = pending_dataready_slot; /* the slot this INT1 announced */ set_irq(CDIRQ_DATA_READY); fire_cdrom_irq(); cd_timing_arm_irq(timing_seq); @@ -2474,7 +2686,9 @@ static void present_pending_dataready(void) { void cdrom_init(const char* cue_path) { memset(param_fifo, 0, sizeof(param_fifo)); memset(response_fifo, 0, sizeof(response_fifo)); - memset(sector_buffer, 0, sizeof(sector_buffer)); + memset(s_sector_ring, 0, sizeof(s_sector_ring)); + s_ring_read = 0; + s_ring_write = 0; memset(last_sector_buffer, 0, sizeof(last_sector_buffer)); /* Rematch re-calls cdrom_init; boot must see 1x until game entry again. */ @@ -2488,12 +2702,10 @@ void cdrom_init(const char* cue_path) { cdrom_irq_generation = 0; cdrom_intc_latched_generation = 0; cdrom_irq_present_due = 0; + pending_present_due = 0; param_count = 0; response_read = 0; response_count = 0; - sector_read_pos = 0; - sector_size = 0; - sector_available = 0; last_sector_lba = -1; last_sector_size = 0; last_sector_frame = 0; @@ -2594,10 +2806,7 @@ uint32_t cdrom_read(uint32_t addr) { case 0x1F801802: if (data_fifo_ready()) { - ret = sector_buffer[sector_read_pos++]; - if (sector_read_pos >= sector_size) { - sector_available = 0; - } + ret = RB_.data[RB_.pos++]; } break; @@ -2661,7 +2870,7 @@ void cdrom_write(uint32_t addr, uint32_t value) { } else if (index_reg == 0) { request_reg = val; if (!(request_reg & CDROM_REQUEST_BFRD)) { - sector_read_pos = 0; + RB_.pos = 0; } } else if (index_reg == 1) { /* Controller IRQ acknowledge. irq_flag is a single numeric response @@ -2678,10 +2887,14 @@ void cdrom_write(uint32_t addr, uint32_t value) { if (val & 0x40) { param_count = 0; } - /* A fully-acked INT presents any pended data-ready first - * (Beetle CheckAIP on IRQ-register clear); then a second-response - * that is already past due_cyc; a queued command waits behind. */ - present_pending_dataready(); + /* A fully-acked INT releases any pended data-ready -- but on a + * SCHEDULE, not inside this store. See CDROM_PEND_PRESENT_DELAY: + * installing the next response and read slot before the ISR has + * set up its DMA makes that DMA drain the wrong sector. Then a + * second-response already past due_cyc; a queued command waits + * behind. */ + if (pending_dataready && pending_present_due == 0) + pending_present_due = psx_cycle_count + CDROM_PEND_PRESENT_DELAY; process_pending(0); try_execute_queued_command(); } @@ -2718,6 +2931,11 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) { best = 0u; } } + /* Scheduled release of a pended data-ready. */ + if (pending_present_due != 0) { + uint32_t d = (uint32_t)cycles_until_due(pending_present_due); + if (d < best) best = d; + } /* Active sector read: next data-ready in read_delay cycles. */ if (reading && !warm_route_consumer_blocked() && read_delay > 0 && (uint32_t)read_delay < best) @@ -2733,6 +2951,13 @@ void cdrom_advance(uint32_t cycles) { refresh_cdrom_irq_line(); process_pending(cycles); try_execute_queued_command(); + /* Release a scheduled data-ready once its delay has elapsed and the guest + * has genuinely finished with the previous INT. */ + if (pending_present_due != 0 && psx_cycle_count >= pending_present_due) { + pending_present_due = 0; + if (pending_dataready && irq_flag == 0) + present_pending_dataready(); + } process_read_stream(cycles); process_cdda_stream(cycles); refresh_cdrom_irq_line(); @@ -2745,13 +2970,10 @@ void cdrom_tick(void) { uint32_t cdrom_dma_read(void) { uint32_t val = 0; int got = 0; - if ((request_reg & CDROM_REQUEST_BFRD) && sector_available && - sector_read_pos + 4 <= sector_size) { - memcpy(&val, sector_buffer + sector_read_pos, 4); - sector_read_pos += 4; - if (sector_read_pos >= sector_size) { - sector_available = 0; - } + if ((request_reg & CDROM_REQUEST_BFRD) && rb_available() && + RB_.pos + 4 <= RB_.size) { + memcpy(&val, RB_.data + RB_.pos, 4); + RB_.pos += 4; got = 1; } /* Per-word DMA data reads flood the CD trace ring (hundreds per sector) and @@ -2770,13 +2992,13 @@ uint32_t cdrom_dma_read(void) { } int cdrom_dma_ready(void) { - return (request_reg & CDROM_REQUEST_BFRD) && - sector_available && - (sector_read_pos + 4 <= sector_size); + if (request_reg & CDROM_REQUEST_BFRD) ring_note_starved(); + return (request_reg & CDROM_REQUEST_BFRD) && rb_available() && + (RB_.pos + 4 <= RB_.size); } uint32_t cdrom_dma_sector_word_count(void) { - int size = sector_size; + int size = RB_.size; /* CD DMA BCR low half zero means transfer one sector-sized payload. * If DMA is armed just before the next sector becomes available, fall @@ -2809,9 +3031,9 @@ void cdrom_debug_snapshot(CDROMDebugState* out) { out->param_count = param_count; out->response_read = response_read; out->response_count = response_count; - out->sector_read_pos = sector_read_pos; - out->sector_available = sector_available; - out->sector_size = sector_size; + out->sector_read_pos = RB_.pos; + out->sector_available = rb_available(); + out->sector_size = RB_.size; out->reading = reading; out->read_min = read_min; out->read_sec = read_sec; @@ -2827,6 +3049,10 @@ void cdrom_debug_snapshot(CDROMDebugState* out) { out->read_hold_events = s_read_hold_events; out->int1_pended = s_int1_pended; out->int1_lost = s_int1_lost; + out->ring_starved = s_ring_starved; + out->ring_dropped = s_ring_dropped; + out->accel_consumer_waits = s_accel_consumer_waits; + out->accel_consumer_wait_cycles = s_accel_consumer_wait_cycles; out->int1_pending_now = pending_dataready; out->pending_pending = pending.pending; out->pending_delay = pending_rem_cycles(); @@ -2874,9 +3100,9 @@ uint32_t cdrom_debug_copy_last_sector(uint32_t offset, uint32_t len, CDROMSectorDebugState* state) { if (state) { memset(state, 0, sizeof(*state)); - state->current_available = sector_available; - state->current_read_pos = sector_read_pos; - state->current_size = sector_size; + state->current_available = rb_available(); + state->current_read_pos = RB_.pos; + state->current_size = RB_.size; state->last_lba = last_sector_lba; state->last_size = last_sector_size; state->last_frame = last_sector_frame; @@ -2996,7 +3222,23 @@ 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); - WB(sector_buffer); WI(sector_read_pos); WI(sector_available); WI(sector_size); + /* 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); + WI(RB_.pos); + WI(rb_available()); + WI(RB_.size); WB(last_sector_buffer); WI(last_sector_lba); WI(last_sector_size); WB(last_valid_subq); WI(last_valid_subq_available); /* last_sector_frame is host s_frame_count — zero on the wire so netplay @@ -3044,13 +3286,33 @@ 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); - RB(sector_buffer); RI(sector_read_pos); RI(sector_available); RI(sector_size); + /* 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; + 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); RU(last_sector_frame); R8(last_sector_mode); R8(last_sector_have_raw); R8(last_sector_raw_mode); R8(last_sector_xa_file); R8(last_sector_xa_channel); R8(last_sector_xa_submode); R8(last_sector_xa_coding); - R8(seek_min); R8(seek_sec); R8(seek_sect); RI(s_setloc_lba); RI(setloc_seek_far); + R8(seek_min); R8(seek_sec); R8(seek_sect); RI(s_setloc_lba); RI(setloc_seek_far); setloc_pending = 0; RI(reading); RI(read_min); RI(read_sec); RI(read_sect); R8(mode_reg); R8(read_cmd); RI(read_delay); R8(filter_file); R8(filter_channel); R8(cd_muted); RI(cdda_playing); RI(cdda_track); RU(cdda_lba); RI(cdda_delay); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index f2fd4794..0f81399d 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -5533,6 +5533,8 @@ static void handle_cdrom_state(int id, const char *json) "\"read_cmd\":\"0x%02X\",\"read_delay\":%d," "\"read_hold_cycles\":%llu,\"read_hold_events\":%llu," "\"int1_pended\":%llu,\"int1_lost\":%llu,\"int1_pending_now\":%u," + "\"accel_consumer_waits\":%llu,\"accel_consumer_wait_cycles\":%llu," + "\"ring_starved\":%llu,\"ring_dropped\":%llu," "\"filter_file\":%u,\"filter_channel\":%u,\"muted\":%u," "\"seek_msf\":[%u,%u,%u]," "\"pending\":{\"cmd\":\"0x%02X\",\"active\":%d,\"delay\":%d,\"phase\":%d}," @@ -5551,6 +5553,10 @@ static void handle_cdrom_state(int id, const char *json) (unsigned long long)s.int1_pended, (unsigned long long)s.int1_lost, s.int1_pending_now, + (unsigned long long)s.accel_consumer_waits, + (unsigned long long)s.accel_consumer_wait_cycles, + (unsigned long long)s.ring_starved, + (unsigned long long)s.ring_dropped, s.filter_file, s.filter_channel, s.muted, s.seek_min, s.seek_sec, s.seek_sect, s.pending_cmd, s.pending_pending, s.pending_delay, @@ -5726,10 +5732,11 @@ static void handle_cdrom_command_history(int id, const char *json) if (frame_hi >= 0 && (int)e->frame > frame_hi) continue; pos += snprintf(buf + pos, bufsz - pos, - "%s{\"seq\":%llu,\"frame\":%u,\"kind\":\"%s\"," + "%s{\"seq\":%llu,\"cycle\":%llu,\"frame\":%u,\"kind\":\"%s\"," "\"cmd\":\"0x%02X\",\"param_count\":%u,\"params\":[", emitted ? "," : "", - (unsigned long long)e->seq, e->frame, + (unsigned long long)e->seq, + (unsigned long long)e->cycle, e->frame, cdrom_command_kind_name(e->kind), e->cmd, e->param_count); for (uint8_t i = 0; i < e->param_count && i < 16 && pos < bufsz - 96; i++) { @@ -11553,6 +11560,12 @@ static void handle_cd_read_log(int id, const char *json) uint32_t *dest, uint32_t *size); int tail = json_get_int(json, "tail", 256); + /* Optional LBA window + output cap, matching the oracle's cd_read_log so + * one call shape returns the same span from both emulators. */ + int lba_lo = json_get_int(json, "lba_lo", -1); + int lba_hi = json_get_int(json, "lba_hi", -1); + int max_entries = json_get_int(json, "max_entries", 65536); + int emitted_rows = 0; uint32_t total = cd_dma_log_get_total(); uint32_t cap = 65536; uint32_t avail = total < cap ? total : cap; @@ -11567,6 +11580,10 @@ static void handle_cd_read_log(int id, const char *json) int lba; uint32_t dest, size; cd_dma_log_get_entry(i, &lba, &dest, &size); if (lba < 0) continue; + if (emitted_rows >= max_entries) break; + if (lba_lo >= 0 && lba < lba_lo) continue; + if (lba_hi >= 0 && lba > lba_hi) continue; + emitted_rows++; send_fmt("%s{\"lba\":%d,\"dest\":\"0x%08X\",\"size\":%u}", first ? "" : ",", lba, dest, size); first = 0; @@ -11700,6 +11717,52 @@ static void handle_cdrom_timing(int id, const char *json) send_fmt("{\"id\":%d,\"ok\":true,%s}\n", id, stats); } +/* cdrom_timing_dump: per-sector records from the L1.5 timing ring. + * Parameters: tail (default 1024), lba_lo / lba_hi (optional filter). + * One row per physical sector deadline: when it was due, when it landed in + * the buffer, when its INT1 was armed and presented to INTC, and the + * data/dma/pended/lost flags -- enough to see a single skipped sector and + * whether the guest or the drive was late around it. */ +static void handle_cdrom_timing_dump(int id, const char *json) +{ + int tail = json_get_int(json, "tail", 1024); + if (tail < 1) tail = 1; + if (tail > 4096) tail = 4096; + int lba_lo = json_get_int(json, "lba_lo", -1); + int lba_hi = json_get_int(json, "lba_hi", -1); + + uint64_t total = cdrom_timing_total(); + uint64_t start = total > (uint64_t)tail ? total - (uint64_t)tail : 0; + send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"entries\":[", + id, (unsigned long long)total); + int first = 1; + for (uint64_t seq = start; seq < total; seq++) { + CdTimingPub r; + if (!cdrom_timing_record(seq, &r)) continue; + if (lba_lo >= 0 && r.lba < lba_lo) continue; + if (lba_hi >= 0 && r.lba > lba_hi) continue; + send_fmt("%s{\"seq\":%llu,\"lba\":%d,\"frame\":%u," + "\"due\":%llu,\"buffer\":%llu,\"irq_arm\":%llu," + "\"intc\":%llu,\"data\":%u,\"dma\":%u," + "\"pended\":%u,\"lost\":%u,\"irq_armed\":%u," + "\"intc_seen\":%u}", + first ? "" : ",", + (unsigned long long)r.seq, r.lba, r.frame, + (unsigned long long)r.due_cycle, + (unsigned long long)r.buffer_cycle, + (unsigned long long)r.irq_arm_cycle, + (unsigned long long)r.intc_cycle, + (r.flags & 0x01u) ? 1 : 0, + (r.flags & 0x02u) ? 1 : 0, + (r.flags & 0x04u) ? 1 : 0, + (r.flags & 0x08u) ? 1 : 0, + (r.flags & 0x10u) ? 1 : 0, + (r.flags & 0x20u) ? 1 : 0); + first = 0; + } + send_fmt("]}\n"); +} + /* autocompile_status: variant-capture automation state — autocapture * enable/trigger counters + the background compile's state and output tail * (in-memory ring; no log files). */ @@ -13470,6 +13533,7 @@ static const CmdEntry s_commands[] = { { "wtrace_reset", handle_wtrace_clear }, { "wtrace_ranges", handle_wtrace_ranges }, { "wtrace_dump", handle_wtrace_dump }, + { "cdrom_timing_dump", handle_cdrom_timing_dump }, { "wtrace_stats", handle_wtrace_stats }, { "wtrace_boot_dump", handle_wtrace_boot_dump }, { "wtrace_boot_summary", handle_wtrace_boot_summary },