Residual scroll jank at 120 Hz (suspected Slint event-loop bug) needs per-frame + per-stage timing the logcat ring buffer can't hold. New cargo feature `perf` (default OFF — all sites are cfg-gated, zero overhead when off): - FR: per-frame app-side render cost + frame gap, from the rendering notifier (BeforeRendering/AfterRendering). - STG: event-loop stage costs (wake/timers/events/poll/lastrender) from timestamps instrumented into the pinned vendor copy of i-slint-backend-android-activity (stg.rs; Cargo.toml now depends on it directly + optional under `perf`). - DECODE: per-thumbnail-job cost on the workers. - All lines mirrored to /sdcard/Download/.gallery_perf.log (logcat rotates too fast at ~120 lines/s). Format + reading guide: docs/BUGS/scroll-jank-120hz.md.
278 lines
11 KiB
Rust
278 lines
11 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
|
|
|
|
// cSpell: ignore androidwindowadapter javahelper
|
|
#![doc = include_str!("README.md")]
|
|
#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
|
|
#![cfg_attr(not(target_os = "android"), allow(rustdoc::broken_intra_doc_links))]
|
|
#![cfg(target_os = "android")]
|
|
#![cfg_attr(slint_nightly_test, feature(non_exhaustive_omitted_patterns_lint))]
|
|
#![cfg_attr(slint_nightly_test, warn(non_exhaustive_omitted_patterns))]
|
|
|
|
mod androidwindowadapter;
|
|
mod javahelper;
|
|
mod vsync;
|
|
|
|
pub mod stg;
|
|
|
|
#[cfg(all(not(feature = "aa-06"), feature = "aa-05"))]
|
|
pub use android_activity_05 as android_activity;
|
|
#[cfg(feature = "aa-06")]
|
|
pub use android_activity_06 as android_activity;
|
|
|
|
#[cfg(all(not(feature = "aa-06"), feature = "aa-05"))]
|
|
use ndk_08 as ndk;
|
|
#[cfg(feature = "aa-06")]
|
|
use ndk_09 as ndk;
|
|
|
|
pub use android_activity::AndroidApp;
|
|
use android_activity::PollEvent;
|
|
use androidwindowadapter::AndroidWindowAdapter;
|
|
use core::ops::ControlFlow;
|
|
use core::time::Duration;
|
|
use i_slint_core::api::{EventLoopError, PlatformError};
|
|
use i_slint_core::platform::{Clipboard, WindowAdapter};
|
|
use i_slint_renderer_skia::SkiaRendererExt;
|
|
use std::cell::RefCell;
|
|
use std::rc::{Rc, Weak};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
thread_local! {
|
|
static CURRENT_WINDOW: RefCell<Weak<AndroidWindowAdapter>> = RefCell::new(Default::default());
|
|
}
|
|
|
|
pub struct AndroidPlatform {
|
|
app: AndroidApp,
|
|
window: Rc<AndroidWindowAdapter>,
|
|
event_listener: Option<Box<dyn Fn(&PollEvent<'_>)>>,
|
|
context: core::cell::OnceCell<i_slint_core::SlintContextWeak>,
|
|
}
|
|
|
|
impl AndroidPlatform {
|
|
/// Instantiate a new Android backend given the [`android_activity::AndroidApp`]
|
|
///
|
|
/// Pass the returned value to [`slint::platform::set_platform()`](`i_slint_core::platform::set_platform()`)
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// #[cfg(target_os = "android")]
|
|
/// #[unsafe(no_mangle)]
|
|
/// fn android_main(app: i_slint_backend_android_activity::AndroidApp) {
|
|
/// slint::platform::set_platform(Box::new(
|
|
/// i_slint_backend_android_activity::AndroidPlatform::new(app),
|
|
/// ))
|
|
/// .unwrap();
|
|
/// // ... your slint application ...
|
|
/// }
|
|
/// ```
|
|
pub fn new(app: AndroidApp) -> Self {
|
|
let window = AndroidWindowAdapter::new(app.clone());
|
|
CURRENT_WINDOW.set(Rc::downgrade(&window));
|
|
Self { app, window, event_listener: None, context: Default::default() }
|
|
}
|
|
|
|
/// Instantiate a new Android backend given the [`android_activity::AndroidApp`]
|
|
/// and a function to process the events.
|
|
///
|
|
/// This is the same as [`AndroidPlatform::new()`], but it allow you to get notified
|
|
/// of events.
|
|
///
|
|
/// Pass the returned value to [`slint::platform::set_platform()`](`i_slint_core::platform::set_platform()`)
|
|
///
|
|
/// # Example
|
|
/// ```
|
|
/// #[cfg(target_os = "android")]
|
|
/// #[unsafe(no_mangle)]
|
|
/// fn android_main(app: i_slint_backend_android_activity::AndroidApp) {
|
|
/// slint::platform::set_platform(Box::new(
|
|
/// i_slint_backend_android_activity::AndroidPlatform::new_with_event_listener(
|
|
/// app,
|
|
/// |event| { eprintln!("got event {event:?}") }
|
|
/// ),
|
|
/// ))
|
|
/// .unwrap();
|
|
/// // ... your slint application ...
|
|
/// }
|
|
/// ```
|
|
pub fn new_with_event_listener(
|
|
app: AndroidApp,
|
|
listener: impl Fn(&PollEvent<'_>) + 'static,
|
|
) -> Self {
|
|
let mut this = Self::new(app);
|
|
this.event_listener = Some(Box::new(listener));
|
|
this
|
|
}
|
|
}
|
|
|
|
impl i_slint_core::platform::Platform for AndroidPlatform {
|
|
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
|
|
Ok(self.window.clone())
|
|
}
|
|
fn run_event_loop(&self) -> Result<(), PlatformError> {
|
|
let ctx = self
|
|
.context
|
|
.get()
|
|
.and_then(|ctx| ctx.upgrade())
|
|
.expect("the event loop runs inside the context that owns this platform");
|
|
let vsync = vsync::VsyncDriver::new(self.app.create_waker());
|
|
loop {
|
|
let mut timeout = ctx.duration_until_next_timer_update();
|
|
// While animating, the vsync thread wakes this loop at each display refresh
|
|
// so rendering tracks the refresh rate instead of a fixed poll timeout.
|
|
let animating = self.window.window.has_active_animations();
|
|
vsync.set_animating(animating);
|
|
if animating && !vsync.is_driving() {
|
|
// The vsync thread is not driving frames (still starting, or unavailable);
|
|
// fall back to a periodic wakeup so animations still advance.
|
|
let frame_duration = Duration::from_millis(10);
|
|
timeout = Some(timeout.map_or(frame_duration, |x| x.min(frame_duration)));
|
|
}
|
|
// `request_redraw()` only raises the flag, so a request issued outside the
|
|
// poll would otherwise wait here for an unrelated wakeup.
|
|
if self.window.pending_redraw.get() {
|
|
timeout = Some(Duration::ZERO);
|
|
}
|
|
// STG (jank-hunt instrumentation): reset per-iteration marks.
|
|
{
|
|
let pr = stg::S_RENDER_START.load(std::sync::atomic::Ordering::Relaxed);
|
|
let pe = stg::S_RENDER_END.load(std::sync::atomic::Ordering::Relaxed);
|
|
if pr != 0 && pe != 0 {
|
|
stg::S_LAST_RENDER_US
|
|
.store(pe - pr, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
}
|
|
stg::S_START.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
|
stg::S_FIRST_EV.store(0, std::sync::atomic::Ordering::Relaxed);
|
|
stg::S_TIMER_US.store(0, std::sync::atomic::Ordering::Relaxed);
|
|
stg::S_EVENT_US.store(0, std::sync::atomic::Ordering::Relaxed);
|
|
stg::S_N_EVENTS.store(0, std::sync::atomic::Ordering::Relaxed);
|
|
let mut r = Ok(ControlFlow::Continue(()));
|
|
self.app.poll_events(timeout, |e| {
|
|
if stg::S_FIRST_EV.load(std::sync::atomic::Ordering::Relaxed) == 0 {
|
|
stg::S_FIRST_EV
|
|
.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
let t1 = stg::now_us();
|
|
ctx.update_timers_and_animations();
|
|
let t2 = stg::now_us();
|
|
r = self.window.process_event(&e);
|
|
let t3 = stg::now_us();
|
|
stg::S_TIMER_US.fetch_add(t2 - t1, std::sync::atomic::Ordering::Relaxed);
|
|
stg::S_EVENT_US.fetch_add(t3 - t2, std::sync::atomic::Ordering::Relaxed);
|
|
stg::S_N_EVENTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
if let Some(event_listener) = &self.event_listener {
|
|
event_listener(&e)
|
|
}
|
|
});
|
|
stg::S_POLL_END.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
|
if r?.is_break() {
|
|
break;
|
|
}
|
|
if self.window.pending_redraw.take() {
|
|
stg::S_RENDER_START.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
|
self.window.do_render()?;
|
|
stg::S_RENDER_END.store(stg::now_us(), std::sync::atomic::Ordering::Relaxed);
|
|
} else {
|
|
stg::S_RENDER_END.store(0, std::sync::atomic::Ordering::Relaxed);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn new_event_loop_proxy(&self) -> Option<Box<dyn i_slint_core::platform::EventLoopProxy>> {
|
|
Some(Box::new(AndroidEventLoopProxy {
|
|
event_queue: self.window.event_queue.clone(),
|
|
waker: self.app.create_waker(),
|
|
}))
|
|
}
|
|
|
|
fn set_clipboard_text(&self, text: &str, clipboard: Clipboard) {
|
|
if clipboard == Clipboard::DefaultClipboard {
|
|
self.window
|
|
.java_helper
|
|
.set_clipboard(text)
|
|
.unwrap_or_else(|e| javahelper::print_jni_error(&self.app, e));
|
|
}
|
|
}
|
|
|
|
fn clipboard_text(&self, clipboard: Clipboard) -> Option<String> {
|
|
if clipboard == Clipboard::DefaultClipboard {
|
|
Some(
|
|
self.window
|
|
.java_helper
|
|
.get_clipboard()
|
|
.unwrap_or_else(|e| javahelper::print_jni_error(&self.app, e)),
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn bind_context(&self, ctx: i_slint_core::SlintContextWeak, _: i_slint_core::InternalToken) {
|
|
let _ = self.context.set(ctx.clone());
|
|
let ctx = ctx.upgrade().expect("bind_context called while the SlintContext is still alive");
|
|
let color_scheme = match self
|
|
.window
|
|
.java_helper
|
|
.color_scheme()
|
|
.unwrap_or_else(|e| javahelper::print_jni_error(&self.app, e))
|
|
{
|
|
0x10 => i_slint_core::items::ColorScheme::Light, // UI_MODE_NIGHT_NO
|
|
0x20 => i_slint_core::items::ColorScheme::Dark, // UI_MODE_NIGHT_YES
|
|
_ => i_slint_core::items::ColorScheme::Unknown,
|
|
};
|
|
ctx.set_color_scheme(color_scheme);
|
|
if let Ok(accent) = self.window.java_helper.accent_color() {
|
|
ctx.set_accent_color(accent);
|
|
}
|
|
if let Ok(scale) = self.window.java_helper.font_scale()
|
|
&& let Some(size) = javahelper::font_scale_to_logical_length(scale)
|
|
{
|
|
ctx.set_platform_default_font_size(Some(size));
|
|
}
|
|
}
|
|
|
|
fn long_press_interval(&self, _: i_slint_core::InternalToken) -> Duration {
|
|
self.window.java_helper.long_press_timeout().unwrap_or(Duration::from_millis(500))
|
|
}
|
|
}
|
|
|
|
enum Event {
|
|
Quit,
|
|
Other(Box<dyn FnOnce() + Send + 'static>),
|
|
}
|
|
|
|
type EventQueue = Arc<Mutex<Vec<Event>>>;
|
|
|
|
struct AndroidEventLoopProxy {
|
|
event_queue: EventQueue,
|
|
waker: android_activity::AndroidAppWaker,
|
|
}
|
|
|
|
impl i_slint_core::platform::EventLoopProxy for AndroidEventLoopProxy {
|
|
fn quit_event_loop(&self) -> Result<(), EventLoopError> {
|
|
self.event_queue.lock().unwrap().push(Event::Quit);
|
|
self.waker.wake();
|
|
Ok(())
|
|
}
|
|
|
|
fn invoke_from_event_loop(
|
|
&self,
|
|
event: Box<dyn FnOnce() + Send>,
|
|
) -> Result<(), EventLoopError> {
|
|
self.event_queue.lock().unwrap().push(Event::Other(event));
|
|
self.waker.wake();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub fn set_requested_graphics_api(
|
|
requested_graphics_api: Option<i_slint_core::graphics::RequestedGraphicsAPI>,
|
|
) -> Result<(), PlatformError> {
|
|
let Some(adapter) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) else {
|
|
return Err(format!("On Android a graphics API for Slint can only be requested after calling slint::android::init()").into());
|
|
};
|
|
adapter.set_requested_graphics_api(requested_graphics_api);
|
|
Ok(())
|
|
}
|