Skip to main content

ferroui_native/
metal.rs

1//! Metal: the platform graphics, the device (graphics context) and its
2//! external objects feature, the render surface of a top-level and its
3//! render target and drawing session.
4
5use crate::gpu_handle_wrap_feature::GpuHandleWrapFeature;
6use crate::helpers::ComResultExt;
7use crate::interop::*;
8use crate::top_level_impl::SurfaceTopLevel;
9use ferroui_base::platform::surfaces::{IPlatformRenderSurface, IPlatformRenderSurfaceRenderTarget};
10use ferroui_base::platform::{
11    IExternalObjectsHandleWrapRenderInterfaceContextFeature, IOptionalFeatureProvider, IPlatformGraphics,
12    IPlatformGraphicsContext, IPlatformHandle,
13    KnownPlatformGraphicsExternalImageHandleTypes, KnownPlatformGraphicsExternalSemaphoreHandleTypes,
14    PlatformGraphicsExternalImageFormat, PlatformGraphicsExternalImageProperties,
15};
16use ferroui_base::reactive::IDisposable;
17use ferroui_base::rendering::composition::CompositionGpuImportedImageSynchronizationCapabilities;
18use ferroui_base::threading::Dispatcher;
19use ferroui_base::PixelSize;
20use ferroui_microcom::{ComPtr, HResult};
21use ferroui_metal::{
22    IMetalDevice, IMetalExternalObjectsFeature, IMetalExternalTexture, IMetalPlatformSurface,
23    IMetalPlatformSurfaceRenderTarget, IMetalPlatformSurfaceRenderingSession, IMetalSharedEvent,
24};
25use std::any::{Any, TypeId};
26use std::cell::RefCell;
27use std::ffi::c_void;
28use std::rc::Rc;
29use std::sync::{Arc, Weak};
30
31/// The Metal platform graphics of the macOS backend.
32// The original holds the factory and hands it to every device, whose
33// handle wrapping feature asks it for a memory management helper. The
34// graphics are shared with the thread that renders, and the factory is an
35// object of the UI thread (releasing its last reference ends the native
36// application state): the helper is asked for once, here, and every device
37// gets a reference to it (DEVIATIONS.md, Native backend).
38pub struct MetalPlatformGraphics {
39    display: ComPtr<IFrnMetalDisplay>,
40    memory_helper: ComPtr<IFrnNativeObjectsMemoryManagement>,
41}
42
43// SAFETY: the platform graphics are shared by the UI thread and the thread
44// that renders (`IPlatformGraphics: Send + Sync`), and each may call
45// `CreateDevice` and drop its handle. The display is a process-lifetime
46// singleton of the native side (`FrnMetalDisplay`, held by a `ComStaticPtr`
47// in `native/FerroUI.Native/src/OSX/metal.mm`) without state: `CreateDevice`
48// reads nothing of the object and makes a new device and command queue per
49// call, so two threads may be inside it at once. The pointer is never
50// replaced, and the reference this object holds is taken once and given
51// back once, by whichever thread drops the last handle: the reference count
52// of a native object is atomic (`ComObject` in
53// `native/FerroUI.Native/inc/comimpl.h`). The device a call returns belongs
54// to the thread that asked.
55//
56// The memory management helper (`MemHelper` in
57// `native/FerroUI.Native/src/OSX/memhelp.mm`) has no state either: its
58// methods retain and release the object they are handed (`CFRetain`,
59// `CFRelease`, `retain` and `release` of an Objective-C object, all of which
60// any thread may call), so a thread that creates a device may take a
61// reference to the helper, and the features and wrappers of that device may
62// call it and release it, while another thread does the same.
63unsafe impl Send for MetalPlatformGraphics {}
64// SAFETY: see `Send`.
65unsafe impl Sync for MetalPlatformGraphics {}
66
67impl MetalPlatformGraphics {
68    /// Obtains the Metal display of the native side; fails when Metal is
69    /// not available.
70    pub fn new(factory: &IFerroNativeFactory) -> Result<MetalPlatformGraphics, HResult> {
71        let display = factory.obtain_metal_display()?.ok_or(HResult::POINTER)?;
72        let memory_helper = factory.create_memory_management_helper()?.ok_or(HResult::POINTER)?;
73        Ok(MetalPlatformGraphics { display, memory_helper })
74    }
75
76    /// Creates a Metal device; fails when the system has none.
77    pub fn try_create_context(&self) -> Result<Rc<MetalDevice>, HResult> {
78        let native = self.display.create_device()?.ok_or(HResult::POINTER)?;
79        Ok(MetalDevice::new(self.memory_helper.clone(), native))
80    }
81}
82
83const _: fn() = || {
84    fn assert_send_sync<T: Send + Sync>() {}
85    assert_send_sync::<MetalPlatformGraphics>();
86};
87
88impl IPlatformGraphics for MetalPlatformGraphics {
89    fn uses_shared_context(&self) -> bool {
90        false
91    }
92
93    fn create_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
94        self.try_create_context().check()
95    }
96
97    fn get_shared_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
98        panic!("Specified method is not supported.");
99    }
100}
101
102/// A Metal device and its command queue.
103pub struct MetalDevice {
104    weak_self: std::rc::Weak<MetalDevice>,
105    sync_root: ferroui_base::utilities::DisposableLock,
106    handle_wrap_feature: Rc<GpuHandleWrapFeature>,
107    external_objects_feature: Rc<MetalExternalObjectsFeature>,
108    native: RefCell<Option<ComPtr<IFrnMetalDevice>>>,
109}
110
111impl MetalDevice {
112    fn new(
113        memory_helper: ComPtr<IFrnNativeObjectsMemoryManagement>,
114        native: ComPtr<IFrnMetalDevice>,
115    ) -> Rc<MetalDevice> {
116        Rc::new_cyclic(|weak_self| MetalDevice {
117            weak_self: weak_self.clone(),
118            sync_root: ferroui_base::utilities::DisposableLock::new(),
119            handle_wrap_feature: Rc::new(GpuHandleWrapFeature::new(memory_helper)),
120            external_objects_feature: Rc::new(MetalExternalObjectsFeature::new(&native, weak_self.clone())),
121            native: RefCell::new(Some(native)),
122        })
123    }
124
125    /// The native device.
126    ///
127    /// # Panics
128    /// Panics when the device is disposed.
129    #[track_caller]
130    pub fn native(&self) -> ComPtr<IFrnMetalDevice> {
131        match self.native.borrow().clone() {
132            Some(native) => native,
133            None => panic!("Cannot access a disposed object: MetalDevice"),
134        }
135    }
136
137    /// Calls the native device where it is held, without taking a reference
138    /// of its own: for the getters, which neither take time nor release the
139    /// device.
140    ///
141    /// # Panics
142    /// Panics when the device is disposed.
143    #[track_caller]
144    fn with_native<R>(&self, f: impl FnOnce(&IFrnMetalDevice) -> R) -> R {
145        match self.native.borrow().as_ref() {
146            Some(native) => f(&**native),
147            None => panic!("Cannot access a disposed object: MetalDevice"),
148        }
149    }
150}
151
152impl IOptionalFeatureProvider for MetalDevice {
153    /// The device announces itself as a Metal device, the feature that
154    /// wraps the handle of an IOSurface or of a shared event for the time
155    /// of its import, and its external objects feature (the import of an
156    /// IOSurface and of a shared event).
157    fn try_get_feature(&self, feature_type: TypeId) -> Option<Rc<dyn Any>> {
158        if feature_type == TypeId::of::<dyn IMetalDevice>() {
159            let this: Rc<dyn IMetalDevice> = self.weak_self.upgrade()?;
160            return Some(Rc::new(this));
161        }
162        if feature_type == TypeId::of::<dyn IExternalObjectsHandleWrapRenderInterfaceContextFeature>() {
163            let feature: Rc<dyn IExternalObjectsHandleWrapRenderInterfaceContextFeature> =
164                self.handle_wrap_feature.clone();
165            return Some(Rc::new(feature));
166        }
167        if feature_type == TypeId::of::<dyn IMetalExternalObjectsFeature>() {
168            let feature: Rc<dyn IMetalExternalObjectsFeature> = self.external_objects_feature.clone();
169            return Some(Rc::new(feature));
170        }
171        None
172    }
173}
174
175impl IPlatformGraphicsContext for MetalDevice {
176    fn is_lost(&self) -> bool {
177        false
178    }
179
180    /// Takes the lock of the device: the UI thread and the render thread
181    /// both use it, one at a time.
182    ///
183    /// The device lives in the graph of the server compositor, so every
184    /// caller already holds the compositor lock: the order is always the
185    /// compositor lock first, then this one. Something that used the device
186    /// outside the compositor would have to keep that order.
187    fn ensure_current(&self) -> Rc<dyn IDisposable> {
188        self.sync_root.lock()
189    }
190
191    fn dispose(&self) {
192        let native = self.native.borrow_mut().take();
193        drop(native);
194    }
195
196    fn as_any(&self) -> &dyn Any {
197        self
198    }
199}
200
201impl IMetalDevice for MetalDevice {
202    fn device(&self) -> *mut c_void {
203        self.with_native(|native| native.get_device())
204    }
205
206    fn command_queue(&self) -> *mut c_void {
207        self.with_native(|native| native.get_queue())
208    }
209}
210
211/// The Metal render surface of a top-level.
212///
213/// The surface is shared with the thread that renders
214/// (`IPlatformRenderSurface: Send + Sync`). What it holds of the native side
215/// is the native top-level, which only the UI thread may use (see
216/// `SurfaceTopLevel`): the render target is created on the UI thread, as in
217/// the reference, and the native side refuses anything else
218/// (`TopLevelImpl::CreateMetalRenderTarget` in
219/// `native/FerroUI.Native/src/OSX/TopLevelImpl.mm` returns
220/// `COR_E_INVALIDOPERATION` off the main thread).
221pub struct MetalPlatformSurface {
222    weak_self: Weak<MetalPlatformSurface>,
223    top_level: SurfaceTopLevel,
224}
225
226impl MetalPlatformSurface {
227    pub(crate) fn new(top_level: ComPtr<IFrnTopLevel>) -> Arc<MetalPlatformSurface> {
228        Arc::new_cyclic(|weak_self| MetalPlatformSurface {
229            weak_self: weak_self.clone(),
230            top_level: SurfaceTopLevel::new(top_level),
231        })
232    }
233
234    /// Releases the native top-level; called by the top-level when it is
235    /// disposed, on the UI thread.
236    pub(crate) fn close(&self) {
237        self.top_level.release();
238    }
239}
240
241impl IPlatformRenderSurface for MetalPlatformSurface {
242    /// Deviation (DEVIATIONS.md, Native backend): the reference throws
243    /// `RenderTargetNotReadyException` from `CreateMetalRenderTarget` off the
244    /// UI thread and the composition target catches it; here the surface
245    /// answers that it is not ready, which the composition target treats the
246    /// same way, without a panic to catch.
247    fn is_ready(&self) -> bool {
248        self.top_level.get().is_some()
249    }
250
251    fn try_get_surface_kind(&self, kind: TypeId) -> Option<Rc<dyn Any>> {
252        if kind == TypeId::of::<dyn IMetalPlatformSurface>() {
253            // The contract hands out an `Rc`, and the surface lives in an
254            // `Arc`: the view forwards to the surface.
255            let this: Rc<dyn IMetalPlatformSurface> = Rc::new(MetalPlatformSurfaceView(self.weak_self.upgrade()?));
256            return Some(Rc::new(this));
257        }
258        None
259    }
260
261    fn as_any(&self) -> &dyn Any {
262        self
263    }
264}
265
266impl IMetalPlatformSurface for MetalPlatformSurface {
267    /// # Panics
268    /// Panics when called off the UI thread or when the top-level is
269    /// disposed (the render target is not ready) and when `device` is not a
270    /// device of this backend.
271    fn create_metal_render_target(&self, device: Rc<dyn IMetalDevice>) -> Rc<dyn IMetalPlatformSurfaceRenderTarget> {
272        if !Dispatcher::ui_thread().check_access() {
273            panic!("The render target is not ready.");
274        }
275
276        let Some(top_level) = self.top_level.get() else {
277            panic!("The render target is not ready.");
278        };
279
280        let Some(dev) = device.as_any().downcast_ref::<MetalDevice>() else {
281            panic!("The Metal device belongs to a different platform backend.");
282        };
283        let native_device = dev.native();
284        let target = top_level.create_metal_render_target(Some(&native_device)).check();
285        Rc::new(MetalRenderTarget { native: RefCell::new(target) })
286    }
287}
288
289/// The Metal surface as the Metal contract hands it out
290/// (`Rc<dyn IMetalPlatformSurface>`); it forwards to the shared surface.
291struct MetalPlatformSurfaceView(Arc<MetalPlatformSurface>);
292
293impl IPlatformRenderSurface for MetalPlatformSurfaceView {
294    fn is_ready(&self) -> bool {
295        self.0.is_ready()
296    }
297
298    fn try_get_surface_kind(&self, kind: TypeId) -> Option<Rc<dyn Any>> {
299        self.0.try_get_surface_kind(kind)
300    }
301
302    fn as_any(&self) -> &dyn Any {
303        self.0.as_any()
304    }
305}
306
307impl IMetalPlatformSurface for MetalPlatformSurfaceView {
308    fn create_metal_render_target(&self, device: Rc<dyn IMetalDevice>) -> Rc<dyn IMetalPlatformSurfaceRenderTarget> {
309        self.0.create_metal_render_target(device)
310    }
311}
312
313/// The external objects of a Metal device: the import of an IOSurface as a
314/// texture and of a shared event, and the commands that wait for an event
315/// and signal one.
316///
317/// The feature is an object of the device: it is created with it, inside
318/// the graph of the server compositor, and used under the compositor lock
319/// like the device.
320// The original holds the native device, which its device releases when it
321// is disposed. Here every holder of a native object has a reference of its
322// own, so the feature reaches the native device through its device: a
323// disposed device fails the feature as it does upstream.
324pub(crate) struct MetalExternalObjectsFeature {
325    device: std::rc::Weak<MetalDevice>,
326    device_luid: Option<Vec<u8>>,
327}
328
329impl MetalExternalObjectsFeature {
330    fn new(native: &IFrnMetalDevice, device: std::rc::Weak<MetalDevice>) -> MetalExternalObjectsFeature {
331        let mut registry_id = 0u64;
332        // SAFETY: the pointer is valid for the one value the native method
333        // writes.
334        let has_registry_id = unsafe { native.get_io_kit_registry_id(&mut registry_id) };
335        // The bytes of the identifier, the most significant first.
336        let device_luid = has_registry_id.then(|| registry_id.to_be_bytes().to_vec());
337        MetalExternalObjectsFeature { device, device_luid }
338    }
339
340    /// The native device.
341    ///
342    /// # Panics
343    /// Panics when the device is disposed.
344    #[track_caller]
345    fn native(&self) -> ComPtr<IFrnMetalDevice> {
346        match self.device.upgrade() {
347            Some(device) => device.native(),
348            None => panic!("Cannot access a disposed object: MetalDevice"),
349        }
350    }
351}
352
353impl IMetalExternalObjectsFeature for MetalExternalObjectsFeature {
354    fn supported_image_handle_types(&self) -> Vec<String> {
355        vec![KnownPlatformGraphicsExternalImageHandleTypes::IO_SURFACE_REF.to_string()]
356    }
357
358    fn supported_semaphore_types(&self) -> Vec<String> {
359        vec![KnownPlatformGraphicsExternalSemaphoreHandleTypes::METAL_SHARED_EVENT.to_string()]
360    }
361
362    fn device_luid(&self) -> Option<Vec<u8>> {
363        self.device_luid.clone()
364    }
365
366    fn get_synchronization_capabilities(
367        &self,
368        _image_handle_type: &str,
369    ) -> CompositionGpuImportedImageSynchronizationCapabilities {
370        CompositionGpuImportedImageSynchronizationCapabilities::TIMELINE_SEMAPHORES
371    }
372
373    /// # Panics
374    /// Panics when the handle is not an IOSurface and when the native
375    /// device cannot make a texture of it.
376    fn import_image(
377        &self,
378        handle: Rc<dyn IPlatformHandle>,
379        properties: PlatformGraphicsExternalImageProperties,
380    ) -> Rc<dyn IMetalExternalTexture> {
381        // Every format of the enumeration is supported: the original
382        // refuses the values it does not name.
383        let format = match properties.format {
384            PlatformGraphicsExternalImageFormat::R8G8B8A8UNorm => FrnPixelFormat::kFrnRgba8888,
385            PlatformGraphicsExternalImageFormat::B8G8R8A8UNorm => FrnPixelFormat::kFrnBgra8888,
386        };
387
388        if handle.handle_descriptor() != Some(KnownPlatformGraphicsExternalImageHandleTypes::IO_SURFACE_REF) {
389            panic!("Specified method is not supported.");
390        }
391
392        // SAFETY: by its descriptor the handle is an `IOSurfaceRef`. The
393        // compositor hands over the handle the wrapping feature of the
394        // device made of it, which holds a reference until the import has
395        // run; another caller keeps the surface alive for the call. The
396        // texture the native device makes of it retains the surface.
397        let texture = unsafe { self.native().import_io_surface(handle.handle() as *mut c_void, format) }
398            .and_then(|texture| texture.ok_or(HResult::POINTER))
399            .check();
400        Rc::new(ImportedTexture { texture: RefCell::new(Some(texture)) })
401    }
402
403    /// # Panics
404    /// Panics when the handle is not a Metal shared event and when the
405    /// native device cannot import it.
406    fn import_shared_event(&self, handle: Rc<dyn IPlatformHandle>) -> Rc<dyn IMetalSharedEvent> {
407        if handle.handle_descriptor() != Some(KnownPlatformGraphicsExternalSemaphoreHandleTypes::METAL_SHARED_EVENT) {
408            panic!("Specified method is not supported.");
409        }
410        // SAFETY: by its descriptor the handle is an `id<MTLSharedEvent>`.
411        // The compositor hands over the handle the wrapping feature of the
412        // device made of it, which holds a reference until the import has
413        // run; another caller keeps the event alive for the call. The
414        // native device makes an event of its own from it.
415        let inner = unsafe { self.native().import_shared_event(handle.handle() as *mut c_void) }
416            .and_then(|event| event.ok_or(HResult::POINTER))
417            .check();
418        Rc::new(SharedEvent { inner: RefCell::new(Some(inner)) })
419    }
420
421    /// # Panics
422    /// Panics when the event was not imported by a device of this backend.
423    fn submit_wait(&self, event: &dyn IMetalSharedEvent, wait_for_value: u64) {
424        let event = SharedEvent::from_contract(event).native();
425        self.native().submit_wait(Some(&*event), wait_for_value).check();
426    }
427
428    /// # Panics
429    /// Panics when the event was not imported by a device of this backend.
430    fn submit_signal(&self, event: &dyn IMetalSharedEvent, signal_value: u64) {
431        let event = SharedEvent::from_contract(event).native();
432        self.native().submit_signal(Some(&*event), signal_value).check();
433    }
434}
435
436/// A texture the native device made of an IOSurface.
437struct ImportedTexture {
438    texture: RefCell<Option<ComPtr<IFrnMetalTexture>>>,
439}
440
441impl ImportedTexture {
442    /// Calls the native texture where it is held.
443    ///
444    /// # Panics
445    /// Panics when the texture is disposed.
446    #[track_caller]
447    fn with_texture<R>(&self, f: impl FnOnce(&IFrnMetalTexture) -> R) -> R {
448        match self.texture.borrow().as_ref() {
449            Some(texture) => f(&**texture),
450            None => panic!("Cannot access a disposed object: ImportedTexture"),
451        }
452    }
453}
454
455impl IMetalExternalTexture for ImportedTexture {
456    fn width(&self) -> i32 {
457        self.with_texture(|texture| texture.get_width())
458    }
459
460    fn height(&self) -> i32 {
461        self.with_texture(|texture| texture.get_height())
462    }
463
464    fn samples(&self) -> i32 {
465        self.with_texture(|texture| texture.get_sample_count())
466    }
467
468    fn handle(&self) -> *mut c_void {
469        self.with_texture(|texture| texture.get_native_handle())
470    }
471
472    fn dispose(&self) {
473        let texture = self.texture.borrow_mut().take();
474        drop(texture);
475    }
476}
477
478/// A shared event the native device imported.
479struct SharedEvent {
480    inner: RefCell<Option<ComPtr<IFrnMTLSharedEvent>>>,
481}
482
483impl SharedEvent {
484    /// The event behind the contract (the cast of the original).
485    #[track_caller]
486    fn from_contract(event: &dyn IMetalSharedEvent) -> &SharedEvent {
487        match event.as_any().downcast_ref::<SharedEvent>() {
488            Some(event) => event,
489            None => panic!("The shared event belongs to a different platform backend."),
490        }
491    }
492
493    /// The native event.
494    ///
495    /// # Panics
496    /// Panics when the event is disposed.
497    #[track_caller]
498    fn native(&self) -> ComPtr<IFrnMTLSharedEvent> {
499        match self.inner.borrow().clone() {
500            Some(inner) => inner,
501            None => panic!("Cannot access a disposed object: SharedEvent"),
502        }
503    }
504}
505
506impl IMetalSharedEvent for SharedEvent {
507    fn handle(&self) -> *mut c_void {
508        self.native().get_native_handle()
509    }
510
511    fn dispose(&self) {
512        let inner = self.inner.borrow_mut().take();
513        drop(inner);
514    }
515
516    fn as_any(&self) -> &dyn Any {
517        self
518    }
519}
520
521/// The Metal render target of a top-level.
522pub struct MetalRenderTarget {
523    native: RefCell<Option<ComPtr<IFrnMetalRenderTarget>>>,
524}
525
526impl MetalRenderTarget {
527    #[track_caller]
528    fn native(&self) -> ComPtr<IFrnMetalRenderTarget> {
529        match self.native.borrow().clone() {
530            Some(native) => native,
531            None => panic!("Cannot access a disposed object: MetalRenderTarget"),
532        }
533    }
534}
535
536impl IPlatformRenderSurfaceRenderTarget for MetalRenderTarget {}
537
538impl IMetalPlatformSurfaceRenderTarget for MetalRenderTarget {
539    fn begin_rendering(&self) -> Rc<dyn IMetalPlatformSurfaceRenderingSession> {
540        let session = self.native().begin_drawing().check();
541        Rc::new(MetalDrawingSession { session: RefCell::new(session) })
542    }
543
544    fn dispose(&self) {
545        let native = self.native.borrow_mut().take();
546        drop(native);
547    }
548}
549
550/// One frame drawn to the Metal render target of a top-level; releasing the
551/// native session presents the frame.
552pub struct MetalDrawingSession {
553    session: RefCell<Option<ComPtr<IFrnMetalRenderingSession>>>,
554}
555
556impl MetalDrawingSession {
557    /// Calls the native session where it is held, without taking a reference
558    /// of its own: its members are getters, and releasing the session is
559    /// what presents the frame.
560    ///
561    /// # Panics
562    /// Panics when the session is disposed.
563    #[track_caller]
564    fn with_session<R>(&self, f: impl FnOnce(&IFrnMetalRenderingSession) -> R) -> R {
565        match self.session.borrow().as_ref() {
566            Some(session) => f(&**session),
567            None => panic!("Cannot access a disposed object: MetalDrawingSession"),
568        }
569    }
570}
571
572impl IMetalPlatformSurfaceRenderingSession for MetalDrawingSession {
573    fn texture(&self) -> *mut c_void {
574        self.with_session(|session| session.get_texture())
575    }
576
577    fn size(&self) -> PixelSize {
578        let size = self.with_session(|session| session.get_pixel_size()).check();
579        PixelSize::new(size.width, size.height)
580    }
581
582    fn scaling(&self) -> f64 {
583        self.with_session(|session| session.get_scaling())
584    }
585
586    fn is_y_flipped(&self) -> bool {
587        false
588    }
589
590    fn dispose(&self) {
591        let session = self.session.borrow_mut().take();
592        drop(session);
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    // Not from upstream, which has no tests of the feature: the feature over
599    // a native device that is implemented here and records its calls.
600    use super::*;
601    use crate::gpu_handle_wrap_feature::tests::helper;
602    use ferroui_base::platform::PlatformHandle;
603
604    type Log = Rc<RefCell<Vec<String>>>;
605
606    struct FakeNativeDevice {
607        log: Log,
608        registry_id: Option<u64>,
609    }
610
611    impl IFrnMetalDeviceImpl for FakeNativeDevice {
612        fn get_device(&self) -> *mut c_void {
613            std::ptr::null_mut()
614        }
615
616        fn get_queue(&self) -> *mut c_void {
617            std::ptr::null_mut()
618        }
619
620        fn get_io_kit_registry_id(&self, value: *mut u64) -> bool {
621            match self.registry_id {
622                Some(registry_id) => {
623                    // SAFETY: the caller passes storage for one value.
624                    unsafe { *value = registry_id };
625                    true
626                }
627                None => false,
628            }
629        }
630
631        fn import_io_surface(
632            &self,
633            handle: *mut c_void,
634            pixel_format: FrnPixelFormat,
635        ) -> Result<Option<ComPtr<IFrnMetalTexture>>, HResult> {
636            self.log.borrow_mut().push(format!("ImportIOSurface({}, {pixel_format:?})", handle as usize));
637            if handle.is_null() {
638                return Err(HResult::FAIL);
639            }
640            Ok(Some(IFrnMetalTexture::from_impl(FakeNativeTexture { log: self.log.clone() })))
641        }
642
643        fn import_shared_event(
644            &self,
645            mtl_shared_event_instance: *mut c_void,
646        ) -> Result<Option<ComPtr<IFrnMTLSharedEvent>>, HResult> {
647            let handle = mtl_shared_event_instance as usize;
648            self.log.borrow_mut().push(format!("ImportSharedEvent({handle})"));
649            Ok(Some(IFrnMTLSharedEvent::from_impl(FakeNativeEvent { log: self.log.clone(), handle })))
650        }
651
652        fn submit_wait(&self, ev: Option<&IFrnMTLSharedEvent>, value: u64) -> Result<(), HResult> {
653            let handle = ev.map(|ev| ev.get_native_handle() as usize);
654            self.log.borrow_mut().push(format!("SubmitWait({handle:?}, {value})"));
655            Ok(())
656        }
657
658        fn submit_signal(&self, ev: Option<&IFrnMTLSharedEvent>, value: u64) -> Result<(), HResult> {
659            let handle = ev.map(|ev| ev.get_native_handle() as usize);
660            self.log.borrow_mut().push(format!("SubmitSignal({handle:?}, {value})"));
661            Ok(())
662        }
663    }
664
665    struct FakeNativeTexture {
666        log: Log,
667    }
668
669    impl IFrnMetalTextureImpl for FakeNativeTexture {
670        fn get_native_handle(&self) -> *mut c_void {
671            77 as *mut c_void
672        }
673
674        fn get_width(&self) -> i32 {
675            30
676        }
677
678        fn get_height(&self) -> i32 {
679            20
680        }
681
682        fn get_sample_count(&self) -> i32 {
683            1
684        }
685    }
686
687    impl Drop for FakeNativeTexture {
688        fn drop(&mut self) {
689            self.log.borrow_mut().push("the texture is released".to_string());
690        }
691    }
692
693    struct FakeNativeEvent {
694        log: Log,
695        handle: usize,
696    }
697
698    impl IFrnMTLSharedEventImpl for FakeNativeEvent {
699        fn get_native_handle(&self) -> *mut c_void {
700            // The native event is one of the device, not the one imported.
701            (self.handle + 1000) as *mut c_void
702        }
703
704        fn wait(&self, _value: u64, _timeout_ms: u64) -> bool {
705            false
706        }
707
708        fn set_signaled_value(&self, _value: u64) {}
709
710        fn get_signaled_value(&self) -> u64 {
711            0
712        }
713    }
714
715    impl Drop for FakeNativeEvent {
716        fn drop(&mut self) {
717            self.log.borrow_mut().push("the event is released".to_string());
718        }
719    }
720
721    fn device(registry_id: Option<u64>) -> (Rc<MetalDevice>, Log) {
722        let log = Log::default();
723        let native = IFrnMetalDevice::from_impl(FakeNativeDevice { log: log.clone(), registry_id });
724        (MetalDevice::new(helper(&log), native), log)
725    }
726
727    fn feature(device: &Rc<MetalDevice>) -> Rc<dyn IMetalExternalObjectsFeature> {
728        let features: &dyn IOptionalFeatureProvider = &**device;
729        features.try_get::<dyn IMetalExternalObjectsFeature>().expect("the device has the feature")
730    }
731
732    fn take(log: &Log) -> Vec<String> {
733        std::mem::take(&mut *log.borrow_mut())
734    }
735
736    fn io_surface(handle: isize) -> Rc<dyn IPlatformHandle> {
737        Rc::new(PlatformHandle::new(handle, Some(KnownPlatformGraphicsExternalImageHandleTypes::IO_SURFACE_REF)))
738    }
739
740    fn shared_event(handle: isize) -> Rc<dyn IPlatformHandle> {
741        Rc::new(PlatformHandle::new(
742            handle,
743            Some(KnownPlatformGraphicsExternalSemaphoreHandleTypes::METAL_SHARED_EVENT),
744        ))
745    }
746
747    fn properties(format: PlatformGraphicsExternalImageFormat) -> PlatformGraphicsExternalImageProperties {
748        PlatformGraphicsExternalImageProperties { width: 30, height: 20, format, ..Default::default() }
749    }
750
751    #[test]
752    fn the_device_announces_what_it_imports_and_how_it_is_synchronized() {
753        let (device, _) = device(Some(0x0102_0304_0506_0708));
754        let feature = feature(&device);
755
756        assert_eq!(vec!["IOSurfaceRef".to_string()], feature.supported_image_handle_types());
757        assert_eq!(vec!["MetalSharedEvent".to_string()], feature.supported_semaphore_types());
758        assert_eq!(
759            CompositionGpuImportedImageSynchronizationCapabilities::TIMELINE_SEMAPHORES,
760            feature.get_synchronization_capabilities("IOSurfaceRef")
761        );
762        // The registry identifier, the most significant byte first.
763        assert_eq!(Some(vec![1, 2, 3, 4, 5, 6, 7, 8]), feature.device_luid());
764    }
765
766    #[test]
767    fn a_device_without_a_registry_identifier_has_no_luid() {
768        let (device, _) = device(None);
769
770        assert_eq!(None, feature(&device).device_luid());
771    }
772
773    #[test]
774    fn an_io_surface_is_imported_with_the_format_of_the_image() {
775        let (device, log) = device(None);
776        let feature = feature(&device);
777
778        let texture = feature.import_image(io_surface(5), properties(PlatformGraphicsExternalImageFormat::R8G8B8A8UNorm));
779        assert_eq!(vec!["ImportIOSurface(5, kFrnRgba8888)"], take(&log));
780        assert_eq!(30, texture.width());
781        assert_eq!(20, texture.height());
782        assert_eq!(1, texture.samples());
783        assert_eq!(77, texture.handle() as usize);
784
785        texture.dispose();
786        assert_eq!(vec!["the texture is released"], take(&log));
787        texture.dispose();
788        assert!(take(&log).is_empty());
789
790        let texture = feature.import_image(io_surface(6), properties(PlatformGraphicsExternalImageFormat::B8G8R8A8UNorm));
791        assert_eq!(vec!["ImportIOSurface(6, kFrnBgra8888)"], take(&log));
792        drop(texture);
793    }
794
795    #[test]
796    #[should_panic(expected = "Cannot access a disposed object: ImportedTexture")]
797    fn a_disposed_texture_cannot_be_read() {
798        let (device, _) = device(None);
799        let texture = feature(&device).import_image(io_surface(5), properties(Default::default()));
800        texture.dispose();
801        let _ = texture.width();
802    }
803
804    #[test]
805    #[should_panic(expected = "Specified method is not supported.")]
806    fn a_handle_that_is_not_an_io_surface_is_not_imported_as_an_image() {
807        let (device, _) = device(None);
808        let _ = feature(&device).import_image(shared_event(5), properties(Default::default()));
809    }
810
811    #[test]
812    #[should_panic(expected = "Native call failed")]
813    fn a_failed_import_of_the_native_device_fails_the_import() {
814        let (device, _) = device(None);
815        let _ = feature(&device).import_image(io_surface(0), properties(Default::default()));
816    }
817
818    #[test]
819    #[should_panic(expected = "Specified method is not supported.")]
820    fn a_handle_that_is_not_a_shared_event_is_not_imported_as_an_event() {
821        let (device, _) = device(None);
822        let _ = feature(&device).import_shared_event(io_surface(5));
823    }
824
825    #[test]
826    fn the_commands_wait_for_and_signal_the_imported_event() {
827        let (device, log) = device(None);
828        let feature = feature(&device);
829
830        let event = feature.import_shared_event(shared_event(9));
831        assert_eq!(vec!["ImportSharedEvent(9)"], take(&log));
832        assert_eq!(1009, event.handle() as usize);
833
834        feature.submit_wait(&*event, 3);
835        feature.submit_signal(&*event, 4);
836        assert_eq!(vec!["SubmitWait(Some(1009), 3)", "SubmitSignal(Some(1009), 4)"], take(&log));
837
838        event.dispose();
839        assert_eq!(vec!["the event is released"], take(&log));
840        event.dispose();
841        assert!(take(&log).is_empty());
842    }
843
844    #[test]
845    #[should_panic(expected = "Cannot access a disposed object: SharedEvent")]
846    fn a_disposed_event_cannot_be_waited_for() {
847        let (device, _) = device(None);
848        let feature = feature(&device);
849        let event = feature.import_shared_event(shared_event(9));
850        event.dispose();
851        feature.submit_wait(&*event, 3);
852    }
853
854    #[test]
855    #[should_panic(expected = "The shared event belongs to a different platform backend.")]
856    fn an_event_of_another_backend_is_refused() {
857        struct ForeignEvent;
858
859        impl IMetalSharedEvent for ForeignEvent {
860            fn handle(&self) -> *mut c_void {
861                std::ptr::null_mut()
862            }
863
864            fn dispose(&self) {}
865
866            fn as_any(&self) -> &dyn Any {
867                self
868            }
869        }
870
871        let (device, _) = device(None);
872        feature(&device).submit_signal(&ForeignEvent, 1);
873    }
874
875    #[test]
876    fn the_device_wraps_the_handles_it_imports() {
877        let (device, log) = device(None);
878        let features: &dyn IOptionalFeatureProvider = &*device;
879        let wrap = features
880            .try_get::<dyn IExternalObjectsHandleWrapRenderInterfaceContextFeature>()
881            .expect("the device has the feature");
882        let feature = feature(&device);
883
884        // An IOSurface: retained when it is handed over, imported from the
885        // wrapper, released when the import has run.
886        let surface = io_surface(5);
887        let wrapped = wrap
888            .wrap_image_handle_on_any_thread(&surface, properties(Default::default()))
889            .expect("an IOSurface is wrapped");
890        drop(surface);
891        let texture = feature.import_image(wrapped.clone().as_platform_handle(), properties(Default::default()));
892        wrapped.dispose();
893        assert_eq!(
894            vec!["RetainCFObject(5)", "ImportIOSurface(5, kFrnRgba8888)", "ReleaseCFObject(5)"],
895            take(&log)
896        );
897        drop(texture);
898        let _ = take(&log);
899
900        // A shared event likewise.
901        let event = shared_event(9);
902        let wrapped = wrap.wrap_semaphore_handle_on_any_thread(&event).expect("a shared event is wrapped");
903        drop(event);
904        let imported = feature.import_shared_event(wrapped.clone().as_platform_handle());
905        wrapped.dispose();
906        assert_eq!(vec!["RetainNSObject(9)", "ImportSharedEvent(9)", "ReleaseNSObject(9)"], take(&log));
907        drop(imported);
908    }
909
910    #[test]
911    fn the_wrapping_feature_outlives_a_disposed_device() {
912        // As in the original, the feature holds the helper and not the
913        // device: a handle wrapped before the device went is still released.
914        let (device, log) = device(None);
915        let features: &dyn IOptionalFeatureProvider = &*device;
916        let wrap = features
917            .try_get::<dyn IExternalObjectsHandleWrapRenderInterfaceContextFeature>()
918            .expect("the device has the feature");
919        let wrapped = wrap.wrap_semaphore_handle_on_any_thread(&shared_event(9)).expect("a shared event is wrapped");
920
921        device.dispose();
922        wrapped.dispose();
923        assert_eq!(vec!["RetainNSObject(9)", "ReleaseNSObject(9)"], take(&log));
924    }
925
926    #[test]
927    #[should_panic(expected = "Cannot access a disposed object: MetalDevice")]
928    fn the_feature_of_a_disposed_device_imports_nothing() {
929        let (device, _) = device(None);
930        let feature = feature(&device);
931        device.dispose();
932        let _ = feature.import_shared_event(shared_event(9));
933    }
934}