1
1
#![ allow( clippy:: let_unit_value) ] // `let () =` being used to constrain result type
2
2
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;
4
7
5
8
use core_graphics_types:: {
6
9
base:: CGFloat ,
7
10
geometry:: { CGRect , CGSize } ,
8
11
} ;
12
+ use metal:: foreign_types:: ForeignType ;
9
13
use objc:: {
10
14
class,
11
15
declare:: ClassDecl ,
@@ -16,7 +20,6 @@ use objc::{
16
20
} ;
17
21
use parking_lot:: { Mutex , RwLock } ;
18
22
19
- #[ cfg( target_os = "macos" ) ]
20
23
#[ link( name = "QuartzCore" , kind = "framework" ) ]
21
24
extern "C" {
22
25
#[ allow( non_upper_case_globals) ]
@@ -46,6 +49,7 @@ impl HalManagedMetalLayerDelegate {
46
49
type Fun = extern "C" fn ( & Class , Sel , * mut Object , CGFloat , * mut Object ) -> BOOL ;
47
50
let mut decl = ClassDecl :: new ( & class_name, class ! ( NSObject ) ) . unwrap ( ) ;
48
51
unsafe {
52
+ // <https://developer.apple.com/documentation/appkit/nsviewlayercontentscaledelegate/3005294-layer?language=objc>
49
53
decl. add_class_method :: < Fun > (
50
54
sel ! ( layer: shouldInheritContentsScale: fromWindow: ) ,
51
55
layer_should_inherit_contents_scale_from_window,
@@ -72,26 +76,15 @@ impl super::Surface {
72
76
/// If not called on the main thread, this will panic.
73
77
#[ allow( clippy:: transmute_ptr_to_ref) ]
74
78
pub unsafe fn from_view (
75
- view : * mut c_void ,
79
+ view : NonNull < Object > ,
76
80
delegate : Option < & HalManagedMetalLayerDelegate > ,
77
81
) -> Self {
78
- let view = view. cast :: < Object > ( ) ;
79
- let render_layer = {
80
- let layer = unsafe { Self :: get_metal_layer ( view, delegate) } ;
81
- let layer = layer. cast :: < metal:: MetalLayerRef > ( ) ;
82
- // SAFETY: This pointer…
83
- //
84
- // - …is properly aligned.
85
- // - …is dereferenceable to a `MetalLayerRef` as an invariant of the `metal`
86
- // field.
87
- // - …points to an _initialized_ `MetalLayerRef`.
88
- // - …is only ever aliased via an immutable reference that lives within this
89
- // lexical scope.
90
- unsafe { & * layer }
91
- }
92
- . to_owned ( ) ;
93
- let _: * mut c_void = msg_send ! [ view, retain] ;
94
- Self :: new ( NonNull :: new ( view) , render_layer)
82
+ let layer = unsafe { Self :: get_metal_layer ( view, delegate) } ;
83
+ // SAFETY: The layer is an initialized instance of `CAMetalLayer`.
84
+ let layer = unsafe { metal:: MetalLayer :: from_ptr ( layer. cast ( ) ) } ;
85
+ let view: * mut Object = msg_send ! [ view. as_ptr( ) , retain] ;
86
+ let view = NonNull :: new ( view) . expect ( "retain should return the same object" ) ;
87
+ Self :: new ( Some ( view) , layer)
95
88
}
96
89
97
90
pub unsafe fn from_layer ( layer : & metal:: MetalLayerRef ) -> Self {
@@ -101,52 +94,155 @@ impl super::Surface {
101
94
Self :: new ( None , layer. to_owned ( ) )
102
95
}
103
96
104
- /// If not called on the main thread, this will panic.
97
+ /// Get or create a new `CAMetalLayer` associated with the given `NSView`
98
+ /// or `UIView`.
99
+ ///
100
+ /// # Panics
101
+ ///
102
+ /// If called from a thread that is not the main thread, this will panic.
103
+ ///
104
+ /// # Safety
105
+ ///
106
+ /// The `view` must be a valid instance of `NSView` or `UIView`.
105
107
pub ( crate ) unsafe fn get_metal_layer (
106
- view : * mut Object ,
108
+ view : NonNull < Object > ,
107
109
delegate : Option < & HalManagedMetalLayerDelegate > ,
108
110
) -> * mut Object {
109
- if view. is_null ( ) {
110
- panic ! ( "window does not have a valid contentView" ) ;
111
- }
112
-
113
111
let is_main_thread: BOOL = msg_send ! [ class!( NSThread ) , isMainThread] ;
114
112
if is_main_thread == NO {
115
113
panic ! ( "get_metal_layer cannot be called in non-ui thread." ) ;
116
114
}
117
115
118
- let main_layer: * mut Object = msg_send ! [ view, layer] ;
119
- let class = class ! ( CAMetalLayer ) ;
120
- let is_valid_layer: BOOL = msg_send ! [ main_layer, isKindOfClass: class] ;
116
+ // Ensure that the view is layer-backed.
117
+ // Views are always layer-backed in UIKit.
118
+ #[ cfg( target_os = "macos" ) ]
119
+ let ( ) = msg_send ! [ view. as_ptr( ) , setWantsLayer: YES ] ;
120
+
121
+ let root_layer: * mut Object = msg_send ! [ view. as_ptr( ) , layer] ;
122
+ // `-[NSView layer]` can return `NULL`, while `-[UIView layer]` should
123
+ // always be available.
124
+ assert ! ( !root_layer. is_null( ) , "failed making the view layer-backed" ) ;
125
+
126
+ // NOTE: We explicitly do not touch properties such as
127
+ // `layerContentsPlacement`, `needsDisplayOnBoundsChange` and
128
+ // `contentsGravity` etc. on the root layer, both since we would like
129
+ // to give the user full control over them, and because the default
130
+ // values suit us pretty well (especially the contents placement being
131
+ // `NSViewLayerContentsRedrawDuringViewResize`, which allows the view
132
+ // to receive `drawRect:`/`updateLayer` calls).
121
133
122
- if is_valid_layer == YES {
123
- main_layer
134
+ let is_metal_layer: BOOL = msg_send ! [ root_layer, isKindOfClass: class!( CAMetalLayer ) ] ;
135
+ if is_metal_layer == YES {
136
+ // The view has a `CAMetalLayer` as the root layer, which can
137
+ // happen for example if user overwrote `-[NSView layerClass]` or
138
+ // the view is `MTKView`.
139
+ //
140
+ // This is easily handled: We take "ownership" over the layer, and
141
+ // render directly into that; after all, the user passed a view
142
+ // with an explicit Metal layer to us, so this is very likely what
143
+ // they expect us to do.
144
+ root_layer
124
145
} else {
125
- // If the main layer is not a CAMetalLayer, we create a CAMetalLayer and use it.
126
- let new_layer: * mut Object = msg_send ! [ class, new] ;
127
- let frame: CGRect = msg_send ! [ main_layer, bounds] ;
146
+ // The view does not have a `CAMetalLayer` as the root layer (this
147
+ // is the default for most views).
148
+ //
149
+ // This case is trickier! We cannot use the existing layer with
150
+ // Metal, so we must do something else. There are a few options:
151
+ //
152
+ // 1. Panic here, and require the user to pass a view with a
153
+ // `CAMetalLayer` layer.
154
+ //
155
+ // While this would "work", it doesn't solve the problem, and
156
+ // instead passes the ball onwards to the user and ecosystem to
157
+ // figure it out.
158
+ //
159
+ // 2. Override the existing layer with a newly created layer.
160
+ //
161
+ // If we overlook that this does not work in UIKit since
162
+ // `UIView`'s `layer` is `readonly`, and that as such we will
163
+ // need to do something different there anyhow, this is
164
+ // actually a fairly good solution, and was what the original
165
+ // implementation did.
166
+ //
167
+ // It has some problems though, due to:
168
+ //
169
+ // a. `wgpu` in our API design choosing not to register a
170
+ // callback with `-[CALayerDelegate displayLayer:]`, but
171
+ // instead leaves it up to the user to figure out when to
172
+ // redraw. That is, we rely on other libraries' callbacks
173
+ // telling us when to render.
174
+ //
175
+ // (If this were an API only for Metal, we would probably
176
+ // make the user provide a `render` closure that we'd call
177
+ // in the right situations. But alas, we have to be
178
+ // cross-platform here).
179
+ //
180
+ // b. Overwriting the `layer` on `NSView` makes the view
181
+ // "layer-hosting", see [wantsLayer], which disables drawing
182
+ // functionality on the view like `drawRect:`/`updateLayer`.
183
+ //
184
+ // These two in combination makes it basically impossible for
185
+ // crates like Winit to provide a robust rendering callback
186
+ // that integrates with the system's built-in mechanisms for
187
+ // redrawing, exactly because overwriting the layer would be
188
+ // implicitly disabling those mechanisms!
189
+ //
190
+ // [wantsLayer]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
191
+ //
192
+ // 3. Create a sublayer.
193
+ //
194
+ // `CALayer` has the concept of "sublayers", which we can use
195
+ // instead of overriding the layer.
196
+ //
197
+ // This is also the recommended solution on UIKit, so it's nice
198
+ // that we can use (almost) the same implementation for these.
199
+ //
200
+ // It _might_, however, perform ever so slightly worse than
201
+ // overriding the layer directly.
202
+ //
203
+ // 4. Create a new `MTKView` (or a custom view), and add it as a
204
+ // subview.
205
+ //
206
+ // Similar to creating a sublayer (see above), but also
207
+ // provides a bunch of event handling that we don't need.
208
+ //
209
+ // Option 3 seems like the most robust solution, so this is what
210
+ // we're going to do.
211
+
212
+ // Create a new sublayer.
213
+ let new_layer: * mut Object = msg_send ! [ class!( CAMetalLayer ) , new] ;
214
+ let ( ) = msg_send ! [ root_layer, addSublayer: new_layer] ;
215
+
216
+ // Automatically resize the sublayer's frame to match the
217
+ // superlayer's bounds.
218
+ //
219
+ // Note that there is a somewhat hidden design decision in this:
220
+ // We define the `width` and `height` in `configure` to control
221
+ // the `drawableSize` of the layer, while `bounds` and `frame` are
222
+ // outside of the user's direct control - instead, though, they
223
+ // can control the size of the view (or root layer), and get the
224
+ // desired effect that way.
225
+ //
226
+ // We _could_ also let `configure` set the `bounds` size, however
227
+ // that would be inconsistent with using the root layer directly
228
+ // (as we may do, see above).
229
+ let width_sizable = 1 << 1 ; // kCALayerWidthSizable
230
+ let height_sizable = 1 << 4 ; // kCALayerHeightSizable
231
+ let mask: c_uint = width_sizable | height_sizable;
232
+ let ( ) = msg_send ! [ new_layer, setAutoresizingMask: mask] ;
233
+
234
+ // Specify the relative size that the auto resizing mask above
235
+ // will keep (i.e. tell it to fill out its superlayer).
236
+ let frame: CGRect = msg_send ! [ root_layer, bounds] ;
128
237
let ( ) = msg_send ! [ new_layer, setFrame: frame] ;
129
- #[ cfg( target_os = "ios" ) ]
130
- {
131
- // Unlike NSView, UIView does not allow to replace main layer.
132
- let ( ) = msg_send ! [ main_layer, addSublayer: new_layer] ;
133
- // On iOS, "from_view" may be called before the application initialization is complete,
134
- // `msg_send![view, window]` and `msg_send![window, screen]` will get null.
135
- let screen: * mut Object = msg_send ! [ class!( UIScreen ) , mainScreen] ;
136
- let scale_factor: CGFloat = msg_send ! [ screen, nativeScale] ;
137
- let ( ) = msg_send ! [ view, setContentScaleFactor: scale_factor] ;
138
- } ;
139
- #[ cfg( target_os = "macos" ) ]
140
- {
141
- let ( ) = msg_send ! [ view, setLayer: new_layer] ;
142
- let ( ) = msg_send ! [ view, setWantsLayer: YES ] ;
143
- let ( ) = msg_send ! [ new_layer, setContentsGravity: unsafe { kCAGravityTopLeft } ] ;
144
- let window: * mut Object = msg_send ! [ view, window] ;
145
- if !window. is_null ( ) {
146
- let scale_factor: CGFloat = msg_send ! [ window, backingScaleFactor] ;
147
- let ( ) = msg_send ! [ new_layer, setContentsScale: scale_factor] ;
148
- }
149
- } ;
238
+
239
+ let _: ( ) = msg_send ! [ new_layer, setContentsGravity: unsafe { kCAGravityTopLeft } ] ;
240
+
241
+ // Set initial scale factor of the layer. This is kept in sync by
242
+ // `configure` (on UIKit), and the delegate below (on AppKit).
243
+ let scale_factor: CGFloat = msg_send ! [ root_layer, contentsScale] ;
244
+ let ( ) = msg_send ! [ new_layer, setContentsScale: scale_factor] ;
245
+
150
246
if let Some ( delegate) = delegate {
151
247
let ( ) = msg_send ! [ new_layer, setDelegate: delegate. 0 ] ;
152
248
}
@@ -210,19 +306,28 @@ impl crate::Surface for super::Surface {
210
306
_ => ( ) ,
211
307
}
212
308
213
- let device_raw = device. shared . device . lock ( ) ;
214
- // On iOS, unless the user supplies a view with a CAMetalLayer, we
215
- // create one as a sublayer. However, when the view changes size,
216
- // its sublayers are not automatically resized, and we must resize
217
- // it here. The drawable size and the layer size don't correlate
218
- #[ cfg( target_os = "ios" ) ]
309
+ // AppKit / UIKit automatically sets the correct scale factor for
310
+ // layers attached to a view. Our layer, however, may not be directly
311
+ // attached to the view; in those cases, we need to set the scale
312
+ // factor ourselves.
313
+ //
314
+ // For AppKit, we do so by adding a delegate on the layer with the
315
+ // `layer:shouldInheritContentsScale:fromWindow:` method returning
316
+ // `true` - this tells the system to automatically update the scale
317
+ // factor when it changes.
318
+ //
319
+ // For UIKit, we manually update the scale factor here.
320
+ //
321
+ // TODO: Is there a way that we could listen to such changes instead?
322
+ #[ cfg( not( target_os = "macos" ) ) ]
219
323
{
220
324
if let Some ( view) = self . view {
221
- let main_layer: * mut Object = msg_send ! [ view. as_ptr( ) , layer] ;
222
- let bounds: CGRect = msg_send ! [ main_layer, bounds] ;
223
- let ( ) = msg_send ! [ * render_layer, setFrame: bounds] ;
325
+ let scale_factor: CGFloat = msg_send ! [ view. as_ptr( ) , contentScaleFactor] ;
326
+ let ( ) = msg_send ! [ render_layer. as_ptr( ) , setContentsScale: scale_factor] ;
224
327
}
225
328
}
329
+
330
+ let device_raw = device. shared . device . lock ( ) ;
226
331
render_layer. set_device ( & device_raw) ;
227
332
render_layer. set_pixel_format ( caps. map_format ( config. format ) ) ;
228
333
render_layer. set_framebuffer_only ( framebuffer_only) ;
0 commit comments