cdrom: a Read that continues the current stream must not restart the drive

Legend of Mana's land-creation effect rendered as hard-edged coloured
wedges instead of a soft bloom. The cause was in the CD-ROM controller,
not the renderer.

The game streams a file by issuing Setloc(next)+ReadN roughly 10,800
guest cycles after each sector arrives. Every ReadN called
start_read_stream(), which clears the sector buffer -- destroying the
sector that had already been delivered and announced but not yet drained
by the guest. Measured at the palette load:

  buffer      psx-runtime   DuckStation
  0x0E1F18      125111        125111
  0x0E2718      125113        125112   <- the effect's colour palette
  0x0DCF18      125113        125113

Sector 125112 carries the effect's 5-colour palette (0x0888F8 =
(248,136,8), 0xB0F8F8 = (248,248,176), verified against the disc image).
Destroyed before the guest could take it, its buffer received 125113's
raw file bytes instead, and the colour routine faithfully scaled those
into 216 distinct vertex colours -- the wedges. The table now holds 5.

DuckStation only calls BeginReading -- the function that clears the
buffers and re-arms the read-start latency -- when the request actually
moves the drive:

  if ((!setloc_pending || setloc_position.ToLBA() == GetNextSectorToBeRead())
      && (drive_state == Reading || ...))
    "Ignoring read command with pending/same setloc, already reading"
  else
    BeginReading();

A Read issued while the drive already streams the sector the pending
Setloc names is the game saying "keep going", not "start over". The
command is still ACKed with the same INT3; only the restart is skipped.

Two things fall out of it. The un-drained sector survives, so the game
receives the consecutive sectors it expects (109,110,111,112,113 rather
than 109,111,111,113,113). And each request stops paying
initial_read_delay_cycles() again: inter-sector gaps drop from ~456,000
cycles to 225,792 -- exactly the 2x cadence -- so this load takes 2 guest
frames instead of 13, matching DuckStation. int1_pended goes 10 -> 0.

Also included, developed alongside and verified in the same runs:

  - An 8-slot sector buffer ring, as the hardware has (nocash;
    DuckStation NUM_SECTOR_BUFFERS = 8). A delivered sector fills the
    NEXT slot and the INT1 that announces it latches the read slot, so a
    sector arriving mid-drain no longer clobbers the FIFO. The read
    pointer follows the slot each INT1 announces, captured when the INT1
    is RAISED: this drive free-runs, so latching "newest" at
    presentation time handed the guest a sector seven ahead of the one
    it asked for.
  - The seamless no-IRQ refill advances the read pointer, because it is
    a continuation of the drain already in progress. Without it a
    multi-sector DMA starves mid-transfer (measured: 1,128,960 starved
    drains).
  - A pended data-ready is released on a short schedule rather than
    inside the guest's ack store, matching DuckStation's
    QueueDeliverAsyncInterrupt.
  - Flow control for accelerated (non-1x) reads: never overwrite a
    sector the guest has not consumed while running faster than
    hardware, capped at the authentic sector period. Inert at authentic
    timing.

New cdrom_state counters ring_starved and ring_dropped, plus
cdrom_timing_dump for per-sector deadline/delivery records, are the
regression tripwires: ring_starved and int1_lost must stay 0.

Verified on Legend of Mana (SLUS-01013): the effect renders correctly,
the palette table holds 5 colour words, sectors arrive at the 2x
cadence, and no notification is lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Vanderveen
2026-08-24 12:19:56 -04:00
committed by Matthew Stanley
parent d1df20514f
commit ffa11fa2dc
3 changed files with 433 additions and 76 deletions
+31
View File
@@ -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;
+336 -74
View File
@@ -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);
+66 -2
View File
@@ -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 },