diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index cc00d65..706abf3 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -49,7 +49,16 @@ impl CompositorHandler for Ferese { .compositor_state } + fn new_surface(&mut self, surface: &WlSurface) { + self.update_surface_preferences(surface); + } + + fn new_subsurface(&mut self, surface: &WlSurface, _parent: &WlSurface) { + self.update_surface_preferences(surface); + } + fn commit(&mut self, surface: &WlSurface) { + self.update_surface_preferences(surface); self.capture_resize_before_commit(surface); if self.surface_affects_backdrop(surface) { self.backdrop_generation = self.backdrop_generation.wrapping_add(1); diff --git a/compositor/src/handlers/layer_shell.rs b/compositor/src/handlers/layer_shell.rs index 5a78909..e7f7da1 100644 --- a/compositor/src/handlers/layer_shell.rs +++ b/compositor/src/handlers/layer_shell.rs @@ -31,20 +31,19 @@ impl WlrLayerShellHandler for Ferese { } tracing::debug!(%namespace, output = %output.name(), "mapped layer surface"); - super::set_surface_tree_scale(layer.wl_surface(), output.current_scale().fractional_scale()); + super::set_surface_tree_output(layer.wl_surface(), &output); self.relayout(); crate::backends::direct::render_all(self); } fn new_popup(&mut self, parent: LayerSurface, popup: PopupSurface) { - let scale = self.space.outputs().find_map(|output| { + let output = self.space.outputs().find(|output| { layer_map_for_output(output) .layers() .any(|layer| layer.wl_surface() == parent.wl_surface()) - .then(|| output.current_scale().fractional_scale()) }); - if let Some(scale) = scale { - super::set_surface_tree_scale(popup.wl_surface(), scale); + if let Some(output) = output { + super::set_surface_tree_output(popup.wl_surface(), output); } self.unconstrain_popup(&popup); if let Err(error) = self.popups.track_popup(PopupKind::Xdg(popup)) { diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index 2e295b4..dc40262 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -79,18 +79,7 @@ impl OutputHandler for Ferese {} impl FractionalScaleHandler for Ferese { fn new_fractional_scale(&mut self, surface: WlSurface) { - let scale = self - .space - .outputs() - .next() - .map(|output| output.current_scale().fractional_scale()) - .unwrap_or(1.0); - - with_states(&surface, |states| { - with_fractional_scale(states, |surface_scale| { - surface_scale.set_preferred_scale(scale); - }); - }); + self.update_surface_preferences(&surface); } } @@ -140,43 +129,69 @@ impl KeyboardShortcutsInhibitHandler for Ferese { } impl Ferese { - pub fn update_fractional_scale(&self, scale: f64) { - for window in self.space.elements() { - let Some(toplevel) = window.toplevel() else { - continue; - }; + pub(crate) fn update_surface_preferences(&self, surface: &WlSurface) { + use smithay::desktop::{WindowSurfaceType, find_popup_root_surface}; + use smithay::wayland::compositor::get_parent; - set_surface_tree_scale(toplevel.wl_surface(), scale); + let mut root = surface.clone(); + while let Some(parent) = get_parent(&root) { + root = parent; } - - // Layer surfaces are not Space elements. Use their own output's scale, - // rather than broadcasting one output's density to every bar. - for output in self.space.outputs() { - let scale = output.current_scale().fractional_scale(); - let layers: Vec<_> = layer_map_for_output(output).layers().cloned().collect(); - for layer in layers { - set_surface_tree_scale(layer.wl_surface(), scale); - } + if let Some(popup) = self.popups.find_popup(&root) + && let Ok(parent) = find_popup_root_surface(&popup) + { + root = parent; } + let window = self.window_ids.iter().find_map(|(window, id)| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == &root) + .then_some(*id) + }); + let output = self.space.outputs().find(|output| { + window.is_some_and(|id| self.window_belongs_to_output(id, output)) + || layer_map_for_output(output) + .layer_for_surface(&root, WindowSurfaceType::ALL) + .is_some() + || self + .session_lock + .surfaces + .get(output) + .is_some_and(|lock| lock.wl_surface() == &root) + }); + // Hidden workspaces retain their last output preferences until presented again. + if window.is_some() && output.is_none() { + return; + } + let output = output + .or_else(|| self.focused_output()) + .or_else(|| self.space.outputs().next()); + with_states(surface, |states| set_surface_preferences(surface, states, output)); } } -pub(crate) fn set_surface_tree_scale(surface: &WlSurface, scale: f64) { +fn set_surface_preferences( + surface: &WlSurface, + states: &smithay::wayland::compositor::SurfaceData, + output: Option<&smithay::output::Output>, +) { + let scale = output.map_or(1.0, |output| output.current_scale().fractional_scale()); + let transform = output.map_or(smithay::utils::Transform::Normal, |output| output.current_transform()); + smithay::wayland::compositor::send_surface_state(surface, states, scale.ceil() as i32, transform); + with_fractional_scale(states, |surface_scale| surface_scale.set_preferred_scale(scale)); +} + +pub(crate) fn set_surface_tree_output(surface: &WlSurface, output: &smithay::output::Output) { let update_tree = |root: &WlSurface| { with_surface_tree_downward( root, (), |_, _, &()| TraversalAction::DoChildren(()), - |_, states, &()| { - with_fractional_scale(states, |surface_scale| { - surface_scale.set_preferred_scale(scale); - }); - }, + |surface, states, &()| set_surface_preferences(surface, states, Some(output)), |_, _, &()| true, ); }; update_tree(surface); - // XDG popups are separate surface trees, not wl_subsurfaces. for (popup, _) in PopupManager::popups_for_surface(surface) { update_tree(popup.wl_surface()); } diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index cb0bfe5..03051b2 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -202,6 +202,7 @@ impl Ferese { pub(crate) fn configure_lock_surfaces(&mut self) { for (output, surface) in &self.session_lock.surfaces { + crate::handlers::set_surface_tree_output(surface.wl_surface(), output); if let Some(geometry) = self.space.output_geometry(output) { surface.with_pending_state(|state| { state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into()) @@ -316,6 +317,7 @@ impl SessionLockHandler for Ferese { { surface .with_pending_state(|state| state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into())); + crate::handlers::set_surface_tree_output(surface.wl_surface(), &output); self.session_lock.surfaces.insert(output, surface); self.focus_lock_surface(); } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 100b870..3409a0b 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -473,7 +473,7 @@ impl Ferese { crate::shell_control::init_global(&display_handle); let alpha_modifier_state = AlphaModifierState::new::(&display_handle); - let compositor_state = CompositorState::new::(&display_handle); + let compositor_state = CompositorState::new_v6::(&display_handle); let cursor_shape_state = CursorShapeManagerState::new::(&display_handle); let xdg_shell_state = XdgShellState::new::(&display_handle); let decoration_state = XdgDecorationState::new::(&display_handle); diff --git a/compositor/src/state/layout.rs b/compositor/src/state/layout.rs index 762a85c..9da0c66 100644 --- a/compositor/src/state/layout.rs +++ b/compositor/src/state/layout.rs @@ -81,9 +81,8 @@ impl Ferese { continue; }; let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds); - let scale = output.current_scale().fractional_scale(); for layer in layer_map_for_output(&output).layers() { - crate::handlers::set_surface_tree_scale(layer.wl_surface(), scale); + crate::handlers::set_surface_tree_output(layer.wl_surface(), &output); } let Some(workspace) = self.workspaces.workspace(workspace_id) else { continue; @@ -140,7 +139,7 @@ impl Ferese { continue; } if let Some(toplevel) = window.toplevel() { - crate::handlers::set_surface_tree_scale(toplevel.wl_surface(), scale); + crate::handlers::set_surface_tree_output(toplevel.wl_surface(), &output); } let is_maximized = self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id); diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 97e80cb..9fa4228 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -70,7 +70,6 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() let clock = Clock::::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 @@ -112,11 +111,6 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() 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"); diff --git a/scripts/tests/fixtures/surface-preferences.c b/scripts/tests/fixtures/surface-preferences.c new file mode 100644 index 0000000..8fab6c4 --- /dev/null +++ b/scripts/tests/fixtures/surface-preferences.c @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include +#include "fractional-scale-client-protocol.h" + +static struct wl_compositor *compositor; +static struct wp_fractional_scale_manager_v1 *manager; +static uint32_t version, fractional, fractional_events; +static int32_t integer_scale = 1, transform; +static unsigned scale_events, transform_events; + +static void global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t advertised) { + (void)data; + if (!strcmp(interface, "wl_compositor")) { + assert(advertised >= 6); + compositor = wl_registry_bind(registry, name, &wl_compositor_interface, version); + } else if (!strcmp(interface, "wp_fractional_scale_manager_v1")) { + manager = wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, 1); + } +} + +static void removed(void *data, struct wl_registry *registry, uint32_t name) { + (void)data; (void)registry; (void)name; +} + +static void scale(void *data, struct wl_surface *surface, int32_t value) { + (void)data; (void)surface; + integer_scale = value; + scale_events++; +} + +static void transformed(void *data, struct wl_surface *surface, uint32_t value) { + (void)data; (void)surface; + transform = (int32_t)value; + transform_events++; +} + +static void preferred(void *data, struct wp_fractional_scale_v1 *object, uint32_t value) { + (void)data; (void)object; + fractional = value; + fractional_events++; +} + +int main(int argc, char **argv) { + assert(argc == 2); + version = (uint32_t)atoi(argv[1]); + struct wl_display *display = wl_display_connect(NULL); + assert(display); + struct wl_registry *registry = wl_display_get_registry(display); + const struct wl_registry_listener registry_listener = {global, removed}; + wl_registry_add_listener(registry, ®istry_listener, NULL); + assert(wl_display_roundtrip(display) >= 0 && compositor && manager); + struct wl_surface *surface = wl_compositor_create_surface(compositor); + const struct wl_surface_listener listener = { + .preferred_buffer_scale = scale, + .preferred_buffer_transform = transformed, + }; + wl_surface_add_listener(surface, &listener, NULL); + assert(wl_display_roundtrip(display) >= 0); + // Create the fractional object after the compositor cached its preference. + struct wp_fractional_scale_v1 *object = + wp_fractional_scale_manager_v1_get_fractional_scale(manager, surface); + const struct wp_fractional_scale_v1_listener fractional_listener = {preferred}; + wp_fractional_scale_v1_add_listener(object, &fractional_listener, NULL); + assert(wl_display_roundtrip(display) >= 0); + assert(fractional > 0 && fractional_events == 1); + if (version >= 6) { + assert(integer_scale == (int32_t)((fractional + 119) / 120)); + assert(scale_events == (integer_scale != 1 ? 1u : 0u)); + assert(transform_events == (transform != 0 ? 1u : 0u)); + } else { + assert(scale_events == 0 && transform_events == 0); + } + for (int i = 0; i < 3; i++) { + wl_surface_commit(surface); + assert(wl_display_roundtrip(display) >= 0); + } + assert(fractional_events == 1); + assert(scale_events == (version >= 6 && integer_scale != 1 ? 1u : 0u)); + assert(transform_events == (version >= 6 && transform != 0 ? 1u : 0u)); + wp_fractional_scale_v1_destroy(object); + wl_surface_destroy(surface); + wl_display_roundtrip(display); + wl_display_disconnect(display); + return 0; +} diff --git a/scripts/tests/test_surface_preferences_isolated.py b/scripts/tests/test_surface_preferences_isolated.py new file mode 100644 index 0000000..0808987 --- /dev/null +++ b/scripts/tests/test_surface_preferences_isolated.py @@ -0,0 +1,69 @@ +"""FERESE_TEST_SURFACE_PREFERENCES=1 python3 scripts/tests/test_surface_preferences_isolated.py""" + +import os +from pathlib import Path +import subprocess +import tempfile +import time +import unittest + + +@unittest.skipUnless(os.environ.get("FERESE_TEST_SURFACE_PREFERENCES") == "1", "opt-in nested test") +class SurfacePreferencesTest(unittest.TestCase): + def test_v6_preferences_and_v5_compatibility(self): + repo = Path(__file__).resolve().parents[2] + with tempfile.TemporaryDirectory(prefix="ferese-surface-preferences-") as temporary: + root = Path(temporary) + protocols = subprocess.check_output( + ["pkg-config", "--variable=pkgdatadir", "wayland-protocols"], text=True + ).strip() + xml = str(Path(protocols) / "staging/fractional-scale/fractional-scale-v1.xml") + for kind, name in [("client-header", "fractional-scale-client-protocol.h"), + ("private-code", "fractional-scale.c")]: + subprocess.run(["wayland-scanner", kind, xml, str(root / name)], check=True) + flags = subprocess.check_output( + ["pkg-config", "--cflags", "--libs", "wayland-client"], text=True + ).split() + subprocess.run([ + "cc", "-Wall", "-Wextra", "-Werror", "-I", str(root), + str(repo / "scripts/tests/fixtures/surface-preferences.c"), + str(root / "fractional-scale.c"), "-o", str(root / "client"), *flags, + ], check=True) + runtime = root / "runtime" + runtime.mkdir(mode=0o700) + config = root / "config/ferese" + config.mkdir(parents=True) + (config / "config.kdl").write_text("\n") + display = Path(os.environ["WAYLAND_DISPLAY"]) + if not display.is_absolute(): + display = Path(os.environ["XDG_RUNTIME_DIR"]) / display + env = dict(os.environ, WAYLAND_DISPLAY=str(display), XDG_RUNTIME_DIR=str(runtime), + XDG_CONFIG_HOME=str(root / "config")) + binary = repo / os.environ.get("FERESE_TEST_BINARY", "target/debug/ferese") + with (root / "compositor.log").open("w") as log: + compositor = subprocess.Popen([str(binary), "--backend", "nested"], + env=env, stdout=log, stderr=log) + try: + sockets = [] + for _ in range(100): + if compositor.poll() is not None: + self.fail((root / "compositor.log").read_text()) + sockets = [p for p in runtime.glob("wayland-*") if p.is_socket()] + if sockets: + break + time.sleep(0.1) + self.assertTrue(sockets, "nested compositor did not create a socket") + for version in [5, 6]: + subprocess.run([str(root / "client"), str(version)], check=True, timeout=10, + env=dict(env, WAYLAND_DISPLAY=str(sockets[0]))) + finally: + compositor.terminate() + try: + compositor.wait(timeout=5) + except subprocess.TimeoutExpired: + compositor.kill() + compositor.wait() + + +if __name__ == "__main__": + unittest.main()