Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions module/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ ifneq ($(DKMS_BUILD),)
KERN_DIR := /lib/modules/$(KERNELRELEASE)/build

ccflags-y := -Iinclude/uapi/drm -Iinclude/drm
evdi-y := evdi_platform_drv.o evdi_platform_dev.o evdi_sysfs.o evdi_modeset.o evdi_connector.o evdi_encoder.o evdi_drm_drv.o evdi_fb.o evdi_gem.o evdi_painter.o evdi_params.o evdi_cursor.o evdi_debug.o evdi_i2c.o
evdi-y := evdi_platform_drv.o evdi_platform_dev.o evdi_sysfs.o evdi_modeset.o evdi_connector.o evdi_encoder.o evdi_drm_drv.o evdi_fb.o evdi_gem.o evdi_painter.o evdi_params.o evdi_cursor.o evdi_color.o evdi_debug.o evdi_i2c.o
evdi-$(CONFIG_COMPAT) += evdi_ioc32.o
obj-m := evdi.o
$(eval $(call evdi_conftest))
Expand All @@ -59,7 +59,7 @@ ifneq ($(KERNELRELEASE),)
# inside kbuild
# Note: this can be removed once it is in kernel tree and Kconfig is properly used
ccflags-y := -isystem include/uapi/drm $(CFLAGS)
evdi-y := evdi_platform_drv.o evdi_platform_dev.o evdi_sysfs.o evdi_modeset.o evdi_connector.o evdi_encoder.o evdi_drm_drv.o evdi_fb.o evdi_gem.o evdi_painter.o evdi_params.o evdi_cursor.o evdi_debug.o evdi_i2c.o
evdi-y := evdi_platform_drv.o evdi_platform_dev.o evdi_sysfs.o evdi_modeset.o evdi_connector.o evdi_encoder.o evdi_drm_drv.o evdi_fb.o evdi_gem.o evdi_painter.o evdi_params.o evdi_cursor.o evdi_color.o evdi_debug.o evdi_i2c.o
evdi-$(CONFIG_COMPAT) += evdi_ioc32.o
CONFIG_DRM_EVDI ?= m
obj-$(CONFIG_DRM_EVDI) := evdi.o
Expand Down
181 changes: 181 additions & 0 deletions module/evdi_color.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2026 DisplayLink (UK) Ltd.
*
* This file is subject to the terms and conditions of the GNU General Public
* License v2. See the file COPYING in the main directory of this archive for
* more details.
*/

#include <linux/kernel.h>
#include <linux/minmax.h>
#include <linux/slab.h>
#include <drm/drm_color_mgmt.h>
#include <drm/drm_crtc.h>
#include <drm/drm_fixed.h>

#include "evdi_color.h"

/*
* struct drm_color_ctm stores each entry as S31.32 sign-magnitude (bit 63
* is the sign, bits 0-62 are the unsigned value) rather than the two's
* complement drm_fixed.h (drm_fixp_t) format the drm_fixp_* helpers use.
* Some newer kernels provide drm_sm2fixp() for this, but it isn't present
* across the full 4.15+ range evdi supports, so convert it ourselves.
*/
static s64 evdi_sm2fixp(u64 sm)
{
s64 magnitude = (s64)(sm & ~BIT_ULL(63));

return (sm & BIT_ULL(63)) ? -magnitude : magnitude;
}

static void evdi_color_update_gamma(struct evdi_color_data *data,
struct drm_crtc_state *crtc_state)
{
const struct drm_color_lut *lut;
unsigned int lut_len;
int i, c;

if (!crtc_state->gamma_lut) {
data->has_gamma = false;
return;
}

lut = (const struct drm_color_lut *)crtc_state->gamma_lut->data;
lut_len = crtc_state->gamma_lut->length / sizeof(*lut);

if (lut_len == 0) {
data->has_gamma = false;
return;
}

/*
* Build a direct 256-entry table indexed by raw 8-bit channel value,
* regardless of the length of the LUT userspace actually uploaded
* (expected to be EVDI_GAMMA_LUT_SIZE, since that's what we advertise
* via drm_mode_crtc_set_gamma_size(), but not all clients necessarily
* respect that hint).
*/
for (i = 0; i < EVDI_GAMMA_LUT_SIZE; ++i) {
s64 idx_fp = drm_fixp_div(drm_int2fixp(i * (lut_len - 1)),
drm_int2fixp(EVDI_GAMMA_LUT_SIZE - 1));
int idx_floor = drm_fixp2int(idx_fp);
int idx_ceil = min_t(int, idx_floor + 1, lut_len - 1);
s64 frac = idx_fp - drm_int2fixp(idx_floor);
u16 raw[3] = { lut[idx_floor].red, lut[idx_floor].green, lut[idx_floor].blue };
u16 raw_ceil[3] = { lut[idx_ceil].red, lut[idx_ceil].green, lut[idx_ceil].blue };

for (c = 0; c < 3; ++c) {
s64 floor_fp = drm_int2fixp(raw[c]);
s64 ceil_fp = drm_int2fixp(raw_ceil[c]);
s64 interp = floor_fp + drm_fixp_mul(ceil_fp - floor_fp, frac);
int val16 = clamp(drm_fixp2int(interp), 0, 65535);

/* 16-bit LUT entry -> 8-bit raw pixel channel */
data->gamma[c][i] = val16 >> 8;
}
}

data->has_gamma = true;
}

