[v4,14/15] libcamera: ipa: simple: Remove Lut algorithm
diff mbox series

Message ID 20260122161935.208562-15-mzamazal@redhat.com
State New
Headers show
Series
  • Simple pipeline IPA cleanup
Related show

Commit Message

Milan Zamazal Jan. 22, 2026, 4:19 p.m. UTC
The Lut algorithm is not really an algorithm.  Moreover, algorithms may
be enabled or disabled but with Lut disabled, nothing will work.

Let's move the construction of lookup tables to CPU debayering, where it
is used.  The implied and related changes are:

- DebayerParams is changed to contain the real params rather than lookup
  tables.
- contrastExp parameter introduced by GPU ISP is used for CPU ISP too.
- The params must be initialised so that debayering gets meaningful
  parameter values even when some algorithms are disabled.
- combinedMatrix must be put to params everywhere where it is modified.
- Matrix changes needn't be tracked in the algorithms any more.
- CPU debayering must watch for changes of the corresponding parameters
  to update the lookup tables when and only when needed.
- Swapping red and blue is integrated into lookup table constructions.
- gpuIspEnabled flags are removed as they are not needed any more.

Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
---
 .../internal/software_isp/debayer_params.h    |  43 +----
 include/libcamera/ipa/soft.mojom              |   3 +-
 src/ipa/simple/algorithms/adjust.cpp          |  17 +-
 src/ipa/simple/algorithms/adjust.h            |   4 -
 src/ipa/simple/algorithms/awb.cpp             |   6 +-
 src/ipa/simple/algorithms/ccm.cpp             |   1 -
 src/ipa/simple/algorithms/lut.cpp             | 140 --------------
 src/ipa/simple/algorithms/lut.h               |  35 ----
 src/ipa/simple/algorithms/meson.build         |   1 -
 src/ipa/simple/data/uncalibrated.yaml         |   1 -
 src/ipa/simple/ipa_context.h                  |  11 --
 src/ipa/simple/soft_simple.cpp                |  11 +-
 src/libcamera/software_isp/debayer.cpp        | 172 +-----------------
 src/libcamera/software_isp/debayer.h          |   8 -
 src/libcamera/software_isp/debayer_cpu.cpp    | 136 ++++++++++++--
 src/libcamera/software_isp/debayer_cpu.h      |  26 ++-
 src/libcamera/software_isp/debayer_egl.cpp    |  22 +--
 src/libcamera/software_isp/software_isp.cpp   |  29 +--
 18 files changed, 188 insertions(+), 478 deletions(-)
 delete mode 100644 src/ipa/simple/algorithms/lut.cpp
 delete mode 100644 src/ipa/simple/algorithms/lut.h

Comments

Robert Mader Jan. 22, 2026, 4:58 p.m. UTC | #1
Hi, thanks for the series!

Trying this commit (on top of current master) on a FP5 (qcom) makes the 
soft-isp fail with

ERROR IPAModuleAlgo module.h:91 IPASoft: Algorithm 'Lut' not found
ERROR SoftwareIsp software_isp.cpp:157 IPA init failed
WARN SimplePipeline simple.cpp:620 Failed to create software ISP, disabling software debayering

for me. It's printed for each sensor/camera when running `cam -l`.

