Files
feresaria/compositor/src/state/prediction.rs
T

336 lines
12 KiB
Rust

use super::*;
/// Output-local presentation values. Sampling never changes authoritative
/// geometry, configure barriers, animation clocks, or another output's view.
pub(crate) struct FrameScene {
pub windows: HashMap<WindowId, WindowFrame>,
pub overview: crate::overview::OverviewMotion,
}
pub(crate) struct WindowFrame {
pub geometry: WindowGeometry,
pub rect: Rect,
pub close_scale: f64,
pub close_alpha: f32,
pub focus: f64,
pub dim: f64,
}
impl Ferese {
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() {
return true;
}
if self
.output_id(output)
.and_then(|id| self.workspace_slides.get(&id))
.is_some_and(|slide| slide.held_progress.is_none() && slide.elapsed < WORKSPACE_SLIDE_DURATION)
{
return true;
}
self.windows.records().any(|(id, record)| {
let Some(mut geometry) = record.geometry else {
return false;
};
if !self.window_belongs_to_output(*id, output) {
return false;
}
if record.resize.is_some()
|| self.render.snapshot(id).is_some()
|| record.closing.as_ref().is_some_and(|close| !close.close_sent)
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|| record.dimming.as_ref().is_some_and(|dim| dim.is_animating())
{
return true;
}
if geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) {
return true;
}
let Some(workspace) = self.workspaces.workspace_for_window(*id) else {
return false;
};
self.viewport_animations.get(&workspace).is_some_and(|viewport| {
let mut viewport = *viewport;
let held = self
.focus_swipe
.as_ref()
.is_some_and(|swipe| swipe.workspace == workspace);
!held && viewport.advance(Duration::ZERO, self.viewport_spring_config)
}) || record.coupled_width.as_ref().is_some_and(|(_, width)| {
let mut width = *width;
width.advance(Duration::ZERO, self.viewport_spring_config)
})
})
}
pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene {
let delta = if self.animations_enabled && self.output_has_animations(output) {
horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed)
} else {
Duration::ZERO
};
let overview = self.overview.sample(delta, self.spring_config);
let blocked = self
.windows
.resizing()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
let mut windows = HashMap::new();
for (&id, record) in self.windows.records() {
let Some(original) = record.geometry else { continue };
if !self.window_belongs_to_output(id, output) {
continue;
}
let workspace = self.workspaces.workspace_for_window(id);
let world = record.world_x.as_ref().and_then(|(workspace, world)| {
let viewport = self.viewport_animations.get(workspace)?;
let held = self
.focus_swipe
.as_ref()
.is_some_and(|swipe| swipe.workspace == *workspace);
Some((*world, *viewport, held))
});
let width = record.coupled_width.as_ref().map(|(_, width)| *width);
let geometry = if delta.is_zero() || workspace.is_some_and(|workspace| blocked.contains(&workspace)) {
original
} else {
predict_geometry(
original,
delta,
self.spring_config,
self.viewport_spring_config,
world,
width,
)
};
let mut rect = overview.presented_rect(id, geometry.visual.current);
if !overview.is_presenting() {
let (x, y) = self.workspace_slide_offset_at(id, delta);
rect.x += x;
rect.y += y;
}
let mut close = record.closing.as_ref().copied().unwrap_or_default();
if record.closing.is_some() && !delta.is_zero() {
close.advance(delta, true);
}
let eased = smoothstep(close.progress);
let focused = if self.overview.is_active() {
self.overview_selected(id)
} else {
self.focused_window == Some(id)
};
let sample_dim = |motion: Option<&DimAnimation>, fallback| {
let Some(mut motion) = motion.cloned() else {
return fallback;
};
if !delta.is_zero() {
motion.predict(delta, self.inactive_dim.duration_ms);
}
motion.current
};
windows.insert(
id,
WindowFrame {
geometry,
rect,
close_scale: 1.0 - eased * 0.02,
close_alpha: (1.0 - eased) as f32,
focus: sample_dim(record.focus.as_ref(), if focused { 1.0 } else { 0.0 }),
dim: sample_dim(record.dimming.as_ref(), 0.0),
},
);
}
FrameScene { windows, overview }
}
}
fn predict_geometry(
original: WindowGeometry,
delta: Duration,
spring: SpringConfig,
viewport_spring: SpringConfig,
world: Option<(AnimatedValue, AnimatedValue, bool)>,
width: Option<AnimatedValue>,
) -> WindowGeometry {
let mut predicted = original;
let zooming = predicted.is_zooming();
if width.is_some() {
predicted.visual.target.width = predicted.visual.current.width;
predicted.visual.velocity.width = 0.0;
}
predicted.advance(delta, spring, true);
predicted.visual.target.width = original.visual.target.width;
if let Some((mut world, mut viewport, held)) = world
&& !zooming
{
world.advance(delta, spring);
if !held {
viewport.advance(delta, viewport_spring);
}
predicted.visual.current.x = world.current - viewport.current;
predicted.visual.velocity.x = world.velocity - viewport.velocity;
}
if let Some(mut width) = width {
width.advance(delta, viewport_spring);
predicted.visual.current.width = width.current;
predicted.visual.velocity.width = width.velocity;
}
predicted
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
fn frame_sampling_preserves_compositor_state_between_output_deadlines() {
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
assert!(
runtime.starts_with(std::env::temp_dir()),
"use a disposable runtime directory"
);
let mut event_loop = EventLoop::try_new().unwrap();
let display = Display::new().unwrap();
let config = crate::config::Config::default().runtime_config().unwrap();
let mut state = Ferese::new(&mut event_loop, display, config).unwrap();
let output = Output::new(
"forecast-test".into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (1920, 1080).into(),
refresh: 60_000,
}),
Some(smithay::utils::Transform::Normal),
None,
Some((0, 0).into()),
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "forecast-test".into());
let id = WindowId(1);
state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap();
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
geometry.visual.set_target(Rect::new(600., 0., 400., 300.));
// This test isolates frame sampling from the protocol client fixture.
state.windows.records.insert(
id,
super::super::window_registry::WindowRecord {
geometry: Some(geometry),
..Default::default()
},
);
let tick = state.last_animation_tick;
for horizon in [
Duration::from_millis(16),
Duration::from_millis(4),
Duration::from_millis(8),
] {
{
let forecast = state.sample_frame(&output, horizon);
assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x);
}
assert_eq!(*state.windows.geometry(&id).unwrap(), geometry);
assert_eq!(state.last_animation_tick, tick);
}
// A prediction must neither advance nor expire a client resize barrier.
state.windows.set_transaction(
id,
crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO),
);
let blocked = state.sample_frame(&output, Duration::from_millis(16));
assert_eq!(blocked.windows[&id].geometry, geometry);
assert!(state.windows.transaction(&id).is_some());
state.windows.clear_transaction(&id);
state.animations_enabled = false;
let forecast = state.sample_frame(&output, Duration::from_millis(16));
assert_eq!(forecast.windows[&id].geometry, geometry);
}
#[test]
fn mixed_refresh_forecasts_do_not_change_authoritative_geometry_or_client_state() {
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
geometry.visual.set_target(Rect::new(600., 0., 500., 300.));
let original = geometry;
let sample = |delta| {
predict_geometry(
geometry,
delta,
SpringConfig::default(),
SpringConfig::default(),
None,
None,
)
};
let fast = sample(Duration::from_nanos(1_000_000_000 / 240));
let slow = sample(Duration::from_nanos(1_000_000_000 / 60));
assert!(fast.visual.current.x > 0.);
assert!(slow.visual.current.x > fast.visual.current.x);
assert_eq!(sample(Duration::from_nanos(1_000_000_000 / 240)), fast);
assert_eq!(geometry, original);
assert_eq!(slow.client, original.client);
assert_eq!(slow.logical, original.logical);
}
#[test]
fn scrolling_coordinates_and_coupled_width_use_the_same_forecast_time() {
let geometry = WindowGeometry::new(Rect::new(500., 0., 400., 300.), None);
let mut world = AnimatedValue::new(500.);
world.set_target(600.);
let mut viewport = AnimatedValue::new(0.);
viewport.set_target(200.);
let mut width = AnimatedValue::new(400.);
width.set_target(500.);
let spring = SpringConfig::default();
let delta = Duration::from_millis(10);
let predicted = predict_geometry(
geometry,
delta,
spring,
spring,
Some((world, viewport, false)),
Some(width),
);
world.advance(delta, spring);
viewport.advance(delta, spring);
width.advance(delta, spring);
assert_eq!(predicted.visual.current.x, world.current - viewport.current);
assert_eq!(predicted.visual.current.width, width.current);
assert_eq!(predicted.visual.target, geometry.visual.target);
}
}