gallery/vendor/i-slint-renderer-skia/wgpu_renderer.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

307 lines
12 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 rasterizers
use std::pin::Pin;
use std::rc::Rc;
use i_slint_core::platform::PlatformError;
use i_slint_core::renderer::RendererSealed;
use i_slint_core::window::WindowAdapter;
use crate::{SkiaRenderer, SkiaSharedContext};
/// Use the Skia renderer with WGPU when implementing a custom Slint platform where you want the
/// scene to be rendered into a WGPU texture. The rendering is done using the
/// [Skia](https://skia.org/) library with platform-native GPU acceleration.
///
/// This is the Skia equivalent of `FemtoVGWGPURenderer`, offering superior font rendering
/// quality through platform-native text rasterizers.
///
/// This type is generic over the wgpu version, one instantiation per enabled
/// `unstable-wgpu-*` feature. Use it through the version-specific aliases,
/// `SkiaWGPU29Renderer` for wgpu 29 and `SkiaWGPU30Renderer` for wgpu 30 —
/// they stay unambiguous when several wgpu versions are enabled at once.
///
/// Rendering notifier callbacks registered via
/// [`Window::set_rendering_notifier()`](i_slint_core::api::Window::set_rendering_notifier)
/// will receive [`GraphicsAPI::WGPU29`](i_slint_core::api::GraphicsAPI::WGPU29) (respectively
/// [`GraphicsAPI::WGPU30`](i_slint_core::api::GraphicsAPI::WGPU30)) with the
/// renderer's instance, device, and queue.
pub struct SkiaWGPURendererGeneric<Surface> {
renderer: SkiaRenderer,
_surface: std::marker::PhantomData<Surface>,
}
/// Renders into wgpu 29 textures. Available with the `unstable-wgpu-29` feature;
/// the matching wgpu API types are re-exported in the `slint::wgpu_29` module.
#[cfg(feature = "wgpu-29")]
pub type SkiaWGPU29Renderer = SkiaWGPURendererGeneric<crate::wgpu_29_surface::WGPUSurface>;
/// Renders into wgpu 30 textures. Available with the `unstable-wgpu-30` feature;
/// the matching wgpu API types are re-exported in the `slint::wgpu_30` module.
#[cfg(feature = "wgpu-30")]
pub type SkiaWGPU30Renderer = SkiaWGPURendererGeneric<crate::wgpu_30_surface::WGPUSurface>;
/// Deprecated alias for the newest enabled wgpu version: wgpu 30 when the
/// `unstable-wgpu-30` feature is enabled, wgpu 29 otherwise.
///
/// Use the versioned names, `SkiaWGPU29Renderer` or `SkiaWGPU30Renderer`, instead;
/// they don't change meaning when another `unstable-wgpu-*` feature gets enabled elsewhere
/// in the dependency graph.
#[deprecated(since = "1.18.0", note = "Use `SkiaWGPU29Renderer` or `SkiaWGPU30Renderer` instead")]
#[cfg(feature = "wgpu-30")]
pub type SkiaWGPURenderer = SkiaWGPU30Renderer;
/// Deprecated alias for the newest enabled wgpu version: wgpu 30 when the
/// `unstable-wgpu-30` feature is enabled, wgpu 29 otherwise.
///
/// Use the versioned names, `SkiaWGPU29Renderer` or `SkiaWGPU30Renderer`, instead;
/// they don't change meaning when another `unstable-wgpu-*` feature gets enabled elsewhere
/// in the dependency graph.
#[deprecated(since = "1.18.0", note = "Use `SkiaWGPU29Renderer` or `SkiaWGPU30Renderer` instead")]
#[cfg(all(feature = "wgpu-29", not(feature = "wgpu-30")))]
pub type SkiaWGPURenderer = SkiaWGPU29Renderer;
impl<Surface: crate::Surface + 'static> SkiaWGPURendererGeneric<Surface> {
fn new_with_surface(surface: Surface) -> Self {
let shared_context = SkiaSharedContext::default();
let renderer = SkiaRenderer::new_with_surface(&shared_context, Box::new(surface));
Self { renderer, _surface: std::marker::PhantomData }
}
// SkiaRenderer owns the surface as a trait object; downcast back to the concrete
// type for the WGPU-specific fields and methods that `render_to_texture` needs.
fn surface(&self) -> std::cell::Ref<'_, Surface> {
std::cell::Ref::map(self.renderer.surface.borrow(), |surface| {
surface.as_ref().unwrap().as_any().downcast_ref::<Surface>().unwrap()
})
}
}
// The version-specific constructors and render entry points below are kept in sync manually,
// like the wgpu_29_surface/wgpu_30_surface modules they build on.
#[cfg(feature = "wgpu-29")]
impl SkiaWGPU29Renderer {
/// Creates a new SkiaWGPU29Renderer.
///
/// The `instance`, `adapter`, `device` and `queue` are the WGPU resources used for rendering.
/// The `adapter` is needed to determine the GPU backend and create the Skia graphics context.
///
/// The wgpu resources are also provided to rendering notifier callbacks via
/// [`GraphicsAPI::WGPU29`](i_slint_core::api::GraphicsAPI::WGPU29).
pub fn new(
instance: wgpu_29::Instance,
adapter: wgpu_29::Adapter,
device: wgpu_29::Device,
queue: wgpu_29::Queue,
) -> Result<Self, PlatformError> {
use crate::wgpu_29_surface::{Backend, WGPUSurface};
let backend = Backend::new(&adapter, &device)?;
let gr_context = backend.make_context(&adapter, &device, &queue).ok_or_else(|| {
PlatformError::from("Failed to create Skia graphics context from WGPU")
})?;
Ok(Self::new_with_surface(WGPUSurface::new_offscreen(
instance, adapter, device, queue, backend, gr_context,
)))
}
/// Render the scene to the given texture.
///
/// The texture must have been created with `RENDER_ATTACHMENT` usage and have a supported
/// format. Supported formats depend on the GPU backend: `Rgba8Unorm` and `Rgba8UnormSrgb`
/// are supported on all backends; `Bgra8Unorm` is additionally supported on Metal and Vulkan.
pub fn render_to_texture(&self, texture: &wgpu_29::Texture) -> Result<(), PlatformError> {
let surface = self.surface();
self.renderer.invoke_rendering_notifier_setup(&*surface)?;
let gr_context = &mut surface.gr_context.borrow_mut();
let mut skia_surface =
surface.backend.make_surface(gr_context, texture).ok_or_else(|| {
PlatformError::from("Failed to wrap WGPU texture as Skia render target")
})?;
let window_adapter = self.renderer.window_adapter()?;
let window = window_adapter.window();
self.renderer.render_to_canvas(
skia_surface.canvas(),
0.,
(0., 0.),
Some(gr_context),
0,
Some(&*surface),
window,
None,
);
surface.flush_and_submit(gr_context);
Ok(())
}
}
#[cfg(feature = "wgpu-30")]
impl SkiaWGPU30Renderer {
/// Creates a new SkiaWGPU30Renderer.
///
/// The `instance`, `adapter`, `device` and `queue` are the WGPU resources used for rendering.
/// The `adapter` is needed to determine the GPU backend and create the Skia graphics context.
///
/// The wgpu resources are also provided to rendering notifier callbacks via
/// [`GraphicsAPI::WGPU30`](i_slint_core::api::GraphicsAPI::WGPU30).
pub fn new(
instance: wgpu_30::Instance,
adapter: wgpu_30::Adapter,
device: wgpu_30::Device,
queue: wgpu_30::Queue,
) -> Result<Self, PlatformError> {
use crate::wgpu_30_surface::{Backend, WGPUSurface};
let backend = Backend::new(&adapter, &device)?;
let gr_context = backend.make_context(&adapter, &device, &queue).ok_or_else(|| {
PlatformError::from("Failed to create Skia graphics context from WGPU")
})?;
Ok(Self::new_with_surface(WGPUSurface::new_offscreen(
instance, adapter, device, queue, backend, gr_context,
)))
}
/// Render the scene to the given texture.
///
/// The texture must have been created with `RENDER_ATTACHMENT` usage and have a supported
/// format. Supported formats depend on the GPU backend: `Rgba8Unorm` and `Rgba8UnormSrgb`
/// are supported on all backends; `Bgra8Unorm` is additionally supported on Metal and Vulkan.
pub fn render_to_texture(&self, texture: &wgpu_30::Texture) -> Result<(), PlatformError> {
let surface = self.surface();
self.renderer.invoke_rendering_notifier_setup(&*surface)?;
let gr_context = &mut surface.gr_context.borrow_mut();
let mut skia_surface =
surface.backend.make_surface(gr_context, texture).ok_or_else(|| {
PlatformError::from("Failed to wrap WGPU texture as Skia render target")
})?;
let window_adapter = self.renderer.window_adapter()?;
let window = window_adapter.window();
self.renderer.render_to_canvas(
skia_surface.canvas(),
0.,
(0., 0.),
Some(gr_context),
0,
Some(&*surface),
window,
None,
);
surface.flush_and_submit(gr_context);
Ok(())
}
}
#[doc(hidden)]
impl<Surface> RendererSealed for SkiaWGPURendererGeneric<Surface> {
fn text_layout_cache(
&self,
) -> Option<&i_slint_core::textlayout::sharedparley::TextLayoutCache> {
RendererSealed::text_layout_cache(&self.renderer)
}
fn set_rendering_notifier(
&self,
callback: Box<dyn i_slint_core::api::RenderingNotifier>,
) -> Result<(), i_slint_core::api::SetRenderingNotifierError> {
self.renderer.set_rendering_notifier(callback)
}
fn free_graphics_resources(
&self,
component: i_slint_core::item_tree::ItemTreeRef,
items: &mut dyn Iterator<Item = Pin<i_slint_core::items::ItemRef<'_>>>,
) -> Result<(), PlatformError> {
self.renderer.free_graphics_resources(component, items)
}
fn set_window_adapter(&self, window_adapter: &Rc<dyn WindowAdapter>) {
self.renderer.set_window_adapter(window_adapter)
}
fn window_adapter(&self) -> Option<Rc<dyn WindowAdapter>> {
RendererSealed::window_adapter(&self.renderer)
}
fn resize(&self, size: i_slint_core::api::PhysicalSize) -> Result<(), PlatformError> {
self.renderer.resize(size)
}
fn take_snapshot(
&self,
) -> Result<
i_slint_core::graphics::SharedPixelBuffer<i_slint_core::graphics::Rgba8Pixel>,
PlatformError,
> {
self.renderer.take_snapshot()
}
fn supports_transformations(&self) -> bool {
self.renderer.supports_transformations()
}
}
// Assert the consumer-visible signatures of every renderer name. Building the crate only
// compiles the definitions; these coercions prove the intended calls resolve, in particular
// that the deprecated `SkiaWGPURenderer::new` stays unambiguous with both wgpu features enabled.
#[cfg(test)]
mod signature_tests {
use super::*;
#[cfg(feature = "wgpu-29")]
const _: fn(
wgpu_29::Instance,
wgpu_29::Adapter,
wgpu_29::Device,
wgpu_29::Queue,
) -> Result<SkiaWGPU29Renderer, PlatformError> = SkiaWGPU29Renderer::new;
#[cfg(feature = "wgpu-29")]
const _: fn(&SkiaWGPU29Renderer, &wgpu_29::Texture) -> Result<(), PlatformError> =
SkiaWGPU29Renderer::render_to_texture;
#[cfg(feature = "wgpu-30")]
const _: fn(
wgpu_30::Instance,
wgpu_30::Adapter,
wgpu_30::Device,
wgpu_30::Queue,
) -> Result<SkiaWGPU30Renderer, PlatformError> = SkiaWGPU30Renderer::new;
#[cfg(feature = "wgpu-30")]
const _: fn(&SkiaWGPU30Renderer, &wgpu_30::Texture) -> Result<(), PlatformError> =
SkiaWGPU30Renderer::render_to_texture;
#[cfg(feature = "wgpu-30")]
#[allow(deprecated)]
const _: fn(
wgpu_30::Instance,
wgpu_30::Adapter,
wgpu_30::Device,
wgpu_30::Queue,
) -> Result<SkiaWGPURenderer, PlatformError> = SkiaWGPURenderer::new;
#[cfg(all(feature = "wgpu-29", not(feature = "wgpu-30")))]
#[allow(deprecated)]
const _: fn(
wgpu_29::Instance,
wgpu_29::Adapter,
wgpu_29::Device,
wgpu_29::Queue,
) -> Result<SkiaWGPURenderer, PlatformError> = SkiaWGPURenderer::new;
}