From 0c9191ec0b18955455ef6ffeffbf743a366aa2d9 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Mon, 27 Jul 2026 14:11:04 -0400 Subject: [PATCH] Emulate signal gain for emulated_camera_mplane. Interact with the control: ``` adb shell "su 0 v4l2-ctl -d /dev/video1 -l" adb shell "su 0 v4l2-ctl -d /dev/video1 --set-ctrl=gain=500" ``` Introduce CameraControls, to manage the state of control values. Bug: b/539617743 Assisted-by: Jetski:Gemini 3.5 Flash --- .../emulated_camera_mplane/src/device.rs | 330 ++++++++++++++++-- 1 file changed, 297 insertions(+), 33 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/device.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/device.rs index d81245ac522..6986837e048 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/device.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/device.rs @@ -57,6 +57,14 @@ use virtio_media::protocol::V4l2Ioctl; use virtio_media::protocol::VIRTIO_MEDIA_MMAP_FLAG_RW; use std::str::FromStr; +/// Rust equivalent of the V4L2_CTRL_ID2WHICH C preprocessor macro. +/// Extracts the control class ID from a control ID by masking out the lower 16 bits +/// and any reserved top bits (uses mask 0x0fff0000). +/// See: https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/control.html +const fn v4l2_ctrl_id2which(id: u32) -> u32 { + id & 0x0fff0000 +} + /// https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#LENS_FACING_FRONT #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LensFacing { @@ -78,6 +86,49 @@ impl FromStr for LensFacing { } } +/// Encapsulates the camera gain value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Gain(i32); + +impl Gain { + pub const MIN: i32 = 100; + pub const MAX: i32 = 1600; + pub const DEFAULT: i32 = 100; + + pub fn new(value: i32) -> Result { + if value < Self::MIN || value > Self::MAX { + Err(libc::ERANGE) + } else { + Ok(Gain(value)) + } + } + + pub fn value(&self) -> i32 { + self.0 + } +} + +impl Default for Gain { + fn default() -> Self { + Gain(Self::DEFAULT) + } +} + +/// State of all camera controls. +struct CameraControls { + lens_facing: LensFacing, + gain: Gain, +} + +impl CameraControls { + fn new(lens_facing: LensFacing) -> Self { + Self { + lens_facing, + gain: Gain::default(), + } + } +} + /// Current status of a buffer. #[derive(Debug, PartialEq, Eq)] enum BufferState { @@ -194,6 +245,7 @@ impl VirtioMediaDeviceSession for EmulatedCameraSession { impl EmulatedCameraSession { fn write_pattern( iteration: u64, + controls: &CameraControls, mut sink_y: WY, mut sink_u: WU, mut sink_v: WV, @@ -201,7 +253,12 @@ impl EmulatedCameraSession { let mut writer_y = BufWriter::new(&mut sink_y); let mut writer_u = BufWriter::new(&mut sink_u); let mut writer_v = BufWriter::new(&mut sink_v); - let y = (iteration % 256) as u8; + // The base Y (luma) value changes over iterations to create a moving pattern. + let base_y = (iteration % 256) as u8; + // Apply gain to the luma channel. + // Gain::MIN (100) represents 1.0x gain. Higher values scale the brightness. + // We clamp the result to 255.0 to avoid overflow. + let y = ((base_y as f32) * (controls.gain.value() as f32 / Gain::MIN as f32)).min(255.0) as u8; let u = ((iteration + 64) % 256) as u8; let v = ((iteration + 128) % 256) as u8; for _ in 0..(WIDTH * HEIGHT) { @@ -220,6 +277,7 @@ impl EmulatedCameraSession { fn process_queued_buffers( &mut self, evt_queue: &mut Q, + controls: &CameraControls, ) -> IoctlResult<()> { while let Some(buf_id) = self.queued_buffers.pop_front() { let iteration = self.iteration; @@ -235,6 +293,7 @@ impl EmulatedCameraSession { Self::write_pattern( iteration, + controls, buffer.planes[0].fd.as_file(), buffer.planes[1].fd.as_file(), buffer.planes[2].fd.as_file(), @@ -271,8 +330,8 @@ pub struct EmulatedCamera, - /// Lens facing configuration. - lens_facing: LensFacing, + /// Camera controls. + controls: CameraControls, } impl EmulatedCamera @@ -285,7 +344,7 @@ where evt_queue, mmap_manager: MmapMappingManager::from(mapper), active_session: None, - lens_facing, + controls: CameraControls::new(lens_facing), } } @@ -300,13 +359,60 @@ where minimum: LensFacing::Front as i64, maximum: LensFacing::External as i64, step: 1, - default_value: self.lens_facing as i64, + default_value: self.controls.lens_facing as i64, flags: bindings::V4L2_CTRL_FLAG_READ_ONLY, elems: 1, elem_size: std::mem::size_of::() as u32, ..Default::default() } } + + fn gain_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { + let name_str = "Gain"; + let mut name = [0u8; 32]; + name[0..name_str.len()].copy_from_slice(name_str.as_bytes()); + bindings::v4l2_query_ext_ctrl { + id: bindings::V4L2_CID_GAIN, + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER, + name: name.map(|b| b as i8), + minimum: Gain::MIN as i64, + maximum: Gain::MAX as i64, + step: 1, + default_value: Gain::DEFAULT as i64, + flags: 0, + elems: 1, + elem_size: std::mem::size_of::() as u32, + ..Default::default() + } + } + + fn user_class_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { + let name_str = "User Controls"; + let mut name = [0u8; 32]; + name[0..name_str.len()].copy_from_slice(name_str.as_bytes()); + bindings::v4l2_query_ext_ctrl { + id: bindings::V4L2_CID_USER_CLASS, + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_CTRL_CLASS, + name: name.map(|b| b as i8), + // V4L2 standard requires control class headers to be marked as both RO and WO. + flags: bindings::V4L2_CTRL_FLAG_READ_ONLY | bindings::V4L2_CTRL_FLAG_WRITE_ONLY, + ..Default::default() + } + } + + fn camera_class_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { + let name_str = "Camera Controls"; + let mut name = [0u8; 32]; + name[0..name_str.len()].copy_from_slice(name_str.as_bytes()); + bindings::v4l2_query_ext_ctrl { + id: bindings::V4L2_CID_CAMERA_CLASS, + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_CTRL_CLASS, + name: name.map(|b| b as i8), + // V4L2 standard requires control class headers to be marked as both RO and WO. + flags: bindings::V4L2_CTRL_FLAG_READ_ONLY | bindings::V4L2_CTRL_FLAG_WRITE_ONLY, + ..Default::default() + } + } } impl VirtioMediaDevice for EmulatedCamera @@ -724,7 +830,7 @@ where let buffer = host_buffer.v4l2_buffer.clone(); if session.streaming { - session.process_queued_buffers(&mut self.evt_queue)?; + session.process_queued_buffers(&mut self.evt_queue, &self.controls)?; } Ok(buffer) @@ -736,7 +842,7 @@ where } session.streaming = true; - session.process_queued_buffers(&mut self.evt_queue)?; + session.process_queued_buffers(&mut self.evt_queue, &self.controls)?; Ok(()) } @@ -839,13 +945,26 @@ where flags: QueryCtrlFlags, ) -> IoctlResult { let id: u32 = unsafe { std::mem::transmute(id) }; - // If V4L2_CTRL_FLAG_NEXT_CTRL present returns the first control with a higher ID. if flags.contains(QueryCtrlFlags::NEXT) { - if id < CID_LENS_FACING { + if id < bindings::V4L2_CID_USER_CLASS { + return Ok(self.user_class_query_ext_ctrl()); + } else if id < bindings::V4L2_CID_GAIN { + return Ok(self.gain_query_ext_ctrl()); + } else if id < bindings::V4L2_CID_CAMERA_CLASS { + return Ok(self.camera_class_query_ext_ctrl()); + } else if id < CID_LENS_FACING { + return Ok(self.lens_facing_query_ext_ctrl()); + } + } else { + if id == bindings::V4L2_CID_USER_CLASS { + return Ok(self.user_class_query_ext_ctrl()); + } else if id == bindings::V4L2_CID_GAIN { + return Ok(self.gain_query_ext_ctrl()); + } else if id == bindings::V4L2_CID_CAMERA_CLASS { + return Ok(self.camera_class_query_ext_ctrl()); + } else if id == CID_LENS_FACING { return Ok(self.lens_facing_query_ext_ctrl()); } - } else if id == CID_LENS_FACING { - return Ok(self.lens_facing_query_ext_ctrl()); } return Err(libc::EINVAL); } @@ -853,15 +972,51 @@ where fn g_ext_ctrls( &mut self, _session: &Self::Session, - _which: CtrlWhich, + which: CtrlWhich, ctrls: &mut bindings::v4l2_ext_controls, ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { - for ctrl in ctrl_array { + // Validate control class. Also handles class support queries when count == 0. + match which { + CtrlWhich::Current | CtrlWhich::Default => {} + CtrlWhich::Class(class) => { + if class != bindings::V4L2_CTRL_CLASS_USER && class != bindings::V4L2_CTRL_CLASS_CAMERA { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + _ => { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + + // Ensure all requested controls belong to the selected class. + if let CtrlWhich::Class(class_id) = which { + for (idx, ctrl) in ctrl_array.iter().enumerate() { + if v4l2_ctrl_id2which(ctrl.id) != class_id { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + } + + // Process controls. Class controls are write-only headers and must fail on read. + for (idx, ctrl) in ctrl_array.iter_mut().enumerate() { match ctrl.id { + bindings::V4L2_CID_USER_CLASS | bindings::V4L2_CID_CAMERA_CLASS => { + ctrls.error_idx = ctrls.count; + return Err(libc::EACCES); + } + bindings::V4L2_CID_GAIN => { + ctrl.__bindgen_anon_1.value = match which { + CtrlWhich::Default => Gain::DEFAULT, + _ => self.controls.gain.value(), + }; + } CID_LENS_FACING => { - ctrl.__bindgen_anon_1.value = self.lens_facing as i32; + ctrl.__bindgen_anon_1.value = self.controls.lens_facing as i32; } _ => { ctrls.error_idx = ctrls.count; @@ -869,44 +1024,144 @@ where } } } + ctrls.error_idx = ctrls.count; Ok(()) } fn try_ext_ctrls( &mut self, _session: &Self::Session, - _which: CtrlWhich, + which: CtrlWhich, ctrls: &mut bindings::v4l2_ext_controls, ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { + // Validate control class. Setting defaults is not allowed for TRY/SET. + match which { + CtrlWhich::Current => {} + CtrlWhich::Class(class) => { + if class != bindings::V4L2_CTRL_CLASS_USER && class != bindings::V4L2_CTRL_CLASS_CAMERA { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + _ => { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + + // Ensure all requested controls belong to the selected class. + if let CtrlWhich::Class(class_id) = which { + for (idx, ctrl) in ctrl_array.iter().enumerate() { + if v4l2_ctrl_id2which(ctrl.id) != class_id { + ctrls.error_idx = idx as u32; + return Err(libc::EINVAL); + } + } + } + + // Validate control values. Class controls are read-only headers and must fail on write/try. for (idx, ctrl) in ctrl_array.iter_mut().enumerate() { - ctrls.error_idx = idx as u32; - let err_code = match ctrl.id { - CID_LENS_FACING => libc::EACCES, - _ => libc::EINVAL, - }; - return Err(err_code); + match ctrl.id { + bindings::V4L2_CID_USER_CLASS | bindings::V4L2_CID_CAMERA_CLASS => { + ctrls.error_idx = idx as u32; + return Err(libc::EACCES); + } + bindings::V4L2_CID_GAIN => { + let value = unsafe { ctrl.__bindgen_anon_1.value }; + if let Err(err) = Gain::new(value) { + ctrls.error_idx = idx as u32; + return Err(err); + } + } + CID_LENS_FACING => { + ctrls.error_idx = idx as u32; + return Err(libc::EACCES); + } + _ => { + ctrls.error_idx = idx as u32; + return Err(libc::EINVAL); + } + } } + ctrls.error_idx = ctrls.count; Ok(()) } fn s_ext_ctrls( &mut self, - _session: &mut Self::Session, - _which: CtrlWhich, + session: &mut Self::Session, + which: CtrlWhich, ctrls: &mut bindings::v4l2_ext_controls, ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { - for ctrl in ctrl_array { - ctrls.error_idx = ctrls.count; - let err_code = match ctrl.id { - CID_LENS_FACING => libc::EACCES, - _ => libc::EINVAL, - }; - return Err(err_code); + // Validate control class. Setting defaults is not allowed for TRY/SET. + match which { + CtrlWhich::Current => {} + CtrlWhich::Class(class) => { + if class != bindings::V4L2_CTRL_CLASS_USER && class != bindings::V4L2_CTRL_CLASS_CAMERA { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + _ => { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + + // Ensure all requested controls belong to the selected class. + if let CtrlWhich::Class(class_id) = which { + for (idx, ctrl) in ctrl_array.iter().enumerate() { + if v4l2_ctrl_id2which(ctrl.id) != class_id { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + } + + // Apply control values. Class controls are read-only headers and must fail on write/try. + for (idx, ctrl) in ctrl_array.iter_mut().enumerate() { + match ctrl.id { + bindings::V4L2_CID_USER_CLASS | bindings::V4L2_CID_CAMERA_CLASS => { + ctrls.error_idx = ctrls.count; + return Err(libc::EACCES); + } + bindings::V4L2_CID_GAIN => { + let value = unsafe { ctrl.__bindgen_anon_1.value }; + match Gain::new(value) { + Ok(gain) => { + if self.controls.gain != gain { + self.controls.gain = gain; + let ctrl_event = bindings::v4l2_event { + type_: bindings::V4L2_EVENT_CTRL, + id: bindings::V4L2_CID_GAIN, + ..Default::default() + }; + self.evt_queue.send_event(V4l2Event::Event(SessionEvent::new( + session.id, ctrl_event, + ))); + } + } + Err(err) => { + ctrls.error_idx = ctrls.count; + return Err(err); + } + } + } + CID_LENS_FACING => { + ctrls.error_idx = ctrls.count; + return Err(libc::EACCES); + } + _ => { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } } + ctrls.error_idx = ctrls.count; Ok(()) } @@ -921,16 +1176,20 @@ where } match event { V4l2EventType::Ctrl(id) => match id { - CID_LENS_FACING => { + CID_LENS_FACING | bindings::V4L2_CID_GAIN => { let ctrl_event = bindings::v4l2_event { type_: bindings::V4L2_EVENT_CTRL, - id: CID_LENS_FACING, + id, ..Default::default() }; self.evt_queue .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); Ok(()) } + bindings::V4L2_CID_USER_CLASS | bindings::V4L2_CID_CAMERA_CLASS => { + // Subscription succeeds, but we do not send any initial event. + Ok(()) + } _ => Err(libc::EINVAL), }, _ => Err(libc::EINVAL), @@ -942,7 +1201,12 @@ where _session: &mut Self::Session, event: bindings::v4l2_event_subscription, ) -> IoctlResult<()> { - return if event.type_ == bindings::V4L2_EVENT_CTRL && event.id == CID_LENS_FACING { + return if event.type_ == bindings::V4L2_EVENT_CTRL + && (event.id == CID_LENS_FACING + || event.id == bindings::V4L2_CID_GAIN + || event.id == bindings::V4L2_CID_USER_CLASS + || event.id == bindings::V4L2_CID_CAMERA_CLASS) + { Ok(()) } else { Err(libc::EINVAL)