[{"id":30551,"web_url":"https://patchwork.libcamera.org/comment/30551/","msgid":"<20240802214932.GD3295@pendragon.ideasonboard.com>","date":"2024-08-02T21:49:32","subject":"Re: [PATCH v6 5/5] libcamera: rkisp1: Plumb the ConverterDW100\n\tconverter","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Umang,\n\nThank you for the patch.\n\nOn Fri, Jul 26, 2024 at 05:17:15PM +0530, Umang Jain wrote:\n> Plumb the ConverterDW100 converter in the rkisp1 pipeline handler.\n> If the dewarper is found, it is instantiated and buffers are exported\n> from it, instead of RkISP1Path. Internal buffers are allocated for the\n> RkISP1Path in case where dewarper is going to be used.\n> \n> The RKISP1 pipeline handler now supports scaler crop control through\n> ConverterDW100. Register the ScalerCrop control for the cameras created\n> in the RKISP1 pipeline handler.\n> \n> Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>\n> ---\n>  src/libcamera/pipeline/rkisp1/rkisp1.cpp | 154 ++++++++++++++++++++++-\n>  1 file changed, 150 insertions(+), 4 deletions(-)\n> \n> diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp\n> index 25f2cc97..32ec0fdf 100644\n> --- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp\n> +++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp\n> @@ -8,9 +8,11 @@\n>  #include <algorithm>\n>  #include <array>\n>  #include <iomanip>\n> +#include <map>\n>  #include <memory>\n>  #include <numeric>\n>  #include <queue>\n> +#include <vector>\n>  \n>  #include <linux/media-bus-format.h>\n>  #include <linux/rkisp1-config.h>\n> @@ -33,6 +35,7 @@\n>  \n>  #include \"libcamera/internal/camera.h\"\n>  #include \"libcamera/internal/camera_sensor.h\"\n> +#include \"libcamera/internal/converter/converter_dw100.h\"\n>  #include \"libcamera/internal/delayed_controls.h\"\n>  #include \"libcamera/internal/device_enumerator.h\"\n>  #include \"libcamera/internal/framebuffer.h\"\n> @@ -62,6 +65,8 @@ struct RkISP1FrameInfo {\n>  \n>  \tbool paramDequeued;\n>  \tbool metadataProcessed;\n> +\n> +\tstd::optional<Rectangle> scalerCrop;\n>  };\n>  \n>  class RkISP1Frames\n> @@ -181,6 +186,7 @@ private:\n>  \tvoid bufferReady(FrameBuffer *buffer);\n>  \tvoid paramReady(FrameBuffer *buffer);\n>  \tvoid statReady(FrameBuffer *buffer);\n> +\tvoid dewarpBufferReady(FrameBuffer *buffer);\n>  \tvoid frameStart(uint32_t sequence);\n>  \n>  \tint allocateBuffers(Camera *camera);\n> @@ -200,6 +206,13 @@ private:\n>  \tRkISP1MainPath mainPath_;\n>  \tRkISP1SelfPath selfPath_;\n>  \n> +\tstd::unique_ptr<ConverterDW100> dewarper_;\n> +\tbool useDewarper_;\n> +\n> +\t/* Internal buffers used when dewarper is being used */\n> +\tstd::vector<std::unique_ptr<FrameBuffer>> mainPathBuffers_;\n> +\tstd::queue<FrameBuffer *> availableMainPathBuffers_;\n> +\n>  \tstd::vector<std::unique_ptr<FrameBuffer>> paramBuffers_;\n>  \tstd::vector<std::unique_ptr<FrameBuffer>> statBuffers_;\n>  \tstd::queue<FrameBuffer *> availableParamBuffers_;\n> @@ -222,6 +235,8 @@ RkISP1FrameInfo *RkISP1Frames::create(const RkISP1CameraData *data, Request *req\n>  \n>  \tFrameBuffer *paramBuffer = nullptr;\n>  \tFrameBuffer *statBuffer = nullptr;\n> +\tFrameBuffer *mainPathBuffer = nullptr;\n> +\tFrameBuffer *selfPathBuffer = nullptr;\n>  \n>  \tif (!isRaw) {\n>  \t\tif (pipe_->availableParamBuffers_.empty()) {\n> @@ -239,10 +254,16 @@ RkISP1FrameInfo *RkISP1Frames::create(const RkISP1CameraData *data, Request *req\n>  \n>  \t\tstatBuffer = pipe_->availableStatBuffers_.front();\n>  \t\tpipe_->availableStatBuffers_.pop();\n> +\n> +\t\tif (pipe_->useDewarper_) {\n> +\t\t\tmainPathBuffer = pipe_->availableMainPathBuffers_.front();\n> +\t\t\tpipe_->availableMainPathBuffers_.pop();\n> +\t\t}\n>  \t}\n>  \n> -\tFrameBuffer *mainPathBuffer = request->findBuffer(&data->mainPathStream_);\n> -\tFrameBuffer *selfPathBuffer = request->findBuffer(&data->selfPathStream_);\n> +\tif (!mainPathBuffer)\n> +\t\tmainPathBuffer = request->findBuffer(&data->mainPathStream_);\n> +\tselfPathBuffer = request->findBuffer(&data->selfPathStream_);\n>  \n>  \tRkISP1FrameInfo *info = new RkISP1FrameInfo;\n>  \n> @@ -268,6 +289,7 @@ int RkISP1Frames::destroy(unsigned int frame)\n>  \n>  \tpipe_->availableParamBuffers_.push(info->paramBuffer);\n>  \tpipe_->availableStatBuffers_.push(info->statBuffer);\n> +\tpipe_->availableMainPathBuffers_.push(info->mainPathBuffer);\n>  \n>  \tframeInfo_.erase(info->frame);\n>  \n> @@ -283,6 +305,7 @@ void RkISP1Frames::clear()\n>  \n>  \t\tpipe_->availableParamBuffers_.push(info->paramBuffer);\n>  \t\tpipe_->availableStatBuffers_.push(info->statBuffer);\n> +\t\tpipe_->availableMainPathBuffers_.push(info->mainPathBuffer);\n>  \n>  \t\tdelete info;\n>  \t}\n> @@ -607,7 +630,7 @@ CameraConfiguration::Status RkISP1CameraConfiguration::validate()\n>   */\n>  \n>  PipelineHandlerRkISP1::PipelineHandlerRkISP1(CameraManager *manager)\n> -\t: PipelineHandler(manager), hasSelfPath_(true)\n> +\t: PipelineHandler(manager), hasSelfPath_(true), useDewarper_(false)\n>  {\n>  }\n>  \n> @@ -785,12 +808,19 @@ int PipelineHandlerRkISP1::configure(Camera *camera, CameraConfiguration *c)\n>  \t\t<< \" crop \" << rect;\n>  \n>  \tstd::map<unsigned int, IPAStream> streamConfig;\n> +\tstd::vector<std::reference_wrapper<StreamConfiguration>> outputCfgs;\n>  \n>  \tfor (const StreamConfiguration &cfg : *config) {\n>  \t\tif (cfg.stream() == &data->mainPathStream_) {\n>  \t\t\tret = mainPath_.configure(cfg, format);\n>  \t\t\tstreamConfig[0] = IPAStream(cfg.pixelFormat,\n>  \t\t\t\t\t\t    cfg.size);\n> +\t\t\t/* Configure dewarp */\n> +\t\t\tif (dewarper_ && !isRaw_) {\n> +\t\t\t\toutputCfgs.push_back(const_cast<StreamConfiguration &>(cfg));\n> +\t\t\t\tret = dewarper_->configure(cfg, outputCfgs);\n> +\t\t\t\tuseDewarper_ = ret ? false : true;\n> +\t\t\t}\n>  \t\t} else if (hasSelfPath_) {\n>  \t\t\tret = selfPath_.configure(cfg, format);\n>  \t\t\tstreamConfig[1] = IPAStream(cfg.pixelFormat,\n> @@ -839,6 +869,9 @@ int PipelineHandlerRkISP1::exportFrameBuffers([[maybe_unused]] Camera *camera, S\n>  \tRkISP1CameraData *data = cameraData(camera);\n>  \tunsigned int count = stream->configuration().bufferCount;\n>  \n> +\tif (useDewarper_)\n> +\t\treturn dewarper_->exportBuffers(&data->mainPathStream_, count, buffers);\n> +\n>  \tif (stream == &data->mainPathStream_)\n>  \t\treturn mainPath_.exportBuffers(count, buffers);\n>  \telse if (hasSelfPath_ && stream == &data->selfPathStream_)\n> @@ -866,6 +899,16 @@ int PipelineHandlerRkISP1::allocateBuffers(Camera *camera)\n>  \t\tret = stat_->allocateBuffers(maxCount, &statBuffers_);\n>  \t\tif (ret < 0)\n>  \t\t\tgoto error;\n> +\n> +\t\t/* If the dewarper is being used, allocate internal buffers for ISP */\n\ns/ISP/ISP./\n\n> +\t\tif (useDewarper_) {\n> +\t\t\tret = mainPath_.exportBuffers(maxCount, &mainPathBuffers_);\n> +\t\t\tif (ret < 0)\n> +\t\t\t\tgoto error;\n> +\n> +\t\t\tfor (std::unique_ptr<FrameBuffer> &buffer : mainPathBuffers_)\n> +\t\t\t\tavailableMainPathBuffers_.push(buffer.get());\n> +\t\t}\n>  \t}\n>  \n>  \tfor (std::unique_ptr<FrameBuffer> &buffer : paramBuffers_) {\n> @@ -889,6 +932,7 @@ int PipelineHandlerRkISP1::allocateBuffers(Camera *camera)\n>  error:\n>  \tparamBuffers_.clear();\n>  \tstatBuffers_.clear();\n> +\tmainPathBuffers_.clear();\n>  \n>  \treturn ret;\n>  }\n> @@ -903,8 +947,12 @@ int PipelineHandlerRkISP1::freeBuffers(Camera *camera)\n>  \twhile (!availableParamBuffers_.empty())\n>  \t\tavailableParamBuffers_.pop();\n>  \n> +\twhile (!availableMainPathBuffers_.empty())\n> +\t\tavailableMainPathBuffers_.pop();\n> +\n>  \tparamBuffers_.clear();\n>  \tstatBuffers_.clear();\n> +\tmainPathBuffers_.clear();\n>  \n>  \tstd::vector<unsigned int> ids;\n>  \tfor (IPABuffer &ipabuf : data->ipaBuffers_)\n> @@ -961,6 +1009,14 @@ int PipelineHandlerRkISP1::start(Camera *camera, [[maybe_unused]] const ControlL\n>  \t\t\t\t<< \"Failed to start statistics \" << camera->id();\n>  \t\t\treturn ret;\n>  \t\t}\n> +\n> +\t\tif (useDewarper_) {\n> +\t\t\tret = dewarper_->start();\n\nYou don't stop the dewarper in the error paths below.\n\n> +\t\t\tif (ret) {\n> +\t\t\t\tLOG(RkISP1, Error) << \"Failed to start dewarper\";\n> +\t\t\t\treturn ret;\n\nAnd there's no error handling here either.\n\n> +\t\t\t}\nu> +\t\t}\n>  \t}\n>  \n>  \tif (data->mainPath_->isEnabled()) {\n> @@ -1015,6 +1071,9 @@ void PipelineHandlerRkISP1::stopDevice(Camera *camera)\n>  \t\tif (ret)\n>  \t\t\tLOG(RkISP1, Warning)\n>  \t\t\t\t<< \"Failed to stop parameters for \" << camera->id();\n> +\n> +\t\tif (useDewarper_)\n> +\t\t\tdewarper_->stop();\n>  \t}\n>  \n>  \tASSERT(data->queuedRequests_.empty());\n> @@ -1045,6 +1104,25 @@ int PipelineHandlerRkISP1::queueRequestDevice(Camera *camera, Request *request)\n>  \t\t\t\t\t     info->paramBuffer->cookie());\n>  \t}\n>  \n> +\tconst auto &crop = request->controls().get(controls::ScalerCrop);\n> +\tif (crop && useDewarper_) {\n> +\t\tRectangle appliedRect = crop.value();\n> +\t\tint ret = dewarper_->setInputCrop(&data->mainPathStream_, &appliedRect);\n\nThis doesn't seem right, you're applying the crop rectangle too early.\n\n> +\t\tif (!ret) {\n> +\t\t\tif (appliedRect != crop.value()) {\n> +\t\t\t\t/*\n> +\t\t\t\t * \\todo How to handle these case?\n> +\t\t\t\t * Do we aim for pixel perfect set rectangles?\n> +\t\t\t\t */\n\nThis needs to be addressed, we need a decision (and a rationale), and we\nneed to document it and handle the outcome accordingly. A warning\nmessage isn't a good solution.\n\n> +\t\t\t\tLOG(RkISP1, Warning)\n> +\t\t\t\t\t<< \"Applied rectangle \" << appliedRect.toString()\n> +\t\t\t\t\t<< \" differs from requested \" << crop.value().toString();\n> +\t\t\t}\n> +\n> +\t\t\tinfo->scalerCrop = appliedRect;\n> +\t\t}\n> +\t}\n> +\n>  \tdata->frame_++;\n>  \n>  \treturn 0;\n> @@ -1110,6 +1188,12 @@ int PipelineHandlerRkISP1::updateControls(RkISP1CameraData *data)\n>  {\n>  \tControlInfoMap::Map rkisp1Controls;\n>  \n> +\tif (dewarper_) {\n> +\t\tauto [minCrop, maxCrop] = dewarper_->inputCropBounds(&data->mainPathStream_);\n> +\n> +\t\trkisp1Controls[&controls::ScalerCrop] = ControlInfo(minCrop, maxCrop, maxCrop);\n> +\t}\n> +\n>  \t/* Add the IPA registered controls to list of camera controls. */\n>  \tfor (const auto &ipaControl : data->ipaControls_)\n>  \t\trkisp1Controls[ipaControl.first] = ipaControl.second;\n> @@ -1173,6 +1257,7 @@ int PipelineHandlerRkISP1::createCamera(MediaEntity *sensor)\n>  \n>  bool PipelineHandlerRkISP1::match(DeviceEnumerator *enumerator)\n>  {\n> +\tstd::shared_ptr<MediaDevice> dwpMediaDevice;\n\nDeclare the variable below, when you assign it.\n\n>  \tconst MediaPad *pad;\n>  \n>  \tDeviceMatch dm(\"rkisp1\");\n> @@ -1250,6 +1335,26 @@ bool PipelineHandlerRkISP1::match(DeviceEnumerator *enumerator)\n>  \tstat_->bufferReady.connect(this, &PipelineHandlerRkISP1::statReady);\n>  \tparam_->bufferReady.connect(this, &PipelineHandlerRkISP1::paramReady);\n>  \n> +\t/* If dewarper is present, create its instance. */\n> +\tDeviceMatch dwp(\"dw100\");\n> +\tdwp.add(\"dw100-source\");\n> +\tdwp.add(\"dw100-sink\");\n> +\n> +\tdwpMediaDevice = enumerator->search(dwp);\n> +\tif (dwpMediaDevice) {\n> +\t\tdewarper_ = std::make_unique<ConverterDW100>(std::move(dwpMediaDevice));\n> +\t\tif (dewarper_->isValid()) {\n> +\t\t\tdewarper_->outputBufferReady.connect(\n> +\t\t\t\tthis, &PipelineHandlerRkISP1::dewarpBufferReady);\n> +\n> +\t\t\tLOG(RkISP1, Info) << \"Using DW100 dewarper \" << dewarper_->deviceNode();\n> +\t\t} else {\n> +\t\t\tLOG(RkISP1, Debug) << \"Found DW100 dewarper \" << dewarper_->deviceNode()\n> +\t\t\t\t\t   << \" but invalid\";\n\t\t\tLOG(RkISP1, Debug)\n\t\t\t\t<< \"Found DW100 dewarper \" << dewarper_->deviceNode()\n\t\t\t\t<< \" but invalid\";\n\nI think this should be a warning at least.\n\n> +\t\t\tdewarper_.reset();\n> +\t\t}\n> +\t}\n> +\n>  \t/*\n>  \t * Enumerate all sensors connected to the ISP and create one\n>  \t * camera instance for each of them.\n> @@ -1296,7 +1401,7 @@ void PipelineHandlerRkISP1::bufferReady(FrameBuffer *buffer)\n>  \t\treturn;\n>  \n>  \tconst FrameMetadata &metadata = buffer->metadata();\n> -\tRequest *request = buffer->request();\n> +\tRequest *request = info->request;\n\nIs this because internal buffers have no request associated with them ?\n\n>  \n>  \tif (metadata.status != FrameMetadata::FrameCancelled) {\n>  \t\t/*\n> @@ -1313,11 +1418,52 @@ void PipelineHandlerRkISP1::bufferReady(FrameBuffer *buffer)\n>  \t\t\t\tdata->delayedCtrls_->get(metadata.sequence);\n>  \t\t\tdata->ipa_->processStatsBuffer(info->frame, 0, ctrls);\n>  \t\t}\n> +\n\nNot needed.\n\n>  \t} else {\n>  \t\tif (isRaw_)\n>  \t\t\tinfo->metadataProcessed = true;\n>  \t}\n>  \n> +\tif (useDewarper_) {\n> +\t\t/* Do not queue cancelled frames to dewarper. */\n> +\t\tif (metadata.status == FrameMetadata::FrameCancelled) {\n> +\t\t\tfor (auto it : request->buffers())\n> +\t\t\t\tcompleteBuffer(request, it.second);\n\nWill this work with multiple streams, won't the other stream also try to\ncomplete its own buffer ? Or do you assume here that there's a single\nstream, given that the DW100 is only found in the i.M8XMP ? A comment\nwould help, but even better, you should try to complete only the buffer\nfor this stream instead of completing all buffers. That should make the\ncode more future-proof.\n\nUpdate: after reading the whole patch, there are clear assumptions that\nthere will be a single stream when the converter is used. I suppose\nthat's OK, but they're expressed in different ways that make the code\nsometimes more complex to follow. For instance in\nPipelineHandlerRkISP1::exportFrameBuffers() you completely bypass the\nmain/self path check when there's a converter, while here you iterate\nover all buffers in the request as if there could be multiple streams.\nThings may benefit from being cleaned up a bit to keep the code in a\nmaintainable state.\n\n> +\n> +\t\t\ttryCompleteRequest(info);\n> +\t\t\treturn;\n> +\t\t}\n> +\n> +\t\t/*\n> +\t\t * Queue input and output buffers to the dewarper. The output\n> +\t\t * buffers for the dewarper are the buffers of the request, supplied\n> +\t\t * by the application.\n> +\t\t */\n> +\t\tint ret = dewarper_->queueBuffers(buffer, request->buffers());\n> +\t\tif (ret < 0)\n> +\t\t\tLOG(RkISP1, Error) << \"Cannot queue buffers to dewarper: \"\n> +\t\t\t\t\t   << strerror(-ret);\n> +\n> +\t\treturn;\n> +\t}\n> +\n> +\tcompleteBuffer(request, buffer);\n> +\ttryCompleteRequest(info);\n\nYou can move this above and call it with\n\n\tif (!useDewarper_) {\n\t\t...\n\t\treturn;\n\t}\n\nand lower the indentation level of the rest of the code.\n\n> +}\n> +\n> +void PipelineHandlerRkISP1::dewarpBufferReady(FrameBuffer *buffer)\n> +{\n> +\tASSERT(activeCamera_);\n> +\tRkISP1CameraData *data = cameraData(activeCamera_);\n> +\tRequest *request = buffer->request();\n> +\n> +\tRkISP1FrameInfo *info = data->frameInfo_.find(buffer->request());\n> +\tif (!info)\n> +\t\treturn;\n> +\n> +\tif (info->scalerCrop)\n> +\t\trequest->metadata().set(controls::ScalerCrop, info->scalerCrop.value());\n\nThis means that the scaler crop metadata will be set only when a request\nhas been queued with the scaler crop control. From that point onwards,\nall request that complete will have the scaler crop metadata. Shouldn't\nyou set it for every frame ?\n\n> +\n>  \tcompleteBuffer(request, buffer);\n>  \ttryCompleteRequest(info);\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 57600BDC71\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  2 Aug 2024 21:49:57 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 85A1B63370;\n\tFri,  2 Aug 2024 23:49:56 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 7B5B16336B\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  2 Aug 2024 23:49:54 +0200 (CEST)","from pendragon.ideasonboard.com (81-175-209-231.bb.dnainternet.fi\n\t[81.175.209.231])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id A14BA2E3;\n\tFri,  2 Aug 2024 23:49:04 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"u/am1WWc\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1722635344;\n\tbh=fGgIdgEYqTkh/sG8ffMHzpHMZriajfgm8cckeX64YHs=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=u/am1WWcc9SJWDXd0rnGN+aOq8G5LgwH+IKkXyGmQe4NltRMSJu0FjUzErdXwv6jQ\n\texCgaMAdeu+L6v0var13MqGd5DBZgC2FqiS4usdS91Ob0wpHtYzes5XPro9hoHtV8B\n\te1vIVHMk0InOltbuqMSzQJVbbt5+n+TYnOjIKrWY=","Date":"Sat, 3 Aug 2024 00:49:32 +0300","From":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"Umang Jain <umang.jain@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v6 5/5] libcamera: rkisp1: Plumb the ConverterDW100\n\tconverter","Message-ID":"<20240802214932.GD3295@pendragon.ideasonboard.com>","References":"<20240726114715.226468-1-umang.jain@ideasonboard.com>\n\t<20240726114715.226468-6-umang.jain@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20240726114715.226468-6-umang.jain@ideasonboard.com>","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>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":30565,"web_url":"https://patchwork.libcamera.org/comment/30565/","msgid":"<20240804153308.GA21608@pendragon.ideasonboard.com>","date":"2024-08-04T15:33:08","subject":"Re: [PATCH v6 5/5] libcamera: rkisp1: Plumb the ConverterDW100\n\tconverter","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Umang,\n\nOn Sat, Aug 03, 2024 at 12:49:32AM +0300, Laurent Pinchart wrote:\n> On Fri, Jul 26, 2024 at 05:17:15PM +0530, Umang Jain wrote:\n> > Plumb the ConverterDW100 converter in the rkisp1 pipeline handler.\n> > If the dewarper is found, it is instantiated and buffers are exported\n> > from it, instead of RkISP1Path. Internal buffers are allocated for the\n> > RkISP1Path in case where dewarper is going to be used.\n> > \n> > The RKISP1 pipeline handler now supports scaler crop control through\n> > ConverterDW100. Register the ScalerCrop control for the cameras created\n> > in the RKISP1 pipeline handler.\n> > \n> > Signed-off-by: Umang Jain <umang.jain@ideasonboard.com>\n> > ---\n> >  src/libcamera/pipeline/rkisp1/rkisp1.cpp | 154 ++++++++++++++++++++++-\n> >  1 file changed, 150 insertions(+), 4 deletions(-)\n> > \n> > diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp\n> > index 25f2cc97..32ec0fdf 100644\n> > --- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp\n> > +++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp\n> > @@ -8,9 +8,11 @@\n> >  #include <algorithm>\n> >  #include <array>\n> >  #include <iomanip>\n> > +#include <map>\n> >  #include <memory>\n> >  #include <numeric>\n> >  #include <queue>\n> > +#include <vector>\n> >  \n> >  #include <linux/media-bus-format.h>\n> >  #include <linux/rkisp1-config.h>\n> > @@ -33,6 +35,7 @@\n> >  \n> >  #include \"libcamera/internal/camera.h\"\n> >  #include \"libcamera/internal/camera_sensor.h\"\n> > +#include \"libcamera/internal/converter/converter_dw100.h\"\n> >  #include \"libcamera/internal/delayed_controls.h\"\n> >  #include \"libcamera/internal/device_enumerator.h\"\n> >  #include \"libcamera/internal/framebuffer.h\"\n> > @@ -62,6 +65,8 @@ struct RkISP1FrameInfo {\n> >  \n> >  \tbool paramDequeued;\n> >  \tbool metadataProcessed;\n> > +\n> > +\tstd::optional<Rectangle> scalerCrop;\n> >  };\n> >  \n> >  class RkISP1Frames\n> > @@ -181,6 +186,7 @@ private:\n> >  \tvoid bufferReady(FrameBuffer *buffer);\n> >  \tvoid paramReady(FrameBuffer *buffer);\n> >  \tvoid statReady(FrameBuffer *buffer);\n> > +\tvoid dewarpBufferReady(FrameBuffer *buffer);\n> >  \tvoid frameStart(uint32_t sequence);\n> >  \n> >  \tint allocateBuffers(Camera *camera);\n> > @@ -200,6 +206,13 @@ private:\n> >  \tRkISP1MainPath mainPath_;\n> >  \tRkISP1SelfPath selfPath_;\n> >  \n> > +\tstd::unique_ptr<ConverterDW100> dewarper_;\n> > +\tbool useDewarper_;\n> > +\n> > +\t/* Internal buffers used when dewarper is being used */\n> > +\tstd::vector<std::unique_ptr<FrameBuffer>> mainPathBuffers_;\n> > +\tstd::queue<FrameBuffer *> availableMainPathBuffers_;\n> > +\n> >  \tstd::vector<std::unique_ptr<FrameBuffer>> paramBuffers_;\n> >  \tstd::vector<std::unique_ptr<FrameBuffer>> statBuffers_;\n> >  \tstd::queue<FrameBuffer *> availableParamBuffers_;\n> > @@ -222,6 +235,8 @@ RkISP1FrameInfo *RkISP1Frames::create(const RkISP1CameraData *data, Request *req\n> >  \n> >  \tFrameBuffer *paramBuffer = nullptr;\n> >  \tFrameBuffer *statBuffer = nullptr;\n> > +\tFrameBuffer *mainPathBuffer = nullptr;\n> > +\tFrameBuffer *selfPathBuffer = nullptr;\n> >  \n> >  \tif (!isRaw) {\n> >  \t\tif (pipe_->availableParamBuffers_.empty()) {\n> > @@ -239,10 +254,16 @@ RkISP1FrameInfo *RkISP1Frames::create(const RkISP1CameraData *data, Request *req\n> >  \n> >  \t\tstatBuffer = pipe_->availableStatBuffers_.front();\n> >  \t\tpipe_->availableStatBuffers_.pop();\n> > +\n> > +\t\tif (pipe_->useDewarper_) {\n> > +\t\t\tmainPathBuffer = pipe_->availableMainPathBuffers_.front();\n> > +\t\t\tpipe_->availableMainPathBuffers_.pop();\n> > +\t\t}\n> >  \t}\n> >  \n> > -\tFrameBuffer *mainPathBuffer = request->findBuffer(&data->mainPathStream_);\n> > -\tFrameBuffer *selfPathBuffer = request->findBuffer(&data->selfPathStream_);\n> > +\tif (!mainPathBuffer)\n> > +\t\tmainPathBuffer = request->findBuffer(&data->mainPathStream_);\n> > +\tselfPathBuffer = request->findBuffer(&data->selfPathStream_);\n> >  \n> >  \tRkISP1FrameInfo *info = new RkISP1FrameInfo;\n> >  \n> > @@ -268,6 +289,7 @@ int RkISP1Frames::destroy(unsigned int frame)\n> >  \n> >  \tpipe_->availableParamBuffers_.push(info->paramBuffer);\n> >  \tpipe_->availableStatBuffers_.push(info->statBuffer);\n> > +\tpipe_->availableMainPathBuffers_.push(info->mainPathBuffer);\n> >  \n> >  \tframeInfo_.erase(info->frame);\n> >  \n> > @@ -283,6 +305,7 @@ void RkISP1Frames::clear()\n> >  \n> >  \t\tpipe_->availableParamBuffers_.push(info->paramBuffer);\n> >  \t\tpipe_->availableStatBuffers_.push(info->statBuffer);\n> > +\t\tpipe_->availableMainPathBuffers_.push(info->mainPathBuffer);\n> >  \n> >  \t\tdelete info;\n> >  \t}\n> > @@ -607,7 +630,7 @@ CameraConfiguration::Status RkISP1CameraConfiguration::validate()\n> >   */\n> >  \n> >  PipelineHandlerRkISP1::PipelineHandlerRkISP1(CameraManager *manager)\n> > -\t: PipelineHandler(manager), hasSelfPath_(true)\n> > +\t: PipelineHandler(manager), hasSelfPath_(true), useDewarper_(false)\n> >  {\n> >  }\n> >  \n> > @@ -785,12 +808,19 @@ int PipelineHandlerRkISP1::configure(Camera *camera, CameraConfiguration *c)\n> >  \t\t<< \" crop \" << rect;\n> >  \n> >  \tstd::map<unsigned int, IPAStream> streamConfig;\n> > +\tstd::vector<std::reference_wrapper<StreamConfiguration>> outputCfgs;\n> >  \n> >  \tfor (const StreamConfiguration &cfg : *config) {\n> >  \t\tif (cfg.stream() == &data->mainPathStream_) {\n> >  \t\t\tret = mainPath_.configure(cfg, format);\n> >  \t\t\tstreamConfig[0] = IPAStream(cfg.pixelFormat,\n> >  \t\t\t\t\t\t    cfg.size);\n> > +\t\t\t/* Configure dewarp */\n> > +\t\t\tif (dewarper_ && !isRaw_) {\n> > +\t\t\t\toutputCfgs.push_back(const_cast<StreamConfiguration &>(cfg));\n> > +\t\t\t\tret = dewarper_->configure(cfg, outputCfgs);\n> > +\t\t\t\tuseDewarper_ = ret ? false : true;\n> > +\t\t\t}\n> >  \t\t} else if (hasSelfPath_) {\n> >  \t\t\tret = selfPath_.configure(cfg, format);\n> >  \t\t\tstreamConfig[1] = IPAStream(cfg.pixelFormat,\n> > @@ -839,6 +869,9 @@ int PipelineHandlerRkISP1::exportFrameBuffers([[maybe_unused]] Camera *camera, S\n> >  \tRkISP1CameraData *data = cameraData(camera);\n> >  \tunsigned int count = stream->configuration().bufferCount;\n> >  \n> > +\tif (useDewarper_)\n> > +\t\treturn dewarper_->exportBuffers(&data->mainPathStream_, count, buffers);\n> > +\n> >  \tif (stream == &data->mainPathStream_)\n> >  \t\treturn mainPath_.exportBuffers(count, buffers);\n> >  \telse if (hasSelfPath_ && stream == &data->selfPathStream_)\n> > @@ -866,6 +899,16 @@ int PipelineHandlerRkISP1::allocateBuffers(Camera *camera)\n> >  \t\tret = stat_->allocateBuffers(maxCount, &statBuffers_);\n> >  \t\tif (ret < 0)\n> >  \t\t\tgoto error;\n> > +\n> > +\t\t/* If the dewarper is being used, allocate internal buffers for ISP */\n> \n> s/ISP/ISP./\n> \n> > +\t\tif (useDewarper_) {\n> > +\t\t\tret = mainPath_.exportBuffers(maxCount, &mainPathBuffers_);\n> > +\t\t\tif (ret < 0)\n> > +\t\t\t\tgoto error;\n> > +\n> > +\t\t\tfor (std::unique_ptr<FrameBuffer> &buffer : mainPathBuffers_)\n> > +\t\t\t\tavailableMainPathBuffers_.push(buffer.get());\n> > +\t\t}\n> >  \t}\n> >  \n> >  \tfor (std::unique_ptr<FrameBuffer> &buffer : paramBuffers_) {\n> > @@ -889,6 +932,7 @@ int PipelineHandlerRkISP1::allocateBuffers(Camera *camera)\n> >  error:\n> >  \tparamBuffers_.clear();\n> >  \tstatBuffers_.clear();\n> > +\tmainPathBuffers_.clear();\n> >  \n> >  \treturn ret;\n> >  }\n> > @@ -903,8 +947,12 @@ int PipelineHandlerRkISP1::freeBuffers(Camera *camera)\n> >  \twhile (!availableParamBuffers_.empty())\n> >  \t\tavailableParamBuffers_.pop();\n> >  \n> > +\twhile (!availableMainPathBuffers_.empty())\n> > +\t\tavailableMainPathBuffers_.pop();\n> > +\n> >  \tparamBuffers_.clear();\n> >  \tstatBuffers_.clear();\n> > +\tmainPathBuffers_.clear();\n> >  \n> >  \tstd::vector<unsigned int> ids;\n> >  \tfor (IPABuffer &ipabuf : data->ipaBuffers_)\n> > @@ -961,6 +1009,14 @@ int PipelineHandlerRkISP1::start(Camera *camera, [[maybe_unused]] const ControlL\n> >  \t\t\t\t<< \"Failed to start statistics \" << camera->id();\n> >  \t\t\treturn ret;\n> >  \t\t}\n> > +\n> > +\t\tif (useDewarper_) {\n> > +\t\t\tret = dewarper_->start();\n> \n> You don't stop the dewarper in the error paths below.\n> \n> > +\t\t\tif (ret) {\n> > +\t\t\t\tLOG(RkISP1, Error) << \"Failed to start dewarper\";\n> > +\t\t\t\treturn ret;\n> \n> And there's no error handling here either.\n\nI've just posted \"[PATCH v3 0/2] libcamera: Simplify error handling with\nScopeExitActions class\" which may be useful for you here.\n\n> > +\t\t\t}\n> > +\t\t}\n> >  \t}\n> >  \n> >  \tif (data->mainPath_->isEnabled()) {\n> > @@ -1015,6 +1071,9 @@ void PipelineHandlerRkISP1::stopDevice(Camera *camera)\n> >  \t\tif (ret)\n> >  \t\t\tLOG(RkISP1, Warning)\n> >  \t\t\t\t<< \"Failed to stop parameters for \" << camera->id();\n> > +\n> > +\t\tif (useDewarper_)\n> > +\t\t\tdewarper_->stop();\n> >  \t}\n> >  \n> >  \tASSERT(data->queuedRequests_.empty());\n> > @@ -1045,6 +1104,25 @@ int PipelineHandlerRkISP1::queueRequestDevice(Camera *camera, Request *request)\n> >  \t\t\t\t\t     info->paramBuffer->cookie());\n> >  \t}\n> >  \n> > +\tconst auto &crop = request->controls().get(controls::ScalerCrop);\n> > +\tif (crop && useDewarper_) {\n> > +\t\tRectangle appliedRect = crop.value();\n> > +\t\tint ret = dewarper_->setInputCrop(&data->mainPathStream_, &appliedRect);\n> \n> This doesn't seem right, you're applying the crop rectangle too early.\n> \n> > +\t\tif (!ret) {\n> > +\t\t\tif (appliedRect != crop.value()) {\n> > +\t\t\t\t/*\n> > +\t\t\t\t * \\todo How to handle these case?\n> > +\t\t\t\t * Do we aim for pixel perfect set rectangles?\n> > +\t\t\t\t */\n> \n> This needs to be addressed, we need a decision (and a rationale), and we\n> need to document it and handle the outcome accordingly. A warning\n> message isn't a good solution.\n> \n> > +\t\t\t\tLOG(RkISP1, Warning)\n> > +\t\t\t\t\t<< \"Applied rectangle \" << appliedRect.toString()\n> > +\t\t\t\t\t<< \" differs from requested \" << crop.value().toString();\n> > +\t\t\t}\n> > +\n> > +\t\t\tinfo->scalerCrop = appliedRect;\n> > +\t\t}\n> > +\t}\n> > +\n> >  \tdata->frame_++;\n> >  \n> >  \treturn 0;\n> > @@ -1110,6 +1188,12 @@ int PipelineHandlerRkISP1::updateControls(RkISP1CameraData *data)\n> >  {\n> >  \tControlInfoMap::Map rkisp1Controls;\n> >  \n> > +\tif (dewarper_) {\n> > +\t\tauto [minCrop, maxCrop] = dewarper_->inputCropBounds(&data->mainPathStream_);\n> > +\n> > +\t\trkisp1Controls[&controls::ScalerCrop] = ControlInfo(minCrop, maxCrop, maxCrop);\n> > +\t}\n> > +\n> >  \t/* Add the IPA registered controls to list of camera controls. */\n> >  \tfor (const auto &ipaControl : data->ipaControls_)\n> >  \t\trkisp1Controls[ipaControl.first] = ipaControl.second;\n> > @@ -1173,6 +1257,7 @@ int PipelineHandlerRkISP1::createCamera(MediaEntity *sensor)\n> >  \n> >  bool PipelineHandlerRkISP1::match(DeviceEnumerator *enumerator)\n> >  {\n> > +\tstd::shared_ptr<MediaDevice> dwpMediaDevice;\n> \n> Declare the variable below, when you assign it.\n> \n> >  \tconst MediaPad *pad;\n> >  \n> >  \tDeviceMatch dm(\"rkisp1\");\n> > @@ -1250,6 +1335,26 @@ bool PipelineHandlerRkISP1::match(DeviceEnumerator *enumerator)\n> >  \tstat_->bufferReady.connect(this, &PipelineHandlerRkISP1::statReady);\n> >  \tparam_->bufferReady.connect(this, &PipelineHandlerRkISP1::paramReady);\n> >  \n> > +\t/* If dewarper is present, create its instance. */\n> > +\tDeviceMatch dwp(\"dw100\");\n> > +\tdwp.add(\"dw100-source\");\n> > +\tdwp.add(\"dw100-sink\");\n> > +\n> > +\tdwpMediaDevice = enumerator->search(dwp);\n> > +\tif (dwpMediaDevice) {\n> > +\t\tdewarper_ = std::make_unique<ConverterDW100>(std::move(dwpMediaDevice));\n> > +\t\tif (dewarper_->isValid()) {\n> > +\t\t\tdewarper_->outputBufferReady.connect(\n> > +\t\t\t\tthis, &PipelineHandlerRkISP1::dewarpBufferReady);\n> > +\n> > +\t\t\tLOG(RkISP1, Info) << \"Using DW100 dewarper \" << dewarper_->deviceNode();\n> > +\t\t} else {\n> > +\t\t\tLOG(RkISP1, Debug) << \"Found DW100 dewarper \" << dewarper_->deviceNode()\n> > +\t\t\t\t\t   << \" but invalid\";\n> \t\t\tLOG(RkISP1, Debug)\n> \t\t\t\t<< \"Found DW100 dewarper \" << dewarper_->deviceNode()\n> \t\t\t\t<< \" but invalid\";\n> \n> I think this should be a warning at least.\n> \n> > +\t\t\tdewarper_.reset();\n> > +\t\t}\n> > +\t}\n> > +\n> >  \t/*\n> >  \t * Enumerate all sensors connected to the ISP and create one\n> >  \t * camera instance for each of them.\n> > @@ -1296,7 +1401,7 @@ void PipelineHandlerRkISP1::bufferReady(FrameBuffer *buffer)\n> >  \t\treturn;\n> >  \n> >  \tconst FrameMetadata &metadata = buffer->metadata();\n> > -\tRequest *request = buffer->request();\n> > +\tRequest *request = info->request;\n> \n> Is this because internal buffers have no request associated with them ?\n> \n> >  \n> >  \tif (metadata.status != FrameMetadata::FrameCancelled) {\n> >  \t\t/*\n> > @@ -1313,11 +1418,52 @@ void PipelineHandlerRkISP1::bufferReady(FrameBuffer *buffer)\n> >  \t\t\t\tdata->delayedCtrls_->get(metadata.sequence);\n> >  \t\t\tdata->ipa_->processStatsBuffer(info->frame, 0, ctrls);\n> >  \t\t}\n> > +\n> \n> Not needed.\n> \n> >  \t} else {\n> >  \t\tif (isRaw_)\n> >  \t\t\tinfo->metadataProcessed = true;\n> >  \t}\n> >  \n> > +\tif (useDewarper_) {\n> > +\t\t/* Do not queue cancelled frames to dewarper. */\n> > +\t\tif (metadata.status == FrameMetadata::FrameCancelled) {\n> > +\t\t\tfor (auto it : request->buffers())\n> > +\t\t\t\tcompleteBuffer(request, it.second);\n> \n> Will this work with multiple streams, won't the other stream also try to\n> complete its own buffer ? Or do you assume here that there's a single\n> stream, given that the DW100 is only found in the i.M8XMP ? A comment\n> would help, but even better, you should try to complete only the buffer\n> for this stream instead of completing all buffers. That should make the\n> code more future-proof.\n> \n> Update: after reading the whole patch, there are clear assumptions that\n> there will be a single stream when the converter is used. I suppose\n> that's OK, but they're expressed in different ways that make the code\n> sometimes more complex to follow. For instance in\n> PipelineHandlerRkISP1::exportFrameBuffers() you completely bypass the\n> main/self path check when there's a converter, while here you iterate\n> over all buffers in the request as if there could be multiple streams.\n> Things may benefit from being cleaned up a bit to keep the code in a\n> maintainable state.\n> \n> > +\n> > +\t\t\ttryCompleteRequest(info);\n> > +\t\t\treturn;\n> > +\t\t}\n> > +\n> > +\t\t/*\n> > +\t\t * Queue input and output buffers to the dewarper. The output\n> > +\t\t * buffers for the dewarper are the buffers of the request, supplied\n> > +\t\t * by the application.\n> > +\t\t */\n> > +\t\tint ret = dewarper_->queueBuffers(buffer, request->buffers());\n> > +\t\tif (ret < 0)\n> > +\t\t\tLOG(RkISP1, Error) << \"Cannot queue buffers to dewarper: \"\n> > +\t\t\t\t\t   << strerror(-ret);\n> > +\n> > +\t\treturn;\n> > +\t}\n> > +\n> > +\tcompleteBuffer(request, buffer);\n> > +\ttryCompleteRequest(info);\n> \n> You can move this above and call it with\n> \n> \tif (!useDewarper_) {\n> \t\t...\n> \t\treturn;\n> \t}\n> \n> and lower the indentation level of the rest of the code.\n> \n> > +}\n> > +\n> > +void PipelineHandlerRkISP1::dewarpBufferReady(FrameBuffer *buffer)\n> > +{\n> > +\tASSERT(activeCamera_);\n> > +\tRkISP1CameraData *data = cameraData(activeCamera_);\n> > +\tRequest *request = buffer->request();\n> > +\n> > +\tRkISP1FrameInfo *info = data->frameInfo_.find(buffer->request());\n> > +\tif (!info)\n> > +\t\treturn;\n> > +\n> > +\tif (info->scalerCrop)\n> > +\t\trequest->metadata().set(controls::ScalerCrop, info->scalerCrop.value());\n> \n> This means that the scaler crop metadata will be set only when a request\n> has been queued with the scaler crop control. From that point onwards,\n> all request that complete will have the scaler crop metadata. Shouldn't\n> you set it for every frame ?\n> \n> > +\n> >  \tcompleteBuffer(request, buffer);\n> >  \ttryCompleteRequest(info);\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 F4055BDC71\n\tfor <parsemail@patchwork.libcamera.org>;\n\tSun,  4 Aug 2024 15:33:32 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 2522763384;\n\tSun,  4 Aug 2024 17:33:32 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 82BD16337F\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tSun,  4 Aug 2024 17:33:30 +0200 (CEST)","from pendragon.ideasonboard.com (81-175-209-231.bb.dnainternet.fi\n\t[81.175.209.231])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 729F51BA;\n\tSun,  4 Aug 2024 17:32:39 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"uJCU1GaO\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1722785559;\n\tbh=YFPDLkUG6g25/fqNzolLP1zHeDnNy4qEz2QhvAXFnOw=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=uJCU1GaOPa+FeGd0E+Fs3IjwhSbaLwOucg4AAIBYkUHW0THEPn8cKDYit06DdpI3U\n\tC/wjtfFAOxbPQggvjQigIk0WnKLDkrJx6Q//05za7LdYGFLvMXUGsfobE/HEEaL2UX\n\tVKnuQPIrQOC3+2iEzroWukNQpfOENP/8ZEp0/dtI=","Date":"Sun, 4 Aug 2024 18:33:08 +0300","From":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"Umang Jain <umang.jain@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH v6 5/5] libcamera: rkisp1: Plumb the ConverterDW100\n\tconverter","Message-ID":"<20240804153308.GA21608@pendragon.ideasonboard.com>","References":"<20240726114715.226468-1-umang.jain@ideasonboard.com>\n\t<20240726114715.226468-6-umang.jain@ideasonboard.com>\n\t<20240802214932.GD3295@pendragon.ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20240802214932.GD3295@pendragon.ideasonboard.com>","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>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]