mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-22 10:49:05 +02:00
VSH: An empty UMD drive, and inserting a disc over the WebSocket debugger
The XMB, booted without a game, now starts with an empty drive: when vsh_module loads with nothing at disc0:, the drive is marked empty, and activating it tells the shell's drive callback (SceVshMediaDetectUMD) there's no disc. Activation used to report a readable disc regardless, so the shell asked for its type and showed "The disc could not be read." The disc type devctl (0x01F20001) answers 0 without a disc, as JPCSP does. game.umd.insert (path) mounts a disc image into the empty drive and calls the drive callback as inserting one does; game.umd.eject takes it out. UmdMount() is UmdReplace() without its need for a disc already in. Inserted into the 6.61 XMB, Daxter shows in the Game column with its icon, background and animated preview, and launching it boots the game to its title menu. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b225c33639
commit
eee153b220
10 files changed
+124
-10
No files matched your search
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
+2
-1
@@ -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);
|
||||
|
||||
@@ -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!
|
||||
|
||||
+41
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
+4
-1
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
Reference in new issue
Block a user