Files
ppsspp/UI/FirmwareScreen.cpp
T
Henrik RydgårdandClaude Opus 5.5 f6f9a00eb7 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) <noreply@anthropic.com>
2026-10-30 12:01:36 +01:00

252 lines
9.1 KiB
C++

// Copyright (c) 2026- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/Log.h"
#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"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/Util/PathUtil.h"
#include "UI/FirmwareScreen.h"
#include "UI/InstallUpdateScreen.h"
#include "UI/MiscViews.h"
std::string_view FirmwareScreen::GetTitle() const {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
return sy->T("PSP Firmware");
}
void FirmwareScreen::BeforeCreateViews() {
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<ScanState>();
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) {
using namespace UI;
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
auto di = GetI18NCategory(I18NCat::DIALOG);
auto st = GetI18NCategory(I18NCat::STORE); // Borrow "Size" from here, like GameScreen does.
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.")));
list->Add(new TextWithImage(ImageID("I_FOLDER"), GetFriendlyPath(nandRoot_)));
return;
}
if (!info_.version.empty()) {
std::string versionLine = std::string(sy->T("Firmware version")) + ": " + info_.version;
list->Add(new TextView(versionLine, ALIGN_LEFT, false))->SetBig(true);
if (!info_.buildDate.empty() || !info_.target.empty()) {
std::string details = info_.buildDate;
if (!info_.target.empty()) {
if (!details.empty()) {
details += " - ";
}
details += info_.target;
}
list->Add(new TextView(details, ALIGN_LEFT, false));
}
} else {
// No flash0:/vsh/etc/version.txt, so this isn't a full firmware - most likely just the
// fonts, which is what installing from a running game's disc leaves behind.
list->Add(new NoticeView(NoticeLevel::INFO, sy->T("Partial firmware installed"),
sy->T("Some system files are present, but not a complete firmware")));
}
// Without any kernel modules there's no firmware here to speak of - it's the fonts we pulled
// off a game's disc, and rows reading "Kernel modules: 0 / XMB: No" say nothing useful.
const bool fullFirmware = info_.kernelModuleCount > 0;
ChoiceGroup *content = list->Add(new ChoiceGroup(sy->T("Contents")));
content->Add(new InfoItem(sy->T("Fonts"), info_.fontCount));
if (fullFirmware) {
content->Add(new InfoItem(sy->T("Kernel modules"), info_.kernelModuleCount));
}
if (fullFirmware || info_.hasVsh) {
content->Add(new InfoItem(sy->T("XMB (VSH)"), info_.hasVsh ? di->T("Yes") : di->T("No")));
}
if (fullFirmware) {
content->Add(new InfoItem(sy->T("Files"), info_.fileCount));
}
content->Add(new InfoItem(st->T("Size"), NiceSizeFormat(info_.totalSize)));
}
void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) {
using namespace UI;
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
// 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)) {
groups->Add(new NoticeView(NoticeLevel::WARN, sy->T("XMB unsupported"),
sy->T("XMBUnsupportedVersion", "Only firmware 6.61 can boot the XMB so far")));
}
ChoiceGroup *launchGroup = groups->Add(new ChoiceGroup());
Choice *launch = launchGroup->Add(new Choice(sy->T("Launch XMB"), ImageID("I_PLAY")));
ChoiceGroup *settings = groups->Add(new ChoiceGroup());
settings->Add(new CheckBox(&g_Config.bAutoUpgradeFirmware, sy->T("Install or upgrade firmware from game discs"),
sy->T("AutoUpgradeFirmwareDesc", "Most game discs carry a firmware updater. Install it when there's no firmware yet, or when the disc's is newer.")));
ChoiceGroup *actions = groups->Add(new ChoiceGroup());
launch->OnClick.Add([this](UI::EventParams &) {
LaunchVSH();
});
launch->SetEnabled(!gameRunning && info_.hasVsh && FirmwareVersionSupportsVSH(info_.version));
if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
Choice *install = actions->Add(new Choice(iz->T("Install PSP firmware update"), ImageID("I_FOLDER_UPLOAD")));
install->OnClick.Add([this](UI::EventParams &) {
BrowseForUpdater();
});
install->SetEnabled(!gameRunning && !scanning);
}
if (info_.anythingInstalled) {
Choice *erase = actions->Add(new Choice(sy->T("Uninstall firmware"), ImageID("I_TRASHCAN")));
erase->OnClick.Add([this](UI::EventParams &) {
AskToErase();
});
erase->SetEnabled(!gameRunning && !scanning);
}
if (System_GetPropertyBool(SYSPROP_HAS_OPEN_DIRECTORY)) {
actions->Add(new Choice(sy->T("Show firmware folder")))->OnClick.Add([this](UI::EventParams &) {
System_LaunchUrl(LaunchUrlType::LOCAL_FOLDER, nandRoot_.ToString());
});
}
auto di = GetI18NCategory(I18NCat::DIALOG);
Choice *moreInfo = groups->Add(new ChoiceGroup())->Add(new Choice(di->T("More info")));
moreInfo->OnClick.Add([](UI::EventParams &) {
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/docs/getting-started/how-to-run-the-vsh/");
});
moreInfo->SetIconRight(ImageID("I_LINK_OUT"));
if (gameRunning) {
groups->Add(new NoticeView(NoticeLevel::INFO,
sy->T("FirmwareNeedsNoGame", "Stop the game to change the installed firmware"), ""));
}
}
void FirmwareScreen::dialogFinished(const Screen *dialog, DialogResult result) {
// 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);
}
void FirmwareScreen::LaunchVSH() {
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, (nandRoot_ / "flash0/vsh/module/vshmain.prx").ToString());
}
void FirmwareScreen::BrowseForUpdater() {
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
System_BrowseForFile(GetRequesterToken(), iz->T("Install PSP firmware update"), BrowseFileType::BOOTABLE,
[this](std::string_view value, int) {
screenManager()->push(new InstallUpdateScreen(Path(value), "", false));
});
}
void FirmwareScreen::AskToErase() {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto di = GetI18NCategory(I18NCat::DIALOG);
std::string question(sy->T("EraseFirmwareConfirm", "This deletes everything in the NAND folder, including the fonts."));
screenManager()->push(new UI::MessagePopupScreen(sy->T("Uninstall firmware"), question, di->T("Delete"), di->T("Cancel"),
[this](bool erase) {
if (!erase) {
return;
}
StartScan(true);
}));
}