Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ futures = "0.3"
raw-window-handle = "0.6"

# ── GPUI core crate (provides Platform trait, event types, geometry, etc.) ───
gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false }
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false }

# ── iOS ──────────────────────────────────────────────────────────────────────

Expand All @@ -80,7 +80,7 @@ uuid = { version = "1", features = ["v4", "v5"] }
block2 = "0.6"

# wgpu renderer (Metal backend on iOS/Android)
gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.7", features = ["font-kit"] }
gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.8", features = ["font-kit"] }
wgpu = "29.0.4"

# Text / font stack
Expand Down Expand Up @@ -111,7 +111,7 @@ android_logger = "0.15"

# wgpu renderer (Vulkan backend on Android)
# Android uses Cosmic Text and system fonts; font-kit pulls in desktop Fontconfig.
gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.7" }
gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.8" }

# rustc-hash is required by the PlatformKeyboardMapper trait signature
rustc-hash = "2"
Expand All @@ -120,7 +120,7 @@ uuid = { version = "1", features = ["v5"] }
# ── dev ──────────────────────────────────────────────────────────────────────

[dev-dependencies]
gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false, features = ["test-support"] }
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["test-support"] }

# ── profiles ─────────────────────────────────────────────────────────────────

Expand Down
6 changes: 3 additions & 3 deletions example/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ path = "src/main.rs"

[dependencies]
# Pin the mobile facade support without requiring a published crate.
gpui-kit = { git = "https://github.com/longbridge/gpui-kit", rev = "2ec5696c2e975ddd2b8380d12ec00631bd159128", default-features = false, features = ["component"] }
gpui-kit = { git = "https://github.com/longbridge/gpui-kit", rev = "87d10ae5e1299d1be18670a37570c5402f952e34", default-features = false, features = ["component"] }
gpui-mobile = { package = "gpui-pre-mobile", path = "..", version = "0.1.0" }
gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false }
reqwest_client = { package = "gpui-pre-reqwest-client", version = "=0.3.7" }
gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false }
reqwest_client = { package = "gpui-pre-reqwest-client", version = "=0.3.8" }
reqwest = { package = "gpui-pre-reqwest", version = "0.12.15", default-features = false, features = ["rustls-tls"] }
log = "0.4"

Expand Down
60 changes: 57 additions & 3 deletions src/android/frame_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,23 @@
//! [`FramePacer`]; this module is the thread-local instance plus the vsync
//! thread and `ALooper` glue around it. Without a choreographer (no looper
//! available to the vsync thread) the pacer spaces frames by a 60 Hz clock.
//!
//! Each frame also carries when and how it was requested, for GPUI's frame
//! profiling ([`take_signal`]). Like the desktop backends, the vsync thread
//! captures the vsync's timestamp only when GPUI's `profiler` feature is on.

use std::{
cell::RefCell,
ptr,
sync::{
atomic::{AtomicBool, AtomicPtr, Ordering},
mpsc, OnceLock,
mpsc, LazyLock, OnceLock,
},
time::{Duration, Instant},
};

use gpui::{FrameRequestSource, PlatformFrameSignal};

use crate::frame_pacer::FramePacer;

/// Longest the loop blocks with nothing due; a missed wakeup costs at most
Expand Down Expand Up @@ -81,6 +87,14 @@ static POST_REQUESTED: AtomicBool = AtomicBool::new(false);
/// A frame callback fired and its frame has not been handed to the pacer yet.
static FRAME_DUE: AtomicBool = AtomicBool::new(false);

/// The timestamp of the vsync behind [`FRAME_DUE`], handed from the vsync
/// thread to the pacer together with it. Empty unless profiling.
static VSYNC_SIGNAL: LazyLock<PlatformFrameSignal> = LazyLock::new(PlatformFrameSignal::new);

/// A vsync timestamp older than this when its callback runs is not trusted;
/// the callback's own arrival time stands in for it.
const MAX_VSYNC_AGE: Duration = Duration::from_secs(1);

/// Demand raised from a thread that is not the loop thread; the loop turns it
/// into [`schedule_frame`] on its next iteration.
static OFF_THREAD_DEMAND: AtomicBool = AtomicBool::new(false);
Expand Down Expand Up @@ -169,13 +183,42 @@ fn vsync_thread(ready: mpsc::Sender<bool>) {
}

