diff --git a/Documentation/Doxyfile.in b/Documentation/Doxyfile.in
index a86ea6c1ec95..75326c1964e9 100644
--- a/Documentation/Doxyfile.in
+++ b/Documentation/Doxyfile.in
@@ -42,6 +42,7 @@ EXCLUDE                = @TOP_SRCDIR@/include/libcamera/base/span.h \
                          @TOP_SRCDIR@/src/libcamera/device_enumerator_udev.cpp \
                          @TOP_SRCDIR@/src/libcamera/ipc_pipe_unixsocket.cpp \
                          @TOP_SRCDIR@/src/libcamera/pipeline/ \
+                         @TOP_SRCDIR@/src/libcamera/sensor/camera_sensor_legacy.cpp \
                          @TOP_SRCDIR@/src/libcamera/tracepoints.cpp \
                          @TOP_BUILDDIR@/include/libcamera/internal/tracepoints.h \
                          @TOP_BUILDDIR@/src/libcamera/proxy/
diff --git a/include/libcamera/internal/camera_sensor.h b/include/libcamera/internal/camera_sensor.h
index 577af779cd6e..d37e66773195 100644
--- a/include/libcamera/internal/camera_sensor.h
+++ b/include/libcamera/internal/camera_sensor.h
@@ -9,10 +9,10 @@
 
 #include <memory>
 #include <string>
+#include <variant>
 #include <vector>
 
 #include <libcamera/base/class.h>
-#include <libcamera/base/log.h>
 
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
@@ -20,8 +20,6 @@
 #include <libcamera/orientation.h>
 #include <libcamera/transform.h>
 
-#include <libcamera/ipa/core_ipa_interface.h>
-
 #include "libcamera/internal/bayer_format.h"
 #include "libcamera/internal/formats.h"
 #include "libcamera/internal/v4l2_subdevice.h"
@@ -32,95 +30,50 @@ class CameraLens;
 class MediaEntity;
 class SensorConfiguration;
 
-struct CameraSensorProperties;
-
 enum class Orientation;
 
-class CameraSensor : protected Loggable
+struct IPACameraSensorInfo;
+
+class CameraSensor
 {
 public:
-	~CameraSensor();
+	virtual ~CameraSensor();
 
-	int init();
+	virtual const std::string &model() const = 0;
+	virtual const std::string &id() const = 0;
 
-	const std::string &model() const { return model_; }
-	const std::string &id() const { return id_; }
+	virtual const MediaEntity *entity() const = 0;
+	virtual V4L2Subdevice *device() = 0;
 
-	const MediaEntity *entity() const { return entity_; }
-	V4L2Subdevice *device() { return subdev_.get(); }
+	virtual CameraLens *focusLens() = 0;
 
-	CameraLens *focusLens() { return focusLens_.get(); }
+	virtual const std::vector<unsigned int> &mbusCodes() const = 0;
+	virtual std::vector<Size> sizes(unsigned int mbusCode) const = 0;
+	virtual Size resolution() const = 0;
 
-	const std::vector<unsigned int> &mbusCodes() const { return mbusCodes_; }
-	std::vector<Size> sizes(unsigned int mbusCode) const;
-	Size resolution() const;
+	virtual V4L2SubdeviceFormat
+	getFormat(const std::vector<unsigned int> &mbusCodes,
+		  const Size &size) const = 0;
+	virtual int setFormat(V4L2SubdeviceFormat *format,
+			      Transform transform = Transform::Identity) = 0;
+	virtual int tryFormat(V4L2SubdeviceFormat *format) const = 0;
 
-	V4L2SubdeviceFormat getFormat(const std::vector<unsigned int> &mbusCodes,
-				      const Size &size) const;
-	int setFormat(V4L2SubdeviceFormat *format,
-		      Transform transform = Transform::Identity);
-	int tryFormat(V4L2SubdeviceFormat *format) const;
+	virtual int applyConfiguration(const SensorConfiguration &config,
+				       Transform transform = Transform::Identity,
+				       V4L2SubdeviceFormat *sensorFormat = nullptr) = 0;
 
-	int applyConfiguration(const SensorConfiguration &config,
-			       Transform transform = Transform::Identity,
-			       V4L2SubdeviceFormat *sensorFormat = nullptr);
+	virtual const ControlList &properties() const = 0;
+	virtual int sensorInfo(IPACameraSensorInfo *info) const = 0;
+	virtual Transform computeTransform(Orientation *orientation) const = 0;
+	virtual BayerFormat::Order bayerOrder(Transform t) const = 0;
 
-	const ControlList &properties() const { return properties_; }
-	int sensorInfo(IPACameraSensorInfo *info) const;
-	Transform computeTransform(Orientation *orientation) const;
-	BayerFormat::Order bayerOrder(Transform t) const;
+	virtual const ControlInfoMap &controls() const = 0;
+	virtual ControlList getControls(const std::vector<uint32_t> &ids) = 0;
+	virtual int setControls(ControlList *ctrls) = 0;
 
-	const ControlInfoMap &controls() const;
-	ControlList getControls(const std::vector<uint32_t> &ids);
-	int setControls(ControlList *ctrls);
-
-	const std::vector<controls::draft::TestPatternModeEnum> &testPatternModes() const
-	{
-		return testPatternModes_;
-	}
-	int setTestPatternMode(controls::draft::TestPatternModeEnum mode);
-
-protected:
-	explicit CameraSensor(const MediaEntity *entity);
-	std::string logPrefix() const override;
-
-private:
-	LIBCAMERA_DISABLE_COPY(CameraSensor)
-
-	int generateId();
-	int validateSensorDriver();
-	void initVimcDefaultProperties();
-	void initStaticProperties();
-	void initTestPatternModes();
-	int initProperties();
-	int discoverAncillaryDevices();
-	int applyTestPatternMode(controls::draft::TestPatternModeEnum mode);
-
-	const MediaEntity *entity_;
-	std::unique_ptr<V4L2Subdevice> subdev_;
-	unsigned int pad_;
-
-	const CameraSensorProperties *staticProps_;
-
-	std::string model_;
-	std::string id_;
-
-	V4L2Subdevice::Formats formats_;
-	std::vector<unsigned int> mbusCodes_;
-	std::vector<Size> sizes_;
-	std::vector<controls::draft::TestPatternModeEnum> testPatternModes_;
-	controls::draft::TestPatternModeEnum testPatternMode_;
-
-	Size pixelArraySize_;
-	Rectangle activeArea_;
-	const BayerFormat *bayerFormat_;
-	bool supportFlips_;
-	bool flipsAlterBayerOrder_;
-	Orientation mountingOrientation_;
-
-	ControlList properties_;
-
-	std::unique_ptr<CameraLens> focusLens_;
+	virtual const std::vector<controls::draft::TestPatternModeEnum> &
+	testPatternModes() const = 0;
+	virtual int setTestPatternMode(controls::draft::TestPatternModeEnum mode) = 0;
 };
 
 class CameraSensorFactoryBase
@@ -138,10 +91,8 @@ private:
 
 	static void registerFactory(CameraSensorFactoryBase *factory);
 
-	virtual bool match(const MediaEntity *entity) const = 0;
-
-	virtual std::unique_ptr<CameraSensor>
-	createInstance(MediaEntity *entity) const = 0;
+	virtual std::variant<std::unique_ptr<CameraSensor>, int>
+	match(MediaEntity *entity) const = 0;
 };
 
 template<typename _CameraSensor>
@@ -154,16 +105,11 @@ public:
 	}
 
 private:
-	bool match(const MediaEntity *entity) const override
+	std::variant<std::unique_ptr<CameraSensor>, int>
+	match(MediaEntity *entity) const override
 	{
 		return _CameraSensor::match(entity);
 	}
-
-	std::unique_ptr<CameraSensor>
-	createInstance(MediaEntity *entity) const override
-	{
-		return _CameraSensor::create(entity);
-	}
 };
 
 #define REGISTER_CAMERA_SENSOR(sensor) \
diff --git a/src/libcamera/sensor/camera_sensor.cpp b/src/libcamera/sensor/camera_sensor.cpp
index a35683eb4b58..7abbe2c76596 100644
--- a/src/libcamera/sensor/camera_sensor.cpp
+++ b/src/libcamera/sensor/camera_sensor.cpp
@@ -6,27 +6,13 @@
  */
 
 #include "libcamera/internal/camera_sensor.h"
-#include "libcamera/internal/media_device.h"
 
-#include <algorithm>
-#include <float.h>
-#include <iomanip>
-#include <limits.h>
-#include <map>
-#include <math.h>
-#include <string.h>
+#include <memory>
+#include <vector>
 
-#include <libcamera/camera.h>
-#include <libcamera/orientation.h>
-#include <libcamera/property_ids.h>
+#include <libcamera/base/log.h>
 
-#include <libcamera/base/utils.h>
-
-#include "libcamera/internal/bayer_format.h"
-#include "libcamera/internal/camera_lens.h"
-#include "libcamera/internal/camera_sensor_properties.h"
-#include "libcamera/internal/formats.h"
-#include "libcamera/internal/sysfs.h"
+#include "libcamera/internal/media_object.h"
 
 /**
  * \file camera_sensor.h
@@ -39,537 +25,16 @@ LOG_DEFINE_CATEGORY(CameraSensor)
 
 /**
  * \class CameraSensor
- * \brief A camera sensor based on V4L2 subdevices
+ * \brief A abstract camera sensor
  *
  * The CameraSensor class eases handling of sensors for pipeline handlers by
- * hiding the details of the V4L2 subdevice kernel API and caching sensor
- * information.
- *
- * The implementation is currently limited to sensors that expose a single V4L2
- * subdevice with a single pad. It will be extended to support more complex
- * devices as the needs arise.
+ * hiding the details of the kernel API and caching sensor information.
  */
 
-/**
- * \brief Construct a CameraSensor
- * \param[in] entity The media entity backing the camera sensor
- *
- * Once constructed the instance must be initialized with init().
- */
-CameraSensor::CameraSensor(const MediaEntity *entity)
-	: entity_(entity), pad_(UINT_MAX), staticProps_(nullptr),
-	  bayerFormat_(nullptr), supportFlips_(false),
-	  flipsAlterBayerOrder_(false), properties_(properties::properties)
-{
-}
-
 /**
  * \brief Destroy a CameraSensor
  */
