Add guarded single-sector resident CD FIFO delivery

This commit is contained in:
Matthew Stanley
2026-10-25 11:33:40 -07:00
parent 0ec8f82eea
commit 619bb418cf
8 changed files with 289 additions and 5 deletions
+35
View File
@@ -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.
+4
View File
@@ -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)
+6
View File
@@ -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
+12
View File
@@ -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`.
+72 -4
View File
@@ -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
+6
View File
@@ -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) {
+142
View File
@@ -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;
}
+12 -1
View File
@@ -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;