[v5,03/11] libcamera: v4l2_subdevice: Return optional selection rectangles
diff mbox series

Message ID 20261029152058.896209-4-halfline@gmail.com
State Rejected
Headers show
Series
  • software-isp: Align metering and report the configured crop
Related show

Commit Message

Ray Strode Oct. 29, 2026, 3:20 p.m. UTC
From: Ray Strode <rstrode@redhat.com>

This commit distinguishes an unavailable selection rectangle from an ioctl
failure by returning the rectangle through std::optional.

The query returns 0 and an empty optional for EINVAL or ENOTTY from
G_SELECTION and propagates other ioctl errors. The output is cleared on
entry so a missing rectangle or failed query cannot leave a previous result
behind.

Invalid pads and nonzero streams on devices without streams support are
rejected before the ioctl. The cached capabilities are initialized so the
check is also safe before open(). EINVAL from the driver still means only
that no rectangle was reported for the current configuration.

Callers handle the empty result explicitly. Raw sensors still require their
selection rectangles. Legacy sensors retain defaults for unavailable
selections and propagate query errors during driver validation and
sensorInfo(), so failed reads cannot produce default geometry.

Signed-off-by: Ray Strode <rstrode@redhat.com>
---
 include/libcamera/internal/v4l2_subdevice.h   |  5 ++-
 src/libcamera/sensor/camera_sensor_legacy.cpp | 42 ++++++++++++++-----
 src/libcamera/sensor/camera_sensor_raw.cpp    | 31 ++++++++------
 src/libcamera/v4l2_subdevice.cpp              | 40 +++++++++++++-----
 4 files changed, 82 insertions(+), 36 deletions(-)

Patch
diff mbox series

diff --git a/include/libcamera/internal/v4l2_subdevice.h b/include/libcamera/internal/v4l2_subdevice.h
index c37a82af..f0818d6d 100644
--- a/include/libcamera/internal/v4l2_subdevice.h
+++ b/include/libcamera/internal/v4l2_subdevice.h
@@ -96,62 +96,63 @@  public:
 		unsigned int pad;
 		unsigned int stream;
 	};
 
 	struct Route {
 		Route()
 			: flags(0)
 		{
 		}
 
 		Route(const Stream &snk, const Stream &src, uint32_t f)
 			: sink(snk), source(src), flags(f)
 		{
 		}
 
 		Stream sink;
 		Stream source;
 		uint32_t flags;
 	};
 
 	using Routing = std::vector<Route>;
 
 	explicit V4L2Subdevice(const MediaEntity *entity);
 	~V4L2Subdevice();
 
 	int open();
 
 	const MediaEntity *entity() const { return entity_; }
 
 	int getSelection(const Stream &stream, unsigned int target,
-			 Rectangle *rect);
-	int getSelection(unsigned int pad, unsigned int target, Rectangle *rect)
+			 std::optional<Rectangle> *rect);
+	int getSelection(unsigned int pad, unsigned int target,
+			 std::optional<Rectangle> *rect)
 	{
 		return getSelection({ pad, 0 }, target, rect);
 	}
 	int setSelection(const Stream &stream, unsigned int target,
 			 Rectangle *rect);
 	int setSelection(unsigned int pad, unsigned int target, Rectangle *rect)
 	{
 		return setSelection({ pad, 0 }, target, rect);
 	}
 
 	Formats formats(const Stream &stream);
 	Formats formats(unsigned int pad)
 	{
 		return formats({ pad, 0 });
 	}
 
 	int getFormat(const Stream &stream, V4L2SubdeviceFormat *format,
 		      Whence whence = ActiveFormat);
 	int getFormat(unsigned int pad, V4L2SubdeviceFormat *format,
 		      Whence whence = ActiveFormat)
 	{
 		return getFormat({ pad, 0 }, format, whence);
 	}
 	int setFormat(const Stream &stream, V4L2SubdeviceFormat *format,
 		      Whence whence = ActiveFormat);
 	int setFormat(unsigned int pad, V4L2SubdeviceFormat *format,
 		      Whence whence = ActiveFormat)
 	{
 		return setFormat({ pad, 0 }, format, whence);
 	}
diff --git a/src/libcamera/sensor/camera_sensor_legacy.cpp b/src/libcamera/sensor/camera_sensor_legacy.cpp
index 792e4b09..570372a3 100644
--- a/src/libcamera/sensor/camera_sensor_legacy.cpp
+++ b/src/libcamera/sensor/camera_sensor_legacy.cpp
@@ -1,44 +1,45 @@ 
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
  * Copyright (C) 2019, Google Inc.
  *
  * A V4L2-backed camera sensor
  */
 
 #include <algorithm>
 #include <cmath>
 #include <float.h>
 #include <iomanip>
 #include <limits.h>
 #include <map>
 #include <memory>
