[v5,7/7] libcamera: software_isp: debayer_egl: Implement input/output frame caching mechanism
diff mbox series

Message ID 20260706113900.2530549-8-bryan.odonoghue@linaro.org
State New
Headers show
Series
  • libcamera: software_isp: gpu: Add go faster stripes
Related show

Commit Message

Bryan O'Donoghue July 6, 2026, 11:39 a.m. UTC
Implement a texture caching mechanism for both input and output frames and
for both types of input frame.

The before/after on a Qualcomm x1e is:

9.737ms per frame
5.691ms per frame

The before/after on a Qualcomm sm8250 is:

21.710ms per frame
17.336ms per frame

for i in {1..20} do
cam -c /base/soc@0/cci@ac16000/i2c-bus@1/camera@10 -s width=1920,height=1080 --capture=60

Interestingly there appears to be an absolute ~ 4.x ms per frame uplift as
opposed to what intuition might suggest a proportional.

Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
---
 src/libcamera/software_isp/debayer_egl.cpp | 112 ++++++++++++++++-----
 src/libcamera/software_isp/debayer_egl.h   |  13 ++-
 2 files changed, 96 insertions(+), 29 deletions(-)

Comments

Robert Mader July 6, 2026, 11:56 a.m. UTC | #1
Hi,

On 06.07.26 13:39, Bryan O'Donoghue wrote:
> Implement a texture caching mechanism for both input and output frames and
> for both types of input frame.
>
> The before/after on a Qualcomm x1e is:
>
> 9.737ms per frame
> 5.691ms per frame
>
> The before/after on a Qualcomm sm8250 is:
>
> 21.710ms per frame
> 17.336ms per frame
>
> for i in {1..20} do
> cam -c /base/soc@0/cci@ac16000/i2c-bus@1/camera@10 -s width=1920,height=1080 --capture=60
>
> Interestingly there appears to be an absolute ~ 4.x ms per frame uplift as
> opposed to what intuition might suggest a proportional.
>
> Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
> ---
>   src/libcamera/software_isp/debayer_egl.cpp | 112 ++++++++++++++++-----
>   src/libcamera/software_isp/debayer_egl.h   |  13 ++-
>   2 files changed, 96 insertions(+), 29 deletions(-)
>
> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index c3a7b7952..ca3353d9f 100644
> --- a/src/libcamera/software_isp/debayer_egl.cpp
> +++ b/src/libcamera/software_isp/debayer_egl.cpp
> @@ -345,6 +345,9 @@ int DebayerEGL::configure(const StreamConfiguration &inputCfg,
>   	 */
>   	stats_->setWindow(Rectangle(window_.size()));
>   
> +	inputBufferCount_ = inputCfg.bufferCount;
> +	outputBufferCount_ = outputCfg.bufferCount;
> +
>   	return 0;
>   }
>   
> @@ -504,36 +507,95 @@ void DebayerEGL::setShaderVariableValues(eGLImage &eglImageIn, const DebayerPara
>   	return;
>   }
>   
> -int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
> +eGLImage *DebayerEGL::getCachedInputFrameBuffer(FrameBuffer *input, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
>   {
> -	bool dmabuf_import_succeeded = false;
> -
> -	/* eGL context switch */
> -	egl_.makeCurrent();
> -
> -	/* Try to create texture for input buffer via dmabuf import */
> -	if (!eglImageBayerIn_->dmabuf_import_failed_) {
> -		if (egl_.createInputDMABufTexture2D(*eglImageBayerIn_, input->planes()[0].fd.get()) == 0)
> -			dmabuf_import_succeeded = true;
> -		else
> -			LOG(Debayer, Info) << "Importing input buffer with DMABuf import failed, falling back to upload";
> +	const SharedFD &fd = input->planes()[0].fd;
> +	bool use_dmabuf = false;
> +
> +	auto [input_cache, cache_miss] = eglImageBayerIn_.try_emplace(fd);
> +	if (cache_miss) {
> +		if (eglImageBayerIn_.size() > inputBufferCount_) {
> +			eglImageBayerIn_.erase(inputRing_.front());
> +			inputRing_.pop_front();
> +			LOG(Debayer, Error) << "Input cache " << inputBufferCount_ << " exceeded evicted entry";
> +		}
> +		input_cache->second = std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_,
> +								 height_, inputConfig_.stride, GL_TEXTURE0, 0);
> +		inputRing_.push_back(fd);
> +	}
> +	eGLImage *eglImageIn = input_cache->second.get();
> +
> +	/* Try to create texture for input buffer via dmabuf import only on cache miss */
> +	if (cache_miss) {
> +		if (egl_.createInputDMABufTexture2D(*eglImageIn, input->planes()[0].fd.get()) == 0)
> +			use_dmabuf = true;
> +	} else if (!eglImageIn->dmabuf_import_failed_) {
> +		use_dmabuf = true;

Style nit: the only use of "use_dmabuf = true" here and above is to skip 
the upload block below now. Just "return eglImageIn;" would be easier to 
read IMO (which is why I reordered things in 
https://gitlab.freedesktop.org/rmader/libcamera/-/commit/01473931ddb8940f192448cfee897bf563972e91).

> +		egl_.activateBindTexture(*eglImageIn);
>   	}
>   
> -	/* Otherwise create texture for input buffer via upload from CPU */
> -	if (!dmabuf_import_succeeded) {
> +	if (!use_dmabuf) {
>   		inDmaSyncer->emplace(input->planes()[0].fd, DmaSyncer::SyncType::Read);
>   		inMapped->emplace(input, MappedFrameBuffer::MapFlag::Read);
>   		if (!inMapped->value().isValid()) {
>   			LOG(Debayer, Error) << "mmap-ing buffer(s) failed";
> -			return -ENODEV;
> +			if (cache_miss) {
> +				eglImageBayerIn_.erase(input_cache);
> +				inputRing_.pop_back();
> +			}
> +			return nullptr;
> +		}
> +		if (cache_miss)
> +			egl_.createTexture2D(*eglImageIn, inMapped->value().planes()[0].data());
> +		else
> +			egl_.updateTexture2D(*eglImageIn, inMapped->value().planes()[0].data());
> +	}
> +
> +	return eglImageIn;
> +}
> +
> +eGLImage *DebayerEGL::getCachedOutputFrameBuffer(FrameBuffer *output)
> +{
> +	const SharedFD &fd = output->planes()[0].fd;
> +
> +	auto [output_cache, cache_miss] = eglImageBayerOut_.try_emplace(fd);
> +	if (cache_miss) {
> +		if (eglImageBayerOut_.size() > outputBufferCount_) {
> +			eglImageBayerOut_.erase(outputRing_.front());
> +			outputRing_.pop_front();
> +			LOG(Debayer, Error) << "Output cache " << outputBufferCount_ << " exceeded evicted entry";
>   		}
> -		egl_.createTexture2D(*eglImageBayerIn_, inMapped->value().planes()[0].data());
> +		output_cache->second = std::make_unique<eGLImage>(GL_RGBA, outputSize_.width,
> +								  outputSize_.height, outputConfig_.stride, GL_TEXTURE1, 1);
> +		if (egl_.createOutputDMABufTexture2D(*output_cache->second, output->planes()[0].fd.get())) {
> +			eglImageBayerOut_.erase(output_cache);
> +			return nullptr;
> +		}
> +		outputRing_.push_back(fd);
>   	}
> +	eGLImage *eglImageOut = output_cache->second.get();
> +
> +	return eglImageOut;
> +}
> +
> +int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
> +{
> +	eGLImage *eglImageIn;
> +	eGLImage *eglImageOut;
> +
> +	/* eGL context switch */
> +	egl_.makeCurrent();
>   
> -	/* Generate the output render framebuffer as render to texture */
> -	egl_.createOutputDMABufTexture2D(*eglImageBayerOut_, output->planes()[0].fd.get());
> +	eglImageIn = getCachedInputFrameBuffer(input, inMapped, inDmaSyncer);
> +	if (!eglImageIn)
> +		return -ENOMEM;
> +	eglImageOut = getCachedOutputFrameBuffer(output);
> +	if (!eglImageOut)
> +		return -ENOMEM;
> +
> +	egl_.attachTextureToFBO(*eglImageOut);
> +	setShaderVariableValues(*eglImageIn, params);
>   
> -	setShaderVariableValues(*eglImageBayerIn_, params);
>   	glViewport(0, 0, width_, height_);
>   	glClear(GL_COLOR_BUFFER_BIT);
>   	glDrawArrays(GL_TRIANGLE_FAN, 0, DEBAYER_OPENGL_COORDS);
> @@ -615,19 +677,15 @@ int DebayerEGL::start()
>   	if (initBayerShaders(inputPixelFormat_, outputPixelFormat_))
>   		return -EINVAL;
>   
> -	/* Raw bayer input as texture */
> -	eglImageBayerIn_ = std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_, height_, inputConfig_.stride, GL_TEXTURE0, 0);
> -
> -	/* Texture we will render to */
> -	eglImageBayerOut_ = std::make_unique<eGLImage>(GL_RGBA, outputSize_.width, outputSize_.height, outputConfig_.stride, GL_TEXTURE1, 1);
> -
>   	return 0;
>   }
>   
>   void DebayerEGL::stop()
>   {
> -	eglImageBayerOut_.reset();
> -	eglImageBayerIn_.reset();
> +	eglImageBayerOut_.clear();
> +	eglImageBayerIn_.clear();
> +	outputRing_.clear();
> +	inputRing_.clear();
>   
>   	if (programId_)
>   		glDeleteProgram(programId_);
> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
> index 348d7305b..dde6ba9bf 100644
> --- a/src/libcamera/software_isp/debayer_egl.h
> +++ b/src/libcamera/software_isp/debayer_egl.h
> @@ -9,9 +9,11 @@
>   
>   #pragma once
>   
> +#include <deque>
>   #include <memory>
>   #include <stdint.h>
>   #include <tuple>
> +#include <unordered_map>
>   #include <vector>
>   
>   #define GL_GLEXT_PROTOTYPES
> @@ -68,14 +70,21 @@ private:
>   	void setShaderVariableValues(eGLImage &eGLImageIn, const DebayerParams &params);
>   	int debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *mappedInputBuffer, std::optional<DmaSyncer> *inputBufferDmaSyncer);
>   
> +	eGLImage *getCachedInputFrameBuffer(FrameBuffer *input, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer);
> +	eGLImage *getCachedOutputFrameBuffer(FrameBuffer *output);
> +
>   	/* Shader program identifiers */
>   	GLuint vertexShaderId_ = 0;
>   	GLuint fragmentShaderId_ = 0;
>   	GLuint programId_ = 0;
>   
>   	/* Pointer to object representing input texture */
> -	std::unique_ptr<eGLImage> eglImageBayerIn_;
> -	std::unique_ptr<eGLImage> eglImageBayerOut_;
> +	std::unordered_map<SharedFD, std::unique_ptr<eGLImage>> eglImageBayerIn_;
> +	std::unordered_map<SharedFD, std::unique_ptr<eGLImage>> eglImageBayerOut_;
> +	std::deque<SharedFD> inputRing_;
> +	std::deque<SharedFD> outputRing_;
> +	unsigned int inputBufferCount_;
> +	unsigned int outputBufferCount_;
>   
>   	/* Shader parameters */
>   	float firstRed_x_;
Robert Mader July 6, 2026, 2:35 p.m. UTC | #2
On 06.07.26 13:39, Bryan O'Donoghue wrote:
> Implement a texture caching mechanism for both input and output frames and
> for both types of input frame.
>
> The before/after on a Qualcomm x1e is:
>
> 9.737ms per frame
> 5.691ms per frame
>
> The before/after on a Qualcomm sm8250 is:
>
> 21.710ms per frame
> 17.336ms per frame
>
> for i in {1..20} do
> cam -c /base/soc@0/cci@ac16000/i2c-bus@1/camera@10 -s width=1920,height=1080 --capture=60
>
> Interestingly there appears to be an absolute ~ 4.x ms per frame uplift as
> opposed to what intuition might suggest a proportional.
>
> Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
> ---
>   src/libcamera/software_isp/debayer_egl.cpp | 112 ++++++++++++++++-----
>   src/libcamera/software_isp/debayer_egl.h   |  13 ++-
>   2 files changed, 96 insertions(+), 29 deletions(-)
>
> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index c3a7b7952..ca3353d9f 100644
> --- a/src/libcamera/software_isp/debayer_egl.cpp
> +++ b/src/libcamera/software_isp/debayer_egl.cpp
> @@ -345,6 +345,9 @@ int DebayerEGL::configure(const StreamConfiguration &inputCfg,
>   	 */
>   	stats_->setWindow(Rectangle(window_.size()));
>   
> +	inputBufferCount_ = inputCfg.bufferCount;
> +	outputBufferCount_ = outputCfg.bufferCount;
> +
>   	return 0;
>   }
>   
> @@ -504,36 +507,95 @@ void DebayerEGL::setShaderVariableValues(eGLImage &eglImageIn, const DebayerPara
>   	return;
>   }
>   
> -int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
> +eGLImage *DebayerEGL::getCachedInputFrameBuffer(FrameBuffer *input, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
>   {
> -	bool dmabuf_import_succeeded = false;
> -
> -	/* eGL context switch */
> -	egl_.makeCurrent();
> -
> -	/* Try to create texture for input buffer via dmabuf import */
> -	if (!eglImageBayerIn_->dmabuf_import_failed_) {
> -		if (egl_.createInputDMABufTexture2D(*eglImageBayerIn_, input->planes()[0].fd.get()) == 0)
> -			dmabuf_import_succeeded = true;
> -		else
> -			LOG(Debayer, Info) << "Importing input buffer with DMABuf import failed, falling back to upload";
> +	const SharedFD &fd = input->planes()[0].fd;
> +	bool use_dmabuf = false;
> +
> +	auto [input_cache, cache_miss] = eglImageBayerIn_.try_emplace(fd);
> +	if (cache_miss) {
> +		if (eglImageBayerIn_.size() > inputBufferCount_) {
> +			eglImageBayerIn_.erase(inputRing_.front());
> +			inputRing_.pop_front();
> +			LOG(Debayer, Error) << "Input cache " << inputBufferCount_ << " exceeded evicted entry";
> +		}
> +		input_cache->second = std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_,
> +								 height_, inputConfig_.stride, GL_TEXTURE0, 0);
> +		inputRing_.push_back(fd);
> +	}
> +	eGLImage *eglImageIn = input_cache->second.get();
> +
> +	/* Try to create texture for input buffer via dmabuf import only on cache miss */
> +	if (cache_miss) {
> +		if (egl_.createInputDMABufTexture2D(*eglImageIn, input->planes()[0].fd.get()) == 0)
> +			use_dmabuf = true;
> +	} else if (!eglImageIn->dmabuf_import_failed_) {
> +		use_dmabuf = true;
> +		egl_.activateBindTexture(*eglImageIn);
>   	}
>   
> -	/* Otherwise create texture for input buffer via upload from CPU */
> -	if (!dmabuf_import_succeeded) {
> +	if (!use_dmabuf) {
>   		inDmaSyncer->emplace(input->planes()[0].fd, DmaSyncer::SyncType::Read);
>   		inMapped->emplace(input, MappedFrameBuffer::MapFlag::Read);
>   		if (!inMapped->value().isValid()) {
>   			LOG(Debayer, Error) << "mmap-ing buffer(s) failed";
> -			return -ENODEV;
> +			if (cache_miss) {
> +				eglImageBayerIn_.erase(input_cache);
> +				inputRing_.pop_back();
> +			}
> +			return nullptr;
> +		}
> +		if (cache_miss)
> +			egl_.createTexture2D(*eglImageIn, inMapped->value().planes()[0].data());
> +		else
> +			egl_.updateTexture2D(*eglImageIn, inMapped->value().planes()[0].data());
> +	}
> +
> +	return eglImageIn;
> +}
> +
> +eGLImage *DebayerEGL::getCachedOutputFrameBuffer(FrameBuffer *output)
> +{
> +	const SharedFD &fd = output->planes()[0].fd;
> +
> +	auto [output_cache, cache_miss] = eglImageBayerOut_.try_emplace(fd);
> +	if (cache_miss) {
> +		if (eglImageBayerOut_.size() > outputBufferCount_) {
> +			eglImageBayerOut_.erase(outputRing_.front());
> +			outputRing_.pop_front();
> +			LOG(Debayer, Error) << "Output cache " << outputBufferCount_ << " exceeded evicted entry";
>   		}
> -		egl_.createTexture2D(*eglImageBayerIn_, inMapped->value().planes()[0].data());
> +		output_cache->second = std::make_unique<eGLImage>(GL_RGBA, outputSize_.width,
> +								  outputSize_.height, outputConfig_.stride, GL_TEXTURE1, 1);
> +		if (egl_.createOutputDMABufTexture2D(*output_cache->second, output->planes()[0].fd.get())) {
> +			eglImageBayerOut_.erase(output_cache);
> +			return nullptr;
> +		}
> +		outputRing_.push_back(fd);
>   	}
> +	eGLImage *eglImageOut = output_cache->second.get();
> +
> +	return eglImageOut;
> +}
> +
> +int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
> +{
> +	eGLImage *eglImageIn;
> +	eGLImage *eglImageOut;
> +
> +	/* eGL context switch */
> +	egl_.makeCurrent();
>   
> -	/* Generate the output render framebuffer as render to texture */
> -	egl_.createOutputDMABufTexture2D(*eglImageBayerOut_, output->planes()[0].fd.get());
> +	eglImageIn = getCachedInputFrameBuffer(input, inMapped, inDmaSyncer);
> +	if (!eglImageIn)
> +		return -ENOMEM;
> +	eglImageOut = getCachedOutputFrameBuffer(output);
> +	if (!eglImageOut)
> +		return -ENOMEM;
> +
> +	egl_.attachTextureToFBO(*eglImageOut);
> +	setShaderVariableValues(*eglImageIn, params);
>   
> -	setShaderVariableValues(*eglImageBayerIn_, params);
>   	glViewport(0, 0, width_, height_);
>   	glClear(GL_COLOR_BUFFER_BIT);
>   	glDrawArrays(GL_TRIANGLE_FAN, 0, DEBAYER_OPENGL_COORDS);
> @@ -615,19 +677,15 @@ int DebayerEGL::start()
>   	if (initBayerShaders(inputPixelFormat_, outputPixelFormat_))
>   		return -EINVAL;
>   
> -	/* Raw bayer input as texture */
> -	eglImageBayerIn_ = std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_, height_, inputConfig_.stride, GL_TEXTURE0, 0);
> -
> -	/* Texture we will render to */
> -	eglImageBayerOut_ = std::make_unique<eGLImage>(GL_RGBA, outputSize_.width, outputSize_.height, outputConfig_.stride, GL_TEXTURE1, 1);
> -
>   	return 0;
>   }
>   
>   void DebayerEGL::stop()
>   {
> -	eglImageBayerOut_.reset();
> -	eglImageBayerIn_.reset();
> +	eglImageBayerOut_.clear();
> +	eglImageBayerIn_.clear();
> +	outputRing_.clear();
> +	inputRing_.clear();
>   
>   	if (programId_)
>   		glDeleteProgram(programId_);
> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
> index 348d7305b..dde6ba9bf 100644
> --- a/src/libcamera/software_isp/debayer_egl.h
> +++ b/src/libcamera/software_isp/debayer_egl.h
> @@ -9,9 +9,11 @@
>   
>   #pragma once
>   
> +#include <deque>
>   #include <memory>
>   #include <stdint.h>
>   #include <tuple>
> +#include <unordered_map>
>   #include <vector>
>   
>   #define GL_GLEXT_PROTOTYPES
> @@ -68,14 +70,21 @@ private:
>   	void setShaderVariableValues(eGLImage &eGLImageIn, const DebayerParams &params);
>   	int debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *mappedInputBuffer, std::optional<DmaSyncer> *inputBufferDmaSyncer);
>   
> +	eGLImage *getCachedInputFrameBuffer(FrameBuffer *input, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer);
> +	eGLImage *getCachedOutputFrameBuffer(FrameBuffer *output);
> +
>   	/* Shader program identifiers */
>   	GLuint vertexShaderId_ = 0;
>   	GLuint fragmentShaderId_ = 0;
>   	GLuint programId_ = 0;
>   
>   	/* Pointer to object representing input texture */
> -	std::unique_ptr<eGLImage> eglImageBayerIn_;
> -	std::unique_ptr<eGLImage> eglImageBayerOut_;
> +	std::unordered_map<SharedFD, std::unique_ptr<eGLImage>> eglImageBayerIn_;
> +	std::unordered_map<SharedFD, std::unique_ptr<eGLImage>> eglImageBayerOut_;
> +	std::deque<SharedFD> inputRing_;
> +	std::deque<SharedFD> outputRing_;

Using an unordered_map / hashmap and then duplicating the keys in 
another ordered list feels redundant - could we just use something like 
std::deque<std::pair<SharedFD, std::unique_ptr<eGLImage>>> instead?

That would require us to iterate over the values instead of using hashes 
- which, however, should be both faster (as the number of buffers is so 
small) as well as allowing us to drop the previous commit ("shared_fd: 
Add hash template") again IIUC.

> +	unsigned int inputBufferCount_;
> +	unsigned int outputBufferCount_;
>   
>   	/* Shader parameters */
>   	float firstRed_x_;

Patch
diff mbox series

diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index c3a7b7952..ca3353d9f 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -345,6 +345,9 @@  int DebayerEGL::configure(const StreamConfiguration &inputCfg,
 	 */
 	stats_->setWindow(Rectangle(window_.size()));
 
+	inputBufferCount_ = inputCfg.bufferCount;
+	outputBufferCount_ = outputCfg.bufferCount;
+
 	return 0;
 }
 
@@ -504,36 +507,95 @@  void DebayerEGL::setShaderVariableValues(eGLImage &eglImageIn, const DebayerPara
 	return;
 }
 
-int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
+eGLImage *DebayerEGL::getCachedInputFrameBuffer(FrameBuffer *input, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
 {
-	bool dmabuf_import_succeeded = false;
-
-	/* eGL context switch */
-	egl_.makeCurrent();
-
-	/* Try to create texture for input buffer via dmabuf import */
-	if (!eglImageBayerIn_->dmabuf_import_failed_) {
-		if (egl_.createInputDMABufTexture2D(*eglImageBayerIn_, input->planes()[0].fd.get()) == 0)
-			dmabuf_import_succeeded = true;
-		else
-			LOG(Debayer, Info) << "Importing input buffer with DMABuf import failed, falling back to upload";
+	const SharedFD &fd = input->planes()[0].fd;
+	bool use_dmabuf = false;
+
+	auto [input_cache, cache_miss] = eglImageBayerIn_.try_emplace(fd);
+	if (cache_miss) {
+		if (eglImageBayerIn_.size() > inputBufferCount_) {
+			eglImageBayerIn_.erase(inputRing_.front());
+			inputRing_.pop_front();
+			LOG(Debayer, Error) << "Input cache " << inputBufferCount_ << " exceeded evicted entry";
+		}
+		input_cache->second = std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_,
+								 height_, inputConfig_.stride, GL_TEXTURE0, 0);
+		inputRing_.push_back(fd);
+	}
+	eGLImage *eglImageIn = input_cache->second.get();
+
+	/* Try to create texture for input buffer via dmabuf import only on cache miss */
+	if (cache_miss) {
+		if (egl_.createInputDMABufTexture2D(*eglImageIn, input->planes()[0].fd.get()) == 0)
+			use_dmabuf = true;
+	} else if (!eglImageIn->dmabuf_import_failed_) {
+		use_dmabuf = true;
+		egl_.activateBindTexture(*eglImageIn);
 	}
 
-	/* Otherwise create texture for input buffer via upload from CPU */
-	if (!dmabuf_import_succeeded) {
+	if (!use_dmabuf) {
 		inDmaSyncer->emplace(input->planes()[0].fd, DmaSyncer::SyncType::Read);
 		inMapped->emplace(input, MappedFrameBuffer::MapFlag::Read);
 		if (!inMapped->value().isValid()) {
 			LOG(Debayer, Error) << "mmap-ing buffer(s) failed";
-			return -ENODEV;
+			if (cache_miss) {
+				eglImageBayerIn_.erase(input_cache);
+				inputRing_.pop_back();
+			}
+			return nullptr;
+		}
+		if (cache_miss)
+			egl_.createTexture2D(*eglImageIn, inMapped->value().planes()[0].data());
+		else
+			egl_.updateTexture2D(*eglImageIn, inMapped->value().planes()[0].data());
+	}
+
+	return eglImageIn;
+}
+
+eGLImage *DebayerEGL::getCachedOutputFrameBuffer(FrameBuffer *output)
+{
+	const SharedFD &fd = output->planes()[0].fd;
+
+	auto [output_cache, cache_miss] = eglImageBayerOut_.try_emplace(fd);
+	if (cache_miss) {
+		if (eglImageBayerOut_.size() > outputBufferCount_) {
+			eglImageBayerOut_.erase(outputRing_.front());
+			outputRing_.pop_front();
+			LOG(Debayer, Error) << "Output cache " << outputBufferCount_ << " exceeded evicted entry";
 		}
-		egl_.createTexture2D(*eglImageBayerIn_, inMapped->value().planes()[0].data());
+		output_cache->second = std::make_unique<eGLImage>(GL_RGBA, outputSize_.width,
+								  outputSize_.height, outputConfig_.stride, GL_TEXTURE1, 1);
+		if (egl_.createOutputDMABufTexture2D(*output_cache->second, output->planes()[0].fd.get())) {
+			eglImageBayerOut_.erase(output_cache);
+			return nullptr;
+		}
+		outputRing_.push_back(fd);
 	}
+	eGLImage *eglImageOut = output_cache->second.get();
+
+	return eglImageOut;
+}
+
+int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
+{
+	eGLImage *eglImageIn;
+	eGLImage *eglImageOut;
+
+	/* eGL context switch */
+	egl_.makeCurrent();
 
-	/* Generate the output render framebuffer as render to texture */
-	egl_.createOutputDMABufTexture2D(*eglImageBayerOut_, output->planes()[0].fd.get());
+	eglImageIn = getCachedInputFrameBuffer(input, inMapped, inDmaSyncer);
+	if (!eglImageIn)
+		return -ENOMEM;
+	eglImageOut = getCachedOutputFrameBuffer(output);
+	if (!eglImageOut)
+		return -ENOMEM;
+
+	egl_.attachTextureToFBO(*eglImageOut);
+	setShaderVariableValues(*eglImageIn, params);
 
-	setShaderVariableValues(*eglImageBayerIn_, params);
 	glViewport(0, 0, width_, height_);
 	glClear(GL_COLOR_BUFFER_BIT);
 	glDrawArrays(GL_TRIANGLE_FAN, 0, DEBAYER_OPENGL_COORDS);
@@ -615,19 +677,15 @@  int DebayerEGL::start()
 	if (initBayerShaders(inputPixelFormat_, outputPixelFormat_))
 		return -EINVAL;
 
-	/* Raw bayer input as texture */
-	eglImageBayerIn_ = std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_, height_, inputConfig_.stride, GL_TEXTURE0, 0);
-
-	/* Texture we will render to */
-	eglImageBayerOut_ = std::make_unique<eGLImage>(GL_RGBA, outputSize_.width, outputSize_.height, outputConfig_.stride, GL_TEXTURE1, 1);
-
 	return 0;
 }
 
 void DebayerEGL::stop()
 {
-	eglImageBayerOut_.reset();
-	eglImageBayerIn_.reset();
+	eglImageBayerOut_.clear();
+	eglImageBayerIn_.clear();
+	outputRing_.clear();
+	inputRing_.clear();
 
 	if (programId_)
 		glDeleteProgram(programId_);
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index 348d7305b..dde6ba9bf 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -9,9 +9,11 @@ 
 
 #pragma once
 
+#include <deque>
 #include <memory>
 #include <stdint.h>
 #include <tuple>
+#include <unordered_map>
 #include <vector>
 
 #define GL_GLEXT_PROTOTYPES
@@ -68,14 +70,21 @@  private:
 	void setShaderVariableValues(eGLImage &eGLImageIn, const DebayerParams &params);
 	int debayerGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *mappedInputBuffer, std::optional<DmaSyncer> *inputBufferDmaSyncer);
 
+	eGLImage *getCachedInputFrameBuffer(FrameBuffer *input, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer);
+	eGLImage *getCachedOutputFrameBuffer(FrameBuffer *output);
+
 	/* Shader program identifiers */
 	GLuint vertexShaderId_ = 0;
 	GLuint fragmentShaderId_ = 0;
 	GLuint programId_ = 0;
 
 	/* Pointer to object representing input texture */
-	std::unique_ptr<eGLImage> eglImageBayerIn_;
-	std::unique_ptr<eGLImage> eglImageBayerOut_;
+	std::unordered_map<SharedFD, std::unique_ptr<eGLImage>> eglImageBayerIn_;
+	std::unordered_map<SharedFD, std::unique_ptr<eGLImage>> eglImageBayerOut_;
+	std::deque<SharedFD> inputRing_;
+	std::deque<SharedFD> outputRing_;
+	unsigned int inputBufferCount_;
+	unsigned int outputBufferCount_;
 
 	/* Shader parameters */
 	float firstRed_x_;