diff --git a/drivers/gpu/drm/nouveau/dispnv50/atom.h b/drivers/gpu/drm/nouveau/dispnv50/atom.h
index b5fae5ab3fa8..894d1fec6f0a 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/atom.h
+++ b/drivers/gpu/drm/nouveau/dispnv50/atom.h
@@ -184,6 +184,11 @@ struct nv50_wndw_atom {
 		} i;
 	} xlut;
 
+	struct {
+		u32 matrix[12];
+		bool valid;
+	} ctm;
+
 	struct {
 		u8  mode:2;
 		u8  interval:4;
@@ -221,6 +226,7 @@ struct nv50_wndw_atom {
 			bool ntfy:1;
 			bool sema:1;
 			bool xlut:1;
+			bool ctm:1;
 			bool image:1;
 			bool scale:1;
 			bool point:1;
diff --git a/drivers/gpu/drm/nouveau/dispnv50/base907c.c b/drivers/gpu/drm/nouveau/dispnv50/base907c.c
index 049ce6da321c..ceadc2e3efe9 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/base907c.c
+++ b/drivers/gpu/drm/nouveau/dispnv50/base907c.c
@@ -83,6 +83,68 @@ base907c_ilut(struct nv50_wndw *wndw, struct nv50_wndw_atom *asyw)
 	asyw->xlut.i.load = head907d_olut_load;
 }
 
+static inline u32
+ctm_drm_to_base(u64 in)
+{
+	/* base takes a 19-bit 2's complement value in S3.16 format */
+	bool sign = in & BIT_ULL(63);
+	u32 integer = (in >> 32) & 0x7fffffff;
+	u32 fraction = in & 0xffffffff;
+
+	if (integer >= 4) {
+		return (1 << 18) - (sign ? 0 : 1);
+	} else {
+		u32 ret = (integer << 16) | (fraction >> 16);
+		if (sign)
+			ret = -ret;
+		return ret & GENMASK(18, 0);
+	}
+}
+
+static void
+base907c_ctm(struct nv50_wndw *wndw, struct nv50_wndw_atom *asyw,
+	     const struct drm_color_ctm *ctm)
+{
+	int i, j;
+
+	for (j = 0; j < 3; j++) {
+		for (i = 0; i < 4; i++) {
+			u32 *val = &asyw->ctm.matrix[j * 4 + i];
+			/* CTM does not support constant offset, while
+			 * HW CSC does. Skip it. */
+			if (i == 3) {
+				*val = 0;
+			} else {
+				*val = ctm_drm_to_base(ctm->matrix[j * 3 + i]);
+			}
+		}
+	}
+}
+
+static void
+base907c_ctm_clr(struct nv50_wndw *wndw)
+{
+	u32 *push;
+	if ((push = evo_wait(&wndw->wndw, 2))) {
+		evo_mthd(push, 0x0140, 1);
+		evo_data(push, 0x00000000);
+		evo_kick(push, &wndw->wndw);
+	}
+}
+
+static void
+base907c_ctm_set(struct nv50_wndw *wndw, struct nv50_wndw_atom *asyw)
+{
+	u32 *push, i;
+	if ((push = evo_wait(&wndw->wndw, 13))) {
+		evo_mthd(push, 0x0140, 12);
+		evo_data(push, asyw->ctm.matrix[0] | 0x80000000);
+		for (i = 1; i < 12; i++)
+			evo_data(push, asyw->ctm.matrix[i]);
+		evo_kick(push, &wndw->wndw);
+	}
+}
+
 const struct nv50_wndw_func
 base907c = {
 	.acquire = base507c_acquire,
@@ -94,6 +156,9 @@ base907c = {
 	.ntfy_clr = base507c_ntfy_clr,
 	.ntfy_wait_begun = base507c_ntfy_wait_begun,
 	.ilut = base907c_ilut,
+	.ctm = base907c_ctm,
+	.ctm_set = base907c_ctm_set,
+	.ctm_clr = base907c_ctm_clr,
 	.olut_core = true,
 	.xlut_set = base907c_xlut_set,
 	.xlut_clr = base907c_xlut_clr,
diff --git a/drivers/gpu/drm/nouveau/dispnv50/wndw.c b/drivers/gpu/drm/nouveau/dispnv50/wndw.c
index 3bee1f063dfb..2ae3ca9a5968 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/wndw.c
+++ b/drivers/gpu/drm/nouveau/dispnv50/wndw.c
@@ -118,6 +118,7 @@ nv50_wndw_flush_clr(struct nv50_wndw *wndw, u32 *interlock, bool flush,
 	if (clr.sema ) wndw->func-> sema_clr(wndw);
 	if (clr.ntfy ) wndw->func-> ntfy_clr(wndw);
 	if (clr.xlut ) wndw->func-> xlut_clr(wndw);
+	if (clr.ctm  ) wndw->func->  ctm_clr(wndw);
 	if (clr.image) wndw->func->image_clr(wndw);
 
 	interlock[wndw->interlock.type] |= wndw->interlock.data;
@@ -145,6 +146,7 @@ nv50_wndw_flush_set(struct nv50_wndw *wndw, u32 *interlock,
 		wndw->func->xlut_set(wndw, asyw);
 	}
 
+	if (asyw->set.ctm  ) wndw->func->ctm_set  (wndw, asyw);
 	if (asyw->set.scale) wndw->func->scale_set(wndw, asyw);
 	if (asyw->set.point) {
 		if (asyw->set.point = false, asyw->set.mask)
@@ -341,6 +343,15 @@ nv50_wndw_atomic_check_lut(struct nv50_wndw *wndw,
 	    (!armw->visible || (armw->xlut.handle && !asyw->xlut.handle)))
 		asyw->set.xlut = true;
 
+	if (wndw->func->ctm && asyh->state.ctm) {
+		const struct drm_color_ctm *ctm = asyh->state.ctm->data;
+		wndw->func->ctm(wndw, asyw, ctm);
+		asyw->ctm.valid = true;
+		asyw->set.ctm = true;
+	} else {
+		asyw->clr.ctm = armw->ctm.valid;
+	}
+
 	/* Can't do an immediate flip while changing the LUT. */
 	asyh->state.pageflip_flags &= ~DRM_MODE_PAGE_FLIP_ASYNC;
 }
@@ -410,6 +421,7 @@ nv50_wndw_atomic_check(struct drm_plane *plane, struct drm_plane_state *state)
 		asyw->clr.ntfy = armw->ntfy.handle != 0;
 		asyw->clr.sema = armw->sema.handle != 0;
 		asyw->clr.xlut = armw->xlut.handle != 0;
+		asyw->clr.ctm  = armw->ctm.valid;
 		if (wndw->func->image_clr)
 			asyw->clr.image = armw->image.handle[0] != 0;
 	}
@@ -501,6 +513,7 @@ nv50_wndw_atomic_duplicate_state(struct drm_plane *plane)
 	asyw->ntfy = armw->ntfy;
 	asyw->ilut = NULL;
 	asyw->xlut = armw->xlut;
+	asyw->ctm  = armw->ctm;
 	asyw->image = armw->image;
 	asyw->point = armw->point;
 	asyw->clr.mask = 0;
diff --git a/drivers/gpu/drm/nouveau/dispnv50/wndw.h b/drivers/gpu/drm/nouveau/dispnv50/wndw.h
index 03f3d8dc235a..b6bdc6617a59 100644
--- a/drivers/gpu/drm/nouveau/dispnv50/wndw.h
+++ b/drivers/gpu/drm/nouveau/dispnv50/wndw.h
@@ -65,6 +65,10 @@ struct nv50_wndw_func {
 	int (*ntfy_wait_begun)(struct nouveau_bo *, u32 offset,
 			       struct nvif_device *);
 	void (*ilut)(struct nv50_wndw *, struct nv50_wndw_atom *);
+	void (*ctm)(struct nv50_wndw *, struct nv50_wndw_atom *,
+		    const struct drm_color_ctm *);
+	void (*ctm_set)(struct nv50_wndw *, struct nv50_wndw_atom *);
+	void (*ctm_clr)(struct nv50_wndw *);
 	bool ilut_identity;
 	bool olut_core;
 	void (*xlut_set)(struct nv50_wndw *, struct nv50_wndw_atom *);
