[libcamera-devel,RFC,v2,4/7] libcamera: ipa: Soft IPA: add a Soft IPA implementation
diff mbox series

Message ID 20231212115046.102726-5-andrey.konovalov@linaro.org
State Superseded
Headers show
Series
  • libcamera: introduce Software ISP and Software IPA
Related show

Commit Message

Andrey Konovalov Dec. 12, 2023, 11:50 a.m. UTC
Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
---
 .../internal/software_isp/meson.build         |   5 +
 .../internal/software_isp/statistics-linaro.h |  17 ++
 meson_options.txt                             |   3 +-
 src/ipa/simple/linaro/data/meson.build        |   9 +
 src/ipa/simple/linaro/data/soft.conf          |   3 +
 src/ipa/simple/linaro/meson.build             |  26 +++
 src/ipa/simple/linaro/soft_linaro.cpp         | 210 ++++++++++++++++++
 src/ipa/simple/meson.build                    |   9 +
 8 files changed, 281 insertions(+), 1 deletion(-)
 create mode 100644 include/libcamera/internal/software_isp/meson.build
 create mode 100644 include/libcamera/internal/software_isp/statistics-linaro.h
 create mode 100644 src/ipa/simple/linaro/data/meson.build
 create mode 100644 src/ipa/simple/linaro/data/soft.conf
 create mode 100644 src/ipa/simple/linaro/meson.build
 create mode 100644 src/ipa/simple/linaro/soft_linaro.cpp

Comments

Pavel Machek Dec. 12, 2023, 3:51 p.m. UTC | #1
Hi!

> Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>

I'm not sure if we should call implementation "Linaro". Company name
seems misleading in this context. Could we call implementation "anna",
for example, after https://en.wikipedia.org/wiki/Anna_Atkins and
because it is first version, thus "A"?

1-4 Reviewed-by: Pavel Machek <pavel@ucw.cz>

Best regards,
							Pavel
Kieran Bingham Dec. 12, 2023, 4:04 p.m. UTC | #2
Quoting Pavel Machek via libcamera-devel (2023-12-12 15:51:10)
> Hi!
> 
> > Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
> 
> I'm not sure if we should call implementation "Linaro". Company name
> seems misleading in this context. Could we call implementation "anna",
> for example, after https://en.wikipedia.org/wiki/Anna_Atkins and
> because it is first version, thus "A"?

I'd agree here ... I can't imagine libcamera want's "5 different
competing softISP implementations.... all in the same code base"

This is just 'SoftISP' in my opinion.

--
Kieran


> 
> 1-4 Reviewed-by: Pavel Machek <pavel@ucw.cz>
> 
> Best regards,
>                                                         Pavel
> -- 
> People of Russia, stop Putin before his war on Ukraine escalates.
Andrey Konovalov Dec. 12, 2023, 4:27 p.m. UTC | #3
On 12.12.2023 19:04, Kieran Bingham wrote:
> Quoting Pavel Machek via libcamera-devel (2023-12-12 15:51:10)
>> Hi!
>>
>>> Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
>>
>> I'm not sure if we should call implementation "Linaro". Company name
>> seems misleading in this context. Could we call implementation "anna",
>> for example, after https://en.wikipedia.org/wiki/Anna_Atkins and
>> because it is first version, thus "A"?
> 
> I'd agree here ... I can't imagine libcamera want's "5 different
> competing softISP implementations.... all in the same code base"
> 
> This is just 'SoftISP' in my opinion.

Then SwIspLinaro could be merged into abstract SoftwareIsp,
the result being called just SoftwareIsp.
And SoftwareIspFactory* are not needed.
Correct?

Thanks,
Andrey

> --
> Kieran
> 
> 
>>
>> 1-4 Reviewed-by: Pavel Machek <pavel@ucw.cz>
>>
>> Best regards,
>>                                                          Pavel
>> -- 
>> People of Russia, stop Putin before his war on Ukraine escalates.
Kieran Bingham Dec. 12, 2023, 4:35 p.m. UTC | #4
Quoting Andrey Konovalov (2023-12-12 16:27:49)
> On 12.12.2023 19:04, Kieran Bingham wrote:
> > Quoting Pavel Machek via libcamera-devel (2023-12-12 15:51:10)
> >> Hi!
> >>
> >>> Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
> >>
> >> I'm not sure if we should call implementation "Linaro". Company name
> >> seems misleading in this context. Could we call implementation "anna",
> >> for example, after https://en.wikipedia.org/wiki/Anna_Atkins and
> >> because it is first version, thus "A"?
> > 
> > I'd agree here ... I can't imagine libcamera want's "5 different
> > competing softISP implementations.... all in the same code base"
> > 
> > This is just 'SoftISP' in my opinion.
> 
> Then SwIspLinaro could be merged into abstract SoftwareIsp,
> the result being called just SoftwareIsp.
> And SoftwareIspFactory* are not needed.
> Correct?

Has a SoftwareISPFactory requirement been discussed somewhere that I
haven't been involved in ?

--
Kieran


> 
> Thanks,
> Andrey
> 
> > --
> > Kieran
> > 
> > 
> >>
> >> 1-4 Reviewed-by: Pavel Machek <pavel@ucw.cz>
> >>
> >> Best regards,
> >>                                                          Pavel
> >> -- 
> >> People of Russia, stop Putin before his war on Ukraine escalates.
Pavel Machek Dec. 12, 2023, 4:37 p.m. UTC | #5
Hi!

> > >>> Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
> > >>
> > >> I'm not sure if we should call implementation "Linaro". Company name
> > >> seems misleading in this context. Could we call implementation "anna",
> > >> for example, after https://en.wikipedia.org/wiki/Anna_Atkins and
> > >> because it is first version, thus "A"?
> > > 
> > > I'd agree here ... I can't imagine libcamera want's "5 different
> > > competing softISP implementations.... all in the same code base"
> > > 
> > > This is just 'SoftISP' in my opinion.
> > 
> > Then SwIspLinaro could be merged into abstract SoftwareIsp,
> > the result being called just SoftwareIsp.
> > And SoftwareIspFactory* are not needed.
> > Correct?
> 
> Has a SoftwareISPFactory requirement been discussed somewhere that I
> haven't been involved in ?

