Skip to content
Merged
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
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 37 additions & 34 deletions crates/renderd-host/src/refine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading