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

259 lines
8.8 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
use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window};
use i_slint_core::graphics::RequestedGraphicsAPI;
use i_slint_core::partial_renderer::DirtyRegion;
use i_slint_core::renderer::DrawOutcome;
use std::cell::RefCell;
use std::num::NonZeroU32;
use std::rc::Rc;
use std::sync::Arc;
use crate::SkiaSharedContext;
pub trait RenderBuffer {
fn with_buffer(
&self,
window: &Window,
size: PhysicalWindowSize,
render_callback: &mut dyn FnMut(
NonZeroU32,
NonZeroU32,
skia_safe::ColorType,
u8,
&mut [u8],
) -> Result<
Option<DirtyRegion>,
i_slint_core::platform::PlatformError,
>,
) -> Result<(), i_slint_core::platform::PlatformError>;
}
#[cfg(feature = "softbuffer")]
struct SoftbufferRenderBuffer {
_context: softbuffer::Context<Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>>,
surface: RefCell<
softbuffer::Surface<
Arc<dyn raw_window_handle::HasDisplayHandle + Send + Sync>,
Arc<dyn raw_window_handle::HasWindowHandle + Send + Sync>,
>,
>,
}
#[cfg(feature = "softbuffer")]
impl RenderBuffer for SoftbufferRenderBuffer {
fn with_buffer(
&self,
window: &Window,
size: PhysicalWindowSize,
render_callback: &mut dyn FnMut(
NonZeroU32,
NonZeroU32,
skia_safe::ColorType,
u8,
&mut [u8],
) -> Result<
Option<DirtyRegion>,
i_slint_core::platform::PlatformError,
>,
) -> Result<(), i_slint_core::platform::PlatformError> {
let Some((width, height)) = size.width.try_into().ok().zip(size.height.try_into().ok())
else {
// Nothing to render
return Ok(());
};
let mut surface = self.surface.borrow_mut();
surface
.resize(width, height)
.map_err(|e| format!("Error resizing softbuffer surface: {e}"))?;
let mut target_buffer = surface
.buffer_mut()
.map_err(|e| format!("Error retrieving softbuffer rendering buffer: {e}"))?;
let dirty_region = render_callback(
width,
height,
skia_safe::ColorType::BGRA8888,
target_buffer.age(),
bytemuck::cast_slice_mut(target_buffer.as_mut()),
)?;
if let Some(dirty_region) = dirty_region {
let scale_factor = i_slint_core::lengths::ScaleFactor::new(window.scale_factor());
let damage_rects = dirty_region
.iter()
.map(|logical| {
let physical_rect = (logical.to_rect() * scale_factor).round_out();
softbuffer::Rect {
x: physical_rect.min_x().ceil() as _,
y: physical_rect.min_y().ceil() as _,
width: ((physical_rect.width() as i32).max(1) as u32).try_into().unwrap(),
height: ((physical_rect.height() as i32).max(1) as u32).try_into().unwrap(),
}
})
.collect::<Vec<_>>();
target_buffer.present_with_damage(&damage_rects)
} else {
target_buffer.present()
}
.map_err(|e| format!("Error presenting softbuffer buffer after skia rendering: {e}"))?;
Ok(())
}
}
/// This surface renders into the given window using Skia's software rasterize.
pub struct SoftwareSurface {
render_buffer: Box<dyn RenderBuffer>,
}
impl super::Surface for SoftwareSurface {
#[cfg(feature = "softbuffer")]
fn new(
_shared_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<Self, i_slint_core::platform::PlatformError> {
let _context = softbuffer::Context::new(display_handle)
.map_err(|e| format!("Error creating softbuffer context: {e}"))?;
let surface =
softbuffer::Surface::new(&_context, window_handle).map_err(|softbuffer_error| {
format!("Error creating softbuffer surface: {softbuffer_error}")
})?;
let surface_access =
Box::new(SoftbufferRenderBuffer { _context, surface: RefCell::new(surface) });
Ok(Self { render_buffer: surface_access })
}
#[cfg(not(feature = "softbuffer"))]
fn new(
_shared_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<Self, i_slint_core::platform::PlatformError> {
struct DummyBuffer;
impl RenderBuffer for DummyBuffer {
fn with_buffer(
&self,
_window: &Window,
_size: PhysicalWindowSize,
_render_callback: &mut dyn FnMut(
std::num::NonZeroU32,
std::num::NonZeroU32,
skia_safe::ColorType,
u8,
&mut [u8],
) -> Result<
Option<DirtyRegion>,
i_slint_core::platform::PlatformError,
>,
) -> Result<(), i_slint_core::platform::PlatformError> {
Err("Slint's Skia renderer compiled without the 'softbuffer' feature cannot render into a window".into())
}
}
Ok(DummyBuffer.into())
}
fn name(&self) -> &'static str {
"software"
}
fn resize_event(
&self,
_size: PhysicalWindowSize,
) -> Result<(), i_slint_core::platform::PlatformError> {
Ok(())
}
fn render(
&self,
window: &Window,
size: PhysicalWindowSize,
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> {
self.render_buffer.with_buffer(
window,
size,
&mut |width, height, pixel_format, age, pixels| {
let image_info = crate::image_info(
(width.get() as i32, height.get() as i32),
pixel_format,
skia_safe::AlphaType::Opaque,
);
// A scanout buffer may carry padding at the end of each row:
// DRM dumb buffers have their pitch aligned by the driver, so a
// 1366 px wide XRGB8888 row occupies 5504 bytes, not 5464.
// Take the row length from the buffer, as the display does.
let row_bytes = pixels.len() / height.get() as usize;
let row_bytes = (row_bytes >= image_info.min_row_bytes()).then_some(row_bytes);
let mut surface_borrow =
skia_safe::surfaces::wrap_pixels(&image_info, pixels, row_bytes, None)
.ok_or_else(|| {
"Error wrapping target buffer for rendering into with Skia".to_string()
})?;
let dirty_region = callback(surface_borrow.canvas(), None, age);
if let Some(pre_present_callback) = pre_present_callback.borrow_mut().as_mut() {
pre_present_callback();
}
Ok(dirty_region)
},
)?;
Ok(DrawOutcome::Success)
}
fn bits_per_pixel(&self) -> Result<u8, i_slint_core::platform::PlatformError> {
Ok(24)
}
fn use_partial_rendering(&self) -> bool {
true
}
}
impl<T: RenderBuffer + 'static> From<T> for SoftwareSurface {
fn from(render_buffer: T) -> Self {
Self { render_buffer: Box::new(render_buffer) }
}
}
impl<T: RenderBuffer + 'static> RenderBuffer for Rc<T> {
fn with_buffer(
&self,
window: &Window,
size: PhysicalWindowSize,
render_callback: &mut dyn FnMut(
NonZeroU32,
NonZeroU32,
skia_safe::ColorType,
u8,
&mut [u8],
) -> Result<
Option<DirtyRegion>,
i_slint_core::platform::PlatformError,
>,
) -> Result<(), i_slint_core::platform::PlatformError> {
self.as_ref().with_buffer(window, size, render_callback)
}
}