On 22.01.26 17:19, Milan Zamazal wrote:
> The Lut algorithm is not really an algorithm.  Moreover, algorithms may
> be enabled or disabled but with Lut disabled, nothing will work.
>
> Let's move the construction of lookup tables to CPU debayering, where it
> is used.  The implied and related changes are:
>
> - DebayerParams is changed to contain the real params rather than lookup
>    tables.
> - contrastExp parameter introduced by GPU ISP is used for CPU ISP too.
> - The params must be initialised so that debayering gets meaningful
>    parameter values even when some algorithms are disabled.
> - combinedMatrix must be put to params everywhere where it is modified.
> - Matrix changes needn't be tracked in the algorithms any more.
> - CPU debayering must watch for changes of the corresponding parameters
>    to update the lookup tables when and only when needed.
> - Swapping red and blue is integrated into lookup table constructions.
> - gpuIspEnabled flags are removed as they are not needed any more.
>
> Signed-off-by: Milan Zamazal<mzamazal@redhat.com>
> ---
>   .../internal/software_isp/debayer_params.h    |  43 +----
>   include/libcamera/ipa/soft.mojom              |   3 +-
>   src/ipa/simple/algorithms/adjust.cpp          |  17 +-
>   src/ipa/simple/algorithms/adjust.h            |   4 -
>   src/ipa/simple/algorithms/awb.cpp             |   6 +-
>   src/ipa/simple/algorithms/ccm.cpp             |   1 -
>   src/ipa/simple/algorithms/lut.cpp             | 140 --------------
>   src/ipa/simple/algorithms/lut.h               |  35 ----
>   src/ipa/simple/algorithms/meson.build         |   1 -
>   src/ipa/simple/data/uncalibrated.yaml         |   1 -
>   src/ipa/simple/ipa_context.h                  |  11 --
>   src/ipa/simple/soft_simple.cpp                |  11 +-
>   src/libcamera/software_isp/debayer.cpp        | 172 +-----------------
>   src/libcamera/software_isp/debayer.h          |   8 -
>   src/libcamera/software_isp/debayer_cpu.cpp    | 136 ++++++++++++--
>   src/libcamera/software_isp/debayer_cpu.h      |  26 ++-
>   src/libcamera/software_isp/debayer_egl.cpp    |  22 +--
>   src/libcamera/software_isp/software_isp.cpp   |  29 +--
>   18 files changed, 188 insertions(+), 478 deletions(-)
>   delete mode 100644 src/ipa/simple/algorithms/lut.cpp
>   delete mode 100644 src/ipa/simple/algorithms/lut.h
>
> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
> index 2d69bd295..1c0412d75 100644
> --- a/include/libcamera/internal/software_isp/debayer_params.h
> +++ b/include/libcamera/internal/software_isp/debayer_params.h
> @@ -1,6 +1,6 @@
>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>   /*
> - * Copyright (C) 2023-2025 Red Hat Inc.
> + * Copyright (C) 2023-2026 Red Hat Inc.
>    *
>    * Authors:
>    * Hans de Goede<hdegoede@redhat.com>
> @@ -10,7 +10,6 @@
>   
>   #pragma once
>   
> -#include <array>
>   #include <stdint.h>
>   
>   #include "libcamera/internal/matrix.h"
> @@ -19,47 +18,11 @@
>   namespace libcamera {
>   
>   struct DebayerParams {
> -	static constexpr unsigned int kRGBLookupSize = 256;
> -
> -	struct CcmColumn {
> -		int16_t r;
> -		int16_t g;
> -		int16_t b;
> -	};
> -
> -	using LookupTable = std::array<uint8_t, kRGBLookupSize>;
> -	using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
> -
> -	/*
> -	 * Color lookup tables when CCM is not used.
> -	 *
> -	 * Each color of a debayered pixel is amended by the corresponding
> -	 * value in the given table.
> -	 */
> -	LookupTable red;
> -	LookupTable green;
> -	LookupTable blue;
> -
> -	/*
> -	 * Color and gamma lookup tables when CCM is used.
> -	 *
> -	 * Each of the CcmLookupTable's corresponds to a CCM column; together they
> -	 * make a complete 3x3 CCM lookup table. The CCM is applied on debayered
> -	 * pixels and then the gamma lookup table is used to set the resulting
> -	 * values of all the three colors.
> -	 */
> -	CcmLookupTable redCcm;
> -	CcmLookupTable greenCcm;
> -	CcmLookupTable blueCcm;
> -	LookupTable gammaLut;
> -
> -	/*
> -	 * Per frame corrections as calculated by the IPA
> -	 */
> -	Matrix<float, 3, 3> ccm;
> +	Matrix<float, 3, 3> combinedMatrix;
>   	RGB<float> blackLevel;
>   	float gamma;
>   	float contrastExp;
> +	RGB<float> gains;
>   };
>   
>   } /* namespace libcamera */
> diff --git a/include/libcamera/ipa/soft.mojom b/include/libcamera/ipa/soft.mojom
> index aff8fcbd3..77328c5fd 100644
> --- a/include/libcamera/ipa/soft.mojom
> +++ b/include/libcamera/ipa/soft.mojom
> @@ -17,8 +17,7 @@ interface IPASoftInterface {
>   	     libcamera.SharedFD fdStats,
>   	     libcamera.SharedFD fdParams,
>   	     libcamera.IPACameraSensorInfo sensorInfo,
> -	     libcamera.ControlInfoMap sensorControls,
> -	     bool gpuIspEnabled)
> +	     libcamera.ControlInfoMap sensorControls)
>   		=> (int32 ret, libcamera.ControlInfoMap ipaControls, bool ccmEnabled);
>   	start() => (int32 ret);
>   	stop();
> diff --git a/src/ipa/simple/algorithms/adjust.cpp b/src/ipa/simple/algorithms/adjust.cpp
> index acdd3f741..068e98404 100644
> --- a/src/ipa/simple/algorithms/adjust.cpp
> +++ b/src/ipa/simple/algorithms/adjust.cpp
> @@ -95,23 +95,20 @@ void Adjust::applySaturation(Matrix<float, 3, 3> &matrix, float saturation)
>   void Adjust::prepare(IPAContext &context,
>   		     [[maybe_unused]] const uint32_t frame,
>   		     IPAFrameContext &frameContext,
> -		     [[maybe_unused]] DebayerParams *params)
> +		     DebayerParams *params)
>   {
>   	frameContext.gamma = context.activeState.knobs.gamma;
>   	frameContext.contrast = context.activeState.knobs.contrast;
>   
> -	if (!context.ccmEnabled)
> -		return;
> -
>   	auto &saturation = context.activeState.knobs.saturation;
> -	frameContext.saturation = saturation;
> -	if (saturation)
> +	if (context.ccmEnabled && saturation) {
>   		applySaturation(context.activeState.combinedMatrix, saturation.value());
> -
> -	if (saturation != lastSaturation_) {
> -		context.activeState.matrixChanged = true;
> -		lastSaturation_ = saturation;
> +		frameContext.saturation = saturation;
>   	}
> +
> +	params->gamma = 1.0 / context.activeState.knobs.gamma;
> +	const float contrast = context.activeState.knobs.contrast.value_or(kDefaultContrast);
> +	params->contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001));
>   }
>   
>   void Adjust::process([[maybe_unused]] IPAContext &context,
> diff --git a/src/ipa/simple/algorithms/adjust.h b/src/ipa/simple/algorithms/adjust.h
> index 7644138ff..fb133b140 100644
> --- a/src/ipa/simple/algorithms/adjust.h
> +++ b/src/ipa/simple/algorithms/adjust.h
> @@ -7,8 +7,6 @@
>   
>   #pragma once
>   
> -#include <optional>
> -
>   #include "libcamera/internal/matrix.h"
>   
>   #include <libipa/interpolator.h>
> @@ -45,8 +43,6 @@ public:
>   
>   private:
>   	void applySaturation(Matrix<float, 3, 3> &ccm, float saturation);
> -
> -	std::optional<float> lastSaturation_;
>   };
>   
>   } /* namespace ipa::soft::algorithms */
> diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
> index 4d2f1df15..d2c1b87f0 100644
> --- a/src/ipa/simple/algorithms/awb.cpp
> +++ b/src/ipa/simple/algorithms/awb.cpp
> @@ -37,7 +37,7 @@ int Awb::configure(IPAContext &context,
>   void Awb::prepare(IPAContext &context,
>   		  [[maybe_unused]] const uint32_t frame,
>   		  IPAFrameContext &frameContext,
> -		  [[maybe_unused]] DebayerParams *params)
> +		  DebayerParams *params)
>   {
>   	auto &gains = context.activeState.awb.gains;
>   	Matrix<float, 3, 3> gainMatrix = { { gains.r(), 0, 0,
> @@ -45,9 +45,11 @@ void Awb::prepare(IPAContext &context,
>   					     0, 0, gains.b() } };
>   	context.activeState.combinedMatrix =
>   		context.activeState.combinedMatrix * gainMatrix;
> -	/* Just report, the gains are applied in LUT algorithm. */
> +
>   	frameContext.gains.red = gains.r();
>   	frameContext.gains.blue = gains.b();
> +
> +	params->gains = gains;
>   }
>   
>   void Awb::process(IPAContext &context,
> diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp
> index 5576a301f..911a5af2c 100644
> --- a/src/ipa/simple/algorithms/ccm.cpp
> +++ b/src/ipa/simple/algorithms/ccm.cpp
> @@ -51,7 +51,6 @@ void Ccm::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>   	    utils::abs_diff(ct, lastCt_) >= kTemperatureThreshold) {
>   		currentCcm_ = ccm_.getInterpolated(ct);
>   		lastCt_ = ct;
> -		context.activeState.matrixChanged = true;
>   	}
>   
>   	context.activeState.combinedMatrix =
> diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp
> deleted file mode 100644
> index fd442259a..000000000
> --- a/src/ipa/simple/algorithms/lut.cpp
> +++ /dev/null
> @@ -1,140 +0,0 @@
> -/* SPDX-License-Identifier: LGPL-2.1-or-later */
> -/*
> - * Copyright (C) 2024-2026, Red Hat Inc.
> - *
> - * Color lookup tables construction
> - */
> -
> -#include "lut.h"
> -
> -#include <algorithm>
> -#include <cmath>
> -#include <optional>
> -#include <stdint.h>
> -
> -#include <libcamera/base/log.h>
> -
> -#include <libcamera/control_ids.h>
> -
> -#include "simple/ipa_context.h"
> -
> -#include "adjust.h"
> -
> -namespace libcamera {
> -
> -LOG_DEFINE_CATEGORY(IPASoftLut)
> -
> -namespace ipa::soft::algorithms {
> -
> -int Lut::configure(IPAContext &context,
> -		   [[maybe_unused]] const IPAConfigInfo &configInfo)
> -{
> -	updateGammaTable(context);
> -
> -	return 0;
> -}
> -
> -void Lut::updateGammaTable(IPAContext &context)
> -{
> -	const auto blackLevel = context.activeState.blc.level;
> -	const auto gamma = 1.0 / context.activeState.knobs.gamma;
> -	const auto contrast = context.activeState.knobs.contrast.value_or(1.0);
> -	/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */
> -	float contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001));
> -
> -	if (!context.gpuIspEnabled) {
> -		auto &gammaTable = context.activeState.gamma.gammaTable;
> -		const unsigned int blackIndex = blackLevel * gammaTable.size() / 256;
> -		const float divisor = gammaTable.size() - blackIndex - 1.0;
> -		for (unsigned int i = blackIndex; i < gammaTable.size(); i++) {
> -			double normalized = (i - blackIndex) / divisor;
> -			/* Apply simple S-curve */
> -			if (normalized < 0.5)
> -				normalized = 0.5 * std::pow(normalized / 0.5, contrastExp);
> -			else
> -				normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);
> -			gammaTable[i] = UINT8_MAX * std::pow(normalized, gamma);
> -		}
> -		/*
> -		 * Due to CCM operations, the table lookup may reach indices below the black
> -		 * level. Let's set the table values below black level to the minimum
> -		 * non-black value to prevent problems when the minimum value is
> -		 * significantly non-zero (for example, when the image should be all grey).
> -		 */
> -		std::fill(gammaTable.begin(), gammaTable.begin() + blackIndex,
> -			  gammaTable[blackIndex]);
> -	}
> -
> -	context.activeState.gamma.gamma = gamma;
> -	context.activeState.gamma.blackLevel = blackLevel;
> -	context.activeState.gamma.contrastExp = contrastExp;
> -}
> -
> -int16_t Lut::matrixValue(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,
> -		  DebayerParams *params)
> -{
> -	/*
> -	 * Update the gamma table if needed. This means if black level changes
> -	 * and since the black level gets updated only if a lower value is
> -	 * observed, it's not permanently prone to minor fluctuations or
> -	 * rounding errors.
> -	 */
> -	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();
> -	const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /
> -			   gammaTableSize;
> -
> -	if (!context.ccmEnabled) {
> -		for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
> -			/* Apply gamma after gain! */
> -			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())];
> -		}
> -	} else if (context.activeState.matrixChanged || gammaUpdateNeeded) {
> -		auto &matrix = context.activeState.combinedMatrix;
> -		auto &red = params->redCcm;
> -		auto &green = params->greenCcm;
> -		auto &blue = params->blueCcm;
> -		params->ccm = matrix;
> -		if (!context.gpuIspEnabled) {
> -			for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
> -				red[i].r = matrixValue(i, matrix[0][0]);
> -				red[i].g = matrixValue(i, matrix[1][0]);
> -				red[i].b = matrixValue(i, matrix[2][0]);
> -				green[i].r = matrixValue(i, matrix[0][1]);
> -				green[i].g = matrixValue(i, matrix[1][1]);
> -				green[i].b = matrixValue(i, matrix[2][1]);
> -				blue[i].r = matrixValue(i, matrix[0][2]);
> -				blue[i].g = matrixValue(i, matrix[1][2]);
> -				blue[i].b = matrixValue(i, matrix[2][2]);
> -				params->gammaLut[i] = gammaTable[i / div];
> -			}
> -		}
> -		context.activeState.matrixChanged = false;
> -	}
> -
> -	params->gamma = context.activeState.gamma.gamma;
> -	params->contrastExp = context.activeState.gamma.contrastExp;
> -}
> -
> -REGISTER_IPA_ALGORITHM(Lut, "Lut") - -} /* namespace ipa::soft::algorithms */ - -} /* namespace 
> libcamera */ diff --git a/src/ipa/simple/algorithms/lut.h 
> b/src/ipa/simple/algorithms/lut.h deleted file mode 100644 index 
> ad16d1e8e..000000000 --- a/src/ipa/simple/algorithms/lut.h +++ 
> /dev/null @@ -1,35 +0,0 @@ -/* SPDX-License-Identifier: 
> LGPL-2.1-or-later */ -/* - * Copyright (C) 2024, Red Hat Inc. - * - * 
> Color lookup tables construction - */ - -#pragma once - -#include "algorithm.h"
> -
> -namespace libcamera {
> -
> -namespace ipa::soft::algorithms {
> -
> -class Lut : public Algorithm
> -{
> -public:
> -	Lut() = default;
> -	~Lut() = default;
> -
> -	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
> -	void prepare(IPAContext &context,
> -		     const uint32_t frame,
> -		     IPAFrameContext &frameContext,
> -		     DebayerParams *params) override;
> -
> -private:
> -	void updateGammaTable(IPAContext &context);
> -	int16_t matrixValue(unsigned int i, float ccm) const;
> -};
> -
> -} /* namespace ipa::soft::algorithms */
> -
> -} /* namespace libcamera */
> diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
> index ebe9f20dd..73c637220 100644
> --- a/src/ipa/simple/algorithms/meson.build
> +++ b/src/ipa/simple/algorithms/meson.build
> @@ -6,5 +6,4 @@ soft_simple_ipa_algorithms = files([
>       'agc.cpp',
>       'blc.cpp',
>       'ccm.cpp',
> -    'lut.cpp',
>   ])
> diff --git a/src/ipa/simple/data/uncalibrated.yaml b/src/ipa/simple/data/uncalibrated.yaml
> index e389e0588..c6feda36d 100644
> --- a/src/ipa/simple/data/uncalibrated.yaml
> +++ b/src/ipa/simple/data/uncalibrated.yaml
> @@ -15,6 +15,5 @@ algorithms:
>                    0, 1, 0,
>                    0, 0, 1]
>     - Adjust:
> -  - Lut:
>     - Agc:
>   ...
> diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
> index 293e35b71..34f7403a4 100644
> --- a/src/ipa/simple/ipa_context.h
> +++ b/src/ipa/simple/ipa_context.h
> @@ -53,17 +53,7 @@ struct IPAActiveState {
>   		unsigned int temperatureK;
>   	} awb;
>   
> -	static constexpr unsigned int kGammaLookupSize = 1024;
> -	struct {
> -		std::array<double, kGammaLookupSize> gammaTable;
> -		uint8_t blackLevel;
> -		float gamma;
> -		float contrast;
> -		float contrastExp;
> -	} gamma;
> -
>   	Matrix<float, 3, 3> combinedMatrix;
> -	bool matrixChanged = false;
>   
>   	struct {
>   		float gamma;
> @@ -103,7 +93,6 @@ struct IPAContext {
>   	FCQueue<IPAFrameContext> frameContexts;
>   	ControlInfoMap::Map ctrlMap;
>   	bool ccmEnabled = false;
> -	bool gpuIspEnabled = false;
>   };
>   
>   } /* namespace ipa::soft */
> diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
> index 732e82510..6bef597c8 100644
> --- a/src/ipa/simple/soft_simple.cpp
> +++ b/src/ipa/simple/soft_simple.cpp
> @@ -26,6 +26,7 @@
>   #include "libcamera/internal/software_isp/swisp_stats.h"
>   #include "libcamera/internal/yaml_parser.h"
>   
> +#include "algorithms/adjust.h"
>   #include "libipa/camera_sensor_helper.h"
>   
>   #include "module.h"
> @@ -55,7 +56,6 @@ public:
>   		 const SharedFD &fdParams,
>   		 const IPACameraSensorInfo &sensorInfo,
>   		 const ControlInfoMap &sensorControls,
> -		 bool gpuIspEnabled,
>   		 ControlInfoMap *ipaControls,
>   		 bool *ccmEnabled) override;
>   	int configure(const IPAConfigInfo &configInfo) override;
> @@ -96,7 +96,6 @@ int IPASoftSimple::init(const IPASettings &settings,
>   			const SharedFD &fdParams,
>   			const IPACameraSensorInfo &sensorInfo,
>   			const ControlInfoMap &sensorControls,
> -			bool gpuIspEnabled,
>   			ControlInfoMap *ipaControls,
>   			bool *ccmEnabled)
>   {
> @@ -108,7 +107,6 @@ int IPASoftSimple::init(const IPASettings &settings,
>   	}
>   
>   	context_.sensorInfo = sensorInfo;
> -	context_.gpuIspEnabled = gpuIspEnabled;
>   
>   	/* Load the tuning data file */
>   	File file(settings.configurationFile);
> @@ -161,6 +159,11 @@ int IPASoftSimple::init(const IPASettings &settings,
>   		}
>   
>   		params_ = static_cast<DebayerParams *>(mem);
> +		params_->blackLevel = { { 0.0, 0.0, 0.0 } };
> +		params_->gamma = 1.0 / algorithms::kDefaultGamma;
> +		params_->contrastExp = 1.0;
> +		params_->gains = { { 1.0, 1.0, 1.0 } };
> +		/* combinedMatrix is reset for each frame. */
>   	}
>   
>   	{
> @@ -287,6 +290,8 @@ void IPASoftSimple::computeParams(const uint32_t frame)
>   	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
>   	for (auto const &algo : algorithms())
>   		algo->prepare(context_, frame, frameContext, params_);
> +	params_->combinedMatrix = context_.activeState.combinedMatrix;
> +
>   	setIspParams.emit();
>   }
>   
> diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
> index 65a1762dd..dccdd86b4 100644
> --- a/src/libcamera/software_isp/debayer.cpp
> +++ b/src/libcamera/software_isp/debayer.cpp
> @@ -1,7 +1,7 @@
>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>   /*
>    * Copyright (C) 2023, Linaro Ltd
> - * Copyright (C) 2023-2025 Red Hat Inc.
> + * Copyright (C) 2023-2026 Red Hat Inc.
>    *
>    * Authors:
>    * Hans de Goede<hdegoede@redhat.com>
> @@ -25,99 +25,28 @@ namespace libcamera {
>    */
>   
>   /**
> - * \var DebayerParams::kRGBLookupSize
> - * \brief Size of a color lookup table
> + * \var DebayerParams::gains
> + * \brief Colour channel gains
>    */
>   
>   /**
> - * \struct DebayerParams::CcmColumn
> - * \brief Type of a single column of a color correction matrix (CCM)
> - *
> - * When multiplying an input pixel, columns in the CCM correspond to the red,
> - * green or blue component of input pixel values, while rows correspond to the
> - * red, green or blue components of the output pixel values. The members of the
> - * CcmColumn structure are named after the colour components of the output pixel
> - * values they correspond to.
> - */
> -
> -/**
> - * \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::LookupTable
> - * \brief Type of the lookup tables for single lookup values
> - */
> -
> -/**
> - * \typedef DebayerParams::CcmLookupTable
> - * \brief Type of the CCM lookup tables for red, green, blue values
> - */
> -
> -/**
> - * \var DebayerParams::red
> - * \brief Lookup table for red color, mapping input values to output values
> - */
> -
> -/**
> - * \var DebayerParams::green
> - * \brief Lookup table for green color, mapping input values to output values
> - */
> -
> -/**
> - * \var DebayerParams::blue
> - * \brief Lookup table for blue color, mapping input values to output values
> - */
> -
> -/**
> - * \var DebayerParams::redCcm
> - * \brief Lookup table for the CCM red column, mapping input values to output values
> - */
> -
> -/**
> - * \var DebayerParams::greenCcm
> - * \brief Lookup table for the CCM green column, mapping input values to output values
> - */
> -
> -/**
> - * \var DebayerParams::blueCcm
> - * \brief Lookup table for the CCM blue column, mapping input values to output values
> - */
> -
> -/**
> - * \var DebayerParams::gammaLut
> - * \brief Gamma lookup table used with color correction matrix
> - */
> -
> -/**
> - * \var DebayerParams::ccm
> - * \brief Per frame colour correction matrix for GPUISP
> + * \var DebayerParams::combinedMatrix
> + * \brief Colour correction matrix, including other adjustments
>    */
>   
>   /**
>    * \var DebayerParams::blackLevel
> - * \brief Blacklevel gains for the GPUISP
> + * \brief Black level values
>    */
>   
>   /**
>    * \var DebayerParams::gamma
> - * \brief Gamma value for the GPUISP
> + * \brief Gamma value, e.g. 1/2.2
>    */
>   
>   /**
>    * \var DebayerParams::contrastExp
> - * \brief Contrast value for GPUISP
> + * \brief Contrast value to be used as an exponent
>    */
>   
>   /**
> @@ -131,13 +60,6 @@ LOG_DEFINE_CATEGORY(Debayer)
>   
>   Debayer::Debayer(const GlobalConfiguration &configuration) : bench_(configuration)
>   {
> -	/* Initialize color lookup tables */
> -	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
> -		red_[i] = green_[i] = blue_[i] = i;
> -		redCcm_[i] = { static_cast<int16_t>(i), 0, 0 };
> -		greenCcm_[i] = { 0, static_cast<int16_t>(i), 0 };
> -		blueCcm_[i] = { 0, 0, static_cast<int16_t>(i) };
> -	}
>   }
>   
>   Debayer::~Debayer()
> @@ -305,56 +227,6 @@ Debayer::~Debayer()
>    * \brief Output size object
>    */
>   
> -/**
> - * \var Debayer::red_
> - * \brief Lookup table for red channel gain and correction values
> - *
> - * This table provides precomputed per-pixel or per-intensity
> - * correction values for the red color channel used during debayering.
> - */
> -
> -/**
> - * \var Debayer::green_
> - * \brief Lookup table for green channel gain and correction values
> - *
> - * This table provides precomputed per-pixel or per-intensity
> - * correction values for the green color channel used during debayering.
> - */
> -
> -/**
> - * \var Debayer::blue_
> - * \brief Lookup table for blue channel gain and correction values
> - *
> - * This table provides precomputed per-pixel or per-intensity
> - * correction values for the blue color channel used during debayering.
> - */
> -
> -/**
> - * \var Debayer::redCcm_
> - * \brief Red channel Color Correction Matrix (CCM) lookup table
> - *
> - * Contains coefficients for green channel color correction.
> - */
> -
> -/**
> - * \var Debayer::greenCcm_
> - * \brief Green channel Color Correction Matrix (CCM) lookup table
> - *
> - * Contains coefficients for green channel color correction.
> - */
> -
> -/**
> - * \var Debayer::blueCcm_
> - * \brief Blue channel Color Correction Matrix (CCM) lookup table
> - *
> - * Contains coefficients for blue channel color correction.
> - */
> -
> -/**
> - * \var Debayer::gammaLut_
> - * \brief Gamma correction lookup table
> - */
> -
>   /**
>    * \var Debayer::swapRedBlueGains_
>    * \brief Flag indicating whether red and blue channel gains should be swapped
> @@ -396,34 +268,6 @@ Debayer::~Debayer()
>    * DebayerEGL::start.
>    */
>   
> -/**
> - * \fn void Debayer::setParams(DebayerParams &params)
> - * \brief Select the bayer params to use for the next frame debayer
> - * \param[in] params The parameters to be used in debayering
> - */
> -void Debayer::setParams(DebayerParams &params)
> -{
> -	green_ = params.green;
> -	greenCcm_ = params.greenCcm;
> -	if (swapRedBlueGains_) {
> -		red_ = params.blue;
> -		blue_ = params.red;
> -		redCcm_ = params.blueCcm;
> -		blueCcm_ = params.redCcm;
> -		for (unsigned int i = 0; i < 256; i++) {
> -			std::swap(redCcm_[i].r, redCcm_[i].b);
> -			std::swap(greenCcm_[i].r, greenCcm_[i].b);
> -			std::swap(blueCcm_[i].r, blueCcm_[i].b);
> -		}
> -	} else {
> -		red_ = params.red;
> -		blue_ = params.blue;
> -		redCcm_ = params.redCcm;
> -		blueCcm_ = params.blueCcm;
> -	}
> -	gammaLut_ = params.gammaLut;
> -}
> -
>   /**
>    * \fn void Debayer::dmaSyncBegin(DebayerParams &params)
>    * \brief Common CPU/GPU Dma Sync Buffer begin
> diff --git a/src/libcamera/software_isp/debayer.h b/src/libcamera/software_isp/debayer.h
> index cd2db9930..652cff4cc 100644
> --- a/src/libcamera/software_isp/debayer.h
> +++ b/src/libcamera/software_isp/debayer.h
> @@ -78,13 +78,6 @@ public:
>   	Size outputSize_;
>   	PixelFormat inputPixelFormat_;
>   	PixelFormat outputPixelFormat_;
> -	DebayerParams::LookupTable red_;
> -	DebayerParams::LookupTable green_;
> -	DebayerParams::LookupTable blue_;
> -	DebayerParams::CcmLookupTable redCcm_;
> -	DebayerParams::CcmLookupTable greenCcm_;
> -	DebayerParams::CcmLookupTable blueCcm_;
> -	DebayerParams::LookupTable gammaLut_;
>   	bool swapRedBlueGains_;
>   	Benchmark bench_;
>   
> @@ -92,7 +85,6 @@ private:
>   	virtual Size patternSize(PixelFormat inputFormat) = 0;
>   
>   protected:
> -	void setParams(DebayerParams &params);
>   	void dmaSyncBegin(std::vector<DmaSyncer> &dmaSyncers, FrameBuffer *input, FrameBuffer *output);
>   	static bool isStandardBayerOrder(BayerFormat::Order order);
>   };
> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
> index 00738c56b..af7af0a8d 100644
> --- a/src/libcamera/software_isp/debayer_cpu.cpp
> +++ b/src/libcamera/software_isp/debayer_cpu.cpp
> @@ -1,7 +1,7 @@
>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>   /*
>    * Copyright (C) 2023, Linaro Ltd
> - * Copyright (C) 2023-2025 Red Hat Inc.
> + * Copyright (C) 2023-2026 Red Hat Inc.
>    *
>    * Authors:
>    * Hans de Goede<hdegoede@redhat.com>
> @@ -68,21 +68,21 @@ DebayerCpu::~DebayerCpu() = default;
>   #define GAMMA(value) \
>   	*dst++ = gammaLut_[std::clamp(value, 0, static_cast<int>(gammaLut_.size()) - 1)]
>   
> -#define STORE_PIXEL(b_, g_, r_)                                        \
> -	if constexpr (ccmEnabled) {                                    \
> -		const DebayerParams::CcmColumn &blue = blueCcm_[b_];   \
> -		const DebayerParams::CcmColumn &green = greenCcm_[g_]; \
> -		const DebayerParams::CcmColumn &red = redCcm_[r_];     \
> -		GAMMA(blue.b + green.b + red.b);                       \
> -		GAMMA(blue.g + green.g + red.g);                       \
> -		GAMMA(blue.r + green.r + red.r);                       \
> -	} else {                                                       \
> -		*dst++ = blue_[b_];                                    \
> -		*dst++ = green_[g_];                                   \
> -		*dst++ = red_[r_];                                     \
> -	}                                                              \
> -	if constexpr (addAlphaByte)                                    \
> -		*dst++ = 255;                                          \
> +#define STORE_PIXEL(b_, g_, r_)                         \
> +	if constexpr (ccmEnabled) {                     \
> +		const CcmColumn &blue = blueCcm_[b_];   \
> +		const CcmColumn &green = greenCcm_[g_]; \
> +		const CcmColumn &red = redCcm_[r_];     \
> +		GAMMA(blue.b + green.b + red.b);        \
> +		GAMMA(blue.g + green.g + red.g);        \
> +		GAMMA(blue.r + green.r + red.r);        \
> +	} else {                                        \
> +		*dst++ = blue_[b_];                     \
> +		*dst++ = green_[g_];                    \
> +		*dst++ = red_[r_];                      \
> +	}                                               \
> +	if constexpr (addAlphaByte)                     \
> +		*dst++ = 255;                           \
>   	x++;
>   
>   /*
> @@ -525,6 +525,16 @@ int DebayerCpu::configure(const StreamConfiguration &inputCfg,
>   	if (ret != 0)
>   		return -EINVAL;
>   
> +	ccmEnabled_ = ccmEnabled;
> +
> +	/*
> +	 * Lookup tables must be initialized because the initial value is used for
> +	 * the first two frames, i.e. until stats processing starts providing its
> +	 * own parameters. Let's enforce recomputing lookup tables by setting the
> +	 * stored last used gamma to an out-of-range value.
> +	 */
> +	params_.gamma = 1.0;
> +
>   	window_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) &
>   		    ~(inputConfig_.patternSize.width - 1);
>   	window_.y = ((inputCfg.size.height - outputCfg.size.height) / 2) &
> @@ -740,6 +750,98 @@ void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst)
>   	}
>   }
>   
> +void DebayerCpu::updateGammaTable(DebayerParams &params)
> +{
> +	const RGB<float> blackLevel = params.blackLevel;
> +	/* Take let's say the green channel black level */
> +	const unsigned int blackIndex = blackLevel[1] * gammaTable_.size();
> +	const float gamma = params.gamma;
> +	const float contrastExp = params.contrastExp;
> +
> +	const float divisor = gammaTable_.size() - blackIndex - 1.0;
> +	for (unsigned int i = blackIndex; i < gammaTable_.size(); i++) {
> +		float normalized = (i - blackIndex) / divisor;
> +		/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */
> +		/* Apply simple S-curve */
> +		if (normalized < 0.5)
> +			normalized = 0.5 * std::pow(normalized / 0.5, contrastExp);
> +		else
> +			normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);
> +		gammaTable_[i] = UINT8_MAX *
> +				 std::pow(normalized, gamma);
> +	}
> +	/*
> +	 * Due to CCM operations, the table lookup may reach indices below the black
> +	 * level. Let's set the table values below black level to the minimum
> +	 * non-black value to prevent problems when the minimum value is
> +	 * significantly non-zero (for example, when the image should be all grey).
> +	 */
> +	std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex,
> +		  gammaTable_[blackIndex]);
> +}
> +
> +void DebayerCpu::updateLookupTables(DebayerParams &params)
> +{
> +	const bool gammaUpdateNeeded =
> +		params.gamma != params_.gamma ||
> +		params.blackLevel != params_.blackLevel ||
> +		params.contrastExp != params_.contrastExp;
> +	if (gammaUpdateNeeded)
> +		updateGammaTable(params);
> +
> +	auto matrixChanged = [](const Matrix<float, 3, 3> &m1, const Matrix<float, 3, 3> &m2) -> bool {
> +		return !std::equal(m1.data().begin(), m1.data().end(), m2.data().begin());
> +	};
> +	const unsigned int gammaTableSize = gammaTable_.size();
> +	const double div = static_cast<double>(kRGBLookupSize) / gammaTableSize;
> +	if (ccmEnabled_) {
> +		if (gammaUpdateNeeded ||
> +		    matrixChanged(params.combinedMatrix, params_.combinedMatrix)) {
> +			auto &red = swapRedBlueGains_ ? blueCcm_ : redCcm_;
> +			auto &green = greenCcm_;
> +			auto &blue = swapRedBlueGains_ ? redCcm_ : blueCcm_;
> +			const unsigned int redIndex = swapRedBlueGains_ ? 2 : 0;
> +			const unsigned int greenIndex = 1;
> +			const unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2;
> +			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
> +				red[i].r = std::round(i * params.combinedMatrix[redIndex][0]);
> +				red[i].g = std::round(i * params.combinedMatrix[greenIndex][0]);
> +				red[i].b = std::round(i * params.combinedMatrix[blueIndex][0]);
> +				green[i].r = std::round(i * params.combinedMatrix[redIndex][1]);
> +				green[i].g = std::round(i * params.combinedMatrix[greenIndex][1]);
> +				green[i].b = std::round(i * params.combinedMatrix[blueIndex][1]);
> +				blue[i].r = std::round(i * params.combinedMatrix[redIndex][2]);
> +				blue[i].g = std::round(i * params.combinedMatrix[greenIndex][2]);
> +				blue[i].b = std::round(i * params.combinedMatrix[blueIndex][2]);
> +				gammaLut_[i] = gammaTable_[i / div];
> +			}
> +		}
> +	} else {
> +		if (gammaUpdateNeeded || params.gains != params_.gains) {
> +			auto &gains = params.gains;
> +			auto &red = swapRedBlueGains_ ? blue_ : red_;
> +			auto &green = green_;
> +			auto &blue = swapRedBlueGains_ ? red_ : blue_;
> +			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
> +				/* Apply gamma after gain! */
> +				const RGB<float> lutGains = (gains * i / div).min(gammaTableSize - 1);
> +				red[i] = gammaTable_[static_cast<unsigned int>(lutGains.r())];
> +				green[i] = gammaTable_[static_cast<unsigned int>(lutGains.g())];
> +				blue[i] = gammaTable_[static_cast<unsigned int>(lutGains.b())];
> +			}
> +		}
> +	}
> +
> +	LOG(Debayer, Debug)
> +		<< "Debayer parameters: blackLevel=" << params.blackLevel
> +		<< "; gamma=" << params.gamma
> +		<< "; contrastExp=" << params.contrastExp
> +		<< "; gains=" << params.gains
> +		<< "; matrix=" << params.combinedMatrix;
> +
> +	params_ = params;
> +}
> +
>   void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output, DebayerParams params)
>   {
>   	bench_.startFrame();
> @@ -748,7 +850,7 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>   
>   	dmaSyncBegin(dmaSyncers, input, output);
>   
> -	setParams(params);
> +	updateLookupTables(params);
>   
>   	/* 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 67df2b93a..b5cbb5bd2 100644
> --- a/src/libcamera/software_isp/debayer_cpu.h
> +++ b/src/libcamera/software_isp/debayer_cpu.h
> @@ -1,7 +1,7 @@
>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>   /*
>    * Copyright (C) 2023, Linaro Ltd
> - * Copyright (C) 2023-2025 Red Hat Inc.
> + * Copyright (C) 2023-2026 Red Hat Inc.
>    *
>    * Authors:
>    * Hans de Goede<hdegoede@redhat.com>
> @@ -18,6 +18,8 @@
>   #include <libcamera/base/object.h>
>   
>   #include "libcamera/internal/bayer_format.h"
> +#include "libcamera/internal/global_configuration.h"
> +#include "libcamera/internal/software_isp/debayer_params.h"
>   #include "libcamera/internal/software_isp/swstats_cpu.h"
>   
>   #include "debayer.h"
> @@ -108,10 +110,32 @@ private:
>   	void memcpyNextLine(const uint8_t *linePointers[]);
>   	void process2(uint32_t frame, const uint8_t *src, uint8_t *dst);
>   	void process4(uint32_t frame, const uint8_t *src, uint8_t *dst);
> +	void updateGammaTable(DebayerParams &params);
> +	void updateLookupTables(DebayerParams &params);
>   
>   	/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
>   	static constexpr unsigned int kMaxLineBuffers = 5;
>   
> +	static constexpr unsigned int kRGBLookupSize = 256;
> +	static constexpr unsigned int kGammaLookupSize = 1024;
> +	struct CcmColumn {
> +		int16_t r;
> +		int16_t g;
> +		int16_t b;
> +	};
> +	using LookupTable = std::array<uint8_t, kRGBLookupSize>;
> +	using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
> +	LookupTable red_;
> +	LookupTable green_;
> +	LookupTable blue_;
> +	CcmLookupTable redCcm_;
> +	CcmLookupTable greenCcm_;
> +	CcmLookupTable blueCcm_;
> +	std::array<double, kGammaLookupSize> gammaTable_;
> +	LookupTable gammaLut_;
> +	bool ccmEnabled_;
> +	DebayerParams params_;
> +
>   	debayerFn debayer0_;
>   	debayerFn debayer1_;
>   	debayerFn debayer2_;
> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index 9693d7252..af04d60ca 100644
> --- a/src/libcamera/software_isp/debayer_egl.cpp
> +++ b/src/libcamera/software_isp/debayer_egl.cpp
> @@ -475,18 +475,18 @@ void DebayerEGL::setShaderVariableValues(DebayerParams &params)
>   			    << " textureUniformProjMatrix_ " << textureUniformProjMatrix_;
>   
>   	GLfloat ccm[9] = {
> -		params.ccm[0][0],
> -		params.ccm[0][1],
> -		params.ccm[0][2],
> -		params.ccm[1][0],
> -		params.ccm[1][1],
> -		params.ccm[1][2],
> -		params.ccm[2][0],
> -		params.ccm[2][1],
> -		params.ccm[2][2],
> +		params.combinedMatrix[0][0],
> +		params.combinedMatrix[0][1],
> +		params.combinedMatrix[0][2],
> +		params.combinedMatrix[1][0],
> +		params.combinedMatrix[1][1],
> +		params.combinedMatrix[1][2],
> +		params.combinedMatrix[2][0],
> +		params.combinedMatrix[2][1],
> +		params.combinedMatrix[2][2],
>   	};
>   	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_FALSE, ccm);
> -	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.ccm;
> +	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
>   
>   	/*
>   	 * 0 = Red, 1 = Green, 2 = Blue
> @@ -544,8 +544,6 @@ void DebayerEGL::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>   
>   	dmaSyncBegin(dmaSyncers, input, nullptr);
>   
> -	setParams(params);
> -
>   	/* Copy metadata from the input buffer */
>   	FrameMetadata &metadata = output->_d()->metadata();
>   	metadata.status = input->metadata().status;
> diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
> index 7ad3511db..a83986b78 100644
> --- a/src/libcamera/software_isp/software_isp.cpp
> +++ b/src/libcamera/software_isp/software_isp.cpp
> @@ -84,23 +84,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>   		   DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap |
>   		   DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
>   {
> -	/*
> -	 * debayerParams_ must be initialized because the initial value is used for
> -	 * the first two frames, i.e. until stats processing starts providing its
> -	 * own parameters.
> -	 *
> -	 * \todo This should be handled in the same place as the related
> -	 * operations, in the IPA module.
> -	 */
> -	std::array<uint8_t, 256> gammaTable;
> -	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];
> -	}
> -
>   	if (!dmaHeap_.isValid()) {
>   		LOG(SoftwareIsp, Error) << "Failed to create DmaBufAllocator object";
>   		return;
> @@ -121,8 +104,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>   	}
>   	stats->statsReady.connect(this, &SoftwareIsp::statsReady);
>   
> -	bool gpuIspEnabled;
> -
>   #if HAVE_DEBAYER_EGL
>   	std::optional<std::string> softISPMode = configuration.envOption("LIBCAMERA_SOFTISP_MODE", { "software_isp", "mode" });
>   	if (softISPMode) {
> @@ -133,15 +114,12 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>   		}
>   	}
>   
> -	if (!softISPMode || softISPMode == "gpu") {
> +	if (!softISPMode || softISPMode == "gpu")
>   		debayer_ = std::make_unique<DebayerEGL>(std::move(stats), configuration);
> -		gpuIspEnabled = true;
> -	}
> +
>   #endif
> -	if (!debayer_) {
> +	if (!debayer_)
>   		debayer_ = std::make_unique<DebayerCpu>(std::move(stats), configuration);
> -		gpuIspEnabled = false;
> -	}
>   
>   	debayer_->inputBufferReady.connect(this, &SoftwareIsp::inputReady);
>   	debayer_->outputBufferReady.connect(this, &SoftwareIsp::outputReady);
> @@ -173,7 +151,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>   			 sharedParams_.fd(),
>   			 sensorInfo,
>   			 sensor->controls(),
> -			 gpuIspEnabled,
>   			 ipaControls,
>   			 &ccmEnabled_);
>   	if (ret) {
Barnabás Pőcze Jan. 22, 2026, 5:01 p.m. UTC | #2
2026. 01. 22. 17:58 keltezéssel, Robert Mader írta:
> Hi, thanks for the series!
> 
> Trying this commit (on top of current master) on a FP5 (qcom) makes the soft-isp fail with
> 
> ERROR IPAModuleAlgo module.h:91 IPASoft: Algorithm 'Lut' not found
> ERROR SoftwareIsp software_isp.cpp:157 IPA init failed
> WARN SimplePipeline simple.cpp:620 Failed to create software ISP, disabling software debayering
> 
> for me. It's printed for each sensor/camera when running `cam -l`.

Maybe old tuning files are loaded? This patch definitely removes it from
uncalibrated.yaml but maybe you have sensor specific ones locally.


> 
> On 22.01.26 17:19, Milan Zamazal wrote:
>> The Lut algorithm is not really an algorithm.  Moreover, algorithms may
>> be enabled or disabled but with Lut disabled, nothing will work.
>>
>> Let's move the construction of lookup tables to CPU debayering, where it
>> is used.  The implied and related changes are:
>>
>> - DebayerParams is changed to contain the real params rather than lookup
>>    tables.
>> - contrastExp parameter introduced by GPU ISP is used for CPU ISP too.
>> - The params must be initialised so that debayering gets meaningful
>>    parameter values even when some algorithms are disabled.
>> - combinedMatrix must be put to params everywhere where it is modified.
>> - Matrix changes needn't be tracked in the algorithms any more.
>> - CPU debayering must watch for changes of the corresponding parameters
>>    to update the lookup tables when and only when needed.
>> - Swapping red and blue is integrated into lookup table constructions.
>> - gpuIspEnabled flags are removed as they are not needed any more.
>>
>> Signed-off-by: Milan Zamazal<mzamazal@redhat.com>
>> ---
>>   .../internal/software_isp/debayer_params.h    |  43 +----
>>   include/libcamera/ipa/soft.mojom              |   3 +-
>>   src/ipa/simple/algorithms/adjust.cpp          |  17 +-
>>   src/ipa/simple/algorithms/adjust.h            |   4 -
>>   src/ipa/simple/algorithms/awb.cpp             |   6 +-
>>   src/ipa/simple/algorithms/ccm.cpp             |   1 -
>>   src/ipa/simple/algorithms/lut.cpp             | 140 --------------
>>   src/ipa/simple/algorithms/lut.h               |  35 ----
>>   src/ipa/simple/algorithms/meson.build         |   1 -
>>   src/ipa/simple/data/uncalibrated.yaml         |   1 -
>>   src/ipa/simple/ipa_context.h                  |  11 --
>>   src/ipa/simple/soft_simple.cpp                |  11 +-
>>   src/libcamera/software_isp/debayer.cpp        | 172 +-----------------
>>   src/libcamera/software_isp/debayer.h          |   8 -
>>   src/libcamera/software_isp/debayer_cpu.cpp    | 136 ++++++++++++--
>>   src/libcamera/software_isp/debayer_cpu.h      |  26 ++-
>>   src/libcamera/software_isp/debayer_egl.cpp    |  22 +--
>>   src/libcamera/software_isp/software_isp.cpp   |  29 +--
>>   18 files changed, 188 insertions(+), 478 deletions(-)
>>   delete mode 100644 src/ipa/simple/algorithms/lut.cpp
>>   delete mode 100644 src/ipa/simple/algorithms/lut.h
>>
>> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
>> index 2d69bd295..1c0412d75 100644
>> --- a/include/libcamera/internal/software_isp/debayer_params.h
>> +++ b/include/libcamera/internal/software_isp/debayer_params.h
>> @@ -1,6 +1,6 @@
>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>   /*
>> - * Copyright (C) 2023-2025 Red Hat Inc.
>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>    *
>>    * Authors:
>>    * Hans de Goede<hdegoede@redhat.com>
>> @@ -10,7 +10,6 @@
>>   
>>   #pragma once
>>   
>> -#include <array>
>>   #include <stdint.h>
>>   
>>   #include "libcamera/internal/matrix.h"
>> @@ -19,47 +18,11 @@
>>   namespace libcamera {
>>   
>>   struct DebayerParams {
>> -	static constexpr unsigned int kRGBLookupSize = 256;
>> -
>> -	struct CcmColumn {
>> -		int16_t r;
>> -		int16_t g;
>> -		int16_t b;
>> -	};
>> -
>> -	using LookupTable = std::array<uint8_t, kRGBLookupSize>;
>> -	using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
>> -
>> -	/*
>> -	 * Color lookup tables when CCM is not used.
>> -	 *
>> -	 * Each color of a debayered pixel is amended by the corresponding
>> -	 * value in the given table.
>> -	 */
>> -	LookupTable red;
>> -	LookupTable green;
>> -	LookupTable blue;
>> -
>> -	/*
>> -	 * Color and gamma lookup tables when CCM is used.
>> -	 *
>> -	 * Each of the CcmLookupTable's corresponds to a CCM column; together they
>> -	 * make a complete 3x3 CCM lookup table. The CCM is applied on debayered
>> -	 * pixels and then the gamma lookup table is used to set the resulting
>> -	 * values of all the three colors.
>> -	 */
>> -	CcmLookupTable redCcm;
>> -	CcmLookupTable greenCcm;
>> -	CcmLookupTable blueCcm;
>> -	LookupTable gammaLut;
>> -
>> -	/*
>> -	 * Per frame corrections as calculated by the IPA
>> -	 */
>> -	Matrix<float, 3, 3> ccm;
>> +	Matrix<float, 3, 3> combinedMatrix;
>>   	RGB<float> blackLevel;
>>   	float gamma;
>>   	float contrastExp;
>> +	RGB<float> gains;
>>   };
>>   
>>   } /* namespace libcamera */
>> diff --git a/include/libcamera/ipa/soft.mojom b/include/libcamera/ipa/soft.mojom
>> index aff8fcbd3..77328c5fd 100644
>> --- a/include/libcamera/ipa/soft.mojom
>> +++ b/include/libcamera/ipa/soft.mojom
>> @@ -17,8 +17,7 @@ interface IPASoftInterface {
>>   	     libcamera.SharedFD fdStats,
>>   	     libcamera.SharedFD fdParams,
>>   	     libcamera.IPACameraSensorInfo sensorInfo,
>> -	     libcamera.ControlInfoMap sensorControls,
>> -	     bool gpuIspEnabled)
>> +	     libcamera.ControlInfoMap sensorControls)
>>   		=> (int32 ret, libcamera.ControlInfoMap ipaControls, bool ccmEnabled);
>>   	start() => (int32 ret);
>>   	stop();
>> diff --git a/src/ipa/simple/algorithms/adjust.cpp b/src/ipa/simple/algorithms/adjust.cpp
>> index acdd3f741..068e98404 100644
>> --- a/src/ipa/simple/algorithms/adjust.cpp
>> +++ b/src/ipa/simple/algorithms/adjust.cpp
>> @@ -95,23 +95,20 @@ void Adjust::applySaturation(Matrix<float, 3, 3> &matrix, float saturation)
>>   void Adjust::prepare(IPAContext &context,
>>   		     [[maybe_unused]] const uint32_t frame,
>>   		     IPAFrameContext &frameContext,
>> -		     [[maybe_unused]] DebayerParams *params)
>> +		     DebayerParams *params)
>>   {
>>   	frameContext.gamma = context.activeState.knobs.gamma;
>>   	frameContext.contrast = context.activeState.knobs.contrast;
>>   
>> -	if (!context.ccmEnabled)
>> -		return;
>> -
>>   	auto &saturation = context.activeState.knobs.saturation;
>> -	frameContext.saturation = saturation;
>> -	if (saturation)
>> +	if (context.ccmEnabled && saturation) {
>>   		applySaturation(context.activeState.combinedMatrix, saturation.value());
>> -
>> -	if (saturation != lastSaturation_) {
>> -		context.activeState.matrixChanged = true;
>> -		lastSaturation_ = saturation;
>> +		frameContext.saturation = saturation;
>>   	}
>> +
>> +	params->gamma = 1.0 / context.activeState.knobs.gamma;
>> +	const float contrast = context.activeState.knobs.contrast.value_or(kDefaultContrast);
>> +	params->contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001));
>>   }
>>   
>>   void Adjust::process([[maybe_unused]] IPAContext &context,
>> diff --git a/src/ipa/simple/algorithms/adjust.h b/src/ipa/simple/algorithms/adjust.h
>> index 7644138ff..fb133b140 100644
>> --- a/src/ipa/simple/algorithms/adjust.h
>> +++ b/src/ipa/simple/algorithms/adjust.h
>> @@ -7,8 +7,6 @@
>>   
>>   #pragma once
>>   
>> -#include <optional>
>> -
>>   #include "libcamera/internal/matrix.h"
>>   
>>   #include <libipa/interpolator.h>
>> @@ -45,8 +43,6 @@ public:
>>   
>>   private:
>>   	void applySaturation(Matrix<float, 3, 3> &ccm, float saturation);
>> -
>> -	std::optional<float> lastSaturation_;
>>   };
>>   
>>   } /* namespace ipa::soft::algorithms */
>> diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
>> index 4d2f1df15..d2c1b87f0 100644
>> --- a/src/ipa/simple/algorithms/awb.cpp
>> +++ b/src/ipa/simple/algorithms/awb.cpp
>> @@ -37,7 +37,7 @@ int Awb::configure(IPAContext &context,
>>   void Awb::prepare(IPAContext &context,
>>   		  [[maybe_unused]] const uint32_t frame,
>>   		  IPAFrameContext &frameContext,
>> -		  [[maybe_unused]] DebayerParams *params)
>> +		  DebayerParams *params)
>>   {
>>   	auto &gains = context.activeState.awb.gains;
>>   	Matrix<float, 3, 3> gainMatrix = { { gains.r(), 0, 0,
>> @@ -45,9 +45,11 @@ void Awb::prepare(IPAContext &context,
>>   					     0, 0, gains.b() } };
>>   	context.activeState.combinedMatrix =
>>   		context.activeState.combinedMatrix * gainMatrix;
>> -	/* Just report, the gains are applied in LUT algorithm. */
>> +
>>   	frameContext.gains.red = gains.r();
>>   	frameContext.gains.blue = gains.b();
>> +
>> +	params->gains = gains;
>>   }
>>   
>>   void Awb::process(IPAContext &context,
>> diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp
>> index 5576a301f..911a5af2c 100644
>> --- a/src/ipa/simple/algorithms/ccm.cpp
>> +++ b/src/ipa/simple/algorithms/ccm.cpp
>> @@ -51,7 +51,6 @@ void Ccm::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
>>   	    utils::abs_diff(ct, lastCt_) >= kTemperatureThreshold) {
>>   		currentCcm_ = ccm_.getInterpolated(ct);
>>   		lastCt_ = ct;
>> -		context.activeState.matrixChanged = true;
>>   	}
>>   
>>   	context.activeState.combinedMatrix =
>> diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp
>> deleted file mode 100644
>> index fd442259a..000000000
>> --- a/src/ipa/simple/algorithms/lut.cpp
>> +++ /dev/null
>> @@ -1,140 +0,0 @@
>> -/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> -/*
>> - * Copyright (C) 2024-2026, Red Hat Inc.
>> - *
>> - * Color lookup tables construction
>> - */
>> -
>> -#include "lut.h"
>> -
>> -#include <algorithm>
>> -#include <cmath>
>> -#include <optional>
>> -#include <stdint.h>
>> -
>> -#include <libcamera/base/log.h>
>> -
>> -#include <libcamera/control_ids.h>
>> -
>> -#include "simple/ipa_context.h"
>> -
>> -#include "adjust.h"
>> -
>> -namespace libcamera {
>> -
>> -LOG_DEFINE_CATEGORY(IPASoftLut)
>> -
>> -namespace ipa::soft::algorithms {
>> -
>> -int Lut::configure(IPAContext &context,
>> -		   [[maybe_unused]] const IPAConfigInfo &configInfo)
>> -{
>> -	updateGammaTable(context);
>> -
>> -	return 0;
>> -}
>> -
>> -void Lut::updateGammaTable(IPAContext &context)
>> -{
>> -	const auto blackLevel = context.activeState.blc.level;
>> -	const auto gamma = 1.0 / context.activeState.knobs.gamma;
>> -	const auto contrast = context.activeState.knobs.contrast.value_or(1.0);
>> -	/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */
>> -	float contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001));
>> -
>> -	if (!context.gpuIspEnabled) {
>> -		auto &gammaTable = context.activeState.gamma.gammaTable;
>> -		const unsigned int blackIndex = blackLevel * gammaTable.size() / 256;
>> -		const float divisor = gammaTable.size() - blackIndex - 1.0;
>> -		for (unsigned int i = blackIndex; i < gammaTable.size(); i++) {
>> -			double normalized = (i - blackIndex) / divisor;
>> -			/* Apply simple S-curve */
>> -			if (normalized < 0.5)
>> -				normalized = 0.5 * std::pow(normalized / 0.5, contrastExp);
>> -			else
>> -				normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);
>> -			gammaTable[i] = UINT8_MAX * std::pow(normalized, gamma);
>> -		}
>> -		/*
>> -		 * Due to CCM operations, the table lookup may reach indices below the black
>> -		 * level. Let's set the table values below black level to the minimum
>> -		 * non-black value to prevent problems when the minimum value is
>> -		 * significantly non-zero (for example, when the image should be all grey).
>> -		 */
>> -		std::fill(gammaTable.begin(), gammaTable.begin() + blackIndex,
>> -			  gammaTable[blackIndex]);
>> -	}
>> -
>> -	context.activeState.gamma.gamma = gamma;
>> -	context.activeState.gamma.blackLevel = blackLevel;
>> -	context.activeState.gamma.contrastExp = contrastExp;
>> -}
>> -
>> -int16_t Lut::matrixValue(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,
>> -		  DebayerParams *params)
>> -{
>> -	/*
>> -	 * Update the gamma table if needed. This means if black level changes
>> -	 * and since the black level gets updated only if a lower value is
>> -	 * observed, it's not permanently prone to minor fluctuations or
>> -	 * rounding errors.
>> -	 */
>> -	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();
>> -	const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /
>> -			   gammaTableSize;
>> -
>> -	if (!context.ccmEnabled) {
>> -		for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
>> -			/* Apply gamma after gain! */
>> -			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())];
>> -		}
>> -	} else if (context.activeState.matrixChanged || gammaUpdateNeeded) {
>> -		auto &matrix = context.activeState.combinedMatrix;
>> -		auto &red = params->redCcm;
>> -		auto &green = params->greenCcm;
>> -		auto &blue = params->blueCcm;
>> -		params->ccm = matrix;
>> -		if (!context.gpuIspEnabled) {
>> -			for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
>> -				red[i].r = matrixValue(i, matrix[0][0]);
>> -				red[i].g = matrixValue(i, matrix[1][0]);
>> -				red[i].b = matrixValue(i, matrix[2][0]);
>> -				green[i].r = matrixValue(i, matrix[0][1]);
>> -				green[i].g = matrixValue(i, matrix[1][1]);
>> -				green[i].b = matrixValue(i, matrix[2][1]);
>> -				blue[i].r = matrixValue(i, matrix[0][2]);
>> -				blue[i].g = matrixValue(i, matrix[1][2]);
>> -				blue[i].b = matrixValue(i, matrix[2][2]);
>> -				params->gammaLut[i] = gammaTable[i / div];
>> -			}
>> -		}
>> -		context.activeState.matrixChanged = false;
>> -	}
>> -
>> -	params->gamma = context.activeState.gamma.gamma;
>> -	params->contrastExp = context.activeState.gamma.contrastExp;
>> -}
>> -
>> -REGISTER_IPA_ALGORITHM(Lut, "Lut") - -} /* namespace ipa::soft::algorithms */ - -} /* namespace libcamera */ diff --git a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h deleted file mode 100644 index ad16d1e8e..000000000 --- a/src/ipa/simple/algorithms/lut.h +++ /dev/null @@ -1,35 +0,0 @@ -/* SPDX-License-Identifier: LGPL-2.1-or-later */ -/* - * Copyright (C) 2024, Red Hat Inc. - * - * Color lookup tables construction - */ - -#pragma once - -#include "algorithm.h"
>> -
>> -namespace libcamera {
>> -
>> -namespace ipa::soft::algorithms {
>> -
>> -class Lut : public Algorithm
>> -{
>> -public:
>> -	Lut() = default;
>> -	~Lut() = default;
>> -
>> -	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
>> -	void prepare(IPAContext &context,
>> -		     const uint32_t frame,
>> -		     IPAFrameContext &frameContext,
>> -		     DebayerParams *params) override;
>> -
>> -private:
>> -	void updateGammaTable(IPAContext &context);
>> -	int16_t matrixValue(unsigned int i, float ccm) const;
>> -};
>> -
>> -} /* namespace ipa::soft::algorithms */
>> -
>> -} /* namespace libcamera */
>> diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
>> index ebe9f20dd..73c637220 100644
>> --- a/src/ipa/simple/algorithms/meson.build
>> +++ b/src/ipa/simple/algorithms/meson.build
>> @@ -6,5 +6,4 @@ soft_simple_ipa_algorithms = files([
>>       'agc.cpp',
>>       'blc.cpp',
>>       'ccm.cpp',
>> -    'lut.cpp',
>>   ])
>> diff --git a/src/ipa/simple/data/uncalibrated.yaml b/src/ipa/simple/data/uncalibrated.yaml
>> index e389e0588..c6feda36d 100644
>> --- a/src/ipa/simple/data/uncalibrated.yaml
>> +++ b/src/ipa/simple/data/uncalibrated.yaml
>> @@ -15,6 +15,5 @@ algorithms:
>>                    0, 1, 0,
>>                    0, 0, 1]
>>     - Adjust:
>> -  - Lut:
>>     - Agc:
>>   ...
>> diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
>> index 293e35b71..34f7403a4 100644
>> --- a/src/ipa/simple/ipa_context.h
>> +++ b/src/ipa/simple/ipa_context.h
>> @@ -53,17 +53,7 @@ struct IPAActiveState {
>>   		unsigned int temperatureK;
>>   	} awb;
>>   
>> -	static constexpr unsigned int kGammaLookupSize = 1024;
>> -	struct {
>> -		std::array<double, kGammaLookupSize> gammaTable;
>> -		uint8_t blackLevel;
>> -		float gamma;
>> -		float contrast;
>> -		float contrastExp;
>> -	} gamma;
>> -
>>   	Matrix<float, 3, 3> combinedMatrix;
>> -	bool matrixChanged = false;
>>   
>>   	struct {
>>   		float gamma;
>> @@ -103,7 +93,6 @@ struct IPAContext {
>>   	FCQueue<IPAFrameContext> frameContexts;
>>   	ControlInfoMap::Map ctrlMap;
>>   	bool ccmEnabled = false;
>> -	bool gpuIspEnabled = false;
>>   };
>>   
>>   } /* namespace ipa::soft */
>> diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
>> index 732e82510..6bef597c8 100644
>> --- a/src/ipa/simple/soft_simple.cpp
>> +++ b/src/ipa/simple/soft_simple.cpp
>> @@ -26,6 +26,7 @@
>>   #include "libcamera/internal/software_isp/swisp_stats.h"
>>   #include "libcamera/internal/yaml_parser.h"
>>   
>> +#include "algorithms/adjust.h"
>>   #include "libipa/camera_sensor_helper.h"
>>   
>>   #include "module.h"
>> @@ -55,7 +56,6 @@ public:
>>   		 const SharedFD &fdParams,
>>   		 const IPACameraSensorInfo &sensorInfo,
>>   		 const ControlInfoMap &sensorControls,
>> -		 bool gpuIspEnabled,
>>   		 ControlInfoMap *ipaControls,
>>   		 bool *ccmEnabled) override;
>>   	int configure(const IPAConfigInfo &configInfo) override;
>> @@ -96,7 +96,6 @@ int IPASoftSimple::init(const IPASettings &settings,
>>   			const SharedFD &fdParams,
>>   			const IPACameraSensorInfo &sensorInfo,
>>   			const ControlInfoMap &sensorControls,
>> -			bool gpuIspEnabled,
>>   			ControlInfoMap *ipaControls,
>>   			bool *ccmEnabled)
>>   {
>> @@ -108,7 +107,6 @@ int IPASoftSimple::init(const IPASettings &settings,
>>   	}
>>   
>>   	context_.sensorInfo = sensorInfo;
>> -	context_.gpuIspEnabled = gpuIspEnabled;
>>   
>>   	/* Load the tuning data file */
>>   	File file(settings.configurationFile);
>> @@ -161,6 +159,11 @@ int IPASoftSimple::init(const IPASettings &settings,
>>   		}
>>   
>>   		params_ = static_cast<DebayerParams *>(mem);
>> +		params_->blackLevel = { { 0.0, 0.0, 0.0 } };
>> +		params_->gamma = 1.0 / algorithms::kDefaultGamma;
>> +		params_->contrastExp = 1.0;
>> +		params_->gains = { { 1.0, 1.0, 1.0 } };
>> +		/* combinedMatrix is reset for each frame. */
>>   	}
>>   
>>   	{
>> @@ -287,6 +290,8 @@ void IPASoftSimple::computeParams(const uint32_t frame)
>>   	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
>>   	for (auto const &algo : algorithms())
>>   		algo->prepare(context_, frame, frameContext, params_);
>> +	params_->combinedMatrix = context_.activeState.combinedMatrix;
>> +
>>   	setIspParams.emit();
>>   }
>>   
>> diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
>> index 65a1762dd..dccdd86b4 100644
>> --- a/src/libcamera/software_isp/debayer.cpp
>> +++ b/src/libcamera/software_isp/debayer.cpp
>> @@ -1,7 +1,7 @@
>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>   /*
>>    * Copyright (C) 2023, Linaro Ltd
>> - * Copyright (C) 2023-2025 Red Hat Inc.
>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>    *
>>    * Authors:
>>    * Hans de Goede<hdegoede@redhat.com>
>> @@ -25,99 +25,28 @@ namespace libcamera {
>>    */
>>   
>>   /**
>> - * \var DebayerParams::kRGBLookupSize
>> - * \brief Size of a color lookup table
>> + * \var DebayerParams::gains
>> + * \brief Colour channel gains
>>    */
>>   
>>   /**
>> - * \struct DebayerParams::CcmColumn
>> - * \brief Type of a single column of a color correction matrix (CCM)
>> - *
>> - * When multiplying an input pixel, columns in the CCM correspond to the red,
>> - * green or blue component of input pixel values, while rows correspond to the
>> - * red, green or blue components of the output pixel values. The members of the
>> - * CcmColumn structure are named after the colour components of the output pixel
>> - * values they correspond to.
>> - */
>> -
>> -/**
>> - * \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::LookupTable
>> - * \brief Type of the lookup tables for single lookup values
>> - */
>> -
>> -/**
>> - * \typedef DebayerParams::CcmLookupTable
>> - * \brief Type of the CCM lookup tables for red, green, blue values
>> - */
>> -
>> -/**
>> - * \var DebayerParams::red
>> - * \brief Lookup table for red color, mapping input values to output values
>> - */
>> -
>> -/**
>> - * \var DebayerParams::green
>> - * \brief Lookup table for green color, mapping input values to output values
>> - */
>> -
>> -/**
>> - * \var DebayerParams::blue
>> - * \brief Lookup table for blue color, mapping input values to output values
>> - */
>> -
>> -/**
>> - * \var DebayerParams::redCcm
>> - * \brief Lookup table for the CCM red column, mapping input values to output values
>> - */
>> -
>> -/**
>> - * \var DebayerParams::greenCcm
>> - * \brief Lookup table for the CCM green column, mapping input values to output values
>> - */
>> -
>> -/**
>> - * \var DebayerParams::blueCcm
>> - * \brief Lookup table for the CCM blue column, mapping input values to output values
>> - */
>> -
>> -/**
>> - * \var DebayerParams::gammaLut
>> - * \brief Gamma lookup table used with color correction matrix
>> - */
>> -
>> -/**
>> - * \var DebayerParams::ccm
>> - * \brief Per frame colour correction matrix for GPUISP
>> + * \var DebayerParams::combinedMatrix
>> + * \brief Colour correction matrix, including other adjustments
>>    */
>>   
>>   /**
>>    * \var DebayerParams::blackLevel
>> - * \brief Blacklevel gains for the GPUISP
>> + * \brief Black level values
>>    */
>>   
>>   /**
>>    * \var DebayerParams::gamma
>> - * \brief Gamma value for the GPUISP
>> + * \brief Gamma value, e.g. 1/2.2
>>    */
>>   
>>   /**
>>    * \var DebayerParams::contrastExp
>> - * \brief Contrast value for GPUISP
>> + * \brief Contrast value to be used as an exponent
>>    */
>>   
>>   /**
>> @@ -131,13 +60,6 @@ LOG_DEFINE_CATEGORY(Debayer)
>>   
>>   Debayer::Debayer(const GlobalConfiguration &configuration) : bench_(configuration)
>>   {
>> -	/* Initialize color lookup tables */
>> -	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
>> -		red_[i] = green_[i] = blue_[i] = i;
>> -		redCcm_[i] = { static_cast<int16_t>(i), 0, 0 };
>> -		greenCcm_[i] = { 0, static_cast<int16_t>(i), 0 };
>> -		blueCcm_[i] = { 0, 0, static_cast<int16_t>(i) };
>> -	}
>>   }
>>   
>>   Debayer::~Debayer()
>> @@ -305,56 +227,6 @@ Debayer::~Debayer()
>>    * \brief Output size object
>>    */
>>   
>> -/**
>> - * \var Debayer::red_
>> - * \brief Lookup table for red channel gain and correction values
>> - *
>> - * This table provides precomputed per-pixel or per-intensity
>> - * correction values for the red color channel used during debayering.
>> - */
>> -
>> -/**
>> - * \var Debayer::green_
>> - * \brief Lookup table for green channel gain and correction values
>> - *
>> - * This table provides precomputed per-pixel or per-intensity
>> - * correction values for the green color channel used during debayering.
>> - */
>> -
>> -/**
>> - * \var Debayer::blue_
>> - * \brief Lookup table for blue channel gain and correction values
>> - *
>> - * This table provides precomputed per-pixel or per-intensity
>> - * correction values for the blue color channel used during debayering.
>> - */
>> -
>> -/**
>> - * \var Debayer::redCcm_
>> - * \brief Red channel Color Correction Matrix (CCM) lookup table
>> - *
>> - * Contains coefficients for green channel color correction.
>> - */
>> -
>> -/**
>> - * \var Debayer::greenCcm_
>> - * \brief Green channel Color Correction Matrix (CCM) lookup table
>> - *
>> - * Contains coefficients for green channel color correction.
>> - */
>> -
>> -/**
>> - * \var Debayer::blueCcm_
>> - * \brief Blue channel Color Correction Matrix (CCM) lookup table
>> - *
>> - * Contains coefficients for blue channel color correction.
>> - */
>> -
>> -/**
>> - * \var Debayer::gammaLut_
>> - * \brief Gamma correction lookup table
>> - */
>> -
>>   /**
>>    * \var Debayer::swapRedBlueGains_
>>    * \brief Flag indicating whether red and blue channel gains should be swapped
>> @@ -396,34 +268,6 @@ Debayer::~Debayer()
>>    * DebayerEGL::start.
>>    */
>>   
>> -/**
>> - * \fn void Debayer::setParams(DebayerParams &params)
>> - * \brief Select the bayer params to use for the next frame debayer
>> - * \param[in] params The parameters to be used in debayering
>> - */
>> -void Debayer::setParams(DebayerParams &params)
>> -{
>> -	green_ = params.green;
>> -	greenCcm_ = params.greenCcm;
>> -	if (swapRedBlueGains_) {
>> -		red_ = params.blue;
>> -		blue_ = params.red;
>> -		redCcm_ = params.blueCcm;
>> -		blueCcm_ = params.redCcm;
>> -		for (unsigned int i = 0; i < 256; i++) {
>> -			std::swap(redCcm_[i].r, redCcm_[i].b);
>> -			std::swap(greenCcm_[i].r, greenCcm_[i].b);
>> -			std::swap(blueCcm_[i].r, blueCcm_[i].b);
>> -		}
>> -	} else {
>> -		red_ = params.red;
>> -		blue_ = params.blue;
>> -		redCcm_ = params.redCcm;
>> -		blueCcm_ = params.blueCcm;
>> -	}
>> -	gammaLut_ = params.gammaLut;
>> -}
>> -
>>   /**
>>    * \fn void Debayer::dmaSyncBegin(DebayerParams &params)
>>    * \brief Common CPU/GPU Dma Sync Buffer begin
>> diff --git a/src/libcamera/software_isp/debayer.h b/src/libcamera/software_isp/debayer.h
>> index cd2db9930..652cff4cc 100644
>> --- a/src/libcamera/software_isp/debayer.h
>> +++ b/src/libcamera/software_isp/debayer.h
>> @@ -78,13 +78,6 @@ public:
>>   	Size outputSize_;
>>   	PixelFormat inputPixelFormat_;
>>   	PixelFormat outputPixelFormat_;
>> -	DebayerParams::LookupTable red_;
>> -	DebayerParams::LookupTable green_;
>> -	DebayerParams::LookupTable blue_;
>> -	DebayerParams::CcmLookupTable redCcm_;
>> -	DebayerParams::CcmLookupTable greenCcm_;
>> -	DebayerParams::CcmLookupTable blueCcm_;
>> -	DebayerParams::LookupTable gammaLut_;
>>   	bool swapRedBlueGains_;
>>   	Benchmark bench_;
>>   
>> @@ -92,7 +85,6 @@ private:
>>   	virtual Size patternSize(PixelFormat inputFormat) = 0;
>>   
>>   protected:
>> -	void setParams(DebayerParams &params);
>>   	void dmaSyncBegin(std::vector<DmaSyncer> &dmaSyncers, FrameBuffer *input, FrameBuffer *output);
>>   	static bool isStandardBayerOrder(BayerFormat::Order order);
>>   };
>> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
>> index 00738c56b..af7af0a8d 100644
>> --- a/src/libcamera/software_isp/debayer_cpu.cpp
>> +++ b/src/libcamera/software_isp/debayer_cpu.cpp
>> @@ -1,7 +1,7 @@
>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>   /*
>>    * Copyright (C) 2023, Linaro Ltd
>> - * Copyright (C) 2023-2025 Red Hat Inc.
>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>    *
>>    * Authors:
>>    * Hans de Goede<hdegoede@redhat.com>
>> @@ -68,21 +68,21 @@ DebayerCpu::~DebayerCpu() = default;
>>   #define GAMMA(value) \
>>   	*dst++ = gammaLut_[std::clamp(value, 0, static_cast<int>(gammaLut_.size()) - 1)]
>>   
>> -#define STORE_PIXEL(b_, g_, r_)                                        \
>> -	if constexpr (ccmEnabled) {                                    \
>> -		const DebayerParams::CcmColumn &blue = blueCcm_[b_];   \
>> -		const DebayerParams::CcmColumn &green = greenCcm_[g_]; \
>> -		const DebayerParams::CcmColumn &red = redCcm_[r_];     \
>> -		GAMMA(blue.b + green.b + red.b);                       \
>> -		GAMMA(blue.g + green.g + red.g);                       \
>> -		GAMMA(blue.r + green.r + red.r);                       \
>> -	} else {                                                       \
>> -		*dst++ = blue_[b_];                                    \
>> -		*dst++ = green_[g_];                                   \
>> -		*dst++ = red_[r_];                                     \
>> -	}                                                              \
>> -	if constexpr (addAlphaByte)                                    \
>> -		*dst++ = 255;                                          \
>> +#define STORE_PIXEL(b_, g_, r_)                         \
>> +	if constexpr (ccmEnabled) {                     \
>> +		const CcmColumn &blue = blueCcm_[b_];   \
>> +		const CcmColumn &green = greenCcm_[g_]; \
>> +		const CcmColumn &red = redCcm_[r_];     \
>> +		GAMMA(blue.b + green.b + red.b);        \
>> +		GAMMA(blue.g + green.g + red.g);        \
>> +		GAMMA(blue.r + green.r + red.r);        \
>> +	} else {                                        \
>> +		*dst++ = blue_[b_];                     \
>> +		*dst++ = green_[g_];                    \
>> +		*dst++ = red_[r_];                      \
>> +	}                                               \
>> +	if constexpr (addAlphaByte)                     \
>> +		*dst++ = 255;                           \
>>   	x++;
>>   
>>   /*
>> @@ -525,6 +525,16 @@ int DebayerCpu::configure(const StreamConfiguration &inputCfg,
>>   	if (ret != 0)
>>   		return -EINVAL;
>>   
>> +	ccmEnabled_ = ccmEnabled;
>> +
>> +	/*
>> +	 * Lookup tables must be initialized because the initial value is used for
>> +	 * the first two frames, i.e. until stats processing starts providing its
>> +	 * own parameters. Let's enforce recomputing lookup tables by setting the
>> +	 * stored last used gamma to an out-of-range value.
>> +	 */
>> +	params_.gamma = 1.0;
>> +
>>   	window_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) &
>>   		    ~(inputConfig_.patternSize.width - 1);
>>   	window_.y = ((inputCfg.size.height - outputCfg.size.height) / 2) &
>> @@ -740,6 +750,98 @@ void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst)
>>   	}
>>   }
>>   
>> +void DebayerCpu::updateGammaTable(DebayerParams &params)
>> +{
>> +	const RGB<float> blackLevel = params.blackLevel;
>> +	/* Take let's say the green channel black level */
>> +	const unsigned int blackIndex = blackLevel[1] * gammaTable_.size();
>> +	const float gamma = params.gamma;
>> +	const float contrastExp = params.contrastExp;
>> +
>> +	const float divisor = gammaTable_.size() - blackIndex - 1.0;
>> +	for (unsigned int i = blackIndex; i < gammaTable_.size(); i++) {
>> +		float normalized = (i - blackIndex) / divisor;
>> +		/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */
>> +		/* Apply simple S-curve */
>> +		if (normalized < 0.5)
>> +			normalized = 0.5 * std::pow(normalized / 0.5, contrastExp);
>> +		else
>> +			normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);
>> +		gammaTable_[i] = UINT8_MAX *
>> +				 std::pow(normalized, gamma);
>> +	}
>> +	/*
>> +	 * Due to CCM operations, the table lookup may reach indices below the black
>> +	 * level. Let's set the table values below black level to the minimum
>> +	 * non-black value to prevent problems when the minimum value is
>> +	 * significantly non-zero (for example, when the image should be all grey).
>> +	 */
>> +	std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex,
>> +		  gammaTable_[blackIndex]);
>> +}
>> +
>> +void DebayerCpu::updateLookupTables(DebayerParams &params)
>> +{
>> +	const bool gammaUpdateNeeded =
>> +		params.gamma != params_.gamma ||
>> +		params.blackLevel != params_.blackLevel ||
>> +		params.contrastExp != params_.contrastExp;
>> +	if (gammaUpdateNeeded)
>> +		updateGammaTable(params);
>> +
>> +	auto matrixChanged = [](const Matrix<float, 3, 3> &m1, const Matrix<float, 3, 3> &m2) -> bool {
>> +		return !std::equal(m1.data().begin(), m1.data().end(), m2.data().begin());
>> +	};
>> +	const unsigned int gammaTableSize = gammaTable_.size();
>> +	const double div = static_cast<double>(kRGBLookupSize) / gammaTableSize;
>> +	if (ccmEnabled_) {
>> +		if (gammaUpdateNeeded ||
>> +		    matrixChanged(params.combinedMatrix, params_.combinedMatrix)) {
>> +			auto &red = swapRedBlueGains_ ? blueCcm_ : redCcm_;
>> +			auto &green = greenCcm_;
>> +			auto &blue = swapRedBlueGains_ ? redCcm_ : blueCcm_;
>> +			const unsigned int redIndex = swapRedBlueGains_ ? 2 : 0;
>> +			const unsigned int greenIndex = 1;
>> +			const unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2;
>> +			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
>> +				red[i].r = std::round(i * params.combinedMatrix[redIndex][0]);
>> +				red[i].g = std::round(i * params.combinedMatrix[greenIndex][0]);
>> +				red[i].b = std::round(i * params.combinedMatrix[blueIndex][0]);
>> +				green[i].r = std::round(i * params.combinedMatrix[redIndex][1]);
>> +				green[i].g = std::round(i * params.combinedMatrix[greenIndex][1]);
>> +				green[i].b = std::round(i * params.combinedMatrix[blueIndex][1]);
>> +				blue[i].r = std::round(i * params.combinedMatrix[redIndex][2]);
>> +				blue[i].g = std::round(i * params.combinedMatrix[greenIndex][2]);
>> +				blue[i].b = std::round(i * params.combinedMatrix[blueIndex][2]);
>> +				gammaLut_[i] = gammaTable_[i / div];
>> +			}
>> +		}
>> +	} else {
>> +		if (gammaUpdateNeeded || params.gains != params_.gains) {
>> +			auto &gains = params.gains;
>> +			auto &red = swapRedBlueGains_ ? blue_ : red_;
>> +			auto &green = green_;
>> +			auto &blue = swapRedBlueGains_ ? red_ : blue_;
>> +			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
>> +				/* Apply gamma after gain! */
>> +				const RGB<float> lutGains = (gains * i / div).min(gammaTableSize - 1);
>> +				red[i] = gammaTable_[static_cast<unsigned int>(lutGains.r())];
>> +				green[i] = gammaTable_[static_cast<unsigned int>(lutGains.g())];
>> +				blue[i] = gammaTable_[static_cast<unsigned int>(lutGains.b())];
>> +			}
>> +		}
>> +	}
>> +
>> +	LOG(Debayer, Debug)
>> +		<< "Debayer parameters: blackLevel=" << params.blackLevel
>> +		<< "; gamma=" << params.gamma
>> +		<< "; contrastExp=" << params.contrastExp
>> +		<< "; gains=" << params.gains
>> +		<< "; matrix=" << params.combinedMatrix;
>> +
>> +	params_ = params;
>> +}
>> +
>>   void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output, DebayerParams params)
>>   {
>>   	bench_.startFrame();
>> @@ -748,7 +850,7 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>>   
>>   	dmaSyncBegin(dmaSyncers, input, output);
>>   
>> -	setParams(params);
>> +	updateLookupTables(params);
>>   
>>   	/* 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 67df2b93a..b5cbb5bd2 100644
>> --- a/src/libcamera/software_isp/debayer_cpu.h
>> +++ b/src/libcamera/software_isp/debayer_cpu.h
>> @@ -1,7 +1,7 @@
>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>   /*
>>    * Copyright (C) 2023, Linaro Ltd
>> - * Copyright (C) 2023-2025 Red Hat Inc.
>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>    *
>>    * Authors:
>>    * Hans de Goede<hdegoede@redhat.com>
>> @@ -18,6 +18,8 @@
>>   #include <libcamera/base/object.h>
>>   
>>   #include "libcamera/internal/bayer_format.h"
>> +#include "libcamera/internal/global_configuration.h"
>> +#include "libcamera/internal/software_isp/debayer_params.h"
>>   #include "libcamera/internal/software_isp/swstats_cpu.h"
>>   
>>   #include "debayer.h"
>> @@ -108,10 +110,32 @@ private:
>>   	void memcpyNextLine(const uint8_t *linePointers[]);
>>   	void process2(uint32_t frame, const uint8_t *src, uint8_t *dst);
>>   	void process4(uint32_t frame, const uint8_t *src, uint8_t *dst);
>> +	void updateGammaTable(DebayerParams &params);
>> +	void updateLookupTables(DebayerParams &params);
>>   
>>   	/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
>>   	static constexpr unsigned int kMaxLineBuffers = 5;
>>   
>> +	static constexpr unsigned int kRGBLookupSize = 256;
>> +	static constexpr unsigned int kGammaLookupSize = 1024;
>> +	struct CcmColumn {
>> +		int16_t r;
>> +		int16_t g;
>> +		int16_t b;
>> +	};
>> +	using LookupTable = std::array<uint8_t, kRGBLookupSize>;
>> +	using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
>> +	LookupTable red_;
>> +	LookupTable green_;
>> +	LookupTable blue_;
>> +	CcmLookupTable redCcm_;
>> +	CcmLookupTable greenCcm_;
>> +	CcmLookupTable blueCcm_;
>> +	std::array<double, kGammaLookupSize> gammaTable_;
>> +	LookupTable gammaLut_;
>> +	bool ccmEnabled_;
>> +	DebayerParams params_;
>> +
>>   	debayerFn debayer0_;
>>   	debayerFn debayer1_;
>>   	debayerFn debayer2_;
>> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
>> index 9693d7252..af04d60ca 100644
>> --- a/src/libcamera/software_isp/debayer_egl.cpp
>> +++ b/src/libcamera/software_isp/debayer_egl.cpp
>> @@ -475,18 +475,18 @@ void DebayerEGL::setShaderVariableValues(DebayerParams &params)
>>   			    << " textureUniformProjMatrix_ " << textureUniformProjMatrix_;
>>   
>>   	GLfloat ccm[9] = {
>> -		params.ccm[0][0],
>> -		params.ccm[0][1],
>> -		params.ccm[0][2],
>> -		params.ccm[1][0],
>> -		params.ccm[1][1],
>> -		params.ccm[1][2],
>> -		params.ccm[2][0],
>> -		params.ccm[2][1],
>> -		params.ccm[2][2],
>> +		params.combinedMatrix[0][0],
>> +		params.combinedMatrix[0][1],
>> +		params.combinedMatrix[0][2],
>> +		params.combinedMatrix[1][0],
>> +		params.combinedMatrix[1][1],
>> +		params.combinedMatrix[1][2],
>> +		params.combinedMatrix[2][0],
>> +		params.combinedMatrix[2][1],
>> +		params.combinedMatrix[2][2],
>>   	};
>>   	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_FALSE, ccm);
>> -	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.ccm;
>> +	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
>>   
>>   	/*
>>   	 * 0 = Red, 1 = Green, 2 = Blue
>> @@ -544,8 +544,6 @@ void DebayerEGL::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>>   
>>   	dmaSyncBegin(dmaSyncers, input, nullptr);
>>   
>> -	setParams(params);
>> -
>>   	/* Copy metadata from the input buffer */
>>   	FrameMetadata &metadata = output->_d()->metadata();
>>   	metadata.status = input->metadata().status;
>> diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
>> index 7ad3511db..a83986b78 100644
>> --- a/src/libcamera/software_isp/software_isp.cpp
>> +++ b/src/libcamera/software_isp/software_isp.cpp
>> @@ -84,23 +84,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>>   		   DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap |
>>   		   DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
>>   {
>> -	/*
>> -	 * debayerParams_ must be initialized because the initial value is used for
>> -	 * the first two frames, i.e. until stats processing starts providing its
>> -	 * own parameters.
>> -	 *
>> -	 * \todo This should be handled in the same place as the related
>> -	 * operations, in the IPA module.
>> -	 */
>> -	std::array<uint8_t, 256> gammaTable;
>> -	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];
>> -	}
>> -
>>   	if (!dmaHeap_.isValid()) {
>>   		LOG(SoftwareIsp, Error) << "Failed to create DmaBufAllocator object";
>>   		return;
>> @@ -121,8 +104,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>>   	}
>>   	stats->statsReady.connect(this, &SoftwareIsp::statsReady);
>>   
>> -	bool gpuIspEnabled;
>> -
>>   #if HAVE_DEBAYER_EGL
>>   	std::optional<std::string> softISPMode = configuration.envOption("LIBCAMERA_SOFTISP_MODE", { "software_isp", "mode" });
>>   	if (softISPMode) {
>> @@ -133,15 +114,12 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>>   		}
>>   	}
>>   
>> -	if (!softISPMode || softISPMode == "gpu") {
>> +	if (!softISPMode || softISPMode == "gpu")
>>   		debayer_ = std::make_unique<DebayerEGL>(std::move(stats), configuration);
>> -		gpuIspEnabled = true;
>> -	}
>> +
>>   #endif
>> -	if (!debayer_) {
>> +	if (!debayer_)
>>   		debayer_ = std::make_unique<DebayerCpu>(std::move(stats), configuration);
>> -		gpuIspEnabled = false;
>> -	}
>>   
>>   	debayer_->inputBufferReady.connect(this, &SoftwareIsp::inputReady);
>>   	debayer_->outputBufferReady.connect(this, &SoftwareIsp::outputReady);
>> @@ -173,7 +151,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
>>   			 sharedParams_.fd(),
>>   			 sensorInfo,
>>   			 sensor->controls(),
>> -			 gpuIspEnabled,
>>   			 ipaControls,
>>   			 &ccmEnabled_);
>>   	if (ret) {
> 
> -- 
> Robert Mader
> Consultant Software Developer
> 
> Collabora Ltd.
> Platinum Building, St John's Innovation Park, Cambridge CB4 0DS, UK
> Registered in England & Wales, no. 5513718
>
Robert Mader Jan. 22, 2026, 5:04 p.m. UTC | #3
On 22.01.26 18:01, Barnabás Pőcze wrote:
> 2026. 01. 22. 17:58 keltezéssel, Robert Mader írta:
>> Hi, thanks for the series!
>>
>> Trying this commit (on top of current master) on a FP5 (qcom) makes 
>> the soft-isp fail with
>>
>> ERROR IPAModuleAlgo module.h:91 IPASoft: Algorithm 'Lut' not found
>> ERROR SoftwareIsp software_isp.cpp:157 IPA init failed
>> WARN SimplePipeline simple.cpp:620 Failed to create software ISP, 
>> disabling software debayering
>>
>> for me. It's printed for each sensor/camera when running `cam -l`.
>
> Maybe old tuning files are loaded? This patch definitely removes it from
> uncalibrated.yaml but maybe you have sensor specific ones locally.
Uh, right, that was it! With that cleaned up it works as expected, thanks.

>
>>
>> On 22.01.26 17:19, Milan Zamazal wrote:
>>> The Lut algorithm is not really an algorithm.  Moreover, algorithms may
>>> be enabled or disabled but with Lut disabled, nothing will work.
>>>
>>> Let's move the construction of lookup tables to CPU debayering, 
>>> where it
>>> is used.  The implied and related changes are:
>>>
>>> - DebayerParams is changed to contain the real params rather than 
>>> lookup
>>>    tables.
>>> - contrastExp parameter introduced by GPU ISP is used for CPU ISP too.
>>> - The params must be initialised so that debayering gets meaningful
>>>    parameter values even when some algorithms are disabled.
>>> - combinedMatrix must be put to params everywhere where it is modified.
>>> - Matrix changes needn't be tracked in the algorithms any more.
>>> - CPU debayering must watch for changes of the corresponding parameters
>>>    to update the lookup tables when and only when needed.
>>> - Swapping red and blue is integrated into lookup table constructions.
>>> - gpuIspEnabled flags are removed as they are not needed any more.
>>>
>>> Signed-off-by: Milan Zamazal<mzamazal@redhat.com>
>>> ---
>>>   .../internal/software_isp/debayer_params.h    |  43 +----
>>>   include/libcamera/ipa/soft.mojom              |   3 +-
>>>   src/ipa/simple/algorithms/adjust.cpp          |  17 +-
>>>   src/ipa/simple/algorithms/adjust.h            |   4 -
>>>   src/ipa/simple/algorithms/awb.cpp             |   6 +-
>>>   src/ipa/simple/algorithms/ccm.cpp             |   1 -
>>>   src/ipa/simple/algorithms/lut.cpp             | 140 --------------
>>>   src/ipa/simple/algorithms/lut.h               |  35 ----
>>>   src/ipa/simple/algorithms/meson.build         |   1 -
>>>   src/ipa/simple/data/uncalibrated.yaml         |   1 -
>>>   src/ipa/simple/ipa_context.h                  |  11 --
>>>   src/ipa/simple/soft_simple.cpp                |  11 +-
>>>   src/libcamera/software_isp/debayer.cpp        | 172 
>>> +-----------------
>>>   src/libcamera/software_isp/debayer.h          |   8 -
>>>   src/libcamera/software_isp/debayer_cpu.cpp    | 136 ++++++++++++--
>>>   src/libcamera/software_isp/debayer_cpu.h      |  26 ++-
>>>   src/libcamera/software_isp/debayer_egl.cpp    |  22 +--
>>>   src/libcamera/software_isp/software_isp.cpp   |  29 +--
>>>   18 files changed, 188 insertions(+), 478 deletions(-)
>>>   delete mode 100644 src/ipa/simple/algorithms/lut.cpp
>>>   delete mode 100644 src/ipa/simple/algorithms/lut.h
>>>
>>> diff --git 
>>> a/include/libcamera/internal/software_isp/debayer_params.h 
>>> b/include/libcamera/internal/software_isp/debayer_params.h
>>> index 2d69bd295..1c0412d75 100644
>>> --- a/include/libcamera/internal/software_isp/debayer_params.h
>>> +++ b/include/libcamera/internal/software_isp/debayer_params.h
>>> @@ -1,6 +1,6 @@
>>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>>   /*
>>> - * Copyright (C) 2023-2025 Red Hat Inc.
>>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>>    *
>>>    * Authors:
>>>    * Hans de Goede<hdegoede@redhat.com>
>>> @@ -10,7 +10,6 @@
>>>     #pragma once
>>>   -#include <array>
>>>   #include <stdint.h>
>>>     #include "libcamera/internal/matrix.h"
>>> @@ -19,47 +18,11 @@
>>>   namespace libcamera {
>>>     struct DebayerParams {
>>> -    static constexpr unsigned int kRGBLookupSize = 256;
>>> -
>>> -    struct CcmColumn {
>>> -        int16_t r;
>>> -        int16_t g;
>>> -        int16_t b;
>>> -    };
>>> -
>>> -    using LookupTable = std::array<uint8_t, kRGBLookupSize>;
>>> -    using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
>>> -
>>> -    /*
>>> -     * Color lookup tables when CCM is not used.
>>> -     *
>>> -     * Each color of a debayered pixel is amended by the corresponding
>>> -     * value in the given table.
>>> -     */
>>> -    LookupTable red;
>>> -    LookupTable green;
>>> -    LookupTable blue;
>>> -
>>> -    /*
>>> -     * Color and gamma lookup tables when CCM is used.
>>> -     *
>>> -     * Each of the CcmLookupTable's corresponds to a CCM column; 
>>> together they
>>> -     * make a complete 3x3 CCM lookup table. The CCM is applied on 
>>> debayered
>>> -     * pixels and then the gamma lookup table is used to set the 
>>> resulting
>>> -     * values of all the three colors.
>>> -     */
>>> -    CcmLookupTable redCcm;
>>> -    CcmLookupTable greenCcm;
>>> -    CcmLookupTable blueCcm;
>>> -    LookupTable gammaLut;
>>> -
>>> -    /*
>>> -     * Per frame corrections as calculated by the IPA
>>> -     */
>>> -    Matrix<float, 3, 3> ccm;
>>> +    Matrix<float, 3, 3> combinedMatrix;
>>>       RGB<float> blackLevel;
>>>       float gamma;
>>>       float contrastExp;
>>> +    RGB<float> gains;
>>>   };
>>>     } /* namespace libcamera */
>>> diff --git a/include/libcamera/ipa/soft.mojom 
>>> b/include/libcamera/ipa/soft.mojom
>>> index aff8fcbd3..77328c5fd 100644
>>> --- a/include/libcamera/ipa/soft.mojom
>>> +++ b/include/libcamera/ipa/soft.mojom
>>> @@ -17,8 +17,7 @@ interface IPASoftInterface {
>>>            libcamera.SharedFD fdStats,
>>>            libcamera.SharedFD fdParams,
>>>            libcamera.IPACameraSensorInfo sensorInfo,
>>> -         libcamera.ControlInfoMap sensorControls,
>>> -         bool gpuIspEnabled)
>>> +         libcamera.ControlInfoMap sensorControls)
>>>           => (int32 ret, libcamera.ControlInfoMap ipaControls, bool 
>>> ccmEnabled);
>>>       start() => (int32 ret);
>>>       stop();
>>> diff --git a/src/ipa/simple/algorithms/adjust.cpp 
>>> b/src/ipa/simple/algorithms/adjust.cpp
>>> index acdd3f741..068e98404 100644
>>> --- a/src/ipa/simple/algorithms/adjust.cpp
>>> +++ b/src/ipa/simple/algorithms/adjust.cpp
>>> @@ -95,23 +95,20 @@ void Adjust::applySaturation(Matrix<float, 3, 3> 
>>> &matrix, float saturation)
>>>   void Adjust::prepare(IPAContext &context,
>>>                [[maybe_unused]] const uint32_t frame,
>>>                IPAFrameContext &frameContext,
>>> -             [[maybe_unused]] DebayerParams *params)
>>> +             DebayerParams *params)
>>>   {
>>>       frameContext.gamma = context.activeState.knobs.gamma;
>>>       frameContext.contrast = context.activeState.knobs.contrast;
>>>   -    if (!context.ccmEnabled)
>>> -        return;
>>> -
>>>       auto &saturation = context.activeState.knobs.saturation;
>>> -    frameContext.saturation = saturation;
>>> -    if (saturation)
>>> +    if (context.ccmEnabled && saturation) {
>>>           applySaturation(context.activeState.combinedMatrix, 
>>> saturation.value());
>>> -
>>> -    if (saturation != lastSaturation_) {
>>> -        context.activeState.matrixChanged = true;
>>> -        lastSaturation_ = saturation;
>>> +        frameContext.saturation = saturation;
>>>       }
>>> +
>>> +    params->gamma = 1.0 / context.activeState.knobs.gamma;
>>> +    const float contrast = 
>>> context.activeState.knobs.contrast.value_or(kDefaultContrast);
>>> +    params->contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, 
>>> M_PI_2 - 0.00001));
>>>   }
>>>     void Adjust::process([[maybe_unused]] IPAContext &context,
>>> diff --git a/src/ipa/simple/algorithms/adjust.h 
>>> b/src/ipa/simple/algorithms/adjust.h
>>> index 7644138ff..fb133b140 100644
>>> --- a/src/ipa/simple/algorithms/adjust.h
>>> +++ b/src/ipa/simple/algorithms/adjust.h
>>> @@ -7,8 +7,6 @@
>>>     #pragma once
>>>   -#include <optional>
>>> -
>>>   #include "libcamera/internal/matrix.h"
>>>     #include <libipa/interpolator.h>
>>> @@ -45,8 +43,6 @@ public:
>>>     private:
>>>       void applySaturation(Matrix<float, 3, 3> &ccm, float saturation);
>>> -
>>> -    std::optional<float> lastSaturation_;
>>>   };
>>>     } /* namespace ipa::soft::algorithms */
>>> diff --git a/src/ipa/simple/algorithms/awb.cpp 
>>> b/src/ipa/simple/algorithms/awb.cpp
>>> index 4d2f1df15..d2c1b87f0 100644
>>> --- a/src/ipa/simple/algorithms/awb.cpp
>>> +++ b/src/ipa/simple/algorithms/awb.cpp
>>> @@ -37,7 +37,7 @@ int Awb::configure(IPAContext &context,
>>>   void Awb::prepare(IPAContext &context,
>>>             [[maybe_unused]] const uint32_t frame,
>>>             IPAFrameContext &frameContext,
>>> -          [[maybe_unused]] DebayerParams *params)
>>> +          DebayerParams *params)
>>>   {
>>>       auto &gains = context.activeState.awb.gains;
>>>       Matrix<float, 3, 3> gainMatrix = { { gains.r(), 0, 0,
>>> @@ -45,9 +45,11 @@ void Awb::prepare(IPAContext &context,
>>>                            0, 0, gains.b() } };
>>>       context.activeState.combinedMatrix =
>>>           context.activeState.combinedMatrix * gainMatrix;
>>> -    /* Just report, the gains are applied in LUT algorithm. */
>>> +
>>>       frameContext.gains.red = gains.r();
>>>       frameContext.gains.blue = gains.b();
>>> +
>>> +    params->gains = gains;
>>>   }
>>>     void Awb::process(IPAContext &context,
>>> diff --git a/src/ipa/simple/algorithms/ccm.cpp 
>>> b/src/ipa/simple/algorithms/ccm.cpp
>>> index 5576a301f..911a5af2c 100644
>>> --- a/src/ipa/simple/algorithms/ccm.cpp
>>> +++ b/src/ipa/simple/algorithms/ccm.cpp
>>> @@ -51,7 +51,6 @@ void Ccm::prepare(IPAContext &context, 
>>> [[maybe_unused]] const uint32_t frame,
>>>           utils::abs_diff(ct, lastCt_) >= kTemperatureThreshold) {
>>>           currentCcm_ = ccm_.getInterpolated(ct);
>>>           lastCt_ = ct;
>>> -        context.activeState.matrixChanged = true;
>>>       }
>>>         context.activeState.combinedMatrix =
>>> diff --git a/src/ipa/simple/algorithms/lut.cpp 
>>> b/src/ipa/simple/algorithms/lut.cpp
>>> deleted file mode 100644
>>> index fd442259a..000000000
>>> --- a/src/ipa/simple/algorithms/lut.cpp
>>> +++ /dev/null
>>> @@ -1,140 +0,0 @@
>>> -/* SPDX-License-Identifier: LGPL-2.1-or-later */
>>> -/*
>>> - * Copyright (C) 2024-2026, Red Hat Inc.
>>> - *
>>> - * Color lookup tables construction
>>> - */
>>> -
>>> -#include "lut.h"
>>> -
>>> -#include <algorithm>
>>> -#include <cmath>
>>> -#include <optional>
>>> -#include <stdint.h>
>>> -
>>> -#include <libcamera/base/log.h>
>>> -
>>> -#include <libcamera/control_ids.h>
>>> -
>>> -#include "simple/ipa_context.h"
>>> -
>>> -#include "adjust.h"
>>> -
>>> -namespace libcamera {
>>> -
>>> -LOG_DEFINE_CATEGORY(IPASoftLut)
>>> -
>>> -namespace ipa::soft::algorithms {
>>> -
>>> -int Lut::configure(IPAContext &context,
>>> -           [[maybe_unused]] const IPAConfigInfo &configInfo)
>>> -{
>>> -    updateGammaTable(context);
>>> -
>>> -    return 0;
>>> -}
>>> -
>>> -void Lut::updateGammaTable(IPAContext &context)
>>> -{
>>> -    const auto blackLevel = context.activeState.blc.level;
>>> -    const auto gamma = 1.0 / context.activeState.knobs.gamma;
>>> -    const auto contrast = 
>>> context.activeState.knobs.contrast.value_or(1.0);
>>> -    /* Convert 0..2 to 0..infinity; avoid actual inifinity at 
>>> tan(pi/2) */
>>> -    float contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, 
>>> M_PI_2 - 0.00001));
>>> -
>>> -    if (!context.gpuIspEnabled) {
>>> -        auto &gammaTable = context.activeState.gamma.gammaTable;
>>> -        const unsigned int blackIndex = blackLevel * 
>>> gammaTable.size() / 256;
>>> -        const float divisor = gammaTable.size() - blackIndex - 1.0;
>>> -        for (unsigned int i = blackIndex; i < gammaTable.size(); 
>>> i++) {
>>> -            double normalized = (i - blackIndex) / divisor;
>>> -            /* Apply simple S-curve */
>>> -            if (normalized < 0.5)
>>> -                normalized = 0.5 * std::pow(normalized / 0.5, 
>>> contrastExp);
>>> -            else
>>> -                normalized = 1.0 - 0.5 * std::pow((1.0 - 
>>> normalized) / 0.5, contrastExp);
>>> -            gammaTable[i] = UINT8_MAX * std::pow(normalized, gamma);
>>> -        }
>>> -        /*
>>> -         * Due to CCM operations, the table lookup may reach 
>>> indices below the black
>>> -         * level. Let's set the table values below black level to 
>>> the minimum
>>> -         * non-black value to prevent problems when the minimum 
>>> value is
>>> -         * significantly non-zero (for example, when the image 
>>> should be all grey).
>>> -         */
>>> -        std::fill(gammaTable.begin(), gammaTable.begin() + blackIndex,
>>> -              gammaTable[blackIndex]);
>>> -    }
>>> -
>>> -    context.activeState.gamma.gamma = gamma;
>>> -    context.activeState.gamma.blackLevel = blackLevel;
>>> -    context.activeState.gamma.contrastExp = contrastExp;
>>> -}
>>> -
>>> -int16_t Lut::matrixValue(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,
>>> -          DebayerParams *params)
>>> -{
>>> -    /*
>>> -     * Update the gamma table if needed. This means if black level 
>>> changes
>>> -     * and since the black level gets updated only if a lower value is
>>> -     * observed, it's not permanently prone to minor fluctuations or
>>> -     * rounding errors.
>>> -     */
>>> -    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();
>>> -    const double div = 
>>> static_cast<double>(DebayerParams::kRGBLookupSize) /
>>> -               gammaTableSize;
>>> -
>>> -    if (!context.ccmEnabled) {
>>> -        for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; 
>>> i++) {
>>> -            /* Apply gamma after gain! */
>>> -            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())];
>>> -        }
>>> -    } else if (context.activeState.matrixChanged || 
>>> gammaUpdateNeeded) {
>>> -        auto &matrix = context.activeState.combinedMatrix;
>>> -        auto &red = params->redCcm;
>>> -        auto &green = params->greenCcm;
>>> -        auto &blue = params->blueCcm;
>>> -        params->ccm = matrix;
>>> -        if (!context.gpuIspEnabled) {
>>> -            for (unsigned int i = 0; i < 
>>> DebayerParams::kRGBLookupSize; i++) {
>>> -                red[i].r = matrixValue(i, matrix[0][0]);
>>> -                red[i].g = matrixValue(i, matrix[1][0]);
>>> -                red[i].b = matrixValue(i, matrix[2][0]);
>>> -                green[i].r = matrixValue(i, matrix[0][1]);
>>> -                green[i].g = matrixValue(i, matrix[1][1]);
>>> -                green[i].b = matrixValue(i, matrix[2][1]);
>>> -                blue[i].r = matrixValue(i, matrix[0][2]);
>>> -                blue[i].g = matrixValue(i, matrix[1][2]);
>>> -                blue[i].b = matrixValue(i, matrix[2][2]);
>>> -                params->gammaLut[i] = gammaTable[i / div];
>>> -            }
>>> -        }
>>> -        context.activeState.matrixChanged = false;
>>> -    }
>>> -
>>> -    params->gamma = context.activeState.gamma.gamma;
>>> -    params->contrastExp = context.activeState.gamma.contrastExp;
>>> -}
>>> -
>>> -REGISTER_IPA_ALGORITHM(Lut, "Lut") - -} /* namespace 
>>> ipa::soft::algorithms */ - -} /* namespace libcamera */ diff --git 
>>> a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h 
>>> deleted file mode 100644 index ad16d1e8e..000000000 --- 
>>> a/src/ipa/simple/algorithms/lut.h +++ /dev/null @@ -1,35 +0,0 @@ -/* 
>>> SPDX-License-Identifier: LGPL-2.1-or-later */ -/* - * Copyright (C) 
>>> 2024, Red Hat Inc. - * - * Color lookup tables construction - */ - 
>>> -#pragma once - -#include "algorithm.h"
>>> -
>>> -namespace libcamera {
>>> -
>>> -namespace ipa::soft::algorithms {
>>> -
>>> -class Lut : public Algorithm
>>> -{
>>> -public:
>>> -    Lut() = default;
>>> -    ~Lut() = default;
>>> -
>>> -    int configure(IPAContext &context, const IPAConfigInfo 
>>> &configInfo) override;
>>> -    void prepare(IPAContext &context,
>>> -             const uint32_t frame,
>>> -             IPAFrameContext &frameContext,
>>> -             DebayerParams *params) override;
>>> -
>>> -private:
>>> -    void updateGammaTable(IPAContext &context);
>>> -    int16_t matrixValue(unsigned int i, float ccm) const;
>>> -};
>>> -
>>> -} /* namespace ipa::soft::algorithms */
>>> -
>>> -} /* namespace libcamera */
>>> diff --git a/src/ipa/simple/algorithms/meson.build 
>>> b/src/ipa/simple/algorithms/meson.build
>>> index ebe9f20dd..73c637220 100644
>>> --- a/src/ipa/simple/algorithms/meson.build
>>> +++ b/src/ipa/simple/algorithms/meson.build
>>> @@ -6,5 +6,4 @@ soft_simple_ipa_algorithms = files([
>>>       'agc.cpp',
>>>       'blc.cpp',
>>>       'ccm.cpp',
>>> -    'lut.cpp',
>>>   ])
>>> diff --git a/src/ipa/simple/data/uncalibrated.yaml 
>>> b/src/ipa/simple/data/uncalibrated.yaml
>>> index e389e0588..c6feda36d 100644
>>> --- a/src/ipa/simple/data/uncalibrated.yaml
>>> +++ b/src/ipa/simple/data/uncalibrated.yaml
>>> @@ -15,6 +15,5 @@ algorithms:
>>>                    0, 1, 0,
>>>                    0, 0, 1]
>>>     - Adjust:
>>> -  - Lut:
>>>     - Agc:
>>>   ...
>>> diff --git a/src/ipa/simple/ipa_context.h 
>>> b/src/ipa/simple/ipa_context.h
>>> index 293e35b71..34f7403a4 100644
>>> --- a/src/ipa/simple/ipa_context.h
>>> +++ b/src/ipa/simple/ipa_context.h
>>> @@ -53,17 +53,7 @@ struct IPAActiveState {
>>>           unsigned int temperatureK;
>>>       } awb;
>>>   -    static constexpr unsigned int kGammaLookupSize = 1024;
>>> -    struct {
>>> -        std::array<double, kGammaLookupSize> gammaTable;
>>> -        uint8_t blackLevel;
>>> -        float gamma;
>>> -        float contrast;
>>> -        float contrastExp;
>>> -    } gamma;
>>> -
>>>       Matrix<float, 3, 3> combinedMatrix;
>>> -    bool matrixChanged = false;
>>>         struct {
>>>           float gamma;
>>> @@ -103,7 +93,6 @@ struct IPAContext {
>>>       FCQueue<IPAFrameContext> frameContexts;
>>>       ControlInfoMap::Map ctrlMap;
>>>       bool ccmEnabled = false;
>>> -    bool gpuIspEnabled = false;
>>>   };
>>>     } /* namespace ipa::soft */
>>> diff --git a/src/ipa/simple/soft_simple.cpp 
>>> b/src/ipa/simple/soft_simple.cpp
>>> index 732e82510..6bef597c8 100644
>>> --- a/src/ipa/simple/soft_simple.cpp
>>> +++ b/src/ipa/simple/soft_simple.cpp
>>> @@ -26,6 +26,7 @@
>>>   #include "libcamera/internal/software_isp/swisp_stats.h"
>>>   #include "libcamera/internal/yaml_parser.h"
>>>   +#include "algorithms/adjust.h"
>>>   #include "libipa/camera_sensor_helper.h"
>>>     #include "module.h"
>>> @@ -55,7 +56,6 @@ public:
>>>            const SharedFD &fdParams,
>>>            const IPACameraSensorInfo &sensorInfo,
>>>            const ControlInfoMap &sensorControls,
>>> -         bool gpuIspEnabled,
>>>            ControlInfoMap *ipaControls,
>>>            bool *ccmEnabled) override;
>>>       int configure(const IPAConfigInfo &configInfo) override;
>>> @@ -96,7 +96,6 @@ int IPASoftSimple::init(const IPASettings &settings,
>>>               const SharedFD &fdParams,
>>>               const IPACameraSensorInfo &sensorInfo,
>>>               const ControlInfoMap &sensorControls,
>>> -            bool gpuIspEnabled,
>>>               ControlInfoMap *ipaControls,
>>>               bool *ccmEnabled)
>>>   {
>>> @@ -108,7 +107,6 @@ int IPASoftSimple::init(const IPASettings 
>>> &settings,
>>>       }
>>>         context_.sensorInfo = sensorInfo;
>>> -    context_.gpuIspEnabled = gpuIspEnabled;
>>>         /* Load the tuning data file */
>>>       File file(settings.configurationFile);
>>> @@ -161,6 +159,11 @@ int IPASoftSimple::init(const IPASettings 
>>> &settings,
>>>           }
>>>             params_ = static_cast<DebayerParams *>(mem);
>>> +        params_->blackLevel = { { 0.0, 0.0, 0.0 } };
>>> +        params_->gamma = 1.0 / algorithms::kDefaultGamma;
>>> +        params_->contrastExp = 1.0;
>>> +        params_->gains = { { 1.0, 1.0, 1.0 } };
>>> +        /* combinedMatrix is reset for each frame. */
>>>       }
>>>         {
>>> @@ -287,6 +290,8 @@ void IPASoftSimple::computeParams(const uint32_t 
>>> frame)
>>>       IPAFrameContext &frameContext = 
>>> context_.frameContexts.get(frame);
>>>       for (auto const &algo : algorithms())
>>>           algo->prepare(context_, frame, frameContext, params_);
>>> +    params_->combinedMatrix = context_.activeState.combinedMatrix;
>>> +
>>>       setIspParams.emit();
>>>   }
>>>   diff --git a/src/libcamera/software_isp/debayer.cpp 
>>> b/src/libcamera/software_isp/debayer.cpp
>>> index 65a1762dd..dccdd86b4 100644
>>> --- a/src/libcamera/software_isp/debayer.cpp
>>> +++ b/src/libcamera/software_isp/debayer.cpp
>>> @@ -1,7 +1,7 @@
>>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>>   /*
>>>    * Copyright (C) 2023, Linaro Ltd
>>> - * Copyright (C) 2023-2025 Red Hat Inc.
>>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>>    *
>>>    * Authors:
>>>    * Hans de Goede<hdegoede@redhat.com>
>>> @@ -25,99 +25,28 @@ namespace libcamera {
>>>    */
>>>     /**
>>> - * \var DebayerParams::kRGBLookupSize
>>> - * \brief Size of a color lookup table
>>> + * \var DebayerParams::gains
>>> + * \brief Colour channel gains
>>>    */
>>>     /**
>>> - * \struct DebayerParams::CcmColumn
>>> - * \brief Type of a single column of a color correction matrix (CCM)
>>> - *
>>> - * When multiplying an input pixel, columns in the CCM correspond 
>>> to the red,
>>> - * green or blue component of input pixel values, while rows 
>>> correspond to the
>>> - * red, green or blue components of the output pixel values. The 
>>> members of the
>>> - * CcmColumn structure are named after the colour components of the 
>>> output pixel
>>> - * values they correspond to.
>>> - */
>>> -
>>> -/**
>>> - * \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::LookupTable
>>> - * \brief Type of the lookup tables for single lookup values
>>> - */
>>> -
>>> -/**
>>> - * \typedef DebayerParams::CcmLookupTable
>>> - * \brief Type of the CCM lookup tables for red, green, blue values
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::red
>>> - * \brief Lookup table for red color, mapping input values to 
>>> output values
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::green
>>> - * \brief Lookup table for green color, mapping input values to 
>>> output values
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::blue
>>> - * \brief Lookup table for blue color, mapping input values to 
>>> output values
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::redCcm
>>> - * \brief Lookup table for the CCM red column, mapping input values 
>>> to output values
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::greenCcm
>>> - * \brief Lookup table for the CCM green column, mapping input 
>>> values to output values
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::blueCcm
>>> - * \brief Lookup table for the CCM blue column, mapping input 
>>> values to output values
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::gammaLut
>>> - * \brief Gamma lookup table used with color correction matrix
>>> - */
>>> -
>>> -/**
>>> - * \var DebayerParams::ccm
>>> - * \brief Per frame colour correction matrix for GPUISP
>>> + * \var DebayerParams::combinedMatrix
>>> + * \brief Colour correction matrix, including other adjustments
>>>    */
>>>     /**
>>>    * \var DebayerParams::blackLevel
>>> - * \brief Blacklevel gains for the GPUISP
>>> + * \brief Black level values
>>>    */
>>>     /**
>>>    * \var DebayerParams::gamma
>>> - * \brief Gamma value for the GPUISP
>>> + * \brief Gamma value, e.g. 1/2.2
>>>    */
>>>     /**
>>>    * \var DebayerParams::contrastExp
>>> - * \brief Contrast value for GPUISP
>>> + * \brief Contrast value to be used as an exponent
>>>    */
>>>     /**
>>> @@ -131,13 +60,6 @@ LOG_DEFINE_CATEGORY(Debayer)
>>>     Debayer::Debayer(const GlobalConfiguration &configuration) : 
>>> bench_(configuration)
>>>   {
>>> -    /* Initialize color lookup tables */
>>> -    for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
>>> -        red_[i] = green_[i] = blue_[i] = i;
>>> -        redCcm_[i] = { static_cast<int16_t>(i), 0, 0 };
>>> -        greenCcm_[i] = { 0, static_cast<int16_t>(i), 0 };
>>> -        blueCcm_[i] = { 0, 0, static_cast<int16_t>(i) };
>>> -    }
>>>   }
>>>     Debayer::~Debayer()
>>> @@ -305,56 +227,6 @@ Debayer::~Debayer()
>>>    * \brief Output size object
>>>    */
>>>   -/**
>>> - * \var Debayer::red_
>>> - * \brief Lookup table for red channel gain and correction values
>>> - *
>>> - * This table provides precomputed per-pixel or per-intensity
>>> - * correction values for the red color channel used during debayering.
>>> - */
>>> -
>>> -/**
>>> - * \var Debayer::green_
>>> - * \brief Lookup table for green channel gain and correction values
>>> - *
>>> - * This table provides precomputed per-pixel or per-intensity
>>> - * correction values for the green color channel used during 
>>> debayering.
>>> - */
>>> -
>>> -/**
>>> - * \var Debayer::blue_
>>> - * \brief Lookup table for blue channel gain and correction values
>>> - *
>>> - * This table provides precomputed per-pixel or per-intensity
>>> - * correction values for the blue color channel used during 
>>> debayering.
>>> - */
>>> -
>>> -/**
>>> - * \var Debayer::redCcm_
>>> - * \brief Red channel Color Correction Matrix (CCM) lookup table
>>> - *
>>> - * Contains coefficients for green channel color correction.
>>> - */
>>> -
>>> -/**
>>> - * \var Debayer::greenCcm_
>>> - * \brief Green channel Color Correction Matrix (CCM) lookup table
>>> - *
>>> - * Contains coefficients for green channel color correction.
>>> - */
>>> -
>>> -/**
>>> - * \var Debayer::blueCcm_
>>> - * \brief Blue channel Color Correction Matrix (CCM) lookup table
>>> - *
>>> - * Contains coefficients for blue channel color correction.
>>> - */
>>> -
>>> -/**
>>> - * \var Debayer::gammaLut_
>>> - * \brief Gamma correction lookup table
>>> - */
>>> -
>>>   /**
>>>    * \var Debayer::swapRedBlueGains_
>>>    * \brief Flag indicating whether red and blue channel gains 
>>> should be swapped
>>> @@ -396,34 +268,6 @@ Debayer::~Debayer()
>>>    * DebayerEGL::start.
>>>    */
>>>   -/**
>>> - * \fn void Debayer::setParams(DebayerParams &params)
>>> - * \brief Select the bayer params to use for the next frame debayer
>>> - * \param[in] params The parameters to be used in debayering
>>> - */
>>> -void Debayer::setParams(DebayerParams &params)
>>> -{
>>> -    green_ = params.green;
>>> -    greenCcm_ = params.greenCcm;
>>> -    if (swapRedBlueGains_) {
>>> -        red_ = params.blue;
>>> -        blue_ = params.red;
>>> -        redCcm_ = params.blueCcm;
>>> -        blueCcm_ = params.redCcm;
>>> -        for (unsigned int i = 0; i < 256; i++) {
>>> -            std::swap(redCcm_[i].r, redCcm_[i].b);
>>> -            std::swap(greenCcm_[i].r, greenCcm_[i].b);
>>> -            std::swap(blueCcm_[i].r, blueCcm_[i].b);
>>> -        }
>>> -    } else {
>>> -        red_ = params.red;
>>> -        blue_ = params.blue;
>>> -        redCcm_ = params.redCcm;
>>> -        blueCcm_ = params.blueCcm;
>>> -    }
>>> -    gammaLut_ = params.gammaLut;
>>> -}
>>> -
>>>   /**
>>>    * \fn void Debayer::dmaSyncBegin(DebayerParams &params)
>>>    * \brief Common CPU/GPU Dma Sync Buffer begin
>>> diff --git a/src/libcamera/software_isp/debayer.h 
>>> b/src/libcamera/software_isp/debayer.h
>>> index cd2db9930..652cff4cc 100644
>>> --- a/src/libcamera/software_isp/debayer.h
>>> +++ b/src/libcamera/software_isp/debayer.h
>>> @@ -78,13 +78,6 @@ public:
>>>       Size outputSize_;
>>>       PixelFormat inputPixelFormat_;
>>>       PixelFormat outputPixelFormat_;
>>> -    DebayerParams::LookupTable red_;
>>> -    DebayerParams::LookupTable green_;
>>> -    DebayerParams::LookupTable blue_;
>>> -    DebayerParams::CcmLookupTable redCcm_;
>>> -    DebayerParams::CcmLookupTable greenCcm_;
>>> -    DebayerParams::CcmLookupTable blueCcm_;
>>> -    DebayerParams::LookupTable gammaLut_;
>>>       bool swapRedBlueGains_;
>>>       Benchmark bench_;
>>>   @@ -92,7 +85,6 @@ private:
>>>       virtual Size patternSize(PixelFormat inputFormat) = 0;
>>>     protected:
>>> -    void setParams(DebayerParams &params);
>>>       void dmaSyncBegin(std::vector<DmaSyncer> &dmaSyncers, 
>>> FrameBuffer *input, FrameBuffer *output);
>>>       static bool isStandardBayerOrder(BayerFormat::Order order);
>>>   };
>>> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp 
>>> b/src/libcamera/software_isp/debayer_cpu.cpp
>>> index 00738c56b..af7af0a8d 100644
>>> --- a/src/libcamera/software_isp/debayer_cpu.cpp
>>> +++ b/src/libcamera/software_isp/debayer_cpu.cpp
>>> @@ -1,7 +1,7 @@
>>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>>   /*
>>>    * Copyright (C) 2023, Linaro Ltd
>>> - * Copyright (C) 2023-2025 Red Hat Inc.
>>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>>    *
>>>    * Authors:
>>>    * Hans de Goede<hdegoede@redhat.com>
>>> @@ -68,21 +68,21 @@ DebayerCpu::~DebayerCpu() = default;
>>>   #define GAMMA(value) \
>>>       *dst++ = gammaLut_[std::clamp(value, 0, 
>>> static_cast<int>(gammaLut_.size()) - 1)]
>>>   -#define STORE_PIXEL(b_, g_, 
>>> r_)                                        \
>>> -    if constexpr (ccmEnabled) {                                    \
>>> -        const DebayerParams::CcmColumn &blue = blueCcm_[b_];   \
>>> -        const DebayerParams::CcmColumn &green = greenCcm_[g_]; \
>>> -        const DebayerParams::CcmColumn &red = redCcm_[r_];     \
>>> -        GAMMA(blue.b + green.b + red.b);                       \
>>> -        GAMMA(blue.g + green.g + red.g);                       \
>>> -        GAMMA(blue.r + green.r + red.r);                       \
>>> -    } else {                                                       \
>>> -        *dst++ = blue_[b_];                                    \
>>> -        *dst++ = green_[g_];                                   \
>>> -        *dst++ = red_[r_];                                     \
>>> - } \
>>> -    if constexpr (addAlphaByte)                                    \
>>> -        *dst++ = 255;                                          \
>>> +#define STORE_PIXEL(b_, g_, r_)                         \
>>> +    if constexpr (ccmEnabled) {                     \
>>> +        const CcmColumn &blue = blueCcm_[b_];   \
>>> +        const CcmColumn &green = greenCcm_[g_]; \
>>> +        const CcmColumn &red = redCcm_[r_];     \
>>> +        GAMMA(blue.b + green.b + red.b);        \
>>> +        GAMMA(blue.g + green.g + red.g);        \
>>> +        GAMMA(blue.r + green.r + red.r);        \
>>> +    } else {                                        \
>>> +        *dst++ = blue_[b_];                     \
>>> +        *dst++ = green_[g_];                    \
>>> +        *dst++ = red_[r_];                      \
>>> +    }                                               \
>>> +    if constexpr (addAlphaByte)                     \
>>> +        *dst++ = 255;                           \
>>>       x++;
>>>     /*
>>> @@ -525,6 +525,16 @@ int DebayerCpu::configure(const 
>>> StreamConfiguration &inputCfg,
>>>       if (ret != 0)
>>>           return -EINVAL;
>>>   +    ccmEnabled_ = ccmEnabled;
>>> +
>>> +    /*
>>> +     * Lookup tables must be initialized because the initial value 
>>> is used for
>>> +     * the first two frames, i.e. until stats processing starts 
>>> providing its
>>> +     * own parameters. Let's enforce recomputing lookup tables by 
>>> setting the
>>> +     * stored last used gamma to an out-of-range value.
>>> +     */
>>> +    params_.gamma = 1.0;
>>> +
>>>       window_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) &
>>>               ~(inputConfig_.patternSize.width - 1);
>>>       window_.y = ((inputCfg.size.height - outputCfg.size.height) / 
>>> 2) &
>>> @@ -740,6 +750,98 @@ void DebayerCpu::process4(uint32_t frame, const 
>>> uint8_t *src, uint8_t *dst)
>>>       }
>>>   }
>>>   +void DebayerCpu::updateGammaTable(DebayerParams &params)
>>> +{
>>> +    const RGB<float> blackLevel = params.blackLevel;
>>> +    /* Take let's say the green channel black level */
>>> +    const unsigned int blackIndex = blackLevel[1] * 
>>> gammaTable_.size();
>>> +    const float gamma = params.gamma;
>>> +    const float contrastExp = params.contrastExp;
>>> +
>>> +    const float divisor = gammaTable_.size() - blackIndex - 1.0;
>>> +    for (unsigned int i = blackIndex; i < gammaTable_.size(); i++) {
>>> +        float normalized = (i - blackIndex) / divisor;
>>> +        /* Convert 0..2 to 0..infinity; avoid actual inifinity at 
>>> tan(pi/2) */
>>> +        /* Apply simple S-curve */
>>> +        if (normalized < 0.5)
>>> +            normalized = 0.5 * std::pow(normalized / 0.5, 
>>> contrastExp);
>>> +        else
>>> +            normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 
>>> 0.5, contrastExp);
>>> +        gammaTable_[i] = UINT8_MAX *
>>> +                 std::pow(normalized, gamma);
>>> +    }
>>> +    /*
>>> +     * Due to CCM operations, the table lookup may reach indices 
>>> below the black
>>> +     * level. Let's set the table values below black level to the 
>>> minimum
>>> +     * non-black value to prevent problems when the minimum value is
>>> +     * significantly non-zero (for example, when the image should 
>>> be all grey).
>>> +     */
>>> +    std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex,
>>> +          gammaTable_[blackIndex]);
>>> +}
>>> +
>>> +void DebayerCpu::updateLookupTables(DebayerParams &params)
>>> +{
>>> +    const bool gammaUpdateNeeded =
>>> +        params.gamma != params_.gamma ||
>>> +        params.blackLevel != params_.blackLevel ||
>>> +        params.contrastExp != params_.contrastExp;
>>> +    if (gammaUpdateNeeded)
>>> +        updateGammaTable(params);
>>> +
>>> +    auto matrixChanged = [](const Matrix<float, 3, 3> &m1, const 
>>> Matrix<float, 3, 3> &m2) -> bool {
>>> +        return !std::equal(m1.data().begin(), m1.data().end(), 
>>> m2.data().begin());
>>> +    };
>>> +    const unsigned int gammaTableSize = gammaTable_.size();
>>> +    const double div = static_cast<double>(kRGBLookupSize) / 
>>> gammaTableSize;
>>> +    if (ccmEnabled_) {
>>> +        if (gammaUpdateNeeded ||
>>> +            matrixChanged(params.combinedMatrix, 
>>> params_.combinedMatrix)) {
>>> +            auto &red = swapRedBlueGains_ ? blueCcm_ : redCcm_;
>>> +            auto &green = greenCcm_;
>>> +            auto &blue = swapRedBlueGains_ ? redCcm_ : blueCcm_;
>>> +            const unsigned int redIndex = swapRedBlueGains_ ? 2 : 0;
>>> +            const unsigned int greenIndex = 1;
>>> +            const unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2;
>>> +            for (unsigned int i = 0; i < kRGBLookupSize; i++) {
>>> +                red[i].r = std::round(i * 
>>> params.combinedMatrix[redIndex][0]);
>>> +                red[i].g = std::round(i * 
>>> params.combinedMatrix[greenIndex][0]);
>>> +                red[i].b = std::round(i * 
>>> params.combinedMatrix[blueIndex][0]);
>>> +                green[i].r = std::round(i * 
>>> params.combinedMatrix[redIndex][1]);
>>> +                green[i].g = std::round(i * 
>>> params.combinedMatrix[greenIndex][1]);
>>> +                green[i].b = std::round(i * 
>>> params.combinedMatrix[blueIndex][1]);
>>> +                blue[i].r = std::round(i * 
>>> params.combinedMatrix[redIndex][2]);
>>> +                blue[i].g = std::round(i * 
>>> params.combinedMatrix[greenIndex][2]);
>>> +                blue[i].b = std::round(i * 
>>> params.combinedMatrix[blueIndex][2]);
>>> +                gammaLut_[i] = gammaTable_[i / div];
>>> +            }
>>> +        }
>>> +    } else {
>>> +        if (gammaUpdateNeeded || params.gains != params_.gains) {
>>> +            auto &gains = params.gains;
>>> +            auto &red = swapRedBlueGains_ ? blue_ : red_;
>>> +            auto &green = green_;
>>> +            auto &blue = swapRedBlueGains_ ? red_ : blue_;
>>> +            for (unsigned int i = 0; i < kRGBLookupSize; i++) {
>>> +                /* Apply gamma after gain! */
>>> +                const RGB<float> lutGains = (gains * i / 
>>> div).min(gammaTableSize - 1);
>>> +                red[i] = gammaTable_[static_cast<unsigned 
>>> int>(lutGains.r())];
>>> +                green[i] = gammaTable_[static_cast<unsigned 
>>> int>(lutGains.g())];
>>> +                blue[i] = gammaTable_[static_cast<unsigned 
>>> int>(lutGains.b())];
>>> +            }
>>> +        }
>>> +    }
>>> +
>>> +    LOG(Debayer, Debug)
>>> +        << "Debayer parameters: blackLevel=" << params.blackLevel
>>> +        << "; gamma=" << params.gamma
>>> +        << "; contrastExp=" << params.contrastExp
>>> +        << "; gains=" << params.gains
>>> +        << "; matrix=" << params.combinedMatrix;
>>> +
>>> +    params_ = params;
>>> +}
>>> +
>>>   void DebayerCpu::process(uint32_t frame, FrameBuffer *input, 
>>> FrameBuffer *output, DebayerParams params)
>>>   {
>>>       bench_.startFrame();
>>> @@ -748,7 +850,7 @@ void DebayerCpu::process(uint32_t frame, 
>>> FrameBuffer *input, FrameBuffer *output
>>>         dmaSyncBegin(dmaSyncers, input, output);
>>>   -    setParams(params);
>>> +    updateLookupTables(params);
>>>         /* 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 67df2b93a..b5cbb5bd2 100644
>>> --- a/src/libcamera/software_isp/debayer_cpu.h
>>> +++ b/src/libcamera/software_isp/debayer_cpu.h
>>> @@ -1,7 +1,7 @@
>>>   /* SPDX-License-Identifier: LGPL-2.1-or-later */
>>>   /*
>>>    * Copyright (C) 2023, Linaro Ltd
>>> - * Copyright (C) 2023-2025 Red Hat Inc.
>>> + * Copyright (C) 2023-2026 Red Hat Inc.
>>>    *
>>>    * Authors:
>>>    * Hans de Goede<hdegoede@redhat.com>
>>> @@ -18,6 +18,8 @@
>>>   #include <libcamera/base/object.h>
>>>     #include "libcamera/internal/bayer_format.h"
>>> +#include "libcamera/internal/global_configuration.h"
>>> +#include "libcamera/internal/software_isp/debayer_params.h"
>>>   #include "libcamera/internal/software_isp/swstats_cpu.h"
>>>     #include "debayer.h"
>>> @@ -108,10 +110,32 @@ private:
>>>       void memcpyNextLine(const uint8_t *linePointers[]);
>>>       void process2(uint32_t frame, const uint8_t *src, uint8_t *dst);
>>>       void process4(uint32_t frame, const uint8_t *src, uint8_t *dst);
>>> +    void updateGammaTable(DebayerParams &params);
>>> +    void updateLookupTables(DebayerParams &params);
>>>         /* Max. supported Bayer pattern height is 4, debayering this 
>>> requires 5 lines */
>>>       static constexpr unsigned int kMaxLineBuffers = 5;
>>>   +    static constexpr unsigned int kRGBLookupSize = 256;
>>> +    static constexpr unsigned int kGammaLookupSize = 1024;
>>> +    struct CcmColumn {
>>> +        int16_t r;
>>> +        int16_t g;
>>> +        int16_t b;
>>> +    };
>>> +    using LookupTable = std::array<uint8_t, kRGBLookupSize>;
>>> +    using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
>>> +    LookupTable red_;
>>> +    LookupTable green_;
>>> +    LookupTable blue_;
>>> +    CcmLookupTable redCcm_;
>>> +    CcmLookupTable greenCcm_;
>>> +    CcmLookupTable blueCcm_;
>>> +    std::array<double, kGammaLookupSize> gammaTable_;
>>> +    LookupTable gammaLut_;
>>> +    bool ccmEnabled_;
>>> +    DebayerParams params_;
>>> +
>>>       debayerFn debayer0_;
>>>       debayerFn debayer1_;
>>>       debayerFn debayer2_;
>>> diff --git a/src/libcamera/software_isp/debayer_egl.cpp 
>>> b/src/libcamera/software_isp/debayer_egl.cpp
>>> index 9693d7252..af04d60ca 100644
>>> --- a/src/libcamera/software_isp/debayer_egl.cpp
>>> +++ b/src/libcamera/software_isp/debayer_egl.cpp
>>> @@ -475,18 +475,18 @@ void 
>>> DebayerEGL::setShaderVariableValues(DebayerParams &params)
>>>                   << " textureUniformProjMatrix_ " << 
>>> textureUniformProjMatrix_;
>>>         GLfloat ccm[9] = {
>>> -        params.ccm[0][0],
>>> -        params.ccm[0][1],
>>> -        params.ccm[0][2],
>>> -        params.ccm[1][0],
>>> -        params.ccm[1][1],
>>> -        params.ccm[1][2],
>>> -        params.ccm[2][0],
>>> -        params.ccm[2][1],
>>> -        params.ccm[2][2],
>>> +        params.combinedMatrix[0][0],
>>> +        params.combinedMatrix[0][1],
>>> +        params.combinedMatrix[0][2],
>>> +        params.combinedMatrix[1][0],
>>> +        params.combinedMatrix[1][1],
>>> +        params.combinedMatrix[1][2],
>>> +        params.combinedMatrix[2][0],
>>> +        params.combinedMatrix[2][1],
>>> +        params.combinedMatrix[2][2],
>>>       };
>>>       glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_FALSE, ccm);
>>> -    LOG(Debayer, Debug) << " ccmUniformDataIn_ " << 
>>> ccmUniformDataIn_ << " data " << params.ccm;
>>> +    LOG(Debayer, Debug) << " ccmUniformDataIn_ " << 
>>> ccmUniformDataIn_ << " data " << params.combinedMatrix;
>>>         /*
>>>        * 0 = Red, 1 = Green, 2 = Blue
>>> @@ -544,8 +544,6 @@ void DebayerEGL::process(uint32_t frame, 
>>> FrameBuffer *input, FrameBuffer *output
>>>         dmaSyncBegin(dmaSyncers, input, nullptr);
>>>   -    setParams(params);
>>> -
>>>       /* Copy metadata from the input buffer */
>>>       FrameMetadata &metadata = output->_d()->metadata();
>>>       metadata.status = input->metadata().status;
>>> diff --git a/src/libcamera/software_isp/software_isp.cpp 
>>> b/src/libcamera/software_isp/software_isp.cpp
>>> index 7ad3511db..a83986b78 100644
>>> --- a/src/libcamera/software_isp/software_isp.cpp
>>> +++ b/src/libcamera/software_isp/software_isp.cpp
>>> @@ -84,23 +84,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, 
>>> const CameraSensor *sensor,
>>>              DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap |
>>>              DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
>>>   {
>>> -    /*
>>> -     * debayerParams_ must be initialized because the initial value 
>>> is used for
>>> -     * the first two frames, i.e. until stats processing starts 
>>> providing its
>>> -     * own parameters.
>>> -     *
>>> -     * \todo This should be handled in the same place as the related
>>> -     * operations, in the IPA module.
>>> -     */
>>> -    std::array<uint8_t, 256> gammaTable;
>>> -    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];
>>> -    }
>>> -
>>>       if (!dmaHeap_.isValid()) {
>>>           LOG(SoftwareIsp, Error) << "Failed to create 
>>> DmaBufAllocator object";
>>>           return;
>>> @@ -121,8 +104,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, 
>>> const CameraSensor *sensor,
>>>       }
>>>       stats->statsReady.connect(this, &SoftwareIsp::statsReady);
>>>   -    bool gpuIspEnabled;
>>> -
>>>   #if HAVE_DEBAYER_EGL
>>>       std::optional<std::string> softISPMode = 
>>> configuration.envOption("LIBCAMERA_SOFTISP_MODE", { "software_isp", 
>>> "mode" });
>>>       if (softISPMode) {
>>> @@ -133,15 +114,12 @@ SoftwareIsp::SoftwareIsp(PipelineHandler 
>>> *pipe, const CameraSensor *sensor,
>>>           }
>>>       }
>>>   -    if (!softISPMode || softISPMode == "gpu") {
>>> +    if (!softISPMode || softISPMode == "gpu")
>>>           debayer_ = std::make_unique<DebayerEGL>(std::move(stats), 
>>> configuration);
>>> -        gpuIspEnabled = true;
>>> -    }
>>> +
>>>   #endif
>>> -    if (!debayer_) {
>>> +    if (!debayer_)
>>>           debayer_ = std::make_unique<DebayerCpu>(std::move(stats), 
>>> configuration);
>>> -        gpuIspEnabled = false;
>>> -    }
>>>         debayer_->inputBufferReady.connect(this, 
>>> &SoftwareIsp::inputReady);
>>>       debayer_->outputBufferReady.connect(this, 
>>> &SoftwareIsp::outputReady);
>>> @@ -173,7 +151,6 @@ SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, 
>>> const CameraSensor *sensor,
>>>                sharedParams_.fd(),
>>>                sensorInfo,
>>>                sensor->controls(),
>>> -             gpuIspEnabled,
>>>                ipaControls,
>>>                &ccmEnabled_);
>>>       if (ret) {
>>
>> -- 
>> Robert Mader
>> Consultant Software Developer
>>
>> Collabora Ltd.
>> Platinum Building, St John's Innovation Park, Cambridge CB4 0DS, UK
>> Registered in England & Wales, no. 5513718
>>
>

Patch
diff mbox series

diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index 2d69bd295..1c0412d75 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -1,6 +1,6 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
- * Copyright (C) 2023-2025 Red Hat Inc.
+ * Copyright (C) 2023-2026 Red Hat Inc.
  *
  * Authors:
  * Hans de Goede <hdegoede@redhat.com>
@@ -10,7 +10,6 @@ 
 
 #pragma once
 
-#include <array>
 #include <stdint.h>
 
 #include "libcamera/internal/matrix.h"
@@ -19,47 +18,11 @@ 
 namespace libcamera {
 
 struct DebayerParams {
-	static constexpr unsigned int kRGBLookupSize = 256;
-
-	struct CcmColumn {
-		int16_t r;
-		int16_t g;
-		int16_t b;
-	};
-
-	using LookupTable = std::array<uint8_t, kRGBLookupSize>;
-	using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
-
-	/*
-	 * Color lookup tables when CCM is not used.
-	 *
-	 * Each color of a debayered pixel is amended by the corresponding
-	 * value in the given table.
-	 */
-	LookupTable red;
-	LookupTable green;
-	LookupTable blue;
-
-	/*
-	 * Color and gamma lookup tables when CCM is used.
-	 *
-	 * Each of the CcmLookupTable's corresponds to a CCM column; together they
-	 * make a complete 3x3 CCM lookup table. The CCM is applied on debayered
-	 * pixels and then the gamma lookup table is used to set the resulting
-	 * values of all the three colors.
-	 */
-	CcmLookupTable redCcm;
-	CcmLookupTable greenCcm;
-	CcmLookupTable blueCcm;
-	LookupTable gammaLut;
-
-	/*
-	 * Per frame corrections as calculated by the IPA
-	 */
-	Matrix<float, 3, 3> ccm;
+	Matrix<float, 3, 3> combinedMatrix;
 	RGB<float> blackLevel;
 	float gamma;
 	float contrastExp;
+	RGB<float> gains;
 };
 
 } /* namespace libcamera */
diff --git a/include/libcamera/ipa/soft.mojom b/include/libcamera/ipa/soft.mojom
index aff8fcbd3..77328c5fd 100644
--- a/include/libcamera/ipa/soft.mojom
+++ b/include/libcamera/ipa/soft.mojom
@@ -17,8 +17,7 @@  interface IPASoftInterface {
 	     libcamera.SharedFD fdStats,
 	     libcamera.SharedFD fdParams,
 	     libcamera.IPACameraSensorInfo sensorInfo,
-	     libcamera.ControlInfoMap sensorControls,
-	     bool gpuIspEnabled)
+	     libcamera.ControlInfoMap sensorControls)
 		=> (int32 ret, libcamera.ControlInfoMap ipaControls, bool ccmEnabled);
 	start() => (int32 ret);
 	stop();
