Overhaul resize presentation and wallpaper rendering

Coordinate native-pixel resize handoffs, fractional geometry and shared animation timing. Delegate wallpaper rendering to the compositor, advertise DMA-BUF device feedback, and improve callback pacing and render metrics.

Handle monitor-local shell surfaces, lid-driven output evacuation and workspace restoration.
This commit is contained in:
Samuel Onoja committed 2026-09-26 14:50:14 +01:00
1 parent 48ac963ffe
commit 0709484efb
20 files changed
+2093 -319

No files matched your search

+197 -6
View File
@@ -12,7 +12,9 @@ use smithay::{
Fourcc,
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
},
drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface},
drm::{
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType,
},
egl::{EGLContext, EGLDisplay},
input::InputEvent,
libinput::{LibinputInputBackend, LibinputSessionInterface},
@@ -36,7 +38,7 @@ use smithay::{
wayland_server::backend::GlobalId,
},
utils::{DeviceFd, Monotonic, Time, Transform},
wayland::presentation::Refresh,
wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh},
};
use crate::{
@@ -51,11 +53,39 @@ use crate::{
pub struct DirectBackendState {
pub session: LibSeatSession,
pub active: bool,
lid_closed: bool,
devices: HashMap<DrmNode, DirectDevice>,
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
}
impl DirectBackendState {
pub(crate) fn capture_resize_snapshot(
&mut self,
window: &smithay::desktop::Window,
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
output: &Output,
remaining: usize,
) -> Result<Option<crate::winit::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError>
{
let Some(device) = self.devices.values_mut().find(|device| {
device
.outputs
.values()
.any(|candidate| &candidate.output == output)
}) else {
return Ok(None);
};
crate::winit::capture_resize_snapshot(
&mut device.renderer,
window,
geometry,
output.current_scale().fractional_scale(),
remaining,
)
}
}
#[derive(Clone, Debug)]
pub(crate) struct ConnectedOutputInfo {
pub connector: String,
@@ -94,6 +124,7 @@ struct DirectDevice {
}
struct DirectOutput {
internal: bool,
connector: connector::Handle,
mode: DrmMode,
settings: OutputSettings,
@@ -143,6 +174,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
state.direct_backend = Some(DirectBackendState {
session,
active: session_active,
lid_closed: initial_lid_closed(),
devices: HashMap::new(),
presentation: HashMap::new(),
connected_outputs: Vec::new(),
@@ -279,10 +311,25 @@ fn open_primary_device(
state.shm_state.update_formats(renderer.shm_formats());
let dmabuf_formats = renderer.dmabuf_formats();
let display_handle = state.display_handle.clone();
// Mesa's Wayland EGL path needs the main device, not just a v3 format
// list, to choose a hardware render node. Without feedback, nested EGL
// clients can fall back to llvmpipe despite a GPU-backed DRM compositor.
let render_node = node
.node_with_type(NodeType::Render)
.and_then(Result::ok)
.unwrap_or(node);
let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), dmabuf_formats).build()?;
state
.dmabuf_state
.create_global::<Ferese>(&display_handle, dmabuf_formats);
let scan = select_outputs(&drm, &state.output_profiles)?;
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
let mut scan = select_outputs(&drm, &state.output_profiles)?;
apply_lid_policy(
&mut scan,
state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.lid_closed),
);
if scan.selections.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
@@ -418,6 +465,68 @@ pub fn switch_vt(state: &mut Ferese, vt: i32) {
}
}
pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
if backend.lid_closed == closed {
return;
}
backend.lid_closed = closed;
tracing::info!(closed, "laptop lid state changed");
let nodes = backend.devices.keys().copied().collect::<Vec<_>>();
for node in nodes {
rescan_device(state, node);
}
}
fn initial_lid_closed() -> bool {
std::fs::read_dir("/proc/acpi/button/lid")
.ok()
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter_map(|entry| std::fs::read_to_string(entry.path().join("state")).ok())
.any(|state| state.split_whitespace().last() == Some("closed"))
}
fn internal_connector(interface: connector::Interface) -> bool {
matches!(
interface,
connector::Interface::EmbeddedDisplayPort
| connector::Interface::LVDS
| connector::Interface::DSI
)
}
fn apply_lid_policy(scan: &mut OutputScan, closed: bool) {
let external_available = scan
.selections
.iter()
.any(|selection| !internal_connector(selection.connector.interface()));
if !lid_hides_panel(closed, external_available) {
return;
}
let internal = scan
.selections
.iter()
.filter(|selection| internal_connector(selection.connector.interface()))
.map(|selection| selection.connector.to_string())
.collect::<HashSet<_>>();
scan.selections
.retain(|selection| !internal_connector(selection.connector.interface()));
for output in &mut scan.connected_outputs {
if internal.contains(&output.connector) {
output.enabled = false;
output.current_mode = None;
}
}
}
fn lid_hides_panel(closed: bool, external_available: bool) -> bool {
closed && external_available
}
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
let missed_deadlines = state
.direct_backend
@@ -570,7 +679,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
return;
};
let scan = match select_outputs(&device.drm, &state.output_profiles) {
let mut scan = match select_outputs(&device.drm, &state.output_profiles) {
Ok(scan) => scan,
Err(error) => {
tracing::error!(?node, %error, "failed to scan DRM connectors");
@@ -578,6 +687,11 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
return;
}
};
let lid_closed = state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.lid_closed);
apply_lid_policy(&mut scan, lid_closed);
if let Some(backend) = state.direct_backend.as_mut() {
backend.connected_outputs = scan.connected_outputs;
}
@@ -587,6 +701,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
.map(|selection| (selection.crtc, selection))
.collect::<HashMap<_, _>>();
let existing = device.outputs.keys().copied().collect::<Vec<_>>();
let mut deferred_removals = Vec::new();
for crtc in existing {
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
@@ -601,6 +716,14 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
continue;
}
// Add replacements on free CRTCs before removing vanished outputs so
// workspace evacuation always has a real destination. Reused CRTCs
// still need their old surface released first.
if !selections.contains_key(&crtc) {
deferred_removals.push(crtc);
continue;
}
if let Some(output) = device.outputs.remove(&crtc) {
state.display_handle.disable_global::<Ferese>(output.global);
state.unregister_output(&output.output);
@@ -643,7 +766,48 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
}
}
for crtc in deferred_removals {
let keep_internal = device
.outputs
.get(&crtc)
.is_some_and(|output| output.internal)
&& lid_closed
&& state.direct_backend.as_ref().is_some_and(|backend| {
device.outputs.get(&crtc).is_some_and(|output| {
backend
.connected_outputs
.iter()
.any(|info| info.connector == output.output.name())
})
})
&& !device.outputs.values().any(|output| !output.internal);
if keep_internal {
tracing::warn!(
?crtc,
"keeping laptop panel enabled: external output activation failed"
);
if let Some(output) = device.outputs.get(&crtc)
&& let Some(backend) = state.direct_backend.as_mut()
&& let Some(info) = backend
.connected_outputs
.iter_mut()
.find(|info| info.connector == output.output.name())
{
info.enabled = true;
info.current_mode = Some(connected_mode_info(output.mode));
}
continue;
}
if let Some(output) = device.outputs.remove(&crtc) {
state.display_handle.disable_global::<Ferese>(output.global);
state.unregister_output(&output.output);
if let Some(backend) = state.direct_backend.as_mut() {
backend.presentation.remove(&(node, crtc));
}
}
}
restore_device(state, node, device);
state.restore_output_focus();
state.relayout();
render_all(state);
}
@@ -657,6 +821,11 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
return;
};
let context = device.renderer.context_id().erased();
state.wallpaper.forget_context(&context);
state
.resize_snapshots
.retain(|_, snapshot| snapshot.context != context);
for (crtc, output) in device.outputs {
state.display_handle.disable_global::<Ferese>(output.global);
state.unregister_output(&output.output);
@@ -901,7 +1070,6 @@ fn create_direct_output(
settings: &OutputSettings,
) -> Result<DirectOutput, Box<dyn Error>> {
let identity = connector_identity(drm, &connector);
let (output, global) = create_output(state, &connector, mode, identity, settings);
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
let allocator = GbmAllocator::new(
gbm.clone(),
@@ -913,11 +1081,15 @@ fn create_direct_output(
&[Fourcc::Argb8888, Fourcc::Abgr8888],
renderer.dmabuf_formats(),
)?;
// Publish only after DRM/GBM creation succeeds; failed activation must not
// leave a phantom output/workspace that could receive evacuated windows.
let (output, global) = create_output(state, &connector, mode, identity, settings);
let damage_tracker = OutputDamageTracker::from_output(&output);
let render_metrics = RenderMetrics::from_environment(output.name());
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
Ok(DirectOutput {
internal: internal_connector(connector.interface()),
connector: connector.handle(),
mode,
settings: settings.clone(),
@@ -1090,6 +1262,25 @@ mod tests {
use super::*;
#[test]
fn lid_only_hides_panels_with_a_usable_external_output() {
assert!(lid_hides_panel(true, true));
assert!(!lid_hides_panel(false, true));
assert!(!lid_hides_panel(true, false));
assert!(!lid_hides_panel(false, false));
}
#[test]
fn panel_detection_uses_connector_type_not_monitor_name() {
assert!(internal_connector(
connector::Interface::EmbeddedDisplayPort
));
assert!(internal_connector(connector::Interface::LVDS));
assert!(internal_connector(connector::Interface::DSI));
assert!(!internal_connector(connector::Interface::HDMIA));
assert!(!internal_connector(connector::Interface::DisplayPort));
}
#[test]
fn connector_identity_hash_is_stable() {
assert_eq!(stable_hash(b"hello"), 0xa430_d846_80aa_bd0b);
+8
View File
@@ -167,6 +167,8 @@ impl Default for InactiveDimConfig {
#[derive(Debug, Default, Deserialize)]
struct ThemeConfig {
#[serde(default)]
background: crate::wallpaper::WallpaperConfig,
#[serde(default)]
surface: ThemeSurfaceConfig,
#[serde(default)]
@@ -179,6 +181,12 @@ struct ThemeConfig {
material: ThemeMaterialConfig,
}
impl Config {
pub(crate) fn wallpaper_settings(&self) -> crate::wallpaper::WallpaperConfig {
self.theme.background.clone()
}
}
#[derive(Debug, Default, Deserialize)]
struct ThemeSurfaceConfig {
#[serde(default)]
+41 -1
View File
@@ -18,6 +18,30 @@ use smithay::{
use super::{layer_shell, xdg_shell};
use crate::{Ferese, state::ClientState};
fn layer_affects_backdrop(layer: smithay::wayland::shell::wlr_layer::Layer) -> bool {
matches!(
layer,
smithay::wayland::shell::wlr_layer::Layer::Background
| smithay::wayland::shell::wlr_layer::Layer::Bottom
)
}
impl Ferese {
fn surface_affects_backdrop(&self, surface: &WlSurface) -> bool {
let mut root = surface.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
if matches!(&self.cursor_status, smithay::input::pointer::CursorImageStatus::Surface(cursor) if cursor == &root)
{
return false;
}
self.space
.layer_for_surface(&root, smithay::desktop::WindowSurfaceType::ALL)
.is_none_or(|layer| layer_affects_backdrop(layer.cached_state().layer))
}
}
impl CompositorHandler for Ferese {
fn compositor_state(&mut self) -> &mut CompositorState {
&mut self.compositor_state
@@ -31,7 +55,10 @@ impl CompositorHandler for Ferese {
}
fn commit(&mut self, surface: &WlSurface) {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.capture_resize_before_commit(surface);
if self.surface_affects_backdrop(surface) {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
}
on_commit_buffer_handler::<Self>(surface);
if !is_sync_subsurface(surface) {
let mut root = surface.clone();
@@ -77,6 +104,19 @@ impl CompositorHandler for Ferese {
}
}
#[cfg(test)]
mod tests {
use super::*;
use smithay::wayland::shell::wlr_layer::Layer;
#[test]
fn panel_commits_do_not_invalidate_their_own_backdrop() {
assert!(!layer_affects_backdrop(Layer::Top));
assert!(!layer_affects_backdrop(Layer::Overlay));
assert!(layer_affects_backdrop(Layer::Bottom));
assert!(layer_affects_backdrop(Layer::Background));
}
}
impl BufferHandler for Ferese {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
}
+14 -4
View File
@@ -4,7 +4,7 @@ use smithay::{
backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
TouchEvent,
Switch, SwitchState, SwitchToggleEvent, TouchEvent,
},
input::{
keyboard::{FilterResult, keysyms},
@@ -27,6 +27,9 @@ impl Ferese {
self.idle_notifier_state.notify_activity(&seat);
match event {
InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => {
crate::backends::direct::set_lid_closed(self, event.state() == SwitchState::On);
}
InputEvent::Keyboard { event, .. } => {
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
let keycode = event.key_code();
@@ -339,12 +342,14 @@ impl Ferese {
}
if let Some(window) = self.window_under_visual(position) {
self.focused_window = self.window_ids.get(&window).copied();
if let Some(focused) = self.focused_window
let focused = self.window_ids.get(&window).copied();
if let Some(focused) = focused
&& let Err(error) = self.workspaces.focus_window(focused)
{
tracing::error!(%error, ?focused, "failed to update workspace focus");
return;
}
self.focused_window = focused;
self.raise_window(&window, true);
let surface = window
.toplevel()
@@ -481,7 +486,12 @@ impl Ferese {
BindingAction::Spawn(mut argv) => {
let program = argv.remove(0);
match Command::new(&program).args(argv).spawn() {
match Command::new(&program)
.args(argv)
.env("WAYLAND_DISPLAY", &self.socket_name)
.env_remove("WAYLAND_SOCKET")
.spawn()
{
Ok(child) => {
tracing::info!(%program, pid = child.id(), "spawned binding command")
}
+10 -4
View File
@@ -11,10 +11,13 @@ mod input;
mod ipc;
mod metrics;
mod overview;
mod presentation;
mod private_client;
mod resize_transaction;
mod shell_control;
mod stacking;
mod state;
mod wallpaper;
mod window_rules;
mod winit;
@@ -68,14 +71,11 @@ fn main() -> Result<(), Box<dyn Error>> {
spring_config: config.spring_config()?,
viewport_spring_config: config.viewport_spring_config()?,
output_profiles,
wallpaper: config.wallpaper_settings(),
};
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
backends::init(launch.backend, &mut event_loop, &mut state)?;
// SAFETY: the backend has already read the host display and Ferese is
// single-threaded before the event loop and optional child start.
unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) };
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
let result = event_loop.run(None, &mut state, |state| {
@@ -118,6 +118,12 @@ fn spawn_client(
let mut command = Command::new(&program);
command.args(args);
// Wallpaper decoding and drivers may already have worker threads. Set
// the child's display explicitly instead of mutating the process env.
command.env("WAYLAND_DISPLAY", &state.socket_name);
if state.wallpaper.owns_background() {
command.env("FERESE_COMPOSITOR_WALLPAPER", "1");
}
let private_connection = if capabilities.is_empty() {
None
} else {
+30
View File
@@ -16,6 +16,7 @@ pub(crate) struct RenderMetrics {
damaged_pixels: u64,
total_render_time: Duration,
longest_render_time: Duration,
frame_times: Vec<Duration>,
}
impl RenderMetrics {
@@ -36,6 +37,7 @@ impl RenderMetrics {
damaged_pixels: 0,
total_render_time: Duration::ZERO,
longest_render_time: Duration::ZERO,
frame_times: Vec::new(),
}
}
@@ -54,6 +56,10 @@ impl RenderMetrics {
self.damaged_pixels += damage.iter().map(rectangle_area).sum::<u64>();
self.total_render_time += render_time;
self.longest_render_time = self.longest_render_time.max(render_time);
if self.frame_times.len() == 512 {
self.frame_times.remove(0);
}
self.frame_times.push(render_time);
let now = Instant::now();
let elapsed = now.saturating_duration_since(self.interval_started);
@@ -66,6 +72,17 @@ impl RenderMetrics {
.as_micros()
.checked_div(u128::from(self.rendered_frames))
.unwrap_or(0);
self.frame_times.sort_unstable();
let percentile = |percent: usize| {
let index = (self.frame_times.len() * percent)
.div_ceil(100)
.saturating_sub(1);
self.frame_times
.get(index)
.copied()
.unwrap_or_default()
.as_micros()
};
tracing::info!(
target: "ferese::render",
output = %self.output,
@@ -74,6 +91,8 @@ impl RenderMetrics {
damaged_pixels = self.damaged_pixels,
average_render_us,
longest_render_us = self.longest_render_time.as_micros(),
p95_render_us = percentile(95),
p99_render_us = percentile(99),
missed_deadlines,
"render performance"
);
@@ -83,6 +102,7 @@ impl RenderMetrics {
self.damaged_pixels = 0;
self.total_render_time = Duration::ZERO;
self.longest_render_time = Duration::ZERO;
self.frame_times.clear();
}
}
@@ -128,5 +148,15 @@ mod tests {
assert_eq!(metrics.damaged_pixels, 5_200);
assert_eq!(metrics.total_render_time, Duration::from_millis(2));
assert_eq!(metrics.longest_render_time, Duration::from_millis(2));
assert_eq!(metrics.frame_times, vec![Duration::from_millis(2)]);
}
#[test]
fn profiling_samples_are_bounded() {
let mut metrics = RenderMetrics::new("test", true, Instant::now());
for _ in 0..2000 {
metrics.record_frame(Duration::from_millis(1), &[], 0, FrameEffectMetrics);
}
assert_eq!(metrics.frame_times.len(), 512);
}
}
+3
View File
@@ -209,6 +209,9 @@ impl Ferese {
y: f64,
) -> Option<(f64, f64)> {
let geometry = self.window_geometry.get(&id)?;
if !self.overview.is_presenting() {
return Some((x - geometry.visual.current.x, y - geometry.visual.current.y));
}
let source = geometry.client.committed_size?;
let presented = self.overview.presented_rect(id, geometry.visual.current);
+309
View File
@@ -0,0 +1,309 @@
//! Presentation geometry is rounded once, in output pixels, never first in
//! logical pixels. Content, clips and decorations share those exact edges.
use std::time::{Duration, Instant};
use smithay::{
backend::renderer::{
element::{Element, Id, Kind, RenderElement},
gles::{
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
element::PixelShaderElement,
},
utils::{CommitCounter, DamageSet},
},
utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform},
};
pub(crate) const HANDOFF: Duration = Duration::from_millis(80);
pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024;
pub(crate) fn frame_delta(last: &mut Instant, now: Instant) -> Duration {
let delta = now.saturating_duration_since(*last);
*last = (*last).max(now);
delta
}
pub(crate) fn physical_rect(
rect: ferese_layout::Rect,
origin: Point<i32, Logical>,
scale: f64,
) -> Rectangle<i32, Physical> {
let left = ((rect.x - f64::from(origin.x)) * scale).round() as i32;
let top = ((rect.y - f64::from(origin.y)) * scale).round() as i32;
let right = ((rect.x + rect.width - f64::from(origin.x)) * scale).round() as i32;
let bottom = ((rect.y + rect.height - f64::from(origin.y)) * scale).round() as i32;
Rectangle::new(
(left, top).into(),
((right - left).max(1), (bottom - top).max(1)).into(),
)
}
pub(crate) fn handoff_alpha(elapsed: Duration) -> f32 {
let progress = (elapsed.as_secs_f64() / HANDOFF.as_secs_f64()).clamp(0.0, 1.0);
(1.0 - progress * progress * (3.0 - 2.0 * progress)) as f32
}
pub(crate) fn resize_needs_old_frame(
visual: ferese_layout::Rect,
target: ferese_layout::Rect,
buffer_width: i32,
buffer_height: i32,
) -> bool {
// Some clients round their configured dimensions to a character grid.
// Do not retain a snapshot forever for that permanent size difference.
visual.width > target.width.max(f64::from(buffer_width)) + 0.5
|| visual.height > target.height.max(f64::from(buffer_height)) + 0.5
}
pub(crate) fn snapshot_covers_source(
snapshot: smithay::utils::Size<i32, Buffer>,
source: smithay::utils::Size<i32, Physical>,
snapshot_scale: f64,
scale: f64,
) -> bool {
(snapshot_scale - scale).abs() < 0.001 && snapshot.w >= source.w && snapshot.h >= source.h
}
pub(crate) fn advance_handoff(
elapsed: &mut Duration,
last: &mut Option<Duration>,
now: Duration,
blocked: bool,
speed: f64,
) -> bool {
if blocked {
// A retarget must not consume its client's waiting time as fade time.
*last = None;
} else {
if let Some(previous) = last.replace(now) {
*elapsed += now.saturating_sub(previous).mul_f64(speed);
}
}
*elapsed < HANDOFF
}
/// A shader canvas whose damage/paint destination uses the shared pixel edges.
#[derive(Clone, Debug)]
pub(crate) struct PhysicalShaderElement {
pub inner: PixelShaderElement,
pub geometry: Rectangle<i32, Physical>,
}
impl Element for PhysicalShaderElement {
fn id(&self) -> &Id {
self.inner.id()
}
fn current_commit(&self) -> CommitCounter {
self.inner.current_commit()
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.inner.src()
}
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry
}
fn damage_since(
&self,
_: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
if commit == Some(self.current_commit()) {
DamageSet::default()
} else {
DamageSet::from_slice(&[Rectangle::from_size(self.geometry.size)])
}
}
}
impl RenderElement<GlesRenderer> for PhysicalShaderElement {
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
self.inner.draw(frame, src, dst, damage, opaque)
}
}
/// Stable native-pixel texture element. Unlike a resized client buffer, an old
/// frame is only translated/cropped; it never changes the size of its glyphs.
#[derive(Clone, Debug)]
pub(crate) struct NativeTextureElement {
pub id: Id,
pub commit: CommitCounter,
pub texture: GlesTexture,
pub geometry: Rectangle<i32, Physical>,
pub source: Rectangle<f64, Buffer>,
pub alpha: f32,
pub program: Option<GlesTexProgram>,
pub uniforms: Vec<Uniform<'static>>,
}
impl Element for NativeTextureElement {
fn id(&self) -> &Id {
&self.id
}
fn current_commit(&self) -> CommitCounter {
self.commit
}
fn src(&self) -> Rectangle<f64, Buffer> {
self.source
}
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
self.geometry
}
fn alpha(&self) -> f32 {
self.alpha
}
fn kind(&self) -> Kind {
Kind::Unspecified
}
fn damage_since(
&self,
_: Scale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
if commit == Some(self.commit) {
DamageSet::default()
} else {
DamageSet::from_slice(&[Rectangle::from_size(self.geometry.size)])
}
}
}
impl RenderElement<GlesRenderer> for NativeTextureElement {
fn draw(
&self,
frame: &mut GlesFrame<'_, '_>,
src: Rectangle<f64, Buffer>,
dst: Rectangle<i32, Physical>,
damage: &[Rectangle<i32, Physical>],
opaque: &[Rectangle<i32, Physical>],
) -> Result<(), GlesError> {
frame.render_texture_from_to(
&self.texture,
src,
dst,
damage,
opaque,
Transform::Normal,
self.alpha,
self.program.as_ref(),
&self.uniforms,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reversing_a_grow_replaces_the_smaller_snapshot() {
assert!(!snapshot_covers_source(
(500, 600).into(),
(1000, 600).into(),
1.0,
1.0
));
assert!(snapshot_covers_source(
(1000, 600).into(),
(500, 600).into(),
1.0,
1.0
));
assert!(!snapshot_covers_source(
(1000, 600).into(),
(500, 600).into(),
1.0,
1.8
));
}
#[test]
fn shrinking_keeps_old_pixels_until_destination_covers_bounds() {
let target = ferese_layout::Rect::new(0.0, 0.0, 500.0, 600.0);
let visual = ferese_layout::Rect::new(0.0, 0.0, 750.0, 600.0);
assert!(resize_needs_old_frame(visual, target, 500, 600));
assert!(!resize_needs_old_frame(target, target, 500, 600));
// Character-grid clients must still release the bounded snapshot.
assert!(!resize_needs_old_frame(target, target, 492, 592));
let growing_target = ferese_layout::Rect::new(0.0, 0.0, 1000.0, 600.0);
assert!(!resize_needs_old_frame(visual, growing_target, 1000, 600));
}
#[test]
fn fresh_snapshot_and_retarget_wait_do_not_skip_the_handoff() {
let mut elapsed = Duration::ZERO;
let mut last = None;
assert!(advance_handoff(
&mut elapsed,
&mut last,
Duration::from_secs(5),
false,
1.0
));
assert_eq!(elapsed, Duration::ZERO);
assert!(advance_handoff(
&mut elapsed,
&mut last,
Duration::from_millis(5010),
false,
1.0
));
assert_eq!(elapsed, Duration::from_millis(10));
advance_handoff(&mut elapsed, &mut last, Duration::from_secs(6), true, 1.0);
advance_handoff(&mut elapsed, &mut last, Duration::from_secs(7), false, 1.0);
assert_eq!(elapsed, Duration::from_millis(10));
assert!(!advance_handoff(
&mut elapsed,
&mut last,
Duration::from_millis(7070),
false,
1.0
));
}
#[test]
fn mixed_refresh_outputs_do_not_double_the_animation_clock() {
let start = Instant::now();
let mut last = start;
let mut elapsed = Duration::ZERO;
for ms in [8, 16, 16, 24, 32, 32] {
elapsed += frame_delta(&mut last, start + Duration::from_millis(ms));
}
assert_eq!(elapsed, Duration::from_millis(32));
assert_eq!(frame_delta(&mut last, start), Duration::ZERO);
assert_eq!(last, start + Duration::from_millis(32));
}
#[test]
fn fractional_motion_does_not_quantize_to_logical_pixels() {
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2);
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).loc, (1, 1).into());
assert_eq!(
physical_rect(rect, (0, 0).into(), 1.8).size,
(180, 144).into()
);
}
#[test]
fn adjacent_frames_share_edges_at_all_output_scales() {
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
let left = physical_rect(
ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0),
(0, 0).into(),
scale,
);
let right = physical_rect(
ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0),
(0, 0).into(),
scale,
);
assert_eq!(left.loc.x + left.size.w, right.loc.x);
}
}
#[test]
fn handoff_has_bounded_lifetime_and_no_alpha_jump() {
assert_eq!(handoff_alpha(Duration::ZERO), 1.0);
assert_eq!(handoff_alpha(HANDOFF), 0.0);
assert_eq!(handoff_alpha(Duration::from_secs(1)), 0.0);
assert!(handoff_alpha(Duration::from_millis(1)) > 0.99);
}
}
+78
View File
@@ -0,0 +1,78 @@
//! A presentation barrier, not an event-loop or client-commit blocker.
//! Grouping barriers by workspace keeps viewport and neighboring tiles on the
//! same clock. A serial identifies readiness even for cell-grid clients whose
//! committed size is smaller than the configure, and after rapid reversals.
use std::time::Duration;
use smithay::utils::{Logical, Rectangle, Serial};
const DEADLINE: Duration = Duration::from_millis(300);
#[derive(Clone, Copy, Debug)]
pub(crate) struct ResizeTransaction {
serial: Serial,
started: Duration,
source_geometry: Option<Rectangle<i32, Logical>>,
}
impl ResizeTransaction {
pub(crate) fn new(serial: Serial, started: Duration) -> Self {
Self {
serial,
started,
source_geometry: None,
}
}
pub(crate) fn with_source_geometry(mut self, geometry: Rectangle<i32, Logical>) -> Self {
self.source_geometry = Some(geometry);
self
}
pub(crate) fn source_geometry(self) -> Option<Rectangle<i32, Logical>> {
self.source_geometry
}
pub(crate) fn accepts(self, committed: Option<Serial>) -> bool {
committed.is_some_and(|serial| serial >= self.serial)
}
pub(crate) fn expired(self, now: Duration) -> bool {
now.saturating_sub(self.started) >= DEADLINE
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acknowledgement_alone_or_stale_commit_does_not_release_resize() {
let transaction = ResizeTransaction::new(12.into(), Duration::ZERO);
assert!(!transaction.accepts(None));
assert!(!transaction.accepts(Some(11.into())));
assert!(transaction.accepts(Some(12.into())));
assert!(transaction.accepts(Some(13.into())));
}
#[test]
fn serial_comparison_handles_wraparound() {
let transaction = ResizeTransaction::new(u32::MAX.into(), Duration::ZERO);
assert!(transaction.accepts(Some(0.into())));
assert!(!transaction.accepts(Some((u32::MAX - 1).into())));
}
#[test]
fn unresponsive_client_has_bounded_wait() {
let transaction = ResizeTransaction::new(1.into(), Duration::from_millis(10));
assert!(!transaction.expired(Duration::from_millis(309)));
assert!(transaction.expired(Duration::from_millis(310)));
}
#[test]
fn shrinking_commit_retains_the_previous_full_frame_geometry() {
let source = Rectangle::new((4, 8).into(), (1200, 800).into());
let transaction =
ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source);
assert_eq!(transaction.source_geometry(), Some(source));
}
}
+257 -10
View File
@@ -64,7 +64,7 @@ use smithay::{
shell::wlr_layer::Layer,
shell::wlr_layer::WlrLayerShellState,
shell::xdg::decoration::XdgDecorationState,
shell::xdg::{SurfaceCachedState, XdgShellState},
shell::xdg::{SurfaceCachedState, XdgShellState, XdgToplevelSurfaceData},
shm::ShmState,
single_pixel_buffer::SinglePixelBufferState,
socket::ListeningSocketSource,
@@ -118,11 +118,16 @@ pub struct Ferese {
output_identity_ids: HashMap<String, OutputId>,
pub window_ids: HashMap<Window, WindowId>,
pub window_geometry: HashMap<WindowId, WindowGeometry>,
resize_transactions: HashMap<WindowId, crate::resize_transaction::ResizeTransaction>,
pub(crate) resize_snapshots: HashMap<WindowId, crate::winit::ResizeSnapshot>,
pub(crate) nested_backend: Option<crate::winit::NestedBackend>,
pub(crate) wallpaper: crate::wallpaper::WallpaperState,
maximized_windows: HashSet<WindowId>,
maximized_column_widths: HashMap<WindowId, ColumnWidth>,
window_stack: crate::stacking::WindowStack,
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
pub(crate) window_dims: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
pub(crate) window_resize_fills: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
pub(crate) window_dimming: HashMap<WindowId, crate::dimming::DimAnimation>,
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
@@ -203,6 +208,7 @@ pub struct Ferese {
}
pub struct RuntimeConfig {
pub(crate) wallpaper: crate::wallpaper::WallpaperConfig,
pub layout_mode: LayoutMode,
pub gap_config: GapConfig,
pub input_settings: InputSettings,
@@ -321,6 +327,15 @@ impl Ferese {
.find_map(|(output, id)| (*id == focused).then_some(output))
}
pub(crate) fn restore_output_focus(&mut self) {
if let Some(output) = self.output_workspaces.focused_output()
&& let Some(workspace) = self.output_workspaces.active_workspace(output)
{
self.activate_output_workspace(output, workspace);
self.restore_keyboard_focus();
}
}
pub(crate) fn output_id(&self, output: &Output) -> Option<OutputId> {
self.output_ids.get(output).copied()
}
@@ -340,12 +355,14 @@ impl Ferese {
let Some(output_id) = self.output_ids.get(&output).copied() else {
return;
};
if self.output_workspaces.focused_output() == Some(output_id) {
return;
}
let Some(workspace) = self.output_workspaces.active_workspace(output_id) else {
return;
};
if self.output_workspaces.focused_output() == Some(output_id)
&& self.workspaces.active_id() == workspace
{
return;
}
self.activate_output_workspace(output_id, workspace);
self.relayout();
@@ -453,11 +470,16 @@ impl Ferese {
output_identity_ids: HashMap::new(),
window_ids: HashMap::new(),
window_geometry: HashMap::new(),
resize_transactions: HashMap::new(),
resize_snapshots: HashMap::new(),
nested_backend: None,
wallpaper: crate::wallpaper::WallpaperState::new(config.wallpaper),
maximized_windows: HashSet::new(),
maximized_column_widths: HashMap::new(),
window_stack: crate::stacking::WindowStack::default(),
window_borders: HashMap::new(),
window_dims: HashMap::new(),
window_resize_fills: HashMap::new(),
window_dimming: HashMap::new(),
window_shadows: HashMap::new(),
rounded_clip_programs: HashMap::new(),
@@ -596,8 +618,12 @@ impl Ferese {
return None;
}
let workspace = self.workspace_under_pointer(position)?;
self.space.elements().rev().find_map(|window| {
let id = self.window_ids.get(window)?;
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
return None;
}
let visual = self.presented_window_rect(*id)?;
let inside_visual = position.x >= visual.x
&& position.y >= visual.y
@@ -666,8 +692,12 @@ impl Ferese {
}
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
let workspace = self.workspace_under_pointer(position)?;
self.space.elements().rev().find_map(|window| {
let id = self.window_ids.get(window)?;
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
return None;
}
let visual = self.presented_window_rect(*id)?;
(position.x >= visual.x
@@ -678,6 +708,19 @@ impl Ferese {
})
}
fn workspace_under_pointer(&self, position: Point<f64, Logical>) -> Option<WorkspaceId> {
let output = self.space.output_under(position).next()?;
self.output_workspaces
.active_workspace(self.output_id(output)?)
}
pub(crate) fn window_belongs_to_output(&self, window: WindowId, output: &Output) -> bool {
let workspace = self
.output_id(output)
.and_then(|output| self.output_workspaces.active_workspace(output));
workspace.is_some() && self.workspaces.workspace_for_window(window) == workspace
}
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
if let Some(id) = self.window_ids.get(window) {
self.window_stack.raise(*id);
@@ -704,6 +747,9 @@ impl Ferese {
}
pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> {
if !self.overview.is_presenting() {
return Some((1.0, 1.0));
}
let id = self.window_ids.get(window)?;
let geometry = self.window_geometry.get(id)?;
let source = geometry.client.committed_size?;
@@ -916,6 +962,9 @@ impl Ferese {
self.space.unmap_elem(window);
self.window_geometry.remove(&id);
self.resize_transactions.remove(&id);
self.resize_snapshots.remove(&id);
self.window_resize_fills.remove(&id);
self.maximized_windows.remove(&id);
self.maximized_column_widths.remove(&id);
self.window_stack.remove(id);
@@ -939,7 +988,9 @@ impl Ferese {
}
pub fn relayout(&mut self) {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
// Consume idle time before setting new targets; it must not become a
// large first animation step after a keypress on an idle desktop.
self.advance_animations(Instant::now());
self.arrange_layers();
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles);
@@ -1071,6 +1122,9 @@ impl Ferese {
.map(|(window, _)| window.clone())
.collect::<Vec<_>>();
let mut layout_changed = hidden
.iter()
.any(|window| self.space.element_location(window).is_some());
for window in hidden {
self.space.unmap_elem(&window);
}
@@ -1100,6 +1154,7 @@ impl Ferese {
} else {
PresentationMode::Normal
};
layout_changed |= !had_geometry || geometry.logical != rect;
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
if !self.animations_enabled {
geometry.advance(Duration::ZERO, self.spring_config, false);
@@ -1198,11 +1253,24 @@ impl Ferese {
});
if requested_size.is_some() || state_changed {
toplevel.send_pending_configure();
if let Some(serial) = toplevel.send_pending_configure()
&& requested_size.is_some()
&& had_geometry
&& self.animations_enabled
{
self.resize_transactions.insert(
id,
crate::resize_transaction::ResizeTransaction::new(serial, now)
.with_source_geometry(window.geometry()),
);
}
}
}
}
self.scrolling_world_x = scrolling_world_x;
if layout_changed {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
}
self.sync_window_stacking();
self.retarget_overview();
self.send_shell_snapshots();
@@ -1211,8 +1279,7 @@ impl Ferese {
}
pub fn advance_animations(&mut self, now: Instant) -> bool {
let delta = now.saturating_duration_since(self.last_animation_tick);
self.last_animation_tick = now;
let delta = crate::presentation::frame_delta(&mut self.last_animation_tick, now);
self.advance_animations_by(delta)
}
@@ -1227,8 +1294,11 @@ impl Ferese {
.direct_backend
.as_mut()
.and_then(|backend| backend.record_presentation(node, crtc, time, sequence));
if let Some(delta) = delta {
self.advance_animations_by(delta);
if delta.is_some() {
// All outputs animate the same scene. Summing each CRTC's frame
// interval makes motion run faster with two monitors (and uneven
// with mixed 60/120 Hz). Advance once by actual elapsed time.
self.advance_animations(Instant::now());
}
}
@@ -1287,10 +1357,73 @@ impl Ferese {
self.send_window_close(id);
}
let now = self.start_time.elapsed();
self.resize_transactions.retain(|id, transaction| {
if transaction.expired(now) {
tracing::warn!(?id, "resize presentation deadline reached");
false
} else {
true
}
});
let blocked_workspaces = self
.resize_transactions
.keys()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
let animations_enabled = self.animations_enabled;
let animation_speed = self.animation_speed;
self.resize_snapshots.retain(|id, snapshot| {
if !animations_enabled {
return false;
}
let waiting_for_client = self
.workspaces
.workspace_for_window(*id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace));
// A shrinking client's destination buffer arrives before the
// animated bounds reach it. Keep the old native pixels covering
// that strip rather than fading them into the neutral resize fill.
let uncovered = self.window_geometry.get(id).is_some_and(|geometry| {
geometry.client.committed_size.is_some_and(|size| {
crate::presentation::resize_needs_old_frame(
geometry.visual.current,
geometry.logical,
size.width,
size.height,
)
})
});
let blocked = waiting_for_client || uncovered;
let active = crate::presentation::advance_handoff(
&mut snapshot.elapsed,
&mut snapshot.last_tick,
now,
blocked,
animation_speed,
);
if !blocked {
snapshot.commit.increment();
}
if !active {
tracing::debug!(
?id,
bytes = snapshot.bytes(),
"released resize handoff snapshot"
);
}
active
});
active_animation |= !self.resize_snapshots.is_empty();
// Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= !blocked_workspaces.is_empty();
for (workspace, viewport) in &mut self.viewport_animations {
if !visible_workspaces.contains(workspace) {
continue;
}
if blocked_workspaces.contains(workspace) {
continue;
}
if self.animations_enabled {
active_animation |= viewport.advance(delta, self.viewport_spring_config);
} else {
@@ -1300,6 +1433,13 @@ impl Ferese {
let mut settled_coupled_widths = Vec::new();
for (window, id) in windows {
if self
.workspaces
.workspace_for_window(id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace))
{
continue;
}
let Some(geometry) = self.window_geometry.get_mut(&id) else {
continue;
};
@@ -1390,10 +1530,115 @@ impl Ferese {
self.animations_enabled
}
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
let Some((window, id)) = self
.window_ids
.iter()
.find(|(window, _)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
})
.map(|(window, id)| (window.clone(), *id))
else {
return;
};
let Some(transaction) = self.resize_transactions.get(&id) else {
return;
};
let serial = with_states(surface, |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
});
if !transaction.accepts(serial) {
return;
}
let Some(output) = self
.space
.outputs()
.find(|output| self.window_belongs_to_output(id, output))
.cloned()
else {
return;
};
let source_geometry = transaction
.source_geometry()
.unwrap_or_else(|| window.geometry());
let scale = output.current_scale().fractional_scale();
let source_size = source_geometry.size.to_physical_precise_round(scale);
if self.resize_snapshots.get(&id).is_some_and(|snapshot| {
crate::presentation::snapshot_covers_source(
smithay::backend::renderer::Texture::size(&snapshot.texture),
source_size,
snapshot.scale,
scale,
)
}) {
return;
}
let used: usize = self
.resize_snapshots
.values()
.map(|snapshot| snapshot.bytes())
.sum();
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
let result = if let Some(backend) = &self.nested_backend {
// Commit dispatch does not run inside the Winit event callback;
// still never risk reentrant renderer access or a compositor panic.
match backend.try_borrow_mut() {
Ok(mut backend) => crate::winit::capture_resize_snapshot(
backend.renderer(),
&window,
source_geometry,
output.current_scale().fractional_scale(),
remaining,
),
Err(_) => return,
}
} else if let Some(backend) = &mut self.direct_backend {
backend.capture_resize_snapshot(&window, source_geometry, &output, remaining)
} else {
return;
};
match result {
Ok(Some(snapshot)) => {
tracing::debug!(
?id,
bytes = snapshot.bytes(),
"captured native resize handoff"
);
self.resize_snapshots.insert(id, snapshot);
}
Ok(None) => {}
Err(error) => {
tracing::debug!(%error, ?id, "resize snapshot unavailable; using native crop")
}
}
}
pub fn record_client_commit(&mut self, window: &Window) {
let Some(id) = self.window_ids.get(window) else {
return;
};
// current_serial is promoted by Smithay on commit, unlike
// configure_serial, which only records an acknowledgement.
let committed_serial = window.toplevel().and_then(|toplevel| {
with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
})
});
if self
.resize_transactions
.get(id)
.is_some_and(|transaction| transaction.accepts(committed_serial))
{
self.resize_transactions.remove(id);
}
let Some(size) = client_size(window) else {
return;
};
@@ -1869,6 +2114,8 @@ impl Ferese {
// Direct manipulation follows the pointer, including while the client is
// still drawing its next buffer. Rendering and hit testing share this rect.
self.resize_transactions.remove(&id);
self.resize_snapshots.remove(&id);
let geometry = self
.window_geometry
.entry(id)
+227
View File
@@ -0,0 +1,227 @@
//! One decoded image and one texture per GPU context, not double-buffered
//! full-screen UI surfaces per monitor. Resize changes sampling only.
use crate::presentation::NativeTextureElement;
use serde::Deserialize;
use smithay::{
backend::{
allocator::Fourcc,
renderer::{
ErasedContextId, ImportMem, Renderer,
element::Id,
gles::{GlesRenderer, GlesTexture},
utils::CommitCounter,
},
},
output::Output,
utils::{Buffer, Physical, Rectangle, Size},
};
use std::{collections::HashMap, path::PathBuf, sync::mpsc};
#[derive(Clone, Debug, Default, Deserialize)]
pub(crate) struct WallpaperConfig {
pub path: Option<PathBuf>,
#[serde(default)]
pub mode: WallpaperMode,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub(crate) enum WallpaperMode {
#[default]
Fill,
Fit,
}
struct WallpaperTexture {
texture: GlesTexture,
id: Id,
}
pub(crate) struct WallpaperState {
owned: bool,
mode: WallpaperMode,
receiver: Option<mpsc::Receiver<Result<image::RgbaImage, String>>>,
pixels: Option<image::RgbaImage>,
textures: HashMap<ErasedContextId, WallpaperTexture>,
}
impl WallpaperState {
pub fn new(config: WallpaperConfig) -> Self {
let (sender, receiver) = mpsc::channel();
let owned = config.path.as_ref().is_some_and(|path| path.is_file());
if owned {
let path = config.path.unwrap();
std::thread::spawn(move || {
let load = || -> Result<image::RgbaImage, String> {
let (width, height) =
image::image_dimensions(&path).map_err(|e| e.to_string())?;
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
return Err("wallpaper exceeds the 256 MiB decode limit".into());
}
image::open(path)
.map(|image| image.into_rgba8())
.map_err(|e| e.to_string())
};
let _ = sender.send(load());
});
}
Self {
owned,
mode: config.mode,
receiver: owned.then_some(receiver),
pixels: None,
textures: HashMap::new(),
}
}
pub fn owns_background(&self) -> bool {
self.owned
}
pub fn forget_context(&mut self, context: &ErasedContextId) {
self.textures.remove(context);
}
pub fn poll(&mut self) -> bool {
let Some(receiver) = &self.receiver else {
return false;
};
match receiver.try_recv() {
Ok(result) => {
self.receiver = None;
match result {
Ok(pixels) => {
tracing::info!(
width = pixels.width(),
height = pixels.height(),
bytes = pixels.as_raw().len(),
"decoded compositor wallpaper once"
);
self.pixels = Some(pixels);
}
Err(error) => {
tracing::warn!(%error, "wallpaper unavailable; using output clear color")
}
}
true
}
Err(mpsc::TryRecvError::Disconnected) => {
self.receiver = None;
false
}
Err(mpsc::TryRecvError::Empty) => false,
}
}
pub fn element(
&mut self,
renderer: &mut GlesRenderer,
output: &Output,
) -> Option<NativeTextureElement> {
let pixels = self.pixels.as_ref()?;
let context = renderer.context_id().erased();
if !self.textures.contains_key(&context) {
let size = Size::from((pixels.width() as i32, pixels.height() as i32));
match renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false) {
Ok(texture) => {
tracing::debug!(
bytes = pixels.as_raw().len(),
"uploaded shared wallpaper texture"
);
self.textures.insert(
context.clone(),
WallpaperTexture {
texture,
id: Id::new(),
},
);
}
Err(error) => {
tracing::debug!(%error, "wallpaper texture import failed");
return None;
}
}
}
let cached = self.textures.get(&context)?;
let output_size = output
.current_transform()
.transform_size(output.current_mode()?.size);
let (geometry, source) = image_geometry(
(pixels.width() as i32, pixels.height() as i32).into(),
output_size,
self.mode,
);
Some(NativeTextureElement {
id: cached.id.clone(),
commit: CommitCounter::default(),
texture: cached.texture.clone(),
geometry,
source,
alpha: 1.0,
program: None,
uniforms: vec![],
})
}
}
fn image_geometry(
image: Size<i32, Buffer>,
output: Size<i32, Physical>,
mode: WallpaperMode,
) -> (Rectangle<i32, Physical>, Rectangle<f64, Buffer>) {
let sx = f64::from(output.w) / f64::from(image.w);
let sy = f64::from(output.h) / f64::from(image.h);
match mode {
WallpaperMode::Fill => {
let scale = sx.max(sy);
let width = f64::from(output.w) / scale;
let height = f64::from(output.h) / scale;
(
Rectangle::from_size(output),
Rectangle::new(
(
(f64::from(image.w) - width) / 2.0,
(f64::from(image.h) - height) / 2.0,
)
.into(),
(width, height).into(),
),
)
}
WallpaperMode::Fit => {
let scale = sx.min(sy);
let size: Size<i32, Physical> = (
(f64::from(image.w) * scale).round() as i32,
(f64::from(image.h) * scale).round() as i32,
)
.into();
(
Rectangle::new(
((output.w - size.w) / 2, (output.h - size.h) / 2).into(),
size,
),
Rectangle::from_size(image.to_f64()),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fill_crops_and_fit_letterboxes_without_reallocating_image() {
let (geometry, source) = image_geometry(
(3840, 2160).into(),
(1600, 1000).into(),
WallpaperMode::Fill,
);
assert_eq!(geometry.size, (1600, 1000).into());
assert_eq!(source.size.h, 2160.0);
assert!(source.loc.x > 0.0);
let (geometry, source) =
image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit);
assert_eq!(geometry.size, (1600, 900).into());
assert_eq!(geometry.loc, (0, 50).into());
assert_eq!(source.size, (3840.0, 2160.0).into());
}
#[test]
fn missing_wallpaper_preserves_external_shell_fallback() {
let state = WallpaperState::new(WallpaperConfig::default());
assert!(!state.owns_background());
}
}
+449 -169
View File
@@ -1,6 +1,8 @@
use std::{
cell::{Cell, RefCell},
collections::HashMap,
error::Error,
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
@@ -12,7 +14,7 @@ use smithay::{
backend::{
allocator::Fourcc,
renderer::{
Color32F, ErasedContextId, Frame, ImportDma, Offscreen, Renderer, Texture,
Bind, Color32F, ErasedContextId, Frame, ImportDma, Offscreen, Renderer, Texture,
damage::OutputDamageTracker,
element::{
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage,
@@ -43,7 +45,10 @@ use smithay::{
},
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
output::{Mode, Output, PhysicalProperties, Scale, Subpixel},
reexports::calloop::EventLoop,
reexports::calloop::{
EventLoop,
timer::{TimeoutAction, Timer},
},
reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind,
reexports::wayland_server::Resource,
utils::{
@@ -57,6 +62,7 @@ use smithay::{
use crate::{
Ferese,
metrics::{FrameEffectMetrics, RenderMetrics},
presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect},
};
type SurfaceRenderElement = CropRenderElement<
@@ -81,6 +87,85 @@ render_elements! {
Solid=SolidColorRenderElement,
Border=PixelShaderElement,
Blur=BlurRenderElement,
Effect=PhysicalShaderElement,
Native=NativeTextureElement,
}
pub(crate) type NestedBackend = Rc<RefCell<winit::WinitGraphicsBackend<GlesRenderer>>>;
#[derive(Debug)]
pub(crate) struct ResizeSnapshot {
pub texture: GlesTexture,
pub context: ErasedContextId,
pub id: Id,
pub commit: CommitCounter,
pub elapsed: Duration,
pub last_tick: Option<Duration>,
pub scale: f64,
}
impl ResizeSnapshot {
pub(crate) fn bytes(&self) -> usize {
self.texture.size().w as usize * self.texture.size().h as usize * 4
}
}
pub(crate) fn capture_resize_snapshot(
renderer: &mut GlesRenderer,
window: &smithay::desktop::Window,
geometry: Rectangle<i32, Logical>,
scale: f64,
remaining: usize,
) -> Result<Option<ResizeSnapshot>, GlesError> {
let Some(toplevel) = window.toplevel() else {
return Ok(None);
};
let size: Size<i32, Physical> = geometry.size.to_physical_precise_round(scale);
let bytes = (size.w.max(0) as usize)
.saturating_mul(size.h.max(0) as usize)
.saturating_mul(4);
if bytes == 0 || bytes > remaining {
return Ok(None);
}
// Called before on_commit_buffer_handler: renderer surface state still
// owns the previous buffer. Copy only during an actual resize handoff.
let content = render_elements_from_surface_tree::<
GlesRenderer,
WaylandSurfaceRenderElement<GlesRenderer>,
>(
renderer,
toplevel.wl_surface(),
Point::<i32, Logical>::from((-geometry.loc.x, -geometry.loc.y))
.to_physical_precise_round(scale),
scale,
1.0,
RenderElementKind::Unspecified,
);
if content.is_empty() {
return Ok(None);
}
let mut texture = Offscreen::<GlesTexture>::create_buffer(
renderer,
Fourcc::Abgr8888,
(size.w, size.h).into(),
)?;
{
let mut target = renderer.bind(&mut texture)?;
let mut frame = renderer.render(&mut target, size, Transform::Normal)?;
let damage = Rectangle::from_size(size);
frame.clear(Color32F::new(0.0, 0.0, 0.0, 0.0), &[damage])?;
draw_render_elements(&mut frame, scale, &content, &[damage])?;
let _ = frame.finish()?;
}
Ok(Some(ResizeSnapshot {
texture,
context: renderer.context_id().erased(),
id: Id::new(),
commit: CommitCounter::default(),
elapsed: Duration::ZERO,
last_tick: None,
scale,
}))
}
render_elements! {
@@ -664,7 +749,11 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
let initial_scale = normalized_scale(backend.scale_factor());
let mode = Mode {
size: backend.window_size(),
refresh: 60_000,
refresh: backend
.window()
.current_monitor()
.and_then(|monitor| monitor.refresh_rate_millihertz())
.unwrap_or(60_000) as i32,
};
let output = Output::new(
"ferese-winit".into(),
@@ -689,163 +778,192 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
let mut damage_tracker = OutputDamageTracker::from_output(&output);
let clock = Clock::<Monotonic>::new();
let mut sequence = 0_u64;
let mut missed_deadlines = 0_u64;
let mut output_scale = initial_scale;
let mut render_metrics = RenderMetrics::from_environment(output.name());
// Do not request a redraw recursively: a no-damage redraw has no EGL
// submission to pace it and otherwise spins at full CPU. Independently
// scheduled frames also let clients receive callbacks without damage.
let refresh = Rc::new(Cell::new(Duration::from_nanos(
1_000_000_000_000 / mode.refresh.max(1) as u64,
)));
let backend = Rc::new(RefCell::new(backend));
state.nested_backend = Some(backend.clone());
let redraw_backend = backend.clone();
let redraw_refresh = refresh.clone();
event_loop
.handle()
.insert_source(event_source, move |event, _, state| match event {
WinitEvent::Resized { size, scale_factor } => {
let scale = normalized_scale(scale_factor);
.insert_source(Timer::from_duration(refresh.get()), move |_, _, _| {
redraw_backend.borrow().window().request_redraw();
TimeoutAction::ToDuration(redraw_refresh.get())
})?;
output.change_current_state(
Some(Mode {
size,
refresh: 60_000,
}),
None,
Some(Scale::Fractional(scale)),
None,
);
event_loop
.handle()
.insert_source(event_source, move |event, _, state| {
let mut backend = backend.borrow_mut();
match event {
WinitEvent::Resized { size, scale_factor } => {
state.backdrop_generation = state.backdrop_generation.wrapping_add(1);
let scale = normalized_scale(scale_factor);
let rate = backend
.window()
.current_monitor()
.and_then(|monitor| monitor.refresh_rate_millihertz())
.unwrap_or(60_000) as i32;
refresh.set(Duration::from_nanos(1_000_000_000_000 / rate.max(1) as u64));
if scale != output_scale {
output_scale = scale;
state.update_fractional_scale(scale);
output.change_current_state(
Some(Mode {
size,
refresh: rate,
}),
None,
Some(Scale::Fractional(scale)),
None,
);
if scale != output_scale {
output_scale = scale;
state.update_fractional_scale(scale);
}
state.relayout();
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush output-resize configure");
}
backend.window().request_redraw();
}
state.relayout();
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush output-resize configure");
}
backend.window().request_redraw();
}
WinitEvent::Input(event) => state.process_input_event(event),
WinitEvent::Redraw => {
state.advance_animations(Instant::now());
let age = backend.buffer_age().unwrap_or(0);
let render_started = Instant::now();
let rendered = (|| -> DamageRenderResult {
{
let (renderer, mut framebuffer) = backend.bind()?;
state.process_dmabuf_imports(renderer);
let elements = animated_window_elements(state, renderer, &output);
let effects = frame_effect_metrics(
&elements,
output.current_scale().fractional_scale(),
);
let result = damage_tracker.render_output(
renderer,
&mut framebuffer,
age,
&elements,
[0.035, 0.04, 0.055, 1.0],
)?;
let cursorless_capture = state.has_pending_screencopy(&output, false);
let captured_with_cursor =
state.process_screencopies(renderer, &framebuffer, &output, true);
if cursorless_capture {
let cursorless_elements =
output_elements(state, renderer, &output, false);
redraw_output(
WinitEvent::Input(event) => state.process_input_event(event),
WinitEvent::Redraw => {
state.advance_animations(Instant::now());
let age = backend.buffer_age().unwrap_or(0);
let render_started = Instant::now();
let rendered = (|| -> DamageRenderResult {
{
let (renderer, mut framebuffer) = backend.bind()?;
state.process_dmabuf_imports(renderer);
let elements = animated_window_elements(state, renderer, &output);
let effects = frame_effect_metrics(
&elements,
output.current_scale().fractional_scale(),
);
let result = damage_tracker.render_output(
renderer,
&mut framebuffer,
&output,
&cursorless_elements,
age,
&elements,
[0.035, 0.04, 0.055, 1.0],
)?;
state.process_screencopies(renderer, &framebuffer, &output, false);
redraw_output(renderer, &mut framebuffer, &output, &elements)?;
} else if captured_with_cursor {
let _ = renderer
.render(
&mut framebuffer,
output.current_mode().expect("output has a mode").size,
output.current_transform(),
)?
.finish()?;
}
let cursorless_capture = state.has_pending_screencopy(&output, false);
let captured_with_cursor =
state.process_screencopies(renderer, &framebuffer, &output, true);
Ok((result.damage.cloned(), effects))
}
})();
let (damage, effects) = match rendered {
Ok((Some(damage), effects)) => (damage, effects),
Ok((None, _)) => {
state.space.refresh();
state.popups.cleanup();
layer_map_for_output(&output).cleanup();
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush clients");
if cursorless_capture {
let cursorless_elements =
output_elements(state, renderer, &output, false);
redraw_output(
renderer,
&mut framebuffer,
&output,
&cursorless_elements,
)?;
state.process_screencopies(renderer, &framebuffer, &output, false);
redraw_output(renderer, &mut framebuffer, &output, &elements)?;
} else if captured_with_cursor {
let _ = renderer
.render(
&mut framebuffer,
output.current_mode().expect("output has a mode").size,
output.current_transform(),
)?
.finish()?;
}
Ok((result.damage.cloned(), effects))
}
backend.window().request_redraw();
return;
}
Err(error) => {
tracing::error!(%error, "nested renderer failed");
})();
let (damage, effects) = match rendered {
Ok((Some(damage), effects)) => (damage, effects),
Ok((None, _)) => {
// A callback means permission to draw the next client
// frame, not proof of a new compositor presentation.
// No-damage frames must still unblock layer clients.
send_nested_frame_callbacks(state, &output, refresh.get());
state.space.refresh();
state.popups.cleanup();
layer_map_for_output(&output).cleanup();
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush clients");
}
return;
}
Err(error) => {
tracing::error!(%error, "nested renderer failed");
state.loop_signal.stop();
return;
}
};
if let Err(error) = backend.submit(Some(&damage)) {
tracing::error!(%error, "nested buffer submission failed");
state.loop_signal.stop();
return;
}
};
if let Err(error) = backend.submit(Some(&damage)) {
tracing::error!(%error, "nested buffer submission failed");
state.loop_signal.stop();
return;
}
render_metrics.record_frame(render_started.elapsed(), &damage, 0, effects);
let elapsed = render_started.elapsed();
if elapsed > refresh.get() {
missed_deadlines += (elapsed.as_nanos() / refresh.get().as_nanos()) as u64;
}
render_metrics.record_frame(elapsed, &damage, missed_deadlines, effects);
let mut presentation = OutputPresentationFeedback::new(&output);
state.space.elements().for_each(|window| {
window.take_presentation_feedback(
&mut presentation,
|_, _| Some(output.clone()),
|_, _| PresentationKind::Vsync,
let mut presentation = OutputPresentationFeedback::new(&output);
state.space.elements().for_each(|window| {
window.take_presentation_feedback(
&mut presentation,
|_, _| Some(output.clone()),
|_, _| PresentationKind::Vsync,
);
});
layer_surfaces(&output).iter().for_each(|layer| {
layer.take_presentation_feedback(
&mut presentation,
|_, _| Some(output.clone()),
|_, _| PresentationKind::Vsync,
);
});
sequence = sequence.wrapping_add(1);
presentation.presented(
clock.now(),
Refresh::fixed(refresh.get()),
sequence,
PresentationKind::Vsync,
);
});
layer_surfaces(&output).iter().for_each(|layer| {
layer.take_presentation_feedback(
&mut presentation,
|_, _| Some(output.clone()),
|_, _| PresentationKind::Vsync,
);
});
sequence = sequence.wrapping_add(1);
presentation.presented(
clock.now(),
Refresh::fixed(Duration::from_nanos(1_000_000_000 / 60)),
sequence,
PresentationKind::Vsync,
);
state.space.elements().for_each(|window| {
window.send_frame(
&output,
state.start_time.elapsed(),
Some(Duration::ZERO),
|_, _| Some(output.clone()),
);
});
layer_surfaces(&output).iter().for_each(|layer| {
layer.send_frame(
&output,
state.start_time.elapsed(),
Some(Duration::ZERO),
|_, _| Some(output.clone()),
);
});
state.send_cursor_frame(&output);
state.space.refresh();
state.popups.cleanup();
layer_map_for_output(&output).cleanup();
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush clients");
send_nested_frame_callbacks(state, &output, refresh.get());
state.space.refresh();
state.popups.cleanup();
layer_map_for_output(&output).cleanup();
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush clients");
}
}
backend.window().request_redraw();
WinitEvent::CloseRequested => state.loop_signal.stop(),
_ => {}
}
WinitEvent::CloseRequested => state.loop_signal.stop(),
_ => {}
})?;
Ok(())
}
fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Duration) {
let time = state.start_time.elapsed();
for window in state.space.elements() {
window.send_frame(output, time, Some(refresh), |_, _| Some(output.clone()));
}
for layer in layer_surfaces(output) {
layer.send_frame(output, time, Some(refresh), |_, _| Some(output.clone()));
}
state.send_cursor_frame(output);
}
pub(crate) fn animated_window_elements(
state: &mut Ferese,
renderer: &mut GlesRenderer,
@@ -875,6 +993,9 @@ fn output_elements(
output: &Output,
include_cursor: bool,
) -> Vec<AnimatedWindowRenderElement> {
if state.wallpaper.poll() {
state.backdrop_generation = state.backdrop_generation.wrapping_add(1);
}
let Some(output_geometry) = state.space.output_geometry(output) else {
return Vec::new();
};
@@ -898,6 +1019,12 @@ fn output_elements(
.rev()
.filter_map(|window| {
let id = *state.window_ids.get(window)?;
// Scrolling columns may sit outside their monitor's rectangle.
// They must not reappear on a neighboring output just because
// their global animated coordinates overlap it.
if !state.window_belongs_to_output(id, output) {
return None;
}
let visual = state.presented_window_rect(id)?;
let (close_scale, close_alpha) = state.closing_visual(id);
let visual = scaled_visual_rect(visual, close_scale);
@@ -912,16 +1039,22 @@ fn output_elements(
for (window, id, visual, decoration_progress, close_alpha) in windows {
let constrain = rounded_visual_rect(visual, output_geometry.loc);
let pixels = physical_rect(visual, output_geometry.loc, scale);
// Only overview/close intentionally scale the complete application.
let scale_content = state.overview.is_presenting() || state.closing_visual(id).0 != 1.0;
let behavior = resize_content_behavior(scale_content);
let dim = state.window_dimming.get(&id).map_or(0.0, |dim| dim.current);
if let Some(overlay) = window_dim_element(
if let Some(overlay) = window_tint_element(
state,
renderer,
id,
constrain,
pixels,
scale,
state.theme_settings.window_radius * decoration_progress,
dim as f32 * close_alpha,
[0.0, 0.0, 0.0, dim as f32 * close_alpha],
false,
output,
) {
// Front-to-back: dim the application and its border, not its shadow
@@ -958,6 +1091,7 @@ fn output_elements(
renderer,
id,
constrain,
pixels,
scale,
window_radius,
border_width,
@@ -972,6 +1106,7 @@ fn output_elements(
renderer,
id,
constrain,
pixels,
scale,
window_radius,
shadow_offset_y,
@@ -981,10 +1116,52 @@ fn output_elements(
output,
&programs,
);
if !scale_content {
if let Some(snapshot) = state.resize_snapshots.get(&id)
&& snapshot.context == renderer.context_id().erased()
&& (snapshot.scale - scale).abs() < 0.001
{
let size = snapshot.texture.size();
let visible =
Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels);
if let Some(visible) = visible {
let clip = framebuffer_clip_rect(
pixels,
output.current_mode().unwrap().size,
output.current_transform().invert(),
);
elements.push(
NativeTextureElement {
id: snapshot.id.clone(),
commit: snapshot.commit,
texture: snapshot.texture.clone(),
geometry: visible,
source: Rectangle::from_size(Size::from((
f64::from(visible.size.w),
f64::from(visible.size.h),
))),
alpha: crate::presentation::handoff_alpha(snapshot.elapsed)
* close_alpha,
program: Some(programs.texture.clone()),
uniforms: vec![
Uniform::new("clip_rect", clip).into_owned(),
Uniform::new(
"radius",
scaled_effect_value(window_radius, constrain, scale),
)
.into_owned(),
],
}
.into(),
);
}
}
}
elements.extend(rounded_window_elements(
renderer,
&window,
constrain,
pixels,
scale,
window_radius,
close_alpha,
@@ -994,7 +1171,31 @@ fn output_elements(
.is_some_and(|geometry| geometry.presentation_changed),
output,
programs.texture.clone(),
behavior,
));
let source = window.geometry().size;
if !scale_content && (source.w < constrain.size.w || source.h < constrain.size.h) {
let mut color = state.theme_settings.surface_base_color.0;
color[3] = close_alpha;
if let Some(fill) = window_tint_element(
state,
renderer,
id,
constrain,
pixels,
scale,
window_radius,
color,
true,
output,
) {
// Front-to-back: fill uncovered strips behind the native
// content instead of stretching it or exposing wallpaper.
elements.push(fill.into());
}
} else {
state.window_resize_fills.remove(&id);
}
if let Some(shadow) = shadow {
elements.push(shadow.into());
}
@@ -1012,7 +1213,7 @@ fn output_elements(
constrain,
ConstrainBehavior {
reference: ConstrainReference::Geometry,
behavior: ConstrainScaleBehavior::Stretch,
behavior,
align: ConstrainAlign::TOP | ConstrainAlign::LEFT,
},
));
@@ -1029,6 +1230,9 @@ fn output_elements(
output,
&[Layer::Bottom, Layer::Background],
));
if let Some(wallpaper) = state.wallpaper.element(renderer, output) {
elements.push(wallpaper.into());
}
elements
}
@@ -1114,13 +1318,14 @@ fn window_border_element(
renderer: &GlesRenderer,
id: ferese_layout::WindowId,
geometry: Rectangle<i32, Logical>,
physical: Rectangle<i32, Physical>,
scale: f64,
requested_radius: f64,
requested_width: f64,
color: [f32; 4],
output: &Output,
programs: &RoundedClipPrograms,
) -> Option<PixelShaderElement> {
) -> Option<PhysicalShaderElement> {
let mode = output.current_mode()?;
let width = scaled_effect_value(requested_width, geometry, scale);
if width == 0.0 {
@@ -1129,11 +1334,7 @@ fn window_border_element(
let parameters = BorderParameters {
geometry,
clip_rect: framebuffer_clip_rect(
geometry.to_physical_precise_round(scale),
mode.size,
output.current_transform().invert(),
),
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
radius: scaled_effect_value(requested_radius, geometry, scale),
width,
color,
@@ -1167,7 +1368,10 @@ fn window_border_element(
cached.parameters = parameters;
}
Some(cached.element.clone())
Some(PhysicalShaderElement {
inner: cached.element.clone(),
geometry: physical,
})
}
fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
@@ -1180,32 +1384,34 @@ fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
}
#[allow(clippy::too_many_arguments)]
fn window_dim_element(
fn window_tint_element(
state: &mut Ferese,
renderer: &mut GlesRenderer,
id: ferese_layout::WindowId,
geometry: Rectangle<i32, Logical>,
physical: Rectangle<i32, Physical>,
scale: f64,
radius: f64,
amount: f32,
color: [f32; 4],
resize_fill: bool,
output: &Output,
) -> Option<PixelShaderElement> {
if amount <= 0.0 {
state.window_dims.remove(&id);
) -> Option<PhysicalShaderElement> {
if color[3] <= 0.0 {
if resize_fill {
state.window_resize_fills.remove(&id);
} else {
state.window_dims.remove(&id);
}
return None;
}
let mode = output.current_mode()?;
let program = material_program(state, renderer)?;
let parameters = BorderParameters {
geometry,
clip_rect: framebuffer_clip_rect(
geometry.to_physical_precise_round(scale),
mode.size,
output.current_transform().invert(),
),
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
radius: scaled_effect_value(radius, geometry, scale),
width: 0.0,
color: [0.0, 0.0, 0.0, amount.clamp(0.0, 1.0)],
color,
};
let uniforms = |p: &BorderParameters| {
vec![
@@ -1218,7 +1424,11 @@ fn window_dim_element(
]
};
let context = renderer.context_id().erased();
let buffers = state.window_dims.entry(id).or_default();
let buffers = if resize_fill {
state.window_resize_fills.entry(id).or_default()
} else {
state.window_dims.entry(id).or_default()
};
let cached = buffers
.contexts
.entry(context)
@@ -1242,7 +1452,10 @@ fn window_dim_element(
cached.element.update_uniforms(uniforms(&parameters));
cached.parameters = parameters;
}
Some(cached.element.clone())
Some(PhysicalShaderElement {
inner: cached.element.clone(),
geometry: physical,
})
}
#[allow(clippy::too_many_arguments)]
@@ -1251,6 +1464,7 @@ fn window_shadow_element(
renderer: &GlesRenderer,
id: ferese_layout::WindowId,
geometry: Rectangle<i32, Logical>,
physical: Rectangle<i32, Physical>,
scale: f64,
requested_radius: f64,
offset_y: f64,
@@ -1259,22 +1473,26 @@ fn window_shadow_element(
color: [f32; 4],
output: &Output,
programs: &RoundedClipPrograms,
) -> Option<PixelShaderElement> {
) -> Option<PhysicalShaderElement> {
let mode = output.current_mode()?;
if opacity == 0.0 || color[3] == 0.0 {
return None;
}
let shadow_geometry = Rectangle::new(
(geometry.loc.x, geometry.loc.y + offset_y.round() as i32).into(),
geometry.size,
(
physical.loc.x,
physical.loc.y + (offset_y * scale).round() as i32,
)
.into(),
physical.size,
);
let bounds = shadow_bounds(geometry, offset_y, blur);
let parameters = ShadowParameters {
blur: (blur * scale) as f32,
bounds,
shadow_rect: framebuffer_clip_rect(
shadow_geometry.to_physical_precise_round(scale),
shadow_geometry,
mode.size,
output.current_transform().invert(),
),
@@ -1311,7 +1529,22 @@ fn window_shadow_element(
cached.parameters = parameters;
}
Some(cached.element.clone())
let extent = (blur * scale * 2.0).ceil() as i32;
Some(PhysicalShaderElement {
inner: cached.element.clone(),
geometry: Rectangle::new(
(
shadow_geometry.loc.x - extent,
shadow_geometry.loc.y - extent,
)
.into(),
(
shadow_geometry.size.w + 2 * extent,
shadow_geometry.size.h + 2 * extent,
)
.into(),
),
})
}
fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'static>> {
@@ -1350,12 +1583,14 @@ fn rounded_window_elements(
renderer: &mut GlesRenderer,
window: &smithay::desktop::Window,
constrain: Rectangle<i32, Logical>,
physical_constrain: Rectangle<i32, Physical>,
scale: f64,
requested_radius: f64,
alpha: f32,
clip_changed: bool,
output: &Output,
program: GlesTexProgram,
behavior: ConstrainScaleBehavior,
) -> Vec<AnimatedWindowRenderElement> {
let Some(toplevel) = window.toplevel() else {
return Vec::new();
@@ -1366,8 +1601,7 @@ fn rounded_window_elements(
let geometry = window.geometry();
let reference = geometry.to_physical_precise_round(scale);
let physical_constrain = constrain.to_physical_precise_round(scale);
let location = (constrain.loc - geometry.loc).to_physical_precise_round(scale);
let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
let clip = framebuffer_clip_rect(
physical_constrain,
mode.size,
@@ -1426,7 +1660,7 @@ fn rounded_window_elements(
location,
physical_constrain,
reference,
ConstrainScaleBehavior::Stretch,
behavior,
ConstrainAlign::TOP | ConstrainAlign::LEFT,
scale,
)
@@ -1434,6 +1668,14 @@ fn rounded_window_elements(
.collect()
}
fn resize_content_behavior(intentional_scale: bool) -> ConstrainScaleBehavior {
if intentional_scale {
ConstrainScaleBehavior::Stretch
} else {
ConstrainScaleBehavior::CutOff
}
}
fn scaled_visual_rect(rect: ferese_layout::Rect, scale: f64) -> ferese_layout::Rect {
let scale = scale.clamp(0.0, 1.0);
let width = rect.width * scale;
@@ -2056,8 +2298,8 @@ mod tests {
};
use super::{
color_with_alpha, framebuffer_clip_rect, normalized_scale, rounded_visual_rect,
scaled_visual_rect, shadow_bounds,
color_with_alpha, framebuffer_clip_rect, normalized_scale, resize_content_behavior,
rounded_visual_rect, scaled_visual_rect, shadow_bounds,
};
#[derive(Debug)]
@@ -2114,6 +2356,44 @@ mod tests {
}
}
#[test]
fn resize_keeps_source_pixels_native_when_growing_and_shrinking() {
use smithay::backend::renderer::element::utils::{
ConstrainAlign, constrain_render_elements,
};
for scale in [1.0, 1.5, 1.8, 2.0] {
for width in [300, 600, 1200] {
let source = Rectangle::<i32, Logical>::from_size((600, 800).into());
let destination = Rectangle::<i32, Logical>::from_size((width, 800).into());
let element = constrain_render_elements(
[DamageElement::new(source)],
(0, 0),
destination.to_physical_precise_round(scale),
source.to_physical_precise_round(scale),
resize_content_behavior(false),
ConstrainAlign::TOP | ConstrainAlign::LEFT,
scale,
)
.next()
.unwrap();
// Comparing destination pixels to sampled source pixels catches
// stretching, unlike tests of configure timing alone.
let physical = element.geometry(scale.into());
let sampled = element.src();
assert!((f64::from(physical.size.w) / sampled.size.w - scale).abs() < 0.01);
assert!((f64::from(physical.size.h) / sampled.size.h - scale).abs() < 0.01);
}
}
}
#[test]
fn overview_retains_intentional_content_scaling() {
assert!(matches!(
resize_content_behavior(true),
smithay::backend::renderer::element::utils::ConstrainScaleBehavior::Stretch
));
}
#[test]
fn accepts_positive_finite_scale() {
assert_eq!(normalized_scale(1.25), 1.25);