mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 18:52:11 -04:00
cdrom: flow-control accelerated reads -- never outrun the guest's ack
THE BUG, measured end to end. Legend of Mana's land-creation effect renders as hard-edged full-screen wedges because ONE sector of a multi-sector scene load never arrives: cd_read_log shows LBA 125111 -> 0x0E1F18 followed by LBA 125113 -> 0x0E2718 -- destinations one sector apart, LBAs two apart. Sector 125112, which the ISO shows holds the effect's 5-colour per-vertex palette (0x0888F8 = (248,136,8), 0xB0F8F8 = (248,248,176)), was skipped; sector 125113's raw file bytes landed at its address and were faithfully rendered as 216 distinct vertex colours. DuckStation places 125112 correctly and draws the clean bloom. The skip is manufactured by accelerated pacing. At authentic cadence the guest has a full sector period (~6.6 ms at 2x) to ack each data-ready INT1; the one-deep pend-then-lose below is Beetle-faithful, and a loss there means the game truly was too slow. With the speed divisor or bounded-instant mode active, sectors arrive in a fraction of that period, so a guest that is merely BUSY -- here, mid scene-load decompression -- gets a sector clobbered that hardware would have delivered. s_int1_lost counted it; the stream corrupted silently. Fix, class-level per RULE -1: when running FASTER than hardware, never overwrite a sector the guest has not consumed -- hold delivery, capped at the authentic sector period, then fall through to faithful clobber semantics. Accelerated mode is thus 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 and keep authentic timing. Warm routes keep their existing indefinite handshake. New counters accel_consumer_waits/_wait_cycles in cdrom_state: nonzero is healthy (the enhancement waited); int1_lost rising while the divisor != 1 is the regression tripwire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -178,6 +178,12 @@ 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. */
|
||||
uint64_t accel_consumer_waits;
|
||||
uint64_t accel_consumer_wait_cycles;
|
||||
uint8_t int1_pending_now;
|
||||
} CDROMDebugState;
|
||||
|
||||
|
||||
@@ -2391,6 +2391,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 +2447,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
|
||||
@@ -2445,6 +2504,8 @@ static void process_read_stream(uint32_t cycles) {
|
||||
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. */
|
||||
@@ -2827,6 +2888,8 @@ 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->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();
|
||||
|
||||
@@ -5591,6 +5591,7 @@ 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,"
|
||||
"\"filter_file\":%u,\"filter_channel\":%u,\"muted\":%u,"
|
||||
"\"seek_msf\":[%u,%u,%u],"
|
||||
"\"pending\":{\"cmd\":\"0x%02X\",\"active\":%d,\"delay\":%d,\"phase\":%d},"
|
||||
@@ -5609,6 +5610,8 @@ 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,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user