diff --git a/crates/anyrender_skia/Cargo.toml b/crates/anyrender_skia/Cargo.toml index 5f496df..1dcb7e0 100644 --- a/crates/anyrender_skia/Cargo.toml +++ b/crates/anyrender_skia/Cargo.toml @@ -12,7 +12,13 @@ edition.workspace = true pixels_window_renderer = ["dep:pixels_window_renderer"] softbuffer_window_renderer = ["dep:softbuffer_window_renderer"] log_frame_times = ["debug_timer/enable", "pixels_window_renderer?/log_frame_times", "softbuffer_window_renderer?/log_frame_times"] -vulkan = ["skia-safe/vulkan"] +vulkan = ["ganesh", "skia-safe?/vulkan"] +# GPU engines used by the window renderer on macOS/iOS (Linux/Windows always use Ganesh via OpenGL). +# rust-skia only publishes prebuilt binaries with one of these enabled: use +# `default-features = false, features = ["graphite"]` to use Graphite without building Skia from source. +ganesh = ["skia-safe?/ganesh"] +graphite = ["dep:skia-safe"] +default = ["ganesh"] [dependencies] anyrender = { workspace = true } @@ -22,11 +28,12 @@ peniko = { workspace = true } raw-window-handle = { workspace = true } softbuffer_window_renderer = { workspace = true, optional = true } pixels_window_renderer = { workspace = true, optional = true } -skia-safe = { version = "0.153.3", features = ["gl", "pdf"]} +skia-safe = { version = "0.153.3", features = ["pdf"] } oaty = "0.2" hashbrown = "0.17.0" [target.'cfg(not(any(target_os = "macos", target_os = "ios")))'.dependencies] +skia-safe = { version = "0.153.3", features = ["gl"] } glutin = { version = "0.32.3", features = ["default", "wgl", "egl"] } gl = "0.14.0" ash = "^0.38.0" @@ -37,9 +44,13 @@ ash-window = "0.13.0" skia-safe = { version = "0.153.3", features = ["textlayout"] } [target.'cfg(target_os = "macos")'.dependencies] +# Optional so that the `graphite` feature only enables Skia's Graphite engine on Apple targets. +skia-safe = { version = "0.153.3", features = ["graphite"], optional = true } objc2-app-kit = { version = "0.3.2", default-features = false, features = ["objc2-quartz-core", "NSView", "NSResponder"] } [target.'cfg(target_os = "ios")'.dependencies] +# Optional so that the `graphite` feature only enables Skia's Graphite engine on Apple targets. +skia-safe = { version = "0.153.3", features = ["graphite"], optional = true } objc2-ui-kit = { version = "0.3.2", default-features = false, features = ["objc2-quartz-core", "UIView", "UIResponder"] } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] diff --git a/crates/anyrender_skia/src/image_renderer.rs b/crates/anyrender_skia/src/image_renderer.rs index ef06a64..d380762 100644 --- a/crates/anyrender_skia/src/image_renderer.rs +++ b/crates/anyrender_skia/src/image_renderer.rs @@ -65,10 +65,10 @@ impl ImageRenderer for SkiaImageRenderer { // Clear surface with transparent background to allow transparency in rendered images surface.canvas().clear(Color::TRANSPARENT); - draw_fn(&mut SkiaScenePainter { - inner: surface.canvas(), - cache: &mut self.scene_cache, - }); + draw_fn(&mut SkiaScenePainter::new( + surface.canvas(), + &mut self.scene_cache, + )); timer.record_time("render"); self.scene_cache.next_gen(); @@ -91,10 +91,10 @@ impl ImageRenderer for SkiaImageRenderer { // Clear surface with transparent background to allow transparency in rendered images surface.canvas().clear(Color::TRANSPARENT); - draw_fn(&mut SkiaScenePainter { - inner: surface.canvas(), - cache: &mut self.scene_cache, - }); + draw_fn(&mut SkiaScenePainter::new( + surface.canvas(), + &mut self.scene_cache, + )); timer.record_time("render"); self.scene_cache.next_gen(); diff --git a/crates/anyrender_skia/src/lib.rs b/crates/anyrender_skia/src/lib.rs index a52bd7e..0943e1c 100644 --- a/crates/anyrender_skia/src/lib.rs +++ b/crates/anyrender_skia/src/lib.rs @@ -6,6 +6,11 @@ mod window_renderer; mod cache; #[cfg(any(target_os = "macos", target_os = "ios"))] mod metal; +#[cfg(all( + any(target_os = "macos", target_os = "ios"), + not(any(feature = "ganesh", feature = "graphite")) +))] +compile_error!("anyrender_skia requires the `ganesh` or `graphite` feature on macOS/iOS"); #[cfg(not(any(target_os = "macos", target_os = "ios")))] mod opengl; #[cfg(feature = "vulkan")] diff --git a/crates/anyrender_skia/src/metal.rs b/crates/anyrender_skia/src/metal.rs index 3dac4cf..d3184f1 100644 --- a/crates/anyrender_skia/src/metal.rs +++ b/crates/anyrender_skia/src/metal.rs @@ -4,151 +4,66 @@ use objc2::{rc::Retained, runtime::ProtocolObject}; #[cfg(target_os = "macos")] use objc2_app_kit::NSView; use objc2_core_foundation::CGSize; -use objc2_metal::{MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice}; -use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer}; +use objc2_metal::MTLDevice; +use objc2_quartz_core::CAMetalLayer; #[cfg(target_os = "ios")] use objc2_ui_kit::UIView; -use skia_safe::{ - ColorType, Surface, - gpu::{self, DirectContext, SurfaceOrigin, backend_render_targets, mtl}, - scalar, -}; -use crate::window_renderer::SkiaBackend; - -pub struct MetalBackend { - pub metal_layer: Retained, - pub command_queue: Retained>, - pub skia: DirectContext, - prepared_drawable: Option>>, -} - -impl MetalBackend { - pub fn new( - window: Arc, - width: u32, - height: u32, - composite_alpha_mode: anyrender::CompositeAlphaMode, - ) -> Self { - let device = MTLCreateSystemDefaultDevice().expect("no device found"); - - let metal_layer = { - let layer = CAMetalLayer::new(); - layer.setDevice(Some(&device)); - layer.setPixelFormat(objc2_metal::MTLPixelFormat::BGRA8Unorm); - layer.setOpaque(matches!( - composite_alpha_mode, - anyrender::CompositeAlphaMode::Opaque | anyrender::CompositeAlphaMode::Auto - )); - layer.setPresentsWithTransaction(false); - // Disabling this option allows Skia's Blend Mode to work. - // More about: https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly - layer.setFramebufferOnly(false); - layer.setDrawableSize(CGSize::new(width as f64, height as f64)); - - let view_ptr = match window.window_handle().unwrap().as_raw() { - #[cfg(target_os = "macos")] - raw_window_handle::RawWindowHandle::AppKit(appkit) => { - appkit.ns_view.as_ptr() as *mut NSView - } - #[cfg(target_os = "ios")] - raw_window_handle::RawWindowHandle::UiKit(uikit) => { - uikit.ui_view.as_ptr() as *mut UIView - } - _ => panic!("Wrong window handle type"), - }; - let view = unsafe { view_ptr.as_ref().unwrap() }; - - #[cfg(target_os = "macos")] - { - view.setWantsLayer(true); - view.setLayer(Some(&layer.clone().into_super())); - } - - #[cfg(target_os = "ios")] - { - // TODO: consider using raw-window-metal crate. It synchronises some properties - // from the parent UIView layer to the child metal layer when they change - layer.setFrame(view.layer().frame()); - view.layer().addSublayer(&layer) - } - - layer - }; - - let command_queue = device - .newCommandQueue() - .expect("unable to get command queue"); - - let backend = unsafe { - mtl::BackendContext::new( - Retained::as_ptr(&device) as mtl::Handle, - Retained::as_ptr(&command_queue) as mtl::Handle, - ) - }; - - let skia_context = gpu::direct_contexts::make_metal(&backend, None).unwrap(); - - Self { - metal_layer, - command_queue, - skia: skia_context, - prepared_drawable: None, +#[cfg(feature = "ganesh")] +mod ganesh; +#[cfg(feature = "graphite")] +mod graphite; + +#[cfg(feature = "ganesh")] +pub(crate) use ganesh::MetalBackend; +#[cfg(feature = "graphite")] +pub(crate) use graphite::MetalGraphiteBackend; + +/// Creates a `CAMetalLayer` for `device` and attaches it to the window's view. +fn create_metal_layer( + device: &ProtocolObject, + window: Arc, + width: u32, + height: u32, + composite_alpha_mode: anyrender::CompositeAlphaMode, +) -> Retained { + let layer = CAMetalLayer::new(); + layer.setDevice(Some(device)); + layer.setPixelFormat(objc2_metal::MTLPixelFormat::BGRA8Unorm); + layer.setOpaque(matches!( + composite_alpha_mode, + anyrender::CompositeAlphaMode::Opaque | anyrender::CompositeAlphaMode::Auto + )); + layer.setPresentsWithTransaction(false); + // Disabling this option allows Skia's Blend Mode to work. + // More about: https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly + layer.setFramebufferOnly(false); + layer.setDrawableSize(CGSize::new(width as f64, height as f64)); + + let view_ptr = match window.window_handle().unwrap().as_raw() { + #[cfg(target_os = "macos")] + raw_window_handle::RawWindowHandle::AppKit(appkit) => { + appkit.ns_view.as_ptr() as *mut NSView } + #[cfg(target_os = "ios")] + raw_window_handle::RawWindowHandle::UiKit(uikit) => uikit.ui_view.as_ptr() as *mut UIView, + _ => panic!("Wrong window handle type"), + }; + let view = unsafe { view_ptr.as_ref().unwrap() }; + + #[cfg(target_os = "macos")] + { + view.setWantsLayer(true); + view.setLayer(Some(&layer.clone().into_super())); } -} -impl SkiaBackend for MetalBackend { - fn set_size(&mut self, width: u32, height: u32) { - self.metal_layer - .setDrawableSize(CGSize::new(width as f64, height as f64)); + #[cfg(target_os = "ios")] + { + // TODO: consider using raw-window-metal crate. It synchronises some properties + // from the parent UIView layer to the child metal layer when they change + layer.setFrame(view.layer().frame()); + view.layer().addSublayer(&layer) } - fn prepare(&mut self) -> Option { - let drawable = self.metal_layer.nextDrawable()?; - - let (drawable_width, drawable_height) = { - let size = self.metal_layer.drawableSize(); - (size.width as scalar, size.height as scalar) - }; - - let surface = { - let texture_info = unsafe { - mtl::TextureInfo::new(Retained::as_ptr(&drawable.texture()) as mtl::Handle) - }; - - let backend_render_target = backend_render_targets::make_mtl( - (drawable_width as i32, drawable_height as i32), - &texture_info, - ); - - gpu::surfaces::wrap_backend_render_target( - &mut self.skia, - &backend_render_target, - SurfaceOrigin::TopLeft, - ColorType::BGRA8888, - None, - None, - ) - .unwrap() - }; - - self.prepared_drawable = Some((&drawable).into()); - - Some(surface) - } - - fn flush(&mut self, surface: Surface) { - self.skia.flush_and_submit(); - drop(surface); - let command_buffer = self - .command_queue - .commandBuffer() - .expect("unable to get command buffer"); - - // TODO: save drawable - let drawable = self.prepared_drawable.take().unwrap(); - command_buffer.presentDrawable(&drawable); - command_buffer.commit(); - } + layer } diff --git a/crates/anyrender_skia/src/metal/ganesh.rs b/crates/anyrender_skia/src/metal/ganesh.rs new file mode 100644 index 0000000..07e92bb --- /dev/null +++ b/crates/anyrender_skia/src/metal/ganesh.rs @@ -0,0 +1,109 @@ +use std::sync::Arc; + +use objc2::{rc::Retained, runtime::ProtocolObject}; +use objc2_core_foundation::CGSize; +use objc2_metal::{MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice}; +use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer}; +use skia_safe::{ + ColorType, Surface, + gpu::{self, DirectContext, SurfaceOrigin, backend_render_targets, mtl}, + scalar, +}; + +use crate::window_renderer::SkiaBackend; + +pub struct MetalBackend { + pub metal_layer: Retained, + pub command_queue: Retained>, + pub skia: DirectContext, + prepared_drawable: Option>>, +} + +impl MetalBackend { + pub fn new( + window: Arc, + width: u32, + height: u32, + composite_alpha_mode: anyrender::CompositeAlphaMode, + ) -> Self { + let device = MTLCreateSystemDefaultDevice().expect("no device found"); + + let metal_layer = + super::create_metal_layer(&device, window, width, height, composite_alpha_mode); + + let command_queue = device + .newCommandQueue() + .expect("unable to get command queue"); + + let backend = unsafe { + mtl::BackendContext::new( + Retained::as_ptr(&device) as mtl::Handle, + Retained::as_ptr(&command_queue) as mtl::Handle, + ) + }; + + let skia_context = gpu::direct_contexts::make_metal(&backend, None).unwrap(); + + Self { + metal_layer, + command_queue, + skia: skia_context, + prepared_drawable: None, + } + } +} + +impl SkiaBackend for MetalBackend { + fn set_size(&mut self, width: u32, height: u32) { + self.metal_layer + .setDrawableSize(CGSize::new(width as f64, height as f64)); + } + + fn prepare(&mut self) -> Option { + let drawable = self.metal_layer.nextDrawable()?; + + let (drawable_width, drawable_height) = { + let size = self.metal_layer.drawableSize(); + (size.width as scalar, size.height as scalar) + }; + + let surface = { + let texture_info = unsafe { + mtl::TextureInfo::new(Retained::as_ptr(&drawable.texture()) as mtl::Handle) + }; + + let backend_render_target = backend_render_targets::make_mtl( + (drawable_width as i32, drawable_height as i32), + &texture_info, + ); + + gpu::surfaces::wrap_backend_render_target( + &mut self.skia, + &backend_render_target, + SurfaceOrigin::TopLeft, + ColorType::BGRA8888, + None, + None, + ) + .unwrap() + }; + + self.prepared_drawable = Some((&drawable).into()); + + Some(surface) + } + + fn flush(&mut self, surface: Surface) { + self.skia.flush_and_submit(); + drop(surface); + let command_buffer = self + .command_queue + .commandBuffer() + .expect("unable to get command buffer"); + + // TODO: save drawable + let drawable = self.prepared_drawable.take().unwrap(); + command_buffer.presentDrawable(&drawable); + command_buffer.commit(); + } +} diff --git a/crates/anyrender_skia/src/metal/graphite.rs b/crates/anyrender_skia/src/metal/graphite.rs new file mode 100644 index 0000000..63c7d57 --- /dev/null +++ b/crates/anyrender_skia/src/metal/graphite.rs @@ -0,0 +1,122 @@ +use std::ffi::c_void; +use std::sync::Arc; + +use objc2::{rc::Retained, runtime::ProtocolObject}; +use objc2_core_foundation::CGSize; +use objc2_metal::{ + MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable, +}; +use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer}; +use skia_safe::{ + ColorType, Surface, + gpu::graphite::{self, InsertRecordingInfo, mtl}, +}; + +use crate::window_renderer::SkiaBackend; + +pub struct MetalGraphiteBackend { + metal_layer: Retained, + command_queue: Retained>, + // Field order matters: the recorder must be dropped before the context. + recorder: graphite::Recorder, + context: graphite::Context, + _backend_context: mtl::BackendContext, + prepared_drawable: Option>>, +} + +impl MetalGraphiteBackend { + pub fn new( + window: Arc, + width: u32, + height: u32, + composite_alpha_mode: anyrender::CompositeAlphaMode, + ) -> Self { + let device = MTLCreateSystemDefaultDevice().expect("no device found"); + + let metal_layer = + super::create_metal_layer(&device, window, width, height, composite_alpha_mode); + + let command_queue = device + .newCommandQueue() + .expect("unable to get command queue"); + + let backend_context = unsafe { + mtl::BackendContext::new( + Retained::as_ptr(&device) as mtl::Handle, + Retained::as_ptr(&command_queue) as mtl::Handle, + ) + }; + + let mut context = mtl::context_factory::make_metal(&backend_context, None) + .expect("unable to create Graphite context"); + let recorder = context + .make_recorder(None) + .expect("unable to create Graphite recorder"); + + Self { + metal_layer, + command_queue, + recorder, + context, + _backend_context: backend_context, + prepared_drawable: None, + } + } +} + +impl SkiaBackend for MetalGraphiteBackend { + fn set_size(&mut self, width: u32, height: u32) { + self.metal_layer + .setDrawableSize(CGSize::new(width as f64, height as f64)); + } + + fn prepare(&mut self) -> Option { + let drawable = self.metal_layer.nextDrawable()?; + + let size = self.metal_layer.drawableSize(); + let texture = drawable.texture(); + // SAFETY: the texture is owned by `drawable`, which is kept alive in + // `prepared_drawable` until the frame has been submitted in `flush`. + let backend_texture = unsafe { + mtl::backend_textures::make_metal( + (size.width as i32, size.height as i32), + Retained::as_ptr(&texture) as *mut c_void, + ) + }; + + let surface = graphite::surfaces::wrap_backend_texture( + &mut self.recorder, + &backend_texture, + ColorType::BGRA8888, + None, + None, + )?; + + self.prepared_drawable = Some((&drawable).into()); + + Some(surface) + } + + fn flush(&mut self, surface: Surface) { + let recording = self.recorder.snap(); + drop(surface); + + if let Some(mut recording) = recording { + self.context + .insert_recording(&InsertRecordingInfo::new(&mut recording)); + } + self.context.submit(None); + + let drawable = self.prepared_drawable.take().unwrap(); + let command_buffer = self + .command_queue + .commandBuffer() + .expect("unable to get command buffer"); + command_buffer.presentDrawable(&drawable); + command_buffer.commit(); + } + + fn recorder(&mut self) -> Option<&mut graphite::Recorder> { + Some(&mut self.recorder) + } +} diff --git a/crates/anyrender_skia/src/scene.rs b/crates/anyrender_skia/src/scene.rs index c25c7b1..0eedfc0 100644 --- a/crates/anyrender_skia/src/scene.rs +++ b/crates/anyrender_skia/src/scene.rs @@ -76,6 +76,9 @@ impl Default for SkiaSceneCache { pub struct SkiaScenePainter<'a> { pub(crate) inner: &'a Canvas, pub(crate) cache: &'a mut SkiaSceneCache, + /// Graphite can't draw raster images, so they are uploaded to textures using this recorder. + #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "graphite"))] + pub(crate) recorder: Option<&'a mut skia_safe::gpu::graphite::Recorder>, } impl SkiaScenePainter<'_> { @@ -83,6 +86,24 @@ impl SkiaScenePainter<'_> { SkiaScenePainter { inner: canvas, cache, + #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "graphite"))] + recorder: None, + } + } + + /// Create a painter for a canvas backed by a Graphite surface that records into `recorder`. + /// + /// Raster images are uploaded to textures using `recorder`, as Graphite can't draw them directly. + #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "graphite"))] + pub fn new_graphite<'a>( + canvas: &'a Canvas, + cache: &'a mut SkiaSceneCache, + recorder: &'a mut skia_safe::gpu::graphite::Recorder, + ) -> SkiaScenePainter<'a> { + SkiaScenePainter { + inner: canvas, + cache, + recorder: Some(recorder), } } @@ -150,7 +171,17 @@ impl SkiaScenePainter<'_> { return; } - let image_shader = sk_peniko::shader_from_image_brush(image_brush, brush_transform); + let image = sk_peniko::image_from_image_data(image_brush.image); + #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "graphite"))] + let image = match self.recorder.as_deref_mut() { + Some(recorder) => { + skia_safe::gpu::graphite::images::texture_from_image(recorder, &image) + .unwrap_or(image) + } + None => image, + }; + let image_shader = + sk_peniko::shader_from_image(image, &image_brush.sampler, brush_transform); if let Some(shader) = &image_shader { self.cache.image_shader.insert( @@ -921,8 +952,8 @@ fn distant_light_direction(light: &DistantLightSource) -> Point3 { mod sk_peniko { use peniko::color::{AlphaColor, ColorSpaceTag, HueDirection, Srgb}; use peniko::{ - BlendMode, Compose, Extend, Gradient, GradientKind, ImageAlphaType, ImageBrush, ImageData, - ImageFormat, Mix, + BlendMode, Compose, Extend, Gradient, GradientKind, ImageAlphaType, ImageData, ImageFormat, + ImageSampler, Mix, }; use peniko::{Fill, color::DynamicColor}; use skia_safe::AlphaType as SkAlphaType; @@ -930,6 +961,7 @@ mod sk_peniko { use skia_safe::Color4f as SkColor4f; use skia_safe::ColorType as SkColorType; use skia_safe::Data as SkData; + use skia_safe::Image as SkImage; use skia_safe::ImageInfo as SkImageInfo; use skia_safe::PathFillType as SkPathFillType; use skia_safe::SamplingOptions as SkSamplingOptions; @@ -942,12 +974,10 @@ mod sk_peniko { linear_gradient, radial_gradient, sweep_gradient, two_point_conical_gradient, }; - pub(super) fn shader_from_image_brush( - image_brush: ImageBrush<&ImageData>, - brush_transform: Option, - ) -> Option { - let image_data = image_brush.image; - + /// Creates a raster image that borrows the pixels of `image_data`. + /// + /// The caller must keep `image_data.data` alive for as long as the image is used. + pub(super) fn image_from_image_data(image_data: &ImageData) -> SkImage { let image_info = SkImageInfo::new( (image_data.width as i32, image_data.height as i32), match image_data.format { @@ -964,11 +994,16 @@ mod sk_peniko { let pixels = unsafe { SkData::new_bytes(image_data.data.data()) // We have to ensure the src image data lives long enough }; - let image = - skia_safe::images::raster_from_data(&image_info, pixels, image_info.min_row_bytes()) - .unwrap(); + skia_safe::images::raster_from_data(&image_info, pixels, image_info.min_row_bytes()) + .unwrap() + } - let sampling = match image_brush.sampler.quality { + pub(super) fn shader_from_image( + image: SkImage, + sampler: &ImageSampler, + brush_transform: Option, + ) -> Option { + let sampling = match sampler.quality { peniko::ImageQuality::Low => { SkSamplingOptions::new(skia_safe::FilterMode::Nearest, skia_safe::MipmapMode::None) } @@ -984,8 +1019,8 @@ mod sk_peniko { skia_safe::shaders::image( image, ( - tile_mode_from_extend(image_brush.sampler.x_extend), - tile_mode_from_extend(image_brush.sampler.y_extend), + tile_mode_from_extend(sampler.x_extend), + tile_mode_from_extend(sampler.y_extend), ), &sampling, &brush_transform.map(super::sk_kurbo::matrix_from_affine), diff --git a/crates/anyrender_skia/src/window_renderer.rs b/crates/anyrender_skia/src/window_renderer.rs index 82431d7..1ae617c 100644 --- a/crates/anyrender_skia/src/window_renderer.rs +++ b/crates/anyrender_skia/src/window_renderer.rs @@ -12,6 +12,12 @@ pub(crate) trait SkiaBackend: Any { fn prepare(&mut self) -> Option; fn flush(&mut self, surface: Surface); + + /// The Graphite recorder that the surface returned by `prepare` records into. + #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "graphite"))] + fn recorder(&mut self) -> Option<&mut skia_safe::gpu::graphite::Recorder> { + None + } } enum RenderState { @@ -123,7 +129,18 @@ impl WindowRenderer for SkiaWindowRenderer { graphics::set_typeface_cache_count_limit(100); graphics::set_resource_cache_total_bytes_limit(10485760); - #[cfg(any(target_os = "macos", target_os = "ios"))] + #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "graphite"))] + let backend = crate::metal::MetalGraphiteBackend::new( + window, + width, + height, + self.options.composite_alpha_mode, + ); + #[cfg(all( + any(target_os = "macos", target_os = "ios"), + feature = "ganesh", + not(feature = "graphite") + ))] let backend = crate::metal::MetalBackend::new( window, width, @@ -181,6 +198,8 @@ impl WindowRenderer for SkiaWindowRenderer { draw_fn(&mut SkiaScenePainter { inner: surface.canvas(), cache: &mut state.scene_cache, + #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "graphite"))] + recorder: state.backend.recorder(), }); timer.record_time("cmd");