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).
302 lines
9.3 KiB
Rust
302 lines
9.3 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 the builtin Path related items.
|
|
|
|
When adding an item or a property, it needs to be kept in sync with different place.
|
|
Lookup the [`crate::items`] module documentation.
|
|
*/
|
|
|
|
use super::{
|
|
FillRule, Item, ItemConsts, ItemRc, ItemRendererRef, LineCap, LineJoin, RenderingResult,
|
|
};
|
|
use crate::graphics::{Brush, FittedPath, PathData, PathDataIterator};
|
|
use crate::input::{
|
|
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
|
|
KeyEventResult, MouseEvent,
|
|
};
|
|
use crate::item_rendering::CachedRenderingData;
|
|
|
|
use crate::items::ImageFit;
|
|
use crate::layout::{LayoutInfo, Orientation};
|
|
use crate::lengths::{
|
|
LogicalBorderRadius, LogicalLength, LogicalPx, LogicalRect, LogicalSize, LogicalVector,
|
|
RectLengths,
|
|
};
|
|
#[cfg(feature = "rtti")]
|
|
use crate::rtti::*;
|
|
use crate::window::WindowAdapter;
|
|
use crate::{Coord, Property};
|
|
use alloc::boxed::Box;
|
|
use alloc::rc::Rc;
|
|
use const_field_offset::FieldOffsets;
|
|
use core::cell::RefCell;
|
|
use core::pin::Pin;
|
|
use euclid::Point2D;
|
|
use euclid::num::Zero;
|
|
use i_slint_core_macros::*;
|
|
|
|
/// The implementation of the `Path` element
|
|
#[repr(C)]
|
|
#[derive(FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
pub struct Path {
|
|
pub elements: Property<PathData>,
|
|
pub fill: Property<Brush>,
|
|
pub fill_rule: Property<FillRule>,
|
|
pub stroke: Property<Brush>,
|
|
pub stroke_width: Property<LogicalLength>,
|
|
pub stroke_line_cap: Property<LineCap>,
|
|
pub stroke_line_join: Property<LineJoin>,
|
|
pub stroke_miter_limit: Property<f32>,
|
|
pub viewbox_x: Property<f32>,
|
|
pub viewbox_y: Property<f32>,
|
|
pub viewbox_width: Property<f32>,
|
|
pub viewbox_height: Property<f32>,
|
|
pub fit: Property<ImageFit>,
|
|
pub clip: Property<bool>,
|
|
pub anti_alias: Property<bool>,
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
fitted_path: FittedPathBox,
|
|
tracker: crate::properties::PropertyTracker,
|
|
}
|
|
|
|
impl Item for Path {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
|
|
|
fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: Orientation,
|
|
_cross_axis_constraint: Coord,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> LayoutInfo {
|
|
LayoutInfo { stretch: 1., ..LayoutInfo::default() }
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
_: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut super::MouseCursorInner,
|
|
) -> InputEventFilterResult {
|
|
InputEventFilterResult::ForwardAndIgnore
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
_: &MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
_: &mut super::MouseCursorInner,
|
|
) -> InputEventResult {
|
|
InputEventResult::EventIgnored
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_: &InternalKeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> KeyEventResult {
|
|
KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> FocusEventResult {
|
|
FocusEventResult::FocusIgnored
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
backend: &mut ItemRendererRef,
|
|
self_rc: &ItemRc,
|
|
size: LogicalSize,
|
|
) -> RenderingResult {
|
|
let clip = self.clip();
|
|
if clip {
|
|
(*backend).save_state();
|
|
(*backend).combine_clip(size.into(), LogicalBorderRadius::zero());
|
|
}
|
|
(*backend).draw_path(self, self_rc, size);
|
|
if clip {
|
|
(*backend).restore_state();
|
|
}
|
|
RenderingResult::ContinueRenderingChildren
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: core::pin::Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
geometry: LogicalRect,
|
|
) -> LogicalRect {
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl Path {
|
|
/// Returns an iterator of the events of the path and an offset, so that the
|
|
/// shape fits into the width/height of the path while respecting the stroke
|
|
/// width.
|
|
pub fn fitted_path_events(
|
|
self: Pin<&Self>,
|
|
self_rc: &ItemRc,
|
|
) -> Option<(LogicalVector, PathDataIterator)> {
|
|
let mut elements_iter = self.elements().iter()?;
|
|
|
|
let fit = self.fit();
|
|
if fit == ImageFit::Preserve {
|
|
return (LogicalVector::zero(), elements_iter).into();
|
|
}
|
|
|
|
let stroke_width = self.stroke_width();
|
|
let geometry = self_rc.geometry();
|
|
let bounds_width = (geometry.width_length() - stroke_width).max(LogicalLength::zero());
|
|
let bounds_height = (geometry.height_length() - stroke_width).max(LogicalLength::zero());
|
|
let offset =
|
|
LogicalVector::from_lengths(stroke_width / 2 as Coord, stroke_width / 2 as Coord);
|
|
|
|
let viewbox_width = self.viewbox_width();
|
|
let viewbox_height = self.viewbox_height();
|
|
|
|
let maybe_viewbox = if viewbox_width > 0. && viewbox_height > 0. {
|
|
Some(
|
|
euclid::rect(self.viewbox_x(), self.viewbox_y(), viewbox_width, viewbox_height)
|
|
.to_box2d(),
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
elements_iter.fit(bounds_width.get() as _, bounds_height.get() as _, maybe_viewbox, fit);
|
|
(offset, elements_iter).into()
|
|
}
|
|
|
|
fn sample_at(
|
|
self: Pin<&Self>,
|
|
self_rc: &ItemRc,
|
|
t: f32,
|
|
) -> Option<(Point2D<f32, LogicalPx>, f32)> {
|
|
if let Some(new_path) =
|
|
Path::FIELD_OFFSETS.tracker().apply_pin(self).evaluate_if_dirty(|| {
|
|
let (offset, elements_iter) = self.fitted_path_events(self_rc)?;
|
|
Some(elements_iter.to_fitted_path(offset))
|
|
})
|
|
{
|
|
*self.fitted_path.borrow_mut() = new_path;
|
|
}
|
|
self.fitted_path.borrow().as_ref()?.sample_at(t)
|
|
}
|
|
|
|
pub fn point_at(self: Pin<&Self>, self_rc: &ItemRc, t: f32) -> Point2D<f32, LogicalPx> {
|
|
self.sample_at(self_rc, t).map(|(pos, _)| pos).unwrap_or_default()
|
|
}
|
|
pub fn angle_at(self: Pin<&Self>, self_rc: &ItemRc, t: f32) -> f32 {
|
|
self.sample_at(self_rc, t).map(|(_, tangent)| tangent).unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
impl ItemConsts for Path {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<Path, CachedRenderingData> =
|
|
Path::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
|
|
}
|
|
|
|
struct FittedPathInner(RefCell<Option<FittedPath>>);
|
|
|
|
/// Opaque box holding the FittedPath, allocated lazily on first use
|
|
#[repr(C)]
|
|
pub struct FittedPathBox(core::cell::Cell<*mut FittedPathInner>);
|
|
|
|
impl Default for FittedPathBox {
|
|
fn default() -> Self {
|
|
FittedPathBox(core::cell::Cell::new(core::ptr::null_mut()))
|
|
}
|
|
}
|
|
impl FittedPathBox {
|
|
fn get_or_init(&self) -> &FittedPathInner {
|
|
if self.0.get().is_null() {
|
|
self.0.set(Box::leak(Box::new(FittedPathInner(Default::default()))));
|
|
}
|
|
// Safety: the pointer is guaranteed non-null above, and was created from a Box::leak
|
|
unsafe { &*self.0.get() }
|
|
}
|
|
}
|
|
impl Drop for FittedPathBox {
|
|
fn drop(&mut self) {
|
|
let ptr = self.0.get();
|
|
if !ptr.is_null() {
|
|
// Safety: ptr was constructed from a Box::leak in get_or_init
|
|
drop(unsafe { Box::from_raw(ptr) });
|
|
}
|
|
}
|
|
}
|
|
impl core::ops::Deref for FittedPathBox {
|
|
type Target = RefCell<Option<FittedPath>>;
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.get_or_init().0
|
|
}
|
|
}
|
|
|
|
/// # Safety
|
|
/// This must be called using a non-null pointer pointing to a chunk of memory big enough to
|
|
/// hold a FittedPathBox
|
|
#[cfg(feature = "ffi")]
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_path_fitted_cache_init(cache: *mut FittedPathBox) {
|
|
unsafe { core::ptr::write(cache, FittedPathBox::default()) };
|
|
}
|
|
|
|
/// # Safety
|
|
/// This must be called using a non-null pointer pointing to an initialized FittedPathBox
|
|
#[cfg(feature = "ffi")]
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_path_fitted_cache_free(cache: *mut FittedPathBox) {
|
|
unsafe {
|
|
core::ptr::drop_in_place(cache);
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ffi")]
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_path_point_at(
|
|
self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
|
|
self_index: u32,
|
|
t: f32,
|
|
) -> crate::lengths::LogicalPoint {
|
|
let self_rc = ItemRc::new(self_component.clone(), self_index);
|
|
self_rc.downcast::<Path>().unwrap().as_pin_ref().point_at(&self_rc, t)
|
|
}
|
|
|
|
#[cfg(feature = "ffi")]
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_path_angle_at(
|
|
self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
|
|
self_index: u32,
|
|
t: f32,
|
|
) -> f32 {
|
|
let self_rc = ItemRc::new(self_component.clone(), self_index);
|
|
self_rc.downcast::<Path>().unwrap().as_pin_ref().angle_at(&self_rc, t)
|
|
}
|