From patchwork Mon May 4 09:17:03 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Max Bretschneider X-Patchwork-Id: 26612 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 E5D37BDCB5 for ; Mon, 4 May 2026 09:17:09 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 81B6663029; Mon, 4 May 2026 11:17:09 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=protonmail.com header.i=@protonmail.com header.b="m3n7YMpe"; dkim-atps=neutral Received: from mail-24428.protonmail.ch (mail-24428.protonmail.ch [109.224.244.28]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 0D74D63022 for ; Mon, 4 May 2026 11:17:08 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonmail.com; s=protonmail3; t=1777886227; x=1778145427; bh=5iKfOQb8SFqhcw16zuUTFtndQ7JYG8ZRG9XssaI3QG0=; h=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References: Feedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID: Message-ID:BIMI-Selector; b=m3n7YMpei7hyX00by5S32LIcPgWeDlhu66YftMmouQn9ODHEDW8j+sHMkXLf9u4Og x9roJidJG6x6FGVEgj23VCqbZ8iM5P1bizCVg7YRXZSk8sZywjxADE0VM823Z3ErRQ /vS2q5zF55EcxBXOeWf1enEu97vFJ+sxyPLRtc1y7tpH1P0/yUYfgrR+SHzRoBd85Q fBoJi04ZVyRpAzQzOoWu0ddT2Ra7NdpFS7J/NSdmxI7v62oPvxKSue9OXGPSB1QGad Q5BRA4dD5AwaqZneGZeeF99W1w5jx5Gqt9vKYO9XU+t0UtirP/5SfWLdQiUg4qPE1o y6qsFP8SPyq1g== Date: Mon, 04 May 2026 09:17:03 +0000 To: libcamera-devel@lists.libcamera.org From: maxbretschneider@protonmail.com Cc: Max Bretschneider Subject: [PATCH 2/2] libcamera: pipeline: virtual: Add raw_frames config and Bayer format support Message-ID: <20260504091623.3354474-3-maxbretschneider@protonmail.com> In-Reply-To: <20260504091623.3354474-1-maxbretschneider@protonmail.com> References: <20260501105137.439519-1-maxbretschneider@protonmail.com> <20260504091623.3354474-1-maxbretschneider@protonmail.com> Feedback-ID: 122687743:user:proton X-Pm-Message-ID: afcca11e86c8eda66757d26c4370419c28b2d852 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: Max Bretschneider Extend parseFrameGenerator() to handle the new raw_frames YAML key alongside the existing test_pattern and frames keys. File collection logic (e.g. either single file or directory with natural sorting) follows the existing frames path handling. This raw_frames block accepts: - path (single file or directory) - bayer_order (RGGB, BGGR, GRBG, GBRG) - bit_depth (8, 10, 12, 14, 16), in accordance to libcamera/formats.yaml Extend generateConfiguration() to return a Bayer StreamConfiguration when the StreamRole::Raw is requested on a camera configured with raw_frames. Extend validate() to allow Bayer formats, also set the correct stride and frameSize for single plane Bayer buffers. Sensor properties required for SoftISP integration are also declared in match(). Signed-off-by: Max Bretschneider --- .../pipeline/virtual/config_parser.cpp | 100 +++++++++++++- src/libcamera/pipeline/virtual/virtual.cpp | 123 +++++++++++++++--- src/libcamera/pipeline/virtual/virtual.h | 3 +- 3 files changed, 203 insertions(+), 23 deletions(-) diff --git a/src/libcamera/pipeline/virtual/config_parser.cpp b/src/libcamera/pipeline/virtual/config_parser.cpp index 5169fd39..fcce70c8 100644 --- a/src/libcamera/pipeline/virtual/config_parser.cpp +++ b/src/libcamera/pipeline/virtual/config_parser.cpp @@ -156,13 +156,20 @@ int ConfigParser::parseFrameGenerator(const ValueNode &cameraConfigData, Virtual { const std::string testPatternKey = "test_pattern"; const std::string framesKey = "frames"; - if (cameraConfigData.contains(testPatternKey)) { - if (cameraConfigData.contains(framesKey)) { - LOG(Virtual, Error) << "A camera should use either " - << testPatternKey << " or " << framesKey; - return -EINVAL; - } + const std::string rawFramesKey = "raw_frames"; + + /* Ensure only one frame source is specified */ + int sourcesSpecified = cameraConfigData.contains(testPatternKey) + + cameraConfigData.contains(framesKey) + + cameraConfigData.contains(rawFramesKey); + if (sourcesSpecified > 1) { + LOG(Virtual, Error) << "A camera should use only one of " + << testPatternKey << ", " << framesKey + << ", or " << rawFramesKey; + return -EINVAL; + } + if (cameraConfigData.contains(testPatternKey)) { auto testPattern = cameraConfigData[testPatternKey].get(""); if (testPattern == "bars") { @@ -178,6 +185,87 @@ int ConfigParser::parseFrameGenerator(const ValueNode &cameraConfigData, Virtual return 0; } + if (cameraConfigData.contains(rawFramesKey)) { + const ValueNode &rawFrames = cameraConfigData[rawFramesKey]; + + if (!rawFrames.isDictionary()) { + LOG(Virtual, Error) << "'raw_frames' is not a dictionary."; + return -EINVAL; + } + + auto path = rawFrames["path"].get(); + if (!path) { + LOG(Virtual, Error) << "raw_frames: path must be specified."; + return -EINVAL; + } + + std::vector files; + + switch (std::filesystem::symlink_status(*path).type()) { + case std::filesystem::file_type::regular: + files.push_back(*path); + break; + + case std::filesystem::file_type::directory: + for (const auto &dentry : std::filesystem::directory_iterator{ *path }) + if (dentry.is_regular_file()) + files.push_back(dentry.path()); + + std::sort(files.begin(), files.end(), [](const auto &a, const auto &b) { + return ::strverscmp(a.c_str(), b.c_str()) < 0; + }); + + if (files.empty()) { + LOG(Virtual, Error) << "raw_frames directory has no files: " << *path; + return -EINVAL; + } + break; + default: + LOG(Virtual, Error) << "raw_frames path: " << *path << " is not supported"; + return -EINVAL; + } + + /* Parse bayer_order */ + auto bayerOrder = rawFrames["bayer_order"].get(); + if (!bayerOrder) { + LOG(Virtual, Error) << "raw_frames: bayer_order must be specified."; + return -EINVAL; + } + + static const std::map bayerOrderMap = { + { "RGGB", properties::draft::ColorFilterArrangementEnum::RGGB }, + { "BGGR", properties::draft::ColorFilterArrangementEnum::BGGR }, + { "GRBG", properties::draft::ColorFilterArrangementEnum::GRBG }, + { "GBRG", properties::draft::ColorFilterArrangementEnum::GBRG }, + }; + + auto it = bayerOrderMap.find(*bayerOrder); + if (it == bayerOrderMap.end()) { + LOG(Virtual, Error) << "raw_frames: unsupported bayer_order: " + << *bayerOrder + << ", must be one of RGGB, BGGR, GRBG, GBRG"; + return -EINVAL; + } + + /* Parse bit_depth */ + auto bitDepth = rawFrames["bit_depth"].get(); + if (!bitDepth) { + LOG(Virtual, Error) << "raw_frames: bit_depth must be specified."; + return -EINVAL; + } + + static const std::set supportedBitDepths = { 8, 10, 12, 14, 16 }; + if (supportedBitDepths.find(*bitDepth) == supportedBitDepths.end()) { + LOG(Virtual, Error) << "raw_frames: bit_depth unsupported: " << *bitDepth + << ", must be one of 8, 10, 12, 14, 16"; + return -EINVAL; + } + + data->config_.frame = RawFrames{ std::move(files), it->second, *bitDepth }; + + return 0; + } + const ValueNode &frames = cameraConfigData[framesKey]; /* When there is no frames provided in the config file, use color bar test pattern */ diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp index 81d2ddda..beec2f5a 100644 --- a/src/libcamera/pipeline/virtual/virtual.cpp +++ b/src/libcamera/pipeline/virtual/virtual.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -37,7 +38,6 @@ #include "libcamera/internal/framebuffer.h" #include "libcamera/internal/pipeline_handler.h" #include "libcamera/internal/request.h" -#include "libcamera/internal/value_node.h" #include "pipeline/virtual/config_parser.h" @@ -202,21 +202,28 @@ CameraConfiguration::Status VirtualCameraConfiguration::validate() adjusted = true; } - if (cfg.pixelFormat != formats::NV12) { - cfg.pixelFormat = formats::NV12; - status = Adjusted; - adjusted = true; - } + const PixelFormatInfo &fmtInfo = PixelFormatInfo::info(cfg.pixelFormat); + const bool rawStream = fmtInfo.colourEncoding == PixelFormatInfo::ColourEncodingRAW; - if (cfg.colorSpace != ColorSpace::Rec709) { - cfg.colorSpace = ColorSpace::Rec709; - status = Adjusted; - adjusted = true; - } + if (!rawStream) { + if (cfg.pixelFormat != formats::NV12) { + cfg.pixelFormat = formats::NV12; + status = Adjusted; + adjusted = true; + } - if (validateColorSpaces() == Adjusted) { - status = Adjusted; - adjusted = true; + if (cfg.colorSpace != ColorSpace::Rec709) { + cfg.colorSpace = ColorSpace::Rec709; + status = Adjusted; + adjusted = true; + } + + if (validateColorSpaces() == Adjusted) { + status = Adjusted; + adjusted = true; + } + } else { + cfg.colorSpace = ColorSpace::Raw; } if (adjusted) @@ -267,7 +274,64 @@ PipelineHandlerVirtual::generateConfiguration(Camera *camera, case StreamRole::Viewfinder: break; - case StreamRole::Raw: + case StreamRole::Raw: { + if (!std::holds_alternative(data->config_.frame)) { + LOG(Virtual, Error) + << "StreamRole::Raw requested but camera is not configured with raw_frames"; + return {}; + } + + const auto &rawFrames = std::get(data->config_.frame); + PixelFormat rawFormat; + + /* + * \todo Possibly replace with a 2D lookup table to + * be able to just index by (cfaPatter, bitDepth), + * might be cleaner. + */ + auto bayerFormat = [&](PixelFormat f8, PixelFormat f10, + PixelFormat f12, PixelFormat f14, + PixelFormat f16) { + switch (rawFrames.bitDepth) { + case 8: + return f8; + case 10: + return f10; + case 12: + return f12; + case 14: + return f14; + default: + return f16; + } + }; + + /* Map bayer order and bit depth to pixel format */ + if (rawFrames.cfaPattern == properties::draft::ColorFilterArrangementEnum::RGGB) + rawFormat = bayerFormat(formats::SRGGB8, formats::SRGGB10, formats::SRGGB12, formats::SRGGB14, formats::SRGGB16); + else if (rawFrames.cfaPattern == properties::draft::ColorFilterArrangementEnum::BGGR) + rawFormat = bayerFormat(formats::SBGGR8, formats::SBGGR10, formats::SBGGR12, formats::SBGGR14, formats::SBGGR16); + else if (rawFrames.cfaPattern == properties::draft::ColorFilterArrangementEnum::GRBG) + rawFormat = bayerFormat(formats::SGRBG8, formats::SGRBG10, formats::SGRBG12, formats::SGRBG14, formats::SGRBG16); + else + rawFormat = bayerFormat(formats::SGBRG8, formats::SGBRG10, formats::SGBRG12, formats::SGBRG14, formats::SGBRG16); + + /* + * Use the Bayer format matching the raw frame + * configuration. + */ + std::map> rawStreamFormats; + rawStreamFormats[rawFormat] = { { data->config_.minResolutionSize, data->config_.maxResolutionSize } }; + StreamFormats rawFormats(rawStreamFormats); + StreamConfiguration rawCfg(rawFormats); + rawCfg.pixelFormat = rawFormat; + rawCfg.size = data->config_.maxResolutionSize; + rawCfg.bufferCount = VirtualCameraConfiguration::kBufferCount; + rawCfg.colorSpace = ColorSpace::Raw; + config->addConfiguration(rawCfg); + continue; + } + default: LOG(Virtual, Error) << "Requested stream role not supported: " << role; @@ -401,6 +465,28 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator std::set streams; for (auto &streamConfig : data->streamConfigs_) streams.insert(&streamConfig.stream); + + /* Add sensor properties and controls required by SoftISP for raw streams */ + if (std::holds_alternative(data->config_.frame)) { + const auto &rawFrames = std::get(data->config_.frame); + data->properties_.set(properties::draft::ColorFilterArrangement, static_cast(rawFrames.cfaPattern)); + data->properties_.set(properties::PixelArraySize, data->config_.maxResolutionSize); + data->properties_.set(properties::UnitCellSize, Size(1000, 1000)); + + /* Extends existing controlInfo_ with SoftISP required controls */ + ControlInfoMap::Map controls; + for (const auto &[id, info] : data->controlInfo_) + controls[id] = info; + + /* \todo Allow configuration via YAML */ + controls[&controls::AnalogueGain] = ControlInfo(1.0f, 16.0f, 1.0f); + controls[&controls::ExposureTime] = ControlInfo(100, 33333, 10000); + controls[&controls::AeEnable] = ControlInfo(false, true, true); + controls[&controls::AwbEnable] = ControlInfo(false, true, true); + + data->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls); + } + std::string id = data->config_.id; std::shared_ptr camera = Camera::create(std::move(data), id, streams); @@ -434,7 +520,12 @@ bool PipelineHandlerVirtual::initFrameGenerator(Camera *camera) [&](ImageFrames &imageFrames) { for (auto &streamConfig : data->streamConfigs_) streamConfig.frameGenerator = ImageFrameGenerator::create(imageFrames); - } }, + }, + [&](RawFrames &rawFrames) { + for (auto &streamConfig : data->streamConfigs_) + streamConfig.frameGenerator = RawFrameGenerator::create(rawFrames); + }, + }, frame); for (auto &streamConfig : data->streamConfigs_) diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h index 215e56fa..48392808 100644 --- a/src/libcamera/pipeline/virtual/virtual.h +++ b/src/libcamera/pipeline/virtual/virtual.h @@ -23,11 +23,12 @@ #include "frame_generator.h" #include "image_frame_generator.h" +#include "raw_frame_generator.h" #include "test_pattern_generator.h" namespace libcamera { -using VirtualFrame = std::variant; +using VirtualFrame = std::variant; class VirtualCameraData : public Camera::Private, public Thread,