-CameraSensor::~CameraSensor()
-{
-}
-
-/**
- * \brief Initialize the camera sensor instance
- *
- * This function performs the initialisation steps of the CameraSensor that may
- * fail. It shall be called once and only once after constructing the instance.
- *
- * \return 0 on success or a negative error code otherwise
- */
-int CameraSensor::init()
-{
-	for (const MediaPad *pad : entity_->pads()) {
-		if (pad->flags() & MEDIA_PAD_FL_SOURCE) {
-			pad_ = pad->index();
-			break;
-		}
-	}
-
-	if (pad_ == UINT_MAX) {
-		LOG(CameraSensor, Error)
-			<< "Sensors with more than one pad are not supported";
-		return -EINVAL;
-	}
-
-	switch (entity_->function()) {
-	case MEDIA_ENT_F_CAM_SENSOR:
-	case MEDIA_ENT_F_PROC_VIDEO_ISP:
-		break;
-
-	default:
-		LOG(CameraSensor, Error)
-			<< "Invalid sensor function "
-			<< utils::hex(entity_->function());
-		return -EINVAL;
-	}
-
-	/* Create and open the subdev. */
-	subdev_ = std::make_unique<V4L2Subdevice>(entity_);
-	int ret = subdev_->open();
-	if (ret < 0)
-		return ret;
-
-	/*
-	 * Clear any flips to be sure we get the "native" Bayer order. This is
-	 * harmless for sensors where the flips don't affect the Bayer order.
-	 */
-	ControlList ctrls(subdev_->controls());
-	if (subdev_->controls().find(V4L2_CID_HFLIP) != subdev_->controls().end())
-		ctrls.set(V4L2_CID_HFLIP, 0);
-	if (subdev_->controls().find(V4L2_CID_VFLIP) != subdev_->controls().end())
-		ctrls.set(V4L2_CID_VFLIP, 0);
-	subdev_->setControls(&ctrls);
-
-	/* Enumerate, sort and cache media bus codes and sizes. */
-	formats_ = subdev_->formats(pad_);
-	if (formats_.empty()) {
-		LOG(CameraSensor, Error) << "No image format found";
-		return -EINVAL;
-	}
-
-	mbusCodes_ = utils::map_keys(formats_);
-	std::sort(mbusCodes_.begin(), mbusCodes_.end());
-
-	for (const auto &format : formats_) {
-		const std::vector<SizeRange> &ranges = format.second;
-		std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes_),
-			       [](const SizeRange &range) { return range.max; });
-	}
-
-	std::sort(sizes_.begin(), sizes_.end());
-
-	/* Remove duplicates. */
-	auto last = std::unique(sizes_.begin(), sizes_.end());
-	sizes_.erase(last, sizes_.end());
-
-	/*
-	 * VIMC is a bit special, as it does not yet support all the mandatory
-	 * requirements regular sensors have to respect.
-	 *
-	 * Do not validate the driver if it's VIMC and initialize the sensor
-	 * properties with static information.
-	 *
-	 * \todo Remove the special case once the VIMC driver has been
-	 * updated in all test platforms.
-	 */
-	if (entity_->device()->driver() == "vimc") {
-		initVimcDefaultProperties();
-
-		ret = initProperties();
-		if (ret)
-			return ret;
-
-		return discoverAncillaryDevices();
-	}
-
-	/* Get the color filter array pattern (only for RAW sensors). */
-	for (unsigned int mbusCode : mbusCodes_) {
-		const BayerFormat &bayerFormat = BayerFormat::fromMbusCode(mbusCode);
-		if (bayerFormat.isValid()) {
-			bayerFormat_ = &bayerFormat;
-			break;
-		}
-	}
-
-	ret = validateSensorDriver();
-	if (ret)
-		return ret;
-
-	ret = initProperties();
-	if (ret)
-		return ret;
-
-	ret = discoverAncillaryDevices();
-	if (ret)
-		return ret;
-
-	/*
-	 * Set HBLANK to the minimum to start with a well-defined line length,
-	 * allowing IPA modules that do not modify HBLANK to use the sensor
-	 * minimum line length in their calculations.
-	 */
-	const struct v4l2_query_ext_ctrl *hblankInfo = subdev_->controlInfo(V4L2_CID_HBLANK);
-	if (hblankInfo && !(hblankInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) {
-		ControlList ctrl(subdev_->controls());
-
-		ctrl.set(V4L2_CID_HBLANK, static_cast<int32_t>(hblankInfo->minimum));
-		ret = subdev_->setControls(&ctrl);
-		if (ret)
-			return ret;
-	}
-
-	return applyTestPatternMode(controls::draft::TestPatternModeEnum::TestPatternModeOff);
-}
-
-int CameraSensor::generateId()
-{
-	const std::string devPath = subdev_->devicePath();
-
-	/* Try to get ID from firmware description. */
-	id_ = sysfs::firmwareNodePath(devPath);
-	if (!id_.empty())
-		return 0;
-
-	/*
-	 * Virtual sensors not described in firmware
-	 *
-	 * Verify it's a platform device and construct ID from the device path
-	 * and model of sensor.
-	 */
-	if (devPath.find("/sys/devices/platform/", 0) == 0) {
-		id_ = devPath.substr(strlen("/sys/devices/")) + " " + model();
-		return 0;
-	}
-
-	LOG(CameraSensor, Error) << "Can't generate sensor ID";
-	return -EINVAL;
-}
-
-int CameraSensor::validateSensorDriver()
-{
-	int err = 0;
-
-	/*
-	 * Optional controls are used to register optional sensor properties. If
-	 * not present, some values will be defaulted.
-	 */
-	static constexpr uint32_t optionalControls[] = {
-		V4L2_CID_CAMERA_SENSOR_ROTATION,
-	};
-
-	const ControlIdMap &controls = subdev_->controls().idmap();
-	for (uint32_t ctrl : optionalControls) {
-		if (!controls.count(ctrl))
-			LOG(CameraSensor, Debug)
-				<< "Optional V4L2 control " << utils::hex(ctrl)
-				<< " not supported";
-	}
-
-	/*
-	 * Recommended controls are similar to optional controls, but will
-	 * become mandatory in the near future. Be loud if they're missing.
-	 */
-	static constexpr uint32_t recommendedControls[] = {
-		V4L2_CID_CAMERA_ORIENTATION,
-	};
-
-	for (uint32_t ctrl : recommendedControls) {
-		if (!controls.count(ctrl)) {
-			LOG(CameraSensor, Warning)
-				<< "Recommended V4L2 control " << utils::hex(ctrl)
-				<< " not supported";
-			err = -EINVAL;
-		}
-	}
-
-	/*
-	 * Verify if sensor supports horizontal/vertical flips
-	 *
-	 * \todo Handle horizontal and vertical flips independently.
-	 */
-	const struct v4l2_query_ext_ctrl *hflipInfo = subdev_->controlInfo(V4L2_CID_HFLIP);
-	const struct v4l2_query_ext_ctrl *vflipInfo = subdev_->controlInfo(V4L2_CID_VFLIP);
-	if (hflipInfo && !(hflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY) &&
-	    vflipInfo && !(vflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) {
-		supportFlips_ = true;
-
-		if (hflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT ||
-		    vflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT)
-			flipsAlterBayerOrder_ = true;
-	}
-
-	if (!supportFlips_)
-		LOG(CameraSensor, Debug)
-			<< "Camera sensor does not support horizontal/vertical flip";
-
-	/*
-	 * Make sure the required selection targets are supported.
-	 *
-	 * Failures in reading any of the targets are not deemed to be fatal,
-	 * but some properties and features, like constructing a
-	 * IPACameraSensorInfo for the IPA module, won't be supported.
-	 *
-	 * \todo Make support for selection targets mandatory as soon as all
-	 * test platforms have been updated.
-	 */
-	Rectangle rect;
-	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_BOUNDS, &rect);
-	if (ret) {
-		/*
-		 * Default the pixel array size to the largest size supported
-		 * by the sensor. The sizes_ vector is sorted in ascending
-		 * order, the largest size is thus the last element.
-		 */
-		pixelArraySize_ = sizes_.back();
-
-		LOG(CameraSensor, Warning)
-			<< "The PixelArraySize property has been defaulted to "
-			<< pixelArraySize_;
-		err = -EINVAL;
-	} else {
-		pixelArraySize_ = rect.size();
-	}
-
-	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_DEFAULT, &activeArea_);
-	if (ret) {
-		activeArea_ = Rectangle(pixelArraySize_);
-		LOG(CameraSensor, Warning)
-			<< "The PixelArrayActiveAreas property has been defaulted to "
-			<< activeArea_;
-		err = -EINVAL;
-	}
-
-	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &rect);
-	if (ret) {
-		LOG(CameraSensor, Warning)
-			<< "Failed to retrieve the sensor crop rectangle";
-		err = -EINVAL;
-	}
-
-	if (err) {
-		LOG(CameraSensor, Warning)
-			<< "The sensor kernel driver needs to be fixed";
-		LOG(CameraSensor, Warning)
-			<< "See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information";
-	}
-
-	if (!bayerFormat_)
-		return 0;
-
-	/*
-	 * For raw sensors, make sure the sensor driver supports the controls
-	 * required by the CameraSensor class.
-	 */
-	static constexpr uint32_t mandatoryControls[] = {
-		V4L2_CID_ANALOGUE_GAIN,
-		V4L2_CID_EXPOSURE,
-		V4L2_CID_HBLANK,
-		V4L2_CID_PIXEL_RATE,
-		V4L2_CID_VBLANK,
-	};
-
-	err = 0;
-	for (uint32_t ctrl : mandatoryControls) {
-		if (!controls.count(ctrl)) {
-			LOG(CameraSensor, Error)
-				<< "Mandatory V4L2 control " << utils::hex(ctrl)
-				<< " not available";
-			err = -EINVAL;
-		}
-	}
-
-	if (err) {
-		LOG(CameraSensor, Error)
-			<< "The sensor kernel driver needs to be fixed";
-		LOG(CameraSensor, Error)
-			<< "See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information";
-		return err;
-	}
-
-	return 0;
-}
-
-/*
- * \brief Initialize properties that cannot be intialized by the
- * regular initProperties() function for VIMC
- */
-void CameraSensor::initVimcDefaultProperties()
-{
-	/* Use the largest supported size. */
-	pixelArraySize_ = sizes_.back();
-	activeArea_ = Rectangle(pixelArraySize_);
-}
-
-void CameraSensor::initStaticProperties()
-{
-	staticProps_ = CameraSensorProperties::get(model_);
-	if (!staticProps_)
-		return;
-
-	/* Register the properties retrieved from the sensor database. */
-	properties_.set(properties::UnitCellSize, staticProps_->unitCellSize);
-
-	initTestPatternModes();
-}
-
-void CameraSensor::initTestPatternModes()
-{
-	const auto &v4l2TestPattern = controls().find(V4L2_CID_TEST_PATTERN);
-	if (v4l2TestPattern == controls().end()) {
-		LOG(CameraSensor, Debug) << "V4L2_CID_TEST_PATTERN is not supported";
-		return;
-	}
-
-	const auto &testPatternModes = staticProps_->testPatternModes;
-	if (testPatternModes.empty()) {
-		/*
-		 * The camera sensor supports test patterns but we don't know
-		 * how to map them so this should be fixed.
-		 */
-		LOG(CameraSensor, Debug) << "No static test pattern map for \'"
-					 << model() << "\'";
-		return;
-	}
-
-	/*
-	 * Create a map that associates the V4L2 control index to the test
-	 * pattern mode by reversing the testPatternModes map provided by the
-	 * camera sensor properties. This makes it easier to verify if the
-	 * control index is supported in the below for loop that creates the
-	 * list of supported test patterns.
-	 */
-	std::map<int32_t, controls::draft::TestPatternModeEnum> indexToTestPatternMode;
-	for (const auto &it : testPatternModes)
-		indexToTestPatternMode[it.second] = it.first;
-
-	for (const ControlValue &value : v4l2TestPattern->second.values()) {
-		const int32_t index = value.get<int32_t>();
-
-		const auto it = indexToTestPatternMode.find(index);
-		if (it == indexToTestPatternMode.end()) {
-			LOG(CameraSensor, Debug)
-				<< "Test pattern mode " << index << " ignored";
-			continue;
-		}
-
-		testPatternModes_.push_back(it->second);
-	}
-}
-
-int CameraSensor::initProperties()
-{
-	model_ = subdev_->model();
-	properties_.set(properties::Model, utils::toAscii(model_));
-
-	/* Generate a unique ID for the sensor. */
-	int ret = generateId();
-	if (ret)
-		return ret;
-
-	/* Initialize the static properties from the sensor database. */
-	initStaticProperties();
-
-	/* Retrieve and register properties from the kernel interface. */
-	const ControlInfoMap &controls = subdev_->controls();
-
-	const auto &orientation = controls.find(V4L2_CID_CAMERA_ORIENTATION);
-	if (orientation != controls.end()) {
-		int32_t v4l2Orientation = orientation->second.def().get<int32_t>();
-		int32_t propertyValue;
-
-		switch (v4l2Orientation) {
-		default:
-			LOG(CameraSensor, Warning)
-				<< "Unsupported camera location "
-				<< v4l2Orientation << ", setting to External";
-			[[fallthrough]];
-		case V4L2_CAMERA_ORIENTATION_EXTERNAL:
-			propertyValue = properties::CameraLocationExternal;
-			break;
-		case V4L2_CAMERA_ORIENTATION_FRONT:
-			propertyValue = properties::CameraLocationFront;
-			break;
-		case V4L2_CAMERA_ORIENTATION_BACK:
-			propertyValue = properties::CameraLocationBack;
-			break;
-		}
-		properties_.set(properties::Location, propertyValue);
-	} else {
-		LOG(CameraSensor, Warning) << "Failed to retrieve the camera location";
-	}
-
-	const auto &rotationControl = controls.find(V4L2_CID_CAMERA_SENSOR_ROTATION);
-	if (rotationControl != controls.end()) {
-		int32_t propertyValue = rotationControl->second.def().get<int32_t>();
-
-		/*
-		 * Cache the Transform associated with the camera mounting
-		 * rotation for later use in computeTransform().
-		 */
-		bool success;
-		mountingOrientation_ = orientationFromRotation(propertyValue, &success);
-		if (!success) {
-			LOG(CameraSensor, Warning)
-				<< "Invalid rotation of " << propertyValue
-				<< " degrees - ignoring";
-			mountingOrientation_ = Orientation::Rotate0;
-		}
-
-		properties_.set(properties::Rotation, propertyValue);
-	} else {
-		LOG(CameraSensor, Warning)
-			<< "Rotation control not available, default to 0 degrees";
-		properties_.set(properties::Rotation, 0);
-		mountingOrientation_ = Orientation::Rotate0;
-	}
-
-	properties_.set(properties::PixelArraySize, pixelArraySize_);
-	properties_.set(properties::PixelArrayActiveAreas, { activeArea_ });
-
-	/* Color filter array pattern, register only for RAW sensors. */
-	if (bayerFormat_) {
-		int32_t cfa;
-		switch (bayerFormat_->order) {
-		case BayerFormat::BGGR:
-			cfa = properties::draft::BGGR;
-			break;
-		case BayerFormat::GBRG:
-			cfa = properties::draft::GBRG;
-			break;
-		case BayerFormat::GRBG:
-			cfa = properties::draft::GRBG;
-			break;
-		case BayerFormat::RGGB:
-			cfa = properties::draft::RGGB;
-			break;
-		case BayerFormat::MONO:
-			cfa = properties::draft::MONO;
-			break;
-		}
-
-		properties_.set(properties::draft::ColorFilterArrangement, cfa);
-	}
-
-	return 0;
-}
-
-/**
- * \brief Check for and initialise any ancillary devices
- *
- * Sensors sometimes have ancillary devices such as a Lens or Flash that could
- * be linked to their MediaEntity by the kernel. Search for and handle any
- * such device.
- *
- * \todo Handle MEDIA_ENT_F_FLASH too.
- */
-int CameraSensor::discoverAncillaryDevices()
-{
-	int ret;
-
-	for (MediaEntity *ancillary : entity_->ancillaryEntities()) {
-		switch (ancillary->function()) {
-		case MEDIA_ENT_F_LENS:
-			focusLens_ = std::make_unique<CameraLens>(ancillary);
-			ret = focusLens_->init();
-			if (ret) {
-				LOG(CameraSensor, Error)
-					<< "Lens initialisation failed, lens disabled";
-				focusLens_.reset();
-			}
-			break;
-
-		default:
-			LOG(CameraSensor, Warning)
-				<< "Unsupported ancillary entity function "
-				<< ancillary->function();
-			break;
-		}
-	}
-
-	return 0;
-}
+CameraSensor::~CameraSensor() = default;
 
 /**
  * \fn CameraSensor::model()
@@ -624,29 +89,15 @@ int CameraSensor::discoverAncillaryDevices()
  */
 
 /**
+ * \fn CameraSensor::sizes()
  * \brief Retrieve the supported frame sizes for a media bus code
  * \param[in] mbusCode The media bus code for which sizes are requested
  *
  * \return The supported frame sizes for \a mbusCode sorted in increasing order
  */
