aboutsummaryrefslogtreecommitdiff
path: root/src/yuzu
diff options
context:
space:
mode:
authorkraibse <33869366+kraibse@users.noreply.github.com>2026-11-02 16:01:57 +0100
committerkraibse <33869366+kraibse@users.noreply.github.com>2026-11-02 16:01:57 +0100
commitb374709aa202d586dd09f81f3389da8d73fea0d7 (patch)
tree5c4033d827c517aca9cc73dc497fbb3e81291bf1 /src/yuzu
parent021c9724371e45fa694c9ee3106b9011364d2853 (diff)
parentce1b1de6f1beed36958368f89cda466937ff9716 (diff)
Merge remote-tracking branch 'origin/nx2' into codex/upstream-loading-hotkeys
Diffstat (limited to 'src/yuzu')
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp22
-rw-r--r--src/yuzu/configuration/shared_widget.cpp8
-rw-r--r--src/yuzu/game/game_list.cpp14
-rw-r--r--src/yuzu/game/game_list.h3
-rw-r--r--src/yuzu/main_window.cpp169
-rw-r--r--src/yuzu/main_window.h11
6 files changed, 219 insertions, 8 deletions
diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp
index e4a140fff5..074b6f405b 100644
--- a/src/yuzu/configuration/configure_graphics.cpp
+++ b/src/yuzu/configuration/configure_graphics.cpp
@@ -218,6 +218,7 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
std::map<u32, QWidget*> hold_graphics;
std::vector<QWidget*> hold_api;
+ QCheckBox* frame_gen_checkbox = nullptr;
for (const auto setting : Settings::values.linkage.by_category[Settings::Category::Renderer]) {
#ifndef HAS_RESHADE
@@ -225,9 +226,6 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
continue;
}
#endif
- if (setting->Id() == Settings::values.frame_gen.Id()) {
- continue;
- }
ConfigurationShared::Widget* widget = [&]() {
if (setting->Id() == Settings::values.fsr_sharpening_slider.Id()) {
@@ -325,6 +323,9 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
hold_graphics.emplace(setting->Id(), widget);
#endif
+ } else if (setting->Id() == Settings::values.frame_gen.Id()) {
+ frame_gen_checkbox = widget->checkbox;
+ hold_graphics.emplace(setting->Id(), widget);
} else {
hold_graphics.emplace(setting->Id(), widget);
}
@@ -334,6 +335,21 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
graphics_layout.addWidget(widget);
}
+ // Hide rather than disable, a disabled widget would read as using the global value
+ if (frame_gen_checkbox != nullptr) {
+ for (const u32 id : {Settings::values.frame_gen_type.Id(),
+ Settings::values.frame_gen_multiplier.Id(),
+ Settings::values.frame_gen_target_rate.Id(),
+ Settings::values.frame_gen_flow_scale_auto.Id(),
+ Settings::values.frame_gen_flow_scale.Id(),
+ Settings::values.frame_gen_queue_target.Id()}) {
+ if (const auto it = hold_graphics.find(id); it != hold_graphics.end()) {
+ it->second->setVisible(frame_gen_checkbox->isChecked());
+ connect(frame_gen_checkbox, &QCheckBox::toggled, it->second, &QWidget::setVisible);
+ }
+ }
+ }
+
for (auto widget : hold_api) {
api_grid_layout->addWidget(widget);
}
diff --git a/src/yuzu/configuration/shared_widget.cpp b/src/yuzu/configuration/shared_widget.cpp
index 0e829e9100..4971c0cfa7 100644
--- a/src/yuzu/configuration/shared_widget.cpp
+++ b/src/yuzu/configuration/shared_widget.cpp
@@ -515,7 +515,13 @@ void Widget::SetupComponent(const QString& label, std::function<void()>& load_fu
layout->setContentsMargins(0, 0, 0, 0);
if (other_setting == nullptr) {
- other_setting = setting.PairedSetting();
+ // A paired setting without the Paired specialization gets a widget of its own, and
+ // several settings can share it, so they must not each save their own checkbox to it
+ auto* const paired_setting = setting.PairedSetting();
+ if (paired_setting != nullptr &&
+ paired_setting->Specialization() == Settings::Specialization::Paired) {
+ other_setting = paired_setting;
+ }
}
const bool require_checkbox = other_setting != nullptr && other_setting->TypeId() == "bool";
diff --git a/src/yuzu/game/game_list.cpp b/src/yuzu/game/game_list.cpp
index 17183c02ec..d2613697bf 100644
--- a/src/yuzu/game/game_list.cpp
+++ b/src/yuzu/game/game_list.cpp
@@ -24,6 +24,7 @@
#include "common/common_types.h"
#include "core/core.h"
+#include "core/file_sys/nup_writer.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/registered_cache.h"
#include "qt_common/config/uisettings.h"
@@ -99,6 +100,10 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
ResetViewMode();
}
+void GameList::SetNupConversionRunning(bool running) {
+ nup_conversion_running = running;
+}
+
GameList::~GameList() {
UnloadController();
}
@@ -481,6 +486,11 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC"));
QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));
+ QAction* convert_nup = nullptr;
+ if (FileSys::Nup::DumpRootFromMain(path)) {
+ convert_nup = context_menu.addAction(tr("Convert to NUP..."));
+ convert_nup->setEnabled(!nup_conversion_running);
+ }
#if !defined(__APPLE__)
QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
@@ -561,6 +571,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
[this, path]() { emit VerifyIntegrityRequested(path); });
connect(copy_tid, &QAction::triggered, this,
[this, program_id]() { emit CopyTIDRequested(program_id); });
+ if (convert_nup) {
+ connect(convert_nup, &QAction::triggered, this,
+ [this, path]() { emit ConvertToNupRequested(path); });
+ }
#if !defined(__APPLE__)
connect(create_desktop_shortcut, &QAction::triggered, this, [this, program_id, path]() {
emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Desktop);
diff --git a/src/yuzu/game/game_list.h b/src/yuzu/game/game_list.h
index 161e66195f..b33dfdd9c8 100644
--- a/src/yuzu/game/game_list.h
+++ b/src/yuzu/game/game_list.h
@@ -89,6 +89,7 @@ public:
void ResetViewMode();
public slots:
+ void SetNupConversionRunning(bool running);
void RefreshGameDirectory();
void RefreshExternalContent();
void UpdateIconSizes();
@@ -107,6 +108,7 @@ signals:
void SetPlayTimeRequested(u64 program_id);
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void VerifyIntegrityRequested(const std::string& game_path);
+ void ConvertToNupRequested(const std::string& game_path);
void CopyTIDRequested(u64 program_id);
void CreateShortcut(u64 program_id, const std::string& game_path,
const QtCommon::Game::ShortcutTarget target);
@@ -166,6 +168,7 @@ private:
Core::System& system;
bool m_isTreeMode = true;
+ bool nup_conversion_running{};
QAbstractItemView* m_currentView = nullptr;
};
diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp
index e374c9f27c..70000a22c9 100644
--- a/src/yuzu/main_window.cpp
+++ b/src/yuzu/main_window.cpp
@@ -152,6 +152,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"
@@ -453,7 +454,7 @@ MainWindow::MainWindow(bool has_broken_vulkan)
QtCommon::SetupContentProviders();
// Gen keys if necessary
- OnCheckFirmwareDecryption();
+ OnCheckFirmwareDecryption(false);
#ifdef __unix__
OnCheckGraphicsBackend();
@@ -557,6 +558,10 @@ MainWindow::MainWindow(bool has_broken_vulkan)
}
MainWindow::~MainWindow() {
+ if (nup_cancel) {
+ nup_cancel->store(true);
+ }
+ nup_watcher.waitForFinished();
#ifdef __APPLE__
AllowOSSleep();
#endif
@@ -1460,6 +1465,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);
@@ -1891,6 +1898,12 @@ 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;
}
@@ -4030,6 +4043,151 @@ 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;
+ 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;
+ 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);
+ 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)
+ return;
+ delete_sources = low_storage->isChecked();
+ }
+
+ 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->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,
+ 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;
+ 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));
+ },
+ Qt::QueuedConnection);
+ };
+ const auto result = FileSys::Nup::PackDump(
+ root, archive, {.delete_sources = delete_sources}, 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->close();
+ 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);
@@ -4455,8 +4613,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?"),
diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h
index 9e15177841..290ecac6fb 100644
--- a/src/yuzu/main_window.h
+++ b/src/yuzu/main_window.h
@@ -13,6 +13,7 @@
#include <QElapsedTimer>
#include <QMainWindow>
#include <QMessageBox>
+#include <QFutureWatcher>
#include <QPushButton>
#include <QTimer>
#include <QTranslator>
@@ -20,6 +21,7 @@
#include "common/common_types.h"
#include "common/settings_enums.h"
+#include "core/file_sys/nup_writer.h"
#include "frontend_common/content_manager.h"
#include "input_common/drivers/tas_input.h"
#include "qt_common/config/qt_config.h"
@@ -353,6 +355,7 @@ private slots:
void OnGameListSetPlayTime(u64 program_id);
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void OnGameListVerifyIntegrity(const std::string& game_path);
+ void OnGameListConvertToNup(const std::string& game_path);
void OnGameListCopyTID(u64 program_id);
void OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
const QtCommon::Game::ShortcutTarget target);
@@ -422,7 +425,8 @@ private slots:
void OnCreateHomeMenuDesktopShortcut();
void OnCreateHomeMenuApplicationMenuShortcut();
void OnCaptureScreenshot();
- void OnCheckFirmwareDecryption();
+ /// Checks for decryption keys; `prompt` asks to install them when they are missing.
+ void OnCheckFirmwareDecryption(bool prompt = true);
#ifdef __unix__
void OnCheckGraphicsBackend();
#endif
@@ -573,6 +577,11 @@ private:
// Install progress dialog
QProgressDialog* install_progress = nullptr;
+ QProgressDialog* nup_progress = nullptr;
+ QFutureWatcher<FileSys::Nup::PackResult> nup_watcher;
+ std::shared_ptr<std::atomic_bool> nup_cancel;
+ bool nup_running{};
+ std::string nup_source_path;
// Last game booted, used for multi-process apps
QString last_filename_booted;