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..58237e9c08b 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 @@ -1,4 +1,4 @@ -load("@rules_rust//rust:defs.bzl", "rust_binary") +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") load("@vhost_user_media_workspace_crates//:defs.bzl", "crate_deps") package(default_visibility = ["//:android_cuttlefish"]) @@ -16,6 +16,10 @@ rust_binary( srcs = [ "src/device.rs", "src/main.rs", + "src/pattern/julia_set.rs", + "src/pattern/mod.rs", + "src/pattern/pulse.rs", + "src/pattern/smpte.rs", ], edition = "2024", deps = crate_deps([ @@ -33,6 +37,12 @@ rust_binary( ], ) +rust_test( + name = "emulated_camera_mplane_test", + crate = ":emulated_camera_mplane_binary", + target_compatible_with = ["@platforms//cpu:x86_64"], +) + genrule( name = "unsupported_binary", outs = ["unsupported.sh"], 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..8a18bb6cb90 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,12 @@ use std::io::Write; use std::os::fd::AsFd; use std::os::fd::BorrowedFd; +use crate::pattern::FramePattern; +use crate::pattern::julia_set::JuliaSet; +use crate::pattern::pulse::Pulse; +use crate::pattern::smpte::SmpteBars; + +use std::str::FromStr; use v4l2r::PixelFormat; use v4l2r::QueueType; use v4l2r::bindings; @@ -55,7 +60,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 +77,62 @@ 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 + )), + } + } +} + +/// 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. + 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), } } } @@ -192,34 +251,24 @@ 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, + test_pattern: TestPattern, + sink_y: &mut dyn Write, + sink_u: &mut dyn Write, + sink_v: &mut dyn Write, ) -> 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(()) + test_pattern + .generator() + .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, + test_pattern: TestPattern, ) -> IoctlResult<()> { while let Some(buf_id) = self.queued_buffers.pop_front() { let iteration = self.iteration; @@ -233,11 +282,15 @@ impl EmulatedCameraSession { .map_err(|_| libc::EIO)?; } + let mut plane_y = buffer.planes[0].fd.as_file(); + let mut plane_u = buffer.planes[1].fd.as_file(); + let mut plane_v = buffer.planes[2].fd.as_file(); Self::write_pattern( iteration, - buffer.planes[0].fd.as_file(), - buffer.planes[1].fd.as_file(), - buffer.planes[2].fd.as_file(), + test_pattern, + &mut plane_y, + &mut plane_u, + &mut plane_v, )?; buffer.set_state(BufferState::Outgoing { @@ -273,6 +326,8 @@ pub struct EmulatedCamera, /// Lens facing configuration. lens_facing: LensFacing, + /// Currently selected test pattern. + current_pattern: TestPattern, } impl EmulatedCamera @@ -286,17 +341,15 @@ where mmap_manager: MmapMappingManager::from(mapper), active_session: None, lens_facing, + current_pattern: TestPattern::Pulse, } } fn lens_facing_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { - let name_str = "LENS_FACING"; - let mut name = [0u8; 32]; - name[0..name_str.len()].copy_from_slice(name_str.as_bytes()); bindings::v4l2_query_ext_ctrl { id: CID_LENS_FACING, type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER, - name: name.map(|b| b as i8), + name: ctrl_name("LENS_FACING").map(|b| b as i8), minimum: LensFacing::Front as i64, maximum: LensFacing::External as i64, step: 1, @@ -307,6 +360,117 @@ where ..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(&self, 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 = self.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_TEST_PATTERN => { + event.u.ctrl.type_ = bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU; + event.u.ctrl.__bindgen_anon_1.value = self.current_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.current_pattern == pattern { + return Ok(()); + } + self.current_pattern = pattern; + let event = self.ctrl_event(bindings::V4L2_CID_TEST_PATTERN)?; + self.evt_queue + .send_event(V4l2Event::Event(SessionEvent::new(session_id, event))); + Ok(()) + } + + /// Checks that every control in `ctrl_array` can be written with the requested value, + /// pointing `error_idx` at the offending control on failure. + fn validate_ext_ctrls( + ctrls: &mut bindings::v4l2_ext_controls, + ctrl_array: &[bindings::v4l2_ext_control], + ) -> IoctlResult<()> { + for (idx, ctrl) in ctrl_array.iter().enumerate() { + let result = match ctrl.id { + CID_LENS_FACING | bindings::V4L2_CID_IMAGE_PROC_CLASS => Err(libc::EACCES), + bindings::V4L2_CID_TEST_PATTERN => { + // SAFETY: this is an integer control, so the guest-provided payload is + // in the `value` arm of the union. + TestPattern::try_from(unsafe { ctrl.__bindgen_anon_1.value }).map(|_| ()) + } + _ => Err(libc::EINVAL), + }; + if let Err(err) = result { + ctrls.error_idx = idx as u32; + return Err(err); + } + } + Ok(()) + } +} + +/// Copies `name` into a NUL-padded, fixed-size V4L2 control name buffer, truncating if +/// it does not fit. +fn ctrl_name(name: &str) -> [u8; 32] { + let mut buf = [0u8; 32]; + let bytes = name.as_bytes(); + let len = std::cmp::min(bytes.len(), buf.len() - 1); + buf[..len].copy_from_slice(&bytes[..len]); + buf } impl VirtioMediaDevice for EmulatedCamera @@ -392,8 +556,8 @@ const CID_OFFSET: u32 = bindings::V4L2_CID_CAMERA_CLASS_BASE + 0x100; const CID_LENS_FACING: u32 = CID_OFFSET + 1; const PIXELFORMAT: u32 = PixelFormat::from_fourcc(b"YM12").to_u32(); -const WIDTH: u32 = 640; -const HEIGHT: u32 = 480; +pub(crate) const WIDTH: u32 = 640; +pub(crate) const HEIGHT: u32 = 480; const FRAME_RATE: u32 = 30; const INPUTS: [bindings::v4l2_input; 1] = [bindings::v4l2_input { @@ -724,7 +888,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) @@ -736,7 +900,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(()) } @@ -838,16 +1002,86 @@ 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. + let requested_id: u32 = unsafe { std::mem::transmute(id) }; + if flags.contains(QueryCtrlFlags::NEXT) { - if id < CID_LENS_FACING { + 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 { + 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) + } + + fn querymenu( + &mut self, + _session: &Self::Session, + 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() + }) + } + + fn g_ctrl(&mut self, _session: &Self::Session, id: u32) -> IoctlResult { + let value = match id { + CID_LENS_FACING => self.lens_facing as i32, + bindings::V4L2_CID_TEST_PATTERN => self.current_pattern as i32, + bindings::V4L2_CID_IMAGE_PROC_CLASS => return Err(libc::EACCES), + _ => return Err(libc::EINVAL), + }; + Ok(bindings::v4l2_control { id, value }) + } + + fn s_ctrl( + &mut self, + session: &mut Self::Session, + id: u32, + value: i32, + ) -> IoctlResult { + match id { + CID_LENS_FACING | bindings::V4L2_CID_IMAGE_PROC_CLASS => Err(libc::EACCES), + bindings::V4L2_CID_TEST_PATTERN => { + let pattern = TestPattern::try_from(value)?; + self.set_test_pattern(session.id, pattern)?; + // Report back the value that was actually applied. + Ok(bindings::v4l2_control { + id, + value: pattern as i32, + }) } - } else if id == CID_LENS_FACING { - return Ok(self.lens_facing_query_ext_ctrl()); + _ => Err(libc::EINVAL), } - return Err(libc::EINVAL); } fn g_ext_ctrls( @@ -858,16 +1092,21 @@ where ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { - for ctrl in ctrl_array { - match ctrl.id { - CID_LENS_FACING => { - ctrl.__bindgen_anon_1.value = self.lens_facing as i32; + for (idx, ctrl) in ctrl_array.iter_mut().enumerate() { + let value = match ctrl.id { + CID_LENS_FACING => self.lens_facing as i32, + bindings::V4L2_CID_TEST_PATTERN => self.current_pattern as i32, + // A control class holds no value that could be read back. + bindings::V4L2_CID_IMAGE_PROC_CLASS => { + ctrls.error_idx = idx as u32; + return Err(libc::EACCES); } _ => { - ctrls.error_idx = ctrls.count; + ctrls.error_idx = idx as u32; return Err(libc::EINVAL); } - } + }; + ctrl.__bindgen_anon_1.value = value; } Ok(()) } @@ -880,32 +1119,31 @@ 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); - } - Ok(()) + Self::validate_ext_ctrls(ctrls, ctrl_array) } fn s_ext_ctrls( &mut self, - _session: &mut Self::Session, + session: &mut Self::Session, _which: CtrlWhich, ctrls: &mut bindings::v4l2_ext_controls, ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { - for ctrl in ctrl_array { - ctrls.error_idx = ctrls.count; - let err_code = match ctrl.id { - CID_LENS_FACING => libc::EACCES, - _ => libc::EINVAL, - }; - return Err(err_code); + // Validate the whole request before applying any of it, so a rejected control + // cannot leave the device half-updated. + Self::validate_ext_ctrls(ctrls, ctrl_array)?; + + for ctrl in ctrl_array.iter_mut() { + if ctrl.id != bindings::V4L2_CID_TEST_PATTERN { + continue; + } + // SAFETY: this is an integer control, so the guest-provided payload is in the + // `value` arm of the union. + let pattern = TestPattern::try_from(unsafe { ctrl.__bindgen_anon_1.value })?; + self.set_test_pattern(session.id, pattern)?; + // Report back the value that was actually applied. + ctrl.__bindgen_anon_1.value = pattern as i32; } Ok(()) } @@ -919,22 +1157,14 @@ where if !flags.contains(SubscribeEventFlags::SEND_INITIAL) { return Err(libc::EINVAL); } - match event { - V4l2EventType::Ctrl(id) => match id { - CID_LENS_FACING => { - let ctrl_event = bindings::v4l2_event { - type_: bindings::V4L2_EVENT_CTRL, - id: CID_LENS_FACING, - ..Default::default() - }; - self.evt_queue - .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); - Ok(()) - } - _ => Err(libc::EINVAL), - }, - _ => Err(libc::EINVAL), - } + let V4l2EventType::Ctrl(id) = event else { + return Err(libc::EINVAL); + }; + + let ctrl_event = self.ctrl_event(id)?; + self.evt_queue + .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); + Ok(()) } fn unsubscribe_event( @@ -942,10 +1172,53 @@ where _session: &mut Self::Session, event: bindings::v4l2_event_subscription, ) -> IoctlResult<()> { - return if event.type_ == bindings::V4L2_EVENT_CTRL && event.id == CID_LENS_FACING { - Ok(()) - } else { - Err(libc::EINVAL) - }; + if event.type_ != bindings::V4L2_EVENT_CTRL { + return Err(libc::EINVAL); + } + match event.id { + CID_LENS_FACING | bindings::V4L2_CID_TEST_PATTERN => Ok(()), + _ => 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_mplane/src/main.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/main.rs index dfd44608c19..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)] @@ -62,7 +63,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, 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..8d73d6db4e8 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/julia_set.rs @@ -0,0 +1,77 @@ +// 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. + +use super::{FramePattern, HEIGHT, WIDTH}; +use std::io::Write; + +/// Escape-time iterations spent on each pixel. +const MAX_ITER: u32 = 32; +/// Luma assigned to a pixel that escapes immediately. +const LUMA_BASE: u32 = 16; +/// Luma added per escape-time iteration. +const LUMA_PER_ITER: u32 = 7; + +// The luma mapping below narrows to `u8`, so the brightest pixel has to fit. +const _: () = assert!(LUMA_BASE + MAX_ITER * LUMA_PER_ITER <= u8::MAX as u32); + +/// How far the constant `c` rotates per frame, in radians. +const ANGLE_STEP: f32 = 0.04; +/// Frames per full rotation. Wrapping `iteration` here keeps `angle` small enough that +/// `f32` can still resolve `ANGLE_STEP`, which it cannot once the angle grows past a few +/// hundred thousand radians. +const PERIOD_FRAMES: u64 = (std::f32::consts::TAU / ANGLE_STEP) as u64; + +/// Animated Julia set, deliberately expensive to render. +pub struct JuliaSet; + +impl FramePattern for JuliaSet { + fn write( + &self, + iteration: u64, + sink_y: &mut dyn Write, + sink_u: &mut dyn Write, + sink_v: &mut dyn Write, + ) -> Result<(), i32> { + let angle = (iteration % PERIOD_FRAMES) as f32 * ANGLE_STEP; + 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 = 0u32; + 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((LUMA_BASE + iter * LUMA_PER_ITER) as u8); + } + } + 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 new file mode 100644 index 00000000000..88ff7a762ea --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs @@ -0,0 +1,98 @@ +// 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. + +use std::io::Write; + +pub mod julia_set; +pub mod pulse; +pub mod smpte; + +/// Frame geometry is owned by the device, which advertises it to the guest through +/// `v4l2_format`. Patterns re-export it so that both always agree on the plane sizes. +pub(crate) use crate::device::HEIGHT; +pub(crate) use crate::device::WIDTH; + +/// Generator for a single multi-planar YUV 4:2:0 frame. +/// +/// The trait is kept object safe so that the active pattern can be selected at runtime +/// and held as a `&dyn FramePattern`. +pub trait FramePattern { + /// Writes the frame identified by `iteration` into the three plane sinks. + /// + /// Implementations must write exactly `WIDTH * HEIGHT` bytes to `sink_y` and + /// `WIDTH * HEIGHT / 4` bytes to each of `sink_u` and `sink_v`, which is the plane + /// size the device advertised to the guest. + /// + /// Returns a raw `errno` value on failure. + fn write( + &self, + iteration: u64, + sink_y: &mut dyn Write, + sink_u: &mut dyn Write, + sink_v: &mut dyn Write, + ) -> Result<(), i32>; +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Renders a few frames and checks that each plane is exactly the size the device + /// advertised. A short write would leave stale bytes in the guest buffer. + fn assert_plane_sizes(pattern: &dyn FramePattern) { + for iteration in [0u64, 1, 255, 4096] { + let mut plane_y = Vec::new(); + let mut plane_u = Vec::new(); + let mut plane_v = Vec::new(); + pattern + .write(iteration, &mut plane_y, &mut plane_u, &mut plane_v) + .expect("writing into a Vec cannot fail"); + assert_eq!(plane_y.len(), (WIDTH * HEIGHT) as usize); + assert_eq!(plane_u.len(), (WIDTH * HEIGHT / 4) as usize); + assert_eq!(plane_v.len(), (WIDTH * HEIGHT / 4) as usize); + } + } + + #[test] + fn pulse_writes_full_planes() { + assert_plane_sizes(&pulse::Pulse); + } + + #[test] + fn smpte_bars_writes_full_planes() { + assert_plane_sizes(&smpte::SmpteBars); + } + + #[test] + fn julia_set_writes_full_planes() { + assert_plane_sizes(&julia_set::JuliaSet); + } + + /// The rotation wraps rather than growing without bound, so a frame far into a long + /// streaming session still animates instead of freezing on a quantized angle. + #[test] + fn julia_set_keeps_animating_after_a_long_run() { + let far_out = 50_000_000u64; + let luma_at = |iteration: u64| { + let mut plane_y = Vec::new(); + let mut plane_u = Vec::new(); + let mut plane_v = Vec::new(); + julia_set::JuliaSet + .write(iteration, &mut plane_y, &mut plane_u, &mut plane_v) + .expect("writing into a Vec cannot fail"); + plane_y + }; + assert_ne!(luma_at(far_out), luma_at(far_out + 1)); + } +} 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..26074b616de --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/pulse.rs @@ -0,0 +1,41 @@ +// 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. + +use super::{FramePattern, HEIGHT, WIDTH}; +use std::io::Write; + +/// Fills the whole frame with a single color that cycles as frames are produced. +pub struct Pulse; + +impl FramePattern for Pulse { + fn write( + &self, + iteration: u64, + sink_y: &mut dyn Write, + sink_u: &mut dyn Write, + sink_v: &mut dyn Write, + ) -> 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(()) + } +} 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..a2b83dbc293 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/smpte.rs @@ -0,0 +1,134 @@ +// 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. + +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); + +/// The color bars of the top row, left to right. +const SMPTE_BARS: [(u8, u8, u8); 7] = [ + SMPTE_COLOR_WHITE, + SMPTE_COLOR_YELLOW, + SMPTE_COLOR_CYAN, + SMPTE_COLOR_GREEN, + SMPTE_COLOR_MAGENTA, + SMPTE_COLOR_RED, + SMPTE_COLOR_BLUE, +]; + +/// SMPTE color bars overlaid with an inverse-color box bouncing around the frame. +pub struct SmpteBars; + +impl FramePattern for SmpteBars { + fn write( + &self, + iteration: u64, + sink_y: &mut dyn Write, + sink_u: &mut dyn Write, + sink_v: &mut dyn Write, + ) -> 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 last_bar = SMPTE_BARS.len() - 1; + let get_smpte_color = |x: u32, y: u32| -> (u8, u8, u8) { + let bar_width = WIDTH / SMPTE_BARS.len() as u32; + // The bars do not divide the width evenly, so the rightmost one absorbs the + // remainder instead of running off the end of the array. + let bar_idx = std::cmp::min(x / bar_width, last_bar as u32) as usize; + + let row1_height = HEIGHT * 2 / 3; + let row2_height = HEIGHT * 3 / 4; + + if y < row1_height { + SMPTE_BARS[bar_idx] + } else if y < row2_height { + // Reversed bars for middle row + SMPTE_BARS[last_bar - 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(()) + } +}