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

150 lines
5.2 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
#![cfg(target_has_atomic = "ptr")] // Arc is not available. TODO: implement using RawWaker
#![warn(missing_docs)]
//! This module contains the code that runs futures
use crate::SlintContext;
use crate::api::EventLoopError;
use alloc::boxed::Box;
use alloc::task::Wake;
use alloc::vec::Vec;
use core::future::Future;
use core::ops::DerefMut;
use core::pin::Pin;
use core::task::Poll;
use portable_atomic as atomic;
enum FutureState<T> {
Running(Pin<Box<dyn Future<Output = T>>>),
Finished(Option<T>),
}
struct FutureRunnerInner<T> {
fut: FutureState<T>,
wakers: Vec<core::task::Waker>,
}
struct FutureRunner<T> {
#[cfg(not(feature = "std"))]
inner: core::cell::RefCell<FutureRunnerInner<T>>,
#[cfg(feature = "std")]
inner: std::sync::Mutex<FutureRunnerInner<T>>,
aborted: atomic::AtomicBool,
proxy: Box<dyn crate::platform::EventLoopProxy>,
#[cfg(feature = "std")]
thread: std::thread::ThreadId,
}
impl<T> FutureRunner<T> {
fn inner(&self) -> impl DerefMut<Target = FutureRunnerInner<T>> + '_ {
#[cfg(feature = "std")]
return self.inner.lock().unwrap();
#[cfg(not(feature = "std"))]
return self.inner.borrow_mut();
}
}
// # Safety:
// The Future might not be Send, but we only poll the future from the main thread.
// (We even assert that)
// We may access the finished value from another thread only if T is Send
// (because JoinHandle only implement Send if T:Send)
#[allow(unsafe_code)]
unsafe impl<T> Send for FutureRunner<T> {}
#[allow(unsafe_code)]
unsafe impl<T> Sync for FutureRunner<T> {}
impl<T: 'static> Wake for FutureRunner<T> {
fn wake(self: alloc::sync::Arc<Self>) {
self.clone().proxy.invoke_from_event_loop(Box::new(move || {
#[cfg(feature = "std")]
assert_eq!(self.thread, std::thread::current().id(), "the future was moved to a thread despite we checked it was created in the event loop thread");
let waker = self.clone().into();
let mut inner = self.inner();
let mut cx = core::task::Context::from_waker(&waker);
if let FutureState::Running(fut) = &mut inner.fut {
if self.aborted.load(atomic::Ordering::Relaxed) {
inner.fut = FutureState::Finished(None);
} else {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(val) => {
inner.fut = FutureState::Finished(Some(val));
for w in core::mem::take(&mut inner.wakers) {
w.wake();
}
}
Poll::Pending => {}
}
}
}
}))
// The event loop may be gone during teardown; the future can't progress, so drop the wake.
.ok();
}
}
/// The return value of the `spawn_local()` function
///
/// Can be used to abort the future, or to get the value from a different thread with `.await`
///
/// This trait implements future. Polling it after it finished or aborted may result in a panic.
pub struct JoinHandle<T>(alloc::sync::Arc<FutureRunner<T>>);
impl<T> Future for JoinHandle<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut core::task::Context<'_>) -> Poll<Self::Output> {
let mut inner = self.0.inner();
match &mut inner.fut {
FutureState::Running(_) => {
let waker = cx.waker();
if !inner.wakers.iter().any(|w| w.will_wake(waker)) {
inner.wakers.push(waker.clone());
}
Poll::Pending
}
FutureState::Finished(x) => {
Poll::Ready(x.take().expect("Polling completed or aborted JoinHandle"))
}
}
}
}
impl<T> JoinHandle<T> {
/// If the future hasn't completed yet, this will make the event loop stop polling the corresponding future and it will be dropped
///
/// Once this handle has been aborted, it can no longer be polled
pub fn abort(self) {
self.0.aborted.store(true, atomic::Ordering::Relaxed);
}
/// Checks if the task associated with this `JoinHandle` has finished.
pub fn is_finished(&self) -> bool {
matches!(self.0.inner().fut, FutureState::Finished(_))
}
}
#[cfg(feature = "std")]
#[allow(unsafe_code)]
// Safety: JoinHandle doesn't access the future, only the
unsafe impl<T: Send> Send for JoinHandle<T> {}
/// Implementation for [`SlintContext::spawn_local`]
pub(crate) fn spawn_local_with_ctx<F: Future + 'static>(
ctx: &SlintContext,
fut: F,
) -> Result<JoinHandle<F::Output>, EventLoopError> {
let arc = alloc::sync::Arc::new(FutureRunner {
#[cfg(feature = "std")]
thread: std::thread::current().id(),
inner: FutureRunnerInner { fut: FutureState::Running(Box::pin(fut)), wakers: Vec::new() }
.into(),
aborted: Default::default(),
proxy: ctx.event_loop_proxy().ok_or(EventLoopError::NoEventLoopProvider)?,
});
arc.wake_by_ref();
Ok(JoinHandle(arc))
}