From 7d2663bf73c03b720ebd2d043abc2a6b0f6489a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 21 Oct 2026 16:02:09 +0200 Subject: [PATCH] VSH: Insert and eject UMDs from the pause screen While the XMB is running, the pause screen offers "Insert UMD" with the drive empty, opening the Switch UMD game picker, and "Eject UMD" with a disc in. The XMB notices the change through its drive callback, as it does over the debugger's game.umd.insert/eject. Games keep "Switch UMD". Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/sceKernelModule.cpp | 4 ++++ Core/HLE/sceKernelModule.h | 2 ++ UI/MainScreen.cpp | 15 +++++++++++++-- UI/MainScreen.h | 5 +++++ UI/PauseScreen.cpp | 15 ++++++++++++++- assets/lang/en_US.ini | 2 ++ 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index c580468901..411d8e20df 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -542,6 +542,10 @@ u32 KernelFindModuleImport(const char *moduleName, const char *libraryName, u32 // than a game), and reset on the next __KernelLoadExec. See ShouldHLEModuleForLoad below. static bool g_runningVSH = false; +bool __KernelIsRunningVSH() { + return g_runningVSH; +} + // A sceKernelLoadModule whose caller is waiting for the file to be read. The module is only put // in memory when the wait ends - see sceKernelLoadModule. struct PendingModuleLoad { diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index c50c92ca6e..0490a1aa70 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -242,6 +242,8 @@ u32 __KernelGetModuleGP(SceUID module); bool KernelModuleIsKernelMode(SceUID module); bool __KernelLoadGEDump(MIPSState *mips, std::string_view base_filename, std::string *error_string); bool __KernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr, std::string *error_string); +// The VSH (the XMB) is what's running, not a game it launched. +bool __KernelIsRunningVSH(); // Exposed so sceVshBridge can reuse it directly for vshKernelLoadModuleBufferVSH, matching JPCSP. SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr); bool __KernelLoadExecFromBuffer(MIPSState *mips, const u8 *data, size_t size, u32 paramPtr, std::string *error_string); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index b8ce7dba2b..305ae3461a 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -906,7 +906,7 @@ void UmdReplaceScreen::CreateViews() { rightColumnItems->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Add([&](UI::EventParams &e) { auto mm = GetI18NCategory(I18NCat::MAINMENU); System_BrowseForFile(GetRequesterToken(), mm->T("Load"), BrowseFileType::BOOTABLE, [this](std::string_view value, int) { - __UmdReplace(Path(value)); + UseDisc(Path(value)); TriggerFinish(DR_OK); }); }); @@ -933,10 +933,21 @@ void UmdReplaceScreen::update() { } void UmdReplaceScreen::OnGameSelected(UI::EventParams &e) { - __UmdReplace(Path(e.s)); + UseDisc(Path(e.s)); TriggerFinish(DR_OK); } +void UmdReplaceScreen::UseDisc(const Path &path) { + if (!insert_) { + __UmdReplace(path); + return; + } + std::string error; + if (!__UmdInsert(path, &error)) { + g_OSD.Show(OSDType::MESSAGE_ERROR, error); + } +} + void UmdReplaceScreen::OnGameSettings(UI::EventParams &e) { screenManager()->push(new GameSettingsScreen(Path())); } diff --git a/UI/MainScreen.h b/UI/MainScreen.h index 688edbd029..bf6f6dc4f3 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -103,6 +103,8 @@ private: class UmdReplaceScreen : public UIBaseDialogScreen { public: + // insert: put a disc into the empty drive (the VSH) rather than swap the one a game is running from. + explicit UmdReplaceScreen(bool insert = false) : insert_(insert) {} const char *tag() const override { return "UmdReplace"; } protected: @@ -112,6 +114,9 @@ protected: private: void OnGameSelected(UI::EventParams &e); void OnGameSettings(UI::EventParams &e); + void UseDisc(const Path &path); + + bool insert_; }; class GridSettingsPopupScreen : public UI::PopupScreen { diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index f5d64c3248..fc1c75d7d5 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -46,6 +46,7 @@ #include "Core/RetroAchievements.h" #include "Core/ELF/ParamSFO.h" #include "Core/HLE/sceDisplay.h" +#include "Core/HLE/sceKernelModule.h" #include "Core/HLE/sceUmd.h" #include "Core/HLE/sceNet.h" #include "Core/HLE/sceNetInet.h" @@ -712,7 +713,19 @@ void GamePauseScreen::CreateViews() { buttonColumn->Add(rightColumnItems); rightColumnItems->SetSpacing(0.0f); - if (getUMDReplacePermit()) { + if (__KernelIsRunningVSH()) { + // The XMB notices discs coming and going by itself, like a PSP's. + if (__UmdIsInserted()) { + rightColumnItems->Add(new Choice(pa->T("Eject UMD"), ImageID("I_UMD")))->OnClick.Add([this](UI::EventParams &) { + __UmdEject(); + RecreateViews(); + }); + } else { + rightColumnItems->Add(new Choice(pa->T("Insert UMD"), ImageID("I_UMD")))->OnClick.Add([this](UI::EventParams &) { + screenManager()->push(new UmdReplaceScreen(true)); + }); + } + } else if (getUMDReplacePermit()) { rightColumnItems->Add(new Choice(pa->T("Switch UMD"), ImageID("I_UMD")))->OnClick.Add([this](UI::EventParams &) { screenManager()->push(new UmdReplaceScreen()); }); diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index 52ce8d0e65..ee3673dbf6 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -1075,8 +1075,10 @@ Cheats = Cheats Continue = Continue Create Game Config = Create game config Delete Game Config = Delete game config +Eject UMD = Eject UMD Exit to menu = Exit to menu Game Settings = Game settings +Insert UMD = Insert UMD Load State = Load state Rewind = Rewind Save State = Save state