Skip to main content

ferroui_native/
ferro_native_gl_platform_graphics.rs

1//! OpenGL over CGL: the platform graphics, the display, the contexts and
2//! their external objects feature, the render surface of a top-level and its
3//! render target and rendering session.
4//!
5//! The native side (`native/FerroUI.Native/src/OSX/cgl.mm` and
6//! `rendertarget.mm`) creates the contexts (OpenGL 3.2 core), makes them
7//! current under the lock of the context (`CGLLockContext`) and renders the
8//! frames of a top-level to an IOSurface that the view shows.
9
10use crate::gpu_handle_wrap_feature::GpuHandleWrapFeature;
11use crate::helpers::ComResultExt;
12use crate::interop::*;
13use crate::top_level_impl::SurfaceTopLevel;
14use ferroui_base::logging::{LogEventLevel, Logger};
15use ferroui_base::platform::surfaces::{IPlatformRenderSurface, IPlatformRenderSurfaceRenderTarget};
16use ferroui_base::platform::{
17    IExternalObjectsHandleWrapRenderInterfaceContextFeature, IOptionalFeatureProvider, IPlatformGraphics,
18    IPlatformGraphicsContext, IPlatformHandle, KnownPlatformGraphicsExternalImageHandleTypes,
19    KnownPlatformGraphicsExternalSemaphoreHandleTypes, PlatformGraphicsContextLostException,
20    PlatformGraphicsExternalImageFormat, PlatformGraphicsExternalImageProperties, PlatformRenderTargetState,
21    RenderTargetSceneInfo,
22};
23use ferroui_base::reactive::IDisposable;
24use ferroui_base::rendering::composition::CompositionGpuImportedImageSynchronizationCapabilities;
25use ferroui_base::threading::Dispatcher;
26use ferroui_base::{PixelSize, RenderTargetNotReadyException};
27use ferroui_microcom::{ComPtr, HResult, IUnknown};
28use ferroui_opengl::gl_consts::*;
29use ferroui_opengl::surfaces::{IGlPlatformSurface, IGlPlatformSurfaceRenderTarget, IGlPlatformSurfaceRenderingSession};
30use ferroui_opengl::{
31    GetProcAddress, GlBasicInfoInterface, GlInterface, GlProfileType, GlVersion, IGlContext,
32    IGlContextExternalObjectsFeature, IGlExportableExternalImageTexture, IGlExternalImageTexture,
33    IGlExternalSemaphore, OpenGlException,
34};
35use std::any::{Any, TypeId};
36use std::cell::{Cell, RefCell};
37use std::ffi::{c_void, CString};
38use std::rc::{Rc, Weak};
39use std::sync::Arc;
40
41/// The OpenGL platform graphics of the macOS backend: one context shared by
42/// every top-level and both threads that render.
43pub struct FerroNativeGlPlatformGraphics {
44    display: ComPtr<IFrnGlDisplay>,
45    gl_display: Rc<GlDisplay>,
46    version: GlVersion,
47    shared_context: Rc<GlContext>,
48}
49
50// SAFETY: the platform graphics are shared by the UI thread and the thread
51// that renders (`IPlatformGraphics: Send + Sync`), as the Metal graphics are
52// (`metal.rs`; docs/porting/render-thread.md, R5.5). They are created on the
53// UI thread, and afterwards the only member that reaches what they hold is
54// the context manager of the server compositor: `uses_shared_context` and
55// `get_shared_context`, under the compositor lock, which serialises the two
56// threads that render; the shared context it hands out lives in the graph of
57// the server compositor from then on and is used under that lock only, as the
58// Metal device is. `create_context` asks the native display for a context; the
59// display is a process-lifetime singleton of the native side
60// (`ComStaticPtr<FrnGlDisplay>` in `native/FerroUI.Native/src/OSX/cgl.mm`)
61// whose `CreateContext` reads nothing of the object, and the reference counts
62// of native objects are atomic (`ComObject` in
63// `native/FerroUI.Native/inc/comimpl.h`). The `Rc` handles the graphics keep
64// (the display object of this file and the shared context) are cloned and
65// released only by those callers, one at a time; the graphics themselves are
66// held by the platform and the service locator of the UI thread, so their
67// last handle is given back there.
68unsafe impl Send for FerroNativeGlPlatformGraphics {}
69// SAFETY: see `Send`.
70unsafe impl Sync for FerroNativeGlPlatformGraphics {}
71
72impl FerroNativeGlPlatformGraphics {
73    /// Creates a context to find out the version of OpenGL the system
74    /// gives, the entry points and the format of the default framebuffer,
75    /// then the shared context.
76    ///
77    /// Fails, as the original throws, when the native display cannot create
78    /// a context or make it current, and when the factory has no memory
79    /// management helper.
80    pub fn new(
81        display: ComPtr<IFrnGlDisplay>,
82        factory: &ComPtr<IFerroNativeFactory>,
83    ) -> Result<FerroNativeGlPlatformGraphics, OpenGlException> {
84        let context = display
85            .create_context(None)
86            .map_err(native_error("CreateContext"))?
87            .ok_or_else(|| OpenGlException::new("CreateContext returned no context"))?;
88
89        let gl_interface;
90        let version;
91        {
92            let current = context.make_current().map_err(native_error("MakeCurrent"))?;
93            let get_proc_address = proc_address_of(&display);
94            // SAFETY: `GetProcAddress` of the native display resolves the
95            // names with `dlsym` in the OpenGL library of the system, and the
96            // context of that library created above is current on this
97            // thread until `current` is released.
98            let basic = unsafe { GlBasicInfoInterface::new(&get_proc_address) };
99            let major = basic.get_integerv(GL_MAJOR_VERSION);
100            let minor = basic.get_integerv(GL_MINOR_VERSION);
101            let profile_mask = basic.get_integerv(GL_CONTEXT_PROFILE_MASK);
102            let is_compatibility_profile =
103                (profile_mask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) == GL_CONTEXT_COMPATIBILITY_PROFILE_BIT;
104
105            version = GlVersion::with_compatibility_profile(GlProfileType::OpenGL, major, minor, is_compatibility_profile);
106            // SAFETY: as above.
107            gl_interface = Rc::new(unsafe { GlInterface::new(version, proc_address_of(&display)) });
108            drop(current);
109        }
110
111        let gl_display = Rc::new(GlDisplay::new(
112            display.clone(),
113            gl_interface,
114            context.get_sample_count(),
115            context.get_stencil_size(),
116            factory.clone(),
117        )?);
118        let shared_context = GlContext::new(
119            &gl_display,
120            None,
121            display
122                .create_context(None)
123                .map_err(native_error("CreateContext"))?
124                .ok_or_else(|| OpenGlException::new("CreateContext returned no context"))?,
125            version,
126        );
127
128        Ok(FerroNativeGlPlatformGraphics { display, gl_display, version, shared_context })
129    }
130
131    /// Whether contexts may share their objects: always.
132    pub fn can_share_contexts(&self) -> bool {
133        true
134    }
135
136    /// Whether the graphics create contexts: always.
137    pub fn can_create_contexts(&self) -> bool {
138        true
139    }
140
141    /// The display the contexts belong to.
142    pub fn gl_display(&self) -> &Rc<GlDisplay> {
143        &self.gl_display
144    }
145
146    /// The context every top-level renders with.
147    pub fn shared_context(&self) -> &Rc<GlContext> {
148        &self.shared_context
149    }
150}
151
152impl IPlatformGraphics for FerroNativeGlPlatformGraphics {
153    fn uses_shared_context(&self) -> bool {
154        true
155    }
156
157    /// A new context of the display, which shares nothing with the shared
158    /// context, as in the original.
159    ///
160    /// # Panics
161    /// Panics when the native display cannot create a context.
162    fn create_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
163        let native = self.display.create_context(None).check();
164        let Some(native) = native else {
165            panic!("CreateContext returned no context");
166        };
167        GlContext::new(&self.gl_display, None, native, self.version)
168    }
169
170    fn get_shared_context(&self) -> Rc<dyn IPlatformGraphicsContext> {
171        self.shared_context.clone()
172    }
173}
174
175/// The function that resolves the entry points of OpenGL through the native
176/// display (`dlsym` in the OpenGL library of the system).
177fn proc_address_of(display: &ComPtr<IFrnGlDisplay>) -> GetProcAddress {
178    let display = display.clone();
179    Rc::new(move |name: &str| -> *const c_void {
180        let Ok(name) = CString::new(name) else {
181            return std::ptr::null();
182        };
183        display.get_proc_address(Some(&name)) as *const c_void
184    })
185}
186
187/// Turns the failure of a native call into the exception of the original.
188fn native_error(call: &'static str) -> impl Fn(HResult) -> OpenGlException {
189    move |error| OpenGlException::new(format!("{call} failed: {error:?}"))
190}
191
192/// The native OpenGL display and what every context of it shares: the
193/// entry points, the format of the default framebuffer and the objects of
194/// the native factory the contexts use.
195pub struct GlDisplay {
196    display: ComPtr<IFrnGlDisplay>,
197    gl_interface: Rc<GlInterface>,
198    sample_count: i32,
199    stencil_size: i32,
200    factory: ComPtr<IFerroNativeFactory>,
201    memory_helper: ComPtr<IFrnNativeObjectsMemoryManagement>,
202}
203
204impl GlDisplay {
205    /// Fails when the factory has no memory management helper.
206    // The factory is kept, as in the original, for the import of shared
207    // events (`ImportMTLSharedEvent`), which a frame of either thread may ask
208    // for. That call has no state on the native side (it wraps the event in
209    // an object of its own), the reference count of the factory is atomic,
210    // and the reference held here is never its last one: the platform binds
211    // the factory in the service locator of the UI thread for the life of
212    // the process (`FerroNativePlatform::initialize_with_factory`). So the
213    // destructor of the native factory, which must run on the UI thread
214    // (DEVIATIONS.md, Native backend), never runs from here.
215    fn new(
216        display: ComPtr<IFrnGlDisplay>,
217        gl_interface: Rc<GlInterface>,
218        sample_count: i32,
219        stencil_size: i32,
220        factory: ComPtr<IFerroNativeFactory>,
221    ) -> Result<GlDisplay, OpenGlException> {
222        let memory_helper = factory
223            .create_memory_management_helper()
224            .map_err(native_error("CreateMemoryManagementHelper"))?
225            .ok_or_else(|| OpenGlException::new("CreateMemoryManagementHelper returned no helper"))?;
226        Ok(GlDisplay { display, sample_count, stencil_size, factory, gl_interface, memory_helper })
227    }
228
229    pub fn gl_interface(&self) -> &Rc<GlInterface> {
230        &self.gl_interface
231    }
232
233    pub fn sample_count(&self) -> i32 {
234        self.sample_count
235    }
236
237    pub fn stencil_size(&self) -> i32 {
238        self.stencil_size
239    }
240
241    pub fn factory(&self) -> &ComPtr<IFerroNativeFactory> {
242        &self.factory
243    }
244
245    pub fn memory_helper(&self) -> &ComPtr<IFrnNativeObjectsMemoryManagement> {
246        &self.memory_helper
247    }
248
249    /// Makes no context current on this thread.
250    pub fn clear_context(&self) {
251        self.display.legacy_clear_current_context();
252    }
253
254    /// Creates a context that shares its objects with `share`.
255    ///
256    /// # Panics
257    /// Panics when `share` is lost.
258    pub fn create_shared_context(self: &Rc<Self>, share: &Rc<GlContext>) -> Result<Rc<GlContext>, OpenGlException> {
259        let share_native = share.native();
260        let native = self
261            .display
262            .create_context(Some(&share_native))
263            .map_err(native_error("CreateContext"))?
264            .ok_or_else(|| OpenGlException::new("CreateContext returned no context"))?;
265        Ok(GlContext::new(self, Some(share.clone()), native, share.version()))
266    }
267}
268
269/// A CGL context.
270pub struct GlContext {
271    this: Weak<GlContext>,
272    display: Rc<GlDisplay>,
273    shared_with: Option<Rc<GlContext>>,
274    handle_wrap_feature: Rc<GpuHandleWrapFeature>,
275    external_objects: Rc<GlExternalObjectsFeature>,
276    context: RefCell<Option<ComPtr<IFrnGlContext>>>,
277    version: GlVersion,
278}
279
280impl GlContext {
281    pub(crate) fn new(
282        display: &Rc<GlDisplay>,
283        shared_with: Option<Rc<GlContext>>,
284        context: ComPtr<IFrnGlContext>,
285        version: GlVersion,
286    ) -> Rc<GlContext> {
287        Rc::new_cyclic(|this| GlContext {
288            this: this.clone(),
289            display: display.clone(),
290            shared_with,
291            handle_wrap_feature: Rc::new(GpuHandleWrapFeature::new(display.memory_helper().clone())),
292            external_objects: Rc::new(GlExternalObjectsFeature::new(this.clone(), &context, display.clone())),
293            context: RefCell::new(Some(context)),
294            version,
295        })
296    }
297
298    /// The native context; `None` once the context is disposed (lost).
299    pub fn context(&self) -> Option<ComPtr<IFrnGlContext>> {
300        self.context.borrow().clone()
301    }
302
303    /// The native context.
304    ///
305    /// # Panics
306    /// Panics when the context is lost.
307    #[track_caller]
308    fn native(&self) -> ComPtr<IFrnGlContext> {
309        self.throw_if_lost();
310        self.context().expect("the context is not lost")
311    }
312
313    /// # Panics
314    /// Panics when the context is lost (the
315    /// `PlatformGraphicsContextLostException` of the original).
316    #[track_caller]
317    pub fn throw_if_lost(&self) {
318        if self.is_lost() {
319            panic!("{}", PlatformGraphicsContextLostException);
320        }
321    }
322
323    /// Makes the context current, or says why it cannot be: for the callers
324    /// that ignore a broken context.
325    fn try_make_current(&self) -> Result<Rc<dyn IDisposable>, HResult> {
326        let Some(native) = self.context() else {
327            return Err(HResult::FAIL);
328        };
329        let saved = native.make_current()?;
330        Ok(Rc::new(RestoreContext { saved: RefCell::new(saved) }))
331    }
332}
333
334/// What making a context current returned: the native object that unlocks
335/// the context and makes the context that was current before current again
336/// when it is released.
337struct RestoreContext {
338    saved: RefCell<Option<ComPtr<IUnknown>>>,
339}
340
341impl IDisposable for RestoreContext {
342    fn dispose(&self) {
343        let saved = self.saved.borrow_mut().take();
344        drop(saved);
345    }
346}
347
348impl Drop for RestoreContext {
349    fn drop(&mut self) {
350        self.dispose();
351    }
352}
353
354impl IOptionalFeatureProvider for GlContext {
355    fn try_get_feature(&self, feature_type: TypeId) -> Option<Rc<dyn Any>> {
356        if feature_type == TypeId::of::<dyn IExternalObjectsHandleWrapRenderInterfaceContextFeature>() {
357            let feature: Rc<dyn IExternalObjectsHandleWrapRenderInterfaceContextFeature> =
358                self.handle_wrap_feature.clone();
359            return Some(Rc::new(feature));
360        }
361        if feature_type == TypeId::of::<dyn IGlContextExternalObjectsFeature>() {
362            let feature: Rc<dyn IGlContextExternalObjectsFeature> = self.external_objects.clone();
363            return Some(Rc::new(feature));
364        }
365        // The context as an OpenGL context: what the original tests the
366        // object for.
367        if feature_type == TypeId::of::<dyn IGlContext>() {
368            let this: Rc<dyn IGlContext> = self.this.upgrade()?;
369            return Some(Rc::new(this));
370        }
371        None
372    }
373}
374
375impl IPlatformGraphicsContext for GlContext {
376    fn is_lost(&self) -> bool {
377        self.context.borrow().is_none()
378    }
379
380    fn ensure_current(&self) -> Rc<dyn IDisposable> {
381        IGlContext::make_current(self)
382    }
383
384    fn dispose(&self) {
385        let context = self.context.borrow_mut().take();
386        drop(context);
387    }
388
389    fn as_any(&self) -> &dyn Any {
390        self
391    }
392}
393
394impl IGlContext for GlContext {
395    fn version(&self) -> GlVersion {
396        self.version
397    }
398
399    fn gl_interface(&self) -> Rc<GlInterface> {
400        self.display.gl_interface().clone()
401    }
402
403    fn sample_count(&self) -> i32 {
404        self.display.sample_count()
405    }
406
407    fn stencil_size(&self) -> i32 {
408        self.display.stencil_size()
409    }
410
411    /// # Panics
412    /// Panics when the context is lost and when the native context cannot
413    /// be made current.
414    fn make_current(&self) -> Rc<dyn IDisposable> {
415        self.throw_if_lost();
416        self.try_make_current().check()
417    }
418
419    /// # Panics
420    /// Panics when `context` is not a context of this backend (the failing
421    /// cast of the original).
422    fn is_shared_with(&self, context: &dyn IGlContext) -> bool {
423        let Some(c) = context.as_any().downcast_ref::<GlContext>() else {
424            panic!("Unable to cast the context to type 'GlContext'.");
425        };
426        let same = |a: &GlContext, b: &GlContext| std::ptr::eq(a, b);
427        same(c, self)
428            || c.shared_with.as_deref().is_some_and(|shared| same(shared, self))
429            || self.shared_with.as_deref().is_some_and(|shared| same(shared, c))
430            || match (&self.shared_with, &c.shared_with) {
431                (Some(a), Some(b)) => same(a, b),
432                _ => false,
433            }
434    }
435
436    fn can_create_shared_context(&self) -> bool {
437        true
438    }
439
440    /// Creates a context that shares its objects with this one (with the
441    /// context this one shares with, when it does). The preferred versions
442    /// are not used, as in the original. A failure is logged and gives
443    /// `None` (the original throws), as for the contexts of GLX and EGL.
444    fn create_shared_context(&self, _preferred_versions: Option<&[GlVersion]>) -> Option<Rc<dyn IGlContext>> {
445        let share = match &self.shared_with {
446            Some(shared_with) => shared_with.clone(),
447            None => self.this.upgrade()?,
448        };
449        match self.display.create_shared_context(&share) {
450            Ok(context) => Some(context),
451            Err(error) => {
452                if let Some(logger) = Logger::try_get(LogEventLevel::Error, "OpenGL") {
453                    let source: &dyn Any = &"GlContext";
454                    logger.log_with_values(Some(source), "Unable to create a shared context: {0}", &[&error]);
455                }
456                None
457            }
458        }
459    }
460}
461
462/// The OpenGL render target of a top-level.
463pub struct GlPlatformSurfaceRenderTarget {
464    target: RefCell<Option<ComPtr<IFrnGlSurfaceRenderTarget>>>,
465    context: Rc<dyn IGlContext>,
466}
467
468impl GlPlatformSurfaceRenderTarget {
469    pub(crate) fn new(target: ComPtr<IFrnGlSurfaceRenderTarget>, context: Rc<dyn IGlContext>) -> Self {
470        Self { target: RefCell::new(Some(target)), context }
471    }
472}
473
474impl IPlatformRenderSurfaceRenderTarget for GlPlatformSurfaceRenderTarget {
475    fn state(&self) -> PlatformRenderTargetState {
476        PlatformRenderTargetState::READY
477    }
478}
479
480impl IGlPlatformSurfaceRenderTarget for GlPlatformSurfaceRenderTarget {
481    /// # Panics
482    /// Panics when the render target is disposed and when the native target
483    /// cannot begin a frame.
484    fn begin_draw(&self, _scene_info: &RenderTargetSceneInfo) -> Rc<dyn IGlPlatformSurfaceRenderingSession> {
485        // The original: "TODO: use expectedPixelSize".
486        let Some(target) = self.target.borrow().clone() else {
487            panic!("Cannot access a disposed object: GlPlatformSurfaceRenderTarget");
488        };
489        let session = target.begin_drawing().check();
490        let Some(session) = session else {
491            panic!("BeginDrawing returned no session");
492        };
493        Rc::new(GlPlatformSurfaceRenderingSession::new(self.context.clone(), session))
494    }
495
496    fn dispose(&self) {
497        let target = self.target.borrow_mut().take();
498        drop(target);
499    }
500}
501
502/// One frame drawn to the OpenGL render target of a top-level: the native
503/// session made the context current and bound the framebuffer of the
504/// IOSurface; releasing it presents the frame and restores the context.
505pub struct GlPlatformSurfaceRenderingSession {
506    context: Rc<dyn IGlContext>,
507    session: RefCell<Option<ComPtr<IFrnGlSurfaceRenderingSession>>>,
508}
509
510impl GlPlatformSurfaceRenderingSession {
511    pub(crate) fn new(context: Rc<dyn IGlContext>, session: ComPtr<IFrnGlSurfaceRenderingSession>) -> Self {
512        Self { context, session: RefCell::new(Some(session)) }
513    }
514
515    /// Calls the native session where it is held.
516    ///
517    /// # Panics
518    /// Panics when the session is disposed.
519    #[track_caller]
520    fn with_session<R>(&self, f: impl FnOnce(&IFrnGlSurfaceRenderingSession) -> R) -> R {
521        match self.session.borrow().as_ref() {
522            Some(session) => f(session),
523            None => panic!("Cannot access a disposed object: GlPlatformSurfaceRenderingSession"),
524        }
525    }
526}
527
528impl IGlPlatformSurfaceRenderingSession for GlPlatformSurfaceRenderingSession {
529    fn context(&self) -> Rc<dyn IGlContext> {
530        self.context.clone()
531    }
532
533    fn size(&self) -> PixelSize {
534        let s = self.with_session(|session| session.get_pixel_size()).check();
535        PixelSize::new(s.width, s.height)
536    }
537
538    fn scaling(&self) -> f64 {
539        self.with_session(|session| session.get_scaling()).check()
540    }
541
542    fn is_y_flipped(&self) -> bool {
543        true
544    }
545
546    fn dispose(&self) {
547        let session = self.session.borrow_mut().take();
548        drop(session);
549    }
550}
551
552/// The OpenGL render surface of a top-level.
553///
554/// The surface is shared with the thread that renders, like the Metal
555/// surface (`metal.rs`): what it holds of the native side is the native
556/// top-level, which only the UI thread may use, so the render target is
557/// created on the UI thread, as in the original, and the surface is not
558/// ready on any other thread.
559pub struct GlPlatformSurface {
560    weak_self: std::sync::Weak<GlPlatformSurface>,
561    top_level: SurfaceTopLevel,
562}
563
564impl GlPlatformSurface {
565    pub(crate) fn new(top_level: ComPtr<IFrnTopLevel>) -> Arc<GlPlatformSurface> {
566        Arc::new_cyclic(|weak_self| GlPlatformSurface {
567            weak_self: weak_self.clone(),
568            top_level: SurfaceTopLevel::new(top_level),
569        })
570    }
571
572    /// Releases the native top-level; called by the top-level when it is
573    /// disposed, on the UI thread.
574    pub(crate) fn close(&self) {
575        self.top_level.release();
576    }
577}
578
579impl IPlatformRenderSurface for GlPlatformSurface {
580    /// Deviation (DEVIATIONS.md, Native backend), as for the Metal surface:
581    /// the original throws `RenderTargetNotReadyException` from
582    /// `CreateGlRenderTarget` off the UI thread and the composition target
583    /// catches it; here the surface answers that it is not ready there.
584    fn is_ready(&self) -> bool {
585        self.top_level.get().is_some()
586    }
587
588    fn try_get_surface_kind(&self, kind: TypeId) -> Option<Rc<dyn Any>> {
589        if kind == TypeId::of::<dyn IGlPlatformSurface>() {
590            // The contract hands out an `Rc`, and the surface lives in an
591            // `Arc`: the view forwards to the surface.
592            let this: Rc<dyn IGlPlatformSurface> = Rc::new(GlPlatformSurfaceView(self.weak_self.upgrade()?));
593            return Some(Rc::new(this));
594        }
595        None
596    }
597
598    fn as_any(&self) -> &dyn Any {
599        self
600    }
601}
602
603impl IGlPlatformSurface for GlPlatformSurface {
604    /// # Panics
605    /// Panics when called off the UI thread or when the top-level is
606    /// disposed (the render target is not ready), when `context` is not a
607    /// context of this backend and when the native top-level cannot create
608    /// the render target.
609    fn create_gl_render_target(&self, context: &Rc<dyn IGlContext>) -> Rc<dyn IGlPlatformSurfaceRenderTarget> {
610        if !Dispatcher::ui_thread().check_access() {
611            panic!("{}", RenderTargetNotReadyException::new());
612        }
613        let Some(top_level) = self.top_level.get() else {
614            panic!("{}", RenderTargetNotReadyException::new());
615        };
616        let Some(native_context) = context.as_any().downcast_ref::<GlContext>() else {
617            panic!("Unable to cast the context to type 'GlContext'.");
618        };
619        let native = native_context.context();
620        let target = top_level.create_gl_render_target(native.as_deref()).check();
621        let Some(target) = target else {
622            panic!("CreateGlRenderTarget returned no render target");
623        };
624        Rc::new(GlPlatformSurfaceRenderTarget::new(target, context.clone()))
625    }
626}
627
628/// The OpenGL surface as the contract hands it out
629/// (`Rc<dyn IGlPlatformSurface>`); it forwards to the shared surface.
630struct GlPlatformSurfaceView(Arc<GlPlatformSurface>);
631
632impl IPlatformRenderSurface for GlPlatformSurfaceView {
633    fn is_ready(&self) -> bool {
634        self.0.is_ready()
635    }
636
637    fn try_get_surface_kind(&self, kind: TypeId) -> Option<Rc<dyn Any>> {
638        self.0.try_get_surface_kind(kind)
639    }
640
641    fn as_any(&self) -> &dyn Any {
642        self.0.as_any()
643    }
644}
645
646impl IGlPlatformSurface for GlPlatformSurfaceView {
647    fn create_gl_render_target(&self, context: &Rc<dyn IGlContext>) -> Rc<dyn IGlPlatformSurfaceRenderTarget> {
648        self.0.create_gl_render_target(context)
649    }
650}
651
652/// The external objects of a CGL context: the import of an IOSurface as a
653/// rectangle texture and of a Metal shared event as a timeline semaphore.
654// The original holds its context, which holds the feature: here the feature
655// holds the context weakly, so that the two do not keep each other alive.
656pub(crate) struct GlExternalObjectsFeature {
657    context: Weak<GlContext>,
658    display: Rc<GlDisplay>,
659    device_luid: Option<Vec<u8>>,
660}
661
662impl GlExternalObjectsFeature {
663    fn new(context: Weak<GlContext>, native: &IFrnGlContext, display: Rc<GlDisplay>) -> GlExternalObjectsFeature {
664        let mut registry_id = 0u64;
665        // SAFETY: the pointer is valid for the one value the native method
666        // writes.
667        let has_registry_id = unsafe { native.get_io_kit_registry_id(&mut registry_id) };
668        // The bytes are reversed to match MoltenVK (LUID is a Vulkan term
669        // after all): the bytes of the identifier, the most significant
670        // first, as for the Metal device.
671        let device_luid = has_registry_id.then(|| registry_id.to_be_bytes().to_vec());
672        GlExternalObjectsFeature { context, display, device_luid }
673    }
674
675    /// # Panics
676    /// Panics when the context is gone.
677    #[track_caller]
678    fn context(&self) -> Rc<GlContext> {
679        match self.context.upgrade() {
680            Some(context) => context,
681            None => panic!("{}", PlatformGraphicsContextLostException),
682        }
683    }
684}
685
686impl IGlContextExternalObjectsFeature for GlExternalObjectsFeature {
687    fn supported_importable_external_image_types(&self) -> Vec<String> {
688        vec![KnownPlatformGraphicsExternalImageHandleTypes::IO_SURFACE_REF.to_string()]
689    }
690
691    fn supported_exportable_external_image_types(&self) -> Vec<String> {
692        Vec::new()
693    }
694
695    fn supported_importable_external_semaphore_types(&self) -> Vec<String> {
696        vec![KnownPlatformGraphicsExternalSemaphoreHandleTypes::METAL_SHARED_EVENT.to_string()]
697    }
698
699    fn supported_exportable_external_semaphore_types(&self) -> Vec<String> {
700        Vec::new()
701    }
702
703    fn get_supported_formats_for_external_memory_type(&self, _type_: &str) -> Vec<PlatformGraphicsExternalImageFormat> {
704        vec![PlatformGraphicsExternalImageFormat::B8G8R8A8UNorm]
705    }
706
707    fn create_image(
708        &self,
709        _type_: &str,
710        _size: PixelSize,
711        _format: PlatformGraphicsExternalImageFormat,
712    ) -> Result<Rc<dyn IGlExportableExternalImageTexture>, OpenGlException> {
713        Err(OpenGlException::new("Specified method is not supported."))
714    }
715
716    fn create_semaphore(&self, _type_: &str) -> Result<Rc<dyn IGlExportableExternalImageTexture>, OpenGlException> {
717        Err(OpenGlException::new("Specified method is not supported."))
718    }
719
720    fn import_image(
721        &self,
722        handle: Rc<dyn IPlatformHandle>,
723        properties: PlatformGraphicsExternalImageProperties,
724    ) -> Result<Rc<dyn IGlExternalImageTexture>, OpenGlException> {
725        if handle.handle_descriptor() == Some(KnownPlatformGraphicsExternalImageHandleTypes::IO_SURFACE_REF) {
726            if properties.format != PlatformGraphicsExternalImageFormat::B8G8R8A8UNorm {
727                return Err(OpenGlException::new("Only B8G8R8A8UNorm format is supported for IOSurfaceRef"));
728            }
729            let context = self.context();
730            let current = context.ensure_current();
731            let gl = context.gl_interface();
732            let old_texture = gl.get_integerv(GL_TEXTURE_BINDING_RECTANGLE);
733            let texture_id = gl.gen_texture();
734            gl.bind_texture(GL_TEXTURE_RECTANGLE, texture_id);
735            // SAFETY: by its descriptor the handle is an `IOSurfaceRef`,
736            // which the caller keeps alive for the call (the compositor
737            // hands over the handle the wrapping feature of the context
738            // made of it); the texture CGL binds to it retains the surface.
739            let error = unsafe {
740                context.native().tex_image_io_surface2d(
741                    GL_TEXTURE_RECTANGLE,
742                    GL_RGBA8,
743                    properties.width,
744                    properties.height,
745                    GL_BGRA,
746                    GL_UNSIGNED_INT_8_8_8_8_REV,
747                    handle.handle() as *mut c_void,
748                    0,
749                )
750            };
751            gl.bind_texture(GL_TEXTURE_RECTANGLE, old_texture);
752
753            if error != 0 {
754                gl.delete_texture(texture_id);
755                current.dispose();
756                return Err(OpenGlException::new(format!("CGLTexImageIOSurface2D returned {error}")));
757            }
758            current.dispose();
759            return Ok(Rc::new(ImportedTexture::new(
760                context,
761                GL_TEXTURE_RECTANGLE,
762                texture_id,
763                GL_RGBA8,
764                properties,
765            )));
766        }
767        Err(OpenGlException::new("This handle type is not supported"))
768    }
769
770    fn import_semaphore(&self, handle: Rc<dyn IPlatformHandle>) -> Result<Rc<dyn IGlExternalSemaphore>, OpenGlException> {
771        if handle.handle_descriptor() == Some(KnownPlatformGraphicsExternalSemaphoreHandleTypes::METAL_SHARED_EVENT) {
772            // SAFETY: by its descriptor the handle is an
773            // `id<MTLSharedEvent>`, which the caller keeps alive for the
774            // call; the native side makes an object of its own that retains
775            // the event.
776            let imported = unsafe { self.display.factory().import_mtl_shared_event(handle.handle() as *mut c_void) }
777                .map_err(native_error("ImportMTLSharedEvent"))?
778                .ok_or_else(|| OpenGlException::new("ImportMTLSharedEvent returned no event"))?;
779            return Ok(Rc::new(MtlEventSemaphore::new(imported)));
780        }
781
782        Err(OpenGlException::new("This handle type is not supported"))
783    }
784
785    fn get_synchronization_capabilities(
786        &self,
787        _image_handle_type: &str,
788    ) -> CompositionGpuImportedImageSynchronizationCapabilities {
789        CompositionGpuImportedImageSynchronizationCapabilities::AUTOMATIC
790            | CompositionGpuImportedImageSynchronizationCapabilities::TIMELINE_SEMAPHORES
791    }
792
793    fn device_luid(&self) -> Option<Vec<u8>> {
794        self.device_luid.clone()
795    }
796
797    fn device_uuid(&self) -> Option<Vec<u8>> {
798        None
799    }
800}
801
802/// A Metal shared event imported as a timeline semaphore.
803pub(crate) struct MtlEventSemaphore {
804    inner: RefCell<Option<ComPtr<IFrnMTLSharedEvent>>>,
805}
806
807impl MtlEventSemaphore {
808    fn new(inner: ComPtr<IFrnMTLSharedEvent>) -> MtlEventSemaphore {
809        MtlEventSemaphore { inner: RefCell::new(Some(inner)) }
810    }
811
812    /// # Panics
813    /// Panics when the semaphore is disposed.
814    #[track_caller]
815    fn inner(&self) -> ComPtr<IFrnMTLSharedEvent> {
816        match self.inner.borrow().clone() {
817            Some(inner) => inner,
818            None => panic!("Cannot access a disposed object: MtlEventSemaphore"),
819        }
820    }
821}
822
823impl IGlExternalSemaphore for MtlEventSemaphore {
824    /// # Panics
825    /// Always: a shared event is a timeline semaphore.
826    fn wait_semaphore(&self, _texture: &dyn IGlExternalImageTexture) {
827        panic!("This is a timeline semaphore");
828    }
829
830    /// # Panics
831    /// Always: a shared event is a timeline semaphore.
832    fn signal_semaphore(&self, _texture: &dyn IGlExternalImageTexture) {
833        panic!("This is a timeline semaphore");
834    }
835
836    fn wait_timeline_semaphore(&self, _texture: &dyn IGlExternalImageTexture, value: u64) {
837        self.inner().wait(value, 1000);
838    }
839
840    fn signal_timeline_semaphore(&self, _texture: &dyn IGlExternalImageTexture, value: u64) {
841        self.inner().set_signaled_value(value);
842    }
843
844    fn dispose(&self) {
845        let inner = self.inner.borrow_mut().take();
846        drop(inner);
847    }
848}
849
850/// An IOSurface imported as a texture of a context.
851pub(crate) struct ImportedTexture {
852    context: Rc<GlContext>,
853    type_: i32,
854    id: i32,
855    internal_format: i32,
856    properties: PlatformGraphicsExternalImageProperties,
857    disposed: Cell<bool>,
858}
859
860impl ImportedTexture {
861    fn new(
862        context: Rc<GlContext>,
863        type_: i32,
864        id: i32,
865        internal_format: i32,
866        properties: PlatformGraphicsExternalImageProperties,
867    ) -> ImportedTexture {
868        ImportedTexture { context, type_, id, internal_format, properties, disposed: Cell::new(false) }
869    }
870}
871
872impl IGlExternalImageTexture for ImportedTexture {
873    /// # Panics
874    /// Always: an IOSurface has no keyed mutex.
875    fn acquire_keyed_mutex(&self, _key: u32) {
876        panic!("Specified method is not supported.");
877    }
878
879    /// # Panics
880    /// Always: an IOSurface has no keyed mutex.
881    fn release_keyed_mutex(&self, _key: u32) {
882        panic!("Specified method is not supported.");
883    }
884
885    fn texture_id(&self) -> i32 {
886        self.id
887    }
888
889    fn internal_format(&self) -> i32 {
890        self.internal_format
891    }
892
893    fn texture_type(&self) -> i32 {
894        self.type_
895    }
896
897    fn properties(&self) -> PlatformGraphicsExternalImageProperties {
898        self.properties.clone()
899    }
900
901    fn dispose(&self) {
902        if self.disposed.replace(true) {
903            return;
904        }
905        // A context that is lost or cannot be made current is likely
906        // broken: the texture is left to it, as in the original.
907        if let Ok(current) = self.context.try_make_current() {
908            self.context.gl_interface().delete_texture(self.id);
909            current.dispose();
910        }
911    }
912}
913
914#[cfg(test)]
915mod tests {
916    // Not from upstream, which has no tests of these classes: the graphics
917    // over the real native display, the OpenGL of the system and real
918    // IOSurfaces and Metal shared events. None of this needs a window.
919    use super::*;
920    use crate::interop::create_ferro_native;
921    use ferroui_base::platform::PlatformHandle;
922    use std::ffi::c_char;
923    use std::panic::{catch_unwind, AssertUnwindSafe};
924
925    fn graphics() -> Option<FerroNativeGlPlatformGraphics> {
926        let factory = create_ferro_native().expect("factory");
927        let display = factory.obtain_gl_display().ok().flatten()?;
928        match FerroNativeGlPlatformGraphics::new(display, &factory) {
929            Ok(graphics) => Some(graphics),
930            Err(error) => {
931                eprintln!("skipped: OpenGL is not available: {error}");
932                None
933            }
934        }
935    }
936
937    fn gl_context(context: Rc<dyn IPlatformGraphicsContext>) -> Rc<dyn IGlContext> {
938        let features: &dyn IOptionalFeatureProvider = &*context;
939        features.try_get::<dyn IGlContext>().expect("an OpenGL context")
940    }
941
942    fn external_objects(context: &Rc<dyn IGlContext>) -> Rc<dyn IGlContextExternalObjectsFeature> {
943        let features: &dyn IOptionalFeatureProvider = &**context;
944        features.try_get::<dyn IGlContextExternalObjectsFeature>().expect("the feature")
945    }
946
947    /// The message of the panic `f` ends with.
948    fn panic_message(f: impl FnOnce()) -> String {
949        let payload = catch_unwind(AssertUnwindSafe(f)).expect_err("a panic");
950        match payload.downcast::<String>() {
951            Ok(message) => *message,
952            Err(payload) => payload.downcast::<&str>().map(|message| message.to_string()).unwrap_or_default(),
953        }
954    }
955
956    #[test]
957    fn the_graphics_hand_out_one_shared_context_of_the_core_profile() {
958        let Some(graphics) = graphics() else { return };
959
960        assert!(graphics.uses_shared_context());
961        assert!(graphics.can_share_contexts());
962        assert!(graphics.can_create_contexts());
963        let shared = gl_context(graphics.get_shared_context());
964        assert!(Rc::ptr_eq(&shared, &gl_context(graphics.get_shared_context())));
965
966        let version = shared.version();
967        assert_eq!(GlProfileType::OpenGL, version.type_());
968        assert!(version.major() > 3 || version.major() == 3 && version.minor() >= 2, "{version:?}");
969        assert!(!version.is_compatibility_profile());
970
971        // The entry points work with the context current.
972        let current = shared.make_current();
973        assert_eq!(version.major(), shared.gl_interface().get_integerv(GL_MAJOR_VERSION));
974        current.dispose();
975        assert!(shared.stencil_size() >= 8);
976        assert!(shared.sample_count() >= 0);
977    }
978
979    #[test]
980    fn contexts_share_with_the_context_they_were_created_from() {
981        let Some(graphics) = graphics() else { return };
982        let shared = gl_context(graphics.get_shared_context());
983        let other = gl_context(graphics.create_context());
984
985        assert!(shared.is_shared_with(&*shared));
986        assert!(!shared.is_shared_with(&*other));
987        assert!(!other.is_shared_with(&*shared));
988
989        assert!(shared.can_create_shared_context());
990        let child = shared.create_shared_context(None).expect("a shared context");
991        assert!(child.is_shared_with(&*shared));
992        assert!(shared.is_shared_with(&*child));
993        // A context created from a shared one shares with the same context.
994        let grandchild = child.create_shared_context(None).expect("a shared context");
995        assert!(grandchild.is_shared_with(&*child));
996        assert!(grandchild.is_shared_with(&*shared));
997        assert!(!grandchild.is_shared_with(&*other));
998
999        child.dispose();
1000        grandchild.dispose();
1001        other.dispose();
1002    }
1003
1004    #[test]
1005    fn a_disposed_context_is_lost() {
1006        let Some(graphics) = graphics() else { return };
1007        let context = gl_context(graphics.create_context());
1008        assert!(!context.is_lost());
1009        context.dispose();
1010        assert!(context.is_lost());
1011        assert_eq!("The platform graphics context was lost.", panic_message(|| drop(context.make_current())));
1012    }
1013
1014    #[test]
1015    fn the_external_objects_feature_names_what_it_imports() {
1016        let Some(graphics) = graphics() else { return };
1017        let context = gl_context(graphics.get_shared_context());
1018        let feature = external_objects(&context);
1019
1020        assert_eq!(vec!["IOSurfaceRef".to_string()], feature.supported_importable_external_image_types());
1021        assert!(feature.supported_exportable_external_image_types().is_empty());
1022        assert_eq!(vec!["MetalSharedEvent".to_string()], feature.supported_importable_external_semaphore_types());
1023        assert!(feature.supported_exportable_external_semaphore_types().is_empty());
1024        assert_eq!(
1025            vec![PlatformGraphicsExternalImageFormat::B8G8R8A8UNorm],
1026            feature.get_supported_formats_for_external_memory_type("IOSurfaceRef")
1027        );
1028        assert_eq!(
1029            CompositionGpuImportedImageSynchronizationCapabilities::AUTOMATIC
1030                | CompositionGpuImportedImageSynchronizationCapabilities::TIMELINE_SEMAPHORES,
1031            feature.get_synchronization_capabilities("IOSurfaceRef")
1032        );
1033        assert_eq!(None, feature.device_uuid());
1034        if let Some(luid) = feature.device_luid() {
1035            assert_eq!(8, luid.len());
1036        }
1037
1038        let size = PixelSize::new(4, 4);
1039        let format = PlatformGraphicsExternalImageFormat::B8G8R8A8UNorm;
1040        assert!(feature.create_image("IOSurfaceRef", size, format).is_err());
1041        assert!(feature.create_semaphore("MetalSharedEvent").is_err());
1042
1043        let event =
1044            Rc::new(PlatformHandle::new(1, Some(KnownPlatformGraphicsExternalSemaphoreHandleTypes::METAL_SHARED_EVENT)));
1045        let error = feature.import_image(event, properties(format)).err().expect("refused");
1046        assert_eq!("This handle type is not supported", error.to_string());
1047        let surface =
1048            Rc::new(PlatformHandle::new(1, Some(KnownPlatformGraphicsExternalImageHandleTypes::IO_SURFACE_REF)));
1049        let error = feature
1050            .import_image(surface.clone(), properties(PlatformGraphicsExternalImageFormat::R8G8B8A8UNorm))
1051            .err()
1052            .expect("refused");
1053        assert_eq!("Only B8G8R8A8UNorm format is supported for IOSurfaceRef", error.to_string());
1054        let error = feature.import_semaphore(surface).err().expect("refused");
1055        assert_eq!("This handle type is not supported", error.to_string());
1056    }
1057
1058    fn properties(format: PlatformGraphicsExternalImageFormat) -> PlatformGraphicsExternalImageProperties {
1059        PlatformGraphicsExternalImageProperties { width: 16, height: 8, format, ..Default::default() }
1060    }
1061
1062    // IOSurface and Core Foundation, for a surface to import.
1063    type CFTypeRef = *const c_void;
1064    #[link(name = "IOSurface", kind = "framework")]
1065    extern "C" {
1066        static kIOSurfaceWidth: CFTypeRef;
1067        static kIOSurfaceHeight: CFTypeRef;
1068        static kIOSurfaceBytesPerElement: CFTypeRef;
1069        static kIOSurfacePixelFormat: CFTypeRef;
1070        fn IOSurfaceCreate(properties: CFTypeRef) -> CFTypeRef;
1071    }
1072    #[link(name = "CoreFoundation", kind = "framework")]
1073    extern "C" {
1074        static kCFTypeDictionaryKeyCallBacks: c_void;
1075        static kCFTypeDictionaryValueCallBacks: c_void;
1076        fn CFDictionaryCreateMutable(
1077            allocator: CFTypeRef,
1078            capacity: isize,
1079            key_callbacks: *const c_void,
1080            value_callbacks: *const c_void,
1081        ) -> *mut c_void;
1082        fn CFDictionarySetValue(dictionary: *mut c_void, key: CFTypeRef, value: CFTypeRef);
1083        fn CFNumberCreate(allocator: CFTypeRef, type_: isize, value: *const c_void) -> CFTypeRef;
1084        fn CFRelease(object: CFTypeRef);
1085    }
1086
1087    /// A new IOSurface of 16 by 8 pixels, four bytes each, blue first.
1088    fn io_surface() -> CFTypeRef {
1089        const K_CF_NUMBER_SINT32_TYPE: isize = 3;
1090        // SAFETY: plain Core Foundation calls with valid arguments; every
1091        // object created here is released here, apart from the surface,
1092        // which the caller releases.
1093        unsafe {
1094            let dictionary = CFDictionaryCreateMutable(
1095                std::ptr::null(),
1096                0,
1097                &kCFTypeDictionaryKeyCallBacks,
1098                &kCFTypeDictionaryValueCallBacks,
1099            );
1100            let pixel_format = u32::from_be_bytes(*b"BGRA") as i32;
1101            for (key, value) in [
1102                (kIOSurfaceWidth, 16i32),
1103                (kIOSurfaceHeight, 8),
1104                (kIOSurfaceBytesPerElement, 4),
1105                (kIOSurfacePixelFormat, pixel_format),
1106            ] {
1107                let number = CFNumberCreate(std::ptr::null(), K_CF_NUMBER_SINT32_TYPE, (&value as *const i32).cast());
1108                CFDictionarySetValue(dictionary, key, number);
1109                CFRelease(number);
1110            }
1111            let surface = IOSurfaceCreate(dictionary);
1112            CFRelease(dictionary);
1113            surface
1114        }
1115    }
1116
1117    fn import(feature: &Rc<dyn IGlContextExternalObjectsFeature>, surface: CFTypeRef) -> Rc<dyn IGlExternalImageTexture> {
1118        let handle = Rc::new(PlatformHandle::new(
1119            surface as isize,
1120            Some(KnownPlatformGraphicsExternalImageHandleTypes::IO_SURFACE_REF),
1121        ));
1122        feature
1123            .import_image(handle, properties(PlatformGraphicsExternalImageFormat::B8G8R8A8UNorm))
1124            .expect("the surface is imported")
1125    }
1126
1127    #[test]
1128    fn an_io_surface_is_imported_as_a_rectangle_texture() {
1129        let Some(graphics) = graphics() else { return };
1130        let context = gl_context(graphics.get_shared_context());
1131        let feature = external_objects(&context);
1132        let surface = io_surface();
1133        assert!(!surface.is_null());
1134
1135        let texture = import(&feature, surface);
1136        assert_eq!(GL_TEXTURE_RECTANGLE, texture.texture_type());
1137        assert_eq!(GL_RGBA8, texture.internal_format());
1138        assert!(texture.texture_id() > 0);
1139        assert_eq!(16, texture.properties().width);
1140        assert_eq!(8, texture.properties().height);
1141        assert_eq!("Specified method is not supported.", panic_message(|| texture.acquire_keyed_mutex(0)));
1142        assert_eq!("Specified method is not supported.", panic_message(|| texture.release_keyed_mutex(0)));
1143
1144        // The import put the binding of the context back.
1145        let current = context.make_current();
1146        assert_eq!(0, context.gl_interface().get_integerv(GL_TEXTURE_BINDING_RECTANGLE));
1147        current.dispose();
1148
1149        texture.dispose();
1150        texture.dispose();
1151        // SAFETY: the surface was created above; the texture let go of it.
1152        unsafe { CFRelease(surface) };
1153    }
1154
1155    // Metal and the Objective-C runtime, for a shared event to import.
1156    #[link(name = "Metal", kind = "framework")]
1157    extern "C" {
1158        fn MTLCreateSystemDefaultDevice() -> *mut c_void;
1159    }
1160    #[link(name = "objc")]
1161    extern "C" {
1162        fn sel_registerName(name: *const c_char) -> *mut c_void;
1163        fn objc_msgSend();
1164        fn objc_release(object: *mut c_void);
1165    }
1166
1167    /// A new shared event of the default Metal device, or `None` without one.
1168    fn shared_event() -> Option<*mut c_void> {
1169        // SAFETY: `objc_msgSend` is called through a pointer with the C
1170        // signature of `newSharedEvent`; the device is a live object,
1171        // released here, and the event comes with the reference the caller
1172        // owns.
1173        unsafe {
1174            let device = MTLCreateSystemDefaultDevice();
1175            if device.is_null() {
1176                return None;
1177            }
1178            let send: unsafe extern "C" fn(*mut c_void, *mut c_void) -> *mut c_void =
1179                std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
1180            let event = send(device, sel_registerName(c"newSharedEvent".as_ptr()));
1181            objc_release(device);
1182            (!event.is_null()).then_some(event)
1183        }
1184    }
1185
1186    /// The `signaledValue` of a shared event.
1187    fn signaled_value(event: *mut c_void) -> u64 {
1188        // SAFETY: as in `shared_event`, with the signature of `signaledValue`.
1189        unsafe {
1190            let send: unsafe extern "C" fn(*mut c_void, *mut c_void) -> u64 =
1191                std::mem::transmute(objc_msgSend as unsafe extern "C" fn());
1192            send(event, sel_registerName(c"signaledValue".as_ptr()))
1193        }
1194    }
1195
1196    #[test]
1197    fn a_shared_event_is_imported_as_a_timeline_semaphore() {
1198        let Some(graphics) = graphics() else { return };
1199        let Some(event) = shared_event() else {
1200            eprintln!("skipped: no Metal device");
1201            return;
1202        };
1203        let context = gl_context(graphics.get_shared_context());
1204        let feature = external_objects(&context);
1205        let surface = io_surface();
1206        let texture = import(&feature, surface);
1207
1208        let semaphore = feature
1209            .import_semaphore(Rc::new(PlatformHandle::new(
1210                event as isize,
1211                Some(KnownPlatformGraphicsExternalSemaphoreHandleTypes::METAL_SHARED_EVENT),
1212            )))
1213            .expect("the event is imported");
1214        semaphore.signal_timeline_semaphore(&*texture, 5);
1215        assert_eq!(5, signaled_value(event));
1216        // Signalled: the wait returns at once.
1217        semaphore.wait_timeline_semaphore(&*texture, 5);
1218
1219        assert_eq!("This is a timeline semaphore", panic_message(|| semaphore.wait_semaphore(&*texture)));
1220        assert_eq!("This is a timeline semaphore", panic_message(|| semaphore.signal_semaphore(&*texture)));
1221
1222        semaphore.dispose();
1223        assert_eq!(
1224            "Cannot access a disposed object: MtlEventSemaphore",
1225            panic_message(|| semaphore.signal_timeline_semaphore(&*texture, 6))
1226        );
1227        texture.dispose();
1228        // SAFETY: both were created above and are released once.
1229        unsafe {
1230            CFRelease(surface);
1231            objc_release(event);
1232        }
1233    }
1234}