Merge feat/gpu-driven: HDR calibration and the HDR toggle crash fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
ce94944c2d
10 changed files with 124 additions and 19 deletions
16
changes/hdr-calibration.md
Normal file
16
changes/hdr-calibration.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
bump: patch
|
||||
type: feat
|
||||
**HDR calibration, and two HDR fixes** — the HDR10 picture follows the player's display instead of one fixed curve.
|
||||
|
||||
- **`r3d_hdr_calibrate(peak, paper, black)`** — nits as float bits: the brightest the display shows (100-10000),
|
||||
where the picture's and the interface's white sit (80-1000, never above the peak) and how far the darkest shade is
|
||||
lifted (0-5, fading out by paper white). The tonemap's and the overlay's HDR10 variants read them, and the display's
|
||||
HDR metadata is sent again with the new peak. The defaults are the old constants: 1000, 200 and 0.
|
||||
- **`ov_hdr_nits(nits)` / `ov_hdr_paper()`** — what the overlay draws next is at that many nits on an HDR10 frame,
|
||||
for a calibration screen's test patches; it closes the batch so far. No effect on an SDR frame.
|
||||
- **Fixed: a crash toggling HDR on the Vulkan renderer.** `gpu_caps_probe()` made and destroyed a second Vulkan
|
||||
instance under NVIDIA Streamline's interposer; the next swapchain rebuild then called through a pointer that
|
||||
instance had left behind, at address 0. With the Vulkan renderer running, the probe asks its instance instead.
|
||||
- **Fixed: yellow read as red in HDR.** The overlay drew sRGB values straight into the HDR10 swapchain (it now has an
|
||||
HDR10 variant, chosen while `gpu_hdr_active()`), and the tonemap extended highlights per channel, which boosted a
|
||||
bright yellow's red far more than its green; it now brightens the graded colour by one factor.
|
||||
|
|
@ -337,9 +337,18 @@ function gpu_caps_probe() -> void {
|
|||
Vk.zero(ici, VkInstanceCreateInfo_sizeof)
|
||||
Vk.put_i32(ici, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO)
|
||||
Vk.put_ptr(ici, VkInstanceCreateInfo_pApplicationInfo, app)
|
||||
let out = bytes(8)
|
||||
if Vk.create_instance(ici, null, out) != VK_SUCCESS { return }
|
||||
let inst = Vk.get_ptr(out, 0)
|
||||
# With the Vulkan renderer running, ask its own instance: the renderer's loader is NVIDIA Streamline's
|
||||
# interposer, and a second instance made and destroyed under it is what this avoids.
|
||||
var inst: pointer = null
|
||||
var own = false
|
||||
if gpu_kind == GPU_VK and gvk_ready {
|
||||
inst = gvk_inst
|
||||
} else {
|
||||
let out = bytes(8)
|
||||
if Vk.create_instance(ici, null, out) != VK_SUCCESS { return }
|
||||
inst = Vk.get_ptr(out, 0)
|
||||
own = true
|
||||
}
|
||||
Vk.put_i32(cnt, 0, 0)
|
||||
Vk.enumerate_physical_devices(inst, cnt, null)
|
||||
let nd = Vk.get_i32(cnt, 0)
|
||||
|
|
@ -385,7 +394,7 @@ function gpu_caps_probe() -> void {
|
|||
gpu_cap_mesh = gpu_ext_in(dexts, ne, VK_EXT_MESH_SHADER_EXTENSION_NAME)
|
||||
gpu_cap_reflex = gpu_ext_in(dexts, ne, VK_NV_LOW_LATENCY_2_EXTENSION_NAME)
|
||||
}
|
||||
Vk.destroy_instance(inst, null)
|
||||
if own { Vk.destroy_instance(inst, null) }
|
||||
print(`r3d: gpu caps: {gpu_cap_device} vulkan {gpu_cap_vulkan} floor {gpu_cap_floor} rt {gpu_cap_rt} mesh {gpu_cap_mesh} rtx {gpu_cap_rtx} reflex {gpu_cap_reflex} hdr {gpu_cap_hdr}`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -973,7 +973,28 @@ function r3d_hdr(on: bool) -> void {
|
|||
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
|
||||
# The player's calibration of their display, in nits (float bits): the brightest it shows, where the
|
||||
# picture's and the interface's white sit, and how far the darkest shade is lifted. Displays differ by
|
||||
# an order of magnitude - a 400-nit monitor and a 2000-nit television - and one fixed curve either
|
||||
# clips the first's highlights flat or leaves the second dim.
|
||||
var r3d_hdr_peak: int = 0 # 0 until r3d_hdr_calibrate: 1000
|
||||
var r3d_hdr_paper: int = 0 # 200
|
||||
var r3d_hdr_black: int = 0 # 0
|
||||
function r3d_hdr_peak_nits() -> int { if r3d_hdr_peak == 0 { return fi(1000) }; return r3d_hdr_peak }
|
||||
function r3d_hdr_paper_nits() -> int { if r3d_hdr_paper == 0 { return fi(200) }; return r3d_hdr_paper }
|
||||
function r3d_hdr_black_nits() -> int { return r3d_hdr_black }
|
||||
function r3d_hdr_calibrate(peak: int, paper: int, black: int) -> void {
|
||||
var pk = f_clamp(peak, fi(100), fi(10000))
|
||||
let pp = f_clamp(paper, fi(80), fi(1000))
|
||||
if f_ls(pk, pp) { pk = pp }
|
||||
let bl = f_clamp(black, F_ZERO, fi(5))
|
||||
if pk == r3d_hdr_peak and pp == r3d_hdr_paper and bl == r3d_hdr_black { return }
|
||||
r3d_hdr_peak = pk; r3d_hdr_paper = pp; r3d_hdr_black = bl
|
||||
# the display is told what the picture now reaches
|
||||
if gvk_hdr_on and gvk_has_hdr_meta and gvk_swap != 0 { gvk_hdr_metadata() }
|
||||
}
|
||||
|
||||
# what the picture is: graded in BT.709 around D65, highlights to the calibrated peak, paper white average
|
||||
function gvk_hdr_metadata() -> void {
|
||||
let md = bytes(VkHdrMetadataEXT_sizeof)
|
||||
Vk.zero(md, VkHdrMetadataEXT_sizeof)
|
||||
|
|
@ -982,10 +1003,10 @@ function gvk_hdr_metadata() -> void {
|
|||
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_maxLuminance, r3d_hdr_peak_nits())
|
||||
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))
|
||||
Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, r3d_hdr_peak_nits())
|
||||
Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, r3d_hdr_paper_nits())
|
||||
let chains = bytes(8)
|
||||
Vk.put_i64(chains, 0, gvk_swap)
|
||||
Vk.set_hdr_metadata_ext(gvk_dev, 1, chains, md)
|
||||
|
|
|
|||
|
|
@ -53,10 +53,40 @@ var ov_clip_y: int = 0
|
|||
var ov_clip_w: int = 0
|
||||
var ov_clip_h: int = 0
|
||||
|
||||
# The interface is sRGB. While the output is HDR10 the overlay draws with its HDR10 variant, which puts
|
||||
# the interface at the picture's paper white; the SDR program there sent sRGB values as PQ, and the
|
||||
# menu's yellow read as red on an HDR display.
|
||||
var ov_prog_sdr: int = 0
|
||||
var ov_prog_hdr: int = 0
|
||||
# How bright the interface's white is while HDR10 is on, as a multiple of paper white. It is 1 for the
|
||||
# interface; a calibration screen draws its test patches at a number of nits with ov_hdr_nits, which
|
||||
# closes the batch so far - the multiple is one uniform per flush. On an SDR frame it does nothing.
|
||||
var ov_hdr_scale: int = 0 # float bits; 0 until ov_begin sets 1
|
||||
function ov_hdr_nits(nits: int) -> void {
|
||||
var s = F_ONE
|
||||
if nits != 0 { s = f_div(nits, r3d_hdr_paper_nits()) }
|
||||
if s == ov_hdr_scale { return }
|
||||
if ov_open { ov_flush() }
|
||||
ov_hdr_scale = s
|
||||
}
|
||||
# back to the interface's own white
|
||||
function ov_hdr_paper() -> void { ov_hdr_nits(0) }
|
||||
function ov_pick_prog() -> void {
|
||||
if gpu_hdr_active() {
|
||||
if ov_prog_hdr == 0 {
|
||||
r3d_program_log("overlay.vert", "overlay.frag", "#define HDR10\n")
|
||||
ov_prog_hdr = gpu_program("#version 410 core\n#define HDR10\n" + r3d_shader_file("overlay.vert"), "#version 410 core\n#define HDR10\n" + r3d_shader_file("overlay.frag"), "overlay.vert", "overlay.frag", "#define HDR10\n")
|
||||
}
|
||||
if ov_prog_hdr != 0 { ov_prog = ov_prog_hdr; return }
|
||||
}
|
||||
ov_prog = ov_prog_sdr
|
||||
}
|
||||
|
||||
function overlay_init(font_dir: string) -> bool {
|
||||
r3d_program_log("overlay.vert", "overlay.frag", "")
|
||||
ov_prog = gpu_program("#version 410 core\n" + r3d_shader_file("overlay.vert"), "#version 410 core\n" + r3d_shader_file("overlay.frag"), "overlay.vert", "overlay.frag", "")
|
||||
if ov_prog == 0 { print("overlay: program failed"); return false }
|
||||
ov_prog_sdr = ov_prog
|
||||
ov_mesh = gpu_mesh_new()
|
||||
ov_vbo = gpu_mesh_vertices(ov_mesh, null, gl_bytes_of(OV_MAX_QUADS * 6 * OV_FLOATS), GPU_DYNAMIC)
|
||||
gpu_mesh_attr(ov_mesh, 0, 2, GPU_F32, OV_FLOATS * 4, 0, false)
|
||||
|
|
@ -163,12 +193,14 @@ function ov_begin() -> void {
|
|||
gpu_cull(false)
|
||||
gpu_blend(true)
|
||||
gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||
ov_pick_prog()
|
||||
gpu_use_program(ov_prog)
|
||||
u_f2(gpu_uniform(ov_prog, "u_screen"), fi(gl_w), fi(gl_h))
|
||||
ov_n = 0; ov_nr = 0; ov_range_start = 0
|
||||
ov_tex = ov_white
|
||||
ov_mode = 2; ov_voff = fi(8)
|
||||
ov_clip_w = 0
|
||||
ov_hdr_scale = F_ONE
|
||||
ov_open = true
|
||||
}
|
||||
# everything drawn until ov_unclip stays inside this rectangle (a scrolling list)
|
||||
|
|
@ -198,12 +230,17 @@ function ov_flush() -> void {
|
|||
# framebuffer operation. Saying the target at each flush is what a render pass says anyway.
|
||||
gpu_fb_bind(gpu_screen_fb())
|
||||
gpu_viewport(0, 0, gl_w, gl_h)
|
||||
ov_pick_prog()
|
||||
gpu_use_program(ov_prog)
|
||||
gpu_mesh_bind(ov_mesh)
|
||||
gpu_buffer_upload(ov_vbo, gl_bytes_of(ov_n * 6 * OV_FLOATS), ov_buf, GPU_STREAM)
|
||||
var font = ov_font
|
||||
if font == 0 { font = ov_white }
|
||||
r3d_bind_2d(ov_prog, "u_font", 1, font)
|
||||
var hs = ov_hdr_scale
|
||||
if hs == 0 { hs = F_ONE }
|
||||
u_f(gpu_uniform(ov_prog, "u_hdr_paper"), r3d_hdr_paper_nits())
|
||||
u_f(gpu_uniform(ov_prog, "u_hdr_scale"), hs)
|
||||
var last = -1
|
||||
var clipped = false
|
||||
for i in 0 .. ov_nr {
|
||||
|
|
|
|||
|
|
@ -327,6 +327,10 @@ function post_tonemap(color_tex: int) -> void {
|
|||
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))
|
||||
# the HDR10 variant's display calibration (the SDR program has none of these, and -1 sets nothing)
|
||||
u_f(gpu_uniform(prog, "u_hdr_peak"), r3d_hdr_peak_nits())
|
||||
u_f(gpu_uniform(prog, "u_hdr_paper"), r3d_hdr_paper_nits())
|
||||
u_f(gpu_uniform(prog, "u_hdr_black"), r3d_hdr_black_nits())
|
||||
mesh_draw(post_fs)
|
||||
# sharpen + grain onto the screen
|
||||
gpu_fb_bind(gpu_screen_fb())
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ 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;
|
||||
// the interface is sRGB: drawn into an HDR10 frame its white sits at the player's paper white, as the
|
||||
// picture's white does. u_hdr_scale multiplies that for one batch (ov_hdr_nits): a calibration
|
||||
// screen's test patches, which have to reach past paper white to the display's peak.
|
||||
uniform float u_hdr_paper;
|
||||
uniform float u_hdr_scale;
|
||||
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;
|
||||
}
|
||||
|
|
@ -30,6 +33,7 @@ void main() {
|
|||
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);
|
||||
float nits = max(u_hdr_paper, 80.0) * max(u_hdr_scale, 0.0);
|
||||
o_color.rgb = pq_encode(bt709_to_2020(pow(max(o_color.rgb, vec3(0.0)), vec3(2.2))) * nits);
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -578,7 +578,7 @@ T 725523d9 u_shadow 9
|
|||
T 725523d9 u_tershadow 10
|
||||
T 725523d9 u_ts_height 11
|
||||
P a4683767 fullscreen.vert tonemap.frag #define HDR10;
|
||||
B a4683767 frag 1 852
|
||||
B a4683767 frag 1 864
|
||||
U a4683767 frag u_cascade_vp 0 mat4 5 64
|
||||
U a4683767 frag u_cascade_split 320 float 5 16
|
||||
U a4683767 frag u_cascade_range 400 float 5 16
|
||||
|
|
@ -618,6 +618,9 @@ U a4683767 frag u_bloom_strength 836 float 1 0
|
|||
U a4683767 frag u_vignette 840 float 1 0
|
||||
U a4683767 frag u_saturation 844 float 1 0
|
||||
U a4683767 frag u_contrast 848 float 1 0
|
||||
U a4683767 frag u_hdr_paper 852 float 1 0
|
||||
U a4683767 frag u_hdr_peak 856 float 1 0
|
||||
U a4683767 frag u_hdr_black 860 float 1 0
|
||||
T a4683767 u_adapt 2
|
||||
T a4683767 u_ao 3
|
||||
T a4683767 u_bloom 4
|
||||
|
|
@ -1932,7 +1935,7 @@ U e86f78e8 vert u_screen 0 vec2 1 0
|
|||
I e86f78e8 a_pos 0 vec2
|
||||
I e86f78e8 a_uv 1 vec2
|
||||
I e86f78e8 a_col 2 vec4
|
||||
B e86f78e8 frag 0 764
|
||||
B e86f78e8 frag 1 772
|
||||
U e86f78e8 frag u_cascade_vp 0 mat4 5 64
|
||||
U e86f78e8 frag u_cascade_split 320 float 5 16
|
||||
U e86f78e8 frag u_cascade_range 400 float 5 16
|
||||
|
|
@ -1961,6 +1964,8 @@ U e86f78e8 frag u_ts_on 748 float 1 0
|
|||
U e86f78e8 frag u_force_cascade 752 int 1 0
|
||||
U e86f78e8 frag u_cloud_shadow 756 float 1 0
|
||||
U e86f78e8 frag u_time 760 float 1 0
|
||||
U e86f78e8 frag u_hdr_paper 764 float 1 0
|
||||
U e86f78e8 frag u_hdr_scale 768 float 1 0
|
||||
T e86f78e8 u_brdf 2
|
||||
T e86f78e8 u_font 3
|
||||
T e86f78e8 u_irradiance 4
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ vec3 aces(vec3 x) {
|
|||
}
|
||||
float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
|
||||
#ifdef HDR10
|
||||
// HDR10 output: nits in BT.2020 primaries, encoded with the SMPTE ST 2084 (PQ) curve
|
||||
const float HDR_PAPER = 200.0; // nits the SDR picture's white sits at
|
||||
const float HDR_PEAK = 1000.0; // nits the brightest highlight rolls on to
|
||||
// HDR10 output: nits in BT.2020 primaries, encoded with the SMPTE ST 2084 (PQ) curve. The three
|
||||
// numbers are the player's calibration of their display (r3d_hdr_calibrate).
|
||||
uniform float u_hdr_paper; // nits the SDR picture's white sits at
|
||||
uniform float u_hdr_peak; // nits the brightest highlight rolls on to: what the display reaches
|
||||
uniform float u_hdr_black; // nits the darkest shade is lifted to, fading out toward paper white
|
||||
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;
|
||||
}
|
||||
|
|
@ -57,9 +59,16 @@ void main() {
|
|||
float vig = 1.0 - u_vignette * dot(q, q) * 0.5;
|
||||
c *= vig;
|
||||
#ifdef HDR10
|
||||
// up to paper white the SDR picture as it is; past it, what ACES clipped runs on to the peak
|
||||
vec3 hi = max(pre - 1.0, vec3(0.0));
|
||||
vec3 nits = (c + hi / (hi + 1.0) * (HDR_PEAK / HDR_PAPER - 1.0) * vig) * HDR_PAPER;
|
||||
// up to paper white the SDR picture as it is; past it, the whole graded colour is brightened by one
|
||||
// factor from its brightest channel. Extending each channel on its own boosted a bright yellow's red
|
||||
// far more than its green, and yellow read as red on an HDR display.
|
||||
float paper = max(u_hdr_paper, 80.0);
|
||||
float peak = max(u_hdr_peak, paper);
|
||||
float over = max(max(pre.r, max(pre.g, pre.b)) - 1.0, 0.0);
|
||||
float lift = 1.0 + over / (over + 1.0) * (peak / paper - 1.0);
|
||||
vec3 nits = c * lift * paper;
|
||||
// the black level: the shadows' floor raised for a display that crushes them, gone by paper white
|
||||
nits += max(u_hdr_black, 0.0) * (1.0 - clamp(nits / paper, 0.0, 1.0));
|
||||
vec3 e = pq_encode(bt709_to_2020(nits));
|
||||
e += (hash(gl_FragCoord.xy) - 0.5) / 1023.0;
|
||||
o_color = vec4(e, 1.0);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue