diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index 7d8fdd48..f8731eae 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -18,11 +18,25 @@ namespace libcamera {
 struct DebayerParams {
 	static constexpr unsigned int kRGBLookupSize = 256;
 
+	struct CcmColumn {
+		int16_t r;
+		int16_t g;
+		int16_t b;
+	};
+
 	using ColorLookupTable = std::array<uint8_t, kRGBLookupSize>;
+	using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
+	using GammaLookupTable = std::array<uint8_t, kRGBLookupSize>;
+
+	union LookupTable {
+		ColorLookupTable simple;
+		CcmLookupTable ccm;
+	};
 
-	ColorLookupTable red;
-	ColorLookupTable green;
-	ColorLookupTable blue;
+	LookupTable red;
+	LookupTable green;
+	LookupTable blue;
+	GammaLookupTable gammaLut;
 };
 
 } /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp
index 243f0818..ddb2adf1 100644
--- a/src/ipa/simple/algorithms/lut.cpp
+++ b/src/ipa/simple/algorithms/lut.cpp
@@ -80,6 +80,11 @@ void Lut::updateGammaTable(IPAContext &context)
 	context.activeState.gamma.contrast = contrast;
 }
 
+int16_t Lut::ccmValue(unsigned int i, float ccm) const
+{
+	return std::round(i * ccm);
+}
+
 void Lut::prepare(IPAContext &context,
 		  [[maybe_unused]] const uint32_t frame,
 		  [[maybe_unused]] IPAFrameContext &frameContext,
@@ -91,28 +96,52 @@ void Lut::prepare(IPAContext &context,
 	 * observed, it's not permanently prone to minor fluctuations or
 	 * rounding errors.
 	 */
-	if (context.activeState.gamma.blackLevel != context.activeState.blc.level ||
-	    context.activeState.gamma.contrast != context.activeState.knobs.contrast)
+	const bool gammaUpdateNeeded =
+		context.activeState.gamma.blackLevel != context.activeState.blc.level ||
+		context.activeState.gamma.contrast != context.activeState.knobs.contrast;
+	if (gammaUpdateNeeded)
 		updateGammaTable(context);
 
 	auto &gains = context.activeState.awb.gains;
 	auto &gammaTable = context.activeState.gamma.gammaTable;
 	const unsigned int gammaTableSize = gammaTable.size();
-
-	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
-		const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /
-				   gammaTableSize;
-		/* Apply gamma after gain! */
-		unsigned int idx;
-		idx = std::min({ static_cast<unsigned int>(i * gains.red / div),
-				 gammaTableSize - 1 });
-		params->red[i] = gammaTable[idx];
-		idx = std::min({ static_cast<unsigned int>(i * gains.green / div),
-				 gammaTableSize - 1 });
-		params->green[i] = gammaTable[idx];
-		idx = std::min({ static_cast<unsigned int>(i * gains.blue / div),
-				 gammaTableSize - 1 });
-		params->blue[i] = gammaTable[idx];
+	const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /
+			   gammaTableSize;
+
+	if (!context.activeState.ccm.enabled) {
+		for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+			/* Apply gamma after gain! */
+			unsigned int idx;
+			idx = std::min({ static_cast<unsigned int>(i * gains.red / div),
+					 gammaTableSize - 1 });
+			params->red.simple[i] = gammaTable[idx];
+			idx = std::min({ static_cast<unsigned int>(i * gains.green / div),
+					 gammaTableSize - 1 });
+			params->green.simple[i] = gammaTable[idx];
+			idx = std::min({ static_cast<unsigned int>(i * gains.blue / div),
+					 gammaTableSize - 1 });
+			params->blue.simple[i] = gammaTable[idx];
+		}
+	} else if (context.activeState.ccm.changed || gammaUpdateNeeded) {
+		Matrix<double, 3, 3> gainCcm = { { gains.red, 0, 0,
+						   0, gains.green, 0,
+						   0, 0, gains.blue } };
+		auto ccm = gainCcm * context.activeState.ccm.ccm;
+		auto &red = params->red.ccm;
+		auto &green = params->green.ccm;
+		auto &blue = params->blue.ccm;
+		for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+			red[i].r = ccmValue(i, ccm[0][0]);
+			red[i].g = ccmValue(i, ccm[1][0]);
+			red[i].b = ccmValue(i, ccm[2][0]);
+			green[i].r = ccmValue(i, ccm[0][1]);
+			green[i].g = ccmValue(i, ccm[1][1]);
+			green[i].b = ccmValue(i, ccm[2][1]);
+			blue[i].r = ccmValue(i, ccm[0][2]);
+			blue[i].g = ccmValue(i, ccm[1][2]);
+			blue[i].b = ccmValue(i, ccm[2][2]);
+			params->gammaLut[i] = gammaTable[i / div];
+		}
 	}
 }
 
diff --git a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h
index 889f864b..77324800 100644
--- a/src/ipa/simple/algorithms/lut.h
+++ b/src/ipa/simple/algorithms/lut.h
@@ -33,6 +33,7 @@ public:
 
 private:
 	void updateGammaTable(IPAContext &context);
