From fd8f5de528b948ba958b7957f2a78744dc676894 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sat, 30 May 2026 22:01:37 +0700 Subject: [PATCH] Support imaging_skia GPU with wgpu 29 Metal wgpu 29 removed the API that let imaging_skia get the raw Metal command queue from a wgpu::Queue. Skia's Ganesh Metal backend still needs an MTLCommandQueue when creating its DirectContext, so create and retain a Skia-owned Metal command queue from the shared Metal device instead of trying to borrow wgpu's queue. This keeps the GPU backend working on wgpu 29 without changing the public imaging_skia API. wgpu is expected to regain the relevant raw queue access in wgpu 30, so this can be revisited when that API is available again. Because Skia now submits work through a separate Metal queue, synchronize the shared texture path more conservatively: flush Metal surfaces with SyncCpu::Yes and wait for the wgpu texture initialization pass before handing textures to Skia. --- Cargo.lock | 5 +++-- imaging_skia/Cargo.toml | 14 ++++++++++---- imaging_skia/src/ganesh.rs | 16 ++++++++++++++-- imaging_skia/src/lib.rs | 15 +++++++++------ imaging_skia/src/metal.rs | 33 +++++++++++++++++++++------------ 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb4f6fc..542d07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1145,14 +1145,15 @@ name = "imaging_skia" version = "0.0.1" dependencies = [ "ash", - "foreign-types-shared", "imaging", "imaging_wgpu", "kurbo", "oaty", + "objc2", + "objc2-metal", "peniko", "skia-safe", - "wgpu 28.0.0", + "wgpu 29.0.3", "windows 0.62.2", ] diff --git a/imaging_skia/Cargo.toml b/imaging_skia/Cargo.toml index 8faad66..fe88963 100644 --- a/imaging_skia/Cargo.toml +++ b/imaging_skia/Cargo.toml @@ -14,10 +14,11 @@ categories = ["graphics", "rendering"] default = [] gpu = [ "dep:imaging_wgpu", - "imaging_wgpu/wgpu-28", - "dep:foreign-types-shared", + "imaging_wgpu/wgpu-29", "dep:wgpu", "dep:ash", + "dep:objc2", + "dep:objc2-metal", "dep:windows", ] @@ -27,10 +28,15 @@ imaging_wgpu = { default-features = false, optional = true, workspace = true } kurbo = { default-features = true, workspace = true } peniko = { default-features = true, workspace = true } skia-safe = { version = "0.97.0" } -wgpu = { optional = true, version = "28.0.0" } +wgpu = { optional = true, version = "29.0.3" } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -foreign-types-shared = { version = "0.3", optional = true } +objc2 = { version = "0.6", optional = true } +objc2-metal = { version = "0.3", default-features = false, features = [ + "MTLCommandQueue", + "MTLDevice", + "MTLTexture", +], optional = true } oaty = "0.1" skia-safe = { version = "0.97.0", features = ["metal"] } diff --git a/imaging_skia/src/ganesh.rs b/imaging_skia/src/ganesh.rs index 30888f9..43fd932 100644 --- a/imaging_skia/src/ganesh.rs +++ b/imaging_skia/src/ganesh.rs @@ -138,8 +138,20 @@ impl GaneshBackend { } pub(crate) fn flush_surface(&mut self, surface: &mut sk::Surface) { - self.direct_context() - .flush_and_submit_surface(surface, sk::gpu::SyncCpu::No); + match self { + #[cfg(any(target_os = "macos", target_os = "ios"))] + Self::Metal(backend) => backend + .direct_context() + .flush_and_submit_surface(surface, sk::gpu::SyncCpu::Yes), + #[cfg(windows)] + Self::Dx12(backend) => backend + .direct_context() + .flush_and_submit_surface(surface, sk::gpu::SyncCpu::No), + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + Self::Vulkan(backend) => backend + .direct_context() + .flush_and_submit_surface(surface, sk::gpu::SyncCpu::No), + } } pub(crate) fn flush_surface_for_readback(&mut self, surface: &mut sk::Surface) { diff --git a/imaging_skia/src/lib.rs b/imaging_skia/src/lib.rs index 1c2f2a4..f118909 100644 --- a/imaging_skia/src/lib.rs +++ b/imaging_skia/src/lib.rs @@ -165,8 +165,6 @@ mod sinks; #[cfg(all(feature = "gpu", not(any(target_os = "macos", target_os = "ios"))))] mod vulkan; -#[cfg(all(feature = "gpu", any(target_os = "macos", target_os = "ios")))] -use foreign_types_shared as _; use imaging::{ Filter, GeometryRef, GlyphRunRef, RgbaImage, record::{Scene, ValidateError, replay}, @@ -194,7 +192,7 @@ use crate::gpu_readback::{ ReadbackError, ScratchTexture, read_texture_into, read_texture_into_target, }; #[cfg(feature = "gpu")] -use imaging_wgpu::v28::TextureRenderer; +use imaging_wgpu::v29::TextureRenderer; #[cfg(feature = "gpu")] use imaging_wgpu::{TextureRendererError, TextureTargetError}; use sinks::MaskCache; @@ -851,7 +849,7 @@ impl SkiaGpuRendererState { "Skia GPU renderer requires RENDER_ATTACHMENT texture usage", )); } - initialize_texture_for_wgpu(&self.device, &self.queue, texture); + initialize_texture_for_wgpu(&self.device, &self.queue, texture)?; Ok(()) } @@ -1162,7 +1160,8 @@ impl ImageRenderer for SkiaRenderer { ) .map_err(map_image_renderer_error)?; let texture = self.scratch_texture_for_format(target.width, target.height, texture_format); - initialize_texture_for_wgpu(&self.state.device, &self.state.queue, &texture); + initialize_texture_for_wgpu(&self.state.device, &self.state.queue, &texture) + .map_err(map_image_renderer_error)?; let mut surface = self .state .backend @@ -1281,7 +1280,7 @@ fn initialize_texture_for_wgpu( device: &wgpu::Device, queue: &wgpu::Queue, texture: &wgpu::Texture, -) { +) -> Result<(), Error> { let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("imaging_skia texture init"), @@ -1304,6 +1303,10 @@ fn initialize_texture_for_wgpu( }); drop(_pass); queue.submit([encoder.finish()]); + device + .poll(wgpu::PollType::wait_indefinitely()) + .map_err(|_| Error::Internal("wgpu texture initialization poll failed"))?; + Ok(()) } #[allow( diff --git a/imaging_skia/src/metal.rs b/imaging_skia/src/metal.rs index 9104c70..f52a9e7 100644 --- a/imaging_skia/src/metal.rs +++ b/imaging_skia/src/metal.rs @@ -3,7 +3,10 @@ #![allow(unsafe_code, reason = "Metal interop requires raw handle bridging")] -use foreign_types_shared::ForeignType; +use core::ptr::NonNull; +use objc2::rc::Retained; +use objc2::runtime::ProtocolObject; +use objc2_metal::{MTLCommandQueue, MTLDevice as _}; use skia_safe as sk; use crate::{Error, color_space_for_wgpu_texture_format, color_type_for_wgpu_texture_format}; @@ -11,31 +14,37 @@ use crate::{Error, color_space_for_wgpu_texture_format, color_type_for_wgpu_text #[derive(Debug)] pub(crate) struct MetalBackend { context: sk::gpu::DirectContext, + _command_queue: Retained>, } impl MetalBackend { - pub(crate) fn from_wgpu(device: &wgpu::Device, queue: &wgpu::Queue) -> Result { + pub(crate) fn from_wgpu(device: &wgpu::Device, _queue: &wgpu::Queue) -> Result { let device = unsafe { device .as_hal::() .ok_or(Error::CreateGpuContext("missing Metal device"))? }; - let queue = unsafe { - queue - .as_hal::() - .ok_or(Error::CreateGpuContext("missing Metal queue"))? - }; + let command_queue = + device + .raw_device() + .newCommandQueue() + .ok_or(Error::CreateGpuContext( + "unable to create Metal command queue", + ))?; let backend = unsafe { sk::gpu::mtl::BackendContext::new( - device.raw_device().as_ptr() as sk::gpu::mtl::Handle, - queue.as_raw().lock().as_ptr() as sk::gpu::mtl::Handle, + Retained::as_ptr(device.raw_device()) as sk::gpu::mtl::Handle, + Retained::as_ptr(&command_queue) as sk::gpu::mtl::Handle, ) }; let context = sk::gpu::direct_contexts::make_metal(&backend, None).ok_or( Error::CreateGpuContext("unable to create Skia Metal context"), )?; - Ok(Self { context }) + Ok(Self { + context, + _command_queue: command_queue, + }) } pub(crate) fn direct_context(&mut self) -> &mut sk::gpu::DirectContext { @@ -53,8 +62,8 @@ impl MetalBackend { .as_hal::() .ok_or(Error::CreateGpuSurface)? }; - let texture_info = - unsafe { sk::gpu::mtl::TextureInfo::new(hal_texture.raw_handle().as_ptr() as _) }; + let texture_handle = NonNull::from(hal_texture.raw_handle()).as_ptr(); + let texture_info = unsafe { sk::gpu::mtl::TextureInfo::new(texture_handle as _) }; let backend_texture = unsafe { sk::gpu::backend_textures::make_mtl( (width, height),