[v3,19/21] ipa: rppx1: gsl: Add
diff mbox series

Message ID 20260918120949.191668-20-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • libcamera: rcar-gen4 + rpp-x1
Related show

Commit Message

Barnabás Pőcze Sept. 18, 2026, 12:09 p.m. UTC
From: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

Add the algorithm to the rppx1 ipa module based on the corresponding
algorithm in the rkisp1 ipa module.

Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
---
 src/ipa/rppx1/algorithms/gsl.cpp     | 151 +++++++++++++++++++++++++++
 src/ipa/rppx1/algorithms/gsl.h       |  35 +++++++
 src/ipa/rppx1/algorithms/meson.build |   1 +
 src/ipa/rppx1/params.h               |   4 +
 4 files changed, 191 insertions(+)
 create mode 100644 src/ipa/rppx1/algorithms/gsl.cpp
 create mode 100644 src/ipa/rppx1/algorithms/gsl.h

Comments

Jacopo Mondi Sept. 25, 2026, 7:51 a.m. UTC | #1
Hi Barnabás

On Fri, Sep 18, 2026 at 02:09:47PM +0200, Barnabás Pőcze wrote:
> From: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>
> Add the algorithm to the rppx1 ipa module based on the corresponding
> algorithm in the rkisp1 ipa module.
>
> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

I just noticed all these patches are missing you sign-off

