[v3,6/6] libcamera: software_isp: Run sw-statistics once every 4th frame
diff mbox series

Message ID 20250927180004.84620-7-hansg@kernel.org
State New
Headers show
Series
  • ipa: software_isp: AGC: Fox AGC oscillation bug
Related show

Commit Message

Hans de Goede Sept. 27, 2025, 6 p.m. UTC
Run sw-statistics once every 4th frame, instead of every frame. There are
2 reasons for this:

1. There really is no need to have statistics for every frame and only
doing this every 4th frame helps save some CPU time.

2. The generic nature of the simple pipeline-handler, so no information
about possible CSI receiver frame-delays. In combination with the software
ISP often being used with sensors without sensor info in the sensor-helper
code, so no reliable control-delay information means that the software ISP
is prone to AGC oscillation. Skipping statistics gathering also means
skipping running the AGC algorithm slowing it down, avoiding this
oscillation.

Note ideally the AGC oscillation problem would be fixed by adding sensor
metadata support all through the stack so that the exact gain and exposure
used for a specific frame are reliably provided by the sensor metadata.

Signed-off-by: Hans de Goede <hansg@kernel.org>
---
Changes in v2:
- This is a new patch in v2 of this series
---
 src/libcamera/software_isp/debayer_cpu.cpp | 25 +++++++++++++---------
 src/libcamera/software_isp/debayer_cpu.h   |  4 ++--
 src/libcamera/software_isp/swstats_cpu.cpp |  5 +++++
 src/libcamera/software_isp/swstats_cpu.h   |  3 +++
 4 files changed, 25 insertions(+), 12 deletions(-)

Comments

Kieran Bingham Sept. 27, 2025, 7:02 p.m. UTC | #1
Quoting Hans de Goede (2025-09-27 19:00:04)
> Run sw-statistics once every 4th frame, instead of every frame. There are
> 2 reasons for this:
> 
> 1. There really is no need to have statistics for every frame and only
> doing this every 4th frame helps save some CPU time.
> 
> 2. The generic nature of the simple pipeline-handler, so no information
> about possible CSI receiver frame-delays. In combination with the software
> ISP often being used with sensors without sensor info in the sensor-helper
> code, so no reliable control-delay information means that the software ISP
> is prone to AGC oscillation. Skipping statistics gathering also means
> skipping running the AGC algorithm slowing it down, avoiding this
> oscillation.
> 
> Note ideally the AGC oscillation problem would be fixed by adding sensor
> metadata support all through the stack so that the exact gain and exposure
> used for a specific frame are reliably provided by the sensor metadata.
> 
> Signed-off-by: Hans de Goede <hansg@kernel.org>
> ---
> Changes in v2:
> - This is a new patch in v2 of this series

Tested on my X13s with no flicker.

I also tested at 
 - kStatPerNumFrames == 2 *Flicker*
 - kStatPerNumFrames == 3 No flicker.
 - kStatPerNumFrames == 4 No flicker.

So either 3 or 4 is good here.

Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>


