fix(render): keep today's HDR look on the float frame, and draw world text in the frame's format

Marked pixels (light sources, the sun, HDR lighting) clip at white before their lift, as on
a unorm frame; only unmarked ones show light over 1, which is how shaders will write light.
Without that, the float frame tinted the sun and its halo instead of lifting them white.

The native world-text pipeline targeted the swapchain's 8-bit format, which a 16-bit or
float frame's pass refuses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Nikita Maximilian GrimmandClaude Opus 5.5 committed 2026-11-06 21:00:16 +01:00
1 parent b376a835f6
commit 44753cefe0
3 files changed
+7 -6

No files matched your search

+1 -1
View File
@@ -3794,7 +3794,7 @@ impl Renderer {
&device,
Some(&entity_layout),
&entity_shader,
config.format.remove_srgb_suffix(),
frame_format,
);
// RenderPipelines.ENTITY_SOLID: a surface of an entity that is coplanar with
// terrain wins the tie in vanilla (the boat's floor over the trapdoor on the
+5 -4
View File
@@ -178,10 +178,11 @@ fn calibration_nits(p: vec2<f32>, size: vec2<f32>) -> f32 {
var lifted = false;
let shows = params.world > 0.5 && params.peak > 1.0
&& (hand || (!covered && shows_world(f.rgb, w.rgb)));
// Light over 1 (a float frame's) shows only where the world does, up to the
// peak; under the HUD and menus, and with HDR Lighting off, it clips at white as
// a unorm frame does.
let c = select(min(f.rgb, vec3(1.0)), f.rgb, shows);
// Light over 1 (a float frame's) shows only where the world does and carries
// no mark (alpha 1), up to the peak: light written as light. Marked pixels clip
// at white before their lift, as on a unorm frame, so light sources, the sun and
// HDR lighting look as they do there; so do the HUD, menus and HDR Lighting off.
let c = select(min(f.rgb, vec3(1.0)), f.rgb, shows && w.a == 1.0);
// Gamma 2.2, as SDR displays and the compositors' SDR paths decode it.
var light = pow(c, vec3(2.2));
// HDR lighting: alpha between 0.76 and 1 is how much of it the surface gets. Vanilla's
+1 -1
View File
@@ -45,7 +45,7 @@ Every setting applies at once, except worker threads, the graphics API and trans
**Translucent batching** (Advanced, after Graphics API; on by default, `batchTranslucent` in `horizon-graphics.txt`, a new key so that the `translucentBatching:false` earlier builds saved for everyone doesn't keep it off; it applies at the next start or resource reload, and is no part of a preset) gives water, glass, ice and other translucent quads a vertex buffer of their own, with pages up to 2 GiB, and their sorted indices the same: the back-to-front list then stays in one page and goes out in a few draw calls instead of switching pages every few sections. The picture is the same and frames come faster, most over water and at high render distances (RTX 4090 at 1080p: 4 to 16% more FPS at render distance 32; over the ocean at 64, 354 draw calls a frame instead of 3,832 and 43% more FPS); it costs a little video memory, as solid and translucent quads no longer share free space (terrain meshes take 1 to 15% more at render distance 32, 5 to 10% more at 64). The large pages shrink once the view has settled (a second after terrain stops arriving): one much larger than what it holds, as after a teleport or a lower render distance, moves into a page of the right size, about 32 MiB a frame. It needs indirect multi-draw that isn't OpenGL's (which loops over the entries on the CPU), and devices whose vanilla frames draw flat (Metal, Intel Gen11 and Gen12 on Vulkan) don't offer it: there the switch is greyed out and reads Off, and macOS doesn't show it. A `shaders/` pack's terrain and shadow passes, and the `[shadow]` pass of one of Horizon's own packs, draw flat where the device can, reading translucent quads beside their section's solid ones: while such a pack runs they go back there. Such a pack picked at the start has the world laid out for it from the first; switching to or from one later meshes the world again.
**HDR** (off by default) has its own page, HDR, greyed where the display path offers no HDR output (so players without an HDR display don't take its settings for their display's); the switch comes first and the sliders below it only while it is on. It presents to an HDR10 swapchain (`Rgb10a2Unorm`, BT.2020 with the PQ curve) or, where only that is offered, an scRGB one (`Rgba16Float`, extended linear sRGB), on displays and compositors that offer either; `HORIZON_HDR_OUTPUT=pq` or `scrgb` allows only one, and offers it to an `--offscreen` run, which has no display to ask: there HDR is drawn and screenshotted (the `-hdr.png` below) but not presented, and Auto takes 203 and 1000 nits, so HDR can be checked without an HDR display. The frame is drawn exactly as in SDR, in vanilla's gamma space (into a 16-bit target (`Rgba16Unorm`, which clips at white after every pass as the 8-bit one does) when HDR is on as the renderer is made, at the start or a resource reload, with **Smooth Gradients** on (the default, `hdrSmoothGradients`; off, the 8-bit frame) and where the device can draw into it, else an 8-bit one; HDR switched off after that keeps the 16-bit frame and copies it to the 8-bit swapchain, without frame generation, until the next start; with `HORIZON_HDR_FRAME=float` and no shader pack, a float one (`Rgba16Float`) in its place whatever Smooth Gradients is, which keeps what blends add past white (the sun on the sky): the last pass shows it where the world shows, up to the peak, and clips it at white under the HUD and menus and with HDR Lighting off ([hdr-linear-light.md](design/hdr-linear-light.md), step 3)), and a last pass (`hdr.wgsl`) decodes it to linear light at the **Paper White** and lifts light sources (blocks that emit light, lava, the sun and the moon) towards the **Peak Brightness**; everything else stays as in SDR. The mesher puts the light a block's face emits in its vertex colour's alpha (`EMISSION_BITS`), the frame's alpha carries the mark, and only pixels that still show the world before the hand and HUD are lifted. On Vulkan, `horizon-hdr` sends HDR metadata (MaxCLL, mastering luminance), which wgpu doesn't: without it a compositor may clip everything above SDR white. Paper White and Peak Brightness are **Auto** by default: they follow what the system reports for the display. On Wayland that's the compositor's preferred image description of the window (`wp_color_management_v1`, in `horizon-hdr`'s `wayland.rs`), asked and then watched while HDR output is on (with HDR off the game doesn't ask), so a move to another output or a recalibrated desktop applies at once. KWin maps the content's reference white (203 nits) onto the desktop's SDR white, so Auto puts Paper White at 203 and Peak Brightness at the display's peak over that ratio; Hyprland (0.57) doesn't: it reports the standard 203 as reference white, so Auto puts Paper White at 203, and it shows HDR windows below its own SDR white (`sdr_max_luminance`, which it doesn't report): in a test, a white browser page was brighter than both the game's white and a 1000-nit patch in mpv. Other compositors are unchecked. On Windows Auto takes what wgpu reads from the OS (`Surface::display_hdr_info`: DXGI's `MaxLuminance` and the SDR white level of Windows' SDR content brightness slider, both absolute nits), read again every 2 seconds since no event says they changed; these often come from the display's EDID and may run optimistic, and this is untested so far. **Calibrate...** (`hdr_calibration.rs`) sets Black Level, Peak Brightness and Paper White full screen, one test picture at a time, drawn by the last pass straight in nits above a band of controls: Horizon's logo at the slider on black, lowered until it disappears; the logo at the slider inside a tenth of the screen at 10 000 nits (small, so the display doesn't dim), raised until it disappears; a white card, set to a white that's comfortable in the light the player plays in. Save writes the three, Cancel or Escape keeps what was set, and a step left untouched keeps Auto. The Peak step needs a path that doesn't tone-map: on KWin the logo never disappears, so where the system reports its values the button reads **Calibrate Anyway...** and says the display is already set up (**Use Automatic Setup** once they were changed by hand). Where nothing is reported, Auto is 203 and 1000 nits, and the HDR page asks the player to calibrate. **Black Level** (0 is Auto: what the system reports, else an OLED's true black) is the display's darkest black, in nits: the last pass lifts the shadows onto it with ITU-R BT.2390's black level lift in PQ, which leaves the peak where it is and fades out by the fourth power above black, and the metadata gives it as the minimum luminance; at 0 the picture is unchanged. **UI Brightness** (100% by default) sets the HUD and menus against paper white: once the world is kept for the last pass, the frame's alpha is zeroed, everything drawn after raises it where it covers the world, Line truncated
**HDR** (off by default) has its own page, HDR, greyed where the display path offers no HDR output (so players without an HDR display don't take its settings for their display's); the switch comes first and the sliders below it only while it is on. It presents to an HDR10 swapchain (`Rgb10a2Unorm`, BT.2020 with the PQ curve) or, where only that is offered, an scRGB one (`Rgba16Float`, extended linear sRGB), on displays and compositors that offer either; `HORIZON_HDR_OUTPUT=pq` or `scrgb` allows only one, and offers it to an `--offscreen` run, which has no display to ask: there HDR is drawn and screenshotted (the `-hdr.png` below) but not presented, and Auto takes 203 and 1000 nits, so HDR can be checked without an HDR display. The frame is drawn exactly as in SDR, in vanilla's gamma space (into a 16-bit target (`Rgba16Unorm`, which clips at white after every pass as the 8-bit one does) when HDR is on as the renderer is made, at the start or a resource reload, with **Smooth Gradients** on (the default, `hdrSmoothGradients`; off, the 8-bit frame) and where the device can draw into it, else an 8-bit one; HDR switched off after that keeps the 16-bit frame and copies it to the 8-bit swapchain, without frame generation, until the next start; with `HORIZON_HDR_FRAME=float` and no shader pack, a float one (`Rgba16Float`) in its place whatever Smooth Gradients is, which keeps light past white: the last pass shows it where the world shows without a light mark (as shaders will write it), up to the peak, and clips it at white on marked pixels (light sources, the sun, HDR lighting, which it lifts as before), under the HUD and menus and with HDR Lighting off ([hdr-linear-light.md](design/hdr-linear-light.md), step 3)), and a last pass (`hdr.wgsl`) decodes it to linear light at the **Paper White** and lifts light sources (blocks that emit light, lava, the sun and the moon) towards the **Peak Brightness**; everything else stays as in SDR. The mesher puts the light a block's face emits in its vertex colour's alpha (`EMISSION_BITS`), the frame's alpha carries the mark, and only pixels that still show the world before the hand and HUD are lifted. On Vulkan, `horizon-hdr` sends HDR metadata (MaxCLL, mastering luminance), which wgpu doesn't: without it a compositor may clip everything above SDR white. Paper White and Peak Brightness are **Auto** by default: they follow what the system reports for the display. On Wayland that's the compositor's preferred image description of the window (`wp_color_management_v1`, in `horizon-hdr`'s `wayland.rs`), asked and then watched while HDR output is on (with HDR off the game doesn't ask), so a move to another output or a recalibrated desktop applies at once. KWin maps the content's reference white (203 nits) onto the desktop's SDR white, so Auto puts Paper White at 203 and Peak Brightness at the display's peak over that ratio; Hyprland (0.57) doesn't: it reports the standard 203 as reference white, so Auto puts Paper White at 203, and it shows HDR windows below its own SDR white (`sdr_max_luminance`, which it doesn't report): in a test, a white browser page was brighter than both the game's white and a 1000-nit patch in mpv. Other compositors are unchecked. On Windows Auto takes what wgpu reads from the OS (`Surface::display_hdr_info`: DXGI's `MaxLuminance` and the SDR white level of Windows' SDR content brightness slider, both absolute nits), read again every 2 seconds since no event says they changed; these often come from the display's EDID and may run optimistic, and this is untested so far. **Calibrate...** (`hdr_calibration.rs`) sets Black Level, Peak Brightness and Paper White full screen, one test picture at a time, drawn by the last pass straight in nits above a band of controls: Horizon's logo at the slider on black, lowered until it disappears; the logo at the slider inside a tenth of the screen at 10 000 nits (small, so the display doesn't dim), raised until it disappears; a white card, set to a white that's comfortable in the light the player plays in. Save writes the three, Cancel or Escape keeps what was set, and a step left untouched keeps Auto. The Peak step needs a path that doesn't tone-map: on KWin the logo never disappears, so where the system reports its values the button reads **Calibrate Anyway...** and says the display is already set up (**Use Automatic Setup** once they were changed by hand). Where nothing is reported, Auto is 203 and 1000 nits, and the HDR page asks the player to calibrate. **Black Level** (0 is Auto: what the system reports, else an OLED's true black) is the display's darkest black, in nits: the last pass lifts the shadows onto it with ITU-R BT.2390's black level lift in PQ, which leaves the peak where it is and fades out by the fourth power above black, and the metadata gives it as the minimum luminance; at 0 the picture is unchanged. **UI Brightness** (100% by default) sets the HUD and menus against paper white: once the world is kept for the Line truncated
HDR also lights the world past paper white: surfaces at block or sky light levels 11 to 15 rise above it, up to 4 times for the strongest light, sky light by how a face meets the sun, easing into the peak from 60% of it so bright textures keep their detail. The frame's alpha between 0.76 and 1 carries it to the last pass, where it amounts to the lightmap going past 1. Darker light stays as in SDR. It's off while a shader pack's post or material stage runs, and `HORIZON_HDR_LIGHTING=0` turns it off to compare.