From d42f8bd034375ebaa8d1849253b3c4e63ba3d9c9 Mon Sep 17 00:00:00 2001 From: Sergio Andres Rodriguez Orama Date: Mon, 11 Aug 2025 11:28:52 -0400 Subject: [PATCH] Port virtio media simple device as a vhost user media device. Port virtio media reference device `simple_device` as a vhost user implementation. First iteration of `simple_device/src/simple_device.rs` is the same as in virtio-media upstream https://github.com/chromeos/virtio-media/blob/main/device/src/devices/simple_device.rs. ``` cargo run --bin simple_device -- --socket-path /tmp/simple_device.sock ``` ``` cargo run --features "media" -- --log-level=debug run --cpus 4 --mem 4096 \ --rwdisk /path/to/debian-12.img \ --params "root=/dev/vda1" \ --vhost-user media,socket=/tmp/simple_device.sock \ /path/to/bzImage v4l2-compliance -d0 -s ``` Bug: 445229097 --- .../host/commands/vhost_user_media/Cargo.toml | 34 ++ .../vhost_user_media/simple_device/Cargo.toml | 16 + .../simple_device/src/main.rs | 94 +++ .../simple_device/src/simple_device.rs | 546 +++++++++++++++++ .../vhost_user_media/vhu_media/Cargo.toml | 22 + .../vhost_user_media/vhu_media/src/lib.rs | 565 ++++++++++++++++++ 6 files changed, 1277 insertions(+) create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/Cargo.toml create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/Cargo.toml create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/main.rs create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/simple_device.rs create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/Cargo.toml create mode 100644 base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/src/lib.rs diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/Cargo.toml b/base/cvd/cuttlefish/host/commands/vhost_user_media/Cargo.toml new file mode 100644 index 00000000000..beba47960fd --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/Cargo.toml @@ -0,0 +1,34 @@ +[workspace] +resolver = "3" +members = ["simple_device", "vhu_media"] + +[workspace.dependencies] +vhu_media = { path = "vhu_media" } + +# External dependencies +anyhow = { version = "1.0.97", features = [ "default", "std" ] } +clap = { version = "4.5", features = ["derive"] } +env_logger = "0.11" +libc = "0.2" +log = "0.4" +thiserror = "2.0" +vhost = { version = "0.14", features = ["vhost-user-backend"] } +vhost-user-backend = "0.20" +virtio-bindings = "0.2.6" +virtio-media = "0.0.7" +virtio-queue = "0.16.0" +vm-allocator = "0.1.3" +vm-memory = "0.16.2" +vmm-sys-util = "0.15.0" +v4l2r = { version = "0.0.6", features = ["arch64"] } +uuid = { version = "1.8.0", features=["v4"] } +zerocopy = { version = "0.8.13", features = ["derive"] } +bitflags = "2.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" + +[patch.crates-io] +vhost = { path = "/usr/local/google/home/sorama/code/github.com/forks/vhost/vhost" } +vhost-user-backend = { path = "/usr/local/google/home/sorama/code/github.com/forks/vhost/vhost-user-backend" } +virtio-media = { path = "/usr/local/google/home/sorama/code/github.com/virtio-media/device" } + diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/Cargo.toml b/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/Cargo.toml new file mode 100644 index 00000000000..64f008efd28 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "simple_device" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { workspace = true } +env_logger = { workspace = true } +libc = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +v4l2r = { workspace = true } +vhost-user-backend = { workspace = true } +virtio-media = { workspace = true } +vm-memory = { workspace = true } +vhu_media = { workspace = true } diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/main.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/main.rs new file mode 100644 index 00000000000..8d796d75967 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/main.rs @@ -0,0 +1,94 @@ +//! simple_device + +use std::{ + path::PathBuf, + process::exit, + sync::{Arc, RwLock}, + thread::{JoinHandle, spawn}, +}; + +use clap::Parser; +use log::error; +use thiserror::Error; +use vhost_user_backend::VhostUserDaemon; +use vhu_media::VhuMediaBackend; +use virtio_media::protocol::VirtioMediaDeviceConfig; +use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; + +mod simple_device; + +#[derive(Debug, Error)] +pub(crate) enum Error { + #[error("Could not create daemon: {0}")] + CouldNotCreateDaemon(vhost_user_backend::Error), + #[error("Fatal error: {0}")] + ServeFailed(vhost_user_backend::Error), +} + +type Result = std::result::Result; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct CmdLineArgs { + /// Location of vhost-user Unix domain socket. + #[clap(short, long, value_name = "SOCKET")] + socket_path: PathBuf, +} + +#[derive(PartialEq, Debug)] +struct Config { + socket_path: PathBuf, +} + +impl TryFrom for Config { + type Error = Error; + + fn try_from(args: CmdLineArgs) -> Result { + Ok(Config { + socket_path: args.socket_path, + }) + } +} + +fn start_backend(args: CmdLineArgs) -> Result<()> { + let config = Config::try_from(args)?; + let socket_path = config.socket_path.clone(); + let handle: JoinHandle> = spawn(move || { + loop { + let mut card = [0u8; 32]; + let card_name = "simple_device"; + card[0..card_name.len()].copy_from_slice(card_name.as_bytes()); + use virtio_media::v4l2r::ioctl::Capabilities; + let config = VirtioMediaDeviceConfig { + device_caps: (Capabilities::VIDEO_CAPTURE | Capabilities::STREAMING).bits(), + // VFL_TYPE_VIDEO + device_type: 0, + card, + }; + let backend = Arc::new(RwLock::new(VhuMediaBackend::new( + config, + |event_queue, host_mapper| { + simple_device::SimpleCaptureDevice::new(event_queue, host_mapper) + }, + ))); + let mut daemon = VhostUserDaemon::new( + String::from("vhost-user-media-backend"), + backend, + GuestMemoryAtomic::new(GuestMemoryMmap::new()), + ) + .map_err(Error::CouldNotCreateDaemon)?; + daemon.serve(&socket_path).map_err(Error::ServeFailed)?; + } + }); + + handle.join().map_err(std::panic::resume_unwind).unwrap() +} + +fn main() { + env_logger::init(); + + if let Err(e) = start_backend(CmdLineArgs::parse()) { + error!("{e}"); + exit(1); + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/simple_device.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/simple_device.rs new file mode 100644 index 00000000000..6a7e6759f16 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/simple_device/src/simple_device.rs @@ -0,0 +1,546 @@ +//! Simple example virtio-media CAPTURE device with no dependency. +//! +//! This module illustrates how to write a device for virtio-media. It exposes a capture device +//! that generates a RGB pattern on the buffers queued by the guest. + +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 v4l2r::PixelFormat; +use v4l2r::QueueType; +use v4l2r::bindings; +use v4l2r::bindings::v4l2_fmtdesc; +use v4l2r::bindings::v4l2_format; +use v4l2r::bindings::v4l2_pix_format; +use v4l2r::bindings::v4l2_requestbuffers; +use v4l2r::ioctl::BufferCapabilities; +use v4l2r::ioctl::BufferField; +use v4l2r::ioctl::BufferFlags; +use v4l2r::ioctl::V4l2Buffer; +use v4l2r::ioctl::V4l2PlanesWithBackingMut; +use v4l2r::memory::MemoryType; +use virtio_media::VirtioMediaDevice; +use virtio_media::VirtioMediaDeviceSession; +use virtio_media::VirtioMediaEventQueue; +use virtio_media::VirtioMediaHostMemoryMapper; +use virtio_media::io::ReadFromDescriptorChain; +use virtio_media::io::WriteToDescriptorChain; +use virtio_media::ioctl::IoctlResult; +use virtio_media::ioctl::VirtioMediaIoctlHandler; +use virtio_media::ioctl::virtio_media_dispatch_ioctl; +use virtio_media::memfd::MemFdBuffer; +use virtio_media::mmap::MmapMappingManager; +use virtio_media::protocol::DequeueBufferEvent; +use virtio_media::protocol::SgEntry; +use virtio_media::protocol::V4l2Event; +use virtio_media::protocol::V4l2Ioctl; +use virtio_media::protocol::VIRTIO_MEDIA_MMAP_FLAG_RW; + +/// Current status of a buffer. +#[derive(Debug, PartialEq, Eq)] +enum BufferState { + /// Buffer has just been created (or streamed off) and not been used yet. + New, + /// Buffer has been QBUF'd by the driver but not yet processed. + Incoming, + /// Buffer has been processed and is ready for dequeue. + Outgoing { + /// Sequence of the generated frame. + sequence: u32, + }, +} + +/// Information about a single buffer. +struct Buffer { + /// Current state of the buffer. + state: BufferState, + /// V4L2 representation of this buffer to be sent to the guest when requested. + v4l2_buffer: V4l2Buffer, + /// Backing storage for the buffer. + fd: MemFdBuffer, + /// Offset that can be used to map the buffer. + /// + /// Cached from `v4l2_buffer` to avoid doing a match. + offset: u32, +} + +impl Buffer { + /// Update the state of the buffer as well as its V4L2 representation. + fn set_state(&mut self, state: BufferState) { + let mut flags = self.v4l2_buffer.flags(); + match state { + BufferState::New => { + *self.v4l2_buffer.get_first_plane_mut().bytesused = 0; + flags -= BufferFlags::QUEUED; + } + BufferState::Incoming => { + *self.v4l2_buffer.get_first_plane_mut().bytesused = 0; + flags |= BufferFlags::QUEUED; + } + BufferState::Outgoing { sequence } => { + *self.v4l2_buffer.get_first_plane_mut().bytesused = BUFFER_SIZE; + self.v4l2_buffer.set_sequence(sequence); + self.v4l2_buffer.set_timestamp(bindings::timeval { + tv_sec: (sequence + 1) as bindings::__time_t / 1000, + tv_usec: (sequence + 1) as bindings::__time_t % 1000, + }); + flags -= BufferFlags::QUEUED; + } + } + + self.v4l2_buffer.set_flags(flags); + self.state = state; + } +} + +/// Session data of [`SimpleCaptureDevice`]. +pub struct SimpleCaptureDeviceSession { + /// Id of the session. + id: u32, + /// Current iteration of the pattern generation cycle. + iteration: u64, + /// Buffers currently allocated for this session. + buffers: Vec, + /// FIFO of queued buffers awaiting processing. + queued_buffers: VecDeque, + /// Is the session currently streaming? + streaming: bool, +} + +impl VirtioMediaDeviceSession for SimpleCaptureDeviceSession { + fn poll_fd(&self) -> Option> { + None + } +} + +impl SimpleCaptureDeviceSession { + /// Generate the data pattern on all queued buffers and send the corresponding + /// [`DequeueBufferEvent`] to the driver. + fn process_queued_buffers( + &mut self, + evt_queue: &mut Q, + ) -> IoctlResult<()> { + while let Some(buf_id) = self.queued_buffers.pop_front() { + let buffer = self.buffers.get_mut(buf_id).ok_or(libc::EIO)?; + let sequence = self.iteration as u32; + + buffer + .fd + .as_file() + .seek(SeekFrom::Start(0)) + .map_err(|_| libc::EIO)?; + let mut writer = BufWriter::new(buffer.fd.as_file()); + let color = [ + 0xffu8 * (sequence as u8 % 2), + 0x55u8 * (sequence as u8 % 3), + 0x10u8 * (sequence as u8 % 16), + ]; + for _ in 0..(WIDTH * HEIGHT) { + let _ = writer.write(&color).map_err(|_| libc::EIO)?; + } + drop(writer); + + *buffer.v4l2_buffer.get_first_plane_mut().bytesused = BUFFER_SIZE; + buffer.set_state(BufferState::Outgoing { sequence }); + // TODO: should we set the DONE flag here? + self.iteration += 1; + + let v4l2_buffer = buffer.v4l2_buffer.clone(); + + evt_queue.send_event(V4l2Event::DequeueBuffer(DequeueBufferEvent::new( + self.id, + v4l2_buffer, + ))); + } + + Ok(()) + } +} + +/// A simplistic video capture device, used to demonstrate how device code can be written, or for +/// testing VMMs and guests without dedicated hardware support. +/// +/// This device supports a single pixel format (`RGB3`) and a single resolution, and generates +/// frames of varying uniform color. The only buffer type supported is `MMAP` +pub struct SimpleCaptureDevice { + /// Queue used to send events to the guest. + evt_queue: Q, + /// Host MMAP mapping manager. + mmap_manager: MmapMappingManager, + /// ID of the session with allocated buffers, if any. + /// + /// v4l2-compliance checks that only a single session can have allocated buffers at a given + /// time, since that's how actual hardware works - no two sessions can access a camera at the + /// same time. It will fails if we allow simultaneous sessions to be active, so we need this + /// artificial limitation to make it pass fully. + active_session: Option, +} + +impl SimpleCaptureDevice +where + Q: VirtioMediaEventQueue, + HM: VirtioMediaHostMemoryMapper, +{ + pub fn new(evt_queue: Q, mapper: HM) -> Self { + Self { + evt_queue, + mmap_manager: MmapMappingManager::from(mapper), + active_session: None, + } + } +} + +impl VirtioMediaDevice for SimpleCaptureDevice +where + Q: VirtioMediaEventQueue, + HM: VirtioMediaHostMemoryMapper, + Reader: ReadFromDescriptorChain, + Writer: WriteToDescriptorChain, +{ + type Session = SimpleCaptureDeviceSession; + + fn new_session(&mut self, session_id: u32) -> Result { + Ok(SimpleCaptureDeviceSession { + id: session_id, + iteration: 0, + buffers: Default::default(), + queued_buffers: Default::default(), + streaming: false, + }) + } + + fn close_session(&mut self, session: Self::Session) { + if self.active_session == Some(session.id) { + self.active_session = None; + } + + for buffer in &session.buffers { + self.mmap_manager.unregister_buffer(buffer.offset); + } + } + + fn do_ioctl( + &mut self, + session: &mut Self::Session, + ioctl: V4l2Ioctl, + reader: &mut Reader, + writer: &mut Writer, + ) -> IoResult<()> { + virtio_media_dispatch_ioctl(self, session, ioctl, reader, writer) + } + + fn do_mmap( + &mut self, + session: &mut Self::Session, + flags: u32, + offset: u32, + ) -> Result<(u64, u64), i32> { + let buffer = session + .buffers + .iter_mut() + .find(|b| b.offset == offset) + .ok_or(libc::EINVAL)?; + let rw = (flags & VIRTIO_MEDIA_MMAP_FLAG_RW) != 0; + let fd = buffer.fd.as_file().as_fd(); + let (guest_addr, size) = self + .mmap_manager + .create_mapping(offset, fd, rw) + .map_err(|_| libc::EINVAL)?; + + // TODO: would be nice to enable this, but how do we find the buffer again during munmap? + // + // Maybe keep a guest_addr -> session map in the device... + // buffer.v4l2_buffer.set_flags(buffer.v4l2_buffer.flags() | BufferFlags::MAPPED); + + Ok((guest_addr, size)) + } + + fn do_munmap(&mut self, guest_addr: u64) -> Result<(), i32> { + self.mmap_manager + .remove_mapping(guest_addr) + .map(|_| ()) + .map_err(|_| libc::EINVAL) + } +} + +const PIXELFORMAT: u32 = PixelFormat::from_fourcc(b"RGB3").to_u32(); +const WIDTH: u32 = 640; +const HEIGHT: u32 = 480; +const BYTES_PER_LINE: u32 = WIDTH * 3; +const BUFFER_SIZE: u32 = BYTES_PER_LINE * HEIGHT; + +const INPUTS: [bindings::v4l2_input; 1] = [bindings::v4l2_input { + index: 0, + name: *b"Default\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", + type_: bindings::V4L2_INPUT_TYPE_CAMERA, + ..unsafe { std::mem::zeroed() } +}]; + +fn default_fmtdesc(queue: QueueType) -> v4l2_fmtdesc { + v4l2_fmtdesc { + index: 0, + type_: queue as u32, + pixelformat: PIXELFORMAT, + ..Default::default() + } +} + +fn default_fmt(queue: QueueType) -> v4l2_format { + let pix = v4l2_pix_format { + width: WIDTH, + height: HEIGHT, + pixelformat: PIXELFORMAT, + field: bindings::v4l2_field_V4L2_FIELD_NONE, + bytesperline: BYTES_PER_LINE, + sizeimage: BUFFER_SIZE, + colorspace: bindings::v4l2_colorspace_V4L2_COLORSPACE_SRGB, + ..Default::default() + }; + + v4l2_format { + type_: queue as u32, + fmt: bindings::v4l2_format__bindgen_ty_1 { pix }, + } +} + +/// Implementations of the ioctls required by a CAPTURE device. +impl VirtioMediaIoctlHandler for SimpleCaptureDevice +where + Q: VirtioMediaEventQueue, + HM: VirtioMediaHostMemoryMapper, +{ + type Session = SimpleCaptureDeviceSession; + + fn enum_fmt( + &mut self, + _session: &Self::Session, + queue: QueueType, + index: u32, + ) -> IoctlResult { + if queue != QueueType::VideoCapture { + return Err(libc::EINVAL); + } + if index > 0 { + return Err(libc::EINVAL); + } + + Ok(default_fmtdesc(queue)) + } + + fn g_fmt(&mut self, _session: &Self::Session, queue: QueueType) -> IoctlResult { + if queue != QueueType::VideoCapture { + return Err(libc::EINVAL); + } + + Ok(default_fmt(queue)) + } + + fn s_fmt( + &mut self, + _session: &mut Self::Session, + queue: QueueType, + _format: v4l2_format, + ) -> IoctlResult { + if queue != QueueType::VideoCapture { + return Err(libc::EINVAL); + } + + Ok(default_fmt(queue)) + } + + fn try_fmt( + &mut self, + _session: &Self::Session, + queue: QueueType, + _format: v4l2_format, + ) -> IoctlResult { + if queue != QueueType::VideoCapture { + return Err(libc::EINVAL); + } + + Ok(default_fmt(queue)) + } + + fn reqbufs( + &mut self, + session: &mut Self::Session, + queue: QueueType, + memory: MemoryType, + count: u32, + ) -> IoctlResult { + if queue != QueueType::VideoCapture { + return Err(libc::EINVAL); + } + if memory != MemoryType::Mmap { + return Err(libc::EINVAL); + } + if session.streaming { + return Err(libc::EBUSY); + } + // Buffers cannot be requested on a session if there is already another session with + // allocated buffers. + match self.active_session { + Some(id) if id != session.id => return Err(libc::EBUSY), + _ => (), + } + + // Reqbufs(0) is an implicit streamoff. + if count == 0 { + self.active_session = None; + self.streamoff(session, queue)?; + } else { + // TODO factorize with streamoff. + session.queued_buffers.clear(); + for buffer in session.buffers.iter_mut() { + buffer.set_state(BufferState::New); + } + self.active_session = Some(session.id); + } + + let count = std::cmp::min(count, 32); + + for buffer in &session.buffers { + self.mmap_manager.unregister_buffer(buffer.offset); + } + + session.buffers = (0..count) + .map(|i| { + MemFdBuffer::new(BUFFER_SIZE as u64) + .map_err(|e| { + log::error!("failed to allocate MMAP buffers: {:#}", e); + libc::ENOMEM + }) + .and_then(|fd| { + let offset = self + .mmap_manager + .register_buffer(None, BUFFER_SIZE) + .map_err(|_| libc::EINVAL)?; + + let mut v4l2_buffer = + V4l2Buffer::new(QueueType::VideoCapture, i, MemoryType::Mmap); + if let V4l2PlanesWithBackingMut::Mmap(mut planes) = + v4l2_buffer.planes_with_backing_iter_mut() + { + // SAFETY: every buffer has at least one plane. + let mut plane = planes.next().unwrap(); + plane.set_mem_offset(offset); + *plane.length = BUFFER_SIZE; + } else { + // SAFETY: we have just set the buffer type to MMAP. Reaching this point means a bug in + // the code. + panic!() + } + v4l2_buffer.set_field(BufferField::None); + v4l2_buffer.set_flags(BufferFlags::TIMESTAMP_MONOTONIC); + + Ok(Buffer { + state: BufferState::New, + v4l2_buffer, + fd, + offset, + }) + }) + }) + .collect::>()?; + + Ok(v4l2_requestbuffers { + count, + type_: queue as u32, + memory: memory as u32, + capabilities: (BufferCapabilities::SUPPORTS_MMAP + | BufferCapabilities::SUPPORTS_ORPHANED_BUFS) + .bits(), + ..Default::default() + }) + } + + fn querybuf( + &mut self, + session: &Self::Session, + queue: QueueType, + index: u32, + ) -> IoctlResult { + if queue != QueueType::VideoCapture { + return Err(libc::EINVAL); + } + let buffer = session.buffers.get(index as usize).ok_or(libc::EINVAL)?; + + Ok(buffer.v4l2_buffer.clone()) + } + + fn qbuf( + &mut self, + session: &mut Self::Session, + buffer: v4l2r::ioctl::V4l2Buffer, + _guest_regions: Vec>, + ) -> IoctlResult { + let host_buffer = session + .buffers + .get_mut(buffer.index() as usize) + .ok_or(libc::EINVAL)?; + // Attempt to queue already queued buffer. + if matches!(host_buffer.state, BufferState::Incoming) { + return Err(libc::EINVAL); + } + + host_buffer.set_state(BufferState::Incoming); + session.queued_buffers.push_back(buffer.index() as usize); + + let buffer = host_buffer.v4l2_buffer.clone(); + + if session.streaming { + session.process_queued_buffers(&mut self.evt_queue)?; + } + + Ok(buffer) + } + + fn streamon(&mut self, session: &mut Self::Session, queue: QueueType) -> IoctlResult<()> { + if queue != QueueType::VideoCapture || session.buffers.is_empty() { + return Err(libc::EINVAL); + } + session.streaming = true; + + session.process_queued_buffers(&mut self.evt_queue)?; + + Ok(()) + } + + fn streamoff(&mut self, session: &mut Self::Session, queue: QueueType) -> IoctlResult<()> { + if queue != QueueType::VideoCapture { + return Err(libc::EINVAL); + } + session.streaming = false; + session.queued_buffers.clear(); + for buffer in session.buffers.iter_mut() { + buffer.set_state(BufferState::New); + } + + Ok(()) + } + + fn g_input(&mut self, _session: &Self::Session) -> IoctlResult { + Ok(0) + } + + fn s_input(&mut self, _session: &mut Self::Session, input: i32) -> IoctlResult { + if input != 0 { Err(libc::EINVAL) } else { Ok(0) } + } + + fn enuminput( + &mut self, + _session: &Self::Session, + index: u32, + ) -> IoctlResult { + match INPUTS.get(index as usize) { + Some(&input) => Ok(input), + None => Err(libc::EINVAL), + } + } +} diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/Cargo.toml b/base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/Cargo.toml new file mode 100644 index 00000000000..9fa53c47414 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "vhu_media" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { workspace = true } +env_logger = { workspace = true } +libc = { workspace = true } +log = { workspace = true } +thiserror = { workspace = true } +vhost = { workspace = true } +vhost-user-backend = { workspace = true } +virtio-bindings = { workspace = true } +virtio-media = { workspace = true } +virtio-queue = { workspace = true } +vm-allocator = { workspace = true } +vm-memory = { workspace = true } +vmm-sys-util = { workspace = true } +uuid = { workspace = true } +zerocopy = { workspace = true } +bitflags = { workspace = true } diff --git a/base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/src/lib.rs b/base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/src/lib.rs new file mode 100644 index 00000000000..5b5a4b077a3 --- /dev/null +++ b/base/cvd/cuttlefish/host/commands/vhost_user_media/vhu_media/src/lib.rs @@ -0,0 +1,565 @@ +use std::collections::HashMap; +use std::convert; +use std::io; +use std::os::fd::BorrowedFd; +use std::os::unix::io::AsRawFd; + +use libc::_SC_PAGESIZE; +use libc::sysconf; +use log::error; +use log::info; +use thiserror::Error as ThisError; +use vhost::vhost_user::Backend; +use vhost::vhost_user::VhostUserFrontendReqHandler; +use vhost::vhost_user::message::VhostUserMMap; +use vhost::vhost_user::message::VhostUserMMapFlags; +use vhost::vhost_user::message::VhostUserProtocolFeatures; +use vhost::vhost_user::message::VhostUserVirtioFeatures; +use vhost_user_backend::{VhostUserBackendMut, VringRwLock, VringT}; +use virtio_bindings::bindings::virtio_config::VIRTIO_F_NOTIFY_ON_EMPTY; +use virtio_bindings::bindings::virtio_config::VIRTIO_F_VERSION_1; +use virtio_bindings::bindings::virtio_ring::VIRTIO_RING_F_EVENT_IDX; +use virtio_media::VirtioMediaDevice; +use virtio_media::VirtioMediaDeviceSession; +use virtio_media::VirtioMediaEventQueue; +use virtio_media::VirtioMediaHostMemoryMapper; +use virtio_media::io::ReadFromDescriptorChain; +use virtio_media::io::WriteToDescriptorChain; +use virtio_media::protocol::CloseCmd; +use virtio_media::protocol::CmdHeader; +use virtio_media::protocol::IoctlCmd; +use virtio_media::protocol::MmapCmd; +use virtio_media::protocol::MmapResp; +use virtio_media::protocol::MunmapCmd; +use virtio_media::protocol::MunmapResp; +use virtio_media::protocol::OpenResp; +use virtio_media::protocol::V4l2Event; +use virtio_media::protocol::VirtioMediaDeviceConfig; +use virtio_media::protocol::{self, V4l2Ioctl}; +use virtio_queue::DescriptorChain; +use virtio_queue::QueueOwnedT; +use virtio_queue::Reader; +use virtio_queue::Writer; +use vm_allocator::AddressAllocator; +use vm_allocator::RangeInclusive; +use vm_memory::GuestAddressSpace; +use vm_memory::GuestMemoryAtomic; +use vm_memory::GuestMemoryLoadGuard; +use vm_memory::GuestMemoryMmap; +use vmm_sys_util::epoll::EventSet; +use vmm_sys_util::event::{EventConsumer, EventNotifier}; +use vmm_sys_util::event::{EventFlag, new_event_consumer_and_notifier}; + +#[derive(Debug, ThisError)] +/// Errors related to vhost-user-media daemon. +pub(crate) enum VhuMediaBackendError { + #[error("Failed to handle event, didn't match EPOLLIN")] + HandleEventNotEpollIn, + #[error("Failed to handle unknown event")] + HandleEventUnknown, + #[error("Descriptor chain error")] + DescriptorChainError(virtio_queue::Error), + #[error("I/O error")] + Io(#[from] std::io::Error), + #[error("Virtio media device not created")] + VirtioMediaDeviceNotCreated, + #[error("Descriptor is unavailable")] + DescriptorUnavailable, +} + +impl convert::From for io::Error { + fn from(e: VhuMediaBackendError) -> Self { + io::Error::other(e) + } +} + +type VhuMediaDescriptorChain = DescriptorChain>>; + +pub struct HostMemoryMapper { + backend: Backend, + allocator: AddressAllocator, + allocated_ranges_map: HashMap, +} + +impl HostMemoryMapper { + fn new(backend: Backend, allocator: AddressAllocator) -> HostMemoryMapper { + HostMemoryMapper { + backend: backend, + allocator: allocator, + allocated_ranges_map: HashMap::new(), + } + } +} + +impl VirtioMediaHostMemoryMapper for HostMemoryMapper { + fn add_mapping( + &mut self, + buffer: BorrowedFd, + length: u64, + _offset: u64, + rw: bool, + ) -> Result { + let range = self + .allocator + .allocate( + length, + pagesize() as u64, + vm_allocator::AllocPolicy::FirstMatch, + ) + .map_err(|e| { + error!("allocate error: {}", e); + libc::ENOMEM + })?; + self.allocated_ranges_map.insert(range.start(), range.end()); + let mut flags = VhostUserMMapFlags::MAP_R; + if rw { + flags |= VhostUserMMapFlags::MAP_W; + } + let msg = VhostUserMMap { + shmid: 0, + padding: [0, 0, 0, 0, 0, 0, 0], + fd_offset: 0, + shm_offset: range.start(), + len: length, + flags: flags.bits(), + }; + + match self.backend.shmem_map(&msg, &buffer.as_raw_fd()) { + Ok(_) => Ok(range.start()), + Err(e) => { + error!("memory map file request error: {}", e); + Err(libc::EINVAL) + } + } + } + + fn remove_mapping(&mut self, shm_offset: u64) -> Result<(), i32> { + let end = match self.allocated_ranges_map.remove(&shm_offset) { + Some(v) => v, + None => { + error!("alloc not found, start: {}", shm_offset); + return Err(libc::EINVAL); + } + }; + let range = RangeInclusive::new(shm_offset, end).map_err(|e| { + error!("invalid range: {}", e); + libc::EINVAL + })?; + self.allocator.free(&range).map_err(|e| { + error!("free error: {}", e); + libc::EINVAL + })?; + let msg = VhostUserMMap { + shmid: 0, + padding: [0, 0, 0, 0, 0, 0, 0], + fd_offset: 0, + shm_offset: shm_offset, + len: 1, + flags: 0, + }; + + match self.backend.shmem_unmap(&msg) { + Ok(_) => Ok(()), + Err(e) => { + error!("memory unmap file request error: {}", e); + Err(libc::EINVAL) + } + } + } +} + +pub struct EventQueue { + mem: GuestMemoryLoadGuard, + vring: VringRwLock, +} + +impl EventQueue { + fn send_events(&mut self, event: V4l2Event) -> Result<(), VhuMediaBackendError> { + let vring = self.vring.clone(); + let mem = self.mem.clone(); + let requests: Vec<_> = vring + .get_mut() + .get_queue_mut() + .iter(mem) + .map_err(|e| VhuMediaBackendError::DescriptorChainError(e))? + .collect(); + if requests.is_empty() { + return Err(VhuMediaBackendError::DescriptorUnavailable); + } + for desc_chain in requests.clone() { + let mem = self.mem.clone(); + let head_index = desc_chain.head_index(); + let mut writer = desc_chain + .writer(&mem) + .map_err(|e| VhuMediaBackendError::DescriptorChainError(e))?; + if let Err(e) = match event { + V4l2Event::DequeueBuffer(e) => WriteToDescriptorChain::write_obj(&mut writer, e), + V4l2Event::Error(e) => WriteToDescriptorChain::write_obj(&mut writer, e), + V4l2Event::Event(e) => WriteToDescriptorChain::write_obj(&mut writer, e), + } { + return Err(VhuMediaBackendError::Io(e)); + } + vring + .get_mut() + .add_used(head_index, writer.bytes_written() as u32) + .map_err(|e| VhuMediaBackendError::DescriptorChainError(e))?; + vring + .signal_used_queue() + .map_err(|e| VhuMediaBackendError::Io(e))?; + break; + } + Ok(()) + } +} + +impl VirtioMediaEventQueue for EventQueue { + fn send_event(&mut self, event: V4l2Event) { + if let Err(e) = self.send_events(event) { + error!("send event failed with error: {}", e); + } + } +} + +pub struct VhuMediaBackend< + S: VirtioMediaDeviceSession, + D: for<'a> VirtioMediaDevice, Writer<'a>>, + F: Fn(EventQueue, HostMemoryMapper) -> D, +> { + backend: Option, + config: VirtioMediaDeviceConfig, + event_idx: bool, + exit_event_fds: Vec<(EventConsumer, EventNotifier)>, + mem: Option>, + sessions: HashMap, + session_id_counter: u32, + device: Option, + create_device_fn: F, +} + +impl VhuMediaBackend +where + S: VirtioMediaDeviceSession + Send + Sync, + D: for<'a> VirtioMediaDevice, Writer<'a>, Session = S> + Send + Sync, + F: Fn(EventQueue, HostMemoryMapper) -> D + Send + Sync, +{ + pub fn new(config: VirtioMediaDeviceConfig, create_device_fn: F) -> Self { + let mut backend = VhuMediaBackend { + backend: None, + config, + event_idx: false, + exit_event_fds: vec![], + mem: None, + sessions: Default::default(), + session_id_counter: 0, + device: None, + create_device_fn: create_device_fn, + }; + // Create a event_fd for each thread. We make it NONBLOCKing in + // order to allow tests maximum flexibility in checking whether + // signals arrived or not. + backend.exit_event_fds = (0..backend.queues_per_thread().len()) + .map(|_| { + new_event_consumer_and_notifier(EventFlag::NONBLOCK) + .expect("Failed to new EventNotifier and EventConsumer") + }) + .collect(); + + backend + } + + fn process_commandq_requests( + &mut self, + requests: Vec, + vring: &VringRwLock, + ) -> Result<(), VhuMediaBackendError> { + if requests.is_empty() { + info!("no pending requests"); + return Ok(()); + } + let device = self + .device + .as_mut() + .ok_or(VhuMediaBackendError::VirtioMediaDeviceNotCreated)?; + let mem = self.mem.as_ref().unwrap().clone(); + for desc_chain in requests.clone() { + let head_index = desc_chain.head_index(); + let mut reader = desc_chain + .clone() + .reader(&mem) + .map_err(|e| VhuMediaBackendError::DescriptorChainError(e))?; + let mut writer = desc_chain + .writer(&mem) + .map_err(|e| VhuMediaBackendError::DescriptorChainError(e))?; + let hdr = ReadFromDescriptorChain::read_obj::(&mut reader) + .map_err(|e| VhuMediaBackendError::Io(e))?; + match hdr.cmd { + protocol::VIRTIO_MEDIA_CMD_OPEN => { + let session_id = self.session_id_counter; + match device.new_session(session_id) { + Ok(session) => { + self.sessions.insert(session_id, session); + self.session_id_counter += 1; + writer + .write_response(OpenResp::ok(session_id)) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + Err(e) => { + error!("device new session error: {}", e); + writer + .write_err_response(e) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + } + } + protocol::VIRTIO_MEDIA_CMD_CLOSE => { + let cmd = ReadFromDescriptorChain::read_obj::(&mut reader) + .map_err(|e| VhuMediaBackendError::Io(e))?; + match self.sessions.remove(&cmd.session_id) { + Some(session) => device.close_session(session), + None => { + error!("session id not found: {}", cmd.session_id); + writer + .write_err_response(libc::EINVAL) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + } + } + protocol::VIRTIO_MEDIA_CMD_IOCTL => { + let cmd = ReadFromDescriptorChain::read_obj::(&mut reader) + .map_err(|e| VhuMediaBackendError::Io(e))?; + match self.sessions.get_mut(&cmd.session_id) { + Some(session) => match V4l2Ioctl::n(cmd.code) { + Some(ioctl) => { + device + .do_ioctl(session, ioctl, &mut reader, &mut writer) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + None => { + error!("unknown ioctl code {}", cmd.code); + writer + .write_err_response(libc::ENOTTY) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + }, + None => { + error!("session id not found: {}", cmd.session_id); + writer + .write_err_response(libc::EINVAL) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + } + } + protocol::VIRTIO_MEDIA_CMD_MMAP => { + let cmd = ReadFromDescriptorChain::read_obj::(&mut reader) + .map_err(|e| VhuMediaBackendError::Io(e))?; + match self.sessions.get_mut(&cmd.session_id) { + Some(session) => match device.do_mmap(session, cmd.flags, cmd.offset) { + Ok((guest_addr, size)) => { + writer + .write_response(MmapResp::ok(guest_addr, size)) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + Err(e) => { + error!("device mmap error: {}", e); + writer + .write_err_response(e) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + }, + None => { + error!("session id not found: {}", cmd.session_id); + writer + .write_err_response(libc::EINVAL) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + } + } + protocol::VIRTIO_MEDIA_CMD_MUNMAP => { + let cmd = ReadFromDescriptorChain::read_obj::(&mut reader) + .map_err(|e| VhuMediaBackendError::Io(e))?; + match device.do_munmap(cmd.driver_addr) { + Ok(()) => { + writer + .write_response(MunmapResp::ok()) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + Err(e) => { + error!("device munmap error: {}", e); + writer + .write_err_response(libc::EINVAL) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + } + } + unknown_cmd => { + error!("unknown virtio media command: {}", unknown_cmd); + writer + .write_err_response(libc::ENOTTY) + .map_err(|e| VhuMediaBackendError::Io(e))?; + } + } + vring + .get_mut() + .add_used(head_index, writer.bytes_written() as u32) + .map_err(|e| VhuMediaBackendError::DescriptorChainError(e))?; + } + + Ok(()) + } + + fn process_commandq_queue(&mut self, vring: &VringRwLock) -> Result<(), VhuMediaBackendError> { + let requests: Vec<_> = vring + .get_mut() + .get_queue_mut() + .iter(self.mem.as_ref().unwrap().clone()) + .map_err(|e| VhuMediaBackendError::DescriptorChainError(e))? + .collect(); + return match self.process_commandq_requests(requests, vring) { + Ok(()) => { + vring + .signal_used_queue() + .map_err(|e| VhuMediaBackendError::Io(e))?; + Ok(()) + } + Err(e) => Err(e), + }; + } +} + +const NUM_QUEUES: usize = 2; +// Use 32768 to avoid Err(InvalidParam) in vhost-user-backend/src/handler.rs:set_vring_num +const QUEUE_SIZE: usize = 32768; + +const COMMANDQ: u16 = 0; + +const EVENTQ: u16 = 1; + +impl VhostUserBackendMut for VhuMediaBackend +where + S: VirtioMediaDeviceSession + Send + Sync, + D: for<'a> VirtioMediaDevice, Writer<'a>, Session = S> + Sync + Send, + F: Fn(EventQueue, HostMemoryMapper) -> D + Sync + Send, +{ + type Vring = VringRwLock; + type Bitmap = (); + + fn num_queues(&self) -> usize { + NUM_QUEUES + } + + fn max_queue_size(&self) -> usize { + QUEUE_SIZE + } + + fn features(&self) -> u64 { + (1 << VIRTIO_F_VERSION_1) + | (1 << VIRTIO_F_NOTIFY_ON_EMPTY) + | (1 << VIRTIO_RING_F_EVENT_IDX) + | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits() + } + + fn protocol_features(&self) -> VhostUserProtocolFeatures { + // Custom protocol feature supported by crosvm frontend. + const SHARED_MEMORY_REGIONS: u64 = 0x0010_0000; + let shared_memory_regions = + unsafe { std::mem::transmute::(SHARED_MEMORY_REGIONS) }; + VhostUserProtocolFeatures::MQ + | VhostUserProtocolFeatures::CONFIG + | VhostUserProtocolFeatures::BACKEND_REQ + | shared_memory_regions + } + + fn set_event_idx(&mut self, enabled: bool) { + self.event_idx = enabled; + } + + fn update_memory(&mut self, mem: GuestMemoryAtomic) -> io::Result<()> { + self.mem = Some(mem.memory()); + Ok(()) + } + + fn set_backend_req_fd(&mut self, backend: Backend) { + self.backend = Some(backend); + } + + fn handle_event( + &mut self, + device_event: u16, + evset: EventSet, + vrings: &[VringRwLock], + _thread_id: usize, + ) -> io::Result<()> { + if evset != EventSet::IN { + return Err(VhuMediaBackendError::HandleEventNotEpollIn.into()); + } + match device_event { + COMMANDQ => { + let vring = &vrings[COMMANDQ as usize]; + if self.event_idx { + // vm-virtio's Queue implementation only checks avail_index + // once, so to properly support EVENT_IDX we need to keep + // calling process_queue() until it stops finding new + // requests on the queue. + loop { + vring.disable_notification().unwrap(); + self.process_commandq_queue(vring)?; + if !vring.enable_notification().unwrap() { + break; + } + } + } else { + self.process_commandq_queue(vring)?; + } + } + EVENTQ => { + let vring = &vrings[EVENTQ as usize]; + let event_queue = EventQueue { + vring: vring.clone(), + mem: self.mem.as_ref().unwrap().clone(), + }; + const HOST_MAPPER_RANGE: u64 = 1 << 32; + let allocator = + vm_allocator::AddressAllocator::new(0, HOST_MAPPER_RANGE - 1).unwrap(); + let host_mapper = + HostMemoryMapper::new(self.backend.as_ref().unwrap().clone(), allocator); + self.device = Some((self.create_device_fn)(event_queue, host_mapper)); + } + _ => { + return Err(VhuMediaBackendError::HandleEventUnknown.into()); + } + } + Ok(()) + } + + fn exit_event(&self, thread_index: usize) -> Option<(EventConsumer, EventNotifier)> { + self.exit_event_fds.get(thread_index).map(|(s, r)| { + ( + s.try_clone().expect("Failed to clone EventConsumer"), + r.try_clone().expect("Failed to clone EventNotifier"), + ) + }) + } + + fn queues_per_thread(&self) -> Vec { + return vec![3]; + } + + fn get_config(&self, offset: u32, size: u32) -> Vec { + let offset = offset as usize; + let size = size as usize; + let buf = self.config.as_ref(); + if offset + size > buf.len() { + return Vec::new(); + } + + buf[offset..offset + size].to_vec() + } +} + +/// Safe wrapper for `sysconf(_SC_PAGESIZE)`. +#[inline(always)] +fn pagesize() -> usize { + // SAFETY: + // Trivially safe + unsafe { sysconf(_SC_PAGESIZE) as usize } +}