+#include <optional>
 #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;
@@ -363,103 +364,117 @@  int CameraSensorLegacy::validateSensorDriver()
 	}
 
 	/* Verify if sensor supports horizontal/vertical flips. */
 	auto queryFlip = [&](uint32_t id, const char *name) -> std::pair<bool, bool> {
 		const struct v4l2_query_ext_ctrl *flipInfo = subdev_->controlInfo(id);
 		if (!flipInfo || flipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY) {
 			LOG(CameraSensor, Debug)
 				<< "Camera sensor does not support " << name << " flip";
 			return { false, false };
 		}
 
 		return { true, flipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT };
 	};
 
 	auto hflip = queryFlip(V4L2_CID_HFLIP, "horizontal");
 	auto vflip = queryFlip(V4L2_CID_VFLIP, "vertical");
 
 	if (hflip.first && vflip.first && (hflip.second != vflip.second)) {
 		LOG(CameraSensor, Error)
 			<< "Flips with differing alter bayer order properties"
 			<< " are unsupported. Disabling flipping.";
 	} else {
 		supportHFlips_ = hflip.first;
 		supportVFlips_ = vflip.first;
 		flipsAlterBayerOrder_ = hflip.second || vflip.second;
 	}
 
 	/*
 	 * Make sure the required selection targets are supported.
 	 *
-	 * Failures in reading any of the targets are not deemed to be fatal,
+	 * Unavailable selection 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.
 	 */
-	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_NATIVE_SIZE, &pixelArrayArea_);
-	if (ret) {
+	std::optional<Rectangle> rect;
+	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_NATIVE_SIZE, &rect);
+	if (ret)
+		return ret;
+	if (!rect) {
 		/*
 		 * 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.
 		 */
 		pixelArrayArea_ = Rectangle(sizes_.back());
 
 		LOG(CameraSensor, Warning)
 			<< "The PixelArrayArea property has been defaulted to "
 			<< pixelArrayArea_;
 		err = -EINVAL;
+	} else {
+		pixelArrayArea_ = *rect;
 	}
 
-	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_BOUNDS, &readableArea_);
-	if (ret) {
+	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_BOUNDS, &rect);
+	if (ret)
+		return ret;
+	if (!rect) {
 		readableArea_ = pixelArrayArea_;
 		LOG(CameraSensor, Warning)
 			<< "The PixelArrayReadableArea property has been defaulted to "
 			<< readableArea_;
 		err = -EINVAL;
+	} else {
+		readableArea_ = *rect;
 	}
 
-	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_DEFAULT, &activeArea_);
-	if (ret) {
+	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP_DEFAULT, &rect);
+	if (ret)
+		return ret;
+	if (!rect) {
 		activeArea_ = readableArea_;
 		LOG(CameraSensor, Warning)
 			<< "The PixelArrayActiveAreas property has been defaulted to "
 			<< activeArea_;
 		err = -EINVAL;
+	} else {
+		activeArea_ = *rect;
 	}
 
-	Rectangle rect;
 	ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &rect);
