From 60e8f8c71e5f26c404a7fcffc4bc253f74461827 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sun, 25 Oct 2026 08:47:03 -0700 Subject: [PATCH] Preserve CD header and cursor when completing resident data reads --- runtime/include/cdrom.h | 10 +++ runtime/include/mod_resident.h | 8 ++ runtime/src/cdrom.c | 75 ++++++++++++++++++ runtime/src/mod_guest_services.c | 19 +++++ runtime/tests/test_cdrom_sector_fifo.c | 101 ++++++++++++++++++++++++- 5 files changed, 212 insertions(+), 1 deletion(-) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index 44caebef..2af3301e 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -33,6 +33,16 @@ void cdrom_notify_game_started(void); * ring buffer to correlate each sector transfer with its disc position. */ int cdrom_get_setloc_lba(void); +/* Trusted opt-in resident adapter: fold a verified Mode2/Form1 data span + * after its real ReadN ACK, before the first sector has arrived. The commit + * callback copies the complete span to RAM and must not advance guest time. + * A zero callback result leaves controller state unchanged. Success retains + * the actual final header/FIFO and next drive cursor, cancels sector delivery, + * and raises no IRQ; the title must finish its audited SDK completion/Pause. + * last_user_data must be the verified final2048-byte payload. */ +int cdrom_resident_complete_read(uint32_t lba, uint32_t sectors, + const uint8_t* last_user_data, int (*commit)(void*), void* context); + /* '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..c3f72fab 100644 --- a/runtime/include/mod_resident.h +++ b/runtime/include/mod_resident.h @@ -48,6 +48,14 @@ typedef struct PSXResidentFile { * the original file; NULL = not catalogued */ } PSXResidentFile; +/* Complete a verified pure-data resident span through the shared CD-DMA RAM + * path. Requires the original ReadN ACK and pristine first-sector ownership; + * busy/media/changed payloads decline without RAM/controller mutation. Keeps + * the actual final GetlocL header, retained FIFO and next drive cursor. The + * trusted title still supplies its original SDK completion and Pause ack. */ +int psx_mod_resident_cd_read(uint32_t address, const void* data, + uint32_t lba, uint32_t sectors); + 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..dd2f7ab8 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -3271,6 +3271,81 @@ int cdrom_data_read_active(void) { return reading && !xa_stream_active; } +int cdrom_resident_complete_read(uint32_t lba, uint32_t sectors, + const uint8_t* last_user_data, int (*commit)(void*), void* context) { + uint8_t raw[RAW_SECTOR_SIZE]; + CDROMSectorDelivery delivery; + uint32_t last; + int next_m, next_s, next_f; + CdSectorBuf* slot; + /* Only the already-ACKed, untouched first sector of a normal ReadN can + * be replaced. An interrupt, DMA or arriving sector keeps full ownership + * on the original path. No defaults or ordinary CD timings change. */ + if (!commit || !last_user_data || !sectors || !has_disc() || + lba >= iso_sector_count(iso_handle) || lba > 0x7FFFFF00u || + sectors > 0x7FFFFF00u - lba || + sectors > iso_sector_count(iso_handle) - lba || + !reading || read_cmd != 0x06 || + (mode_reg != 0x80u && mode_reg != 0xA0u) || + xa_stream_active || cdda_playing || + s_setloc_lba != (int)lba || 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() || rb_available()) + return 0; + last = lba + sectors - 1u; + if (!iso_read_raw_sector(iso_handle, last, raw, sizeof raw) || + raw[15] != CDROM_SECTOR_MODE2 || + memcmp(raw + 16, raw + 20, 4) || !(raw[18] & 0x08u) || + (raw[18] & 0x66u) || /* audio, video, Form2, real-time */ + memcmp(raw + RAW_USER_DATA_OFFSET, last_user_data, SECTOR_SIZE)) + return 0; + /* RAM delivery validates its entire destination before writing. Neither + * it nor overlay-capture bookkeeping executes guest code, so the guarded + * controller ownership cannot change before this atomic commit. */ + if (!commit(context)) return 0; + delivery = classify_raw_sector(raw, 1); + delivery.data_delivered = 1; + stop_read_stream(); + clear_sector_buffer(); + s_ring_read = s_ring_write = (int)(sectors % CDROM_NUM_SECTOR_BUFFERS); + slot = &s_sector_ring[s_ring_write]; + if (mode_reg & 0x20u) { + memcpy(slot->data, raw + WHOLE_SECTOR_OFFSET, WHOLE_SECTOR_SIZE); + slot->size = WHOLE_SECTOR_SIZE; + } else { + memcpy(slot->data, raw + RAW_USER_DATA_OFFSET, SECTOR_SIZE); + slot->size = SECTOR_SIZE; + } + slot->pos = slot->size; + s_data_fifo = *slot; + /* PsyQ consumes 12 header/subheader bytes before the 2048 user bytes in + * whole-sector mode. Retain the ECC tail and BFRD latch just as that + * transfer does; a later explicit BFRD can reload the retained sector. */ + s_data_fifo.pos = (mode_reg & 0x20u) ? 12 + SECTOR_SIZE : SECTOR_SIZE; + request_reg |= CDROM_REQUEST_BFRD; + memcpy(last_sector_buffer, slot->data, (size_t)slot->size); + last_sector_lba = (int)last; + last_sector_size = slot->size; + last_sector_frame = (uint32_t)s_frame_count; + last_sector_mode = mode_reg; + last_sector_have_raw = 1; + last_sector_raw_mode = delivery.raw_mode; + last_sector_xa_file = delivery.xa_file; + last_sector_xa_channel = delivery.xa_channel; + last_sector_xa_submode = delivery.xa_submode; + last_sector_xa_coding = delivery.xa_coding; + lba_to_msf((int)(lba + sectors), 150, &next_m, &next_s, &next_f); + read_min = next_m; read_sec = next_s; read_sect = next_f; + setloc_pending = 0; + if (subq_replacements_active) update_last_valid_subq(last); + record_sector_history((int)last, slot->size, mode_reg, 1, slot->data, &delivery); + trace_cdrom('H', lba, sectors, 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..4e1d8542 100644 --- a/runtime/src/mod_guest_services.c +++ b/runtime/src/mod_guest_services.c @@ -2,7 +2,9 @@ * one shared implementation of an effect several titles' adapters need; see * the header for the contract. */ #include "mod_plugins.h" +#include "mod_resident.h" #include "cpu_state.h" +#include "cdrom.h" #include "dma.h" #include "gpu.h" #include "interrupts.h" @@ -15,6 +17,23 @@ * guest-mutating service refuses then (docs/NETPLAY.md). */ int psx_mod_host_output_scope(void); +typedef struct ResidentCDCopy { + uint32_t address, bytes, lba; + const void* data; +} ResidentCDCopy; +static int resident_cd_copy(void* opaque) { + ResidentCDCopy* copy = (ResidentCDCopy*)opaque; + return psx_mod_dma_write_ram(copy->address, copy->data, copy->bytes, (int)copy->lba); +} +int psx_mod_resident_cd_read(uint32_t address, const void* data, + uint32_t lba, uint32_t sectors) { + if (psx_mod_host_output_scope() || !data || !sectors || + sectors > UINT32_MAX / 2048u || (address & 3u)) return 0; + ResidentCDCopy copy = {address, sectors * 2048u, lba, data}; + return cdrom_resident_complete_read(lba, sectors, + (const uint8_t*)data + copy.bytes - 2048u, resident_cd_copy, ©); +} + 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_sector_fifo.c b/runtime/tests/test_cdrom_sector_fifo.c index 44e89231..28146182 100644 --- a/runtime/tests/test_cdrom_sector_fifo.c +++ b/runtime/tests/test_cdrom_sector_fifo.c @@ -5,22 +5,119 @@ 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_copy_result = 1, test_copy_calls; +static uint8_t test_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); + out[15]=2;out[16]=out[20]=9;out[17]=out[21]=7; + out[18]=out[22]=test_submode; 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=0;test_submode=8;test_copy_result=1;test_copy_calls=0; psx_cycle_count = 1000000; cdrom_init("synthetic"); + setloc_pending = 0; mode_reg = 0x80; reading = 1; read_min = 0; read_sec = 3; read_sect = 25; /* LBA 100 */ } +static void sector(void); +static void ack(void); +static void request(void); +static void drain(unsigned lba, int words); +static int resident_copy(void* context) { + CHECK(context == &test_copy_calls); + ++test_copy_calls; + return test_copy_result; +} +static void resident_setup(uint8_t mode) { + setup(); mode_reg=mode; reading=0; + seek_min=0; seek_sec=3; seek_sect=25; + s_setloc_lba=100; setloc_pending=1; + exec_command(6); + CHECK(irq_flag==CDIRQ_ACK && response_count==1); + (void)cdrom_read(0x1F801801); + CHECK(response_read==response_count && response_count!=0); + /* ReadN's consumed response and elapsed presentation deadline remain + * after the SDK acknowledges it; neither means there is pending work. */ + psx_cycle_count+=CDROM_IRQ_PRESENT_DELAY; + cdrom_advance(0); ack(); i_stat=0; + CHECK(cdrom_irq_present_due!=0 && !irq_flag && reading); +} +static void resident_header_cursor_and_restore(void) { + const uint8_t modes[]={0x80,0xA0}; + for(unsigned n=0;n