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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 20:06:18 +03:00
parent 0224af64ab
commit 6525c11b3c
16 changed files with 273 additions and 29 deletions

View file

@ -21,6 +21,19 @@ vec3 aces(vec3 x) {
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}
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
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() {
vec3 hdr = sane(texture(u_hdr, v_uv).rgb);
vec4 gi = texture(u_ao, v_uv);
@ -34,14 +47,25 @@ void main() {
// filmic contrast around mid grey in log space
c = max(c, vec3(0.0));
c = pow(c / 0.18, vec3(u_contrast)) * 0.18;
vec3 pre = c; // what ACES is about to roll off, kept for HDR highlights
c = aces(c);
// lift / gain grade in display space
c = c * u_gain + u_lift * (1.0 - c);
float lum = dot(c, vec3(0.2126, 0.7152, 0.0722));
c = mix(vec3(lum), c, u_saturation);
vec2 q = v_uv * 2.0 - 1.0;
c *= 1.0 - u_vignette * dot(q, q) * 0.5;
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;
vec3 e = pq_encode(bt709_to_2020(nits));
e += (hash(gl_FragCoord.xy) - 0.5) / 1023.0;
o_color = vec4(e, 1.0);
#else
c = pow(c, vec3(1.0 / 2.2));
c += (hash(gl_FragCoord.xy) - 0.5) / 255.0;
o_color = vec4(c, 1.0);
#endif
}