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).
256 lines
9.6 KiB
Rust
256 lines
9.6 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
|
|
|
|
/*!
|
|
This module contains a cache helper for caching box shadow textures.
|
|
*/
|
|
|
|
use alloc::boxed::Box;
|
|
use std::{
|
|
cell::{Cell, RefCell},
|
|
collections::BTreeMap,
|
|
};
|
|
|
|
use crate::items::ItemRc;
|
|
use crate::{
|
|
Color,
|
|
lengths::{PhysicalBorderRadius, PhysicalPx, RectLengths, ScaleFactor},
|
|
};
|
|
|
|
/// Struct to store options affecting the rendering of a box shadow
|
|
#[derive(Clone, PartialEq, Debug, Default)]
|
|
pub struct BoxShadowOptions {
|
|
/// The width of the box shadow texture in physical pixels.
|
|
pub width: euclid::Length<f32, PhysicalPx>,
|
|
/// The height of the box shadow texture in physical pixels.
|
|
pub height: euclid::Length<f32, PhysicalPx>,
|
|
/// The color for the box shadow.
|
|
pub color: Color,
|
|
/// The blur to apply to the box shadow in pixels.
|
|
pub blur: euclid::Length<f32, PhysicalPx>,
|
|
/// The radii of the box shadow.
|
|
pub radius: PhysicalBorderRadius,
|
|
/// The spread radius in physical pixels. Positive grows the shadow shape, negative shrinks it.
|
|
pub spread: euclid::Length<f32, PhysicalPx>,
|
|
/// Whether the shadow is rendered inside the element's geometry.
|
|
pub inset: bool,
|
|
/// Horizontal offset in physical pixels. Only used by inset shadows (drop-shadow offset is
|
|
/// applied at blit time and is not part of the cached image).
|
|
pub offset_x_inset: f32,
|
|
/// Vertical offset in physical pixels. Only used by inset shadows.
|
|
pub offset_y_inset: f32,
|
|
}
|
|
|
|
impl Eq for BoxShadowOptions {}
|
|
impl Ord for BoxShadowOptions {
|
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
|
let lhs = (
|
|
self.width,
|
|
self.height,
|
|
self.color,
|
|
self.blur,
|
|
self.radius.top_left.to_bits(),
|
|
self.radius.top_right.to_bits(),
|
|
self.radius.bottom_right.to_bits(),
|
|
self.radius.bottom_left.to_bits(),
|
|
self.spread,
|
|
self.inset,
|
|
self.offset_x_inset.to_bits(),
|
|
self.offset_y_inset.to_bits(),
|
|
);
|
|
let rhs = (
|
|
other.width,
|
|
other.height,
|
|
other.color,
|
|
other.blur,
|
|
other.radius.top_left.to_bits(),
|
|
other.radius.top_right.to_bits(),
|
|
other.radius.bottom_right.to_bits(),
|
|
other.radius.bottom_left.to_bits(),
|
|
other.spread,
|
|
other.inset,
|
|
other.offset_x_inset.to_bits(),
|
|
other.offset_y_inset.to_bits(),
|
|
);
|
|
if rhs < lhs {
|
|
std::cmp::Ordering::Less
|
|
} else if lhs < rhs {
|
|
std::cmp::Ordering::Greater
|
|
} else {
|
|
std::cmp::Ordering::Equal
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for BoxShadowOptions {
|
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
/// The geometry of a box shadow, derived from [`BoxShadowOptions`].
|
|
///
|
|
/// The CSS rules the renderers agree on: the shadow shape is the element's geometry
|
|
/// grown by the spread, its corner radii grow with it, and the texture it is rendered
|
|
/// into is padded by the blur on every side so the Gaussian can fade out to
|
|
/// transparency within it.
|
|
impl BoxShadowOptions {
|
|
/// The size of the shadow shape: the element's size grown by the spread on each
|
|
/// side. A negative spread shrinks it, down to nothing.
|
|
pub fn shape_size(&self) -> euclid::Size2D<f32, PhysicalPx> {
|
|
euclid::size2(
|
|
(self.width.get() + 2. * self.spread.get()).max(0.),
|
|
(self.height.get() + 2. * self.spread.get()).max(0.),
|
|
)
|
|
}
|
|
|
|
/// The size of the texture a drop shadow is rendered into: the shape padded by the
|
|
/// blur on each side.
|
|
pub fn drop_texture_size(&self) -> euclid::Size2D<f32, PhysicalPx> {
|
|
self.shape_size() + euclid::size2(2. * self.blur.get(), 2. * self.blur.get())
|
|
}
|
|
|
|
/// Where the shape sits within the drop shadow texture, i.e. the blur padding.
|
|
pub fn shape_origin(&self) -> euclid::Point2D<f32, PhysicalPx> {
|
|
euclid::point2(self.blur.get(), self.blur.get())
|
|
}
|
|
|
|
/// The corner radii of the shadow shape: `max(0, radius + spread)`.
|
|
pub fn outer_radius(&self) -> PhysicalBorderRadius {
|
|
(self.radius + PhysicalBorderRadius::new_uniform(self.spread.get())).max(Default::default())
|
|
}
|
|
|
|
/// The corner radii of the hole an inset shadow leaves: `max(0, radius - spread)`.
|
|
pub fn inner_radius(&self) -> PhysicalBorderRadius {
|
|
(self.radius - PhysicalBorderRadius::new_uniform(self.spread.get())).max(Default::default())
|
|
}
|
|
|
|
/// The Gaussian sigma corresponding to the CSS blur radius.
|
|
pub fn blur_sigma(&self) -> f32 {
|
|
self.blur.get() / 2.
|
|
}
|
|
|
|
/// Extracts the rendering specific properties from the BoxShadow item and scales the logical
|
|
/// coordinates to physical pixels used in the BoxShadowOptions. Returns None if for example the
|
|
/// alpha on the box shadow would imply that no shadow is to be rendered.
|
|
pub fn new(
|
|
item_rc: &ItemRc,
|
|
box_shadow: std::pin::Pin<&crate::items::BoxShadow>,
|
|
scale_factor: ScaleFactor,
|
|
) -> Option<Self> {
|
|
let color = box_shadow.color();
|
|
if color.alpha() == 0 {
|
|
return None;
|
|
}
|
|
let geometry = item_rc.geometry();
|
|
let width = geometry.width_length() * scale_factor;
|
|
let height = geometry.height_length() * scale_factor;
|
|
if width.get() < 1. || height.get() < 1. {
|
|
return None;
|
|
}
|
|
let inset = box_shadow.inset();
|
|
let (offset_x_inset, offset_y_inset) = if inset {
|
|
(
|
|
(box_shadow.offset_x() * scale_factor).get(),
|
|
(box_shadow.offset_y() * scale_factor).get(),
|
|
)
|
|
} else {
|
|
(0., 0.)
|
|
};
|
|
Some(Self {
|
|
width,
|
|
height,
|
|
color,
|
|
blur: box_shadow.blur() * scale_factor, // This effectively becomes the blur radius, so scale to physical pixels
|
|
radius: box_shadow.logical_border_radius() * scale_factor,
|
|
spread: box_shadow.spread() * scale_factor,
|
|
inset,
|
|
offset_x_inset,
|
|
offset_y_inset,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Upper bound on the number of shadow textures kept alive by a [`BoxShadowCache`].
|
|
const MAX_CACHED_SHADOWS: usize = 16;
|
|
|
|
struct CacheEntry<ImageType> {
|
|
image: Option<ImageType>,
|
|
/// Value of the cache's access counter when this entry was last returned, for LRU eviction.
|
|
last_used: u64,
|
|
}
|
|
|
|
/// Cache to hold box textures for given box shadow options.
|
|
pub struct BoxShadowCache<ImageType> {
|
|
entries: RefCell<BTreeMap<BoxShadowOptions, CacheEntry<ImageType>>>,
|
|
access_counter: Cell<u64>,
|
|
/// Track if the window scale factor changes; used to clear the cache if necessary.
|
|
window_scale_factor_tracker: core::pin::Pin<Box<crate::properties::PropertyTracker>>,
|
|
}
|
|
|
|
impl<ImageType> Default for BoxShadowCache<ImageType> {
|
|
fn default() -> Self {
|
|
Self {
|
|
entries: Default::default(),
|
|
access_counter: Default::default(),
|
|
window_scale_factor_tracker: Box::pin(Default::default()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<ImageType> BoxShadowCache<ImageType> {
|
|
/// Removes all cached box shadow textures.
|
|
pub fn clear(&self) {
|
|
self.entries.borrow_mut().clear();
|
|
}
|
|
|
|
/// Clears the cache if the window's scale factor has changed since the last call, as the
|
|
/// cached textures are rendered in physical pixels.
|
|
pub fn clear_cache_if_scale_factor_changed(&self, window: &crate::api::Window) {
|
|
if self.window_scale_factor_tracker.is_dirty() {
|
|
self.window_scale_factor_tracker
|
|
.as_ref()
|
|
.evaluate_as_dependency_root(|| window.scale_factor());
|
|
self.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<ImageType: Clone> BoxShadowCache<ImageType> {
|
|
/// Look up a box shadow texture for a given box shadow item, or create a new one if needed.
|
|
pub fn get_box_shadow(
|
|
&self,
|
|
item_rc: &ItemRc,
|
|
item_cache: &crate::item_rendering::ItemCache<Option<ImageType>>,
|
|
box_shadow: std::pin::Pin<&crate::items::BoxShadow>,
|
|
scale_factor: ScaleFactor,
|
|
shadow_render_fn: impl FnOnce(&BoxShadowOptions) -> Option<ImageType>,
|
|
) -> Option<ImageType> {
|
|
item_cache.get_or_update_cache_entry(item_rc, || {
|
|
let shadow_options = BoxShadowOptions::new(item_rc, box_shadow, scale_factor)?;
|
|
let mut entries = self.entries.borrow_mut();
|
|
// Shadow options that change on every frame (an animated blur for example) would grow
|
|
// the cache without bounds, so evict the least recently used entry when it gets too big.
|
|
// Note that evicted images may still be alive through the per-item cache; eviction only
|
|
// means that the shadow has to be re-rendered on the next per-item cache miss.
|
|
if entries.len() >= MAX_CACHED_SHADOWS
|
|
&& !entries.contains_key(&shadow_options)
|
|
&& let Some(least_recently_used) = entries
|
|
.iter()
|
|
.min_by_key(|(_, entry)| entry.last_used)
|
|
.map(|(options, _)| options.clone())
|
|
{
|
|
entries.remove(&least_recently_used);
|
|
}
|
|
let stamp = self.access_counter.get() + 1;
|
|
self.access_counter.set(stamp);
|
|
let entry = entries.entry(shadow_options.clone()).or_insert_with(|| CacheEntry {
|
|
image: shadow_render_fn(&shadow_options),
|
|
last_used: stamp,
|
|
});
|
|
entry.last_used = stamp;
|
|
entry.image.clone()
|
|
})
|
|
}
|
|
}
|