> ---
>  src/libcamera/software_isp/debayer_cpu.cpp | 25 +++++++++++++---------
>  src/libcamera/software_isp/debayer_cpu.h   |  4 ++--
>  src/libcamera/software_isp/swstats_cpu.cpp |  5 +++++
>  src/libcamera/software_isp/swstats_cpu.h   |  3 +++
>  4 files changed, 25 insertions(+), 12 deletions(-)
> 
> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
> index bfa60888..9010333e 100644
> --- a/src/libcamera/software_isp/debayer_cpu.cpp
> +++ b/src/libcamera/software_isp/debayer_cpu.cpp
> @@ -655,7 +655,7 @@ void DebayerCpu::memcpyNextLine(const uint8_t *linePointers[])
>         lineBufferIndex_ = (lineBufferIndex_ + 1) % (patternHeight + 1);
>  }
>  
> -void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
> +void DebayerCpu::process2(uint32_t frame, const uint8_t *src, uint8_t *dst)
>  {
>         unsigned int yEnd = window_.y + window_.height;
>         /* Holds [0] previous- [1] current- [2] next-line */
> @@ -681,7 +681,8 @@ void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
>         for (unsigned int y = window_.y; y < yEnd; y += 2) {
>                 shiftLinePointers(linePointers, src);
>                 memcpyNextLine(linePointers);
> -               stats_->processLine0(y, linePointers);
> +               if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +                       stats_->processLine0(y, linePointers);
>                 (this->*debayer0_)(dst, linePointers);
>                 src += inputConfig_.stride;
>                 dst += outputConfig_.stride;
> @@ -696,7 +697,8 @@ void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
>         if (window_.y == 0) {
>                 shiftLinePointers(linePointers, src);
>                 memcpyNextLine(linePointers);
> -               stats_->processLine0(yEnd, linePointers);
> +               if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +                       stats_->processLine0(yEnd, linePointers);
>                 (this->*debayer0_)(dst, linePointers);
>                 src += inputConfig_.stride;
>                 dst += outputConfig_.stride;
> @@ -710,7 +712,7 @@ void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
>         }
>  }
>  
> -void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
> +void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst)
>  {
>         const unsigned int yEnd = window_.y + window_.height;
>         /*
> @@ -733,7 +735,8 @@ void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
>         for (unsigned int y = window_.y; y < yEnd; y += 4) {
>                 shiftLinePointers(linePointers, src);
>                 memcpyNextLine(linePointers);
> -               stats_->processLine0(y, linePointers);
> +               if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +                       stats_->processLine0(y, linePointers);
>                 (this->*debayer0_)(dst, linePointers);
>                 src += inputConfig_.stride;
>                 dst += outputConfig_.stride;
> @@ -746,7 +749,8 @@ void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
>  
>                 shiftLinePointers(linePointers, src);
>                 memcpyNextLine(linePointers);
> -               stats_->processLine2(y, linePointers);
> +               if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +                       stats_->processLine2(y, linePointers);
>                 (this->*debayer2_)(dst, linePointers);
>                 src += inputConfig_.stride;
>                 dst += outputConfig_.stride;
> @@ -821,12 +825,13 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>                 return;
>         }
>  
> -       stats_->startFrame();
> +       if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +               stats_->startFrame();
>  
>         if (inputConfig_.patternSize.height == 2)
> -               process2(in.planes()[0].data(), out.planes()[0].data());
> +               process2(frame, in.planes()[0].data(), out.planes()[0].data());
>         else
> -               process4(in.planes()[0].data(), out.planes()[0].data());
> +               process4(frame, in.planes()[0].data(), out.planes()[0].data());
>  
>         metadata.planes()[0].bytesused = out.planes()[0].size();
>  
> @@ -851,7 +856,7 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>          *
>          * \todo Pass real bufferId once stats buffer passing is changed.
>          */
> -       stats_->finishFrame(frame, 0, true);
> +       stats_->finishFrame(frame, 0, frame % SwStatsCpu::kStatPerNumFrames == 0);
>         outputBufferReady.emit(output);
>         inputBufferReady.emit(input);
>  }
> diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
> index 9d343e46..03e0d784 100644
> --- a/src/libcamera/software_isp/debayer_cpu.h
> +++ b/src/libcamera/software_isp/debayer_cpu.h
> @@ -133,8 +133,8 @@ private:
>         void setupInputMemcpy(const uint8_t *linePointers[]);
>         void shiftLinePointers(const uint8_t *linePointers[], const uint8_t *src);
>         void memcpyNextLine(const uint8_t *linePointers[]);
> -       void process2(const uint8_t *src, uint8_t *dst);
> -       void process4(const uint8_t *src, uint8_t *dst);
> +       void process2(uint32_t frame, const uint8_t *src, uint8_t *dst);
> +       void process4(uint32_t frame, const uint8_t *src, uint8_t *dst);
>  
>         /* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
>         static constexpr unsigned int kMaxLineBuffers = 5;
> diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
> index da91f912..35ba0a46 100644
> --- a/src/libcamera/software_isp/swstats_cpu.cpp
> +++ b/src/libcamera/software_isp/swstats_cpu.cpp
> @@ -89,6 +89,11 @@ namespace libcamera {
>   * \brief Signals that the statistics are ready
>   */
>  
> +/**
> + * \var SwStatsCpu::kStatPerNumFrames
> + * \brief Run stats once every kStatPerNumFrames frames
> + */
> +
>  /**
>   * \typedef SwStatsCpu::statsProcessFn
>   * \brief Called when there is data to get statistics from
> diff --git a/src/libcamera/software_isp/swstats_cpu.h b/src/libcamera/software_isp/swstats_cpu.h
> index 6ac3c4de..ea0e6d5a 100644
> --- a/src/libcamera/software_isp/swstats_cpu.h
> +++ b/src/libcamera/software_isp/swstats_cpu.h
> @@ -32,6 +32,9 @@ public:
>         SwStatsCpu();
>         ~SwStatsCpu() = default;
>  
> +       /* Run stats once every 4 frames */
> +       static constexpr uint32_t kStatPerNumFrames = 4;
> +
>         bool isValid() const { return sharedStats_.fd().isValid(); }
>  
>         const SharedFD &getStatsFD() { return sharedStats_.fd(); }
> -- 
> 2.51.0
>
Robert Mader Sept. 27, 2025, 8:01 p.m. UTC | #2
Very happy to see this and would like to expand on 1: in case of the 
GPU-ISP we'll be able to avoid mmapping the input buffer for debayering 
if we can directly import it with the GPU (1). So if we also don't need 
to mmap the buffer for statistics, the saved overhead can be 
significant, especially on aarch64 where we need relatively expensive 
sync ioctls. So big thumbs-up for this.

