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).
480 lines
16 KiB
Rust
480 lines
16 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
|
|
|
|
#![warn(missing_docs)]
|
|
//! The animation system
|
|
|
|
use alloc::boxed::Box;
|
|
use core::cell::Cell;
|
|
#[cfg(not(feature = "std"))]
|
|
use num_traits::Float;
|
|
|
|
pub(crate) mod simulations;
|
|
|
|
mod cubic_bezier {
|
|
//! This is a copy from lyon_algorithms::geom::cubic_bezier implementation
|
|
//! (from lyon_algorithms 0.17)
|
|
type S = f32;
|
|
use euclid::default::Point2D as Point;
|
|
#[allow(unused)]
|
|
use num_traits::Float;
|
|
trait Scalar {
|
|
const ONE: f32 = 1.;
|
|
const THREE: f32 = 3.;
|
|
const HALF: f32 = 0.5;
|
|
const SIX: f32 = 6.;
|
|
const NINE: f32 = 9.;
|
|
fn value(v: f32) -> f32 {
|
|
v
|
|
}
|
|
}
|
|
impl Scalar for f32 {}
|
|
pub struct CubicBezierSegment {
|
|
pub from: Point<S>,
|
|
pub ctrl1: Point<S>,
|
|
pub ctrl2: Point<S>,
|
|
pub to: Point<S>,
|
|
}
|
|
|
|
impl CubicBezierSegment {
|
|
/// Sample the x coordinate of the curve at t (expecting t between 0 and 1).
|
|
pub fn x(&self, t: S) -> S {
|
|
let t2 = t * t;
|
|
let t3 = t2 * t;
|
|
let one_t = S::ONE - t;
|
|
let one_t2 = one_t * one_t;
|
|
let one_t3 = one_t2 * one_t;
|
|
|
|
self.from.x * one_t3
|
|
+ self.ctrl1.x * S::THREE * one_t2 * t
|
|
+ self.ctrl2.x * S::THREE * one_t * t2
|
|
+ self.to.x * t3
|
|
}
|
|
|
|
/// Sample the y coordinate of the curve at t (expecting t between 0 and 1).
|
|
pub fn y(&self, t: S) -> S {
|
|
let t2 = t * t;
|
|
let t3 = t2 * t;
|
|
let one_t = S::ONE - t;
|
|
let one_t2 = one_t * one_t;
|
|
let one_t3 = one_t2 * one_t;
|
|
|
|
self.from.y * one_t3
|
|
+ self.ctrl1.y * S::THREE * one_t2 * t
|
|
+ self.ctrl2.y * S::THREE * one_t * t2
|
|
+ self.to.y * t3
|
|
}
|
|
|
|
#[inline]
|
|
fn derivative_coefficients(&self, t: S) -> (S, S, S, S) {
|
|
let t2 = t * t;
|
|
(
|
|
-S::THREE * t2 + S::SIX * t - S::THREE,
|
|
S::NINE * t2 - S::value(12.0) * t + S::THREE,
|
|
-S::NINE * t2 + S::SIX * t,
|
|
S::THREE * t2,
|
|
)
|
|
}
|
|
|
|
/// Sample the x coordinate of the curve's derivative at t (expecting t between 0 and 1).
|
|
pub fn dx(&self, t: S) -> S {
|
|
let (c0, c1, c2, c3) = self.derivative_coefficients(t);
|
|
self.from.x * c0 + self.ctrl1.x * c1 + self.ctrl2.x * c2 + self.to.x * c3
|
|
}
|
|
}
|
|
|
|
impl CubicBezierSegment {
|
|
// This is actually in the Monotonic<CubicBezierSegment<S>> impl
|
|
pub fn solve_t_for_x(&self, x: S, t_range: core::ops::Range<S>, tolerance: S) -> S {
|
|
debug_assert!(t_range.start <= t_range.end);
|
|
let from = self.x(t_range.start);
|
|
let to = self.x(t_range.end);
|
|
if x <= from {
|
|
return t_range.start;
|
|
}
|
|
if x >= to {
|
|
return t_range.end;
|
|
}
|
|
|
|
// Newton's method.
|
|
let mut t = (x - from) / (to - from);
|
|
let mut degenerate = false;
|
|
for _ in 0..8 {
|
|
let x2 = self.x(t);
|
|
let dx = self.dx(t);
|
|
|
|
if dx <= S::EPSILON {
|
|
degenerate = true;
|
|
break;
|
|
}
|
|
|
|
let step = (x2 - x) / dx;
|
|
t -= step;
|
|
|
|
if S::abs(step) <= tolerance {
|
|
return t.max(t_range.start).min(t_range.end);
|
|
}
|
|
}
|
|
|
|
if !degenerate {
|
|
return t.max(t_range.start).min(t_range.end);
|
|
}
|
|
|
|
// Fall back to binary search.
|
|
let mut min = t_range.start;
|
|
let mut max = t_range.end;
|
|
let mut t = S::HALF;
|
|
|
|
while min < max {
|
|
let x2 = self.x(t);
|
|
|
|
if S::abs(x2 - x) < tolerance {
|
|
return t;
|
|
}
|
|
|
|
if x > x2 {
|
|
min = t;
|
|
} else {
|
|
max = t;
|
|
}
|
|
|
|
t = (max - min) * S::HALF + min;
|
|
}
|
|
|
|
t
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The representation of an easing curve, for animations
|
|
#[repr(C, u32)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
|
pub enum EasingCurve {
|
|
/// The linear curve
|
|
#[default]
|
|
Linear,
|
|
/// A Cubic bezier curve, with its 4 parameters
|
|
CubicBezier([f32; 4]),
|
|
/// Easing curve as defined at: <https://easings.net/#easeInElastic>
|
|
EaseInElastic,
|
|
/// Easing curve as defined at: <https://easings.net/#easeOutElastic>
|
|
EaseOutElastic,
|
|
/// Easing curve as defined at: <https://easings.net/#easeInOutElastic>
|
|
EaseInOutElastic,
|
|
/// Easing curve as defined at: <https://easings.net/#easeInBounce>
|
|
EaseInBounce,
|
|
/// Easing curve as defined at: <https://easings.net/#easeOutBounce>
|
|
EaseOutBounce,
|
|
/// Easing curve as defined at: <https://easings.net/#easeInOutBounce>
|
|
EaseInOutBounce,
|
|
/// A spring animation, configured via `PropertyAnimation`'s `duration`, and the passed in
|
|
/// `bounce`
|
|
Spring(f32),
|
|
// Custom(Box<dyn Fn(f32) -> f32>),
|
|
}
|
|
|
|
/// Represent an instant, in milliseconds since the AnimationDriver's initial_instant
|
|
#[repr(transparent)]
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Ord, PartialOrd, Eq)]
|
|
pub struct Instant(pub u64);
|
|
|
|
impl core::ops::Sub<Instant> for Instant {
|
|
type Output = core::time::Duration;
|
|
fn sub(self, other: Self) -> core::time::Duration {
|
|
core::time::Duration::from_millis(self.0 - other.0)
|
|
}
|
|
}
|
|
|
|
impl core::ops::Sub<core::time::Duration> for Instant {
|
|
type Output = Instant;
|
|
fn sub(self, other: core::time::Duration) -> Instant {
|
|
Self(self.0 - other.as_millis() as u64)
|
|
}
|
|
}
|
|
|
|
impl core::ops::Add<core::time::Duration> for Instant {
|
|
type Output = Instant;
|
|
fn add(self, other: core::time::Duration) -> Instant {
|
|
Self(self.0 + other.as_millis() as u64)
|
|
}
|
|
}
|
|
|
|
impl core::ops::AddAssign<core::time::Duration> for Instant {
|
|
fn add_assign(&mut self, other: core::time::Duration) {
|
|
self.0 += other.as_millis() as u64;
|
|
}
|
|
}
|
|
|
|
impl core::ops::SubAssign<core::time::Duration> for Instant {
|
|
fn sub_assign(&mut self, other: core::time::Duration) {
|
|
self.0 -= other.as_millis() as u64;
|
|
}
|
|
}
|
|
|
|
impl Instant {
|
|
/// Returns the amount of time elapsed since an other instant.
|
|
///
|
|
/// Equivalent to `self - earlier`
|
|
pub fn duration_since(self, earlier: Instant) -> core::time::Duration {
|
|
self - earlier
|
|
}
|
|
|
|
/// Wrapper around [`std::time::Instant::now()`] that delegates to the backend
|
|
/// and allows working in no_std environments.
|
|
///
|
|
/// Takes the context rather than reaching for an ambient one because the origin is the
|
|
/// platform's start time: instants from different contexts are not comparable, so the
|
|
/// caller has to say which clock it means.
|
|
pub fn now(ctx: &crate::SlintContext) -> Self {
|
|
Self(ctx.platform().duration_since_start().as_millis() as u64)
|
|
}
|
|
|
|
/// Return the number of milliseconds this `Instant` is after the backend has started
|
|
pub fn as_millis(&self) -> u64 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
/// The AnimationDriver
|
|
pub struct AnimationDriver {
|
|
/// Indicate whether there are any active animations that require a future call to update_animations.
|
|
active_animations: Cell<bool>,
|
|
global_instant: core::pin::Pin<Box<crate::Property<Instant>>>,
|
|
}
|
|
|
|
impl Default for AnimationDriver {
|
|
fn default() -> Self {
|
|
AnimationDriver {
|
|
active_animations: Cell::default(),
|
|
global_instant: Box::pin(crate::Property::new_named(
|
|
Instant::default(),
|
|
"i_slint_core::AnimationDriver::global_instant",
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AnimationDriver {
|
|
/// Iterates through all animations based on the new time tick and updates their state. This should be called by
|
|
/// the windowing system driver for every frame.
|
|
pub fn update_animations(&self, new_tick: Instant) {
|
|
let current_tick = self.global_instant.as_ref().get_untracked();
|
|
assert!(current_tick <= new_tick, "The platform's clock is not monotonic!");
|
|
if current_tick != new_tick {
|
|
self.active_animations.set(false);
|
|
self.global_instant.as_ref().set(new_tick);
|
|
}
|
|
}
|
|
|
|
/// Returns true if there are any active or ready animations. This is used by the windowing system to determine
|
|
/// if a new animation frame is required or not. Returns false otherwise.
|
|
pub fn has_active_animations(&self) -> bool {
|
|
self.active_animations.get()
|
|
}
|
|
|
|
/// Tell the driver that there are active animations
|
|
pub fn set_has_active_animations(&self) {
|
|
self.active_animations.set(true);
|
|
}
|
|
/// The current instant that is to be used for animation
|
|
/// using this function register the current binding as a dependency
|
|
pub fn current_tick(&self) -> Instant {
|
|
self.global_instant.as_ref().get()
|
|
}
|
|
}
|
|
|
|
crate::thread_local!(
|
|
/// This is the default instance of the animation driver that's used to advance all property animations
|
|
/// at the same time.
|
|
pub static CURRENT_ANIMATION_DRIVER : AnimationDriver = AnimationDriver::default()
|
|
);
|
|
|
|
/// The current instant that is to be used for animation
|
|
/// using this function register the current binding as a dependency
|
|
pub fn current_tick() -> Instant {
|
|
CURRENT_ANIMATION_DRIVER.with(|driver| driver.current_tick())
|
|
}
|
|
|
|
/// Same as [`current_tick`], but also register that one should be running animation
|
|
/// on next frame
|
|
pub fn animation_tick() -> u64 {
|
|
CURRENT_ANIMATION_DRIVER.with(|driver| {
|
|
driver.set_has_active_animations();
|
|
driver.current_tick().0
|
|
})
|
|
}
|
|
|
|
fn ease_out_bounce_curve(value: f32) -> f32 {
|
|
const N1: f32 = 7.5625;
|
|
const D1: f32 = 2.75;
|
|
|
|
if value < 1.0 / D1 {
|
|
N1 * value * value
|
|
} else if value < 2.0 / D1 {
|
|
let value = value - (1.5 / D1);
|
|
N1 * value * value + 0.75
|
|
} else if value < 2.5 / D1 {
|
|
let value = value - (2.25 / D1);
|
|
N1 * value * value + 0.9375
|
|
} else {
|
|
let value = value - (2.625 / D1);
|
|
N1 * value * value + 0.984375
|
|
}
|
|
}
|
|
|
|
/// How close to the target position/velocity a `SpringSimulation` must get before it is
|
|
/// considered settled and snaps to rest. This is the "settling duration" and is distinct from the
|
|
/// user-facing `duration` that fixes the natural frequency
|
|
const SPRING_SETTLE_POSITION_EPSILON: f32 = 0.001;
|
|
const SPRING_SETTLE_VELOCITY_EPSILON: f32 = 0.05;
|
|
|
|
/// Evaluates a mass/stiffness/damping spring at `elapsed_secs`, returning `(progress, settled)`.
|
|
pub fn spring_settle_progress(
|
|
regime: &simulations::spring::SpringRegime,
|
|
elapsed_secs: f32,
|
|
) -> (f32, bool) {
|
|
let (rel_pos, rel_vel) = regime.evaluate(elapsed_secs);
|
|
let settled = rel_pos.abs() < SPRING_SETTLE_POSITION_EPSILON
|
|
&& rel_vel.abs() < SPRING_SETTLE_VELOCITY_EPSILON;
|
|
(1.0 + rel_pos, settled)
|
|
}
|
|
|
|
/// The damping ratio (zeta) required for a spring to settle within 9x `duration` is a fixed
|
|
/// value, since zeta is proportional to bounce and duration.
|
|
///
|
|
/// The spring runs at its literal bounce for every iteration except the last, where it gets
|
|
/// clamped to this value if it hasn't settled by then -- hence needing to settle within 9x
|
|
/// (not some other multiple of) `duration`. Found empirically: the actual value is ~0.8803, but
|
|
/// 0.87 is used to leave some floating-point leeway for comparisons.
|
|
const SPRING_SETTLE_ZETA: f32 = 1.0 - 0.87;
|
|
|
|
/// Set the spring to settle within 10x duration
|
|
pub fn spring_settle_within(
|
|
regime: &simulations::spring::SpringRegime,
|
|
elapsed_secs: f32,
|
|
w_n: f32,
|
|
) -> simulations::spring::SpringRegime {
|
|
let (rel_pos, rel_vel) = regime.evaluate(elapsed_secs);
|
|
let zeta = regime.zeta().max(SPRING_SETTLE_ZETA);
|
|
simulations::spring::SpringRegime::new(rel_pos, rel_vel, w_n, zeta)
|
|
}
|
|
|
|
/// map a value between 0 and 1 to another value between 0 and 1 according to the curve
|
|
pub fn easing_curve(curve: &EasingCurve, value: f32) -> f32 {
|
|
match curve {
|
|
EasingCurve::Linear => value,
|
|
EasingCurve::CubicBezier([a, b, c, d]) => {
|
|
if !(0.0..=1.0).contains(a) && !(0.0..=1.0).contains(c) {
|
|
return value;
|
|
};
|
|
let curve = cubic_bezier::CubicBezierSegment {
|
|
from: (0., 0.).into(),
|
|
ctrl1: (*a, *b).into(),
|
|
ctrl2: (*c, *d).into(),
|
|
to: (1., 1.).into(),
|
|
};
|
|
curve.y(curve.solve_t_for_x(value, 0.0..1.0, 0.01))
|
|
}
|
|
EasingCurve::EaseInElastic => {
|
|
const C4: f32 = 2.0 * core::f32::consts::PI / 3.0;
|
|
|
|
if value == 0.0 {
|
|
0.0
|
|
} else if value == 1.0 {
|
|
1.0
|
|
} else {
|
|
-f32::powf(2.0, 10.0 * value - 10.0) * f32::sin((value * 10.0 - 10.75) * C4)
|
|
}
|
|
}
|
|
EasingCurve::EaseOutElastic => {
|
|
let c4 = (2.0 * core::f32::consts::PI) / 3.0;
|
|
|
|
if value == 0.0 {
|
|
0.0
|
|
} else if value == 1.0 {
|
|
1.0
|
|
} else {
|
|
2.0f32.powf(-10.0 * value) * ((value * 10.0 - 0.75) * c4).sin() + 1.0
|
|
}
|
|
}
|
|
EasingCurve::EaseInOutElastic => {
|
|
const C5: f32 = 2.0 * core::f32::consts::PI / 4.5;
|
|
|
|
if value == 0.0 {
|
|
0.0
|
|
} else if value == 1.0 {
|
|
1.0
|
|
} else if value < 0.5 {
|
|
-(f32::powf(2.0, 20.0 * value - 10.0) * f32::sin((20.0 * value - 11.125) * C5))
|
|
/ 2.0
|
|
} else {
|
|
(f32::powf(2.0, -20.0 * value + 10.0) * f32::sin((20.0 * value - 11.125) * C5))
|
|
/ 2.0
|
|
+ 1.0
|
|
}
|
|
}
|
|
EasingCurve::EaseInBounce => 1.0 - ease_out_bounce_curve(1.0 - value),
|
|
EasingCurve::EaseOutBounce => ease_out_bounce_curve(value),
|
|
EasingCurve::EaseInOutBounce => {
|
|
if value < 0.5 {
|
|
(1.0 - ease_out_bounce_curve(1.0 - 2.0 * value)) / 2.0
|
|
} else {
|
|
(1.0 + ease_out_bounce_curve(2.0 * value - 1.0)) / 2.0
|
|
}
|
|
}
|
|
EasingCurve::Spring(_) => {
|
|
panic!("Springs are handled separately");
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
#[test]
|
|
fn easing_test() {
|
|
fn test_curve(name: &str, curve: &EasingCurve) {
|
|
let mut img = image::ImageBuffer::new(500, 500);
|
|
let white = image::Rgba([255 as u8, 255 as u8, 255 as u8, 255 as u8]);
|
|
|
|
for x in 0..img.width() {
|
|
let t = (x as f32) / (img.width() as f32);
|
|
let y = easing_curve(curve, t);
|
|
let y = (y * (img.height() as f32)) as u32;
|
|
let y = y.min(img.height() - 1);
|
|
*img.get_pixel_mut(x, img.height() - 1 - y) = white;
|
|
}
|
|
|
|
img.save(
|
|
std::path::PathBuf::from(std::env::var_os("HOME").unwrap())
|
|
.join(format!("{}.png", name)),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
test_curve("linear", &EasingCurve::Linear);
|
|
test_curve("linear2", &EasingCurve::CubicBezier([0.0, 0.0, 1.0, 1.0]));
|
|
test_curve("ease", &EasingCurve::CubicBezier([0.25, 0.1, 0.25, 1.0]));
|
|
test_curve("ease_in", &EasingCurve::CubicBezier([0.42, 0.0, 1.0, 1.0]));
|
|
test_curve("ease_in_out", &EasingCurve::CubicBezier([0.42, 0.0, 0.58, 1.0]));
|
|
test_curve("ease_out", &EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]));
|
|
}
|
|
*/
|
|
|
|
/// Update the global animation time to `now`.
|
|
///
|
|
/// The driver is per-thread while `now` comes from whichever context is driving it, so a
|
|
/// thread running several contexts with different clock origins would see the tick jump.
|
|
/// Per-context animation drivers would mean reaching a context from every binding
|
|
/// evaluation, which is a much larger change.
|
|
pub fn update_animations(now: Instant) {
|
|
CURRENT_ANIMATION_DRIVER.with(|driver| {
|
|
#[allow(unused_mut)]
|
|
let mut duration = now.0;
|
|
#[cfg(feature = "std")]
|
|
if let Ok(val) = std::env::var("SLINT_SLOW_ANIMATIONS") {
|
|
let factor = val.parse().unwrap_or(2).max(1);
|
|
duration /= factor;
|
|
};
|
|
driver.update_animations(Instant(duration))
|
|
});
|
|
}
|