fix(shell): debounce viewport resize so animations stop flashing
Sidebar / window-edge animations emit a new viewport bounds on every GPUI paint. The previous flow fired `runtime.ensure_tab` synchronously on every change, which reached down into Servo's surfman backend and destroyed + reallocated the rendering framebuffer 50+ times per second — each cycle leaving the NSView with an empty surface until Servo's next paint completed. The user saw the page strobe blank-then-content on every gesture, plus a one-time Metal driver warning about an unloadable texture (the previous framebuffer caught mid-recreate). Track `viewport_size_changed_at` on `PerTabSurface` and propagate the debounce on two seams: - `record_viewport_size` returns `Buffered` for a transition that arrives inside the 80 ms window of the previous one, so the synchronous `flush_external_web_surface_tick` from the GPUI paint callback skips the resize altogether. The very first measurement and the first transition after a quiet period still return `Applied` so a single drag step or page-load is not delayed. - `ensure_surface` skips while the viewport is settling, but only *after* the initial ensure has installed `last_ensure_key`. The first ensure must fire even mid-gesture or the page never loads. - `next_tick_delay` clamps the poll cadence to `ACTIVE_POLL_INTERVAL` while any visible tab is settling, so the trailing-edge resize fires within a frame of the gesture stopping instead of waiting for an 80 ms idle tick. The synchronous `record + ensure` pattern in `failed_surface_ensure_waits_for_a_new_key_before_retrying` can't advance an `Instant`, so it now calls a `#[cfg(test)]` `clear_viewport_resize_debounce_for_test` to simulate the trailing edge and exercise the retry-on-new-key business rule in isolation. Adds `rapid_viewport_changes_buffer_until_gesture_settles` to lock the new behaviour.
This commit is contained in:
@@ -126,6 +126,13 @@ pub(super) struct WebSurfaceTickResult {
|
||||
pub(super) struct PerTabSurface {
|
||||
pub(super) viewport_bounds: Option<Bounds<Pixels>>,
|
||||
pub(super) viewport_size: Option<WebSurfaceSize>,
|
||||
/// When [`viewport_size`] last *transitioned* to a new value. The
|
||||
/// initial measurement does not update this; only subsequent
|
||||
/// genuine size changes do. Drives [`viewport_size_is_settling`]
|
||||
/// so a sidebar / window animation that emits a viewport bounds on
|
||||
/// every GPUI paint does not translate into a Servo framebuffer
|
||||
/// destroy/recreate every frame (the "page flashing" symptom).
|
||||
viewport_size_changed_at: Option<Instant>,
|
||||
pub(super) native_surface: Option<NativeSurfaceHandle>,
|
||||
pub(super) last_ensure_key: Option<WebSurfaceEnsureKey>,
|
||||
pub(super) hover_point: Option<WebSurfaceClickPoint>,
|
||||
@@ -146,6 +153,7 @@ impl PerTabSurface {
|
||||
Self {
|
||||
viewport_bounds: None,
|
||||
viewport_size: None,
|
||||
viewport_size_changed_at: None,
|
||||
native_surface: None,
|
||||
last_ensure_key: None,
|
||||
hover_point: None,
|
||||
@@ -188,6 +196,37 @@ impl PerTabSurface {
|
||||
self.last_ensure_key.as_ref() != Some(key) || self.has_pending_input()
|
||||
}
|
||||
|
||||
/// True when the viewport bounds have changed within the last
|
||||
/// [`VIEWPORT_RESIZE_DEBOUNCE`] window. The renderer treats this
|
||||
/// as "the user is mid-animation" and holds off telling Servo to
|
||||
/// resize its rendering context until the gesture settles —
|
||||
/// Servo's surfman backend recreates the framebuffer on every
|
||||
/// `rendering_context.resize`, which would otherwise flash a
|
||||
/// blank surface for every animation frame.
|
||||
pub(super) fn viewport_size_is_settling(&self, now: Instant) -> bool {
|
||||
self.viewport_size_changed_at
|
||||
.is_some_and(|last| now.duration_since(last) < VIEWPORT_RESIZE_DEBOUNCE)
|
||||
}
|
||||
|
||||
/// Stamp the moment a genuine viewport size transition happened.
|
||||
/// Only called when [`viewport_size`] is actually moving to a new
|
||||
/// value — the very first measurement does *not* invoke this so
|
||||
/// `viewport_size_is_settling` stays false at page-load time and
|
||||
/// the initial `ensure_surface` is allowed to fire immediately.
|
||||
pub(super) fn mark_viewport_size_changed(&mut self, now: Instant) {
|
||||
self.viewport_size_changed_at = Some(now);
|
||||
}
|
||||
|
||||
/// Test-only escape hatch: simulate the trailing edge of a resize
|
||||
/// animation by clearing the debounce timestamp, so a synchronous
|
||||
/// `record + ensure` sequence in tests behaves as if the gesture
|
||||
/// has fully settled. Production code drives this via the poll
|
||||
/// cadence and time elapsing — tests can't advance an `Instant`.
|
||||
#[cfg(test)]
|
||||
pub(super) fn clear_viewport_resize_debounce_for_test(&mut self) {
|
||||
self.viewport_size_changed_at = None;
|
||||
}
|
||||
|
||||
pub(super) fn mark_ensured(&mut self, key: WebSurfaceEnsureKey) {
|
||||
self.last_ensure_key = Some(key);
|
||||
}
|
||||
@@ -226,6 +265,12 @@ impl PerTabSurface {
|
||||
|
||||
const HOVER_INPUT_MIN_INTERVAL: Duration = Duration::from_millis(32);
|
||||
|
||||
/// How long the viewport must hold its new size before we propagate
|
||||
/// the resize down to Servo. 80 ms is short enough that a user-driven
|
||||
/// drag still feels live, and long enough to collapse a 60 Hz sidebar
|
||||
/// animation (~17 ms per frame) into a single trailing-edge resize.
|
||||
const VIEWPORT_RESIZE_DEBOUNCE: Duration = Duration::from_millis(80);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) struct WebSurfaceEnsureKey {
|
||||
requested_url: String,
|
||||
|
||||
Reference in New Issue
Block a user