From f56e1f6a528150ed5fe0a5330016a3f14b9cf65b Mon Sep 17 00:00:00 2001 From: Furkan Sahin Date: Thu, 7 May 2026 16:15:46 -0400 Subject: [PATCH 1/5] ext_image_capture_source_v1: render hidden sRGB for ICC correct screencopy When a compositor uses color transforms (ICC profiles), the output's post-render buffer is in the display's color space, not sRGB. A screenshot client like grim receives this buffer and saves it as an untagged PNG, which then appears over-saturated in non-color-managed viewers. To avoid this, the output capture source now creates a hidden headless output that re-renders the same scene graph with an identity color transform (sRGB). The hidden output is driven entirely within the capture source and does not affect the real output or cause any visual flicker. --- .../types/wlr_ext_image_capture_source_v1.h | 8 + types/ext_image_capture_source_v1/output.c | 166 +++++++++++++++--- 2 files changed, 149 insertions(+), 25 deletions(-) diff --git a/include/wlr/types/wlr_ext_image_capture_source_v1.h b/include/wlr/types/wlr_ext_image_capture_source_v1.h index 0a93c49f6..31ceade86 100644 --- a/include/wlr/types/wlr_ext_image_capture_source_v1.h +++ b/include/wlr/types/wlr_ext_image_capture_source_v1.h @@ -12,6 +12,7 @@ #include #include #include +#include struct wlr_scene_node; struct wlr_allocator; @@ -79,12 +80,19 @@ struct wlr_ext_image_capture_source_v1_cursor { */ struct wlr_ext_output_image_capture_source_manager_v1 { struct wl_global *global; + struct wl_display *display; + struct wlr_scene *scene; struct { struct wl_listener display_destroy; } WLR_PRIVATE; + struct wlr_backend *headless_backend; }; +void wlr_ext_output_image_capture_source_manager_v1_set_scene( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_scene *scene); + /** * Interface exposing one screen capture source per foreign toplevel. */ diff --git a/types/ext_image_capture_source_v1/output.c b/types/ext_image_capture_source_v1/output.c index 0e3a57823..a358cbb13 100644 --- a/types/ext_image_capture_source_v1/output.c +++ b/types/ext_image_capture_source_v1/output.c @@ -7,7 +7,12 @@ #include #include #include +#include +#include +#include "types/wlr_scene.h" +#include #include "ext-image-capture-source-v1-protocol.h" +#include #define OUTPUT_IMAGE_SOURCE_MANAGER_V1_VERSION 1 @@ -35,6 +40,13 @@ struct wlr_ext_output_image_capture_source_v1 { size_t num_started; bool software_cursors_locked; + + // headless output to avoid icc profile stickiness + struct wlr_ext_output_image_capture_source_manager_v1 *manager; + struct wlr_output *hidden_output; + struct wlr_scene_output *hidden_scene_output; + struct wl_listener hidden_output_frame; + struct wl_listener hidden_output_commit; }; struct wlr_ext_output_image_capture_source_v1_frame_event { @@ -43,30 +55,137 @@ struct wlr_ext_output_image_capture_source_v1_frame_event { struct timespec when; }; +static void handle_hidden_commit(struct wl_listener *listener, void *data) { + struct wlr_ext_output_image_capture_source_v1 *source = + wl_container_of(listener, source, hidden_output_commit); + struct wlr_output_event_commit *event = data; + + if (!(event->state->committed & WLR_OUTPUT_STATE_BUFFER)) + return; + struct wlr_buffer *buffer = event->state->buffer; + pixman_region32_t damage; + pixman_region32_init_rect(&damage, 0, 0, buffer->width, buffer->height); + + struct wlr_ext_output_image_capture_source_v1_frame_event frame_event = { + .base = { .damage = &damage }, + .buffer = buffer, + .when = event->when, + }; + wl_signal_emit_mutable(&source->base.events.frame, &frame_event); + pixman_region32_fini(&damage); +} + +static void handle_hidden_frame(struct wl_listener *listener, void *data) { + struct wlr_ext_output_image_capture_source_v1 *source = + wl_container_of(listener, source, hidden_output_frame); + + + pixman_region32_t damage; + pixman_region32_init_rect(&damage, 0, 0, + source->hidden_output->width, source->hidden_output->height); + pixman_region32_copy(&source->hidden_scene_output->pending_commit_damage, &damage); + pixman_region32_fini(&damage); + + struct wlr_scene_output_state_options opts = { + .color_transform = NULL, // sRGB + }; + struct wlr_output_state state; + wlr_output_state_init(&state); + wlr_output_state_set_enabled(&state, true); + if (!wlr_scene_output_build_state(source->hidden_scene_output, + &state, &opts)) { + wlr_output_state_finish(&state); + return; + } + wlr_output_commit_state(source->hidden_output, &state); + wlr_output_state_finish(&state); +} + +static struct wlr_backend *ensure_headless_backend( + struct wlr_ext_output_image_capture_source_manager_v1 *manager) { + if (manager->headless_backend) + return manager->headless_backend; + + struct wl_event_loop *loop = + wl_display_get_event_loop(manager->display); + manager->headless_backend = wlr_headless_backend_create(loop); + if (manager->headless_backend) + wlr_backend_start(manager->headless_backend); + return manager->headless_backend; +} + +static void source_update_buffer_constraints(struct wlr_ext_output_image_capture_source_v1 *source) { + struct wlr_output *output = source->output; + if (!wlr_output_configure_primary_swapchain(output, NULL, &output->swapchain)) { + return; + } + + wlr_ext_image_capture_source_v1_set_constraints_from_swapchain(&source->base, + output->swapchain, output->renderer); +} + + static void output_source_start(struct wlr_ext_image_capture_source_v1 *base, bool with_cursors) { - struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(base, source, base); + struct wlr_ext_output_image_capture_source_v1 *source = + wl_container_of(base, source, base); source->num_started++; if (source->num_started > 1) { return; } - wlr_output_lock_attach_render(source->output, true); - if (with_cursors) { - wlr_output_lock_software_cursors(source->output, true); + + // Stop the real output from sending its ICC buffer to the capture session + wl_list_remove(&source->output_commit.link); + + struct wlr_output *real = source->output; + struct wlr_backend *headless = ensure_headless_backend(source->manager); + if (!headless) { + return; } - source->software_cursors_locked = with_cursors; + + source->hidden_output = wlr_headless_add_output(headless, + real->width, real->height); + if (!source->hidden_output) { + return; + } + wlr_output_init_render(source->hidden_output, + real->allocator, real->renderer); + source->hidden_scene_output = wlr_scene_output_create( + source->manager->scene, source->hidden_output); + if (!source->hidden_scene_output) { + wlr_output_destroy(source->hidden_output); + source->hidden_output = NULL; + return; + } + source->hidden_output_frame.notify = handle_hidden_frame; + wl_signal_add(&source->hidden_output->events.frame, + &source->hidden_output_frame); + source->hidden_output_commit.notify = handle_hidden_commit; + wl_signal_add(&source->hidden_output->events.commit, + &source->hidden_output_commit); + + source_update_buffer_constraints(source); + wl_signal_emit_mutable(&source->hidden_output->events.frame, source->hidden_output); } static void output_source_stop(struct wlr_ext_image_capture_source_v1 *base) { - struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(base, source, base); - assert(source->num_started > 0); + struct wlr_ext_output_image_capture_source_v1 *source = + wl_container_of(base, source, base); source->num_started--; if (source->num_started > 0) { return; } - wlr_output_lock_attach_render(source->output, false); - if (source->software_cursors_locked) { - wlr_output_lock_software_cursors(source->output, false); + + // Let real output commit event flow once more + wl_signal_add(&source->output->events.commit, &source->output_commit); + + if (source->hidden_output) { + wl_list_remove(&source->hidden_output_frame.link); + wl_list_remove(&source->hidden_output_commit.link); + wlr_scene_output_destroy(source->hidden_scene_output); + wlr_output_destroy(source->hidden_output); + source->hidden_scene_output = NULL; + source->hidden_output = NULL; } } @@ -107,21 +226,6 @@ static const struct wlr_ext_image_capture_source_v1_interface output_source_impl .get_pointer_cursor = output_source_get_pointer_cursor, }; -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; - } - - wlr_ext_image_capture_source_v1_set_constraints_from_swapchain(&source->base, - output->swapchain, output->renderer); -} - static void source_handle_output_commit(struct wl_listener *listener, void *data) { struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(listener, source, output_commit); @@ -200,6 +304,8 @@ static void output_manager_handle_create_source(struct wl_client *client, wlr_addon_init(&source->addon, &output->addons, NULL, &output_addon_impl); source->output = output; + source->manager = wl_resource_get_user_data(manager_resource); + source->output_commit.notify = source_handle_output_commit; wl_signal_add(&output->events.commit, &source->output_commit); @@ -253,6 +359,10 @@ struct wlr_ext_output_image_capture_source_manager_v1 *wlr_ext_output_image_capt return NULL; } + manager->display = display; + manager->headless_backend = NULL; + + manager->global = wl_global_create(display, &ext_output_image_capture_source_manager_v1_interface, version, manager, output_manager_bind); if (manager->global == NULL) { @@ -266,6 +376,12 @@ struct wlr_ext_output_image_capture_source_manager_v1 *wlr_ext_output_image_capt return manager; } +void wlr_ext_output_image_capture_source_manager_v1_set_scene( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_scene *scene) { + manager->scene = scene; +} + static void output_cursor_source_request_frame(struct wlr_ext_image_capture_source_v1 *base, bool schedule_frame) { struct output_cursor_source *cursor_source = wl_container_of(base, cursor_source, base); From 77e59d5991d9792faa767f2e7516e7721d5fa8d5 Mon Sep 17 00:00:00 2001 From: Furkan Sahin Date: Fri, 8 May 2026 09:45:01 -0400 Subject: [PATCH 2/5] Allow copying of any outputs current attempts at copying regions outside the first output end up wrapping into the first output. Fix this by allowing compositors to expose the layout. --- .../wlr/types/wlr_ext_image_capture_source_v1.h | 6 ++++++ types/ext_image_capture_source_v1/output.c | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/wlr/types/wlr_ext_image_capture_source_v1.h b/include/wlr/types/wlr_ext_image_capture_source_v1.h index 31ceade86..647d8ee21 100644 --- a/include/wlr/types/wlr_ext_image_capture_source_v1.h +++ b/include/wlr/types/wlr_ext_image_capture_source_v1.h @@ -17,6 +17,7 @@ struct wlr_scene_node; struct wlr_allocator; struct wlr_renderer; +struct wlr_output_layout; /** * A screen capture source. @@ -82,6 +83,7 @@ struct wlr_ext_output_image_capture_source_manager_v1 { struct wl_global *global; struct wl_display *display; struct wlr_scene *scene; + struct wlr_output_layout *layout; struct { struct wl_listener display_destroy; @@ -89,6 +91,10 @@ struct wlr_ext_output_image_capture_source_manager_v1 { struct wlr_backend *headless_backend; }; +void wlr_ext_output_image_capture_source_manager_v1_set_layout( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_output_layout *layout); + void wlr_ext_output_image_capture_source_manager_v1_set_scene( struct wlr_ext_output_image_capture_source_manager_v1 *manager, struct wlr_scene *scene); diff --git a/types/ext_image_capture_source_v1/output.c b/types/ext_image_capture_source_v1/output.c index a358cbb13..65b99691c 100644 --- a/types/ext_image_capture_source_v1/output.c +++ b/types/ext_image_capture_source_v1/output.c @@ -6,13 +6,13 @@ #include #include #include +#include #include #include #include #include "types/wlr_scene.h" #include #include "ext-image-capture-source-v1-protocol.h" -#include #define OUTPUT_IMAGE_SOURCE_MANAGER_V1_VERSION 1 @@ -124,6 +124,11 @@ static void source_update_buffer_constraints(struct wlr_ext_output_image_capture output->swapchain, output->renderer); } +void wlr_ext_output_image_capture_source_manager_v1_set_layout( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_output_layout *layout) { + manager->layout = layout; +} static void output_source_start(struct wlr_ext_image_capture_source_v1 *base, bool with_cursors) { @@ -152,6 +157,14 @@ static void output_source_start(struct wlr_ext_image_capture_source_v1 *base, real->allocator, real->renderer); source->hidden_scene_output = wlr_scene_output_create( source->manager->scene, source->hidden_output); + if (source->manager->layout) { + struct wlr_box box; + wlr_output_layout_get_box(source->manager->layout, + source->output, &box); + wlr_scene_output_set_position(source->hidden_scene_output, + box.x, box.y); + } + if (!source->hidden_scene_output) { wlr_output_destroy(source->hidden_output); source->hidden_output = NULL; From 8b95c2025098dd57ee7c4837cd1c0a4f5eef5995 Mon Sep 17 00:00:00 2001 From: Furkan Sahin Date: Sat, 9 May 2026 09:53:40 -0400 Subject: [PATCH 3/5] account for scale factor --- types/ext_image_capture_source_v1/output.c | 1 + 1 file changed, 1 insertion(+) diff --git a/types/ext_image_capture_source_v1/output.c b/types/ext_image_capture_source_v1/output.c index 65b99691c..e653bb826 100644 --- a/types/ext_image_capture_source_v1/output.c +++ b/types/ext_image_capture_source_v1/output.c @@ -92,6 +92,7 @@ static void handle_hidden_frame(struct wl_listener *listener, void *data) { struct wlr_output_state state; wlr_output_state_init(&state); wlr_output_state_set_enabled(&state, true); + wlr_output_state_set_scale(&state, source->output->scale); if (!wlr_scene_output_build_state(source->hidden_scene_output, &state, &opts)) { wlr_output_state_finish(&state); From 07ff0a8b1e985a033746ed919beb3708e8e392d0 Mon Sep 17 00:00:00 2001 From: Furkan Sahin Date: Mon, 1 Jun 2026 12:34:04 -0400 Subject: [PATCH 4/5] Revert previous 3 commits As suggested by Felix Poisot, it's better to extend the `scene_node_source` rather than turn output based source into another scene based source. --- .../types/wlr_ext_image_capture_source_v1.h | 14 -- types/ext_image_capture_source_v1/output.c | 180 +++--------------- 2 files changed, 25 insertions(+), 169 deletions(-) diff --git a/include/wlr/types/wlr_ext_image_capture_source_v1.h b/include/wlr/types/wlr_ext_image_capture_source_v1.h index 647d8ee21..0a93c49f6 100644 --- a/include/wlr/types/wlr_ext_image_capture_source_v1.h +++ b/include/wlr/types/wlr_ext_image_capture_source_v1.h @@ -12,12 +12,10 @@ #include #include #include -#include struct wlr_scene_node; struct wlr_allocator; struct wlr_renderer; -struct wlr_output_layout; /** * A screen capture source. @@ -81,24 +79,12 @@ struct wlr_ext_image_capture_source_v1_cursor { */ struct wlr_ext_output_image_capture_source_manager_v1 { struct wl_global *global; - struct wl_display *display; - struct wlr_scene *scene; - struct wlr_output_layout *layout; struct { struct wl_listener display_destroy; } WLR_PRIVATE; - struct wlr_backend *headless_backend; }; -void wlr_ext_output_image_capture_source_manager_v1_set_layout( - struct wlr_ext_output_image_capture_source_manager_v1 *manager, - struct wlr_output_layout *layout); - -void wlr_ext_output_image_capture_source_manager_v1_set_scene( - struct wlr_ext_output_image_capture_source_manager_v1 *manager, - struct wlr_scene *scene); - /** * Interface exposing one screen capture source per foreign toplevel. */ diff --git a/types/ext_image_capture_source_v1/output.c b/types/ext_image_capture_source_v1/output.c index e653bb826..0e3a57823 100644 --- a/types/ext_image_capture_source_v1/output.c +++ b/types/ext_image_capture_source_v1/output.c @@ -6,12 +6,7 @@ #include #include #include -#include #include -#include -#include -#include "types/wlr_scene.h" -#include #include "ext-image-capture-source-v1-protocol.h" #define OUTPUT_IMAGE_SOURCE_MANAGER_V1_VERSION 1 @@ -40,13 +35,6 @@ struct wlr_ext_output_image_capture_source_v1 { size_t num_started; bool software_cursors_locked; - - // headless output to avoid icc profile stickiness - struct wlr_ext_output_image_capture_source_manager_v1 *manager; - struct wlr_output *hidden_output; - struct wlr_scene_output *hidden_scene_output; - struct wl_listener hidden_output_frame; - struct wl_listener hidden_output_commit; }; struct wlr_ext_output_image_capture_source_v1_frame_event { @@ -55,151 +43,30 @@ struct wlr_ext_output_image_capture_source_v1_frame_event { struct timespec when; }; -static void handle_hidden_commit(struct wl_listener *listener, void *data) { - struct wlr_ext_output_image_capture_source_v1 *source = - wl_container_of(listener, source, hidden_output_commit); - struct wlr_output_event_commit *event = data; - - if (!(event->state->committed & WLR_OUTPUT_STATE_BUFFER)) - return; - struct wlr_buffer *buffer = event->state->buffer; - pixman_region32_t damage; - pixman_region32_init_rect(&damage, 0, 0, buffer->width, buffer->height); - - struct wlr_ext_output_image_capture_source_v1_frame_event frame_event = { - .base = { .damage = &damage }, - .buffer = buffer, - .when = event->when, - }; - wl_signal_emit_mutable(&source->base.events.frame, &frame_event); - pixman_region32_fini(&damage); -} - -static void handle_hidden_frame(struct wl_listener *listener, void *data) { - struct wlr_ext_output_image_capture_source_v1 *source = - wl_container_of(listener, source, hidden_output_frame); - - - pixman_region32_t damage; - pixman_region32_init_rect(&damage, 0, 0, - source->hidden_output->width, source->hidden_output->height); - pixman_region32_copy(&source->hidden_scene_output->pending_commit_damage, &damage); - pixman_region32_fini(&damage); - - struct wlr_scene_output_state_options opts = { - .color_transform = NULL, // sRGB - }; - struct wlr_output_state state; - wlr_output_state_init(&state); - wlr_output_state_set_enabled(&state, true); - wlr_output_state_set_scale(&state, source->output->scale); - if (!wlr_scene_output_build_state(source->hidden_scene_output, - &state, &opts)) { - wlr_output_state_finish(&state); - return; - } - wlr_output_commit_state(source->hidden_output, &state); - wlr_output_state_finish(&state); -} - -static struct wlr_backend *ensure_headless_backend( - struct wlr_ext_output_image_capture_source_manager_v1 *manager) { - if (manager->headless_backend) - return manager->headless_backend; - - struct wl_event_loop *loop = - wl_display_get_event_loop(manager->display); - manager->headless_backend = wlr_headless_backend_create(loop); - if (manager->headless_backend) - wlr_backend_start(manager->headless_backend); - return manager->headless_backend; -} - -static void source_update_buffer_constraints(struct wlr_ext_output_image_capture_source_v1 *source) { - struct wlr_output *output = source->output; - if (!wlr_output_configure_primary_swapchain(output, NULL, &output->swapchain)) { - return; - } - - wlr_ext_image_capture_source_v1_set_constraints_from_swapchain(&source->base, - output->swapchain, output->renderer); -} - -void wlr_ext_output_image_capture_source_manager_v1_set_layout( - struct wlr_ext_output_image_capture_source_manager_v1 *manager, - struct wlr_output_layout *layout) { - manager->layout = layout; -} - static void output_source_start(struct wlr_ext_image_capture_source_v1 *base, bool with_cursors) { - struct wlr_ext_output_image_capture_source_v1 *source = - wl_container_of(base, source, base); + struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(base, source, base); source->num_started++; if (source->num_started > 1) { return; } - - // Stop the real output from sending its ICC buffer to the capture session - wl_list_remove(&source->output_commit.link); - - struct wlr_output *real = source->output; - struct wlr_backend *headless = ensure_headless_backend(source->manager); - if (!headless) { - return; + wlr_output_lock_attach_render(source->output, true); + if (with_cursors) { + wlr_output_lock_software_cursors(source->output, true); } - - source->hidden_output = wlr_headless_add_output(headless, - real->width, real->height); - if (!source->hidden_output) { - return; - } - wlr_output_init_render(source->hidden_output, - real->allocator, real->renderer); - source->hidden_scene_output = wlr_scene_output_create( - source->manager->scene, source->hidden_output); - if (source->manager->layout) { - struct wlr_box box; - wlr_output_layout_get_box(source->manager->layout, - source->output, &box); - wlr_scene_output_set_position(source->hidden_scene_output, - box.x, box.y); - } - - if (!source->hidden_scene_output) { - wlr_output_destroy(source->hidden_output); - source->hidden_output = NULL; - return; - } - source->hidden_output_frame.notify = handle_hidden_frame; - wl_signal_add(&source->hidden_output->events.frame, - &source->hidden_output_frame); - source->hidden_output_commit.notify = handle_hidden_commit; - wl_signal_add(&source->hidden_output->events.commit, - &source->hidden_output_commit); - - source_update_buffer_constraints(source); - wl_signal_emit_mutable(&source->hidden_output->events.frame, source->hidden_output); + source->software_cursors_locked = with_cursors; } static void output_source_stop(struct wlr_ext_image_capture_source_v1 *base) { - struct wlr_ext_output_image_capture_source_v1 *source = - wl_container_of(base, source, base); + struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(base, source, base); + assert(source->num_started > 0); source->num_started--; if (source->num_started > 0) { return; } - - // Let real output commit event flow once more - wl_signal_add(&source->output->events.commit, &source->output_commit); - - if (source->hidden_output) { - wl_list_remove(&source->hidden_output_frame.link); - wl_list_remove(&source->hidden_output_commit.link); - wlr_scene_output_destroy(source->hidden_scene_output); - wlr_output_destroy(source->hidden_output); - source->hidden_scene_output = NULL; - source->hidden_output = NULL; + wlr_output_lock_attach_render(source->output, false); + if (source->software_cursors_locked) { + wlr_output_lock_software_cursors(source->output, false); } } @@ -240,6 +107,21 @@ static const struct wlr_ext_image_capture_source_v1_interface output_source_impl .get_pointer_cursor = output_source_get_pointer_cursor, }; +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; + } + + wlr_ext_image_capture_source_v1_set_constraints_from_swapchain(&source->base, + output->swapchain, output->renderer); +} + static void source_handle_output_commit(struct wl_listener *listener, void *data) { struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(listener, source, output_commit); @@ -318,8 +200,6 @@ static void output_manager_handle_create_source(struct wl_client *client, wlr_addon_init(&source->addon, &output->addons, NULL, &output_addon_impl); source->output = output; - source->manager = wl_resource_get_user_data(manager_resource); - source->output_commit.notify = source_handle_output_commit; wl_signal_add(&output->events.commit, &source->output_commit); @@ -373,10 +253,6 @@ struct wlr_ext_output_image_capture_source_manager_v1 *wlr_ext_output_image_capt return NULL; } - manager->display = display; - manager->headless_backend = NULL; - - manager->global = wl_global_create(display, &ext_output_image_capture_source_manager_v1_interface, version, manager, output_manager_bind); if (manager->global == NULL) { @@ -390,12 +266,6 @@ struct wlr_ext_output_image_capture_source_manager_v1 *wlr_ext_output_image_capt return manager; } -void wlr_ext_output_image_capture_source_manager_v1_set_scene( - struct wlr_ext_output_image_capture_source_manager_v1 *manager, - struct wlr_scene *scene) { - manager->scene = scene; -} - static void output_cursor_source_request_frame(struct wlr_ext_image_capture_source_v1 *base, bool schedule_frame) { struct output_cursor_source *cursor_source = wl_container_of(base, cursor_source, base); From 53b399d0548d5d3b21893c6543b02d14b6bb6e0f Mon Sep 17 00:00:00 2001 From: Furkan Sahin Date: Mon, 1 Jun 2026 12:15:27 -0400 Subject: [PATCH 5/5] ext_image_capture_source_v1: add scene-per-output capture source When a compositor uses color transforms (ICC profiles) the output's postrender buffer is in the display's color space, not sRGB. A screenshot client receives this buffer and saves it as an untagged PNG, which appears oversaturated in non-colormanaged viewers. To fix this without altering the semantics of the raw output source (which must deliver the exact hardware scanned buffer, including overlays and direct scanout), add an optional, compositor driven scene-per-output capture source. This source re-renders the entire scene graph for a given output with an identity color transform (sRGB), using a hidden headless output to avoid flicker. The new function `wlr_ext_image_capture_source_v1_create_with_scene_output()` takes a wlr_scene, a reference wlr_output (for dimensions, scale, renderer/allocator), and an optional wlr_output_layout (for correct positioning). The source is created on demand in the existing output capture manager when the compositor has called `wlr_ext_output_image_capture_source_manager_v1_set_scene()` and `wlr_ext_output_image_capture_source_manager_v1_set_layout()`. If the compositor never provides a scene, the manager continues to create the original raw output source, preserving backward compatibility and hardware plane capture for compositors that need it. --- .../types/wlr_ext_image_capture_source_v1.h | 17 + types/ext_image_capture_source_v1/output.c | 115 +++++-- .../scene_output.c | 321 ++++++++++++++++++ types/meson.build | 1 + 4 files changed, 435 insertions(+), 19 deletions(-) create mode 100644 types/ext_image_capture_source_v1/scene_output.c diff --git a/include/wlr/types/wlr_ext_image_capture_source_v1.h b/include/wlr/types/wlr_ext_image_capture_source_v1.h index 0a93c49f6..4244aa09f 100644 --- a/include/wlr/types/wlr_ext_image_capture_source_v1.h +++ b/include/wlr/types/wlr_ext_image_capture_source_v1.h @@ -13,9 +13,12 @@ #include #include +struct wlr_scene; struct wlr_scene_node; struct wlr_allocator; struct wlr_renderer; +struct wlr_output; +struct wlr_output_layout; /** * A screen capture source. @@ -79,6 +82,8 @@ struct wlr_ext_image_capture_source_v1_cursor { */ struct wlr_ext_output_image_capture_source_manager_v1 { struct wl_global *global; + struct wlr_scene *scene; + struct wlr_output_layout *layout; struct { struct wl_listener display_destroy; @@ -122,6 +127,14 @@ struct wlr_ext_image_capture_source_v1 *wlr_ext_image_capture_source_v1_from_res struct wlr_ext_output_image_capture_source_manager_v1 *wlr_ext_output_image_capture_source_manager_v1_create( struct wl_display *display, uint32_t version); +void wlr_ext_output_image_capture_source_manager_v1_set_scene( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_scene *scene); + +void wlr_ext_output_image_capture_source_manager_v1_set_layout( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_output_layout *layout); + struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1 * wlr_ext_foreign_toplevel_image_capture_source_manager_v1_create(struct wl_display *display, uint32_t version); @@ -133,6 +146,10 @@ struct wlr_ext_image_capture_source_v1 *wlr_ext_image_capture_source_v1_create_w struct wlr_scene_node *node, struct wl_event_loop *event_loop, struct wlr_allocator *allocator, struct wlr_renderer *renderer); +struct wlr_ext_image_capture_source_v1 *wlr_ext_image_capture_source_v1_create_with_scene_output( + struct wlr_scene *scene, struct wlr_output *reference_output, + struct wlr_output_layout *layout); + /** * Returns the corresponding wlr_output for a image capture source * managed by wlr_ext_output_image_capture_source_manager_v1 diff --git a/types/ext_image_capture_source_v1/output.c b/types/ext_image_capture_source_v1/output.c index 0e3a57823..e7cc14089 100644 --- a/types/ext_image_capture_source_v1/output.c +++ b/types/ext_image_capture_source_v1/output.c @@ -37,12 +37,41 @@ struct wlr_ext_output_image_capture_source_v1 { bool software_cursors_locked; }; +struct scene_output_source_addon { + struct wlr_addon addon; + struct wlr_ext_image_capture_source_v1 *source; + struct wl_listener source_destroy; +}; + struct wlr_ext_output_image_capture_source_v1_frame_event { struct wlr_ext_image_capture_source_v1_frame_event base; struct wlr_buffer *buffer; struct timespec when; }; +static void scene_output_source_addon_handle_source_destroy(struct wl_listener *listener, + void *data) { + struct scene_output_source_addon *addon = + wl_container_of(listener, addon, source_destroy); + (void)data; + wl_list_remove(&addon->source_destroy.link); + wlr_addon_finish(&addon->addon); + free(addon); +} + +static void scene_output_source_addon_destroy(struct wlr_addon *addon) { + struct scene_output_source_addon *scene_addon = + wl_container_of(addon, scene_addon, addon); + wl_list_remove(&scene_addon->source_destroy.link); + wlr_addon_finish(&scene_addon->addon); + free(scene_addon); +} + +static const struct wlr_addon_interface scene_output_source_addon_impl = { + .name = "wlr_ext_output_image_capture_scene_source_v1", + .destroy = scene_output_source_addon_destroy, +}; + static void output_source_start(struct wlr_ext_image_capture_source_v1 *base, bool with_cursors) { struct wlr_ext_output_image_capture_source_v1 *source = wl_container_of(base, source, base); @@ -185,30 +214,66 @@ static void output_manager_handle_create_source(struct wl_client *client, return; } - struct wlr_ext_output_image_capture_source_v1 *source; - struct wlr_addon *addon = wlr_addon_find(&output->addons, NULL, &output_addon_impl); - if (addon != NULL) { - source = wl_container_of(addon, source, addon); + struct wlr_ext_output_image_capture_source_manager_v1 *manager = + wl_resource_get_user_data(manager_resource); + struct wlr_ext_image_capture_source_v1 *capture_source = NULL; + + if (manager->scene != NULL) { + struct scene_output_source_addon *scene_addon = NULL; + struct wlr_addon *addon = wlr_addon_find(&output->addons, + NULL, &scene_output_source_addon_impl); + if (addon != NULL) { + scene_addon = wl_container_of(addon, scene_addon, addon); + capture_source = scene_addon->source; + } else { + scene_addon = calloc(1, sizeof(*scene_addon)); + if (scene_addon == NULL) { + wl_resource_post_no_memory(manager_resource); + return; + } + + capture_source = wlr_ext_image_capture_source_v1_create_with_scene_output( + manager->scene, output, manager->layout); + if (capture_source == NULL) { + free(scene_addon); + wl_resource_post_no_memory(manager_resource); + return; + } + + scene_addon->source = capture_source; + scene_addon->source_destroy.notify = scene_output_source_addon_handle_source_destroy; + wl_signal_add(&capture_source->events.destroy, &scene_addon->source_destroy); + wlr_addon_init(&scene_addon->addon, &output->addons, NULL, + &scene_output_source_addon_impl); + } } else { - source = calloc(1, sizeof(*source)); - if (source == NULL) { - wl_resource_post_no_memory(manager_resource); - return; + struct wlr_ext_output_image_capture_source_v1 *source; + struct wlr_addon *addon = wlr_addon_find(&output->addons, NULL, &output_addon_impl); + if (addon != NULL) { + source = wl_container_of(addon, source, addon); + } else { + source = calloc(1, sizeof(*source)); + if (source == NULL) { + wl_resource_post_no_memory(manager_resource); + return; + } + + wlr_ext_image_capture_source_v1_init(&source->base, &output_source_impl); + wlr_addon_init(&source->addon, &output->addons, NULL, &output_addon_impl); + source->output = output; + + source->output_commit.notify = source_handle_output_commit; + wl_signal_add(&output->events.commit, &source->output_commit); + + source_update_buffer_constraints(source); + + output_cursor_source_init(&source->cursor, output); } - wlr_ext_image_capture_source_v1_init(&source->base, &output_source_impl); - wlr_addon_init(&source->addon, &output->addons, NULL, &output_addon_impl); - source->output = output; - - source->output_commit.notify = source_handle_output_commit; - wl_signal_add(&output->events.commit, &source->output_commit); - - source_update_buffer_constraints(source); - - output_cursor_source_init(&source->cursor, output); + capture_source = &source->base; } - if (!wlr_ext_image_capture_source_v1_create_resource(&source->base, client, new_id)) { + if (!wlr_ext_image_capture_source_v1_create_resource(capture_source, client, new_id)) { return; } } @@ -236,6 +301,18 @@ static void output_manager_bind(struct wl_client *client, void *data, wl_resource_set_implementation(resource, &output_manager_impl, manager, NULL); } +void wlr_ext_output_image_capture_source_manager_v1_set_scene( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_scene *scene) { + manager->scene = scene; +} + +void wlr_ext_output_image_capture_source_manager_v1_set_layout( + struct wlr_ext_output_image_capture_source_manager_v1 *manager, + struct wlr_output_layout *layout) { + manager->layout = layout; +} + static void output_manager_handle_display_destroy(struct wl_listener *listener, void *data) { struct wlr_ext_output_image_capture_source_manager_v1 *manager = wl_container_of(listener, manager, display_destroy); diff --git a/types/ext_image_capture_source_v1/scene_output.c b/types/ext_image_capture_source_v1/scene_output.c new file mode 100644 index 000000000..f9fa0454f --- /dev/null +++ b/types/ext_image_capture_source_v1/scene_output.c @@ -0,0 +1,321 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct scene_output_source { + struct wlr_ext_image_capture_source_v1 base; + + struct wlr_scene *scene; + struct wlr_output *ref_output; + struct wlr_output_layout *layout; + + struct wlr_backend *headless_backend; + struct wlr_output *headless; + struct wlr_scene_output *scene_output; + + struct wl_listener headless_frame; + struct wl_listener headless_commit; + struct wl_listener scene_output_destroy; + struct wl_listener ref_output_commit; + struct wl_listener ref_output_destroy; + + size_t num_started; +}; + +struct scene_output_source_frame_event { + struct wlr_ext_image_capture_source_v1_frame_event base; + struct wlr_buffer *buffer; + struct timespec when; +}; + +static void scene_output_source_update_constraints(struct scene_output_source *source) { + struct wlr_output *output = source->ref_output; + + if (!output->enabled) { + return; + } + + if (!wlr_output_configure_primary_swapchain(output, NULL, &output->swapchain)) { + return; + } + + wlr_ext_image_capture_source_v1_set_constraints_from_swapchain(&source->base, + output->swapchain, output->renderer); +} + +static void scene_output_source_handle_ref_output_commit(struct wl_listener *listener, + void *data) { + struct scene_output_source *source = wl_container_of(listener, source, ref_output_commit); + struct wlr_output_event_commit *event = data; + + if (event->state->committed & (WLR_OUTPUT_STATE_MODE | + WLR_OUTPUT_STATE_RENDER_FORMAT | WLR_OUTPUT_STATE_ENABLED)) { + scene_output_source_update_constraints(source); + } +} + +static void scene_output_source_handle_scene_output_destroy(struct wl_listener *listener, + void *data) { + struct scene_output_source *source = wl_container_of(listener, source, scene_output_destroy); + (void)data; + source->scene_output = NULL; + wl_list_remove(&source->scene_output_destroy.link); + wl_list_init(&source->scene_output_destroy.link); +} + +static void scene_output_source_handle_headless_frame(struct wl_listener *listener, + void *data) { + struct scene_output_source *source = wl_container_of(listener, source, headless_frame); + (void)data; + + if (source->scene_output == NULL) { + return; + } + + int width = source->ref_output->width; + int height = source->ref_output->height; + if (width <= 0 || height <= 0) { + return; + } + + pixman_region32_t damage; + pixman_region32_init_rect(&damage, 0, 0, width, height); + pixman_region32_copy(&source->scene_output->pending_commit_damage, &damage); + pixman_region32_fini(&damage); + + struct wlr_scene_output_state_options options = { + .color_transform = NULL, + }; + + struct wlr_output_state state; + wlr_output_state_init(&state); + wlr_output_state_set_enabled(&state, true); + wlr_output_state_set_custom_mode(&state, width, height, source->ref_output->refresh); + wlr_output_state_set_scale(&state, source->ref_output->scale); + wlr_output_state_set_transform(&state, source->ref_output->transform); + wlr_output_state_set_render_format(&state, source->ref_output->render_format); + if (!wlr_scene_output_build_state(source->scene_output, &state, &options)) { + wlr_output_state_finish(&state); + return; + } + + wlr_output_commit_state(source->headless, &state); + wlr_output_state_finish(&state); +} + +static void scene_output_source_handle_headless_commit(struct wl_listener *listener, + void *data) { + struct scene_output_source *source = wl_container_of(listener, source, headless_commit); + struct wlr_output_event_commit *event = data; + + if (!(event->state->committed & WLR_OUTPUT_STATE_BUFFER)) { + return; + } + + struct wlr_buffer *buffer = event->state->buffer; + pixman_region32_t full_damage; + + const pixman_region32_t *damage; + if (event->state->committed & WLR_OUTPUT_STATE_DAMAGE) { + damage = &event->state->damage; + } else { + pixman_region32_init_rect(&full_damage, 0, 0, buffer->width, buffer->height); + damage = &full_damage; + } + + struct scene_output_source_frame_event frame_event = { + .base = { + .damage = damage, + }, + .buffer = buffer, + .when = event->when, + }; + wl_signal_emit_mutable(&source->base.events.frame, &frame_event.base); + + if (damage == &full_damage) { + pixman_region32_fini(&full_damage); + } +} + +static void scene_output_source_start(struct wlr_ext_image_capture_source_v1 *base, + bool with_cursors) { + struct scene_output_source *source = wl_container_of(base, source, base); + (void)with_cursors; + + source->num_started++; + if (source->num_started > 1) { + return; + } + + bool created_backend = false; + if (source->headless_backend == NULL) { + source->headless_backend = wlr_headless_backend_create(source->ref_output->event_loop); + if (source->headless_backend == NULL) { + source->num_started--; + return; + } + created_backend = true; + if (!wlr_backend_start(source->headless_backend)) { + wlr_backend_destroy(source->headless_backend); + source->headless_backend = NULL; + source->num_started--; + return; + } + } + + source->headless = wlr_headless_add_output(source->headless_backend, + source->ref_output->width, source->ref_output->height); + if (source->headless == NULL) { + if (created_backend) { + wlr_backend_destroy(source->headless_backend); + source->headless_backend = NULL; + } + source->num_started--; + return; + } + + wlr_output_init_render(source->headless, + source->ref_output->allocator, source->ref_output->renderer); + + source->scene_output = wlr_scene_output_create(source->scene, source->headless); + if (source->scene_output == NULL) { + wlr_output_destroy(source->headless); + source->headless = NULL; + if (created_backend) { + wlr_backend_destroy(source->headless_backend); + source->headless_backend = NULL; + } + source->num_started--; + return; + } + + if (source->layout != NULL) { + struct wlr_box box; + wlr_output_layout_get_box(source->layout, source->ref_output, &box); + wlr_scene_output_set_position(source->scene_output, box.x, box.y); + } + + source->headless_frame.notify = scene_output_source_handle_headless_frame; + wl_signal_add(&source->headless->events.frame, &source->headless_frame); + + source->headless_commit.notify = scene_output_source_handle_headless_commit; + wl_signal_add(&source->headless->events.commit, &source->headless_commit); + + source->scene_output_destroy.notify = scene_output_source_handle_scene_output_destroy; + wl_signal_add(&source->scene_output->events.destroy, &source->scene_output_destroy); + + scene_output_source_update_constraints(source); + wl_signal_emit_mutable(&source->headless->events.frame, source->headless); +} + +static void scene_output_source_stop(struct wlr_ext_image_capture_source_v1 *base) { + struct scene_output_source *source = wl_container_of(base, source, base); + assert(source->num_started > 0); + + source->num_started--; + if (source->num_started > 0) { + return; + } + + if (source->headless != NULL) { + wl_list_remove(&source->headless_frame.link); + wl_list_remove(&source->headless_commit.link); + if (source->scene_output != NULL) { + wl_list_remove(&source->scene_output_destroy.link); + wlr_scene_output_destroy(source->scene_output); + source->scene_output = NULL; + } + wlr_output_destroy(source->headless); + source->headless = NULL; + } + + if (source->headless_backend != NULL) { + wlr_backend_destroy(source->headless_backend); + source->headless_backend = NULL; + } +} + +static void scene_output_source_request_frame(struct wlr_ext_image_capture_source_v1 *base, + bool schedule_frame) { + struct scene_output_source *source = wl_container_of(base, source, base); + if (schedule_frame && source->headless != NULL) { + wlr_output_schedule_frame(source->headless); + } +} + +static void scene_output_source_copy_frame(struct wlr_ext_image_capture_source_v1 *base, + struct wlr_ext_image_copy_capture_frame_v1 *frame, + struct wlr_ext_image_capture_source_v1_frame_event *base_event) { + struct scene_output_source *source = wl_container_of(base, source, base); + struct scene_output_source_frame_event *event = wl_container_of(base_event, event, base); + + if (wlr_ext_image_copy_capture_frame_v1_copy_buffer(frame, + event->buffer, source->ref_output->renderer)) { + wlr_ext_image_copy_capture_frame_v1_ready(frame, + source->ref_output->transform, &event->when); + } +} + +static const struct wlr_ext_image_capture_source_v1_interface scene_output_source_impl = { + .start = scene_output_source_start, + .stop = scene_output_source_stop, + .request_frame = scene_output_source_request_frame, + .copy_frame = scene_output_source_copy_frame, +}; + +static void scene_output_source_destroy(struct scene_output_source *source) { + if (source->num_started > 0) { + scene_output_source_stop(&source->base); + } + + wl_list_remove(&source->ref_output_commit.link); + wl_list_remove(&source->ref_output_destroy.link); + + wlr_ext_image_capture_source_v1_finish(&source->base); + free(source); +} + +static void scene_output_source_handle_ref_output_destroy(struct wl_listener *listener, + void *data) { + struct scene_output_source *source = wl_container_of(listener, source, ref_output_destroy); + (void)data; + scene_output_source_destroy(source); +} + +struct wlr_ext_image_capture_source_v1 *wlr_ext_image_capture_source_v1_create_with_scene_output( + struct wlr_scene *scene, struct wlr_output *reference_output, + struct wlr_output_layout *layout) { + struct scene_output_source *source = calloc(1, sizeof(*source)); + if (source == NULL) { + return NULL; + } + + *source = (struct scene_output_source){ + .scene = scene, + .ref_output = reference_output, + .layout = layout, + }; + + wlr_ext_image_capture_source_v1_init(&source->base, &scene_output_source_impl); + + wl_list_init(&source->headless_frame.link); + wl_list_init(&source->headless_commit.link); + wl_list_init(&source->scene_output_destroy.link); + + source->ref_output_commit.notify = scene_output_source_handle_ref_output_commit; + wl_signal_add(&reference_output->events.commit, &source->ref_output_commit); + + source->ref_output_destroy.notify = scene_output_source_handle_ref_output_destroy; + wl_signal_add(&reference_output->events.destroy, &source->ref_output_destroy); + + scene_output_source_update_constraints(source); + + return &source->base; +} diff --git a/types/meson.build b/types/meson.build index 0513b8fc8..b70fa72ea 100644 --- a/types/meson.build +++ b/types/meson.build @@ -7,6 +7,7 @@ wlr_files += files( 'ext_image_capture_source_v1/output.c', 'ext_image_capture_source_v1/foreign_toplevel.c', 'ext_image_capture_source_v1/scene.c', + 'ext_image_capture_source_v1/scene_output.c', 'output/cursor.c', 'output/output.c', 'output/render.c',