diff --git a/src/ipa/simple/algorithms/adjust.cpp b/src/ipa/simple/algorithms/adjust.cpp
index acdd3f741..068e98404 100644
--- a/src/ipa/simple/algorithms/adjust.cpp
+++ b/src/ipa/simple/algorithms/adjust.cpp
@@ -95,23 +95,20 @@  void Adjust::applySaturation(Matrix<float, 3, 3> &matrix, float saturation)
 void Adjust::prepare(IPAContext &context,
 		     [[maybe_unused]] const uint32_t frame,
 		     IPAFrameContext &frameContext,
-		     [[maybe_unused]] DebayerParams *params)
+		     DebayerParams *params)
 {
 	frameContext.gamma = context.activeState.knobs.gamma;
 	frameContext.contrast = context.activeState.knobs.contrast;
 
-	if (!context.ccmEnabled)
-		return;
-
 	auto &saturation = context.activeState.knobs.saturation;
-	frameContext.saturation = saturation;
-	if (saturation)
+	if (context.ccmEnabled && saturation) {
 		applySaturation(context.activeState.combinedMatrix, saturation.value());
-
-	if (saturation != lastSaturation_) {
-		context.activeState.matrixChanged = true;
-		lastSaturation_ = saturation;
+		frameContext.saturation = saturation;
 	}
+
+	params->gamma = 1.0 / context.activeState.knobs.gamma;
+	const float contrast = context.activeState.knobs.contrast.value_or(kDefaultContrast);
+	params->contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001));
 }
 
 void Adjust::process([[maybe_unused]] IPAContext &context,
diff --git a/src/ipa/simple/algorithms/adjust.h b/src/ipa/simple/algorithms/adjust.h
index 7644138ff..fb133b140 100644
--- a/src/ipa/simple/algorithms/adjust.h
+++ b/src/ipa/simple/algorithms/adjust.h
@@ -7,8 +7,6 @@ 
 
 #pragma once
 
-#include <optional>
-
 #include "libcamera/internal/matrix.h"
 
 #include <libipa/interpolator.h>
@@ -45,8 +43,6 @@  public:
 
 private:
 	void applySaturation(Matrix<float, 3, 3> &ccm, float saturation);
-
-	std::optional<float> lastSaturation_;
 };
 
 } /* namespace ipa::soft::algorithms */
