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        gpu_resource_cache_limit: usize,
54    ) -> (Self, Window) {
55        let transparent = window_attributes.transparent;
56        let window = event_loop
57            .create_window(window_attributes)
58            .expect("Could not create window with Metal context");
59
60        let device = MTLCreateSystemDefaultDevice().expect("No Metal-capable device found");
61
62        let size = window.inner_size();
63
64        let metal_layer = {
65            let layer = CAMetalLayer::new();
66            layer.setDevice(Some(&device));
67            layer.setPixelFormat(MTLPixelFormat::BGRA8Unorm);
68            layer.setPresentsWithTransaction(false);
69            // Disabling framebufferOnly allows Skia's blend modes to work correctly.
70            // See: https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly
71            layer.setFramebufferOnly(false);
72            layer.setDrawableSize(CGSize::new(size.width as f64, size.height as f64));
73
74            // Handle transparency
75            if transparent {
76                layer.setOpaque(false);
77            }
78
79            let raw_handle = window
80                .window_handle()
81                .expect("Could not get window handle")
82                .as_raw();
83
84            match raw_handle {
85                RawWindowHandle::AppKit(appkit) => {
86                    let view = unsafe { (appkit.ns_view.as_ptr() as *mut NSView).as_ref() }
87                        .expect("NSView pointer is null");
88
89                    view.setWantsLayer(true);
90                    view.setLayer(Some(&layer));
91                }
92                _ => panic!("Metal driver only supports AppKit (macOS) windows"),
93            };
94
95            layer
96        };
97
98        let command_queue = device
99            .newCommandQueue()
100            .expect("Could not create Metal command queue");
101
102        let backend = unsafe {
103            mtl::BackendContext::new(
104                Retained::as_ptr(&device) as mtl::Handle,
105                Retained::as_ptr(&command_queue) as mtl::Handle,
106            )
107        };
108
109        let mut gr_context =
110            direct_contexts::make_metal(&backend, None).expect("Could not create Metal context");
111
112        gr_context.set_resource_cache_limit(gpu_resource_cache_limit);
113
114        let driver = Self {
115            metal_layer,
116            command_queue,
117            gr_context,
118        };
119
120        (driver, window)
121    }
122
123    pub fn present(
124        &mut self,
125        _size: PhysicalSize<u32>,
126        window: &Window,
127        render: impl FnOnce(&mut SkiaSurface),
128    ) {
129        let Some(drawable) = self.metal_layer.nextDrawable() else {
130            // No drawable available, skip this frame
131            return;
132        };
133
134        let (drawable_width, drawable_height) = {
135            let size = self.metal_layer.drawableSize();
136            (size.width as i32, size.height as i32)
137        };
138
139        let texture_info =
140            unsafe { mtl::TextureInfo::new(Retained::as_ptr(&drawable.texture()) as mtl::Handle) };
141
142        let backend_render_target =
143            backend_render_targets::make_mtl((drawable_width, drawable_height), &texture_info);
144
145        let mut surface = wrap_backend_render_target(
146            &mut self.gr_context,
147            &backend_render_target,
148            SurfaceOrigin::TopLeft,
149            ColorType::BGRA8888,
150            None,
151            None,
152        )
153        .expect("Could not create Skia surface from Metal texture");
154
155        render(&mut surface);
156
157        window.pre_present_notify();
158        self.gr_context.flush_and_submit();
159        drop(surface);
160
161        let command_buffer = self
162            .command_queue
163            .commandBuffer()
164            .expect("Could not get Metal command buffer");
165
166        let mtl_drawable: Retained<ProtocolObject<dyn MTLDrawable>> = (&drawable).into();
167        command_buffer.presentDrawable(&mtl_drawable);
168        command_buffer.commit();
169    }
170
171    pub fn resize(&mut self, size: PhysicalSize<u32>) {
172        self.metal_layer
173            .setDrawableSize(CGSize::new(size.width as f64, size.height as f64));
174    }
175}