-std::vector<Size> CameraSensor::sizes(unsigned int mbusCode) const
-{
-	std::vector<Size> sizes;
-
-	const auto &format = formats_.find(mbusCode);
-	if (format == formats_.end())
-		return sizes;
-
-	const std::vector<SizeRange> &ranges = format->second;
-	std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes),
-		       [](const SizeRange &range) { return range.max; });
-
-	std::sort(sizes.begin(), sizes.end());
-
-	return sizes;
-}
 
 /**
+ * \fn CameraSensor::resolution()
  * \brief Retrieve the camera sensor resolution
  *
  * The camera sensor resolution is the active pixel area size, clamped to the
@@ -659,12 +110,9 @@ std::vector<Size> CameraSensor::sizes(unsigned int mbusCode) const
  *
  * \return The camera sensor resolution in pixels
  */
-Size CameraSensor::resolution() const
-{
-	return std::min(sizes_.back(), activeArea_.size());
-}
 
 /**
+ * \fn CameraSensor::getFormat()
  * \brief Retrieve the best sensor format for a desired output
  * \param[in] mbusCodes The list of acceptable media bus codes
  * \param[in] size The desired size
@@ -700,59 +148,9 @@ Size CameraSensor::resolution() const
  * \return The best sensor output format matching the desired media bus codes
  * and size on success, or an empty format otherwise.
  */
-V4L2SubdeviceFormat CameraSensor::getFormat(const std::vector<unsigned int> &mbusCodes,
-					    const Size &size) const
-{
-	unsigned int desiredArea = size.width * size.height;
-	unsigned int bestArea = UINT_MAX;
-	float desiredRatio = static_cast<float>(size.width) / size.height;
-	float bestRatio = FLT_MAX;
-	const Size *bestSize = nullptr;
-	uint32_t bestCode = 0;
-
-	for (unsigned int code : mbusCodes) {
-		const auto formats = formats_.find(code);
-		if (formats == formats_.end())
-			continue;
-
-		for (const SizeRange &range : formats->second) {
-			const Size &sz = range.max;
-
-			if (sz.width < size.width || sz.height < size.height)
-				continue;
-
-			float ratio = static_cast<float>(sz.width) / sz.height;
-			float ratioDiff = fabsf(ratio - desiredRatio);
-			unsigned int area = sz.width * sz.height;
-			unsigned int areaDiff = area - desiredArea;
-
-			if (ratioDiff > bestRatio)
-				continue;
-
-			if (ratioDiff < bestRatio || areaDiff < bestArea) {
-				bestRatio = ratioDiff;
-				bestArea = areaDiff;
-				bestSize = &sz;
-				bestCode = code;
-			}
-		}
-	}
-
-	if (!bestSize) {
-		LOG(CameraSensor, Debug) << "No supported format or size found";
-		return {};
-	}
-
-	V4L2SubdeviceFormat format{
-		.code = bestCode,
-		.size = *bestSize,
-		.colorSpace = ColorSpace::Raw,
-	};
-
-	return format;
-}
 
 /**
+ * \fn CameraSensor::setFormat()
  * \brief Set the sensor output format
  * \param[in] format The desired sensor output format
  * \param[in] transform The transform to be applied on the sensor.
@@ -767,32 +165,9 @@ V4L2SubdeviceFormat CameraSensor::getFormat(const std::vector<unsigned int> &mbu
  *
  * \return 0 on success or a negative error code otherwise
  */