diff --git a/src/ipa/simple/algorithms/awb.cpp b/src/ipa/simple/algorithms/awb.cpp
index 4d2f1df15..d2c1b87f0 100644
--- a/src/ipa/simple/algorithms/awb.cpp
+++ b/src/ipa/simple/algorithms/awb.cpp
@@ -37,7 +37,7 @@  int Awb::configure(IPAContext &context,
 void Awb::prepare(IPAContext &context,
 		  [[maybe_unused]] const uint32_t frame,
 		  IPAFrameContext &frameContext,
-		  [[maybe_unused]] DebayerParams *params)
+		  DebayerParams *params)
 {
 	auto &gains = context.activeState.awb.gains;
 	Matrix<float, 3, 3> gainMatrix = { { gains.r(), 0, 0,
@@ -45,9 +45,11 @@  void Awb::prepare(IPAContext &context,
 					     0, 0, gains.b() } };
 	context.activeState.combinedMatrix =
 		context.activeState.combinedMatrix * gainMatrix;
-	/* Just report, the gains are applied in LUT algorithm. */
+
 	frameContext.gains.red = gains.r();
 	frameContext.gains.blue = gains.b();
+
+	params->gains = gains;
 }
 
 void Awb::process(IPAContext &context,
diff --git a/src/ipa/simple/algorithms/ccm.cpp b/src/ipa/simple/algorithms/ccm.cpp
index 5576a301f..911a5af2c 100644
--- a/src/ipa/simple/algorithms/ccm.cpp
+++ b/src/ipa/simple/algorithms/ccm.cpp
@@ -51,7 +51,6 @@  void Ccm::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
 	    utils::abs_diff(ct, lastCt_) >= kTemperatureThreshold) {
 		currentCcm_ = ccm_.getInterpolated(ct);
 		lastCt_ = ct;
-		context.activeState.matrixChanged = true;
 	}
 
 	context.activeState.combinedMatrix =
diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp
deleted file mode 100644
index fd442259a..000000000
--- a/src/ipa/simple/algorithms/lut.cpp
+++ /dev/null
@@ -1,140 +0,0 @@ 
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2024-2026, Red Hat Inc.
- *
- * Color lookup tables construction
- */
-
-#include "lut.h"
-
-#include <algorithm>
-#include <cmath>
-#include <optional>
-#include <stdint.h>
-
-#include <libcamera/base/log.h>
-
-#include <libcamera/control_ids.h>
-
-#include "simple/ipa_context.h"
-
-#include "adjust.h"
-
-namespace libcamera {
-
-LOG_DEFINE_CATEGORY(IPASoftLut)
-
-namespace ipa::soft::algorithms {
-
-int Lut::configure(IPAContext &context,
-		   [[maybe_unused]] const IPAConfigInfo &configInfo)
-{
-	updateGammaTable(context);
-
-	return 0;
-}
-
-void Lut::updateGammaTable(IPAContext &context)
-{
-	const auto blackLevel = context.activeState.blc.level;
-	const auto gamma = 1.0 / context.activeState.knobs.gamma;
-	const auto contrast = context.activeState.knobs.contrast.value_or(1.0);
-	/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */
-	float contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001));
-
-	if (!context.gpuIspEnabled) {
-		auto &gammaTable = context.activeState.gamma.gammaTable;
-		const unsigned int blackIndex = blackLevel * gammaTable.size() / 256;
-		const float divisor = gammaTable.size() - blackIndex - 1.0;
-		for (unsigned int i = blackIndex; i < gammaTable.size(); i++) {
-			double normalized = (i - blackIndex) / divisor;
-			/* Apply simple S-curve */
-			if (normalized < 0.5)
-				normalized = 0.5 * std::pow(normalized / 0.5, contrastExp);
-			else
-				normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);
-			gammaTable[i] = UINT8_MAX * std::pow(normalized, gamma);
-		}
-		/*
-		 * Due to CCM operations, the table lookup may reach indices below the black
-		 * level. Let's set the table values below black level to the minimum
-		 * non-black value to prevent problems when the minimum value is
-		 * significantly non-zero (for example, when the image should be all grey).
-		 */
-		std::fill(gammaTable.begin(), gammaTable.begin() + blackIndex,
-			  gammaTable[blackIndex]);
-	}
-
-	context.activeState.gamma.gamma = gamma;
-	context.activeState.gamma.blackLevel = blackLevel;
-	context.activeState.gamma.contrastExp = contrastExp;
-}
-
-int16_t Lut::matrixValue(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,
-		  DebayerParams *params)
-{
-	/*
-	 * Update the gamma table if needed. This means if black level changes
-	 * and since the black level gets updated only if a lower value is
-	 * observed, it's not permanently prone to minor fluctuations or
-	 * rounding errors.
-	 */
-	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();
-	const double div = static_cast<double>(DebayerParams::kRGBLookupSize) /
-			   gammaTableSize;
-
-	if (!context.ccmEnabled) {
-		for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
-			/* Apply gamma after gain! */
-			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())];
-		}
-	} else if (context.activeState.matrixChanged || gammaUpdateNeeded) {
-		auto &matrix = context.activeState.combinedMatrix;
-		auto &red = params->redCcm;
-		auto &green = params->greenCcm;
-		auto &blue = params->blueCcm;
-		params->ccm = matrix;
-		if (!context.gpuIspEnabled) {
-			for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
-				red[i].r = matrixValue(i, matrix[0][0]);
-				red[i].g = matrixValue(i, matrix[1][0]);
-				red[i].b = matrixValue(i, matrix[2][0]);
-				green[i].r = matrixValue(i, matrix[0][1]);
-				green[i].g = matrixValue(i, matrix[1][1]);
-				green[i].b = matrixValue(i, matrix[2][1]);
-				blue[i].r = matrixValue(i, matrix[0][2]);
-				blue[i].g = matrixValue(i, matrix[1][2]);
-				blue[i].b = matrixValue(i, matrix[2][2]);
-				params->gammaLut[i] = gammaTable[i / div];
-			}
-		}
-		context.activeState.matrixChanged = false;
-	}
-
-	params->gamma = context.activeState.gamma.gamma;
-	params->contrastExp = context.activeState.gamma.contrastExp;
-}
-
-REGISTER_IPA_ALGORITHM(Lut, "Lut")
-
-} /* namespace ipa::soft::algorithms */
-
-} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h
deleted file mode 100644
index ad16d1e8e..000000000
--- a/src/ipa/simple/algorithms/lut.h
+++ /dev/null
@@ -1,35 +0,0 @@ 
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2024, Red Hat Inc.
- *
- * Color lookup tables construction
- */
-
-#pragma once
-
-#include "algorithm.h"
-
-namespace libcamera {
-
-namespace ipa::soft::algorithms {
-
-class Lut : public Algorithm
-{
-public:
-	Lut() = default;
-	~Lut() = default;
-
-	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
-	void prepare(IPAContext &context,
-		     const uint32_t frame,
-		     IPAFrameContext &frameContext,
-		     DebayerParams *params) override;
-
-private:
-	void updateGammaTable(IPAContext &context);
-	int16_t matrixValue(unsigned int i, float ccm) const;
-};
-
-} /* namespace ipa::soft::algorithms */
-
-} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
index ebe9f20dd..73c637220 100644
--- a/src/ipa/simple/algorithms/meson.build
+++ b/src/ipa/simple/algorithms/meson.build
@@ -6,5 +6,4 @@  soft_simple_ipa_algorithms = files([
     'agc.cpp',
     'blc.cpp',
     'ccm.cpp',
-    'lut.cpp',
 ])
