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).
1150 lines
43 KiB
Rust
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,
|
|
)
|
|
}
|
|
}
|