> ---
>  src/ipa/rppx1/algorithms/gsl.cpp     | 151 +++++++++++++++++++++++++++
>  src/ipa/rppx1/algorithms/gsl.h       |  35 +++++++
>  src/ipa/rppx1/algorithms/meson.build |   1 +
>  src/ipa/rppx1/params.h               |   4 +
>  4 files changed, 191 insertions(+)
>  create mode 100644 src/ipa/rppx1/algorithms/gsl.cpp
>  create mode 100644 src/ipa/rppx1/algorithms/gsl.h
>
> diff --git a/src/ipa/rppx1/algorithms/gsl.cpp b/src/ipa/rppx1/algorithms/gsl.cpp
> new file mode 100644
> index 0000000000..1f2d52d338
> --- /dev/null
> +++ b/src/ipa/rppx1/algorithms/gsl.cpp
> @@ -0,0 +1,151 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * RPP-X1 Gamma Sensor Linearization control
> + */
> +
> +#include "gsl.h"
> +
> +#include <algorithm>
> +#include <array>
> +#include <cmath>
> +#include <span>
> +#include <string_view>
> +
> +#include <libcamera/base/log.h>
> +#include <libcamera/base/utils.h>
> +
> +#include "libcamera/internal/value_node.h"
> +
> +/**
> + * \file gsl.h
> + */
> +
> +namespace libcamera {
> +
> +namespace ipa::rppx1::algorithms {
> +
> +LOG_DEFINE_CATEGORY(RppX1Gsl)
> +
> +/* `LinPre1` only */
> +constexpr uint32_t kDxOffset = 8;
> +constexpr uint32_t kMaxDx = (1u << 4) - 1;
> +constexpr uint32_t kResolution = 1u << 24;
> +
> +namespace {
> +
> +[[nodiscard]]
> +bool
> +parseYLut(const ValueNode &yObject, std::string_view key, uint32_t scale,

bool parseYLut(const ValueNode &yObject, std::string_view key, uint32_t scale,

> +	  std::span<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> res)
> +{
> +	auto vals = yObject[key].get<std::vector<float>>().value_or(utils::defopt);
> +	if (vals.size() != res.size()) {
> +		LOG(RppX1Gsl, Error)
> +			<< "Invalid 'y:" << key << "': expected "

What's the "y:" for  ?

This will read "Invalid 'y:red':

is it intentional ?

> +			<< res.size() << " elements, got " << vals.size();
> +		return false;
> +	}
> +
> +	for (const auto &[i, y] : utils::enumerate(vals)) {
> +		if (y < 0 || y > 1) {

rkisp1 has integers in the tuning file. I think it's better to have
floats like you do here, but this means we can't exchange tuning data
between the two platforms.

> +			LOG(RppX1Gsl, Error)
> +				<< "Invalid 'y:" << key << "': elements must be in [0; 1]";
> +			return false;
> +		}
> +
> +		res[i] = std::lround(y * scale);
> +	}
> +
> +	return true;
> +}
> +
> +} /* namespace */
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::init
> + */
> +int GammaSensorLinearization::init([[maybe_unused]] IPAContext &context,
> +				   const ValueNode &tuningData)
> +{
> +	/*
> +	 * \todo `LinPre2` has different ranges:
> +	 *   * kDxOffset = 4
> +	 *   * kMaxDx = 7
> +	 *   * kResolution = 2^12
> +	 * Maybe this parameter could be reworked to be compatible with both.
> +	 */
> +	gammaDx_ = tuningData["x-intervals"].get<std::vector<uint8_t>>().value_or(utils::defopt);
> +	if (gammaDx_.size() != RPPX1_LIN_SAMPLE_POINTS_NUM) {
> +		LOG(RppX1Gsl, Error)
> +			<< "Invalid 'x-intervals': expected "
> +			<< RPPX1_LIN_SAMPLE_POINTS_NUM << " elements, got "
> +			<< gammaDx_.size();
> +
> +		return -EINVAL;
> +	}
> +
> +	uint32_t xSum = 0;
> +	for (const auto &x : gammaDx_) {

        for (uint8_t x : gammaDx_)
?

Or does a const & to a pod type make any difference ?

> +		if (x > kMaxDx) {
> +			LOG(RppX1Gsl, Error)
> +				<< "Invalid 'x-intervals': must be at most " << kMaxDx
> +				<< ", got " << x;
> +			return -EINVAL;
> +		}
> +
> +		xSum += 1u << (x + kDxOffset);
> +	}
> +
> +	/*
> +	 * > Typically, the accumulated sum of dx[i] should cover the complete data

Stray > at the begining of the line

> +	 * > input range of [0; 2^12) or [0; 2^24).
> +	 */
> +	if (xSum != kResolution) {
> +		LOG(RppX1Gsl, Error)
> +			<< "Invalid 'x-intervals': must cover full input range of " << kResolution
> +			<< ", got " << xSum;
> +		return -EINVAL;
> +	}

This is a stricter requirement compared to rkisp1, I wonder if we need
to be so strict

> +
> +	const ValueNode &yObject = tuningData["y"];
> +	if (!yObject.isDictionary()) {
> +		LOG(RppX1Gsl, Error)
> +			<< "Invalid 'y': must be a dictionary";
> +		return -EINVAL;
> +	}
> +
> +	if (!parseYLut(yObject, "red", kResolution - 1, curveYr_) ||
> +	    !parseYLut(yObject, "green", kResolution - 1, curveYg_) ||
> +	    !parseYLut(yObject, "blue", kResolution - 1, curveYb_))
> +		return -EINVAL;
> +
> +	return 0;
> +}
> +
> +/**
> + * \copydoc libcamera::ipa::Algorithm::prepare
> + */
> +void GammaSensorLinearization::prepare([[maybe_unused]] IPAContext &context,
> +				       const uint32_t frame,
> +				       [[maybe_unused]] IPAFrameContext &frameContext,
> +				       RppX1Params *params)
> +{
> +	if (frame > 0)
> +		return;
> +
> +	auto config = params->block<BlockType::LinPre1>();
> +	config.setEnabled(true);
> +
> +	std::copy(gammaDx_.begin(), gammaDx_.end(), config->dx);
> +	std::copy(curveYr_.begin(), curveYr_.end(), config->curve_r);
> +	std::copy(curveYg_.begin(), curveYg_.end(), config->curve_g);
> +	std::copy(curveYb_.begin(), curveYb_.end(), config->curve_b);
> +}
> +
> +REGISTER_IPA_ALGORITHM(GammaSensorLinearization, "GammaSensorLinearization")
> +
> +} /* namespace ipa::rppx1::algorithms */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/rppx1/algorithms/gsl.h b/src/ipa/rppx1/algorithms/gsl.h
> new file mode 100644
> index 0000000000..0c86fbf1ff
> --- /dev/null
> +++ b/src/ipa/rppx1/algorithms/gsl.h
> @@ -0,0 +1,35 @@
> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
> +/*
> + * Copyright (C) 2026, Ideas On Board
> + *
> + * RPP-X1 Gamma Sensor Linearization control
> + */
> +
> +#pragma once
> +
> +#include <vector>
> +
> +#include "algorithm.h"
> +
> +namespace libcamera {
> +
> +namespace ipa::rppx1::algorithms {
> +
> +class GammaSensorLinearization : public Algorithm
> +{
> +public:
> +	int init(IPAContext &context, const ValueNode &tuningData) override;
> +	void prepare(IPAContext &context, const uint32_t frame,
> +		     IPAFrameContext &frameContext,
> +		     RppX1Params *params) override;
> +
> +private:
> +	std::vector<uint8_t> gammaDx_;
> +	std::array<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> curveYr_;
> +	std::array<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> curveYg_;
> +	std::array<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> curveYb_;
> +};
> +
> +} /* namespace ipa::rppx1::algorithms */
> +
> +} /* namespace libcamera */
> diff --git a/src/ipa/rppx1/algorithms/meson.build b/src/ipa/rppx1/algorithms/meson.build
> index d93b22c634..e7d78a00d2 100644
> --- a/src/ipa/rppx1/algorithms/meson.build
> +++ b/src/ipa/rppx1/algorithms/meson.build
> @@ -6,5 +6,6 @@ rppx1_ipa_algorithms = files([
>      'blc.cpp',
>      'ccm.cpp',
>      'goc.cpp',
> +    'gsl.cpp',
>      'lux.cpp',
>  ])
> diff --git a/src/ipa/rppx1/params.h b/src/ipa/rppx1/params.h
> index b724fa3162..87a70d960f 100644
> --- a/src/ipa/rppx1/params.h
> +++ b/src/ipa/rppx1/params.h
> @@ -22,6 +22,8 @@ enum class BlockType : uint16_t {
>  	ExmPre1,
>  	GaHv,
>  	HistPost,
> +	LinPre1,
> +	LinPre2,
>  	WbMeasPost,
>  };
>
> @@ -45,6 +47,8 @@ RPPX1_DEFINE_BLOCK_TYPE(CcorPost, ccor, CCOR_POST)
>  RPPX1_DEFINE_BLOCK_TYPE(ExmPre1, exm, EXM_PRE1)
>  RPPX1_DEFINE_BLOCK_TYPE(GaHv, ga, GA_HV)
>  RPPX1_DEFINE_BLOCK_TYPE(HistPost, hist, HIST_POST)
> +RPPX1_DEFINE_BLOCK_TYPE(LinPre1, lin, LIN_PRE1)
> +RPPX1_DEFINE_BLOCK_TYPE(LinPre2, lin, LIN_PRE2)
>  RPPX1_DEFINE_BLOCK_TYPE(WbMeasPost, wbmeas, WBMEAS_POST)
>
>  struct params_traits {

Minors apart
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>

> --
> 2.55.0
>
Barnabás Pőcze Sept. 25, 2026, 1 p.m. UTC | #2
2026. 09. 25. 9:51 keltezéssel, Jacopo Mondi írta:
> Hi Barnabás
> 
> On Fri, Sep 18, 2026 at 02:09:47PM +0200, Barnabás Pőcze wrote:
>> From: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
>>
>> Add the algorithm to the rppx1 ipa module based on the corresponding
>> algorithm in the rkisp1 ipa module.
>>
>> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
> 
> I just noticed all these patches are missing you sign-off
> 
>> ---
>>   src/ipa/rppx1/algorithms/gsl.cpp     | 151 +++++++++++++++++++++++++++
>>   src/ipa/rppx1/algorithms/gsl.h       |  35 +++++++
>>   src/ipa/rppx1/algorithms/meson.build |   1 +
>>   src/ipa/rppx1/params.h               |   4 +
>>   4 files changed, 191 insertions(+)
>>   create mode 100644 src/ipa/rppx1/algorithms/gsl.cpp
>>   create mode 100644 src/ipa/rppx1/algorithms/gsl.h
>>
>> diff --git a/src/ipa/rppx1/algorithms/gsl.cpp b/src/ipa/rppx1/algorithms/gsl.cpp
>> new file mode 100644
>> index 0000000000..1f2d52d338
>> --- /dev/null
>> +++ b/src/ipa/rppx1/algorithms/gsl.cpp
>> @@ -0,0 +1,151 @@
>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
>> +/*
>> + * Copyright (C) 2026, Ideas On Board
>> + *
>> + * RPP-X1 Gamma Sensor Linearization control
>> + */
>> +
>> +#include "gsl.h"
>> +
>> +#include <algorithm>
>> +#include <array>
>> +#include <cmath>
>> +#include <span>
>> +#include <string_view>
>> +
>> +#include <libcamera/base/log.h>
>> +#include <libcamera/base/utils.h>
>> +
>> +#include "libcamera/internal/value_node.h"
>> +
>> +/**
>> + * \file gsl.h
>> + */
>> +
>> +namespace libcamera {
>> +
>> +namespace ipa::rppx1::algorithms {
>> +
>> +LOG_DEFINE_CATEGORY(RppX1Gsl)
>> +
>> +/* `LinPre1` only */
>> +constexpr uint32_t kDxOffset = 8;
>> +constexpr uint32_t kMaxDx = (1u << 4) - 1;
>> +constexpr uint32_t kResolution = 1u << 24;
>> +
>> +namespace {
>> +
>> +[[nodiscard]]
>> +bool
>> +parseYLut(const ValueNode &yObject, std::string_view key, uint32_t scale,
> 
> bool parseYLut(const ValueNode &yObject, std::string_view key, uint32_t scale,
> 
>> +	  std::span<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> res)
>> +{
>> +	auto vals = yObject[key].get<std::vector<float>>().value_or(utils::defopt);
>> +	if (vals.size() != res.size()) {
>> +		LOG(RppX1Gsl, Error)
>> +			<< "Invalid 'y:" << key << "': expected "
> 
> What's the "y:" for  ?
> 
> This will read "Invalid 'y:red':
> 
> is it intentional ?

This is supposed to mirror the rkisp1 parsing, the "y" refers to the "y"
subobject of the tuning file.

   GammaSensorLinearization:
     x-intervals: ...
     y:
       red: ...
       green: ...
       blue: ...


> 
>> +			<< res.size() << " elements, got " << vals.size();
>> +		return false;
>> +	}
>> +
>> +	for (const auto &[i, y] : utils::enumerate(vals)) {
>> +		if (y < 0 || y > 1) {
> 
> rkisp1 has integers in the tuning file. I think it's better to have
> floats like you do here, but this means we can't exchange tuning data
> between the two platforms.
> 
>> +			LOG(RppX1Gsl, Error)
>> +				<< "Invalid 'y:" << key << "': elements must be in [0; 1]";
>> +			return false;
>> +		}
>> +
>> +		res[i] = std::lround(y * scale);
>> +	}
>> +
>> +	return true;
>> +}
>> +
>> +} /* namespace */
>> +
>> +/**
>> + * \copydoc libcamera::ipa::Algorithm::init
>> + */
>> +int GammaSensorLinearization::init([[maybe_unused]] IPAContext &context,
>> +				   const ValueNode &tuningData)
>> +{
>> +	/*
>> +	 * \todo `LinPre2` has different ranges:
>> +	 *   * kDxOffset = 4
>> +	 *   * kMaxDx = 7
>> +	 *   * kResolution = 2^12
>> +	 * Maybe this parameter could be reworked to be compatible with both.
>> +	 */
>> +	gammaDx_ = tuningData["x-intervals"].get<std::vector<uint8_t>>().value_or(utils::defopt);
>> +	if (gammaDx_.size() != RPPX1_LIN_SAMPLE_POINTS_NUM) {
>> +		LOG(RppX1Gsl, Error)
>> +			<< "Invalid 'x-intervals': expected "
>> +			<< RPPX1_LIN_SAMPLE_POINTS_NUM << " elements, got "
>> +			<< gammaDx_.size();
>> +
>> +		return -EINVAL;
>> +	}
>> +
>> +	uint32_t xSum = 0;
>> +	for (const auto &x : gammaDx_) {
> 
>          for (uint8_t x : gammaDx_)
> ?
> 
> Or does a const & to a pod type make any difference ?

No.


> 
>> +		if (x > kMaxDx) {
>> +			LOG(RppX1Gsl, Error)
>> +				<< "Invalid 'x-intervals': must be at most " << kMaxDx
>> +				<< ", got " << x;
>> +			return -EINVAL;
>> +		}
>> +
>> +		xSum += 1u << (x + kDxOffset);
>> +	}
>> +
>> +	/*
>> +	 * > Typically, the accumulated sum of dx[i] should cover the complete data
> 
> Stray > at the begining of the line

The intention was to signal that this is more or less a direct quote.


> 
>> +	 * > input range of [0; 2^12) or [0; 2^24).
>> +	 */
>> +	if (xSum != kResolution) {
>> +		LOG(RppX1Gsl, Error)
>> +			<< "Invalid 'x-intervals': must cover full input range of " << kResolution
>> +			<< ", got " << xSum;
>> +		return -EINVAL;
>> +	}
> 
> This is a stricter requirement compared to rkisp1, I wonder if we need
> to be so strict

I see no reason not to be, if this turns out to be too strict, it
can always be relaxed, but it is usually harder to make it strict later.


> 
>> +
>> +	const ValueNode &yObject = tuningData["y"];
>> +	if (!yObject.isDictionary()) {
>> +		LOG(RppX1Gsl, Error)
>> +			<< "Invalid 'y': must be a dictionary";
>> +		return -EINVAL;
>> +	}
>> +
>> +	if (!parseYLut(yObject, "red", kResolution - 1, curveYr_) ||
>> +	    !parseYLut(yObject, "green", kResolution - 1, curveYg_) ||
>> +	    !parseYLut(yObject, "blue", kResolution - 1, curveYb_))
>> +		return -EINVAL;
>> +
>> +	return 0;
>> +}
>> +
>> +/**
>> + * \copydoc libcamera::ipa::Algorithm::prepare
>> + */
>> +void GammaSensorLinearization::prepare([[maybe_unused]] IPAContext &context,
>> +				       const uint32_t frame,
>> +				       [[maybe_unused]] IPAFrameContext &frameContext,
>> +				       RppX1Params *params)
>> +{
>> +	if (frame > 0)
>> +		return;
>> +
>> +	auto config = params->block<BlockType::LinPre1>();
>> +	config.setEnabled(true);
>> +
>> +	std::copy(gammaDx_.begin(), gammaDx_.end(), config->dx);
>> +	std::copy(curveYr_.begin(), curveYr_.end(), config->curve_r);
>> +	std::copy(curveYg_.begin(), curveYg_.end(), config->curve_g);
>> +	std::copy(curveYb_.begin(), curveYb_.end(), config->curve_b);
>> +}
>> +
>> +REGISTER_IPA_ALGORITHM(GammaSensorLinearization, "GammaSensorLinearization")
>> +
>> +} /* namespace ipa::rppx1::algorithms */
>> +
>> +} /* namespace libcamera */
> [...]

Patch
diff mbox series

diff --git a/src/ipa/rppx1/algorithms/gsl.cpp b/src/ipa/rppx1/algorithms/gsl.cpp
new file mode 100644
index 0000000000..1f2d52d338
--- /dev/null
+++ b/src/ipa/rppx1/algorithms/gsl.cpp
@@ -0,0 +1,151 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * RPP-X1 Gamma Sensor Linearization control
+ */
+
+#include "gsl.h"
+
+#include <algorithm>
+#include <array>
+#include <cmath>
+#include <span>
+#include <string_view>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include "libcamera/internal/value_node.h"
+
+/**
+ * \file gsl.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rppx1::algorithms {
+
+LOG_DEFINE_CATEGORY(RppX1Gsl)
+
+/* `LinPre1` only */
+constexpr uint32_t kDxOffset = 8;
+constexpr uint32_t kMaxDx = (1u << 4) - 1;
+constexpr uint32_t kResolution = 1u << 24;
+
+namespace {
+
+[[nodiscard]]
+bool
+parseYLut(const ValueNode &yObject, std::string_view key, uint32_t scale,
+	  std::span<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> res)
+{
+	auto vals = yObject[key].get<std::vector<float>>().value_or(utils::defopt);
+	if (vals.size() != res.size()) {
+		LOG(RppX1Gsl, Error)
+			<< "Invalid 'y:" << key << "': expected "
+			<< res.size() << " elements, got " << vals.size();
+		return false;
+	}
+
+	for (const auto &[i, y] : utils::enumerate(vals)) {
+		if (y < 0 || y > 1) {
+			LOG(RppX1Gsl, Error)
+				<< "Invalid 'y:" << key << "': elements must be in [0; 1]";
+			return false;
+		}
+
+		res[i] = std::lround(y * scale);
+	}
+
+	return true;
+}
+
+} /* namespace */
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int GammaSensorLinearization::init([[maybe_unused]] IPAContext &context,
+				   const ValueNode &tuningData)
+{
+	/*
+	 * \todo `LinPre2` has different ranges:
+	 *   * kDxOffset = 4
+	 *   * kMaxDx = 7
+	 *   * kResolution = 2^12
+	 * Maybe this parameter could be reworked to be compatible with both.
+	 */
+	gammaDx_ = tuningData["x-intervals"].get<std::vector<uint8_t>>().value_or(utils::defopt);
+	if (gammaDx_.size() != RPPX1_LIN_SAMPLE_POINTS_NUM) {
+		LOG(RppX1Gsl, Error)
+			<< "Invalid 'x-intervals': expected "
+			<< RPPX1_LIN_SAMPLE_POINTS_NUM << " elements, got "
+			<< gammaDx_.size();
+
+		return -EINVAL;
+	}
+
+	uint32_t xSum = 0;
+	for (const auto &x : gammaDx_) {
+		if (x > kMaxDx) {
+			LOG(RppX1Gsl, Error)
+				<< "Invalid 'x-intervals': must be at most " << kMaxDx
+				<< ", got " << x;
+			return -EINVAL;
+		}
+
+		xSum += 1u << (x + kDxOffset);
+	}
+
+	/*
+	 * > Typically, the accumulated sum of dx[i] should cover the complete data
+	 * > input range of [0; 2^12) or [0; 2^24).
+	 */
+	if (xSum != kResolution) {
+		LOG(RppX1Gsl, Error)
+			<< "Invalid 'x-intervals': must cover full input range of " << kResolution
+			<< ", got " << xSum;
+		return -EINVAL;
+	}
+
+	const ValueNode &yObject = tuningData["y"];
+	if (!yObject.isDictionary()) {
+		LOG(RppX1Gsl, Error)
+			<< "Invalid 'y': must be a dictionary";
+		return -EINVAL;
+	}
+
+	if (!parseYLut(yObject, "red", kResolution - 1, curveYr_) ||
+	    !parseYLut(yObject, "green", kResolution - 1, curveYg_) ||
+	    !parseYLut(yObject, "blue", kResolution - 1, curveYb_))
+		return -EINVAL;
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void GammaSensorLinearization::prepare([[maybe_unused]] IPAContext &context,
+				       const uint32_t frame,
+				       [[maybe_unused]] IPAFrameContext &frameContext,
+				       RppX1Params *params)
+{
+	if (frame > 0)
+		return;
+
+	auto config = params->block<BlockType::LinPre1>();
+	config.setEnabled(true);
+
+	std::copy(gammaDx_.begin(), gammaDx_.end(), config->dx);
+	std::copy(curveYr_.begin(), curveYr_.end(), config->curve_r);
+	std::copy(curveYg_.begin(), curveYg_.end(), config->curve_g);
+	std::copy(curveYb_.begin(), curveYb_.end(), config->curve_b);
+}
+
+REGISTER_IPA_ALGORITHM(GammaSensorLinearization, "GammaSensorLinearization")
+
+} /* namespace ipa::rppx1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rppx1/algorithms/gsl.h b/src/ipa/rppx1/algorithms/gsl.h
new file mode 100644
index 0000000000..0c86fbf1ff
--- /dev/null
+++ b/src/ipa/rppx1/algorithms/gsl.h
@@ -0,0 +1,35 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Ideas On Board
+ *
+ * RPP-X1 Gamma Sensor Linearization control
+ */
+
+#pragma once
+
+#include <vector>
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rppx1::algorithms {
+
+class GammaSensorLinearization : public Algorithm
+{
+public:
+	int init(IPAContext &context, const ValueNode &tuningData) override;
+	void prepare(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     RppX1Params *params) override;
+
+private:
+	std::vector<uint8_t> gammaDx_;
+	std::array<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> curveYr_;
+	std::array<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> curveYg_;
+	std::array<uint32_t, RPPX1_LIN_DEGAMMA_CURVE_NUM> curveYb_;
+};
+
+} /* namespace ipa::rppx1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rppx1/algorithms/meson.build b/src/ipa/rppx1/algorithms/meson.build
index d93b22c634..e7d78a00d2 100644
--- a/src/ipa/rppx1/algorithms/meson.build
+++ b/src/ipa/rppx1/algorithms/meson.build
@@ -6,5 +6,6 @@  rppx1_ipa_algorithms = files([
     'blc.cpp',
     'ccm.cpp',
     'goc.cpp',
+    'gsl.cpp',
     'lux.cpp',
 ])
diff --git a/src/ipa/rppx1/params.h b/src/ipa/rppx1/params.h
index b724fa3162..87a70d960f 100644
--- a/src/ipa/rppx1/params.h
+++ b/src/ipa/rppx1/params.h
@@ -22,6 +22,8 @@  enum class BlockType : uint16_t {
 	ExmPre1,
 	GaHv,
 	HistPost,
+	LinPre1,
+	LinPre2,
 	WbMeasPost,
 };
 
@@ -45,6 +47,8 @@  RPPX1_DEFINE_BLOCK_TYPE(CcorPost, ccor, CCOR_POST)
 RPPX1_DEFINE_BLOCK_TYPE(ExmPre1, exm, EXM_PRE1)
 RPPX1_DEFINE_BLOCK_TYPE(GaHv, ga, GA_HV)
 RPPX1_DEFINE_BLOCK_TYPE(HistPost, hist, HIST_POST)
+RPPX1_DEFINE_BLOCK_TYPE(LinPre1, lin, LIN_PRE1)
+RPPX1_DEFINE_BLOCK_TYPE(LinPre2, lin, LIN_PRE2)
 RPPX1_DEFINE_BLOCK_TYPE(WbMeasPost, wbmeas, WBMEAS_POST)
 
 struct params_traits {