diff --git a/.github/workflows/vulkan-dlss.yml b/.github/workflows/vulkan-dlss.yml new file mode 100644 index 000000000..f711ac31b --- /dev/null +++ b/.github/workflows/vulkan-dlss.yml @@ -0,0 +1,115 @@ +# NVIDIA DLSS is a local build's option: NVIDIA's SDK is never part of Hookshot or its downloads. This only checks that +# hookshot_vk.dll still compiles and links with it, through llvm-mingw as players build it. It runs by hand, and on a push or +# pull request only when a file DLSS's build depends on changed (the paths below), not on every push. scripts/ngx_sdk.py +# fetches the SDK at its pinned release, SHA-256 checked, without DLSS's runtime: NVIDIA's license is accepted for that run +# alone (--accept-nvidia-license), and nothing of it is uploaded or cached. +name: Vulkan back end with NVIDIA DLSS + +on: + push: + branches: [main, dev] + paths: + - 'gpu/vk/vk_dlss*' + - 'gpu/vk/vk_reflex*' + - 'gpu/vk/vk_ngx_msvc_rt.cpp' + - 'gpu/vk/upscaler_rows.h' + - 'gpu/vk/dlss_math.h' + - 'gpu/vk/shaders/dlss_*' + - 'host/mingw_msvc_shims.c' + - 'nx2/oot_jitter.py' + - 'nx2/build.py' + - 'nx2/make_game.py' + - 'scripts/ngx_sdk.py' + - '.github/workflows/vulkan-dlss.yml' + pull_request: + paths: + - 'gpu/vk/vk_dlss*' + - 'gpu/vk/vk_reflex*' + - 'gpu/vk/vk_ngx_msvc_rt.cpp' + - 'gpu/vk/upscaler_rows.h' + - 'gpu/vk/dlss_math.h' + - 'gpu/vk/shaders/dlss_*' + - 'host/mingw_msvc_shims.c' + - 'nx2/oot_jitter.py' + - 'nx2/build.py' + - 'nx2/make_game.py' + - 'scripts/ngx_sdk.py' + - '.github/workflows/vulkan-dlss.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +defaults: + run: + shell: bash + +jobs: + vulkan-dlss: + name: Windows Vulkan back end with NVIDIA DLSS (llvm-mingw, link check) + runs-on: windows-2022 + timeout-minutes: 45 + steps: + - name: Check out source + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Check out pinned Vulkan headers + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: KhronosGroup/Vulkan-Headers + ref: 74d8a6cb930c68ef617b202c3ff3c59d919e086b # v1.4.345 + path: _deps/cpm/vulkanheaders/v1.4.345 + persist-credentials: false + - name: Check out pinned SPIR-V headers + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: KhronosGroup/SPIRV-Headers + ref: 04f10f650d514df88b76d25e83db360142c7b174 # vulkan-sdk-1.4.341.0 + path: _deps/cpm/spirv-headers/vulkan-sdk-1.4.341.0 + persist-credentials: false + - name: Check out pinned VulkanMemoryAllocator + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator + ref: 1d8f600fd424278486eade7ed3e877c99f0846b1 # v3.3.0 + path: _deps/cpm/vulkanmemoryallocator/v3.3.0 + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.12' + - name: llvm-mingw version + id: llvm + run: echo "name=$(python -I scripts/release_parts.py llvm-mingw --name)" >> "$GITHUB_OUTPUT" + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ runner.temp }}/llvm + key: ${{ steps.llvm.outputs.name }} + - name: llvm-mingw + run: echo "HOOKSHOT_MINGW=$(python -I scripts/release_parts.py llvm-mingw "$RUNNER_TEMP/llvm")" >> "$GITHUB_ENV" + - name: NVIDIA's DLSS SDK (headers and nvsdk_ngx_s.lib) + run: python -I scripts/ngx_sdk.py --platform windows --build-only --accept-nvidia-license "$RUNNER_TEMP/ngx-sdk" + - name: hookshot_vk.dll with NVIDIA DLSS + env: + RX_VK_ONLY: '1' + RX_RELEASE: '1' + run: | + OOT_WORK="$RUNNER_TEMP/vk-work" RX_CPM="$GITHUB_WORKSPACE/_deps/cpm" HOOKSHOT_NGX_SDK="$RUNNER_TEMP/ngx-sdk" \ + python -I nx2/build.py --standalone --jobs "$NUMBER_OF_PROCESSORS" | tee "$RUNNER_TEMP/vk-build.log" + test "${PIPESTATUS[0]}" -eq 0 + grep -q "with NVIDIA DLSS" "$RUNNER_TEMP/vk-build.log" + # (what it loads is only Windows' own: the system DLLs and the UCRT's api-ms-win-crt-*. NVIDIA's runtime, nvngx_dlss.dll, + # is loaded by NGX at run time from the game folder's ngx folder, and its core, _nvngx.dll, from the driver's) + - name: Imports + run: | + dll="$RUNNER_TEMP/vk-work/sa/build/hookshot_vk.dll" + "$HOOKSHOT_MINGW/bin/llvm-objdump" -p "$dll" | sed -n 's/^ *DLL Name: //p' | tr -d '\r' | sort -u | tee "$RUNNER_TEMP/imports.txt" + if grep -viE '^(kernel32|user32|gdi32|advapi32)\.dll$|^api-ms-win-crt-[a-z0-9-]+\.dll$' "$RUNNER_TEMP/imports.txt"; then + echo "::error::hookshot_vk.dll with NVIDIA DLSS imports a DLL that is not Windows' own (above)" + exit 1 + fi diff --git a/.gitignore b/.gitignore index 67c26d131..736ed9600 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ __pycache__/ # ---- Linux builds ---- build-linux/ *.so + +# ---- NVIDIA's DLSS SDK (scripts/ngx_sdk.py): NVIDIA's files, never committed ---- +ngx-sdk/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 822ec4023..eef9a5914 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,6 +88,32 @@ scripts/linux/appimage.sh # the AppImage, in build-linux/ The AppImage needs glibc 2.35 or newer and a Vulkan 1.3 driver with mesh shaders. `HOOKSHOT_JOBS` sets how many files compile at once. The README's [Linux section](readme.md#on-linux) has the details and the runtime paths. +### NVIDIA DLSS (optional) + +DLSS (`gpu/vk/vk_dlss.cpp`) is built in only with NVIDIA's DLSS SDK, which is NVIDIA's, under NVIDIA's own license, and +is never committed, bundled or uploaded: not in a PR, not in a release, not in a CI artifact. Fetch it for your own +builds with `scripts/ngx_sdk.py` (it prints NVIDIA's license notice and needs `--accept-nvidia-license` or a typed yes), +then build with `HOOKSHOT_NGX_SDK=`: + +```powershell +python -I scripts/ngx_sdk.py --platform windows C:\path\ngx-sdk # headers, nvsdk_ngx_s.lib, nvngx_dlss.dll +$env:HOOKSHOT_NGX_SDK = "C:\path\ngx-sdk" +python -I nx2/make_game.py --nup "C:\path\your-game.nup" --out "C:\Games\Hookshot-dlss" +``` + +On Windows the llvm-mingw build links NVIDIA's static `nvsdk_ngx_s.lib` (built for Microsoft's static runtime) into +`hookshot_vk.dll`, with `gpu/vk/vk_ngx_msvc_rt.cpp` and `host/mingw_msvc_shims.c` for what it needs of that runtime. +`make_game.py` copies `nvngx_dlss.dll` into the game folder's `ngx` folder; never put NGX's core, `_nvngx.dll`, beside +the game (it is the driver's). On Linux it is `--platform linux` (or `scripts/linux/ngx-sdk.sh`), with `libnvsdk_ngx.a` +and `libnvidia-ngx-dlss.so`. A Windows game with DLSS can be built on Linux too, with `RX_TARGET=windows` as below. +CI's `vulkan-dlss` workflow (`.github/workflows/vulkan-dlss.yml`) checks only that `hookshot_vk.dll` still links with the +SDK. It runs by hand, and on a push or pull request only when a file DLSS's build depends on changes; it accepts NVIDIA's +license for that run alone and uploads nothing. + +Never share a game folder built with the SDK. A change to the pinned release updates the file table and SHA-256s in +`scripts/ngx_sdk.py` (and `tests/test_ngx_sdk.py`), and the release named in the builder's license text +(`builder/HookshotBuilder/MainWindow.xaml.cs`). + ### Hookshot Builder The builder is a .NET 8 WPF app. Open `builder/HookshotBuilder.sln` in Visual Studio 2022 with the .NET desktop @@ -219,6 +245,9 @@ with an incompatible license, and do not paste decompiled game code or anything The third-party parts (Eden, suyu, Sirit, the Vulkan Memory Allocator, SDL, Opus, stb, zbic and the a64-kit tools) keep their own notices. Preserve them, and do not relicense their code. +NVIDIA's DLSS SDK is not one of them: it is never in the repository, and nothing of it may be added (see +[NVIDIA DLSS](#nvidia-dlss-optional)). + ## Where to talk - **Issues:** [use the forms](https://github.com/NotRexed/Hookshot/issues/new/choose). diff --git a/builder/HookshotBuilder/MainWindow.xaml b/builder/HookshotBuilder/MainWindow.xaml index e358b23db..3d281e968 100644 --- a/builder/HookshotBuilder/MainWindow.xaml +++ b/builder/HookshotBuilder/MainWindow.xaml @@ -101,6 +101,7 @@ + @@ -173,6 +174,15 @@ + + + + + + diff --git a/builder/HookshotBuilder/MainWindow.xaml.cs b/builder/HookshotBuilder/MainWindow.xaml.cs index ad0b23c9b..3bb36b41f 100644 --- a/builder/HookshotBuilder/MainWindow.xaml.cs +++ b/builder/HookshotBuilder/MainWindow.xaml.cs @@ -259,6 +259,7 @@ sealed class BuilderSettings public string? Nup { get; set; } public string? Romfs { get; set; } public string? Exefs { get; set; } + public bool Dlss { get; set; } // (NVIDIA DLSS ticked: NVIDIA's license was accepted) } static string SettingsPath => System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "HookshotBuilder", "builder-settings.json"); @@ -269,6 +270,7 @@ void ApplySettings() try { s = System.Text.Json.JsonSerializer.Deserialize(File.ReadAllText(SettingsPath)); } catch (Exception) { } if (s == null) return; if (!string.IsNullOrEmpty(s.Out)) OutPath.Text = s.Out; + DlssTick.IsChecked = s.Dlss; if (s.Romfs != null && s.Exefs != null && Directory.Exists(s.Romfs) && File.Exists(System.IO.Path.Combine(s.Exefs, "main"))) { romfs = s.Romfs; @@ -290,7 +292,8 @@ void SaveSettings() { try { - var s = new BuilderSettings { Out = OutPath.Text, Nup = romfs == null ? chosenNup : null, Romfs = romfs, Exefs = exefs }; + var s = new BuilderSettings { Out = OutPath.Text, Nup = romfs == null ? chosenNup : null, Romfs = romfs, Exefs = exefs, + Dlss = DlssTick.IsChecked == true }; Directory.CreateDirectory(System.IO.Path.GetDirectoryName(SettingsPath)!); File.WriteAllText(SettingsPath, System.Text.Json.JsonSerializer.Serialize(s)); } @@ -689,7 +692,24 @@ async void StartBuild(object sender, RoutedEventArgs e) else if (chosenNup != null) args.AddRange(new[] { "--nup", chosenNup }); else return; args.AddRange(new[] { "--out", OutPath.Text, "--language", lang.Code }); - Run(tools, args); + Run(tools, args, dlss: DlssTick.IsChecked == true); + } + + const string DlssLicense = + "NVIDIA DLSS is NVIDIA's, not Hookshot's. With it ticked, the build downloads NVIDIA's DLSS SDK (release v310.9.16) from " + + "github.com/NVIDIA/DLSS, under NVIDIA's own license, the NVIDIA RTX SDKs license:\n\n" + + "https://github.com/NVIDIA/DLSS/blob/9981c6350dc58c0ce21f024fd65e31e851c31689/LICENSE.txt\n\n" + + "Its files stay on this PC (in the builder's ngx-sdk folder, and nvngx_dlss.dll in the game folder's ngx folder). " + + "Hookshot never includes or shares them: never share a game folder built with them.\n\n" + + "Do you accept NVIDIA's license?"; + + /// The NVIDIA DLSS tick: NVIDIA's license first, and the tick stays only when it is accepted + void DlssChanged(object sender, RoutedEventArgs e) + { + if (DlssTick.IsChecked == true && + MessageBox.Show(this, DlssLicense, "NVIDIA DLSS", MessageBoxButton.YesNo, MessageBoxImage.Information) != MessageBoxResult.Yes) + DlssTick.IsChecked = false; + SaveSettings(); // (kept at once, not only when Build is pressed: ticked and closed, it is still ticked next time) } static readonly (string Id, string Build, string Update)[] Stages = @@ -719,7 +739,8 @@ void MakeRows(bool update) if (update) rows["read"].Small("Your game files are kept: the .nup isn't needed"); } - void Run(Tools tools, List args, bool keepRows = false) + /// dlss: NVIDIA DLSS, its license accepted: NVIDIA's SDK fetched into Storage.NgxSdk first (only what is missing), and built in + void Run(Tools tools, List args, bool keepRows = false, bool dlss = false) { int? workers; try { workers = BuildWorkers.Selected(WorkerBox.SelectedItem); } @@ -756,7 +777,8 @@ void Run(Tools tools, List args, bool keepRows = false) clock.Start(); Tick(); if (Storage.WorkChosen && !args.Contains("--work")) args.AddRange(new[] { "--work", Storage.Work }); // (the build files where the player chose) - runner = new Runner(tools, args, workers); + if (dlss && !args.Contains("--fetch-ngx-sdk")) args.AddRange(new[] { "--fetch-ngx-sdk", "--accept-nvidia-license" }); + runner = new Runner(tools, args, workers, dlss ? Storage.NgxSdk : null); runner.Line += l => Dispatcher.BeginInvoke(() => Line(l)); runner.Finished += failure => Dispatcher.BeginInvoke(() => Ended(failure)); runner.Start(); @@ -1146,7 +1168,10 @@ internal void UpdateBuild(Tools tools, string folder) { download = null; if (rows.TryGetValue("download", out var d)) { d.Done(); d.Small("Build tools checked"); } - Run(tools, new List { "--update", folder }, keepRows: true); + // (a folder built with NVIDIA DLSS, its license accepted then, is built with it again) + var dlss = File.Exists(System.IO.Path.Combine(folder, "ngx", "nvngx_dlss.dll")) && + File.Exists(System.IO.Path.Combine(Storage.NgxSdk, "LICENSE.txt")); + Run(tools, new List { "--update", folder }, keepRows: true, dlss: dlss); } // ================= the window ================= diff --git a/builder/HookshotBuilder/Runner.cs b/builder/HookshotBuilder/Runner.cs index bf6ba4849..6522fab20 100644 --- a/builder/HookshotBuilder/Runner.cs +++ b/builder/HookshotBuilder/Runner.cs @@ -60,6 +60,7 @@ sealed class Runner readonly Tools tools; readonly List args; readonly int? workers; + readonly string? ngxSdk; Process? process; string? error; bool cancelled; @@ -67,15 +68,17 @@ sealed class Runner public event Action? Line; public event Action? Finished; - public Runner(Tools tools, List args, int? workers = null) + /// ngxSdk: NVIDIA DLSS (the builder's tick): the folder of NVIDIA's DLSS SDK, as HOOKSHOT_NGX_SDK; null builds without it + public Runner(Tools tools, List args, int? workers = null, string? ngxSdk = null) { this.tools = tools; this.args = args; if (workers is <= 0) throw new ArgumentOutOfRangeException(nameof(workers), "Build workers must be positive."); this.workers = workers; + this.ngxSdk = ngxSdk; } - static ProcessStartInfo Info(Tools tools, IEnumerable args, int? workers = null) + static ProcessStartInfo Info(Tools tools, IEnumerable args, int? workers = null, string? ngxSdk = null) { var si = new ProcessStartInfo(tools.Python) { @@ -89,6 +92,9 @@ static ProcessStartInfo Info(Tools tools, IEnumerable args, int? workers BuildWorkers.Apply(si, workers); // (written into the game folder's hookshot_version.txt: the game starts this app again to update, host/hos_update.c) if (Environment.ProcessPath != null) si.Environment["HOOKSHOT_BUILDER_EXE"] = Environment.ProcessPath; + // (NVIDIA DLSS: only when ticked. Without it, one set on the PC is not passed on either: a build is with DLSS only when asked) + if (ngxSdk != null) si.Environment["HOOKSHOT_NGX_SDK"] = ngxSdk; + else si.Environment.Remove("HOOKSHOT_NGX_SDK"); // (the folder's own compiler, headers and libraries, when it has them: nothing of the PC's but Windows' own) var sets = new[] { ("EMSDK", "emsdk"), ("RX_CPM", "cpm"), ("WGPU_NATIVE", "wgpu-native") }; foreach (var (name, folder) in sets) @@ -109,7 +115,7 @@ public void Start() { try { - using var p = Process.Start(Info(tools, args, workers))!; + using var p = Process.Start(Info(tools, args, workers, ngxSdk))!; process = p; p.ErrorDataReceived += (_, e) => { if (e.Data != null) Line?.Invoke(e.Data); }; p.BeginErrorReadLine(); diff --git a/builder/HookshotBuilder/Storage.cs b/builder/HookshotBuilder/Storage.cs index f55bf9624..66d1f7578 100644 --- a/builder/HookshotBuilder/Storage.cs +++ b/builder/HookshotBuilder/Storage.cs @@ -36,6 +36,10 @@ public static string Work public static bool WorkChosen => !Same(Work, Base); + /// (NVIDIA DLSS, only when ticked) NVIDIA's DLSS SDK, downloaded under NVIDIA's license by the build (make_game.py + /// --fetch-ngx-sdk, scripts/ngx_sdk.py): HOOKSHOT_NGX_SDK. NVIDIA's files, never part of the builder or its downloads + public static string NgxSdk => Path.Combine(Base, "ngx-sdk"); + /// The build files go to a folder of their own in the one the player picked (null: back to the builder's own). What the last /// builds left at the old place goes: the next build would not look for it there public static void MoveWork(string? picked) diff --git a/gpu/vk/dlss_actor.h b/gpu/vk/dlss_actor.h new file mode 100644 index 000000000..d9a36d987 --- /dev/null +++ b/gpu/vk/dlss_actor.h @@ -0,0 +1,56 @@ +// (hookshot) NVIDIA DLSS's actor motion (vk_dlss.cpp): what is worked out on the CPU for a skinned draw drawn again, kept +// apart so tests/test_dlss_actor.cpp checks it without a GPU. Header only, no Vulkan. +#pragma once + +#include +#include +#include +#include + +namespace dlss_actor { + +/// A draw's name across frames: its model (vertex and index buffers, count, base vertex, program) and which of the frame's +/// draws of that model it is (the same model drawn twice: the n-th draw of it is matched with the n-th the frame before) +inline uint64_t key(uint64_t vbuf, uint64_t ibuf, int count, int base_vertex, uint64_t program, int nth) { + uint64_t h = 0x5bd1e995; + for (uint64_t v : {vbuf, ibuf, (uint64_t)(uint32_t)count, (uint64_t)(uint32_t)base_vertex, program}) { + h ^= v + 0x9e3779b97f4a7c15ull + (h << 6) + (h >> 2); + h *= 0xff51afd7ed558ccdull; + } + return (h & ~0xffull) | (uint64_t)std::clamp(nth, 0, 255); +} +inline bool same_model(uint64_t a, uint64_t b) { return a >> 8 == b >> 8; } + +/// shaders/dlss_actor.vert's push constants (the fragment stage's the same) +struct Push { + uint64_t vertices; // (the vertex stream's device address) + uint64_t indices; // (the index buffer's, 4-aligned down: first says where the draw's indices start in it) + uint64_t origins[2]; // (the draw's origin, xyz: this frame's and the frame before's, copied into the pose buffers) + uint64_t bones[2]; // (its palettes: 3 x 4 row-major matrices, 12 floats a bone) + uint32_t nbones; + uint32_t stride; // (in words) + uint32_t count; // (the vertices in the stream: an index past them draws nothing) + uint32_t first; // (in indices, from the address) + uint32_t itype; // (0: 8-bit, 1: 16-bit, 2: 32-bit indices) + int32_t base_vertex; + uint32_t pos, sets; // (bytes in a vertex: the position; the joint and weight sets, 1 or 2) + uint32_t joints[2], weights[2]; + uint32_t formats; // (kHalfPosition, kByteJoints << set) + uint32_t tolerance; // (depth steps from the scene's within which a pixel is the draw's) +}; +static_assert(sizeof(Push) == 6 * 8 + 14 * 4 && sizeof(Push) <= 128, "dlss_actor.vert's push constants"); + +/// Where a draw's indices are for the shader: the address 4-aligned down, and its first index from there +inline void index_start(uint64_t address, int itype, uint64_t* aligned, uint32_t* first) { + *aligned = address & ~3ull; + *first = (uint32_t)((address & 3) >> (itype < 0 ? 0 : itype > 2 ? 2 : itype)); +} + +/// Push::formats: the position is RGBA16F (else RGB32F); a set's joints are RGBA8UI (else RGBA16UI) +constexpr uint32_t kHalfPosition = 1, kByteJoints = 2; + +/// The cameras' buffer, 36 words: this frame's camera (as drawn, jittered), the frame before's (its jitter taken off), this +/// frame's jitter in clip space, the render size +constexpr uint32_t kHeadWords = 36; + +} // namespace dlss_actor diff --git a/gpu/vk/dlss_math.h b/gpu/vk/dlss_math.h new file mode 100644 index 000000000..24ea32095 --- /dev/null +++ b/gpu/vk/dlss_math.h @@ -0,0 +1,103 @@ +/* (hookshot) NVIDIA DLSS's camera math, as plain functions: the jitter's sequence (host/rx_backend.c, in C) and the motion + vectors' reprojection (gpu/vk/vk_dlss.cpp). No Vulkan, no NGX: tests/test_dlss_math.cpp checks them. */ +#pragma once + +#ifdef __cplusplus +#include +#include +#endif + +/* The radical inverse of index in base: the Halton sequence's index-th value, in [0, 1) */ +static inline float dlss_halton(int index, int base) { + float f = 1.0f, r = 0.0f; + for (int i = index; i > 0; i /= base) { + f /= (float)base; + r += f * (float)(i % base); + } + return r; +} + +/* The frame's sub-pixel jitter, in pixels, each in [-0.5, 0.5): the Halton (2, 3) sequence over `phases` frames (from 1: its + first value, 0, would put every phase's start on the pixel's corner) */ +static inline void dlss_jitter_pixels(unsigned frame, int phases, float out[2]) { + const int i = (int)(frame % (unsigned)(phases > 0 ? phases : 1)) + 1; + out[0] = dlss_halton(i, 2) - 0.5f; + out[1] = dlss_halton(i, 3) - 0.5f; +} + +/* The frame's jitter for a projection matrix p (row major, as the game fills its views' uniform buffers: nx2/oot_jitter.py), + in clip space (what p[0][2] and p[1][2] get) into out, for a scene of w x h pixels; 0 (none) when p is not one of the + picture's: not perspective (its w row is not 0 0 -1 0: a shadow map's, the HUD's) or not of 16:9 or the window's ratio + wide (0: 16:9 only), or when there is no scene size (DLSS not drawing) */ +static inline int dlss_jitter_clip(const float p[16], float wide, unsigned frame, int phases, float w, float h, float out[2]) { + if (!(w >= 1.0f) || !(h >= 1.0f)) return 0; + if (p[12] != 0.0f || p[13] != 0.0f || p[14] != -1.0f || p[15] != 0.0f || !(p[0] > 0.0f)) return 0; + const float r = p[5] / p[0], d = r - 16.0f / 9.0f, e = wide > 0.0f ? r - wide : 1.0f; + if (!((d <= 0.01f && d >= -0.01f) || (e <= 0.01f && e >= -0.01f))) return 0; + float px[2]; + dlss_jitter_pixels(frame, phases, px); + out[0] = 2.0f * px[0] / w, out[1] = 2.0f * px[1] / h; + return 1; +} + +/* How many frames the jitter's sequence takes: NVIDIA's guide, 8 times the share of pixels DLSS fills in, (output / render) + squared, so each output pixel is covered about 8 times; 8 at least (DLAA), 64 at most (a long sequence is seen less of + before the camera moves) */ +static inline int dlss_jitter_phases(unsigned render_w, unsigned render_h, unsigned out_w, unsigned out_h) { + if (!render_w || !render_h) return 8; + const double area = (double)out_w * (double)out_h / ((double)render_w * (double)render_h); + const int n = (int)(8.0 * area + 0.5); + return n < 8 ? 8 : n > 64 ? 64 : n; +} + +#ifdef __cplusplus +namespace mkwvk::dlss_math { + +/// This frame's clip space to the frame before's: previous * inverse(current), both row major (clip = M * position); false +/// when current is singular or the result is not finite +inline bool reprojection(const float* current, const float* previous, float out[16]) { + double a[4][8]; + for (int r = 0; r < 4; r++) + for (int c = 0; c < 8; c++) a[r][c] = c < 4 ? current[r * 4 + c] : (c - 4 == r ? 1.0 : 0.0); + for (int c = 0; c < 4; c++) { // (Gauss-Jordan with partial pivoting) + int p = c; + for (int r = c + 1; r < 4; r++) + if (std::fabs(a[r][c]) > std::fabs(a[p][c])) p = r; + if (std::fabs(a[p][c]) < 1e-12) return false; + if (p != c) + for (int k = 0; k < 8; k++) std::swap(a[p][k], a[c][k]); + const double inv = 1.0 / a[c][c]; + for (int k = 0; k < 8; k++) a[c][k] *= inv; + for (int r = 0; r < 4; r++) { + if (r == c || a[r][c] == 0.0) continue; + const double f = a[r][c]; + for (int k = 0; k < 8; k++) a[r][k] -= f * a[c][k]; + } + } + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) { + double v = 0; + for (int k = 0; k < 4; k++) v += previous[r * 4 + k] * a[k][c + 4]; + if (!std::isfinite(v)) return false; + out[r * 4 + c] = (float)v; + } + return true; +} + +/// A view-projection's jitter taken off (row major): a jitter moves clip x and y by jitter * w, so jitter times the w row +/// comes off the first two rows +inline void unjitter(float m[16], const float jitter[2]) { + for (int c = 0; c < 4; c++) { + m[0 * 4 + c] -= jitter[0] * m[3 * 4 + c]; + m[1 * 4 + c] -= jitter[1] * m[3 * 4 + c]; + } +} + +/// A jitter in clip space as DLSS takes it: in render pixels, x right and y down (clip y is up) +inline void jitter_to_pixels(const float clip[2], unsigned width, unsigned height, float out[2]) { + out[0] = clip[0] * 0.5f * (float)width; + out[1] = -clip[1] * 0.5f * (float)height; +} + +} // namespace mkwvk::dlss_math +#endif diff --git a/gpu/vk/menu_restart.h b/gpu/vk/menu_restart.h new file mode 100644 index 000000000..951eee0c4 --- /dev/null +++ b/gpu/vk/menu_restart.h @@ -0,0 +1,70 @@ +/* The Insert menu's rows that take a restart (vk_menu.cpp: GPU device, Output resolution, Handheld profile, Ultrawide, + Robust buffers), as plain code: which of them differ from what the game started with, whether closing the menu asks to + restart, and the rows' names as the menu lists them. No Vulkan, no menu: tests/test_menu_restart.cpp checks it. */ +#pragma once + +#include + +namespace mkwvk::menu_restart { + +constexpr int kMax = 8; // (rows that take a restart: the menu has five) + +struct State { + int n = 0; + int start[kMax] = {}; // each row's value at the start: what runs + int declined[kMax] = {}; // the values when the player last chose Later + bool later = false; // Later was chosen, and nothing has changed since + + /// A row that takes a restart, with the value the game started with: its slot + int add(int start_value) { + if (n >= kMax) return -1; + start[n] = start_value; + return n++; + } + /// The slot's start value again (the GPU row's, once the cards are counted) + void restart_value(int slot, int start_value) { + if (slot >= 0 && slot < n) start[slot] = start_value; + } + /// The rows (a bit a slot) whose value now differs from the start's: a restart is pending while it is not 0. A row + /// changed back to its start value is no longer pending + unsigned pending(const int* now) const { + unsigned m = 0; + for (int i = 0; i < n; i++) + if (now[i] != start[i]) m |= 1u << i; + return m; + } + /// After any change of a row: a value other than the ones Later was chosen for asks again at the next close (even if + /// it is changed back to them later) + void note(const int* now) { + if (!later) return; + for (int i = 0; i < n; i++) + if (now[i] != declined[i]) later = false; + } + /// Closing the menu: ask to restart when something is pending, unless Later was chosen for the same values + bool ask(const int* now) { + note(now); + return pending(now) && !later; + } + /// Later: the settings stay saved for the next start, and the same choice is not asked about again + void choose_later(const int* now) { + later = true; + for (int i = 0; i < n; i++) declined[i] = now[i]; + } +}; + +/// The names of the rows in mask (a bit a slot, names[slot]), in slot order: "A, B" with sep ", ", the last one after +/// last ("A, B and C" with last " and ") +inline std::string names(const char* const* names, int n, unsigned mask, const char* sep = ", ", const char* last = ", ") { + std::string s; + int left = 0; + for (int i = 0; i < n; i++) left += (mask >> i) & 1; + for (int i = 0; i < n; i++) { + if (!((mask >> i) & 1)) continue; + if (!s.empty()) s += left == 1 ? last : sep; + s += names[i]; + left--; + } + return s; +} + +} // namespace mkwvk::menu_restart diff --git a/gpu/vk/shaders/dlss_actor.frag b/gpu/vk/shaders/dlss_actor.frag new file mode 100644 index 000000000..bb64abbcd --- /dev/null +++ b/gpu/vk/shaders/dlss_actor.frag @@ -0,0 +1,38 @@ +#version 450 +#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require +// (hookshot) NVIDIA DLSS's actor motion (dlss_actor.vert): where the draw is what the scene shows (its depth within +// pc.tolerance steps of the scene's), the motion from where it is now (the pixel, the jitter taken off) to where it was, in +// render pixels as shaders/dlss_prep.comp makes them +layout(std430, binding = 0) readonly buffer Data { uint w[]; } data; +layout(binding = 1) uniform sampler2D scene_depth; +layout(push_constant) uniform P { + uint64_t vertices; // (the vertex stream's address) + uint64_t indices; // (the index buffer's address, 4-aligned down; first: the draw's first index in it) + uint64_t origins[2]; // (the draw's origin, xyz: this frame's and the frame before's) + uint64_t bones[2]; // (its palettes: this frame's, the frame before's) + uint nbones; + uint stride; // (in words) + uint count; // (vertices in the stream: an index past them draws nothing) + uint first; + uint itype; // (0: 8-bit, 1: 16-bit, 2: 32-bit indices) + int base_vertex; + uint pos, sets; // (byte offsets in a vertex, the joint and weight sets: 1 or 2) + uint joints[2], weights[2]; + uint formats; // (1: the position RGBA16F, else RGB32F; 2 << set: that set's joints RGBA8UI, else RGBA16UI) + uint tolerance; // (the fragment stage's) +} pc; +layout(location = 0) in vec4 before; +layout(location = 0) out vec2 motion; + +void main() { + const uint scene = floatBitsToUint(texelFetch(scene_depth, ivec2(gl_FragCoord.xy), 0).r); + const uint here = floatBitsToUint(gl_FragCoord.z); + if (max(scene, here) - min(scene, here) > pc.tolerance || before.w <= 1e-6) discard; + const vec2 size = vec2(uintBitsToFloat(data.w[34]), uintBitsToFloat(data.w[35])); + const vec2 jitter = vec2(uintBitsToFloat(data.w[32]), uintBitsToFloat(data.w[33])); + const vec2 now = vec2(gl_FragCoord.x / size.x * 2.0 - 1.0, 1.0 - gl_FragCoord.y / size.y * 2.0) - jitter; + const vec2 then = before.xy / before.w; + const vec2 px = vec2(then.x - now.x, now.y - then.y) * 0.5 * size; + if (!all(lessThan(abs(px), vec2(16384.0)))) discard; + motion = px; +} diff --git a/gpu/vk/shaders/dlss_actor.vert b/gpu/vk/shaders/dlss_actor.vert new file mode 100644 index 000000000..1e899de79 --- /dev/null +++ b/gpu/vk/shaders/dlss_actor.vert @@ -0,0 +1,77 @@ +#version 450 +#extension GL_EXT_buffer_reference : require +#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require +// (hookshot) NVIDIA DLSS's actor motion (vk_dlss.cpp): a skinned draw of the scene drawn again, its vertices skinned with this +// frame's bones and with the frame before's, so each pixel it covers gets its own motion and not that of what is behind it. +// The cameras are in a storage buffer (data): this frame's view-projection as the game drew with it, the jitter in it; the frame +// before's with its jitter taken off (both row major, clip = M * position); the jitter; the render size. The vertices and +// indices are read where the game keeps them, the poses (bone palettes of 3 x 4 row-major matrices, origins) where vk_dlss.cpp +// copied them, by their device addresses. The game skins in model space and adds the object's origin: +// world = sum(w * B[j] * p) + origin. +layout(std430, binding = 0) readonly buffer Data { uint w[]; } data; +layout(buffer_reference, std430, buffer_reference_align = 4) readonly buffer Words { uint w[]; }; +layout(push_constant) uniform P { + uint64_t vertices; // (the vertex stream's address) + uint64_t indices; // (the index buffer's address, 4-aligned down; first: the draw's first index in it) + uint64_t origins[2]; // (the draw's origin, xyz: this frame's and the frame before's) + uint64_t bones[2]; // (its palettes: this frame's, the frame before's) + uint nbones; + uint stride; // (in words) + uint count; // (vertices in the stream: an index past them draws nothing) + uint first; + uint itype; // (0: 8-bit, 1: 16-bit, 2: 32-bit indices) + int base_vertex; + uint pos, sets; // (byte offsets in a vertex, the joint and weight sets: 1 or 2) + uint joints[2], weights[2]; + uint formats; // (1: the position RGBA16F, else RGB32F; 2 << set: that set's joints RGBA8UI, else RGBA16UI) + uint tolerance; // (the fragment stage's) +} pc; +layout(location = 0) out vec4 before; + +float f(uint i) { return uintBitsToFloat(data.w[i]); } +vec4 f4(uint i) { return vec4(f(i), f(i + 1), f(i + 2), f(i + 3)); } +mat4 camera(uint at) { // (row major in the buffer) + return transpose(mat4(f4(at), f4(at + 4), f4(at + 8), f4(at + 12))); +} +vec4 row(Words m, uint i) { return uintBitsToFloat(uvec4(m.w[i], m.w[i + 1u], m.w[i + 2u], m.w[i + 3u])); } +void skin(Words palette, uvec4 j, vec4 wt, vec4 p, inout vec3 acc) { + for (int k = 0; k < 4; k++) { + if (wt[k] <= 0.0 || j[k] >= pc.nbones) continue; + const uint b = j[k] * 12u; + acc += wt[k] * vec3(dot(row(palette, b), p), dot(row(palette, b + 4u), p), dot(row(palette, b + 8u), p)); + } +} +void main() { + const uint at = pc.first + uint(gl_VertexIndex); + Words ib = Words(pc.indices); + uint index; + if (pc.itype == 2u) index = ib.w[at]; + else if (pc.itype == 1u) index = (ib.w[at >> 1] >> ((at & 1u) * 16u)) & 0xffffu; + else index = (ib.w[at >> 2] >> ((at & 3u) * 8u)) & 0xffu; + const int vi = pc.base_vertex + int(index); + if (vi < 0 || uint(vi) >= pc.count) { // (outside the stream: nothing, its triangle dropped) + gl_Position = vec4(0.0, 0.0, 0.0, -1.0); + before = vec4(0.0); + return; + } + Words vb = Words(pc.vertices + uint64_t(uint(vi)) * uint64_t(pc.stride) * 4ul); + const uint q = pc.pos / 4u; + const vec4 p = (pc.formats & 1u) != 0u ? vec4(unpackHalf2x16(vb.w[q]), unpackHalf2x16(vb.w[q + 1u]).x, 1.0) + : vec4(uintBitsToFloat(vb.w[q]), uintBitsToFloat(vb.w[q + 1u]), + uintBitsToFloat(vb.w[q + 2u]), 1.0); + vec3 now = vec3(0.0), then = vec3(0.0); + for (uint s = 0u; s < pc.sets; s++) { + const uint a = vb.w[pc.joints[s] / 4u]; + const uvec4 j = (pc.formats & (2u << s)) != 0u ? (uvec4(a, a >> 8, a >> 16, a >> 24) & 0xffu) + : uvec4(a & 0xffffu, a >> 16, vb.w[pc.joints[s] / 4u + 1u] & 0xffffu, + vb.w[pc.joints[s] / 4u + 1u] >> 16); + const vec4 wt = unpackUnorm4x8(vb.w[pc.weights[s] / 4u]); + skin(Words(pc.bones[0]), j, wt, p, now); + skin(Words(pc.bones[1]), j, wt, p, then); + } + const vec3 o0 = row(Words(pc.origins[0]), 0u).xyz, o1 = row(Words(pc.origins[1]), 0u).xyz; + const vec4 clip = camera(0u) * vec4(now + o0, 1.0); + before = camera(16u) * vec4(then + o1, 1.0); + // (the game's clip space: y up, depth -1 to 1; Vulkan's: y down, depth 0 to 1) + gl_Position = vec4(clip.x, -clip.y, (clip.z + clip.w) * 0.5, clip.w); +} diff --git a/gpu/vk/shaders/dlss_actor_frag_spv.h b/gpu/vk/shaders/dlss_actor_frag_spv.h new file mode 100644 index 000000000..0715d9d8a --- /dev/null +++ b/gpu/vk/shaders/dlss_actor_frag_spv.h @@ -0,0 +1,137 @@ + // 1116.6.0 + #pragma once +const uint32_t DLSS_ACTOR_FRAG_SPV[] = { + 0x07230203,0x00010600,0x0008000b,0x00000099,0x00000000,0x00020011,0x00000001,0x00020011, + 0x0000000b,0x00020011,0x00001503,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e, + 0x00000000,0x0003000e,0x00000000,0x00000001,0x000b000f,0x00000004,0x00000004,0x6e69616d, + 0x00000000,0x0000000d,0x00000011,0x00000033,0x0000003c,0x0000004a,0x00000097,0x00030010, + 0x00000004,0x00000007,0x00030003,0x00000002,0x000001c2,0x000d0004,0x455f4c47,0x735f5458, + 0x65646168,0x78655f72,0x63696c70,0x615f7469,0x68746972,0x6974656d,0x79745f63,0x5f736570, + 0x36746e69,0x00000034,0x00040005,0x00000004,0x6e69616d,0x00000000,0x00040005,0x00000008, + 0x6e656373,0x00000065,0x00050005,0x0000000d,0x6e656373,0x65645f65,0x00687470,0x00060005, + 0x00000011,0x465f6c67,0x43676172,0x64726f6f,0x00000000,0x00040005,0x0000001e,0x65726568, + 0x00000000,0x00030005,0x00000031,0x00000050,0x00060006,0x00000031,0x00000000,0x74726576, + 0x73656369,0x00000000,0x00050006,0x00000031,0x00000001,0x69646e69,0x00736563,0x00050006, + 0x00000031,0x00000002,0x6769726f,0x00736e69,0x00050006,0x00000031,0x00000003,0x656e6f62, + 0x00000073,0x00050006,0x00000031,0x00000004,0x6e6f626e,0x00007365,0x00050006,0x00000031, + 0x00000005,0x69727473,0x00006564,0x00050006,0x00000031,0x00000006,0x6e756f63,0x00000074, + 0x00050006,0x00000031,0x00000007,0x73726966,0x00000074,0x00050006,0x00000031,0x00000008, + 0x70797469,0x00000065,0x00060006,0x00000031,0x00000009,0x65736162,0x7265765f,0x00786574, + 0x00040006,0x00000031,0x0000000a,0x00736f70,0x00050006,0x00000031,0x0000000b,0x73746573, + 0x00000000,0x00050006,0x00000031,0x0000000c,0x6e696f6a,0x00007374,0x00050006,0x00000031, + 0x0000000d,0x67696577,0x00737468,0x00050006,0x00000031,0x0000000e,0x6d726f66,0x00737461, + 0x00060006,0x00000031,0x0000000f,0x656c6f74,0x636e6172,0x00000065,0x00030005,0x00000033, + 0x00006370,0x00040005,0x0000003c,0x6f666562,0x00006572,0x00040005,0x00000046,0x657a6973, + 0x00000000,0x00040005,0x00000048,0x61746144,0x00000000,0x00040006,0x00000048,0x00000000, + 0x00000077,0x00040005,0x0000004a,0x61746164,0x00000000,0x00040005,0x00000055,0x7474696a, + 0x00007265,0x00030005,0x0000005f,0x00776f6e,0x00040005,0x00000075,0x6e656874,0x00000000, + 0x00030005,0x0000007c,0x00007870,0x00040005,0x00000097,0x69746f6d,0x00006e6f,0x00040047, + 0x0000000d,0x00000021,0x00000001,0x00040047,0x0000000d,0x00000022,0x00000000,0x00040047, + 0x00000011,0x0000000b,0x0000000f,0x00040047,0x0000002d,0x00000006,0x00000008,0x00040047, + 0x0000002e,0x00000006,0x00000008,0x00040047,0x0000002f,0x00000006,0x00000004,0x00040047, + 0x00000030,0x00000006,0x00000004,0x00030047,0x00000031,0x00000002,0x00050048,0x00000031, + 0x00000000,0x00000023,0x00000000,0x00050048,0x00000031,0x00000001,0x00000023,0x00000008, + 0x00050048,0x00000031,0x00000002,0x00000023,0x00000010,0x00050048,0x00000031,0x00000003, + 0x00000023,0x00000020,0x00050048,0x00000031,0x00000004,0x00000023,0x00000030,0x00050048, + 0x00000031,0x00000005,0x00000023,0x00000034,0x00050048,0x00000031,0x00000006,0x00000023, + 0x00000038,0x00050048,0x00000031,0x00000007,0x00000023,0x0000003c,0x00050048,0x00000031, + 0x00000008,0x00000023,0x00000040,0x00050048,0x00000031,0x00000009,0x00000023,0x00000044, + 0x00050048,0x00000031,0x0000000a,0x00000023,0x00000048,0x00050048,0x00000031,0x0000000b, + 0x00000023,0x0000004c,0x00050048,0x00000031,0x0000000c,0x00000023,0x00000050,0x00050048, + 0x00000031,0x0000000d,0x00000023,0x00000058,0x00050048,0x00000031,0x0000000e,0x00000023, + 0x00000060,0x00050048,0x00000031,0x0000000f,0x00000023,0x00000064,0x00040047,0x0000003c, + 0x0000001e,0x00000000,0x00040047,0x00000047,0x00000006,0x00000004,0x00030047,0x00000048, + 0x00000002,0x00040048,0x00000048,0x00000000,0x00000018,0x00050048,0x00000048,0x00000000, + 0x00000023,0x00000000,0x00030047,0x0000004a,0x00000018,0x00040047,0x0000004a,0x00000021, + 0x00000000,0x00040047,0x0000004a,0x00000022,0x00000000,0x00040047,0x00000097,0x0000001e, + 0x00000000,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00040015,0x00000006, + 0x00000020,0x00000000,0x00040020,0x00000007,0x00000007,0x00000006,0x00030016,0x00000009, + 0x00000020,0x00090019,0x0000000a,0x00000009,0x00000001,0x00000000,0x00000000,0x00000000, + 0x00000001,0x00000000,0x0003001b,0x0000000b,0x0000000a,0x00040020,0x0000000c,0x00000000, + 0x0000000b,0x0004003b,0x0000000c,0x0000000d,0x00000000,0x00040017,0x0000000f,0x00000009, + 0x00000004,0x00040020,0x00000010,0x00000001,0x0000000f,0x0004003b,0x00000010,0x00000011, + 0x00000001,0x00040017,0x00000012,0x00000009,0x00000002,0x00040015,0x00000015,0x00000020, + 0x00000001,0x00040017,0x00000016,0x00000015,0x00000002,0x0004002b,0x00000015,0x00000018, + 0x00000000,0x0004002b,0x00000006,0x0000001b,0x00000000,0x0004002b,0x00000006,0x0000001f, + 0x00000002,0x00040020,0x00000020,0x00000001,0x00000009,0x00020014,0x00000024,0x00040015, + 0x0000002c,0x00000040,0x00000000,0x0004001c,0x0000002d,0x0000002c,0x0000001f,0x0004001c, + 0x0000002e,0x0000002c,0x0000001f,0x0004001c,0x0000002f,0x00000006,0x0000001f,0x0004001c, + 0x00000030,0x00000006,0x0000001f,0x0012001e,0x00000031,0x0000002c,0x0000002c,0x0000002d, + 0x0000002e,0x00000006,0x00000006,0x00000006,0x00000006,0x00000006,0x00000015,0x00000006, + 0x00000006,0x0000002f,0x00000030,0x00000006,0x00000006,0x00040020,0x00000032,0x00000009, + 0x00000031,0x0004003b,0x00000032,0x00000033,0x00000009,0x0004002b,0x00000015,0x00000034, + 0x0000000f,0x00040020,0x00000035,0x00000009,0x00000006,0x0004003b,0x00000010,0x0000003c, + 0x00000001,0x0004002b,0x00000006,0x0000003d,0x00000003,0x0004002b,0x00000009,0x00000040, + 0x358637bd,0x00040020,0x00000045,0x00000007,0x00000012,0x0003001d,0x00000047,0x00000006, + 0x0003001e,0x00000048,0x00000047,0x00040020,0x00000049,0x0000000c,0x00000048,0x0004003b, + 0x00000049,0x0000004a,0x0000000c,0x0004002b,0x00000015,0x0000004b,0x00000022,0x00040020, + 0x0000004c,0x0000000c,0x00000006,0x0004002b,0x00000015,0x00000050,0x00000023,0x0004002b, + 0x00000015,0x00000056,0x00000020,0x0004002b,0x00000015,0x0000005a,0x00000021,0x00040020, + 0x00000062,0x00000007,0x00000009,0x0004002b,0x00000009,0x00000066,0x40000000,0x0004002b, + 0x00000009,0x00000068,0x3f800000,0x0004002b,0x00000006,0x0000006a,0x00000001,0x0004002b, + 0x00000009,0x00000088,0x3f000000,0x0004002b,0x00000009,0x0000008e,0x46800000,0x0005002c, + 0x00000012,0x0000008f,0x0000008e,0x0000008e,0x00040017,0x00000090,0x00000024,0x00000002, + 0x00040020,0x00000096,0x00000003,0x00000012,0x0004003b,0x00000096,0x00000097,0x00000003, + 0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b, + 0x00000007,0x00000008,0x00000007,0x0004003b,0x00000007,0x0000001e,0x00000007,0x0004003b, + 0x00000045,0x00000046,0x00000007,0x0004003b,0x00000045,0x00000055,0x00000007,0x0004003b, + 0x00000045,0x0000005f,0x00000007,0x0004003b,0x00000045,0x00000075,0x00000007,0x0004003b, + 0x00000045,0x0000007c,0x00000007,0x0004003d,0x0000000b,0x0000000e,0x0000000d,0x0004003d, + 0x0000000f,0x00000013,0x00000011,0x0007004f,0x00000012,0x00000014,0x00000013,0x00000013, + 0x00000000,0x00000001,0x0004006e,0x00000016,0x00000017,0x00000014,0x00040064,0x0000000a, + 0x00000019,0x0000000e,0x0007005f,0x0000000f,0x0000001a,0x00000019,0x00000017,0x00000002, + 0x00000018,0x00050051,0x00000009,0x0000001c,0x0000001a,0x00000000,0x0004007c,0x00000006, + 0x0000001d,0x0000001c,0x0003003e,0x00000008,0x0000001d,0x00050041,0x00000020,0x00000021, + 0x00000011,0x0000001f,0x0004003d,0x00000009,0x00000022,0x00000021,0x0004007c,0x00000006, + 0x00000023,0x00000022,0x0003003e,0x0000001e,0x00000023,0x0004003d,0x00000006,0x00000025, + 0x00000008,0x0004003d,0x00000006,0x00000026,0x0000001e,0x0007000c,0x00000006,0x00000027, + 0x00000001,0x00000029,0x00000025,0x00000026,0x0004003d,0x00000006,0x00000028,0x00000008, + 0x0004003d,0x00000006,0x00000029,0x0000001e,0x0007000c,0x00000006,0x0000002a,0x00000001, + 0x00000026,0x00000028,0x00000029,0x00050082,0x00000006,0x0000002b,0x00000027,0x0000002a, + 0x00050041,0x00000035,0x00000036,0x00000033,0x00000034,0x0004003d,0x00000006,0x00000037, + 0x00000036,0x000500ac,0x00000024,0x00000038,0x0000002b,0x00000037,0x000400a8,0x00000024, + 0x00000039,0x00000038,0x000300f7,0x0000003b,0x00000000,0x000400fa,0x00000039,0x0000003a, + 0x0000003b,0x000200f8,0x0000003a,0x00050041,0x00000020,0x0000003e,0x0000003c,0x0000003d, + 0x0004003d,0x00000009,0x0000003f,0x0000003e,0x000500bc,0x00000024,0x00000041,0x0000003f, + 0x00000040,0x000200f9,0x0000003b,0x000200f8,0x0000003b,0x000700f5,0x00000024,0x00000042, + 0x00000038,0x00000005,0x00000041,0x0000003a,0x000300f7,0x00000044,0x00000000,0x000400fa, + 0x00000042,0x00000043,0x00000044,0x000200f8,0x00000043,0x00011504,0x000200f9,0x00000044, + 0x000200f8,0x00000044,0x00060041,0x0000004c,0x0000004d,0x0000004a,0x00000018,0x0000004b, + 0x0004003d,0x00000006,0x0000004e,0x0000004d,0x0004007c,0x00000009,0x0000004f,0x0000004e, + 0x00060041,0x0000004c,0x00000051,0x0000004a,0x00000018,0x00000050,0x0004003d,0x00000006, + 0x00000052,0x00000051,0x0004007c,0x00000009,0x00000053,0x00000052,0x00050050,0x00000012, + 0x00000054,0x0000004f,0x00000053,0x0003003e,0x00000046,0x00000054,0x00060041,0x0000004c, + 0x00000057,0x0000004a,0x00000018,0x00000056,0x0004003d,0x00000006,0x00000058,0x00000057, + 0x0004007c,0x00000009,0x00000059,0x00000058,0x00060041,0x0000004c,0x0000005b,0x0000004a, + 0x00000018,0x0000005a,0x0004003d,0x00000006,0x0000005c,0x0000005b,0x0004007c,0x00000009, + 0x0000005d,0x0000005c,0x00050050,0x00000012,0x0000005e,0x00000059,0x0000005d,0x0003003e, + 0x00000055,0x0000005e,0x00050041,0x00000020,0x00000060,0x00000011,0x0000001b,0x0004003d, + 0x00000009,0x00000061,0x00000060,0x00050041,0x00000062,0x00000063,0x00000046,0x0000001b, + 0x0004003d,0x00000009,0x00000064,0x00000063,0x00050088,0x00000009,0x00000065,0x00000061, + 0x00000064,0x00050085,0x00000009,0x00000067,0x00000065,0x00000066,0x00050083,0x00000009, + 0x00000069,0x00000067,0x00000068,0x00050041,0x00000020,0x0000006b,0x00000011,0x0000006a, + 0x0004003d,0x00000009,0x0000006c,0x0000006b,0x00050041,0x00000062,0x0000006d,0x00000046, + 0x0000006a,0x0004003d,0x00000009,0x0000006e,0x0000006d,0x00050088,0x00000009,0x0000006f, + 0x0000006c,0x0000006e,0x00050085,0x00000009,0x00000070,0x0000006f,0x00000066,0x00050083, + 0x00000009,0x00000071,0x00000068,0x00000070,0x00050050,0x00000012,0x00000072,0x00000069, + 0x00000071,0x0004003d,0x00000012,0x00000073,0x00000055,0x00050083,0x00000012,0x00000074, + 0x00000072,0x00000073,0x0003003e,0x0000005f,0x00000074,0x0004003d,0x0000000f,0x00000076, + 0x0000003c,0x0007004f,0x00000012,0x00000077,0x00000076,0x00000076,0x00000000,0x00000001, + 0x00050041,0x00000020,0x00000078,0x0000003c,0x0000003d,0x0004003d,0x00000009,0x00000079, + 0x00000078,0x00050050,0x00000012,0x0000007a,0x00000079,0x00000079,0x00050088,0x00000012, + 0x0000007b,0x00000077,0x0000007a,0x0003003e,0x00000075,0x0000007b,0x00050041,0x00000062, + 0x0000007d,0x00000075,0x0000001b,0x0004003d,0x00000009,0x0000007e,0x0000007d,0x00050041, + 0x00000062,0x0000007f,0x0000005f,0x0000001b,0x0004003d,0x00000009,0x00000080,0x0000007f, + 0x00050083,0x00000009,0x00000081,0x0000007e,0x00000080,0x00050041,0x00000062,0x00000082, + 0x0000005f,0x0000006a,0x0004003d,0x00000009,0x00000083,0x00000082,0x00050041,0x00000062, + 0x00000084,0x00000075,0x0000006a,0x0004003d,0x00000009,0x00000085,0x00000084,0x00050083, + 0x00000009,0x00000086,0x00000083,0x00000085,0x00050050,0x00000012,0x00000087,0x00000081, + 0x00000086,0x0005008e,0x00000012,0x00000089,0x00000087,0x00000088,0x0004003d,0x00000012, + 0x0000008a,0x00000046,0x00050085,0x00000012,0x0000008b,0x00000089,0x0000008a,0x0003003e, + 0x0000007c,0x0000008b,0x0004003d,0x00000012,0x0000008c,0x0000007c,0x0006000c,0x00000012, + 0x0000008d,0x00000001,0x00000004,0x0000008c,0x000500b8,0x00000090,0x00000091,0x0000008d, + 0x0000008f,0x0004009b,0x00000024,0x00000092,0x00000091,0x000400a8,0x00000024,0x00000093, + 0x00000092,0x000300f7,0x00000095,0x00000000,0x000400fa,0x00000093,0x00000094,0x00000095, + 0x000200f8,0x00000094,0x00011504,0x000200f9,0x00000095,0x000200f8,0x00000095,0x0004003d, + 0x00000012,0x00000098,0x0000007c,0x0003003e,0x00000097,0x00000098,0x000100fd,0x00010038 +}; diff --git a/gpu/vk/shaders/dlss_actor_vert_spv.h b/gpu/vk/shaders/dlss_actor_vert_spv.h new file mode 100644 index 000000000..7faba9583 --- /dev/null +++ b/gpu/vk/shaders/dlss_actor_vert_spv.h @@ -0,0 +1,421 @@ + // 1116.6.0 + #pragma once +const uint32_t DLSS_ACTOR_VERT_SPV[] = { + 0x07230203,0x00010600,0x0008000b,0x0000021e,0x00000000,0x00020011,0x00000001,0x00020011, + 0x0000000b,0x00020011,0x000014e3,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e, + 0x00000000,0x0003000e,0x000014e4,0x00000001,0x000a000f,0x00000000,0x00000004,0x6e69616d, + 0x00000000,0x00000030,0x000000b0,0x000000e4,0x00000130,0x00000135,0x00030003,0x00000002, + 0x000001c2,0x00070004,0x455f4c47,0x625f5458,0x65666675,0x65725f72,0x65726566,0x0065636e, + 0x000d0004,0x455f4c47,0x735f5458,0x65646168,0x78655f72,0x63696c70,0x615f7469,0x68746972, + 0x6974656d,0x79745f63,0x5f736570,0x36746e69,0x00000034,0x00040005,0x00000004,0x6e69616d, + 0x00000000,0x00040005,0x0000000b,0x31752866,0x0000003b,0x00030005,0x0000000a,0x00000069, + 0x00040005,0x00000010,0x75283466,0x00003b31,0x00030005,0x0000000f,0x00000069,0x00050005, + 0x00000015,0x656d6163,0x75286172,0x00003b31,0x00030005,0x00000014,0x00007461,0x00040005, + 0x00000019,0x64726f57,0x00000073,0x00040006,0x00000019,0x00000000,0x00000077,0x00050005, + 0x0000001e,0x28776f72,0x31753b31,0x0000003b,0x00030005,0x0000001c,0x0000006d,0x00030005, + 0x0000001d,0x00000069,0x00080005,0x0000002b,0x6e696b73,0x763b3128,0x763b3475,0x763b3466, + 0x763b3466,0x003b3366,0x00040005,0x00000026,0x656c6170,0x00657474,0x00030005,0x00000027, + 0x0000006a,0x00030005,0x00000028,0x00007477,0x00030005,0x00000029,0x00000070,0x00030005, + 0x0000002a,0x00636361,0x00040005,0x0000002e,0x61746144,0x00000000,0x00040006,0x0000002e, + 0x00000000,0x00000077,0x00040005,0x00000030,0x61746164,0x00000000,0x00040005,0x0000003a, + 0x61726170,0x0000006d,0x00040005,0x00000040,0x61726170,0x0000006d,0x00040005,0x00000045, + 0x61726170,0x0000006d,0x00040005,0x0000004a,0x61726170,0x0000006d,0x00040005,0x0000004f, + 0x61726170,0x0000006d,0x00040005,0x00000055,0x61726170,0x0000006d,0x00040005,0x0000005a, + 0x61726170,0x0000006d,0x00040005,0x0000005f,0x61726170,0x0000006d,0x00030005,0x00000094, + 0x0000006b,0x00030005,0x000000ae,0x00000050,0x00060006,0x000000ae,0x00000000,0x74726576, + 0x73656369,0x00000000,0x00050006,0x000000ae,0x00000001,0x69646e69,0x00736563,0x00050006, + 0x000000ae,0x00000002,0x6769726f,0x00736e69,0x00050006,0x000000ae,0x00000003,0x656e6f62, + 0x00000073,0x00050006,0x000000ae,0x00000004,0x6e6f626e,0x00007365,0x00050006,0x000000ae, + 0x00000005,0x69727473,0x00006564,0x00050006,0x000000ae,0x00000006,0x6e756f63,0x00000074, + 0x00050006,0x000000ae,0x00000007,0x73726966,0x00000074,0x00050006,0x000000ae,0x00000008, + 0x70797469,0x00000065,0x00060006,0x000000ae,0x00000009,0x65736162,0x7265765f,0x00786574, + 0x00040006,0x000000ae,0x0000000a,0x00736f70,0x00050006,0x000000ae,0x0000000b,0x73746573, + 0x00000000,0x00050006,0x000000ae,0x0000000c,0x6e696f6a,0x00007374,0x00050006,0x000000ae, + 0x0000000d,0x67696577,0x00737468,0x00050006,0x000000ae,0x0000000e,0x6d726f66,0x00737461, + 0x00060006,0x000000ae,0x0000000f,0x656c6f74,0x636e6172,0x00000065,0x00030005,0x000000b0, + 0x00006370,0x00030005,0x000000b9,0x00000062,0x00040005,0x000000c1,0x61726170,0x0000006d, + 0x00040005,0x000000c3,0x61726170,0x0000006d,0x00040005,0x000000ca,0x61726170,0x0000006d, + 0x00040005,0x000000cc,0x61726170,0x0000006d,0x00040005,0x000000d2,0x61726170,0x0000006d, + 0x00040005,0x000000d4,0x61726170,0x0000006d,0x00030005,0x000000df,0x00007461,0x00060005, + 0x000000e4,0x565f6c67,0x65747265,0x646e4978,0x00007865,0x00030005,0x000000e8,0x00006269, + 0x00040005,0x000000f3,0x65646e69,0x00000078,0x00030005,0x00000117,0x00006976,0x00060005, + 0x0000012e,0x505f6c67,0x65567265,0x78657472,0x00000000,0x00060006,0x0000012e,0x00000000, + 0x505f6c67,0x7469736f,0x006e6f69,0x00070006,0x0000012e,0x00000001,0x505f6c67,0x746e696f, + 0x657a6953,0x00000000,0x00070006,0x0000012e,0x00000002,0x435f6c67,0x4470696c,0x61747369, + 0x0065636e,0x00070006,0x0000012e,0x00000003,0x435f6c67,0x446c6c75,0x61747369,0x0065636e, + 0x00030005,0x00000130,0x00000000,0x00040005,0x00000135,0x6f666562,0x00006572,0x00030005, + 0x00000138,0x00006276,0x00030005,0x00000147,0x00000071,0x00030005,0x0000014c,0x00000070, + 0x00030005,0x0000017a,0x00776f6e,0x00040005,0x0000017c,0x6e656874,0x00000000,0x00030005, + 0x0000017d,0x00000073,0x00030005,0x00000188,0x00000061,0x00030005,0x00000191,0x0000006a, + 0x00030005,0x000001c0,0x00007477,0x00040005,0x000001ce,0x61726170,0x0000006d,0x00040005, + 0x000001cf,0x61726170,0x0000006d,0x00040005,0x000001d1,0x61726170,0x0000006d,0x00040005, + 0x000001d3,0x61726170,0x0000006d,0x00040005,0x000001d5,0x61726170,0x0000006d,0x00040005, + 0x000001dc,0x61726170,0x0000006d,0x00040005,0x000001dd,0x61726170,0x0000006d,0x00040005, + 0x000001df,0x61726170,0x0000006d,0x00040005,0x000001e1,0x61726170,0x0000006d,0x00040005, + 0x000001e3,0x61726170,0x0000006d,0x00030005,0x000001e9,0x0000306f,0x00040005,0x000001ed, + 0x61726170,0x0000006d,0x00040005,0x000001ee,0x61726170,0x0000006d,0x00030005,0x000001f1, + 0x0000316f,0x00040005,0x000001f5,0x61726170,0x0000006d,0x00040005,0x000001f6,0x61726170, + 0x0000006d,0x00040005,0x000001f9,0x70696c63,0x00000000,0x00040005,0x000001fa,0x61726170, + 0x0000006d,0x00040005,0x00000204,0x61726170,0x0000006d,0x00040047,0x00000018,0x00000006, + 0x00000004,0x00030047,0x00000019,0x00000002,0x00040048,0x00000019,0x00000000,0x00000018, + 0x00050048,0x00000019,0x00000000,0x00000023,0x00000000,0x00030047,0x0000001c,0x000014ec, + 0x00030047,0x00000026,0x000014ec,0x00040047,0x0000002d,0x00000006,0x00000004,0x00030047, + 0x0000002e,0x00000002,0x00040048,0x0000002e,0x00000000,0x00000018,0x00050048,0x0000002e, + 0x00000000,0x00000023,0x00000000,0x00030047,0x00000030,0x00000018,0x00040047,0x00000030, + 0x00000021,0x00000000,0x00040047,0x00000030,0x00000022,0x00000000,0x00040047,0x000000aa, + 0x00000006,0x00000008,0x00040047,0x000000ab,0x00000006,0x00000008,0x00040047,0x000000ac, + 0x00000006,0x00000004,0x00040047,0x000000ad,0x00000006,0x00000004,0x00030047,0x000000ae, + 0x00000002,0x00050048,0x000000ae,0x00000000,0x00000023,0x00000000,0x00050048,0x000000ae, + 0x00000001,0x00000023,0x00000008,0x00050048,0x000000ae,0x00000002,0x00000023,0x00000010, + 0x00050048,0x000000ae,0x00000003,0x00000023,0x00000020,0x00050048,0x000000ae,0x00000004, + 0x00000023,0x00000030,0x00050048,0x000000ae,0x00000005,0x00000023,0x00000034,0x00050048, + 0x000000ae,0x00000006,0x00000023,0x00000038,0x00050048,0x000000ae,0x00000007,0x00000023, + 0x0000003c,0x00050048,0x000000ae,0x00000008,0x00000023,0x00000040,0x00050048,0x000000ae, + 0x00000009,0x00000023,0x00000044,0x00050048,0x000000ae,0x0000000a,0x00000023,0x00000048, + 0x00050048,0x000000ae,0x0000000b,0x00000023,0x0000004c,0x00050048,0x000000ae,0x0000000c, + 0x00000023,0x00000050,0x00050048,0x000000ae,0x0000000d,0x00000023,0x00000058,0x00050048, + 0x000000ae,0x0000000e,0x00000023,0x00000060,0x00050048,0x000000ae,0x0000000f,0x00000023, + 0x00000064,0x00030047,0x000000c1,0x000014ec,0x00030047,0x000000ca,0x000014ec,0x00030047, + 0x000000d2,0x000014ec,0x00040047,0x000000e4,0x0000000b,0x0000002a,0x00030047,0x000000e8, + 0x000014ec,0x00030047,0x0000012e,0x00000002,0x00050048,0x0000012e,0x00000000,0x0000000b, + 0x00000000,0x00050048,0x0000012e,0x00000001,0x0000000b,0x00000001,0x00050048,0x0000012e, + 0x00000002,0x0000000b,0x00000003,0x00050048,0x0000012e,0x00000003,0x0000000b,0x00000004, + 0x00040047,0x00000135,0x0000001e,0x00000000,0x00030047,0x00000138,0x000014ec,0x00030047, + 0x000001ce,0x000014ec,0x00030047,0x000001dc,0x000014ec,0x00030047,0x000001ed,0x000014ec, + 0x00030047,0x000001f5,0x000014ec,0x00020013,0x00000002,0x00030021,0x00000003,0x00000002, + 0x00040015,0x00000006,0x00000020,0x00000000,0x00040020,0x00000007,0x00000007,0x00000006, + 0x00030016,0x00000008,0x00000020,0x00040021,0x00000009,0x00000008,0x00000007,0x00040017, + 0x0000000d,0x00000008,0x00000004,0x00040021,0x0000000e,0x0000000d,0x00000007,0x00040018, + 0x00000012,0x0000000d,0x00000004,0x00040021,0x00000013,0x00000012,0x00000007,0x00030027, + 0x00000017,0x000014e5,0x0003001d,0x00000018,0x00000006,0x0003001e,0x00000019,0x00000018, + 0x00040020,0x00000017,0x000014e5,0x00000019,0x00040020,0x0000001a,0x00000007,0x00000017, + 0x00050021,0x0000001b,0x0000000d,0x0000001a,0x00000007,0x00040017,0x00000020,0x00000006, + 0x00000004,0x00040020,0x00000021,0x00000007,0x00000020,0x00040020,0x00000022,0x00000007, + 0x0000000d,0x00040017,0x00000023,0x00000008,0x00000003,0x00040020,0x00000024,0x00000007, + 0x00000023,0x00080021,0x00000025,0x00000002,0x0000001a,0x00000021,0x00000022,0x00000022, + 0x00000024,0x0003001d,0x0000002d,0x00000006,0x0003001e,0x0000002e,0x0000002d,0x00040020, + 0x0000002f,0x0000000c,0x0000002e,0x0004003b,0x0000002f,0x00000030,0x0000000c,0x00040015, + 0x00000031,0x00000020,0x00000001,0x0004002b,0x00000031,0x00000032,0x00000000,0x00040020, + 0x00000034,0x0000000c,0x00000006,0x0004002b,0x00000006,0x0000003e,0x00000001,0x0004002b, + 0x00000006,0x00000043,0x00000002,0x0004002b,0x00000006,0x00000048,0x00000003,0x0004002b, + 0x00000006,0x00000053,0x00000004,0x0004002b,0x00000006,0x00000058,0x00000008,0x0004002b, + 0x00000006,0x0000005d,0x0000000c,0x0004002b,0x00000008,0x00000061,0x3f800000,0x0004002b, + 0x00000008,0x00000062,0x00000000,0x00040020,0x0000007d,0x000014e5,0x00000006,0x00040020, + 0x00000093,0x00000007,0x00000031,0x0004002b,0x00000031,0x0000009b,0x00000004,0x00020014, + 0x0000009c,0x00040020,0x0000009f,0x00000007,0x00000008,0x00040015,0x000000a9,0x00000040, + 0x00000000,0x0004001c,0x000000aa,0x000000a9,0x00000043,0x0004001c,0x000000ab,0x000000a9, + 0x00000043,0x0004001c,0x000000ac,0x00000006,0x00000043,0x0004001c,0x000000ad,0x00000006, + 0x00000043,0x0012001e,0x000000ae,0x000000a9,0x000000a9,0x000000aa,0x000000ab,0x00000006, + 0x00000006,0x00000006,0x00000006,0x00000006,0x00000031,0x00000006,0x00000006,0x000000ac, + 0x000000ad,0x00000006,0x00000006,0x00040020,0x000000af,0x00000009,0x000000ae,0x0004003b, + 0x000000af,0x000000b0,0x00000009,0x00040020,0x000000b1,0x00000009,0x00000006,0x0004002b, + 0x00000031,0x000000dd,0x00000001,0x0004002b,0x00000031,0x000000e0,0x00000007,0x00040020, + 0x000000e3,0x00000001,0x00000031,0x0004003b,0x000000e3,0x000000e4,0x00000001,0x00040020, + 0x000000e9,0x00000009,0x000000a9,0x0004002b,0x00000031,0x000000ed,0x00000008,0x0004002b, + 0x00000006,0x00000105,0x00000010,0x0004002b,0x00000006,0x00000108,0x0000ffff,0x0004002b, + 0x00000031,0x0000010d,0x00000002,0x0004002b,0x00000006,0x00000115,0x000000ff,0x0004002b, + 0x00000031,0x00000118,0x00000009,0x00040020,0x00000119,0x00000009,0x00000031,0x0004002b, + 0x00000031,0x00000126,0x00000006,0x0004001c,0x0000012d,0x00000008,0x0000003e,0x0006001e, + 0x0000012e,0x0000000d,0x00000008,0x0000012d,0x0000012d,0x00040020,0x0000012f,0x00000003, + 0x0000012e,0x0004003b,0x0000012f,0x00000130,0x00000003,0x0004002b,0x00000008,0x00000131, + 0xbf800000,0x0007002c,0x0000000d,0x00000132,0x00000062,0x00000062,0x00000062,0x00000131, + 0x00040020,0x00000133,0x00000003,0x0000000d,0x0004003b,0x00000133,0x00000135,0x00000003, + 0x0007002c,0x0000000d,0x00000136,0x00000062,0x00000062,0x00000062,0x00000062,0x0004002b, + 0x00000031,0x0000013e,0x00000005,0x0005002b,0x000000a9,0x00000143,0x00000004,0x00000000, + 0x0004002b,0x00000031,0x00000148,0x0000000a,0x0004002b,0x00000031,0x0000014d,0x0000000e, + 0x0004002b,0x00000006,0x00000151,0x00000000,0x00040017,0x0000015a,0x00000008,0x00000002, + 0x0006002c,0x00000023,0x0000017b,0x00000062,0x00000062,0x00000062,0x0004002b,0x00000031, + 0x00000184,0x0000000b,0x0004002b,0x00000031,0x0000018a,0x0000000c,0x0004002b,0x00000031, + 0x0000019f,0x00000010,0x0004002b,0x00000031,0x000001a2,0x00000018,0x0004002b,0x00000031, + 0x000001c2,0x0000000d,0x0004002b,0x00000031,0x000001ca,0x00000003,0x0004002b,0x00000008, + 0x00000218,0x3f000000,0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8, + 0x00000005,0x0004003b,0x00000007,0x000000df,0x00000007,0x0004003b,0x0000001a,0x000000e8, + 0x00000007,0x0004003b,0x00000007,0x000000f3,0x00000007,0x0004003b,0x00000093,0x00000117, + 0x00000007,0x0004003b,0x0000001a,0x00000138,0x00000007,0x0004003b,0x00000007,0x00000147, + 0x00000007,0x0004003b,0x00000022,0x0000014c,0x00000007,0x0004003b,0x00000022,0x00000153, + 0x00000007,0x0004003b,0x00000024,0x0000017a,0x00000007,0x0004003b,0x00000024,0x0000017c, + 0x00000007,0x0004003b,0x00000007,0x0000017d,0x00000007,0x0004003b,0x00000007,0x00000188, + 0x00000007,0x0004003b,0x00000021,0x00000191,0x00000007,0x0004003b,0x00000021,0x00000198, + 0x00000007,0x0004003b,0x00000022,0x000001c0,0x00000007,0x0004003b,0x0000001a,0x000001ce, + 0x00000007,0x0004003b,0x00000021,0x000001cf,0x00000007,0x0004003b,0x00000022,0x000001d1, + 0x00000007,0x0004003b,0x00000022,0x000001d3,0x00000007,0x0004003b,0x00000024,0x000001d5, + 0x00000007,0x0004003b,0x0000001a,0x000001dc,0x00000007,0x0004003b,0x00000021,0x000001dd, + 0x00000007,0x0004003b,0x00000022,0x000001df,0x00000007,0x0004003b,0x00000022,0x000001e1, + 0x00000007,0x0004003b,0x00000024,0x000001e3,0x00000007,0x0004003b,0x00000024,0x000001e9, + 0x00000007,0x0004003b,0x0000001a,0x000001ed,0x00000007,0x0004003b,0x00000007,0x000001ee, + 0x00000007,0x0004003b,0x00000024,0x000001f1,0x00000007,0x0004003b,0x0000001a,0x000001f5, + 0x00000007,0x0004003b,0x00000007,0x000001f6,0x00000007,0x0004003b,0x00000022,0x000001f9, + 0x00000007,0x0004003b,0x00000007,0x000001fa,0x00000007,0x0004003b,0x00000007,0x00000204, + 0x00000007,0x00050041,0x000000b1,0x000000e1,0x000000b0,0x000000e0,0x0004003d,0x00000006, + 0x000000e2,0x000000e1,0x0004003d,0x00000031,0x000000e5,0x000000e4,0x0004007c,0x00000006, + 0x000000e6,0x000000e5,0x00050080,0x00000006,0x000000e7,0x000000e2,0x000000e6,0x0003003e, + 0x000000df,0x000000e7,0x00050041,0x000000e9,0x000000ea,0x000000b0,0x000000dd,0x0004003d, + 0x000000a9,0x000000eb,0x000000ea,0x00040078,0x00000017,0x000000ec,0x000000eb,0x0003003e, + 0x000000e8,0x000000ec,0x00050041,0x000000b1,0x000000ee,0x000000b0,0x000000ed,0x0004003d, + 0x00000006,0x000000ef,0x000000ee,0x000500aa,0x0000009c,0x000000f0,0x000000ef,0x00000043, + 0x000300f7,0x000000f2,0x00000000,0x000400fa,0x000000f0,0x000000f1,0x000000f8,0x000200f8, + 0x000000f1,0x0004003d,0x00000017,0x000000f4,0x000000e8,0x0004003d,0x00000006,0x000000f5, + 0x000000df,0x00060041,0x0000007d,0x000000f6,0x000000f4,0x00000032,0x000000f5,0x0006003d, + 0x00000006,0x000000f7,0x000000f6,0x00000002,0x00000004,0x0003003e,0x000000f3,0x000000f7, + 0x000200f9,0x000000f2,0x000200f8,0x000000f8,0x00050041,0x000000b1,0x000000f9,0x000000b0, + 0x000000ed,0x0004003d,0x00000006,0x000000fa,0x000000f9,0x000500aa,0x0000009c,0x000000fb, + 0x000000fa,0x0000003e,0x000300f7,0x000000fd,0x00000000,0x000400fa,0x000000fb,0x000000fc, + 0x0000010a,0x000200f8,0x000000fc,0x0004003d,0x00000017,0x000000fe,0x000000e8,0x0004003d, + 0x00000006,0x000000ff,0x000000df,0x000500c2,0x00000006,0x00000100,0x000000ff,0x000000dd, + 0x00060041,0x0000007d,0x00000101,0x000000fe,0x00000032,0x00000100,0x0006003d,0x00000006, + 0x00000102,0x00000101,0x00000002,0x00000004,0x0004003d,0x00000006,0x00000103,0x000000df, + 0x000500c7,0x00000006,0x00000104,0x00000103,0x0000003e,0x00050084,0x00000006,0x00000106, + 0x00000104,0x00000105,0x000500c2,0x00000006,0x00000107,0x00000102,0x00000106,0x000500c7, + 0x00000006,0x00000109,0x00000107,0x00000108,0x0003003e,0x000000f3,0x00000109,0x000200f9, + 0x000000fd,0x000200f8,0x0000010a,0x0004003d,0x00000017,0x0000010b,0x000000e8,0x0004003d, + 0x00000006,0x0000010c,0x000000df,0x000500c2,0x00000006,0x0000010e,0x0000010c,0x0000010d, + 0x00060041,0x0000007d,0x0000010f,0x0000010b,0x00000032,0x0000010e,0x0006003d,0x00000006, + 0x00000110,0x0000010f,0x00000002,0x00000004,0x0004003d,0x00000006,0x00000111,0x000000df, + 0x000500c7,0x00000006,0x00000112,0x00000111,0x00000048,0x00050084,0x00000006,0x00000113, + 0x00000112,0x00000058,0x000500c2,0x00000006,0x00000114,0x00000110,0x00000113,0x000500c7, + 0x00000006,0x00000116,0x00000114,0x00000115,0x0003003e,0x000000f3,0x00000116,0x000200f9, + 0x000000fd,0x000200f8,0x000000fd,0x000200f9,0x000000f2,0x000200f8,0x000000f2,0x00050041, + 0x00000119,0x0000011a,0x000000b0,0x00000118,0x0004003d,0x00000031,0x0000011b,0x0000011a, + 0x0004003d,0x00000006,0x0000011c,0x000000f3,0x0004007c,0x00000031,0x0000011d,0x0000011c, + 0x00050080,0x00000031,0x0000011e,0x0000011b,0x0000011d,0x0003003e,0x00000117,0x0000011e, + 0x0004003d,0x00000031,0x0000011f,0x00000117,0x000500b1,0x0000009c,0x00000120,0x0000011f, + 0x00000032,0x000400a8,0x0000009c,0x00000121,0x00000120,0x000300f7,0x00000123,0x00000000, + 0x000400fa,0x00000121,0x00000122,0x00000123,0x000200f8,0x00000122,0x0004003d,0x00000031, + 0x00000124,0x00000117,0x0004007c,0x00000006,0x00000125,0x00000124,0x00050041,0x000000b1, + 0x00000127,0x000000b0,0x00000126,0x0004003d,0x00000006,0x00000128,0x00000127,0x000500ae, + 0x0000009c,0x00000129,0x00000125,0x00000128,0x000200f9,0x00000123,0x000200f8,0x00000123, + 0x000700f5,0x0000009c,0x0000012a,0x00000120,0x000000f2,0x00000129,0x00000122,0x000300f7, + 0x0000012c,0x00000000,0x000400fa,0x0000012a,0x0000012b,0x0000012c,0x000200f8,0x0000012b, + 0x00050041,0x00000133,0x00000134,0x00000130,0x00000032,0x0003003e,0x00000134,0x00000132, + 0x0003003e,0x00000135,0x00000136,0x000100fd,0x000200f8,0x0000012c,0x00050041,0x000000e9, + 0x00000139,0x000000b0,0x00000032,0x0004003d,0x000000a9,0x0000013a,0x00000139,0x0004003d, + 0x00000031,0x0000013b,0x00000117,0x0004007c,0x00000006,0x0000013c,0x0000013b,0x00040071, + 0x000000a9,0x0000013d,0x0000013c,0x00050041,0x000000b1,0x0000013f,0x000000b0,0x0000013e, + 0x0004003d,0x00000006,0x00000140,0x0000013f,0x00040071,0x000000a9,0x00000141,0x00000140, + 0x00050084,0x000000a9,0x00000142,0x0000013d,0x00000141,0x00050084,0x000000a9,0x00000144, + 0x00000142,0x00000143,0x00050080,0x000000a9,0x00000145,0x0000013a,0x00000144,0x00040078, + 0x00000017,0x00000146,0x00000145,0x0003003e,0x00000138,0x00000146,0x00050041,0x000000b1, + 0x00000149,0x000000b0,0x00000148,0x0004003d,0x00000006,0x0000014a,0x00000149,0x00050086, + 0x00000006,0x0000014b,0x0000014a,0x00000053,0x0003003e,0x00000147,0x0000014b,0x00050041, + 0x000000b1,0x0000014e,0x000000b0,0x0000014d,0x0004003d,0x00000006,0x0000014f,0x0000014e, + 0x000500c7,0x00000006,0x00000150,0x0000014f,0x0000003e,0x000500ab,0x0000009c,0x00000152, + 0x00000150,0x00000151,0x000300f7,0x00000155,0x00000000,0x000400fa,0x00000152,0x00000154, + 0x00000166,0x000200f8,0x00000154,0x0004003d,0x00000017,0x00000156,0x00000138,0x0004003d, + 0x00000006,0x00000157,0x00000147,0x00060041,0x0000007d,0x00000158,0x00000156,0x00000032, + 0x00000157,0x0006003d,0x00000006,0x00000159,0x00000158,0x00000002,0x00000004,0x0006000c, + 0x0000015a,0x0000015b,0x00000001,0x0000003e,0x00000159,0x0004003d,0x00000017,0x0000015c, + 0x00000138,0x0004003d,0x00000006,0x0000015d,0x00000147,0x00050080,0x00000006,0x0000015e, + 0x0000015d,0x0000003e,0x00060041,0x0000007d,0x0000015f,0x0000015c,0x00000032,0x0000015e, + 0x0006003d,0x00000006,0x00000160,0x0000015f,0x00000002,0x00000004,0x0006000c,0x0000015a, + 0x00000161,0x00000001,0x0000003e,0x00000160,0x00050051,0x00000008,0x00000162,0x00000161, + 0x00000000,0x00050051,0x00000008,0x00000163,0x0000015b,0x00000000,0x00050051,0x00000008, + 0x00000164,0x0000015b,0x00000001,0x00070050,0x0000000d,0x00000165,0x00000163,0x00000164, + 0x00000162,0x00000061,0x0003003e,0x00000153,0x00000165,0x000200f9,0x00000155,0x000200f8, + 0x00000166,0x0004003d,0x00000017,0x00000167,0x00000138,0x0004003d,0x00000006,0x00000168, + 0x00000147,0x00060041,0x0000007d,0x00000169,0x00000167,0x00000032,0x00000168,0x0006003d, + 0x00000006,0x0000016a,0x00000169,0x00000002,0x00000004,0x0004007c,0x00000008,0x0000016b, + 0x0000016a,0x0004003d,0x00000017,0x0000016c,0x00000138,0x0004003d,0x00000006,0x0000016d, + 0x00000147,0x00050080,0x00000006,0x0000016e,0x0000016d,0x0000003e,0x00060041,0x0000007d, + 0x0000016f,0x0000016c,0x00000032,0x0000016e,0x0006003d,0x00000006,0x00000170,0x0000016f, + 0x00000002,0x00000004,0x0004007c,0x00000008,0x00000171,0x00000170,0x0004003d,0x00000017, + 0x00000172,0x00000138,0x0004003d,0x00000006,0x00000173,0x00000147,0x00050080,0x00000006, + 0x00000174,0x00000173,0x00000043,0x00060041,0x0000007d,0x00000175,0x00000172,0x00000032, + 0x00000174,0x0006003d,0x00000006,0x00000176,0x00000175,0x00000002,0x00000004,0x0004007c, + 0x00000008,0x00000177,0x00000176,0x00070050,0x0000000d,0x00000178,0x0000016b,0x00000171, + 0x00000177,0x00000061,0x0003003e,0x00000153,0x00000178,0x000200f9,0x00000155,0x000200f8, + 0x00000155,0x0004003d,0x0000000d,0x00000179,0x00000153,0x0003003e,0x0000014c,0x00000179, + 0x0003003e,0x0000017a,0x0000017b,0x0003003e,0x0000017c,0x0000017b,0x0003003e,0x0000017d, + 0x00000151,0x000200f9,0x0000017e,0x000200f8,0x0000017e,0x000400f6,0x00000180,0x00000181, + 0x00000000,0x000200f9,0x00000182,0x000200f8,0x00000182,0x0004003d,0x00000006,0x00000183, + 0x0000017d,0x00050041,0x000000b1,0x00000185,0x000000b0,0x00000184,0x0004003d,0x00000006, + 0x00000186,0x00000185,0x000500b0,0x0000009c,0x00000187,0x00000183,0x00000186,0x000400fa, + 0x00000187,0x0000017f,0x00000180,0x000200f8,0x0000017f,0x0004003d,0x00000017,0x00000189, + 0x00000138,0x0004003d,0x00000006,0x0000018b,0x0000017d,0x00060041,0x000000b1,0x0000018c, + 0x000000b0,0x0000018a,0x0000018b,0x0004003d,0x00000006,0x0000018d,0x0000018c,0x00050086, + 0x00000006,0x0000018e,0x0000018d,0x00000053,0x00060041,0x0000007d,0x0000018f,0x00000189, + 0x00000032,0x0000018e,0x0006003d,0x00000006,0x00000190,0x0000018f,0x00000002,0x00000004, + 0x0003003e,0x00000188,0x00000190,0x00050041,0x000000b1,0x00000192,0x000000b0,0x0000014d, + 0x0004003d,0x00000006,0x00000193,0x00000192,0x0004003d,0x00000006,0x00000194,0x0000017d, + 0x000500c4,0x00000006,0x00000195,0x00000043,0x00000194,0x000500c7,0x00000006,0x00000196, + 0x00000193,0x00000195,0x000500ab,0x0000009c,0x00000197,0x00000196,0x00000151,0x000300f7, + 0x0000019a,0x00000000,0x000400fa,0x00000197,0x00000199,0x000001a7,0x000200f8,0x00000199, + 0x0004003d,0x00000006,0x0000019b,0x00000188,0x0004003d,0x00000006,0x0000019c,0x00000188, + 0x000500c2,0x00000006,0x0000019d,0x0000019c,0x000000ed,0x0004003d,0x00000006,0x0000019e, + 0x00000188,0x000500c2,0x00000006,0x000001a0,0x0000019e,0x0000019f,0x0004003d,0x00000006, + 0x000001a1,0x00000188,0x000500c2,0x00000006,0x000001a3,0x000001a1,0x000001a2,0x00070050, + 0x00000020,0x000001a4,0x0000019b,0x0000019d,0x000001a0,0x000001a3,0x00070050,0x00000020, + 0x000001a5,0x00000115,0x00000115,0x00000115,0x00000115,0x000500c7,0x00000020,0x000001a6, + 0x000001a4,0x000001a5,0x0003003e,0x00000198,0x000001a6,0x000200f9,0x0000019a,0x000200f8, + 0x000001a7,0x0004003d,0x00000006,0x000001a8,0x00000188,0x000500c7,0x00000006,0x000001a9, + 0x000001a8,0x00000108,0x0004003d,0x00000006,0x000001aa,0x00000188,0x000500c2,0x00000006, + 0x000001ab,0x000001aa,0x0000019f,0x0004003d,0x00000017,0x000001ac,0x00000138,0x0004003d, + 0x00000006,0x000001ad,0x0000017d,0x00060041,0x000000b1,0x000001ae,0x000000b0,0x0000018a, + 0x000001ad,0x0004003d,0x00000006,0x000001af,0x000001ae,0x00050086,0x00000006,0x000001b0, + 0x000001af,0x00000053,0x00050080,0x00000006,0x000001b1,0x000001b0,0x0000003e,0x00060041, + 0x0000007d,0x000001b2,0x000001ac,0x00000032,0x000001b1,0x0006003d,0x00000006,0x000001b3, + 0x000001b2,0x00000002,0x00000004,0x000500c7,0x00000006,0x000001b4,0x000001b3,0x00000108, + 0x0004003d,0x00000017,0x000001b5,0x00000138,0x0004003d,0x00000006,0x000001b6,0x0000017d, + 0x00060041,0x000000b1,0x000001b7,0x000000b0,0x0000018a,0x000001b6,0x0004003d,0x00000006, + 0x000001b8,0x000001b7,0x00050086,0x00000006,0x000001b9,0x000001b8,0x00000053,0x00050080, + 0x00000006,0x000001ba,0x000001b9,0x0000003e,0x00060041,0x0000007d,0x000001bb,0x000001b5, + 0x00000032,0x000001ba,0x0006003d,0x00000006,0x000001bc,0x000001bb,0x00000002,0x00000004, + 0x000500c2,0x00000006,0x000001bd,0x000001bc,0x0000019f,0x00070050,0x00000020,0x000001be, + 0x000001a9,0x000001ab,0x000001b4,0x000001bd,0x0003003e,0x00000198,0x000001be,0x000200f9, + 0x0000019a,0x000200f8,0x0000019a,0x0004003d,0x00000020,0x000001bf,0x00000198,0x0003003e, + 0x00000191,0x000001bf,0x0004003d,0x00000017,0x000001c1,0x00000138,0x0004003d,0x00000006, + 0x000001c3,0x0000017d,0x00060041,0x000000b1,0x000001c4,0x000000b0,0x000001c2,0x000001c3, + 0x0004003d,0x00000006,0x000001c5,0x000001c4,0x00050086,0x00000006,0x000001c6,0x000001c5, + 0x00000053,0x00060041,0x0000007d,0x000001c7,0x000001c1,0x00000032,0x000001c6,0x0006003d, + 0x00000006,0x000001c8,0x000001c7,0x00000002,0x00000004,0x0006000c,0x0000000d,0x000001c9, + 0x00000001,0x00000040,0x000001c8,0x0003003e,0x000001c0,0x000001c9,0x00060041,0x000000e9, + 0x000001cb,0x000000b0,0x000001ca,0x00000032,0x0004003d,0x000000a9,0x000001cc,0x000001cb, + 0x00040078,0x00000017,0x000001cd,0x000001cc,0x0003003e,0x000001ce,0x000001cd,0x0004003d, + 0x00000020,0x000001d0,0x00000191,0x0003003e,0x000001cf,0x000001d0,0x0004003d,0x0000000d, + 0x000001d2,0x000001c0,0x0003003e,0x000001d1,0x000001d2,0x0004003d,0x0000000d,0x000001d4, + 0x0000014c,0x0003003e,0x000001d3,0x000001d4,0x0004003d,0x00000023,0x000001d6,0x0000017a, + 0x0003003e,0x000001d5,0x000001d6,0x00090039,0x00000002,0x000001d7,0x0000002b,0x000001ce, + 0x000001cf,0x000001d1,0x000001d3,0x000001d5,0x0004003d,0x00000023,0x000001d8,0x000001d5, + 0x0003003e,0x0000017a,0x000001d8,0x00060041,0x000000e9,0x000001d9,0x000000b0,0x000001ca, + 0x000000dd,0x0004003d,0x000000a9,0x000001da,0x000001d9,0x00040078,0x00000017,0x000001db, + 0x000001da,0x0003003e,0x000001dc,0x000001db,0x0004003d,0x00000020,0x000001de,0x00000191, + 0x0003003e,0x000001dd,0x000001de,0x0004003d,0x0000000d,0x000001e0,0x000001c0,0x0003003e, + 0x000001df,0x000001e0,0x0004003d,0x0000000d,0x000001e2,0x0000014c,0x0003003e,0x000001e1, + 0x000001e2,0x0004003d,0x00000023,0x000001e4,0x0000017c,0x0003003e,0x000001e3,0x000001e4, + 0x00090039,0x00000002,0x000001e5,0x0000002b,0x000001dc,0x000001dd,0x000001df,0x000001e1, + 0x000001e3,0x0004003d,0x00000023,0x000001e6,0x000001e3,0x0003003e,0x0000017c,0x000001e6, + 0x000200f9,0x00000181,0x000200f8,0x00000181,0x0004003d,0x00000006,0x000001e7,0x0000017d, + 0x00050080,0x00000006,0x000001e8,0x000001e7,0x000000dd,0x0003003e,0x0000017d,0x000001e8, + 0x000200f9,0x0000017e,0x000200f8,0x00000180,0x00060041,0x000000e9,0x000001ea,0x000000b0, + 0x0000010d,0x00000032,0x0004003d,0x000000a9,0x000001eb,0x000001ea,0x00040078,0x00000017, + 0x000001ec,0x000001eb,0x0003003e,0x000001ed,0x000001ec,0x0003003e,0x000001ee,0x00000151, + 0x00060039,0x0000000d,0x000001ef,0x0000001e,0x000001ed,0x000001ee,0x0008004f,0x00000023, + 0x000001f0,0x000001ef,0x000001ef,0x00000000,0x00000001,0x00000002,0x0003003e,0x000001e9, + 0x000001f0,0x00060041,0x000000e9,0x000001f2,0x000000b0,0x0000010d,0x000000dd,0x0004003d, + 0x000000a9,0x000001f3,0x000001f2,0x00040078,0x00000017,0x000001f4,0x000001f3,0x0003003e, + 0x000001f5,0x000001f4,0x0003003e,0x000001f6,0x00000151,0x00060039,0x0000000d,0x000001f7, + 0x0000001e,0x000001f5,0x000001f6,0x0008004f,0x00000023,0x000001f8,0x000001f7,0x000001f7, + 0x00000000,0x00000001,0x00000002,0x0003003e,0x000001f1,0x000001f8,0x0003003e,0x000001fa, + 0x00000151,0x00050039,0x00000012,0x000001fb,0x00000015,0x000001fa,0x0004003d,0x00000023, + 0x000001fc,0x0000017a,0x0004003d,0x00000023,0x000001fd,0x000001e9,0x00050081,0x00000023, + 0x000001fe,0x000001fc,0x000001fd,0x00050051,0x00000008,0x000001ff,0x000001fe,0x00000000, + 0x00050051,0x00000008,0x00000200,0x000001fe,0x00000001,0x00050051,0x00000008,0x00000201, + 0x000001fe,0x00000002,0x00070050,0x0000000d,0x00000202,0x000001ff,0x00000200,0x00000201, + 0x00000061,0x00050091,0x0000000d,0x00000203,0x000001fb,0x00000202,0x0003003e,0x000001f9, + 0x00000203,0x0003003e,0x00000204,0x00000105,0x00050039,0x00000012,0x00000205,0x00000015, + 0x00000204,0x0004003d,0x00000023,0x00000206,0x0000017c,0x0004003d,0x00000023,0x00000207, + 0x000001f1,0x00050081,0x00000023,0x00000208,0x00000206,0x00000207,0x00050051,0x00000008, + 0x00000209,0x00000208,0x00000000,0x00050051,0x00000008,0x0000020a,0x00000208,0x00000001, + 0x00050051,0x00000008,0x0000020b,0x00000208,0x00000002,0x00070050,0x0000000d,0x0000020c, + 0x00000209,0x0000020a,0x0000020b,0x00000061,0x00050091,0x0000000d,0x0000020d,0x00000205, + 0x0000020c,0x0003003e,0x00000135,0x0000020d,0x00050041,0x0000009f,0x0000020e,0x000001f9, + 0x00000151,0x0004003d,0x00000008,0x0000020f,0x0000020e,0x00050041,0x0000009f,0x00000210, + 0x000001f9,0x0000003e,0x0004003d,0x00000008,0x00000211,0x00000210,0x0004007f,0x00000008, + 0x00000212,0x00000211,0x00050041,0x0000009f,0x00000213,0x000001f9,0x00000043,0x0004003d, + 0x00000008,0x00000214,0x00000213,0x00050041,0x0000009f,0x00000215,0x000001f9,0x00000048, + 0x0004003d,0x00000008,0x00000216,0x00000215,0x00050081,0x00000008,0x00000217,0x00000214, + 0x00000216,0x00050085,0x00000008,0x00000219,0x00000217,0x00000218,0x00050041,0x0000009f, + 0x0000021a,0x000001f9,0x00000048,0x0004003d,0x00000008,0x0000021b,0x0000021a,0x00070050, + 0x0000000d,0x0000021c,0x0000020f,0x00000212,0x00000219,0x0000021b,0x00050041,0x00000133, + 0x0000021d,0x00000130,0x00000032,0x0003003e,0x0000021d,0x0000021c,0x000100fd,0x00010038, + 0x00050036,0x00000008,0x0000000b,0x00000000,0x00000009,0x00030037,0x00000007,0x0000000a, + 0x000200f8,0x0000000c,0x0004003d,0x00000006,0x00000033,0x0000000a,0x00060041,0x00000034, + 0x00000035,0x00000030,0x00000032,0x00000033,0x0004003d,0x00000006,0x00000036,0x00000035, + 0x0004007c,0x00000008,0x00000037,0x00000036,0x000200fe,0x00000037,0x00010038,0x00050036, + 0x0000000d,0x00000010,0x00000000,0x0000000e,0x00030037,0x00000007,0x0000000f,0x000200f8, + 0x00000011,0x0004003b,0x00000007,0x0000003a,0x00000007,0x0004003b,0x00000007,0x00000040, + 0x00000007,0x0004003b,0x00000007,0x00000045,0x00000007,0x0004003b,0x00000007,0x0000004a, + 0x00000007,0x0004003d,0x00000006,0x0000003b,0x0000000f,0x0003003e,0x0000003a,0x0000003b, + 0x00050039,0x00000008,0x0000003c,0x0000000b,0x0000003a,0x0004003d,0x00000006,0x0000003d, + 0x0000000f,0x00050080,0x00000006,0x0000003f,0x0000003d,0x0000003e,0x0003003e,0x00000040, + 0x0000003f,0x00050039,0x00000008,0x00000041,0x0000000b,0x00000040,0x0004003d,0x00000006, + 0x00000042,0x0000000f,0x00050080,0x00000006,0x00000044,0x00000042,0x00000043,0x0003003e, + 0x00000045,0x00000044,0x00050039,0x00000008,0x00000046,0x0000000b,0x00000045,0x0004003d, + 0x00000006,0x00000047,0x0000000f,0x00050080,0x00000006,0x00000049,0x00000047,0x00000048, + 0x0003003e,0x0000004a,0x00000049,0x00050039,0x00000008,0x0000004b,0x0000000b,0x0000004a, + 0x00070050,0x0000000d,0x0000004c,0x0000003c,0x00000041,0x00000046,0x0000004b,0x000200fe, + 0x0000004c,0x00010038,0x00050036,0x00000012,0x00000015,0x00000000,0x00000013,0x00030037, + 0x00000007,0x00000014,0x000200f8,0x00000016,0x0004003b,0x00000007,0x0000004f,0x00000007, + 0x0004003b,0x00000007,0x00000055,0x00000007,0x0004003b,0x00000007,0x0000005a,0x00000007, + 0x0004003b,0x00000007,0x0000005f,0x00000007,0x0004003d,0x00000006,0x00000050,0x00000014, + 0x0003003e,0x0000004f,0x00000050,0x00050039,0x0000000d,0x00000051,0x00000010,0x0000004f, + 0x0004003d,0x00000006,0x00000052,0x00000014,0x00050080,0x00000006,0x00000054,0x00000052, + 0x00000053,0x0003003e,0x00000055,0x00000054,0x00050039,0x0000000d,0x00000056,0x00000010, + 0x00000055,0x0004003d,0x00000006,0x00000057,0x00000014,0x00050080,0x00000006,0x00000059, + 0x00000057,0x00000058,0x0003003e,0x0000005a,0x00000059,0x00050039,0x0000000d,0x0000005b, + 0x00000010,0x0000005a,0x0004003d,0x00000006,0x0000005c,0x00000014,0x00050080,0x00000006, + 0x0000005e,0x0000005c,0x0000005d,0x0003003e,0x0000005f,0x0000005e,0x00050039,0x0000000d, + 0x00000060,0x00000010,0x0000005f,0x00050051,0x00000008,0x00000063,0x00000051,0x00000000, + 0x00050051,0x00000008,0x00000064,0x00000051,0x00000001,0x00050051,0x00000008,0x00000065, + 0x00000051,0x00000002,0x00050051,0x00000008,0x00000066,0x00000051,0x00000003,0x00050051, + 0x00000008,0x00000067,0x00000056,0x00000000,0x00050051,0x00000008,0x00000068,0x00000056, + 0x00000001,0x00050051,0x00000008,0x00000069,0x00000056,0x00000002,0x00050051,0x00000008, + 0x0000006a,0x00000056,0x00000003,0x00050051,0x00000008,0x0000006b,0x0000005b,0x00000000, + 0x00050051,0x00000008,0x0000006c,0x0000005b,0x00000001,0x00050051,0x00000008,0x0000006d, + 0x0000005b,0x00000002,0x00050051,0x00000008,0x0000006e,0x0000005b,0x00000003,0x00050051, + 0x00000008,0x0000006f,0x00000060,0x00000000,0x00050051,0x00000008,0x00000070,0x00000060, + 0x00000001,0x00050051,0x00000008,0x00000071,0x00000060,0x00000002,0x00050051,0x00000008, + 0x00000072,0x00000060,0x00000003,0x00070050,0x0000000d,0x00000073,0x00000063,0x00000064, + 0x00000065,0x00000066,0x00070050,0x0000000d,0x00000074,0x00000067,0x00000068,0x00000069, + 0x0000006a,0x00070050,0x0000000d,0x00000075,0x0000006b,0x0000006c,0x0000006d,0x0000006e, + 0x00070050,0x0000000d,0x00000076,0x0000006f,0x00000070,0x00000071,0x00000072,0x00070050, + 0x00000012,0x00000077,0x00000073,0x00000074,0x00000075,0x00000076,0x00040054,0x00000012, + 0x00000078,0x00000077,0x000200fe,0x00000078,0x00010038,0x00050036,0x0000000d,0x0000001e, + 0x00000000,0x0000001b,0x00030037,0x0000001a,0x0000001c,0x00030037,0x00000007,0x0000001d, + 0x000200f8,0x0000001f,0x0004003d,0x00000017,0x0000007b,0x0000001c,0x0004003d,0x00000006, + 0x0000007c,0x0000001d,0x00060041,0x0000007d,0x0000007e,0x0000007b,0x00000032,0x0000007c, + 0x0006003d,0x00000006,0x0000007f,0x0000007e,0x00000002,0x00000004,0x0004003d,0x00000017, + 0x00000080,0x0000001c,0x0004003d,0x00000006,0x00000081,0x0000001d,0x00050080,0x00000006, + 0x00000082,0x00000081,0x0000003e,0x00060041,0x0000007d,0x00000083,0x00000080,0x00000032, + 0x00000082,0x0006003d,0x00000006,0x00000084,0x00000083,0x00000002,0x00000004,0x0004003d, + 0x00000017,0x00000085,0x0000001c,0x0004003d,0x00000006,0x00000086,0x0000001d,0x00050080, + 0x00000006,0x00000087,0x00000086,0x00000043,0x00060041,0x0000007d,0x00000088,0x00000085, + 0x00000032,0x00000087,0x0006003d,0x00000006,0x00000089,0x00000088,0x00000002,0x00000004, + 0x0004003d,0x00000017,0x0000008a,0x0000001c,0x0004003d,0x00000006,0x0000008b,0x0000001d, + 0x00050080,0x00000006,0x0000008c,0x0000008b,0x00000048,0x00060041,0x0000007d,0x0000008d, + 0x0000008a,0x00000032,0x0000008c,0x0006003d,0x00000006,0x0000008e,0x0000008d,0x00000002, + 0x00000004,0x00070050,0x00000020,0x0000008f,0x0000007f,0x00000084,0x00000089,0x0000008e, + 0x0004007c,0x0000000d,0x00000090,0x0000008f,0x000200fe,0x00000090,0x00010038,0x00050036, + 0x00000002,0x0000002b,0x00000000,0x00000025,0x00030037,0x0000001a,0x00000026,0x00030037, + 0x00000021,0x00000027,0x00030037,0x00000022,0x00000028,0x00030037,0x00000022,0x00000029, + 0x00030037,0x00000024,0x0000002a,0x000200f8,0x0000002c,0x0004003b,0x00000093,0x00000094, + 0x00000007,0x0004003b,0x00000007,0x000000b9,0x00000007,0x0004003b,0x0000001a,0x000000c1, + 0x00000007,0x0004003b,0x00000007,0x000000c3,0x00000007,0x0004003b,0x0000001a,0x000000ca, + 0x00000007,0x0004003b,0x00000007,0x000000cc,0x00000007,0x0004003b,0x0000001a,0x000000d2, + 0x00000007,0x0004003b,0x00000007,0x000000d4,0x00000007,0x0003003e,0x00000094,0x00000032, + 0x000200f9,0x00000095,0x000200f8,0x00000095,0x000400f6,0x00000097,0x00000098,0x00000000, + 0x000200f9,0x00000099,0x000200f8,0x00000099,0x0004003d,0x00000031,0x0000009a,0x00000094, + 0x000500b1,0x0000009c,0x0000009d,0x0000009a,0x0000009b,0x000400fa,0x0000009d,0x00000096, + 0x00000097,0x000200f8,0x00000096,0x0004003d,0x00000031,0x0000009e,0x00000094,0x00050041, + 0x0000009f,0x000000a0,0x00000028,0x0000009e,0x0004003d,0x00000008,0x000000a1,0x000000a0, + 0x000500bc,0x0000009c,0x000000a2,0x000000a1,0x00000062,0x000400a8,0x0000009c,0x000000a3, + 0x000000a2,0x000300f7,0x000000a5,0x00000000,0x000400fa,0x000000a3,0x000000a4,0x000000a5, + 0x000200f8,0x000000a4,0x0004003d,0x00000031,0x000000a6,0x00000094,0x00050041,0x00000007, + 0x000000a7,0x00000027,0x000000a6,0x0004003d,0x00000006,0x000000a8,0x000000a7,0x00050041, + 0x000000b1,0x000000b2,0x000000b0,0x0000009b,0x0004003d,0x00000006,0x000000b3,0x000000b2, + 0x000500ae,0x0000009c,0x000000b4,0x000000a8,0x000000b3,0x000200f9,0x000000a5,0x000200f8, + 0x000000a5,0x000700f5,0x0000009c,0x000000b5,0x000000a2,0x00000096,0x000000b4,0x000000a4, + 0x000300f7,0x000000b7,0x00000000,0x000400fa,0x000000b5,0x000000b6,0x000000b7,0x000200f8, + 0x000000b6,0x000200f9,0x00000098,0x000200f8,0x000000b7,0x0004003d,0x00000031,0x000000ba, + 0x00000094,0x00050041,0x00000007,0x000000bb,0x00000027,0x000000ba,0x0004003d,0x00000006, + 0x000000bc,0x000000bb,0x00050084,0x00000006,0x000000bd,0x000000bc,0x0000005d,0x0003003e, + 0x000000b9,0x000000bd,0x0004003d,0x00000031,0x000000be,0x00000094,0x00050041,0x0000009f, + 0x000000bf,0x00000028,0x000000be,0x0004003d,0x00000008,0x000000c0,0x000000bf,0x0004003d, + 0x00000017,0x000000c2,0x00000026,0x0003003e,0x000000c1,0x000000c2,0x0004003d,0x00000006, + 0x000000c4,0x000000b9,0x0003003e,0x000000c3,0x000000c4,0x00060039,0x0000000d,0x000000c5, + 0x0000001e,0x000000c1,0x000000c3,0x0004003d,0x0000000d,0x000000c6,0x00000029,0x00050094, + 0x00000008,0x000000c7,0x000000c5,0x000000c6,0x0004003d,0x00000006,0x000000c8,0x000000b9, + 0x00050080,0x00000006,0x000000c9,0x000000c8,0x00000053,0x0004003d,0x00000017,0x000000cb, + 0x00000026,0x0003003e,0x000000ca,0x000000cb,0x0003003e,0x000000cc,0x000000c9,0x00060039, + 0x0000000d,0x000000cd,0x0000001e,0x000000ca,0x000000cc,0x0004003d,0x0000000d,0x000000ce, + 0x00000029,0x00050094,0x00000008,0x000000cf,0x000000cd,0x000000ce,0x0004003d,0x00000006, + 0x000000d0,0x000000b9,0x00050080,0x00000006,0x000000d1,0x000000d0,0x00000058,0x0004003d, + 0x00000017,0x000000d3,0x00000026,0x0003003e,0x000000d2,0x000000d3,0x0003003e,0x000000d4, + 0x000000d1,0x00060039,0x0000000d,0x000000d5,0x0000001e,0x000000d2,0x000000d4,0x0004003d, + 0x0000000d,0x000000d6,0x00000029,0x00050094,0x00000008,0x000000d7,0x000000d5,0x000000d6, + 0x00060050,0x00000023,0x000000d8,0x000000c7,0x000000cf,0x000000d7,0x0005008e,0x00000023, + 0x000000d9,0x000000d8,0x000000c0,0x0004003d,0x00000023,0x000000da,0x0000002a,0x00050081, + 0x00000023,0x000000db,0x000000da,0x000000d9,0x0003003e,0x0000002a,0x000000db,0x000200f9, + 0x00000098,0x000200f8,0x00000098,0x0004003d,0x00000031,0x000000dc,0x00000094,0x00050080, + 0x00000031,0x000000de,0x000000dc,0x000000dd,0x0003003e,0x00000094,0x000000de,0x000200f9, + 0x00000095,0x000200f8,0x00000097,0x000100fd,0x00010038 +}; diff --git a/gpu/vk/shaders/dlss_out.comp b/gpu/vk/shaders/dlss_out.comp new file mode 100644 index 000000000..30114cfac --- /dev/null +++ b/gpu/vk/shaders/dlss_out.comp @@ -0,0 +1,17 @@ +#version 450 +// (hookshot) NVIDIA DLSS's picture into OoT's gameplay upscale's output (vk_dlss.cpp): what the network's own output +// does, the square of its perceptual encoding (linear colour again). DLAA's picture is at the render size: scaled up to the +// output's with the sampler's linear filter. +layout(local_size_x = 8, local_size_y = 8) in; +layout(binding = 0) uniform sampler2D src; +layout(binding = 1, r11f_g11f_b10f) uniform writeonly image2D dst; +layout(push_constant) uniform P { ivec2 size; } pc; +void main() { + const ivec2 g = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(g, pc.size))) return; + vec3 c = all(equal(textureSize(src, 0), pc.size)) ? texelFetch(src, g, 0).rgb + : textureLod(src, (vec2(g) + 0.5) / vec2(pc.size), 0.0).rgb; + c = max(c, vec3(0.0)); + c *= c; + imageStore(dst, g, vec4(c, 1.0)); +} diff --git a/gpu/vk/shaders/dlss_out_comp_spv.h b/gpu/vk/shaders/dlss_out_comp_spv.h new file mode 100644 index 000000000..56cb5a392 --- /dev/null +++ b/gpu/vk/shaders/dlss_out_comp_spv.h @@ -0,0 +1,72 @@ + // 1116.6.0 + #pragma once +const uint32_t DLSS_OUT_COMP_SPV[] = { + 0x07230203,0x00010600,0x0008000b,0x0000005d,0x00000000,0x00020011,0x00000001,0x00020011, + 0x00000031,0x00020011,0x00000032,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e, + 0x00000000,0x0003000e,0x00000000,0x00000001,0x0009000f,0x00000005,0x00000004,0x6e69616d, + 0x00000000,0x0000000d,0x00000015,0x00000028,0x00000051,0x00060010,0x00000004,0x00000011, + 0x00000008,0x00000008,0x00000001,0x00030003,0x00000002,0x000001c2,0x00040005,0x00000004, + 0x6e69616d,0x00000000,0x00030005,0x00000009,0x00000067,0x00080005,0x0000000d,0x475f6c67, + 0x61626f6c,0x766e496c,0x7461636f,0x496e6f69,0x00000044,0x00030005,0x00000013,0x00000050, + 0x00050006,0x00000013,0x00000000,0x657a6973,0x00000000,0x00030005,0x00000015,0x00006370, + 0x00030005,0x00000024,0x00000063,0x00030005,0x00000028,0x00637273,0x00030005,0x00000051, + 0x00747364,0x00040047,0x0000000d,0x0000000b,0x0000001c,0x00030047,0x00000013,0x00000002, + 0x00050048,0x00000013,0x00000000,0x00000023,0x00000000,0x00040047,0x00000028,0x00000021, + 0x00000000,0x00040047,0x00000028,0x00000022,0x00000000,0x00030047,0x00000051,0x00000019, + 0x00040047,0x00000051,0x00000021,0x00000001,0x00040047,0x00000051,0x00000022,0x00000000, + 0x00020013,0x00000002,0x00030021,0x00000003,0x00000002,0x00040015,0x00000006,0x00000020, + 0x00000001,0x00040017,0x00000007,0x00000006,0x00000002,0x00040020,0x00000008,0x00000007, + 0x00000007,0x00040015,0x0000000a,0x00000020,0x00000000,0x00040017,0x0000000b,0x0000000a, + 0x00000003,0x00040020,0x0000000c,0x00000001,0x0000000b,0x0004003b,0x0000000c,0x0000000d, + 0x00000001,0x00040017,0x0000000e,0x0000000a,0x00000002,0x0003001e,0x00000013,0x00000007, + 0x00040020,0x00000014,0x00000009,0x00000013,0x0004003b,0x00000014,0x00000015,0x00000009, + 0x0004002b,0x00000006,0x00000016,0x00000000,0x00040020,0x00000017,0x00000009,0x00000007, + 0x00020014,0x0000001a,0x00040017,0x0000001b,0x0000001a,0x00000002,0x00030016,0x00000021, + 0x00000020,0x00040017,0x00000022,0x00000021,0x00000003,0x00040020,0x00000023,0x00000007, + 0x00000022,0x00090019,0x00000025,0x00000021,0x00000001,0x00000000,0x00000000,0x00000000, + 0x00000001,0x00000000,0x0003001b,0x00000026,0x00000025,0x00040020,0x00000027,0x00000000, + 0x00000026,0x0004003b,0x00000027,0x00000028,0x00000000,0x00040017,0x00000036,0x00000021, + 0x00000004,0x00040017,0x0000003c,0x00000021,0x00000002,0x0004002b,0x00000021,0x0000003e, + 0x3f000000,0x0004002b,0x00000021,0x00000045,0x00000000,0x0006002c,0x00000022,0x0000004a, + 0x00000045,0x00000045,0x00000045,0x00090019,0x0000004f,0x00000021,0x00000001,0x00000000, + 0x00000000,0x00000000,0x00000002,0x00000008,0x00040020,0x00000050,0x00000000,0x0000004f, + 0x0004003b,0x00000050,0x00000051,0x00000000,0x0004002b,0x00000021,0x00000055,0x3f800000, + 0x0004002b,0x0000000a,0x0000005a,0x00000008,0x0004002b,0x0000000a,0x0000005b,0x00000001, + 0x0006002c,0x0000000b,0x0000005c,0x0000005a,0x0000005a,0x0000005b,0x00050036,0x00000002, + 0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008,0x00000009, + 0x00000007,0x0004003b,0x00000023,0x00000024,0x00000007,0x0004003b,0x00000023,0x00000030, + 0x00000007,0x0004003d,0x0000000b,0x0000000f,0x0000000d,0x0007004f,0x0000000e,0x00000010, + 0x0000000f,0x0000000f,0x00000000,0x00000001,0x0004007c,0x00000007,0x00000011,0x00000010, + 0x0003003e,0x00000009,0x00000011,0x0004003d,0x00000007,0x00000012,0x00000009,0x00050041, + 0x00000017,0x00000018,0x00000015,0x00000016,0x0004003d,0x00000007,0x00000019,0x00000018, + 0x000500af,0x0000001b,0x0000001c,0x00000012,0x00000019,0x0004009a,0x0000001a,0x0000001d, + 0x0000001c,0x000300f7,0x0000001f,0x00000000,0x000400fa,0x0000001d,0x0000001e,0x0000001f, + 0x000200f8,0x0000001e,0x000100fd,0x000200f8,0x0000001f,0x0004003d,0x00000026,0x00000029, + 0x00000028,0x00040064,0x00000025,0x0000002a,0x00000029,0x00050067,0x00000007,0x0000002b, + 0x0000002a,0x00000016,0x00050041,0x00000017,0x0000002c,0x00000015,0x00000016,0x0004003d, + 0x00000007,0x0000002d,0x0000002c,0x000500aa,0x0000001b,0x0000002e,0x0000002b,0x0000002d, + 0x0004009b,0x0000001a,0x0000002f,0x0000002e,0x000300f7,0x00000032,0x00000000,0x000400fa, + 0x0000002f,0x00000031,0x00000039,0x000200f8,0x00000031,0x0004003d,0x00000026,0x00000033, + 0x00000028,0x0004003d,0x00000007,0x00000034,0x00000009,0x00040064,0x00000025,0x00000035, + 0x00000033,0x0007005f,0x00000036,0x00000037,0x00000035,0x00000034,0x00000002,0x00000016, + 0x0008004f,0x00000022,0x00000038,0x00000037,0x00000037,0x00000000,0x00000001,0x00000002, + 0x0003003e,0x00000030,0x00000038,0x000200f9,0x00000032,0x000200f8,0x00000039,0x0004003d, + 0x00000026,0x0000003a,0x00000028,0x0004003d,0x00000007,0x0000003b,0x00000009,0x0004006f, + 0x0000003c,0x0000003d,0x0000003b,0x00050050,0x0000003c,0x0000003f,0x0000003e,0x0000003e, + 0x00050081,0x0000003c,0x00000040,0x0000003d,0x0000003f,0x00050041,0x00000017,0x00000041, + 0x00000015,0x00000016,0x0004003d,0x00000007,0x00000042,0x00000041,0x0004006f,0x0000003c, + 0x00000043,0x00000042,0x00050088,0x0000003c,0x00000044,0x00000040,0x00000043,0x00070058, + 0x00000036,0x00000046,0x0000003a,0x00000044,0x00000002,0x00000045,0x0008004f,0x00000022, + 0x00000047,0x00000046,0x00000046,0x00000000,0x00000001,0x00000002,0x0003003e,0x00000030, + 0x00000047,0x000200f9,0x00000032,0x000200f8,0x00000032,0x0004003d,0x00000022,0x00000048, + 0x00000030,0x0003003e,0x00000024,0x00000048,0x0004003d,0x00000022,0x00000049,0x00000024, + 0x0007000c,0x00000022,0x0000004b,0x00000001,0x00000028,0x00000049,0x0000004a,0x0003003e, + 0x00000024,0x0000004b,0x0004003d,0x00000022,0x0000004c,0x00000024,0x0004003d,0x00000022, + 0x0000004d,0x00000024,0x00050085,0x00000022,0x0000004e,0x0000004d,0x0000004c,0x0003003e, + 0x00000024,0x0000004e,0x0004003d,0x0000004f,0x00000052,0x00000051,0x0004003d,0x00000007, + 0x00000053,0x00000009,0x0004003d,0x00000022,0x00000054,0x00000024,0x00050051,0x00000021, + 0x00000056,0x00000054,0x00000000,0x00050051,0x00000021,0x00000057,0x00000054,0x00000001, + 0x00050051,0x00000021,0x00000058,0x00000054,0x00000002,0x00070050,0x00000036,0x00000059, + 0x00000056,0x00000057,0x00000058,0x00000055,0x00040063,0x00000052,0x00000053,0x00000059, + 0x000100fd,0x00010038 +}; diff --git a/gpu/vk/shaders/dlss_prep.comp b/gpu/vk/shaders/dlss_prep.comp new file mode 100644 index 000000000..997597065 --- /dev/null +++ b/gpu/vk/shaders/dlss_prep.comp @@ -0,0 +1,52 @@ +#version 450 +// (hookshot) NVIDIA DLSS's inputs for OoT's gameplay upscale (vk_dlss.cpp), at the render size, in one pass: +// - colour: the network's input as it samples it, the square root of the scene colour (tonemapped, perceptual): DLSS +// takes it as LDR colour +// - depth: the scene's depth as a plain float image +// - motion: where each pixel was in the frame before, in render pixels. The game has no motion vectors: the scene depth +// is taken back to clip space and through the frame before's camera (pc.reproject: this frame's clip space to the +// frame before's, from the camera's two matrices, its jitter taken off). Only the camera's motion: what moves by itself gets +// the motion of what is behind it, until vk_dlss.cpp's actor motion draws the skinned draws' own over it (dlss_actor.vert). pc.flags bit 0: motion (none on the first frame); +// bit 1: the motion shown as the colour instead (RX_VK_DLSS_DEBUG=motion); bit 2: only that, from the motion image as it is +// then (after vk_dlss.cpp's actor motion drew into it: dlss_actor.vert) +// The game's clip space: depth from -1 to 1 (NVN's default), y up, the picture's rows top down. +layout(local_size_x = 8, local_size_y = 8) in; +layout(binding = 0) uniform sampler2D scene; +layout(binding = 1) uniform sampler2D scene_depth; +layout(binding = 2, rgba16f) uniform writeonly image2D color; +layout(binding = 3, r32f) uniform writeonly image2D depth; +layout(binding = 4, rg16f) uniform image2D motion; +layout(push_constant, row_major) uniform P { + mat4 reproject; // (this frame's jittered clip space to the frame before's without its jitter) + vec2 jitter; // (this frame's jitter in clip space: taken off, so the motion has none) + ivec2 size; + int flags; +} pc; +void main() { + const ivec2 g = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(g, pc.size))) return; + const vec2 size = vec2(pc.size); + const vec2 uv = (vec2(g) + 0.5) / size; + if ((pc.flags & 4) != 0) { + const vec2 m = imageLoad(motion, g).xy; + imageStore(color, g, vec4(clamp(abs(m) / 16.0, 0.0, 1.0), textureLod(scene_depth, uv, 0.0).r, 1.0)); + return; + } + const vec3 c = textureLod(scene, uv, 0.0).rgb; + imageStore(color, g, vec4(clamp(c, 0.0, 1.0), 1.0)); + const float d = textureLod(scene_depth, uv, 0.0).r; + imageStore(depth, g, vec4(d)); + vec2 mv = vec2(0.0); + if ((pc.flags & 1) != 0 && d >= 0.0 && d <= 1.0) { + const vec4 clip = vec4(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, d * 2.0 - 1.0, 1.0); + const vec4 before = pc.reproject * clip; + if (before.w > 1e-6) { + const vec2 then = before.xy / before.w; + const vec2 now = clip.xy - pc.jitter; + const vec2 px = vec2(then.x - now.x, now.y - then.y) * 0.5 * size; + if (all(lessThan(abs(px), vec2(16384.0)))) mv = px; + } + } + imageStore(motion, g, vec4(mv, 0.0, 0.0)); + if ((pc.flags & 2) != 0) imageStore(color, g, vec4(clamp(abs(mv) / 16.0, 0.0, 1.0), d, 1.0)); +} diff --git a/gpu/vk/shaders/dlss_prep_comp_spv.h b/gpu/vk/shaders/dlss_prep_comp_spv.h new file mode 100644 index 000000000..fcd1b77ab --- /dev/null +++ b/gpu/vk/shaders/dlss_prep_comp_spv.h @@ -0,0 +1,178 @@ + // 1116.6.0 + #pragma once +const uint32_t DLSS_PREP_COMP_SPV[] = { + 0x07230203,0x00010600,0x0008000b,0x000000ec,0x00000000,0x00020011,0x00000001,0x00020011, + 0x00000031,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e, + 0x00000000,0x00000001,0x000c000f,0x00000005,0x00000004,0x6e69616d,0x00000000,0x0000000d, + 0x00000019,0x0000003f,0x00000046,0x00000056,0x00000063,0x0000007a,0x00060010,0x00000004, + 0x00000011,0x00000008,0x00000008,0x00000001,0x00030003,0x00000002,0x000001c2,0x00040005, + 0x00000004,0x6e69616d,0x00000000,0x00030005,0x00000009,0x00000067,0x00080005,0x0000000d, + 0x475f6c67,0x61626f6c,0x766e496c,0x7461636f,0x496e6f69,0x00000044,0x00030005,0x00000017, + 0x00000050,0x00060006,0x00000017,0x00000000,0x72706572,0x63656a6f,0x00000074,0x00050006, + 0x00000017,0x00000001,0x7474696a,0x00007265,0x00050006,0x00000017,0x00000002,0x657a6973, + 0x00000000,0x00050006,0x00000017,0x00000003,0x67616c66,0x00000073,0x00030005,0x00000019, + 0x00006370,0x00040005,0x00000026,0x657a6973,0x00000000,0x00030005,0x0000002a,0x00007675, + 0x00030005,0x0000003c,0x0000006d,0x00040005,0x0000003f,0x69746f6d,0x00006e6f,0x00040005, + 0x00000046,0x6f6c6f63,0x00000072,0x00050005,0x00000056,0x6e656373,0x65645f65,0x00687470, + 0x00030005,0x00000062,0x00000063,0x00040005,0x00000063,0x6e656373,0x00000065,0x00030005, + 0x00000073,0x00000064,0x00040005,0x0000007a,0x74706564,0x00000068,0x00030005,0x0000007f, + 0x0000766d,0x00040005,0x0000008f,0x70696c63,0x00000000,0x00040005,0x0000009e,0x6f666562, + 0x00006572,0x00040005,0x000000ab,0x6e656874,0x00000000,0x00030005,0x000000b2,0x00776f6e, + 0x00030005,0x000000b9,0x00007870,0x00040047,0x0000000d,0x0000000b,0x0000001c,0x00030047, + 0x00000017,0x00000002,0x00040048,0x00000017,0x00000000,0x00000004,0x00050048,0x00000017, + 0x00000000,0x00000007,0x00000010,0x00050048,0x00000017,0x00000000,0x00000023,0x00000000, + 0x00050048,0x00000017,0x00000001,0x00000023,0x00000040,0x00050048,0x00000017,0x00000002, + 0x00000023,0x00000048,0x00050048,0x00000017,0x00000003,0x00000023,0x00000050,0x00040047, + 0x0000003f,0x00000021,0x00000004,0x00040047,0x0000003f,0x00000022,0x00000000,0x00030047, + 0x00000046,0x00000019,0x00040047,0x00000046,0x00000021,0x00000002,0x00040047,0x00000046, + 0x00000022,0x00000000,0x00040047,0x00000056,0x00000021,0x00000001,0x00040047,0x00000056, + 0x00000022,0x00000000,0x00040047,0x00000063,0x00000021,0x00000000,0x00040047,0x00000063, + 0x00000022,0x00000000,0x00030047,0x0000007a,0x00000019,0x00040047,0x0000007a,0x00000021, + 0x00000003,0x00040047,0x0000007a,0x00000022,0x00000000,0x00020013,0x00000002,0x00030021, + 0x00000003,0x00000002,0x00040015,0x00000006,0x00000020,0x00000001,0x00040017,0x00000007, + 0x00000006,0x00000002,0x00040020,0x00000008,0x00000007,0x00000007,0x00040015,0x0000000a, + 0x00000020,0x00000000,0x00040017,0x0000000b,0x0000000a,0x00000003,0x00040020,0x0000000c, + 0x00000001,0x0000000b,0x0004003b,0x0000000c,0x0000000d,0x00000001,0x00040017,0x0000000e, + 0x0000000a,0x00000002,0x00030016,0x00000013,0x00000020,0x00040017,0x00000014,0x00000013, + 0x00000004,0x00040018,0x00000015,0x00000014,0x00000004,0x00040017,0x00000016,0x00000013, + 0x00000002,0x0006001e,0x00000017,0x00000015,0x00000016,0x00000007,0x00000006,0x00040020, + 0x00000018,0x00000009,0x00000017,0x0004003b,0x00000018,0x00000019,0x00000009,0x0004002b, + 0x00000006,0x0000001a,0x00000002,0x00040020,0x0000001b,0x00000009,0x00000007,0x00020014, + 0x0000001e,0x00040017,0x0000001f,0x0000001e,0x00000002,0x00040020,0x00000025,0x00000007, + 0x00000016,0x0004002b,0x00000013,0x0000002d,0x3f000000,0x0004002b,0x00000006,0x00000032, + 0x00000003,0x00040020,0x00000033,0x00000009,0x00000006,0x0004002b,0x00000006,0x00000036, + 0x00000004,0x0004002b,0x00000006,0x00000038,0x00000000,0x00090019,0x0000003d,0x00000013, + 0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000007,0x00040020,0x0000003e, + 0x00000000,0x0000003d,0x0004003b,0x0000003e,0x0000003f,0x00000000,0x00090019,0x00000044, + 0x00000013,0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000002,0x00040020, + 0x00000045,0x00000000,0x00000044,0x0004003b,0x00000045,0x00000046,0x00000000,0x0004002b, + 0x00000013,0x0000004b,0x41800000,0x0004002b,0x00000013,0x0000004e,0x00000000,0x0004002b, + 0x00000013,0x0000004f,0x3f800000,0x00090019,0x00000053,0x00000013,0x00000001,0x00000000, + 0x00000000,0x00000000,0x00000001,0x00000000,0x0003001b,0x00000054,0x00000053,0x00040020, + 0x00000055,0x00000000,0x00000054,0x0004003b,0x00000055,0x00000056,0x00000000,0x0004002b, + 0x0000000a,0x0000005a,0x00000000,0x00040017,0x00000060,0x00000013,0x00000003,0x00040020, + 0x00000061,0x00000007,0x00000060,0x0004003b,0x00000055,0x00000063,0x00000000,0x00040020, + 0x00000072,0x00000007,0x00000013,0x00090019,0x00000078,0x00000013,0x00000001,0x00000000, + 0x00000000,0x00000000,0x00000002,0x00000003,0x00040020,0x00000079,0x00000000,0x00000078, + 0x0004003b,0x00000079,0x0000007a,0x00000000,0x0005002c,0x00000016,0x00000080,0x0000004e, + 0x0000004e,0x0004002b,0x00000006,0x00000083,0x00000001,0x00040020,0x0000008e,0x00000007, + 0x00000014,0x0004002b,0x00000013,0x00000092,0x40000000,0x0004002b,0x0000000a,0x00000095, + 0x00000001,0x00040020,0x0000009f,0x00000009,0x00000015,0x0004002b,0x0000000a,0x000000a4, + 0x00000003,0x0004002b,0x00000013,0x000000a7,0x358637bd,0x00040020,0x000000b5,0x00000009, + 0x00000016,0x0004002b,0x00000013,0x000000ca,0x46800000,0x0005002c,0x00000016,0x000000cb, + 0x000000ca,0x000000ca,0x0004002b,0x0000000a,0x000000ea,0x00000008,0x0006002c,0x0000000b, + 0x000000eb,0x000000ea,0x000000ea,0x00000095,0x00050036,0x00000002,0x00000004,0x00000000, + 0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008,0x00000009,0x00000007,0x0004003b, + 0x00000025,0x00000026,0x00000007,0x0004003b,0x00000025,0x0000002a,0x00000007,0x0004003b, + 0x00000025,0x0000003c,0x00000007,0x0004003b,0x00000061,0x00000062,0x00000007,0x0004003b, + 0x00000072,0x00000073,0x00000007,0x0004003b,0x00000025,0x0000007f,0x00000007,0x0004003b, + 0x0000008e,0x0000008f,0x00000007,0x0004003b,0x0000008e,0x0000009e,0x00000007,0x0004003b, + 0x00000025,0x000000ab,0x00000007,0x0004003b,0x00000025,0x000000b2,0x00000007,0x0004003b, + 0x00000025,0x000000b9,0x00000007,0x0004003d,0x0000000b,0x0000000f,0x0000000d,0x0007004f, + 0x0000000e,0x00000010,0x0000000f,0x0000000f,0x00000000,0x00000001,0x0004007c,0x00000007, + 0x00000011,0x00000010,0x0003003e,0x00000009,0x00000011,0x0004003d,0x00000007,0x00000012, + 0x00000009,0x00050041,0x0000001b,0x0000001c,0x00000019,0x0000001a,0x0004003d,0x00000007, + 0x0000001d,0x0000001c,0x000500af,0x0000001f,0x00000020,0x00000012,0x0000001d,0x0004009a, + 0x0000001e,0x00000021,0x00000020,0x000300f7,0x00000023,0x00000000,0x000400fa,0x00000021, + 0x00000022,0x00000023,0x000200f8,0x00000022,0x000100fd,0x000200f8,0x00000023,0x00050041, + 0x0000001b,0x00000027,0x00000019,0x0000001a,0x0004003d,0x00000007,0x00000028,0x00000027, + 0x0004006f,0x00000016,0x00000029,0x00000028,0x0003003e,0x00000026,0x00000029,0x0004003d, + 0x00000007,0x0000002b,0x00000009,0x0004006f,0x00000016,0x0000002c,0x0000002b,0x00050050, + 0x00000016,0x0000002e,0x0000002d,0x0000002d,0x00050081,0x00000016,0x0000002f,0x0000002c, + 0x0000002e,0x0004003d,0x00000016,0x00000030,0x00000026,0x00050088,0x00000016,0x00000031, + 0x0000002f,0x00000030,0x0003003e,0x0000002a,0x00000031,0x00050041,0x00000033,0x00000034, + 0x00000019,0x00000032,0x0004003d,0x00000006,0x00000035,0x00000034,0x000500c7,0x00000006, + 0x00000037,0x00000035,0x00000036,0x000500ab,0x0000001e,0x00000039,0x00000037,0x00000038, + 0x000300f7,0x0000003b,0x00000000,0x000400fa,0x00000039,0x0000003a,0x0000003b,0x000200f8, + 0x0000003a,0x0004003d,0x0000003d,0x00000040,0x0000003f,0x0004003d,0x00000007,0x00000041, + 0x00000009,0x00050062,0x00000014,0x00000042,0x00000040,0x00000041,0x0007004f,0x00000016, + 0x00000043,0x00000042,0x00000042,0x00000000,0x00000001,0x0003003e,0x0000003c,0x00000043, + 0x0004003d,0x00000044,0x00000047,0x00000046,0x0004003d,0x00000007,0x00000048,0x00000009, + 0x0004003d,0x00000016,0x00000049,0x0000003c,0x0006000c,0x00000016,0x0000004a,0x00000001, + 0x00000004,0x00000049,0x00050050,0x00000016,0x0000004c,0x0000004b,0x0000004b,0x00050088, + 0x00000016,0x0000004d,0x0000004a,0x0000004c,0x00050050,0x00000016,0x00000050,0x0000004e, + 0x0000004e,0x00050050,0x00000016,0x00000051,0x0000004f,0x0000004f,0x0008000c,0x00000016, + 0x00000052,0x00000001,0x0000002b,0x0000004d,0x00000050,0x00000051,0x0004003d,0x00000054, + 0x00000057,0x00000056,0x0004003d,0x00000016,0x00000058,0x0000002a,0x00070058,0x00000014, + 0x00000059,0x00000057,0x00000058,0x00000002,0x0000004e,0x00050051,0x00000013,0x0000005b, + 0x00000059,0x00000000,0x00050051,0x00000013,0x0000005c,0x00000052,0x00000000,0x00050051, + 0x00000013,0x0000005d,0x00000052,0x00000001,0x00070050,0x00000014,0x0000005e,0x0000005c, + 0x0000005d,0x0000005b,0x0000004f,0x00040063,0x00000047,0x00000048,0x0000005e,0x000100fd, + 0x000200f8,0x0000003b,0x0004003d,0x00000054,0x00000064,0x00000063,0x0004003d,0x00000016, + 0x00000065,0x0000002a,0x00070058,0x00000014,0x00000066,0x00000064,0x00000065,0x00000002, + 0x0000004e,0x0008004f,0x00000060,0x00000067,0x00000066,0x00000066,0x00000000,0x00000001, + 0x00000002,0x0003003e,0x00000062,0x00000067,0x0004003d,0x00000044,0x00000068,0x00000046, + 0x0004003d,0x00000007,0x00000069,0x00000009,0x0004003d,0x00000060,0x0000006a,0x00000062, + 0x00060050,0x00000060,0x0000006b,0x0000004e,0x0000004e,0x0000004e,0x00060050,0x00000060, + 0x0000006c,0x0000004f,0x0000004f,0x0000004f,0x0008000c,0x00000060,0x0000006d,0x00000001, + 0x0000002b,0x0000006a,0x0000006b,0x0000006c,0x00050051,0x00000013,0x0000006e,0x0000006d, + 0x00000000,0x00050051,0x00000013,0x0000006f,0x0000006d,0x00000001,0x00050051,0x00000013, + 0x00000070,0x0000006d,0x00000002,0x00070050,0x00000014,0x00000071,0x0000006e,0x0000006f, + 0x00000070,0x0000004f,0x00040063,0x00000068,0x00000069,0x00000071,0x0004003d,0x00000054, + 0x00000074,0x00000056,0x0004003d,0x00000016,0x00000075,0x0000002a,0x00070058,0x00000014, + 0x00000076,0x00000074,0x00000075,0x00000002,0x0000004e,0x00050051,0x00000013,0x00000077, + 0x00000076,0x00000000,0x0003003e,0x00000073,0x00000077,0x0004003d,0x00000078,0x0000007b, + 0x0000007a,0x0004003d,0x00000007,0x0000007c,0x00000009,0x0004003d,0x00000013,0x0000007d, + 0x00000073,0x00070050,0x00000014,0x0000007e,0x0000007d,0x0000007d,0x0000007d,0x0000007d, + 0x00040063,0x0000007b,0x0000007c,0x0000007e,0x0003003e,0x0000007f,0x00000080,0x00050041, + 0x00000033,0x00000081,0x00000019,0x00000032,0x0004003d,0x00000006,0x00000082,0x00000081, + 0x000500c7,0x00000006,0x00000084,0x00000082,0x00000083,0x000500ab,0x0000001e,0x00000085, + 0x00000084,0x00000038,0x0004003d,0x00000013,0x00000086,0x00000073,0x000500be,0x0000001e, + 0x00000087,0x00000086,0x0000004e,0x000500a7,0x0000001e,0x00000088,0x00000085,0x00000087, + 0x0004003d,0x00000013,0x00000089,0x00000073,0x000500bc,0x0000001e,0x0000008a,0x00000089, + 0x0000004f,0x000500a7,0x0000001e,0x0000008b,0x00000088,0x0000008a,0x000300f7,0x0000008d, + 0x00000000,0x000400fa,0x0000008b,0x0000008c,0x0000008d,0x000200f8,0x0000008c,0x00050041, + 0x00000072,0x00000090,0x0000002a,0x0000005a,0x0004003d,0x00000013,0x00000091,0x00000090, + 0x00050085,0x00000013,0x00000093,0x00000091,0x00000092,0x00050083,0x00000013,0x00000094, + 0x00000093,0x0000004f,0x00050041,0x00000072,0x00000096,0x0000002a,0x00000095,0x0004003d, + 0x00000013,0x00000097,0x00000096,0x00050085,0x00000013,0x00000098,0x00000097,0x00000092, + 0x00050083,0x00000013,0x00000099,0x0000004f,0x00000098,0x0004003d,0x00000013,0x0000009a, + 0x00000073,0x00050085,0x00000013,0x0000009b,0x0000009a,0x00000092,0x00050083,0x00000013, + 0x0000009c,0x0000009b,0x0000004f,0x00070050,0x00000014,0x0000009d,0x00000094,0x00000099, + 0x0000009c,0x0000004f,0x0003003e,0x0000008f,0x0000009d,0x00050041,0x0000009f,0x000000a0, + 0x00000019,0x00000038,0x0004003d,0x00000015,0x000000a1,0x000000a0,0x0004003d,0x00000014, + 0x000000a2,0x0000008f,0x00050091,0x00000014,0x000000a3,0x000000a1,0x000000a2,0x0003003e, + 0x0000009e,0x000000a3,0x00050041,0x00000072,0x000000a5,0x0000009e,0x000000a4,0x0004003d, + 0x00000013,0x000000a6,0x000000a5,0x000500ba,0x0000001e,0x000000a8,0x000000a6,0x000000a7, + 0x000300f7,0x000000aa,0x00000000,0x000400fa,0x000000a8,0x000000a9,0x000000aa,0x000200f8, + 0x000000a9,0x0004003d,0x00000014,0x000000ac,0x0000009e,0x0007004f,0x00000016,0x000000ad, + 0x000000ac,0x000000ac,0x00000000,0x00000001,0x00050041,0x00000072,0x000000ae,0x0000009e, + 0x000000a4,0x0004003d,0x00000013,0x000000af,0x000000ae,0x00050050,0x00000016,0x000000b0, + 0x000000af,0x000000af,0x00050088,0x00000016,0x000000b1,0x000000ad,0x000000b0,0x0003003e, + 0x000000ab,0x000000b1,0x0004003d,0x00000014,0x000000b3,0x0000008f,0x0007004f,0x00000016, + 0x000000b4,0x000000b3,0x000000b3,0x00000000,0x00000001,0x00050041,0x000000b5,0x000000b6, + 0x00000019,0x00000083,0x0004003d,0x00000016,0x000000b7,0x000000b6,0x00050083,0x00000016, + 0x000000b8,0x000000b4,0x000000b7,0x0003003e,0x000000b2,0x000000b8,0x00050041,0x00000072, + 0x000000ba,0x000000ab,0x0000005a,0x0004003d,0x00000013,0x000000bb,0x000000ba,0x00050041, + 0x00000072,0x000000bc,0x000000b2,0x0000005a,0x0004003d,0x00000013,0x000000bd,0x000000bc, + 0x00050083,0x00000013,0x000000be,0x000000bb,0x000000bd,0x00050041,0x00000072,0x000000bf, + 0x000000b2,0x00000095,0x0004003d,0x00000013,0x000000c0,0x000000bf,0x00050041,0x00000072, + 0x000000c1,0x000000ab,0x00000095,0x0004003d,0x00000013,0x000000c2,0x000000c1,0x00050083, + 0x00000013,0x000000c3,0x000000c0,0x000000c2,0x00050050,0x00000016,0x000000c4,0x000000be, + 0x000000c3,0x0005008e,0x00000016,0x000000c5,0x000000c4,0x0000002d,0x0004003d,0x00000016, + 0x000000c6,0x00000026,0x00050085,0x00000016,0x000000c7,0x000000c5,0x000000c6,0x0003003e, + 0x000000b9,0x000000c7,0x0004003d,0x00000016,0x000000c8,0x000000b9,0x0006000c,0x00000016, + 0x000000c9,0x00000001,0x00000004,0x000000c8,0x000500b8,0x0000001f,0x000000cc,0x000000c9, + 0x000000cb,0x0004009b,0x0000001e,0x000000cd,0x000000cc,0x000300f7,0x000000cf,0x00000000, + 0x000400fa,0x000000cd,0x000000ce,0x000000cf,0x000200f8,0x000000ce,0x0004003d,0x00000016, + 0x000000d0,0x000000b9,0x0003003e,0x0000007f,0x000000d0,0x000200f9,0x000000cf,0x000200f8, + 0x000000cf,0x000200f9,0x000000aa,0x000200f8,0x000000aa,0x000200f9,0x0000008d,0x000200f8, + 0x0000008d,0x0004003d,0x0000003d,0x000000d1,0x0000003f,0x0004003d,0x00000007,0x000000d2, + 0x00000009,0x0004003d,0x00000016,0x000000d3,0x0000007f,0x00050051,0x00000013,0x000000d4, + 0x000000d3,0x00000000,0x00050051,0x00000013,0x000000d5,0x000000d3,0x00000001,0x00070050, + 0x00000014,0x000000d6,0x000000d4,0x000000d5,0x0000004e,0x0000004e,0x00040063,0x000000d1, + 0x000000d2,0x000000d6,0x00050041,0x00000033,0x000000d7,0x00000019,0x00000032,0x0004003d, + 0x00000006,0x000000d8,0x000000d7,0x000500c7,0x00000006,0x000000d9,0x000000d8,0x0000001a, + 0x000500ab,0x0000001e,0x000000da,0x000000d9,0x00000038,0x000300f7,0x000000dc,0x00000000, + 0x000400fa,0x000000da,0x000000db,0x000000dc,0x000200f8,0x000000db,0x0004003d,0x00000044, + 0x000000dd,0x00000046,0x0004003d,0x00000007,0x000000de,0x00000009,0x0004003d,0x00000016, + 0x000000df,0x0000007f,0x0006000c,0x00000016,0x000000e0,0x00000001,0x00000004,0x000000df, + 0x00050050,0x00000016,0x000000e1,0x0000004b,0x0000004b,0x00050088,0x00000016,0x000000e2, + 0x000000e0,0x000000e1,0x00050050,0x00000016,0x000000e3,0x0000004e,0x0000004e,0x00050050, + 0x00000016,0x000000e4,0x0000004f,0x0000004f,0x0008000c,0x00000016,0x000000e5,0x00000001, + 0x0000002b,0x000000e2,0x000000e3,0x000000e4,0x0004003d,0x00000013,0x000000e6,0x00000073, + 0x00050051,0x00000013,0x000000e7,0x000000e5,0x00000000,0x00050051,0x00000013,0x000000e8, + 0x000000e5,0x00000001,0x00070050,0x00000014,0x000000e9,0x000000e7,0x000000e8,0x000000e6, + 0x0000004f,0x00040063,0x000000dd,0x000000de,0x000000e9,0x000200f9,0x000000dc,0x000200f8, + 0x000000dc,0x000100fd,0x00010038 +}; diff --git a/gpu/vk/status_line.h b/gpu/vk/status_line.h new file mode 100644 index 000000000..0fec42361 --- /dev/null +++ b/gpu/vk/status_line.h @@ -0,0 +1,43 @@ +/* The renderer's status line (vk_replay.cpp status_tick): every 600 of the game's frames one line of what the video memory + holds, with DLSS's details when it draws, and the warning when the use passes what the driver allows. Plain functions: + tests/test_status_line.cpp checks them. */ +#pragma once + +#include +#include + +namespace mkwvk::status_line { + +constexpr int kEvery = 600; // (the game's frames between two status lines) +constexpr int kCheckEvery = 60; // (and between two looks at the allowance, for the warning) + +inline double gb(uint64_t bytes) { return bytes / (1024.0 * 1024.0 * 1024.0); } + +/// The video memory part: "VRAM 7.1 of 8.0 GB" (used of what the driver allows the game, VK_EXT_memory_budget), or without +/// a budget from the driver what the game's own allocations hold +inline int vram_text(char* b, size_t n, uint64_t used, uint64_t allowed, bool budget_known) { + if (budget_known && allowed) return snprintf(b, n, "VRAM %.1f of %.1f GB", gb(used), gb(allowed)); + return snprintf(b, n, "VRAM %.1f GB in the game's allocations (no budget from the driver)", gb(used)); +} + +/// The warning's once and again: true the first time a reading is over the allowance, then not until a reading has been +/// back under it (over: the state between readings). Nothing known (allowed 0): under +inline bool crossed(bool& over, uint64_t used, uint64_t allowed) { + const bool now = allowed && used > allowed; + const bool tell = now && !over; + over = now; + return tell; +} + +/// What the warning adds for the upscaler drawing (1 DLSS, 2 DLAA; 0 neither: nothing) +inline const char* over_advice(int dlss_mode) { + if (dlss_mode == 2) return ": DLAA may stutter here; Quality or Balanced uses less"; + if (dlss_mode == 1) return ": DLSS may stutter here; a lower Render resolution mode uses less"; + return ""; +} +inline int over_text(char* b, size_t n, uint64_t used, uint64_t allowed, int dlss_mode) { + return snprintf(b, n, "vk: over the driver's video-memory allowance (%.1f of %.1f GB)%s", gb(used), gb(allowed), + over_advice(dlss_mode)); +} + +} // namespace mkwvk::status_line diff --git a/gpu/vk/upscaler_rows.h b/gpu/vk/upscaler_rows.h new file mode 100644 index 000000000..225d6c44a --- /dev/null +++ b/gpu/vk/upscaler_rows.h @@ -0,0 +1,121 @@ +/* The Insert menu's scaling rows (vk_menu.cpp), as plain functions: what the Render resolution row steps through for each + Upscaler, and the names it shows for DLSS's modes. No Vulkan, no menu: tests/test_upscaler_rows.cpp checks them. */ +#pragma once + +namespace mkwvk::upscaler_rows { + +/// The Upscaler row (opts.dlss, RX_VK_DLSS): the game's own network, NVIDIA DLSS, NVIDIA DLAA +enum { GAME = 0, DLSS = 1, DLAA = 2 }; + +/// opts.resolution: 0 Auto (the game's dynamic resolution), else a share of the output in percent. 1 to 4 are the values from +/// before (100, 75, 67, 50: saved options keep their meaning); 5 and 6 came with DLSS's Balanced and Ultra Performance. Each +/// is a twelfth of the output (vk_menu.cpp twelfths): 8/12, 7/12, 6/12 and 4/12 are DLSS's own ratios +constexpr int kPercent[] = {0, 100, 75, 67, 50, 58, 33}; +constexpr int kValues = sizeof kPercent / sizeof kPercent[0]; +inline int percent(int v) { return v > 0 && v < kValues ? kPercent[v] : 0; } + +/// The steps the Render resolution row takes, best first after Auto. DLSS: its Quality, Balanced, Performance and Ultra +/// Performance; Auto too, since DLSS follows the game's dynamic size (vk_dlss.cpp makes its feature again when the size +/// leaves the mode's range). DLAA: none (the 3D is drawn at full size) +constexpr int kGameSteps[] = {0, 1, 2, 3, 4}; +constexpr int kDlssSteps[] = {0, 3, 5, 4, 6}; +inline const int* steps(int upscaler, int& n) { + if (upscaler == DLSS) return n = sizeof kDlssSteps / sizeof kDlssSteps[0], kDlssSteps; + return n = sizeof kGameSteps / sizeof kGameSteps[0], kGameSteps; +} + +/// DLSS's name for a Render resolution value: nullptr for Auto, or a share that is not one of its modes +inline const char* dlss_mode_name(int v) { + switch (percent(v)) { + case 67: return "Quality"; + case 58: return "Balanced"; + case 50: return "Performance"; + case 33: return "Ultra Performance"; + default: return nullptr; + } +} + +/// The Render resolution row's next value from v (dir +1 Right, -1 Left), wrapping. A value that is not one of the upscaler's +/// steps (one saved with another upscaler) moves to its neighbour by size. DLAA: v, unchanged +inline int step(int upscaler, int v, int dir) { + if (upscaler == DLAA) return v; + int n = 0; + const int* s = steps(upscaler, n); + for (int i = 0; i < n; i++) + if (s[i] == v) return s[(i + (dir > 0 ? 1 : n - 1)) % n]; + // (not a step: where it would sit, Auto first and then larger shares first) + const int p = percent(v); + int pos = 1; + while (pos < n && percent(s[pos]) > p) pos++; + return dir > 0 ? s[pos % n] : s[pos - 1]; +} + +/// v as one of the upscaler's steps: itself when it is one, else the step nearest in size (the larger on a tie). Auto stays +/// Auto. DLAA: v, unchanged (its row does not apply; v comes back with the next upscaler) +inline int snap(int upscaler, int v) { + if (upscaler == DLAA || percent(v) == 0) return percent(v) == 0 ? 0 : v; + int n = 0; + const int* s = steps(upscaler, n); + int best = v, best_d = 1000; + for (int i = 0; i < n; i++) { + if (s[i] == v) return v; + if (!percent(s[i])) continue; + const int d = percent(s[i]) > percent(v) ? percent(s[i]) - percent(v) : percent(v) - percent(s[i]); + if (d < best_d || (d == best_d && percent(s[i]) > percent(best))) best = s[i], best_d = d; + } + return best; +} + +/// The Upscaler row's next value from v (dir +1 / -1), wrapping over the choices there are: DLSS and DLAA only when +/// dlss_ok (a build with NVIDIA's SDK, on a GPU DLSS runs on). Without them it stays on the game's own +inline int step_upscaler(int v, int dir, bool dlss_ok) { + if (!dlss_ok) return GAME; + const int n = 3; + return ((v < 0 || v >= n ? GAME : v) + (dir > 0 ? 1 : n - 1)) % n; +} + +/// The DLSS model row (opts.dlss_model, saved as dlss_model): which of NVIDIA's networks DLSS and DLAA draw with, by its +/// preset (NVSDK_NGX_DLSS_Hint_Render_Preset_*). 0 the transformer, preset K (the default here, the one tested); 1 the second +/// generation transformer, preset M (NVIDIA's own pick for Performance); 2 the same generation as tuned for Ultra +/// Performance, preset L; 3 NVIDIA's default, preset 0 (the runtime picks per mode, and a runtime update can change it) +enum { MODEL_K = 0, MODEL_M = 1, MODEL_L = 2, MODEL_DEFAULT = 3, kModels = 4 }; +inline unsigned model_preset(int model) { + switch (model) { + case MODEL_M: return 13; + case MODEL_L: return 12; + case MODEL_DEFAULT: return 0; + default: return 11; // (K, also for a value past the row) + } +} +/// The preset DLSS is asked for: RX_VK_DLSS_PRESET over the row (env: its value, or nullptr), a letter (A = 1 ... Z = 26, +/// either case) or 0 for NVIDIA's default; anything else leaves the row's +inline bool preset_env(const char* env) { + return env && ((*env >= 'A' && *env <= 'Z') || (*env >= 'a' && *env <= 'z') || (env[0] == '0' && !env[1])); +} +inline unsigned dlss_preset(int model, const char* env) { + if (!preset_env(env)) return model_preset(model); + if (*env == '0') return 0; + return (unsigned)(*env >= 'a' ? *env - 'a' + 1 : *env - 'A' + 1); +} +/// A preset's letter for the log ('A' + preset - 1), or '0' for NVIDIA's default +inline char preset_letter(unsigned preset) { return preset ? (char)('A' + preset - 1) : '0'; } + +/// The Upscaler row is there only in a build with DLSS (built: dlss_built()): elsewhere the game's own is all there is, and +/// the Render resolution row is the game's own's +inline bool upscaler_shown(bool built) { return built; } +/// Rows with nothing else to choose here (shown without arrows; Left and Right leave the saved value alone): the Upscaler +/// where DLSS cannot run (dlss_ok: dlss_why() == DLSS_OK), the Render resolution with DLAA (the 3D at full size), the DLSS +/// model while DLSS and DLAA are not drawing +inline bool upscaler_fixed(bool dlss_ok) { return !dlss_ok; } +inline bool resolution_fixed(int upscaler) { return upscaler == DLAA; } +/// The DLSS model row: shown where the Upscaler is (a build with DLSS), with nothing to choose while the game's own upscales +inline bool model_shown(bool built) { return built; } +/// (env_set: RX_VK_DLSS_PRESET names a preset, which wins over the row for this run) +inline bool model_fixed(int upscaler, bool env_set) { return upscaler == GAME || env_set; } +/// The Window scaling filter does something only when the window's size is not the game's picture's (the Output resolution); +/// a size not known yet (0) counts as different +inline bool filter_needed(unsigned picture_w, unsigned picture_h, unsigned window_w, unsigned window_h) { + return !picture_w || !window_w || picture_w != window_w || picture_h != window_h; +} + +} // namespace mkwvk::upscaler_rows diff --git a/gpu/vk/vk_api.h b/gpu/vk/vk_api.h index 91bad8771..ab699f59e 100644 --- a/gpu/vk/vk_api.h +++ b/gpu/vk/vk_api.h @@ -20,6 +20,9 @@ enum { HOS_OPT_CHEAT_HEARTS = 32, HOS_OPT_CHEAT_FLIGHT, HOS_OPT_CHEAT_ACTION, HO /* (hookshot) the Controls tab's motion rows (host/pad_sdl.c reads them, vk_replay.cpp's option answers them): Motion controls on or off, the horizontal's and the vertical's sensitivity in percent, and each turned around */ enum { HOS_OPT_GYRO = 18, HOS_OPT_GYRO_SENS_X, HOS_OPT_GYRO_INV_X, HOS_OPT_GYRO_INV_Y, HOS_OPT_GYRO_SENS_Y }; +/* (hookshot) NVIDIA DLSS's camera jitter (host/rx_backend.c, gpu/vk/vk_dlss.cpp): the scene's width and height while DLSS + draws, 0 when there is to be no jitter, and the jitter sequence's length. 24 on: 18 to 22 are the motion rows' */ +enum { HOS_OPT_DLSS_JITTER_W = 24, HOS_OPT_DLSS_JITTER_H, HOS_OPT_DLSS_JITTER_PHASES }; /* (hookshot) the controls' names (host/nn_hid.c, host/pad_sdl.c, the pad_ and key_ lines of hookshot_options.ini): the Switch's buttons in nn::hid's bit order, and a gamepad's inputs by position, SDL's order (the bottom face diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp new file mode 100644 index 000000000..c8bacace7 --- /dev/null +++ b/gpu/vk/vk_dlss.cpp @@ -0,0 +1,1160 @@ +// (hookshot) NVIDIA DLSS Super Resolution in place of OoT's gameplay upscale network (vk_dlss.h). +// +// OoT has no DLSS of its own: its scene is drawn at a lower size and a network of its own (the program +// fd029a317a9a4b94 at 3/4, another for each other share; one dispatch, cooperative matrices) scales it up, from the +// square root of the scene's tonemapped colour, into the frame's B10G11R11 picture (before the HUD). Here that dispatch is replaced: +// - the scene's camera is taken from the draws into the scene's colour target: their vertex stage's uniform buffer 3 +// holds the target's size at 0x120, this frame's view-projection at 0x30 (row major, clip = M * position; the projection's w row ends -1, 0 at 0xa8). The largest such target in a frame is +// the scene (reflections and half-size passes have the same layout). The scene's depth is that draw's depth target. +// - shaders/dlss_prep.comp makes DLSS's inputs at the render size: the colour as the network samples it (LDR), the +// depth, and motion vectors from the depth reprojected through the two cameras. The game has no motion vectors of its +// own (no velocity target: the scene pass draws into one B10G11R11 target, and its upscale network samples the colour alone), +// so that is the camera's motion, and what moves by itself would ghost. The scene camera's sub-pixel jitter +// (nx2/oot_jitter.py, while DLSS draws) is read back from its projection, given to DLSS and taken off the motion. +// - actor motion (actor_motion, shaders/dlss_actor.vert): the skinned draws into the scene (vk_draw.cpp's dlss_skin_check: +// Link, the other characters, the animals, what the game animates with bones) are drawn again over that motion, each +// vertex skinned with this frame's bones and the frame before's, so their pixels get their own motion. Their poses are +// copied on the GPU from where the game has them into two buffers that take turns; a pixel is the draw's where its depth is +// the scene's (within RX_VK_DLSS_ACTOR_TOL steps, default 64). What moves without bones (particles, water, foliage moved in +// its vertex shader, mesh-shader draws) keeps the camera's motion. RX_VK_DLSS_ACTORS=0: camera motion alone. +// - DLSS (NGX, Vulkan) scales them to the output's size; shaders/dlss_out.comp squares that back to linear colour +// into the network's output. +// Anything unexpected (no camera this frame, other sizes, DLSS not there or failing) leaves the network to run, the +// reason in the log when it changes. +// +// RX_VK_DLSS=0 / 1 / 2: the game's own, DLSS, DLAA over the Insert menu's Upscaler row (0 also leaves NGX's extensions +// off). DLSS's runtime (Windows: nvngx_dlss.dll; Linux: libnvidia-ngx-dlss.so.*) is looked for in RX_VK_DLSS_DIR= +// when set, the game folder's ngx folder, and %LOCALAPPDATA%\Hookshot\ngx (Linux: $XDG_DATA_HOME/hookshot/ngx): NGX takes +// the newest one it finds in them. Windows' paths are wide all the way (GetModuleFileNameW, _wgetenv), so any folder name +// works. The Insert menu's DLSS model row (opts.dlss_model, upscaler_rows.h) chooses DLSS's preset: K by default, M, L, or +// NVIDIA's default (0). RX_VK_DLSS_PRESET= (or 0) over it. +// RX_VK_DLSS_DEBUG=motion: the motion vectors shown instead of the picture (|x|, |y| over 16 px as red and green, the actors' +// in it); =alternate: every other frame the game's own upscale (a test's reference beside DLSS's). RX_VK_DLSS_LOG=1: NGX's +// own log in ours. +// RX_VK_DLSS_SQUARE=0: the output not squared. +// +// NVIDIA's NGX SDK is not part of Hookshot (NVIDIA's license): a local build fetches it (scripts/ngx_sdk.py) and builds with +// HOOKSHOT_NGX_SDK, which compiles this file with HOOKSHOT_NGX. Without it only the stubs at the end are here. +// NGX's core is the driver's (Windows: _nvngx.dll, which NVIDIA's loader finds in the driver's own folder; Linux: +// libnvidia-ngx.so). It is never put beside the game: a copy there would be loaded in place of the driver's. +#include "vk_dlss.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dlss_actor.h" +#include "dlss_math.h" +#include "upscaler_rows.h" +#include "vk_draw.h" +#include "vk_menu.h" +#include "vk_paths.h" +#include "vk_texture.h" + +#ifdef HOOKSHOT_NGX +#include "nvsdk_ngx_vk.h" +#include "nvsdk_ngx_helpers.h" +#include "nvsdk_ngx_helpers_vk.h" + +#include "shaders/dlss_actor_frag_spv.h" +#include "shaders/dlss_actor_vert_spv.h" +#include "shaders/dlss_out_comp_spv.h" +#include "shaders/dlss_prep_comp_spv.h" +#endif +#include "vk_work.h" // (after the others: counts the commands that do GPU work, for full_barrier) + +namespace mkwvk { + +namespace { +int env_switch(const char* name) { // (-1: unset) + const char* e = getenv(name); + return e && *e ? atoi(e) : -1; +} +} // namespace + +// (what is known of DLSS on this device: -1 not yet, else a DlssWhy. Set once the device is made and when NGX answers) +std::atomic g_why{-1}; + +DlssWhy dlss_why() { + if (!dlss_built()) return DLSS_NOT_BUILT; + static const bool env_off = env_switch("RX_VK_DLSS") == 0; + if (env_off) return DLSS_ENV_OFF; + const int w = g_why.load(std::memory_order_relaxed); + return w < 0 ? DLSS_OK : (DlssWhy)w; +} + +int dlss_mode() { + if (dlss_why() != DLSS_OK) return 0; + static const int env = env_switch("RX_VK_DLSS"); + return std::clamp(env >= 0 ? env : opts.dlss, 0, 2); +} +bool dlss_wanted() { return dlss_mode() != 0; } +bool dlss_full_scale() { return dlss_mode() == 2; } + +#ifdef HOOKSHOT_NGX + +namespace { + +struct Image { + VkImage image = VK_NULL_HANDLE; + VmaAllocation memory = nullptr; + VkImageView view = VK_NULL_HANDLE; + VkFormat format = VK_FORMAT_UNDEFINED; + uint32_t width = 0, height = 0; +}; + +struct Camera { + float current[16]{}; // (its view-projection) + float jitter[2]{}; // (the projection's offset of the picture's centre, in clip space: nx2/oot_jitter.py) + uint32_t width = 0, height = 0; + Texture* depth = nullptr; + uint64_t depth_gen = 0; // (texture_destroy_generation when taken: a depth texture destroyed since is not used) + uint64_t frame = 0; // (texture_frame_no when taken) + bool valid = false; +}; + +struct State { + std::mutex mu; + bool ngx_tried = false, ngx_ok = false; + std::atomic failed{false}; // (atomic: dlss_jitter_size reads it on the game's thread, without the lock) + NVSDK_NGX_Parameter* caps = nullptr; + NVSDK_NGX_Parameter* params = nullptr; + NVSDK_NGX_Handle* feature = nullptr; + uint32_t feature_rw = 0, feature_rh = 0, feature_ow = 0, feature_oh = 0; // (its largest render size, its output) + uint32_t min_rw = 0, min_rh = 0; + unsigned feature_preset = ~0u; // (the preset it was made with: another one from the menu makes it again) + uint64_t features_made = 0; // (how many times: a feature made again in play costs a stall, so each says why) + Image color, depth, motion, output; + VkDescriptorSetLayout prep_set = VK_NULL_HANDLE, out_set = VK_NULL_HANDLE; + VkPipelineLayout prep_layout = VK_NULL_HANDLE, out_layout = VK_NULL_HANDLE; + VkPipeline prep = VK_NULL_HANDLE, out = VK_NULL_HANDLE; + VkSampler point = VK_NULL_HANDLE, linear = VK_NULL_HANDLE; + Camera cam; // (this frame's, since the last upscale) + std::atomic last_frame{0}; // (the frame DLSS last drew: read on the game's thread too) + float last_jitter[2]{}; // (its camera's jitter: the frame before's, for the next one) + float last_vp[16]{}; // (its camera's view-projection: the frame before's) + std::atomic jitter_w{0}, jitter_h{0}; // (the scene's size while DLSS draws: the host's jitter's scale) + std::atomic jitter_phases{8}; // (its sequence's length, for DLSS's scale: dlss_jitter_phases) + std::unordered_map depth_views; + uint64_t depth_views_gen = 0; + const char* reason = nullptr; + int reason_repeats = 0; + bool told_first = false; + // (actor motion: the skinned draws into the scene since the last upscale; the poses DLSS last drew, by draw: the + // frame before's, where they were copied to in one of the two pose buffers (each frame's poses go to the other one)) + struct Skin { + DlssSkin k; + uint64_t key = 0; + uint32_t nbones = 0; + }; + std::vector skins; + struct Pose { + uint32_t at = 0, nbones = 0; // (bytes into its buffer: the origin, then the bones from at + 16) + uint64_t frame = 0; + }; + std::unordered_map poses; + std::unordered_map model_bones; // (by model: the bones its vertices use, 0 when unreadable) + struct PoseBuffer { + VkBuffer buffer = VK_NULL_HANDLE; + VmaAllocation memory = nullptr; + uint64_t address = 0; + } pose_buffers[2]; + int pose_at = 0; // (this frame's) + VkPipelineLayout actor_layout = VK_NULL_HANDLE; + VkPipeline actor = VK_NULL_HANDLE; + bool actor_tried = false; + struct Upload { + VkBuffer buffer = VK_NULL_HANDLE; + VmaAllocation memory = nullptr; + void* map = nullptr; + } uploads[6]; // (the cameras: a ring, one in use stays so for a few frames, as what later() frees) + int upload_at = 0; + uint64_t actor_draws = 0, actor_frames = 0, actor_skipped = 0; + int64_t actor_ns = 0; // (the CPU's time in it, since the last status line) + uint64_t status_draws = 0, status_frames = 0; // (actor_draws and _frames at the last status line: dlss_status) + uint64_t status_drawn = 0, status_passed = 0; // (since the last status line: frames DLSS drew, and frames a network + // of the upscale's shape ran without it (its scene not this frame's)) + float status_jitter[2]{}; // (the last evaluation's jitter, in render pixels, and whether it had motion) + bool status_motion = false; + struct Dead { uint64_t frame; std::function free; }; + std::vector dead; // (what work in flight may still use: freed a few frames on) + uint64_t asked = 0; // (the frame an upscale last asked for DLSS: dlss_idle) + std::atomic holding{false}; // (a feature, images or frees still to do: dlss_idle has something to let go) +}; +State& st() { + static State s; + return s; +} + +bool fail(const char* why) { // (the network runs: why, when it changes and every 600th time) + State& s = st(); + if (why != s.reason || ++s.reason_repeats % 600 == 0) logf("vk: NVIDIA DLSS: the game's upscale runs (%s)", why); + if (why != s.reason) s.reason = why, s.reason_repeats = 0; + return false; +} + +void later(std::function f) { st().dead.push_back({texture_frame_no(), std::move(f)}); } +void free_dead(bool all) { + State& s = st(); + const uint64_t now = texture_frame_no(); + for (size_t i = 0; i < s.dead.size();) { + if (all || now - s.dead[i].frame > 4) { + s.dead[i].free(); + s.dead[i] = std::move(s.dead.back()); + s.dead.pop_back(); + } else { + i++; + } + } +} + +void drop_image(Image& im) { + if (!im.image) return; + const Image old = im; + later([old] { + vkDestroyImageView(dev.device, old.view, nullptr); + destroy_image(old.image, old.memory); + }); + im = {}; +} + +bool make_image(VkCommandBuffer cmd, Image& im, VkFormat format, uint32_t w, uint32_t h, VkImageUsageFlags extra = 0) { + if (im.image && im.format == format && im.width == w && im.height == h) return true; + drop_image(im); + VkImageCreateInfo ic{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + ic.imageType = VK_IMAGE_TYPE_2D, ic.format = format, ic.extent = {w, h, 1}, ic.mipLevels = 1, ic.arrayLayers = 1; + ic.samples = VK_SAMPLE_COUNT_1_BIT, ic.tiling = VK_IMAGE_TILING_OPTIMAL; + ic.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | extra; + if (!create_image(ic, im.image, im.memory)) return im = {}, false; + VkImageViewCreateInfo vc{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + vc.image = im.image, vc.viewType = VK_IMAGE_VIEW_TYPE_2D, vc.format = format; + vc.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + if (vkCreateImageView(dev.device, &vc, nullptr, &im.view) != VK_SUCCESS) { + destroy_image(im.image, im.memory); + return im = {}, false; + } + im.format = format, im.width = w, im.height = h; + VkImageMemoryBarrier2 b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2}; // (UNDEFINED -> GENERAL, once) + b.srcStageMask = VK_PIPELINE_STAGE_2_NONE, b.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + b.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, b.newLayout = VK_IMAGE_LAYOUT_GENERAL; + b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = im.image, b.subresourceRange = vc.subresourceRange; + VkDependencyInfo di{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; + di.imageMemoryBarrierCount = 1, di.pImageMemoryBarriers = &b; + vkCmdPipelineBarrier2(cmd, &di); + return true; +} + +NVSDK_NGX_Resource_VK resource(const Image& im, bool write) { + return NVSDK_NGX_Create_ImageView_Resource_VK(im.view, im.image, {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}, im.format, + im.width, im.height, write); +} + +void NVSDK_CONV ngx_log(const char* message, NVSDK_NGX_Logging_Level, NVSDK_NGX_Feature) { + std::string m(message ? message : ""); + while (!m.empty() && (m.back() == '\n' || m.back() == '\r')) m.pop_back(); + logf("ngx: %s", m.c_str()); +} + +namespace fs = std::filesystem; +#ifdef _WIN32 +/// The game folder (game_module_path's rule: bin\hookshot.exe beside image.bin is the folder above bin), from +/// GetModuleFileNameW: a folder with characters outside the ANSI code page is still the right one +fs::path game_dir() { + std::wstring exe(32768, L'\0'); + const DWORD n = GetModuleFileNameW(nullptr, exe.data(), (DWORD)exe.size()); + if (!n || n >= exe.size()) return {}; + exe.resize(n); + fs::path dir = fs::path(exe).parent_path(); + if (!_wcsicmp(dir.filename().c_str(), L"bin") && GetFileAttributesW((dir.parent_path() / L"image.bin").c_str()) != INVALID_FILE_ATTRIBUTES) + dir = dir.parent_path(); + return dir; +} +/// An environment variable as a path (_wgetenv: not through the ANSI code page); empty when unset +fs::path env_path(const wchar_t* name) { + const wchar_t* e = _wgetenv(name); + return e && *e ? fs::path(e) : fs::path(); +} +/// A path for our log (UTF-8) +std::string shown(const fs::path& p) { + const std::wstring& w = p.native(); + const int n = WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), nullptr, 0, nullptr, nullptr); + std::string s(n > 0 ? n : 0, '\0'); + if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), s.data(), n, nullptr, nullptr); + return s; +} +/// A path for NGX, which takes wide strings: Windows' own +std::wstring wide(const fs::path& p) { return p.native(); } +#else +fs::path game_dir() { + char exe[MAX_PATH]; + const DWORD n = game_module_path(exe, MAX_PATH); + return fs::path(std::string(exe, n)).parent_path(); +} +fs::path env_path(const char* name) { + const char* e = getenv(name); + return e && *e ? fs::path(e) : fs::path(); +} +std::string shown(const fs::path& p) { return p.native(); } +std::wstring wide(const fs::path& p) { + const std::string& s = p.native(); + return std::wstring(s.begin(), s.end()); +} +#endif + +#ifdef _WIN32 +constexpr const char* kRuntime = "nvngx_dlss.dll"; // (DLSS's runtime, as scripts/ngx_sdk.py fetches it) +constexpr const fs::path::value_type* kRuntimeName = L"nvngx_dlss.dll"; +#else +constexpr const char* kRuntime = "libnvidia-ngx-dlss.so"; +constexpr const fs::path::value_type* kRuntimeName = "libnvidia-ngx-dlss.so"; +#endif + +/// NGX on this device, once: false when DLSS cannot run here (the reason logged) +bool ngx_init() { + State& s = st(); + if (s.ngx_tried) return s.ngx_ok; + s.ngx_tried = true; + // where DLSS's runtime (kRuntime) is looked for (NGX takes the newest it finds), and its own files go + std::vector dirs; +#ifdef _WIN32 + if (fs::path e = env_path(L"RX_VK_DLSS_DIR"); !e.empty()) dirs.push_back(e); + dirs.push_back(game_dir() / L"ngx"); + if (fs::path l = env_path(L"LOCALAPPDATA"); !l.empty()) dirs.push_back(l / L"Hookshot" / L"ngx"); +#else + if (fs::path e = env_path("RX_VK_DLSS_DIR"); !e.empty()) dirs.push_back(e); + dirs.push_back(game_dir() / "ngx"); + if (fs::path x = env_path("XDG_DATA_HOME"); !x.empty()) dirs.push_back(x / "hookshot" / "ngx"); + else if (fs::path h = env_path("HOME"); !h.empty()) dirs.push_back(h / ".local" / "share" / "hookshot" / "ngx"); +#endif + std::vector wdirs; + for (const auto& d : dirs) wdirs.push_back(wide(d)); + std::vector paths; + for (const auto& d : wdirs) paths.push_back(d.c_str()); + const fs::path data = game_dir() / "ngx"; + std::error_code made; // (not the throwing overload: a folder that cannot be made, read-only or so, must not end the game) + fs::create_directories(data, made); // (NGX's logs and its own files: the game folder's, not /usr/share) + const std::wstring wdata = wide(data); + NVSDK_NGX_FeatureCommonInfo info{}; + info.PathListInfo.Path = paths.data(); + info.PathListInfo.Length = (unsigned)paths.size(); + static const bool verbose = env_switch("RX_VK_DLSS_LOG") > 0; + info.LoggingInfo.LoggingCallback = verbose ? ngx_log : nullptr; + info.LoggingInfo.MinimumLoggingLevel = verbose ? NVSDK_NGX_LOGGING_LEVEL_ON : NVSDK_NGX_LOGGING_LEVEL_OFF; + // (NGX also writes its own log files into the data folder; with RX_VK_DLSS_LOG=1 its lines go to ours instead) + info.LoggingInfo.DisableOtherLoggingSinks = verbose; + // (a project id of Hookshot's own: NGX wants one with the custom engine type, any GUID) + NVSDK_NGX_Result r = NVSDK_NGX_VULKAN_Init_with_ProjectID("7b2f5d0c-6a1e-4c39-9f84-3d5e2a1b0c96", NVSDK_NGX_ENGINE_TYPE_CUSTOM, + "1.0", wdata.c_str(), dev.instance, dev.physical, dev.device, + instance_proc(), vkGetDeviceProcAddr, &info); + if (NVSDK_NGX_FAILED(r)) { + logf("vk: NVIDIA DLSS: NGX did not start (%#x): not an NVIDIA GPU, or its driver has no NGX", (unsigned)r); + g_why = DLSS_NEEDS_RTX; + return false; + } + if (NVSDK_NGX_FAILED(NVSDK_NGX_VULKAN_GetCapabilityParameters(&s.caps)) || !s.caps) { + logf("vk: NVIDIA DLSS: NGX has no capabilities for this device"); + g_why = DLSS_NEEDS_RTX; + return false; + } + // (NGX's C functions, not NVSDK_NGX_Parameter's virtual ones: its object comes from NVIDIA's MSVC-built library, whose + // vtable order is not the Itanium ABI's that llvm-mingw calls with. The overloads only lined up by luck) + int available = 0, needs_driver = 0; + unsigned major = 0, minor = 0; + NVSDK_NGX_Parameter_GetI(s.caps, NVSDK_NGX_Parameter_SuperSampling_Available, &available); + NVSDK_NGX_Parameter_GetI(s.caps, NVSDK_NGX_Parameter_SuperSampling_NeedsUpdatedDriver, &needs_driver); + NVSDK_NGX_Parameter_GetUI(s.caps, NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMajor, &major); + NVSDK_NGX_Parameter_GetUI(s.caps, NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMinor, &minor); + std::string where; + for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + shown(d); + if (!available) { + // (for the menu: a runtime found in none of the folders is the likely reason; with one there, the GPU (a GTX)) + int init = 0; + NVSDK_NGX_Parameter_GetI(s.caps, NVSDK_NGX_Parameter_SuperSampling_FeatureInitResult, &init); + bool runtime = false; + std::error_code ec; + for (const auto& d : dirs) + for (const auto& f : fs::directory_iterator(d, ec)) + runtime |= f.path().filename().native().rfind(kRuntimeName, 0) == 0; + if (needs_driver) logf("vk: NVIDIA DLSS: needs NVIDIA's driver %u.%u or newer", major, minor); + else if (!runtime) logf("vk: NVIDIA DLSS: not available: no %s in %s (scripts/ngx_sdk.py fetches it)", kRuntime, where.c_str()); + else logf("vk: NVIDIA DLSS: not available on this GPU (%#x): DLSS needs an NVIDIA RTX GPU", (unsigned)init); + g_why = needs_driver ? DLSS_NEEDS_DRIVER : !runtime ? DLSS_NO_RUNTIME : DLSS_NEEDS_RTX; + return false; + } + if (NVSDK_NGX_FAILED(NVSDK_NGX_VULKAN_AllocateParameters(&s.params)) || !s.params) return false; + logf("vk: NVIDIA DLSS: NGX started (DLSS's runtime: the newest %s in %s)", kRuntime, where.c_str()); + s.ngx_ok = true; + g_why = DLSS_OK; + return true; +} + +/// One compute pipeline with push descriptors; what it made is destroyed again when a step fails +bool make_pipeline(const uint32_t* code, size_t size, const VkDescriptorType* types, uint32_t n, uint32_t push, + VkDescriptorSetLayout& set, VkPipelineLayout& layout, VkPipeline& pipe) { + const auto undo = [&] { + if (layout) vkDestroyPipelineLayout(dev.device, layout, nullptr); + if (set) vkDestroyDescriptorSetLayout(dev.device, set, nullptr); + set = VK_NULL_HANDLE, layout = VK_NULL_HANDLE, pipe = VK_NULL_HANDLE; + return false; + }; + VkDescriptorSetLayoutBinding b[8]{}; + for (uint32_t i = 0; i < n; i++) b[i] = {i, types[i], 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo li{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + li.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR; + li.bindingCount = n, li.pBindings = b; + if (vkCreateDescriptorSetLayout(dev.device, &li, nullptr, &set) != VK_SUCCESS) return set = VK_NULL_HANDLE, undo(); + const VkPushConstantRange pr{VK_SHADER_STAGE_COMPUTE_BIT, 0, push}; + VkPipelineLayoutCreateInfo pi{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + pi.setLayoutCount = 1, pi.pSetLayouts = &set, pi.pushConstantRangeCount = 1, pi.pPushConstantRanges = ≺ + if (vkCreatePipelineLayout(dev.device, &pi, nullptr, &layout) != VK_SUCCESS) return layout = VK_NULL_HANDLE, undo(); + VkShaderModuleCreateInfo mi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + mi.codeSize = size, mi.pCode = code; + VkShaderModule m{}; + if (vkCreateShaderModule(dev.device, &mi, nullptr, &m) != VK_SUCCESS) return undo(); + VkComputePipelineCreateInfo ci{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + ci.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, m, "main", nullptr}; + ci.layout = layout; + const bool ok = vkCreateComputePipelines(dev.device, VK_NULL_HANDLE, 1, &ci, nullptr, &pipe) == VK_SUCCESS; + vkDestroyShaderModule(dev.device, m, nullptr); + return ok || undo(); +} + +/// The two passes' pipelines and the samplers, once (dlss_upscale stops trying when this fails) +bool make_pipelines() { + State& s = st(); + if (s.prep && s.out && s.point && s.linear) return true; + if (!dev.push_descriptor || !vkCmdPushDescriptorSetKHR) return false; + const VkDescriptorType S = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, I = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + const VkDescriptorType prep_types[5] = {S, S, I, I, I}, out_types[2] = {S, I}; + if (!s.prep && !make_pipeline(DLSS_PREP_COMP_SPV, sizeof DLSS_PREP_COMP_SPV, prep_types, 5, 84, s.prep_set, s.prep_layout, s.prep)) + return false; + if (!s.out && !make_pipeline(DLSS_OUT_COMP_SPV, sizeof DLSS_OUT_COMP_SPV, out_types, 2, 8, s.out_set, s.out_layout, s.out)) + return false; + VkSamplerCreateInfo si{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + si.magFilter = si.minFilter = VK_FILTER_NEAREST; + si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + if (!s.point && vkCreateSampler(dev.device, &si, nullptr, &s.point) != VK_SUCCESS) return s.point = VK_NULL_HANDLE, false; + si.magFilter = si.minFilter = VK_FILTER_LINEAR; + if (!s.linear && vkCreateSampler(dev.device, &si, nullptr, &s.linear) != VK_SUCCESS) return s.linear = VK_NULL_HANDLE, false; + return true; +} + +/// A view of a depth texture's depth alone, for sampling (made once per image) +VkImageView depth_view(Texture* t) { + State& s = st(); + const uint64_t gen = texture_destroy_generation(); + if (gen != s.depth_views_gen) { // (a texture was destroyed: its image's view may be stale) + auto old = std::move(s.depth_views); + s.depth_views.clear(); + later([old] { + for (const auto& [image, view] : old) vkDestroyImageView(dev.device, view, nullptr); + }); + s.depth_views_gen = gen; + } + if (auto it = s.depth_views.find(t->image); it != s.depth_views.end()) return it->second; + VkImageViewCreateInfo vc{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + vc.image = t->image, vc.viewType = VK_IMAGE_VIEW_TYPE_2D, vc.format = t->format; + vc.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1}; + VkImageView v = VK_NULL_HANDLE; + if (vkCreateImageView(dev.device, &vc, nullptr, &v) != VK_SUCCESS) v = VK_NULL_HANDLE; + s.depth_views[t->image] = v; + return v; +} + +// ---- actor motion: the skinned draws drawn again into the motion, each pixel they show moved by its own bones ---- + +using ActorPush = dlss_actor::Push; + +/// Its pipeline, once (false: none, the motion stays the camera's) +bool make_actor_pipeline() { + State& s = st(); + if (s.actor) return true; + if (s.actor_tried) return false; + s.actor_tried = true; + VkPhysicalDeviceFeatures2 f{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2}; + vkGetPhysicalDeviceFeatures2(dev.physical, &f); + if (!dev.vk12.bufferDeviceAddress || !f.features.shaderInt64) { // (its shaders read the game's buffers by their addresses) + logf("vk: NVIDIA DLSS: no buffer device addresses or 64-bit integers in shaders: camera motion alone"); + return false; + } + VkDescriptorSetLayoutBinding b[2]{}; + b[0] = {0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + b[1] = {1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo li{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + li.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR; + li.bindingCount = 2, li.pBindings = b; + VkDescriptorSetLayout set{}; + if (vkCreateDescriptorSetLayout(dev.device, &li, nullptr, &set) != VK_SUCCESS) return false; + const VkPushConstantRange pr{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(ActorPush)}; + VkPipelineLayoutCreateInfo pi{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + pi.setLayoutCount = 1, pi.pSetLayouts = &set, pi.pushConstantRangeCount = 1, pi.pPushConstantRanges = ≺ + if (vkCreatePipelineLayout(dev.device, &pi, nullptr, &s.actor_layout) != VK_SUCCESS) return false; + VkShaderModule m[2]{}; + const uint32_t* code[2] = {DLSS_ACTOR_VERT_SPV, DLSS_ACTOR_FRAG_SPV}; + const size_t size[2] = {sizeof DLSS_ACTOR_VERT_SPV, sizeof DLSS_ACTOR_FRAG_SPV}; + for (int i = 0; i < 2; i++) { + VkShaderModuleCreateInfo mi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + mi.codeSize = size[i], mi.pCode = code[i]; + if (vkCreateShaderModule(dev.device, &mi, nullptr, &m[i]) != VK_SUCCESS) return false; + } + VkPipelineShaderStageCreateInfo stages[2]{}; + for (int i = 0; i < 2; i++) { + stages[i] = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + stages[i].stage = i ? VK_SHADER_STAGE_FRAGMENT_BIT : VK_SHADER_STAGE_VERTEX_BIT; + stages[i].module = m[i], stages[i].pName = "main"; + } + VkPipelineVertexInputStateCreateInfo vi{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + VkPipelineViewportStateCreateInfo vp{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + vp.viewportCount = vp.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rs{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rs.polygonMode = VK_POLYGON_MODE_FILL, rs.cullMode = VK_CULL_MODE_NONE, rs.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState ba{}; + ba.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT; + VkPipelineColorBlendStateCreateInfo cb{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + cb.attachmentCount = 1, cb.pAttachments = &ba; + const VkDynamicState dyn[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo ds{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + ds.dynamicStateCount = 2, ds.pDynamicStates = dyn; + const VkFormat format = VK_FORMAT_R16G16_SFLOAT; + VkPipelineRenderingCreateInfo ri{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO}; + ri.colorAttachmentCount = 1, ri.pColorAttachmentFormats = &format; + VkGraphicsPipelineCreateInfo gi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + gi.pNext = &ri; + gi.stageCount = 2, gi.pStages = stages; + gi.pVertexInputState = &vi, gi.pInputAssemblyState = &ia, gi.pViewportState = &vp, gi.pRasterizationState = &rs; + gi.pMultisampleState = &ms, gi.pColorBlendState = &cb, gi.pDynamicState = &ds, gi.layout = s.actor_layout; + const bool ok = vkCreateGraphicsPipelines(dev.device, VK_NULL_HANDLE, 1, &gi, nullptr, &s.actor) == VK_SUCCESS; + for (VkShaderModule x : m) vkDestroyShaderModule(dev.device, x, nullptr); + if (!ok) s.actor = VK_NULL_HANDLE, logf("vk: NVIDIA DLSS: the actor motion's pipeline was not made: camera motion alone"); + return ok; +} + +/// A range of guest memory as this recording binds it, by its device address (the pools and their mirrors have them) +uint64_t device_address(const BufferRef& r) { + if (!r) return 0; + VkBufferDeviceAddressInfo da{VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO}; + da.buffer = r.buffer; + const VkDeviceAddress base = vkGetBufferDeviceAddress(dev.device, &da); + return base ? base + r.offset : 0; +} + +constexpr uint64_t kPoseBytes = 4 << 20; // (a pose buffer: 280 bones of 48 bytes are 13 KB a draw, 300 of them) + +/// The skinned draws into the scene (cam's depth) with a pose of the frame before (DLSS's last), drawn into the motion +/// image over the camera's motion. Each one's bones and origin are copied on the GPU, from where the game has them, into +/// this frame's pose buffer, for this frame and the next. before: the frame before's camera without its jitter. history: +/// DLSS has the frame before. Returns the draws drawn +int actor_motion(VkCommandBuffer cmd, const Camera& cam, const float* before, uint32_t rw, uint32_t rh, VkImageView dview, + bool history) { + State& s = st(); + struct Timed { + State& s; + std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now(); + ~Timed() { s.actor_ns += (std::chrono::steady_clock::now() - t0).count(); } + } timed{s}; + std::vector skins = std::move(s.skins); + s.skins.clear(); + std::unordered_map old = std::move(s.poses); + s.poses.clear(); + static const bool off = env_switch("RX_VK_DLSS_ACTORS") == 0; + if (off || skins.empty() || !make_actor_pipeline()) return 0; + for (auto& b : s.pose_buffers) + if (!b.buffer) { + if (!create_device_buffer(kPoseBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, + b.buffer, b.memory)) + return b = {}, 0; + b.address = device_address({b.buffer, 0, kPoseBytes, 0}); + } + const uint64_t now = texture_frame_no(); + const State::PoseBuffer& here = s.pose_buffers[s.pose_at]; + const State::PoseBuffer& there = s.pose_buffers[s.pose_at ^ 1]; + static const uint32_t tolerance = env_switch("RX_VK_DLSS_ACTOR_TOL") >= 0 ? (uint32_t)env_switch("RX_VK_DLSS_ACTOR_TOL") : 64; + std::vector draws; + std::vector counts; + uint32_t at = 0; + for (const auto& k : skins) { + if (k.k.depth != cam.depth) continue; + const uint32_t bytes = 16 + k.nbones * 48; + if (at + bytes > kPoseBytes) break; + // (this frame's pose, from where the game has it: its bones, then its origin at the object's 0x40) + const BufferRef bones = gpu_buffer_bind(k.k.bones, (uint64_t)k.nbones * 48, false); + const BufferRef origin = gpu_buffer_bind(k.k.object + 0x40, 12, false); + if (!bones || !origin) { + s.actor_skipped++; + continue; + } + const VkBufferCopy c0{origin.offset, at, 12}, c1{bones.offset, at + 16, (uint64_t)k.nbones * 48}; + vkCmdCopyBuffer(cmd, origin.buffer, here.buffer, 1, &c0); + vkCmdCopyBuffer(cmd, bones.buffer, here.buffer, 1, &c1); + s.poses[k.key] = {at, k.nbones, now}; + const uint32_t mine = at; + at += (bytes + 15) & ~15u; + const auto it = old.find(k.key); + if (!history || it == old.end() || it->second.frame != s.last_frame || it->second.nbones != k.nbones) { + s.actor_skipped++; + continue; + } + // (the vertices and indices where the game has them: its buffers as this recording binds them) + const uint64_t va = device_address(gpu_buffer_bind(k.k.vbuf, k.k.vsize, false)); + const uint64_t ia = device_address(gpu_buffer_bind(k.k.ibuf, (uint64_t)k.k.count << k.k.itype, false)); + if (!va || !ia || va % 4 || k.k.count <= 0 || k.k.itype < 0 || k.k.itype > 2) { + s.actor_skipped++; + continue; + } + ActorPush a{}; + a.vertices = va, a.count = (uint32_t)std::min(k.k.vsize / k.k.stride, UINT32_MAX); + dlss_actor::index_start(ia, k.k.itype, &a.indices, &a.first); + a.origins[0] = here.address + mine, a.origins[1] = there.address + it->second.at; + a.bones[0] = a.origins[0] + 16, a.bones[1] = a.origins[1] + 16; + a.nbones = k.nbones; + a.itype = (uint32_t)k.k.itype, a.base_vertex = k.k.base_vertex; + a.stride = k.k.stride / 4, a.pos = k.k.pos, a.sets = (uint32_t)k.k.sets; + for (int i = 0; i < 2; i++) a.joints[i] = k.k.joints[i], a.weights[i] = k.k.weights[i]; + a.formats = k.k.formats; + a.tolerance = tolerance; + draws.push_back(a); + counts.push_back((uint32_t)k.k.count); + } + s.pose_at ^= 1; + if (draws.empty()) return 0; + // (the cameras: this frame's as drawn, the frame before's without its jitter, the jitter, the render size) + State::Upload& u = s.uploads[s.upload_at]; + s.upload_at = (s.upload_at + 1) % (int)(sizeof s.uploads / sizeof s.uploads[0]); + if (!u.buffer && !create_buffer_cached(dlss_actor::kHeadWords * 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, u.buffer, u.memory, &u.map)) + return u = {}, 0; + float* head = (float*)u.map; + std::memcpy(head, cam.current, 64); + std::memcpy(head + 16, before, 64); + head[32] = cam.jitter[0], head[33] = cam.jitter[1], head[34] = (float)rw, head[35] = (float)rh; + flush_mapped(u.memory, dlss_actor::kHeadWords * 4); + full_barrier(cmd); // (the poses copied) + VkRenderingAttachmentInfo ca{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; + ca.imageView = s.motion.view, ca.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + ca.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, ca.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfo ri{VK_STRUCTURE_TYPE_RENDERING_INFO}; + ri.renderArea = {{0, 0}, {rw, rh}}, ri.layerCount = 1, ri.colorAttachmentCount = 1, ri.pColorAttachments = &ca; + vkCmdBeginRendering(cmd, &ri); + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, s.actor); + const VkViewport viewport{0, 0, (float)rw, (float)rh, 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, {rw, rh}}; + vkCmdSetViewport(cmd, 0, 1, &viewport); + vkCmdSetScissor(cmd, 0, 1, &scissor); + const VkDescriptorBufferInfo bi{u.buffer, 0, dlss_actor::kHeadWords * 4}; + const VkDescriptorImageInfo ii{s.point, dview, VK_IMAGE_LAYOUT_GENERAL}; + VkWriteDescriptorSet w[2]{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}}; + w[0].dstBinding = 0, w[0].descriptorCount = 1, w[0].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, w[0].pBufferInfo = &bi; + w[1].dstBinding = 1, w[1].descriptorCount = 1, w[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w[1].pImageInfo = ⅈ + vkCmdPushDescriptorSetKHR(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, s.actor_layout, 0, 2, w); + for (size_t i = 0; i < draws.size(); i++) { // (vertex i of a draw is its i-th index) + vkCmdPushConstants(cmd, s.actor_layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof draws[i], &draws[i]); + vkCmdDraw(cmd, counts[i], 1, 0, 0); + } + vkCmdEndRendering(cmd); + s.actor_draws += draws.size(), s.actor_frames++; + return (int)draws.size(); +} + +/// DLSS's preset: the DLSS model row's, RX_VK_DLSS_PRESET over it (upscaler_rows.h dlss_preset) +unsigned wanted_preset() { + static const char* const env = getenv("RX_VK_DLSS_PRESET"); + return upscaler_rows::dlss_preset(opts.dlss_model, env && *env ? env : nullptr); +} + +/// DLSS's feature for this output size and preset, made when either changes (cmd: its creation's work) +bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, uint32_t oh) { + State& s = st(); + const unsigned preset = wanted_preset(); + if (s.feature && s.feature_ow == ow && s.feature_oh == oh && rw <= s.feature_rw && rh <= s.feature_rh && + rw >= s.min_rw && rh >= s.min_rh && s.feature_preset == preset) + return true; + // (why it is made: in the log with its time, so a stall in play can be matched with one) + char why[160]; + if (!s.feature && !s.features_made) snprintf(why, sizeof why, "the first"); + else if (!s.feature) snprintf(why, sizeof why, "again after it was let go"); + else if (s.feature_ow != ow || s.feature_oh != oh) + snprintf(why, sizeof why, "the output went from %ux%u to %ux%u", s.feature_ow, s.feature_oh, ow, oh); + else if (s.feature_preset != preset) + snprintf(why, sizeof why, "preset %c, was %c (the DLSS model row)", upscaler_rows::preset_letter(preset), + upscaler_rows::preset_letter(s.feature_preset)); + else + snprintf(why, sizeof why, "the render size %ux%u left its range, %ux%u to %ux%u", rw, rh, s.min_rw, s.min_rh, + s.feature_rw, s.feature_rh); + if (s.feature) { + NVSDK_NGX_Handle* old = s.feature; + later([old] { NVSDK_NGX_VULKAN_ReleaseFeature(old); }); + s.feature = nullptr; + } + // (the quality mode whose range of render sizes holds this one: the network's 3/4 is DLSS's MaxQuality range) + // (DLAA's when the render size is the output's) + static const NVSDK_NGX_PerfQuality_Value scaled[] = {NVSDK_NGX_PerfQuality_Value_MaxQuality, NVSDK_NGX_PerfQuality_Value_DLAA, + NVSDK_NGX_PerfQuality_Value_Balanced, NVSDK_NGX_PerfQuality_Value_MaxPerf, + NVSDK_NGX_PerfQuality_Value_UltraPerformance}; + static const NVSDK_NGX_PerfQuality_Value same[] = {NVSDK_NGX_PerfQuality_Value_DLAA, NVSDK_NGX_PerfQuality_Value_MaxQuality}; + const bool aa = rw == ow && rh == oh; + const NVSDK_NGX_PerfQuality_Value* modes = aa ? same : scaled; + const size_t nmodes = aa ? 2 : 5; + NVSDK_NGX_PerfQuality_Value mode = modes[0]; + unsigned max_w = 0, max_h = 0, min_w = 0, min_h = 0; + bool found = aa; + if (aa) max_w = min_w = ow, max_h = min_h = oh; // (DLAA: the render size is the output's, one size) + for (size_t i = 0; i < nmodes && !found; i++) { + const NVSDK_NGX_PerfQuality_Value m = modes[i]; + unsigned w = 0, h = 0, xw = 0, xh = 0, nw = 0, nh = 0; + float sharp = 0; + if (NVSDK_NGX_FAILED(NGX_DLSS_GET_OPTIMAL_SETTINGS(s.caps, ow, oh, m, &w, &h, &xw, &xh, &nw, &nh, &sharp)) || !w) continue; + if (rw >= nw && rh >= nh && rw <= xw && rh <= xh) { + mode = m, max_w = xw, max_h = xh, min_w = nw, min_h = nh, found = true; + break; + } + } + if (!found) return fail("no DLSS mode takes this render size"); + // (the DLSS model row's preset for every mode, K by default: NVIDIA's transformer model. 0: NVIDIA's own per mode) + static_assert(NVSDK_NGX_DLSS_Hint_Render_Preset_K == 11 && NVSDK_NGX_DLSS_Hint_Render_Preset_L == 12 && + NVSDK_NGX_DLSS_Hint_Render_Preset_M == 13 && NVSDK_NGX_DLSS_Hint_Render_Preset_Default == 0); + for (const char* k : {NVSDK_NGX_Parameter_DLSS_Hint_Render_Preset_DLAA, NVSDK_NGX_Parameter_DLSS_Hint_Render_Preset_Quality, + NVSDK_NGX_Parameter_DLSS_Hint_Render_Preset_Balanced, NVSDK_NGX_Parameter_DLSS_Hint_Render_Preset_Performance, + NVSDK_NGX_Parameter_DLSS_Hint_Render_Preset_UltraPerformance, + NVSDK_NGX_Parameter_DLSS_Hint_Render_Preset_UltraQuality}) + NVSDK_NGX_Parameter_SetUI(s.params, k, preset); + NVSDK_NGX_DLSS_Create_Params cp{}; + cp.Feature.InWidth = max_w, cp.Feature.InHeight = max_h, cp.Feature.InTargetWidth = ow, cp.Feature.InTargetHeight = oh; + cp.Feature.InPerfQualityValue = mode; + // (LDR colour, already tonemapped: no exposure; motion at the render size, not jittered; depth not reversed) + cp.InFeatureCreateFlags = NVSDK_NGX_DLSS_Feature_Flags_MVLowRes; + const auto t0 = std::chrono::steady_clock::now(); + const NVSDK_NGX_Result r = NGX_VULKAN_CREATE_DLSS_EXT(cmd, 1, 1, &s.feature, s.params, &cp); + const double made_ms = std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); + note_work(); // (NGX's own commands, which vk_work.h does not see: the next full_barrier is not left out) + if (NVSDK_NGX_FAILED(r) || !s.feature) { + logf("vk: NVIDIA DLSS: its feature was not made (%#x) for %ux%u -> %ux%u", (unsigned)r, max_w, max_h, ow, oh); + s.feature = nullptr; + s.failed = true; + return false; + } + s.feature_rw = max_w, s.feature_rh = max_h, s.feature_ow = ow, s.feature_oh = oh, s.min_rw = min_w, s.min_rh = min_h; + s.feature_preset = preset; + s.features_made++; + logf("vk: NVIDIA DLSS: feature %llu made (%s; frame %llu, %.1f ms of CPU): %ux%u (%ux%u to %ux%u) -> %ux%u, mode %d, " + "preset %c%s", (unsigned long long)s.features_made, why, (unsigned long long)texture_frame_no(), made_ms, rw, rh, + min_w, min_h, max_w, max_h, ow, oh, (int)mode, upscaler_rows::preset_letter(preset), + preset ? "" : " (NVIDIA's default)"); + s.last_frame = 0; // (a new history) + return true; +} + +} // namespace + +bool dlss_built() { return true; } + +void dlss_instance_extensions(std::vector& want, const std::vector& all) { + if (env_switch("RX_VK_DLSS") == 0) return; + for (const auto& e : all) + if (!strcmp(e.extensionName, VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME)) { + want.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); + return; + } +} + +void dlss_device_extensions(std::vector& want, const std::vector& all) { + // (NGX's: VK_NVX_binary_import and VK_NVX_image_view_handle, with push descriptors and buffer device addresses, + // which the replay enables already: core 1.2's bufferDeviceAddress stands for VK_EXT_buffer_device_address. The NVX ones + // are on whenever the device has them, so the Insert menu's row needs no restart) + if (env_switch("RX_VK_DLSS") == 0) return; + bool both = true; + for (const char* name : {VK_NVX_BINARY_IMPORT_EXTENSION_NAME, VK_NVX_IMAGE_VIEW_HANDLE_EXTENSION_NAME}) { + bool have = false, listed = false; + for (const auto& e : all) have |= !strcmp(e.extensionName, name); + for (const char* w : want) listed |= !strcmp(w, name); + if (have && !listed) want.push_back(name); + both &= have; + } + // (NVIDIA's own extensions: without them this is not an NVIDIA GPU, or not one NGX runs on. With them, NGX says, in + // dlss_probe or at the first upscale: a GTX has them too) + if (!both) g_why = DLSS_NEEDS_RTX; +} + +void dlss_probe() { + if (dlss_why() != DLSS_OK || g_why.load(std::memory_order_relaxed) >= 0 || !dev.device) return; + State& s = st(); + std::lock_guard lk(s.mu); + ngx_init(); +} + +void dlss_note_scene_draw(const Texture* color0, uint32_t width, uint32_t height, Texture* depth, uint64_t camera, + uint64_t camera_size) { + if (!color0 || !depth || !camera || camera_size < 0x230 || width < 640 || height < 360) return; + if (color0->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || !dlss_wanted()) return; + State& s = st(); + std::lock_guard lk(s.mu); + const uint64_t now = texture_frame_no(); + if (s.cam.valid && s.cam.frame == now && (uint64_t)width * height <= (uint64_t)s.cam.width * s.cam.height) + return; // (the largest one of the frame wins) + float c[0x230 / 4]; + if (!memory_read_as_submitted(camera, c, sizeof c)) return; + // (the target's size at 0x120; the projection at 0x70, its w row (0, 0, -1, 0) at 0xa0) + if (c[0x120 / 4] != (float)width || c[0x124 / 4] != (float)height || c[0xa0 / 4] != 0.0f || c[0xa4 / 4] != 0.0f || + c[0xa8 / 4] != -1.0f || c[0xac / 4] != 0.0f) + return; + Camera& k = s.cam; + std::memcpy(k.current, c + 0x30 / 4, sizeof k.current); + // (the offset sead's projection gave the frustum, in its third column: 2 * offset, which moves the picture by -that) + k.jitter[0] = -c[0x78 / 4], k.jitter[1] = -c[0x88 / 4]; + k.width = width, k.height = height, k.depth = depth, k.depth_gen = texture_destroy_generation(), k.frame = now; + k.valid = true; +} + +uint32_t dlss_jitter_size(int axis) { + // (RX_VK_DLSS_JITTER=0: none. 0 too once DLSS has not drawn for a while: the camera as the game has it) + static const bool off = env_switch("RX_VK_DLSS_JITTER") == 0; + State& s = st(); + const uint64_t last = s.last_frame.load(std::memory_order_relaxed); + if (off || !dlss_wanted() || s.failed.load(std::memory_order_relaxed) || !last || texture_frame_no() - last > 30) return 0; + if (axis == 2) return s.jitter_phases.load(std::memory_order_relaxed); + return axis ? s.jitter_h.load(std::memory_order_relaxed) : s.jitter_w.load(std::memory_order_relaxed); +} + +int dlss_status(char* b, size_t n) { + State& s = st(); + const uint64_t last = s.last_frame.load(std::memory_order_relaxed); + const int mode = dlss_mode(); + if (!mode || s.failed.load(std::memory_order_relaxed) || !last || texture_frame_no() - last > 30) return 0; + std::lock_guard lk(s.mu); + // (its own video memory: the images and buffers it made here, and what NGX says its feature holds, when it says) + uint64_t own = 0; + for (const Image* im : {&s.color, &s.depth, &s.motion, &s.output}) + if (im->image) own += allocation_size(im->memory); + for (const auto& pb : s.pose_buffers) + if (pb.buffer) own += allocation_size(pb.memory); + for (const auto& u : s.uploads) + if (u.buffer) own += allocation_size(u.memory); + unsigned long long ngx = 0; + const bool ngx_known = s.caps && NVSDK_NGX_SUCCEED(NGX_DLSS_GET_STATS(s.caps, &ngx)); + const uint64_t draws = s.actor_draws - s.status_draws, frames = s.actor_frames - s.status_frames; + char feature[48] = ""; + if (ngx_known) snprintf(feature, sizeof feature, ", its feature %llu MB", ngx >> 20); + char passed[64] = ""; + if (s.status_passed) snprintf(passed, sizeof passed, ", %llu left to the game's network", (unsigned long long)s.status_passed); + snprintf(b, n, "; %s: %llu frames drawn%s, %llu features made, jitter %+.3f %+.3f px%s, actor motion %llu draws in %llu " + "frames (%.3f ms CPU a frame), its images %llu MB%s", + mode == 2 ? "DLAA" : "DLSS", (unsigned long long)s.status_drawn, passed, (unsigned long long)s.features_made, + s.status_jitter[0], s.status_jitter[1], s.status_motion ? "" : " (no motion)", + (unsigned long long)draws, (unsigned long long)frames, frames ? s.actor_ns / 1e6 / (double)frames : 0.0, + (unsigned long long)(own >> 20), feature); + s.status_draws = s.actor_draws, s.status_frames = s.actor_frames, s.actor_ns = 0, s.status_drawn = s.status_passed = 0; + return mode; +} + +void dlss_idle() { + State& s = st(); + if (!s.holding.load(std::memory_order_relaxed)) return; + std::lock_guard lk(s.mu); + const uint64_t now = texture_frame_no(); + if (now - s.asked <= 120) return; // (DLSS in use, or off for under 120 frames (2 s): kept, a row stepped through comes back at once) + if (s.feature) { + NVSDK_NGX_Handle* old = s.feature; + later([old] { NVSDK_NGX_VULKAN_ReleaseFeature(old); }); + s.feature = nullptr; + s.feature_rw = s.feature_rh = s.feature_ow = s.feature_oh = s.min_rw = s.min_rh = 0; + logf("vk: NVIDIA DLSS: not used for 120 frames (the last at frame %llu, now %llu): its feature and images let go; " + "the next upscale with it makes them again", (unsigned long long)s.asked, (unsigned long long)now); + } + for (Image* im : {&s.color, &s.depth, &s.motion, &s.output}) drop_image(*im); + for (auto& pb : s.pose_buffers) // (the actor motion's poses and cameras) + if (pb.buffer) { + const State::PoseBuffer gone = pb; + later([gone] { destroy_buffer(gone.buffer, gone.memory); }); + pb = {}; + } + for (auto& u : s.uploads) + if (u.buffer) { + const State::Upload gone = u; + later([gone] { destroy_buffer(gone.buffer, gone.memory); }); + u = {}; + } + s.poses.clear(), s.skins.clear(), s.model_bones.clear(); + if (!s.depth_views.empty()) { + auto old = std::move(s.depth_views); + s.depth_views.clear(); + later([old] { + for (const auto& [image, view] : old) vkDestroyImageView(dev.device, view, nullptr); + }); + } + s.last_frame = 0; // (a new history when it comes back) + free_dead(false); + s.holding = !s.dead.empty(); +} + +namespace { +/// The bones a skinned draw's vertices use (the largest joint with a weight, plus one), read once a model; 0 when its +/// buffers cannot be read +uint32_t bones_used(const DlssSkin& k) { + if (k.count <= 0 || k.count > (1 << 20) || k.itype < 0 || k.itype > 2 || !k.stride) return 0; + std::vector idx((size_t)k.count << k.itype); + if (!memory_read_as_submitted(k.ibuf, idx.data(), idx.size())) return 0; + uint32_t lo = UINT32_MAX, hi = 0; + for (int i = 0; i < k.count; i++) { + uint32_t v = 0; + std::memcpy(&v, idx.data() + ((size_t)i << k.itype), (size_t)1 << k.itype); + lo = std::min(lo, v), hi = std::max(hi, v); + } + const int64_t first = (int64_t)k.base_vertex + lo, n = (int64_t)hi - lo + 1; + if (first < 0 || n > (1 << 20) || (uint64_t)(first + n) * k.stride > k.vsize) return 0; + std::vector v((size_t)n * k.stride); + if (!memory_read_as_submitted(k.vbuf + (uint64_t)first * k.stride, v.data(), v.size())) return 0; + uint32_t most = 0; + for (int64_t i = 0; i < n; i++) { + const uint8_t* q = v.data() + (size_t)i * k.stride; + for (int set = 0; set < k.sets; set++) + for (int j = 0; j < 4; j++) { + if (!q[k.weights[set] + j]) continue; + uint32_t joint = 0; + if (k.formats & (dlss_actor::kByteJoints << set)) joint = q[k.joints[set] + j]; + else std::memcpy(&joint, q + k.joints[set] + 2 * j, 2); + most = std::max(most, joint + 1); + } + } + return most; +} +} // namespace + +void dlss_note_skinned_draw(const DlssSkin& k) { + static const bool off = env_switch("RX_VK_DLSS_ACTORS") == 0; + if (off || !dlss_wanted() || !k.depth || !k.bones || k.bones_size < 48 || !k.object || k.object_size < 0x4c) return; + State& s = st(); + std::lock_guard lk(s.mu); + if (s.skins.size() >= 512) s.skins.clear(); // (no upscale for a while: what was kept is stale) + struct Timed { + State& s; + std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now(); + ~Timed() { s.actor_ns += (std::chrono::steady_clock::now() - t0).count(); } + } timed{s}; + State::Skin d; + d.k = k; + const uint64_t model = dlss_actor::key(k.vbuf, k.ibuf, k.count, k.base_vertex, k.program, 0); + // (only the bones its vertices use are taken: a palette holds up to 280, a model uses a few dozen) + auto mb = s.model_bones.find(model); + if (mb == s.model_bones.end()) { + if (s.model_bones.size() > 4096) s.model_bones.clear(); + mb = s.model_bones.emplace(model, bones_used(k)).first; + } + d.nbones = (uint32_t)std::min(mb->second, std::min(k.bones_size / 48, 280)); + if (!d.nbones) return; + int nth = 0; // (the same model drawn twice in a frame: the n-th draw of it is the n-th the frame before) + for (const auto& o : s.skins) nth += dlss_actor::same_model(o.key, model); + d.key = dlss_actor::key(k.vbuf, k.ibuf, k.count, k.base_vertex, k.program, nth); + s.skins.push_back(std::move(d)); +} + +bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, VkImageView in_view, Texture* out, + VkImageView out_view, uint32_t out_level) { + if (!dlss_wanted()) return false; + State& s = st(); + std::lock_guard lk(s.mu); + if (!known) { // (another network of the upscale's shape: only the scene into the picture, the rest untouched and unsaid) + const Camera& c = s.cam; + // (the gameplay networks' own formats: they sample the scene's B10G11R11 colour and write the B10G11R11 picture. + // Other cooperative-matrix networks of the same shape (one RGBA8 into A2B10G10R10, RGB9E5 into B10G11R11) are not it) + if (!in || !out || in->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || + (out->width >> out_level) <= in->width) + return false; + // (one in the gameplay networks' formats whose input is not this frame's scene: counted for the status line) + if (!c.valid || texture_frame_no() - c.frame > 1 || in->width != c.width || in->height != c.height) + return s.status_passed++, false; + static std::atomic told{0}; + if (told.exchange(key) != key) logf("vk: NVIDIA DLSS: network %016llx is the gameplay upscale (%ux%u -> %ux%u)", + (unsigned long long)key, in->width, in->height, out->width >> out_level, + out->height >> out_level); + } + free_dead(false); + const Camera cam = s.cam; + s.cam.valid = false; // (each frame's own camera) + struct SkinsDone { // (the skinned draws, each frame's own too: actor_motion takes them, or they go here) + State& s; + ~SkinsDone() { s.skins.clear(); } + } skins_done{s}; + + if (s.failed) return fail("DLSS failed earlier"); + s.asked = texture_frame_no(), s.holding = true; // (not after a failure: what it holds is let go, dlss_idle) + if (!in || !in_view || !out || !out_view) return fail("no input or output view"); + if (!cam.valid || texture_frame_no() - cam.frame > 1) return fail("no scene camera this frame"); + if (cam.depth_gen != texture_destroy_generation()) return fail("the scene depth may be gone"); + const uint32_t rw = in->width, rh = in->height; + const uint32_t ow = std::max(1u, out->width >> out_level), oh = std::max(1u, out->height >> out_level); + if (rw != cam.width || rh != cam.height) return fail("the input is not the scene's size"); + if (cam.depth->width != rw || cam.depth->height != rh || !(cam.depth->usage & VK_IMAGE_USAGE_SAMPLED_BIT) || + cam.depth->samples != VK_SAMPLE_COUNT_1_BIT || !(cam.depth->aspect & VK_IMAGE_ASPECT_DEPTH_BIT)) + return fail("unexpected scene depth"); + if (out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || ow < rw || oh < rh || ow > rw * 3 || oh > rh * 3) + return fail("unexpected output"); + if (!ngx_init()) { + s.failed = true; + return fail("DLSS not available"); + } + if (!make_pipelines()) { + s.failed = true; + return fail("its shaders were not made"); + } + const VkImageView dview = depth_view(cam.depth); + if (!dview) return fail("no view of the scene depth"); + // (DLAA: DLSS at the render size, anti-aliasing alone; its picture then scaled to the output's by dlss_out.comp. With + // it the 3D is drawn at full scale, dlss_full_scale) + const bool dlaa = dlss_mode() == 2; + const uint32_t dw = dlaa ? rw : ow, dh = dlaa ? rh : oh; + // (from here commands are recorded, NGX's own too: whichever way this returns, the program's descriptor pushes are gone from + // the bind point and are pushed again) + struct Disturbed { + ~Disturbed() { descriptors_disturbed(); } + } disturbed; + if (!make_feature(cmd, rw, rh, dw, dh)) return false; + if (!make_image(cmd, s.color, VK_FORMAT_R16G16B16A16_SFLOAT, s.feature_rw, s.feature_rh) || + !make_image(cmd, s.depth, VK_FORMAT_R32_SFLOAT, s.feature_rw, s.feature_rh) || + !make_image(cmd, s.motion, VK_FORMAT_R16G16_SFLOAT, s.feature_rw, s.feature_rh, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) || + !make_image(cmd, s.output, VK_FORMAT_R16G16B16A16_SFLOAT, dw, dh)) + return fail("its images were not made"); + + const uint64_t now = texture_frame_no(); + const uint64_t last = s.last_frame; + const bool reset = !last || now - last > 2; + struct { + float reproject[16]; + float jitter[2]; + int32_t size[2]; + int32_t flags; + } pc{}; + static const bool show_motion = getenv("RX_VK_DLSS_DEBUG") && !strcmp(getenv("RX_VK_DLSS_DEBUG"), "motion"); + // (the motion without the jitter: from this frame's jittered clip space to the frame before's without its jitter. + // A jitter moves clip x by jitter * w: taken off the previous matrix's first two rows) + // (the frame before's camera as DLSS last drew it: the buffer's own slot for it, 0x180, holds zeros in this version) + float previous[16]; + std::memcpy(previous, s.last_vp, sizeof previous); + dlss_math::unjitter(previous, s.last_jitter); + const bool motion = !reset && dlss_math::reprojection(cam.current, previous, pc.reproject); + pc.jitter[0] = cam.jitter[0], pc.jitter[1] = cam.jitter[1]; + pc.size[0] = (int32_t)rw, pc.size[1] = (int32_t)rh, pc.flags = (motion ? 1 : 0) | (show_motion ? 2 : 0); + + full_barrier(cmd); // (the scene's colour and depth, written before) + { + const VkDescriptorImageInfo info[5] = {{s.point, in_view, VK_IMAGE_LAYOUT_GENERAL}, + {s.point, dview, VK_IMAGE_LAYOUT_GENERAL}, + {VK_NULL_HANDLE, s.color.view, VK_IMAGE_LAYOUT_GENERAL}, + {VK_NULL_HANDLE, s.depth.view, VK_IMAGE_LAYOUT_GENERAL}, + {VK_NULL_HANDLE, s.motion.view, VK_IMAGE_LAYOUT_GENERAL}}; + VkWriteDescriptorSet w[5]{}; + for (uint32_t i = 0; i < 5; i++) { + w[i] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + w[i].dstBinding = i, w[i].descriptorCount = 1, w[i].pImageInfo = &info[i]; + w[i].descriptorType = i < 2 ? VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + } + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s.prep); + vkCmdPushDescriptorSetKHR(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s.prep_layout, 0, 5, w); + vkCmdPushConstants(cmd, s.prep_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc); + vkCmdDispatch(cmd, (rw + 7) / 8, (rh + 7) / 8, 1); + } + full_barrier(cmd); + // (what moves by itself: the skinned draws drawn again over the camera's motion, with their own) + const int actors = actor_motion(cmd, cam, previous, rw, rh, dview, motion); + if (actors) full_barrier(cmd); + if (show_motion && actors) { // (RX_VK_DLSS_DEBUG=motion: the motion as it is now, the actors' in it) + pc.flags = 4; + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s.prep); + const VkDescriptorImageInfo info[5] = {{s.point, in_view, VK_IMAGE_LAYOUT_GENERAL}, + {s.point, dview, VK_IMAGE_LAYOUT_GENERAL}, + {VK_NULL_HANDLE, s.color.view, VK_IMAGE_LAYOUT_GENERAL}, + {VK_NULL_HANDLE, s.depth.view, VK_IMAGE_LAYOUT_GENERAL}, + {VK_NULL_HANDLE, s.motion.view, VK_IMAGE_LAYOUT_GENERAL}}; + VkWriteDescriptorSet w[5]{}; + for (uint32_t i = 0; i < 5; i++) { + w[i] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + w[i].dstBinding = i, w[i].descriptorCount = 1, w[i].pImageInfo = &info[i]; + w[i].descriptorType = i < 2 ? VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + } + vkCmdPushDescriptorSetKHR(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s.prep_layout, 0, 5, w); + vkCmdPushConstants(cmd, s.prep_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc); + vkCmdDispatch(cmd, (rw + 7) / 8, (rh + 7) / 8, 1); + full_barrier(cmd); + } + NVSDK_NGX_Resource_VK color = resource(s.color, false), depth = resource(s.depth, false), mv = resource(s.motion, false), + output = resource(s.output, true); + NVSDK_NGX_VK_DLSS_Eval_Params ep{}; + ep.Feature.pInColor = &color, ep.Feature.pInOutput = &output; + ep.pInDepth = &depth, ep.pInMotionVectors = &mv; + // (the jitter in render pixels, x right and y down) + float jitter_px[2]; + dlss_math::jitter_to_pixels(cam.jitter, rw, rh, jitter_px); + ep.InJitterOffsetX = jitter_px[0], ep.InJitterOffsetY = jitter_px[1]; + ep.InRenderSubrectDimensions = {rw, rh}; + ep.InReset = reset ? 1 : 0; + ep.InMVScaleX = ep.InMVScaleY = 1.0f; + const NVSDK_NGX_Result r = NGX_VULKAN_EVALUATE_DLSS_EXT(cmd, s.feature, s.params, &ep); + note_work(); // (as at its creation: the barrier after it, and the game's next one, are recorded) + if (NVSDK_NGX_FAILED(r)) { + logf("vk: NVIDIA DLSS: its evaluation failed (%#x): the game's upscale from now on", (unsigned)r); + s.failed = true; + return false; // (the network writes its output over whatever the evaluation did) + } + full_barrier(cmd); + { + const int32_t opc[2] = {(int32_t)ow, (int32_t)oh}; + const VkDescriptorImageInfo info[2] = {{dlaa ? s.linear : s.point, s.output.view, VK_IMAGE_LAYOUT_GENERAL}, + {VK_NULL_HANDLE, out_view, VK_IMAGE_LAYOUT_GENERAL}}; + VkWriteDescriptorSet w[2]{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}}; + w[0].dstBinding = 0, w[0].descriptorCount = 1, w[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + w[0].pImageInfo = &info[0]; + w[1].dstBinding = 1, w[1].descriptorCount = 1, w[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + w[1].pImageInfo = &info[1]; + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s.out); + vkCmdPushDescriptorSetKHR(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s.out_layout, 0, 2, w); + vkCmdPushConstants(cmd, s.out_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof opc, opc); + vkCmdDispatch(cmd, (ow + 7) / 8, (oh + 7) / 8, 1); + } + s.last_frame = now; + s.last_jitter[0] = cam.jitter[0], s.last_jitter[1] = cam.jitter[1]; + // (for the status line, dlss_status: the jitter DLSS was given, in render pixels) + s.status_jitter[0] = ep.InJitterOffsetX, s.status_jitter[1] = ep.InJitterOffsetY, s.status_motion = motion; + s.status_drawn++; + + std::memcpy(s.last_vp, cam.current, sizeof s.last_vp); + s.jitter_w = rw, s.jitter_h = rh, s.jitter_phases = (uint32_t)dlss_jitter_phases(rw, rh, dw, dh); + if (s.reason) logf("vk: NVIDIA DLSS draws the upscale again (frame %llu)", (unsigned long long)now); + s.reason = nullptr; + // (RX_VK_DLSS_DEBUG=alternate, a test's reference: every other frame the game's own upscale over DLSS's, which still + // draws and keeps its history. A frame of DLSS then has the game's picture of the frames either side to be measured against) + static const bool alternate = getenv("RX_VK_DLSS_DEBUG") && !strcmp(getenv("RX_VK_DLSS_DEBUG"), "alternate"); + if (alternate && (now & 1)) return false; + if (!s.told_first) { + s.told_first = true; + logf("vk: NVIDIA DLSS draws OoT's upscale: %ux%u -> %ux%u%s", rw, rh, ow, oh, show_motion ? " (its motion shown)" : ""); + } + return true; +} + +#else // (no NGX SDK in this build: DLSS is not there) + +bool dlss_built() { return false; } +void dlss_probe() {} +void dlss_instance_extensions(std::vector&, const std::vector&) {} +void dlss_device_extensions(std::vector&, const std::vector&) {} +void dlss_note_scene_draw(const Texture*, uint32_t, uint32_t, Texture*, uint64_t, uint64_t) {} +bool dlss_upscale(VkCommandBuffer, uint64_t, bool, Texture*, VkImageView, Texture*, VkImageView, uint32_t) { return false; } +void dlss_idle() {} +int dlss_status(char*, size_t) { return 0; } +uint32_t dlss_jitter_size(int) { return 0; } +void dlss_note_skinned_draw(const DlssSkin&) {} + +#endif + +} // namespace mkwvk diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h new file mode 100644 index 000000000..c53254ca2 --- /dev/null +++ b/gpu/vk/vk_dlss.h @@ -0,0 +1,75 @@ +// NVIDIA DLSS Super Resolution in place of OoT's gameplay upscale network (vk_dlss.cpp). Built in only with NVIDIA's NGX +// SDK (HOOKSHOT_NGX, scripts/ngx_sdk.py: Linux and Windows); without it these say DLSS is not there and the game's network runs. +#pragma once + +#include + +#include "vk_internal.h" + +namespace mkwvk { + +struct Texture; + +/// Whether this build has DLSS in it (HOOKSHOT_NGX) +bool dlss_built(); +/// Why DLSS cannot be chosen here (the Insert menu's Upscaler row says it), DLSS_OK when it can or is not known yet +enum DlssWhy { DLSS_OK, DLSS_NOT_BUILT, DLSS_ENV_OFF, DLSS_NEEDS_RTX, DLSS_NEEDS_DRIVER, DLSS_NO_RUNTIME, DLSS_WHY_COUNT }; +DlssWhy dlss_why(); +/// Asks NGX once whether DLSS runs on this device, so dlss_why knows before the first upscale (the menu, when it opens). +/// Nothing off an NVIDIA device with NGX's extensions, or in a build without DLSS +void dlss_probe(); +/// What is asked for: 0 off, 1 DLSS (in place of the game's gameplay upscale, at any render share), 2 DLAA (anti-aliasing at the render size, the 3D at full scale): +/// the Insert menu's Upscaler row (opts.dlss), or RX_VK_DLSS=0 / 1 / 2 over it. 0 when dlss_why() says it cannot run +int dlss_mode(); +bool dlss_wanted(); // (dlss_mode() != 0) +/// DLAA: the 3D drawn at full scale, whatever the Render resolution row says (rx_option 2 and 3) +bool dlss_full_scale(); +/// The instance's extensions DLSS needs, added to `want` when the loader has them (all: what it has) +void dlss_instance_extensions(std::vector& want, const std::vector& all); +/// The device's extensions DLSS needs, added to `want` when the device has them +void dlss_device_extensions(std::vector& want, const std::vector& all); +/// A draw into a scene-sized target (colour target 0 and the depth target), with the vertex stage's uniform buffer 3 at +/// `camera` (its size): the camera of the frame, when that buffer holds one +void dlss_note_scene_draw(const Texture* color0, uint32_t width, uint32_t height, Texture* depth, uint64_t camera, + uint64_t camera_size); +/// A skinned draw into a scene-sized target (vk_draw.cpp finds them by their vertex layout): what DLSS's motion needs to draw +/// it again with this frame's bones and the frame before's (actor motion: Link, the others, what moves by itself) +struct DlssSkin { + const Texture* depth = nullptr; // (its depth target: only the scene's are taken) + uint64_t vbuf = 0, vsize = 0; // (vertex stream 0) + uint32_t stride = 0; + uint32_t pos = 0; // (byte offsets in a vertex: the position, RGB32F or RGBA16F) + uint32_t joints[2]{}, weights[2]{}; // (RGBA16UI or RGBA8UI joints and RGBA8 weights, four a set; sets: 1 or 2) + int sets = 0; + uint32_t formats = 0; // (dlss_actor::Push::formats) + uint64_t ibuf = 0; + int itype = 0, count = 0, base_vertex = 0; + uint64_t bones = 0, bones_size = 0; // (the palette: 3 x 4 row-major matrices, model space to the camera's world) + uint64_t object = 0, object_size = 0; // (the object's buffer: its origin at 0x40) + uint64_t program = 0; +}; +void dlss_note_skinned_draw(const DlssSkin& k); + +/// A gameplay upscale's dispatch (program `key`; vk_draw.cpp's dlss_stand_in finds them: the 4/3 network fd029a317a9a4b94 at +/// the game's usual 3/4, the same-size pass 9dc98871eb7fc101 at full size, and one network of its own for each other share): its input as it samples it (in, in_view) and its output's storage +/// view (out at level 0). known: one of the gameplay networks by its key (the 4/3 one, the full-size pass); else a network of +/// that shape (the fixed shares' own), taken only when its input is this frame's scene and its output the picture, and left +/// to run without a word otherwise. true: DLSS wrote the output (the dispatch is left out); false: the network runs, the +/// reason logged +bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, VkImageView in_view, Texture* out, + VkImageView out_view, uint32_t out_level); + +/// Once a frame (the game's present): after 120 frames without DLSS (off, or the upscale not taken) its feature, images and +/// deferred frees are let go, so its video memory comes back. Nothing to do costs one atomic load +void dlss_idle(); + +/// For the renderer's status line (vk_replay.cpp status_tick): while DLSS or DLAA draws, its details since the last call +/// into b ("; DLSS: jitter ..., actor motion ..., its images ... MB") and 1 (DLSS) or 2 (DLAA); else 0 and b left alone +int dlss_status(char* b, size_t n); + +/// The scene's width (axis 0) or height (1) for the host's camera jitter (nx2/oot_jitter.py, HOS_OPT_DLSS_JITTER_W and _H): 0 when +/// there is to be none (DLSS not drawing, or RX_VK_DLSS_JITTER=0). Axis 2: the jitter sequence's length for DLSS's scale +/// (dlss_jitter_phases, HOS_OPT_DLSS_JITTER_PHASES) +uint32_t dlss_jitter_size(int axis); + +} // namespace mkwvk diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index 7d9c9d5c4..e9e349d02 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -30,6 +30,8 @@ #include "vk_paths.h" #include "vk_draw.h" +#include "dlss_actor.h" +#include "vk_dlss.h" #include "vk_dump.h" #include "vk_gprof.h" #include "vk_jobs.h" @@ -4967,6 +4969,73 @@ void bind_index(VkCommandBuffer cmd, VkBuffer buffer, VkDeviceSize offset, VkInd last_ib = {b, (uint64_t)offset, (uint32_t)type, true}; } +/// (hookshot, a diagnostic for NVIDIA DLSS's motion: vk_dlss.cpp) RX_VK_DLSS_TRACE=: every that many frames, one frame's +/// draws into targets at least 640 wide, logged: the program, its targets' formats, the geometry and the uniform buffers +static void dlss_trace_draw(const NvnState& s, const Program& p, const DrawArgs& d, const Targets& at) { + static const int every = getenv("RX_VK_DLSS_TRACE") ? atoi(getenv("RX_VK_DLSS_TRACE")) : 0; + if (every <= 0 || at.width < 640) return; + const uint64_t f = texture_frame_no(); + if (f % (uint64_t)every != (uint64_t)every / 2) return; + char line[3072]; + int n = snprintf(line, sizeof line, "vk: dlss-trace f%llu prog %016llx%s%s %ux%u c[", (unsigned long long)f, + (unsigned long long)p.key, d.mesh ? " mesh" : "", p.fallback ? "(fb)" : "", at.width, at.height); + for (int i = 0; i < at.n && n < 1800; i++) n += snprintf(line + n, sizeof line - n, "%s%d", i ? "," : "", (int)at.color_format[i]); + n += snprintf(line + n, sizeof line - n, "] d%d %s n%d i%d bv%d inst%d ib%llx g%d,%d,%d ind%llx", (int)at.depth_format, + d.indexed ? "idx" : "arr", d.count, d.first, d.base_vertex, d.instances, (unsigned long long)d.ibuf, + d.groups[0], d.groups[1], d.groups[2], (unsigned long long)d.indirect); + for (int v = 0; v < 4 && n < 1800; v++) + if (s.vbuf[v].address) n += snprintf(line + n, sizeof line - n, " vb%d=%llx/%llu/s%u", v, (unsigned long long)s.vbuf[v].address, + (unsigned long long)s.vbuf[v].size, s.vstream[v].stride); + n += snprintf(line + n, sizeof line - n, " va"); + for (int i = 0; i < s.nvattr && i < 16 && n < 1800; i++) { + const nvn_va_st a = *reinterpret_cast(&s.vattr[i]); + n += snprintf(line + n, sizeof line - n, "%s%x@%u:%u", i ? "," : "=", a.fmt, a.offset, a.stream); + } + for (const Stage& st : p.stages) { + const int ns = std::clamp(st.nvn_stage, 0, kNvnStages - 1); + n += snprintf(line + n, sizeof line - n, " st%d:", ns); + for (int u = 0; u < 16 && n < 1900; u++) + if (s.ubo[ns][u].addr) n += snprintf(line + n, sizeof line - n, " u%d=%llx/%llu", u, (unsigned long long)s.ubo[ns][u].addr, + (unsigned long long)s.ubo[ns][u].size); + for (int u = 0; u < 16 && n < 1900; u++) + if (s.ssbo[ns][u].addr) n += snprintf(line + n, sizeof line - n, " s%d=%llx/%llu", u, (unsigned long long)s.ssbo[ns][u].addr, + (unsigned long long)s.ssbo[ns][u].size); + } + logf("%s", line); +} + +/// (hookshot) A skinned draw into the scene, for NVIDIA DLSS's actor motion (vk_dlss.cpp). OoT's characters and the things that +/// move by themselves are indexed draws of one vertex stream: the position at attribute 0 (RGB32F or RGBA16F), the weights at 4 +/// (and 5, RGBA8), the joints at 6 (and 7, RGBA16UI or RGBA8UI), the bone palette in the vertex stage's uniform buffer 6 (NVN's +/// binding 3: 3 x 4 matrices, a whole number of them) and the object's origin in its buffer 4 (binding 1, at 0x40) +static void dlss_skin_check(const NvnState& s, const Program& p, const DrawArgs& d, const Targets& at) { + if (d.mesh || !d.indexed || d.indirect || d.instances != 1 || !at.n || !at.color[0] || !at.depth || at.width < 640 || + at.color_format[0] != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || s.nvattr < 7 || !dlss_wanted()) + return; + const auto attr = [&](int i) { return *reinterpret_cast(&s.vattr[i]); }; + const nvn_va_st pos = attr(0); + if ((pos.fmt != NVN_FORMAT_RGB32F && pos.fmt != NVN_FORMAT_RGBA16F) || pos.stream != 0) return; + const nvn_p_bind_buf& bones = s.ubo[0][3]; + if (!bones.addr || bones.size < 48 || bones.size % 48) return; + DlssSkin k; + k.formats = pos.fmt == NVN_FORMAT_RGBA16F ? dlss_actor::kHalfPosition : 0; + for (int set = 0; set < 2 && 6 + set < s.nvattr; set++) { + const nvn_va_st w = attr(4 + set), j = attr(6 + set); + if (w.fmt != NVN_FORMAT_RGBA8 || (j.fmt != NVN_FORMAT_RGBA16UI && j.fmt != NVN_FORMAT_RGBA8UI) || w.stream || j.stream || + w.offset % 4 || j.offset % 4) + break; + if (j.fmt == NVN_FORMAT_RGBA8UI) k.formats |= dlss_actor::kByteJoints << set; + k.weights[set] = w.offset, k.joints[set] = j.offset, k.sets = set + 1; + } + const uint32_t stride = s.vstream[0].stride; + if (!k.sets || !stride || stride % 4 || pos.offset % 4 || !s.vbuf[0].address) return; + k.depth = at.depth, k.vbuf = s.vbuf[0].address, k.vsize = s.vbuf[0].size, k.stride = stride, k.pos = pos.offset; + k.ibuf = d.ibuf, k.itype = d.itype, k.count = d.count, k.base_vertex = d.base_vertex; + k.bones = bones.addr, k.bones_size = bones.size, k.object = s.ubo[0][1].addr, k.object_size = s.ubo[0][1].size; + k.program = p.key; + dlss_note_skinned_draw(k); +} + void draw(VkCommandBuffer cmd, NvnState& s, const DrawArgs& d) { PROF_HOT(PROF_DRAW); FINE_N(FC_DRAWS, 1); @@ -4980,6 +5049,9 @@ void draw(VkCommandBuffer cmd, NvnState& s, const DrawArgs& d) { } const Targets at = bare_targets(s, targets(s)); if (!at.width || !at.height) SKIP_HERE(); + // (hookshot) the scene's camera and depth for NVIDIA DLSS (vk_dlss.cpp): the vertex stage's uniform buffer 3 (NVN's binding 0) + if (at.n && at.color[0] && at.depth) + dlss_note_scene_draw(at.color[0], at.width, at.height, at.depth, s.ubo[0][0].addr, s.ubo[0][0].size); uint8_t types[8]; for (int i = 0; i < 8; i++) { const VkFormat f = i < at.n ? at.color_format[i] : VK_FORMAT_UNDEFINED; @@ -5114,6 +5186,8 @@ void draw(VkCommandBuffer cmd, NvnState& s, const DrawArgs& d) { const VkPipeline pl = pe->pipeline; if (!pl) SKIP_HERE(); cur_program = &p; + dlss_trace_draw(s, p, d, at); + dlss_skin_check(s, p, d, at); if (!record_progs().empty() && std::find(record_progs().begin(), record_progs().end(), p.key) != record_progs().end()) record_draw(s, p, d, at); // (RX_VK_DUMP_EACH=,...: in a dump, the pass ends after each draw of those programs, so every one of them is @@ -5943,6 +6017,36 @@ static bool typed_upscale(VkCommandBuffer cmd, VkImageView input_view, VkImageVi if (temporal) probe_image(cmd, out, level, 0, key, 't'); return true; } +/// (hookshot) NVIDIA DLSS in place of the gameplay upscale network (vk_dlss.cpp), when it is on and its inputs are as expected: +/// the network's input as it samples it, its output's storage view +bool dlss_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { + // (the 4/3 network and the full-size pass by their keys; the game's other gameplay upscales, one network for each fixed + // share of the Render resolution row (67%: 6676619641320ba7, 58% f10782f9e58b6a7c, 50% 72ba090c70fe2614, 33% + // 0775215f8da720e6 at 2048x1152), by their shape: a network sampling one texture into one image. dlss_upscale takes those + // only when the texture is this frame's scene, in the gameplay networks' input format, and the image the picture) + if (!dlss_wanted() || p.stages.empty()) return false; + const Stage& st = p.stages[0]; + if (!gameplay_network(p.key) && !(st.info.uses_cooperative_matrix && st.info.texture_descriptors.size() == 1 && + st.info.image_descriptors.size() == 1)) + return false; + if (st.info.texture_descriptors.empty() || st.info.image_descriptors.empty()) return false; + size_t ntex = 0; // (build_descriptors' image infos: the textures', then the images') + for (const auto& d : st.info.texture_descriptors) ntex += d.count; + if (dw().imgs.size() <= ntex || !dw().imgs[0].imageView || !dw().imgs[ntex].imageView) return false; + Texture* in = nullptr; + const uint32_t in_handle = handle_of(s, st, st.info.texture_descriptors[0], 0); + if (in_handle) texture_view_of(in_handle, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Sample, VK_FORMAT_UNDEFINED, &in); + const auto& od = st.info.image_descriptors[0]; + Texture* out = nullptr; + uint32_t level = 0; + const uint32_t out_handle = handle_of(s, st, od, 0); + if (out_handle) texture_view_of(out_handle, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Storage, image_format(od.format), &out, &level); + const bool done = dlss_upscale(cmd, p.key, gameplay_network(p.key), in, dw().imgs[0].imageView, out, dw().imgs[ntex].imageView, level); + // (its actor motion may have drawn with a pipeline, viewport and descriptors of its own: the next draw sets all of its again) + last_dyn_valid = false, last_desc_valid = false, last_vb_valid = false, last_pipeline = VK_NULL_HANDLE; + vertex_set.on = false, last_ib.on = false; + return done; +} bool taau_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { if (!taau_key(p.key) || !dev.push_descriptor || !vkCmdPushDescriptorSetKHR || p.stages.empty()) return false; const Stage& st = p.stages[0]; @@ -6563,6 +6667,7 @@ void dispatch(VkCommandBuffer cmd, NvnState& s, uint32_t x, uint32_t y, uint32_t logf("%s", line); } } + if (dlss_stand_in(cmd, s, p)) return void(st_dispatches++); if (taau_stand_in(cmd, s, p)) return void(st_dispatches++); if (!p.compute_pipeline) SKIP_HERE(); dump_ssbos(cmd, p, "before"); diff --git a/gpu/vk/vk_internal.h b/gpu/vk/vk_internal.h index 7b2811d7e..31853f7ba 100644 --- a/gpu/vk/vk_internal.h +++ b/gpu/vk/vk_internal.h @@ -233,6 +233,9 @@ bool create_device_buffer(uint64_t size, VkBufferUsageFlags usage, VkBuffer& buf /// Device-local memory this process uses and what the driver allows it now (VK_EXT_memory_budget), summed over the /// device-local heaps; false when unknown bool vram_usage(uint64_t* used, uint64_t* budget, uint64_t* block_free = nullptr); +/// vram_usage asked of the driver now (VMA asks again for a new frame index), for the status line; false when the driver +/// gives no budget (VK_EXT_memory_budget): used is then the game's own allocations +bool vram_usage_now(uint64_t* used, uint64_t* budget); void vma_log_heaps(); PFN_vkGetInstanceProcAddr instance_proc(); // vk_main.cpp diff --git a/gpu/vk/vk_main.cpp b/gpu/vk/vk_main.cpp index bf851c70e..617d1a824 100644 --- a/gpu/vk/vk_main.cpp +++ b/gpu/vk/vk_main.cpp @@ -20,6 +20,8 @@ #include "vk_internal.h" #include "gpu_choice.h" #include "vk_menu.h" +#include "vk_dlss.h" +#include "vk_reflex.h" #include "vk_progress.h" namespace vkx { @@ -111,8 +113,16 @@ bool create_instance() { app.apiVersion = VK_API_VERSION_1_3; VkInstanceCreateInfo ci{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; ci.pApplicationInfo = &app; - ci.enabledExtensionCount = n_exts; - ci.ppEnabledExtensionNames = exts; + std::vector ext_list(exts, exts + n_exts); + { // (hookshot) what NVIDIA DLSS needs (vk_dlss.cpp; nothing in a build without it) + uint32_t n = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &n, nullptr); + std::vector all(n); + vkEnumerateInstanceExtensionProperties(nullptr, &n, all.data()); + dlss_instance_extensions(ext_list, all); + } + ci.enabledExtensionCount = (uint32_t)ext_list.size(); + ci.ppEnabledExtensionNames = ext_list.data(); // RX_VK_VALIDATION=1: the Khronos validation layer (when installed) const char* layer = "VK_LAYER_KHRONOS_validation"; if (getenv("RX_VK_VALIDATION")) ci.enabledLayerCount = 1, ci.ppEnabledLayerNames = &layer; @@ -235,6 +245,8 @@ bool create_device() { optional(VK_AMD_BUFFER_MARKER_EXTENSION_NAME, dev.buffer_marker); optional(VK_EXT_DEVICE_FAULT_EXTENSION_NAME, dev.device_fault); optional(VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME, dev.coop_matrix); + dlss_device_extensions(want, exts); // (hookshot: NVIDIA DLSS's, vk_dlss.cpp) + reflex_device_extensions(want, exts); // (hookshot: NVIDIA Reflex's, vk_reflex.cpp) // the queue: one family for graphics, compute and transfer vkGetPhysicalDeviceQueueFamilyProperties(dev.physical, &n, nullptr); @@ -314,6 +326,7 @@ bool create_device() { if (dev.view_2d_of_3d) *tail = &v2d3d, tail = &v2d3d.pNext; VkPhysicalDeviceCooperativeMatrixFeaturesKHR coop{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_COOPERATIVE_MATRIX_FEATURES_KHR}; if (dev.coop_matrix) *tail = &coop, tail = &coop.pNext; + reflex_device_features(tail); // (present ids, when NVIDIA Reflex's extensions are there) vkGetPhysicalDeviceFeatures2(dev.physical, &f); dev.view_2d_of_3d = dev.view_2d_of_3d && v2d3d.sampler2DViewOf3D; coop.cooperativeMatrixRobustBufferAccess = VK_FALSE; @@ -415,6 +428,7 @@ bool create_device() { if (compute_family != UINT32_MAX) vkGetDeviceQueue(dev.device, compute_family, 0, &dev.present_queue), dev.present_family = compute_family; if (dev.buffer_marker) buffer_markers_init(); coopmat_init(); + reflex_device_created(); if (dev.push_descriptor) { VkPhysicalDevicePushDescriptorPropertiesKHR pp{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PUSH_DESCRIPTOR_PROPERTIES_KHR}; VkPhysicalDeviceProperties2 p2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2}; @@ -511,6 +525,7 @@ bool create_swapchain(uint32_t width, uint32_t height) { } ci.clipped = VK_TRUE; ci.oldSwapchain = dev.swapchain; + reflex_swapchain_info(ci); // (NVIDIA Reflex: latency mode on) VkSwapchainKHR sc; result = vkCreateSwapchainKHR(dev.device, &ci, nullptr, &sc); if (result != VK_SUCCESS) { @@ -546,6 +561,7 @@ bool create_swapchain(uint32_t width, uint32_t height) { vkGetSwapchainImagesKHR(dev.device, sc, &n, dev.swap_images.data()); logf("vk: swapchain %ux%u, %u images, %s", width, height, n, mode == VK_PRESENT_MODE_MAILBOX_KHR ? "mailbox" : mode == VK_PRESENT_MODE_IMMEDIATE_KHR ? "immediate" : "fifo"); + reflex_swapchain_created(sc); // (NVIDIA Reflex: its sleep mode on the new swapchain) return true; } diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index d4f8ba76d..0e3bd44c2 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -9,6 +9,10 @@ // half changes it down or up, on a toggle flips it, on a slider's track sets it (or drag it); the wheel changes the value // under the cursor, else moves the selection; a right click is Esc. On the Cheats page and its lists a click is Enter (on a // choice's or an amount's left or right half, Left or Right) and a right click goes back. vk_menu_hit.h has the layout. +// (hookshot) The rows that take a restart (Row::restart) say "(after restart)" while their value is not the one the game +// started with, and the Graphics tab lists them. Closing the menu then asks whether to restart now (menu_restart.h): +// Later keeps the saved settings for the next start and is not asked again until they change; Restart now saves and +// closes the window as the player would, and the game starts again (WM_APP + 9: host/hos_relaunch.c). #include #include #include @@ -22,9 +26,14 @@ #include "vk_paths.h" #include "vk_menu.h" #include "vk_menu_hit.h" +#include "vk_draw.h" // (pipeline_cache_save: Restart now keeps what was built) #include "oot_cheat_items.h" #include "vk_startup_paint.h" #include "vk_work.h" // (after the others: counts the commands that do GPU work, for full_barrier) +#include "vk_dlss.h" +#include "vk_reflex.h" +#include "upscaler_rows.h" +#include "menu_restart.h" #ifndef HOOKSHOT_VERSION // (nx2/build.py gives it to this file: nx2/version.py) #define HOOKSHOT_VERSION "unknown" #endif @@ -49,9 +58,23 @@ struct Row { int* value; int lo, hi, step; const char* (*text)(int v); - const char* help; // (shown under the list while the row is selected) + const char* help; // (shown under the list while the row is selected: what the row is, the same whatever its value) + // (a line about the value chosen, under the help, or nullptr: hint(v, -1) is the one to show now for v, hint(v, 0), + // hint(v, 1), ... every one v can have (those that depend on the other rows or on the GPU), until nullptr, so the + // menu keeps the height of the tallest: measure_help) + const char* (*hint)(int v, int variant) = nullptr; + bool restart = false; // (read once, at the start: a change applies after a restart) bool preset_exempt = false; // (a change here leaves the Quality preset alone: the controls, the window, the counter) }; +/// A row's hints from a table, the value lo first (nullptr: none for that value) +const char* hint_in(const char* const* t, int n, int lo, int v, int variant) { + return variant <= 0 && v - lo >= 0 && v - lo < n ? t[v - lo] : nullptr; +} +#define MENU_HINTS(fn, lo, ...) \ + const char* fn(int v, int variant) { \ + static const char* const t[] = {__VA_ARGS__}; \ + return hint_in(t, (int)(sizeof t / sizeof t[0]), lo, v, variant); \ + } const char* tx_framegen(int v) { static const char* t[] = {"Off", "", "2x", "3x", "4x", "5x", "6x", "7x", "8x"}; return t[v]; @@ -72,11 +95,101 @@ const char* tx_sens(int v) { snprintf(b, sizeof b, "%.2f", v / 100.0); return b; } +// (the game's picture and the window it is shown in, as the last present had them: menu_note_present_sizes) +std::atomic present_sizes{0}; +bool filter_needed() { + const uint64_t v = present_sizes.load(std::memory_order_relaxed); + return upscaler_rows::filter_needed((unsigned)(v >> 48), (unsigned)(v >> 32) & 0xffff, (unsigned)(v >> 16) & 0xffff, + (unsigned)v & 0xffff); +} const char* tx_upscale(int v) { static const char* t[] = {"Off", "Catmull-Rom", "Lanczos"}; if ((unsigned)v >= sizeof t / sizeof t[0]) return t[0]; // (a setting past the row: the label, not the next bytes) return t[v]; } +/// The Window scaling filter row's value: its filter, or that none is needed (the saved value stays, for another window size) +const char* tx_filter(int v) { return filter_needed() ? tx_upscale(v) : "Not needed (window = output)"; } +const char* tx_reflex(int v) { + static const char* t[] = {"Off", "On", "On + Boost"}; + return t[reflex_available() ? std::clamp(v, 0, 2) : 0]; +} +/// Reflex: on the value, or that it is not available here (the row shows Off) +const char* hn_reflex(int v, int variant) { + static const char* const t[] = {"The game as without it.", "The driver paces the frames for less latency.", + "As On, and the GPU's clocks are kept up as well."}; + static const char* const none = "Not available here: it needs an NVIDIA GPU whose driver has VK_NV_low_latency2."; + if (variant < 0) return reflex_available() ? t[std::clamp(v, 0, 2)] : none; + return variant == 0 ? t[std::clamp(v, 0, 2)] : variant == 1 ? none : nullptr; +} +// The Upscaler (opts.dlss): what scales the 3D up to the output. DLSS and DLAA only where dlss_why() lets them run; the menu +// shows (and steps from) what runs, dlss_mode(), and keeps the saved choice for a PC where it can +namespace ur = upscaler_rows; +int upscaler() { return dlss_mode(); } +const char* tx_upscaler(int) { + static const char* t[] = {"Game's own", "NVIDIA DLSS", "NVIDIA DLAA"}; + return t[std::clamp(upscaler(), 0, 2)]; +} +/// The Upscaler: the one shown (what runs, upscaler()), and with the game's own, why DLSS and DLAA are not offered +const char* hn_upscaler(int v, int variant) { + static const char* const t[] = { + "The game's own network.", + "NVIDIA's, from a smaller 3D. Softer than the game's own: set Sharpness to around 80%. Things that move by " + "themselves can trail for now.", + "NVIDIA's, the 3D at full size with its edges smoothed; the slowest. Softer than the game's own: set Sharpness to " + "around 80%. Things that move by themselves can trail for now.", + }; + static const char* const why[DLSS_WHY_COUNT] = { + nullptr, + "DLSS and DLAA: not in this build (built without NVIDIA's DLSS SDK, see the readme).", + "DLSS and DLAA: off for this run (RX_VK_DLSS=0).", + "DLSS and DLAA: need an NVIDIA RTX GPU.", + "DLSS and DLAA: need a newer NVIDIA driver.", + "DLSS and DLAA: DLSS's runtime was not found (the game folder's ngx folder, see the readme).", + }; + // (now: the value shown and what runs here; every one: each value, and the game's own with each reason) + int w = 0; + if (variant < 0) v = std::clamp(upscaler(), 0, 2), w = (int)dlss_why(); + else if (v == ur::GAME) w = variant; + else if (variant > 0) return nullptr; + v = std::clamp(v, 0, 2); + if (w >= DLSS_WHY_COUNT) return nullptr; + if (v != ur::GAME || !why[w]) return t[v]; + static char b[DLSS_WHY_COUNT][256]; + snprintf(b[w], sizeof b[w], "%s %s", t[0], why[w]); + return b[w]; +} +/// The DLSS model row (opts.dlss_model): NVIDIA's network by its preset, or RX_VK_DLSS_PRESET's for this run +const char* dlss_preset_env() { + static const char* const e = getenv("RX_VK_DLSS_PRESET"); + return ur::preset_env(e) ? e : nullptr; +} +const char* tx_dlss_model(int v) { + static const char* const t[] = {"Transformer (K)", "Transformer 2 (M)", "Transformer 2 (L)", "NVIDIA default"}; + if (const char* e = dlss_preset_env()) { + static char b[48]; + const unsigned p = ur::dlss_preset(v, e); + if (p) snprintf(b, sizeof b, "Preset %c (RX_VK_DLSS_PRESET)", ur::preset_letter(p)); + else snprintf(b, sizeof b, "NVIDIA default (RX_VK_DLSS_PRESET)"); + return b; + } + return t[std::clamp(v, 0, ur::kModels - 1)]; +} +/// The DLSS model: the value chosen; variant 1: the game's own upscales (the row waits for DLSS or DLAA); variant 2: set for +/// this run by RX_VK_DLSS_PRESET +const char* hn_dlss_model(int v, int variant) { + static const char* const t[] = { + "NVIDIA's transformer model: the one tested most here.", + "NVIDIA's second-generation transformer, its pick for Performance: sharper, at a little more GPU time.", + "The second-generation transformer as NVIDIA tuned it for Ultra Performance, the smallest Render resolutions.", + "Lets DLSS's runtime pick for each mode: what it picks can change with a runtime update.", + }; + static const char* const unused = "Used by NVIDIA DLSS and DLAA: choose one in the Upscaler row."; + static const char* const env = "Set for this run by RX_VK_DLSS_PRESET."; + if (variant < 0) variant = dlss_preset_env() ? 2 : upscaler() == ur::GAME ? 1 : 0; + if (variant == 1) return unused; + if (variant == 2) return env; + return variant == 0 ? t[std::clamp(v, 0, ur::kModels - 1)] : nullptr; +} const char* tx_percent(int v) { static char b[8]; snprintf(b, sizeof b, "%d%%", v); @@ -101,7 +214,6 @@ int step_output_choice(int id, int dir) { Out shown_output() { return opts.handheld ? Out{1536, 864} : kOutputs[output_choice(opts.output_res)]; } -constexpr int kResPercent[] = {0, 100, 75, 67, 50}; // The size the game draws the 3D at for a fixed scale of its output, a twelfth of it: every twelfth asked came out exact at the three // outputs (2048x1152: 0.58 drew 1195x672, 0.6667 1366x768). So the scale the game is given is the twelfth too: 0.67 drew 2615x1471 at // 3840x2160, not the 2560x1440 of 8/12. @@ -113,17 +225,51 @@ void game_size(float scale, uint32_t& w, uint32_t& h) { } const char* tx_resolution(int v) { static char b[48]; - if (v <= 0 || v > 4) return "Auto (dynamic)"; + const int up = upscaler(); + if (up == ur::DLAA) return "Native (DLAA)"; + if (!ur::percent(v)) return "Auto (dynamic)"; + if (const char* mode = up == ur::DLSS ? ur::dlss_mode_name(v) : nullptr) { + snprintf(b, sizeof b, "%s (%d%%)", mode, ur::percent(v)); + return b; + } uint32_t w, h; - game_size(kResPercent[v] / 100.0f, w, h); - snprintf(b, sizeof b, "%d%% (%ux%u)", kResPercent[v], w, h); + game_size(ur::percent(v) / 100.0f, w, h); + snprintf(b, sizeof b, "%d%% (%ux%u)", ur::percent(v), w, h); return b; } +/// The Render resolution: the value chosen, for the Upscaler shown (every one: each value with each Upscaler) +const char* hn_resolution(int v, int variant) { + const int up = variant < 0 ? std::clamp(upscaler(), 0, 2) : variant; + if (up > 2) return nullptr; + if (up == ur::DLAA) + return "DLAA draws the 3D at full size: nothing to set here. Choose DLSS or the game's own upscaler to draw it " + "smaller."; + const int p = ur::percent(v); + if (up == ur::DLSS) { + switch (p) { + case 0: return "The game lowers it when the GPU is busy, and DLSS follows. Set Sharpness to around 80%."; + case 67: return "DLSS's sharpest mode. Set Sharpness to around 80%."; + case 58: return "Between Quality and Performance. Set Sharpness to around 80%."; + case 50: return "Faster, and softer. Set Sharpness to around 80%."; + case 33: return "DLSS's fastest mode, and its softest. Set Sharpness to around 80%."; + default: return nullptr; // (not one of DLSS's steps: the row moves to one) + } + } + if (!p) return "The game lowers it when the GPU is busy."; + if (p == 100) return "The 3D at the output's full size: the sharpest, and the slowest."; + return "A steady size: lower is faster, and softer."; +} +/// A row with nothing else to choose here: shown without its arrows, Left and Right leave it (and the saved value) alone +bool row_fixed(const int* value) { + return (value == &opts.reflex && !reflex_available()) || (value == &opts.dlss && ur::upscaler_fixed(dlss_why() == DLSS_OK)) || + (value == &opts.resolution && ur::resolution_fixed(upscaler())) || (value == &opts.upscale && !filter_needed()) || + (value == &opts.dlss_model && ur::model_fixed(upscaler(), dlss_preset_env() != nullptr)); +} const char* tx_output_res(int v) { - static char b[48]; + static char b[32]; v = output_choice(v); const Out o = kOutputs[v]; - snprintf(b, sizeof b, "%ux%u (%srestart)", o.w, o.h, v ? "" : "game, "); + snprintf(b, sizeof b, "%ux%u%s", o.w, o.h, v ? "" : " (game's)"); return b; } const char* tx_speed(int v) { return v ? "Real time" : "Per frame"; } @@ -132,18 +278,14 @@ const char* tx_text_speed(int v) { if ((unsigned)v >= sizeof t / sizeof t[0]) return t[0]; return t[v]; } -const char* tx_handheld(int v) { return v ? "On (restart)" : "Off (restart)"; } -const char* tx_ultrawide(int v) { return v ? "Monitor's shape (restart)" : "16:9 (restart)"; } -int gpu_device_at_start = 0; // (the row's value the game started with: menu_set_gpu_count) +const char* tx_ultrawide(int v) { return v ? "Monitor's shape" : "16:9"; } const char* tx_gpu(int v) { static char buf[300]; const int idx = v - 1; if (v <= 0) snprintf(buf, sizeof buf, "Auto"); else if (idx < (int)gpu_device_names.size()) snprintf(buf, sizeof buf, "%d: %s", idx, gpu_device_names[idx].c_str()); else snprintf(buf, sizeof buf, "%d: Device %d", idx, idx); - // (a GPU other than the one in use: it takes a restart, said where the choice is, as Handheld and Ultrawide say it. - // The whole name shows: the selected row slides through it, see marquee_pos) - if (v != gpu_device_at_start) strncat(buf, " (restart)", sizeof buf - strlen(buf) - 1); + // (the whole name shows: the selected row slides through it, see marquee_pos) return buf; } const char* tx_pad_layout(int v); @@ -178,9 +320,20 @@ const char* tx_mic(int v) { snprintf(buf, sizeof buf, "%s", n && *n ? n : "Microphone"); return buf; } +/// The Microphone row: Off, the default, a microphone of the list, or the chosen one while it is unplugged (variant 1) +const char* hn_mic(int v, int variant) { + if (v <= 0) return variant <= 0 ? "The game hears silence." : nullptr; + if (v == 1) + return variant <= 0 ? "The system's default recording device. On Windows, desktop apps must be allowed to use the " + "microphone (Settings, Privacy & security)." + : nullptr; + const bool missing = variant < 0 ? mic_missing && v == 2 + mic_listed : variant == 1; + if (variant > 1) return nullptr; + return missing ? "Not plugged in: Default stands in until it is back." : "This microphone, by its name. A change applies at once."; +} const char* tx_rumble(int v) { return v ? tx_percent(v) : "Off"; } const char* tx_robust(int v) { - static const char* t[] = {"Auto (restart)", "Off (restart)", "On (restart)"}; + static const char* t[] = {"Auto", "Off", "On"}; return (unsigned)v < 3 ? t[v] : t[0]; } const char* tx_preset(int v) { @@ -222,84 +375,146 @@ void apply_preset(int p) { default: break; } + // (the Upscaler is the player's: a preset never turns DLSS on or off. With DLSS its resolution is DLSS's nearest mode) + if (p > 0) opts.resolution = ur::snap(upscaler(), opts.resolution); sync_preset_budgets(p); } +MENU_HINTS(hn_preset, 0, "Each row as you set it. Changing most rows yourself comes back to Custom.", + "Resolution 50% (with DLSS its nearest mode), Lanczos, Sharpness 50%, 2x frames, Handheld profile.", + "Resolution 75% (with DLSS its nearest mode), Lanczos, Sharpness 35%, 2x frames, Handheld profile.", + "Resolution 100% (with DLSS its nearest mode), Catmull-Rom, Sharpness 25%, 2x frames.", + "Resolution 100% (with DLSS its nearest mode), Lanczos, Sharpness 30%, 2x frames.", + "Resolution 100% (with DLSS its nearest mode), Lanczos, Sharpness 40%, 3x frames.") +MENU_HINTS(hn_gpu, 0, "The best dedicated GPU.") +/// Frame generation: how many frames are made for each of the game's +const char* hn_framegen(int v, int variant) { + if (variant > 0 || v == 1) return nullptr; + if (v <= 0) return "Only the game's own frames: no added delay."; + static char b[9][96]; + snprintf(b[v], sizeof b[v], "%s between each two of the game's.", v == 2 ? "One made frame" : v == 3 ? "Two made frames" : v == 4 ? "Three made frames" + : v == 5 ? "Four made frames" : v == 6 ? "Five made frames" : v == 7 ? "Six made frames" : "Seven made frames"); + return b[std::clamp(v, 2, 8)]; +} +MENU_HINTS(hn_pace, 0, "As soon as it is ready.", "At an even interval: less stutter, at up to one frame of delay.") +MENU_HINTS(hn_output_res, 0, "The game's own size.", + "The sharpest on a 2560x1440 screen: nothing to scale to the window.", + "The sharpest on a 3840x2160 screen. The game's render targets get a bigger GPU pool, so a full-size 3D fits.", + "The sharpest on a 1920x1080 screen: the HUD and text pixel for pixel, nothing to scale to the window.") +/// The Window scaling filter: its filter, or that none is needed at this window's size +const char* hn_filter(int v, int variant) { + static const char* const t[] = {"The plain copy.", "Sharper.", "Sharper still; can ring on edges."}; + static const char* const none = "The window is the Output resolution's size: nothing to scale. The choice is kept for " + "another size."; + if (variant < 0) return filter_needed() ? t[std::clamp(v, 0, 2)] : none; + return variant == 0 ? t[std::clamp(v, 0, 2)] : variant == 1 ? none : nullptr; +} +/// Sharpness: with DLSS or DLAA, the amount that suits them +const char* hn_sharpness(int v, int variant) { + static const char* const dlss = "DLSS and DLAA look softer than the game's own upscaler: around 80% suits them."; + const bool with_dlss = variant < 0 ? upscaler() != ur::GAME : variant == 1; + if (variant > 1) return nullptr; + return with_dlss ? dlss : v == 0 ? "Off: the picture as it is." : nullptr; +} +MENU_HINTS(hn_handheld, 0, "Docked: the Output resolution's size.", "Lower resolutions, lighter on the GPU.") +MENU_HINTS(hn_ultrawide, 0, "The game's own shape.", "The camera widens to fill the screen. The HUD is still stretched.") +MENU_HINTS(hn_robust, 0, "On, on every card.", "Can be faster on some NVIDIA cards.", "Always on.") +MENU_HINTS(hn_fps, 0, nullptr, "With frame generation on, the game's own rate too.") +MENU_HINTS(hn_speed, 0, "A step each frame: the game slows down whenever it runs below 60 fps.", + "Normal speed down to 15 fps, by taking longer steps.") +MENU_HINTS(hn_text_speed, 0, "The game's own pace, with its pauses.", "Twice as fast.", "Four times as fast.", + "Each page at once, as pressing A does.") +MENU_HINTS(hn_background, 0, "Only while the game's window has the focus.", + "Also behind other windows, for streaming or other apps. The keyboard and mouse still need the game's window.") +MENU_HINTS(hn_mousecam, 0, "The mouse stands in for the right stick, with its curve and top speed.", + "The mouse turns the camera itself, one to one, as in a PC game.") +MENU_HINTS(hn_mouse_invert, 0, nullptr, "Moving the mouse forward tilts the camera down.") +MENU_HINTS(hn_gyro, 0, "The sticks alone aim.", + "Switch Pro controllers and pads in Switch mode, DualSense and DualShock 4. The value says whether the " + "connected pad has a gyroscope.") +MENU_HINTS(hn_pad_layout, 0, "The Switch's order: A on the right, B at the bottom, X at the top, Y on the left.", + "A at the bottom (action and talk), B on the right, X on the left, Y at the top.", + "A at the bottom, B on the right; X and Y as on the Switch.", + "Bindings of your own, from the Controls page.") + Row rows[] = { - // Graphics (11 rows) + // Graphics (the Upscaler only in a build with DLSS: row_shown) {TAB_GRAPHICS, "Quality preset", &opts.preset, 0, 5, 1, tx_preset, - "Automatically configures resolution, upscaling, frame generation, and dynamic VRAM limits tailored to your GPU tier."}, + "Sets the scaling rows, frame generation and memory limits for a GPU with that much memory. The Upscaler stays yours.", + hn_preset}, {TAB_GRAPHICS, "Fullscreen", &opts.fullscreen, 0, 1, 1, tx_onoff, - "Fills the whole screen with the game, without the window's border. The same as F11.", true}, + "Fills the whole screen with the game, without the window's border. The same as F11.", nullptr, false, true}, {TAB_GRAPHICS, "GPU device", &opts.gpu_device, 0, 8, 1, tx_gpu, - "Selects which graphics card Vulkan uses. Auto picks the best dedicated GPU. Applies after restart.", true}, + "Which graphics card the game runs on. Applies after a restart.", hn_gpu, true, true}, {TAB_GRAPHICS, "Frame generation", &opts.framegen, 0, 8, 1, tx_framegen, - "Makes extra frames between the game's own (2x up to 8x), for smoother motion. Adds a little input delay, and fast motion or camera cuts " - "can show artifacts."}, - {TAB_GRAPHICS, "Frame pacing", &opts.pace, 0, 1, 1, tx_onoff, - "Shows each frame at an even interval instead of as soon as it is ready. Less stutter, at up to one frame of delay."}, - {TAB_GRAPHICS, "Resolution", &opts.resolution, 0, 4, 1, tx_resolution, - "How sharp the 3D is drawn. Auto lets the game lower it when the GPU is busy. A fixed percentage stays steady: lower is " - "faster."}, + "Makes frames between the game's own, for smoother motion, at a little input delay. Fast motion or camera cuts can " + "show artifacts.", hn_framegen}, + {TAB_GRAPHICS, "Frame pacing", &opts.pace, 0, 1, 1, tx_onoff, "When each frame is shown.", hn_pace}, + {TAB_GRAPHICS, "NVIDIA Reflex", &opts.reflex, 0, 2, 1, tx_reflex, + "NVIDIA GPUs: each frame starts as late as it can, for less input delay. Not used with frame generation.", hn_reflex}, + // (the picture's way, top to bottom: the output's size, what fills it from the 3D, the 3D's size, then the window) {TAB_GRAPHICS, "Output resolution", &opts.output_res, 0, kLastOutput, 1, tx_output_res, - "The size the game draws its whole frame at, HUD included, before it is scaled to your window. The 3D is drawn at the " - "Resolution percentage of it. Docked only. Applies after a restart."}, - {TAB_GRAPHICS, "Upscaling", &opts.upscale, 0, 2, 1, tx_upscale, - "The filter that scales the picture to your window. Off is the plain copy. Catmull-Rom is sharper, Lanczos sharper still."}, + "The game's whole frame, HUD included, before it is scaled to the window. Docked only. Applies after a restart.", + hn_output_res, true}, + {TAB_GRAPHICS, "Upscaler", &opts.dlss, 0, 2, 1, tx_upscaler, "What scales the 3D up to the Output resolution.", + hn_upscaler}, + {TAB_GRAPHICS, "DLSS model", &opts.dlss_model, 0, ur::kModels - 1, 1, tx_dlss_model, + "Which of NVIDIA's DLSS networks draws the picture.", hn_dlss_model, false, true}, + {TAB_GRAPHICS, "Render resolution", &opts.resolution, 0, ur::kValues - 1, 1, tx_resolution, + "The share of the output the 3D is drawn at; the Upscaler fills in the rest.", hn_resolution}, + {TAB_GRAPHICS, "Window scaling filter", &opts.upscale, 0, 2, 1, tx_filter, + "How the finished picture is scaled to the window, when its size differs from the Output resolution.", hn_filter}, {TAB_GRAPHICS, "Sharpness", &opts.sharpness, 0, 100, 10, tx_percent, - "Sharpens the picture as it goes to the window, with or without an upscaling filter. Too much shows halos around edges."}, - {TAB_GRAPHICS, "Handheld profile", &opts.handheld, 0, 1, 1, tx_handheld, - "Runs the game as if the console were undocked: lower resolutions, lighter on the GPU. Applies after a restart."}, + "Sharpens the picture as it goes to the window, with any filter. Too much shows halos around edges.", hn_sharpness}, + {TAB_GRAPHICS, "Handheld profile", &opts.handheld, 0, 1, 1, tx_onoff, + "Runs the game as if the console were undocked. Applies after a restart.", hn_handheld, true}, {TAB_GRAPHICS, "Ultrawide", &opts.ultrawide, 0, 1, 1, tx_ultrawide, - "Monitor's shape widens the camera to fill a screen wider than 16:9. The HUD is still stretched. Applies after a restart."}, + "The camera's shape on a screen wider than 16:9. Applies after a restart.", hn_ultrawide, true}, {TAB_GRAPHICS, "Robust buffers", &opts.robust, 0, 2, 1, tx_robust, - "Bounds-checked buffer reads: a read past a buffer's end is dropped instead of stopping the GPU. Auto is on, on every card; " - "Off can be faster on some NVIDIA cards. Applies after restart."}, + "Bounds-checked buffer reads: a read past a buffer's end is dropped instead of stopping the GPU. Applies after a " + "restart.", hn_robust, true}, // Gameplay (3 rows) - {TAB_GAMEPLAY, "FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, - "Shows frames per second in the top right corner. With frame generation on, it also shows the game's own rate.", true}, - {TAB_GAMEPLAY, "Game speed", &opts.realtime, 0, 1, 1, tx_speed, - "Per frame: the game slows down whenever it runs below 60 fps. Real time: it keeps normal speed down to 15 fps by taking " - "longer steps."}, + {TAB_GAMEPLAY, "FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, "Frames per second in the top right corner.", hn_fps, + false, true}, + {TAB_GAMEPLAY, "Game speed", &opts.realtime, 0, 1, 1, tx_speed, "How the game keeps time when it runs below 60 fps.", + hn_speed}, {TAB_GAMEPLAY, "Text speed", &opts.text_speed, 0, 3, 1, tx_text_speed, - "How fast the letters of dialogue and other messages appear. Normal is the game's own pace, with its pauses. Instant shows " - "each page at once, as pressing A does."}, + "How fast the letters of dialogue and other messages appear.", hn_text_speed}, {TAB_GAMEPLAY, "Microphone", &opts.mic, 0, 2 + kMaxMics, 1, tx_mic, - "What the ocarina hears when the game's microphone option is on (playing an instrument, or humming). Default follows " - "Windows' default recording device. Windows must allow desktop apps to use the microphone.", true}, + "What the ocarina hears when the game's microphone option is on (playing an instrument, or humming).", hn_mic, false, + true}, // Controls (14 rows) {TAB_CONTROLS, "Controller in background", &opts.pad_background, 0, 1, 1, tx_onoff, - "On: the controller keeps playing the game while another window has the focus, for streaming or playing beside other " - "apps. The keyboard and mouse still need the game's window."}, - {TAB_CONTROLS, "Mouse camera", &opts.mouse_native, 0, 1, 1, tx_mousecam, - "Native: the captured mouse turns the camera itself, one to one, as in a PC game. As the stick: the mouse stands in for " - "the right stick, with its curve and top speed.", true}, + "Whether the controller plays the game while another window has the focus.", hn_background}, + {TAB_CONTROLS, "Mouse camera", &opts.mouse_native, 0, 1, 1, tx_mousecam, "How the captured mouse turns the camera.", + hn_mousecam, false, true}, {TAB_CONTROLS, "Mouse sensitivity", &opts.mouse_sens, 10, 500, 10, tx_sens, - "How far the camera turns for a given mouse movement, for either Mouse camera. At 1.00 native look turns a tenth of a " - "degree a count. The - and = keys change it in the game too.", true}, - {TAB_CONTROLS, "Invert mouse vertical", &opts.mouse_invert, 0, 1, 1, tx_onoff, - "Swaps up and down for the mouse: moving it forward tilts the camera down.", true}, + "How far the camera turns for a mouse movement, with either Mouse camera: at 1.00 native look turns 0.1 degree a " + "count. The - and = keys change it in game too.", nullptr, false, true}, + {TAB_CONTROLS, "Invert mouse vertical", &opts.mouse_invert, 0, 1, 1, tx_onoff, "Swaps up and down for the mouse.", + hn_mouse_invert, false, true}, {TAB_CONTROLS, "Invert camera X", &opts.invert_cam_x, 0, 1, 1, tx_onoff, - "Inverts horizontal look (left/right) on the right analog stick.", true}, + "Inverts horizontal look (left/right) on the right analog stick.", nullptr, false, true}, {TAB_CONTROLS, "Invert camera Y", &opts.invert_cam_y, 0, 1, 1, tx_onoff, - "Inverts vertical look (up/down) on the right analog stick.", true}, + "Inverts vertical look (up/down) on the right analog stick.", nullptr, false, true}, {TAB_CONTROLS, "Rumble strength", &opts.rumble, 0, 150, 25, tx_rumble, - "How strongly the controller rumbles, in percent of the game's rumble. Off turns it off.", true}, + "How strongly the controller rumbles, in percent of the game's rumble. Off turns it off.", nullptr, false, true}, {TAB_CONTROLS, "Motion controls", &opts.gyro, 0, 1, 1, tx_gyro, - "Aims and looks with the controller's gyroscope where the game uses motion, as on the Switch. Works with Switch Pro " - "controllers and pads in Switch mode, DualSense and DualShock 4. Shown beside it: whether the connected pad has one."}, + "Aiming and looking with the controller's gyroscope where the game uses motion, as on the Switch.", hn_gyro}, {TAB_CONTROLS, "Motion sensitivity X", &opts.gyro_sens_x, 10, 300, 10, tx_percent, - "How far the aim moves left and right for a given turn of the controller. 100% is the console's own.", true}, + "How far the aim moves left and right for a given turn of the controller. 100% is the console's own.", nullptr, false, + true}, {TAB_CONTROLS, "Motion sensitivity Y", &opts.gyro_sens_y, 10, 300, 10, tx_percent, - "How far the aim moves up and down for a given tilt of the controller. 100% is the console's own.", true}, - {TAB_CONTROLS, "Invert motion X", &opts.gyro_inv_x, 0, 1, 1, tx_onoff, - "Turns the motion aim's left and right around.", true}, - {TAB_CONTROLS, "Invert motion Y", &opts.gyro_inv_y, 0, 1, 1, tx_onoff, - "Turns the motion aim's up and down around.", true}, + "How far the aim moves up and down for a given tilt of the controller. 100% is the console's own.", nullptr, false, + true}, + {TAB_CONTROLS, "Invert motion X", &opts.gyro_inv_x, 0, 1, 1, tx_onoff, "Turns the motion aim's left and right around.", + nullptr, false, true}, + {TAB_CONTROLS, "Invert motion Y", &opts.gyro_inv_y, 0, 1, 1, tx_onoff, "Turns the motion aim's up and down around.", + nullptr, false, true}, {TAB_CONTROLS, "Layout preset", &opts.pad_layout, 0, 3, 1, tx_pad_layout, - "Nintendo (default Switch order), Xbox / PC (A for action/talk), or Swap Face. Individual bindings switch this to Custom."}, + "Which face button is which. Binding one yourself on the Controls page makes it Custom.", hn_pad_layout}, {TAB_CONTROLS, "Controls", nullptr, 0, 0, 0, nullptr, // (hookshot: opens its page) "Which gamepad button and which keys press each of the Switch's buttons."}, {TAB_GAMEPLAY, "Cheats", nullptr, 0, 0, 0, nullptr, @@ -396,9 +611,67 @@ constexpr int kMenuWidth = menu_hit::kWidth; // (at a scale of 1: 720 lines) int cur_tab = TAB_GRAPHICS; int tab_selected[TAB_COUNT] = {0, 0, 0}; +// ---- the rows that take a restart: their values at the start (menu_init, the GPU's at menu_set_gpu_count) ---- +namespace mr = menu_restart; +mr::State restart_state; +int restart_slot[kRows]; // (a row's slot in restart_state, -1 none) +const char* restart_names[mr::kMax]; +void restart_init() { + static bool done = false; + if (done) return; + done = true; + for (int i = 0; i < kRows; i++) { + restart_slot[i] = rows[i].restart && rows[i].value ? restart_state.add(*rows[i].value) : -1; + if (restart_slot[i] >= 0) restart_names[restart_slot[i]] = rows[i].name; + } +} +int restart_slot_of(const Row& r) { return restart_slot[&r - rows]; } +void restart_values(int* now) { + for (int i = 0; i < kRows; i++) + if (restart_slot[i] >= 0) now[restart_slot[i]] = *rows[i].value; +} +/// The rows whose value is not the one the game runs with (a bit a slot) +unsigned restart_pending() { + int now[mr::kMax]; + restart_values(now); + return restart_state.pending(now); +} +/// Whether closing the menu now asks to restart: something pending, not declined with Later, and a window to close +bool restart_ask() { + int now[mr::kMax]; + restart_values(now); + return app_window() && restart_state.ask(now); +} +std::string restart_list(const char* last) { + return mr::names(restart_names, restart_state.n, restart_pending(), ", ", last); +} +int ask_sel = 0; // (the restart question's choice: 0 Later, 1 Restart now) +/// "Restart needed: " and the pending rows, on one line of width: the last ones as "and N more" when they do not fit +std::string restart_line(HDC dc, int width) { + const unsigned all = restart_pending(); + std::string line; + for (int keep = restart_state.n; keep >= 0; keep--) { // (the first `keep` pending rows by name) + unsigned shown = 0; + int named = 0, more = 0; + for (int i = 0; i < restart_state.n; i++) + if ((all >> i) & 1) (named < keep ? (shown |= 1u << i, named++) : more++); + line = "Restart needed: " + mr::names(restart_names, restart_state.n, shown); + if (more) line += (named ? " and " : "") + std::to_string(more) + (named ? " more" : more > 1 ? " settings" : " setting"); + SIZE sz{}; + GetTextExtentPoint32A(dc, line.c_str(), (int)line.size(), &sz); + if (sz.cx <= width) break; + } + return line; +} + +/// A row on its tab: the Cheats one only with cheats on, the Upscaler only in a build with DLSS +bool row_shown(int i) { + return (i != kCheatsRow || cheats_enabled) && (rows[i].value != &opts.dlss || ur::upscaler_shown(dlss_built())) && + (rows[i].value != &opts.dlss_model || ur::model_shown(dlss_built())); +} int tab_row_count(int t) { int c = 0; - for (int i = 0; i < kRows; i++) if (rows[i].tab == (MenuTab)t && (i != kCheatsRow || cheats_enabled)) c++; + for (int i = 0; i < kRows; i++) if (rows[i].tab == (MenuTab)t && row_shown(i)) c++; return c; } /// The most rows a tab has: the menu keeps that height whichever tab is shown @@ -411,7 +684,7 @@ int max_tab_rows() { int tab_row_to_global(int t, int local_idx) { int count = 0; for (int i = 0; i < kRows; i++) { - if (rows[i].tab == (MenuTab)t && (i != kCheatsRow || cheats_enabled)) { + if (rows[i].tab == (MenuTab)t && row_shown(i)) { if (count == local_idx) return i; count++; } @@ -450,6 +723,10 @@ std::string ini_path() { } const char* ini_key(const Row& r) { if (r.value == &opts.output_res) return "output_res"; // (host/nn_applet.c reads it at the start) + // (rows renamed since their values were first saved keep their keys: an options file from before still loads) + if (r.value == &opts.resolution) return "resolution"; + if (r.value == &opts.upscale) return "upscaling"; + if (r.value == &opts.dlss) return "dlss"; // (as opts.dlss and RX_VK_DLSS) static char k[32]; size_t n = 0; for (const char* p = r.name; *p && n + 1 < sizeof k; p++) k[n++] = *p == ' ' ? '_' : (char)tolower(*p); @@ -569,7 +846,12 @@ void save() { /// A row's new value v, as the arrows set it (dir: 1 up, -1 down, for the rows that skip or wrap), saved unless !keep /// (a slider's drag: saved once it is let go) void set_row(Row& r, int v, int dir, bool keep = true) { - if (r.value == &opts.output_res) { + // (nothing else to choose: no change, no save. A saved choice that cannot run here is kept) + if (row_fixed(r.value)) return; + // (the scaling rows: only what can run here; the Render resolution row through the Upscaler's own steps) + if (r.value == &opts.dlss) v = ur::step_upscaler(upscaler(), dir, true); + if (r.value == &opts.resolution) v = ur::step(upscaler(), *r.value, dir); + if (r.value == &opts.output_res) { // (smallest to largest: kOutputOrder) v = step_output_choice(*r.value, dir); if (v == *r.value) return; } @@ -585,12 +867,19 @@ void set_row(Row& r, int v, int dir, bool keep = true) { apply_preset(*r.value); } else { *r.value = v > r.hi ? r.lo : v < r.lo ? r.hi : v; + // (another Upscaler: the Render resolution moves to the nearest of its steps, DLSS's modes or the game's) + if (r.value == &opts.dlss) opts.resolution = ur::snap(upscaler(), opts.resolution); if (!r.preset_exempt) { // (the controls, the window and the counter are not the preset's) opts.preset = 0; // Custom sync_preset_budgets(0); } } if (r.value == &opts.mic && can_mic()) mic_chosen(); // (hookshot) + { // (a row that takes a restart, or a preset that sets one: asked again at the next close) + int now[mr::kMax]; + restart_values(now); + restart_state.note(now); + } if (keep) save(); } @@ -679,7 +968,7 @@ const std::vector& bindable_keys() { } return k; } -int page = 0; // 0 the options, 1 the controls, 2 the cheats, 3 the teleport list +int page = 0; // 0 the options, 1 the controls, 2 the cheats, 3 the teleport list, 4 the inventory, 5 the restart question int csel = 0, ccol = 0; // the controls' row (kControls: reset) and column (0 the gamepad, 1 the keyboard) int listening = 0; // waiting for the new input: 1 a gamepad's, 2 a key bool listen_armed = false; // (once what started it was let go) @@ -774,6 +1063,7 @@ HFONT font(int px, int weight) { DEFAULT_PITCH, "Segoe UI"); } int help_px = 0; // (the help text's height: measure_help) +constexpr int kHintGap = 4, kHintIndent = 10; // (the value's line: below the row's help, in from its bar) /// The help text word-wrapped to width, a TextOutA a line (the POSIX GDI stand-in has no DrawTextA), drawn at (x, y) /// when draw: its height in pixels either way int wrap_help(HDC dc, const char* text, int width, int x, int y, bool draw) { @@ -810,7 +1100,8 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { HFONT title = font(20 * s, FW_SEMIBOLD), body = font(16 * s, FW_NORMAL); HGDIOBJ old = SelectObject(dc, title); SetTextColor(dc, RGB(236, 240, 236)); - const char* heading = page == 4 ? "Inventory" : page == 3 ? "Teleport" : page == 2 ? "Cheats" : "Hookshot"; + const char* heading = page == 5 ? "Restart needed" : page == 4 ? "Inventory" : page == 3 ? "Teleport" + : page == 2 ? "Cheats" : "Hookshot"; TextOutA(dc, 16 * s, 12 * s, heading, (int)strlen(heading)); { // (which build, beside the title, so a report can say it without opening the log; the release alone when the whole // line would run into the title) @@ -832,6 +1123,38 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { DeleteObject(small); } SelectObject(dc, body); + if (page == 5) { // (hookshot) the restart question, as the menu closes with a setting that takes a restart + SetTextColor(dc, RGB(236, 240, 236)); + const std::string ask = "Restart to apply " + restart_list(" and ") + "?"; + const int asked = wrap_help(dc, ask.c_str(), (int)w - 32 * s, 16 * s, 46 * s, true); // (one line or two) + HFONT small = font(14 * s, FW_NORMAL); + SelectObject(dc, small); + SetTextColor(dc, RGB(176, 186, 180)); + const char* lost = "Progress since your last save is lost."; + TextOutA(dc, 16 * s, std::min(46 * s + asked + 4 * s, 94 * s), lost, (int)strlen(lost)); + SelectObject(dc, body); + const char* const labels[2] = {"Later", "Restart now"}; + for (int i = 0; i < 2; i++) { + const int x = menu_hit::ask_button_x(i, s), y = menu_hit::kAskButtonY * s; + const int bw = menu_hit::kAskButtonW * s, bh = menu_hit::kAskButtonH * s; + const bool sel = i == ask_sel; + if (sel) startup_gdi::round_rect(dc, x - s, y - s, bw + 2 * s, bh + 2 * s, 7 * s, RGB(104, 186, 124)); // (a green edge) + startup_gdi::round_rect(dc, x, y, bw, bh, 6 * s, sel ? RGB(44, 72, 54) : RGB(30, 40, 34)); + SIZE sz{}; + GetTextExtentPoint32A(dc, labels[i], (int)strlen(labels[i]), &sz); + SetTextColor(dc, sel ? RGB(255, 255, 255) : RGB(196, 204, 198)); + TextOutA(dc, x + (bw - (int)sz.cx) / 2, y + (bh - (int)sz.cy) / 2, labels[i], (int)strlen(labels[i])); + } + SelectObject(dc, small); + SetTextColor(dc, RGB(140, 150, 144)); + const char* hint = "Left/Right choose Enter confirms Esc: Later"; + TextOutA(dc, 16 * s, 168 * s, hint, (int)strlen(hint)); + SelectObject(dc, old); + DeleteObject(small); + DeleteObject(title); + DeleteObject(body); + return; + } if (page == 1) { // (hookshot) the Controls page TextOutA(dc, 16 * s + 110 * s, 12 * s + 4 * s, "Controls", 8); const int rh = menu_hit::kCtlRowH * s, top = menu_hit::kCtlTop * s, xpad = menu_hit::kCtlPad * s, @@ -1031,8 +1354,9 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { SIZE nsz{}; GetTextExtentPoint32A(dc, r.name, (int)strlen(r.name), &nsz); const int room = (int)w - 18 * s - (18 * s + (int)nsz.cx + 12 * s); - const std::string label = r.value ? r.text(*r.value) : ">"; // (">": a page) - const bool arrows = r.value && sel; + std::string label = r.value ? r.text(*r.value) : ">"; // (">": a page) + if (r.value && r.restart && *r.value != restart_state.start[restart_slot_of(r)]) label += " (after restart)"; + const bool arrows = r.value && sel && !row_fixed(r.value); auto width = [&](const std::string& t) { SIZE z{}; GetTextExtentPoint32A(dc, t.c_str(), (int)t.size(), &z); @@ -1082,11 +1406,26 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { HFONT help = font(14 * s, FW_NORMAL); SelectObject(dc, help); SetTextColor(dc, RGB(176, 186, 180)); - wrap_help(dc, rows[tab_row_to_global(cur_tab, tab_selected[cur_tab])].help, (int)w - 32 * s, 16 * s, help_top + 4 * s, true); + { // (what the row is, then on a line of its own, set off by a bar, what its value does: measure_help keeps the room) + const Row& r = rows[tab_row_to_global(cur_tab, tab_selected[cur_tab])]; + const int about = wrap_help(dc, r.help, (int)w - 32 * s, 16 * s, help_top + 4 * s, true); + if (const char* hint = r.value && r.hint ? r.hint(*r.value, -1) : nullptr) { + const int y = help_top + 4 * s + about + kHintGap * s; + SetTextColor(dc, RGB(150, 166, 156)); + const int tall = wrap_help(dc, hint, (int)w - (32 + kHintIndent) * s, (16 + kHintIndent) * s, y, true); + fill(dc, 16 * s, y + 2 * s, 2 * s, tall - 4 * s, RGB(96, 176, 112)); + } + } SelectObject(dc, body); SetTextColor(dc, RGB(140, 150, 144)); const char* hint = "Arrows change Q/E or LB/RB: tabs Insert, F1 or Home closes"; // (fits 448 px at 16 px) TextOutA(dc, 16 * s, help_top + 4 * s + help_px + 10 * s, hint, (int)strlen(hint)); + if (cur_tab == TAB_GRAPHICS && restart_pending()) { // (the rows that take a restart, changed: measure_pending's room) + SelectObject(dc, help); + SetTextColor(dc, RGB(235, 205, 95)); + const std::string line = restart_line(dc, (int)w - 32 * s); + TextOutA(dc, 16 * s, help_top + 4 * s + help_px + 34 * s, line.c_str(), (int)line.size()); + } SelectObject(dc, old); DeleteObject(title); DeleteObject(body); @@ -1127,19 +1466,43 @@ void paint_fps(HDC dc, uint32_t w, uint32_t h, int s) { DeleteObject(label); DeleteObject(big); } -/// The tallest help text at scale s, in pixels: the menu keeps that height whichever row is selected +/// The tallest help text at scale s, a row's help and its value's line, in pixels: the menu keeps that height whichever +/// row is selected and whatever its value int measure_help(int s) { HDC dc = CreateCompatibleDC(nullptr); HFONT f = font(14 * s, FW_NORMAL); HGDIOBJ old = SelectObject(dc, f); int most = 0; - for (const Row& r : rows) most = std::max(most, wrap_help(dc, r.help, (kMenuWidth - 32) * s, 0, 0, false)); + for (const Row& r : rows) { // (the row's help, and the tallest line any of its values can have) + int hint = 0; + for (int v = r.lo; r.value && r.hint && v <= r.hi; v++) + for (int k = 0; const char* t = r.hint(v, k); k++) + hint = std::max(hint, wrap_help(dc, t, (kMenuWidth - 32 - kHintIndent) * s, 0, 0, false)); + most = std::max(most, wrap_help(dc, r.help, (kMenuWidth - 32) * s, 0, 0, false) + (hint ? kHintGap * s + hint : 0)); + } SelectObject(dc, old); DeleteObject(f); DeleteDC(dc); return most; } +/// The Graphics tab's "Restart needed" line, one line high (the room measure_pending keeps) +int pending_px = 0; +int measure_pending(int s) { + HDC dc = CreateCompatibleDC(nullptr); + HFONT f = font(14 * s, FW_NORMAL); + HGDIOBJ old = SelectObject(dc, f); + SIZE sz{}; + GetTextExtentPoint32A(dc, "Ag", 2, &sz); + SelectObject(dc, old); + DeleteObject(f); + DeleteDC(dc); + return (int)sz.cy; +} void redraw_menu(int s) { + if (page == 5) { + gdi_draw(menu_bmp, kMenuWidth * s, (unsigned)menu_hit::kAskHeight * s, paint_menu, s); + return; + } if (page == 3 || page == 4) { gdi_draw(menu_bmp, kMenuWidth * s, (100u + kTeleportLines * (unsigned)menu_hit::kListRowH) * s, paint_menu, s); return; @@ -1153,10 +1516,10 @@ void redraw_menu(int s) { return; } static int measured_at = 0; - if (measured_at != s) help_px = measure_help(s), measured_at = s; - // (title, the tab strip, the most rows a tab has, the rule and the help text, the hint line) - gdi_draw(menu_bmp, (uint32_t)(kMenuWidth * s), (uint32_t)((44 + 32 + 8 + max_tab_rows() * 28 + 8 + 10 + 30) * s + help_px), - paint_menu, s); + if (measured_at != s) help_px = measure_help(s), pending_px = measure_pending(s), measured_at = s; + // (title, the tab strip, the most rows a tab has, the rule and the help text, the hint line, the restart line) + gdi_draw(menu_bmp, (uint32_t)(kMenuWidth * s), + (uint32_t)((44 + 32 + 8 + max_tab_rows() * 28 + 8 + 10 + 30 + 6) * s + help_px + pending_px), paint_menu, s); } std::atomic game_frames{0}, shown_frames{0}; @@ -1288,11 +1651,12 @@ void menu_init() { if (const char* e = getenv("HOS_REALTIME_STEP")) opts.realtime = atoi(e) != 0; if (const char* e = getenv("HOS_DYNRES")) opts.resolution = atoi(e) != 0 ? 0 : 1; if (const char* e = getenv("HOS_OUTPUT_RES")) opts.output_res = output_choice(atoi(e)); + restart_init(); // (what this run started with: the rows that take a restart differ from it once changed) } bool resolution_auto() { return opts.resolution == 0; } int resolution_scale() { - const float s = opts.resolution > 0 && opts.resolution <= 4 ? kResPercent[opts.resolution] / 100.0f : 0; + const float s = upscaler_rows::percent(opts.resolution) / 100.0f; return s > 0 ? (int)(twelfths(s) * 10000.0f / 12.0f + 0.5f) : 0; // (the twelfth game_size labels) } @@ -1303,7 +1667,9 @@ void menu_set_gpu_count(int count) { if (opts.gpu_device > count) opts.gpu_device = 0; } } - gpu_device_at_start = opts.gpu_device; + restart_init(); + for (int i = 0; i < kRows; i++) // (the row's value the game started with, the cards counted) + if (rows[i].value == &opts.gpu_device) restart_state.restart_value(restart_slot[i], opts.gpu_device); } bool menu_is_open() { return open_now.load(std::memory_order_relaxed); } @@ -1312,6 +1678,10 @@ int menu_player_flight() { return cheats_enabled ? player_flight.load(std::memor int menu_free_camera() { return free_camera.load(std::memory_order_relaxed); } int menu_take_cheat_action() { return cheat_action.exchange(0, std::memory_order_relaxed); } +void menu_note_present_sizes(uint32_t picture_w, uint32_t picture_h, uint32_t window_w, uint32_t window_h) { + const auto c = [](uint32_t v) { return (uint64_t)std::min(v, 0xffffu); }; + present_sizes.store(c(picture_w) << 48 | c(picture_h) << 32 | c(window_w) << 16 | c(window_h), std::memory_order_relaxed); +} void menu_note_frame(bool game_frame) { shown_frames.fetch_add(1, std::memory_order_relaxed); if (game_frame) game_frames.fetch_add(1, std::memory_order_relaxed); @@ -1502,15 +1872,46 @@ void menu_tick() { last_x = mx, last_y = my, mouse_read = true; // menu as it opens leaves the selection alone) if (!mouse) ptr.pressed = 0, ptr.wheel = 0; } - if (press[0] || press[6]) { + // (closing with a setting that takes a restart changed: the restart question first, Later already chosen on it) + auto later = [&] { + int now[mr::kMax]; + restart_values(now); + restart_state.choose_later(now); + open_now = false, page = 0, changed = true; + }; + if ((press[0] || press[6]) && open_now.load() && page == 5) { + later(); + } else if ((press[0] || press[6]) && open_now.load() && restart_ask()) { + page = 5, ask_sel = 0, listening = 0, changed = true; + } else if (press[0] || press[6]) { open_now = !open_now.load(), changed = true; page = 0, listening = 0; // (the layout row shows the controls as they are: host/nn_hid.c keeps them, the ini's pad_ lines, and a saved // layout is not applied over them at start) if (open_now.load()) opts.pad_layout = detect_pad_layout(); + if (open_now.load()) dlss_probe(); // (whether the Upscaler row can offer DLSS here: NGX asked once) if (open_now.load()) refresh_mics(); // (the microphones as they are now) } - if (open_now.load() && page == 1) { // (hookshot) the Controls page + if (open_now.load() && page == 5) { // (hookshot) the restart question: Later (the default) or Restart now + if (mouse) { // (a button under the cursor chosen, a click on it pressed; the wheel changes it, a right click is Later) + const menu_hit::Hit hit = menu_hit::ask(mx, my, scale, mw, mh); + if (hit.part == menu_hit::kButton && (moved || ptr.pressed) && hit.index != ask_sel) ask_sel = hit.index, changed = true; + if ((ptr.pressed & HOS_MOUSE_LEFT) && hit.part == menu_hit::kButton) press[7] = true; + if (ptr.pressed & HOS_MOUSE_RIGHT) press[5] = true; + if (ptr.wheel) ask_sel ^= 1, changed = true; + } + if (press[1] || press[2] || press[3] || press[4] || tab_left || tab_right) ask_sel ^= 1, changed = true; + if (press[5] || (press[7] && ask_sel == 0)) { + later(); + } else if (press[7]) { // (Restart now: everything saved, then the window closes as the player would close it) + logf("menu: restart now, to apply %s", restart_list(", ").c_str()); + save(); + pipeline_cache_save(true); + open_now = false, page = 0, changed = true; + PostMessageA(app_window(), WM_APP + 9, 0, 0); + } + if (changed && open_now.load()) redraw_menu(scale); + } else if (open_now.load() && page == 1) { // (hookshot) the Controls page if (!can_bind()) page = 0; else if (listening) { if (press[5] || prof_now() > listen_end) listening = 0, changed = true; // (Esc, or nothing for a while) @@ -1763,7 +2164,8 @@ void menu_tick() { } } } - if (press[5]) open_now = false; + if (press[5] && restart_ask()) page = 5, ask_sel = 0, changed = true; // (the restart question first) + else if (press[5]) open_now = false; // (the tabs: LB and RB, Q and E, or Tab) if (tab_left) cur_tab = (cur_tab + TAB_COUNT - 1) % TAB_COUNT, changed = true; else if (tab_right) cur_tab = (cur_tab + 1) % TAB_COUNT, changed = true; diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index da7df6f48..7f0264dcb 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -9,9 +9,15 @@ namespace mkwvk { struct Options { int framegen = 0; // made frames: 0 off, or 2, 3, 4 times the game's frames int pace = 0; // frames shown on an even line (vk_framegen.cpp) - int resolution = 0; // the 3D's: 0 Auto (the game's dynamic resolution), else 100%, 75%, 67%, 50% of its full scale (nx2/oot_patches.py) - int output_res = 0; // the game's whole frame, docked: 0 its 2048x1152, 1 2560x1440, 2 3840x2160, 3 1920x1080 (host/nn_applet.c) - int upscale = 0; // to the window: 0 off (the plain copy to the window), 1 Catmull-Rom, 2 Lanczos + int resolution = 0; // the 3D's (Render resolution): 0 Auto (the game's dynamic resolution), else a share of the output, + // 1..6 = 100%, 75%, 67%, 50%, 58%, 33% (upscaler_rows.h; nx2/oot_patches.py) + int output_res = 0; // the game's whole frame, docked: 0 its 2048x1152, 1 2560x1440, 2 3840x2160, 3 1920x1080; read once, at the start (host/nn_applet.c) + int upscale = 0; // to the window (Window scaling filter, saved as upscaling): 0 off (the plain copy), 1 Catmull-Rom, 2 Lanczos + int reflex = 0; // NVIDIA Reflex: 0 off, 1 on, 2 on + boost (vk_reflex.cpp; NVIDIA GPUs with VK_NV_low_latency2) + int dlss = 0; // the Upscaler: 0 the game's own network, 1 NVIDIA DLSS, 2 NVIDIA DLAA (vk_dlss.cpp; a build with NVIDIA's + // SDK only). Saved as dlss + int dlss_model = 0; // DLSS's network, by its preset: 0 K (transformer), 1 M, 2 L (second-generation transformer), 3 NVIDIA's + // default (upscaler_rows.h model_preset; vk_dlss.cpp). RX_VK_DLSS_PRESET over it int sharpness = 0; // 0 to 100 int realtime = 1; // game time follows the clock (host/nn_vi.c); 0: per frame (slower frames, slower game) int handheld = 0; // the game's handheld profile: ~4 fps, softer; read once, at the start (host/nn_applet.c) @@ -57,6 +63,9 @@ int menu_player_flight(); int menu_free_camera(); int menu_take_cheat_action(); void menu_note_frame(bool game_frame); // (the frame rates the FPS counter shows) +/// The game's picture's size (the Output resolution, its crop) and the window's, at each present: the Window scaling filter +/// row says when it is not needed +void menu_note_present_sizes(uint32_t picture_w, uint32_t picture_h, uint32_t window_w, uint32_t window_h); /// Over the window's image (TRANSFER_DST_OPTIMAL), w x h: the menu and the FPS counter. slot: the frame in flight, 0 to 2. void menu_record(VkCommandBuffer cmd, VkImage dst, int slot, uint32_t w, uint32_t h); /// (hookshot) The start-up screen while the shaders are prepared (vk_main.cpp, vk_startup_paint.h), drawn by menu_record under the diff --git a/gpu/vk/vk_menu_hit.h b/gpu/vk/vk_menu_hit.h index b782c961f..882913672 100644 --- a/gpu/vk/vk_menu_hit.h +++ b/gpu/vk/vk_menu_hit.h @@ -19,6 +19,10 @@ constexpr int kCtlWidth = 600, kCtlTop = 64, kCtlRowH = 24, kCtlPad = 200, kCtlK constexpr int kCheatTop = 44, kCheatRowH = 28; // the cheats' rows, from kCheatTop constexpr int kListTop = 48, kListRowH = 24; // a list's lines (a section's heading is one too), from kListTop constexpr int kListValueRight = 16; // a cheat's or a list line's value ends kListValueRight from the right +// ---- the restart question (closing the menu with a setting that takes a restart): its two buttons, Later and Restart now ---- +constexpr int kAskHeight = 196; // the page's height (kWidth wide) +constexpr int kAskButtonY = 120, kAskButtonH = 34; +constexpr int kAskButtonX = 16, kAskButtonW = 150, kAskButtonGap = 16; // Later at kAskButtonX, Restart now after the gap enum Part { kNone, // off the menu's controls (still over the menu: a click does nothing) @@ -32,6 +36,7 @@ enum Part { kCtlCell, // a binding: index the row, side 0 the gamepad's, 1 the keyboard's kCheatRow, // the Cheats page: a row (index), side -1 the value's left half, 1 its right half, 0 the name kListLine, // a list (the teleport list, the inventory): a line as shown (index, 0 the top one), side as kCheatRow + kButton, // the restart question's buttons: index 0 Later, 1 Restart now }; struct Hit { Part part = kNone; @@ -149,4 +154,15 @@ inline Hit list_lines(int x, int y, int s, int w, int h, int count, const int* v return row_list(x, y, s, w, h, kListTop, kListRowH, count, value_l, kListLine); } +/// The restart question: (x, y) in its pixels, w x h its size, s its scale +inline int ask_button_x(int i, int s) { return (kAskButtonX + i * (kAskButtonW + kAskButtonGap)) * s; } +inline Hit ask(int x, int y, int s, int w, int h) { + Hit hit; + if (x < 0 || y < 0 || x >= w || y >= h) return hit.part = kOutside, hit; + if (y < kAskButtonY * s || y >= (kAskButtonY + kAskButtonH) * s) return hit; + for (int i = 0; i < 2; i++) + if (x >= ask_button_x(i, s) && x < ask_button_x(i, s) + kAskButtonW * s) hit.part = kButton, hit.index = i; + return hit; +} + } // namespace mkwvk::menu_hit diff --git a/gpu/vk/vk_ngx_msvc_rt.cpp b/gpu/vk/vk_ngx_msvc_rt.cpp new file mode 100644 index 000000000..f0b261051 --- /dev/null +++ b/gpu/vk/vk_ngx_msvc_rt.cpp @@ -0,0 +1,94 @@ +// (hookshot) NVIDIA DLSS in the llvm-mingw build of hookshot_vk.dll: what NVIDIA's static NGX loader (nvsdk_ngx_s.lib, built +// with Microsoft's compiler against its static C++ runtime, /MT) asks of that runtime and the MinGW one does not have. +// The C parts it shares with the static FFmpeg libraries (/GS's cookie and handler, _fltused) are host/mingw_msvc_shims.c, +// linked into this DLL too; these are the C++ ones. Its C library calls go to the UCRT (api-ms-win-crt-*), and its C++ frame +// handler is the system's (__CxxFrameHandler3, which the UCRT exports). nx2/build.py compiles this file only for the llvm-mingw +// build with NVIDIA's SDK (HOOKSHOT_NGX_SDK); the MSVC build (HOOKSHOT_MSVC=1) has Microsoft's own runtime and no need of it. +#if defined(__MINGW32__) && defined(HOOKSHOT_NGX) +#include + +#include +#include +#include + +extern "C" { + +// (/GS frames with C++ exception handling: the cookie check (skipped, as __GSHandlerCheck's in host/mingw_msvc_shims.c), then +// the C++ frame handler, as Microsoft's runtime does) +EXCEPTION_DISPOSITION __CxxFrameHandler3(struct _EXCEPTION_RECORD* record, void* frame, struct _CONTEXT* context, + void* dispatcher); +EXCEPTION_DISPOSITION __GSHandlerCheck_EH(struct _EXCEPTION_RECORD* record, void* frame, struct _CONTEXT* context, + void* dispatcher) { + return __CxxFrameHandler3(record, frame, context, dispatcher); +} + +// Thread-safe function-local statics, Microsoft's way (its thread_safe_statics.cpp): each static's guard is 0 before, -1 +// while one thread initialises it, then the epoch it was done in. A thread whose own epoch is older than a guard takes the +// lock and looks; the others skip it. One lock and one condition variable for all of them. +int _Init_global_epoch = INT_MIN; +thread_local int _Init_thread_epoch = INT_MIN; // (read by the compiled code itself, through the TLS index) + +static SRWLOCK init_lock = SRWLOCK_INIT; +static CONDITION_VARIABLE init_done = CONDITION_VARIABLE_INIT; + +void _Init_thread_header(int* once) { + AcquireSRWLockExclusive(&init_lock); + if (*once == 0) { + *once = -1; // (this thread initialises it) + } else { + while (*once == -1) { + SleepConditionVariableSRW(&init_done, &init_lock, 100, 0); + if (*once == 0) { // (the other thread's initialiser threw: this one tries) + *once = -1; + ReleaseSRWLockExclusive(&init_lock); + return; + } + } + _Init_thread_epoch = _Init_global_epoch; + } + ReleaseSRWLockExclusive(&init_lock); +} + +void _Init_thread_footer(int* once) { + AcquireSRWLockExclusive(&init_lock); + ++_Init_global_epoch; + *once = _Init_global_epoch; + _Init_thread_epoch = _Init_global_epoch; + ReleaseSRWLockExclusive(&init_lock); + WakeAllConditionVariable(&init_done); +} + +void _Init_thread_abort(int* once) { // (an initialiser that threw: the next thread tries again) + AcquireSRWLockExclusive(&init_lock); + *once = 0; + ReleaseSRWLockExclusive(&init_lock); + WakeAllConditionVariable(&init_done); +} + +// Microsoft's operator new and delete (its names: ??2@YAPEAX_K@Z, ??3@YAXPEAX@Z): the C heap. Only NVIDIA's code calls them, +// and frees only what it allocated, so they need not be libc++'s. Out of memory ends the process: an exception of libc++'s +// would not be one Microsoft's handlers catch. +[[noreturn]] static void msvc_fatal(const char* what) { + fprintf(stderr, "vk: NVIDIA DLSS: %s in NVIDIA's NGX code\n", what); + fflush(stderr); + abort(); +} + +void* msvc_operator_new(size_t size) __asm__("??2@YAPEAX_K@Z"); +void* msvc_operator_new(size_t size) { + if (void* p = malloc(size ? size : 1)) return p; + msvc_fatal("out of memory"); +} + +void msvc_operator_delete(void* p) __asm__("??3@YAXPEAX@Z"); +void msvc_operator_delete(void* p) { free(p); } + +// Microsoft's C++ library's error reports (std::_Xbad_alloc, std::_Xlength_error), which throw in its runtime +[[noreturn]] void msvc_xbad_alloc() __asm__("?_Xbad_alloc@std@@YAXXZ"); +void msvc_xbad_alloc() { msvc_fatal("out of memory (std::bad_alloc)"); } + +[[noreturn]] void msvc_xlength_error(const char* what) __asm__("?_Xlength_error@std@@YAXPEBD@Z"); +void msvc_xlength_error(const char* what) { msvc_fatal(what ? what : "std::length_error"); } + +} // extern "C" +#endif diff --git a/gpu/vk/vk_reflex.cpp b/gpu/vk/vk_reflex.cpp new file mode 100644 index 000000000..1d3a2302b --- /dev/null +++ b/gpu/vk/vk_reflex.cpp @@ -0,0 +1,238 @@ +// (hookshot) NVIDIA Reflex through VK_NV_low_latency2, without NVIDIA's Streamline (vk_reflex.h). +// +// While the Insert menu's NVIDIA Reflex row (Off / On / On + Boost; RX_VK_REFLEX=0 / 1 / 2 over it) is on, the swapchain is +// made with latency mode on (VkSwapchainLatencyCreateInfoNV) and its sleep mode set from the row; turning the row on or off +// makes the swapchain again (reflex_swapchain_stale, at the next present). Off is the game as without Reflex: no latency +// mode, no markers, no sleep, no present ids, nothing logged. The device's extensions are enabled whenever it has them, so +// the row needs no restart: enabled alone they do nothing, nothing of them is used until the row is on. Each frame the driver gets markers with the +// present's id: the game's simulation start (and input) once the sleep returns, its end and the rendering's submission start +// at the game's present (do_present), the submission's end before the present's submit, and the present's start and end +// around vkQueuePresentKHR. vkLatencySleepNV after the present waits until the driver wants the next frame to start: with +// sleep mode off it returns at once. +// +// What the markers stand for is approximate: OoT simulates on its main thread and renders on its presentation thread, and the +// replay records the GPU work as the game submits it; the sleep holds the presentation thread, which the main thread waits for. +// With frame generation on, presents come from the display thread: Reflex is not used then. +// +// RX_VK_REFLEX_LOG=1: every 300 frames with Reflex on, the driver's own timings (vkGetLatencyTimingsNV) averaged in the log: +// simulation start to present end, and to the GPU's end of rendering. +#include "vk_reflex.h" + +#include +#include +#include +#include +#include + +#include "vk_framegen.h" +#include "vk_menu.h" + +namespace mkwvk { + +namespace { + +struct Reflex { + bool have_ext = false, ready = false; + bool latency_next = false; // (the swapchain being made has latency mode on: reflex_swapchain_info) + bool latency = false; // (the current one has) + VkPhysicalDevicePresentIdFeaturesKHR present_id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR}; + VkSwapchainLatencyCreateInfoNV latency_info{VK_STRUCTURE_TYPE_SWAPCHAIN_LATENCY_CREATE_INFO_NV}; + PFN_vkSetLatencySleepModeNV set_mode = nullptr; + PFN_vkLatencySleepNV sleep = nullptr; + PFN_vkSetLatencyMarkerNV marker = nullptr; + PFN_vkGetLatencyTimingsNV timings = nullptr; + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkSemaphore wake = VK_NULL_HANDLE; // (timeline: the sleep's end) + uint64_t wake_value = 0; + uint64_t id = 1; // (the frame's present id) + int mode = -1; // (as set on the swapchain) + bool frame_marked = false; // (this frame's start markers given: its other markers count) + VkPresentIdKHR pid{VK_STRUCTURE_TYPE_PRESENT_ID_KHR}; + std::mutex mu; +}; +Reflex& rx() { + static Reflex r; + return r; +} + +int wanted_mode() { + static const char* e = getenv("RX_VK_REFLEX"); + return std::clamp(e && *e ? atoi(e) : opts.reflex, 0, 2); +} + +/// Usable now: the row on, the extension and its feature, a swapchain made with latency mode, and no frame generation (its +/// own display thread) +bool on() { + Reflex& r = rx(); + return r.ready && r.swapchain && r.latency && wanted_mode() > 0 && !framegen_on(); +} + +void mark(VkLatencyMarkerNV m) { + Reflex& r = rx(); + VkSetLatencyMarkerInfoNV i{VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV}; + i.presentID = r.id, i.marker = m; + r.marker(dev.device, r.swapchain, &i); +} + +void apply_mode() { + Reflex& r = rx(); + const int want = wanted_mode(); + if (want == r.mode) return; + VkLatencySleepModeInfoNV m{VK_STRUCTURE_TYPE_LATENCY_SLEEP_MODE_INFO_NV}; + m.lowLatencyMode = want >= 1, m.lowLatencyBoost = want == 2, m.minimumIntervalUs = 0; + const VkResult res = r.set_mode(dev.device, r.swapchain, &m); + logf("vk: NVIDIA Reflex: %s%s", want == 0 ? "off" : want == 1 ? "on" : "on + boost", + res == VK_SUCCESS ? "" : " (the driver refused the sleep mode)"); + r.mode = want; +} + +void log_timings() { + static const bool wanted = getenv("RX_VK_REFLEX_LOG") && atoi(getenv("RX_VK_REFLEX_LOG")) > 0; + Reflex& r = rx(); + if (!wanted || !r.timings || r.id % 300) return; + VkGetLatencyMarkerInfoNV g{VK_STRUCTURE_TYPE_GET_LATENCY_MARKER_INFO_NV}; + r.timings(dev.device, r.swapchain, &g); + if (!g.timingCount) return; + std::vector t(g.timingCount, {VK_STRUCTURE_TYPE_LATENCY_TIMINGS_FRAME_REPORT_NV}); + g.pTimings = t.data(); + r.timings(dev.device, r.swapchain, &g); + double sim_present = 0, gpu_end = 0, sim = 0; + int n = 0; + for (uint32_t i = 0; i < g.timingCount; i++) { + const auto& f = t[i]; + if (!f.simStartTimeUs || !f.presentEndTimeUs || f.presentEndTimeUs < f.simStartTimeUs) continue; + sim_present += (double)(f.presentEndTimeUs - f.simStartTimeUs); + sim += f.simEndTimeUs > f.simStartTimeUs ? (double)(f.simEndTimeUs - f.simStartTimeUs) : 0.0; + gpu_end += f.gpuRenderEndTimeUs > f.simStartTimeUs ? (double)(f.gpuRenderEndTimeUs - f.simStartTimeUs) : 0.0; + n++; + } + if (n) + logf("vk: NVIDIA Reflex (%s): over %d frames, simulation start to present end %.1f ms, to the GPU's end %.1f ms, " + "simulation %.1f ms", r.mode == 0 ? "off" : r.mode == 1 ? "on" : "on + boost", n, sim_present / n / 1000.0, + gpu_end / n / 1000.0, sim / n / 1000.0); +} + +} // namespace + +void reflex_device_extensions(std::vector& want, const std::vector& all) { + static const char* e = getenv("RX_VK_REFLEX"); + if (e && !strcmp(e, "-")) return; // (RX_VK_REFLEX=-: the extensions not even enabled) + bool ll = false, pid = false; + for (const auto& x : all) { + ll |= !strcmp(x.extensionName, VK_NV_LOW_LATENCY_2_EXTENSION_NAME); + pid |= !strcmp(x.extensionName, VK_KHR_PRESENT_ID_EXTENSION_NAME); + } + if (!ll || !pid) return; + for (const char* n : {VK_NV_LOW_LATENCY_2_EXTENSION_NAME, VK_KHR_PRESENT_ID_EXTENSION_NAME}) { + bool listed = false; + for (const char* w : want) listed |= !strcmp(w, n); + if (!listed) want.push_back(n); + } + rx().have_ext = true; +} + +void reflex_device_features(void**& tail) { + Reflex& r = rx(); + if (!r.have_ext) return; + *tail = &r.present_id, tail = &r.present_id.pNext; +} + +void reflex_device_created() { + Reflex& r = rx(); + if (!r.have_ext) return; + if (!r.present_id.presentId) { + logf("vk: NVIDIA Reflex: the device has no present ids: off"); + return; + } + r.set_mode = (PFN_vkSetLatencySleepModeNV)vkGetDeviceProcAddr(dev.device, "vkSetLatencySleepModeNV"); + r.sleep = (PFN_vkLatencySleepNV)vkGetDeviceProcAddr(dev.device, "vkLatencySleepNV"); + r.marker = (PFN_vkSetLatencyMarkerNV)vkGetDeviceProcAddr(dev.device, "vkSetLatencyMarkerNV"); + r.timings = (PFN_vkGetLatencyTimingsNV)vkGetDeviceProcAddr(dev.device, "vkGetLatencyTimingsNV"); + VkSemaphoreTypeCreateInfo tc{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO}; + tc.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE; + VkSemaphoreCreateInfo sc{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; + sc.pNext = &tc; + r.ready = r.set_mode && r.sleep && r.marker && vkCreateSemaphore(dev.device, &sc, nullptr, &r.wake) == VK_SUCCESS; + logf("vk: NVIDIA Reflex: %s", r.ready ? "available (VK_NV_low_latency2)" : "its functions are missing: off"); +} + +bool reflex_available() { return rx().ready; } + +void reflex_swapchain_info(VkSwapchainCreateInfoKHR& ci) { + Reflex& r = rx(); + r.latency_next = r.ready && wanted_mode() > 0; + if (!r.latency_next) return; // (Off: the swapchain as without Reflex) + r.latency_info.pNext = ci.pNext; + r.latency_info.latencyModeEnable = VK_TRUE; + ci.pNext = &r.latency_info; +} + +bool reflex_swapchain_stale() { + Reflex& r = rx(); + return r.ready && r.swapchain && r.latency != (wanted_mode() > 0); +} + +void reflex_swapchain_created(VkSwapchainKHR swapchain) { + Reflex& r = rx(); + if (!r.ready) return; + std::lock_guard lk(r.mu); + const bool was = r.latency; + r.swapchain = swapchain, r.mode = -1, r.frame_marked = false, r.latency = r.latency_next; + if (r.latency) apply_mode(); + else if (was) logf("vk: NVIDIA Reflex: off"); +} + +void reflex_present_begins() { + if (!on()) return; + Reflex& r = rx(); + std::lock_guard lk(r.mu); + if (!r.frame_marked) return; + mark(VK_LATENCY_MARKER_SIMULATION_END_NV); + mark(VK_LATENCY_MARKER_RENDERSUBMIT_START_NV); +} + +void reflex_submit_ends() { + if (!on()) return; + Reflex& r = rx(); + std::lock_guard lk(r.mu); + if (r.frame_marked) mark(VK_LATENCY_MARKER_RENDERSUBMIT_END_NV); +} + +const void* reflex_present_id(const void* next) { + if (!on()) return next; + Reflex& r = rx(); + std::lock_guard lk(r.mu); + if (!r.frame_marked) return next; + mark(VK_LATENCY_MARKER_PRESENT_START_NV); + r.pid.pNext = next, r.pid.swapchainCount = 1, r.pid.pPresentIds = &r.id; + return &r.pid; +} + +void reflex_presented() { + if (!on()) return; + Reflex& r = rx(); + std::lock_guard lk(r.mu); + if (r.frame_marked) mark(VK_LATENCY_MARKER_PRESENT_END_NV); +} + +void reflex_sleep() { + if (!on()) return; + Reflex& r = rx(); + { + std::lock_guard lk(r.mu); + apply_mode(); // (the menu's row may have changed) + if (r.frame_marked) log_timings(), r.id++; + VkLatencySleepInfoNV s{VK_STRUCTURE_TYPE_LATENCY_SLEEP_INFO_NV}; + s.signalSemaphore = r.wake, s.value = ++r.wake_value; + if (r.sleep(dev.device, r.swapchain, &s) != VK_SUCCESS) return; + } + VkSemaphoreWaitInfo w{VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO}; + w.semaphoreCount = 1, w.pSemaphores = &r.wake, w.pValues = &r.wake_value; + vkWaitSemaphores(dev.device, &w, 100000000ull); // (100 ms at most: a driver that never wakes it does not stop the game) + std::lock_guard lk(r.mu); + mark(VK_LATENCY_MARKER_SIMULATION_START_NV); + mark(VK_LATENCY_MARKER_INPUT_SAMPLE_NV); + r.frame_marked = true; +} + +} // namespace mkwvk diff --git a/gpu/vk/vk_reflex.h b/gpu/vk/vk_reflex.h new file mode 100644 index 000000000..797fa3de2 --- /dev/null +++ b/gpu/vk/vk_reflex.h @@ -0,0 +1,36 @@ +// NVIDIA Reflex through VK_NV_low_latency2 (vk_reflex.cpp): the driver paces the game's frames so that the CPU starts one +// as late as it can and the frame waits less in the queue, for less delay from input to the screen. +#pragma once + +#include + +#include "vk_internal.h" + +namespace mkwvk { + +/// The extensions it needs (VK_NV_low_latency2, VK_KHR_present_id), added to `want` when the device has them +void reflex_device_extensions(std::vector& want, const std::vector& all); +/// Its feature (presentId) into the device's feature chain (*tail), before the features are queried +void reflex_device_features(void**& tail); +/// After the device is made: whether Reflex can run (the feature there), its functions loaded +void reflex_device_created(); +/// Reflex can run on this device (VK_NV_low_latency2 and its functions): the Insert menu's row offers it only then +bool reflex_available(); +/// Into a swapchain's creation: latency mode on while the row is on, nothing while it is off (the chained struct lives until +/// the next call) +void reflex_swapchain_info(VkSwapchainCreateInfoKHR& ci); +/// The row was turned on or off since the swapchain was made: it is to be made again (present_image) +bool reflex_swapchain_stale(); +/// A swapchain made: its sleep mode set as the menu has it +void reflex_swapchain_created(VkSwapchainKHR swapchain); +/// The frame's markers, at the game's present: its simulation ended and its rendering's submission starts +void reflex_present_begins(); +/// Before the present's submit: the rendering's submission ended +void reflex_submit_ends(); +/// The present: a VkPresentIdKHR for VkPresentInfoKHR's pNext (or null), and the present's start marker +const void* reflex_present_id(const void* next); +void reflex_presented(); +/// After the present: the driver's sleep until the next frame should start, then that frame's start markers +void reflex_sleep(); + +} // namespace mkwvk diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 48b2db8e8..e6b315d98 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -11,6 +11,7 @@ // Without a window (HOS_HEADLESS) presents render into an image of their own; RX_VK_SHOTS= writes every // RX_VK_SHOT_EVERY-th one (default 300) there as a PNG. #include +#include #include #include #include @@ -25,6 +26,9 @@ #include #include +#include "vk_dlss.h" +#include "status_line.h" +#include "vk_reflex.h" #include "vk_draw.h" #include "vk_dump.h" #include "vk_gprof.h" @@ -2534,8 +2538,49 @@ void log_frame_stats() { } } +/// (hookshot) Once a game frame: every 600 a status line (the video memory, DLSS's details while it draws), and every 60 +/// a look at the driver's allowance, warned of once when the use passes it (again only after it was back under) +void status_tick() { + namespace sl = status_line; + static uint64_t frames = 0; + // (the slowest frame since the last line, present to present: a stall shows here, to be matched with what the log says + // around it, such as DLSS's feature made again) + static int64_t last_ns = 0, slowest_ns = 0; + const int64_t now_ns = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + if (last_ns) slowest_ns = std::max(slowest_ns, now_ns - last_ns); + last_ns = now_ns; + if (++frames % sl::kCheckEvery) return; + uint64_t used = 0, allowed = 0; + const bool known = vram_usage_now(&used, &allowed); + static bool told_unknown = false; + if (!known && !told_unknown) { + told_unknown = true; + logf("vk: status: the driver gives no video-memory budget (VK_EXT_memory_budget): only the game's own allocations are known"); + } + char dlss[320] = ""; + const bool line = frames % sl::kEvery == 0; + const int mode = line ? dlss_status(dlss, sizeof dlss) : dlss_mode(); // (dlss_status: since the last line, so only for one) + static bool over = false; + if (known && sl::crossed(over, used, allowed)) { + char w[200]; + sl::over_text(w, sizeof w, used, allowed, mode); + logf("%s", w); + } + if (!line) return; + char v[128]; + sl::vram_text(v, sizeof v, used, allowed, known); + logf("vk: status: %d frames, the slowest %.1f ms; %s%s", sl::kEvery, slowest_ns / 1e6, v, dlss); + slowest_ns = 0; +} + uint64_t present_value = 0; // (the present job's value, handed out as it came; 0: the next one) void do_present(const nvn_tex* t, const int crop[4]) { + // (hookshot) NVIDIA Reflex (vk_reflex.cpp): this frame's simulation ended, its submission starts; on the way out (after the + // present) the driver's sleep until the next frame should begin + dlss_idle(); // (NVIDIA DLSS: its video memory let go once it has not been used for a while) + status_tick(); // (the status line, and the video-memory warning) + reflex_present_begins(); + struct ReflexSleep { ~ReflexSleep() { reflex_sleep(); } } reflex_sleep_after; { // (hookshot, RX_VK_PASS_TRACE_AT=: the passes of the frames around these frame numbers, for finding what a frame is made of) static std::atomic seen{0}; static const int64_t at = getenv("RX_VK_PASS_TRACE_AT") ? atoll(getenv("RX_VK_PASS_TRACE_AT")) : 0; @@ -2614,6 +2659,12 @@ void do_present(const nvn_tex* t, const int crop[4]) { static const int64_t shot_from = getenv("RX_VK_SHOT_FROM") ? atoll(getenv("RX_VK_SHOT_FROM")) : -1; static const int64_t shot_to = getenv("RX_VK_SHOT_TO") ? atoll(getenv("RX_VK_SHOT_TO")) : -1; shot = shots && (presents % shot_every == 0 || (presents >= shot_from && presents <= shot_to)); + // (RX_VK_SHOT_GO=: every present while that file is there too, a test's run of frames) + static const char* shot_go = getenv("RX_VK_SHOT_GO"); + if (shots && shot_go && !shot) { + struct stat sb; + shot = stat(shot_go, &sb) == 0; + } // (RX_VK_SHOT_MADE=1: the frame interpolation made before it too, written beside it as vk_N_made.png) static const bool shot_made = getenv("RX_VK_SHOT_MADE") != nullptr; uint32_t mw = 0, mh = 0; @@ -3006,6 +3057,16 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI if (const uint64_t want = dev.swap_resize.exchange(0, std::memory_order_acquire)) { // (the window was resized: main's resize) std::lock_guard qk(dev.present_mu); create_swapchain((uint32_t)(want >> 32), (uint32_t)want); + } else if (reflex_swapchain_stale()) { // (NVIDIA Reflex turned on or off: its latency mode is the swapchain's) + std::lock_guard qk(dev.present_mu); + create_swapchain(dev.swap_extent.width, dev.swap_extent.height); + } + if (src) { // (the Window scaling filter's row: whether the window is the picture's size. Only the game's frames and the + // made ones: the start-up screen (no src) is drawn at the window's own size, which made the row read "Not + // needed" while the game's output was another size; until the first frame the size is not known: needed) + const bool cropped = crop && crop[2] > 0 && crop[3] > 0; + menu_note_present_sizes(cropped ? (uint32_t)crop[2] : sw, cropped ? (uint32_t)crop[3] : sh, dev.swap_extent.width, + dev.swap_extent.height); } menu_tick(); Frame& f = frames[frame_index]; @@ -3114,8 +3175,10 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI st_flushes0 = st_flushes, st_cbs0 = st_batched_cbs; std::memcpy(r0, st_reason, sizeof r0); } + reflex_submit_ends(); vkQueueSubmit2(dev.queue, 1, &si, f.done); VkPresentInfoKHR pi{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; + pi.pNext = reflex_present_id(nullptr); // (NVIDIA Reflex: the frame's present id, and its present's markers) pi.waitSemaphoreCount = 1; pi.pWaitSemaphores = &dev.swap_rendered[index]; pi.swapchainCount = 1; @@ -3133,6 +3196,7 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI } else { res = vkQueuePresentKHR(dev.queue, &pi); } + reflex_presented(); if (res < 0 && res != VK_ERROR_OUT_OF_DATE_KHR) { // (a present that failed: said, a few times) static int logged; if (logged++ < 8) logf("vk: present failed (VkResult %d%s)", (int)res, dev.present_queue ? ", on the presents' own queue" : ""); @@ -3666,13 +3730,15 @@ void fill_replay_table(MkwVkRenderer& t) { t.nvn_present = present; t.nvn_register_texture = register_texture_queued; t.nvn_event_signal = event_signal_queued; - t.option = [](int id) -> int { // (3: the fixed path's scale in 1/10000, vk_menu.cpp) + t.option = [](int id) -> int { // (3: the fixed path's scale in 1/10000, vk_menu.cpp; DLAA: full scale, vk_dlss.cpp) if (id == HOS_OPT_CHEAT_HEARTS) return menu_heart_protection(); if (id == HOS_OPT_CHEAT_FLIGHT) return menu_player_flight(); if (id == HOS_OPT_CHEAT_ACTION) return menu_take_cheat_action(); if (id == HOS_OPT_CHEAT_CAMERA) return menu_free_camera(); - return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? resolution_auto() : id == 3 ? resolution_scale() : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed : id == 6 ? opts.mouse_native : - id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 9 ? opts.rumble : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : id == HOS_OPT_GYRO ? opts.gyro : id == HOS_OPT_GYRO_SENS_X ? opts.gyro_sens_x : id == HOS_OPT_GYRO_SENS_Y ? opts.gyro_sens_y : id == HOS_OPT_GYRO_INV_X ? opts.gyro_inv_x : id == HOS_OPT_GYRO_INV_Y ? opts.gyro_inv_y : 0; + return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? resolution_auto() && !dlss_full_scale() : id == 3 ? (dlss_full_scale() ? 10000 : resolution_scale()) : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed : id == 6 ? opts.mouse_native : + id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 9 ? opts.rumble : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : id == HOS_OPT_GYRO ? opts.gyro : id == HOS_OPT_GYRO_SENS_X ? opts.gyro_sens_x : id == HOS_OPT_GYRO_SENS_Y ? opts.gyro_sens_y : id == HOS_OPT_GYRO_INV_X ? opts.gyro_inv_x : id == HOS_OPT_GYRO_INV_Y ? opts.gyro_inv_y : + id == HOS_OPT_DLSS_JITTER_W || id == HOS_OPT_DLSS_JITTER_H ? (int)dlss_jitter_size(id - HOS_OPT_DLSS_JITTER_W) : + id == HOS_OPT_DLSS_JITTER_PHASES ? (int)dlss_jitter_size(2) : 0; }; t.option_set = menu_option_set; t.nvn_register_sampler = register_sampler_queued; diff --git a/gpu/vk/vk_vma.cpp b/gpu/vk/vk_vma.cpp index d217223d3..91dcb51c6 100644 --- a/gpu/vk/vk_vma.cpp +++ b/gpu/vk/vk_vma.cpp @@ -7,6 +7,7 @@ #include "vk_internal.h" #include #include +#include #include namespace mkwvk { @@ -185,9 +186,15 @@ bool vram_usage(uint64_t* used, uint64_t* budget, uint64_t* block_free) { /// The system-memory heaps the GPU reaches: what the process uses there (the imported guest memory too: pinned by the /// driver) and what the driver allows it, asked now (a new frame index makes VMA ask the driver again); false when unknown +namespace { +std::atomic budget_frame{0}; // (VMA's frame index: a new one makes it ask the driver for the budgets again) +} +bool vram_usage_now(uint64_t* used, uint64_t* budget) { + vmaSetCurrentFrameIndex(vma, ++budget_frame); + return vram_usage(used, budget) && dev.memory_budget; +} bool system_heap_usage(uint64_t* used, uint64_t* budget) { - static uint32_t frame = 0; - vmaSetCurrentFrameIndex(vma, ++frame); + vmaSetCurrentFrameIndex(vma, ++budget_frame); VmaBudget b[VK_MAX_MEMORY_HEAPS]{}; vmaGetHeapBudgets(vma, b); uint64_t u = 0, t = 0; diff --git a/host/display.c b/host/display.c index 3b551fb22..e3ad7d799 100644 --- a/host/display.c +++ b/host/display.c @@ -17,6 +17,9 @@ #include "../gpu/render/rx.h" void rx_close_asked(void); /* host/rx_backend.c */ #include "../gpu/vk/vk_api.h" + +void hos_relaunch_ask(void); /* (host/hos_relaunch.c) */ +void hos_window_closed_exit(void); #ifndef WM_DPICHANGED #define WM_DPICHANGED 0x02E0 #endif @@ -243,11 +246,12 @@ static LRESULT CALLBACK wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { break; case WM_ERASEBKGND: return renderer ? 1 : DefWindowProcA(w, msg, wp, lp); + case WM_APP + 9: /* (the Insert menu's Restart now: closed as by the player, then started again: host/hos_relaunch.c) */ + hos_relaunch_ask(); + /* fall through */ case WM_CLOSE: rx_close_asked(); /* (a start still preparing the shaders keeps what it made) */ - hos_log("display: window closed, exiting"); - hos_log_flush(); - ExitProcess(0); + hos_window_closed_exit(); return 0; } return DefWindowProcA(w, msg, wp, lp); diff --git a/host/hos_main.c b/host/hos_main.c index 2c9f1fca4..2a58e2683 100644 --- a/host/hos_main.c +++ b/host/hos_main.c @@ -1220,6 +1220,18 @@ static int game_main(int argc, char **argv) { } #endif { // (started from its icon the game has nowhere to log to: hookshot_log.txt beside it, last run only, for players to send in) +#ifdef _WIN32 + { /* (started again by the Insert menu's Restart now (host/hos_relaunch.c): the run before still has its log open until + it exits, so wait for it (up to 10 s) before the log moves to .1, or the move fails and its log is overwritten) */ + const char *of = getenv("HOS_RESTART_OF"); + if (of && *of) { + HANDLE before = OpenProcess(SYNCHRONIZE, FALSE, (DWORD)strtoul(of, NULL, 10)); + if (before) WaitForSingleObject(before, 10000), CloseHandle(before); + SetEnvironmentVariableA("HOS_RESTART_OF", NULL); /* (not passed on: a later restart sets its own) */ + _putenv("HOS_RESTART_OF="); + } + } +#endif HANDLE err = GetStdHandle(STD_ERROR_HANDLE); if (!err || err == INVALID_HANDLE_VALUE || GetFileType(err) == FILE_TYPE_UNKNOWN) { char path[MAX_PATH + 32]; diff --git a/host/hos_relaunch.c b/host/hos_relaunch.c new file mode 100644 index 000000000..65ae060bf --- /dev/null +++ b/host/hos_relaunch.c @@ -0,0 +1,95 @@ +/* (hookshot) The Insert menu's Restart now (gpu/vk/vk_menu.cpp): it saves its options and asks the window to close for a + restart (WM_APP + 9: host/display.c, host/posix/display_sdl.c). The window closes as when the player closes it, and at + the very end of that, instead of only exiting, the game is started again: + Linux: the process becomes the game again (execve) with the arguments and the environment it started with + (/proc/self/cmdline and /proc/self/environ: hookshot_env.txt's lines are read again, not kept), so whatever waits + for it (the launcher, a stream, a test) still has it. The game is bin/hookshot on disk, never run from inside the + AppImage (hookshot-launch starts it with the AppImage's variables taken away), so /proc/self/exe is the game; + argv[0] on the PATH if that is gone (the folder built again meanwhile). + Windows: a new process of this executable (GetModuleFileNameW) with this one's command line (GetCommandLineW), in + the same folder and environment; then this one exits. */ +#include "hos.h" +#include +#include +#include +#include + +static volatile LONG relaunch_asked; + +void hos_relaunch_ask(void) { InterlockedExchange(&relaunch_asked, 1); } + +#ifdef _WIN32 +static void relaunch(void) { + wchar_t exe[MAX_PATH * 4]; + const DWORD n = GetModuleFileNameW(NULL, exe, MAX_PATH * 4); + if (!n || n >= MAX_PATH * 4) { + hos_log("restart: this executable's path was not found: the game only exits"); + return; + } + const wchar_t *line = GetCommandLineW(); + const size_t len = wcslen(line); + wchar_t *cmd = (wchar_t *)malloc((len + 1) * sizeof *cmd); /* (CreateProcessW may write into it) */ + if (!cmd) return; + memcpy(cmd, line, (len + 1) * sizeof *cmd); + STARTUPINFOW si; + PROCESS_INFORMATION pi; + memset(&si, 0, sizeof si); + si.cb = sizeof si; + /* (the new process waits for this one to end before it moves hookshot_log.txt to .1 and opens its own: this one still + has it open until it exits, and a rename of an open file fails on Windows, so the new run overwrote the old run's + log. hos_main.c reads HOS_RESTART_OF) */ + wchar_t pid[16]; + swprintf(pid, 16, L"%lu", (unsigned long)GetCurrentProcessId()); + SetEnvironmentVariableW(L"HOS_RESTART_OF", pid); + if (CreateProcessW(exe, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + } else { + hos_log("restart: the game could not be started again (error %lu): it only exits", (unsigned long)GetLastError()); + hos_log_flush(); + } + free(cmd); +} +#else +#include "hos_relaunch.h" +#include +#include +#include + +extern char **environ; + +static void relaunch(void) { + HosRelaunch r; + if (!hos_relaunch_command("/proc/self/cmdline", "/proc/self/environ", &r)) { + hos_log("restart: this process's command was not found: the game only exits"); + hos_log_flush(); + return; + } + /* (every file this process opened closes with it, as at an exit: the new one opens its own) */ + DIR *d = opendir("/proc/self/fd"); + if (d) { + const int skip = dirfd(d); + for (struct dirent *e; (e = readdir(d));) { + const int fd = atoi(e->d_name); + if (fd > 2 && fd != skip) fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); + } + closedir(d); + } + fflush(NULL); + execve("/proc/self/exe", r.argv, r.envp); + environ = r.envp; /* (execvp searches the PATH of the environment it is given: the one the game started with) */ + execvp(r.argv[0], r.argv); + hos_log("restart: the game could not be started again: it only exits"); + hos_log_flush(); +} +#endif + +/* The window was closed (host/display.c's WM_CLOSE, host/posix/display_sdl.c's quit): the game exits, and starts again + first when the menu asked for it */ +void hos_window_closed_exit(void) { + const int again = relaunch_asked != 0; + hos_log(again ? "display: window closed for a restart (the Insert menu's Restart now)" : "display: window closed, exiting"); + hos_log_flush(); + if (again) relaunch(); + ExitProcess(0); +} diff --git a/host/hos_relaunch.h b/host/hos_relaunch.h new file mode 100644 index 000000000..4036fc13e --- /dev/null +++ b/host/hos_relaunch.h @@ -0,0 +1,70 @@ +/* (hookshot) The command a restart starts again (host/hos_relaunch.c, the Insert menu's Restart now), as plain C: + on Linux the arguments and the environment the process started with, read from /proc/self/cmdline and + /proc/self/environ (strings ended by a NUL each). tests/test_menu_restart.py checks it. */ +#pragma once + +#include +#include +#include + +typedef struct { + char *args, *env; /* (the files' bytes, each string ended by a NUL) */ + char **argv, **envp; /* (into them, each ended by a NULL) */ + int argc, envc; +} HosRelaunch; + +/* A file's whole contents and one NUL more (a /proc file says it is empty: read until its end). NULL if it cannot be read */ +static inline char *hos_relaunch_slurp(const char *path, size_t *len) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + size_t n = 0, cap = 4096; + char *b = (char *)malloc(cap + 1); + for (size_t got; b && (got = fread(b + n, 1, cap - n, f)) > 0;) { + n += got; + if (n == cap) { + char *more = (char *)realloc(b, cap * 2 + 1); + if (!more) free(b); + b = more, cap *= 2; + } + } + fclose(f); + if (!b) return NULL; + b[n] = 0; + *len = n; + return b; +} + +/* The strings in blob (len bytes, each ended by a NUL; blob[len] is a NUL too, for a last one without its own) as a + NULL-ended array: their count, -1 out of memory */ +static inline int hos_relaunch_split(char *blob, size_t len, char ***out) { + int n = 0; + for (size_t i = 0; i < len; i++) + if (!blob[i] || i + 1 == len) n++; + char **v = (char **)malloc(sizeof *v * (size_t)(n + 1)); + if (!v) return -1; + int k = 0; + for (size_t i = 0; i < len && k < n;) { + v[k++] = blob + i; + while (i < len && blob[i]) i++; + i++; + } + v[k] = NULL; + *out = v; + return k; +} + +/* The command from those two files (/proc/self/cmdline, /proc/self/environ): 1, or 0 when either cannot be read or + there is no argument (argv[0]) */ +static inline int hos_relaunch_command(const char *cmdline, const char *environ_file, HosRelaunch *r) { + size_t an = 0, en = 0; + memset(r, 0, sizeof *r); + r->args = hos_relaunch_slurp(cmdline, &an); + r->env = hos_relaunch_slurp(environ_file, &en); + if (!r->args || !r->env || (r->argc = hos_relaunch_split(r->args, an, &r->argv)) < 1 || + (r->envc = hos_relaunch_split(r->env, en, &r->envp)) < 0) { + free(r->args), free(r->env), free(r->argv), free(r->envp); + memset(r, 0, sizeof *r); + return 0; + } + return 1; +} diff --git a/host/mingw_msvc_shims.c b/host/mingw_msvc_shims.c index 66a2c036e..fed51eff6 100644 --- a/host/mingw_msvc_shims.c +++ b/host/mingw_msvc_shims.c @@ -1,7 +1,8 @@ /* (hookshot) The llvm-mingw build (HOOKSHOT_MINGW, as TotK Recomp builds): the static FFmpeg libraries in the CPM cache were built with Microsoft's compiler and name a few things of its C runtime that the MinGW one does not have: the stack-buffer check (/GS), the float marker and the range-check report. Here they are, as the MSVC runtime defines them. The MSVC build - compiles nothing of this file. */ + compiles nothing of this file. NVIDIA's NGX library (nvsdk_ngx_s.lib, a build with NVIDIA DLSS) asks for the same: this file + is linked into hookshot_vk.dll too then, with gpu/vk/vk_ngx_msvc_rt.cpp for its C++ parts. */ #if defined(__MINGW32__) #include #include diff --git a/host/posix/display_sdl.c b/host/posix/display_sdl.c index 76c89ad65..a37eb8cea 100644 --- a/host/posix/display_sdl.c +++ b/host/posix/display_sdl.c @@ -24,6 +24,8 @@ int rx_start(void *hwnd); void rx_resize(int width, int height); void rx_close_asked(void); void hos_sdl_pads_pump(void); /* host/pad_sdl.c: controllers, on this thread, as SDL's queue wants */ +void hos_relaunch_ask(void); /* (host/hos_relaunch.c: the Insert menu's Restart now) */ +void hos_window_closed_exit(void); volatile LONG64 hos_present_count; /* (nvn_native.c: the game's presents) */ __attribute__((visibility("default"))) volatile uint8_t hos_posix_keys[256]; @@ -145,13 +147,15 @@ void hos_renderer_wait(void) { static volatile int start_asked; static volatile int fullscreen_asked = -1; /* (the Insert menu's row, from the renderer's thread: applied on this one) */ static volatile int menu_asked = -1; /* (the Insert menu opened or closed, likewise) */ +static volatile int restart_asked; /* (the Insert menu's Restart now: the window closes as by the player) */ /* PostMessageA to the window (host/posix/win32_posix.c): WM_APP + 7, the Insert menu's Fullscreen row, WM_APP + 8 the - menu opened or closed (host/display.c's) */ + menu opened or closed, WM_APP + 9 its Restart now (host/display.c's) */ __attribute__((visibility("default"))) void hos_posix_post_message(uint32_t msg, uintptr_t wp, intptr_t lp) { (void)lp; if (msg == WM_APP + 7) __atomic_store_n(&fullscreen_asked, wp ? 1 : 0, __ATOMIC_RELEASE); if (msg == WM_APP + 8) __atomic_store_n(&menu_asked, wp ? 1 : 0, __ATOMIC_RELEASE); + if (msg == WM_APP + 9) __atomic_store_n(&restart_asked, 1, __ATOMIC_RELEASE); } /* the menu opened: the mouse let go (its capture kept for later); closed: the capture back, if the window still has the focus */ static int captured_before_menu; @@ -247,9 +251,7 @@ void hos_display_loop(void) { case HOS_SDL_EV_QUIT: case HOS_SDL_EV_CLOSE: rx_close_asked(); /* (a start still preparing the shaders keeps what it made) */ - hos_log("display: window closed, exiting"); - hos_log_flush(); - ExitProcess(0); + hos_window_closed_exit(); case HOS_SDL_EV_RESIZED: if (renderer) { int w = 0, h = 0; @@ -309,6 +311,11 @@ void hos_display_loop(void) { if (asked >= 0) set_fullscreen(asked); const int menu = __atomic_exchange_n(&menu_asked, -1, __ATOMIC_ACQ_REL); if (menu >= 0) menu_mouse(menu); + if (__atomic_load_n(&restart_asked, __ATOMIC_ACQUIRE)) { + hos_relaunch_ask(); + rx_close_asked(); + hos_window_closed_exit(); + } keys_update(); } } diff --git a/host/rx_backend.c b/host/rx_backend.c index 42a48340a..797de7e4a 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -9,6 +9,7 @@ #include #include "../gpu/vk/vk_api.h" +#include "../gpu/vk/dlss_math.h" /* the WebGPU renderer (gpu/render, renamed) */ int wgr_start(void *hwnd); @@ -300,7 +301,34 @@ void rx_wait_writebacks_reading(const void *p, uint64_t n) { vk ? vk->wait_writebacks_reading(p, n) : wgr_wait_writebacks_reading(p, n); } void rx_nvn_finish(void) { vk ? vk->nvn_finish() : wgr_nvn_finish(); } -void rx_nvn_present(const nvn_tex *t, const int crop[4]) { vk ? vk->nvn_present(t, crop) : wgr_nvn_present(t, crop); } +static volatile LONG presents; /* (the frames shown, for the camera jitter below) */ +void rx_nvn_present(const nvn_tex *t, const int crop[4]) { + InterlockedIncrement(&presents); + vk ? vk->nvn_present(t, crop) : wgr_nvn_present(t, crop); +} +/* (hookshot, nx2/oot_jitter.py) NVIDIA DLSS's camera jitter: this frame's sub-pixel offset of the scene's projection, in clip + space (what P[0][2] and P[1][2] get), into out, for the projection matrix at guest address projection (row major, as the + game fills its views' uniform buffers from it); 0 when there is none: DLSS off (or a renderer without it: rx_option + HOS_OPT_DLSS_JITTER_W and _H, the scene's size in pixels, are 0), or a projection that is not one of the picture's + (gpu/vk/dlss_math.h dlss_jitter_clip: a shadow map's, the HUD's). The renderer reads the offset back from + the camera's uniform buffer (gpu/vk/vk_dlss.cpp), so this only has to pick sub-pixel offsets that cover a pixel evenly: + the Halton (2, 3) sequence, over as many frames as DLSS's scale wants (HOS_OPT_DLSS_JITTER_PHASES: gpu/vk/dlss_math.h + dlss_jitter_phases, 8 for DLAA up to 64). */ +int hos_dlss_jitter_clip(uint64_t projection, float out[2]) { + static LONG at = -1; + static float w, h; + static int phases; + const LONG frame = presents; + if (frame != at) { /* (asked once a frame: the renderer's lock is not taken for every view) */ + at = frame; + w = (float)rx_option(HOS_OPT_DLSS_JITTER_W, 0), h = (float)rx_option(HOS_OPT_DLSS_JITTER_H, 0); + phases = rx_option(HOS_OPT_DLSS_JITTER_PHASES, 16); + } + float p[16]; + memcpy(p, GP(projection), sizeof p); + extern volatile float hos_wide_ratio; + return dlss_jitter_clip(p, hos_wide_ratio, (unsigned)frame, phases, w, h, out); +} static void forward(const early_call *c) { switch (c->kind) { case 0: { diff --git a/nx2/build.py b/nx2/build.py index 99ecf3986..eb56b75e1 100644 --- a/nx2/build.py +++ b/nx2/build.py @@ -177,6 +177,37 @@ def tree_headers(*roots, skip=()): return h.hexdigest().encode() +def ngx_library(sdk, linux): + """NVIDIA's NGX library in a HOOKSHOT_NGX_SDK folder (scripts/ngx_sdk.py's layout), for this build's platform: libnvsdk_ngx.a + on Linux, nvsdk_ngx_s.lib on Windows (the static one, built against Microsoft's static runtime: gpu/vk/vk_ngx_msvc_rt.cpp)""" + return os.path.join(sdk, "lib", "libnvsdk_ngx.a" if linux else "nvsdk_ngx_s.lib") + + +def ngx_sdk(sdk, linux): + """HOOKSHOT_NGX_SDK as the build takes it: the folder when it has this platform's library, else "" (and why, said)""" + if sdk and not os.path.isfile(ngx_library(sdk, linux)): + print("build: HOOKSHOT_NGX_SDK=%s has no lib/%s (scripts/ngx_sdk.py --platform %s fetches it): built without NVIDIA " + "DLSS" % (sdk, os.path.basename(ngx_library(sdk, linux)), "linux" if linux else "windows"), flush=True) + return "" + return sdk + + +# HOOKSHOT_NGX_SDK=: the Vulkan back end with NVIDIA DLSS Super Resolution, from NVIDIA's NGX SDK fetched into that +# folder by scripts/ngx_sdk.py (its include/ and this platform's lib/; never committed, NVIDIA's license). Off without it. +NGX_SDK = ngx_sdk(os.environ.get("HOOKSHOT_NGX_SDK", ""), LINUX) +# (llvm-mingw: what NVIDIA's library asks of Microsoft's runtime, linked in with it. Not compiled otherwise) +NGX_MSVC_RT = "vk_ngx_msvc_rt.cpp" + + +def ngx_link(sdk, mingw): + """What hookshot_vk.dll's link adds for NVIDIA DLSS on Windows: NVIDIA's library and the registry calls it makes; with + llvm-mingw, not Microsoft's C++ runtime it names (libcpmt: vk_ngx_msvc_rt.cpp has what it needs of it, and the other + libraries of Microsoft's are toolchain.native_link's /nodefaultlib already). Nothing without the SDK""" + if not sdk: + return [] + return [ngx_library(sdk, False), "-ladvapi32"] + (["-Wl,-Xlink=/nodefaultlib:libcpmt"] if mingw else []) + + def vk_sources(): """The Vulkan back end (hookshot_vk.dll, gpu/vk/vk_api.h): Eden's shader recompiler with its SPIR-V back end (gpu/vk/eden), sirit with Eden's mesh shader patch (gpu/vk/sirit) and the back end itself (gpu/vk/*.cpp)""" @@ -185,7 +216,7 @@ def vk_sources(): for sub in ("eden", os.path.join("sirit", "src")): for d, _, files in os.walk(os.path.join(vk, sub)): out += [os.path.join(d, f) for f in files if f.endswith(".cpp")] - out += [os.path.join(vk, f) for f in os.listdir(vk) if f.endswith(".cpp")] + out += [os.path.join(vk, f) for f in os.listdir(vk) if f.endswith(".cpp") and f != NGX_MSVC_RT] vc = os.path.join(GPU, "third_party", "suyu", "video_core") out += [os.path.join(vc, "textures", f) for f in ("decoders.cpp", "astc.cpp")] # (the WebGPU back end's unswizzle) return sorted(out) @@ -700,6 +731,7 @@ def main(): if LINUX: host_deps += b"".join(open(os.path.join(POSIX, f), "rb").read() for f in sorted(os.listdir(POSIX)) if f.endswith(".h")) host_deps += open(os.path.join(GPU, "vk", "vk_api.h"), "rb").read() + host_deps += open(os.path.join(GPU, "vk", "dlss_math.h"), "rb").read() # (host/rx_backend.c's DLSS jitter) host_deps += ffmpeg_version_key() jobs += [(cc, s, os.path.join(obj_dir, "host_" + os.path.basename(s)[:-2] + ".o"), host_fl + ["-I" + os.path.join(OPUS, "include")] + @@ -761,6 +793,11 @@ def main(): "-I" + os.path.join(vk, "sirit", "include"), "-I" + os.path.join(vk, "sirit", "src"), "-I" + os.path.join(spirv_headers, "include"), "-I" + os.path.join(spirv_headers, "include", "spirv", "unified1"), "-I" + os.path.join(vk_headers, "include"), "-I" + vk, "-I" + gen_dir] + (["-I" + os.path.join(cpm_package("vulkanmemoryallocator"), "include")] if cpm_package("vulkanmemoryallocator") else []) + # (NVIDIA DLSS, a local build's option: scripts/ngx_sdk.py. Only vk_dlss.cpp sees NVIDIA's headers; without them + # it is the stubs that say DLSS is not there) + ngx_flags = ["-DHOOKSHOT_NGX", "-I" + os.path.join(NGX_SDK, "include")] if NGX_SDK else [] + if NGX_SDK: + print("the Vulkan back end: with NVIDIA DLSS (NVIDIA's NGX SDK in %s)" % NGX_SDK, flush=True) # (Eden's recompiler and sirit see only their own headers: a change to the back end's vk_*.h does not # rebuild their ~500 files; the back end itself depends on everything, suyu's texture decoders included) vk_lib_deps = tree_headers(os.path.join(vk, "eden"), os.path.join(vk, "sirit"), os.path.join(vk, "compat"), @@ -772,8 +809,15 @@ def main(): lib = os.path.relpath(src, vk).split(os.sep)[0] in ("eden", "sirit") vk_jobs.append((cxx, src, os.path.join(obj_dir, "vk_" + name + ".o"), vflags + ([shader_stamp] if os.path.basename(src) == "vk_shader.cpp" else []) + - ([stamp_def] if os.path.basename(src) == "vk_menu.cpp" else []), + ([stamp_def] if os.path.basename(src) == "vk_menu.cpp" else []) + + (ngx_flags if os.path.basename(src) == "vk_dlss.cpp" else []), vk_lib_deps if lib else vdeps + (logo_text if os.path.basename(src) == "vk_menu.cpp" else b""))) + if NGX_SDK and not LINUX and toolchain.mingw(): + # (llvm-mingw: NVIDIA's static library was built with Microsoft's compiler for its static runtime (/MT). What it asks + # of that runtime: the C parts as for the static FFmpeg (host/mingw_msvc_shims.c), the C++ ones in vk_ngx_msvc_rt.cpp) + vk_jobs.append((cxx, os.path.join(vk, NGX_MSVC_RT), os.path.join(obj_dir, "vk_ngx_msvc_rt.o"), vflags + ngx_flags, vdeps)) + vk_jobs.append((cc, os.path.join(HOST, "mingw_msvc_shims.c"), os.path.join(obj_dir, "vk_ngx_mingw_msvc_shims.o"), + target() + ["-O2", "-g0", "-Wno-everything"], b"")) if LINUX: # (its own copy of the Win32 calls on POSIX, and the bit of GDI the Insert menu draws text with) cflags = target() + CPU + ["-fPIC", "-O2", "-g0", "-Wno-everything", "-D_GNU_SOURCE", "-I" + POSIX, "-I" + os.path.join(kit.HERE, "..", "third_party", "stb")] @@ -922,10 +966,11 @@ def main(): if LINUX: # (its C++ runtime inside: the game folder runs where the AppImage's sysroot is older than the system) link = [toolchain.need(toolchain.find("clang++"))] + target() + [ "-fuse-ld=lld", "-shared", "-o", vdll, "-Wl,-Map=" + vdll + ".map", "-Wl,-Bsymbolic", "-Wl,--gc-sections", - "-static-libstdc++", "-static-libgcc", "@" + vrsp, "-ldl", "-lpthread", "-lm"] + "-static-libstdc++", "-static-libgcc", "@" + vrsp] + \ + ([ngx_library(NGX_SDK, True)] if NGX_SDK else []) + ["-ldl", "-lpthread", "-lm"] else: link = [cc] + toolchain.native_target() + toolchain.native_link() + \ - ["-shared", "-o", vdll, toolchain.link_opt("/map:" + vdll[:-4] + ".map"), "@" + vrsp, "-luser32", "-lgdi32"] + (["-lc++", "-lunwind"] if toolchain.mingw() else []) + ["-shared", "-o", vdll, toolchain.link_opt("/map:" + vdll[:-4] + ".map"), "@" + vrsp, "-luser32", "-lgdi32"] + (["-lc++", "-lunwind"] if toolchain.mingw() else []) + ngx_link(NGX_SDK, toolchain.mingw()) r = subprocess.run(link, capture_output=True, text=True) if r.returncode: undefined = sorted({l.strip() for l in (r.stdout + r.stderr).splitlines() if "undefined symbol" in l}) diff --git a/nx2/lift.py b/nx2/lift.py index c659cc8be..a35e6460c 100644 --- a/nx2/lift.py +++ b/nx2/lift.py @@ -158,6 +158,9 @@ def main(): if a.standalone: # (hookshot) widescreen and ultrawide: C after a few of the game's instructions import oot_wide oot_wide.install(L, img) + import oot_jitter # (NVIDIA DLSS's camera jitter in a build with it, none unless DLSS runs: host/rx_backend.c) + if oot_jitter.wanted(): + oot_jitter.install(L, img) import oot_occlusion # (no occlusion queries when HOOKSHOT_OCCLUSION_OFF=1: Fritchii's Occlusion Off) oot_occlusion.install(L, img) L.discover(max_insns=a.max_func) diff --git a/nx2/make_game.py b/nx2/make_game.py index ef5e237aa..b778e0862 100644 --- a/nx2/make_game.py +++ b/nx2/make_game.py @@ -869,6 +869,59 @@ def lifted_before(work, key): return os.path.isfile(os.path.join(image, "image.bin")) and os.path.isfile(os.path.join(gen, "a64_table.c")) +def ngx_script(): + """scripts/ngx_sdk.py: NVIDIA's DLSS SDK, its file table and where each file goes""" + import importlib.util + spec = importlib.util.spec_from_file_location("ngx_sdk", os.path.join(ROOT, "scripts", "ngx_sdk.py")) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def ngx_platform(): + return "linux" if LINUX else "windows" + + +def fetch_ngx_sdk(a): + """--fetch-ngx-sdk (the Hookshot Builder's NVIDIA DLSS tick): NVIDIA's DLSS SDK into HOOKSHOT_NGX_SDK before the build, once + NVIDIA's license was accepted (--accept-nvidia-license: the builder asked first). Only what is missing or not the file + expected is downloaded""" + sdk = os.environ.get("HOOKSHOT_NGX_SDK", "") + if not sdk: + fail("--fetch-ngx-sdk needs HOOKSHOT_NGX_SDK, the folder NVIDIA's DLSS SDK goes into.") + if not a.accept_nvidia_license: + fail("NVIDIA DLSS needs NVIDIA's license accepted first (--accept-nvidia-license).") + ngx = ngx_script() + print(ngx.NOTICE, flush=True) + print("NVIDIA's license accepted: getting NVIDIA's DLSS SDK into %s" % sdk, flush=True) + try: + ngx.fetch(sdk, ngx_platform(), say=lambda line: print(line, flush=True)) + except (OSError, ValueError) as e: + fail("NVIDIA's DLSS SDK could not be downloaded (%s). Check the connection and build again, or untick NVIDIA DLSS." % e) + + +def ngx_built(): + """This build has NVIDIA DLSS: HOOKSHOT_NGX_SDK holds this platform's library (what nx2/build.py takes)""" + sdk = os.environ.get("HOOKSHOT_NGX_SDK", "") + return bool(sdk) and os.path.isfile(ngx_script().library(sdk, ngx_platform())) + + +def place_ngx_runtime(out): + """(NVIDIA DLSS) a game built with NVIDIA's SDK (HOOKSHOT_NGX_SDK, as nx2/build.py takes it) gets DLSS's runtime in its ngx + folder, where gpu/vk/vk_dlss.cpp looks first: nvngx_dlss.dll on Windows, libnvidia-ngx-dlss.so. on Linux. Never + NGX's core (_nvngx.dll is the driver's) and nothing beside the exe. Returns the file placed, or None""" + sdk = os.environ.get("HOOKSHOT_NGX_SDK", "") + if not sdk: + return None + ngx = ngx_script() + platform = ngx_platform() + if not (os.path.isfile(ngx.library(sdk, platform)) and os.path.isfile(ngx.runtime(sdk, platform))): + return None + dst = ngx.install_runtime(sdk, platform, os.path.join(out, "ngx")) + print("NVIDIA DLSS: %s (NVIDIA's, under NVIDIA's license: never share this game folder)" % dst, flush=True) + return dst + + def parse_args(): p = argparse.ArgumentParser(description=__doc__.split("\n")[0]) p.add_argument("--nup") @@ -884,6 +937,9 @@ def parse_args(): p.add_argument("--check", action="store_true") p.add_argument("--update", help="a game folder built before: built again from the game files it keeps (the game's update offer)") p.add_argument("--log", metavar="FILE", help="also save build output to a UTF-8 file (replaces the file if it exists)") + # (NVIDIA DLSS, off unless asked: NVIDIA's SDK into HOOKSHOT_NGX_SDK first, under NVIDIA's license. scripts/ngx_sdk.py) + p.add_argument("--fetch-ngx-sdk", action="store_true", help="download NVIDIA's DLSS SDK into HOOKSHOT_NGX_SDK before the build") + p.add_argument("--accept-nvidia-license", action="store_true", help="NVIDIA's DLSS SDK license, accepted (with --fetch-ngx-sdk)") a = p.parse_args() a.default_work = p.get_default("work") # (main: the builder's own work folder, stop_stray_compilers) return a @@ -917,6 +973,8 @@ def main(a=None): check_windows_sdk() stop_stray_compilers(work, a.default_work) drop_stale_objects(work) + if getattr(a, "fetch_ngx_sdk", False): + fetch_ngx_sdk(a) exefs = os.path.join(work, "exefs") romfs = os.path.join(out, "romfs") @@ -991,6 +1049,8 @@ def main(a=None): if os.path.exists(os.path.join(work, "hookshot_pieces.prof")) and profile_readable(os.path.join(work, "hookshot_pieces.prof")): env["RX_PGO"] = os.path.join(work, "hookshot_pieces.prof") + if ngx_built(): # (NVIDIA DLSS's camera jitter: only in a build with it, nx2/oot_jitter.py) + env["OOT_DLSS_JITTER"] = "1" say("stage", "lift", "Translating the game to PC code") # (the same game, tools and settings as the lift before: its C is what this lift would write, and nothing recompiles from it. A # rebuild after a change to host/ alone skips the minutes of the lift) @@ -1132,6 +1192,7 @@ def compiled(line): os.remove(os.path.join(out, n)) except OSError: pass + place_ngx_runtime(out) # every shader of the game, for its first start to translate (gpu/vk/vk_shader.cpp seed_open). Without it the game still runs, compiling them # as it meets them: not worth stopping a finished build for if run(shader_census_cmd(romfs, os.path.join(out, "hookshot_shaders.bin")), check=False, output=sys.stderr): diff --git a/nx2/oot_jitter.py b/nx2/oot_jitter.py new file mode 100644 index 000000000..cace22c9b --- /dev/null +++ b/nx2/oot_jitter.py @@ -0,0 +1,71 @@ +"""NVIDIA DLSS's camera jitter for Ocarina of Time 1.1.1: C added after a few instructions of the game (a64_lift.py Lifter.after). + +DLSS (gpu/vk/vk_dlss.cpp) resolves detail finer than a pixel from frames whose camera moved by a different fraction of a +pixel each time. The game's cameras do not move so: these sites give the scene's projection a sub-pixel offset each frame, +in the GPU's copy only. The game fills each view's uniform buffers in one function (main+0x6bc520): for each view it loads +the view's projection matrix (the view at x28, its projection's rows at +0x1a8) and writes it, the view-projection made from +it and more into the buffers the draws read. The sites are those loads: the jitter goes on the third column of the first two +rows (P[0][2], P[1][2]), in the registers, so the buffers get the jittered matrix and everything made from it, while the +game's own cameras and views keep the projection as it is. + +The jitter used to go into sead's perspective projection itself (its frustum offset, +0xb4 and +0xb8), where the game's +CPU work saw it too: the shadow cascades are fitted to the view's frustum, so with the camera still the shadow map's origin +moved by a sixth of a shadow texel each frame (up to a third; 0.003 without the jitter), and with it the shadows' edges +(#841: a tester saw shadows flicker at DLAA). Here the cascades (and culling, and anything else on the CPU) see the view as +the game set it. + +host/rx_backend.c hos_dlss_jitter_clip picks the jitter and checks the projection is one of the picture's: perspective, of +16:9 or the window's ratio (nx2/oot_wide.py). It is none (the game as shipped) unless DLSS runs, and the renderer reads the +offset back from the camera's uniform buffer. Which view is the scene's, the renderer learns from its draws, too late for +this: the other views of the picture's ratio (a reflection, a portrait) are drawn without DLSS and move by under a pixel +of the scene's size, too little to see. + +Installed only in a build with NVIDIA's DLSS SDK (make_game.py sets OOT_DLSS_JITTER=1 for the lift then): other builds have +none of this code. +""" + +import os + +import oot_wide + +# (main offset, the original instruction word there, and the registers whose third lane gets the jitter's x and y: None +# for one not loaded there). The view is x28 at all three; its projection's rows are at +0x1a8, 16 bytes apart. +SITES = ( + # the view-projection block: ldp q19, q20, [x8] (rows 0 and 1), then this ldp q21, q22, [x8, #0x20] (rows 2 and 3) + (0x6bc708, 0xad415915, 19, 20), + # the second block: ldr q26, [x9] (row 0), ldr q6, [x9] (row 1) + (0x6bcf8c, 0x3dc0013a, 26, None), + (0x6bcf9c, 0x3dc00126, None, 6), +) +VIEW, PROJECTION = 28, 0x1a8 + + +def _text(xreg, yreg): + add = "".join("{ float _o; memcpy(&_o, (uint8_t *)c->vreg[%d] + 8, 4); _o += _j[%d]; memcpy((uint8_t *)c->vreg[%d] + 8, &_o, 4); } " + % (r, axis, r) for axis, r in ((0, xreg), (1, yreg)) if r is not None) + return ("{ extern int hos_dlss_jitter_clip(uint64_t projection, float out[2]); float _j[2]; " + "if (hos_dlss_jitter_clip(c->x[%d] + %#x, _j)) { %s} } /* (nx2/oot_jitter.py: DLSS camera jitter) */" + % (VIEW, PROJECTION, add)) + + +def wanted(): + """A build with NVIDIA DLSS (make_game.py: HOOKSHOT_NGX_SDK with its library): OOT_DLSS_JITTER=1, in the lift's key""" + return os.environ.get("OOT_DLSS_JITTER") == "1" + + +def install(lifter, img): + """The sites into lifter.after, after what is there; a site whose word differs (another game version) is left out""" + n = 0 + for off, word, xreg, yreg in SITES: + pc = oot_wide.MAIN + off + try: + have = img.word(pc) + except Exception: + have = None + if have != word: + print("dlss jitter: main+%#x holds %s, not %08x: left out" % (off, "?" if have is None else "%08x" % have, word)) + continue + lifter.after[pc] = (lifter.after[pc] + "\n" if pc in lifter.after else "") + _text(xreg, yreg) + n += 1 + print("dlss jitter: %d of %d sites" % (n, len(SITES))) + return n diff --git a/readme.md b/readme.md index b91983c98..896d57c91 100644 --- a/readme.md +++ b/readme.md @@ -167,7 +167,7 @@ with `--update `. | Problem | What to do | |---|---| -| Where is the log? | `hookshot_log.txt` in the game folder holds the last run. The four runs before are `hookshot_log.1.txt` (the last) to `.4.txt`. Started from a terminal, the game prints there instead | +| Where is the log? | `hookshot_log.txt` in the game folder holds the last run. The four runs before are `hookshot_log.1.txt` (the last) to `.4.txt`. Started from a terminal, the game prints there instead. Every 600 frames it has a `vk: status:` line: the video memory in use of what the driver allows, and DLSS's details while it draws; `vk: over the driver's video-memory allowance` says when the game passed it | | "Hookshot supports build e0f3ccfa" or "This version isn't supported" | Check that you chose the right game with its update installed | | The game opens and closes, or the log says "device lost" | Update your graphics driver. On AMD cards there are known crashes ([#708](https://github.com/NotRexed/Hookshot/issues/708), [#614](https://github.com/NotRexed/Hookshot/issues/614)) | | Parts of the picture are missing, or a message says your card has no mesh shader support | The card or its driver has no mesh shaders. Update the driver; older cards are not supported ([#640](https://github.com/NotRexed/Hookshot/issues/640)) | @@ -202,8 +202,14 @@ Press Insert (or F1) in game, or on a controller the Home The same keys close it, and so does Esc. The menu has three tabs, Graphics, Gameplay and Controls: Q and E (or Tab and Shift+Tab) switch between them, LB and RB on a controller. Up and Down choose a row, Left and Right change it -(Enter changes it too). On a controller the D-pad chooses and changes, the bottom face button changes too, -and the right one closes the menu. Your choices are saved in `hookshot_options.ini` in the game folder. +(Enter changes it too); under the rows, the help says what the row is and, on a line of its own, what the +value chosen does. On a controller the D-pad chooses and changes, the bottom face button changes too, and the right +one closes the menu. Your choices are saved in `hookshot_options.ini` in the game folder. + +The rows marked "after a restart" below are read when the game starts. While one is changed, its value says "(after +restart)" and the foot of the Graphics tab lists it; closing the menu then asks whether to restart now. **Later** keeps +the change for the next start (and is not asked again until you change one of those rows); **Restart now** saves your +choices and starts the game again, so progress since your last save is lost. | Tab | Option | Choices | Default | |---|---|---|---| @@ -212,9 +218,12 @@ and the right one closes the menu. Your choices are saved in `hookshot_options.i | | GPU device | Auto, or each graphics card by name (after a restart) | Auto | | | Frame generation | Off, 2x to 8x | Off | | | Frame pacing | Off, On | Off | -| | Resolution | Auto (the game's own dynamic resolution), 100%, 75%, 67%, 50% (each with its size) | Auto | +| | NVIDIA Reflex | Off, On, On + Boost (NVIDIA GPUs with `VK_NV_low_latency2`) | Off | | | Output resolution | 1920x1080, 2048x1152, 2560x1440, 3840x2160 (after a restart) | 2048x1152 | -| | Upscaling | Off, Catmull-Rom, Lanczos | Off | +| | Upscaler | Game's own, NVIDIA DLSS, NVIDIA DLAA (only in builds with NVIDIA's DLSS SDK, below; DLSS and DLAA on RTX GPUs) | Game's own | +| | DLSS model | Transformer (K), Transformer 2 (M), Transformer 2 (L), NVIDIA default (builds with DLSS; used by DLSS and DLAA) | Transformer (K) | +| | Render resolution | Auto (the game's own dynamic resolution), 100%, 75%, 67%, 50% (each with its size). With DLSS: Auto, Quality (67%), Balanced (58%), Performance (50%), Ultra Performance (33%). With DLAA: Native | Auto | +| | Window scaling filter | Off, Catmull-Rom, Lanczos | Off | | | Sharpness | 0 to 100%, in steps of 10 | 0% | | | Handheld profile | Off, On (after a restart) | Off | | | Ultrawide | 16:9, Monitor's shape (after a restart) | 16:9 | @@ -238,21 +247,31 @@ and the right one closes the menu. Your choices are saved in `hookshot_options.i | | Layout preset | Nintendo (Def), Xbox / PC, Swap Face, Custom | Nintendo (Def) | | | Controls | Opens the Controls page | | -- **Quality preset** sets Resolution, Upscaling, Sharpness, Frame generation, Frame pacing, Game speed, Handheld - profile and the video memory limits for a card with that much memory. Changing most other rows yourself sets it back - to Custom. +- **Quality preset** sets Render resolution, Window scaling filter, Sharpness, Frame generation, Frame pacing, Game + speed, Handheld profile and the video memory limits for a card with that much memory. It leaves the Upscaler as you + chose it: with DLSS, the preset's resolution becomes DLSS's nearest mode. Changing most other rows yourself sets it + back to Custom. - **GPU device** matters on a PC with more than one graphics card (a laptop's integrated and dedicated GPUs, or a second card for Lossless Scaling). Auto prefers a dedicated one, and only cards with Vulkan 1.3 are listed. `hookshot_log.txt` lists every card as `vk: device `; `RX_VK_DEVICE=` chooses one for a single run and wins over the menu. - **Frame generation** makes extra frames between the game's own. It adds a little input delay, and fast motion can show artifacts. **Frame pacing** shows frames at an even interval: less stutter, up to one frame of delay. -- **Output resolution** is the size of the game's whole frame, HUD included, before it is scaled to your window. - On a 1920x1080 display, the 1920x1080 option keeps HUD and text sharp when the game fills the screen by mapping them - pixel for pixel. The separate **Resolution** setting scales the 3D scene as a share of the output size. Docked only. - At 3840x2160 the game's render targets get a bigger GPU pool so full-size 3D fits. For one run, set `HOS_OUTPUT_RES` +- **The scaling rows**, in the picture's order: **Output resolution** is the size of the game's whole frame, HUD + included. The **Upscaler** fills it from the 3D: the game's own network, or NVIDIA DLSS or DLAA (the 3D at full size, + edges smoothed). Both look softer than the game's own: set **Sharpness** to around 80% with them. **DLSS model** + chooses their network (below). **Render + resolution** is the 3D's share of the output. The **Window scaling filter** then scales the frame to your window, + and **Sharpness** sharpens it. +- **Output resolution:** its rows below show the sizes for the output you chose. Docked only. On a 1920x1080 display, + the 1920x1080 option keeps HUD and text sharp when the game fills the screen by mapping them pixel for pixel. At + 3840x2160 the game's render targets get a bigger GPU pool, so a full-size 3D fits. For one run, set `HOS_OUTPUT_RES` to `3` for 1920x1080, `0` for 2048x1152, `1` for 2560x1440, or `2` for 3840x2160. -- **Upscaling** and **Sharpness** also work when Frame generation is Off. +- **Window scaling filter** matters only when your window's size differs from the Output resolution: for the sharpest + picture, set the Output resolution to your screen's, and the row shows "Not needed (window = output)". It and + **Sharpness** also work when Frame generation is Off. +- **NVIDIA Reflex** Off is the game as without it. `RX_VK_REFLEX_LOG=1` logs the driver's latency timings every 300 + frames while it is on. - **Handheld profile** runs the game as if the console were undocked: lower resolution, lighter on the GPU. - **Robust buffers:** a read past a buffer's end is dropped instead of stopping the GPU. Auto is on, on every card; Off can be faster on some NVIDIA cards. @@ -430,6 +449,56 @@ cd ~/Games/Hookshot `hookshot_env.txt` and the other test switches are documented below. +
+NVIDIA DLSS (optional, NVIDIA's SDK under NVIDIA's license) + +A game built with NVIDIA's DLSS SDK offers **NVIDIA DLSS** and **NVIDIA DLAA** in the Graphics tab's **Upscaler** row on +NVIDIA RTX cards (`dlss=` in `hookshot_options.ini`, `RX_VK_DLSS=0/1/2` for one run): DLSS Super Resolution in place of +the game's own upscaler, with camera jitter (**NVIDIA Reflex** needs no SDK and is in +every build). NVIDIA's files are NVIDIA's, under its own license, the +[NVIDIA RTX SDKs license](https://github.com/NVIDIA/DLSS/blob/9981c6350dc58c0ce21f024fd65e31e851c31689/LICENSE.txt). +Hookshot never includes or redistributes them: each build downloads them for itself, and only after you accept that +license. Never share a game folder built with them, and do not post builds made with them. + +- **Windows, the Hookshot Builder:** tick **NVIDIA DLSS (downloads NVIDIA's SDK under NVIDIA's license)** before you + build. It is off by default and shows NVIDIA's license when you tick it. The build downloads the SDK into + `%LOCALAPPDATA%\HookshotBuilder\ngx-sdk` and puts DLSS's runtime, `nvngx_dlss.dll`, in the game folder's `ngx` folder. +- **Windows, the command line:** `python -I scripts/ngx_sdk.py --platform windows ` asks you to accept + NVIDIA's license (or pass `--accept-nvidia-license`) and fetches the headers, `nvsdk_ngx_s.lib` and `nvngx_dlss.dll`, + each checked against its SHA-256. Then build with `HOOKSHOT_NGX_SDK=`: `make_game.py` copies + `nvngx_dlss.dll` into `\ngx`. NGX is also pointed at `%LOCALAPPDATA%\Hookshot\ngx`, and at + `RX_VK_DLSS_DIR` when it is set, and uses the newest `nvngx_dlss.dll` it finds across them (to try a given one, make + sure no other folder has a newer one). Never copy NGX's core, `_nvngx.dll`, beside the game: it comes with NVIDIA's + driver. +- **Linux:** the same with `--platform linux` (or `scripts/linux/ngx-sdk.sh`): `libnvsdk_ngx.a` for the build and + `libnvidia-ngx-dlss.so` for the game folder's `ngx` folder (also looked for in `$XDG_DATA_HOME/hookshot/ngx`). NGX's + core comes with NVIDIA's Linux driver. + +The **DLSS model** row chooses DLSS's network, by NVIDIA's preset letter, for every mode: **Transformer (K)**, the +transformer model and the one tested most here (the default); **Transformer 2 (M)**, NVIDIA's second-generation +transformer as it picks it for Performance; **Transformer 2 (L)**, the same generation as tuned for Ultra Performance; +**NVIDIA default**, which lets DLSS's runtime pick for each mode, so it can change with a runtime update. The runtime +the SDK comes with (310.9.16) has all of them. A change applies at once, without a restart (`dlss_model=` in +`hookshot_options.ini`, 0 to 3 in that order). `RX_VK_DLSS_PRESET=` (or `0` for NVIDIA's default) chooses +the preset for one run and wins over the row; the log's `vk: NVIDIA DLSS: ... preset ` line says which one was +asked for, and with `RX_VK_DLSS_LOG=1` NGX's own `Using App hint Preset ` lines say which it took. + +**8 GB cards:** DLAA draws the 3D at full size, and DLSS and DLAA add images of their own beside the game's. In busy +areas that can pass what the driver allows the game, and the game then stutters: Quality or Balanced is safer there. +The log's `vk: status:` line every 600 frames shows the video memory in use of the allowance, with DLSS's details +(its images' size, the jitter and the actor motion) appended while it draws, and a `vk: over the driver's +video-memory allowance` line says when the game passed it. + +The game's log says whether DLSS started, and why not when it did not (`vk: NVIDIA DLSS:` lines in `hookshot_log.txt`). +NGX starts on demand: when the Insert menu first opens (to know whether the Upscaler can offer DLSS) or at the first +gameplay upscale with DLSS chosen, so on the title screen the log has no NGX lines yet. +With `RX_VK_DLSS_LOG=1` NGX's own lines go there too (`ngx:`). Some of them look like errors but are not: +`failed to load NGXCore: 126` (twice) is NVIDIA's library looking for `_nvngx.dll` and `nvngx.dll` beside the game, which +are not there on purpose, before it loads the driver's; `Feature ... failed to load from cache` is NGX finding no model +of its own downloads (`C:\ProgramData\NVIDIA\NGX\models`) before it uses the game folder's `nvngx_dlss.dll`. + +
+
Switches for testing (hookshot_env.txt) @@ -495,6 +564,8 @@ Hookshot exists thanks to the work of many people: after Load (Horizon's mutex handoff) and the exposure flicker fix. - **[zortos293/Lichii](https://github.com/zortos293/Lichii)**: reference for Ampere shader lowering, and its compare-exchange retry fix. - **[mstorsjo/llvm-mingw](https://github.com/mstorsjo/llvm-mingw)**: the bundled Windows compiler (no Visual Studio needed). +- **NVIDIA DLSS**: builds made with NVIDIA's DLSS SDK (optional, downloaded by each build, never bundled) use NVIDIA DLSS + Super Resolution, NVIDIA's technology. NVIDIA and DLSS are trademarks of NVIDIA Corporation. - **[kinnay/zbic](https://github.com/kinnay/zbic)**, **Opus** and **SDL**: decompression, audio and controllers. - **[sostenesfreitas/ocarina-recomp](https://github.com/sostenesfreitas/ocarina-recomp)**: this README's layout, and its notes confirmed the mesh stage's control section. diff --git a/scripts/linux/ngx-sdk.sh b/scripts/linux/ngx-sdk.sh new file mode 100755 index 000000000..e820587d3 --- /dev/null +++ b/scripts/linux/ngx-sdk.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# ngx-sdk.sh: NVIDIA's DLSS SDK files for a local Linux build with NVIDIA DLSS (HOOKSHOT_NGX_SDK): scripts/ngx_sdk.py, which +# says what it fetches and asks for NVIDIA's license (--accept-nvidia-license: accepted without the question). +# +# Usage: scripts/linux/ngx-sdk.sh [--accept-nvidia-license] [] [] +# : default build-linux/ngx-sdk; : libnvidia-ngx-dlss.so copied there too (a game folder's +# ngx/, or $XDG_DATA_HOME/hookshot/ngx; RX_VK_DLSS_DIR names another) +# Then build with HOOKSHOT_NGX_SDK=. +set -euo pipefail +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +accept=() +if [ "${1:-}" = --accept-nvidia-license ]; then accept=(--accept-nvidia-license); shift; fi +args=(--platform linux "${accept[@]}") +[ -n "${2:-}" ] && args+=(--runtime-dir "$2") +exec "${PYTHON:-python3}" -I "$ROOT/scripts/ngx_sdk.py" "${args[@]}" ${1:+"$1"} diff --git a/scripts/ngx_sdk.py b/scripts/ngx_sdk.py new file mode 100644 index 000000000..0ee283305 --- /dev/null +++ b/scripts/ngx_sdk.py @@ -0,0 +1,207 @@ +"""NVIDIA's DLSS SDK files for a local build with NVIDIA DLSS Super Resolution (HOOKSHOT_NGX_SDK, gpu/vk/vk_dlss.cpp). + +NVIDIA's files are not part of Hookshot and are never committed or bundled: they are under NVIDIA's own license (the DLSS +SDK's LICENSE.txt, fetched beside them; read it before using them). This fetches them from NVIDIA's DLSS repository at one +pinned release, each file checked against its SHA-256, into : + - LICENSE.txt and the SDK's headers (include/), the same for every platform; + - the library the build links (lib/): libnvsdk_ngx.a on Linux, nvsdk_ngx_s.lib (the /MT one) on Windows; + - the DLSS runtime the driver loads (runtime/): libnvidia-ngx-dlss.so. on Linux, nvngx_dlss.dll on Windows. +The NGX core itself comes with NVIDIA's driver (libnvidia-ngx.so on Linux; _nvngx.dll on Windows, which is loaded from the +driver's own folder and is never to be put beside the game). + +Usage: python -I scripts/ngx_sdk.py [--accept-nvidia-license] [--platform linux|windows] [--game ] + [--runtime-dir ] [--build-only] [] + : default build-linux/ngx-sdk on Linux, work/ngx-sdk on Windows + --game: the DLSS runtime copied into /ngx too, where the game looks first (nx2/make_game.py does the same for + the folder it builds when HOOKSHOT_NGX_SDK is set) + --runtime-dir: the runtime copied into that folder instead (Linux: $XDG_DATA_HOME/hookshot/ngx; Windows: + %LOCALAPPDATA%\\Hookshot\\ngx. RX_VK_DLSS_DIR names another when the game runs) + --build-only: only what the build reads, not DLSS's runtime (CI's link check) + --accept-nvidia-license: NVIDIA's license accepted without the question (scripts, the Hookshot Builder's DLSS tick, CI) +Then build with HOOKSHOT_NGX_SDK=. +""" +import argparse +import hashlib +import os +import shutil +import sys +import urllib.request + +COMMIT = "9981c6350dc58c0ce21f024fd65e31e851c31689" # (v310.9.16) +BASE = "https://raw.githubusercontent.com/NVIDIA/DLSS/" + COMMIT +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LICENSE_URL = "https://github.com/NVIDIA/DLSS/blob/%s/LICENSE.txt" % COMMIT + +# (path in NVIDIA's repository, its SHA-256): what every platform's build reads +COMMON = [ + ("LICENSE.txt", "d4216e39ebef5f9b50a6712ebb37beeb5379862a67733a9999c651f21592aaf0"), + ("include/nvsdk_ngx_defs_dlssd.h", "2e97fe1595b1334f71b51724f2cb67496e8c296a7a2eec5606da53055882e716"), + ("include/nvsdk_ngx_defs_dlssg.h", "c29765ab9e0125f9d724ae9811cc2abe03738d17486ec8eee80d78a1504549e1"), + ("include/nvsdk_ngx_defs.h", "ea23f33497cd274860d1c25a97644fce807dcb0037c594547203343103fad03e"), + ("include/nvsdk_ngx_defs_vk.h", "0a24d0861ace7d6b9362a67b7f08bea1b33ea123c5ce730b19133de8a891d031"), + ("include/nvsdk_ngx.h", "dc38e7467cf415379c9d12ae1b6e4a494c453ed92720fb53e92aecb523e7b848"), + ("include/nvsdk_ngx_helpers_cuda.h", "f2851a76107bdf4fbcb7b261f3c35573c312d8ecb4bc18fb02d9e2cec7705042"), + ("include/nvsdk_ngx_helpers_d3d.h", "ec75224f36ed6580baaf250fa83620405ad81a1d98c2c436654a0c9cf6a6b8ba"), + ("include/nvsdk_ngx_helpers_dlssd_cuda.h", "7e35cde214d905346a487b9a28c05d35e1c55b480e237fe3de69580a7e332730"), + ("include/nvsdk_ngx_helpers_dlssd_d3d.h", "6df9d02c6f47feee3afc8d12340a9f554a7ddce4ddacb1b6a13a42b26c2c6293"), + ("include/nvsdk_ngx_helpers_dlssd.h", "a25fdda925b479dfe869593aeba311ef08793da54e0391f2fe2fe756cbef5532"), + ("include/nvsdk_ngx_helpers_dlssd_vk.h", "11df8fbda532a97fe867c8ef2b40058b49b7898183710f3468e55b1eedb8f40b"), + ("include/nvsdk_ngx_helpers_dlssg_d3d.h", "6335ebcb95de39a361e87fe0257d6ce0723e5d8deaa15db3b35afacf2b3f4220"), + ("include/nvsdk_ngx_helpers_dlssg.h", "6881d21419c40c78ec948b326902ece09fc112b0223144d51c4905f3fced7a1f"), + ("include/nvsdk_ngx_helpers_dlssg_vk.h", "b371d90b9f4372b9a415e68a0afbdc2f31b7b552e219a9637d848749bc965316"), + ("include/nvsdk_ngx_helpers.h", "5bcbadfe7478b802cf6d3aca4dc5ddd7d0889b99726e69c63f9e9bd555f44471"), + ("include/nvsdk_ngx_helpers_vk.h", "c192bff72138f12f770db48e12c1d8f712dfc81ea0e8e055ebb27d8dd1b31623"), + ("include/nvsdk_ngx_loader.h", "abdf5f66a692620538357d29fdf78e3e2a677ee8140b7a638962ecb766585e75"), + ("include/nvsdk_ngx_params_dlssd.h", "d41e3ba8f1d72583b55273092c99bf426dc4a0d46de827229e25d5b1325a370e"), + ("include/nvsdk_ngx_params_dlssg.h", "c24fa5aa68ffa2808c828b75cfa32843f1606f80a145f84055ac450d7e6c74ad"), + ("include/nvsdk_ngx_params.h", "943bc8cc5cdae03b6303016fbad3183636f2335ae27a2d18776798c3b4efabbc"), + ("include/nvsdk_ngx_standalone_common.h", "38413c3338b0aea0202c07da95d426bfee7080a42913823fa2d9dd585b087b53"), + ("include/nvsdk_ngx_standalone_cuda.h", "b23436ed5040051ac8e81554c9af080f2463c941435bbc3b82222e5431a09499"), + ("include/nvsdk_ngx_vk.h", "2d364ce7132881eb669e9498fd570d74cba563b1fbebc82c235f8e1ad2dd8b6d"), +] +# each platform's library (into lib/) and DLSS runtime (into runtime/) +PLATFORMS = { + "linux": { + "library": ("lib/Linux_x86_64/libnvsdk_ngx.a", "d3c50bb0d9624fc35e502a8e929b8fc1e2b505017ebdb84c0d5b627e11a235df"), + "runtime": ("lib/Linux_x86_64/rel/libnvidia-ngx-dlss.so.310.9.16", + "30982c937afed85c96b8d313323de9a650a2eec6de1bb0f714b21750f4ce98f6"), + }, + "windows": { + # (the static NGX loader built against the static C runtime (/MT); llvm-mingw links it with gpu/vk/vk_ngx_msvc_rt.cpp) + "library": ("lib/Windows_x86_64/x64/nvsdk_ngx_s.lib", "7b9a5239c1145af5f645ab472aa066f9ea79db36ac819562312aae69d65f3765"), + "runtime": ("lib/Windows_x86_64/rel/nvngx_dlss.dll", "d00b5b0ab29c3ca101b70c3a5ecb3a01b7ef33766a3383a28a18623083e6123e"), + }, +} + +NOTICE = """\ +NVIDIA DLSS: this downloads NVIDIA's DLSS SDK (release v310.9.16) from github.com/NVIDIA/DLSS. +NVIDIA's files are NVIDIA's, not Hookshot's, and are under NVIDIA's own license, the NVIDIA RTX SDKs license: + %s +(saved as LICENSE.txt beside the files). Using them means accepting it. Hookshot never includes or redistributes them: +they stay on this PC, in the SDK folder and the game folder's ngx folder. Never share a game folder built with them.""" % LICENSE_URL + + +def host_platform(): + """The platform a build here is for: Windows on Windows or with RX_TARGET=windows (a cross build), else Linux""" + return "windows" if os.name == "nt" or os.environ.get("RX_TARGET", "").lower() == "windows" else "linux" + + +def destination(path): + """Where a file of NVIDIA's repository goes in the SDK folder (a relative path)""" + if path.startswith("lib/"): + return ("runtime/" if "/rel/" in path else "lib/") + path.rsplit("/", 1)[1] + return path + + +def files(platform, runtime=True): + """[(path in NVIDIA's repository, SHA-256, path in the SDK folder)] for one platform; runtime=False: what a build reads only""" + spec = PLATFORMS[platform] + return [(p, s, destination(p)) for p, s in COMMON + [spec["library"]] + ([spec["runtime"]] if runtime else [])] + + +def library(sdk, platform): + """The library a build with this SDK folder links, for that platform""" + return os.path.join(sdk, *destination(PLATFORMS[platform]["library"][0]).split("/")) + + +def runtime(sdk, platform): + """The DLSS runtime in this SDK folder, for that platform""" + return os.path.join(sdk, *destination(PLATFORMS[platform]["runtime"][0]).split("/")) + + +def runtime_name(platform): + return PLATFORMS[platform]["runtime"][0].rsplit("/", 1)[1] + + +def sha256(path): + h = hashlib.sha256() + with open(path, "rb") as f: + for block in iter(lambda: f.read(1 << 20), b""): + h.update(block) + return h.hexdigest() + + +TIMEOUT = 60 # (seconds without an answer, or between two reads, before a download gives up: a stalled one ends the build) + + +def fetch_one(path, digest, dst, opener=urllib.request.urlopen): + """One file into dst, unless it is there already with that SHA-256. The download goes to dst.part and is moved in only + when its SHA-256 is the expected one; otherwise it is deleted and ValueError says which""" + if os.path.isfile(dst) and sha256(dst) == digest: + return False + os.makedirs(os.path.dirname(dst) or ".", exist_ok=True) + tmp = dst + ".part" + try: + with opener(BASE + "/" + path, timeout=TIMEOUT) as r, open(tmp, "wb") as f: + shutil.copyfileobj(r, f, 1 << 20) + if sha256(tmp) != digest: + raise ValueError("ngx-sdk: %s is not the file expected (SHA-256): not used" % path) + os.replace(tmp, dst) + finally: + if os.path.exists(tmp): + os.remove(tmp) + return True + + +def fetch(sdk, platform, opener=urllib.request.urlopen, say=print, runtime=True): + """Every file of that platform into the SDK folder (runtime=False: not DLSS's runtime)""" + for path, digest, rel in files(platform, runtime): + if fetch_one(path, digest, os.path.join(sdk, *rel.split("/")), opener): + say("ngx-sdk: %s" % rel) + + +def install_runtime(sdk, platform, folder): + """The SDK folder's DLSS runtime copied into folder (made when missing): where the game finds it""" + src = runtime(sdk, platform) + dst = os.path.join(folder, runtime_name(platform)) + os.makedirs(folder, exist_ok=True) + if not (os.path.isfile(dst) and os.path.getsize(dst) == os.path.getsize(src) and sha256(dst) == sha256(src)): + shutil.copyfile(src, dst + ".part") + os.replace(dst + ".part", dst) + return dst + + +def accepted(flag, ask=input, interactive=None): + """NVIDIA's license, accepted: the flag, or a typed yes when there is someone to ask""" + if flag: + return True + if not (sys.stdin.isatty() if interactive is None else interactive): + return False + try: + return ask("Do you accept NVIDIA's license? Type yes to go on: ").strip().lower() in ("y", "yes") + except EOFError: + return False + + +def main(argv=None): + p = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + p.add_argument("sdk", nargs="?") + p.add_argument("--platform", choices=sorted(PLATFORMS), default=host_platform()) + p.add_argument("--game", help="a game folder: the DLSS runtime into its ngx folder") + p.add_argument("--runtime-dir", help="the DLSS runtime into this folder") + p.add_argument("--accept-nvidia-license", action="store_true") + p.add_argument("--build-only", action="store_true", help="not DLSS's runtime: only what the build reads") + a = p.parse_args(argv) + if a.build_only and (a.game or a.runtime_dir): + p.error("--build-only fetches no runtime for --game or --runtime-dir") + sdk = os.path.abspath(a.sdk or os.path.join(ROOT, "build-linux" if a.platform == "linux" else "work", "ngx-sdk")) + print(NOTICE, flush=True) + if not accepted(a.accept_nvidia_license): + print("ngx-sdk: NVIDIA's license was not accepted (--accept-nvidia-license, or yes when asked): nothing downloaded", + file=sys.stderr, flush=True) + return 2 + try: + fetch(sdk, a.platform, runtime=not a.build_only) + except (OSError, ValueError) as e: + print("ngx-sdk: %s" % e, file=sys.stderr, flush=True) + return 1 + print("ngx-sdk: the SDK's headers and library in %s (NVIDIA's license: %s)" % (sdk, os.path.join(sdk, "LICENSE.txt")), + flush=True) + for folder in ([os.path.join(a.game, "ngx")] if a.game else []) + ([a.runtime_dir] if a.runtime_dir else []): + print("ngx-sdk: the DLSS runtime in %s" % install_runtime(sdk, a.platform, folder), flush=True) + print("ngx-sdk: build with HOOKSHOT_NGX_SDK=%s" % sdk, flush=True) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/builder_workers/Program.cs b/tests/builder_workers/Program.cs index d03ec5ad3..6842544f4 100644 --- a/tests/builder_workers/Program.cs +++ b/tests/builder_workers/Program.cs @@ -16,7 +16,7 @@ static void Require(bool ok, string message) } static ProcessStartInfo Info(Tools tools, string[] args, int? workers = null) => - (ProcessStartInfo)InfoMethod.Invoke(null, new object?[] { tools, args, workers })!; + (ProcessStartInfo)InfoMethod.Invoke(null, new object?[] { tools, args, workers, null })!; // (ngxSdk: none) static async Task Delivery(ProcessStartInfo info) { diff --git a/tests/run_all.py b/tests/run_all.py index 6a18848c0..9c5587861 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -42,6 +42,7 @@ "tests/test_taau_env_lookup.py": ["--cxx", "g++"], "tests/test_target_overlap.py": ["--cxx", "g++"], "tests/test_menu_hit.py": ["--cxx", "g++"], + "tests/test_menu_restart.py": ["--cxx", "g++", "--cc", "gcc"], "tests/test_output_resolution.py": ["--cxx", "{cxx}"], "tests/test_nvn_buffer_map.py": ["--cc", "gcc"], "tests/test_startup_progress.py": ["--cxx", "g++"], diff --git a/tests/screensaver_test.c b/tests/screensaver_test.c index 258c4f596..f64833c88 100644 --- a/tests/screensaver_test.c +++ b/tests/screensaver_test.c @@ -77,9 +77,8 @@ static BOOL SetWindowPos(HWND w, HWND after, int x, int y, int width, int height static void *SetCursor(void *cursor) { (void)cursor; unexpected(); return NULL; } static void rx_resize(unsigned width, unsigned height) { (void)width; (void)height; unexpected(); } static BOOL ValidateRect(HWND w, const RECT *rect) { (void)w; (void)rect; unexpected(); return FALSE; } -static void hos_log(const char *text) { (void)text; unexpected(); } -static void hos_log_flush(void) { unexpected(); } -static void ExitProcess(unsigned code) { (void)code; unexpected(); } +static void hos_relaunch_ask(void) { unexpected(); } /* (host/hos_relaunch.c: WM_APP + 9 and WM_CLOSE end the process) */ +static void hos_window_closed_exit(void) { unexpected(); } static void rx_close_asked(void) { unexpected(); } /* (host/rx_backend.c: WM_CLOSE, not one of the checked messages) */ void hos_vk_note_key_press(unsigned key) { (void)key; } /* (host/rx_backend.c: the menu notes key presses; nothing here) */ diff --git a/tests/test_dlss_actor.cpp b/tests/test_dlss_actor.cpp new file mode 100644 index 000000000..74970dfd8 --- /dev/null +++ b/tests/test_dlss_actor.cpp @@ -0,0 +1,31 @@ +#include "../gpu/vk/dlss_actor.h" + +#include + +#define CHECK(name, condition) do { if (!(condition)) { std::fprintf(stderr, "FAIL %s at line %d\n", name, __LINE__); return 1; } } while (0) + +int main() { + namespace da = dlss_actor; + // names: the same draw keeps its name, another draw of the same model is the next one, another model another name + const uint64_t a0 = da::key(0x1000, 0x2000, 9702, 0, 0xabc, 0), a1 = da::key(0x1000, 0x2000, 9702, 0, 0xabc, 1); + CHECK("the same draw, the same name", a0 == da::key(0x1000, 0x2000, 9702, 0, 0xabc, 0)); + CHECK("the second draw of a model is another", a0 != a1 && da::same_model(a0, a1)); + CHECK("another model", !da::same_model(a0, da::key(0x1000, 0x2000, 9702, 3, 0xabc, 0)) && + !da::same_model(a0, da::key(0x1040, 0x2000, 9702, 0, 0xabc, 0)) && + !da::same_model(a0, da::key(0x1000, 0x2000, 9702, 0, 0xabd, 0))); + CHECK("many draws of one model", da::same_model(a0, da::key(0x1000, 0x2000, 9702, 0, 0xabc, 900))); + + // where a draw's indices start for the shader, which reads 32-bit words: the address 4-aligned down, the first index in it + uint64_t at = 0; + uint32_t first = 0; + da::index_start(0x10002, 1, &at, &first); + CHECK("16-bit at +2: the second half of the word", at == 0x10000 && first == 1); + da::index_start(0x10003, 0, &at, &first); + CHECK("8-bit at +3: the fourth byte", at == 0x10000 && first == 3); + da::index_start(0x10008, 2, &at, &first); + CHECK("32-bit, aligned", at == 0x10008 && first == 0); + da::index_start(0x10004, 1, &at, &first); + CHECK("16-bit, aligned", at == 0x10004 && first == 0); + std::puts("dlss_actor: ok"); + return 0; +} diff --git a/tests/test_dlss_actor.py b/tests/test_dlss_actor.py new file mode 100644 index 000000000..e0aa20bef --- /dev/null +++ b/tests/test_dlss_actor.py @@ -0,0 +1,69 @@ +"""NVIDIA DLSS's actor motion (gpu/vk/vk_dlss.cpp, gpu/vk/dlss_actor.h): the CPU side's draw names and index addresses +(tests/test_dlss_actor.cpp), the push constants of shaders/dlss_actor.vert and .frag against dlss_actor::Push, and the +SPIR-V headers of the DLSS shaders made from their sources as they are now (when glslangValidator is here). No game or GPU. + +python -I -B tests/test_dlss_actor.py [--cxx g++] +""" +import argparse +import re +import shutil +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SHADERS = ROOT / "gpu" / "vk" / "shaders" + + +def fields(text, start, end): + """The field names of a block: uint a, b[2]; -> ['a', 'b[2]']""" + body = text[text.index(start) + len(start):] + body = body[:body.index(end)] + body = re.sub(r"//[^\n]*", "", body) + out = [] + for decl in body.split(";"): + decl = decl.strip() + if not decl: + continue + names = re.sub(r"^\s*(uint64_t|uint32_t|int32_t|uint|int)\s+", "", decl) + out += [n.strip() for n in names.split(",")] + return out + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="g++") + args = parser.parse_args() + header = (ROOT / "gpu" / "vk" / "dlss_actor.h").read_text(encoding="utf-8") + want = fields(header, "struct Push {", "};") + if len(want) != 16: + raise SystemExit("dlss_actor::Push: %d fields read: %s" % (len(want), want)) + for name in ("dlss_actor.vert", "dlss_actor.frag"): + got = fields((SHADERS / name).read_text(encoding="utf-8"), "uniform P {", "} pc;") + if got != want: + raise SystemExit("%s: its push constants %s are not dlss_actor::Push's %s" % (name, got, want)) + dlss = (ROOT / "gpu" / "vk" / "vk_dlss.cpp").read_text(encoding="utf-8") + if "using ActorPush = dlss_actor::Push;" not in dlss or "dlss_actor::index_start(" not in dlss: + raise SystemExit("vk_dlss.cpp does not use dlss_actor.h") + glslang = shutil.which("glslangValidator") + with tempfile.TemporaryDirectory(prefix="dlss-actor-") as tmp: + if glslang: + for src in ("dlss_actor.vert", "dlss_actor.frag", "dlss_prep.comp", "dlss_out.comp"): + var = src.replace(".", "_").upper() + "_SPV" + out = Path(tmp) / (src.replace(".", "_") + "_spv.h") + subprocess.run([glslang, "-V", "--target-env", "vulkan1.3", "--vn", var, "-o", str(out), str(SHADERS / src)], + check=True, capture_output=True) + words = lambda t: re.findall(r"0x[0-9a-f]{8}", t) + if words(out.read_text()) != words((SHADERS / out.name).read_text()): + raise SystemExit("shaders/%s is not made from %s as it is now (glslangValidator -V --target-env vulkan1.3 " + "--vn %s -o %s %s)" % (out.name, src, var, out.name, src)) + else: + print("glslangValidator not found: the SPIR-V headers not checked") + exe = Path(tmp) / "dlss-actor" + subprocess.run([args.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(ROOT / "tests" / "test_dlss_actor.cpp"), + "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tests/test_dlss_math.cpp b/tests/test_dlss_math.cpp new file mode 100644 index 000000000..432ae3f1c --- /dev/null +++ b/tests/test_dlss_math.cpp @@ -0,0 +1,174 @@ +#include "../gpu/vk/dlss_math.h" + +#include +#include +#include + +#define CHECK(name, condition) do { if (!(condition)) { std::fprintf(stderr, "FAIL %s at line %d\n", name, __LINE__); return 1; } } while (0) + +namespace dm = mkwvk::dlss_math; + +static bool near(float a, float b, float eps = 1e-5f) { return std::fabs(a - b) <= eps; } +static void mul(const float* a, const float* b, float* out) { // (row major 4x4: out = a * b) + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) { + float v = 0; + for (int k = 0; k < 4; k++) v += a[r * 4 + k] * b[k * 4 + c]; + out[r * 4 + c] = v; + } +} +static void apply(const float* m, const float* v, float* out) { + for (int r = 0; r < 4; r++) out[r] = m[r * 4] * v[0] + m[r * 4 + 1] * v[1] + m[r * 4 + 2] * v[2] + m[r * 4 + 3] * v[3]; +} +/// A camera as the game's: a perspective projection (w row 0 0 -1 0) after a view of yaw and a position, plus a jitter +static void camera(float yaw, float x, float z, const float jitter[2], float out[16]) { + const float f = 2.41421f, a = 16.0f / 9.0f, n = 0.1f, fa = 1000.0f; + float p[16] = {f / a, 0, 0, 0, 0, f, 0, 0, 0, 0, -(fa + n) / (fa - n), -2 * fa * n / (fa - n), 0, 0, -1, 0}; + p[2] += jitter[0] * -1.0f, p[6] += jitter[1] * -1.0f; // (sead's offset: in the third column, times -w's -1) + const float c = std::cos(yaw), s = std::sin(yaw); + const float v[16] = {c, 0, -s, -(c * x - s * z), 0, 1, 0, 0, s, 0, c, -(s * x + c * z), 0, 0, 0, 1}; + mul(p, v, out); +} +/// dlss_prep.comp's motion for a pixel at clip (x, y) and depth d: in render pixels (size w x h) +static void motion(const float reproject[16], float x, float y, float d, const float jitter[2], float w, float h, float out[2]) { + const float clip[4] = {x, y, d, 1.0f}; + float before[4]; + apply(reproject, clip, before); + const float then[2] = {before[0] / before[3], before[1] / before[3]}; + const float now[2] = {x - jitter[0], y - jitter[1]}; + out[0] = (then[0] - now[0]) * 0.5f * w, out[1] = (now[1] - then[1]) * 0.5f * h; +} + +int main() { + // the Halton sequence + CHECK("halton base 2", near(dlss_halton(1, 2), 0.5f) && near(dlss_halton(2, 2), 0.25f) && near(dlss_halton(3, 2), 0.75f) && + near(dlss_halton(4, 2), 0.125f)); + CHECK("halton base 3", near(dlss_halton(1, 3), 1 / 3.0f) && near(dlss_halton(2, 3), 2 / 3.0f) && near(dlss_halton(3, 3), 1 / 9.0f)); + CHECK("halton of 0", dlss_halton(0, 2) == 0.0f); + for (int phases : {8, 16, 32, 64}) { + float sum[2] = {0, 0}; + for (int i = 0; i < phases; i++) { + float j[2]; + dlss_jitter_pixels((unsigned)i, phases, j); + CHECK("jitter within the pixel", j[0] >= -0.5f && j[0] < 0.5f && j[1] >= -0.5f && j[1] < 0.5f); + for (int k = 0; k < i; k++) { + float o[2]; + dlss_jitter_pixels((unsigned)k, phases, o); + CHECK("each phase its own offset", !(near(o[0], j[0]) && near(o[1], j[1]))); + } + sum[0] += j[0], sum[1] += j[1]; + } + CHECK("jitter centred on the pixel", std::fabs(sum[0] / phases) < 0.07f && std::fabs(sum[1] / phases) < 0.07f); + float a[2], b[2]; + dlss_jitter_pixels(3, phases, a), dlss_jitter_pixels(3 + phases, phases, b); + CHECK("the sequence repeats", a[0] == b[0] && a[1] == b[1]); + } + { // (the same as host/rx_backend.c's sequence before it moved here) + for (int frame = 0; frame < 40; frame++) { + const int i = frame % 16 + 1; + float x = 0.0f, y = 0.0f, f = 0.5f; + for (int k = i; k > 0; k /= 2, f *= 0.5f) x += f * (float)(k % 2); + f = 1.0f / 3.0f; + for (int k = i; k > 0; k /= 3, f /= 3.0f) y += f * (float)(k % 3); + float j[2]; + dlss_jitter_pixels((unsigned)frame, 16, j); + CHECK("the sequence as before", near(j[0], x - 0.5f) && near(j[1], y - 0.5f)); + } + } + CHECK("no phases is one", [] { float j[2]; dlss_jitter_pixels(5, 0, j); return near(j[0], 0.0f) && near(j[1], 1 / 3.0f - 0.5f); }()); + + // the sequence's length: 8 x (output / render) squared, 8 to 64 + CHECK("DLAA: 8", dlss_jitter_phases(2560, 1440, 2560, 1440) == 8); + CHECK("Quality (2/3): 18", dlss_jitter_phases(1707, 960, 2560, 1440) == 18); + CHECK("the game's 3/4: 14", dlss_jitter_phases(1536, 864, 2048, 1152) == 14); + CHECK("Performance (1/2): 32", dlss_jitter_phases(1280, 720, 2560, 1440) == 32); + CHECK("Ultra Performance (1/3): 64 at most", dlss_jitter_phases(683, 384, 2048, 1152) == 64); + CHECK("no size: 8", dlss_jitter_phases(0, 0, 2048, 1152) == 8); + + // the reprojection: previous * inverse(current) + const float none[2] = {0, 0}; + float cur[16], prev[16], r[16]; + camera(0.3f, 1.0f, 2.0f, none, cur); + CHECK("same camera: identity", dm::reprojection(cur, cur, r)); + for (int i = 0; i < 16; i++) CHECK("identity", near(r[i], i % 5 == 0 ? 1.0f : 0.0f, 1e-4f)); + camera(0.35f, 1.2f, 1.9f, none, prev); + CHECK("another camera", dm::reprojection(cur, prev, r)); + float back[16]; + mul(r, cur, back); + for (int i = 0; i < 16; i++) CHECK("reprojection * current = previous", near(back[i], prev[i], 1e-3f)); + const float singular[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; + CHECK("a singular camera", !dm::reprojection(singular, prev, r)); + + // the jitter taken off: a jittered camera unjittered is the camera + const float jit[2] = {0.0013f, -0.0021f}; + float jittered[16], plain[16]; + camera(0.3f, 1.0f, 2.0f, jit, jittered); + camera(0.3f, 1.0f, 2.0f, none, plain); + dm::unjitter(jittered, jit); + for (int i = 0; i < 16; i++) CHECK("unjitter", near(jittered[i], plain[i], 1e-5f)); + + // the motion of a still camera with a different jitter each frame is none, anywhere on the screen + { + const float j0[2] = {-0.0009f, 0.0016f}, j1[2] = {0.0012f, -0.0004f}; + float c1[16], c0[16], m[16], mv[2]; + camera(0.3f, 1.0f, 2.0f, j1, c1); + camera(0.3f, 1.0f, 2.0f, j0, c0); + dm::unjitter(c0, j0); + CHECK("still camera", dm::reprojection(c1, c0, m)); + for (float x : {-0.9f, 0.0f, 0.7f}) + for (float y : {-0.5f, 0.3f}) + for (float d : {0.2f, 0.9f}) { + motion(m, x, y, d, j1, 1536, 864, mv); + CHECK("no motion from the jitter", near(mv[0], 0.0f, 1e-2f) && near(mv[1], 0.0f, 1e-2f)); + } + } + // a camera that turns: the motion is the turn's, the same with or without jitter + { + const float j0[2] = {-0.0009f, 0.0016f}, j1[2] = {0.0012f, -0.0004f}; + float a1[16], a0[16], b1[16], b0[16], ma[16], mb[16], va[2], vb[2]; + camera(0.32f, 1.0f, 2.0f, j1, a1), camera(0.3f, 1.0f, 2.0f, j0, a0); + camera(0.32f, 1.0f, 2.0f, none, b1), camera(0.3f, 1.0f, 2.0f, none, b0); + dm::unjitter(a0, j0); + CHECK("turning camera", dm::reprojection(a1, a0, ma) && dm::reprojection(b1, b0, mb)); + // (the same point of the scene: on the jittered screen it is at the plain screen's place plus the jitter) + float p[4] = {0.5f, 0.2f, -3.0f, 1.0f}, cb[4], ca[4]; + apply(b1, p, cb), apply(a1, p, ca); + const float xb = cb[0] / cb[3], yb = cb[1] / cb[3], db = cb[2] / cb[3], xa = ca[0] / ca[3], ya = ca[1] / ca[3]; + motion(mb, xb, yb, db, none, 1536, 864, vb); + motion(ma, xa, ya, db, j1, 1536, 864, va); + CHECK("the turn's motion", std::fabs(vb[0]) > 1.0f && near(va[0], vb[0], 0.05f) && near(va[1], vb[1], 0.05f)); + } + + // the jitter as DLSS takes it: render pixels, y down + float px[2]; + const float clip[2] = {2.0f / 1536, 2.0f / 864}; + dm::jitter_to_pixels(clip, 1536, 864, px); + CHECK("jitter in pixels", near(px[0], 1.0f) && near(px[1], -1.0f)); + + // which projections get the jitter (nx2/oot_jitter.py: the game's views' uniform buffers): the picture's perspective ones + { + const float f = 2.14451f, n = 0.1f; + float scene[16] = {f / (16.0f / 9.0f), 0, 0, 0, 0, f, 0, 0, 0, 0, -1.00001f, -n, 0, 0, -1, 0}; + float out[2], expect[2]; + dlss_jitter_pixels(5, 8, expect); + CHECK("the scene's projection", dlss_jitter_clip(scene, 0.0f, 5, 8, 3840, 2160, out) && + near(out[0], 2 * expect[0] / 3840) && near(out[1], 2 * expect[1] / 2160)); + // (in the jitter's clip units: the picture moves by out * w / 2 pixels, what jitter_to_pixels gives back) + dm::jitter_to_pixels(out, 3840, 2160, px); + CHECK("a sub-pixel offset", near(px[0], expect[0]) && near(px[1], -expect[1])); + CHECK("none while DLSS is not drawing", !dlss_jitter_clip(scene, 0.0f, 5, 8, 0, 0, out)); + const float ortho[16] = {0.0984f, 0, 0, 5.78f, 0, 0.1535f, 0, 0.31f, 0, 0, -0.11f, 10.66f, 0, 0, 0, 1}; + CHECK("not a shadow map's (orthographic)", !dlss_jitter_clip(ortho, 0.0f, 5, 8, 3840, 2160, out)); + float square[16]; + std::memcpy(square, scene, sizeof square); + square[0] = f; // (1:1: the per-character shadow projector, a portrait) + CHECK("not another ratio's", !dlss_jitter_clip(square, 0.0f, 5, 8, 3840, 2160, out)); + float wide[16]; + std::memcpy(wide, scene, sizeof wide); + wide[0] = f / (21.0f / 9.0f); // (an ultrawide window's: nx2/oot_wide.py gives the picture's cameras its ratio) + CHECK("not 21:9 on a 16:9 window", !dlss_jitter_clip(wide, 0.0f, 5, 8, 3840, 2160, out)); + CHECK("21:9 on a 21:9 window", dlss_jitter_clip(wide, 21.0f / 9.0f, 5, 8, 5040, 2160, out)); + CHECK("16:9 still, on a 21:9 window", dlss_jitter_clip(scene, 21.0f / 9.0f, 5, 8, 5040, 2160, out)); + } + std::puts("dlss math: all checks passed"); +} diff --git a/tests/test_dlss_math.py b/tests/test_dlss_math.py new file mode 100644 index 000000000..0344f322e --- /dev/null +++ b/tests/test_dlss_math.py @@ -0,0 +1,33 @@ +"""NVIDIA DLSS's camera math (gpu/vk/dlss_math.h): the jitter's Halton sequence, the reprojection that makes the motion +vectors, the jitter taken off the motion, the jitter in DLSS's pixels and which projections get it. Built as C++ and run; +the header is also built as C, as host/rx_backend.c includes it. No game, GPU or NVIDIA SDK required. + +python -I -B tests/test_dlss_math.py [--cxx g++] [--cc gcc] +""" +import argparse +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="g++") + parser.add_argument("--cc", default="gcc") + args = parser.parse_args() + with tempfile.TemporaryDirectory(prefix="dlss-math-") as tmp: + c = Path(tmp) / "c.c" + c.write_text('#include "%s"\nint main(void) { float j[2]; dlss_jitter_pixels(3, 16, j); return j[0] < 0.5f ? 0 : 1; }\n' + % (ROOT / "gpu" / "vk" / "dlss_math.h").as_posix()) + subprocess.run([args.cc, "-std=c11", "-Wall", "-Wextra", "-Werror", str(c), "-o", str(Path(tmp) / "c")], check=True) + subprocess.run([str(Path(tmp) / "c")], check=True) + exe = Path(tmp) / "dlss-math" + subprocess.run([args.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(ROOT / "tests" / "test_dlss_math.cpp"), + "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + main() diff --git a/tests/test_make_game_ngx.py b/tests/test_make_game_ngx.py new file mode 100644 index 000000000..035a2564b --- /dev/null +++ b/tests/test_make_game_ngx.py @@ -0,0 +1,128 @@ +"""nx2/make_game.py and NVIDIA DLSS: the runtime placed in the game folder's ngx folder (never beside the exe), and the +fetch the Hookshot Builder's DLSS tick asks for, which needs NVIDIA's license accepted. No network, no game. + +Run with: python -I tests/test_make_game_ngx.py +""" +import importlib.util +import io +import os +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location("make_game_ngx_under_test", ROOT / "nx2" / "make_game.py") +make_game = importlib.util.module_from_spec(spec) +spec.loader.exec_module(make_game) + + +class NgxRuntimeTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="hookshot-make-game-ngx-") + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.sdk = self.root / "sdk" + self.out = self.root / "game" + (self.out / "bin").mkdir(parents=True) + self.ngx = make_game.ngx_script() + + def sdk_files(self, platform, runtime=True): + lib = Path(self.ngx.library(str(self.sdk), platform)) + lib.parent.mkdir(parents=True, exist_ok=True) + lib.write_bytes(b"lib") + if runtime: + rt = Path(self.ngx.runtime(str(self.sdk), platform)) + rt.parent.mkdir(parents=True, exist_ok=True) + rt.write_bytes(b"runtime") + + def place(self, platform, sdk=None): + env = {"HOOKSHOT_NGX_SDK": str(self.sdk) if sdk is None else sdk} + with mock.patch.object(make_game, "LINUX", platform == "linux"), mock.patch.dict(os.environ, env), \ + mock.patch("sys.stdout", io.StringIO()): + return make_game.place_ngx_runtime(str(self.out)) + + def test_windows_runtime_goes_to_the_ngx_folder(self): + self.sdk_files("windows") + dst = self.place("windows") + self.assertEqual(Path(dst), self.out / "ngx" / "nvngx_dlss.dll") + self.assertEqual(Path(dst).read_bytes(), b"runtime") + self.assertEqual(sorted(p.name for p in self.out.iterdir()), ["bin", "ngx"]) + self.assertEqual(list((self.out / "bin").iterdir()), []) # (nothing beside the exe) + self.assertFalse([p for p in self.out.rglob("*") if p.name.lower() == "_nvngx.dll"]) + + def built(self, platform, sdk=None): + env = {"HOOKSHOT_NGX_SDK": str(self.sdk) if sdk is None else sdk} + with mock.patch.object(make_game, "LINUX", platform == "linux"), mock.patch.dict(os.environ, env): + return make_game.ngx_built() + + def test_jitter_only_in_a_build_with_the_sdk(self): + # (make_game sets OOT_DLSS_JITTER=1 for the lift only then: nx2/oot_jitter.py) + self.assertFalse(self.built("linux", sdk="")) + self.assertFalse(self.built("linux")) # (no library yet) + self.sdk_files("linux", runtime=False) + self.assertTrue(self.built("linux")) + self.assertFalse(self.built("windows")) # (another platform's library only) + import sys + sys.path.insert(0, str(ROOT / "nx2")) + try: + import oot_jitter + finally: + sys.path.pop(0) + with mock.patch.dict(os.environ, {"OOT_DLSS_JITTER": "1"}): + self.assertTrue(oot_jitter.wanted()) + with mock.patch.dict(os.environ, {}, clear=True): + self.assertFalse(oot_jitter.wanted()) + + def test_linux_runtime(self): + self.sdk_files("linux") + self.assertEqual(Path(self.place("linux")), self.out / "ngx" / "libnvidia-ngx-dlss.so.310.9.16") + + def test_nothing_without_the_sdk_or_its_platform(self): + self.assertIsNone(self.place("windows", sdk="")) + self.sdk_files("linux") + self.assertIsNone(self.place("windows")) # (a Linux SDK folder: no Windows library, no DLSS in this build) + self.sdk_files("windows", runtime=False) + self.assertIsNone(self.place("windows")) + self.assertFalse((self.out / "ngx").exists()) + + +class FetchGateTests(unittest.TestCase): + def fetch(self, accept, sdk="sdk"): + calls = [] + ngx = SimpleNamespace(NOTICE="notice", fetch=lambda *a, **k: calls.append(a)) + with mock.patch.object(make_game, "ngx_script", return_value=ngx), mock.patch.dict(os.environ, {"HOOKSHOT_NGX_SDK": sdk}), \ + mock.patch("sys.stdout", io.StringIO()) as out: + try: + make_game.fetch_ngx_sdk(SimpleNamespace(accept_nvidia_license=accept)) + except SystemExit as e: + return calls, e.code, out.getvalue() + return calls, 0, out.getvalue() + + def test_needs_the_license_accepted(self): + calls, code, out = self.fetch(False) + self.assertEqual((calls, code), ([], 1)) + self.assertIn("@@error", out) + self.assertIn("license", out) + + def test_needs_the_sdk_folder(self): + calls, code, _ = self.fetch(True, sdk="") + self.assertEqual((calls, code), ([], 1)) + + def test_fetches_this_platforms_files_after_the_notice(self): + calls, code, out = self.fetch(True) + self.assertEqual(code, 0) + self.assertEqual(calls, [("sdk", make_game.ngx_platform())]) + self.assertTrue(out.startswith("notice")) + + def test_flags_are_off_by_default(self): + with mock.patch("sys.argv", ["make_game.py", "--out", "x", "--nup", "y"]): + a = make_game.parse_args() + self.assertFalse(a.fetch_ngx_sdk) + self.assertFalse(a.accept_nvidia_license) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_menu_hit.cpp b/tests/test_menu_hit.cpp index ecd8514ec..90e4de08f 100644 --- a/tests/test_menu_hit.cpp +++ b/tests/test_menu_hit.cpp @@ -176,5 +176,23 @@ int main() { CHECK("list, scale 2: the last line", h.part == kListLine && h.index == 19 && h.side == 0); } + // ---- the restart question: Later from 16, Restart now from 182, both 150 wide, 120 to 154 high ---- + { + const int ah = kAskHeight; + h = ask(20, 130, 1, kWidth, ah); + CHECK("ask: Later", h.part == kButton && h.index == 0); + h = ask(331, 153, 1, kWidth, ah); + CHECK("ask: Restart now", h.part == kButton && h.index == 1); + CHECK("ask: between the buttons", ask(170, 130, 1, kWidth, ah).part == kNone); + CHECK("ask: past Restart now", ask(340, 130, 1, kWidth, ah).part == kNone); + CHECK("ask: above the buttons", ask(20, 119, 1, kWidth, ah).part == kNone); + CHECK("ask: below the buttons", ask(20, 154, 1, kWidth, ah).part == kNone); + CHECK("ask: off the page", ask(20, ah, 1, kWidth, ah).part == kOutside); + h = ask(190 * 2, 125 * 2, 2, kWidth * 2, ah * 2); + CHECK("ask, scale 2: Restart now", h.part == kButton && h.index == 1); + h = ask(17 * 2, 153 * 2 + 1, 2, kWidth * 2, ah * 2); + CHECK("ask, scale 2: Later", h.part == kButton && h.index == 0); + } + std::puts("menu hit-test: all checks passed"); } diff --git a/tests/test_menu_restart.cpp b/tests/test_menu_restart.cpp new file mode 100644 index 000000000..c52793136 --- /dev/null +++ b/tests/test_menu_restart.cpp @@ -0,0 +1,73 @@ +// The Insert menu's rows that take a restart (gpu/vk/menu_restart.h): what is pending, when closing the menu asks to +// restart, and the names the menu lists. +#include "../gpu/vk/menu_restart.h" + +#include + +#define CHECK(name, condition) do { if (!(condition)) { std::fprintf(stderr, "FAIL %s at line %d\n", name, __LINE__); return 1; } } while (0) + +int main() { + namespace mr = mkwvk::menu_restart; + const char* names[] = {"GPU device", "Output resolution", "Handheld profile", "Ultrawide", "Robust buffers"}; + mr::State st; + // (the five rows as the game started: Auto, 2048x1152, Off, 16:9, Auto) + int now[5] = {0, 0, 0, 0, 0}; + for (int v : now) CHECK("a slot each", st.add(v) >= 0); + CHECK("five slots", st.n == 5); + CHECK("nothing pending at the start", st.pending(now) == 0 && !st.ask(now)); + + // a change: pending, asked at the close + now[1] = 1; + st.note(now); + CHECK("Output resolution pending", st.pending(now) == 2u); + CHECK("asked at the close", st.ask(now)); + // changed back: nothing pending, not asked + now[1] = 0; + st.note(now); + CHECK("changed back: nothing pending", st.pending(now) == 0 && !st.ask(now)); + + // Later: not asked again for the same values + now[1] = 2, now[3] = 1; + st.note(now); + CHECK("two pending", st.pending(now) == (2u | 8u) && st.ask(now)); + st.choose_later(now); + CHECK("Later: still pending", st.pending(now) == (2u | 8u)); + CHECK("Later: not asked again", !st.ask(now) && !st.ask(now)); + // ... until something changes: then asked again, even once it is back to the values Later was chosen for + now[3] = 0; + st.note(now); + CHECK("Later, then a change: asked", st.ask(now)); + now[3] = 1; + st.note(now); + CHECK("changed back to Later's values: still asked", st.ask(now)); + st.choose_later(now); + CHECK("Later again", !st.ask(now)); + now[1] = 1; // (a change seen only at the close, not through note) + CHECK("a change seen at the close: asked", st.ask(now)); + // Later with everything back as it started: nothing to ask, and a new change asks + now[1] = 0, now[3] = 0; + st.choose_later(now); + now[4] = 2; + st.note(now); + CHECK("Later with nothing pending, then a change: asked", st.ask(now)); + + // the GPU row's start value comes once the cards are counted + mr::State gpu; + int g[1] = {3}; + gpu.add(0); + gpu.restart_value(0, 3); + CHECK("the GPU's start value again", gpu.pending(g) == 0 && !gpu.ask(g)); + gpu.restart_value(4, 1); // (no such slot: nothing) + CHECK("a slot past the rows", gpu.start[0] == 3); + mr::State full; + for (int i = 0; i < mr::kMax; i++) full.add(0); + CHECK("no slot past kMax", full.add(0) == -1); + + // the names, in the rows' order + CHECK("one name", mr::names(names, 5, 2u) == "Output resolution"); + CHECK("two names", mr::names(names, 5, 2u | 8u) == "Output resolution, Ultrawide"); + CHECK("the question's", mr::names(names, 5, 2u | 8u, ", ", " and ") == "Output resolution and Ultrawide"); + CHECK("three, the question's", mr::names(names, 5, 1u | 2u | 16u, ", ", " and ") == "GPU device, Output resolution and Robust buffers"); + CHECK("none", mr::names(names, 5, 0).empty()); + std::puts("menu restart: all checks passed"); +} diff --git a/tests/test_menu_restart.py b/tests/test_menu_restart.py new file mode 100644 index 000000000..0ac7e4e5f --- /dev/null +++ b/tests/test_menu_restart.py @@ -0,0 +1,144 @@ +"""The Insert menu's rows that take a restart: what is pending and when closing the menu asks (gpu/vk/menu_restart.h, +by behaviour in tests/test_menu_restart.cpp), the menu's use of it, and the path from Restart now to a new instance: the +window closes as the player closes it (host/display.c, host/posix/display_sdl.c), then host/hos_relaunch.c starts the +game again. The command it starts (host/hos_relaunch.h) is checked against this test's own /proc/self files and made-up +ones. No game or GPU required. + +python -I -B tests/test_menu_restart.py [--cxx g++] [--cc gcc] +""" +import argparse +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +RELAUNCH_TEST = r""" +#include "hos_relaunch.h" +#include + +static void write_file(const char *path, const char *bytes, size_t n) { + FILE *f = fopen(path, "wb"); + fwrite(bytes, 1, n, f); + fclose(f); +} +int main(int argc, char **argv) { + const char *dir = argv[1]; + char a[1024], e[1024]; + snprintf(a, sizeof a, "%s/cmdline", dir), snprintf(e, sizeof e, "%s/environ", dir); + /* (an empty argument kept, the last string ended by its NUL or not) */ + static const char args[] = "./bin/hookshot\0--user\0\0last"; + static const char env[] = "WAYLAND_DISPLAY=wayland-1\0XDG_DATA_HOME=/tmp/x\0"; + write_file(a, args, sizeof args - 1), write_file(e, env, sizeof env - 1); + HosRelaunch r; + assert(hos_relaunch_command(a, e, &r)); + assert(r.argc == 4 && !strcmp(r.argv[0], "./bin/hookshot") && !strcmp(r.argv[1], "--user") && !strcmp(r.argv[2], "") && + !strcmp(r.argv[3], "last") && !r.argv[4]); + assert(r.envc == 2 && !strcmp(r.envp[0], "WAYLAND_DISPLAY=wayland-1") && !strcmp(r.envp[1], "XDG_DATA_HOME=/tmp/x") && + !r.envp[2]); + /* a long command line (more than one read) */ + static char big[20000]; + for (int i = 0; i < (int)sizeof big; i++) big[i] = i % 100 == 99 ? 0 : 'a'; + write_file(a, big, sizeof big); + assert(hos_relaunch_command(a, e, &r) && r.argc == 200 && strlen(r.argv[199]) == 99); + /* no arguments, or no file: nothing to start */ + write_file(a, "", 0); + assert(!hos_relaunch_command(a, e, &r)); + snprintf(a, sizeof a, "%s/missing", dir); + assert(!hos_relaunch_command(a, e, &r)); + /* an empty environment is still one */ + snprintf(a, sizeof a, "%s/cmdline", dir); + write_file(a, "x", 1), write_file(e, "", 0); + assert(hos_relaunch_command(a, e, &r) && r.argc == 1 && r.envc == 0 && !r.envp[0]); + /* this process's own: its arguments and its environment as it started */ + assert(hos_relaunch_command("/proc/self/cmdline", "/proc/self/environ", &r)); + assert(r.argc == argc); + for (int i = 0; i < argc; i++) assert(!strcmp(r.argv[i], argv[i])); + int found = 0; + for (int i = 0; i < r.envc; i++) found |= !strcmp(r.envp[i], "HOS_RELAUNCH_TEST=one two"); + assert(found); + puts("relaunch command: all checks passed"); + return 0; +} +""" + + +def function(source, start): + at = source.find(start) + if at < 0: + raise SystemExit("%s is missing" % start) + body = source[at:] + return body[:body.index("\n}\n")] + + +def sources(): + menu = (ROOT / "gpu" / "vk" / "vk_menu.cpp").read_text(encoding="utf-8") + for row in ('"GPU device", &opts.gpu_device', '"Output resolution", &opts.output_res', '"Handheld profile", &opts.handheld', + '"Ultrawide", &opts.ultrawide', '"Robust buffers", &opts.robust'): + at = menu.find(row) + if at < 0: + raise SystemExit("vk_menu.cpp: a row that takes a restart is missing: " + row) + entry = menu[at:menu.index("},", at)] + if not re.search(r",\s*true$", entry): + raise SystemExit("vk_menu.cpp: %s is not marked as taking a restart" % row.split(",")[0]) + if "(restart)" in menu: + raise SystemExit("vk_menu.cpp: a value still says (restart) whatever runs: \"(after restart)\" is added while it differs") + if '" (after restart)"' not in menu or "Restart needed: " not in menu: + raise SystemExit("vk_menu.cpp: no (after restart) on a changed value, or no Restart needed line") + if "restart_init();" not in function(menu, "void menu_init() {"): + raise SystemExit("menu_init does not keep the start values") + if "restart_state.note(" not in function(menu, "void set_row("): + raise SystemExit("set_row does not tell the restart state of a change") + tick = function(menu, "void menu_tick() {") + for need in ("restart_ask()", "choose_later(", "menu_hit::ask(", "PostMessageA(app_window(), WM_APP + 9", "save();", + "pipeline_cache_save(true)"): + if need not in tick: + raise SystemExit("menu_tick: the restart question lacks " + need) + win = (ROOT / "host" / "display.c").read_text(encoding="utf-8") + sdl = (ROOT / "host" / "posix" / "display_sdl.c").read_text(encoding="utf-8") + close = win[win.index("case WM_APP + 9:"):] + close = close[:close.index("return 0;")] + if "hos_relaunch_ask();" not in close or "case WM_CLOSE:" not in close or "hos_window_closed_exit();" not in close: + raise SystemExit("display.c: WM_APP + 9 does not close the window as WM_CLOSE does, with the restart asked") + quit_ = sdl[sdl.index("case HOS_SDL_EV_QUIT:"):] + if "hos_window_closed_exit();" not in quit_[:quit_.index("case HOS_SDL_EV_RESIZED:")]: + raise SystemExit("display_sdl.c: closing the window does not go through hos_window_closed_exit") + if "WM_APP + 9" not in sdl or "hos_relaunch_ask();" not in sdl: + raise SystemExit("display_sdl.c: no WM_APP + 9") + relaunch = (ROOT / "host" / "hos_relaunch.c").read_text(encoding="utf-8") + end = function(relaunch, "void hos_window_closed_exit(void) {") + if end.index("hos_log_flush();") > end.index("relaunch();") or end.index("relaunch();") > end.index("ExitProcess(0);"): + raise SystemExit("hos_relaunch.c: the restart is not the last step before the exit (after the log)") + for need in ("GetModuleFileNameW(", "GetCommandLineW()", "CreateProcessW(", '"/proc/self/cmdline"', '"/proc/self/environ"', + 'execve("/proc/self/exe"'): + if need not in relaunch: + raise SystemExit("hos_relaunch.c: no " + need) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="g++") + parser.add_argument("--cc", default="gcc") + args = parser.parse_args() + sources() + with tempfile.TemporaryDirectory(prefix="menu-restart-") as tmp: + exe = Path(tmp) / "menu-restart" + subprocess.run([args.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(ROOT / "tests" / "test_menu_restart.cpp"), + "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + if sys.platform.startswith("linux"): + src = Path(tmp) / "relaunch_test.c" + src.write_text(RELAUNCH_TEST, encoding="utf-8") + rexe = Path(tmp) / "relaunch-test" + subprocess.run([args.cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "host"), str(src), "-o", str(rexe)], + check=True) + subprocess.run([str(rexe), tmp, "with an argument", ""], check=True, + env=dict(os.environ, HOS_RELAUNCH_TEST="one two")) + print("PASS: the restart rows and Restart now") + + +if __name__ == "__main__": + main() diff --git a/tests/test_ngx_sdk.py b/tests/test_ngx_sdk.py new file mode 100644 index 000000000..474d6bc09 --- /dev/null +++ b/tests/test_ngx_sdk.py @@ -0,0 +1,164 @@ +"""scripts/ngx_sdk.py: its per-platform file table, the SHA-256 checks and NVIDIA's license gate, without the network. + +Run with: python -I tests/test_ngx_sdk.py +""" +import hashlib +import importlib.util +import io +import os +from pathlib import Path +import tempfile +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location("ngx_sdk_under_test", ROOT / "scripts" / "ngx_sdk.py") +ngx_sdk = importlib.util.module_from_spec(spec) +spec.loader.exec_module(ngx_sdk) + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +class Opener: + """urlopen over a dict of path -> bytes; records what was asked for""" + def __init__(self, contents): + self.contents, self.asked = contents, [] + + def __call__(self, url, timeout=None): + if timeout != ngx_sdk.TIMEOUT: + raise AssertionError("urlopen without the timeout (%r)" % (timeout,)) + path = url[len(ngx_sdk.BASE) + 1:] + self.asked.append(path) + return io.BytesIO(self.contents[path]) + + +class TableTests(unittest.TestCase): + def test_pinned_release(self): + self.assertRegex(ngx_sdk.COMMIT, r"^[0-9a-f]{40}$") + self.assertTrue(ngx_sdk.BASE.startswith("https://raw.githubusercontent.com/NVIDIA/DLSS/" + ngx_sdk.COMMIT)) + + def test_every_file_has_a_sha256(self): + for platform in ngx_sdk.PLATFORMS: + for path, sha, _ in ngx_sdk.files(platform): + self.assertRegex(sha, r"^[0-9a-f]{64}$", path) + + def test_windows_files(self): + table = {rel: (path, sha) for path, sha, rel in ngx_sdk.files("windows")} + self.assertEqual(table["lib/nvsdk_ngx_s.lib"], + ("lib/Windows_x86_64/x64/nvsdk_ngx_s.lib", + "7b9a5239c1145af5f645ab472aa066f9ea79db36ac819562312aae69d65f3765")) + self.assertEqual(table["runtime/nvngx_dlss.dll"][0], "lib/Windows_x86_64/rel/nvngx_dlss.dll") + self.assertIn("LICENSE.txt", table) + self.assertIn("include/nvsdk_ngx_vk.h", table) + self.assertIn("include/nvsdk_ngx_helpers_vk.h", table) + # (nothing of Linux's, and never the NGX core: _nvngx.dll comes with the driver and must not be beside the game) + self.assertFalse([r for r in table if "nvngx.dll" in r and not r.endswith("nvngx_dlss.dll")]) + self.assertFalse([p for p, _ in table.values() if "Linux" in p]) + + def test_linux_files(self): + table = {rel: path for path, _, rel in ngx_sdk.files("linux")} + self.assertEqual(table["lib/libnvsdk_ngx.a"], "lib/Linux_x86_64/libnvsdk_ngx.a") + self.assertEqual(table["runtime/libnvidia-ngx-dlss.so.310.9.16"], "lib/Linux_x86_64/rel/libnvidia-ngx-dlss.so.310.9.16") + self.assertFalse([p for p in table.values() if "Windows" in p]) + + def test_build_only_leaves_out_the_runtime(self): + for platform in ngx_sdk.PLATFORMS: + full = {rel for _, _, rel in ngx_sdk.files(platform)} + build = {rel for _, _, rel in ngx_sdk.files(platform, runtime=False)} + self.assertEqual(full - build, {"runtime/" + ngx_sdk.runtime_name(platform)}) + + def test_platforms_share_the_headers_and_one_folder(self): + linux = {rel for _, _, rel in ngx_sdk.files("linux")} + windows = {rel for _, _, rel in ngx_sdk.files("windows")} + self.assertEqual({r for r in linux if not r.startswith(("lib/", "runtime/"))}, + {r for r in windows if not r.startswith(("lib/", "runtime/"))}) + self.assertEqual(len(linux & windows), len(ngx_sdk.COMMON)) # (both platforms' libraries fit in one SDK folder) + + def test_library_and_runtime_paths(self): + sdk = os.path.join("x", "sdk") + self.assertEqual(ngx_sdk.library(sdk, "windows"), os.path.join(sdk, "lib", "nvsdk_ngx_s.lib")) + self.assertEqual(ngx_sdk.library(sdk, "linux"), os.path.join(sdk, "lib", "libnvsdk_ngx.a")) + self.assertEqual(ngx_sdk.runtime(sdk, "windows"), os.path.join(sdk, "runtime", "nvngx_dlss.dll")) + self.assertEqual(ngx_sdk.runtime_name("windows"), "nvngx_dlss.dll") + + def test_host_platform(self): + with mock.patch.dict(os.environ, {"RX_TARGET": "windows"}): + self.assertEqual(ngx_sdk.host_platform(), "windows") + if os.name != "nt": + with mock.patch.dict(os.environ, {"RX_TARGET": ""}): + self.assertEqual(ngx_sdk.host_platform(), "linux") + + +class FetchTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="hookshot-ngx-sdk-") + self.addCleanup(self.temp.cleanup) + self.sdk = os.path.join(self.temp.name, "sdk") + self.good = {"lib/Windows_x86_64/x64/nvsdk_ngx_s.lib": b"lib", "LICENSE.txt": b"license", + "lib/Windows_x86_64/rel/nvngx_dlss.dll": b"runtime"} + self.table = [(p, digest(d), ngx_sdk.destination(p)) for p, d in self.good.items()] + + def test_fetch_checks_and_skips_what_is_there(self): + opener = Opener(self.good) + with mock.patch.object(ngx_sdk, "files", lambda platform, runtime=True: self.table): + ngx_sdk.fetch(self.sdk, "windows", opener, say=lambda *_: None) + self.assertEqual(sorted(opener.asked), sorted(self.good)) + self.assertEqual(Path(ngx_sdk.library(self.sdk, "windows")).read_bytes(), b"lib") + opener.asked.clear() + ngx_sdk.fetch(self.sdk, "windows", opener, say=lambda *_: None) # (all there and right: nothing downloaded) + self.assertEqual(opener.asked, []) + + def test_wrong_download_is_not_kept(self): + bad = dict(self.good, **{"lib/Windows_x86_64/x64/nvsdk_ngx_s.lib": b"not nvidia's"}) + with mock.patch.object(ngx_sdk, "files", lambda platform, runtime=True: self.table): + with self.assertRaisesRegex(ValueError, "nvsdk_ngx_s.lib is not the file expected"): + ngx_sdk.fetch(self.sdk, "windows", Opener(bad), say=lambda *_: None) + lib = ngx_sdk.library(self.sdk, "windows") + self.assertFalse(os.path.exists(lib)) + self.assertFalse(os.path.exists(lib + ".part")) + + def test_wrong_file_there_is_downloaded_again(self): + lib = ngx_sdk.library(self.sdk, "windows") + os.makedirs(os.path.dirname(lib)) + Path(lib).write_bytes(b"old") + path = "lib/Windows_x86_64/x64/nvsdk_ngx_s.lib" + self.assertTrue(ngx_sdk.fetch_one(path, digest(b"lib"), lib, Opener(self.good))) + self.assertEqual(Path(lib).read_bytes(), b"lib") + + def test_runtime_into_the_game_folder(self): + rt = ngx_sdk.runtime(self.sdk, "windows") + os.makedirs(os.path.dirname(rt)) + Path(rt).write_bytes(b"runtime") + game = os.path.join(self.temp.name, "game") + dst = ngx_sdk.install_runtime(self.sdk, "windows", os.path.join(game, "ngx")) + self.assertEqual(dst, os.path.join(game, "ngx", "nvngx_dlss.dll")) + self.assertEqual(Path(dst).read_bytes(), b"runtime") + self.assertEqual(os.listdir(game), ["ngx"]) # (nothing beside the exe) + + +class LicenseTests(unittest.TestCase): + def test_notice_names_nvidia_and_the_license(self): + self.assertIn("NVIDIA", ngx_sdk.NOTICE) + self.assertIn(ngx_sdk.LICENSE_URL, ngx_sdk.NOTICE) + self.assertIn("Never share", ngx_sdk.NOTICE) + + def test_flag_or_a_yes(self): + self.assertTrue(ngx_sdk.accepted(True, ask=lambda _: self.fail("asked"), interactive=False)) + self.assertFalse(ngx_sdk.accepted(False, ask=lambda _: self.fail("asked"), interactive=False)) + self.assertTrue(ngx_sdk.accepted(False, ask=lambda _: "yes", interactive=True)) + self.assertFalse(ngx_sdk.accepted(False, ask=lambda _: "", interactive=True)) + self.assertFalse(ngx_sdk.accepted(False, ask=lambda _: "no", interactive=True)) + + def test_nothing_downloaded_without_acceptance(self): + with mock.patch.object(ngx_sdk, "fetch", side_effect=AssertionError("fetched")), \ + mock.patch.object(ngx_sdk, "accepted", return_value=False), \ + mock.patch("sys.stdout", io.StringIO()), mock.patch("sys.stderr", io.StringIO()) as err: + self.assertEqual(ngx_sdk.main(["--platform", "windows", "x"]), 2) + self.assertIn("not accepted", err.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_oot_jitter.py b/tests/test_oot_jitter.py new file mode 100644 index 000000000..778d0ef86 --- /dev/null +++ b/tests/test_oot_jitter.py @@ -0,0 +1,137 @@ +"""nx2/oot_jitter.py, NVIDIA DLSS's camera jitter: its sites are the loads of a view's projection where the game fills the +views' uniform buffers, and the C after each adds the jitter to the loaded registers only (the third lane: P[0][2], P[1][2]), +never to the game's memory, where the shadow cascades' fit and culling would see it too (#841: shadows flickered). The +snippets are built with gcc against a stand-in context and run. No game or GPU needed. + +python -I -B tests/test_oot_jitter.py [--cc gcc] +""" +import argparse +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "nx2")) +import oot_jitter # noqa: E402 +import oot_wide # noqa: E402 + +CC = "gcc" + + +class Image: + def __init__(self, words): + self.words = words + + def word(self, pc): + return self.words[pc] + + +class Lifter: + def __init__(self): + self.after = {} + + +def lift(words): + L = Lifter() + n = oot_jitter.install(L, Image(words)) + return L, n + + +class SiteTests(unittest.TestCase): + def test_the_three_loads(self): + words = {oot_wide.MAIN + off: word for off, word, _, _ in oot_jitter.SITES} + L, n = lift(words) + self.assertEqual(n, 3) + self.assertEqual(sorted(L.after), sorted(words)) + for text in L.after.values(): + self.assertIn("hos_dlss_jitter_clip(c->x[28] + 0x1a8", text) + self.assertNotIn("recomp_store", text) # (the game's memory is left alone) + + def test_another_version_left_out(self): + words = {oot_wide.MAIN + off: word for off, word, _, _ in oot_jitter.SITES} + first = oot_wide.MAIN + oot_jitter.SITES[0][0] + words[first] ^= 1 + L, n = lift(words) + self.assertEqual(n, 2) + self.assertNotIn(first, L.after) + + def test_after_what_is_there(self): + words = {oot_wide.MAIN + off: word for off, word, _, _ in oot_jitter.SITES} + L = Lifter() + pc = oot_wide.MAIN + oot_jitter.SITES[1][0] + L.after[pc] = "/* before */" + oot_jitter.install(L, Image(words)) + self.assertTrue(L.after[pc].startswith("/* before */\n")) + + def test_registers_get_the_jitter_in_their_third_lane(self): + """Each snippet, built and run: the named registers' lane 2 (bytes 8..11) moves by the jitter's x or y, the rest of + every register stays, and with no jitter (hos_dlss_jitter_clip 0) nothing moves""" + cases = [] + for i, (_, _, xreg, yreg) in enumerate(oot_jitter.SITES): + cases.append("static void site%d(Ctx *c) { %s }" % (i, oot_jitter._text(xreg, yreg))) + runs = [] + for i, (_, _, xreg, yreg) in enumerate(oot_jitter.SITES): + for on in (1, 0): + runs.append("check(site%d, %d, %d, %d);" % (i, -1 if xreg is None else xreg, -1 if yreg is None else yreg, on)) + src = r""" +#include +#include +#include +typedef struct { uint64_t x[32]; uint64_t vreg[32][2]; } Ctx; +static int jitter_on; +static uint64_t asked; +int hos_dlss_jitter_clip(uint64_t projection, float out[2]) { + asked = projection; + if (!jitter_on) return 0; + out[0] = 0.25f, out[1] = -0.5f; + return 1; +} +%s +static int check(void (*site)(Ctx *), int xreg, int yreg, int on) { + Ctx c, before; + for (int r = 0; r < 32; r++) { + c.x[r] = 0x1000u * r; + const float lanes[4] = {r + 0.5f, r + 1.5f, 1.0f, r + 3.5f}; + memcpy(c.vreg[r], lanes, 16); + } + before = c; + jitter_on = on, asked = 0; + site(&c); + if (asked != 0x1000u * 28 + 0x1a8) { printf("projection asked at %%llx\n", (unsigned long long)asked); return 1; } + if (memcmp(c.x, before.x, sizeof c.x)) { puts("x registers changed"); return 1; } + for (int r = 0; r < 32; r++) { + float now[4], was[4]; + memcpy(now, c.vreg[r], 16), memcpy(was, before.vreg[r], 16); + const float want = !on ? 1.0f : r == xreg ? 1.25f : r == yreg ? 0.5f : 1.0f; + if (now[0] != was[0] || now[1] != was[1] || now[3] != was[3] || now[2] != want) { + printf("v%%d: %%g %%g %%g %%g (lane 2 wanted %%g)\n", r, now[0], now[1], now[2], now[3], want); + return 1; + } + } + return 0; +} +int main(void) { + int bad = 0; +#define check(...) bad |= check(__VA_ARGS__) + %s + return bad; +} +""" % ("\n".join(cases), "\n ".join(runs)) + with tempfile.TemporaryDirectory(prefix="oot-jitter-") as tmp: + c = os.path.join(tmp, "sites.c") + exe = os.path.join(tmp, "sites") + Path(c).write_text(src) + subprocess.run([CC, "-std=c11", "-Wall", "-Werror", "-Wno-unused-function", c, "-o", exe], check=True) + r = subprocess.run([exe], capture_output=True, text=True) + self.assertEqual(r.returncode, 0, r.stdout + r.stderr) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("--cc", default="gcc") + args, rest = ap.parse_known_args() + CC = args.cc + unittest.main(argv=[sys.argv[0]] + rest) diff --git a/tests/test_output_resolution.py b/tests/test_output_resolution.py index 92620438e..76f7e0f09 100644 --- a/tests/test_output_resolution.py +++ b/tests/test_output_resolution.py @@ -30,15 +30,23 @@ def main(): void hos_log(const char *, ...) {} volatile uint64_t hos_wide_out = 0; // (host/hos_main.c: the widescreen picture, 0 = none; nn_applet.c reads it) struct { int handheld, output_res; } opts; +namespace mkwvk { +int up_now = 0; // (the Upscaler shown: 0 the game's own, 1 DLSS, 2 DLAA) +int upscaler() { return up_now; } +} +#include "gpu/vk/upscaler_rows.h" +using namespace mkwvk; +namespace ur = upscaler_rows; ''' + host[host.index("static int ini_option("):host.index("HLE(_ZN2nn2oe16GetOperationModeEv)")] + menu[ - menu.index("struct Out {"):menu.index("const char* tx_speed(")] + r''' + menu.index("struct Out {"):menu.index("/// The Render resolution: the value chosen")] + menu[ + menu.index("const char* tx_output_res("):menu.index("const char* tx_speed(")] + r''' int main(int argc, char **argv) { assert(argc == 5); const int mode = atoi(argv[1]); const unsigned w = atoi(argv[2]), h = atoi(argv[3]); assert(output_mode() == mode); assert(hos_output_size(0) == w && hos_output_size(1) == h); - assert(hos_rt_pool_size(305ull << 20) == ((mode == 2 ? 687ull : 305ull) << 20)); + assert(hos_rt_pool_size(305ull << 20) == ((mode == 2 ? 687ull : 512ull) << 20)); // (512 MB at least: nn_applet.c) assert(kLastOutput == 3); assert(output_choice(-1) == 0 && output_choice(4) == 0); assert(step_output_choice(3, -1) == 3); @@ -54,22 +62,34 @@ def main(): assert(scene_w == (opts.handheld ? 1536 : mode ? w : 2048)); assert(scene_h == (opts.handheld ? 864 : mode ? h : 1152)); if (mode == 3) assert(strcmp(tx_resolution(2), "75% (1440x810)") == 0); + if (mode == 3) { // (DLSS at 1080p: its modes' render sizes, each a twelfth of the output, exact; DLAA the full size) + up_now = ur::DLSS; + assert(strcmp(tx_resolution(3), "Quality (67%)") == 0 && strcmp(tx_resolution(6), "Ultra Performance (33%)") == 0); + const unsigned want[][3] = {{3, 1280, 720}, {5, 1120, 630}, {4, 960, 540}, {6, 640, 360}}; + for (const auto& m : want) { + game_size(ur::percent(m[0]) / 100.0f, scene_w, scene_h); + assert(scene_w == m[1] && scene_h == m[2] && scene_w * 1080 == scene_h * 1920); + } + up_now = ur::DLAA; + assert(strcmp(tx_resolution(1), "Native (DLAA)") == 0); + up_now = ur::GAME; + } } ''' cases = ( - (0, 0, None, 0, 2560, 1440, "2048x1152 (game, restart)"), - (1, 0, None, 1, 2560, 1440, "2560x1440 (restart)"), - (2, 0, None, 2, 3840, 2160, "3840x2160 (restart)"), - (3, 0, None, 3, 1920, 1080, "1920x1080 (restart)"), - (1, 0, "3", 3, 1920, 1080, "1920x1080 (restart)"), - (3, 1, "3", 0, 2560, 1440, "2048x1152 (game, restart)"), - (4, 0, None, 0, 2560, 1440, "2048x1152 (game, restart)"), + (0, 0, None, 0, 2560, 1440, "2048x1152 (game's)"), + (1, 0, None, 1, 2560, 1440, "2560x1440"), + (2, 0, None, 2, 3840, 2160, "3840x2160"), + (3, 0, None, 3, 1920, 1080, "1920x1080"), + (1, 0, "3", 3, 1920, 1080, "1920x1080"), + (3, 1, "3", 0, 2560, 1440, "2048x1152 (game's)"), + (4, 0, None, 0, 2560, 1440, "2048x1152 (game's)"), ) with tempfile.TemporaryDirectory(prefix="output-resolution-") as tmp: work = Path(tmp) source, exe = work / "fixture.cpp", work / "fixture.exe" source.write_text(fixture, encoding="utf-8") - subprocess.run([args.cxx, "-std=c++17", str(source), "-o", str(exe)], check=True) + subprocess.run([args.cxx, "-std=c++17", "-I", str(ROOT), str(source), "-o", str(exe)], check=True) for saved, handheld, override, mode, w, h, label in cases: (work / "hookshot_options.ini").write_text(f"output_res={saved}\nhandheld_profile={handheld}\n", encoding="ascii") env = os.environ.copy() @@ -78,7 +98,7 @@ def main(): if override is not None: env["HOS_OUTPUT_RES"] = override subprocess.run([str(exe), str(mode), str(w), str(h), label], cwd=work, env=env, check=True) - print("PASS: output sizes, menu order, 1080p override, handheld and render-target budgets") + print("PASS: output sizes, menu order, 1080p override, handheld, render-target budgets and DLSS sizes at 1080p") if __name__ == "__main__": diff --git a/tests/test_rumble_menu.py b/tests/test_rumble_menu.py index 322238fd4..d8f0670c8 100644 --- a/tests/test_rumble_menu.py +++ b/tests/test_rumble_menu.py @@ -63,6 +63,17 @@ def one_line(text, head): void apply_pad_layout(int) {} void apply_preset(int) { presets++; } void sync_preset_budgets(int) {} +/* (stand-ins for the rest of set_row, which the Rumble strength row never reaches: the Upscaler's rows, the restart offer) */ +bool row_fixed(const int*) { return false; } +int upscaler() { return 0; } +namespace ur { +int step_upscaler(int, int, bool) { return 0; } +int step(int, int v, int dir) { return v + dir; } +int snap(int, int v) { return v; } +} +namespace mr { constexpr int kMax = 8; } +void restart_values(int*) {} +struct { void note(const int*) {} } restart_state; %(is_slider)s %(stops)s %(snap)s @@ -154,7 +165,7 @@ def menu_fixture(): "set_row": definition(menu, "void set_row"), "wheel": block(tick, loop), "arrow": "bool press[5] = {false, false, false, !right, right}; " + arrow.group(1), - "exempt": ", true" if exempt else "", + "exempt": ", nullptr, false, true" if exempt else "", # (after the help: no hint, no restart) "lo": row.group(1), "hi": row.group(2), "step": row.group(3), } @@ -162,7 +173,7 @@ def menu_fixture(): def option_ids(): """option 9 is the menu's Rumble strength, and no other option has its id""" replay, api = read("gpu", "vk", "vk_replay.cpp"), read("gpu", "vk", "vk_api.h") - value = {} # (every HOS_OPT_* enum: the cheats' and the motion rows') + value = {} # (every HOS_OPT_* enum: the cheats', the motion rows' and DLSS's jitter) for first, start, rest in re.findall(r"enum \{ (HOS_OPT_\w+) = (\d+), ([^}]*)\}", api): names = [first] + [n.strip() for n in rest.split(",") if n.strip()] value.update({n: int(start) + i for i, n in enumerate(names)}) diff --git a/tests/test_shader_stamp_cache.py b/tests/test_shader_stamp_cache.py index 7c0102b10..4cac5f14b 100644 --- a/tests/test_shader_stamp_cache.py +++ b/tests/test_shader_stamp_cache.py @@ -47,6 +47,9 @@ def setUp(self): "gpu/vk/vk_menu.cpp": "int menu;\n", "gpu/vk/vk_state.h": "struct State {};\n", "gpu/vk/vk_api.h": "struct Api {};\n", + "gpu/vk/vk_dlss.cpp": "int dlss;\n", + "gpu/vk/vk_ngx_msvc_rt.cpp": "int msvc_rt;\n", + "host/mingw_msvc_shims.c": "int shims;\n", "gpu/vk/eden/translate.cpp": "int translator;\n", "gpu/vk/eden/ir.h": "struct IR {};\n", "gpu/vk/sirit/src/sirit.cpp": "int sirit;\n", @@ -67,19 +70,21 @@ def setUp(self): path.write_bytes(text.encode()) self.code = production_code(BUILD_SCRIPT) - def jobs(self, linux, version="fixture", release=False): + def jobs(self, linux, version="fixture", release=False, ngx="", mingw="llvm-mingw"): namespace = { "os": os, "hashlib": hashlib, - "GPU": str(self.root / "gpu"), "POSIX": str(self.root / "host/posix"), + "GPU": str(self.root / "gpu"), "POSIX": str(self.root / "host/posix"), "HOST": str(self.root / "host"), "LINUX": linux, "CPU": ["-march=x86-64"], "kit": SimpleNamespace(ROOT=str(self.root), HERE=str(self.root / "nx2")), - "toolchain": SimpleNamespace(find=lambda name: name, need=lambda name: name), + "toolchain": SimpleNamespace(find=lambda name: name, need=lambda name: name, mingw=lambda: mingw), "target": lambda: ["--target=x86_64-linux-gnu" if linux else "--target=x86_64-w64-mingw32"], "cpm_package": lambda name: str(self.root / "deps" / name), "spirv_headers": str(self.root / "deps/spirv-headers"), "vk_headers": str(self.root / "deps/vulkanheaders"), "obj_dir": str(self.root / "obj"), "cc": "clang", "jobs": [], "stamp_def": '-DHOOKSHOT_VERSION="%s"' % version, "logo_text": b"", "gen_dir": str(self.root / "gen"), + "NGX_SDK": ngx, # ("": no NVIDIA NGX SDK, vk_dlss.cpp built without DLSS, as by default) + "NGX_MSVC_RT": "vk_ngx_msvc_rt.cpp", } with mock.patch.dict(os.environ, {"RX_RELEASE": "1"} if release else {}, clear=True), warnings.catch_warnings(): # The production key/header helpers use short-lived open(...).read() expressions. @@ -156,6 +161,61 @@ def test_stamp_version_and_release_flags_reach_their_consumers(self): self.assertEqual(self.changed(before, self.jobs(linux, version="new", release=True)), {"gpu/vk/vk_menu.cpp"}) + def test_ngx_sdk_reaches_only_vk_dlss_and_the_mingw_runtime_shims(self): + sdk = str(self.root / "ngx-sdk") + for linux, mingw in ((False, "llvm-mingw"), (False, ""), (True, "")): + with self.subTest(linux=linux, mingw=bool(mingw)): + plain = self.jobs(linux, mingw=mingw) + self.assertNotIn("gpu/vk/vk_ngx_msvc_rt.cpp", plain) # (never without the SDK) + self.assertNotIn("host/mingw_msvc_shims.c", plain) + self.assertFalse([n for n, j in plain.items() if "-DHOOKSHOT_NGX" in j["flags"]]) + with mock.patch("builtins.print"): + ngx = self.jobs(linux, ngx=sdk, mingw=mingw) + shims = {"gpu/vk/vk_ngx_msvc_rt.cpp", "host/mingw_msvc_shims.c"} + # (the shims only for llvm-mingw: the MSVC build (HOOKSHOT_MSVC=1) and Linux have their own runtime) + self.assertEqual(set(ngx) - set(plain), shims if mingw and not linux else set()) + self.assertEqual({n for n, j in ngx.items() if "-DHOOKSHOT_NGX" in j["flags"]}, + {"gpu/vk/vk_dlss.cpp"} | ({"gpu/vk/vk_ngx_msvc_rt.cpp"} if mingw and not linux else set())) + self.assertEqual(self.changed(plain, {n: ngx[n] for n in plain}), {"gpu/vk/vk_dlss.cpp"}) + + +class NgxSdkTests(unittest.TestCase): + """nx2/build.py's HOOKSHOT_NGX_SDK: which library each platform needs, and what the Windows link adds""" + def setUp(self): + tree = ast.parse(BUILD_SCRIPT.read_text(encoding="utf-8"), filename=str(BUILD_SCRIPT)) + names = {"ngx_library", "ngx_sdk", "ngx_link"} + functions = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in names] + self.assertEqual({n.name for n in functions}, names) + self.ns = {"os": os} + exec(compile(ast.Module(body=functions, type_ignores=[]), str(BUILD_SCRIPT), "exec"), self.ns) + self.temp = tempfile.TemporaryDirectory(prefix="hookshot-ngx-") + self.addCleanup(self.temp.cleanup) + self.sdk = Path(self.temp.name) + (self.sdk / "lib").mkdir() + + def quiet(self, sdk, linux): + with mock.patch("builtins.print"): + return self.ns["ngx_sdk"](sdk, linux) + + def test_each_platform_needs_its_own_library(self): + sdk = str(self.sdk) + self.assertEqual(self.quiet("", True), "") + self.assertEqual(self.quiet(sdk, True), "") + self.assertEqual(self.quiet(sdk, False), "") + (self.sdk / "lib" / "nvsdk_ngx_s.lib").write_bytes(b"") + self.assertEqual(self.quiet(sdk, False), sdk) # (Windows: the static /MT library) + self.assertEqual(self.quiet(sdk, True), "") + (self.sdk / "lib" / "libnvsdk_ngx.a").write_bytes(b"") + self.assertEqual(self.quiet(sdk, True), sdk) + + def test_windows_link(self): + sdk = str(self.sdk) + self.assertEqual(self.ns["ngx_link"]("", "llvm-mingw"), []) + self.assertEqual(self.ns["ngx_link"]("", ""), []) + lib = os.path.join(sdk, "lib", "nvsdk_ngx_s.lib") + self.assertEqual(self.ns["ngx_link"](sdk, "llvm-mingw"), [lib, "-ladvapi32", "-Wl,-Xlink=/nodefaultlib:libcpmt"]) + self.assertEqual(self.ns["ngx_link"](sdk, ""), [lib, "-ladvapi32"]) # (MSVC: its own runtime, nothing left out) + if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) diff --git a/tests/test_status_line.cpp b/tests/test_status_line.cpp new file mode 100644 index 000000000..31dc692f3 --- /dev/null +++ b/tests/test_status_line.cpp @@ -0,0 +1,39 @@ +// tests/test_status_line.py: gpu/vk/status_line.h, the renderer's status line and its video-memory warning +#include "../gpu/vk/status_line.h" + +#include +#include + +#define CHECK(name, condition) do { if (!(condition)) { std::fprintf(stderr, "FAIL %s at line %d\n", name, __LINE__); return 1; } } while (0) + +int main() { + namespace sl = mkwvk::status_line; + constexpr uint64_t GB = 1ull << 30; + char b[256]; + sl::vram_text(b, sizeof b, 7 * GB + GB / 10, 8 * GB, true); + CHECK("used of allowed", !std::strcmp(b, "VRAM 7.1 of 8.0 GB")); + sl::vram_text(b, sizeof b, 3 * GB, 0, false); + CHECK("no budget", !std::strcmp(b, "VRAM 3.0 GB in the game's allocations (no budget from the driver)")); + sl::vram_text(b, sizeof b, 3 * GB, 6 * GB, false); + CHECK("no budget, VMA's guess not shown", std::strstr(b, "no budget") && !std::strstr(b, "of 6.0")); + + // the warning: once when the use passes the allowance, again only after a reading back under it + bool over = false; + CHECK("under", !sl::crossed(over, 7 * GB, 8 * GB) && !over); + CHECK("at the allowance is not over", !sl::crossed(over, 8 * GB, 8 * GB)); + CHECK("over: told", sl::crossed(over, 8 * GB + 1, 8 * GB) && over); + CHECK("still over: not again", !sl::crossed(over, 9 * GB, 8 * GB) && over); + CHECK("back under", !sl::crossed(over, 7 * GB, 8 * GB) && !over); + CHECK("over again: told again", sl::crossed(over, 9 * GB, 8 * GB)); + bool unknown = false; + CHECK("nothing known is never over", !sl::crossed(unknown, 9 * GB, 0) && !unknown); + + sl::over_text(b, sizeof b, 8 * GB + 3 * GB / 10, 8 * GB, 2); + CHECK("DLAA's warning", !std::strcmp(b, "vk: over the driver's video-memory allowance (8.3 of 8.0 GB): DLAA may stutter here; " + "Quality or Balanced uses less")); + sl::over_text(b, sizeof b, 8 * GB + 3 * GB / 10, 8 * GB, 1); + CHECK("DLSS's warning", std::strstr(b, ": DLSS may stutter here; a lower Render resolution mode uses less")); + sl::over_text(b, sizeof b, 8 * GB + 3 * GB / 10, 8 * GB, 0); + CHECK("the game's own: no advice", !std::strcmp(b, "vk: over the driver's video-memory allowance (8.3 of 8.0 GB)")); + std::puts("status line: all checks passed"); +} diff --git a/tests/test_status_line.py b/tests/test_status_line.py new file mode 100644 index 000000000..4a82d9fc7 --- /dev/null +++ b/tests/test_status_line.py @@ -0,0 +1,41 @@ +"""The renderer's status line (gpu/vk/status_line.h, vk_replay.cpp status_tick): its video-memory text, the warning past +the driver's allowance (once, again only after dropping back under) and its advice for DLSS and DLAA; and that the +renderer uses them: one line every 600 frames, DLSS's details in it rather than a line of their own. No game or GPU. + +python -I -B tests/test_status_line.py [--cxx g++] +""" +import argparse +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="g++") + args = parser.parse_args() + vk = ROOT / "gpu" / "vk" + replay = (vk / "vk_replay.cpp").read_text(encoding="utf-8") + tick = replay[replay.index("void status_tick() {"):] + tick = tick[:tick.index("\n}\n")] + for use in ("vram_usage_now(", "sl::crossed(", "sl::over_text(", "sl::vram_text(", "dlss_status(", "sl::kEvery"): + if use not in tick: + raise SystemExit("status_tick does not use %s" % use) + present = replay[replay.index("void do_present("):] + if "status_tick();" not in present[:present.index("\n}\n")]: + raise SystemExit("do_present does not call status_tick: no status line") + dlss = (vk / "vk_dlss.cpp").read_text(encoding="utf-8") + if "frames drawn; this one's jitter" in dlss: + raise SystemExit("vk_dlss.cpp still logs DLSS's own 600-frame line beside the status line") + with tempfile.TemporaryDirectory(prefix="status-line-") as tmp: + exe = Path(tmp) / "status-line" + subprocess.run([args.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(ROOT / "tests" / "test_status_line.cpp"), + "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + print("PASS: the status line and its video-memory warning") + + +if __name__ == "__main__": + main() diff --git a/tests/test_upscaler_rows.cpp b/tests/test_upscaler_rows.cpp new file mode 100644 index 000000000..a12a55328 --- /dev/null +++ b/tests/test_upscaler_rows.cpp @@ -0,0 +1,78 @@ +#include "../gpu/vk/upscaler_rows.h" + +#include +#include + +#define CHECK(name, condition) do { if (!(condition)) { std::fprintf(stderr, "FAIL %s at line %d\n", name, __LINE__); return 1; } } while (0) + +int main() { + namespace ur = mkwvk::upscaler_rows; + using ur::DLAA; + using ur::DLSS; + using ur::GAME; + // (the values saved before keep their shares; the new ones are DLSS's ratios, each a twelfth of the output) + CHECK("saved values keep their shares", ur::percent(0) == 0 && ur::percent(1) == 100 && ur::percent(2) == 75 && + ur::percent(3) == 67 && ur::percent(4) == 50); + CHECK("Balanced and Ultra Performance", ur::percent(5) == 58 && ur::percent(6) == 33); + CHECK("a value past the row is Auto", ur::percent(7) == 0 && ur::percent(-1) == 0); + for (int v = 1; v < ur::kValues; v++) { + const int k = (int)(ur::percent(v) * 12 / 100.0 + 0.5); + CHECK("each share is a twelfth", k * 100 / 12 == ur::percent(v) || (k * 100 + 11) / 12 == ur::percent(v)); + } + CHECK("DLSS's mode names", !std::strcmp(ur::dlss_mode_name(3), "Quality") && !std::strcmp(ur::dlss_mode_name(5), "Balanced") && + !std::strcmp(ur::dlss_mode_name(4), "Performance") && + !std::strcmp(ur::dlss_mode_name(6), "Ultra Performance")); + CHECK("no mode name for Auto, 100% or 75%", !ur::dlss_mode_name(0) && !ur::dlss_mode_name(1) && !ur::dlss_mode_name(2)); + + // the game's own: Auto, 100, 75, 67, 50, as before + CHECK("game: Right", ur::step(GAME, 0, 1) == 1 && ur::step(GAME, 1, 1) == 2 && ur::step(GAME, 3, 1) == 4 && ur::step(GAME, 4, 1) == 0); + CHECK("game: Left", ur::step(GAME, 0, -1) == 4 && ur::step(GAME, 1, -1) == 0 && ur::step(GAME, 4, -1) == 3); + // DLSS: Auto, Quality 67, Balanced 58, Performance 50, Ultra Performance 33 + CHECK("dlss: Right", ur::step(DLSS, 0, 1) == 3 && ur::step(DLSS, 3, 1) == 5 && ur::step(DLSS, 5, 1) == 4 && + ur::step(DLSS, 4, 1) == 6 && ur::step(DLSS, 6, 1) == 0); + CHECK("dlss: Left", ur::step(DLSS, 0, -1) == 6 && ur::step(DLSS, 3, -1) == 0 && ur::step(DLSS, 6, -1) == 4); + // a value that is not a step (saved with the other upscaler): its neighbours by size + CHECK("dlss from 75%", ur::step(DLSS, 2, 1) == 3 && ur::step(DLSS, 2, -1) == 0); + CHECK("dlss from 100%", ur::step(DLSS, 1, 1) == 3 && ur::step(DLSS, 1, -1) == 0); + CHECK("game from 58%", ur::step(GAME, 5, 1) == 4 && ur::step(GAME, 5, -1) == 3); + CHECK("game from 33%", ur::step(GAME, 6, 1) == 0 && ur::step(GAME, 6, -1) == 4); + CHECK("DLAA does not step", ur::step(DLAA, 3, 1) == 3 && ur::step(DLAA, 0, -1) == 0); + + // another upscaler: the nearest step (the larger on a tie), Auto stays Auto + CHECK("snap to DLSS", ur::snap(DLSS, 0) == 0 && ur::snap(DLSS, 1) == 3 && ur::snap(DLSS, 2) == 3 && ur::snap(DLSS, 3) == 3 && + ur::snap(DLSS, 4) == 4); + CHECK("snap to the game's", ur::snap(GAME, 5) == 4 && ur::snap(GAME, 6) == 4 && ur::snap(GAME, 3) == 3 && ur::snap(GAME, 1) == 1); + CHECK("DLAA keeps the value", ur::snap(DLAA, 5) == 5 && ur::snap(DLAA, 1) == 1); + CHECK("a value past the row snaps to Auto", ur::snap(GAME, 9) == 0 && ur::snap(DLSS, -3) == 0); + + // the Upscaler row: all three where DLSS runs, the game's own alone elsewhere + CHECK("upscaler with DLSS", ur::step_upscaler(0, 1, true) == 1 && ur::step_upscaler(1, 1, true) == 2 && + ur::step_upscaler(2, 1, true) == 0 && ur::step_upscaler(0, -1, true) == 2); + CHECK("upscaler without DLSS", ur::step_upscaler(0, 1, false) == 0 && ur::step_upscaler(2, -1, false) == 0); + CHECK("upscaler from a bad value", ur::step_upscaler(7, 1, true) == 1); + + // the Upscaler row only in a build with DLSS; the rows with nothing to choose + CHECK("upscaler row shown", ur::upscaler_shown(true) && !ur::upscaler_shown(false)); + CHECK("upscaler fixed where DLSS cannot run", ur::upscaler_fixed(false) && !ur::upscaler_fixed(true)); + CHECK("render resolution fixed with DLAA only", ur::resolution_fixed(DLAA) && !ur::resolution_fixed(DLSS) && + !ur::resolution_fixed(GAME)); + // the Window scaling filter: needed only when the window is not the picture's size (unknown: needed) + CHECK("filter not needed at the output's size", !ur::filter_needed(2560, 1440, 2560, 1440)); + CHECK("filter needed otherwise", ur::filter_needed(2048, 1152, 2560, 1440) && ur::filter_needed(2560, 1440, 2560, 1080) && + ur::filter_needed(0, 0, 0, 0) && ur::filter_needed(2560, 1440, 0, 0)); + // the DLSS model row: its presets (NVIDIA's numbers, K = 11), RX_VK_DLSS_PRESET over it, and when it applies + CHECK("model presets", ur::model_preset(ur::MODEL_K) == 11 && ur::model_preset(ur::MODEL_M) == 13 && + ur::model_preset(ur::MODEL_L) == 12 && ur::model_preset(ur::MODEL_DEFAULT) == 0); + CHECK("model: a value past the row is K", ur::model_preset(-1) == 11 && ur::model_preset(ur::kModels) == 11); + CHECK("model without the switch", ur::dlss_preset(ur::MODEL_M, nullptr) == 13 && ur::dlss_preset(ur::MODEL_DEFAULT, nullptr) == 0); + CHECK("RX_VK_DLSS_PRESET wins", ur::dlss_preset(ur::MODEL_M, "K") == 11 && ur::dlss_preset(ur::MODEL_K, "m") == 13 && + ur::dlss_preset(ur::MODEL_K, "J") == 10 && ur::dlss_preset(ur::MODEL_M, "0") == 0); + CHECK("RX_VK_DLSS_PRESET not a preset", !ur::preset_env(nullptr) && !ur::preset_env("") && !ur::preset_env("1") && + !ur::preset_env("05") && ur::dlss_preset(ur::MODEL_M, "7") == 13 && + ur::dlss_preset(ur::MODEL_L, "") == 12); + CHECK("preset letters", ur::preset_letter(11) == 'K' && ur::preset_letter(13) == 'M' && ur::preset_letter(0) == '0'); + CHECK("model row shown with DLSS built", ur::model_shown(true) && !ur::model_shown(false)); + CHECK("model row fixed with the game's own or the switch", ur::model_fixed(GAME, false) && !ur::model_fixed(DLSS, false) && + !ur::model_fixed(DLAA, false) && ur::model_fixed(DLSS, true)); + std::puts("upscaler rows: all checks passed"); +} diff --git a/tests/test_upscaler_rows.py b/tests/test_upscaler_rows.py new file mode 100644 index 000000000..78ceff627 --- /dev/null +++ b/tests/test_upscaler_rows.py @@ -0,0 +1,105 @@ +"""The Insert menu's scaling rows: the Upscaler (opts.dlss) and the Render resolution's steps for it (gpu/vk/upscaler_rows.h), +their order on the Graphics tab, options files from before still loading, and their help (why DLSS is not offered, the +Sharpness DLSS wants). No game or GPU required. + +python -I -B tests/test_upscaler_rows.py [--cxx g++] +""" +import argparse +import re +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def function(source, start): + """A function's body from its first line to its closing brace at the start of a line""" + at = source.find(start) + if at < 0: + raise SystemExit("%s is missing" % start.rstrip(" {(")) + body = source[at:] + return body[:body.index("\n}\n")] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="g++") + args = parser.parse_args() + menu = (ROOT / "gpu" / "vk" / "vk_menu.cpp").read_text(encoding="utf-8") + # the picture's way, top to bottom; Reflex with the frame rows + order = ['"Frame generation", &opts.framegen', '"Frame pacing", &opts.pace', '"NVIDIA Reflex", &opts.reflex', + '"Output resolution", &opts.output_res', '"Upscaler", &opts.dlss', '"DLSS model", &opts.dlss_model', + '"Render resolution", &opts.resolution', + '"Window scaling filter", &opts.upscale', '"Sharpness", &opts.sharpness'] + at = [menu.find(row) for row in order] + if -1 in at: + raise SystemExit("vk_menu.cpp: a scaling row is missing: " + order[at.index(-1)]) + if at != sorted(at): + raise SystemExit("vk_menu.cpp: the scaling rows are not in the picture's order") + if '"NVIDIA DLSS", &opts.dlss' in menu: + raise SystemExit("vk_menu.cpp: DLSS still has a row of its own") + # saved options: the renamed rows keep their keys (data, so read from the source) + key = function(menu, "const char* ini_key(const Row& r) {") + for value, name in (("opts.resolution", "resolution"), ("opts.upscale", "upscaling"), ("opts.dlss", "dlss")): + if not re.search(r"&%s\)\s*return\s*\"%s\"" % (re.escape(value), name), key): + raise SystemExit("ini_key: %s is not saved as %s (an options file from before would lose it)" % (value, name)) + # the menu's rules are upscaler_rows.h's, checked by behaviour in test_upscaler_rows.cpp: here only that the menu uses them + uses = { + "row_fixed": ("ur::upscaler_fixed(", "ur::resolution_fixed(", "reflex_available()", "ur::model_fixed("), + "row_shown": ("ur::upscaler_shown(dlss_built())", "ur::model_shown(dlss_built())"), + "set_row": ("ur::step_upscaler(", "ur::step(", "ur::snap(", "row_fixed("), + } + for name, calls in uses.items(): + body = function(menu, "%s %s(" % ("void" if name == "set_row" else "bool", name)) + for call in calls: + if call not in body: + raise SystemExit("%s does not use %s (upscaler_rows.h)" % (name, call)) + # the help: a row's own, then a line for the value chosen (Row::hint). The Upscaler's says why DLSS is not offered, and + # DLSS's values ask for more Sharpness (it looks softer than the game's own) + upscaler = function(menu, "const char* hn_upscaler(int v, int variant) {") + for why in ("not in this build", "RX_VK_DLSS=0", "NVIDIA RTX GPU", "newer NVIDIA driver", "runtime was not found", + "(int)dlss_why()"): + if why not in upscaler: + raise SystemExit("hn_upscaler: the help no longer says why DLSS is not offered (%s)" % why) + if upscaler.count("Sharpness to") < 2: + raise SystemExit("hn_upscaler: NVIDIA DLSS and DLAA do not say to set Sharpness to around 80%") + resolution = function(menu, "const char* hn_resolution(int v, int variant) {") + if resolution.count("Sharpness to around 80%") < 5 or "upscaler()" not in resolution: + raise SystemExit("hn_resolution: DLSS's modes do not say to set Sharpness to around 80%, or not for the Upscaler shown") + measure = function(menu, "int measure_help(int s) {") + if "r.hint(v, k)" not in measure: + raise SystemExit("measure_help leaves the values' lines out: the help's room would jump as a value changes") + for row, hint in (('"Upscaler", &opts.dlss', "hn_upscaler"), ('"DLSS model", &opts.dlss_model', "hn_dlss_model"), ('"Render resolution", &opts.resolution', "hn_resolution"), + ('"NVIDIA Reflex", &opts.reflex', "hn_reflex"), ('"Window scaling filter", &opts.upscale', "hn_filter")): + entry = menu[menu.index(row):] + if hint not in entry[:entry.index("},")]: + raise SystemExit("vk_menu.cpp: %s has no %s" % (row.split(",")[0], hint)) + # the DLSS model row: not the Quality preset's (preset_exempt), no restart, and its preset is what DLSS is made with + model = menu[menu.index('"DLSS model", &opts.dlss_model'):] + if not model[:model.index("},")].rstrip().endswith("hn_dlss_model, false, true"): + raise SystemExit("vk_menu.cpp: the DLSS model row takes a restart or turns the Quality preset to Custom") + # the Window scaling filter's sizes: only from the game's frames. The start-up screen is drawn at the window's size + # (present_image with no source): taken as the picture's, the row read "Not needed" while the output was another size + replay = (ROOT / "gpu" / "vk" / "vk_replay.cpp").read_text(encoding="utf-8") + present = function(replay, "void present_image(") + note = present.index("menu_note_present_sizes(") + if "if (src) {" not in present[:note] or present.rindex("if (src) {", 0, note) < present.rindex("\n }", 0, note): + raise SystemExit("present_image: the start-up screen's frames (no src) count as the game's picture for the filter row") + dlss = (ROOT / "gpu" / "vk" / "vk_dlss.cpp").read_text(encoding="utf-8") + feature = function(dlss, "bool make_feature(") + if "wanted_preset()" not in feature or "s.feature_preset == preset" not in feature: + raise SystemExit("vk_dlss.cpp: DLSS's feature is not made again for another DLSS model") + if "upscaler_rows::dlss_preset(opts.dlss_model" not in function(dlss, "unsigned wanted_preset() {"): + raise SystemExit("vk_dlss.cpp: the DLSS model row (and RX_VK_DLSS_PRESET over it) is not the preset asked for") + if "dlss_why() != DLSS_OK" not in function(dlss, "int dlss_mode() {"): + raise SystemExit("dlss_mode asks for DLSS where it cannot run (DLAA would hold the 3D at full scale for nothing)") + with tempfile.TemporaryDirectory(prefix="upscaler-rows-") as tmp: + exe = Path(tmp) / "upscaler-rows" + subprocess.run([args.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", + str(ROOT / "tests" / "test_upscaler_rows.cpp"), "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + + +if __name__ == "__main__": + main()