Skip to content

Commit c028c8a

Browse files
committed
[metal]: Create a new layer instead of overwriting the existing one
Overriding the `layer` on `NSView` makes the view "layer-hosting", see [wantsLayer], which disables drawing functionality on the view like `drawRect:`/`updateLayer`. This prevents crates like Winit from providing a robust rendering callback that integrates well with the rest of the system. Instead, if the layer is not CAMetalLayer, we create a new sublayer, and render to that instead. [wantsLayer]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
1 parent 28be38c commit c028c8a

File tree

3 files changed

+176
-72
lines changed

3 files changed

+176
-72
lines changed

wgpu-hal/src/metal/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ impl crate::Instance for Instance {
102102
#[cfg(target_os = "ios")]
103103
raw_window_handle::RawWindowHandle::UiKit(handle) => {
104104
let _ = &self.managed_metal_layer_delegate;
105-
Ok(unsafe { Surface::from_view(handle.ui_view.as_ptr(), None) })
105+
Ok(unsafe { Surface::from_view(handle.ui_view.cast(), None) })
106106
}
107107
#[cfg(target_os = "macos")]
108108
raw_window_handle::RawWindowHandle::AppKit(handle) => Ok(unsafe {
109109
Surface::from_view(
110-
handle.ns_view.as_ptr(),
110+
handle.ns_view.cast(),
111111
Some(&self.managed_metal_layer_delegate),
112112
)
113113
}),

wgpu-hal/src/metal/surface.rs

Lines changed: 169 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
#![allow(clippy::let_unit_value)] // `let () =` being used to constrain result type
22

3-
use std::{os::raw::c_void, ptr::NonNull, sync::Once, thread};
3+
use std::ffi::c_uint;
4+
use std::ptr::NonNull;
5+
use std::sync::Once;
6+
use std::thread;
47

58
use core_graphics_types::{
69
base::CGFloat,
710
geometry::{CGRect, CGSize},
811
};
12+
use metal::foreign_types::ForeignType;
913
use objc::{
1014
class,
1115
declare::ClassDecl,
@@ -16,7 +20,6 @@ use objc::{
1620
};
1721
use parking_lot::{Mutex, RwLock};
1822

19-
#[cfg(target_os = "macos")]
2023
#[link(name = "QuartzCore", kind = "framework")]
2124
extern "C" {
2225
#[allow(non_upper_case_globals)]
@@ -47,6 +50,7 @@ impl HalManagedMetalLayerDelegate {
4750
let mut decl = ClassDecl::new(&class_name, class!(NSObject)).unwrap();
4851
#[allow(trivial_casts)] // false positive
4952
unsafe {
53+
// <https://developer.apple.com/documentation/appkit/nsviewlayercontentscaledelegate/3005294-layer?language=objc>
5054
decl.add_class_method(
5155
sel!(layer:shouldInheritContentsScale:fromWindow:),
5256
layer_should_inherit_contents_scale_from_window as Fun,
@@ -73,26 +77,15 @@ impl super::Surface {
7377
/// If not called on the main thread, this will panic.
7478
#[allow(clippy::transmute_ptr_to_ref)]
7579
pub unsafe fn from_view(
76-
view: *mut c_void,
80+
view: NonNull<Object>,
7781
delegate: Option<&HalManagedMetalLayerDelegate>,
7882
) -> Self {
79-
let view = view.cast::<Object>();
80-
let render_layer = {
81-
let layer = unsafe { Self::get_metal_layer(view, delegate) };
82-
let layer = layer.cast::<metal::MetalLayerRef>();
83-
// SAFETY: This pointer…
84-
//
85-
// - …is properly aligned.
86-
// - …is dereferenceable to a `MetalLayerRef` as an invariant of the `metal`
87-
// field.
88-
// - …points to an _initialized_ `MetalLayerRef`.
89-
// - …is only ever aliased via an immutable reference that lives within this
90-
// lexical scope.
91-
unsafe { &*layer }
92-
}
93-
.to_owned();
94-
let _: *mut c_void = msg_send![view, retain];
95-
Self::new(NonNull::new(view), render_layer)
83+
let layer = unsafe { Self::get_metal_layer(view, delegate) };
84+
// SAFETY: The layer is an initialized instance of `CAMetalLayer`.
85+
let layer = unsafe { metal::MetalLayer::from_ptr(layer.cast()) };
86+
let view: *mut Object = msg_send![view.as_ptr(), retain];
87+
let view = NonNull::new(view).expect("retain should return the same object");
88+
Self::new(Some(view), layer)
9689
}
9790

9891
pub unsafe fn from_layer(layer: &metal::MetalLayerRef) -> Self {
@@ -102,52 +95,155 @@ impl super::Surface {
10295
Self::new(None, layer.to_owned())
10396
}
10497

105-
/// If not called on the main thread, this will panic.
98+
/// Get or create a new `CAMetalLayer` associated with the given `NSView`
99+
/// or `UIView`.
100+
///
101+
/// # Panics
102+
///
103+
/// If called from a thread that is not the main thread, this will panic.
104+
///
105+
/// # Safety
106+
///
107+
/// The `view` must be a valid instance of `NSView` or `UIView`.
106108
pub(crate) unsafe fn get_metal_layer(
107-
view: *mut Object,
109+
view: NonNull<Object>,
108110
delegate: Option<&HalManagedMetalLayerDelegate>,
109111
) -> *mut Object {
110-
if view.is_null() {
111-
panic!("window does not have a valid contentView");
112-
}
113-
114112
let is_main_thread: BOOL = msg_send![class!(NSThread), isMainThread];
115113
if is_main_thread == NO {
116114
panic!("get_metal_layer cannot be called in non-ui thread.");
117115
}
118116

119-
let main_layer: *mut Object = msg_send![view, layer];
120-
let class = class!(CAMetalLayer);
121-
let is_valid_layer: BOOL = msg_send![main_layer, isKindOfClass: class];
117+
// Ensure that the view is layer-backed.
118+
// Views are always layer-backed in UIKit.
119+
#[cfg(target_os = "macos")]
120+
let () = msg_send![view.as_ptr(), setWantsLayer: YES];
121+
122+
let root_layer: *mut Object = msg_send![view.as_ptr(), layer];
123+
// `-[NSView layer]` can return `NULL`, while `-[UIView layer]` should
124+
// always be available.
125+
assert!(!root_layer.is_null(), "failed making the view layer-backed");
126+
127+
// NOTE: We explicitly do not touch properties such as
128+
// `layerContentsPlacement`, `needsDisplayOnBoundsChange` and
129+
// `contentsGravity` etc. on the root layer, both since we would like
130+
// to give the user full control over them, and because the default
131+
// values suit us pretty well (especially the contents placement being
132+
// `NSViewLayerContentsRedrawDuringViewResize`, which allows the view
133+
// to receive `drawRect:`/`updateLayer` calls).
122134

123-
if is_valid_layer == YES {
124-
main_layer
135+
let is_metal_layer: BOOL = msg_send![root_layer, isKindOfClass: class!(CAMetalLayer)];
136+
if is_metal_layer == YES {
137+
// The view has a `CAMetalLayer` as the root layer, which can
138+
// happen for example if user overwrote `-[NSView layerClass]` or
139+
// the view is `MTKView`.
140+
//
141+
// This is easily handled: We take "ownership" over the layer, and
142+
// render directly into that; after all, the user passed a view
143+
// with an explicit Metal layer to us, so this is very likely what
144+
// they expect us to do.
145+
root_layer
125146
} else {
126-
// If the main layer is not a CAMetalLayer, we create a CAMetalLayer and use it.
127-
let new_layer: *mut Object = msg_send![class, new];
128-
let frame: CGRect = msg_send![main_layer, bounds];
147+
// The view does not have a `CAMetalLayer` as the root layer (this
148+
// is the default for most views).
149+
//
150+
// This case is trickier! We cannot use the existing layer with
151+
// Metal, so we must do something else. There are a few options:
152+
//
153+
// 1. Panic here, and require the user to pass a view with a
154+
// `CAMetalLayer` layer.
155+
//
156+
// While this would "work", it doesn't solve the problem, and
157+
// instead passes the ball onwards to the user and ecosystem to
158+
// figure it out.
159+
//
160+
// 2. Override the existing layer with a newly created layer.
161+
//
162+
// If we overlook that this does not work in UIKit since
163+
// `UIView`'s `layer` is `readonly`, and that as such we will
164+
// need to do something different there anyhow, this is
165+
// actually a fairly good solution, and was what the original
166+
// implementation did.
167+
//
168+
// It has some problems though, due to:
169+
//
170+
// a. `wgpu` in our API design choosing not to register a
171+
// callback with `-[CALayerDelegate displayLayer:]`, but
172+
// instead leaves it up to the user to figure out when to
173+
// redraw. That is, we rely on other libraries' callbacks
174+
// telling us when to render.
175+
//
176+
// (If this were an API only for Metal, we would probably
177+
// make the user provide a `render` closure that we'd call
178+
// in the right situations. But alas, we have to be
179+
// cross-platform here).
180+
//
181+
// b. Overwriting the `layer` on `NSView` makes the view
182+
// "layer-hosting", see [wantsLayer], which disables drawing
183+
// functionality on the view like `drawRect:`/`updateLayer`.
184+
//
185+
// These two in combination makes it basically impossible for
186+
// crates like Winit to provide a robust rendering callback
187+
// that integrates with the system's built-in mechanisms for
188+
// redrawing, exactly because overwriting the layer would be
189+
// implicitly disabling those mechanisms!
190+
//
191+
// [wantsLayer]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
192+
//
193+
// 3. Create a sublayer.
194+
//
195+
// `CALayer` has the concept of "sublayers", which we can use
196+
// instead of overriding the layer.
197+
//
198+
// This is also the recommended solution on UIKit, so it's nice
199+
// that we can use (almost) the same implementation for these.
200+
//
201+
// It _might_, however, perform ever so slightly worse than
202+
// overriding the layer directly.
203+
//
204+
// 4. Create a new `MTKView` (or a custom view), and add it as a
205+
// subview.
206+
//
207+
// Similar to creating a sublayer (see above), but also
208+
// provides a bunch of event handling that we don't need.
209+
//
210+
// Option 3 seems like the most robust solution, so this is what
211+
// we're going to do.
212+
213+
// Create a new sublayer.
214+
let new_layer: *mut Object = msg_send![class!(CAMetalLayer), new];
215+
let () = msg_send![root_layer, addSublayer: new_layer];
216+
217+
// Automatically resize the sublayer's frame to match the
218+
// superlayer's bounds.
219+
//
220+
// Note that there is a somewhat hidden design decision in this:
221+
// We define the `width` and `height` in `configure` to control
222+
// the `drawableSize` of the layer, while `bounds` and `frame` are
223+
// outside of the user's direct control - instead, though, they
224+
// can control the size of the view (or root layer), and get the
225+
// desired effect that way.
226+
//
227+
// We _could_ also let `configure` set the `bounds` size, however
228+
// that would be inconsistent with using the root layer directly
229+
// (as we may do, see above).
230+
let width_sizable = 1 << 1; // kCALayerWidthSizable
231+
let height_sizable = 1 << 4; // kCALayerHeightSizable
232+
let mask: c_uint = width_sizable | height_sizable;
233+
let () = msg_send![new_layer, setAutoresizingMask: mask];
234+
235+
// Specify the relative size that the auto resizing mask above
236+
// will keep (i.e. tell it to fill out its superlayer).
237+
let frame: CGRect = msg_send![root_layer, bounds];
129238
let () = msg_send![new_layer, setFrame: frame];
130-
#[cfg(target_os = "ios")]
131-
{
132-
// Unlike NSView, UIView does not allow to replace main layer.
133-
let () = msg_send![main_layer, addSublayer: new_layer];
134-
// On iOS, "from_view" may be called before the application initialization is complete,
135-
// `msg_send![view, window]` and `msg_send![window, screen]` will get null.
136-
let screen: *mut Object = msg_send![class!(UIScreen), mainScreen];
137-
let scale_factor: CGFloat = msg_send![screen, nativeScale];
138-
let () = msg_send![view, setContentScaleFactor: scale_factor];
139-
};
140-
#[cfg(target_os = "macos")]
141-
{
142-
let () = msg_send![view, setLayer: new_layer];
143-
let () = msg_send![view, setWantsLayer: YES];
144-
let () = msg_send![new_layer, setContentsGravity: unsafe { kCAGravityTopLeft }];
145-
let window: *mut Object = msg_send![view, window];
146-
if !window.is_null() {
147-
let scale_factor: CGFloat = msg_send![window, backingScaleFactor];
148-
let () = msg_send![new_layer, setContentsScale: scale_factor];
149-
}
150-
};
239+
240+
let _: () = msg_send![new_layer, setContentsGravity: unsafe { kCAGravityTopLeft }];
241+
242+
// Set initial scale factor of the layer. This is kept in sync by
243+
// `configure` (on UIKit), and the delegate below (on AppKit).
244+
let scale_factor: CGFloat = msg_send![root_layer, contentsScale];
245+
let () = msg_send![new_layer, setContentsScale: scale_factor];
246+
151247
if let Some(delegate) = delegate {
152248
let () = msg_send![new_layer, setDelegate: delegate.0];
153249
}
@@ -211,19 +307,28 @@ impl crate::Surface for super::Surface {
211307
_ => (),
212308
}
213309

214-
let device_raw = device.shared.device.lock();
215-
// On iOS, unless the user supplies a view with a CAMetalLayer, we
216-
// create one as a sublayer. However, when the view changes size,
217-
// its sublayers are not automatically resized, and we must resize
218-
// it here. The drawable size and the layer size don't correlate
219-
#[cfg(target_os = "ios")]
310+
// AppKit / UIKit automatically sets the correct scale factor for
311+
// layers attached to a view. Our layer, however, may not be directly
312+
// attached to the view; in those cases, we need to set the scale
313+
// factor ourselves.
314+
//
315+
// For AppKit, we do so by adding a delegate on the layer with the
316+
// `layer:shouldInheritContentsScale:fromWindow:` method returning
317+
// `true` - this tells the system to automatically update the scale
318+
// factor when it changes.
319+
//
320+
// For UIKit, we manually update the scale factor here.
321+
//
322+
// TODO: Is there a way that we could listen to such changes instead?
323+
#[cfg(not(target_os = "macos"))]
220324
{
221325
if let Some(view) = self.view {
222-
let main_layer: *mut Object = msg_send![view.as_ptr(), layer];
223-
let bounds: CGRect = msg_send![main_layer, bounds];
224-
let () = msg_send![*render_layer, setFrame: bounds];
326+
let scale_factor: CGFloat = msg_send![view.as_ptr(), contentScaleFactor];
327+
let () = msg_send![render_layer.as_ptr(), setContentsScale: scale_factor];
225328
}
226329
}
330+
331+
let device_raw = device.shared.device.lock();
227332
render_layer.set_device(&device_raw);
228333
render_layer.set_pixel_format(caps.map_format(config.format));
229334
render_layer.set_framebuffer_only(framebuffer_only);

wgpu-hal/src/vulkan/instance.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{
22
ffi::{c_void, CStr, CString},
3+
ptr::NonNull,
34
slice,
45
str::FromStr,
56
sync::Arc,
@@ -506,17 +507,15 @@ impl super::Instance {
506507
#[cfg(metal)]
507508
fn create_surface_from_view(
508509
&self,
509-
view: *mut c_void,
510+
view: NonNull<c_void>,
510511
) -> Result<super::Surface, crate::InstanceError> {
511512
if !self.shared.extensions.contains(&ext::metal_surface::NAME) {
512513
return Err(crate::InstanceError::new(String::from(
513514
"Vulkan driver does not support VK_EXT_metal_surface",
514515
)));
515516
}
516517

517-
let layer = unsafe {
518-
crate::metal::Surface::get_metal_layer(view.cast::<objc::runtime::Object>(), None)
519-
};
518+
let layer = unsafe { crate::metal::Surface::get_metal_layer(view.cast(), None) };
520519

521520
let surface = {
522521
let metal_loader =
@@ -866,13 +865,13 @@ impl crate::Instance for super::Instance {
866865
(Rwh::AppKit(handle), _)
867866
if self.shared.extensions.contains(&ext::metal_surface::NAME) =>
868867
{
869-
self.create_surface_from_view(handle.ns_view.as_ptr())
868+
self.create_surface_from_view(handle.ns_view)
870869
}
871870
#[cfg(all(target_os = "ios", feature = "metal"))]
872871
(Rwh::UiKit(handle), _)
873872
if self.shared.extensions.contains(&ext::metal_surface::NAME) =>
874873
{
875-
self.create_surface_from_view(handle.ui_view.as_ptr())
874+
self.create_surface_from_view(handle.ui_view)
876875
}
877876
(_, _) => Err(crate::InstanceError::new(format!(
878877
"window handle {window_handle:?} is not a Vulkan-compatible handle"

0 commit comments

Comments
 (0)