diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c42107..9ff8fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Performance - **Sharpen on idle.** On a slow link the encoder runs below the display's resolution, and a still screen stayed soft however long it sat there: no refinement pass restores pixels that were - never encoded. Once the screen has been still for 500 ms the host now moves the encoder up to + never encoded. Once the screen has been still for 200 ms the host now moves the encoder up to the display's native size for one full-resolution keyframe plus two refinement passes, and drops back to the link's size when the screen moves again (four captures within 300 ms, so a blinking cursor does not undo it; 2 s cooldown before sharpening again). Skipped when the keyframe would - take over 1.2 s to drain at the current bitrate. ([`host::refine`](crates/renderd-host/src/refine.rs), + take over 1.2 s to drain at the current bitrate. The low-resolution refinement passes are held + back while a sharpen is coming, so nothing queues ahead of the full-size keyframe. ([`host::refine`](crates/renderd-host/src/refine.rs), [`EncodePipeline::sharpen`](crates/renderd-host/src/encode.rs)) ### Fixed diff --git a/crates/renderd-host/src/refine.rs b/crates/renderd-host/src/refine.rs index bc3e1e8..0b3ec98 100644 --- a/crates/renderd-host/src/refine.rs +++ b/crates/renderd-host/src/refine.rs @@ -58,9 +58,10 @@ pub const MAX_QUEUE: Duration = Duration::from_millis(10); /// Stillness before the encoder is moved up to the display's native size. /// -/// Long enough that typing and cursor movement do not trigger it, short -/// enough to feel like the picture sharpening as soon as you stop. -pub const SHARPEN_AFTER: Duration = Duration::from_millis(500); +/// Short enough to feel like the picture sharpening as soon as you stop. A +/// pause between keystrokes or mid-read can still start one; that costs a +/// keyframe and is undone by [`MotionDetector`] when the screen moves again. +pub const SHARPEN_AFTER: Duration = Duration::from_millis(200); /// Minimum time at the link's size after moving, before sharpening again. pub const SHARPEN_COOLDOWN: Duration = Duration::from_secs(2); @@ -379,39 +380,41 @@ mod macos { } } let now = Instant::now(); - match state.schedule.next_at() { - None => { - match sharpen_wanted(&state, encode, now) { - SharpenPlan::Now => { - state.sharpen_done = true; - let native = state.native; - drop(state); - let landed = match encode.sharpen(native) { - Ok(changed) => changed, - Err(e) => { - tracing::debug!("sharpen failed: {e}"); - false - } - }; - state = shared.state.lock().unwrap_or_else(PoisonError::into_inner); - if landed { - // The new session starts with a keyframe of the - // still screen, then refines it like any other. - state.schedule.on_capture(Instant::now()); - state.pending = None; - } - continue; + // A full-resolution keyframe supersedes the low-resolution passes, so + // while one is coming they are held back rather than queued ahead of it. + match sharpen_wanted(&state, encode, now) { + SharpenPlan::Now => { + state.sharpen_done = true; + let native = state.native; + drop(state); + let landed = match encode.sharpen(native) { + Ok(changed) => changed, + Err(e) => { + tracing::debug!("sharpen failed: {e}"); + false } - SharpenPlan::At(at) => { - state = shared - .wake - .wait_timeout(state, at.saturating_duration_since(now)) - .unwrap_or_else(PoisonError::into_inner) - .0; - continue; - } - SharpenPlan::Never => {} + }; + state = shared.state.lock().unwrap_or_else(PoisonError::into_inner); + if landed { + // The new session starts with a keyframe of the still + // screen, then refines it like any other. + state.schedule.on_capture(Instant::now()); + state.pending = None; } + continue; + } + SharpenPlan::At(at) => { + state = shared + .wake + .wait_timeout(state, at.saturating_duration_since(now)) + .unwrap_or_else(PoisonError::into_inner) + .0; + continue; + } + SharpenPlan::Never => {} + } + match state.schedule.next_at() { + None => { // Every pass ran: hand the surface back to the capture pool. state.latest = None; state = shared