One day we may have SwIspGPU or maybe SwIspIntelMMX, so that support
might be worth keeping.

Best regards,
								Pavel
Kieran Bingham Dec. 12, 2023, 4:43 p.m. UTC | #6
Quoting Pavel Machek (2023-12-12 16:37:09)
> Hi!
> 
> > > >>> Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
> > > >>
> > > >> I'm not sure if we should call implementation "Linaro". Company name
> > > >> seems misleading in this context. Could we call implementation "anna",
> > > >> for example, after https://en.wikipedia.org/wiki/Anna_Atkins and
> > > >> because it is first version, thus "A"?
> > > > 
> > > > I'd agree here ... I can't imagine libcamera want's "5 different
> > > > competing softISP implementations.... all in the same code base"
> > > > 
> > > > This is just 'SoftISP' in my opinion.
> > > 
> > > Then SwIspLinaro could be merged into abstract SoftwareIsp,
> > > the result being called just SoftwareIsp.
> > > And SoftwareIspFactory* are not needed.
> > > Correct?
> > 
> > Has a SoftwareISPFactory requirement been discussed somewhere that I
> > haven't been involved in ?
> 
> One day we may have SwIspGPU or maybe SwIspIntelMMX, so that support
> might be worth keeping.

I envisioned that a libcamera software ISP should detect the
capabilities of the system it's on and construct a pipeline accordingly.

I guess there might be some scenarios where specific control over which
parts can be used might want to be controlled or configured though.



> 
> Best regards,
>                                                                 Pavel
> -- 
> People of Russia, stop Putin before his war on Ukraine escalates.
Hans de Goede Dec. 12, 2023, 9:38 p.m. UTC | #7
Hi,

On 12/12/23 17:37, Pavel Machek via libcamera-devel wrote:
> Hi!
> 
>>>>>> Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
>>>>>
>>>>> I'm not sure if we should call implementation "Linaro". Company name
>>>>> seems misleading in this context. Could we call implementation "anna",
>>>>> for example, after https://en.wikipedia.org/wiki/Anna_Atkins and
>>>>> because it is first version, thus "A"?
>>>>
>>>> I'd agree here ... I can't imagine libcamera want's "5 different
>>>> competing softISP implementations.... all in the same code base"
>>>>
>>>> This is just 'SoftISP' in my opinion.
>>>
>>> Then SwIspLinaro could be merged into abstract SoftwareIsp,
>>> the result being called just SoftwareIsp.
>>> And SoftwareIspFactory* are not needed.
>>> Correct?
>>
>> Has a SoftwareISPFactory requirement been discussed somewhere that I
>> haven't been involved in ?
> 
> One day we may have SwIspGPU or maybe SwIspIntelMMX, so that support
> might be worth keeping.

Right, so about having multiple SoftwareIsp implementations.

I have been working on stasistics + debayer
base classes and CPU implementations of both based on refactoring
Andrey's work.

I was hoping to have a clean branch with these ready but
getting them in shape took longer then I hoped.

Stil I would like to share what I have now before tomorrows
SoftwareIsp video call / meeting.

ATM the statistics and debayer classes are mostly ready,
but I need to merge this with Andrey's v2 RFC and
then produce a clean new series from this
(assuming people like the approach).

The base idea behind this is that we can have multiple
statistics and debayer implementations, e.g. CPU vs GPU
and that the SoftwareIsp class picks which one to
instantiate.  The current DebayerCpu class mostly 
replaces the IspWorker class making the SoftwareIsp
code quite a bit simpler (note that the code is mostly
moved not gone).

Anyways if you have time, please take a look
at the swstats base class + linaro implementation
as well as at the debayer base class as well
as the DebayerCpu implementation of that:

https://gitlab.freedesktop.org/camera/libcamera-softisp/-/tree/SoftwareISP-v02-hans2

Which are the last few commits there.

Note as I already mentioned I really need to merge
this with Andrey's latest v03 branch into a new
logical clean patch series suitable for upstream submission.

Regards,

Hans









> 
> Best regards,
> 								Pavel
Pavel Machek Dec. 12, 2023, 10:26 p.m. UTC | #8
Hi!

> > One day we may have SwIspGPU or maybe SwIspIntelMMX, so that support
> > might be worth keeping.
> 
> Right, so about having multiple SoftwareIsp implementations.
> 
> I have been working on stasistics + debayer
> base classes and CPU implementations of both based on refactoring
> Andrey's work.
> 
> I was hoping to have a clean branch with these ready but
> getting them in shape took longer then I hoped.
> 
> Stil I would like to share what I have now before tomorrows
> SoftwareIsp video call / meeting.

Thank you, already noticed new branch in git and was browsing :-).

Is there way I could join the video call?

Best regards,
								Pavel
Pavel Machek Dec. 13, 2023, 7:39 a.m. UTC | #9
Hi!

> ATM the statistics and debayer classes are mostly ready,
> but I need to merge this with Andrey's v2 RFC and
> then produce a clean new series from this
> (assuming people like the approach).

Ok, what about this one? I only converted half of the code, but the
other half of conversion should be very similar.

Compile-tested only as my sensor is bayer 8.

It makes it easier to see how we are skipping the low bits in 10P
bayer, and will make bayer 8 support easier, too, since the pixel_XXX
helpers are designed to be shared.

Best regards,
								Pavel

diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index 97ab6c14..a340e500 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -29,13 +29,37 @@ DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsLinaro> stats)
 		green_[i] = i;
 }
 
