From ab461abe8a99b9721a2a86dfa3d7a9197078c297 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Tue, 10 Nov 2026 17:11:01 -0500 Subject: [PATCH 1/3] Make emulated camera controls pluggable per backend Each backend now selects the V4L2 controls it exposes. No change in the controls exposed by either backend. Bug: b/584385597 Assisted-by: Jetski:Claude --- .../emulated_camera/BUILD.bazel | 6 + .../emulated_camera/src/capture_channel.rs | 35 +- .../emulated_camera/src/control.rs | 489 +++++++++++++ .../emulated_camera/src/controls/gain.rs | 71 ++ .../src/controls/lens_facing.rs | 94 +++ .../emulated_camera/src/controls/mod.rs | 21 + .../src/controls/test_pattern.rs | 82 +++ .../emulated_camera/src/device.rs | 657 ++---------------- .../emulated_camera/src/emulated_scene.rs | 6 +- .../emulated_camera/src/lib.rs | 16 +- .../emulated_camera/src/pattern/julia_set.rs | 4 +- .../emulated_camera/src/pattern/mod.rs | 12 +- .../emulated_camera/src/pattern/pulse.rs | 6 +- .../emulated_camera/src/pattern/smpte.rs | 4 +- .../emulated_camera/src/render_params.rs | 120 ++++ .../emulated_camera_mplane/src/main.rs | 14 +- .../src/main.rs | 13 +- 17 files changed, 996 insertions(+), 654 deletions(-) create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/control.rs create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/gain.rs create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/lens_facing.rs create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/mod.rs create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/test_pattern.rs create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/render_params.rs diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/BUILD.bazel b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/BUILD.bazel index 1205d460f2b..3d30e5f16f2 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/BUILD.bazel @@ -7,6 +7,11 @@ rust_library( name = "emulated_camera", srcs = [ "src/capture_channel.rs", + "src/control.rs", + "src/controls/gain.rs", + "src/controls/lens_facing.rs", + "src/controls/mod.rs", + "src/controls/test_pattern.rs", "src/device.rs", "src/emulated_scene.rs", "src/format.rs", @@ -16,6 +21,7 @@ rust_library( "src/pattern/mod.rs", "src/pattern/pulse.rs", "src/pattern/smpte.rs", + "src/render_params.rs", ], edition = "2024", target_compatible_with = ["@platforms//cpu:x86_64"], diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/capture_channel.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/capture_channel.rs index 6ffb79436e5..70a96f00965 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/capture_channel.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/capture_channel.rs @@ -18,8 +18,9 @@ use std::io::Write; use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender}; use std::time::{Duration, Instant}; -use crate::device::{CameraControls, FrameSource}; +use crate::device::FrameSource; use crate::format::Yuv420m; +use crate::render_params::RenderParams; /// Where a capture request is rendered. pub trait CaptureSink: Send + Sync { @@ -48,8 +49,8 @@ pub enum ChannelCmd { StopCapture, /// Enqueue a capture request into the channel's request queue. Queue(CaptureRequest), - /// Update channel controls (e.g. pattern, gain). - SetControls(CameraControls), + /// Update the render parameters (e.g. pattern, gain). + SetRenderParams(RenderParams), /// Terminate capture worker thread. Shutdown, } @@ -166,7 +167,7 @@ impl CaptureClock { /// Generate capture data. pub struct CaptureProcessor { session_id: u32, - controls: CameraControls, + params: RenderParams, frame_source: FrameSource, width: u32, height: u32, @@ -180,13 +181,13 @@ pub struct CaptureProcessor { impl CaptureProcessor { pub fn new( session_id: u32, - initial_controls: CameraControls, + initial_params: RenderParams, frame_source: FrameSource, completion_tx: Sender, ) -> Self { Self { session_id, - controls: initial_controls, + params: initial_params, frame_source, width: 640, height: 480, @@ -257,8 +258,8 @@ impl CaptureProcessor { ChannelCmd::Queue(req) => { self.pending_requests.push_back(req); } - ChannelCmd::SetControls(c) => { - self.controls = c; + ChannelCmd::SetRenderParams(params) => { + self.params = params; } ChannelCmd::Shutdown => unreachable!(), } @@ -274,7 +275,7 @@ impl CaptureProcessor { if let Err(e) = Self::write_pattern( seq as u64, self.frame_source, - &self.controls, + &self.params, self.width, self.height, sink_y, @@ -317,7 +318,7 @@ impl CaptureProcessor { fn write_pattern( iteration: u64, frame_source: FrameSource, - controls: &CameraControls, + params: &RenderParams, width: u32, height: u32, sink_y: &mut dyn Write, @@ -325,8 +326,8 @@ impl CaptureProcessor { sink_v: &mut dyn Write, ) -> AnyhowResult<()> { frame_source - .generator(controls) - .write(iteration, controls, width, height, sink_y, sink_u, sink_v) + .generator(params) + .write(iteration, params, width, height, sink_y, sink_u, sink_v) .map_err(|e| anyhow::anyhow!("Pattern generator write error: errno {}", e)) } } @@ -339,7 +340,7 @@ pub struct CaptureChannel { impl CaptureChannel { pub fn new( session_id: u32, - initial_controls: CameraControls, + initial_params: RenderParams, frame_source: FrameSource, completion_tx: Sender, ) -> AnyhowResult { @@ -347,7 +348,7 @@ impl CaptureChannel { let (cmd_tx, cmd_rx) = std::sync::mpsc::channel(); let mut processor = - CaptureProcessor::new(session_id, initial_controls, frame_source, completion_tx); + CaptureProcessor::new(session_id, initial_params, frame_source, completion_tx); std::thread::Builder::new() .name(format!("channel-{}", session_id)) @@ -372,9 +373,9 @@ impl CaptureChannel { self.send_cmd(ChannelCmd::Queue(CaptureRequest { buffer_id, sink })) } - /// Updates channel controls (e.g. test pattern, gain). - pub fn set_controls(&self, controls: CameraControls) -> AnyhowResult<()> { - self.send_cmd(ChannelCmd::SetControls(controls)) + /// Updates the render parameters (e.g. test pattern, gain). + pub fn set_render_params(&self, params: RenderParams) -> AnyhowResult<()> { + self.send_cmd(ChannelCmd::SetRenderParams(params)) } /// Signals the processor thread to terminate. diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/control.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/control.rs new file mode 100644 index 00000000000..f91b71a980d --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/control.rs @@ -0,0 +1,489 @@ +// Copyright 2026, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Generic V4L2 control machinery. + +use v4l2r::bindings; +use v4l2r::ioctl::CtrlWhich; +use virtio_media::ioctl::IoctlResult; + +use crate::render_params::RenderParams; + +/// 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 +pub const fn v4l2_ctrl_id2which(id: u32) -> u32 { + id & 0x0fff0000 +} + +/// `V4L2_CID_*_CLASS` id of `class`. +const fn class_header_id(class: u32) -> u32 { + class | 1 +} + +/// Formats an ASCII name for a `v4l2_query_ext_ctrl` or `v4l2_querymenu` response, NULL-padding +/// the remainder of the 32-byte array. If `name` is longer than 31 bytes it is truncated and +/// the last byte is still forced to NULL. +pub fn ctrl_name(name: &str) -> [u8; 32] { + const MAX_LEN: usize = 32; + let mut out = [0u8; MAX_LEN]; + let copy_len = std::cmp::min(name.len(), MAX_LEN - 1); + out[..copy_len].copy_from_slice(&name.as_bytes()[..copy_len]); + out +} + +/// Name of a control class, as reported by the kernel. +fn class_name(class: u32) -> &'static str { + match class { + bindings::V4L2_CTRL_CLASS_USER => "User Controls", + bindings::V4L2_CTRL_CLASS_CAMERA => "Camera Controls", + bindings::V4L2_CTRL_CLASS_IMAGE_PROC => "Image Processing Controls", + _ => unreachable!( + "control class {:#x} has no name, add it to class_name()", + class + ), + } +} + +/// What `VIDIOC_QUERY_EXT_CTRL` reports for a control, besides its id. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ControlDescriptor { + /// `V4L2_CTRL_TYPE_*`. + pub type_: u32, + /// Truncated to 31 bytes. + pub name: &'static str, + pub minimum: i64, + pub maximum: i64, + pub step: u64, + pub default_value: i64, + /// `V4L2_CTRL_FLAG_*`. + pub flags: u32, +} + +impl ControlDescriptor { + fn is_64bit(&self) -> bool { + self.type_ == bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER64 + } + + fn elem_size(&self) -> u32 { + if self.is_64bit() { + std::mem::size_of::() as u32 + } else { + std::mem::size_of::() as u32 + } + } + + fn is_read_only(&self) -> bool { + self.flags & bindings::V4L2_CTRL_FLAG_READ_ONLY != 0 + } +} + +/// A V4L2 control. +pub trait Control: Send + Sync { + /// `V4L2_CID_*`. + fn id(&self) -> u32; + + fn descriptor(&self) -> ControlDescriptor; + + /// Current value. + fn value(&self) -> i64; + + /// Validates `value` without applying it. Returns the `errno` reported to the guest. + fn validate(&self, value: i64) -> Result<(), i32>; + + /// Validates and applies `value`. Returns whether the value changed. + fn set(&mut self, value: i64) -> Result; + + /// Name of menu entry `index`, `None` if there is no such entry. + fn menu_name(&self, _index: u32) -> Option<&'static str> { + None + } + + /// Projects the value into `params`. + fn apply(&self, _params: &mut RenderParams) {} +} + +/// Result of a successful `VIDIOC_S_CTRL`. +pub struct SetControlOutcome { + pub control: bindings::v4l2_control, + /// Set if the value changed. + pub event: Option, +} + +/// The controls of a device. +pub struct ControlSet { + /// Sorted by id. + controls: Vec>, + /// Classes of `controls`, sorted. + classes: Vec, +} + +impl ControlSet { + /// Panics if two controls share an id. + pub fn new(mut controls: Vec>) -> Self { + controls.sort_by_key(|control| control.id()); + if let Some(pair) = controls + .windows(2) + .find(|pair| pair[0].id() == pair[1].id()) + { + panic!("duplicate control id {:#x}", pair[0].id()); + } + let mut classes: Vec = controls + .iter() + .map(|control| v4l2_ctrl_id2which(control.id())) + .collect(); + classes.dedup(); + Self { controls, classes } + } + + pub fn empty() -> Self { + Self::new(Vec::new()) + } + + pub fn render_params(&self) -> RenderParams { + let mut params = RenderParams::default(); + for control in &self.controls { + control.apply(&mut params); + } + params + } + + fn find(&self, id: u32) -> Option<&dyn Control> { + self.controls + .iter() + .find(|control| control.id() == id) + .map(|control| control.as_ref()) + } + + fn find_mut(&mut self, id: u32) -> Option<&mut Box> { + self.controls.iter_mut().find(|control| control.id() == id) + } + + fn is_class_header(&self, id: u32) -> bool { + self.classes + .iter() + .any(|class| class_header_id(*class) == id) + } + + fn has_class(&self, class: u32) -> bool { + self.classes.contains(&class) + } + + /// Whether `id` is a control or a class header. + pub fn knows(&self, id: u32) -> bool { + self.is_class_header(id) || self.find(id).is_some() + } + + /// Ids in `V4L2_CTRL_FLAG_NEXT_CTRL` order. + fn enumeration(&self) -> impl Iterator + '_ { + let mut ids: Vec = self + .classes + .iter() + .map(|class| class_header_id(*class)) + .chain(self.controls.iter().map(|control| control.id())) + .collect(); + ids.sort_unstable(); + ids.into_iter() + } + + fn class_header_query_ext_ctrl(class: u32) -> bindings::v4l2_query_ext_ctrl { + bindings::v4l2_query_ext_ctrl { + id: class_header_id(class), + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_CTRL_CLASS, + name: ctrl_name(class_name(class)).map(|b| b as i8), + // A control class holds no value of its own, so it can be neither read nor + // written. This mirrors what `v4l2_ctrl_fill()` does in the kernel. + flags: bindings::V4L2_CTRL_FLAG_READ_ONLY | bindings::V4L2_CTRL_FLAG_WRITE_ONLY, + elems: 1, + elem_size: std::mem::size_of::() as u32, + ..Default::default() + } + } + + fn control_query_ext_ctrl(control: &dyn Control) -> bindings::v4l2_query_ext_ctrl { + let descriptor = control.descriptor(); + bindings::v4l2_query_ext_ctrl { + id: control.id(), + type_: descriptor.type_, + name: ctrl_name(descriptor.name).map(|b| b as i8), + minimum: descriptor.minimum, + maximum: descriptor.maximum, + step: descriptor.step, + default_value: descriptor.default_value, + flags: descriptor.flags, + elems: 1, + elem_size: descriptor.elem_size(), + ..Default::default() + } + } + + fn query(&self, id: u32) -> Option { + if self.is_class_header(id) { + Some(Self::class_header_query_ext_ctrl(v4l2_ctrl_id2which(id))) + } else { + self.find(id).map(Self::control_query_ext_ctrl) + } + } + + /// `VIDIOC_QUERY_EXT_CTRL`. With `next` the first id greater than `id` is reported. + /// + /// https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-queryctrl.html + pub fn query_ext_ctrl( + &self, + id: u32, + next: bool, + ) -> IoctlResult { + let id = if next { + self.enumeration() + .find(|candidate| *candidate > id) + .ok_or(libc::EINVAL)? + } else { + id + }; + self.query(id).ok_or(libc::EINVAL) + } + + /// `VIDIOC_QUERYMENU`. + pub fn querymenu(&self, id: u32, index: u32) -> IoctlResult { + let name = self + .find(id) + .and_then(|control| control.menu_name(index)) + .ok_or(libc::EINVAL)?; + Ok(bindings::v4l2_querymenu { + id, + index, + __bindgen_anon_1: bindings::v4l2_querymenu__bindgen_ty_1 { + name: ctrl_name(name), + }, + ..Default::default() + }) + } + + /// `VIDIOC_G_CTRL`. + pub fn g_ctrl(&self, id: u32) -> IoctlResult { + if self.is_class_header(id) { + return Err(libc::EACCES); + } + let control = self.find(id).ok_or(libc::EINVAL)?; + Ok(bindings::v4l2_control { + id, + value: control.value() as i32, + }) + } + + /// `VIDIOC_S_CTRL`. + pub fn s_ctrl(&mut self, id: u32, value: i32) -> IoctlResult { + if self.is_class_header(id) { + return Err(libc::EACCES); + } + let control = self.find_mut(id).ok_or(libc::EINVAL)?; + if control.descriptor().is_read_only() { + return Err(libc::EACCES); + } + let changed = control.set(value as i64)?; + let control = bindings::v4l2_control { + id, + value: control.value() as i32, + }; + let event = if changed { + Some(self.ctrl_event(id)?) + } else { + None + }; + Ok(SetControlOutcome { control, event }) + } + + /// Validates `which` and, for a class, that every control in `ctrl_array` belongs to it. + fn check_which( + &self, + which: &CtrlWhich, + allow_default: bool, + ctrls: &mut bindings::v4l2_ext_controls, + ctrl_array: &[bindings::v4l2_ext_control], + per_control_error_idx: bool, + ) -> IoctlResult<()> { + match which { + CtrlWhich::Current => {} + CtrlWhich::Default if allow_default => {} + CtrlWhich::Class(class) if self.has_class(*class) => { + for (idx, ctrl) in ctrl_array.iter().enumerate() { + if v4l2_ctrl_id2which(ctrl.id) != *class { + ctrls.error_idx = if per_control_error_idx { + idx as u32 + } else { + ctrls.count + }; + return Err(libc::EINVAL); + } + } + } + _ => { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + } + } + Ok(()) + } + + fn read_value(ctrl: &bindings::v4l2_ext_control, descriptor: &ControlDescriptor) -> i64 { + unsafe { + if descriptor.is_64bit() { + ctrl.__bindgen_anon_1.value64 + } else { + ctrl.__bindgen_anon_1.value as i64 + } + } + } + + fn write_value( + ctrl: &mut bindings::v4l2_ext_control, + descriptor: &ControlDescriptor, + value: i64, + ) { + if descriptor.is_64bit() { + ctrl.__bindgen_anon_1.value64 = value; + } else { + ctrl.__bindgen_anon_1.value = value as i32; + } + } + + /// `VIDIOC_G_EXT_CTRLS`. + pub fn g_ext_ctrls( + &self, + which: CtrlWhich, + ctrls: &mut bindings::v4l2_ext_controls, + ctrl_array: &mut [bindings::v4l2_ext_control], + ) -> IoctlResult<()> { + self.check_which(&which, true, ctrls, ctrl_array, false)?; + + for ctrl in ctrl_array.iter_mut() { + // Class headers hold no value and must fail on read. + if self.is_class_header(ctrl.id) { + ctrls.error_idx = ctrls.count; + return Err(libc::EACCES); + } + let Some(control) = self.find(ctrl.id) else { + ctrls.error_idx = ctrls.count; + return Err(libc::EINVAL); + }; + let descriptor = control.descriptor(); + let value = match which { + CtrlWhich::Default => descriptor.default_value, + _ => control.value(), + }; + Self::write_value(ctrl, &descriptor, value); + } + ctrls.error_idx = ctrls.count; + Ok(()) + } + + /// `VIDIOC_TRY_EXT_CTRLS`. + pub fn try_ext_ctrls( + &self, + which: CtrlWhich, + ctrls: &mut bindings::v4l2_ext_controls, + ctrl_array: &mut [bindings::v4l2_ext_control], + ) -> IoctlResult<()> { + self.check_which(&which, false, ctrls, ctrl_array, true)?; + + for (idx, ctrl) in ctrl_array.iter().enumerate() { + // Class headers hold no value and must fail on write. + if self.is_class_header(ctrl.id) { + ctrls.error_idx = idx as u32; + return Err(libc::EACCES); + } + let Some(control) = self.find(ctrl.id) else { + ctrls.error_idx = idx as u32; + return Err(libc::EINVAL); + }; + let descriptor = control.descriptor(); + if descriptor.is_read_only() { + ctrls.error_idx = idx as u32; + return Err(libc::EACCES); + } + if let Err(err) = control.validate(Self::read_value(ctrl, &descriptor)) { + ctrls.error_idx = idx as u32; + return Err(err); + } + } + ctrls.error_idx = ctrls.count; + Ok(()) + } + + /// `VIDIOC_S_EXT_CTRLS`. Returns the events of the controls whose value changed. + pub fn s_ext_ctrls( + &mut self, + which: CtrlWhich, + ctrls: &mut bindings::v4l2_ext_controls, + ctrl_array: &mut [bindings::v4l2_ext_control], + ) -> IoctlResult> { + // Validate everything first so that the set is applied atomically. + if let Err(err) = self.try_ext_ctrls(which, ctrls, ctrl_array) { + ctrls.error_idx = ctrls.count; + return Err(err); + } + + let mut events = Vec::new(); + for ctrl in ctrl_array.iter_mut() { + let id = ctrl.id; + let Some(control) = self.find_mut(id) else { + continue; + }; + let descriptor = control.descriptor(); + let value = Self::read_value(ctrl, &descriptor); + if let Ok(true) = control.set(value) { + if let Ok(event) = self.ctrl_event(id) { + events.push(event); + } + } + } + ctrls.error_idx = ctrls.count; + Ok(events) + } + + /// Builds the `V4L2_EVENT_CTRL` payload describing the current state of `id`. + pub fn ctrl_event(&self, id: u32) -> IoctlResult { + let control = self.find(id).ok_or(libc::EINVAL)?; + let descriptor = control.descriptor(); + let mut event = bindings::v4l2_event { + type_: bindings::V4L2_EVENT_CTRL, + id, + ..Default::default() + }; + // Listeners gate on this mask, so an event without it is silently ignored. + event.u.ctrl.changes = bindings::V4L2_EVENT_CTRL_CH_VALUE; + event.u.ctrl.type_ = descriptor.type_; + if descriptor.is_64bit() { + event.u.ctrl.__bindgen_anon_1.value64 = control.value(); + } else { + event.u.ctrl.__bindgen_anon_1.value = control.value() as i32; + } + event.u.ctrl.flags = descriptor.flags; + event.u.ctrl.minimum = descriptor.minimum as i32; + event.u.ctrl.maximum = descriptor.maximum as i32; + event.u.ctrl.step = descriptor.step as i32; + event.u.ctrl.default_value = descriptor.default_value as i32; + Ok(event) + } + + /// `VIDIOC_SUBSCRIBE_EVENT` for `V4L2_EVENT_CTRL` on `id`. Returns the initial event, `None` + /// for class headers. + pub fn subscribe_ctrl_event(&self, id: u32) -> IoctlResult> { + if self.is_class_header(id) { + return Ok(None); + } + self.ctrl_event(id).map(Some) + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/gain.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/gain.rs new file mode 100644 index 00000000000..aa74316548f --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/gain.rs @@ -0,0 +1,71 @@ +// Copyright 2026, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! `V4L2_CID_GAIN`: luma scaling applied by the frame generators. + +use v4l2r::bindings; + +use crate::control::{Control, ControlDescriptor}; +use crate::render_params::{Gain, RenderParams}; + +/// Read-write integer control backed by [`Gain`]. +#[derive(Debug, Default)] +pub struct GainControl { + gain: Gain, +} + +impl GainControl { + pub fn new() -> Self { + Self::default() + } +} + +impl Control for GainControl { + fn id(&self) -> u32 { + bindings::V4L2_CID_GAIN + } + + fn descriptor(&self) -> ControlDescriptor { + ControlDescriptor { + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER, + name: "Gain", + minimum: Gain::MIN as i64, + maximum: Gain::MAX as i64, + step: 1, + default_value: Gain::DEFAULT as i64, + flags: 0, + } + } + + fn value(&self) -> i64 { + self.gain.value() as i64 + } + + fn validate(&self, value: i64) -> Result<(), i32> { + let value = i32::try_from(value).map_err(|_| libc::ERANGE)?; + Gain::new(value).map(|_| ()) + } + + fn set(&mut self, value: i64) -> Result { + let value = i32::try_from(value).map_err(|_| libc::ERANGE)?; + let gain = Gain::new(value)?; + let changed = self.gain != gain; + self.gain = gain; + Ok(changed) + } + + fn apply(&self, params: &mut RenderParams) { + params.gain = self.gain; + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/lens_facing.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/lens_facing.rs new file mode 100644 index 00000000000..f0186f7f148 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/lens_facing.rs @@ -0,0 +1,94 @@ +// Copyright 2026, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! `CID_LENS_FACING`: read-only, virtio-media specific control telling the guest camera HAL +//! which way the camera faces. + +use std::str::FromStr; + +use v4l2r::bindings; + +use crate::control::{Control, ControlDescriptor}; + +/// Offset for virtio-media custom camera class control id values. +const CID_OFFSET: u32 = bindings::V4L2_CID_CAMERA_CLASS_BASE + 0x100; +/// Id of the lens facing control. +pub const CID_LENS_FACING: u32 = CID_OFFSET + 1; + +/// https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#LENS_FACING_FRONT +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LensFacing { + Front = 0, + Back = 1, + External = 2, +} + +impl FromStr for LensFacing { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "FRONT" => Ok(LensFacing::Front), + "BACK" => Ok(LensFacing::Back), + "EXTERNAL" => Ok(LensFacing::External), + _ => Err(format!( + "Invalid lens facing: {}. Expected FRONT, BACK, or EXTERNAL", + s + )), + } + } +} + +/// Read-only integer control reporting a fixed [`LensFacing`]. +#[derive(Debug)] +pub struct LensFacingControl { + lens_facing: LensFacing, +} + +impl LensFacingControl { + pub fn new(lens_facing: LensFacing) -> Self { + Self { lens_facing } + } +} + +impl Control for LensFacingControl { + fn id(&self) -> u32 { + CID_LENS_FACING + } + + fn descriptor(&self) -> ControlDescriptor { + ControlDescriptor { + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER, + name: "LENS_FACING", + minimum: LensFacing::Front as i64, + maximum: LensFacing::External as i64, + step: 1, + // The value never changes, so it is its own default. + default_value: self.lens_facing as i64, + flags: bindings::V4L2_CTRL_FLAG_READ_ONLY, + } + } + + fn value(&self) -> i64 { + self.lens_facing as i64 + } + + fn validate(&self, _value: i64) -> Result<(), i32> { + Err(libc::EACCES) + } + + fn set(&mut self, _value: i64) -> Result { + Err(libc::EACCES) + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/mod.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/mod.rs new file mode 100644 index 00000000000..85ab9b4ed2a --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/mod.rs @@ -0,0 +1,21 @@ +// Copyright 2026, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod gain; +pub mod lens_facing; +pub mod test_pattern; + +pub use gain::GainControl; +pub use lens_facing::{CID_LENS_FACING, LensFacing, LensFacingControl}; +pub use test_pattern::TestPatternControl; diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/test_pattern.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/test_pattern.rs new file mode 100644 index 00000000000..516b894abd7 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/controls/test_pattern.rs @@ -0,0 +1,82 @@ +// Copyright 2026, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! `V4L2_CID_TEST_PATTERN`: menu control selecting the generated test pattern. + +use v4l2r::bindings; + +use crate::control::{Control, ControlDescriptor}; +use crate::render_params::{RenderParams, TestPattern}; + +/// Read-write menu control backed by [`TestPattern`]. +#[derive(Debug, Default)] +pub struct TestPatternControl { + pattern: TestPattern, +} + +impl TestPatternControl { + pub fn new() -> Self { + Self::default() + } + + fn parse(value: i64) -> Result { + let value = i32::try_from(value).map_err(|_| libc::ERANGE)?; + TestPattern::try_from(value) + } +} + +impl Control for TestPatternControl { + fn id(&self) -> u32 { + bindings::V4L2_CID_TEST_PATTERN + } + + fn descriptor(&self) -> ControlDescriptor { + ControlDescriptor { + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU, + name: "Test Pattern", + minimum: TestPattern::MIN as i64, + maximum: TestPattern::MAX as i64, + step: 1, + default_value: TestPattern::DEFAULT as i64, + flags: 0, + } + } + + fn value(&self) -> i64 { + self.pattern as i64 + } + + fn validate(&self, value: i64) -> Result<(), i32> { + Self::parse(value).map(|_| ()) + } + + fn set(&mut self, value: i64) -> Result { + let pattern = Self::parse(value)?; + let changed = self.pattern != pattern; + self.pattern = pattern; + Ok(changed) + } + + fn menu_name(&self, index: u32) -> Option<&'static str> { + // Menu indices are the `TestPattern` discriminants, so the enum decides the range. + i32::try_from(index) + .ok() + .and_then(|index| TestPattern::try_from(index).ok()) + .map(TestPattern::name) + } + + fn apply(&self, params: &mut RenderParams) { + params.test_pattern = self.pattern; + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/device.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/device.rs index 4383fa35337..baf474564a6 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/device.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/device.rs @@ -15,17 +15,15 @@ use anyhow::{Context, Result as AnyhowResult}; use std::io::Result as IoResult; use std::os::fd::BorrowedFd; -use std::str::FromStr; use std::sync::{Arc, Mutex}; use crate::capture_channel::CaptureChannel; +use crate::control::ControlSet; +use crate::emulated_scene::EmulatedScene; use crate::format::Yuv420m; use crate::memory::{BufferMemoryHandler, QbufRequest, QueuedBuffer}; -use crate::emulated_scene::EmulatedScene; use crate::pattern::FramePattern; -use crate::pattern::julia_set::JuliaSet; -use crate::pattern::pulse::Pulse; -use crate::pattern::smpte::SmpteBars; +use crate::render_params::RenderParams; use v4l2r::QueueType; use v4l2r::bindings; @@ -56,136 +54,6 @@ use virtio_media::protocol::SgEntry; use virtio_media::protocol::V4l2Event; use virtio_media::protocol::V4l2Ioctl; -/// 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 { - Front = 0, - Back = 1, - External = 2, -} - -impl FromStr for LensFacing { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "FRONT" => Ok(LensFacing::Front), - "BACK" => Ok(LensFacing::Back), - "EXTERNAL" => Ok(LensFacing::External), - _ => Err(format!( - "Invalid lens facing: {}. Expected FRONT, BACK, or EXTERNAL", - s - )), - } - } -} - -/// 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) - } -} - -/// Test pattern selectable through `V4L2_CID_TEST_PATTERN`. -/// -/// The discriminants double as the menu indices reported by `VIDIOC_QUERYMENU`, so they -/// must stay contiguous and start at [`TestPattern::MIN`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TestPattern { - Pulse = 0, - SmpteBars = 1, - JuliaSet = 2, -} - -impl TestPattern { - /// Lowest menu index, reported as `minimum` for `V4L2_CID_TEST_PATTERN`. - const MIN: i32 = TestPattern::Pulse as i32; - /// Highest menu index, reported as `maximum` for `V4L2_CID_TEST_PATTERN`. - const MAX: i32 = TestPattern::JuliaSet as i32; - /// Pattern selected until the guest asks for something else. - const DEFAULT: TestPattern = TestPattern::Pulse; - - /// Human readable name reported by `VIDIOC_QUERYMENU`. - fn name(self) -> &'static str { - match self { - TestPattern::Pulse => "Pulse", - TestPattern::SmpteBars => "SMPTE + Bouncing Box", - TestPattern::JuliaSet => "Animated Julia Set", - } - } - - /// Frame generator backing this pattern. - pub(crate) fn generator(self) -> &'static dyn FramePattern { - match self { - TestPattern::Pulse => &Pulse, - TestPattern::SmpteBars => &SmpteBars, - TestPattern::JuliaSet => &JuliaSet, - } - } -} - -impl TryFrom for TestPattern { - /// Raw `errno` reported to the guest for an out-of-range menu index. - type Error = i32; - - fn try_from(value: i32) -> Result { - match value { - 0 => Ok(TestPattern::Pulse), - 1 => Ok(TestPattern::SmpteBars), - 2 => Ok(TestPattern::JuliaSet), - _ => Err(libc::ERANGE), - } - } -} - -/// State of all camera controls. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CameraControls { - pub lens_facing: LensFacing, - pub gain: Gain, - pub test_pattern: TestPattern, -} - -impl CameraControls { - pub fn new(lens_facing: LensFacing) -> Self { - Self { - lens_facing, - gain: Gain::default(), - test_pattern: TestPattern::DEFAULT, - } - } -} - static EMULATED_SCENE: EmulatedScene = EmulatedScene { hour: EmulatedScene::DEFAULT_HOUR, }; @@ -200,25 +68,15 @@ pub enum FrameSource { } impl FrameSource { - /// Frame generator for this source given the current controls. - pub(crate) fn generator(self, controls: &CameraControls) -> &'static dyn FramePattern { + /// Frame generator for this source given the current render parameters. + pub(crate) fn generator(self, params: &RenderParams) -> &'static dyn FramePattern { match self { - FrameSource::TestPatterns => controls.test_pattern.generator(), + FrameSource::TestPatterns => params.test_pattern.generator(), FrameSource::EmulatedScene => &EMULATED_SCENE, } } } -/// Formats an ASCII name for a `v4l2_query_ext_ctrl` or `v4l2_querymenu` response, NUL-padding -/// the remainder of the 32-byte array. If `name` is longer than 31 bytes it is truncated and -/// the last byte is still forced to NUL. -fn ctrl_name(name: &str) -> [u8; 32] { - let mut out = [0u8; 32]; - let copy_len = std::cmp::min(name.len(), 31); - out[..copy_len].copy_from_slice(&name.as_bytes()[..copy_len]); - out -} - /// Current status of a buffer. #[derive(Debug, PartialEq, Eq)] enum BufferState { @@ -349,7 +207,8 @@ impl VirtioMediaDeviceSession for EmulatedCameraSession { /// This implementation looks forward to have feature parity with existing Android Guest Emulated /// camera https://cs.android.com/android/platform/superproject/main/+/main:hardware/google/camera/devices/EmulatedCamera/ /// -/// `M` decides how buffers are backed, see [`BufferMemoryHandler`]. +/// `M` decides how buffers are backed, see [`BufferMemoryHandler`]. The V4L2 controls exposed +/// to the guest are whatever the backend registered in its [`ControlSet`]. pub struct EmulatedCamera { /// Memory handler of the capture buffers. buffer_memory_handler: M, @@ -362,8 +221,8 @@ pub struct EmulatedCamera { active_session: Option, /// Queue used to send events to the guest. evt_queue: Arc>, - /// Camera controls (gain, lens facing, test pattern). - controls: CameraControls, + /// V4L2 controls exposed to the guest. + controls: ControlSet, /// What is rendered into capture buffers. frame_source: FrameSource, /// Width of the video. @@ -380,14 +239,14 @@ where pub fn new( evt_queue: Q, buffer_memory_handler: M, - lens_facing: LensFacing, + controls: ControlSet, frame_source: FrameSource, ) -> Self { Self { buffer_memory_handler, active_session: None, evt_queue: Arc::new(Mutex::new(evt_queue)), - controls: CameraControls::new(lens_facing), + controls, frame_source, width: WIDTH, height: HEIGHT, @@ -413,155 +272,19 @@ where Ok(Buffer::new(v4l2_buffer, allocation)) } - fn lens_facing_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { - let lens_facing = self.controls.lens_facing; - bindings::v4l2_query_ext_ctrl { - id: CID_LENS_FACING, - type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER, - name: ctrl_name("LENS_FACING").map(|b| b as i8), - minimum: LensFacing::Front as i64, - maximum: LensFacing::External as i64, - step: 1, - default_value: 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() - } - } - - fn image_proc_class_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { - bindings::v4l2_query_ext_ctrl { - id: bindings::V4L2_CID_IMAGE_PROC_CLASS, - type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_CTRL_CLASS, - name: ctrl_name("Image Processing Controls").map(|b| b as i8), - minimum: 0, - maximum: 0, - step: 0, - default_value: 0, - // A control class holds no value of its own, so it can be neither read nor - // written. This mirrors what `v4l2_ctrl_fill()` does in the kernel. - flags: bindings::V4L2_CTRL_FLAG_READ_ONLY | bindings::V4L2_CTRL_FLAG_WRITE_ONLY, - elems: 1, - elem_size: std::mem::size_of::() as u32, - ..Default::default() - } - } - - fn test_pattern_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { - bindings::v4l2_query_ext_ctrl { - id: bindings::V4L2_CID_TEST_PATTERN, - type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU, - name: ctrl_name("Test Pattern").map(|b| b as i8), - minimum: TestPattern::MIN as i64, - maximum: TestPattern::MAX as i64, - step: 1, - default_value: TestPattern::DEFAULT as i64, - flags: 0, - elems: 1, - elem_size: std::mem::size_of::() as u32, - ..Default::default() - } - } - - /// Builds the `V4L2_EVENT_CTRL` payload describing the current state of `id`. - fn ctrl_event(controls: &CameraControls, id: u32) -> IoctlResult { - let mut event = bindings::v4l2_event { - type_: bindings::V4L2_EVENT_CTRL, - id, - ..Default::default() - }; - match id { - CID_LENS_FACING => { - event.u.ctrl.type_ = bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER; - event.u.ctrl.__bindgen_anon_1.value = controls.lens_facing as i32; - event.u.ctrl.minimum = LensFacing::Front as i32; - event.u.ctrl.maximum = LensFacing::External as i32; - event.u.ctrl.step = 1; - event.u.ctrl.default_value = LensFacing::Front as i32; - } - bindings::V4L2_CID_GAIN => { - event.u.ctrl.type_ = bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER; - event.u.ctrl.__bindgen_anon_1.value = controls.gain.value(); - event.u.ctrl.minimum = Gain::MIN; - event.u.ctrl.maximum = Gain::MAX; - event.u.ctrl.step = 1; - event.u.ctrl.default_value = Gain::DEFAULT; - } - bindings::V4L2_CID_TEST_PATTERN => { - event.u.ctrl.type_ = bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU; - event.u.ctrl.__bindgen_anon_1.value = controls.test_pattern as i32; - event.u.ctrl.minimum = TestPattern::MIN; - event.u.ctrl.maximum = TestPattern::MAX; - event.u.ctrl.step = 1; - event.u.ctrl.default_value = TestPattern::DEFAULT as i32; - } - _ => return Err(libc::EINVAL), - } - // Listeners gate on this mask, so an event without it is silently ignored. - event.u.ctrl.changes = bindings::V4L2_EVENT_CTRL_CH_VALUE; - Ok(event) - } - - /// Applies `pattern`, signalling subscribers if the value actually changed. - fn set_test_pattern(&mut self, session_id: u32, pattern: TestPattern) -> IoctlResult<()> { - if self.controls.test_pattern == pattern { - return Ok(()); - } - self.controls.test_pattern = pattern; - let event = Self::ctrl_event(&self.controls, bindings::V4L2_CID_TEST_PATTERN)?; + /// Broadcasts `event` to the guest on behalf of `session_id`. + fn send_event(&self, session_id: u32, event: bindings::v4l2_event) { self.evt_queue .lock() .unwrap() .send_event(V4l2Event::Event(SessionEvent::new(session_id, event))); - Ok(()) + } + + /// Pushes the current control state to the frame generators of `session`. + fn sync_render_params(&self, session: &EmulatedCameraSession) { + let _ = session + .channel + .set_render_params(self.controls.render_params()); } } @@ -582,7 +305,7 @@ where let channel = CaptureChannel::new( session_id, - self.controls.clone(), + self.controls.render_params(), self.frame_source, capture_completed_tx, ) @@ -676,10 +399,6 @@ where } } -// Use an offset for virtio-media custom camera class control id values. -const CID_OFFSET: u32 = bindings::V4L2_CID_CAMERA_CLASS_BASE + 0x100; -const CID_LENS_FACING: u32 = CID_OFFSET + 1; - const WIDTH: u32 = 640; const HEIGHT: u32 = 480; const FRAME_RATE: u32 = 30; @@ -1139,38 +858,8 @@ where flags: QueryCtrlFlags, ) -> IoctlResult { let requested_id: u32 = unsafe { std::mem::transmute(id) }; - - if flags.contains(QueryCtrlFlags::NEXT) { - if requested_id < bindings::V4L2_CID_USER_CLASS { - return Ok(self.user_class_query_ext_ctrl()); - } else if requested_id < bindings::V4L2_CID_GAIN { - return Ok(self.gain_query_ext_ctrl()); - } else if requested_id < bindings::V4L2_CID_CAMERA_CLASS { - return Ok(self.camera_class_query_ext_ctrl()); - } else if requested_id < CID_LENS_FACING { - return Ok(self.lens_facing_query_ext_ctrl()); - } else if requested_id < bindings::V4L2_CID_IMAGE_PROC_CLASS { - return Ok(self.image_proc_class_query_ext_ctrl()); - } else if requested_id < bindings::V4L2_CID_TEST_PATTERN { - return Ok(self.test_pattern_query_ext_ctrl()); - } - } else { - match requested_id { - bindings::V4L2_CID_USER_CLASS => return Ok(self.user_class_query_ext_ctrl()), - bindings::V4L2_CID_GAIN => return Ok(self.gain_query_ext_ctrl()), - bindings::V4L2_CID_CAMERA_CLASS => return Ok(self.camera_class_query_ext_ctrl()), - CID_LENS_FACING => return Ok(self.lens_facing_query_ext_ctrl()), - bindings::V4L2_CID_IMAGE_PROC_CLASS => { - return Ok(self.image_proc_class_query_ext_ctrl()); - } - bindings::V4L2_CID_TEST_PATTERN => { - return Ok(self.test_pattern_query_ext_ctrl()); - } - _ => {} - } - } - - Err(libc::EINVAL) + self.controls + .query_ext_ctrl(requested_id, flags.contains(QueryCtrlFlags::NEXT)) } fn querymenu( @@ -1179,36 +868,11 @@ where id: u32, index: u32, ) -> IoctlResult { - if id != bindings::V4L2_CID_TEST_PATTERN { - return Err(libc::EINVAL); - } - // Menu indices are the `TestPattern` discriminants, so the enum decides the range. - let pattern = i32::try_from(index) - .ok() - .and_then(|index| TestPattern::try_from(index).ok()) - .ok_or(libc::EINVAL)?; - - Ok(bindings::v4l2_querymenu { - id, - index, - __bindgen_anon_1: bindings::v4l2_querymenu__bindgen_ty_1 { - name: ctrl_name(pattern.name()), - }, - ..Default::default() - }) + self.controls.querymenu(id, index) } fn g_ctrl(&mut self, _session: &Self::Session, id: u32) -> IoctlResult { - let value = match id { - CID_LENS_FACING => self.controls.lens_facing as i32, - bindings::V4L2_CID_GAIN => self.controls.gain.value(), - bindings::V4L2_CID_TEST_PATTERN => self.controls.test_pattern as i32, - bindings::V4L2_CID_USER_CLASS - | bindings::V4L2_CID_CAMERA_CLASS - | bindings::V4L2_CID_IMAGE_PROC_CLASS => return Err(libc::EACCES), - _ => return Err(libc::EINVAL), - }; - Ok(bindings::v4l2_control { id, value }) + self.controls.g_ctrl(id) } fn s_ctrl( @@ -1217,38 +881,12 @@ where id: u32, value: i32, ) -> IoctlResult { - match id { - CID_LENS_FACING - | bindings::V4L2_CID_USER_CLASS - | bindings::V4L2_CID_CAMERA_CLASS - | bindings::V4L2_CID_IMAGE_PROC_CLASS => Err(libc::EACCES), - bindings::V4L2_CID_GAIN => { - let gain = Gain::new(value)?; - if self.controls.gain != gain { - self.controls.gain = gain; - let event = Self::ctrl_event(&self.controls, bindings::V4L2_CID_GAIN)?; - self.evt_queue - .lock() - .unwrap() - .send_event(V4l2Event::Event(SessionEvent::new(session.id, event))); - let _ = session.channel.set_controls(self.controls.clone()); - } - Ok(bindings::v4l2_control { - id, - value: gain.value(), - }) - } - bindings::V4L2_CID_TEST_PATTERN => { - let pattern = TestPattern::try_from(value)?; - self.set_test_pattern(session.id, pattern)?; - let _ = session.channel.set_controls(self.controls.clone()); - Ok(bindings::v4l2_control { - id, - value: pattern as i32, - }) - } - _ => Err(libc::EINVAL), + let outcome = self.controls.s_ctrl(id, value)?; + if let Some(event) = outcome.event { + self.send_event(session.id, event); } + self.sync_render_params(session); + Ok(outcome.control) } fn g_ext_ctrls( @@ -1259,66 +897,7 @@ where ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { - // 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 - && class != bindings::V4L2_CTRL_CLASS_IMAGE_PROC - { - 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 ctrl in ctrl_array.iter() { - 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 ctrl in ctrl_array.iter_mut() { - match ctrl.id { - bindings::V4L2_CID_USER_CLASS - | bindings::V4L2_CID_CAMERA_CLASS - | bindings::V4L2_CID_IMAGE_PROC_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.controls.lens_facing as i32; - } - bindings::V4L2_CID_TEST_PATTERN => { - ctrl.__bindgen_anon_1.value = match which { - CtrlWhich::Default => TestPattern::DEFAULT as i32, - _ => self.controls.test_pattern as i32, - }; - } - _ => { - ctrls.error_idx = ctrls.count; - return Err(libc::EINVAL); - } - } - } - ctrls.error_idx = ctrls.count; - Ok(()) + self.controls.g_ext_ctrls(which, ctrls, ctrl_array) } fn try_ext_ctrls( @@ -1329,66 +908,7 @@ where 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 - && class != bindings::V4L2_CTRL_CLASS_IMAGE_PROC - { - 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() { - match ctrl.id { - bindings::V4L2_CID_USER_CLASS - | bindings::V4L2_CID_CAMERA_CLASS - | bindings::V4L2_CID_IMAGE_PROC_CLASS - | CID_LENS_FACING => { - 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); - } - } - bindings::V4L2_CID_TEST_PATTERN => { - let value = unsafe { ctrl.__bindgen_anon_1.value }; - if let Err(err) = TestPattern::try_from(value) { - ctrls.error_idx = idx as u32; - return Err(err); - } - } - _ => { - ctrls.error_idx = idx as u32; - return Err(libc::EINVAL); - } - } - } - ctrls.error_idx = ctrls.count; - Ok(()) + self.controls.try_ext_ctrls(which, ctrls, ctrl_array) } fn s_ext_ctrls( @@ -1397,43 +917,13 @@ where which: CtrlWhich, ctrls: &mut bindings::v4l2_ext_controls, ctrl_array: &mut Vec, - user_regions: Vec>, + _user_regions: Vec>, ) -> IoctlResult<()> { - // Validate control class, selection, and values via try_ext_ctrls. - if let Err(err) = self.try_ext_ctrls(session, which, ctrls, ctrl_array, user_regions) { - ctrls.error_idx = ctrls.count; - return Err(err); + let events = self.controls.s_ext_ctrls(which, ctrls, ctrl_array)?; + for event in events { + self.send_event(session.id, event); } - - // Apply control values. Class controls are read-only headers and must fail on write/try. - for ctrl in ctrl_array.iter_mut() { - match ctrl.id { - bindings::V4L2_CID_GAIN => { - let value = unsafe { ctrl.__bindgen_anon_1.value }; - if let Ok(gain) = Gain::new(value) { - if self.controls.gain != gain { - self.controls.gain = gain; - if let Ok(event) = - Self::ctrl_event(&self.controls, bindings::V4L2_CID_GAIN) - { - self.evt_queue.lock().unwrap().send_event(V4l2Event::Event( - SessionEvent::new(session.id, event), - )); - } - } - } - } - bindings::V4L2_CID_TEST_PATTERN => { - let value = unsafe { ctrl.__bindgen_anon_1.value }; - if let Ok(pattern) = TestPattern::try_from(value) { - self.set_test_pattern(session.id, pattern)?; - } - } - _ => {} - } - } - let _ = session.channel.set_controls(self.controls.clone()); - ctrls.error_idx = ctrls.count; + self.sync_render_params(session); Ok(()) } @@ -1447,23 +937,12 @@ where return Err(libc::EINVAL); } match event { - V4l2EventType::Ctrl(id) => match id { - CID_LENS_FACING | bindings::V4L2_CID_GAIN | bindings::V4L2_CID_TEST_PATTERN => { - let ctrl_event = Self::ctrl_event(&self.controls, id)?; - self.evt_queue - .lock() - .unwrap() - .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); - Ok(()) + V4l2EventType::Ctrl(id) => { + if let Some(initial) = self.controls.subscribe_ctrl_event(id)? { + self.send_event(session.id, initial); } - bindings::V4L2_CID_USER_CLASS - | bindings::V4L2_CID_CAMERA_CLASS - | bindings::V4L2_CID_IMAGE_PROC_CLASS => { - // Subscription succeeds, but we do not send any initial event. - Ok(()) - } - _ => Err(libc::EINVAL), - }, + Ok(()) + } _ => Err(libc::EINVAL), } } @@ -1473,58 +952,10 @@ where _session: &mut Self::Session, event: bindings::v4l2_event_subscription, ) -> IoctlResult<()> { - return if event.type_ == bindings::V4L2_EVENT_CTRL - && (event.id == CID_LENS_FACING - || event.id == bindings::V4L2_CID_GAIN - || event.id == bindings::V4L2_CID_TEST_PATTERN - || event.id == bindings::V4L2_CID_USER_CLASS - || event.id == bindings::V4L2_CID_CAMERA_CLASS - || event.id == bindings::V4L2_CID_IMAGE_PROC_CLASS) - { + if event.type_ == bindings::V4L2_EVENT_CTRL && self.controls.knows(event.id) { Ok(()) } else { Err(libc::EINVAL) - }; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Every index between `MIN` and `MAX` is advertised as a menu entry, so each one has - /// to map back to a pattern with a name and a generator. - #[test] - fn every_advertised_menu_index_maps_to_a_pattern() { - for value in TestPattern::MIN..=TestPattern::MAX { - let pattern = TestPattern::try_from(value).expect("advertised index must be valid"); - assert_eq!(pattern as i32, value); - assert!(!pattern.name().is_empty()); } } - - #[test] - fn out_of_range_menu_index_is_rejected() { - assert_eq!( - TestPattern::try_from(TestPattern::MIN - 1), - Err(libc::ERANGE) - ); - assert_eq!( - TestPattern::try_from(TestPattern::MAX + 1), - Err(libc::ERANGE) - ); - } - - #[test] - fn ctrl_name_is_nul_padded() { - let name = ctrl_name("Test Pattern"); - assert_eq!(&name[..12], b"Test Pattern"); - assert!(name[12..].iter().all(|byte| *byte == 0)); - } - - #[test] - fn ctrl_name_truncates_and_stays_nul_terminated() { - let name = ctrl_name("This control name is definitely far too long to fit"); - assert_eq!(name[31], 0); - } } diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/emulated_scene.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/emulated_scene.rs index f7def7c3ec8..f5be41c67c4 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/emulated_scene.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/emulated_scene.rs @@ -14,7 +14,7 @@ //! Minimal port of the Android emulated camera scene `https://cs.android.com/android/platform/superproject/main/+/main:hardware/google/camera/devices/EmulatedCamera/hwl/EmulatedScene.h` -use crate::device::{CameraControls, Gain}; +use crate::render_params::{Gain, RenderParams}; use crate::pattern::FramePattern; use std::io::Write; @@ -517,14 +517,14 @@ impl FramePattern for EmulatedScene { fn write( &self, iteration: u64, - controls: &CameraControls, + params: &RenderParams, width: u32, height: u32, sink_y: &mut dyn Write, sink_u: &mut dyn Write, sink_v: &mut dyn Write, ) -> Result<(), i32> { - let (palette, time_since_idx_ns) = self.palette(iteration, controls.gain); + let (palette, time_since_idx_ns) = self.palette(iteration, params.gain); let mapping = Mapping::new(width, height, time_since_idx_ns); // Precomputed frame column -> scene column (0..20) mapping, so scene_col() does not have diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/lib.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/lib.rs index 519353cc58c..f941a7abcf3 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/lib.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/lib.rs @@ -31,17 +31,23 @@ use virtio_media::v4l2r::ioctl::Capabilities; use vm_memory::{GuestMemoryAtomic, GuestMemoryLoadGuard, GuestMemoryMmap}; pub mod capture_channel; +pub mod control; +pub mod controls; pub mod device; pub mod emulated_scene; pub mod format; pub mod memory; pub mod pattern; +pub mod render_params; pub use capture_channel::CaptureSink; -pub use device::{CameraControls, EmulatedCamera, FrameSource, Gain, LensFacing, TestPattern}; +pub use control::{Control, ControlDescriptor, ControlSet}; +pub use controls::LensFacing; +pub use device::{EmulatedCamera, FrameSource}; pub use emulated_scene::EmulatedScene; pub use format::Yuv420m; pub use memory::{BufferMemoryHandler, QbufRequest, QueuedBuffer}; +pub use render_params::{Gain, RenderParams, TestPattern}; /// Errors returned by [`serve`]. #[derive(Debug, Error)] @@ -74,15 +80,16 @@ fn device_config(card_name: &str) -> VirtioMediaDeviceConfig { /// `card_name` is reported to the guest through `VIDIOC_QUERYCAP`. When the main VM shuts down /// the daemon exits gracefully; this function then re-creates it so that the backend survives /// VM restarts without having to be started again manually. It therefore only returns on error. -pub fn serve( +pub fn serve( socket_path: &Path, card_name: &str, - lens_facing: LensFacing, frame_source: FrameSource, + create_control_set: C, create_buffer_memory_handler: F, ) -> Result<()> where M: BufferMemoryHandler, + C: Fn() -> ControlSet + Clone + Send + Sync + 'static, F: Fn(HostMemoryMapper, GuestMemoryLoadGuard) -> M + Clone + Send @@ -90,6 +97,7 @@ where + 'static, { loop { + let create_control_set = create_control_set.clone(); let create_buffer_memory_handler = create_buffer_memory_handler.clone(); let backend = Arc::new(RwLock::new(VhuMediaBackend::new( device_config(card_name), @@ -97,7 +105,7 @@ where EmulatedCamera::new( event_queue, create_buffer_memory_handler(host_mapper, guest_mem), - lens_facing, + create_control_set(), frame_source, ) }, diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/julia_set.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/julia_set.rs index 32507875997..11e78cf3f61 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/julia_set.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/julia_set.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::FramePattern; -use crate::device::CameraControls; +use crate::render_params::RenderParams; use std::io::Write; /// Escape-time iterations spent on each pixel. @@ -45,7 +45,7 @@ impl FramePattern for JuliaSet { &self, iteration: u64, // Gain is intentionally ignored for Julia Set to preserve the intended escape-time gradient contrast. - _controls: &CameraControls, + _params: &RenderParams, width: u32, height: u32, sink_y: &mut dyn Write, diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/mod.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/mod.rs index 12e77490fa9..abb79555397 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/mod.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/mod.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::device::CameraControls; +use crate::render_params::RenderParams; use std::io::Write; pub mod julia_set; @@ -33,7 +33,7 @@ pub trait FramePattern { fn write( &self, iteration: u64, - controls: &CameraControls, + params: &RenderParams, width: u32, height: u32, sink_y: &mut dyn Write, @@ -45,12 +45,12 @@ pub trait FramePattern { #[cfg(test)] mod tests { use super::*; - use crate::device::{Gain, LensFacing}; + use crate::render_params::Gain; /// Renders a few frames across supported resolutions and checks that each plane /// is exactly the expected size. A short write would leave stale bytes in the guest buffer. fn assert_plane_sizes(pattern: &dyn FramePattern) { - let controls = CameraControls::new(LensFacing::Front); + let controls = RenderParams::default(); let resolutions: [(u32, u32); 4] = [(320, 240), (640, 480), (1280, 720), (1920, 1080)]; for (width, height) in resolutions { for iteration in [0u64, 1, 255, 4096] { @@ -82,7 +82,7 @@ mod tests { #[test] fn pulse_applies_gain_scaling() { - let mut controls = CameraControls::new(LensFacing::Front); + let mut controls = RenderParams::default(); controls.gain = Gain::new(200).unwrap(); // 2.0x gain let mut plane_y = Vec::new(); @@ -135,7 +135,7 @@ mod tests { #[test] fn julia_set_keeps_animating_after_a_long_run() { let far_out = 50_000_000u64; - let controls = CameraControls::new(LensFacing::Front); + let controls = RenderParams::default(); let luma_at = |iteration: u64| { let mut plane_y = Vec::new(); let mut plane_u = Vec::new(); diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/pulse.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/pulse.rs index 05e51cdd7c2..0c01a27c59b 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/pulse.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/pulse.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::FramePattern; -use crate::device::{CameraControls, Gain}; +use crate::render_params::{Gain, RenderParams}; use std::io::Write; /// Fills the frame with two horizontal color bands that cycle as frames are produced. @@ -23,7 +23,7 @@ impl FramePattern for Pulse { fn write( &self, iteration: u64, - controls: &CameraControls, + params: &RenderParams, width: u32, height: u32, sink_y: &mut dyn Write, @@ -38,7 +38,7 @@ impl FramePattern for Pulse { // 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 gain_scale = controls.gain.value() as f32 / Gain::MIN as f32; + let gain_scale = params.gain.value() as f32 / Gain::MIN as f32; let y_top = ((base_y_top as f32) * gain_scale).min(255.0) as u8; let y_bottom = ((base_y_bottom as f32) * gain_scale).min(255.0) as u8; let u = ((sequence + 64) % 256) as u8; diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/smpte.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/smpte.rs index e338070bdd5..df3cc424783 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/smpte.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/pattern/smpte.rs @@ -13,7 +13,7 @@ // limitations under the License. use super::FramePattern; -use crate::device::CameraControls; +use crate::render_params::RenderParams; use std::io::Write; const SMPTE_COLOR_WHITE: (u8, u8, u8) = (180, 128, 128); @@ -49,7 +49,7 @@ impl FramePattern for SmpteBars { &self, iteration: u64, // Gain is intentionally ignored for SMPTE bars to preserve exact YUV color calibration values. - _controls: &CameraControls, + _params: &RenderParams, width: u32, height: u32, sink_y: &mut dyn Write, diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/render_params.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/render_params.rs new file mode 100644 index 00000000000..c434c5f909e --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera/src/render_params.rs @@ -0,0 +1,120 @@ +// Copyright 2026, The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Parameters consumed by the frame generators. +//! +//! [`RenderParams`] is the only thing the rendering pipeline knows about V4L2 controls. It is a +//! plain value with a default for every field, so a backend that does not expose a given control +//! simply renders with the default. Controls project their value into it through +//! [`crate::control::Control::apply`]. + +use crate::pattern::FramePattern; +use crate::pattern::julia_set::JuliaSet; +use crate::pattern::pulse::Pulse; +use crate::pattern::smpte::SmpteBars; + +/// 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) + } +} + +/// Test pattern selectable through `V4L2_CID_TEST_PATTERN`. +/// +/// The discriminants double as the menu indices reported by `VIDIOC_QUERYMENU`, so they +/// must stay contiguous and start at [`TestPattern::MIN`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TestPattern { + Pulse = 0, + SmpteBars = 1, + JuliaSet = 2, +} + +impl TestPattern { + /// Lowest menu index, reported as `minimum` for `V4L2_CID_TEST_PATTERN`. + pub const MIN: i32 = TestPattern::Pulse as i32; + /// Highest menu index, reported as `maximum` for `V4L2_CID_TEST_PATTERN`. + pub const MAX: i32 = TestPattern::JuliaSet as i32; + /// Pattern selected until the guest asks for something else. + pub const DEFAULT: TestPattern = TestPattern::Pulse; + + /// Human readable name reported by `VIDIOC_QUERYMENU`. + pub fn name(self) -> &'static str { + match self { + TestPattern::Pulse => "Pulse", + TestPattern::SmpteBars => "SMPTE + Bouncing Box", + TestPattern::JuliaSet => "Animated Julia Set", + } + } + + /// Frame generator backing this pattern. + pub(crate) fn generator(self) -> &'static dyn FramePattern { + match self { + TestPattern::Pulse => &Pulse, + TestPattern::SmpteBars => &SmpteBars, + TestPattern::JuliaSet => &JuliaSet, + } + } +} + +impl Default for TestPattern { + fn default() -> Self { + Self::DEFAULT + } +} + +impl TryFrom for TestPattern { + /// Raw `errno` reported to the guest for an out-of-range menu index. + type Error = i32; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(TestPattern::Pulse), + 1 => Ok(TestPattern::SmpteBars), + 2 => Ok(TestPattern::JuliaSet), + _ => Err(libc::ERANGE), + } + } +} + +/// Everything the frame generators need to know about the current control state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct RenderParams { + /// Luma scaling, `Gain::MIN` is 1.0x. + pub gain: Gain, + /// Pattern rendered by [`crate::device::FrameSource::TestPatterns`]. + pub test_pattern: TestPattern, +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs index 4e5e61e1378..00ca3e01079 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs @@ -19,7 +19,8 @@ mod userptr; use std::path::PathBuf; use clap::Parser; -use emulated_camera::{FrameSource, LensFacing}; +use emulated_camera::controls::{GainControl, LensFacingControl, TestPatternControl}; +use emulated_camera::{Control, ControlSet, FrameSource, LensFacing}; use thiserror::Error; use crate::userptr::UserPtrHandler; @@ -72,6 +73,14 @@ impl TryFrom for Config { } } +fn controls(lens_facing: LensFacing) -> Vec> { + vec![ + Box::new(LensFacingControl::new(lens_facing)), + Box::new(GainControl::new()), + Box::new(TestPatternControl::new()), + ] +} + fn init_logging(verbosity: log::LevelFilter) { env_logger::builder() .format_timestamp_secs() @@ -85,11 +94,12 @@ fn main() -> Result<()> { init_logging(args.verbosity); let config = Config::try_from(args)?; + let lens_facing = config.lens_facing; emulated_camera::serve( &config.socket_path, CARD_NAME, - config.lens_facing, FrameSource::EmulatedScene, + move || ControlSet::new(controls(lens_facing)), |_mapper, guest_mem| UserPtrHandler::new(guest_mem), )?; Ok(()) diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs index 449b045fc2f..6a310edd7dc 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs @@ -21,7 +21,8 @@ mod mmap; use std::path::PathBuf; use clap::Parser; -use emulated_camera::{FrameSource, LensFacing}; +use emulated_camera::controls::{GainControl, LensFacingControl, TestPatternControl}; +use emulated_camera::{Control, ControlSet, FrameSource, LensFacing}; use crate::mmap::MmapHandler; @@ -39,6 +40,14 @@ struct CmdLineArgs { verbosity: log::LevelFilter, } +fn controls() -> Vec> { + vec![ + Box::new(LensFacingControl::new(LensFacing::External)), + Box::new(GainControl::new()), + Box::new(TestPatternControl::new()), + ] +} + fn init_logging(verbosity: log::LevelFilter) { env_logger::builder() .format_timestamp_secs() @@ -54,8 +63,8 @@ fn main() -> emulated_camera::Result<()> { emulated_camera::serve( &args.socket_path, CARD_NAME, - LensFacing::External, FrameSource::TestPatterns, + || ControlSet::new(controls()), |mapper, _guest_mem| MmapHandler::new(mapper), ) } From 63b1c002e7b789cd726843a573b70633e226aaaf Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Tue, 10 Nov 2026 17:12:08 -0500 Subject: [PATCH 2/3] emulated_camera_mplane: drop V4L2_CID_TEST_PATTERN The backend renders the emulated scene, so the control had no effect. Bug: b/584385597 Assisted-by: Jetski:Claude --- .../vhost_user_media/emulated_camera_mplane/src/main.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs index 00ca3e01079..cf3feb8a55e 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs @@ -19,7 +19,7 @@ mod userptr; use std::path::PathBuf; use clap::Parser; -use emulated_camera::controls::{GainControl, LensFacingControl, TestPatternControl}; +use emulated_camera::controls::{GainControl, LensFacingControl}; use emulated_camera::{Control, ControlSet, FrameSource, LensFacing}; use thiserror::Error; @@ -77,7 +77,6 @@ fn controls(lens_facing: LensFacing) -> Vec> { vec![ Box::new(LensFacingControl::new(lens_facing)), Box::new(GainControl::new()), - Box::new(TestPatternControl::new()), ] } From 0b722dd0753936424b6205fbcdda57427ecb392a Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Tue, 10 Nov 2026 17:13:12 -0500 Subject: [PATCH 3/3] emulated_camera_mplane_external: drop CID_LENS_FACING and V4L2_CID_GAIN These controls are only relevant for emulated_camera_mplane. Bug: b/584385597 Assisted-by: Jetski:Claude --- .../emulated_camera_mplane_external/src/main.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs index 6a310edd7dc..00f4fd362fa 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane_external/src/main.rs @@ -21,8 +21,8 @@ mod mmap; use std::path::PathBuf; use clap::Parser; -use emulated_camera::controls::{GainControl, LensFacingControl, TestPatternControl}; -use emulated_camera::{Control, ControlSet, FrameSource, LensFacing}; +use emulated_camera::controls::TestPatternControl; +use emulated_camera::{Control, ControlSet, FrameSource}; use crate::mmap::MmapHandler; @@ -41,11 +41,7 @@ struct CmdLineArgs { } fn controls() -> Vec> { - vec![ - Box::new(LensFacingControl::new(LensFacing::External)), - Box::new(GainControl::new()), - Box::new(TestPatternControl::new()), - ] + vec![Box::new(TestPatternControl::new())] } fn init_logging(verbosity: log::LevelFilter) {