gallery/vendor/i-slint-renderer-skia/lib.rs
Greg Pomerantz 93b27d724b Root-cause and fix the Mali scroll jank; strip jank-hunt instrumentation
The 19-26 ms mid-scroll stalls (Mali-only, fixed content offsets) were
the Skia renderer's final synchronous GrDirectContext::flush() blocking
the UI thread on Mali driver back-pressure — proven by per-stage
timestamps in the vendored renderer (stall frames: flush 19-24 ms,
walk 1-3 ms, 1 binding re-eval, image imports 0.1 ms each).

- vendor/i-slint-renderer-skia (pinned 1.18.1, [patch.crates-io]):
  remove that one flush. Redundant on the wgpu backend: the surface
  already calls flush_and_submit before queue.present, same Vulkan
  queue, in-order. Phone: 0 stalls across 4 runs (max 3.6-7.4 ms vs
  the 8.3 ms budget), display verified clean.
- docs/BUGS/slint-skia-final-flush-jank.md: full evidence, prior art
  (slint-ui/slint#12030 closest), open question for a future
  upstream filing (not filed yet).
- Profiling also retired the O(n)-walk theory: Slint 1.18 prunes the
  item walk to ~133-152 items; the windowed Flickable built on that
  theory was already reverted (ListView is smooth with the fix).
- Strip all jank-hunt instrumentation: FR/SCR/TICK/APLYD/STG logging,
  stg markers, .gallery_* file knobs, jsamp thread sampler,
  DbgScroll programmatic scroll, nblog perf-file mirroring.
  Also fixes the 'launches into unstoppable fast scroll' bug: a
  leftover .gallery_cmd was re-read on every launch (file never
  deleted) and overrode user swipes — the hook is gone.
- Keep: nblog (logd backpressure is real), zero-dimension decode
  guards, keep-alive (default-off).
- HANDOFF: replace stale windowed-Flickable section with the actual
  root cause/fix; 15 unit tests pass.

Verified on phone (aarch64, 8580 photos) and emulator (x86_64).
2026-10-28 18:23:50 -04:00

1150 lines
43 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
// cSpell: ignore upcasting
#![doc = include_str!("README.md")]
#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
#![cfg_attr(slint_nightly_test, feature(non_exhaustive_omitted_patterns_lint))]
#![cfg_attr(slint_nightly_test, warn(non_exhaustive_omitted_patterns))]
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};
use std::sync::Arc;
use i_slint_core::Brush;
use i_slint_core::api::{
GraphicsAPI, PhysicalSize as PhysicalWindowSize, RenderingNotifier, RenderingState,
SetRenderingNotifierError, Window,
};
use i_slint_core::graphics::RequestedGraphicsAPI;
use i_slint_core::graphics::euclid::{self, Vector2D};
use i_slint_core::graphics::rendering_metrics_collector::RenderingMetricsCollector;
use i_slint_core::graphics::{BorderRadius, SharedPixelBuffer};
use i_slint_core::item_rendering::{ItemCache, ItemRenderer};
use i_slint_core::item_tree::ItemTreeWeak;
use i_slint_core::lengths::{LogicalPoint, LogicalRect, PhysicalPx, ScaleFactor};
use i_slint_core::partial_renderer::{DirtyRegion, PartialRenderingState};
use i_slint_core::platform::PlatformError;
use i_slint_core::renderer::DrawOutcome;
use i_slint_core::textlayout::sharedparley;
use i_slint_core::window::{WindowAdapter, WindowInner};
type PhysicalLength = euclid::Length<f32, PhysicalPx>;
type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
type PhysicalSize = euclid::Size2D<f32, PhysicalPx>;
type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>;
type PhysicalBorderRadius = BorderRadius<f32, PhysicalPx>;
mod cached_image;
mod font_cache;
mod itemrenderer;
#[cfg(skia_backend_software)]
pub mod software_surface;
#[cfg(any(not(target_vendor = "apple"), target_os = "macos"))]
pub mod opengl_surface;
#[cfg(skia_wgpu_vulkan)]
mod vulkan_handle;
#[cfg(feature = "wgpu-29")]
pub mod wgpu_29_surface;
#[cfg(feature = "wgpu-30")]
pub mod wgpu_30_surface;
#[cfg(any(feature = "wgpu-29", feature = "wgpu-30"))]
mod wgpu_renderer;
#[cfg(feature = "wgpu-29")]
pub use wgpu_renderer::SkiaWGPU29Renderer;
#[cfg(feature = "wgpu-30")]
pub use wgpu_renderer::SkiaWGPU30Renderer;
#[cfg(any(feature = "wgpu-29", feature = "wgpu-30"))]
#[allow(deprecated)]
pub use wgpu_renderer::SkiaWGPURenderer;
#[cfg(any(feature = "wgpu-29", feature = "wgpu-30"))]
pub use wgpu_renderer::SkiaWGPURendererGeneric;
use itemrenderer::to_skia_rect;
pub use skia_safe;
/// Returns the color space for Slint colors, decoded image pixels and raster buffers.
///
/// The components of a `slint::Color` are gamma encoded sRGB, not linear.
/// The same holds for the pixels of decoded images and for the 8-bit buffers Skia renders into,
/// which have no hardware transfer function of their own.
fn srgb_color_space() -> skia_safe::ColorSpace {
skia_safe::ColorSpace::new_srgb()
}
fn linear_srgb_color_space() -> skia_safe::ColorSpace {
skia_safe::ColorSpace::new_srgb_linear()
}
/// How a texture's format encodes the values it stores.
// Only WGPU hands us sRGB formats, so `Srgb` is unreachable in a build without it.
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub(crate) enum TextureEncoding {
/// A UNORM format: the bytes are gamma encoded sRGB and the GPU converts nothing.
Unorm,
/// An sRGB format: the GPU decodes when sampling and encodes when storing.
Srgb,
}
impl TextureEncoding {
#[cfg(any(feature = "wgpu-29", feature = "wgpu-30"))]
pub(crate) fn from_format_is_srgb(is_srgb: bool) -> Self {
if is_srgb { Self::Srgb } else { Self::Unorm }
}
}
/// Returns the color space to give Skia for a render target.
///
/// An sRGB attachment applies the transfer function when storing, so Skia has to hand it linear
/// values. A UNORM attachment stores what it is given, so Skia keeps working in gamma encoded
/// sRGB and no conversion happens at all.
pub(crate) fn attachment_color_space(encoding: TextureEncoding) -> skia_safe::ColorSpace {
match encoding {
TextureEncoding::Srgb => linear_srgb_color_space(),
TextureEncoding::Unorm => srgb_color_space(),
}
}
/// Returns the color space to give Skia for a texture it samples from.
///
/// Sampling an sRGB texture decodes the transfer function in hardware, so the values that reach
/// Skia are linear. A UNORM texture is handed over untouched and its bytes are gamma encoded
/// sRGB by convention.
// Outside of WGPU the only caller is the OpenGL texture import, which iOS doesn't compile.
#[allow(dead_code)]
pub(crate) fn sampled_texture_color_space(encoding: TextureEncoding) -> skia_safe::ColorSpace {
match encoding {
TextureEncoding::Srgb => linear_srgb_color_space(),
TextureEncoding::Unorm => srgb_color_space(),
}
}
/// Describes a raster buffer to Skia, tagged as sRGB.
pub(crate) fn image_info(
size: impl Into<skia_safe::ISize>,
color_type: skia_safe::ColorType,
alpha_type: skia_safe::AlphaType,
) -> skia_safe::ImageInfo {
skia_safe::ImageInfo::new(size, color_type, alpha_type, srgb_color_space())
}
/// Builds a paint filled with a Slint color.
///
/// A paint holds its color as sRGB, per the `setColor` docs in Skia's `SkPaint.h`, which is the
/// space a `slint::Color` is already in. Skia converts from there to the target when drawing.
pub(crate) fn solid_paint(color: &i_slint_core::Color) -> skia_safe::Paint {
let mut paint = skia_safe::Paint::default();
paint.set_color(itemrenderer::to_skia_color(color));
paint
}
/// Builds a shader of a single Slint color, see [`solid_paint`] for the color space.
pub(crate) fn color_shader(color: &i_slint_core::Color) -> skia_safe::Shader {
skia_safe::shaders::color(itemrenderer::to_skia_color(color))
}
/// Builds the stops of a gradient, tagged as sRGB.
pub(crate) fn gradient_colors<'a>(
colors: &'a [skia_safe::Color4f],
pos: &'a [f32],
) -> skia_safe::gradient::Colors<'a> {
skia_safe::gradient::Colors::new(
colors,
Some(pos),
skia_safe::TileMode::Clamp,
srgb_color_space(),
)
}
#[cfg(skia_backend_wgpu)]
fn default_wgpu_surface_factory(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Sync + Send>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Sync + Send>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError> {
cfg_if::cfg_if! {
if #[cfg(feature = "wgpu-30")] {
surface_factory::<wgpu_30_surface::WGPUSurface>(
context,
window_handle,
display_handle,
size,
requested_graphics_api,
)
} else {
surface_factory::<wgpu_29_surface::WGPUSurface>(
context,
window_handle,
display_handle,
size,
requested_graphics_api,
)
}
}
}
#[cfg(all(target_vendor = "apple", skia_backend_wgpu))]
fn metal_wgpu_surface_factory(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Sync + Send>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Sync + Send>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError> {
default_wgpu_surface_factory(
context,
window_handle,
display_handle,
size,
Some(requested_graphics_api.unwrap_or(RequestedGraphicsAPI::Metal)),
)
}
#[cfg(skia_wgpu_vulkan)]
fn vulkan_wgpu_surface_factory(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Sync + Send>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Sync + Send>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError> {
default_wgpu_surface_factory(
context,
window_handle,
display_handle,
size,
Some(requested_graphics_api.unwrap_or(RequestedGraphicsAPI::Vulkan)),
)
}
#[cfg(all(target_family = "windows", skia_backend_wgpu))]
fn direct3d_wgpu_surface_factory(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Sync + Send>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Sync + Send>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError> {
default_wgpu_surface_factory(
context,
window_handle,
display_handle,
size,
Some(requested_graphics_api.unwrap_or(RequestedGraphicsAPI::Direct3D)),
)
}
#[cfg(skia_windowed)]
fn create_default_surface(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Sync + Send>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Sync + Send>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError> {
#[cfg(feature = "unstable-wgpu-29")]
if matches!(requested_graphics_api, Some(RequestedGraphicsAPI::WGPU29(..))) {
// user explicitly asked for wgpu29 even if 30 is enabled
return surface_factory::<wgpu_29_surface::WGPUSurface>(
context,
window_handle,
display_handle,
size,
requested_graphics_api,
);
}
#[cfg(feature = "unstable-wgpu-30")]
if matches!(requested_graphics_api, Some(RequestedGraphicsAPI::WGPU30(..))) {
// this is also what would happen if we got the default wgpu surface since 30 is newest
return surface_factory::<wgpu_30_surface::WGPUSurface>(
context,
window_handle,
display_handle,
size,
requested_graphics_api,
);
}
let candidates: [(&str, SurfaceFactoryFn); _] = [
// vulkan feature enables but also prioritizes vulkan
#[cfg(all(feature = "vulkan", skia_wgpu_vulkan))]
("wgpu", vulkan_wgpu_surface_factory),
// opengl feature enables but also prioritizes opengl
#[cfg(all(feature = "opengl", any(not(target_vendor = "apple"), target_os = "macos")))]
("opengl", surface_factory::<opengl_surface::OpenGLSurface>),
#[cfg(skia_backend_wgpu)]
("wgpu", default_wgpu_surface_factory),
#[cfg(all(not(feature = "opengl"), skia_backend_opengl))]
("opengl", surface_factory::<opengl_surface::OpenGLSurface>),
#[cfg(skia_backend_softbuffer)]
("software", surface_factory::<software_surface::SoftwareSurface>),
];
let mut errors = Vec::new();
for (index, (name, factory)) in candidates.iter().enumerate() {
match factory(
context,
window_handle.clone(),
display_handle.clone(),
size,
requested_graphics_api.clone(),
) {
Ok(surface) => return Ok(surface),
Err(err) => {
i_slint_core::debug_log!(
"Failed to initialize Skia {} surface: {} .{}",
name,
err,
if index != candidates.len().saturating_sub(1) {
" Falling back to the next surface candidate"
} else {
""
}
);
errors.push(format!("{name}: {err}"));
}
}
}
Err(if errors.is_empty() {
"No Skia surface implementation was enabled at compile time".into()
} else {
format!("Could not initialize any Skia surface. Tried {}", errors.join(", ")).into()
})
}
enum DirtyRegionDebugMode {
NoDebug,
Visualize,
Log,
}
impl Default for DirtyRegionDebugMode {
fn default() -> Self {
match std::env::var("SLINT_SKIA_PARTIAL_RENDERING").as_deref() {
Ok("visualize") => DirtyRegionDebugMode::Visualize,
Ok("log") => DirtyRegionDebugMode::Log,
_ => DirtyRegionDebugMode::NoDebug,
}
}
}
fn create_partial_renderer_state(
maybe_surface: Option<&dyn Surface>,
) -> Option<PartialRenderingState> {
maybe_surface
.map_or_else(
|| std::env::var("SLINT_SKIA_PARTIAL_RENDERING").as_deref().is_ok(),
|surface| surface.use_partial_rendering(),
)
.then(PartialRenderingState::default)
}
#[derive(Default)]
struct SkiaSharedContextInner {
// Weak: the surfaces own these, see `SharedWgpuState`.
#[cfg(feature = "wgpu-29")]
wgpu_29_state: RefCell<Weak<wgpu_29_surface::SharedWgpuState>>,
#[cfg(feature = "wgpu-30")]
wgpu_30_state: RefCell<Weak<wgpu_30_surface::SharedWgpuState>>,
}
/// This data structure contains data that's intended to be shared across several instances of SkiaRenderer.
/// For example, for Vulkan rendering, this shares the Vulkan instance.
///
/// Create an instance once and pass clones of it to the difference constructor functions, to ensure most
/// efficient resource usage.
#[derive(Clone, Default)]
pub struct SkiaSharedContext(#[allow(dead_code)] Rc<SkiaSharedContextInner>);
/// Use the SkiaRenderer when implementing a custom Slint platform where you deliver events to
/// Slint and want the scene to be rendered using Skia as underlying graphics library.
pub struct SkiaRenderer {
maybe_window_adapter: RefCell<Option<Weak<dyn WindowAdapter>>>,
rendering_notifier: RefCell<Option<Box<dyn RenderingNotifier>>>,
image_cache: ItemCache<Option<skia_safe::Image>>,
layer_cache: ItemCache<Option<(PhysicalPoint, skia_safe::Image)>>,
path_cache: ItemCache<Option<(Vector2D<f32, PhysicalPx>, skia_safe::Path)>>,
box_shadow_cache: itemrenderer::SkiaBoxShadowCache,
text_layout_cache: sharedparley::TextLayoutCache,
rendering_metrics_collector: RefCell<Option<Rc<RenderingMetricsCollector>>>,
rendering_first_time: Cell<bool>,
surface: RefCell<Option<Box<dyn Surface>>>,
surface_factory: SurfaceFactoryFn,
pre_present_callback: RefCell<Option<Box<dyn FnMut()>>>,
partial_rendering_state: Option<PartialRenderingState>,
dirty_region_debug_mode: DirtyRegionDebugMode,
/// Tracking dirty regions indexed by buffer age - 1. More than 3 back buffers aren't supported, but also unlikely to happen.
dirty_region_history: RefCell<[DirtyRegion; 3]>,
shared_context: SkiaSharedContext,
}
/// Function pointer for creating the surface to render on, when needed.
type SurfaceFactoryFn = fn(
&SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Send + Sync>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError>;
/// Creates a boxed surface of the concrete type `S`, in the shape of [`SurfaceFactoryFn`].
fn surface_factory<S: Surface + 'static>(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Send + Sync>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Box<dyn Surface>, PlatformError> {
S::new(context, window_handle, display_handle, size, requested_graphics_api)
.map(|r| Box::new(r) as Box<dyn Surface>)
}
impl SkiaRenderer {
fn new_with_surface_factory(
context: &SkiaSharedContext,
surface_factory: SurfaceFactoryFn,
partial_rendering_state: Option<PartialRenderingState>,
) -> Self {
Self {
maybe_window_adapter: Default::default(),
rendering_notifier: Default::default(),
image_cache: Default::default(),
layer_cache: Default::default(),
path_cache: Default::default(),
box_shadow_cache: Default::default(),
text_layout_cache: Default::default(),
rendering_metrics_collector: Default::default(),
rendering_first_time: Default::default(),
surface: Default::default(),
surface_factory,
pre_present_callback: Default::default(),
partial_rendering_state,
dirty_region_debug_mode: Default::default(),
dirty_region_history: Default::default(),
shared_context: context.clone(),
}
}
#[cfg(skia_windowed)]
pub fn default(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
create_default_surface,
create_partial_renderer_state(None),
)
}
#[cfg(skia_backend_software)]
/// Creates a new SkiaRenderer that will always use Skia's software renderer.
pub fn default_software(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
surface_factory::<software_surface::SoftwareSurface>,
PartialRenderingState::default().into(),
)
}
#[cfg(any(not(target_vendor = "apple"), target_os = "macos"))]
/// Creates a new SkiaRenderer that will always use Skia's OpenGL renderer.
pub fn default_opengl(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
surface_factory::<opengl_surface::OpenGLSurface>,
create_partial_renderer_state(None),
)
}
#[cfg(all(target_vendor = "apple", skia_backend_wgpu))]
/// Creates a new SkiaRenderer that will always render using Metal, through WGPU.
pub fn default_metal(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
metal_wgpu_surface_factory,
create_partial_renderer_state(None),
)
}
#[cfg(skia_wgpu_vulkan)]
/// Creates a new SkiaRenderer that will always render using Vulkan, through WGPU.
pub fn default_vulkan(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
vulkan_wgpu_surface_factory,
create_partial_renderer_state(None),
)
}
#[cfg(all(target_family = "windows", skia_backend_wgpu))]
/// Creates a new SkiaRenderer that will always render using Direct3D 12, through WGPU.
pub fn default_direct3d(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
direct3d_wgpu_surface_factory,
create_partial_renderer_state(None),
)
}
#[cfg(feature = "wgpu-30")]
/// Creates a new SkiaRenderer that will always use Skia's WGPU 30.x renderer.
pub fn default_wgpu_30(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
surface_factory::<wgpu_30_surface::WGPUSurface>,
create_partial_renderer_state(None),
)
}
#[cfg(feature = "wgpu-29")]
/// Creates a new SkiaRenderer that will always use Skia's WGPU 29.x renderer.
pub fn default_wgpu_29(context: &SkiaSharedContext) -> Self {
Self::new_with_surface_factory(
context,
surface_factory::<wgpu_29_surface::WGPUSurface>,
create_partial_renderer_state(None),
)
}
/// Creates a new renderer is associated with the provided window adapter.
#[cfg(skia_windowed)]
pub fn new(
context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Send + Sync>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>,
size: PhysicalWindowSize,
) -> Result<Self, PlatformError> {
Ok(Self::new_with_surface(
context,
create_default_surface(context, window_handle, display_handle, size, None)?,
))
}
/// Creates a new renderer with the given surface trait implementation.
pub fn new_with_surface(
context: &SkiaSharedContext,
surface: Box<dyn Surface + 'static>,
) -> Self {
let renderer = Self::new_with_surface_factory(
context,
|_, _, _, _, _| {
Err("Skia renderer constructed with surface does not support dynamic surface re-creation".into())
},
create_partial_renderer_state(Some(surface.as_ref())),
);
renderer.rendering_first_time.set(true);
*renderer.surface.borrow_mut() = Some(surface);
renderer
}
/// Releases every cache that holds a GPU-backed resource.
fn clear_graphics_caches(&self) {
self.image_cache.clear_all();
self.layer_cache.clear_all();
self.path_cache.clear_all();
self.box_shadow_cache.clear();
self.text_layout_cache.clear_all();
}
/// Reset the surface to a new surface. (destroy the previously set surface if any)
pub fn set_surface(&self, surface: Box<dyn Surface + 'static>) {
self.clear_graphics_caches();
self.rendering_first_time.set(true);
*self.surface.borrow_mut() = Some(surface);
}
/// Releases the graphics caches and destroys the surface.
fn clear_surface(&self) {
self.clear_graphics_caches();
let Some(surface) = self.surface.borrow_mut().take() else {
return;
};
// If we've rendered a frame before, then we need to invoke the RenderingTearDown notifier.
if !self.rendering_first_time.get()
&& let Some(callback) = self.rendering_notifier.borrow_mut().as_mut()
{
surface
.with_active_surface(&mut || {
surface.with_graphics_api(&mut |api| {
callback.notify(RenderingState::RenderingTeardown, &api)
})
})
.ok();
}
drop(surface);
}
/// Suspends the renderer by freeing all graphics related resources as well as the underlying
/// rendering surface. Call [`Self::set_window_handle()`] to re-associate the renderer with a new
/// window surface for subsequent rendering.
pub fn suspend(&self) -> Result<(), PlatformError> {
// Destroy the old surface before allocating the new one, to work around
// the vivante drivers using zwp_linux_explicit_synchronization_v1 and
// trying to create a second synchronization object and that's not allowed.
self.clear_surface();
Ok(())
}
/// Reset the surface to the window given the window handle
pub fn set_window_handle(
&self,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Send + Sync>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
transparent: bool,
) -> Result<(), PlatformError> {
// just in case
self.suspend()?;
let surface = (self.surface_factory)(
&self.shared_context,
window_handle,
display_handle,
size,
requested_graphics_api,
)?;
surface.set_transparent(transparent)?;
self.set_surface(surface);
Ok(())
}
/// Adjusts the surface for a window that became transparent or opaque after it was created,
/// so that the scene's alpha is kept or discarded to match.
pub fn set_transparent(&self, transparent: bool) -> Result<(), PlatformError> {
match self.surface.borrow().as_ref() {
Some(surface) => surface.set_transparent(transparent),
None => Ok(()),
}
}
/// Render the scene in the previously associated window.
pub fn render(&self) -> Result<DrawOutcome, i_slint_core::platform::PlatformError> {
let window_adapter = self.window_adapter()?;
let size = window_adapter.window().size();
self.internal_render_with_post_callback(0., (0., 0.), size, None)
}
fn invoke_rendering_notifier_setup(
&self,
surface: &dyn Surface,
) -> Result<(), i_slint_core::platform::PlatformError> {
if self.rendering_first_time.take() {
*self.rendering_metrics_collector.borrow_mut() =
RenderingMetricsCollector::new(&format!(
"Skia renderer (skia backend {}; surface: {} bpp)",
surface.name(),
surface.bits_per_pixel()?
));
if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
surface.with_graphics_api(&mut |api| {
callback.notify(RenderingState::RenderingSetup, &api)
})
}
}
Ok(())
}
fn internal_render_with_post_callback(
&self,
rotation_angle_degrees: f32,
translation: (f32, f32),
surface_size: PhysicalWindowSize,
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
) -> Result<DrawOutcome, i_slint_core::platform::PlatformError> {
let surface = self.surface.borrow();
let Some(surface) = surface.as_ref() else { return Ok(DrawOutcome::Success) };
self.invoke_rendering_notifier_setup(surface.as_ref())?;
let window_adapter = self.window_adapter()?;
let window = window_adapter.window();
surface.render(
window,
surface_size,
&|skia_canvas, gr_context, back_buffer_age| {
self.render_to_canvas(
skia_canvas,
rotation_angle_degrees,
translation,
gr_context,
back_buffer_age,
Some(surface.as_ref()),
window,
post_render_cb,
)
},
&self.pre_present_callback,
)
}
fn render_to_canvas(
&self,
skia_canvas: &skia_safe::Canvas,
rotation_angle_degrees: f32,
translation: (f32, f32),
gr_context: Option<&mut skia_safe::gpu::DirectContext>,
back_buffer_age: u8,
surface: Option<&dyn Surface>,
window: &i_slint_core::api::Window,
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
) -> Option<DirtyRegion> {
skia_canvas.rotate(rotation_angle_degrees, None);
skia_canvas.translate(translation);
let window_inner = WindowInner::from_pub(window);
let dirty_region = window_inner
.draw_contents(|components, post_render| {
self.render_components_to_canvas(
skia_canvas,
gr_context,
back_buffer_age,
surface,
window,
post_render_cb,
components,
post_render,
)
})
.unwrap_or_default();
if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut()
&& let Some(surface) = surface
{
surface
.with_graphics_api(&mut |api| callback.notify(RenderingState::AfterRendering, &api))
}
dirty_region
}
fn render_components_to_canvas(
&self,
skia_canvas: &skia_safe::Canvas,
mut gr_context: Option<&mut skia_safe::gpu::DirectContext>,
back_buffer_age: u8,
surface: Option<&dyn Surface>,
window: &i_slint_core::api::Window,
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
components: &[(ItemTreeWeak, LogicalPoint)],
post_render: &dyn Fn(&mut dyn ItemRenderer),
) -> Option<DirtyRegion> {
let window_inner = WindowInner::from_pub(window);
let window_adapter = window_inner.window_adapter();
self.image_cache.clear_cache_if_scale_factor_changed(window);
self.path_cache.clear_cache_if_scale_factor_changed(window);
self.box_shadow_cache.clear_cache_if_scale_factor_changed(window);
let mut skia_item_renderer = itemrenderer::SkiaItemRenderer::new(
skia_canvas,
window,
surface,
&self.image_cache,
&self.layer_cache,
&self.path_cache,
&self.text_layout_cache,
&self.box_shadow_cache,
);
let scale_factor = ScaleFactor::new(window_inner.scale_factor());
let logical_window_size = i_slint_core::lengths::logical_size_from_api(
window.size().to_logical(window_inner.scale_factor()),
);
let mut dirty_region = None;
{
let mut item_renderer: &mut dyn ItemRenderer = &mut skia_item_renderer;
let mut partial_renderer;
let mut dirty_region_to_visualize = None;
if let Some(partial_rendering_state) = self.partial_rendering_state() {
partial_renderer =
partial_rendering_state.create_partial_renderer(skia_item_renderer);
let mut dirty_region_history = self.dirty_region_history.borrow_mut();
let buffer_dirty_region = if back_buffer_age > 0
&& back_buffer_age as usize - 1 < dirty_region_history.len()
{
// The dirty region is the union of all the previous dirty regions
Some(
dirty_region_history[0..back_buffer_age as usize - 1]
.iter()
.fold(DirtyRegion::default(), |acc, region| acc.union(region)),
)
} else {
Some(LogicalRect::from_size(logical_window_size).into())
};
let dirty_region_for_this_frame = partial_rendering_state.apply_dirty_region(
&mut partial_renderer,
components,
logical_window_size,
buffer_dirty_region,
);
if matches!(self.dirty_region_debug_mode, DirtyRegionDebugMode::Log) {
let area_to_repaint: f32 =
partial_renderer.dirty_region.iter().map(|b| b.area()).sum();
i_slint_core::debug_log!(
"repainting {:.2}%",
area_to_repaint * 100. / logical_window_size.area()
);
}
dirty_region = partial_renderer.dirty_region.clone().into();
dirty_region_history.rotate_right(1);
dirty_region_history[0] = dirty_region_for_this_frame;
let visualize =
matches!(self.dirty_region_debug_mode, DirtyRegionDebugMode::Visualize);
if !visualize && partial_renderer.dirty_region.iter().count() == 1 {
let dirty_rect = partial_renderer.dirty_region.iter().next().unwrap();
let physical_rect = (dirty_rect * scale_factor).to_rect().round_out();
skia_canvas.clip_rect(to_skia_rect(&physical_rect), None, false);
} else {
let mut clip_path_builder = skia_safe::PathBuilder::new();
for dirty_rect in partial_renderer.dirty_region.iter() {
let physical_rect = (dirty_rect * scale_factor).to_rect().round_out();
clip_path_builder.add_rect(to_skia_rect(&physical_rect), None, None);
}
let clip_path = clip_path_builder.detach();
skia_canvas.clip_path(&clip_path, None, false);
if visualize {
dirty_region_to_visualize = Some(clip_path);
}
}
item_renderer = &mut partial_renderer;
}
if let Some(window_item_rc) = window_inner.window_item_rc() {
let window_item =
window_item_rc.downcast::<i_slint_core::items::WindowItem>().unwrap();
if let Brush::SolidColor(clear_color) = window_item.as_pin_ref().background() {
skia_canvas.clear(itemrenderer::to_skia_color(&clear_color));
} else {
// Draws the window background as gradient
item_renderer.draw_rectangle(
window_item.as_pin_ref(),
&window_item_rc,
i_slint_core::lengths::logical_size_from_api(
window.size().to_logical(window_inner.scale_factor()),
),
&window_item.as_pin_ref().cached_rendering_data,
);
}
}
if let Some(callback) = self.rendering_notifier.borrow_mut().as_mut() {
// For the BeforeRendering rendering notifier callback it's important that this happens *after* clearing
// the back buffer, in order to allow the callback to provide its own rendering of the background.
// Skia's clear() will merely schedule a clear call, so flush right away to make it immediate.
if let Some(ctx) = gr_context.as_mut() {
ctx.flush(None);
}
if let Some(surface) = surface {
surface.with_graphics_api(&mut |api| {
callback.notify(RenderingState::BeforeRendering, &api)
})
}
}
for (component, origin) in components {
if let Some(component) = ItemTreeWeak::upgrade(component) {
i_slint_core::item_rendering::render_component_items(
&component,
item_renderer,
*origin,
&window_adapter,
);
}
}
post_render(item_renderer);
if let Some(path) = dirty_region_to_visualize {
let mut paint = skia_safe::Paint::new(
skia_safe::Color4f { a: 0.5, r: 1.0, g: 0., b: 0. },
None,
);
paint.set_style(skia_safe::PaintStyle::Stroke);
skia_canvas.draw_path(&path, &paint);
}
if let Some(collector) = &self.rendering_metrics_collector.borrow_mut().as_ref() {
collector.measure_frame_rendered(item_renderer, Default::default());
if collector.refresh_mode()
== i_slint_core::graphics::rendering_metrics_collector::RefreshMode::FullSpeed
&& let Some(partial_rendering_state) = self.partial_rendering_state()
{
partial_rendering_state.force_screen_refresh();
}
}
if let Some(cb) = post_render_cb.as_ref() {
cb(item_renderer)
}
}
// jank fix (2026-10-28, keep): removed the final synchronous
// GrDirectContext::flush. It blocked the UI thread 19-24 ms on the
// Mali G715 whenever the driver queue was momentarily busy (the
// scroll jank in docs/BUGS/slint-skia-final-flush-jank.md). It is
// redundant here: wgpu_30_surface::render calls
// flush_and_submit (gr_context.submit) before queue.present, and
// Vulkan executes both in order on the same queue.
// if let Some(ctx) = gr_context.as_mut() {
// ctx.flush(None);
// }
dirty_region
}
fn window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
self.maybe_window_adapter.borrow().as_ref().and_then(|w| w.upgrade()).ok_or_else(|| {
"Renderer must be associated with component before use".to_string().into()
})
}
/// Sets the specified callback, that's invoked before presenting the rendered buffer to the windowing system.
/// This can be useful to implement frame throttling, i.e. for requesting a frame callback from the wayland compositor.
pub fn set_pre_present_callback(&self, callback: Option<Box<dyn FnMut()>>) {
*self.pre_present_callback.borrow_mut() = callback;
}
fn partial_rendering_state(&self) -> Option<&PartialRenderingState> {
// We don't know where the application might render to, so disable partial rendering.
if self.rendering_notifier.borrow().is_some() {
None
} else {
self.partial_rendering_state.as_ref()
}
}
}
impl i_slint_core::renderer::RendererSealed for SkiaRenderer {
fn text_layout_cache(&self) -> Option<&sharedparley::TextLayoutCache> {
Some(&self.text_layout_cache)
}
fn set_rendering_notifier(
&self,
callback: Box<dyn RenderingNotifier>,
) -> std::result::Result<(), SetRenderingNotifierError> {
let mut notifier = self.rendering_notifier.borrow_mut();
if notifier.replace(callback).is_some() {
Err(SetRenderingNotifierError::AlreadySet)
} else {
Ok(())
}
}
fn free_graphics_resources(
&self,
component: i_slint_core::item_tree::ItemTreeRef,
items: &mut dyn Iterator<Item = std::pin::Pin<i_slint_core::items::ItemRef<'_>>>,
) -> Result<(), i_slint_core::platform::PlatformError> {
self.image_cache.component_destroyed(component);
self.path_cache.component_destroyed(component);
self.text_layout_cache.component_destroyed(component);
if let Some(partial_rendering_state) = self.partial_rendering_state() {
partial_rendering_state.free_graphics_resources(component, items);
}
Ok(())
}
fn set_window_adapter(&self, window_adapter: &Rc<dyn WindowAdapter>) {
*self.maybe_window_adapter.borrow_mut() = Some(Rc::downgrade(window_adapter));
self.clear_graphics_caches();
if let Some(partial_rendering_state) = self.partial_rendering_state() {
partial_rendering_state.clear_cache();
}
}
fn window_adapter(&self) -> Option<Rc<dyn WindowAdapter>> {
self.maybe_window_adapter
.borrow()
.as_ref()
.and_then(|window_adapter| window_adapter.upgrade())
}
fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> {
if size.width == 0 || size.height == 0 {
return Ok(());
}
if let Some(surface) = self.surface.borrow().as_ref() {
surface.resize_event(size)
} else {
Ok(())
}
}
/// Returns an image buffer with the contents of the window, obtained by re-rendering the scene into a CPU-side surface.
fn take_snapshot(
&self,
) -> Result<SharedPixelBuffer<i_slint_core::graphics::Rgba8Pixel>, PlatformError> {
let window_adapter = self.window_adapter()?;
let window = window_adapter.window();
let size = window_adapter.window().size();
let (width, height) = (size.width, size.height);
let mut target_buffer =
SharedPixelBuffer::<i_slint_core::graphics::Rgba8Pixel>::new(width, height);
let mut surface_borrow = skia_safe::surfaces::wrap_pixels(
&image_info(
(width as i32, height as i32),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Opaque,
),
target_buffer.make_mut_bytes(),
None,
None,
)
.ok_or_else(|| "Error wrapping target buffer for rendering into with Skia".to_string())?;
self.render_to_canvas(surface_borrow.canvas(), 0., (0.0, 0.0), None, 0, None, window, None);
Ok(target_buffer)
}
fn mark_dirty_region(&self, region: DirtyRegion) {
if let Some(partial_rendering_state) = self.partial_rendering_state() {
partial_rendering_state.mark_dirty_region(region);
}
}
fn supports_transformations(&self) -> bool {
true
}
}
impl Drop for SkiaRenderer {
fn drop(&mut self) {
self.clear_surface()
}
}
/// This trait represents the interface between the Skia renderer and the underlying rendering surface, such as a window
/// with a metal layer, a wayland window with an OpenGL context, etc.
pub trait Surface {
/// Creates a new surface with the given window, display, and size.
fn new(
shared_context: &SkiaSharedContext,
window_handle: Arc<dyn raw_window_handle::HasWindowHandle + Sync + Send>,
display_handle: Arc<dyn raw_window_handle::HasDisplayHandle + Sync + Send>,
size: PhysicalWindowSize,
requested_graphics_api: Option<RequestedGraphicsAPI>,
) -> Result<Self, PlatformError>
where
Self: Sized;
/// Returns the name of the surface, for diagnostic purposes.
fn name(&self) -> &'static str;
/// If supported, this invokes the specified callback with access to the platform graphics API.
fn with_graphics_api(&self, _callback: &mut dyn FnMut(GraphicsAPI<'_>)) {}
/// Invokes the callback with the surface active. This has only a meaning for OpenGL rendering, where
/// the implementation must make the GL context current.
fn with_active_surface(
&self,
callback: &mut dyn FnMut(),
) -> Result<(), i_slint_core::platform::PlatformError> {
callback();
Ok(())
}
/// Prepares the surface for rendering and invokes the provided callback with access to a Skia canvas and
/// rendering context. Returning `DrawOutcome::Occluded` or `Timeout` lets the caller
/// re-arm its redraw flag without rendering.
fn render(
&self,
window: &Window,
size: PhysicalWindowSize,
render_callback: &dyn Fn(
&skia_safe::Canvas,
Option<&mut skia_safe::gpu::DirectContext>,
u8,
) -> Option<DirtyRegion>,
pre_present_callback: &RefCell<Option<Box<dyn FnMut()>>>,
) -> Result<DrawOutcome, i_slint_core::platform::PlatformError>;
/// Called when the surface should be resized.
fn resize_event(
&self,
size: PhysicalWindowSize,
) -> Result<(), i_slint_core::platform::PlatformError>;
fn bits_per_pixel(&self) -> Result<u8, PlatformError>;
fn use_partial_rendering(&self) -> bool {
false
}
fn import_opengl_texture(
&self,
_canvas: &skia_safe::Canvas,
_texture: &i_slint_core::graphics::BorrowedOpenGLTexture,
) -> Option<skia_safe::Image> {
None
}
#[cfg(any(feature = "unstable-wgpu-29", feature = "unstable-wgpu-30"))]
fn import_wgpu_texture(
&self,
_canvas: &skia_safe::Canvas,
_texture: &i_slint_core::graphics::WGPUTexture,
) -> Option<skia_safe::Image> {
None
}
/// Implementations should return self to allow upcasting.
fn as_any(&self) -> &dyn core::any::Any {
&()
}
fn set_transparent(&self, _: bool) -> Result<(), PlatformError> {
Ok(())
}
}
pub trait SkiaRendererExt {
fn render_transformed_with_post_callback(
&self,
rotation_angle_degrees: f32,
translation: (f32, f32),
surface_size: PhysicalWindowSize,
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
) -> Result<DrawOutcome, i_slint_core::platform::PlatformError>;
}
impl SkiaRendererExt for SkiaRenderer {
fn render_transformed_with_post_callback(
&self,
rotation_angle_degrees: f32,
translation: (f32, f32),
surface_size: PhysicalWindowSize,
post_render_cb: Option<&dyn Fn(&mut dyn ItemRenderer)>,
) -> Result<DrawOutcome, i_slint_core::platform::PlatformError> {
self.internal_render_with_post_callback(
rotation_angle_degrees,
translation,
surface_size,
post_render_cb,
)
}
}