+struct ctxt {
+	/* Pointers to previous, current and next lines */
+	const uint8_t *prev;
+	const uint8_t *curr;
+	const uint8_t *next;
+
+	/* Pointers to gamma correction tables */
+	const uint8_t *red;
+	const uint8_t *green;
+	const uint8_t *blue;
+};
+  
+static inline void pixel_bggr(const struct ctxt &c, uint8_t *&dst, int x, int p, int n)
+{
+	*dst++ = c.blue[c.curr[x]];
+	*dst++ = c.green[(c.prev[x] + c.curr[x - p] + c.curr[x + p] + c.next[x]) / 4];
+	*dst++ = c.red[(c.prev[x - p] + c.prev[x + n] + c.next[x - p]  + c.next[x + n]) / 4];
+}
+
+static inline void pixel_gbrg(const struct ctxt &c, uint8_t *&dst, int x, int p, int n)
+{
+	*dst++ = c.blue[(c.curr[x - p] + c.curr[x + n]) / 2];
+	*dst++ = c.green[c.curr[x]];
+	*dst++ = c.red[(c.prev[x] + c.next[x]) / 2];
+}
+
 void DebayerCpu::debayerBGGR10PLine0(uint8_t *dst, const uint8_t *src)
 {
 	const int width_in_bytes = window_.width * 5 / 4;
-	/* Pointers to previous, current and next lines */
-	const uint8_t *prev = src - inputStride_;
-	const uint8_t *curr = src;
-	const uint8_t *next = src + inputStride_;
+	struct ctxt c = { src - inputStride_, src, src + inputStride_,
+			  red_, green_, blue_ };
 
 	/*
 	 * For the first pixel getting a pixel from the previous column uses
@@ -49,9 +73,7 @@ void DebayerCpu::debayerBGGR10PLine0(uint8_t *dst, const uint8_t *src)
 		 *                       RGR
 		 * Write BGR
 		 */
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 2] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 2] + prev[x + 1] + next[x - 2]  + next[x + 1]) / 4];
+		pixel_bggr(c, dst, x, 2, 1);
 		x++;
 
 		/*
@@ -60,20 +82,14 @@ void DebayerCpu::debayerBGGR10PLine0(uint8_t *dst, const uint8_t *src)
 		 *                      GRG
 		 * Write BGR
 		 */
-		*dst++ = blue_[(curr[x - 1] + curr[x + 1]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
+		pixel_gbrg(c, dst, x, 1, 1);
 		x++;
 
 		/* Same thing for next 2 pixels */
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 1] + prev[x + 1] + next[x - 1]  + next[x + 1]) / 4];
+		pixel_bggr(c, dst, x, 1, 1);
 		x++;
 
-		*dst++ = blue_[(curr[x - 1] + curr[x + 2]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
+		pixel_gbrg(c, dst, x, 1, 2);
 	}
 }
 
@@ -123,10 +139,8 @@ void DebayerCpu::debayerBGGR10PLine1(uint8_t *dst, const uint8_t *src)
 void DebayerCpu::debayerGBRG10PLine0(uint8_t *dst, const uint8_t *src)
 {
 	const int width_in_bytes = window_.width * 5 / 4;
-	/* Pointers to previous, current and next lines */
-	const uint8_t *prev = src - inputStride_;
-	const uint8_t *curr = src;
-	const uint8_t *next = src + inputStride_;
+	struct ctxt c = { src - inputStride_, src, src + inputStride_,
+			  red_, green_, blue_ };
 
 	for (int x = 0; x < width_in_bytes; x += 2) {
 		/*
@@ -135,9 +149,7 @@ void DebayerCpu::debayerGBRG10PLine0(uint8_t *dst, const uint8_t *src)
 		 *                       GRG
 		 * Write BGR
 		 */
-		*dst++ = blue_[(curr[x - 2] + curr[x + 1]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
+		pixel_gbrg(c, dst, x, 2, 1);
 		x++;
 
 		/*
@@ -146,20 +158,14 @@ void DebayerCpu::debayerGBRG10PLine0(uint8_t *dst, const uint8_t *src)
 		 *                      RGR
 		 * Write BGR
 		 */
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 1] + prev[x + 1] + next[x - 1]  + next[x + 1]) / 4];
+		pixel_bggr(c, dst, x, 1, 1);
 		x++;
 
 		/* Same thing for next 2 pixels */
-		*dst++ = blue_[(curr[x - 1] + curr[x + 1]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
+		pixel_gbrg(c, dst, x, 1, 1);
 		x++;
 
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 2] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 1] + prev[x + 2] + next[x - 1]  + next[x + 2]) / 4];
+		pixel_bggr(c, dst, x, 1, 2);
 	}
 }
Bryan O'Donoghue Dec. 13, 2023, 9:51 a.m. UTC | #10
On 12/12/2023 22:26, Pavel Machek wrote:
> Hi!
> 
>>> One day we may have SwIspGPU or maybe SwIspIntelMMX, so that support
>>> might be worth keeping.
>>
>> Right, so about having multiple SoftwareIsp implementations.
>>
>> I have been working on stasistics + debayer
>> base classes and CPU implementations of both based on refactoring
>> Andrey's work.
>>
>> I was hoping to have a clean branch with these ready but
>> getting them in shape took longer then I hoped.
>>
>> Stil I would like to share what I have now before tomorrows
>> SoftwareIsp video call / meeting.
> 
> Thank you, already noticed new branch in git and was browsing :-).
> 
> Is there way I could join the video call?
> 
> Best regards,
> 								Pavel

Added you to the invitee list.

---
bod
Hans de Goede Dec. 13, 2023, 8:35 p.m. UTC | #11
Hi Pavel,

On 12/13/23 08:39, Pavel Machek wrote:
> Hi!
> 
>> ATM the statistics and debayer classes are mostly ready,
>> but I need to merge this with Andrey's v2 RFC and
>> then produce a clean new series from this
>> (assuming people like the approach).
> 
> Ok, what about this one? I only converted half of the code, but the
> other half of conversion should be very similar.
> 
> Compile-tested only as my sensor is bayer 8.
> 
> It makes it easier to see how we are skipping the low bits in 10P
> bayer, and will make bayer 8 support easier, too, since the pixel_XXX
> helpers are designed to be shared.

I have merged a variant of this with:

