diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index 5a84b1524b..b430264df8 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -40,6 +40,7 @@ #include "Core/Debugger/WebSocket/WebSocketUtils.h" #include "Core/ELF/ParamSFO.h" #include "Core/HLE/sceDisplay.h" +#include "Core/HLE/sceUmd.h" #include "Core/RetroAchievements.h" #include "Core/SaveState.h" #include "Core/System.h" @@ -245,6 +246,8 @@ DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map) { map["game.state.verify"] = [p](DebuggerRequest &req) { p->StateOperation(req, StateOp::VERIFY); }; map["game.speed.get"] = &WebSocketGameSpeedGet; map["game.speed.set"] = &WebSocketGameSpeedSet; + map["game.umd.insert"] = &WebSocketGameUmdInsert; + map["game.umd.eject"] = &WebSocketGameUmdEject; map["version"] = &WebSocketVersion; return p; @@ -468,3 +471,52 @@ void WebSocketVersion(DebuggerRequest &req) { else json.writeString("path", path); } + +// Put a disc in the drive (game.umd.insert) +// +// For the XMB, booted with --vsh and no game, whose drive starts out empty. The shell gets the +// drive's callback, as on a PSP. To swap discs in a running game, use the Switch UMD menu. +// +// Parameters: +// - path: string, the disc image (ISO, CSO, ...) or extracted disc directory to insert. +// +// Response (same event name) with no extra data, or error if a disc is already in the drive or the +// image can't be mounted. +void WebSocketGameUmdInsert(DebuggerRequest &req) { + std::string path; + if (!req.ParamString("path", &path)) + return; + + bool running = false; + bool inserted = false; + std::string error; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + if (running) + inserted = __UmdInsert(Path(path), &error); + }); + + if (!running) + return req.Fail("Game not running"); + if (!inserted) + return req.Fail("Couldn't insert the disc: " + error); + req.Respond(); +} + +// Take the disc out of the drive (game.umd.eject) +// +// No parameters. +// +// Response (same event name) with no extra data. Ejecting an empty drive does nothing. +void WebSocketGameUmdEject(DebuggerRequest &req) { + bool running = false; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + if (running) + __UmdEject(); + }); + + if (!running) + return req.Fail("Game not running"); + req.Respond(); +} diff --git a/Core/Debugger/WebSocket/GameSubscriber.h b/Core/Debugger/WebSocket/GameSubscriber.h index 68943c1af7..d5807d1b6b 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.h +++ b/Core/Debugger/WebSocket/GameSubscriber.h @@ -25,4 +25,6 @@ void WebSocketGameReset(DebuggerRequest &req); void WebSocketGameStatus(DebuggerRequest &req); void WebSocketGameSpeedGet(DebuggerRequest &req); void WebSocketGameSpeedSet(DebuggerRequest &req); +void WebSocketGameUmdInsert(DebuggerRequest &req); +void WebSocketGameUmdEject(DebuggerRequest &req); void WebSocketVersion(DebuggerRequest &req); diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index 74b523ae0f..2c34e5fecf 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -1766,7 +1766,8 @@ static u32 sceIoDevctl(const char *name, int cmd, u32 argAddr, int argLen, u32 o case 0x01F20001: // Get UMD disc type if (Memory::IsValid4AlignedRange(outPtr, 8) && outLen >= 8) { - Memory::WriteUnchecked_U32(0x10, outPtr + 4); // Always return game disc (if present) + // A game disc, or none in an empty drive (as JPCSP answers without a disc). + Memory::WriteUnchecked_U32(__UmdIsInserted() ? PSP_UMD_TYPE_GAME : 0, outPtr + 4); return hleLogDebug(Log::sceIo, 0); } else { return hleLogError(Log::sceIo, SCE_ERROR_MEMSTICK_DEVCTL_BAD_PARAMS); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index c55a59e229..c580468901 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -73,6 +73,7 @@ #include "Core/HLE/scePsmf.h" #include "Core/HLE/sceAtrac.h" #include "Core/HLE/sceIo.h" +#include "Core/HLE/sceUmd.h" #include "Core/HLE/KernelWaitHelpers.h" #include "Core/ELF/ParamSFO.h" @@ -1897,6 +1898,10 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load if ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0 || equals(modinfo->name, "vsh_module")) { g_runningVSH = true; INFO_LOG(Log::sceModule, "VSH mode module detected: %s", modinfo->name); + // The shell booted without a game: its drive starts out empty, for a disc to go in later. + if (!pspFileSystem.GetSystem("disc0:")) { + __UmdEject(); + } } // OK, even if it's an ELF module, it might be one we shouldn't fully load and execute! diff --git a/Core/HLE/sceUmd.cpp b/Core/HLE/sceUmd.cpp index fec369bbc0..8da4ad0579 100644 --- a/Core/HLE/sceUmd.cpp +++ b/Core/HLE/sceUmd.cpp @@ -177,6 +177,10 @@ static void __KernelUmdActivate() if (sceKernelGetCompiledSdkVersion() != 0) { notifyArg |= PSP_UMD_READY; } + // An empty drive (the XMB's) says so: the shell otherwise asks for the disc and reports it unreadable. + if (!UMDInserted) { + notifyArg = PSP_UMD_NOT_PRESENT; + } if (driveCBId != 0) __KernelNotifyCallback(driveCBId, notifyArg); @@ -187,7 +191,7 @@ static void __KernelUmdActivate() static void __KernelUmdDeactivate() { - u32 notifyArg = PSP_UMD_PRESENT | PSP_UMD_READY; + u32 notifyArg = UMDInserted ? PSP_UMD_PRESENT | PSP_UMD_READY : PSP_UMD_NOT_PRESENT; if (driveCBId != 0) __KernelNotifyCallback(driveCBId, notifyArg); @@ -506,6 +510,42 @@ void __UmdReplace(const Path &filepath) { __KernelNotifyCallback(driveCBId, notifyArg); } +bool __UmdInsert(const Path &filepath, std::string *error) { + if (UMDInserted) { + *error = "a disc is already in the drive"; + return false; + } + FileLoader *fileLoader; + if (!UmdMount(filepath, &fileLoader, *error)) { + return false; + } + Achievements::ChangeUMD(filepath, fileLoader); + + UMDInserted = true; + UmdWakeThreads(); + if (driveCBId != 0) + __KernelNotifyCallback(driveCBId, PSP_UMD_PRESENT | PSP_UMD_READABLE | PSP_UMD_CHANGED); + return true; +} + +void __UmdEject() { + if (!UMDInserted) { + return; + } + UMDInserted = false; + pspFileSystem.Unmount("umd0:"); + pspFileSystem.Unmount("umd1:"); + pspFileSystem.Unmount("umd:"); + pspFileSystem.Unmount("disc0:"); + UmdWakeThreads(); + if (driveCBId != 0) + __KernelNotifyCallback(driveCBId, PSP_UMD_NOT_PRESENT | PSP_UMD_CHANGED); +} + +bool __UmdIsInserted() { + return UMDInserted; +} + bool getUMDReplacePermit() { return g_UMDReplacePermit; } diff --git a/Core/HLE/sceUmd.h b/Core/HLE/sceUmd.h index 7d45925d8e..165f7e3153 100644 --- a/Core/HLE/sceUmd.h +++ b/Core/HLE/sceUmd.h @@ -40,6 +40,10 @@ void __UmdInit(); void __UmdDoState(PointerWrap &p); void __UmdReplace(const Path &filepath); +// A drive that can be empty: the XMB's, booted without a game. Insert fails if a disc is in. +bool __UmdInsert(const Path &filepath, std::string *error); +void __UmdEject(); +bool __UmdIsInserted(); bool getUMDReplacePermit(); void Register_sceUmdUser(); diff --git a/Core/Loaders.cpp b/Core/Loaders.cpp index 6b13248cc1..095dd3b321 100644 --- a/Core/Loaders.cpp +++ b/Core/Loaders.cpp @@ -353,7 +353,10 @@ bool UmdReplace(const Path &filepath, FileLoader **fileLoader, std::string &erro error = "has no disc"; return false; } + return UmdMount(filepath, fileLoader, error); +} +bool UmdMount(const Path &filepath, FileLoader **fileLoader, std::string &error) { FileLoader *loadedFile = ConstructFileLoader(filepath); if (!loadedFile || !loadedFile->Exists()) { @@ -374,7 +377,7 @@ bool UmdReplace(const Path &filepath, FileLoader **fileLoader, std::string &erro case IdentifiedFileType::PSP_ISO_NP: case IdentifiedFileType::PSP_DISC_DIRECTORY: if (!MountGameISO(loadedFile, &error)) { - error = "mounting the replaced ISO failed: " + error; + error = "mounting the ISO failed: " + error; return false; } break; diff --git a/Core/Loaders.h b/Core/Loaders.h index 13108bef46..acd696f6b3 100644 --- a/Core/Loaders.h +++ b/Core/Loaders.h @@ -163,6 +163,8 @@ Path ResolvePBPFile(const Path &filename); IdentifiedFileType Identify_File(FileLoader *fileLoader, std::string *errorString); bool UmdReplace(const Path &filepath, FileLoader **fileLoader, std::string &error); +// Mounts a disc image at umd0:, umd1:, umd: and disc0:, whether or not one was there. +bool UmdMount(const Path &filepath, FileLoader **fileLoader, std::string &error); enum class ArchiveType { ZIP, diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md index 76d4a882ca..1db9dbfedb 100644 --- a/docs/VSHBootInvestigation.md +++ b/docs/VSHBootInvestigation.md @@ -35,12 +35,17 @@ code is not correct." Cancel it with circle and the menu is there and usable. Two separate things: -- **It thinks a disc is inserted**, because nothing in PPSSPP models an empty drive. - `sceUmd.cpp` reports `PSP_UMD_PRESENT | PSP_UMD_READY` unconditionally, and `sceIoDevctl` - `0x01F20001` (get disc type) always answers `0x10`, "game disc". JPCSP answers `0` when no ISO is - loaded, which is why it never asks the next question. Fixing this properly means giving those two - a notion of "no disc" - low risk for games, which always have one, but it is on a path every game - uses. +- **It thought a disc was inserted**, because nothing in PPSSPP modelled an empty drive. Now a shell + booted without a game starts with an empty drive: `sceUmdActivate` tells the shell's drive callback + (`SceVshMediaDetectUMD`) there's no disc, and `sceIoDevctl` `0x01F20001` (get disc type) answers `0`, + as JPCSP does. Activation used to report a readable disc regardless, so the shell asked for its type, + got nothing it knew, and showed "The disc could not be read." + + A disc goes in with the WebSocket debugger's `game.umd.insert` (`path`), which mounts the image + and calls the drive callback the way a PSP does when a disc is inserted, and comes out with + `game.umd.eject`. The shell then lists the disc in the Game column with its icon, background and + animated preview, and launching it boots the game through `sceKernelLoadExecVSHDisc` (Daxter on + 6.61, to its title menu). - **It then asked whether the region matched and we didn't answer.** `sceIoDevctl` `0x01E18030` is the region check, and unusually it answers through the *return value* (1 matches, 0 doesn't) rather than an output buffer. It was unimplemented, so the VSH read the failure as a mismatch and diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 9d38871071..2258345e4a 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -271,7 +271,7 @@ file - this is just an index. | Category | Events | File | |---|---|---| -| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file given as `path`, or a `slot` of the running game, applied at the end of the next frame the CPU runs; with `wait: true` the response comes once it's done, or as an error if it failed), `game.state.save` (the same for saving one), `game.state.rewind` (the same for the newest rewind snapshot, which it keeps, so only a rewind within 0.8 seconds of the last steps further back; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how far back that is in emulated time), `game.state.verify` (the same for checking that saving the current state and reading it back agree), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `version` | `GameSubscriber.cpp` | +| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file given as `path`, or a `slot` of the running game, applied at the end of the next frame the CPU runs; with `wait: true` the response comes once it's done, or as an error if it failed), `game.state.save` (the same for saving one), `game.state.rewind` (the same for the newest rewind snapshot, which it keeps, so only a rewind within 0.8 seconds of the last steps further back; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how far back that is in emulated time), `game.state.verify` (the same for checking that saving the current state and reading it back agree), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `game.umd.insert` / `game.umd.eject` (put a disc image, given as `path`, in an empty drive, or take it out: for the XMB booted with `--vsh` and no game, which then lists the disc and can launch it), `version` | `GameSubscriber.cpp` | | CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | | Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` | | Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` |