diff --git a/src/ipa/simple/data/uncalibrated.yaml b/src/ipa/simple/data/uncalibrated.yaml
index e389e0588..c6feda36d 100644
--- a/src/ipa/simple/data/uncalibrated.yaml
+++ b/src/ipa/simple/data/uncalibrated.yaml
@@ -15,6 +15,5 @@  algorithms:
                  0, 1, 0,
                  0, 0, 1]
   - Adjust:
-  - Lut:
   - Agc:
 ...
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 293e35b71..34f7403a4 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -53,17 +53,7 @@  struct IPAActiveState {
 		unsigned int temperatureK;
 	} awb;
 
-	static constexpr unsigned int kGammaLookupSize = 1024;
-	struct {
-		std::array<double, kGammaLookupSize> gammaTable;
-		uint8_t blackLevel;
-		float gamma;
-		float contrast;
-		float contrastExp;
-	} gamma;
-
 	Matrix<float, 3, 3> combinedMatrix;
-	bool matrixChanged = false;
 
 	struct {
 		float gamma;
@@ -103,7 +93,6 @@  struct IPAContext {
 	FCQueue<IPAFrameContext> frameContexts;
 	ControlInfoMap::Map ctrlMap;
 	bool ccmEnabled = false;
-	bool gpuIspEnabled = false;
 };
 
 } /* namespace ipa::soft */
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index 732e82510..6bef597c8 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -26,6 +26,7 @@ 
 #include "libcamera/internal/software_isp/swisp_stats.h"
 #include "libcamera/internal/yaml_parser.h"
 
