From 6525c11b3c3917e26e87549e7d8ed36ae545b4b3 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 15 Sep 2026 20:06:18 +0300 Subject: [PATCH] feat(render3d): HDR10 output, and DLSS that stays still HDR output: an HDR10 swapchain (A2B10G10R10, ST 2084 over BT.2020) when the setting asks and the display offers it, with HDR metadata. The tonemap's HDR10 variant keeps the SDR picture up to a 200-nit paper white and rolls highlights on to 1000 nits; the overlay's converts the interface to the same white. The screen and LDR images go 10-bit with it; screenshots refuse while it is on. OpenGL and the Vulkan SDR frame are unchanged. The instance asks for VK_EXT_swapchain_colorspace. HDR metadata only where the loader has vkSetHdrMetadataEXT: Streamline's interposer does not, and calling the thunk crashed the game the moment the swapchain came up HDR10. PC 4K monitor: HDR10, validation 0. R3D_HDR overrides the setting. DLSS: - the vertical jitter offset flips with Streamline's image (rows from the top): unflipped, Quality resolved the ground into concentric rings; - preset K in every mode: the default M put Performance at 18 ms a frame at 4K on an RTX 3070 Ti (33 fps against 41 with DLSS off; with K, 60); - the camera is jittered only while this frame holds a token and the last evaluate worked. R3D_DLSS_PRESET, R3D_CAM_LOG (the camera and DLSS state a frame) and R3D_NOGRAIN for measuring. Co-Authored-By: Claude Opus 5 --- packages/ludic.render3d/gpu.ludic | 5 +- packages/ludic.render3d/gpu_vk.ludic | 10 +- packages/ludic.render3d/gpu_vk_draw.ludic | 57 ++++++++++- packages/ludic.render3d/gpu_vk_res.ludic | 1 + packages/ludic.render3d/post.ludic | 55 ++++++---- packages/ludic.render3d/render.ludic | 7 ++ packages/ludic.render3d/shaders/overlay.frag | 15 +++ .../shaders/spv/725523d9.frag.spv | Bin 8276 -> 8388 bytes .../shaders/spv/a4683767.frag.spv | Bin 0 -> 9940 bytes .../shaders/spv/a4683767.vert.spv | Bin 0 -> 1612 bytes .../shaders/spv/e86f78e8.frag.spv | Bin 0 -> 6060 bytes .../shaders/spv/e86f78e8.vert.spv | Bin 0 -> 2028 bytes .../ludic.render3d/shaders/spv/manifest.txt | 94 ++++++++++++++++++ packages/ludic.render3d/shaders/tonemap.frag | 26 ++++- packages/ludic.render3d/shaders/variants.list | 2 + packages/ludic.render3d/streamline.ludic | 30 +++++- 16 files changed, 273 insertions(+), 29 deletions(-) create mode 100644 packages/ludic.render3d/shaders/spv/a4683767.frag.spv create mode 100644 packages/ludic.render3d/shaders/spv/a4683767.vert.spv create mode 100644 packages/ludic.render3d/shaders/spv/e86f78e8.frag.spv create mode 100644 packages/ludic.render3d/shaders/spv/e86f78e8.vert.spv diff --git a/packages/ludic.render3d/gpu.ludic b/packages/ludic.render3d/gpu.ludic index caed494d..224c32bb 100644 --- a/packages/ludic.render3d/gpu.ludic +++ b/packages/ludic.render3d/gpu.ludic @@ -391,9 +391,10 @@ function gpu_caps_probe() -> void { # Whether the renderer actually draws a feature yet. Until a feature lands, choosing it is saved and # shown, and says it takes effect later. The Vulkan renderer draws the whole game (phase 37-38), and -# DLSS super resolution and Reflex run through NVIDIA Streamline (streamline.ludic); whether this +# DLSS super resolution and Reflex run through NVIDIA Streamline (streamline.ludic), and HDR output is an +# HDR10 swapchain (gpu_vk_draw.ludic); whether this # machine can use one is the caps' question, not this one. -function gpu_feature_implemented(f: int) -> bool { return f == GF_VULKAN or f == GF_DLSS or f == GF_REFLEX } +function gpu_feature_implemented(f: int) -> bool { return f == GF_VULKAN or f == GF_DLSS or f == GF_REFLEX or f == GF_HDR } # ---- vertex data -------------------------------------------------------------------- # A Mesh is built through these and records what it is made of - which buffer feeds which diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index 01cad858..e7dd3788 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -95,6 +95,9 @@ function gvk_init() -> bool { Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_SURFACE_EXTENSION_NAME); n_iext += 1 Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_WIN32_SURFACE_EXTENSION_NAME); n_iext += 1 } + # HDR output: an instance only lists the HDR colour spaces when it asks for them + gvk_has_colorspace = gvk_has_surface and gvk_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) + if gvk_has_colorspace { Vk.put_ptr(iext_names, n_iext * 8, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); n_iext += 1 } if n_iext > 0 { Vk.put_i32(ici, VkInstanceCreateInfo_enabledExtensionCount, n_iext) Vk.put_ptr(ici, VkInstanceCreateInfo_ppEnabledExtensionNames, iext_names) @@ -203,7 +206,7 @@ function gvk_init() -> bool { Vk.put_i32(qci, VkDeviceQueueCreateInfo_queueFamilyIndex, gvk_family) Vk.put_i32(qci, VkDeviceQueueCreateInfo_queueCount, 1) Vk.put_ptr(qci, VkDeviceQueueCreateInfo_pQueuePriorities, prio) - let dext_names = bytes(32) + let dext_names = bytes(48) var n_dext = 0 let dci = bytes(VkDeviceCreateInfo_sizeof) Vk.zero(dci, VkDeviceCreateInfo_sizeof) @@ -217,6 +220,11 @@ function gvk_init() -> bool { Vk.put_ptr(dext_names, n_dext * 8, VK_KHR_SWAPCHAIN_EXTENSION_NAME); n_dext += 1 # Reflex: Streamline adds VK_NV_low_latency2 to this device, which needs present ids it does not add if gsl_on and gvk_ext_in(dexts, nde, "VK_KHR_present_id") { Vk.put_ptr(dext_names, n_dext * 8, "VK_KHR_present_id"); n_dext += 1 } + # HDR output tells the display what the picture holds + # and only where the loader has the command: Streamline's interposer exports no vkSetHdrMetadataEXT, + # and calling its thunk there crashed the game the moment the swapchain came up HDR10 + gvk_has_hdr_meta = gvk_ext_in(dexts, nde, VK_EXT_HDR_METADATA_EXTENSION_NAME) and Vk.has("vkSetHdrMetadataEXT") == 1 + if gvk_has_hdr_meta { Vk.put_ptr(dext_names, n_dext * 8, VK_EXT_HDR_METADATA_EXTENSION_NAME); n_dext += 1 } } if n_dext > 0 { Vk.put_i32(dci, VkDeviceCreateInfo_enabledExtensionCount, n_dext) diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index b927d0b3..f668d565 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -943,6 +943,43 @@ var gvk_layer_views: []string = null # "tex:layer" -> index into gvk_layer_view var gvk_layer_view: []long = null var gvk_fb_ncolor: words = null # per framebuffer: colour slots drawn (draw buffers; 0 = none) +# ---- HDR output ------------------------------------------------------------------------------ +# HDR10 (ST 2084 over BT.2020) when the player asks for it and the surface offers it. The screen +# image and the tonemap's LDR image go 10-bit with it; the tonemap and the overlay switch to their +# HDR10 variants (gpu_hdr_active). Headless there is no surface, so never. +var gvk_hdr_want: bool = false # r3d_hdr: the setting +var gvk_hdr_on: bool = false # the swapchain is HDR10 now +var gvk_has_colorspace: bool = false # the instance took VK_EXT_swapchain_colorspace +var gvk_has_hdr_meta: bool = false # the device took VK_EXT_hdr_metadata +# R3D_HDR=1 / 0 overrides the setting, for a desktop test +function r3d_hdr(on: bool) -> void { + var want = on + if Os.has_env("R3D_HDR") { want = Text.to_int(Os.env("R3D_HDR")) != 0 } + if want == gvk_hdr_want { return } + gvk_hdr_want = want + if gvk_swap != 0 { gvk_swap_stale = true } +} +function gpu_hdr_active() -> bool { return gpu_kind == GPU_VK and gvk_hdr_on } +function gvk_screen_fmt() -> int { if gvk_hdr_on { return GL_RGB10_A2 }; return GL_RGBA8 } + +# what the picture is: graded in BT.709 around D65, highlights to 1000 nits, a 200-nit average +function gvk_hdr_metadata() -> void { + let md = bytes(VkHdrMetadataEXT_sizeof) + Vk.zero(md, VkHdrMetadataEXT_sizeof) + Vk.put_i32(md, VkHdrMetadataEXT_sType, VK_STRUCTURE_TYPE_HDR_METADATA_EXT) + Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_x, fl(0.64)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_y, fl(0.33)) + Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_x, fl(0.30)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_y, fl(0.60)) + Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_x, fl(0.15)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_y, fl(0.06)) + Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_x, fl(0.3127)); Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_y, fl(0.3290)) + Vk.put_i32(md, VkHdrMetadataEXT_maxLuminance, fi(1000)) + Vk.put_i32(md, VkHdrMetadataEXT_minLuminance, fl(0.001)) + Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, fi(1000)) + Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, fi(200)) + let chains = bytes(8) + Vk.put_i64(chains, 0, gvk_swap) + Vk.set_hdr_metadata_ext(gvk_dev, 1, chains, md) +} + function gvk_screen_make(w: int, h: int) -> bool { if gvk_vp == null { gvk_vp = words(4); gvk_sc = words(5); gvk_clear_rgba = words(4) @@ -955,7 +992,7 @@ function gvk_screen_make(w: int, h: int) -> bool { gvk_screen_h = h if gvk_screen_color == 0 { gvk_screen_color = gvk_tex_new(); gvk_screen_depth = gvk_tex_new() } gvk_vp[0] = 0; gvk_vp[1] = 0; gvk_vp[2] = w; gvk_vp[3] = h - return gvk_tex_storage(gvk_screen_color, false, GL_RGBA8, w, h, 1, false) and gvk_tex_storage(gvk_screen_depth, false, GL_DEPTH_COMPONENT32F, w, h, 1, false) + return gvk_tex_storage(gvk_screen_color, false, gvk_screen_fmt(), w, h, 1, false) and gvk_tex_storage(gvk_screen_depth, false, GL_DEPTH_COMPONENT32F, w, h, 1, false) } function gvk_frame_cb() -> pointer { @@ -1292,6 +1329,8 @@ function gvk_present() -> void { # The screen as a binary PPM, top row first. The frame so far is finished first, then read back; # row 0 of the image is OpenGL's bottom row, so rows are written last to first, as gl_screenshot does. function gvk_screenshot(path: string) -> bool { + # the screen image is PQ-encoded 10-bit while HDR is on, which an 8-bit PPM would misread + if gvk_hdr_on { print("r3d: vulkan: screenshots are SDR only - turn HDR output off to take one"); return false } gvk_present() let w = gvk_screen_w let h = gvk_screen_h @@ -1584,6 +1623,17 @@ function gvk_swap_make(w: int, h: int) -> bool { let c = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_colorSpace) if (f == VK_FORMAT_B8G8R8A8_UNORM or f == VK_FORMAT_R8G8B8A8_UNORM) and c == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR { fmt = f; cs = c } } + var hdr = false + if gvk_hdr_want and gvk_has_colorspace { + for i in 0 .. nf { + let f = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_format) + let c = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_colorSpace) + if f == VK_FORMAT_A2B10G10R10_UNORM_PACK32 and c == VK_COLOR_SPACE_HDR10_ST2084_EXT { fmt = f; cs = c; hdr = true } + } + if not hdr { print("r3d: vulkan: HDR output asked for, but this display offers no HDR10 swapchain") } + } + # the screen image carries the swapchain's depth of colour: remade when HDR comes or goes + if hdr != gvk_hdr_on { gvk_hdr_on = hdr; gvk_screen_make(gvk_screen_w, gvk_screen_h) } Vk.put_i32(cnt, 0, 0) Vk.get_physical_device_surface_present_modes_khr(gvk_pd, gvk_surface, cnt, null) let nm = Vk.get_i32(cnt, 0) @@ -1620,6 +1670,7 @@ function gvk_swap_make(w: int, h: int) -> bool { if r != VK_SUCCESS { return gvk_fail("vkCreateSwapchainKHR", r) } if gvk_swap != 0 { Vk.destroy_swapchain_khr(gvk_dev, gvk_swap, null) } gvk_swap = gvk_handle(out) + if gvk_hdr_on and gvk_has_hdr_meta { gvk_hdr_metadata() } Vk.put_i32(cnt, 0, 0) Vk.get_swapchain_images_khr(gvk_dev, gvk_swap, cnt, null) gvk_swap_n = Vk.get_i32(cnt, 0) @@ -1632,7 +1683,9 @@ function gvk_swap_make(w: int, h: int) -> bool { var mname = "fifo" if mode == VK_PRESENT_MODE_MAILBOX_KHR { mname = "mailbox" } if mode == VK_PRESENT_MODE_IMMEDIATE_KHR { mname = "immediate" } - print(`r3d: vulkan swapchain {ew}x{eh}, {gvk_swap_n} images, {mname}`) + var cname = "SDR" + if gvk_hdr_on { cname = "HDR10" } + print(`r3d: vulkan swapchain {ew}x{eh}, {gvk_swap_n} images, {mname}, {cname}`) return true } diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index 8250c33a..09852920 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -11,6 +11,7 @@ # supported Vulkan image format, so they are stored with four and expanded on upload. function gvk_format(ifmt: int) -> int { if ifmt == GL_RGBA8 or ifmt == GL_RGB8 { return VK_FORMAT_R8G8B8A8_UNORM } + if ifmt == GL_RGB10_A2 { return VK_FORMAT_A2B10G10R10_UNORM_PACK32 } # the HDR10 screen and LDR image if ifmt == GL_SRGB8_ALPHA8 or ifmt == GL_SRGB8 { return VK_FORMAT_R8G8B8A8_SRGB } if ifmt == GL_R8 { return VK_FORMAT_R8_UNORM } if ifmt == GL_RG8 { return VK_FORMAT_R8G8_UNORM } diff --git a/packages/ludic.render3d/post.ludic b/packages/ludic.render3d/post.ludic index e5ea51f2..e32c925d 100644 --- a/packages/ludic.render3d/post.ludic +++ b/packages/ludic.render3d/post.ludic @@ -46,6 +46,10 @@ var post_color_w: int = 0 # its size: the display's when DLSS upscaled var post_color_h: int = 0 var post_color: int = 0 # the HDR colour the rest of post reads # the resolved depth, copied so passes can read it while drawing into the frame var post_p_sharp: int = 0 +var post_p_tone_hdr: int = 0 # the tonemap's HDR10 variant, made the first time HDR is on +var post_ldr_hdr: bool = false # post_ldr was made for HDR10 output (10-bit) +# the LDR image is 10-bit while the output is HDR10: PQ in 8 bits bands +function post_ldr_fmt() -> int { if gpu_hdr_active() { return GL_RGB10_A2 }; return GL_RGBA8 } var post_sharpen: int = 0 var post_grain: int = 0 @@ -112,7 +116,8 @@ function post_init(w: int, h: int) -> void { post_ao = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR) post_ao_blur = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR) if post_p_ao == 0 { post_p_ao = r3d_program("fullscreen.vert", "ssgi.frag", ""); post_p_ao_blur = r3d_program("fullscreen.vert", "ssao_blur.frag", "") } - post_ldr = target_new(w, h, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR) + post_ldr = target_new(w, h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR) + post_ldr_hdr = gpu_hdr_active() post_depth_copy = target_new(w, h, GL_R8, GL_RED, GL_UNSIGNED_BYTE, true, GL_NEAREST) post_prev = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR) post_scene = target_new(w, h, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, false, GL_LINEAR) @@ -291,26 +296,37 @@ function post_bloom_pass() -> void { } function post_tonemap(color_tex: int) -> void { + var prog = post_p_tone + if gpu_hdr_active() { + if post_p_tone_hdr == 0 { post_p_tone_hdr = r3d_program("fullscreen.vert", "tonemap.frag", "#define HDR10\n") } + prog = post_p_tone_hdr + } + if post_ldr_hdr != gpu_hdr_active() { + let w = post_ldr.w; let h = post_ldr.h + target_free(post_ldr) + post_ldr = target_new(w, h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR) + post_ldr_hdr = gpu_hdr_active() + } if post_auto { post_measure() } target_bind(post_ldr) gpu_depth_test(false) - gpu_use_program(post_p_tone) - r3d_bind_2d(post_p_tone, "u_hdr", 0, color_tex) - r3d_bind_2d(post_p_tone, "u_bloom", 1, post_bloom[0].color) - r3d_bind_2d(post_p_tone, "u_ao", 2, post_ao_blur.color) - u_f(gpu_uniform(post_p_tone, "u_ao_strength"), post_ao_strength) - u_f(gpu_uniform(post_p_tone, "u_gi_strength"), post_gi_strength) - u_f(gpu_uniform(post_p_tone, "u_exposure"), post_exposure) + gpu_use_program(prog) + r3d_bind_2d(prog, "u_hdr", 0, color_tex) + r3d_bind_2d(prog, "u_bloom", 1, post_bloom[0].color) + r3d_bind_2d(prog, "u_ao", 2, post_ao_blur.color) + u_f(gpu_uniform(prog, "u_ao_strength"), post_ao_strength) + u_f(gpu_uniform(prog, "u_gi_strength"), post_gi_strength) + u_f(gpu_uniform(prog, "u_exposure"), post_exposure) var auto = F_ZERO - if post_auto and post_adapt_t != null { auto = F_ONE; r3d_bind_2d(post_p_tone, "u_adapt", 3, post_adapt_t[post_adapt_i].color) } - u_f(gpu_uniform(post_p_tone, "u_auto"), auto) - u_f(gpu_uniform(post_p_tone, "u_bloom_strength"), post_bloom_strength) - u_f(gpu_uniform(post_p_tone, "u_vignette"), post_vignette) - u_f(gpu_uniform(post_p_tone, "u_saturation"), post_saturation) - u_f(gpu_uniform(post_p_tone, "u_contrast"), post_contrast) - u_f3(gpu_uniform(post_p_tone, "u_wb"), fl(1.02), F_ONE, fl(0.97)) - u_f3(gpu_uniform(post_p_tone, "u_lift"), fl(0.004), fl(0.004), fl(0.012)) - u_f3(gpu_uniform(post_p_tone, "u_gain"), fl(0.99), fl(0.995), fl(1.0)) + if post_auto and post_adapt_t != null { auto = F_ONE; r3d_bind_2d(prog, "u_adapt", 3, post_adapt_t[post_adapt_i].color) } + u_f(gpu_uniform(prog, "u_auto"), auto) + u_f(gpu_uniform(prog, "u_bloom_strength"), post_bloom_strength) + u_f(gpu_uniform(prog, "u_vignette"), post_vignette) + u_f(gpu_uniform(prog, "u_saturation"), post_saturation) + u_f(gpu_uniform(prog, "u_contrast"), post_contrast) + u_f3(gpu_uniform(prog, "u_wb"), fl(1.02), F_ONE, fl(0.97)) + u_f3(gpu_uniform(prog, "u_lift"), fl(0.004), fl(0.004), fl(0.012)) + u_f3(gpu_uniform(prog, "u_gain"), fl(0.99), fl(0.995), fl(1.0)) mesh_draw(post_fs) # sharpen + grain onto the screen gpu_fb_bind(gpu_screen_fb()) @@ -319,7 +335,10 @@ function post_tonemap(color_tex: int) -> void { r3d_bind_2d(post_p_sharp, "u_src", 0, post_ldr.color) u_f2(gpu_uniform(post_p_sharp, "u_texel"), fr(1, post_ldr.w), fr(1, post_ldr.h)) u_f(gpu_uniform(post_p_sharp, "u_amount"), post_sharpen) - u_f(gpu_uniform(post_p_sharp, "u_grain"), post_grain) + # R3D_NOGRAIN=1: no film grain, so two frames of a still camera can be compared for what else moves + var grain = post_grain + if Os.has_env("R3D_NOGRAIN") { grain = F_ZERO } + u_f(gpu_uniform(post_p_sharp, "u_grain"), grain) u_f(gpu_uniform(post_p_sharp, "u_time"), r3d_time) mesh_draw(post_fs) } diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index eb198125..82013944 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -157,6 +157,7 @@ function r3d_resize() -> void { if water_refl != null { target_free(water_refl); water_refl = null } print(`r3d: resized to {gl_w}x{gl_h}`) } +var r3d_cam_log: int = -1 function r3d_frame(time: int) -> void { gpu_glcheck_after("the time between frames") outline_frame() @@ -185,6 +186,12 @@ function r3d_frame(time: int) -> void { prof_gen_frame() prof_mark_start() cam_begin_frame(post_frame, gl_w, gl_h) + # R3D_CAM_LOG=1: the camera each frame, in millimetres and thousandths of a radian - to tell a + # camera that moves while the hiker stands still from a picture that shakes on its own + if r3d_cam_log < 0 { r3d_cam_log = 0; if Os.has_env("R3D_CAM_LOG") { r3d_cam_log = 1 } } + if r3d_cam_log == 1 { + print(`cam {r3d_test_frame} pos {f_to_int(f_mul(cam_pos[0], fi(1000)))} {f_to_int(f_mul(cam_pos[1], fi(1000)))} {f_to_int(f_mul(cam_pos[2], fi(1000)))} yaw {f_to_int(f_mul(cam_yaw, fi(1000)))} pitch {f_to_int(f_mul(cam_pitch, fi(1000)))} jitter {f_to_int(f_mul(gsl_jitter_x, fi(1000000)))} {f_to_int(f_mul(gsl_jitter_y, fi(1000000)))} render {post_w}x{post_h} reset {gsl_reset} evalok {gsl_eval_ok} fresh {gsl_fresh}`) + } # the height-field shadow rebakes as the light moves in steps (daylight), or with the sky yaw when there is no clock if (not day_on and ter_shadow_yaw != sky_yaw) or ter_shadow_gen != day_gen { ter_shadow_gen = day_gen diff --git a/packages/ludic.render3d/shaders/overlay.frag b/packages/ludic.render3d/shaders/overlay.frag index e77a927b..070dabce 100644 --- a/packages/ludic.render3d/shaders/overlay.frag +++ b/packages/ludic.render3d/shaders/overlay.frag @@ -6,6 +6,18 @@ in vec4 v_col; uniform sampler2D u_tex; uniform sampler2D u_font; out vec4 o_color; +#ifdef HDR10 +// the interface is sRGB: drawn into an HDR10 frame it sits at paper white, as the picture's white does +const float HDR_PAPER = 200.0; +vec3 bt709_to_2020(vec3 c) { + return mat3(0.6274, 0.0691, 0.0164, 0.3293, 0.9195, 0.0880, 0.0433, 0.0114, 0.8956) * c; +} +vec3 pq_encode(vec3 nits) { + const float m1 = 0.1593017578125, m2 = 78.84375, c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875; + vec3 yp = pow(clamp(nits / 10000.0, 0.0, 1.0), vec3(m1)); + return pow((c1 + c2 * yp) / (1.0 + c3 * yp), vec3(m2)); +} +#endif void main() { float mode = floor((v_uv.y + 0.5) * 0.25); vec2 uv = vec2(v_uv.x, v_uv.y - 4.0 * mode); @@ -17,4 +29,7 @@ void main() { vec4 t = texture(u_tex, uv); o_color = vec4(v_col.rgb * t.rgb, v_col.a * t.a); } +#ifdef HDR10 + o_color.rgb = pq_encode(bt709_to_2020(pow(max(o_color.rgb, vec3(0.0)), vec3(2.2))) * HDR_PAPER); +#endif } diff --git a/packages/ludic.render3d/shaders/spv/725523d9.frag.spv b/packages/ludic.render3d/shaders/spv/725523d9.frag.spv index 83879b1d3190699d73ec5ae92178b06624b8db66..25caa671d2b708d7443bc0f3b2b120ea5f8e6aa4 100644 GIT binary patch delta 2103 zcmYk7OK6l=6vt08^G#wVF-|ffw!YHnq9}DOVpOD+QpC7WR}xaAF%X$JYF&tYh=_t9 z$hgsM7Vd+Z^GneKZp4*VB zZmlt^vaHqG=v@upL@&&2==)_0qBvy-W%m_VSY&A%#O^G1uGSn`%Fb!-UB5c9Z^&K~ z_}iMl>**GsF=}=9c6?Rr?4DOB6uaamwO(reSSxKq@(Ufy3%!M6k{4B(tuN&=%?n0g zb!ZPOdwSQcKuud+!hKupT1mCYMkfB+Rh?ah4doRgYfd<21e&<~z?#hL^ddHu6&i*4 zD#zjz1O`5fx5!pohW*bFHPG1-dae7Vmh70GD0pnHGMdd1JtFA%l{yPui@jMdV%Ss7KNaFG7HXQkjG}CRySam2<8lP*O zRU;-NP@y)caAe@qe}-3};|wZfOPg}7alM$VK!t{&!jXZ`9WZ>)jq-0aew{RjW{i@* zch8V}DY>GP(2U-D1}1^P-Tp8;Qw`#ITsLGGj9 zuJF-c4!mmG5yi8VPS_!#UuaV=2ZKLDPaN$BVWco9pc4n@xj)K=o2&T!ntu}d1fG<~ z90~l^Q1hsK<;8~JiN|G=$lLS86SCpmKPj8V+QYj#m1yW?YzYgTmXAifFM38c_0Tpa zak1H1IqU|}PY34@`l9Bm z0;lIo^J3Qm@5QdmhjY~HM}sT+S>UaFA+R9DiS+}aQ*)H&UxdZNP8}$Q_$`op((NMQ zZVHtxg}WsXi$-Cd*e!wU;{bPsK7pSCKfv#S_r?E^PhRk2yB7#=?|u0M!E$W^tf%Q57oZ}OaK4? delta 1975 zcmYk6Nl2AZ7>3XF4(h#{*EA%dxfWpqE+sUjMMy=qh#(NTIgns!x1>eHPefD%LEJXF zZP88;cP+xow0WLwuz5~xE}P!xzvr0e@^ZfS{f2YC@BjbXS9PN@H<&Re)0lvXnJklk zD0fA8*cda`UArn=5;DhSwai$HP?;_dBSoP;PB}J3(n!QWUJY6J{zYNelCGOD>WJ| zn$PjDu=T=g$@2QP?#;uu0;? zpqDA$-HPX_6&)AisjA6G=QT*vWG0E z{1bsZcl=Y!-8`S!7<3*KGkh*k2cOkE^TIkhweyq-d?|2Ed>p_mUI`4A{@|~zMy#6$ znu~uUoA?|>6aQXdmc;@-{*fw-mwxYYlE^>HsKCF_YNgTXN2{T;D8hjw+VSx{ap_M& zp@8lh`D}HX+h^mx%0}m(t=zNSeG}of?v3~^n?MKmUjL9rEPmHDn(O+PZ0fABhy9j~ o#&X&p*=RIU=ET2N%+Ow=&Y1)e6d(aVkAy?{&ow>e)Ak1c0Vx9eHvj+t diff --git a/packages/ludic.render3d/shaders/spv/a4683767.frag.spv b/packages/ludic.render3d/shaders/spv/a4683767.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..00a755faa70e2acbd36cb45ae4374be6ef45d913 GIT binary patch literal 9940 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 N*|2Yt&_{Us_+LidT9*I- literal 0 HcmV?d00001 diff --git a/packages/ludic.render3d/shaders/spv/e86f78e8.vert.spv b/packages/ludic.render3d/shaders/spv/e86f78e8.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..2259b608971424f76d26daf6436aeab4d803a8ae GIT binary patch literal 2028 zcmYk6Sx;0!5QQ6N0XGy8a6udx7Zk+}5JZ?^Q6S-A5~8n@$#5|@Uha@ti19^@G4aV? z<`444#PjuZ4K%6gKBrDsS6A01(_b8LuFqX?gYKQ{j$zj?#)-@Od3$evZ?4ron44dC zhGNKNyb#T0cge4_`aP<}DFCJ@L;PI!Qnn!5lD(B>^w+QXj{@XeR)42^b2>9GoOBN2 zdi7&cCFl&gjPh~rSjHaCs8x>}EsqD4hjUVIMTfOc(te-DhmGdZM$)K%ay}w{MyJ}U zH>1esNzCCct(bW?rL|&IqUMLF*^WNrA{%%HzD6r<$Bk6%faj@!=i{`!AAgC^nU6cl zx0%GpTXCyhOY2e3M+Nb9lBDM&AL_>7j@0j@+Bq>yEo)~mbv>!p8;NH0)VS8ed0%gx z(Y?%Wdccix%9}S3zngY*3LfXSLtQ83XKX7U>c%~Gw9{&6Iijb2Rv*LD;u!45XZuaZ zJ2G~(-?*zD=Xy0o(|3j=1{`%G7xuA+qklM?dY$H;^}Pos4u0x&7E>=UvGLQN#CsGl zbpp@IMx;VFrBgYV^_W(JnhUaV?LVg(jISVkUb8+f&x0>&O)ltG0;Z4QCKZeN;zN5y zGjZV1f~hs!s4(>^$k;m@@QBt7_FvPCmU#Hd>$*&%Z1|>!-%8|uTWd7blK9l6AY-5h z-;r_Ngm6JKdbr8(jaUqPUCX*@t*O_PjC+8KfwQ=Gg&E|1PctB!e~YkuwAcb&q%l8n9ZKLfWO__Ah;x1qJgi63y1cfZnGq_`;afg?!)_X zcfJq4FEIJ=hC?#GiFKV#50A>|d+ukNvB3Y5hCSml_QC%S+>OA~^OeBe(wd&fegtkp zYqa>d1Np#lKD~Hf21k!t%^n2aG&4cNw_uuwTEl-0@4^2RxQcE|uRWF#2cG^WC*skc hb26(fIf2pJ{a void { @@ -273,6 +274,13 @@ function gsl_fill_options(w: int, h: int) -> void { Vk.put_i32(gsl_opts, 48, F_ONE) # preExposure Vk.put_i32(gsl_opts, 52, F_ONE) # exposureScale Vk.put_i32(gsl_opts, 56, 1) # colorBuffersHDR eTrue + # The model: preset K (the transformer NVIDIA calls its best image quality) in every mode. The + # defaults put Performance on preset M, which on an RTX 3070 Ti at 4K evaluated in 18 ms against + # K's 2.8 - slower than no DLSS at all (33 fps against 41; with K, 60). R3D_DLSS_PRESET= + # (sl::DLSSPreset: 11 K, 12 L, 13 M) sets every mode's, to measure them against each other. + var preset = 11 + if Os.has_env("R3D_DLSS_PRESET") { preset = Text.to_int(Os.env("R3D_DLSS_PRESET")) } + if preset > 0 { for f in 0 .. 6 { Vk.put_i32(gsl_opts, 60 + f * 4, preset) } } # dlaa, quality, balanced, performance, ultra performance, ultra quality } # the render size for the display's size and the mode, asked once per change @@ -311,7 +319,10 @@ function gsl_halton(i: int, b: int) -> int { # cam_begin_frame: this frame's sub-pixel offset, before the camera builds its matrices function gsl_jitter_frame() -> void { gsl_jitter_x = F_ZERO; gsl_jitter_y = F_ZERO; gsl_jpx = F_ZERO; gsl_jpy = F_ZERO - if not r3d_dlss_live() or post_w <= 0 or post_h <= 0 { return } + # a jittered frame nobody resolves shakes on screen however still the camera is: jitter only while + # this frame holds a DLSS token and the last evaluate worked. (Not gsl_fresh: gsl_frame_start has + # already taken the token and cleared it by the time the camera asks, which turned jitter off.) + if not r3d_dlss_live() or not gsl_eval_ok or gsl_token == null or post_w <= 0 or post_h <= 0 { return } # DLSS wants at least 8 x (display / render)^2 phases; 32 covers performance mode let i = (gsl_frame_n % 32) + 1 gsl_jpx = f_sub(gsl_halton(i, 2), F_HALF) @@ -400,9 +411,16 @@ function gsl_constants() -> void { m4_inverse(p2c, c2p) gsl_put_m4(k, 224, c2p) # clipToPrevClip gsl_put_m4(k, 288, p2c) # prevClipToClip - # the sample's offset from the pixel centre, in the flipped image - Vk.put_i32(k, 352, f_neg(gsl_jpx)) - Vk.put_i32(k, 356, gsl_jpy) + # the sample's offset from the pixel centre, in the image Streamline sees: row 0 at the top, so the + # vertical offset flips with it. With jy unflipped DLSS resolved the ground into concentric + # rings; with jx flipped too, thin stems doubled sideways (PC shots, 2026-09-15). + var jx = f_neg(gsl_jpx) + var jy = f_neg(gsl_jpy) + # R3D_DLSS_JX / R3D_DLSS_JY = -1 flip a sign, to check the convention against the picture + if Os.has_env("R3D_DLSS_JX") and Text.to_int(Os.env("R3D_DLSS_JX")) < 0 { jx = f_neg(jx) } + if Os.has_env("R3D_DLSS_JY") and Text.to_int(Os.env("R3D_DLSS_JY")) < 0 { jy = f_neg(jy) } + Vk.put_i32(k, 352, jx) + Vk.put_i32(k, 356, jy) Vk.put_i32(k, 360, F_ONE) # mvecScale Vk.put_i32(k, 364, F_ONE) gsl_put_v3(k, 376, cam_pos) @@ -438,7 +456,8 @@ function gsl_targets() -> void { # the LDR image the tonemap writes follows the upscaled size if post_ldr.w != gl_w or post_ldr.h != gl_h { target_free(post_ldr) - post_ldr = target_new(gl_w, gl_h, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR) + post_ldr = target_new(gl_w, gl_h, post_ldr_fmt(), GL_RGBA, GL_UNSIGNED_BYTE, false, GL_LINEAR) + post_ldr_hdr = gpu_hdr_active() } if gsl_res == null { gsl_res = gsl_struct(4 * 112); gsl_tags = gsl_struct(4 * 64); gsl_inputs = bytes(8) } } @@ -470,6 +489,7 @@ function gsl_dlss_eval() -> int { gsl_reset = false # Streamline records its own pipeline and descriptors into the command buffer; nothing needs # forgetting, because every gvk_draw binds its pipeline, view and set afresh + gsl_eval_ok = r == 0 if r != 0 { if not gsl_said { print(`r3d: streamline: DLSS evaluate failed ({r}); drawing without it`); gsl_said = true } post_color_w = post_w; post_color_h = post_h