-
Notifications
You must be signed in to change notification settings - Fork 953
/
surface.rs
399 lines (360 loc) · 16.8 KB
/
surface.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
#![allow(clippy::let_unit_value)] // `let () =` being used to constrain result type
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},
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 {
// <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())
}
}
impl super::Surface {
fn new(layer: metal::MetalLayer) -> Self {
Self {
render_layer: Mutex::new(layer),
swapchain_format: RwLock::new(None),
extent: RwLock::new(wgt::Extent3d::default()),
main_thread_id: thread::current().id(),
present_with_transaction: false,
}
}
/// If not called on the main thread, this will panic.
#[allow(clippy::transmute_ptr_to_ref)]
pub unsafe fn from_view(view: NonNull<Object>) -> 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 {
let class = class!(CAMetalLayer);
let proper_kind: BOOL = 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<Object>) -> 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];
#[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) }
}
}
pub(super) fn dimensions(&self) -> wgt::Extent3d {
let (size, scale): (CGSize, CGFloat) = unsafe {
let render_layer_borrow = self.render_layer.lock();
let render_layer = render_layer_borrow.as_ref();
let bounds: CGRect = msg_send![render_layer, bounds];
let contents_scale: CGFloat = msg_send![render_layer, contentsScale];
(bounds.size, contents_scale)
};
wgt::Extent3d {
width: (size.width * scale) as u32,
height: (size.height * scale) as u32,
depth_or_array_layers: 1,
}
}
}
impl crate::Surface for super::Surface {
type A = super::Api;
unsafe fn configure(
&self,
device: &super::Device,
config: &crate::SurfaceConfiguration,
) -> Result<(), crate::SurfaceError> {
log::debug!("build swapchain {:?}", config);
let caps = &device.shared.private_caps;
*self.swapchain_format.write() = Some(config.format);
*self.extent.write() = config.extent;
let render_layer = self.render_layer.lock();
let framebuffer_only = config.usage == crate::TextureUses::COLOR_TARGET;
let display_sync = match config.present_mode {
wgt::PresentMode::Fifo => true,
wgt::PresentMode::Immediate => false,
m => unreachable!("Unsupported present mode: {m:?}"),
};
let drawable_size = CGSize::new(config.extent.width as f64, config.extent.height as f64);
match config.composite_alpha_mode {
wgt::CompositeAlphaMode::Opaque => render_layer.set_opaque(true),
wgt::CompositeAlphaMode::PostMultiplied => render_layer.set_opaque(false),
_ => (),
}
// 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));
render_layer.set_framebuffer_only(framebuffer_only);
render_layer.set_presents_with_transaction(self.present_with_transaction);
// opt-in to Metal EDR
// EDR potentially more power used in display and more bandwidth, memory footprint.
let wants_edr = config.format == wgt::TextureFormat::Rgba16Float;
if wants_edr != render_layer.wants_extended_dynamic_range_content() {
render_layer.set_wants_extended_dynamic_range_content(wants_edr);
}
// this gets ignored on iOS for certain OS/device combinations (iphone5s iOS 10.3)
render_layer.set_maximum_drawable_count(config.maximum_frame_latency as u64 + 1);
render_layer.set_drawable_size(drawable_size);
if caps.can_set_next_drawable_timeout {
let () = msg_send![*render_layer, setAllowsNextDrawableTimeout:false];
}
if caps.can_set_display_sync {
let () = msg_send![*render_layer, setDisplaySyncEnabled: display_sync];
}
Ok(())
}
unsafe fn unconfigure(&self, _device: &super::Device) {
*self.swapchain_format.write() = None;
}
unsafe fn acquire_texture(
&self,
_timeout_ms: Option<std::time::Duration>, //TODO
_fence: &super::Fence,
) -> Result<Option<crate::AcquiredSurfaceTexture<super::Api>>, crate::SurfaceError> {
let render_layer = self.render_layer.lock();
let (drawable, texture) = match autoreleasepool(|| {
render_layer
.next_drawable()
.map(|drawable| (drawable.to_owned(), drawable.texture().to_owned()))
}) {
Some(pair) => pair,
None => return Ok(None),
};
let swapchain_format = self.swapchain_format.read().unwrap();
let extent = self.extent.read();
let suf_texture = super::SurfaceTexture {
texture: super::Texture {
raw: texture,
format: swapchain_format,
raw_type: metal::MTLTextureType::D2,
array_layers: 1,
mip_levels: 1,
copy_size: crate::CopyExtent {
width: extent.width,
height: extent.height,
depth: 1,
},
},
drawable,
present_with_transaction: self.present_with_transaction,
};
Ok(Some(crate::AcquiredSurfaceTexture {
texture: suf_texture,
suboptimal: false,
}))
}
unsafe fn discard_texture(&self, _texture: super::SurfaceTexture) {}
}