[{"id":28932,"web_url":"https://patchwork.libcamera.org/comment/28932/","msgid":"<878r2n4leg.fsf@redhat.com>","date":"2024-03-12T13:41:59","subject":"Re: [PATCH v5 08/18] libcamera: software_isp: Add DebayerCpu class","submitter":{"id":177,"url":"https://patchwork.libcamera.org/api/people/177/","name":"Milan Zamazal","email":"mzamazal@redhat.com"},"content":"Hans de Goede <hdegoede@redhat.com> writes:\n\n> Add CPU based debayering implementation. This initial implementation\n> only supports debayering packed 10 bits per pixel bayer data in\n> the 4 standard bayer orders.\n>\n> Doxygen documentation by Dennis Bonke.\n>\n> Tested-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org> # sc8280xp Lenovo x13s\n> Tested-by: Pavel Machek <pavel@ucw.cz>\n> Reviewed-by: Pavel Machek <pavel@ucw.cz>\n> Co-developed-by: Dennis Bonke <admin@dennisbonke.com>\n> Signed-off-by: Dennis Bonke <admin@dennisbonke.com>\n> Co-developed-by: Andrey Konovalov <andrey.konovalov@linaro.org>\n> Signed-off-by: Andrey Konovalov <andrey.konovalov@linaro.org>\n> Co-developed-by: Pavel Machek <pavel@ucw.cz>\n> Signed-off-by: Pavel Machek <pavel@ucw.cz>\n> Signed-off-by: Hans de Goede <hdegoede@redhat.com>\n\nReviewed-by: Milan Zamazal <mzamazal@redhat.com>\n\n> ---\n> Changes in v5:\n> - Rework debayer10P debayer functions to avoid:\n>   clang -Werror,-Wfor-loop-analysis warnings turned into errors\n>\n> Changes in v3:\n> - Move debayer_cpu.h to src/libcamera/software_isp/\n> - Move documentation to .cpp file\n> - Document how/why an array of src pointers is passed to\n>   the debayer functions\n> ---\n>  src/libcamera/software_isp/debayer_cpu.cpp | 626 +++++++++++++++++++++\n>  src/libcamera/software_isp/debayer_cpu.h   | 143 +++++\n>  src/libcamera/software_isp/meson.build     |   1 +\n>  3 files changed, 770 insertions(+)\n>  create mode 100644 src/libcamera/software_isp/debayer_cpu.cpp\n>  create mode 100644 src/libcamera/software_isp/debayer_cpu.h\n>\n> diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp\n> new file mode 100644\n> index 00000000..f932362c\n> --- /dev/null\n> +++ b/src/libcamera/software_isp/debayer_cpu.cpp\n> @@ -0,0 +1,626 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2023, Linaro Ltd\n> + * Copyright (C) 2023, Red Hat Inc.\n> + *\n> + * Authors:\n> + * Hans de Goede <hdegoede@redhat.com>\n> + *\n> + * debayer_cpu.cpp - CPU based debayering class\n> + */\n> +\n> +#include \"debayer_cpu.h\"\n> +\n> +#include <math.h>\n> +#include <stdlib.h>\n> +#include <time.h>\n> +\n> +#include <libcamera/formats.h>\n> +\n> +#include \"libcamera/internal/bayer_format.h\"\n> +#include \"libcamera/internal/framebuffer.h\"\n> +#include \"libcamera/internal/mapped_framebuffer.h\"\n> +\n> +namespace libcamera {\n> +\n> +/**\n> + * \\class DebayerCpu\n> + * \\brief Class for debayering on the CPU\n> + *\n> + * Implementation for CPU based debayering\n> + */\n> +\n> +/**\n> + * \\brief Constructs a DebayerCpu object.\n> + * \\param[in] stats Pointer to the stats object to use.\n> + */\n> +DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)\n> +\t: stats_(std::move(stats)), gamma_correction_(1.0)\n> +{\n> +#ifdef __x86_64__\n> +\tenableInputMemcpy_ = false;\n> +#else\n> +\tenableInputMemcpy_ = true;\n> +#endif\n> +\t/* Initialize gamma to 1.0 curve */\n> +\tfor (unsigned int i = 0; i < kGammaLookupSize; i++)\n> +\t\tgamma_[i] = i / (kGammaLookupSize / kRGBLookupSize);\n> +\n> +\tfor (unsigned int i = 0; i < kMaxLineBuffers; i++)\n> +\t\tlineBuffers_[i] = nullptr;\n> +}\n> +\n> +DebayerCpu::~DebayerCpu()\n> +{\n> +\tfor (unsigned int i = 0; i < kMaxLineBuffers; i++)\n> +\t\tfree(lineBuffers_[i]);\n> +}\n> +\n> +// RGR\n> +// GBG\n> +// RGR\n> +#define BGGR_BGR888(p, n, div)                                                                \\\n> +\t*dst++ = blue_[curr[x] / (div)];                                                      \\\n> +\t*dst++ = green_[(prev[x] + curr[x - p] + curr[x + n] + next[x]) / (4 * (div))];       \\\n> +\t*dst++ = red_[(prev[x - p] + prev[x + n] + next[x - p] + next[x + n]) / (4 * (div))]; \\\n> +\tx++;\n> +\n> +// GBG\n> +// RGR\n> +// GBG\n> +#define GRBG_BGR888(p, n, div)                                    \\\n> +\t*dst++ = blue_[(prev[x] + next[x]) / (2 * (div))];        \\\n> +\t*dst++ = green_[curr[x] / (div)];                         \\\n> +\t*dst++ = red_[(curr[x - p] + curr[x + n]) / (2 * (div))]; \\\n> +\tx++;\n> +\n> +// GRG\n> +// BGB\n> +// GRG\n> +#define GBRG_BGR888(p, n, div)                                     \\\n> +\t*dst++ = blue_[(curr[x - p] + curr[x + n]) / (2 * (div))]; \\\n> +\t*dst++ = green_[curr[x] / (div)];                          \\\n> +\t*dst++ = red_[(prev[x] + next[x]) / (2 * (div))];          \\\n> +\tx++;\n> +\n> +// BGB\n> +// GRG\n> +// BGB\n> +#define RGGB_BGR888(p, n, div)                                                                 \\\n> +\t*dst++ = blue_[(prev[x - p] + prev[x + n] + next[x - p] + next[x + n]) / (4 * (div))]; \\\n> +\t*dst++ = green_[(prev[x] + curr[x - p] + curr[x + n] + next[x]) / (4 * (div))];        \\\n> +\t*dst++ = red_[curr[x] / (div)];                                                        \\\n> +\tx++;\n> +\n> +void DebayerCpu::debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])\n> +{\n> +\tconst int width_in_bytes = window_.width * 5 / 4;\n> +\tconst uint8_t *prev = (const uint8_t *)src[0];\n> +\tconst uint8_t *curr = (const uint8_t *)src[1];\n> +\tconst uint8_t *next = (const uint8_t *)src[2];\n> +\n> +\t/*\n> +\t * For the first pixel getting a pixel from the previous column uses\n> +\t * x - 2 to skip the 5th byte with least-significant bits for 4 pixels.\n> +\t * Same for last pixel (uses x + 2) and looking at the next column.\n> +\t */\n> +\tfor (int x = 0; x < width_in_bytes;) {\n> +\t\t/* First pixel */\n> +\t\tBGGR_BGR888(2, 1, 1)\n> +\t\t/* Second pixel BGGR -> GBRG */\n> +\t\tGBRG_BGR888(1, 1, 1)\n> +\t\t/* Same thing for third and fourth pixels */\n> +\t\tBGGR_BGR888(1, 1, 1)\n> +\t\tGBRG_BGR888(1, 2, 1)\n> +\t\t/* Skip 5th src byte with 4 x 2 least-significant-bits */\n> +\t\tx++;\n> +\t}\n> +}\n> +\n> +void DebayerCpu::debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])\n> +{\n> +\tconst int width_in_bytes = window_.width * 5 / 4;\n> +\tconst uint8_t *prev = (const uint8_t *)src[0];\n> +\tconst uint8_t *curr = (const uint8_t *)src[1];\n> +\tconst uint8_t *next = (const uint8_t *)src[2];\n> +\n> +\tfor (int x = 0; x < width_in_bytes;) {\n> +\t\t/* First pixel */\n> +\t\tGRBG_BGR888(2, 1, 1)\n> +\t\t/* Second pixel GRBG -> RGGB */\n> +\t\tRGGB_BGR888(1, 1, 1)\n> +\t\t/* Same thing for third and fourth pixels */\n> +\t\tGRBG_BGR888(1, 1, 1)\n> +\t\tRGGB_BGR888(1, 2, 1)\n> +\t\t/* Skip 5th src byte with 4 x 2 least-significant-bits */\n> +\t\tx++;\n> +\t}\n> +}\n> +\n> +void DebayerCpu::debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[])\n> +{\n> +\tconst int width_in_bytes = window_.width * 5 / 4;\n> +\tconst uint8_t *prev = (const uint8_t *)src[0];\n> +\tconst uint8_t *curr = (const uint8_t *)src[1];\n> +\tconst uint8_t *next = (const uint8_t *)src[2];\n> +\n> +\tfor (int x = 0; x < width_in_bytes;) {\n> +\t\t/* Even pixel */\n> +\t\tGBRG_BGR888(2, 1, 1)\n> +\t\t/* Odd pixel GBGR -> BGGR */\n> +\t\tBGGR_BGR888(1, 1, 1)\n> +\t\t/* Same thing for next 2 pixels */\n> +\t\tGBRG_BGR888(1, 1, 1)\n> +\t\tBGGR_BGR888(1, 2, 1)\n> +\t\t/* Skip 5th src byte with 4 x 2 least-significant-bits */\n> +\t\tx++;\n> +\t}\n> +}\n> +\n> +void DebayerCpu::debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[])\n> +{\n> +\tconst int width_in_bytes = window_.width * 5 / 4;\n> +\tconst uint8_t *prev = (const uint8_t *)src[0];\n> +\tconst uint8_t *curr = (const uint8_t *)src[1];\n> +\tconst uint8_t *next = (const uint8_t *)src[2];\n> +\n> +\tfor (int x = 0; x < width_in_bytes;) {\n> +\t\t/* Even pixel */\n> +\t\tRGGB_BGR888(2, 1, 1)\n> +\t\t/* Odd pixel RGGB -> GRBG */\n> +\t\tGRBG_BGR888(1, 1, 1)\n> +\t\t/* Same thing for next 2 pixels */\n> +\t\tRGGB_BGR888(1, 1, 1)\n> +\t\tGRBG_BGR888(1, 2, 1)\n> +\t\t/* Skip 5th src byte with 4 x 2 least-significant-bits */\n> +\t\tx++;\n> +\t}\n> +}\n> +\n> +static bool isStandardBayerOrder(BayerFormat::Order order)\n> +{\n> +\treturn order == BayerFormat::BGGR || order == BayerFormat::GBRG ||\n> +\t       order == BayerFormat::GRBG || order == BayerFormat::RGGB;\n> +}\n> +\n> +/*\n> + * Setup the Debayer object according to the passed in parameters.\n> + * Return 0 on success, a negative errno value on failure\n> + * (unsupported parameters).\n> + */\n> +int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config)\n> +{\n> +\tBayerFormat bayerFormat =\n> +\t\tBayerFormat::fromPixelFormat(inputFormat);\n> +\n> +\tif (bayerFormat.bitDepth == 10 &&\n> +\t    bayerFormat.packing == BayerFormat::Packing::CSI2 &&\n> +\t    isStandardBayerOrder(bayerFormat.order)) {\n> +\t\tconfig.bpp = 10;\n> +\t\tconfig.patternSize.width = 4; /* 5 bytes per *4* pixels */\n> +\t\tconfig.patternSize.height = 2;\n> +\t\tconfig.outputFormats = std::vector<PixelFormat>({ formats::RGB888 });\n> +\t\treturn 0;\n> +\t}\n> +\n> +\tLOG(Debayer, Info)\n> +\t\t<< \"Unsupported input format \" << inputFormat.toString();\n> +\treturn -EINVAL;\n> +}\n> +\n> +int DebayerCpu::getOutputConfig(PixelFormat outputFormat, DebayerOutputConfig &config)\n> +{\n> +\tif (outputFormat == formats::RGB888) {\n> +\t\tconfig.bpp = 24;\n> +\t\treturn 0;\n> +\t}\n> +\n> +\tLOG(Debayer, Info)\n> +\t\t<< \"Unsupported output format \" << outputFormat.toString();\n> +\treturn -EINVAL;\n> +}\n> +\n> +/* TODO: this ignores outputFormat since there is only 1 supported outputFormat for now */\n> +int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat, [[maybe_unused]] PixelFormat outputFormat)\n> +{\n> +\tBayerFormat bayerFormat =\n> +\t\tBayerFormat::fromPixelFormat(inputFormat);\n> +\n> +\tif (bayerFormat.bitDepth == 10 &&\n> +\t    bayerFormat.packing == BayerFormat::Packing::CSI2) {\n> +\t\tswitch (bayerFormat.order) {\n> +\t\tcase BayerFormat::BGGR:\n> +\t\t\tdebayer0_ = &DebayerCpu::debayer10P_BGBG_BGR888;\n> +\t\t\tdebayer1_ = &DebayerCpu::debayer10P_GRGR_BGR888;\n> +\t\t\treturn 0;\n> +\t\tcase BayerFormat::GBRG:\n> +\t\t\tdebayer0_ = &DebayerCpu::debayer10P_GBGB_BGR888;\n> +\t\t\tdebayer1_ = &DebayerCpu::debayer10P_RGRG_BGR888;\n> +\t\t\treturn 0;\n> +\t\tcase BayerFormat::GRBG:\n> +\t\t\tdebayer0_ = &DebayerCpu::debayer10P_GRGR_BGR888;\n> +\t\t\tdebayer1_ = &DebayerCpu::debayer10P_BGBG_BGR888;\n> +\t\t\treturn 0;\n> +\t\tcase BayerFormat::RGGB:\n> +\t\t\tdebayer0_ = &DebayerCpu::debayer10P_RGRG_BGR888;\n> +\t\t\tdebayer1_ = &DebayerCpu::debayer10P_GBGB_BGR888;\n> +\t\t\treturn 0;\n> +\t\tdefault:\n> +\t\t\tbreak;\n> +\t\t}\n> +\t}\n> +\n> +\tLOG(Debayer, Error) << \"Unsupported input output format combination\";\n> +\treturn -EINVAL;\n> +}\n> +\n> +int DebayerCpu::configure(const StreamConfiguration &inputCfg,\n> +\t\t\t  const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs)\n> +{\n> +\tif (getInputConfig(inputCfg.pixelFormat, inputConfig_) != 0)\n> +\t\treturn -EINVAL;\n> +\n> +\tif (stats_->configure(inputCfg) != 0)\n> +\t\treturn -EINVAL;\n> +\n> +\tconst Size &stats_pattern_size = stats_->patternSize();\n> +\tif (inputConfig_.patternSize.width != stats_pattern_size.width ||\n> +\t    inputConfig_.patternSize.height != stats_pattern_size.height) {\n> +\t\tLOG(Debayer, Error)\n> +\t\t\t<< \"mismatching stats and debayer pattern sizes for \"\n> +\t\t\t<< inputCfg.pixelFormat.toString();\n> +\t\treturn -EINVAL;\n> +\t}\n> +\n> +\tinputConfig_.stride = inputCfg.stride;\n> +\n> +\tif (outputCfgs.size() != 1) {\n> +\t\tLOG(Debayer, Error)\n> +\t\t\t<< \"Unsupported number of output streams: \"\n> +\t\t\t<< outputCfgs.size();\n> +\t\treturn -EINVAL;\n> +\t}\n> +\n> +\tconst StreamConfiguration &outputCfg = outputCfgs[0];\n> +\tSizeRange outSizeRange = sizes(inputCfg.pixelFormat, inputCfg.size);\n> +\tstd::tie(outputConfig_.stride, outputConfig_.frameSize) =\n> +\t\tstrideAndFrameSize(outputCfg.pixelFormat, outputCfg.size);\n> +\n> +\tif (!outSizeRange.contains(outputCfg.size) || outputConfig_.stride != outputCfg.stride) {\n> +\t\tLOG(Debayer, Error)\n> +\t\t\t<< \"Invalid output size/stride: \"\n> +\t\t\t<< \"\\n  \" << outputCfg.size << \" (\" << outSizeRange << \")\"\n> +\t\t\t<< \"\\n  \" << outputCfg.stride << \" (\" << outputConfig_.stride << \")\";\n> +\t\treturn -EINVAL;\n> +\t}\n> +\n> +\tif (setDebayerFunctions(inputCfg.pixelFormat, outputCfg.pixelFormat) != 0)\n> +\t\treturn -EINVAL;\n> +\n> +\twindow_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) &\n> +\t\t    ~(inputConfig_.patternSize.width - 1);\n> +\twindow_.y = ((inputCfg.size.height - outputCfg.size.height) / 2) &\n> +\t\t    ~(inputConfig_.patternSize.height - 1);\n> +\twindow_.width = outputCfg.size.width;\n> +\twindow_.height = outputCfg.size.height;\n> +\n> +\t/* Don't pass x,y since process() already adjusts src before passing it */\n> +\tstats_->setWindow(Rectangle(window_.size()));\n> +\n> +\t/* pad with patternSize.Width on both left and right side */\n> +\tlineBufferPadding_ = inputConfig_.patternSize.width * inputConfig_.bpp / 8;\n> +\tlineBufferLength_ = window_.width * inputConfig_.bpp / 8 +\n> +\t\t\t    2 * lineBufferPadding_;\n> +\tfor (unsigned int i = 0;\n> +\t     i < (inputConfig_.patternSize.height + 1) && enableInputMemcpy_;\n> +\t     i++) {\n> +\t\tfree(lineBuffers_[i]);\n> +\t\tlineBuffers_[i] = (uint8_t *)malloc(lineBufferLength_);\n> +\t\tif (!lineBuffers_[i])\n> +\t\t\treturn -ENOMEM;\n> +\t}\n> +\n> +\tmeasuredFrames_ = 0;\n> +\tframeProcessTime_ = 0;\n> +\n> +\treturn 0;\n> +}\n> +\n> +/*\n> + * Get width and height at which the bayer-pattern repeats.\n> + * Return pattern-size or an empty Size for an unsupported inputFormat.\n> + */\n> +Size DebayerCpu::patternSize(PixelFormat inputFormat)\n> +{\n> +\tDebayerCpu::DebayerInputConfig config;\n> +\n> +\tif (getInputConfig(inputFormat, config) != 0)\n> +\t\treturn {};\n> +\n> +\treturn config.patternSize;\n> +}\n> +\n> +std::vector<PixelFormat> DebayerCpu::formats(PixelFormat inputFormat)\n> +{\n> +\tDebayerCpu::DebayerInputConfig config;\n> +\n> +\tif (getInputConfig(inputFormat, config) != 0)\n> +\t\treturn std::vector<PixelFormat>();\n> +\n> +\treturn config.outputFormats;\n> +}\n> +\n> +std::tuple<unsigned int, unsigned int>\n> +DebayerCpu::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size)\n> +{\n> +\tDebayerCpu::DebayerOutputConfig config;\n> +\n> +\tif (getOutputConfig(outputFormat, config) != 0)\n> +\t\treturn std::make_tuple(0, 0);\n> +\n> +\t/* round up to multiple of 8 for 64 bits alignment */\n> +\tunsigned int stride = (size.width * config.bpp / 8 + 7) & ~7;\n> +\n> +\treturn std::make_tuple(stride, stride * size.height);\n> +}\n> +\n> +void DebayerCpu::setupInputMemcpy(const uint8_t *linePointers[])\n> +{\n> +\tconst unsigned int patternHeight = inputConfig_.patternSize.height;\n> +\n> +\tif (!enableInputMemcpy_)\n> +\t\treturn;\n> +\n> +\tfor (unsigned int i = 0; i < patternHeight; i++) {\n> +\t\tmemcpy(lineBuffers_[i], linePointers[i + 1] - lineBufferPadding_,\n> +\t\t       lineBufferLength_);\n> +\t\tlinePointers[i + 1] = lineBuffers_[i] + lineBufferPadding_;\n> +\t}\n> +\n> +\t/* Point lineBufferIndex_ to first unused lineBuffer */\n> +\tlineBufferIndex_ = patternHeight;\n> +}\n> +\n> +void DebayerCpu::shiftLinePointers(const uint8_t *linePointers[], const uint8_t *src)\n> +{\n> +\tconst unsigned int patternHeight = inputConfig_.patternSize.height;\n> +\n> +\tfor (unsigned int i = 0; i < patternHeight; i++)\n> +\t\tlinePointers[i] = linePointers[i + 1];\n> +\n> +\tlinePointers[patternHeight] = src +\n> +\t\t\t\t      (patternHeight / 2) * (int)inputConfig_.stride;\n> +}\n> +\n> +void DebayerCpu::memcpyNextLine(const uint8_t *linePointers[])\n> +{\n> +\tconst unsigned int patternHeight = inputConfig_.patternSize.height;\n> +\n> +\tif (!enableInputMemcpy_)\n> +\t\treturn;\n> +\n> +\tmemcpy(lineBuffers_[lineBufferIndex_], linePointers[patternHeight] - lineBufferPadding_,\n> +\t       lineBufferLength_);\n> +\tlinePointers[patternHeight] = lineBuffers_[lineBufferIndex_] + lineBufferPadding_;\n> +\n> +\tlineBufferIndex_ = (lineBufferIndex_ + 1) % (patternHeight + 1);\n> +}\n> +\n> +void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)\n> +{\n> +\tunsigned int y_end = window_.y + window_.height;\n> +\t/* Holds [0] previous- [1] current- [2] next-line */\n> +\tconst uint8_t *linePointers[3];\n> +\n> +\t/* Adjust src to top left corner of the window */\n> +\tsrc += window_.y * inputConfig_.stride + window_.x * inputConfig_.bpp / 8;\n> +\n> +\t/* [x] becomes [x - 1] after initial shiftLinePointers() call */\n> +\tif (window_.y) {\n> +\t\tlinePointers[1] = src - inputConfig_.stride; /* previous-line */\n> +\t\tlinePointers[2] = src;\n> +\t} else {\n> +\t\t/* window_.y == 0, use the next line as prev line */\n> +\t\tlinePointers[1] = src + inputConfig_.stride;\n> +\t\tlinePointers[2] = src;\n> +\t\t/* Last 2 lines also need special handling */\n> +\t\ty_end -= 2;\n> +\t}\n> +\n> +\tsetupInputMemcpy(linePointers);\n> +\n> +\tfor (unsigned int y = window_.y; y < y_end; y += 2) {\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\tmemcpyNextLine(linePointers);\n> +\t\tstats_->processLine0(y, linePointers);\n> +\t\t(this->*debayer0_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\tmemcpyNextLine(linePointers);\n> +\t\t(this->*debayer1_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\t}\n> +\n> +\tif (window_.y == 0) {\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\tmemcpyNextLine(linePointers);\n> +\t\tstats_->processLine0(y_end, linePointers);\n> +\t\t(this->*debayer0_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\t/* next line may point outside of src, use prev. */\n> +\t\tlinePointers[2] = linePointers[0];\n> +\t\t(this->*debayer1_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\t}\n> +}\n> +\n> +void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)\n> +{\n> +\tconst unsigned int y_end = window_.y + window_.height;\n> +\t/*\n> +\t * This holds pointers to [0] 2-lines-up [1] 1-line-up [2] current-line\n> +\t * [3] 1-line-down [4] 2-lines-down.\n> +\t */\n> +\tconst uint8_t *linePointers[5];\n> +\n> +\t/* Adjust src to top left corner of the window */\n> +\tsrc += window_.y * inputConfig_.stride + window_.x * inputConfig_.bpp / 8;\n> +\n> +\t/* [x] becomes [x - 1] after initial shiftLinePointers() call */\n> +\tlinePointers[1] = src - 2 * inputConfig_.stride;\n> +\tlinePointers[2] = src - inputConfig_.stride;\n> +\tlinePointers[3] = src;\n> +\tlinePointers[4] = src + inputConfig_.stride;\n> +\n> +\tsetupInputMemcpy(linePointers);\n> +\n> +\tfor (unsigned int y = window_.y; y < y_end; y += 4) {\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\tmemcpyNextLine(linePointers);\n> +\t\tstats_->processLine0(y, linePointers);\n> +\t\t(this->*debayer0_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\tmemcpyNextLine(linePointers);\n> +\t\t(this->*debayer1_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\tmemcpyNextLine(linePointers);\n> +\t\tstats_->processLine2(y, linePointers);\n> +\t\t(this->*debayer2_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\n> +\t\tshiftLinePointers(linePointers, src);\n> +\t\tmemcpyNextLine(linePointers);\n> +\t\t(this->*debayer3_)(dst, linePointers);\n> +\t\tsrc += inputConfig_.stride;\n> +\t\tdst += outputConfig_.stride;\n> +\t}\n> +}\n> +\n> +static inline int64_t timeDiff(timespec &after, timespec &before)\n> +{\n> +\treturn (after.tv_sec - before.tv_sec) * 1000000000LL +\n> +\t       (int64_t)after.tv_nsec - (int64_t)before.tv_nsec;\n> +}\n> +\n> +void DebayerCpu::process(FrameBuffer *input, FrameBuffer *output, DebayerParams params)\n> +{\n> +\ttimespec frameStartTime;\n> +\n> +\tif (measuredFrames_ < DebayerCpu::kLastFrameToMeasure) {\n> +\t\tframeStartTime = {};\n> +\t\tclock_gettime(CLOCK_MONOTONIC_RAW, &frameStartTime);\n> +\t}\n> +\n> +\t/* Apply DebayerParams */\n> +\tif (params.gamma != gamma_correction_) {\n> +\t\tfor (unsigned int i = 0; i < kGammaLookupSize; i++)\n> +\t\t\tgamma_[i] = UINT8_MAX * powf(i / (kGammaLookupSize - 1.0), params.gamma);\n> +\n> +\t\tgamma_correction_ = params.gamma;\n> +\t}\n> +\n> +\tfor (unsigned int i = 0; i < kRGBLookupSize; i++) {\n> +\t\tconstexpr unsigned int div =\n> +\t\t\tkRGBLookupSize * DebayerParams::kGain10 / kGammaLookupSize;\n> +\t\tunsigned int idx;\n> +\n> +\t\t/* Apply gamma after gain! */\n> +\t\tidx = std::min({ i * params.gainR / div, (kGammaLookupSize - 1) });\n> +\t\tred_[i] = gamma_[idx];\n> +\n> +\t\tidx = std::min({ i * params.gainG / div, (kGammaLookupSize - 1) });\n> +\t\tgreen_[i] = gamma_[idx];\n> +\n> +\t\tidx = std::min({ i * params.gainB / div, (kGammaLookupSize - 1) });\n> +\t\tblue_[i] = gamma_[idx];\n> +\t}\n> +\n> +\t/* Copy metadata from the input buffer */\n> +\tFrameMetadata &metadata = output->_d()->metadata();\n> +\tmetadata.status = input->metadata().status;\n> +\tmetadata.sequence = input->metadata().sequence;\n> +\tmetadata.timestamp = input->metadata().timestamp;\n> +\n> +\tMappedFrameBuffer in(input, MappedFrameBuffer::MapFlag::Read);\n> +\tMappedFrameBuffer out(output, MappedFrameBuffer::MapFlag::Write);\n> +\tif (!in.isValid() || !out.isValid()) {\n> +\t\tLOG(Debayer, Error) << \"mmap-ing buffer(s) failed\";\n> +\t\tmetadata.status = FrameMetadata::FrameError;\n> +\t\treturn;\n> +\t}\n> +\n> +\tstats_->startFrame();\n> +\n> +\tif (inputConfig_.patternSize.height == 2)\n> +\t\tprocess2(in.planes()[0].data(), out.planes()[0].data());\n> +\telse\n> +\t\tprocess4(in.planes()[0].data(), out.planes()[0].data());\n> +\n> +\tmetadata.planes()[0].bytesused = out.planes()[0].size();\n> +\n> +\t/* Measure before emitting signals */\n> +\tif (measuredFrames_ < DebayerCpu::kLastFrameToMeasure &&\n> +\t    ++measuredFrames_ > DebayerCpu::kFramesToSkip) {\n> +\t\ttimespec frameEndTime = {};\n> +\t\tclock_gettime(CLOCK_MONOTONIC_RAW, &frameEndTime);\n> +\t\tframeProcessTime_ += timeDiff(frameEndTime, frameStartTime);\n> +\t\tif (measuredFrames_ == DebayerCpu::kLastFrameToMeasure) {\n> +\t\t\tconst unsigned int measuredFrames = DebayerCpu::kLastFrameToMeasure -\n> +\t\t\t\t\t\t\t    DebayerCpu::kFramesToSkip;\n> +\t\t\tLOG(Debayer, Info)\n> +\t\t\t\t<< \"Processed \" << measuredFrames\n> +\t\t\t\t<< \" frames in \" << frameProcessTime_ / 1000 << \"us, \"\n> +\t\t\t\t<< frameProcessTime_ / (1000 * measuredFrames)\n> +\t\t\t\t<< \" us/frame\";\n> +\t\t}\n> +\t}\n> +\n> +\tstats_->finishFrame();\n> +\toutputBufferReady.emit(output);\n> +\tinputBufferReady.emit(input);\n> +}\n> +\n> +SizeRange DebayerCpu::sizes(PixelFormat inputFormat, const Size &inputSize)\n> +{\n> +\tSize pattern_size = patternSize(inputFormat);\n> +\tunsigned int border_height = pattern_size.height;\n> +\n> +\tif (pattern_size.isNull())\n> +\t\treturn {};\n> +\n> +\t/* No need for top/bottom border with a pattern height of 2 */\n> +\tif (pattern_size.height == 2)\n> +\t\tborder_height = 0;\n> +\n> +\t/*\n> +\t * For debayer interpolation a border is kept around the entire image\n> +\t * and the minimum output size is pattern-height x pattern-width.\n> +\t */\n> +\tif (inputSize.width < (3 * pattern_size.width) ||\n> +\t    inputSize.height < (2 * border_height + pattern_size.height)) {\n> +\t\tLOG(Debayer, Warning)\n> +\t\t\t<< \"Input format size too small: \" << inputSize.toString();\n> +\t\treturn {};\n> +\t}\n> +\n> +\treturn SizeRange(Size(pattern_size.width, pattern_size.height),\n> +\t\t\t Size((inputSize.width - 2 * pattern_size.width) & ~(pattern_size.width - 1),\n> +\t\t\t      (inputSize.height - 2 * border_height) & ~(pattern_size.height - 1)),\n> +\t\t\t pattern_size.width, pattern_size.height);\n> +}\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h\n> new file mode 100644\n> index 00000000..8a51ed85\n> --- /dev/null\n> +++ b/src/libcamera/software_isp/debayer_cpu.h\n> @@ -0,0 +1,143 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2023, Linaro Ltd\n> + * Copyright (C) 2023, Red Hat Inc.\n> + *\n> + * Authors:\n> + * Hans de Goede <hdegoede@redhat.com>\n> + *\n> + * debayer_cpu.h - CPU based debayering header\n> + */\n> +\n> +#pragma once\n> +\n> +#include <memory>\n> +#include <stdint.h>\n> +#include <vector>\n> +\n> +#include <libcamera/base/object.h>\n> +\n> +#include \"debayer.h\"\n> +#include \"swstats_cpu.h\"\n> +\n> +namespace libcamera {\n> +\n> +class DebayerCpu : public Debayer, public Object\n> +{\n> +public:\n> +\tDebayerCpu(std::unique_ptr<SwStatsCpu> stats);\n> +\t~DebayerCpu();\n> +\n> +\tint configure(const StreamConfiguration &inputCfg,\n> +\t\t      const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs);\n> +\tSize patternSize(PixelFormat inputFormat);\n> +\tstd::vector<PixelFormat> formats(PixelFormat input);\n> +\tstd::tuple<unsigned int, unsigned int>\n> +\tstrideAndFrameSize(const PixelFormat &outputFormat, const Size &size);\n> +\tvoid process(FrameBuffer *input, FrameBuffer *output, DebayerParams params);\n> +\tSizeRange sizes(PixelFormat inputFormat, const Size &inputSize);\n> +\n> +\t/**\n> +\t * \\brief Get the file descriptor for the statistics.\n> +\t *\n> +\t * \\return the file descriptor pointing to the statistics.\n> +\t */\n> +\tconst SharedFD &getStatsFD() { return stats_->getStatsFD(); }\n> +\n> +\t/**\n> +\t * \\brief Get the output frame size.\n> +\t *\n> +\t * \\return The output frame size.\n> +\t */\n> +\tunsigned int frameSize() { return outputConfig_.frameSize; }\n> +\n> +private:\n> +\t/**\n> +\t * \\brief Called to debayer 1 line of Bayer input data to output format\n> +\t * \\param[out] dst Pointer to the start of the output line to write\n> +\t * \\param[in] src The input data\n> +\t *\n> +\t * Input data is an array of (patternSize_.height + 1) src\n> +\t * pointers each pointing to a line in the Bayer source. The middle\n> +\t * element of the array will point to the actual line being processed.\n> +\t * Earlier element(s) will point to the previous line(s) and later\n> +\t * element(s) to the next line(s).\n> +\t *\n> +\t * These functions take an array of src pointers, rather than\n> +\t * a single src pointer + a stride for the source, so that when the src\n> +\t * is slow uncached memory it can be copied to faster memory before\n> +\t * debayering. Debayering a standard 2x2 Bayer pattern requires access\n> +\t * to the previous and next src lines for interpolating the missing\n> +\t * colors. To allow copying the src lines only once 3 temporary buffers\n> +\t * each holding a single line are used, re-using the oldest buffer for\n> +\t * the next line and the pointers are swizzled so that:\n> +\t * src[0] = previous-line, src[1] = currrent-line, src[2] = next-line.\n> +\t * This way the 3 pointers passed to the debayer functions form\n> +\t * a sliding window over the src avoiding the need to copy each\n> +\t * line more than once.\n> +\t *\n> +\t * Similarly for bayer patterns which repeat every 4 lines, 5 src\n> +\t * pointers are passed holding: src[0] = 2-lines-up, src[1] = 1-line-up\n> +\t * src[2] = current-line, src[3] = 1-line-down, src[4] = 2-lines-down.\n> +\t */\n> +\tusing debayerFn = void (DebayerCpu::*)(uint8_t *dst, const uint8_t *src[]);\n> +\n> +\t/* CSI-2 packed 10-bit raw bayer format (all the 4 orders) */\n> +\tvoid debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);\n> +\tvoid debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);\n> +\tvoid debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[]);\n> +\tvoid debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[]);\n> +\n> +\tstruct DebayerInputConfig {\n> +\t\tSize patternSize;\n> +\t\tunsigned int bpp; /* Memory used per pixel, not precision */\n> +\t\tunsigned int stride;\n> +\t\tstd::vector<PixelFormat> outputFormats;\n> +\t};\n> +\n> +\tstruct DebayerOutputConfig {\n> +\t\tunsigned int bpp; /* Memory used per pixel, not precision */\n> +\t\tunsigned int stride;\n> +\t\tunsigned int frameSize;\n> +\t};\n> +\n> +\tint getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config);\n> +\tint getOutputConfig(PixelFormat outputFormat, DebayerOutputConfig &config);\n> +\tint setDebayerFunctions(PixelFormat inputFormat, PixelFormat outputFormat);\n> +\tvoid setupInputMemcpy(const uint8_t *linePointers[]);\n> +\tvoid shiftLinePointers(const uint8_t *linePointers[], const uint8_t *src);\n> +\tvoid memcpyNextLine(const uint8_t *linePointers[]);\n> +\tvoid process2(const uint8_t *src, uint8_t *dst);\n> +\tvoid process4(const uint8_t *src, uint8_t *dst);\n> +\n> +\tstatic constexpr unsigned int kGammaLookupSize = 1024;\n> +\tstatic constexpr unsigned int kRGBLookupSize = 256;\n> +\t/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */\n> +\tstatic constexpr unsigned int kMaxLineBuffers = 5;\n> +\n> +\tstd::array<uint8_t, kGammaLookupSize> gamma_;\n> +\tstd::array<uint8_t, kRGBLookupSize> red_;\n> +\tstd::array<uint8_t, kRGBLookupSize> green_;\n> +\tstd::array<uint8_t, kRGBLookupSize> blue_;\n> +\tdebayerFn debayer0_;\n> +\tdebayerFn debayer1_;\n> +\tdebayerFn debayer2_;\n> +\tdebayerFn debayer3_;\n> +\tRectangle window_;\n> +\tDebayerInputConfig inputConfig_;\n> +\tDebayerOutputConfig outputConfig_;\n> +\tstd::unique_ptr<SwStatsCpu> stats_;\n> +\tuint8_t *lineBuffers_[kMaxLineBuffers];\n> +\tunsigned int lineBufferLength_;\n> +\tunsigned int lineBufferPadding_;\n> +\tunsigned int lineBufferIndex_;\n> +\tbool enableInputMemcpy_;\n> +\tfloat gamma_correction_;\n> +\tunsigned int measuredFrames_;\n> +\tint64_t frameProcessTime_;\n> +\t/* Skip 30 frames for things to stabilize then measure 30 frames */\n> +\tstatic constexpr unsigned int kFramesToSkip = 30;\n> +\tstatic constexpr unsigned int kLastFrameToMeasure = 60;\n> +};\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/libcamera/software_isp/meson.build b/src/libcamera/software_isp/meson.build\n> index 62095f61..71b46539 100644\n> --- a/src/libcamera/software_isp/meson.build\n> +++ b/src/libcamera/software_isp/meson.build\n> @@ -9,5 +9,6 @@ endif\n>  \n>  libcamera_sources += files([\n>      'debayer.cpp',\n> +    'debayer_cpu.cpp',\n>      'swstats_cpu.cpp',\n>  ])","headers":{"Return-Path":"<libcamera-devel-bounces@lists.libcamera.org>","X-Original-To":"parsemail@patchwork.libcamera.org","Delivered-To":"parsemail@patchwork.libcamera.org","Received":["from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id B38FCBD1F1\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 12 Mar 2024 13:42:08 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D9D3E62C80;\n\tTue, 12 Mar 2024 14:42:07 +0100 (CET)","from us-smtp-delivery-124.mimecast.com\n\t(us-smtp-delivery-124.mimecast.com [170.10.133.124])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 105F56286C\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 12 Mar 2024 14:42:05 +0100 (CET)","from mail-wr1-f71.google.com (mail-wr1-f71.google.com\n\t[209.85.221.71]) by relay.mimecast.com with ESMTP with STARTTLS\n\t(version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id\n\tus-mta-52-xQceoSI5NHG2tFZm3gap4A-1; Tue, 12 Mar 2024 09:42:03 -0400","by mail-wr1-f71.google.com with SMTP id\n\tffacd0b85a97d-33e8b017632so1894620f8f.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 12 Mar 2024 06:42:02 -0700 (PDT)","from nuthatch (nat-pool-brq-t.redhat.com. [213.175.37.10])\n\tby smtp.gmail.com with ESMTPSA id\n\ta8-20020a5d53c8000000b0033e745b8bcfsm9111112wrw.88.2024.03.12.06.41.59\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tTue, 12 Mar 2024 06:41:59 -0700 (PDT)"],"Authentication-Results":"lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n\tunprotected) header.d=redhat.com header.i=@redhat.com\n\theader.b=\"TWh/bNAK\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n\ts=mimecast20190719; t=1710250924;\n\th=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n\tto:to:cc:cc:mime-version:mime-version:content-type:content-type:\n\tin-reply-to:in-reply-to:references:references;\n\tbh=Yi2UG0KZ+uaFJUYMOKQSdTRMIfijnX+tihIg4UPeu+s=;\n\tb=TWh/bNAKG1cMIPm5l2HWUZ6ZTADOf0OriZnMEbDTAow0LtGALF9dIvX8Dzi5SKKJmAkSOX\n\tcJ8ESrHEav6jvwn0ci7kGient5lsxQgFqb20YLjpJHn/BYUm8VvgN3gM7xvzrsHgnZ5QDh\n\tYc7TXcaiBUovpwkuxO8kspmG5h3wXXk=","X-MC-Unique":"xQceoSI5NHG2tFZm3gap4A-1","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1710250921; x=1710855721;\n\th=mime-version:user-agent:message-id:date:references:in-reply-to\n\t:subject:cc:to:from:x-gm-message-state:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=Yi2UG0KZ+uaFJUYMOKQSdTRMIfijnX+tihIg4UPeu+s=;\n\tb=Phjcnp6H+/dudSYoNLCbErpy8gPaoydTdSwuwRxH1UjxrJMgo2JHWeyPKfYHq1NUtQ\n\tLrNHxEGCPRDDLen7vb8bgnIro+ZoaCfs4j5PRmCBneNShSY+HmoukKKhjVtozSKJYwg5\n\tydJB8EOlPLcXuCrCBFR+D5Fusw1awPyg0jRxi3Mz3pn1/zJISz5YCst26x/O3jEW7VO2\n\tySIJ53xQ5PqAJ/9cI2zDm21oISV7vguNqi9V6iHpkFlaEmv66Ax6aOmDZCO1UAlYRp0Z\n\tN2T5rWDqme5KtTxlU72V3UJZLYD/y/p2nATLrYV9oxbP3ggfMN0zHOzb/8jjZuljY5bT\n\tdcdw==","X-Gm-Message-State":"AOJu0YyiTxfcD5ruWlxM+urdqEfI18ciE2UeJQXnIfG6eaKDUKnGID1x\n\tY5kWipr3z1ikPyVCFadxs8TocFl38Td00tuyj6I1vqRiMWyJ16sYoA/DUSAqkAFE7AmmLKpo8Sn\n\toqhgljEwEWMsDwP9BCVClT0HEEa3rddpxMWNMOnSHa8n/Pann2e9nJ2PU5MtWSEbqMvmMA0c2Qd\n\t6vK/A=","X-Received":["by 2002:a05:6000:ed2:b0:33e:707a:2411 with SMTP id\n\tea18-20020a0560000ed200b0033e707a2411mr265546wrb.44.1710250920857; \n\tTue, 12 Mar 2024 06:42:00 -0700 (PDT)","by 2002:a05:6000:ed2:b0:33e:707a:2411 with SMTP id\n\tea18-20020a0560000ed200b0033e707a2411mr265528wrb.44.1710250920340; \n\tTue, 12 Mar 2024 06:42:00 -0700 (PDT)"],"X-Google-Smtp-Source":"AGHT+IHco6QKh6VK5k9qa6yDcTXnQ9drwQiq7ejVZ7KaEr8GCpS3bn5UIYFADonQHMrkERhgSpoCtA==","From":"Milan Zamazal <mzamazal@redhat.com>","To":"Hans de Goede <hdegoede@redhat.com>","Subject":"Re: [PATCH v5 08/18] libcamera: software_isp: Add DebayerCpu class","In-Reply-To":"<20240311141524.27192-9-hdegoede@redhat.com> (Hans de Goede's\n\tmessage of \"Mon, 11 Mar 2024 15:15:12 +0100\")","References":"<20240311141524.27192-1-hdegoede@redhat.com>\n\t<20240311141524.27192-9-hdegoede@redhat.com>","Date":"Tue, 12 Mar 2024 14:41:59 +0100","Message-ID":"<878r2n4leg.fsf@redhat.com>","User-Agent":"Gnus/5.13 (Gnus v5.13)","MIME-Version":"1.0","X-Mimecast-Spam-Score":"0","X-Mimecast-Originator":"redhat.com","Content-Type":"text/plain","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Cc":"Maxime Ripard <mripard@redhat.com>, libcamera-devel@lists.libcamera.org, \n\tPavel Machek <pavel@ucw.cz>,\n\tBryan O'Donoghue <bryan.odonoghue@linaro.org>, \n\tDennis Bonke <admin@dennisbonke.com>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]