-	if (ret) {
+	if (ret)
+		return ret;
+	if (!rect) {
 		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)) {
@@ -871,65 +886,70 @@  int CameraSensorLegacy::applyConfiguration(const SensorConfiguration &config,
 	/* \todo Handle AnalogCrop. Most sensors do not support set_selection */
 	/* \todo Handle scaling in the digital domain. */
 
 	return 0;
 }
 
 V4L2Subdevice::Stream CameraSensorLegacy::imageStream() const
 {
 	return { pad_, 0 };
 }
 
 int CameraSensorLegacy::sensorInfo(IPACameraSensorInfo *info) const
 {
 	if (!bayerFormat_)
 		return -EINVAL;
 
 	info->model = model();
 	info->pixelArrayArea = pixelArrayArea_;
 
 	/*
 	 * The active area is a static property, while the crop rectangle needs
 	 * to be re-read as it depends on the sensor configuration.
 	 */
 	info->activeArea = activeArea_;
 
 	/*
 	 * \todo Support for retrieving 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) {
+	std::optional<Rectangle> crop;
+	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &crop);
+	if (ret)
+		return ret;
+	if (!crop) {
 		info->analogCrop = activeArea_;
 		LOG(CameraSensor, Warning)
 			<< "The analogue crop rectangle has been defaulted to the active area size";
+	} else {
+		info->analogCrop = *crop;
 	}
 
 	/* 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::ColorFilterArrangementRGB;
 
 	/*
 	 * 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.
 	 */
 	static constexpr uint32_t cids[] = {
 		V4L2_CID_PIXEL_RATE,
 		V4L2_CID_HBLANK,
 		V4L2_CID_VBLANK,
 	};
 
 	ControlList ctrls = subdev_->getControls(cids);
 	if (ctrls.empty()) {
 		LOG(CameraSensor, Error)
 			<< "Failed to retrieve camera info controls";
 		return -EINVAL;
 	}
diff --git a/src/libcamera/sensor/camera_sensor_raw.cpp b/src/libcamera/sensor/camera_sensor_raw.cpp
index 55b386ce..641801a1 100644
--- a/src/libcamera/sensor/camera_sensor_raw.cpp
+++ b/src/libcamera/sensor/camera_sensor_raw.cpp
@@ -397,92 +397,93 @@  std::optional<int> CameraSensorRaw::init()
 		/* Drop non-raw formats (in case we have a hybrid sensor). */
 		const MediaBusFormatInfo &info = MediaBusFormatInfo::info(code);
 		if (info.colourEncoding != PixelFormatInfo::ColourEncodingRAW)
 			continue;
 
 		mbusCodes_.push_back(code);
 		std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes_),
 			       [](const SizeRange &range) { return range.max; });
 	}
 
 	if (mbusCodes_.empty()) {
 		LOG(CameraSensor, Debug) << "No raw image formats found";
 		return { 0 };
 	}
 
 	std::sort(mbusCodes_.begin(), mbusCodes_.end());
 	std::sort(sizes_.begin(), sizes_.end());
 
 	/*
 	 * Remove duplicate sizes. There are no duplicate media bus codes as
 	 * they are the keys in the formats map.
 	 */
 	auto last = std::unique(sizes_.begin(), sizes_.end());
 	sizes_.erase(last, sizes_.end());
 
 	/*
 	 * 3. Query selection rectangles. Retrieve properties, and verify that
 	 * all the expected selection rectangles are supported.
 	 */
 
-	Rectangle rect;
+	std::optional<Rectangle> rect;
 	ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP_BOUNDS,
 				    &rect);
-	if (ret) {
+	if (ret || !rect) {
 		LOG(CameraSensor, Error) << "No pixel array crop bounds";
-		return { ret };
+		return { ret ? ret : -EINVAL };
 	}
 
 	/*
 	 * \todo Implement querying the physical sensor size based on pad 1,
 	 * stream 0. See
 	 * https://lore.kernel.org/linux-media/20260409201501.975242-23-sakari.ailus@linux.intel.com/
 	 */
 	LOG(CameraSensor, Warning) << "PixelArrayArea might be incorrect";
 	pixelArrayArea_ = Rectangle(0, 0,
-				    rect.x + rect.width,
-				    rect.y + rect.height);
+				    rect->x + rect->width,
+				    rect->y + rect->height);
 
-	readableArea_ = rect;
+	readableArea_ = *rect;
 
 	ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP_DEFAULT,
-				    &activeArea_);
-	if (ret) {
+				    &rect);
+	if (ret || !rect) {
 		LOG(CameraSensor, Error) << "No pixel array crop default";
-		return { ret };
+		return { ret ? ret : -EINVAL };
 	}
+	activeArea_ = *rect;
 
 	ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP,
 				    &rect);
