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..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 @@ -16,6 +16,10 @@ rust_binary( srcs = [ "src/device.rs", "src/main.rs", + "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 d81245ac522..0110d7719a8 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,32 @@ 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 + )), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TestPattern { + Undefined = 0, + Pulse = 1, + SmpteBars = 2, + JuliaSet = 3, +} + +impl TryFrom for TestPattern { + type Error = i32; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(TestPattern::Undefined), + 1 => Ok(TestPattern::Pulse), + 2 => Ok(TestPattern::SmpteBars), + 3 => Ok(TestPattern::JuliaSet), + _ => Err(libc::ERANGE), } } } @@ -192,34 +221,31 @@ 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, + pattern: TestPattern, + 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)?; + match pattern { + TestPattern::SmpteBars => SmpteBars + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO), + TestPattern::JuliaSet => JuliaSet + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO), + _ => Pulse + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO), } - Ok(()) } /// Write basic pattern into the queued buffers fn process_queued_buffers( &mut self, evt_queue: &mut Q, + pattern: TestPattern, ) -> IoctlResult<()> { while let Some(buf_id) = self.queued_buffers.pop_front() { let iteration = self.iteration; @@ -235,6 +261,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(), @@ -273,6 +300,8 @@ pub struct EmulatedCamera, /// Lens facing configuration. lens_facing: LensFacing, + /// Currently selected test pattern. + current_pattern: TestPattern, } impl EmulatedCamera @@ -286,6 +315,7 @@ where mmap_manager: MmapMappingManager::from(mapper), active_session: None, lens_facing, + current_pattern: TestPattern::Pulse, } } @@ -309,6 +339,73 @@ where } } +struct ControlDef { + id: u32, + type_: u32, + name: &'static str, + minimum: i32, + maximum: i32, + step: u32, + default_value: i32, + flags: u32, +} + +impl ControlDef { + fn to_v4l2_query_ext_ctrl(&self) -> bindings::v4l2_query_ext_ctrl { + let mut name = [0 as ::std::os::raw::c_char; 32]; + let bytes = self.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; + } + + let (max_val, default_val) = if self.id == bindings::V4L2_CID_TEST_PATTERN { + (3, 1) // Pulse is 1, SmpteBars is 2, JuliaSet is 3 + } else { + (self.maximum as i64, self.default_value as i64) + }; + + bindings::v4l2_query_ext_ctrl { + id: self.id, + type_: self.type_, + name, + minimum: self.minimum as i64, + maximum: max_val, + step: self.step as u64, + default_value: default_val, + flags: self.flags, + elem_size: 4, + elems: 1, + nr_of_dims: 0, + dims: [0; 4], + ..Default::default() + } + } +} + +const CONTROLS: [ControlDef; 2] = [ + ControlDef { + id: bindings::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, + }, + ControlDef { + id: bindings::V4L2_CID_TEST_PATTERN, + type_: bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_MENU, + name: "Test Pattern", + minimum: 0, + maximum: 1, + step: 1, + default_value: 1, + flags: 0, + }, +]; + impl VirtioMediaDevice for EmulatedCamera where Q: VirtioMediaEventQueue, @@ -724,7 +821,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 +833,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 +935,81 @@ 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()); + } + if let Some(ctrl) = CONTROLS.iter().find(|ctrl| ctrl.id > requested_id) { + return Ok(ctrl.to_v4l2_query_ext_ctrl()); + } + } else { + if requested_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()); + if let Some(ctrl) = CONTROLS.iter().find(|ctrl| ctrl.id == requested_id) { + return Ok(ctrl.to_v4l2_query_ext_ctrl()); + } } - return Err(libc::EINVAL); + + 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); + } + if index > 3 { + return Err(libc::EINVAL); + } + + let mut name = [0u8; 32]; + let name_str = match index { + 0 => "Undefined", + 1 => "Pulse", + 2 => "SMPTE + Bouncing Box", + _ => "Animated Julia Set", + }; + 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() + }) + } + + fn g_ctrl(&mut self, _session: &Self::Session, id: u32) -> IoctlResult { + if id != bindings::V4L2_CID_TEST_PATTERN { + return Err(libc::EINVAL); + } + Ok(bindings::v4l2_control { + id, + value: self.current_pattern as i32, + }) + } + + fn s_ctrl( + &mut self, + _session: &mut Self::Session, + id: u32, + value: i32, + ) -> IoctlResult { + if id != bindings::V4L2_CID_TEST_PATTERN { + return Err(libc::EINVAL); + } + + let pattern = TestPattern::try_from(value).map_err(|_| libc::ERANGE)?; + self.current_pattern = pattern; + Ok(bindings::v4l2_control { id, value }) } fn g_ext_ctrls( @@ -858,13 +1020,20 @@ 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; } - _ => { + bindings::V4L2_CID_TEST_PATTERN => { + ctrl.__bindgen_anon_1.value = self.current_pattern as i32; + } + bindings::V4L2_CID_IMAGE_PROC_CLASS => { ctrls.error_idx = ctrls.count; + return Err(libc::EACCES); + } + _ => { + ctrls.error_idx = i as u32; return Err(libc::EINVAL); } } @@ -880,13 +1049,17 @@ 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().enumerate() { + ctrls.error_idx = i as u32; + match ctrl.id { + CID_LENS_FACING => return Err(libc::EACCES), + bindings::V4L2_CID_IMAGE_PROC_CLASS => return Err(libc::EACCES), + bindings::V4L2_CID_TEST_PATTERN => { + let val = unsafe { ctrl.__bindgen_anon_1.value }; + TestPattern::try_from(val).map_err(|_| libc::ERANGE)?; + } + _ => return Err(libc::EINVAL), + } } Ok(()) } @@ -899,13 +1072,37 @@ where ctrl_array: &mut Vec, _user_regions: Vec>, ) -> IoctlResult<()> { + for (i, ctrl) in ctrl_array.iter().enumerate() { + ctrls.error_idx = i as u32; + match ctrl.id { + CID_LENS_FACING => return Err(libc::EACCES), + bindings::V4L2_CID_IMAGE_PROC_CLASS => return Err(libc::EACCES), + bindings::V4L2_CID_TEST_PATTERN => { + let val = unsafe { ctrl.__bindgen_anon_1.value }; + TestPattern::try_from(val).map_err(|_| libc::ERANGE)?; + } + _ => return Err(libc::EINVAL), + } + } 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); + if ctrl.id == bindings::V4L2_CID_TEST_PATTERN { + let val = unsafe { ctrl.__bindgen_anon_1.value }; + if let Ok(pattern) = TestPattern::try_from(val) { + self.current_pattern = pattern; + + let mut ctrl_event: bindings::v4l2_event = unsafe { std::mem::zeroed() }; + ctrl_event.type_ = bindings::V4L2_EVENT_CTRL; + ctrl_event.id = bindings::V4L2_CID_TEST_PATTERN; + ctrl_event.u.ctrl.type_ = bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER; + ctrl_event.u.ctrl.__bindgen_anon_1.value = self.current_pattern as i32; + ctrl_event.u.ctrl.minimum = 0; + ctrl_event.u.ctrl.maximum = 3; + ctrl_event.u.ctrl.step = 1; + ctrl_event.u.ctrl.default_value = 1; + self.evt_queue + .send_event(V4l2Event::Event(SessionEvent::new(_session.id, ctrl_event))); + } + } } Ok(()) } @@ -919,20 +1116,40 @@ 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), - }, + let id = match event { + V4l2EventType::Ctrl(id) => id, + _ => return Err(libc::EINVAL), + }; + + match id { + CID_LENS_FACING => { + let mut ctrl_event: bindings::v4l2_event = unsafe { std::mem::zeroed() }; + ctrl_event.type_ = bindings::V4L2_EVENT_CTRL; + ctrl_event.id = CID_LENS_FACING; + ctrl_event.u.ctrl.type_ = bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER; + ctrl_event.u.ctrl.__bindgen_anon_1.value = self.lens_facing as i32; + ctrl_event.u.ctrl.minimum = LensFacing::Front as i32; + ctrl_event.u.ctrl.maximum = LensFacing::External as i32; + ctrl_event.u.ctrl.step = 1; + ctrl_event.u.ctrl.default_value = LensFacing::Front as i32; + self.evt_queue + .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); + Ok(()) + } + bindings::V4L2_CID_TEST_PATTERN => { + let mut ctrl_event: bindings::v4l2_event = unsafe { std::mem::zeroed() }; + ctrl_event.type_ = bindings::V4L2_EVENT_CTRL; + ctrl_event.id = bindings::V4L2_CID_TEST_PATTERN; + ctrl_event.u.ctrl.type_ = bindings::v4l2_ctrl_type_V4L2_CTRL_TYPE_INTEGER; + ctrl_event.u.ctrl.__bindgen_anon_1.value = self.current_pattern as i32; + ctrl_event.u.ctrl.minimum = 0; + ctrl_event.u.ctrl.maximum = 1; + ctrl_event.u.ctrl.step = 1; + ctrl_event.u.ctrl.default_value = 1; + self.evt_queue + .send_event(V4l2Event::Event(SessionEvent::new(session.id, ctrl_event))); + Ok(()) + } _ => Err(libc::EINVAL), } } @@ -942,10 +1159,12 @@ where _session: &mut Self::Session, event: bindings::v4l2_event_subscription, ) -> IoctlResult<()> { - return if event.type_ == bindings::V4L2_EVENT_CTRL && event.id == CID_LENS_FACING { - Ok(()) - } else { - Err(libc::EINVAL) - }; + if event.type_ != bindings::V4L2_EVENT_CTRL { + return Err(libc::EINVAL); + } + if event.id != CID_LENS_FACING && event.id != bindings::V4L2_CID_TEST_PATTERN { + return Err(libc::EINVAL); + } + Ok(()) } } 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..918adbe97e8 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/julia_set.rs @@ -0,0 +1,61 @@ +// 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; + +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 new file mode 100644 index 00000000000..a16f612fd9c --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs @@ -0,0 +1,32 @@ +// 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; + +pub const WIDTH: u32 = 640; +pub const HEIGHT: u32 = 480; + +pub trait FramePattern: Send + Sync { + fn write( + &self, + iteration: u64, + sink_y: WY, + sink_u: WU, + sink_v: WV, + ) -> Result<(), i32>; +} 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..4c037cea107 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/pulse.rs @@ -0,0 +1,40 @@ +// 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; + +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(()) + } +} 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..f29120c186e --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/smpte.rs @@ -0,0 +1,129 @@ +// 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); + +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(()) + } +}