unsafe extern "C" fn on_vsync(
_frame_time_nanos: std::os::raw::c_long,
frame_time_nanos: std::os::raw::c_long,
_data: *mut std::ffi::c_void,
) {
if let Some(at) = PlatformFrameSignal::capture(|| vsync_instant(frame_time_nanos)) {
VSYNC_SIGNAL.record(at, FrameRequestSource::NativeCallback);
}
FRAME_DUE.store(true, Ordering::Release);
wake_looper(&MAIN_LOOPER);
}

/// The vsync's timestamp as an [`Instant`]. `frame_time_nanos` is on
/// `CLOCK_MONOTONIC`, but an `Instant` cannot be built from raw nanoseconds,
/// so both clocks are sampled and the callback's arrival time is stepped back
/// by the vsync's age. The arrival time stands in when the clock read fails
/// or the age is implausible (the 32-bit `long` of this callback truncates
/// the timestamp).
fn vsync_instant(frame_time_nanos: std::os::raw::c_long) -> Instant {
let received_at = Instant::now();
let mut now = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
// SAFETY: `now` is a valid out-pointer for the duration of the call.
if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) } != 0 {
return received_at;
}
let now_nanos = i128::from(now.tv_sec) * 1_000_000_000 + i128::from(now.tv_nsec);
let age_nanos = now_nanos - i128::from(frame_time_nanos);
if !(0..=MAX_VSYNC_AGE.as_nanos() as i128).contains(&age_nanos) {
return received_at;
}
received_at
.checked_sub(Duration::from_nanos(age_nanos as u64))
.unwrap_or(received_at)
}

fn wake_looper(looper: &AtomicPtr<ndk_sys::ALooper>) {
let looper = looper.load(Ordering::Acquire);
if !looper.is_null() {
Expand All @@ -191,7 +234,7 @@ fn with_pacer<R>(f: impl FnOnce(&FramePacer) -> R) -> Option<R> {
let slot = slot.borrow();
let pacer = slot.as_ref()?;
if FRAME_DUE.swap(false, Ordering::AcqRel) {
pacer.on_vsync();
pacer.on_vsync(VSYNC_SIGNAL.take().map(|(at, _)| at));
}
Some(f(pacer))
})
Expand Down Expand Up @@ -224,6 +267,17 @@ pub(crate) fn take_frame() -> bool {
with_pacer(|pacer| pacer.take_frame(Instant::now())).unwrap_or(false)
}

/// When and how the frame the loop just took with [`take_frame`] was
/// requested, for `RequestFrameOptions::signal_at` / `signal_source`. The
/// window's frame callback drains it; `None` when the time is unknown.
///
/// Unlike the other entry points this does not hand the pacer a vsync that
/// arrived meanwhile: that vsync owes the *next* frame, and draining it here
/// would charge its time to this one.
pub(crate) fn take_signal() -> Option<(Instant, FrameRequestSource)> {
PACER.with(|slot| slot.borrow().as_ref()?.take_signal())
}

/// How long the loop may block on its looper: zero with a frame due, up to
/// the next delayed dispatcher task or the fallback frame otherwise, and
/// never more than [`IDLE_POLL_CAP`]. `can_draw` is whether the loop would
Expand Down
15 changes: 12 additions & 3 deletions src/android/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
use anyhow::{Context as _, Result};
use futures::channel::oneshot;
use gpui::{
self, AtlasKey, AtlasTile, Capslock, DispatchEventResult, GpuSpecs, Modifiers, PlatformAtlas,
PlatformDisplay, PlatformInputHandler, PlatformWindow, PromptButton, PromptLevel,
RequestFrameOptions, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
self, AtlasKey, AtlasTile, Capslock, DispatchEventResult, FrameRequestSource, GpuSpecs,
Modifiers, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, PromptButton,
PromptLevel, RequestFrameOptions, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
WindowVisibility,
};
use gpui_wgpu::{wgpu, GpuContext, WgpuRenderer, WgpuSurfaceConfig};
Expand Down Expand Up @@ -1629,10 +1629,19 @@ impl PlatformWindow for AndroidPlatformWindow {
// A freshly attached surface has an empty swapchain; repaint in full.
let surface_new = force_render_once.swap(false, std::sync::atomic::Ordering::AcqRel);

// When and why `frame_source` let this frame through: a vsync, or
// demand the pacer served by itself.
let (signal_at, signal_source) = super::frame_source::take_signal().map_or(
(None, FrameRequestSource::NativeCallback),
|(at, source)| (Some(at), source),
);

let mut cb = send_callback.lock();
cb(RequestFrameOptions {
require_presentation: false,
force_render: text_dirty || surface_new,
signal_at,
signal_source,
});
});
}
Expand Down
Loading
Loading