-	if (ret) {
+	if (ret || !rect) {
 		LOG(CameraSensor, Error) << "No pixel array crop rectangle";
-		return { ret };
+		return { ret ? ret : -EINVAL };
 	}
 
 	/*
 	 * 4. Verify that all required controls are present.
 	 */
 
 	const ControlIdMap &controls = subdev_->controls().idmap();
 
 	static constexpr uint32_t mandatoryControls[] = {
 		V4L2_CID_ANALOGUE_GAIN,
 		V4L2_CID_CAMERA_ORIENTATION,
 		V4L2_CID_EXPOSURE,
 		V4L2_CID_HBLANK,
 		V4L2_CID_PIXEL_RATE,
 		V4L2_CID_VBLANK,
 	};
 
 	ret = 0;
 
 	for (uint32_t ctrl : mandatoryControls) {
 		if (!controls.count(ctrl)) {
 			LOG(CameraSensor, Error)
 				<< "Mandatory V4L2 control " << utils::hex(ctrl)
 				<< " not available";
 			ret = -EINVAL;
 		}
 	}
 
 	if (ret) {
 		LOG(CameraSensor, Error)
@@ -987,64 +988,70 @@  int CameraSensorRaw::setEmbeddedDataEnabled(bool enable)
 	 * source stream, as there would be something seriously wrong
 	 * otherwise.
 	 */
 	bool enabled = false;
 
 	for (const V4L2Subdevice::Route &route : routing) {
 		if (route.source != streams_.edata->source)
 			continue;
 
 		enabled = route.flags & V4L2_SUBDEV_ROUTE_FL_ACTIVE;
 		break;
 	}
 
 	if (enabled != enable)
 		return enabled ? -EISCONN : -ENOSTR;
 
 	return 0;
 }
 
 int CameraSensorRaw::sensorInfo(IPACameraSensorInfo *info) const
 {
 	info->model = model();
 	info->pixelArrayArea = pixelArrayArea_;
 
 	/*
 	 * The active area is a static property, while the crop rectangle needs
 	 * to be re-read as it depends on the sensor configuration.
 	 */
 	info->activeArea = activeArea_;
 
+	std::optional<Rectangle> crop;
 	int ret = subdev_->getSelection(streams_.image.sink, V4L2_SEL_TGT_CROP,
-					&info->analogCrop);
+					&crop);
 	if (ret)
 		return ret;
+	if (!crop) {
+		LOG(CameraSensor, Error) << "No pixel array crop rectangle";
+		return -EINVAL;
+	}
+	info->analogCrop = *crop;
 
 	/* The bit depth and image size depend on the currently applied format. */
 	V4L2SubdeviceFormat format{};
 	ret = subdev_->getFormat(streams_.image.source, &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::ColorFilterArrangementRGB;
 
 	/*
 	 * 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.
 	 */
 	static constexpr uint32_t cids[] = {
 		V4L2_CID_PIXEL_RATE,
 		V4L2_CID_HBLANK,
 		V4L2_CID_VBLANK,
 	};
 
 	ControlList ctrls = subdev_->getControls(cids);
 	if (ctrls.empty()) {
 		LOG(CameraSensor, Error)
 			<< "Failed to retrieve camera info controls";
 		return -EINVAL;
 	}
 
diff --git a/src/libcamera/v4l2_subdevice.cpp b/src/libcamera/v4l2_subdevice.cpp
index a69de154..0ab9d9ff 100644
--- a/src/libcamera/v4l2_subdevice.cpp
+++ b/src/libcamera/v4l2_subdevice.cpp
@@ -1082,163 +1082,181 @@  std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Route &route)
 /**
  * \typedef V4L2Subdevice::Routing
  * \brief V4L2 subdevice routing table
  *
  * This class stores a subdevice routing table as a vector of routes.
  */
 
 /**
  * \brief Insert a text representation of a V4L2Subdevice::Routing into an
  * output stream
  * \param[in] out The output stream
  * \param[in] routing The V4L2Subdevice::Routing
  * \return The output stream \a out
  */
 std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Routing &routing)
 {
 	for (const auto &[i, route] : utils::enumerate(routing)) {
 		out << "[" << i << "] " << route;
 		if (i != routing.size() - 1)
 			out << ", ";
 	}
 
 	return out;
 }
 
 /**
  * \brief Create a V4L2 subdevice from a MediaEntity using its device node
  * path
  */
 V4L2Subdevice::V4L2Subdevice(const MediaEntity *entity)
