From d331bd6c8c9d0080c891081eafddef18efa83fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 23 Oct 2026 10:46:59 +0200 Subject: [PATCH] UI: Play UMD Video discs in the XMB, straight from the game list With firmware 2.71 or later installed, picking a UMD Video disc boots the XMB with the disc in the drive and UMD auto-start forced on for that boot, so the disc's menu comes up. Without one, the info screen saying UMD Video isn't supported shows as before. The info screen's Play does the same. A disc with both a game and video asks which to start. For the video, the XMB comes up with auto-start forced off, since auto-start runs the game. Older firmware can't play these: 1.50, 2.00 and 2.60 crash in video_plugin or paf with a video disc in the drive, and 2.50 refuses it. 2.71 through 6.61 reach Bourne Supremacy's menu. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/CoreParameter.h | 3 ++ Core/HLE/sceReg.cpp | 12 ++++++-- Core/Util/PSARUnpack.cpp | 7 +++++ Core/Util/PSARUnpack.h | 3 ++ UI/EmuScreen.cpp | 6 ++-- UI/EmuScreen.h | 5 +++- UI/GameScreen.cpp | 14 ++++++--- UI/MainScreen.cpp | 58 ++++++++++++++++++++++++++++++++---- UI/MainScreen.h | 3 ++ docs/VSHBootInvestigation.md | 10 +++++++ 10 files changed, 105 insertions(+), 16 deletions(-) diff --git a/Core/CoreParameter.h b/Core/CoreParameter.h index 1fcd5be0cd..efbe80c0e8 100644 --- a/Core/CoreParameter.h +++ b/Core/CoreParameter.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include "Common/File/Path.h" @@ -68,6 +69,8 @@ struct CoreParameter { Path mountIso; // If non-empty, and fileToStart is an ELF or PBP, will mount this ISO in the background to umd1:. Path mountRoot; // If non-empty, and fileToStart is an ELF or PBP, mount this as host0:. + // Overrides the VSH's "UMD auto-start" setting (bVshUmdAutoboot) for this boot. + std::optional umdAutoboot; Path nandRoot; // If non-empty, use this directory as the root above flash0:-flash3: (which are hosted in subdirectories flash0-flash3 of it). diff --git a/Core/HLE/sceReg.cpp b/Core/HLE/sceReg.cpp index 5d1b27c7f0..60c3133000 100644 --- a/Core/HLE/sceReg.cpp +++ b/Core/HLE/sceReg.cpp @@ -9,6 +9,7 @@ #include "Common/Serialize/SerializeMap.h" #include "Common/StringUtils.h" #include "Core/Config.h" +#include "Core/System.h" #define SYSTEM_REGISTRY "/system" #define REG_KEYNAME_SIZE 27 @@ -61,9 +62,14 @@ struct KeyValue { const KeyValue *dirContents; // intValue is the count. // If set, an INT key reads this setting (as 0 or 1), even after the guest writes it: the VSH // rewrites every key with its default at each boot, since category_version is below its schema. - const bool *boolSetting = nullptr; + bool (*boolSetting)() = nullptr; }; +static bool UmdAutoboot() { + // Forced on when a UMD Video disc picked from the game list boots the XMB. + return PSP_CoreParameter().umdAutoboot.value_or(g_Config.bVshUmdAutoboot); +} + // Guest-written registry values (sceRegCreateKey / sceRegSetKeyValue), keyed by category path // (no leading slash, e.g. "DATA/FONT") then key name. The static dump above is read-only, but // plugins like CLANNAD's chfont.prx register their font by creating /DATA/FONT/PROPERTY/INFO18 @@ -768,7 +774,7 @@ static const KeyValue tree_CONFIG_SYSTEM_CHARACTER_SET[] = { static const KeyValue tree_CONFIG_SYSTEM[] = { { "owner_name", ValueType::STR, "P939", 5 }, { "backlight_brightness", ValueType::INT, "", (int)0x3 }, // decimal: 3 - { "umd_autoboot", ValueType::INT, "", (int)0x1, nullptr, &g_Config.bVshUmdAutoboot }, // decimal: 1 + { "umd_autoboot", ValueType::INT, "", (int)0x1, nullptr, &UmdAutoboot }, // decimal: 1 { "usb_charge", ValueType::INT, "", (int)0x1 }, // decimal: 1 { "umd_cache", ValueType::INT, "", (int)0x1 }, // decimal: 1 { "usb_auto_connect", ValueType::INT, "", (int)0x1 }, // decimal: 1 @@ -1096,7 +1102,7 @@ static const MergedCategory &GetMergedCategory(const std::string &path) { for (auto &kv : merged.keyvals) { if (kv.boolSetting) { - kv.intValue = *kv.boolSetting ? 1 : 0; + kv.intValue = kv.boolSetting() ? 1 : 0; } } diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 15d5d758a8..0fc1526b5d 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1356,6 +1356,13 @@ bool FirmwareVersionSupportsVSH(std::string_view version) { return FirmwareVersionToInt(version) >= 150; } +bool FirmwareVersionPlaysUmdVideo(std::string_view version) { + // Measured by booting Bourne Supremacy with auto-start on: 2.71 through 6.61 reach its disc menu. + // 1.50, 2.00 and 2.60 crash in video_plugin or paf as soon as the disc is in the drive, and + // 2.50 says the system software (version ".") is too old for it. + return FirmwareVersionSupportsVSH(version) && FirmwareVersionToInt(version) >= 271; +} + std::string BundledUpdateInfo::Describe() const { if (!present) { return std::string(); diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index 9af8392c9d..d19bf69329 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -185,6 +185,9 @@ int FirmwareVersionToInt(std::string_view version); // the module patches it needs are version-specific, so it won't get anywhere. bool FirmwareVersionSupportsVSH(std::string_view version); +// Whether that VSH's own player plays UMD Video discs. +bool FirmwareVersionPlaysUmdVideo(std::string_view version); + // Installs a firmware into the NAND directory. Two firmwares can't be merged - a file the new one // doesn't have would linger and still get loaded - so this replaces rather than overlays. // diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 50e1229ce1..5a0df93fe2 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -161,8 +161,8 @@ void EmuScreen::SetPSPAnalog(int iInternalScreenRotation, int stick, float x, fl __CtrlSetAnalogXY(stick, x, y); } -EmuScreen::EmuScreen(const Path &filename, const Path &mountIso) - : gamePath_(filename), mountIso_(mountIso) { +EmuScreen::EmuScreen(const Path &filename, const Path &mountIso, std::optional umdAutoboot) + : gamePath_(filename), mountIso_(mountIso), umdAutoboot_(umdAutoboot) { saveStateSlot_ = SaveState::GetCurrentSlot(); g_controlMapper.AddListener(this); @@ -322,6 +322,7 @@ void EmuScreen::ProcessGameBoot(const Path &filename) { coreParam.enableSound = g_Config.bEnableSound; coreParam.fileToStart = filename; coreParam.mountIso = mountIso_; + coreParam.umdAutoboot = umdAutoboot_; coreParam.mountRoot = g_Config.mountRoot; coreParam.startBreak = !g_Config.bAutoRun; coreParam.headLess = false; @@ -620,6 +621,7 @@ void EmuScreen::sendMessage(UIMessage message, const char *value) { bootIsReset_ = false; gamePath_ = newGamePath; mountIso_.clear(); + umdAutoboot_.reset(); } } else if (message == UIMessage::CONFIG_LOADED) { // In case we need to position touch controls differently. diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index 7536e6b49c..8aa1ed8be5 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include @@ -42,7 +43,8 @@ struct ImGuiContext; class EmuScreen : public UIScreen, protected ControlListener { public: // mountIso, if set, is mounted on umd1: when filename is an ELF/PBP (like the VSH). - EmuScreen(const Path &filename, const Path &mountIso = Path()); + // umdAutoboot overrides the VSH's UMD auto-start setting. + EmuScreen(const Path &filename, const Path &mountIso = Path(), std::optional umdAutoboot = std::nullopt); ~EmuScreen(); const char *tag() const override { return "Emu"; } @@ -109,6 +111,7 @@ private: bool bootIsReset_ = false; Path gamePath_; Path mountIso_; + std::optional umdAutoboot_; bool quit_ = false; std::string errorMessage_; diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index 6acc975c81..8ab9c69deb 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -367,7 +367,7 @@ void GameScreen::CreateContentViews(UI::ViewGroup *parent) { LinearLayout *infoLayout = new LinearLayout(ORIENT_VERTICAL, new AnchorLayoutParams(10, 200, NONE, NONE)); leftColumn->Add(infoLayout); - if (info_->fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO) { + if (info_->fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO && GetUmdVideoVSHPath().empty()) { auto er = GetI18NCategory(I18NCat::ERRORS); leftColumn->Add(new NoticeView(NoticeLevel::INFO, er->T("PPSSPP doesn't support UMD Video."), "")); leftColumn->Add(new Choice(di->T("More info"), ImageID("I_LINK_OUT_QUESTION"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add([](UI::EventParams &e) { @@ -553,10 +553,16 @@ void GameScreen::CreateSettingsViews(UI::ViewGroup *rightColumn) { rightColumnItems->SetSpacing(0.0f); rightColumn->Add(rightColumnItems); - if (!inGame_ && FileTypeIsPlayable(info_->fileType)) { + // UMD Video plays in the XMB, with the disc in the drive. + const Path vshPath = info_->fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO ? GetUmdVideoVSHPath() : Path(); + if (!inGame_ && (FileTypeIsPlayable(info_->fileType) || !vshPath.empty())) { Choice *play = rightColumnItems->Add(new Choice(ga->T("Play"), ImageID("I_PLAY"))); - play->OnClick.Add([this](UI::EventParams &e) { - screenManager()->switchScreen(new EmuScreen(gamePath_)); + play->OnClick.Add([this, vshPath](UI::EventParams &e) { + if (vshPath.empty()) { + screenManager()->switchScreen(new EmuScreen(gamePath_)); + } else { + screenManager()->switchScreen(new EmuScreen(vshPath, gamePath_, true)); + } }); // The file is being read, and maybe about to be replaced. play->SetEnabled(!GamePacker::IsPacking(gamePath_)); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 305ae3461a..0b7a3f95d4 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -58,9 +58,26 @@ #include "Core/Util/DarwinFileSystemServices.h" // For the browser #include "Core/HLE/sceUmd.h" +#include "Core/Util/PSARUnpack.h" bool MainScreen::showHomebrewTab = false; +Path GetUmdVideoVSHPath() { + InstalledFirmwareInfo info; + ReadInstalledFirmwareInfo(g_Config.nandRootDirectory, &info, false); + if (!info.hasVsh || !FirmwareVersionPlaysUmdVideo(info.version)) { + return Path(); + } + return g_Config.nandRootDirectory / "flash0/vsh/module/vshmain.prx"; +} + +static void LaunchInEmuScreen(ScreenManager *screenManager, Screen *currentScreen, EmuScreen *emuScreen) { + if (currentScreen) { + screenManager->cancelScreensAbove(currentScreen); + } + screenManager->switchScreen(emuScreen); +} + static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, const Path &path) { if (GamePacker::IsPacking(path)) { // Can't play it while it's read (and maybe replaced), so show its progress instead. @@ -87,9 +104,41 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons switch (info->fileType) { case IdentifiedFileType::PSP_UMD_VIDEO_ISO: - // We show info about it. - screenManager->push(new GameScreen(path, false)); + { + // The XMB plays it, if there's one to boot. Otherwise we show info about it. + const Path vshPath = GetUmdVideoVSHPath(); + if (vshPath.empty()) { + screenManager->push(new GameScreen(path, false)); + } else { + LaunchInEmuScreen(screenManager, currentScreen, new EmuScreen(vshPath, path, true)); + } return; + } + case IdentifiedFileType::PSP_ISO: + { + // A disc with both a game and video on it. The XMB would start the game by itself, so for + // the video, it comes up without auto-start and the disc waits in the Video column. + const Path vshPath = GetUmdVideoVSHPath(); + if (vshPath.empty()) { + break; + } + std::unique_ptr fileLoader(ConstructFileLoader(path)); + if (!fileLoader || !(DetectUmdDiscType(fileLoader.get()) & PSP_UMD_TYPE_VIDEO)) { + break; + } + auto ga = GetI18NCategory(I18NCat::GAME); + const std::vector choices = { std::string(ga->T("Play game")), std::string(ga->T("Watch video in the XMB")) }; + UI::ListPopupScreen *popup = new UI::ListPopupScreen(ga->T("This disc has both a game and video"), choices, -1, [=](int choice) { + if (choice == 0) { + LaunchInEmuScreen(screenManager, currentScreen, new EmuScreen(path)); + } else if (choice == 1) { + LaunchInEmuScreen(screenManager, currentScreen, new EmuScreen(vshPath, path, false)); + } + }); + popup->SetChoiceIcons({ { 0, ImageID("I_PLAY") }, { 1, ImageID("I_UMD_VIDEO_ISO") } }); + screenManager->push(popup); + return; + } case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: { // Show the savedata popup, why not? @@ -114,11 +163,8 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons default: break; } - if (currentScreen) { - screenManager->cancelScreensAbove(currentScreen); - } // Otherwise let the EmuScreen take care of it, including error handling. - screenManager->switchScreen(new EmuScreen(path)); + LaunchInEmuScreen(screenManager, currentScreen, new EmuScreen(path)); } } diff --git a/UI/MainScreen.h b/UI/MainScreen.h index bf6f6dc4f3..3ed60f3094 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -136,3 +136,6 @@ private: }; void LaunchBuyGold(ScreenManager *screenManager); + +// vshmain.prx, if the installed firmware's XMB can play UMD Video discs. Empty otherwise. +Path GetUmdVideoVSHPath(); diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md index 8f6f9feadd..d3d44716b1 100644 --- a/docs/VSHBootInvestigation.md +++ b/docs/VSHBootInvestigation.md @@ -54,6 +54,16 @@ Two separate things: and the Spider-Man 2 trailer on the EUR Video Demo Disc play with sound. What that player needed from our Media Engine HLE (per-picture SEI records, keyframe flags, field conversion in `sceMpegBaseCscAvc`) is in pspautotests `video/mpeg/avcdetail` and `video/mpeg/cscavc`. + + Picking a UMD Video disc in the game list boots the XMB with the disc in the drive and UMD + auto-start forced on (`CoreParameter::umdAutoboot`), so it goes straight to the disc's menu. That + needs firmware 2.71 or later: 1.50, 2.00 and 2.60 crash in `video_plugin`/paf as soon as the disc + is in, and 2.50 refuses it, claiming the system software is version "." (all measured with + Bourne Supremacy). A disc with both a game and video asks which one; for video the XMB comes up + without auto-start, since auto-start runs the game. The disc type comes from the fourth field of + `UMD_DATA.BIN` (`G`, `V`, `A`), and `PSP_GAME` without `SYSDIR/EBOOT.BIN` doesn't count: some video + discs (Spider-Man) carry a `PSP_GAME` holding only a firmware update, and reporting those as game + discs made the XMB say "The disc could not be read." - **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