Keep a drag in draft until it stops, then render sharp once
During any drag longer than 120 ms the canvas rendered a full-resolution frame every 128 ms under the finger. The settle timer was armed by the first draft of a burst and not re-armed by later ones, so it counted from the start of the gesture rather than from its last movement, fired mid-drag, and the next coalesced event armed it again. Each of those frames is the most expensive one the canvas draws, landing where the frame budget is tightest. The draft/sharp decision now lives in `refine::Refine`, apart from the timers that carry it out. Every draft frame arms a settle timer carrying a generation token and only the newest token is honoured, so the sharp frame lands SETTLE_DELAY after the last movement. A request arriving while a settle is still owed also counts as part of the gesture, so a slow stretch of a drag (one event per frame, nothing to coalesce) no longer renders sharp between drafts. A timer that fires with a render already posted defers to it. The tests drive the state machine through simulated timelines; the long-drag case reproduced the four mid-drag sharp frames before the fix.
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
//! TRACES: FR-DSP-4
|
||||
//! When the canvas renders a draft and when it renders sharp.
|
||||
//!
|
||||
//! The decision `build_redraw` acts on, kept apart from the timers and the
|
||||
//! window that carry it out so it can be driven event by event in a test.
|
||||
//! `lib.rs` owns the plumbing: it posts a zero-delay timer when
|
||||
//! [`Refine::request`] says to, calls [`Refine::tick`] from it, and arms a
|
||||
//! [`SETTLE_DELAY`](crate::SETTLE_DELAY) timer carrying whatever token
|
||||
//! `tick` hands back, which calls [`Refine::settle`] when it fires.
|
||||
|
||||
/// Which quality of frame to render.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Frame {
|
||||
/// Half resolution, while a gesture is still moving.
|
||||
Draft,
|
||||
/// Full resolution.
|
||||
Sharp,
|
||||
}
|
||||
|
||||
/// A settle timer's claim on the sharp frame, redeemed by [`Refine::settle`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Settle(u64);
|
||||
|
||||
/// The coalescing and settling state behind the canvas redraw.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Refine {
|
||||
/// A render is posted and has not run yet.
|
||||
pending: bool,
|
||||
/// Something changed since the last render.
|
||||
dirty: bool,
|
||||
/// A request arrived while a render was already posted.
|
||||
coalesced: bool,
|
||||
/// A settle timer is armed.
|
||||
settling: bool,
|
||||
/// The token the most recent settle timer carries.
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl Refine {
|
||||
/// Something changed; `true` means post a render.
|
||||
pub(crate) fn request(&mut self) -> bool {
|
||||
self.dirty = true;
|
||||
if self.pending {
|
||||
self.coalesced = true;
|
||||
return false;
|
||||
}
|
||||
self.pending = true;
|
||||
true
|
||||
}
|
||||
|
||||
/// The posted render runs: which frame to draw, if any, and a settle
|
||||
/// timer to arm afterwards, if one is wanted.
|
||||
///
|
||||
/// **Every draft frame arms a fresh settle timer**, and only the newest
|
||||
/// one is honoured (see [`Self::settle`]). That is a debounce: the sharp
|
||||
/// frame arrives [`SETTLE_DELAY`](crate::SETTLE_DELAY) after the *last*
|
||||
/// movement, never mid-gesture. It used to arm once per burst and not
|
||||
/// again while that timer was pending, so the timer counted from the
|
||||
/// *first* draft of a drag and fired underneath the finger — a full
|
||||
/// resolution render every 128 ms of any drag longer than the delay,
|
||||
/// each one the most expensive frame the canvas draws, landing exactly
|
||||
/// where the budget is tightest.
|
||||
///
|
||||
/// **What counts as "still dragging".** No control reports a gesture
|
||||
/// boundary, and threading one out of every slider, curve point and crop
|
||||
/// handle would be a lot of surface for a rendering concern. Two signals
|
||||
/// stand in for it. A request that had to coalesce into a render already
|
||||
/// posted can only mean a control moved again. And a request arriving
|
||||
/// while a settle is still owed belongs to the gesture that owes it: a
|
||||
/// slow stretch of a drag, one event per frame, would otherwise render
|
||||
/// sharp between two drafts. One-off changes — a click, a reset, a resize
|
||||
/// — meet neither, so they render sharp the first time and never draw a
|
||||
/// draft frame at all.
|
||||
pub(crate) fn tick(&mut self) -> Option<(Frame, Option<Settle>)> {
|
||||
self.pending = false;
|
||||
if !std::mem::take(&mut self.dirty) {
|
||||
return None;
|
||||
}
|
||||
let dragging = std::mem::take(&mut self.coalesced) || self.settling;
|
||||
if !dragging {
|
||||
return Some((Frame::Sharp, None));
|
||||
}
|
||||
self.settling = true;
|
||||
self.generation += 1;
|
||||
Some((Frame::Draft, Some(Settle(self.generation))))
|
||||
}
|
||||
|
||||
/// A settle timer fired: `Some` means render sharp now.
|
||||
///
|
||||
/// `None` for any timer a later draft has superseded, and also when a
|
||||
/// render is already posted: that one will draw a draft and arm its own
|
||||
/// timer, and a sharp frame drawn now would be replaced a moment later.
|
||||
pub(crate) fn settle(&mut self, token: Settle) -> Option<Frame> {
|
||||
if !self.settling || token.0 != self.generation || self.pending {
|
||||
return None;
|
||||
}
|
||||
self.settling = false;
|
||||
Some(Frame::Sharp)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The settle delay, in the simulated milliseconds below.
|
||||
const DELAY: u64 = 120;
|
||||
|
||||
/// Drive a `Refine` through a timeline of input events the way the event
|
||||
/// loop would, and record every frame rendered with the time it was.
|
||||
///
|
||||
/// `inputs` are the milliseconds at which a control moved. Each posted
|
||||
/// render runs one millisecond after the request that posted it, and a
|
||||
/// burst of requests landing in the same millisecond coalesces into it.
|
||||
fn run(inputs: &[u64]) -> Vec<(u64, Frame)> {
|
||||
let mut r = Refine::default();
|
||||
let mut frames = Vec::new();
|
||||
let mut tick_at: Option<u64> = None;
|
||||
let mut settles: Vec<(u64, Settle)> = Vec::new();
|
||||
let end = inputs.last().copied().unwrap_or(0) + 10 * DELAY;
|
||||
let mut inputs = inputs.iter().copied().peekable();
|
||||
for now in 0..=end {
|
||||
// Timers whose time has come, in the order they were armed.
|
||||
if tick_at == Some(now) {
|
||||
tick_at = None;
|
||||
if let Some((frame, arm)) = r.tick() {
|
||||
frames.push((now, frame));
|
||||
if let Some(token) = arm {
|
||||
settles.push((now + DELAY, token));
|
||||
}
|
||||
}
|
||||
}
|
||||
let due: Vec<Settle> = settles
|
||||
.iter()
|
||||
.filter(|(t, _)| *t == now)
|
||||
.map(|(_, s)| *s)
|
||||
.collect();
|
||||
settles.retain(|(t, _)| *t != now);
|
||||
for token in due {
|
||||
if let Some(frame) = r.settle(token) {
|
||||
frames.push((now, frame));
|
||||
}
|
||||
}
|
||||
while inputs.peek() == Some(&now) {
|
||||
inputs.next();
|
||||
if r.request() {
|
||||
tick_at = Some(now + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
frames
|
||||
}
|
||||
|
||||
/// A drag: two moves per 16 ms frame from `from` for `len` milliseconds.
|
||||
fn drag(from: u64, len: u64) -> Vec<u64> {
|
||||
(from..from + len)
|
||||
.step_by(16)
|
||||
.flat_map(|t| [t, t])
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The sharp frames rendered in `(from, to]`.
|
||||
fn sharp_between(frames: &[(u64, Frame)], from: u64, to: u64) -> Vec<(u64, Frame)> {
|
||||
frames
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|(t, f)| *f == Frame::Sharp && *t > from && *t <= to)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_drag_renders_no_sharp_frame_until_it_stops() {
|
||||
// The regression: before the debounce this drew sharp frames at 121,
|
||||
// 249, 377 and 505 ms, the settle timer counting from the first draft.
|
||||
let inputs = drag(0, 600);
|
||||
let last = *inputs.last().unwrap();
|
||||
let frames = run(&inputs);
|
||||
let mid = sharp_between(&frames, 1, last + 1);
|
||||
assert!(mid.is_empty(), "sharp frames mid-drag: {mid:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_sharp_frame_lands_one_delay_after_the_last_movement() {
|
||||
let inputs = drag(0, 600);
|
||||
let last = *inputs.last().unwrap();
|
||||
let frames = run(&inputs);
|
||||
let sharp = sharp_between(&frames, 1, u64::MAX);
|
||||
// The last draft renders a millisecond after the last input, and the
|
||||
// sharp frame the settle delay after that.
|
||||
assert_eq!(sharp, vec![(last + 1 + DELAY, Frame::Sharp)]);
|
||||
assert_eq!(frames.last(), Some(&(last + 1 + DELAY, Frame::Sharp)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_slow_stretch_inside_a_drag_stays_draft() {
|
||||
// Coalesced moves, then one move per 40 ms (none coalesce), then stop.
|
||||
let mut inputs = drag(0, 100);
|
||||
inputs.extend((112..400).step_by(40));
|
||||
let last = *inputs.last().unwrap();
|
||||
let frames = run(&inputs);
|
||||
let mid = sharp_between(&frames, 1, last + 1);
|
||||
assert!(mid.is_empty(), "sharp frames mid-drag: {mid:?}");
|
||||
assert_eq!(frames.last(), Some(&(last + 1 + DELAY, Frame::Sharp)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pause_longer_than_the_delay_settles_and_the_next_drag_drafts_again() {
|
||||
let mut inputs = drag(0, 100);
|
||||
inputs.extend(drag(400, 100));
|
||||
let frames = run(&inputs);
|
||||
let sharp = sharp_between(&frames, 1, u64::MAX);
|
||||
assert_eq!(sharp.len(), 2, "one settle per drag: {frames:?}");
|
||||
assert!(frames
|
||||
.iter()
|
||||
.any(|(t, f)| *f == Frame::Draft && (400..500).contains(t)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_change_renders_sharp_once_and_arms_nothing() {
|
||||
assert_eq!(run(&[5]), vec![(6, Frame::Sharp)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_superseded_timer_does_nothing() {
|
||||
let mut r = Refine::default();
|
||||
assert!(r.request());
|
||||
assert!(!r.request());
|
||||
let (f, first) = r.tick().unwrap();
|
||||
assert_eq!(f, Frame::Draft);
|
||||
assert!(r.request());
|
||||
let (f, second) = r.tick().unwrap();
|
||||
assert_eq!(f, Frame::Draft);
|
||||
assert_eq!(r.settle(first.unwrap()), None);
|
||||
assert_eq!(r.settle(second.unwrap()), Some(Frame::Sharp));
|
||||
// And once redeemed, not again.
|
||||
assert_eq!(r.settle(second.unwrap()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_timer_firing_with_a_render_posted_defers_to_it() {
|
||||
let mut r = Refine::default();
|
||||
r.request();
|
||||
r.request();
|
||||
let (_, token) = r.tick().unwrap();
|
||||
assert!(r.request());
|
||||
assert_eq!(r.settle(token.unwrap()), None);
|
||||
let (f, next) = r.tick().unwrap();
|
||||
assert_eq!(f, Frame::Draft);
|
||||
assert_eq!(r.settle(next.unwrap()), Some(Frame::Sharp));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user