gallery/vendor/i-slint-backend-android-activity-1.18.1/lib.rs
Greg Pomerantz 7c2d0f354e 120Hz: early waker thread + render freshness guard (vsync-edge pacing)
The Choreographer vsync callback reaches the UI thread only 2.5-6 ms
AFTER the vsync edge (measured on the phone). With a 2.5-5 ms Skia
render plus present, that left no phase margin inside the 8.3 ms slot:
51% of frames landed one slot late (judder) and ~4% two slots late
(hitches), on every build tested so far.

Attempted fixes, all measured and retired (see commit history):
- render-ahead hold (present previous frame at next render start):
  the held present lands after the NEXT wake's 3.2-6 ms Choreographer
  latency -> 52% one-slot late.
- present-at-end + present GATE (sleep on the UI thread to quantize
  the present to the vsync grid): the gate sleep (2.5-4 ms) added to
  the slot budget (wake 5.3 + core 3 + gate 3 + present 1 = 12.3 ms >
  8.3 ms) -> cadence collapsed to 60 Hz.

Working fix (this commit):
- vsync.rs: an 'early waker' thread learns the vsync grid from the
  callback's frame_time_nanos (vkprobe::note_vsync_edge) and wakes the
  main looper ~0.8 ms BEFORE each edge while animating. Render start
  moves from edge+2.5..6 to edge-0.8..+0.5, so the present lands at a
  natural ~edge+3..5 ms phase with no UI-thread sleep.
- lib.rs: a render freshness guard (min gap = period - 1.5 ms) makes
  the FIRST wake of each slot (early or the late Choreographer
  fallback) render and rejects the second (double render = 60 Hz
  collapse). Threshold sits between late-after-early (5.8 ms, reject)
  and early-after-late (7.5 ms, accept).
- lib.rs: locked renders no longer consume pending_redraw (it carried
  the motion invalidations; consuming it starved the follow-up
  unlocked iteration -> 60 Hz collapse).
- stg.rs: IT lines now log the render decision (locked/saw_wake/
  pr_before/do_render); vkprobe logs gate sleep (GATE_US, now 0).

Measured (phone, 8580-file grid, slow 4 s drags):
  in-burst cadence: 8.29-8.44 ms median, p90 8.6-10.2 ms
  (was 8.36 median / 10.2 p90 with 3.7-20% two-slot gaps)
  in-burst >16.7 ms gaps: 0-2 per burst (was 3-13)
  present-to-present: 27% exact one slot, 33% one-slot-late steady
  state (imvisible), 3% f-jumps (Skia core 2.5-5 ms variance).
Residual judder lever: Skia core cost variance (next step).

Test hygiene reminders (HANDOFF 8.10): verify ResumedActivity, keep
the task clean (sz=1), record the perf-log byte offset before each
run (the file accumulates across process launches).
2026-10-30 11:06:58 -04:00

