diff options
Diffstat (limited to 'src/yuzu/main_window.cpp')
| -rw-r--r-- | src/yuzu/main_window.cpp | 329 |
1 files changed, 298 insertions, 31 deletions
diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index b0c0446add..dbe5ea2334 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -10,6 +10,7 @@ #include <boost/algorithm/string/split.hpp> #include "common/fs/path_util.h" +#include "common/game_presets.h" #include "common/settings.h" #include "common/settings_enums.h" #include "frontend_common/settings_generator.h" @@ -42,6 +43,7 @@ #include "set_play_time_dialog.h" #include "util/util.h" #include "yuzu/game/game_list.h" +#include "yuzu/nup_dialogs.h" #include "applets/qt_amiibo_settings.h" #include "applets/qt_controller.h" @@ -73,8 +75,11 @@ #include <QClipboard> #include <QDesktopServices> #include <QDir> +#include <QElapsedTimer> +#include <QEventLoop> #include <QFileDialog> #include <QFileInfo> +#include <QFutureWatcher> #include <QGuiApplication> #include <QInputDialog> #include <QMimeData> @@ -149,6 +154,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "core/file_sys/card_image.h" #include "core/file_sys/common_funcs.h" +#include "core/file_sys/nup_writer.h" #include "core/file_sys/romfs.h" #include "core/file_sys/savedata_factory.h" @@ -315,6 +321,7 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; constexpr int default_mouse_hide_timeout = 2500; constexpr int default_input_update_timeout = 1; +constexpr int EmuThreadStopTimeoutMs = 10'000; constexpr size_t CopyBufferSize = 1_MiB; @@ -450,7 +457,7 @@ MainWindow::MainWindow(bool has_broken_vulkan) QtCommon::SetupContentProviders(); // Gen keys if necessary - OnCheckFirmwareDecryption(); + OnCheckFirmwareDecryption(false); #ifdef __unix__ OnCheckGraphicsBackend(); @@ -554,6 +561,10 @@ MainWindow::MainWindow(bool has_broken_vulkan) } MainWindow::~MainWindow() { + if (nup_cancel) { + nup_cancel->store(true); + } + nup_watcher.waitForFinished(); #ifdef __APPLE__ AllowOSSleep(); #endif @@ -1288,7 +1299,6 @@ void MainWindow::InitializeHotkeys() { hotkey_registry.LoadHotkeys(); LinkActionShortcut(ui->action_Load_File, QStringLiteral("Load File")); - LinkActionShortcut(ui->action_Load_Amiibo, QStringLiteral("Load/Remove Amiibo")); LinkActionShortcut(ui->action_Exit, QStringLiteral("Exit Eglantine")); LinkActionShortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); @@ -1458,6 +1468,8 @@ void MainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::DumpRomFSRequested, this, &MainWindow::OnGameListDumpRomFS); connect(game_list, &GameList::VerifyIntegrityRequested, this, &MainWindow::OnGameListVerifyIntegrity); + connect(game_list, &GameList::ConvertToNupRequested, this, + &MainWindow::OnGameListConvertToNup); connect(game_list, &GameList::CopyTIDRequested, this, &MainWindow::OnGameListCopyTID); connect(game_list, &GameList::CreateShortcut, this, &MainWindow::OnGameListCreateShortcut); connect(game_list, &GameList::AddDirectory, this, &MainWindow::OnGameListAddDirectory); @@ -1826,24 +1838,6 @@ bool MainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPar const Core::SystemResultStatus result{ QtCommon::system->Load(*render_window, filename.toStdString(), params)}; - const auto drd_callout = (UISettings::values.callout_flags.GetValue() & - static_cast<u32>(CalloutFlag::DRDDeprecation)) == 0; - - if (result == Core::SystemResultStatus::Success && - QtCommon::system->GetAppLoader().GetFileType() == - Loader::FileType::DeconstructedRomDirectory && - drd_callout) { - UISettings::values.callout_flags = UISettings::values.callout_flags.GetValue() | - static_cast<u32>(CalloutFlag::DRDDeprecation); - QMessageBox::warning( - this, tr("Warning: Outdated Game Format"), - tr("You are using the deconstructed ROM directory format for this game, which is an " - "outdated format that has been superseded by others such as NCA, NAX, XCI, or " - "NSP. Deconstructed ROM directories lack icons, metadata, and update " - "support.<br>For an explanation of the various Switch formats Eglantine supports, " - "out our user handbook. This message will not be shown again.")); - } - if (result != Core::SystemResultStatus::Success) { switch (result) { case Core::SystemResultStatus::ErrorGetLoader: @@ -1907,6 +1901,37 @@ bool MainWindow::SelectAndSetCurrentUser( void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletParameters params, StartGameType type) { + if (nup_running && filename.toStdString() == nup_source_path) { + QMessageBox::information(this, tr("Conversion in Progress"), + tr("This game cannot be started while its dump is being " + "converted.")); + return; + } + if (boot_in_progress || close_pending) { + return; + } + boot_in_progress = true; + game_list->SetScanEnabled(false); + QElapsedTimer boot_timer; + boot_timer.start(); + SCOPE_EXIT { + boot_in_progress = false; + LOG_INFO(Frontend, "Launch finished: elapsed_ms={} running={}", boot_timer.elapsed(), + emulation_running); + if (!emulation_running && !close_pending) { + game_list->SetScanEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); + } + if (close_pending) { + QTimer::singleShot(0, this, &QWidget::close); + } + }; + if (emulation_running) { + ShutdownGame(); + } + if (close_pending) { + return; + } LOG_INFO(Frontend, "Eglantine starting..."); if (params.program_id == 0 || @@ -1941,6 +1966,26 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa QtCommon::system->ApplySettings(); } + if (title_id == Settings::MarioKartWorldProgramId && !asked_recommended_mkw_settings) { + asked_recommended_mkw_settings = true; + use_recommended_mkw_settings = + QMessageBox::question( + this, tr("Recommended Mario Kart World settings"), + tr("Use the recommended settings for Mario Kart World?\n\n" + "Vulkan, 1x resolution, GPU Fast, asynchronous shaders, BC3, conservative VRAM, " + "reactive flushing off, FIFO Relaxed VSync, handheld mode, ScaleForce, and " + "FXAA.\n\n" + "They apply to Mario Kart World until Eglantine is closed. Your saved settings " + "are not changed."), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; + LOG_INFO(Config, "MKW recommended settings accepted: {}", use_recommended_mkw_settings); + } + if (Settings::ApplyMarioKartWorldPreset(Settings::values, title_id, + use_recommended_mkw_settings)) { + QtCommon::system->ApplySettings(); + Settings::UpdateGPUAccuracy(); + LOG_INFO(Config, "Applied recommended MKW settings after per-game configuration"); + } Settings::LogSettings(); if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) { @@ -1960,9 +2005,43 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa // behavior of asking. user_flag_cmd_line = false; + FileSys::VirtualFile prepared_romfs; + if (loader && loader->GetFileType() == Loader::FileType::DeconstructedRomDirectory) { + QProgressDialog progress(tr("Preparing game files..."), QString{}, 0, 0, this); + progress.setWindowModality(Qt::ApplicationModal); + progress.setWindowFlag(Qt::WindowCloseButtonHint, false); + progress.setCancelButton(nullptr); + progress.setMinimumDuration(0); + QFutureWatcher<FileSys::VirtualFile> watcher; + QEventLoop loop; + connect(&watcher, &QFutureWatcher<FileSys::VirtualFile>::finished, &loop, &QEventLoop::quit); + watcher.setFuture(QtConcurrent::run([&loader] { + FileSys::VirtualFile image; + loader->ReadRomFS(image); + return image; + })); + progress.show(); + loop.exec(); + try { + prepared_romfs = watcher.result(); + } catch (const std::exception& error) { + LOG_ERROR(Frontend, "Loose RomFS preparation failed: {}", error.what()); + QMessageBox::critical(this, tr("Unable to prepare game files"), + tr("Game preparation failed. See the log for details.")); + return; + } + if (close_pending) { + return; + } + } + + QElapsedTimer load_timer; + load_timer.start(); if (!LoadROM(filename, params)) { + LOG_ERROR(Frontend, "Core load failed: elapsed_ms={}", load_timer.elapsed()); return; } + LOG_INFO(Frontend, "Core load completed: elapsed_ms={}", load_timer.elapsed()); QtCommon::system->SetShuttingDown(false); game_list->setDisabled(true); @@ -2097,6 +2176,9 @@ bool MainWindow::OnShutdownBegin() { Settings::values.current_speed_mode = Settings::SpeedMode::Standard; if (QtCommon::system->IsShuttingDown()) { + status_bar_update_timer.stop(); + connect(QtCommon::emu_thread.get(), &QThread::finished, this, + &MainWindow::OnEmulationStopped, Qt::UniqueConnection); return false; } @@ -2107,6 +2189,8 @@ bool MainWindow::OnShutdownBegin() { } QtCommon::system->SetShuttingDown(true); + status_bar_update_timer.stop(); + LOG_INFO(Frontend, "Shutdown requested; waiting for emulation thread"); discord_rpc->Pause(); RequestGameExit(); @@ -2126,7 +2210,8 @@ bool MainWindow::OnShutdownBegin() { shutdown_timer.setSingleShot(true); shutdown_timer.start(shutdown_time); - connect(&shutdown_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeExpired); + connect(&shutdown_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeExpired, + Qt::UniqueConnection); // Disable everything to prevent anything from being triggered here ui->action_Pause->setEnabled(false); @@ -2146,18 +2231,38 @@ void MainWindow::OnShutdownBeginDialog() { void MainWindow::OnEmulationStopTimeExpired() { if (QtCommon::emu_thread) { QtCommon::emu_thread->ForceStop(); + emu_terminate_timer.setSingleShot(true); + emu_terminate_timer.start(EmuThreadStopTimeoutMs); + connect(&emu_terminate_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeout, + Qt::UniqueConnection); + } +} + +void MainWindow::OnEmulationStopTimeout() { + TerminateEmulationThread(); + OnEmulationStopped(); +} + +void MainWindow::TerminateEmulationThread() { + if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) { + LOG_ERROR(Frontend, "Emulation thread failed to stop, terminating it"); + QtCommon::emu_thread->terminate(); + QtCommon::emu_thread->wait(); } } void MainWindow::OnEmulationStopped() { + if (!emulation_running) { + return; + } + if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) { + return; + } shutdown_timer.stop(); + emu_terminate_timer.stop(); if (QtCommon::emu_thread) { QtCommon::emu_thread->disconnect(); - if (!QtCommon::emu_thread->wait(QDeadlineTimer(10'000))) { - LOG_ERROR(Frontend, "Emulation thread failed to stop, terminating it"); - QtCommon::emu_thread->terminate(); - QtCommon::emu_thread->wait(); - } + QtCommon::emu_thread->wait(); QtCommon::emu_thread.reset(); } @@ -2226,6 +2331,13 @@ void MainWindow::OnEmulationStopped() { Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn()); QtCommon::system->HIDCore().ReloadInputDevices(); UpdateStatusButtons(); + LOG_INFO(Frontend, "Emulation cleanup completed"); + if (close_pending) { + QTimer::singleShot(0, this, &QWidget::close); + } else if (!boot_in_progress) { + game_list->SetScanEnabled(true); + game_list->PopulateAsync(UISettings::values.game_dirs); + } } void MainWindow::ShutdownGame() { @@ -2237,6 +2349,17 @@ void MainWindow::ShutdownGame() { play_time_manager->Stop(); OnShutdownBegin(); OnEmulationStopTimeExpired(); + emu_terminate_timer.stop(); + if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) { + QEventLoop loop; + QTimer timeout; + timeout.setSingleShot(true); + connect(QtCommon::emu_thread.get(), &QThread::finished, &loop, &QEventLoop::quit); + connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit); + timeout.start(EmuThreadStopTimeoutMs); + loop.exec(QEventLoop::ExcludeUserInputEvents); + } + TerminateEmulationThread(); OnEmulationStopped(); } @@ -3069,8 +3192,6 @@ void MainWindow::OnPauseContinueGame() { void MainWindow::OnStopGame() { if (ConfirmShutdownGame()) { play_time_manager->Stop(); - // Update game list to show new play time - game_list->PopulateAsync(UISettings::values.game_dirs); if (OnShutdownBegin()) { OnShutdownBeginDialog(); } else { @@ -3967,6 +4088,129 @@ void MainWindow::OnGameListRefresh() { SetFirmwareVersion(); } +void MainWindow::OnGameListConvertToNup(const std::string& game_path) { + if (nup_running) + return; + + const auto root = FileSys::Nup::DumpRootFromMain(game_path); + if (!root) + return; + const QString convert_title = tr("Convert to NUP"); + const auto archive = FileSys::Nup::DefaultArchivePath(*root); + if (std::filesystem::exists(archive)) { + QMessageBox::information(this, convert_title, + tr("The target archive already exists:\n%1") + .arg(QString::fromStdString(Common::FS::PathToUTF8String(archive)))); + return; + } + + const bool resume = FileSys::Nup::HasResumableJournal(archive); + bool delete_sources = resume; + FileSys::Nup::Metadata metadata; + QString progress_title; + QPixmap progress_icon; + if (resume) { + if (QMessageBox::question(this, tr("Resume Conversion"), + tr("Resume the interrupted conversion?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) + return; + } else { + u64 needed{}; + { + std::error_code ec; + for (std::filesystem::recursive_directory_iterator it(*root, ec), end; + !ec && it != end; it.increment(ec)) { + if (it->is_regular_file(ec)) + needed += it->file_size(ec); + } + } + std::error_code space_error; + const auto space = std::filesystem::space(archive.parent_path(), space_error); + NupMetadataDialog dialog(*root, archive, needed, space_error ? 0 : space.available, this); + if (dialog.exec() != QDialog::Accepted) + return; + delete_sources = dialog.DeleteSources(); + metadata = dialog.Metadata(); + progress_title = dialog.DisplayName(); + progress_icon = dialog.CurrentIcon(); + } + + nup_running = true; + nup_source_path = game_path; + game_list->SetNupConversionRunning(true); + nup_cancel = std::make_shared<std::atomic_bool>(false); + nup_progress = new NupProgressDialog(progress_title, progress_icon, this); + connect(nup_progress, &NupProgressDialog::CancelRequested, this, + [this, cancel = nup_cancel] { + cancel->store(true); + if (nup_progress) + nup_progress->SetCancelling(); + }); + nup_progress->show(); + + const auto cancel = nup_cancel; + auto last_update = std::make_shared<std::chrono::steady_clock::time_point>(); + nup_watcher.setFuture(QtConcurrent::run([this, root = *root, archive, delete_sources, metadata, + cancel, last_update] { + std::string error; + const auto on_progress = [this, last_update]( + const FileSys::Nup::PackProgress& progress) { + const auto now = std::chrono::steady_clock::now(); + if (now - *last_update < std::chrono::milliseconds(200)) + return; + *last_update = now; + QMetaObject::invokeMethod( + this, + [this, progress] { + if (!nup_progress) + return; + nup_progress->SetProgress(progress); + }, + Qt::QueuedConnection); + }; + const auto result = FileSys::Nup::PackDump( + root, archive, {.delete_sources = delete_sources, .metadata = metadata}, on_progress, + cancel.get(), &error); + QMetaObject::invokeMethod( + this, + [this, result, delete_sources, error] { + nup_running = false; + nup_source_path.clear(); + game_list->SetNupConversionRunning(false); + if (nup_progress) { + nup_progress->Finish(); + nup_progress->deleteLater(); + nup_progress = nullptr; + } + nup_cancel.reset(); + switch (result) { + case FileSys::Nup::PackResult::Success: + QMessageBox::information( + this, tr("Conversion Complete"), + delete_sources ? tr("The NUP was created and the loose folder was " + "removed.") + : tr("The NUP was created. You can delete the loose " + "folder if you no longer need it.")); + OnGameListRefresh(); + break; + case FileSys::Nup::PackResult::Cancelled: + if (delete_sources) + QMessageBox::information(this, tr("Conversion Cancelled"), + tr("The conversion can be resumed by choosing " + "Convert to NUP again.")); + break; + case FileSys::Nup::PackResult::InvalidSource: + case FileSys::Nup::PackResult::IoError: + QMessageBox::critical(this, tr("Conversion Failed"), + QString::fromStdString(error)); + break; + } + }, + Qt::QueuedConnection); + return result; + })); +} + void MainWindow::LaunchFirmwareApplet(u64 raw_program_id, std::optional<Service::NFP::CabinetMode> cabinet_mode) { auto const program_id = Service::AM::AppletProgramId(raw_program_id); @@ -4392,8 +4636,13 @@ void MainWindow::OnMouseActivity() { } } -void MainWindow::OnCheckFirmwareDecryption() { - if (!ContentManager::AreKeysPresent()) { +void MainWindow::OnCheckFirmwareDecryption(bool prompt) { + if (!ContentManager::AreKeysPresent() && !prompt) { + // At startup only log it: loose ExeFS/RomFS dumps (all Switch 2 games so far) need no + // keys, and a modal question blocked every unattended launch. Tools > Install Decryption + // Keys still installs them. + LOG_WARNING(Frontend, "Decryption keys are missing: NCA, NSP and XCI games need them"); + } else if (!ContentManager::AreKeysPresent()) { const auto res = QtCommon::Frontend::Warning( tr("Derivation Components Missing"), tr("Decryption keys are missing. Install them now?"), @@ -4576,10 +4825,28 @@ bool MainWindow::ConfirmClose() { } void MainWindow::closeEvent(QCloseEvent* event) { - if (!ConfirmClose()) { + if (!close_pending && !ConfirmClose()) { + event->ignore(); + return; + } + close_pending = true; + game_list->SetScanEnabled(false); + if (boot_in_progress) { event->ignore(); return; } + if (QtCommon::emu_thread && emulation_running) { + event->ignore(); + if (!QtCommon::emu_thread->isRunning()) { + OnEmulationStopped(); + return; + } + if (OnShutdownBegin()) { + OnShutdownBeginDialog(); + } + return; + } + LOG_INFO(Frontend, "Closing frontend after emulation cleanup"); UpdateUISettings(); game_list->SaveInterfaceLayout(); |
