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

447 lines
20 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 crate::Property;
use crate::api::PlatformError;
use crate::graphics::Color;
use crate::input::InternalKeyboardModifierState;
use crate::item_tree::{ItemRc, ItemTreeRc};
use crate::items::ColorScheme;
use crate::lengths::LogicalLength;
use crate::platform::{EventLoopProxy, Platform, WindowAdapter, WindowEvent};
use alloc::boxed::Box;
use alloc::rc::Rc;
use core::cell::Cell;
use core::cell::RefCell;
use pin_weak::rc::PinWeak;
/// Type alias for the closure type installed via [`set_window_event_hook`].
/// Exposed so callers (notably tests) can save and restore a previously-installed hook.
pub type WindowEventHook =
Box<dyn Fn(&Rc<dyn WindowAdapter>, &WindowEvent, crate::platform::WindowEventDispatchResult)>;
crate::thread_local! {
pub(crate) static GLOBAL_CONTEXT : once_cell::unsync::OnceCell<SlintContext>
= const { once_cell::unsync::OnceCell::new() }
}
#[pin_project::pin_project]
pub(crate) struct SlintContextInner {
platform: Box<dyn Platform>,
pub(crate) window_count: core::cell::RefCell<isize>,
/// Read by all translations, and marked dirty when the language changes so every
/// translated string re-translates. The value is the currently selected language
/// when bundling translations.
#[pin]
pub(crate) translations_dirty: Property<usize>,
pub(crate) translations_bundle:
core::cell::RefCell<Option<alloc::vec::Vec<i_slint_common::TranslationsBundled>>>,
#[cfg(feature = "tr")]
external_translator: core::cell::RefCell<Option<Box<dyn tr::Translator>>>,
#[pin]
pub(crate) locale_decimal_separator: Property<char>,
/// Process-wide color scheme. Backends' system-theme observers write here; bindings
/// read from it through [`SlintContext::color_scheme`]. Window-less components like
/// `SystemTrayIcon` rely on this as their default source.
#[pin]
pub(crate) color_scheme: Property<ColorScheme>,
/// Process-wide system accent color. Backends' system-theme observers write here;
/// bindings read from it through [`SlintContext::accent_color`]. Defaults to a
/// transparent color when the platform doesn't expose one.
#[pin]
pub(crate) accent_color: Property<Color>,
/// Process-wide default font size as reported by the platform (e.g. iOS Dynamic
/// Type). Backends write here; `WindowItem::resolved_default_font_size` consults it
/// before falling back to `textlayout::DEFAULT_FONT_SIZE`. `None` when the backend
/// doesn't report one.
#[pin]
pub(crate) platform_default_font_size: Property<Option<LogicalLength>>,
pub(crate) window_shown_hook:
core::cell::RefCell<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>>,
pub(crate) window_event_hook: core::cell::RefCell<Option<WindowEventHook>>,
pub(crate) log_message_handler: RefCell<Option<crate::debug_log::LogMessageHandler>>,
#[cfg(all(unix, not(target_os = "macos")))]
xdg_app_id: core::cell::RefCell<Option<crate::SharedString>>,
#[cfg(feature = "shared-parley")]
pub(crate) font_context: core::cell::RefCell<crate::textlayout::sharedparley::FontContext>,
#[cfg(feature = "shared-swash")]
pub(crate) swash_scale_context: core::cell::RefCell<swash::scale::ScaleContext>,
pub(crate) modifiers: Cell<InternalKeyboardModifierState>,
/// The timers registered on this context. Shared, so that `Timer` handles can hold a
/// `Weak` to the list they registered in without knowing which context owns it.
pub(crate) timers: crate::timers::TimerListRc,
}
/// This context is meant to hold the state and the backend.
/// Currently it is not possible to have several platform at the same time in one process, but in the future it might be.
/// See issue #4294
#[derive(Clone)]
pub struct SlintContext(pub(crate) core::pin::Pin<Rc<SlintContextInner>>);
impl SlintContext {
/// Create a new context with a given platform.
///
/// If this thread has no context yet, the new one becomes it — first come, first
/// served. That is what the ambient APIs resolve to: [`crate::timers::Timer`],
/// `spawn_local`, `quit_event_loop` and friends. Contexts created afterwards are
/// perfectly usable, but are not the thread's current one, so code holding such a
/// context has to be explicit about it (e.g. [`Self::new_timer`]).
pub fn new(platform: Box<dyn Platform + 'static>) -> Self {
#[cfg(feature = "shared-parley")]
let collection = i_slint_common::sharedfontique::create_collection(true);
let this = Self(Rc::pin(SlintContextInner {
platform,
window_count: 0.into(),
translations_dirty: Property::new_named(0, "SlintContext::translations"),
translations_bundle: Default::default(),
#[cfg(feature = "tr")]
external_translator: Default::default(),
locale_decimal_separator: Property::new_named(
i_slint_common::DEFAULT_DECIMAL_SEPARATOR,
"SlintContext::locale_decimal_separator",
),
color_scheme: Property::new_named(ColorScheme::Unknown, "SlintContext::color_scheme"),
accent_color: Property::new_named(Color::default(), "SlintContext::accent_color"),
platform_default_font_size: Property::new_named(
None,
"SlintContext::platform_default_font_size",
),
window_shown_hook: Default::default(),
window_event_hook: Default::default(),
log_message_handler: Default::default(),
#[cfg(all(unix, not(target_os = "macos")))]
xdg_app_id: Default::default(),
#[cfg(feature = "shared-parley")]
font_context: {
let font_context = parley::FontContext {
collection: collection.inner,
source_cache: collection.source_cache,
};
core::cell::RefCell::new(crate::textlayout::sharedparley::FontContext::new(
font_context,
))
},
#[cfg(feature = "shared-swash")]
swash_scale_context: core::cell::RefCell::new(swash::scale::ScaleContext::new()),
modifiers: Cell::new(Default::default()),
// Timers started before this thread had a context registered in the pending
// list; take it over so those timers keep working. It is the very list they
// hold a `Weak` to, so nothing needs fixing up.
timers: crate::timers::take_pending_timers(),
}));
// The list's deadlines are measured on this context's clock from now on. Done after
// construction because it needs a handle to the context that owns it.
crate::timers::set_owning_context(&this.0.timers, &this);
// Claim this thread's context slot if it is still free, so that the ambient APIs
// resolve here rather than to a list nothing drives. Fails harmlessly when the
// thread already has a context: that one stays current.
GLOBAL_CONTEXT.with(|slot| {
let _ = slot.set(this.clone());
});
// Every context tells its platform which context it belongs to, not just the one
// that becomes this thread's global: a platform is owned by exactly one context, and
// a backend driving a context needs to be able to find it.
this.platform().bind_context(this.downgrade(), crate::InternalToken);
this
}
/// Return a reference to the platform abstraction
pub fn platform(&self) -> &dyn Platform {
&*self.0.platform
}
/// Return a reference to the font context
#[cfg(feature = "shared-parley")]
pub fn font_context(
&self,
) -> &core::cell::RefCell<crate::textlayout::sharedparley::FontContext> {
&self.0.font_context
}
/// Return a reference to the swash scale context
#[cfg(feature = "shared-swash")]
pub fn swash_scale_context(&self) -> &core::cell::RefCell<swash::scale::ScaleContext> {
&self.0.swash_scale_context
}
/// Return an event proxy
// FIXME: Make EvenLoopProxy cloneable, and maybe wrap in a struct
pub fn event_loop_proxy(&self) -> Option<Box<dyn EventLoopProxy>> {
self.0.platform.new_event_loop_proxy()
}
#[cfg(target_has_atomic = "ptr")]
/// Context specific version of `slint::spawn_local`
pub fn spawn_local<F: core::future::Future + 'static>(
&self,
fut: F,
) -> Result<crate::future::JoinHandle<F::Output>, crate::api::EventLoopError> {
crate::future::spawn_local_with_ctx(self, fut)
}
pub fn run_event_loop(&self) -> Result<(), PlatformError> {
self.0.platform.run_event_loop()
}
/// Creates a [`Timer`](crate::timers::Timer) that registers on this context rather than
/// on whichever one is current when it is started.
///
/// For the context that a thread runs its event loop on this is the same as
/// `Timer::default()`, and the event loop activates the timer as usual. A context that
/// isn't the current one has no event loop driving it, so its owner is responsible for
/// calling [`Self::maybe_activate_timers`].
pub fn new_timer(&self) -> crate::timers::Timer {
crate::timers::Timer::with_list(&self.0.timers)
}
/// Runs `callback` once, `duration` from now, on this context.
///
/// The context-bound counterpart of [`Timer::single_shot`](crate::timers::Timer::single_shot),
/// which registers on whichever context is current instead.
pub fn single_shot(&self, duration: core::time::Duration, callback: impl FnOnce() + 'static) {
crate::timers::single_shot_on(&self.0.timers, duration, callback);
}
/// Advances this context's animations and timers to its own clock, and runs any change
/// handlers that fall out of it.
///
/// This is what an event loop driving this context should call at the top of each
/// iteration. [`crate::platform::update_timers_and_animations`] is the same thing for
/// whichever context is this thread's global one.
pub fn update_timers_and_animations(&self) {
let now = crate::animations::Instant::now(self);
crate::animations::update_animations(now);
self.maybe_activate_timers(now);
crate::properties::ChangeTracker::run_change_handlers();
}
/// How long this context can go to sleep before its next timer is due, or `None` when it
/// has no active timer.
///
/// The deadline and the clock it is measured against both come from this context, so
/// they cannot disagree.
pub fn duration_until_next_timer_update(&self) -> Option<core::time::Duration> {
let timeout = self.next_timer_timeout()?;
let now = crate::animations::Instant::now(self);
Some(core::time::Duration::from_millis(timeout.0.saturating_sub(now.0)))
}
/// Fires the callbacks of this context's timers that have expired by `now`, and returns
/// whether any of them was activated.
pub fn maybe_activate_timers(&self, now: crate::animations::Instant) -> bool {
crate::timers::TimerList::activate_expired(&self.0.timers, now)
}
/// Returns when this context's next timer is due, or `None` if it has no active timer.
pub fn next_timer_timeout(&self) -> Option<crate::animations::Instant> {
self.0.timers.borrow().first_timeout()
}
/// Returns the effective color scheme for the given component root, or the
/// process-wide scheme when `root` is `None`. A `SystemTrayIcon`-rooted
/// component resolves against the tray's own scheme first, falling back to
/// the process-wide value when the tray reports `Unknown`. Reads register a
/// property dependency, so bindings re-evaluate when the platform reports a
/// system-theme change.
pub fn color_scheme(&self, root: Option<&ItemTreeRc>) -> ColorScheme {
if let Some(root) = root {
let root_item = ItemRc::new_root(root.clone());
if let Some(tray) = root_item.downcast::<crate::items::SystemTrayIcon>() {
let scheme = tray.as_pin_ref().color_scheme();
if scheme != ColorScheme::Unknown {
return scheme;
}
}
}
self.0.as_ref().project_ref().color_scheme.get()
}
/// Backend-side write path for the process-wide color scheme. Called by each
/// platform's system-theme observer; `Property::set` short-circuits no-op writes.
pub fn set_color_scheme(&self, scheme: ColorScheme) {
self.0.as_ref().project_ref().color_scheme.set(scheme);
}
/// Returns the process-wide system accent color. Reads register a property dependency,
/// so bindings re-evaluate when the platform reports an accent-color change.
pub fn accent_color(&self) -> Color {
self.0.as_ref().project_ref().accent_color.get()
}
/// Backend-side write path for the process-wide accent color. Called by each
/// platform's system-theme observer; `Property::set` short-circuits no-op writes.
pub fn set_accent_color(&self, color: Color) {
self.0.as_ref().project_ref().accent_color.set(color);
}
/// Returns the platform-reported default font size, or `None` if the backend doesn't
/// report one. Reads register a property dependency, so bindings re-evaluate when the
/// platform reports a change (e.g. the user adjusts the system text size).
pub fn platform_default_font_size(&self) -> Option<LogicalLength> {
self.0.as_ref().project_ref().platform_default_font_size.get()
}
/// Backend-side write path for the platform-reported default font size. Called by
/// backends that track the system setting; `Property::set` short-circuits no-op writes.
pub fn set_platform_default_font_size(&self, size: Option<LogicalLength>) {
self.0.as_ref().project_ref().platform_default_font_size.set(size);
}
#[doc(hidden)]
pub fn dispatch_log_message(&self, message: crate::debug_log::LogMessage<'_>) {
if let Some(handler) = self.0.log_message_handler.borrow().as_ref() {
handler(message);
} else {
self.0.platform.debug_log(message.message_arguments());
}
}
#[doc(hidden)]
pub fn set_log_message_handler(
&self,
handler: Option<crate::debug_log::LogMessageHandler>,
) -> Option<crate::debug_log::LogMessageHandler> {
let mut slot = self.0.log_message_handler.borrow_mut();
core::mem::replace(&mut *slot, handler)
}
/// Add one to the counter of "things keeping the event loop alive".
/// Visible windows and visible system tray icons are the canonical
/// callers; they pair with [`Self::release_keepalive`].
pub(crate) fn acquire_keepalive(&self) {
*self.0.window_count.borrow_mut() += 1;
}
/// Subtract one from the keepalive counter and quit the event loop if
/// nothing is keeping it alive anymore. Mirrors the post-decrement quit
/// that [`crate::window::WindowInner::hide`] used to do inline.
pub(crate) fn release_keepalive(&self) {
let mut count = self.0.window_count.borrow_mut();
*count -= 1;
if *count <= 0 {
drop(count);
let _ = self.event_loop_proxy().and_then(|p| p.quit_event_loop().ok());
}
}
pub fn set_xdg_app_id(&self, _app_id: crate::SharedString) {
#[cfg(all(unix, not(target_os = "macos")))]
{
self.0.xdg_app_id.replace(Some(_app_id));
}
}
#[cfg(all(unix, not(target_os = "macos")))]
pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
self.0.xdg_app_id.borrow().clone()
}
#[cfg(not(all(unix, not(target_os = "macos"))))]
pub fn xdg_app_id(&self) -> Option<crate::SharedString> {
None
}
/// Returns the locale's decimal separator, falling back to `translations::DEFAULT_SEPARATOR`.
pub fn locale_decimal_separator(&self) -> char {
self.0.as_ref().project_ref().locale_decimal_separator.get()
}
/// Override the locale used for decimal separator detection (testing only).
#[cfg(feature = "std")]
pub fn set_locale(&self, locale: &str) {
self.0
.as_ref()
.project_ref()
.locale_decimal_separator
.set(i_slint_common::decimal_separator_for_locale(locale));
}
#[cfg(feature = "tr")]
pub fn set_external_translator(&self, translator: Option<Box<dyn tr::Translator>>) {
*self.0.external_translator.borrow_mut() = translator;
self.0.as_ref().project_ref().translations_dirty.mark_dirty();
}
#[cfg(feature = "tr")]
pub fn external_translator(&self) -> Option<core::cell::Ref<'_, Box<dyn tr::Translator>>> {
core::cell::Ref::filter_map(self.0.external_translator.borrow(), |maybe_translator| {
maybe_translator.as_ref()
})
.ok()
}
/// Returns a weak handle to this context, suitable for stashing in places that must
/// not keep the context alive (e.g. a backend that's owned by the context itself).
pub fn downgrade(&self) -> SlintContextWeak {
SlintContextWeak(PinWeak::downgrade(self.0.clone()))
}
}
/// Weak handle to a [`SlintContext`]. Backends that opt into
/// [`crate::platform::Platform::bind_context`] receive one of these right after
/// `set_platform` so they can spawn futures and write process-wide state without
/// holding the context strongly.
#[derive(Clone)]
pub struct SlintContextWeak(PinWeak<SlintContextInner>);
impl SlintContextWeak {
/// Attempts to upgrade to a strong [`SlintContext`].
pub fn upgrade(&self) -> Option<SlintContext> {
self.0.upgrade().map(SlintContext)
}
}
/// Internal function to access the context.
/// The factory function is called if the platform abstraction is not yet
/// initialized, and should be given by the platform_selector
pub fn with_global_context<R>(
factory: impl FnOnce() -> Result<Box<dyn Platform + 'static>, PlatformError>,
f: impl FnOnce(&SlintContext) -> R,
) -> Result<R, PlatformError> {
GLOBAL_CONTEXT.with(|p| match p.get() {
Some(ctx) => Ok(f(ctx)),
None => {
if crate::platform::with_event_loop_proxy(|proxy| proxy.is_some()) {
return Err(PlatformError::SetPlatformError(
crate::platform::SetPlatformError::AlreadySet,
));
}
crate::platform::set_platform(factory()?).map_err(PlatformError::SetPlatformError)?;
Ok(f(p.get().unwrap()))
}
})
}
/// Internal function to set a hook that's invoked whenever a slint::Window is shown. This
/// is used by the system testing module. Returns a previously set hook, if any.
pub fn set_window_shown_hook(
hook: Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>,
) -> Result<Option<Box<dyn FnMut(&Rc<dyn crate::platform::WindowAdapter>)>>, PlatformError> {
GLOBAL_CONTEXT.with(|p| match p.get() {
Some(ctx) => Ok(ctx.0.window_shown_hook.replace(hook)),
None => Err(PlatformError::NoPlatform),
})
}
/// Internal function to set a hook that's invoked after a window event was dispatched.
/// This is used by the system testing module. Returns a previously set hook, if any.
pub fn set_window_event_hook(
hook: Option<WindowEventHook>,
) -> Result<Option<WindowEventHook>, PlatformError> {
GLOBAL_CONTEXT.with(|p| match p.get() {
Some(ctx) => {
let mut slot = ctx.0.window_event_hook.try_borrow_mut().map_err(|_| {
PlatformError::Other(alloc::string::String::from("event hook is currently in use"))
})?;
Ok(core::mem::replace(&mut *slot, hook))
}
None => Err(PlatformError::NoPlatform),
})
}