+#include "algorithms/adjust.h"
 #include "libipa/camera_sensor_helper.h"
 
 #include "module.h"
@@ -55,7 +56,6 @@  public:
 		 const SharedFD &fdParams,
 		 const IPACameraSensorInfo &sensorInfo,
 		 const ControlInfoMap &sensorControls,
-		 bool gpuIspEnabled,
 		 ControlInfoMap *ipaControls,
 		 bool *ccmEnabled) override;
 	int configure(const IPAConfigInfo &configInfo) override;
@@ -96,7 +96,6 @@  int IPASoftSimple::init(const IPASettings &settings,
 			const SharedFD &fdParams,
 			const IPACameraSensorInfo &sensorInfo,
 			const ControlInfoMap &sensorControls,
-			bool gpuIspEnabled,
 			ControlInfoMap *ipaControls,
 			bool *ccmEnabled)
 {
@@ -108,7 +107,6 @@  int IPASoftSimple::init(const IPASettings &settings,
 	}
 
 	context_.sensorInfo = sensorInfo;
-	context_.gpuIspEnabled = gpuIspEnabled;
 
 	/* Load the tuning data file */
 	File file(settings.configurationFile);
@@ -161,6 +159,11 @@  int IPASoftSimple::init(const IPASettings &settings,
 		}
 
 		params_ = static_cast<DebayerParams *>(mem);
+		params_->blackLevel = { { 0.0, 0.0, 0.0 } };
+		params_->gamma = 1.0 / algorithms::kDefaultGamma;
+		params_->contrastExp = 1.0;
+		params_->gains = { { 1.0, 1.0, 1.0 } };
+		/* combinedMatrix is reset for each frame. */
 	}
 
 	{
@@ -287,6 +290,8 @@  void IPASoftSimple::computeParams(const uint32_t frame)
 	IPAFrameContext &frameContext = context_.frameContexts.get(frame);
 	for (auto const &algo : algorithms())
 		algo->prepare(context_, frame, frameContext, params_);
+	params_->combinedMatrix = context_.activeState.combinedMatrix;
+
 	setIspParams.emit();
 }
 
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index 65a1762dd..dccdd86b4 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -1,7 +1,7 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
  * Copyright (C) 2023, Linaro Ltd
