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).
527 lines
17 KiB
Rust
527 lines
17 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 enable runtime type information for the builtin items and
|
|
property so that the viewer can handle them
|
|
*/
|
|
|
|
#![allow(clippy::result_unit_err)] // We have nothing better to report
|
|
|
|
pub type FieldOffset<T, U> = const_field_offset::FieldOffset<T, U, const_field_offset::AllowPin>;
|
|
use crate::Property;
|
|
use crate::items::PropertyAnimation;
|
|
use crate::properties::InterpolatedPropertyValue;
|
|
use alloc::boxed::Box;
|
|
use alloc::rc::Rc;
|
|
use alloc::vec::Vec;
|
|
use core::convert::{TryFrom, TryInto};
|
|
use core::ffi::c_void;
|
|
use core::pin::Pin;
|
|
|
|
macro_rules! declare_ValueType {
|
|
($($ty:ty,)*) => {
|
|
pub trait ValueType: 'static + PartialEq + Default + Clone $(+ TryInto<$ty> + TryFrom<$ty>)* {}
|
|
};
|
|
}
|
|
|
|
macro_rules! declare_ValueType_2 {
|
|
($( $(#[$enum_doc:meta])* $vis:vis enum $Name:ident { $($body:tt)* })*) => {
|
|
declare_ValueType![
|
|
(),
|
|
bool,
|
|
u32,
|
|
u64,
|
|
i32,
|
|
i64,
|
|
f32,
|
|
f64,
|
|
crate::SharedString,
|
|
crate::graphics::Image,
|
|
crate::Color,
|
|
crate::PathData,
|
|
crate::animations::EasingCurve,
|
|
crate::items::StandardListViewItem,
|
|
crate::items::TableColumn,
|
|
crate::input::KeyEvent,
|
|
crate::Brush,
|
|
crate::graphics::Point,
|
|
crate::items::PointerEvent,
|
|
crate::items::PointerScrollEvent,
|
|
crate::lengths::LogicalLength,
|
|
crate::lengths::LogicalPoint,
|
|
crate::lengths::LogicalSize,
|
|
crate::lengths::LogicalEdges,
|
|
crate::component_factory::ComponentFactory,
|
|
crate::api::LogicalPosition,
|
|
crate::items::FontMetrics,
|
|
crate::items::InputMethodHints,
|
|
crate::items::MenuEntry,
|
|
crate::items::DropEvent,
|
|
crate::model::ModelRc<crate::items::MenuEntry>,
|
|
crate::styled_text::StyledText,
|
|
crate::input::Keys,
|
|
crate::data_transfer::DataTransfer,
|
|
crate::cursor::MouseCursorInner,
|
|
$(crate::items::$Name,)*
|
|
];
|
|
};
|
|
}
|
|
|
|
i_slint_common::for_each_enums!(declare_ValueType_2);
|
|
|
|
/// What kind of animation is on a binding
|
|
pub enum AnimatedBindingKind {
|
|
/// No animation is on the binding
|
|
NotAnimated,
|
|
/// Single animation
|
|
Animation(Box<dyn Fn() -> PropertyAnimation>),
|
|
/// Transition
|
|
Transition(Box<dyn Fn() -> (PropertyAnimation, crate::animations::Instant)>),
|
|
}
|
|
|
|
impl AnimatedBindingKind {
|
|
/// return a PropertyAnimation if self contains AnimatedBindingKind::Animation
|
|
pub fn as_animation(self) -> Option<PropertyAnimation> {
|
|
match self {
|
|
AnimatedBindingKind::NotAnimated => None,
|
|
AnimatedBindingKind::Animation(a) => Some(a()),
|
|
AnimatedBindingKind::Transition(_) => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub trait TwoWayBindingMapping<Value> {
|
|
fn map_to(&self, value: &Value) -> Value;
|
|
fn map_from(&self, value: &mut Value, from: &Value);
|
|
}
|
|
|
|
impl<Value, F1: Fn(&Value) -> Value, F2: Fn(&mut Value, &Value)> TwoWayBindingMapping<Value>
|
|
for (F1, F2)
|
|
{
|
|
fn map_to(&self, value: &Value) -> Value {
|
|
(self.0)(value)
|
|
}
|
|
|
|
fn map_from(&self, value: &mut Value, from: &Value) {
|
|
(self.1)(value, from)
|
|
}
|
|
}
|
|
|
|
pub trait PropertyInfo<Item, Value> {
|
|
fn get(&self, item: Pin<&Item>) -> Result<Value, ()>;
|
|
fn set(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
value: Value,
|
|
animation: Option<PropertyAnimation>,
|
|
) -> Result<(), ()>;
|
|
fn set_binding(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
binding: Box<dyn Fn() -> Value>,
|
|
animation: AnimatedBindingKind,
|
|
) -> Result<(), ()>;
|
|
|
|
/// The offset of the property in the item.
|
|
/// The use of this is unsafe
|
|
fn offset(&self) -> usize;
|
|
|
|
/// Returns self. This is just a trick to get auto-deref specialization of
|
|
/// MaybeAnimatedPropertyInfoWrapper working.
|
|
fn as_property_info(&'static self) -> &'static dyn PropertyInfo<Item, Value>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
self
|
|
}
|
|
|
|
/// Calls Property::link_two_ways with the property represented here and the property pointer
|
|
///
|
|
/// # Safety
|
|
/// the property2 must be a pinned pointer to a Property of the same type
|
|
#[allow(unsafe_code)]
|
|
unsafe fn link_two_ways(&self, item: Pin<&Item>, property2: *const c_void);
|
|
|
|
/// Set the debug name of this property (only effective with `cfg(slint_debug_property)`)
|
|
#[cfg(slint_debug_property)]
|
|
fn set_debug_name(&self, _item: Pin<&Item>, _name: alloc::string::String) {}
|
|
|
|
/// Prepare the property for two way binding and return the "common" shared property in the TwoWayBinding
|
|
///
|
|
/// Every call for the same property must hand out a property backed by that one
|
|
/// common property, otherwise a later call detaches what an earlier one linked.
|
|
fn prepare_for_two_way_binding(&self, item: Pin<&Item>) -> Pin<Rc<Property<Value>>>;
|
|
|
|
/// Link another property to this property with a mapping function
|
|
///
|
|
/// if the mapper is None, it uses the identity mapping
|
|
fn link_two_way_with_map(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
property2: Pin<Rc<Property<Value>>>,
|
|
mapper: Option<Rc<dyn TwoWayBindingMapping<Value>>>,
|
|
);
|
|
|
|
/// Install a two-way binding between this property and a row of a model.
|
|
/// `getter` reads the current row value (or `None` if the row no longer
|
|
/// exists); `setter` writes a new value back into the row.
|
|
fn link_two_way_to_model_data(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
getter: Box<dyn Fn() -> Option<Value>>,
|
|
setter: Box<dyn Fn(&Value)>,
|
|
);
|
|
}
|
|
|
|
// The two helpers below only depend on `Value`, so they are kept out of the
|
|
// generic `PropertyInfo` impl: there they would be instantiated once per
|
|
// item property type instead of once.
|
|
|
|
/// Returns the common property of `p`, installing a two-way binding on it first if needed.
|
|
///
|
|
/// link_two_way installs a TwoWayBinding on p (moving any
|
|
/// existing binding into the shared common property), which
|
|
/// check_common_property finds on subsequent calls. This keeps
|
|
/// prepare_for_two_way_binding idempotent: when several fields of
|
|
/// the same struct property are two-way bound, they must all share
|
|
/// one common property instead of each creating its own.
|
|
fn common_property<Value: Clone + PartialEq + Default + 'static>(
|
|
p: Pin<&Property<Value>>,
|
|
) -> Pin<Rc<Property<Value>>> {
|
|
if let Some(cp) = Property::check_common_property(p) {
|
|
return cp;
|
|
}
|
|
let anchor = Rc::pin(Property::<Value>::default());
|
|
Property::link_two_way(anchor.as_ref(), p);
|
|
Property::check_common_property(p).unwrap()
|
|
}
|
|
|
|
/// Links `prop2` to the common property `prop1`, through `mapper` when given.
|
|
fn link_common_properties<Value: Clone + PartialEq + 'static>(
|
|
prop1: Pin<Rc<Property<Value>>>,
|
|
prop2: Pin<Rc<Property<Value>>>,
|
|
mapper: Option<Rc<dyn TwoWayBindingMapping<Value>>>,
|
|
) {
|
|
match mapper {
|
|
Some(m1) => {
|
|
let m2 = m1.clone();
|
|
Property::link_two_way_with_map_to_common_property(
|
|
prop2,
|
|
prop1.as_ref(),
|
|
move |value| m1.map_to(value),
|
|
move |value, value2| m2.map_from(value, value2),
|
|
true,
|
|
);
|
|
}
|
|
None => {
|
|
Property::link_two_way_with_map_to_common_property(
|
|
prop2,
|
|
prop1.as_ref(),
|
|
|value| value.clone(),
|
|
|value, value2| *value = value2.clone(),
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<Item: 'static, T, Value> PropertyInfo<Item, Value> for FieldOffset<Item, Property<T>>
|
|
where
|
|
Value: TryInto<T> + Clone + PartialEq + Default + 'static,
|
|
T: TryInto<Value> + Clone + PartialEq + Default + 'static,
|
|
{
|
|
fn get(&self, item: Pin<&Item>) -> Result<Value, ()> {
|
|
self.apply_pin(item).get().try_into().map_err(|_| ())
|
|
}
|
|
fn set(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
value: Value,
|
|
animation: Option<PropertyAnimation>,
|
|
) -> Result<(), ()> {
|
|
if animation.is_some() {
|
|
Err(())
|
|
} else {
|
|
self.apply_pin(item).set(value.try_into().map_err(|_| ())?);
|
|
Ok(())
|
|
}
|
|
}
|
|
fn set_binding(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
binding: Box<dyn Fn() -> Value>,
|
|
animation: AnimatedBindingKind,
|
|
) -> Result<(), ()> {
|
|
if !matches!(animation, AnimatedBindingKind::NotAnimated) {
|
|
Err(())
|
|
} else {
|
|
self.apply_pin(item).set_binding(move || {
|
|
binding().try_into().map_err(|_| ()).expect("binding was of the wrong type")
|
|
});
|
|
Ok(())
|
|
}
|
|
}
|
|
fn offset(&self) -> usize {
|
|
self.get_byte_offset()
|
|
}
|
|
|
|
#[cfg(slint_debug_property)]
|
|
fn set_debug_name(&self, item: Pin<&Item>, name: alloc::string::String) {
|
|
self.apply_pin(item).debug_name.replace(name);
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
unsafe fn link_two_ways(&self, item: Pin<&Item>, property2: *const c_void) {
|
|
let p1 = self.apply_pin(item);
|
|
// Safety: that's the invariant of this function
|
|
let p2 = unsafe { Pin::new_unchecked((property2 as *const Property<T>).as_ref().unwrap()) };
|
|
Property::link_two_way(p1, p2);
|
|
}
|
|
|
|
fn prepare_for_two_way_binding(&self, item: Pin<&Item>) -> Pin<Rc<Property<Value>>> {
|
|
if let Some(self_) =
|
|
(self as &dyn core::any::Any).downcast_ref::<FieldOffset<Item, Property<Value>>>()
|
|
{
|
|
return common_property(self_.apply_pin(item));
|
|
}
|
|
|
|
let shared_property = Rc::pin(Property::<Value>::default());
|
|
Property::link_two_way_with_map(
|
|
self.apply_pin(item),
|
|
shared_property.as_ref(),
|
|
|v| v.clone().try_into().unwrap_or_default(),
|
|
|v, v2| *v = v2.clone().try_into().unwrap_or_default(),
|
|
);
|
|
shared_property
|
|
}
|
|
|
|
fn link_two_way_with_map(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
prop2: Pin<Rc<Property<Value>>>,
|
|
mapper: Option<Rc<dyn TwoWayBindingMapping<Value>>>,
|
|
) {
|
|
let prop1 = self.prepare_for_two_way_binding(item);
|
|
link_common_properties(prop1, prop2, mapper);
|
|
}
|
|
|
|
fn link_two_way_to_model_data(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
getter: Box<dyn Fn() -> Option<Value>>,
|
|
setter: Box<dyn Fn(&Value)>,
|
|
) {
|
|
self.apply_pin(item).link_two_way_to_model_data(
|
|
(),
|
|
move |_| getter().and_then(|v| v.try_into().ok()),
|
|
move |_, v: &T| setter(&v.clone().try_into().unwrap_or_default()),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Wrapper for a field offset that optionally implement PropertyInfo and uses
|
|
/// the auto deref specialization trick
|
|
#[derive(derive_more::Deref)]
|
|
pub struct MaybeAnimatedPropertyInfoWrapper<T, U>(pub FieldOffset<T, U>);
|
|
|
|
impl<Item: 'static, T, Value> PropertyInfo<Item, Value>
|
|
for MaybeAnimatedPropertyInfoWrapper<Item, Property<T>>
|
|
where
|
|
Value: TryInto<T> + Clone + PartialEq + Default + 'static,
|
|
T: TryInto<Value> + Clone + PartialEq + Default + 'static,
|
|
T: InterpolatedPropertyValue,
|
|
{
|
|
fn get(&self, item: Pin<&Item>) -> Result<Value, ()> {
|
|
self.0.get(item)
|
|
}
|
|
fn set(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
value: Value,
|
|
animation: Option<PropertyAnimation>,
|
|
) -> Result<(), ()> {
|
|
if let Some(animation) = animation {
|
|
self.apply_pin(item).set_animated_value(value.try_into().map_err(|_| ())?, animation);
|
|
Ok(())
|
|
} else {
|
|
self.0.set(item, value, None)
|
|
}
|
|
}
|
|
fn set_binding(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
binding: Box<dyn Fn() -> Value>,
|
|
animation: AnimatedBindingKind,
|
|
) -> Result<(), ()> {
|
|
// Put in a function that does not depends on Item to avoid code bloat
|
|
fn set_binding_impl<T, Value>(
|
|
p: Pin<&Property<T>>,
|
|
binding: Box<dyn Fn() -> Value>,
|
|
animation: AnimatedBindingKind,
|
|
) -> Result<(), ()>
|
|
where
|
|
T: Clone + TryInto<Value> + InterpolatedPropertyValue + 'static,
|
|
Value: TryInto<T> + 'static,
|
|
{
|
|
match animation {
|
|
AnimatedBindingKind::NotAnimated => {
|
|
p.set_binding(move || {
|
|
binding().try_into().map_err(|_| ()).expect("binding was of the wrong type")
|
|
});
|
|
Ok(())
|
|
}
|
|
AnimatedBindingKind::Animation(animation) => {
|
|
p.set_animated_binding(
|
|
move || {
|
|
binding()
|
|
.try_into()
|
|
.map_err(|_| ())
|
|
.expect("binding was of the wrong type")
|
|
},
|
|
move || (animation(), None),
|
|
);
|
|
Ok(())
|
|
}
|
|
AnimatedBindingKind::Transition(tr) => {
|
|
p.set_animated_binding(
|
|
move || {
|
|
binding()
|
|
.try_into()
|
|
.map_err(|_| ())
|
|
.expect("binding was of the wrong type")
|
|
},
|
|
move || {
|
|
let (animation, start_time) = tr();
|
|
(animation, Some(start_time))
|
|
},
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
set_binding_impl(self.apply_pin(item), binding, animation)
|
|
}
|
|
fn offset(&self) -> usize {
|
|
self.get_byte_offset()
|
|
}
|
|
|
|
#[cfg(slint_debug_property)]
|
|
fn set_debug_name(&self, item: Pin<&Item>, name: alloc::string::String) {
|
|
self.0.set_debug_name(item, name);
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
unsafe fn link_two_ways(&self, item: Pin<&Item>, property2: *const c_void) {
|
|
let p1 = self.apply_pin(item);
|
|
// Safety: that's the invariant of this function
|
|
let p2 = unsafe { Pin::new_unchecked((property2 as *const Property<T>).as_ref().unwrap()) };
|
|
Property::link_two_way(p1, p2);
|
|
}
|
|
|
|
fn prepare_for_two_way_binding(&self, item: Pin<&Item>) -> Pin<Rc<Property<Value>>> {
|
|
self.0.prepare_for_two_way_binding(item)
|
|
}
|
|
|
|
fn link_two_way_with_map(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
property2: Pin<Rc<Property<Value>>>,
|
|
mapper: Option<Rc<dyn TwoWayBindingMapping<Value>>>,
|
|
) {
|
|
self.0.link_two_way_with_map(item, property2, mapper)
|
|
}
|
|
|
|
fn link_two_way_to_model_data(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
getter: Box<dyn Fn() -> Option<Value>>,
|
|
setter: Box<dyn Fn(&Value)>,
|
|
) {
|
|
self.0.link_two_way_to_model_data(item, getter, setter)
|
|
}
|
|
}
|
|
|
|
pub trait CallbackInfo<Item, Value> {
|
|
fn call(&self, item: Pin<&Item>, args: &[Value]) -> Result<Value, ()>;
|
|
fn set_handler(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
handler: Box<dyn Fn(&[Value]) -> Value>,
|
|
) -> Result<(), ()>;
|
|
}
|
|
|
|
impl<Item, Value: Default + 'static, Ret: Default> CallbackInfo<Item, Value>
|
|
for FieldOffset<Item, crate::Callback<(), Ret>>
|
|
where
|
|
Value: TryInto<Ret>,
|
|
Ret: TryInto<Value>,
|
|
{
|
|
fn call(&self, item: Pin<&Item>, _args: &[Value]) -> Result<Value, ()> {
|
|
self.apply_pin(item).call(&()).try_into().map_err(|_| ())
|
|
}
|
|
|
|
fn set_handler(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
handler: Box<dyn Fn(&[Value]) -> Value>,
|
|
) -> Result<(), ()> {
|
|
self.apply_pin(item).set_handler(move |()| handler(&[]).try_into().ok().unwrap());
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<Item, Value: Clone + Default + 'static, T: Clone, Ret: Default> CallbackInfo<Item, Value>
|
|
for FieldOffset<Item, crate::Callback<(T,), Ret>>
|
|
where
|
|
Value: TryInto<T>,
|
|
T: TryInto<Value>,
|
|
Value: TryInto<Ret>,
|
|
Ret: TryInto<Value>,
|
|
{
|
|
fn call(&self, item: Pin<&Item>, args: &[Value]) -> Result<Value, ()> {
|
|
let value = args.first().ok_or(())?;
|
|
let value = value.clone().try_into().map_err(|_| ())?;
|
|
self.apply_pin(item).call(&(value,)).try_into().map_err(|_| ())
|
|
}
|
|
|
|
fn set_handler(
|
|
&self,
|
|
item: Pin<&Item>,
|
|
handler: Box<dyn Fn(&[Value]) -> Value>,
|
|
) -> Result<(), ()> {
|
|
self.apply_pin(item).set_handler(move |(val,)| {
|
|
let val: Value = val.clone().try_into().ok().unwrap();
|
|
handler(&[val]).try_into().ok().unwrap()
|
|
});
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub trait FieldInfo<Item, Value> {
|
|
fn set_field(&self, item: &mut Item, value: Value) -> Result<(), ()>;
|
|
}
|
|
|
|
impl<Item, T, Value: 'static> FieldInfo<Item, Value> for FieldOffset<Item, T>
|
|
where
|
|
Value: TryInto<T>,
|
|
T: TryInto<Value>,
|
|
{
|
|
fn set_field(&self, item: &mut Item, value: Value) -> Result<(), ()> {
|
|
*self.apply_mut(item) = value.try_into().map_err(|_| ())?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub trait BuiltinItem: Sized {
|
|
fn name() -> &'static str;
|
|
fn properties<Value: ValueType>() -> Vec<(&'static str, &'static dyn PropertyInfo<Self, Value>)>;
|
|
fn fields<Value: ValueType>() -> Vec<(&'static str, &'static dyn FieldInfo<Self, Value>)>;
|
|
fn callbacks<Value: ValueType>() -> Vec<(&'static str, &'static dyn CallbackInfo<Self, Value>)>;
|
|
}
|
|
|
|
/// Trait implemented by builtin globals
|
|
pub trait BuiltinGlobal: BuiltinItem {
|
|
fn new() -> Pin<Rc<Self>>;
|
|
}
|