Thanks and best regards,

Robert

1. PoC patch: 
https://gitlab.freedesktop.org/rmader/libcamera/-/commit/1f5c2764c4d42c916e52f9c06c44f4ae52519a78

On 9/27/25 20:00, Hans de Goede wrote:
> Run sw-statistics once every 4th frame, instead of every frame. There are
> 2 reasons for this:
>
> 1. There really is no need to have statistics for every frame and only
> doing this every 4th frame helps save some CPU time.
>
> 2. The generic nature of the simple pipeline-handler, so no information
> about possible CSI receiver frame-delays. In combination with the software
> ISP often being used with sensors without sensor info in the sensor-helper
> code, so no reliable control-delay information means that the software ISP
> is prone to AGC oscillation. Skipping statistics gathering also means
> skipping running the AGC algorithm slowing it down, avoiding this
> oscillation.
>
> Note ideally the AGC oscillation problem would be fixed by adding sensor
> metadata support all through the stack so that the exact gain and exposure
> used for a specific frame are reliably provided by the sensor metadata.
>
> Signed-off-by: Hans de Goede <hansg@kernel.org>
> ---
> Changes in v2:
> - This is a new patch in v2 of this series
> ---
>   src/libcamera/software_isp/debayer_cpu.cpp | 25 +++++++++++++---------
>   src/libcamera/software_isp/debayer_cpu.h   |  4 ++--
>   src/libcamera/software_isp/swstats_cpu.cpp |  5 +++++
>   src/libcamera/software_isp/swstats_cpu.h   |  3 +++
>   4 files changed, 25 insertions(+), 12 deletions(-)
>
> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
> index bfa60888..9010333e 100644
> --- a/src/libcamera/software_isp/debayer_cpu.cpp
> +++ b/src/libcamera/software_isp/debayer_cpu.cpp
> @@ -655,7 +655,7 @@ void DebayerCpu::memcpyNextLine(const uint8_t *linePointers[])
>   	lineBufferIndex_ = (lineBufferIndex_ + 1) % (patternHeight + 1);
>   }
>   
> -void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
> +void DebayerCpu::process2(uint32_t frame, const uint8_t *src, uint8_t *dst)
>   {
>   	unsigned int yEnd = window_.y + window_.height;
>   	/* Holds [0] previous- [1] current- [2] next-line */
> @@ -681,7 +681,8 @@ void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
>   	for (unsigned int y = window_.y; y < yEnd; y += 2) {
>   		shiftLinePointers(linePointers, src);
>   		memcpyNextLine(linePointers);
> -		stats_->processLine0(y, linePointers);
> +		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +			stats_->processLine0(y, linePointers);
>   		(this->*debayer0_)(dst, linePointers);
>   		src += inputConfig_.stride;
>   		dst += outputConfig_.stride;
> @@ -696,7 +697,8 @@ void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
>   	if (window_.y == 0) {
>   		shiftLinePointers(linePointers, src);
>   		memcpyNextLine(linePointers);
> -		stats_->processLine0(yEnd, linePointers);
> +		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +			stats_->processLine0(yEnd, linePointers);
>   		(this->*debayer0_)(dst, linePointers);
>   		src += inputConfig_.stride;
>   		dst += outputConfig_.stride;
> @@ -710,7 +712,7 @@ void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
>   	}
>   }
>   
> -void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
> +void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst)
>   {
>   	const unsigned int yEnd = window_.y + window_.height;
>   	/*
> @@ -733,7 +735,8 @@ void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
>   	for (unsigned int y = window_.y; y < yEnd; y += 4) {
>   		shiftLinePointers(linePointers, src);
>   		memcpyNextLine(linePointers);
> -		stats_->processLine0(y, linePointers);
> +		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +			stats_->processLine0(y, linePointers);
>   		(this->*debayer0_)(dst, linePointers);
>   		src += inputConfig_.stride;
>   		dst += outputConfig_.stride;
> @@ -746,7 +749,8 @@ void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
>   
>   		shiftLinePointers(linePointers, src);
>   		memcpyNextLine(linePointers);
> -		stats_->processLine2(y, linePointers);
> +		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +			stats_->processLine2(y, linePointers);
>   		(this->*debayer2_)(dst, linePointers);
>   		src += inputConfig_.stride;
>   		dst += outputConfig_.stride;
> @@ -821,12 +825,13 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>   		return;
>   	}
>   
> -	stats_->startFrame();
> +	if (frame % SwStatsCpu::kStatPerNumFrames == 0)
> +		stats_->startFrame();
>   
>   	if (inputConfig_.patternSize.height == 2)
> -		process2(in.planes()[0].data(), out.planes()[0].data());
> +		process2(frame, in.planes()[0].data(), out.planes()[0].data());
>   	else
> -		process4(in.planes()[0].data(), out.planes()[0].data());
> +		process4(frame, in.planes()[0].data(), out.planes()[0].data());
>   
>   	metadata.planes()[0].bytesused = out.planes()[0].size();
>   
> @@ -851,7 +856,7 @@ void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
>   	 *
>   	 * \todo Pass real bufferId once stats buffer passing is changed.
>   	 */
> -	stats_->finishFrame(frame, 0, true);
> +	stats_->finishFrame(frame, 0, frame % SwStatsCpu::kStatPerNumFrames == 0);
>   	outputBufferReady.emit(output);
>   	inputBufferReady.emit(input);
>   }
> diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
> index 9d343e46..03e0d784 100644
> --- a/src/libcamera/software_isp/debayer_cpu.h
> +++ b/src/libcamera/software_isp/debayer_cpu.h
> @@ -133,8 +133,8 @@ private:
>   	void setupInputMemcpy(const uint8_t *linePointers[]);
>   	void shiftLinePointers(const uint8_t *linePointers[], const uint8_t *src);
>   	void memcpyNextLine(const uint8_t *linePointers[]);
> -	void process2(const uint8_t *src, uint8_t *dst);
> -	void process4(const uint8_t *src, uint8_t *dst);
> +	void process2(uint32_t frame, const uint8_t *src, uint8_t *dst);
> +	void process4(uint32_t frame, const uint8_t *src, uint8_t *dst);
>   
>   	/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
>   	static constexpr unsigned int kMaxLineBuffers = 5;
> diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
> index da91f912..35ba0a46 100644
> --- a/src/libcamera/software_isp/swstats_cpu.cpp
> +++ b/src/libcamera/software_isp/swstats_cpu.cpp
> @@ -89,6 +89,11 @@ namespace libcamera {
>    * \brief Signals that the statistics are ready
>    */
>   
> +/**
> + * \var SwStatsCpu::kStatPerNumFrames
> + * \brief Run stats once every kStatPerNumFrames frames
> + */
> +
>   /**
>    * \typedef SwStatsCpu::statsProcessFn
>    * \brief Called when there is data to get statistics from
> diff --git a/src/libcamera/software_isp/swstats_cpu.h b/src/libcamera/software_isp/swstats_cpu.h
> index 6ac3c4de..ea0e6d5a 100644
> --- a/src/libcamera/software_isp/swstats_cpu.h
> +++ b/src/libcamera/software_isp/swstats_cpu.h
> @@ -32,6 +32,9 @@ public:
>   	SwStatsCpu();
>   	~SwStatsCpu() = default;
>   
> +	/* Run stats once every 4 frames */
> +	static constexpr uint32_t kStatPerNumFrames = 4;
> +
>   	bool isValid() const { return sharedStats_.fd().isValid(); }
>   
>   	const SharedFD &getStatsFD() { return sharedStats_.fd(); }

Patch
diff mbox series

diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index bfa60888..9010333e 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -655,7 +655,7 @@  void DebayerCpu::memcpyNextLine(const uint8_t *linePointers[])
 	lineBufferIndex_ = (lineBufferIndex_ + 1) % (patternHeight + 1);
 }
 
