Add layer observer based on raw-window-metal (#7026)

Co-authored-by: Connor Fitzgerald <connorwadefitzgerald@gmail.com>
This commit is contained in:
Mads Marquart
2025-02-20 22:30:58 +01:00
committed by GitHub
parent 48e2298ed9
commit 7b00140b16
4 changed files with 204 additions and 181 deletions

View File

@@ -190,6 +190,10 @@ By @brodycj in [#6924](https://github.com/gfx-rs/wgpu/pull/6924).
- Stop naga causing undefined behavior when a ray query misses. By @Vecvec in [#6752](https://github.com/gfx-rs/wgpu/pull/6752).
#### Metal
- Use resize observers for smoother resizing. By @madsmtm in [#7026](https://github.com/gfx-rs/wgpu/pull/7026).
#### Gles
- Support OpenHarmony render with `gles`. By @richerfu in [#7085](https://github.com/gfx-rs/wgpu/pull/7085)

View File

@@ -0,0 +1,190 @@
//! A rewrite of `raw-window-metal` using `objc` instead of `objc2`.
//!
//! See that for details: <https://docs.rs/raw-window-metal/1.1.0/>
//!
//! This should be temporary, see <https://github.com/gfx-rs/wgpu/pull/6210>.
use core::ffi::{c_void, CStr};
use core_graphics_types::base::CGFloat;
use core_graphics_types::geometry::CGRect;
use objc::declare::ClassDecl;
use objc::rc::StrongPtr;
use objc::runtime::{Class, Object, Sel, BOOL, NO};
use objc::{class, msg_send, sel, sel_impl};
use std::sync::OnceLock;
#[link(name = "Foundation", kind = "framework")]
extern "C" {
static NSKeyValueChangeNewKey: &'static Object;
}
#[allow(non_upper_case_globals)]
const NSKeyValueObservingOptionNew: usize = 0x01;
#[allow(non_upper_case_globals)]
const NSKeyValueObservingOptionInitial: usize = 0x04;
const CONTENTS_SCALE: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"contentsScale\0") };
const BOUNDS: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"bounds\0") };
/// Create a new custom layer that tracks parameters from the given super layer.
///
/// Same as <https://docs.rs/raw-window-metal/1.1.0/src/raw_window_metal/observer.rs.html#74-132>.
pub unsafe fn new_observer_layer(root_layer: *mut Object) -> StrongPtr {
let this: *mut Object = unsafe { msg_send![class(), new] };
// Add the layer as a sublayer of the root layer.
let _: () = unsafe { msg_send![root_layer, addSublayer: this] };
// Register for key-value observing.
let key_path: *const Object =
unsafe { msg_send![class!(NSString), stringWithUTF8String: CONTENTS_SCALE.as_ptr()] };
let _: () = unsafe {
msg_send![
root_layer,
addObserver: this
forKeyPath: key_path
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
context: context_ptr()
]
};
let key_path: *const Object =
unsafe { msg_send![class!(NSString), stringWithUTF8String: BOUNDS.as_ptr()] };
let _: () = unsafe {
msg_send![
root_layer,
addObserver: this
forKeyPath: key_path
options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
context: context_ptr()
]
};
// Uncomment when debugging resize issues.
// extern "C" {
// static kCAGravityTopLeft: *mut Object;
// }
// let _: () = unsafe { msg_send![this, setContentsGravity: kCAGravityTopLeft] };
unsafe { StrongPtr::new(this) }
}
/// Same as <https://docs.rs/raw-window-metal/1.1.0/src/raw_window_metal/observer.rs.html#74-132>.
fn class() -> &'static Class {
static CLASS: OnceLock<&'static Class> = OnceLock::new();
CLASS.get_or_init(|| {
let superclass = class!(CAMetalLayer);
let class_name = format!("WgpuObserverLayer@{:p}", &CLASS);
let mut decl = ClassDecl::new(&class_name, superclass).unwrap();
// From NSKeyValueObserving.
let sel = sel!(observeValueForKeyPath:ofObject:change:context:);
let method: extern "C" fn(
&Object,
Sel,
*mut Object,
*mut Object,
*mut Object,
*mut c_void,
) = observe_value;
unsafe { decl.add_method(sel, method) };
let sel = sel!(dealloc);
let method: extern "C" fn(&Object, Sel) = dealloc;
unsafe { decl.add_method(sel, method) };
decl.register()
})
}
/// The unique context pointer for this class.
fn context_ptr() -> *mut c_void {
let ptr: *const Class = class();
ptr.cast_mut().cast()
}
/// Same as <https://docs.rs/raw-window-metal/1.1.0/src/raw_window_metal/observer.rs.html#74-132>.
extern "C" fn observe_value(
this: &Object,
_cmd: Sel,
key_path: *mut Object,
object: *mut Object,
change: *mut Object,
context: *mut c_void,
) {
// An unrecognized context must belong to the super class.
if context != context_ptr() {
// SAFETY: The signature is correct, and it's safe to forward to
// the superclass' method when we're overriding the method.
return unsafe {
msg_send![
super(this, class!(CAMetalLayer)),
observeValueForKeyPath: key_path
ofObject: object
change: change
context: context
]
};
}
assert!(!change.is_null());
let key = unsafe { NSKeyValueChangeNewKey };
let new: *mut Object = unsafe { msg_send![change, objectForKey: key] };
assert!(!new.is_null());
let to_compare: *const Object =
unsafe { msg_send![class!(NSString), stringWithUTF8String: CONTENTS_SCALE.as_ptr()] };
let is_equal: BOOL = unsafe { msg_send![key_path, isEqual: to_compare] };
if is_equal != NO {
// `contentsScale` is a CGFloat, and so the observed value is always a NSNumber.
let scale_factor: CGFloat = if cfg!(target_pointer_width = "64") {
unsafe { msg_send![new, doubleValue] }
} else {
unsafe { msg_send![new, floatValue] }
};
// Set the scale factor of the layer to match the root layer.
let _: () = unsafe { msg_send![this, setContentsScale: scale_factor] };
return;
}
let to_compare: *const Object =
unsafe { msg_send![class!(NSString), stringWithUTF8String: BOUNDS.as_ptr()] };
let is_equal: BOOL = unsafe { msg_send![key_path, isEqual: to_compare] };
if is_equal != NO {
// `bounds` is a CGRect, and so the observed value is always a NSNumber.
let bounds: CGRect = unsafe { msg_send![new, rectValue] };
// Set `bounds` and `position` to match the root layer.
//
// This differs from just setting the `bounds`, as it also takes into account any
// translation that the superlayer may have that we'd want to preserve.
let _: () = unsafe { msg_send![this, setFrame: bounds] };
return;
}
panic!("unknown observed keypath {key_path:?}");
}
extern "C" fn dealloc(this: &Object, _cmd: Sel) {
// Load the root layer if it still exists, and deregister the observer.
//
// This is not entirely sound, as the ObserverLayer _could_ have been
// moved to another layer; but Wgpu doesn't do that, so it should be fine.
//
// `raw-window-metal` uses a weak instance variable to do it correctly:
// https://docs.rs/raw-window-metal/1.1.0/src/raw_window_metal/observer.rs.html#74-132
// (but that's difficult to do with `objc`).
let root_layer: *mut Object = unsafe { msg_send![this, superlayer] };
if !root_layer.is_null() {
let key_path: *const Object =
unsafe { msg_send![class!(NSString), stringWithUTF8String: CONTENTS_SCALE.as_ptr()] };
let _: () = unsafe { msg_send![root_layer, removeObserver: this forKeyPath: key_path] };
let key_path: *const Object =
unsafe { msg_send![class!(NSString), stringWithUTF8String: BOUNDS.as_ptr()] };
let _: () = unsafe { msg_send![root_layer, removeObserver: this forKeyPath: key_path] };
}
}