- * Copyright (C) 2023-2025 Red Hat Inc.
+ * Copyright (C) 2023-2026 Red Hat Inc.
  *
  * Authors:
  * Hans de Goede <hdegoede@redhat.com>
@@ -25,99 +25,28 @@  namespace libcamera {
  */
 
 /**
- * \var DebayerParams::kRGBLookupSize
- * \brief Size of a color lookup table
+ * \var DebayerParams::gains
+ * \brief Colour channel gains
  */
 
 /**
- * \struct DebayerParams::CcmColumn
- * \brief Type of a single column of a color correction matrix (CCM)
- *
- * When multiplying an input pixel, columns in the CCM correspond to the red,
- * green or blue component of input pixel values, while rows correspond to the
- * red, green or blue components of the output pixel values. The members of the
- * CcmColumn structure are named after the colour components of the output pixel
- * values they correspond to.
- */
-
-/**
- * \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::LookupTable
- * \brief Type of the lookup tables for single lookup values
- */
-
-/**
- * \typedef DebayerParams::CcmLookupTable
- * \brief Type of the CCM lookup tables for red, green, blue values
- */
-
-/**
- * \var DebayerParams::red
- * \brief Lookup table for red color, mapping input values to output values
- */
-
-/**
- * \var DebayerParams::green
- * \brief Lookup table for green color, mapping input values to output values
- */
-
-/**
- * \var DebayerParams::blue
- * \brief Lookup table for blue color, mapping input values to output values
- */
-
-/**
- * \var DebayerParams::redCcm
- * \brief Lookup table for the CCM red column, mapping input values to output values
- */
-
-/**
- * \var DebayerParams::greenCcm
- * \brief Lookup table for the CCM green column, mapping input values to output values
- */
-
-/**
- * \var DebayerParams::blueCcm
- * \brief Lookup table for the CCM blue column, mapping input values to output values
- */
-
-/**
- * \var DebayerParams::gammaLut
- * \brief Gamma lookup table used with color correction matrix
- */
-
-/**
- * \var DebayerParams::ccm
- * \brief Per frame colour correction matrix for GPUISP
+ * \var DebayerParams::combinedMatrix
+ * \brief Colour correction matrix, including other adjustments
  */
 
 /**
  * \var DebayerParams::blackLevel
- * \brief Blacklevel gains for the GPUISP
+ * \brief Black level values
  */
 
 /**
  * \var DebayerParams::gamma
- * \brief Gamma value for the GPUISP
+ * \brief Gamma value, e.g. 1/2.2
  */
 
 /**
  * \var DebayerParams::contrastExp
- * \brief Contrast value for GPUISP
+ * \brief Contrast value to be used as an exponent
  */
 
 /**
@@ -131,13 +60,6 @@  LOG_DEFINE_CATEGORY(Debayer)
 
 Debayer::Debayer(const GlobalConfiguration &configuration) : bench_(configuration)
 {
-	/* Initialize color lookup tables */
-	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
-		red_[i] = green_[i] = blue_[i] = i;
-		redCcm_[i] = { static_cast<int16_t>(i), 0, 0 };
-		greenCcm_[i] = { 0, static_cast<int16_t>(i), 0 };
-		blueCcm_[i] = { 0, 0, static_cast<int16_t>(i) };
-	}
 }
 
 Debayer::~Debayer()
