From patchwork Mon May 11 20:23:48 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Max Bretschneider X-Patchwork-Id: 26722 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 50DEBBDCBD for ; Mon, 11 May 2026 20:23:55 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id F1B866302A; Mon, 11 May 2026 22:23:54 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=protonmail.com header.i=@protonmail.com header.b="EA/tfYFa"; dkim-atps=neutral Received: from mail-24417.protonmail.ch (mail-24417.protonmail.ch [109.224.244.17]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 28B3963020 for ; Mon, 11 May 2026 22:23:53 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonmail.com; s=protonmail3; t=1778531031; x=1778790231; bh=rGG8gQYP/tCPtHEagfHZLlJ2ekYYmlM6YBiRJIyo9ww=; 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=EA/tfYFaGIKS7a+A+9mX1gWsiN/KJAltS66JBdryIZXnPv0oAiqXkx1cguEN/tEtL q/Dow0eGduBg1oYQ6m0bAllO0jnLmDJ1kKY7m2mp8wbMj9ifUcxQBg7m8zCh+74+/r jGE79WoD8Xxd4atjBSUSi+AAcWAHNirCyZA15gknt+sYIWQpL6mHky1HJIPtsfaA5i AkWONaBXD0PsUr8qkhNozH+GjWySdwiGmOXpokcWwGgzr+NhbGIe64XwJAEizNftQG MvOa1oRCpRXJsnKyueWe6s8PmDFFgSgChaa0b4/90jkOcfe5ukKDZ+VKhFuxF/gExU ZSOzR/OJvNkWQ== Date: Mon, 11 May 2026 20:23:48 +0000 To: libcamera-devel@lists.libcamera.org From: Max Bretschneider Cc: Max Bretschneider Subject: [RFC PATCH v3 2/2] libcamera: pipeline: virtual: Add raw_frames config and Bayer format support Message-ID: <20260511202327.40191-3-maxbretschneider@protonmail.com> In-Reply-To: <20260511202327.40191-1-maxbretschneider@protonmail.com> References: <20260501105137.439519-1-maxbretschneider@protonmail.com> <20260504091623.3354474-1-maxbretschneider@protonmail.com> <20260511202327.40191-1-maxbretschneider@protonmail.com> Feedback-ID: 122687743:user:proton X-Pm-Message-ID: 42b4aab9f27bf004d0f2731d2526cda323c4247a 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" 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. Signed-off-by: Max Bretschneider --- .../pipeline/virtual/config_parser.cpp | 156 ++++++++++++++---- src/libcamera/pipeline/virtual/virtual.cpp | 99 +++++++++-- src/libcamera/pipeline/virtual/virtual.h | 3 +- 3 files changed, 210 insertions(+), 48 deletions(-) diff --git a/src/libcamera/pipeline/virtual/config_parser.cpp b/src/libcamera/pipeline/virtual/config_parser.cpp index 5169fd39..12249c37 100644 --- a/src/libcamera/pipeline/virtual/config_parser.cpp +++ b/src/libcamera/pipeline/virtual/config_parser.cpp @@ -7,6 +7,7 @@ #include "config_parser.h" +#include #include #include @@ -24,6 +25,38 @@ namespace libcamera { LOG_DECLARE_CATEGORY(Virtual) +namespace { + +std::optional> collectFiles(const std::string &path) +{ + 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()) + return std::nullopt; + break; + + default: + return std::nullopt; + } + return files; +} + +} /* namespace */ + std::vector> ConfigParser::parseConfigFile(File &file, PipelineHandler *pipe) { @@ -156,13 +189,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 +218,75 @@ 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; + } + + auto type = std::filesystem::symlink_status(*path).type(); + if (type != std::filesystem::file_type::regular && + type != std::filesystem::file_type::directory) { + LOG(Virtual, Error) << "raw_frames path: " << *path << " is not supported"; + return -EINVAL; + } + + auto files = collectFiles(*path); + + if (!files) { + LOG(Virtual, Error) << "raw_frames directory has no files: " << *path; + 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 */ @@ -198,35 +307,20 @@ int ConfigParser::parseFrameGenerator(const ValueNode &cameraConfigData, Virtual 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) << "Directory has no files: " << *path; - return -EINVAL; - } - break; - - default: + auto type = std::filesystem::symlink_status(*path).type(); + if (type != std::filesystem::file_type::regular && + type != std::filesystem::file_type::directory) { LOG(Virtual, Error) << "Frame: " << *path << " is not supported"; return -EINVAL; } - data->config_.frame = ImageFrames{ std::move(files) }; + auto files = collectFiles(*path); + if (!files) { + LOG(Virtual, Error) << "Directory has no files: " << *path; + return -EINVAL; + } + + data->config_.frame = ImageFrames{ std::move(*files) }; return 0; } diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp index 81d2ddda..4ef72c2c 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 @@ -31,13 +32,13 @@ #include #include +#include "libcamera/internal/bayer_format.h" #include "libcamera/internal/camera.h" #include "libcamera/internal/dma_buf_allocator.h" #include "libcamera/internal/formats.h" #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 +203,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 +275,54 @@ PipelineHandlerVirtual::generateConfiguration(Camera *camera, case StreamRole::Viewfinder: break; - case StreamRole::Raw: + case StreamRole::Raw: { + const auto *rawFrames = std::get_if(&data->config_.frame); + if (!rawFrames) { + LOG(Virtual, Error) + << "StreamRole::Raw requested but camera is not configured with raw_frames"; + return {}; + } + + static const std::map cfaToOrder = { + { properties::draft::ColorFilterArrangementEnum::RGGB, BayerFormat::RGGB }, + { properties::draft::ColorFilterArrangementEnum::BGGR, BayerFormat::BGGR }, + { properties::draft::ColorFilterArrangementEnum::GRBG, BayerFormat::GRBG }, + { properties::draft::ColorFilterArrangementEnum::GBRG, BayerFormat::GBRG }, + }; + + auto it = cfaToOrder.find(rawFrames->cfaPattern); + if (it == cfaToOrder.end()) { + LOG(Virtual, Error) << "Unsupported CFA pattern"; + return {}; + } + + PixelFormat rawFormat = + BayerFormat(it->second, rawFrames->bitDepth, + BayerFormat::Packing::None) + .toPixelFormat(); + + if (!rawFormat.isValid()) { + LOG(Virtual, Error) << "Could not find PixelFormat for Bayer format"; + return {}; + } + + /* + * 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 +456,13 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator std::set streams; for (auto &streamConfig : data->streamConfigs_) streams.insert(&streamConfig.stream); + + if (const auto *rawFrames = std::get_if(&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)); + } + std::string id = data->config_.id; std::shared_ptr camera = Camera::create(std::move(data), id, streams); @@ -434,7 +496,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,