cdrom: model timed tray cycle for reinsert

This commit is contained in:
Alexandros Mandravillis
2026-08-30 23:24:47 +03:00
parent 47bda8172e
commit fb02451aab
6 changed files with 214 additions and 17 deletions
+7
View File
@@ -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,
+3
View File
@@ -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
+59 -17
View File
@@ -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));
}
+55
View File
@@ -0,0 +1,55 @@
#ifndef PSXRECOMP_CDROM_LID_H
#define PSXRECOMP_CDROM_LID_H
#include <stdint.h>
/* 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
@@ -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")
+47
View File
@@ -0,0 +1,47 @@
/* Validate the guest-visible CD tray interval used for disc reinsertion. */
#include "cdrom_lid.h"
#include <stdio.h>
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;
}