1. The missing bits completed
2. Various renames to make it clear that the helpers are for 8bit bayer
   sources (we are treating 10bpp packed as 8 bit here) and that
   the output is bgr888 this should make life easier when adding
   support for more input / output fmts.

Here is what I've merged / squashed:

From 9e7bd0f37c38bf9500924187720621538f4581da Mon Sep 17 00:00:00 2001
From: Hans de Goede <hdegoede@redhat.com>
Date: Wed, 13 Dec 2023 19:19:11 +0100
Subject: [PATCH] Pavel debayer changes

Signed-off-by: Hans de Goede <hdegoede@redhat.com>
---
 .../internal/software_isp/debayer_cpu.h       |   8 +-
 src/libcamera/software_isp/debayer_cpu.cpp    | 247 +++++++-----------
 2 files changed, 97 insertions(+), 158 deletions(-)

diff --git a/include/libcamera/internal/software_isp/debayer_cpu.h b/include/libcamera/internal/software_isp/debayer_cpu.h
index 89824851..7fb5be77 100644
--- a/include/libcamera/internal/software_isp/debayer_cpu.h
+++ b/include/libcamera/internal/software_isp/debayer_cpu.h
@@ -62,10 +62,10 @@ private:
 	void process2(const uint8_t *src, uint8_t *dst);
 	void process4(const uint8_t *src, uint8_t *dst);
 	/* CSI-2 packed 10-bit raw bayer format (all the 4 orders) */
-	void debayerBGGR10PLine0(uint8_t *dst, const uint8_t *src);
-	void debayerBGGR10PLine1(uint8_t *dst, const uint8_t *src);
-	void debayerGBRG10PLine0(uint8_t *dst, const uint8_t *src);
-	void debayerGBRG10PLine1(uint8_t *dst, const uint8_t *src);
+	void debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src);
+	void debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src);
+	void debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src);
+	void debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src);
 
 	typedef void (DebayerCpu::*debayerFn)(uint8_t *dst, const uint8_t *src);
 
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index b6ed12de..3eacdd5d 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -29,180 +29,121 @@ DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)
 		green_[i] = i;
 }
 
-void DebayerCpu::debayerBGGR10PLine0(uint8_t *dst, const uint8_t *src)
+struct ctxt_8bit_src {
+	/* Pointers to previous, current and next lines */
+	const uint8_t *prev;
+	const uint8_t *curr;
+	const uint8_t *next;
+
+	/* Pointers to gamma correction tables */
+	const uint8_t *red;
+	const uint8_t *green;
+	const uint8_t *blue;
+};
+
+static inline void bggr8_bgr888(const struct ctxt_8bit_src &c, uint8_t *&dst, int x, int p, int n)
+{
+	*dst++ = c.blue[c.curr[x]];
+	*dst++ = c.green[(c.prev[x] + c.curr[x - p] + c.curr[x + n] + c.next[x]) / 4];
+	*dst++ = c.red[(c.prev[x - p] + c.prev[x + n] + c.next[x - p]  + c.next[x + n]) / 4];
+}
+
+static inline void grbg8_bgr888(const struct ctxt_8bit_src &c, uint8_t *&dst, int x, int p, int n)
+{
+	*dst++ = c.blue[(c.prev[x] + c.next[x]) / 2];
+	*dst++ = c.green[c.curr[x]];
+	*dst++ = c.red[(c.curr[x - p] + c.curr[x + n]) / 2];
+}
+
+static inline void gbrg8_bgr888(const struct ctxt_8bit_src &c, uint8_t *&dst, int x, int p, int n)
+{
+	*dst++ = c.blue[(c.curr[x - p] + c.curr[x + n]) / 2];
+	*dst++ = c.green[c.curr[x]];
+	*dst++ = c.red[(c.prev[x] + c.next[x]) / 2];
+}
+
+static inline void rggb8_bgr888(const struct ctxt_8bit_src &c, uint8_t *&dst, int x, int p, int n)
+{
+	*dst++ = c.blue[(c.prev[x - p] + c.prev[x + n] + c.next[x - p]  + c.next[x + n]) / 4];
+	*dst++ = c.green[(c.prev[x] + c.curr[x - p] + c.curr[x + n] + c.next[x]) / 4];
+	*dst++ = c.red[c.curr[x]];
+}
+
+void DebayerCpu::debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src)
 {
 	const int width_in_bytes = window_.width * 5 / 4;
-	/* Pointers to previous, current and next lines */
-	const uint8_t *prev = src - inputConfig_.stride;
-	const uint8_t *curr = src;
-	const uint8_t *next = src + inputConfig_.stride;
+	struct ctxt_8bit_src c = {
+		src - inputConfig_.stride, src, src + inputConfig_.stride,
+		red_, green_, blue_ };
 
 	/*
 	 * For the first pixel getting a pixel from the previous column uses
 	 * x - 2 to skip the 5th byte with least-significant bits for 4 pixels.
 	 * Same for last pixel (uses x + 2) and looking at the next column.
+	 * x++ in the for-loop skips the 5th byte with 4 x 2 lsb-s for 10bit packed.
 	 */
-	for (int x = 0; x < width_in_bytes; x += 2) {
-		/*
-		 * BGBG line even pixel: RGR
-		 *                       GBG
-		 *                       RGR
-		 * Write BGR
-		 */
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 2] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 2] + prev[x + 1] + next[x - 2]  + next[x + 1]) / 4];
-		x++;
-
-		/*
-		 * BGBG line odd pixel: GRG
-		 *                      BGB
-		 *                      GRG
-		 * Write BGR
-		 */
-		*dst++ = blue_[(curr[x - 1] + curr[x + 1]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
-		x++;
-
+	for (int x = 0; x < width_in_bytes; x++) {
+		/* Even pixel */
+		bggr8_bgr888(c, dst, x++, 2, 1);
+		/* Odd pixel BGGR -> GBRG */
+		gbrg8_bgr888(c, dst, x++, 1, 1);
 		/* Same thing for next 2 pixels */
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 1] + prev[x + 1] + next[x - 1]  + next[x + 1]) / 4];
-		x++;
-
-		*dst++ = blue_[(curr[x - 1] + curr[x + 2]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
+		bggr8_bgr888(c, dst, x++, 1, 1);
+		gbrg8_bgr888(c, dst, x++, 1, 2);
 	}
 }
 