-int CameraSensor::setFormat(V4L2SubdeviceFormat *format, Transform transform)
-{
-	/* Configure flips if the sensor supports that. */
-	if (supportFlips_) {
-		ControlList flipCtrls(subdev_->controls());
-
-		flipCtrls.set(V4L2_CID_HFLIP,
-			      static_cast<int32_t>(!!(transform & Transform::HFlip)));
-		flipCtrls.set(V4L2_CID_VFLIP,
-			      static_cast<int32_t>(!!(transform & Transform::VFlip)));
-
-		int ret = subdev_->setControls(&flipCtrls);
-		if (ret)
-			return ret;
-	}
-
-	/* Apply format on the subdev. */
-	int ret = subdev_->setFormat(pad_, format);
-	if (ret)
-		return ret;
-
-	subdev_->updateControlInfo();
-	return 0;
-}
 
 /**
+ * \fn CameraSensor::tryFormat()
  * \brief Try the sensor output format
  * \param[in] format The desired sensor output format
  *
@@ -803,13 +178,9 @@ int CameraSensor::setFormat(V4L2SubdeviceFormat *format, Transform transform)
  *
  * \return 0 on success or a negative error code otherwise
  */
-int CameraSensor::tryFormat(V4L2SubdeviceFormat *format) const
-{
-	return subdev_->setFormat(pad_, format,
-				  V4L2Subdevice::Whence::TryFormat);
-}
 
 /**
+ * \fn CameraSensor::applyConfiguration()
  * \brief Apply a sensor configuration to the camera sensor
  * \param[in] config The sensor configuration
  * \param[in] transform The transform to be applied on the sensor.
@@ -824,74 +195,6 @@ int CameraSensor::tryFormat(V4L2SubdeviceFormat *format) const
  * \return 0 if \a config is applied correctly to the camera sensor, a negative
  * error code otherwise
  */
-int CameraSensor::applyConfiguration(const SensorConfiguration &config,
-				     Transform transform,
-				     V4L2SubdeviceFormat *sensorFormat)
-{
-	if (!config.isValid()) {
-		LOG(CameraSensor, Error) << "Invalid sensor configuration";
-		return -EINVAL;
-	}
-
-	std::vector<unsigned int> filteredCodes;
-	std::copy_if(mbusCodes_.begin(), mbusCodes_.end(),
-		     std::back_inserter(filteredCodes),
-		     [&config](unsigned int mbusCode) {
-			     BayerFormat bayer = BayerFormat::fromMbusCode(mbusCode);
-			     if (bayer.bitDepth == config.bitDepth)
-				     return true;
-			     return false;
-		     });
-	if (filteredCodes.empty()) {
-		LOG(CameraSensor, Error)
-			<< "Cannot find any format with bit depth "
-			<< config.bitDepth;
-		return -EINVAL;
-	}
-
-	/*
-	 * Compute the sensor's data frame size by applying the cropping
-	 * rectangle, subsampling and output crop to the sensor's pixel array
-	 * size.
-	 *
-	 * \todo The actual size computation is for now ignored and only the
-	 * output size is considered. This implies that resolutions obtained
-	 * with two different cropping/subsampling will look identical and
-	 * only the first found one will be considered.
-	 */
-	V4L2SubdeviceFormat subdevFormat = {};
-	for (unsigned int code : filteredCodes) {
-		for (const Size &size : sizes(code)) {
-			if (size.width != config.outputSize.width ||
-			    size.height != config.outputSize.height)
-				continue;
-
-			subdevFormat.code = code;
-			subdevFormat.size = size;
-			break;
-		}
-	}
-	if (!subdevFormat.code) {
-		LOG(CameraSensor, Error) << "Invalid output size in sensor configuration";
-		return -EINVAL;
-	}
-
-	int ret = setFormat(&subdevFormat, transform);
-	if (ret)
-		return ret;
-
-	/*
-	 * Return to the caller the format actually applied to the sensor.
-	 * This is relevant if transform has changed the bayer pattern order.
-	 */
-	if (sensorFormat)
-		*sensorFormat = subdevFormat;
-
-	/* \todo Handle AnalogCrop. Most sensors do not support set_selection */
-	/* \todo Handle scaling in the digital domain. */
-
-	return 0;
-}
 
 /**
  * \fn CameraSensor::properties()
@@ -900,6 +203,7 @@ int CameraSensor::applyConfiguration(const SensorConfiguration &config,
  */
 
 /**
+ * \fn CameraSensor::sensorInfo()
  * \brief Assemble and return the camera sensor info
  * \param[out] info The camera sensor info
  *
@@ -913,82 +217,9 @@ int CameraSensor::applyConfiguration(const SensorConfiguration &config,
  *
  * \return 0 on success, a negative error code otherwise
  */
-int CameraSensor::sensorInfo(IPACameraSensorInfo *info) const
-{
-	if (!bayerFormat_)
-		return -EINVAL;
-
-	info->model = model();
-
-	/*
-	 * The active area size is a static property, while the crop
-	 * rectangle needs to be re-read as it depends on the sensor
-	 * configuration.
-	 */
-	info->activeAreaSize = { activeArea_.width, activeArea_.height };
-
-	/*
-	 * \todo Support for retreiving the crop rectangle is scheduled to
-	 * become mandatory. For the time being use the default value if it has
-	 * been initialized at sensor driver validation time.
-	 */
-	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &info->analogCrop);
-	if (ret) {
-		info->analogCrop = activeArea_;
-		LOG(CameraSensor, Warning)
-			<< "The analogue crop rectangle has been defaulted to the active area size";
-	}
-
-	/*
-	 * IPACameraSensorInfo::analogCrop::x and IPACameraSensorInfo::analogCrop::y
-	 * are defined relatively to the active pixel area, while V4L2's
-	 * TGT_CROP target is defined in respect to the full pixel array.
-	 *
-	 * Compensate it by subtracting the active area offset.
-	 */
-	info->analogCrop.x -= activeArea_.x;
-	info->analogCrop.y -= activeArea_.y;
-
-	/* The bit depth and image size depend on the currently applied format. */
-	V4L2SubdeviceFormat format{};
-	ret = subdev_->getFormat(pad_, &format);
-	if (ret)
-		return ret;
-
-	info->bitsPerPixel = MediaBusFormatInfo::info(format.code).bitsPerPixel;
-	info->outputSize = format.size;
-
-	std::optional<int32_t> cfa = properties_.get(properties::draft::ColorFilterArrangement);
-	info->cfaPattern = cfa ? *cfa : properties::draft::RGB;
-
-	/*
-	 * Retrieve the pixel rate, line length and minimum/maximum frame
-	 * duration through V4L2 controls. Support for the V4L2_CID_PIXEL_RATE,
-	 * V4L2_CID_HBLANK and V4L2_CID_VBLANK controls is mandatory.
-	 */
-	ControlList ctrls = subdev_->getControls({ V4L2_CID_PIXEL_RATE,
-						   V4L2_CID_HBLANK,
-						   V4L2_CID_VBLANK });
-	if (ctrls.empty()) {
-		LOG(CameraSensor, Error)
-			<< "Failed to retrieve camera info controls";
-		return -EINVAL;
-	}
-
-	info->pixelRate = ctrls.get(V4L2_CID_PIXEL_RATE).get<int64_t>();
-
-	const ControlInfo hblank = ctrls.infoMap()->at(V4L2_CID_HBLANK);
-	info->minLineLength = info->outputSize.width + hblank.min().get<int32_t>();
-	info->maxLineLength = info->outputSize.width + hblank.max().get<int32_t>();
-
-	const ControlInfo vblank = ctrls.infoMap()->at(V4L2_CID_VBLANK);
-	info->minFrameLength = info->outputSize.height + vblank.min().get<int32_t>();
-	info->maxFrameLength = info->outputSize.height + vblank.max().get<int32_t>();
-
-	return 0;
-}
 
 /**
+ * \fn CameraSensor::computeTransform()
  * \brief Compute the Transform that gives the requested \a orientation
  * \param[inout] orientation The desired image orientation
  *
@@ -1014,40 +245,9 @@ int CameraSensor::sensorInfo(IPACameraSensorInfo *info) const
  * \return A Transform instance that applied to the CameraSensor produces images
  * with \a orientation
  */
-Transform CameraSensor::computeTransform(Orientation *orientation) const
-{
-	/*
-	 * If we cannot do any flips we cannot change the native camera mounting
-	 * orientation.
-	 */
-	if (!supportFlips_) {
-		*orientation = mountingOrientation_;
-		return Transform::Identity;
-	}
-
-	/*
-	 * Now compute the required transform to obtain 'orientation' starting
-	 * from the mounting rotation.
-	 *
-	 * As a note:
-	 * 	orientation / mountingOrientation_ = transform
-	 * 	mountingOrientation_ * transform = orientation
-	 */
-	Transform transform = *orientation / mountingOrientation_;
-
-	/*
-	 * If transform contains any Transpose we cannot do it, so adjust
-	 * 'orientation' to report the image native orientation and return Identity.
-	 */
-	if (!!(transform & Transform::Transpose)) {
-		*orientation = mountingOrientation_;
-		return Transform::Identity;
-	}
-
-	return transform;
-}
 
 /**
+ * \fn CameraSensor::bayerOrder()
  * \brief Compute the Bayer order that results from the given Transform
  * \param[in] t The Transform to apply to the sensor
  *
@@ -1059,23 +259,9 @@ Transform CameraSensor::computeTransform(Orientation *orientation) const
  *
  * \return The Bayer order produced by the sensor when the Transform is applied
  */
-BayerFormat::Order CameraSensor::bayerOrder(Transform t) const
-{
-	/* Return a defined by meaningless value for non-Bayer sensors. */
-	if (!bayerFormat_)
-		return BayerFormat::Order::BGGR;
-
-	if (!flipsAlterBayerOrder_)
-		return bayerFormat_->order;
-
-	/*
-	 * Apply the transform to the native (i.e. untransformed) Bayer order,
-	 * using the rest of the Bayer format supplied by the caller.
-	 */
-	return bayerFormat_->transform(t).order;
-}
 
 /**
+ * \fn CameraSensor::controls()
  * \brief Retrieve the supported V4L2 controls and their information
  *
  * Control information is updated automatically to reflect the current sensor
@@ -1084,12 +270,9 @@ BayerFormat::Order CameraSensor::bayerOrder(Transform t) const
  *
  * \return A map of the V4L2 controls supported by the sensor
  */
