Merge branch 'color-pipeline' into 'master'

Draft: Apply output color transform in KMS

See merge request wlroots/wlroots!5083
This commit is contained in:
Simon Ser 2026-02-01 18:07:12 +00:00
commit 7b02490f00
12 changed files with 290 additions and 20 deletions

View file

@ -6,6 +6,7 @@
#include <wlr/util/box.h> #include <wlr/util/box.h>
#include <wlr/util/log.h> #include <wlr/util/log.h>
#include <xf86drmMode.h> #include <xf86drmMode.h>
#include "backend/drm/color.h"
#include "backend/drm/drm.h" #include "backend/drm/drm.h"
#include "backend/drm/fb.h" #include "backend/drm/fb.h"
#include "backend/drm/iface.h" #include "backend/drm/iface.h"
@ -153,6 +154,31 @@ static bool create_gamma_lut_blob(struct wlr_drm_backend *drm,
return true; return true;
} }
static bool create_ctm_blob(struct wlr_drm_backend *drm,
const float *matrix, uint32_t *blob_id) {
if (matrix == NULL) {
*blob_id = 0;
return true;
}
// Convert to S31.32 sign-magnitude fixed floating-point encoding
struct drm_color_ctm ctm = {0};
for (size_t i = 0; i < 9; i++) {
uint64_t v = fabs(matrix[i]) * ((uint64_t)1 << 32);
if (matrix[i] < 0) {
v |= (uint64_t)1 << 63;
}
ctm.matrix[i] = v;
}
if (drmModeCreatePropertyBlob(drm->fd, &ctm, sizeof(ctm), blob_id) != 0) {
wlr_log_errno(WLR_ERROR, "Unable to create CTM property blob");
return false;
}
return true;
}
bool create_fb_damage_clips_blob(struct wlr_drm_backend *drm, bool create_fb_damage_clips_blob(struct wlr_drm_backend *drm,
int width, int height, const pixman_region32_t *damage, uint32_t *blob_id) { int width, int height, const pixman_region32_t *damage, uint32_t *blob_id) {
pixman_region32_t clipped; pixman_region32_t clipped;
@ -331,28 +357,39 @@ bool drm_atomic_connector_prepare(struct wlr_drm_connector_state *state, bool mo
} }
uint32_t gamma_lut = crtc->gamma_lut; uint32_t gamma_lut = crtc->gamma_lut;
uint32_t ctm = crtc->ctm;
if (state->base->committed & WLR_OUTPUT_STATE_COLOR_TRANSFORM) { if (state->base->committed & WLR_OUTPUT_STATE_COLOR_TRANSFORM) {
size_t dim = 0; size_t lut_dim = 0;
uint16_t *lut = NULL; uint16_t *lut = NULL;
if (state->base->color_transform != NULL) { const float *matrix = NULL;
struct wlr_color_transform_lut_3x1d *tr = if (state->crtc_color_transform != NULL) {
color_transform_lut_3x1d_from_base(state->base->color_transform); lut_dim = state->crtc_color_transform->lut_3x1d->dim;
dim = tr->dim; lut = state->crtc_color_transform->lut_3x1d->lut_3x1d;
lut = tr->lut_3x1d; if (state->crtc_color_transform->has_matrix) {
matrix = state->crtc_color_transform->matrix;
}
} }
// Fallback to legacy gamma interface when gamma properties are not // Fallback to legacy gamma interface when gamma properties are not
// available (can happen on older Intel GPUs that support gamma but not // available (can happen on older Intel GPUs that support gamma but not
// degamma). // degamma).
if (crtc->props.gamma_lut == 0) { if (crtc->props.gamma_lut == 0) {
if (!drm_legacy_crtc_set_gamma(drm, crtc, dim, lut)) { if (!drm_legacy_crtc_set_gamma(drm, crtc, lut_dim, lut)) {
return false; return false;
} }
} else { } else {
if (!create_gamma_lut_blob(drm, dim, lut, &gamma_lut)) { if (!create_gamma_lut_blob(drm, lut_dim, lut, &gamma_lut)) {
return false; return false;
} }
} }
if (matrix != NULL && crtc->props.ctm == 0) {
wlr_log(WLR_DEBUG, "CRTC %"PRIu32" doesn't support CTM", crtc->id);
return false;
}
if (!create_ctm_blob(drm, matrix, &ctm)) {
return false;
}
} }
uint32_t fb_damage_clips = 0; uint32_t fb_damage_clips = 0;
@ -395,6 +432,7 @@ bool drm_atomic_connector_prepare(struct wlr_drm_connector_state *state, bool mo
state->mode_id = mode_id; state->mode_id = mode_id;
state->gamma_lut = gamma_lut; state->gamma_lut = gamma_lut;
state->ctm = ctm;
state->fb_damage_clips = fb_damage_clips; state->fb_damage_clips = fb_damage_clips;
state->primary_in_fence_fd = in_fence_fd; state->primary_in_fence_fd = in_fence_fd;
state->vrr_enabled = vrr_enabled; state->vrr_enabled = vrr_enabled;
@ -414,6 +452,7 @@ void drm_atomic_connector_apply_commit(struct wlr_drm_connector_state *state) {
crtc->own_mode_id = true; crtc->own_mode_id = true;
commit_blob(drm, &crtc->mode_id, state->mode_id); commit_blob(drm, &crtc->mode_id, state->mode_id);
commit_blob(drm, &crtc->gamma_lut, state->gamma_lut); commit_blob(drm, &crtc->gamma_lut, state->gamma_lut);
commit_blob(drm, &crtc->ctm, state->ctm);
commit_blob(drm, &conn->hdr_output_metadata, state->hdr_output_metadata); commit_blob(drm, &conn->hdr_output_metadata, state->hdr_output_metadata);
conn->output.adaptive_sync_status = state->vrr_enabled ? conn->output.adaptive_sync_status = state->vrr_enabled ?
@ -440,6 +479,7 @@ void drm_atomic_connector_rollback_commit(struct wlr_drm_connector_state *state)
rollback_blob(drm, &crtc->mode_id, state->mode_id); rollback_blob(drm, &crtc->mode_id, state->mode_id);
rollback_blob(drm, &crtc->gamma_lut, state->gamma_lut); rollback_blob(drm, &crtc->gamma_lut, state->gamma_lut);
rollback_blob(drm, &crtc->ctm, state->ctm);
rollback_blob(drm, &conn->hdr_output_metadata, state->hdr_output_metadata); rollback_blob(drm, &conn->hdr_output_metadata, state->hdr_output_metadata);
destroy_blob(drm, state->fb_damage_clips); destroy_blob(drm, state->fb_damage_clips);
@ -544,6 +584,9 @@ static void atomic_connector_add(struct atomic *atom,
if (crtc->props.gamma_lut != 0) { if (crtc->props.gamma_lut != 0) {
atomic_add(atom, crtc->id, crtc->props.gamma_lut, state->gamma_lut); atomic_add(atom, crtc->id, crtc->props.gamma_lut, state->gamma_lut);
} }
if (crtc->props.ctm != 0) {
atomic_add(atom, crtc->id, crtc->props.ctm, state->ctm);
}
if (crtc->props.vrr_enabled != 0) { if (crtc->props.vrr_enabled != 0) {
atomic_add(atom, crtc->id, crtc->props.vrr_enabled, state->vrr_enabled); atomic_add(atom, crtc->id, crtc->props.vrr_enabled, state->vrr_enabled);
} }

177
backend/drm/color.c Normal file
View file

@ -0,0 +1,177 @@
#include <stdlib.h>
#include "backend/drm/color.h"
#include "backend/drm/drm.h"
#include "render/color.h"
#include "util/matrix.h"
enum wlr_drm_crtc_color_transform_stage {
WLR_DRM_CRTC_COLOR_TRANSFORM_MATRIX,
WLR_DRM_CRTC_COLOR_TRANSFORM_LUT_3X1D,
};
static struct wlr_color_transform_lut_3x1d *create_identity_3x1dlut(size_t dim) {
if (dim == 0) {
return NULL;
}
uint16_t *lut = malloc(dim * sizeof(lut[0]));
if (lut == NULL) {
return NULL;
}
for (size_t i = 0; i < dim; i++) {
float x = (float)i / (dim - 1);
lut[i] = (uint16_t)roundf(UINT16_MAX * x);
}
struct wlr_color_transform *out = wlr_color_transform_init_lut_3x1d(dim, lut, lut, lut);
free(lut);
return color_transform_lut_3x1d_from_base(out);
}
static bool set_stage(enum wlr_drm_crtc_color_transform_stage *current,
enum wlr_drm_crtc_color_transform_stage next) {
// Enforce that we fill the pipeline in order: first CTM then GAMMA_LUT
if (*current > next) {
return false;
}
*current = next;
return true;
}
static bool drm_crtc_color_transform_init_lut_3x1d(struct wlr_drm_crtc_color_transform *out,
enum wlr_drm_crtc_color_transform_stage *stage, size_t dim) {
if (!set_stage(stage, WLR_DRM_CRTC_COLOR_TRANSFORM_LUT_3X1D)) {
return false;
}
if (out->lut_3x1d != NULL) {
return true;
}
out->lut_3x1d = create_identity_3x1dlut(dim);
return out->lut_3x1d != NULL;
}
static bool drm_crtc_color_transform_convert(struct wlr_drm_crtc_color_transform *out,
struct wlr_color_transform *in, enum wlr_drm_crtc_color_transform_stage *stage,
size_t lut_3x1d_dim) {
switch (in->type) {
case COLOR_TRANSFORM_INVERSE_EOTF:;
struct wlr_color_transform_inverse_eotf *inv_eotf =
wlr_color_transform_inverse_eotf_from_base(in);
if (!drm_crtc_color_transform_init_lut_3x1d(out, stage, lut_3x1d_dim)) {
return false;
}
for (size_t i = 0; i < 3; i++) {
for (size_t j = 0; j < lut_3x1d_dim; j++) {
size_t offset = i * lut_3x1d_dim + j;
float v = (float)out->lut_3x1d->lut_3x1d[offset] / UINT16_MAX;
v = wlr_color_transfer_function_eval_inverse_eotf(inv_eotf->tf, v);
out->lut_3x1d->lut_3x1d[offset] = (uint16_t)roundf(UINT16_MAX * v);
}
}
return true;
case COLOR_TRANSFORM_LUT_3X1D:;
struct wlr_color_transform_lut_3x1d *lut_3x1d =
color_transform_lut_3x1d_from_base(in);
if (lut_3x1d->dim != lut_3x1d_dim) {
return false;
}
if (!drm_crtc_color_transform_init_lut_3x1d(out, stage, lut_3x1d_dim)) {
return false;
}
// TODO: we loose precision when the input color transform is a lone 3×1D LUT
for (size_t i = 0; i < 3 * lut_3x1d->dim; i++) {
out->lut_3x1d->lut_3x1d[i] *= lut_3x1d->lut_3x1d[i];
}
return true;
case COLOR_TRANSFORM_MATRIX:;
struct wlr_color_transform_matrix *matrix = wl_container_of(in, matrix, base);
if (!set_stage(stage, WLR_DRM_CRTC_COLOR_TRANSFORM_MATRIX)) {
return false;
}
wlr_matrix_multiply(out->matrix, matrix->matrix, out->matrix);
out->has_matrix = true;
return true;
case COLOR_TRANSFORM_LCMS2:
return false; // unsupported
case COLOR_TRANSFORM_PIPELINE:;
struct wlr_color_transform_pipeline *pipeline = wl_container_of(in, pipeline, base);
for (size_t i = 0; i < pipeline->len; i++) {
if (!drm_crtc_color_transform_convert(out, pipeline->transforms[i], stage, lut_3x1d_dim)) {
return false;
}
}
return true;
}
abort(); // unreachable
}
static void addon_destroy(struct wlr_addon *addon) {
struct wlr_drm_crtc_color_transform *tr = wl_container_of(addon, tr, addon);
if (tr->lut_3x1d != NULL) {
wlr_color_transform_unref(&tr->lut_3x1d->base);
}
free(tr);
}
static const struct wlr_addon_interface addon_impl = {
.name = "wlr_drm_crtc_color_transform",
.destroy = addon_destroy,
};
static struct wlr_drm_crtc_color_transform *drm_crtc_color_transform_create(
struct wlr_drm_backend *backend, struct wlr_drm_crtc *crtc,
struct wlr_color_transform *base) {
struct wlr_drm_crtc_color_transform *tr = calloc(1, sizeof(*tr));
if (tr == NULL) {
return NULL;
}
tr->base = base;
wlr_addon_init(&tr->addon, &base->addons, crtc, &addon_impl);
wlr_matrix_identity(tr->matrix);
size_t lut_3x1d_dim = drm_crtc_get_gamma_lut_size(backend, crtc);
enum wlr_drm_crtc_color_transform_stage stage = WLR_DRM_CRTC_COLOR_TRANSFORM_MATRIX;
tr->failed = !drm_crtc_color_transform_convert(tr, base, &stage, lut_3x1d_dim);
return tr;
}
struct wlr_drm_crtc_color_transform *drm_crtc_color_transform_import(
struct wlr_drm_backend *backend, struct wlr_drm_crtc *crtc,
struct wlr_color_transform *base) {
struct wlr_drm_crtc_color_transform *tr;
struct wlr_addon *addon = wlr_addon_find(&base->addons, crtc, &addon_impl);
if (addon != NULL) {
tr = wl_container_of(addon, tr, addon);
} else {
tr = drm_crtc_color_transform_create(backend, crtc, base);
if (tr == NULL) {
return NULL;
}
}
if (tr->failed) {
// We failed to convert the color transform to a 3×1D LUT. Keep the
// addon attached so that we remember that this color transform cannot
// be imported next time a commit contains it.
return NULL;
}
wlr_color_transform_ref(tr->base);
return tr;
}
void drm_crtc_color_transform_unref(struct wlr_drm_crtc_color_transform *tr) {
if (tr == NULL) {
return;
}
wlr_color_transform_unref(tr->base);
}

View file

@ -20,6 +20,7 @@
#include <wlr/util/transform.h> #include <wlr/util/transform.h>
#include <xf86drm.h> #include <xf86drm.h>
#include <xf86drmMode.h> #include <xf86drmMode.h>
#include "backend/drm/color.h"
#include "backend/drm/drm.h" #include "backend/drm/drm.h"
#include "backend/drm/fb.h" #include "backend/drm/fb.h"
#include "backend/drm/iface.h" #include "backend/drm/iface.h"
@ -390,6 +391,9 @@ void finish_drm_resources(struct wlr_drm_backend *drm) {
if (crtc->gamma_lut) { if (crtc->gamma_lut) {
drmModeDestroyPropertyBlob(drm->fd, crtc->gamma_lut); drmModeDestroyPropertyBlob(drm->fd, crtc->gamma_lut);
} }
if (crtc->ctm) {
drmModeDestroyPropertyBlob(drm->fd, crtc->ctm);
}
} }
free(drm->crtcs); free(drm->crtcs);
@ -707,6 +711,7 @@ static void drm_connector_state_finish(struct wlr_drm_connector_state *state) {
drm_fb_clear(&state->primary_fb); drm_fb_clear(&state->primary_fb);
drm_fb_clear(&state->cursor_fb); drm_fb_clear(&state->cursor_fb);
wlr_drm_syncobj_timeline_unref(state->wait_timeline); wlr_drm_syncobj_timeline_unref(state->wait_timeline);
drm_crtc_color_transform_unref(state->crtc_color_transform);
} }
static bool drm_connector_state_update_primary_fb(struct wlr_drm_connector *conn, static bool drm_connector_state_update_primary_fb(struct wlr_drm_connector *conn,
@ -858,11 +863,15 @@ static bool drm_connector_prepare(struct wlr_drm_connector_state *conn_state, bo
} }
} }
if ((state->committed & WLR_OUTPUT_STATE_COLOR_TRANSFORM) && state->color_transform != NULL && if ((state->committed & WLR_OUTPUT_STATE_COLOR_TRANSFORM) && state->color_transform != NULL) {
state->color_transform->type != COLOR_TRANSFORM_LUT_3X1D) { assert(conn_state->crtc_color_transform == NULL);
wlr_drm_conn_log(conn, WLR_DEBUG, conn_state->crtc_color_transform = drm_crtc_color_transform_import(conn->backend,
"Only 3x1D LUT color transforms are supported"); conn->crtc, state->color_transform);
return false; if (conn_state->crtc_color_transform == NULL) {
wlr_drm_conn_log(conn, WLR_DEBUG,
"Failed to import color transform");
return false;
}
} }
if ((state->committed & WLR_OUTPUT_STATE_IMAGE_DESCRIPTION) && if ((state->committed & WLR_OUTPUT_STATE_IMAGE_DESCRIPTION) &&

View file

@ -3,6 +3,7 @@
#include <wlr/util/log.h> #include <wlr/util/log.h>
#include <xf86drm.h> #include <xf86drm.h>
#include <xf86drmMode.h> #include <xf86drmMode.h>
#include "backend/drm/color.h"
#include "backend/drm/drm.h" #include "backend/drm/drm.h"
#include "backend/drm/fb.h" #include "backend/drm/fb.h"
#include "backend/drm/iface.h" #include "backend/drm/iface.h"
@ -128,11 +129,9 @@ static bool legacy_crtc_commit(const struct wlr_drm_connector_state *state,
if (state->base->committed & WLR_OUTPUT_STATE_COLOR_TRANSFORM) { if (state->base->committed & WLR_OUTPUT_STATE_COLOR_TRANSFORM) {
size_t dim = 0; size_t dim = 0;
uint16_t *lut = NULL; uint16_t *lut = NULL;
if (state->base->color_transform != NULL) { if (state->crtc_color_transform != NULL) {
struct wlr_color_transform_lut_3x1d *tr = dim = state->crtc_color_transform->lut_3x1d->dim;
color_transform_lut_3x1d_from_base(state->base->color_transform); lut = state->crtc_color_transform->lut_3x1d->lut_3x1d;
dim = tr->dim;
lut = tr->lut_3x1d;
} }
if (!drm_legacy_crtc_set_gamma(drm, crtc, dim, lut)) { if (!drm_legacy_crtc_set_gamma(drm, crtc, dim, lut)) {

View file

@ -328,6 +328,9 @@ static bool add_connector(drmModeAtomicReq *req,
if (crtc->props.gamma_lut != 0) { if (crtc->props.gamma_lut != 0) {
ok = ok && add_prop(req, crtc->id, crtc->props.gamma_lut, state->gamma_lut); ok = ok && add_prop(req, crtc->id, crtc->props.gamma_lut, state->gamma_lut);
} }
if (crtc->props.ctm != 0) {
ok = ok && add_prop(req, crtc->id, crtc->props.ctm, state->ctm);
}
if (crtc->props.vrr_enabled != 0) { if (crtc->props.vrr_enabled != 0) {
ok = ok && add_prop(req, crtc->id, crtc->props.vrr_enabled, state->vrr_enabled); ok = ok && add_prop(req, crtc->id, crtc->props.vrr_enabled, state->vrr_enabled);
} }

View file

@ -38,6 +38,7 @@ wlr_files += pnpids_c
wlr_files += files( wlr_files += files(
'atomic.c', 'atomic.c',
'backend.c', 'backend.c',
'color.c',
'drm.c', 'drm.c',
'fb.c', 'fb.c',
'legacy.c', 'legacy.c',

View file

@ -40,6 +40,7 @@ static const struct prop_info connector_info[] = {
static const struct prop_info crtc_info[] = { static const struct prop_info crtc_info[] = {
#define INDEX(name) (offsetof(struct wlr_drm_crtc_props, name) / sizeof(uint32_t)) #define INDEX(name) (offsetof(struct wlr_drm_crtc_props, name) / sizeof(uint32_t))
{ "ACTIVE", INDEX(active) }, { "ACTIVE", INDEX(active) },
{ "CTM", INDEX(ctm) },
{ "GAMMA_LUT", INDEX(gamma_lut) }, { "GAMMA_LUT", INDEX(gamma_lut) },
{ "GAMMA_LUT_SIZE", INDEX(gamma_lut_size) }, { "GAMMA_LUT_SIZE", INDEX(gamma_lut_size) },
{ "MODE_ID", INDEX(mode_id) }, { "MODE_ID", INDEX(mode_id) },

View file

@ -0,0 +1,25 @@
#ifndef BACKEND_DRN_COLOR_H
#define BACKEND_DRN_COLOR_H
#include <wlr/util/addon.h>
struct wlr_drm_backend;
struct wlr_drm_crtc;
struct wlr_color_transform;
struct wlr_drm_crtc_color_transform {
struct wlr_color_transform *base;
struct wlr_addon addon; // wlr_color_transform.addons
bool failed;
struct wlr_color_transform_lut_3x1d *lut_3x1d;
float matrix[9];
bool has_matrix;
};
struct wlr_drm_crtc_color_transform *drm_crtc_color_transform_import(
struct wlr_drm_backend *backend, struct wlr_drm_crtc *crtc,
struct wlr_color_transform *base);
void drm_crtc_color_transform_unref(struct wlr_drm_crtc_color_transform *tr);
#endif

View file

@ -15,6 +15,8 @@
#include "backend/drm/properties.h" #include "backend/drm/properties.h"
#include "backend/drm/renderer.h" #include "backend/drm/renderer.h"
struct wlr_drm_crtc_color_transform;
struct wlr_drm_viewport { struct wlr_drm_viewport {
struct wlr_fbox src_box; struct wlr_fbox src_box;
struct wlr_box dst_box; struct wlr_box dst_box;
@ -74,6 +76,7 @@ struct wlr_drm_crtc {
bool own_mode_id; bool own_mode_id;
uint32_t mode_id; uint32_t mode_id;
uint32_t gamma_lut; uint32_t gamma_lut;
uint32_t ctm;
// Legacy only // Legacy only
int legacy_gamma_size; int legacy_gamma_size;
@ -152,9 +155,12 @@ struct wlr_drm_connector_state {
struct wlr_drm_syncobj_timeline *wait_timeline; struct wlr_drm_syncobj_timeline *wait_timeline;
uint64_t wait_point; uint64_t wait_point;
struct wlr_drm_crtc_color_transform *crtc_color_transform;
// used by atomic // used by atomic
uint32_t mode_id; uint32_t mode_id;
uint32_t gamma_lut; uint32_t gamma_lut;
uint32_t ctm;
uint32_t fb_damage_clips; uint32_t fb_damage_clips;
int primary_in_fence_fd, out_fence_fd; int primary_in_fence_fd, out_fence_fd;
bool vrr_enabled; bool vrr_enabled;

View file

@ -35,6 +35,7 @@ struct wlr_drm_crtc_props {
uint32_t vrr_enabled; uint32_t vrr_enabled;
uint32_t gamma_lut; uint32_t gamma_lut;
uint32_t gamma_lut_size; uint32_t gamma_lut_size;
uint32_t ctm;
// atomic-modesetting only // atomic-modesetting only

View file

@ -108,4 +108,9 @@ void wlr_color_primaries_to_xyz(const struct wlr_color_primaries *primaries, flo
void wlr_color_transfer_function_get_default_luminance(enum wlr_color_transfer_function tf, void wlr_color_transfer_function_get_default_luminance(enum wlr_color_transfer_function tf,
struct wlr_color_luminances *lum); struct wlr_color_luminances *lum);
/**
* Apply a color transfer function's EOTF¹ operation.
*/
float wlr_color_transfer_function_eval_inverse_eotf(enum wlr_color_transfer_function tf, float x);
#endif #endif

View file

@ -188,7 +188,7 @@ static float bt1886_eval_inverse_eotf(float x) {
return powf(x / a, 1.0 / 2.4) - b; return powf(x / a, 1.0 / 2.4) - b;
} }
static float transfer_function_eval_inverse_eotf( float wlr_color_transfer_function_eval_inverse_eotf(
enum wlr_color_transfer_function tf, float x) { enum wlr_color_transfer_function tf, float x) {
switch (tf) { switch (tf) {
case WLR_COLOR_TRANSFER_FUNCTION_SRGB: case WLR_COLOR_TRANSFER_FUNCTION_SRGB:
@ -209,7 +209,7 @@ static void color_transform_inverse_eotf_eval(
struct wlr_color_transform_inverse_eotf *tr, struct wlr_color_transform_inverse_eotf *tr,
float out[static 3], const float in[static 3]) { float out[static 3], const float in[static 3]) {
for (size_t i = 0; i < 3; i++) { for (size_t i = 0; i < 3; i++) {
out[i] = transfer_function_eval_inverse_eotf(tr->tf, in[i]); out[i] = wlr_color_transfer_function_eval_inverse_eotf(tr->tf, in[i]);
} }
} }