From e444836bf2e2638b64976397e07162cc8fcd4119 Mon Sep 17 00:00:00 2001 From: Andri Yngvason Date: Thu, 5 Mar 2026 10:23:06 +0000 Subject: [PATCH 01/37] image_capture_source/output: Update constraints on enable Without observing the enable event, clients receive no pixel formats and buffer dimensions are reported as 0 after an output has been re-enabled. (cherry picked from commit 3336d28813698eb6fba908a4dbcf38b79ed2e3db) --- types/ext_image_capture_source_v1/output.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/types/ext_image_capture_source_v1/output.c b/types/ext_image_capture_source_v1/output.c index 75fa15f4d..0e3a57823 100644 --- a/types/ext_image_capture_source_v1/output.c +++ b/types/ext_image_capture_source_v1/output.c @@ -110,6 +110,10 @@ static const struct wlr_ext_image_capture_source_v1_interface output_source_impl static void source_update_buffer_constraints(struct wlr_ext_output_image_capture_source_v1 *source) { struct wlr_output *output = source->output; + if (!output->enabled) { + return; + } + if (!wlr_output_configure_primary_swapchain(output, NULL, &output->swapchain)) { return; } @@ -123,7 +127,8 @@ static void source_handle_output_commit(struct wl_listener *listener, struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(listener, source, output_commit); struct wlr_output_event_commit *event = data; - if (event->state->committed & (WLR_OUTPUT_STATE_MODE | WLR_OUTPUT_STATE_RENDER_FORMAT)) { + if (event->state->committed & (WLR_OUTPUT_STATE_MODE | + WLR_OUTPUT_STATE_RENDER_FORMAT | WLR_OUTPUT_STATE_ENABLED)) { source_update_buffer_constraints(source); } From 557fde4d0101cef19f0ae06b82bb72bccd454e32 Mon Sep 17 00:00:00 2001 From: Simon Zeni Date: Fri, 6 Mar 2026 09:37:31 -0500 Subject: [PATCH 02/37] ci: update dalligi upstream repo (cherry picked from commit 67ce318b1fb4a5973e80b1ea721fbb23f210d665) --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e02463a9c..3b02ef16f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -include: https://git.sr.ht/~emersion/dalligi/blob/master/templates/multi.yml +include: https://gitlab.freedesktop.org/emersion/dalligi/-/raw/master/templates/multi.yml alpine: extends: .dalligi pages: true From 3105ac2b16fd9623808579594db12924aa941e29 Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Fri, 6 Mar 2026 18:44:26 -0800 Subject: [PATCH 03/37] scene: fix color format compare bool doesn't really support negative values. Fixes: 7cb3393e7 (scene: send color_management_v1 surface feedback) (cherry picked from commit 9a931d9ffa1f8771c5dc957e2c224192589633b0) --- types/scene/surface.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/scene/surface.c b/types/scene/surface.c index bce8c74a6..7b4d7fd36 100644 --- a/types/scene/surface.c +++ b/types/scene/surface.c @@ -38,7 +38,7 @@ static struct wlr_output *get_surface_frame_pacing_output(struct wlr_surface *su return frame_pacing_output; } -static bool get_tf_preference(enum wlr_color_transfer_function tf) { +static int get_tf_preference(enum wlr_color_transfer_function tf) { switch (tf) { case WLR_COLOR_TRANSFER_FUNCTION_GAMMA22: return 0; @@ -52,7 +52,7 @@ static bool get_tf_preference(enum wlr_color_transfer_function tf) { abort(); // unreachable } -static bool get_primaries_preference(enum wlr_color_named_primaries primaries) { +static int get_primaries_preference(enum wlr_color_named_primaries primaries) { switch (primaries) { case WLR_COLOR_NAMED_PRIMARIES_SRGB: return 0; From ec26eb250a5bb2c6d96a16080522e54a318eb4e4 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Thu, 26 Feb 2026 16:24:16 +0100 Subject: [PATCH 04/37] util/box: Use integer min/max for intersection wlr_box_intersection only operates on integers, so we shouldn't use fmin/fmax. Do the usual and add a local integer min/max helper. (cherry picked from commit 285cee5f3a0001db775009abccfee177f560e1a6) --- util/box.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/util/box.c b/util/box.c index aae09888f..489c5ad2e 100644 --- a/util/box.c +++ b/util/box.c @@ -4,6 +4,14 @@ #include #include +static int max(int a, int b) { + return a > b ? a : b; +} + +static int min(int a, int b) { + return a < b ? a : b; +} + void wlr_box_closest_point(const struct wlr_box *box, double x, double y, double *dest_x, double *dest_y) { // if box is empty, then it contains no points, so no closest point either @@ -56,10 +64,10 @@ bool wlr_box_intersection(struct wlr_box *dest, const struct wlr_box *box_a, return false; } - int x1 = fmax(box_a->x, box_b->x); - int y1 = fmax(box_a->y, box_b->y); - int x2 = fmin(box_a->x + box_a->width, box_b->x + box_b->width); - int y2 = fmin(box_a->y + box_a->height, box_b->y + box_b->height); + int x1 = max(box_a->x, box_b->x); + int y1 = max(box_a->y, box_b->y); + int x2 = min(box_a->x + box_a->width, box_b->x + box_b->width); + int y2 = min(box_a->y + box_a->height, box_b->y + box_b->height); dest->x = x1; dest->y = y1; From af228b879aa6f375cffd20345baa5c7530615b3b Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 10 Mar 2026 22:44:56 -0600 Subject: [PATCH 05/37] backend/x11: ignore DestroyNotify events The X11 backend subscribes to StructureNotify events, so when output_destroy() calls xcb_destroy_window() the server sends a DestroyNotify back. This is expected and harmless but was logged as an unhandled event. Silence it the same way MAP_NOTIFY and UNMAP_NOTIFY are already silenced. (cherry picked from commit 3c8d199ec13ef247c230a950e0ad3ab1b160ed59) --- backend/x11/backend.c | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/x11/backend.c b/backend/x11/backend.c index 0983bad0a..4dc4bf16d 100644 --- a/backend/x11/backend.c +++ b/backend/x11/backend.c @@ -115,6 +115,7 @@ static void handle_x11_event(struct wlr_x11_backend *x11, handle_x11_error(x11, ev); break; } + case XCB_DESTROY_NOTIFY: case XCB_UNMAP_NOTIFY: case XCB_MAP_NOTIFY: break; From b7275214491264ebfd29ef042c330abadd90f8bc Mon Sep 17 00:00:00 2001 From: Diego Viola Date: Mon, 9 Mar 2026 00:39:50 -0300 Subject: [PATCH 06/37] wlr-export-dmabuf-unstable-v1: fix typo (cherry picked from commit 736c0f3f25d813ce68ed0efc99f4068a7250866a) --- protocol/wlr-export-dmabuf-unstable-v1.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/wlr-export-dmabuf-unstable-v1.xml b/protocol/wlr-export-dmabuf-unstable-v1.xml index 80ea012f5..d41efbb1d 100644 --- a/protocol/wlr-export-dmabuf-unstable-v1.xml +++ b/protocol/wlr-export-dmabuf-unstable-v1.xml @@ -192,7 +192,7 @@ - Unreferences the frame. This request must be called as soon as its no + Unreferences the frame. This request must be called as soon as it's no longer used. It can be called at any time by the client. The client will still have From 9c3bfbeb480beea2ee4e59ad28ea80d2978d6641 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Fri, 13 Mar 2026 17:43:52 +0100 Subject: [PATCH 07/37] scene: avoid redundant wl_surface.enter/leave events Currently we send wl_surface.enter/leave when a surface is hidden and shown again on the same output. In practice, this happens very often since compositors like river and sway enable and disable the scene nodes of surfaces as part of their atomic transaction strategy involving rendering saved buffers while waiting for clients to submit new buffers of the desired size. The new strategy documented in the new comments avoids sending redundant events in this case. (cherry picked from commit 39e918edc89306661b4f96393b557e25523adc40) --- include/wlr/types/wlr_scene.h | 2 -- types/scene/surface.c | 58 +++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/include/wlr/types/wlr_scene.h b/include/wlr/types/wlr_scene.h index 46635f4bf..962b01bcc 100644 --- a/include/wlr/types/wlr_scene.h +++ b/include/wlr/types/wlr_scene.h @@ -131,8 +131,6 @@ struct wlr_scene_surface { struct wlr_addon addon; struct wl_listener outputs_update; - struct wl_listener output_enter; - struct wl_listener output_leave; struct wl_listener output_sample; struct wl_listener frame_done; struct wl_listener surface_destroy; diff --git a/types/scene/surface.c b/types/scene/surface.c index 7b4d7fd36..c7a53a1b3 100644 --- a/types/scene/surface.c +++ b/types/scene/surface.c @@ -94,14 +94,44 @@ static void handle_scene_buffer_outputs_update( struct wl_listener *listener, void *data) { struct wlr_scene_surface *surface = wl_container_of(listener, surface, outputs_update); + struct wlr_scene_outputs_update_event *event = data; struct wlr_scene *scene = scene_node_get_root(&surface->buffer->node); // If the surface is no longer visible on any output, keep the last sent // preferred configuration to avoid unnecessary redraws - if (wl_list_empty(&surface->surface->current_outputs)) { + if (event->size == 0) { return; } + // To avoid sending redundant leave/enter events when a surface is hidden and then shown + // without moving to a different output the following policy is implemented: + // + // 1. When a surface transitions from being visible on >0 outputs to being visible on 0 outputs + // don't send any leave events. + // + // 2. When a surface transitions from being visible on 0 outputs to being visible on >0 outputs + // send leave events for all entered outputs on which the surface is no longer visible as + // well as enter events for any outputs not already entered. + struct wlr_surface_output *entered_output; + wl_list_for_each(entered_output, &surface->surface->current_outputs, link) { + bool active = false; + for (size_t i = 0; i < event->size; i++) { + if (entered_output->output == event->active[i]->output) { + active = true; + break; + } + } + if (!active) { + wlr_surface_send_leave(surface->surface, entered_output->output); + } + } + + for (size_t i = 0; i < event->size; i++) { + // This function internally checks if an enter event was already sent for the output + // to avoid sending redundant events. + wlr_surface_send_enter(surface->surface, event->active[i]->output); + } + double scale = get_surface_preferred_buffer_scale(surface->surface); wlr_fractional_scale_v1_notify_scale(surface->surface, scale); wlr_surface_set_preferred_buffer_scale(surface->surface, ceil(scale)); @@ -114,24 +144,6 @@ static void handle_scene_buffer_outputs_update( } } -static void handle_scene_buffer_output_enter( - struct wl_listener *listener, void *data) { - struct wlr_scene_surface *surface = - wl_container_of(listener, surface, output_enter); - struct wlr_scene_output *output = data; - - wlr_surface_send_enter(surface->surface, output->output); -} - -static void handle_scene_buffer_output_leave( - struct wl_listener *listener, void *data) { - struct wlr_scene_surface *surface = - wl_container_of(listener, surface, output_leave); - struct wlr_scene_output *output = data; - - wlr_surface_send_leave(surface->surface, output->output); -} - static void handle_scene_buffer_output_sample( struct wl_listener *listener, void *data) { struct wlr_scene_surface *surface = @@ -380,8 +392,6 @@ static void surface_addon_destroy(struct wlr_addon *addon) { wlr_addon_finish(&surface->addon); wl_list_remove(&surface->outputs_update.link); - wl_list_remove(&surface->output_enter.link); - wl_list_remove(&surface->output_leave.link); wl_list_remove(&surface->output_sample.link); wl_list_remove(&surface->frame_done.link); wl_list_remove(&surface->surface_destroy.link); @@ -427,12 +437,6 @@ struct wlr_scene_surface *wlr_scene_surface_create(struct wlr_scene_tree *parent surface->outputs_update.notify = handle_scene_buffer_outputs_update; wl_signal_add(&scene_buffer->events.outputs_update, &surface->outputs_update); - surface->output_enter.notify = handle_scene_buffer_output_enter; - wl_signal_add(&scene_buffer->events.output_enter, &surface->output_enter); - - surface->output_leave.notify = handle_scene_buffer_output_leave; - wl_signal_add(&scene_buffer->events.output_leave, &surface->output_leave); - surface->output_sample.notify = handle_scene_buffer_output_sample; wl_signal_add(&scene_buffer->events.output_sample, &surface->output_sample); From ba32abbddb01eed5eda5b2ccd6be439814f9addc Mon Sep 17 00:00:00 2001 From: llyyr Date: Fri, 13 Mar 2026 23:32:06 +0530 Subject: [PATCH 08/37] scene: use wl_list_for_each_safe to iterate outputs The outputs loop in handle_scene_buffer_outputs_update may remove entries from the list while iterating, so use wl_list_for_each_safe instead of wl_list_for_each. Fixes: 39e918edc893 ("scene: avoid redundant wl_surface.enter/leave events") (cherry picked from commit 3cb2cf9425a35333661b1edcc0d34e92ea109fd3) --- types/scene/surface.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/scene/surface.c b/types/scene/surface.c index c7a53a1b3..7aa75c6d4 100644 --- a/types/scene/surface.c +++ b/types/scene/surface.c @@ -112,8 +112,8 @@ static void handle_scene_buffer_outputs_update( // 2. When a surface transitions from being visible on 0 outputs to being visible on >0 outputs // send leave events for all entered outputs on which the surface is no longer visible as // well as enter events for any outputs not already entered. - struct wlr_surface_output *entered_output; - wl_list_for_each(entered_output, &surface->surface->current_outputs, link) { + struct wlr_surface_output *entered_output, *tmp; + wl_list_for_each_safe(entered_output, tmp, &surface->surface->current_outputs, link) { bool active = false; for (size_t i = 0; i < event->size; i++) { if (entered_output->output == event->active[i]->output) { From 47a43e14ae3b746bed1ee7781966959626ade04f Mon Sep 17 00:00:00 2001 From: Scott Moreau Date: Thu, 12 Mar 2026 17:34:41 -0600 Subject: [PATCH 09/37] wlr_ext_image_copy_capture_v1: Fix crash when client creates a cursor session not implemented server side This guards against a crash where the server implements wlr_ext_image_capture_source_v1_interface without setting .get_pointer_cursor(). In general, we should install a NULL check here because this is a crash waiting to happen. Now, instead of crashing, the resource will be created and the copy capture session will be stopped. (cherry picked from commit 1fc928d5280b77fdc19238225a64ad0585f8b723) --- types/wlr_ext_image_copy_capture_v1.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/types/wlr_ext_image_copy_capture_v1.c b/types/wlr_ext_image_copy_capture_v1.c index c9e7b0079..2e969b4a9 100644 --- a/types/wlr_ext_image_copy_capture_v1.c +++ b/types/wlr_ext_image_copy_capture_v1.c @@ -518,14 +518,16 @@ static void cursor_session_handle_get_capture_session(struct wl_client *client, return; } - cursor_session->capture_session_created = true; - + struct wlr_ext_image_copy_capture_manager_v1 *manager = NULL; struct wlr_ext_image_capture_source_v1 *source = NULL; + if (cursor_session != NULL) { + manager = cursor_session->manager; + cursor_session->capture_session_created = true; source = &cursor_session->source->base; } - session_create(cursor_session_resource, new_id, source, 0, cursor_session->manager); + session_create(cursor_session_resource, new_id, source, 0, manager); } static const struct ext_image_copy_capture_cursor_session_v1_interface cursor_session_impl = { From 6a902237efb5f090fcbbce8479981812c7e82fc4 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Tue, 17 Mar 2026 09:29:21 +0100 Subject: [PATCH 10/37] virtual-keyboard: add wlr_virtual_keyboard_v1_from_resource() I want to use the zwp_virtual_keyboard_v1 object in a custom river protocol and need to be able to obtain the corresponding wlroots struct. (cherry picked from commit ec746d3e3ee2f1f4a9bd49e800b3ba8b73be72a9) --- include/wlr/types/wlr_virtual_keyboard_v1.h | 11 +++++++++++ types/wlr_virtual_keyboard_v1.c | 10 +++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/include/wlr/types/wlr_virtual_keyboard_v1.h b/include/wlr/types/wlr_virtual_keyboard_v1.h index babc535a5..a5fc589cb 100644 --- a/include/wlr/types/wlr_virtual_keyboard_v1.h +++ b/include/wlr/types/wlr_virtual_keyboard_v1.h @@ -38,7 +38,18 @@ struct wlr_virtual_keyboard_v1 { struct wlr_virtual_keyboard_manager_v1* wlr_virtual_keyboard_manager_v1_create( struct wl_display *display); +/** + * Get the struct wlr_virtual_keyboard_v1 corresponding to a zwp_virtual_keyboard_v1 resource. + * + * Asserts that the resource is a valid zwp_virtual_keyboard_v1 resource created by wlroots. + * + * Returns NULL if the resource is inert. + */ +struct wlr_virtual_keyboard_v1 *wlr_virtual_keyboard_v1_from_resource( + struct wl_resource *resource); + struct wlr_virtual_keyboard_v1 *wlr_input_device_get_virtual_keyboard( struct wlr_input_device *wlr_dev); + #endif diff --git a/types/wlr_virtual_keyboard_v1.c b/types/wlr_virtual_keyboard_v1.c index e7dcb3ec3..2bc703fb4 100644 --- a/types/wlr_virtual_keyboard_v1.c +++ b/types/wlr_virtual_keyboard_v1.c @@ -15,7 +15,7 @@ static const struct wlr_keyboard_impl keyboard_impl = { static const struct zwp_virtual_keyboard_v1_interface virtual_keyboard_impl; -static struct wlr_virtual_keyboard_v1 *virtual_keyboard_from_resource( +struct wlr_virtual_keyboard_v1 *wlr_virtual_keyboard_v1_from_resource( struct wl_resource *resource) { assert(wl_resource_instance_of(resource, &zwp_virtual_keyboard_v1_interface, &virtual_keyboard_impl)); @@ -39,7 +39,7 @@ static void virtual_keyboard_keymap(struct wl_client *client, struct wl_resource *resource, uint32_t format, int32_t fd, uint32_t size) { struct wlr_virtual_keyboard_v1 *keyboard = - virtual_keyboard_from_resource(resource); + wlr_virtual_keyboard_v1_from_resource(resource); if (keyboard == NULL) { return; } @@ -76,7 +76,7 @@ static void virtual_keyboard_key(struct wl_client *client, struct wl_resource *resource, uint32_t time, uint32_t key, uint32_t state) { struct wlr_virtual_keyboard_v1 *keyboard = - virtual_keyboard_from_resource(resource); + wlr_virtual_keyboard_v1_from_resource(resource); if (keyboard == NULL) { return; } @@ -99,7 +99,7 @@ static void virtual_keyboard_modifiers(struct wl_client *client, struct wl_resource *resource, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { struct wlr_virtual_keyboard_v1 *keyboard = - virtual_keyboard_from_resource(resource); + wlr_virtual_keyboard_v1_from_resource(resource); if (keyboard == NULL) { return; } @@ -115,7 +115,7 @@ static void virtual_keyboard_modifiers(struct wl_client *client, static void virtual_keyboard_destroy_resource(struct wl_resource *resource) { struct wlr_virtual_keyboard_v1 *keyboard = - virtual_keyboard_from_resource(resource); + wlr_virtual_keyboard_v1_from_resource(resource); if (keyboard == NULL) { return; } From 88718e84c9e325a4b87b3ad8d261d8498dfcdc37 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Tue, 17 Mar 2026 09:51:50 +0100 Subject: [PATCH 11/37] virtual-keyboard: handle seat destroy We must make the virtual keyboard inert when the seat is destroyed. (cherry picked from commit 1fa8bb8f7ac0b68aee1fb6754446039f51b1a380) --- include/wlr/types/wlr_virtual_keyboard_v1.h | 4 ++ types/wlr_virtual_keyboard_v1.c | 41 ++++++++++++++------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/include/wlr/types/wlr_virtual_keyboard_v1.h b/include/wlr/types/wlr_virtual_keyboard_v1.h index a5fc589cb..dda1530b1 100644 --- a/include/wlr/types/wlr_virtual_keyboard_v1.h +++ b/include/wlr/types/wlr_virtual_keyboard_v1.h @@ -33,6 +33,10 @@ struct wlr_virtual_keyboard_v1 { bool has_keymap; struct wl_list link; // wlr_virtual_keyboard_manager_v1.virtual_keyboards + + struct { + struct wl_listener seat_destroy; + } WLR_PRIVATE; }; struct wlr_virtual_keyboard_manager_v1* wlr_virtual_keyboard_manager_v1_create( diff --git a/types/wlr_virtual_keyboard_v1.c b/types/wlr_virtual_keyboard_v1.c index 2bc703fb4..011fbfec7 100644 --- a/types/wlr_virtual_keyboard_v1.c +++ b/types/wlr_virtual_keyboard_v1.c @@ -113,21 +113,24 @@ static void virtual_keyboard_modifiers(struct wl_client *client, mods_depressed, mods_latched, mods_locked, group); } -static void virtual_keyboard_destroy_resource(struct wl_resource *resource) { - struct wlr_virtual_keyboard_v1 *keyboard = - wlr_virtual_keyboard_v1_from_resource(resource); - if (keyboard == NULL) { - return; - } - - wlr_keyboard_finish(&keyboard->keyboard); - - wl_resource_set_user_data(keyboard->resource, NULL); - wl_list_remove(&keyboard->link); - free(keyboard); +static void virtual_keyboard_destroy(struct wlr_virtual_keyboard_v1 *virtual_keyboard) { + wlr_keyboard_finish(&virtual_keyboard->keyboard); + wl_resource_set_user_data(virtual_keyboard->resource, NULL); + wl_list_remove(&virtual_keyboard->seat_destroy.link); + wl_list_remove(&virtual_keyboard->link); + free(virtual_keyboard); } -static void virtual_keyboard_destroy(struct wl_client *client, +static void virtual_keyboard_destroy_resource(struct wl_resource *resource) { + struct wlr_virtual_keyboard_v1 *virtual_keyboard = + wlr_virtual_keyboard_v1_from_resource(resource); + if (virtual_keyboard == NULL) { + return; + } + virtual_keyboard_destroy(virtual_keyboard); +} + +static void virtual_keyboard_handle_destroy(struct wl_client *client, struct wl_resource *resource) { wl_resource_destroy(resource); } @@ -136,7 +139,7 @@ static const struct zwp_virtual_keyboard_v1_interface virtual_keyboard_impl = { .keymap = virtual_keyboard_keymap, .key = virtual_keyboard_key, .modifiers = virtual_keyboard_modifiers, - .destroy = virtual_keyboard_destroy, + .destroy = virtual_keyboard_handle_destroy, }; static const struct zwp_virtual_keyboard_manager_v1_interface manager_impl; @@ -148,6 +151,13 @@ static struct wlr_virtual_keyboard_manager_v1 *manager_from_resource( return wl_resource_get_user_data(resource); } +static void virtual_keyboard_handle_seat_destroy(struct wl_listener *listener, + void *data) { + struct wlr_virtual_keyboard_v1 *virtual_keyboard = wl_container_of(listener, virtual_keyboard, + seat_destroy); + virtual_keyboard_destroy(virtual_keyboard); +} + static void virtual_keyboard_manager_create_virtual_keyboard( struct wl_client *client, struct wl_resource *resource, struct wl_resource *seat, uint32_t id) { @@ -181,6 +191,9 @@ static void virtual_keyboard_manager_create_virtual_keyboard( virtual_keyboard->seat = seat_client->seat; wl_resource_set_user_data(keyboard_resource, virtual_keyboard); + wl_signal_add(&seat_client->events.destroy, &virtual_keyboard->seat_destroy); + virtual_keyboard->seat_destroy.notify = virtual_keyboard_handle_seat_destroy; + wl_list_insert(&manager->virtual_keyboards, &virtual_keyboard->link); wl_signal_emit_mutable(&manager->events.new_virtual_keyboard, From c4ff394f7f362003f8f0b6f4dda995a82fcc670a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 31 Jan 2025 18:23:22 +0000 Subject: [PATCH 12/37] render/drm_syncobj: add wlr_drm_syncobj_timeline_signal() (cherry picked from commit 0af9b9d003bd6b565f30f94755bd9f9a81c0926d) --- include/wlr/render/drm_syncobj.h | 4 ++++ render/drm_syncobj.c | 8 ++++++++ types/wlr_linux_drm_syncobj_v1.c | 6 +----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/include/wlr/render/drm_syncobj.h b/include/wlr/render/drm_syncobj.h index deef72dc9..fabe23d1c 100644 --- a/include/wlr/render/drm_syncobj.h +++ b/include/wlr/render/drm_syncobj.h @@ -90,6 +90,10 @@ bool wlr_drm_syncobj_timeline_transfer(struct wlr_drm_syncobj_timeline *dst, */ bool wlr_drm_syncobj_timeline_check(struct wlr_drm_syncobj_timeline *timeline, uint64_t point, uint32_t flags, bool *result); +/** + * Signals a timeline point. + */ +bool wlr_drm_syncobj_timeline_signal(struct wlr_drm_syncobj_timeline *timeline, uint64_t point); /** * Asynchronously wait for a timeline point. * diff --git a/render/drm_syncobj.c b/render/drm_syncobj.c index e1a407a1e..2abe2cff6 100644 --- a/render/drm_syncobj.c +++ b/render/drm_syncobj.c @@ -177,6 +177,14 @@ bool wlr_drm_syncobj_timeline_check(struct wlr_drm_syncobj_timeline *timeline, return true; } +bool wlr_drm_syncobj_timeline_signal(struct wlr_drm_syncobj_timeline *timeline, uint64_t point) { + if (drmSyncobjTimelineSignal(timeline->drm_fd, &timeline->handle, &point, 1) != 0) { + wlr_log(WLR_ERROR, "drmSyncobjTimelineSignal() failed"); + return false; + } + return true; +} + static int handle_eventfd_ready(int ev_fd, uint32_t mask, void *data) { struct wlr_drm_syncobj_timeline_waiter *waiter = data; diff --git a/types/wlr_linux_drm_syncobj_v1.c b/types/wlr_linux_drm_syncobj_v1.c index 988d44e01..a2e56dcbb 100644 --- a/types/wlr_linux_drm_syncobj_v1.c +++ b/types/wlr_linux_drm_syncobj_v1.c @@ -475,11 +475,7 @@ struct release_signaller { static void release_signaller_handle_buffer_release(struct wl_listener *listener, void *data) { struct release_signaller *signaller = wl_container_of(listener, signaller, buffer_release); - if (drmSyncobjTimelineSignal(signaller->timeline->drm_fd, &signaller->timeline->handle, - &signaller->point, 1) != 0) { - wlr_log(WLR_ERROR, "drmSyncobjTimelineSignal() failed"); - } - + wlr_drm_syncobj_timeline_signal(signaller->timeline, signaller->point); wlr_drm_syncobj_timeline_unref(signaller->timeline); wl_list_remove(&signaller->buffer_release.link); free(signaller); From 2f14845ce0f95ebb079835c777987569ea27c189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 31 Jan 2025 18:36:07 +0000 Subject: [PATCH 13/37] scene: add buffer release point to 'sample' event (cherry picked from commit 1f3d351abb1585454098830ee3a43e4cfef67550) --- include/wlr/types/wlr_scene.h | 4 ++++ types/scene/wlr_scene.c | 27 +++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/include/wlr/types/wlr_scene.h b/include/wlr/types/wlr_scene.h index 962b01bcc..6c627b1f9 100644 --- a/include/wlr/types/wlr_scene.h +++ b/include/wlr/types/wlr_scene.h @@ -153,6 +153,8 @@ struct wlr_scene_outputs_update_event { struct wlr_scene_output_sample_event { struct wlr_scene_output *output; bool direct_scanout; + struct wlr_drm_syncobj_timeline *release_timeline; + uint64_t release_point; }; struct wlr_scene_frame_done_event { @@ -266,6 +268,8 @@ struct wlr_scene_output { struct wlr_drm_syncobj_timeline *in_timeline; uint64_t in_point; + struct wlr_drm_syncobj_timeline *out_timeline; + uint64_t out_point; } WLR_PRIVATE; }; diff --git a/types/scene/wlr_scene.c b/types/scene/wlr_scene.c index 51373e15a..54c09bbfe 100644 --- a/types/scene/wlr_scene.c +++ b/types/scene/wlr_scene.c @@ -1553,6 +1553,8 @@ static void scene_entry_render(struct render_list_entry *entry, const struct ren struct wlr_scene_output_sample_event sample_event = { .output = data->output, .direct_scanout = false, + .release_timeline = data->output->in_timeline, + .release_point = data->output->in_point, }; wl_signal_emit_mutable(&scene_buffer->events.output_sample, &sample_event); @@ -1785,7 +1787,10 @@ struct wlr_scene_output *wlr_scene_output_create(struct wlr_scene *scene, if (drm_fd >= 0 && output->backend->features.timeline && output->renderer != NULL && output->renderer->features.timeline) { scene_output->in_timeline = wlr_drm_syncobj_timeline_create(drm_fd); - if (scene_output->in_timeline == NULL) { + scene_output->out_timeline = wlr_drm_syncobj_timeline_create(drm_fd); + if (scene_output->in_timeline == NULL || scene_output->out_timeline == NULL) { + wlr_drm_syncobj_timeline_unref(scene_output->in_timeline); + wlr_drm_syncobj_timeline_unref(scene_output->out_timeline); return NULL; } } @@ -1840,7 +1845,14 @@ void wlr_scene_output_destroy(struct wlr_scene_output *scene_output) { wl_list_remove(&scene_output->output_commit.link); wl_list_remove(&scene_output->output_damage.link); wl_list_remove(&scene_output->output_needs_frame.link); - wlr_drm_syncobj_timeline_unref(scene_output->in_timeline); + if (scene_output->in_timeline != NULL) { + wlr_drm_syncobj_timeline_signal(scene_output->in_timeline, UINT64_MAX); + wlr_drm_syncobj_timeline_unref(scene_output->in_timeline); + } + if (scene_output->out_timeline != NULL) { + wlr_drm_syncobj_timeline_signal(scene_output->out_timeline, UINT64_MAX); + wlr_drm_syncobj_timeline_unref(scene_output->out_timeline); + } wlr_color_transform_unref(scene_output->gamma_lut_color_transform); wlr_color_transform_unref(scene_output->prev_gamma_lut_color_transform); wlr_color_transform_unref(scene_output->prev_supplied_color_transform); @@ -2136,6 +2148,12 @@ static enum scene_direct_scanout_result scene_entry_try_direct_scanout( if (buffer->wait_timeline != NULL) { wlr_output_state_set_wait_timeline(&pending, buffer->wait_timeline, buffer->wait_point); } + + if (scene_output->out_timeline) { + scene_output->out_point++; + wlr_output_state_set_signal_timeline(&pending, scene_output->out_timeline, scene_output->out_point); + } + if (!wlr_output_test_state(scene_output->output, &pending)) { wlr_output_state_finish(&pending); return SCANOUT_CANDIDATE; @@ -2147,6 +2165,8 @@ static enum scene_direct_scanout_result scene_entry_try_direct_scanout( struct wlr_scene_output_sample_event sample_event = { .output = scene_output, .direct_scanout = true, + .release_timeline = data->output->out_timeline, + .release_point = data->output->out_point, }; wl_signal_emit_mutable(&buffer->events.output_sample, &sample_event); return SCANOUT_SUCCESS; @@ -2606,6 +2626,9 @@ bool wlr_scene_output_build_state(struct wlr_scene_output *scene_output, if (scene_output->in_timeline != NULL) { wlr_output_state_set_wait_timeline(state, scene_output->in_timeline, scene_output->in_point); + scene_output->out_point++; + wlr_output_state_set_signal_timeline(state, scene_output->out_timeline, + scene_output->out_point); } if (!render_gamma_lut) { From 288ba9e75b65feabf0bb7f7dd9c73659d9da9230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 31 Jan 2025 18:43:23 +0000 Subject: [PATCH 14/37] drm/syncobj: add timeline point merger utility (cherry picked from commit e83a679e2363d27275996cb64fe51d8804bdd7f1) --- include/render/drm_syncobj_merger.h | 44 +++++++++ render/drm_syncobj_merger.c | 133 ++++++++++++++++++++++++++++ render/meson.build | 2 + 3 files changed, 179 insertions(+) create mode 100644 include/render/drm_syncobj_merger.h create mode 100644 render/drm_syncobj_merger.c diff --git a/include/render/drm_syncobj_merger.h b/include/render/drm_syncobj_merger.h new file mode 100644 index 000000000..c55c87314 --- /dev/null +++ b/include/render/drm_syncobj_merger.h @@ -0,0 +1,44 @@ +#ifndef WLR_RENDER_DRM_SYNCOBJ_MERGER_H +#define WLR_RENDER_DRM_SYNCOBJ_MERGER_H + +#include + +/** + * Accumulate timeline points, to have a destination timeline point be + * signalled when all inputs are + */ +struct wlr_drm_syncobj_merger { + int n_ref; + struct wlr_drm_syncobj_timeline *dst_timeline; + uint64_t dst_point; + int sync_fd; +}; + +/** + * Create a new merger. + * + * The given timeline point will be signalled when all input points are + * signalled and the merger is destroyed. + */ +struct wlr_drm_syncobj_merger *wlr_drm_syncobj_merger_create( + struct wlr_drm_syncobj_timeline *dst_timeline, uint64_t dst_point); + +struct wlr_drm_syncobj_merger *wlr_drm_syncobj_merger_ref( + struct wlr_drm_syncobj_merger *merger); + +/** + * Target timeline point is materialized when all inputs are, and the merger is + * destroyed. + */ +void wlr_drm_syncobj_merger_unref(struct wlr_drm_syncobj_merger *merger); +/** + * Add a new timeline point to wait for. + * + * If the point is not materialized, the supplied event loop is used to schedule + * a wait. + */ +bool wlr_drm_syncobj_merger_add(struct wlr_drm_syncobj_merger *merger, + struct wlr_drm_syncobj_timeline *dst_timeline, uint64_t dst_point, + struct wl_event_loop *loop); + +#endif diff --git a/render/drm_syncobj_merger.c b/render/drm_syncobj_merger.c new file mode 100644 index 000000000..d50d28c28 --- /dev/null +++ b/render/drm_syncobj_merger.c @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "render/drm_syncobj_merger.h" + +#include "config.h" + +#if HAVE_LINUX_SYNC_FILE + +#include +#include + +static int sync_file_merge(int fd1, int fd2) { + // The kernel will automatically prune signalled fences + struct sync_merge_data merge_data = { .fd2 = fd2 }; + if (ioctl(fd1, SYNC_IOC_MERGE, &merge_data) < 0) { + wlr_log_errno(WLR_ERROR, "ioctl(SYNC_IOC_MERGE) failed"); + return -1; + } + + return merge_data.fence; +} + +#else + +static int sync_file_merge(int fd1, int fd2) { + wlr_log(WLR_ERROR, "sync_file support is unavailable"); + return -1; +} + +#endif + +struct wlr_drm_syncobj_merger *wlr_drm_syncobj_merger_create( + struct wlr_drm_syncobj_timeline *dst_timeline, uint64_t dst_point) { + struct wlr_drm_syncobj_merger *merger = calloc(1, sizeof(*merger)); + if (merger == NULL) { + return NULL; + } + merger->n_ref = 1; + merger->dst_timeline = wlr_drm_syncobj_timeline_ref(dst_timeline); + merger->dst_point = dst_point; + merger->sync_fd = -1; + return merger; +} + +struct wlr_drm_syncobj_merger *wlr_drm_syncobj_merger_ref( + struct wlr_drm_syncobj_merger *merger) { + assert(merger->n_ref > 0); + merger->n_ref++; + return merger; +} + +void wlr_drm_syncobj_merger_unref(struct wlr_drm_syncobj_merger *merger) { + if (merger == NULL) { + return; + } + assert(merger->n_ref > 0); + merger->n_ref--; + if (merger->n_ref > 0) { + return; + } + + if (merger->sync_fd != -1) { + wlr_drm_syncobj_timeline_import_sync_file(merger->dst_timeline, + merger->dst_point, merger->sync_fd); + close(merger->sync_fd); + } else { + wlr_drm_syncobj_timeline_signal(merger->dst_timeline, merger->dst_point); + } + wlr_drm_syncobj_timeline_unref(merger->dst_timeline); + free(merger); +} + +static bool merger_add_exportable(struct wlr_drm_syncobj_merger *merger, + struct wlr_drm_syncobj_timeline *src_timeline, uint64_t src_point) { + int new_sync = wlr_drm_syncobj_timeline_export_sync_file(src_timeline, src_point); + if (merger->sync_fd != -1) { + int fd2 = new_sync; + new_sync = sync_file_merge(merger->sync_fd, fd2); + close(fd2); + close(merger->sync_fd); + } + merger->sync_fd = new_sync; + return true; +} + +struct export_waiter { + struct wlr_drm_syncobj_timeline_waiter waiter; + struct wlr_drm_syncobj_merger *merger; + struct wlr_drm_syncobj_timeline *src_timeline; + uint64_t src_point; +}; + +static void export_waiter_handle_ready(struct wlr_drm_syncobj_timeline_waiter *waiter) { + struct export_waiter *add = wl_container_of(waiter, add, waiter); + merger_add_exportable(add->merger, add->src_timeline, add->src_point); + wlr_drm_syncobj_merger_unref(add->merger); + wlr_drm_syncobj_timeline_unref(add->src_timeline); + wlr_drm_syncobj_timeline_waiter_finish(&add->waiter); + free(add); +} + +bool wlr_drm_syncobj_merger_add(struct wlr_drm_syncobj_merger *merger, + struct wlr_drm_syncobj_timeline *src_timeline, uint64_t src_point, + struct wl_event_loop *loop) { + assert(loop != NULL); + bool exportable = false; + int flags = DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE; + if (!wlr_drm_syncobj_timeline_check(src_timeline, src_point, flags, &exportable)) { + return false; + } + if (exportable) { + return merger_add_exportable(merger, src_timeline, src_point); + } + struct export_waiter *add = calloc(1, sizeof(*add)); + if (add == NULL) { + return false; + } + if (!wlr_drm_syncobj_timeline_waiter_init(&add->waiter, src_timeline, src_point, + flags, loop, export_waiter_handle_ready)) { + return false; + } + add->merger = merger; + add->src_timeline = wlr_drm_syncobj_timeline_ref(src_timeline); + add->src_point = src_point; + merger->n_ref++; + return true; +} diff --git a/render/meson.build b/render/meson.build index aaaf2ec48..517d76bf0 100644 --- a/render/meson.build +++ b/render/meson.build @@ -9,6 +9,7 @@ wlr_files += files( 'color.c', 'dmabuf.c', 'drm_format_set.c', + 'drm_syncobj_merger.c', 'drm_syncobj.c', 'pass.c', 'pixel_format.c', @@ -28,6 +29,7 @@ else endif internal_config.set10('HAVE_EVENTFD', cc.has_header('sys/eventfd.h')) +internal_config.set10('HAVE_LINUX_SYNC_FILE', cc.has_header('linux/sync_file.h')) if 'gles2' in renderers or 'auto' in renderers egl = dependency('egl', required: 'gles2' in renderers) From 52564ea97c5e335e94d6478c172a3db0486cf145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Thu, 12 Mar 2026 23:03:10 +0000 Subject: [PATCH 15/37] linux_drm_syncobj_v1: add release point accumulation This changes the behavior of wlr_linux_drm_syncobj_surface_v1 to automatically signal release of previous commits as they are replaced. Users must call wlr_linux_drm_syncobj_v1_state_add_release_point or wlr_linux_drm_syncobj_v1_state_signal_release_with_buffer to delay the signal as appropriate. (cherry picked from commit bfd6e619fc8642fb3f2cca6f9cb94d257def8780) --- include/wlr/types/wlr_linux_drm_syncobj_v1.h | 22 +++++++++- types/wlr_linux_drm_syncobj_v1.c | 45 ++++++++++++++++---- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/include/wlr/types/wlr_linux_drm_syncobj_v1.h b/include/wlr/types/wlr_linux_drm_syncobj_v1.h index 733350412..70a3f547f 100644 --- a/include/wlr/types/wlr_linux_drm_syncobj_v1.h +++ b/include/wlr/types/wlr_linux_drm_syncobj_v1.h @@ -19,8 +19,11 @@ struct wlr_linux_drm_syncobj_surface_v1_state { struct wlr_drm_syncobj_timeline *acquire_timeline; uint64_t acquire_point; - struct wlr_drm_syncobj_timeline *release_timeline; - uint64_t release_point; + struct { + struct wlr_drm_syncobj_timeline *release_timeline; + uint64_t release_point; + struct wlr_drm_syncobj_merger *release_merger; + } WLR_PRIVATE; }; struct wlr_linux_drm_syncobj_manager_v1 { @@ -55,4 +58,19 @@ struct wlr_linux_drm_syncobj_surface_v1_state *wlr_linux_drm_syncobj_v1_get_surf bool wlr_linux_drm_syncobj_v1_state_signal_release_with_buffer( struct wlr_linux_drm_syncobj_surface_v1_state *state, struct wlr_buffer *buffer); +/** + * Register a release point for buffer usage. + * + * This function may be called multiple times for the same commit. The client's + * release point will be signalled when all registered points are signalled, and + * a new buffer has been committed. + * + * Because the given release point may not be materialized, a wl_event_loop must + * be supplied to schedule a wait internally, if needed + */ +bool wlr_linux_drm_syncobj_v1_state_add_release_point( + struct wlr_linux_drm_syncobj_surface_v1_state *state, + struct wlr_drm_syncobj_timeline *release_timeline, uint64_t release_point, + struct wl_event_loop *event_loop); + #endif diff --git a/types/wlr_linux_drm_syncobj_v1.c b/types/wlr_linux_drm_syncobj_v1.c index a2e56dcbb..200f4c9e6 100644 --- a/types/wlr_linux_drm_syncobj_v1.c +++ b/types/wlr_linux_drm_syncobj_v1.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include "config.h" #include "linux-drm-syncobj-v1-protocol.h" +#include "render/drm_syncobj_merger.h" #define LINUX_DRM_SYNCOBJ_V1_VERSION 1 @@ -158,20 +160,38 @@ static void surface_synced_finish_state(void *_state) { struct wlr_linux_drm_syncobj_surface_v1_state *state = _state; wlr_drm_syncobj_timeline_unref(state->acquire_timeline); wlr_drm_syncobj_timeline_unref(state->release_timeline); + wlr_drm_syncobj_merger_unref(state->release_merger); } static void surface_synced_move_state(void *_dst, void *_src) { struct wlr_linux_drm_syncobj_surface_v1_state *dst = _dst, *src = _src; - // TODO: immediately signal dst.release_timeline if necessary + if (src->acquire_timeline == NULL) { + // ignore commits that did not attach a buffer + return; + } surface_synced_finish_state(dst); *dst = *src; *src = (struct wlr_linux_drm_syncobj_surface_v1_state){0}; } +static void surface_synced_commit(struct wlr_surface_synced *synced) { + struct wlr_linux_drm_syncobj_surface_v1 *surface = wl_container_of(synced, surface, synced); + + if (surface->current.release_merger != NULL) { + // ignore commits that did not attach a buffer + return; + } + + surface->current.release_merger = wlr_drm_syncobj_merger_create( + surface->current.release_timeline, surface->current.release_point); +} + + static const struct wlr_surface_synced_impl surface_synced_impl = { .state_size = sizeof(struct wlr_linux_drm_syncobj_surface_v1_state), .finish_state = surface_synced_finish_state, .move_state = surface_synced_move_state, + .commit = surface_synced_commit, }; static void manager_handle_destroy(struct wl_client *client, @@ -422,6 +442,11 @@ struct wlr_linux_drm_syncobj_manager_v1 *wlr_linux_drm_syncobj_manager_v1_create struct wl_display *display, uint32_t version, int drm_fd) { assert(version <= LINUX_DRM_SYNCOBJ_V1_VERSION); + if (!HAVE_LINUX_SYNC_FILE) { + wlr_log(WLR_INFO, "Linux sync_file unavailable, disabling linux-drm-syncobj-v1"); + return NULL; + } + if (!check_syncobj_eventfd(drm_fd)) { wlr_log(WLR_INFO, "DRM syncobj eventfd unavailable, disabling linux-drm-syncobj-v1"); return NULL; @@ -467,16 +492,14 @@ wlr_linux_drm_syncobj_v1_get_surface_state(struct wlr_surface *wlr_surface) { } struct release_signaller { - struct wlr_drm_syncobj_timeline *timeline; - uint64_t point; + struct wlr_drm_syncobj_merger *merger; struct wl_listener buffer_release; }; static void release_signaller_handle_buffer_release(struct wl_listener *listener, void *data) { struct release_signaller *signaller = wl_container_of(listener, signaller, buffer_release); - wlr_drm_syncobj_timeline_signal(signaller->timeline, signaller->point); - wlr_drm_syncobj_timeline_unref(signaller->timeline); + wlr_drm_syncobj_merger_unref(signaller->merger); wl_list_remove(&signaller->buffer_release.link); free(signaller); } @@ -496,11 +519,17 @@ bool wlr_linux_drm_syncobj_v1_state_signal_release_with_buffer( return false; } - signaller->timeline = wlr_drm_syncobj_timeline_ref(state->release_timeline); - signaller->point = state->release_point; - + signaller->merger = wlr_drm_syncobj_merger_ref(state->release_merger); signaller->buffer_release.notify = release_signaller_handle_buffer_release; wl_signal_add(&buffer->events.release, &signaller->buffer_release); return true; } + +bool wlr_linux_drm_syncobj_v1_state_add_release_point( + struct wlr_linux_drm_syncobj_surface_v1_state *state, + struct wlr_drm_syncobj_timeline *release_timeline, uint64_t release_point, + struct wl_event_loop *event_loop) { + return wlr_drm_syncobj_merger_add(state->release_merger, + release_timeline, release_point, event_loop); +} From 8daba3246d16220ed25bb9544bdd28668cecb2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 31 Jan 2025 18:49:48 +0000 Subject: [PATCH 16/37] scene: transfer sample syncobj to client timeline (cherry picked from commit b2f6a390a490cdc423fd1fe880bf226708b22951) --- types/scene/surface.c | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/types/scene/surface.c b/types/scene/surface.c index 7aa75c6d4..e6ea1333a 100644 --- a/types/scene/surface.c +++ b/types/scene/surface.c @@ -159,6 +159,13 @@ static void handle_scene_buffer_output_sample( } else { wlr_presentation_surface_textured_on_output(surface->surface, output); } + + struct wlr_linux_drm_syncobj_surface_v1_state *syncobj_surface_state = + wlr_linux_drm_syncobj_v1_get_surface_state(surface->surface); + if (syncobj_surface_state != NULL && event->release_timeline != NULL) { + wlr_linux_drm_syncobj_v1_state_add_release_point(syncobj_surface_state, + event->release_timeline, event->release_point, output->event_loop); + } } static void handle_scene_buffer_frame_done( @@ -323,27 +330,15 @@ static void surface_reconfigure(struct wlr_scene_surface *scene_surface) { struct wlr_linux_drm_syncobj_surface_v1_state *syncobj_surface_state = wlr_linux_drm_syncobj_v1_get_surface_state(surface); - struct wlr_drm_syncobj_timeline *wait_timeline = NULL; - uint64_t wait_point = 0; - if (syncobj_surface_state != NULL) { - wait_timeline = syncobj_surface_state->acquire_timeline; - wait_point = syncobj_surface_state->acquire_point; - } - struct wlr_scene_buffer_set_buffer_options options = { .damage = &surface->buffer_damage, - .wait_timeline = wait_timeline, - .wait_point = wait_point, }; + if (syncobj_surface_state != NULL) { + options.wait_timeline = syncobj_surface_state->acquire_timeline; + options.wait_point = syncobj_surface_state->acquire_point; + } wlr_scene_buffer_set_buffer_with_options(scene_buffer, &surface->buffer->base, &options); - - if (syncobj_surface_state != NULL && - (surface->current.committed & WLR_SURFACE_STATE_BUFFER) && - surface->buffer->source != NULL) { - wlr_linux_drm_syncobj_v1_state_signal_release_with_buffer(syncobj_surface_state, - surface->buffer->source); - } } else { wlr_scene_buffer_set_buffer(scene_buffer, NULL); } From eff5aa52e6cf49fa7c636162448e57d078fbc7f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 31 Jan 2025 18:59:10 +0000 Subject: [PATCH 17/37] backend/drm: properly delay syncobj signalling DRM CRTC signals when scanout begins, but wlr_output_state_set_signal_timeline() is defined to signal buffer release. Delay to the next page flip (cherry picked from commit cd555f9261d5443ba13f484fe4baa6dc3978c859) --- backend/drm/atomic.c | 9 +++++++-- backend/drm/drm.c | 30 ++++++++++++++++++++++++++++++ include/backend/drm/drm.h | 4 ++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/backend/drm/atomic.c b/backend/drm/atomic.c index 41773d4f5..86a025195 100644 --- a/backend/drm/atomic.c +++ b/backend/drm/atomic.c @@ -425,8 +425,13 @@ void drm_atomic_connector_apply_commit(struct wlr_drm_connector_state *state) { } if (state->out_fence_fd >= 0) { // TODO: error handling - wlr_drm_syncobj_timeline_import_sync_file(state->base->signal_timeline, - state->base->signal_point, state->out_fence_fd); + if (crtc->primary->current_release_timeline != NULL) { + wlr_drm_syncobj_timeline_import_sync_file(crtc->primary->current_release_timeline, + crtc->primary->current_release_point, state->out_fence_fd); + wlr_drm_syncobj_timeline_unref(crtc->primary->current_release_timeline); + crtc->primary->current_release_timeline = NULL; + crtc->primary->current_release_point = 0; + } close(state->out_fence_fd); } diff --git a/backend/drm/drm.c b/backend/drm/drm.c index fe9dcc341..76fb408c1 100644 --- a/backend/drm/drm.c +++ b/backend/drm/drm.c @@ -367,7 +367,17 @@ static void drm_plane_finish_surface(struct wlr_drm_plane *plane) { } drm_fb_clear(&plane->queued_fb); + if (plane->queued_release_timeline != NULL) { + wlr_drm_syncobj_timeline_signal(plane->queued_release_timeline, plane->queued_release_point); + wlr_drm_syncobj_timeline_unref(plane->queued_release_timeline); + plane->queued_release_timeline = NULL; + } drm_fb_clear(&plane->current_fb); + if (plane->current_release_timeline != NULL) { + wlr_drm_syncobj_timeline_signal(plane->current_release_timeline, plane->current_release_point); + wlr_drm_syncobj_timeline_unref(plane->current_release_timeline); + plane->current_release_timeline = NULL; + } finish_drm_surface(&plane->mgpu_surf); } @@ -556,6 +566,18 @@ static void drm_connector_apply_commit(const struct wlr_drm_connector_state *sta struct wlr_drm_crtc *crtc = conn->crtc; drm_fb_copy(&crtc->primary->queued_fb, state->primary_fb); + if (crtc->primary->queued_release_timeline != NULL) { + wlr_drm_syncobj_timeline_signal(crtc->primary->queued_release_timeline, crtc->primary->queued_release_point); + wlr_drm_syncobj_timeline_unref(crtc->primary->queued_release_timeline); + } + if (state->base->signal_timeline != NULL) { + crtc->primary->queued_release_timeline = wlr_drm_syncobj_timeline_ref(state->base->signal_timeline); + crtc->primary->queued_release_point = state->base->signal_point; + } else { + crtc->primary->queued_release_timeline = NULL; + crtc->primary->queued_release_point = 0; + } + crtc->primary->viewport = state->primary_viewport; if (crtc->cursor != NULL) { drm_fb_copy(&crtc->cursor->queued_fb, state->cursor_fb); @@ -2018,6 +2040,14 @@ static void handle_page_flip(int fd, unsigned seq, struct wlr_drm_plane *plane = conn->crtc->primary; if (plane->queued_fb) { drm_fb_move(&plane->current_fb, &plane->queued_fb); + if (plane->current_release_timeline != NULL) { + wlr_drm_syncobj_timeline_signal(plane->current_release_timeline, plane->current_release_point); + wlr_drm_syncobj_timeline_unref(plane->current_release_timeline); + } + plane->current_release_timeline = plane->queued_release_timeline; + plane->current_release_point = plane->queued_release_point; + plane->queued_release_timeline = NULL; + plane->queued_release_point = 0; } if (conn->crtc->cursor && conn->crtc->cursor->queued_fb) { drm_fb_move(&conn->crtc->cursor->current_fb, diff --git a/include/backend/drm/drm.h b/include/backend/drm/drm.h index af4231f54..e07c16efe 100644 --- a/include/backend/drm/drm.h +++ b/include/backend/drm/drm.h @@ -29,8 +29,12 @@ struct wlr_drm_plane { /* Buffer submitted to the kernel, will be presented on next vblank */ struct wlr_drm_fb *queued_fb; + struct wlr_drm_syncobj_timeline *queued_release_timeline; + uint64_t queued_release_point; /* Buffer currently displayed on screen */ struct wlr_drm_fb *current_fb; + struct wlr_drm_syncobj_timeline *current_release_timeline; + uint64_t current_release_point; /* Viewport belonging to the last committed fb */ struct wlr_drm_viewport viewport; From df1539d9f02e2778c37d2c9e43552679e8dbda36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 14 Mar 2025 12:16:21 +0000 Subject: [PATCH 18/37] output/drm: don't use OUT_FENCE_PTR The returned fence is not required to be signalled at the earliest possible time. It is not intended to replace the drm flip event, and is expected to be signalled only much later (cherry picked from commit 8d454e1e34988868b45693c8e2178c70bd19ad2f) --- backend/drm/atomic.c | 30 ------------------------------ backend/drm/drm.c | 1 - backend/drm/libliftoff.c | 4 ---- include/backend/drm/drm.h | 2 +- 4 files changed, 1 insertion(+), 36 deletions(-) diff --git a/backend/drm/atomic.c b/backend/drm/atomic.c index 86a025195..faa535d44 100644 --- a/backend/drm/atomic.c +++ b/backend/drm/atomic.c @@ -423,17 +423,6 @@ void drm_atomic_connector_apply_commit(struct wlr_drm_connector_state *state) { if (state->primary_in_fence_fd >= 0) { close(state->primary_in_fence_fd); } - if (state->out_fence_fd >= 0) { - // TODO: error handling - if (crtc->primary->current_release_timeline != NULL) { - wlr_drm_syncobj_timeline_import_sync_file(crtc->primary->current_release_timeline, - crtc->primary->current_release_point, state->out_fence_fd); - wlr_drm_syncobj_timeline_unref(crtc->primary->current_release_timeline); - crtc->primary->current_release_timeline = NULL; - crtc->primary->current_release_point = 0; - } - close(state->out_fence_fd); - } conn->colorspace = state->colorspace; } @@ -451,9 +440,6 @@ void drm_atomic_connector_rollback_commit(struct wlr_drm_connector_state *state) if (state->primary_in_fence_fd >= 0) { close(state->primary_in_fence_fd); } - if (state->out_fence_fd >= 0) { - close(state->out_fence_fd); - } } static void plane_disable(struct atomic *atom, struct wlr_drm_plane *plane) { @@ -505,19 +491,6 @@ static void set_plane_in_fence_fd(struct atomic *atom, atomic_add(atom, plane->id, plane->props.in_fence_fd, sync_file_fd); } -static void set_crtc_out_fence_ptr(struct atomic *atom, struct wlr_drm_crtc *crtc, - int *fd_ptr) { - if (!crtc->props.out_fence_ptr) { - wlr_log(WLR_ERROR, - "CRTC %"PRIu32" is missing the OUT_FENCE_PTR property", - crtc->id); - atom->failed = true; - return; - } - - atomic_add(atom, crtc->id, crtc->props.out_fence_ptr, (uintptr_t)fd_ptr); -} - static void atomic_connector_add(struct atomic *atom, struct wlr_drm_connector_state *state, bool modeset) { struct wlr_drm_connector *conn = state->connector; @@ -562,9 +535,6 @@ static void atomic_connector_add(struct atomic *atom, if (state->primary_in_fence_fd >= 0) { set_plane_in_fence_fd(atom, crtc->primary, state->primary_in_fence_fd); } - if (state->base->committed & WLR_OUTPUT_STATE_SIGNAL_TIMELINE) { - set_crtc_out_fence_ptr(atom, crtc, &state->out_fence_fd); - } if (crtc->cursor) { if (drm_connector_is_cursor_visible(conn)) { struct wlr_fbox cursor_src = { diff --git a/backend/drm/drm.c b/backend/drm/drm.c index 76fb408c1..bd29872fb 100644 --- a/backend/drm/drm.c +++ b/backend/drm/drm.c @@ -668,7 +668,6 @@ static void drm_connector_state_init(struct wlr_drm_connector_state *state, .base = base, .active = output_pending_enabled(&conn->output, base), .primary_in_fence_fd = -1, - .out_fence_fd = -1, }; struct wlr_output_mode *mode = conn->output.current_mode; diff --git a/backend/drm/libliftoff.c b/backend/drm/libliftoff.c index 12761afd4..333beacad 100644 --- a/backend/drm/libliftoff.c +++ b/backend/drm/libliftoff.c @@ -352,10 +352,6 @@ static bool add_connector(drmModeAtomicReq *req, liftoff_layer_set_property(crtc->liftoff_composition_layer, "IN_FENCE_FD", state->primary_in_fence_fd); } - if (state->base->committed & WLR_OUTPUT_STATE_SIGNAL_TIMELINE) { - ok = ok && add_prop(req, crtc->id, crtc->props.out_fence_ptr, - (uintptr_t)&state->out_fence_fd); - } if (state->base->committed & WLR_OUTPUT_STATE_LAYERS) { for (size_t i = 0; i < state->base->layers_len; i++) { diff --git a/include/backend/drm/drm.h b/include/backend/drm/drm.h index e07c16efe..a8c5e077a 100644 --- a/include/backend/drm/drm.h +++ b/include/backend/drm/drm.h @@ -160,7 +160,7 @@ struct wlr_drm_connector_state { uint32_t mode_id; uint32_t gamma_lut; uint32_t fb_damage_clips; - int primary_in_fence_fd, out_fence_fd; + int primary_in_fence_fd; bool vrr_enabled; uint32_t colorspace; uint32_t hdr_output_metadata; From bba38a0d82f65234c320f4a31b9e3e8d1d42366b Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 19 Mar 2026 20:02:25 +0100 Subject: [PATCH 19/37] build: bump version to 0.20.0-rc5 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index d9e215afd..d1c3addf1 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'wlroots', 'c', - version: '0.20.0-rc4', + version: '0.20.0-rc5', license: 'MIT', meson_version: '>=1.3', default_options: [ From ad827405183686e49186042bd1cc0b06bf9c800f Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 13 Mar 2026 17:32:38 +0100 Subject: [PATCH 20/37] color_management_v1: use early continue in surface loop (cherry picked from commit 7287f700ab50874e7bdcc6bd5f4b079dbbe745ef) --- types/wlr_color_management_v1.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/types/wlr_color_management_v1.c b/types/wlr_color_management_v1.c index 0a19e8e78..6f00c0710 100644 --- a/types/wlr_color_management_v1.c +++ b/types/wlr_color_management_v1.c @@ -1002,16 +1002,19 @@ void wlr_color_manager_v1_set_surface_preferred_image_description( struct wlr_color_management_surface_feedback_v1 *surface_feedback; wl_list_for_each(surface_feedback, &manager->surface_feedbacks, link) { - if (surface_feedback->surface == surface) { - surface_feedback->data = *data; - uint32_t version = wl_resource_get_version(surface_feedback->resource); - if (version >= WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_PREFERRED_CHANGED2_SINCE_VERSION) { - wp_color_management_surface_feedback_v1_send_preferred_changed2( - surface_feedback->resource, identity_hi, identity_lo); - } else { - wp_color_management_surface_feedback_v1_send_preferred_changed( - surface_feedback->resource, identity_lo); - } + if (surface_feedback->surface != surface) { + continue; + } + + surface_feedback->data = *data; + + uint32_t version = wl_resource_get_version(surface_feedback->resource); + if (version >= WP_COLOR_MANAGEMENT_SURFACE_FEEDBACK_V1_PREFERRED_CHANGED2_SINCE_VERSION) { + wp_color_management_surface_feedback_v1_send_preferred_changed2( + surface_feedback->resource, identity_hi, identity_lo); + } else { + wp_color_management_surface_feedback_v1_send_preferred_changed( + surface_feedback->resource, identity_lo); } } } From 1a9b1292e2eab59f9b1339af42fee0a4219cfb58 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 13 Mar 2026 17:37:20 +0100 Subject: [PATCH 21/37] color_management_v1: ignore surface update if no-op If the new image description is identical to the old one, skip the event. (cherry picked from commit 4ca40004fd8f35a85f4f342c6b12d3792fbb23ea) --- types/wlr_color_management_v1.c | 38 ++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/types/wlr_color_management_v1.c b/types/wlr_color_management_v1.c index 6f00c0710..aa924e36d 100644 --- a/types/wlr_color_management_v1.c +++ b/types/wlr_color_management_v1.c @@ -74,6 +74,41 @@ static float decode_cie1931_coord(int32_t raw) { return (float)raw / (1000 * 1000); } +static bool cie1931_xy_equal(const struct wlr_color_cie1931_xy *a, + const struct wlr_color_cie1931_xy *b) { + return a->x == b->x && a->y == b->y; +} + +static bool primaries_equal(const struct wlr_color_primaries *a, + const struct wlr_color_primaries *b) { + return cie1931_xy_equal(&a->red, &b->red) && + cie1931_xy_equal(&a->green, &b->green) && + cie1931_xy_equal(&a->blue, &b->blue) && + cie1931_xy_equal(&a->white, &b->white); +} + +static bool img_desc_data_equal(const struct wlr_image_description_v1_data *a, + const struct wlr_image_description_v1_data *b) { + if (a->tf_named != b->tf_named || + a->primaries_named != b->primaries_named || + a->has_mastering_display_primaries != b->has_mastering_display_primaries || + a->has_mastering_luminance != b->has_mastering_luminance || + a->max_cll != b->max_cll || + a->max_fall != b->max_fall) { + return false; + } + if (a->has_mastering_display_primaries && + !primaries_equal(&a->mastering_display_primaries, &b->mastering_display_primaries)) { + return false; + } + if (a->has_mastering_luminance && + (a->mastering_luminance.min != b->mastering_luminance.min || + a->mastering_luminance.max != b->mastering_luminance.max)) { + return false; + } + return true; +} + static const struct wp_image_description_v1_interface image_desc_impl; static struct wlr_image_description_v1 *image_desc_from_resource(struct wl_resource *resource) { @@ -1002,7 +1037,8 @@ void wlr_color_manager_v1_set_surface_preferred_image_description( struct wlr_color_management_surface_feedback_v1 *surface_feedback; wl_list_for_each(surface_feedback, &manager->surface_feedbacks, link) { - if (surface_feedback->surface != surface) { + if (surface_feedback->surface != surface || + img_desc_data_equal(&surface_feedback->data, data)) { continue; } From 2bfbec4af1575e8e9af81429c28878f8899c9976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 20 Mar 2026 12:35:21 +0000 Subject: [PATCH 22/37] linux_drm_syncobj_v1: fix handling of empty first commit As reported in https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4979#note_3385626, bfd6e619fc8642fb3f2cca6f9cb94d257def8780 did not correctly handle clients that don't immediately follow their call to `wp_linux_drm_syncobj_manager_v1.get_surface` with a commit attaching a buffer Fixes: bfd6e619fc8642fb3f2cca6f9cb94d257def8780 (cherry picked from commit fd870f6d27601063bad2219308a80775a9b9a12b) --- include/wlr/types/wlr_linux_drm_syncobj_v1.h | 1 + types/wlr_linux_drm_syncobj_v1.c | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/include/wlr/types/wlr_linux_drm_syncobj_v1.h b/include/wlr/types/wlr_linux_drm_syncobj_v1.h index 70a3f547f..7fd55ec2e 100644 --- a/include/wlr/types/wlr_linux_drm_syncobj_v1.h +++ b/include/wlr/types/wlr_linux_drm_syncobj_v1.h @@ -20,6 +20,7 @@ struct wlr_linux_drm_syncobj_surface_v1_state { uint64_t acquire_point; struct { + bool committed; struct wlr_drm_syncobj_timeline *release_timeline; uint64_t release_point; struct wlr_drm_syncobj_merger *release_merger; diff --git a/types/wlr_linux_drm_syncobj_v1.c b/types/wlr_linux_drm_syncobj_v1.c index 200f4c9e6..53fc2fd43 100644 --- a/types/wlr_linux_drm_syncobj_v1.c +++ b/types/wlr_linux_drm_syncobj_v1.c @@ -171,19 +171,20 @@ static void surface_synced_move_state(void *_dst, void *_src) { } surface_synced_finish_state(dst); *dst = *src; + dst->committed = true; *src = (struct wlr_linux_drm_syncobj_surface_v1_state){0}; } static void surface_synced_commit(struct wlr_surface_synced *synced) { struct wlr_linux_drm_syncobj_surface_v1 *surface = wl_container_of(synced, surface, synced); - if (surface->current.release_merger != NULL) { - // ignore commits that did not attach a buffer + if (!surface->current.committed) { return; } surface->current.release_merger = wlr_drm_syncobj_merger_create( surface->current.release_timeline, surface->current.release_point); + surface->current.committed = false; } @@ -507,7 +508,7 @@ static void release_signaller_handle_buffer_release(struct wl_listener *listener bool wlr_linux_drm_syncobj_v1_state_signal_release_with_buffer( struct wlr_linux_drm_syncobj_surface_v1_state *state, struct wlr_buffer *buffer) { assert(buffer->n_locks > 0); - if (state->release_timeline == NULL) { + if (state->release_merger == NULL) { // This can happen if an existing surface with a buffer has a // syncobj_surface_v1_state created but no new buffer with release // timeline committed. @@ -530,6 +531,12 @@ bool wlr_linux_drm_syncobj_v1_state_add_release_point( struct wlr_linux_drm_syncobj_surface_v1_state *state, struct wlr_drm_syncobj_timeline *release_timeline, uint64_t release_point, struct wl_event_loop *event_loop) { + if (state->release_merger == NULL) { + // This can happen if an existing surface with a buffer has a + // syncobj_surface_v1_state created but no new buffer with release + // timeline committed. + return true; + } return wlr_drm_syncobj_merger_add(state->release_merger, release_timeline, release_point, event_loop); } From a945fd89409812c3a4dc09b8818699fb85712f76 Mon Sep 17 00:00:00 2001 From: Simon Zeni Date: Tue, 24 Mar 2026 09:59:18 -0400 Subject: [PATCH 23/37] render/vulkan: compile against vulkan 1.2 header Uses the EXT version of VK_PIPELINE_COMPILE_REQUIRED in `vulkan_strerror` func since it requires Vulkan 1.3, switch to VK_EXT_global_priority instead of VK_KHR_global_priority which is only promoted to core in Vulkan 1.3 as well. (cherry picked from commit 413664e0b016bb7dde644b4b5477f176d28e5631) --- render/vulkan/util.c | 2 +- render/vulkan/vulkan.c | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/render/vulkan/util.c b/render/vulkan/util.c index 8c31dc797..73a14073a 100644 --- a/render/vulkan/util.c +++ b/render/vulkan/util.c @@ -44,7 +44,7 @@ const char *vulkan_strerror(VkResult err) { ERR_STR(ERROR_INVALID_EXTERNAL_HANDLE); ERR_STR(ERROR_FRAGMENTATION); ERR_STR(ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS); - ERR_STR(PIPELINE_COMPILE_REQUIRED); + ERR_STR(PIPELINE_COMPILE_REQUIRED_EXT); ERR_STR(ERROR_SURFACE_LOST_KHR); ERR_STR(ERROR_NATIVE_WINDOW_IN_USE_KHR); ERR_STR(SUBOPTIMAL_KHR); diff --git a/render/vulkan/vulkan.c b/render/vulkan/vulkan.c index ee7adc011..0c0445d80 100644 --- a/render/vulkan/vulkan.c +++ b/render/vulkan/vulkan.c @@ -564,17 +564,17 @@ struct wlr_vk_device *vulkan_device_create(struct wlr_vk_instance *ini, .pQueuePriorities = &prio, }; - VkDeviceQueueGlobalPriorityCreateInfoKHR global_priority; + VkDeviceQueueGlobalPriorityCreateInfoEXT global_priority; bool has_global_priority = check_extension(avail_ext_props, avail_extc, - VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME); + VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME); if (has_global_priority) { // If global priorities are supported, request a high-priority context - global_priority = (VkDeviceQueueGlobalPriorityCreateInfoKHR){ - .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_KHR, - .globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_KHR, + global_priority = (VkDeviceQueueGlobalPriorityCreateInfoEXT){ + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT, + .globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT, }; qinfo.pNext = &global_priority; - extensions[extensions_len++] = VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME; + extensions[extensions_len++] = VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME; wlr_log(WLR_DEBUG, "Requesting a high-priority device queue"); } else { wlr_log(WLR_DEBUG, "Global priorities are not supported, " From c1d38536c926134698ff2615843e0d4103e84ac4 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 26 Mar 2026 17:38:50 +0100 Subject: [PATCH 24/37] build: bump version to 0.20.0 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index d1c3addf1..9d6b21222 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'wlroots', 'c', - version: '0.20.0-rc5', + version: '0.20.0', license: 'MIT', meson_version: '>=1.3', default_options: [ From 7610326b8d39bca7ae14b7993ebab6b22db2bce8 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Mon, 30 Mar 2026 11:45:54 +0200 Subject: [PATCH 25/37] ext_image_capture_source_v1/scene: fix extents Currently the width/height of the extents is too small if the first node visited has position/dimensions 0,0,100,100 and the second node has position/dimensions -20,-20,10,10. In this case the current code calculates total extents as -20,-20,100,100 but the correct extents are -20,-20,120,120. References: https://codeberg.org/river/river-classic/issues/17 (cherry picked from commit e22084f63967aa72cb56873aeb48b01fb8dc2c9b) --- types/ext_image_capture_source_v1/scene.c | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/types/ext_image_capture_source_v1/scene.c b/types/ext_image_capture_source_v1/scene.c index 99d34e012..7d4b8928a 100644 --- a/types/ext_image_capture_source_v1/scene.c +++ b/types/ext_image_capture_source_v1/scene.c @@ -34,13 +34,14 @@ struct scene_node_source_frame_event { static size_t last_output_num = 0; -static void _get_scene_node_extents(struct wlr_scene_node *node, struct wlr_box *box, int lx, int ly) { +static void _get_scene_node_extents(struct wlr_scene_node *node, int lx, int ly, + int *x_min, int *y_min, int *x_max, int *y_max) { switch (node->type) { case WLR_SCENE_NODE_TREE:; struct wlr_scene_tree *scene_tree = wlr_scene_tree_from_node(node); struct wlr_scene_node *child; wl_list_for_each(child, &scene_tree->children, link) { - _get_scene_node_extents(child, box, lx + child->x, ly + child->y); + _get_scene_node_extents(child, lx + child->x, ly + child->y, x_min, y_min, x_max, y_max); } break; case WLR_SCENE_NODE_RECT: @@ -48,27 +49,30 @@ static void _get_scene_node_extents(struct wlr_scene_node *node, struct wlr_box struct wlr_box node_box = { .x = lx, .y = ly }; scene_node_get_size(node, &node_box.width, &node_box.height); - if (node_box.x < box->x) { - box->x = node_box.x; + if (node_box.x < *x_min) { + *x_min = node_box.x; } - if (node_box.y < box->y) { - box->y = node_box.y; + if (node_box.y < *y_min) { + *y_min = node_box.y; } - if (node_box.x + node_box.width > box->x + box->width) { - box->width = node_box.x + node_box.width - box->x; + if (node_box.x + node_box.width > *x_max) { + *x_max = node_box.x + node_box.width; } - if (node_box.y + node_box.height > box->y + box->height) { - box->height = node_box.y + node_box.height - box->y; + if (node_box.y + node_box.height > *y_max) { + *y_max = node_box.y + node_box.height; } break; } } static void get_scene_node_extents(struct wlr_scene_node *node, struct wlr_box *box) { - *box = (struct wlr_box){ .x = INT_MAX, .y = INT_MAX }; int lx = 0, ly = 0; wlr_scene_node_coords(node, &lx, &ly); - _get_scene_node_extents(node, box, lx, ly); + *box = (struct wlr_box){ .x = INT_MAX, .y = INT_MAX }; + int x_max = INT_MIN, y_max = INT_MIN; + _get_scene_node_extents(node, lx, ly, &box->x, &box->y, &x_max, &y_max); + box->width = x_max - box->x; + box->height = y_max - box->y; } static void source_render(struct scene_node_source *source) { From 13bac746d60ba68cd46eb348c6bf8d2c46b6a9da Mon Sep 17 00:00:00 2001 From: Consolatis <40171-Consolatis@users.noreply.gitlab.freedesktop.org> Date: Fri, 3 Apr 2026 23:03:44 +0200 Subject: [PATCH 26/37] toplevel_capture: allocate new_request argument on the stack This fixes a memory leak. --- .../foreign_toplevel.c | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/types/ext_image_capture_source_v1/foreign_toplevel.c b/types/ext_image_capture_source_v1/foreign_toplevel.c index e5c75e280..b1110d155 100644 --- a/types/ext_image_capture_source_v1/foreign_toplevel.c +++ b/types/ext_image_capture_source_v1/foreign_toplevel.c @@ -34,18 +34,13 @@ static void foreign_toplevel_manager_handle_create_source(struct wl_client *clie return; } - struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request *request = - calloc(1, sizeof(*request)); - if (request == NULL) { - wl_resource_post_no_memory(manager_resource); - return; - } + struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request request = { + .toplevel_handle = toplevel_handle, + .client = client, + .new_id = new_id, + }; - request->toplevel_handle = toplevel_handle; - request->client = client; - request->new_id = new_id; - - wl_signal_emit_mutable(&manager->events.new_request, request); + wl_signal_emit_mutable(&manager->events.new_request, &request); } static void foreign_toplevel_manager_handle_destroy(struct wl_client *client, From d0fbe1ce56a45b5bc76318d9b68e768b7ec43487 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Sun, 12 Apr 2026 00:23:59 +0900 Subject: [PATCH 27/37] wlr-foreign-toplevel-management: add new toplevels to end of list Prior to this patch, when the client binds the manager, the existing toplevel handles were notified in the opposite order of their creation, as wl_list_insert() adds a new handle to the head of the list and wl_list_for_each() iterates from head to tail. (cherry picked from commit 84b45e41578164d1a47bde85ac6a5da3b63f5685) --- types/wlr_foreign_toplevel_management_v1.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/wlr_foreign_toplevel_management_v1.c b/types/wlr_foreign_toplevel_management_v1.c index c98457a3d..739e4c185 100644 --- a/types/wlr_foreign_toplevel_management_v1.c +++ b/types/wlr_foreign_toplevel_management_v1.c @@ -530,7 +530,7 @@ wlr_foreign_toplevel_handle_v1_create( return NULL; } - wl_list_insert(&manager->toplevels, &toplevel->link); + wl_list_insert(manager->toplevels.prev, &toplevel->link); toplevel->manager = manager; wl_list_init(&toplevel->resources); From 61ed450247f24a13155990bc57dbe44a7318ccaa Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Sun, 12 Apr 2026 00:22:32 +0900 Subject: [PATCH 28/37] ext-foreign-toplevel-list: add new toplevels to end of list Same as the prior patch for wlr-foreign-toplevel-management. (cherry picked from commit 8dd1a7761521bba3b79a1009f1a95377bdfb6488) --- types/wlr_ext_foreign_toplevel_list_v1.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/wlr_ext_foreign_toplevel_list_v1.c b/types/wlr_ext_foreign_toplevel_list_v1.c index f20ddc877..cb2f21272 100644 --- a/types/wlr_ext_foreign_toplevel_list_v1.c +++ b/types/wlr_ext_foreign_toplevel_list_v1.c @@ -168,7 +168,7 @@ wlr_ext_foreign_toplevel_handle_v1_create(struct wlr_ext_foreign_toplevel_list_v return NULL; } - wl_list_insert(&list->toplevels, &toplevel->link); + wl_list_insert(list->toplevels.prev, &toplevel->link); toplevel->list = list; if (state->app_id) { toplevel->app_id = strdup(state->app_id); From b9ca8fd6d6f6669ef89e035ab80eba4629453647 Mon Sep 17 00:00:00 2001 From: Johan Malm Date: Sat, 11 Apr 2026 13:40:22 +0100 Subject: [PATCH 29/37] wlr_ext_workspace_v1.c: add new workspaces to end of list ...so that panels/bars like xfce4-panel and waybar show workspaces in the right order. (cherry picked from commit e8c03e9ce9aa54b6c1866c8086cdcd8830399830) --- types/wlr_ext_workspace_v1.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/wlr_ext_workspace_v1.c b/types/wlr_ext_workspace_v1.c index 7aaf80338..95b8a4997 100644 --- a/types/wlr_ext_workspace_v1.c +++ b/types/wlr_ext_workspace_v1.c @@ -790,7 +790,7 @@ struct wlr_ext_workspace_handle_v1 *wlr_ext_workspace_handle_v1_create( wl_array_init(&workspace->coordinates); wl_signal_init(&workspace->events.destroy); - wl_list_insert(&manager->workspaces, &workspace->link); + wl_list_insert(manager->workspaces.prev, &workspace->link); struct wlr_ext_workspace_manager_v1_resource *manager_res; wl_list_for_each(manager_res, &manager->resources, link) { From 7310756297003c8cfc3c280f305b8ed9b2b0797b Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Mon, 6 Apr 2026 15:16:12 +0200 Subject: [PATCH 30/37] wlr_virtual_keyboard_v1: specify size when creating keymap xkb_keymap_new_from_string() assumes that the string is NULL-terminated, but we don't check this so the function might access outside the mmap'ed region. Use the safer xkb_keymap_new_from_buffer() function. Reported-by: Julian Orth Closes: https://gitlab.freedesktop.org/wlroots/wlroots/-/work_items/4072 (cherry picked from commit c393fb6bfa994e18c1f3cecd7cc306b0f6b49191) --- types/wlr_virtual_keyboard_v1.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/wlr_virtual_keyboard_v1.c b/types/wlr_virtual_keyboard_v1.c index 011fbfec7..465b97dd3 100644 --- a/types/wlr_virtual_keyboard_v1.c +++ b/types/wlr_virtual_keyboard_v1.c @@ -52,7 +52,7 @@ static void virtual_keyboard_keymap(struct wl_client *client, if (data == MAP_FAILED) { goto fd_fail; } - struct xkb_keymap *keymap = xkb_keymap_new_from_string(context, data, + struct xkb_keymap *keymap = xkb_keymap_new_from_buffer(context, data, size, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); munmap(data, size); if (!keymap) { From 5d2172b7faf6e6e197e2040d1ebe073dc0145d0c Mon Sep 17 00:00:00 2001 From: Consolatis <40171-Consolatis@users.noreply.gitlab.freedesktop.org> Date: Thu, 2 Apr 2026 00:53:06 +0200 Subject: [PATCH 31/37] render/pixman: fix bilinear filtering to match gles2 renderer Before this patch the pixman renderer would use "constant padding" for bilinear scaling which meant that the edges would either be dark or turn transparent. The effect was most obvious when trying to scale a single row buffer to a height like 100. The center would have the desired color but the edges to both sides would fade into transparency. We now use PIXMAN_REPEAT_PAD which clamps out-of-bound pixels and seems to match the behavior of the gles2 renderer. (cherry picked from commit c66a910753941c905299e62d9e31a4e90c0bbe98) --- render/pixman/pass.c | 1 + 1 file changed, 1 insertion(+) diff --git a/render/pixman/pass.c b/render/pixman/pass.c index d3ee17dca..af738bb42 100644 --- a/render/pixman/pass.c +++ b/render/pixman/pass.c @@ -159,6 +159,7 @@ static void render_pass_add_texture(struct wlr_render_pass *wlr_pass, switch (options->filter_mode) { case WLR_SCALE_FILTER_BILINEAR: + pixman_image_set_repeat(texture->image, PIXMAN_REPEAT_PAD); pixman_image_set_filter(texture->image, PIXMAN_FILTER_BILINEAR, NULL, 0); break; case WLR_SCALE_FILTER_NEAREST: From 1688cfb814ea1b09f626c5d6b0b06ae28ab8b2cc Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Mon, 30 Mar 2026 17:13:57 +0200 Subject: [PATCH 32/37] keyboard: fix modifiers event when no keymap set Actually send the modifiers event when there is no keymap set. Compositors may need lower-level "raw" access to the key/modifiers events from the backend. Currently it is impossible for a compositor to get modifier events from the backend without them being filtered through an xkb_state controlled by wlroots. I need this feature for river in order to fix some keyboard state synchronization bugs. Note that setting a keymap resets the modifiers so I don't think setting wlr_keyboard->modifiers directly here is a concern. (cherry picked from commit 9de0ec308917262f8380e5a0c6d6110464f64314) --- types/wlr_keyboard.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/types/wlr_keyboard.c b/types/wlr_keyboard.c index a24335b3a..adc705db5 100644 --- a/types/wlr_keyboard.c +++ b/types/wlr_keyboard.c @@ -84,6 +84,16 @@ void wlr_keyboard_notify_modifiers(struct wlr_keyboard *keyboard, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { if (keyboard->xkb_state == NULL) { + if (keyboard->modifiers.depressed != mods_depressed || + keyboard->modifiers.latched != mods_latched || + keyboard->modifiers.locked != mods_locked || + keyboard->modifiers.group != group) { + keyboard->modifiers.depressed = mods_depressed; + keyboard->modifiers.latched = mods_latched; + keyboard->modifiers.locked = mods_locked; + keyboard->modifiers.group = group; + wl_signal_emit_mutable(&keyboard->events.modifiers, keyboard); + } return; } xkb_state_update_mask(keyboard->xkb_state, mods_depressed, mods_latched, From a8b883707cb2ad7c81181c8acde8d383ceb1ba71 Mon Sep 17 00:00:00 2001 From: Consolatis <40171-Consolatis@users.noreply.gitlab.freedesktop.org> Date: Sun, 12 Apr 2026 17:18:12 +0200 Subject: [PATCH 33/37] scene/surface: schedule on frame pacing output Otherwise wlr_scene_output_send_frame_done() drops frame callbacks if only parts of a window on a non frame-pacing output is damaged. (cherry picked from commit 700ee83ab805b01bec3ccb072844e67d21d0986d) --- types/scene/surface.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/types/scene/surface.c b/types/scene/surface.c index e6ea1333a..a6958645c 100644 --- a/types/scene/surface.c +++ b/types/scene/surface.c @@ -361,10 +361,9 @@ static void handle_scene_surface_surface_commit( // the surface anyway. int lx, ly; bool enabled = wlr_scene_node_coords(&scene_buffer->node, &lx, &ly); - - if (!wl_list_empty(&surface->surface->current.frame_callback_list) && - surface->buffer->primary_output != NULL && enabled) { - wlr_output_schedule_frame(surface->buffer->primary_output->output); + struct wlr_output *output = get_surface_frame_pacing_output(surface->surface); + if (!wl_list_empty(&surface->surface->current.frame_callback_list) && output && enabled) { + wlr_output_schedule_frame(output); } } From b23a3e8131d086ff10426844df88a98b18bf006f Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Sat, 28 Mar 2026 15:02:41 +0100 Subject: [PATCH 34/37] wlr_compositor: Apply state before updating surface_damage When locking surface state, surface_cache_pending will move the pending surface state to a new, empty `wlr_surface_state`. This new surface state will only contain the fields committed in the pending state, as surface_state_move does not copy anything else. surface_update_damage is called before we move state from pending to current to merge buffer damage and surface damage, and it expects that the pending surface state still contains prior committed details such as scale and transform. This is not the case when we finally commit the cached surface state. Move surface_update_damage after surface_state_move and make it operate purely on the current surface state. (cherry picked from commit fba00c4a04f7fcd4144d51d51915e33421b04950) --- types/wlr_compositor.c | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/types/wlr_compositor.c b/types/wlr_compositor.c index 6b31ab857..75faed190 100644 --- a/types/wlr_compositor.c +++ b/types/wlr_compositor.c @@ -246,40 +246,40 @@ static void surface_finalize_pending(struct wlr_surface *surface) { } static void surface_update_damage(pixman_region32_t *buffer_damage, - struct wlr_surface_state *current, struct wlr_surface_state *pending) { + struct wlr_surface_state *state) { pixman_region32_clear(buffer_damage); // Copy over surface damage + buffer damage pixman_region32_t surface_damage; pixman_region32_init(&surface_damage); - pixman_region32_copy(&surface_damage, &pending->surface_damage); + pixman_region32_copy(&surface_damage, &state->surface_damage); - if (pending->viewport.has_dst) { + if (state->viewport.has_dst) { int src_width, src_height; - surface_state_viewport_src_size(pending, &src_width, &src_height); - float scale_x = (float)pending->viewport.dst_width / src_width; - float scale_y = (float)pending->viewport.dst_height / src_height; + surface_state_viewport_src_size(state, &src_width, &src_height); + float scale_x = (float)state->viewport.dst_width / src_width; + float scale_y = (float)state->viewport.dst_height / src_height; wlr_region_scale_xy(&surface_damage, &surface_damage, 1.0 / scale_x, 1.0 / scale_y); } - if (pending->viewport.has_src) { + if (state->viewport.has_src) { // This is lossy: do a best-effort conversion pixman_region32_translate(&surface_damage, - floor(pending->viewport.src.x), - floor(pending->viewport.src.y)); + floor(state->viewport.src.x), + floor(state->viewport.src.y)); } - wlr_region_scale(&surface_damage, &surface_damage, pending->scale); + wlr_region_scale(&surface_damage, &surface_damage, state->scale); int width, height; - surface_state_transformed_buffer_size(pending, &width, &height); + surface_state_transformed_buffer_size(state, &width, &height); wlr_region_transform(&surface_damage, &surface_damage, - wlr_output_transform_invert(pending->transform), + wlr_output_transform_invert(state->transform), width, height); pixman_region32_union(buffer_damage, - &pending->buffer_damage, &surface_damage); + &state->buffer_damage, &surface_damage); pixman_region32_fini(&surface_damage); } @@ -521,8 +521,6 @@ static void surface_commit_state(struct wlr_surface *surface, surface->unmap_commit = false; } - surface_update_damage(&surface->buffer_damage, &surface->current, next); - surface->previous.scale = surface->current.scale; surface->previous.transform = surface->current.transform; surface->previous.width = surface->current.width; @@ -531,6 +529,7 @@ static void surface_commit_state(struct wlr_surface *surface, surface->previous.buffer_height = surface->current.buffer_height; surface_state_move(&surface->current, next, surface); + surface_update_damage(&surface->buffer_damage, &surface->current); if (invalid_buffer) { surface_apply_damage(surface); From ac378cea419feab2c5755f20d183f638f28f5537 Mon Sep 17 00:00:00 2001 From: xurui Date: Fri, 8 May 2026 15:22:46 +0800 Subject: [PATCH 35/37] linux_drm_syncobj_v1: fix memory leak Signed-off-by: xurui (cherry picked from commit 19df074c1615cf71633c9a022799e78bc72040ce) --- types/wlr_linux_drm_syncobj_v1.c | 1 + 1 file changed, 1 insertion(+) diff --git a/types/wlr_linux_drm_syncobj_v1.c b/types/wlr_linux_drm_syncobj_v1.c index 53fc2fd43..2e09b9e2e 100644 --- a/types/wlr_linux_drm_syncobj_v1.c +++ b/types/wlr_linux_drm_syncobj_v1.c @@ -386,6 +386,7 @@ static void manager_handle_import_timeline(struct wl_client *client, struct wl_resource *timeline_resource = wl_resource_create(client, &wp_linux_drm_syncobj_timeline_v1_interface, version, id); if (timeline_resource == NULL) { + wlr_drm_syncobj_timeline_unref(timeline); wl_resource_post_no_memory(resource); return; } From 9d3ec642c20da8923aaa3555267e1e80f3baad14 Mon Sep 17 00:00:00 2001 From: Ronan Pigott Date: Thu, 26 Mar 2026 20:04:25 -0700 Subject: [PATCH 36/37] scene: only send leave events to outputs with matching scene root In handling scene buffer output updates, wlroots would send a leave event to all entered outputs, even those that the scene root for the scene output update event did not own. Leaving the output list inaccurate. Sending leave events only for the given scene introduces a problem, though: existing logic to de-duplicate leave events stops us from sending a leave event when we leave all the outputs in a scene, and when the surface then becomes visible in another scene, the frame pacing output cannot be selected accurately. This breaks screen capture for off-screen windows in sway. So, let us also mark outputs that would have been left but were spared by the deduplication logic as "suspended" indicating they are ineligible as frame pacing outputs. Fixes: https://github.com/swaywm/sway/issues/9094 (cherry picked from commit e532b4c26c53b60cca6e7bb998f6e12c6e47dfca) --- include/wlr/types/wlr_compositor.h | 2 ++ types/scene/surface.c | 29 ++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/include/wlr/types/wlr_compositor.h b/include/wlr/types/wlr_compositor.h index 9331e8af6..aff42aea6 100644 --- a/include/wlr/types/wlr_compositor.h +++ b/include/wlr/types/wlr_compositor.h @@ -127,6 +127,8 @@ struct wlr_surface_output { struct { struct wl_listener bind; struct wl_listener destroy; + + bool suspended; } WLR_PRIVATE; }; diff --git a/types/scene/surface.c b/types/scene/surface.c index a6958645c..89c54a9ed 100644 --- a/types/scene/surface.c +++ b/types/scene/surface.c @@ -30,8 +30,8 @@ static struct wlr_output *get_surface_frame_pacing_output(struct wlr_surface *su struct wlr_output *frame_pacing_output = NULL; struct wlr_surface_output *surface_output; wl_list_for_each(surface_output, &surface->current_outputs, link) { - if (frame_pacing_output == NULL || - surface_output->output->refresh > frame_pacing_output->refresh) { + if (!surface_output->suspended && (frame_pacing_output == NULL || + surface_output->output->refresh > frame_pacing_output->refresh)) { frame_pacing_output = surface_output->output; } } @@ -97,11 +97,9 @@ static void handle_scene_buffer_outputs_update( struct wlr_scene_outputs_update_event *event = data; struct wlr_scene *scene = scene_node_get_root(&surface->buffer->node); - // If the surface is no longer visible on any output, keep the last sent - // preferred configuration to avoid unnecessary redraws - if (event->size == 0) { - return; - } + // If the surface is no longer visible on any output in the scene, keep the + // last sent preferred configuration to avoid unnecessary redraws + bool suspend = event->size == 0; // To avoid sending redundant leave/enter events when a surface is hidden and then shown // without moving to a different output the following policy is implemented: @@ -121,11 +119,24 @@ static void handle_scene_buffer_outputs_update( break; } } - if (!active) { - wlr_surface_send_leave(surface->surface, entered_output->output); + + struct wlr_scene_output *scene_output; + wl_list_for_each(scene_output, &scene->outputs, link) { + if (scene_output->output == entered_output->output) { + entered_output->suspended = suspend; + if (!suspend && !active) { + wlr_surface_send_leave(surface->surface, entered_output->output); + } + break; + } } } + // No reason to update the preferred configuration if we aren't sending leave/enter events. + if (suspend) { + return; + } + for (size_t i = 0; i < event->size; i++) { // This function internally checks if an enter event was already sent for the output // to avoid sending redundant events. From a7f20066270c042799ae70b71dfa4d561ba85121 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Tue, 19 May 2026 22:56:13 +0200 Subject: [PATCH 37/37] build: bump version to 0.20.1 --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 9d6b21222..b232905d6 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'wlroots', 'c', - version: '0.20.0', + version: '0.20.1', license: 'MIT', meson_version: '>=1.3', default_options: [