-const ControlInfoMap &CameraSensor::controls() const
-{
-	return subdev_->controls();
-}
 
 /**
+ * \fn CameraSensor::getControls()
  * \brief Read V4L2 controls from the sensor
  * \param[in] ids The list of controls to read, specified by their ID
  *
@@ -1107,12 +290,9 @@ const ControlInfoMap &CameraSensor::controls() const
  * \return The control values in a ControlList on success, or an empty list on
  * error
  */
-ControlList CameraSensor::getControls(const std::vector<uint32_t> &ids)
-{
-	return subdev_->getControls(ids);
-}
 
 /**
+ * \fn CameraSensor::setControls()
  * \brief Write V4L2 controls to the sensor
  * \param[in] ctrls The list of controls to write
  *
@@ -1137,10 +317,6 @@ ControlList CameraSensor::getControls(const std::vector<uint32_t> &ids)
  * \retval -EINVAL One of the control is not supported or not accessible
  * \retval i The index of the control that failed
  */
-int CameraSensor::setControls(ControlList *ctrls)
-{
-	return subdev_->setControls(ctrls);
-}
 
 /**
  * \fn CameraSensor::testPatternModes()
@@ -1151,6 +327,7 @@ int CameraSensor::setControls(ControlList *ctrls)
  */
 
 /**
+ * \fn CameraSensor::setTestPatternMode()
  * \brief Set the test pattern mode for the camera sensor
  * \param[in] mode The test pattern mode
  *
@@ -1158,84 +335,6 @@ int CameraSensor::setControls(ControlList *ctrls)
  * pattern mode. Otherwise, this function is a no-op. Setting the same test
  * pattern mode for every frame thus incurs no performance penalty.
  */
-int CameraSensor::setTestPatternMode(controls::draft::TestPatternModeEnum mode)
-{
-	if (testPatternMode_ == mode)
-		return 0;
-
-	if (testPatternModes_.empty()) {
-		LOG(CameraSensor, Error)
-			<< "Camera sensor does not support test pattern modes.";
-		return -EINVAL;
-	}
-
-	return applyTestPatternMode(mode);
-}
-
-int CameraSensor::applyTestPatternMode(controls::draft::TestPatternModeEnum mode)
-{
-	if (testPatternModes_.empty())
-		return 0;
-
-	auto it = std::find(testPatternModes_.begin(), testPatternModes_.end(),
-			    mode);
-	if (it == testPatternModes_.end()) {
-		LOG(CameraSensor, Error) << "Unsupported test pattern mode "
-					 << mode;
-		return -EINVAL;
-	}
-
-	LOG(CameraSensor, Debug) << "Apply test pattern mode " << mode;
-
-	int32_t index = staticProps_->testPatternModes.at(mode);
-	ControlList ctrls{ controls() };
-	ctrls.set(V4L2_CID_TEST_PATTERN, index);
-
-	int ret = setControls(&ctrls);
-	if (ret)
-		return ret;
-
-	testPatternMode_ = mode;
-
-	return 0;
-}
-
-std::string CameraSensor::logPrefix() const
-{
-	return "'" + entity_->name() + "'";
-}
-
-namespace {
-
-/* Transitory default camera sensor implementation */
-class CameraSensorDefault : public CameraSensor
-{
-public:
-	CameraSensorDefault(MediaEntity *entity)
-		: CameraSensor(entity)
-	{
-	}
-
-	static bool match([[maybe_unused]] const MediaEntity *entity)
-	{
-		return true;
-	}
-
-	static std::unique_ptr<CameraSensorDefault> create(MediaEntity *entity)
-	{
-		std::unique_ptr<CameraSensorDefault> sensor =
-			std::make_unique<CameraSensorDefault>(entity);
-
-		if (sensor->init())
-			return nullptr;
-
-		return sensor;
-	}
-};
-
-REGISTER_CAMERA_SENSOR(CameraSensorDefault)
-
-}; /* namespace */
 
 /**
  * \class CameraSensorFactoryBase
@@ -1271,18 +370,18 @@ std::unique_ptr<CameraSensor> CameraSensorFactoryBase::create(MediaEntity *entit
 		CameraSensorFactoryBase::factories();
 
 	for (const CameraSensorFactoryBase *factory : factories) {
-		if (!factory->match(entity))
-			continue;
+		std::variant<std::unique_ptr<CameraSensor>, int> result =
+			factory->match(entity);
 
-		std::unique_ptr<CameraSensor> sensor = factory->createInstance(entity);
-		if (!sensor) {
+		if (std::holds_alternative<std::unique_ptr<CameraSensor>>(result))
+			return std::get<std::unique_ptr<CameraSensor>>(std::move(result));
+
+		if (std::get<int>(result)) {
 			LOG(CameraSensor, Error)
 				<< "Failed to create sensor for '"
-				<< entity->name();
+				<< entity->name() << ": " << std::get<int>(result);
 			return nullptr;
 		}
-
-		return sensor;
 	}
 
 	return nullptr;
@@ -1337,33 +436,27 @@ void CameraSensorFactoryBase::registerFactory(CameraSensorFactoryBase *factory)
  * function.
  */
 
-/**
- * \fn CameraSensorFactory::createInstance() const
- * \brief Create an instance of the CameraSensor corresponding to the factory
- *
- * \return A unique pointer to a newly constructed instance of the CameraSensor
- * subclass corresponding to the factory
- */
-
 /**
  * \def REGISTER_CAMERA_SENSOR(sensor)
  * \brief Register a camera sensor type to the sensor factory
  * \param[in] sensor Class name of the CameraSensor derived class to register
  *
  * Register a CameraSensor subclass with the factory and make it available to
- * try and match sensors. The subclass needs to implement two static functions:
+ * try and match sensors. The subclass needs to implement a static match
+ * function:
  *
  * \code{.cpp}
- * static bool match(const MediaEntity *entity);
- * static std::unique_ptr<sensor> create(MediaEntity *entity);
+ * static std::variant<std::unique_ptr<CameraSensor>, int> match(MediaEntity *entity);
  * \endcode
  *
- * The match() function tests if the sensor class supports the camera sensor
- * identified by a MediaEntity.
+ * The function tests if the sensor class supports the camera sensor identified
+ * by a MediaEntity. If so, it creates a new instance of the sensor class. The
+ * return value is a variant that contains
  *
- * The create() function creates a new instance of the sensor class. It may
- * return a null pointer if initialization of the instance fails. It will only
- * be called if the match() function has returned true for the given entity.
+ * - A new instance of the camera sensor class if the entity matched and
+ *   creation succeeded ;
+ * - A non-zero error code if the entity matched and the creation failed ; or
+ * - A zero error code if the entity didn't match.
  */
 
 } /* namespace libcamera */
