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..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 @@ -13,14 +13,20 @@ // 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; -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::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 +61,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 +78,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 + )), } } } @@ -192,34 +200,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: u32, + 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)?; + match pattern { + 0 => Pulse + .write(iteration, sink_y, sink_u, sink_v) + .map_err(|_| libc::EIO), + 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), } - 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(()) } /// 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; @@ -235,6 +240,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 +279,8 @@ pub struct EmulatedCamera, /// Lens facing configuration. lens_facing: LensFacing, + /// Currently selected test pattern. + current_pattern: u32, } impl EmulatedCamera @@ -286,6 +294,7 @@ where mmap_manager: MmapMappingManager::from(mapper), active_session: None, lens_facing, + current_pattern: 0, } } @@ -309,6 +318,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: 2, + 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, @@ -724,7 +788,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 +800,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 +902,97 @@ 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 > 2 { + return Err(libc::EINVAL); + } + let mut name = [0u8; 32]; + let name_str = match index { + 0 => "Pulse", + 1 => "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() + }) + } 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..=2).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( @@ -858,13 +1003,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); } } @@ -880,13 +1031,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..=2).contains(&val) { + return Err(libc::ERANGE); + } + } + _ => return Err(libc::EINVAL), + } } Ok(()) } @@ -899,13 +1056,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..=2).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(()) } @@ -931,6 +1104,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), @@ -942,7 +1125,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) 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..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 new file mode 100644 index 00000000000..33a962651cc --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/emulated_camera_mplane/src/pattern/mod.rs @@ -0,0 +1,18 @@ +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..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(()) + } +} 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(()) + } +}