diff options
| author | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:01:57 +0100 |
|---|---|---|
| committer | kraibse <33869366+kraibse@users.noreply.github.com> | 2026-11-02 16:01:57 +0100 |
| commit | b374709aa202d586dd09f81f3389da8d73fea0d7 (patch) | |
| tree | 5c4033d827c517aca9cc73dc497fbb3e81291bf1 /src/yuzu | |
| parent | 021c9724371e45fa694c9ee3106b9011364d2853 (diff) | |
| parent | ce1b1de6f1beed36958368f89cda466937ff9716 (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.cpp | 22 | ||||
| -rw-r--r-- | src/yuzu/configuration/shared_widget.cpp | 8 | ||||
| -rw-r--r-- | src/yuzu/game/game_list.cpp | 14 | ||||
| -rw-r--r-- | src/yuzu/game/game_list.h | 3 | ||||
| -rw-r--r-- | src/yuzu/main_window.cpp | 169 | ||||
| -rw-r--r-- | src/yuzu/main_window.h | 11 |
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; |
