diff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build
index c8434593..c6576142 100644
--- a/src/libcamera/pipeline/virtual/meson.build
+++ b/src/libcamera/pipeline/virtual/meson.build
@@ -3,6 +3,7 @@
 libcamera_internal_sources += files([
     'config_parser.cpp',
     'image_frame_generator.cpp',
+    'raw_frame_generator.cpp',
     'test_pattern_generator.cpp',
     'virtual.cpp',
 ])
diff --git a/src/libcamera/pipeline/virtual/raw_frame_generator.cpp b/src/libcamera/pipeline/virtual/raw_frame_generator.cpp
new file mode 100644
index 00000000..e0be28cf
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/raw_frame_generator.cpp
@@ -0,0 +1,131 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, max.bretschneider@leica-geosystems.com
+ *
+ * Derived class of FrameGenerator for generating raw Bayer frames
+ */
+
+#include "raw_frame_generator.h"
+
+#include <errno.h>
+#include <string.h>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/log.h>
+
+#include <libcamera/framebuffer.h>
+
+#include "libcamera/internal/mapped_framebuffer.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Virtual)
+
+/*
+ * Factory function to create a RawFrameGenerator object.
+ * Read the raw Bayer frames from disk and store them in memory.
+ */
+std::unique_ptr<RawFrameGenerator>
+RawFrameGenerator::create(RawFrames &rawFrames)
+{
+	std::unique_ptr<RawFrameGenerator> rawFrameGenerator =
+		std::make_unique<RawFrameGenerator>();
+
+	/*
+	 * For each file in the directory, load the raw frame
+	 * and store it. No format conversion is performed, but
+	 * raw Bayer bytes are stored as-is.
+	 */
+	for (const auto &path : rawFrames.files) {
+		File file(path);
+		if (!file.open(File::OpenModeFlag::ReadOnly)) {
+			LOG(Virtual, Error) << "Failed to open raw frame file "
+					    << file.fileName()
+					    << ": " << strerror(file.error());
+			return nullptr;
+		}
+
+		auto fileSize = file.size();
+		auto buffer = std::make_unique<uint8_t[]>(fileSize);
+		if (file.read({ buffer.get(), static_cast<size_t>(fileSize) }) != fileSize) {
+			LOG(Virtual, Error) << "Failed to read raw frame file "
+					    << file.fileName()
+					    << ": " << strerror(file.error());
+			return nullptr;
+		}
+
+		rawFrameGenerator->framesDatas_.emplace_back(
+			RawFrameData{ std::move(buffer), static_cast<size_t>(fileSize) });
+	}
+
+	ASSERT(!rawFrameGenerator->framesDatas_.empty());
+
+	return rawFrameGenerator;
+}
+
+void RawFrameGenerator::configure(const Size & /*size*/)
+{
+	/*
+	 * Raw frames cannot be scaled, the configured size is not used for
+	 * processing but mismatches are caught in generateFrame().
+	 */
+	frameIndex_ = 0;
+	parameter_ = 0;
+}
+
+int RawFrameGenerator::generateFrame(const Size & /*size*/, const FrameBuffer *buffer)
+{
+	ASSERT(!framesDatas_.empty());
+
+	MappedFrameBuffer mappedFrameBuffer(buffer,
+					    MappedFrameBuffer::MapFlag::Write);
+
+	const auto &planes = mappedFrameBuffer.planes();
+
+	/* Loop only around the number of frames available */
+	frameIndex_ %= framesDatas_.size();
+
+	const auto &frame = framesDatas_[frameIndex_];
+
+	/*
+	 * Raw Bayer frames must exactly match the configured output size.
+	 * They cannot be scaled to fit.
+	 */
+	if (frame.size != planes[0].size()) {
+		LOG(Virtual, Error) << "Raw frame size mismatch: file has "
+				    << frame.size << " bytes, buffer expects "
+				    << planes[0].size() << " bytes";
+		return -EINVAL;
+	}
+
+	memcpy(planes[0].data(), frame.data.get(), frame.size);
+
+	/* Proceed to the next frame on every request */
+	parameter_++;
+	if (parameter_ % frameRepeat == 0) {
+		frameIndex_++;
+	}
+
+	return 0;
+}
+
+/*
+ * \var RawFrameGenerator::frameRepeat
+ * \brief Number of frames to repeat before proceeding to the next frame
+ */
+
+/*
+ * \var RawFrameGenerator::framesDatas_
+ * \brief List of raw Bayer frame buffers loaded from disk
+ */
+
+/* \var RawFrameGenerator::frameIndex_
+ * \brief Index of the current frame in framesDatas_
+ */
+
+/*
+ * \var RawFrameGenerator::parameter_
+ * \brief Counter used to implement frameRepeat behaviour
+ */
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/raw_frame_generator.h b/src/libcamera/pipeline/virtual/raw_frame_generator.h
new file mode 100644
index 00000000..d711a47d
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/raw_frame_generator.h
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, max.bretschneider@leica-geosystems.com
+ *
+ * Raw Bayer frame generator for the virtual pipeline handler
+ */
+
+#pragma once
+
+#include <filesystem>
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+#include "frame_generator.h"
+
+namespace libcamera {
+
+/* Frame configuration provided by the config file */
+struct RawFrames {
+	std::vector<std::filesystem::path> files;
+	uint32_t cfaPattern;
+	unsigned int bitDepth;
+};
+
+class RawFrameGenerator : public FrameGenerator
+{
+public:
+	static std::unique_ptr<RawFrameGenerator> create(RawFrames &rawFrames);
+
+private:
+	static constexpr unsigned int frameRepeat = 1; /*advance every frame*/
+
+	struct RawFrameData {
+		std::unique_ptr<uint8_t[]> data;
+		size_t size;
+	};
+
+	void configure(const Size &size) override;
+	int generateFrame(const Size &size, const FrameBuffer *buffer) override;
+
+	std::vector<RawFrameData> framesDatas_;
+	unsigned int frameIndex_;
+	unsigned int parameter_;
+};
+
+} /* namespace libcamera */
