From 3b837f4e2d96a6e60f103cfd1b5cf5059cb6b349 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Mon, 31 Aug 2026 01:26:09 +0300 Subject: [PATCH 1/4] Fix CD-ROM seek and read handoff --- docs/disc-speed.md | 5 + recompiler/CMakeLists.txt | 1 + runtime/CMakeLists.txt | 3 + runtime/src/cdrom.c | 29 +++++- runtime/tests/test_cdrom_implicit_seek.py | 106 ++++++++++++++++++++++ 5 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 runtime/tests/test_cdrom_implicit_seek.py diff --git a/docs/disc-speed.md b/docs/disc-speed.md index ff55bbe8..bbb7b457 100644 --- a/docs/disc-speed.md +++ b/docs/disc-speed.md @@ -49,6 +49,11 @@ Change is localized to `runtime/src/cdrom.c`. The timing delay paths (seek durat read interval) check `disc_speed` and either apply the emulated delay or skip it. No other subsystem is affected. +The CD-ROM state sequence does not change at higher speeds. `Setloc` followed by `ReadN` or +`ReadS` keeps the drive in `SEEK` until the logical seek is complete. The drive then changes +to `READ` and delivers the requested sector first. An explicit `SeekL` or `SeekP` command +stops any active read stream before it starts the new seek. + --- ## Relation to overlay-discovery diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 81dd2cf3..799c951e 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -579,6 +579,7 @@ if(BUILD_TESTING) foreach(_t IN ITEMS bios_selection_guards capture_history + cdrom_implicit_seek default_renderer_guards mod_owned_display_controls fmv_quiet_guards diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 2494923a..3125e284 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -480,6 +480,9 @@ if(BUILD_TESTING) find_package(Python3 COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME cdrom_implicit_seek_test + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cdrom_implicit_seek.py) add_test(NAME frame_interpolation_context_ownership_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_frame_interpolation_context_ownership.py) diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 59529a42..110663c8 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -1560,6 +1560,7 @@ static int read_continues_current_stream(void) { } static void start_read_stream(uint8_t cmd) { + int implicit_seek = setloc_pending; cdda_playing = 0; cdda_track = 0; cdda_delay = 0; @@ -1578,13 +1579,23 @@ static void start_read_stream(uint8_t cmd) { read_sec = seek_sec; read_sect = seek_sect; read_cmd = cmd; - read_delay = initial_read_delay_cycles(); + /* An unprocessed Setloc makes ReadN/ReadS perform a logical seek before + * the first sector. Keep the current bounded seek model, but do not let + * sector delivery begin while that seek is still active. */ + read_delay = implicit_seek + ? seek_complete_delay_cycles() + sector_delay_cycles() + : initial_read_delay_cycles(); s_cd_probe_read_start_count++; s_cd_probe_read_start_cycles += (uint64_t)read_delay; s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay; s_cd_timing_stream_starts++; reading = 1; - stat_reg |= CDSTAT_READ; + if (implicit_seek) { + stat_reg &= (uint8_t)~CDSTAT_READ; + stat_reg |= CDSTAT_SEEK; + } else { + stat_reg |= CDSTAT_READ; + } /* ENQUEUE: sector-read stream scheduled (due in read_delay cycles). A * content load that happens in OFF but not ON shows up as a missing * SRC_CD_READ enqueue here. */ @@ -2349,9 +2360,14 @@ static void exec_command(uint8_t cmd) { set_irq(CDIRQ_ERROR); break; } + /* A seek owns the drive and cancels any prior ReadN/ReadS generation. + * Otherwise the old stream continues during the seek and later reads + * never consume the new Setloc target. */ + stop_read_stream(); xa_reset_decode(); spu_cd_audio_reset(); stop_cdda_playback(); + stat_reg &= (uint8_t)~(CDSTAT_READ | CDSTAT_PLAY); stat_reg |= CDSTAT_SEEK; response_push(stat_reg); set_irq(CDIRQ_ACK); @@ -2496,7 +2512,7 @@ static void process_pending(uint32_t cycles) { stat_reg &= ~CDSTAT_SEEK; stat_reg |= CDSTAT_READ; /* PSX-CD-003: GT1 waits for READ after seek */ setloc_seek_far = 0; - setloc_pending = 0; + setloc_pending = 0; response_push(stat_reg); set_irq(CDIRQ_COMPLETE); fire_cdrom_irq(); @@ -2637,6 +2653,13 @@ static void process_read_stream(uint32_t cycles) { } if (read_delay <= 0) { + /* The implicit Setloc seek completes when the first requested sector + * becomes eligible. Status changes from SEEK to READ at this point. */ + if (stat_reg & CDSTAT_SEEK) { + stat_reg &= (uint8_t)~CDSTAT_SEEK; + stat_reg |= CDSTAT_READ; + setloc_seek_far = 0; + } uint64_t timing_seq = cd_timing_begin_sector( msf_to_lba(read_min, read_sec, read_sect)); if (irq_flag == 0) { diff --git a/runtime/tests/test_cdrom_implicit_seek.py b/runtime/tests/test_cdrom_implicit_seek.py new file mode 100644 index 00000000..ec8de956 --- /dev/null +++ b/runtime/tests/test_cdrom_implicit_seek.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Title-neutral Setloc plus ReadN/ReadS implicit-seek contract.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def function_body(source: str, signature: str, next_signature: str) -> str: + start = source.index(signature) + end = source.index(next_signature, start) + return source[start:end] + + +def require_in_order(body: str, *needles: str) -> None: + position = -1 + for needle in needles: + position = body.index(needle, position + 1) + + +def model_contract() -> None: + target_lba = 900 + current_lba = 100 + setloc_pending = True + seek_far = abs(target_lba - current_lba) > 16 + single_speed_sector_cycles = 451_584 + double_speed = True + + seek_cycles = single_speed_sector_cycles * 4 if seek_far else 0x800 + sector_cycles = ( + single_speed_sector_cycles // 2 + if double_speed + else single_speed_sector_cycles + ) + first_sector_delay = seek_cycles + sector_cycles + + status = "SEEK" + elapsed = seek_cycles + assert elapsed < first_sector_delay + assert status == "SEEK" + + elapsed = first_sector_delay + status = "READ" + delivered_lba = target_lba + setloc_pending = False + assert elapsed == first_sector_delay + assert status == "READ" + assert delivered_lba == target_lba + assert not setloc_pending + + +def model_explicit_seek_ownership() -> None: + reading = True + status = {"READ"} + + reading = False + status.discard("READ") + status.discard("PLAY") + status.add("SEEK") + + assert not reading + assert status == {"SEEK"} + + +def main() -> None: + source = (ROOT / "runtime/src/cdrom.c").read_text(encoding="utf-8") + start = function_body(source, "static void start_read_stream", "static void stop_read_stream") + commands = function_body(source, "static void exec_command", "static void process_pending") + stream = function_body( + source, + "static void process_read_stream", + "static void present_pending_dataready", + ) + + require_in_order( + start, + "int implicit_seek = setloc_pending;", + "setloc_pending = 0;", + "seek_complete_delay_cycles() + sector_delay_cycles()", + "stat_reg |= CDSTAT_SEEK;", + ) + require_in_order( + stream, + "if (read_delay <= 0)", + "if (stat_reg & CDSTAT_SEEK)", + "stat_reg &= (uint8_t)~CDSTAT_SEEK;", + "stat_reg |= CDSTAT_READ;", + "cd_timing_begin_sector", + ) + require_in_order( + commands, + "case 0x15:", + "case 0x16:", + "stop_read_stream();", + "stat_reg &= (uint8_t)~(CDSTAT_READ | CDSTAT_PLAY);", + "stat_reg |= CDSTAT_SEEK;", + ) + + model_contract() + model_explicit_seek_ownership() + print("CD-ROM seek semantics contract: PASS") + + +if __name__ == "__main__": + main() From 25e7624a0d05754ed62e0a45239baabbac96a326 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Mon, 31 Aug 2026 01:58:52 +0300 Subject: [PATCH 2/4] Harden CD-ROM seek state coverage --- recompiler/CMakeLists.txt | 19 ++- runtime/CMakeLists.txt | 19 ++- runtime/src/cdrom.c | 144 +++++++++++++++++++--- runtime/tests/test_cdrom_implicit_seek.py | 106 ---------------- runtime/tests/test_cdrom_seek_semantics.c | 109 ++++++++++++++++ 5 files changed, 268 insertions(+), 129 deletions(-) delete mode 100644 runtime/tests/test_cdrom_implicit_seek.py create mode 100644 runtime/tests/test_cdrom_seek_semantics.c diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 799c951e..4e74eddc 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -356,6 +356,24 @@ if(BUILD_TESTING) ../runtime/include) add_test(NAME debug_trace_ranges_test COMMAND debug_trace_ranges_test) + add_executable(cdrom_seek_semantics_test + ../runtime/tests/test_cdrom_seek_semantics.c + ../runtime/src/cdrom.c) + target_include_directories(cdrom_seek_semantics_test PRIVATE + ../runtime/include + ../runtime/src) + target_compile_definitions(cdrom_seek_semantics_test PRIVATE + PSX_CDROM_SEEK_SEMANTICS_TEST=1) + if(MSVC) + target_compile_options(cdrom_seek_semantics_test PRIVATE /Gy) + target_link_options(cdrom_seek_semantics_test PRIVATE /OPT:REF) + else() + target_compile_options(cdrom_seek_semantics_test PRIVATE + -ffunction-sections -fdata-sections) + target_link_options(cdrom_seek_semantics_test PRIVATE -Wl,--gc-sections) + endif() + add_test(NAME cdrom_seek_semantics_test COMMAND cdrom_seek_semantics_test) + add_executable(cli_boot_path_test tests/cli_boot_path_test.cpp src/cli_boot_path.cpp @@ -579,7 +597,6 @@ if(BUILD_TESTING) foreach(_t IN ITEMS bios_selection_guards capture_history - cdrom_implicit_seek default_renderer_guards mod_owned_display_controls fmv_quiet_guards diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 3125e284..1995dbae 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -330,6 +330,22 @@ if(BUILD_TESTING) target_include_directories(cdrom_irq_mask_test PRIVATE src) add_test(NAME cdrom_irq_mask_test COMMAND cdrom_irq_mask_test) + add_executable(cdrom_seek_semantics_test + tests/test_cdrom_seek_semantics.c + src/cdrom.c) + target_include_directories(cdrom_seek_semantics_test PRIVATE include src) + target_compile_definitions(cdrom_seek_semantics_test PRIVATE + PSX_CDROM_SEEK_SEMANTICS_TEST=1) + if(MSVC) + target_compile_options(cdrom_seek_semantics_test PRIVATE /Gy) + target_link_options(cdrom_seek_semantics_test PRIVATE /OPT:REF) + else() + target_compile_options(cdrom_seek_semantics_test PRIVATE + -ffunction-sections -fdata-sections) + target_link_options(cdrom_seek_semantics_test PRIVATE -Wl,--gc-sections) + endif() + add_test(NAME cdrom_seek_semantics_test COMMAND cdrom_seek_semantics_test) + # Cross-device I_STAT ownership: a memory-card ACK IRQ (SIO0) must not # clear another device's pending source — pinned on SPU (I_STAT bit 9) # staying set across card ACK delivery until the guest acknowledges it. @@ -480,9 +496,6 @@ if(BUILD_TESTING) find_package(Python3 COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) - add_test(NAME cdrom_implicit_seek_test - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cdrom_implicit_seek.py) add_test(NAME frame_interpolation_context_ownership_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_frame_interpolation_context_ownership.py) diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 110663c8..24177992 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -1559,6 +1559,29 @@ static int read_continues_current_stream(void) { return 1; } +static int read_start_delay_cycles(int implicit_seek) { + return implicit_seek + ? seek_complete_delay_cycles() + sector_delay_cycles() + : initial_read_delay_cycles(); +} + +static void apply_read_start_status(int implicit_seek) { + if (implicit_seek) { + stat_reg &= (uint8_t)~(CDSTAT_READ | CDSTAT_PLAY); + stat_reg |= CDSTAT_SEEK; + } else { + stat_reg &= (uint8_t)~CDSTAT_SEEK; + stat_reg |= CDSTAT_READ; + } +} + +static void finish_implicit_seek_status(void) { + if (!(stat_reg & CDSTAT_SEEK)) return; + stat_reg &= (uint8_t)~CDSTAT_SEEK; + stat_reg |= CDSTAT_READ; + setloc_seek_far = 0; +} + static void start_read_stream(uint8_t cmd) { int implicit_seek = setloc_pending; cdda_playing = 0; @@ -1582,20 +1605,13 @@ static void start_read_stream(uint8_t cmd) { /* An unprocessed Setloc makes ReadN/ReadS perform a logical seek before * the first sector. Keep the current bounded seek model, but do not let * sector delivery begin while that seek is still active. */ - read_delay = implicit_seek - ? seek_complete_delay_cycles() + sector_delay_cycles() - : initial_read_delay_cycles(); + read_delay = read_start_delay_cycles(implicit_seek); s_cd_probe_read_start_count++; s_cd_probe_read_start_cycles += (uint64_t)read_delay; s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay; s_cd_timing_stream_starts++; reading = 1; - if (implicit_seek) { - stat_reg &= (uint8_t)~CDSTAT_READ; - stat_reg |= CDSTAT_SEEK; - } else { - stat_reg |= CDSTAT_READ; - } + apply_read_start_status(implicit_seek); /* ENQUEUE: sector-read stream scheduled (due in read_delay cycles). A * content load that happens in OFF but not ON shows up as a missing * SRC_CD_READ enqueue here. */ @@ -1630,6 +1646,13 @@ static void stop_read_stream(void) { cdrom_clear_pending_dataready(); } +static void take_explicit_seek_ownership(void) { + stop_read_stream(); + clear_sector_buffer(); + stat_reg &= (uint8_t)~(CDSTAT_READ | CDSTAT_PLAY); + stat_reg |= CDSTAT_SEEK; +} + static void stop_cdda_playback(void) { cdda_playing = 0; cdda_track = 0; @@ -2363,12 +2386,10 @@ static void exec_command(uint8_t cmd) { /* A seek owns the drive and cancels any prior ReadN/ReadS generation. * Otherwise the old stream continues during the seek and later reads * never consume the new Setloc target. */ - stop_read_stream(); + take_explicit_seek_ownership(); xa_reset_decode(); spu_cd_audio_reset(); stop_cdda_playback(); - stat_reg &= (uint8_t)~(CDSTAT_READ | CDSTAT_PLAY); - stat_reg |= CDSTAT_SEEK; response_push(stat_reg); set_irq(CDIRQ_ACK); { @@ -2655,11 +2676,7 @@ static void process_read_stream(uint32_t cycles) { if (read_delay <= 0) { /* The implicit Setloc seek completes when the first requested sector * becomes eligible. Status changes from SEEK to READ at this point. */ - if (stat_reg & CDSTAT_SEEK) { - stat_reg &= (uint8_t)~CDSTAT_SEEK; - stat_reg |= CDSTAT_READ; - setloc_seek_far = 0; - } + finish_implicit_seek_status(); uint64_t timing_seq = cd_timing_begin_sector( msf_to_lba(read_min, read_sec, read_sect)); if (irq_flag == 0) { @@ -3257,6 +3274,17 @@ int cdrom_savestate_boost_vblanks_remaining(void) { /* LE field wire — PendingCmd/QueuedCmd have host padding. */ #include "pst_wire.h" +/* Keep the existing four-byte wire slot. Old snapshots stored only 0 or 1 + * here, so bit 1 is free for the pending Setloc state. */ +static int32_t pack_setloc_snapshot_state(void) { + return (setloc_seek_far ? 1 : 0) | (setloc_pending ? 2 : 0); +} + +static void unpack_setloc_snapshot_state(int32_t state) { + setloc_seek_far = (state & 1) != 0; + setloc_pending = (state & 2) != 0; +} + static int cdrom_snap_emit(PstW *w) { #define W8(f) do { if (!pst_w_u8(w, (uint8_t)(f))) return 0; } while (0) #define WI(f) do { if (!pst_w_i32(w, (int32_t)(f))) return 0; } while (0) @@ -3292,7 +3320,8 @@ static int cdrom_snap_emit(PstW *w) { WU(0u); W8(last_sector_mode); W8(last_sector_have_raw); W8(last_sector_raw_mode); W8(last_sector_xa_file); W8(last_sector_xa_channel); W8(last_sector_xa_submode); W8(last_sector_xa_coding); - W8(seek_min); W8(seek_sec); W8(seek_sect); WI(s_setloc_lba); WI(setloc_seek_far); + W8(seek_min); W8(seek_sec); W8(seek_sect); WI(s_setloc_lba); + WI(pack_setloc_snapshot_state()); WI(reading); WI(read_min); WI(read_sec); WI(read_sect); W8(mode_reg); W8(read_cmd); WI(read_delay); W8(filter_file); W8(filter_channel); W8(cd_muted); WI(cdda_playing); WI(cdda_track); WU(cdda_lba); WI(cdda_delay); @@ -3358,7 +3387,8 @@ static int cdrom_snap_parse(PstR *r) { RU(last_sector_frame); R8(last_sector_mode); R8(last_sector_have_raw); R8(last_sector_raw_mode); R8(last_sector_xa_file); R8(last_sector_xa_channel); R8(last_sector_xa_submode); R8(last_sector_xa_coding); - R8(seek_min); R8(seek_sec); R8(seek_sect); RI(s_setloc_lba); RI(setloc_seek_far); setloc_pending = 0; + R8(seek_min); R8(seek_sec); R8(seek_sect); RI(s_setloc_lba); + RI(i); unpack_setloc_snapshot_state(i); RI(reading); RI(read_min); RI(read_sec); RI(read_sect); R8(mode_reg); R8(read_cmd); RI(read_delay); R8(filter_file); R8(filter_channel); R8(cd_muted); RI(cdda_playing); RI(cdda_track); RU(cdda_lba); RI(cdda_delay); @@ -3408,6 +3438,82 @@ int cdrom_snapshot_read(const uint8_t *p, uint32_t len) { return 1; } +#ifdef PSX_CDROM_SEEK_SEMANTICS_TEST +int cdrom_test_explicit_seek_ownership(void) { + int failures = 0; + reading = 1; + read_cmd = 0x06; + read_delay = 1234; + pending_dataready = 1; + pending_dataready_stat = CDSTAT_READ; + pending_present_due = 5678; + request_reg |= CDROM_REQUEST_BFRD; + stat_reg = CDSTAT_MOTOR | CDSTAT_READ | CDSTAT_PLAY; + iso_handle = (void *)(uintptr_t)1; + irq_flag = 0; + for (int slot = 0; slot < CDROM_NUM_SECTOR_BUFFERS; slot++) { + s_sector_ring[slot].size = 32 + slot; + s_sector_ring[slot].pos = slot; + } + + exec_command(0x15); + + if (reading || read_cmd || read_delay || s_cd_timing_next_due) failures |= 1; + if (pending_dataready || pending_dataready_stat || pending_present_due) failures |= 2; + if (request_reg & CDROM_REQUEST_BFRD) failures |= 4; + for (int slot = 0; slot < CDROM_NUM_SECTOR_BUFFERS; slot++) { + if (s_sector_ring[slot].size || s_sector_ring[slot].pos) failures |= 8; + } + if ((stat_reg & (CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK)) != CDSTAT_SEEK) + failures |= 16; + return failures; +} + +int cdrom_test_implicit_seek_sequence(void) { + const int expected = CDROM_SINGLE_SPEED_SECTOR_CYCLES * 4 + + CDROM_SINGLE_SPEED_SECTOR_CYCLES / 2; + int failures = 0; + mode_reg = 0x80; + g_disc_speed_divisor = 1; + xa_stream_active = 0; + s_warm_route_active = 0; + reading = 0; + pending.pending = 0; + iso_handle = (void *)(uintptr_t)1; + last_sector_lba = 0; + stat_reg = CDSTAT_MOTOR | CDSTAT_READ | CDSTAT_PLAY; + irq_flag = 0; + param_fifo[0] = 0x00; + param_fifo[1] = 0x10; + param_fifo[2] = 0x00; + param_count = 3; + + exec_command(0x02); + irq_flag = 0; + exec_command(0x06); + + if (read_delay != expected) failures |= 1; + if ((stat_reg & (CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK)) != CDSTAT_SEEK) + failures |= 2; + if (setloc_pending) failures |= 4; + irq_flag = 0; + process_read_stream((uint32_t)expected); + if ((stat_reg & (CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK)) != CDSTAT_READ) + failures |= 8; + if (setloc_seek_far) failures |= 16; + return failures; +} + +void cdrom_test_set_setloc_state(int far_state, int pending_state) { + setloc_seek_far = far_state != 0; + setloc_pending = pending_state != 0; +} + +int cdrom_test_get_setloc_state(void) { + return pack_setloc_snapshot_state(); +} +#endif + void debug_force_cd_reinsert(void) { // Simulamos la apertura de la bandeja borrando los flujos actuales stop_read_stream(); diff --git a/runtime/tests/test_cdrom_implicit_seek.py b/runtime/tests/test_cdrom_implicit_seek.py deleted file mode 100644 index ec8de956..00000000 --- a/runtime/tests/test_cdrom_implicit_seek.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -"""Title-neutral Setloc plus ReadN/ReadS implicit-seek contract.""" - -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[2] - - -def function_body(source: str, signature: str, next_signature: str) -> str: - start = source.index(signature) - end = source.index(next_signature, start) - return source[start:end] - - -def require_in_order(body: str, *needles: str) -> None: - position = -1 - for needle in needles: - position = body.index(needle, position + 1) - - -def model_contract() -> None: - target_lba = 900 - current_lba = 100 - setloc_pending = True - seek_far = abs(target_lba - current_lba) > 16 - single_speed_sector_cycles = 451_584 - double_speed = True - - seek_cycles = single_speed_sector_cycles * 4 if seek_far else 0x800 - sector_cycles = ( - single_speed_sector_cycles // 2 - if double_speed - else single_speed_sector_cycles - ) - first_sector_delay = seek_cycles + sector_cycles - - status = "SEEK" - elapsed = seek_cycles - assert elapsed < first_sector_delay - assert status == "SEEK" - - elapsed = first_sector_delay - status = "READ" - delivered_lba = target_lba - setloc_pending = False - assert elapsed == first_sector_delay - assert status == "READ" - assert delivered_lba == target_lba - assert not setloc_pending - - -def model_explicit_seek_ownership() -> None: - reading = True - status = {"READ"} - - reading = False - status.discard("READ") - status.discard("PLAY") - status.add("SEEK") - - assert not reading - assert status == {"SEEK"} - - -def main() -> None: - source = (ROOT / "runtime/src/cdrom.c").read_text(encoding="utf-8") - start = function_body(source, "static void start_read_stream", "static void stop_read_stream") - commands = function_body(source, "static void exec_command", "static void process_pending") - stream = function_body( - source, - "static void process_read_stream", - "static void present_pending_dataready", - ) - - require_in_order( - start, - "int implicit_seek = setloc_pending;", - "setloc_pending = 0;", - "seek_complete_delay_cycles() + sector_delay_cycles()", - "stat_reg |= CDSTAT_SEEK;", - ) - require_in_order( - stream, - "if (read_delay <= 0)", - "if (stat_reg & CDSTAT_SEEK)", - "stat_reg &= (uint8_t)~CDSTAT_SEEK;", - "stat_reg |= CDSTAT_READ;", - "cd_timing_begin_sector", - ) - require_in_order( - commands, - "case 0x15:", - "case 0x16:", - "stop_read_stream();", - "stat_reg &= (uint8_t)~(CDSTAT_READ | CDSTAT_PLAY);", - "stat_reg |= CDSTAT_SEEK;", - ) - - model_contract() - model_explicit_seek_ownership() - print("CD-ROM seek semantics contract: PASS") - - -if __name__ == "__main__": - main() diff --git a/runtime/tests/test_cdrom_seek_semantics.c b/runtime/tests/test_cdrom_seek_semantics.c new file mode 100644 index 00000000..c670d83a --- /dev/null +++ b/runtime/tests/test_cdrom_seek_semantics.c @@ -0,0 +1,109 @@ +/* + * Validate the production CD-ROM seek helpers and snapshot round trip. + * + * Build/run: ctest -R cdrom_seek_semantics_test + */ +#include "cdrom.h" + +#include +#include +#include + +uint64_t psx_cycle_count; +uint32_t i_stat; +uint32_t g_debug_current_func_addr; +uint32_t g_debug_last_store_pc; +uint64_t s_frame_count; + +/* cdrom.c is compiled as one production translation unit. These inert host + * edges satisfy its linker contract; the test does not execute them. */ +void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; } +void event_ring_record(uint16_t kind, uint8_t detail) { (void)kind; (void)detail; } +void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux) { + (void)kind; (void)detail; (void)aux; +} +void audio_trace_event(uint16_t kind, uint32_t a, uint32_t b) { + (void)kind; (void)a; (void)b; +} +uint32_t interrupts_get_cycles_since_vblank(void) { return 0; } +int dma_cdrom_transfer_active(void) { return 0; } +void spu_cd_audio_push(const int16_t *stereo, int frames) { + (void)stereo; (void)frames; +} +void spu_cd_audio_reset(void) {} +int psx_netplay_active(void) { return 0; } +int psx_netplay_cd_bisect_active(void) { return 0; } +int psx_netplay_is_resimulating(void) { return 0; } +uint32_t psx_netplay_sim_tick(void) { return 0; } + +void *iso_open(const char *path) { (void)path; return NULL; } +void iso_close(void *handle) { (void)handle; } +int iso_read_sector(void *handle, uint32_t lba, uint8_t *buffer, int size) { + (void)handle; (void)lba; (void)buffer; (void)size; return 0; +} +int iso_read_raw_sector(void *handle, uint32_t lba, uint8_t *buffer, int size) { + (void)handle; (void)lba; (void)buffer; (void)size; return 0; +} +int iso_read_subq(void *handle, uint32_t lba, uint8_t *buffer, int size, + int *valid) { + (void)handle; (void)lba; (void)buffer; (void)size; + if (valid) *valid = 0; + return 0; +} +int iso_has_subq_replacements(void *handle) { (void)handle; return 0; } +uint32_t iso_sector_count(void *handle) { (void)handle; return 0; } +int iso_track_count(void *handle) { (void)handle; return 0; } +uint32_t iso_track_start_lba(void *handle, int track) { + (void)handle; (void)track; return 0; +} +uint32_t iso_track_pregap_lba(void *handle, int track) { + (void)handle; (void)track; return 0; +} +int iso_track_is_audio(void *handle, int track) { + (void)handle; (void)track; return 0; +} + +int cdrom_test_explicit_seek_ownership(void); +int cdrom_test_implicit_seek_sequence(void); +void cdrom_test_set_setloc_state(int far_state, int pending_state); +int cdrom_test_get_setloc_state(void); + +static int failures; + +#define CHECK(condition, label) do { \ + if (!(condition)) { \ + fprintf(stderr, "FAIL: %s\n", (label)); \ + failures++; \ + } \ +} while (0) + +int main(void) { + uint32_t snapshot_size; + uint8_t *snapshot; + + CHECK(cdrom_test_explicit_seek_ownership() == 0, + "SeekL/SeekP clears the old stream, pending INT1, ring, and BFRD"); + CHECK(cdrom_test_implicit_seek_sequence() == 0, + "Setloc plus ReadN keeps SEEK until the first sector is eligible"); + + snapshot_size = cdrom_snapshot_bytes(); + snapshot = (uint8_t *)malloc(snapshot_size); + CHECK(snapshot != NULL, "snapshot allocation"); + if (snapshot) { + cdrom_test_set_setloc_state(1, 1); + cdrom_snapshot_write(snapshot); + cdrom_test_set_setloc_state(0, 0); + CHECK(cdrom_snapshot_read(snapshot, snapshot_size) == 1, + "snapshot parser accepts the unchanged wire size"); + CHECK(cdrom_test_get_setloc_state() == 3, + "snapshot restores far-seek and pending-Setloc bits"); + free(snapshot); + } + + if (failures) { + fprintf(stderr, "FAILED (%d)\n", failures); + return 1; + } + puts("CD-ROM seek semantics: ALL PASS"); + return 0; +} From 12a3eb7d119930e9ed2cc36740a97afb51c904b1 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Mon, 31 Aug 2026 02:08:09 +0300 Subject: [PATCH 3/4] Complete CD-ROM reset and snapshot guards --- runtime/src/cdrom.c | 9 +++++++++ runtime/tests/test_cdrom_seek_semantics.c | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 24177992..bc4b4ff0 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -2801,6 +2801,7 @@ void cdrom_init(const char* cue_path) { seek_min = seek_sec = seek_sect = 0; s_setloc_lba = -1; setloc_seek_far = 0; + setloc_pending = 0; s_warm_routes_count = 0; s_warm_route_configured = 0; s_warm_route_enabled = 0; @@ -3439,6 +3440,13 @@ int cdrom_snapshot_read(const uint8_t *p, uint32_t len) { } #ifdef PSX_CDROM_SEEK_SEMANTICS_TEST +int cdrom_test_init_clears_setloc(void) { + setloc_seek_far = 1; + setloc_pending = 1; + cdrom_init(NULL); + return setloc_seek_far || setloc_pending; +} + int cdrom_test_explicit_seek_ownership(void) { int failures = 0; reading = 1; @@ -3501,6 +3509,7 @@ int cdrom_test_implicit_seek_sequence(void) { if ((stat_reg & (CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK)) != CDSTAT_READ) failures |= 8; if (setloc_seek_far) failures |= 16; + if (last_sector_lba != s_setloc_lba) failures |= 32; return failures; } diff --git a/runtime/tests/test_cdrom_seek_semantics.c b/runtime/tests/test_cdrom_seek_semantics.c index c670d83a..918b37bd 100644 --- a/runtime/tests/test_cdrom_seek_semantics.c +++ b/runtime/tests/test_cdrom_seek_semantics.c @@ -8,6 +8,7 @@ #include #include #include +#include uint64_t psx_cycle_count; uint32_t i_stat; @@ -39,7 +40,9 @@ uint32_t psx_netplay_sim_tick(void) { return 0; } void *iso_open(const char *path) { (void)path; return NULL; } void iso_close(void *handle) { (void)handle; } int iso_read_sector(void *handle, uint32_t lba, uint8_t *buffer, int size) { - (void)handle; (void)lba; (void)buffer; (void)size; return 0; + (void)handle; (void)lba; + memset(buffer, 0, (size_t)size); + return 1; } int iso_read_raw_sector(void *handle, uint32_t lba, uint8_t *buffer, int size) { (void)handle; (void)lba; (void)buffer; (void)size; return 0; @@ -65,11 +68,14 @@ int iso_track_is_audio(void *handle, int track) { int cdrom_test_explicit_seek_ownership(void); int cdrom_test_implicit_seek_sequence(void); +int cdrom_test_init_clears_setloc(void); void cdrom_test_set_setloc_state(int far_state, int pending_state); int cdrom_test_get_setloc_state(void); static int failures; +#define EXPECTED_CDROM_SNAPSHOT_BYTES 4931u + #define CHECK(condition, label) do { \ if (!(condition)) { \ fprintf(stderr, "FAIL: %s\n", (label)); \ @@ -81,12 +87,16 @@ int main(void) { uint32_t snapshot_size; uint8_t *snapshot; + CHECK(cdrom_test_init_clears_setloc() == 0, + "controller reset clears pending Setloc state"); CHECK(cdrom_test_explicit_seek_ownership() == 0, "SeekL/SeekP clears the old stream, pending INT1, ring, and BFRD"); CHECK(cdrom_test_implicit_seek_sequence() == 0, "Setloc plus ReadN keeps SEEK until the first sector is eligible"); snapshot_size = cdrom_snapshot_bytes(); + CHECK(snapshot_size == EXPECTED_CDROM_SNAPSHOT_BYTES, + "CD-ROM snapshot wire size remains backward-compatible"); snapshot = (uint8_t *)malloc(snapshot_size); CHECK(snapshot != NULL, "snapshot allocation"); if (snapshot) { @@ -97,6 +107,16 @@ int main(void) { "snapshot parser accepts the unchanged wire size"); CHECK(cdrom_test_get_setloc_state() == 3, "snapshot restores far-seek and pending-Setloc bits"); + + /* Old snapshots stored only setloc_seek_far in this slot. The value + * 1 must still decode as far=true, pending=false. */ + cdrom_test_set_setloc_state(1, 0); + cdrom_snapshot_write(snapshot); + cdrom_test_set_setloc_state(0, 1); + CHECK(cdrom_snapshot_read(snapshot, snapshot_size) == 1, + "snapshot parser accepts the old Setloc field value"); + CHECK(cdrom_test_get_setloc_state() == 1, + "old Setloc value restores without a pending seek"); free(snapshot); } From d43442bfe7f66ecc9f416c930321e2e10653bda1 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Mon, 31 Aug 2026 02:14:03 +0300 Subject: [PATCH 4/4] Clarify CD-ROM test fixture usage --- runtime/tests/test_cdrom_seek_semantics.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/tests/test_cdrom_seek_semantics.c b/runtime/tests/test_cdrom_seek_semantics.c index 918b37bd..a98ae0b3 100644 --- a/runtime/tests/test_cdrom_seek_semantics.c +++ b/runtime/tests/test_cdrom_seek_semantics.c @@ -16,8 +16,9 @@ uint32_t g_debug_current_func_addr; uint32_t g_debug_last_store_pc; uint64_t s_frame_count; -/* cdrom.c is compiled as one production translation unit. These inert host - * edges satisfy its linker contract; the test does not execute them. */ +/* cdrom.c is compiled as one production translation unit. Most host edges are + * inert linker stubs. iso_read_sector is the deliberate exception: the + * implicit-seek sequence uses it to complete the target-sector delivery. */ void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; } void event_ring_record(uint16_t kind, uint8_t detail) { (void)kind; (void)detail; } void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux) {