aboutsummaryrefslogtreecommitdiff
path: root/src/yuzu/main_window.cpp
diff options
context:
space:
mode:
authorPastaLaPate <pastalapate@proton.me>2026-11-02 20:34:15 +0100
committerGitHub <noreply@github.com>2026-11-02 20:34:15 +0100
commit6f002badce45fa2fdb414043544c829e834a7e27 (patch)
tree51c21a851fbc8218ebe7febb69fb710a891cc4fc /src/yuzu/main_window.cpp
parent396f011ea875dfe4fdb1b0f489651ab74a505b74 (diff)
parent046646e3514ca4950622d75609e68498f889989e (diff)
Merge branch 'nx2' into ci/qt-6.12.0
Diffstat (limited to 'src/yuzu/main_window.cpp')
-rw-r--r--src/yuzu/main_window.cpp213
1 files changed, 139 insertions, 74 deletions
diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp
index 2b43bcd2d0..e5a8f81424 100644
--- a/src/yuzu/main_window.cpp
+++ b/src/yuzu/main_window.cpp
@@ -42,6 +42,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 +74,11 @@
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
+#include <QElapsedTimer>
+#include <QEventLoop>
#include <QFileDialog>
#include <QFileInfo>
+#include <QFutureWatcher>
#include <QGuiApplication>
#include <QInputDialog>
#include <QMimeData>
@@ -451,7 +455,7 @@ MainWindow::MainWindow(bool has_broken_vulkan)
QtCommon::SetupContentProviders();
// Gen keys if necessary
- OnCheckFirmwareDecryption();
+ OnCheckFirmwareDecryption(false);
#ifdef __unix__
OnCheckGraphicsBackend();
@@ -1293,7 +1297,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"));
@@ -1833,24 +1836,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:
@@ -1920,6 +1905,31 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa
"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 ||
@@ -1973,9 +1983,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);
@@ -2110,6 +2154,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;
}
@@ -2120,6 +2167,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();
@@ -2139,7 +2188,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);
@@ -2163,14 +2213,16 @@ void MainWindow::OnEmulationStopTimeExpired() {
}
void MainWindow::OnEmulationStopped() {
+ if (!emulation_running) {
+ return;
+ }
+ if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) {
+ return;
+ }
shutdown_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();
}
@@ -2239,6 +2291,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() {
@@ -2250,6 +2309,13 @@ void MainWindow::ShutdownGame() {
play_time_manager->Stop();
OnShutdownBegin();
OnEmulationStopTimeExpired();
+ if (QtCommon::emu_thread) {
+ QEventLoop loop;
+ connect(QtCommon::emu_thread.get(), &QThread::finished, &loop, &QEventLoop::quit);
+ if (QtCommon::emu_thread->isRunning()) {
+ loop.exec(QEventLoop::ExcludeUserInputEvents);
+ }
+ }
OnEmulationStopped();
}
@@ -3082,8 +3148,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 {
@@ -3998,21 +4062,15 @@ void MainWindow::OnGameListConvertToNup(const std::string& game_path) {
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 {
- QMessageBox prompt(QMessageBox::Question, convert_title,
- tr("Source folder:\n%1\n\nTarget archive:\n%2")
- .arg(QString::fromStdString(Common::FS::PathToUTF8String(*root)),
- QString::fromStdString(Common::FS::PathToUTF8String(archive))),
- QMessageBox::Yes | QMessageBox::No, this);
- // QMessageBox takes ownership of its check box.
- auto* low_storage = new QCheckBox(tr("Low storage: delete the original files as they "
- "are packed (cannot be undone)"));
- prompt.setCheckBox(low_storage);
u64 needed{};
{
std::error_code ec;
@@ -4024,38 +4082,31 @@ void MainWindow::OnGameListConvertToNup(const std::string& game_path) {
}
std::error_code space_error;
const auto space = std::filesystem::space(archive.parent_path(), space_error);
- const auto to_mb = [](u64 bytes) { return bytes / (1024 * 1024); };
- const QString space_info = tr("\n\nApproximate space needed: %1 MB\nFree space on target "
- "volume: %2 MB")
- .arg(to_mb(needed))
- .arg(space_error ? 0 : to_mb(space.available));
- prompt.setInformativeText(space_info);
- if (!space_error && space.available < needed) {
- prompt.setText(prompt.text() + tr("\n\nWarning: free space is less than the "
- "estimated space needed."));
- }
- if (prompt.exec() != QMessageBox::Yes)
+ NupMetadataDialog dialog(*root, archive, needed, space_error ? 0 : space.available, this);
+ if (dialog.exec() != QDialog::Accepted)
return;
- delete_sources = low_storage->isChecked();
+ 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 QProgressDialog(tr("Preparing conversion..."), tr("Cancel"), 0, 1000, this);
- nup_progress->setWindowTitle(tr("Converting to NUP"));
- nup_progress->setMinimumDuration(0);
- nup_progress->setAutoClose(false);
- nup_progress->setAutoReset(false);
- connect(nup_progress, &QProgressDialog::canceled, this,
- [cancel = nup_cancel] { cancel->store(true); });
+ 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,
+ 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](
@@ -4069,22 +4120,13 @@ void MainWindow::OnGameListConvertToNup(const std::string& game_path) {
[this, progress] {
if (!nup_progress)
return;
- const auto percent = progress.bytes_total == 0
- ? 0
- : static_cast<int>(progress.bytes_done * 1000 /
- progress.bytes_total);
- nup_progress->setValue(percent);
- nup_progress->setLabelText(
- tr("%1 / %2 MB\n%3 / %4 files")
- .arg(progress.bytes_done / (1024 * 1024))
- .arg(progress.bytes_total / (1024 * 1024))
- .arg(progress.files_done)
- .arg(progress.files_total));
+ nup_progress->SetProgress(progress);
},
Qt::QueuedConnection);
};
const auto result = FileSys::Nup::PackDump(
- root, archive, {.delete_sources = delete_sources}, on_progress, cancel.get(), &error);
+ root, archive, {.delete_sources = delete_sources, .metadata = metadata}, on_progress,
+ cancel.get(), &error);
QMetaObject::invokeMethod(
this,
[this, result, delete_sources, error] {
@@ -4092,7 +4134,7 @@ void MainWindow::OnGameListConvertToNup(const std::string& game_path) {
nup_source_path.clear();
game_list->SetNupConversionRunning(false);
if (nup_progress) {
- nup_progress->close();
+ nup_progress->Finish();
nup_progress->deleteLater();
nup_progress = nullptr;
}
@@ -4550,8 +4592,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?"),
@@ -4734,10 +4781,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();