From d6b351af9dcc8c79756240c84a1550fc8dd4cb87 Mon Sep 17 00:00:00 2001 From: Changyeon Jo Date: Wed, 15 Jul 2026 05:05:33 +0000 Subject: [PATCH 1/6] Run cargo fmt on baseline emulated_camera_mplane Format pre-existing files inside `emulated_camera_mplane` to comply with the standard Rust formatting style. This eliminates formatting noise in subsequent functional commits. Bug: b/490699808 Bug: b/502639876 --- .../vhost_user_media/emulated_camera_mplane/src/device.rs | 7 +++++-- .../vhost_user_media/emulated_camera_mplane/src/main.rs | 4 +++- 2 files changed, 8 insertions(+), 3 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..e7bd8f15dce 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 @@ -21,6 +21,7 @@ use std::io::Write; use std::os::fd::AsFd; use std::os::fd::BorrowedFd; +use std::str::FromStr; use v4l2r::PixelFormat; use v4l2r::QueueType; use v4l2r::bindings; @@ -55,7 +56,6 @@ use virtio_media::protocol::SgEntry; use virtio_media::protocol::V4l2Event; use virtio_media::protocol::V4l2Ioctl; use virtio_media::protocol::VIRTIO_MEDIA_MMAP_FLAG_RW; -use std::str::FromStr; /// https://developer.android.com/reference/android/hardware/camera2/CameraMetadata#LENS_FACING_FRONT #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -73,7 +73,10 @@ impl FromStr for LensFacing { "FRONT" => Ok(LensFacing::Front), "BACK" => Ok(LensFacing::Back), "EXTERNAL" => Ok(LensFacing::External), - _ => Err(format!("Invalid lens facing: {}. Expected FRONT, BACK, or EXTERNAL", s)), + _ => Err(format!( + "Invalid lens facing: {}. Expected FRONT, BACK, or EXTERNAL", + s + )), } } } 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 dfd44608c19..aebc5a8709e 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 @@ -62,7 +62,9 @@ impl TryFrom for Config { type Error = Error; fn try_from(args: CmdLineArgs) -> Result { - let lens_facing = args.lens_facing.parse::() + let lens_facing = args + .lens_facing + .parse::() .map_err(Error::InvalidArgument)?; Ok(Config { socket_path: args.socket_path, From 5caa28ce23e00a5b9b15ba93e60ea9c081461782 Mon Sep 17 00:00:00 2001 From: Changyeon Jo Date: Wed, 15 Jul 2026 16:15:15 +0000 Subject: [PATCH 2/6] Introduce FramePattern trait for modular video emulators Declare the trait-based `FramePattern` interface inside `src/pattern/mod.rs`. This trait defines the interface for dynamically writing multi-planar YUV video frames into queued virtio-media buffers. This first commit adds the trait definitions and registers the pattern module in the build system as a standalone declaration. Bug: b/490699808 Bug: b/502639876 --- .../emulated_camera_mplane/BUILD.bazel | 2 ++ .../emulated_camera_mplane/src/main.rs | 1 + .../emulated_camera_mplane/src/pattern/mod.rs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel index 91d42116f63..951536a8582 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel @@ -16,6 +16,8 @@ rust_binary( srcs = [ "src/device.rs", "src/main.rs", + "src/pattern/mod.rs", + "src/pattern/pulse.rs", ], edition = "2024", deps = crate_deps([ 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 aebc5a8709e..3220b0356f3 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 @@ -24,6 +24,7 @@ use virtio_media::protocol::VirtioMediaDeviceConfig; use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; mod device; +mod pattern; use device::LensFacing; #[derive(Debug, Error)] diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs new file mode 100644 index 00000000000..1b723f4948e --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs @@ -0,0 +1,16 @@ +use std::io::Write; + +pub const WIDTH: u32 = 640; +pub const HEIGHT: u32 = 480; +pub const Y_SIZE: usize = (WIDTH * HEIGHT) as usize; +pub const UV_SIZE: usize = (WIDTH * HEIGHT / 4) as usize; + +pub trait FramePattern: Send + Sync { + fn write( + &self, + iteration: u64, + sink_y: WY, + sink_u: WU, + sink_v: WV, + ) -> Result<(), i32>; +} From facd959c63abadbcf504fd9f09f02128565f3496 Mon Sep 17 00:00:00 2001 From: Changyeon Jo Date: Wed, 15 Jul 2026 17:10:45 +0000 Subject: [PATCH 3/6] Implement Pulse pattern and refactor internal frame writer Implement the color-cycling `Pulse` pattern inside `src/pattern/pulse.rs`. Refactor the internal `write_pattern` helper inside `device.rs` to call the `CameraPattern::Pulse` abstraction instead of hardcoding YUV loop math. Bug: b/490699808 Bug: b/502639876 --- .../emulated_camera_mplane/src/device.rs | 31 ++++++------------- .../emulated_camera_mplane/src/pattern/mod.rs | 4 +-- .../src/pattern/pulse.rs | 26 ++++++++++++++++ 3 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/pulse.rs 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 e7bd8f15dce..8755780d5d5 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 @@ -13,7 +13,6 @@ // limitations under the License. use std::collections::VecDeque; -use std::io::BufWriter; use std::io::Result as IoResult; use std::io::Seek; use std::io::SeekFrom; @@ -21,6 +20,9 @@ use std::io::Write; use std::os::fd::AsFd; use std::os::fd::BorrowedFd; +use crate::pattern::FramePattern; +use crate::pattern::pulse::Pulse; + use std::str::FromStr; use v4l2r::PixelFormat; use v4l2r::QueueType; @@ -195,28 +197,15 @@ impl VirtioMediaDeviceSession for EmulatedCameraSession { } impl EmulatedCameraSession { - fn write_pattern( + fn write_pattern( iteration: u64, - mut sink_y: WY, - mut sink_u: WU, - mut sink_v: WV, + sink_y: WY, + sink_u: WU, + sink_v: WV, ) -> IoctlResult<()> { - 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; - let u = ((iteration + 64) % 256) as u8; - let v = ((iteration + 128) % 256) as u8; - for _ in 0..(WIDTH * HEIGHT) { - writer_y.write_all(&[y]).map_err(|_| libc::EIO)?; - } - for _ in 0..(WIDTH * HEIGHT / 4) { - writer_u.write_all(&[u]).map_err(|_| libc::EIO)?; - } - for _ in 0..(WIDTH * HEIGHT / 4) { - writer_v.write_all(&[v]).map_err(|_| libc::EIO)?; - } - Ok(()) + Pulse + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO) } /// Write basic pattern into the queued buffers diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs index 1b723f4948e..3b121608397 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs @@ -1,9 +1,9 @@ use std::io::Write; +pub mod pulse; + pub const WIDTH: u32 = 640; pub const HEIGHT: u32 = 480; -pub const Y_SIZE: usize = (WIDTH * HEIGHT) as usize; -pub const UV_SIZE: usize = (WIDTH * HEIGHT / 4) as usize; pub trait FramePattern: Send + Sync { fn write( diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/pulse.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/pulse.rs new file mode 100644 index 00000000000..951de8b1dea --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/pulse.rs @@ -0,0 +1,26 @@ +use super::{FramePattern, HEIGHT, WIDTH}; +use std::io::Write; + +pub struct Pulse; + +impl FramePattern for Pulse { + fn write( + &self, + iteration: u64, + mut sink_y: WY, + mut sink_u: WU, + mut sink_v: WV, + ) -> Result<(), i32> { + let sequence = iteration; + let y = (sequence % 256) as u8; + let u = ((sequence + 64) % 256) as u8; + let v = ((sequence + 128) % 256) as u8; + let y_plane = vec![y; (WIDTH * HEIGHT) as usize]; + let u_plane = vec![u; (WIDTH * HEIGHT / 4) as usize]; + let v_plane = vec![v; (WIDTH * HEIGHT / 4) as usize]; + sink_y.write_all(&y_plane).map_err(|_| libc::EIO)?; + sink_u.write_all(&u_plane).map_err(|_| libc::EIO)?; + sink_v.write_all(&v_plane).map_err(|_| libc::EIO)?; + Ok(()) + } +} From f0044ce17a15b1fb7beff245f9227c822853ce51 Mon Sep 17 00:00:00 2001 From: Changyeon Jo Date: Wed, 15 Jul 2026 17:31:23 +0000 Subject: [PATCH 4/6] Add dynamic V4L2 test pattern controls and query menus Introduce the V4L2 controls to allow runtime switching of emulated camera test patterns. Wires up: - V4L2_CID_IMAGE_PROC_CLASS - V4L2_CID_TEST_PATTERN Exposes these controls via standard query and control ioctl handlers, including querymenu and control change event subscriptions. Only Pattern 0 (Pulse) is selectable in this commit. We can use the standard `v4l2-ctl` command-line tool inside the guest OS to list, query, and change the test patterns at runtime: # List all available camera controls (shows "Test Pattern" menu) v4l2-ctl -d /dev/video1 --list-ctrls # Get the currently selected pattern index v4l2-ctl -d /dev/video1 -C test_pattern # Switch the selected pattern to Pulse (index 0) v4l2-ctl -d /dev/video1 -c test_pattern=0 Bug: b/490699808 Bug: b/502639876 --- .../emulated_camera_mplane/src/device.rs | 243 +++++++++++++++--- 1 file changed, 213 insertions(+), 30 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 8755780d5d5..af2b6098206 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 @@ -16,10 +16,11 @@ use std::collections::VecDeque; use std::io::Result as IoResult; use std::io::Seek; use std::io::SeekFrom; -use std::io::Write; use std::os::fd::AsFd; use std::os::fd::BorrowedFd; +use std::io::Write; + use crate::pattern::FramePattern; use crate::pattern::pulse::Pulse; @@ -199,19 +200,23 @@ impl VirtioMediaDeviceSession for EmulatedCameraSession { impl EmulatedCameraSession { fn write_pattern( iteration: u64, + pattern: u32, sink_y: WY, sink_u: WU, sink_v: WV, ) -> IoctlResult<()> { - Pulse - .write(iteration, sink_y, sink_u, sink_v) - .map_err(|_| libc::EIO) + match pattern { + _ => Pulse + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO), + } } /// Write basic pattern into the queued buffers fn process_queued_buffers( &mut self, evt_queue: &mut Q, + pattern: u32, ) -> IoctlResult<()> { while let Some(buf_id) = self.queued_buffers.pop_front() { let iteration = self.iteration; @@ -227,6 +232,7 @@ impl EmulatedCameraSession { Self::write_pattern( iteration, + pattern, buffer.planes[0].fd.as_file(), buffer.planes[1].fd.as_file(), buffer.planes[2].fd.as_file(), @@ -265,6 +271,8 @@ pub struct EmulatedCamera, /// Lens facing configuration. lens_facing: LensFacing, + /// Currently selected test pattern. + current_pattern: u32, } impl EmulatedCamera @@ -278,6 +286,7 @@ where mmap_manager: MmapMappingManager::from(mapper), active_session: None, lens_facing, + current_pattern: 0, } } @@ -301,6 +310,61 @@ where } } +const V4L2_CID_IMAGE_PROC_CLASS: u32 = 0x009f0000; +const V4L2_CID_TEST_PATTERN: u32 = 0x009f0903; + +struct ControlDef { + id: u32, + type_: u32, + name: &'static str, + minimum: i32, + maximum: i32, + step: u32, + default_value: i32, + flags: u32, +} + +const CONTROLS: [ControlDef; 3] = [ + ControlDef { + id: CID_LENS_FACING, + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER, + name: "LENS_FACING", + minimum: LensFacing::Front as i32, + maximum: LensFacing::External as i32, + step: 1, + default_value: LensFacing::Front as i32, + flags: bindings::V4L2_CTRL_FLAG_READ_ONLY, + }, + ControlDef { + id: V4L2_CID_IMAGE_PROC_CLASS, + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_CTRL_CLASS, + name: "Image Processing Controls", + minimum: 0, + maximum: 0, + step: 0, + default_value: 0, + flags: bindings::V4L2_CTRL_FLAG_READ_ONLY | bindings::V4L2_CTRL_FLAG_WRITE_ONLY, + }, + ControlDef { + id: V4L2_CID_TEST_PATTERN, + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU, + name: "Test Pattern", + minimum: 0, + maximum: 0, + step: 1, + default_value: 0, + flags: 0, + }, +]; + +fn lookup_control(requested_id: u32, flags: QueryCtrlFlags) -> Option<&'static ControlDef> { + if flags.contains(QueryCtrlFlags::NEXT) { + CONTROLS.iter().find(|ctrl| ctrl.id > requested_id) + } else { + CONTROLS.iter().find(|ctrl| ctrl.id == requested_id) + } +} + impl VirtioMediaDevice for EmulatedCamera where Q: VirtioMediaEventQueue, @@ -716,7 +780,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.current_pattern)?; } Ok(buffer) @@ -728,7 +792,7 @@ where } session.streaming = true; - session.process_queued_buffers(&mut self.evt_queue)?; + session.process_queued_buffers(&mut self.evt_queue, self.current_pattern)?; Ok(()) } @@ -830,16 +894,95 @@ where id: CtrlId, 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 { + let requested_id: u32 = unsafe { std::mem::transmute(id) }; + + if let Some(ctrl) = lookup_control(requested_id, flags) { + if ctrl.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()); + + let mut name = [0 as ::std::os::raw::c_char; 32]; + let bytes = ctrl.name.as_bytes(); + let len = std::cmp::min(bytes.len(), 31); + for i in 0..len { + name[i] = bytes[i] as ::std::os::raw::c_char; + } + + Ok(bindings::v4l2_query_ext_ctrl { + id: ctrl.id, + type_: ctrl.type_, + name, + minimum: ctrl.minimum as i64, + maximum: ctrl.maximum as i64, + step: ctrl.step as u64, + default_value: ctrl.default_value as i64, + flags: ctrl.flags, + elem_size: 4, + elems: 1, + nr_of_dims: 0, + dims: [0; 4], + ..Default::default() + }) + } else { + Err(libc::EINVAL) + } + } + + fn querymenu( + &mut self, + _session: &Self::Session, + id: u32, + index: u32, + ) -> IoctlResult { + if id == V4L2_CID_TEST_PATTERN { + if index > 0 { + return Err(libc::EINVAL); + } + let mut name = [0u8; 32]; + let name_str = match index { + _ => "Pulse", + }; + let bytes = name_str.as_bytes(); + name[0..bytes.len()].copy_from_slice(bytes); + + Ok(bindings::v4l2_querymenu { + id, + index, + __bindgen_anon_1: bindings::v4l2_querymenu__bindgen_ty_1 { name }, + ..Default::default() + }) + } else { + Err(libc::EINVAL) + } + } + + fn g_ctrl(&mut self, _session: &Self::Session, id: u32) -> IoctlResult { + if id == V4L2_CID_TEST_PATTERN { + Ok(bindings::v4l2_control { + id, + value: self.current_pattern as i32, + }) + } else { + Err(libc::EINVAL) + } + } + + fn s_ctrl( + &mut self, + _session: &mut Self::Session, + id: u32, + value: i32, + ) -> IoctlResult { + if id == V4L2_CID_TEST_PATTERN { + if (0..=0).contains(&value) { + self.current_pattern = value as u32; + Ok(bindings::v4l2_control { id, value }) + } else { + Err(libc::ERANGE) + } + } else { + Err(libc::EINVAL) } - return Err(libc::EINVAL); } fn g_ext_ctrls( @@ -850,13 +993,19 @@ where ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { - for ctrl in ctrl_array { + for (i, ctrl) in ctrl_array.iter_mut().enumerate() { match ctrl.id { CID_LENS_FACING => { ctrl.__bindgen_anon_1.value = self.lens_facing as i32; } + V4L2_CID_IMAGE_PROC_CLASS => { + ctrl.__bindgen_anon_1.value = 0; + } + V4L2_CID_TEST_PATTERN => { + ctrl.__bindgen_anon_1.value = self.current_pattern as i32; + } _ => { - ctrls.error_idx = ctrls.count; + ctrls.error_idx = i as u32; return Err(libc::EINVAL); } } @@ -872,13 +1021,19 @@ where ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { - 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); + for (i, ctrl) in ctrl_array.iter_mut().enumerate() { + ctrls.error_idx = i as u32; + match ctrl.id { + CID_LENS_FACING => return Err(libc::EACCES), + V4L2_CID_IMAGE_PROC_CLASS => {} + V4L2_CID_TEST_PATTERN => { + let val = unsafe { ctrl.__bindgen_anon_1.value }; + if !(0..=0).contains(&val) { + return Err(libc::ERANGE); + } + } + _ => return Err(libc::EINVAL), + } } Ok(()) } @@ -891,13 +1046,29 @@ where 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); + for (i, ctrl) in ctrl_array.iter_mut().enumerate() { + ctrls.error_idx = i as u32; + match ctrl.id { + CID_LENS_FACING => return Err(libc::EACCES), + V4L2_CID_IMAGE_PROC_CLASS => return Err(libc::EACCES), + V4L2_CID_TEST_PATTERN => { + let val = unsafe { ctrl.__bindgen_anon_1.value }; + if !(0..=0).contains(&val) { + return Err(libc::ERANGE); + } + self.current_pattern = val as u32; + + // Emit event to notify guest of change + let ctrl_event = bindings::v4l2_event { + type_: bindings::V4L2_EVENT_CTRL, + id: V4L2_CID_TEST_PATTERN, + ..Default::default() + }; + self.evt_queue + .send_event(V4l2Event::Event(SessionEvent::new(_session.id, ctrl_event))); + } + _ => return Err(libc::EINVAL), + } } Ok(()) } @@ -923,6 +1094,16 @@ where .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); Ok(()) } + V4L2_CID_TEST_PATTERN => { + let ctrl_event = bindings::v4l2_event { + type_: bindings::V4L2_EVENT_CTRL, + id: V4L2_CID_TEST_PATTERN, + ..Default::default() + }; + self.evt_queue + .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); + Ok(()) + } _ => Err(libc::EINVAL), }, _ => Err(libc::EINVAL), @@ -934,7 +1115,9 @@ 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 == V4L2_CID_TEST_PATTERN) + { Ok(()) } else { Err(libc::EINVAL) From 0316285c1359d91e3ea2269eee35b7adae867608 Mon Sep 17 00:00:00 2001 From: Changyeon Jo Date: Wed, 15 Jul 2026 18:08:17 +0000 Subject: [PATCH 5/6] Add SMPTE Bars test pattern Implement the color-bar SMPTE Bars test pattern with an animated inverse-color bouncing box overlay inside `src/pattern/smpte.rs`. Register it in the `pattern` module and expand `device.rs` V4L2 control ranges and query menus to support selecting index 1. Bug: b/490699808 Bug: b/502639876 --- .../emulated_camera_mplane/BUILD.bazel | 1 + .../emulated_camera_mplane/src/device.rs | 19 +-- .../emulated_camera_mplane/src/pattern/mod.rs | 1 + .../src/pattern/smpte.rs | 115 ++++++++++++++++++ 4 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/smpte.rs diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel index 951536a8582..e6e856c1d47 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel @@ -18,6 +18,7 @@ rust_binary( "src/main.rs", "src/pattern/mod.rs", "src/pattern/pulse.rs", + "src/pattern/smpte.rs", ], edition = "2024", deps = crate_deps([ 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 af2b6098206..d51baddf4eb 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 @@ -23,6 +23,7 @@ use std::io::Write; use crate::pattern::FramePattern; use crate::pattern::pulse::Pulse; +use crate::pattern::smpte::SmpteBars; use std::str::FromStr; use v4l2r::PixelFormat; @@ -206,7 +207,10 @@ impl EmulatedCameraSession { sink_v: WV, ) -> IoctlResult<()> { match pattern { - _ => Pulse + 0 => Pulse + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO), + _ => SmpteBars .write(iteration, sink_y, sink_u, sink_v) .map_err(|_| libc::EIO), } @@ -350,7 +354,7 @@ const CONTROLS: [ControlDef; 3] = [ type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU, name: "Test Pattern", minimum: 0, - maximum: 0, + maximum: 1, step: 1, default_value: 0, flags: 0, @@ -935,12 +939,13 @@ where index: u32, ) -> IoctlResult { if id == V4L2_CID_TEST_PATTERN { - if index > 0 { + if index > 1 { return Err(libc::EINVAL); } let mut name = [0u8; 32]; let name_str = match index { - _ => "Pulse", + 0 => "Pulse", + _ => "SMPTE + Bouncing Box", }; let bytes = name_str.as_bytes(); name[0..bytes.len()].copy_from_slice(bytes); @@ -974,7 +979,7 @@ where value: i32, ) -> IoctlResult { if id == V4L2_CID_TEST_PATTERN { - if (0..=0).contains(&value) { + if (0..=1).contains(&value) { self.current_pattern = value as u32; Ok(bindings::v4l2_control { id, value }) } else { @@ -1028,7 +1033,7 @@ where V4L2_CID_IMAGE_PROC_CLASS => {} V4L2_CID_TEST_PATTERN => { let val = unsafe { ctrl.__bindgen_anon_1.value }; - if !(0..=0).contains(&val) { + if !(0..=1).contains(&val) { return Err(libc::ERANGE); } } @@ -1053,7 +1058,7 @@ where V4L2_CID_IMAGE_PROC_CLASS => return Err(libc::EACCES), V4L2_CID_TEST_PATTERN => { let val = unsafe { ctrl.__bindgen_anon_1.value }; - if !(0..=0).contains(&val) { + if !(0..=1).contains(&val) { return Err(libc::ERANGE); } self.current_pattern = val as u32; diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs index 3b121608397..91b12dc81ba 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs @@ -1,6 +1,7 @@ use std::io::Write; pub mod pulse; +pub mod smpte; pub const WIDTH: u32 = 640; pub const HEIGHT: u32 = 480; diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/smpte.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/smpte.rs new file mode 100644 index 00000000000..bcd62b28a21 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/smpte.rs @@ -0,0 +1,115 @@ +use super::{FramePattern, HEIGHT, WIDTH}; +use std::io::Write; + +const SMPTE_COLOR_WHITE: (u8, u8, u8) = (180, 128, 128); +const SMPTE_COLOR_YELLOW: (u8, u8, u8) = (162, 44, 142); +const SMPTE_COLOR_CYAN: (u8, u8, u8) = (131, 156, 44); +const SMPTE_COLOR_GREEN: (u8, u8, u8) = (112, 72, 58); +const SMPTE_COLOR_MAGENTA: (u8, u8, u8) = (84, 184, 198); +const SMPTE_COLOR_RED: (u8, u8, u8) = (65, 100, 212); +const SMPTE_COLOR_BLUE: (u8, u8, u8) = (35, 212, 114); +const SMPTE_COLOR_BLACK: (u8, u8, u8) = (16, 128, 128); +const SMPTE_COLOR_DARK_GRAY: (u8, u8, u8) = (25, 128, 128); + +pub struct SmpteBars; + +impl FramePattern for SmpteBars { + fn write( + &self, + iteration: u64, + mut sink_y: WY, + mut sink_u: WU, + mut sink_v: WV, + ) -> Result<(), i32> { + let sequence = iteration; + let box_size = 80u32; // Scaled for 640x480 + + // Helper for triangle wave (constant velocity bounce) + let bouncing_box_coord = |t: u64, range: u32| -> u32 { + let range64 = range as u64; + let period = 2 * range64; + let val = t % period; + (if val < range64 { val } else { period - val }) as u32 + }; + + let box_x = bouncing_box_coord(sequence * 8, WIDTH - box_size); + let box_y = bouncing_box_coord(sequence * 5, HEIGHT - box_size); + + let is_inside_box = |x: u32, y: u32| -> bool { + x >= box_x && x < box_x + box_size && y >= box_y && y < box_y + box_size + }; + + let get_smpte_color = |x: u32, y: u32| -> (u8, u8, u8) { + let bars = [ + SMPTE_COLOR_WHITE, + SMPTE_COLOR_YELLOW, + SMPTE_COLOR_CYAN, + SMPTE_COLOR_GREEN, + SMPTE_COLOR_MAGENTA, + SMPTE_COLOR_RED, + SMPTE_COLOR_BLUE, + SMPTE_COLOR_BLACK, + ]; + let bar_width = WIDTH / 7; + let bar_idx = std::cmp::min(x / bar_width, 6) as usize; + + let row1_height = HEIGHT * 2 / 3; + let row2_height = HEIGHT * 3 / 4; + + if y < row1_height { + bars[bar_idx] + } else if y < row2_height { + // Reversed bars for middle row + bars[6 - bar_idx] + } else { + // Bottom row blocks + if x < bar_width { + SMPTE_COLOR_BLUE + } else if x < bar_width * 2 { + SMPTE_COLOR_WHITE + } else if x < bar_width * 3 { + SMPTE_COLOR_MAGENTA + } else if x < bar_width * 4 { + SMPTE_COLOR_BLACK + } else { + SMPTE_COLOR_DARK_GRAY + } + } + }; + + // Write Y Plane + let mut y_plane = Vec::with_capacity((WIDTH * HEIGHT) as usize); + for y_idx in 0..HEIGHT { + for x_idx in 0..WIDTH { + let (y, _, _) = get_smpte_color(x_idx, y_idx); + let is_box = is_inside_box(x_idx, y_idx); + y_plane.push(if is_box { 255 - y } else { y }); + } + } + sink_y.write_all(&y_plane).map_err(|_| libc::EIO)?; + + // Write U Plane + let mut u_plane = Vec::with_capacity((WIDTH * HEIGHT / 4) as usize); + for y_idx in 0..(HEIGHT / 2) { + for x_idx in 0..(WIDTH / 2) { + let (_, u, _) = get_smpte_color(x_idx * 2, y_idx * 2); + let is_box = is_inside_box(x_idx * 2, y_idx * 2); + u_plane.push(if is_box { 255 - u } else { u }); + } + } + sink_u.write_all(&u_plane).map_err(|_| libc::EIO)?; + + // Write V Plane + let mut v_plane = Vec::with_capacity((WIDTH * HEIGHT / 4) as usize); + for y_idx in 0..(HEIGHT / 2) { + for x_idx in 0..(WIDTH / 2) { + let (_, _, v) = get_smpte_color(x_idx * 2, y_idx * 2); + let is_box = is_inside_box(x_idx * 2, y_idx * 2); + v_plane.push(if is_box { 255 - v } else { v }); + } + } + sink_v.write_all(&v_plane).map_err(|_| libc::EIO)?; + + Ok(()) + } +} From b23512b5d5fedc265a063158a5473734ef199758 Mon Sep 17 00:00:00 2001 From: Changyeon Jo Date: Wed, 15 Jul 2026 18:08:22 +0000 Subject: [PATCH 6/6] Add Julia Set fractal test pattern Implement the CPU-intensive animated Julia Set fractal pattern inside `src/pattern/julia_set.rs`. Register it in the `pattern` module and expand `device.rs` V4L2 control ranges and query menus to support selecting index 2. Bug: b/490699808 Bug: b/502639876 --- .../emulated_camera_mplane/BUILD.bazel | 1 + .../emulated_camera_mplane/src/device.rs | 19 +++++--- .../src/pattern/julia_set.rs | 47 +++++++++++++++++++ .../emulated_camera_mplane/src/pattern/mod.rs | 1 + 4 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/julia_set.rs diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel index e6e856c1d47..7329933e01c 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/BUILD.bazel @@ -19,6 +19,7 @@ rust_binary( "src/pattern/mod.rs", "src/pattern/pulse.rs", "src/pattern/smpte.rs", + "src/pattern/julia_set.rs", ], edition = "2024", deps = crate_deps([ 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 d51baddf4eb..40c2191aebf 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 @@ -22,6 +22,7 @@ use std::os::fd::BorrowedFd; use std::io::Write; use crate::pattern::FramePattern; +use crate::pattern::julia_set::JuliaSet; use crate::pattern::pulse::Pulse; use crate::pattern::smpte::SmpteBars; @@ -210,7 +211,10 @@ impl EmulatedCameraSession { 0 => Pulse .write(iteration, sink_y, sink_u, sink_v) .map_err(|_| libc::EIO), - _ => SmpteBars + 1 => SmpteBars + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO), + _ => JuliaSet .write(iteration, sink_y, sink_u, sink_v) .map_err(|_| libc::EIO), } @@ -354,7 +358,7 @@ const CONTROLS: [ControlDef; 3] = [ type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU, name: "Test Pattern", minimum: 0, - maximum: 1, + maximum: 2, step: 1, default_value: 0, flags: 0, @@ -939,13 +943,14 @@ where index: u32, ) -> IoctlResult { if id == V4L2_CID_TEST_PATTERN { - if index > 1 { + if index > 2 { return Err(libc::EINVAL); } let mut name = [0u8; 32]; let name_str = match index { 0 => "Pulse", - _ => "SMPTE + Bouncing Box", + 1 => "SMPTE + Bouncing Box", + _ => "Animated Julia Set", }; let bytes = name_str.as_bytes(); name[0..bytes.len()].copy_from_slice(bytes); @@ -979,7 +984,7 @@ where value: i32, ) -> IoctlResult { if id == V4L2_CID_TEST_PATTERN { - if (0..=1).contains(&value) { + if (0..=2).contains(&value) { self.current_pattern = value as u32; Ok(bindings::v4l2_control { id, value }) } else { @@ -1033,7 +1038,7 @@ where V4L2_CID_IMAGE_PROC_CLASS => {} V4L2_CID_TEST_PATTERN => { let val = unsafe { ctrl.__bindgen_anon_1.value }; - if !(0..=1).contains(&val) { + if !(0..=2).contains(&val) { return Err(libc::ERANGE); } } @@ -1058,7 +1063,7 @@ where V4L2_CID_IMAGE_PROC_CLASS => return Err(libc::EACCES), V4L2_CID_TEST_PATTERN => { let val = unsafe { ctrl.__bindgen_anon_1.value }; - if !(0..=1).contains(&val) { + if !(0..=2).contains(&val) { return Err(libc::ERANGE); } self.current_pattern = val as u32; diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/julia_set.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/julia_set.rs new file mode 100644 index 00000000000..dfb57ffc812 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/julia_set.rs @@ -0,0 +1,47 @@ +use super::{FramePattern, HEIGHT, WIDTH}; +use std::io::Write; + +pub struct JuliaSet; + +impl FramePattern for JuliaSet { + fn write( + &self, + iteration: u64, + mut sink_y: WY, + mut sink_u: WU, + mut sink_v: WV, + ) -> Result<(), i32> { + let sequence = iteration; + let max_iter = 32usize; + let angle = (sequence as f32) * 0.04f32; + let c_re = 0.7885f32 * angle.cos(); + let c_im = 0.7885f32 * angle.sin(); + + // Write Y Plane (Fractal Detail) + let mut y_plane = Vec::with_capacity((WIDTH * HEIGHT) as usize); + for y_idx in 0..HEIGHT as usize { + for x_idx in 0..WIDTH as usize { + let mut z_re = 1.5f32 * (x_idx as f32 - WIDTH as f32 / 2.0) / (0.5 * WIDTH as f32); + let mut z_im = (y_idx as f32 - HEIGHT as f32 / 2.0) / (0.5 * HEIGHT as f32); + let mut iter = 0usize; + while z_re * z_re + z_im * z_im < 4.0 && iter < max_iter { + let next_re = z_re * z_re - z_im * z_im + c_re; + z_im = 2.0 * z_re * z_im + c_im; + z_re = next_re; + iter += 1; + } + y_plane.push((iter as u8 * 7).wrapping_add(16)); + } + } + sink_y.write_all(&y_plane).map_err(|_| libc::EIO)?; + + // Write U/V Planes (Constant Neutral) + let uv_size = (WIDTH * HEIGHT / 4) as usize; + let u_plane = vec![128u8; uv_size]; + let v_plane = vec![128u8; uv_size]; + sink_u.write_all(&u_plane).map_err(|_| libc::EIO)?; + sink_v.write_all(&v_plane).map_err(|_| libc::EIO)?; + + Ok(()) + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs index 91b12dc81ba..33a962651cc 100644 --- a/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs @@ -1,5 +1,6 @@ use std::io::Write; +pub mod julia_set; pub mod pulse; pub mod smpte;