Skip to content

Commit

Permalink
Add support of metal Render backend.
Browse files Browse the repository at this point in the history
  • Loading branch information
crupest authored and Yuqian Yang committed Oct 11, 2024
1 parent 6d0aa1a commit 616bb93
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 6 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/cmd_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
208 changes: 208 additions & 0 deletions src/renderer/metal.rs
Original file line number Diff line number Diff line change
@@ -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<ProtocolObject<dyn CAMetalDrawable>>,
pub metal_drawable: Retained<ProtocolObject<dyn MTLDrawable>>,
pub surface: Surface,
}

impl MetalDrawableSurface {
fn new(
drawable: Retained<ProtocolObject<dyn CAMetalDrawable>>,
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::<ProtocolObject<dyn MTLDrawable>>(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<dyn MTLDrawable> {
&self.metal_drawable
}
}

pub struct MetalSkiaRenderer {
window: Window,
_device: Retained<ProtocolObject<dyn MTLDevice>>,
command_queue: Retained<ProtocolObject<dyn MTLCommandQueue>>,
metal_layer: Retained<CAMetalLayer>,
_backend: BackendContext,
context: DirectContext,
metal_drawable_surface: Option<MetalDrawableSurface>,
}

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<UserEvent>) -> VSync {
VSync::MacosMetal()
}
}
23 changes: 22 additions & 1 deletion src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -496,6 +499,8 @@ pub enum WindowConfigType {
OpenGL(glutin::config::Config),
#[cfg(target_os = "windows")]
Direct3D,
#[cfg(target_os = "macos")]
Metal,
}

pub struct WindowConfig {
Expand All @@ -519,7 +524,19 @@ pub fn build_window_config(
}
}

#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
{
let cmd_line_settings = SETTINGS.get::<CmdLineSettings>();
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)
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/vsync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub enum VSync {
WindowsSwapChain(VSyncWinSwapChain),
#[cfg(target_os = "macos")]
MacosDisplayLink(VSyncMacosDisplayLink),
#[cfg(target_os = "macos")]
MacosMetal(),
}

impl VSync {
Expand Down

0 comments on commit 616bb93

Please sign in to comment.