// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use core_graphics_types::geometry::CGSize; use foreign_types::{ForeignType, ForeignTypeRef}; use i_slint_core::api::PhysicalSize as PhysicalWindowSize; use metal::MTLPixelFormat; use objc::{msg_send, sel, sel_impl}; use objc::{ rc::autoreleasepool, runtime::{Object, BOOL, NO}, }; use skia_safe::gpu::mtl; use std::cell::RefCell; use std::rc::Rc; #[link(name = "QuartzCore", kind = "framework")] extern "C" { #[allow(non_upper_case_globals)] static kCAGravityTopLeft: *mut Object; #[allow(non_upper_case_globals)] static kCAGravityBottomLeft: *mut Object; } /// This surface renders into the given window using Metal. The provided display argument /// is ignored, as it has no meaning on macOS. pub struct MetalSurface { command_queue: metal::CommandQueue, layer: metal::MetalLayer, gr_context: RefCell, } impl super::Surface for MetalSurface { fn new( window_handle: Rc, _display_handle: Rc, size: PhysicalWindowSize, ) -> Result { let layer = match window_handle .window_handle() .map_err(|e| format!("Error obtaining window handle for skia metal renderer: {e}"))? .as_raw() { raw_window_handle::RawWindowHandle::AppKit(handle) => unsafe { raw_window_metal::Layer::from_ns_view(handle.ns_view) }, raw_window_handle::RawWindowHandle::UiKit(handle) => unsafe { raw_window_metal::Layer::from_ui_view(handle.ui_view) }, _ => return Err("Skia Renderer: Metal surface is only supported with AppKit".into()), }; // SAFETY: The layer is an initialized instance of `CAMetalLayer`, and // we transfer the retain count to `MetalLayer` using `into_raw`. let layer = unsafe { metal::MetalLayer::from_ptr(layer.into_raw().cast().as_ptr()) }; let device = metal::Device::system_default() .ok_or_else(|| format!("Skia Renderer: No metal device found"))?; layer.set_device(&device); layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); layer.set_opaque(false); layer.set_presents_with_transaction(false); layer.set_drawable_size(CGSize::new(size.width as f64, size.height as f64)); let flipped: BOOL = unsafe { msg_send![layer.as_ptr(), contentsAreFlipped] }; let gravity = if flipped == NO { unsafe { kCAGravityTopLeft } } else { unsafe { kCAGravityBottomLeft } }; let _: () = unsafe { msg_send![layer.as_ptr(), setContentsGravity: gravity] }; let command_queue = device.new_command_queue(); let backend = unsafe { mtl::BackendContext::new( device.as_ptr() as mtl::Handle, command_queue.as_ptr() as mtl::Handle, ) }; let gr_context = skia_safe::gpu::direct_contexts::make_metal(&backend, None).unwrap().into(); Ok(Self { command_queue, layer, gr_context }) } fn name(&self) -> &'static str { "metal" } fn resize_event( &self, size: PhysicalWindowSize, ) -> Result<(), i_slint_core::platform::PlatformError> { self.layer.set_drawable_size(CGSize::new(size.width as f64, size.height as f64)); Ok(()) } fn render( &self, _size: PhysicalWindowSize, callback: &dyn Fn(&skia_safe::Canvas, Option<&mut skia_safe::gpu::DirectContext>), pre_present_callback: &RefCell>>, ) -> Result<(), i_slint_core::platform::PlatformError> { autoreleasepool(|| { let drawable = match self.layer.next_drawable() { Some(drawable) => drawable, None => { return Err(format!( "Skia Metal Renderer: Failed to retrieve next drawable for rendering" ) .into()) } }; let gr_context = &mut self.gr_context.borrow_mut(); let size = self.layer.drawable_size(); let mut surface = unsafe { let texture_info = mtl::TextureInfo::new(drawable.texture().as_ptr() as mtl::Handle); let backend_render_target = skia_safe::gpu::backend_render_targets::make_mtl( (size.width as i32, size.height as i32), &texture_info, ); skia_safe::gpu::surfaces::wrap_backend_render_target( gr_context, &backend_render_target, skia_safe::gpu::SurfaceOrigin::TopLeft, skia_safe::ColorType::BGRA8888, None, None, ) .unwrap() }; callback(surface.canvas(), Some(gr_context)); drop(surface); gr_context.submit(None); if let Some(pre_present_callback) = pre_present_callback.borrow_mut().as_mut() { pre_present_callback(); } let command_buffer = self.command_queue.new_command_buffer(); command_buffer.present_drawable(drawable); command_buffer.commit(); Ok(()) }) } fn bits_per_pixel(&self) -> Result { // From https://developer.apple.com/documentation/metal/mtlpixelformat: // The storage size of each pixel format is determined by the sum of its components. // For example, the storage size of BGRA8Unorm is 32 bits (four 8-bit components) and // the storage size of BGR5A1Unorm is 16 bits (three 5-bit components and one 1-bit component). Ok(match self.layer.pixel_format() { MTLPixelFormat::B5G6R5Unorm | MTLPixelFormat::A1BGR5Unorm | MTLPixelFormat::ABGR4Unorm | MTLPixelFormat::BGR5A1Unorm => 16, MTLPixelFormat::RGBA8Unorm | MTLPixelFormat::RGBA8Unorm_sRGB | MTLPixelFormat::RGBA8Snorm | MTLPixelFormat::RGBA8Uint | MTLPixelFormat::RGBA8Sint | MTLPixelFormat::BGRA8Unorm | MTLPixelFormat::BGRA8Unorm_sRGB => 32, MTLPixelFormat::RGB10A2Unorm | MTLPixelFormat::RGB10A2Uint | MTLPixelFormat::BGR10A2Unorm => 32, MTLPixelFormat::RGBA16Unorm | MTLPixelFormat::RGBA16Snorm | MTLPixelFormat::RGBA16Uint | MTLPixelFormat::RGBA16Sint => 64, MTLPixelFormat::RGBA32Uint | MTLPixelFormat::RGBA32Sint => 128, fmt @ _ => { return Err(format!( "Skia Metal Renderer: Unsupported layer pixel format found {fmt:?}" ) .into()) } }) } }