diff --git a/src/libcamera/sensor/camera_sensor_legacy.cpp b/src/libcamera/sensor/camera_sensor_legacy.cpp
new file mode 100644
index 000000000000..34677339241c
--- /dev/null
+++ b/src/libcamera/sensor/camera_sensor_legacy.cpp
@@ -0,0 +1,1015 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2019, Google Inc.
+ *
+ * camera_sensor_legacy.cpp - A V4L2-backed camera sensor
+ */
+
+#include <algorithm>
+#include <float.h>
+#include <iomanip>
+#include <limits.h>
+#include <map>
+#include <math.h>
+#include <memory>
+#include <string.h>
+#include <string>
+#include <vector>
+
+#include <libcamera/base/class.h>
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/geometry.h>
+#include <libcamera/orientation.h>
+#include <libcamera/property_ids.h>
+#include <libcamera/transform.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/camera_lens.h"
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/camera_sensor_properties.h"
+#include "libcamera/internal/formats.h"
+#include "libcamera/internal/media_device.h"
+#include "libcamera/internal/sysfs.h"
+#include "libcamera/internal/v4l2_subdevice.h"
+
+namespace libcamera {
+
+class BayerFormat;
+class CameraLens;
+class MediaEntity;
+class SensorConfiguration;
+
+struct CameraSensorProperties;
+
+enum class Orientation;
+
+LOG_DECLARE_CATEGORY(CameraSensor)
+
+class CameraSensorLegacy : public CameraSensor, protected Loggable
+{
+public:
+	CameraSensorLegacy(const MediaEntity *entity);
+	~CameraSensorLegacy();
+
+	static std::variant<std::unique_ptr<CameraSensor>, int>
+	match(MediaEntity *entity);
+
+	const std::string &model() const override { return model_; }
+	const std::string &id() const override { return id_; }
+
+	const MediaEntity *entity() const override { return entity_; }
+	V4L2Subdevice *device() override { return subdev_.get(); }
+
+	CameraLens *focusLens() override { return focusLens_.get(); }
+
+	const std::vector<unsigned int> &mbusCodes() const override { return mbusCodes_; }
+	std::vector<Size> sizes(unsigned int mbusCode) const override;
+	Size resolution() const override;
+
+	V4L2SubdeviceFormat getFormat(const std::vector<unsigned int> &mbusCodes,
+				      const Size &size) const override;
+	int setFormat(V4L2SubdeviceFormat *format,
+		      Transform transform = Transform::Identity) override;
+	int tryFormat(V4L2SubdeviceFormat *format) const override;
+
+	int applyConfiguration(const SensorConfiguration &config,
+			       Transform transform = Transform::Identity,
+			       V4L2SubdeviceFormat *sensorFormat = nullptr) override;
+
+	const ControlList &properties() const override { return properties_; }
+	int sensorInfo(IPACameraSensorInfo *info) const override;
+	Transform computeTransform(Orientation *orientation) const override;
+	BayerFormat::Order bayerOrder(Transform t) const override;
+
+	const ControlInfoMap &controls() const override;
+	ControlList getControls(const std::vector<uint32_t> &ids) override;
+	int setControls(ControlList *ctrls) override;
+
+	const std::vector<controls::draft::TestPatternModeEnum> &
+	testPatternModes() const override { return testPatternModes_; }
+	int setTestPatternMode(controls::draft::TestPatternModeEnum mode) override;
+
+protected:
+	std::string logPrefix() const override;
+
+private:
+	LIBCAMERA_DISABLE_COPY(CameraSensorLegacy)
+
+	int init();
+	int generateId();
+	int validateSensorDriver();
+	void initVimcDefaultProperties();
+	void initStaticProperties();
+	void initTestPatternModes();
+	int initProperties();
+	int applyTestPatternMode(controls::draft::TestPatternModeEnum mode);
+	int discoverAncillaryDevices();
+
+	const MediaEntity *entity_;
+	std::unique_ptr<V4L2Subdevice> subdev_;
+	unsigned int pad_;
+
+	const CameraSensorProperties *staticProps_;
+
+	std::string model_;
+	std::string id_;
+
+	V4L2Subdevice::Formats formats_;
+	std::vector<unsigned int> mbusCodes_;
+	std::vector<Size> sizes_;
+	std::vector<controls::draft::TestPatternModeEnum> testPatternModes_;
+	controls::draft::TestPatternModeEnum testPatternMode_;
+
+	Size pixelArraySize_;
+	Rectangle activeArea_;
+	const BayerFormat *bayerFormat_;
+	bool supportFlips_;
+	bool flipsAlterBayerOrder_;
+	Orientation mountingOrientation_;
+
+	ControlList properties_;
+
+	std::unique_ptr<CameraLens> focusLens_;
+};
+
+/**
+ * \class CameraSensorLegacy
+ * \brief A camera sensor based on V4L2 subdevices
+ *
+ * The implementation is currently limited to sensors that expose a single V4L2
+ * subdevice with a single pad. It will be extended to support more complex
+ * devices as the needs arise.
+ */
+
+CameraSensorLegacy::CameraSensorLegacy(const MediaEntity *entity)
+	: entity_(entity), pad_(UINT_MAX), staticProps_(nullptr),
+	  bayerFormat_(nullptr), supportFlips_(false),
+	  flipsAlterBayerOrder_(false), properties_(properties::properties)
+{
+}
+
+CameraSensorLegacy::~CameraSensorLegacy() = default;
+
+std::variant<std::unique_ptr<CameraSensor>, int>
+CameraSensorLegacy::match(MediaEntity *entity)
+{
+	std::unique_ptr<CameraSensorLegacy> sensor =
+		std::make_unique<CameraSensorLegacy>(entity);
+
+	int ret = sensor->init();
+	if (ret)
+		return { ret };
+
+	return { std::move(sensor) };
+}
+
+int CameraSensorLegacy::init()
+{
+	for (const MediaPad *pad : entity_->pads()) {
+		if (pad->flags() & MEDIA_PAD_FL_SOURCE) {
+			pad_ = pad->index();
+			break;
+		}
+	}
+
+	if (pad_ == UINT_MAX) {
+		LOG(CameraSensor, Error)
+			<< "Sensors with more than one pad are not supported";
+		return -EINVAL;
+	}
+
+	switch (entity_->function()) {
+	case MEDIA_ENT_F_CAM_SENSOR:
+	case MEDIA_ENT_F_PROC_VIDEO_ISP:
+		break;
+
+	default:
+		LOG(CameraSensor, Error)
+			<< "Invalid sensor function "
+			<< utils::hex(entity_->function());
+		return -EINVAL;
+	}
+
+	/* Create and open the subdev. */
+	subdev_ = std::make_unique<V4L2Subdevice>(entity_);
+	int ret = subdev_->open();
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Clear any flips to be sure we get the "native" Bayer order. This is
+	 * harmless for sensors where the flips don't affect the Bayer order.
+	 */
+	ControlList ctrls(subdev_->controls());
+	if (subdev_->controls().find(V4L2_CID_HFLIP) != subdev_->controls().end())
+		ctrls.set(V4L2_CID_HFLIP, 0);
+	if (subdev_->controls().find(V4L2_CID_VFLIP) != subdev_->controls().end())
+		ctrls.set(V4L2_CID_VFLIP, 0);
+	subdev_->setControls(&ctrls);
+
+	/* Enumerate, sort and cache media bus codes and sizes. */
+	formats_ = subdev_->formats(pad_);
+	if (formats_.empty()) {
+		LOG(CameraSensor, Error) << "No image format found";
+		return -EINVAL;
+	}
+
+	mbusCodes_ = utils::map_keys(formats_);
+	std::sort(mbusCodes_.begin(), mbusCodes_.end());
+
+	for (const auto &format : formats_) {
+		const std::vector<SizeRange> &ranges = format.second;
+		std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes_),
+			       [](const SizeRange &range) { return range.max; });
+	}
+
+	std::sort(sizes_.begin(), sizes_.end());
+
+	/* Remove duplicates. */
+	auto last = std::unique(sizes_.begin(), sizes_.end());
+	sizes_.erase(last, sizes_.end());
+
+	/*
+	 * VIMC is a bit special, as it does not yet support all the mandatory
+	 * requirements regular sensors have to respect.
+	 *
+	 * Do not validate the driver if it's VIMC and initialize the sensor
+	 * properties with static information.
+	 *
+	 * \todo Remove the special case once the VIMC driver has been
+	 * updated in all test platforms.
+	 */
+	if (entity_->device()->driver() == "vimc") {
+		initVimcDefaultProperties();
+
+		ret = initProperties();
+		if (ret)
+			return ret;
+
+		return discoverAncillaryDevices();
+	}
+
+	/* Get the color filter array pattern (only for RAW sensors). */
+	for (unsigned int mbusCode : mbusCodes_) {
+		const BayerFormat &bayerFormat = BayerFormat::fromMbusCode(mbusCode);
+		if (bayerFormat.isValid()) {
+			bayerFormat_ = &bayerFormat;
+			break;
+		}
+	}
+
+	ret = validateSensorDriver();
+	if (ret)
+		return ret;
+
+	ret = initProperties();
+	if (ret)
+		return ret;
+
+	ret = discoverAncillaryDevices();
+	if (ret)
+		return ret;
+
+	/*
+	 * Set HBLANK to the minimum to start with a well-defined line length,
+	 * allowing IPA modules that do not modify HBLANK to use the sensor
+	 * minimum line length in their calculations.
+	 */
+	const struct v4l2_query_ext_ctrl *hblankInfo = subdev_->controlInfo(V4L2_CID_HBLANK);
+	if (hblankInfo && !(hblankInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) {
+		ControlList ctrl(subdev_->controls());
+
+		ctrl.set(V4L2_CID_HBLANK, static_cast<int32_t>(hblankInfo->minimum));
+		ret = subdev_->setControls(&ctrl);
+		if (ret)
+			return ret;
+	}
+
+	return applyTestPatternMode(controls::draft::TestPatternModeEnum::TestPatternModeOff);
+}
+
+int CameraSensorLegacy::generateId()
+{
+	const std::string devPath = subdev_->devicePath();
+
+	/* Try to get ID from firmware description. */
+	id_ = sysfs::firmwareNodePath(devPath);
+	if (!id_.empty())
+		return 0;
+
+	/*
+	 * Virtual sensors not described in firmware
+	 *
+	 * Verify it's a platform device and construct ID from the device path
+	 * and model of sensor.
+	 */
+	if (devPath.find("/sys/devices/platform/", 0) == 0) {
+		id_ = devPath.substr(strlen("/sys/devices/")) + " " + model();
+		return 0;
+	}
+
+	LOG(CameraSensor, Error) << "Can't generate sensor ID";
+	return -EINVAL;
+}
+
+int CameraSensorLegacy::validateSensorDriver()
+{
+	int err = 0;
+
+	/*
+	 * Optional controls are used to register optional sensor properties. If
+	 * not present, some values will be defaulted.
+	 */
+	static constexpr uint32_t optionalControls[] = {
+		V4L2_CID_CAMERA_SENSOR_ROTATION,
+	};
+
+	const ControlIdMap &controls = subdev_->controls().idmap();
+	for (uint32_t ctrl : optionalControls) {
+		if (!controls.count(ctrl))
+			LOG(CameraSensor, Debug)
+				<< "Optional V4L2 control " << utils::hex(ctrl)
+				<< " not supported";
+	}
+
+	/*
+	 * Recommended controls are similar to optional controls, but will
+	 * become mandatory in the near future. Be loud if they're missing.
+	 */
+	static constexpr uint32_t recommendedControls[] = {
+		V4L2_CID_CAMERA_ORIENTATION,
+	};
+
+	for (uint32_t ctrl : recommendedControls) {
+		if (!controls.count(ctrl)) {
+			LOG(CameraSensor, Warning)
+				<< "Recommended V4L2 control " << utils::hex(ctrl)
+				<< " not supported";
+			err = -EINVAL;
+		}
+	}
+
+	/*
+	 * Verify if sensor supports horizontal/vertical flips
+	 *
+	 * \todo Handle horizontal and vertical flips independently.
+	 */
+	const struct v4l2_query_ext_ctrl *hflipInfo = subdev_->controlInfo(V4L2_CID_HFLIP);
+	const struct v4l2_query_ext_ctrl *vflipInfo = subdev_->controlInfo(V4L2_CID_VFLIP);
+	if (hflipInfo && !(hflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY) &&
+	    vflipInfo && !(vflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) {
+		supportFlips_ = true;
+
+		if (hflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT ||
+		    vflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT)
+			flipsAlterBayerOrder_ = true;
+	}
+
+	if (!supportFlips_)
+		LOG(CameraSensor, Debug)
+			<< "Camera sensor does not support horizontal/vertical flip";
+
+	/*
+	 * Make sure the required selection targets are supported.
+	 *
+	 * Failures in reading any of the targets are not deemed to be fatal,
+	 * but some properties and features, like constructing a
+	 * IPACameraSensorInfo for the IPA module, won't be supported.
+	 *
+	 * \todo Make support for selection targets mandatory as soon as all
+	 * test platforms have been updated.
+	 */
+	Rectangle rect;
+	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_BOUNDS, &rect);
+	if (ret) {
+		/*
+		 * Default the pixel array size to the largest size supported
+		 * by the sensor. The sizes_ vector is sorted in ascending
+		 * order, the largest size is thus the last element.
+		 */
+		pixelArraySize_ = sizes_.back();
+
+		LOG(CameraSensor, Warning)
+			<< "The PixelArraySize property has been defaulted to "
+			<< pixelArraySize_;
+		err = -EINVAL;
+	} else {
+		pixelArraySize_ = rect.size();
+	}
+
+	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_DEFAULT, &activeArea_);
+	if (ret) {
+		activeArea_ = Rectangle(pixelArraySize_);
+		LOG(CameraSensor, Warning)
+			<< "The PixelArrayActiveAreas property has been defaulted to "
+			<< activeArea_;
+		err = -EINVAL;
+	}
+
+	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &rect);
+	if (ret) {
+		LOG(CameraSensor, Warning)
+			<< "Failed to retrieve the sensor crop rectangle";
+		err = -EINVAL;
+	}
+
+	if (err) {
+		LOG(CameraSensor, Warning)
+			<< "The sensor kernel driver needs to be fixed";
+		LOG(CameraSensor, Warning)
+			<< "See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information";
+	}
+
+	if (!bayerFormat_)
+		return 0;
+
+	/*
+	 * For raw sensors, make sure the sensor driver supports the controls
+	 * required by the CameraSensor class.
+	 */
+	static constexpr uint32_t mandatoryControls[] = {
+		V4L2_CID_ANALOGUE_GAIN,
+		V4L2_CID_EXPOSURE,
+		V4L2_CID_HBLANK,
+		V4L2_CID_PIXEL_RATE,
+		V4L2_CID_VBLANK,
+	};
+
+	err = 0;
+	for (uint32_t ctrl : mandatoryControls) {
+		if (!controls.count(ctrl)) {
+			LOG(CameraSensor, Error)
+				<< "Mandatory V4L2 control " << utils::hex(ctrl)
+				<< " not available";
+			err = -EINVAL;
+		}
+	}
+
+	if (err) {
+		LOG(CameraSensor, Error)
+			<< "The sensor kernel driver needs to be fixed";
+		LOG(CameraSensor, Error)
+			<< "See Documentation/sensor_driver_requirements.rst in the libcamera sources for more information";
+		return err;
+	}
+
+	return 0;
+}
+
+void CameraSensorLegacy::initVimcDefaultProperties()
+{
+	/* Use the largest supported size. */
+	pixelArraySize_ = sizes_.back();
+	activeArea_ = Rectangle(pixelArraySize_);
+}
+
+void CameraSensorLegacy::initStaticProperties()
+{
+	staticProps_ = CameraSensorProperties::get(model_);
+	if (!staticProps_)
+		return;
+
+	/* Register the properties retrieved from the sensor database. */
+	properties_.set(properties::UnitCellSize, staticProps_->unitCellSize);
+
+	initTestPatternModes();
+}
+
+void CameraSensorLegacy::initTestPatternModes()
+{
+	const auto &v4l2TestPattern = controls().find(V4L2_CID_TEST_PATTERN);
+	if (v4l2TestPattern == controls().end()) {
+		LOG(CameraSensor, Debug) << "V4L2_CID_TEST_PATTERN is not supported";
+		return;
+	}
+
+	const auto &testPatternModes = staticProps_->testPatternModes;
+	if (testPatternModes.empty()) {
+		/*
+		 * The camera sensor supports test patterns but we don't know
+		 * how to map them so this should be fixed.
+		 */
+		LOG(CameraSensor, Debug) << "No static test pattern map for \'"
+					 << model() << "\'";
+		return;
+	}
+
+	/*
+	 * Create a map that associates the V4L2 control index to the test
+	 * pattern mode by reversing the testPatternModes map provided by the
+	 * camera sensor properties. This makes it easier to verify if the
+	 * control index is supported in the below for loop that creates the
+	 * list of supported test patterns.
+	 */
+	std::map<int32_t, controls::draft::TestPatternModeEnum> indexToTestPatternMode;
+	for (const auto &it : testPatternModes)
+		indexToTestPatternMode[it.second] = it.first;
+
+	for (const ControlValue &value : v4l2TestPattern->second.values()) {
+		const int32_t index = value.get<int32_t>();
+
+		const auto it = indexToTestPatternMode.find(index);
+		if (it == indexToTestPatternMode.end()) {
+			LOG(CameraSensor, Debug)
+				<< "Test pattern mode " << index << " ignored";
+			continue;
+		}
+
+		testPatternModes_.push_back(it->second);
+	}
+}
+
+int CameraSensorLegacy::initProperties()
+{
+	model_ = subdev_->model();
+	properties_.set(properties::Model, utils::toAscii(model_));
+
+	/* Generate a unique ID for the sensor. */
+	int ret = generateId();
+	if (ret)
+		return ret;
+
+	/* Initialize the static properties from the sensor database. */
+	initStaticProperties();
+
+	/* Retrieve and register properties from the kernel interface. */
+	const ControlInfoMap &controls = subdev_->controls();
+
+	const auto &orientation = controls.find(V4L2_CID_CAMERA_ORIENTATION);
+	if (orientation != controls.end()) {
+		int32_t v4l2Orientation = orientation->second.def().get<int32_t>();
+		int32_t propertyValue;
+
+		switch (v4l2Orientation) {
+		default:
+			LOG(CameraSensor, Warning)
+				<< "Unsupported camera location "
+				<< v4l2Orientation << ", setting to External";
+			[[fallthrough]];
+		case V4L2_CAMERA_ORIENTATION_EXTERNAL:
+			propertyValue = properties::CameraLocationExternal;
+			break;
+		case V4L2_CAMERA_ORIENTATION_FRONT:
+			propertyValue = properties::CameraLocationFront;
+			break;
+		case V4L2_CAMERA_ORIENTATION_BACK:
+			propertyValue = properties::CameraLocationBack;
+			break;
+		}
+		properties_.set(properties::Location, propertyValue);
+	} else {
+		LOG(CameraSensor, Warning) << "Failed to retrieve the camera location";
+	}
+
+	const auto &rotationControl = controls.find(V4L2_CID_CAMERA_SENSOR_ROTATION);
+	if (rotationControl != controls.end()) {
+		int32_t propertyValue = rotationControl->second.def().get<int32_t>();
+
+		/*
+		 * Cache the Transform associated with the camera mounting
+		 * rotation for later use in computeTransform().
+		 */
+		bool success;
+		mountingOrientation_ = orientationFromRotation(propertyValue, &success);
+		if (!success) {
+			LOG(CameraSensor, Warning)
+				<< "Invalid rotation of " << propertyValue
+				<< " degrees - ignoring";
+			mountingOrientation_ = Orientation::Rotate0;
+		}
+
+		properties_.set(properties::Rotation, propertyValue);
+	} else {
+		LOG(CameraSensor, Warning)
+			<< "Rotation control not available, default to 0 degrees";
+		properties_.set(properties::Rotation, 0);
+		mountingOrientation_ = Orientation::Rotate0;
+	}
+
+	properties_.set(properties::PixelArraySize, pixelArraySize_);
+	properties_.set(properties::PixelArrayActiveAreas, { activeArea_ });
+
+	/* Color filter array pattern, register only for RAW sensors. */
+	if (bayerFormat_) {
+		int32_t cfa;
+		switch (bayerFormat_->order) {
+		case BayerFormat::BGGR:
+			cfa = properties::draft::BGGR;
+			break;
+		case BayerFormat::GBRG:
+			cfa = properties::draft::GBRG;
+			break;
+		case BayerFormat::GRBG:
+			cfa = properties::draft::GRBG;
+			break;
+		case BayerFormat::RGGB:
+			cfa = properties::draft::RGGB;
+			break;
+		case BayerFormat::MONO:
+			cfa = properties::draft::MONO;
+			break;
+		}
+
+		properties_.set(properties::draft::ColorFilterArrangement, cfa);
+	}
+
+	return 0;
+}
+
+int CameraSensorLegacy::discoverAncillaryDevices()
+{
+	int ret;
+
+	for (MediaEntity *ancillary : entity_->ancillaryEntities()) {
+		switch (ancillary->function()) {
+		case MEDIA_ENT_F_LENS:
+			focusLens_ = std::make_unique<CameraLens>(ancillary);
+			ret = focusLens_->init();
+			if (ret) {
+				LOG(CameraSensor, Error)
+					<< "Lens initialisation failed, lens disabled";
+				focusLens_.reset();
+			}
+			break;
+
+		default:
+			LOG(CameraSensor, Warning)
+				<< "Unsupported ancillary entity function "
+				<< ancillary->function();
+			break;
+		}
+	}
+
+	return 0;
+}
+
+std::vector<Size> CameraSensorLegacy::sizes(unsigned int mbusCode) const
+{
+	std::vector<Size> sizes;
+
+	const auto &format = formats_.find(mbusCode);
+	if (format == formats_.end())
+		return sizes;
+
+	const std::vector<SizeRange> &ranges = format->second;
+	std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes),
+		       [](const SizeRange &range) { return range.max; });
+
+	std::sort(sizes.begin(), sizes.end());
+
+	return sizes;
+}
+
+Size CameraSensorLegacy::resolution() const
+{
+	return std::min(sizes_.back(), activeArea_.size());
+}
+
+V4L2SubdeviceFormat
+CameraSensorLegacy::getFormat(const std::vector<unsigned int> &mbusCodes,
+			      const Size &size) const
+{
+	unsigned int desiredArea = size.width * size.height;
+	unsigned int bestArea = UINT_MAX;
+	float desiredRatio = static_cast<float>(size.width) / size.height;
+	float bestRatio = FLT_MAX;
+	const Size *bestSize = nullptr;
+	uint32_t bestCode = 0;
+
+	for (unsigned int code : mbusCodes) {
+		const auto formats = formats_.find(code);
+		if (formats == formats_.end())
+			continue;
+
+		for (const SizeRange &range : formats->second) {
+			const Size &sz = range.max;
+
+			if (sz.width < size.width || sz.height < size.height)
+				continue;
+
+			float ratio = static_cast<float>(sz.width) / sz.height;
+			float ratioDiff = fabsf(ratio - desiredRatio);
+			unsigned int area = sz.width * sz.height;
+			unsigned int areaDiff = area - desiredArea;
+
+			if (ratioDiff > bestRatio)
+				continue;
+
+			if (ratioDiff < bestRatio || areaDiff < bestArea) {
+				bestRatio = ratioDiff;
+				bestArea = areaDiff;
+				bestSize = &sz;
+				bestCode = code;
+			}
+		}
+	}
+
+	if (!bestSize) {
+		LOG(CameraSensor, Debug) << "No supported format or size found";
+		return {};
+	}
+
+	V4L2SubdeviceFormat format{
+		.code = bestCode,
+		.size = *bestSize,
+		.colorSpace = ColorSpace::Raw,
+	};
+
+	return format;
+}
+
+int CameraSensorLegacy::setFormat(V4L2SubdeviceFormat *format, Transform transform)
+{
+	/* Configure flips if the sensor supports that. */
+	if (supportFlips_) {
+		ControlList flipCtrls(subdev_->controls());
+
+		flipCtrls.set(V4L2_CID_HFLIP,
+			      static_cast<int32_t>(!!(transform & Transform::HFlip)));
+		flipCtrls.set(V4L2_CID_VFLIP,
+			      static_cast<int32_t>(!!(transform & Transform::VFlip)));
+
+		int ret = subdev_->setControls(&flipCtrls);
+		if (ret)
+			return ret;
+	}
+
+	/* Apply format on the subdev. */
+	int ret = subdev_->setFormat(pad_, format);
+	if (ret)
+		return ret;
+
+	subdev_->updateControlInfo();
+	return 0;
+}
+
+int CameraSensorLegacy::tryFormat(V4L2SubdeviceFormat *format) const
+{
+	return subdev_->setFormat(pad_, format,
+				  V4L2Subdevice::Whence::TryFormat);
+}
+
+int CameraSensorLegacy::applyConfiguration(const SensorConfiguration &config,
+					   Transform transform,
+					   V4L2SubdeviceFormat *sensorFormat)
+{
+	if (!config.isValid()) {
+		LOG(CameraSensor, Error) << "Invalid sensor configuration";
+		return -EINVAL;
+	}
+
+	std::vector<unsigned int> filteredCodes;
+	std::copy_if(mbusCodes_.begin(), mbusCodes_.end(),
+		     std::back_inserter(filteredCodes),
+		     [&config](unsigned int mbusCode) {
+			     BayerFormat bayer = BayerFormat::fromMbusCode(mbusCode);
+			     if (bayer.bitDepth == config.bitDepth)
+				     return true;
+			     return false;
+		     });
+	if (filteredCodes.empty()) {
+		LOG(CameraSensor, Error)
+			<< "Cannot find any format with bit depth "
+			<< config.bitDepth;
+		return -EINVAL;
+	}
+
+	/*
+	 * Compute the sensor's data frame size by applying the cropping
+	 * rectangle, subsampling and output crop to the sensor's pixel array
+	 * size.
+	 *
+	 * \todo The actual size computation is for now ignored and only the
+	 * output size is considered. This implies that resolutions obtained
+	 * with two different cropping/subsampling will look identical and
+	 * only the first found one will be considered.
+	 */
+	V4L2SubdeviceFormat subdevFormat = {};
+	for (unsigned int code : filteredCodes) {
+		for (const Size &size : sizes(code)) {
+			if (size.width != config.outputSize.width ||
+			    size.height != config.outputSize.height)
+				continue;
+
+			subdevFormat.code = code;
+			subdevFormat.size = size;
+			break;
+		}
+	}
+	if (!subdevFormat.code) {
+		LOG(CameraSensor, Error) << "Invalid output size in sensor configuration";
+		return -EINVAL;
+	}
+
+	int ret = setFormat(&subdevFormat, transform);
+	if (ret)
+		return ret;
+
+	/*
+	 * Return to the caller the format actually applied to the sensor.
+	 * This is relevant if transform has changed the bayer pattern order.
+	 */
+	if (sensorFormat)
+		*sensorFormat = subdevFormat;
+
+	/* \todo Handle AnalogCrop. Most sensors do not support set_selection */
+	/* \todo Handle scaling in the digital domain. */
+
+	return 0;
+}
+
+int CameraSensorLegacy::sensorInfo(IPACameraSensorInfo *info) const
+{
+	if (!bayerFormat_)
+		return -EINVAL;
+
+	info->model = model();
+
+	/*
+	 * The active area size is a static property, while the crop
+	 * rectangle needs to be re-read as it depends on the sensor
+	 * configuration.
+	 */
+	info->activeAreaSize = { activeArea_.width, activeArea_.height };
+
+	/*
+	 * \todo Support for retreiving the crop rectangle is scheduled to
+	 * become mandatory. For the time being use the default value if it has
+	 * been initialized at sensor driver validation time.
+	 */
+	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &info->analogCrop);
+	if (ret) {
+		info->analogCrop = activeArea_;
+		LOG(CameraSensor, Warning)
+			<< "The analogue crop rectangle has been defaulted to the active area size";
+	}
+
+	/*
+	 * IPACameraSensorInfo::analogCrop::x and IPACameraSensorInfo::analogCrop::y
+	 * are defined relatively to the active pixel area, while V4L2's
+	 * TGT_CROP target is defined in respect to the full pixel array.
+	 *
+	 * Compensate it by subtracting the active area offset.
+	 */
+	info->analogCrop.x -= activeArea_.x;
+	info->analogCrop.y -= activeArea_.y;
+
+	/* The bit depth and image size depend on the currently applied format. */
+	V4L2SubdeviceFormat format{};
+	ret = subdev_->getFormat(pad_, &format);
+	if (ret)
+		return ret;
+	info->bitsPerPixel = MediaBusFormatInfo::info(format.code).bitsPerPixel;
+	info->outputSize = format.size;
+
+	std::optional<int32_t> cfa = properties_.get(properties::draft::ColorFilterArrangement);
+	info->cfaPattern = cfa ? *cfa : properties::draft::RGB;
+
+	/*
+	 * Retrieve the pixel rate, line length and minimum/maximum frame
+	 * duration through V4L2 controls. Support for the V4L2_CID_PIXEL_RATE,
+	 * V4L2_CID_HBLANK and V4L2_CID_VBLANK controls is mandatory.
+	 */
+	ControlList ctrls = subdev_->getControls({ V4L2_CID_PIXEL_RATE,
+						   V4L2_CID_HBLANK,
+						   V4L2_CID_VBLANK });
+	if (ctrls.empty()) {
+		LOG(CameraSensor, Error)
+			<< "Failed to retrieve camera info controls";
+		return -EINVAL;
+	}
+
+	info->pixelRate = ctrls.get(V4L2_CID_PIXEL_RATE).get<int64_t>();
+
+	const ControlInfo hblank = ctrls.infoMap()->at(V4L2_CID_HBLANK);
+	info->minLineLength = info->outputSize.width + hblank.min().get<int32_t>();
+	info->maxLineLength = info->outputSize.width + hblank.max().get<int32_t>();
+
+	const ControlInfo vblank = ctrls.infoMap()->at(V4L2_CID_VBLANK);
+	info->minFrameLength = info->outputSize.height + vblank.min().get<int32_t>();
+	info->maxFrameLength = info->outputSize.height + vblank.max().get<int32_t>();
+
+	return 0;
+}
+
+Transform CameraSensorLegacy::computeTransform(Orientation *orientation) const
+{
+	/*
+	 * If we cannot do any flips we cannot change the native camera mounting
+	 * orientation.
+	 */
+	if (!supportFlips_) {
+		*orientation = mountingOrientation_;
+		return Transform::Identity;
+	}
+
+	/*
+	 * Now compute the required transform to obtain 'orientation' starting
+	 * from the mounting rotation.
+	 *
+	 * As a note:
+	 * 	orientation / mountingOrientation_ = transform
+	 * 	mountingOrientation_ * transform = orientation
+	 */
+	Transform transform = *orientation / mountingOrientation_;
+
+	/*
+	 * If transform contains any Transpose we cannot do it, so adjust
+	 * 'orientation' to report the image native orientation and return Identity.
+	 */
+	if (!!(transform & Transform::Transpose)) {
+		*orientation = mountingOrientation_;
+		return Transform::Identity;
+	}
+
+	return transform;
+}
+
+BayerFormat::Order CameraSensorLegacy::bayerOrder(Transform t) const
+{
+	/* Return a defined by meaningless value for non-Bayer sensors. */
+	if (!bayerFormat_)
+		return BayerFormat::Order::BGGR;
+
+	if (!flipsAlterBayerOrder_)
+		return bayerFormat_->order;
+
+	/*
+	 * Apply the transform to the native (i.e. untransformed) Bayer order,
+	 * using the rest of the Bayer format supplied by the caller.
+	 */
+	return bayerFormat_->transform(t).order;
+}
+
+const ControlInfoMap &CameraSensorLegacy::controls() const
+{
+	return subdev_->controls();
+}
+
+ControlList CameraSensorLegacy::getControls(const std::vector<uint32_t> &ids)
+{
+	return subdev_->getControls(ids);
+}
+
+int CameraSensorLegacy::setControls(ControlList *ctrls)
+{
+	return subdev_->setControls(ctrls);
+}
+
+int CameraSensorLegacy::setTestPatternMode(controls::draft::TestPatternModeEnum mode)
+{
+	if (testPatternMode_ == mode)
+		return 0;
+
+	if (testPatternModes_.empty()) {
+		LOG(CameraSensor, Error)
+			<< "Camera sensor does not support test pattern modes.";
+		return -EINVAL;
+	}
+
+	return applyTestPatternMode(mode);
+}
+
+int CameraSensorLegacy::applyTestPatternMode(controls::draft::TestPatternModeEnum mode)
+{
+	if (testPatternModes_.empty())
+		return 0;
+
+	auto it = std::find(testPatternModes_.begin(), testPatternModes_.end(),
+			    mode);
+	if (it == testPatternModes_.end()) {
+		LOG(CameraSensor, Error) << "Unsupported test pattern mode "
+					 << mode;
+		return -EINVAL;
+	}
+
+	LOG(CameraSensor, Debug) << "Apply test pattern mode " << mode;
+
+	int32_t index = staticProps_->testPatternModes.at(mode);
+	ControlList ctrls{ controls() };
+	ctrls.set(V4L2_CID_TEST_PATTERN, index);
+
+	int ret = setControls(&ctrls);
+	if (ret)
+		return ret;
+
+	testPatternMode_ = mode;
+
+	return 0;
+}
+
+std::string CameraSensorLegacy::logPrefix() const
+{
+	return "'" + entity_->name() + "'";
+}
+
+REGISTER_CAMERA_SENSOR(CameraSensorLegacy)
+
+} /* namespace libcamera */
diff --git a/src/libcamera/sensor/meson.build b/src/libcamera/sensor/meson.build
index bf4b131a94b1..e83020fc22c3 100644
--- a/src/libcamera/sensor/meson.build
+++ b/src/libcamera/sensor/meson.build
@@ -2,5 +2,6 @@
 
 libcamera_sources += files([
     'camera_sensor.cpp',
+    'camera_sensor_legacy.cpp',
     'camera_sensor_properties.cpp',
 ])