@@ -305,56 +227,6 @@  Debayer::~Debayer()
  * \brief Output size object
  */
 
-/**
- * \var Debayer::red_
- * \brief Lookup table for red channel gain and correction values
- *
- * This table provides precomputed per-pixel or per-intensity
- * correction values for the red color channel used during debayering.
- */
-
-/**
- * \var Debayer::green_
- * \brief Lookup table for green channel gain and correction values
- *
- * This table provides precomputed per-pixel or per-intensity
- * correction values for the green color channel used during debayering.
- */
-
-/**
- * \var Debayer::blue_
- * \brief Lookup table for blue channel gain and correction values
- *
- * This table provides precomputed per-pixel or per-intensity
- * correction values for the blue color channel used during debayering.
- */
-
-/**
- * \var Debayer::redCcm_
- * \brief Red channel Color Correction Matrix (CCM) lookup table
- *
- * Contains coefficients for green channel color correction.
- */
-
-/**
- * \var Debayer::greenCcm_
- * \brief Green channel Color Correction Matrix (CCM) lookup table
- *
- * Contains coefficients for green channel color correction.
- */
-
-/**
- * \var Debayer::blueCcm_
- * \brief Blue channel Color Correction Matrix (CCM) lookup table
- *
- * Contains coefficients for blue channel color correction.
- */
-
-/**
- * \var Debayer::gammaLut_
- * \brief Gamma correction lookup table
- */
-
 /**
  * \var Debayer::swapRedBlueGains_
  * \brief Flag indicating whether red and blue channel gains should be swapped
@@ -396,34 +268,6 @@  Debayer::~Debayer()
  * DebayerEGL::start.
  */
 
-/**
- * \fn void Debayer::setParams(DebayerParams &params)
- * \brief Select the bayer params to use for the next frame debayer
- * \param[in] params The parameters to be used in debayering
- */
-void Debayer::setParams(DebayerParams &params)
-{
-	green_ = params.green;
-	greenCcm_ = params.greenCcm;
-	if (swapRedBlueGains_) {
-		red_ = params.blue;
-		blue_ = params.red;
-		redCcm_ = params.blueCcm;
-		blueCcm_ = params.redCcm;
-		for (unsigned int i = 0; i < 256; i++) {
-			std::swap(redCcm_[i].r, redCcm_[i].b);
-			std::swap(greenCcm_[i].r, greenCcm_[i].b);
-			std::swap(blueCcm_[i].r, blueCcm_[i].b);
-		}
-	} else {
-		red_ = params.red;
-		blue_ = params.blue;
-		redCcm_ = params.redCcm;
-		blueCcm_ = params.blueCcm;
-	}
-	gammaLut_ = params.gammaLut;
-}
-
 /**
  * \fn void Debayer::dmaSyncBegin(DebayerParams &params)
  * \brief Common CPU/GPU Dma Sync Buffer begin
diff --git a/src/libcamera/software_isp/debayer.h b/src/libcamera/software_isp/debayer.h
index cd2db9930..652cff4cc 100644
--- a/src/libcamera/software_isp/debayer.h
+++ b/src/libcamera/software_isp/debayer.h
@@ -78,13 +78,6 @@  public:
 	Size outputSize_;
 	PixelFormat inputPixelFormat_;
 	PixelFormat outputPixelFormat_;
-	DebayerParams::LookupTable red_;
-	DebayerParams::LookupTable green_;
-	DebayerParams::LookupTable blue_;
-	DebayerParams::CcmLookupTable redCcm_;
-	DebayerParams::CcmLookupTable greenCcm_;
-	DebayerParams::CcmLookupTable blueCcm_;
-	DebayerParams::LookupTable gammaLut_;
 	bool swapRedBlueGains_;
 	Benchmark bench_;
 
@@ -92,7 +85,6 @@  private:
 	virtual Size patternSize(PixelFormat inputFormat) = 0;
 
 protected:
-	void setParams(DebayerParams &params);
 	void dmaSyncBegin(std::vector<DmaSyncer> &dmaSyncers, FrameBuffer *input, FrameBuffer *output);
 	static bool isStandardBayerOrder(BayerFormat::Order order);
 };
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 00738c56b..af7af0a8d 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -1,7 +1,7 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
  * Copyright (C) 2023, Linaro Ltd
- * Copyright (C) 2023-2025 Red Hat Inc.
+ * Copyright (C) 2023-2026 Red Hat Inc.
  *
  * Authors:
  * Hans de Goede <hdegoede@redhat.com>
@@ -68,21 +68,21 @@  DebayerCpu::~DebayerCpu() = default;
 #define GAMMA(value) \
 	*dst++ = gammaLut_[std::clamp(value, 0, static_cast<int>(gammaLut_.size()) - 1)]
 
-#define STORE_PIXEL(b_, g_, r_)                                        \
-	if constexpr (ccmEnabled) {                                    \
-		const DebayerParams::CcmColumn &blue = blueCcm_[b_];   \
-		const DebayerParams::CcmColumn &green = greenCcm_[g_]; \
-		const DebayerParams::CcmColumn &red = redCcm_[r_];     \
-		GAMMA(blue.b + green.b + red.b);                       \
-		GAMMA(blue.g + green.g + red.g);                       \
-		GAMMA(blue.r + green.r + red.r);                       \
-	} else {                                                       \
-		*dst++ = blue_[b_];                                    \
-		*dst++ = green_[g_];                                   \
-		*dst++ = red_[r_];                                     \
-	}                                                              \
-	if constexpr (addAlphaByte)                                    \
-		*dst++ = 255;                                          \
+#define STORE_PIXEL(b_, g_, r_)                         \
+	if constexpr (ccmEnabled) {                     \
+		const CcmColumn &blue = blueCcm_[b_];   \
+		const CcmColumn &green = greenCcm_[g_]; \
+		const CcmColumn &red = redCcm_[r_];     \
+		GAMMA(blue.b + green.b + red.b);        \
+		GAMMA(blue.g + green.g + red.g);        \
+		GAMMA(blue.r + green.r + red.r);        \
+	} else {                                        \
+		*dst++ = blue_[b_];                     \
+		*dst++ = green_[g_];                    \
+		*dst++ = red_[r_];                      \
+	}                                               \
+	if constexpr (addAlphaByte)                     \
+		*dst++ = 255;                           \
 	x++;
 
 /*
@@ -525,6 +525,16 @@  int DebayerCpu::configure(const StreamConfiguration &inputCfg,
 	if (ret != 0)
 		return -EINVAL;
 
+	ccmEnabled_ = ccmEnabled;
+
+	/*
+	 * Lookup tables must be initialized because the initial value is used for
+	 * the first two frames, i.e. until stats processing starts providing its
+	 * own parameters. Let's enforce recomputing lookup tables by setting the
+	 * stored last used gamma to an out-of-range value.
+	 */
+	params_.gamma = 1.0;
+
 	window_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) &
 		    ~(inputConfig_.patternSize.width - 1);
 	window_.y = ((inputCfg.size.height - outputCfg.size.height) / 2) &
@@ -740,6 +750,98 @@  void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst)
 	}
 }
 
+void DebayerCpu::updateGammaTable(DebayerParams &params)
+{
+	const RGB<float> blackLevel = params.blackLevel;
+	/* Take let's say the green channel black level */
+	const unsigned int blackIndex = blackLevel[1] * gammaTable_.size();
+	const float gamma = params.gamma;
+	const float contrastExp = params.contrastExp;
+
+	const float divisor = gammaTable_.size() - blackIndex - 1.0;
+	for (unsigned int i = blackIndex; i < gammaTable_.size(); i++) {
+		float normalized = (i - blackIndex) / divisor;
+		/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */
+		/* Apply simple S-curve */
+		if (normalized < 0.5)
+			normalized = 0.5 * std::pow(normalized / 0.5, contrastExp);
+		else
+			normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);
+		gammaTable_[i] = UINT8_MAX *
+				 std::pow(normalized, gamma);
+	}
+	/*
+	 * Due to CCM operations, the table lookup may reach indices below the black
+	 * level. Let's set the table values below black level to the minimum
+	 * non-black value to prevent problems when the minimum value is
+	 * significantly non-zero (for example, when the image should be all grey).
+	 */
+	std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex,
+		  gammaTable_[blackIndex]);
+}
+
+void DebayerCpu::updateLookupTables(DebayerParams &params)
+{
+	const bool gammaUpdateNeeded =
+		params.gamma != params_.gamma ||
+		params.blackLevel != params_.blackLevel ||
+		params.contrastExp != params_.contrastExp;
+	if (gammaUpdateNeeded)
+		updateGammaTable(params);
+
+	auto matrixChanged = [](const Matrix<float, 3, 3> &m1, const Matrix<float, 3, 3> &m2) -> bool {
+		return !std::equal(m1.data().begin(), m1.data().end(), m2.data().begin());
+	};
+	const unsigned int gammaTableSize = gammaTable_.size();
+	const double div = static_cast<double>(kRGBLookupSize) / gammaTableSize;
+	if (ccmEnabled_) {
+		if (gammaUpdateNeeded ||
+		    matrixChanged(params.combinedMatrix, params_.combinedMatrix)) {
+			auto &red = swapRedBlueGains_ ? blueCcm_ : redCcm_;
+			auto &green = greenCcm_;
+			auto &blue = swapRedBlueGains_ ? redCcm_ : blueCcm_;
+			const unsigned int redIndex = swapRedBlueGains_ ? 2 : 0;
+			const unsigned int greenIndex = 1;
+			const unsigned int blueIndex = swapRedBlueGains_ ? 0 : 2;
+			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
+				red[i].r = std::round(i * params.combinedMatrix[redIndex][0]);
+				red[i].g = std::round(i * params.combinedMatrix[greenIndex][0]);
+				red[i].b = std::round(i * params.combinedMatrix[blueIndex][0]);
+				green[i].r = std::round(i * params.combinedMatrix[redIndex][1]);
+				green[i].g = std::round(i * params.combinedMatrix[greenIndex][1]);
+				green[i].b = std::round(i * params.combinedMatrix[blueIndex][1]);
+				blue[i].r = std::round(i * params.combinedMatrix[redIndex][2]);
+				blue[i].g = std::round(i * params.combinedMatrix[greenIndex][2]);
+				blue[i].b = std::round(i * params.combinedMatrix[blueIndex][2]);
+				gammaLut_[i] = gammaTable_[i / div];
+			}
+		}
+	} else {
+		if (gammaUpdateNeeded || params.gains != params_.gains) {
+			auto &gains = params.gains;
+			auto &red = swapRedBlueGains_ ? blue_ : red_;
+			auto &green = green_;
+			auto &blue = swapRedBlueGains_ ? red_ : blue_;
+			for (unsigned int i = 0; i < kRGBLookupSize; i++) {
+				/* Apply gamma after gain! */
+				const RGB<float> lutGains = (gains * i / div).min(gammaTableSize - 1);
+				red[i] = gammaTable_[static_cast<unsigned int>(lutGains.r())];
+				green[i] = gammaTable_[static_cast<unsigned int>(lutGains.g())];
+				blue[i] = gammaTable_[static_cast<unsigned int>(lutGains.b())];
+			}
+		}
+	}
+
+	LOG(Debayer, Debug)
+		<< "Debayer parameters: blackLevel=" << params.blackLevel
+		<< "; gamma=" << params.gamma
+		<< "; contrastExp=" << params.contrastExp
+		<< "; gains=" << params.gains
+		<< "; matrix=" << params.combinedMatrix;
+
+	params_ = params;
+}
+
 void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output, DebayerParams params)
 {
 	bench_.startFrame();
@@ -748,7 +850,7 @@  void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 
 	dmaSyncBegin(dmaSyncers, input, output);
 
-	setParams(params);
+	updateLookupTables(params);
 
 	/* 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 67df2b93a..b5cbb5bd2 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -1,7 +1,7 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
  * Copyright (C) 2023, Linaro Ltd
- * Copyright (C) 2023-2025 Red Hat Inc.
+ * Copyright (C) 2023-2026 Red Hat Inc.
  *
  * Authors:
  * Hans de Goede <hdegoede@redhat.com>
@@ -18,6 +18,8 @@ 
 #include <libcamera/base/object.h>
 
 #include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/global_configuration.h"
+#include "libcamera/internal/software_isp/debayer_params.h"
 #include "libcamera/internal/software_isp/swstats_cpu.h"
 
 #include "debayer.h"
@@ -108,10 +110,32 @@  private:
 	void memcpyNextLine(const uint8_t *linePointers[]);
 	void process2(uint32_t frame, const uint8_t *src, uint8_t *dst);
 	void process4(uint32_t frame, const uint8_t *src, uint8_t *dst);
+	void updateGammaTable(DebayerParams &params);
+	void updateLookupTables(DebayerParams &params);
 
 	/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
 	static constexpr unsigned int kMaxLineBuffers = 5;
 
+	static constexpr unsigned int kRGBLookupSize = 256;
+	static constexpr unsigned int kGammaLookupSize = 1024;
+	struct CcmColumn {
+		int16_t r;
+		int16_t g;
+		int16_t b;
+	};
+	using LookupTable = std::array<uint8_t, kRGBLookupSize>;
+	using CcmLookupTable = std::array<CcmColumn, kRGBLookupSize>;
+	LookupTable red_;
+	LookupTable green_;
+	LookupTable blue_;
+	CcmLookupTable redCcm_;
+	CcmLookupTable greenCcm_;
+	CcmLookupTable blueCcm_;
+	std::array<double, kGammaLookupSize> gammaTable_;
+	LookupTable gammaLut_;
+	bool ccmEnabled_;
+	DebayerParams params_;
+
 	debayerFn debayer0_;
 	debayerFn debayer1_;
 	debayerFn debayer2_;
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 9693d7252..af04d60ca 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -475,18 +475,18 @@  void DebayerEGL::setShaderVariableValues(DebayerParams &params)
 			    << " textureUniformProjMatrix_ " << textureUniformProjMatrix_;
 
 	GLfloat ccm[9] = {
-		params.ccm[0][0],
-		params.ccm[0][1],
-		params.ccm[0][2],
-		params.ccm[1][0],
-		params.ccm[1][1],
-		params.ccm[1][2],
-		params.ccm[2][0],
-		params.ccm[2][1],
-		params.ccm[2][2],
+		params.combinedMatrix[0][0],
+		params.combinedMatrix[0][1],
+		params.combinedMatrix[0][2],
+		params.combinedMatrix[1][0],
+		params.combinedMatrix[1][1],
+		params.combinedMatrix[1][2],
+		params.combinedMatrix[2][0],
+		params.combinedMatrix[2][1],
+		params.combinedMatrix[2][2],
 	};
 	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_FALSE, ccm);
-	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.ccm;
+	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
 
 	/*
 	 * 0 = Red, 1 = Green, 2 = Blue
@@ -544,8 +544,6 @@  void DebayerEGL::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 
 	dmaSyncBegin(dmaSyncers, input, nullptr);
 
-	setParams(params);
-
 	/* Copy metadata from the input buffer */
 	FrameMetadata &metadata = output->_d()->metadata();
 	metadata.status = input->metadata().status;
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index 7ad3511db..a83986b78 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -84,23 +84,6 @@  SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 		   DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap |
 		   DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
 {
-	/*
-	 * debayerParams_ must be initialized because the initial value is used for
-	 * the first two frames, i.e. until stats processing starts providing its
-	 * own parameters.
-	 *
-	 * \todo This should be handled in the same place as the related
-	 * operations, in the IPA module.
-	 */
-	std::array<uint8_t, 256> gammaTable;
-	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];
-	}
-
 	if (!dmaHeap_.isValid()) {
 		LOG(SoftwareIsp, Error) << "Failed to create DmaBufAllocator object";
 		return;
@@ -121,8 +104,6 @@  SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 	}
 	stats->statsReady.connect(this, &SoftwareIsp::statsReady);
 
-	bool gpuIspEnabled;
-
 #if HAVE_DEBAYER_EGL
 	std::optional<std::string> softISPMode = configuration.envOption("LIBCAMERA_SOFTISP_MODE", { "software_isp", "mode" });
 	if (softISPMode) {
@@ -133,15 +114,12 @@  SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 		}
 	}
 
-	if (!softISPMode || softISPMode == "gpu") {
+	if (!softISPMode || softISPMode == "gpu")
 		debayer_ = std::make_unique<DebayerEGL>(std::move(stats), configuration);
-		gpuIspEnabled = true;
-	}
+
 #endif
-	if (!debayer_) {
+	if (!debayer_)
 		debayer_ = std::make_unique<DebayerCpu>(std::move(stats), configuration);
-		gpuIspEnabled = false;
-	}
 
 	debayer_->inputBufferReady.connect(this, &SoftwareIsp::inputReady);
 	debayer_->outputBufferReady.connect(this, &SoftwareIsp::outputReady);
@@ -173,7 +151,6 @@  SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor,
 			 sharedParams_.fd(),
 			 sensorInfo,
 			 sensor->controls(),
-			 gpuIspEnabled,
 			 ipaControls,
 			 &ccmEnabled_);
 	if (ret) {