diff --git a/Kernel/Core/lib/mod.rs b/Kernel/Core/lib/mod.rs
index 48aafd7240673cae8e7b82e0ffc161ca6d26e7fc..2559aff13805542a291d95b7b89064439ae7e787 100644
--- a/Kernel/Core/lib/mod.rs
+++ b/Kernel/Core/lib/mod.rs
@@ -14,7 +14,7 @@ pub use self::sparse_vec::SparseVec;
 pub use self::string::String;
 pub use self::lazy_static::LazyStatic;
 pub use self::vec_deque::VecDeque;
-pub use self::pod::POD;
+pub use self::pod::{POD, PodHelpers};
 
 pub use self::pod::{as_byte_slice, as_byte_slice_mut};
 
diff --git a/Kernel/Core/lib/pod.rs b/Kernel/Core/lib/pod.rs
index 85e24523c7e455b1802c10eaf242ecc05eb4e731..a4ed5a5bbbe91e4cdacc5c4ff7be91ab645f115c 100644
--- a/Kernel/Core/lib/pod.rs
+++ b/Kernel/Core/lib/pod.rs
@@ -6,6 +6,7 @@
 
 /// Plain-old-data trait
 pub unsafe auto trait POD {}
+
 //impl<T: ::core::ops::Drop> !POD for T {}  // - I would love this, but it collides with every other !POD impl
 impl<T> !POD for ::core::cell::UnsafeCell<T> {}
 impl<T> !POD for ::core::ptr::NonNull<T> {}
@@ -27,3 +28,21 @@ pub fn as_byte_slice_mut<T: ?Sized + POD>(s: &mut T) -> &mut [u8] {
 	// SAFE: Plain-old-data
 	unsafe { ::core::slice::from_raw_parts_mut(s as *mut _ as *mut u8, ::core::mem::size_of_val(s)) }
 }
+
+pub trait PodHelpers
+{
+	fn zeroed() -> Self where Self: Sized + POD {
+		// SAFE: This method is only ever valid when Self: POD, which allows any bit pattern
+		unsafe { ::core::mem::zeroed() }
+	}
+	fn as_byte_slice(&self) -> &[u8];
+	fn as_byte_slice_mut(&mut self) -> &mut [u8];
+}
+impl<T: ?Sized + POD> PodHelpers for T {
+	fn as_byte_slice(&self) -> &[u8] {
+		as_byte_slice(self)
+	}
+	fn as_byte_slice_mut(&mut self) -> &mut [u8] {
+		as_byte_slice_mut(self)
+	}
+}
diff --git a/Kernel/Modules/virtio/devices/mod.rs b/Kernel/Modules/virtio/devices/mod.rs
index 11f25151d2c060d4e51f3530185a60267ce2e801..7b81754719c271370fd8b1316722ba3b158ab066 100644
--- a/Kernel/Modules/virtio/devices/mod.rs
+++ b/Kernel/Modules/virtio/devices/mod.rs
@@ -7,9 +7,10 @@ use kernel::device_manager;
 use interface::Interface;
 
 mod block;
+mod video;
 //mod network;
 
