From 98cc44dadd7ab4b63cf0685360ac50738e8e5c3e Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:13:48 -0500 Subject: yuzu: show NVIDIA's DLSS files in the Graphics tab and fetch them from NVIDIA Under the NVIDIA DLSS checkbox, a panel says whether the Streamline loader and DLSS were found and which versions, with "Open Folder" and a link to NVIDIA's Streamline releases. When files are missing, "Download from NVIDIA" takes only those files out of NVIDIA's Streamline 2.14.16 release zip on GitHub with HTTP range requests (about 1 MB for the loader, 32 MB with DLSS, of the 276 MB zip), after showing the Streamline and DLSS licence texts from the release for acceptance. Each file's CRC and signature are checked before it is saved to the user folder's "streamline" folder. Assisted-by: Claude Opus 5.5 (implementation) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/yuzu/configuration/nvidia_dlss_panel.cpp | 549 +++++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 src/yuzu/configuration/nvidia_dlss_panel.cpp (limited to 'src/yuzu/configuration/nvidia_dlss_panel.cpp') diff --git a/src/yuzu/configuration/nvidia_dlss_panel.cpp b/src/yuzu/configuration/nvidia_dlss_panel.cpp new file mode 100644 index 0000000000..fa1af23c4a --- /dev/null +++ b/src/yuzu/configuration/nvidia_dlss_panel.cpp @@ -0,0 +1,549 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifdef EGLANTINE_STREAMLINE + +#include "common/httplib.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/common_types.h" +#include "common/logging.h" +#include "video_core/vulkan_common/streamline.h" +#include "yuzu/configuration/nvidia_dlss_panel.h" + +#ifdef YUZU_BUNDLED_OPENSSL +#include +#endif + +namespace { + +namespace Streamline = Vulkan::Streamline; + +constexpr const char* RELEASE_PAGE = "https://github.com/NVIDIA-RTX/Streamline/releases"; +constexpr const char* RELEASE_VERSION = "2.14.16"; +constexpr const char* ZIP_HOST = "https://github.com"; +constexpr const char* ZIP_PATH = + "/NVIDIA-RTX/Streamline/releases/download/v2.14.16/streamline-sdk-v2.14.16.zip"; +constexpr const char* INTERPOSER_ENTRY = "bin/x64/sl.interposer.dll"; +constexpr const char* MODEL_ENTRY = "bin/x64/nvngx_dlss.dll"; +constexpr const char* STREAMLINE_LICENSE_ENTRY = "license.txt"; +constexpr const char* DLSS_LICENSE_ENTRY = "bin/x64/nvngx_dlss.license.txt"; + +QString Tr(const char* text) { + return QCoreApplication::translate("NvidiaDlssPanel", text); +} + +u16 U16(const std::string& data, size_t at) { + return static_cast(static_cast(data[at]) | static_cast(data[at + 1]) << 8); +} + +u32 U32(const std::string& data, size_t at) { + return static_cast(U16(data, at)) | static_cast(U16(data, at + 2)) << 16; +} + +u64 U64(const std::string& data, size_t at) { + return static_cast(U32(data, at)) | static_cast(U32(data, at + 4)) << 32; +} + +struct ZipEntry { + u16 method{}; + u32 crc{}; + u64 compressed{}; + u64 size{}; + u64 offset{}; +}; + +/// Reads single files out of a zip on a web server with range requests, so only the files +/// needed are downloaded (about 32 MB of the 276 MB release). +class RemoteZip { +public: + /// Follows GitHub's redirect to the file's (short-lived) download address. + bool Resolve() { + host = ZIP_HOST; + path = ZIP_PATH; + for (int redirect = 0; redirect < 5; ++redirect) { + const auto result = MakeClient()->Get(path, httplib::Headers{{"Range", "bytes=0-0"}}); + if (!result) { + error = httplib::to_string(result.error()); + return false; + } + if (result->status == 206) { + // "bytes 0-0/"; the server doesn't take "last N bytes" ranges. + const std::string range = result->get_header_value("Content-Range"); + const size_t slash = range.rfind('/'); + zip_size = slash == std::string::npos + ? 0 + : std::strtoull(range.c_str() + slash + 1, nullptr, 10); + if (zip_size == 0) { + error = "no file size"; + return false; + } + return true; + } + const std::string location = result->get_header_value("Location"); + const size_t scheme = location.find("://"); + const size_t slash = + scheme == std::string::npos ? std::string::npos : location.find('/', scheme + 3); + if (result->status < 300 || result->status >= 400 || slash == std::string::npos) { + error = fmt::format("HTTP {}", result->status); + return false; + } + host = location.substr(0, slash); + path = location.substr(slash); + } + error = "too many redirects"; + return false; + } + + bool Open() { + if (!Resolve()) { + return false; + } + const u64 tail_start = zip_size > 65558 ? zip_size - 65558 : 0; + const std::optional tail = + Fetch(fmt::format("bytes={}-{}", tail_start, zip_size - 1)); + if (!tail) { + return false; + } + const size_t end = tail->rfind(std::string{"PK\x05\x06", 4}); + if (end == std::string::npos || end + 22 > tail->size()) { + error = "no zip directory"; + return false; + } + u64 directory_size = U32(*tail, end + 12); + u64 directory_offset = U32(*tail, end + 16); + if (directory_offset == 0xFFFFFFFF && end >= 20 && + tail->compare(end - 20, 4, std::string{"PK\x06\x07", 4}) == 0) { + const u64 record = U64(*tail, end - 20 + 8); + const std::optional zip64 = + Fetch(fmt::format("bytes={}-{}", record, record + 55)); + if (!zip64 || zip64->size() < 56) { + return false; + } + directory_size = U64(*zip64, 40); + directory_offset = U64(*zip64, 48); + } + const std::optional directory = Fetch( + fmt::format("bytes={}-{}", directory_offset, directory_offset + directory_size - 1)); + if (!directory) { + return false; + } + for (size_t at = 0; at + 46 <= directory->size() && U32(*directory, at) == 0x02014b50;) { + const u16 name_length = U16(*directory, at + 28); + const u16 extra_length = U16(*directory, at + 30); + const u16 comment_length = U16(*directory, at + 32); + ZipEntry entry{ + .method = U16(*directory, at + 10), + .crc = U32(*directory, at + 16), + .compressed = U32(*directory, at + 20), + .size = U32(*directory, at + 24), + .offset = U32(*directory, at + 42), + }; + const std::string name = directory->substr(at + 46, name_length); + // Zip64 sizes and offset, in this order, for the fields that overflowed. + const size_t extra_end = at + 46 + name_length + extra_length; + for (size_t extra = at + 46 + name_length; extra + 4 <= extra_end;) { + const u16 id = U16(*directory, extra); + const u16 length = U16(*directory, extra + 2); + if (id == 0x0001) { + size_t field = extra + 4; + for (u64* value : {&entry.size, &entry.compressed, &entry.offset}) { + if (*value == 0xFFFFFFFF) { + *value = U64(*directory, field); + field += 8; + } + } + } + extra += 4 + length; + } + entries.emplace(name, entry); + at += 46 + name_length + extra_length + comment_length; + } + return true; + } + + const ZipEntry* Find(const std::string& name) const { + const auto it = entries.find(name); + return it == entries.end() ? nullptr : &it->second; + } + + std::optional Read(const ZipEntry& entry, + const std::function& progress = {}) { + const std::optional header = + Fetch(fmt::format("bytes={}-{}", entry.offset, entry.offset + 29)); + if (!header) { + return std::nullopt; + } + if (header->size() < 30 || U32(*header, 0) != 0x04034b50) { + error = "bad zip entry"; + return std::nullopt; + } + const u64 start = entry.offset + 30 + U16(*header, 26) + U16(*header, 28); + std::string data; + if (entry.compressed != 0) { + std::optional fetched = + Fetch(fmt::format("bytes={}-{}", start, start + entry.compressed - 1), progress); + if (!fetched) { + return std::nullopt; + } + data = std::move(*fetched); + } + std::string out; + if (entry.method == 0) { + out = std::move(data); + } else if (entry.method == 8) { + out.resize(entry.size); + z_stream stream{}; + if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) { + error = "zlib"; + return std::nullopt; + } + stream.next_in = reinterpret_cast(data.data()); + stream.avail_in = static_cast(data.size()); + stream.next_out = reinterpret_cast(out.data()); + stream.avail_out = static_cast(out.size()); + const int result = inflate(&stream, Z_FINISH); + inflateEnd(&stream); + if (result != Z_STREAM_END || stream.total_out != entry.size) { + error = "could not unpack"; + return std::nullopt; + } + } else { + error = "unknown zip compression"; + return std::nullopt; + } + const uLong crc = crc32(0L, reinterpret_cast(out.data()), + static_cast(out.size())); + if (crc != entry.crc) { + error = "damaged download (checksum)"; + return std::nullopt; + } + return out; + } + + bool canceled{}; + std::string error; + +private: + std::unique_ptr MakeClient() const { + auto client = std::make_unique(host); + client->set_connection_timeout(30); + client->set_read_timeout(30); + client->set_write_timeout(30); +#ifdef YUZU_BUNDLED_OPENSSL + client->load_ca_cert_store(kCert, sizeof(kCert)); +#endif + return client; + } + + std::optional Fetch(const std::string& range, + const std::function& progress = {}) { + std::string body; + int status = 0; + const auto result = MakeClient()->Get( + path, httplib::Headers{{"Range", range}}, + [&status](const httplib::Response& response) { + status = response.status; + return response.status == 206; + }, + [&](const char* data, size_t length) { + body.append(data, length); + if (progress && !progress(body.size())) { + canceled = true; + return false; + } + return true; + }); + if (!result || status != 206) { + error = result ? fmt::format("HTTP {}", status) : httplib::to_string(result.error()); + return std::nullopt; + } + return body; + } + + std::string host; + std::string path; + u64 zip_size{}; + std::map entries; +}; + +QString Named(const QString& name, const std::string& version) { + return version.empty() ? name : QStringLiteral("%1 %2").arg(name, QString::fromStdString(version)); +} + +QString PathText(const std::filesystem::path& path) { + return QString::fromStdWString(path.wstring()); +} + +} // Anonymous namespace + +NvidiaDlssPanel::NvidiaDlssPanel(QWidget* parent) : QWidget(parent) { + QVBoxLayout* layout = new QVBoxLayout(this); + layout->setContentsMargins(24, 0, 0, 8); + layout->setSpacing(6); + + QHBoxLayout* status_row = new QHBoxLayout; + status_row->setSpacing(8); + icon_label = new QLabel(this); + icon_label->setTextFormat(Qt::RichText); + icon_label->setAlignment(Qt::AlignTop | Qt::AlignHCenter); + icon_label->setFixedWidth(16); + status_label = new QLabel(this); + status_label->setTextFormat(Qt::RichText); + status_label->setWordWrap(true); + status_row->addWidget(icon_label); + status_row->addWidget(status_label, 1); + + QHBoxLayout* button_row = new QHBoxLayout; + button_row->setContentsMargins(24, 0, 0, 0); + download_button = new QPushButton(this); + folder_button = new QPushButton(Tr("Open Folder"), this); + QLabel* about = new QLabel( + QStringLiteral("%2").arg(QString::fromLatin1(RELEASE_PAGE), + Tr("Where do these files come from?")), + this); + about->setOpenExternalLinks(true); + about->setToolTip(Tr("NVIDIA's Streamline releases on GitHub. The files are in bin\\x64 of " + "the release zip.")); + button_row->addWidget(download_button); + button_row->addWidget(folder_button); + button_row->addStretch(1); + button_row->addWidget(about); + + layout->addLayout(status_row); + layout->addLayout(button_row); + + connect(download_button, &QPushButton::clicked, this, [this] { Download(); }); + connect(folder_button, &QPushButton::clicked, this, [] { + const std::filesystem::path folder = Streamline::FindDlssFiles().folder; + std::error_code ec; + std::filesystem::create_directories(folder, ec); + QDesktopServices::openUrl(QUrl::fromLocalFile(PathText(folder))); + }); + Refresh(); +} + +void NvidiaDlssPanel::showEvent(QShowEvent* event) { + Refresh(); + QWidget::showEvent(event); +} + +void NvidiaDlssPanel::Refresh() { + const Streamline::DlssFiles files = Streamline::FindDlssFiles(); + const bool has_loader = !files.interposer.empty(); + const bool has_model = !files.model.empty() || !files.app_model.empty(); + const QString dlss = !files.model.empty() + ? Named(QStringLiteral("DLSS"), files.model_version) + : Tr("%1 from the NVIDIA app") + .arg(Named(QStringLiteral("DLSS"), files.app_model_version)); + + QString status; + QString button; + if (has_loader && has_model) { + status = Tr("Ready. Found %1 and %2.") + .arg(Named(QStringLiteral("NVIDIA Streamline"), files.interposer_version), dlss); + } else if (has_model) { + status = Tr("One file needed: NVIDIA's Streamline loader (sl.interposer.dll). " + "%1 will be used. Until then, games use AMD FSR 3.1.") + .arg(dlss); + button = Tr("Download from NVIDIA (1 MB)"); + } else if (has_loader) { + status = Tr("One file needed: NVIDIA DLSS (nvngx_dlss.dll). Until then, games use " + "AMD FSR 3.1."); + button = Tr("Download from NVIDIA (31 MB)"); + } else { + status = Tr("NVIDIA's files needed: the Streamline loader and DLSS " + "(sl.interposer.dll and nvngx_dlss.dll). Until then, games use AMD FSR 3.1."); + button = Tr("Download from NVIDIA (32 MB)"); + } + const bool ready = has_loader && has_model; + icon_label->setText(ready ? QStringLiteral("✔") + : QStringLiteral("⚠")); + status_label->setText(status); + status_label->setToolTip(Tr("Folder: %1").arg(PathText(files.folder))); + download_button->setText(button); + download_button->setVisible(!ready); +} + +void NvidiaDlssPanel::Download() { + const Streamline::DlssFiles files = Streamline::FindDlssFiles(); + std::vector wanted; + if (files.interposer.empty()) { + wanted.emplace_back(INTERPOSER_ENTRY); + } + if (files.model.empty() && files.app_model.empty()) { + wanted.emplace_back(MODEL_ENTRY); + } + if (wanted.empty()) { + return; + } + const std::filesystem::path folder = Streamline::UserFolder(); + const auto fail = [this](const std::string& error) { + LOG_ERROR(Frontend, "NVIDIA DLSS download failed: {}", error); + QMessageBox::warning(this, Tr("NVIDIA DLSS"), + Tr("Could not download NVIDIA's files: %1") + .arg(QString::fromStdString(error))); + Refresh(); + }; + + RemoteZip zip; + std::string licences; + { + QProgressDialog busy(Tr("Contacting NVIDIA's download..."), QString{}, 0, 0, this); + busy.setWindowTitle(Tr("NVIDIA DLSS")); + busy.setWindowModality(Qt::WindowModal); + busy.setMinimumDuration(0); + busy.show(); + QApplication::processEvents(); + if (!zip.Open()) { + busy.close(); + fail(zip.error); + return; + } + std::vector> texts{ + {Tr("NVIDIA Streamline"), STREAMLINE_LICENSE_ENTRY}}; + if (std::ranges::find(wanted, MODEL_ENTRY) != wanted.end()) { + texts.emplace_back(Tr("NVIDIA DLSS"), DLSS_LICENSE_ENTRY); + } + for (const auto& [title, entry_name] : texts) { + const ZipEntry* const entry = zip.Find(entry_name); + const std::optional text = entry ? zip.Read(*entry) : std::nullopt; + licences += fmt::format("==== {} ====\n\n{}\n\n", title.toStdString(), + text ? *text : std::string{"(see the release on GitHub)"}); + } + } + + QStringList names; + for (const std::string& entry : wanted) { + names << QString::fromStdString(std::filesystem::path{entry}.filename().string()); + } + QDialog dialog(this); + dialog.setWindowTitle(Tr("Download NVIDIA DLSS")); + QVBoxLayout* layout = new QVBoxLayout(&dialog); + QLabel* intro = new QLabel( + Tr("Eglantine will download %1 from NVIDIA's Streamline %2 release on GitHub into %3.
" + "These files are NVIDIA's, under NVIDIA's licence terms below.") + .arg(names.join(QStringLiteral(" and ")), QString::fromLatin1(RELEASE_VERSION), + PathText(folder)), + &dialog); + intro->setWordWrap(true); + QPlainTextEdit* licence_text = new QPlainTextEdit(QString::fromStdString(licences), &dialog); + licence_text->setReadOnly(true); + licence_text->setMinimumSize(640, 380); + QDialogButtonBox* buttons = new QDialogButtonBox(&dialog); + buttons->addButton(Tr("Accept and Download"), QDialogButtonBox::AcceptRole); + buttons->addButton(QDialogButtonBox::Cancel); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(intro); + layout->addWidget(licence_text, 1); + layout->addWidget(buttons); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + u64 total = 0; + std::vector entries; + for (const std::string& name : wanted) { + const ZipEntry* const entry = zip.Find(name); + if (!entry) { + fail(name + " is not in the release"); + return; + } + entries.push_back(entry); + total += entry->compressed; + } + QProgressDialog progress(Tr("Downloading NVIDIA's DLSS files..."), Tr("Cancel"), 0, 1000, + this); + progress.setWindowTitle(Tr("NVIDIA DLSS")); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(0); + progress.setValue(0); + QApplication::processEvents(); + // The download address from the first step only lasts a few minutes. + if (!zip.Resolve()) { + progress.close(); + fail(zip.error); + return; + } + std::error_code ec; + std::filesystem::create_directories(folder, ec); + u64 done = 0; + for (size_t i = 0; i < wanted.size(); ++i) { + const std::optional contents = zip.Read(*entries[i], [&](u64 received) { + progress.setValue(static_cast((done + received) * 1000 / std::max(total, 1))); + QApplication::processEvents(); + return !progress.wasCanceled(); + }); + if (!contents) { + progress.close(); + if (!zip.canceled) { + fail(zip.error); + } + return; + } + done += entries[i]->compressed; + const std::filesystem::path target = folder / std::filesystem::path{wanted[i]}.filename(); + std::filesystem::path partial = target; + partial += L".download"; + { + std::ofstream file(partial, std::ios::binary | std::ios::trunc); + file.write(contents->data(), static_cast(contents->size())); + if (!file) { + progress.close(); + fail("could not write " + partial.string()); + return; + } + } + const bool signed_ok = wanted[i] == INTERPOSER_ENTRY ? Streamline::IsStreamlineModule(partial) + : Streamline::IsSignedByNvidia(partial); + if (!signed_ok) { + std::filesystem::remove(partial, ec); + progress.close(); + fail(target.filename().string() + " is not signed by NVIDIA"); + return; + } + std::filesystem::rename(partial, target, ec); + if (ec) { + std::filesystem::remove(partial, ec); + progress.close(); + fail("could not save " + target.string()); + return; + } + LOG_INFO(Frontend, "Downloaded {} from NVIDIA's Streamline {} release", target.string(), + RELEASE_VERSION); + } + progress.setValue(1000); + progress.close(); + Refresh(); + QMessageBox::information(this, Tr("NVIDIA DLSS"), + Tr("NVIDIA DLSS is ready. It is used the next time a game starts.")); +} + +#endif -- cgit v1.2.3 From f4f076fe447b2ddc158fcc12ee81ed2af2aecad7 Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:56:27 -0500 Subject: video_core, yuzu: write NVIDIA DLSS file paths to logs as UTF-8 The Streamline start-up and DLSS download code formatted paths with std::filesystem::path::string(), which on MSVC converts to the active code page and throws when a character has no mapping there, for example a user name in another script under %APPDATA%. The start-up log runs inside the renderer's constructor, which only catches vk::Exception, so such a user would crash at game start with the setting on. Use Common::FS::PathToUTF8String for every path in these logs and messages. Found by CodeRabbit review. Assisted-by: Claude Opus 5.5 (implementation) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/video_core/vulkan_common/streamline.cpp | 17 ++++++++++------- src/yuzu/configuration/nvidia_dlss_panel.cpp | 14 ++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) (limited to 'src/yuzu/configuration/nvidia_dlss_panel.cpp') diff --git a/src/video_core/vulkan_common/streamline.cpp b/src/video_core/vulkan_common/streamline.cpp index 966de82cb5..4f8fd796c1 100644 --- a/src/video_core/vulkan_common/streamline.cpp +++ b/src/video_core/vulkan_common/streamline.cpp @@ -15,6 +15,7 @@ #include +#include "common/fs/fs_util.h" #include "common/fs/path_util.h" #include "common/logging.h" #include "common/settings.h" @@ -168,7 +169,7 @@ std::filesystem::path UseAppModel(const DlssFiles& files) { if (!current) { if (!IsSignedByNvidia(files.app_model)) { LOG_WARNING(Render_Vulkan, "The NVIDIA app's DLSS {} is not signed by NVIDIA", - files.app_model.string()); + Common::FS::PathToUTF8String(files.app_model)); return {}; } std::filesystem::create_directories(dir, ec); @@ -176,7 +177,7 @@ std::filesystem::path UseAppModel(const DlssFiles& files) { std::filesystem::copy_options::overwrite_existing, ec); if (ec) { LOG_WARNING(Render_Vulkan, "Could not copy the NVIDIA app's DLSS to {}: {}", - target.string(), ec.message()); + Common::FS::PathToUTF8String(target), ec.message()); return {}; } } @@ -186,12 +187,13 @@ std::filesystem::path UseAppModel(const DlssFiles& files) { bool Load(State& state, const std::filesystem::path& path) { if (!IsStreamlineModule(path)) { LOG_ERROR(Render_Vulkan, "{} is not signed by NVIDIA; not loading it", - path.string()); + Common::FS::PathToUTF8String(path)); return false; } state.module = LoadLibraryW(path.c_str()); if (!state.module) { - LOG_ERROR(Render_Vulkan, "Could not load {} (error {})", path.string(), GetLastError()); + LOG_ERROR(Render_Vulkan, "Could not load {} (error {})", Common::FS::PathToUTF8String(path), + GetLastError()); return false; } Api& api = state.api; @@ -298,7 +300,7 @@ bool Start() { LOG_WARNING(Render_Vulkan, "NVIDIA DLSS is on, but sl.interposer.dll (NVIDIA Streamline) is not in " "{} (the Graphics settings can download it); using FSR 3.1", - files.folder.string()); + Common::FS::PathToUTF8String(files.folder)); return false; } if (!Load(state, files.interposer)) { @@ -313,7 +315,7 @@ bool Start() { } } else if (files.model.empty()) { LOG_WARNING(Render_Vulkan, "No nvngx_dlss.dll in {} and no DLSS from the NVIDIA app", - files.folder.string()); + Common::FS::PathToUTF8String(files.folder)); } } @@ -371,7 +373,8 @@ bool Start() { sl::FeatureVersion version{}; api.get_feature_version(sl::kFeatureDLSS, version); LOG_INFO(Render_Vulkan, "NVIDIA Streamline {} started from {} (DLSS {})", - version.versionSL.toStr(), std::filesystem::path{state.plugin_dir}.string(), + version.versionSL.toStr(), + Common::FS::PathToUTF8String(std::filesystem::path{state.plugin_dir}), version.versionNGX.toStr()); state.started = true; return true; diff --git a/src/yuzu/configuration/nvidia_dlss_panel.cpp b/src/yuzu/configuration/nvidia_dlss_panel.cpp index fa1af23c4a..2329693a03 100644 --- a/src/yuzu/configuration/nvidia_dlss_panel.cpp +++ b/src/yuzu/configuration/nvidia_dlss_panel.cpp @@ -33,6 +33,7 @@ #include #include "common/common_types.h" +#include "common/fs/fs_util.h" #include "common/logging.h" #include "video_core/vulkan_common/streamline.h" #include "yuzu/configuration/nvidia_dlss_panel.h" @@ -441,7 +442,8 @@ void NvidiaDlssPanel::Download() { QStringList names; for (const std::string& entry : wanted) { - names << QString::fromStdString(std::filesystem::path{entry}.filename().string()); + names << QString::fromStdString( + Common::FS::PathToUTF8String(std::filesystem::path{entry}.filename())); } QDialog dialog(this); dialog.setWindowTitle(Tr("Download NVIDIA DLSS")); @@ -517,7 +519,7 @@ void NvidiaDlssPanel::Download() { file.write(contents->data(), static_cast(contents->size())); if (!file) { progress.close(); - fail("could not write " + partial.string()); + fail("could not write " + Common::FS::PathToUTF8String(partial)); return; } } @@ -526,18 +528,18 @@ void NvidiaDlssPanel::Download() { if (!signed_ok) { std::filesystem::remove(partial, ec); progress.close(); - fail(target.filename().string() + " is not signed by NVIDIA"); + fail(Common::FS::PathToUTF8String(target.filename()) + " is not signed by NVIDIA"); return; } std::filesystem::rename(partial, target, ec); if (ec) { std::filesystem::remove(partial, ec); progress.close(); - fail("could not save " + target.string()); + fail("could not save " + Common::FS::PathToUTF8String(target)); return; } - LOG_INFO(Frontend, "Downloaded {} from NVIDIA's Streamline {} release", target.string(), - RELEASE_VERSION); + LOG_INFO(Frontend, "Downloaded {} from NVIDIA's Streamline {} release", + Common::FS::PathToUTF8String(target), RELEASE_VERSION); } progress.setValue(1000); progress.close(); -- cgit v1.2.3 From f9d0c802e8e4e71644bf4b7e6cceca8e6df22464 Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:56:43 -0500 Subject: yuzu: bound the zip directory reads in the DLSS download The central-directory parser trusted each entry's name, extra and comment lengths and the Zip64 extra field's length, so a malformed directory could make it read past the downloaded buffer. The data comes from NVIDIA's GitHub release over TLS and the saved files are signature-checked, so the risk is low. Stop with an error when an entry runs past the directory, and read Zip64 fields only inside their record. Found by CodeRabbit review. Assisted-by: Claude Opus 5.5 (implementation) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/yuzu/configuration/nvidia_dlss_panel.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'src/yuzu/configuration/nvidia_dlss_panel.cpp') diff --git a/src/yuzu/configuration/nvidia_dlss_panel.cpp b/src/yuzu/configuration/nvidia_dlss_panel.cpp index 2329693a03..040ea90557 100644 --- a/src/yuzu/configuration/nvidia_dlss_panel.cpp +++ b/src/yuzu/configuration/nvidia_dlss_panel.cpp @@ -166,16 +166,21 @@ public: .size = U32(*directory, at + 24), .offset = U32(*directory, at + 42), }; + const size_t extra_end = at + 46 + name_length + extra_length; + if (extra_end + comment_length > directory->size()) { + error = "bad zip directory"; + return false; + } const std::string name = directory->substr(at + 46, name_length); // Zip64 sizes and offset, in this order, for the fields that overflowed. - const size_t extra_end = at + 46 + name_length + extra_length; for (size_t extra = at + 46 + name_length; extra + 4 <= extra_end;) { const u16 id = U16(*directory, extra); const u16 length = U16(*directory, extra + 2); + const size_t record_end = std::min(extra + 4 + length, extra_end); if (id == 0x0001) { size_t field = extra + 4; for (u64* value : {&entry.size, &entry.compressed, &entry.offset}) { - if (*value == 0xFFFFFFFF) { + if (*value == 0xFFFFFFFF && field + 8 <= record_end) { *value = U64(*directory, field); field += 8; } -- cgit v1.2.3