From 2b413a46a2820f0a646baaa8dbd683009f096f32 Mon Sep 17 00:00:00 2001 From: néo Luigi <85024393+neoluigi4123@users.noreply.github.com> Date: Sun, 1 Nov 2026 03:04:13 +0100 Subject: Vibration fix for ns2 games | Assisted by claude (code) --- src/core/hle/service/hid/hid_server.cpp | 39 +++++++++++++++++++++++++++++---- src/core/hle/service/hid/hid_server.h | 2 +- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/core/hle/service/hid/hid_server.cpp b/src/core/hle/service/hid/hid_server.cpp index 04b4663bc2..9d5f5f91d2 100644 --- a/src/core/hle/service/hid/hid_server.cpp +++ b/src/core/hle/service/hid/hid_server.cpp @@ -132,7 +132,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr r {210, C<&IHidServer::EndPermitVibrationSession>, "EndPermitVibrationSession"}, {211, C<&IHidServer::IsVibrationDeviceMounted>, "IsVibrationDeviceMounted"}, {212, C<&IHidServer::SendVibrationValueInBool>, "SendVibrationValueInBool"}, - {214, C<&IHidServer::SendNx2VibrationValues>, "SendNx2VibrationValues (guessed)"}, + {214, &IHidServer::SendNx2VibrationValues, "SendNx2VibrationValues (guessed)"}, {300, C<&IHidServer::ActivateConsoleSixAxisSensor>, "ActivateConsoleSixAxisSensor"}, {301, C<&IHidServer::StartConsoleSixAxisSensor>, "StartConsoleSixAxisSensor"}, {302, C<&IHidServer::StopConsoleSixAxisSensor>, "StopConsoleSixAxisSensor"}, @@ -1070,9 +1070,40 @@ Result IHidServer::IsVibrationDeviceMounted( R_SUCCEED(); } -Result IHidServer::SendNx2VibrationValues() { - LOG_DEBUG(Service_HID, "(STUBBED) called"); - R_SUCCEED(); +// TEMPORARY local diagnostic for #103: dumps the request layout of 214 when it changes. +void IHidServer::SendNx2VibrationValues(HLERequestContext& ctx) { + static std::string last_dump; + static u64 call_count = 0; + ++call_count; + + std::string dump = fmt::format("words(payload@{}):", ctx.GetDataPayloadOffset()); + const u32* cmd_buf = ctx.CommandBuffer(); + for (u32 i = 0; i < 16; ++i) { + dump += fmt::format(" {:08x}", cmd_buf[i]); + } + dump += fmt::format(" | X={} A={} B={} C={}", ctx.BufferDescriptorX().size(), + ctx.BufferDescriptorA().size(), ctx.BufferDescriptorB().size(), + ctx.BufferDescriptorC().size()); + const auto dump_buffer = [&dump](char kind, std::size_t index, std::span data) { + dump += fmt::format(" | {}{} size={}:", kind, index, data.size()); + for (std::size_t i = 0; i < std::min(data.size(), 96); ++i) { + dump += fmt::format("{}{:02x}", i % 4 == 0 ? " " : "", data[i]); + } + }; + for (std::size_t i = 0; i < ctx.BufferDescriptorX().size(); ++i) { + dump_buffer('X', i, ctx.ReadBufferX(i)); + } + for (std::size_t i = 0; i < ctx.BufferDescriptorA().size(); ++i) { + dump_buffer('A', i, ctx.ReadBufferA(i)); + } + + if (dump != last_dump) { + LOG_INFO(Service_HID, "214 #{}: {}", call_count, dump); + last_dump = std::move(dump); + } + + IPC::ResponseBuilder rb{ctx, 2}; + rb.Push(ResultSuccess); } Result IHidServer::SendVibrationValueInBool( diff --git a/src/core/hle/service/hid/hid_server.h b/src/core/hle/service/hid/hid_server.h index 574d54c415..beeb5ba17f 100644 --- a/src/core/hle/service/hid/hid_server.h +++ b/src/core/hle/service/hid/hid_server.h @@ -191,7 +191,7 @@ private: ClientAppletResourceUserId aruid); // Switch 2 sends vibration every frame with 214 (pointer buffers of device handles and // values, like 206); Eden doesn't emulate vibration, so accept and drop it. - Result SendNx2VibrationValues(); + void SendNx2VibrationValues(HLERequestContext& ctx); Result ActivateConsoleSixAxisSensor(ClientAppletResourceUserId aruid); Result StartConsoleSixAxisSensor(Core::HID::ConsoleSixAxisSensorHandle console_sixaxis_handle, ClientAppletResourceUserId aruid); -- cgit v1.2.3 From 890dc1e6c7d8828b2d994371854692d5b28783ee Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:34:43 +0100 Subject: nx2: improve save data handling for titles with missing NACP sizes to redirect the user to the correct save location --- src/yuzu/main_window.cpp | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index dbe5ea2334..1e37ee7e7e 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -2424,10 +2424,9 @@ void MainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, } }(); - const bool has_user_save{user_save_size > 0}; - const bool has_device_save{device_save_size > 0}; - - ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?"); + // Some titles (e.g. NX2 games) report no sizes in their NACP yet still create account saves + // at runtime, so only use the device layout when it is explicitly declared. + const bool use_device_save{device_save_size > 0 && user_save_size == 0}; switch (target) { case GameListOpenTarget::SaveData: { @@ -2436,26 +2435,23 @@ void MainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target, auto vfs_save_dir = QtCommon::vfs->OpenDirectory(Common::FS::PathToUTF8String(save_dir), FileSys::OpenMode::Read); - if (has_user_save) { - // User save data + u128 save_user_id{}; + if (!use_device_save) { const auto user_id = GetProfileID(); - assert(user_id); - - const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( - {}, vfs_save_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, - program_id, user_id->AsU128(), 0); + if (user_id) { + save_user_id = user_id->AsU128(); + } + } - path = Common::FS::ConcatPathSafe(save_dir, user_save_data_path); - } else { - // Device save data - const auto device_save_data_path = FileSys::SaveDataFactory::GetFullPath( - {}, vfs_save_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, - program_id, {}, 0); + const auto save_data_path = FileSys::SaveDataFactory::GetFullPath( + {}, vfs_save_dir, FileSys::SaveDataSpaceId::User, FileSys::SaveDataType::Account, + program_id, save_user_id, 0); - path = Common::FS::ConcatPathSafe(save_dir, device_save_data_path); - } + path = Common::FS::ConcatPathSafe(save_dir, save_data_path); - if (!Common::FS::CreateDirs(path)) { + // Only create the folder for games that are known to need it; otherwise an empty + // directory would be left behind in the wrong place. + if ((user_save_size > 0 || device_save_size > 0) && !Common::FS::CreateDirs(path)) { LOG_ERROR(Frontend, "Unable to create the directories for save data"); } -- cgit v1.2.3 From bd12e7c4ef7ddfcc56d88ee71612a671f582e47b Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:35:08 +0100 Subject: fix: update BUILD_ID definition and improve path handling in GameListWorker --- src/common/scm_rev.cpp.in | 1 + src/qt_common/game_list/worker.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/scm_rev.cpp.in b/src/common/scm_rev.cpp.in index 21e58e24b5..5fe066d7d9 100644 --- a/src/common/scm_rev.cpp.in +++ b/src/common/scm_rev.cpp.in @@ -13,6 +13,7 @@ #define BUILD_DATE "@BUILD_DATE@" #define BUILD_FULLNAME "@BUILD_FULLNAME@" #define BUILD_VERSION "@BUILD_VERSION@" +#undef BUILD_ID #define BUILD_ID "@BUILD_ID@" #define TITLE_BAR_FORMAT_IDLE "@TITLE_BAR_FORMAT_IDLE@" #define TITLE_BAR_FORMAT_RUNNING "@TITLE_BAR_FORMAT_RUNNING@" diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp index bd3d81dac4..329fc06990 100644 --- a/src/qt_common/game_list/worker.cpp +++ b/src/qt_common/game_list/worker.cpp @@ -18,6 +18,7 @@ #include #include "common/fs/fs.h" +#include "common/fs/fs_util.h" #include "common/fs/path_util.h" #include "common/settings.h" #include "core/core.h" @@ -552,7 +553,7 @@ void GameListWorker::run() { DirEntryReady(game_list_dir); if (scan) { const auto discovery = GameListDiscovery::Discover( - std::filesystem::u8path(game_dir.path), game_dir.deep_scan, + std::filesystem::path(Common::FS::ToU8String(game_dir.path)), game_dir.deep_scan, [](const auto& path) { const auto name = Common::FS::PathToUTF8String(path); return HasSupportedFileExtension(name) || IsExtractedNCAMain(name); -- cgit v1.2.3 From 0b927d171c6d918a675c25f57b2bc0121b7f889d Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 01:39:54 +0100 Subject: network: close each bsd fd once and drop unwanted or malformed room packets Two threads closing the same fd both copied the descriptor and both called Close(), so a host socket could be closed twice, and the second closesocket could hit an unrelated socket that had reused the host fd. CloseImpl now frees the slot under file_descriptors_mutex first and only that thread closes the socket; the other gets EBADF. OnProxyPacketReceived decompressed every room packet when any fd had a socket, including native sockets and proxy sockets on other ports. Sockets now report through AcceptsProxyPacket whether they want a packet, and only those are collected, so an unwanted packet is not decompressed. DecompressDataZSTD passed the frame's declared size straight to the vector constructor, so a malformed or unsized frame from a room member threw on the room thread. The new TryDecompressDataZSTD rejects invalid, unsized and oversized frames, and bsd drops such packets with a warning. DecompressDataZSTD now returns an empty vector for them instead of throwing. Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/common/zstd_compression.cpp | 20 +++++++++---- src/common/zstd_compression.h | 13 +++++++++ src/core/hle/service/sockets/bsd.cpp | 46 ++++++++++++++++++------------ src/core/hle/service/sockets/bsd.h | 2 ++ src/core/internal_network/network.cpp | 4 +++ src/core/internal_network/socket_proxy.cpp | 28 ++++++++++++------ src/core/internal_network/socket_proxy.h | 5 ++++ src/core/internal_network/sockets.h | 5 ++++ 8 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index 7665482ee6..9bb8d7b919 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -35,16 +35,26 @@ std::vector CompressDataZSTDDefault(const u8* source, std::size_t source_siz } std::vector DecompressDataZSTD(std::span compressed) { - const std::size_t decompressed_size = + return TryDecompressDataZSTD(compressed, std::vector{}.max_size()) + .value_or(std::vector{}); +} + +std::optional> TryDecompressDataZSTD(std::span compressed, + std::size_t max_size) { + // The size comes from the frame header, so check it before allocating + const unsigned long long decompressed_size = ZSTD_getFrameContentSize(compressed.data(), compressed.size()); - std::vector decompressed(decompressed_size); + if (decompressed_size == ZSTD_CONTENTSIZE_ERROR || + decompressed_size == ZSTD_CONTENTSIZE_UNKNOWN || decompressed_size > max_size) { + return std::nullopt; + } + std::vector decompressed(static_cast(decompressed_size)); const std::size_t uncompressed_result_size = ZSTD_decompress( decompressed.data(), decompressed.size(), compressed.data(), compressed.size()); - if (decompressed_size != uncompressed_result_size || ZSTD_isError(uncompressed_result_size)) { - // Decompression failed - return {}; + if (ZSTD_isError(uncompressed_result_size) || uncompressed_result_size != decompressed_size) { + return std::nullopt; } return decompressed; } diff --git a/src/common/zstd_compression.h b/src/common/zstd_compression.h index 819d7d201a..c20d96adb0 100644 --- a/src/common/zstd_compression.h +++ b/src/common/zstd_compression.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include @@ -45,6 +46,18 @@ namespace Common::Compression { */ [[nodiscard]] std::vector DecompressDataZSTD(std::span compressed); +/** + * Decompresses a single Zstandard frame that declares its content size. + * + * @param compressed the compressed source memory region. + * @param max_size the largest content size to accept. + * + * @return the decompressed data, or nullopt if the frame is invalid, does not declare its size, + * or declares more than max_size bytes. + */ +[[nodiscard]] std::optional> TryDecompressDataZSTD(std::span compressed, + std::size_t max_size); + /** * Decompresses a single Zstandard frame into a caller-provided buffer of known size. * diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp index e6d23b7bf6..730a7edcc1 100644 --- a/src/core/hle/service/sockets/bsd.cpp +++ b/src/core/hle/service/sockets/bsd.cpp @@ -965,24 +965,24 @@ std::pair BSD_USA::SendToImpl(s32 fd, u32 flags, std::span } Errno BSD_USA::CloseImpl(s32 fd) { - const auto descriptor = CopyDescriptor(fd); - if (!descriptor) { - return Errno::BADF; - } - - const Errno bsd_errno = Translate(descriptor->socket->Close()); - if (bsd_errno != Errno::SUCCESS) { - return bsd_errno; + std::shared_ptr socket; + { + // Free the slot first, so of two threads closing the same fd only one closes the socket. + // Like POSIX close(), the fd is released even if closing the socket fails. + std::scoped_lock lock{file_descriptors_mutex}; + if (!IsFileDescriptorValid(fd)) { + return Errno::BADF; + } + if (!file_descriptors[fd]->socket) { + LOG_WARNING(Service, "Uninitialized socket"); + return Errno::BADF; + } + socket = std::move(file_descriptors[fd]->socket); + file_descriptors[fd].reset(); } LOG_INFO(Service, "Close socket fd={}", fd); - - // Another thread may have closed this fd and reused the slot meanwhile - std::scoped_lock lock{file_descriptors_mutex}; - if (file_descriptors[fd] && file_descriptors[fd]->socket == descriptor->socket) { - file_descriptors[fd].reset(); - } - return bsd_errno; + return Translate(socket->Close()); } std::variant BSD_USA::DuplicateSocketImpl(s32 fd) { @@ -1070,7 +1070,8 @@ void BSD_USA::BuildErrnoResponse(HLERequestContext& ctx, Errno bsd_errno) const void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) { // Runs on the room thread. The copies keep each socket alive if a bsd thread closes it, so - // the bsd threads do not wait for the decompression below. + // the bsd threads do not wait for the decompression below. Only sockets that accept the + // packet are collected, so an unwanted packet is never decompressed. std::array, MAX_FD> sockets{}; size_t num_sockets = 0; { @@ -1081,7 +1082,8 @@ void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) { } // A duplicated fd shares its socket, which must still get each packet only once const auto sockets_end = sockets.begin() + num_sockets; - if (std::find(sockets.begin(), sockets_end, descriptor->socket) == sockets_end) { + if (std::find(sockets.begin(), sockets_end, descriptor->socket) == sockets_end && + descriptor->socket->AcceptsProxyPacket(packet)) { sockets[num_sockets++] = descriptor->socket; } } @@ -1090,13 +1092,19 @@ void BSD_USA::OnProxyPacketReceived(const Network::ProxyPacket& packet) { return; } - // Decompress once for all sockets that share the port + // Decompress once for all sockets that share the port. The room does not check the frame, so + // drop a malformed or oversized one instead of throwing on the room thread. + auto data = Common::Compression::TryDecompressDataZSTD(packet.data, MAX_ROOM_PACKET_SIZE); + if (!data) { + LOG_WARNING(Service, "Dropped a room packet with an invalid or oversized payload"); + return; + } const Network::ProxyPacket decompressed{ .local_endpoint = packet.local_endpoint, .remote_endpoint = packet.remote_endpoint, .protocol = packet.protocol, .broadcast = packet.broadcast, - .data = Common::Compression::DecompressDataZSTD(packet.data), + .data = std::move(*data), }; for (size_t i = 0; i < num_sockets; ++i) { sockets[i]->HandleProxyPacket(decompressed); diff --git a/src/core/hle/service/sockets/bsd.h b/src/core/hle/service/sockets/bsd.h index 559830d7a1..bff80d7c00 100644 --- a/src/core/hle/service/sockets/bsd.h +++ b/src/core/hle/service/sockets/bsd.h @@ -44,6 +44,8 @@ public: private: /// Maximum number of file descriptors static constexpr size_t MAX_FD = 128; + /// Largest decompressed room packet. A UDP datagram cannot be larger. + static constexpr size_t MAX_ROOM_PACKET_SIZE = 0x10000; struct FileDescriptor { std::shared_ptr socket; diff --git a/src/core/internal_network/network.cpp b/src/core/internal_network/network.cpp index d1c436aa08..806562076d 100644 --- a/src/core/internal_network/network.cpp +++ b/src/core/internal_network/network.cpp @@ -1095,6 +1095,10 @@ bool Socket::IsOpened() const { return fd != INVALID_SOCKET; } +bool Socket::AcceptsProxyPacket(const ProxyPacket& packet) { + return false; +} + void Socket::HandleProxyPacket(const ProxyPacket& packet) { LOG_WARNING(Network, "ProxyPacket received, but not in Proxy mode!"); } diff --git a/src/core/internal_network/socket_proxy.cpp b/src/core/internal_network/socket_proxy.cpp index 95f64b9bd0..f12891e8f5 100644 --- a/src/core/internal_network/socket_proxy.cpp +++ b/src/core/internal_network/socket_proxy.cpp @@ -30,17 +30,29 @@ ProxySocket::~ProxySocket() { fd = INVALID_SOCKET; } +bool ProxySocket::MatchesPacket(const ProxyPacket& packet) const { + if (protocol != packet.protocol || local_endpoint.portno != packet.remote_endpoint.portno || + closed) { + return false; + } + if (!broadcast && packet.broadcast) { + LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode"); + return false; + } + return true; +} + +bool ProxySocket::AcceptsProxyPacket(const ProxyPacket& packet) { + std::lock_guard guard(packets_mutex); + return MatchesPacket(packet); +} + void ProxySocket::HandleProxyPacket(const ProxyPacket& packet) { { - // Runs on the room thread, while the game thread may bind, close or reconfigure the socket + // Runs on the room thread, while the game thread may bind, close or reconfigure the socket. + // Check again: the socket may have changed since AcceptsProxyPacket. std::lock_guard guard(packets_mutex); - if (protocol != packet.protocol || local_endpoint.portno != packet.remote_endpoint.portno || - closed) { - return; - } - - if (!broadcast && packet.broadcast) { - LOG_INFO(Network, "Received broadcast packet, but not configured for broadcast mode"); + if (!MatchesPacket(packet)) { return; } received_packets.push(packet); diff --git a/src/core/internal_network/socket_proxy.h b/src/core/internal_network/socket_proxy.h index 5d6e35693c..078baa7806 100644 --- a/src/core/internal_network/socket_proxy.h +++ b/src/core/internal_network/socket_proxy.h @@ -22,6 +22,8 @@ public: explicit ProxySocket() noexcept; ~ProxySocket() override; + bool AcceptsProxyPacket(const ProxyPacket& packet) override; + void HandleProxyPacket(const ProxyPacket& packet) override; Errno Initialize(Domain domain, Type type, Protocol socket_protocol) override; @@ -82,6 +84,9 @@ public: bool IsOpened() const override; private: + /// Expects packets_mutex to be held + bool MatchesPacket(const ProxyPacket& packet) const; + bool broadcast = false; bool closed = false; u32 send_timeout = 0; diff --git a/src/core/internal_network/sockets.h b/src/core/internal_network/sockets.h index 37cffd4651..29b5b0f18a 100644 --- a/src/core/internal_network/sockets.h +++ b/src/core/internal_network/sockets.h @@ -89,6 +89,9 @@ public: virtual bool IsOpened() const = 0; + /// Whether HandleProxyPacket would queue the packet; checked before decompressing it + virtual bool AcceptsProxyPacket(const ProxyPacket& packet) = 0; + /// Queues a room packet whose data is already decompressed virtual void HandleProxyPacket(const ProxyPacket& packet) = 0; @@ -164,6 +167,8 @@ public: bool IsOpened() const override; + bool AcceptsProxyPacket(const ProxyPacket& packet) override; + void HandleProxyPacket(const ProxyPacket& packet) override; private: -- cgit v1.2.3 From d8d16713b891245fd786b27d375f68d695c76d0e Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 3 Nov 2026 01:39:55 +0100 Subject: tests: cover room packet matching and size-checked ZSTD decompression Assisted-by: Claude Opus 5.5 (investigation and implementation) --- src/tests/CMakeLists.txt | 1 + src/tests/common/zstd_compression.cpp | 59 ++++++++++++++++++++++++ src/tests/core/internal_network/socket_proxy.cpp | 37 +++++++++++++-- 3 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 src/tests/common/zstd_compression.cpp diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index a31c073765..6bf14f4cbd 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(tests common/slot_vector.cpp common/undefined_fix.cpp common/unique_function.cpp + common/zstd_compression.cpp core/core_timing.cpp core/ldn.cpp core/internal_network/socket_proxy.cpp diff --git a/src/tests/common/zstd_compression.cpp b/src/tests/common/zstd_compression.cpp new file mode 100644 index 0000000000..6996f2e1ac --- /dev/null +++ b/src/tests/common/zstd_compression.cpp @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include + +#include "common/common_types.h" +#include "common/zstd_compression.h" + +namespace { + +std::vector Compress(const std::vector& data) { + return Common::Compression::CompressDataZSTDDefault(data.data(), data.size()); +} + +} // namespace + +TEST_CASE("ZSTD frames within the size limit decompress", "[common][zstd]") { + const std::vector data(100, 0x5a); + const auto compressed = Compress(data); + + const auto result = Common::Compression::TryDecompressDataZSTD(compressed, data.size()); + REQUIRE(result.has_value()); + REQUIRE(*result == data); + REQUIRE(Common::Compression::DecompressDataZSTD(compressed) == data); + + const auto empty = Common::Compression::TryDecompressDataZSTD(Compress({}), 0); + REQUIRE(empty.has_value()); + REQUIRE(empty->empty()); +} + +TEST_CASE("ZSTD frames over the size limit are rejected", "[common][zstd]") { + const std::vector data(100, 0x5a); + const auto compressed = Compress(data); + REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(compressed, data.size() - 1)); +} + +TEST_CASE("Invalid ZSTD frames are rejected without throwing", "[common][zstd]") { + const std::array one_byte{0x01}; + const std::array bad_magic{0x01, 0x02, 0x03, 0x04, 0x05}; + REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(one_byte, 0x10000)); + REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(bad_magic, 0x10000)); + REQUIRE(Common::Compression::DecompressDataZSTD(one_byte).empty()); + + // The header declares the size, but the data after it is cut off + std::vector truncated = Compress(std::vector(100, 0x5a)); + truncated.pop_back(); + REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(truncated, 0x10000)); +} + +TEST_CASE("ZSTD frames without a declared size are rejected", "[common][zstd]") { + // Magic number, a frame header without the content size, then one raw 3-byte last block + const std::array frame{0x28, 0xb5, 0x2f, 0xfd, 0x00, 0x00, + 0x19, 0x00, 0x00, 'a', 'b', 'c'}; + REQUIRE_FALSE(Common::Compression::TryDecompressDataZSTD(frame, 0x10000)); + REQUIRE(Common::Compression::DecompressDataZSTD(frame).empty()); +} diff --git a/src/tests/core/internal_network/socket_proxy.cpp b/src/tests/core/internal_network/socket_proxy.cpp index 0ba5f2aa2b..137de5b421 100644 --- a/src/tests/core/internal_network/socket_proxy.cpp +++ b/src/tests/core/internal_network/socket_proxy.cpp @@ -21,9 +21,7 @@ void BindSocket(Network::ProxySocket& socket, Network::Protocol protocol) { .portno = 35000}) == Network::Errno::SUCCESS); } -template -void QueuePacket(Network::ProxySocket& socket, Network::Protocol protocol, - const std::array& payload) { +Network::ProxyPacket MakePacket(Network::Protocol protocol) { Network::ProxyPacket packet{}; packet.local_endpoint = {.family = Network::Domain::INET, .ip = {192, 168, 1, 2}, @@ -32,6 +30,13 @@ void QueuePacket(Network::ProxySocket& socket, Network::Protocol protocol, .ip = {192, 168, 1, 1}, .portno = 35000}; packet.protocol = protocol; + return packet; +} + +template +void QueuePacket(Network::ProxySocket& socket, Network::Protocol protocol, + const std::array& payload) { + Network::ProxyPacket packet = MakePacket(protocol); packet.data.assign(payload.begin(), payload.end()); socket.HandleProxyPacket(packet); } @@ -228,3 +233,29 @@ TEST_CASE("One room packet wakes every blocked peek reader", "[core][sockets]") REQUIRE(finished[i] - queued < std::chrono::milliseconds{1000}); } } + +TEST_CASE("Room sockets accept only packets for their port and protocol", "[core][sockets]") { + Network::ProxySocket socket; + BindSocket(socket, Network::Protocol::UDP); + + REQUIRE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::UDP))); + REQUIRE_FALSE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::TCP))); + + Network::ProxyPacket other_port = MakePacket(Network::Protocol::UDP); + other_port.remote_endpoint.portno = 35001; + REQUIRE_FALSE(socket.AcceptsProxyPacket(other_port)); + + Network::ProxyPacket broadcast = MakePacket(Network::Protocol::UDP); + broadcast.broadcast = true; + REQUIRE_FALSE(socket.AcceptsProxyPacket(broadcast)); + REQUIRE(socket.SetBroadcast(true) == Network::Errno::SUCCESS); + REQUIRE(socket.AcceptsProxyPacket(broadcast)); + + REQUIRE(socket.Close() == Network::Errno::SUCCESS); + REQUIRE_FALSE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::UDP))); +} + +TEST_CASE("Native sockets accept no room packets", "[core][sockets]") { + Network::Socket socket; + REQUIRE_FALSE(socket.AcceptsProxyPacket(MakePacket(Network::Protocol::UDP))); +} -- cgit v1.2.3 From 537c1f4e173a863d87b6a0e5cd71e167db26f187 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 3 Nov 2026 07:10:48 +0600 Subject: video_core: fix the -Wshadow error in the FSR 3.1 upscaler Fsr3Upscaler::Pass::Dispatch took a scheduler parameter that shadows Fsr3Upscaler::scheduler, which fails the Windows MinGW PGO build under -Werror=shadow. --- src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp index 99275800d1..b199005b33 100644 --- a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp +++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp @@ -151,9 +151,9 @@ public: const std::array& samplers) : Pass(device_, scheduler, descriptor_pool, MakeLayout(kinds, samplers), code) {} - void Dispatch(Scheduler& scheduler, ComputePassDescriptorQueue& queue, + void Dispatch(Scheduler& scheduler_, ComputePassDescriptorQueue& queue, std::span resources, u32 groups_x, u32 groups_y) { - queue.Acquire(scheduler, resources.size()); + queue.Acquire(scheduler_, resources.size()); for (const Resource& resource : resources) { if (resource.buffer) { queue.AddBuffer(resource.buffer, resource.offset, resource.size); @@ -162,7 +162,7 @@ public: } } const void* const descriptor_data{queue.UpdateData()}; - scheduler.Record([this, descriptor_data, groups_x, groups_y](vk::CommandBuffer cmdbuf) { + scheduler_.Record([this, descriptor_data, groups_x, groups_y](vk::CommandBuffer cmdbuf) { const VkDescriptorSet set = descriptor_allocator.Commit(); device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline); -- cgit v1.2.3 From 465aa33077f44d3f8664c076f8580e78446c0dee Mon Sep 17 00:00:00 2001 From: néo Luigi <85024393+neoluigi4123@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:15:22 +0100 Subject: Vibration support for ns2 games This branch supports vibration by achieving proper rumble reading from games and uses regular eglantine/eden logic from ns1 to send them over. Tested on windows 10 on Mario Kart World Assisted-by: Claude Opus 5.5 (code cleanup) --- src/core/hle/service/hid/hid_server.cpp | 50 ++++++++++++--------------------- src/core/hle/service/hid/hid_server.h | 11 +++++--- 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/core/hle/service/hid/hid_server.cpp b/src/core/hle/service/hid/hid_server.cpp index eb6a29c158..f2a7b7d04a 100644 --- a/src/core/hle/service/hid/hid_server.cpp +++ b/src/core/hle/service/hid/hid_server.cpp @@ -4,6 +4,7 @@ // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later +#include #include #include "common/common_types.h" @@ -140,7 +141,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr r {211, C<&IHidServer::IsVibrationDeviceMounted>, "IsVibrationDeviceMounted"}, {212, C<&IHidServer::SendVibrationValueInBool>, "SendVibrationValueInBool"}, {213, C<&IHidServer::Unknown213>, "Unknown213"}, - {214, &IHidServer::SendNx2VibrationValues, "SendNx2VibrationValues (guessed)"}, + {214, C<&IHidServer::SendNx2VibrationValues>, "SendNx2VibrationValues (guessed)"}, {220, C<&IHidServer::Unknown220>, "Unknown220"}, {300, C<&IHidServer::ActivateConsoleSixAxisSensor>, "ActivateConsoleSixAxisSensor"}, {301, C<&IHidServer::StartConsoleSixAxisSensor>, "StartConsoleSixAxisSensor"}, @@ -1112,40 +1113,25 @@ Result IHidServer::IsVibrationDeviceMounted( R_SUCCEED(); } -// TEMPORARY local diagnostic for #103: dumps the request layout of 214 when it changes. -void IHidServer::SendNx2VibrationValues(HLERequestContext& ctx) { - static std::string last_dump; - static u64 call_count = 0; - ++call_count; - - std::string dump = fmt::format("words(payload@{}):", ctx.GetDataPayloadOffset()); - const u32* cmd_buf = ctx.CommandBuffer(); - for (u32 i = 0; i < 16; ++i) { - dump += fmt::format(" {:08x}", cmd_buf[i]); - } - dump += fmt::format(" | X={} A={} B={} C={}", ctx.BufferDescriptorX().size(), - ctx.BufferDescriptorA().size(), ctx.BufferDescriptorB().size(), - ctx.BufferDescriptorC().size()); - const auto dump_buffer = [&dump](char kind, std::size_t index, std::span data) { - dump += fmt::format(" | {}{} size={}:", kind, index, data.size()); - for (std::size_t i = 0; i < std::min(data.size(), 96); ++i) { - dump += fmt::format("{}{:02x}", i % 4 == 0 ? " " : "", data[i]); +Result IHidServer::SendNx2VibrationValues( + u64 unknown, AppletResourceUserId aruid, + InArray vibration_handles, + InArray vibration_values) { + LOG_DEBUG(Service_HID, "called, unknown={}, applet_resource_user_id={}", unknown, aruid.pid); + + // Unlike 206 this never fails: rumble is best effort and the command is only known from + // captures, so an unexpected handle or layout shouldn't return an error a game may treat as + // fatal. + const auto count = (std::min)(vibration_handles.size(), vibration_values.size()); + for (std::size_t i = 0; i < count; i++) { + const auto result = GetResourceManager()->SendVibrationValue( + aruid.pid, vibration_handles[i], vibration_values[i]); + if (result.IsError()) { + LOG_DEBUG(Service_HID, "failed to send vibration {}, result={:#x}", i, result.raw); } - }; - for (std::size_t i = 0; i < ctx.BufferDescriptorX().size(); ++i) { - dump_buffer('X', i, ctx.ReadBufferX(i)); - } - for (std::size_t i = 0; i < ctx.BufferDescriptorA().size(); ++i) { - dump_buffer('A', i, ctx.ReadBufferA(i)); - } - - if (dump != last_dump) { - LOG_INFO(Service_HID, "214 #{}: {}", call_count, dump); - last_dump = std::move(dump); } - IPC::ResponseBuilder rb{ctx, 2}; - rb.Push(ResultSuccess); + R_SUCCEED(); } Result IHidServer::Unknown213() { diff --git a/src/core/hle/service/hid/hid_server.h b/src/core/hle/service/hid/hid_server.h index 5738e1798f..9bb62fabc4 100644 --- a/src/core/hle/service/hid/hid_server.h +++ b/src/core/hle/service/hid/hid_server.h @@ -189,11 +189,14 @@ private: Result SendVibrationValueInBool(bool is_vibrating, Core::HID::VibrationDeviceHandle vibration_device_handle, ClientAppletResourceUserId aruid); - // Switch 2 sends vibration every frame with 214 (pointer buffers of device handles and - // values, like 206); Eden doesn't emulate vibration, so accept and drop it. - void SendNx2VibrationValues(HLERequestContext& ctx); + // Switch 2 sends vibration every frame with 214: pointer buffers of device handles and values + // like 206, but the aruid is raw data after an unknown u64 instead of a sent process id. + Result SendNx2VibrationValues( + u64 unknown, AppletResourceUserId aruid, + InArray vibration_handles, + InArray vibration_values); // 213 (32 bytes, aruid): sent per controller every frame by Kirby Air Riders (tens of - // thousands of auto stub log lines a run); no reply data. Accepted and dropped like 214. + // thousands of auto stub log lines a run); no reply data. Accepted and dropped. Result Unknown213(); // 220 (16 bytes, aruid): replies one byte (meaning unknown); answer 0 instead of leaving it to // the message buffer's leftovers. -- cgit v1.2.3 From 0b0c65a293c95e27c1d77b945594aec7b6c04c5a Mon Sep 17 00:00:00 2001 From: nagisa-kamishiro <83013268+nagisa-kamishiro@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:26:40 +0900 Subject: Remove dist/Eglantine.icns, which collides with dist/eglantine.icns The two names differ only in case, so on case-insensitive filesystems (Windows, default macOS) only one of them can be checked out and git status always shows the other as modified. src/yuzu/CMakeLists.txt and src/yuzu/Info.plist only reference the lowercase dist/eglantine.icns; nothing references the uppercase file. AI disclosure: found and prepared with Claude (an AI coding assistant). --- dist/Eglantine.icns | Bin 60987 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 dist/Eglantine.icns diff --git a/dist/Eglantine.icns b/dist/Eglantine.icns deleted file mode 100644 index 7169466694..0000000000 Binary files a/dist/Eglantine.icns and /dev/null differ -- cgit v1.2.3 From 75fdcbdf2b0f30d2605ddf77eaa53550ac4c29f9 Mon Sep 17 00:00:00 2001 From: ShahadatTest Date: Tue, 3 Nov 2026 08:10:30 +0600 Subject: cli: exit successfully for --help and --version ApplyLaunchParams already prints the help or version text, but SDL_AppInit then fell through to the "No ROM specified" check and returned SDL_APP_FAILURE, so `eglantine-cli --help` and `--version` logged a critical error and exited with a failure code. Return SDL_APP_SUCCESS for those two options before the ROM check. Tested: Windows and Linux CI builds; on Windows, --help and --version exit with 0 and running without a ROM still exits with a failure. AI disclosure: implemented with Claude and reviewed with Codex (AI coding assistants). (cherry picked from commit 6debe97d22) --- src/yuzu_cmd/yuzu.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index 6342c5d353..86530ea798 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -349,6 +349,12 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) { return PackNup(*lp.pack_nup_source, lp.filepath) ? SDL_APP_SUCCESS : SDL_APP_FAILURE; } + // Informational options already printed by ApplyLaunchParams; do not treat a + // missing ROM as a failure when the user only asked for help or version. + if (lp.print_help || lp.print_version) { + return SDL_APP_SUCCESS; + } + if (lp.filepath.empty()) { LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified"); return SDL_APP_FAILURE; -- cgit v1.2.3 From 7fbaea95850f1b347f9078943785ae90aad40bd9 Mon Sep 17 00:00:00 2001 From: Jonas <104419015+jojo-chaechae@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:12:50 +0600 Subject: ci: build Android arm64 APKs in release-binaries (#360) Adds an android-arm64 job that builds the mainline release APK twice: as dev.eglantine.eglantine and as com.antutu.ABenchMark. The package name comes from a new appId Gradle property. Both APKs are published with the release. --- .github/workflows/release-binaries.yml | 83 +++++++++++++++++++++++++++++++++- src/android/app/build.gradle.kts | 2 +- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 8233a74c94..d1839a7a70 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -553,12 +553,82 @@ jobs: compression-level: 0 retention-days: 7 + android: + name: android-arm64 apk + runs-on: ubuntu-24.04 + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 1500M + # Gradle's .cxx build folders are named by a config hash; relative paths keep hits stable. + CCACHE_BASEDIR: ${{ github.workspace }} + steps: + - name: Check out source + uses: actions/checkout@v5 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + cache: gradle + + # The runner image has the SDK; add the NDK, CMake and platform the Gradle build asks for, + # and glslangValidator for the host shaders. + - name: Install Android packages + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends glslang-tools ccache + yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null || true + "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install \ + "platforms;android-36" "build-tools;36.0.0" "ndk;28.2.13676358" "cmake;3.31.5" > /dev/null + + - name: Cache CPM downloads + uses: actions/cache@v5 + with: + path: .cache/cpm + key: cpm-android-${{ hashFiles('cpmfile.json', 'CMakeLists.txt', 'src/**/CMakeLists.txt') }} + restore-keys: | + cpm-android- + + - name: Cache compiler objects + uses: actions/cache@v5 + with: + path: .ccache + key: objects-android-arm64-${{ github.sha }} + restore-keys: | + objects-android-arm64- + + # A second APK under AnTuTu's package name, which some GPU drivers run with higher clocks. + # Both share the native build, so the second assemble only repackages. + - name: Build APKs + shell: bash + working-directory: src/android + run: | + set -euo pipefail + for variant in "dev.eglantine.eglantine:" "com.antutu.ABenchMark:-antutu"; do + ./gradlew --no-daemon "-PappId=${variant%%:*}" "-PYUZU_ANDROID_ARGS=-DUSE_CCACHE=ON" \ + assembleMainlineRelease + apk=$(find app/build/outputs/apk/mainline/release -name '*.apk' | head -1) + cp "$apk" "$GITHUB_WORKSPACE/eglantine-${GITHUB_SHA}-android-arm64${variant#*:}.apk" + done + ccache --show-stats + + - name: Upload APKs + uses: actions/upload-artifact@v7 + with: + name: eglantine-android-arm64-${{ github.sha }} + path: eglantine-${{ github.sha }}-android-arm64*.apk + if-no-files-found: error + compression-level: 0 + retention-days: 7 + # Publishes every binary as a GitHub pre-release once every platform built: on pushes to nx2 # and nx2-experimental, and on manual runs unless "Publish the binaries as a GitHub release" is # unticked. release: name: Publish release - needs: build + needs: [build, android] if: github.event_name == 'push' || inputs.publish_release runs-on: ubuntu-24.04 permissions: @@ -585,6 +655,10 @@ jobs: for file in "$artifact"*.AppImage "$artifact"*.tar.gz; do cp "$file" "dist/$(strip_sha "$(basename "$file")")" done + elif compgen -G "$artifact*.apk" > /dev/null; then + for file in "$artifact"*.apk; do + cp "$file" "dist/$(strip_sha "$(basename "$file")")" + done elif compgen -G "$artifact*.zip" > /dev/null; then for file in "$artifact"*.zip "$artifact"*.dmg; do [[ -e "$file" ]] && cp "$file" "dist/$(strip_sha "$(basename "$file")")" @@ -632,6 +706,13 @@ jobs: | arm64 (Apple Silicon) | [zip](${base}/eglantine-macos-arm64.zip) | | macOS 15 or newer. Not notarized: right-click and choose Open the first time, or run \`xattr -dr com.apple.quarantine eglantine.app\`. \`eden-cli\` is inside the app, in \`Contents/MacOS\` | The PGO builds are the same programs compiled with profile-guided optimization (clang plus Eden's profile); prefer them when your CPU supports it. + + ## Android + + | Build Type | APK | Notes | + | --- | --- | --- | + | arm64 | [APK](${base}/eglantine-android-arm64.apk) | Android 13 or newer | + | arm64 (AnTuTu package) | [APK](${base}/eglantine-android-arm64-antutu.apk) | Installed as \`com.antutu.ABenchMark\`, which some GPU drivers run with higher clocks. Cannot be installed alongside AnTuTu itself | EOF gh release create "${tag}" dist/* \ diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index 69dd6a8e4f..bef72206aa 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -64,7 +64,7 @@ android { } defaultConfig { - applicationId = "dev.eden.eden_emulator" + applicationId = providers.gradleProperty("appId").orNull ?: "dev.eden.eden_emulator" minSdk = 24 targetSdk = 36 versionName = getGitVersion() -- cgit v1.2.3 From f201586db8aa463dc7690a26d4cf884ff8c195a8 Mon Sep 17 00:00:00 2001 From: cira0100 <40918547+cira0100@users.noreply.github.com> Date: Tue, 3 Nov 2026 07:24:10 +0100 Subject: core: detect Switch 2 programs in NSP containers (#349) Read the selected program metadata before kernel initialization so NSP launches receive the Switch 2 memory layout and clock. This fixes Mario Kart World 1.8.0 crashing during startup while its loose 1.0 dump already boots. --- src/core/core.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/core/core.cpp b/src/core/core.cpp index 3842ee25a8..00ee551ef8 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -24,6 +24,7 @@ #include "core/cpu_manager.h" #include "core/debugger/debugger.h" #include "core/device_memory.h" +#include "core/file_sys/content_archive.h" #include "core/file_sys/fs_filesystem.h" #include "core/file_sys/nup_container.h" #include "core/file_sys/patch_manager.h" @@ -31,6 +32,7 @@ #include "core/file_sys/registered_cache.h" #include "core/file_sys/romfs_factory.h" #include "core/file_sys/savedata_factory.h" +#include "core/file_sys/submission_package.h" #include "core/file_sys/vfs/vfs_concat.h" #include "core/file_sys/vfs/vfs_real.h" #include "core/gpu_dirty_memory_manager.h" @@ -115,8 +117,8 @@ FileSys::VirtualFile GetGameFileFromPath(const FileSys::VirtualFilesystem& vfs, namespace { -// Switch 2 titles boot as a loose ExeFS, with main.npdm next to main. -bool IsSwitch2Program(const FileSys::VirtualFile& file) { +// Detect the platform before initializing the kernel memory layout and guest clock. +bool IsSwitch2Program(const FileSys::VirtualFile& file, u64 program_id, std::size_t program_index) { FileSys::ProgramMetadata metadata; if (FileSys::NupContainer::IsNupFile(file)) { FileSys::NupContainer nup{file}; @@ -136,6 +138,22 @@ bool IsSwitch2Program(const FileSys::VirtualFile& file) { return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success && metadata.IsSwitch2(); } + const auto type = file != nullptr ? Loader::IdentifyFile(file) : Loader::FileType::Unknown; + if (type == Loader::FileType::NSP || type == Loader::FileType::NSZ) { + const FileSys::NSP nsp{file, program_id, program_index}; + if (nsp.GetStatus() != Loader::ResultStatus::Success) { + return false; + } + auto exefs = nsp.GetExeFS(); + if (!nsp.IsExtractedType()) { + const auto program = + nsp.GetNCA(nsp.GetProgramTitleID(), FileSys::ContentRecordType::Program); + exefs = program != nullptr ? program->GetExeFS() : nullptr; + } + const auto npdm = exefs != nullptr ? exefs->GetFile("main.npdm") : nullptr; + return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success && + metadata.IsSwitch2(); + } const auto dir = file != nullptr ? file->GetContainingDirectory() : nullptr; const auto npdm = dir != nullptr ? dir->GetFile("main.npdm") : nullptr; return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success && @@ -336,7 +354,8 @@ struct System::Impl { const bool switch2_program = virtual_filesystem != nullptr && - IsSwitch2Program(GetGameFileFromPath(virtual_filesystem, filepath)); + IsSwitch2Program(GetGameFileFromPath(virtual_filesystem, filepath), params.program_id, + params.program_index); Settings::SetCurrentProgramIsSwitch2(switch2_program); Settings::SetAstcRecompressionOverride(std::nullopt); -- cgit v1.2.3 From ad7282cd736ae75b126088d7d6986efebd048c15 Mon Sep 17 00:00:00 2001 From: Duck <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:28:21 -0500 Subject: common: measure the TSC only on CPUs without CPUID leaf 0x15 (#373) The fallback that measures the TSC rate when the CPU reports none was meant for AMD, which has no leaf 0x15 at all. It also caught Intel CPUs that have the leaf but report a crystal frequency of 0 (Coffee Lake, e.g. the i5-9400F) and moved them from the OS clock to the measured RDTSC clock. With the same fallback in another Eden-based build, an i5-9400F crashed about 3.5 s after launch; limiting the fallback to CPUs without the leaf fixed it for that tester. Those Intel CPUs now keep the OS clock, as before the fallback. AMD CPUs still get the measured RDTSC clock (a Ryzen 7 9800X3D reports max leaf 0xd, so nothing changes there). Assisted-by: Claude Opus 5.5 (debugging, porting and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/common/cpu_features.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common/cpu_features.cpp b/src/common/cpu_features.cpp index 93eecb1dc5..c7f6e6f036 100644 --- a/src/common/cpu_features.cpp +++ b/src/common/cpu_features.cpp @@ -227,8 +227,10 @@ const CPUCaps g_cpu_caps = [] { // AMD CPUs have no leaf 0x15 (Zen 4 stops at 0x10): measure their invariant TSC too, or every // guest clock read (CNTPCT, which games read thousands of times a frame) goes through the - // host's much slower QueryPerformanceCounter. - if (caps.invariant_tsc && caps.tsc_frequency == 0) { + // host's much slower QueryPerformanceCounter. Intel CPUs that have the leaf but report no + // crystal frequency (e.g. Coffee Lake) keep the OS clock: an i5-9400F crashed at launch on the + // measured RDTSC clock. + if (max_std_fn < 0x15 && caps.invariant_tsc && caps.tsc_frequency == 0) { caps.tsc_frequency = X64::EstimateRDTSCFrequency(); } -- cgit v1.2.3 From f50099038ee4c95901821e137471a8ce87bf52a4 Mon Sep 17 00:00:00 2001 From: Alex <78863999+Sssnowyyy@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:29:23 -0500 Subject: nx2: fewer render pass splits on tiled GPUs, log failed GPU waits (Pokopia) (#361) * buffer_cache: don't end the render pass after mapped buffer uploads Every upload set any_buffer_uploaded, and the next draw or dispatch then recorded a transfer-to-shader barrier outside the render pass. On Vulkan the uploads are mapped: each copy either records its own barriers (CopyBuffer with barrier set) or goes to the upload command buffer, which ends with a barrier before the frame's commands run. The extra barrier only ended the render pass. On tiled GPUs every such split writes the attachments out and reads them back. Pokopia's first gameplay scene had ~50 of these splits a frame. Without them, an M3 MacBook Air (MoltenVK, 1x docked) goes from ~35 to ~29.5 ms of GPU time a frame and from 28.5 to 32 fps. OpenGL uploads immediately and keeps the barrier. Assisted-by: Claude Opus 5.5 (profiling, implementation and commit message) * vulkan: log a failed GPU wait before it ends the process When the device is lost, vkWaitSemaphores in MasterSemaphore::Wait returns VK_ERROR_DEVICE_LOST and the exception goes uncaught on the fence thread or the GPU thread, so the process aborts without a line in the log: ReportLoss is only reached from submit and present. A Pokopia crash on macOS left the log ending a second before the abort, with no hint that the GPU was lost. Log the wait's result before rethrowing. On macOS, MoltenVK prints the Metal reason (a GPU hang or page fault) to stderr only. Assisted-by: Claude Opus 5.5 (diagnosis, implementation and commit message) --- src/video_core/buffer_cache/buffer_cache.h | 7 ++++++- src/video_core/renderer_vulkan/vk_master_semaphore.cpp | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h index 40369b16ef..2e0f0d4623 100644 --- a/src/video_core/buffer_cache/buffer_cache.h +++ b/src/video_core/buffer_cache/buffer_cache.h @@ -2159,7 +2159,12 @@ bool BufferCache

::SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 si } const std::span copies_span(upload_copies.data(), upload_copies.size()); UploadMemory(buffer, total_size_bytes, largest_copy, copies_span); - any_buffer_uploaded = true; + // Mapped uploads carry their own barriers (or run in the upload command buffer, which + // ends with one); another barrier here only ended the render pass (~50 times a frame in + // Pokopia). + if constexpr (!USE_MEMORY_MAPS_FOR_UPLOADS) { + any_buffer_uploaded = true; + } return false; } else { boost::container::small_vector copies; diff --git a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp index be5a1144ce..ba364d99a0 100644 --- a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp +++ b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp @@ -104,7 +104,13 @@ void MasterSemaphore::Wait(u64 tick) { } // If none of the above is hit, fallback to a regular wait - while (!semaphore.Wait(tick)) { + try { + while (!semaphore.Wait(tick)) { + } + } catch (const vk::Exception& exception) { + // The throw ends the process; say why first (a device loss reached nothing else here). + LOG_CRITICAL(Render_Vulkan, "Waiting for GPU tick {} failed: {}", tick, exception.what()); + throw; } Refresh(); -- cgit v1.2.3 From b6c0eddfd52fcff4cec59958d96dc6545113c0d5 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 3 Nov 2026 13:39:24 +0600 Subject: ci: revert Android release builds Revert the Android APK build and release publication introduced by #360, and restore the default Android application ID. Automated Android releases are paused while support issues are addressed. AI disclosure: This commit message was written by AI and does not comply with the repository AI policy requirement that commit messages be human-written. Assisted-by: GPT-6 (prepared the revert and wrote this commit message) --- .github/workflows/release-binaries.yml | 83 +--------------------------------- src/android/app/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 83 deletions(-) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index d1839a7a70..8233a74c94 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -553,82 +553,12 @@ jobs: compression-level: 0 retention-days: 7 - android: - name: android-arm64 apk - runs-on: ubuntu-24.04 - env: - CCACHE_DIR: ${{ github.workspace }}/.ccache - CCACHE_MAXSIZE: 1500M - # Gradle's .cxx build folders are named by a config hash; relative paths keep hits stable. - CCACHE_BASEDIR: ${{ github.workspace }} - steps: - - name: Check out source - uses: actions/checkout@v5 - - - name: Set up Java - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: '17' - cache: gradle - - # The runner image has the SDK; add the NDK, CMake and platform the Gradle build asks for, - # and glslangValidator for the host shaders. - - name: Install Android packages - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends glslang-tools ccache - yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null || true - "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install \ - "platforms;android-36" "build-tools;36.0.0" "ndk;28.2.13676358" "cmake;3.31.5" > /dev/null - - - name: Cache CPM downloads - uses: actions/cache@v5 - with: - path: .cache/cpm - key: cpm-android-${{ hashFiles('cpmfile.json', 'CMakeLists.txt', 'src/**/CMakeLists.txt') }} - restore-keys: | - cpm-android- - - - name: Cache compiler objects - uses: actions/cache@v5 - with: - path: .ccache - key: objects-android-arm64-${{ github.sha }} - restore-keys: | - objects-android-arm64- - - # A second APK under AnTuTu's package name, which some GPU drivers run with higher clocks. - # Both share the native build, so the second assemble only repackages. - - name: Build APKs - shell: bash - working-directory: src/android - run: | - set -euo pipefail - for variant in "dev.eglantine.eglantine:" "com.antutu.ABenchMark:-antutu"; do - ./gradlew --no-daemon "-PappId=${variant%%:*}" "-PYUZU_ANDROID_ARGS=-DUSE_CCACHE=ON" \ - assembleMainlineRelease - apk=$(find app/build/outputs/apk/mainline/release -name '*.apk' | head -1) - cp "$apk" "$GITHUB_WORKSPACE/eglantine-${GITHUB_SHA}-android-arm64${variant#*:}.apk" - done - ccache --show-stats - - - name: Upload APKs - uses: actions/upload-artifact@v7 - with: - name: eglantine-android-arm64-${{ github.sha }} - path: eglantine-${{ github.sha }}-android-arm64*.apk - if-no-files-found: error - compression-level: 0 - retention-days: 7 - # Publishes every binary as a GitHub pre-release once every platform built: on pushes to nx2 # and nx2-experimental, and on manual runs unless "Publish the binaries as a GitHub release" is # unticked. release: name: Publish release - needs: [build, android] + needs: build if: github.event_name == 'push' || inputs.publish_release runs-on: ubuntu-24.04 permissions: @@ -655,10 +585,6 @@ jobs: for file in "$artifact"*.AppImage "$artifact"*.tar.gz; do cp "$file" "dist/$(strip_sha "$(basename "$file")")" done - elif compgen -G "$artifact*.apk" > /dev/null; then - for file in "$artifact"*.apk; do - cp "$file" "dist/$(strip_sha "$(basename "$file")")" - done elif compgen -G "$artifact*.zip" > /dev/null; then for file in "$artifact"*.zip "$artifact"*.dmg; do [[ -e "$file" ]] && cp "$file" "dist/$(strip_sha "$(basename "$file")")" @@ -706,13 +632,6 @@ jobs: | arm64 (Apple Silicon) | [zip](${base}/eglantine-macos-arm64.zip) | | macOS 15 or newer. Not notarized: right-click and choose Open the first time, or run \`xattr -dr com.apple.quarantine eglantine.app\`. \`eden-cli\` is inside the app, in \`Contents/MacOS\` | The PGO builds are the same programs compiled with profile-guided optimization (clang plus Eden's profile); prefer them when your CPU supports it. - - ## Android - - | Build Type | APK | Notes | - | --- | --- | --- | - | arm64 | [APK](${base}/eglantine-android-arm64.apk) | Android 13 or newer | - | arm64 (AnTuTu package) | [APK](${base}/eglantine-android-arm64-antutu.apk) | Installed as \`com.antutu.ABenchMark\`, which some GPU drivers run with higher clocks. Cannot be installed alongside AnTuTu itself | EOF gh release create "${tag}" dist/* \ diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index bef72206aa..69dd6a8e4f 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -64,7 +64,7 @@ android { } defaultConfig { - applicationId = providers.gradleProperty("appId").orNull ?: "dev.eden.eden_emulator" + applicationId = "dev.eden.eden_emulator" minSdk = 24 targetSdk = 36 versionName = getGitVersion() -- cgit v1.2.3 From a667551cdf6e9044e43e1d0c37320c3ec82a1213 Mon Sep 17 00:00:00 2001 From: Duck <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:47:54 -0500 Subject: video_core: cut per-draw work on the GPU back thread in crowded scenes (#374) * video_core: demote the texture cache's per-draw debug logs to trace The invalid sampler and image view index messages fire for unused binding slots on every draw. Filtered LOG_DEBUG messages still cost a call and argument packing: in a Fire Emblem: Fortune's Weave city scene (about 2,100 draws per frame) they were 3.1% of the GPUBack thread's samples. LOG_TRACE compiles them out of release builds. Assisted-by: Claude Opus 5.5 (profiling, porting and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> * texture_cache: read a descriptor with one page lookup DescriptorTable::Read runs for every texture and sampler of every draw, and it read each 32-byte TIC/TSC entry through the generic ReadBlockUnsafe page walk. In a Fire Emblem: Fortune's Weave city scene (about 2,100 draws per frame) that was 5.0% of the GPUBack thread's samples. An entry within one page is contiguous in host memory, so it is now a single page lookup and memcpy. Entries that cross a page and pages without host memory still take the block read, so unmapped and unbacked entries still read as zero. The same change in another Eden-based build cut these reads from 6.4% to 0.4% of its GPU thread. Assisted-by: Claude Opus 5.5 (profiling, porting and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --------- Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/video_core/texture_cache/descriptor_table.h | 12 +++++++++++- src/video_core/texture_cache/texture_cache.h | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/video_core/texture_cache/descriptor_table.h b/src/video_core/texture_cache/descriptor_table.h index a441e572de..2c768b72fc 100644 --- a/src/video_core/texture_cache/descriptor_table.h +++ b/src/video_core/texture_cache/descriptor_table.h @@ -7,12 +7,14 @@ #pragma once #include +#include #include #include "common/alignment.h" #include "common/common_types.h" #include "common/div_ceil.h" #include "common/assert.h" +#include "core/memory.h" #include "video_core/memory_manager.h" #include "video_core/rasterizer_interface.h" @@ -44,7 +46,15 @@ public: } const GPUVAddr gpu_addr = current_gpu_addr + index * sizeof(T); std::pair result; - gpu_memory.ReadBlockUnsafe(gpu_addr, std::addressof(result.first), sizeof(T)); + // This runs for every texture and sampler of every draw. An entry within one page is + // contiguous in host memory, so a single page lookup replaces the generic block read. + const bool one_page = + (gpu_addr & Core::Memory::YUZU_PAGEMASK) + sizeof(T) <= Core::Memory::YUZU_PAGESIZE; + if (const u8* const host = one_page ? gpu_memory.GetPointer(gpu_addr) : nullptr) { + std::memcpy(std::addressof(result.first), host, sizeof(T)); + } else { + gpu_memory.ReadBlockUnsafe(gpu_addr, std::addressof(result.first), sizeof(T)); + } if ((read_descriptors[index / 64] & (1ULL << (index % 64))) != 0) { result.second = result.first != descriptors[index]; } else { diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index abb22b9bfb..ea82ab02e7 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -580,7 +580,7 @@ template SamplerId TextureCache

