From 44e8cf501b328a18d985b27340c5b791ed96f53a Mon Sep 17 00:00:00 2001 From: Christopher Addison Date: Sun, 18 Oct 2026 11:37:46 -0400 Subject: [PATCH 1/2] gpui_apple: return Metal renderer creation failures instead of panicking Building the Metal renderer compiles the generated MSL with the driver's runtime compiler and creates one pipeline state per primitive. Some drivers refuse a step ("Compiler encountered an internal error" on an Intel MacBook Air), and the renderer panicked with "error building metal library", "error locating " or "could not create render pipeline state". A missing Metal device called std::process::exit(1). MetalRenderer::new, new_headless and MetalHeadlessRenderer::new now return Result<_, MetalRendererError>. The error carries the device name, the step that failed and Metal's own message, and new_renderer passes it to open_window's Result, so an application can report it and exit. gpui_platform re-exports the type on macOS for downcast_ref. The headless renderer factory used by tests keeps panicking, as the WGPU one does. Co-Authored-By: Claude Opus 5.5 --- crates/gpui_apple/src/metal_renderer.rs | 363 +++++++++++++++------- crates/gpui_platform/src/gpui_platform.rs | 19 +- 2 files changed, 261 insertions(+), 121 deletions(-) diff --git a/crates/gpui_apple/src/metal_renderer.rs b/crates/gpui_apple/src/metal_renderer.rs index 8a7ad20da1..986553b138 100644 --- a/crates/gpui_apple/src/metal_renderer.rs +++ b/crates/gpui_apple/src/metal_renderer.rs @@ -170,9 +170,54 @@ pub fn new_renderer( _bounds: gpui::Size, transparent: bool, ) -> Result { - Ok(MetalRenderer::new(context, transparent)) + Ok(MetalRenderer::new(context, transparent)?) } +/// Why a [`MetalRenderer`] could not be created. +/// +/// Building the renderer compiles the generated MSL with the device's runtime +/// compiler and creates a pipeline state per primitive. A driver can refuse +/// either step (for example "Compiler encountered an internal error" on some +/// older GPUs), and a window cannot draw without them. The error says what +/// failed, so an application can report it and exit instead of panicking. +/// `open_window` returns it inside an `anyhow::Error`; use `downcast_ref`. +#[derive(Clone, Debug)] +pub struct MetalRendererError { + /// The `MTLDevice` name of the GPU, or `None` when no Metal device was found. + pub device_name: Option, + /// What the renderer was doing, such as "compiling the quads shader library". + pub stage: String, + /// Metal's own error text. + pub message: String, +} + +impl MetalRendererError { + fn new( + device: Option<&metal::DeviceRef>, + stage: impl Into, + message: impl Into, + ) -> Self { + Self { + device_name: device.map(|device| device.name().to_string()), + stage: stage.into(), + message: message.into(), + } + } +} + +impl std::fmt::Display for MetalRendererError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let device = self.device_name.as_deref().unwrap_or("no Metal device"); + write!( + f, + "failed to create the Metal renderer on {device} while {}: {}", + self.stage, self.message + ) + } +} + +impl std::error::Error for MetalRendererError {} + pub struct InstanceBufferPool { buffer_size: usize, buffers: Vec, @@ -289,8 +334,14 @@ pub struct MetalRenderer { impl MetalRenderer { /// Creates a new MetalRenderer with a CAMetalLayer for window-based rendering. - pub fn new(instance_buffer_pool: Arc>, transparent: bool) -> Self { - let device = Self::create_device(); + /// + /// Fails when no Metal device is available, or when the device cannot build + /// the renderer's shader libraries and pipeline states. + pub fn new( + instance_buffer_pool: Arc>, + transparent: bool, + ) -> Result { + let device = Self::create_device()?; let layer = new_window_layer(transparent); layer.set_device(&device); @@ -313,12 +364,14 @@ impl MetalRenderer { /// This renderer can render scenes to images without requiring a CAMetalLayer, /// window, or AppKit. Use `render_scene_to_image()` to render scenes. #[cfg(any(test, feature = "bench-support", feature = "test-support"))] - pub fn new_headless(instance_buffer_pool: Arc>) -> Self { - let device = Self::create_device(); + pub fn new_headless( + instance_buffer_pool: Arc>, + ) -> Result { + let device = Self::create_device()?; Self::new_internal(device, None, true, instance_buffer_pool) } - fn create_device() -> metal::Device { + fn create_device() -> Result { // Prefer low‐power integrated GPUs on Intel Mac. On Apple // Silicon, there is only ever one GPU, so this is equivalent to // `metal::Device::system_default()`. @@ -326,16 +379,19 @@ impl MetalRenderer { .into_iter() .min_by_key(|d| (d.is_removable(), !d.is_low_power())) { - d + Ok(d) } else { // For some reason `all()` can return an empty list, see https://github.com/zed-industries/zed/issues/37689 // In that case, we fall back to the system default device. log::error!( "Unable to enumerate Metal devices; attempting to use system default device" ); - metal::Device::system_default().unwrap_or_else(|| { - log::error!("unable to access a compatible graphics device"); - std::process::exit(1); + metal::Device::system_default().ok_or_else(|| { + MetalRendererError::new( + None, + "selecting a graphics device", + "unable to access a compatible graphics device", + ) }) } } @@ -345,7 +401,21 @@ impl MetalRenderer { layer: Option, opaque: bool, instance_buffer_pool: Arc>, - ) -> Self { + ) -> Result { + Self::new_internal_with_sources(device, layer, opaque, instance_buffer_pool, |shader| { + shader.msl + }) + } + + /// `new_internal`, with the MSL for each shader chosen by `msl_for`, so a test can + /// hand the device a source it refuses. + fn new_internal_with_sources( + device: metal::Device, + layer: Option, + opaque: bool, + instance_buffer_pool: Arc>, + msl_for: impl Fn(&'static NativeShader) -> &'static str, + ) -> Result { // Shared memory can be used only if CPU and GPU share the same memory space. // https://developer.apple.com/documentation/metal/setting-resource-storage-modes let is_unified_memory = device.has_unified_memory(); @@ -358,136 +428,137 @@ impl MetalRenderer { // Compile the Naga-generated MSL with the device's runtime compiler, deduplicating // per source so each module compiles exactly once. let mut libraries: Vec<(&'static str, metal::Library)> = Vec::new(); - let mut library_for = |source: &'static str| -> metal::Library { - if let Some((_, library)) = libraries - .iter() - .find(|(registered, _)| *registered == source) - { - return library.clone(); - } - let library = device - .new_library_with_source(source, &metal::CompileOptions::new()) - .unwrap_or_else(|error| panic!("error building metal library: {error}")); - libraries.push((source, library.clone())); - library - }; + let mut library_for = + |shader: &'static NativeShader| -> Result { + let source = msl_for(shader); + if let Some((_, library)) = libraries + .iter() + .find(|(registered, _)| *registered == source) + { + return Ok(library.clone()); + } + let library = compile_library(&device, shader.label, source)?; + libraries.push((source, library.clone())); + Ok(library) + }; - let mut pipeline = |label: &str| -> (&'static NativeShader, metal::Library) { - let shader = native_shader(label); - (shader, library_for(shader.msl)) - }; + let mut pipeline = + |label: &str| -> Result<(&'static NativeShader, metal::Library), MetalRendererError> { + let shader = native_shader(label); + Ok((shader, library_for(shader)?)) + }; let (path_rasterization_shader, path_rasterization_library) = - pipeline("path_rasterization"); + pipeline("path_rasterization")?; let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state( &device, &path_rasterization_library, path_rasterization_shader, MTLPixelFormat::BGRA8Unorm, PATH_SAMPLE_COUNT, - ); - let (paths_shader, paths_library) = pipeline("paths"); + )?; + let (paths_shader, paths_library) = pipeline("paths")?; let path_sprites_pipeline_state = build_path_sprite_pipeline_state( &device, &paths_library, paths_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (shadows_shader, shadows_library) = pipeline("shadows"); + )?; + let (shadows_shader, shadows_library) = pipeline("shadows")?; let shadows_pipeline_state = build_pipeline_state( &device, &shadows_library, shadows_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (smoothed_shadows_shader, smoothed_shadows_library) = pipeline("smoothed_shadows"); + )?; + let (smoothed_shadows_shader, smoothed_shadows_library) = pipeline("smoothed_shadows")?; let smoothed_shadows_pipeline_state = build_pipeline_state( &device, &smoothed_shadows_library, smoothed_shadows_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (quads_shader, quads_library) = pipeline("quads"); + )?; + let (quads_shader, quads_library) = pipeline("quads")?; let quads_pipeline_state = build_pipeline_state( &device, &quads_library, quads_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (smoothed_quads_shader, smoothed_quads_library) = pipeline("smoothed_quads"); + )?; + let (smoothed_quads_shader, smoothed_quads_library) = pipeline("smoothed_quads")?; let smoothed_quads_pipeline_state = build_pipeline_state( &device, &smoothed_quads_library, smoothed_quads_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (underlines_shader, underlines_library) = pipeline("underlines"); + )?; + let (underlines_shader, underlines_library) = pipeline("underlines")?; let underlines_pipeline_state = build_pipeline_state( &device, &underlines_library, underlines_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (monochrome_shader, monochrome_library) = pipeline("monochrome_sprites"); + )?; + let (monochrome_shader, monochrome_library) = pipeline("monochrome_sprites")?; let monochrome_sprites_pipeline_state = build_pipeline_state( &device, &monochrome_library, monochrome_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (polychrome_shader, polychrome_library) = pipeline("polychrome_sprites"); + )?; + let (polychrome_shader, polychrome_library) = pipeline("polychrome_sprites")?; let polychrome_sprites_pipeline_state = build_pipeline_state( &device, &polychrome_library, polychrome_shader, MTLPixelFormat::BGRA8Unorm, - ); + )?; let (smoothed_polychrome_shader, smoothed_polychrome_library) = - pipeline("smoothed_polychrome_sprites"); + pipeline("smoothed_polychrome_sprites")?; let smoothed_polychrome_sprites_pipeline_state = build_pipeline_state( &device, &smoothed_polychrome_library, smoothed_polychrome_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (surfaces_shader, surfaces_library) = pipeline("surfaces"); + )?; + let (surfaces_shader, surfaces_library) = pipeline("surfaces")?; let surfaces_pipeline_state = build_path_sprite_pipeline_state( &device, &surfaces_library, surfaces_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (blur_downsample_shader, blur_downsample_library) = pipeline("blur_downsample"); + )?; + let (blur_downsample_shader, blur_downsample_library) = pipeline("blur_downsample")?; let blur_downsample_pipeline_state = build_blur_pipeline_state( &device, &blur_downsample_library, blur_downsample_shader, MTLPixelFormat::BGRA8Unorm, - ); - let (blur_shader, blur_library) = pipeline("blur"); + )?; + let (blur_shader, blur_library) = pipeline("blur")?; let blur_pipeline_state = build_blur_pipeline_state( &device, &blur_library, blur_shader, MTLPixelFormat::BGRA8Unorm, - ); + )?; // Premultiplied blend (One / OneMinusSourceAlpha) — the composite outputs a premultiplied // blurred sample; straight-alpha blending would darken the faded edges. - let (blur_composite_shader, blur_composite_library) = pipeline("blur_composite"); + let (blur_composite_shader, blur_composite_library) = pipeline("blur_composite")?; let blur_composite_pipeline_state = build_path_sprite_pipeline_state( &device, &blur_composite_library, blur_composite_shader, MTLPixelFormat::BGRA8Unorm, - ); + )?; let (smoothed_blur_composite_shader, smoothed_blur_composite_library) = - pipeline("smoothed_blur_composite"); + pipeline("smoothed_blur_composite")?; let smoothed_blur_composite_pipeline_state = build_path_sprite_pipeline_state( &device, &smoothed_blur_composite_library, smoothed_blur_composite_shader, MTLPixelFormat::BGRA8Unorm, - ); + )?; let sampler_descriptor = SamplerDescriptor::new(); sampler_descriptor.set_min_filter(metal::MTLSamplerMinMagFilter::Linear); @@ -496,10 +567,16 @@ impl MetalRenderer { let command_queue = device.new_command_queue(); let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), is_apple_gpu)); - let core_video_texture_cache = - CVMetalTextureCache::new(None, device.clone(), None).unwrap(); + let core_video_texture_cache = CVMetalTextureCache::new(None, device.clone(), None) + .map_err(|status| { + MetalRendererError::new( + Some(&device), + "creating the CoreVideo texture cache", + format!("CVReturn {status}"), + ) + })?; - Self { + Ok(Self { device, layer, presents_with_transaction: false, @@ -537,7 +614,7 @@ impl MetalRenderer { path_sample_count: PATH_SAMPLE_COUNT, #[cfg(any(test, feature = "bench-support", feature = "test-support"))] headless_render_target: None, - } + }) } pub fn layer(&self) -> Option<&metal::MetalLayerRef> { @@ -1993,18 +2070,57 @@ fn new_command_encoder_for_texture<'a>( command_encoder } +/// Compiles one generated MSL module with the device's runtime compiler. +fn compile_library( + device: &metal::DeviceRef, + label: &str, + source: &str, +) -> Result { + device + .new_library_with_source(source, &metal::CompileOptions::new()) + .map_err(|error| { + MetalRendererError::new( + Some(device), + format!("compiling the {label} shader library"), + error, + ) + }) +} + +fn locate_function( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + entry: &str, +) -> Result { + library + .get_function(entry, None) + .map_err(|error| MetalRendererError::new(Some(device), format!("locating {entry}"), error)) +} + +fn new_pipeline_state( + device: &metal::DeviceRef, + shader: &NativeShader, + descriptor: &metal::RenderPipelineDescriptorRef, +) -> Result { + device + .new_render_pipeline_state(descriptor) + .map_err(|error| { + MetalRendererError::new( + Some(device), + format!("creating the {} pipeline state", shader.label), + error, + ) + }) +} + fn build_pipeline_state( device: &metal::DeviceRef, library: &metal::LibraryRef, shader: &NativeShader, pixel_format: metal::MTLPixelFormat, -) -> metal::RenderPipelineState { - let vertex_fn = library - .get_function(shader.vertex_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.vertex_entry)); - let fragment_fn = library - .get_function(shader.fragment_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.fragment_entry)); +) -> Result { + let vertex_fn = locate_function(device, library, shader.vertex_entry)?; + let fragment_fn = locate_function(device, library, shader.fragment_entry)?; let descriptor = metal::RenderPipelineDescriptor::new(); descriptor.set_label(shader.label); @@ -2020,9 +2136,7 @@ fn build_pipeline_state( color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") + new_pipeline_state(device, shader, &descriptor) } fn build_path_sprite_pipeline_state( @@ -2030,13 +2144,9 @@ fn build_path_sprite_pipeline_state( library: &metal::LibraryRef, shader: &NativeShader, pixel_format: metal::MTLPixelFormat, -) -> metal::RenderPipelineState { - let vertex_fn = library - .get_function(shader.vertex_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.vertex_entry)); - let fragment_fn = library - .get_function(shader.fragment_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.fragment_entry)); +) -> Result { + let vertex_fn = locate_function(device, library, shader.vertex_entry)?; + let fragment_fn = locate_function(device, library, shader.fragment_entry)?; let descriptor = metal::RenderPipelineDescriptor::new(); descriptor.set_label(shader.label); @@ -2052,9 +2162,7 @@ fn build_path_sprite_pipeline_state( color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") + new_pipeline_state(device, shader, &descriptor) } fn build_path_rasterization_pipeline_state( @@ -2063,13 +2171,9 @@ fn build_path_rasterization_pipeline_state( shader: &NativeShader, pixel_format: metal::MTLPixelFormat, path_sample_count: u32, -) -> metal::RenderPipelineState { - let vertex_fn = library - .get_function(shader.vertex_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.vertex_entry)); - let fragment_fn = library - .get_function(shader.fragment_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.fragment_entry)); +) -> Result { + let vertex_fn = locate_function(device, library, shader.vertex_entry)?; + let fragment_fn = locate_function(device, library, shader.fragment_entry)?; let descriptor = metal::RenderPipelineDescriptor::new(); descriptor.set_label(shader.label); @@ -2089,9 +2193,7 @@ fn build_path_rasterization_pipeline_state( color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::OneMinusSourceAlpha); - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") + new_pipeline_state(device, shader, &descriptor) } // Blur downsample/gaussian passes overwrite their target (no blending). The composite pass @@ -2101,13 +2203,9 @@ fn build_blur_pipeline_state( library: &metal::LibraryRef, shader: &NativeShader, pixel_format: metal::MTLPixelFormat, -) -> metal::RenderPipelineState { - let vertex_fn = library - .get_function(shader.vertex_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.vertex_entry)); - let fragment_fn = library - .get_function(shader.fragment_entry, None) - .unwrap_or_else(|error| panic!("error locating {}: {error}", shader.fragment_entry)); +) -> Result { + let vertex_fn = locate_function(device, library, shader.vertex_entry)?; + let fragment_fn = locate_function(device, library, shader.fragment_entry)?; let descriptor = metal::RenderPipelineDescriptor::new(); descriptor.set_label(shader.label); @@ -2117,9 +2215,7 @@ fn build_blur_pipeline_state( color_attachment.set_pixel_format(pixel_format); color_attachment.set_blending_enabled(false); - device - .new_render_pipeline_state(&descriptor) - .expect("could not create render pipeline state") + new_pipeline_state(device, shader, &descriptor) } fn required_instance_buffer_size(scene: &Scene) -> usize { @@ -2178,10 +2274,10 @@ pub struct MetalHeadlessRenderer { #[cfg(any(test, feature = "bench-support", feature = "test-support"))] impl MetalHeadlessRenderer { - pub fn new() -> Self { + pub fn new() -> Result { let instance_buffer_pool = Arc::new(Mutex::new(InstanceBufferPool::default())); - let renderer = MetalRenderer::new_headless(instance_buffer_pool); - Self { renderer } + let renderer = MetalRenderer::new_headless(instance_buffer_pool)?; + Ok(Self { renderer }) } } @@ -2215,10 +2311,49 @@ mod tests { }; use std::borrow::Cow; + #[test] + fn a_shader_library_the_device_refuses_is_an_error() { + let device = MetalRenderer::create_device().unwrap(); + let device_name = device.name().to_string(); + let pool = Arc::new(Mutex::new(InstanceBufferPool::default())); + + let result = MetalRenderer::new_internal_with_sources(device, None, true, pool, |shader| { + if shader.label == "quads" { + "this is not Metal Shading Language" + } else { + shader.msl + } + }); + + let Err(error) = result else { + panic!("a shader library the compiler rejects still built a renderer"); + }; + assert_eq!(error.device_name.as_deref(), Some(device_name.as_str())); + assert_eq!(error.stage, "compiling the quads shader library"); + assert!( + !error.message.is_empty(), + "the compiler's error text is kept" + ); + assert!(error.to_string().contains(&device_name)); + } + + #[test] + fn a_missing_shader_entry_point_is_an_error() { + let device = MetalRenderer::create_device().unwrap(); + let shader = native_shader("quads"); + let library = compile_library(&device, shader.label, shader.msl).unwrap(); + + let Err(error) = locate_function(&device, &library, "no_such_entry_point") else { + panic!("a missing entry point was found"); + }; + assert_eq!(error.stage, "locating no_such_entry_point"); + assert_eq!(error.device_name.as_deref(), Some(device.name())); + } + #[test] fn intermediate_textures_follow_scene_requirements() { let pool = Arc::new(Mutex::new(InstanceBufferPool::default())); - let mut renderer = MetalRenderer::new_headless(pool); + let mut renderer = MetalRenderer::new_headless(pool).unwrap(); let target_size = size(DevicePixels(5), DevicePixels(5)); let mut empty_scene = Scene::default(); empty_scene.finish(); @@ -2257,7 +2392,7 @@ mod tests { #[test] fn generated_sprite_shader_preserves_monochrome_coverage() { let pool = Arc::new(Mutex::new(InstanceBufferPool::default())); - let mut renderer = MetalRenderer::new_headless(pool); + let mut renderer = MetalRenderer::new_headless(pool).unwrap(); let tile_size = Size { width: DevicePixels(8), height: DevicePixels(8), @@ -2365,7 +2500,7 @@ mod tests { #[test] fn generated_quad_shaders_render_background_contracts() { - let mut renderer = MetalHeadlessRenderer::new(); + let mut renderer = MetalHeadlessRenderer::new().unwrap(); let bounds = |x, y| Bounds { origin: gpui::point(ScaledPixels(x), ScaledPixels(y)), size: Size { @@ -2489,7 +2624,7 @@ mod tests { #[test] fn generated_underline_shaders_render_line_contracts() { - let mut renderer = MetalHeadlessRenderer::new(); + let mut renderer = MetalHeadlessRenderer::new().unwrap(); let underline_bounds = Bounds { origin: gpui::point(ScaledPixels(1.5), ScaledPixels(3.5)), size: Size { @@ -2592,7 +2727,7 @@ mod tests { #[test] fn generated_border_shader_renders_dashed_rounded_rect() { - let mut renderer = MetalHeadlessRenderer::new(); + let mut renderer = MetalHeadlessRenderer::new().unwrap(); let full = Bounds { origin: gpui::point(ScaledPixels(0.0), ScaledPixels(0.0)), size: Size { @@ -2692,7 +2827,7 @@ mod tests { #[test] fn generated_shadow_shader_renders_soft_falloff() { - let mut renderer = MetalHeadlessRenderer::new(); + let mut renderer = MetalHeadlessRenderer::new().unwrap(); let full = Bounds { origin: gpui::point(ScaledPixels(0.0), ScaledPixels(0.0)), size: Size { @@ -2755,7 +2890,7 @@ mod tests { #[test] fn generated_blur_shader_softens_backdrop() { - let mut renderer = MetalHeadlessRenderer::new(); + let mut renderer = MetalHeadlessRenderer::new().unwrap(); let full = Bounds { origin: gpui::point(ScaledPixels(0.0), ScaledPixels(0.0)), size: Size { @@ -2827,7 +2962,7 @@ mod tests { #[test] fn generated_sprite_shader_blends_opacity() { - let mut renderer = MetalHeadlessRenderer::new(); + let mut renderer = MetalHeadlessRenderer::new().unwrap(); let key = AtlasKey::Image(RenderImageParams { image_id: ImageId(0), frame_index: 0, @@ -2935,7 +3070,7 @@ mod tests { #[test] fn generated_path_shader_fills_and_antialiases() { - let mut renderer = MetalHeadlessRenderer::new(); + let mut renderer = MetalHeadlessRenderer::new().unwrap(); let mut path = Path::new(gpui::point(px(1.0), px(1.0))); path.line_to(gpui::point(px(7.0), px(1.0))); path.line_to(gpui::point(px(4.0), px(7.0))); diff --git a/crates/gpui_platform/src/gpui_platform.rs b/crates/gpui_platform/src/gpui_platform.rs index 5cd971426b..d29fe67976 100644 --- a/crates/gpui_platform/src/gpui_platform.rs +++ b/crates/gpui_platform/src/gpui_platform.rs @@ -3,6 +3,11 @@ pub use gpui::Platform; +/// What `open_window` fails with on macOS when the device cannot build the Metal +/// renderer. Find it with `anyhow::Error::downcast_ref`. +#[cfg(target_os = "macos")] +pub use gpui_apple::metal_renderer::MetalRendererError; + use std::rc::Rc; /// Returns a background executor for the current platform. @@ -82,17 +87,17 @@ pub fn current_platform(headless: bool) -> Rc { /// Returns a new [`HeadlessRenderer`] for the current platform, if available. /// -/// macOS renders with Metal. Linux, FreeBSD and Windows render with WGPU under the -/// `test-support` feature, and panic when no GPU adapter is found; a software one, -/// such as Mesa's lavapipe or WARP, is enough. +/// macOS renders with Metal, and panics when the device cannot build the renderer. +/// Linux, FreeBSD and Windows render with WGPU under the `test-support` feature, and +/// panic when no GPU adapter is found; a software one, such as Mesa's lavapipe or +/// WARP, is enough. #[cfg(any(feature = "bench-support", feature = "test-support"))] pub fn current_headless_renderer() -> Option> { #[cfg(target_os = "macos")] { - Some( - Box::new(gpui_apple::metal_renderer::MetalHeadlessRenderer::new()) - as Box, - ) + let renderer = gpui_apple::metal_renderer::MetalHeadlessRenderer::new() + .unwrap_or_else(|error| panic!("no headless Metal renderer: {error}")); + Some(Box::new(renderer) as Box) } #[cfg(all( From 46b0c3340355a51f4499f4275b2fa698d0815f5b Mon Sep 17 00:00:00 2001 From: Christopher Addison Date: Sun, 18 Oct 2026 11:50:43 -0400 Subject: [PATCH 2/2] gpui: let the renderer audit accept the optional Windows wgpu feature windows_default_renderer_has_no_wgpu_path failed on unity/hub-consolidated itself: it required crates/gpui_windows/Cargo.toml to never mention gpui_wgpu, and window.rs to hold no wgpu path, but the branch carries an optional, non-default `wgpu` feature (the per-window WGPU renderer). The audit now allows gpui_wgpu only as an optional dependency that the `wgpu` feature enables, fails when `default` or any other feature turns it on, and checks that a build without the feature creates the DirectX renderer. Mutation-checked: adding `wgpu` to `default`, or dropping `optional = true`, fails it. Co-Authored-By: Claude Opus 5.5 --- crates/gpui/tests/renderer_source_audit.rs | 103 ++++++++++++++++++--- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/crates/gpui/tests/renderer_source_audit.rs b/crates/gpui/tests/renderer_source_audit.rs index 8369783c7c..e7f40d013a 100644 --- a/crates/gpui/tests/renderer_source_audit.rs +++ b/crates/gpui/tests/renderer_source_audit.rs @@ -215,23 +215,102 @@ fn windows_default_renderer_has_no_wgpu_path() { let manifest_path = root.join("crates/gpui_windows/Cargo.toml"); let manifest = fs::read_to_string(&manifest_path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", manifest_path.display())); - assert!( - !manifest.contains("gpui_wgpu") && !manifest.contains("wgpu.workspace"), - "{} must not pull WGPU into the native Windows renderer", - manifest_path.display() - ); + // WGPU may only be an optional dependency that the non-default `wgpu` feature + // turns on; a default build of the Windows platform never compiles it. + let mut in_dependencies = false; + for line in manifest.lines().map(str::trim) { + if line.starts_with('[') { + in_dependencies = line.ends_with("dependencies]"); + continue; + } + let declares_dependency = in_dependencies + && [ + "gpui_wgpu ", + "gpui_wgpu=", + "gpui_wgpu.", + "wgpu ", + "wgpu=", + "wgpu.", + ] + .iter() + .any(|prefix| line.starts_with(prefix)); + assert!( + !declares_dependency || line.contains("optional = true"), + "{} must declare WGPU only as an optional dependency: {line}", + manifest_path.display() + ); + } + for (name, enables) in manifest_features(&manifest) { + if name == "wgpu" { + continue; + } + assert!( + !enables.contains("dep:gpui_wgpu") + && !enables.contains("\"gpui_wgpu/") + && !enables.contains("\"wgpu\""), + "{}: feature `{name}` must not enable WGPU (only `wgpu` may, and only \ + `gpui_wgpu?/` weak references are allowed elsewhere): {enables}", + manifest_path.display() + ); + if name == "default" { + assert!( + !enables.contains("wgpu"), + "{}: the default features must not mention WGPU: {enables}", + manifest_path.display() + ); + } + } + // Without the feature, every window draws with DirectX. let window_path = root.join("crates/gpui_windows/src/window.rs"); let window = fs::read_to_string(&window_path) .unwrap_or_else(|error| panic!("failed to read {}: {error}", window_path.display())); + let directx_without_wgpu = window.lines().map(str::trim).collect::>(); assert!( - window.contains("RefCell"), - "{} must use the native DirectX renderer", - window_path.display() - ); - assert!( - !window.contains("feature = \"wgpu\"") && !window.contains("WgpuRenderer"), - "{} must not contain a dormant WGPU renderer path", + directx_without_wgpu.windows(2).any(|pair| { + pair[0] == "#[cfg(not(feature = \"wgpu\"))]" + && pair[1].starts_with("let renderer = DirectXRenderer::new(") + }), + "{} must create the native DirectX renderer when the `wgpu` feature is off", window_path.display() ); } + +/// The `[features]` table of a manifest as (name, enabled list) pairs, with each +/// list's lines joined. Enough for the audit above without a TOML parser. +fn manifest_features(manifest: &str) -> Vec<(String, String)> { + let mut features = Vec::new(); + let mut in_features = false; + let mut current: Option<(String, String)> = None; + for line in manifest.lines().map(str::trim) { + if line.starts_with('[') && !line.starts_with("[\"") { + if let Some(done) = current.take() { + features.push(done); + } + in_features = line == "[features]"; + continue; + } + if !in_features || line.is_empty() || line.starts_with('#') { + continue; + } + if let Some((name, rest)) = line.split_once('=') + && current.is_none() + { + current = Some((name.trim().to_string(), rest.trim().to_string())); + } else if let Some((_, body)) = current.as_mut() { + body.push(' '); + body.push_str(line); + } + if current + .as_ref() + .is_some_and(|(_, body)| body.trim_end().ends_with(']')) + && let Some(done) = current.take() + { + features.push(done); + } + } + if let Some(done) = current.take() { + features.push(done); + } + features +}