[libcamera-devel,RFC,v2,5/7] libcamera: software_isp: add SwIspLinaro implementation of SoftwareIsp
diff mbox series

Message ID 20231212115046.102726-6-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
The implementation of SoftwareIsp handles creation of Soft IPA
and interactions with it, so that the pipeline handler wouldn't
need to care about the Soft IPA.

Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>
---
 include/libcamera/internal/meson.build        |   1 +
 .../internal/software_isp/meson.build         |   1 +
 .../internal/software_isp/swisp_linaro.h      | 117 ++++
 src/libcamera/meson.build                     |   1 +
 src/libcamera/software_isp/meson.build        |  20 +
 src/libcamera/software_isp/swisp_linaro.cpp   | 589 ++++++++++++++++++
 6 files changed, 729 insertions(+)
 create mode 100644 include/libcamera/internal/software_isp/swisp_linaro.h
 create mode 100644 src/libcamera/software_isp/meson.build
 create mode 100644 src/libcamera/software_isp/swisp_linaro.cpp

Patch
diff mbox series

diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
index b780777c..eeae801c 100644
--- a/include/libcamera/internal/meson.build
+++ b/include/libcamera/internal/meson.build
@@ -50,3 +50,4 @@  libcamera_internal_headers = files([
 ])
 
 subdir('converter')
+subdir('software_isp')
diff --git a/include/libcamera/internal/software_isp/meson.build b/include/libcamera/internal/software_isp/meson.build
index a24fe7df..9f84f00e 100644
--- a/include/libcamera/internal/software_isp/meson.build
+++ b/include/libcamera/internal/software_isp/meson.build
@@ -2,4 +2,5 @@ 
 
 libcamera_internal_headers += files([
     'statistics-linaro.h',
+    'swisp_linaro.h',
 ])
