From 619bb418cf57f602cf816e82c7eb0b116695e638 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 25 Oct 2026 11:33:40 -0700 Subject: [PATCH] Add guarded single-sector resident CD FIFO delivery --- docs/LOAD_TIME_ZERO.md | 35 +++++ runtime/CMakeLists.txt | 4 + runtime/include/cdrom.h | 6 + runtime/include/mod_resident.h | 12 ++ runtime/src/cdrom.c | 76 ++++++++++- runtime/src/mod_guest_services.c | 6 + runtime/tests/test_cdrom_resident_sector.c | 142 +++++++++++++++++++++ runtime/tests/test_cdrom_sector_fifo.c | 13 +- 8 files changed, 289 insertions(+), 5 deletions(-) create mode 100644 runtime/tests/test_cdrom_resident_sector.c diff --git a/docs/LOAD_TIME_ZERO.md b/docs/LOAD_TIME_ZERO.md index f52c477d..ff07c197 100644 --- a/docs/LOAD_TIME_ZERO.md +++ b/docs/LOAD_TIME_ZERO.md @@ -386,3 +386,38 @@ rejected on paper. `spike/load-shards` (TombaRecomp). Recreate worktrees before E4. - `cdrom_bursts` ring (`cdrom.c:311-360`) already measures load durations — E0's window boundary source. + + +## Incremental resident sectors + +`psx_mod_resident_cd_sector(lba, user_data)` admits exactly one verified data +sector through the actual controller FIFO and IRQ path. It keeps ReadN active, +advances the drive cursor by one sector, and restores ordinary timing for the +next sector. The original guest Ready callback, header/payload DMA, resource +consumers and final Pause acknowledgement still run. It never writes guest +RAM, calls a guest callback or retains a host pointer/transfer cursor. + +The title must prove its stock file/span, original code and live callback, +and that the next destination is free of pending consumers. A zero ring +occupancy byte alone is insufficient when an SDK queue retains its source +pointer. Check both the software GPU queue and active GPU/SPU DMA before each +admission. The shared helper checks CD ownership only; it does not know the +title's upload queues or grant a whole-file transfer. + +Admission requires the original ReadN ACK and response to be fully consumed, +mode 0x80 or 0xA0, the exact next drive LBA, and no pending command, IRQ, CD DMA +or unread payload. Mode 0xA0 may retain only the already-consumed sector's ECC +tail. The helper witnesses the real raw sector's mode, submode, duplicate +subheader and BCD address and compares every user byte with resident data. +XA, FMV, Form2, realtime, changed bytes and busy ownership decline without +controller mutation. This first version still reads raw sector metadata from +the mounted image for verification; it does not claim removal of all host I/O. + +No extra snapshot fields are needed: every accepted effect lives in the +ordinary controller buffers, cursor, status and IRQ state. The real-controller +`cdrom_resident_sector_test` covers both modes, repeated one-sector admission, +24 current-format snapshot restores, subsequent original-stream delivery and +real Pause completion, plus 33 byte-for-byte atomic decline cases. Existing +FIFO and IRQ-mask tests also pass. These tests qualify the helper's controller +contract, not a title adapter or load-time improvement. Tron consumer integration +and measured runtime outcomes remain pending under beads-eio.3.412/eio.27.3. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 0d34ccbe..84407538 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -1624,6 +1624,10 @@ if(BUILD_TESTING) 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_resident_sector_test tests/test_cdrom_resident_sector.c) + target_include_directories(cdrom_resident_sector_test PRIVATE include src) + add_test(NAME cdrom_resident_sector_test COMMAND cdrom_resident_sector_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) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index 44caebef..1159e560 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -33,6 +33,12 @@ void cdrom_notify_game_started(void); * ring buffer to correlate each sector transfer with its disc position. */ int cdrom_get_setloc_lba(void); +/* Admit one verified resident data sector into the real ReadN FIFO/IRQ path. + * Requires an ACKed stream at lba, no pending controller/DMA/payload owner, + * and matching pure-data raw bytes. Never stops the stream or calls a guest + * callback. The title must separately prove its destination/upload ownership. */ +int cdrom_resident_feed_sector(uint32_t lba, const uint8_t* user_data); + /* 'instant' per-frame sector-IRQ budget (step 3). Clamped to [1, 4096]; * the per-sector period additionally floors at CDROM_MIN_DELAY. Writers: * game.toml [runtime] instant_max_per_frame, the cdrom_instant_rate TCP diff --git a/runtime/include/mod_resident.h b/runtime/include/mod_resident.h index 1a861d9e..f8e5290c 100644 --- a/runtime/include/mod_resident.h +++ b/runtime/include/mod_resident.h @@ -48,6 +48,18 @@ typedef struct PSXResidentFile { * the original file; NULL = not catalogued */ } PSXResidentFile; +/* Deliver exactly one stock data sector through the actual controller FIFO, + * IRQ and original guest Ready/DMA path. The ReadN stream remains active and + * its next sector keeps ordinary timing. Busy/changed/media sectors decline + * without mutation. No host pointer or transfer cursor is retained; ordinary + * controller snapshots contain every accepted effect. + * + * Trusted title code must prove the file/span/code/callback and a free next + * destination, including queued GPU source pointers and active GPU/SPU DMA. + * The controller checks CD ownership only. Do not use for XA/FMV or to bypass + * original consumers, publish completion, or replace a final Pause ack. */ +int psx_mod_resident_cd_sector(uint32_t lba, const void* user_data); + typedef struct PSXResidentSink PSXResidentSink; /* Store one derived blob (decoded asset, table, ...) for source file `file`. diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 1d104243..aaf24392 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -1434,7 +1434,8 @@ static int maybe_deliver_xa_audio(const uint8_t* raw_data, int lba, return 1; } -static int read_sector_at(int min, int sec, int sect) { +static int read_sector_at_with_raw(int min, int sec, int sect, + const uint8_t* supplied_raw) { int lba = msf_to_lba(min, sec, sect); uint8_t user_data[SECTOR_SIZE]; uint8_t raw_data[RAW_SECTOR_SIZE]; @@ -1443,7 +1444,12 @@ static int read_sector_at(int min, int sec, int sect) { const uint8_t *history_bytes = user_data; int history_size = SECTOR_SIZE; - if (iso_handle) { + if (supplied_raw) { + memcpy(raw_data, supplied_raw, sizeof raw_data); + memcpy(user_data, raw_data + RAW_USER_DATA_OFFSET, sizeof user_data); + have_raw = 1; + if (subq_replacements_active) update_last_valid_subq((uint32_t)lba); + } else if (iso_handle) { have_raw = iso_read_raw_sector(iso_handle, lba, raw_data, RAW_SECTOR_SIZE); if (have_raw) { memcpy(user_data, raw_data + RAW_USER_DATA_OFFSET, SECTOR_SIZE); @@ -1540,6 +1546,10 @@ static int read_sector_at(int min, int sec, int sect) { return delivery.data_delivered ? 1 : 0; } +static int read_sector_at(int min, int sec, int sect) { + return read_sector_at_with_raw(min, sec, sect, NULL); +} + static void advance_msf(int* m, int* s, int* f) { (*f)++; if (*f >= 75) { *f = 0; (*s)++; } @@ -1877,8 +1887,9 @@ static int data_fifo_ready(void) { static uint64_t s_dataready_fires; /* INT1 (data-ready) raised per streamed sector — FMV dispatch probe */ uint64_t cdrom_get_dataready_fires(void) { return s_dataready_fires; } -static int deliver_read_sector(void) { - int delivered = read_sector_at(read_min, read_sec, read_sect); +static int deliver_read_sector_with_raw(const uint8_t* supplied_raw) { + int delivered = read_sector_at_with_raw(read_min, read_sec, read_sect, + supplied_raw); advance_msf(&read_min, &read_sec, &read_sect); if (xa_data_end_pending) { xa_data_end_pending = 0; @@ -1900,6 +1911,10 @@ static int deliver_read_sector(void) { return 1; } +static int deliver_read_sector(void) { + return deliver_read_sector_with_raw(NULL); +} + /* 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 @@ -3271,6 +3286,59 @@ int cdrom_data_read_active(void) { return reading && !xa_stream_active; } +int cdrom_resident_feed_sector(uint32_t lba, const uint8_t* user_data) { + uint8_t raw[RAW_SECTOR_SIZE]; + uint64_t timing_seq; + int min, sec, frame; + /* The title owns the verified file, callback and destination consumer. + * This helper owns one real controller delivery, never guest RAM or a + * host-side continuation. A declined call leaves the controller intact. */ + if (!user_data || !has_disc() || lba >= iso_sector_count(iso_handle) || + lba > 0x7FFFFF00u || !reading || read_cmd != 0x06 || + (mode_reg != 0x80u && mode_reg != 0xA0u) || + xa_stream_active || xa_data_end_pending || cdda_playing || + cdda_data_end_pending || setloc_pending || + msf_to_lba(read_min, read_sec, read_sect) != (int)lba || + irq_flag || pending.pending || queued_cmd.pending || + pending_dataready || pending_present_due || param_count || + response_read < response_count || dma_cdrom_transfer_active()) + return 0; + /* PsyQ reads 12 header bytes and 2048 user bytes, retaining 280 ECC + * bytes in whole-sector mode. That tail is not another owner's payload. + * A partial header/payload or any other unread FIFO must not be replaced. */ + if (rb_available() && + !(mode_reg == 0xA0u && RB_.size == WHOLE_SECTOR_SIZE && + RB_.pos == 12 + SECTOR_SIZE && last_sector_mode == mode_reg && + last_sector_have_raw && last_sector_lba >= 0 && + (uint32_t)last_sector_lba + 1u == lba)) + return 0; + if (!iso_read_raw_sector(iso_handle, lba, raw, sizeof raw) || + raw[15] != CDROM_SECTOR_MODE2 || + memcmp(raw + 16, raw + 20, 4) || !(raw[18] & 0x08u) || + (raw[18] & 0x66u) || + memcmp(raw + RAW_USER_DATA_OFFSET, user_data, SECTOR_SIZE)) + return 0; + lba_to_msf((int)lba, 150, &min, &sec, &frame); + if (raw[12] != bin_to_bcd(min) || raw[13] != bin_to_bcd(sec) || + raw[14] != bin_to_bcd(frame)) + return 0; + + /* Keep the stock header/subheader/ECC and supply the verified resident + * payload. The ordinary FIFO, status response, IRQ presentation and DMA + * paths handle the sector; the guest still executes its original Ready. + * There is no saved host pointer or multi-sector grant to invalidate. */ + memcpy(raw + RAW_USER_DATA_OFFSET, user_data, SECTOR_SIZE); + timing_seq = cd_timing_begin_sector((int)lba); + (void)deliver_read_sector_with_raw(raw); + cd_timing_flag(timing_seq, CDT_DATA); + cd_timing_arm_irq(timing_seq); + s_accel_block_accum = 0; + read_delay = sector_delay_cycles(); + s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay; + trace_cdrom('H', lba, 1, 0); + return 1; +} + /* Savestate post-load: authentic CD second-response delays (ReadTOC ~30M * cycles, Init ~1.1M, far seeks, etc.) leave the restored frame on screen * for up to ~1s+ of wall time. Clamp those timers so the next cdrom_advance diff --git a/runtime/src/mod_guest_services.c b/runtime/src/mod_guest_services.c index a15282bf..f9dfd847 100644 --- a/runtime/src/mod_guest_services.c +++ b/runtime/src/mod_guest_services.c @@ -3,6 +3,7 @@ * the header for the contract. */ #include "mod_plugins.h" #include "cpu_state.h" +#include "cdrom.h" #include "dma.h" #include "gpu.h" #include "interrupts.h" @@ -15,6 +16,11 @@ * guest-mutating service refuses then (docs/NETPLAY.md). */ int psx_mod_host_output_scope(void); +int psx_mod_resident_cd_sector(uint32_t lba, const void* user_data) { + if (psx_mod_host_output_scope()) return 0; + return cdrom_resident_feed_sector(lba, (const uint8_t*)user_data); +} + uint32_t psx_mod_call_guest(struct CPUState* cpu, uint32_t function, uint32_t return_address, uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3) { diff --git a/runtime/tests/test_cdrom_resident_sector.c b/runtime/tests/test_cdrom_resident_sector.c new file mode 100644 index 00000000..199ff254 --- /dev/null +++ b/runtime/tests/test_cdrom_resident_sector.c @@ -0,0 +1,142 @@ +/* Actual controller integration: original FIFO/IRQ/DMA/snapshots, no guest + * callback or controller-state stubs. The backing disc alone is synthetic. */ +#define main fifo_baseline_main +#include "test_cdrom_sector_fifo.c" +#undef main + +static uint8_t resident_data[SECTOR_SIZE]; + +static void setup_resident(uint8_t mode) { + setup(); + mode_reg = mode; + reading = 0; + exec_command(0x06); + CHECK(reading && irq_flag == CDIRQ_ACK); + CHECK(cdrom_read(0x1F801801) == stat_reg); + ack(); + memset(resident_data, 100, sizeof resident_data); +} + +static void consume_resident(unsigned lba, uint8_t mode) { + int m, s, f; + CHECK(irq_flag == CDIRQ_DATA_READY); + CHECK(cdrom_read(0x1F801801) == stat_reg); + ack(); request(); + if (mode == 0xA0) { + lba_to_msf((int)lba, 150, &m, &s, &f); + CHECK(cdrom_dma_read() == (uint32_t)(bin_to_bcd(m) | + bin_to_bcd(s) << 8 | bin_to_bcd(f) << 16 | 2u << 24)); + CHECK(cdrom_dma_read() == 0x00080000u); + CHECK(cdrom_dma_read() == 0x00080000u); + } + drain(lba, 512); +} + +static void original_continuation_and_pause(void) { + for (unsigned k = 0; k < 2; ++k) { + uint8_t mode = k ? 0xA0 : 0x80; + setup_resident(mode); + const uint64_t fires = s_dataready_fires; + CHECK(cdrom_resident_feed_sector(100, resident_data)); + CHECK(reading && read_cmd == 0x06 && last_sector_lba == 100); + CHECK(msf_to_lba(read_min, read_sec, read_sect) == 101); + CHECK(s_dataready_fires == fires + 1); + CHECK(read_delay == sector_delay_cycles()); + CHECK(!cdrom_resident_feed_sector(101, resident_data)); + consume_resident(100, mode); + /* No host transfer state is required to continue through real timing. */ + sector(); + consume_resident(101, mode); + CHECK(msf_to_lba(read_min, read_sec, read_sect) == 102); + exec_command(0x09); + CHECK(!reading && irq_flag == CDIRQ_ACK && pending.pending); + (void)cdrom_read(0x1F801801); ack(); + psx_cycle_count += 10000000; + process_pending(0); + CHECK(irq_flag == CDIRQ_COMPLETE && !pending.pending); + } +} + +static void repeated_one_sector_and_snapshot(void) { + for (unsigned k = 0; k < 2; ++k) { + uint8_t mode = k ? 0xA0 : 0x80; + setup_resident(mode); + for (unsigned lba = 100; lba < 112; ++lba) { + memset(resident_data, lba, sizeof resident_data); + CHECK(cdrom_resident_feed_sector(lba, resident_data)); + const uint32_t size = cdrom_snapshot_bytes(); + uint8_t* wire = malloc(size); CHECK(wire); + cdrom_snapshot_write(wire); + consume_resident(lba, mode); + CHECK(cdrom_snapshot_read(wire, size)); + consume_resident(lba, mode); + free(wire); + CHECK(reading && msf_to_lba(read_min, read_sec, read_sect) == (int)lba + 1); + } + } +} + +static void decline_without_mutation(void) { + for (unsigned n = 0; n < 33; ++n) { + setup_resident(0xA0); + uint32_t lba = 100; + const uint8_t* data = resident_data; + switch (n) { + case 0: data = NULL; break; + case 1: lba = 99; break; + case 2: reading = 0; break; + case 3: read_cmd = 0x1B; break; + case 4: mode_reg = 0xE0; break; + case 5: xa_stream_active = 1; break; + case 6: cdda_playing = 1; break; + case 7: setloc_pending = 1; break; + case 8: irq_flag = CDIRQ_ACK; break; + case 9: pending.pending = 1; break; + case 10: queued_cmd.pending = 1; break; + case 11: pending_dataready = 1; break; + case 12: pending_present_due = psx_cycle_count + 10; break; + case 13: param_count = 1; break; + case 14: response_read = 0; response_count = 1; break; + case 15: test_dma_active = 1; break; + case 16: RB_.size = SECTOR_SIZE; RB_.pos = 4; break; + case 17: test_raw_fail = 1; break; + case 18: resident_data[42] ^= 1; break; + case 19: test_raw_bad_header = 1; break; + case 20: test_raw_bad_copy = 1; break; + case 21: test_raw_mode = 1; break; + case 22: test_raw_submode = 0x0A; break; /* video */ + case 23: test_raw_submode = 0x0C; break; /* audio */ + case 24: test_raw_submode = 0x28; break; /* Form2 */ + case 25: test_raw_submode = 0x48; break; /* realtime */ + case 26: test_raw_submode = 0; break; + case 27: lba = 1000; read_sec = 15; read_sect = 25; break; + case 28: mode_reg = 0; break; + case 29: + case 30: + RB_.size = WHOLE_SECTOR_SIZE; RB_.pos = 12 + SECTOR_SIZE; + last_sector_mode = mode_reg; last_sector_have_raw = 1; + last_sector_lba = n == 29 ? 98 : 99; + if (n == 30) --RB_.pos; + break; + case 31: xa_data_end_pending = 1; break; + case 32: cdda_data_end_pending = 1; break; + } + const uint32_t size = cdrom_snapshot_bytes(); + uint8_t *before = malloc(size), *after = malloc(size); + CHECK(before && after); + cdrom_snapshot_write(before); + CHECK(!cdrom_resident_feed_sector(lba, data)); + cdrom_snapshot_write(after); + CHECK(!memcmp(before, after, size)); + free(before); free(after); + } +} + +int main(void) { + CHECK(fifo_baseline_main() == 0); + original_continuation_and_pause(); + repeated_one_sector_and_snapshot(); + decline_without_mutation(); + puts("PASS: resident single-sector FIFO/IRQ, 24 snapshot restores, original continuation/Pause, 33 atomic declines"); + return 0; +} diff --git a/runtime/tests/test_cdrom_sector_fifo.c b/runtime/tests/test_cdrom_sector_fifo.c index 44e89231..658d1df3 100644 --- a/runtime/tests/test_cdrom_sector_fifo.c +++ b/runtime/tests/test_cdrom_sector_fifo.c @@ -5,16 +5,27 @@ 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; +static int test_raw_fail, test_raw_bad_header, test_raw_bad_copy; +static uint8_t test_raw_mode = 2, test_raw_submode = 8; 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; + int m, s, f; + (void)h; if (test_raw_fail) return 0; + memset(out, 0, size); + lba_to_msf((int)lba, 150, &m, &s, &f); + out[12]=bin_to_bcd(m); out[13]=bin_to_bcd(s); + out[14]=bin_to_bcd(f) ^ (test_raw_bad_header ? 1u : 0u); + out[15]=test_raw_mode; out[18]=out[22]=test_raw_submode; + if (test_raw_bad_copy) out[20]=1; 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; + test_raw_fail = test_raw_bad_header = test_raw_bad_copy = 0; + test_raw_mode = 2; test_raw_submode = 8; psx_cycle_count = 1000000; cdrom_init("synthetic"); mode_reg = 0x80;