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.
This commit is contained in:
Furkan Sahin 2026-05-07 16:15:46 -04:00
parent 57441ded02
commit 5c1ba2cf04
2 changed files with 149 additions and 25 deletions

View file

@ -12,6 +12,7 @@
#include <pixman.h>
#include <wayland-server-core.h>
#include <wlr/render/drm_format_set.h>
#include <wlr/backend.h>
struct wlr_scene_node;
struct wlr_allocator;
@ -77,12 +78,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.
*/

View file

@ -7,7 +7,12 @@
#include <wlr/types/wlr_ext_image_copy_capture_v1.h>
#include <wlr/types/wlr_output.h>
#include <wlr/util/addon.h>
#include <wlr/backend/headless.h>
#include <wlr/types/wlr_scene.h>
#include "types/wlr_scene.h"
#include <wayland-server-core.h>
#include "ext-image-capture-source-v1-protocol.h"
#include <wlr/util/log.h>
#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);