diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
index 4e31e386..ec77c6e5 100644
--- a/src/ipa/simple/algorithms/awb.cpp
+++ b/src/ipa/simple/algorithms/awb.cpp
@@ -59,9 +59,9 @@ void Awb::process(IPAContext &context,
 	 */
 	auto &gains = context.activeState.awb.gains;
 	gains = { {
-		sumR <= sumG / 4 ? 4.0 : static_cast<double>(sumG) / sumR,
+		sumR <= sumG / 4 ? 4.0f : static_cast<float>(sumG) / sumR,
 		1.0,
-		sumB <= sumG / 4 ? 4.0 : static_cast<double>(sumG) / sumB,
+		sumB <= sumG / 4 ? 4.0f : static_cast<float>(sumG) / sumB,
 	} };
 
 	RGB<double> rgbGains{ { 1 / gains.r(), 1 / gains.g(), 1 / gains.b() } };
diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp
new file mode 100644
index 00000000..86e0395c
--- /dev/null
+++ b/src/ipa/simple/algorithms/ccm.cpp
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ * Copyright (C) 2024-2025, Red Hat Inc.
+ *
+ * Color correction matrix
+ */
+
+#include "ccm.h"
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+
+namespace {
+
+constexpr unsigned int kTemperatureThreshold = 100;
+
+}
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+LOG_DEFINE_CATEGORY(IPASoftCcm)
+
+int Ccm::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData)
+{
+	int ret = ccm_.readYaml(tuningData["ccms"], "ct", "ccm");
+	if (ret < 0) {
+		LOG(IPASoftCcm, Error)
+			<< "Failed to parse 'ccm' parameter from tuning file.";
+		return ret;
+	}
+
+	return 0;
+}
+
+void Ccm::prepare(IPAContext &context, const uint32_t frame,
+		  IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)
+{
+	const unsigned int ct = context.activeState.awb.temperatureK;
+
+	/* Change CCM only on bigger temperature changes. */
+	if (frame > 0 &&
+	    utils::abs_diff(ct, lastCt_) < kTemperatureThreshold) {
+		frameContext.ccm.ccm = context.activeState.ccm.ccm;
+		context.activeState.ccm.changed = false;
+		return;
+	}
+
+	lastCt_ = ct;
+	Matrix<float, 3, 3> ccm = ccm_.getInterpolated(ct);
+
+	context.activeState.ccm.ccm = ccm;
+	frameContext.ccm.ccm = ccm;
+	context.activeState.ccm.changed = true;
+}
+
+void Ccm::process([[maybe_unused]] IPAContext &context,
+		  [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext,
+		  [[maybe_unused]] const SwIspStats *stats,
+		  ControlList &metadata)
+{
+	metadata.set(controls::ColourCorrectionMatrix, frameContext.ccm.ccm.data());
+}
+
+REGISTER_IPA_ALGORITHM(Ccm, "Ccm")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/ccm.h b/src/ipa/simple/algorithms/ccm.h
new file mode 100644
index 00000000..f4e2b85b
--- /dev/null
+++ b/src/ipa/simple/algorithms/ccm.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024-2025, Red Hat Inc.
+ *
+ * Color correction matrix
+ */
+
+#pragma once
+
+#include "libcamera/internal/matrix.h"
+
+#include <libipa/interpolator.h>
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class Ccm : public Algorithm
+{
+public:
+	Ccm() = default;
+	~Ccm() = default;
+
+	int init(IPAContext &context, const YamlObject &tuningData) override;
+	void prepare(IPAContext &context,
+		     const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     DebayerParams *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const SwIspStats *stats,
+		     ControlList &metadata) override;
+
+private:
+	unsigned int lastCt_;
+	Interpolator<Matrix<float, 3, 3>> ccm_;
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp
index 60dd0624..352bbf57 100644
--- a/src/ipa/simple/algorithms/lut.cpp
+++ b/src/ipa/simple/algorithms/lut.cpp
@@ -103,7 +103,7 @@ void Lut::prepare(IPAContext &context,
 		const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /
 				   gammaTableSize;
 		/* Apply gamma after gain! */
-		const RGB<double> lutGains = (gains * i / div).min(gammaTableSize - 1);
+		const RGB<float> lutGains = (gains * i / div).min(gammaTableSize - 1);
 		params->red[i] = gammaTable[static_cast<unsigned int>(lutGains.r())];
 		params->green[i] = gammaTable[static_cast<unsigned int>(lutGains.g())];
 		params->blue[i] = gammaTable[static_cast<unsigned int>(lutGains.b())];
diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
index 37a2eb53..2d0adb05 100644
--- a/src/ipa/simple/algorithms/meson.build
+++ b/src/ipa/simple/algorithms/meson.build
@@ -4,5 +4,6 @@ soft_simple_ipa_algorithms = files([
     'awb.cpp',
     'agc.cpp',
     'blc.cpp',
+    'ccm.cpp',
     'lut.cpp',
 ])
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index df0552db..57883218 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -13,6 +13,8 @@
 
 #include <libcamera/controls.h>
 
+#include "libcamera/internal/matrix.h"
+
 #include <libipa/fc_queue.h>
 #include <libipa/vector.h>
 
@@ -37,7 +39,7 @@ struct IPAActiveState {
 	} blc;
 
 	struct {
-		RGB<double> gains;
+		RGB<float> gains;
 		unsigned int temperatureK;
 	} awb;
 
@@ -47,6 +49,12 @@ struct IPAActiveState {
 		uint8_t blackLevel;
 		double contrast;
 	} gamma;
+
+	struct {
+		Matrix<float, 3, 3> ccm;
+		bool changed;
+	} ccm;
+
 	struct {
 		/* 0..2 range, 1.0 = normal */
 		std::optional<double> contrast;
@@ -54,6 +62,10 @@ struct IPAActiveState {
 };
 
 struct IPAFrameContext : public FrameContext {
+	struct {
+		Matrix<float, 3, 3> ccm;
+	} ccm;
+
 	struct {
 		int32_t exposure;
 		double gain;
