diff --git a/src/tests/meson.build b/src/tests/meson.build
index 9e449a2f5..fd91633e0 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -492,6 +492,17 @@ test_cases += [
cursor_screen_cast_client,
]
},
+ {
+ 'name': 'surface-scale-tests',
+ 'suite': 'backends/native',
+ 'sources': [
+ 'surface-scale-tests.c',
+ wayland_test_utils,
+ ],
+ 'depends': [
+ test_client_executables.get('cursor-tests-client'),
+ ]
+ },
]
# KMS tests
diff --git a/src/tests/surface-scale-tests.c b/src/tests/surface-scale-tests.c
new file mode 100644
index 000000000..546ca1900
--- /dev/null
+++ b/src/tests/surface-scale-tests.c
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2025 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ *
+ */
+
+#include "config.h"
+
+#include "tests/meta-monitor-test-utils.h"
+#include "tests/meta-test-utils.h"
+#include "tests/meta-test/meta-context-test.h"
+#include "tests/meta-wayland-test-driver.h"
+#include "tests/meta-wayland-test-utils.h"
+
+static MetaContext *test_context;
+
+static MonitorTestCaseSetup test_case_base_setup = {
+ .modes = {
+ {
+ .width = 1920,
+ .height = 1080,
+ .refresh_rate = 60.0
+ },
+ },
+ .n_modes = 1,
+ .outputs = {
+ {
+ .crtc = 0,
+ .modes = { 0 },
+ .n_modes = 1,
+ .preferred_mode = 0,
+ .possible_crtcs = { 0 },
+ .n_possible_crtcs = 1,
+ .width_mm = 150,
+ .height_mm = 85,
+ },
+ },
+ .n_outputs = 1,
+ .crtcs = {
+ {
+ .current_mode = -1
+ },
+ },
+ .n_crtcs = 1
+};
+
+static void
+bump_output_serial (const char **serial)
+{
+ static int output_serial_counter = 0x1230000;
+
+ g_clear_pointer ((gpointer *) serial, g_free);
+ *serial = g_strdup_printf ("0x%x", output_serial_counter++);
+}
+
+static void
+meta_test_wayland_surface_scales (void)
+{
+ MetaBackend *backend = meta_context_get_backend (test_context);
+ ClutterSeat *seat = meta_backend_get_default_seat (backend);
+ MetaWaylandCompositor *compositor =
+ meta_context_get_wayland_compositor (test_context);
+ MetaMonitorManager *monitor_manager =
+ meta_backend_get_monitor_manager (backend);
+ MetaMonitorManagerTest *monitor_manager_test =
+ META_MONITOR_MANAGER_TEST (monitor_manager);
+ g_autoptr (MetaWaylandTestDriver) test_driver = NULL;
+ g_autoptr (ClutterVirtualInputDevice) virtual_pointer = NULL;
+ MetaWaylandTestClient *wayland_test_client;
+ MonitorTestCaseSetup test_case_setup = test_case_base_setup;
+ MetaMonitorTestSetup *test_setup;
+ float scale;
+
+ virtual_pointer = clutter_seat_create_virtual_device (seat,
+ CLUTTER_POINTER_DEVICE);
+
+ test_driver = meta_wayland_test_driver_new (compositor);
+ meta_wayland_test_driver_set_property_int (test_driver,
+ "cursor-theme-size",
+ meta_prefs_get_cursor_size ());
+
+
+ g_debug ("Testing with scale 2.0, then launching client");
+ scale = 2.0f;
+ test_case_setup.outputs[0].scale = scale;
+ bump_output_serial (&test_case_setup.outputs[0].serial);
+ test_setup = meta_create_monitor_test_setup (backend,
+ &test_case_setup,
+ MONITOR_TEST_FLAG_NO_STORED);
+ meta_monitor_manager_test_emulate_hotplug (monitor_manager_test, test_setup);
+
+ wayland_test_client = meta_wayland_test_client_new (test_context,
+ "surface-scale-client");
+ meta_wait_for_window_cursor (test_context);
+ meta_wayland_test_driver_emit_sync_event (test_driver,
+ (uint32_t) (scale * 120.0f));
+ meta_wayland_test_driver_wait_for_sync_point (test_driver, 0);
+
+ g_debug ("Testing with scale 2.5 with existing client");
+ scale = 2.5f;
+ test_case_setup.outputs[0].scale = scale;
+ bump_output_serial (&test_case_setup.outputs[0].serial);
+ test_setup = meta_create_monitor_test_setup (backend,
+ &test_case_setup,
+ MONITOR_TEST_FLAG_NO_STORED);
+ meta_monitor_manager_test_emulate_hotplug (monitor_manager_test, test_setup);
+ meta_wayland_test_driver_emit_sync_event (test_driver,
+ (uint32_t) (scale * 120.0f));
+ meta_wayland_test_driver_wait_for_sync_point (test_driver, 0);
+
+ g_debug ("Terminating client");
+ meta_wayland_test_driver_emit_sync_event (test_driver, 0);
+
+ g_clear_pointer ((gpointer *) &test_case_setup.outputs[0].serial, g_free);
+ meta_wayland_test_client_finish (wayland_test_client);
+}
+
+static void
+init_tests (void)
+{
+ g_test_add_func ("/wayland/surface/surface-scales",
+ meta_test_wayland_surface_scales);
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ g_autoptr (MetaContext) context = NULL;
+
+ context = meta_create_test_context (META_CONTEXT_TEST_TYPE_TEST,
+ META_CONTEXT_TEST_FLAG_NO_X11);
+ g_assert_true (meta_context_configure (context, &argc, &argv, NULL));
+
+ test_context = context;
+
+ init_tests ();
+
+ return meta_context_test_run_tests (META_CONTEXT_TEST (context),
+ META_TEST_RUN_FLAG_NONE);
+}
diff --git a/src/tests/wayland-test-clients/meson.build b/src/tests/wayland-test-clients/meson.build
index 0d76ac1bb..ec7370b7c 100644
--- a/src/tests/wayland-test-clients/meson.build
+++ b/src/tests/wayland-test-clients/meson.build
@@ -109,6 +109,12 @@ wayland_test_clients = [
libmutter_mtk_dep,
],
},
+ {
+ 'name': 'surface-scale-client',
+ 'extra_deps': [
+ wayland_cursor_dep,
+ ],
+ },
]
test_client_executables = {}
diff --git a/src/tests/wayland-test-clients/surface-scale-client.c b/src/tests/wayland-test-clients/surface-scale-client.c
new file mode 100644
index 000000000..5fb2a9ae2
--- /dev/null
+++ b/src/tests/wayland-test-clients/surface-scale-client.c
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2025 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see .
+ *
+ */
+
+#include "config.h"
+
+#include
+
+#include "wayland-test-client-utils.h"
+
+static float toplevel_scale;
+static float cursor_scale;
+static float subsurface_scale;
+
+static WaylandSurface *toplevel_surface;
+static WaylandSurface *cursor_surface;
+static WaylandSurface *subsurface;
+
+static void
+check_scales (float scale)
+{
+ g_assert_cmpfloat_with_epsilon (toplevel_scale, scale, FLT_EPSILON);
+ g_assert_cmpint (toplevel_surface->preferred_buffer_scale,
+ ==,
+ (int32_t) ceilf (scale));
+
+ g_assert_cmpfloat_with_epsilon (cursor_scale, scale, FLT_EPSILON);
+ g_assert_cmpint (cursor_surface->preferred_buffer_scale,
+ ==,
+ (int32_t) ceilf (scale));
+
+ g_assert_cmpfloat_with_epsilon (subsurface_scale, scale, FLT_EPSILON);
+ g_assert_cmpint (subsurface->preferred_buffer_scale,
+ ==,
+ (int32_t) ceilf (scale));
+}
+
+static void
+handle_preferred_scale (void *data,
+ struct wp_fractional_scale_v1 *fractional_scale,
+ uint32_t wire_scale)
+{
+ float *scale_ptr = data;
+
+ *scale_ptr = wire_scale / 120.0f;
+}
+
+static const struct wp_fractional_scale_v1_listener fractional_scale_listener = {
+ .preferred_scale = handle_preferred_scale,
+};
+
+static void
+watch_preferred_scales (WaylandDisplay *display,
+ struct wl_surface *wl_surface,
+ float *scale_ptr)
+{
+ struct wp_fractional_scale_v1 *fractional_scale;
+
+ fractional_scale =
+ wp_fractional_scale_manager_v1_get_fractional_scale (display->fractional_scale_mgr,
+ wl_surface);
+ wp_fractional_scale_v1_add_listener (fractional_scale,
+ &fractional_scale_listener,
+ scale_ptr);
+}
+
+static void
+on_pointer_enter (WaylandSurface *surface,
+ struct wl_pointer *pointer,
+ uint32_t serial)
+{
+ WaylandDisplay *display = surface->display;
+ struct wl_cursor_theme *cursor_theme;
+ struct wl_cursor *cursor;
+ struct wl_cursor_image *image;
+ struct wl_buffer *buffer;
+ int theme_size;
+
+ if (!cursor_surface)
+ {
+ cursor_surface = wayland_surface_new_unassigned (display);
+
+ watch_preferred_scales (display,
+ cursor_surface->wl_surface,
+ &cursor_scale);
+ }
+
+ theme_size = lookup_property_int (display, "cursor-theme-size");
+
+ cursor_theme = wl_cursor_theme_load (NULL,
+ theme_size,
+ display->shm);
+ cursor = wl_cursor_theme_get_cursor (cursor_theme, "left_ptr");
+ image = cursor->images[0];
+ buffer = wl_cursor_image_get_buffer (image);
+ g_assert_nonnull (buffer);
+
+ wl_pointer_set_cursor (pointer, serial,
+ cursor_surface->wl_surface,
+ image->hotspot_x, image->hotspot_y);
+ wl_surface_attach (cursor_surface->wl_surface, buffer, 0, 0);
+ wl_surface_damage_buffer (cursor_surface->wl_surface, 0, 0,
+ image->width, image->height);
+ wl_surface_commit (cursor_surface->wl_surface);
+
+ wl_cursor_theme_destroy (cursor_theme);
+}
+
+static void
+on_sync_event (WaylandDisplay *display,
+ uint32_t serial,
+ uint32_t *out_scale)
+{
+ *out_scale = serial;
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ g_autoptr (WaylandDisplay) display = NULL;
+ uint32_t new_scale = UINT32_MAX;
+ uint32_t prev_scale;
+ struct wl_subsurface *wl_subsurface;
+
+ display = wayland_display_new (WAYLAND_DISPLAY_CAPABILITY_TEST_DRIVER);
+ g_signal_connect (display, "sync-event",
+ G_CALLBACK (on_sync_event), &new_scale);
+ toplevel_surface = wayland_surface_new (display,
+ "cursor-tests-surface",
+ 100, 100, 0xffffffff);
+ g_signal_connect (toplevel_surface, "pointer-enter",
+ G_CALLBACK (on_pointer_enter), NULL);
+ xdg_toplevel_set_fullscreen (toplevel_surface->xdg_toplevel, NULL);
+ watch_preferred_scales (display,
+ toplevel_surface->wl_surface,
+ &toplevel_scale);
+
+ subsurface = wayland_surface_new_unassigned (display);
+ wl_subsurface =
+ wl_subcompositor_get_subsurface (display->subcompositor,
+ subsurface->wl_surface,
+ toplevel_surface->wl_surface);
+ draw_surface (display, subsurface->wl_surface, 10, 10, 0xff0000ff);
+ watch_preferred_scales (display,
+ subsurface->wl_surface,
+ &subsurface_scale);
+ wl_surface_commit (subsurface->wl_surface);
+
+ wl_surface_commit (toplevel_surface->wl_surface);
+
+ g_debug ("Waiting for scales to check");
+ while (new_scale > 0)
+ {
+ prev_scale = new_scale;
+ wayland_display_dispatch (display);
+ wl_display_roundtrip (display->display);
+ if (prev_scale != new_scale && new_scale > 0)
+ {
+ float scale = new_scale / 120.0f;
+
+ g_debug ("Checking scale %f", scale);
+ check_scales (scale);
+ test_driver_sync_point (display->test_driver, 0, NULL);
+ }
+ }
+
+ g_clear_pointer (&wl_subsurface, wl_subsurface_destroy);
+
+ g_clear_object (&toplevel_surface);
+ g_clear_object (&cursor_surface);
+ g_clear_object (&subsurface);
+
+ return EXIT_SUCCESS;
+}