Skip to main content

freya_winit/drivers/
metal.rs

1use freya_engine::prelude::{
2    ColorType,
3    DirectContext,
4    Surface as SkiaSurface,
5    SurfaceOrigin,
6    backend_render_targets,
7    direct_contexts,
8    mtl,
9    wrap_backend_render_target,
10};
11use objc2::{
12    rc::Retained,
13    runtime::ProtocolObject,
14};
15use objc2_app_kit::NSView;
16use objc2_core_foundation::CGSize;
17use objc2_metal::{
18    MTLCommandBuffer,
19    MTLCommandQueue,
20    MTLCreateSystemDefaultDevice,
21    MTLDevice,
22    MTLDrawable,
23    MTLPixelFormat,
24};
25use objc2_quartz_core::{
26    CAMetalDrawable,
27    CAMetalLayer,
28};
29use raw_window_handle::{
30    HasWindowHandle,
31    RawWindowHandle,
32};
33use winit::{
34    dpi::PhysicalSize,
35    event_loop::ActiveEventLoop,
36    window::{
37        Window,
38        WindowAttributes,
39    },
40};
41
42/// Graphics driver using Metal (macOS native).
43pub struct MetalDriver {
44    metal_layer: Retained<CAMetalLayer>,
45    command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
46    gr_context: DirectContext,
47}
48
49impl MetalDriver {
50    pub fn new(
51        event_loop: &ActiveEventLoop,
52        window_attributes: WindowAttributes,
53    ) -> (Self, Window) {
54        let transparent = window_attributes.transparent;
55        let window = event_loop
56            .create_window(window_attributes)
57            .expect("Could not create window with Metal context");
58
59        let device = MTLCreateSystemDefaultDevice().expect("No Metal-capable device found");
60
61        let size = window.inner_size();
62
63        let metal_layer = {
64            let layer = CAMetalLayer::new();
65            layer.setDevice(Some(&device));
66            layer.setPixelFormat(MTLPixelFormat::BGRA8Unorm);
67            layer.setPresentsWithTransaction(false);
68            // Disabling framebufferOnly allows Skia's blend modes to work correctly.
69            // See: https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly
70            layer.setFramebufferOnly(false);
71            layer.setDrawableSize(CGSize::new(size.width as f64, size.height as f64));
72
73            // Handle transparency
74            if transparent {
75                layer.setOpaque(false);
76            }
77
78            let raw_handle = window
79                .window_handle()
80                .expect("Could not get window handle")
81                .as_raw();
82
83            match raw_handle {
84                RawWindowHandle::AppKit(appkit) => {
85                    let view = unsafe { (appkit.ns_view.as_ptr() as *mut NSView).as_ref() }
86                        .expect("NSView pointer is null");
87
88                    view.setWantsLayer(true);
89                    view.setLayer(Some(&layer));
90                }
91                _ => panic!("Metal driver only supports AppKit (macOS) windows"),
92            };
93
94            layer
95        };
96
97        let command_queue = device
98            .newCommandQueue()
99            .expect("Could not create Metal command queue");
100
101        let backend = unsafe {
102            mtl::BackendContext::new(
103                Retained::as_ptr(&device) as mtl::Handle,
104                Retained::as_ptr(&command_queue) as mtl::Handle,
105            )
106        };
107
108        let gr_context =
109            direct_contexts::make_metal(&backend, None).expect("Could not create Metal context");
110
111        let driver = Self {
112            metal_layer,
113            command_queue,
114            gr_context,
115        };
116
117        (driver, window)
118    }
119
120    pub fn present(
121        &mut self,
122        _size: PhysicalSize<u32>,
123        window: &Window,
124        render: impl FnOnce(&mut SkiaSurface),
125    ) {
126        let Some(drawable) = self.metal_layer.nextDrawable() else {
127            // No drawable available, skip this frame
128            return;
129        };
130
131        let (drawable_width, drawable_height) = {
132            let size = self.metal_layer.drawableSize();
133            (size.width as i32, size.height as i32)
134        };
135
136        let texture_info =
137            unsafe { mtl::TextureInfo::new(Retained::as_ptr(&drawable.texture()) as mtl::Handle) };
138
139        let backend_render_target =
140            backend_render_targets::make_mtl((drawable_width, drawable_height), &texture_info);
141
142        let mut surface = wrap_backend_render_target(
143            &mut self.gr_context,
144            &backend_render_target,
145            SurfaceOrigin::TopLeft,
146            ColorType::BGRA8888,
147            None,
148            None,
149        )
150        .expect("Could not create Skia surface from Metal texture");
151
152        render(&mut surface);
153
154        window.pre_present_notify();
155        self.gr_context.flush_and_submit();
156        drop(surface);
157
158        let command_buffer = self
159            .command_queue
160            .commandBuffer()
161            .expect("Could not get Metal command buffer");
162
163        let mtl_drawable: Retained<ProtocolObject<dyn MTLDrawable>> = (&drawable).into();
164        command_buffer.presentDrawable(&mtl_drawable);
165        command_buffer.commit();
166    }
167
168    pub fn resize(&mut self, size: PhysicalSize<u32>) {
169        self.metal_layer
170            .setDrawableSize(CGSize::new(size.width as f64, size.height as f64));
171    }
172}