static void evdi_color_update_ctm(struct evdi_color_data *data,
struct drm_crtc_state *crtc_state)
{
const struct drm_color_ctm *ctm;
int r, c;

if (!crtc_state->ctm) {
data->has_ctm = false;
return;
}

ctm = (const struct drm_color_ctm *)crtc_state->ctm->data;
for (r = 0; r < 3; ++r)
for (c = 0; c < 3; ++c)
data->ctm[r][c] = evdi_sm2fixp(ctm->matrix[r * 3 + c]);

data->has_ctm = true;
}

void evdi_color_transform_init(struct evdi_color_transform *color)
{
mutex_init(&color->lock);
memset(&color->data, 0, sizeof(color->data));
}

void evdi_color_transform_update(struct evdi_color_transform *color,
struct drm_crtc_state *crtc_state)
{
if (!crtc_state->color_mgmt_changed)
return;

mutex_lock(&color->lock);
evdi_color_update_gamma(&color->data, crtc_state);
evdi_color_update_ctm(&color->data, crtc_state);
color->data.active = color->data.has_gamma || color->data.has_ctm;
mutex_unlock(&color->lock);
}

bool evdi_color_transform_snapshot(struct evdi_color_transform *color,
struct evdi_color_data *snapshot)
{
mutex_lock(&color->lock);
*snapshot = color->data;
mutex_unlock(&color->lock);

return snapshot->active;
}

static s64 evdi_color_apply_ctm_channel(const struct evdi_color_data *snapshot,
int row, s64 r, s64 g, s64 b)
{
return drm_fixp_mul(snapshot->ctm[row][0], r) +
drm_fixp_mul(snapshot->ctm[row][1], g) +
drm_fixp_mul(snapshot->ctm[row][2], b);
}

void evdi_color_transform_apply_row(const struct evdi_color_data *snapshot,
void *row, int width_px, bool swap_rb)
{
/*
* XRGB8888/ARGB8888 ("x:R:G:B" MSB-to-LSB, little endian per
* drm_fourcc.h) store B at byte 0 and R at byte 2; XBGR8888/
* ABGR8888 swap that. G (byte 1) and X/alpha (byte 3) never move.
*/
u8 *px = row;
const int r_idx = swap_rb ? 0 : 2;
const int b_idx = swap_rb ? 2 : 0;
int i;

if (!snapshot->active)
return;

for (i = 0; i < width_px; ++i, px += 4) {
int val[3] = { px[r_idx], px[1], px[b_idx] };

if (snapshot->has_ctm) {
s64 r = drm_int2fixp(val[0]);
s64 g = drm_int2fixp(val[1]);
s64 b = drm_int2fixp(val[2]);

val[0] = clamp(drm_fixp2int_round(
evdi_color_apply_ctm_channel(snapshot, 0, r, g, b)), 0, 255);
val[1] = clamp(drm_fixp2int_round(
evdi_color_apply_ctm_channel(snapshot, 1, r, g, b)), 0, 255);
val[2] = clamp(drm_fixp2int_round(
evdi_color_apply_ctm_channel(snapshot, 2, r, g, b)), 0, 255);
}

if (snapshot->has_gamma) {
val[0] = snapshot->gamma[0][val[0]];
val[1] = snapshot->gamma[1][val[1]];
val[2] = snapshot->gamma[2][val[2]];
}

px[r_idx] = val[0];
px[1] = val[1];
px[b_idx] = val[2];
}
}
63 changes: 63 additions & 0 deletions module/evdi_color.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* SPDX-License-Identifier: GPL-2.0-only
* Copyright (c) 2026 DisplayLink (UK) Ltd.
*
* This file is subject to the terms and conditions of the GNU General Public
* License v2. See the file COPYING in the main directory of this archive for
* more details.
*/

#ifndef EVDI_COLOR_H
#define EVDI_COLOR_H

#include <linux/mutex.h>
#include <linux/types.h>

struct drm_crtc_state;

