From patchwork Thu Oct 17 12:52:16 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21655 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id E8554C326C for ; Thu, 17 Oct 2024 12:52:36 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 3CD3C6538B; Thu, 17 Oct 2024 14:52:34 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="cmzNJars"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 43E6B6537F for ; Thu, 17 Oct 2024 14:52:31 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.64.27]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 73F2F827; Thu, 17 Oct 2024 14:50:47 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1729169448; bh=6RvijhuVcz/qVbhAnx944ewCoc9YkX8D0jaV43Ajw7U=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=cmzNJarsnzqume36FUwMhRdkWDei4rLBylbDgreIgoWL8JPPaGNLtKzN7c4UhqNKf 89EEtOzCPJIeQMcQD9PdbhVaPZ3K6yzsdQ/MhzkUqnhFv9oYzaly5QlCy77Qceohgq aXrFfWE4BbSQ3gaLPyFDoO//+lhzx8CpH0Big040= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Paul Elder Subject: [RFC v2 1/4] pipeline: Add support for dumping capture script and metadata Date: Thu, 17 Oct 2024 14:52:16 +0200 Message-ID: <20241017125220.60567-2-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.47.0 In-Reply-To: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> References: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Paul Elder Add support for dumping capture scripts and metadata. The capture scripts can then be fed into the cam application and a capture can thus be "replayed". Metadata can also be dumped. Camera configuration is also dumped to the capture script. The cam application currently does not support loading configuration from the capture script, but support for that will be added in a subsequent patch. These can be enabled by a new environment variable. Signed-off-by: Paul Elder --- include/libcamera/internal/camera.h | 3 + include/libcamera/internal/pipeline_handler.h | 16 ++++ src/libcamera/camera.cpp | 13 +++ src/libcamera/pipeline_handler.cpp | 93 ++++++++++++++++++- 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/include/libcamera/internal/camera.h b/include/libcamera/internal/camera.h index 0add0428bb5d..a42f03d4c755 100644 --- a/include/libcamera/internal/camera.h +++ b/include/libcamera/internal/camera.h @@ -19,6 +19,8 @@ namespace libcamera { +enum class Orientation; + class CameraControlValidator; class PipelineHandler; class Stream; @@ -65,6 +67,7 @@ private: std::string id_; std::set streams_; std::set activeStreams_; + Orientation orientation_; bool disconnected_; std::atomic state_; diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h index 0d38080369c5..fb3914185a01 100644 --- a/include/libcamera/internal/pipeline_handler.h +++ b/include/libcamera/internal/pipeline_handler.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,8 @@ namespace libcamera { +enum class Orientation; + class Camera; class CameraConfiguration; class CameraManager; @@ -68,6 +71,9 @@ public: CameraManager *cameraManager() const { return manager_; } + void dumpConfiguration(const std::set &streams, + const Orientation &orientation); + protected: void registerCamera(std::shared_ptr camera); void hotplugMediaDevice(MediaDevice *media); @@ -81,6 +87,11 @@ protected: CameraManager *manager_; private: + enum DumpMode { + Controls, + Metadata, + }; + void unlockMediaDevices(); void mediaDeviceDisconnected(MediaDevice *media); @@ -89,6 +100,8 @@ private: void doQueueRequest(Request *request); void doQueueRequests(); + void dumpRequest(Request *request, DumpMode mode); + std::vector> mediaDevices_; std::vector> cameras_; @@ -97,6 +110,9 @@ private: const char *name_; unsigned int useCount_; + std::ostream *dumpCaptureScript_; + std::ostream *dumpMetadata_; + friend class PipelineHandlerFactoryBase; }; diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp index 7507e9ddae77..9d28b1202397 100644 --- a/src/libcamera/camera.cpp +++ b/src/libcamera/camera.cpp @@ -1215,6 +1215,9 @@ int Camera::configure(CameraConfiguration *config) d->activeStreams_.insert(stream); } + /* TODO Save sensor configuration for dumping it to capture script */ + d->orientation_ = config->orientation; + d->setState(Private::CameraConfigured); return 0; @@ -1356,6 +1359,16 @@ int Camera::start(const ControlList *controls) ASSERT(d->requestSequence_ == 0); + /* + * Invoke method in blocking mode to avoid the risk of writing after + * streaming has started. + * This needs to be here as PipelineHandler::start is a virtual function + * so it is impractical to add the dumping there. + * TODO Pass the sensor configuration, once it is supported + */ + d->pipe_->invokeMethod(&PipelineHandler::dumpConfiguration, + ConnectionTypeBlocking, d->activeStreams_, d->orientation_); + ret = d->pipe_->invokeMethod(&PipelineHandler::start, ConnectionTypeBlocking, this, controls); if (ret) diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp index e5940469127e..7002b4323bdd 100644 --- a/src/libcamera/pipeline_handler.cpp +++ b/src/libcamera/pipeline_handler.cpp @@ -8,6 +8,7 @@ #include "libcamera/internal/pipeline_handler.h" #include +#include #include #include @@ -68,14 +69,36 @@ LOG_DEFINE_CATEGORY(Pipeline) * through the PipelineHandlerFactoryBase::create() function. */ PipelineHandler::PipelineHandler(CameraManager *manager) - : manager_(manager), useCount_(0) + : manager_(manager), useCount_(0), + dumpCaptureScript_(nullptr), dumpMetadata_(nullptr) { + /* TODO Print notification that we're dumping capture script */ + const char *file = utils::secure_getenv("LIBCAMERA_DUMP_CAPTURE_SCRIPT"); + if (!file) + return; + + dumpCaptureScript_ = new std::ofstream(file); + + /* + * Metadata needs to go into a separate file because otherwise it'll + * flood the capture script + */ + dumpMetadata_ = new std::ofstream(std::string(file) + ".metadata"); + std::string str = "frames:\n"; + dumpMetadata_->write(str.c_str(), str.size()); + dumpMetadata_->flush(); } PipelineHandler::~PipelineHandler() { for (std::shared_ptr media : mediaDevices_) media->release(); + + if (dumpCaptureScript_) + delete dumpCaptureScript_; + + if (dumpMetadata_) + delete dumpMetadata_; } /** @@ -464,6 +487,8 @@ void PipelineHandler::doQueueRequest(Request *request) request->_d()->sequence_ = data->requestSequence_++; + dumpRequest(request, DumpMode::Controls); + if (request->_d()->cancelled_) { completeRequest(request); return; @@ -555,6 +580,8 @@ void PipelineHandler::completeRequest(Request *request) request->_d()->complete(); + dumpRequest(request, DumpMode::Metadata); + Camera::Private *data = camera->_d(); while (!data->queuedRequests_.empty()) { @@ -758,6 +785,70 @@ void PipelineHandler::disconnect() * \return The CameraManager for this pipeline handler */ +void PipelineHandler::dumpConfiguration(const std::set &streams, + const Orientation &orientation) +{ + if (!dumpCaptureScript_) + return; + + std::stringstream ss; + ss << "configuration:" << std::endl; + ss << " orientation: " << orientation << std::endl; + + /* TODO Dump Sensor configuration */ + + ss << " streams:" << std::endl; + for (const auto &stream : streams) { + const StreamConfiguration &streamConfig = stream->configuration(); + ss << " - pixelFormat: " << streamConfig.pixelFormat << std::endl; + ss << " size: " << streamConfig.size << std::endl; + ss << " stride: " << streamConfig.stride << std::endl; + ss << " frameSize: " << streamConfig.frameSize << std::endl; + ss << " bufferCount: " << streamConfig.bufferCount << std::endl; + if (streamConfig.colorSpace) + ss << " colorSpace: " << streamConfig.colorSpace->toString() << std::endl; + } + + dumpCaptureScript_->write(ss.str().c_str(), ss.str().size()); + + std::string str = "frames:\n"; + dumpCaptureScript_->write(str.c_str(), str.size()); + dumpCaptureScript_->flush(); +} + +void PipelineHandler::dumpRequest(Request *request, DumpMode mode) +{ + ControlList &controls = + mode == DumpMode::Controls ? request->controls() + : request->metadata(); + std::ostream *output = + mode == DumpMode::Controls ? dumpCaptureScript_ + : dumpMetadata_; + + if (!output || controls.empty()) + return; + + std::stringstream ss; + /* TODO Figure out PFC */ + ss << " - " << request->sequence() << ":" << std::endl; + + const ControlIdMap *idMap = controls.idMap(); + for (const auto &pair : controls) { + const ControlId *ctrlId = idMap->at(pair.first); + /* TODO Prettify enums (probably by upgrading ControlValue::toString()) */ + ss << " " << ctrlId->name() << ": " << pair.second.toString() << std::endl; + } + + /* + * TODO Investigate the overhead of flushing this frequently + * Controls aren't going to be queued too frequently so it should be + * fine to dump controls every frame. Metadata on the other hand needs + * to be investigated. + */ + output->write(ss.str().c_str(), ss.str().size()); + output->flush(); +} + /** * \class PipelineHandlerFactoryBase * \brief Base class for pipeline handler factories From patchwork Thu Oct 17 12:52:17 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21656 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id DC563C326C for ; Thu, 17 Oct 2024 12:52:39 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 8378265388; Thu, 17 Oct 2024 14:52:39 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="fpDS8xHp"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 07D1E65383 for ; Thu, 17 Oct 2024 14:52:32 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.64.27]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id C5D4A6DC; Thu, 17 Oct 2024 14:50:48 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1729169449; bh=xHz6ndBR0M3jsIGSHeETxbTW+B6B3MkQyyrSGEuk+rA=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=fpDS8xHplAV9po4uPR3pwZtmsAm7rwkkYtkvYL6cNj7u5LGAh75ckHsXcVtNLuafj kmdRodDkWrgWG2FYb80TFp8W3NkdWW/OOEhr8WIRkMkGGbWXRDZknekk094APXfXaO gOYPaDfmLPskXNpuhRdDnI6rqfzaqWzLDyBvelEo= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi , Paul Elder Subject: [RFC v2 2/4] apps: cam: Add support for loading configuration from capture script Date: Thu, 17 Oct 2024 14:52:17 +0200 Message-ID: <20241017125220.60567-3-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.47.0 In-Reply-To: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> References: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Paul Elder Add support to the cam application for loading the camera configuration from a capture script. These are not expected to be written by hand, but rather dumped via the LIBCAMERA_DUMP_CAPTURE_SCRIPT environment variable. If any configuration options are specified by command line parameters, those will take precedence. Signed-off-by: Paul Elder --- src/apps/cam/camera_session.cpp | 22 +++-- src/apps/cam/capture_script.cpp | 164 ++++++++++++++++++++++++++++++++ src/apps/cam/capture_script.h | 8 ++ 3 files changed, 184 insertions(+), 10 deletions(-) diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp index edc49b875450..6f1d8b58870f 100644 --- a/src/apps/cam/camera_session.cpp +++ b/src/apps/cam/camera_session.cpp @@ -70,6 +70,18 @@ CameraSession::CameraSession(CameraManager *cm, return; } + if (options_.isSet(OptCaptureScript)) { + std::string scriptName = options_[OptCaptureScript].toString(); + script_ = std::make_unique(camera_, scriptName); + if (!script_->valid()) { + std::cerr << "Invalid capture script '" << scriptName + << "'" << std::endl; + return; + } + + script_->populateConfiguration(config.get()); + } + if (options_.isSet(OptOrientation)) { std::string orientOpt = options_[OptOrientation].toString(); static const std::map orientations{ @@ -119,16 +131,6 @@ CameraSession::CameraSession(CameraManager *cm, } #endif - if (options_.isSet(OptCaptureScript)) { - std::string scriptName = options_[OptCaptureScript].toString(); - script_ = std::make_unique(camera_, scriptName); - if (!script_->valid()) { - std::cerr << "Invalid capture script '" << scriptName - << "'" << std::endl; - return; - } - } - switch (config->validate()) { case CameraConfiguration::Valid: break; diff --git a/src/apps/cam/capture_script.cpp b/src/apps/cam/capture_script.cpp index fc1dfa75f2d4..7f166f45657e 100644 --- a/src/apps/cam/capture_script.cpp +++ b/src/apps/cam/capture_script.cpp @@ -7,6 +7,7 @@ #include "capture_script.h" +#include #include #include #include @@ -162,6 +163,10 @@ int CaptureScript::parseScript(FILE *script) ret = parseFrames(); if (ret) return ret; + } else if (section == "configuration") { + ret = parseConfiguration(); + if (ret) + return ret; } else { std::cerr << "Unsupported section '" << section << "'" << std::endl; @@ -322,6 +327,165 @@ int CaptureScript::parseControl(EventPtr event, ControlList &controls) return 0; } +int CaptureScript::parseOrientation(EventPtr event) +{ + static const std::map orientations{ + { "Rotate0", libcamera::Orientation::Rotate0 }, + { "Rotate0Mirror", libcamera::Orientation::Rotate0Mirror }, + { "Rotate180", libcamera::Orientation::Rotate180 }, + { "Rotate180Mirror", libcamera::Orientation::Rotate180Mirror }, + { "Rotate90Mirror", libcamera::Orientation::Rotate90Mirror }, + { "Rotate270", libcamera::Orientation::Rotate270 }, + { "Rotate270Mirror", libcamera::Orientation::Rotate270Mirror }, + { "Rotate90", libcamera::Orientation::Rotate90 }, + }; + + std::string orientation = eventScalarValue(event); + + auto it = orientations.find(orientation); + if (it == orientations.end()) { + std::cerr << "Invalid orientation '" << orientation + << "' in capture script" << std::endl; + return -EINVAL; + } + + orientation_ = it->second; + + return 0; +} + +int CaptureScript::parseStream(EventPtr event, unsigned int index) +{ + if (!checkEvent(event, YAML_MAPPING_START_EVENT)) + return -EINVAL; + + StreamConfiguration config; + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + std::string key = eventScalarValue(event); + + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + std::string value = eventScalarValue(event); + + if (key == "pixelFormat") { + config.pixelFormat = libcamera::PixelFormat::fromString(value); + } else if (key == "size") { + unsigned int split = value.find("x"); + if (split == std::string::npos) { + std::cerr << "Invalid size '" << value + << "' in stream configuration " + << index << std::endl; + } + + std::string width = value.substr(0, split); + std::string height = value.substr(split + 1); + config.size = Size(std::stoi(width), std::stoi(height)); + } else if (key == "stride") { + config.stride = std::stoi(value); + } else if (key == "frameSize") { + config.frameSize = std::stoi(value); + } else if (key == "bufferCount") { + config.bufferCount = std::stoi(value); + } else if (key == "colorSpace") { + config.colorSpace = libcamera::ColorSpace::fromString(value); + } else { + std::cerr << "Unknown key-value pair '" + << key << "': '" << value + << "' in stream configuration " + << index << std::endl; + return -EINVAL; + } + } + + streamConfigs_.push_back(config); + + return 0; +} + +int CaptureScript::parseStreams(EventPtr event) +{ + if (!checkEvent(event, YAML_SEQUENCE_START_EVENT)) + return -EINVAL; + + unsigned int index = 0; + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_SEQUENCE_END_EVENT) + return 0; + + if (event->type == YAML_MAPPING_START_EVENT) { + parseStream(std::move(event), index++); + continue; + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return -EINVAL; + } + } + + return 0; +} + +int CaptureScript::parseConfiguration() +{ + int ret; + + EventPtr event = nextEvent(YAML_MAPPING_START_EVENT); + if (!event) + return -EINVAL; + + while (1) { + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + std::string key = eventScalarValue(event); + + event = nextEvent(); + if (!event) + return -EINVAL; + if (event->type == YAML_MAPPING_END_EVENT) + break; + + /* TODO Load sensor configuration */ + if (key == "orientation") { + ret = parseOrientation(std::move(event)); + if (ret) + return ret; + } else if (key == "streams") { + ret = parseStreams(std::move(event)); + if (ret) + return ret; + } + } + + return 0; +} + +void CaptureScript::populateConfiguration(CameraConfiguration *configuration) const +{ + if (!configuration) + return; + + configuration->orientation = orientation_; + + for (unsigned int i = 0; i < streamConfigs_.size(); i++) + (*configuration)[i] = streamConfigs_[i]; +} + std::string CaptureScript::parseScalar() { EventPtr event = nextEvent(YAML_SCALAR_EVENT); diff --git a/src/apps/cam/capture_script.h b/src/apps/cam/capture_script.h index 294b920363ba..4ba862d742cf 100644 --- a/src/apps/cam/capture_script.h +++ b/src/apps/cam/capture_script.h @@ -26,6 +26,7 @@ public: const libcamera::ControlList &frameControls(unsigned int frame); + void populateConfiguration(libcamera::CameraConfiguration *configuration) const; private: struct EventDeleter { void operator()(yaml_event_t *event) const @@ -43,6 +44,9 @@ private: unsigned int loop_; bool valid_; + libcamera::Orientation orientation_; + std::vector streamConfigs_; + EventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT); bool checkEvent(const EventPtr &event, yaml_event_type_t expectedType) const; static std::string eventScalarValue(const EventPtr &event); @@ -55,6 +59,10 @@ private: int parseFrames(); int parseFrame(EventPtr event); int parseControl(EventPtr event, libcamera::ControlList &controls); + int parseConfiguration(); + int parseOrientation(EventPtr event); + int parseStreams(EventPtr event); + int parseStream(EventPtr event, unsigned int index); libcamera::ControlValue parseScalarControl(const libcamera::ControlId *id, const std::string repr); From patchwork Thu Oct 17 12:52:18 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21657 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 8B912C32FD for ; Thu, 17 Oct 2024 12:52:41 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id F30416538C; Thu, 17 Oct 2024 14:52:40 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="ZKpRyAlc"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 6E84B65387 for ; Thu, 17 Oct 2024 14:52:33 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.64.27]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 072DA827; Thu, 17 Oct 2024 14:50:49 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1729169450; bh=XXHKXPzAfJiudNvP4pazjj/DWrTK6xt/dq4EJHQaHDM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ZKpRyAlcb1O+I4wvvPyF0LlZvbz+QWh+ONS//iUhgJWLO86sP1/u8bKiflwhafwV3 mKI9TNy08/NZ9stvmdNOpsD/E73vqdIh6FBQJFzOmjMsBi29BIFpnYgFHyHYQpbjku EDWNJPrLAD+7apulThUJjyViucK7MzLXfcOiPNWY= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [RFC v2 3/4] libcamera: Implement YamlEmitter Date: Thu, 17 Oct 2024 14:52:18 +0200 Message-ID: <20241017125220.60567-4-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.47.0 In-Reply-To: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> References: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Signed-off-by: Jacopo Mondi --- include/libcamera/internal/meson.build | 1 + include/libcamera/internal/yaml_emitter.h | 162 ++++++++++ src/libcamera/meson.build | 1 + src/libcamera/yaml_emitter.cpp | 362 ++++++++++++++++++++++ 4 files changed, 526 insertions(+) create mode 100644 include/libcamera/internal/yaml_emitter.h create mode 100644 src/libcamera/yaml_emitter.cpp diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build index 1c5eef9cab80..7533b075fde2 100644 --- a/include/libcamera/internal/meson.build +++ b/include/libcamera/internal/meson.build @@ -41,6 +41,7 @@ libcamera_internal_headers = files([ 'v4l2_pixelformat.h', 'v4l2_subdevice.h', 'v4l2_videodevice.h', + 'yaml_emitter.h', 'yaml_parser.h', ]) diff --git a/include/libcamera/internal/yaml_emitter.h b/include/libcamera/internal/yaml_emitter.h new file mode 100644 index 000000000000..e4a0e3b440a5 --- /dev/null +++ b/include/libcamera/internal/yaml_emitter.h @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * libcamera YAML emitter helper + */ + +#pragma once + +#include +#include + +#include +#include +#include + +#include + +namespace libcamera { + +class YamlDict; +class YamlEvent; +class YamlList; +class YamlRoot; +class YamlScalar; + +class YamlEmitter final +{ +public: + ~YamlEmitter(); + + static std::unique_ptr root(std::string_view path); + + int emit(); + yaml_event_t *event() { return &event_; } + +private: + LIBCAMERA_DISABLE_COPY(YamlEmitter) + + class Emitter + { + public: + ~Emitter(); + + void init(File *file); + + int emit(yaml_event_t *event); + + private: + void logError(); + + yaml_emitter_t emitter_; + }; + + YamlEmitter() = default; + + void init(); + + std::unique_ptr file_; + yaml_event_t event_; + Emitter emitter_; +}; + +class YamlOutput +{ +public: + virtual ~YamlOutput() {}; + + YamlOutput(YamlOutput &&other) + { + emitter_ = other.emitter_; + other.emitter_ = nullptr; + } + + YamlScalar scalar(); + std::unique_ptr dict(); + std::unique_ptr list(); + +protected: + YamlOutput(YamlEmitter *emitter) + : emitter_(emitter) + { + } + + int emitScalar(std::string_view scalar); + int emitMappingStart(); + int emitMappingEnd(); + int emitSequenceStart(); + int emitSequenceEnd(); + + YamlEmitter *emitter_; + yaml_event_t event_; +}; + +class YamlRoot : public YamlOutput +{ +public: + ~YamlRoot(); + + std::unique_ptr list(); + std::unique_ptr dict(); + void scalar(std::string_view scalar); + +private: + friend class YamlEmitter; + + YamlRoot(YamlEmitter *emitter) + : YamlOutput(emitter) + { + emitterRoot_ = std::unique_ptr(emitter); + } + + std::unique_ptr emitterRoot_; +}; + +class YamlScalar : public YamlOutput +{ +public: + ~YamlScalar() = default; + + void operator=(std::string_view scalar); + +private: + friend class YamlOutput; + + YamlScalar(YamlEmitter *emitter); +}; + +class YamlList : public YamlOutput +{ +public: + YamlList(YamlList &&other) = default; + ~YamlList(); + + std::unique_ptr list(); + std::unique_ptr dict(); + void scalar(std::string_view scalar); + +private: + friend class YamlOutput; + + YamlList(YamlEmitter *emitter); +}; + +class YamlDict : public YamlOutput +{ +public: + YamlDict(YamlDict &&other) = default; + ~YamlDict(); + + std::unique_ptr list(std::string_view key); + std::unique_ptr dict(std::string_view key); + + YamlScalar operator[](std::string_view key); + +private: + friend class YamlOutput; + + YamlDict(YamlEmitter *emitter); +}; + +} /* namespace libcamera */ diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build index aa9ab0291854..5b8af4103085 100644 --- a/src/libcamera/meson.build +++ b/src/libcamera/meson.build @@ -51,6 +51,7 @@ libcamera_internal_sources = files([ 'v4l2_pixelformat.cpp', 'v4l2_subdevice.cpp', 'v4l2_videodevice.cpp', + 'yaml_emitter.cpp', 'yaml_parser.cpp', ]) diff --git a/src/libcamera/yaml_emitter.cpp b/src/libcamera/yaml_emitter.cpp new file mode 100644 index 000000000000..f2aaa1c1c1a6 --- /dev/null +++ b/src/libcamera/yaml_emitter.cpp @@ -0,0 +1,362 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2024, Ideas On Board Oy + * + * libcamera YAML emitter helper + */ + +#include "libcamera/internal/yaml_emitter.h" + +#include + +/** + * \file yaml_emitter.h + * \brief A YAML emitter helper + * + * The YAML Emitter helpers allows users to emit output in YAML format. + * + * To emit YAML users of this classes should create a root node with + * + * \code + std::string filePath("..."); + auto root = YamlEmitter::root(filePath); + \endcode + * + * A YamlRoot implements RAII-style handling of YAML sequence and document + * events handling. Creating a YamlRoot emits the STREAM_START and DOC_START + * events. Once a YamlRoot gets deleted the DOC_SEND and STREAM_END events + * gets automatically deleted. + * + * Once a root node has been created it is possible to populate it with + * scalars, list or dictionaries. + * + * YamlList and YamlDict can only be created wrapped in unique_ptr<> instances, + * to implement a RAII-style handling of YAML of lists and dictionaries. + * Creating a YamlList and a YamlDict emits the YAML sequence and mapping start + * events. Once an instance gets deleted, the sequence and mapping gets + * automatically emitted. + * + * A YamlScalar is a simpler object that can be assigned to different types + * to emit them as strings. + */ + +namespace libcamera { + +LOG_DEFINE_CATEGORY(YamlEmitter) + +namespace { + +int yamlWrite(void *data, unsigned char *buffer, size_t size) +{ + File *file = static_cast(data); + + Span buf{ buffer, size }; + ssize_t ret = file->write(buf); + if (ret < 0) { + ret = errno; + LOG(YamlEmitter, Error) << "Write error : " << strerror(ret); + return 0; + } + + return 1; +} + +} /* namespace */ + +/** + * \class YamlEmitter + * + * Yaml Emitter entry point. Allows to create a YamlRoot object that users + * can populate. + */ + +YamlEmitter::~YamlEmitter() +{ + yaml_event_delete(&event_); +} + +/** + * \brief Create a YamlRoot that applications can start populating with YamlOutput + * \param[in] path The YAML output file path + * \return A unique pointer to a YamlRoot + */ +std::unique_ptr YamlEmitter::root(std::string_view path) +{ + YamlEmitter *emitter = new YamlEmitter(); + + std::string filePath(path); + emitter->file_ = std::make_unique(filePath); + emitter->file_->open(File::OpenModeFlag::WriteOnly); + + emitter->init(); + + YamlRoot *root = new YamlRoot(emitter); + return std::unique_ptr(root); +} + +/** + * \brief Emit a yaml event + */ +int YamlEmitter::emit() +{ + return emitter_.emit(&event_); +} + +void YamlEmitter::init() +{ + emitter_.init(file_.get()); + + yaml_stream_start_event_initialize(&event_, YAML_UTF8_ENCODING); + emitter_.emit(&event_); + + yaml_document_start_event_initialize(&event_, NULL, NULL, NULL, 0); + emitter_.emit(&event_); +} + +/** + * \class YamlEmitter::Emitter + * + * yaml_emitter_t wrapper. Initialize the yaml_emitter_t on creation allows + * YamlOutput classes to emit events. + */ + +YamlEmitter::Emitter::~Emitter() +{ + yaml_emitter_delete(&emitter_); +} + +void YamlEmitter::Emitter::init(File *file) +{ + yaml_emitter_initialize(&emitter_); + yaml_emitter_set_output(&emitter_, yamlWrite, file); +} + +void YamlEmitter::Emitter::logError() +{ + switch (emitter_.error) { + case YAML_MEMORY_ERROR: + LOG(YamlEmitter, Error) + << "Memory error: Not enough memory for emitting"; + break; + + case YAML_WRITER_ERROR: + LOG(YamlEmitter, Error) + << "Writer error: " << emitter_.problem; + break; + + case YAML_EMITTER_ERROR: + LOG(YamlEmitter, Error) + << "Emitter error: " << emitter_.problem; + break; + + default: + LOG(YamlEmitter, Error) << "Internal problem"; + break; + } +} + +int YamlEmitter::Emitter::emit(yaml_event_t *event) +{ + int ret = yaml_emitter_emit(&emitter_, event); + if (!ret) { + logError(); + return -EINVAL; + } + + return 0; +} + +/** + * \class YamlOutput + * + * The YamlOutput base class. From this class YamlScalar, YamlList and YamlDict + * are derived. + * + * The YamlOutput class provides functions to create a scalar, a list or a + * dictionary. + * + * The class cannot be instantiated directly by applications. + */ + +YamlScalar YamlOutput::scalar() +{ + return YamlScalar(emitter_); +} + +std::unique_ptr YamlOutput::dict() +{ + YamlDict dict(emitter_); + + return std::make_unique(std::move(dict)); +} + +std::unique_ptr YamlOutput::list() +{ + YamlList list(emitter_); + + return std::make_unique(std::move(list)); +} + +int YamlOutput::emitScalar(std::string_view scalar) +{ + const unsigned char *value = reinterpret_cast + (scalar.data()); + yaml_scalar_event_initialize(emitter_->event(), NULL, NULL, value, + scalar.length(), true, false, + YAML_PLAIN_SCALAR_STYLE); + return emitter_->emit(); +} + +int YamlOutput::emitMappingStart() +{ + yaml_mapping_start_event_initialize(emitter_->event(), NULL, NULL, + true, YAML_BLOCK_MAPPING_STYLE); + return emitter_->emit(); +} + +int YamlOutput::emitMappingEnd() +{ + yaml_mapping_end_event_initialize(emitter_->event()); + return emitter_->emit(); +} + +int YamlOutput::emitSequenceStart() +{ + yaml_sequence_start_event_initialize(emitter_->event(), NULL, NULL, true, + YAML_BLOCK_SEQUENCE_STYLE); + return emitter_->emit(); +} + +int YamlOutput::emitSequenceEnd() +{ + yaml_sequence_end_event_initialize(emitter_->event()); + return emitter_->emit(); +} + +/** + * \class YamlRoot + * + * Yaml root node. A root node can be populated with a scalar, a list or a dict. + */ + +YamlRoot::~YamlRoot() +{ + yaml_document_end_event_initialize(emitter_->event(), 0); + emitterRoot_->emit(); + + yaml_stream_end_event_initialize(emitter_->event()); + emitterRoot_->emit(); +} + +std::unique_ptr YamlRoot::dict() +{ + emitMappingStart(); + + return YamlOutput::dict(); +} + +std::unique_ptr YamlRoot::list() +{ + emitSequenceStart(); + + return YamlOutput::list(); +} + +/** + * \class YamlScalar + * + * A YamlScalar can be assigned to an std::string_view and other libcamera + * types to emit them as YAML scalars. + */ + +YamlScalar::YamlScalar(YamlEmitter *emitter) + : YamlOutput(emitter) +{ +} + +void YamlScalar::operator=(std::string_view scalar) +{ + emitScalar(scalar); +} + +/** + * \class YamlList + * + * A YamlList can be populated with scalar and allows to create other lists + * and dictionaries. + */ + +YamlList::YamlList(YamlEmitter *emitter) + : YamlOutput(emitter) +{ +} + +YamlList::~YamlList() +{ + if (emitter_) + emitSequenceEnd(); +} + +void YamlList::scalar(std::string_view scalar) +{ + emitScalar(scalar); +} + +std::unique_ptr YamlList::list() +{ + emitSequenceStart(); + + return YamlOutput::list(); +} + +std::unique_ptr YamlList::dict() +{ + emitMappingStart(); + + return YamlOutput::dict(); +} + +/** + * \class YamlDict + * + * A YamlDict derives an unordered_map that maps strings to YAML scalar. + * + * A YamlDict can be populated with scalars using operator[] and allows to + * create other lists and dictionaries. + */ + +YamlDict::YamlDict(YamlEmitter *emitter) + : YamlOutput(emitter) +{ +} + +YamlDict::~YamlDict() +{ + if (emitter_) + emitMappingEnd(); +} + +std::unique_ptr YamlDict::list(std::string_view key) +{ + emitScalar(key); + emitSequenceStart(); + + return YamlOutput::list(); +} + +std::unique_ptr YamlDict::dict(std::string_view key) +{ + emitScalar(key); + emitMappingStart(); + + return YamlOutput::dict(); +} + +YamlScalar YamlDict::operator[](std::string_view key) +{ + emitScalar(key); + + return YamlOutput::scalar(); +} + +} /* namespace libcamera */ From patchwork Thu Oct 17 12:52:19 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jacopo Mondi X-Patchwork-Id: 21658 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 62A4CC326C for ; Thu, 17 Oct 2024 12:52:43 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 53B9D6538A; Thu, 17 Oct 2024 14:52:42 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="SrlWD+nU"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 9C3786538C for ; Thu, 17 Oct 2024 14:52:34 +0200 (CEST) Received: from ideasonboard.com (unknown [5.77.64.27]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id F062C6DC; Thu, 17 Oct 2024 14:50:50 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1729169451; bh=p0lwCGrwbm63cvKfCrEcu3tKK5/Lq8nEjhETfACwRfg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=SrlWD+nUoR1Hg4YiqTfJOXkOG0ksOO4r+yGYCZdI9NZ+bFvSl0ZVwaCXZEc9AaKX9 Jv1BMs0c5GsoEJSJSwaFsD5rG7452h5Km+p5S+nUv43IoENdSF8hmj88oK/KXPfFPF vZPMcnZRp8fKt4RcZ4nc2jLI6BdaWQKLYbLnlk+c= From: Jacopo Mondi To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [RFC v2 4/4] libcamera: pipeline_handler: Use YamlEmitter Date: Thu, 17 Oct 2024 14:52:19 +0200 Message-ID: <20241017125220.60567-5-jacopo.mondi@ideasonboard.com> X-Mailer: git-send-email 2.47.0 In-Reply-To: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> References: <20241017125220.60567-1-jacopo.mondi@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Replace raw output usage with YamlEmitter. Signed-off-by: Jacopo Mondi --- include/libcamera/internal/pipeline_handler.h | 11 +- src/libcamera/pipeline_handler.cpp | 102 +++++++++--------- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h index fb3914185a01..a10d7b1fab8c 100644 --- a/include/libcamera/internal/pipeline_handler.h +++ b/include/libcamera/internal/pipeline_handler.h @@ -19,6 +19,8 @@ #include #include +#include "libcamera/internal/yaml_emitter.h" + namespace libcamera { enum class Orientation; @@ -110,8 +112,13 @@ private: const char *name_; unsigned int useCount_; - std::ostream *dumpCaptureScript_; - std::ostream *dumpMetadata_; + std::unique_ptr controlsEmitter_; + std::unique_ptr controlsDict_; + std::unique_ptr controlsList_; + + std::unique_ptr metadataEmitter_; + std::unique_ptr metadataDict_; + std::unique_ptr metadataList_; friend class PipelineHandlerFactoryBase; }; diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp index 7002b4323bdd..533d0f8fc132 100644 --- a/src/libcamera/pipeline_handler.cpp +++ b/src/libcamera/pipeline_handler.cpp @@ -69,36 +69,31 @@ LOG_DEFINE_CATEGORY(Pipeline) * through the PipelineHandlerFactoryBase::create() function. */ PipelineHandler::PipelineHandler(CameraManager *manager) - : manager_(manager), useCount_(0), - dumpCaptureScript_(nullptr), dumpMetadata_(nullptr) + : manager_(manager), useCount_(0) { /* TODO Print notification that we're dumping capture script */ const char *file = utils::secure_getenv("LIBCAMERA_DUMP_CAPTURE_SCRIPT"); if (!file) return; - dumpCaptureScript_ = new std::ofstream(file); + std::string filePath(file); + controlsEmitter_ = YamlEmitter::root(filePath); + controlsDict_ = controlsEmitter_->dict(); /* * Metadata needs to go into a separate file because otherwise it'll * flood the capture script */ - dumpMetadata_ = new std::ofstream(std::string(file) + ".metadata"); - std::string str = "frames:\n"; - dumpMetadata_->write(str.c_str(), str.size()); - dumpMetadata_->flush(); + std::string metadataFilePath = filePath + ".metadata"; + metadataEmitter_ = YamlEmitter::root(metadataFilePath); + metadataDict_ = metadataEmitter_->dict(); + metadataList_ = metadataDict_->list("frames"); } PipelineHandler::~PipelineHandler() { for (std::shared_ptr media : mediaDevices_) media->release(); - - if (dumpCaptureScript_) - delete dumpCaptureScript_; - - if (dumpMetadata_) - delete dumpMetadata_; } /** @@ -788,65 +783,72 @@ void PipelineHandler::disconnect() void PipelineHandler::dumpConfiguration(const std::set &streams, const Orientation &orientation) { - if (!dumpCaptureScript_) + if (!controlsEmitter_) return; - std::stringstream ss; - ss << "configuration:" << std::endl; - ss << " orientation: " << orientation << std::endl; + auto configurationDict = controlsDict_->dict("configuration"); + + std::stringstream o; + o << orientation; + (*configurationDict)["orientation"] = o.str(); /* TODO Dump Sensor configuration */ - ss << " streams:" << std::endl; + auto streamsList = configurationDict->list("streams"); + for (const auto &stream : streams) { const StreamConfiguration &streamConfig = stream->configuration(); - ss << " - pixelFormat: " << streamConfig.pixelFormat << std::endl; - ss << " size: " << streamConfig.size << std::endl; - ss << " stride: " << streamConfig.stride << std::endl; - ss << " frameSize: " << streamConfig.frameSize << std::endl; - ss << " bufferCount: " << streamConfig.bufferCount << std::endl; - if (streamConfig.colorSpace) - ss << " colorSpace: " << streamConfig.colorSpace->toString() << std::endl; - } + auto yamlStream = streamsList->dict(); - dumpCaptureScript_->write(ss.str().c_str(), ss.str().size()); + (*yamlStream)["pixelformat"] = streamConfig.pixelFormat.toString(); + (*yamlStream)["size"] = streamConfig.size.toString(); + (*yamlStream)["stride"] = std::to_string(streamConfig.stride); + (*yamlStream)["frameSize"] = std::to_string(streamConfig.frameSize); + (*yamlStream)["bufferCount"] = std::to_string(streamConfig.bufferCount); - std::string str = "frames:\n"; - dumpCaptureScript_->write(str.c_str(), str.size()); - dumpCaptureScript_->flush(); + if (streamConfig.colorSpace) + (*yamlStream)["colorSpace"] = + streamConfig.colorSpace->toString(); + } } void PipelineHandler::dumpRequest(Request *request, DumpMode mode) { - ControlList &controls = - mode == DumpMode::Controls ? request->controls() - : request->metadata(); - std::ostream *output = - mode == DumpMode::Controls ? dumpCaptureScript_ - : dumpMetadata_; - - if (!output || controls.empty()) + if (!controlsEmitter_) return; - std::stringstream ss; + ControlList &controls = mode == DumpMode::Controls ? request->controls() + : request->metadata(); + if (controls.empty()) + return; + + std::unique_ptr yamlFrame; + if (mode == DumpMode::Controls) { + if (!controlsEmitter_) + return; + + if (!controlsList_) + controlsList_ = controlsDict_->list("frames"); + + yamlFrame = controlsList_->dict(); + } else { + if (!metadataEmitter_) + return; + + yamlFrame = metadataList_->dict(); + } + + auto yamlCtrls = yamlFrame->dict(std::to_string(request->sequence())); + /* TODO Figure out PFC */ - ss << " - " << request->sequence() << ":" << std::endl; const ControlIdMap *idMap = controls.idMap(); for (const auto &pair : controls) { const ControlId *ctrlId = idMap->at(pair.first); + /* TODO Prettify enums (probably by upgrading ControlValue::toString()) */ - ss << " " << ctrlId->name() << ": " << pair.second.toString() << std::endl; + (*yamlCtrls)[ctrlId->name()] = pair.second.toString(); } - - /* - * TODO Investigate the overhead of flushing this frequently - * Controls aren't going to be queued too frequently so it should be - * fine to dump controls every frame. Metadata on the other hand needs - * to be investigated. - */ - output->write(ss.str().c_str(), ss.str().size()); - output->flush(); } /**