View File

@@ -22,6 +22,7 @@ mod adapter;
mod command;
mod conv;
mod device;
mod layer_observer;
mod surface;
mod time;

View File

@@ -2,7 +2,6 @@
use std::mem::ManuallyDrop;
use std::ptr::NonNull;
use std::sync::Once;
use std::thread;
use core_graphics_types::{
@@ -11,55 +10,17 @@ use core_graphics_types::{
};
use metal::foreign_types::ForeignType;
use objc::{
class,
declare::ClassDecl,
msg_send,
class, msg_send,
rc::{autoreleasepool, StrongPtr},
runtime::{Class, Object, Sel, BOOL, NO, YES},
runtime::{Object, BOOL, NO, YES},
sel, sel_impl,
};
use parking_lot::{Mutex, RwLock};
use crate::metal::layer_observer::new_observer_layer;
#[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 {
// <https://developer.apple.com/documentation/appkit/nsviewlayercontentscaledelegate/3005294-layer?language=objc>
decl.add_class_method::<Fun>(
sel!(layer:shouldInheritContentsScale:fromWindow:),
layer_should_inherit_contents_scale_from_window,
);
}
decl.register();
});
Self(Class::get(&class_name).unwrap())
}
}
extern "C" {}
impl super::Surface {
fn new(layer: metal::MetalLayer) -> Self {
@@ -140,120 +101,10 @@ impl super::Surface {
// 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];
#[cfg(target_os = "macos")]
{
// 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: std::ffi::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) }
// Metal, so we must do something else. There are a few options,
// we do the same as outlined in:
// https://docs.rs/raw-window-metal/1.1.0/raw_window_metal/#reasoning-behind-creating-a-sublayer
unsafe { new_observer_layer(root_layer) }
}
}
@@ -303,29 +154,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));