Patch Detail
Show a patch.
GET /api/patches/26722/?format=api
{ "id": 26722, "url": "https://patchwork.libcamera.org/api/patches/26722/?format=api", "web_url": "https://patchwork.libcamera.org/patch/26722/", "project": { "id": 1, "url": "https://patchwork.libcamera.org/api/projects/1/?format=api", "name": "libcamera", "link_name": "libcamera", "list_id": "libcamera_core", "list_email": "libcamera-devel@lists.libcamera.org", "web_url": "", "scm_url": "", "webscm_url": "" }, "msgid": "<20260511202327.40191-3-maxbretschneider@protonmail.com>", "date": "2026-05-11T20:23:48", "name": "[RFC,v3,2/2] libcamera: pipeline: virtual: Add raw_frames config and Bayer format support", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "f969398d4f3e25a176c9df6d2bc8cbd90cceb116", "submitter": { "id": 266, "url": "https://patchwork.libcamera.org/api/people/266/?format=api", "name": "Max Bretschneider", "email": "maxbretschneider@protonmail.com" }, "delegate": null, "mbox": "https://patchwork.libcamera.org/patch/26722/mbox/", "series": [ { "id": 5935, "url": "https://patchwork.libcamera.org/api/series/5935/?format=api", "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=5935", "date": "2026-05-11T20:23:37", "name": "libcamera: pipeline: virtual: Add raw Bayer frame support", "version": 3, "mbox": "https://patchwork.libcamera.org/series/5935/mbox/" } ], "comments": "https://patchwork.libcamera.org/api/patches/26722/comments/", "check": "pending", "checks": "https://patchwork.libcamera.org/api/patches/26722/checks/", "tags": {}, "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 50DEBBDCBD\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 11 May 2026 20:23:55 +0000 (UTC)", "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id F1B866302A;\n\tMon, 11 May 2026 22:23:54 +0200 (CEST)", "from mail-24417.protonmail.ch (mail-24417.protonmail.ch\n\t[109.224.244.17])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 28B3963020\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 11 May 2026 22:23:53 +0200 (CEST)" ], "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=protonmail.com header.i=@protonmail.com\n\theader.b=\"EA/tfYFa\"; dkim-atps=neutral", "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=protonmail.com;\n\ts=protonmail3; t=1778531031; x=1778790231;\n\tbh=rGG8gQYP/tCPtHEagfHZLlJ2ekYYmlM6YBiRJIyo9ww=;\n\th=Date:To:From:Cc:Subject:Message-ID:In-Reply-To:References:\n\tFeedback-ID:From:To:Cc:Date:Subject:Reply-To:Feedback-ID:\n\tMessage-ID:BIMI-Selector;\n\tb=EA/tfYFaGIKS7a+A+9mX1gWsiN/KJAltS66JBdryIZXnPv0oAiqXkx1cguEN/tEtL\n\tq/Dow0eGduBg1oYQ6m0bAllO0jnLmDJ1kKY7m2mp8wbMj9ifUcxQBg7m8zCh+74+/r\n\tjGE79WoD8Xxd4atjBSUSi+AAcWAHNirCyZA15gknt+sYIWQpL6mHky1HJIPtsfaA5i\n\tAkWONaBXD0PsUr8qkhNozH+GjWySdwiGmOXpokcWwGgzr+NhbGIe64XwJAEizNftQG\n\tMvOa1oRCpRXJsnKyueWe6s8PmDFFgSgChaa0b4/90jkOcfe5ukKDZ+VKhFuxF/gExU\n\tZSOzR/OJvNkWQ==", "Date": "Mon, 11 May 2026 20:23:48 +0000", "To": "libcamera-devel@lists.libcamera.org", "From": "Max Bretschneider <maxbretschneider@protonmail.com>", "Cc": "Max Bretschneider <maxbretschneider@protonmail.com>", "Subject": "[RFC PATCH v3 2/2] libcamera: pipeline: virtual: Add raw_frames\n\tconfig 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>\n\t<20260504091623.3354474-1-maxbretschneider@protonmail.com>\n\t<20260511202327.40191-1-maxbretschneider@protonmail.com>", "Feedback-ID": "122687743:user:proton", "X-Pm-Message-ID": "42b4aab9f27bf004d0f2731d2526cda323c4247a", "MIME-Version": "1.0", "Content-Type": "text/plain; charset=utf-8", "Content-Transfer-Encoding": "quoted-printable", "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>" }, "content": "Extend parseFrameGenerator() to handle the new raw_frames YAML key\nalongside the existing test_pattern and frames keys. File collection\nlogic (e.g. either single file or directory with natural sorting)\nfollows the existing frames path handling. This raw_frames block\naccepts:\n- path (single file or directory)\n- bayer_order (RGGB, BGGR, GRBG, GBRG)\n- bit_depth (8, 10, 12, 14, 16), in accordance to libcamera/formats.yaml\n\nExtend generateConfiguration() to return a Bayer StreamConfiguration\nwhen the StreamRole::Raw is requested on a camera configured with\nraw_frames. Extend validate() to allow Bayer formats, also set the correct stride\nand frameSize for single plane Bayer buffers.\n\nSigned-off-by: Max Bretschneider <maxbretschneider@protonmail.com>\n---\n .../pipeline/virtual/config_parser.cpp | 156 ++++++++++++++----\n src/libcamera/pipeline/virtual/virtual.cpp | 99 +++++++++--\n src/libcamera/pipeline/virtual/virtual.h | 3 +-\n 3 files changed, 210 insertions(+), 48 deletions(-)", "diff": "diff --git a/src/libcamera/pipeline/virtual/config_parser.cpp b/src/libcamera/pipeline/virtual/config_parser.cpp\nindex 5169fd39..12249c37 100644\n--- a/src/libcamera/pipeline/virtual/config_parser.cpp\n+++ b/src/libcamera/pipeline/virtual/config_parser.cpp\n@@ -7,6 +7,7 @@\n \n #include \"config_parser.h\"\n \n+#include <filesystem>\n #include <string.h>\n #include <utility>\n \n@@ -24,6 +25,38 @@ namespace libcamera {\n \n LOG_DECLARE_CATEGORY(Virtual)\n \n+namespace {\n+\n+std::optional<std::vector<std::filesystem::path>> collectFiles(const std::string &path)\n+{\n+\tstd::vector<std::filesystem::path> files;\n+\n+\tswitch (std::filesystem::symlink_status(path).type()) {\n+\tcase std::filesystem::file_type::regular:\n+\t\tfiles.push_back(path);\n+\t\tbreak;\n+\n+\tcase std::filesystem::file_type::directory:\n+\t\tfor (const auto &dentry : std::filesystem::directory_iterator{ path })\n+\t\t\tif (dentry.is_regular_file())\n+\t\t\t\tfiles.push_back(dentry.path());\n+\n+\t\tstd::sort(files.begin(), files.end(), [](const auto &a, const auto &b) {\n+\t\t\treturn ::strverscmp(a.c_str(), b.c_str()) < 0;\n+\t\t});\n+\n+\t\tif (files.empty())\n+\t\t\treturn std::nullopt;\n+\t\tbreak;\n+\n+\tdefault:\n+\t\treturn std::nullopt;\n+\t}\n+\treturn files;\n+}\n+\n+} /* namespace */\n+\n std::vector<std::unique_ptr<VirtualCameraData>>\n ConfigParser::parseConfigFile(File &file, PipelineHandler *pipe)\n {\n@@ -156,13 +189,20 @@ int ConfigParser::parseFrameGenerator(const ValueNode &cameraConfigData, Virtual\n {\n \tconst std::string testPatternKey = \"test_pattern\";\n \tconst std::string framesKey = \"frames\";\n-\tif (cameraConfigData.contains(testPatternKey)) {\n-\t\tif (cameraConfigData.contains(framesKey)) {\n-\t\t\tLOG(Virtual, Error) << \"A camera should use either \"\n-\t\t\t\t\t << testPatternKey << \" or \" << framesKey;\n-\t\t\treturn -EINVAL;\n-\t\t}\n+\tconst std::string rawFramesKey = \"raw_frames\";\n+\n+\t/* Ensure only one frame source is specified */\n+\tint sourcesSpecified = cameraConfigData.contains(testPatternKey) +\n+\t\t\t cameraConfigData.contains(framesKey) +\n+\t\t\t cameraConfigData.contains(rawFramesKey);\n+\tif (sourcesSpecified > 1) {\n+\t\tLOG(Virtual, Error) << \"A camera should use only one of \"\n+\t\t\t\t << testPatternKey << \", \" << framesKey\n+\t\t\t\t << \", or \" << rawFramesKey;\n+\t\treturn -EINVAL;\n+\t}\n \n+\tif (cameraConfigData.contains(testPatternKey)) {\n \t\tauto testPattern = cameraConfigData[testPatternKey].get<std::string>(\"\");\n \n \t\tif (testPattern == \"bars\") {\n@@ -178,6 +218,75 @@ int ConfigParser::parseFrameGenerator(const ValueNode &cameraConfigData, Virtual\n \t\treturn 0;\n \t}\n \n+\tif (cameraConfigData.contains(rawFramesKey)) {\n+\t\tconst ValueNode &rawFrames = cameraConfigData[rawFramesKey];\n+\n+\t\tif (!rawFrames.isDictionary()) {\n+\t\t\tLOG(Virtual, Error) << \"'raw_frames' is not a dictionary.\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tauto path = rawFrames[\"path\"].get<std::string>();\n+\t\tif (!path) {\n+\t\t\tLOG(Virtual, Error) << \"raw_frames: path must be specified.\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tauto type = std::filesystem::symlink_status(*path).type();\n+\t\tif (type != std::filesystem::file_type::regular &&\n+\t\t type != std::filesystem::file_type::directory) {\n+\t\t\tLOG(Virtual, Error) << \"raw_frames path: \" << *path << \" is not supported\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tauto files = collectFiles(*path);\n+\n+\t\tif (!files) {\n+\t\t\tLOG(Virtual, Error) << \"raw_frames directory has no files: \" << *path;\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\t/* Parse bayer_order */\n+\t\tauto bayerOrder = rawFrames[\"bayer_order\"].get<std::string>();\n+\t\tif (!bayerOrder) {\n+\t\t\tLOG(Virtual, Error) << \"raw_frames: bayer_order must be specified.\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tstatic const std::map<std::string, uint32_t> bayerOrderMap = {\n+\t\t\t{ \"RGGB\", properties::draft::ColorFilterArrangementEnum::RGGB },\n+\t\t\t{ \"BGGR\", properties::draft::ColorFilterArrangementEnum::BGGR },\n+\t\t\t{ \"GRBG\", properties::draft::ColorFilterArrangementEnum::GRBG },\n+\t\t\t{ \"GBRG\", properties::draft::ColorFilterArrangementEnum::GBRG },\n+\t\t};\n+\n+\t\tauto it = bayerOrderMap.find(*bayerOrder);\n+\t\tif (it == bayerOrderMap.end()) {\n+\t\t\tLOG(Virtual, Error) << \"raw_frames: unsupported bayer_order: \"\n+\t\t\t\t\t << *bayerOrder\n+\t\t\t\t\t << \", must be one of RGGB, BGGR, GRBG, GBRG\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\t/* Parse bit_depth */\n+\t\tauto bitDepth = rawFrames[\"bit_depth\"].get<unsigned int>();\n+\t\tif (!bitDepth) {\n+\t\t\tLOG(Virtual, Error) << \"raw_frames: bit_depth must be specified.\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tstatic const std::set<unsigned int> supportedBitDepths = { 8, 10, 12, 14, 16 };\n+\t\tif (supportedBitDepths.find(*bitDepth) == supportedBitDepths.end()) {\n+\t\t\tLOG(Virtual, Error) << \"raw_frames: bit_depth unsupported: \" << *bitDepth\n+\t\t\t\t\t << \", must be one of 8, 10, 12, 14, 16\";\n+\t\t\treturn -EINVAL;\n+\t\t}\n+\n+\t\tdata->config_.frame = RawFrames{ std::move(*files), it->second, *bitDepth };\n+\n+\t\treturn 0;\n+\t}\n+\n \tconst ValueNode &frames = cameraConfigData[framesKey];\n \n \t/* When there is no frames provided in the config file, use color bar test pattern */\n@@ -198,35 +307,20 @@ int ConfigParser::parseFrameGenerator(const ValueNode &cameraConfigData, Virtual\n \t\treturn -EINVAL;\n \t}\n \n-\tstd::vector<std::filesystem::path> files;\n-\n-\tswitch (std::filesystem::symlink_status(*path).type()) {\n-\tcase std::filesystem::file_type::regular:\n-\t\tfiles.push_back(*path);\n-\t\tbreak;\n-\n-\tcase std::filesystem::file_type::directory:\n-\t\tfor (const auto &dentry : std::filesystem::directory_iterator{ *path }) {\n-\t\t\tif (dentry.is_regular_file())\n-\t\t\t\tfiles.push_back(dentry.path());\n-\t\t}\n-\n-\t\tstd::sort(files.begin(), files.end(), [](const auto &a, const auto &b) {\n-\t\t\treturn ::strverscmp(a.c_str(), b.c_str()) < 0;\n-\t\t});\n-\n-\t\tif (files.empty()) {\n-\t\t\tLOG(Virtual, Error) << \"Directory has no files: \" << *path;\n-\t\t\treturn -EINVAL;\n-\t\t}\n-\t\tbreak;\n-\n-\tdefault:\n+\tauto type = std::filesystem::symlink_status(*path).type();\n+\tif (type != std::filesystem::file_type::regular &&\n+\t type != std::filesystem::file_type::directory) {\n \t\tLOG(Virtual, Error) << \"Frame: \" << *path << \" is not supported\";\n \t\treturn -EINVAL;\n \t}\n \n-\tdata->config_.frame = ImageFrames{ std::move(files) };\n+\tauto files = collectFiles(*path);\n+\tif (!files) {\n+\t\tLOG(Virtual, Error) << \"Directory has no files: \" << *path;\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tdata->config_.frame = ImageFrames{ std::move(*files) };\n \n \treturn 0;\n }\ndiff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp\nindex 81d2ddda..4ef72c2c 100644\n--- a/src/libcamera/pipeline/virtual/virtual.cpp\n+++ b/src/libcamera/pipeline/virtual/virtual.cpp\n@@ -19,6 +19,7 @@\n #include <string>\n #include <time.h>\n #include <utility>\n+#include <variant>\n #include <vector>\n \n #include <libcamera/base/flags.h>\n@@ -31,13 +32,13 @@\n #include <libcamera/pixel_format.h>\n #include <libcamera/property_ids.h>\n \n+#include \"libcamera/internal/bayer_format.h\"\n #include \"libcamera/internal/camera.h\"\n #include \"libcamera/internal/dma_buf_allocator.h\"\n #include \"libcamera/internal/formats.h\"\n #include \"libcamera/internal/framebuffer.h\"\n #include \"libcamera/internal/pipeline_handler.h\"\n #include \"libcamera/internal/request.h\"\n-#include \"libcamera/internal/value_node.h\"\n \n #include \"pipeline/virtual/config_parser.h\"\n \n@@ -202,21 +203,28 @@ CameraConfiguration::Status VirtualCameraConfiguration::validate()\n \t\t\tadjusted = true;\n \t\t}\n \n-\t\tif (cfg.pixelFormat != formats::NV12) {\n-\t\t\tcfg.pixelFormat = formats::NV12;\n-\t\t\tstatus = Adjusted;\n-\t\t\tadjusted = true;\n-\t\t}\n+\t\tconst PixelFormatInfo &fmtInfo = PixelFormatInfo::info(cfg.pixelFormat);\n+\t\tconst bool rawStream = fmtInfo.colourEncoding == PixelFormatInfo::ColourEncodingRAW;\n \n-\t\tif (cfg.colorSpace != ColorSpace::Rec709) {\n-\t\t\tcfg.colorSpace = ColorSpace::Rec709;\n-\t\t\tstatus = Adjusted;\n-\t\t\tadjusted = true;\n-\t\t}\n+\t\tif (!rawStream) {\n+\t\t\tif (cfg.pixelFormat != formats::NV12) {\n+\t\t\t\tcfg.pixelFormat = formats::NV12;\n+\t\t\t\tstatus = Adjusted;\n+\t\t\t\tadjusted = true;\n+\t\t\t}\n \n-\t\tif (validateColorSpaces() == Adjusted) {\n-\t\t\tstatus = Adjusted;\n-\t\t\tadjusted = true;\n+\t\t\tif (cfg.colorSpace != ColorSpace::Rec709) {\n+\t\t\t\tcfg.colorSpace = ColorSpace::Rec709;\n+\t\t\t\tstatus = Adjusted;\n+\t\t\t\tadjusted = true;\n+\t\t\t}\n+\n+\t\t\tif (validateColorSpaces() == Adjusted) {\n+\t\t\t\tstatus = Adjusted;\n+\t\t\t\tadjusted = true;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tcfg.colorSpace = ColorSpace::Raw;\n \t\t}\n \n \t\tif (adjusted)\n@@ -267,7 +275,54 @@ PipelineHandlerVirtual::generateConfiguration(Camera *camera,\n \t\tcase StreamRole::Viewfinder:\n \t\t\tbreak;\n \n-\t\tcase StreamRole::Raw:\n+\t\tcase StreamRole::Raw: {\n+\t\t\tconst auto *rawFrames = std::get_if<RawFrames>(&data->config_.frame);\n+\t\t\tif (!rawFrames) {\n+\t\t\t\tLOG(Virtual, Error)\n+\t\t\t\t\t<< \"StreamRole::Raw requested but camera is not configured with raw_frames\";\n+\t\t\t\treturn {};\n+\t\t\t}\n+\n+\t\t\tstatic const std::map<uint32_t, BayerFormat::Order> cfaToOrder = {\n+\t\t\t\t{ properties::draft::ColorFilterArrangementEnum::RGGB, BayerFormat::RGGB },\n+\t\t\t\t{ properties::draft::ColorFilterArrangementEnum::BGGR, BayerFormat::BGGR },\n+\t\t\t\t{ properties::draft::ColorFilterArrangementEnum::GRBG, BayerFormat::GRBG },\n+\t\t\t\t{ properties::draft::ColorFilterArrangementEnum::GBRG, BayerFormat::GBRG },\n+\t\t\t};\n+\n+\t\t\tauto it = cfaToOrder.find(rawFrames->cfaPattern);\n+\t\t\tif (it == cfaToOrder.end()) {\n+\t\t\t\tLOG(Virtual, Error) << \"Unsupported CFA pattern\";\n+\t\t\t\treturn {};\n+\t\t\t}\n+\n+\t\t\tPixelFormat rawFormat =\n+\t\t\t\tBayerFormat(it->second, rawFrames->bitDepth,\n+\t\t\t\t\t BayerFormat::Packing::None)\n+\t\t\t\t\t.toPixelFormat();\n+\n+\t\t\tif (!rawFormat.isValid()) {\n+\t\t\t\tLOG(Virtual, Error) << \"Could not find PixelFormat for Bayer format\";\n+\t\t\t\treturn {};\n+\t\t\t}\n+\n+\t\t\t/*\n+\t\t\t * Use the Bayer format matching the raw frame\n+\t\t\t * configuration.\n+\t\t\t */\n+\t\t\tstd::map<PixelFormat, std::vector<SizeRange>> rawStreamFormats;\n+\t\t\trawStreamFormats[rawFormat] = { { data->config_.minResolutionSize,\n+\t\t\t\t\t\t\t data->config_.maxResolutionSize } };\n+\t\t\tStreamFormats rawFormats(rawStreamFormats);\n+\t\t\tStreamConfiguration rawCfg(rawFormats);\n+\t\t\trawCfg.pixelFormat = rawFormat;\n+\t\t\trawCfg.size = data->config_.maxResolutionSize;\n+\t\t\trawCfg.bufferCount = VirtualCameraConfiguration::kBufferCount;\n+\t\t\trawCfg.colorSpace = ColorSpace::Raw;\n+\t\t\tconfig->addConfiguration(rawCfg);\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tdefault:\n \t\t\tLOG(Virtual, Error)\n \t\t\t\t<< \"Requested stream role not supported: \" << role;\n@@ -401,6 +456,13 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator\n \t\tstd::set<Stream *> streams;\n \t\tfor (auto &streamConfig : data->streamConfigs_)\n \t\t\tstreams.insert(&streamConfig.stream);\n+\n+\t\tif (const auto *rawFrames = std::get_if<RawFrames>(&data->config_.frame)) {\n+\t\t\tdata->properties_.set(properties::draft::ColorFilterArrangement, static_cast<int32_t>(rawFrames->cfaPattern));\n+\t\t\tdata->properties_.set(properties::PixelArraySize, data->config_.maxResolutionSize);\n+\t\t\tdata->properties_.set(properties::UnitCellSize, Size(1000, 1000));\n+\t\t}\n+\n \t\tstd::string id = data->config_.id;\n \t\tstd::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);\n \n@@ -434,7 +496,12 @@ bool PipelineHandlerVirtual::initFrameGenerator(Camera *camera)\n \t\t\t [&](ImageFrames &imageFrames) {\n \t\t\t\t for (auto &streamConfig : data->streamConfigs_)\n \t\t\t\t\t streamConfig.frameGenerator = ImageFrameGenerator::create(imageFrames);\n-\t\t\t } },\n+\t\t\t },\n+\t\t\t [&](RawFrames &rawFrames) {\n+\t\t\t\t for (auto &streamConfig : data->streamConfigs_)\n+\t\t\t\t\t streamConfig.frameGenerator = RawFrameGenerator::create(rawFrames);\n+\t\t\t },\n+\t\t },\n \t\t frame);\n \n \tfor (auto &streamConfig : data->streamConfigs_)\ndiff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h\nindex 215e56fa..48392808 100644\n--- a/src/libcamera/pipeline/virtual/virtual.h\n+++ b/src/libcamera/pipeline/virtual/virtual.h\n@@ -23,11 +23,12 @@\n \n #include \"frame_generator.h\"\n #include \"image_frame_generator.h\"\n+#include \"raw_frame_generator.h\"\n #include \"test_pattern_generator.h\"\n \n namespace libcamera {\n \n-using VirtualFrame = std::variant<TestPattern, ImageFrames>;\n+using VirtualFrame = std::variant<TestPattern, ImageFrames, RawFrames>;\n \n class VirtualCameraData : public Camera::Private,\n \t\t\t public Thread,\n", "prefixes": [ "RFC", "v3", "2/2" ] }