Merge remote-tracking branch 'upstream/master'

* upstream/master:
  ipa: rpi: common: Handle AEC/AGC flicker controls
  libcamera: controls: Add controls for AEC/AGC flicker avoidance
  libcamera: rpi: pipeline_base: Cache sensor format
  libcamera: rpi: pipeline_base: Move findBestFormat to CameraData
  libcamera: rpi: pipeline_base: Remove populateSensorFormats()
  utils: checkstyle.py: Check trailers for Amendment commits
  utils: checkstyle.py: Derive Amendment from Commit
  utils: checkstyle.py: Initialise staged trailers
  utils: checkstyle.py: Treat Malformed trailers as a CommitIssue
  libcamera v0.1.0
  tests: gstreamer: Fix compiler error with gcc 8.4.0
  libcamera: CameraManager: Remove ::get(dev_t)
  v4l2: Use SystemDevices properties to identify cameras
  v4l2: v4l2_camera_proxy: Prevent ioctl sign-extensions
  ipa: rpi: imx296_mono: Disable all colour shading
  ipa: rpi: imx708: Fix mode switch drop frame count

Change-Id: Ib5f840fde37f6e99d80769ec77c53e094b9966fb
diff --git a/include/libcamera/camera_manager.h b/include/libcamera/camera_manager.h
index 9767acc..1a891ca 100644
--- a/include/libcamera/camera_manager.h
+++ b/include/libcamera/camera_manager.h
@@ -32,7 +32,6 @@
 
 	std::vector<std::shared_ptr<Camera>> cameras() const;
 	std::shared_ptr<Camera> get(const std::string &id);
-	std::shared_ptr<Camera> get(dev_t devnum);
 
 	static const std::string &version() { return version_; }
 
diff --git a/include/libcamera/internal/camera_manager.h b/include/libcamera/internal/camera_manager.h
index e4f5aaf..33ebe06 100644
--- a/include/libcamera/internal/camera_manager.h
+++ b/include/libcamera/internal/camera_manager.h
@@ -50,11 +50,10 @@
 	 * This mutex protects
 	 *
 	 * - initialized_ and status_ during initialization
-	 * - cameras_ and camerasByDevnum_ after initialization
+	 * - cameras_ after initialization
 	 */
 	mutable Mutex mutex_;
 	std::vector<std::shared_ptr<Camera>> cameras_ LIBCAMERA_TSA_GUARDED_BY(mutex_);
-	std::map<dev_t, std::weak_ptr<Camera>> camerasByDevnum_ LIBCAMERA_TSA_GUARDED_BY(mutex_);
 
 	ConditionVariable cv_;
 	bool initialized_ LIBCAMERA_TSA_GUARDED_BY(mutex_);
diff --git a/meson.build b/meson.build
index 363de45..031e1ed 100644
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@
 
 project('libcamera', 'c', 'cpp',
     meson_version : '>= 0.57',
-    version : '0.0.5',
+    version : '0.1.0',
     default_options : [
         'werror=true',
         'warning_level=2',
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
index 641ba18..dce39cd 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
@@ -21,6 +21,8 @@
 using namespace libcamera;
 using libcamera::utils::Duration;
 
+using namespace std::literals::chrono_literals;
+
 namespace libcamera {
 LOG_DECLARE_CATEGORY(IPARPI)
 }
@@ -56,7 +58,8 @@
 		       int &vblankDelay, int &hblankDelay) const override;
 	bool sensorEmbeddedDataPresent() const override;
 	double getModeSensitivity(const CameraMode &mode) const override;
-	unsigned int hideFramesModeSwitch() const override { return 1; } // seems to be required for HDR
+	unsigned int hideFramesModeSwitch() const override;
+	unsigned int hideFramesStartup() const override;
 
 private:
 	/*
@@ -225,6 +228,26 @@
 	return (mode.width > 2304) ? 1.0 : 2.0;
 }
 
+unsigned int CamHelperImx708::hideFramesModeSwitch() const
+{
+	/*
+	 * We need to drop the first startup frame in HDR mode.
+	 * Unfortunately the only way to currently determine if the sensor is in
+	 * the HDR mode is to match with the resolution and framerate - the HDR
+	 * mode only runs upto 30fps.
+	 */
+	if (mode_.width == 2304 && mode_.height == 1296 &&
+	    mode_.minFrameDuration > 1.0s / 32)
+		return 1;
+	else
+		return 0;
+}
+
+unsigned int CamHelperImx708::hideFramesStartup() const
+{
+	return hideFramesModeSwitch();
+}
+
 void CamHelperImx708::populateMetadata(const MdParser::RegisterMap &registers,
 				       Metadata &metadata) const
 {
diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp
index f40f2e7..4438ecd 100644
--- a/src/ipa/rpi/common/ipa_base.cpp
+++ b/src/ipa/rpi/common/ipa_base.cpp
@@ -61,6 +61,10 @@
 	{ &controls::AeConstraintMode, ControlInfo(controls::AeConstraintModeValues) },
 	{ &controls::AeExposureMode, ControlInfo(controls::AeExposureModeValues) },
 	{ &controls::ExposureValue, ControlInfo(-8.0f, 8.0f, 0.0f) },
+	{ &controls::AeFlickerMode, ControlInfo(static_cast<int>(controls::FlickerOff),
+						static_cast<int>(controls::FlickerManual),
+						static_cast<int>(controls::FlickerOff)) },
+	{ &controls::AeFlickerPeriod, ControlInfo(100, 1000000) },
 	{ &controls::Brightness, ControlInfo(-1.0f, 1.0f, 0.0f) },
 	{ &controls::Contrast, ControlInfo(0.0f, 32.0f, 1.0f) },
 	{ &controls::Sharpness, ControlInfo(0.0f, 16.0f, 1.0f) },
@@ -97,7 +101,7 @@
 
 IpaBase::IpaBase()
 	: controller_(), frameCount_(0), mistrustCount_(0), lastRunTimestamp_(0),
-	  firstStart_(true)
+	  firstStart_(true), flickerState_({ 0, 0s })
 {
 }
 
@@ -812,6 +816,64 @@
 			break;
 		}
 
