From fb02451aab77be47720fe0ac6e7c221aa264c894 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Sun, 30 Aug 2026 23:24:47 +0300 Subject: [PATCH] cdrom: model timed tray cycle for reinsert --- runtime/CMakeLists.txt | 7 ++ runtime/include/cdrom.h | 3 + runtime/src/cdrom.c | 76 ++++++++++++++++----- runtime/src/cdrom_lid.h | 55 +++++++++++++++ runtime/tests/test_cdrom_lid_integration.py | 43 ++++++++++++ runtime/tests/test_cdrom_lid_state.c | 47 +++++++++++++ 6 files changed, 214 insertions(+), 17 deletions(-) create mode 100644 runtime/src/cdrom_lid.h create mode 100644 runtime/tests/test_cdrom_lid_integration.py create mode 100644 runtime/tests/test_cdrom_lid_state.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 2494923a..3b0fe57e 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -330,6 +330,10 @@ 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_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) + # 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. @@ -504,6 +508,9 @@ if(BUILD_TESTING) add_test(NAME pal_cadence_host_refresh_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_pal_cadence_host_refresh.py) + add_test(NAME cdrom_lid_integration_test + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cdrom_lid_integration.py) endif() # Full card protocol regression: read, write, address echo, per-slot state, diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index d8bc5da3..df5cb346 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -67,6 +67,9 @@ typedef struct CdTimingPub { } CdTimingPub; uint64_t cdrom_timing_total(void); int cdrom_timing_record(uint64_t seq, CdTimingPub* out); +/* Notify the guest that the mounted disc was reinserted. The controller stops + * active transfers, reports an open shell, waits two emulated seconds, then + * makes the mounted media readable. This call does not mount a different image. */ void debug_force_cd_reinsert(void); /* FMV auto-skip detection: cdrom_xa_stream_active() lets the frontend detect * that streaming XA (FMV/CDDA) is in progress. The skip itself is done by the diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 59529a42..a34d7016 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -12,6 +12,7 @@ #include "cdrom.h" #include "cdrom_irq.h" +#include "cdrom_lid.h" #include "dma.h" #include "spu.h" #include "event_ring.h" @@ -61,6 +62,8 @@ static uint8_t stat_reg; static uint8_t request_reg; static uint8_t irq_enable; static uint8_t irq_flag; +static CdromLidState s_lid; +static int s_lid_irq_pending; /* Disc license region string returned in GetID's last four response bytes * ("SCEE" PAL / "SCEA" NTSC-U / "SCEI" NTSC-J). Real hardware reports the @@ -350,6 +353,10 @@ void cdrom_resync_deadlines_after_restore(void) s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay; else if (!reading) s_cd_timing_next_due = 0; + /* Disc reinsertion is disabled in netplay. Reset the host-side lid timer + * when a rollback snapshot restores the shared controller wire state. */ + cdrom_lid_reset(&s_lid); + s_lid_irq_pending = 0; /* pending.due_cyc / cdrom_irq_present_due rebuilt in snap_parse from * relative remaining (same guest clock as the restore). */ } @@ -870,7 +877,7 @@ static void record_sector_history(int lba, int size, uint8_t mode, int have_raw, } static int has_disc(void) { - return iso_handle != NULL; + return cdrom_lid_media_ready(&s_lid, iso_handle != NULL); } /* CD status bits */ @@ -917,6 +924,34 @@ static void set_irq(int type) { event_ring_record_aux(EV_DEQ, (uint8_t)SRC_CD_IRQ, (uint32_t)type); } +static void fire_cdrom_irq(void); + +static void present_lid_open_irq_if_ready(void) +{ + if (!s_lid_irq_pending || irq_flag != 0) + return; + stat_reg = CDSTAT_ERROR | CDSTAT_SHELL; + response_clear(); + response_push(stat_reg); + response_push(0x08); /* shell open */ + set_irq(CDIRQ_ERROR); + fire_cdrom_irq(); + s_lid_irq_pending = 0; +} + +static void process_lid_state(void) +{ + present_lid_open_irq_if_ready(); + if (!s_lid_irq_pending && cdrom_lid_close_if_due(&s_lid, psx_cycle_count)) { + stat_reg &= (uint8_t)~(CDSTAT_ERROR | CDSTAT_READ | CDSTAT_PLAY | + CDSTAT_SEEK); + if (iso_handle) + stat_reg |= CDSTAT_MOTOR; + stat_reg |= CDSTAT_SHELL; + trace_cdrom('w', 0, iso_handle ? 1u : 0u, 0); + } +} + /* Present the current CD INT to the CPU interrupt controller exactly once per * generation. Re-presentation of the same unacked INT is suppressed by the * latch (this is what stops the CPS exception re-entry storm). The next @@ -2043,6 +2078,8 @@ static void exec_command(uint8_t cmd) { stat_reg |= CDSTAT_SHELL; } response_push(stat_reg); + if (cdrom_lid_acknowledge_closed_shell(&s_lid)) + stat_reg &= (uint8_t)~CDSTAT_SHELL; set_irq(CDIRQ_ACK); break; @@ -2721,6 +2758,8 @@ void cdrom_init(const char* cue_path) { request_reg = 0; irq_enable = 0x1F; irq_flag = 0; + cdrom_lid_reset(&s_lid); + s_lid_irq_pending = 0; cdrom_intc_request_latched = 0; cdrom_irq_generation = 0; cdrom_intc_latched_generation = 0; @@ -2906,6 +2945,7 @@ void cdrom_write(uint32_t addr, uint32_t value) { irq_flag &= ~(val & 0x1F); if (had_active_irq && (irq_flag & 0x1F) == 0) { cdrom_intc_request_latched = 0; + present_lid_open_irq_if_ready(); } if (val & 0x40) { param_count = 0; @@ -2971,6 +3011,7 @@ void cdrom_advance(uint32_t cycles) { * it. Note: do NOT freeze CD during rollback Replay. MotK FMV skip resim * resumes into VLC/sector waits; frozen IRQs → no finish_frame hang. * Menu CD asymmetry is contained by no-invent during media + core POST. */ + process_lid_state(); refresh_cdrom_irq_line(); process_pending(cycles); try_execute_queued_command(); @@ -2983,6 +3024,7 @@ void cdrom_advance(uint32_t cycles) { } process_read_stream(cycles); process_cdda_stream(cycles); + process_lid_state(); refresh_cdrom_irq_line(); } @@ -3382,30 +3424,30 @@ int cdrom_snapshot_read(const uint8_t *p, uint32_t len) { /* Absolute host deadlines are not on the wire — rebuild from restored * relative read_delay (psx_cycle_count is resynced by the load caller). */ cdrom_resync_deadlines_after_restore(); + /* The snapshot wire predates the timed-lid helper. Reconstruct its short + * host-side deadline from the saved hardware status instead of changing + * the section size and invalidating existing save states. */ + if (iso_handle && (stat_reg & (CDSTAT_ERROR | CDSTAT_SHELL)) == + (CDSTAT_ERROR | CDSTAT_SHELL)) { + cdrom_lid_begin_open(&s_lid, psx_cycle_count); + } else if (iso_handle && (stat_reg & CDSTAT_SHELL)) { + s_lid.shell_open_latched = 1; + } return 1; } void debug_force_cd_reinsert(void) { - // Simulamos la apertura de la bandeja borrando los flujos actuales + /* Stop the old transfer before exposing a physical tray-open event. */ stop_read_stream(); stop_cdda_playback(); xa_reset_decode(); spu_cd_audio_reset(); - // Forzamos el estado de la lectora a "Bandeja Abierta" temporalmente - stat_reg = CDSTAT_SHELL; + stat_reg = CDSTAT_ERROR | CDSTAT_SHELL; cdrom_clear_pending_dataready(); - response_clear(); - - // Forzamos al emulador a reinicializar el lector con el archivo de disco actual - if (iso_handle) { - stat_reg = CDSTAT_MOTOR; // Volvemos a encender el motor virtual - } - - // Emitimos una interrupción de ACK para despertar al kernel del juego - set_irq(CDIRQ_ACK); - fire_cdrom_irq(); - - // Forzamos la ejecución de cualquier comando atascado en cola - try_execute_queued_command(); + cdrom_lid_begin_open(&s_lid, psx_cycle_count); + s_lid_irq_pending = 1; + present_lid_open_irq_if_ready(); + trace_cdrom('O', 0, (uint32_t)CDROM_LID_CLOSE_DELAY_CYCLES, + (uint32_t)(CDROM_LID_CLOSE_DELAY_CYCLES >> 32)); } diff --git a/runtime/src/cdrom_lid.h b/runtime/src/cdrom_lid.h new file mode 100644 index 00000000..45a3416b --- /dev/null +++ b/runtime/src/cdrom_lid.h @@ -0,0 +1,55 @@ +#ifndef PSXRECOMP_CDROM_LID_H +#define PSXRECOMP_CDROM_LID_H + +#include + +/* The PS1 CPU clock is 33.8688 MHz. Keep inserted media hidden from the + * guest for two seconds so software can observe a physical open/close cycle. */ +#define CDROM_LID_CLOSE_DELAY_CYCLES (33868800ull * 2ull) + +typedef struct CdromLidState { + uint64_t close_due; + uint8_t physical_open; + uint8_t shell_open_latched; +} CdromLidState; + +static inline void cdrom_lid_reset(CdromLidState *state) +{ + state->close_due = 0; + state->physical_open = 0; + state->shell_open_latched = 0; +} + +static inline void cdrom_lid_begin_open(CdromLidState *state, uint64_t now) +{ + state->close_due = now + CDROM_LID_CLOSE_DELAY_CYCLES; + state->physical_open = 1; + state->shell_open_latched = 1; +} + +static inline int cdrom_lid_media_ready(const CdromLidState *state, + int host_media_present) +{ + return host_media_present && !state->physical_open; +} + +static inline int cdrom_lid_close_if_due(CdromLidState *state, uint64_t now) +{ + if (!state->physical_open || now < state->close_due) + return 0; + state->physical_open = 0; + state->close_due = 0; + return 1; +} + +/* GetStat returns the latched ShellOpen bit once after the physical lid has + * closed. The next GetStat can report the inserted disc normally. */ +static inline int cdrom_lid_acknowledge_closed_shell(CdromLidState *state) +{ + if (state->physical_open || !state->shell_open_latched) + return 0; + state->shell_open_latched = 0; + return 1; +} + +#endif diff --git a/runtime/tests/test_cdrom_lid_integration.py b/runtime/tests/test_cdrom_lid_integration.py new file mode 100644 index 00000000..38eda4bf --- /dev/null +++ b/runtime/tests/test_cdrom_lid_integration.py @@ -0,0 +1,43 @@ +"""Guard the CD controller wiring for the timed tray cycle.""" + +from pathlib import Path +import re + + +SOURCE = (Path(__file__).parents[1] / "src" / "cdrom.c").read_text( + encoding="utf-8" +) + + +def function_body(name: str) -> str: + match = re.search(rf"\b{name}\s*\([^)]*\)\s*\{{", SOURCE) + assert match, f"missing function: {name}" + start = match.end() + depth = 1 + cursor = start + while cursor < len(SOURCE) and depth: + if SOURCE[cursor] == "{": + depth += 1 + elif SOURCE[cursor] == "}": + depth -= 1 + cursor += 1 + assert depth == 0, f"unterminated function: {name}" + return SOURCE[start : cursor - 1] + + +reinsert = function_body("debug_force_cd_reinsert") +assert "cdrom_lid_begin_open" in reinsert +assert "s_lid_irq_pending = 1" in reinsert +assert "set_irq(CDIRQ_ACK)" not in reinsert + +present = function_body("present_lid_open_irq_if_ready") +assert "response_push(0x08)" in present +assert "set_irq(CDIRQ_ERROR)" in present + +has_disc = function_body("has_disc") +assert "cdrom_lid_media_ready" in has_disc + +advance = function_body("cdrom_advance") +assert advance.count("process_lid_state()") >= 2 + +print("PASS: timed CD lid integration guards") diff --git a/runtime/tests/test_cdrom_lid_state.c b/runtime/tests/test_cdrom_lid_state.c new file mode 100644 index 00000000..1b2102e7 --- /dev/null +++ b/runtime/tests/test_cdrom_lid_state.c @@ -0,0 +1,47 @@ +/* Validate the guest-visible CD tray interval used for disc reinsertion. */ +#include "cdrom_lid.h" + +#include + +static int failures; + +#define CHECK(expr, label) do { \ + if (!(expr)) { \ + fprintf(stderr, "FAIL: %s\n", (label)); \ + failures++; \ + } \ +} while (0) + +int main(void) +{ + CdromLidState lid; + const uint64_t start = 1234567ull; + + cdrom_lid_reset(&lid); + CHECK(cdrom_lid_media_ready(&lid, 1), "mounted disc starts readable"); + + cdrom_lid_begin_open(&lid, start); + CHECK(lid.physical_open, "reinsert opens the physical lid"); + CHECK(lid.shell_open_latched, "reinsert latches ShellOpen"); + CHECK(!cdrom_lid_media_ready(&lid, 1), + "mounted image stays hidden while the lid is open"); + CHECK(!cdrom_lid_close_if_due( + &lid, start + CDROM_LID_CLOSE_DELAY_CYCLES - 1), + "lid remains open for the complete delay"); + CHECK(cdrom_lid_close_if_due( + &lid, start + CDROM_LID_CLOSE_DELAY_CYCLES), + "lid closes exactly at the deadline"); + CHECK(cdrom_lid_media_ready(&lid, 1), + "mounted image becomes readable after the lid closes"); + CHECK(cdrom_lid_acknowledge_closed_shell(&lid), + "first closed-lid GetStat consumes the ShellOpen latch"); + CHECK(!cdrom_lid_acknowledge_closed_shell(&lid), + "later GetStat calls do not repeat ShellOpen"); + + if (failures) { + fprintf(stderr, "FAILED (%d)\n", failures); + return 1; + } + puts("ALL PASS"); + return 0; +}