-void DebayerCpu::debayerBGGR10PLine1(uint8_t *dst, const uint8_t *src)
+void DebayerCpu::debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src)
 {
 	const int width_in_bytes = window_.width * 5 / 4;
-	/* Pointers to previous, current and next lines */
-	const uint8_t *prev = src - inputConfig_.stride;
-	const uint8_t *curr = src;
-	const uint8_t *next = src + inputConfig_.stride;
-
-	for (int x = 0; x < width_in_bytes; x += 2) {
-		/*
-		 * GRGR line even pixel: GBG
-		 *                       RGR
-		 *                       GBG
-		 * Write BGR
-		 */
-		*dst++ = blue_[(prev[x] + next[x]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(curr[x - 2] + curr[x + 1]) / 2];
-		x++;
-
-		/*
-		 * GRGR line odd pixel: BGB
-		 *                      GRG
-		 *                      BGB
-		 * Write BGR
-		 */
-		*dst++ = blue_[(prev[x - 1] + prev[x + 1] + next[x - 1] + next[x + 1]) / 4];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[curr[x]];
-		x++;
+	struct ctxt_8bit_src c = {
+		src - inputConfig_.stride, src, src + inputConfig_.stride,
+		red_, green_, blue_ };
 
+	for (int x = 0; x < width_in_bytes; x++) {
+		/* Even pixel */
+		grbg8_bgr888(c, dst, x++, 2, 1);
+		/* Odd pixel GRBG -> RGGB */
+		rggb8_bgr888(c, dst, x++, 1, 1);
 		/* Same thing for next 2 pixels */
-		*dst++ = blue_[(prev[x] + next[x]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(curr[x - 1] + curr[x + 1]) / 2];
-		x++;
-
-		*dst++ = blue_[(prev[x - 1] + prev[x + 2] + next[x - 1] + next[x + 2]) / 4];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 2] + next[x]) / 4];
-		*dst++ = red_[curr[x]];
+		grbg8_bgr888(c, dst, x++, 1, 1);
+		rggb8_bgr888(c, dst, x++, 1, 2);
 	}
 }
 
-void DebayerCpu::debayerGBRG10PLine0(uint8_t *dst, const uint8_t *src)
+void DebayerCpu::debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src)
 {
 	const int width_in_bytes = window_.width * 5 / 4;
-	/* Pointers to previous, current and next lines */
-	const uint8_t *prev = src - inputConfig_.stride;
-	const uint8_t *curr = src;
-	const uint8_t *next = src + inputConfig_.stride;
-
-	for (int x = 0; x < width_in_bytes; x += 2) {
-		/*
-		 * GBGB line even pixel: GRG
-		 *                       BGB
-		 *                       GRG
-		 * Write BGR
-		 */
-		*dst++ = blue_[(curr[x - 2] + curr[x + 1]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
-		x++;
-
-		/*
-		 * GBGB line odd pixel: RGR
-		 *                      GBG
-		 *                      RGR
-		 * Write BGR
-		 */
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 1] + prev[x + 1] + next[x - 1]  + next[x + 1]) / 4];
-		x++;
+	struct ctxt_8bit_src c = {
+		src - inputConfig_.stride, src, src + inputConfig_.stride,
+		red_, green_, blue_ };
 
+	for (int x = 0; x < width_in_bytes; x++) {
+		/* Even pixel */
+		gbrg8_bgr888(c, dst, x++, 2, 1);
+		/* Odd pixel GBGR -> BGGR */
+		bggr8_bgr888(c, dst, x++, 1, 1);
 		/* Same thing for next 2 pixels */
-		*dst++ = blue_[(curr[x - 1] + curr[x + 1]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(prev[x] + next[x]) / 2];
-		x++;
-
-		*dst++ = blue_[curr[x]];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 2] + next[x]) / 4];
-		*dst++ = red_[(prev[x - 1] + prev[x + 2] + next[x - 1]  + next[x + 2]) / 4];
+		gbrg8_bgr888(c, dst, x++, 1, 1);
+		bggr8_bgr888(c, dst, x++, 1, 2);
 	}
 }
 
