Three root causes, all fixed and verified on the Pixel 9 Pro: 1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the render to the vsync wake; RENDER_AHEAD presents the previous frame at the start of render() (phase = wake + ~1.1 ms) and holds the new frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never merely dropped, before the next acquire (AlreadyAcquired panic at launch otherwise); discard_pending_frame presents too. 2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled through a ~20-cell viewport forever (137 decodes/20 s) and each eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes, ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone cooldown on evicted paths (idle prefetch skips, urgent tap bypasses) + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us). 3. (earlier) Skia final flush + Mailbox present mode (vendor, M3). Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK lines for cross-thread closure timing and push-site tagging. Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact 120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero >33 ms); PRES phase median ~994 us, zero late frames. Docs updated (docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference). Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch), vkshim profiling tooling, backend made a non-optional dep, debuggable flag for simpleperf (REMOVE before shipping).
148 lines
5.4 KiB
Rust
148 lines
5.4 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 super::{ImageCacheKey, SharedImageBuffer, SharedPixelBuffer};
|
|
#[cfg(all(feature = "image-decoders", not(target_arch = "wasm32")))]
|
|
use crate::SharedString;
|
|
use crate::lengths::PhysicalPx;
|
|
use resvg::{tiny_skia, usvg};
|
|
|
|
pub struct ParsedSVG {
|
|
svg_tree: usvg::Tree,
|
|
cache_key: ImageCacheKey,
|
|
weight_in_bytes: usize,
|
|
}
|
|
|
|
impl super::OpaqueImage for ParsedSVG {
|
|
fn size(&self) -> crate::graphics::IntSize {
|
|
self.size()
|
|
}
|
|
fn cache_key(&self) -> ImageCacheKey {
|
|
self.cache_key.clone()
|
|
}
|
|
}
|
|
|
|
impl core::fmt::Debug for ParsedSVG {
|
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
f.debug_tuple("ParsedSVG").finish()
|
|
}
|
|
}
|
|
|
|
impl ParsedSVG {
|
|
fn new(svg_tree: usvg::Tree, cache_key: ImageCacheKey, source_size: usize) -> Self {
|
|
// The parsed tree typically costs a few times the source size, and outlining
|
|
// a short `<text>` element expands it far more; charge a generous multiple of
|
|
// the source size, plus a constant for the fixed cost of even a tiny tree,
|
|
// so that the cache stays bounded without measuring the tree.
|
|
let weight_in_bytes = source_size.saturating_mul(16).saturating_add(8192);
|
|
Self { svg_tree, cache_key, weight_in_bytes }
|
|
}
|
|
|
|
pub fn size(&self) -> crate::graphics::IntSize {
|
|
let size = self.svg_tree.size().to_int_size();
|
|
[size.width(), size.height()].into()
|
|
}
|
|
|
|
pub fn cache_key(&self) -> ImageCacheKey {
|
|
self.cache_key.clone()
|
|
}
|
|
|
|
/// Approximate number of bytes the parsed tree keeps alive, for cache accounting.
|
|
pub fn weight_in_bytes(&self) -> usize {
|
|
self.weight_in_bytes
|
|
}
|
|
|
|
/// Renders the SVG with the specified size, if no size is specified, get the size from the image
|
|
#[allow(clippy::unnecessary_cast)] // Coord
|
|
pub fn render(
|
|
&self,
|
|
size: Option<euclid::Size2D<u32, PhysicalPx>>,
|
|
) -> Result<SharedImageBuffer, usvg::Error> {
|
|
let tree = &self.svg_tree;
|
|
|
|
let (target_size, transform) = match size {
|
|
Some(size) => {
|
|
let target_size = tiny_skia::IntSize::from_wh(size.width, size.height)
|
|
.ok_or(usvg::Error::InvalidSize)?;
|
|
let target_size = tree.size().to_int_size().scale_to(target_size);
|
|
let target_size_f = target_size.to_size();
|
|
|
|
let transform = tiny_skia::Transform::from_scale(
|
|
target_size_f.width() as f32 / tree.size().width() as f32,
|
|
target_size_f.height() as f32 / tree.size().height() as f32,
|
|
);
|
|
(target_size, transform)
|
|
}
|
|
None => (tree.size().to_int_size(), tiny_skia::Transform::default()),
|
|
};
|
|
|
|
let mut buffer = SharedPixelBuffer::new(target_size.width(), target_size.height());
|
|
let mut skia_buffer = tiny_skia::PixmapMut::from_bytes(
|
|
buffer.make_mut_bytes(),
|
|
target_size.width(),
|
|
target_size.height(),
|
|
)
|
|
.ok_or(usvg::Error::InvalidSize)?;
|
|
|
|
resvg::render(tree, transform, &mut skia_buffer);
|
|
Ok(SharedImageBuffer::RGBA8Premultiplied(buffer))
|
|
}
|
|
}
|
|
|
|
/// Resolves SVG `<text>` fonts through the shared [`sharedfontique::svg`] bridge
|
|
/// against the running `SlintContext`'s collection, so SVG text uses the same fonts
|
|
/// as the rest of the UI.
|
|
/// Without a context the text stays unresolved, which never happens for a displayable
|
|
/// image since the platform is up by then.
|
|
///
|
|
/// Gated on `shared-parley`: without Slint's text engine there is no text layout, so
|
|
/// SVG text resolution would be moot.
|
|
#[cfg(all(feature = "shared-parley", not(target_arch = "wasm32")))]
|
|
fn svg_options() -> usvg::Options<'static> {
|
|
use i_slint_common::sharedfontique::{self, fontique};
|
|
|
|
fn find_font(
|
|
families: &[fontique::QueryFamily],
|
|
attributes: fontique::Attributes,
|
|
require_char: Option<char>,
|
|
) -> Option<fontique::QueryFont> {
|
|
crate::context::GLOBAL_CONTEXT.with(|p| {
|
|
let ctx = p.get()?;
|
|
let mut font_context = ctx.font_context().try_borrow_mut().ok()?;
|
|
let parley::FontContext { collection, source_cache } = &mut font_context.inner;
|
|
sharedfontique::svg::query_font(
|
|
collection,
|
|
source_cache,
|
|
families,
|
|
attributes,
|
|
require_char,
|
|
)
|
|
})
|
|
}
|
|
|
|
sharedfontique::svg::options(find_font)
|
|
}
|
|
|
|
#[cfg(all(not(feature = "shared-parley"), not(target_arch = "wasm32")))]
|
|
fn svg_options() -> usvg::Options<'static> {
|
|
usvg::Options::default()
|
|
}
|
|
|
|
#[cfg(all(feature = "image-decoders", not(target_arch = "wasm32")))]
|
|
pub fn load_from_path(
|
|
path: &SharedString,
|
|
cache_key: ImageCacheKey,
|
|
) -> Result<ParsedSVG, std::io::Error> {
|
|
let svg_data = std::fs::read(std::path::Path::new(&path.as_str()))?;
|
|
|
|
usvg::Tree::from_data(&svg_data, &svg_options())
|
|
.map(|svg| ParsedSVG::new(svg, cache_key, svg_data.len()))
|
|
.map_err(std::io::Error::other)
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn load_from_data(slice: &[u8], cache_key: ImageCacheKey) -> Result<ParsedSVG, usvg::Error> {
|
|
usvg::Tree::from_data(slice, &svg_options())
|
|
.map(|svg| ParsedSVG::new(svg, cache_key, slice.len()))
|
|
}
|