Merge pull request #3203 from senjinthedragon/hdr-screen

HDR: its settings get a screen of their own
This commit is contained in:
levzzz authored and GitHub committed 2026-10-29 22:38:18 +05:00
commit d009ebd140
9 files changed
+151 -36

No files matched your search

+1
View File
@@ -1182,6 +1182,7 @@ impl Renderer {
}
surface.configure(&device, &config);
let hdr_output = hdr::output(&caps);
crate::graphics::offer_hdr(hdr_output.is_some());
if previous.is_none() {
log::info!(
"Horizon renderer: HDR output {}",
@@ -285,6 +285,16 @@ pub fn reduced_far_detail_suggested() -> bool {
REDUCED_SUGGESTED.load(std::sync::atomic::Ordering::Relaxed)
}
static HDR_OFFERED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Whether the display path offers HDR output (HDR10 or scRGB): set when the renderer
/// opens its surface; the settings open HDR's screen only then.
pub fn offer_hdr(on: bool) {
HDR_OFFERED.store(on, std::sync::atomic::Ordering::Relaxed);
}
pub fn hdr_offered() -> bool {
HDR_OFFERED.load(std::sync::atomic::Ordering::Relaxed)
}
/// The farthest LOD Distance, in chunks, on integrated GPUs other than
/// Apple's: they share the CPU's memory bandwidth, and past it far terrain
/// costs them more frame time than it is worth.
@@ -130,8 +130,9 @@ impl Style {
label: &Text,
value: f32,
hover: bool,
active: bool,
) {
ui::slider(batch, atlas, x, y, w, label, value, hover, true);
ui::slider(batch, atlas, x, y, w, label, value, hover && active, active);
}
/// The cost tag and the description beside a control, wrapped to two lines.
pub fn description(
+123 -29
View File
@@ -1,4 +1,4 @@
//! Horizon's graphics settings screens, Simple and Advanced: which settings
//! Horizon's graphics settings screens, Simple, Advanced and HDR: which settings
//! they show, in what groups, with what description and cost, and their
//! input. How they look comes from `graphics_style`.
use crate::{
@@ -14,6 +14,8 @@ use crate::{
pub enum Level {
Simple,
Advanced,
/// HDR output's own settings, opened from Advanced while HDR is on.
Hdr,
}
/// Horizon's own settings (`graphics::Graphics`).
@@ -38,6 +40,8 @@ pub enum H {
Antialiasing,
Upscaling,
Hdr,
/// Opens `Level::Hdr`.
HdrSettings,
HdrBlackLevel,
HdrPeak,
HdrPaperWhite,
@@ -122,6 +126,34 @@ pub fn rows(level: Level) -> Vec<Row> {
);
let mut rows = match level {
Level::Simple => vec![preset, render, lod, far, fov, fps, vsync, gui],
Level::Hdr => vec![
Row::Header("HDR"),
h(
H::Hdr,
None,
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.",
),
h(
H::HdrBlackLevel,
None,
"Your display's darkest black. 0 for OLED; LCDs glow a little.",
),
h(
H::HdrPeak,
None,
"How bright the brightest highlights get. Your display's peak.",
),
h(
H::HdrPaperWhite,
None,
"How bright white paper and menus look. Match your desktop.",
),
h(
H::HdrUiBrightness,
None,
"How bright the HUD and menus are, against paper white.",
),
],
Level::Advanced => vec![
Row::Header("Rendering"),
render,
@@ -232,30 +264,10 @@ pub fn rows(level: Level) -> Vec<Row> {
"Renders the world smaller and rebuilds it with TAA. Faster, softer.",
),
h(
H::Hdr,
H::HdrSettings,
None,
"HDR output on an HDR display: lava, fire and sunlight shine brighter.",
),
h(
H::HdrBlackLevel,
None,
"HDR: your display's darkest black. 0 for OLED; LCDs glow a little.",
),
h(
H::HdrPeak,
None,
"HDR: how bright the brightest highlights get. Your display's peak.",
),
h(
H::HdrPaperWhite,
None,
"HDR: how bright white paper and menus look. Match your desktop.",
),
h(
H::HdrUiBrightness,
None,
"HDR: how bright the HUD and menus are, against paper white.",
),
h(
H::PresentMode,
None,
@@ -417,6 +429,7 @@ impl H {
),
H::Upscaling => labelled("Upscaling", g.upscaling.label()),
H::Hdr => labelled("HDR", on_off(g.hdr)),
H::HdrSettings => Text::literal("HDR Settings..."),
H::HdrPaperWhite => labelled("Paper White", format!("{} nits", g.hdr_paper_white)),
H::HdrPeak => labelled("Peak Brightness", format!("{} nits", g.hdr_peak)),
H::HdrBlackLevel => labelled(
@@ -525,6 +538,25 @@ impl Item {
fn available(self) -> bool {
!matches!(self, Item::Vanilla(o) if !o.available())
}
/// Why a control can't be used now, when it can't: HDR's screen opens only where
/// the display offers HDR output, whatever `horizon-graphics.txt` says (a copied game
/// folder, a display swapped for an SDR one: the renderer leaves HDR off there, and
/// the saved values wait for an HDR display), and its sliders do something only
/// while it is on.
fn blocked(self, s: &Settings) -> Option<&'static str> {
if !self.available() {
return Some("This feature is not yet available in Horizon.");
}
match self {
Item::Horizon(H::HdrSettings | H::Hdr) if !crate::graphics::hdr_offered() => {
Some("Your display or system doesn't offer HDR output.")
}
Item::Horizon(
H::HdrBlackLevel | H::HdrPeak | H::HdrPaperWhite | H::HdrUiBrightness,
) if !s.horizon.hdr => Some("Turn HDR on to use it."),
_ => None,
}
}
/// On/off settings and their value.
pub fn switch(self, s: &Settings) -> Option<bool> {
let g = &s.horizon;
@@ -626,6 +658,7 @@ pub enum Action {
SliderDrag,
OpenAdvanced,
OpenSimple,
OpenHdr,
Done,
}
@@ -709,6 +742,17 @@ impl GraphicsUi {
let other = match level {
Level::Simple => Action::OpenAdvanced,
Level::Advanced => Action::OpenSimple,
// One Done, back to Advanced.
Level::Hdr => {
widgets.push(Widget {
x: cx - 100.0,
y: by,
w: 200.0,
target: Target::Button(Action::Done),
list: false,
});
return (widgets, headers, content);
}
};
widgets.push(Widget {
x: cx - 155.0,
@@ -770,12 +814,10 @@ impl GraphicsUi {
continue;
}
let label = item.label(s);
let live = item.available();
if hover && !live {
tooltip = Some(atlas.font.split(
&Text::literal("This feature is not yet available in Horizon."),
170.0,
));
let blocked = item.blocked(s);
let live = blocked.is_none();
if hover && let Some(why) = blocked {
tooltip = Some(atlas.font.split(&Text::literal(why), 170.0));
}
match item.slider(s) {
Some(value) => STYLE.slider(
@@ -785,6 +827,7 @@ impl GraphicsUi {
&label,
value,
hover || self.dragging == Some(item),
live,
),
None => STYLE.button(
batch,
@@ -834,6 +877,7 @@ impl GraphicsUi {
let title = match level {
Level::Simple => "Graphics",
Level::Advanced => "Graphics: Advanced",
Level::Hdr => "Graphics: HDR",
};
STYLE.title(batch, atlas, &Text::literal(title));
if let Some(lines) = tooltip {
@@ -889,7 +933,10 @@ impl GraphicsUi {
return match wd.target {
Target::Button(action) => vec![Action::ButtonClick, action],
// A disabled control does nothing: no sound, no preset change.
Target::Item(item) if !item.available() => Vec::new(),
Target::Item(item) if item.blocked(s).is_some() => Vec::new(),
Target::Item(Item::Horizon(H::HdrSettings)) => {
vec![Action::ButtonClick, Action::OpenHdr]
}
Target::Item(item) => {
if item.slider(s).is_some() {
self.dragging = Some(item);
@@ -921,6 +968,53 @@ impl GraphicsUi {
mod tests {
use super::*;
#[test]
fn hdr_settings_open_where_hdr_is_offered() {
let mut s = Settings::default();
let mut ui = GraphicsUi::default();
let size = (480.0, 2000.0);
let at = |ui: &mut GraphicsUi, s: &Settings, level, item| {
let (widgets, _, _) = ui.layout(level, s, size.0, size.1);
let w = widgets
.iter()
.find(|w| matches!(w.target, Target::Item(i) if i == item))
.unwrap();
(w.x + 1.0, w.y + 1.0)
};
let button = at(&mut ui, &s, Level::Advanced, Item::Horizon(H::HdrSettings));
let click = |ui: &mut GraphicsUi, s: &mut Settings, level, point| {
ui.click(level, s, point, size, [480, 2000], false, false)
};
crate::graphics::offer_hdr(false);
assert!(click(&mut ui, &mut s, Level::Advanced, button).is_empty());
// Not even with HDR saved on, from another computer.
s.horizon.hdr = true;
assert!(click(&mut ui, &mut s, Level::Advanced, button).is_empty());
s.horizon.hdr = false;
crate::graphics::offer_hdr(true);
assert_eq!(
click(&mut ui, &mut s, Level::Advanced, button),
[Action::ButtonClick, Action::OpenHdr]
);
// On HDR's screen, the sliders wait for the switch above them.
let peak = at(&mut ui, &s, Level::Hdr, Item::Horizon(H::HdrPeak));
assert!(click(&mut ui, &mut s, Level::Hdr, peak).is_empty());
s.horizon.hdr = true;
assert_eq!(
click(&mut ui, &mut s, Level::Hdr, peak),
[Action::SliderDrag]
);
let on = |level, item| {
extra_rows(level, &s)
.iter()
.any(|r| matches!(r, Row::Setting(x) if x.item == Item::Horizon(item)))
};
assert!(on(Level::Hdr, H::Hdr) && !on(Level::Advanced, H::Hdr));
assert!(on(Level::Hdr, H::HdrPeak) && !on(Level::Advanced, H::HdrPeak));
for item in [H::HdrBlackLevel, H::HdrPaperWhite, H::HdrUiBrightness] {
assert!(on(Level::Hdr, item) && !on(Level::Advanced, item));
}
}
#[test]
fn graphics_buttons_sound_on_press_and_sliders_on_release() {
let mut s = Settings::default();
@@ -993,6 +993,7 @@ pub enum Page {
/// Horizon's graphics settings (`graphics_ui`).
Graphics,
GraphicsAdvanced,
GraphicsHdr,
/// Horizon's controller settings and button binds.
Controller,
PadBinds,
@@ -1367,7 +1368,10 @@ impl OptionsUi {
if page == Page::Packs {
self.pack_screen = None;
}
if matches!(page, Page::Graphics | Page::GraphicsAdvanced) {
if matches!(
page,
Page::Graphics | Page::GraphicsAdvanced | Page::GraphicsHdr
) {
self.graphics.reset();
}
}
@@ -1376,6 +1380,7 @@ impl OptionsUi {
match page {
Page::Graphics => Some(crate::graphics_ui::Level::Simple),
Page::GraphicsAdvanced => Some(crate::graphics_ui::Level::Advanced),
Page::GraphicsHdr => Some(crate::graphics_ui::Level::Hdr),
_ => None,
}
}
@@ -1397,7 +1402,7 @@ impl OptionsUi {
Page::Mods => "Mods",
Page::NativeMod(_) => "Mod Settings",
Page::ShaderPacks => "Shader Packs",
Page::Graphics | Page::GraphicsAdvanced => "options.videoTitle",
Page::Graphics | Page::GraphicsAdvanced | Page::GraphicsHdr => "options.videoTitle",
Page::Controller => "Controller Settings",
Page::PadBinds => "Controller Binds",
Page::Extras => "Horizon Extras",
@@ -2656,6 +2661,7 @@ impl OptionsUi {
Action::Changed => events.push(Event::Changed),
Action::SliderDrag => events.push(Event::SliderDrag),
Action::OpenAdvanced => self.push(Page::GraphicsAdvanced),
Action::OpenHdr => self.push(Page::GraphicsHdr),
Action::OpenSimple => {
if self.stack.len() >= 2
&& self.stack[self.stack.len() - 2] == Page::Graphics
@@ -146,6 +146,7 @@ const PAGES: Ty = Ty::Enum(&[
"shader-packs",
"graphics",
"graphics-advanced",
"graphics-hdr",
"extras",
"horizon",
"agent",
+2 -1
View File
@@ -34,7 +34,7 @@ const HELP: &[&str] = &[
"chat TEXT send a chat line or /command as if typed",
"type TEXT type into the open chat box (key enter tap sends)",
"chat-log [N] the newest N chat messages (default 10)",
"options [PAGE] open the options (video sound controls keybinds mouse chat accessibility language font packs mods graphics graphics-advanced)",
"options [PAGE] open the options (video sound controls keybinds mouse chat accessibility language font packs mods graphics graphics-advanced graphics-hdr)",
"stats [reset] frame-time summary since the last reset",
"bench SECONDS reset stats, wait, then reply with the summary",
"fly X1 Y1 Z1 X2 Y2 Z2 SECONDS [PITCH [raw]] fly the camera along a line at constant speed, then reply with the frame summary (raw: every frame time too)",
@@ -685,6 +685,7 @@ impl Game<'_> {
"shader-packs" => Page::ShaderPacks,
"graphics" => Page::Graphics,
"graphics-advanced" => Page::GraphicsAdvanced,
"graphics-hdr" => Page::GraphicsHdr,
"extras" => Page::Extras,
"credits" => Page::Credits,
"horizon" => Page::Horizon,
+3 -2
View File
@@ -21,7 +21,8 @@ Every row has a one-line description and its cost in frame time (none, low, medi
| Quality | smooth lighting, mipmap levels, biome blend, clouds, cloud distance, weather radius, particles, entity shadows, cutout leaves | `options.txt` |
| | sky, fog, weather, animated textures | `horizon-graphics.txt` |
| Display | FOV, max framerate, VSync, fullscreen, GUI scale | `options.txt` |
| | resolution scale, anti-aliasing (FXAA or TAA), TAA upscaling, HDR, paper white, peak brightness, present mode, frame pacing | `horizon-graphics.txt` |
| | resolution scale, anti-aliasing (FXAA or TAA), TAA upscaling, present mode, frame pacing | `horizon-graphics.txt` |
| HDR (its own screen, from Display) | HDR, black level, peak brightness, paper white, UI brightness | `horizon-graphics.txt` |
| Performance | battery mode, worker threads, upload budget per frame | `horizon-graphics.txt` |
| Overlay | performance overlay: FPS, frame/CPU/GPU times, frame-time graph | `horizon-graphics.txt` |
@@ -29,7 +30,7 @@ Every row has a one-line description and its cost in frame time (none, low, medi
**Battery mode** (Performance, off by default) is for laptops and handhelds: at most 60 frames per second in a world, 30 in menus without a world or while another window has focus, and half the automatic worker threads (chunks load slower). It changes only when frames are drawn: what they show and the game's 20 ticks per second stay as they are.
**HDR** (Display, off by default) 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. The frame is drawn exactly as in SDR, in vanilla's gamma space, 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. KWin maps the content's reference white onto the desktop's SDR brightness, so on Linux the default Paper White matches SDR mode. **Black Level** (0 by default, for 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, the first-person hand's is then scaled down to 1/255 (it stays at the world's brightness), and the last pass scales the rest by UI Brightness and lifts none of it (so text over the sun stays text). `HORIZON_HDR_DEBUG=1` paints lifted pixels magenta. Not lifted yet: glowing particles, distant LOD terrain.
**HDR** (off by default) lives on its own screen, Graphics: HDR, behind **HDR Settings...** under Display, which is greyed where the display path offers no HDR output (so players without an HDR display don't take its settings for their display's); there 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. The frame is drawn exactly as in SDR, in vanilla's gamma space, 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. KWin maps the content's reference white onto the desktop's SDR brightness, so on Linux the default Paper White matches SDR mode. **Black Level** (0 by default, for 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, the first-person hand's is then scaled down to 1/255 (it stays at the world's brightness), and the last pass scales the rest by UI Brightness and lifts none of it (so text over the sun stays text). `HORIZON_HDR_DEBUG=1` paints lifted pixels magenta. Not lifted yet: glowing particles, distant LOD terrain.
Which settings each screen shows, their descriptions and costs are in `crates/horizon-client-render/src/graphics_ui.rs` (`rows`); `graphics_style.rs` draws them.
+1 -1
View File
@@ -94,7 +94,7 @@ Every command `horizon-ctl` takes, its MCP tool in `horizon-ctl mcp` (the launch
| `respawn` | `respawn` | Respawn from the death screen | none |
| `pause` | `pause` | Open the pause menu | none |
| `resume` | `resume` | Leave the pause menu | none |
| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, shader-packs, graphics, graphics-advanced, extras, horizon, agent): the page (default: the main options) |
| `options [PAGE]` | `options` | Open the options screen, or one of its pages (Horizon's too) | `page` (main, video, sound, controls, keybinds, controller, controller-binds, mouse, chat, accessibility, language, font, packs, mods, shader-packs, graphics, graphics-advanced, graphics-hdr, extras, horizon, agent): the page (default: the main options) |
| `book [next\|back\|take\|done]` | `book` | The open book or lectern: its page text, or press one of its buttons | `action` (next, back, take, done): a button to press |
| `signedit X Y Z [front\|back]` | `signedit` | Open the sign editor of the sign at X Y Z (then type and close) | `x` (integer, required): block X<br>`y` (integer, required): block Y<br>`z` (integer, required): block Z<br>`side` (front, back): which side |
| `mount-menu` | `mount_menu` | Open the inventory of the horse, donkey, mule or llama being ridden | none |