497 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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");
// 120Hz scroll fix (docs/BUGS/scroll-jank-120hz.md): enable the
// render-ahead pipeline in the wgpu surface. It is only ACTIVE per-
// frame while `locked` (VSYNC_LOCKED_RENDER && animating); unlocked
// renders always present immediately. See RENDER_AHEAD / RENDER_AHEAD_ACTIVE.
i_slint_renderer_skia::wgpu_30_surface::vkprobe::RENDER_AHEAD
.store(true, std::sync::atomic::Ordering::Relaxed);
let vsync = vsync::VsyncDriver::new(self.app.create_waker());
// RENDER_AHEAD flush: tracks the previous iteration's locked state so
// that when animation ends we do one final render() — which presents
// the frame held by the render-ahead pipeline (otherwise the last
// content change would never be displayed).
let mut prev_locked = false;
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.
//
// We also treat an ACTIVE POINTER DRAG as "animating" (see
// stg::pointer_recently_active): during a direct drag Slint has no
// physics animation, so has_active_animations() is false and, without
// this, the loop would sleep until the next timer (16 ms) between
// pointer events → 16-30 ms frame gaps. Keeping the vsync driver
// alive makes poll_events wake at each display refresh instead.
let animating = self.window.window.has_active_animations()
|| stg::pointer_recently_active();
vsync.set_animating(animating);
if animating {
// When animating (scrolling/flinging), cap the poll timeout to 6ms
// as a safety net in case a vsync wake is lost. Rendering still
// only happens when `pending_redraw` is set (content actually
// changed) — we do NOT force a redraw here, which would waste
// GPU re-rendering an unchanged scene on every wake.
let frame_duration = Duration::from_millis(6);
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.
// EXCEPT in vsync-locked mode: the render is deliberately deferred
// to the vsync WAKE, so a zero timeout here would busy-spin the
// loop at ~100 kHz (timeout event, no wake, flag never consumed)
// for up to 8.3 ms after every input event. Block up to the cap
// instead and let the wake arrive.
let pr_before = self.window.pending_redraw.get();
let locked = stg::VSYNC_LOCKED_RENDER
.load(std::sync::atomic::Ordering::Relaxed)
&& animating;
if pr_before && !locked {
timeout = Some(Duration::ZERO);
}
// Jank-hunt: classify the timeout path to explain the 1451 iters/s
// busy-spin. ZERO = forced by pending_redraw; CAP = 6 ms anim cap;
// NATURAL = the next timer (>= ~7 ms).
if stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed) {
let to_ms = timeout.map(|d| d.as_secs_f64() * 1000.0).unwrap_or(f64::MAX);
let kind = if pr_before {
0
} else if animating && to_ms <= 6.0 {
1
} else {
2
};
stg::note_timeout_class(kind);
}
// 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);
}
}
let t_start = stg::now_us();
let timeout_ms = timeout.map(|d| d.as_millis() as u64).unwrap_or(u64::MAX);
stg::S_START.store(t_start, 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(()));
// Jank-hunt / 120Hz: did this poll return because the vsync driver
// (or any waker) fired? Used to lock rendering to the vsync edge
// (see the render decision below) instead of rendering in reaction
// to input events (which arrive at a random phase in the slot and
// push the present past the ~5.5 ms compositor deadline → every
// frame shown one vsync late → effective 60 Hz + judder).
let mut saw_wake = false;
self.app.poll_events(timeout, |e| {
if matches!(e, PollEvent::Wake) {
saw_wake = true;
}
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 t3_start = stg::now_us();
let t3_cpu_start = stg::thread_cpu_us();
r = self.window.process_event(&e);
let t3_end = stg::now_us();
let t3_cpu_end = stg::thread_cpu_us();
let __dt3 = t3_end - t3_start;
// Jank-hunt: count wake sources. If WAKE events dominate, the
// loop is being woken far more often than vsync (120/s) or input,
// which explains the 1600 iters/s busy-spin.
match e {
PollEvent::Wake => { stg::WAKE_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }
PollEvent::Timeout => { stg::TIMEOUT_EV_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }
PollEvent::Main(android_activity::MainEvent::InputAvailable) => {
stg::INPUT_EV_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
_ => {}
}
// Jank-hunt: identify which MainEvent variant causes the big
// `ev` spikes (pointer input is ~2.5 ms max, so a 20 ms event
// is something else entirely).
if __dt3 > 5_000 && stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed) {
let nm = match e {
PollEvent::Main(android_activity::MainEvent::InputAvailable) => "Input",
PollEvent::Main(android_activity::MainEvent::InitWindow { .. }) => "InitWin",
PollEvent::Main(android_activity::MainEvent::TerminateWindow { .. }) => "TermWin",
PollEvent::Main(android_activity::MainEvent::WindowResized { .. }) => "WinResized",
PollEvent::Main(android_activity::MainEvent::RedrawNeeded { .. }) => "RedrawNeeded",
PollEvent::Main(_) => "Main-other",
_ => "NonMain",
};
stg::log_slow_event(nm, __dt3, t3_cpu_end.saturating_sub(t3_cpu_start));
}
stg::S_EVENT_US.fetch_add(__dt3, 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)
}
});
let t_poll1 = stg::now_us();
let lv1 = stg::lv_snapshot();
// Unconditionally update timers and animations on every event-loop wakeup
ctx.update_timers_and_animations();
let t2 = stg::now_us();
stg::S_TIMER_US.store(t2 - t_poll1, std::sync::atomic::Ordering::Relaxed);
stg::S_POLL_END.store(t2, std::sync::atomic::Ordering::Relaxed);
if r?.is_break() {
break;
}
let mut render_us = 0u64;
// Jank-hunt / 120Hz: vsync-locked rendering (the native-Android
// model). While animating, input events only MARK the window dirty;
// the render happens on the next vsync WAKE, so every present
// starts at a deterministic phase (vsync edge + wake latency) and
// lands well inside the compositor's ~5.5 ms deadline. Cost: up
// to one slot of extra input latency.
let do_render = if locked {
// Present EVERY vsync while animating (not only on content
// change): the regular 120 Hz present stream locks the
// display cadence. Re-rendering unchanged content is cheap
// with Skia's incremental pipeline.
//
// IMPORTANT: do NOT `pending_redraw.take()` here. The render
// is driven by the vsync wake, but the pending flag also
// carries the motion events' invalidations. Consuming it
// here starves the follow-up unlocked iteration (which runs
// 0.4 ms later, before the next motion event arrives) of
// anything to render, collapsing the cadence to 60 Hz
// (one render per two vsyncs). Letting the flag survive
// means the next iteration renders too; if the content is
// unchanged the extra render is invisible.
//
// FRESHNESS GUARD: with the early waker (vsync.rs), TWO
// wakes can arrive per slot — the early one (~0.8 ms BEFORE
// the vsync edge) and the Choreographer's late fallback
// (~5 ms after it). The first one of each slot renders;
// the second must NOT (double render = 2× the slot budget =
// 60 Hz collapse). The intervals (measured):
// late-after-early = ~5.8 ms → must be REJECTED
// early-after-late = ~7.5 ms → must be ACCEPTED
// same-kind = ~8.33 ms → ACCEPTED
// Threshold `period − 1.5 ms` (≈6.8 ms) sits between 5.8
// and 7.5 with ~0.7-1.0 ms of jitter margin on both sides.
// (A 0.7-period threshold rejects early-after-late and a
// 1.0-period threshold locks the cadence onto the late
// wakes — both measured to fragment the burst.)
let __now_ns = i_slint_renderer_skia::wgpu_30_surface::vkprobe::monotonic_ns();
let __period_ns = i_slint_renderer_skia::wgpu_30_surface::vkprobe::VSYNC_PERIOD_NS
.load(std::sync::atomic::Ordering::Relaxed);
let __last = i_slint_renderer_skia::wgpu_30_surface::vkprobe::LAST_RENDER_START_NS
.load(std::sync::atomic::Ordering::Relaxed);
let __min_gap = __period_ns.saturating_sub(1_500_000);
let __fresh = __period_ns == 0
|| __now_ns.saturating_sub(__last) >= __min_gap;
if __fresh {
i_slint_renderer_skia::wgpu_30_surface::vkprobe::LAST_RENDER_START_NS
.store(__now_ns, std::sync::atomic::Ordering::Relaxed);
}
saw_wake && __fresh
} else {
self.window.pending_redraw.get()
};
if do_render {
// Locked (vsync-wake) renders leave the flag set so the
// follow-up unlocked iteration renders too (60 Hz collapse,
// see the do_render note); unlocked renders consume it.
if !locked {
self.window.pending_redraw.take();
}
// RENDER_AHEAD (hold-without-present) is DISABLED: the held
// frame is presented at the start of the NEXT render, i.e.
// after the next vsync wake's Choreographer latency
// (measured 3.2–6 ms on this device) plus the present cost —
// landing at vsync+12.4–15.2 ms against a 13.8 ms latch
// deadline: a 1.4 ms margin that missed 52% of frames
// (one slot late) during real slow scrolls. Presenting at
// the END of the render instead still left 51% one slot
// late (the wake-arrival latency 2.4–6 ms + render 3–9 ms
// make the present phase random). The actual fix is the
// PRESENT GATE: the render still runs on the vsync wake
// (locked pacing kept), but `Queue::present` sleeps to a
// fixed vsync-grid phase (next edge + 1.5 ms, from the
// Choreographer frameTimeNanos), so cadence is exactly one
// period. See `vkprobe::present_gate` and
// docs/BUGS/scroll-jank-120hz.md §9.
i_slint_renderer_skia::wgpu_30_surface::vkprobe::RENDER_AHEAD_ACTIVE
.store(false, std::sync::atomic::Ordering::Relaxed);
// PRESENT GATE: quantize the present to the vsync grid while
// animating (drag/fling) — including the follow-up UNLOCKED
// renders (see the do_render note above), so every present
// in the burst lands on the same grid phase. One-off
// redraws (taps, model updates) stay ungated for minimum
// latency.
// PRESENT GATE: DISABLED. Sleeping on the UI thread to
// quantize the present proved counterproductive — every ms
// of gate sleep adds to the slot budget (wake 5.3 + core 3
// + gate 3 + present 1 = 12.3 ms > 8.3 ms slot → 60 Hz
// collapse, measured). The early waker (vsync.rs) instead
// moves the RENDER START to the vsync edge, so the present
// lands at a constant natural phase (~edge+3 ms) with no
// sleep at all. The gate code stays for reference.
i_slint_renderer_skia::wgpu_30_surface::vkprobe::PRESENT_GATE_ON
.store(false, std::sync::atomic::Ordering::Relaxed);
stg::S_RENDER_START.store(t2, std::sync::atomic::Ordering::Relaxed);
self.window.do_render()?;
let t3 = stg::now_us();
stg::S_RENDER_END.store(t3, std::sync::atomic::Ordering::Relaxed);
render_us = t3 - t2;
} else {
stg::S_RENDER_END.store(0, std::sync::atomic::Ordering::Relaxed);
}
// RENDER_AHEAD flush (see above): on the locked → unlocked
// transition, render once more to present the held frame.
if prev_locked && !locked {
self.window.pending_redraw.take();
i_slint_renderer_skia::wgpu_30_surface::vkprobe::RENDER_AHEAD_ACTIVE
.store(false, std::sync::atomic::Ordering::Relaxed);
self.window.do_render()?;
}
prev_locked = locked;
if stg::IT_ON.load(std::sync::atomic::Ordering::Relaxed) {
let first_ev = stg::S_FIRST_EV.load(std::sync::atomic::Ordering::Relaxed);
let lv2 = stg::lv_snapshot();
stg::log_iter(
t_start,
t_poll1 - t_start,
stg::S_N_EVENTS.load(std::sync::atomic::Ordering::Relaxed),
if first_ev == 0 { 0 } else { first_ev - t_start },
t2 - t_poll1,
stg::S_EVENT_US.load(std::sync::atomic::Ordering::Relaxed),
render_us,
animating,
timeout_ms,
lv1,
lv2,
stg::flick_snapshot(),
stg::input_snapshot(),
stg::timeout_snapshot(),
stg::wake_snapshot(),
(locked, saw_wake, pr_before, do_render),
);
}
}
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> {
let n = crate::stg::INVK_PUSH_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
crate::stg::log_line(&format!("INVK n={n}"));
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(())
}