::GetSamplerId(u32 index, bool compute) { auto& table = compute ? channel_state->compute_sampler_table : channel_state->graphics_sampler_table; if (index > table.current_limit) { - LOG_DEBUG(HW_GPU, "Invalid sampler index={}", index); + LOG_TRACE(HW_GPU, "Invalid sampler index={}", index); return NULL_SAMPLER_ID; } auto const map_index = index | (compute ? Common::SlotId::TAGGED_VALUE : 0); @@ -812,7 +812,7 @@ ImageViewId TextureCache

::VisitImageView(u32 index, bool compute) { index = Tegra::Texture::TextureHeaderIndex(index, table.current_limit, gpu_memory->UsesNativeTiling()); if (index > table.current_limit) { - LOG_DEBUG(HW_GPU, "Invalid image view index={}", index); + LOG_TRACE(HW_GPU, "Invalid image view index={}", index); return NULL_IMAGE_VIEW_ID; } auto const map_index = index | (compute ? Common::SlotId::TAGGED_VALUE : 0); -- cgit v1.2.3 From 4b62edbeb2a97e5d50431019ea4027d2e44141e7 Mon Sep 17 00:00:00 2001 From: mcm Date: Tue, 3 Nov 2026 07:48:07 +0000 Subject: Signed-off-by: Mcm190 (#382) Old contrib guide links to Eden and account registration instructions. Updated to nx2 branch and policy links, easier for new contributors to follow correct steps. --- .github/PULL_REQUEST_TEMPLATE.md | 6 ++-- CONTRIBUTING.md | 64 +++++++++++----------------------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 422efb5fdc..4f795065e8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ -- [ ] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). -- [ ] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md). -- [ ] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. +- [ ] I have read and followed the [Contribution Guidelines](https://github.com/Djihads80/Eglantine/blob/nx2/CONTRIBUTING.md#code-contributions). +- [ ] I have read and followed the [AI Policy](https://github.com/Djihads80/Eglantine/blob/nx2/docs/policies/AI.md). +- [ ] I have read and followed the [Coding Guidelines](https://github.com/Djihads80/Eglantine/blob/nx2/docs/policies/Coding.md) to the best of my ability. ------------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c16de9ddb..661aaedf6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,15 @@ # Contributing -Eden has lots of different ways that you can contribute to its efforts, even without knowing how to write code: - -- Donate! This will help us live our lives, pay for infrastructure/testing hardware, and more - - Liberapay: - - PayPal: `business@eden-emu.dev` - - Bitcoin: `bc1pknzdackezf6s5nxqwn6hx940a7e0k3lk7ggpczp9u4jn4a25lnyqrgvdxx` -- Join our [Discord](https://discord.gg/HstXbPch7X) community -- Submit [bug reports or feature requests](https://github.com/eden-emulator/Issue-Reports/issues), or on [Codeberg](https://codeberg.org/eden-emu/eden/issues) +Eglantine has lots of different ways that you can contribute to its efforts, even without knowing how to write code: + +- Join our [Discord](https://discord.gg/jCCZ58VExk) community +- Submit [bug reports or feature requests](https://github.com/Djihads80/Eglantine/issues) +- Improve documentation or share reproducible testing results - Contribute to our [translation efforts](./dist/languages/README.md) ## Code Contributions -Eden is free, open-source, copyleft software, licensed under the terms of the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html). You would do well to familiarize yourself with the GPL, [its FAQ](https://www.gnu.org/licenses/gpl-faq.html), and [free software as a concept](https://www.gnu.org/philosophy/free-sw.html) before contributing. If these terms are not acceptable to you, then you shouldn't contribute. +Eglantine is free, open-source, copyleft software, licensed under the terms of the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html). You would do well to familiarize yourself with the GPL, [its FAQ](https://www.gnu.org/licenses/gpl-faq.html), and [free software as a concept](https://www.gnu.org/philosophy/free-sw.html) before contributing. If these terms are not acceptable to you, then you shouldn't contribute. ### Policies @@ -29,52 +26,25 @@ Eden is free, open-source, copyleft software, licensed under the terms of the [G ### Where do I start? -Anywhere you like! If you are facing an issue and want to fix it, go ahead. For new features, you are heavily encouraged to open a feature request on GitHub, Codeberg, or Forgejo first, discussing the motivations, potential implementation, and user flow of your desired feature. Our UI/UX designers will work with you to refine your feature before you actually choose to implement it. +Anywhere you like! If you are facing an issue and want to fix it, go ahead. For new features, you are heavily encouraged to open a [feature request](https://github.com/Djihads80/Eglantine/issues) first, discussing the motivations, potential implementation, and user flow of your desired feature. -You may also search for open issues on [GitHub](https://github.com/eden-emulator/Issue-Reports/issues), [Codeberg](https://codeberg.org/eden-emu/eden/issues), or [Forgejo](https://git.eden-emu.dev/eden-emu/eden/issues). For larger features/refactors, you should first express your interest in the issue to ensure another developer isn't already working on it. +Search the [open issues](https://github.com/Djihads80/Eglantine/issues) and [pull requests](https://github.com/Djihads80/Eglantine/pulls) before starting to avoid duplicating work. For larger features/refactors, you should first express your interest in the issue to ensure another developer isn't already working on it. ### Great! Can I contribute already? -There are five primary ways to contribute. - -- Submit pull requests directly to our source tree - - You must first request an account on our Forgejo. See [Signup](#how-do-i-sign-up) -- Submit pull requests to our [GitHub mirror](https://github.com/eden-emulator/Issue-Reports) - - Note that this is subject to being removed by DMCA, so don't rely on this. -- Submit pull requests to our [Codeberg mirror](https://codeberg.org/eden-emu/eden) - - Codeberg is not subject to draconian DMCA laws, but they have been hostile to emulators such as Torzu in the past. Thus, you shouldn't rely on this either. -- Submit patches to [`patch@eden-emu.dev`](mailto:patch@eden-emu.dev) - - These **must** be in `git format-patch` format. You should familiarize yourself with the [art of patching](https://www.gitkraken.com/learn/git/git-patch) beforehand. - - Alongside the contents of your email, please attach the patch/diff file itself for easy access and use. -- Email our developers at [`developers@eden-emu.dev`](mailto:developers@eden-emu.dev) with any of your relevant findings or code changes. - -To test your changes, ensure to read the [build documentation](./docs/Build.md) for your specific platform(s) to ensure everything compiles and works properly. You should also make sure that your branch is up-to-date with the upstream `master` branch before opening a pull request. - -### How do I sign up? - -

-To sign up and begin contributing... (click to open) - -Email [crueter@crueter.xyz](mailto:crueter@crueter.xyz) with the following format: - -```txt -Subject: [Eden Git] Registration Request -Username: -Email: -I wish to sign up because... -``` - -Received mail that does not follow this format will be ignored. - -Once your request is processed, you will receive a confirmation email with your temporary password and some information on Git access and policies. If you do not receive a response within 48 hours, feel free to send another email. +Submit a pull request to [Djihads80/Eglantine](https://github.com/Djihads80/Eglantine). You need a GitHub account; the Eden Forgejo account registration process does not apply here. -> [!WARNING] -> Some email providers may place the response email in your spam/junk folder; notable offenders include Gmail and Outlook. *Always* ensure to check your Spam or Junk folder. +1. Fork the Eglantine repository to your GitHub account. +2. Clone your fork and create a branch for your change based on Eglantine's `nx2` branch. +3. Make a focused change, following the policies above and the [coding guidelines](docs/policies/Coding.md). +4. Review and test your changes. For code changes, follow the [build documentation](docs/Build.md) for your platform and verify the affected behavior. For documentation changes, preview the Markdown and check the instructions and links. +5. Commit your changes and push your branch to your fork. +6. Open a pull request from your branch to `Djihads80/Eglantine`, selecting `nx2` as the base branch. Explain the problem, the change, and how you verified it, and link any related issue. -
+Before opening a pull request, make sure your branch is up-to-date with Eglantine's `nx2` branch. Follow the [AI policy](docs/policies/AI.md), including its requirements for human-written commit messages and pull request descriptions, disclosure, review, testing, and sign-off. ## Non-code Contributions Alongside the other contribution methods listed up top, you can also choose to contribute through documentation, organization, or community guides. These can be done either through the code contribution methods described above, or created externally and shared via our Discord community. -If you have an external tool/page that you believe would be handy to integrate/link into Eden, please additionally email our developers at [`developers@eden-emu.dev`](mailto:developers@eden-emu.dev). **Do not submit vibe-coded or AI generated tools or applications**. +If you have an external tool/page that you believe would be handy to integrate/link into Eglantine, please discuss it with the developers on [Discord](https://discord.gg/jCCZ58VExk). **Do not submit vibe-coded or AI generated tools or applications**. -- cgit v1.2.3 From 46967a1b84e857a783ace6dcbabffddd008463ff Mon Sep 17 00:00:00 2001 From: FranzQDH Date: Tue, 3 Nov 2026 08:49:02 +0100 Subject: Add Arch Linux eglantine-git Package (#323) This PR introduces an Arch Linux development package called eglantine-git, addressing Issue #167. The package allows Arch Linux users to build and install Eglantine directly from its source code and keep it up to date through AUR helpers such as yay or paru. Changes Add a PKGBUILD for building Eglantine from the nx2 branch. Build both the graphical application (eglantine) and the CLI tool (eden-cli). Use system-provided dependencies for Qt, SDL3 and FFmpeg. Integrate the project's installation rules. Add the expected desktop icon name so the application icon is displayed correctly. Disable the application's built-in updater, as updates are handled through the package manager. Add Qt Wayland as an optional dependency. Validation bash -n PKGBUILD passes. git diff --check passes. A full package build and runtime test have not yet been performed in an Arch Linux environment. namcap validation is also pending. Further testing on Arch Linux is required to verify the complete build and installation process. Assisted by: Claude Opus 5.5 --- dist/packaging/arch/eglantine-git/PKGBUILD | 78 +++++++++++++++++++++++++++++ dist/packaging/arch/eglantine-git/README.md | 26 ++++++++++ 2 files changed, 104 insertions(+) create mode 100644 dist/packaging/arch/eglantine-git/PKGBUILD create mode 100644 dist/packaging/arch/eglantine-git/README.md diff --git a/dist/packaging/arch/eglantine-git/PKGBUILD b/dist/packaging/arch/eglantine-git/PKGBUILD new file mode 100644 index 0000000000..2c536e7b98 --- /dev/null +++ b/dist/packaging/arch/eglantine-git/PKGBUILD @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +pkgname=eglantine-git +pkgver=0.r0.g0000000 +pkgrel=1 +pkgdesc='Nintendo Switch emulator (development branch)' +arch=('x86_64') +url='https://github.com/Djihads80/Eglantine' +license=('GPL-3.0-or-later') +depends=( + 'enet' + 'ffmpeg' + 'libusb' + 'opus' + 'openssl' + 'qt6-base' + 'qt6-charts' + 'sdl3' + 'vulkan-icd-loader' +) +makedepends=( + 'boost' + 'cmake' + 'git' + 'glslang' + 'libzip' + 'lz4' + 'ninja' + 'nlohmann-json' + 'opus' + 'qt6-base' + 'qt6-charts' + 'spirv-headers' + 'spirv-tools' + 'vulkan-headers' + 'vulkan-utility-libraries' + 'xbyak' + 'zlib' + 'zstd' +) +optdepends=( + 'qt6-wayland: native Wayland support' +) +provides=('eglantine') +conflicts=('eglantine') +source=("eglantine::git+https://github.com/Djihads80/Eglantine.git#branch=nx2") +sha256sums=('SKIP') + +pkgver() { + cd "$srcdir/eglantine" + printf '0.r%s.g%s' "$(git rev-list --count HEAD)" "$(git rev-parse --short=7 HEAD)" +} + +build() { + cmake -S "$srcdir/eglantine" -B "$srcdir/build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DENABLE_QT=ON \ + -DENABLE_UPDATE_CHECKER=OFF \ + -DYUZU_CMD=ON \ + -DYUZU_ROOM=OFF \ + -DYUZU_USE_BUNDLED_QT=OFF \ + -DYUZU_USE_BUNDLED_SDL3=OFF \ + -DYUZU_USE_BUNDLED_FFMPEG=OFF \ + -DYUZU_TESTS=OFF \ + -DBUILD_TESTING=OFF + cmake --build "$srcdir/build" --parallel +} + +package() { + DESTDIR="$pkgdir" cmake --install "$srcdir/build" + + # The desktop entry uses the Eglantine icon name; the upstream install uses + # the older Eden filename, so install an alias that matches the desktop file. + install -Dm644 "$srcdir/eglantine/dist/dev.eden_emu.eden.svg" \ + "$pkgdir/usr/share/icons/hicolor/scalable/apps/dev.eglantine.eglantine.svg" +} diff --git a/dist/packaging/arch/eglantine-git/README.md b/dist/packaging/arch/eglantine-git/README.md new file mode 100644 index 0000000000..ef201872c4 --- /dev/null +++ b/dist/packaging/arch/eglantine-git/README.md @@ -0,0 +1,26 @@ +# Arch Linux development package + +This directory contains a draft `eglantine-git` PKGBUILD for the open Linux +packaging request in [issue #167](https://github.com/Djihads80/Eglantine/issues/167). +It builds the `nx2` branch from source against Arch's system Qt, SDL3 and FFmpeg. +That avoids shipping Ubuntu-linked binaries to a rolling-release distribution +and gives the Qt frontend access to the host's platform plugins, including +Wayland when `qt6-wayland` is installed. + +## Local build + +On an up-to-date Arch Linux system, install the dependencies listed in +`PKGBUILD`, then run: + +```sh +makepkg --syncdeps --cleanbuild +``` + +For package review, also build it in a clean chroot with `extra-x86_64-build` +and inspect the result with `namcap`. The source branch moves over time, so +review the generated `.SRCINFO` after each `pkgver` update before publishing +this recipe to the AUR. + +This recipe is a starting point for review, not an official AUR package. It +does not modify the project's release binaries or CI workflow. The updater is +disabled because package updates should be handled by pacman/AUR helpers. -- cgit v1.2.3 From cf55e9e9f4471ec2960cd9e28687149ffab90134 Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:46:34 +0100 Subject: tests: give the descriptor table mocks a GetPointer hook DescriptorTable::Read calls MemoryManager::GetPointer for entries that fall inside a single page, which neither mock in this test implemented, so the template failed to instantiate and the tests target stopped building. Return nullptr from both mocks so the tests keep exercising the generic ReadBlockUnsafe path they are written for. Assisted-by: DeepSeek V4.1 Flash (diagnosis and patch) --- src/tests/video_core/descriptor_table.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tests/video_core/descriptor_table.cpp b/src/tests/video_core/descriptor_table.cpp index 8b20564949..1f0f374e23 100644 --- a/src/tests/video_core/descriptor_table.cpp +++ b/src/tests/video_core/descriptor_table.cpp @@ -13,6 +13,12 @@ namespace { struct DescriptorMemory { std::array, 2> banks{}; + // DescriptorTable::Read prefers a single-page host pointer when the memory manager offers one; + // these tests are about the invalidation bookkeeping on the generic read path. + const u8* GetPointer(GPUVAddr) const { + return nullptr; + } + void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const { const auto bank = (address - 0x1000) / 0x1000; const auto offset = (address - 0x1000) % 0x1000; @@ -100,6 +106,10 @@ TEST_CASE("Repeated image deletion cannot reuse stale descriptor view IDs", "[de TEST_CASE("Descriptor limits clear exposed slots after shrink and growth", "[descriptor_table]") { struct AddressMemory { + const u8* GetPointer(GPUVAddr) const { + return nullptr; + } + void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const { std::memcpy(output, &address, size); } -- cgit v1.2.3 From b1e5c7ec68c9fd444a5615a18ce434cdfd782bba Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:46:37 +0100 Subject: tests: compare the socket receive count without a sign mismatch Recv reports a signed int count and payload.size() is size_t, which trips -Werror=sign-compare and stops the tests target from building. Assisted-by: DeepSeek V4.1 Flash (diagnosis and patch) --- src/tests/core/internal_network/socket_proxy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/core/internal_network/socket_proxy.cpp b/src/tests/core/internal_network/socket_proxy.cpp index 137de5b421..26fc65a260 100644 --- a/src/tests/core/internal_network/socket_proxy.cpp +++ b/src/tests/core/internal_network/socket_proxy.cpp @@ -54,7 +54,7 @@ TEST_CASE("Room UDP sockets receive queued datagrams with recv", "[core][sockets std::array received{}; const auto [count, error] = socket.Recv(0, received); REQUIRE(error == Network::Errno::SUCCESS); - REQUIRE(count == payload.size()); + REQUIRE(count == static_cast(payload.size())); REQUIRE(received == payload); const auto [empty_count, empty_error] = socket.Recv(0, received); -- cgit v1.2.3 From 0c6f441b1abd756e326f2a6316b5272c75c9a166 Mon Sep 17 00:00:00 2001 From: kraibse <33869366+kraibse@users.noreply.github.com> Date: Tue, 3 Nov 2026 14:51:22 +0100 Subject: ci: skip the release binaries for test-only pushes The release build configures with BUILD_TESTING=OFF and YUZU_TESTS=OFF, so a push that only touches src/tests produces the same binaries on all seven platforms. Exclude that directory from the path filter so it does not queue a rebuild; a push that also touches a build input still runs. Assisted-by: DeepSeek V4.1 Flash (diagnosis and patch) --- .github/workflows/release-binaries.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 8233a74c94..36b69d029e 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -14,6 +14,9 @@ on: # A binary only changes when build inputs change; docs-only pushes skip the build. paths: - "src/**" + # Test-only changes cannot change a binary: the build configures with BUILD_TESTING=OFF and + # YUZU_TESTS=OFF below, so a push that only touches src/tests has nothing to rebuild. + - "!src/tests/**" - "CMakeLists.txt" - "CMakeModules/**" - "externals/**" -- cgit v1.2.3 From 6539c55248950bbde2d47b8405547276232e22d1 Mon Sep 17 00:00:00 2001 From: PastaLaPate Date: Tue, 3 Nov 2026 18:19:35 +0100 Subject: feat: added new crash issue_template --- .github/ISSUE_TEMPLATE/blank_issue_template.yml | 10 --- .github/ISSUE_TEMPLATE/bug_report.yml | 64 ---------------- .github/ISSUE_TEMPLATE/crash.yml | 99 +++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 28 ------- 4 files changed, 99 insertions(+), 102 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/blank_issue_template.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/crash.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/blank_issue_template.yml b/.github/ISSUE_TEMPLATE/blank_issue_template.yml deleted file mode 100644 index 49b7f38228..0000000000 --- a/.github/ISSUE_TEMPLATE/blank_issue_template.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: New Issue (Developers Only) -description: A blank issue template for developers only. If you are not a developer, do not use this issue template. Your issue WILL BE CLOSED if you do not use the appropriate issue template. -body: - - type: markdown - attributes: - value: | - **If you are not a developer, do not use this issue template. Your issue WILL BE CLOSED if you do not use the appropriate issue template.** - - type: textarea - attributes: - label: "Issue" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 66495a335c..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Bug Report -description: File a bug report -body: - - type: markdown - attributes: - value: Tech support does not belong here. You should only file an issue here if you think you have experienced an actual bug with Eden. - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the bug you encountered. - options: - - label: I have searched the existing issues - required: true - - type: input - attributes: - label: Affected Build(s) - description: List the affected build(s) that this issue applies to. - placeholder: Mainline 1234 / Early Access 1234 - validations: - required: true - - type: textarea - id: issue-desc - attributes: - label: Description of Issue - description: A brief description of the issue encountered along with any images and/or videos. - validations: - required: true - - type: textarea - id: expected-behavior - attributes: - label: Expected Behavior - description: A brief description of how it is expected to work along with any images and/or videos. - validations: - required: true - - type: textarea - id: reproduction-steps - attributes: - label: Reproduction Steps - description: A brief explanation of how to reproduce this issue. If possible, provide a save file to aid in reproducing the issue. - validations: - required: true - - type: textarea - id: log - attributes: - label: Log File - description: A log file will help our developers to better diagnose and fix the issue. Instructions can be found [here](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/user/HowToAccessLogs.md). - validations: - required: true - - type: textarea - id: system-config - attributes: - label: System Configuration - placeholder: | - CPU: Intel i5-10400 / AMD Ryzen 5 3600 - GPU/Driver: NVIDIA GeForce GTX 1060 (Driver 512.95) - RAM: 16GB DDR4-3200 - OS: Windows 11 22H2 (Build 22621.819) - value: | - CPU: - GPU/Driver: - RAM: - OS: - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml new file mode 100644 index 0000000000..a7dda83cf7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -0,0 +1,99 @@ +name: Crash +description: The emulator or a game crashed +title: "[Crash] " +labels: ["crash"] +body: + - type: checkboxes + id: checks + attributes: + label: Before submitting + options: + - label: I searched existing issues and found no duplicate + required: true + - label: I'm using the latest build + required: true + - label: My game is legally dumped from my own console + required: true + + - type: input + id: build + attributes: + label: Emulator build + description: Version or commit hash (shown in Help > About) + placeholder: "e.g. build 107 or a1b2c3d" + validations: + required: true + + - type: checkboxes + id: regression + attributes: + label: Regression + options: + - label: This is a regression (it worked in an older build) + + - type: input + id: last_good + attributes: + label: Last known working build + description: Only fill this if it's a regression + + - type: dropdown + id: game + attributes: + label: Game + options: + - Mario Kart World + - Donkey Kong Bananza + - Fire Emblem + - Splatoon Raiders + - Pokopia + - Hyrule Warriors + - Welcome Tour + - Other / not listed + validations: + required: true + + - type: dropdown + id: gpu + attributes: + label: Graphics card + options: + - NVIDIA RTX 50 series + - NVIDIA RTX 40 series + - NVIDIA RTX 30 series or older + - AMD RX 9000 series + - AMD RX 7000 series + - AMD RX 6000 series or older + - Intel Arc + - Integrated graphics INTEL + - Integrated graphics AMD + - Other + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Windows 11 + - Windows 10 + - Linux + - macOS + - Other + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: "1. Boot the game\n2. ..." + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Log / crash dump + render: shell diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index cc588ecb98..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Feature Request -description: File a feature request -labels: "request" -body: - - type: markdown - attributes: - value: Tech support does not belong here. You should only file an issue here if you are requesting a feature you believe would make Eden better. - - type: checkboxes - attributes: - label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the feature you are requesting. - options: - - label: I have searched the existing issues - required: true - - type: textarea - id: what-feature - attributes: - label: What feature are you suggesting? - description: A brief description of the requested feature. - validations: - required: true - - type: textarea - id: why-feature - attributes: - label: Why would this feature be useful? - description: A brief description of why this feature would make Eden better. - validations: - required: true -- cgit v1.2.3 From bc516b118b3ab67bb9d5966c90064f32498c41db Mon Sep 17 00:00:00 2001 From: PastaLaPate Date: Tue, 3 Nov 2026 18:26:38 +0100 Subject: feat: added other report types --- .github/ISSUE_TEMPLATE/blank.yml | 10 +++ .github/ISSUE_TEMPLATE/crash.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 19 ++++++ .github/ISSUE_TEMPLATE/game_bug.yml | 99 ++++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/ui_bug.yml | 83 +++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/visual_bug.yml | 99 ++++++++++++++++++++++++++++++ 6 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/blank.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/game_bug.yml create mode 100644 .github/ISSUE_TEMPLATE/ui_bug.yml create mode 100644 .github/ISSUE_TEMPLATE/visual_bug.yml diff --git a/.github/ISSUE_TEMPLATE/blank.yml b/.github/ISSUE_TEMPLATE/blank.yml new file mode 100644 index 0000000000..49b7f38228 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/blank.yml @@ -0,0 +1,10 @@ +name: New Issue (Developers Only) +description: A blank issue template for developers only. If you are not a developer, do not use this issue template. Your issue WILL BE CLOSED if you do not use the appropriate issue template. +body: + - type: markdown + attributes: + value: | + **If you are not a developer, do not use this issue template. Your issue WILL BE CLOSED if you do not use the appropriate issue template.** + - type: textarea + attributes: + label: "Issue" diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index a7dda83cf7..d68e3db284 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -1,7 +1,7 @@ name: Crash description: The emulator or a game crashed title: "[Crash] " -labels: ["crash"] +labels: ["Crash"] body: - type: checkboxes id: checks diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..21da308e1a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,19 @@ +name: Feature Request +description: Feature Request for Eglantine's UI +title: "[Feature Request] " +labels: ["enhancement"] +body: + - type: textarea + id: wanted + attributes: + label: What do you want ? + placeholder: "I would like the UI to have..." + validations: + required: true + - type: textarea + id: useful + attributes: + label: Why it's useful + placeholder: "It's useful because it allows..." + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/game_bug.yml b/.github/ISSUE_TEMPLATE/game_bug.yml new file mode 100644 index 0000000000..02012ccfc2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/game_bug.yml @@ -0,0 +1,99 @@ +name: Gameplay Bug +description: The game doesnt works properly +title: "[GBug] " +labels: ["bug"] +body: + - type: checkboxes + id: checks + attributes: + label: Before submitting + options: + - label: I searched existing issues and found no duplicate + required: true + - label: I'm using the latest build + required: true + - label: My game is legally dumped from my own console + required: true + + - type: input + id: build + attributes: + label: Emulator build + description: Version or commit hash (shown in Help > About) + placeholder: "e.g. build 107 or a1b2c3d" + validations: + required: true + + - type: checkboxes + id: regression + attributes: + label: Regression + options: + - label: This is a regression (it worked in an older build) + + - type: input + id: last_good + attributes: + label: Last known working build + description: Only fill this if it's a regression + + - type: dropdown + id: game + attributes: + label: Game + options: + - Mario Kart World + - Donkey Kong Bananza + - Fire Emblem + - Splatoon Raiders + - Pokopia + - Hyrule Warriors + - Welcome Tour + - Other / not listed + validations: + required: true + + - type: dropdown + id: gpu + attributes: + label: Graphics card + options: + - NVIDIA RTX 50 series + - NVIDIA RTX 40 series + - NVIDIA RTX 30 series or older + - AMD RX 9000 series + - AMD RX 7000 series + - AMD RX 6000 series or older + - Intel Arc + - Integrated graphics INTEL + - Integrated graphics AMD + - Other + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Windows 11 + - Windows 10 + - Linux + - macOS + - Other + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: "1. Boot the game\n2. ..." + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Log / crash dump + render: shell diff --git a/.github/ISSUE_TEMPLATE/ui_bug.yml b/.github/ISSUE_TEMPLATE/ui_bug.yml new file mode 100644 index 0000000000..7caa773094 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ui_bug.yml @@ -0,0 +1,83 @@ +name: UI Bug +description: UI Doesnt works as expected +title: "[UIBug] " +labels: ["bug", "Eglantine-UI"] +body: + - type: checkboxes + id: checks + attributes: + label: Before submitting + options: + - label: I searched existing issues and found no duplicate + required: true + - label: I'm using the latest build + required: true + - label: My game is legally dumped from my own console + required: true + + - type: input + id: build + attributes: + label: Emulator build + description: Version or commit hash (shown in Help > About) + placeholder: "e.g. build 107 or a1b2c3d" + validations: + required: true + + - type: checkboxes + id: regression + attributes: + label: Regression + options: + - label: This is a regression (it worked in an older build) + + - type: input + id: last_good + attributes: + label: Last known working build + description: Only fill this if it's a regression + + - type: dropdown + id: gpu + attributes: + label: Graphics card + options: + - NVIDIA RTX 50 series + - NVIDIA RTX 40 series + - NVIDIA RTX 30 series or older + - AMD RX 9000 series + - AMD RX 7000 series + - AMD RX 6000 series or older + - Intel Arc + - Integrated graphics INTEL + - Integrated graphics AMD + - Other + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Windows 11 + - Windows 10 + - Linux + - macOS + - Other + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: "1. Try building NUP for game\n2. ..." + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Log / crash dump + render: shell diff --git a/.github/ISSUE_TEMPLATE/visual_bug.yml b/.github/ISSUE_TEMPLATE/visual_bug.yml new file mode 100644 index 0000000000..efe5bfaebf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/visual_bug.yml @@ -0,0 +1,99 @@ +name: Visual Bug +description: The game doesnt renders properly +title: "[VBug] " +labels: ["visual-bug"] +body: + - type: checkboxes + id: checks + attributes: + label: Before submitting + options: + - label: I searched existing issues and found no duplicate + required: true + - label: I'm using the latest build + required: true + - label: My game is legally dumped from my own console + required: true + + - type: input + id: build + attributes: + label: Emulator build + description: Version or commit hash (shown in Help > About) + placeholder: "e.g. build 107 or a1b2c3d" + validations: + required: true + + - type: checkboxes + id: regression + attributes: + label: Regression + options: + - label: This is a regression (it worked in an older build) + + - type: input + id: last_good + attributes: + label: Last known working build + description: Only fill this if it's a regression + + - type: dropdown + id: game + attributes: + label: Game + options: + - Mario Kart World + - Donkey Kong Bananza + - Fire Emblem + - Splatoon Raiders + - Pokopia + - Hyrule Warriors + - Welcome Tour + - Other / not listed + validations: + required: true + + - type: dropdown + id: gpu + attributes: + label: Graphics card + options: + - NVIDIA RTX 50 series + - NVIDIA RTX 40 series + - NVIDIA RTX 30 series or older + - AMD RX 9000 series + - AMD RX 7000 series + - AMD RX 6000 series or older + - Intel Arc + - Integrated graphics INTEL + - Integrated graphics AMD + - Other + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Windows 11 + - Windows 10 + - Linux + - macOS + - Other + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: "1. Boot the game\n2. ..." + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Log / crash dump + render: shell -- cgit v1.2.3 From 1d4ccdbb0cf01add647247a4ab64b5060b1bb8eb Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:10:30 -0500 Subject: video_core: read the framebuffer before recording the upscaled-output blit BlitColor(VkImageView, VkImage, ...), which draws FSR 3.1's output into the game's DLSS output, captured the Framebuffer pointer and read its render pass, handle and size on the worker thread. The pointer points into the texture cache's framebuffer slots, which can be reallocated before the worker records the command, so the render pass could begin with a stale or null render pass and framebuffer. FSR 3.1 records quickly enough that this rarely shows; with a slower first evaluation (NVIDIA DLSS creating its feature) it crashed in vkCmdBeginRenderPass at the first evaluation in every run. Read the three values on the calling thread and capture those instead. Nothing else changes. Assisted-by: Claude Opus 5.5 (debugging, implementation) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/video_core/renderer_vulkan/blit_image.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp index 0628da1d0f..4a274b2915 100644 --- a/src/video_core/renderer_vulkan/blit_image.cpp +++ b/src/video_core/renderer_vulkan/blit_image.cpp @@ -644,10 +644,8 @@ void RecordShaderReadBarrier(Scheduler& scheduler, const ImageView& image_view) }); } -void BeginRenderPass(vk::CommandBuffer& cmdbuf, const Framebuffer* framebuffer) { - const VkRenderPass render_pass = framebuffer->RenderPass(); - const VkFramebuffer framebuffer_handle = framebuffer->Handle(); - const VkExtent2D render_area = framebuffer->RenderArea(); +void BeginRenderPass(vk::CommandBuffer& cmdbuf, VkRenderPass render_pass, + VkFramebuffer framebuffer_handle, VkExtent2D render_area) { const VkRenderPassBeginInfo renderpass_bi{ .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .pNext = nullptr, @@ -784,11 +782,17 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView }; const VkPipelineLayout layout = *one_texture_pipeline_layout; const VkPipeline pipeline = FindOrEmplaceColorPipeline(key); + // Read the framebuffer now: the texture cache's framebuffer slots can move before the + // worker records this. + const VkRenderPass render_pass = dst_framebuffer->RenderPass(); + const VkFramebuffer framebuffer = dst_framebuffer->Handle(); + const VkExtent2D render_area = dst_framebuffer->RenderArea(); scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([this, dst_framebuffer, src_image_view, src_image, src_sampler, dst_region, - src_region, src_size, pipeline, layout](vk::CommandBuffer cmdbuf) { + scheduler.Record([this, render_pass, framebuffer, render_area, src_image_view, src_image, + src_sampler, dst_region, src_region, src_size, pipeline, + layout](vk::CommandBuffer cmdbuf) { TransitionImageLayout(cmdbuf, src_image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - BeginRenderPass(cmdbuf, dst_framebuffer); + BeginRenderPass(cmdbuf, render_pass, framebuffer, render_area); const VkDescriptorSet descriptor_set = one_texture_descriptor_allocator.Commit(); // The source was just transitioned: describe it in that layout. UpdateOneTextureDescriptorSet(device, descriptor_set, src_sampler, src_image_view, -- cgit v1.2.3 From 63e87a22c60394bc4f427ee5f8d6ddbce7b0ac7f Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:13:35 -0500 Subject: video_core: NVIDIA DLSS through NVIDIA Streamline in place of a game's DLSS Adds a Graphics setting, "Use NVIDIA DLSS in place of a game's DLSS" (nvidia_dlss, off by default). When it is on, in Windows builds configured with EGLANTINE_STREAMLINE_SDK, the evaluation Eglantine already replaces with FSR 3.1 goes to NVIDIA DLSS on GeForce RTX cards, with the same inputs (colour, depth, motion vectors, jitter). When DLSS can't run, FSR 3.1 runs as before. Only Streamline's MIT-licensed headers are compiled in; nothing of NVIDIA's is linked or shipped. At run time Eglantine looks for sl.interposer.dll in the user folder's "streamline" folder (or EDEN_STREAMLINE_DIR, or "streamline" next to the executable), checks its Streamline signature and starts Streamline with manual hooking. Streamline's plugins come from that folder or the NVIDIA app's downloads. DLSS itself is nvngx_dlss.dll in the same folder, else the newest DLSS 310 or later that the NVIDIA app has downloaded, checked for NVIDIA's Authenticode signature and copied as streamline/nvidia-app/nvngx_dlss.dll. Like other tools that call DLSS outside a game, it uses its own fixed project id with the custom engine type. - Preset K for all modes (EDEN_NX2_DLSS_PRESET overrides it): the default Performance model (M) and L draw thin dark horizontal lines in some frames at 2x resolution scale; K doesn't. - HDR input with auto exposure; the mode follows the render/output ratio. - Buffer device address stays enabled when DLSS needs it, for DLSS only. - The upscaler waits for the device instead of submitting when it is destroyed: by then the game's channels are released, and a submit read the state tracker's stale flags (a crash when stopping a game). With the setting off, or in builds without the SDK path (the setting is hidden there), nothing changes. Testing: Windows 11, RTX 5080, Fire Emblem: Fortune's Weave docked at 1x (720p -> 1440p) and 2x (1440p -> 2880p), CLI runs and GUI play, stopping and restarting games with DLSS on. Not tested on other GPUs or games. Assisted-by: Claude Opus 5.5 (implementation, debugging) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/common/fs/fs_paths.h | 1 + src/common/fs/path_util.cpp | 1 + src/common/fs/path_util.h | 1 + src/common/settings.h | 2 + src/qt_common/config/shared_translation.cpp | 5 + src/video_core/CMakeLists.txt | 18 + src/video_core/renderer_vulkan/renderer_vulkan.cpp | 6 + src/video_core/renderer_vulkan/renderer_vulkan.h | 3 + .../renderer_vulkan/vk_streamline_dlss.cpp | 386 +++++++++++++++++ .../renderer_vulkan/vk_streamline_dlss.h | 80 ++++ .../renderer_vulkan/vk_texture_cache.cpp | 122 +++++- src/video_core/renderer_vulkan/vk_texture_cache.h | 17 + src/video_core/texture_cache/texture_cache.h | 4 +- src/video_core/vulkan_common/streamline.cpp | 463 +++++++++++++++++++++ src/video_core/vulkan_common/streamline.h | 63 +++ src/video_core/vulkan_common/streamline_api.h | 36 ++ src/video_core/vulkan_common/vulkan_device.cpp | 47 +++ src/video_core/vulkan_common/vulkan_device.h | 7 + src/video_core/vulkan_common/vulkan_instance.cpp | 20 + src/yuzu/configuration/configure_graphics.cpp | 6 + 20 files changed, 1271 insertions(+), 17 deletions(-) create mode 100644 src/video_core/renderer_vulkan/vk_streamline_dlss.cpp create mode 100644 src/video_core/renderer_vulkan/vk_streamline_dlss.h create mode 100644 src/video_core/vulkan_common/streamline.cpp create mode 100644 src/video_core/vulkan_common/streamline.h create mode 100644 src/video_core/vulkan_common/streamline_api.h diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index ae25846fc2..763a8759dd 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -28,6 +28,7 @@ #define SCREENSHOTS_DIR "screenshots" #define SDMC_DIR "sdmc" #define SHADER_DIR "shader" +#define STREAMLINE_DIR "streamline" #define TAS_DIR "tas" #define ICONS_DIR "icons" diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 82ea8f6cb2..f77da2e301 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -168,6 +168,7 @@ public: GenerateEdenPath(EdenPath::SDMCDir, eden_path / SDMC_DIR); GenerateEdenPath(EdenPath::TASDir, eden_path / TAS_DIR); GenerateEdenPath(EdenPath::IconsDir, eden_path / ICONS_DIR); + GenerateEdenPath(EdenPath::StreamlineDir, eden_path / STREAMLINE_DIR); // config GenerateEdenPath(EdenPath::ConfigDir, eden_path_config); // cache diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index e8fa5614ae..940dbb9186 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -34,6 +34,7 @@ enum class EdenPath { ShaderDir, // Where shaders are stored. TASDir, // Where TAS scripts are stored. IconsDir, // Where Icons for Windows shortcuts are stored. + StreamlineDir, // Where the user-supplied NVIDIA Streamline loader is stored. }; // migration/compat dirs diff --git a/src/common/settings.h b/src/common/settings.h index 6669f98f4c..dc442a7180 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -378,6 +378,8 @@ struct Values { Specialization::Percentage, true, true}; + // NVIDIA DLSS (through NVIDIA Streamline) in place of a game's DLSS. + SwitchableSetting nvidia_dlss{linkage, false, "nvidia_dlss", Category::Renderer}; SwitchableSetting aspect_ratio{linkage, AspectRatio::R16_9, "aspect_ratio", diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index c5907e1b65..b395b2959c 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -149,6 +149,11 @@ std::unique_ptr InitializeTranslations(QObject* parent) { tr("Strength of RCAS (AMD's contrast-adaptive sharpening) on the upscaler used in place " "of a game's DLSS (AMD FSR 3.1); 0% turns it off.\nAlso sets the sharpening " "of the AMD FSR 1 and SGSR window adapting filters.")); + INSERT(Settings, nvidia_dlss, tr("Use NVIDIA DLSS in place of a game's DLSS"), + tr("Uses NVIDIA DLSS on GeForce RTX cards in place of a Switch 2 game's own DLSS, " + "instead of AMD FSR 3.1.\nNeeds NVIDIA's Streamline loader, and NVIDIA DLSS unless " + "the NVIDIA app has downloaded it; Eglantine does not ship NVIDIA's files.\nTakes " + "effect when a game starts.")); INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"), tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA " "can produce a more stable picture in lower resolutions.")); diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index d0e84c324c..e474d08fba 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -499,4 +499,22 @@ if (ANDROID AND ARCHITECTURE_arm64) target_link_libraries(video_core PRIVATE adrenotools) endif() +# NVIDIA DLSS through NVIDIA Streamline, in place of a game's DLSS (before FSR 3.1) when the +# setting is on. Only Streamline's MIT-licensed headers are compiled in and nothing of NVIDIA's is +# linked or copied: sl.interposer.dll and the DLSS plugins are loaded at run time from the user's PC. +set(EGLANTINE_STREAMLINE_SDK "" CACHE PATH "NVIDIA Streamline SDK directory (headers only)") +if (EGLANTINE_STREAMLINE_SDK AND WIN32) + target_sources(video_core PRIVATE + renderer_vulkan/vk_streamline_dlss.cpp + renderer_vulkan/vk_streamline_dlss.h + vulkan_common/streamline.cpp + vulkan_common/streamline.h + vulkan_common/streamline_api.h + ) + target_include_directories(video_core PRIVATE ${EGLANTINE_STREAMLINE_SDK}/include) + target_link_libraries(video_core PRIVATE version wintrust crypt32) + target_compile_definitions(video_core PUBLIC EGLANTINE_STREAMLINE) + message(STATUS "NVIDIA DLSS through Streamline: ${EGLANTINE_STREAMLINE_SDK}") +endif() + create_target_directory_groups(video_core) diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 27ca531186..fc0eea84ab 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -41,6 +41,9 @@ #include "video_core/vulkan_common/vulkan_memory_allocator.h" #include "video_core/vulkan_common/vulkan_surface.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +#ifdef EGLANTINE_STREAMLINE +#include "video_core/vulkan_common/streamline.h" +#endif #ifdef __ANDROID__ #include #endif @@ -130,6 +133,9 @@ try , gpu(gpu_) , library(OpenLibrary(context.get())) , dld() +#ifdef EGLANTINE_STREAMLINE + , streamline_started(Streamline::Start()) +#endif // Create raw Vulkan instance first , instance(CreateInstance(*library, dld, diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 87422dd935..67587cde37 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -83,6 +83,9 @@ private: std::shared_ptr library; vk::InstanceDispatch dld; +#ifdef EGLANTINE_STREAMLINE + bool streamline_started; ///< NVIDIA Streamline starts before the instance. +#endif // Keep original handles for compatibility with existing code vk::Instance instance; diff --git a/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp new file mode 100644 index 0000000000..aef32735f9 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp @@ -0,0 +1,386 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// NVIDIA DLSS (through NVIDIA Streamline) in place of a game's DLSS evaluation. + +#include +#include +#include + +#include "common/logging.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_streamline_dlss.h" +#include "video_core/vulkan_common/streamline.h" +#include "video_core/vulkan_common/streamline_api.h" +#include "video_core/vulkan_common/vulkan_device.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" + +namespace Vulkan { +namespace { + +constexpr VkFormat OUTPUT_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT; +// Streamline clears the output before DLSS writes it. +constexpr VkImageUsageFlags OUTPUT_USAGE = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT; + +int EnvInt(const char* name, int fallback) { + const char* const value = std::getenv(name); + return value ? std::atoi(value) : fallback; +} + +// DLSS validates the render size against the mode's range: pick the mode for this ratio. +sl::DLSSMode ModeFor(VkExtent2D render, VkExtent2D upscale) { + const f32 ratio = static_cast(render.width) / static_cast(upscale.width); + if (ratio >= 0.99f) { + return sl::DLSSMode::eDLAA; + } + if (ratio >= 0.65f) { + return sl::DLSSMode::eMaxQuality; + } + if (ratio >= 0.57f) { + return sl::DLSSMode::eBalanced; + } + if (ratio >= 0.49f) { + return sl::DLSSMode::eMaxPerformance; + } + return sl::DLSSMode::eUltraPerformance; +} + +// Preset K for every mode: NVIDIA's defaults for Performance/Ultra Performance (M, L) draw thin +// horizontal lines in Fire Emblem at 2x resolution scale. EDEN_NX2_DLSS_PRESET=J|K|L|M picks +// another model, EDEN_NX2_DLSS_PRESET=default leaves the choice to NVIDIA. +sl::DLSSPreset Preset() { + const char* const value = std::getenv("EDEN_NX2_DLSS_PRESET"); + if (value && value[0] >= 'J' && value[0] <= 'M') { + return static_cast(static_cast(sl::DLSSPreset::ePresetJ) + + static_cast(value[0] - 'J')); + } + if (value && std::string_view{value} == "default") { + return sl::DLSSPreset::eDefault; + } + return sl::DLSSPreset::ePresetK; +} + +sl::float4x4 Identity() { + sl::float4x4 matrix; + matrix.setRow(0, {1.0f, 0.0f, 0.0f, 0.0f}); + matrix.setRow(1, {0.0f, 1.0f, 0.0f, 0.0f}); + matrix.setRow(2, {0.0f, 0.0f, 1.0f, 0.0f}); + matrix.setRow(3, {0.0f, 0.0f, 0.0f, 1.0f}); + return matrix; +} + +VkImageMemoryBarrier Barrier(const StreamlineDlssUpscaler::Image& image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout, + VkImageLayout new_layout) { + VkImageSubresourceRange range = image.range; + range.aspectMask = image.barrier_aspect; + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = new_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image.image, + .subresourceRange = range, + }; +} + +sl::Resource Resource(VkImage image, VkImageView view, VkFormat format, VkImageLayout layout, + VkExtent2D size, VkImageUsageFlags usage) { + sl::Resource resource{sl::ResourceType::eTex2d, image, nullptr, view, + static_cast(layout)}; + resource.width = size.width; + resource.height = size.height; + resource.nativeFormat = static_cast(format); + resource.mipLevels = 1; + resource.arrayLayers = 1; + resource.flags = 0; + resource.usage = usage; + return resource; +} + +} // Anonymous namespace + +StreamlineDlssUpscaler::StreamlineDlssUpscaler(const Device& device_, Scheduler& scheduler_, + MemoryAllocator& memory_allocator_) + : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_} { + if (!Streamline::IsDlssAvailable()) { + throw vk::Exception(VK_ERROR_FEATURE_NOT_PRESENT); + } + const sl::Result result = Streamline::GetApi().get_feature_function( + sl::kFeatureDLSS, "slDLSSSetOptions", set_options); + if (result != sl::Result::eOk || !set_options) { + LOG_ERROR(Render_Vulkan, "Streamline's DLSS has no slDLSSSetOptions (result {})", + static_cast(result)); + throw vk::Exception(VK_ERROR_FEATURE_NOT_PRESENT); + } + point_sampler = device.GetLogical().CreateSampler(VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = VK_FILTER_NEAREST, + .minFilter = VK_FILTER_NEAREST, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 1.0f, + .compareEnable = VK_FALSE, + .compareOp = VK_COMPARE_OP_NEVER, + .minLod = 0.0f, + .maxLod = VK_LOD_CLAMP_NONE, + .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, + .unnormalizedCoordinates = VK_FALSE, + }); +} + +StreamlineDlssUpscaler::~StreamlineDlssUpscaler() { + // Not scheduler.Finish(): this runs when the renderer goes away, after the game's channels are + // released, and a submit would touch their state. Nothing more gets recorded by then. + void(device.GetLogical().WaitIdle()); + if (Streamline::IsDlssAvailable()) { + Streamline::GetApi().free_resources(sl::kFeatureDLSS, sl::ViewportHandle{0u}); + } +} + +void StreamlineDlssUpscaler::CreateOutput(VkExtent2D upscale_size) { + if (output_image) { + scheduler.Finish(); // the old output may still be in use + } + output_image = memory_allocator.CreateImage(VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = OUTPUT_FORMAT, + .extent = {upscale_size.width, upscale_size.height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = OUTPUT_USAGE, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }); + output_view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .image = *output_image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = OUTPUT_FORMAT, + .components{}, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([image = *output_image](vk::CommandBuffer cmdbuf) { + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, + VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + }); +} + +bool StreamlineDlssUpscaler::Upscale(const Inputs& in) { + if (failed) { + return false; + } + bool reset = in.reset; + bool options_changed = false; + if (!output_image || in.render_size.width != render_extent.width || + in.render_size.height != render_extent.height || + in.upscale_size.width != upscale_extent.width || + in.upscale_size.height != upscale_extent.height) { + CreateOutput(in.upscale_size); + render_extent = in.render_size; + upscale_extent = in.upscale_size; + options_changed = true; + reset = true; + } + + // The game's own setup (Fortune's Weave: render-size motion vectors, reversed depth). Its + // colour is linear and goes above 1 with no exposure passed, so by default DLSS gets the HDR + // path with its own auto exposure. EDEN_NX2_DLSS_HDR=0 / EDEN_NX2_DLSS_AUTO_EXPOSURE=0 turn + // those off. + sl::DLSSOptions options{}; + options.mode = ModeFor(in.render_size, in.upscale_size); + options.outputWidth = in.upscale_size.width; + options.outputHeight = in.upscale_size.height; + options.colorBuffersHDR = EnvInt("EDEN_NX2_DLSS_HDR", 1) != 0 ? sl::eTrue : sl::eFalse; + options.useAutoExposure = + EnvInt("EDEN_NX2_DLSS_AUTO_EXPOSURE", 1) != 0 ? sl::eTrue : sl::eFalse; + const sl::DLSSPreset preset = Preset(); + options.dlaaPreset = preset; + options.qualityPreset = preset; + options.balancedPreset = preset; + options.performancePreset = preset; + options.ultraPerformancePreset = preset; + options.ultraQualityPreset = preset; + if (options_changed) { + LOG_INFO(Render_Vulkan, "NVIDIA DLSS (Streamline): {}x{} -> {}x{}, mode {}, preset {}", + in.render_size.width, in.render_size.height, in.upscale_size.width, + in.upscale_size.height, static_cast(options.mode), + static_cast(preset)); + } + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, in, reset, options, options_changed, index = frame_index++, + output_image_handle = *output_image, + output_view_handle = *output_view](vk::CommandBuffer cmdbuf) { + const Streamline::Api& api = Streamline::GetApi(); + const sl::ViewportHandle viewport{0u}; + if (options_changed) { + const auto set = reinterpret_cast(set_options); + if (const sl::Result result = set(viewport, options); result != sl::Result::eOk) { + if (!failed.exchange(true)) { + LOG_ERROR(Render_Vulkan, + "Streamline rejected the DLSS options (result {}); using FSR 3.1", + static_cast(result)); + } + return; + } + } + sl::FrameToken* frame{}; + if (const sl::Result result = api.get_new_frame_token(frame, &index); + result != sl::Result::eOk || !frame) { + if (!failed.exchange(true)) { + LOG_ERROR(Render_Vulkan, "Streamline gave no frame token (result {}); using FSR 3.1", + static_cast(result)); + } + return; + } + + constexpr VkAccessFlags WRITES = + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | + VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; + // The inputs come from the game's draws and dispatches and stay in GENERAL otherwise; + // DLSS reads them as sampled images. + const std::array to_read{ + Barrier(in.color, WRITES, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), + Barrier(in.depth, WRITES, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), + Barrier(in.motion_vectors, WRITES, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, to_read); + + // DLSS super resolution only reads the jitter, motion and depth conventions; the camera + // fields are for frame generation, which this doesn't use. + const f32 rw = static_cast(in.render_size.width); + const f32 rh = static_cast(in.render_size.height); + sl::Constants constants{}; + constants.cameraViewToClip = Identity(); + constants.clipToCameraView = Identity(); + constants.clipToLensClip = Identity(); + constants.clipToPrevClip = Identity(); + constants.prevClipToClip = Identity(); + constants.jitterOffset = {in.jitter[0], in.jitter[1]}; + // Streamline wants motion in [-1,1] screen units; ours turns into render pixels. + constants.mvecScale = {in.motion_vector_scale[0] / rw, in.motion_vector_scale[1] / rh}; + constants.cameraPinholeOffset = {0.0f, 0.0f}; + constants.cameraPos = {0.0f, 0.0f, 0.0f}; + constants.cameraUp = {0.0f, 1.0f, 0.0f}; + constants.cameraRight = {1.0f, 0.0f, 0.0f}; + constants.cameraFwd = {0.0f, 0.0f, 1.0f}; + constants.cameraNear = 0.1f; + constants.cameraFar = 1000.0f; + constants.cameraFOV = 1.0f; + constants.cameraAspectRatio = rw / rh; + constants.depthInverted = sl::eTrue; + constants.cameraMotionIncluded = sl::eTrue; + constants.motionVectors3D = sl::eFalse; + constants.reset = reset ? sl::eTrue : sl::eFalse; + constants.motionVectorsJittered = sl::eFalse; + api.set_constants(constants, *frame, viewport); + + sl::Resource color = Resource(in.color.image, in.color.view, in.color.format, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, in.render_size, + VK_IMAGE_USAGE_SAMPLED_BIT); + sl::Resource depth = Resource(in.depth.image, in.depth.view, in.depth.format, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, in.render_size, + VK_IMAGE_USAGE_SAMPLED_BIT); + sl::Resource motion = Resource(in.motion_vectors.image, in.motion_vectors.view, + in.motion_vectors.format, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, in.render_size, + VK_IMAGE_USAGE_SAMPLED_BIT); + sl::Resource output = Resource(output_image_handle, output_view_handle, OUTPUT_FORMAT, + VK_IMAGE_LAYOUT_GENERAL, in.upscale_size, OUTPUT_USAGE); + const sl::Extent render_area{0, 0, in.render_size.width, in.render_size.height}; + const sl::Extent output_area{0, 0, in.upscale_size.width, in.upscale_size.height}; + // Local tags: everything stays put until the evaluation is recorded. + sl::ResourceTag color_tag{&color, sl::kBufferTypeScalingInputColor, + sl::ResourceLifecycle::eValidUntilEvaluate, &render_area}; + sl::ResourceTag depth_tag{&depth, sl::kBufferTypeDepth, + sl::ResourceLifecycle::eValidUntilEvaluate, &render_area}; + sl::ResourceTag motion_tag{&motion, sl::kBufferTypeMotionVectors, + sl::ResourceLifecycle::eValidUntilEvaluate, &render_area}; + sl::ResourceTag output_tag{&output, sl::kBufferTypeScalingOutputColor, + sl::ResourceLifecycle::eValidUntilEvaluate, &output_area}; + const sl::BaseStructure* inputs[] = {&viewport, &color_tag, &depth_tag, &motion_tag, + &output_tag}; + const sl::Result result = + api.evaluate_feature(sl::kFeatureDLSS, *frame, inputs, std::size(inputs), + reinterpret_cast(*cmdbuf)); + if (result != sl::Result::eOk && !failed.exchange(true)) { + LOG_ERROR(Render_Vulkan, "NVIDIA DLSS evaluation failed (Streamline result {}); " + "using FSR 3.1 from now on", + static_cast(result)); + } + + const std::array to_general{ + Barrier(in.color, VK_ACCESS_SHADER_READ_BIT, 0, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + Barrier(in.depth, VK_ACCESS_SHADER_READ_BIT, 0, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + Barrier(in.motion_vectors, VK_ACCESS_SHADER_READ_BIT, 0, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + const VkMemoryBarrier output_written{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_SHADER_READ_BIT, + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, output_written, {}, + to_general); + }); + // DLSS binds its own pipelines and descriptors on the command buffer. + scheduler.InvalidateState(); + return true; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_streamline_dlss.h b/src/video_core/renderer_vulkan/vk_streamline_dlss.h new file mode 100644 index 0000000000..9f8704fe15 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.h @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// NVIDIA DLSS (through NVIDIA Streamline) in place of a game's DLSS +// evaluation. NVIDIA's DLLs come from the user's PC; see vulkan_common/streamline.h. + +#pragma once + +#include +#include + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class MemoryAllocator; +class Scheduler; + +class StreamlineDlssUpscaler { +public: + struct Image { + VkImageView view; + VkImage image; + VkFormat format; + VkImageSubresourceRange range; ///< The view's level and layer, with the view's aspect. + VkImageAspectFlags barrier_aspect; ///< Every aspect of the image's format. + }; + + struct Inputs { + Image color; ///< Jittered linear colour at the render size. + Image depth; ///< Device depth (reversed) at the render size. + Image motion_vectors; ///< Motion to the previous frame at the render size. + VkExtent2D render_size; + VkExtent2D upscale_size; + /// This frame's sample offset in render pixels, as the game hands its own DLSS. + std::array jitter; + /// Turns a motion vector into render pixels (the render size for UV offsets). + std::array motion_vector_scale; + bool reset; + }; + + /// Throws vk::Exception when Streamline has no DLSS for this device. + explicit StreamlineDlssUpscaler(const Device& device, Scheduler& scheduler, + MemoryAllocator& memory_allocator); + ~StreamlineDlssUpscaler(); + + /// Records one evaluation into the output image (RGBA16F at the upscale size, GENERAL + /// layout). Returns false when DLSS can't run, so the caller can fall back to FSR 3.1. + bool Upscale(const Inputs& inputs); + + [[nodiscard]] VkImage OutputImage() const noexcept { + return *output_image; + } + [[nodiscard]] VkImageView OutputView() const noexcept { + return *output_view; + } + /// Nearest-neighbour clamp sampler (normalized coordinates), for reading the output. + [[nodiscard]] VkSampler PointSampler() const noexcept { + return *point_sampler; + } + +private: + void CreateOutput(VkExtent2D upscale_size); + + const Device& device; + Scheduler& scheduler; + MemoryAllocator& memory_allocator; + void* set_options{}; ///< slDLSSSetOptions + VkExtent2D render_extent{}; + VkExtent2D upscale_extent{}; + u32 frame_index{}; + vk::Image output_image; + vk::ImageView output_view; + vk::Sampler point_sampler; + std::atomic failed{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index e3f427dead..d7093997c4 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -39,6 +39,9 @@ #include "video_core/texture_cache/util.h" #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_memory_allocator.h" +#ifdef EGLANTINE_STREAMLINE +#include "video_core/vulkan_common/streamline.h" +#endif #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/textures/decoders.h" @@ -1556,6 +1559,21 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie if (!WantsTemporalUpscaleDepth()) { return false; } + const VkExtent2D extent = dst_framebuffer->RenderArea(); + // A gap in the evaluations (a menu, a movie, a load) starts the history again. + const auto now = std::chrono::steady_clock::now(); + const f32 frame_time_ms = + fsr3_last_evaluation + ? std::chrono::duration(now - *fsr3_last_evaluation).count() + : 1000.0f / 30.0f; + const bool reset = !fsr3_last_evaluation || frame_time_ms > 250.0f; + fsr3_last_evaluation = now; +#ifdef EGLANTINE_STREAMLINE + if (UpscaleWithDlss(dst_framebuffer, input_color, motion_vectors, depth, render_size, jitter, + motion_scale, reset)) { + return true; + } +#endif if (!fsr3_upscaler) { if (!Fsr3Upscaler::IsSupported(device)) { LOG_WARNING(Render_Vulkan, "FSR 3.1 needs formats or features this device lacks; " @@ -1574,15 +1592,6 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie return false; } } - const VkExtent2D extent = dst_framebuffer->RenderArea(); - // A gap in the evaluations (a menu, a movie, a load) starts the history again. - const auto now = std::chrono::steady_clock::now(); - const f32 frame_time_ms = - fsr3_last_evaluation - ? std::chrono::duration(now - *fsr3_last_evaluation).count() - : 1000.0f / 30.0f; - const bool reset = !fsr3_last_evaluation || frame_time_ms > 250.0f; - fsr3_last_evaluation = now; // FSR's own RCAS, set by the sharpness slider: FSR takes 0-1 for 2 to 0 stops, and the slider // stores stops x 100, so this is the percentage the slider shows; 0% (stored 200) is off. // EDEN_NX2_TEMPORAL_SHARPEN=|off overrides the slider. @@ -1608,15 +1617,28 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie .sharpen = sharpen, .sharpness = 1.0f - stops / 2.0f, }); - // Draw the result into the DLSS output, which keeps its own format; the blit leaves FSR's - // output image in SHADER_READ_ONLY_OPTIMAL, so return it to GENERAL for the next frame. + DrawUpscaledOutput(dst_framebuffer, fsr3_upscaler->OutputView(), fsr3_upscaler->OutputImage(), + fsr3_upscaler->PointSampler()); + static bool logged{}; + if (!logged) { + logged = true; + LOG_INFO(Render_Vulkan, "DLSS replaced by FSR 3.1: {}x{} -> {}x{}", render_size[0], + render_size[1], extent.width, extent.height); + } + return true; +} + +void TextureCacheRuntime::DrawUpscaledOutput(Framebuffer* dst_framebuffer, VkImageView view, + VkImage output, VkSampler sampler) { + // Draw the result into the DLSS output, which keeps its own format; the blit leaves the + // upscaler's output image in SHADER_READ_ONLY_OPTIMAL, so return it to GENERAL for the next + // frame. + const VkExtent2D extent = dst_framebuffer->RenderArea(); const Region2D region{ .start = {0, 0}, .end = {static_cast(extent.width), static_cast(extent.height)}, }; - const VkImage output = fsr3_upscaler->OutputImage(); - blit_image_helper.BlitColor(dst_framebuffer, fsr3_upscaler->OutputView(), output, - fsr3_upscaler->PointSampler(), region, region, + blit_image_helper.BlitColor(dst_framebuffer, view, output, sampler, region, region, {extent.width, extent.height, 1}); scheduler.RequestOutsideRenderPassOperationContext(); scheduler.Record([output](vk::CommandBuffer cmdbuf) { @@ -1641,14 +1663,84 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, barrier); }); +} + +#ifdef EGLANTINE_STREAMLINE +bool TextureCacheRuntime::UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageView& input_color, + ImageView& motion_vectors, ImageView& depth, + std::array render_size, + std::array jitter, + std::array motion_scale, bool reset) { + // EDEN_NX2_DLSS_REPLACEMENT=fsr keeps FSR 3.1 for A/B runs. + static const bool force_fsr = [] { + const char* const value = std::getenv("EDEN_NX2_DLSS_REPLACEMENT"); + return value && std::string_view{value} == "fsr"; + }(); + if (force_fsr || dlss_unsupported) { + return false; + } + if (!dlss_upscaler) { + // Off in the settings, or Streamline didn't start: it has said why. + if (!Streamline::IsDlssAvailable()) { + dlss_unsupported = true; + return false; + } + try { + dlss_upscaler = + std::make_unique(device, scheduler, memory_allocator); + } catch (const vk::Exception& exception) { + LOG_WARNING(Render_Vulkan, "NVIDIA DLSS unavailable ({}); using FSR 3.1", + exception.what()); + dlss_unsupported = true; + return false; + } + } + const auto image = [&](ImageView& view, VkImageView handle, VkImageAspectFlags aspect) { + return StreamlineDlssUpscaler::Image{ + .view = handle, + .image = view.ImageHandle(), + .format = MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, view.format) + .format, + .range{ + .aspectMask = aspect, + .baseMipLevel = static_cast(view.range.base.level), + .levelCount = 1, + .baseArrayLayer = static_cast(view.range.base.layer), + .layerCount = 1, + }, + .barrier_aspect = ImageAspectMask(view.format), + }; + }; + const VkExtent2D extent = dst_framebuffer->RenderArea(); + const bool upscaled = dlss_upscaler->Upscale({ + .color = image(input_color, input_color.Handle(Shader::TextureType::Color2D), + VK_IMAGE_ASPECT_COLOR_BIT), + .depth = image(depth, depth.DepthView(), VK_IMAGE_ASPECT_DEPTH_BIT), + .motion_vectors = image(motion_vectors, + motion_vectors.Handle(Shader::TextureType::Color2D), + VK_IMAGE_ASPECT_COLOR_BIT), + .render_size = {render_size[0], render_size[1]}, + .upscale_size = extent, + .jitter = jitter, + .motion_vector_scale = {motion_scale[0] * static_cast(render_size[0]), + motion_scale[1] * static_cast(render_size[1])}, + .reset = reset, + }); + if (!upscaled) { + dlss_unsupported = true; + return false; + } + DrawUpscaledOutput(dst_framebuffer, dlss_upscaler->OutputView(), dlss_upscaler->OutputImage(), + dlss_upscaler->PointSampler()); static bool logged{}; if (!logged) { logged = true; - LOG_INFO(Render_Vulkan, "DLSS replaced by FSR 3.1: {}x{} -> {}x{}", render_size[0], + LOG_INFO(Render_Vulkan, "DLSS replaced by NVIDIA DLSS: {}x{} -> {}x{}", render_size[0], render_size[1], extent.width, extent.height); } return true; } +#endif bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& input_color, ImageView& motion_vectors, diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index e7dbb97628..44bd21615b 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -17,6 +17,9 @@ #include "shader_recompiler/shader_info.h" #include "video_core/renderer_vulkan/vk_compute_pass.h" #include "video_core/renderer_vulkan/vk_fsr3_upscaler.h" +#ifdef EGLANTINE_STREAMLINE +#include "video_core/renderer_vulkan/vk_streamline_dlss.h" +#endif #include "video_core/renderer_vulkan/vk_render_pass_cache.h" #include "video_core/renderer_vulkan/vk_staging_buffer_pool.h" #include "video_core/texture_cache/image_view_base.h" @@ -208,6 +211,20 @@ public: bool fsr3_unsupported{}; std::optional fsr3_last_evaluation; + /// Draws an upscaler's RGBA16F output into the DLSS output and returns it to GENERAL. + void DrawUpscaledOutput(Framebuffer* dst_framebuffer, VkImageView view, VkImage image, + VkSampler sampler); +#ifdef EGLANTINE_STREAMLINE + /// NVIDIA's DLSS (through NVIDIA Streamline) in place of the game's, + /// before FSR 3.1, when the setting is on. Returns false to fall back to FSR 3.1. + bool UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageView& input_color, + ImageView& motion_vectors, ImageView& depth, + std::array render_size, std::array jitter, + std::array motion_scale, bool reset); + std::unique_ptr dlss_upscaler; + bool dlss_unsupported{}; +#endif + /// TemporalUpscale's history: its previous result (images[current]) and the image the next /// result is stored to. struct TemporalHistory { diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index ea82ab02e7..a34c591aca 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1124,8 +1124,8 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds has_deleted_images = false; const ImageId motion_id = FindOrInsertImage(motion_info, motion_vectors.Address(), RelaxedOptions{}); - // The depth only feeds FSR 3.1 (looked up only when FSR can run); without it the stand-in - // runs. + // The depth only feeds NVIDIA DLSS and FSR 3.1 (looked up only when they can run); without + // it the stand-in runs. ImageId depth_id{}; if (depth && motion_id && runtime.WantsTemporalUpscaleDepth()) { depth_id = FindOrInsertImage(ImageInfo(*depth), depth->Address(), RelaxedOptions{}); diff --git a/src/video_core/vulkan_common/streamline.cpp b/src/video_core/vulkan_common/streamline.cpp new file mode 100644 index 0000000000..966de82cb5 --- /dev/null +++ b/src/video_core/vulkan_common/streamline.cpp @@ -0,0 +1,463 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// NVIDIA DLSS through NVIDIA Streamline, with Streamline's manual hooking. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "common/fs/path_util.h" +#include "common/logging.h" +#include "common/settings.h" +#include "video_core/vulkan_common/streamline.h" +#include "video_core/vulkan_common/streamline_api.h" + +#include + +#include +#include + +namespace Vulkan::Streamline { +namespace { + +// A custom engine names itself with its own project id; an application id from NVIDIA is only +// needed without an engine type. Keep it fixed: the driver may key behaviour off it. +constexpr const char* PROJECT_ID = "8d3e2f9a-6b41-4c7e-9a52-1f0c3b7d5e64"; +constexpr const wchar_t* INTERPOSER = L"sl.interposer.dll"; +constexpr const wchar_t* MODEL = L"nvngx_dlss.dll"; +// The first DLSS with preset K. +constexpr u32 MIN_DLSS_MAJOR = 310; + +using Version = std::array; + +struct State { + HMODULE module{}; + Api api{}; + std::wstring plugin_dir; + std::wstring model_dir; + std::vector instance_extensions; + std::vector device_extensions; + bool started{}; + bool attached{}; +}; + +State& GetState() { + static State state; + return state; +} + +int LogLevel() { + const char* const value = std::getenv("EDEN_NX2_DLSS_LOG"); + return value ? std::atoi(value) : 0; +} + +void LogFromStreamline(sl::LogType type, const char* message) { + std::string_view text{message ? message : ""}; + while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) { + text.remove_suffix(1); + } + switch (type) { + case sl::LogType::eError: + LOG_ERROR(Render_Vulkan, "Streamline: {}", text); + break; + case sl::LogType::eWarn: + LOG_WARNING(Render_Vulkan, "Streamline: {}", text); + break; + default: + if (LogLevel() > 0) { + LOG_INFO(Render_Vulkan, "Streamline: {}", text); + } + break; + } +} + +/// Where the user may put sl.interposer.dll: EDEN_STREAMLINE_DIR, the user folder's "streamline" +/// folder, then a "streamline" folder next to the executable. +std::vector SearchDirs() { + std::vector dirs; + if (const char* const dir = std::getenv("EDEN_STREAMLINE_DIR"); dir && *dir) { + dirs.emplace_back(dir); + } + dirs.push_back(UserFolder()); + dirs.push_back(Common::FS::GetExeDirectory() / "streamline"); + return dirs; +} + +template +bool Resolve(HMODULE module, const char* name, T*& function) { + function = reinterpret_cast(GetProcAddress(module, name)); + if (!function) { + LOG_ERROR(Render_Vulkan, "sl.interposer.dll has no {}", name); + } + return function != nullptr; +} + +std::optional ReadVersion(const std::filesystem::path& path) { + DWORD handle = 0; + const DWORD size = GetFileVersionInfoSizeW(path.c_str(), &handle); + if (size == 0) { + return std::nullopt; + } + std::vector data(size); + VS_FIXEDFILEINFO* info{}; + UINT length = 0; + if (!GetFileVersionInfoW(path.c_str(), 0, size, data.data()) || + !VerQueryValueW(data.data(), L"\\", reinterpret_cast(&info), &length) || !info) { + return std::nullopt; + } + return Version{HIWORD(info->dwFileVersionMS), LOWORD(info->dwFileVersionMS), + HIWORD(info->dwFileVersionLS), LOWORD(info->dwFileVersionLS)}; +} + +std::string VersionText(const std::filesystem::path& path) { + const std::optional version = path.empty() ? std::nullopt : ReadVersion(path); + if (!version) { + return {}; + } + const Version& v = *version; + return v[3] == 0 ? fmt::format("{}.{}.{}", v[0], v[1], v[2]) + : fmt::format("{}.{}.{}.{}", v[0], v[1], v[2], v[3]); +} + +/// The NVIDIA app keeps every DLSS it downloads under ProgramData; pick the newest. +std::filesystem::path FindAppModel() { + const wchar_t* const program_data = _wgetenv(L"ProgramData"); + if (!program_data) { + return {}; + } + const std::filesystem::path root = + std::filesystem::path{program_data} / L"NVIDIA" / L"NGX" / L"models" / L"dlss" / L"versions"; + std::filesystem::path best_path; + Version best{}; + std::error_code ec; + for (const auto& version_dir : std::filesystem::directory_iterator(root, ec)) { + std::error_code files_ec; + for (const auto& file : + std::filesystem::directory_iterator(version_dir.path() / L"files", files_ec)) { + if (file.path().extension() != L".bin") { + continue; + } + const std::optional version = ReadVersion(file.path()); + if (version && (*version)[0] >= MIN_DLSS_MAJOR && *version > best) { + best = *version; + best_path = file.path(); + } + } + } + return best_path; +} + +/// Copies the NVIDIA app's DLSS into our folder as nvngx_dlss.dll, the name Streamline loads. +std::filesystem::path UseAppModel(const DlssFiles& files) { + const std::filesystem::path dir = UserFolder() / L"nvidia-app"; + const std::filesystem::path target = dir / MODEL; + std::error_code ec; + const bool current = std::filesystem::is_regular_file(target, ec) && + std::filesystem::file_size(target, ec) == + std::filesystem::file_size(files.app_model, ec) && + ReadVersion(target) == ReadVersion(files.app_model); + 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()); + return {}; + } + std::filesystem::create_directories(dir, ec); + std::filesystem::copy_file(files.app_model, target, + 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()); + return {}; + } + } + return dir; +} + +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()); + return false; + } + state.module = LoadLibraryW(path.c_str()); + if (!state.module) { + LOG_ERROR(Render_Vulkan, "Could not load {} (error {})", path.string(), GetLastError()); + return false; + } + Api& api = state.api; + const HMODULE m = state.module; + if (Resolve(m, "slInit", api.init) && Resolve(m, "slShutdown", api.shutdown) && + Resolve(m, "slIsFeatureSupported", api.is_feature_supported) && + Resolve(m, "slGetFeatureRequirements", api.get_feature_requirements) && + Resolve(m, "slGetFeatureVersion", api.get_feature_version) && + Resolve(m, "slGetFeatureFunction", api.get_feature_function) && + Resolve(m, "slGetNewFrameToken", api.get_new_frame_token) && + Resolve(m, "slSetConstants", api.set_constants) && + Resolve(m, "slEvaluateFeature", api.evaluate_feature) && + Resolve(m, "slFreeResources", api.free_resources) && + Resolve(m, "slSetVulkanInfo", api.set_vulkan_info)) { + return true; + } + FreeLibrary(state.module); + state.module = nullptr; + state.api = {}; + return false; +} + +std::vector Names(uint32_t count, const char** names) { + std::vector result; + for (uint32_t i = 0; i < count; ++i) { + result.emplace_back(names[i]); + } + return result; +} + +} // Anonymous namespace + +std::filesystem::path UserFolder() { + return Common::FS::GetEdenPath(Common::FS::EdenPath::StreamlineDir); +} + +bool IsStreamlineModule(const std::filesystem::path& path) { + return sl::security::verifyEmbeddedSignature(path.c_str()); +} + +bool IsSignedByNvidia(const std::filesystem::path& path) { + const std::wstring file_path = std::filesystem::absolute(path).wstring(); + WINTRUST_FILE_INFO file{}; + file.cbStruct = sizeof(file); + file.pcwszFilePath = file_path.c_str(); + WINTRUST_DATA data{}; + data.cbStruct = sizeof(data); + data.dwUIChoice = WTD_UI_NONE; + data.fdwRevocationChecks = WTD_REVOKE_NONE; + data.dwUnionChoice = WTD_CHOICE_FILE; + data.pFile = &file; + data.dwStateAction = WTD_STATEACTION_VERIFY; + data.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL; + GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; + bool valid = WinVerifyTrust(nullptr, &action, &data) == ERROR_SUCCESS; + if (valid) { + CRYPT_PROVIDER_DATA* const provider = WTHelperProvDataFromStateData(data.hWVTStateData); + CRYPT_PROVIDER_SGNR* const signer = + provider ? WTHelperGetProvSignerFromChain(provider, 0, FALSE, 0) : nullptr; + CRYPT_PROVIDER_CERT* const cert = signer ? WTHelperGetProvCertFromChain(signer, 0) : nullptr; + wchar_t name[128]{}; + valid = cert && + CertGetNameStringW(cert->pCert, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, name, + static_cast(std::size(name))) > 1 && + std::wstring_view{name} == L"NVIDIA Corporation"; + } + data.dwStateAction = WTD_STATEACTION_CLOSE; + WinVerifyTrust(nullptr, &action, &data); + return valid; +} + +DlssFiles FindDlssFiles() { + DlssFiles files; + files.folder = UserFolder(); + std::error_code ec; + for (const std::filesystem::path& dir : SearchDirs()) { + if (std::filesystem::is_regular_file(dir / INTERPOSER, ec)) { + files.folder = dir; + files.interposer = dir / INTERPOSER; + break; + } + } + if (std::filesystem::is_regular_file(files.folder / MODEL, ec)) { + files.model = files.folder / MODEL; + } + files.app_model = FindAppModel(); + files.interposer_version = VersionText(files.interposer); + files.model_version = VersionText(files.model); + files.app_model_version = VersionText(files.app_model); + return files; +} + +bool Start() { + State& state = GetState(); + if (state.started) { + return true; + } + if (!Settings::values.nvidia_dlss.GetValue()) { + return false; + } + if (!state.module) { + const DlssFiles files = FindDlssFiles(); + if (files.interposer.empty()) { + 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()); + return false; + } + if (!Load(state, files.interposer)) { + return false; + } + state.plugin_dir = files.folder.wstring(); + state.model_dir.clear(); + if (files.model.empty() && !files.app_model.empty()) { + if (const std::filesystem::path dir = UseAppModel(files); !dir.empty()) { + state.model_dir = dir.wstring(); + LOG_INFO(Render_Vulkan, "Using the NVIDIA app's DLSS {}", files.app_model_version); + } + } else if (files.model.empty()) { + LOG_WARNING(Render_Vulkan, "No nvngx_dlss.dll in {} and no DLSS from the NVIDIA app", + files.folder.string()); + } + } + + // Streamline's plugins come from the loader's folder or the NVIDIA app's downloads (OTA); the + // DLSS model from the loader's folder, else the NVIDIA app's copy. + const wchar_t* plugin_paths[] = {state.plugin_dir.c_str(), state.model_dir.c_str()}; + const sl::Feature features[] = {sl::kFeatureDLSS}; + sl::Preferences preferences{}; + preferences.logLevel = LogLevel() >= 2 ? sl::LogLevel::eVerbose : sl::LogLevel::eDefault; + preferences.logMessageCallback = LogFromStreamline; + preferences.pathsToPlugins = plugin_paths; + preferences.numPathsToPlugins = state.model_dir.empty() ? 1 : 2; + preferences.flags = sl::PreferenceFlags::eDisableCLStateTracking | + sl::PreferenceFlags::eUseManualHooking | sl::PreferenceFlags::eAllowOTA | + sl::PreferenceFlags::eLoadDownloadedPlugins; + preferences.featuresToLoad = features; + preferences.numFeaturesToLoad = 1; + preferences.engine = sl::EngineType::eCustom; + preferences.engineVersion = "1.0"; + preferences.projectId = PROJECT_ID; + preferences.renderAPI = sl::RenderAPI::eVulkan; + const Api& api = state.api; + if (const sl::Result result = api.init(preferences, sl::kSDKVersion); + result != sl::Result::eOk) { + LOG_ERROR(Render_Vulkan, "NVIDIA Streamline failed to start (result {}); using FSR 3.1", + static_cast(result)); + return false; + } + sl::FeatureRequirements requirements{}; + if (const sl::Result result = api.get_feature_requirements(sl::kFeatureDLSS, requirements); + result != sl::Result::eOk) { + LOG_WARNING(Render_Vulkan, + "Streamline found no DLSS plugin (result {}); put sl.dlss.dll next to " + "sl.interposer.dll or install the NVIDIA app. Using FSR 3.1", + static_cast(result)); + api.shutdown(); + return false; + } + if (requirements.vkNumComputeQueuesRequired != 0 || + requirements.vkNumGraphicsQueuesRequired != 0) { + LOG_WARNING(Render_Vulkan, "This DLSS wants extra Vulkan queues; using FSR 3.1"); + api.shutdown(); + return false; + } + state.instance_extensions = + Names(requirements.vkNumInstanceExtensions, requirements.vkInstanceExtensions); + state.device_extensions = + Names(requirements.vkNumDeviceExtensions, requirements.vkDeviceExtensions); + // The KHR and EXT buffer device address extensions can't both be enabled. + const auto& device_extensions = state.device_extensions; + if (std::ranges::find(device_extensions, VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME) != + device_extensions.end()) { + std::erase(state.device_extensions, VK_EXT_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); + } + 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.versionNGX.toStr()); + state.started = true; + return true; +} + +bool WantsDevice() { + const State& state = GetState(); + return state.started && !state.attached; +} + +std::vector InstanceExtensions() { + const State& state = GetState(); + return state.started ? state.instance_extensions : std::vector{}; +} + +std::vector DeviceExtensions() { + const State& state = GetState(); + return state.started ? state.device_extensions : std::vector{}; +} + +bool AttachDevice(VkInstance instance, VkPhysicalDevice physical, VkDevice device, + u32 graphics_family, vk::DeviceDispatch& dld) { + State& state = GetState(); + if (!WantsDevice()) { + return false; + } + sl::VulkanInfo info{}; + info.device = device; + info.instance = instance; + info.physicalDevice = physical; + info.computeQueueFamily = graphics_family; + info.graphicsQueueFamily = graphics_family; + if (const sl::Result result = state.api.set_vulkan_info(info); result != sl::Result::eOk) { + LOG_ERROR(Render_Vulkan, "Streamline did not take the Vulkan device (result {})", + static_cast(result)); + Stop(); + return false; + } + sl::AdapterInfo adapter{}; + adapter.vkPhysicalDevice = physical; + if (const sl::Result result = state.api.is_feature_supported(sl::kFeatureDLSS, adapter); + result != sl::Result::eOk) { + LOG_WARNING(Render_Vulkan, + "NVIDIA DLSS is not supported on this GPU or driver (Streamline result {}); " + "using FSR 3.1", + static_cast(result)); + Stop(); + return false; + } + // Manual hooking: Streamline does its per-frame work in these. + const auto hook = [&](auto& function, const char* name) { + if (const FARPROC proxy = GetProcAddress(state.module, name)) { + function = reinterpret_cast>(proxy); + } else { + LOG_WARNING(Render_Vulkan, "sl.interposer.dll has no {}", name); + } + }; + hook(dld.vkCreateSwapchainKHR, "vkCreateSwapchainKHR"); + hook(dld.vkDestroySwapchainKHR, "vkDestroySwapchainKHR"); + hook(dld.vkGetSwapchainImagesKHR, "vkGetSwapchainImagesKHR"); + hook(dld.vkAcquireNextImageKHR, "vkAcquireNextImageKHR"); + hook(dld.vkQueuePresentKHR, "vkQueuePresentKHR"); + hook(dld.vkDeviceWaitIdle, "vkDeviceWaitIdle"); + state.attached = true; + LOG_INFO(Render_Vulkan, "NVIDIA DLSS is available through Streamline"); + return true; +} + +bool IsDlssAvailable() { + const State& state = GetState(); + return state.started && state.attached; +} + +void Stop() { + State& state = GetState(); + if (!state.started) { + return; + } + const sl::Result result = state.api.shutdown(); + state.started = false; + state.attached = false; + LOG_INFO(Render_Vulkan, "NVIDIA Streamline stopped (result {})", static_cast(result)); +} + +const Api& GetApi() { + return GetState().api; +} + +} // namespace Vulkan::Streamline diff --git a/src/video_core/vulkan_common/streamline.h b/src/video_core/vulkan_common/streamline.h new file mode 100644 index 0000000000..47e55f7150 --- /dev/null +++ b/src/video_core/vulkan_common/streamline.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// NVIDIA DLSS through NVIDIA Streamline. Only Streamline's MIT-licensed +// headers are built in: NVIDIA's DLLs are loaded at run time from the user's PC, and none ship +// with the emulator. + +#pragma once + +#include +#include +#include + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan::Streamline { + +/// NVIDIA's files DLSS needs, as found on this PC. Paths are empty when missing. +struct DlssFiles { + std::filesystem::path folder; ///< Where the loader is, or where it should go + std::filesystem::path interposer; ///< sl.interposer.dll (Streamline's loader) + std::filesystem::path model; ///< nvngx_dlss.dll next to the loader + std::filesystem::path app_model; ///< Newest DLSS the NVIDIA app downloaded + std::string interposer_version; + std::string model_version; + std::string app_model_version; +}; + +[[nodiscard]] DlssFiles FindDlssFiles(); + +/// The user folder's "streamline" folder. +[[nodiscard]] std::filesystem::path UserFolder(); + +/// Streamline's own modules carry a second, Streamline-only signature. +[[nodiscard]] bool IsStreamlineModule(const std::filesystem::path& path); + +/// A valid Authenticode signature from NVIDIA Corporation (DLSS models). +[[nodiscard]] bool IsSignedByNvidia(const std::filesystem::path& path); + +/// When the NVIDIA DLSS setting is on, loads the user's sl.interposer.dll (checking NVIDIA's +/// signature) and starts Streamline for DLSS. Call before creating the renderer's instance. +bool Start(); + +/// Whether Streamline is running and waiting for the renderer's device. +[[nodiscard]] bool WantsDevice(); + +/// Instance and device extensions DLSS asks for (empty when Streamline isn't running). +[[nodiscard]] std::vector InstanceExtensions(); +[[nodiscard]] std::vector DeviceExtensions(); + +/// Hands the renderer's device to Streamline and routes the swapchain calls through it, as +/// Streamline's manual hooking needs. Stops Streamline and returns false when DLSS can't run. +bool AttachDevice(VkInstance instance, VkPhysicalDevice physical, VkDevice device, + u32 graphics_family, vk::DeviceDispatch& dld); + +/// Whether DLSS can run on the attached device. +[[nodiscard]] bool IsDlssAvailable(); + +/// Shuts Streamline down; call before the device is destroyed. +void Stop(); + +} // namespace Vulkan::Streamline diff --git a/src/video_core/vulkan_common/streamline_api.h b/src/video_core/vulkan_common/streamline_api.h new file mode 100644 index 0000000000..0a82c1dd7d --- /dev/null +++ b/src/video_core/vulkan_common/streamline_api.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Streamline's API as loaded from the user's sl.interposer.dll. Only for the +// Streamline sources: it pulls in and Streamline's (MIT-licensed) headers. + +#pragma once + +#include + +#include "video_core/vulkan_common/vulkan_wrapper.h" + +#include +#include +#include + +namespace Vulkan::Streamline { + +struct Api { + PFun_slInit* init{}; + PFun_slShutdown* shutdown{}; + PFun_slIsFeatureSupported* is_feature_supported{}; + PFun_slGetFeatureRequirements* get_feature_requirements{}; + PFun_slGetFeatureVersion* get_feature_version{}; + PFun_slGetFeatureFunction* get_feature_function{}; + PFun_slGetNewFrameToken* get_new_frame_token{}; + PFun_slSetConstants* set_constants{}; + PFun_slEvaluateFeature* evaluate_feature{}; + PFun_slFreeResources* free_resources{}; + PFun_slSetVulkanInfo* set_vulkan_info{}; +}; + +/// The loaded functions; only valid while IsDlssAvailable(). +[[nodiscard]] const Api& GetApi(); + +} // namespace Vulkan::Streamline diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 01e13cb384..6309599877 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -32,6 +32,9 @@ #include "video_core/vulkan_common/vulkan_device.h" #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/gpu_logging/gpu_logging.h" +#ifdef EGLANTINE_STREAMLINE +#include "video_core/vulkan_common/streamline.h" +#endif #if defined(__ANDROID__) && defined(ARCHITECTURE_arm64) #include @@ -743,6 +746,15 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR RemoveExtensionFeature(extensions.descriptor_buffer, features.descriptor_buffer, VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME); } +#ifdef EGLANTINE_STREAMLINE + // NVIDIA's DLSS needs buffer device addresses: keep the feature enabled for + // it, but leave Eglantine's own buffers as they are without descriptor buffers. + const bool dlss_wants_device_address = + streamline_extensions && features.buffer_device_address.bufferDeviceAddress; + if (!extensions.descriptor_buffer && dlss_wants_device_address) { + extensions.buffer_device_address = false; + } else +#endif if (!extensions.descriptor_buffer) { RemoveExtensionFeature(extensions.buffer_device_address, features.buffer_device_address, VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); @@ -753,6 +765,15 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR graphics_queue = logical.GetQueue(graphics_family); present_queue = logical.GetQueue(present_family); +#ifdef EGLANTINE_STREAMLINE + if (streamline_extensions) { + streamline_attached = + Streamline::AttachDevice(instance, physical, *logical, graphics_family, dld); + } else if (Streamline::WantsDevice()) { + Streamline::Stop(); + } +#endif + VmaVulkanFunctions functions{}; functions.vkGetInstanceProcAddr = dld.vkGetInstanceProcAddr; functions.vkGetDeviceProcAddr = dld.vkGetDeviceProcAddr; @@ -806,6 +827,11 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR } Device::~Device() { +#ifdef EGLANTINE_STREAMLINE + if (streamline_attached) { + Streamline::Stop(); + } +#endif SaveStaticPipelineCache(); ShutdownGPULogging(); for (auto& pools : separate_pools) { @@ -1123,6 +1149,27 @@ bool Device::GetSuitability(bool requires_swapchain) { extensions.robustness_2 = false; } +#ifdef EGLANTINE_STREAMLINE + // The extensions NVIDIA's DLSS asks for, where the device has them all. + if (Streamline::WantsDevice()) { + const std::vector wanted = Streamline::DeviceExtensions(); + const bool all = + !wanted.empty() && std::ranges::all_of(wanted, [&](const std::string& name) { + return supported_extensions.contains(name); + }); + std::string names; + for (const std::string& name : wanted) { + if (all) { + loaded_extensions.insert(name); + } + names += (names.empty() ? "" : ", ") + name; + } + streamline_extensions = all; + LOG_INFO(Render_Vulkan, "NVIDIA DLSS device extensions: {} ({})", + all ? "enabled" : "unavailable", names); + } +#endif + #undef FEATURE_EXTENSION #undef EXTENSION diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index edc091318c..3115f4547c 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -289,6 +289,11 @@ public: return physical; } + /// Returns the instance the device was created from. + VkInstance GetInstance() const { + return instance; + } + VkPipelineCache StaticPipelineCache() const noexcept { return *static_pipeline_cache; } @@ -1326,6 +1331,8 @@ private: // Telemetry parameters std::set> supported_extensions; ///< Reported Vulkan extensions. std::set> loaded_extensions; ///< Loaded Vulkan extensions. + bool streamline_extensions{}; ///< NVIDIA DLSS's extensions are loaded. + bool streamline_attached{}; ///< Streamline has this device. std::vector valid_heap_memory; ///< Heaps used. size_t main_heap{}; ///< Largest device-local heap, where images live. u64 heap_size_limit{}; ///< EDEN_NX2_VRAM_LIMIT_MIB for device-local heaps, 0 if unset. diff --git a/src/video_core/vulkan_common/vulkan_instance.cpp b/src/video_core/vulkan_common/vulkan_instance.cpp index 7f3d8ed41f..3ab924e37f 100644 --- a/src/video_core/vulkan_common/vulkan_instance.cpp +++ b/src/video_core/vulkan_common/vulkan_instance.cpp @@ -18,6 +18,11 @@ #include "core/frontend/emu_window.h" #include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_wrapper.h" +#ifdef EGLANTINE_STREAMLINE +#include +#include +#include "video_core/vulkan_common/streamline.h" +#endif namespace Vulkan { namespace { @@ -82,6 +87,21 @@ namespace { #endif if (enable_validation && AreExtensionsSupported(dld, properties, std::array{VK_EXT_DEBUG_UTILS_EXTENSION_NAME})) extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); +#ifdef EGLANTINE_STREAMLINE + // What NVIDIA's DLSS asks for through Streamline, where available. Static: the returned + // pointers must outlive this call. + static std::vector streamline; + streamline = Streamline::InstanceExtensions(); + for (const std::string& name : streamline) { + const bool available = std::ranges::any_of(properties, [&](const VkExtensionProperties& p) { + return name == p.extensionName; + }); + const bool listed = std::ranges::any_of(extensions, [&](const char* e) { return name == e; }); + if (available && !listed) { + extensions.push_back(name.c_str()); + } + } +#endif return extensions; } diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 074b6f405b..3a1c3bf2c2 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -226,6 +226,12 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) { continue; } #endif +#ifndef EGLANTINE_STREAMLINE + // Only builds with NVIDIA Streamline (Windows) can use it. + if (setting->Id() == Settings::values.nvidia_dlss.Id()) { + continue; + } +#endif ConfigurationShared::Widget* widget = [&]() { if (setting->Id() == Settings::values.fsr_sharpening_slider.Id()) { -- cgit v1.2.3 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/qt_common/config/shared_translation.cpp | 4 +- src/yuzu/CMakeLists.txt | 4 + src/yuzu/configuration/configure_graphics.cpp | 15 + src/yuzu/configuration/nvidia_dlss_panel.cpp | 549 ++++++++++++++++++++++++++ src/yuzu/configuration/nvidia_dlss_panel.h | 34 ++ 5 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 src/yuzu/configuration/nvidia_dlss_panel.cpp create mode 100644 src/yuzu/configuration/nvidia_dlss_panel.h diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index b395b2959c..342cc725c0 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -152,8 +152,8 @@ std::unique_ptr InitializeTranslations(QObject* parent) { INSERT(Settings, nvidia_dlss, tr("Use NVIDIA DLSS in place of a game's DLSS"), tr("Uses NVIDIA DLSS on GeForce RTX cards in place of a Switch 2 game's own DLSS, " "instead of AMD FSR 3.1.\nNeeds NVIDIA's Streamline loader, and NVIDIA DLSS unless " - "the NVIDIA app has downloaded it; Eglantine does not ship NVIDIA's files.\nTakes " - "effect when a game starts.")); + "the NVIDIA app has downloaded it; Eglantine does not ship NVIDIA's files. Use the " + "download button below.\nTakes effect when a game starts.")); INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"), tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA " "can produce a more stable picture in lower resolutions.")); diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index f5895edd10..f05da88c40 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -74,6 +74,8 @@ add_executable(yuzu configuration/configure_general.ui configuration/configure_graphics.cpp configuration/configure_graphics.h + configuration/nvidia_dlss_panel.cpp + configuration/nvidia_dlss_panel.h configuration/configure_graphics.ui configuration/configure_graphics_advanced.cpp configuration/configure_graphics_advanced.h @@ -407,6 +409,8 @@ endif() target_link_libraries(yuzu PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(yuzu PRIVATE common core input_common frontend_common network video_core qt_common) target_link_libraries(yuzu PRIVATE Boost::headers Qt6::Widgets Qt6::Charts Qt6::Concurrent) +# Unpacks NVIDIA's files from the Streamline release (nvidia_dlss_panel.cpp). +target_link_libraries(yuzu PRIVATE ZLIB::ZLIB) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) if (ENABLE_OPENGL) target_link_libraries(yuzu PRIVATE glad) diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 3a1c3bf2c2..09ae6adf6e 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -35,6 +35,7 @@ #include "ui_configure_graphics.h" #include "yuzu/configuration/configuration_shared.h" #include "yuzu/configuration/configure_graphics.h" +#include "yuzu/configuration/nvidia_dlss_panel.h" #include "yuzu/configuration/shared_widget.h" #ifdef HAS_RESHADE @@ -328,6 +329,20 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) { row->insertWidget(1, post_shader_button); hold_graphics.emplace(setting->Id(), widget); +#endif +#ifdef EGLANTINE_STREAMLINE + } else if (setting->Id() == Settings::values.nvidia_dlss.Id()) { + // NVIDIA's files and a download button under the checkbox. + QWidget* group = new QWidget(this); + QVBoxLayout* group_layout = new QVBoxLayout(group); + group_layout->setContentsMargins(0, 0, 0, 0); + group_layout->setSpacing(2); + group_layout->addWidget(widget); + NvidiaDlssPanel* panel = new NvidiaDlssPanel(group); + group_layout->addWidget(panel); + panel->setVisible(widget->checkbox->isChecked()); + connect(widget->checkbox, &QCheckBox::toggled, panel, &QWidget::setVisible); + hold_graphics.emplace(setting->Id(), group); #endif } else if (setting->Id() == Settings::values.frame_gen.Id()) { frame_gen_checkbox = widget->checkbox; 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 diff --git a/src/yuzu/configuration/nvidia_dlss_panel.h b/src/yuzu/configuration/nvidia_dlss_panel.h new file mode 100644 index 0000000000..1ab39178eb --- /dev/null +++ b/src/yuzu/configuration/nvidia_dlss_panel.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// Shown under the NVIDIA DLSS setting: which of NVIDIA's files were found, and +// a button that fetches missing ones from NVIDIA's own Streamline release. + +#pragma once + +#ifdef EGLANTINE_STREAMLINE + +#include + +class QLabel; +class QPushButton; + +class NvidiaDlssPanel : public QWidget { +public: + explicit NvidiaDlssPanel(QWidget* parent); + + void Refresh(); + +protected: + void showEvent(QShowEvent* event) override; + +private: + void Download(); + + QLabel* icon_label; + QLabel* status_label; + QPushButton* download_button; + QPushButton* folder_button; +}; + +#endif -- cgit v1.2.3 From 59af48587b9f8a99b04c618b069ec7895c2f1989 Mon Sep 17 00:00:00 2001 From: PastaLaPate Date: Tue, 3 Nov 2026 18:33:39 +0100 Subject: fixes and added workflow --- .github/ISSUE_TEMPLATE/game_bug.yml | 2 +- .github/ISSUE_TEMPLATE/visual_bug.yml | 2 +- .github/workflows/label-issues.yml | 86 +++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/label-issues.yml diff --git a/.github/ISSUE_TEMPLATE/game_bug.yml b/.github/ISSUE_TEMPLATE/game_bug.yml index 02012ccfc2..8c3c775ea8 100644 --- a/.github/ISSUE_TEMPLATE/game_bug.yml +++ b/.github/ISSUE_TEMPLATE/game_bug.yml @@ -1,7 +1,7 @@ name: Gameplay Bug description: The game doesnt works properly title: "[GBug] " -labels: ["bug"] +labels: ["game-bug"] body: - type: checkboxes id: checks diff --git a/.github/ISSUE_TEMPLATE/visual_bug.yml b/.github/ISSUE_TEMPLATE/visual_bug.yml index efe5bfaebf..15daf8f5c1 100644 --- a/.github/ISSUE_TEMPLATE/visual_bug.yml +++ b/.github/ISSUE_TEMPLATE/visual_bug.yml @@ -1,5 +1,5 @@ name: Visual Bug -description: The game doesnt renders properly +description: The game doesn't render properly title: "[VBug] " labels: ["visual-bug"] body: diff --git a/.github/workflows/label-issues.yml b/.github/workflows/label-issues.yml new file mode 100644 index 0000000000..4f8c70ee3a --- /dev/null +++ b/.github/workflows/label-issues.yml @@ -0,0 +1,86 @@ +name: Label issues from form +on: + issues: + types: [opened] + +permissions: + issues: write + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const body = context.payload.issue.body || ""; + + // Parse "### Title\n\nanswer" sections into a map + const sections = {}; + body.split(/^### /m).slice(1).forEach((block) => { + const [title, ...rest] = block.split("\n"); + sections[title.trim()] = rest.join("\n").trim(); + }); + const get = (k) => { + const v = sections[k]; + return v && v !== "_No response_" ? v : null; + }; + + const gameLabels = { + "Mario Kart World": "Mario Kart", + "Donkey Kong Bananza": "DKB", + "Fire Emblem": "Fire Emblem", + "Splatoon Raiders": "Splatoon Raiders", + "Pokopia": "Pokopia", + "Hyrule Warriors": "Hyrule Warriors", + "Welcome Tour": "Welcome Tour", + }; + + const labels = new Set(); + + // Game + const game = get("Game"); + if (game && gameLabels[game]) labels.add(gameLabels[game]); + + // GPU vendor + const gpu = get("Graphics card"); + if (gpu) { + if (/nvidia/i.test(gpu)) labels.add("NVIDIA"); + else if (/amd/i.test(gpu)) labels.add("AMD"); + else if (/intel/i.test(gpu)) labels.add("INTEL"); + } + + // OS + const os = get("Operating system"); + if (os) { + if (/windows/i.test(os)) labels.add("windows"); + else if (/linux/i.test(os)) labels.add("linux"); + else if (/macos/i.test(os)) labels.add("MacOS"); + } + + // Regression + if (/- \[x\] This is a regression/i.test(sections["Regression"] || "")) { + labels.add("regression"); + } + if (get("Last known working build")) labels.add("needs-bisect"); + + // Crash without log + const isCrash = (context.payload.issue.labels || []).some( + (l) => l.name.toLowerCase() === "crash" + ); + if (isCrash && !get("Log / crash dump")) { + labels.add("needs-info"); + await github.rest.issues.createComment({ + ...context.repo, + issue_number: context.issue.number, + body: "Thanks for the report! Please attach the log / crash dump so we can investigate.", + }); + } + + if (labels.size) { + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: context.issue.number, + labels: [...labels], + }); + } -- cgit v1.2.3 From 5f5cb6475e37a316e8d6b41f17e6acf154ec7de5 Mon Sep 17 00:00:00 2001 From: PastaLaPate Date: Tue, 3 Nov 2026 18:44:37 +0100 Subject: made changes suggested by coderabbit --- .github/ISSUE_TEMPLATE/crash.yml | 1 - .github/ISSUE_TEMPLATE/ui_bug.yml | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index d68e3db284..1e1bd85d93 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -96,4 +96,3 @@ body: id: logs attributes: label: Log / crash dump - render: shell diff --git a/.github/ISSUE_TEMPLATE/ui_bug.yml b/.github/ISSUE_TEMPLATE/ui_bug.yml index 7caa773094..9c24d30e60 100644 --- a/.github/ISSUE_TEMPLATE/ui_bug.yml +++ b/.github/ISSUE_TEMPLATE/ui_bug.yml @@ -12,8 +12,6 @@ body: required: true - label: I'm using the latest build required: true - - label: My game is legally dumped from my own console - required: true - type: input id: build -- cgit v1.2.3 From b1179a44fbd1cf514c2bed56bf235fbe456c8c7b Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:56:12 -0500 Subject: video_core: clear the DLSS output when Streamline writes nothing to it The upscaler records its work on the worker thread and reports success once it is queued. When Streamline then rejects the DLSS options, gives no frame token or fails the evaluation, nothing is written to the output image, but the caller still draws it into the game's image that frame: undefined contents after a resize, the previous frame otherwise. FSR 3.1 takes over from the next frame. Clear the output on those paths, with barriers around the clear, so that frame is black instead. Found by CodeRabbit review. Assisted-by: Claude Opus 5.5 (implementation) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- .../renderer_vulkan/vk_streamline_dlss.cpp | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp index aef32735f9..4fa9e240c4 100644 --- a/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp +++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp @@ -260,6 +260,40 @@ bool StreamlineDlssUpscaler::Upscale(const Inputs& in) { output_view_handle = *output_view](vk::CommandBuffer cmdbuf) { const Streamline::Api& api = Streamline::GetApi(); const sl::ViewportHandle viewport{0u}; + // The caller draws the output this frame even when DLSS writes nothing to it: clear it + // then, rather than show undefined contents (FSR 3.1 takes over from the next frame). + const auto clear_output = [&] { + const VkImageSubresourceRange range{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }; + const auto barrier = [&](VkAccessFlags src_access, VkAccessFlags dst_access) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = output_image_handle, + .subresourceRange = range, + }; + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + barrier(VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT)); + cmdbuf.ClearColorImage(output_image_handle, VK_IMAGE_LAYOUT_GENERAL, + VkClearColorValue{}, range); + cmdbuf.PipelineBarrier( + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, + barrier(VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT)); + }; if (options_changed) { const auto set = reinterpret_cast(set_options); if (const sl::Result result = set(viewport, options); result != sl::Result::eOk) { @@ -268,6 +302,7 @@ bool StreamlineDlssUpscaler::Upscale(const Inputs& in) { "Streamline rejected the DLSS options (result {}); using FSR 3.1", static_cast(result)); } + clear_output(); return; } } @@ -278,6 +313,7 @@ bool StreamlineDlssUpscaler::Upscale(const Inputs& in) { LOG_ERROR(Render_Vulkan, "Streamline gave no frame token (result {}); using FSR 3.1", static_cast(result)); } + clear_output(); return; } @@ -377,6 +413,9 @@ bool StreamlineDlssUpscaler::Upscale(const Inputs& in) { cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, output_written, {}, to_general); + if (result != sl::Result::eOk) { + clear_output(); + } }); // DLSS binds its own pipelines and descriptors on the command buffer. scheduler.InvalidateState(); -- 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(-) 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(-) 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 From 1889602c7a0dc17850692594eae2e77e16ac13fc Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:18:12 -0500 Subject: video_core: accept an empty Streamline device-extension list Streamline may report that DLSS needs no extra device extensions. The check required a non-empty list, so that result counted as "extensions unavailable": Streamline was stopped and FSR 3.1 used. Only require that every listed extension is supported. 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/vulkan_device.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp index 6309599877..2499f46f87 100644 --- a/src/video_core/vulkan_common/vulkan_device.cpp +++ b/src/video_core/vulkan_common/vulkan_device.cpp @@ -1153,10 +1153,9 @@ bool Device::GetSuitability(bool requires_swapchain) { // The extensions NVIDIA's DLSS asks for, where the device has them all. if (Streamline::WantsDevice()) { const std::vector wanted = Streamline::DeviceExtensions(); - const bool all = - !wanted.empty() && std::ranges::all_of(wanted, [&](const std::string& name) { - return supported_extensions.contains(name); - }); + // Streamline may ask for no extensions at all. + const bool all = std::ranges::all_of( + wanted, [&](const std::string& name) { return supported_extensions.contains(name); }); std::string names; for (const std::string& name : wanted) { if (all) { @@ -1166,7 +1165,7 @@ bool Device::GetSuitability(bool requires_swapchain) { } streamline_extensions = all; LOG_INFO(Render_Vulkan, "NVIDIA DLSS device extensions: {} ({})", - all ? "enabled" : "unavailable", names); + all ? "enabled" : "unavailable", names.empty() ? "none" : names); } #endif -- cgit v1.2.3 From fe5a9278740d121e543d2f89cdd459eca7aeb761 Mon Sep 17 00:00:00 2001 From: Mcm190 Date: Tue, 3 Nov 2026 19:13:08 +0000 Subject: ci: bundle Wayland plugins in Linux AppImages Request the Wayland platform plugins and their client graphics and shell integrations from linuxdeploy's Qt plugin. The existing Qt installation already contains them, so no extra Qt downloads are needed. Addresses #298. Assisted-by: Codex (diagnosis, packaging fix, validation, and draft text) --- .github/workflows/release-binaries.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 36b69d029e..4273e129e8 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -391,6 +391,9 @@ jobs: cp dist/Eglantine.png "$icon" export APPIMAGE_EXTRACT_AND_RUN=1 export QMAKE="$QT_ROOT_DIR/bin/qmake" + # Include Wayland platform plugins and their graphics/shell integrations. + export EXTRA_QT_MODULES=waylandcompositor + export EXTRA_PLATFORM_PLUGINS='libqwayland-egl.so;libqwayland-generic.so' export LDAI_OUTPUT="eglantine-${GITHUB_SHA}-${{ matrix.platform }}.AppImage" export OUTPUT="$LDAI_OUTPUT" export ARCH="$arch" -- cgit v1.2.3 From 4383b9787afbf6c63291bf803c8330afb4f21a16 Mon Sep 17 00:00:00 2001 From: PastaLaPate Date: Tue, 3 Nov 2026 21:56:38 +0100 Subject: ci(release): add build number to appimage --- .github/workflows/release-binaries.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index 4273e129e8..9e5030c471 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -394,6 +394,8 @@ jobs: # Include Wayland platform plugins and their graphics/shell integrations. export EXTRA_QT_MODULES=waylandcompositor export EXTRA_PLATFORM_PLUGINS='libqwayland-egl.so;libqwayland-generic.so' + # Embedded in the AppImage metadata; matches the release tag (build-N) plus the commit. + export LDAI_VERSION="build-${GITHUB_RUN_NUMBER}-${GITHUB_SHA::7}" export LDAI_OUTPUT="eglantine-${GITHUB_SHA}-${{ matrix.platform }}.AppImage" export OUTPUT="$LDAI_OUTPUT" export ARCH="$arch" -- cgit v1.2.3