From c17fbd61c109d7bfad6d544517002ac430c7d7df Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 12:31:53 -0500 Subject: [PATCH 01/52] vk: NVIDIA DLSS Super Resolution in place of OoT's gameplay upscale (Linux, opt-in build) OoT scales its scene up by 4/3 with a network of its own (fd029a317a9a4b94). With the new Insert menu row NVIDIA DLSS on (or RX_VK_DLSS=1), NVIDIA's DLSS draws that output instead, through NGX on Vulkan: - the scene camera comes from the draws into the scene's colour target (the vertex stage's uniform buffer 3: the view-projection at 0x30, the projection at 0x70, the target's size at 0x120), the depth from their depth target - shaders/dlss_prep.comp makes DLSS's inputs: the network's input colour (LDR, the square root of the scene's), the depth, and motion vectors from the depth reprojected through this camera and the one DLSS last drew with (camera motion only: what moves by itself can ghost) - shaders/dlss_out.comp squares DLSS's output into the network's output Anything unexpected leaves the network to run, the reason in the log. NVIDIA's SDK is not part of Hookshot: scripts/linux/ngx-sdk.sh fetches its headers and libnvsdk_ngx.a (and the DLSS runtime, libnvidia-ngx-dlss.so) from NVIDIA's DLSS repository at a pinned release, checked by SHA-256, and a build with HOOKSHOT_NGX_SDK= compiles vk_dlss.cpp with it. Without it the menu row says the build has no DLSS. --- gpu/vk/shaders/dlss_out.comp | 14 + gpu/vk/shaders/dlss_out_comp_spv.h | 61 +++ gpu/vk/shaders/dlss_prep.comp | 46 +++ gpu/vk/shaders/dlss_prep_comp_spv.h | 159 ++++++++ gpu/vk/vk_dlss.cpp | 608 ++++++++++++++++++++++++++++ gpu/vk/vk_dlss.h | 34 ++ gpu/vk/vk_draw.cpp | 24 ++ gpu/vk/vk_main.cpp | 14 +- gpu/vk/vk_menu.cpp | 5 + gpu/vk/vk_menu.h | 1 + nx2/build.py | 20 +- scripts/linux/ngx-sdk.sh | 69 ++++ 12 files changed, 1051 insertions(+), 4 deletions(-) create mode 100644 gpu/vk/shaders/dlss_out.comp create mode 100644 gpu/vk/shaders/dlss_out_comp_spv.h create mode 100644 gpu/vk/shaders/dlss_prep.comp create mode 100644 gpu/vk/shaders/dlss_prep_comp_spv.h create mode 100644 gpu/vk/vk_dlss.cpp create mode 100644 gpu/vk/vk_dlss.h create mode 100755 scripts/linux/ngx-sdk.sh diff --git a/gpu/vk/shaders/dlss_out.comp b/gpu/vk/shaders/dlss_out.comp new file mode 100644 index 000000000..32618c76a --- /dev/null +++ b/gpu/vk/shaders/dlss_out.comp @@ -0,0 +1,14 @@ +#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). pc.square = 0: as it is. +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; int square; } pc; +void main() { + const ivec2 g = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(g, pc.size))) return; + vec3 c = max(texelFetch(src, g, 0).rgb, vec3(0.0)); + if (pc.square != 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..ee01ec5cc --- /dev/null +++ b/gpu/vk/shaders/dlss_out_comp_spv.h @@ -0,0 +1,61 @@ + // 1116.6.0 + #pragma once +const uint32_t DLSS_OUT_COMP_SPV[] = { + 0x07230203,0x00010600,0x0008000b,0x0000004a,0x00000000,0x00020011,0x00000001,0x00020011, + 0x00000031,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e, + 0x00000000,0x00000001,0x0009000f,0x00000005,0x00000004,0x6e69616d,0x00000000,0x0000000d, + 0x00000015,0x00000028,0x0000003e,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,0x00050006,0x00000013,0x00000001,0x61757173,0x00006572, + 0x00030005,0x00000015,0x00006370,0x00030005,0x00000024,0x00000063,0x00030005,0x00000028, + 0x00637273,0x00030005,0x0000003e,0x00747364,0x00040047,0x0000000d,0x0000000b,0x0000001c, + 0x00030047,0x00000013,0x00000002,0x00050048,0x00000013,0x00000000,0x00000023,0x00000000, + 0x00050048,0x00000013,0x00000001,0x00000023,0x00000008,0x00040047,0x00000028,0x00000021, + 0x00000000,0x00040047,0x00000028,0x00000022,0x00000000,0x00030047,0x0000003e,0x00000019, + 0x00040047,0x0000003e,0x00000021,0x00000001,0x00040047,0x0000003e,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,0x0004001e,0x00000013,0x00000007, + 0x00000006,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,0x0000002c, + 0x00000021,0x00000004,0x0004002b,0x00000021,0x0000002f,0x00000000,0x0006002c,0x00000022, + 0x00000030,0x0000002f,0x0000002f,0x0000002f,0x0004002b,0x00000006,0x00000032,0x00000001, + 0x00040020,0x00000033,0x00000009,0x00000006,0x00090019,0x0000003c,0x00000021,0x00000001, + 0x00000000,0x00000000,0x00000000,0x00000002,0x00000008,0x00040020,0x0000003d,0x00000000, + 0x0000003c,0x0004003b,0x0000003d,0x0000003e,0x00000000,0x0004002b,0x00000021,0x00000042, + 0x3f800000,0x0004002b,0x0000000a,0x00000047,0x00000008,0x0004002b,0x0000000a,0x00000048, + 0x00000001,0x0006002c,0x0000000b,0x00000049,0x00000047,0x00000047,0x00000048,0x00050036, + 0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008, + 0x00000009,0x00000007,0x0004003b,0x00000023,0x00000024,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,0x0004003d,0x00000007, + 0x0000002a,0x00000009,0x00040064,0x00000025,0x0000002b,0x00000029,0x0007005f,0x0000002c, + 0x0000002d,0x0000002b,0x0000002a,0x00000002,0x00000016,0x0008004f,0x00000022,0x0000002e, + 0x0000002d,0x0000002d,0x00000000,0x00000001,0x00000002,0x0007000c,0x00000022,0x00000031, + 0x00000001,0x00000028,0x0000002e,0x00000030,0x0003003e,0x00000024,0x00000031,0x00050041, + 0x00000033,0x00000034,0x00000015,0x00000032,0x0004003d,0x00000006,0x00000035,0x00000034, + 0x000500ab,0x0000001a,0x00000036,0x00000035,0x00000016,0x000300f7,0x00000038,0x00000000, + 0x000400fa,0x00000036,0x00000037,0x00000038,0x000200f8,0x00000037,0x0004003d,0x00000022, + 0x00000039,0x00000024,0x0004003d,0x00000022,0x0000003a,0x00000024,0x00050085,0x00000022, + 0x0000003b,0x0000003a,0x00000039,0x0003003e,0x00000024,0x0000003b,0x000200f9,0x00000038, + 0x000200f8,0x00000038,0x0004003d,0x0000003c,0x0000003f,0x0000003e,0x0004003d,0x00000007, + 0x00000040,0x00000009,0x0004003d,0x00000022,0x00000041,0x00000024,0x00050051,0x00000021, + 0x00000043,0x00000041,0x00000000,0x00050051,0x00000021,0x00000044,0x00000041,0x00000001, + 0x00050051,0x00000021,0x00000045,0x00000041,0x00000002,0x00070050,0x0000002c,0x00000046, + 0x00000043,0x00000044,0x00000045,0x00000042,0x00040063,0x0000003f,0x00000040,0x00000046, + 0x000100fd,0x00010038 +}; diff --git a/gpu/vk/shaders/dlss_prep.comp b/gpu/vk/shaders/dlss_prep.comp new file mode 100644 index 000000000..665ab65b5 --- /dev/null +++ b/gpu/vk/shaders/dlss_prep.comp @@ -0,0 +1,46 @@ +#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 (Link, the +// others, water, particles) gets the motion of what is behind it. pc.flags bit 0: motion (none on the first frame); +// bit 1: the motion shown as the colour instead (RX_VK_DLSS_DEBUG=motion) +// 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 writeonly 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; + 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..4a54c7faa --- /dev/null +++ b/gpu/vk/shaders/dlss_prep_comp_spv.h @@ -0,0 +1,159 @@ + // 1116.6.0 + #pragma once +const uint32_t DLSS_PREP_COMP_SPV[] = { + 0x07230203,0x00010600,0x0008000b,0x000000cf,0x00000000,0x00020011,0x00000001,0x00020011, + 0x00000031,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e, + 0x00000000,0x00000001,0x000c000f,0x00000005,0x00000004,0x6e69616d,0x00000000,0x0000000d, + 0x00000019,0x00000038,0x00000040,0x0000004e,0x00000056,0x000000b2,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,0x00000034,0x00000063,0x00040005,0x00000038,0x6e656373,0x00000065,0x00040005, + 0x00000040,0x6f6c6f63,0x00000072,0x00030005,0x0000004d,0x00000064,0x00050005,0x0000004e, + 0x6e656373,0x65645f65,0x00687470,0x00040005,0x00000056,0x74706564,0x00000068,0x00030005, + 0x0000005b,0x0000766d,0x00040005,0x0000006e,0x70696c63,0x00000000,0x00040005,0x0000007d, + 0x6f666562,0x00006572,0x00040005,0x0000008a,0x6e656874,0x00000000,0x00030005,0x00000091, + 0x00776f6e,0x00030005,0x00000098,0x00007870,0x00040005,0x000000b2,0x69746f6d,0x00006e6f, + 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,0x00000038,0x00000021,0x00000000, + 0x00040047,0x00000038,0x00000022,0x00000000,0x00030047,0x00000040,0x00000019,0x00040047, + 0x00000040,0x00000021,0x00000002,0x00040047,0x00000040,0x00000022,0x00000000,0x00040047, + 0x0000004e,0x00000021,0x00000001,0x00040047,0x0000004e,0x00000022,0x00000000,0x00030047, + 0x00000056,0x00000019,0x00040047,0x00000056,0x00000021,0x00000003,0x00040047,0x00000056, + 0x00000022,0x00000000,0x00030047,0x000000b2,0x00000019,0x00040047,0x000000b2,0x00000021, + 0x00000004,0x00040047,0x000000b2,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,0x00040017,0x00000032,0x00000013, + 0x00000003,0x00040020,0x00000033,0x00000007,0x00000032,0x00090019,0x00000035,0x00000013, + 0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,0x00000000,0x0003001b,0x00000036, + 0x00000035,0x00040020,0x00000037,0x00000000,0x00000036,0x0004003b,0x00000037,0x00000038, + 0x00000000,0x0004002b,0x00000013,0x0000003b,0x00000000,0x00090019,0x0000003e,0x00000013, + 0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000002,0x00040020,0x0000003f, + 0x00000000,0x0000003e,0x0004003b,0x0000003f,0x00000040,0x00000000,0x0004002b,0x00000013, + 0x00000044,0x3f800000,0x00040020,0x0000004c,0x00000007,0x00000013,0x0004003b,0x00000037, + 0x0000004e,0x00000000,0x0004002b,0x0000000a,0x00000052,0x00000000,0x00090019,0x00000054, + 0x00000013,0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000003,0x00040020, + 0x00000055,0x00000000,0x00000054,0x0004003b,0x00000055,0x00000056,0x00000000,0x0005002c, + 0x00000016,0x0000005c,0x0000003b,0x0000003b,0x0004002b,0x00000006,0x0000005d,0x00000003, + 0x00040020,0x0000005e,0x00000009,0x00000006,0x0004002b,0x00000006,0x00000061,0x00000001, + 0x0004002b,0x00000006,0x00000063,0x00000000,0x00040020,0x0000006d,0x00000007,0x00000014, + 0x0004002b,0x00000013,0x00000071,0x40000000,0x0004002b,0x0000000a,0x00000074,0x00000001, + 0x00040020,0x0000007e,0x00000009,0x00000015,0x0004002b,0x0000000a,0x00000083,0x00000003, + 0x0004002b,0x00000013,0x00000086,0x358637bd,0x00040020,0x00000094,0x00000009,0x00000016, + 0x0004002b,0x00000013,0x000000a9,0x46800000,0x0005002c,0x00000016,0x000000aa,0x000000a9, + 0x000000a9,0x00090019,0x000000b0,0x00000013,0x00000001,0x00000000,0x00000000,0x00000000, + 0x00000002,0x00000007,0x00040020,0x000000b1,0x00000000,0x000000b0,0x0004003b,0x000000b1, + 0x000000b2,0x00000000,0x0004002b,0x00000013,0x000000c3,0x41800000,0x0004002b,0x0000000a, + 0x000000cd,0x00000008,0x0006002c,0x0000000b,0x000000ce,0x000000cd,0x000000cd,0x00000074, + 0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b, + 0x00000008,0x00000009,0x00000007,0x0004003b,0x00000025,0x00000026,0x00000007,0x0004003b, + 0x00000025,0x0000002a,0x00000007,0x0004003b,0x00000033,0x00000034,0x00000007,0x0004003b, + 0x0000004c,0x0000004d,0x00000007,0x0004003b,0x00000025,0x0000005b,0x00000007,0x0004003b, + 0x0000006d,0x0000006e,0x00000007,0x0004003b,0x0000006d,0x0000007d,0x00000007,0x0004003b, + 0x00000025,0x0000008a,0x00000007,0x0004003b,0x00000025,0x00000091,0x00000007,0x0004003b, + 0x00000025,0x00000098,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,0x0004003d,0x00000036,0x00000039, + 0x00000038,0x0004003d,0x00000016,0x0000003a,0x0000002a,0x00070058,0x00000014,0x0000003c, + 0x00000039,0x0000003a,0x00000002,0x0000003b,0x0008004f,0x00000032,0x0000003d,0x0000003c, + 0x0000003c,0x00000000,0x00000001,0x00000002,0x0003003e,0x00000034,0x0000003d,0x0004003d, + 0x0000003e,0x00000041,0x00000040,0x0004003d,0x00000007,0x00000042,0x00000009,0x0004003d, + 0x00000032,0x00000043,0x00000034,0x00060050,0x00000032,0x00000045,0x0000003b,0x0000003b, + 0x0000003b,0x00060050,0x00000032,0x00000046,0x00000044,0x00000044,0x00000044,0x0008000c, + 0x00000032,0x00000047,0x00000001,0x0000002b,0x00000043,0x00000045,0x00000046,0x00050051, + 0x00000013,0x00000048,0x00000047,0x00000000,0x00050051,0x00000013,0x00000049,0x00000047, + 0x00000001,0x00050051,0x00000013,0x0000004a,0x00000047,0x00000002,0x00070050,0x00000014, + 0x0000004b,0x00000048,0x00000049,0x0000004a,0x00000044,0x00040063,0x00000041,0x00000042, + 0x0000004b,0x0004003d,0x00000036,0x0000004f,0x0000004e,0x0004003d,0x00000016,0x00000050, + 0x0000002a,0x00070058,0x00000014,0x00000051,0x0000004f,0x00000050,0x00000002,0x0000003b, + 0x00050051,0x00000013,0x00000053,0x00000051,0x00000000,0x0003003e,0x0000004d,0x00000053, + 0x0004003d,0x00000054,0x00000057,0x00000056,0x0004003d,0x00000007,0x00000058,0x00000009, + 0x0004003d,0x00000013,0x00000059,0x0000004d,0x00070050,0x00000014,0x0000005a,0x00000059, + 0x00000059,0x00000059,0x00000059,0x00040063,0x00000057,0x00000058,0x0000005a,0x0003003e, + 0x0000005b,0x0000005c,0x00050041,0x0000005e,0x0000005f,0x00000019,0x0000005d,0x0004003d, + 0x00000006,0x00000060,0x0000005f,0x000500c7,0x00000006,0x00000062,0x00000060,0x00000061, + 0x000500ab,0x0000001e,0x00000064,0x00000062,0x00000063,0x0004003d,0x00000013,0x00000065, + 0x0000004d,0x000500be,0x0000001e,0x00000066,0x00000065,0x0000003b,0x000500a7,0x0000001e, + 0x00000067,0x00000064,0x00000066,0x0004003d,0x00000013,0x00000068,0x0000004d,0x000500bc, + 0x0000001e,0x00000069,0x00000068,0x00000044,0x000500a7,0x0000001e,0x0000006a,0x00000067, + 0x00000069,0x000300f7,0x0000006c,0x00000000,0x000400fa,0x0000006a,0x0000006b,0x0000006c, + 0x000200f8,0x0000006b,0x00050041,0x0000004c,0x0000006f,0x0000002a,0x00000052,0x0004003d, + 0x00000013,0x00000070,0x0000006f,0x00050085,0x00000013,0x00000072,0x00000070,0x00000071, + 0x00050083,0x00000013,0x00000073,0x00000072,0x00000044,0x00050041,0x0000004c,0x00000075, + 0x0000002a,0x00000074,0x0004003d,0x00000013,0x00000076,0x00000075,0x00050085,0x00000013, + 0x00000077,0x00000076,0x00000071,0x00050083,0x00000013,0x00000078,0x00000044,0x00000077, + 0x0004003d,0x00000013,0x00000079,0x0000004d,0x00050085,0x00000013,0x0000007a,0x00000079, + 0x00000071,0x00050083,0x00000013,0x0000007b,0x0000007a,0x00000044,0x00070050,0x00000014, + 0x0000007c,0x00000073,0x00000078,0x0000007b,0x00000044,0x0003003e,0x0000006e,0x0000007c, + 0x00050041,0x0000007e,0x0000007f,0x00000019,0x00000063,0x0004003d,0x00000015,0x00000080, + 0x0000007f,0x0004003d,0x00000014,0x00000081,0x0000006e,0x00050091,0x00000014,0x00000082, + 0x00000080,0x00000081,0x0003003e,0x0000007d,0x00000082,0x00050041,0x0000004c,0x00000084, + 0x0000007d,0x00000083,0x0004003d,0x00000013,0x00000085,0x00000084,0x000500ba,0x0000001e, + 0x00000087,0x00000085,0x00000086,0x000300f7,0x00000089,0x00000000,0x000400fa,0x00000087, + 0x00000088,0x00000089,0x000200f8,0x00000088,0x0004003d,0x00000014,0x0000008b,0x0000007d, + 0x0007004f,0x00000016,0x0000008c,0x0000008b,0x0000008b,0x00000000,0x00000001,0x00050041, + 0x0000004c,0x0000008d,0x0000007d,0x00000083,0x0004003d,0x00000013,0x0000008e,0x0000008d, + 0x00050050,0x00000016,0x0000008f,0x0000008e,0x0000008e,0x00050088,0x00000016,0x00000090, + 0x0000008c,0x0000008f,0x0003003e,0x0000008a,0x00000090,0x0004003d,0x00000014,0x00000092, + 0x0000006e,0x0007004f,0x00000016,0x00000093,0x00000092,0x00000092,0x00000000,0x00000001, + 0x00050041,0x00000094,0x00000095,0x00000019,0x00000061,0x0004003d,0x00000016,0x00000096, + 0x00000095,0x00050083,0x00000016,0x00000097,0x00000093,0x00000096,0x0003003e,0x00000091, + 0x00000097,0x00050041,0x0000004c,0x00000099,0x0000008a,0x00000052,0x0004003d,0x00000013, + 0x0000009a,0x00000099,0x00050041,0x0000004c,0x0000009b,0x00000091,0x00000052,0x0004003d, + 0x00000013,0x0000009c,0x0000009b,0x00050083,0x00000013,0x0000009d,0x0000009a,0x0000009c, + 0x00050041,0x0000004c,0x0000009e,0x00000091,0x00000074,0x0004003d,0x00000013,0x0000009f, + 0x0000009e,0x00050041,0x0000004c,0x000000a0,0x0000008a,0x00000074,0x0004003d,0x00000013, + 0x000000a1,0x000000a0,0x00050083,0x00000013,0x000000a2,0x0000009f,0x000000a1,0x00050050, + 0x00000016,0x000000a3,0x0000009d,0x000000a2,0x0005008e,0x00000016,0x000000a4,0x000000a3, + 0x0000002d,0x0004003d,0x00000016,0x000000a5,0x00000026,0x00050085,0x00000016,0x000000a6, + 0x000000a4,0x000000a5,0x0003003e,0x00000098,0x000000a6,0x0004003d,0x00000016,0x000000a7, + 0x00000098,0x0006000c,0x00000016,0x000000a8,0x00000001,0x00000004,0x000000a7,0x000500b8, + 0x0000001f,0x000000ab,0x000000a8,0x000000aa,0x0004009b,0x0000001e,0x000000ac,0x000000ab, + 0x000300f7,0x000000ae,0x00000000,0x000400fa,0x000000ac,0x000000ad,0x000000ae,0x000200f8, + 0x000000ad,0x0004003d,0x00000016,0x000000af,0x00000098,0x0003003e,0x0000005b,0x000000af, + 0x000200f9,0x000000ae,0x000200f8,0x000000ae,0x000200f9,0x00000089,0x000200f8,0x00000089, + 0x000200f9,0x0000006c,0x000200f8,0x0000006c,0x0004003d,0x000000b0,0x000000b3,0x000000b2, + 0x0004003d,0x00000007,0x000000b4,0x00000009,0x0004003d,0x00000016,0x000000b5,0x0000005b, + 0x00050051,0x00000013,0x000000b6,0x000000b5,0x00000000,0x00050051,0x00000013,0x000000b7, + 0x000000b5,0x00000001,0x00070050,0x00000014,0x000000b8,0x000000b6,0x000000b7,0x0000003b, + 0x0000003b,0x00040063,0x000000b3,0x000000b4,0x000000b8,0x00050041,0x0000005e,0x000000b9, + 0x00000019,0x0000005d,0x0004003d,0x00000006,0x000000ba,0x000000b9,0x000500c7,0x00000006, + 0x000000bb,0x000000ba,0x0000001a,0x000500ab,0x0000001e,0x000000bc,0x000000bb,0x00000063, + 0x000300f7,0x000000be,0x00000000,0x000400fa,0x000000bc,0x000000bd,0x000000be,0x000200f8, + 0x000000bd,0x0004003d,0x0000003e,0x000000bf,0x00000040,0x0004003d,0x00000007,0x000000c0, + 0x00000009,0x0004003d,0x00000016,0x000000c1,0x0000005b,0x0006000c,0x00000016,0x000000c2, + 0x00000001,0x00000004,0x000000c1,0x00050050,0x00000016,0x000000c4,0x000000c3,0x000000c3, + 0x00050088,0x00000016,0x000000c5,0x000000c2,0x000000c4,0x00050050,0x00000016,0x000000c6, + 0x0000003b,0x0000003b,0x00050050,0x00000016,0x000000c7,0x00000044,0x00000044,0x0008000c, + 0x00000016,0x000000c8,0x00000001,0x0000002b,0x000000c5,0x000000c6,0x000000c7,0x0004003d, + 0x00000013,0x000000c9,0x0000004d,0x00050051,0x00000013,0x000000ca,0x000000c8,0x00000000, + 0x00050051,0x00000013,0x000000cb,0x000000c8,0x00000001,0x00070050,0x00000014,0x000000cc, + 0x000000ca,0x000000cb,0x000000c9,0x00000044,0x00040063,0x000000bf,0x000000c0,0x000000cc, + 0x000200f9,0x000000be,0x000200f8,0x000000be,0x000100fd,0x00010038 +}; diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp new file mode 100644 index 000000000..9044145b7 --- /dev/null +++ b/gpu/vk/vk_dlss.cpp @@ -0,0 +1,608 @@ +// (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, one dispatch, cooperative matrices) scales it up by 4/3, 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 none: only the +// camera's motion, what moves by itself ghosts). No jitter yet (the game's camera has none). +// - 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=1 / 0: on / off over the Insert menu's row. RX_VK_DLSS_DIR=: where libnvidia-ngx-dlss.so.* is +// (else the game folder's ngx/, then $XDG_DATA_HOME/hookshot/ngx). RX_VK_DLSS_PRESET=: DLSS's preset (default K). +// RX_VK_DLSS_DEBUG=motion: the motion vectors shown instead of the picture. 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/linux/ngx-sdk.sh) and +// builds with HOOKSHOT_NGX_SDK, which compiles this file with HOOKSHOT_NGX. Without it only the stubs at the end are here. +#include "vk_dlss.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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_out_comp_spv.h" +#include "shaders/dlss_prep_comp_spv.h" +#endif + +namespace mkwvk { + +namespace { +int env_switch(const char* name) { // (-1: unset) + const char* e = getenv(name); + return e && *e ? atoi(e) : -1; +} +} // namespace + +bool dlss_wanted() { + if (!dlss_built()) return false; + static const int env = env_switch("RX_VK_DLSS"); + return env >= 0 ? env != 0 : opts.dlss != 0; +} + +#ifdef HOOKSHOT_NGX + +namespace { + +constexpr uint64_t kGameplayUpscale = 0xfd029a317a9a4b94ull; + +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, failed = false; + 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; + Image color, depth, motion, output; + 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; + Camera cam; // (this frame's, since the last upscale) + uint64_t last_frame = 0; // (the frame DLSS last drew) + 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::unordered_map depth_views; + uint64_t depth_views_gen = 0; + const char* reason = nullptr; + int reason_repeats = 0; + bool told_first = false; + struct Dead { uint64_t frame; std::function free; }; + std::vector dead; // (what work in flight may still use: freed a few frames on) +}; +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) { + 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; + 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()); +} + +std::string game_dir() { + char exe[MAX_PATH]; + const DWORD n = game_module_path(exe, MAX_PATH); + std::string dir(exe, n); + return dir.substr(0, dir.find_last_of("\\/") + 1); +} +std::wstring wide(const std::string& s) { return std::wstring(s.begin(), s.end()); } + +/// 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 libnvidia-ngx-dlss.so. is looked for (NGX takes the newest it finds), and its own files go + std::vector dirs; + if (const char* e = getenv("RX_VK_DLSS_DIR")) dirs.push_back(e); + dirs.push_back(game_dir() + "ngx"); + if (const char* x = getenv("XDG_DATA_HOME"); x && *x) dirs.push_back(std::string(x) + "/hookshot/ngx"); + else if (const char* h = getenv("HOME")) dirs.push_back(std::string(h) + "/.local/share/hookshot/ngx"); + 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 std::string data = game_dir() + "ngx"; + std::filesystem::create_directories(data); // (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); + 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"); + return false; + } + int available = 0, needs_driver = 0; + unsigned major = 0, minor = 0; + s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_Available, &available); + s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_NeedsUpdatedDriver, &needs_driver); + s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMajor, &major); + s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMinor, &minor); + if (!available) { + std::string where; + for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + d; + if (needs_driver) logf("vk: NVIDIA DLSS: needs NVIDIA's driver %u.%u or newer", major, minor); + else logf("vk: NVIDIA DLSS: not available: no libnvidia-ngx-dlss.so in %s (scripts/linux/ngx-sdk.sh fetches it)", where.c_str()); + return false; + } + if (NVSDK_NGX_FAILED(NVSDK_NGX_VULKAN_AllocateParameters(&s.params)) || !s.params) return false; + logf("vk: NVIDIA DLSS: NGX started (DLSS from %s)", dirs.size() > 1 ? "the folders searched" : dirs[0].c_str()); + s.ngx_ok = true; + return true; +} + +bool make_pipelines() { + State& s = st(); + if (s.prep && s.out) return true; + if (!dev.push_descriptor || !vkCmdPushDescriptorSetKHR) return false; + const auto make = [](const uint32_t* code, size_t size, const VkDescriptorType* types, uint32_t n, uint32_t push, + VkPipelineLayout& layout, VkPipeline& pipe) { + 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; + VkDescriptorSetLayout set{}; + if (vkCreateDescriptorSetLayout(dev.device, &li, nullptr, &set) != VK_SUCCESS) return false; + 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 false; + 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 false; + 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; + }; + 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 (!make(DLSS_PREP_COMP_SPV, sizeof DLSS_PREP_COMP_SPV, prep_types, 5, 84, s.prep_layout, s.prep) || + !make(DLSS_OUT_COMP_SPV, sizeof DLSS_OUT_COMP_SPV, out_types, 2, 12, s.out_layout, s.out)) { + s.prep = s.out = VK_NULL_HANDLE; + 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; + return vkCreateSampler(dev.device, &si, nullptr, &s.point) == VK_SUCCESS; +} + +/// 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; +} + +/// this frame's clip space to the frame before's: previous * inverse(current), row major; false when singular +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; +} + +/// DLSS's feature for this output size, made when the size 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(); + 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) + return true; + 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) + static const NVSDK_NGX_PerfQuality_Value modes[] = {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}; + NVSDK_NGX_PerfQuality_Value mode = modes[0]; + unsigned max_w = 0, max_h = 0, min_w = 0, min_h = 0; + bool found = false; + for (const auto m : modes) { + 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"); + // (preset K, NVIDIA's transformer model, for every mode: RX_VK_DLSS_PRESET= another) + unsigned preset = NVSDK_NGX_DLSS_Hint_Render_Preset_K; + if (const char* p = getenv("RX_VK_DLSS_PRESET"); p && *p >= 'A' && *p <= 'Z') preset = (unsigned)(*p - 'A' + 1); + 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}) + s.params->Set(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 NVSDK_NGX_Result r = NGX_VULKAN_CREATE_DLSS_EXT(cmd, 1, 1, &s.feature, s.params, &cp); + 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; + logf("vk: NVIDIA DLSS: %ux%u (up to %ux%u) -> %ux%u, mode %d, preset %c", rw, rh, max_w, max_h, ow, oh, (int)mode, + (char)('A' + preset - 1)); + 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; + 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); + } +} + +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; + static int told; + if (told++ % 3000 == 0) { // (now and then: what the camera's buffer held, to check its layout) + std::string line; + for (int i = 0; i < 0x230 / 4; i++) { + char b[24]; + snprintf(b, sizeof b, "%s%g", i % 4 ? " " : i ? " | " : "", c[i]); + line += b; + } + logf("vk: NVIDIA DLSS: the scene camera (%ux%u): %s", width, height, line.c_str()); + } + 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(); + if (off || !dlss_wanted() || s.failed || !s.last_frame || texture_frame_no() - s.last_frame > 30) return 0; + return axis ? s.jitter_h.load(std::memory_order_relaxed) : s.jitter_w.load(std::memory_order_relaxed); +} + +bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in_view, Texture* out, VkImageView out_view, + uint32_t out_level) { + if (key != kGameplayUpscale || !dlss_wanted()) return false; + State& s = st(); + std::lock_guard lk(s.mu); + free_dead(false); + const Camera cam = s.cam; + s.cam.valid = false; // (each frame's own camera) + if (s.failed) return fail("DLSS failed earlier"); + 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 * 2 || oh > rh * 2) + 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"); + if (!make_feature(cmd, rw, rh, ow, oh)) 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) || + !make_image(cmd, s.output, VK_FORMAT_R16G16B16A16_SFLOAT, ow, oh)) + return fail("its images were not made"); + + const uint64_t now = texture_frame_no(); + const bool reset = !s.last_frame || now - s.last_frame > 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); + for (int c = 0; c < 4; c++) { + previous[0 * 4 + c] -= s.last_jitter[0] * previous[3 * 4 + c]; + previous[1 * 4 + c] -= s.last_jitter[1] * previous[3 * 4 + c]; + } + const bool motion = !reset && 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); + 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; RX_VK_DLSS_JITTER_SIGN=x, y or xy: those turned round, to check) + static const char* sign = getenv("RX_VK_DLSS_JITTER_SIGN") ? getenv("RX_VK_DLSS_JITTER_SIGN") : ""; + ep.InJitterOffsetX = cam.jitter[0] * 0.5f * (float)rw * (strchr(sign, 'x') ? -1.0f : 1.0f); + ep.InJitterOffsetY = -cam.jitter[1] * 0.5f * (float)rh * (strchr(sign, 'y') ? -1.0f : 1.0f); + 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); + 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; + descriptors_disturbed(); + return false; // (the network writes its output over whatever the evaluation did) + } + full_barrier(cmd); + { + static const bool square = env_switch("RX_VK_DLSS_SQUARE") != 0; + const int32_t opc[3] = {(int32_t)ow, (int32_t)oh, square && !show_motion ? 1 : 0}; + const VkDescriptorImageInfo info[2] = {{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); + } + descriptors_disturbed(); // (the program's own pushes are gone from the bind point) + s.last_frame = now; + s.last_jitter[0] = cam.jitter[0], s.last_jitter[1] = cam.jitter[1]; + std::memcpy(s.last_vp, cam.current, sizeof s.last_vp); + s.jitter_w = rw, s.jitter_h = rh; + if (s.reason) logf("vk: NVIDIA DLSS draws the upscale again (frame %llu)", (unsigned long long)now); + s.reason = nullptr; + 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_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, Texture*, VkImageView, Texture*, VkImageView, uint32_t) { return false; } +uint32_t dlss_jitter_size(int) { return 0; } + +#endif + +} // namespace mkwvk diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h new file mode 100644 index 000000000..fc1cebc43 --- /dev/null +++ b/gpu/vk/vk_dlss.h @@ -0,0 +1,34 @@ +// 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/linux/ngx-sdk.sh); 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(); +/// DLSS asked for: the Insert menu's NVIDIA DLSS row, or RX_VK_DLSS=1 / 0 over it +bool dlss_wanted(); +/// 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); +/// The gameplay upscale's dispatch (program `key`): its input as it samples it (in, in_view) and its output's storage +/// view (out at level 0). 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, Texture* in, VkImageView in_view, Texture* out, VkImageView out_view, + uint32_t out_level); + +/// The scene's width (axis 0) or height (1) for the host's camera jitter (nx2/oot_jitter.py, rx_option 6 and 7): 0 when +/// there is to be none (DLSS not drawing, or RX_VK_DLSS_JITTER=0) +uint32_t dlss_jitter_size(int axis); + +} // namespace mkwvk diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index dbf192774..ab6a15ea4 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -27,6 +27,7 @@ #include "vk_paths.h" #include "vk_draw.h" +#include "vk_dlss.h" #include "vk_dump.h" #include "vk_gprof.h" #include "vk_jobs.h" @@ -4467,6 +4468,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; @@ -5392,6 +5396,25 @@ 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) { + if (p.key != 0xfd029a317a9a4b94ull || !dlss_wanted() || p.stages.empty()) return false; + const Stage& st = p.stages[0]; + 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); + return dlss_upscale(cmd, p.key, in, dw.imgs[0].imageView, out, dw.imgs[ntex].imageView, level); +} bool taau_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { const char* keys = getenv("RX_VK_TAAU_KEYS") ? getenv("RX_VK_TAAU_KEYS") : "fd029a317a9a4b94"; if (!taau_key(p.key) || !dev.push_descriptor || !vkCmdPushDescriptorSetKHR || p.stages.empty()) return false; @@ -5944,6 +5967,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_main.cpp b/gpu/vk/vk_main.cpp index 908e29f63..81aa20b0c 100644 --- a/gpu/vk/vk_main.cpp +++ b/gpu/vk/vk_main.cpp @@ -17,6 +17,7 @@ #include "vk_internal.h" #include "gpu_choice.h" #include "vk_menu.h" +#include "vk_dlss.h" namespace vkx { #define VKX_DEFINE(f) PFN_##f f; @@ -107,8 +108,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; @@ -228,6 +237,7 @@ 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) // the queue: one family for graphics, compute and transfer vkGetPhysicalDeviceQueueFamilyProperties(dev.physical, &n, nullptr); diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 0d23898c2..293d83e8c 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -15,6 +15,7 @@ #include "vk_paths.h" #include "vk_menu.h" #include "vk_work.h" // (after the others: counts the commands that do GPU work, for full_barrier) +#include "vk_dlss.h" #ifndef HOOKSHOT_VERSION // (nx2/build.py gives it to this file: nx2/version.py) #define HOOKSHOT_VERSION "unknown" #endif @@ -57,6 +58,7 @@ const char* tx_upscale(int v) { 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]; } +const char* tx_dlss(int v) { return !dlss_built() ? "Not in this build" : v ? "On" : "Off"; } const char* tx_percent(int v) { static char b[8]; snprintf(b, sizeof b, "%d%%", v); @@ -151,6 +153,9 @@ Row rows[] = { "faster."}, {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."}, + {TAB_GRAPHICS, "NVIDIA DLSS", &opts.dlss, 0, 1, 1, tx_dlss, + "NVIDIA's DLSS scales the 3D up in place of the game's own upscaler (NVIDIA GPUs, in a build made with NVIDIA's SDK). " + "Sharper and steadier edges; things that move by themselves can leave trails for now."}, {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, diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index ae7ad0bc9..f67f3104b 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -11,6 +11,7 @@ struct Options { 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 upscale = 0; // to the window: 0 off (the plain copy to the window), 1 Catmull-Rom, 2 Lanczos + int dlss = 0; // NVIDIA DLSS in place of the game's own upscale (vk_dlss.cpp; a build with NVIDIA's SDK only) 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) diff --git a/nx2/build.py b/nx2/build.py index 3d4b0f589..bb44a73c1 100644 --- a/nx2/build.py +++ b/nx2/build.py @@ -135,6 +135,15 @@ def tree_headers(*roots, skip=()): return h.hexdigest().encode() +# (linux) HOOKSHOT_NGX_SDK=: the Vulkan back end with NVIDIA DLSS Super Resolution, from NVIDIA's NGX SDK fetched into +# that folder by scripts/linux/ngx-sdk.sh (its include/ and lib/libnvsdk_ngx.a; never committed, NVIDIA's license). Off without it. +NGX_SDK = os.environ.get("HOOKSHOT_NGX_SDK", "") +if NGX_SDK and not (LINUX and os.path.exists(os.path.join(NGX_SDK, "lib", "libnvsdk_ngx.a"))): + print("build: HOOKSHOT_NGX_SDK=%s has no lib/libnvsdk_ngx.a (scripts/linux/ngx-sdk.sh), or this is not Linux: built " + "without NVIDIA DLSS" % NGX_SDK, flush=True) + NGX_SDK = "" + + 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)""" @@ -711,6 +720,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" + os.path.join(cpm_package("vulkanmemoryallocator"), "include")] if cpm_package("vulkanmemoryallocator") else []) + # (NVIDIA DLSS, a local build's option: scripts/linux/ngx-sdk.sh. 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"), @@ -721,7 +735,8 @@ def main(): name = os.path.relpath(src, vk).replace(os.sep, "_")[:-4] 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 + ([stamp_def] if os.path.basename(src) == "vk_menu.cpp" else []), + vflags + ([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)) 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, @@ -869,7 +884,8 @@ 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] + \ + ([os.path.join(NGX_SDK, "lib", "libnvsdk_ngx.a")] 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 []) diff --git a/scripts/linux/ngx-sdk.sh b/scripts/linux/ngx-sdk.sh new file mode 100755 index 000000000..339c81e16 --- /dev/null +++ b/scripts/linux/ngx-sdk.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# ngx-sdk.sh: 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: they are under NVIDIA's own license (the DLSS SDK's +# LICENSE.txt, fetched beside them; read it before using them). This fetches, from NVIDIA's DLSS repository at one pinned +# release, each file checked against its SHA-256: +# - the SDK headers and libnvsdk_ngx.a, the build's part, into (default build-linux/ngx-sdk) +# - libnvidia-ngx-dlss.so, the DLSS runtime the driver loads, into when given (a game folder's ngx/, or +# $XDG_DATA_HOME/hookshot/ngx; RX_VK_DLSS_DIR names another) +# The NGX core itself (libnvidia-ngx.so) comes with NVIDIA's Linux driver. +# +# Usage: scripts/linux/ngx-sdk.sh [] [] +# Then build with HOOKSHOT_NGX_SDK=. +set -euo pipefail +COMMIT=9981c6350dc58c0ce21f024fd65e31e851c31689 # (v310.9.16) +BASE=https://raw.githubusercontent.com/NVIDIA/DLSS/$COMMIT +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +SDK=${1:-$ROOT/build-linux/ngx-sdk} +RUNTIME=${2:-} +FILES=" + LICENSE.txt d4216e39ebef5f9b50a6712ebb37beeb5379862a67733a9999c651f21592aaf0 + lib/Linux_x86_64/libnvsdk_ngx.a d3c50bb0d9624fc35e502a8e929b8fc1e2b505017ebdb84c0d5b627e11a235df + 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 +" +RUNTIME_FILE="lib/Linux_x86_64/rel/libnvidia-ngx-dlss.so.310.9.16 30982c937afed85c96b8d313323de9a650a2eec6de1bb0f714b21750f4ce98f6" + +fetch() { # (path in the repository, its SHA-256, where it goes) + local tmp="$3.part" + mkdir -p "$(dirname "$3")" + if [ -f "$3" ] && echo "$2 $3" | sha256sum -c --status; then return; fi + curl -fsSL -o "$tmp" "$BASE/$1" + if ! echo "$2 $tmp" | sha256sum -c --status; then + rm -f "$tmp"; echo "ngx-sdk: $1 is not the file expected (SHA-256)"; exit 1 + fi + mv "$tmp" "$3" +} +echo "$FILES" | while read -r path sum; do + [ -n "$path" ] || continue + case "$path" in lib/*) dst=$SDK/lib/$(basename "$path") ;; *) dst=$SDK/$path ;; esac + fetch "$path" "$sum" "$dst" +done +echo "ngx-sdk: the SDK's headers and library in $SDK (NVIDIA's license: $SDK/LICENSE.txt)" +if [ -n "$RUNTIME" ]; then + set -- $RUNTIME_FILE + fetch "$1" "$2" "$RUNTIME/$(basename "$1")" + echo "ngx-sdk: the DLSS runtime in $RUNTIME" +fi From a9a69c6ccf6b181592f3b65d04e6c87e8f7f9864 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:40:51 -0500 Subject: [PATCH 02/52] DLSS: sub-pixel camera jitter for OoT's scene camera while NVIDIA DLSS draws DLSS resolves detail finer than a pixel only from frames whose camera moves by a different fraction of a pixel each time. nx2/oot_jitter.py adds C after the loads of sead's perspective projection's field of view and ratio (its matrix update and the game's three inlined copies of it): for cameras of the picture's ratio it writes the projection's centre offset (+0xb4, +0xb8), which the update reads next, from host/rx_backend.c hos_dlss_jitter (Halton 2, 3 over 16 frames, scaled by the scene's size the renderer reports as rx_option 6 and 7). The renderer reports 0 unless DLSS is drawing, so the game runs as shipped otherwise. The renderer reads the offset back from the projection in the camera's uniform buffer, hands it to DLSS, and takes it off the motion vectors. RX_VK_DLSS_JITTER=0: none. --- gpu/vk/vk_dlss.h | 2 +- gpu/vk/vk_replay.cpp | 4 +++- host/rx_backend.c | 28 +++++++++++++++++++++++++- nx2/lift.py | 2 ++ nx2/oot_jitter.py | 48 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 nx2/oot_jitter.py diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index fc1cebc43..9c9af7519 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -27,7 +27,7 @@ void dlss_note_scene_draw(const Texture* color0, uint32_t width, uint32_t height bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in_view, Texture* out, VkImageView out_view, uint32_t out_level); -/// The scene's width (axis 0) or height (1) for the host's camera jitter (nx2/oot_jitter.py, rx_option 6 and 7): 0 when +/// The scene's width (axis 0) or height (1) for the host's camera jitter (nx2/oot_jitter.py, rx_option 18 and 19): 0 when /// there is to be none (DLSS not drawing, or RX_VK_DLSS_JITTER=0) uint32_t dlss_jitter_size(int axis); diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 4caafcc30..bc1cfc6d1 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -25,6 +25,7 @@ #include #include +#include "vk_dlss.h" #include "vk_draw.h" #include "vk_dump.h" #include "vk_gprof.h" @@ -3541,7 +3542,8 @@ void fill_replay_table(MkwVkRenderer& t) { t.nvn_event_signal = event_signal_queued; t.option = [](int id) -> int { return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? opts.resolution == 0 : id == 3 ? (opts.resolution == 1 ? 100 : opts.resolution == 2 ? 75 : opts.resolution == 3 ? 67 : opts.resolution == 4 ? 50 : 0) : 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 == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : 0; + id == 7 ? opts.mouse_sens : id == 8 ? opts.mouse_invert : id == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : + id == 18 || id == 19 ? (int)dlss_jitter_size(id - 18) : 0; }; t.option_set = menu_option_set; t.nvn_register_sampler = register_sampler_queued; diff --git a/host/rx_backend.c b/host/rx_backend.c index ebd912a93..f61cdad0d 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -271,7 +271,33 @@ 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 camera, in sead's + projection offset (a fraction of the view's width and height), into out; 0 when there is none (DLSS off, or a renderer + without it: rx_option 18 and 19, the scene's size in pixels, are 0). The renderer reads the offset the camera ended up + with 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 16 frames. */ +int hos_dlss_jitter(float out[2]) { + static LONG at = -1; + static float w, h; + const LONG frame = presents; + if (frame != at) { /* (asked once a frame: the renderer's lock is not taken for every camera) */ + at = frame; + w = (float)rx_option(18, 0), h = (float)rx_option(19, 0); + } + if (w < 1.0f || h < 1.0f) return 0; + const int i = (int)(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); + out[0] = (x - 0.5f) / w, out[1] = (y - 0.5f) / h; + return 1; +} static void forward(const early_call *c) { switch (c->kind) { case 0: { diff --git a/nx2/lift.py b/nx2/lift.py index 1ffd043cd..9b645f87b 100644 --- a/nx2/lift.py +++ b/nx2/lift.py @@ -158,6 +158,8 @@ 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, none unless DLSS runs: host/rx_backend.c hos_dlss_jitter) + 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/oot_jitter.py b/nx2/oot_jitter.py new file mode 100644 index 000000000..bb917a487 --- /dev/null +++ b/nx2/oot_jitter.py @@ -0,0 +1,48 @@ +"""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 camera's projection a sub-pixel offset each +frame. sead's perspective projection keeps an offset of the frustum's centre at +0xb4 (x) and +0xb8 (y), in fractions of +its width and height, which its matrix update (and the game's three inlined copies of it) reads right after the field of +view and aspect ratio at +0xac. The sites are those loads (the same ones nx2/oot_wide.py's ratio patch follows): the +offset is written before it is read, for cameras of the picture's ratio. host/rx_backend.c hos_dlss_jitter picks it; it +is none (the game as shipped) unless DLSS runs, and the renderer reads the offset back from the camera's uniform buffer. +""" + +import oot_wide + +# (main offset, the original instruction word there, the register the aspect ratio was loaded into, the projection's register) +SITES = ( + (0x3485cc0, 0x2d558400, 1, 0), # sead's perspective projection's matrix update: ldp s0, s1, [x0, #0xac] + (0x4f1e8, 0x2d558ea0, 3, 21), # its inlined copies + (0x23b9a8, 0x2d558c00, 3, 0), + (0x33b82c, 0x2d558440, 1, 2), +) + + +def _text(vreg, base): + # (16:9, or the window's ratio once the widescreen patch before this one has put it in: the picture's cameras) + return ("{ extern int hos_dlss_jitter(float out[2]); extern volatile float hos_wide_ratio; float _j[2]; " + "if (hos_dlss_jitter(_j)) { float _r; memcpy(&_r, c->vreg[%d], 4); const float _w = hos_wide_ratio; " + "const float _d = _r - (16.0f / 9.0f), _e = _w > 0.0f ? _r - _w : 1.0f; " + "if ((_d <= 0.01f && _d >= -0.01f) || (_e <= 0.01f && _e >= -0.01f)) { uint32_t _u[2]; memcpy(_u, _j, 8); " + "recomp_store32(c, c->x[%d] + 0xb4, _u[0]); recomp_store32(c, c->x[%d] + 0xb8, _u[1]); } } }" + " /* (nx2/oot_jitter.py: DLSS camera jitter) */" % (vreg, base, base)) + + +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, vreg, base 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(vreg, base) + n += 1 + print("dlss jitter: %d of %d sites" % (n, len(SITES))) + return n From a07f2ab99bf9556ab9efeadb4b388bb92967b154 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:20:37 -0500 Subject: [PATCH 03/52] tests: the shader stamp test's build.py namespace has NGX_SDK (none) The test runs nx2/build.py's Vulkan job construction in a namespace of its own; that code now reads NGX_SDK (NVIDIA DLSS's SDK folder, empty when there is none), which the namespace lacked. --- tests/test_shader_stamp_cache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_shader_stamp_cache.py b/tests/test_shader_stamp_cache.py index c7c62d5f2..31ef75a0d 100644 --- a/tests/test_shader_stamp_cache.py +++ b/tests/test_shader_stamp_cache.py @@ -80,6 +80,7 @@ def jobs(self, linux, version="fixture", release=False): "vk_headers": str(self.root / "deps/vulkanheaders"), "obj_dir": str(self.root / "obj"), "cc": "clang", "jobs": [], "stamp_def": '-DHOOKSHOT_VERSION="%s"' % version, + "NGX_SDK": "", # (no NVIDIA NGX SDK: vk_dlss.cpp built without DLSS, as by default) } 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. From b5dc03f2da35071111e2df4f57598db3ffa1a2e4 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:31:55 -0500 Subject: [PATCH 04/52] DLSS: DLAA, NVIDIA's anti-aliasing at the render size, as a third choice of the NVIDIA DLSS row The row is now Off / DLSS / DLAA (RX_VK_DLSS=0 / 1 / 2). DLAA runs DLSS at a scale of 1 on the scene as drawn, with the 3D held at full scale whatever the Resolution row says (rx_option 2 and 3), and dlss_out.comp scales its picture to the game's upscale output with a linear filter. (The game reduces that output to 1080p for its presentation pass anyway: DLSS at 100% resolution is close to it, and cheaper.) --- gpu/vk/shaders/dlss_out.comp | 7 +- gpu/vk/shaders/dlss_out_comp_spv.h | 130 ++++++++++++++++------------- gpu/vk/vk_dlss.cpp | 45 +++++++--- gpu/vk/vk_dlss.h | 8 +- gpu/vk/vk_menu.cpp | 12 ++- gpu/vk/vk_menu.h | 2 +- gpu/vk/vk_replay.cpp | 2 +- 7 files changed, 126 insertions(+), 80 deletions(-) diff --git a/gpu/vk/shaders/dlss_out.comp b/gpu/vk/shaders/dlss_out.comp index 32618c76a..8b4b84c3a 100644 --- a/gpu/vk/shaders/dlss_out.comp +++ b/gpu/vk/shaders/dlss_out.comp @@ -1,6 +1,7 @@ #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). pc.square = 0: as it is. +// does, the square of its perceptual encoding (linear colour again). pc.square = 0: as it is. 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; @@ -8,7 +9,9 @@ layout(push_constant) uniform P { ivec2 size; int square; } pc; void main() { const ivec2 g = ivec2(gl_GlobalInvocationID.xy); if (any(greaterThanEqual(g, pc.size))) return; - vec3 c = max(texelFetch(src, g, 0).rgb, vec3(0.0)); + 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)); if (pc.square != 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 index ee01ec5cc..9d6f66d07 100644 --- a/gpu/vk/shaders/dlss_out_comp_spv.h +++ b/gpu/vk/shaders/dlss_out_comp_spv.h @@ -1,61 +1,77 @@ // 1116.6.0 #pragma once const uint32_t DLSS_OUT_COMP_SPV[] = { - 0x07230203,0x00010600,0x0008000b,0x0000004a,0x00000000,0x00020011,0x00000001,0x00020011, - 0x00000031,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e,0x00000000,0x0003000e, - 0x00000000,0x00000001,0x0009000f,0x00000005,0x00000004,0x6e69616d,0x00000000,0x0000000d, - 0x00000015,0x00000028,0x0000003e,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,0x00050006,0x00000013,0x00000001,0x61757173,0x00006572, - 0x00030005,0x00000015,0x00006370,0x00030005,0x00000024,0x00000063,0x00030005,0x00000028, - 0x00637273,0x00030005,0x0000003e,0x00747364,0x00040047,0x0000000d,0x0000000b,0x0000001c, - 0x00030047,0x00000013,0x00000002,0x00050048,0x00000013,0x00000000,0x00000023,0x00000000, - 0x00050048,0x00000013,0x00000001,0x00000023,0x00000008,0x00040047,0x00000028,0x00000021, - 0x00000000,0x00040047,0x00000028,0x00000022,0x00000000,0x00030047,0x0000003e,0x00000019, - 0x00040047,0x0000003e,0x00000021,0x00000001,0x00040047,0x0000003e,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,0x0004001e,0x00000013,0x00000007, - 0x00000006,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,0x0000002c, - 0x00000021,0x00000004,0x0004002b,0x00000021,0x0000002f,0x00000000,0x0006002c,0x00000022, - 0x00000030,0x0000002f,0x0000002f,0x0000002f,0x0004002b,0x00000006,0x00000032,0x00000001, - 0x00040020,0x00000033,0x00000009,0x00000006,0x00090019,0x0000003c,0x00000021,0x00000001, - 0x00000000,0x00000000,0x00000000,0x00000002,0x00000008,0x00040020,0x0000003d,0x00000000, - 0x0000003c,0x0004003b,0x0000003d,0x0000003e,0x00000000,0x0004002b,0x00000021,0x00000042, - 0x3f800000,0x0004002b,0x0000000a,0x00000047,0x00000008,0x0004002b,0x0000000a,0x00000048, - 0x00000001,0x0006002c,0x0000000b,0x00000049,0x00000047,0x00000047,0x00000048,0x00050036, - 0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b,0x00000008, - 0x00000009,0x00000007,0x0004003b,0x00000023,0x00000024,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,0x0004003d,0x00000007, - 0x0000002a,0x00000009,0x00040064,0x00000025,0x0000002b,0x00000029,0x0007005f,0x0000002c, - 0x0000002d,0x0000002b,0x0000002a,0x00000002,0x00000016,0x0008004f,0x00000022,0x0000002e, - 0x0000002d,0x0000002d,0x00000000,0x00000001,0x00000002,0x0007000c,0x00000022,0x00000031, - 0x00000001,0x00000028,0x0000002e,0x00000030,0x0003003e,0x00000024,0x00000031,0x00050041, - 0x00000033,0x00000034,0x00000015,0x00000032,0x0004003d,0x00000006,0x00000035,0x00000034, - 0x000500ab,0x0000001a,0x00000036,0x00000035,0x00000016,0x000300f7,0x00000038,0x00000000, - 0x000400fa,0x00000036,0x00000037,0x00000038,0x000200f8,0x00000037,0x0004003d,0x00000022, - 0x00000039,0x00000024,0x0004003d,0x00000022,0x0000003a,0x00000024,0x00050085,0x00000022, - 0x0000003b,0x0000003a,0x00000039,0x0003003e,0x00000024,0x0000003b,0x000200f9,0x00000038, - 0x000200f8,0x00000038,0x0004003d,0x0000003c,0x0000003f,0x0000003e,0x0004003d,0x00000007, - 0x00000040,0x00000009,0x0004003d,0x00000022,0x00000041,0x00000024,0x00050051,0x00000021, - 0x00000043,0x00000041,0x00000000,0x00050051,0x00000021,0x00000044,0x00000041,0x00000001, - 0x00050051,0x00000021,0x00000045,0x00000041,0x00000002,0x00070050,0x0000002c,0x00000046, - 0x00000043,0x00000044,0x00000045,0x00000042,0x00040063,0x0000003f,0x00000040,0x00000046, - 0x000100fd,0x00010038 + 0x07230203,0x00010600,0x0008000b,0x00000064,0x00000000,0x00020011,0x00000001,0x00020011, + 0x00000031,0x00020011,0x00000032,0x0006000b,0x00000001,0x4c534c47,0x6474732e,0x3035342e, + 0x00000000,0x0003000e,0x00000000,0x00000001,0x0009000f,0x00000005,0x00000004,0x6e69616d, + 0x00000000,0x0000000d,0x00000015,0x00000028,0x00000058,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,0x00050006,0x00000013,0x00000001, + 0x61757173,0x00006572,0x00030005,0x00000015,0x00006370,0x00030005,0x00000024,0x00000063, + 0x00030005,0x00000028,0x00637273,0x00030005,0x00000058,0x00747364,0x00040047,0x0000000d, + 0x0000000b,0x0000001c,0x00030047,0x00000013,0x00000002,0x00050048,0x00000013,0x00000000, + 0x00000023,0x00000000,0x00050048,0x00000013,0x00000001,0x00000023,0x00000008,0x00040047, + 0x00000028,0x00000021,0x00000000,0x00040047,0x00000028,0x00000022,0x00000000,0x00030047, + 0x00000058,0x00000019,0x00040047,0x00000058,0x00000021,0x00000001,0x00040047,0x00000058, + 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,0x0004001e, + 0x00000013,0x00000007,0x00000006,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,0x0004002b,0x00000006, + 0x0000004c,0x00000001,0x00040020,0x0000004d,0x00000009,0x00000006,0x00090019,0x00000056, + 0x00000021,0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000008,0x00040020, + 0x00000057,0x00000000,0x00000056,0x0004003b,0x00000057,0x00000058,0x00000000,0x0004002b, + 0x00000021,0x0000005c,0x3f800000,0x0004002b,0x0000000a,0x00000061,0x00000008,0x0004002b, + 0x0000000a,0x00000062,0x00000001,0x0006002c,0x0000000b,0x00000063,0x00000061,0x00000061, + 0x00000062,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,0x00050041,0x0000004d,0x0000004e, + 0x00000015,0x0000004c,0x0004003d,0x00000006,0x0000004f,0x0000004e,0x000500ab,0x0000001a, + 0x00000050,0x0000004f,0x00000016,0x000300f7,0x00000052,0x00000000,0x000400fa,0x00000050, + 0x00000051,0x00000052,0x000200f8,0x00000051,0x0004003d,0x00000022,0x00000053,0x00000024, + 0x0004003d,0x00000022,0x00000054,0x00000024,0x00050085,0x00000022,0x00000055,0x00000054, + 0x00000053,0x0003003e,0x00000024,0x00000055,0x000200f9,0x00000052,0x000200f8,0x00000052, + 0x0004003d,0x00000056,0x00000059,0x00000058,0x0004003d,0x00000007,0x0000005a,0x00000009, + 0x0004003d,0x00000022,0x0000005b,0x00000024,0x00050051,0x00000021,0x0000005d,0x0000005b, + 0x00000000,0x00050051,0x00000021,0x0000005e,0x0000005b,0x00000001,0x00050051,0x00000021, + 0x0000005f,0x0000005b,0x00000002,0x00070050,0x00000036,0x00000060,0x0000005d,0x0000005e, + 0x0000005f,0x0000005c,0x00040063,0x00000059,0x0000005a,0x00000060,0x000100fd,0x00010038 }; diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 9044145b7..4b826fca6 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -56,11 +56,13 @@ int env_switch(const char* name) { // (-1: unset) } } // namespace -bool dlss_wanted() { - if (!dlss_built()) return false; +int dlss_mode() { + if (!dlss_built()) return 0; static const int env = env_switch("RX_VK_DLSS"); - return env >= 0 ? env != 0 : opts.dlss != 0; + 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 @@ -97,7 +99,7 @@ struct State { Image color, depth, motion, output; 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; + VkSampler point = VK_NULL_HANDLE, linear = VK_NULL_HANDLE; Camera cam; // (this frame's, since the last upscale) uint64_t last_frame = 0; // (the frame DLSS last drew) float last_jitter[2]{}; // (its camera's jitter: the frame before's, for the next one) @@ -290,7 +292,9 @@ bool make_pipelines() { 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; - return vkCreateSampler(dev.device, &si, nullptr, &s.point) == VK_SUCCESS; + if (vkCreateSampler(dev.device, &si, nullptr, &s.point) != VK_SUCCESS) return false; + si.magFilter = si.minFilter = VK_FILTER_LINEAR; + return vkCreateSampler(dev.device, &si, nullptr, &s.linear) == VK_SUCCESS; } /// A view of a depth texture's depth alone, for sampling (made once per image) @@ -357,13 +361,20 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui s.feature = nullptr; } // (the quality mode whose range of render sizes holds this one: the network's 3/4 is DLSS's MaxQuality range) - static const NVSDK_NGX_PerfQuality_Value modes[] = {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}; + // (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 = false; - for (const auto m : modes) { + 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; @@ -497,11 +508,15 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in } const VkImageView dview = depth_view(cam.depth); if (!dview) return fail("no view of the scene depth"); - if (!make_feature(cmd, rw, rh, ow, oh)) return false; + // (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; + 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) || - !make_image(cmd, s.output, VK_FORMAT_R16G16B16A16_SFLOAT, ow, oh)) + !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(); @@ -568,7 +583,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in { static const bool square = env_switch("RX_VK_DLSS_SQUARE") != 0; const int32_t opc[3] = {(int32_t)ow, (int32_t)oh, square && !show_motion ? 1 : 0}; - const VkDescriptorImageInfo info[2] = {{s.point, s.output.view, VK_IMAGE_LAYOUT_GENERAL}, + 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; @@ -583,6 +598,10 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in descriptors_disturbed(); // (the program's own pushes are gone from the bind point) s.last_frame = now; s.last_jitter[0] = cam.jitter[0], s.last_jitter[1] = cam.jitter[1]; + static int evaluations; + if (++evaluations % 600 == 0) // (now and then: the jitter DLSS was given, in render pixels) + logf("vk: NVIDIA DLSS: %d frames drawn; this one's jitter %+.3f %+.3f px%s", evaluations, ep.InJitterOffsetX, + ep.InJitterOffsetY, motion ? "" : ", no motion"); std::memcpy(s.last_vp, cam.current, sizeof s.last_vp); s.jitter_w = rw, s.jitter_h = rh; if (s.reason) logf("vk: NVIDIA DLSS draws the upscale again (frame %llu)", (unsigned long long)now); diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index 9c9af7519..ab0dd66d6 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -12,8 +12,12 @@ struct Texture; /// Whether this build has DLSS in it (HOOKSHOT_NGX) bool dlss_built(); -/// DLSS asked for: the Insert menu's NVIDIA DLSS row, or RX_VK_DLSS=1 / 0 over it -bool dlss_wanted(); +/// What is asked for: 0 off, 1 DLSS (the game's 4/3 upscale), 2 DLAA (anti-aliasing at the render size, the 3D at full scale): +/// the Insert menu's NVIDIA DLSS row, or RX_VK_DLSS=0 / 1 / 2 over it +int dlss_mode(); +bool dlss_wanted(); // (dlss_mode() != 0) +/// DLAA: the 3D drawn at full scale, whatever the 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 diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 293d83e8c..61afc7a48 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -58,7 +58,10 @@ const char* tx_upscale(int v) { 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]; } -const char* tx_dlss(int v) { return !dlss_built() ? "Not in this build" : v ? "On" : "Off"; } +const char* tx_dlss(int v) { + static const char* t[] = {"Off", "DLSS", "DLAA"}; + return !dlss_built() ? "Not in this build" : t[std::clamp(v, 0, 2)]; +} const char* tx_percent(int v) { static char b[8]; snprintf(b, sizeof b, "%d%%", v); @@ -153,9 +156,10 @@ Row rows[] = { "faster."}, {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."}, - {TAB_GRAPHICS, "NVIDIA DLSS", &opts.dlss, 0, 1, 1, tx_dlss, - "NVIDIA's DLSS scales the 3D up in place of the game's own upscaler (NVIDIA GPUs, in a build made with NVIDIA's SDK). " - "Sharper and steadier edges; things that move by themselves can leave trails for now."}, + {TAB_GRAPHICS, "NVIDIA DLSS", &opts.dlss, 0, 2, 1, tx_dlss, + "NVIDIA's DLSS in place of the game's own upscaler (NVIDIA GPUs, in a build made with NVIDIA's SDK). DLSS scales the 3D " + "up from the Resolution row's size. DLAA draws the 3D at full size and only smooths its edges: sharpest, and slower. " + "Things that move by themselves can leave trails for now."}, {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, diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index f67f3104b..8773cb86e 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -11,7 +11,7 @@ struct Options { 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 upscale = 0; // to the window: 0 off (the plain copy to the window), 1 Catmull-Rom, 2 Lanczos - int dlss = 0; // NVIDIA DLSS in place of the game's own upscale (vk_dlss.cpp; a build with NVIDIA's SDK only) + int dlss = 0; // NVIDIA DLSS in place of the game's own upscale: 0 off, 1 DLSS, 2 DLAA (vk_dlss.cpp; a build with NVIDIA's SDK only) 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) diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index bc1cfc6d1..770452b51 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -3541,7 +3541,7 @@ void fill_replay_table(MkwVkRenderer& t) { t.nvn_register_texture = register_texture_queued; t.nvn_event_signal = event_signal_queued; t.option = [](int id) -> int { - return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? opts.resolution == 0 : id == 3 ? (opts.resolution == 1 ? 100 : opts.resolution == 2 ? 75 : opts.resolution == 3 ? 67 : opts.resolution == 4 ? 50 : 0) : id == 4 ? opts.pad_background : id == 5 ? opts.text_speed : id == 6 ? opts.mouse_native : + return id == 0 ? opts.realtime : id == 1 ? menu_is_open() : id == 2 ? opts.resolution == 0 && !dlss_full_scale() : id == 3 ? (dlss_full_scale() || opts.resolution == 1 ? 100 : opts.resolution == 2 ? 75 : opts.resolution == 3 ? 67 : opts.resolution == 4 ? 50 : 0) : 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 == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : id == 18 || id == 19 ? (int)dlss_jitter_size(id - 18) : 0; }; From 0854d221e0989f6943c7f3302e4af7057fcf95ed Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:34:45 -0500 Subject: [PATCH 05/52] vk: NVIDIA Reflex through VK_NV_low_latency2 (Insert menu: Off / On / On + Boost) No Streamline: the swapchain is made with latency mode on when the device has VK_NV_low_latency2 and present ids, its sleep mode follows the new NVIDIA Reflex row (RX_VK_REFLEX=0 / 1 / 2 over it; =-: the extensions not enabled), and each frame the driver gets its markers with the present's id: simulation start and input after vkLatencySleepNV, simulation end and render submit start at the game's present, render submit end before the present's submit, present start and end around vkQueuePresentKHR. The sleep follows the present on the game's presentation thread. Not used with frame generation (its own display thread). Every 300 frames the log has the driver's timings (vkGetLatencyTimingsNV), averaged, to compare On and Off. --- gpu/vk/vk_main.cpp | 6 ++ gpu/vk/vk_menu.cpp | 7 ++ gpu/vk/vk_menu.h | 1 + gpu/vk/vk_reflex.cpp | 222 +++++++++++++++++++++++++++++++++++++++++++ gpu/vk/vk_reflex.h | 31 ++++++ gpu/vk/vk_replay.cpp | 8 ++ 6 files changed, 275 insertions(+) create mode 100644 gpu/vk/vk_reflex.cpp create mode 100644 gpu/vk/vk_reflex.h diff --git a/gpu/vk/vk_main.cpp b/gpu/vk/vk_main.cpp index 81aa20b0c..bbca181b2 100644 --- a/gpu/vk/vk_main.cpp +++ b/gpu/vk/vk_main.cpp @@ -18,6 +18,7 @@ #include "gpu_choice.h" #include "vk_menu.h" #include "vk_dlss.h" +#include "vk_reflex.h" namespace vkx { #define VKX_DEFINE(f) PFN_##f f; @@ -238,6 +239,7 @@ bool create_device() { 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); @@ -288,6 +290,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; @@ -382,6 +385,7 @@ bool create_device() { vkGetDeviceQueue(dev.device, dev.queue_family, 0, &dev.queue); 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}; @@ -472,6 +476,7 @@ bool create_swapchain(uint32_t width, uint32_t height) { ci.presentMode = mode; 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) { @@ -500,6 +505,7 @@ bool create_swapchain(uint32_t width, uint32_t height) { vkDestroySwapchainKHR(dev.device, dev.swapchain, nullptr); } dev.swapchain = sc; + reflex_swapchain_created(sc); dev.swap_rendered = std::move(rendered); dev.swap_format = fmt.format; dev.swap_extent = {width, height}; diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 61afc7a48..fe1db2e9c 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -58,6 +58,10 @@ const char* tx_upscale(int v) { 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]; } +const char* tx_reflex(int v) { + static const char* t[] = {"Off", "On", "On + Boost"}; + return t[std::clamp(v, 0, 2)]; +} const char* tx_dlss(int v) { static const char* t[] = {"Off", "DLSS", "DLAA"}; return !dlss_built() ? "Not in this build" : t[std::clamp(v, 0, 2)]; @@ -160,6 +164,9 @@ Row rows[] = { "NVIDIA's DLSS in place of the game's own upscaler (NVIDIA GPUs, in a build made with NVIDIA's SDK). DLSS scales the 3D " "up from the Resolution row's size. DLAA draws the 3D at full size and only smooths its edges: sharpest, and slower. " "Things that move by themselves can leave trails for now."}, + {TAB_GRAPHICS, "NVIDIA Reflex", &opts.reflex, 0, 2, 1, tx_reflex, + "NVIDIA GPUs: the driver starts each frame as late as it can, so less time passes between your input and the screen. " + "Boost also keeps the GPU's clocks up. Not used with frame generation."}, {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, diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index 8773cb86e..290eb1703 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -11,6 +11,7 @@ struct Options { 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 upscale = 0; // to the window: 0 off (the plain copy to the window), 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; // NVIDIA DLSS in place of the game's own upscale: 0 off, 1 DLSS, 2 DLAA (vk_dlss.cpp; a build with NVIDIA's SDK only) 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) diff --git a/gpu/vk/vk_reflex.cpp b/gpu/vk/vk_reflex.cpp new file mode 100644 index 000000000..7071f0702 --- /dev/null +++ b/gpu/vk/vk_reflex.cpp @@ -0,0 +1,222 @@ +// (hookshot) NVIDIA Reflex through VK_NV_low_latency2, without NVIDIA's Streamline (vk_reflex.h). +// +// The swapchain is made with latency mode on (VkSwapchainLatencyCreateInfoNV) and its sleep mode set from the Insert menu's +// NVIDIA Reflex row (Off / On / On + Boost; RX_VK_REFLEX=0 / 1 / 2 over it). 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. +// +// Every 300 frames the log gives the driver's own timings (vkGetLatencyTimingsNV) averaged: simulation start to present end, +// and present end to the GPU's end of rendering, so On and Off can be compared. RX_VK_REFLEX_LOG=0: none. +#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; + VkPhysicalDevicePresentIdFeaturesKHR present_id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_FEATURES_KHR}; + VkSwapchainLatencyCreateInfoNV latency{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 extension and its feature, a swapchain made with it, and no frame generation (its own display thread) +bool on() { + Reflex& r = rx(); + return r.ready && r.swapchain && !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 quiet = getenv("RX_VK_REFLEX_LOG") && !atoi(getenv("RX_VK_REFLEX_LOG")); + Reflex& r = rx(); + if (quiet || !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"); +} + +void reflex_swapchain_info(VkSwapchainCreateInfoKHR& ci) { + Reflex& r = rx(); + if (!r.ready) return; + r.latency.pNext = ci.pNext; + r.latency.latencyModeEnable = VK_TRUE; + ci.pNext = &r.latency; +} + +void reflex_swapchain_created(VkSwapchainKHR swapchain) { + Reflex& r = rx(); + if (!r.ready) return; + std::lock_guard lk(r.mu); + r.swapchain = swapchain, r.mode = -1, r.frame_marked = false; + apply_mode(); +} + +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..a5ab6c19f --- /dev/null +++ b/gpu/vk/vk_reflex.h @@ -0,0 +1,31 @@ +// 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(); +/// Into a swapchain's creation: latency mode on (the chained struct lives until the next call) +void reflex_swapchain_info(VkSwapchainCreateInfoKHR& ci); +/// 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 770452b51..f41617a45 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -26,6 +26,7 @@ #include #include "vk_dlss.h" +#include "vk_reflex.h" #include "vk_draw.h" #include "vk_dump.h" #include "vk_gprof.h" @@ -2476,6 +2477,10 @@ void log_frame_stats() { 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 + 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; @@ -3054,14 +3059,17 @@ 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; pi.pSwapchains = &dev.swapchain; pi.pImageIndices = &index; res = vkQueuePresentKHR(dev.queue, &pi); + reflex_presented(); if (res == VK_ERROR_OUT_OF_DATE_KHR || res == VK_SUBOPTIMAL_KHR) create_swapchain(dev.swap_extent.width, dev.swap_extent.height); } From cc717e751180b227687feddb49944c644609e6f9 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:39:29 -0500 Subject: [PATCH 06/52] Reflex: the swapchain's sleep mode set after its setup is complete tests/test_present_semaphores.py compiles the swapchain's setup, from its image query to the log line, with stubs of its own: the call to Reflex sat inside that span. It now follows the log line, where the swapchain is the device's in full. --- gpu/vk/vk_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpu/vk/vk_main.cpp b/gpu/vk/vk_main.cpp index bbca181b2..0bc4aaeb8 100644 --- a/gpu/vk/vk_main.cpp +++ b/gpu/vk/vk_main.cpp @@ -505,7 +505,6 @@ bool create_swapchain(uint32_t width, uint32_t height) { vkDestroySwapchainKHR(dev.device, dev.swapchain, nullptr); } dev.swapchain = sc; - reflex_swapchain_created(sc); dev.swap_rendered = std::move(rendered); dev.swap_format = fmt.format; dev.swap_extent = {width, height}; @@ -514,6 +513,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; } From 5953b03020f781e794f2a9c9ccb5bc6fca4d3b19 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:50:36 -0500 Subject: [PATCH 07/52] DLSS: render sizes down to a third of the output (Resolution 50% at 4K and the like) With #849 the output is 2048x1152, 2560x1440 or 3840x2160 and the Resolution row scales the 3D against it, so the upscale's ratio is no longer 4/3. The sizes already follow the dispatch's input and output, and the feature is made again for a new output size or a render size outside its mode's range; the check on the ratio now takes up to 3x (DLSS's Ultra Performance), and a render size equal to the output's runs as DLAA. --- gpu/vk/vk_dlss.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 4b826fca6..273e1b03a 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -496,7 +496,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in 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 * 2 || oh > rh * 2) + 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; From 64d65f733bb21555e1f4daa3aed51c3db0a8c1c2 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:26:27 -0500 Subject: [PATCH 08/52] DLSS: the gameplay network's full-size pass too (DLAA, and Resolution at the output's size) With the 3D drawn at the picture's size (DLAA, or the Resolution row's top step since #849) the game runs the same network as a pass at one size (9dc98871eb7fc101, 2560x1440 in and out) instead of its 4/3 upscale, and DLSS was never asked. That pass is replaced as well: render size equal to the output, DLSS's DLAA mode. Checked at 2560x1440: 2560x1440 -> 2560x1440, mode DLAA. --- gpu/vk/vk_dlss.cpp | 6 ++++-- gpu/vk/vk_draw.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 273e1b03a..430b460db 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -68,7 +68,9 @@ bool dlss_full_scale() { return dlss_mode() == 2; } namespace { -constexpr uint64_t kGameplayUpscale = 0xfd029a317a9a4b94ull; +// (the gameplay network: the 4/3 upscale when the 3D is drawn smaller than the picture, and a pass at the same size when it +// is drawn at full size, the Resolution row's top step and DLAA's) +constexpr uint64_t kGameplayUpscale = 0xfd029a317a9a4b94ull, kGameplayFullSize = 0x9dc98871eb7fc101ull; struct Image { VkImage image = VK_NULL_HANDLE; @@ -480,7 +482,7 @@ uint32_t dlss_jitter_size(int axis) { bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in_view, Texture* out, VkImageView out_view, uint32_t out_level) { - if (key != kGameplayUpscale || !dlss_wanted()) return false; + if ((key != kGameplayUpscale && key != kGameplayFullSize) || !dlss_wanted()) return false; State& s = st(); std::lock_guard lk(s.mu); free_dead(false); diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index ab6a15ea4..a018d0804 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -5399,7 +5399,7 @@ static bool typed_upscale(VkCommandBuffer cmd, VkImageView input_view, VkImageVi /// (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) { - if (p.key != 0xfd029a317a9a4b94ull || !dlss_wanted() || p.stages.empty()) return false; + if (!gameplay_network(p.key) || !dlss_wanted() || p.stages.empty()) return false; const Stage& st = p.stages[0]; 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') From 4b52803f680f84ce6bd322ff8c20c9ab4b00a808 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 23:59:08 -0500 Subject: [PATCH 09/52] DLSS: one fetch script for NVIDIA's SDK, Linux and Windows scripts/ngx_sdk.py takes over from scripts/linux/ngx-sdk.sh, which is now a thin wrapper around it: one file table per platform at the pinned NVIDIA/DLSS release (v310.9.16), each file checked against its SHA-256 before it is kept. On Windows it fetches LICENSE.txt, the headers, x64/nvsdk_ngx_s.lib and rel/nvngx_dlss.dll; --game copies the runtime into /ngx. It prints NVIDIA's license notice first and downloads nothing until the license is accepted (--accept-nvidia-license, or a typed yes). Nothing of NVIDIA's is committed or bundled. tests/test_ngx_sdk.py checks the table, the hash checks and the license gate without the network. --- scripts/linux/ngx-sdk.sh | 74 ++------------- scripts/ngx_sdk.py | 200 +++++++++++++++++++++++++++++++++++++++ tests/test_ngx_sdk.py | 156 ++++++++++++++++++++++++++++++ 3 files changed, 366 insertions(+), 64 deletions(-) create mode 100644 scripts/ngx_sdk.py create mode 100644 tests/test_ngx_sdk.py diff --git a/scripts/linux/ngx-sdk.sh b/scripts/linux/ngx-sdk.sh index 339c81e16..e820587d3 100755 --- a/scripts/linux/ngx-sdk.sh +++ b/scripts/linux/ngx-sdk.sh @@ -1,69 +1,15 @@ #!/bin/bash -# ngx-sdk.sh: NVIDIA's DLSS SDK files for a local build with NVIDIA DLSS Super Resolution (HOOKSHOT_NGX_SDK, gpu/vk/vk_dlss.cpp). +# 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). # -# NVIDIA's files are not part of Hookshot and are never committed: they are under NVIDIA's own license (the DLSS SDK's -# LICENSE.txt, fetched beside them; read it before using them). This fetches, from NVIDIA's DLSS repository at one pinned -# release, each file checked against its SHA-256: -# - the SDK headers and libnvsdk_ngx.a, the build's part, into (default build-linux/ngx-sdk) -# - libnvidia-ngx-dlss.so, the DLSS runtime the driver loads, into when given (a game folder's ngx/, or -# $XDG_DATA_HOME/hookshot/ngx; RX_VK_DLSS_DIR names another) -# The NGX core itself (libnvidia-ngx.so) comes with NVIDIA's Linux driver. -# -# Usage: scripts/linux/ngx-sdk.sh [] [] +# 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 -COMMIT=9981c6350dc58c0ce21f024fd65e31e851c31689 # (v310.9.16) -BASE=https://raw.githubusercontent.com/NVIDIA/DLSS/$COMMIT ROOT=$(cd "$(dirname "$0")/../.." && pwd) -SDK=${1:-$ROOT/build-linux/ngx-sdk} -RUNTIME=${2:-} -FILES=" - LICENSE.txt d4216e39ebef5f9b50a6712ebb37beeb5379862a67733a9999c651f21592aaf0 - lib/Linux_x86_64/libnvsdk_ngx.a d3c50bb0d9624fc35e502a8e929b8fc1e2b505017ebdb84c0d5b627e11a235df - 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 -" -RUNTIME_FILE="lib/Linux_x86_64/rel/libnvidia-ngx-dlss.so.310.9.16 30982c937afed85c96b8d313323de9a650a2eec6de1bb0f714b21750f4ce98f6" - -fetch() { # (path in the repository, its SHA-256, where it goes) - local tmp="$3.part" - mkdir -p "$(dirname "$3")" - if [ -f "$3" ] && echo "$2 $3" | sha256sum -c --status; then return; fi - curl -fsSL -o "$tmp" "$BASE/$1" - if ! echo "$2 $tmp" | sha256sum -c --status; then - rm -f "$tmp"; echo "ngx-sdk: $1 is not the file expected (SHA-256)"; exit 1 - fi - mv "$tmp" "$3" -} -echo "$FILES" | while read -r path sum; do - [ -n "$path" ] || continue - case "$path" in lib/*) dst=$SDK/lib/$(basename "$path") ;; *) dst=$SDK/$path ;; esac - fetch "$path" "$sum" "$dst" -done -echo "ngx-sdk: the SDK's headers and library in $SDK (NVIDIA's license: $SDK/LICENSE.txt)" -if [ -n "$RUNTIME" ]; then - set -- $RUNTIME_FILE - fetch "$1" "$2" "$RUNTIME/$(basename "$1")" - echo "ngx-sdk: the DLSS runtime in $RUNTIME" -fi +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..8ae2b78dc --- /dev/null +++ b/scripts/ngx_sdk.py @@ -0,0 +1,200 @@ +"""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 ] [] + : 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) + --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): + """[(path in NVIDIA's repository, SHA-256, path in the SDK folder)] for one platform""" + spec = PLATFORMS[platform] + return [(p, s, destination(p)) for p, s in COMMON + [spec["library"], spec["runtime"]]] + + +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() + + +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) 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): + """Every file of that platform into the SDK folder""" + for path, digest, rel in files(platform): + 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") + a = p.parse_args(argv) + 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) + 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/test_ngx_sdk.py b/tests/test_ngx_sdk.py new file mode 100644 index 000000000..d5a02ccd8 --- /dev/null +++ b/tests/test_ngx_sdk.py @@ -0,0 +1,156 @@ +"""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): + 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_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: 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: 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() From 5fb1cecfe65562b5b9c29da6f3d83bf68ab26ec4 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:02:26 -0500 Subject: [PATCH 10/52] DLSS: hookshot_vk.dll with NVIDIA's NGX library on Windows nx2/build.py takes HOOKSHOT_NGX_SDK on Windows too, when the folder has lib/nvsdk_ngx_s.lib (NVIDIA's static loader, built for Microsoft's static runtime, /MT), and links it into hookshot_vk.dll with -ladvapi32 (its registry reads). With llvm-mingw, Microsoft's C++ runtime it names is left out (/nodefaultlib:libcpmt) and two objects stand in for what it needs of it: host/mingw_msvc_shims.c (the /GS cookie and handler and _fltused, as for the static FFmpeg) and the new gpu/vk/vk_ngx_msvc_rt.cpp (__GSHandlerCheck_EH on the system's __CxxFrameHandler3, thread-safe statics on an SRW lock and a condition variable, Microsoft's operator new and delete, std::_Xbad_alloc and std::_Xlength_error). Neither is compiled without the SDK, nor for the MSVC build (HOOKSHOT_MSVC=1), which has Microsoft's runtime itself. Cross-built with llvm-mingw: the DLL links, and imports only KERNEL32, USER32, GDI32, ADVAPI32 and api-ms-win-crt-*. Without the SDK it is the same objects as before, with the same imports and exports. --- gpu/vk/vk_ngx_msvc_rt.cpp | 94 ++++++++++++++++++++++++++++++++ host/mingw_msvc_shims.c | 3 +- nx2/build.py | 50 +++++++++++++---- tests/test_shader_stamp_cache.py | 67 +++++++++++++++++++++-- 4 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 gpu/vk/vk_ngx_msvc_rt.cpp 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/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/nx2/build.py b/nx2/build.py index ce69ccd2a..938f6920f 100644 --- a/nx2/build.py +++ b/nx2/build.py @@ -135,13 +135,35 @@ def tree_headers(*roots, skip=()): return h.hexdigest().encode() -# (linux) HOOKSHOT_NGX_SDK=: the Vulkan back end with NVIDIA DLSS Super Resolution, from NVIDIA's NGX SDK fetched into -# that folder by scripts/linux/ngx-sdk.sh (its include/ and lib/libnvsdk_ngx.a; never committed, NVIDIA's license). Off without it. -NGX_SDK = os.environ.get("HOOKSHOT_NGX_SDK", "") -if NGX_SDK and not (LINUX and os.path.exists(os.path.join(NGX_SDK, "lib", "libnvsdk_ngx.a"))): - print("build: HOOKSHOT_NGX_SDK=%s has no lib/libnvsdk_ngx.a (scripts/linux/ngx-sdk.sh), or this is not Linux: built " - "without NVIDIA DLSS" % NGX_SDK, flush=True) - NGX_SDK = "" +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(): @@ -152,7 +174,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) @@ -720,7 +742,7 @@ 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" + os.path.join(cpm_package("vulkanmemoryallocator"), "include")] if cpm_package("vulkanmemoryallocator") else []) - # (NVIDIA DLSS, a local build's option: scripts/linux/ngx-sdk.sh. Only vk_dlss.cpp sees NVIDIA's headers; without them + # (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: @@ -739,6 +761,12 @@ def main(): ([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)) + 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")] @@ -886,10 +914,10 @@ def main(): 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] + \ - ([os.path.join(NGX_SDK, "lib", "libnvsdk_ngx.a")] if NGX_SDK else []) + ["-ldl", "-lpthread", "-lm"] + ([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/tests/test_shader_stamp_cache.py b/tests/test_shader_stamp_cache.py index 31ef75a0d..50771d5d2 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,20 +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, - "NGX_SDK": "", # (no NVIDIA NGX SDK: vk_dlss.cpp built without DLSS, as by default) + "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. @@ -157,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__) From ee8851ac98b35baa3b40d668b7704d506bec1607 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:03:14 -0500 Subject: [PATCH 11/52] DLSS: Windows paths for NVIDIA's runtime On Windows, vk_dlss.cpp looks for nvngx_dlss.dll in RX_VK_DLSS_DIR, the game folder's ngx\ and %LOCALAPPDATA%\Hookshot\ngx (Linux keeps the game folder's ngx/ and $XDG_DATA_HOME/hookshot/ngx), and its log names the file of the platform it runs on. The paths NGX gets are converted from the ANSI code page (MultiByteToWideChar, CP_ACP), as GetModuleFileNameA and getenv give them, so a game folder with letters outside ASCII is still found. An empty RX_VK_DLSS_DIR is ignored. A comment says why NGX's core, _nvngx.dll, is never put beside the game: it is the driver's. --- gpu/vk/vk_dlss.cpp | 42 ++++++++++++++++++++++++++++++++++-------- gpu/vk/vk_dlss.h | 2 +- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 430b460db..e1edc7ad8 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -14,13 +14,17 @@ // 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=1 / 0: on / off over the Insert menu's row. RX_VK_DLSS_DIR=: where libnvidia-ngx-dlss.so.* is -// (else the game folder's ngx/, then $XDG_DATA_HOME/hookshot/ngx). RX_VK_DLSS_PRESET=: DLSS's preset (default K). +// RX_VK_DLSS=1 / 0: on / off over the Insert menu's row. RX_VK_DLSS_DIR=: where DLSS's runtime is looked for +// first (Windows: nvngx_dlss.dll, else in the game folder's ngx\, then %LOCALAPPDATA%\Hookshot\ngx; Linux: +// libnvidia-ngx-dlss.so.*, else in the game folder's ngx/, then $XDG_DATA_HOME/hookshot/ngx). RX_VK_DLSS_PRESET=: +// DLSS's preset (default K). // RX_VK_DLSS_DEBUG=motion: the motion vectors shown instead of the picture. 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/linux/ngx-sdk.sh) and -// builds with HOOKSHOT_NGX_SDK, which compiles this file with HOOKSHOT_NGX. Without it only the stubs at the end are here. +// 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 @@ -197,19 +201,41 @@ std::string game_dir() { std::string dir(exe, n); return dir.substr(0, dir.find_last_of("\\/") + 1); } -std::wstring wide(const std::string& s) { return std::wstring(s.begin(), s.end()); } +/// A path for NGX, which takes wide strings. Windows: the narrow paths here are the ANSI code page's (GetModuleFileNameA, getenv), +/// so they are converted from it, and a game folder with letters outside ASCII is still the right folder +std::wstring wide(const std::string& s) { +#ifdef _WIN32 + if (s.empty()) return {}; + const int n = MultiByteToWideChar(CP_ACP, 0, s.data(), (int)s.size(), nullptr, 0); + std::wstring w(n > 0 ? n : 0, L'\0'); + if (n > 0) MultiByteToWideChar(CP_ACP, 0, s.data(), (int)s.size(), w.data(), n); + return w; +#else + 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) +#else +constexpr const char* kRuntime = "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 libnvidia-ngx-dlss.so. is looked for (NGX takes the newest it finds), and its own files go + // where DLSS's runtime (kRuntime) is looked for (NGX takes the newest it finds), and its own files go std::vector dirs; - if (const char* e = getenv("RX_VK_DLSS_DIR")) dirs.push_back(e); + if (const char* e = getenv("RX_VK_DLSS_DIR"); e && *e) dirs.push_back(e); dirs.push_back(game_dir() + "ngx"); +#ifdef _WIN32 + if (const char* l = getenv("LOCALAPPDATA"); l && *l) dirs.push_back(std::string(l) + "\\Hookshot\\ngx"); +#else if (const char* x = getenv("XDG_DATA_HOME"); x && *x) dirs.push_back(std::string(x) + "/hookshot/ngx"); else if (const char* h = getenv("HOME")) dirs.push_back(std::string(h) + "/.local/share/hookshot/ngx"); +#endif std::vector wdirs; for (const auto& d : dirs) wdirs.push_back(wide(d)); std::vector paths; @@ -247,7 +273,7 @@ bool ngx_init() { std::string where; for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + d; if (needs_driver) logf("vk: NVIDIA DLSS: needs NVIDIA's driver %u.%u or newer", major, minor); - else logf("vk: NVIDIA DLSS: not available: no libnvidia-ngx-dlss.so in %s (scripts/linux/ngx-sdk.sh fetches it)", where.c_str()); + else logf("vk: NVIDIA DLSS: not available: no %s in %s (scripts/ngx_sdk.py fetches it)", kRuntime, where.c_str()); return false; } if (NVSDK_NGX_FAILED(NVSDK_NGX_VULKAN_AllocateParameters(&s.params)) || !s.params) return false; diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index ab0dd66d6..7e2915538 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -1,5 +1,5 @@ // 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/linux/ngx-sdk.sh); without it these say DLSS is not there and the game's network runs. +// 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 From cf30c2ce012937bb59aa351a4cf022a30b7aa0a7 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:05:43 -0500 Subject: [PATCH 12/52] DLSS: an opt-in NVIDIA DLSS tick in the Hookshot Builder A new tick, "NVIDIA DLSS (downloads NVIDIA's SDK under NVIDIA's license)", off by default. Ticking it shows NVIDIA's license and stays ticked only when it is accepted. The build then passes HOOKSHOT_NGX_SDK (the builder's ngx-sdk folder) through Runner.cs and runs make_game.py with --fetch-ngx-sdk --accept-nvidia-license, which fetches NVIDIA's SDK with scripts/ngx_sdk.py before anything else (only what is missing or not the file expected). Without the tick, HOOKSHOT_NGX_SDK is not passed on. An update of a folder built with DLSS builds it with DLSS again. make_game.py places DLSS's runtime in the game folder's ngx folder whenever the build had the SDK (nvngx_dlss.dll on Windows, libnvidia-ngx-dlss.so on Linux), never beside the exe. tests/test_make_game_ngx.py checks both. The builder's C# is not compiled here (no .NET on this PC). --- builder/HookshotBuilder/MainWindow.xaml | 10 ++ builder/HookshotBuilder/MainWindow.xaml.cs | 34 ++++++- builder/HookshotBuilder/Runner.cs | 12 ++- builder/HookshotBuilder/Storage.cs | 4 + nx2/make_game.py | 53 +++++++++++ tests/test_make_game_ngx.py | 105 +++++++++++++++++++++ 6 files changed, 210 insertions(+), 8 deletions(-) create mode 100644 tests/test_make_game_ngx.py 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 d68b5141e..7da4f2147 100644 --- a/builder/HookshotBuilder/MainWindow.xaml.cs +++ b/builder/HookshotBuilder/MainWindow.xaml.cs @@ -255,6 +255,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"); @@ -265,6 +266,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; @@ -287,7 +289,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)); } @@ -675,7 +678,23 @@ 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) return; + if (MessageBox.Show(this, DlssLicense, "NVIDIA DLSS", MessageBoxButton.YesNo, MessageBoxImage.Information) != MessageBoxResult.Yes) + DlssTick.IsChecked = false; } static readonly (string Id, string Build, string Update)[] Stages = @@ -705,7 +724,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); } @@ -742,7 +762,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(); @@ -1119,7 +1140,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 a6f948187..84082d5f2 100644 --- a/builder/HookshotBuilder/Storage.cs +++ b/builder/HookshotBuilder/Storage.cs @@ -33,6 +33,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/nx2/make_game.py b/nx2/make_game.py index e05bf0a38..4f6f9c5dd 100644 --- a/nx2/make_game.py +++ b/nx2/make_game.py @@ -841,6 +841,53 @@ 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 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") @@ -856,6 +903,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 @@ -889,6 +939,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") @@ -1080,6 +1132,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/tests/test_make_game_ngx.py b/tests/test_make_game_ngx.py new file mode 100644 index 000000000..8bd3841a9 --- /dev/null +++ b/tests/test_make_game_ngx.py @@ -0,0 +1,105 @@ +"""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 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() From ee1c1bb3d92f2742ab52436168f627dc658af026 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:07:14 -0500 Subject: [PATCH 13/52] DLSS: a CI link check of hookshot_vk.dll with NVIDIA's SDK on Windows windows-builder.yml gets a job of its own, vulkan-dlss, beside the Vulkan back end's: it fetches NVIDIA's DLSS SDK with scripts/ngx_sdk.py (the new --build-only: the headers and nvsdk_ngx_s.lib, not the 59 MB runtime), builds hookshot_vk.dll with it through llvm-mingw, checks the build said it was with NVIDIA DLSS, and fails when the DLL imports anything but KERNEL32, USER32, GDI32, ADVAPI32 and api-ms-win-crt-*. Nothing is uploaded, and the release job does not need it. actionlint 1.7.12 (with shellcheck) passes. --- .github/workflows/windows-builder.yml | 69 +++++++++++++++++++++++++++ scripts/ngx_sdk.py | 20 ++++---- tests/test_ngx_sdk.py | 10 +++- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/.github/workflows/windows-builder.yml b/.github/workflows/windows-builder.yml index 815818770..e8a9af0a4 100644 --- a/.github/workflows/windows-builder.yml +++ b/.github/workflows/windows-builder.yml @@ -184,6 +184,75 @@ jobs: path: ${{ runner.temp }}/vk-work/sa/build/hookshot_vk.dll if-no-files-found: error + # (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: nothing of it is uploaded, and + # nothing needs it. scripts/ngx_sdk.py fetches the SDK at its pinned release, SHA-256 checked, without DLSS's runtime) + vulkan-dlss: + name: Windows Vulkan back end with NVIDIA DLSS (llvm-mingw, link check) + if: github.ref_name != 'builder-parts' + 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 + release: name: Release assets # (off until the maintainer switches it on: releases are published by hand with HookshotBuilder.exe and hookshot-setup.txt, diff --git a/scripts/ngx_sdk.py b/scripts/ngx_sdk.py index 8ae2b78dc..340067217 100644 --- a/scripts/ngx_sdk.py +++ b/scripts/ngx_sdk.py @@ -10,12 +10,13 @@ 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 ] [] + [--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=. """ @@ -92,10 +93,10 @@ def destination(path): return path -def files(platform): - """[(path in NVIDIA's repository, SHA-256, path in the SDK folder)] for one platform""" +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"]]] + return [(p, s, destination(p)) for p, s in COMMON + [spec["library"]] + ([spec["runtime"]] if runtime else [])] def library(sdk, platform): @@ -139,9 +140,9 @@ def fetch_one(path, digest, dst, opener=urllib.request.urlopen): return True -def fetch(sdk, platform, opener=urllib.request.urlopen, say=print): - """Every file of that platform into the SDK folder""" - for path, digest, rel in files(platform): +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) @@ -176,7 +177,10 @@ def main(argv=None): 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): @@ -184,7 +188,7 @@ def main(argv=None): file=sys.stderr, flush=True) return 2 try: - fetch(sdk, a.platform) + 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 diff --git a/tests/test_ngx_sdk.py b/tests/test_ngx_sdk.py index d5a02ccd8..e214d4b17 100644 --- a/tests/test_ngx_sdk.py +++ b/tests/test_ngx_sdk.py @@ -62,6 +62,12 @@ def test_linux_files(self): 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")} @@ -95,7 +101,7 @@ def setUp(self): def test_fetch_checks_and_skips_what_is_there(self): opener = Opener(self.good) - with mock.patch.object(ngx_sdk, "files", lambda platform: self.table): + 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") @@ -105,7 +111,7 @@ def test_fetch_checks_and_skips_what_is_there(self): 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: self.table): + 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") From 3c764d3b597509e37d2cdab6cf2a70b29c6b73c5 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:08:00 -0500 Subject: [PATCH 14/52] DLSS: docs for Windows (and Linux) builds with NVIDIA's SDK readme.md gets an NVIDIA DLSS section under Build from source: the Builder's tick, the command line on Windows (scripts/ngx_sdk.py, HOOKSHOT_NGX_SDK, where nvngx_dlss.dll goes and where the game looks), Linux, NVIDIA's license, and never to share a game folder built with NVIDIA's files or to put _nvngx.dll beside the game. Credits name NVIDIA DLSS. CONTRIBUTING.md says how to build with the SDK, that nothing of NVIDIA's is ever committed, bundled or uploaded, and what to update when the pinned release changes. --- CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ readme.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 599c7dc1e..b69fa01f9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,6 +88,30 @@ 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` job checks only that `hookshot_vk.dll` still links with the SDK, 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 +243,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/readme.md b/readme.md index 0289f121d..6f25ff27d 100644 --- a/readme.md +++ b/readme.md @@ -353,6 +353,32 @@ The game folder runs under Proton or on Windows. +
+NVIDIA DLSS (optional, NVIDIA's SDK under NVIDIA's license) + +A game built with NVIDIA's DLSS SDK has the Graphics tab's **NVIDIA DLSS** row (Off, DLSS, DLAA) on NVIDIA RTX cards: +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`. The game also looks in `%LOCALAPPDATA%\Hookshot\ngx`, and first in + `RX_VK_DLSS_DIR` when it is set. 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 game's log says whether DLSS started, and why not when it did not (`vk: NVIDIA DLSS:` lines in `hookshot_log.txt`). + +
+
Switches for testing (hookshot_env.txt) @@ -408,6 +434,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. From 83c39701bc3c288f71bf62be9fa3802001fc49fa Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 00:56:05 -0500 Subject: [PATCH 15/52] Menu: DLSS in an Upscaler row, the scaling rows in the picture's order The Graphics tab's three scales read top to bottom now: Output resolution, the Upscaler (Game's own, NVIDIA DLSS, NVIDIA DLAA: the NVIDIA DLSS row's choice), Render resolution, Window scaling filter (the Upscaling row), Sharpness. NVIDIA Reflex moves up beside Frame pacing. With DLSS the Render resolution row steps through DLSS's modes, Auto, Quality (67%), Balanced (58%), Performance (50%) and Ultra Performance (33%), each a twelfth of the output (two new values, 5 and 6; 1 to 4 keep their shares). With DLAA it shows Native (DLAA) and does not step. Another Upscaler moves the resolution to the nearest of its steps. DLSS and DLAA are offered only where they can run: a build with NVIDIA's SDK, NGX's extensions on the device, and NGX's answer (asked once when the menu opens: dlss_probe). The help says why not (not in this build, an NVIDIA RTX GPU, a newer driver, DLSS's runtime missing), and dlss_mode is 0 there, so a saved DLAA does not hold the 3D at full scale on a GPU without DLSS. The saved choice is kept. Reflex is offered only with VK_NV_low_latency2. Quality presets leave the Upscaler alone; with DLSS their resolution becomes DLSS's nearest mode. Saved options still load: resolution and upscaling keep their keys, the Upscaler is saved as dlss and the NVIDIA DLSS row's nvidia_dlss is read as it. tests/test_upscaler_rows checks the steps, the names and the keys. --- gpu/vk/upscaler_rows.h | 87 +++++++++++++++++++++ gpu/vk/vk_dlss.cpp | 46 ++++++++++- gpu/vk/vk_dlss.h | 10 ++- gpu/vk/vk_menu.cpp | 143 +++++++++++++++++++++++++++-------- gpu/vk/vk_menu.h | 8 +- gpu/vk/vk_reflex.cpp | 2 + gpu/vk/vk_reflex.h | 2 + readme.md | 27 ++++--- tests/test_upscaler_rows.cpp | 59 +++++++++++++++ tests/test_upscaler_rows.py | 63 +++++++++++++++ 10 files changed, 396 insertions(+), 51 deletions(-) create mode 100644 gpu/vk/upscaler_rows.h create mode 100644 tests/test_upscaler_rows.cpp create mode 100644 tests/test_upscaler_rows.py diff --git a/gpu/vk/upscaler_rows.h b/gpu/vk/upscaler_rows.h new file mode 100644 index 000000000..e5f8017ed --- /dev/null +++ b/gpu/vk/upscaler_rows.h @@ -0,0 +1,87 @@ +/* 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 options file's key for a row's saved value, read back under the names it had before: the Upscaler's choice was saved as +/// nvidia_dlss (the NVIDIA DLSS row), and is dlss now (as opts.dlss and RX_VK_DLSS). nullptr: no other name +inline const char* ini_alias(const char* key) { + const char* old = "nvidia_dlss"; + for (int i = 0;; i++) { + if (key[i] != old[i]) return nullptr; + if (!key[i]) return "dlss"; + } +} + +} // namespace mkwvk::upscaler_rows diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index e1edc7ad8..e73489b42 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -14,7 +14,8 @@ // 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=1 / 0: on / off over the Insert menu's row. RX_VK_DLSS_DIR=: where DLSS's runtime is looked for +// 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). RX_VK_DLSS_DIR=: where DLSS's runtime is looked for // first (Windows: nvngx_dlss.dll, else in the game folder's ngx\, then %LOCALAPPDATA%\Hookshot\ngx; Linux: // libnvidia-ngx-dlss.so.*, else in the game folder's ngx/, then $XDG_DATA_HOME/hookshot/ngx). RX_VK_DLSS_PRESET=: // DLSS's preset (default K). @@ -60,8 +61,19 @@ int env_switch(const char* name) { // (-1: unset) } } // 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_built()) return 0; + 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); } @@ -73,7 +85,7 @@ bool dlss_full_scale() { return dlss_mode() == 2; } namespace { // (the gameplay network: the 4/3 upscale when the 3D is drawn smaller than the picture, and a pass at the same size when it -// is drawn at full size, the Resolution row's top step and DLAA's) +// is drawn at full size, the Render resolution row's 100% and DLAA's) constexpr uint64_t kGameplayUpscale = 0xfd029a317a9a4b94ull, kGameplayFullSize = 0x9dc98871eb7fc101ull; struct Image { @@ -257,10 +269,12 @@ bool ngx_init() { 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; } int available = 0, needs_driver = 0; @@ -272,13 +286,24 @@ bool ngx_init() { if (!available) { std::string where; for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + d; + // (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; + s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_FeatureInitResult, &init); + bool runtime = false; + std::error_code ec; + for (const auto& d : dirs) + for (const auto& f : std::filesystem::directory_iterator(d, ec)) + runtime |= f.path().filename().string().rfind(kRuntime, 0) == 0; if (needs_driver) logf("vk: NVIDIA DLSS: needs NVIDIA's driver %u.%u or newer", major, minor); - else logf("vk: NVIDIA DLSS: not available: no %s in %s (scripts/ngx_sdk.py fetches it)", kRuntime, where.c_str()); + 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 from %s)", dirs.size() > 1 ? "the folders searched" : dirs[0].c_str()); s.ngx_ok = true; + g_why = DLSS_OK; return true; } @@ -457,12 +482,24 @@ void dlss_device_extensions(std::vector& want, const std::vector= 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, @@ -644,6 +681,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in #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) {} diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index 7e2915538..6d0fcc8b2 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -12,11 +12,17 @@ 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 (the game's 4/3 upscale), 2 DLAA (anti-aliasing at the render size, the 3D at full scale): -/// the Insert menu's NVIDIA DLSS row, or RX_VK_DLSS=0 / 1 / 2 over it +/// 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 Resolution row says (rx_option 2 and 3) +/// 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); diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 2493ba630..bb265e2b8 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -17,6 +17,8 @@ #include "oot_cheat_items.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" #ifndef HOOKSHOT_VERSION // (nx2/build.py gives it to this file: nx2/version.py) #define HOOKSHOT_VERSION "unknown" #endif @@ -42,7 +44,11 @@ struct Row { int lo, hi, step; const char* (*text)(int v); const char* help; // (shown under the list while the row is selected) + // (a help that depends on the other rows or on the GPU: help_at(-1) is the one to show now, help_at(0, 1, ...) every + // one it can be, until nullptr, so the menu keeps the height of the tallest: measure_help) + const char* (*help_at)(int variant) = nullptr; }; +const char* help_of(const Row& r, int variant) { return r.help_at ? r.help_at(variant) : variant <= 0 ? r.help : nullptr; } const char* tx_framegen(int v) { static const char* t[] = {"Off", "", "2x", "3x", "4x", "5x", "6x", "7x", "8x"}; return t[v]; @@ -61,11 +67,45 @@ const char* tx_upscale(int v) { } const char* tx_reflex(int v) { static const char* t[] = {"Off", "On", "On + Boost"}; - return t[std::clamp(v, 0, 2)]; + return t[reflex_available() ? std::clamp(v, 0, 2) : 0]; } -const char* tx_dlss(int v) { - static const char* t[] = {"Off", "DLSS", "DLAA"}; - return !dlss_built() ? "Not in this build" : t[std::clamp(v, 0, 2)]; +const char* reflex_help(int variant) { + static const char* const base = "NVIDIA GPUs: the driver starts each frame as late as it can, so less time passes between " + "your input and the screen. Boost also keeps the GPU's clocks up. Not used with frame " + "generation."; + static const char* const none = "NVIDIA GPUs: the driver starts each frame as late as it can, so less time passes between " + "your input and the screen. Not available here: it needs an NVIDIA GPU whose driver has " + "VK_NV_low_latency2."; + if (variant > 1) return nullptr; + return (variant < 0 ? reflex_available() : variant == 0) ? base : none; +} +// 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)]; +} +const char* upscaler_help(int variant) { + static const char* const base = "What scales the 3D up to the output. Game's own: the game's own network. NVIDIA DLSS: " + "NVIDIA's, from a smaller 3D; it looks softer than the game's own, Sharpness helps. NVIDIA " + "DLAA: the 3D at full size, only its edges smoothed; the slowest. With either, things that " + "move by themselves can leave trails 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).", + }; + const int w = variant < 0 ? (int)dlss_why() : variant; + if (w >= DLSS_WHY_COUNT) return nullptr; + if (!why[w]) return base; + static char b[DLSS_WHY_COUNT][512]; + snprintf(b[w], sizeof b[w], "What scales the 3D up to the output: the game's own network. %s", why[w]); + return b[w]; } const char* tx_percent(int v) { static char b[8]; @@ -81,7 +121,6 @@ Out shown_output() { static const Out t[] = {{2048, 1152}, {2560, 1440}, {3840, 2160}}; return opts.handheld ? Out{1536, 864} : t[std::min(std::max(opts.output_res, 0), 2)]; } -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. @@ -93,12 +132,35 @@ 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; } +const char* resolution_help(int variant) { + static const char* const t[] = { + "The share of the output the 3D is drawn at; the upscaler fills in the rest. Auto lets the game lower it when the GPU " + "is busy. A fixed percentage stays steady: lower is faster, and softer.", + "DLSS's modes: the share of the output the 3D is drawn at before DLSS fills in the rest. Quality is the sharpest, " + "Ultra Performance the fastest. Auto lets the game lower it when the GPU is busy, and DLSS follows.", + "DLAA draws the 3D at the output's full size, so there is nothing to set here. Choose NVIDIA DLSS or the game's own " + "upscaler to draw it smaller.", + }; + const int i = variant < 0 ? std::clamp(upscaler(), 0, 2) : variant; + return i < 3 ? t[i] : nullptr; +} +/// 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 && dlss_why() != DLSS_OK) || + (value == &opts.resolution && upscaler() == ur::DLAA); +} const char* tx_output_res(int v) { static const char* t[] = {"2048x1152 (game, restart)", "2560x1440 (restart)", "3840x2160 (restart)"}; return t[v]; @@ -167,13 +229,16 @@ 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); } Row rows[] = { // Graphics (11 rows) {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 video memory limits for a GPU with that much memory. The Upscaler stays " + "as you chose it; with DLSS, the preset's resolution becomes DLSS's nearest mode."}, {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."}, {TAB_GRAPHICS, "GPU device", &opts.gpu_device, 0, 8, 1, tx_gpu, @@ -183,23 +248,19 @@ Row rows[] = { "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."}, + {TAB_GRAPHICS, "NVIDIA Reflex", &opts.reflex, 0, 2, 1, tx_reflex, nullptr, reflex_help}, + // (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, 2, 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."}, - {TAB_GRAPHICS, "NVIDIA DLSS", &opts.dlss, 0, 2, 1, tx_dlss, - "NVIDIA's DLSS in place of the game's own upscaler (NVIDIA GPUs, in a build made with NVIDIA's SDK). DLSS scales the 3D " - "up from the Resolution row's size. DLAA draws the 3D at full size and only smooths its edges: sharpest, and slower. " - "Things that move by themselves can leave trails for now."}, - {TAB_GRAPHICS, "NVIDIA Reflex", &opts.reflex, 0, 2, 1, tx_reflex, - "NVIDIA GPUs: the driver starts each frame as late as it can, so less time passes between your input and the screen. " - "Boost also keeps the GPU's clocks up. Not used with frame generation."}, + "The size of the game's whole frame, HUD included, before it is scaled to your window: what the Upscaler fills from the " + "3D. Docked only. Applies after a restart."}, + {TAB_GRAPHICS, "Upscaler", &opts.dlss, 0, 2, 1, tx_upscaler, nullptr, upscaler_help}, + {TAB_GRAPHICS, "Render resolution", &opts.resolution, 0, ur::kValues - 1, 1, tx_resolution, nullptr, resolution_help}, + {TAB_GRAPHICS, "Window scaling filter", &opts.upscale, 0, 2, 1, tx_upscale, + "How the finished picture is scaled to your window when the sizes differ. Off is the plain copy. Catmull-Rom is sharper, " + "Lanczos sharper still."}, {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."}, + "Sharpens the picture as it goes to the window, with any filter. DLSS looks softer than the game's own upscaler, so it " + "often wants more. 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."}, {TAB_GRAPHICS, "Ultrawide", &opts.ultrawide, 0, 1, 1, tx_ultrawide, @@ -370,6 +431,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; nvidia_dlss before: upscaler_rows::ini_alias) 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); @@ -867,7 +932,7 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { 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; + 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); @@ -900,7 +965,8 @@ 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); + wrap_help(dc, help_of(rows[tab_row_to_global(cur_tab, tab_selected[cur_tab])], -1), (int)w - 32 * s, 16 * s, help_top + 4 * s, + true); 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) @@ -951,7 +1017,8 @@ int measure_help(int s) { 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) + for (int v = 0; const char* t = help_of(r, v); v++) most = std::max(most, wrap_help(dc, t, (kMenuWidth - 32) * s, 0, 0, false)); SelectObject(dc, old); DeleteObject(f); DeleteDC(dc); @@ -1035,8 +1102,10 @@ void menu_init() { char* eq = strchr(line, '='); if (!eq) continue; *eq = 0; + const char* alias = upscaler_rows::ini_alias(line); for (Row& r : rows) - if (r.value && !strcmp(line, ini_key(r))) *r.value = std::min(std::max(atoi(eq + 1), r.lo), r.hi); + if (r.value && (!strcmp(line, ini_key(r)) || (alias && !strcmp(alias, ini_key(r))))) + *r.value = std::min(std::max(atoi(eq + 1), r.lo), r.hi); has_resolution |= !strcmp(line, "resolution"); v2 |= !strcmp(line, "options_version"); if (!strcmp(line, "dynamic_resolution")) old_dynres = atoi(eq + 1); @@ -1069,7 +1138,7 @@ void menu_init() { 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) } @@ -1242,6 +1311,7 @@ void menu_tick() { // (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() && page == 1) { // (hookshot) the Controls page if (!can_bind()) page = 0; @@ -1417,7 +1487,15 @@ void menu_tick() { int v = *r.value + (press[4] ? r.step : -r.step); if (r.value == &opts.framegen && v == 1) v = press[4] ? 2 : 0; // (off, 2x ... 8x) if (r.value == &opts.mouse_sens) v = std::min(std::max(v, r.lo), r.hi); // (a scale: it stops at its ends) - if (r.value == &opts.pad_layout) { + // (the scaling rows: only what can run here; the Render resolution row through the Upscaler's own steps) + const int dir = press[4] ? 1 : -1; + // (nothing else to choose: no change, no save. A saved choice that cannot run here is kept) + const bool stays = row_fixed(r.value); + 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 (stays) { + // (the row as it was) + } else if (r.value == &opts.pad_layout) { if (v > 2) v = press[4] ? 0 : 2; if (v < 0) v = 2; *r.value = v; @@ -1427,6 +1505,8 @@ void menu_tick() { 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.value != &opts.fullscreen && r.value != &opts.show_fps && r.value != &opts.gpu_device && r.value != &opts.invert_cam_x && r.value != &opts.invert_cam_y && r.value != &opts.mouse_native && r.value != &opts.mouse_sens && r.value != &opts.mouse_invert) { // (the controls are not the preset's) @@ -1434,8 +1514,7 @@ void menu_tick() { sync_preset_budgets(0); } } - changed = true; - save(); + if (!stays) changed = true, save(); } const int64_t now = prof_now(); if (changed) { // (a new row or value: its name from the start, after a rest) diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index a1a0fe2ae..697dc39bd 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -9,11 +9,13 @@ 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 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; read once, at the start (host/nn_applet.c) - int upscale = 0; // to the window: 0 off (the plain copy to the window), 1 Catmull-Rom, 2 Lanczos + 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; // NVIDIA DLSS in place of the game's own upscale: 0 off, 1 DLSS, 2 DLAA (vk_dlss.cpp; a build with NVIDIA's SDK only) + 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 (nvidia_dlss before) 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) diff --git a/gpu/vk/vk_reflex.cpp b/gpu/vk/vk_reflex.cpp index 7071f0702..75793bfaf 100644 --- a/gpu/vk/vk_reflex.cpp +++ b/gpu/vk/vk_reflex.cpp @@ -150,6 +150,8 @@ void reflex_device_created() { 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(); if (!r.ready) return; diff --git a/gpu/vk/vk_reflex.h b/gpu/vk/vk_reflex.h index a5ab6c19f..318f8dea3 100644 --- a/gpu/vk/vk_reflex.h +++ b/gpu/vk/vk_reflex.h @@ -14,6 +14,8 @@ void reflex_device_extensions(std::vector& want, const std::vector< 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 (the chained struct lives until the next call) void reflex_swapchain_info(VkSwapchainCreateInfoKHR& ci); /// A swapchain made: its sleep mode set as the menu has it diff --git a/readme.md b/readme.md index 6f25ff27d..a958da978 100644 --- a/readme.md +++ b/readme.md @@ -187,9 +187,11 @@ 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 | 2048x1152, 2560x1440, 3840x2160 (after a restart) | 2048x1152 | -| | Upscaling | Off, Catmull-Rom, Lanczos | Off | +| | Upscaler | Game's own, NVIDIA DLSS, NVIDIA DLAA (DLSS and DLAA: builds with NVIDIA's DLSS SDK, below, on RTX GPUs) | Game's own | +| | 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 | @@ -206,19 +208,23 @@ 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. - Resolution is a share of it, and its row shows the size for the output you chose. Docked only. At 3840x2160 the game's +- **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 (softer than the game's own: + Sharpness helps) or DLAA (the 3D at full size, edges smoothed). **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. At 3840x2160 the game's render targets get a bigger GPU pool, so a full-size 3D fits. `HOS_OUTPUT_RES=0/1/2` sets it for one run. -- **Upscaling** and **Sharpness** also work when Frame generation is Off. +- **Window scaling filter** and **Sharpness** also work when Frame generation is Off. - **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. @@ -356,8 +362,9 @@ The game folder runs under Proton or on Windows.
NVIDIA DLSS (optional, NVIDIA's SDK under NVIDIA's license) -A game built with NVIDIA's DLSS SDK has the Graphics tab's **NVIDIA DLSS** row (Off, DLSS, DLAA) on NVIDIA RTX cards: -DLSS Super Resolution in place of the game's own upscaler, with camera jitter (**NVIDIA Reflex** needs no SDK and is in +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 diff --git a/tests/test_upscaler_rows.cpp b/tests/test_upscaler_rows.cpp new file mode 100644 index 000000000..4beb55c32 --- /dev/null +++ b/tests/test_upscaler_rows.cpp @@ -0,0 +1,59 @@ +#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 options file: the NVIDIA DLSS row's key loads as the Upscaler's + CHECK("nvidia_dlss is dlss", ur::ini_alias("nvidia_dlss") && !std::strcmp(ur::ini_alias("nvidia_dlss"), "dlss")); + CHECK("other keys have no alias", !ur::ini_alias("dlss") && !ur::ini_alias("nvidia_dlss2") && !ur::ini_alias("nvidia") && + !ur::ini_alias("")); + 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..50977daa3 --- /dev/null +++ b/tests/test_upscaler_rows.py @@ -0,0 +1,63 @@ +"""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, and options files from before still loading. No game or GPU required. + +python -I -B tests/test_upscaler_rows.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() + 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', '"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, and the old DLSS row's key is read + key = menu[menu.index("const char* ini_key(const Row& r) {"):] + key = key[:key.index("\n}\n")] + for need in ('&opts.resolution) return "resolution"', '&opts.upscale) return "upscaling"', '&opts.dlss) return "dlss"'): + if need not in key: + raise SystemExit("ini_key: %s is missing (an options file from before would lose it)" % need) + load = menu[menu.index("void menu_init() {"):] + load = load[:load.index("\n}\n")] + if "upscaler_rows::ini_alias(line)" not in load: + raise SystemExit("menu_init does not read the old key names") + if "ur::step(upscaler(), *r.value, dir)" not in menu or "ur::step_upscaler(upscaler(), dir, true)" not in menu: + raise SystemExit("the Upscaler and Render resolution rows do not step through upscaler_rows.h") + fixed = menu[menu.index("bool row_fixed(const int* value) {"):] + fixed = fixed[:fixed.index("\n}\n")] + for need in ("&opts.dlss && dlss_why() != DLSS_OK", "&opts.reflex && !reflex_available()", "upscaler() == ur::DLAA"): + if need not in fixed: + raise SystemExit("row_fixed: %s is missing (a choice that cannot run here would be offered)" % need) + if "const bool stays = row_fixed(r.value);" not in menu: + raise SystemExit("the rows step past row_fixed") + if "if (r.value == &opts.dlss) opts.resolution = ur::snap(upscaler(), opts.resolution);" not in menu: + raise SystemExit("another Upscaler leaves the Render resolution off its steps") + dlss = (ROOT / "gpu" / "vk" / "vk_dlss.cpp").read_text(encoding="utf-8") + mode = dlss[dlss.index("int dlss_mode() {"):] + if "dlss_why() != DLSS_OK" not in mode[:mode.index("\n}\n")]: + 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() From c40db7ee72666553aa151a602c64a4004bbab4cd Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 01:36:45 -0500 Subject: [PATCH 16/52] DLSS: in place of the game's upscale at every Render resolution share DLSS stood in only for the 4/3 network (fd029a317a9a4b94) and the full-size pass: the 3D at 75% (Auto's usual size) and DLAA. At the other fixed shares the game runs a network of its own for each (2048x1152: 67% 6676619641320ba7, 58% f10782f9e58b6a7c, 50% 72ba090c70fe2614, 33% 0775215f8da720e6), so DLSS at 67% or 50% left the game's upscale running, with the scene camera taken and nothing said. Those are now found by their shape (a cooperative-matrix network sampling one texture into one image) and taken only when the texture is this frame's scene and the image the B10G11R11 picture; any other such network runs untouched, without a log line. The log names a network the first time DLSS takes it. --- gpu/vk/vk_dlss.cpp | 28 +++++++++++++++++++--------- gpu/vk/vk_dlss.h | 11 +++++++---- gpu/vk/vk_draw.cpp | 11 +++++++++-- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index e73489b42..6cfeb1194 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -1,8 +1,8 @@ // (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, one dispatch, cooperative matrices) scales it up by 4/3, from the square root of the scene's -// tonemapped colour, into the frame's B10G11R11 picture (before the HUD). Here that dispatch is replaced: +// 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. @@ -84,9 +84,9 @@ bool dlss_full_scale() { return dlss_mode() == 2; } namespace { -// (the gameplay network: the 4/3 upscale when the 3D is drawn smaller than the picture, and a pass at the same size when it -// is drawn at full size, the Render resolution row's 100% and DLAA's) -constexpr uint64_t kGameplayUpscale = 0xfd029a317a9a4b94ull, kGameplayFullSize = 0x9dc98871eb7fc101ull; +// (the gameplay networks, which vk_draw.cpp's dlss_stand_in finds: the 4/3 upscale (fd029a317a9a4b94) when the 3D is drawn at +// 3/4 of the picture, the game's dynamic resolution's usual size; a pass at the same size (9dc98871eb7fc101) when it is drawn +// at full size, the Render resolution row's 100% and DLAA's; and a network of its own for each other fixed share) struct Image { VkImage image = VK_NULL_HANDLE; @@ -543,11 +543,21 @@ uint32_t dlss_jitter_size(int axis) { return axis ? s.jitter_h.load(std::memory_order_relaxed) : s.jitter_w.load(std::memory_order_relaxed); } -bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, Texture* in, VkImageView in_view, Texture* out, VkImageView out_view, - uint32_t out_level) { - if ((key != kGameplayUpscale && key != kGameplayFullSize) || !dlss_wanted()) return false; +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; + if (!in || !out || !c.valid || texture_frame_no() - c.frame > 1 || in->width != c.width || in->height != c.height || + out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || (out->width >> out_level) <= in->width) + return 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) @@ -685,7 +695,7 @@ 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, Texture*, VkImageView, Texture*, VkImageView, uint32_t) { return false; } +bool dlss_upscale(VkCommandBuffer, uint64_t, bool, Texture*, VkImageView, Texture*, VkImageView, uint32_t) { return false; } uint32_t dlss_jitter_size(int) { return 0; } #endif diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index 6d0fcc8b2..b8611b2dc 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -32,10 +32,13 @@ void dlss_device_extensions(std::vector& want, const std::vector Date: Mon, 9 Nov 2026 02:20:27 -0500 Subject: [PATCH 17/52] DLSS: count NGX's commands and the shaders' dispatches in work_epoch vk_dlss.cpp did not include vk_work.h, so its dispatches and NGX's own commands did not move work_epoch, and full_barrier left out the barriers around the evaluation (and the game's next one): NGX read its inputs and dlss_out.comp read DLSS's output without a barrier. vk_work.h is now included last, and note_work() follows the feature's creation and the evaluation. --- gpu/vk/vk_dlss.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 6cfeb1194..511508e7c 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -51,6 +51,7 @@ #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 { @@ -451,6 +452,7 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui // (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 NVSDK_NGX_Result r = NGX_VULKAN_CREATE_DLSS_EXT(cmd, 1, 1, &s.feature, s.params, &cp); + 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; @@ -648,6 +650,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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; From 867f66d3dd54d56c229011a3725e6bff74f5dac9 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:20:45 -0500 Subject: [PATCH 18/52] tests: pass Runner.Info's ngxSdk in the builder workers test Runner.Info gained a fourth parameter (ngxSdk) and the reflection call still passed three, so the test threw and CI's source regressions failed. It passes null: a build without NVIDIA DLSS, as before. --- tests/builder_workers/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From bfacc41b8bebf772c421f6862920c52d768e1482 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:21:18 -0500 Subject: [PATCH 19/52] DLSS: read and set NGX's parameters through its C functions NVSDK_NGX_Parameter's virtual Get and Set were called from C++. On Windows the object comes from NVIDIA's MSVC-built library, whose vtable puts overloads in another order than the Itanium ABI llvm-mingw uses, so the calls landed on other overloads and only worked because those had the same size. NVSDK_NGX_Parameter_GetI, GetUI and SetUI are NVIDIA's own implementations and are used on every platform. --- gpu/vk/vk_dlss.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 511508e7c..ba2456ae0 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -278,18 +278,20 @@ bool ngx_init() { 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; - s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_Available, &available); - s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_NeedsUpdatedDriver, &needs_driver); - s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMajor, &major); - s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMinor, &minor); + 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); if (!available) { std::string where; for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + d; // (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; - s.caps->Get(NVSDK_NGX_Parameter_SuperSampling_FeatureInitResult, &init); + NVSDK_NGX_Parameter_GetI(s.caps, NVSDK_NGX_Parameter_SuperSampling_FeatureInitResult, &init); bool runtime = false; std::error_code ec; for (const auto& d : dirs) @@ -445,7 +447,7 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui 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}) - s.params->Set(k, preset); + 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; From 0404e4260029605db1f5515ca724324fec67a3fe Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:21:24 -0500 Subject: [PATCH 20/52] DLSS: make NGX's data folder without the throwing overload std::filesystem::create_directories threw when the game folder's ngx folder could not be made (a read-only folder, a file of that name), which ended the game. The error_code overload is used; NGX then reports the folder itself. --- gpu/vk/vk_dlss.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index ba2456ae0..285bac636 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -254,7 +254,8 @@ bool ngx_init() { std::vector paths; for (const auto& d : wdirs) paths.push_back(d.c_str()); const std::string data = game_dir() + "ngx"; - std::filesystem::create_directories(data); // (NGX's logs and its own files: the game folder's, not /usr/share) + std::error_code made; // (not the throwing overload: a folder that cannot be made, read-only or so, must not end the game) + std::filesystem::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(); From 33aee89a6fcba252f80f22b891a82bee2e5be7bd Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:23:59 -0500 Subject: [PATCH 21/52] DLSS: wide paths for the runtime's folders on Windows The game folder came from GetModuleFileNameA and the environment from getenv, converted from the ANSI code page: a folder with characters outside it was mangled. GetModuleFileNameW (with game_module_path's bin rule), _wgetenv for RX_VK_DLSS_DIR and LOCALAPPDATA, and std::filesystem paths are used instead, and paths are logged as UTF-8. Linux is as before. --- gpu/vk/vk_dlss.cpp | 90 ++++++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 285bac636..9f76b1546 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -15,10 +15,10 @@ // 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). RX_VK_DLSS_DIR=: where DLSS's runtime is looked for -// first (Windows: nvngx_dlss.dll, else in the game folder's ngx\, then %LOCALAPPDATA%\Hookshot\ngx; Linux: -// libnvidia-ngx-dlss.so.*, else in the game folder's ngx/, then $XDG_DATA_HOME/hookshot/ngx). RX_VK_DLSS_PRESET=: -// DLSS's preset (default K). +// 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. RX_VK_DLSS_PRESET=: DLSS's preset (default K). // RX_VK_DLSS_DEBUG=motion: the motion vectors shown instead of the picture. RX_VK_DLSS_LOG=1: NGX's own log in ours. // RX_VK_DLSS_SQUARE=0: the output not squared. // @@ -208,30 +208,58 @@ void NVSDK_CONV ngx_log(const char* message, NVSDK_NGX_Logging_Level, NVSDK_NGX_ logf("ngx: %s", m.c_str()); } -std::string game_dir() { +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); - std::string dir(exe, n); - return dir.substr(0, dir.find_last_of("\\/") + 1); + return fs::path(std::string(exe, n)).parent_path(); } -/// A path for NGX, which takes wide strings. Windows: the narrow paths here are the ANSI code page's (GetModuleFileNameA, getenv), -/// so they are converted from it, and a game folder with letters outside ASCII is still the right folder -std::wstring wide(const std::string& s) { -#ifdef _WIN32 - if (s.empty()) return {}; - const int n = MultiByteToWideChar(CP_ACP, 0, s.data(), (int)s.size(), nullptr, 0); - std::wstring w(n > 0 ? n : 0, L'\0'); - if (n > 0) MultiByteToWideChar(CP_ACP, 0, s.data(), (int)s.size(), w.data(), n); - return w; -#else +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 } +#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) @@ -240,22 +268,24 @@ bool ngx_init() { 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; - if (const char* e = getenv("RX_VK_DLSS_DIR"); e && *e) dirs.push_back(e); - dirs.push_back(game_dir() + "ngx"); + std::vector dirs; #ifdef _WIN32 - if (const char* l = getenv("LOCALAPPDATA"); l && *l) dirs.push_back(std::string(l) + "\\Hookshot\\ngx"); + 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 (const char* x = getenv("XDG_DATA_HOME"); x && *x) dirs.push_back(std::string(x) + "/hookshot/ngx"); - else if (const char* h = getenv("HOME")) dirs.push_back(std::string(h) + "/.local/share/hookshot/ngx"); + 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 std::string data = game_dir() + "ngx"; + 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) - std::filesystem::create_directories(data, made); // (NGX's logs and its own files: the game folder's, not /usr/share) + 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(); @@ -289,15 +319,15 @@ bool ngx_init() { NVSDK_NGX_Parameter_GetUI(s.caps, NVSDK_NGX_Parameter_SuperSampling_MinDriverVersionMinor, &minor); if (!available) { std::string where; - for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + d; + for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + shown(d); // (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 : std::filesystem::directory_iterator(d, ec)) - runtime |= f.path().filename().string().rfind(kRuntime, 0) == 0; + 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); @@ -305,7 +335,7 @@ bool ngx_init() { return false; } if (NVSDK_NGX_FAILED(NVSDK_NGX_VULKAN_AllocateParameters(&s.params)) || !s.params) return false; - logf("vk: NVIDIA DLSS: NGX started (DLSS from %s)", dirs.size() > 1 ? "the folders searched" : dirs[0].c_str()); + logf("vk: NVIDIA DLSS: NGX started (DLSS from %s)", dirs.size() > 1 ? "the folders searched" : shown(dirs[0]).c_str()); s.ngx_ok = true; g_why = DLSS_OK; return true; From 0e07fe308df0e4f495e6d9b278ac8d769087319f Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:24:10 -0500 Subject: [PATCH 22/52] readme: NGX takes the newest DLSS runtime; harmless NGX log lines RX_VK_DLSS_DIR does not win over the other folders: NGX uses the newest runtime across all the paths it is given. And two kinds of NGX log line that look like errors are explained (NGXCore not beside the game, a model not in NGX's own cache). --- readme.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 25010f857..aae4158e1 100644 --- a/readme.md +++ b/readme.md @@ -411,13 +411,19 @@ license. Never share a game folder built with them, and do not post builds made - **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`. The game also looks in `%LOCALAPPDATA%\Hookshot\ngx`, and first in - `RX_VK_DLSS_DIR` when it is set. Never copy NGX's core, `_nvngx.dll`, beside the game: it comes with NVIDIA's driver. + `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 game's log says whether DLSS started, and why not when it did not (`vk: NVIDIA DLSS:` lines in `hookshot_log.txt`). +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`.
From 24ad39510a226050632639313be46bf021d46900 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:25:11 -0500 Subject: [PATCH 23/52] Reflex: nothing at all while the row is Off With the row Off, every NVIDIA GPU still got a swapchain with latency mode, the frame markers, the driver's sleep, present ids and a timings line every 300 frames. Now the latency create-info is chained only while Reflex is on, and turning the row on or off makes the swapchain again at the next present; the markers, sleep and present ids need a swapchain made with latency mode and the row on. The timings log is opt-in (RX_VK_REFLEX_LOG=1). The device extensions are still enabled when the device has them, so the row works without a restart: enabled alone they change nothing. --- gpu/vk/vk_reflex.cpp | 44 +++++++++++++++++++++++++++++--------------- gpu/vk/vk_reflex.h | 5 ++++- gpu/vk/vk_replay.cpp | 3 +++ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/gpu/vk/vk_reflex.cpp b/gpu/vk/vk_reflex.cpp index 75793bfaf..1d3a2302b 100644 --- a/gpu/vk/vk_reflex.cpp +++ b/gpu/vk/vk_reflex.cpp @@ -1,7 +1,10 @@ // (hookshot) NVIDIA Reflex through VK_NV_low_latency2, without NVIDIA's Streamline (vk_reflex.h). // -// The swapchain is made with latency mode on (VkSwapchainLatencyCreateInfoNV) and its sleep mode set from the Insert menu's -// NVIDIA Reflex row (Off / On / On + Boost; RX_VK_REFLEX=0 / 1 / 2 over it). Each frame the driver gets markers with the +// 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 @@ -11,8 +14,8 @@ // 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. // -// Every 300 frames the log gives the driver's own timings (vkGetLatencyTimingsNV) averaged: simulation start to present end, -// and present end to the GPU's end of rendering, so On and Off can be compared. RX_VK_REFLEX_LOG=0: none. +// 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 @@ -30,8 +33,10 @@ 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{VK_STRUCTURE_TYPE_SWAPCHAIN_LATENCY_CREATE_INFO_NV}; + VkSwapchainLatencyCreateInfoNV latency_info{VK_STRUCTURE_TYPE_SWAPCHAIN_LATENCY_CREATE_INFO_NV}; PFN_vkSetLatencySleepModeNV set_mode = nullptr; PFN_vkLatencySleepNV sleep = nullptr; PFN_vkSetLatencyMarkerNV marker = nullptr; @@ -55,10 +60,11 @@ int wanted_mode() { return std::clamp(e && *e ? atoi(e) : opts.reflex, 0, 2); } -/// Usable now: the extension and its feature, a swapchain made with it, and no frame generation (its own display thread) +/// 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 && !framegen_on(); + return r.ready && r.swapchain && r.latency && wanted_mode() > 0 && !framegen_on(); } void mark(VkLatencyMarkerNV m) { @@ -81,9 +87,9 @@ void apply_mode() { } void log_timings() { - static const bool quiet = getenv("RX_VK_REFLEX_LOG") && !atoi(getenv("RX_VK_REFLEX_LOG")); + static const bool wanted = getenv("RX_VK_REFLEX_LOG") && atoi(getenv("RX_VK_REFLEX_LOG")) > 0; Reflex& r = rx(); - if (quiet || !r.timings || r.id % 300) return; + 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; @@ -154,18 +160,26 @@ bool reflex_available() { return rx().ready; } void reflex_swapchain_info(VkSwapchainCreateInfoKHR& ci) { Reflex& r = rx(); - if (!r.ready) return; - r.latency.pNext = ci.pNext; - r.latency.latencyModeEnable = VK_TRUE; - ci.pNext = &r.latency; + 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); - r.swapchain = swapchain, r.mode = -1, r.frame_marked = false; - apply_mode(); + 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() { diff --git a/gpu/vk/vk_reflex.h b/gpu/vk/vk_reflex.h index 318f8dea3..797fa3de2 100644 --- a/gpu/vk/vk_reflex.h +++ b/gpu/vk/vk_reflex.h @@ -16,8 +16,11 @@ void reflex_device_features(void**& tail); 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 (the chained struct lives until the next call) +/// 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 diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index f67474934..39aad44f4 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -2956,6 +2956,9 @@ 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); } menu_tick(); Frame& f = frames[frame_index]; From b304aeec39aa4f3209c07bb436658bd3771f8205 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:26:42 -0500 Subject: [PATCH 24/52] DLSS jitter: give each camera its own offset back, only in DLSS builds The jitter sites wrote the offset over the camera's own and never undid it: once DLSS stopped, cameras kept the last frame's sub-pixel offset. host/rx_backend.c now keeps each jittered camera's own offset, puts the jitter on top of it, and writes it back the next time the camera passes the site with no jitter (a value the game set itself in between is the camera's own from then on). The sites are installed only in a build with NVIDIA's DLSS SDK: make_game.py sets OOT_DLSS_JITTER=1 for the lift (in its key) when HOOKSHOT_NGX_SDK holds this platform's library. That every perspective camera of the picture's ratio is jittered, not only the scene's, is documented in oot_jitter.py: which one is the scene's is known only to the renderer, after the update. --- host/rx_backend.c | 37 ++++++++++++++++++++++++++++++++++++- nx2/lift.py | 5 +++-- nx2/make_game.py | 8 ++++++++ nx2/oot_jitter.py | 31 +++++++++++++++++++++++++------ tests/test_make_game_ngx.py | 23 +++++++++++++++++++++++ 5 files changed, 95 insertions(+), 9 deletions(-) diff --git a/host/rx_backend.c b/host/rx_backend.c index 77cb62319..b3a7a99da 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -295,7 +295,7 @@ void rx_nvn_present(const nvn_tex *t, const int crop[4]) { without it: rx_option 18 and 19, the scene's size in pixels, are 0). The renderer reads the offset the camera ended up with 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 16 frames. */ -int hos_dlss_jitter(float out[2]) { +static int dlss_jitter(float out[2]) { static LONG at = -1; static float w, h; const LONG frame = presents; @@ -312,6 +312,41 @@ int hos_dlss_jitter(float out[2]) { out[0] = (x - 0.5f) / w, out[1] = (y - 0.5f) / h; return 1; } +/* The cameras jittered, each with its own offset from before the jitter: the jitter goes on top of it, and once there is no + jitter (DLSS turned off) each camera gets it back the next time it passes the site, so none is left offset. A value that + is not the one written last was set by the game itself: it is the camera's own from then on. A camera not seen for long + (gone) gives its place to a new one */ +typedef struct { uint64_t cam; uint32_t own[2], wrote[2]; LONG seen; } jittered; +static jittered jittered_cams[32]; +static volatile LONG jitter_lock; +int hos_dlss_jitter_cam(uint64_t cam, const uint32_t now[2], uint32_t out[2]) { + float j[2]; + const int on = dlss_jitter(j); + while (InterlockedCompareExchange(&jitter_lock, 1, 0)) {} + jittered *e = NULL, *spare = NULL; + for (int i = 0; i < 32; i++) { + jittered *t = &jittered_cams[i]; + if (t->cam == cam && cam) e = t; + else if (!spare || !t->cam || (spare->cam && t->seen < spare->seen)) spare = t; + } + int write = 0; + if (on) { + if (!e && spare) e = spare, e->cam = cam, memcpy(e->own, now, 8); + else if (e && memcmp(e->wrote, now, 8)) memcpy(e->own, now, 8); + if (e) { + float own[2]; + memcpy(own, e->own, 8); + own[0] += j[0], own[1] += j[1]; + memcpy(out, own, 8), memcpy(e->wrote, out, 8); + e->seen = presents, write = 1; + } + } else if (e) { /* (no jitter now: the camera's own offset back, unless the game has set another since) */ + if (!memcmp(e->wrote, now, 8)) memcpy(out, e->own, 8), write = 1; + memset(e, 0, sizeof *e); + } + InterlockedExchange(&jitter_lock, 0); + return write; +} static void forward(const early_call *c) { switch (c->kind) { case 0: { diff --git a/nx2/lift.py b/nx2/lift.py index 36b188b94..a35e6460c 100644 --- a/nx2/lift.py +++ b/nx2/lift.py @@ -158,8 +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, none unless DLSS runs: host/rx_backend.c hos_dlss_jitter) - oot_jitter.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 ce3a9b971..479476767 100644 --- a/nx2/make_game.py +++ b/nx2/make_game.py @@ -886,6 +886,12 @@ def fetch_ngx_sdk(a): 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 @@ -1029,6 +1035,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) diff --git a/nx2/oot_jitter.py b/nx2/oot_jitter.py index bb917a487..26a4ca615 100644 --- a/nx2/oot_jitter.py +++ b/nx2/oot_jitter.py @@ -7,8 +7,19 @@ view and aspect ratio at +0xac. The sites are those loads (the same ones nx2/oot_wide.py's ratio patch follows): the offset is written before it is read, for cameras of the picture's ratio. host/rx_backend.c hos_dlss_jitter picks it; it is none (the game as shipped) unless DLSS runs, and the renderer reads the offset back from the camera's uniform buffer. +host/rx_backend.c hos_dlss_jitter_cam keeps each camera's own offset: the jitter goes on top of it, and it is put back once +DLSS stops, so no camera is left offset. + +Every perspective camera of the picture's ratio gets the jitter, not only the scene's: which one the scene's is, the +renderer learns from its draws, too late for the CPU's update. The others (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, the register the aspect ratio was loaded into, the projection's register) @@ -21,13 +32,21 @@ def _text(vreg, base): - # (16:9, or the window's ratio once the widescreen patch before this one has put it in: the picture's cameras) - return ("{ extern int hos_dlss_jitter(float out[2]); extern volatile float hos_wide_ratio; float _j[2]; " - "if (hos_dlss_jitter(_j)) { float _r; memcpy(&_r, c->vreg[%d], 4); const float _w = hos_wide_ratio; " + # (16:9, or the window's ratio once the widescreen patch before this one has put it in: the picture's cameras. The host + # puts the jitter on top of the camera's own offset, and that offset back once there is no jitter) + return ("{ extern int hos_dlss_jitter_cam(uint64_t cam, const uint32_t now[2], uint32_t out[2]); " + "extern volatile float hos_wide_ratio; float _r; memcpy(&_r, c->vreg[%d], 4); const float _w = hos_wide_ratio; " "const float _d = _r - (16.0f / 9.0f), _e = _w > 0.0f ? _r - _w : 1.0f; " - "if ((_d <= 0.01f && _d >= -0.01f) || (_e <= 0.01f && _e >= -0.01f)) { uint32_t _u[2]; memcpy(_u, _j, 8); " - "recomp_store32(c, c->x[%d] + 0xb4, _u[0]); recomp_store32(c, c->x[%d] + 0xb8, _u[1]); } } }" - " /* (nx2/oot_jitter.py: DLSS camera jitter) */" % (vreg, base, base)) + "if ((_d <= 0.01f && _d >= -0.01f) || (_e <= 0.01f && _e >= -0.01f)) { " + "uint32_t _n[2] = {(uint32_t)recomp_load32(c, c->x[%d] + 0xb4), (uint32_t)recomp_load32(c, c->x[%d] + 0xb8)}, _u[2]; " + "if (hos_dlss_jitter_cam(c->x[%d], _n, _u)) { recomp_store32(c, c->x[%d] + 0xb4, _u[0]); " + "recomp_store32(c, c->x[%d] + 0xb8, _u[1]); } } }" + " /* (nx2/oot_jitter.py: DLSS camera jitter) */" % (vreg, base, base, base, base, base)) + + +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): diff --git a/tests/test_make_game_ngx.py b/tests/test_make_game_ngx.py index 8bd3841a9..035a2564b 100644 --- a/tests/test_make_game_ngx.py +++ b/tests/test_make_game_ngx.py @@ -53,6 +53,29 @@ def test_windows_runtime_goes_to_the_ngx_folder(self): 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") From 5d7e2e695199f91ee867bab7ffd08c03fc098766 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:31:45 -0500 Subject: [PATCH 25/52] CI: NVIDIA DLSS link check only when DLSS's files change; ngx-sdk/ ignored The vulkan-dlss job fetched NVIDIA's SDK, accepting its license, on every push and pull request. It moves to its own workflow, run by hand and on a push or pull request only when a file DLSS's build depends on changed (vk_dlss*, vk_reflex*, the MSVC runtime shims, upscaler_rows.h, the DLSS shaders and math header, oot_jitter.py, build.py, make_game.py, ngx_sdk.py, the workflow itself). The other Windows jobs are unchanged. The license is accepted for that run alone and nothing is uploaded. .gitignore gains ngx-sdk/, where scripts/ngx_sdk.py is usually pointed. --- .github/workflows/vulkan-dlss.yml | 115 ++++++++++++++++++++++++++ .github/workflows/windows-builder.yml | 69 ---------------- .gitignore | 3 + CONTRIBUTING.md | 4 +- 4 files changed, 121 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/vulkan-dlss.yml 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/.github/workflows/windows-builder.yml b/.github/workflows/windows-builder.yml index e8a9af0a4..815818770 100644 --- a/.github/workflows/windows-builder.yml +++ b/.github/workflows/windows-builder.yml @@ -184,75 +184,6 @@ jobs: path: ${{ runner.temp }}/vk-work/sa/build/hookshot_vk.dll if-no-files-found: error - # (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: nothing of it is uploaded, and - # nothing needs it. scripts/ngx_sdk.py fetches the SDK at its pinned release, SHA-256 checked, without DLSS's runtime) - vulkan-dlss: - name: Windows Vulkan back end with NVIDIA DLSS (llvm-mingw, link check) - if: github.ref_name != 'builder-parts' - 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 - release: name: Release assets # (off until the maintainer switches it on: releases are published by hand with HookshotBuilder.exe and hookshot-setup.txt, 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 b5fe24624..eef9a5914 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,7 +106,9 @@ On Windows the llvm-mingw build links NVIDIA's static `nvsdk_ngx_s.lib` (built f `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` job checks only that `hookshot_vk.dll` still links with the SDK, and uploads nothing. +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 From ba8e50d28582209f809c66cc11e363738ed3fefc Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:31:51 -0500 Subject: [PATCH 26/52] ngx_sdk.py: a 60 s timeout on each download urlopen had none, so a connection that stalled held the build forever. The test opener checks that the timeout is passed. --- scripts/ngx_sdk.py | 5 ++++- tests/test_ngx_sdk.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/ngx_sdk.py b/scripts/ngx_sdk.py index 340067217..0ee283305 100644 --- a/scripts/ngx_sdk.py +++ b/scripts/ngx_sdk.py @@ -121,6 +121,9 @@ def sha256(path): 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""" @@ -129,7 +132,7 @@ def fetch_one(path, digest, dst, opener=urllib.request.urlopen): os.makedirs(os.path.dirname(dst) or ".", exist_ok=True) tmp = dst + ".part" try: - with opener(BASE + "/" + path) as r, open(tmp, "wb") as f: + 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) diff --git a/tests/test_ngx_sdk.py b/tests/test_ngx_sdk.py index e214d4b17..474d6bc09 100644 --- a/tests/test_ngx_sdk.py +++ b/tests/test_ngx_sdk.py @@ -27,7 +27,9 @@ class Opener: def __init__(self, contents): self.contents, self.asked = contents, [] - def __call__(self, url): + 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]) From d86f0c68e44a34f958d939b2a4f1ec3de4cb528c Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:31:59 -0500 Subject: [PATCH 27/52] Menu: no Upscaler row without DLSS; the filter row says when it is not needed A build without NVIDIA's SDK (every official build) showed an Upscaler row that could only say DLSS is not in this build. It is not shown there: the game's own is all there is, and Render resolution is the game's own's row. The nvidia_dlss key alias goes: that key never existed on main. The Window scaling filter's help says it matters only when the window's size differs from the Output resolution, and that an Output resolution at the screen's size needs no filter. While the window is the picture's size the row shows "Not needed (window = output)" and Left and Right leave the saved value alone (present_image notes both sizes). upscaler_rows.h gains the rows' rules (upscaler_shown, upscaler_fixed, resolution_fixed, filter_needed), tested in test_upscaler_rows.cpp. --- gpu/vk/upscaler_rows.h | 19 +++++++++-------- gpu/vk/vk_menu.cpp | 40 +++++++++++++++++++++++++----------- gpu/vk/vk_menu.h | 5 ++++- gpu/vk/vk_replay.cpp | 5 +++++ tests/test_upscaler_rows.cpp | 13 ++++++++---- tests/test_upscaler_rows.py | 9 +++----- 6 files changed, 60 insertions(+), 31 deletions(-) diff --git a/gpu/vk/upscaler_rows.h b/gpu/vk/upscaler_rows.h index e5f8017ed..f0ea1adef 100644 --- a/gpu/vk/upscaler_rows.h +++ b/gpu/vk/upscaler_rows.h @@ -74,14 +74,17 @@ inline int step_upscaler(int v, int dir, bool dlss_ok) { return ((v < 0 || v >= n ? GAME : v) + (dir > 0 ? 1 : n - 1)) % n; } -/// The options file's key for a row's saved value, read back under the names it had before: the Upscaler's choice was saved as -/// nvidia_dlss (the NVIDIA DLSS row), and is dlss now (as opts.dlss and RX_VK_DLSS). nullptr: no other name -inline const char* ini_alias(const char* key) { - const char* old = "nvidia_dlss"; - for (int i = 0;; i++) { - if (key[i] != old[i]) return nullptr; - if (!key[i]) return "dlss"; - } +/// 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) +inline bool upscaler_fixed(bool dlss_ok) { return !dlss_ok; } +inline bool resolution_fixed(int upscaler) { return upscaler == DLAA; } +/// 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_menu.cpp b/gpu/vk/vk_menu.cpp index bb265e2b8..daa0900bf 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -60,11 +60,20 @@ 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]; @@ -158,8 +167,8 @@ const char* resolution_help(int variant) { } /// 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 && dlss_why() != DLSS_OK) || - (value == &opts.resolution && upscaler() == ur::DLAA); + 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()); } const char* tx_output_res(int v) { static const char* t[] = {"2048x1152 (game, restart)", "2560x1440 (restart)", "3840x2160 (restart)"}; @@ -235,7 +244,7 @@ void apply_preset(int p) { } 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, "Sets the scaling rows, frame generation and video memory limits for a GPU with that much memory. The Upscaler stays " "as you chose it; with DLSS, the preset's resolution becomes DLSS's nearest mode."}, @@ -255,9 +264,10 @@ Row rows[] = { "3D. Docked only. Applies after a restart."}, {TAB_GRAPHICS, "Upscaler", &opts.dlss, 0, 2, 1, tx_upscaler, nullptr, upscaler_help}, {TAB_GRAPHICS, "Render resolution", &opts.resolution, 0, ur::kValues - 1, 1, tx_resolution, nullptr, resolution_help}, - {TAB_GRAPHICS, "Window scaling filter", &opts.upscale, 0, 2, 1, tx_upscale, - "How the finished picture is scaled to your window when the sizes differ. Off is the plain copy. Catmull-Rom is sharper, " - "Lanczos sharper still."}, + {TAB_GRAPHICS, "Window scaling filter", &opts.upscale, 0, 2, 1, tx_filter, + "How the finished picture is scaled to your window. It matters only when the window's size differs from the Output " + "resolution: for the sharpest picture, set the Output resolution to your screen's and no filter is needed. Off is the " + "plain copy, Catmull-Rom sharper, Lanczos sharper still."}, {TAB_GRAPHICS, "Sharpness", &opts.sharpness, 0, 100, 10, tx_percent, "Sharpens the picture as it goes to the window, with any filter. DLSS looks softer than the game's own upscaler, so it " "often wants more. Too much shows halos around edges."}, @@ -393,9 +403,13 @@ constexpr int kMenuWidth = 480; // (at a scale of 1: 720 lines) int cur_tab = TAB_GRAPHICS; int tab_selected[TAB_COUNT] = {0, 0, 0}; +/// 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())); +} 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 @@ -408,7 +422,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++; } @@ -434,7 +448,7 @@ const char* ini_key(const Row& r) { // (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; nvidia_dlss before: upscaler_rows::ini_alias) + 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); @@ -1102,10 +1116,8 @@ void menu_init() { char* eq = strchr(line, '='); if (!eq) continue; *eq = 0; - const char* alias = upscaler_rows::ini_alias(line); for (Row& r : rows) - if (r.value && (!strcmp(line, ini_key(r)) || (alias && !strcmp(alias, ini_key(r))))) - *r.value = std::min(std::max(atoi(eq + 1), r.lo), r.hi); + if (r.value && !strcmp(line, ini_key(r))) *r.value = std::min(std::max(atoi(eq + 1), r.lo), r.hi); has_resolution |= !strcmp(line, "resolution"); v2 |= !strcmp(line, "options_version"); if (!strcmp(line, "dynamic_resolution")) old_dynres = atoi(eq + 1); @@ -1158,6 +1170,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); diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index 697dc39bd..f1c54d070 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -15,7 +15,7 @@ struct Options { 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 (nvidia_dlss before) + // SDK only). Saved as dlss 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) @@ -54,6 +54,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); diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 39aad44f4..3a0f36fb7 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -2960,6 +2960,11 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI std::lock_guard qk(dev.present_mu); create_swapchain(dev.swap_extent.width, dev.swap_extent.height); } + { // (the Window scaling filter's row: whether the window is the picture's size) + 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]; const int log_slot = frame_index; diff --git a/tests/test_upscaler_rows.cpp b/tests/test_upscaler_rows.cpp index 4beb55c32..9303f5339 100644 --- a/tests/test_upscaler_rows.cpp +++ b/tests/test_upscaler_rows.cpp @@ -51,9 +51,14 @@ int main() { 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 options file: the NVIDIA DLSS row's key loads as the Upscaler's - CHECK("nvidia_dlss is dlss", ur::ini_alias("nvidia_dlss") && !std::strcmp(ur::ini_alias("nvidia_dlss"), "dlss")); - CHECK("other keys have no alias", !ur::ini_alias("dlss") && !ur::ini_alias("nvidia_dlss2") && !ur::ini_alias("nvidia") && - !ur::ini_alias("")); + // 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)); std::puts("upscaler rows: all checks passed"); } diff --git a/tests/test_upscaler_rows.py b/tests/test_upscaler_rows.py index 50977daa3..641b3d2f4 100644 --- a/tests/test_upscaler_rows.py +++ b/tests/test_upscaler_rows.py @@ -27,21 +27,18 @@ def main(): 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, and the old DLSS row's key is read + # saved options: the renamed rows keep their keys key = menu[menu.index("const char* ini_key(const Row& r) {"):] key = key[:key.index("\n}\n")] for need in ('&opts.resolution) return "resolution"', '&opts.upscale) return "upscaling"', '&opts.dlss) return "dlss"'): if need not in key: raise SystemExit("ini_key: %s is missing (an options file from before would lose it)" % need) - load = menu[menu.index("void menu_init() {"):] - load = load[:load.index("\n}\n")] - if "upscaler_rows::ini_alias(line)" not in load: - raise SystemExit("menu_init does not read the old key names") if "ur::step(upscaler(), *r.value, dir)" not in menu or "ur::step_upscaler(upscaler(), dir, true)" not in menu: raise SystemExit("the Upscaler and Render resolution rows do not step through upscaler_rows.h") fixed = menu[menu.index("bool row_fixed(const int* value) {"):] fixed = fixed[:fixed.index("\n}\n")] - for need in ("&opts.dlss && dlss_why() != DLSS_OK", "&opts.reflex && !reflex_available()", "upscaler() == ur::DLAA"): + for need in ("ur::upscaler_fixed(dlss_why() == DLSS_OK)", "&opts.reflex && !reflex_available()", + "ur::resolution_fixed(upscaler())"): if need not in fixed: raise SystemExit("row_fixed: %s is missing (a choice that cannot run here would be offered)" % need) if "const bool stays = row_fixed(r.value);" not in menu: From d809f911c9db3579f0c4d082934236c8dbff3b6f Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:35:07 -0500 Subject: [PATCH 28/52] DLSS: failed and last_frame as atomics dlss_jitter_size reads them on the game's thread, without the lock the renderer writes them under. --- gpu/vk/vk_dlss.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 9f76b1546..cd618505d 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -109,7 +109,8 @@ struct Camera { struct State { std::mutex mu; - bool ngx_tried = false, ngx_ok = false, failed = false; + 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; @@ -120,7 +121,7 @@ struct State { 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) - uint64_t last_frame = 0; // (the frame DLSS last drew) + 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) @@ -574,7 +575,8 @@ 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(); - if (off || !dlss_wanted() || s.failed || !s.last_frame || texture_frame_no() - s.last_frame > 30) return 0; + 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; return axis ? s.jitter_h.load(std::memory_order_relaxed) : s.jitter_w.load(std::memory_order_relaxed); } @@ -630,7 +632,8 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk return fail("its images were not made"); const uint64_t now = texture_frame_no(); - const bool reset = !s.last_frame || now - s.last_frame > 2; + const uint64_t last = s.last_frame; + const bool reset = !last || now - last > 2; struct { float reproject[16]; float jitter[2]; From 3439071400528e7c947a386053ca9c700ca39e1a Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:35:28 -0500 Subject: [PATCH 29/52] DLSS: descriptors_disturbed on every way out once commands are recorded A failure after the feature's creation (its images not made, NGX's creation failing) returned without it, leaving the program's descriptor pushes taken for bound after NGX's commands. A scope guard calls it from the first recorded command on, whichever way dlss_upscale returns. --- gpu/vk/vk_dlss.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index cd618505d..8061a4f1c 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -624,6 +624,11 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk // 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) || @@ -690,7 +695,6 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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; - descriptors_disturbed(); return false; // (the network writes its output over whatever the evaluation did) } full_barrier(cmd); @@ -709,7 +713,6 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk vkCmdPushConstants(cmd, s.out_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof opc, opc); vkCmdDispatch(cmd, (ow + 7) / 8, (oh + 7) / 8, 1); } - descriptors_disturbed(); // (the program's own pushes are gone from the bind point) s.last_frame = now; s.last_jitter[0] = cam.jitter[0], s.last_jitter[1] = cam.jitter[1]; static int evaluations; From 46a861175cadd6e980dd839a3ba10bddea183c3e Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:40:20 -0500 Subject: [PATCH 30/52] DLSS: let its feature and images go after 120 frames unused dlss_upscale returns before free_dead when DLSS is not wanted, so turning it off kept NGX's feature, the four images and the deferred frees (some hundreds of MB at 4K) for the rest of the run. dlss_idle, at each of the game's presents, lets them go once no upscale has asked for DLSS for 120 frames, and runs the deferred frees until none is left. Nothing to do costs one atomic load. --- gpu/vk/vk_dlss.cpp | 30 ++++++++++++++++++++++++++++++ gpu/vk/vk_dlss.h | 4 ++++ gpu/vk/vk_replay.cpp | 1 + 3 files changed, 35 insertions(+) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 8061a4f1c..f3cb75c9f 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -132,6 +132,8 @@ struct State { bool told_first = 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; @@ -580,6 +582,32 @@ uint32_t dlss_jitter_size(int axis) { return axis ? s.jitter_h.load(std::memory_order_relaxed) : s.jitter_w.load(std::memory_order_relaxed); } +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: its feature and images let go"); + } + for (Image* im : {&s.color, &s.depth, &s.motion, &s.output}) drop_image(*im); + 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(); +} + 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; @@ -599,6 +627,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk const Camera cam = s.cam; s.cam.valid = false; // (each frame's own camera) 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"); @@ -738,6 +767,7 @@ void dlss_instance_extensions(std::vector&, const 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() {} uint32_t dlss_jitter_size(int) { return 0; } #endif diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index b8611b2dc..998bbdc15 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -40,6 +40,10 @@ void dlss_note_scene_draw(const Texture* color0, uint32_t width, uint32_t height 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(); + /// The scene's width (axis 0) or height (1) for the host's camera jitter (nx2/oot_jitter.py, rx_option 18 and 19): 0 when /// there is to be none (DLSS not drawing, or RX_VK_DLSS_JITTER=0) uint32_t dlss_jitter_size(int axis); diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 3a0f36fb7..32c482d44 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -2484,6 +2484,7 @@ uint64_t present_value = 0; // (the present job's value, handed out as it came 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) 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) From 4066db1c1b5f3156a10598ae49ddfc4869977569 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:44:55 -0500 Subject: [PATCH 31/52] DLSS: drop the investigation switches and the camera dump RX_VK_DLSS_JITTER_SIGN and RX_VK_DLSS_SQUARE were for finding the jitter's signs and the output's encoding, both settled. dlss_out.comp now always squares (its push constant loses the switch, its SPIR-V rebuilt with glslangValidator 16.6.0, vulkan1.3), and the 140-float dump of the camera's buffer every 3000 scene draws goes. --- gpu/vk/shaders/dlss_out.comp | 8 +- gpu/vk/shaders/dlss_out_comp_spv.h | 131 ++++++++++++++--------------- gpu/vk/vk_dlss.cpp | 23 ++--- 3 files changed, 72 insertions(+), 90 deletions(-) diff --git a/gpu/vk/shaders/dlss_out.comp b/gpu/vk/shaders/dlss_out.comp index 8b4b84c3a..30114cfac 100644 --- a/gpu/vk/shaders/dlss_out.comp +++ b/gpu/vk/shaders/dlss_out.comp @@ -1,17 +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). pc.square = 0: as it is. DLAA's picture is at the render -// size: scaled up to the output's with the sampler's linear filter. +// 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; int square; } pc; +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)); - if (pc.square != 0) c *= c; + 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 index 9d6f66d07..56cb5a392 100644 --- a/gpu/vk/shaders/dlss_out_comp_spv.h +++ b/gpu/vk/shaders/dlss_out_comp_spv.h @@ -1,77 +1,72 @@ // 1116.6.0 #pragma once const uint32_t DLSS_OUT_COMP_SPV[] = { - 0x07230203,0x00010600,0x0008000b,0x00000064,0x00000000,0x00020011,0x00000001,0x00020011, + 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,0x00000058,0x00060010,0x00000004,0x00000011, + 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,0x00050006,0x00000013,0x00000001, - 0x61757173,0x00006572,0x00030005,0x00000015,0x00006370,0x00030005,0x00000024,0x00000063, - 0x00030005,0x00000028,0x00637273,0x00030005,0x00000058,0x00747364,0x00040047,0x0000000d, - 0x0000000b,0x0000001c,0x00030047,0x00000013,0x00000002,0x00050048,0x00000013,0x00000000, - 0x00000023,0x00000000,0x00050048,0x00000013,0x00000001,0x00000023,0x00000008,0x00040047, - 0x00000028,0x00000021,0x00000000,0x00040047,0x00000028,0x00000022,0x00000000,0x00030047, - 0x00000058,0x00000019,0x00040047,0x00000058,0x00000021,0x00000001,0x00040047,0x00000058, - 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,0x0004001e, - 0x00000013,0x00000007,0x00000006,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,0x0004002b,0x00000006, - 0x0000004c,0x00000001,0x00040020,0x0000004d,0x00000009,0x00000006,0x00090019,0x00000056, - 0x00000021,0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000008,0x00040020, - 0x00000057,0x00000000,0x00000056,0x0004003b,0x00000057,0x00000058,0x00000000,0x0004002b, - 0x00000021,0x0000005c,0x3f800000,0x0004002b,0x0000000a,0x00000061,0x00000008,0x0004002b, - 0x0000000a,0x00000062,0x00000001,0x0006002c,0x0000000b,0x00000063,0x00000061,0x00000061, - 0x00000062,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,0x00050041,0x0000004d,0x0000004e, - 0x00000015,0x0000004c,0x0004003d,0x00000006,0x0000004f,0x0000004e,0x000500ab,0x0000001a, - 0x00000050,0x0000004f,0x00000016,0x000300f7,0x00000052,0x00000000,0x000400fa,0x00000050, - 0x00000051,0x00000052,0x000200f8,0x00000051,0x0004003d,0x00000022,0x00000053,0x00000024, - 0x0004003d,0x00000022,0x00000054,0x00000024,0x00050085,0x00000022,0x00000055,0x00000054, - 0x00000053,0x0003003e,0x00000024,0x00000055,0x000200f9,0x00000052,0x000200f8,0x00000052, - 0x0004003d,0x00000056,0x00000059,0x00000058,0x0004003d,0x00000007,0x0000005a,0x00000009, - 0x0004003d,0x00000022,0x0000005b,0x00000024,0x00050051,0x00000021,0x0000005d,0x0000005b, - 0x00000000,0x00050051,0x00000021,0x0000005e,0x0000005b,0x00000001,0x00050051,0x00000021, - 0x0000005f,0x0000005b,0x00000002,0x00070050,0x00000036,0x00000060,0x0000005d,0x0000005e, - 0x0000005f,0x0000005c,0x00040063,0x00000059,0x0000005a,0x00000060,0x000100fd,0x00010038 + 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/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index f3cb75c9f..0bb7d8d25 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -20,7 +20,6 @@ // the newest one it finds in them. Windows' paths are wide all the way (GetModuleFileNameW, _wgetenv), so any folder name // works. RX_VK_DLSS_PRESET=: DLSS's preset (default K). // RX_VK_DLSS_DEBUG=motion: the motion vectors shown instead of the picture. 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. @@ -375,7 +374,7 @@ bool make_pipelines() { 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 (!make(DLSS_PREP_COMP_SPV, sizeof DLSS_PREP_COMP_SPV, prep_types, 5, 84, s.prep_layout, s.prep) || - !make(DLSS_OUT_COMP_SPV, sizeof DLSS_OUT_COMP_SPV, out_types, 2, 12, s.out_layout, s.out)) { + !make(DLSS_OUT_COMP_SPV, sizeof DLSS_OUT_COMP_SPV, out_types, 2, 8, s.out_layout, s.out)) { s.prep = s.out = VK_NULL_HANDLE; return false; } @@ -555,16 +554,6 @@ void dlss_note_scene_draw(const Texture* color0, uint32_t width, uint32_t height 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; - static int told; - if (told++ % 3000 == 0) { // (now and then: what the camera's buffer held, to check its layout) - std::string line; - for (int i = 0; i < 0x230 / 4; i++) { - char b[24]; - snprintf(b, sizeof b, "%s%g", i % 4 ? " " : i ? " | " : "", c[i]); - line += b; - } - logf("vk: NVIDIA DLSS: the scene camera (%ux%u): %s", width, height, line.c_str()); - } 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) @@ -712,10 +701,9 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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; RX_VK_DLSS_JITTER_SIGN=x, y or xy: those turned round, to check) - static const char* sign = getenv("RX_VK_DLSS_JITTER_SIGN") ? getenv("RX_VK_DLSS_JITTER_SIGN") : ""; - ep.InJitterOffsetX = cam.jitter[0] * 0.5f * (float)rw * (strchr(sign, 'x') ? -1.0f : 1.0f); - ep.InJitterOffsetY = -cam.jitter[1] * 0.5f * (float)rh * (strchr(sign, 'y') ? -1.0f : 1.0f); + // (the jitter in render pixels, x right and y down) + ep.InJitterOffsetX = cam.jitter[0] * 0.5f * (float)rw; + ep.InJitterOffsetY = -cam.jitter[1] * 0.5f * (float)rh; ep.InRenderSubrectDimensions = {rw, rh}; ep.InReset = reset ? 1 : 0; ep.InMVScaleX = ep.InMVScaleY = 1.0f; @@ -728,8 +716,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk } full_barrier(cmd); { - static const bool square = env_switch("RX_VK_DLSS_SQUARE") != 0; - const int32_t opc[3] = {(int32_t)ow, (int32_t)oh, square && !show_motion ? 1 : 0}; + 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}}; From 582266b11ca0752c60d26219b994a9307595d253 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:45:21 -0500 Subject: [PATCH 32/52] DLSS: take another network of the upscale's shape only with the gameplay input's format A cooperative-matrix network sampling one texture into one image was taken for a fixed share's gameplay upscale whenever its input had the scene's size and its output was B10G11R11. Its input must now be B10G11R11 too, the format the gameplay networks sample (fd029a31 and the per-share ones, seen in game), so another network of that shape (an RGB9E5 or RGBA8 input) is never replaced. The shader's declared image format cannot be used for this in vk_draw.cpp: it is typeless there. --- gpu/vk/vk_dlss.cpp | 5 ++++- gpu/vk/vk_draw.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 0bb7d8d25..af2b60817 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -604,8 +604,11 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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 || !c.valid || texture_frame_no() - c.frame > 1 || in->width != c.width || in->height != c.height || - out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || (out->width >> out_level) <= in->width) + in->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || + (out->width >> out_level) <= in->width) return 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)", diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index 763ea0032..2499b0644 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -5576,7 +5576,7 @@ 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 and the image the picture) + // 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 && From ec2c4e47b9eca315d7ef8e33296b453eb4569f31 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:45:36 -0500 Subject: [PATCH 33/52] DLSS: correct stale comments vk_dlss.cpp said there was no jitter yet; vk_dlss.h called DLSS the game's 4/3 upscale, when it takes every share's; the note on the gameplay networks sat orphaned above struct Image and moves to dlss_upscale's declaration. --- gpu/vk/vk_dlss.cpp | 7 ++----- gpu/vk/vk_dlss.h | 5 +++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index af2b60817..1c9e453dc 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -8,7 +8,8 @@ // 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 none: only the -// camera's motion, what moves by itself ghosts). No jitter yet (the game's camera has none). +// camera's motion, what moves by itself ghosts). 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. // - 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 @@ -84,10 +85,6 @@ bool dlss_full_scale() { return dlss_mode() == 2; } namespace { -// (the gameplay networks, which vk_draw.cpp's dlss_stand_in finds: the 4/3 upscale (fd029a317a9a4b94) when the 3D is drawn at -// 3/4 of the picture, the game's dynamic resolution's usual size; a pass at the same size (9dc98871eb7fc101) when it is drawn -// at full size, the Render resolution row's 100% and DLAA's; and a network of its own for each other fixed share) - struct Image { VkImage image = VK_NULL_HANDLE; VmaAllocation memory = nullptr; diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index 998bbdc15..e7b2e9900 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -18,7 +18,7 @@ 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 (the game's 4/3 upscale), 2 DLAA (anti-aliasing at the render size, the 3D at full scale): +/// 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) @@ -32,7 +32,8 @@ void dlss_device_extensions(std::vector& want, const std::vector Date: Mon, 9 Nov 2026 02:45:56 -0500 Subject: [PATCH 34/52] DLSS: say where NGX looked for the runtime when it starts The log's "DLSS from" branch was always "the folders searched": there is always more than one folder. It now names them, and that the newest runtime among them is the one used. --- gpu/vk/vk_dlss.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 1c9e453dc..791c4036d 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -316,9 +316,9 @@ bool ngx_init() { 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) { - std::string where; - for (const auto& d : dirs) where += (where.empty() ? "" : ", ") + shown(d); // (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); @@ -334,7 +334,7 @@ bool ngx_init() { return false; } if (NVSDK_NGX_FAILED(NVSDK_NGX_VULKAN_AllocateParameters(&s.params)) || !s.params) return false; - logf("vk: NVIDIA DLSS: NGX started (DLSS from %s)", dirs.size() > 1 ? "the folders searched" : shown(dirs[0]).c_str()); + 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; From 612f178a43d33d6a62bfe7b1d4cd37ac9268ed16 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:46:37 -0500 Subject: [PATCH 35/52] DLSS: no leaks in make_pipelines The descriptor set layouts were never kept or destroyed, and a pipeline whose later step failed left its layout behind, with the other pass's handles reset over made objects. Each pipeline is now made by make_pipeline, which destroys what it made when a step fails; the set layouts are kept in State; and the samplers count in the early return. --- gpu/vk/vk_dlss.cpp | 71 ++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 791c4036d..135849625 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -113,6 +113,7 @@ struct State { 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; 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; @@ -340,47 +341,55 @@ bool ngx_init() { 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) return true; + if (s.prep && s.out && s.point && s.linear) return true; if (!dev.push_descriptor || !vkCmdPushDescriptorSetKHR) return false; - const auto make = [](const uint32_t* code, size_t size, const VkDescriptorType* types, uint32_t n, uint32_t push, - VkPipelineLayout& layout, VkPipeline& pipe) { - 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; - VkDescriptorSetLayout set{}; - if (vkCreateDescriptorSetLayout(dev.device, &li, nullptr, &set) != VK_SUCCESS) return false; - 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 false; - 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 false; - 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; - }; 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 (!make(DLSS_PREP_COMP_SPV, sizeof DLSS_PREP_COMP_SPV, prep_types, 5, 84, s.prep_layout, s.prep) || - !make(DLSS_OUT_COMP_SPV, sizeof DLSS_OUT_COMP_SPV, out_types, 2, 8, s.out_layout, s.out)) { - s.prep = s.out = VK_NULL_HANDLE; + 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 (vkCreateSampler(dev.device, &si, nullptr, &s.point) != VK_SUCCESS) return false; + 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; - return vkCreateSampler(dev.device, &si, nullptr, &s.linear) == VK_SUCCESS; + 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) From 49f0fb86b78f926e4386d520adae914e2153af0b Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:46:50 -0500 Subject: [PATCH 36/52] DLSS: name the jitter's options HOS_OPT_DLSS_JITTER_W and _H rx_option 18 and 19 were bare numbers in vk_replay.cpp and rx_backend.c; they are named in vk_api.h beside HOS_OPT_CHEAT_*. --- gpu/vk/vk_api.h | 3 +++ gpu/vk/vk_dlss.h | 2 +- gpu/vk/vk_replay.cpp | 2 +- host/rx_backend.c | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gpu/vk/vk_api.h b/gpu/vk/vk_api.h index adccbc187..fbdb879dc 100644 --- a/gpu/vk/vk_api.h +++ b/gpu/vk/vk_api.h @@ -17,6 +17,9 @@ extern "C" { #define MKWVK_API_VERSION 6 enum { HOS_OPT_CHEAT_HEARTS = 32, HOS_OPT_CHEAT_FLIGHT, HOS_OPT_CHEAT_ACTION, HOS_OPT_CHEAT_CAMERA }; +/* (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 */ +enum { HOS_OPT_DLSS_JITTER_W = 18, HOS_OPT_DLSS_JITTER_H = 19 }; /* (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.h b/gpu/vk/vk_dlss.h index e7b2e9900..6a56ab336 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -45,7 +45,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk /// deferred frees are let go, so its video memory comes back. Nothing to do costs one atomic load void dlss_idle(); -/// The scene's width (axis 0) or height (1) for the host's camera jitter (nx2/oot_jitter.py, rx_option 18 and 19): 0 when +/// 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) uint32_t dlss_jitter_size(int axis); diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 32c482d44..1d39929ad 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -3569,7 +3569,7 @@ void fill_replay_table(MkwVkRenderer& t) { if (id == HOS_OPT_CHEAT_CAMERA) return menu_free_camera(); 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 == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : - id == 18 || id == 19 ? (int)dlss_jitter_size(id - 18) : 0; + id == HOS_OPT_DLSS_JITTER_W || id == HOS_OPT_DLSS_JITTER_H ? (int)dlss_jitter_size(id - HOS_OPT_DLSS_JITTER_W) : 0; }; t.option_set = menu_option_set; t.nvn_register_sampler = register_sampler_queued; diff --git a/host/rx_backend.c b/host/rx_backend.c index b3a7a99da..6854aa8bc 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -292,7 +292,7 @@ void rx_nvn_present(const nvn_tex *t, const int crop[4]) { } /* (hookshot, nx2/oot_jitter.py) NVIDIA DLSS's camera jitter: this frame's sub-pixel offset of the scene camera, in sead's projection offset (a fraction of the view's width and height), into out; 0 when there is none (DLSS off, or a renderer - without it: rx_option 18 and 19, the scene's size in pixels, are 0). The renderer reads the offset the camera ended up + without it: rx_option HOS_OPT_DLSS_JITTER_W and _H, the scene's size in pixels, are 0). The renderer reads the offset the camera ended up with 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 16 frames. */ static int dlss_jitter(float out[2]) { @@ -301,7 +301,7 @@ static int dlss_jitter(float out[2]) { const LONG frame = presents; if (frame != at) { /* (asked once a frame: the renderer's lock is not taken for every camera) */ at = frame; - w = (float)rx_option(18, 0), h = (float)rx_option(19, 0); + w = (float)rx_option(HOS_OPT_DLSS_JITTER_W, 0), h = (float)rx_option(HOS_OPT_DLSS_JITTER_H, 0); } if (w < 1.0f || h < 1.0f) return 0; const int i = (int)(frame % 16) + 1; From f77726f898883344257a21eca974bd2d9fd736ea Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 02:53:48 -0500 Subject: [PATCH 37/52] DLSS: the camera math in dlss_math.h, tested on the CPU reprojection(), the jitter taken off the frame before's camera, the jitter in DLSS's pixels and the Halton sequence (in C, for host/rx_backend.c) move to gpu/vk/dlss_math.h, unchanged. tests/test_dlss_math.py builds it as C and C++ and checks the sequence (the same as before, inside the pixel, centred, each phase its own), the reprojection against matrix products, that a still camera's jitter makes no motion and a turning camera's motion is the same with and without jitter, and the jitter's pixels and signs. test_upscaler_rows.py's exact-text checks go: the rules are checked by behaviour in test_upscaler_rows.cpp, and the script checks only the rows' order, the saved keys and that the menu calls upscaler_rows.h. --- gpu/vk/dlss_math.h | 78 ++++++++++++++++++++ gpu/vk/vk_dlss.cpp | 43 ++--------- host/rx_backend.c | 10 ++- tests/test_dlss_math.cpp | 140 ++++++++++++++++++++++++++++++++++++ tests/test_dlss_math.py | 33 +++++++++ tests/test_upscaler_rows.py | 47 ++++++------ 6 files changed, 288 insertions(+), 63 deletions(-) create mode 100644 gpu/vk/dlss_math.h create mode 100644 tests/test_dlss_math.cpp create mode 100644 tests/test_dlss_math.py diff --git a/gpu/vk/dlss_math.h b/gpu/vk/dlss_math.h new file mode 100644 index 000000000..e0189ce1f --- /dev/null +++ b/gpu/vk/dlss_math.h @@ -0,0 +1,78 @@ +/* (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; +} + +#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/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 135849625..0a09df476 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -38,6 +38,7 @@ #include #include +#include "dlss_math.h" #include "vk_draw.h" #include "vk_menu.h" #include "vk_paths.h" @@ -414,36 +415,6 @@ VkImageView depth_view(Texture* t) { return v; } -/// this frame's clip space to the frame before's: previous * inverse(current), row major; false when singular -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; -} - /// DLSS's feature for this output size, made when the size 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(); @@ -678,11 +649,8 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk // (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); - for (int c = 0; c < 4; c++) { - previous[0 * 4 + c] -= s.last_jitter[0] * previous[3 * 4 + c]; - previous[1 * 4 + c] -= s.last_jitter[1] * previous[3 * 4 + c]; - } - const bool motion = !reset && reprojection(cam.current, previous, pc.reproject); + 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); @@ -711,8 +679,9 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk ep.Feature.pInColor = &color, ep.Feature.pInOutput = &output; ep.pInDepth = &depth, ep.pInMotionVectors = &mv; // (the jitter in render pixels, x right and y down) - ep.InJitterOffsetX = cam.jitter[0] * 0.5f * (float)rw; - ep.InJitterOffsetY = -cam.jitter[1] * 0.5f * (float)rh; + 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; diff --git a/host/rx_backend.c b/host/rx_backend.c index 6854aa8bc..a7c20e504 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); @@ -304,12 +305,9 @@ static int dlss_jitter(float out[2]) { w = (float)rx_option(HOS_OPT_DLSS_JITTER_W, 0), h = (float)rx_option(HOS_OPT_DLSS_JITTER_H, 0); } if (w < 1.0f || h < 1.0f) return 0; - const int i = (int)(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); - out[0] = (x - 0.5f) / w, out[1] = (y - 0.5f) / h; + float px[2]; + dlss_jitter_pixels((unsigned)frame, 16, px); + out[0] = px[0] / w, out[1] = px[1] / h; return 1; } /* The cameras jittered, each with its own offset from before the jitter: the jitter goes on top of it, and once there is no diff --git a/tests/test_dlss_math.cpp b/tests/test_dlss_math.cpp new file mode 100644 index 000000000..3a05fa690 --- /dev/null +++ b/tests/test_dlss_math.cpp @@ -0,0 +1,140 @@ +#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 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)); + 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..a903de64e --- /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, and the jitter in DLSS's pixels. 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_upscaler_rows.py b/tests/test_upscaler_rows.py index 641b3d2f4..142bbed73 100644 --- a/tests/test_upscaler_rows.py +++ b/tests/test_upscaler_rows.py @@ -4,6 +4,7 @@ python -I -B tests/test_upscaler_rows.py [--cxx g++] """ import argparse +import re import subprocess import tempfile from pathlib import Path @@ -11,6 +12,15 @@ 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++") @@ -27,27 +37,24 @@ def main(): 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 - key = menu[menu.index("const char* ini_key(const Row& r) {"):] - key = key[:key.index("\n}\n")] - for need in ('&opts.resolution) return "resolution"', '&opts.upscale) return "upscaling"', '&opts.dlss) return "dlss"'): - if need not in key: - raise SystemExit("ini_key: %s is missing (an options file from before would lose it)" % need) - if "ur::step(upscaler(), *r.value, dir)" not in menu or "ur::step_upscaler(upscaler(), dir, true)" not in menu: - raise SystemExit("the Upscaler and Render resolution rows do not step through upscaler_rows.h") - fixed = menu[menu.index("bool row_fixed(const int* value) {"):] - fixed = fixed[:fixed.index("\n}\n")] - for need in ("ur::upscaler_fixed(dlss_why() == DLSS_OK)", "&opts.reflex && !reflex_available()", - "ur::resolution_fixed(upscaler())"): - if need not in fixed: - raise SystemExit("row_fixed: %s is missing (a choice that cannot run here would be offered)" % need) - if "const bool stays = row_fixed(r.value);" not in menu: - raise SystemExit("the rows step past row_fixed") - if "if (r.value == &opts.dlss) opts.resolution = ur::snap(upscaler(), opts.resolution);" not in menu: - raise SystemExit("another Upscaler leaves the Render resolution off its steps") + # 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()"), + "row_shown": ("ur::upscaler_shown(dlss_built())",), + "menu_tick": ("ur::step_upscaler(", "ur::step(", "ur::snap(", "row_fixed("), + } + for name, calls in uses.items(): + body = function(menu, "bool %s(" % name if name != "menu_tick" else "void menu_tick() {") + for call in calls: + if call not in body: + raise SystemExit("%s does not use %s (upscaler_rows.h)" % (name, call)) dlss = (ROOT / "gpu" / "vk" / "vk_dlss.cpp").read_text(encoding="utf-8") - mode = dlss[dlss.index("int dlss_mode() {"):] - if "dlss_why() != DLSS_OK" not in mode[:mode.index("\n}\n")]: + 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" From ccdf548f3b049b64103eb8ae10f836b37461570d Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:06:20 -0500 Subject: [PATCH 38/52] DLSS jitter: a sequence as long as DLSS's scale wants The Halton sequence was 16 frames at every scale. NVIDIA's guide asks for about 8 x (output / render)^2 phases, so each output pixel is covered about 8 times: dlss_jitter_phases gives that, 8 (DLAA) to 64 (Ultra Performance's 72 capped), and the renderer hands it to the host as HOS_OPT_DLSS_JITTER_PHASES. --- gpu/vk/dlss_math.h | 10 ++++++++++ gpu/vk/vk_api.h | 2 +- gpu/vk/vk_dlss.cpp | 4 +++- gpu/vk/vk_dlss.h | 3 ++- gpu/vk/vk_replay.cpp | 3 ++- host/rx_backend.c | 7 +++++-- tests/test_dlss_math.cpp | 8 ++++++++ 7 files changed, 31 insertions(+), 6 deletions(-) diff --git a/gpu/vk/dlss_math.h b/gpu/vk/dlss_math.h index e0189ce1f..970b4155a 100644 --- a/gpu/vk/dlss_math.h +++ b/gpu/vk/dlss_math.h @@ -25,6 +25,16 @@ static inline void dlss_jitter_pixels(unsigned frame, int phases, float out[2]) out[1] = dlss_halton(i, 3) - 0.5f; } +/* 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 { diff --git a/gpu/vk/vk_api.h b/gpu/vk/vk_api.h index fbdb879dc..d833f76d0 100644 --- a/gpu/vk/vk_api.h +++ b/gpu/vk/vk_api.h @@ -19,7 +19,7 @@ extern "C" { enum { HOS_OPT_CHEAT_HEARTS = 32, HOS_OPT_CHEAT_FLIGHT, HOS_OPT_CHEAT_ACTION, HOS_OPT_CHEAT_CAMERA }; /* (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 */ -enum { HOS_OPT_DLSS_JITTER_W = 18, HOS_OPT_DLSS_JITTER_H = 19 }; +enum { HOS_OPT_DLSS_JITTER_W = 18, HOS_OPT_DLSS_JITTER_H = 19, HOS_OPT_DLSS_JITTER_PHASES = 20 }; /* (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 index 0a09df476..7939adeb1 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -123,6 +123,7 @@ struct State { 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; @@ -545,6 +546,7 @@ uint32_t dlss_jitter_size(int axis) { 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); } @@ -714,7 +716,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk logf("vk: NVIDIA DLSS: %d frames drawn; this one's jitter %+.3f %+.3f px%s", evaluations, ep.InJitterOffsetX, ep.InJitterOffsetY, motion ? "" : ", no motion"); std::memcpy(s.last_vp, cam.current, sizeof s.last_vp); - s.jitter_w = rw, s.jitter_h = rh; + 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; if (!s.told_first) { diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index 6a56ab336..e590c57b8 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -46,7 +46,8 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk void dlss_idle(); /// 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) +/// 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_replay.cpp b/gpu/vk/vk_replay.cpp index 1d39929ad..b8c3cdc85 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -3569,7 +3569,8 @@ void fill_replay_table(MkwVkRenderer& t) { if (id == HOS_OPT_CHEAT_CAMERA) return menu_free_camera(); 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 == 16 ? opts.invert_cam_x : id == 17 ? opts.invert_cam_y : - id == HOS_OPT_DLSS_JITTER_W || id == HOS_OPT_DLSS_JITTER_H ? (int)dlss_jitter_size(id - HOS_OPT_DLSS_JITTER_W) : 0; + 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/host/rx_backend.c b/host/rx_backend.c index a7c20e504..ced01ec00 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -295,18 +295,21 @@ void rx_nvn_present(const nvn_tex *t, const int crop[4]) { projection offset (a fraction of the view's width and height), into out; 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). The renderer reads the offset the camera ended up with 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 16 frames. */ + 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). */ static int dlss_jitter(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 camera) */ 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); } if (w < 1.0f || h < 1.0f) return 0; float px[2]; - dlss_jitter_pixels((unsigned)frame, 16, px); + dlss_jitter_pixels((unsigned)frame, phases, px); out[0] = px[0] / w, out[1] = px[1] / h; return 1; } diff --git a/tests/test_dlss_math.cpp b/tests/test_dlss_math.cpp index 3a05fa690..5a011b2bf 100644 --- a/tests/test_dlss_math.cpp +++ b/tests/test_dlss_math.cpp @@ -77,6 +77,14 @@ int main() { } 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]; From b3c2cde0c037ff51c419f644a8aa043a46712f29 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:06:21 -0500 Subject: [PATCH 39/52] readme: the Upscaler row only in DLSS builds; the filter row; Reflex's log --- readme.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index aae4158e1..c570adcdd 100644 --- a/readme.md +++ b/readme.md @@ -195,7 +195,7 @@ and the right one closes the menu. Your choices are saved in `hookshot_options.i | | Frame pacing | Off, On | Off | | | NVIDIA Reflex | Off, On, On + Boost (NVIDIA GPUs with `VK_NV_low_latency2`) | Off | | | Output resolution | 2048x1152, 2560x1440, 3840x2160 (after a restart) | 2048x1152 | -| | Upscaler | Game's own, NVIDIA DLSS, NVIDIA DLAA (DLSS and DLAA: builds with NVIDIA's DLSS SDK, below, on RTX GPUs) | Game's own | +| | 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 | | | 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% | @@ -230,7 +230,11 @@ and the right one closes the menu. Your choices are saved in `hookshot_options.i 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. At 3840x2160 the game's render targets get a bigger GPU pool, so a full-size 3D fits. `HOS_OUTPUT_RES=0/1/2` sets it for one run. -- **Window scaling filter** 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. From fbca4fd0ab65a24aab1fd6fe75a748fb133a62e2 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:25:31 -0500 Subject: [PATCH 40/52] Build: dlss_math.h in the host objects' key and in the AppImage host/rx_backend.c includes gpu/vk/dlss_math.h, which the host objects' key did not hash (a change to it would have left rx_backend.o stale) and the AppImage did not carry (a player's build there would not compile). --- nx2/build.py | 1 + scripts/linux/appimage.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/nx2/build.py b/nx2/build.py index 6ee1ceb79..8f568a4ff 100644 --- a/nx2/build.py +++ b/nx2/build.py @@ -684,6 +684,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")] + diff --git a/scripts/linux/appimage.sh b/scripts/linux/appimage.sh index a9f9ba735..e3d1aebe5 100755 --- a/scripts/linux/appimage.sh +++ b/scripts/linux/appimage.sh @@ -49,6 +49,7 @@ cp -a builder/linux "$T/builder/" cp -a gpu/render/*.h "$T/gpu/render/" cp -a gpu/vk/vk_api.h "$T/gpu/vk/" cp -a gpu/vk/oot_cheat_items.h "$T/gpu/vk/" +cp -a gpu/vk/dlss_math.h "$T/gpu/vk/" # (host/rx_backend.c's DLSS jitter) cp -a third_party/opus "$T/third_party/" mkdir -p "$T/third_party/zbic" "$T/third_party/a64-kit" cp -a third_party/zbic/zbic.so third_party/zbic/LICENSE third_party/zbic/PROVENANCE.md "$T/third_party/zbic/" From 6011b34b5e89be5a93763b94661cf1ba407fe43f Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:44:03 -0500 Subject: [PATCH 41/52] vk: RX_VK_SHOT_GO=, every present shot while that file is there --- gpu/vk/vk_replay.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index b8c3cdc85..5f5f8e060 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 @@ -2565,6 +2566,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; From 26a5247cd90a3866569e815b8786ea0b968b5e1c Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:44:13 -0500 Subject: [PATCH 42/52] DLSS: RX_VK_DLSS_TRACE, a frame's draws into the scene logged (targets, geometry, uniform buffers) --- gpu/vk/vk_draw.cpp | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index 3abd283fe..1bff2d7d9 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -4847,6 +4847,41 @@ 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); +} + void draw(VkCommandBuffer cmd, NvnState& s, const DrawArgs& d) { PROF(PROF_DRAW); FINE_N(FC_DRAWS, 1); @@ -4967,6 +5002,7 @@ 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); 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 From 8b925b67b17efa2f7ac3e7ece51940892b13bd56 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 03:44:17 -0500 Subject: [PATCH 43/52] DLSS: actor motion, the skinned draws' own motion vectors (Link and the others no longer ghost) OoT has no velocity buffer: its scene pass draws into one B10G11R11 target and its upscale network samples the colour alone, so DLSS had the camera's motion only and everything that moves by itself ghosted. The skinned draws into the scene (indexed draws with the weights at attributes 4 and 5, the joints at 6 and 7, the palette in the vertex stage's binding 3, the origin at binding 1's 0x40: Link, the other characters, the animals) are now noted as they draw and, at the upscale, drawn again over the camera's motion: each vertex skinned with this frame's bones and the frame before's, read where the game has them by their device addresses. The poses are copied on the GPU into two buffers that take turns, so nothing is read back; a pixel takes the draw's motion where its depth is the scene's. Headless, Link running past the camera, the same 100 frames against the game's own upscale: the error over Link 16.0 -> 9.7 (40% less, better in every frame). RX_VK_DLSS_ACTORS=0: camera motion alone; RX_VK_DLSS_DEBUG=motion shows the actors' motion; RX_VK_DLSS_DEBUG=alternate gives the game's picture every other frame, a reference for tests. --- gpu/vk/dlss_actor.h | 56 ++++ gpu/vk/shaders/dlss_actor.frag | 38 +++ gpu/vk/shaders/dlss_actor.vert | 77 +++++ gpu/vk/shaders/dlss_actor_frag_spv.h | 137 +++++++++ gpu/vk/shaders/dlss_actor_vert_spv.h | 421 +++++++++++++++++++++++++++ gpu/vk/shaders/dlss_prep.comp | 14 +- gpu/vk/shaders/dlss_prep_comp_spv.h | 269 +++++++++-------- gpu/vk/vk_dlss.cpp | 371 ++++++++++++++++++++++- gpu/vk/vk_dlss.h | 18 ++ gpu/vk/vk_draw.cpp | 40 ++- tests/test_dlss_actor.cpp | 31 ++ tests/test_dlss_actor.py | 69 +++++ 12 files changed, 1402 insertions(+), 139 deletions(-) create mode 100644 gpu/vk/dlss_actor.h create mode 100644 gpu/vk/shaders/dlss_actor.frag create mode 100644 gpu/vk/shaders/dlss_actor.vert create mode 100644 gpu/vk/shaders/dlss_actor_frag_spv.h create mode 100644 gpu/vk/shaders/dlss_actor_vert_spv.h create mode 100644 tests/test_dlss_actor.cpp create mode 100644 tests/test_dlss_actor.py 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/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_prep.comp b/gpu/vk/shaders/dlss_prep.comp index 665ab65b5..997597065 100644 --- a/gpu/vk/shaders/dlss_prep.comp +++ b/gpu/vk/shaders/dlss_prep.comp @@ -5,16 +5,17 @@ // - 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 (Link, the -// others, water, particles) gets the motion of what is behind it. pc.flags bit 0: motion (none on the first frame); -// bit 1: the motion shown as the colour instead (RX_VK_DLSS_DEBUG=motion) +// 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 writeonly image2D motion; +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) @@ -26,6 +27,11 @@ void main() { 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; diff --git a/gpu/vk/shaders/dlss_prep_comp_spv.h b/gpu/vk/shaders/dlss_prep_comp_spv.h index 4a54c7faa..fcd1b77ab 100644 --- a/gpu/vk/shaders/dlss_prep_comp_spv.h +++ b/gpu/vk/shaders/dlss_prep_comp_spv.h @@ -1,10 +1,10 @@ // 1116.6.0 #pragma once const uint32_t DLSS_PREP_COMP_SPV[] = { - 0x07230203,0x00010600,0x0008000b,0x000000cf,0x00000000,0x00020011,0x00000001,0x00020011, + 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,0x00000038,0x00000040,0x0000004e,0x00000056,0x000000b2,0x00060010,0x00000004, + 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, @@ -12,23 +12,23 @@ const uint32_t DLSS_PREP_COMP_SPV[] = { 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,0x00000034,0x00000063,0x00040005,0x00000038,0x6e656373,0x00000065,0x00040005, - 0x00000040,0x6f6c6f63,0x00000072,0x00030005,0x0000004d,0x00000064,0x00050005,0x0000004e, - 0x6e656373,0x65645f65,0x00687470,0x00040005,0x00000056,0x74706564,0x00000068,0x00030005, - 0x0000005b,0x0000766d,0x00040005,0x0000006e,0x70696c63,0x00000000,0x00040005,0x0000007d, - 0x6f666562,0x00006572,0x00040005,0x0000008a,0x6e656874,0x00000000,0x00030005,0x00000091, - 0x00776f6e,0x00030005,0x00000098,0x00007870,0x00040005,0x000000b2,0x69746f6d,0x00006e6f, - 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,0x00000038,0x00000021,0x00000000, - 0x00040047,0x00000038,0x00000022,0x00000000,0x00030047,0x00000040,0x00000019,0x00040047, - 0x00000040,0x00000021,0x00000002,0x00040047,0x00000040,0x00000022,0x00000000,0x00040047, - 0x0000004e,0x00000021,0x00000001,0x00040047,0x0000004e,0x00000022,0x00000000,0x00030047, - 0x00000056,0x00000019,0x00040047,0x00000056,0x00000021,0x00000003,0x00040047,0x00000056, - 0x00000022,0x00000000,0x00030047,0x000000b2,0x00000019,0x00040047,0x000000b2,0x00000021, - 0x00000004,0x00040047,0x000000b2,0x00000022,0x00000000,0x00020013,0x00000002,0x00030021, + 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, @@ -39,35 +39,36 @@ const uint32_t DLSS_PREP_COMP_SPV[] = { 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,0x00040017,0x00000032,0x00000013, - 0x00000003,0x00040020,0x00000033,0x00000007,0x00000032,0x00090019,0x00000035,0x00000013, - 0x00000001,0x00000000,0x00000000,0x00000000,0x00000001,0x00000000,0x0003001b,0x00000036, - 0x00000035,0x00040020,0x00000037,0x00000000,0x00000036,0x0004003b,0x00000037,0x00000038, - 0x00000000,0x0004002b,0x00000013,0x0000003b,0x00000000,0x00090019,0x0000003e,0x00000013, - 0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000002,0x00040020,0x0000003f, - 0x00000000,0x0000003e,0x0004003b,0x0000003f,0x00000040,0x00000000,0x0004002b,0x00000013, - 0x00000044,0x3f800000,0x00040020,0x0000004c,0x00000007,0x00000013,0x0004003b,0x00000037, - 0x0000004e,0x00000000,0x0004002b,0x0000000a,0x00000052,0x00000000,0x00090019,0x00000054, - 0x00000013,0x00000001,0x00000000,0x00000000,0x00000000,0x00000002,0x00000003,0x00040020, - 0x00000055,0x00000000,0x00000054,0x0004003b,0x00000055,0x00000056,0x00000000,0x0005002c, - 0x00000016,0x0000005c,0x0000003b,0x0000003b,0x0004002b,0x00000006,0x0000005d,0x00000003, - 0x00040020,0x0000005e,0x00000009,0x00000006,0x0004002b,0x00000006,0x00000061,0x00000001, - 0x0004002b,0x00000006,0x00000063,0x00000000,0x00040020,0x0000006d,0x00000007,0x00000014, - 0x0004002b,0x00000013,0x00000071,0x40000000,0x0004002b,0x0000000a,0x00000074,0x00000001, - 0x00040020,0x0000007e,0x00000009,0x00000015,0x0004002b,0x0000000a,0x00000083,0x00000003, - 0x0004002b,0x00000013,0x00000086,0x358637bd,0x00040020,0x00000094,0x00000009,0x00000016, - 0x0004002b,0x00000013,0x000000a9,0x46800000,0x0005002c,0x00000016,0x000000aa,0x000000a9, - 0x000000a9,0x00090019,0x000000b0,0x00000013,0x00000001,0x00000000,0x00000000,0x00000000, - 0x00000002,0x00000007,0x00040020,0x000000b1,0x00000000,0x000000b0,0x0004003b,0x000000b1, - 0x000000b2,0x00000000,0x0004002b,0x00000013,0x000000c3,0x41800000,0x0004002b,0x0000000a, - 0x000000cd,0x00000008,0x0006002c,0x0000000b,0x000000ce,0x000000cd,0x000000cd,0x00000074, - 0x00050036,0x00000002,0x00000004,0x00000000,0x00000003,0x000200f8,0x00000005,0x0004003b, - 0x00000008,0x00000009,0x00000007,0x0004003b,0x00000025,0x00000026,0x00000007,0x0004003b, - 0x00000025,0x0000002a,0x00000007,0x0004003b,0x00000033,0x00000034,0x00000007,0x0004003b, - 0x0000004c,0x0000004d,0x00000007,0x0004003b,0x00000025,0x0000005b,0x00000007,0x0004003b, - 0x0000006d,0x0000006e,0x00000007,0x0004003b,0x0000006d,0x0000007d,0x00000007,0x0004003b, - 0x00000025,0x0000008a,0x00000007,0x0004003b,0x00000025,0x00000091,0x00000007,0x0004003b, - 0x00000025,0x00000098,0x00000007,0x0004003d,0x0000000b,0x0000000f,0x0000000d,0x0007004f, + 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, @@ -79,81 +80,99 @@ const uint32_t DLSS_PREP_COMP_SPV[] = { 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,0x0004003d,0x00000036,0x00000039, - 0x00000038,0x0004003d,0x00000016,0x0000003a,0x0000002a,0x00070058,0x00000014,0x0000003c, - 0x00000039,0x0000003a,0x00000002,0x0000003b,0x0008004f,0x00000032,0x0000003d,0x0000003c, - 0x0000003c,0x00000000,0x00000001,0x00000002,0x0003003e,0x00000034,0x0000003d,0x0004003d, - 0x0000003e,0x00000041,0x00000040,0x0004003d,0x00000007,0x00000042,0x00000009,0x0004003d, - 0x00000032,0x00000043,0x00000034,0x00060050,0x00000032,0x00000045,0x0000003b,0x0000003b, - 0x0000003b,0x00060050,0x00000032,0x00000046,0x00000044,0x00000044,0x00000044,0x0008000c, - 0x00000032,0x00000047,0x00000001,0x0000002b,0x00000043,0x00000045,0x00000046,0x00050051, - 0x00000013,0x00000048,0x00000047,0x00000000,0x00050051,0x00000013,0x00000049,0x00000047, - 0x00000001,0x00050051,0x00000013,0x0000004a,0x00000047,0x00000002,0x00070050,0x00000014, - 0x0000004b,0x00000048,0x00000049,0x0000004a,0x00000044,0x00040063,0x00000041,0x00000042, - 0x0000004b,0x0004003d,0x00000036,0x0000004f,0x0000004e,0x0004003d,0x00000016,0x00000050, - 0x0000002a,0x00070058,0x00000014,0x00000051,0x0000004f,0x00000050,0x00000002,0x0000003b, - 0x00050051,0x00000013,0x00000053,0x00000051,0x00000000,0x0003003e,0x0000004d,0x00000053, - 0x0004003d,0x00000054,0x00000057,0x00000056,0x0004003d,0x00000007,0x00000058,0x00000009, - 0x0004003d,0x00000013,0x00000059,0x0000004d,0x00070050,0x00000014,0x0000005a,0x00000059, - 0x00000059,0x00000059,0x00000059,0x00040063,0x00000057,0x00000058,0x0000005a,0x0003003e, - 0x0000005b,0x0000005c,0x00050041,0x0000005e,0x0000005f,0x00000019,0x0000005d,0x0004003d, - 0x00000006,0x00000060,0x0000005f,0x000500c7,0x00000006,0x00000062,0x00000060,0x00000061, - 0x000500ab,0x0000001e,0x00000064,0x00000062,0x00000063,0x0004003d,0x00000013,0x00000065, - 0x0000004d,0x000500be,0x0000001e,0x00000066,0x00000065,0x0000003b,0x000500a7,0x0000001e, - 0x00000067,0x00000064,0x00000066,0x0004003d,0x00000013,0x00000068,0x0000004d,0x000500bc, - 0x0000001e,0x00000069,0x00000068,0x00000044,0x000500a7,0x0000001e,0x0000006a,0x00000067, - 0x00000069,0x000300f7,0x0000006c,0x00000000,0x000400fa,0x0000006a,0x0000006b,0x0000006c, - 0x000200f8,0x0000006b,0x00050041,0x0000004c,0x0000006f,0x0000002a,0x00000052,0x0004003d, - 0x00000013,0x00000070,0x0000006f,0x00050085,0x00000013,0x00000072,0x00000070,0x00000071, - 0x00050083,0x00000013,0x00000073,0x00000072,0x00000044,0x00050041,0x0000004c,0x00000075, - 0x0000002a,0x00000074,0x0004003d,0x00000013,0x00000076,0x00000075,0x00050085,0x00000013, - 0x00000077,0x00000076,0x00000071,0x00050083,0x00000013,0x00000078,0x00000044,0x00000077, - 0x0004003d,0x00000013,0x00000079,0x0000004d,0x00050085,0x00000013,0x0000007a,0x00000079, - 0x00000071,0x00050083,0x00000013,0x0000007b,0x0000007a,0x00000044,0x00070050,0x00000014, - 0x0000007c,0x00000073,0x00000078,0x0000007b,0x00000044,0x0003003e,0x0000006e,0x0000007c, - 0x00050041,0x0000007e,0x0000007f,0x00000019,0x00000063,0x0004003d,0x00000015,0x00000080, - 0x0000007f,0x0004003d,0x00000014,0x00000081,0x0000006e,0x00050091,0x00000014,0x00000082, - 0x00000080,0x00000081,0x0003003e,0x0000007d,0x00000082,0x00050041,0x0000004c,0x00000084, - 0x0000007d,0x00000083,0x0004003d,0x00000013,0x00000085,0x00000084,0x000500ba,0x0000001e, - 0x00000087,0x00000085,0x00000086,0x000300f7,0x00000089,0x00000000,0x000400fa,0x00000087, - 0x00000088,0x00000089,0x000200f8,0x00000088,0x0004003d,0x00000014,0x0000008b,0x0000007d, - 0x0007004f,0x00000016,0x0000008c,0x0000008b,0x0000008b,0x00000000,0x00000001,0x00050041, - 0x0000004c,0x0000008d,0x0000007d,0x00000083,0x0004003d,0x00000013,0x0000008e,0x0000008d, - 0x00050050,0x00000016,0x0000008f,0x0000008e,0x0000008e,0x00050088,0x00000016,0x00000090, - 0x0000008c,0x0000008f,0x0003003e,0x0000008a,0x00000090,0x0004003d,0x00000014,0x00000092, - 0x0000006e,0x0007004f,0x00000016,0x00000093,0x00000092,0x00000092,0x00000000,0x00000001, - 0x00050041,0x00000094,0x00000095,0x00000019,0x00000061,0x0004003d,0x00000016,0x00000096, - 0x00000095,0x00050083,0x00000016,0x00000097,0x00000093,0x00000096,0x0003003e,0x00000091, - 0x00000097,0x00050041,0x0000004c,0x00000099,0x0000008a,0x00000052,0x0004003d,0x00000013, - 0x0000009a,0x00000099,0x00050041,0x0000004c,0x0000009b,0x00000091,0x00000052,0x0004003d, - 0x00000013,0x0000009c,0x0000009b,0x00050083,0x00000013,0x0000009d,0x0000009a,0x0000009c, - 0x00050041,0x0000004c,0x0000009e,0x00000091,0x00000074,0x0004003d,0x00000013,0x0000009f, - 0x0000009e,0x00050041,0x0000004c,0x000000a0,0x0000008a,0x00000074,0x0004003d,0x00000013, - 0x000000a1,0x000000a0,0x00050083,0x00000013,0x000000a2,0x0000009f,0x000000a1,0x00050050, - 0x00000016,0x000000a3,0x0000009d,0x000000a2,0x0005008e,0x00000016,0x000000a4,0x000000a3, - 0x0000002d,0x0004003d,0x00000016,0x000000a5,0x00000026,0x00050085,0x00000016,0x000000a6, - 0x000000a4,0x000000a5,0x0003003e,0x00000098,0x000000a6,0x0004003d,0x00000016,0x000000a7, - 0x00000098,0x0006000c,0x00000016,0x000000a8,0x00000001,0x00000004,0x000000a7,0x000500b8, - 0x0000001f,0x000000ab,0x000000a8,0x000000aa,0x0004009b,0x0000001e,0x000000ac,0x000000ab, - 0x000300f7,0x000000ae,0x00000000,0x000400fa,0x000000ac,0x000000ad,0x000000ae,0x000200f8, - 0x000000ad,0x0004003d,0x00000016,0x000000af,0x00000098,0x0003003e,0x0000005b,0x000000af, - 0x000200f9,0x000000ae,0x000200f8,0x000000ae,0x000200f9,0x00000089,0x000200f8,0x00000089, - 0x000200f9,0x0000006c,0x000200f8,0x0000006c,0x0004003d,0x000000b0,0x000000b3,0x000000b2, - 0x0004003d,0x00000007,0x000000b4,0x00000009,0x0004003d,0x00000016,0x000000b5,0x0000005b, - 0x00050051,0x00000013,0x000000b6,0x000000b5,0x00000000,0x00050051,0x00000013,0x000000b7, - 0x000000b5,0x00000001,0x00070050,0x00000014,0x000000b8,0x000000b6,0x000000b7,0x0000003b, - 0x0000003b,0x00040063,0x000000b3,0x000000b4,0x000000b8,0x00050041,0x0000005e,0x000000b9, - 0x00000019,0x0000005d,0x0004003d,0x00000006,0x000000ba,0x000000b9,0x000500c7,0x00000006, - 0x000000bb,0x000000ba,0x0000001a,0x000500ab,0x0000001e,0x000000bc,0x000000bb,0x00000063, - 0x000300f7,0x000000be,0x00000000,0x000400fa,0x000000bc,0x000000bd,0x000000be,0x000200f8, - 0x000000bd,0x0004003d,0x0000003e,0x000000bf,0x00000040,0x0004003d,0x00000007,0x000000c0, - 0x00000009,0x0004003d,0x00000016,0x000000c1,0x0000005b,0x0006000c,0x00000016,0x000000c2, - 0x00000001,0x00000004,0x000000c1,0x00050050,0x00000016,0x000000c4,0x000000c3,0x000000c3, - 0x00050088,0x00000016,0x000000c5,0x000000c2,0x000000c4,0x00050050,0x00000016,0x000000c6, - 0x0000003b,0x0000003b,0x00050050,0x00000016,0x000000c7,0x00000044,0x00000044,0x0008000c, - 0x00000016,0x000000c8,0x00000001,0x0000002b,0x000000c5,0x000000c6,0x000000c7,0x0004003d, - 0x00000013,0x000000c9,0x0000004d,0x00050051,0x00000013,0x000000ca,0x000000c8,0x00000000, - 0x00050051,0x00000013,0x000000cb,0x000000c8,0x00000001,0x00070050,0x00000014,0x000000cc, - 0x000000ca,0x000000cb,0x000000c9,0x00000044,0x00040063,0x000000bf,0x000000c0,0x000000cc, - 0x000200f9,0x000000be,0x000200f8,0x000000be,0x000100fd,0x00010038 + 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/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index 7939adeb1..b4b692a82 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -7,9 +7,16 @@ // 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 none: only the -// camera's motion, what moves by itself ghosts). 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. +// 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 @@ -20,7 +27,10 @@ // 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. RX_VK_DLSS_PRESET=: DLSS's preset (default K). -// RX_VK_DLSS_DEBUG=motion: the motion vectors shown instead of the picture. RX_VK_DLSS_LOG=1: NGX's own log in ours. +// 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. @@ -33,11 +43,13 @@ #include #include #include +#include #include #include #include #include +#include "dlss_actor.h" #include "dlss_math.h" #include "vk_draw.h" #include "vk_menu.h" @@ -49,6 +61,8 @@ #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 @@ -129,6 +143,37 @@ struct State { 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) 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) @@ -171,13 +216,13 @@ void drop_image(Image& im) { im = {}; } -bool make_image(VkCommandBuffer cmd, Image& im, VkFormat format, uint32_t w, uint32_t h) { +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; + 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; @@ -416,6 +461,204 @@ VkImageView depth_view(Texture* t) { 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 feature for this output size, made when the size 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(); @@ -564,6 +807,19 @@ void dlss_idle() { logf("vk: NVIDIA DLSS: not used for 120 frames: its feature and images let go"); } 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(); @@ -576,6 +832,67 @@ void dlss_idle() { 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; @@ -597,6 +914,11 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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"); @@ -632,7 +954,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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) || + !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"); @@ -675,6 +997,28 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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{}; @@ -713,12 +1057,20 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk s.last_jitter[0] = cam.jitter[0], s.last_jitter[1] = cam.jitter[1]; static int evaluations; if (++evaluations % 600 == 0) // (now and then: the jitter DLSS was given, in render pixels) - logf("vk: NVIDIA DLSS: %d frames drawn; this one's jitter %+.3f %+.3f px%s", evaluations, ep.InJitterOffsetX, - ep.InJitterOffsetY, motion ? "" : ", no motion"); + logf("vk: NVIDIA DLSS: %d frames drawn; this one's jitter %+.3f %+.3f px%s; actor motion: %llu draws in %llu frames, " + "%llu without a pose before, %.3f ms of CPU a frame", evaluations, ep.InJitterOffsetX, ep.InJitterOffsetY, + motion ? "" : ", no motion", (unsigned long long)s.actor_draws, (unsigned long long)s.actor_frames, + (unsigned long long)s.actor_skipped, s.actor_ns / 1e6 / 600); + if (evaluations % 600 == 0) s.actor_ns = 0; + 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)" : ""); @@ -736,6 +1088,7 @@ void dlss_note_scene_draw(const Texture*, uint32_t, uint32_t, Texture*, uint64_t bool dlss_upscale(VkCommandBuffer, uint64_t, bool, Texture*, VkImageView, Texture*, VkImageView, uint32_t) { return false; } void dlss_idle() {} uint32_t dlss_jitter_size(int) { return 0; } +void dlss_note_skinned_draw(const DlssSkin&) {} #endif diff --git a/gpu/vk/vk_dlss.h b/gpu/vk/vk_dlss.h index e590c57b8..2f2d9d4f5 100644 --- a/gpu/vk/vk_dlss.h +++ b/gpu/vk/vk_dlss.h @@ -32,6 +32,24 @@ void dlss_device_extensions(std::vector& want, const std::vector(&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(PROF_DRAW); FINE_N(FC_DRAWS, 1); @@ -5003,6 +5036,7 @@ void draw(VkCommandBuffer cmd, NvnState& s, const DrawArgs& d) { 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 @@ -5856,7 +5890,11 @@ bool dlss_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { 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); - return dlss_upscale(cmd, p.key, gameplay_network(p.key), in, dw.imgs[0].imageView, out, dw.imgs[ntex].imageView, 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) { const char* keys = getenv("RX_VK_TAAU_KEYS") ? getenv("RX_VK_TAAU_KEYS") : "fd029a317a9a4b94"; 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() From 4ee204a3c345de75f261e19de094e96d6e227e33 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 09:00:29 -0500 Subject: [PATCH 44/52] DLSS jitter: in the views' uniform buffers only, not the game's cameras (steady shadow maps) The jitter went into sead's perspective projection itself (its frustum offset at +0xb4 and +0xb8), so the game's CPU work saw it as well as the GPU. The shadow cascades are fitted to the view's frustum: with the camera still and DLAA at 3840x2160, the cascade's projection offset moved by 0.17 shadow texels a frame on average (up to 0.32-0.44) with the jitter, against 0.0035 without it, so the shadow map was rasterised at a different sub-texel origin every frame. The jitter now goes on where the game fills each view's uniform buffers (main+0x6bc520): after the loads of the view's projection (its rows at +0x1a8), on P[0][2] and P[1][2] in the registers, so the buffers get the jittered projection and the view-projection made from it, and nothing in the game's memory changes. hos_dlss_jitter_clip takes the place of hos_dlss_jitter_cam (no per-camera bookkeeping is needed: nothing is written, so nothing has to be put back when DLSS stops); gpu/vk/dlss_math.h dlss_jitter_clip picks the projections (perspective, 16:9 or the window's ratio). Measured headless (Kokiri Forest spawn, still camera, DLAA 3840x2160): the cascade offset's step per frame 0.163-0.175 texels before, 0.0034-0.0036 after (0.0034-0.0037 with the jitter off), three sessions each; DLSS still gets the jitter (the scene camera's P[0][2] and P[1][2] in its uniform buffer, the log's per-frame jitter) and the picture's detail is the same as before. On screen at that spot the shadows' share of the frame-to-frame change is small: in one session switching between the two every 64-96 frames, neither was steadier in every segment; jitter on (either way) changes ~15% more pixels from frame to frame than jitter off. Frame rate unchanged (39.0-39.4 fps, jitter on or off). tests/test_oot_jitter.py builds the sites' C and checks only the named lanes move; tests/test_dlss_math.cpp checks which projections get the jitter. --- gpu/vk/dlss_math.h | 15 +++++ host/rx_backend.c | 62 ++++-------------- nx2/oot_jitter.py | 62 +++++++++--------- tests/test_dlss_math.cpp | 26 ++++++++ tests/test_dlss_math.py | 4 +- tests/test_oot_jitter.py | 137 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 79 deletions(-) create mode 100644 tests/test_oot_jitter.py diff --git a/gpu/vk/dlss_math.h b/gpu/vk/dlss_math.h index 970b4155a..24ea32095 100644 --- a/gpu/vk/dlss_math.h +++ b/gpu/vk/dlss_math.h @@ -25,6 +25,21 @@ static inline void dlss_jitter_pixels(unsigned frame, int phases, float out[2]) 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) */ diff --git a/host/rx_backend.c b/host/rx_backend.c index 15804c1af..88ff31b6b 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -293,62 +293,28 @@ 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 camera, in sead's - projection offset (a fraction of the view's width and height), into out; 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). The renderer reads the offset the camera ended up - with 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). */ -static int dlss_jitter(float out[2]) { +/* (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 camera) */ + 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); } - if (w < 1.0f || h < 1.0f) return 0; - float px[2]; - dlss_jitter_pixels((unsigned)frame, phases, px); - out[0] = px[0] / w, out[1] = px[1] / h; - return 1; -} -/* The cameras jittered, each with its own offset from before the jitter: the jitter goes on top of it, and once there is no - jitter (DLSS turned off) each camera gets it back the next time it passes the site, so none is left offset. A value that - is not the one written last was set by the game itself: it is the camera's own from then on. A camera not seen for long - (gone) gives its place to a new one */ -typedef struct { uint64_t cam; uint32_t own[2], wrote[2]; LONG seen; } jittered; -static jittered jittered_cams[32]; -static volatile LONG jitter_lock; -int hos_dlss_jitter_cam(uint64_t cam, const uint32_t now[2], uint32_t out[2]) { - float j[2]; - const int on = dlss_jitter(j); - while (InterlockedCompareExchange(&jitter_lock, 1, 0)) {} - jittered *e = NULL, *spare = NULL; - for (int i = 0; i < 32; i++) { - jittered *t = &jittered_cams[i]; - if (t->cam == cam && cam) e = t; - else if (!spare || !t->cam || (spare->cam && t->seen < spare->seen)) spare = t; - } - int write = 0; - if (on) { - if (!e && spare) e = spare, e->cam = cam, memcpy(e->own, now, 8); - else if (e && memcmp(e->wrote, now, 8)) memcpy(e->own, now, 8); - if (e) { - float own[2]; - memcpy(own, e->own, 8); - own[0] += j[0], own[1] += j[1]; - memcpy(out, own, 8), memcpy(e->wrote, out, 8); - e->seen = presents, write = 1; - } - } else if (e) { /* (no jitter now: the camera's own offset back, unless the game has set another since) */ - if (!memcmp(e->wrote, now, 8)) memcpy(out, e->own, 8), write = 1; - memset(e, 0, sizeof *e); - } - InterlockedExchange(&jitter_lock, 0); - return write; + 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) { diff --git a/nx2/oot_jitter.py b/nx2/oot_jitter.py index 26a4ca615..cace22c9b 100644 --- a/nx2/oot_jitter.py +++ b/nx2/oot_jitter.py @@ -1,18 +1,24 @@ """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 camera's projection a sub-pixel offset each -frame. sead's perspective projection keeps an offset of the frustum's centre at +0xb4 (x) and +0xb8 (y), in fractions of -its width and height, which its matrix update (and the game's three inlined copies of it) reads right after the field of -view and aspect ratio at +0xac. The sites are those loads (the same ones nx2/oot_wide.py's ratio patch follows): the -offset is written before it is read, for cameras of the picture's ratio. host/rx_backend.c hos_dlss_jitter picks it; it -is none (the game as shipped) unless DLSS runs, and the renderer reads the offset back from the camera's uniform buffer. -host/rx_backend.c hos_dlss_jitter_cam keeps each camera's own offset: the jitter goes on top of it, and it is put back once -DLSS stops, so no camera is left offset. +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. -Every perspective camera of the picture's ratio gets the jitter, not only the scene's: which one the scene's is, the -renderer learns from its draws, too late for the CPU's update. The others (a reflection, a portrait) are drawn without DLSS -and move by under a pixel of the scene's size, too little to see. +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. @@ -22,26 +28,24 @@ import oot_wide -# (main offset, the original instruction word there, the register the aspect ratio was loaded into, the projection's register) +# (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 = ( - (0x3485cc0, 0x2d558400, 1, 0), # sead's perspective projection's matrix update: ldp s0, s1, [x0, #0xac] - (0x4f1e8, 0x2d558ea0, 3, 21), # its inlined copies - (0x23b9a8, 0x2d558c00, 3, 0), - (0x33b82c, 0x2d558440, 1, 2), + # 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(vreg, base): - # (16:9, or the window's ratio once the widescreen patch before this one has put it in: the picture's cameras. The host - # puts the jitter on top of the camera's own offset, and that offset back once there is no jitter) - return ("{ extern int hos_dlss_jitter_cam(uint64_t cam, const uint32_t now[2], uint32_t out[2]); " - "extern volatile float hos_wide_ratio; float _r; memcpy(&_r, c->vreg[%d], 4); const float _w = hos_wide_ratio; " - "const float _d = _r - (16.0f / 9.0f), _e = _w > 0.0f ? _r - _w : 1.0f; " - "if ((_d <= 0.01f && _d >= -0.01f) || (_e <= 0.01f && _e >= -0.01f)) { " - "uint32_t _n[2] = {(uint32_t)recomp_load32(c, c->x[%d] + 0xb4), (uint32_t)recomp_load32(c, c->x[%d] + 0xb8)}, _u[2]; " - "if (hos_dlss_jitter_cam(c->x[%d], _n, _u)) { recomp_store32(c, c->x[%d] + 0xb4, _u[0]); " - "recomp_store32(c, c->x[%d] + 0xb8, _u[1]); } } }" - " /* (nx2/oot_jitter.py: DLSS camera jitter) */" % (vreg, base, base, base, base, base)) +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(): @@ -52,7 +56,7 @@ def wanted(): 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, vreg, base in SITES: + for off, word, xreg, yreg in SITES: pc = oot_wide.MAIN + off try: have = img.word(pc) @@ -61,7 +65,7 @@ def install(lifter, img): 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(vreg, base) + 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/tests/test_dlss_math.cpp b/tests/test_dlss_math.cpp index 5a011b2bf..432ae3f1c 100644 --- a/tests/test_dlss_math.cpp +++ b/tests/test_dlss_math.cpp @@ -144,5 +144,31 @@ int main() { 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 index a903de64e..0344f322e 100644 --- a/tests/test_dlss_math.py +++ b/tests/test_dlss_math.py @@ -1,6 +1,6 @@ """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, and the jitter in DLSS's pixels. 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. +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] """ 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) From 740304bac0b2ad41cb39a4b68295d9b249a4475b Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:12:06 -0500 Subject: [PATCH 45/52] Menu: offer a restart when a setting that takes one changes The five Graphics rows read once at the start (GPU device, Output resolution, Handheld profile, Ultrawide, Robust buffers) are marked as taking a restart (Row::restart), and the menu keeps the value each had at the start (gpu/vk/menu_restart.h). Their values no longer say "(restart)" whatever runs: "(after restart)" follows a value only while it is not the one the game runs with, and while any is, the Graphics tab says so at its foot ("Restart needed: Output resolution, Ultrawide", one line, the last ones as "and N more" when they do not fit). A row changed back is no longer pending. Closing the menu with something pending asks first, on a page of its own: "Restart to apply Output resolution and Ultrawide?", "Progress since your last save is lost.", [Later] (chosen at first) and [Restart now], by keys, the gamepad and the mouse (vk_menu_hit.h ask). Later closes the menu with the settings saved for the next start, as before, and is not asked again until one of those rows changes. Restart now saves the options and the pipeline cache and closes the window as the player would (WM_APP + 9: host/display.c, host/posix/display_sdl.c): rx_close_asked, the log, and at the very end of that close host/hos_relaunch.c starts the game again. Linux: the process becomes the game again (execve of /proc/self/exe, argv[0] on the PATH if that is gone) with the arguments and the environment it started with, from /proc/self/cmdline and /proc/self/environ, so a launcher or a stream waiting for it keeps it. The game is never run from inside the AppImage (hookshot-launch takes its variables away), so /proc/self/exe is the game. Windows: CreateProcessW of GetModuleFileNameW with GetCommandLineW, then the exit. tests/test_menu_restart.py checks the pending rules (a change, a change back, Later and a change after it), the names, the relaunch command (made-up files and its own /proc/self ones), and the path from the menu to the window's close; test_menu_hit.cpp the question's buttons. The screensaver fixture's window procedure closes through hos_window_closed_exit. --- gpu/vk/menu_restart.h | 70 +++++++++++++ gpu/vk/vk_menu.cpp | 204 +++++++++++++++++++++++++++++++----- gpu/vk/vk_menu_hit.h | 16 +++ host/display.c | 10 +- host/hos_relaunch.c | 89 ++++++++++++++++ host/hos_relaunch.h | 70 +++++++++++++ host/posix/display_sdl.c | 15 ++- readme.md | 5 + tests/run_all.py | 1 + tests/screensaver_test.c | 5 +- tests/test_menu_hit.cpp | 18 ++++ tests/test_menu_restart.cpp | 73 +++++++++++++ tests/test_menu_restart.py | 144 +++++++++++++++++++++++++ 13 files changed, 685 insertions(+), 35 deletions(-) create mode 100644 gpu/vk/menu_restart.h create mode 100644 host/hos_relaunch.c create mode 100644 host/hos_relaunch.h create mode 100644 tests/test_menu_restart.cpp create mode 100644 tests/test_menu_restart.py 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/vk_menu.cpp b/gpu/vk/vk_menu.cpp index e876b3bf1..8c139c0d8 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 @@ -21,12 +25,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 @@ -55,6 +61,7 @@ struct Row { // (a help that depends on the other rows or on the GPU: help_at(-1) is the one to show now, help_at(0, 1, ...) every // one it can be, until nullptr, so the menu keeps the height of the tallest: measure_help) const char* (*help_at)(int variant) = nullptr; + bool restart = false; // (read once, at the start: a change applies after a restart) }; const char* help_of(const Row& r, int variant) { return r.help_at ? r.help_at(variant) : variant <= 0 ? r.help : nullptr; } const char* tx_framegen(int v) { @@ -179,7 +186,7 @@ bool row_fixed(const int* value) { (value == &opts.resolution && ur::resolution_fixed(upscaler())) || (value == &opts.upscale && !filter_needed()); } const char* tx_output_res(int v) { - static const char* t[] = {"2048x1152 (game, restart)", "2560x1440 (restart)", "3840x2160 (restart)"}; + static const char* t[] = {"2048x1152 (game's)", "2560x1440", "3840x2160"}; return t[v]; } const char* tx_speed(int v) { return v ? "Real time" : "Per frame"; } @@ -188,23 +195,19 @@ 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); 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) { @@ -259,7 +262,7 @@ Row rows[] = { {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."}, {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."}, + "Selects which graphics card Vulkan uses. Auto picks the best dedicated GPU. Applies after restart.", nullptr, 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."}, @@ -269,7 +272,7 @@ Row rows[] = { // (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, 2, 1, tx_output_res, "The size of the game's whole frame, HUD included, before it is scaled to your window: what the Upscaler fills from the " - "3D. Docked only. Applies after a restart."}, + "3D. Docked only. Applies after a restart.", nullptr, true}, {TAB_GRAPHICS, "Upscaler", &opts.dlss, 0, 2, 1, tx_upscaler, nullptr, upscaler_help}, {TAB_GRAPHICS, "Render resolution", &opts.resolution, 0, ur::kValues - 1, 1, tx_resolution, nullptr, resolution_help}, {TAB_GRAPHICS, "Window scaling filter", &opts.upscale, 0, 2, 1, tx_filter, @@ -279,13 +282,15 @@ Row rows[] = { {TAB_GRAPHICS, "Sharpness", &opts.sharpness, 0, 100, 10, tx_percent, "Sharpens the picture as it goes to the window, with any filter. DLSS looks softer than the game's own upscaler, so it " "often wants more. 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."}, + {TAB_GRAPHICS, "Handheld profile", &opts.handheld, 0, 1, 1, tx_onoff, + "Runs the game as if the console were undocked: lower resolutions, lighter on the GPU. Applies after a restart.", nullptr, + 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."}, + "Monitor's shape widens the camera to fill a screen wider than 16:9. The HUD is still stretched. Applies after a restart.", + nullptr, 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."}, + "Off can be faster on some NVIDIA cards. Applies after restart.", nullptr, true}, // Gameplay (3 rows) {TAB_GAMEPLAY, "FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, @@ -411,6 +416,59 @@ 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())); @@ -567,6 +625,11 @@ void set_row(Row& r, int v, int dir, bool keep = true) { sync_preset_budgets(0); } } + { // (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(); } @@ -655,7 +718,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) @@ -786,7 +849,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) @@ -808,6 +872,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, @@ -1007,7 +1103,8 @@ 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) + 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{}; @@ -1059,6 +1156,12 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { 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); @@ -1112,7 +1215,24 @@ int measure_help(int s) { 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; @@ -1126,10 +1246,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}; @@ -1246,6 +1366,7 @@ 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 = std::min(std::max(atoi(e), 0), 2); + 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; } @@ -1261,7 +1382,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); } @@ -1464,7 +1587,18 @@ 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 @@ -1472,7 +1606,26 @@ void menu_tick() { 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() && 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) @@ -1725,7 +1878,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_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/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_relaunch.c b/host/hos_relaunch.c new file mode 100644 index 000000000..e3e0b8cee --- /dev/null +++ b/host/hos_relaunch.c @@ -0,0 +1,89 @@ +/* (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; + 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/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/readme.md b/readme.md index c570adcdd..b69ee3695 100644 --- a/readme.md +++ b/readme.md @@ -186,6 +186,11 @@ a controller. Up and Down choose a row, Left an (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. +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 | |---|---|---|---| | Graphics | Quality preset | Custom, Minimum (4GB), Low (6GB), Balanced (8GB), High (12GB), Ultra (16GB+) | Custom | diff --git a/tests/run_all.py b/tests/run_all.py index b2032de1e..807d9350c 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -40,6 +40,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_nvn_buffer_map.py": ["--cc", "gcc"], "tests/test_startup_progress.py": ["--cxx", "g++"], "tests/test_audio_voice_biquad.py": ["--cc", "gcc"], 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_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() From 4eeca972b7ffb65b4626f28c0721c9d263ee53cd Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 10:13:17 -0500 Subject: [PATCH 46/52] Menu: help as what a row is, and a line for the value chosen Each row's help was one long text describing every choice. A row now has a short help that stays the same whatever its value (what the row is), and a hint for the value chosen (Row::hint), drawn on a line of its own under it, dimmer and set off by a green bar: Window scaling filter says "How the finished picture is scaled to the window, when its size differs from the Output resolution.", then "The plain copy.", "Sharper." or "Sharper still; can ring on edges." Every row of every tab is rewritten so; what the old texts said moves to the value it is about. The help that depended on the GPU or the other rows is a hint now: hint(v, -1) is the one to show, hint(v, 0), hint(v, 1)... every one a value can have. The Upscaler's says why DLSS and DLAA are not offered (not in this build, RX_VK_DLSS=0, no RTX GPU, an old driver, no runtime), Reflex's that it is not available, the Render resolution's follows the Upscaler shown, the filter's says when nothing is to be scaled. measure_help keeps the room of the tallest help and hint together, so the panel does not move as a value changes. NVIDIA DLSS and DLAA, DLSS's Render resolution modes and Sharpness while DLSS or DLAA runs say to set Sharpness to around 80%: both look softer than the game's own upscaler. The readme's menu section says so too, and how the help reads. test_upscaler_rows.py checks the Upscaler's reasons, the 80%, and that measure_help counts the values' lines. --- gpu/vk/vk_menu.cpp | 244 ++++++++++++++++++++++++------------ readme.md | 12 +- tests/test_upscaler_rows.py | 23 +++- 3 files changed, 194 insertions(+), 85 deletions(-) diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 8c139c0d8..82250b29c 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -57,13 +57,22 @@ 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) - // (a help that depends on the other rows or on the GPU: help_at(-1) is the one to show now, help_at(0, 1, ...) every - // one it can be, until nullptr, so the menu keeps the height of the tallest: measure_help) - const char* (*help_at)(int variant) = nullptr; + 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) }; -const char* help_of(const Row& r, int variant) { return r.help_at ? r.help_at(variant) : variant <= 0 ? r.help : nullptr; } +/// 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]; @@ -93,15 +102,13 @@ const char* tx_reflex(int v) { static const char* t[] = {"Off", "On", "On + Boost"}; return t[reflex_available() ? std::clamp(v, 0, 2) : 0]; } -const char* reflex_help(int variant) { - static const char* const base = "NVIDIA GPUs: the driver starts each frame as late as it can, so less time passes between " - "your input and the screen. Boost also keeps the GPU's clocks up. Not used with frame " - "generation."; - static const char* const none = "NVIDIA GPUs: the driver starts each frame as late as it can, so less time passes between " - "your input and the screen. Not available here: it needs an NVIDIA GPU whose driver has " - "VK_NV_low_latency2."; - if (variant > 1) return nullptr; - return (variant < 0 ? reflex_available() : variant == 0) ? base : none; +/// 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 @@ -111,11 +118,15 @@ const char* tx_upscaler(int) { static const char* t[] = {"Game's own", "NVIDIA DLSS", "NVIDIA DLAA"}; return t[std::clamp(upscaler(), 0, 2)]; } -const char* upscaler_help(int variant) { - static const char* const base = "What scales the 3D up to the output. Game's own: the game's own network. NVIDIA DLSS: " - "NVIDIA's, from a smaller 3D; it looks softer than the game's own, Sharpness helps. NVIDIA " - "DLAA: the 3D at full size, only its edges smoothed; the slowest. With either, things that " - "move by themselves can leave trails for now."; +/// 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).", @@ -124,11 +135,16 @@ const char* upscaler_help(int variant) { "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).", }; - const int w = variant < 0 ? (int)dlss_why() : variant; + // (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 (!why[w]) return base; - static char b[DLSS_WHY_COUNT][512]; - snprintf(b[w], sizeof b[w], "What scales the 3D up to the output: the game's own network. %s", why[w]); + 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]; } const char* tx_percent(int v) { @@ -168,17 +184,27 @@ const char* tx_resolution(int v) { snprintf(b, sizeof b, "%d%% (%ux%u)", ur::percent(v), w, h); return b; } -const char* resolution_help(int variant) { - static const char* const t[] = { - "The share of the output the 3D is drawn at; the upscaler fills in the rest. Auto lets the game lower it when the GPU " - "is busy. A fixed percentage stays steady: lower is faster, and softer.", - "DLSS's modes: the share of the output the 3D is drawn at before DLSS fills in the rest. Quality is the sharpest, " - "Ultra Performance the fastest. Auto lets the game lower it when the GPU is busy, and DLSS follows.", - "DLAA draws the 3D at the output's full size, so there is nothing to set here. Choose NVIDIA DLSS or the game's own " - "upscaler to draw it smaller.", - }; - const int i = variant < 0 ? std::clamp(upscaler(), 0, 2) : variant; - return i < 3 ? t[i] : nullptr; +/// 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) { @@ -254,72 +280,117 @@ void apply_preset(int p) { 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 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_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 (the Upscaler only in a build with DLSS: row_shown) {TAB_GRAPHICS, "Quality preset", &opts.preset, 0, 5, 1, tx_preset, - "Sets the scaling rows, frame generation and video memory limits for a GPU with that much memory. The Upscaler stays " - "as you chose it; with DLSS, the preset's resolution becomes DLSS's nearest mode."}, + "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."}, {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.", nullptr, true}, + "Which graphics card the game runs on. Applies after a restart.", hn_gpu, 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, "NVIDIA Reflex", &opts.reflex, 0, 2, 1, tx_reflex, nullptr, reflex_help}, + "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, 2, 1, tx_output_res, - "The size of the game's whole frame, HUD included, before it is scaled to your window: what the Upscaler fills from the " - "3D. Docked only. Applies after a restart.", nullptr, true}, - {TAB_GRAPHICS, "Upscaler", &opts.dlss, 0, 2, 1, tx_upscaler, nullptr, upscaler_help}, - {TAB_GRAPHICS, "Render resolution", &opts.resolution, 0, ur::kValues - 1, 1, tx_resolution, nullptr, resolution_help}, + "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, "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 your window. It matters only when the window's size differs from the Output " - "resolution: for the sharpest picture, set the Output resolution to your screen's and no filter is needed. Off is the " - "plain copy, Catmull-Rom sharper, Lanczos sharper still."}, + "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 any filter. DLSS looks softer than the game's own upscaler, so it " - "often wants more. Too much shows halos around edges."}, + "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: lower resolutions, lighter on the GPU. Applies after a restart.", nullptr, - true}, + "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.", - nullptr, true}, + "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.", nullptr, true}, + "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."}, - {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}, + {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}, // Controls (8 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."}, + "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}, {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."}, - {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."}, + "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."}, + {TAB_CONTROLS, "Invert mouse vertical", &opts.mouse_invert, 0, 1, 1, tx_onoff, "Swaps up and down for the mouse.", + hn_mouse_invert}, {TAB_CONTROLS, "Invert camera X", &opts.invert_cam_x, 0, 1, 1, tx_onoff, "Inverts horizontal look (left/right) on the right analog stick."}, {TAB_CONTROLS, "Invert camera Y", &opts.invert_cam_y, 0, 1, 1, tx_onoff, "Inverts vertical look (up/down) on the right analog stick."}, {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, @@ -813,6 +884,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) { @@ -1150,8 +1222,16 @@ 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, help_of(rows[tab_row_to_global(cur_tab, tab_selected[cur_tab])], -1), (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) @@ -1202,14 +1282,20 @@ 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) - for (int v = 0; const char* t = help_of(r, v); v++) most = std::max(most, wrap_help(dc, t, (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); diff --git a/readme.md b/readme.md index b69ee3695..996454db7 100644 --- a/readme.md +++ b/readme.md @@ -183,8 +183,9 @@ 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 @@ -230,9 +231,10 @@ choices and starts the game again, so progress since your last save is lost. - **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. - **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 (softer than the game's own: - Sharpness helps) or DLAA (the 3D at full size, edges smoothed). **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. + 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. **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. At 3840x2160 the game's render targets get a bigger GPU pool, so a full-size 3D fits. `HOS_OUTPUT_RES=0/1/2` sets it for one run. - **Window scaling filter** matters only when your window's size differs from the Output resolution: for the sharpest diff --git a/tests/test_upscaler_rows.py b/tests/test_upscaler_rows.py index 2c6970379..bda30b4d4 100644 --- a/tests/test_upscaler_rows.py +++ b/tests/test_upscaler_rows.py @@ -1,5 +1,6 @@ """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, and options files from before still loading. No game or GPU required. +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++] """ @@ -53,6 +54,26 @@ def main(): 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"), ('"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)) dlss = (ROOT / "gpu" / "vk" / "vk_dlss.cpp").read_text(encoding="utf-8") 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)") From d378bd0e8a41670962f1b4c5119588908916da0f Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:20:55 -0500 Subject: [PATCH 47/52] DLSS: the Builder keeps its NVIDIA DLSS tick at once; NGX's log lines in UTF-8 on Windows From the Windows tests on 842121f: the Builder saved the tick only when Build was pressed, so ticking it, accepting NVIDIA's license and closing the Builder forgot it. DlssChanged now saves the settings either way. NGX writes its paths in the ANSI code page, so in hookshot_log.txt its 'ngx:' lines showed a folder's non-ASCII letters as replacement characters while ours (UTF-8) showed them right. ngx_log converts a line that isn't valid UTF-8 from the ANSI code page. The readme says NGX starts on demand (the Insert menu's first opening, or the first upscale with DLSS), so a log from the title screen has no NGX lines yet. --- builder/HookshotBuilder/MainWindow.xaml.cs | 5 +++-- gpu/vk/vk_dlss.cpp | 14 ++++++++++++++ readme.md | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/builder/HookshotBuilder/MainWindow.xaml.cs b/builder/HookshotBuilder/MainWindow.xaml.cs index ad925477a..3bb36b41f 100644 --- a/builder/HookshotBuilder/MainWindow.xaml.cs +++ b/builder/HookshotBuilder/MainWindow.xaml.cs @@ -706,9 +706,10 @@ async void StartBuild(object sender, RoutedEventArgs e) /// 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) return; - if (MessageBox.Show(this, DlssLicense, "NVIDIA DLSS", MessageBoxButton.YesNo, MessageBoxImage.Information) != MessageBoxResult.Yes) + 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 = diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index b4b692a82..c46c0c225 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -252,6 +252,20 @@ NVSDK_NGX_Resource_VK resource(const Image& im, bool 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(); +#ifdef _WIN32 + // (NGX writes its paths in the ANSI code page: as UTF-8, like the rest of our log, unless it already is) + if (!m.empty() && MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, m.data(), (int)m.size(), nullptr, 0) == 0) { + const int nw = MultiByteToWideChar(CP_ACP, 0, m.data(), (int)m.size(), nullptr, 0); + std::wstring w(nw > 0 ? nw : 0, L'\0'); + if (nw > 0) MultiByteToWideChar(CP_ACP, 0, m.data(), (int)m.size(), w.data(), nw); + const int n = WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), nullptr, 0, nullptr, nullptr); + if (n > 0) { + std::string u(n, '\0'); + WideCharToMultiByte(CP_UTF8, 0, w.data(), (int)w.size(), u.data(), n, nullptr, nullptr); + m.swap(u); + } + } +#endif logf("ngx: %s", m.c_str()); } diff --git a/readme.md b/readme.md index 996454db7..9746b4d1c 100644 --- a/readme.md +++ b/readme.md @@ -431,6 +431,8 @@ license. Never share a game folder built with them, and do not post builds made core comes with NVIDIA's Linux driver. 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 From de67a562cb5629f8776e8662a74ce5d81590f6a8 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:05:38 -0500 Subject: [PATCH 48/52] Restart now on Windows keeps the run before's log From the Windows retest of d378bd0: after Restart now, hookshot_log.1.txt and .2.txt were missing and the run before was overwritten. The new process started while the old one still had hookshot_log.txt open; Windows does not rename an open file, so its move to hookshot_log.1.txt failed and the new run's freopen truncated it. The old process now passes its id (HOS_RESTART_OF) and the new one waits for it to end, up to 10 s, before it moves the logs and opens its own. --- host/hos_main.c | 12 ++++++++++++ host/hos_relaunch.c | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/host/hos_main.c b/host/hos_main.c index d9e7bca36..13ec85e1f 100644 --- a/host/hos_main.c +++ b/host/hos_main.c @@ -1190,6 +1190,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 index e3e0b8cee..65ae060bf 100644 --- a/host/hos_relaunch.c +++ b/host/hos_relaunch.c @@ -35,6 +35,12 @@ static void relaunch(void) { 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); From 739035769a868d1d07d24b8fb8735daf50dcc94f Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:23:15 -0500 Subject: [PATCH 49/52] DLSS model row: Transformer (K), Transformer 2 (M and L), NVIDIA default A Graphics row under the Upscaler, in builds with DLSS, chooses the network DLSS and DLAA draw with, by NVIDIA's preset: K (the transformer, the default and the one tested most here), M (the second-generation transformer, NVIDIA's pick for Performance), L (the same generation as tuned for Ultra Performance) or NVIDIA's default (preset 0: the runtime picks per mode, which can change with a runtime update). The SDK's runtime, 310.9.16, takes M ("Using App hint Preset M" in NGX's log for every mode). - It has nothing to choose while the game's own upscaler runs, or while RX_VK_DLSS_PRESET sets the preset for the run (that switch still wins, now also as 0 for NVIDIA's default, and in either case). - A change makes DLSS's feature again with the new preset, as a new size does: no restart. Saved as dlss_model; the Quality preset leaves it alone. - Its help: one line for the row, one for each value (the game's own and RX_VK_DLSS_PRESET have their own). - The log's feature line says the preset asked for, or "0 (NVIDIA's default)". - upscaler_rows.h: the presets, RX_VK_DLSS_PRESET over them, and when the row is shown and fixed, checked by tests/test_upscaler_rows.cpp; tests/test_upscaler_rows.py checks the menu and vk_dlss.cpp use them. - readme: the row, its values and RX_VK_DLSS_PRESET. --- gpu/vk/upscaler_rows.h | 33 ++++++++++++++++++++++++++++- gpu/vk/vk_dlss.cpp | 27 ++++++++++++++++-------- gpu/vk/vk_menu.cpp | 40 ++++++++++++++++++++++++++++++++++-- gpu/vk/vk_menu.h | 2 ++ readme.md | 13 +++++++++++- tests/test_upscaler_rows.cpp | 14 +++++++++++++ tests/test_upscaler_rows.py | 18 ++++++++++++---- 7 files changed, 131 insertions(+), 16 deletions(-) diff --git a/gpu/vk/upscaler_rows.h b/gpu/vk/upscaler_rows.h index f0ea1adef..225d6c44a 100644 --- a/gpu/vk/upscaler_rows.h +++ b/gpu/vk/upscaler_rows.h @@ -74,13 +74,44 @@ inline int step_upscaler(int v, int dir, bool dlss_ok) { 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) +/// 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) { diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index b4b692a82..fea43bc15 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -26,7 +26,8 @@ // 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. RX_VK_DLSS_PRESET=: DLSS's preset (default K). +// 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. @@ -51,6 +52,7 @@ #include "dlss_actor.h" #include "dlss_math.h" +#include "upscaler_rows.h" #include "vk_draw.h" #include "vk_menu.h" #include "vk_paths.h" @@ -127,6 +129,7 @@ struct State { 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) 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; @@ -659,11 +662,18 @@ int actor_motion(VkCommandBuffer cmd, const Camera& cam, const float* before, ui return (int)draws.size(); } -/// DLSS's feature for this output size, made when the size changes (cmd: its creation's work) +/// 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) + rw >= s.min_rw && rh >= s.min_rh && s.feature_preset == preset) return true; if (s.feature) { NVSDK_NGX_Handle* old = s.feature; @@ -694,9 +704,9 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui } } if (!found) return fail("no DLSS mode takes this render size"); - // (preset K, NVIDIA's transformer model, for every mode: RX_VK_DLSS_PRESET= another) - unsigned preset = NVSDK_NGX_DLSS_Hint_Render_Preset_K; - if (const char* p = getenv("RX_VK_DLSS_PRESET"); p && *p >= 'A' && *p <= 'Z') preset = (unsigned)(*p - 'A' + 1); + // (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, @@ -716,8 +726,9 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui 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; - logf("vk: NVIDIA DLSS: %ux%u (up to %ux%u) -> %ux%u, mode %d, preset %c", rw, rh, max_w, max_h, ow, oh, (int)mode, - (char)('A' + preset - 1)); + s.feature_preset = preset; + logf("vk: NVIDIA DLSS: %ux%u (up to %ux%u) -> %ux%u, mode %d, preset %c%s", rw, rh, 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; } diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index bdbb46c3a..e10a1939d 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -149,6 +149,38 @@ const char* hn_upscaler(int v, int variant) { 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); @@ -221,7 +253,8 @@ const char* hn_resolution(int v, int variant) { /// 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.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[32]; @@ -413,6 +446,8 @@ Row rows[] = { 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, @@ -607,7 +642,8 @@ std::string restart_line(HDC dc, int width) { /// 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())); + 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; diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index 9b06756ad..a467f88e1 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -16,6 +16,8 @@ struct Options { 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) diff --git a/readme.md b/readme.md index e155b5f35..8df2df98a 100644 --- a/readme.md +++ b/readme.md @@ -205,6 +205,7 @@ choices and starts the game again, so progress since your last save is lost. | | NVIDIA Reflex | Off, On, On + Boost (NVIDIA GPUs with `VK_NV_low_latency2`) | Off | | | Output resolution | 1920x1080, 2048x1152, 2560x1440, 3840x2160 (after a restart) | 2048x1152 | | | 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% | @@ -237,7 +238,8 @@ choices and starts the game again, so progress since your last save is lost. show artifacts. **Frame pacing** shows frames at an even interval: less stutter, up to one frame of delay. - **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. **Render + 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, @@ -444,6 +446,15 @@ license. Never share a game folder built with them, and do not post builds made `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. + 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. diff --git a/tests/test_upscaler_rows.cpp b/tests/test_upscaler_rows.cpp index 9303f5339..a12a55328 100644 --- a/tests/test_upscaler_rows.cpp +++ b/tests/test_upscaler_rows.cpp @@ -60,5 +60,19 @@ int main() { 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 index bda30b4d4..ab546ca68 100644 --- a/tests/test_upscaler_rows.py +++ b/tests/test_upscaler_rows.py @@ -29,7 +29,8 @@ def main(): 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', '"Render resolution", &opts.resolution', + '"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: @@ -45,8 +46,8 @@ def main(): 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()"), - "row_shown": ("ur::upscaler_shown(dlss_built())",), + "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(): @@ -69,12 +70,21 @@ def main(): 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"), ('"Render resolution", &opts.resolution', "hn_resolution"), + 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") 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: From d4dc814428f394066a6d34b78e9e4818e456d9a8 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:35:13 -0500 Subject: [PATCH 50/52] A status line every 600 frames: video memory, and DLSS's details while it draws Every 600 of the game's frames the log has one line, with or without DLSS: "vk: status: 600 frames; VRAM 7.1 of 8.0 GB" (the video memory in use of what the driver allows the game, asked of it then through VK_EXT_memory_budget; without that extension it says so once, and the line gives the game's own allocations). While DLSS or DLAA draws, its details go on the same line instead of a line of their own: the jitter it was last given, the actor motion's draws, frames and CPU time since the last line, the size of its images and buffers, and its feature's when NGX says it (NGX_DLSS_GET_STATS). DLSS's own 600-frame line is gone. Every 60 frames the use is compared with the allowance: the first time it is over, "vk: over the driver's video-memory allowance (8.3 of 8.0 GB)", with ": DLAA may stutter here; Quality or Balanced uses less" while DLAA is chosen (DLSS: a lower Render resolution mode). Again only after it was back under. The work a frame is a counter. - gpu/vk/status_line.h: the line's text, the warning's once and again, its advice; tests/test_status_line.cpp checks them, and tests/test_status_line.py that the renderer uses them. - vk_vma.cpp: vram_usage_now (VMA asks the driver again for a new frame index, shared with system_heap_usage). - readme: the status line where the log is described, and an 8 GB note in the DLSS section. --- gpu/vk/status_line.h | 43 ++++++++++++++++++++++++++++++++++++++ gpu/vk/vk_dlss.cpp | 42 ++++++++++++++++++++++++++++++------- gpu/vk/vk_dlss.h | 4 ++++ gpu/vk/vk_internal.h | 3 +++ gpu/vk/vk_replay.cpp | 30 ++++++++++++++++++++++++++ gpu/vk/vk_vma.cpp | 11 ++++++++-- readme.md | 8 ++++++- tests/test_status_line.cpp | 39 ++++++++++++++++++++++++++++++++++ tests/test_status_line.py | 41 ++++++++++++++++++++++++++++++++++++ 9 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 gpu/vk/status_line.h create mode 100644 tests/test_status_line.cpp create mode 100644 tests/test_status_line.py 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/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index fea43bc15..b17b47b97 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -176,7 +176,10 @@ struct State { } 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) + 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) + 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) @@ -804,6 +807,33 @@ uint32_t dlss_jitter_size(int axis) { 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); + snprintf(b, n, "; %s: 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", 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; + return mode; +} + void dlss_idle() { State& s = st(); if (!s.holding.load(std::memory_order_relaxed)) return; @@ -1066,13 +1096,8 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk } s.last_frame = now; s.last_jitter[0] = cam.jitter[0], s.last_jitter[1] = cam.jitter[1]; - static int evaluations; - if (++evaluations % 600 == 0) // (now and then: the jitter DLSS was given, in render pixels) - logf("vk: NVIDIA DLSS: %d frames drawn; this one's jitter %+.3f %+.3f px%s; actor motion: %llu draws in %llu frames, " - "%llu without a pose before, %.3f ms of CPU a frame", evaluations, ep.InJitterOffsetX, ep.InJitterOffsetY, - motion ? "" : ", no motion", (unsigned long long)s.actor_draws, (unsigned long long)s.actor_frames, - (unsigned long long)s.actor_skipped, s.actor_ns / 1e6 / 600); - if (evaluations % 600 == 0) s.actor_ns = 0; + // (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; 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); @@ -1098,6 +1123,7 @@ void dlss_device_extensions(std::vector&, const std::vector #include "vk_dlss.h" +#include "status_line.h" #include "vk_reflex.h" #include "vk_draw.h" #include "vk_dump.h" @@ -2537,11 +2538,40 @@ 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; + 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; %s%s", sl::kEvery, v, dlss); +} + 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) 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/readme.md b/readme.md index 8df2df98a..6d876fbdd 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)) | @@ -455,6 +455,12 @@ the SDK comes with (310.9.16) has all of them. A change applies at once, without 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. 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() From d4db1d4499d811b5e921daca484a3a9f355dd85a Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:35:38 -0500 Subject: [PATCH 51/52] Window scaling filter: the start-up screen's frames are not the game's picture With the menu opened during the start-up screen, the Window scaling filter read "Not needed (window = output)" in a 1280x720 window with a 2048x1152 output: the start-up screen is presented at the window's own size, and present_image took every present's size as the picture's. Now only the game's frames (and the made ones) note the sizes; until the first one they are not known, and the row counts as needed (upscaler_rows.h filter_needed, already so). Checked by tests/test_upscaler_rows.py. --- gpu/vk/vk_replay.cpp | 4 +++- tests/test_upscaler_rows.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 9b96b17b8..f7b9451a0 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -3054,7 +3054,9 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI std::lock_guard qk(dev.present_mu); create_swapchain(dev.swap_extent.width, dev.swap_extent.height); } - { // (the Window scaling filter's row: whether the window is the picture's size) + 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); diff --git a/tests/test_upscaler_rows.py b/tests/test_upscaler_rows.py index ab546ca68..78ceff627 100644 --- a/tests/test_upscaler_rows.py +++ b/tests/test_upscaler_rows.py @@ -79,6 +79,13 @@ def main(): 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: From 752f83298be09cf057f38dc264418ef1a7afe041 Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:46:02 -0500 Subject: [PATCH 52/52] DLSS: say why each feature is made, and what a stall in play was A tester saw DLSS's minimum frame rate fall far below the game's own upscaler's at a 3840x2160 output, and once a 1.5 s stall. Making DLSS's feature again in play would do that, so the log now says each time why: - "vk: NVIDIA DLSS: feature made (; frame , of CPU): ...". The reason is one of: the first; again after it was let go; the output's size changed; another preset from the DLSS model row; or the render size left the feature's range (the range is now in the line). - The 120-frame release names the last frame DLSS was asked for and the frame it let go at. - The status line adds the slowest frame since the last line (present to present). While DLSS draws it also adds the frames it drew, the frames a network of the upscale's shape and formats ran without it (its input not this frame's scene, which used to pass without a word), and how many features have been made. --- gpu/vk/vk_dlss.cpp | 44 ++++++++++++++++++++++++++++++++++++-------- gpu/vk/vk_replay.cpp | 9 ++++++++- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/gpu/vk/vk_dlss.cpp b/gpu/vk/vk_dlss.cpp index b17b47b97..c8bacace7 100644 --- a/gpu/vk/vk_dlss.cpp +++ b/gpu/vk/vk_dlss.cpp @@ -130,6 +130,7 @@ struct State { 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; @@ -178,6 +179,8 @@ struct State { 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; }; @@ -678,6 +681,18 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui 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); }); @@ -720,7 +735,9 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui 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); @@ -730,8 +747,11 @@ bool make_feature(VkCommandBuffer cmd, uint32_t rw, uint32_t rh, uint32_t ow, ui } 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; - logf("vk: NVIDIA DLSS: %ux%u (up to %ux%u) -> %ux%u, mode %d, preset %c%s", rw, rh, max_w, max_h, ow, oh, (int)mode, - upscaler_rows::preset_letter(preset), preset ? "" : " (NVIDIA's default)"); + 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; } @@ -826,11 +846,15 @@ int dlss_status(char* b, size_t n) { 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); - snprintf(b, n, "; %s: 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", s.status_jitter[0], s.status_jitter[1], s.status_motion ? "" : " (no motion)", + 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_draws = s.actor_draws, s.status_frames = s.actor_frames, s.actor_ns = 0, s.status_drawn = s.status_passed = 0; return mode; } @@ -845,7 +869,8 @@ void dlss_idle() { 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: its feature and images let go"); + 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) @@ -943,10 +968,12 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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 || !c.valid || texture_frame_no() - c.frame > 1 || in->width != c.width || in->height != c.height || - in->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || + 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, @@ -1098,6 +1125,7 @@ bool dlss_upscale(VkCommandBuffer cmd, uint64_t key, bool known, Texture* in, Vk 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); diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index f7b9451a0..d1b297053 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -2543,6 +2543,12 @@ void log_frame_stats() { 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); @@ -2563,7 +2569,8 @@ void status_tick() { if (!line) return; char v[128]; sl::vram_text(v, sizeof v, used, allowed, known); - logf("vk: status: %d frames; %s%s", sl::kEvery, v, dlss); + 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)