From b8f0cde9004d8067c8b11a048efecc3ad4698871 Mon Sep 17 00:00:00 2001 From: Kacpherek Date: Tue, 27 Oct 2026 21:59:16 +0100 Subject: [PATCH 1/2] Fix the Metal HDR colour space lookup that crashed macOS at start wgpu-hal's Metal backend read the CoreGraphics colour space names (kCGColorSpaceITUR_2100_PQ and others) as if dlsym returned the string, but it returns the address of the global that holds it. Configuring a PQ swapchain passed that address to CGColorSpaceCreateWithName, which faulted with no Rust panic. Read through the pointer. --- Cargo.toml | 6 ++++-- third_party/wgpu-hal/src/metal/surface.rs | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 908badb65..1b8d3d5da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,8 +60,10 @@ large_enum_variant = "allow" # wgpu-hal 30.0.1 with gfx-rs/wgpu#10124: wgpu 30 creates a Vulkan descriptor # pool with no pool sizes while it creates the device, which Vulkan allows only # since 1.3.215, and Intel's Windows driver for 6th-10th gen iGPUs (HD 520/530, -# UHD 620/630) crashes on it with an access violation (#746). Drop this and the -# folder once a wgpu release has the fix. +# UHD 620/630) crashes on it with an access violation (#746). It also fixes the +# Metal backend's lookup of the CoreGraphics HDR colour space names, which handed +# CoreGraphics a symbol's address as the string and crashed HDR output on macOS. +# Drop this and the folder once a wgpu release has both fixes. [patch.crates-io] wgpu-hal = { path = "third_party/wgpu-hal" } diff --git a/third_party/wgpu-hal/src/metal/surface.rs b/third_party/wgpu-hal/src/metal/surface.rs index f8028a4f3..5dc81e1c5 100644 --- a/third_party/wgpu-hal/src/metal/surface.rs +++ b/third_party/wgpu-hal/src/metal/surface.rs @@ -228,9 +228,12 @@ static COLOR_SPACES: LazyLock> = LazyLo lib: &libloading::Library, name: &[u8], ) -> Result<&'static CFString, crate::SurfaceError> { - let sym = unsafe { lib.get(name) } + // The symbols are `CFStringRef` globals, so `dlsym` returns the address of the + // global and the symbol's type is a pointer to the reference. Typing it as + // the reference itself would hand CoreGraphics that address as the string. + let sym = unsafe { lib.get::<*const &'static CFString>(name) } .map_err(|_| crate::SurfaceError::Other("error resolving symbol in CoreGraphics"))?; - Ok(*sym) + Ok(unsafe { **sym }) } let extended_display_p3 = lookup(&lib, b"kCGColorSpaceExtendedDisplayP3\0")?; let itur_bt2100_pq = lookup(&lib, b"kCGColorSpaceITUR_2100_PQ\0")?; From dddcc9972687042a6aa9a3ad79dbf65b3a09c086 Mon Sep 17 00:00:00 2001 From: Kacpherek Date: Tue, 27 Oct 2026 22:02:16 +0100 Subject: [PATCH 2/2] Start with HDR off after a start that died switching it on HDR output now writes horizon-hdr-pending before it is switched on and removes it after the first presented HDR frame. A start that finds the file turns hdr off in horizon-graphics.txt and logs it, so a native crash in HDR output costs one start, not every start. --- crates/horizon-client/src/game/options.rs | 1 + crates/horizon-client/src/gfx.rs | 20 +++++++++++++++++++- crates/horizon-client/src/graphics.rs | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/horizon-client/src/game/options.rs b/crates/horizon-client/src/game/options.rs index 7ac4ae98b..a71494075 100644 --- a/crates/horizon-client/src/game/options.rs +++ b/crates/horizon-client/src/game/options.rs @@ -273,6 +273,7 @@ impl Game<'_> { s.horizon.hdr, s.horizon.hdr_paper_white as f32, s.horizon.hdr_peak as f32, + &Self::options_path().with_file_name(horizon_client::graphics::HDR_MARKER), ); } // Window.updateFullscreenIfChanged: only a changed option asks the diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs index ad1fd0561..69449f67f 100644 --- a/crates/horizon-client/src/gfx.rs +++ b/crates/horizon-client/src/gfx.rs @@ -537,6 +537,9 @@ pub struct Renderer { hdr: Option, /// The HDR output the window's surface can present. hdr_output: Option, + /// The marker file of an HDR output that has not presented a frame yet + /// (`graphics::HDR_MARKER`). + hdr_marker: Option, /// The shader pack's post-processing, and the dimension (legacy id) it was loaded for. pack_post: Option<(i32, Option)>, /// The post passes of one of Horizon's own shader packs; never beside `pack_post`'s. @@ -2642,6 +2645,7 @@ impl Renderer { world_post, hdr: None, hdr_output, + hdr_marker: None, pack_post: None, native: None, pass_timing: pack_timing::PassTiming::logging(), @@ -3200,9 +3204,17 @@ impl Renderer { } /// HDR output (Horizon setting): HDR10 or scRGB at `paper_white` nits, the world's /// highlights expanded up to `peak` nits. Off where the surface can't present it. - pub fn set_hdr(&mut self, on: bool, paper_white: f32, peak: f32) { + pub fn set_hdr(&mut self, on: bool, paper_white: f32, peak: f32, marker: &std::path::Path) { let output = self.hdr_output.filter(|_| on && self.surface.is_some()); if output.is_some() != self.hdr.is_some() { + // If the game dies before HDR output shows a frame, the marker is still there at + // the next start, which then comes up with HDR off. + if output.is_some() { + let _ = std::fs::write(marker, "HDR output was being switched on\n"); + self.hdr_marker = Some(marker.to_owned()); + } else if let Some(marker) = self.hdr_marker.take() { + let _ = std::fs::remove_file(marker); + } let format = self.config.format.remove_srgb_suffix(); self.hdr = output.map(|output| hdr::Hdr::new(&self.device, format, output)); self.configure_surface(); @@ -5229,6 +5241,12 @@ impl Renderer { startup_step(format_args!("presenting the first frame")); } self.queue.present(surface_frame); + if self.hdr.is_some() + && let Some(marker) = self.hdr_marker.take() + { + let _ = std::fs::remove_file(marker); + log::info!("Horizon renderer: HDR output presented its first frame"); + } if reconfigure && let Some(surface) = &self.surface { Self::configure( &self.device, diff --git a/crates/horizon-client/src/graphics.rs b/crates/horizon-client/src/graphics.rs index fbf67f9ad..436c6afbb 100644 --- a/crates/horizon-client/src/graphics.rs +++ b/crates/horizon-client/src/graphics.rs @@ -5,6 +5,9 @@ use std::{fs, path::Path}; pub const FILE: &str = "horizon-graphics.txt"; +/// Written beside it before HDR output is switched on and removed once the first HDR frame is +/// presented. Found at the next start, it means HDR output took the game down, so HDR starts off. +pub const HDR_MARKER: &str = "horizon-hdr-pending"; /// Simple-level presets. `Custom` leaves every option as the player set it. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] @@ -310,9 +313,17 @@ fn bool_text(v: bool) -> &'static str { } impl Graphics { pub fn load(dir: &Path) -> Self { - fs::read_to_string(dir.join(FILE)) + let mut g: Self = fs::read_to_string(dir.join(FILE)) .map(|t| Self::parse(&t)) - .unwrap_or_default() + .unwrap_or_default(); + if fs::remove_file(dir.join(HDR_MARKER)).is_ok() && g.hdr { + g.hdr = false; + g.save(dir); + log::warn!( + "the last start ended before HDR output showed a frame; HDR is off now (graphics option hdr, in {FILE})" + ); + } + g } pub fn parse(text: &str) -> Self { let mut g = Self::default();