raw_window_metal/
lib.rs

1//! # Interop between Metal and [`raw-window-handle`]
2//!
3//! Helpers for constructing a [`CAMetalLayer`] from a handle given by [`raw-window-handle`]. See
4//! the [`Layer`] type for the full API.
5//!
6//! [`raw-window-handle`]: https://crates.io/crates/raw-window-handle
7//!
8//!
9//! ## Example
10//!
11//! Create a layer from a window that implements [`HasWindowHandle`].
12//!
13//! ```
14//! use objc2::rc::Retained;
15//! use objc2_quartz_core::CAMetalLayer;
16//! use raw_window_handle::{RawWindowHandle, HasWindowHandle};
17//! use raw_window_metal::Layer;
18//! #
19//! # let mtm = objc2::MainThreadMarker::new().expect("doc tests to run on main thread");
20//! #
21//! # #[cfg(target_os = "macos")]
22//! # let view = unsafe { objc2_app_kit::NSView::new(mtm) };
23//! # #[cfg(target_os = "macos")]
24//! # let handle = RawWindowHandle::AppKit(raw_window_handle::AppKitWindowHandle::new(std::ptr::NonNull::from(&*view).cast()));
25//! #
26//! # #[cfg(not(target_os = "macos"))]
27//! # let view = unsafe { objc2_ui_kit::UIView::new(mtm) };
28//! # #[cfg(not(target_os = "macos"))]
29//! # let handle = RawWindowHandle::UiKit(raw_window_handle::UiKitWindowHandle::new(std::ptr::NonNull::from(&*view).cast()));
30//! # let window = unsafe { raw_window_handle::WindowHandle::borrow_raw(handle) };
31//!
32//! let layer = match window.window_handle().expect("handle available").as_raw() {
33//!     // SAFETY: The handle is a valid `NSView` because it came from `WindowHandle<'_>`.
34//!     RawWindowHandle::AppKit(handle) => unsafe { Layer::from_ns_view(handle.ns_view) },
35//!     // SAFETY: The handle is a valid `UIView` because it came from `WindowHandle<'_>`.
36//!     RawWindowHandle::UiKit(handle) => unsafe { Layer::from_ui_view(handle.ui_view) },
37//!     _ => panic!("unsupported handle"),
38//! };
39//! let layer: *mut CAMetalLayer = layer.into_raw().as_ptr().cast();
40//! // SAFETY: The pointer is a valid `CAMetalLayer`, and because we consumed `Layer` with
41//! // `into_raw`, the pointer has +1 retain count.
42//! let layer = unsafe { Retained::from_raw(layer).unwrap() };
43//!
44//! // Use `CAMetalLayer` here.
45//! ```
46//!
47//! [`HasWindowHandle`]: https://docs.rs/raw-window-handle/0.6.2/raw_window_handle/trait.HasWindowHandle.html
48//!
49//!
50//! ## Semantics
51//!
52//! As the user of this crate, you are likely creating a library yourself, and need to interface
53//! with a layer provided by a windowing library like Winit or SDL.
54//!
55//! In that sense, when the user hands your library a view or a layer via. `raw-window-handle`, they
56//! likely expect you to render into it. You should freely do that, but you should refrain from
57//! doing things like resizing the layer by changing its `bounds`, changing its `contentsGravity`,
58//! `opacity`, and similar such properties; semantically, these are things that are "outside" of
59//! your library's control, and interferes with the platforms normal handling of such things (i.e.
60//! the user creating a `MTKView`, and placing it inside a `NSStackView`. In such cases, you should
61//! not change the bounds of the view, as that will be done automatically at a "higher" level).
62//!
63//! Properties specific to `CAMetalLayer` like `drawableSize`, `colorspace` and so on probably _are_
64//! fine to change, because these are properties that the user _expects_ your library to change when
65//! they've given it to you (and they won't be changed by e.g. the layer being inside a stack view).
66//!
67//!
68//! ## Reasoning behind creating a sublayer
69//!
70//! If a view does not have a `CAMetalLayer` as the root layer (as is the default for most views),
71//! then we're in a bit of a tricky position! We cannot use the existing layer with Metal, so we
72//! must do something else. There are a few options:
73//!
74//! 1. Panic, and require the user to pass a view with a `CAMetalLayer` layer.
75//!
76//!    While this would "work", it doesn't solve the problem, and instead passes the ball onwards to
77//!    the user and ecosystem to figure it out.
78//!
79//! 2. Override the existing layer with a newly created layer.
80//!
81//!    If we overlook that this does not work in UIKit since `UIView`'s `layer` is `readonly`, and
82//!    that as such we will need to do something different there anyhow, this is actually a fairly
83//!    good solution, and was what the original implementation did.
84//!
85//!    It has some problems though, due to:
86//!
87//!    a. Consumers of `raw-window-metal` like Wgpu and Ash in their API design choosing not to
88//!       register a callback with `-[CALayerDelegate displayLayer:]`, but instead leaves it up to
89//!       the user to figure out when to redraw. That is, they rely on other libraries' callbacks
90//!       telling them when to render.
91//!
92//!       (If you were to make an API only for Metal, you would probably make the user provide a
93//!       `render` closure that'd be called in the right situations).
94//!
95//!    b. Overwriting the `layer` on `NSView` makes the view "layer-hosting", see [wantsLayer],
96//!       which disables drawing functionality on the view like `drawRect:`/`updateLayer`.
97//!
98//!    These two in combination makes it basically impossible for crates like Winit to provide a
99//!    robust rendering callback that integrates with the system's built-in mechanisms for
100//!    redrawing, exactly because overwriting the layer would be disabling those mechanisms!
101//!
102//!    [wantsLayer]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
103//!
104//! 3. Create a sublayer.
105//!
106//!    `CALayer` has the concept of "sublayers", which we can use instead of overriding the layer.
107//!
108//!    This is also the recommended solution on UIKit, so it's nice that we can use the same
109//!    implementation regardless of operating system.
110//!
111//!    It _might_, however, perform ever so slightly worse than overriding the layer directly.
112//!
113//! 4. Create a new `MTKView` (or a custom view), and add it as a subview.
114//!
115//!    Similar to creating a sublayer (see above), but also provides a bunch of event handling that
116//!    we don't need.
117//!
118//! Option 3 seems like the most robust solution, so this is what this crate does.
119//!
120//! Now we have another problem though: The `bounds` and `contentsScale` of sublayers are not
121//! automatically updated from the super layer.
122//!
123//! We could again choose to let that be up to the user of our crate, but that would be very
124//! cumbersome. Instead, this crate registers the necessary observers to make the sublayer track the
125//! size and scale factor of its super layer automatically. This makes it extra important that you
126//! do not modify common `CALayer` properties of the layer that `raw-window-metal` creates, since
127//! they may just end up being overwritten (see also "Semantics" above).
128
129#![no_std]
130#![cfg(target_vendor = "apple")]
131#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide), doc(cfg_hide(doc)))]
132#![deny(unsafe_op_in_unsafe_fn)]
133#![warn(clippy::undocumented_unsafe_blocks)]
134// Update in Cargo.toml as well.
135#![doc(html_root_url = "https://docs.rs/raw-window-metal/1.1.0")]
136
137mod observer;
138
139use core::ffi::{c_void, CStr};
140use core::hash;
141use core::panic::{RefUnwindSafe, UnwindSafe};
142use core::ptr::NonNull;
143
144use objc2::rc::Retained;
145use objc2::runtime::AnyClass;
146use objc2::{msg_send, ClassType, MainThreadMarker, Message};
147use objc2_foundation::{NSObject, NSObjectProtocol};
148use objc2_quartz_core::{CALayer, CAMetalLayer};
149
150use crate::observer::ObserverLayer;
151
152#[cfg(not(feature = "alloc"))]
153compile_error!("The `alloc` feature must currently be enabled.");
154
155#[cfg(not(feature = "std"))]
156compile_error!("The `std` feature must currently be enabled.");
157
158/// A wrapper around [`CAMetalLayer`].
159#[doc(alias = "CAMetalLayer")]
160#[derive(Debug, Clone)]
161pub struct Layer {
162    layer: Retained<CAMetalLayer>,
163    pre_existing: bool,
164}
165
166impl PartialEq for Layer {
167    #[inline]
168    fn eq(&self, other: &Self) -> bool {
169        self.layer.eq(&other.layer)
170    }
171}
172
173impl Eq for Layer {}
174
175impl hash::Hash for Layer {
176    #[inline]
177    fn hash<H: hash::Hasher>(&self, state: &mut H) {
178        self.layer.hash(state);
179    }
180}
181
182// SAFETY: `CAMetalLayer` is thread safe, like most things in Core Animation, see:
183// https://developer.apple.com/documentation/quartzcore/catransaction/1448267-lock?language=objc
184// https://stackoverflow.com/questions/76250226/how-to-render-content-of-calayer-on-a-background-thread
185//
186// TODO(madsmtm): Move this to `objc2-quartz-core`.
187unsafe impl Send for Layer {}
188// SAFETY: Same as above.
189unsafe impl Sync for Layer {}
190
191// Layer methods may panic, but that won't leave the layer in an invalid state.
192//
193// TODO(madsmtm): Move this to `objc2-quartz-core`.
194impl UnwindSafe for Layer {}
195impl RefUnwindSafe for Layer {}
196
197impl Layer {
198    /// Get a pointer to the underlying [`CAMetalLayer`].
199    ///
200    /// The pointer is valid for at least as long as the [`Layer`] is valid, but can be extended by
201    /// retaining it.
202    ///
203    /// You should usually not change general `CALayer` properties like `bounds`, `contentsScale`
204    /// and so on of this layer, but instead modify the layer that it was created from.
205    ///
206    /// You can safely modify `CAMetalLayer` properties like `drawableSize` to match your needs,
207    /// though beware that if it does not match the actual size of the layer, the contents will be
208    /// scaled.
209    ///
210    ///
211    /// # Example
212    ///
213    /// ```no_run
214    /// use objc2_quartz_core::CAMetalLayer;
215    /// use raw_window_metal::Layer;
216    ///
217    /// let layer: Layer;
218    /// # layer = unimplemented!();
219    ///
220    /// // SAFETY: The pointer is a valid `CAMetalLayer`.
221    /// let layer: &CAMetalLayer = unsafe { layer.as_ptr().cast().as_ref() };
222    ///
223    /// // Use the `CAMetalLayer` here.
224    /// ```
225    #[inline]
226    pub fn as_ptr(&self) -> NonNull<c_void> {
227        let ptr: *const CAMetalLayer = Retained::as_ptr(&self.layer);
228        // Unwrap is fine, `Retained` stores a non-null pointer
229        NonNull::new(ptr as *mut _).unwrap()
230    }
231
232    /// Consume the layer, and return a pointer with +1 retain count to the underlying
233    /// [`CAMetalLayer`].
234    ///
235    /// After calling this function, the caller is responsible for releasing the pointer, otherwise
236    /// the layer will be leaked.
237    ///
238    ///
239    /// # Example
240    ///
241    /// Convert a layer to a [`Retained`] `CAMetalLayer`.
242    ///
243    /// ```no_run
244    /// use objc2::rc::Retained;
245    /// use objc2_quartz_core::CAMetalLayer;
246    /// use raw_window_metal::Layer;
247    ///
248    /// let layer: Layer;
249    /// # layer = unimplemented!();
250    ///
251    /// let layer: *mut CAMetalLayer = layer.into_raw().as_ptr().cast();
252    /// // SAFETY: The pointer is a valid `CAMetalLayer`, and because we consumed `Layer` with
253    /// // `into_raw`, the pointer has +1 retain count.
254    /// let layer = unsafe { Retained::from_raw(layer).unwrap() };
255    ///
256    /// // Use the `CAMetalLayer` here.
257    /// ```
258    #[inline]
259    pub fn into_raw(self) -> NonNull<c_void> {
260        // Unwrap is fine, `Retained` stores a non-null pointer
261        NonNull::new(Retained::into_raw(self.layer).cast()).unwrap()
262    }
263
264    /// If `raw-window-metal` created a new [`CAMetalLayer`] for you, this returns `false`.
265    ///
266    /// This may be useful if you want to override some part of `raw-window-metal`'s behaviour, and
267    /// need to do so based on whether it ended up creating a layer or not.
268    ///
269    /// You should try to avoid this, and instead:
270    /// - Modify `CALayer` properties on the layer that you created this from.
271    /// - Modify `CAMetalLayer` properties on the layer returned from `as_ptr`.
272    #[inline]
273    pub fn pre_existing(&self) -> bool {
274        self.pre_existing
275    }
276
277    /// Get or create a new `CAMetalLayer` from the given `CALayer`.
278    ///
279    /// If the given layer is a `CAMetalLayer`, this will simply return that layer. If not, a new
280    /// `CAMetalLayer` is created and inserted as a sublayer, and then configured such that it will
281    /// have the same bounds and scale factor as the given layer.
282    ///
283    ///
284    /// # Safety
285    ///
286    /// The given layer pointer must be a valid instance of `CALayer`.
287    ///
288    ///
289    /// # Examples
290    ///
291    /// Create a new layer from a `CAMetalLayer`.
292    ///
293    /// ```
294    /// use std::ptr::NonNull;
295    /// use objc2_quartz_core::CAMetalLayer;
296    /// use raw_window_metal::Layer;
297    ///
298    /// let layer = unsafe { CAMetalLayer::new() };
299    /// let ptr: NonNull<CAMetalLayer> = NonNull::from(&*layer);
300    ///
301    /// let layer = unsafe { Layer::from_ca_layer(ptr.cast()) };
302    /// assert!(layer.pre_existing());
303    /// ```
304    ///
305    /// Create a `CAMetalLayer` sublayer in a `CALayer`.
306    ///
307    /// ```
308    /// use std::ptr::NonNull;
309    /// use objc2_quartz_core::CALayer;
310    /// use raw_window_metal::Layer;
311    ///
312    /// let layer = CALayer::new();
313    /// let ptr: NonNull<CALayer> = NonNull::from(&*layer);
314    ///
315    /// let layer = unsafe { Layer::from_ca_layer(ptr.cast()) };
316    /// assert!(!layer.pre_existing());
317    /// ```
318    pub unsafe fn from_ca_layer(layer_ptr: NonNull<c_void>) -> Self {
319        // SAFETY: Caller ensures that the pointer is a valid `CALayer`.
320        let root_layer: &CALayer = unsafe { layer_ptr.cast().as_ref() };
321
322        // Debug check that the given layer actually _is_ a CALayer.
323        if cfg!(debug_assertions) {
324            assert!(
325                root_layer.isKindOfClass(CALayer::class()),
326                "view was not a valid CALayer"
327            );
328        }
329
330        if let Some(layer) = root_layer.downcast_ref::<CAMetalLayer>() {
331            Layer {
332                layer: layer.retain(),
333                pre_existing: true,
334            }
335        } else {
336            let layer = ObserverLayer::new(root_layer);
337            Layer {
338                layer: layer.into_super(),
339                pre_existing: false,
340            }
341        }
342    }
343
344    fn from_retained_layer(root_layer: Retained<CALayer>) -> Self {
345        match root_layer.downcast::<CAMetalLayer>() {
346            Ok(layer) => Layer {
347                layer,
348                pre_existing: true,
349            },
350            Err(root_layer) => {
351                let layer = ObserverLayer::new(&root_layer);
352                Layer {
353                    layer: layer.into_super(),
354                    pre_existing: false,
355                }
356            }
357        }
358    }
359
360    /// Get or create a new `CAMetalLayer` from the given `NSView`.
361    ///
362    /// If the given view is not [layer-backed], it will be made so.
363    ///
364    /// If the given view has a `CAMetalLayer` as the root layer (which can happen for example if
365    /// the view has overwritten `-[NSView layerClass]` or the view is `MTKView`) this will simply
366    /// return that layer. If not, a new `CAMetalLayer` is created and inserted as a sublayer into
367    /// the view's layer, and then configured such that it will have the same bounds and scale
368    /// factor as the given view.
369    ///
370    ///
371    /// # Panics
372    ///
373    /// Panics if called from a thread that is not the main thread.
374    ///
375    ///
376    /// # Safety
377    ///
378    /// The given view pointer must be a valid instance of `NSView`.
379    ///
380    ///
381    /// # Example
382    ///
383    /// Construct a layer from an [`AppKitWindowHandle`].
384    ///
385    /// ```
386    /// use raw_window_handle::AppKitWindowHandle;
387    /// use raw_window_metal::Layer;
388    ///
389    /// let handle: AppKitWindowHandle;
390    /// # let mtm = objc2::MainThreadMarker::new().expect("doc tests to run on main thread");
391    /// # #[cfg(target_os = "macos")]
392    /// # let view = unsafe { objc2_app_kit::NSView::new(mtm) };
393    /// # #[cfg(target_os = "macos")]
394    /// # { handle = AppKitWindowHandle::new(std::ptr::NonNull::from(&*view).cast()) };
395    /// # #[cfg(not(target_os = "macos"))]
396    /// # { handle = unimplemented!() };
397    /// let layer = unsafe { Layer::from_ns_view(handle.ns_view) };
398    /// ```
399    ///
400    /// [layer-backed]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
401    /// [`AppKitWindowHandle`]: https://docs.rs/raw-window-handle/0.6.2/raw_window_handle/struct.AppKitWindowHandle.html
402    pub unsafe fn from_ns_view(ns_view_ptr: NonNull<c_void>) -> Self {
403        let _mtm = MainThreadMarker::new().expect("can only access NSView on the main thread");
404
405        // SAFETY: Caller ensures that the pointer is a valid `NSView`.
406        //
407        // We use `NSObject` here to avoid importing `objc2-app-kit`.
408        let ns_view: &NSObject = unsafe { ns_view_ptr.cast().as_ref() };
409
410        // Debug check that the given view actually _is_ a NSView.
411        if cfg!(debug_assertions) {
412            // Load the class at runtime (instead of using `class!`)
413            // to ensure that this still works if AppKit isn't linked.
414            let cls = AnyClass::get(CStr::from_bytes_with_nul(b"NSView\0").unwrap()).unwrap();
415            assert!(ns_view.isKindOfClass(cls), "view was not a valid NSView");
416        }
417
418        // Force the view to become layer backed
419        // SAFETY: The signature of `NSView::setWantsLayer` is correctly specified.
420        let _: () = unsafe { msg_send![ns_view, setWantsLayer: true] };
421
422        // SAFETY: `-[NSView layer]` returns an optional `CALayer`
423        let root_layer: Option<Retained<CALayer>> = unsafe { msg_send![ns_view, layer] };
424        let root_layer = root_layer.expect("failed making the view layer-backed");
425
426        Self::from_retained_layer(root_layer)
427    }
428
429    /// Get or create a new `CAMetalLayer` from the given `UIView`.
430    ///
431    /// If the given view has a `CAMetalLayer` as the root layer (which can happen for example if
432    /// the view has overwritten `-[UIView layerClass]` or the view is `MTKView`) this will simply
433    /// return that layer. If not, a new `CAMetalLayer` is created and inserted as a sublayer into
434    /// the view's layer, and then configured such that it will have the same bounds and scale
435    /// factor as the given view.
436    ///
437    ///
438    /// # Panics
439    ///
440    /// Panics if called from a thread that is not the main thread.
441    ///
442    ///
443    /// # Safety
444    ///
445    /// The given view pointer must be a valid instance of `UIView`.
446    ///
447    ///
448    /// # Example
449    ///
450    /// Construct a layer from a [`UiKitWindowHandle`].
451    ///
452    /// ```no_run
453    /// use raw_window_handle::UiKitWindowHandle;
454    /// use raw_window_metal::Layer;
455    ///
456    /// let handle: UiKitWindowHandle;
457    /// # handle = unimplemented!();
458    /// let layer = unsafe { Layer::from_ui_view(handle.ui_view) };
459    /// ```
460    ///
461    /// [`UiKitWindowHandle`]: https://docs.rs/raw-window-handle/0.6.2/raw_window_handle/struct.UiKitWindowHandle.html
462    pub unsafe fn from_ui_view(ui_view_ptr: NonNull<c_void>) -> Self {
463        let _mtm = MainThreadMarker::new().expect("can only access UIView on the main thread");
464
465        // SAFETY: Caller ensures that the pointer is a valid `UIView`.
466        //
467        // We use `NSObject` here to avoid importing `objc2-ui-kit`.
468        let ui_view: &NSObject = unsafe { ui_view_ptr.cast().as_ref() };
469
470        // Debug check that the given view actually _is_ a UIView.
471        if cfg!(debug_assertions) {
472            // Load the class at runtime (instead of using `class!`)
473            // to ensure that this still works if UIKit isn't linked.
474            let cls = AnyClass::get(CStr::from_bytes_with_nul(b"UIView\0").unwrap()).unwrap();
475            assert!(ui_view.isKindOfClass(cls), "view was not a valid UIView");
476        }
477
478        // SAFETY: `-[UIView layer]` returns a non-optional `CALayer`
479        let root_layer: Retained<CALayer> = unsafe { msg_send![ui_view, layer] };
480
481        // Unlike on macOS, we cannot replace the main view as `UIView` does
482        // not allow it (when `NSView` does).
483        Self::from_retained_layer(root_layer)
484    }
485}