gallery/vendor/i-slint-core/properties/erased_bindings.rs
Greg Pomerantz 2f4768f354 Fix 120Hz scroll jank end-to-end: render-ahead present + full-cache eviction off the UI thread
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).
2026-10-30 02:49:23 -04:00

178 lines
5.8 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
//! Install property bindings and callback handlers on a component without
//! monomorphizing the machinery per component type.
//!
//! The generated code of every component would otherwise instantiate its own
//! copy of the binding holder machinery for each property type it uses. The
//! functions here keep the component behind an [`ErasedWeakFn`], which pairs a
//! type-erased weak reference with the binding function so that everything past
//! the `#[inline]` shims is monomorphized per property type only, not per
//! component. The erasure and its safety live in the `vtable` crate; this module
//! is entirely safe.
//!
//! The binding functions all take an extra `&Arg` (`&()` for those without a
//! meaningful argument), so that erasing them never changes the function's arity.
use super::Property;
use crate::callbacks::Callback;
use core::pin::Pin;
use vtable::{ErasedWeakFn, VRcMapped, VTableMetaDropInPlace};
/// Sets the binding of `property` to evaluate `binding` on the component
/// `self_rc`, or to produce the default value once the component is gone.
#[inline]
pub fn set_property_binding_erased<
T: Clone + Default + 'static,
VT: VTableMetaDropInPlace + 'static,
X,
>(
property: Pin<&Property<T>>,
self_rc: &VRcMapped<VT, X>,
binding: fn(Pin<&X>, &()) -> T,
) {
set_property_binding_impl(property, VRcMapped::downgrade_erased_fn(self_rc, binding))
}
fn set_property_binding_impl<T: Clone + Default + 'static, VT: VTableMetaDropInPlace + 'static>(
property: Pin<&Property<T>>,
binding: ErasedWeakFn<VT, T>,
) {
property.set_binding(move || binding.upgrade_and_call(&()).unwrap_or_default())
}
/// Like [`set_property_binding_erased`], for an animated binding.
/// The component must outlive the binding.
#[inline]
pub fn set_animated_property_binding_erased<
T: Clone + super::InterpolatedPropertyValue + 'static,
VT: VTableMetaDropInPlace + 'static,
X,
>(
property: Pin<&Property<T>>,
self_rc: &VRcMapped<VT, X>,
binding: fn(Pin<&X>, &()) -> T,
compute_animation_details: fn(
Pin<&X>,
&(),
) -> (
crate::items::PropertyAnimation,
Option<crate::animations::Instant>,
),
) {
set_animated_property_binding_impl(
property,
VRcMapped::downgrade_erased_fn(self_rc, binding),
VRcMapped::downgrade_erased_fn(self_rc, compute_animation_details),
)
}
fn set_animated_property_binding_impl<
T: Clone + super::InterpolatedPropertyValue + 'static,
VT: VTableMetaDropInPlace + 'static,
>(
property: Pin<&Property<T>>,
binding: ErasedWeakFn<VT, T>,
compute_animation_details: ErasedWeakFn<
VT,
(crate::items::PropertyAnimation, Option<crate::animations::Instant>),
>,
) {
property.set_animated_binding(
move || binding.upgrade_and_call(&()).expect("binding evaluated on dropped component"),
move || {
compute_animation_details
.upgrade_and_call(&())
.expect("binding evaluated on dropped component")
},
)
}
/// Like [`set_property_binding_erased`], for a state binding.
/// The component must outlive the binding.
#[inline]
pub fn set_property_state_binding_erased<VT: VTableMetaDropInPlace + 'static, X>(
property: Pin<&Property<super::StateInfo>>,
self_rc: &VRcMapped<VT, X>,
binding: fn(Pin<&X>, &()) -> i32,
) {
set_property_state_binding_impl(property, VRcMapped::downgrade_erased_fn(self_rc, binding))
}
fn set_property_state_binding_impl<VT: VTableMetaDropInPlace + 'static>(
property: Pin<&Property<super::StateInfo>>,
binding: ErasedWeakFn<VT, i32>,
) {
super::set_state_binding(property, move || {
binding.upgrade_and_call(&()).expect("binding evaluated on dropped component")
})
}
/// Initialize `change_tracker` to evaluate `eval` on the component `self_rc`
/// and call `notify` when the result changes. The component must outlive the
/// change tracker.
#[inline]
pub fn change_tracker_init_erased<
T: Default + PartialEq + 'static,
VT: VTableMetaDropInPlace + 'static,
X,
>(
change_tracker: &super::ChangeTracker,
self_rc: &VRcMapped<VT, X>,
eval: fn(Pin<&X>, &()) -> T,
notify: fn(Pin<&X>, &T),
) {
change_tracker_init_impl(
change_tracker,
VRcMapped::downgrade_erased_fn(self_rc, eval),
VRcMapped::downgrade_erased_fn(self_rc, notify),
)
}
fn change_tracker_init_impl<
T: Default + PartialEq + 'static,
VT: VTableMetaDropInPlace + 'static,
>(
change_tracker: &super::ChangeTracker,
eval: ErasedWeakFn<VT, T>,
notify: ErasedWeakFn<VT, (), T>,
) {
change_tracker.init(
(eval, notify),
|(eval, _)| eval.upgrade_and_call(&()).expect("change tracker on dropped component"),
|(_, notify), value| {
notify.upgrade_and_call(value).expect("change tracker on dropped component");
},
)
}
/// Sets the handler of `callback` to evaluate `handler` on the component
/// `self_rc`. The component must outlive the callback handler.
#[inline]
pub fn set_callback_handler_erased<
Arg: ?Sized + 'static,
Ret: Default + 'static,
VT: VTableMetaDropInPlace + 'static,
X,
>(
callback: Pin<&Callback<Arg, Ret>>,
self_rc: &VRcMapped<VT, X>,
handler: fn(Pin<&X>, &Arg) -> Ret,
) {
set_callback_handler_impl(callback, VRcMapped::downgrade_erased_fn(self_rc, handler))
}
fn set_callback_handler_impl<
Arg: ?Sized + 'static,
Ret: Default + 'static,
VT: VTableMetaDropInPlace + 'static,
>(
callback: Pin<&Callback<Arg, Ret>>,
handler: ErasedWeakFn<VT, Ret, Arg>,
) {
callback.set_handler(move |arg| {
handler.upgrade_and_call(arg).expect("callback invoked on a dropped component")
})
}