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).
664 lines
23 KiB
Rust
664 lines
23 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
|
|
|
|
use super::*;
|
|
use crate::graphics::{Brush, Color};
|
|
use crate::items::PropertyAnimation;
|
|
use core::ffi::c_void;
|
|
|
|
#[repr(C)]
|
|
/// Has the same layout as PropertyHandle
|
|
pub struct PropertyHandleOpaque(PropertyHandle);
|
|
|
|
/// Initialize the first pointer of the Property. Does not initialize the content.
|
|
/// `out` is assumed to be uninitialized
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_init(out: *mut PropertyHandleOpaque) {
|
|
unsafe { core::ptr::write(out, PropertyHandleOpaque(PropertyHandle::default())) };
|
|
}
|
|
|
|
/// To be called before accessing the value
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_update(handle: &PropertyHandleOpaque, val: *mut c_void) {
|
|
unsafe {
|
|
let handle = Pin::new_unchecked(&handle.0);
|
|
handle.update(val);
|
|
handle.register_as_dependency_to_current_binding();
|
|
}
|
|
}
|
|
|
|
/// Register this property as a dependency of the current tracking scope
|
|
/// without evaluating any binding.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_register_as_dependency(handle: &PropertyHandleOpaque) {
|
|
unsafe {
|
|
let handle = Pin::new_unchecked(&handle.0);
|
|
handle.register_as_dependency_to_current_binding();
|
|
}
|
|
}
|
|
|
|
/// Mark the fact that the property was changed and that its binding need to be removed, and
|
|
/// the dependencies marked dirty.
|
|
/// To be called after the `value` has been changed
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_changed(
|
|
handle: &PropertyHandleOpaque,
|
|
value: *const c_void,
|
|
) {
|
|
unsafe {
|
|
if !handle.0.access(|b| {
|
|
b.is_some_and(|b| (b.vtable.intercept_set)(&*b as *const BindingHolder, value))
|
|
}) {
|
|
handle.0.remove_binding();
|
|
}
|
|
handle.0.mark_dirty();
|
|
}
|
|
}
|
|
|
|
fn make_c_function_binding(
|
|
binding: extern "C" fn(*mut c_void, *mut c_void),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
intercept_set: Option<
|
|
extern "C" fn(user_data: *mut c_void, pointer_to_value: *const c_void) -> bool,
|
|
>,
|
|
intercept_set_binding: Option<
|
|
extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool,
|
|
>,
|
|
) -> impl BindingCallable<c_void> {
|
|
struct CFunctionBinding<T> {
|
|
binding_function: extern "C" fn(*mut c_void, *mut T),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
intercept_set:
|
|
Option<extern "C" fn(user_data: *mut c_void, pointer_to_value: *const T) -> bool>,
|
|
intercept_set_binding:
|
|
Option<extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool>,
|
|
}
|
|
|
|
impl<T> Drop for CFunctionBinding<T> {
|
|
fn drop(&mut self) {
|
|
if let Some(x) = self.drop_user_data {
|
|
x(self.user_data)
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe impl<T> BindingCallable<T> for CFunctionBinding<T> {
|
|
fn evaluate(self: Pin<&Self>, value: &mut T) -> BindingResult {
|
|
(self.binding_function)(self.user_data, value as *mut T);
|
|
BindingResult::KeepBinding
|
|
}
|
|
fn intercept_set(self: Pin<&Self>, value: &T) -> bool {
|
|
match self.intercept_set {
|
|
None => false,
|
|
Some(intercept_set) => intercept_set(self.user_data, value as *const T),
|
|
}
|
|
}
|
|
unsafe fn intercept_set_binding(self: Pin<&Self>, new_binding: *mut BindingHolder) -> bool {
|
|
match self.intercept_set_binding {
|
|
None => false,
|
|
Some(intercept_set_b) => intercept_set_b(self.user_data, new_binding.cast()),
|
|
}
|
|
}
|
|
}
|
|
|
|
CFunctionBinding {
|
|
binding_function: binding,
|
|
user_data,
|
|
drop_user_data,
|
|
intercept_set,
|
|
intercept_set_binding,
|
|
}
|
|
}
|
|
|
|
/// Set a binding
|
|
///
|
|
/// The current implementation will do usually two memory allocation:
|
|
/// 1. the allocation from the calling code to allocate user_data
|
|
/// 2. the box allocation within this binding
|
|
///
|
|
/// It might be possible to reduce that by passing something with a
|
|
/// vtable, so there is the need for less memory allocation.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_binding(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: extern "C" fn(user_data: *mut c_void, pointer_to_value: *mut c_void),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
intercept_set: Option<
|
|
extern "C" fn(user_data: *mut c_void, pointer_to_value: *const c_void) -> bool,
|
|
>,
|
|
intercept_set_binding: Option<
|
|
extern "C" fn(user_data: *mut c_void, new_binding: *mut c_void) -> bool,
|
|
>,
|
|
) {
|
|
let binding = make_c_function_binding(
|
|
binding,
|
|
user_data,
|
|
drop_user_data,
|
|
intercept_set,
|
|
intercept_set_binding,
|
|
);
|
|
unsafe { handle.0.set_binding(binding) };
|
|
}
|
|
|
|
/// Set a binding using an already allocated building holder
|
|
///
|
|
/// (take ownership of the binding)
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_binding_internal(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: *mut c_void,
|
|
) {
|
|
handle.0.set_binding_impl(binding.cast());
|
|
}
|
|
|
|
/// Delete a binding. The pointer must be a pointer to a binding (so a BindingHolder)
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_delete_binding(binding: *mut c_void) {
|
|
let b = binding as *mut BindingHolder;
|
|
unsafe { ((*b).vtable.drop)(b) };
|
|
}
|
|
|
|
/// Evaluate a raw binding
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_evaluate_binding(binding: *mut c_void, value: *mut c_void) {
|
|
let b = binding as *mut BindingHolder;
|
|
unsafe { ((*b).vtable.evaluate)(b, value) };
|
|
}
|
|
|
|
/// Call `intercept_set` on a raw binding, returning whether the binding accepted the write
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_intercept_set_binding(
|
|
binding: *mut c_void,
|
|
value: *const c_void,
|
|
) -> bool {
|
|
let b = binding as *mut BindingHolder;
|
|
unsafe { ((*b).vtable.intercept_set)(b, value) }
|
|
}
|
|
|
|
/// Returns whether the property behind this handle is marked as dirty
|
|
#[unsafe(no_mangle)]
|
|
pub extern "C" fn slint_property_is_dirty(handle: &PropertyHandleOpaque) -> bool {
|
|
handle.0.access(|binding| binding.is_some_and(|b| b.dirty.get()))
|
|
}
|
|
|
|
/// Marks the property as dirty and notifies dependencies.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "C" fn slint_property_mark_dirty(handle: &PropertyHandleOpaque) {
|
|
handle.0.mark_dirty()
|
|
}
|
|
|
|
/// Returns true if a binding is currently being evaluated, so that property
|
|
/// accesses register dependencies.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "C" fn slint_property_is_currently_tracking() -> bool {
|
|
crate::properties::is_currently_tracking()
|
|
}
|
|
|
|
/// Marks the property as dirty and notifies dependencies.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "C" fn slint_property_set_constant(handle: &PropertyHandleOpaque) {
|
|
handle.0.set_constant()
|
|
}
|
|
|
|
/// Destroy handle
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_drop(handle: *mut PropertyHandleOpaque) {
|
|
unsafe {
|
|
core::ptr::drop_in_place(handle);
|
|
}
|
|
}
|
|
|
|
fn c_set_animated_value<T: InterpolatedPropertyValue + Clone>(
|
|
handle: &PropertyHandleOpaque,
|
|
from: T,
|
|
to: T,
|
|
animation_data: &PropertyAnimation,
|
|
) {
|
|
let d = RefCell::new(properties_animations::PropertyValueAnimationData::new(
|
|
from,
|
|
Some(to),
|
|
animation_data.clone(),
|
|
));
|
|
// Safety: The BindingCallable is for type T
|
|
unsafe {
|
|
handle.0.set_binding(move |val: &mut T| {
|
|
let (value, finished) = d.borrow_mut().compute_interpolated_value();
|
|
*val = value;
|
|
if finished {
|
|
BindingResult::RemoveBinding
|
|
} else {
|
|
crate::animations::CURRENT_ANIMATION_DRIVER
|
|
.with(|driver| driver.set_has_active_animations());
|
|
BindingResult::KeepBinding
|
|
}
|
|
})
|
|
};
|
|
handle.0.mark_dirty();
|
|
}
|
|
|
|
/// Internal function to set up a property animation to the specified target value for an integer property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_value_int(
|
|
handle: &PropertyHandleOpaque,
|
|
from: i32,
|
|
to: i32,
|
|
animation_data: &PropertyAnimation,
|
|
) {
|
|
c_set_animated_value(handle, from, to, animation_data)
|
|
}
|
|
|
|
/// Internal function to set up a property animation to the specified target value for a float property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_value_float(
|
|
handle: &PropertyHandleOpaque,
|
|
from: f32,
|
|
to: f32,
|
|
animation_data: &PropertyAnimation,
|
|
) {
|
|
c_set_animated_value(handle, from, to, animation_data)
|
|
}
|
|
|
|
/// Internal function to set up a property animation to the specified target value for a color property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_value_color(
|
|
handle: &PropertyHandleOpaque,
|
|
from: Color,
|
|
to: Color,
|
|
animation_data: &PropertyAnimation,
|
|
) {
|
|
c_set_animated_value(handle, from, to, animation_data);
|
|
}
|
|
|
|
/// Internal function to set up a property animation to the specified target value for a brush property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_value_brush(
|
|
handle: &PropertyHandleOpaque,
|
|
from: &Brush,
|
|
to: &Brush,
|
|
animation_data: &PropertyAnimation,
|
|
) {
|
|
c_set_animated_value(handle, from.clone(), to.clone(), animation_data);
|
|
}
|
|
|
|
unsafe fn c_set_animated_binding<T: InterpolatedPropertyValue + Clone>(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: extern "C" fn(*mut c_void, *mut T),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
transition_data: extern "C" fn(
|
|
user_data: *mut c_void,
|
|
start_instant: &mut *mut u64,
|
|
) -> PropertyAnimation,
|
|
) {
|
|
unsafe {
|
|
let binding = core::mem::transmute::<
|
|
extern "C" fn(*mut c_void, *mut T),
|
|
extern "C" fn(*mut c_void, *mut c_void),
|
|
>(binding);
|
|
let original_binding = PropertyHandle {
|
|
handle: Cell::new(
|
|
(alloc_binding_holder(make_c_function_binding(
|
|
binding,
|
|
user_data,
|
|
drop_user_data,
|
|
None,
|
|
None,
|
|
)) as *mut ())
|
|
.map_addr(|a| a | 0b10),
|
|
),
|
|
};
|
|
let animation_data = RefCell::new(properties_animations::PropertyValueAnimationData::new(
|
|
T::default(),
|
|
None,
|
|
PropertyAnimation::default(),
|
|
));
|
|
|
|
handle.0.set_binding(properties_animations::AnimatedBindingCallable::<T, _> {
|
|
original_binding,
|
|
state: Cell::new(properties_animations::AnimatedBindingState::NotAnimating),
|
|
animation_data,
|
|
compute_animation_details: move || -> properties_animations::AnimationDetail {
|
|
// The transition_data function receives a *mut *mut u64 pointer for the
|
|
// timestamp.
|
|
// If the function sets the pointer to nullptr, it doesn't provide a start_time.
|
|
// Otherwise, we assume it has written a value to the start_instant.
|
|
// This basically models a `&mut Option<u64>`, which is then converted to an
|
|
// `Option<Instant>`
|
|
let mut start_instant = 0u64;
|
|
let mut start_instant_ref = &mut start_instant as *mut u64;
|
|
let anim = transition_data(user_data, &mut start_instant_ref);
|
|
let start_instant = if start_instant_ref.is_null() {
|
|
None
|
|
} else {
|
|
Some(crate::animations::Instant(start_instant))
|
|
};
|
|
(anim, start_instant)
|
|
},
|
|
dirty_time: Cell::new(crate::animations::current_tick()),
|
|
carried_velocity: Cell::new(0.0),
|
|
});
|
|
handle.0.mark_dirty();
|
|
}
|
|
}
|
|
|
|
/// Internal function to set up a property animation between values produced by the specified binding for an integer property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_binding_int(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: extern "C" fn(*mut c_void, *mut core::ffi::c_int),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
transition_data: extern "C" fn(
|
|
user_data: *mut c_void,
|
|
start_instant: &mut *mut u64,
|
|
) -> PropertyAnimation,
|
|
) {
|
|
unsafe {
|
|
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
|
}
|
|
}
|
|
|
|
/// Internal function to set up a property animation between values produced by the specified binding for a float property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_binding_float(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: extern "C" fn(*mut c_void, *mut f32),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
transition_data: extern "C" fn(
|
|
user_data: *mut c_void,
|
|
start_instant: &mut *mut u64,
|
|
) -> PropertyAnimation,
|
|
) {
|
|
unsafe {
|
|
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
|
}
|
|
}
|
|
|
|
/// Internal function to set up a property animation between values produced by the specified binding for a color property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_binding_color(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: extern "C" fn(*mut c_void, *mut Color),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
transition_data: extern "C" fn(
|
|
user_data: *mut c_void,
|
|
start_instant: &mut *mut u64,
|
|
) -> PropertyAnimation,
|
|
) {
|
|
unsafe {
|
|
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
|
}
|
|
}
|
|
|
|
/// Internal function to set up a property animation between values produced by the specified binding for a brush property.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_animated_binding_brush(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: extern "C" fn(*mut c_void, *mut Brush),
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
transition_data: extern "C" fn(
|
|
user_data: *mut c_void,
|
|
start_instant: &mut *mut u64,
|
|
) -> PropertyAnimation,
|
|
) {
|
|
unsafe {
|
|
c_set_animated_binding(handle, binding, user_data, drop_user_data, transition_data);
|
|
}
|
|
}
|
|
|
|
/// Internal function to set up a state binding on a Property<StateInfo>.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_set_state_binding(
|
|
handle: &PropertyHandleOpaque,
|
|
binding: extern "C" fn(*mut c_void) -> i32,
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
) {
|
|
struct CStateBinding {
|
|
binding: extern "C" fn(*mut c_void) -> i32,
|
|
user_data: *mut c_void,
|
|
drop_user_data: Option<extern "C" fn(*mut c_void)>,
|
|
}
|
|
|
|
impl Drop for CStateBinding {
|
|
fn drop(&mut self) {
|
|
if let Some(x) = self.drop_user_data {
|
|
x(self.user_data)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CStateBinding {
|
|
fn call(&self) -> i32 {
|
|
(self.binding)(self.user_data)
|
|
}
|
|
}
|
|
|
|
let c_state_binding = CStateBinding { binding, user_data, drop_user_data };
|
|
let bind_callable = StateInfoBinding {
|
|
dirty_time: Cell::new(None),
|
|
binding: move || c_state_binding.call(),
|
|
_phantom: core::marker::PhantomData::<fn() -> StateInfo>,
|
|
};
|
|
unsafe { handle.0.set_binding(bind_callable) }
|
|
}
|
|
|
|
#[repr(C)]
|
|
/// Opaque type representing the PropertyTracker
|
|
pub struct PropertyTrackerOpaque {
|
|
dependencies: usize,
|
|
dep_nodes: usize,
|
|
vtable: usize,
|
|
dirty: bool,
|
|
}
|
|
|
|
static_assertions::assert_eq_align!(PropertyTrackerOpaque, PropertyTracker);
|
|
static_assertions::assert_eq_size!(PropertyTrackerOpaque, PropertyTracker);
|
|
|
|
/// Initialize the first pointer of the PropertyTracker.
|
|
/// `out` is assumed to be uninitialized
|
|
/// slint_property_tracker_drop need to be called after that
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_tracker_init(out: *mut PropertyTrackerOpaque) {
|
|
unsafe {
|
|
core::ptr::write(out as *mut PropertyTracker, PropertyTracker::default());
|
|
}
|
|
}
|
|
|
|
/// Call the callback with the user data. Any properties access within the callback will be registered.
|
|
/// Any currently evaluated bindings or property trackers will be notified if accessed properties are changed.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_tracker_evaluate(
|
|
handle: *const PropertyTrackerOpaque,
|
|
callback: extern "C" fn(user_data: *mut c_void),
|
|
user_data: *mut c_void,
|
|
) {
|
|
unsafe { Pin::new_unchecked(&*(handle as *const PropertyTracker)) }
|
|
.evaluate(|| callback(user_data))
|
|
}
|
|
|
|
/// Call the callback with the user data. Any properties access within the callback will be registered.
|
|
/// Any currently evaluated bindings or property trackers will be not notified if accessed properties are changed.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_tracker_evaluate_as_dependency_root(
|
|
handle: *const PropertyTrackerOpaque,
|
|
callback: extern "C" fn(user_data: *mut c_void),
|
|
user_data: *mut c_void,
|
|
) {
|
|
unsafe { Pin::new_unchecked(&*(handle as *const PropertyTracker)) }
|
|
.evaluate_as_dependency_root(|| callback(user_data))
|
|
}
|
|
/// Query if the property tracker is dirty
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_tracker_is_dirty(
|
|
handle: *const PropertyTrackerOpaque,
|
|
) -> bool {
|
|
unsafe { (*(handle as *const PropertyTracker)).is_dirty() }
|
|
}
|
|
|
|
/// Destroy handle
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_property_tracker_drop(handle: *mut PropertyTrackerOpaque) {
|
|
unsafe { core::ptr::drop_in_place(handle as *mut PropertyTracker) };
|
|
}
|
|
|
|
#[repr(C)]
|
|
/// Opaque type representing the ChangeTracker
|
|
pub struct ChangeTrackerOpaque {
|
|
_inner: *const c_void,
|
|
}
|
|
|
|
static_assertions::assert_eq_align!(ChangeTrackerOpaque, ChangeTracker);
|
|
static_assertions::assert_eq_size!(ChangeTrackerOpaque, ChangeTracker);
|
|
|
|
/// Construct a ChangeTracker
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_change_tracker_construct(ct: *mut ChangeTrackerOpaque) {
|
|
unsafe { core::ptr::write(ct as *mut ChangeTracker, ChangeTracker::default()) };
|
|
}
|
|
|
|
/// Drop a ChangeTracker
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_change_tracker_drop(ct: *mut ChangeTrackerOpaque) {
|
|
unsafe { core::ptr::drop_in_place(ct as *mut ChangeTracker) };
|
|
}
|
|
|
|
/// initialize the change tracker
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_change_tracker_init(
|
|
ct: *const ChangeTrackerOpaque,
|
|
user_data: *mut c_void,
|
|
drop_user_data: extern "C" fn(user_data: *mut c_void),
|
|
eval_fn: extern "C" fn(user_data: *mut c_void) -> bool,
|
|
notify_fn: extern "C" fn(user_data: *mut c_void),
|
|
) {
|
|
let ct = unsafe { &*ct.cast::<ChangeTracker>() };
|
|
#[allow(non_camel_case_types)]
|
|
struct C_ChangeTrackerInner {
|
|
user_data: *mut c_void,
|
|
drop_user_data: extern "C" fn(user_data: *mut c_void),
|
|
eval_fn: extern "C" fn(user_data: *mut c_void) -> bool,
|
|
notify_fn: extern "C" fn(user_data: *mut c_void),
|
|
}
|
|
impl Drop for C_ChangeTrackerInner {
|
|
fn drop(&mut self) {
|
|
(self.drop_user_data)(self.user_data);
|
|
}
|
|
}
|
|
|
|
unsafe fn drop(_self: *mut BindingHolder) {
|
|
core::mem::drop(unsafe {
|
|
Box::from_raw(_self as *mut BindingHolder<C_ChangeTrackerInner>)
|
|
});
|
|
}
|
|
|
|
unsafe fn evaluate(_self: *const BindingHolder, _value: *mut c_void) -> BindingResult {
|
|
let _self_raw = _self;
|
|
let _self = _self as *mut BindingHolder<C_ChangeTrackerInner>;
|
|
let inner = unsafe { core::ptr::addr_of_mut!((*_self).binding).as_mut().unwrap() };
|
|
unsafe { *(*core::ptr::addr_of!((*_self).dep_nodes)).get() = Default::default() };
|
|
let notify = super::current_binding_storage::set(Some(_self_raw), || {
|
|
(inner.eval_fn)(inner.user_data)
|
|
});
|
|
if notify {
|
|
(inner.notify_fn)(inner.user_data);
|
|
}
|
|
BindingResult::KeepBinding
|
|
}
|
|
|
|
const VT: &BindingVTable = &BindingVTable {
|
|
drop,
|
|
evaluate,
|
|
mark_dirty: ChangeTracker::mark_dirty,
|
|
intercept_set: |_, _| false,
|
|
intercept_set_binding: |_, _| false,
|
|
velocity: |_| None,
|
|
};
|
|
|
|
ct.clear();
|
|
|
|
let inner = C_ChangeTrackerInner { user_data, drop_user_data, eval_fn, notify_fn };
|
|
|
|
let holder = BindingHolder {
|
|
dependencies: Cell::new(core::ptr::null_mut()),
|
|
dep_nodes: Default::default(),
|
|
vtable: VT,
|
|
dirty: Cell::new(false),
|
|
is_two_way_binding: false,
|
|
pinned: PhantomPinned,
|
|
binding: inner,
|
|
#[cfg(slint_debug_property)]
|
|
debug_name: "<ChangeTracker>".into(),
|
|
};
|
|
|
|
let raw = Box::into_raw(Box::new(holder));
|
|
unsafe { ct.set_internal(raw as *mut BindingHolder) };
|
|
|
|
let inner = unsafe { core::ptr::addr_of_mut!((*raw).binding).as_mut().unwrap() };
|
|
super::current_binding_storage::set(Some(raw as *const BindingHolder), || {
|
|
(inner.eval_fn)(inner.user_data)
|
|
});
|
|
}
|
|
|
|
/// return the current animation tick for the `animation-tick` function
|
|
#[unsafe(no_mangle)]
|
|
pub extern "C" fn slint_animation_tick() -> u64 {
|
|
crate::animations::animation_tick()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod ffi_change_tracker_leak_test {
|
|
use super::*;
|
|
use crate::properties::ChangeTracker;
|
|
use alloc::boxed::Box;
|
|
use core::cell::Cell;
|
|
use core::pin::Pin;
|
|
|
|
// What the generated C++ stores for a `changed` handler: the watched
|
|
// property and the last seen value.
|
|
struct EvalState {
|
|
prop: *const Property<i32>,
|
|
last: Cell<i32>,
|
|
}
|
|
|
|
extern "C" fn eval_fn(user_data: *mut c_void) -> bool {
|
|
let st = unsafe { &*(user_data as *const EvalState) };
|
|
let v = unsafe { Pin::new_unchecked(&*st.prop) }.get();
|
|
let changed = v != st.last.get();
|
|
st.last.set(v);
|
|
changed
|
|
}
|
|
extern "C" fn notify_fn(_user_data: *mut c_void) {}
|
|
extern "C" fn drop_fn(_user_data: *mut c_void) {}
|
|
|
|
// The dependency nodes must not accumulate across re-evaluations.
|
|
#[test]
|
|
fn ffi_change_tracker_does_not_leak_dep_nodes() {
|
|
let prop = Box::pin(Property::new(0));
|
|
let state = EvalState { prop: &*prop as *const _, last: Cell::new(0) };
|
|
let ct = ChangeTracker::default();
|
|
unsafe {
|
|
slint_change_tracker_init(
|
|
&ct as *const ChangeTracker as *const ChangeTrackerOpaque,
|
|
&state as *const EvalState as *mut c_void,
|
|
drop_fn,
|
|
eval_fn,
|
|
notify_fn,
|
|
);
|
|
}
|
|
assert_eq!(ct.test_dep_node_count(), 1);
|
|
|
|
for i in 1..=200 {
|
|
prop.as_ref().set(i);
|
|
ChangeTracker::run_change_handlers();
|
|
assert_eq!(ct.test_dep_node_count(), 1, "leaked a DependencyNode at iteration {i}");
|
|
}
|
|
}
|
|
}
|