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) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-25 00:52:55 -07:00
parent 68667cd58b
commit fdfad8f567
4 changed files with 52 additions and 0 deletions
+9
View File
@@ -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]);
+4
View File
@@ -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) {
+23
View File
@@ -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(),
+16
View File
@@ -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"