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"