-pub fn new_boxed<T: Interface+Send+'static>(dev: u32, io: device_manager::IOBinding, irq: u32) -> Box<device_manager::DriverInstance>
+pub fn new_boxed<T: Interface+Send+Sync+'static>(dev: u32, io: device_manager::IOBinding, irq: u32) -> Box<device_manager::DriverInstance>
 {
 	match dev
 	{
@@ -21,6 +22,7 @@ pub fn new_boxed<T: Interface+Send+'static>(dev: u32, io: device_manager::IOBind
 		Box::new(NullDevice)
 		}
 	2 => Box::new( block::BlockDevice::new(T::new(io, irq)) ),
+	16 => Box::new( video::VideoDevice::new(T::new(io, irq)) ),
 	dev @ _ => {
 		log_error!("VirtIO device has unknown device ID {:#x}", dev);
 		Box::new(NullDevice)
diff --git a/Kernel/Modules/virtio/devices/video.rs b/Kernel/Modules/virtio/devices/video.rs
new file mode 100644
index 0000000000000000000000000000000000000000..47be6b8122b3d6354bd8c8f6645c474d0c5d9199
--- /dev/null
+++ b/Kernel/Modules/virtio/devices/video.rs
@@ -0,0 +1,192 @@
+/*
+ */
+use kernel::prelude::*;
+use kernel::metadevs::video;
+use interface::Interface;
+use queue::{Queue,Buffer};
+use kernel::lib::mem::aref::{Aref,ArefBorrow};
+use kernel::async::Mutex;
+
+pub struct VideoDevice<I>
+where
+	I: Interface + Send + Sync
+{
+	_core: Aref<DeviceCore<I>>
+}
+impl<I> ::kernel::device_manager::DriverInstance for VideoDevice<I>
+where
+	I: Interface + Send + Sync
+{
+}
+
+struct DeviceCore<I>
+where
+	I: Interface + Send + Sync
+{
+	interface: I,
+	controlq: Queue,
+	cursorq: Queue,
+
+	scanouts: Mutex<Vec<Option<Framebuffer<I>>>>,
+}
+
+struct Framebuffer<I>
+where
+	I: Interface + Send + Sync
+{
+	dev: ArefBorrow<DeviceCore<I>>,
+	/// Handle to video metadev registration
+	_video_handle: video::FramebufferRegistration,
+}
+
+impl<I> VideoDevice<I>
+where
+	I: 'static + Interface + Send + Sync
+{
+	pub fn new(mut int: I) -> Self
+	{
+		// SAFE: Read-only field
+		let num_scanouts = unsafe { int.cfg_read_32(8) } as usize;
+
+		let core = Aref::new(DeviceCore {
+			controlq: int.get_queue(0, 0).expect("Queue #0 'controlq' missing on virtio gpu device"),
+			cursorq: int.get_queue(1, 0).expect("Queue #1 'cursorq' missing on virtio gpu device"),
+			scanouts: Mutex::new(Vec::from_fn(num_scanouts, |_| None)),
+			interface: int,
+			});
+
+		let di = core.get_display_info();
+		log_debug!("di = {:?}", di);
+
+		VideoDevice {
+			_core: core,
+			}
+	}
+}
+
+impl<I> DeviceCore<I>
+where
+	I: Interface + Send + Sync
+{
+
+	fn get_display_info(&self) -> /*SmallVec<*/[hw::DisplayOne; 16]//>
+	{
+		let hdr = hw::CtrlHeader {
+			type_: hw::VIRTIO_GPU_CMD_GET_DISPLAY_INFO as u32,
+			flags: hw::VIRTIO_GPU_FLAG_FENCE,
+			fence_id: 1,
+			ctx_id: 0,
+			_padding: 0,
+			};
+		let mut ret_hdr: hw::CtrlHeader = ::kernel::lib::PodHelpers::zeroed();
+		let mut ret_info: [hw::DisplayOne; 16] = ::kernel::lib::PodHelpers::zeroed();
+		let h = self.controlq.send_buffers(&self.interface, &mut [
+			Buffer::Read(::kernel::lib::as_byte_slice(&hdr)),
+			Buffer::Write(::kernel::lib::as_byte_slice_mut(&mut ret_hdr)),
+			Buffer::Write(::kernel::lib::as_byte_slice_mut(&mut ret_info)),
+			]);
+		match h.wait_for_completion()
+		{
+		Ok(bytes) => todo!("{} bytes from gpu request", bytes),
+		Err( () ) => panic!("TODO"),
+		}
+	}
+}
+
+impl<I> video::Framebuffer for Framebuffer<I>
+where
+	I: 'static + Interface + Send + Sync
+{
+	fn as_any(&self) -> &Any {
+		self as &Any
+	}
+	fn activate(&mut self) {
+		// TODO
+	}
+	
+	fn get_size(&self) -> video::Dims {
+		// TODO
+		todo!("");
+	}
+	fn set_size(&mut self, _newsize: video::Dims) -> bool {
+		// TODO
+		false
+	}
+	
+	fn blit_inner(&mut self, dst: video::Rect, src: video::Rect) {
+	}
+	fn blit_ext(&mut self, dst: video::Rect, src: video::Rect, srf: &video::Framebuffer) -> bool {
+		false
+	}
+	fn blit_buf(&mut self, dst: video::Rect, buf: &[u32]) {
+	}
+	fn fill(&mut self, dst: video::Rect, colour: u32) {
+	}
+	fn move_cursor(&mut self, _p: Option<video::Pos>) {
+	}
+}
+
+
+mod hw
+{
+	#[repr(u32)]
+	#[allow(non_camel_case_types)]
+	#[allow(dead_code)]
+	pub enum CtrlType
+	{
+		/* 2d commands */
+		VIRTIO_GPU_CMD_GET_DISPLAY_INFO = 0x0100,
+		VIRTIO_GPU_CMD_RESOURCE_CREATE_2D,
+		VIRTIO_GPU_CMD_RESOURCE_UNREF,
+		VIRTIO_GPU_CMD_SET_SCANOUT,
+		VIRTIO_GPU_CMD_RESOURCE_FLUSH,
+		VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D,
+		VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING,
+		VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING,
+		/* cursor commands */
+		VIRTIO_GPU_CMD_UPDATE_CURSOR = 0x0300,
+		VIRTIO_GPU_CMD_MOVE_CURSOR,
+		/* success responses */
+		VIRTIO_GPU_RESP_OK_NODATA = 0x1100,
+		VIRTIO_GPU_RESP_OK_DISPLAY_INFO,
+		/* error responses */
+		VIRTIO_GPU_RESP_ERR_UNSPEC = 0x1200,
+		VIRTIO_GPU_RESP_ERR_OUT_OF_MEMORY,
+		VIRTIO_GPU_RESP_ERR_INVALID_SCANOUT_ID,
+		VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID,
+		VIRTIO_GPU_RESP_ERR_INVALID_CONTEXT_ID,
+		VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER,
+	}
+	pub use self::CtrlType::*;
+
+	pub const VIRTIO_GPU_FLAG_FENCE: u32 = 1 << 0;
+
+	#[repr(C)]
+	pub struct CtrlHeader
+	{
+		pub type_: u32,
+		pub flags: u32,
+		pub fence_id: u64,
+		pub ctx_id: u32,
+		pub _padding: u32,
+	}
+
+	#[repr(C)]
+	#[derive(Debug)]
+	pub struct Rect
+	{
+		pub x: u32,
+		pub y: u32,
+		pub width: u32,
+		pub height: u32,
+	}
+	#[repr(C)]
+	#[derive(Debug)]
+	pub struct DisplayOne
+	{
+		pub r: Rect,
+		pub enabled: u32,
+		pub flags: u32,
+	}
+}
+
diff --git a/Kernel/Modules/virtio/drivers.rs b/Kernel/Modules/virtio/drivers.rs
index e3368a619b2268d7ddc4bb3ca9659b1be5d2956c..66d2d59dca047a0ead89e3e742370e81449e80ec 100644
--- a/Kernel/Modules/virtio/drivers.rs
+++ b/Kernel/Modules/virtio/drivers.rs
@@ -7,10 +7,12 @@ use kernel::device_manager;
 use devices::NullDevice;
 
 
+static S_PCI_DRIVER: Pci = Pci;
 static S_FDT_MMIO_DRIVER: FdtMmioDriver = FdtMmioDriver;
 
 pub fn register()
 {
+	device_manager::register_driver(&S_PCI_DRIVER);
 	device_manager::register_driver(&S_FDT_MMIO_DRIVER);
 }
 
@@ -51,4 +53,49 @@ impl device_manager::Driver for FdtMmioDriver
 	}
 }
 