diff --git a/include/libcamera/internal/software_isp/swisp_linaro.h b/include/libcamera/internal/software_isp/swisp_linaro.h
new file mode 100644
index 00000000..c0df7863
--- /dev/null
+++ b/include/libcamera/internal/software_isp/swisp_linaro.h
@@ -0,0 +1,117 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * swisp_linaro.h - software ISP implementation by Linaro
+ */
+
+#pragma once
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/thread.h>
+
+#include <libcamera/pixel_format.h>
+
+#include <libcamera/ipa/soft_ipa_interface.h>
+#include <libcamera/ipa/soft_ipa_proxy.h>
+
+#include "libcamera/internal/shared_mem_object.h"
+#include "libcamera/internal/software_isp.h"
+#include "libcamera/internal/software_isp/statistics-linaro.h"
+
+namespace libcamera {
+
+class SwIspLinaro : public SoftwareIsp
+{
+public:
+	SwIspLinaro(PipelineHandler *pipe, const ControlInfoMap &sensorControls);
+	~SwIspLinaro() {}
+
+	int loadConfiguration([[maybe_unused]] const std::string &filename) { return 0; }
+	bool isValid() const;
+
+	std::vector<PixelFormat> formats(PixelFormat input);
+	SizeRange sizes(PixelFormat inputFormat, const Size &inputSize);
+
+	std::tuple<unsigned int, unsigned int>
+	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size);
+
+	int configure(const StreamConfiguration &inputCfg,
+		      const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs);
+	int exportBuffers(unsigned int output, unsigned int count,
+			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
+	void processStats(const ControlList &sensorControls);
+
+	int start();
+	void stop();
+
+	int queueBuffers(FrameBuffer *input,
+			 const std::map<unsigned int, FrameBuffer *> &outputs);
+
+	Signal<const ControlList &> &getSignalSetSensorControls();
+
+	void process(FrameBuffer *input, FrameBuffer *output);
+
+private:
+	SharedMemObject<SwIspStats> sharedStats_;
+
+	class IspWorker : public Object
+	{
+	public:
+		IspWorker(SwIspLinaro *swIsp);
+
+		std::vector<PixelFormat> formats(PixelFormat input);
+		SizeRange sizes(PixelFormat inputFormat, const Size &inputSize);
+		unsigned int outStride(const PixelFormat &outputFormat,
+				       const Size &outSize);
+
+		int configure(const StreamConfiguration &inputCfg,
+			      const StreamConfiguration &outputCfg);
+		unsigned int outBufferSize();
+		void process(FrameBuffer *input, FrameBuffer *output);
+
+	private:
+		SwIspLinaro *swIsp_;
+
+		typedef void (SwIspLinaro::IspWorker::*debayerFn)(uint8_t *dst, const uint8_t *src);
+		typedef SizeRange (*outSizesFn)(const Size &inSize);
+		typedef unsigned int (*outStrideFn)(const Size &outSize);
+		struct debayerInfo {
+			PixelFormat outPixelFmt;
+			debayerFn debayer;
+			outSizesFn getOutSizes;
+			outStrideFn getOutStride;
+		};
+		// TODO: use inputFormat+outputFormat as the map key
+		// to enable multiple output formats
+		// TODO: use BayerFormat instead of PixelFormat as inputFormat
+		std::map<PixelFormat, IspWorker::debayerInfo> debayerInfos_;
+		int setDebayerInfo(PixelFormat format);
+		debayerInfo *debayerInfo_;
+
+		/* CSI-2 packed 10-bit raw bayer format (all the 4 orders) */
+		void debayerRaw10P(uint8_t *dst, const uint8_t *src);
+		static SizeRange outSizesRaw10P(const Size &inSize);
+		static unsigned int outStrideRaw10P(const Size &outSize);
+
+		unsigned int width_;
+		unsigned int height_;
+		unsigned int stride_;
+		Point redShift_;
+		unsigned int outHeight_;
+		unsigned int outStride_;
+
+		unsigned long rNumerat_, rDenomin_; /* red gain for AWB */
+		unsigned long bNumerat_, bDenomin_; /* blue gain for AWB */
+		unsigned long gNumerat_, gDenomin_; /* green gain for AWB */
+
+		SwIspStats stats_;
+	};
+
+	std::unique_ptr<IspWorker> ispWorker_;
+	Thread ispWorkerThread_;
+
+	std::unique_ptr<ipa::soft::IPAProxySoft> ipa_;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index b3606969..e758ac9c 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -70,6 +70,7 @@  subdir('converter')
 subdir('ipa')
 subdir('pipeline')
 subdir('proxy')
+subdir('software_isp')
 
 null_dep = dependency('', required : false)
 
diff --git a/src/libcamera/software_isp/meson.build b/src/libcamera/software_isp/meson.build
new file mode 100644
index 00000000..d4a8d499
--- /dev/null
+++ b/src/libcamera/software_isp/meson.build
@@ -0,0 +1,20 @@ 
+# SPDX-License-Identifier: CC0-1.0
+
+# Software ISP is enabled for 'simple' pipeline handler.
+# The 'pipelines' option value should be
+# 'simple/<Software ISP implementation>' e.g.:
+#   -Dpipelines=simple/linaro
+# The source file should be named swisp_<Software ISP implementation>.cpp,
+# e.g. 'swisp_linaro.cpp'.
+
+foreach pipeline : pipelines
+    pipeline = pipeline.split('/')
+    if pipeline.length() == 2 and pipeline[0] == 'simple'
+        libcamera_sources += files([
+            'swisp_' + pipeline[1] + '.cpp',
+        ])
+        # the 'break' below can be removed if/when multiple
+        # Software ISP implementations are allowed in single build
+        break
+    endif
+endforeach
diff --git a/src/libcamera/software_isp/swisp_linaro.cpp b/src/libcamera/software_isp/swisp_linaro.cpp
new file mode 100644
index 00000000..b7f36db1
--- /dev/null
+++ b/src/libcamera/software_isp/swisp_linaro.cpp
@@ -0,0 +1,589 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * swisp_linaro.cpp - software ISP implementation by Linaro
+ */
+
+#include "libcamera/internal/software_isp/swisp_linaro.h"
+
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <libcamera/formats.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/framebuffer.h"
+#include "libcamera/internal/ipa_manager.h"
+#include "libcamera/internal/mapped_framebuffer.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(SoftwareIsp)
+
+SwIspLinaro::SwIspLinaro(PipelineHandler *pipe, const ControlInfoMap &sensorControls)
+	: SoftwareIsp(pipe, sensorControls)
+{
+	ispWorker_ = std::make_unique<SwIspLinaro::IspWorker>(this);
+	if (!ispWorker_) {
+		LOG(SoftwareIsp, Error)
+			<< "Failed to create ISP worker";
+		return;
+	}
+
+	sharedStats_ = SharedMemObject<SwIspStats>("softIsp_stats");
+	if (!sharedStats_.fd().isValid()) {
+		LOG(SoftwareIsp, Error)
+			<< "Failed to create shared memory for statistics";
+		ispWorker_.reset();
+		return;
+	}
+
+	ipa_ = IPAManager::createIPA<ipa::soft::IPAProxySoft>(pipe, 0, 0);
+	if (!ipa_) {
+		LOG(SoftwareIsp, Error)
+			<< "Creating IPA for software ISP failed";
+		ispWorker_.reset();
+		return;
+	}
+
+	int ret = ipa_->init(IPASettings{ "No cfg file", "No sensor model" },
+			     sharedStats_.fd(),
+			     sensorControls);
+	if (ret) {
+		LOG(SoftwareIsp, Error) << "IPA init failed";
+		ispWorker_.reset();
+		return;
+	}
+
+	ipa_->configure(sensorControls);
+
+	ispWorker_->moveToThread(&ispWorkerThread_);
+}
+
+void SwIspLinaro::processStats(const ControlList &sensorControls)
+{
+	ASSERT(ipa_);
+	ipa_->processStats(sensorControls);
+}
+
+Signal<const ControlList &> &SwIspLinaro::getSignalSetSensorControls()
+{
+	ASSERT(ipa_);
+	return ipa_->setSensorControls;
+}
+
+bool SwIspLinaro::isValid() const
+{
+	return !!ispWorker_;
+}
+
+/*
+ * Demosaic Raw10P frame into RGB888 format.
+ * Two LS bits of the RAW10P's total 10 are ignored.
+ * Also this function performs the statistics calculations, and has a fairly
+ * naive grey world AWB algorithm implementation.
+ * \todo Split the stats calculations out of this function.
+ * \todo Move the AWB algorithm into the IPA module.
+ */
+void SwIspLinaro::IspWorker::debayerRaw10P(uint8_t *dst, const uint8_t *src)
+{
+	/* for brightness values in the 0 to 255 range: */
+	static const unsigned int BRIGHT_LVL = 200U << 8;
+	static const unsigned int TOO_BRIGHT_LVL = 240U << 8;
+
+	static const unsigned int RED_Y_MUL = 77;	/* 0.30 * 256 */
+	static const unsigned int GREEN_Y_MUL = 150;	/* 0.59 * 256 */
+	static const unsigned int BLUE_Y_MUL = 29;	/* 0.11 * 256 */
+
+	int w_out = width_ - 2;
+	int h_out = height_ - 2;
+
+	unsigned long sumR = 0;
+	unsigned long sumB = 0;
+	unsigned long sumG = 0;
+
+	unsigned long bright_sum = 0;
+	unsigned long too_bright_sum = 0;
+
+	for (int y = 0; y < h_out; y++) {
+		const uint8_t *pin_base = src + (y + 1) * stride_;
+		uint8_t *pout = dst + y * w_out * 3;
+		int phase_y = (y + redShift_.y) % 2;
+
+		for (int x = 0; x < w_out; x++) {
+			int phase_x = (x + redShift_.x) % 2;
+			int phase = 2 * phase_y + phase_x;
+
+			/* x part of the offset in the input buffer: */
+			int x_m1 = x + x / 4;		/* offset for (x-1) */
+			int x_0 = x + 1 + (x + 1) / 4;	/* offset for x */
+			int x_p1 = x + 2 + (x + 2) / 4;	/* offset for (x+1) */
+			/* the colour component value to write to the output */
+			unsigned val;
+			/* Y value times 256 */
+			unsigned y_val;
+
+			switch (phase) {
+			case 0: /* at R pixel */
+				/* blue: ((-1,-1)+(1,-1)+(-1,1)+(1,1)) / 4 */
+				val = ( *(pin_base + x_m1 - stride_)
+					+ *(pin_base + x_p1 - stride_)
+					+ *(pin_base + x_m1 + stride_)
+					+ *(pin_base + x_p1 + stride_) ) >> 2;
+				y_val = BLUE_Y_MUL * val;
+				val = val * bNumerat_ / bDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* green: ((0,-1)+(-1,0)+(1,0)+(0,1)) / 4 */
+				val = ( *(pin_base + x_0 - stride_)
+					+ *(pin_base + x_p1)
+					+ *(pin_base + x_m1)
+					+ *(pin_base + x_0 + stride_) ) >> 2;
+				val = val * gNumerat_ / gDenomin_;
+				y_val += GREEN_Y_MUL * val;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* red: (0,0) */
+				val = *(pin_base + x_0);
+				sumR += val;
+				y_val += RED_Y_MUL * val;
+				if (y_val > BRIGHT_LVL) ++bright_sum;
+				if (y_val > TOO_BRIGHT_LVL) ++too_bright_sum;
+				val = val * rNumerat_ / rDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				break;
+			case 1: /* at Gr pixel */
+				/* blue: ((0,-1) + (0,1)) / 2 */
+				val = ( *(pin_base + x_0 - stride_)
+					+ *(pin_base + x_0 + stride_) ) >> 1;
+				y_val = BLUE_Y_MUL * val;
+				val = val * bNumerat_ / bDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* green: (0,0) */
+				val = *(pin_base + x_0);
+				sumG += val;
+				y_val += GREEN_Y_MUL * val;
+				val = val * gNumerat_ / gDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* red: ((-1,0) + (1,0)) / 2 */
+				val = ( *(pin_base + x_m1)
+					+ *(pin_base + x_p1) ) >> 1;
+				y_val += RED_Y_MUL * val;
+				if (y_val > BRIGHT_LVL) ++bright_sum;
+				if (y_val > TOO_BRIGHT_LVL) ++too_bright_sum;
+				val = val * rNumerat_ / rDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				break;
+			case 2: /* at Gb pixel */
+				/* blue: ((-1,0) + (1,0)) / 2 */
+				val = ( *(pin_base + x_m1)
+					+ *(pin_base + x_p1) ) >> 1;
+				y_val = BLUE_Y_MUL * val;
+				val = val * bNumerat_ / bDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* green: (0,0) */
+				val = *(pin_base + x_0);
+				sumG += val;
+				y_val += GREEN_Y_MUL * val;
+				val = val * gNumerat_ / gDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* red: ((0,-1) + (0,1)) / 2 */
+				val = ( *(pin_base + x_0 - stride_)
+					+ *(pin_base + x_0 + stride_) ) >> 1;
+				y_val += RED_Y_MUL * val;
+				if (y_val > BRIGHT_LVL) ++bright_sum;
+				if (y_val > TOO_BRIGHT_LVL) ++too_bright_sum;
+				val = val * rNumerat_ / rDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				break;
+			default: /* at B pixel */
+				/* blue: (0,0) */
+				val = *(pin_base + x_0);
+				sumB += val;
+				y_val = BLUE_Y_MUL * val;
+				val = val * bNumerat_ / bDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* green: ((0,-1)+(-1,0)+(1,0)+(0,1)) / 4 */
+				val = ( *(pin_base + x_0 - stride_)
+					+ *(pin_base + x_p1)
+					+ *(pin_base + x_m1)
+					+ *(pin_base + x_0 + stride_) ) >> 2;
+				y_val += GREEN_Y_MUL * val;
+				val = val * gNumerat_ / gDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+				/* red: ((-1,-1)+(1,-1)+(-1,1)+(1,1)) / 4 */
+				val = ( *(pin_base + x_m1 - stride_)
+					+ *(pin_base + x_p1 - stride_)
+					+ *(pin_base + x_m1 + stride_)
+					+ *(pin_base + x_p1 + stride_) ) >> 2;
+				y_val += RED_Y_MUL * val;
+				if (y_val > BRIGHT_LVL) ++bright_sum;
+				if (y_val > TOO_BRIGHT_LVL) ++too_bright_sum;
+				val = val * rNumerat_ / rDenomin_;
+				*pout++ = (uint8_t)std::min(val, 0xffU);
+			}
+		}
+	}
+
+	/* calculate the fractions of "bright" and "too bright" pixels */
+	stats_.bright_ratio = (float)bright_sum / (h_out * w_out);
+	stats_.too_bright_ratio = (float)too_bright_sum / (h_out * w_out);
+
+	/* calculate red and blue gains for simple AWB */
+	LOG(SoftwareIsp, Debug)
+		<< "sumR = " << sumR << ", sumB = " << sumB << ", sumG = " << sumG;
+
+	sumG /= 2; /* the number of G pixels is twice as big vs R and B ones */
+
+	/* normalize red, blue, and green sums to fit into 22-bit value */
+	unsigned long fRed = sumR / 0x400000;
+	unsigned long fBlue = sumB / 0x400000;
+	unsigned long fGreen = sumG / 0x400000;
+	unsigned long fNorm = std::max({ 1UL, fRed, fBlue, fGreen });
+	sumR /= fNorm;
+	sumB /= fNorm;
+	sumG /= fNorm;
+
+	LOG(SoftwareIsp, Debug) << "fNorm = " << fNorm;
+	LOG(SoftwareIsp, Debug)
+		<< "Normalized: sumR = " << sumR
+		<< ", sumB= " << sumB << ", sumG = " << sumG;
+
+	/* make sure red/blue gains never exceed approximately 256 */
+	unsigned long minDenom;
+	rNumerat_ = (sumR + sumB + sumG) / 3;
+	minDenom = rNumerat_ / 0x100;
+	rDenomin_ = std::max(minDenom, sumR);
+	bNumerat_ = rNumerat_;
+	bDenomin_ = std::max(minDenom, sumB);
+	gNumerat_ = rNumerat_;
+	gDenomin_ = std::max(minDenom, sumG);
+
+	LOG(SoftwareIsp, Debug)
+		<< "rGain = [ " << rNumerat_ << " / " << rDenomin_
+		<< " ], bGain = [ " << bNumerat_ << " / " << bDenomin_
+		<< " ], gGain = [ " << gNumerat_ << " / " << gDenomin_
+		<< " (minDenom = " << minDenom << ")";
+}
+
+SizeRange SwIspLinaro::IspWorker::outSizesRaw10P(const Size &inSize)
+{
+	if (inSize.width < 2 || inSize.height < 2) {
+		LOG(SoftwareIsp, Error)
+			<< "Input format size too small: " << inSize.toString();
+		return {};
+	}
+
+	return SizeRange(Size(inSize.width - 2, inSize.height - 2));
+}
+
+unsigned int SwIspLinaro::IspWorker::outStrideRaw10P(const Size &outSize)
+{
+	return outSize.width * 3;
+}
+
+SwIspLinaro::IspWorker::IspWorker(SwIspLinaro *swIsp)
+	: swIsp_(swIsp)
+{
+	debayerInfos_[formats::SBGGR10_CSI2P] = { formats::RGB888,
+						  &SwIspLinaro::IspWorker::debayerRaw10P,
+						  &SwIspLinaro::IspWorker::outSizesRaw10P,
+						  &SwIspLinaro::IspWorker::outStrideRaw10P };
+	debayerInfos_[formats::SGBRG10_CSI2P] = { formats::RGB888,
+						  &SwIspLinaro::IspWorker::debayerRaw10P,
+						  &SwIspLinaro::IspWorker::outSizesRaw10P,
+						  &SwIspLinaro::IspWorker::outStrideRaw10P };
+	debayerInfos_[formats::SGRBG10_CSI2P] = { formats::RGB888,
+						  &SwIspLinaro::IspWorker::debayerRaw10P,
+						  &SwIspLinaro::IspWorker::outSizesRaw10P,
+						  &SwIspLinaro::IspWorker::outStrideRaw10P };
+	debayerInfos_[formats::SRGGB10_CSI2P] = { formats::RGB888,
+						  &SwIspLinaro::IspWorker::debayerRaw10P,
+						  &SwIspLinaro::IspWorker::outSizesRaw10P,
+						  &SwIspLinaro::IspWorker::outStrideRaw10P };
+}
+
+int SwIspLinaro::IspWorker::setDebayerInfo(PixelFormat format)
+{
+	const auto it = debayerInfos_.find(format);
+	if (it == debayerInfos_.end())
+		return -1;
+
+	debayerInfo_ = &it->second;
+	return 0;
+}
+
+std::vector<PixelFormat> SwIspLinaro::IspWorker::formats(PixelFormat input)
+{
+	std::vector<PixelFormat> pixelFormats;
+
+	const auto it = debayerInfos_.find(input);
+	if (it == debayerInfos_.end())
+		LOG(SoftwareIsp, Info)
+			<< "Unsupported input format " << input.toString();
+	else
+		pixelFormats.push_back(it->second.outPixelFmt);
+
+	return pixelFormats;
+}
+
+SizeRange SwIspLinaro::IspWorker::sizes(PixelFormat inputFormat,
+					const Size &inputSize)
+{
+	const auto it = debayerInfos_.find(inputFormat);
+	if (it == debayerInfos_.end()) {
+		LOG(SoftwareIsp, Info)
+			<< "Unsupported input format " << inputFormat.toString();
+		return {};
+	}
+
+	return (*it->second.getOutSizes)(inputSize);
+}
+
+unsigned int SwIspLinaro::IspWorker::outStride(const PixelFormat &outputFormat,
+					       const Size &outSize)
+{
+	/*
+	 * Assuming that the output stride depends only on the outputFormat,
+	 * we use the first debayerInfos_ entry with the matching output format
+	 */
+	for (auto it = debayerInfos_.begin(); it != debayerInfos_.end(); it++) {
+		if (it->second.outPixelFmt == outputFormat)
+			return (*it->second.getOutStride)(outSize);
+	}
+
+	return 0;
+}
+
+int SwIspLinaro::IspWorker::configure(const StreamConfiguration &inputCfg,
+				      const StreamConfiguration &outputCfg)
+{
+	if (setDebayerInfo(inputCfg.pixelFormat) != 0) {
+		LOG(SoftwareIsp, Error)
+			<< "Input format " << inputCfg.pixelFormat
+			<< "not supported";
+		return -EINVAL;
+	}
+
+	/* check that:
+	 * - output format is valid
+	 * - output size matches the input size and is valid */
+	SizeRange outSizeRange = (*debayerInfo_->getOutSizes)(inputCfg.size);
+	if (debayerInfo_->outPixelFmt != outputCfg.pixelFormat ||
+	    outputCfg.size.isNull() || !outSizeRange.contains(outputCfg.size) ||
+	    (*debayerInfo_->getOutStride)(outputCfg.size) != outputCfg.stride) {
+		LOG(SoftwareIsp, Error)
+			<< "Invalid output format/size/stride: "
+			<< "\n  " << outputCfg.pixelFormat << " ("
+			<< debayerInfo_->outPixelFmt << ")"
+			<< "\n  " << outputCfg.size << " ("
+			<< outSizeRange << ")"
+			<< "\n  " << outputCfg.stride << " ("
+			<< (*debayerInfo_->getOutStride)(outputCfg.size) << ")";
+		return -EINVAL;
+	}
+
+	width_ = inputCfg.size.width;
+	height_ = inputCfg.size.height;
+	stride_ = inputCfg.stride;
+
+	BayerFormat bayerFormat =
+		BayerFormat::fromPixelFormat(inputCfg.pixelFormat);
+	switch (bayerFormat.order) {
+	case BayerFormat::BGGR:
+		redShift_ = Point(0, 0);
+		break;
+	case BayerFormat::GBRG:
+		redShift_ = Point(1, 0);
+		break;
+	case BayerFormat::GRBG:
+		redShift_ = Point(0, 1);
+		break;
+	case BayerFormat::RGGB:
+	default:
+		redShift_ = Point(1, 1);
+		break;
+	}
+
+	outStride_ = outputCfg.stride;
+	outHeight_ = outputCfg.size.height;
+
+	LOG(SoftwareIsp, Info)
+		<< "SoftwareISP configuration: "
+		<< inputCfg.size << "-" << inputCfg.pixelFormat << " -> "
+		<< outputCfg.size << "-" << outputCfg.pixelFormat;
+
+	/* set r/g/b gains to 1.0 until frame data collected */
+	rNumerat_ = rDenomin_ = 1;
+	bNumerat_ = bDenomin_ = 1;
+	gNumerat_ = gDenomin_ = 1;
+
+	return 0;
+}
+
+/* May not be called before SwIspLinaro::IspWorker::configure() */
+unsigned int SwIspLinaro::IspWorker::outBufferSize()
+{
+	return outHeight_ * outStride_;
+}
+
+std::vector<PixelFormat> SwIspLinaro::formats(PixelFormat inputFormat)
+{
+	ASSERT(ispWorker_ != nullptr);
+
+	return ispWorker_->formats(inputFormat);
+}
+
+SizeRange SwIspLinaro::sizes(PixelFormat inputFormat, const Size &inputSize)
+{
+	ASSERT(ispWorker_ != nullptr);
+
+	return ispWorker_->sizes(inputFormat, inputSize);
+}
+
+std::tuple<unsigned int, unsigned int>
+SwIspLinaro::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size)
+{
+	ASSERT(ispWorker_ != nullptr);
+
+	unsigned int stride = ispWorker_->outStride(outputFormat, size);
+
+	return std::make_tuple(stride, stride * size.height);
+}
+
+int SwIspLinaro::configure(const StreamConfiguration &inputCfg,
+			   const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs)
+{
+	ASSERT(ispWorker_ != nullptr);
+
+	if (outputCfgs.size() != 1) {
+		LOG(SoftwareIsp, Error)
+			<< "Unsupported number of output streams: "
+			<< outputCfgs.size();
+		return -EINVAL;
+	}
+
+	return ispWorker_->configure(inputCfg, outputCfgs[0]);
+}
+
+int SwIspLinaro::exportBuffers(unsigned int output, unsigned int count,
+			       std::vector<std::unique_ptr<FrameBuffer>> *buffers)
+{
+	ASSERT(ispWorker_ != nullptr);
+
+	/* single output for now */
+	if (output >= 1)
+		return -EINVAL;
+
+	unsigned int bufSize = ispWorker_->outBufferSize();
+
+	/* TODO: allocate from dma_heap; memfd buffs aren't allowed in FrameBuffer */
+	for (unsigned int i = 0; i < count; i++) {
+		std::string name = "frame-" + std::to_string(i);
+
+		const int ispFd = memfd_create(name.c_str(), 0);
+		int ret = ftruncate(ispFd, bufSize);
+		if (ret < 0) {
+			LOG(SoftwareIsp, Error)
+				<< "ftruncate() for memfd failed "
+				<< strerror(-ret);
+			return ret;
+		}
+
+		FrameBuffer::Plane outPlane;
+		outPlane.fd = SharedFD(std::move(ispFd));
+		outPlane.offset = 0;
+		outPlane.length = bufSize;
+
+		std::vector<FrameBuffer::Plane> planes{ outPlane };
+		buffers->emplace_back(std::make_unique<FrameBuffer>(std::move(planes)));
+	}
+
+	return count;
+}
+
+int SwIspLinaro::queueBuffers(FrameBuffer *input,
+			      const std::map<unsigned int, FrameBuffer *> &outputs)
+{
+	unsigned int mask = 0;
+
+	/*
+	 * Validate the outputs as a sanity check: at least one output is
+	 * required, all outputs must reference a valid stream and no two
+	 * outputs can reference the same stream.
+	 */
+	if (outputs.empty())
+		return -EINVAL;
+
+	for (auto [index, buffer] : outputs) {
+		if (!buffer)
+			return -EINVAL;
+		if (index >= 1) /* only single stream atm */
+			return -EINVAL;
+		if (mask & (1 << index))
+			return -EINVAL;
+
+		mask |= 1 << index;
+	}
+
+	process(input, outputs.at(0));
+
+	return 0;
+}
+
+int SwIspLinaro::start()
+{
+	int ret = ipa_->start();
+	if (ret)
+		return ret;
+
+	ispWorkerThread_.start();
+	return 0;
+}
+
+void SwIspLinaro::stop()
+{
+	ispWorkerThread_.exit();
+	ispWorkerThread_.wait();
+
+	ipa_->stop();
+}
+
+void SwIspLinaro::IspWorker::process(FrameBuffer *input, FrameBuffer *output)
+{
+	/* Copy metadata from the input buffer */
+	FrameMetadata &metadata = output->_d()->metadata();
+	metadata.status = input->metadata().status;
+	metadata.sequence = input->metadata().sequence;
+	metadata.timestamp = input->metadata().timestamp;
+
+	MappedFrameBuffer in(input, MappedFrameBuffer::MapFlag::Read);
+	MappedFrameBuffer out(output, MappedFrameBuffer::MapFlag::Write);
+	if (!in.isValid() || !out.isValid()) {
+		LOG(SoftwareIsp, Error) << "mmap-ing buffer(s) failed";
+		metadata.status = FrameMetadata::FrameError;
+		swIsp_->outputBufferReady.emit(output);
+		swIsp_->inputBufferReady.emit(input);
+		return;
+	}
+
+	(this->*debayerInfo_->debayer)(out.planes()[0].data(), in.planes()[0].data());
+	metadata.planes()[0].bytesused = out.planes()[0].size();
+
+	*swIsp_->sharedStats_ = stats_;
+	swIsp_->ispStatsReady.emit(0);
+
+	swIsp_->outputBufferReady.emit(output);
+	swIsp_->inputBufferReady.emit(input);
+}
+
+void SwIspLinaro::process(FrameBuffer *input, FrameBuffer *output)
+{
+	ispWorker_->invokeMethod(&SwIspLinaro::IspWorker::process,
+				 ConnectionTypeQueued, input, output);
+}
+
+REGISTER_SOFTWAREISP(SwIspLinaro)
+
+} /* namespace libcamera */