Skip to main content

swiftui_native/
metal.rs

1//! Metal shader effects — `colorEffect`, `layerEffect` and `distortionEffect`.
2//!
3//! SwiftUI runs shaders written in Metal Shading Language. Its own
4//! `ShaderLibrary` will only take one already compiled — as a file or as bytes
5//! — and the offline compiler that produces those ships with Xcode alone, so
6//! the obvious reading is that shaders cannot be built on a device.
7//!
8//! They can. Metal has always compiled source at run time; the missing piece
9//! was a way to hand the result to SwiftUI. A `ShaderLibrary` turns out to be a
10//! single word wrapping an `RBShaderLibrary`, the RenderBox object SwiftUI
11//! renders through, and that class takes an `MTLLibrary` directly. That is
12//! [`ShaderLibrary::from_source`], and it is the only one of these three that
13//! works on a device without help from a Mac.
14//!
15//! ```text
16//!   source ──(MTLDevice, at run time)──► MTLLibrary ─┐
17//!                                                    ├──► ShaderLibrary
18//!   source ──(metal compiler, on a Mac)──► .metallib ┘
19//!                                                    from_path / from_data
20//!        │  .function("name")
21//!        ▼
22//!   ShaderFunction ──.call(&[args])──► Shader ──► View::color_effect
23//! ```
24//!
25//! [`from_source`](ShaderLibrary::from_source) reaches into SwiftUI's internals
26//! and could break on any release; [`from_path`](ShaderLibrary::from_path) and
27//! [`from_data`](ShaderLibrary::from_data) go through public initialisers and
28//! will not. [`compile`] wraps the toolchain call for a build script that wants
29//! to produce a `.metallib` ahead of time, on macOS or cross-built for a device.
30//!
31//! Shader entry points must be marked `[[ stitchable ]]`. A color effect takes
32//! `(float2 position, half4 color, ...)` and returns `half4`.
33//!
34//! ```ignore
35//! use swiftui_native::metal::{ShaderArg, ShaderLibrary};
36//!
37//! let lib = ShaderLibrary::from_source(SRC)?;   // no toolchain, works on iOS
38//! let shader = lib.function("tintify").call(&[ShaderArg::Float(0.3)]);
39//! let view = text("hello").color_effect(&shader);
40//! ```
41
42use crate::abi;
43use crate::resolve;
44use crate::views::ViewHandle;
45use crate::Color;
46use core::ffi::c_void;
47// Only the Mac-side compiler helpers take paths.
48#[cfg(target_os = "macos")]
49use std::path::Path;
50use crate::mangle;
51
52mangle::modifier_types! {
53    /// `_ShaderFilterEffect` — the modifier all three shader entry points add.
54    /// Composed against the receiver, so the result is
55    /// `ModifiedContent<Receiver, _ShaderFilterEffect>`.
56    SHADER_EFFECT_T = ty("_ShaderFilterEffect");
57}
58
59/// An argument passed through to the shader function, after the arguments
60/// SwiftUI supplies itself.
61#[derive(Clone, Copy, Debug)]
62pub enum ShaderArg {
63    Float(f32),
64    Float2(f32, f32),
65    Color(Color),
66}
67
68/// A holder for a Swift value of a resilient type, whose size is only known at
69/// runtime. Every type in the shader chain is address-only, so they are all
70/// carried as raw bytes rather than as Rust structs.
71struct SwiftValue {
72    bytes: Vec<u8>,
73}
74
75impl SwiftValue {
76    unsafe fn empty(meta: *const c_void) -> Self {
77        // Stride, not size: a callee may write the trailing padding too.
78        let n = unsafe { abi::type_bytes(meta) };
79        Self { bytes: vec![0u8; n.max(1)] }
80    }
81
82    fn as_ptr(&self) -> *mut u8 {
83        self.bytes.as_ptr() as *mut u8
84    }
85}
86
87/// An `NSError`'s `localizedDescription`, or a stand-in.
88unsafe fn describe_error(error: *mut c_void) -> String {
89    unsafe {
90        if error.is_null() {
91            return "the Metal compiler failed without saying why".into();
92        }
93        let text = crate::objc::send(error, crate::objc::sel(c"localizedDescription"));
94        if text.is_null() {
95            return "the Metal compiler failed without saying why".into();
96        }
97        let utf8 = crate::objc::send_ret_ptr(text, crate::objc::sel(c"UTF8String"));
98        if utf8.is_null() {
99            return "the Metal compiler failed without saying why".into();
100        }
101        core::ffi::CStr::from_ptr(utf8 as *const i8).to_string_lossy().into_owned()
102    }
103}
104
105/// A compiled Metal library.
106pub struct ShaderLibrary(SwiftValue);
107
108impl ShaderLibrary {
109    /// The underlying `SwiftUI.ShaderLibrary` value, for diagnostics.
110    #[doc(hidden)]
111    pub fn value_ptr(&self) -> *mut c_void {
112        self.0.as_ptr() as *mut c_void
113    }
114}
115
116/// One `[[ stitchable ]]` function inside a library.
117pub struct ShaderFunction(SwiftValue);
118
119/// A shader function bound to its arguments, ready to apply to a view.
120pub struct Shader(SwiftValue);
121
122impl Shader {
123    fn as_ptr(&self) -> *mut u8 {
124        self.0.as_ptr()
125    }
126}
127
128impl ShaderLibrary {
129    /// Load a `.metallib` from disk.
130    pub fn from_path(path: &str) -> Self {
131        unsafe {
132            // Foundation.URL is resilient, so it is built into a buffer and
133            // handed onward by pointer rather than in registers.
134            let url_meta = abi::metadata0(resolve::url_ma());
135            let url = SwiftValue::empty(url_meta);
136            let ss = abi::swift_string(path);
137            let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
138            let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
139            core::arch::asm!("blr {f}", f = in(reg) resolve::url_file_path_init(),
140                in("x0") s0, in("x1") s1,
141                in("x8") url.as_ptr(),
142                in("x20") url_meta,
143                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
144                lateout("x6") _, lateout("x7") _, lateout("x9") _, lateout("x10") _,
145                lateout("x11") _, lateout("x12") _, lateout("x13") _, lateout("x14") _,
146                lateout("x15") _, lateout("x16") _, lateout("x17") _,
147                lateout("lr") _, lateout("x21") _, clobber_abi("C"),
148            );
149
150            let lib_meta = abi::metadata0(resolve::shader_library_ma());
151            let lib = SwiftValue::empty(lib_meta);
152            // ShaderLibrary.init(url:) — x0 = @in URL, x8 = @out ShaderLibrary
153            core::arch::asm!("blr {f}", f = in(reg) resolve::shader_library_url_init(),
154                in("x0") url.as_ptr(),
155                in("x8") lib.as_ptr(),
156                in("x20") lib_meta,
157                lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
158                lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x9") _,
159                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
160                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
161                lateout("lr") _, lateout("x21") _, clobber_abi("C"),
162            );
163            Self(lib)
164        }
165    }
166
167    /// Compile Metal Shading Language source at run time.
168    ///
169    /// No `.metallib`, no Xcode, and it works on a device — which is what
170    /// [`compile`] cannot do, since the offline Metal compiler ships with Xcode
171    /// alone. Metal itself has always been able to compile at run time; what was
172    /// missing was a way to hand the result to SwiftUI, whose `ShaderLibrary`
173    /// takes only an already-compiled library's bytes.
174    ///
175    /// There is one, as it turns out. A `ShaderLibrary` is a single word
176    /// wrapping an `RBShaderLibrary` — the RenderBox object SwiftUI actually
177    /// renders through — and that class accepts an `MTLLibrary` directly. So
178    /// the source goes through `MTLDevice`, which reports errors properly, and
179    /// the resulting library is wrapped.
180    ///
181    /// `RBShaderLibrary` is internal to SwiftUI: it is not API, and a future
182    /// release may rename it or change its shape. [`from_data`](Self::from_data)
183    /// and [`from_path`](Self::from_path) go through SwiftUI's public
184    /// initialisers and will not.
185    ///
186    /// # Errors
187    /// The compiler's own diagnostics, or a note that the runtime pieces are
188    /// missing. Worth propagating: a library built from source that does not
189    /// compile still *renders*, as a shader that silently does nothing.
190    pub fn from_source(source: &str) -> Result<Self, String> {
191        unsafe {
192            let _pool = crate::objc::AutoreleasePool::new();
193            unsafe extern "C" {
194                fn MTLCreateSystemDefaultDevice() -> *mut c_void;
195            }
196            let device = MTLCreateSystemDefaultDevice();
197            if device.is_null() {
198                return Err("no Metal device".into());
199            }
200
201            // -newLibraryWithSource:options:error:
202            type NewLibrary = unsafe extern "C" fn(
203                *mut c_void, *const c_void, *mut c_void, *mut c_void, *mut *mut c_void,
204            ) -> *mut c_void;
205            let new_library: NewLibrary =
206                core::mem::transmute(crate::objc::msg_send_addr());
207            let mut error: *mut c_void = core::ptr::null_mut();
208            let library = new_library(
209                device,
210                crate::objc::sel(c"newLibraryWithSource:options:error:"),
211                crate::objc::nsstring(source),
212                core::ptr::null_mut(),
213                &mut error,
214            );
215            if library.is_null() {
216                return Err(describe_error(error));
217            }
218
219            let class = crate::objc::class_named(c"RBShaderLibrary");
220            if class.is_null() {
221                return Err("SwiftUI's RBShaderLibrary is not available".into());
222            }
223            let rb = crate::objc::send_id(
224                class,
225                crate::objc::sel(c"libraryWithMetalLibrary:"),
226                library,
227            );
228            if rb.is_null() {
229                return Err("RBShaderLibrary rejected the compiled library".into());
230            }
231            // The convenience constructor hands back an autoreleased object;
232            // the value being built has to own one. The `MTLLibrary` came from
233            // a `new...` and is owned here, and `RBShaderLibrary` has taken its
234            // own reference by now.
235            crate::objc::send(rb, crate::objc::sel(c"retain"));
236            crate::objc::send(library, crate::objc::sel(c"release"));
237
238            let meta = abi::metadata0(resolve::shader_library_ma());
239            let value = SwiftValue::empty(meta);
240            debug_assert_eq!(abi::type_size(meta), 8, "ShaderLibrary is one word");
241            *(value.as_ptr() as *mut u64) = rb as u64;
242            Ok(Self(value))
243        }
244    }
245
246    /// Load a `.metallib` from bytes already in memory.
247    ///
248    /// A `.metallib` has to be built ahead of time — see [`compile_for`] — but
249    /// it does not have to be a *file* at run time. `include_bytes!` it into the
250    /// binary and this needs no bundle resource and no path, which on iOS is
251    /// the difference between shipping a shader and shipping an app layout.
252    ///
253    /// `Data` is bridged from an `NSData` rather than built directly:
254    /// `Data.init(bytes:count:)` is inlinable and so has no symbol to call,
255    /// while `init(referencing:)` is a real entry point.
256    pub fn from_data(bytes: &[u8]) -> Self {
257        unsafe {
258            let _pool = crate::objc::AutoreleasePool::new();
259            // `alloc`/`init` rather than the autoreleased convenience
260            // constructor: `Data` holds onto this reference and there is no
261            // pool here whose drain it would survive.
262            type InitBytes =
263                unsafe extern "C" fn(*mut c_void, *const c_void, *const u8, u64) -> *mut c_void;
264            let init: InitBytes = core::mem::transmute(crate::objc::msg_send_addr());
265            let nsdata = init(
266                crate::objc::send(
267                    crate::objc::class(c"OBJC_CLASS_$_NSData"),
268                    crate::objc::sel(c"alloc"),
269                ),
270                crate::objc::sel(c"initWithBytes:length:"),
271                bytes.as_ptr(),
272                bytes.len() as u64,
273            );
274            assert!(!nsdata.is_null(), "NSData creation failed");
275
276            // `Data` is sixteen bytes and *loadable*, unlike the resilient
277            // `URL` next door: it comes back in x0/x1 rather than through an
278            // indirect result, and goes on to `ShaderLibrary` the same way.
279            // Reading x8 instead yields an untouched buffer — an empty `Data`,
280            // which produces a library that loads and silently shades nothing.
281            let data_meta = abi::metadata0(resolve::data_ma());
282            let (d0, d1): (u64, u64);
283            core::arch::asm!("blr {f}", f = in(reg) resolve::data_referencing_init(),
284                in("x0") nsdata,
285                in("x20") data_meta,
286                lateout("x0") d0, lateout("x1") d1,
287                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
288                lateout("x6") _, lateout("x7") _, lateout("x8") _, lateout("x9") _,
289                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
290                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
291                lateout("lr") _, lateout("x21") _, clobber_abi("C"),
292            );
293
294            let lib_meta = abi::metadata0(resolve::shader_library_ma());
295            let lib = SwiftValue::empty(lib_meta);
296            // ShaderLibrary.init(data:) — x0/x1 = Data, x8 = @out ShaderLibrary
297            core::arch::asm!("blr {f}", f = in(reg) resolve::shader_library_data_init(),
298                in("x0") d0,
299                in("x1") d1,
300                in("x8") lib.as_ptr(),
301                in("x20") lib_meta,
302                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
303                lateout("x6") _, lateout("x7") _, lateout("x9") _,
304                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
305                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
306                lateout("lr") _, lateout("x21") _, clobber_abi("C"),
307            );
308            Self(lib)
309        }
310    }
311
312    /// Look up a `[[ stitchable ]]` function by name.
313    ///
314    /// In Swift this is `library.myFunction`, spelled through
315    /// `@dynamicMemberLookup`; the underlying subscript takes the name as an
316    /// ordinary string.
317    pub fn function(&self, name: &str) -> ShaderFunction {
318        unsafe {
319            let fn_meta = abi::metadata0(resolve::shader_function_ma());
320            let out = SwiftValue::empty(fn_meta);
321            let ss = abi::swift_string(name);
322            let s0 = u64::from_le_bytes(ss[..8].try_into().unwrap());
323            let s1 = u64::from_le_bytes(ss[8..].try_into().unwrap());
324            // subscript(dynamicMember:).getter
325            //   x0/x1 = name, x8 = @out ShaderFunction, x20 = @in_guaranteed self
326            core::arch::asm!("blr {f}", f = in(reg) resolve::shader_library_subscript(),
327                in("x0") s0, in("x1") s1,
328                in("x8") out.as_ptr(),
329                in("x20") self.0.as_ptr(),
330                lateout("x2") _, lateout("x3") _, lateout("x4") _, lateout("x5") _,
331                lateout("x6") _, lateout("x7") _, lateout("x9") _, lateout("x10") _,
332                lateout("x11") _, lateout("x12") _, lateout("x13") _, lateout("x14") _,
333                lateout("x15") _, lateout("x16") _, lateout("x17") _,
334                lateout("lr") _, lateout("x21") _, clobber_abi("C"),
335            );
336            ShaderFunction(out)
337        }
338    }
339}
340
341impl ShaderFunction {
342    /// Bind arguments, producing a `Shader`.
343    pub fn call(&self, args: &[ShaderArg]) -> Shader {
344        unsafe {
345            let arg_meta = abi::metadata0(resolve::shader_argument_ma());
346            let stride = abi::type_stride(arg_meta);
347
348            // Each argument is built by its own static factory, then packed into
349            // a Swift Array at the element stride.
350            let mut packed = vec![0u8; stride * args.len()];
351            for (i, a) in args.iter().enumerate() {
352                let value = build_argument(*a, arg_meta);
353                packed[i * stride..i * stride + value.bytes.len()]
354                    .copy_from_slice(&value.bytes);
355            }
356            let array = abi::swift_array(arg_meta, args.len(), &packed);
357
358            let shader_meta = abi::metadata0(resolve::shader_ma());
359            let out = SwiftValue::empty(shader_meta);
360            // dynamicallyCall(withArguments:) — x0 = Array, x8 = @out Shader,
361            // x20 = @in_guaranteed self
362            core::arch::asm!("blr {f}", f = in(reg) resolve::shader_fn_call(),
363                in("x0") array,
364                in("x8") out.as_ptr(),
365                in("x20") self.0.as_ptr(),
366                lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
367                lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x9") _,
368                lateout("x10") _, lateout("x11") _, lateout("x12") _, lateout("x13") _,
369                lateout("x14") _, lateout("x15") _, lateout("x16") _, lateout("x17") _,
370                lateout("lr") _, lateout("x21") _, clobber_abi("C"),
371            );
372            Shader(out)
373        }
374    }
375}
376
377/// Build one `Shader.Argument` through its static factory.
378unsafe fn build_argument(arg: ShaderArg, meta: *const c_void) -> SwiftValue {
379    unsafe {
380        let out = SwiftValue::empty(meta);
381        match arg {
382            // Swift `Float` is 32-bit and travels in s0/s1.
383            ShaderArg::Float(v) => {
384                core::arch::asm!("blr {f}", f = in(reg) resolve::shader_arg_float(),
385                    in("s0") v,
386                    in("x8") out.as_ptr(),
387                    in("x20") meta,
388                    lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
389                    lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
390                    lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
391                    lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
392                    lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
393                );
394            }
395            ShaderArg::Float2(x, y) => {
396                core::arch::asm!("blr {f}", f = in(reg) resolve::shader_arg_float2(),
397                    in("s0") x, in("s1") y,
398                    in("x8") out.as_ptr(),
399                    in("x20") meta,
400                    lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
401                    lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
402                    lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
403                    lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
404                    lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
405                );
406            }
407            ShaderArg::Color(c) => {
408                let value = crate::views::color_value(c.r, c.g, c.b, c.a);
409                core::arch::asm!("blr {f}", f = in(reg) resolve::shader_arg_color(),
410                    in("x0") value,
411                    in("x8") out.as_ptr(),
412                    in("x20") meta,
413                    lateout("x1") _, lateout("x2") _, lateout("x3") _,
414                    lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
415                    lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
416                    lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
417                    lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
418                );
419            }
420        }
421        out
422    }
423}
424
425/// Which shader modifier to apply.
426enum Kind {
427    Color,
428    Layer,
429    Distortion,
430}
431
432unsafe fn apply(view: &ViewHandle, shader: &Shader, kind: Kind, max: (f64, f64)) -> ViewHandle {
433    unsafe {
434        let (func, has_offset) = match kind {
435            Kind::Color => (resolve::color_effect_fn(), false),
436            Kind::Layer => (resolve::layer_effect_fn(), true),
437            Kind::Distortion => (resolve::distortion_effect_fn(), true),
438        };
439        let (meta, wt, size) = SHADER_EFFECT_T.applied_to(view.meta);
440        let mut result = vec![0u8; size];
441        let this = view.value_ptr();
442
443        // colorEffect(_:isEnabled:)          x0 = @in Shader, x1 = isEnabled
444        // layerEffect(_:maxSampleOffset:isEnabled:) adds a CGSize in d0/d1
445        //   then Self.Type and the Self: View witness table, x8 = @out, x20 = self
446        if has_offset {
447            core::arch::asm!("blr {f}", f = in(reg) func,
448                in("x0") shader.as_ptr(),
449                in("d0") max.0, in("d1") max.1,
450                in("x1") 1u64,
451                in("x2") view.meta,
452                in("x3") view.wt,
453                in("x8") result.as_mut_ptr(),
454                in("x20") this,
455                lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
456                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
457                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
458                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
459            );
460        } else {
461            core::arch::asm!("blr {f}", f = in(reg) func,
462                in("x0") shader.as_ptr(),
463                in("x1") 1u64,
464                in("x2") view.meta,
465                in("x3") view.wt,
466                in("x8") result.as_mut_ptr(),
467                in("x20") this,
468                lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
469                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
470                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
471                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
472            );
473        }
474        crate::views::wrap_owned(result, meta, wt)
475    }
476}
477
478/// Apply `.colorEffect(_:)` — recolours each pixel.
479pub fn color_effect(view: &ViewHandle, shader: &Shader) -> ViewHandle {
480    unsafe { apply(view, shader, Kind::Color, (0.0, 0.0)) }
481}
482
483/// Apply `.layerEffect(_:maxSampleOffset:)` — can sample neighbouring pixels.
484pub fn layer_effect(view: &ViewHandle, shader: &Shader, max_w: f64, max_h: f64) -> ViewHandle {
485    unsafe { apply(view, shader, Kind::Layer, (max_w, max_h)) }
486}
487
488/// Apply `.distortionEffect(_:maxSampleOffset:)` — warps geometry.
489pub fn distortion_effect(view: &ViewHandle, shader: &Shader, max_w: f64, max_h: f64) -> ViewHandle {
490    unsafe { apply(view, shader, Kind::Distortion, (max_w, max_h)) }
491}
492
493/// Compile Metal Shading Language source into a `.metallib` for the Mac.
494///
495/// Only on the Mac, and deliberately: the offline Metal compiler ships with
496/// Xcode and nowhere else, so there is nothing for this to call on a device.
497/// Anything shipping to iOS builds its library ahead of time with
498/// [`compile_for`] and loads it with [`ShaderLibrary::from_path`] or
499/// [`ShaderLibrary::from_data`] — both of which work on every platform.
500///
501/// Metal *can* compile source at runtime on iOS, through
502/// `MTLDevice.makeLibrary(source:options:)`. It is no help here: that hands
503/// back an `MTLLibrary` object, and SwiftUI's `ShaderLibrary` takes a compiled
504/// library's *bytes*. There is no public way from one to the other.
505#[cfg(target_os = "macos")]
506pub fn compile(source: &str, out_path: impl AsRef<Path>) -> std::io::Result<()> {
507    compile_for("macosx", source, out_path)
508}
509
510/// Compile for a named SDK — `macosx`, `iphonesimulator`, `iphoneos`.
511///
512/// A `.metallib` is per-platform, so building one for a device or a simulator
513/// has to name the SDK explicitly. This runs on the Mac whichever target it is
514/// producing for.
515#[cfg(target_os = "macos")]
516pub fn compile_for(sdk: &str, source: &str, out_path: impl AsRef<Path>) -> std::io::Result<()> {
517    use std::process::Command;
518    let out = out_path.as_ref();
519    let src = out.with_extension("metal");
520    std::fs::write(&src, source)?;
521    let status = Command::new("xcrun")
522        .args(["-sdk", sdk, "metal", "-o"])
523        .arg(out)
524        .arg(&src)
525        .output()?;
526    if !status.status.success() {
527        return Err(std::io::Error::other(String::from_utf8_lossy(&status.stderr).to_string()));
528    }
529    Ok(())
530}