Preserve CD header and cursor when completing resident data reads

This commit is contained in:
Matthew Stanley
2026-10-25 08:47:03 -07:00
parent e2becd441e
commit 60e8f8c71e
5 changed files with 212 additions and 1 deletions
+10
View File
@@ -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
+8
View File
@@ -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`.
+75
View File
@@ -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
+19
View File
@@ -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, &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) {
+100 -1
View File
@@ -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<sizeof modes;++n) {
uint8_t payload[2048]; memset(payload,102,sizeof payload);
resident_setup(modes[n]);
CHECK(cdrom_resident_complete_read(100,3,payload,resident_copy,&test_copy_calls));
CHECK(test_copy_calls==1 && !reading && !irq_flag && !i_stat);
CHECK(last_sector_lba==102 && msf_to_lba(read_min,read_sec,read_sect)==103);
CHECK(s_setloc_lba==100 && !setloc_pending);
CHECK(s_ring_read==s_ring_write && s_ring_write==3);
CHECK(s_data_fifo.pos==((modes[n]&0x20)?2060:2048));
CHECK(last_sector_xa_file==9 && last_sector_xa_channel==7);
const uint32_t size=cdrom_snapshot_bytes();
uint8_t* wire=malloc(size);CHECK(wire);
cdrom_snapshot_write(wire);
setup(); CHECK(cdrom_snapshot_read(wire,size));free(wire);
CHECK(last_sector_lba==102 && msf_to_lba(read_min,read_sec,read_sect)==103);
CHECK(s_data_fifo.pos==((modes[n]&0x20)?2060:2048));
exec_command(0x10);
CHECK(response_count==8 && response_fifo[0]==0 && response_fifo[1]==3 &&
response_fifo[2]==0x27 && response_fifo[3]==2 &&
response_fifo[4]==9 && response_fifo[5]==7 && response_fifo[6]==8);
response_clear();ack();
exec_command(0x11);
CHECK(response_count==8 && response_fifo[5]==0 &&
response_fifo[6]==3 && response_fifo[7]==0x28);
response_clear();ack();
/* Original Pause/ack and a retail read with no new Setloc resume
* at the next sector, never the start or last-header sector. */
exec_command(9);response_clear();ack();
psx_cycle_count+=100000000;process_pending(0);response_clear();ack();
mode_reg=0x80;exec_command(6);response_clear();ack();
CHECK(msf_to_lba(read_min,read_sec,read_sect)==103);
sector();request();drain(103,512);
}
}
static void resident_declines_atomically(void) {
for(unsigned reason=0;reason<14;++reason) {
uint8_t payload[2048];memset(payload,102,sizeof payload);
resident_setup(0xA0);
switch(reason) {
case 0: reading=0;break;
case 1: read_sect++;break; /* A physical sector already arrived. */
case 2: irq_flag=3;break;
case 3: test_dma_active=1;break;
case 4: mode_reg=0xE0;break;
case 5: pending.pending=1;break;
case 6: queued_cmd.pending=1;break;
case 7: test_submode=0x24;break;
case 8: test_submode=0x0A;break;
case 9: test_raw_fail=1;break;
case 10: payload[42]^=1;break;
case 11: test_copy_result=0;break;
case 12: setloc_pending=1;break;
case 13: response_read=0;break; /* Unconsumed command response. */
}
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_complete_read(100,3,payload,resident_copy,&test_copy_calls));
CHECK(test_copy_calls==(reason==11?1:0));
cdrom_snapshot_write(after);CHECK(!memcmp(before,after,size));
free(before);free(after);
}
}
static void sector(void) {
psx_cycle_count += 225792;
read_delay = 0;
@@ -231,6 +328,8 @@ static void snapshot_setloc_and_paused_head(void) {
free(wire);
}
int main(void) {
resident_header_cursor_and_restore();
resident_declines_atomically();
pause_read_resumes_position();
snapshot_setloc_and_paused_head();
invalid_setloc_preserves_target();