From f11b43227af2269eda610d26a13d5c487e1d4618 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Thu, 6 Aug 2026 23:22:19 +1000 Subject: [PATCH] System: Add option to install BIOS when missing --- src/core/bios.cpp | 20 ++++++------ src/core/bios.h | 2 +- src/core/fullscreenui.cpp | 23 +++++++++++--- src/core/fullscreenui_private.h | 1 + src/core/fullscreenui_settings.cpp | 13 +++++--- src/core/system.cpp | 50 +++++++++++++++++------------- src/core/system.h | 9 +++++- src/duckstation-qt/qthost.cpp | 28 ++++++++++++++--- 8 files changed, 100 insertions(+), 46 deletions(-) diff --git a/src/core/bios.cpp b/src/core/bios.cpp index aa06c01e4..b691c135f 100644 --- a/src/core/bios.cpp +++ b/src/core/bios.cpp @@ -348,7 +348,7 @@ DiscRegion BIOS::GetPSExeDiscRegion(const PSEXEHeader& header) return DiscRegion::Other; } -std::optional BIOS::GetBIOSImage(ConsoleRegion region, Error* error) +std::optional BIOS::GetBIOSImage(ConsoleRegion region, bool* using_auto_select, Error* error) { std::string bios_name; switch (region) @@ -369,7 +369,10 @@ std::optional BIOS::GetBIOSImage(ConsoleRegion region, Error* error std::optional image; - if (bios_name.empty()) + const bool auto_select = bios_name.empty(); + if (using_auto_select) + *using_auto_select = auto_select; + if (auto_select) { // auto-detect image = FindBIOSImageInDirectory(region, EmuFolders::Bios.c_str(), error); @@ -436,18 +439,13 @@ std::optional BIOS::FindBIOSImageInDirectory(ConsoleRegion region, if (Error::IsValid(error)) Error::AddSuffix(error, "\n\n"); -#ifndef __ANDROID__ Error::AddSuffixFmt( error, - TRANSLATE_FS("System", "No BIOS image found for {} region.\n\nDuckStation requires a PS1 or PS2 BIOS in order to " - "run.\n\nFor legal reasons, you *must* obtain a BIOS from an actual PS1 unit that you own " - "(borrowing doesn't count).\n\nOnce dumped, this BIOS image should be placed in the bios " - "folder within the data directory (Tools Menu -> Open Data Directory)."), + TRANSLATE_FS( + "System", + "No BIOS image found for {} region.\n\nDuckStation requires a PS1 or PS2 BIOS in order to run.\n\nFor legal " + "reasons, you must obtain a BIOS from an actual PS1/PS2 unit that you own (borrowing doesn't count)."), Settings::GetConsoleRegionName(region)); -#else - Error::AddSuffixFmt(error, TRANSLATE_FS("System", "No BIOS image found for {} region."), - Settings::GetConsoleRegionName(region)); -#endif return image; } diff --git a/src/core/bios.h b/src/core/bios.h index a8130e17d..0d0f26df5 100644 --- a/src/core/bios.h +++ b/src/core/bios.h @@ -96,7 +96,7 @@ bool IsValidPSExeHeader(const PSEXEHeader& header, size_t file_size); DiscRegion GetPSExeDiscRegion(const PSEXEHeader& header); /// Loads the BIOS image for the specified region. -std::optional GetBIOSImage(ConsoleRegion region, Error* error); +std::optional GetBIOSImage(ConsoleRegion region, bool* using_auto_select, Error* error); /// Searches for a BIOS image for the specified region in the specified directory. If no match is found, the first /// BIOS image within 512KB and 4MB will be used. diff --git a/src/core/fullscreenui.cpp b/src/core/fullscreenui.cpp index a9d1e608d..9d3331ef5 100644 --- a/src/core/fullscreenui.cpp +++ b/src/core/fullscreenui.cpp @@ -705,14 +705,29 @@ void FullscreenUI::DoStartPath(std::string path, std::string state, std::optiona // This can "fail" if HC mode is enabled and the user cancels, or other startup cancel paths. Error error; - if (!System::BootSystem(std::move(params), &error)) + if (const System::BootResult result = System::BootSystem(std::move(params), &error); + result != System::BootResult::Success) { - VideoThread::RunOnThread([error_desc = error.TakeDescription()]() { + VideoThread::RunOnThread([error_desc = error.TakeDescription(), result]() { if (!IsInitialized()) return; - OpenInfoMessageDialog(ICON_EMOJI_NO_ENTRY_SIGN, TRANSLATE_STR("System", "Error"), - fmt::format(TRANSLATE_FS("System", "Failed to boot system: {}"), error_desc)); + std::string title = TRANSLATE_STR("System", "Error"); + std::string message = fmt::format("{}\n\n{}", TRANSLATE_SV("System", "Failed to boot system:"), error_desc); + if (result == System::BootResult::MissingBIOS) + { + fmt::format_to(std::back_inserter(message), "\n\n{}", + TRANSLATE_SV("System", "Do you want to install a BIOS file now?")); + OpenConfirmMessageDialog(ICON_EMOJI_NO_ENTRY_SIGN, std::move(title), std::move(message), [](bool result) { + if (result) + StartInstallBIOS(); + }); + } + else + { + OpenInfoMessageDialog(ICON_EMOJI_NO_ENTRY_SIGN, std::move(title), std::move(message)); + } + ClearSaveStateEntryList(); UpdateRunIdleState(); }); diff --git a/src/core/fullscreenui_private.h b/src/core/fullscreenui_private.h index 595cf0662..ae899ecf7 100644 --- a/src/core/fullscreenui_private.h +++ b/src/core/fullscreenui_private.h @@ -115,6 +115,7 @@ void SwitchToSettings(SettingsPage page = SettingsPage::Interface); bool SwitchToGameSettings(SettingsPage page = SettingsPage::Summary); void SwitchToGameSettings(const GameList::Entry* entry, SettingsPage page = SettingsPage::Summary); bool SwitchToGameSettingsForPath(const std::string& path, SettingsPage page = SettingsPage::Summary); +void StartInstallBIOS(); void DrawSettingsWindow(); SettingsPage GetCurrentSettingsPage(); bool IsInputBindingDialogOpen(); diff --git a/src/core/fullscreenui_settings.cpp b/src/core/fullscreenui_settings.cpp index 8ace70e6f..b18b76f0c 100644 --- a/src/core/fullscreenui_settings.cpp +++ b/src/core/fullscreenui_settings.cpp @@ -3183,10 +3183,7 @@ void FullscreenUI::DrawBIOSSettingsPage() if (MenuButton(FSUI_ICONVSTR(ICON_FA_FILE_IMPORT, "Install BIOS"), FSUI_VSTR("Copies a BIOS image to the configured global BIOS directory."))) { - OpenFileSelector(FSUI_ICONVSTR(ICON_FA_FILE_IMPORT, "Select BIOS Image"), {}, {}, [](std::string path) { - if (!path.empty()) - InstallBIOS(std::move(path), false); - }); + StartInstallBIOS(); } DrawFolderSetting(bsi, FSUI_ICONVSTR(ICON_FA_FOLDER, "BIOS Directory"), "BIOS", "SearchDirectory", EmuFolders::Bios); @@ -3198,6 +3195,14 @@ void FullscreenUI::DrawBIOSSettingsPage() EndMenuButtons(); } +void FullscreenUI::StartInstallBIOS() +{ + OpenFileSelector(FSUI_ICONVSTR(ICON_FA_FILE_IMPORT, "Select BIOS Image"), {}, {}, [](std::string path) { + if (!path.empty()) + InstallBIOS(std::move(path), false); + }); +} + void FullscreenUI::InstallBIOS(std::string source_path, bool replace) { Error error; diff --git a/src/core/system.cpp b/src/core/system.cpp index cf8adc479..af8c29531 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -158,8 +158,8 @@ static void LogUnsafeSettingsToConsole(const SmallStringBase& messages); static bool Initialize(std::unique_ptr disc, DiscRegion disc_region, bool force_software_renderer, std::optional start_fullscreen, Error* error); -static bool LoadBIOS(Error* error); -static bool SetBootMode(BootMode new_boot_mode, DiscRegion disc_region, Error* error); +static bool LoadBIOS(bool* using_auto_select, Error* error); +static bool SetBootMode(BootMode new_boot_mode, DiscRegion disc_region, bool* missing_bios, Error* error); static void InternalReset(); static void ClearRunningGame(); static void DestroySystem(); @@ -1447,7 +1447,7 @@ void System::ResetSystem() const BootMode new_boot_mode = (s_state.boot_mode == BootMode::BootEXE || s_state.boot_mode == BootMode::BootPSF) ? s_state.boot_mode : (g_settings.bios_patch_fast_boot ? BootMode::FastBoot : BootMode::FullBoot); - if (Error error; !SetBootMode(new_boot_mode, CDROM::GetDiscRegion(), &error)) + if (Error error; !SetBootMode(new_boot_mode, CDROM::GetDiscRegion(), nullptr, &error)) ERROR_LOG("Failed to reload BIOS on boot mode change, the system may be unstable: {}", error.GetDescription()); // Have to turn on turbo if fast forwarding boot. @@ -1544,7 +1544,7 @@ bool System::SaveResumeState(Error* error) return SaveState(std::move(path), error, false, true); } -bool System::BootSystem(SystemBootParameters parameters, Error* error) +System::BootResult System::BootSystem(SystemBootParameters parameters, Error* error) { Timer boot_timer; @@ -1584,7 +1584,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) { gpu_dump = GPUDump::Player::Open(parameters.path, error); if (!gpu_dump) - return false; + return BootResult::Failure; boot_mode = BootMode::ReplayGPUDump; } @@ -1603,7 +1603,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) if (!disc) { Error::AddPrefixFmt(error, "Failed to open CD image '{}':\n", Path::GetFileName(parameters.path)); - return false; + return BootResult::Failure; } disc_region = GameList::GetCustomRegionForPath(parameters.path).value_or(GetRegionForImage(disc.get())); @@ -1619,7 +1619,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) { Error::AddPrefixFmt(error, "Failed to switch to subimage {} in '{}':\n", parameters.media_playlist_index, Path::GetFileName(parameters.path)); - return false; + return BootResult::Failure; } // Can't early cancel without destroying past this point. @@ -1648,7 +1648,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) Path::GetFileName(parameters.override_exe)); Host::OnSystemStopping(); DestroySystem(); - return false; + return BootResult::Failure; } INFO_LOG("Overriding boot executable: '{}'", parameters.override_exe); @@ -1676,7 +1676,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) // Technically a failure, but user-initiated. Returning false here would try to display a non-existent error. Host::OnSystemStopping(); DestroySystem(); - return true; + return BootResult::Success; } } @@ -1687,23 +1687,30 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) boot_mode = BootMode::FastBoot; } - // Load BIOS image, component setup, check for subchannel in games that need it. + // Load BIOS image. const std::optional start_fullscreen = parameters.override_fullscreen.has_value() ? std::optional(parameters.override_fullscreen.value()) : (ShouldStartFullscreen() ? std::optional(true) : std::nullopt); - if (!SetBootMode(boot_mode, disc_region, error) || - !Initialize(std::move(disc), disc_region, parameters.force_software_renderer, start_fullscreen, error)) + if (bool missing_bios = false; !SetBootMode(boot_mode, disc_region, &missing_bios, error)) { Host::OnSystemStopping(); DestroySystem(); - return false; + return missing_bios ? BootResult::MissingBIOS : BootResult::Failure; + } + + // Component setup. + if (!Initialize(std::move(disc), disc_region, parameters.force_software_renderer, start_fullscreen, error)) + { + Host::OnSystemStopping(); + DestroySystem(); + return BootResult::Failure; } // Check for required subchannel data. // Annoyingly we can't do this before initializing, because subchannel data can be loaded from outside the image. if (!parameters.ignore_missing_subchannel && !CheckForRequiredSubQ(error)) { - bool result = false; + BootResult result = BootResult::Failure; if (Core::GetBoolSettingValue("CDROM", "AllowBootingWithoutSBIFile", false)) { Host::ConfirmMessageAsync( @@ -1723,7 +1730,8 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) } }); - result = true; + // Prevent this boot from going through, but don't treat it as a failure. The user can choose to continue or not. + result = BootResult::Success; } Host::OnSystemStopping(); @@ -1753,7 +1761,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) Path::GetFileName(parameters.save_state)); Host::OnSystemStopping(); DestroySystem(); - return false; + return BootResult::Failure; } InputManager::UpdateHostMouseMode(); @@ -1784,7 +1792,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error) INFO_LOG("System booted in {:.2f}ms", boot_timer.GetTimeMilliseconds()); PerformanceCounters::Reset(); ResetThrottler(); - return true; + return BootResult::Success; } bool System::Initialize(std::unique_ptr disc, DiscRegion disc_region, bool force_software_renderer, @@ -2666,9 +2674,9 @@ void System::DoMemoryState(StateWrapper& sw, MemorySaveState& mss, bool update_d GPU::UpdateDisplay(false); } -bool System::LoadBIOS(Error* error) +bool System::LoadBIOS(bool* using_auto_select, Error* error) { - std::optional bios_image = BIOS::GetBIOSImage(s_state.region, error); + std::optional bios_image = BIOS::GetBIOSImage(s_state.region, using_auto_select, error); if (!bios_image.has_value()) return false; @@ -2720,7 +2728,7 @@ void System::InternalReset() s_state.internal_frame_number = 0; } -bool System::SetBootMode(BootMode new_boot_mode, DiscRegion disc_region, Error* error) +bool System::SetBootMode(BootMode new_boot_mode, DiscRegion disc_region, bool* missing_bios, Error* error) { // Can we actually fast boot? If starting, s_bios_image_info won't be valid. const bool can_fast_boot = @@ -2735,7 +2743,7 @@ bool System::SetBootMode(BootMode new_boot_mode, DiscRegion disc_region, Error* return true; // Need to reload the BIOS to wipe out the patching. - if (new_boot_mode != BootMode::ReplayGPUDump && !LoadBIOS(error)) + if (new_boot_mode != BootMode::ReplayGPUDump && !LoadBIOS(missing_bios, error)) return false; // Handle the case of BIOSes not being able to full boot. diff --git a/src/core/system.h b/src/core/system.h index d545de043..3c93ceced 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -117,6 +117,13 @@ enum class BootMode : u8 ReplayGPUDump, }; +enum class BootResult : u8 +{ + Success, + Failure, + MissingBIOS, +}; + enum class Taint : u8 { CPUOverclock, @@ -276,7 +283,7 @@ void ReloadInputBindings(); /// Reloads only controller settings. void UpdateControllerSettings(); -bool BootSystem(SystemBootParameters parameters, Error* error); +BootResult BootSystem(SystemBootParameters parameters, Error* error); void PauseSystem(bool paused); void ResetSystem(); diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 8c823f45c..1aff85c8a 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -3,6 +3,7 @@ #include "qthost.h" #include "autoupdaterdialog.h" +#include "biossettingswidget.h" #include "displaywidget.h" #include "logwindow.h" #include "mainwindow.h" @@ -1089,10 +1090,29 @@ void CoreThread::bootSystem(std::shared_ptr params) return; Error error; - if (!System::BootSystem(std::move(*params), &error)) - { - emit errorReported(tr("Error"), - tr("Failed to boot system: %1").arg(QString::fromStdString(error.GetDescription()))); + if (const System::BootResult result = System::BootSystem(std::move(*params), &error); + result != System::BootResult::Success) + { + std::string message = + fmt::format("{}\n\n{}", TRANSLATE_SV("System", "Failed to boot system:"), error.GetDescription()); + Host::RunOnUIThread([message = QString::fromStdString(message), result]() { + const QString title = QCoreApplication::translate("System", "Error"); + if (result == System::BootResult::MissingBIOS) + { + QMessageBox* const mbox = QtUtils::NewMessageBox( + g_main_window, QMessageBox::Critical, title, + QStringLiteral("%1\n\n%2") + .arg(message) + .arg(QCoreApplication::translate("System", "Do you want to install a BIOS file now?")), + QMessageBox::Yes | QMessageBox::No); + connect(mbox, &QMessageBox::accepted, g_main_window, []() { BIOSSettingsWidget::installBIOS(g_main_window); }); + mbox->open(); + } + else + { + QtUtils::AsyncMessageBox(g_main_window, QMessageBox::Critical, title, message); + } + }); } }