+		case controls::AE_FLICKER_MODE: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AeFlickerMode - no AGC algorithm";
+				break;
+			}
+
+			int32_t mode = ctrl.second.get<int32_t>();
+			bool modeValid = true;
+
+			switch (mode) {
+			case controls::FlickerOff:
+				agc->setFlickerPeriod(0us);
+
+				break;
+
+			case controls::FlickerManual:
+				agc->setFlickerPeriod(flickerState_.manualPeriod);
+
+				break;
+
+			default:
+				LOG(IPARPI, Error) << "Flicker mode " << mode << " is not supported";
+				modeValid = false;
+
+				break;
+			}
+
+			if (modeValid)
+				flickerState_.mode = mode;
+
+			break;
+		}
+
+		case controls::AE_FLICKER_PERIOD: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AeFlickerPeriod - no AGC algorithm";
+				break;
+			}
+
+			uint32_t manualPeriod = ctrl.second.get<int32_t>();
+			flickerState_.manualPeriod = manualPeriod * 1.0us;
+
+			/*
+			 * We note that it makes no difference if the mode gets set to "manual"
+			 * first, and the period updated after, or vice versa.
+			 */
+			if (flickerState_.mode == controls::FlickerManual)
+				agc->setFlickerPeriod(flickerState_.manualPeriod);
+
+			break;
+		}
+
 		case controls::AWB_ENABLE: {
 			/* Silently ignore this control for a mono sensor. */
 			if (monoSensor_)
diff --git a/src/ipa/rpi/common/ipa_base.h b/src/ipa/rpi/common/ipa_base.h
index 39d0076..097f436 100644
--- a/src/ipa/rpi/common/ipa_base.h
+++ b/src/ipa/rpi/common/ipa_base.h
@@ -116,6 +116,12 @@
 	/* Frame duration (1/fps) limits. */
 	utils::Duration minFrameDuration_;
 	utils::Duration maxFrameDuration_;
+
+	/* The current state of flicker avoidance. */
+	struct FlickerState {
+		int32_t mode;
+		utils::Duration manualPeriod;
+	} flickerState_;
 };
 
 } /* namespace ipa::RPi */