+struct Pci;
+impl device_manager::Driver for Pci
+{
+	fn name(&self) -> &str {
+		"virtio-pci"
+	}
+	fn bus_type(&self) -> &str {
+		"pci"
+	}
+	fn handles(&self, bus_dev: &::kernel::device_manager::BusDevice) -> u32
+	{
+		let vendor = bus_dev.get_attr("vendor").unwrap_u32();
+		let device = bus_dev.get_attr("device").unwrap_u32();
+		if vendor == 0x1AF4 && (0x1000 <= device && device <= 0x107F)  {
+			2
+		}
+		else {
+			0
+		}
+	}
+	fn bind(&self, bus_dev: &mut ::kernel::device_manager::BusDevice) -> Box<::kernel::device_manager::DriverInstance+'static>
+	{
+		let irq = bus_dev.get_irq(0);
+		// TODO: The IO space may not be in BAR0? Instead referenced in PCI capabilities
+		// - The PCI capabilities list includes entries for each region the driver uses, which can sub-slice a BAR
+		// - Need to be able to read the capabilities list, AND get a sub-slice of a BAR
+		let io = bus_dev.bind_io(0);
+		let dev = match bus_dev.get_attr("device").unwrap_u32()
+			{
+			0x1000 => 1,	// network card
+			0x1001 => 2,	// block dev
+			0x1002 => 5,	// memory baloon
+			0x1003 => 3,	// console
+			0x1004 => 8,	// SCSI host
+			0x1005 => 4,	// entropy source
+			v @ 0x1006 ... 0x1008 => todo!("Unknown PCI ID {:#x}", v),
+			0x1009 => 9,	// "9P transport"
+			v @ 0x100A ... 0x103F => todo!("Unknown PCI ID {:#x}", v),
+			v @ 0x1040 ... 0x107F => v - 0x1040,
+			v @ _ => panic!("BUGCHECK: Binding with unexpected PCI device id {:#x}", v),
+			};
+
+		::devices::new_boxed::<::interface::Mmio>(dev, io, irq)
+	}
+}