diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index cea4eb16..bbef8e07 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -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; diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 8ea24a1f..5ff1a349 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -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(); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 1e1903d9..eb9d6a61 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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,