aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-11-03 21:58:24 +0100
committercirakg <cirakg@cirakg.xyz>2026-11-03 21:58:24 +0100
commitcdfb1ae93444c0aa02dfe592af36d70ab36549fd (patch)
tree2fa8d7062d907d5ab8d85d64c66be87ea4cdf0d1
parent51341ec09b25de1d9b01d0c00ee828dd5b861d5c (diff)
parent4383b9787afbf6c63291bf803c8330afb4f21a16 (diff)
Merge upstream Djihads80/Eglantine nx2 into nx2HEADnx2
# Conflicts: # .github/workflows/release-binaries.yml
-rw-r--r--.github/ISSUE_TEMPLATE/blank.yml (renamed from .github/ISSUE_TEMPLATE/blank_issue_template.yml)0
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yml64
-rw-r--r--.github/ISSUE_TEMPLATE/crash.yml98
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.yml27
-rw-r--r--.github/ISSUE_TEMPLATE/game_bug.yml99
-rw-r--r--.github/ISSUE_TEMPLATE/ui_bug.yml81
-rw-r--r--.github/ISSUE_TEMPLATE/visual_bug.yml99
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md6
-rw-r--r--.github/workflows/label-issues.yml86
-rw-r--r--.github/workflows/release-binaries.yml8
-rw-r--r--CONTRIBUTING.md64
-rw-r--r--dist/Eglantine.icnsbin60987 -> 0 bytes
-rw-r--r--dist/packaging/arch/eglantine-git/PKGBUILD78
-rw-r--r--dist/packaging/arch/eglantine-git/README.md26
-rw-r--r--src/common/cpu_features.cpp6
-rw-r--r--src/common/fs/fs_paths.h1
-rw-r--r--src/common/fs/path_util.cpp1
-rw-r--r--src/common/fs/path_util.h1
-rw-r--r--src/common/scm_rev.cpp.in1
-rw-r--r--src/common/settings.h2
-rw-r--r--src/core/hle/service/hid/hid_server.cpp21
-rw-r--r--src/core/hle/service/hid/hid_server.h11
-rw-r--r--src/qt_common/config/shared_translation.cpp5
-rw-r--r--src/qt_common/game_list/worker.cpp3
-rw-r--r--src/tests/core/internal_network/socket_proxy.cpp2
-rw-r--r--src/tests/video_core/descriptor_table.cpp10
-rw-r--r--src/video_core/CMakeLists.txt18
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h7
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp18
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.cpp6
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.h3
-rw-r--r--src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp6
-rw-r--r--src/video_core/renderer_vulkan/vk_master_semaphore.cpp8
-rw-r--r--src/video_core/renderer_vulkan/vk_streamline_dlss.cpp425
-rw-r--r--src/video_core/renderer_vulkan/vk_streamline_dlss.h80
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp122
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h17
-rw-r--r--src/video_core/texture_cache/descriptor_table.h12
-rw-r--r--src/video_core/texture_cache/texture_cache.h8
-rw-r--r--src/video_core/vulkan_common/streamline.cpp466
-rw-r--r--src/video_core/vulkan_common/streamline.h63
-rw-r--r--src/video_core/vulkan_common/streamline_api.h36
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp46
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h7
-rw-r--r--src/video_core/vulkan_common/vulkan_instance.cpp20
-rw-r--r--src/yuzu/CMakeLists.txt4
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp21
-rw-r--r--src/yuzu/configuration/nvidia_dlss_panel.cpp556
-rw-r--r--src/yuzu/configuration/nvidia_dlss_panel.h34
-rw-r--r--src/yuzu/main_window.cpp36
-rw-r--r--src/yuzu_cmd/yuzu.cpp6
51 files changed, 2631 insertions, 194 deletions
diff --git a/.github/ISSUE_TEMPLATE/blank_issue_template.yml b/.github/ISSUE_TEMPLATE/blank.yml
index 49b7f38228..49b7f38228 100644
--- a/.github/ISSUE_TEMPLATE/blank_issue_template.yml
+++ b/.github/ISSUE_TEMPLATE/blank.yml
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..1e1bd85d93
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash.yml
@@ -0,0 +1,98 @@
+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
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index cc588ecb98..21da308e1a 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -1,28 +1,19 @@
name: Feature Request
-description: File a feature request
-labels: "request"
+description: Feature Request for Eglantine's UI
+title: "[Feature Request] "
+labels: ["enhancement"]
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
+ id: wanted
attributes:
- label: What feature are you suggesting?
- description: A brief description of the requested feature.
+ label: What do you want ?
+ placeholder: "I would like the UI to have..."
validations:
required: true
- type: textarea
- id: why-feature
+ id: useful
attributes:
- label: Why would this feature be useful?
- description: A brief description of why this feature would make Eden better.
+ 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..8c3c775ea8
--- /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: ["game-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..9c24d30e60
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/ui_bug.yml
@@ -0,0 +1,81 @@
+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
+
+ - 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..15daf8f5c1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/visual_bug.yml
@@ -0,0 +1,99 @@
+name: Visual Bug
+description: The game doesn't render 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
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/.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],
+ });
+ }
diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml
index db663a98a1..746e352c17 100644
--- a/.github/workflows/release-binaries.yml
+++ b/.github/workflows/release-binaries.yml
@@ -15,6 +15,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/**"
@@ -178,6 +181,11 @@ 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'
+ # 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}-${PLATFORM}.AppImage"
export OUTPUT="$LDAI_OUTPUT"
export ARCH=x86_64
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: <https://liberapay.com/crueter>
- - 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?
-
-<details>
-<summary>To sign up and begin contributing... (click to open)</summary>
-
-Email [crueter@crueter.xyz](mailto:crueter@crueter.xyz) with the following format:
-
-```txt
-Subject: [Eden Git] Registration Request
-Username: <Your Desired Username>
-Email: <Your Desired Email>
-I wish to sign up because... <your reason here>
-```
-
-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.
-</details>
+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**.
diff --git a/dist/Eglantine.icns b/dist/Eglantine.icns
deleted file mode 100644
index 7169466694..0000000000
--- a/dist/Eglantine.icns
+++ /dev/null
Binary files differ
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.
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();
}
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/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/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<bool> nvidia_dlss{linkage, false, "nvidia_dlss", Category::Renderer};
SwitchableSetting<AspectRatio, true> aspect_ratio{linkage,
AspectRatio::R16_9,
"aspect_ratio",
diff --git a/src/core/hle/service/hid/hid_server.cpp b/src/core/hle/service/hid/hid_server.cpp
index 20118345a9..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 <algorithm>
#include <array>
#include "common/common_types.h"
@@ -1112,8 +1113,24 @@ Result IHidServer::IsVibrationDeviceMounted(
R_SUCCEED();
}
-Result IHidServer::SendNx2VibrationValues() {
- LOG_DEBUG(Service_HID, "(STUBBED) called");
+Result IHidServer::SendNx2VibrationValues(
+ u64 unknown, AppletResourceUserId aruid,
+ InArray<Core::HID::VibrationDeviceHandle, BufferAttr_HipcPointer> vibration_handles,
+ InArray<Core::HID::VibrationValue, BufferAttr_HipcPointer> 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);
+ }
+ }
+
R_SUCCEED();
}
diff --git a/src/core/hle/service/hid/hid_server.h b/src/core/hle/service/hid/hid_server.h
index 0767bb91b7..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.
- Result SendNx2VibrationValues();
+ // 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<Core::HID::VibrationDeviceHandle, BufferAttr_HipcPointer> vibration_handles,
+ InArray<Core::HID::VibrationValue, BufferAttr_HipcPointer> 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.
diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp
index c5907e1b65..342cc725c0 100644
--- a/src/qt_common/config/shared_translation.cpp
+++ b/src/qt_common/config/shared_translation.cpp
@@ -149,6 +149,11 @@ std::unique_ptr<TranslationMap> 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. 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/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 <QSettings>
#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);
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<u8, 4> received{};
const auto [count, error] = socket.Recv(0, received);
REQUIRE(error == Network::Errno::SUCCESS);
- REQUIRE(count == payload.size());
+ REQUIRE(count == static_cast<int>(payload.size()));
REQUIRE(received == payload);
const auto [empty_count, empty_error] = socket.Recv(0, received);
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<std::array<u64, 128>, 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);
}
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/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<P>::SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 si
}
const std::span<BufferCopy> 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<BufferCopy, 4> copies;
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,
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 <jni.h>
#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<Common::DynamicLibrary> 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_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<VkSampler, 2>& samplers)
: Pass(device_, scheduler, descriptor_pool, MakeLayout(kinds, samplers), code) {}
- void Dispatch(Scheduler& scheduler, ComputePassDescriptorQueue& queue,
+ void Dispatch(Scheduler& scheduler_, ComputePassDescriptorQueue& queue,
std::span<const Resource> 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);
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();
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..4fa9e240c4
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp
@@ -0,0 +1,425 @@
+// 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 <array>
+#include <cstdlib>
+#include <string_view>
+
+#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<f32>(render.width) / static_cast<f32>(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<sl::DLSSPreset>(static_cast<u32>(sl::DLSSPreset::ePresetJ) +
+ static_cast<u32>(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<uint32_t>(layout)};
+ resource.width = size.width;
+ resource.height = size.height;
+ resource.nativeFormat = static_cast<uint32_t>(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<int>(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<int>(options.mode),
+ static_cast<int>(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};
+ // 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<PFun_slDLSSSetOptions*>(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<int>(result));
+ }
+ clear_output();
+ 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<int>(result));
+ }
+ clear_output();
+ 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<f32>(in.render_size.width);
+ const f32 rh = static_cast<f32>(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<sl::CommandBuffer*>(*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<int>(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);
+ if (result != sl::Result::eOk) {
+ clear_output();
+ }
+ });
+ // 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 <array>
+#include <atomic>
+
+#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<f32, 2> jitter;
+ /// Turns a motion vector into render pixels (the render size for UV offsets).
+ std::array<f32, 2> 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<bool> 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<f32, std::milli>(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<f32, std::milli>(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=<stops>|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<s32>(extent.width), static_cast<s32>(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<u32, 2> render_size,
+ std::array<f32, 2> jitter,
+ std::array<f32, 2> 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<StreamlineDlssUpscaler>(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<u32>(view.range.base.level),
+ .levelCount = 1,
+ .baseArrayLayer = static_cast<u32>(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<f32>(render_size[0]),
+ motion_scale[1] * static_cast<f32>(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<std::chrono::steady_clock::time_point> 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<u32, 2> render_size, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale, bool reset);
+ std::unique_ptr<StreamlineDlssUpscaler> 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/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 <algorithm>
+#include <cstring>
#include <vector>
#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<T, bool> 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..a34c591aca 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -580,7 +580,7 @@ template <class P>
SamplerId TextureCache<P>::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<P>::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);
@@ -1124,8 +1124,8 @@ bool TextureCache<P>::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..4f8fd796c1
--- /dev/null
+++ b/src/video_core/vulkan_common/streamline.cpp
@@ -0,0 +1,466 @@
+// 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 <algorithm>
+#include <array>
+#include <cstdlib>
+#include <filesystem>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <type_traits>
+#include <vector>
+
+#include <fmt/format.h>
+
+#include "common/fs/fs_util.h"
+#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 <sl_security.h>
+
+#include <softpub.h>
+#include <wintrust.h>
+
+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<u32, 4>;
+
+struct State {
+ HMODULE module{};
+ Api api{};
+ std::wstring plugin_dir;
+ std::wstring model_dir;
+ std::vector<std::string> instance_extensions;
+ std::vector<std::string> 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<std::filesystem::path> SearchDirs() {
+ std::vector<std::filesystem::path> 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 <typename T>
+bool Resolve(HMODULE module, const char* name, T*& function) {
+ function = reinterpret_cast<T*>(GetProcAddress(module, name));
+ if (!function) {
+ LOG_ERROR(Render_Vulkan, "sl.interposer.dll has no {}", name);
+ }
+ return function != nullptr;
+}
+
+std::optional<Version> 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<u8> data(size);
+ VS_FIXEDFILEINFO* info{};
+ UINT length = 0;
+ if (!GetFileVersionInfoW(path.c_str(), 0, size, data.data()) ||
+ !VerQueryValueW(data.data(), L"\\", reinterpret_cast<void**>(&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> 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> 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",
+ Common::FS::PathToUTF8String(files.app_model));
+ 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 {}: {}",
+ Common::FS::PathToUTF8String(target), 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",
+ Common::FS::PathToUTF8String(path));
+ return false;
+ }
+ state.module = LoadLibraryW(path.c_str());
+ if (!state.module) {
+ LOG_ERROR(Render_Vulkan, "Could not load {} (error {})", Common::FS::PathToUTF8String(path),
+ 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<std::string> Names(uint32_t count, const char** names) {
+ std::vector<std::string> 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<DWORD>(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",
+ Common::FS::PathToUTF8String(files.folder));
+ 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",
+ Common::FS::PathToUTF8String(files.folder));
+ }
+ }
+
+ // 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<int>(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<int>(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(),
+ Common::FS::PathToUTF8String(std::filesystem::path{state.plugin_dir}),
+ version.versionNGX.toStr());
+ state.started = true;
+ return true;
+}
+
+bool WantsDevice() {
+ const State& state = GetState();
+ return state.started && !state.attached;
+}
+
+std::vector<std::string> InstanceExtensions() {
+ const State& state = GetState();
+ return state.started ? state.instance_extensions : std::vector<std::string>{};
+}
+
+std::vector<std::string> DeviceExtensions() {
+ const State& state = GetState();
+ return state.started ? state.device_extensions : std::vector<std::string>{};
+}
+
+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<int>(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<int>(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<std::remove_reference_t<decltype(function)>>(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<int>(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 <filesystem>
+#include <string>
+#include <vector>
+
+#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<std::string> InstanceExtensions();
+[[nodiscard]] std::vector<std::string> 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 <windows.h> and Streamline's (MIT-licensed) headers.
+
+#pragma once
+
+#include <windows.h>
+
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+#include <sl.h>
+#include <sl_dlss.h>
+#include <sl_helpers_vk.h>
+
+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..2499f46f87 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 <adrenotools/bcenabler.h>
@@ -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,26 @@ 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<std::string> wanted = Streamline::DeviceExtensions();
+ // 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) {
+ loaded_extensions.insert(name);
+ }
+ names += (names.empty() ? "" : ", ") + name;
+ }
+ streamline_extensions = all;
+ LOG_INFO(Render_Vulkan, "NVIDIA DLSS device extensions: {} ({})",
+ all ? "enabled" : "unavailable", names.empty() ? "none" : 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<std::string, std::less<>> supported_extensions; ///< Reported Vulkan extensions.
std::set<std::string, std::less<>> loaded_extensions; ///< Loaded Vulkan extensions.
+ bool streamline_extensions{}; ///< NVIDIA DLSS's extensions are loaded.
+ bool streamline_attached{}; ///< Streamline has this device.
std::vector<size_t> 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 <algorithm>
+#include <string>
+#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<std::string> 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/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 074b6f405b..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
@@ -226,6 +227,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()) {
@@ -323,6 +330,20 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
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;
hold_graphics.emplace(setting->Id(), widget);
diff --git a/src/yuzu/configuration/nvidia_dlss_panel.cpp b/src/yuzu/configuration/nvidia_dlss_panel.cpp
new file mode 100644
index 0000000000..040ea90557
--- /dev/null
+++ b/src/yuzu/configuration/nvidia_dlss_panel.cpp
@@ -0,0 +1,556 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifdef EGLANTINE_STREAMLINE
+
+#include "common/httplib.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <filesystem>
+#include <fstream>
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include <QApplication>
+#include <QDesktopServices>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QMessageBox>
+#include <QPlainTextEdit>
+#include <QProgressDialog>
+#include <QPushButton>
+#include <QStringList>
+#include <QUrl>
+#include <QVBoxLayout>
+#include <fmt/format.h>
+#include <zlib.h>
+
+#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"
+
+#ifdef YUZU_BUNDLED_OPENSSL
+#include <openssl/cert.h>
+#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<u16>(static_cast<u8>(data[at]) | static_cast<u8>(data[at + 1]) << 8);
+}
+
+u32 U32(const std::string& data, size_t at) {
+ return static_cast<u32>(U16(data, at)) | static_cast<u32>(U16(data, at + 2)) << 16;
+}
+
+u64 U64(const std::string& data, size_t at) {
+ return static_cast<u64>(U32(data, at)) | static_cast<u64>(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/<size>"; 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<std::string> 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<std::string> 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<std::string> 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 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.
+ 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<size_t>(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 && field + 8 <= record_end) {
+ *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<std::string> Read(const ZipEntry& entry,
+ const std::function<bool(u64)>& progress = {}) {
+ const std::optional<std::string> 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<std::string> 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<Bytef*>(data.data());
+ stream.avail_in = static_cast<uInt>(data.size());
+ stream.next_out = reinterpret_cast<Bytef*>(out.data());
+ stream.avail_out = static_cast<uInt>(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<const Bytef*>(out.data()),
+ static_cast<uInt>(out.size()));
+ if (crc != entry.crc) {
+ error = "damaged download (checksum)";
+ return std::nullopt;
+ }
+ return out;
+ }
+
+ bool canceled{};
+ std::string error;
+
+private:
+ std::unique_ptr<httplib::Client> MakeClient() const {
+ auto client = std::make_unique<httplib::Client>(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<std::string> Fetch(const std::string& range,
+ const std::function<bool(u64)>& 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<std::string, ZipEntry> 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("<a href=\"%1\">%2</a>").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("<b>Ready.</b> Found %1 and %2.")
+ .arg(Named(QStringLiteral("NVIDIA Streamline"), files.interposer_version), dlss);
+ } else if (has_model) {
+ status = Tr("<b>One file needed:</b> 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("<b>One file needed:</b> NVIDIA DLSS (nvngx_dlss.dll). Until then, games use "
+ "AMD FSR 3.1.");
+ button = Tr("Download from NVIDIA (31 MB)");
+ } else {
+ status = Tr("<b>NVIDIA's files needed:</b> 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("<span style=\"color:#2ea043\">&#10004;</span>")
+ : QStringLiteral("<span style=\"color:#d29922\">&#9888;</span>"));
+ 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<std::string> 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<std::pair<QString, const char*>> 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<std::string> 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(
+ Common::FS::PathToUTF8String(std::filesystem::path{entry}.filename()));
+ }
+ 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.<br>"
+ "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<const ZipEntry*> 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<std::string> contents = zip.Read(*entries[i], [&](u64 received) {
+ progress.setValue(static_cast<int>((done + received) * 1000 / std::max<u64>(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<std::streamsize>(contents->size()));
+ if (!file) {
+ progress.close();
+ fail("could not write " + Common::FS::PathToUTF8String(partial));
+ 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(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 " + Common::FS::PathToUTF8String(target));
+ return;
+ }
+ LOG_INFO(Frontend, "Downloaded {} from NVIDIA's Streamline {} release",
+ Common::FS::PathToUTF8String(target), 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 <QWidget>
+
+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
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");
}
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;