diff --git a/src/ipa/rpi/vc4/data/imx296_mono.json b/src/ipa/rpi/vc4/data/imx296_mono.json
index 955af43..d4140c8 100644
--- a/src/ipa/rpi/vc4/data/imx296_mono.json
+++ b/src/ipa/rpi/vc4/data/imx296_mono.json
@@ -123,18 +123,18 @@
                         "ct": 4000,
                         "table":
                         [
-                            2.554, 2.554, 2.541, 2.534, 2.495, 2.506, 2.516, 2.517, 2.518, 2.515, 2.513, 2.495, 2.481, 2.533, 2.533, 2.521,
-                            2.522, 2.534, 2.539, 2.531, 2.531, 2.506, 2.506, 2.513, 2.513, 2.509, 2.498, 2.496, 2.508, 2.517, 2.521, 2.521,
-                            2.509, 2.517, 2.534, 2.529, 2.531, 2.521, 2.517, 2.517, 2.515, 2.514, 2.506, 2.499, 2.508, 2.508, 2.521, 2.537,
-                            2.507, 2.508, 2.517, 2.516, 2.495, 2.487, 2.519, 2.534, 2.535, 2.531, 2.499, 2.494, 2.501, 2.511, 2.526, 2.526,
-                            2.509, 2.517, 2.507, 2.501, 2.494, 2.519, 2.539, 2.539, 2.537, 2.537, 2.533, 2.499, 2.503, 2.511, 2.529, 2.525,
-                            2.521, 2.522, 2.476, 2.501, 2.501, 2.539, 2.546, 2.538, 2.531, 2.538, 2.541, 2.531, 2.529, 2.526, 2.529, 2.525,
-                            2.516, 2.519, 2.469, 2.499, 2.499, 2.543, 2.543, 2.531, 2.528, 2.534, 2.541, 2.535, 2.531, 2.526, 2.531, 2.528,
-                            2.509, 2.515, 2.465, 2.487, 2.487, 2.539, 2.543, 2.539, 2.533, 2.549, 2.542, 2.531, 2.529, 2.524, 2.532, 2.533,
-                            2.499, 2.499, 2.475, 2.482, 2.471, 2.509, 2.539, 2.544, 2.543, 2.545, 2.533, 2.498, 2.521, 2.521, 2.537, 2.536,
-                            2.499, 2.488, 2.488, 2.488, 2.471, 2.462, 2.509, 2.539, 2.539, 2.532, 2.498, 2.498, 2.518, 2.518, 2.539, 2.539,
-                            2.483, 2.484, 2.488, 2.488, 2.502, 2.496, 2.508, 2.514, 2.518, 2.517, 2.521, 2.518, 2.518, 2.518, 2.525, 2.539,
-                            2.483, 2.487, 2.478, 2.478, 2.507, 2.509, 2.514, 2.513, 2.514, 2.517, 2.536, 2.559, 2.501, 2.501, 2.503, 2.525
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
                         ]
                     }
                 ],
@@ -143,18 +143,18 @@
                         "ct": 4000,
                         "table":
                         [
-                            2.619, 2.603, 2.599, 2.597, 2.595, 2.594, 2.589, 2.587, 2.586, 2.589, 2.592, 2.597, 2.601, 2.608, 2.621, 2.621,
-                            2.619, 2.615, 2.603, 2.601, 2.596, 2.595, 2.591, 2.589, 2.589, 2.592, 2.599, 2.593, 2.601, 2.613, 2.622, 2.631,
-                            2.617, 2.617, 2.612, 2.611, 2.604, 2.598, 2.593, 2.591, 2.592, 2.591, 2.593, 2.595, 2.599, 2.614, 2.623, 2.631,
-                            2.624, 2.619, 2.615, 2.612, 2.605, 2.602, 2.597, 2.596, 2.592, 2.592, 2.595, 2.599, 2.602, 2.606, 2.619, 2.624,
-                            2.629, 2.627, 2.627, 2.617, 2.609, 2.598, 2.612, 2.623, 2.615, 2.604, 2.589, 2.595, 2.599, 2.608, 2.611, 2.614,
-                            2.629, 2.632, 2.637, 2.627, 2.612, 2.612, 2.629, 2.631, 2.628, 2.621, 2.604, 2.597, 2.598, 2.604, 2.609, 2.609,
-                            2.635, 2.636, 2.642, 2.628, 2.623, 2.623, 2.636, 2.636, 2.634, 2.628, 2.616, 2.599, 2.597, 2.601, 2.603, 2.601,
-                            2.641, 2.639, 2.646, 2.632, 2.627, 2.625, 2.632, 2.635, 2.634, 2.627, 2.614, 2.596, 2.595, 2.599, 2.599, 2.598,
-                            2.643, 2.644, 2.651, 2.649, 2.629, 2.617, 2.624, 2.629, 2.625, 2.614, 2.586, 2.599, 2.595, 2.597, 2.592, 2.595,
-                            2.645, 2.646, 2.649, 2.649, 2.638, 2.624, 2.616, 2.617, 2.609, 2.604, 2.603, 2.603, 2.595, 2.589, 2.587, 2.592,
-                            2.641, 2.643, 2.649, 2.647, 2.638, 2.618, 2.615, 2.608, 2.602, 2.595, 2.596, 2.595, 2.593, 2.584, 2.581, 2.583,
-                            2.638, 2.637, 2.647, 2.634, 2.634, 2.618, 2.621, 2.621, 2.611, 2.602, 2.596, 2.583, 2.581, 2.581, 2.576, 2.574
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
                         ]
                     }
                 ],
diff --git a/src/libcamera/camera_manager.cpp b/src/libcamera/camera_manager.cpp
index 7c65bb9..355f3ad 100644
--- a/src/libcamera/camera_manager.cpp
+++ b/src/libcamera/camera_manager.cpp
@@ -178,15 +178,9 @@
 		}
 	}
 
