| //! Driver for VirtIO GPU devices. |
| |
| use crate::hal::{BufferDirection, Dma, Hal}; |
| use crate::queue::VirtQueue; |
| use crate::transport::Transport; |
| use crate::volatile::{volread, ReadOnly, Volatile, WriteOnly}; |
| use crate::{pages, Error, Result}; |
| use bitflags::bitflags; |
| use log::info; |
| use zerocopy::{AsBytes, FromBytes}; |
| |
| const QUEUE_SIZE: u16 = 2; |
| |
| /// A virtio based graphics adapter. |
| /// |
| /// It can operate in 2D mode and in 3D (virgl) mode. |
| /// 3D mode will offload rendering ops to the host gpu and therefore requires |
| /// a gpu with 3D support on the host machine. |
| /// In 2D mode the virtio-gpu device provides support for ARGB Hardware cursors |
| /// and multiple scanouts (aka heads). |
| pub struct VirtIOGpu<'a, H: Hal, T: Transport> { |
| transport: T, |
| rect: Option<Rect>, |
| /// DMA area of frame buffer. |
| frame_buffer_dma: Option<Dma<H>>, |
| /// DMA area of cursor image buffer. |
| cursor_buffer_dma: Option<Dma<H>>, |
| /// Queue for sending control commands. |
| control_queue: VirtQueue<H, { QUEUE_SIZE as usize }>, |
| /// Queue for sending cursor commands. |
| cursor_queue: VirtQueue<H, { QUEUE_SIZE as usize }>, |
| /// DMA region for sending data to the device. |
| dma_send: Dma<H>, |
| /// DMA region for receiving data from the device. |
| dma_recv: Dma<H>, |
| /// Send buffer for queue. |
| queue_buf_send: &'a mut [u8], |
| /// Recv buffer for queue. |
| queue_buf_recv: &'a mut [u8], |
| } |
| |
| impl<H: Hal, T: Transport> VirtIOGpu<'_, H, T> { |
| /// Create a new VirtIO-Gpu driver. |
| pub fn new(mut transport: T) -> Result<Self> { |
| transport.begin_init(|features| { |
| let features = Features::from_bits_truncate(features); |
| info!("Device features {:?}", features); |
| let supported_features = Features::empty(); |
| (features & supported_features).bits() |
| }); |
| |
| // read configuration space |
| let config_space = transport.config_space::<Config>()?; |
| unsafe { |
| let events_read = volread!(config_space, events_read); |
| let num_scanouts = volread!(config_space, num_scanouts); |
| info!( |
| "events_read: {:#x}, num_scanouts: {:#x}", |
| events_read, num_scanouts |
| ); |
| } |
| |
| let control_queue = VirtQueue::new(&mut transport, QUEUE_TRANSMIT)?; |
| let cursor_queue = VirtQueue::new(&mut transport, QUEUE_CURSOR)?; |
| |
| let dma_send = Dma::new(1, BufferDirection::DriverToDevice)?; |
| let dma_recv = Dma::new(1, BufferDirection::DeviceToDriver)?; |
| let queue_buf_send = unsafe { dma_send.raw_slice().as_mut() }; |
| let queue_buf_recv = unsafe { dma_recv.raw_slice().as_mut() }; |
| |
| transport.finish_init(); |
| |
| Ok(VirtIOGpu { |
| transport, |
| frame_buffer_dma: None, |
| cursor_buffer_dma: None, |
| rect: None, |
| control_queue, |
| cursor_queue, |
| dma_send, |
| dma_recv, |
| queue_buf_send, |
| queue_buf_recv, |
| }) |
| } |
| |
| /// Acknowledge interrupt. |
| pub fn ack_interrupt(&mut self) -> bool { |
| self.transport.ack_interrupt() |
| } |
| |
| /// Get the resolution (width, height). |
| pub fn resolution(&mut self) -> Result<(u32, u32)> { |
| let display_info = self.get_display_info()?; |
| Ok((display_info.rect.width, display_info.rect.height)) |
| } |
| |
| /// Setup framebuffer |
| pub fn setup_framebuffer(&mut self) -> Result<&mut [u8]> { |
| // get display info |
| let display_info = self.get_display_info()?; |
| info!("=> {:?}", display_info); |
| self.rect = Some(display_info.rect); |
| |
| // create resource 2d |
| self.resource_create_2d( |
| RESOURCE_ID_FB, |
| display_info.rect.width, |
| display_info.rect.height, |
| )?; |
| |
| // alloc continuous pages for the frame buffer |
| let size = display_info.rect.width * display_info.rect.height * 4; |
| let frame_buffer_dma = Dma::new(pages(size as usize), BufferDirection::DriverToDevice)?; |
| |
| // resource_attach_backing |
| self.resource_attach_backing(RESOURCE_ID_FB, frame_buffer_dma.paddr() as u64, size)?; |
| |
| // map frame buffer to screen |
| self.set_scanout(display_info.rect, SCANOUT_ID, RESOURCE_ID_FB)?; |
| |
| let buf = unsafe { frame_buffer_dma.raw_slice().as_mut() }; |
| self.frame_buffer_dma = Some(frame_buffer_dma); |
| Ok(buf) |
| } |
| |
| /// Flush framebuffer to screen. |
| pub fn flush(&mut self) -> Result { |
| let rect = self.rect.ok_or(Error::NotReady)?; |
| // copy data from guest to host |
| self.transfer_to_host_2d(rect, 0, RESOURCE_ID_FB)?; |
| // flush data to screen |
| self.resource_flush(rect, RESOURCE_ID_FB)?; |
| Ok(()) |
| } |
| |
| /// Set the pointer shape and position. |
| pub fn setup_cursor( |
| &mut self, |
| cursor_image: &[u8], |
| pos_x: u32, |
| pos_y: u32, |
| hot_x: u32, |
| hot_y: u32, |
| ) -> Result { |
| let size = CURSOR_RECT.width * CURSOR_RECT.height * 4; |
| if cursor_image.len() != size as usize { |
| return Err(Error::InvalidParam); |
| } |
| let cursor_buffer_dma = Dma::new(pages(size as usize), BufferDirection::DriverToDevice)?; |
| let buf = unsafe { cursor_buffer_dma.raw_slice().as_mut() }; |
| buf.copy_from_slice(cursor_image); |
| |
| self.resource_create_2d(RESOURCE_ID_CURSOR, CURSOR_RECT.width, CURSOR_RECT.height)?; |
| self.resource_attach_backing(RESOURCE_ID_CURSOR, cursor_buffer_dma.paddr() as u64, size)?; |
| self.transfer_to_host_2d(CURSOR_RECT, 0, RESOURCE_ID_CURSOR)?; |
| self.update_cursor( |
| RESOURCE_ID_CURSOR, |
| SCANOUT_ID, |
| pos_x, |
| pos_y, |
| hot_x, |
| hot_y, |
| false, |
| )?; |
| self.cursor_buffer_dma = Some(cursor_buffer_dma); |
| Ok(()) |
| } |
| |
| /// Move the pointer without updating the shape. |
| pub fn move_cursor(&mut self, pos_x: u32, pos_y: u32) -> Result { |
| self.update_cursor(RESOURCE_ID_CURSOR, SCANOUT_ID, pos_x, pos_y, 0, 0, true)?; |
| Ok(()) |
| } |
| |
| /// Send a request to the device and block for a response. |
| fn request<Req: AsBytes, Rsp: FromBytes>(&mut self, req: Req) -> Result<Rsp> { |
| req.write_to_prefix(&mut *self.queue_buf_send).unwrap(); |
| self.control_queue.add_notify_wait_pop( |
| &[self.queue_buf_send], |
| &mut [self.queue_buf_recv], |
| &mut self.transport, |
| )?; |
| Ok(Rsp::read_from_prefix(&*self.queue_buf_recv).unwrap()) |
| } |
| |
| /// Send a mouse cursor operation request to the device and block for a response. |
| fn cursor_request<Req: AsBytes>(&mut self, req: Req) -> Result { |
| req.write_to_prefix(&mut *self.queue_buf_send).unwrap(); |
| self.cursor_queue.add_notify_wait_pop( |
| &[self.queue_buf_send], |
| &mut [], |
| &mut self.transport, |
| )?; |
| Ok(()) |
| } |
| |
| fn get_display_info(&mut self) -> Result<RespDisplayInfo> { |
| let info: RespDisplayInfo = |
| self.request(CtrlHeader::with_type(Command::GET_DISPLAY_INFO))?; |
| info.header.check_type(Command::OK_DISPLAY_INFO)?; |
| Ok(info) |
| } |
| |
| fn resource_create_2d(&mut self, resource_id: u32, width: u32, height: u32) -> Result { |
| let rsp: CtrlHeader = self.request(ResourceCreate2D { |
| header: CtrlHeader::with_type(Command::RESOURCE_CREATE_2D), |
| resource_id, |
| format: Format::B8G8R8A8UNORM, |
| width, |
| height, |
| })?; |
| rsp.check_type(Command::OK_NODATA) |
| } |
| |
| fn set_scanout(&mut self, rect: Rect, scanout_id: u32, resource_id: u32) -> Result { |
| let rsp: CtrlHeader = self.request(SetScanout { |
| header: CtrlHeader::with_type(Command::SET_SCANOUT), |
| rect, |
| scanout_id, |
| resource_id, |
| })?; |
| rsp.check_type(Command::OK_NODATA) |
| } |
| |
| fn resource_flush(&mut self, rect: Rect, resource_id: u32) -> Result { |
| let rsp: CtrlHeader = self.request(ResourceFlush { |
| header: CtrlHeader::with_type(Command::RESOURCE_FLUSH), |
| rect, |
| resource_id, |
| _padding: 0, |
| })?; |
| rsp.check_type(Command::OK_NODATA) |
| } |
| |
| fn transfer_to_host_2d(&mut self, rect: Rect, offset: u64, resource_id: u32) -> Result { |
| let rsp: CtrlHeader = self.request(TransferToHost2D { |
| header: CtrlHeader::with_type(Command::TRANSFER_TO_HOST_2D), |
| rect, |
| offset, |
| resource_id, |
| _padding: 0, |
| })?; |
| rsp.check_type(Command::OK_NODATA) |
| } |
| |
| fn resource_attach_backing(&mut self, resource_id: u32, paddr: u64, length: u32) -> Result { |
| let rsp: CtrlHeader = self.request(ResourceAttachBacking { |
| header: CtrlHeader::with_type(Command::RESOURCE_ATTACH_BACKING), |
| resource_id, |
| nr_entries: 1, |
| addr: paddr, |
| length, |
| _padding: 0, |
| })?; |
| rsp.check_type(Command::OK_NODATA) |
| } |
| |
| fn update_cursor( |
| &mut self, |
| resource_id: u32, |
| scanout_id: u32, |
| pos_x: u32, |
| pos_y: u32, |
| hot_x: u32, |
| hot_y: u32, |
| is_move: bool, |
| ) -> Result { |
| self.cursor_request(UpdateCursor { |
| header: if is_move { |
| CtrlHeader::with_type(Command::MOVE_CURSOR) |
| } else { |
| CtrlHeader::with_type(Command::UPDATE_CURSOR) |
| }, |
| pos: CursorPos { |
| scanout_id, |
| x: pos_x, |
| y: pos_y, |
| _padding: 0, |
| }, |
| resource_id, |
| hot_x, |
| hot_y, |
| _padding: 0, |
| }) |
| } |
| } |
| |
| impl<H: Hal, T: Transport> Drop for VirtIOGpu<'_, H, T> { |
| fn drop(&mut self) { |
| // Clear any pointers pointing to DMA regions, so the device doesn't try to access them |
| // after they have been freed. |
| self.transport.queue_unset(QUEUE_TRANSMIT); |
| self.transport.queue_unset(QUEUE_CURSOR); |
| } |
| } |
| |
| #[repr(C)] |
| struct Config { |
| /// Signals pending events to the driver。 |
| events_read: ReadOnly<u32>, |
| |
| /// Clears pending events in the device. |
| events_clear: WriteOnly<u32>, |
| |
| /// Specifies the maximum number of scanouts supported by the device. |
| /// |
| /// Minimum value is 1, maximum value is 16. |
| num_scanouts: Volatile<u32>, |
| } |
| |
| /// Display configuration has changed. |
| const EVENT_DISPLAY: u32 = 1 << 0; |
| |
| bitflags! { |
| struct Features: u64 { |
| /// virgl 3D mode is supported. |
| const VIRGL = 1 << 0; |
| /// EDID is supported. |
| const EDID = 1 << 1; |
| |
| // device independent |
| const NOTIFY_ON_EMPTY = 1 << 24; // legacy |
| const ANY_LAYOUT = 1 << 27; // legacy |
| const RING_INDIRECT_DESC = 1 << 28; |
| const RING_EVENT_IDX = 1 << 29; |
| const UNUSED = 1 << 30; // legacy |
| const VERSION_1 = 1 << 32; // detect legacy |
| |
| // since virtio v1.1 |
| const ACCESS_PLATFORM = 1 << 33; |
| const RING_PACKED = 1 << 34; |
| const IN_ORDER = 1 << 35; |
| const ORDER_PLATFORM = 1 << 36; |
| const SR_IOV = 1 << 37; |
| const NOTIFICATION_DATA = 1 << 38; |
| } |
| } |
| |
| #[repr(transparent)] |
| #[derive(AsBytes, Clone, Copy, Debug, Eq, PartialEq, FromBytes)] |
| struct Command(u32); |
| |
| impl Command { |
| const GET_DISPLAY_INFO: Command = Command(0x100); |
| const RESOURCE_CREATE_2D: Command = Command(0x101); |
| const RESOURCE_UNREF: Command = Command(0x102); |
| const SET_SCANOUT: Command = Command(0x103); |
| const RESOURCE_FLUSH: Command = Command(0x104); |
| const TRANSFER_TO_HOST_2D: Command = Command(0x105); |
| const RESOURCE_ATTACH_BACKING: Command = Command(0x106); |
| const RESOURCE_DETACH_BACKING: Command = Command(0x107); |
| const GET_CAPSET_INFO: Command = Command(0x108); |
| const GET_CAPSET: Command = Command(0x109); |
| const GET_EDID: Command = Command(0x10a); |
| |
| const UPDATE_CURSOR: Command = Command(0x300); |
| const MOVE_CURSOR: Command = Command(0x301); |
| |
| const OK_NODATA: Command = Command(0x1100); |
| const OK_DISPLAY_INFO: Command = Command(0x1101); |
| const OK_CAPSET_INFO: Command = Command(0x1102); |
| const OK_CAPSET: Command = Command(0x1103); |
| const OK_EDID: Command = Command(0x1104); |
| |
| const ERR_UNSPEC: Command = Command(0x1200); |
| const ERR_OUT_OF_MEMORY: Command = Command(0x1201); |
| const ERR_INVALID_SCANOUT_ID: Command = Command(0x1202); |
| } |
| |
| const GPU_FLAG_FENCE: u32 = 1 << 0; |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug, Clone, Copy, FromBytes)] |
| struct CtrlHeader { |
| hdr_type: Command, |
| flags: u32, |
| fence_id: u64, |
| ctx_id: u32, |
| _padding: u32, |
| } |
| |
| impl CtrlHeader { |
| fn with_type(hdr_type: Command) -> CtrlHeader { |
| CtrlHeader { |
| hdr_type, |
| flags: 0, |
| fence_id: 0, |
| ctx_id: 0, |
| _padding: 0, |
| } |
| } |
| |
| /// Return error if the type is not same as expected. |
| fn check_type(&self, expected: Command) -> Result { |
| if self.hdr_type == expected { |
| Ok(()) |
| } else { |
| Err(Error::IoError) |
| } |
| } |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug, Copy, Clone, Default, FromBytes)] |
| struct Rect { |
| x: u32, |
| y: u32, |
| width: u32, |
| height: u32, |
| } |
| |
| #[repr(C)] |
| #[derive(Debug, FromBytes)] |
| struct RespDisplayInfo { |
| header: CtrlHeader, |
| rect: Rect, |
| enabled: u32, |
| flags: u32, |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug)] |
| struct ResourceCreate2D { |
| header: CtrlHeader, |
| resource_id: u32, |
| format: Format, |
| width: u32, |
| height: u32, |
| } |
| |
| #[repr(u32)] |
| #[derive(AsBytes, Debug)] |
| enum Format { |
| B8G8R8A8UNORM = 1, |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug)] |
| struct ResourceAttachBacking { |
| header: CtrlHeader, |
| resource_id: u32, |
| nr_entries: u32, // always 1 |
| addr: u64, |
| length: u32, |
| _padding: u32, |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug)] |
| struct SetScanout { |
| header: CtrlHeader, |
| rect: Rect, |
| scanout_id: u32, |
| resource_id: u32, |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug)] |
| struct TransferToHost2D { |
| header: CtrlHeader, |
| rect: Rect, |
| offset: u64, |
| resource_id: u32, |
| _padding: u32, |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug)] |
| struct ResourceFlush { |
| header: CtrlHeader, |
| rect: Rect, |
| resource_id: u32, |
| _padding: u32, |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug, Clone, Copy)] |
| struct CursorPos { |
| scanout_id: u32, |
| x: u32, |
| y: u32, |
| _padding: u32, |
| } |
| |
| #[repr(C)] |
| #[derive(AsBytes, Debug, Clone, Copy)] |
| struct UpdateCursor { |
| header: CtrlHeader, |
| pos: CursorPos, |
| resource_id: u32, |
| hot_x: u32, |
| hot_y: u32, |
| _padding: u32, |
| } |
| |
| const QUEUE_TRANSMIT: u16 = 0; |
| const QUEUE_CURSOR: u16 = 1; |
| |
| const SCANOUT_ID: u32 = 0; |
| const RESOURCE_ID_FB: u32 = 0xbabe; |
| const RESOURCE_ID_CURSOR: u32 = 0xdade; |
| |
| const CURSOR_RECT: Rect = Rect { |
| x: 0, |
| y: 0, |
| width: 64, |
| height: 64, |
| }; |