From 49babddc7ccdb91b9293d3607da0ccdd1e4194dc Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Mon, 29 Jun 2026 12:53:26 +0200 Subject: [PATCH 1/6] cursor/native: Fix typo --- src/backends/native/meta-cursor-renderer-native.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/native/meta-cursor-renderer-native.c b/src/backends/native/meta-cursor-renderer-native.c index 942bfe0c8..fbcc58af5 100644 --- a/src/backends/native/meta-cursor-renderer-native.c +++ b/src/backends/native/meta-cursor-renderer-native.c @@ -1374,7 +1374,7 @@ load_scaled_and_transformed_cursor_sprite (MetaCursorRendererNative *native, cursor_sprite, cairo_image_surface_get_data (surface), cairo_image_surface_get_width (surface), - cairo_image_surface_get_width (surface), + cairo_image_surface_get_height (surface), cairo_image_surface_get_stride (surface), gbm_format); From 41842acd11235bcbd81b26e27d2d58cd7a4ff603 Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Tue, 30 Jun 2026 16:20:54 +0200 Subject: [PATCH 2/6] cursor/native: Track actual HW cursor BO size Fix motion rendering in Wayland with HiDPI. Moving the mouse cursor in HiDPI scale 2, in Wayland, resulted in only the top-left quarter of the cursor being visible during motion. Fixes https://github.com/linuxmint/muffin/issues/835 Store the dimensions of each GBM (Graphics Buffer Manager) cursor BO (buffer object) alongside the BO itself, and use those dimensions when assigning the cursor plane. Wayland HiDPI cursors can be preprocessed into an image smaller than the hardware cursor maximum. Allocating and presenting the cursor using the actual preprocessed size keeps the legacy DRM (Direct Rendering Manager) cursor ioctl (input/output control) dimensions aligned with the BO layout, avoiding padded hardware-max buffers being interpreted as the visible cursor image. --- .../native/meta-cursor-renderer-native.c | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/backends/native/meta-cursor-renderer-native.c b/src/backends/native/meta-cursor-renderer-native.c index fbcc58af5..c3b7a546c 100644 --- a/src/backends/native/meta-cursor-renderer-native.c +++ b/src/backends/native/meta-cursor-renderer-native.c @@ -113,6 +113,8 @@ typedef struct _MetaCursorNativeGpuState guint active_bo; MetaCursorGbmBoState pending_bo_state; struct gbm_bo *bos[HW_CURSOR_BUFFER_COUNT]; + int widths[HW_CURSOR_BUFFER_COUNT]; + int heights[HW_CURSOR_BUFFER_COUNT]; } MetaCursorNativeGpuState; typedef struct _MetaCursorNativePrivate @@ -209,7 +211,9 @@ get_active_cursor_sprite_gbm_bo (MetaCursorNativeGpuState *cursor_gpu_state) static void set_pending_cursor_sprite_gbm_bo (MetaCursorSprite *cursor_sprite, MetaGpuKms *gpu_kms, - struct gbm_bo *bo) + struct gbm_bo *bo, + int width, + int height) { MetaCursorNativePrivate *cursor_priv; MetaCursorNativeGpuState *cursor_gpu_state; @@ -220,6 +224,8 @@ set_pending_cursor_sprite_gbm_bo (MetaCursorSprite *cursor_sprite, pending_bo = get_pending_cursor_sprite_gbm_bo_index (cursor_gpu_state); cursor_gpu_state->bos[pending_bo] = bo; + cursor_gpu_state->widths[pending_bo] = width; + cursor_gpu_state->heights[pending_bo] = height; cursor_gpu_state->pending_bo_state = META_CURSOR_GBM_BO_STATE_SET; } @@ -260,8 +266,6 @@ set_crtc_cursor (MetaCursorRendererNative *native, meta_cursor_renderer_native_get_instance_private (native); MetaCursorNativePrivate *cursor_priv = get_cursor_priv (cursor_sprite); MetaGpuKms *gpu_kms = META_GPU_KMS (meta_crtc_get_gpu (crtc)); - MetaCursorRendererNativeGpuData *cursor_renderer_gpu_data = - meta_cursor_renderer_native_gpu_data_from_gpu (gpu_kms); MetaCursorNativeGpuState *cursor_gpu_state = get_cursor_gpu_state (cursor_priv, gpu_kms); MetaKmsCrtc *kms_crtc; @@ -278,9 +282,20 @@ set_crtc_cursor (MetaCursorRendererNative *native, MetaKmsPlaneAssignment *plane_assignment; if (cursor_gpu_state->pending_bo_state == META_CURSOR_GBM_BO_STATE_SET) - bo = get_pending_cursor_sprite_gbm_bo (cursor_gpu_state); + { + guint pending_bo; + + pending_bo = get_pending_cursor_sprite_gbm_bo_index (cursor_gpu_state); + bo = get_pending_cursor_sprite_gbm_bo (cursor_gpu_state); + cursor_width = cursor_gpu_state->widths[pending_bo]; + cursor_height = cursor_gpu_state->heights[pending_bo]; + } else - bo = get_active_cursor_sprite_gbm_bo (cursor_gpu_state); + { + bo = get_active_cursor_sprite_gbm_bo (cursor_gpu_state); + cursor_width = cursor_gpu_state->widths[cursor_gpu_state->active_bo]; + cursor_height = cursor_gpu_state->heights[cursor_gpu_state->active_bo]; + } kms_crtc = meta_crtc_kms_get_kms_crtc (crtc); kms_device = meta_kms_crtc_get_device (kms_crtc); @@ -289,8 +304,6 @@ set_crtc_cursor (MetaCursorRendererNative *native, handle = gbm_bo_get_handle (bo); - cursor_width = cursor_renderer_gpu_data->cursor_width; - cursor_height = cursor_renderer_gpu_data->cursor_height; src_rect = (MetaFixed16Rectangle) { .x = meta_fixed_16_from_int (0), .y = meta_fixed_16_from_int (0), @@ -1109,6 +1122,8 @@ invalidate_cursor_gpu_state (MetaCursorSprite *cursor_sprite) pending_bo = get_pending_cursor_sprite_gbm_bo_index (cursor_gpu_state); g_clear_pointer (&cursor_gpu_state->bos[pending_bo], gbm_bo_destroy); + cursor_gpu_state->widths[pending_bo] = 0; + cursor_gpu_state->heights[pending_bo] = 0; cursor_gpu_state->pending_bo_state = META_CURSOR_GBM_BO_STATE_INVALIDATED; } } @@ -1194,10 +1209,10 @@ load_cursor_sprite_gbm_buffer_for_gpu (MetaCursorRendererNative *native, GBM_BO_USE_CURSOR | GBM_BO_USE_WRITE)) { struct gbm_bo *bo; - uint8_t buf[4 * cursor_width * cursor_height]; + uint8_t buf[4 * width * height]; uint i; - bo = gbm_bo_create (gbm_device, cursor_width, cursor_height, + bo = gbm_bo_create (gbm_device, width, height, gbm_format, GBM_BO_USE_CURSOR | GBM_BO_USE_WRITE); if (!bo) { @@ -1207,8 +1222,8 @@ load_cursor_sprite_gbm_buffer_for_gpu (MetaCursorRendererNative *native, memset (buf, 0, sizeof(buf)); for (i = 0; i < height; i++) - memcpy (buf + i * 4 * cursor_width, pixels + i * rowstride, width * 4); - if (gbm_bo_write (bo, buf, cursor_width * cursor_height * 4) != 0) + memcpy (buf + i * 4 * width, pixels + i * rowstride, width * 4); + if (gbm_bo_write (bo, buf, width * height * 4) != 0) { meta_warning ("Failed to write cursors buffer data: %s", g_strerror (errno)); @@ -1216,7 +1231,8 @@ load_cursor_sprite_gbm_buffer_for_gpu (MetaCursorRendererNative *native, return; } - set_pending_cursor_sprite_gbm_bo (cursor_sprite, gpu_kms, bo); + set_pending_cursor_sprite_gbm_bo (cursor_sprite, gpu_kms, bo, + width, height); } else { @@ -1526,7 +1542,8 @@ realize_cursor_sprite_from_wl_buffer_for_gpu (MetaCursorRenderer *renderer, unset_can_preprocess (cursor_sprite); - set_pending_cursor_sprite_gbm_bo (cursor_sprite, gpu_kms, bo); + set_pending_cursor_sprite_gbm_bo (cursor_sprite, gpu_kms, bo, + width, height); } } #endif From 5848bfdd2c1110918a44f031cdbc1d58ba1726e3 Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Sun, 28 Jun 2026 19:24:45 +0200 Subject: [PATCH 3/6] meta-wayland-surface: Fix damage scaling when wp_viewport sets dst without src rect Fixes https://github.com/linuxmint/muffin/issues/835 When wp_viewport sets a destination size without a source rect, the viewport maps the full buffer to the destination size. Surface damage from wl_surface.damage is in destination/logical coordinates; after scaling by surface->scale, the damage region is in dst * scale coordinates. In this case the source rectangle passed to meta_region_crop_and_scale() must describe the full buffer. Using the destination-sized surface rect, or dividing the buffer size by surface->scale, makes the source rect too small and causes the viewport damage transform to clip or scale damage incorrectly. Use buffer_rect as the implicit source rectangle so damage is mapped through the viewport transform into the correct buffer coordinates. --- src/wayland/meta-wayland-surface.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/wayland/meta-wayland-surface.c b/src/wayland/meta-wayland-surface.c index d93b30e10..1cb39fb00 100644 --- a/src/wayland/meta-wayland-surface.c +++ b/src/wayland/meta-wayland-surface.c @@ -363,10 +363,26 @@ surface_process_damage (MetaWaylandSurface *surface, } else { - src_rect = (graphene_rect_t) { - .size.width = surface_rect.width * surface->scale, - .size.height = surface_rect.height * surface->scale, - }; + if (surface->viewport.has_dst_size) + { + /* When wp_viewport sets a destination without a source rect, the + * viewport maps the full buffer to the dst size. Surface damage + * (from wl_surface.damage) is in dst/logical coordinates and + * scaled_region is in dst * surface->scale coordinates. Use the + * full buffer as the source rectangle so meta_region_crop_and_scale() + * maps damage through the viewport transform to buffer coordinates. */ + src_rect = (graphene_rect_t) { + .size.width = buffer_rect.width, + .size.height = buffer_rect.height, + }; + } + else + { + src_rect = (graphene_rect_t) { + .size.width = surface_rect.width * surface->scale, + .size.height = surface_rect.height * surface->scale, + }; + } } viewport_region = meta_region_crop_and_scale (scaled_region, &src_rect, From a85a711e006f70e9f39027ee51597bfb8b749048 Mon Sep 17 00:00:00 2001 From: Clement Lefebvre Date: Tue, 30 Jun 2026 16:43:49 +0200 Subject: [PATCH 4/6] wayland: Handle early maximized client commits Some Wayland clients can request maximization before they have completed their first configure-ack-commit cycle, then commit a stale restored size instead of the compositor-configured maximized size. Track whether a client has acked a configure yet, preserve the compositor size for fixed-size states, and schedule an initial maximized-state transition when needed so clients update their maximized state after mapping. Fixes Slack/VScode, probably most Chromium-based apps, initial window mapping when they were maximized beforehand. These apps used to not fill the screen in HiDPI, though their state was maximized, leading to non-working titlebar unmaximize button, quarter-sized window dimensions and wrong coordinates. --- src/wayland/meta-wayland-xdg-shell.c | 16 +++++ src/wayland/meta-window-wayland.c | 89 ++++++++++++++++++++++++++++ src/wayland/meta-window-wayland.h | 3 + 3 files changed, 108 insertions(+) diff --git a/src/wayland/meta-wayland-xdg-shell.c b/src/wayland/meta-wayland-xdg-shell.c index a9c148605..c394dd777 100644 --- a/src/wayland/meta-wayland-xdg-shell.c +++ b/src/wayland/meta-wayland-xdg-shell.c @@ -438,6 +438,7 @@ xdg_toplevel_set_maximized (struct wl_client *client, { MetaWaylandSurface *surface = surface_from_xdg_toplevel_resource (resource); MetaWindow *window; + gboolean was_maximized; window = meta_wayland_surface_get_window (surface); if (!window) @@ -446,8 +447,23 @@ xdg_toplevel_set_maximized (struct wl_client *client, if (!window->has_maximize_func) return; + was_maximized = META_WINDOW_MAXIMIZED (window); + meta_window_force_placement (window, TRUE); + + /* If the window has not yet completed a configure-ack-commit cycle its + * renderer is not yet ready. Schedule a NOT_MAXIMIZED -> MAXIMIZED + * transition so clients that rely on an explicit transition update their + * maximize state. */ + if (!was_maximized) + meta_window_wayland_schedule_maximize_transition (window); + meta_window_maximize (window, META_MAXIMIZE_BOTH); + + /* meta_window_maximize() is a no-op when already maximized, but the client + * still expects a configure in reply. */ + if (was_maximized) + meta_window_wayland_maybe_send_configure (window); } static void diff --git a/src/wayland/meta-window-wayland.c b/src/wayland/meta-window-wayland.c index 521566096..50736a3f7 100644 --- a/src/wayland/meta-window-wayland.c +++ b/src/wayland/meta-window-wayland.c @@ -62,6 +62,15 @@ struct _MetaWindowWayland gboolean has_been_shown; + /* Set when a client requests maximization before it has completed a + * configure-ack-commit cycle. Cleared and consumed in finish_move_resize to + * force a NOT_MAXIMIZED -> MAXIMIZED transition for clients that only update + * their maximize state after seeing an explicit state transition. */ + gboolean needs_maximize_transition; + + /* Becomes TRUE the first time the client acks a tracked configure. */ + gboolean client_has_acked_configure; + cairo_surface_t *icon; cairo_surface_t *mini_icon; gboolean icon_dirty; @@ -995,6 +1004,7 @@ meta_window_wayland_finish_move_resize (MetaWindow *window, MetaMoveResizeFlags flags; MetaWaylandWindowConfiguration *acked_configuration; gboolean is_window_being_resized; + gboolean needs_reconfigure = FALSE; /* new_geom is in the logical pixel coordinate space, but MetaWindow wants its * rects to represent what in turn will end up on the stage, i.e. we need to @@ -1020,6 +1030,9 @@ meta_window_wayland_finish_move_resize (MetaWindow *window, acked_configuration = acquire_acked_configuration (wl_window, pending); + if (acked_configuration) + wl_window->client_has_acked_configure = TRUE; + /* x/y are ignored when we're doing interactive resizing */ is_window_being_resized = (meta_grab_op_is_resizing (display->grab_op) && display->grab_window == window); @@ -1067,6 +1080,42 @@ meta_window_wayland_finish_move_resize (MetaWindow *window, rect.x += dx; rect.y += dy; + /* For fixed-size states the compositor controls the size; the client must + * use what was configured. If it committed different dimensions (e.g. a + * stale saved size from a previous session), override with the expected + * size so we don't corrupt last_sent_rect and trigger a wrong configure. */ + if (META_WINDOW_MAXIMIZED (window) || + meta_window_is_fullscreen (window) || + window->tile_mode != META_TILE_NONE) + { + int expected_w, expected_h; + + if (acked_configuration && acked_configuration->has_size) + { + expected_w = acked_configuration->width; + expected_h = acked_configuration->height; + } + else if (wl_window->has_last_sent_configuration) + { + expected_w = wl_window->last_sent_rect.width; + expected_h = wl_window->last_sent_rect.height; + } + else + { + expected_w = new_geom.width; + expected_h = new_geom.height; + } + + if (new_geom.width != expected_w || new_geom.height != expected_h) + { + rect.x = wl_window->last_sent_rect.x; + rect.y = wl_window->last_sent_rect.y; + rect.width = expected_w; + rect.height = expected_h; + needs_reconfigure = TRUE; + } + } + if (rect.x != window->rect.x || rect.y != window->rect.y) flags |= META_MOVE_RESIZE_MOVE_ACTION; @@ -1085,6 +1134,26 @@ meta_window_wayland_finish_move_resize (MetaWindow *window, gravity = META_GRAVITY_STATIC; meta_window_move_resize_internal (window, flags, gravity, rect); + /* Force a NOT_MAXIMIZED -> MAXIMIZED configure transition for clients that + * need it to fire their 'maximize' event. Triggered when the client requested + * maximization before completing its first configure-ack-commit cycle. */ + { + gboolean do_maximize_transition = + META_WINDOW_MAXIMIZED (window) && wl_window->needs_maximize_transition; + + wl_window->needs_maximize_transition = FALSE; + + if (do_maximize_transition) + { + meta_window_unmaximize (window, META_MAXIMIZE_BOTH); + meta_window_maximize (window, META_MAXIMIZE_BOTH); + } + else if (needs_reconfigure) + { + surface_state_changed (window); + } + } + g_clear_pointer (&acked_configuration, meta_wayland_window_configuration_free); } @@ -1279,3 +1348,23 @@ meta_window_wayland_get_max_size (MetaWindow *window, scale = 1.0 / (float) meta_window_wayland_get_geometry_scale (window); scale_size (width, height, scale); } + +void +meta_window_wayland_maybe_send_configure (MetaWindow *window) +{ + MetaWindowWayland *wl_window = META_WINDOW_WAYLAND (window); + + if (!wl_window->has_last_sent_configuration) + return; + + surface_state_changed (window); +} + +void +meta_window_wayland_schedule_maximize_transition (MetaWindow *window) +{ + MetaWindowWayland *wl_window = META_WINDOW_WAYLAND (window); + + if (!wl_window->client_has_acked_configure) + wl_window->needs_maximize_transition = TRUE; +} diff --git a/src/wayland/meta-window-wayland.h b/src/wayland/meta-window-wayland.h index 71377c187..d63b361df 100644 --- a/src/wayland/meta-window-wayland.h +++ b/src/wayland/meta-window-wayland.h @@ -81,4 +81,7 @@ void meta_window_wayland_set_custom_icon (MetaWindow *window, cairo_surface_t *icon, cairo_surface_t *mini_icon); +void meta_window_wayland_maybe_send_configure (MetaWindow *window); +void meta_window_wayland_schedule_maximize_transition (MetaWindow *window); + #endif From 2ff6d6267973429d06e13c4f98645574a31bcae1 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Tue, 30 Jun 2026 17:46:34 -0400 Subject: [PATCH 5/6] wayland: Avoid size-change effects during forced maximize transition. --- src/compositor/compositor.c | 7 ++++++- src/core/window-private.h | 6 ++++++ src/wayland/meta-window-wayland.c | 7 +++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/compositor/compositor.c b/src/compositor/compositor.c index a9a1ddfb0..e95eaf0ba 100644 --- a/src/compositor/compositor.c +++ b/src/compositor/compositor.c @@ -1035,7 +1035,12 @@ meta_compositor_size_change_window (MetaCompositor *compositor, MetaRectangle *old_frame_rect, MetaRectangle *old_buffer_rect) { - MetaWindowActor *window_actor = meta_window_actor_from_window (window); + MetaWindowActor *window_actor; + + if (window->compositor_skip_size_change) + return; + + window_actor = meta_window_actor_from_window (window); meta_window_actor_size_change (window_actor, which_change, old_frame_rect, old_buffer_rect); } diff --git a/src/core/window-private.h b/src/core/window-private.h index 4e2aeab13..63bb21377 100644 --- a/src/core/window-private.h +++ b/src/core/window-private.h @@ -326,6 +326,12 @@ struct _MetaWindow */ guint pending_compositor_effect : 4; /* MetaCompEffect */ + /* Suppress compositor size-change effects for a synthetic state + * transition (e.g. an immediate unmaximize/maximize toggle) where there + * is no visible resize to animate. + */ + guint compositor_skip_size_change : 1; + /* Iconic is the state in WM_STATE; happens for workspaces/shading * in addition to minimize */ diff --git a/src/wayland/meta-window-wayland.c b/src/wayland/meta-window-wayland.c index 50736a3f7..7e491d754 100644 --- a/src/wayland/meta-window-wayland.c +++ b/src/wayland/meta-window-wayland.c @@ -1145,8 +1145,15 @@ meta_window_wayland_finish_move_resize (MetaWindow *window, if (do_maximize_transition) { + /* This toggle exists only to emit a state transition; the window is + * already maximized and stays maximized, so there is nothing to + * animate. Suppress the compositor size-change effects, whose paired + * synchronous firing would otherwise corrupt the actor's size-change + * accounting before either effect gets a chance to run. */ + window->compositor_skip_size_change = TRUE; meta_window_unmaximize (window, META_MAXIMIZE_BOTH); meta_window_maximize (window, META_MAXIMIZE_BOTH); + window->compositor_skip_size_change = FALSE; } else if (needs_reconfigure) { From a92dc1915a6c09a7ecb91ad3c5065ea4692ec0b6 Mon Sep 17 00:00:00 2001 From: Michael Webster Date: Tue, 30 Jun 2026 17:48:22 -0400 Subject: [PATCH 6/6] xdg-shell: Don't warn on geometry committed before first buffer. --- src/wayland/meta-wayland-xdg-shell.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/wayland/meta-wayland-xdg-shell.c b/src/wayland/meta-wayland-xdg-shell.c index c394dd777..acc181577 100644 --- a/src/wayland/meta-wayland-xdg-shell.c +++ b/src/wayland/meta-wayland-xdg-shell.c @@ -1734,8 +1734,22 @@ meta_wayland_xdg_surface_post_apply_state (MetaWaylandSurfaceRole *surface_role meta_wayland_shell_surface_determine_geometry (shell_surface, &pending->new_geometry, &priv->geometry); + if (priv->geometry.width == 0 || priv->geometry.height == 0) { + MetaWaylandSurface *surface = + meta_wayland_surface_role_get_surface (surface_role); + + /* A client may commit window geometry before attaching its first + * buffer (e.g. answering an initial maximized configure). The + * geometry can't be resolved against an empty surface; defer + * silently until a buffer arrives rather than warning. */ + if (!surface->buffer_ref->buffer) + { + priv->has_set_geometry = TRUE; + return; + } + g_warning ("Invalid window geometry for xdg_surface@%d. Ignoring " "for now, but this will result in client termination " "in the future.",