-	auto devnums = camera->properties()
-			       .get(properties::SystemDevices)
-			       .value_or(Span<int64_t>{});
-
 	cameras_.push_back(std::move(camera));
 
 	unsigned int index = cameras_.size() - 1;
-	for (dev_t devnum : devnums)
-		camerasByDevnum_[devnum] = cameras_[index];
 
 	/* Report the addition to the public signal */
 	CameraManager *const o = LIBCAMERA_O_PTR();
@@ -219,13 +213,6 @@
 	LOG(Camera, Debug)
 		<< "Unregistering camera '" << camera->id() << "'";
 
-	auto iter_d = std::find_if(camerasByDevnum_.begin(), camerasByDevnum_.end(),
-				   [camera](const std::pair<dev_t, std::weak_ptr<Camera>> &p) {
-					   return p.second.lock().get() == camera.get();
-				   });
-	if (iter_d != camerasByDevnum_.end())
-		camerasByDevnum_.erase(iter_d);
-
 	cameras_.erase(iter);
 
 	/* Report the removal to the public signal */
@@ -367,35 +354,6 @@
 }
 
 /**
- * \brief Retrieve a camera based on device number
- * \param[in] devnum Device number of camera to get
- *
- * This function is meant solely for the use of the V4L2 compatibility
- * layer, to map device nodes to Camera instances. Applications shall
- * not use it and shall instead retrieve cameras by name.
- *
- * Before calling this function the caller is responsible for ensuring that
- * the camera manager is running.
- *
- * \context This function is \threadsafe.
- *
- * \return Shared pointer to Camera object, which is empty if the camera is
- * not found
- */
-std::shared_ptr<Camera> CameraManager::get(dev_t devnum)
-{
-	Private *const d = _d();
-
-	MutexLocker locker(d->mutex_);
-
-	auto iter = d->camerasByDevnum_.find(devnum);
-	if (iter == d->camerasByDevnum_.end())
-		return nullptr;
-
-	return iter->second.lock();
-}
-
-/**
  * \var CameraManager::cameraAdded
  * \brief Notify of a new camera added to the system
  *
diff --git a/src/libcamera/control_ids.yaml b/src/libcamera/control_ids.yaml
index 056886e..f2e542f 100644
--- a/src/libcamera/control_ids.yaml
+++ b/src/libcamera/control_ids.yaml
@@ -156,6 +156,79 @@
         control of which features should be automatically adjusted shouldn't
         better be handled through a separate AE mode control.
 
+  - AeFlickerMode:
+      type: int32_t
+      description: |
+        Set the flicker mode, which determines whether, and how, the AGC/AEC
+        algorithm attempts to hide flicker effects caused by the duty cycle of
+        artificial lighting.
+
+        Although implementation dependent, many algorithms for "flicker
+        avoidance" work by restricting this exposure time to integer multiples
+        of the cycle period, wherever possible.
+
+        Implementations may not support all of the flicker modes listed below.
+
+        By default the system will start in FlickerAuto mode if this is
+        supported, otherwise the flicker mode will be set to FlickerOff.
+
+      enum:
+        - name: FlickerOff
+          value: 0
+          description: No flicker avoidance is performed.
+        - name: FlickerManual
+          value: 1
+          description: Manual flicker avoidance.
+            Suppress flicker effects caused by lighting running with a period
+            specified by the AeFlickerPeriod control.
+            \sa AeFlickerPeriod
+        - name: FlickerAuto
+          value: 2
+          description: Automatic flicker period detection and avoidance.
+            The system will automatically determine the most likely value of
+            flicker period, and avoid flicker of this frequency. Once flicker
+            is being corrected, it is implementation dependent whether the
+            system is still able to detect a change in the flicker period.
+            \sa AeFlickerDetected
+
+  - AeFlickerPeriod:
+      type: int32_t
+      description: Manual flicker period in microseconds.
+        This value sets the current flicker period to avoid. It is used when
+        AeFlickerMode is set to FlickerManual.
+
+        To cancel 50Hz mains flicker, this should be set to 10000 (corresponding
+        to 100Hz), or 8333 (120Hz) for 60Hz mains.
+
+        Setting the mode to FlickerManual when no AeFlickerPeriod has ever been
+        set means that no flicker cancellation occurs (until the value of this
+        control is updated).
+
+        Switching to modes other than FlickerManual has no effect on the
+        value of the AeFlickerPeriod control.
+
+        \sa AeFlickerMode
+
+  - AeFlickerDetected:
+      type: int32_t
+      description: Flicker period detected in microseconds.
+        The value reported here indicates the currently detected flicker
+        period, or zero if no flicker at all is detected.
+
+        When AeFlickerMode is set to FlickerAuto, there may be a period during
+        which the value reported here remains zero. Once a non-zero value is
+        reported, then this is the flicker period that has been detected and is
+        now being cancelled.
+
+        In the case of 50Hz mains flicker, the value would be 10000
+        (corresponding to 100Hz), or 8333 (120Hz) for 60Hz mains flicker.
+
+        It is implementation dependent whether the system can continue to detect
+        flicker of different periods when another frequency is already being
+        cancelled.
+
+        \sa AeFlickerMode
+
   - Brightness:
       type: float
       description: |
@@ -850,23 +923,6 @@
           value: 1
           description: The lens shading map mode is available.
 
-  - SceneFlicker:
-      type: int32_t
-      draft: true
-      description: |
-       Control to report the detected scene light frequency. Currently
-       identical to ANDROID_STATISTICS_SCENE_FLICKER.
-      enum:
-        - name: SceneFickerOff
-          value: 0
-          description: No flickering detected.
-        - name: SceneFicker50Hz
-          value: 1
-          description: 50Hz flickering detected.
-        - name: SceneFicker60Hz
-          value: 2
-          description: 60Hz flickering detected.
-
   - PipelineDepth:
       type: int32_t
       draft: true
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
index 179a5b8..e0fbeec 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
@@ -66,16 +66,6 @@
 	return pix;
 }
 
-SensorFormats populateSensorFormats(std::unique_ptr<CameraSensor> &sensor)
-{
-	SensorFormats formats;
-
-	for (auto const mbusCode : sensor->mbusCodes())
-		formats.emplace(mbusCode, sensor->sizes(mbusCode));
-
-	return formats;
-}
-
 bool isMonoSensor(std::unique_ptr<CameraSensor> &sensor)
 {
 	unsigned int mbusCode = sensor->mbusCodes()[0];
@@ -84,63 +74,6 @@
 	return bayer.order == BayerFormat::Order::MONO;
 }
 
-double scoreFormat(double desired, double actual)
-{
-	double score = desired - actual;
-	/* Smaller desired dimensions are preferred. */
-	if (score < 0.0)
-		score = (-score) / 8;
-	/* Penalise non-exact matches. */
-	if (actual != desired)
-		score *= 2;
-
-	return score;
-}
-
-V4L2SubdeviceFormat findBestFormat(const SensorFormats &formatsMap, const Size &req, unsigned int bitDepth)
-{
-	double bestScore = std::numeric_limits<double>::max(), score;
-	V4L2SubdeviceFormat bestFormat;
-	bestFormat.colorSpace = ColorSpace::Raw;
-
-	constexpr float penaltyAr = 1500.0;
-	constexpr float penaltyBitDepth = 500.0;
-
-	/* Calculate the closest/best mode from the user requested size. */
-	for (const auto &iter : formatsMap) {
-		const unsigned int mbusCode = iter.first;
-		const PixelFormat format = mbusCodeToPixelFormat(mbusCode,
-								 BayerFormat::Packing::None);
-		const PixelFormatInfo &info = PixelFormatInfo::info(format);
-
-		for (const Size &size : iter.second) {
-			double reqAr = static_cast<double>(req.width) / req.height;
-			double fmtAr = static_cast<double>(size.width) / size.height;
-
-			/* Score the dimensions for closeness. */
-			score = scoreFormat(req.width, size.width);
-			score += scoreFormat(req.height, size.height);
-			score += penaltyAr * scoreFormat(reqAr, fmtAr);
-
-			/* Add any penalties... this is not an exact science! */
-			score += utils::abs_diff(info.bitsPerPixel, bitDepth) * penaltyBitDepth;
-
-			if (score <= bestScore) {
-				bestScore = score;
-				bestFormat.mbus_code = mbusCode;
-				bestFormat.size = size;
-			}
-
-			LOG(RPI, Debug) << "Format: " << size
-					<< " fmt " << format
-					<< " Score: " << score
-					<< " (best " << bestScore << ")";
-		}
-	}
-
-	return bestFormat;
-}
-
 const std::vector<ColorSpace> validColorSpaces = {
 	ColorSpace::Sycc,
 	ColorSpace::Smpte170m,
@@ -274,6 +207,17 @@
 	std::sort(outStreams.begin(), outStreams.end(),
 		  [](auto &l, auto &r) { return l.cfg->size > r.cfg->size; });
 
+	/* Compute the sensor configuration. */
+	unsigned int bitDepth = defaultRawBitDepth;
+	if (!rawStreams.empty()) {
+		BayerFormat bayerFormat = BayerFormat::fromPixelFormat(rawStreams[0].cfg->pixelFormat);
+		bitDepth = bayerFormat.bitDepth;
+	}
+
+	sensorFormat_ = data_->findBestFormat(rawStreams.empty() ? outStreams[0].cfg->size
+								 : rawStreams[0].cfg->size,
+					      bitDepth);
+
 	/* Do any platform specific fixups. */
 	status = data_->platformValidate(rawStreams, outStreams);
 	if (status == Invalid)
@@ -284,12 +228,8 @@
 		StreamConfiguration &cfg = config_.at(raw.index);
 		V4L2DeviceFormat rawFormat;
 
-		const PixelFormatInfo &info = PixelFormatInfo::info(cfg.pixelFormat);
-		unsigned int bitDepth = info.isValid() ? info.bitsPerPixel : defaultRawBitDepth;
-		V4L2SubdeviceFormat sensorFormat = findBestFormat(data_->sensorFormats_, cfg.size, bitDepth);
-
 		BayerFormat::Packing packing = BayerFormat::fromPixelFormat(cfg.pixelFormat).packing;
-		rawFormat = PipelineHandlerBase::toV4L2DeviceFormat(raw.dev, sensorFormat, packing);
+		rawFormat = PipelineHandlerBase::toV4L2DeviceFormat(raw.dev, sensorFormat_, packing);
 
 		int ret = raw.dev->tryFormat(&rawFormat);
 		if (ret)
@@ -401,7 +341,7 @@
 		switch (role) {
 		case StreamRole::Raw:
 			size = sensorSize;
-			sensorFormat = findBestFormat(data->sensorFormats_, size, defaultRawBitDepth);
+			sensorFormat = data->findBestFormat(size, defaultRawBitDepth);
 			pixelFormat = mbusCodeToPixelFormat(sensorFormat.mbus_code,
 							    BayerFormat::Packing::CSI2);
 			ASSERT(pixelFormat.isValid());
@@ -509,8 +449,6 @@
 		stream->clearFlags(StreamFlag::External);
 
 	std::vector<CameraData::StreamParams> rawStreams, ispStreams;
-	std::optional<BayerFormat::Packing> packing;
-	unsigned int bitDepth = defaultRawBitDepth;
 
 	for (unsigned i = 0; i < config->size(); i++) {
 		StreamConfiguration *cfg = &config->at(i);
@@ -528,31 +466,23 @@
 	std::sort(ispStreams.begin(), ispStreams.end(),
 		  [](auto &l, auto &r) { return l.cfg->size > r.cfg->size; });
 
-	/*
-	 * Calculate the best sensor mode we can use based on the user's request,
-	 * and apply it to the sensor with the cached tranform, if any.
-	 *
-	 * If we have been given a RAW stream, use that size for setting up the sensor.
-	 */
-	if (!rawStreams.empty()) {
-		BayerFormat bayerFormat = BayerFormat::fromPixelFormat(rawStreams[0].cfg->pixelFormat);
-		/* Replace the user requested packing/bit-depth. */
-		packing = bayerFormat.packing;
-		bitDepth = bayerFormat.bitDepth;
-	}
+	/* Apply the format on the sensor with any cached transform. */
+	const RPiCameraConfiguration *rpiConfig =
+				static_cast<const RPiCameraConfiguration *>(config);
+	V4L2SubdeviceFormat sensorFormat = rpiConfig->sensorFormat_;
 
-	V4L2SubdeviceFormat sensorFormat = findBestFormat(data->sensorFormats_,
-							  rawStreams.empty() ? ispStreams[0].cfg->size
-									     : rawStreams[0].cfg->size,
-							  bitDepth);
-	/* Apply any cached transform. */
-	const RPiCameraConfiguration *rpiConfig = static_cast<const RPiCameraConfiguration *>(config);
-
-	/* Then apply the format on the sensor. */
 	ret = data->sensor_->setFormat(&sensorFormat, rpiConfig->combinedTransform_);
 	if (ret)
 		return ret;
 
+	/* Use the user requested packing/bit-depth. */
+	std::optional<BayerFormat::Packing> packing;
+	if (!rawStreams.empty()) {
+		BayerFormat bayerFormat =
+			BayerFormat::fromPixelFormat(rawStreams[0].cfg->pixelFormat);
+		packing = bayerFormat.packing;
+	}
+
 	/*
 	 * Platform specific internal stream configuration. This also assigns
 	 * external streams which get configured below.
@@ -804,7 +734,10 @@
 	if (data->sensor_->init())
 		return -EINVAL;
 
-	data->sensorFormats_ = populateSensorFormats(data->sensor_);
+	/* Populate the map of sensor supported formats and sizes. */
+	for (auto const mbusCode : data->sensor_->mbusCodes())
+		data->sensorFormats_.emplace(mbusCode,
+					     data->sensor_->sizes(mbusCode));
 
 	/*
 	 * Enumerate all the Video Mux/Bridge devices across the sensor -> Fr
@@ -961,6 +894,63 @@
 	return 0;
 }
 
+double CameraData::scoreFormat(double desired, double actual) const
+{
+	double score = desired - actual;
+	/* Smaller desired dimensions are preferred. */
+	if (score < 0.0)
+		score = (-score) / 8;
+	/* Penalise non-exact matches. */
+	if (actual != desired)
+		score *= 2;
+
+	return score;
+}
+
+V4L2SubdeviceFormat CameraData::findBestFormat(const Size &req, unsigned int bitDepth) const
+{
+	double bestScore = std::numeric_limits<double>::max(), score;
+	V4L2SubdeviceFormat bestFormat;
+	bestFormat.colorSpace = ColorSpace::Raw;
+
+	constexpr float penaltyAr = 1500.0;
+	constexpr float penaltyBitDepth = 500.0;
+
+	/* Calculate the closest/best mode from the user requested size. */
+	for (const auto &iter : sensorFormats_) {
+		const unsigned int mbusCode = iter.first;
+		const PixelFormat format = mbusCodeToPixelFormat(mbusCode,
+								 BayerFormat::Packing::None);
+		const PixelFormatInfo &info = PixelFormatInfo::info(format);
+
+		for (const Size &size : iter.second) {
+			double reqAr = static_cast<double>(req.width) / req.height;
+			double fmtAr = static_cast<double>(size.width) / size.height;
+
+			/* Score the dimensions for closeness. */
+			score = scoreFormat(req.width, size.width);
+			score += scoreFormat(req.height, size.height);
+			score += penaltyAr * scoreFormat(reqAr, fmtAr);
+
+			/* Add any penalties... this is not an exact science! */
+			score += utils::abs_diff(info.bitsPerPixel, bitDepth) * penaltyBitDepth;
+
+			if (score <= bestScore) {
+				bestScore = score;
+				bestFormat.mbus_code = mbusCode;
+				bestFormat.size = size;
+			}
+
+			LOG(RPI, Debug) << "Format: " << size
+					<< " fmt " << format
+					<< " Score: " << score
+					<< " (best " << bestScore << ")";
+		}
+	}
+
+	return bestFormat;
+}
+
 void CameraData::freeBuffers()
 {
 	if (ipa_) {
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h
index f648e81..a139c98 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.h
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h
@@ -81,6 +81,9 @@
 	virtual void platformStart() = 0;
 	virtual void platformStop() = 0;
 
+	double scoreFormat(double desired, double actual) const;
+	V4L2SubdeviceFormat findBestFormat(const Size &req, unsigned int bitDepth) const;
+
 	void freeBuffers();
 	virtual void platformFreeBuffers() = 0;
 
@@ -259,6 +262,8 @@
 
 	/* Cache the combinedTransform_ that will be applied to the sensor */
 	Transform combinedTransform_;
+	/* The sensor format computed in validate() */
+	V4L2SubdeviceFormat sensorFormat_;
 
 private:
 	const CameraData *data_;
diff --git a/src/v4l2/v4l2_camera_proxy.cpp b/src/v4l2/v4l2_camera_proxy.cpp
index 55ff62c..341f790 100644
--- a/src/v4l2/v4l2_camera_proxy.cpp
+++ b/src/v4l2/v4l2_camera_proxy.cpp
@@ -778,10 +778,20 @@
 	VIDIOC_STREAMOFF,
 };
 
-int V4L2CameraProxy::ioctl(V4L2CameraFile *file, unsigned long request, void *arg)
+int V4L2CameraProxy::ioctl(V4L2CameraFile *file, unsigned long longRequest, void *arg)
 {
 	MutexLocker locker(proxyMutex_);
 
+	/*
+	 * The Linux Kernel only processes 32 bits of an IOCTL.
+	 *
+	 * Prevent unexpected sign-extensions that could occur if applications
+	 * use a signed int for the ioctl request, which would sign-extend to
+	 * an incorrect value for unsigned longs on 64 bit architectures by
+	 * explicitly casting as an unsigned int here.
+	 */
+	unsigned int request = longRequest;
+
 	if (!arg && (_IOC_DIR(request) & _IOC_WRITE)) {
 		errno = EFAULT;
 		return -1;
diff --git a/src/v4l2/v4l2_compat_manager.cpp b/src/v4l2/v4l2_compat_manager.cpp
index 0f7575c..5e8cdb4 100644
--- a/src/v4l2/v4l2_compat_manager.cpp
+++ b/src/v4l2/v4l2_compat_manager.cpp
@@ -24,6 +24,7 @@
 
 #include <libcamera/camera.h>
 #include <libcamera/camera_manager.h>
+#include <libcamera/property_ids.h>
 
 #include "v4l2_camera_file.h"
 
@@ -113,14 +114,35 @@
 	if (ret < 0)
 		return -1;
 
-	std::shared_ptr<Camera> target = cm_->get(statbuf.st_rdev);
-	if (!target)
-		return -1;
+	const dev_t devnum = statbuf.st_rdev;
 
+	/*
+	 * Iterate each known camera and identify if it reports this nodes
+	 * device number in its list of SystemDevices.
+	 */
 	auto cameras = cm_->cameras();
 	for (auto [index, camera] : utils::enumerate(cameras)) {
-		if (camera == target)
-			return index;
+		Span<const int64_t> devices = camera->properties()
+						      .get(properties::SystemDevices)
+						      .value_or(Span<int64_t>{});
+
+		/*
+		 * While there may be multiple cameras that could reference the
+		 * same device node, we take a first match as a best effort for
+		 * now.
+		 *
+		 * \todo Each camera can be accessed through any of the video
+		 * device nodes that it uses. This may confuse applications.
+		 * Consider reworking the V4L2 adaptation layer to instead
+		 * expose each Camera instance through a single video device
+		 * node (with a consistent and stable mapping). The other
+		 * device nodes could possibly be hidden from the application
+		 * by intercepting additional calls to the C library.
+		 */
+		for (const int64_t dev : devices) {
+			if (dev == static_cast<int64_t>(devnum))
+				return index;
+		}
 	}
 
 	return -1;
diff --git a/test/gstreamer/gstreamer_device_provider_test.cpp b/test/gstreamer/gstreamer_device_provider_test.cpp
index c8606b9..237af8c 100644
--- a/test/gstreamer/gstreamer_device_provider_test.cpp
+++ b/test/gstreamer/gstreamer_device_provider_test.cpp
@@ -34,7 +34,7 @@
 
 	int run() override
 	{
-		g_autoptr(GstDeviceProvider) provider;
+		g_autoptr(GstDeviceProvider) provider = NULL;
 		GList *devices, *l;
 		std::vector<std::string> cameraNames;
 		std::unique_ptr<libcamera::CameraManager> cm;
diff --git a/utils/checkstyle.py b/utils/checkstyle.py
index 3558740..214509b 100755
--- a/utils/checkstyle.py
+++ b/utils/checkstyle.py
@@ -208,6 +208,17 @@
         self.commit = commit
         self._parse()
 
+    def _parse_trailers(self, lines):
+        self._trailers = []
+        for index in range(1, len(lines)):
+            line = lines[index]
+            if not line:
+                break
+
+            self._trailers.append(line)
+
+        return index
+
     def _parse(self):
         # Get the commit title and list of files.
         ret = subprocess.run(['git', 'show', '--format=%s%n%(trailers:only,unfold)', '--name-status',
@@ -217,14 +228,7 @@
 
         self._title = lines[0]
 
-        self._trailers = []
-        for index in range(1, len(lines)):
-            line = lines[index]
-            if not line:
-                break
-
-            self._trailers.append(line)
-
+        index = self._parse_trailers(lines)
         self._files = [CommitFile(f) for f in lines[index:] if f]
 
     def files(self, filter='AMR'):
@@ -253,6 +257,9 @@
     def __init__(self):
         Commit.__init__(self, '')
 
+        # There are no trailers to parse on a Staged Change.
+        self._trailers = []
+
     def _parse(self):
         ret = subprocess.run(['git', 'diff', '--staged', '--name-status'],
                              stdout=subprocess.PIPE).stdout.decode('utf-8')
@@ -266,9 +273,9 @@
         return parse_diff(diff.splitlines(True))
 
 
-class Amendment(StagedChanges):
+class Amendment(Commit):
     def __init__(self):
-        StagedChanges.__init__(self)
+        Commit.__init__(self, '')
 
     def _parse(self):
         # Create a title using HEAD commit
@@ -280,6 +287,12 @@
                              stdout=subprocess.PIPE).stdout.decode('utf-8')
         self._files = [CommitFile(f) for f in ret.splitlines()]
 
+        # Parse trailers from the existing commit only.
+        ret = subprocess.run(['git', 'show', '--format=%n%(trailers:only,unfold)',
+                             '--no-patch'],
+                             stdout=subprocess.PIPE).stdout.decode('utf-8')
+        self._parse_trailers(ret.splitlines())
+
     def get_diff(self, top_level, filename):
         diff = subprocess.run(['git', 'diff', '--staged', 'HEAD~', '--',
                                '%s/%s' % (top_level, filename)],
@@ -479,7 +492,8 @@
         for trailer in commit.trailers:
             match = TrailersChecker.trailer_regex.fullmatch(trailer)
             if not match:
-                raise RuntimeError(f"Malformed commit trailer '{trailer}'")
+                issues.append(CommitIssue(f"Malformed commit trailer '{trailer}'"))
+                continue
 
             key, value = match.groups()