diff --git a/Cargo.lock b/Cargo.lock index c3fec5c45b..fdb6129662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,6 +1478,8 @@ dependencies = [ "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", "parking_lot", "rand 0.8.5", "raw-window-handle", diff --git a/Cargo.toml b/Cargo.toml index cc2c2b10e5..10a43192a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,13 +101,16 @@ windows = { version = "0.56.0", features = [ ] } windows-registry = "0.2.0" -[target.'cfg(not(target_os = "windows"))'.dependencies] -skia-safe = { version = "0.75.0", features = ["gl", "textlayout"] } - [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.5.2" objc2-foundation = { version = "0.2.2", features = [ "NSUserDefaults" ] } -objc2-app-kit = { version = "0.2.2", features = [ "NSLayoutConstraint" ] } +objc2-app-kit = { version = "0.2.2", features = [ "NSLayoutConstraint", "objc2-quartz-core" ] } +objc2-quartz-core = { version = "0.2.2", features = [ "objc2-metal", "CALayer", "CAMetalLayer" ] } +objc2-metal = { version = "0.2.2", features = [ "MTLCommandQueue", "MTLCommandBuffer" ] } +skia-safe = { version = "0.75.0", features = ["metal", "gl", "textlayout"] } + +[target.'cfg(not(any(target_os = "windows", target_os = "macos")))'.dependencies] +skia-safe = { version = "0.75.0", features = ["gl", "textlayout"] } [target.'cfg(target_os = "windows")'.build-dependencies] winres = "0.1.12" diff --git a/src/cmd_line.rs b/src/cmd_line.rs index f5544779cd..c099f8bde7 100644 --- a/src/cmd_line.rs +++ b/src/cmd_line.rs @@ -138,7 +138,7 @@ pub struct CmdLineSettings { pub geometry: GeometryArgs, /// Force opengl on Windows - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] #[arg(long = "opengl", env = "NEOVIDE_OPENGL", action = ArgAction::SetTrue, value_parser = FalseyValueParser::new())] pub opengl: bool, } diff --git a/src/renderer/metal.rs b/src/renderer/metal.rs new file mode 100644 index 0000000000..4216f98964 --- /dev/null +++ b/src/renderer/metal.rs @@ -0,0 +1,208 @@ +use objc2::{rc::Retained, runtime::ProtocolObject}; +use objc2_foundation::{CGFloat, CGSize}; +use objc2_metal::{ + MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable, + MTLPixelFormat, MTLTexture, +}; +use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer}; +use skia_safe::{ + gpu::{ + self, + mtl::{BackendContext, TextureInfo}, + surfaces::wrap_backend_render_target, + DirectContext, SurfaceOrigin, + }, + Canvas, ColorSpace, ColorType, Surface, +}; +use winit::{event_loop::EventLoopProxy, window::Window}; + +use crate::{ + profiling::tracy_gpu_zone, + renderer::{SkiaRenderer, VSync}, + window::{macos::get_ns_window, UserEvent}, +}; + +struct MetalDrawableSurface { + pub _drawable: Retained>, + pub metal_drawable: Retained>, + pub surface: Surface, +} + +impl MetalDrawableSurface { + fn new( + drawable: Retained>, + context: &mut DirectContext, + ) -> MetalDrawableSurface { + tracy_gpu_zone!("MetalDrawableSurface.new"); + + let texture = unsafe { drawable.texture() }; + let texture_info = unsafe { TextureInfo::new(Retained::as_ptr(&texture).cast()) }; + let backend_render_target = gpu::backend_render_targets::make_mtl( + (texture.width() as i32, texture.height() as i32), + &texture_info, + ); + + let metal_drawable = + unsafe { Retained::cast::>(drawable.clone()) }; + + let surface = wrap_backend_render_target( + context, + &backend_render_target, + SurfaceOrigin::TopLeft, + ColorType::BGRA8888, + ColorSpace::new_srgb(), + None, + ) + .expect("Failed to create skia surface with metal drawable."); + + MetalDrawableSurface { + _drawable: drawable, + metal_drawable, + surface, + } + } + + fn metal_drawable(&self) -> &ProtocolObject { + &self.metal_drawable + } +} + +pub struct MetalSkiaRenderer { + window: Window, + _device: Retained>, + command_queue: Retained>, + metal_layer: Retained, + _backend: BackendContext, + context: DirectContext, + metal_drawable_surface: Option, +} + +impl MetalSkiaRenderer { + pub fn new(window: Window, srgb: bool, vsync: bool) -> Self { + log::info!("Initialize MetalSkiaRenderer..."); + + let draw_size = window.inner_size(); + let ns_window = get_ns_window(&window); + + let device = unsafe { + Retained::retain(MTLCreateSystemDefaultDevice()) + .expect("Failed to create Metal system default device.") + }; + let metal_layer = unsafe { + let metal_layer = CAMetalLayer::new(); + metal_layer.setDevice(Some(&device)); + metal_layer.setPixelFormat(if srgb { + MTLPixelFormat::BGRA8Unorm_sRGB + } else { + MTLPixelFormat::BGRA8Unorm + }); + metal_layer.setPresentsWithTransaction(false); + metal_layer.setFramebufferOnly(false); + metal_layer.setDisplaySyncEnabled(vsync); + metal_layer.setOpaque(false); + + let ns_view = ns_window.contentView().unwrap(); + ns_view.setWantsLayer(true); + ns_view.setLayer(Some(&metal_layer)); + + metal_layer + .setDrawableSize(CGSize::new(draw_size.width as f64, draw_size.height as f64)); + metal_layer + }; + + let command_queue = device + .newCommandQueue() + .expect("Failed to create command queue."); + + let backend = unsafe { + BackendContext::new( + Retained::as_ptr(&device).cast(), + Retained::as_ptr(&command_queue).cast(), + ) + }; + + let context = gpu::direct_contexts::make_metal(&backend, None).unwrap(); + + MetalSkiaRenderer { + window, + _device: device, + metal_layer, + command_queue, + _backend: backend, + context, + metal_drawable_surface: None, + } + } + + fn move_to_next_frame(&mut self) { + tracy_gpu_zone!("move_to_next_frame"); + + let drawable = unsafe { + self.metal_layer + .nextDrawable() + .expect("Failed to get next drawable of metal layer.") + }; + + self.metal_drawable_surface = Some(MetalDrawableSurface::new(drawable, &mut self.context)); + } +} + +impl SkiaRenderer for MetalSkiaRenderer { + fn window(&self) -> &Window { + &self.window + } + + fn flush(&mut self) { + tracy_gpu_zone!("flush"); + + self.context.flush_and_submit(); + } + + fn swap_buffers(&mut self) { + tracy_gpu_zone!("swap buffers"); + + let command_buffer = self + .command_queue + .commandBuffer() + .expect("Failed to create command buffer."); + command_buffer.presentDrawable( + self.metal_drawable_surface + .as_mut() + .expect("No drawable surface now.") + .metal_drawable(), + ); + command_buffer.commit(); + + self.metal_drawable_surface = None; + } + + fn canvas(&mut self) -> &Canvas { + tracy_gpu_zone!("canvas"); + + self.move_to_next_frame(); + + self.metal_drawable_surface + .as_mut() + .expect("Not metal drawable surface now.") + .surface + .canvas() + } + + fn resize(&mut self) { + tracy_gpu_zone!("resize"); + + let window_size = self.window.inner_size(); + unsafe { + self.metal_layer.setDrawableSize(CGSize::new( + window_size.width as CGFloat, + window_size.height as CGFloat, + )); + } + + self.window.request_redraw(); + } + + fn create_vsync(&self, _proxy: EventLoopProxy) -> VSync { + VSync::MacosMetal() + } +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 75874445b2..ee66cd903d 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -11,6 +11,9 @@ mod vsync; #[cfg(target_os = "windows")] pub mod d3d; +#[cfg(target_os = "macos")] +mod metal; + use std::{ cmp::Ordering, collections::{hash_map::Entry, HashMap}, @@ -496,6 +499,8 @@ pub enum WindowConfigType { OpenGL(glutin::config::Config), #[cfg(target_os = "windows")] Direct3D, + #[cfg(target_os = "macos")] + Metal, } pub struct WindowConfig { @@ -519,7 +524,19 @@ pub fn build_window_config( } } - #[cfg(not(target_os = "windows"))] + #[cfg(target_os = "macos")] + { + let cmd_line_settings = SETTINGS.get::(); + if cmd_line_settings.opengl { + opengl::build_window(window_attributes, event_loop) + } else { + let window = event_loop.create_window(window_attributes).unwrap(); + let config = WindowConfigType::Metal; + WindowConfig { window, config } + } + } + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] { opengl::build_window(window_attributes, event_loop) } @@ -547,6 +564,10 @@ pub fn create_skia_renderer( } #[cfg(target_os = "windows")] WindowConfigType::Direct3D => Box::new(d3d::D3DSkiaRenderer::new(window.window)), + #[cfg(target_os = "macos")] + WindowConfigType::Metal => { + Box::new(metal::MetalSkiaRenderer::new(window.window, srgb, vsync)) + } }; tracy_create_gpu_context("main_render_context", renderer.as_ref()); renderer diff --git a/src/renderer/vsync/mod.rs b/src/renderer/vsync/mod.rs index 8dde7620c3..8c0d59f634 100644 --- a/src/renderer/vsync/mod.rs +++ b/src/renderer/vsync/mod.rs @@ -34,6 +34,8 @@ pub enum VSync { WindowsSwapChain(VSyncWinSwapChain), #[cfg(target_os = "macos")] MacosDisplayLink(VSyncMacosDisplayLink), + #[cfg(target_os = "macos")] + MacosMetal(), } impl VSync {