From fdfad8f56796890084bb843ba3454e69e55980e1 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 25 Jul 2026 00:52:55 -0700 Subject: [PATCH] cdrom: fail loudly when a requested disc does not mount Root cause of the "black screen on a .cue that works as a .bin" reports. cdrom_init() calls iso_open() and, on failure, simply leaves the drive empty (stat_reg = CDSTAT_SHELL). That is correct for a BIOS-only target, which runs with no disc on purpose -- but when a game was GIVEN a disc path and the image does not mount, the game boots against an empty drive and renders nothing. No error, no log, just black. The earlier validate_disc_for_launch() pass cannot catch this, because the two sides read the disc differently: identify_disc() opens the data-track FILE directly, so it reported a green "Disc verified" badge for cues that the MOUNTING reader then rejected. Verified-but-unmountable was a silent state. Demonstrated against the pre-fix reader (a8aa640) on one cue: lowercase/unquoted cue -> FAILS (empty drive -> black screen) canonical cue -> MOUNTS bare .bin -> MOUNTS <- why picking the .bin "fixed" it So the symptom was never about cue-vs-bin semantics; it was cue PARSING strictness. The old mount-side parser required the literal uppercase "BINARY" and a quoted filename, while the verify side uppercased the line first. The shared cue parser landed in 68667cd already fixes that class (case-insensitive keywords, unquoted names, CRLF, case-insensitive payload lookup), which is why all four titles now boot from either file. This commit closes the remaining hole -- any OTHER reason a mount fails must not present as a black screen: - cdrom_has_disc() exposes the drive state (has_disc() was file-static, so no caller could tell a successful mount from an empty drive). - main() checks it right after cdrom_init() and, when a disc path was supplied but nothing mounted, reports what was selected, what it was mounted as, the resolver's note, and that the .bin can be selected directly -- then exits instead of booting into black. Also pins the regression: the lowercase/unquoted cue must MOUNT, not merely parse, so the verify side and the mount side can never silently disagree again. test_disc_path_resolve: 36 -> 38 checks. Co-Authored-By: Claude Opus 5 (1M context) --- runtime/include/cdrom.h | 9 +++++++++ runtime/src/cdrom.c | 4 ++++ runtime/src/main.cpp | 23 +++++++++++++++++++++++ runtime/tests/test_disc_path_resolve.cpp | 16 ++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index 388ae9c9..7ef03f47 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -8,6 +8,15 @@ extern "C" { #endif void cdrom_init(const char* cue_path); + +/* Did cdrom_init() actually mount an image? 0 = the drive is empty. + * + * cdrom_init() is deliberately non-fatal when iso_open() fails (a BIOS-only + * target legitimately runs with no disc), so a game that was GIVEN a disc path + * whose image could not be mounted would otherwise boot straight into an empty + * drive and render nothing -- a black screen with no explanation. Callers that + * passed a real path must check this and report. */ +int cdrom_has_disc(void); /* Disc license region for GetID ("SCEE"/"SCEA"/"SCEI"); derived from the * mounted disc's boot serial at launch. Defaults to "SCEA" (SCPH1001). */ void cdrom_set_disc_scex(const char scex[4]); diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 889fa028..767e7b22 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -2308,6 +2308,10 @@ void cdrom_init(const char* cue_path) { trace_cdrom('N', 0, has_disc() ? 1u : 0u, 0); } +int cdrom_has_disc(void) { + return has_disc(); +} + uint32_t cdrom_read(uint32_t addr) { uint32_t ret = 0; switch (addr) { diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 4f211aab..372348ab 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -5812,6 +5812,29 @@ session_reboot: std::fprintf(stdout, "psxrecomp: SPU float-shadow enabled (verified-enhancement)\n"); spu_init(); cdrom_init(disc_path_str.empty() ? NULL : disc_path_str.c_str()); + + /* A disc was requested but nothing mounted. cdrom_init() is non-fatal here + * (BIOS-only targets run with an empty drive on purpose), so without this + * check the game would boot into an empty drive and render NOTHING -- the + * "black screen on a .cue that works as a .bin" symptom. The earlier + * validate_disc_for_launch() pass cannot catch it: identify_disc() reads + * the data track FILE directly, so a cue whose sheet the mounting reader + * rejects still shows a green "Disc verified" badge. Report the actual + * failure instead of leaving the player staring at black. */ + if (!disc_path_str.empty() && !cdrom_has_disc()) { + const PSXRecompV4::DiscPathResolution r = + PSXRecompV4::resolve_disc_path(disc_path_str); + std::string detail = + "The disc image was found and verified, but the CD-ROM drive could " + "not mount it, so the game would boot with an empty drive.\n\n" + "Selected:\n" + disc_path_str; + if (r.mount != r.picked) detail += "\nMounted as:\n" + r.mount.string(); + if (!r.note.empty()) detail += "\n\n" + r.note; + detail += "\n\nIf this is a .cue, check that every FILE line it names " + "exists next to it; selecting the .bin directly also works."; + launcher_warning("Disc Could Not Be Mounted", detail); + return 1; + } for (const auto& route : warm_cd_routes) { cdrom_register_warm_route(route.arm_lba, route.lbas.data(), (int)route.lbas.size(), diff --git a/runtime/tests/test_disc_path_resolve.cpp b/runtime/tests/test_disc_path_resolve.cpp index 6fd07130..80d2e7ee 100644 --- a/runtime/tests/test_disc_path_resolve.cpp +++ b/runtime/tests/test_disc_path_resolve.cpp @@ -168,6 +168,22 @@ void test_cue_syntax_tolerance(const fs::path& root) { check(lower.usable(), "syntax: lowercase + unquoted + CRLF parses"); check(same(lower.files.front().path, dir / "game.bin"), "syntax: resolves the payload"); + // THE BLACK-SCREEN CASE. The old mount-side parser matched the literal + // uppercase "BINARY" and required a quoted filename, so a cue like the one + // above made ISOReader::Open() return false -- while the verify side + // (identify_disc) uppercased the line, found the payload, and happily + // reported "Disc verified". The launcher therefore showed a green badge and + // the runtime then booted with an EMPTY DRIVE: a black screen that went + // away if you picked the .bin instead. Both sides share one parser now, so + // pin that they agree: this cue must MOUNT, not just parse. + { + PS1::ISOReader r; + check(r.Open((dir / "lower.cue").string()), + "syntax: a cue the verify side accepts must also MOUNT (black-screen regression)"); + check(r.TrackCount() >= 1, "syntax: mounted cue exposes its data track"); + r.Close(); + } + // A WAVE payload has no fixed sector geometry — refuse rather than guess. write_text(dir / "wave.cue", "FILE \"game.bin\" BINARY\n"