gallery/vendor/i-slint-core/data_transfer/ffi.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

264 lines
8.5 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
#![allow(unsafe_code)]
use alloc::rc::Rc;
use core::ffi::c_void;
use super::DataTransfer;
use crate::SharedString;
#[cfg(feature = "std")]
use crate::SharedVector;
use crate::api::Image;
#[cfg(feature = "std")]
use crate::slice::Slice;
// The unit of a path's native representation, matching C++'s
// `std::filesystem::path::value_type`: UTF-16 code units on Windows, bytes
// elsewhere. Paths cross the FFI as slices of this type so that any valid
// path round-trips losslessly.
#[cfg(all(feature = "std", windows))]
pub type PathValueType = u16;
#[cfg(all(feature = "std", not(windows)))]
pub type PathValueType = u8;
#[cfg(feature = "std")]
fn path_from_units(units: &[PathValueType]) -> std::path::PathBuf {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
std::ffi::OsStr::from_bytes(units).into()
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStringExt;
std::ffi::OsString::from_wide(units).into()
}
#[cfg(not(any(unix, windows)))]
{
alloc::string::String::from_utf8_lossy(units).into_owned().into()
}
}
#[cfg(feature = "std")]
fn path_to_units(path: &std::path::Path) -> SharedVector<PathValueType> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().into()
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
path.as_os_str().encode_wide().collect()
}
#[cfg(not(any(unix, windows)))]
{
path.to_string_lossy().as_bytes().into()
}
}
/// Opaque placeholder used by C++ to reserve storage with the same size and
/// alignment as Rust's `DataTransfer`. The actual `DataTransfer` contains
/// `Option<Rc<...>>` fields whose layout cannot be expressed via cbindgen, so
/// C++ never inspects these fields directly: copy/destruction goes through
/// the `slint_data_transfer_*` FFI functions below, which operate on a real
/// `DataTransfer`.
///
/// The three pointer-sized fields correspond to:
/// - `_rc_inner`: thin `Rc` pointer for `Option<Rc<DataTransferInner>>` (null = `None`)
/// - `_rc_any_0`/`_rc_any_1`: data + vtable pointers of the
/// `Option<Rc<dyn Any>>` user data fat pointer (null data = `None`)
#[repr(C)]
pub struct DataTransferOpaque {
_rc_inner: *mut core::ffi::c_void,
_rc_any_0: *mut core::ffi::c_void,
_rc_any_1: *mut core::ffi::c_void,
}
static_assertions::assert_eq_align!(DataTransferOpaque, DataTransfer);
static_assertions::assert_eq_size!(DataTransferOpaque, DataTransfer);
/// Default-construct a `DataTransfer` in place at `out`.
///
/// # Safety
/// `out` must be valid for writes of `DataTransfer` and must not currently
/// hold an initialized `DataTransfer` (otherwise the previous value is leaked).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_data_transfer_init_default(out: *mut DataTransfer) {
unsafe { core::ptr::write(out, DataTransfer::default()) }
}
/// Drop a `DataTransfer` in place.
///
/// # Safety
/// `d` must point to an initialized `DataTransfer` and must not be used after
/// this call returns.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_data_transfer_drop(d: *mut DataTransfer) {
unsafe { core::ptr::drop_in_place(d) }
}
/// Clone `src` into the uninitialized memory at `out`.
///
/// # Safety
/// `out` must be valid for writes of `DataTransfer` and must not currently
/// hold an initialized `DataTransfer` (otherwise the previous value is leaked).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_data_transfer_clone(out: *mut DataTransfer, src: &DataTransfer) {
unsafe { core::ptr::write(out, src.clone()) }
}
/// Compare two `DataTransfer` values for equality (same semantics as
/// `<DataTransfer as PartialEq>::eq`).
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_eq(a: &DataTransfer, b: &DataTransfer) -> bool {
a == b
}
/// Set the plain text representation of `d` to a clone of `text`.
///
/// An empty `text` clears the previously-set plain text instead of storing it.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_set_plain_text(d: &mut DataTransfer, text: &SharedString) {
d.set_plain_text(text.clone());
}
/// Set the image representation of `d` to a clone of `image`.
///
/// A default-constructed `image` clears the previously-set image instead of storing it.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_set_image(d: &mut DataTransfer, image: &Image) {
d.set_image(image.clone());
}
/// Set the file path list of `d`. Each path is a slice of its native
/// representation units (see `PathValueType`).
///
/// An empty `paths` clears the previously-set file paths instead of storing it.
#[cfg(feature = "std")]
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_set_file_paths(
d: &mut DataTransfer,
paths: Slice<Slice<PathValueType>>,
) {
d.set_file_paths_vec(paths.iter().map(|path| path_from_units(path.as_slice())).collect());
}
/// Returns `true` if `d` advertises a plain text representation.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_has_plain_text(d: &DataTransfer) -> bool {
d.has_plain_text()
}
/// Returns `true` if `d` advertises an image representation.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_has_image(d: &DataTransfer) -> bool {
d.has_image()
}
/// Returns `true` if `d` advertises a list of file paths.
#[cfg(feature = "std")]
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_has_file_paths(d: &DataTransfer) -> bool {
d.has_file_paths()
}
/// Returns `true` if `d` carries no data: no plain text, no image, no file paths,
/// and no user data.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_is_empty(d: &DataTransfer) -> bool {
d.is_empty()
}
/// If `d` has a plain text representation, write a clone of it into `out` and
/// return `true`. Otherwise leave `out` unchanged and return `false`.
///
/// `out` must point to an initialized `SharedString`.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_plain_text(d: &DataTransfer, out: &mut SharedString) -> bool {
match d.plain_text() {
Ok(s) => {
*out = s;
true
}
Err(_) => false,
}
}
/// If `d` has an image representation, write a clone of it into `out` and
/// return `true`. Otherwise leave `out` unchanged and return `false`.
///
/// `out` must point to an initialized `Image`.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_image(d: &DataTransfer, out: &mut Image) -> bool {
match d.image() {
Ok(i) => {
*out = i;
true
}
Err(_) => false,
}
}
/// If `d` has a list of file paths, write them into `out` and return `true`.
/// Otherwise leave `out` unchanged and return `false`. Each path is a vector
/// of its native representation units (see `PathValueType`).
///
/// `out` must point to an initialized `SharedVector<SharedVector<PathValueType>>`.
#[cfg(feature = "std")]
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_file_paths(
d: &DataTransfer,
out: &mut SharedVector<SharedVector<PathValueType>>,
) -> bool {
match d.file_paths() {
Ok(paths) => {
*out = paths.map(path_to_units).collect();
true
}
Err(_) => false,
}
}
/// C++-owned user-data handle stored in a `DataTransfer`.
struct CppUserData {
handle: *mut c_void,
drop_fn: unsafe extern "C" fn(*mut c_void),
}
impl Drop for CppUserData {
fn drop(&mut self) {
unsafe { (self.drop_fn)(self.handle) }
}
}
/// Store `handle` as the user data of `d`. `drop_fn(handle)` runs when `d` is dropped.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_data_transfer_set_user_data(
d: &mut DataTransfer,
handle: *mut c_void,
drop_fn: unsafe extern "C" fn(*mut c_void),
) {
d.set_user_data(Rc::new(CppUserData { handle, drop_fn }));
}
/// Write a borrowed pointer to `d`'s C++ user-data handle into `out_handle`.
/// Returns `false` if `d` has no C++ user data.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_data_transfer_user_data(
d: &DataTransfer,
out_handle: *mut *const c_void,
) -> bool {
let Some(any) = d.user_data() else { return false };
let Some(cpp) = any.downcast_ref::<CppUserData>() else { return false };
unsafe { *out_handle = cpp.handle as *const c_void };
true
}
/// Clear the user data of `d`, if any.
#[unsafe(no_mangle)]
pub extern "C" fn slint_data_transfer_clear_user_data(d: &mut DataTransfer) {
d.user_data = None;
}