From 9d04bcc0c8ea1b72cdb38cbf5ad561dbdec17a87 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Wed, 4 Sep 2024 13:16:23 +0200 Subject: [PATCH] Use raw-window-metal to do Metal layer creation This uses observers internally, which fixes resizing when using Wgpu together with an auto-layout NSView. Though it probably won't matter much, it's also more reliable to update the scale factor this way, rather than haphazardly in `configure`. --- Cargo.lock | 84 ++++++++++- Cargo.toml | 1 + wgpu-hal/Cargo.toml | 5 + wgpu-hal/src/metal/mod.rs | 34 +++-- wgpu-hal/src/metal/surface.rs | 258 +------------------------------- wgpu-hal/src/vulkan/instance.rs | 19 ++- 6 files changed, 119 insertions(+), 282 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17bbe72b556..dbe0285f05f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -365,7 +365,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" dependencies = [ "block-sys", - "objc2", + "objc2 0.4.1", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", ] [[package]] @@ -1808,9 +1817,9 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" dependencies = [ - "block2", + "block2 0.3.0", "dispatch", - "objc2", + "objc2 0.4.1", ] [[package]] @@ -2010,7 +2019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2531,7 +2540,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" dependencies = [ "objc-sys", - "objc2-encode", + "objc2-encode 3.0.0", +] + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode 4.0.3", ] [[package]] @@ -2540,6 +2559,49 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "object" version = "0.36.3" @@ -2921,6 +2983,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "0.4.0" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation", + "objc2-quartz-core", +] + [[package]] name = "rayon" version = "1.10.0" @@ -4372,6 +4443,7 @@ dependencies = [ "profiling", "range-alloc", "raw-window-handle 0.6.2", + "raw-window-metal", "renderdoc-sys", "rustc-hash", "smallvec", @@ -4879,7 +4951,7 @@ dependencies = [ "memmap2 0.9.4", "ndk 0.8.0", "ndk-sys 0.5.0+25.2.9519653", - "objc2", + "objc2 0.4.1", "once_cell", "orbclient", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index fe3bc0b37c2..c3f96f45571 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ block = "0.1" core-graphics-types = "0.1" metal = { version = "0.29.0" } objc = "0.2.5" +raw-window-metal = { path = "../raw-window-metal" } # Vulkan dependencies android_system_properties = "0.1.1" diff --git a/wgpu-hal/Cargo.toml b/wgpu-hal/Cargo.toml index ee2808b19d5..36b4079d664 100644 --- a/wgpu-hal/Cargo.toml +++ b/wgpu-hal/Cargo.toml @@ -44,6 +44,7 @@ metal = [ # Metal is only available on Apple platforms, therefore request MSL output also only if we target an Apple platform. "naga/msl-out-if-target-apple", "dep:block", + "dep:raw-window-metal", ] vulkan = [ "naga/spv-out", @@ -53,6 +54,7 @@ vulkan = [ "dep:libloading", "dep:smallvec", "dep:android_system_properties", + "dep:raw-window-metal", ] gles = [ "naga/glsl-out", @@ -167,6 +169,9 @@ glutin_wgl_sys = { workspace = true, optional = true } # backend: Metal block = { workspace = true, optional = true } +# backend: Metal + Vulkan +raw-window-metal = { workspace = true, optional = true } + metal.workspace = true objc.workspace = true core-graphics-types.workspace = true diff --git a/wgpu-hal/src/metal/mod.rs b/wgpu-hal/src/metal/mod.rs index 1935e843ec6..0e403bb8fe5 100644 --- a/wgpu-hal/src/metal/mod.rs +++ b/wgpu-hal/src/metal/mod.rs @@ -34,7 +34,7 @@ use std::{ use arrayvec::ArrayVec; use bitflags::bitflags; -use metal::foreign_types::ForeignTypeRef as _; +use metal::foreign_types::{ForeignType as _, ForeignTypeRef as _}; use parking_lot::{Mutex, RwLock}; #[derive(Clone, Debug)] @@ -100,7 +100,7 @@ pub struct Instance {} impl Instance { pub fn create_surface_from_layer(&self, layer: &metal::MetalLayerRef) -> Surface { - unsafe { Surface::from_layer(layer) } + Surface::from_layer(layer) } } @@ -119,19 +119,25 @@ impl crate::Instance for Instance { _display_handle: raw_window_handle::RawDisplayHandle, window_handle: raw_window_handle::RawWindowHandle, ) -> Result { - match window_handle { - #[cfg(target_os = "ios")] - raw_window_handle::RawWindowHandle::UiKit(handle) => { - Ok(unsafe { Surface::from_view(handle.ui_view.cast()) }) + let layer = match window_handle { + raw_window_handle::RawWindowHandle::AppKit(handle) => unsafe { + raw_window_metal::Layer::from_ns_view(handle.ns_view) + }, + raw_window_handle::RawWindowHandle::UiKit(handle) => unsafe { + raw_window_metal::Layer::from_ui_view(handle.ui_view) + }, + _ => { + return Err(crate::InstanceError::new(format!( + "window handle {window_handle:?} is not a Metal-compatible handle" + ))) } - #[cfg(target_os = "macos")] - raw_window_handle::RawWindowHandle::AppKit(handle) => { - Ok(unsafe { Surface::from_view(handle.ns_view.cast()) }) - } - _ => Err(crate::InstanceError::new(format!( - "window handle {window_handle:?} is not a Metal-compatible handle" - ))), - } + }; + + // SAFETY: The layer is an initialized instance of `CAMetalLayer`, and + // we transfer the retain count to `MetalLayer` using `into_raw`. + let layer = unsafe { metal::MetalLayer::from_ptr(layer.into_raw().cast()) }; + + Ok(Surface::new(layer)) } unsafe fn enumerate_adapters( diff --git a/wgpu-hal/src/metal/surface.rs b/wgpu-hal/src/metal/surface.rs index 668b602474d..4f348144124 100644 --- a/wgpu-hal/src/metal/surface.rs +++ b/wgpu-hal/src/metal/surface.rs @@ -1,69 +1,21 @@ #![allow(clippy::let_unit_value)] // `let () =` being used to constrain result type -use std::ffi::c_uint; -use std::mem::ManuallyDrop; -use std::ptr::NonNull; -use std::sync::Once; use std::thread; use core_graphics_types::{ base::CGFloat, geometry::{CGRect, CGSize}, }; -use metal::foreign_types::ForeignType; use objc::{ - class, - declare::ClassDecl, - msg_send, - rc::{autoreleasepool, StrongPtr}, - runtime::{Class, Object, Sel, BOOL, NO, YES}, + class, msg_send, + rc::autoreleasepool, + runtime::{BOOL, YES}, sel, sel_impl, }; use parking_lot::{Mutex, RwLock}; -#[link(name = "QuartzCore", kind = "framework")] -extern "C" { - #[allow(non_upper_case_globals)] - static kCAGravityResize: *mut Object; -} - -extern "C" fn layer_should_inherit_contents_scale_from_window( - _: &Class, - _: Sel, - _layer: *mut Object, - _new_scale: CGFloat, - _from_window: *mut Object, -) -> BOOL { - YES -} - -static CAML_DELEGATE_REGISTER: Once = Once::new(); - -#[derive(Debug)] -pub struct HalManagedMetalLayerDelegate(&'static Class); - -impl HalManagedMetalLayerDelegate { - pub fn new() -> Self { - let class_name = format!("HalManagedMetalLayerDelegate@{:p}", &CAML_DELEGATE_REGISTER); - - CAML_DELEGATE_REGISTER.call_once(|| { - type Fun = extern "C" fn(&Class, Sel, *mut Object, CGFloat, *mut Object) -> BOOL; - let mut decl = ClassDecl::new(&class_name, class!(NSObject)).unwrap(); - unsafe { - // - decl.add_class_method::( - sel!(layer:shouldInheritContentsScale:fromWindow:), - layer_should_inherit_contents_scale_from_window, - ); - } - decl.register(); - }); - Self(Class::get(&class_name).unwrap()) - } -} - impl super::Surface { - fn new(layer: metal::MetalLayer) -> Self { + pub fn new(layer: metal::MetalLayer) -> Self { Self { render_layer: Mutex::new(layer), swapchain_format: RwLock::new(None), @@ -73,188 +25,13 @@ impl super::Surface { } } - /// If not called on the main thread, this will panic. - #[allow(clippy::transmute_ptr_to_ref)] - pub unsafe fn from_view(view: NonNull) -> Self { - let layer = unsafe { Self::get_metal_layer(view) }; - let layer = ManuallyDrop::new(layer); - // SAFETY: The layer is an initialized instance of `CAMetalLayer`, and - // we transfer the retain count to `MetalLayer` using `ManuallyDrop`. - let layer = unsafe { metal::MetalLayer::from_ptr(layer.cast()) }; - Self::new(layer) - } - - pub unsafe fn from_layer(layer: &metal::MetalLayerRef) -> Self { + pub fn from_layer(layer: &metal::MetalLayerRef) -> Self { let class = class!(CAMetalLayer); - let proper_kind: BOOL = msg_send![layer, isKindOfClass: class]; + let proper_kind: BOOL = unsafe { msg_send![layer, isKindOfClass: class] }; assert_eq!(proper_kind, YES); Self::new(layer.to_owned()) } - /// Get or create a new `CAMetalLayer` associated with the given `NSView` - /// or `UIView`. - /// - /// # Panics - /// - /// If called from a thread that is not the main thread, this will panic. - /// - /// # Safety - /// - /// The `view` must be a valid instance of `NSView` or `UIView`. - pub(crate) unsafe fn get_metal_layer(view: NonNull) -> StrongPtr { - let is_main_thread: BOOL = msg_send![class!(NSThread), isMainThread]; - if is_main_thread == NO { - panic!("get_metal_layer cannot be called in non-ui thread."); - } - - // Ensure that the view is layer-backed. - // Views are always layer-backed in UIKit. - #[cfg(target_os = "macos")] - let () = msg_send![view.as_ptr(), setWantsLayer: YES]; - - let root_layer: *mut Object = msg_send![view.as_ptr(), layer]; - // `-[NSView layer]` can return `NULL`, while `-[UIView layer]` should - // always be available. - assert!(!root_layer.is_null(), "failed making the view layer-backed"); - - // NOTE: We explicitly do not touch properties such as - // `layerContentsPlacement`, `needsDisplayOnBoundsChange` and - // `contentsGravity` etc. on the root layer, both since we would like - // to give the user full control over them, and because the default - // values suit us pretty well (especially the contents placement being - // `NSViewLayerContentsRedrawDuringViewResize`, which allows the view - // to receive `drawRect:`/`updateLayer` calls). - - let is_metal_layer: BOOL = msg_send![root_layer, isKindOfClass: class!(CAMetalLayer)]; - if is_metal_layer == YES { - // The view has a `CAMetalLayer` as the root layer, which can - // happen for example if user overwrote `-[NSView layerClass]` or - // the view is `MTKView`. - // - // This is easily handled: We take "ownership" over the layer, and - // render directly into that; after all, the user passed a view - // with an explicit Metal layer to us, so this is very likely what - // they expect us to do. - unsafe { StrongPtr::retain(root_layer) } - } else { - // The view does not have a `CAMetalLayer` as the root layer (this - // is the default for most views). - // - // This case is trickier! We cannot use the existing layer with - // Metal, so we must do something else. There are a few options: - // - // 1. Panic here, and require the user to pass a view with a - // `CAMetalLayer` layer. - // - // While this would "work", it doesn't solve the problem, and - // instead passes the ball onwards to the user and ecosystem to - // figure it out. - // - // 2. Override the existing layer with a newly created layer. - // - // If we overlook that this does not work in UIKit since - // `UIView`'s `layer` is `readonly`, and that as such we will - // need to do something different there anyhow, this is - // actually a fairly good solution, and was what the original - // implementation did. - // - // It has some problems though, due to: - // - // a. `wgpu` in our API design choosing not to register a - // callback with `-[CALayerDelegate displayLayer:]`, but - // instead leaves it up to the user to figure out when to - // redraw. That is, we rely on other libraries' callbacks - // telling us when to render. - // - // (If this were an API only for Metal, we would probably - // make the user provide a `render` closure that we'd call - // in the right situations. But alas, we have to be - // cross-platform here). - // - // b. Overwriting the `layer` on `NSView` makes the view - // "layer-hosting", see [wantsLayer], which disables drawing - // functionality on the view like `drawRect:`/`updateLayer`. - // - // These two in combination makes it basically impossible for - // crates like Winit to provide a robust rendering callback - // that integrates with the system's built-in mechanisms for - // redrawing, exactly because overwriting the layer would be - // implicitly disabling those mechanisms! - // - // [wantsLayer]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc - // - // 3. Create a sublayer. - // - // `CALayer` has the concept of "sublayers", which we can use - // instead of overriding the layer. - // - // This is also the recommended solution on UIKit, so it's nice - // that we can use (almost) the same implementation for these. - // - // It _might_, however, perform ever so slightly worse than - // overriding the layer directly. - // - // 4. Create a new `MTKView` (or a custom view), and add it as a - // subview. - // - // Similar to creating a sublayer (see above), but also - // provides a bunch of event handling that we don't need. - // - // Option 3 seems like the most robust solution, so this is what - // we're going to do. - - // Create a new sublayer. - let new_layer: *mut Object = msg_send![class!(CAMetalLayer), new]; - let () = msg_send![root_layer, addSublayer: new_layer]; - - // Automatically resize the sublayer's frame to match the - // superlayer's bounds. - // - // Note that there is a somewhat hidden design decision in this: - // We define the `width` and `height` in `configure` to control - // the `drawableSize` of the layer, while `bounds` and `frame` are - // outside of the user's direct control - instead, though, they - // can control the size of the view (or root layer), and get the - // desired effect that way. - // - // We _could_ also let `configure` set the `bounds` size, however - // that would be inconsistent with using the root layer directly - // (as we may do, see above). - let width_sizable = 1 << 1; // kCALayerWidthSizable - let height_sizable = 1 << 4; // kCALayerHeightSizable - let mask: c_uint = width_sizable | height_sizable; - let () = msg_send![new_layer, setAutoresizingMask: mask]; - - // Specify the relative size that the auto resizing mask above - // will keep (i.e. tell it to fill out its superlayer). - let frame: CGRect = msg_send![root_layer, bounds]; - let () = msg_send![new_layer, setFrame: frame]; - - // The gravity to use when the layer's `drawableSize` isn't the - // same as the bounds rectangle. - // - // The desired content gravity is `kCAGravityResize`, because it - // masks / alleviates issues with resizing when - // `present_with_transaction` is disabled, and behaves better when - // moving the window between monitors. - // - // Unfortunately, it also makes it harder to see changes to - // `width` and `height` in `configure`. When debugging resize - // issues, swap this for `kCAGravityTopLeft` instead. - let _: () = msg_send![new_layer, setContentsGravity: unsafe { kCAGravityResize }]; - - // Set initial scale factor of the layer. This is kept in sync by - // `configure` (on UIKit), and the delegate below (on AppKit). - let scale_factor: CGFloat = msg_send![root_layer, contentsScale]; - let () = msg_send![new_layer, setContentsScale: scale_factor]; - - let delegate = HalManagedMetalLayerDelegate::new(); - let () = msg_send![new_layer, setDelegate: delegate.0]; - - unsafe { StrongPtr::new(new_layer) } - } - } - pub(super) fn dimensions(&self) -> wgt::Extent3d { let (size, scale): (CGSize, CGFloat) = unsafe { let render_layer_borrow = self.render_layer.lock(); @@ -301,29 +78,6 @@ impl crate::Surface for super::Surface { _ => (), } - // AppKit / UIKit automatically sets the correct scale factor for - // layers attached to a view. Our layer, however, may not be directly - // attached to a view; in those cases, we need to set the scale - // factor ourselves. - // - // For AppKit, we do so by adding a delegate on the layer with the - // `layer:shouldInheritContentsScale:fromWindow:` method returning - // `true` - this tells the system to automatically update the scale - // factor when it changes. - // - // For UIKit, we manually update the scale factor from the super layer - // here, if there is one. - // - // TODO: Is there a way that we could listen to such changes instead? - #[cfg(not(target_os = "macos"))] - { - let superlayer: *mut Object = msg_send![render_layer.as_ptr(), superlayer]; - if !superlayer.is_null() { - let scale_factor: CGFloat = msg_send![superlayer, contentsScale]; - let () = msg_send![render_layer.as_ptr(), setContentsScale: scale_factor]; - } - } - let device_raw = device.shared.device.lock(); render_layer.set_device(&device_raw); render_layer.set_pixel_format(caps.map_format(config.format)); diff --git a/wgpu-hal/src/vulkan/instance.rs b/wgpu-hal/src/vulkan/instance.rs index f52a055284c..bf29e1eaca1 100644 --- a/wgpu-hal/src/vulkan/instance.rs +++ b/wgpu-hal/src/vulkan/instance.rs @@ -508,9 +508,9 @@ impl super::Instance { } #[cfg(metal)] - fn create_surface_from_view( + fn create_surface_from_layer( &self, - view: std::ptr::NonNull, + layer: raw_window_metal::Layer, ) -> Result { if !self.shared.extensions.contains(&ext::metal_surface::NAME) { return Err(crate::InstanceError::new(String::from( @@ -518,17 +518,14 @@ impl super::Instance { ))); } - let layer = unsafe { crate::metal::Surface::get_metal_layer(view.cast()) }; // NOTE: The layer is retained by Vulkan's `vkCreateMetalSurfaceEXT`, // so no need to retain it beyond the scope of this function. - let layer_ptr = (*layer).cast(); - let surface = { let metal_loader = ext::metal_surface::Instance::new(&self.shared.entry, &self.shared.raw); let vk_info = vk::MetalSurfaceCreateInfoEXT::default() .flags(vk::MetalSurfaceCreateFlagsEXT::empty()) - .layer(layer_ptr); + .layer(layer.as_ptr()); unsafe { metal_loader.create_metal_surface(&vk_info, None).unwrap() } }; @@ -867,17 +864,19 @@ impl crate::Instance for super::Instance { })?; self.create_surface_from_hwnd(hinstance.get(), handle.hwnd.get()) } - #[cfg(all(target_os = "macos", feature = "metal"))] + #[cfg(target_vendor = "apple")] (Rwh::AppKit(handle), _) if self.shared.extensions.contains(&ext::metal_surface::NAME) => { - self.create_surface_from_view(handle.ns_view) + let layer = unsafe { raw_window_metal::Layer::from_ns_view(handle.ns_view) }; + self.create_surface_from_layer(layer) } - #[cfg(all(target_os = "ios", feature = "metal"))] + #[cfg(target_vendor = "apple")] (Rwh::UiKit(handle), _) if self.shared.extensions.contains(&ext::metal_surface::NAME) => { - self.create_surface_from_view(handle.ui_view) + let layer = unsafe { raw_window_metal::Layer::from_ui_view(handle.ui_view) }; + self.create_surface_from_layer(layer) } (_, _) => Err(crate::InstanceError::new(format!( "window handle {window_handle:?} is not a Vulkan-compatible handle"