-	: V4L2Device(entity->deviceNode()), entity_(entity)
+	: V4L2Device(entity->deviceNode()), entity_(entity), caps_{}
 {
 }
 
 V4L2Subdevice::~V4L2Subdevice()
 {
 	close();
 }
 
 /**
  * \brief Open a V4L2 subdevice
  * \return 0 on success or a negative error code otherwise
  */
 int V4L2Subdevice::open()
 {
 	int ret = V4L2Device::open(O_RDWR);
 	if (ret)
 		return ret;
 
 	/*
 	 * Try to query the subdev capabilities. The VIDIOC_SUBDEV_QUERYCAP API
 	 * was introduced in kernel v5.8, ENOTTY errors must be ignored to
 	 * support older kernels.
 	 */
 	caps_ = {};
 	ret = ioctl(VIDIOC_SUBDEV_QUERYCAP, &caps_);
 	if (ret < 0 && errno != ENOTTY) {
 		ret = -errno;
 		LOG(V4L2, Error)
 			<< "Unable to query capabilities: " << strerror(-ret);
 		return ret;
 	}
 
 	/* If the subdev supports streams, enable the streams API. */
 	if (caps_.hasStreams()) {
 		struct v4l2_subdev_client_capability clientCaps{};
 		clientCaps.capabilities = V4L2_SUBDEV_CLIENT_CAP_STREAMS;
 
 		ret = ioctl(VIDIOC_SUBDEV_S_CLIENT_CAP, &clientCaps);
 		if (ret < 0) {
 			ret = -errno;
 			LOG(V4L2, Error)
 				<< "Unable to set client capabilities: "
 				<< strerror(-ret);
 			return ret;
 		}
 	}
 
 	return 0;
 }
 
 /**
  * \fn V4L2Subdevice::entity()
  * \brief Retrieve the media entity associated with the subdevice
  * \return The subdevice's associated media entity.
  */
 
 /**
  * \brief Get selection rectangle \a rect for \a target
  * \param[in] stream The stream the rectangle is retrieved from
  * \param[in] target The selection target defined by the V4L2_SEL_TGT_* flags
- * \param[out] rect The retrieved selection rectangle
+ * \param[out] rect The retrieved selection rectangle, if available
+ *
+ * If the ioctl returns EINVAL or ENOTTY, no rectangle is reported and \a rect
+ * is empty. EINVAL can indicate an unsupported target or unavailable stream
+ * state; an empty result does not distinguish these cases.
+ *
+ * Invalid pad indices and nonzero streams on devices without streams support
+ * are rejected with -EINVAL before issuing the ioctl. All other ioctl errors
+ * are returned unchanged. The output \a rect is cleared on entry.
  *
  * \todo Define a V4L2SelectionTarget enum for the selection target
  *
- * \return 0 on success or a negative error code otherwise
+ * \return 0 if the rectangle was retrieved or is unavailable, or a negative
+ * error code otherwise
  */
 int V4L2Subdevice::getSelection(const Stream &stream, unsigned int target,
-				Rectangle *rect)
+				std::optional<Rectangle> *rect)
 {
+	rect->reset();
+
+	if (stream.pad >= entity_->pads().size() ||
+	    (stream.stream && !caps_.hasStreams())) {
+		LOG(V4L2, Error) << "Invalid selection stream " << stream;
+		return -EINVAL;
+	}
+
 	struct v4l2_subdev_selection sel = {};
 
 	sel.which = V4L2_SUBDEV_FORMAT_ACTIVE;
 	sel.pad = stream.pad;
 	sel.stream = stream.stream;
 	sel.target = target;
 	sel.flags = 0;
 
 	int ret = ioctl(VIDIOC_SUBDEV_G_SELECTION, &sel);
 	if (ret < 0) {
+		if (ret == -EINVAL || ret == -ENOTTY)
+			return 0;
+
 		LOG(V4L2, Error)
 			<< "Unable to get rectangle " << target << " on pad "
 			<< stream << ": " << strerror(-ret);
 		return ret;
 	}
 
-	rect->x = sel.r.left;
-	rect->y = sel.r.top;
-	rect->width = sel.r.width;
-	rect->height = sel.r.height;
+	*rect = Rectangle(sel.r.left, sel.r.top, sel.r.width, sel.r.height);
 
 	return 0;
 }
 
 /**
  * \fn V4L2Subdevice::getSelection(unsigned int pad, unsigned int target,
- * Rectangle *rect)
+ * std::optional<Rectangle> *rect)
  * \brief Get selection rectangle \a rect for \a target
  * \param[in] pad The 0-indexed pad number the rectangle is retrieved from
  * \param[in] target The selection target defined by the V4L2_SEL_TGT_* flags
- * \param[out] rect The retrieved selection rectangle
+ * \param[out] rect The retrieved selection rectangle, if available
  *
- * \return 0 on success or a negative error code otherwise
+ * \return 0 if the rectangle was retrieved or is unavailable, or a negative
+ * error code otherwise
  */
 
 /**
  * \brief Set selection rectangle \a rect for \a target
  * \param[in] stream The stream the rectangle is to be applied to
  * \param[in] target The selection target defined by the V4L2_SEL_TGT_* flags
  * \param[inout] rect The selection rectangle to be applied
  *
  * \todo Define a V4L2SelectionTarget enum for the selection target
  *
  * \return 0 on success or a negative error code otherwise
  */
 int V4L2Subdevice::setSelection(const Stream &stream, unsigned int target,
 				Rectangle *rect)
 {
 	struct v4l2_subdev_selection sel = {};
 
 	sel.which = V4L2_SUBDEV_FORMAT_ACTIVE;
 	sel.pad = stream.pad;
 	sel.stream = stream.stream;
 	sel.target = target;
 	sel.flags = 0;
 
 	sel.r.left = rect->x;
 	sel.r.top = rect->y;
 	sel.r.width = rect->width;
 	sel.r.height = rect->height;
 
 	int ret = ioctl(VIDIOC_SUBDEV_S_SELECTION, &sel);
 	if (ret < 0) {