[v4,1/1] libcamera: v4l2_subdevice: Return optional selection rectangles
diff mbox series

Message ID 20261029132535.369555-1-halfline@gmail.com
State New
Headers show
Series
  • [v4,1/1] libcamera: v4l2_subdevice: Return optional selection rectangles
Related show

Commit Message

Ray Strode Oct. 29, 2026, 1:25 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.

Return 0 and an empty optional for EINVAL or ENOTTY from G_SELECTION, and
propagate other ioctl errors. Clear the output on entry so a missing
rectangle or failed query cannot leave a previous result behind.

Reject invalid pads and nonzero streams on devices without streams support
before issuing the ioctl. Initialize the cached capabilities so this check
is also safe before open(). EINVAL from the driver still means only that no
rectangle was reported for the current configuration.

Update callers to 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(-)


base-commit: 6e19ca0fa7aa75aa965511072f43b5e50dec1904

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) {