+	int16_t ccmValue(unsigned int i, float ccm) const;
 };
 
 } /* namespace ipa::soft::algorithms */
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index 45fe6960..559de3e6 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -23,11 +23,56 @@ namespace libcamera {
  * \brief Size of a color lookup table
  */
 
+/**
+ * \struct DebayerParams::CcmColumn
+ * \brief Type of a single column of a color correction matrix
+ */
+
+/**
+ * \var DebayerParams::CcmColumn::r
+ * \brief Red (first) component of a CCM column
+ */
+
+/**
+ * \var DebayerParams::CcmColumn::g
+ * \brief Green (second) component of a CCM column
+ */
+
+/**
+ * \var DebayerParams::CcmColumn::b
+ * \brief Blue (third) component of a CCM column
+ */
+
 /**
  * \typedef DebayerParams::ColorLookupTable
+ * \brief Type of the simple lookup tables for red, green, blue values
+ */
+
+/**
+ * \typedef DebayerParams::CcmLookupTable
+ * \brief Type of the CCM lookup tables for red, green, blue values
+ */
+
+/**
+ * \typedef DebayerParams::GammaLookupTable
+ * \brief Type of the gamma lookup tables for CCM
+ */
+
+/**
+ * \union DebayerParams::LookupTable
  * \brief Type of the lookup tables for red, green, blue values
  */
 
+/**
+ * \var DebayerParams::LookupTable::simple
+ * \brief Simple lookup table for red, green, blue values
+ */
+
+/**
+ * \var DebayerParams::LookupTable::ccm
+ * \brief CCM lookup table for red, green, blue values
+ */
+
 /**
  * \var DebayerParams::red
  * \brief Lookup table for red color, mapping input values to output values
@@ -43,6 +88,11 @@ namespace libcamera {
  * \brief Lookup table for blue color, mapping input values to output values
  */
 
+/**
+ * \var DebayerParams::gammaLut
+ * \brief Gamma lookup table used with color correction matrix
+ */
+
 /**
  * \class Debayer
  * \brief Base debayering class
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 3c6597f9..38715b72 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -11,6 +11,7 @@
 
 #include "debayer_cpu.h"
 
+#include <algorithm>
 #include <stdlib.h>
 #include <sys/ioctl.h>
 #include <time.h>
@@ -51,8 +52,12 @@ DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)
 	enableInputMemcpy_ = true;
 
 	/* Initialize color lookup tables */
-	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++)
-		red_[i] = green_[i] = blue_[i] = i;
+	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+		red_.simple[i] = green_.simple[i] = blue_.simple[i] = i;
+		red_.ccm[i].r = red_.ccm[i].g = red_.ccm[i].b = 0;
+		green_.ccm[i].r = green_.ccm[i].g = green_.ccm[i].b = 0;
+		blue_.ccm[i].r = blue_.ccm[i].g = blue_.ccm[i].b = 0;
+	}
 }
 
 DebayerCpu::~DebayerCpu() = default;
@@ -62,12 +67,24 @@ DebayerCpu::~DebayerCpu() = default;
 	const pixel_t *curr = (const pixel_t *)src[1] + xShift_; \
 	const pixel_t *next = (const pixel_t *)src[2] + xShift_;
 
-#define STORE_PIXEL(b, g, r)        \
-	*dst++ = blue_[b];          \
-	*dst++ = green_[g];         \
-	*dst++ = red_[r];           \
-	if constexpr (addAlphaByte) \
-		*dst++ = 255;       \
+#define GAMMA(value) \
+	*dst++ = gammaLut_[std::clamp(value, 0, static_cast<int>(gammaLut_.size()) - 1)]
+
+#define STORE_PIXEL(b_, g_, r_)                                   \
+	if constexpr (ccmEnabled) {                               \
+		DebayerParams::CcmColumn &blue = blue_.ccm[b_];   \
+		DebayerParams::CcmColumn &green = green_.ccm[g_]; \
+		DebayerParams::CcmColumn &red = red_.ccm[r_];     \
+		GAMMA(blue.r + blue.g + blue.b);                  \
+		GAMMA(green.r + green.g + green.b);               \
+		GAMMA(red.r + red.g + red.b);                     \
+	} else {                                                  \
+		*dst++ = blue_.simple[b_];                        \
+		*dst++ = green_.simple[g_];                       \
+		*dst++ = red_.simple[r_];                         \
+	}                                                         \
+	if constexpr (addAlphaByte)                               \
+		*dst++ = 255;                                     \
 	x++;
 
 /*
@@ -752,6 +769,7 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 	green_ = params.green;
 	red_ = swapRedBlueGains_ ? params.blue : params.red;
 	blue_ = swapRedBlueGains_ ? params.red : params.blue;
+	gammaLut_ = params.gammaLut;
 
 	/* Copy metadata from the input buffer */
 	FrameMetadata &metadata = output->_d()->metadata();
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
index b2ec8f1b..35c51e4f 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -137,9 +137,10 @@ private:
 	/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
 	static constexpr unsigned int kMaxLineBuffers = 5;
 
-	DebayerParams::ColorLookupTable red_;
-	DebayerParams::ColorLookupTable green_;
-	DebayerParams::ColorLookupTable blue_;
+	DebayerParams::LookupTable red_;
+	DebayerParams::LookupTable green_;
+	DebayerParams::LookupTable blue_;
+	DebayerParams::GammaLookupTable gammaLut_;
 	debayerFn debayer0_;
 	debayerFn debayer1_;
 	debayerFn debayer2_;
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index 6a07de85..40e11b9e 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -82,9 +82,9 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 	for (unsigned int i = 0; i < 256; i++)
 		gammaTable[i] = UINT8_MAX * std::pow(i / 256.0, 0.5);
 	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
-		debayerParams_.red[i] = gammaTable[i];
-		debayerParams_.green[i] = gammaTable[i];
-		debayerParams_.blue[i] = gammaTable[i];
+		debayerParams_.red.simple[i] = gammaTable[i];
+		debayerParams_.green.simple[i] = gammaTable[i];
+		debayerParams_.blue.simple[i] = gammaTable[i];
 	}
 
 	if (!dmaHeap_.isValid()) {