/*
* evdi has no hardware CRTC gamma/CTM block, so Night Light and other
* DRM color management clients need the transform applied in software
* to the raw framebuffer bytes before they leave the driver.
*
* Advertised via drm_mode_crtc_set_gamma_size()/drm_crtc_enable_color_mgmt()
* and consumed here as a direct 8-bit-indexed lookup table, independent of
* whatever length LUT userspace actually uploads (see evdi_color_transform_update()).
*/
#define EVDI_GAMMA_LUT_SIZE 256

struct evdi_color_data {
bool active;
bool has_gamma;
bool has_ctm;
u8 gamma[3][EVDI_GAMMA_LUT_SIZE];
/* row-major 3x3, drm_fixed.h S32.32 two's-complement fixed point */
s64 ctm[3][3];
};

struct evdi_color_transform {
struct mutex lock;
struct evdi_color_data data;
};

void evdi_color_transform_init(struct evdi_color_transform *color);

/* Recomputes the LUT/CTM from crtc_state; cheap no-op unless
* crtc_state->color_mgmt_changed is set (i.e. on actual property writes).
*/
void evdi_color_transform_update(struct evdi_color_transform *color,
struct drm_crtc_state *crtc_state);

/* Copies the current transform out under lock. Returns whether it's a
* no-op identity transform, letting the caller skip apply_row() entirely.
*/
bool evdi_color_transform_snapshot(struct evdi_color_transform *color,
struct evdi_color_data *snapshot);

/* Applies gamma/CTM in place to one packed 32bpp scanline of width_px
* pixels. swap_rb selects XBGR/ABGR (R and B swapped vs XRGB/ARGB) byte
* order; the alpha/padding byte is always left untouched.
*/
void evdi_color_transform_apply_row(const struct evdi_color_data *snapshot,
void *row, int width_px, bool swap_rb);

#endif
1 change: 1 addition & 0 deletions module/evdi_drm_drv.c
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ static int evdi_drm_device_init(struct drm_device *dev)
ret = evdi_cursor_init(&evdi->cursor);
if (ret)
goto err_free;
evdi_color_transform_init(&evdi->color);

evdi_modeset_init(dev);

Expand Down
2 changes: 2 additions & 0 deletions module/evdi_drm_drv.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include <drm/drm_framebuffer.h>
#include <drm/drm_fb_helper.h>

#include "evdi_color.h"
#include "evdi_debug.h"
#include "tests/evdi_test.h"

Expand All @@ -49,6 +50,7 @@ struct evdi_device {
struct drm_connector *conn;
struct evdi_cursor *cursor;
bool cursor_events_enabled;
struct evdi_color_transform color;

uint32_t pixel_area_limit;
uint32_t pixel_per_second_limit;
Expand Down
14 changes: 14 additions & 0 deletions module/evdi_modeset.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
#include <drm/drmP.h>
#endif
#include <drm/drm_atomic.h>
#include <drm/drm_color_mgmt.h>
#include <drm/drm_crtc.h>
#include <drm/drm_crtc_helper.h>
#include <drm/drm_plane_helper.h>
#include <drm/drm_atomic_helper.h>
#include "evdi_drm.h"
#include "evdi_drm_drv.h"
#include "evdi_color.h"
#include "evdi_cursor.h"
#include "evdi_params.h"
#ifdef EVDI_HAVE_DRM_GEM_PLANE_HELPER_PREPARE_FB
Expand Down Expand Up @@ -87,6 +89,8 @@ static void evdi_crtc_atomic_flush(
(crtc_state->mode_changed || evdi_painter_needs_full_modeset(evdi->painter));
bool notify_dpms = crtc_state->active_changed || evdi_painter_needs_full_modeset(evdi->painter);

evdi_color_transform_update(&evdi->color, crtc_state);

if (notify_mode_changed)
evdi_painter_mode_changed_notify(evdi, &crtc_state->adjusted_mode);

Expand Down Expand Up @@ -503,6 +507,16 @@ static int evdi_crtc_init(struct drm_device *dev)
EVDI_DEBUG("drm_crtc_init: %d p%p\n", status, primary_plane);
drm_crtc_helper_add(crtc, &evdi_helper_funcs);

/*
* evdi has no hardware gamma/CTM block to program; the properties
* registered here are applied in software to the raw framebuffer
* bytes in evdi_painter.c's copy_primary_pixels()/_on_xe(), since
* that's the only place the driver has direct access to pixel data
* before it's handed to userspace.
*/
drm_mode_crtc_set_gamma_size(crtc, EVDI_GAMMA_LUT_SIZE);
drm_crtc_enable_color_mgmt(crtc, 0, true, EVDI_GAMMA_LUT_SIZE);

return 0;
}

Expand Down
Loading