//! virtio-gpu, 2D only: a framebuffer the host shows on its screen. //! `-vga none -device virtio-vga` (virtio-gpu plus the old VGA modes, so //! the BIOS and our VGA text console keep working until we switch over). //! //! The GPU doesn't scan out of our memory directly. The protocol has //! *resources* (images living on the host) and *backing* (guest pages //! holding the pixels). Setting up a framebuffer, every step one command on //! the control queue: //! //! 1. GET_DISPLAY_INFO which outputs ("scanouts") exist, what size //! 2. RESOURCE_CREATE_2D a 1024x768 image, format B8G8R8X8, id 1 //! 3. RESOURCE_ATTACH_BACKING its pixels live in these guest pages //! 4. SET_SCANOUT show resource 1 on scanout 0 //! //! Then, whenever we've drawn something: //! //! 5. TRANSFER_TO_HOST_2D copy a rectangle from our pages to the host //! 6. RESOURCE_FLUSH and update the screen there //! //! Each command is a request buffer (a 24-byte header + arguments) and a //! response buffer the device writes (a header whose type says OK or which //! error). We wait for each answer by polling -- it's a function call into //! QEMU, microseconds. //! //! 3D (virgl) would add contexts, 3D resources and a command stream of //! Gallium/OpenGL commands that Mesa in the guest produces and //! virglrenderer on the host executes. That's a lot of protocol; 2D is the //! foundation it builds on (3D still ends in SET_SCANOUT + RESOURCE_FLUSH). use super::{Transport, Virtqueue}; use crate::memory; use crate::pci::PciDevice; use alloc::{format, string::String, vec::Vec}; use conquer_once::spin::OnceCell; use spin::Mutex; use x86_64::{PhysAddr, instructions::interrupts::without_interrupts}; /// The framebuffer size we ask for (the device's preferred size is only /// logged: fixed is predictable for tests and screenshots). pub const WIDTH: u32 = 1024; pub const HEIGHT: u32 = 768; // Command types (virtio spec 5.7.6.7) and response types. pub const CMD_GET_DISPLAY_INFO: u32 = 0x0100; pub const CMD_RESOURCE_CREATE_2D: u32 = 0x0101; pub const CMD_SET_SCANOUT: u32 = 0x0103; pub const CMD_RESOURCE_FLUSH: u32 = 0x0104; pub const CMD_TRANSFER_TO_HOST_2D: u32 = 0x0105; pub const CMD_RESOURCE_ATTACH_BACKING: u32 = 0x0106; pub const RESP_OK_NODATA: u32 = 0x1100; pub const RESP_OK_DISPLAY_INFO: u32 = 0x1101; pub const RESP_ERR_INVALID_RESOURCE_ID: u32 = 0x1203; /// Pixel format: bytes B, G, R, unused -- i.e. a little-endian u32 is /// 0x00RRGGBB, the usual way to write colours. const FORMAT_B8G8R8X8: u32 = 2; const RESOURCE_ID: u32 = 1; /// Where the pixels are. Pixel (x, y) is the u32 at `base + y * stride + x`. #[derive(Clone, Copy, Debug)] pub struct FbInfo { pub base: u64, // kernel virtual address pub phys: u64, pub width: u32, pub height: u32, pub stride: u32, // in pixels } impl FbInfo { pub fn pixels(&self) -> *mut u32 { self.base as *mut u32 } pub fn size_bytes(&self) -> usize { (self.stride * self.height * 4) as usize } } struct Gpu { transport: Transport, control: Virtqueue, cmd_phys: u64, // one page: requests at 0 and 512, responses at 2048 and 2560 } static GPU: Mutex> = Mutex::new(None); static FB: OnceCell = OnceCell::uninit(); /// The host's preferred display size, as reported by GET_DISPLAY_INFO. static PREFERRED: Mutex<(u32, u32)> = Mutex::new((0, 0)); /// virtio-gpu: modern-only, id 0x1040 + 16. (virtio-vga is the same id /// with the VGA display class.) pub fn is_virtio_gpu(d: &PciDevice) -> bool { d.vendor_id == super::VENDOR && d.device_id == 0x1050 } /// The framebuffer, once the GPU is set up. pub fn framebuffer() -> Option { FB.get().copied() } impl Gpu { /// Copy `words` into request slot `slot`, as a command with a zeroed /// header (flags, fence, context) after the type. fn put_request(&self, slot: u64, cmd: u32, args: &[u32]) -> u32 { let base: *mut u32 = memory::phys_to_virt(PhysAddr::new(self.cmd_phys + 512 * slot)).as_mut_ptr(); let header = [cmd, 0, 0, 0, 0, 0]; // type, flags, fence_id (u64), ctx_id, ring_idx+padding for (i, w) in header.iter().chain(args).enumerate() { unsafe { base.add(i).write_volatile(*w) }; } ((header.len() + args.len()) * 4) as u32 } fn response(&self, slot: u64) -> *const u32 { memory::phys_to_virt(PhysAddr::new(self.cmd_phys + 2048 + 512 * slot)).as_ptr() } /// Send commands (at most 2) and wait for all answers. Returns the /// response type of each. fn run(&mut self, cmds: &[(u32, &[u32])], resp_len: u32) -> [u32; 2] { let mut types = [0u32; 2]; for (slot, (cmd, args)) in cmds.iter().enumerate().take(2) { let len = self.put_request(slot as u64, *cmd, args); unsafe { (self.response(slot as u64) as *mut u32).write_volatile(0) }; let req = self.cmd_phys + 512 * slot as u64; let resp = self.cmd_phys + 2048 + 512 * slot as u64; if self.control.push(&[(req, len, false), (resp, resp_len, true)]).is_none() { return types; } } self.control.notify(); for _ in 0..cmds.len().min(2) { if self.control.wait_used(1_000_000).is_none() { return types; // a dead device: 0 = no answer } } for slot in 0..cmds.len().min(2) { types[slot] = unsafe { self.response(slot as u64).read_volatile() }; } types } } /// Send one raw command and return the response type (0 = no answer). /// Exposed for the tests, which check that errors come back as errors. pub fn command(cmd: u32, args: &[u32]) -> u32 { without_interrupts(|| match GPU.lock().as_mut() { Some(gpu) => gpu.run(&[(cmd, args)], 24)[0], None => 0, }) } /// Push the rectangle (x, y, w, h) of the framebuffer to the screen: /// TRANSFER_TO_HOST_2D + RESOURCE_FLUSH, both queued before we wait. /// Clipped to the framebuffer; returns false if the GPU said no. pub fn flush(x: u32, y: u32, w: u32, h: u32) -> bool { let Some(fb) = framebuffer() else { return false }; let (x, y) = (x.min(fb.width), y.min(fb.height)); let (w, h) = (w.min(fb.width - x), h.min(fb.height - y)); if w == 0 || h == 0 { return true; } let offset = (y as u64 * fb.stride as u64 + x as u64) * 4; // where the rect starts in the backing without_interrupts(|| { let mut guard = GPU.lock(); let Some(gpu) = guard.as_mut() else { return false }; let transfer = [x, y, w, h, offset as u32, (offset >> 32) as u32, RESOURCE_ID, 0]; let flush = [x, y, w, h, RESOURCE_ID, 0]; let r = gpu.run(&[(CMD_TRANSFER_TO_HOST_2D, &transfer), (CMD_RESOURCE_FLUSH, &flush)], 24); r == [RESP_OK_NODATA, RESP_OK_NODATA] }) } /// The host's preferred display size (from GET_DISPLAY_INFO). pub fn preferred_size() -> (u32, u32) { *PREFERRED.lock() } pub fn init(devices: &[PciDevice]) -> Option { let pci = devices.iter().find(|d| is_virtio_gpu(d))?; match setup(pci) { Ok(line) => Some(line), Err(why) => Some(format!("gpu: virtio-gpu found but {}", why)), } } fn setup(pci: &PciDevice) -> Result { let transport = Transport::new(pci).ok_or("has no modern virtio registers")?; transport.begin_init(0).ok_or("feature negotiation failed")?; // no VIRGL, no EDID: plain 2D let control = transport.setup_queue(0, 16, false).ok_or("has no control queue")?; transport.driver_ok(); let cmd_phys = memory::alloc_contiguous_frames(1).ok_or("out of memory")?.as_u64(); let mut gpu = Gpu { transport, control, cmd_phys }; // 1. Display info: 16 scanouts x {x, y, width, height, enabled, flags}. let [t, _] = gpu.run(&[(CMD_GET_DISPLAY_INFO, &[])], 24 + 16 * 24); if t != RESP_OK_DISPLAY_INFO { return Err(format!("GET_DISPLAY_INFO answered {:#x}", t)); } let info = gpu.response(0); let (pw, ph, enabled) = unsafe { (info.add(6 + 2).read(), info.add(6 + 3).read(), info.add(6 + 4).read()) }; *PREFERRED.lock() = (pw, ph); // The pixels: 1024 x 768 x 4 bytes = 3 MiB of contiguous frames, so one // backing entry covers it. (Pages anywhere would do, with one entry per // page; contiguous is just less bookkeeping.) let bytes = WIDTH * HEIGHT * 4; let phys = memory::alloc_contiguous_frames((bytes / 4096) as usize).ok_or("out of memory for the framebuffer")?.as_u64(); // 2-4. Create the resource, give it its pages, show it. let steps: [(u32, Vec); 3] = [ (CMD_RESOURCE_CREATE_2D, alloc::vec![RESOURCE_ID, FORMAT_B8G8R8X8, WIDTH, HEIGHT]), // resource, number of entries, then {addr u64, length u32, padding} (CMD_RESOURCE_ATTACH_BACKING, alloc::vec![RESOURCE_ID, 1, phys as u32, (phys >> 32) as u32, bytes, 0]), // rect {x, y, w, h}, scanout id, resource id (CMD_SET_SCANOUT, alloc::vec![0, 0, WIDTH, HEIGHT, 0, RESOURCE_ID]), ]; for (cmd, args) in &steps { let [t, _] = gpu.run(&[(*cmd, args)], 24); if t != RESP_OK_NODATA { return Err(format!("command {:#x} answered {:#x}", cmd, t)); } } let fb = FbInfo { base: memory::phys_to_virt(PhysAddr::new(phys)).as_u64(), phys, width: WIDTH, height: HEIGHT, stride: WIDTH, }; without_interrupts(|| *GPU.lock() = Some(gpu)); FB.init_once(|| fb); flush(0, 0, WIDTH, HEIGHT); // show the (black) picture Ok(format!( "gpu: virtio-gpu 2D at {:02x}:{:02x}.{}, display {}x{}{}, framebuffer {}x{} ({} KiB)", pci.bus, pci.device, pci.function, pw, ph, if enabled != 0 { "" } else { " (disabled)" }, WIDTH, HEIGHT, bytes / 1024 )) } /// Acknowledge interrupts (e.g. "display config changed" when the QEMU /// window is resized); we poll, so nothing else to do. pub fn handle_irq() { if let Some(guard) = GPU.try_lock() { if let Some(gpu) = guard.as_ref() { gpu.transport.read_isr(); } } }