aboutsummaryrefslogtreecommitdiff
path: root/src/yuzu/main_window.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/yuzu/main_window.cpp')
-rw-r--r--src/yuzu/main_window.cpp329
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();