From f6f9a00eb738d7895537ef565c963ab0a0a096f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 24 Oct 2026 02:55:18 +0200 Subject: [PATCH] UI: Move file I/O off the render thread in a few screens On Android the render thread runs the UI, and surfaceDestroyed waits for it, so slow storage (SAF especially) there stutters and can become an ANR. - Store: only check whether the game is installed when an install or uninstall finishes, not every frame. - Savedata: read the modification time with SIZE on the GameInfo worker. The popup asked for it every frame, and each button on first draw. - FirmwareScreen: scan the NAND, and erase it, on a worker thread. It rescanned on every RecreateViews, which is after every popup. - GameScreen: look up the installed update, plugins and what the firmware can play on a worker thread. They ran on every RecreateViews, which happens as each piece of game info arrives. - LaunchFile: wait only for the file type, and for PARAM.SFO just in the cases that use it. Also add --start-screen=firmware. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/CmdLine.cpp | 2 +- UI/FirmwareScreen.cpp | 73 +++++++++++++++++++++++++++++------- UI/FirmwareScreen.h | 18 +++++++++ UI/GameInfoCache.cpp | 59 ++++++++++++++++------------- UI/GameInfoCache.h | 9 ++++- UI/GameScreen.cpp | 86 +++++++++++++++++++++++++++++++------------ UI/GameScreen.h | 31 +++++++++++++--- UI/MainScreen.cpp | 13 +++++-- UI/Store.cpp | 10 ++++- 9 files changed, 225 insertions(+), 76 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 7ea12e14a1..f2615113e0 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -216,7 +216,7 @@ const char *g_ExceptionActionValues[] = { static const CommandLineParam g_autoParams[] = { {POFF(fullscreen), CmdParamType::Bool, "fullscreen", '\0', "Force full screen mode", CmdLineMode::Application}, {POFF(fullscreen), CmdParamType::BoolInverse, "windowed", '\0', "Force windowed mode", CmdLineMode::Application}, - {POFF(startScreen), CmdParamType::String, "start-screen", '\0', "Start on a specific screen (e.g. 'gamesettings', 'touchscreentest')", CmdLineMode::Application}, + {POFF(startScreen), CmdParamType::String, "start-screen", '\0', "Start on a specific screen (e.g. 'gamesettings', 'touchscreentest', 'firmware')", CmdLineMode::Application}, {POFF(escapeExit), CmdParamType::Bool, "escape-exit", '\0', "Escape key exits the application", CmdLineMode::Application}, {POFF(gameWindow), CmdParamType::Bool, "game-window", '\0', "Show the game in a second window, for testing multiple outputs (Vulkan, or Metal on Mac)", CmdLineMode::Application}, {POFF(pauseMenuExit), CmdParamType::Bool, "pause-menu-exit", '\0', "Exit the application when the game ends, instead of returning to the menu", CmdLineMode::Application}, diff --git a/UI/FirmwareScreen.cpp b/UI/FirmwareScreen.cpp index 782d9fdc53..1f0a972541 100644 --- a/UI/FirmwareScreen.cpp +++ b/UI/FirmwareScreen.cpp @@ -21,6 +21,8 @@ #include "Common/StringUtils.h" #include "Common/System/Request.h" #include "Common/System/System.h" +#include "Common/Thread/Promise.h" +#include "Common/Thread/ThreadManager.h" #include "Common/UI/Notice.h" #include "Common/UI/PopupScreens.h" #include "Common/UI/ScreenManager.h" @@ -41,8 +43,51 @@ std::string_view FirmwareScreen::GetTitle() const { } void FirmwareScreen::BeforeCreateViews() { - nandRoot_ = GetSysDirectory(DIRECTORY_NAND); - ReadInstalledFirmwareInfo(nandRoot_, &info_); + if (!scanned_ && !scan_) { + nandRoot_ = GetSysDirectory(DIRECTORY_NAND); + StartScan(false); + } +} + +void FirmwareScreen::StartScan(bool eraseFirst) { + if (scan_) { + rescanPending_ = true; + erasePending_ = erasePending_ || eraseFirst; + return; + } + scan_ = std::make_shared(); + g_threadManager.EnqueueTask(new IndependentTask(TaskType::IO_BLOCKING, TaskPriority::NORMAL, + [state = scan_, nandRoot = nandRoot_, eraseFirst]() { + if (eraseFirst) { + EraseInstalledFirmware(nandRoot, &state->eraseError); + } + ReadInstalledFirmwareInfo(nandRoot, &state->info); + // Everything above is published by this store. + state->done = true; + })); + // Disables the buttons that would touch the NAND while this runs. + RecreateViews(); +} + +void FirmwareScreen::update() { + UITwoPaneBaseDialogScreen::update(); + if (!scan_ || !scan_->done) { + return; + } + if (!scan_->eraseError.empty()) { + ERROR_LOG(Log::Loader, "Failed to erase the firmware: %s", scan_->eraseError.c_str()); + } + info_ = scan_->info; + scan_.reset(); + scanned_ = true; + if (rescanPending_) { + const bool erase = erasePending_; + rescanPending_ = false; + erasePending_ = false; + StartScan(erase); + } else { + RecreateViews(); + } } void FirmwareScreen::CreateContentViews(UI::ViewGroup *parent) { @@ -55,6 +100,12 @@ void FirmwareScreen::CreateContentViews(UI::ViewGroup *parent) { ChoiceGroupList *list = parent->Add(new ChoiceGroupList()); + if (!scanned_) { + auto mm = GetI18NCategory(I18NCat::MAINMENU); + list->Add(new TextView(mm->T("Loading..."), ALIGN_LEFT, false)); + return; + } + if (!info_.anythingInstalled) { list->Add(new NoticeView(NoticeLevel::INFO, sy->T("No firmware installed"), sy->T("FirmwareSources", "PPSSPP has its own replacements for the PSP's system files, so a firmware is optional. Installing one gets you the real fonts, and lets you launch the XMB. Most game discs carry a firmware updater - open a game's info screen to install from it."))); @@ -109,6 +160,7 @@ void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) { // Booting anything replaces the running game, and erasing the NAND out from under a game // that has flash0 mounted is worse - so while a game is running, this screen is read-only. const bool gameRunning = PSP_IsInited(); + const bool scanning = scan_ != nullptr; ChoiceGroupList *groups = parent->Add(new ChoiceGroupList()); if (info_.hasVsh && !FirmwareVersionSupportsVSH(info_.version)) { @@ -134,7 +186,7 @@ void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) { install->OnClick.Add([this](UI::EventParams &) { BrowseForUpdater(); }); - install->SetEnabled(!gameRunning); + install->SetEnabled(!gameRunning && !scanning); } if (info_.anythingInstalled) { @@ -142,7 +194,7 @@ void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) { erase->OnClick.Add([this](UI::EventParams &) { AskToErase(); }); - erase->SetEnabled(!gameRunning); + erase->SetEnabled(!gameRunning && !scanning); } if (System_GetPropertyBool(SYSPROP_HAS_OPEN_DIRECTORY)) { @@ -166,10 +218,9 @@ void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) { } void FirmwareScreen::dialogFinished(const Screen *dialog, DialogResult result) { - // Coming back from the installer (or the erase confirmation), the NAND may well look - // different than it did when we scanned it. Rescanning is cheap, so just always redo it - - // BeforeCreateViews does the scan. - RecreateViews(); + // Coming back from the installer, the NAND may well look different than it did when we + // scanned it, so always rescan. + StartScan(false); UITwoPaneBaseDialogScreen::dialogFinished(dialog, result); } @@ -195,10 +246,6 @@ void FirmwareScreen::AskToErase() { if (!erase) { return; } - std::string error; - if (!EraseInstalledFirmware(nandRoot_, &error)) { - ERROR_LOG(Log::Loader, "Failed to erase the firmware: %s", error.c_str()); - } - RecreateViews(); + StartScan(true); })); } diff --git a/UI/FirmwareScreen.h b/UI/FirmwareScreen.h index 9f3e495207..365b230010 100644 --- a/UI/FirmwareScreen.h +++ b/UI/FirmwareScreen.h @@ -17,6 +17,10 @@ #pragma once +#include +#include +#include + #include "Common/File/Path.h" #include "Common/UI/UIScreen.h" #include "Common/UI/ViewGroup.h" @@ -35,6 +39,8 @@ public: const char *tag() const override { return "Firmware"; } + void update() override; + protected: std::string_view GetTitle() const override; void BeforeCreateViews() override; @@ -46,7 +52,19 @@ private: void LaunchVSH(); void BrowseForUpdater(); void AskToErase(); + // Scans (and first erases, if asked) on a worker thread, since the NAND can be on slow storage. + void StartScan(bool eraseFirst); + + struct ScanState { + std::atomic done{}; + InstalledFirmwareInfo info; + std::string eraseError; + }; Path nandRoot_; InstalledFirmwareInfo info_; + std::shared_ptr scan_; // While a scan is running. + bool scanned_ = false; + bool rescanPending_ = false; + bool erasePending_ = false; }; diff --git a/UI/GameInfoCache.cpp b/UI/GameInfoCache.cpp index d0231b97a0..80bda19e18 100644 --- a/UI/GameInfoCache.cpp +++ b/UI/GameInfoCache.cpp @@ -221,37 +221,40 @@ u64 GameInfo::GetSizeUncompressedInBytes() { } } -std::string GetFileDateAsString(const Path &filename) { - tm time; - if (File::GetModifTime(filename, time)) { - char buf[256]; - switch (g_Config.iDateFormat) { - case PSP_SYSTEMPARAM_DATE_FORMAT_YYYYMMDD: - strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &time); - break; - case PSP_SYSTEMPARAM_DATE_FORMAT_MMDDYYYY: - strftime(buf, sizeof(buf), "%m-%d-%Y %H:%M:%S", &time); - break; - case PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY: - strftime(buf, sizeof(buf), "%d-%m-%Y %H:%M:%S", &time); - break; - default: // Should never happen - return ""; - } - return std::string(buf); +static std::string FormatFileDate(const tm &time) { + char buf[256]; + switch (g_Config.iDateFormat) { + case PSP_SYSTEMPARAM_DATE_FORMAT_YYYYMMDD: + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &time); + break; + case PSP_SYSTEMPARAM_DATE_FORMAT_MMDDYYYY: + strftime(buf, sizeof(buf), "%m-%d-%Y %H:%M:%S", &time); + break; + case PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY: + strftime(buf, sizeof(buf), "%d-%m-%Y %H:%M:%S", &time); + break; + default: // Should never happen + return ""; + } + return std::string(buf); +} + +Path GameInfo::GetMTimePath() const { + switch (fileType) { + case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: + return GetFilePath() / "PARAM.SFO"; + case IdentifiedFileType::PSP_PBP_DIRECTORY: + return GetFilePath() / "EBOOT.PBP"; + default: + return GetFilePath(); } - return ""; } std::string GameInfo::GetMTime() const { - switch (fileType) { - case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: - return GetFileDateAsString(GetFilePath() / "PARAM.SFO"); - case IdentifiedFileType::PSP_PBP_DIRECTORY: - return GetFileDateAsString(GetFilePath() / "EBOOT.PBP"); - default: - return GetFileDateAsString(GetFilePath()); + if (!hasMTime) { + return ""; } + return FormatFileDate(mtime); } // Not too meaningful if the object itself is a savedata directory... @@ -974,11 +977,15 @@ handleELF: if (flags_ & GameInfoFlags::SIZE) { const u64 gameSizeOnDisk = info_->GetSizeOnDiskInBytes(); + tm mtime{}; + const bool hasMTime = File::GetModifTime(info_->GetMTimePath(), mtime); // NOTE: Don't touch saveDataSize/installDataSize here - those belong to SAVEDATA_SIZE, // which can have been fetched by an earlier work item. std::lock_guard lock(info_->lock); info_->gameSizeOnDisk = gameSizeOnDisk; + info_->mtime = mtime; + info_->hasMTime = hasMTime; } if (flags_ & GameInfoFlags::SAVEDATA_SIZE) { diff --git a/UI/GameInfoCache.h b/UI/GameInfoCache.h index 335dee8ea6..9b97f5bb8e 100644 --- a/UI/GameInfoCache.h +++ b/UI/GameInfoCache.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -97,9 +98,10 @@ public: u64 GetGameSavedataSizeInBytes() const; // For games u64 GetInstallDataSizeInBytes() const; - // For various kinds of savedata, mainly. - // NOTE: This one actually performs I/O directly, not cached. + // For various kinds of savedata, mainly. Read along with SIZE, empty before that. std::string GetMTime() const; + // The file whose modification time GetMTime() shows. + Path GetMTimePath() const; void ParseParamSFO(IdentifiedFileType type); const ParamSFOData &GetParamSFO() const { @@ -190,6 +192,9 @@ public: u64 gameSizeOnDisk = 0; // compressed size, in case of CSO u64 saveDataSize = 0; u64 installDataSize = 0; + // With SIZE. + bool hasMTime = false; + tm mtime{}; // The firmware updater bundled on the disc, if any - see GameInfoFlags::BUNDLED_UPDATE_INFO. // Always left empty for anything that isn't an ISO. diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index 7f4fc1eb73..f3b10b2ff6 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -20,6 +20,8 @@ #include "ppsspp_config.h" #include "Common/Render/DrawBuffer.h" +#include "Common/Thread/Promise.h" +#include "Common/Thread/ThreadManager.h" #include "Common/UI/Context.h" #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" @@ -188,6 +190,19 @@ void GameScreen::update() { } } + if (!lookupsStarted_ && (knownFlags_ & GameInfoFlags::PARAM_SFO)) { + StartLookups(); + } + if (lookupTask_ && lookupTask_->done) { + lookups_ = std::move(lookupTask_->result); + lookupTask_.reset(); + if (lookupsPending_) { + lookupsPending_ = false; + StartLookups(); + } + recreate = true; + } + // Has the user requested a CRC32? if (CRC32string == "...") { // Wait until the CRC32 is ready. It might take time on some devices. @@ -229,7 +244,7 @@ static bool FileTypeHasIcon(IdentifiedFileType fileType) { } } -static bool FileTypeIsPlayable(IdentifiedFileType fileType) { +static bool FileTypeIsPlayable(IdentifiedFileType fileType, bool firmwareRunsPS1Discs) { switch (fileType) { case IdentifiedFileType::ERROR_IDENTIFYING: case IdentifiedFileType::UNKNOWN: @@ -245,19 +260,44 @@ static bool FileTypeIsPlayable(IdentifiedFileType fileType) { return false; case IdentifiedFileType::PSX_ISO: // Through the firmware's POPS. - return InstalledFirmwareRunsPS1Discs(); + return firmwareRunsPS1Discs; default: return true; } } -void GameScreen::RefreshInstalledUpdate() { - hasInstalledUpdate_ = false; - // Homebrew reuses real disc IDs often enough that we'd get false positives. - if (isHomebrew_ || !(knownFlags_ & GameInfoFlags::PARAM_SFO)) { +void GameScreen::StartLookups() { + lookupsStarted_ = true; + if (lookupTask_) { + lookupsPending_ = true; return; } - hasInstalledUpdate_ = FindInstalledGameUpdate(info_->id, &installedUpdate_); + lookupTask_ = std::make_shared(); + // Homebrew reuses real disc IDs often enough that we'd get false positives. + const std::string updateId = info_->region == GameRegion::HOMEBREW ? std::string() : info_->id; + g_threadManager.EnqueueTask(new IndependentTask(TaskType::IO_BLOCKING, TaskPriority::NORMAL, + [task = lookupTask_, id = info_->id, updateId, fileType = info_->fileType, lang = g_Config.sLanguageIni]() { + Lookups &result = task->result; + if (!updateId.empty()) { + result.hasInstalledUpdate = FindInstalledGameUpdate(updateId, &result.installedUpdate); + } + result.plugins = HLEPlugins::FindPlugins(id, lang); + if (fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO) { + result.umdVideoVSHPath = GetUmdVideoVSHPath(); + } else if (fileType == IdentifiedFileType::PSX_ISO) { + result.firmwareRunsPS1Discs = InstalledFirmwareRunsPS1Discs(); + } + // Everything above is published by this store. + task->done = true; + })); +} + +void GameScreen::dialogFinished(const Screen *dialog, DialogResult result) { + // Could have been the firmware installer, or a deleted game update. + if (lookupsStarted_) { + StartLookups(); + } + UITwoPaneBaseDialogScreen::dialogFinished(dialog, result); } // Both panes use isHomebrew_, and which is created first depends on orientation. @@ -370,7 +410,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 && GetUmdVideoVSHPath().empty()) { + if (info_->fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO && !lookupTask_ && lookupsStarted_ && lookups_.umdVideoVSHPath.empty()) { auto er = GetI18NCategory(I18NCat::ERRORS); leftColumn->Add(new NoticeView(NoticeLevel::INFO, ApplySafeSubstitutions(er->T("PSP firmware version %1 or later must be installed to play UMD video"), UMD_VIDEO_MIN_FIRMWARE), "")); leftColumn->Add(new Choice(di->T("More info"), ImageID("I_LINK_OUT_QUESTION"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)))->OnClick.Add([](UI::EventParams &e) { @@ -510,24 +550,24 @@ void GameScreen::CreateContentViews(UI::ViewGroup *parent) { // An installed game update replaces the disc's executable, so it's worth saying so here - // otherwise there's nothing in the UI to explain why a patched game is running. - RefreshInstalledUpdate(); - if (hasInstalledUpdate_) { + const InstalledGameUpdate &installedUpdate = lookups_.installedUpdate; + if (lookups_.hasInstalledUpdate) { infoLayout->Add(new ItemHeader(ga->T("Game update"))); - std::string updateLine = installedUpdate_.title; - if (!installedUpdate_.appVer.empty()) { - const std::string version = ApplySafeSubstitutions(ga->T("Version %1"), installedUpdate_.appVer); + std::string updateLine = installedUpdate.title; + if (!installedUpdate.appVer.empty()) { + const std::string version = ApplySafeSubstitutions(ga->T("Version %1"), installedUpdate.appVer); updateLine = updateLine.empty() ? version : updateLine + " - " + version; } if (updateLine.empty()) { updateLine = ga->T("Installed"); } - updateLine += " - " + NiceSizeFormat(installedUpdate_.sizeOnDisk); + updateLine += " - " + NiceSizeFormat(installedUpdate.sizeOnDisk); infoLayout->Add(new TextView(updateLine, ALIGN_LEFT, true))->SetBullet(true); - infoLayout->Add(new TextView(GetFriendlyPath(installedUpdate_.folder), ALIGN_LEFT | FLAG_WRAP_TEXT, true))->SetBullet(true); + infoLayout->Add(new TextView(GetFriendlyPath(installedUpdate.folder), ALIGN_LEFT | FLAG_WRAP_TEXT, true))->SetBullet(true); } // Show plugin info_, if any. Later might add checkboxes. - auto plugins = HLEPlugins::FindPlugins(info_->id, g_Config.sLanguageIni); + const std::vector &plugins = lookups_.plugins; if (!plugins.empty()) { auto sy = GetI18NCategory(I18NCat::SYSTEM); infoLayout->Add(new ItemHeader(sy->T("Plugins"))); @@ -558,8 +598,8 @@ void GameScreen::CreateSettingsViews(UI::ViewGroup *rightColumn) { ChoiceGroup *choices = rightColumnItems->Add(new ChoiceGroup()); // 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())) { + const Path vshPath = info_->fileType == IdentifiedFileType::PSP_UMD_VIDEO_ISO ? lookups_.umdVideoVSHPath : Path(); + if (!inGame_ && (FileTypeIsPlayable(info_->fileType, lookups_.firmwareRunsPS1Discs) || !vshPath.empty())) { Choice *play = choices->Add(new Choice(ga->T("Play"), ImageID("I_PLAY"))); play->OnClick.Add([this, vshPath](UI::EventParams &e) { if (vshPath.empty()) { @@ -693,8 +733,7 @@ void GameScreen::CreateContextMenu(UI::ViewGroup *parent) { }); } - RefreshInstalledUpdate(); - if (!inGame_ && hasInstalledUpdate_) { + if (!inGame_ && lookups_.hasInstalledUpdate) { Choice *btnDeleteUpdate = parent->Add(new Choice(ga->T("Delete game update"), ImageID("I_TRASHCAN"))); btnDeleteUpdate->OnClick.Handle(this, &GameScreen::OnDeleteGameUpdate); } @@ -735,9 +774,10 @@ void GameScreen::CreateContextMenu(UI::ViewGroup *parent) { } void GameScreen::OnDeleteGameUpdate(UI::EventParams &e) { - if (!hasInstalledUpdate_) { + if (!lookups_.hasInstalledUpdate) { return; } + const InstalledGameUpdate &installedUpdate = lookups_.installedUpdate; auto di = GetI18NCategory(I18NCat::DIALOG); auto ga = GetI18NCategory(I18NCat::GAME); @@ -745,10 +785,10 @@ void GameScreen::OnDeleteGameUpdate(UI::EventParams &e) { prompt += "\n\n"; // Say exactly what disappears - for a digital game the folder holds the game itself, so only // the update's executable goes. - prompt += GetFriendlyPath(installedUpdate_.sharesFolderWithGame ? installedUpdate_.pbootPath : installedUpdate_.folder); + prompt += GetFriendlyPath(installedUpdate.sharesFolderWithGame ? installedUpdate.pbootPath : installedUpdate.folder); const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN); - const InstalledGameUpdate update = installedUpdate_; + const InstalledGameUpdate update = installedUpdate; screenManager()->push( new UI::MessagePopupScreen(ga->T("Delete game update"), prompt, trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"), [this, update](bool yes) { diff --git a/UI/GameScreen.h b/UI/GameScreen.h index 9c819c5e09..b31f95b0b8 100644 --- a/UI/GameScreen.h +++ b/UI/GameScreen.h @@ -17,12 +17,16 @@ #pragma once +#include #include +#include +#include #include "UI/BaseScreens.h" #include "Common/UI/UIScreen.h" #include "Common/File/Path.h" #include "UI/GameInfoCache.h" +#include "Core/HLE/Plugins.h" #include "Core/Util/PkgUnpack.h" #include "UI/SimpleDialogScreen.h" @@ -52,6 +56,7 @@ protected: void CreateContentViews(UI::ViewGroup *parent) override; void CreateSettingsViews(UI::ViewGroup *parent) override; void CreateContextMenu(UI::ViewGroup *parent) override; + void dialogFinished(const Screen *dialog, DialogResult result) override; std::string_view GetTitle() const override; // The progress bar of a pack running in the background. @@ -67,9 +72,21 @@ private: void OnSetBackground(UI::EventParams &e); void OnDeleteGameUpdate(UI::EventParams &e); - // Checks whether a game update is installed for this game. Both the info pane and the context - // menu need the answer, and either can be built first. - void RefreshInstalledUpdate(); + // What's known about the game from outside its file: an installed update, plugins, and whether + // the installed firmware can play it. All disk access, so it's looked up on a worker thread, + // and the views are recreated when it's in. + struct Lookups { + bool hasInstalledUpdate = false; + InstalledGameUpdate installedUpdate; + std::vector plugins; + Path umdVideoVSHPath; + bool firmwareRunsPS1Discs = false; + }; + struct LookupTask { + std::atomic done{}; + Lookups result; + }; + void StartLookups(); std::string CRC32string; @@ -87,9 +104,11 @@ private: int packGeneration_ = 0; bool packBarShown_ = false; - // A game update installed in PSP/GAME/, which is what actually runs when there is one. - InstalledGameUpdate installedUpdate_; - bool hasInstalledUpdate_ = false; + // installedUpdate is a game update in PSP/GAME/, which is what actually runs when there is one. + Lookups lookups_; + std::shared_ptr lookupTask_; // While one runs. + bool lookupsStarted_ = false; + bool lookupsPending_ = false; std::shared_ptr info_; mutable std::string titleCache_; diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 145876b40d..c8548bbbc4 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -89,9 +89,14 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons // Otherwise (launching from a file association, a shortcut, drag-and-drop...) we have to block until // it's available - we can't decide what to do below without it. HIGH, so we don't wait behind // whatever else is queued, like a search's loads for every game in a big list. - const GameInfoFlags neededFlags = GameInfoFlags::FILE_TYPE | GameInfoFlags::PARAM_SFO; - std::shared_ptr info = g_gameInfoCache->GetInfo(nullptr, path, neededFlags, nullptr, GameInfoFlags::EMPTY, TaskPriority::HIGH); - info->WaitUntilReady(neededFlags); + // Only the file type at first: just a few of the cases below need the PARAM.SFO, and reading + // it out of a disc image costs more disk access. + std::shared_ptr info = g_gameInfoCache->GetInfo(nullptr, path, GameInfoFlags::FILE_TYPE, nullptr, GameInfoFlags::EMPTY, TaskPriority::HIGH); + info->WaitUntilReady(GameInfoFlags::FILE_TYPE); + const auto waitForParamSFO = [&]() { + g_gameInfoCache->GetInfo(nullptr, path, GameInfoFlags::PARAM_SFO, nullptr, GameInfoFlags::EMPTY, TaskPriority::HIGH); + info->WaitUntilReady(GameInfoFlags::PARAM_SFO); + }; switch (info->fileType) { case IdentifiedFileType::PSP_UMD_VIDEO_ISO: @@ -138,6 +143,7 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: { // Show the savedata popup, why not? + waitForParamSFO(); std::string title = SanitizeString(info->GetTitle(), StringRestriction::NoLineBreaksOrSpecials, 0, 200); screenManager->push(new SavedataPopupScreen(Path(), path, title)); return; @@ -147,6 +153,7 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons { // Check if it's an update file. If so, we'll offer to install it directly, // instead of running it (which currently will not work). + waitForParamSFO(); if (info->id == "MSTKUPDATE") { std::string title = info->GetTitle(); // includes the version. // The unpacker wants the PBP itself, not the folder it happens to sit in. diff --git a/UI/Store.cpp b/UI/Store.cpp index ab2f300dcf..1e8e67529d 100644 --- a/UI/Store.cpp +++ b/UI/Store.cpp @@ -210,6 +210,7 @@ private: UI::Button *launchButton_ = nullptr; UI::Button *cancelButton_ = nullptr; bool wasInstalled_ = false; + GameManagerState lastState_ = GameManagerState::IDLE; }; void ProductView::CreateViews() { @@ -293,8 +294,13 @@ void ProductView::CreateViews() { } void ProductView::Update() { - if (wasInstalled_ != IsGameInstalled()) { - CreateViews(); + // Installing or uninstalling is the only thing that changes it, so don't hit the disk every frame. + const GameManagerState state = g_GameManager.GetState(); + if (state != lastState_) { + lastState_ = state; + if (state == GameManagerState::IDLE && wasInstalled_ != IsGameInstalled()) { + CreateViews(); + } } if (installButton_) { installButton_->SetEnabled(g_GameManager.GetState() == GameManagerState::IDLE);