From be4c5ef08f1b38b4da91109573180b4eac10ccc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Sat, 10 Oct 2026 00:46:14 -0400 Subject: [PATCH 1/2] metal: make a shader from a precompiled Metal library `ShaderSource::MetalLibrary` takes the bytes of a library compiled ahead of time, as `xcrun metal` writes one, holding `vertexShader` and `fragmentShader`. The Metal backend loads it with `newLibraryWithData:error:` instead of compiling MSL source with `newLibraryWithSource:options:error:`. OpenGL panics on it, as it does on MSL source. A device that hasn't cached a shader compiles its MSL in tens of milliseconds: 21 to 155 ms, about 70 on average, on an iPhone 17 Pro. An app pays that for every shader on its first launch, after a system update, and for each shader an update changes. A library compiled as the app builds skips it, leaving only the pipelines to build. A macroquad game (CHOMP) with 29 materials, which compiles and draws each once on its loading screen: on an iPhone 17 Pro just after installing it, that took 2.50 to 2.56 s from source and 0.57 to 0.59 s from libraries. Its deterministic captures on macOS draw the same pixels either way. Co-Authored-By: Claude Opus 5.5 --- src/graphics.rs | 8 +++++++- src/graphics/metal.rs | 33 +++++++++++++++++++++++---------- src/native/apple/frameworks.rs | 10 ++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/graphics.rs b/src/graphics.rs index dbdfffc65..a29adee92 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -1042,6 +1042,11 @@ impl<'a> UniformsSource<'a> { pub enum ShaderSource<'a> { Glsl { vertex: &'a str, fragment: &'a str }, Msl { program: &'a str }, + /// A Metal library compiled ahead of time, as `xcrun metal` writes + /// one, holding `vertexShader` and `fragmentShader`. Loading one skips + /// compiling MSL at runtime, which can take tens of milliseconds per + /// shader on a device that hasn't cached it yet. + MetalLibrary { data: &'a [u8] }, } #[derive(Debug, Clone, Copy, PartialEq, Hash, Eq)] @@ -1099,7 +1104,8 @@ impl ContextInfo { pub trait RenderingBackend { fn info(&self) -> ContextInfo; - /// For metal context's ShaderSource should contain MSL source string, for GL - glsl. + /// For metal context's ShaderSource should contain MSL source string or a compiled + /// Metal library, for GL - glsl. /// /// If in doubt, _most_ OpenGL contexts support "#version 100" glsl shaders. /// So far miniquad never encountered where it can create a rendering context, diff --git a/src/graphics/metal.rs b/src/graphics/metal.rs index e18ec9f75..0368438ee 100644 --- a/src/graphics/metal.rs +++ b/src/graphics/metal.rs @@ -755,18 +755,31 @@ impl RenderingBackend for MetalContext { _meta: ShaderMeta, ) -> Result { unsafe { - let program = match shader { - ShaderSource::Msl { program } => program, + let mut error: ObjcId = nil; + let library: ObjcId = match shader { + ShaderSource::Msl { program } => { + let shader = apple_util::str_to_nsstring(program); + msg_send![ + self.device, + newLibraryWithSource: shader + options:nil + error: &mut error + ] + } + ShaderSource::MetalLibrary { data } => { + let data = dispatch_data_create( + data.as_ptr() as *const c_void, + data.len(), + nil, + DISPATCH_DATA_DESTRUCTOR_DEFAULT, + ); + let library = + msg_send![self.device, newLibraryWithData: data error: &mut error]; + dispatch_release(data); + library + } _ => panic!("OpenGl source on Metal context"), }; - let shader = apple_util::str_to_nsstring(program); - let mut error: ObjcId = nil; - let library: ObjcId = msg_send![ - self.device, - newLibraryWithSource: shader - options:nil - error: &mut error - ]; if library.is_null() { let description: ObjcId = msg_send![error, localizedDescription]; let string = apple_util::nsstring_to_string(description); diff --git a/src/native/apple/frameworks.rs b/src/native/apple/frameworks.rs index b82187d7b..23f87f859 100644 --- a/src/native/apple/frameworks.rs +++ b/src/native/apple/frameworks.rs @@ -69,8 +69,18 @@ impl Drop for RcObjcId { extern "C" { pub static _NSConcreteStackBlock: [*const c_void; 32]; pub static _NSConcreteBogusBlock: [*const c_void; 32]; + pub fn dispatch_data_create( + buffer: *const c_void, + size: usize, + queue: ObjcId, + destructor: ObjcId, + ) -> ObjcId; + pub fn dispatch_release(object: ObjcId); } +/// Has `dispatch_data_create` copy the buffer it's given. +pub const DISPATCH_DATA_DESTRUCTOR_DEFAULT: ObjcId = nil; + #[link(name = "Foundation", kind = "framework")] extern "C" { pub static NSRunLoopCommonModes: ObjcId; From 66046f8efee819993a542e427766d1e371e4e8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Sat, 10 Oct 2026 01:07:57 -0400 Subject: [PATCH 2/2] metal: return an error for a library Metal can't load `new_shader` panicked when Metal couldn't load a `MetalLibrary`, as it does on MSL that fails to compile. A library is a file the app ships, though, and if it's damaged, the app can still compile the shader's MSL. So it now returns a `ShaderError::LinkError` with Metal's description, such as "Invalid library file". Co-Authored-By: Claude Opus 5.5 --- src/graphics.rs | 4 +++- src/graphics/metal.rs | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/graphics.rs b/src/graphics.rs index a29adee92..894d9ae0e 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -1045,7 +1045,9 @@ pub enum ShaderSource<'a> { /// A Metal library compiled ahead of time, as `xcrun metal` writes /// one, holding `vertexShader` and `fragmentShader`. Loading one skips /// compiling MSL at runtime, which can take tens of milliseconds per - /// shader on a device that hasn't cached it yet. + /// shader on a device that hasn't cached it yet. If Metal can't load + /// it, `new_shader` returns a `ShaderError::LinkError`, so the app + /// can compile the MSL instead. MetalLibrary { data: &'a [u8] }, } diff --git a/src/graphics/metal.rs b/src/graphics/metal.rs index 0368438ee..719413916 100644 --- a/src/graphics/metal.rs +++ b/src/graphics/metal.rs @@ -773,9 +773,15 @@ impl RenderingBackend for MetalContext { nil, DISPATCH_DATA_DESTRUCTOR_DEFAULT, ); - let library = + let library: ObjcId = msg_send![self.device, newLibraryWithData: data error: &mut error]; dispatch_release(data); + if library.is_null() { + let description: ObjcId = msg_send![error, localizedDescription]; + return Err(ShaderError::LinkError(apple_util::nsstring_to_string( + description, + ))); + } library } _ => panic!("OpenGl source on Metal context"),