-void DebayerCpu::debayerGBRG10PLine1(uint8_t *dst, const uint8_t *src)
+void DebayerCpu::debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src)
 {
 	const int width_in_bytes = window_.width * 5 / 4;
-	/* Pointers to previous, current and next lines */
-	const uint8_t *prev = src - inputConfig_.stride;
-	const uint8_t *curr = src;
-	const uint8_t *next = src + inputConfig_.stride;
-
-	for (int x = 0; x < width_in_bytes; x += 2) {
-		/*
-		 * RGRG line even pixel: BGB
-		 *                       GRG
-		 *                       BGB
-		 * Write BGR
-		 */
-		*dst++ = blue_[(prev[x - 2] + prev[x + 1] + next[x - 2] + next[x + 1]) / 4];
-		*dst++ = green_[(prev[x] + curr[x - 2] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[curr[x]];
-		x++;
-
-		/*
-		 * RGRG line odd pixel: GBG
-		 *                      RGR
-		 *                      GBG
-		 * Write BGR
-		 */
-		*dst++ = blue_[(prev[x] + next[x]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(curr[x - 1] + curr[x + 1]) / 2];
-		x++;
+	struct ctxt_8bit_src c = {
+		src - inputConfig_.stride, src, src + inputConfig_.stride,
+		red_, green_, blue_ };
 
+	for (int x = 0; x < width_in_bytes; x++) {
+		/* Even pixel */
+		rggb8_bgr888(c, dst, x++, 2, 1);
+		/* Odd pixel RGGB -> GRBG*/
+		grbg8_bgr888(c, dst, x++, 1, 1);
 		/* Same thing for next 2 pixels */
-		*dst++ = blue_[(prev[x - 1] + prev[x + 1] + next[x - 1] + next[x + 1]) / 4];
-		*dst++ = green_[(prev[x] + curr[x - 1] + curr[x + 1] + next[x]) / 4];
-		*dst++ = red_[curr[x]];
-		x++;
-
-		*dst++ = blue_[(prev[x] + next[x]) / 2];
-		*dst++ = green_[curr[x]];
-		*dst++ = red_[(curr[x - 1] + curr[x + 2]) / 2];
+		rggb8_bgr888(c, dst, x++, 1, 1);
+		grbg8_bgr888(c, dst, x++, 1, 2);
 	}
 }
 
@@ -258,22 +199,20 @@ int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat, [[maybe_unused]] Pi
 	    bayerFormat.packing == BayerFormat::Packing::CSI2) {
 		switch (bayerFormat.order) {
 		case BayerFormat::BGGR:
-			debayer0_ = &DebayerCpu::debayerBGGR10PLine0;
-			debayer1_ = &DebayerCpu::debayerBGGR10PLine1;
+			debayer0_ = &DebayerCpu::debayer10P_BGBG_BGR888;
+			debayer1_ = &DebayerCpu::debayer10P_GRGR_BGR888;
 			return 0;
 		case BayerFormat::GBRG:
-			debayer0_ = &DebayerCpu::debayerGBRG10PLine0;
-			debayer1_ = &DebayerCpu::debayerGBRG10PLine1;
+			debayer0_ = &DebayerCpu::debayer10P_GBGB_BGR888;
+			debayer1_ = &DebayerCpu::debayer10P_RGRG_BGR888;
 			return 0;
 		case BayerFormat::GRBG:
-			/* GRBG is BGGR with the lines swapped */
-			debayer0_ = &DebayerCpu::debayerBGGR10PLine1;
-			debayer1_ = &DebayerCpu::debayerBGGR10PLine0;
+			debayer0_ = &DebayerCpu::debayer10P_GRGR_BGR888;
+			debayer1_ = &DebayerCpu::debayer10P_BGBG_BGR888;
 			return 0;
 		case BayerFormat::RGGB:
-			/* RGGB is GBRG with the lines swapped */
-			debayer0_ = &DebayerCpu::debayerGBRG10PLine1;
-			debayer1_ = &DebayerCpu::debayerGBRG10PLine0;
+			debayer0_ = &DebayerCpu::debayer10P_RGRG_BGR888;
+			debayer1_ = &DebayerCpu::debayer10P_GBGB_BGR888;
 			return 0;
 		default:
 			break;
Pavel Machek Dec. 13, 2023, 9:19 p.m. UTC | #12
Hi!

> >> ATM the statistics and debayer classes are mostly ready,
> >> but I need to merge this with Andrey's v2 RFC and
> >> then produce a clean new series from this
> >> (assuming people like the approach).
> > 
> > Ok, what about this one? I only converted half of the code, but the
> > other half of conversion should be very similar.
> > 
> > Compile-tested only as my sensor is bayer 8.
> > 
> > It makes it easier to see how we are skipping the low bits in 10P
> > bayer, and will make bayer 8 support easier, too, since the pixel_XXX
> > helpers are designed to be shared.
> 
> I have merged a variant of this with:
> 
> 1. The missing bits completed
> 2. Various renames to make it clear that the helpers are for 8bit bayer
>    sources (we are treating 10bpp packed as 8 bit here) and that
>    the output is bgr888 this should make life easier when adding
>    support for more input / output fmts.
> 
> Here is what I've merged / squashed:

Thanks a lot, looks good to me!

Best regards,
								Pavel
Hans de Goede Dec. 13, 2023, 9:54 p.m. UTC | #13
Hi all,

On 12/12/23 22:38, Hans de Goede wrote:

<snip>

> Anyways if you have time, please take a look
> at the swstats base class + linaro implementation
> as well as at the debayer base class as well
> as the DebayerCpu implementation of that:
> 
> https://gitlab.freedesktop.org/camera/libcamera-softisp/-/tree/SoftwareISP-v02-hans2
> 
> Which are the last few commits there.
> 
> Note as I already mentioned I really need to merge
> this with Andrey's latest v03 branch into a new
> logical clean patch series suitable for upstream submission.

And as promised during our call today I now have
a new v03 based branch ready integrating my
put swstats and debayer into separate classes
work (with the more optimized implementations):

https://gitlab.freedesktop.org/camera/libcamera-softisp/-/commits/SoftwareISP-v03-hans1/

The big change compared to Andrey's SoftwareISP-v03 /
RFC v2 posting is:

"libcamera: software_isp: Make swisp_linaro use the new DebayerCpu class "

Andrey, if you can take a look at this and let me know if those
changes are ok with you that would be great.

The intend is to squash "Make swisp_linaro use the new DebayerCpu class"
into the patch adding swisp_linaro before submitting a new series
upstream.

After the squashing the only item remaining before posting
a new version upstream is renaming things away from using
the linaro name for the current swisp and softipa implementations,
I will do this rename tomorrow.

Regards,

Hans
Andrey Konovalov Dec. 14, 2023, 8:56 a.m. UTC | #14
Hi Hans,

On 14.12.2023 00:54, Hans de Goede wrote:
> Hi all,
> 
> On 12/12/23 22:38, Hans de Goede wrote:
> 
> <snip>
> 
>> Anyways if you have time, please take a look
>> at the swstats base class + linaro implementation
>> as well as at the debayer base class as well
>> as the DebayerCpu implementation of that:
>>
>> https://gitlab.freedesktop.org/camera/libcamera-softisp/-/tree/SoftwareISP-v02-hans2
>>
>> Which are the last few commits there.
>>
>> Note as I already mentioned I really need to merge
>> this with Andrey's latest v03 branch into a new
>> logical clean patch series suitable for upstream submission.
> 
> And as promised during our call today I now have
> a new v03 based branch ready integrating my
> put swstats and debayer into separate classes
> work (with the more optimized implementations):
> 
> https://gitlab.freedesktop.org/camera/libcamera-softisp/-/commits/SoftwareISP-v03-hans1/
> 
> The big change compared to Andrey's SoftwareISP-v03 /
> RFC v2 posting is:
> 
> "libcamera: software_isp: Make swisp_linaro use the new DebayerCpu class"
> 
> Andrey, if you can take a look at this and let me know if those
> changes are ok with you that would be great.

Your changes look good to me.
And I do see significant fps increase for the higher resolutions on my target. Now
the fps reaches the frame rate from the camera sensor for all the resolutions - up
to the maximal 8M@15Hz.

> The intend is to squash "Make swisp_linaro use the new DebayerCpu class"
> into the patch adding swisp_linaro before submitting a new series
> upstream.

Agreed.

> After the squashing the only item remaining before posting
> a new version upstream is renaming things away from using
> the linaro name for the current swisp and softipa implementations,
> I will do this rename tomorrow.

Sounds good!


Thanks,
Andrey

> Regards,
> 
> Hans
> 
> 
> 
>

Patch
diff mbox series

diff --git a/include/libcamera/internal/software_isp/meson.build b/include/libcamera/internal/software_isp/meson.build
new file mode 100644
index 00000000..a24fe7df
--- /dev/null
+++ b/include/libcamera/internal/software_isp/meson.build
@@ -0,0 +1,5 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+libcamera_internal_headers += files([
+    'statistics-linaro.h',
+])
diff --git a/include/libcamera/internal/software_isp/statistics-linaro.h b/include/libcamera/internal/software_isp/statistics-linaro.h
new file mode 100644
index 00000000..20c64e44
--- /dev/null
+++ b/include/libcamera/internal/software_isp/statistics-linaro.h
@@ -0,0 +1,17 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * statistics.h - Statistics data format used by the software ISP
+ */
+
+#pragma once
+
+namespace libcamera {
+
+struct SwIspStats {
+	float bright_ratio;
+	float too_bright_ratio;
+};
+
+} /* namespace libcamera */
diff --git a/meson_options.txt b/meson_options.txt
index 5fdc7be8..49939421 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -27,7 +27,7 @@  option('gstreamer',
 
 option('ipas',
         type : 'array',
-        choices : ['ipu3', 'rkisp1', 'rpi/vc4', 'vimc'],
+        choices : ['ipu3', 'rkisp1', 'rpi/vc4', 'simple/linaro', 'vimc'],
         description : 'Select which IPA modules to build')
 
 option('lc-compliance',
@@ -46,6 +46,7 @@  option('pipelines',
             'rkisp1',
             'rpi/vc4',
             'simple',
+            'simple/linaro',
             'uvcvideo',
             'vimc'
         ],
diff --git a/src/ipa/simple/linaro/data/meson.build b/src/ipa/simple/linaro/data/meson.build
new file mode 100644
index 00000000..33548cc6
--- /dev/null
+++ b/src/ipa/simple/linaro/data/meson.build
@@ -0,0 +1,9 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+conf_files = files([
+    'soft.conf',
+])
+
+install_data(conf_files,
+             install_dir : ipa_data_dir / 'soft',
+             install_tag : 'runtime')
diff --git a/src/ipa/simple/linaro/data/soft.conf b/src/ipa/simple/linaro/data/soft.conf
new file mode 100644
index 00000000..0c70e7c0
--- /dev/null
+++ b/src/ipa/simple/linaro/data/soft.conf
@@ -0,0 +1,3 @@ 
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# Dummy configuration file for the soft IPA.
diff --git a/src/ipa/simple/linaro/meson.build b/src/ipa/simple/linaro/meson.build
new file mode 100644
index 00000000..97bf5d6f
--- /dev/null
+++ b/src/ipa/simple/linaro/meson.build
@@ -0,0 +1,26 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+ipa_name = 'ipa_soft_linaro'
+
+mod = shared_module(ipa_name,
+                    ['soft_linaro.cpp', libcamera_generated_ipa_headers],
+                    name_prefix : '',
+                    include_directories : [ipa_includes, libipa_includes, '..'],
+                    dependencies : libcamera_private,
+                    link_with : libipa,
+                    link_whole : soft_ipa_common_lib,
+                    install : true,
+                    install_dir : ipa_install_dir)
+
+if ipa_sign_module
+    custom_target(ipa_name + '.so.sign',
+                  input : mod,
+                  output : ipa_name + '.so.sign',
+                  command : [ipa_sign, ipa_priv_key, '@INPUT@', '@OUTPUT@'],
+                  install : false,
+                  build_by_default : true)
+endif
+
+subdir('data')
+
+ipa_names += ipa_name
diff --git a/src/ipa/simple/linaro/soft_linaro.cpp b/src/ipa/simple/linaro/soft_linaro.cpp
new file mode 100644
index 00000000..0b2e83bf
--- /dev/null
+++ b/src/ipa/simple/linaro/soft_linaro.cpp
@@ -0,0 +1,210 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * soft.cpp - Software Image Processing Algorithm module
+ */
+
+#include <sys/mman.h>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/log.h>
+
+#include <libcamera/control_ids.h>
+
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/ipa_module_info.h>
+
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/software_isp/statistics-linaro.h"
+
+#include "common/soft_base.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(IPASoft)
+
+namespace ipa::soft {
+
+class IPASoftLinaro final : public IPASoftBase
+{
+public:
+	IPASoftLinaro()
+		: IPASoftBase(), ignore_updates_(0)
+	{
+	}
+
+	~IPASoftLinaro()
+	{
+		if (stats_)
+			munmap(stats_, sizeof(SwIspStats));
+	}
+
+	int platformInit(const ControlInfoMap &sensorInfoMap) override;
+	int platformConfigure(const ControlInfoMap &sensorInfoMap) override;
+	int platformStart() override;
+	void platformStop() override;
+	void platformProcessStats(const ControlList &sensorControls) override;
+
+private:
+	void update_exposure(double ev_adjustment);
+
+	SwIspStats *stats_;
+	int exposure_min_, exposure_max_;
+	int again_min_, again_max_;
+	int again_, exposure_;
+	int ignore_updates_;
+};
+
+int IPASoftLinaro::platformInit(const ControlInfoMap &sensorInfoMap)
+{
+	stats_ = static_cast<SwIspStats *>(mmap(nullptr, sizeof(SwIspStats),
+						PROT_READ | PROT_WRITE, MAP_SHARED,
+						fdStats_.get(), 0));
+	if (!stats_) {
+		LOG(IPASoft, Error) << "Unable to map Statistics";
+		return -ENODEV;
+	}
+
+	if (sensorInfoMap.find(V4L2_CID_EXPOSURE) == sensorInfoMap.end()) {
+		LOG(IPASoft, Error) << "Don't have exposure control";
+		return -EINVAL;
+	}
+
+	if (sensorInfoMap.find(V4L2_CID_ANALOGUE_GAIN) == sensorInfoMap.end()) {
+		LOG(IPASoft, Error) << "Don't have gain control";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int IPASoftLinaro::platformConfigure(const ControlInfoMap &sensorInfoMap)
+{
+	const ControlInfo &exposure_info = sensorInfoMap.find(V4L2_CID_EXPOSURE)->second;
+	const ControlInfo &gain_info = sensorInfoMap.find(V4L2_CID_ANALOGUE_GAIN)->second;
+
+	exposure_min_ = exposure_info.min().get<int>();
+	if (!exposure_min_) {
+		LOG(IPASoft, Warning) << "Minimum exposure is zero, that can't be linear";
+		exposure_min_ = 1;
+	}
+	exposure_max_ = exposure_info.max().get<int>();
+	again_min_ = gain_info.min().get<int>();
+	if (!again_min_) {
+		LOG(IPASoft, Warning) << "Minimum gain is zero, that can't be linear";
+		again_min_ = 100;
+	}
+	again_max_ = gain_info.max().get<int>();
+
+	LOG(IPASoft, Info) << "Exposure " << exposure_min_ << "-" << exposure_max_
+			   << ", gain " << again_min_ << "-" << again_max_;
+
+	return 0;
+}
+
+int IPASoftLinaro::platformStart()
+{
+	return 0;
+}
+
+void IPASoftLinaro::platformStop()
+{
+}
+
+void IPASoftLinaro::platformProcessStats(const ControlList &sensorControls)
+{
+	double ev_adjustment = 0.0;
+	ControlList ctrls(sensorControls);
+
+	/*
+	 * Use 2 frames delay to make sure that the exposure and the gain set
+	 * have applied to the camera sensor
+	 */
+	if (ignore_updates_ > 0) {
+		LOG(IPASoft, Debug) << "Skipping exposure update: "
+				    << ignore_updates_;
+		--ignore_updates_;
+		return;
+	}
+
+	if (stats_->bright_ratio < 0.01)
+		ev_adjustment = 1.1;
+	if (stats_->too_bright_ratio > 0.04)
+		ev_adjustment = 0.9;
+
+	if (ev_adjustment != 0.0) {
+		/* sanity check */
+		if (!sensorControls.contains(V4L2_CID_EXPOSURE) ||
+		    !sensorControls.contains(V4L2_CID_ANALOGUE_GAIN)) {
+			LOG(IPASoft, Error) << "Control(s) missing";
+			return;
+		}
+
+		exposure_ = ctrls.get(V4L2_CID_EXPOSURE).get<int>();
+		again_ = ctrls.get(V4L2_CID_ANALOGUE_GAIN).get<int>();
+
+		update_exposure(ev_adjustment);
+
+		ctrls.set(V4L2_CID_EXPOSURE, exposure_);
+		ctrls.set(V4L2_CID_ANALOGUE_GAIN, again_);
+
+		ignore_updates_ = 2;
+
+		setSensorControls.emit(ctrls);
+	}
+}
+
+void IPASoftLinaro::update_exposure(double ev_adjustment)
+{
+	double exp = (double)exposure_;
+	double gain = (double)again_;
+	double ev = ev_adjustment * exp * gain;
+
+	/*
+	 * Try to use the minimal possible analogue gain.
+	 * The exposure can be any value from exposure_min_ to exposure_max_,
+	 * and normally this should keep the frame rate intact.
+	 */
+
+	exp = ev / again_min_;
+	if (exp > exposure_max_)
+		exposure_ = exposure_max_;
+	else if (exp < exposure_min_)
+		exposure_ = exposure_min_;
+	else
+		exposure_ = (int)exp;
+
+	gain = ev / exposure_;
+	if (gain > again_max_)
+		again_ = again_max_;
+	else if (gain < again_min_)
+		again_ = again_min_;
+	else
+		again_ = (int)gain;
+
+	LOG(IPASoft, Debug) << "Desired EV = " << ev
+			    << ", real EV = " << (double)again_ * exposure_;
+}
+
+} /* namespace ipa::soft */
+
+/*
+ * External IPA module interface
+ */
+extern "C" {
+const struct IPAModuleInfo ipaModuleInfo = {
+	IPA_MODULE_API_VERSION,
+	0,
+	"SimplePipelineHandler",
+	"soft/linaro",
+};
+
+IPAInterface *ipaCreate()
+{
+	return new ipa::soft::IPASoftLinaro();
+}
+
+} /* extern "C" */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/meson.build b/src/ipa/simple/meson.build
index 9688bbdb..14be5dc2 100644
--- a/src/ipa/simple/meson.build
+++ b/src/ipa/simple/meson.build
@@ -1,3 +1,12 @@ 
 # SPDX-License-Identifier: CC0-1.0
 
 subdir('common')
+
+foreach pipeline : pipelines
+    pipeline = pipeline.split('/')
+    if pipeline.length() < 2 or pipeline[0] != 'simple'
+        continue
+    endif
+
+    subdir(pipeline[1])
+endforeach