-void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
+void DebayerCpu::process2(uint32_t frame, const uint8_t *src, uint8_t *dst)
 {
 	unsigned int yEnd = window_.y + window_.height;
 	/* Holds [0] previous- [1] current- [2] next-line */
@@ -681,7 +681,8 @@  void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
 	for (unsigned int y = window_.y; y < yEnd; y += 2) {
 		shiftLinePointers(linePointers, src);
 		memcpyNextLine(linePointers);
-		stats_->processLine0(y, linePointers);
+		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
+			stats_->processLine0(y, linePointers);
 		(this->*debayer0_)(dst, linePointers);
 		src += inputConfig_.stride;
 		dst += outputConfig_.stride;
@@ -696,7 +697,8 @@  void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
 	if (window_.y == 0) {
 		shiftLinePointers(linePointers, src);
 		memcpyNextLine(linePointers);
-		stats_->processLine0(yEnd, linePointers);
+		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
+			stats_->processLine0(yEnd, linePointers);
 		(this->*debayer0_)(dst, linePointers);
 		src += inputConfig_.stride;
 		dst += outputConfig_.stride;
@@ -710,7 +712,7 @@  void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
 	}
 }
 
-void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
+void DebayerCpu::process4(uint32_t frame, const uint8_t *src, uint8_t *dst)
 {
 	const unsigned int yEnd = window_.y + window_.height;
 	/*
@@ -733,7 +735,8 @@  void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
 	for (unsigned int y = window_.y; y < yEnd; y += 4) {
 		shiftLinePointers(linePointers, src);
 		memcpyNextLine(linePointers);
-		stats_->processLine0(y, linePointers);
+		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
+			stats_->processLine0(y, linePointers);
 		(this->*debayer0_)(dst, linePointers);
 		src += inputConfig_.stride;
 		dst += outputConfig_.stride;
@@ -746,7 +749,8 @@  void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
 
 		shiftLinePointers(linePointers, src);
 		memcpyNextLine(linePointers);
-		stats_->processLine2(y, linePointers);
+		if (frame % SwStatsCpu::kStatPerNumFrames == 0)
+			stats_->processLine2(y, linePointers);
 		(this->*debayer2_)(dst, linePointers);
 		src += inputConfig_.stride;
 		dst += outputConfig_.stride;
@@ -821,12 +825,13 @@  void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 		return;
 	}
 
-	stats_->startFrame();
+	if (frame % SwStatsCpu::kStatPerNumFrames == 0)
+		stats_->startFrame();
 
 	if (inputConfig_.patternSize.height == 2)
-		process2(in.planes()[0].data(), out.planes()[0].data());
+		process2(frame, in.planes()[0].data(), out.planes()[0].data());
 	else
-		process4(in.planes()[0].data(), out.planes()[0].data());
+		process4(frame, in.planes()[0].data(), out.planes()[0].data());
 
 	metadata.planes()[0].bytesused = out.planes()[0].size();
 
@@ -851,7 +856,7 @@  void DebayerCpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output
 	 *
 	 * \todo Pass real bufferId once stats buffer passing is changed.
 	 */
-	stats_->finishFrame(frame, 0, true);
+	stats_->finishFrame(frame, 0, frame % SwStatsCpu::kStatPerNumFrames == 0);
 	outputBufferReady.emit(output);
 	inputBufferReady.emit(input);
 }
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
index 9d343e46..03e0d784 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -133,8 +133,8 @@  private:
 	void setupInputMemcpy(const uint8_t *linePointers[]);
 	void shiftLinePointers(const uint8_t *linePointers[], const uint8_t *src);
 	void memcpyNextLine(const uint8_t *linePointers[]);
-	void process2(const uint8_t *src, uint8_t *dst);
-	void process4(const uint8_t *src, uint8_t *dst);
+	void process2(uint32_t frame, const uint8_t *src, uint8_t *dst);
+	void process4(uint32_t frame, const uint8_t *src, uint8_t *dst);
 
 	/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
 	static constexpr unsigned int kMaxLineBuffers = 5;
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index da91f912..35ba0a46 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -89,6 +89,11 @@  namespace libcamera {
  * \brief Signals that the statistics are ready
  */
 
+/**
+ * \var SwStatsCpu::kStatPerNumFrames
+ * \brief Run stats once every kStatPerNumFrames frames
+ */
+
 /**
  * \typedef SwStatsCpu::statsProcessFn
  * \brief Called when there is data to get statistics from
diff --git a/src/libcamera/software_isp/swstats_cpu.h b/src/libcamera/software_isp/swstats_cpu.h
index 6ac3c4de..ea0e6d5a 100644
--- a/src/libcamera/software_isp/swstats_cpu.h
+++ b/src/libcamera/software_isp/swstats_cpu.h
@@ -32,6 +32,9 @@  public:
 	SwStatsCpu();
 	~SwStatsCpu() = default;
 
+	/* Run stats once every 4 frames */
+	static constexpr uint32_t kStatPerNumFrames = 4;
+
 	bool isValid() const { return sharedStats_.fd().isValid(); }
 
 	const SharedFD &getStatsFD() { return sharedStats_.fd(); }