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}