// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 #[cfg(feature = "unstable-wgpu-29")] use i_slint_core::api::GraphicsAPI; use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::RequestedGraphicsAPI; use i_slint_core::partial_renderer::DirtyRegion; use i_slint_core::platform::PlatformError; use i_slint_core::renderer::DrawOutcome; use std::cell::{Cell, RefCell}; use std::sync::Arc; use wgpu_29 as wgpu; use crate::SkiaSharedContext; #[cfg(target_family = "windows")] mod dx12; #[cfg(target_vendor = "apple")] mod metal; #[cfg(all(target_family = "unix", not(target_vendor = "apple")))] mod vulkan; /// Skia rendering surface backed by WGPU. Supports both on-screen rendering (with a /// window surface) and offscreen rendering into caller-provided textures. pub struct WGPUSurface { pub(crate) gr_context: RefCell, instance: wgpu::Instance, device: wgpu::Device, queue: wgpu::Queue, surface_config: RefCell>, surface: Option>, textures_to_transition_for_sampling: RefCell>, pub(crate) backend: Backend, // DarkRoom patch: pre-rotation. The adapter is kept to ask the surface // for its `currentTransform`; `window_size` is the size the window // reported, which differs from the swapchain's when `rotation` is 90° or // 270°; `rotation` is what the current swapchain was promised. adapter: Option, window_size: Cell, rotation: Cell, } /// DarkRoom patch: the right-angle transforms a swapchain can be pre-rotated /// by, clockwise, as the presentation engine will apply them. #[derive(Clone, Copy, PartialEq, Eq, Debug)] enum Rotation { None, Cw90, Cw180, Cw270, } impl Rotation { fn swaps_axes(self) -> bool { matches!(self, Rotation::Cw90 | Rotation::Cw270) } /// Map window coordinates onto a buffer sized for this rotation. /// /// The window is `w` × `h`. A clockwise quarter turn sends its top-left /// corner to the buffer's top-right, so a point (x, y) lands at /// (h − y, x): Skia's `rotate(90)` gives (−y, x), and translating by the /// buffer width `h` first brings that back on screen. The other two follow /// the same way. fn apply(self, canvas: &skia_safe::Canvas, w: f32, h: f32) { match self { Rotation::None => {} Rotation::Cw90 => { canvas.translate((h, 0.)); canvas.rotate(90., None); } Rotation::Cw180 => { canvas.translate((w, h)); canvas.rotate(180., None); } Rotation::Cw270 => { canvas.translate((0., w)); canvas.rotate(270., None); } } } } impl WGPUSurface { pub fn new_with_surface( surface_target: impl Into, size: PhysicalWindowSize, requested_graphics_api: Option, ) -> Result { let (instance, adapter, device, queue, surface) = i_slint_core::graphics::wgpu_29::init_instance_adapter_device_queue_surface( surface_target, requested_graphics_api, wgpu::Backends::GL /* we're not mapping that to skia because we can't save/restore state */ .union(if cfg!(target_os = "windows") { wgpu::Backends::VULKAN } else { wgpu::Backends::empty() }), )?; let mut surface_config = surface.get_default_config(&adapter, size.width, size.height).unwrap(); let rotation = pre_rotate(&surface, &adapter, &mut surface_config, size); let swapchain_capabilities = surface.get_capabilities(&adapter); let swapchain_format = swapchain_capabilities .formats .iter() .find(|f| { matches!(f, wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm) }) .copied() .unwrap_or_else(|| swapchain_capabilities.formats[0]); surface_config.format = swapchain_format; surface.configure(&device, &surface_config); let backend: Backend = adapter.get_info().backend.try_into()?; let gr_context = backend.make_context(&adapter, &device, &queue); Ok(Self { gr_context: RefCell::new( gr_context.ok_or_else(|| { PlatformError::from("Failed to create Skia context from WGPU") })?, ), instance, device, queue, surface_config: Some(surface_config).into(), surface: Some(surface), textures_to_transition_for_sampling: RefCell::new(Vec::new()), backend, adapter: Some(adapter), window_size: Cell::new(size), rotation: Cell::new(rotation), }) } pub(crate) fn new_offscreen( instance: wgpu::Instance, device: wgpu::Device, queue: wgpu::Queue, backend: Backend, gr_context: skia_safe::gpu::DirectContext, ) -> Self { Self { gr_context: RefCell::new(gr_context), instance, device, queue, surface_config: None.into(), surface: None, textures_to_transition_for_sampling: RefCell::new(Vec::new()), backend, adapter: None, window_size: Cell::new(PhysicalWindowSize::default()), rotation: Cell::new(Rotation::None), } } /// Transitions any imported wgpu textures to sampling state and flushes /// the Skia graphics context. Must be called after rendering to ensure /// Skia's GPU work is submitted. pub(crate) fn flush_and_submit(&self, gr_context: &mut skia_safe::gpu::DirectContext) { let textures_to_transition = self.textures_to_transition_for_sampling.take(); if !textures_to_transition.is_empty() { let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Skia texture transition encoder"), }); encoder.transition_resources( std::iter::empty(), textures_to_transition.iter().map(|texture| wgpu::TextureTransition { texture, selector: None, state: wgpu::TextureUses::RESOURCE, }), ); self.queue.submit(Some(encoder.finish())); } gr_context.submit(None); } /// DarkRoom patch: configure the surface for a window of `size`, sized /// and pre-rotated for the transform the display has right now. fn configure_for( &self, surface_config: &mut wgpu::SurfaceConfiguration, size: PhysicalWindowSize, ) { let (Some(surface), Some(adapter)) = (&self.surface, &self.adapter) else { return }; let rotation = pre_rotate(surface, adapter, surface_config, size); surface.configure(&self.device, surface_config); self.window_size.set(size); self.rotation.set(rotation); } } /// DarkRoom patch: size `config` for a window of `size` under the display's /// current transform, and tell wgpu-hal to promise that transform to the /// next swapchain. Returns the rotation every frame must then be drawn with. /// /// Android only. Everywhere else, and whenever the transform is not a plain /// rotation (a mirrored one, or a query that failed), this leaves the /// configuration at the window's size with `IDENTITY` — upstream's /// behaviour, under which the compositor rotates the buffer itself. fn pre_rotate( surface: &wgpu::Surface<'static>, adapter: &wgpu::Adapter, config: &mut wgpu::SurfaceConfiguration, size: PhysicalWindowSize, ) -> Rotation { let rotation = current_rotation(surface, adapter); #[cfg(target_os = "android")] unsafe { use ash::vk::SurfaceTransformFlagsKHR as T; if let Some(hal) = surface.as_hal::() { hal.set_pre_transform(match rotation { Rotation::None => T::IDENTITY, Rotation::Cw90 => T::ROTATE_90, Rotation::Cw180 => T::ROTATE_180, Rotation::Cw270 => T::ROTATE_270, }); } } (config.width, config.height) = if rotation.swaps_axes() { (size.height, size.width) } else { (size.width, size.height) }; rotation } #[cfg(target_os = "android")] fn current_rotation(surface: &wgpu::Surface<'static>, adapter: &wgpu::Adapter) -> Rotation { use ash::vk::SurfaceTransformFlagsKHR as T; let transform = unsafe { let (Some(surface), Some(adapter)) = ( surface.as_hal::(), adapter.as_hal::(), ) else { return Rotation::None; }; surface.current_transform(&adapter) }; match transform { Some(T::ROTATE_90) => Rotation::Cw90, Some(T::ROTATE_180) => Rotation::Cw180, Some(T::ROTATE_270) => Rotation::Cw270, _ => Rotation::None, } } #[cfg(not(target_os = "android"))] fn current_rotation(_surface: &wgpu::Surface<'static>, _adapter: &wgpu::Adapter) -> Rotation { Rotation::None } impl crate::Surface for WGPUSurface { fn new( _shared_context: &SkiaSharedContext, window_handle: Arc, display_handle: Arc, size: PhysicalWindowSize, requested_graphics_api: Option, ) -> Result { Self::new_with_surface( Box::new(WindowAndDisplayHandle(window_handle, display_handle)) as Box, size, requested_graphics_api, ) } fn name(&self) -> &'static str { if self.surface.is_some() { "wgpu" } else { "wgpu-texture" } } fn resize_event(&self, size: PhysicalWindowSize) -> Result<(), PlatformError> { let mut surface_config_opt = self.surface_config.borrow_mut(); let (Some(surface_config), Some(_)) = (surface_config_opt.as_mut(), &self.surface) else { return Ok(()); }; // Skip reconfigure if size hasn't changed — DRM/KMS surfaces don't // support being reconfigured. // // DarkRoom patch: compared against the window's size rather than the // swapchain's, which is transposed while pre-rotated a quarter turn. if self.window_size.get() == size { return Ok(()); } { let gr_context = &mut self.gr_context.borrow_mut(); // This is brute force, but for the lack of access to the fences this seems to work: Avoid any pending work so that // IDXGISwapChain::ResizeBuffers doesn't complain that the surface is still in use. gr_context.flush_submit_and_sync_cpu(); } // Prefer FIFO modes over possible Mailbox setting for frame pacing and better energy efficiency. surface_config.present_mode = wgpu::PresentMode::AutoVsync; self.configure_for(surface_config, size); Ok(()) } fn render( &self, _window: &Window, _size: PhysicalWindowSize, callback: &dyn Fn( &skia_safe::Canvas, Option<&mut skia_safe::gpu::DirectContext>, u8, ) -> Option, pre_present_callback: &RefCell>>, ) -> Result { // DarkRoom patch: a turn from landscape to reverse landscape changes // the display's transform without changing the window's size, so no // resize arrives to say so — and wgpu-hal hides the // `VK_SUBOPTIMAL_KHR` that would, on Android. Check before every // frame and reconfigure when it moved; the query is one call into // the native window. if let (Some(surface), Some(adapter)) = (&self.surface, &self.adapter) && current_rotation(surface, adapter) != self.rotation.get() && let Some(surface_config) = self.surface_config.borrow_mut().as_mut() { self.gr_context.borrow_mut().flush_submit_and_sync_cpu(); self.configure_for(surface_config, self.window_size.get()); } let (Some(surface), Some(surface_config)) = (&self.surface, &*self.surface_config.borrow()) else { return Err("WGPUSurface::render() called on offscreen surface".into()); }; let gr_context = &mut self.gr_context.borrow_mut(); let frame = match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(t) => t, wgpu::CurrentSurfaceTexture::Occluded => return Ok(DrawOutcome::Occluded), wgpu::CurrentSurfaceTexture::Timeout => return Ok(DrawOutcome::Timeout), wgpu::CurrentSurfaceTexture::Validation => { return Err("WGPU surface validation error in get_current_texture".into()); } stale @ (wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Suboptimal(_) | wgpu::CurrentSurfaceTexture::Lost) => { // `Suboptimal` carries a live `SurfaceTexture`; matched with `_` it is not bound, // so the value returned by `get_current_texture()` keeps it alive across the // `surface.configure()` below — which wgpu forbids ("`SurfaceOutput` must be // dropped before a new `Surface` is made"), panicking on the first frame on // Wayland. Drop it first. (`Outdated`/`Lost` carry nothing → no-op.) drop(stale); surface.configure(&self.device, surface_config); match surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(t) => t, _ => return Ok(DrawOutcome::Occluded), } } }; let skia_surface = self.backend.make_surface(gr_context, &frame.texture); let mut skia_surface = skia_surface .ok_or_else(|| PlatformError::from("Failed to create Skia surface from WGPU"))?; // DarkRoom patch: everything Slint draws, in window coordinates, goes // through this one matrix into the pre-rotated buffer. let window_size = self.window_size.get(); self.rotation.get().apply( skia_surface.canvas(), window_size.width as f32, window_size.height as f32, ); callback(skia_surface.canvas(), Some(gr_context), 0); self.flush_and_submit(gr_context); if let Some(pre_present_callback) = pre_present_callback.borrow_mut().as_mut() { pre_present_callback(); } frame.present(); Ok(DrawOutcome::Success) } fn bits_per_pixel(&self) -> Result { if let Some(surface_config) = &*self.surface_config.borrow() { Ok(match surface_config.format { wgpu_29::TextureFormat::Rgba8Unorm | wgpu_29::TextureFormat::Rgba8UnormSrgb | wgpu_29::TextureFormat::Bgra8Unorm | wgpu_29::TextureFormat::Bgra8UnormSrgb => 32, fmt => return Err(format!("Unsupported surface format {:#?}", fmt).into()), }) } else { // All supported render-target formats (Rgba8Unorm, Bgra8Unorm, and sRGB variants) are 32bpp. Ok(32) } } #[cfg(feature = "unstable-wgpu-29")] fn with_graphics_api(&self, callback: &mut dyn FnMut(GraphicsAPI<'_>)) { let api = i_slint_core::graphics::create_graphics_api_wgpu_29( self.instance.clone(), self.device.clone(), self.queue.clone(), ); callback(api) } #[cfg(any(feature = "unstable-wgpu-28", feature = "unstable-wgpu-29"))] fn import_wgpu_texture( &self, canvas: &skia_safe::Canvas, any_wgpu_texture: &i_slint_core::graphics::WGPUTexture, ) -> Option { let texture = match any_wgpu_texture { #[cfg(feature = "unstable-wgpu-28")] i_slint_core::graphics::WGPUTexture::WGPU28Texture(..) => return None, #[cfg(feature = "unstable-wgpu-29")] i_slint_core::graphics::WGPUTexture::WGPU29Texture(texture) => texture.clone(), }; // Skia won't submit commands right away, so remember the texture and transition before // submitting. self.textures_to_transition_for_sampling.borrow_mut().push(texture.clone()); self.backend.import_texture(canvas, texture) } } struct WindowAndDisplayHandle( Arc, Arc, ); impl raw_window_handle::HasWindowHandle for WindowAndDisplayHandle { fn window_handle( &self, ) -> Result, raw_window_handle::HandleError> { self.0.window_handle() } } impl raw_window_handle::HasDisplayHandle for WindowAndDisplayHandle { fn display_handle( &self, ) -> Result, raw_window_handle::HandleError> { self.1.display_handle() } } pub(crate) enum Backend { #[cfg(target_vendor = "apple")] Metal, #[cfg(target_family = "windows")] Dx12, #[cfg(all(target_family = "unix", not(target_vendor = "apple")))] Vulkan, } impl TryFrom for Backend { type Error = PlatformError; fn try_from(wgpu_backend: wgpu::Backend) -> Result { match wgpu_backend { wgpu_29::Backend::Noop => { Err(PlatformError::from("Cannot use WGPU Noop backend with Skia")) } #[cfg(all(target_family = "unix", not(target_vendor = "apple")))] wgpu_29::Backend::Vulkan => Ok(Self::Vulkan), #[cfg(target_vendor = "apple")] wgpu_29::Backend::Metal => Ok(Self::Metal), #[cfg(target_family = "windows")] wgpu_29::Backend::Dx12 => Ok(Self::Dx12), other => Err(PlatformError::from(format!( "Unsupported WGPU backend for use with Skia: {}", other ))), } } } impl Backend { pub(crate) fn make_context( &self, _adapter: &wgpu::Adapter, device: &wgpu::Device, queue: &wgpu::Queue, ) -> Option { match self { #[cfg(target_vendor = "apple")] Self::Metal => metal::make_metal_context(device, queue), #[cfg(target_family = "windows")] Self::Dx12 => unsafe { dx12::make_dx12_context(&_adapter, &device, &queue) }, #[cfg(all(target_family = "unix", not(target_vendor = "apple")))] Self::Vulkan => unsafe { vulkan::make_vulkan_context(device, queue) }, } } pub(crate) fn make_surface( &self, gr_context: &mut skia_safe::gpu::DirectContext, texture: &wgpu::Texture, ) -> Option { match self { #[cfg(target_vendor = "apple")] Self::Metal => unsafe { metal::make_metal_surface(gr_context, texture) }, #[cfg(target_family = "windows")] Self::Dx12 => unsafe { dx12::make_dx12_surface(gr_context, texture) }, #[cfg(all(target_family = "unix", not(target_vendor = "apple")))] Self::Vulkan => unsafe { vulkan::make_vulkan_surface(gr_context, texture) }, } } pub(crate) fn import_texture( &self, canvas: &skia_safe::Canvas, texture: wgpu::Texture, ) -> Option { match self { #[cfg(target_vendor = "apple")] Self::Metal => unsafe { metal::import_metal_texture(canvas, texture) }, #[cfg(target_family = "windows")] Self::Dx12 => unsafe { dx12::import_dx12_texture(canvas, texture) }, #[cfg(all(target_family = "unix", not(target_vendor = "apple")))] Self::Vulkan => unsafe { vulkan::import_vulkan_texture(canvas, texture) }, } } }