[RFC,v3,2/2] libcamera: pipeline: virtual: Add raw_frames config and Bayer format support
diff mbox series

Message ID 20260511202327.40191-3-maxbretschneider@protonmail.com
State New
Headers show
Series
  • libcamera: pipeline: virtual: Add raw Bayer frame support
Related show

Commit Message

Max Bretschneider May 11, 2026, 8:23 p.m. UTC
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 <maxbretschneider@protonmail.com>
---
 .../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(-)

Patch
diff mbox series

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 <filesystem>
 #include <string.h>
 #include <utility>
 
@@ -24,6 +25,38 @@  namespace libcamera {
 
 LOG_DECLARE_CATEGORY(Virtual)
 
+namespace {
+
+std::optional<std::vector<std::filesystem::path>> collectFiles(const std::string &path)
+{
+	std::vector<std::filesystem::path> 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<std::unique_ptr<VirtualCameraData>>
 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<std::string>("");
 
 		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<std::string>();
+		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<std::string>();
+		if (!bayerOrder) {
+			LOG(Virtual, Error) << "raw_frames: bayer_order must be specified.";
+			return -EINVAL;
+		}
+
+		static const std::map<std::string, uint32_t> 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<unsigned int>();
+		if (!bitDepth) {
+			LOG(Virtual, Error) << "raw_frames: bit_depth must be specified.";
+			return -EINVAL;
+		}
+
+		static const std::set<unsigned int> 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<std::filesystem::path> 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 <string>
 #include <time.h>
 #include <utility>
+#include <variant>
 #include <vector>
 
 #include <libcamera/base/flags.h>
@@ -31,13 +32,13 @@ 
 #include <libcamera/pixel_format.h>
 #include <libcamera/property_ids.h>
 
+#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<RawFrames>(&data->config_.frame);
+			if (!rawFrames) {
+				LOG(Virtual, Error)
+					<< "StreamRole::Raw requested but camera is not configured with raw_frames";
+				return {};
+			}
+
+			static const std::map<uint32_t, BayerFormat::Order> 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<PixelFormat, std::vector<SizeRange>> 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<Stream *> streams;
 		for (auto &streamConfig : data->streamConfigs_)
 			streams.insert(&streamConfig.stream);
+
+		if (const auto *rawFrames = std::get_if<RawFrames>(&data->config_.frame)) {
+			data->properties_.set(properties::draft::ColorFilterArrangement, static_cast<int32_t>(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 = 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<TestPattern, ImageFrames>;
+using VirtualFrame = std::variant<TestPattern, ImageFrames, RawFrames>;
 
 class VirtualCameraData : public Camera::Private,
 			  public Thread,