RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / virtio / gpu.rs
10183 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
//! 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<Option<Gpu>> = Mutex::new(None);
static FB: OnceCell<FbInfo> = 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<FbInfo> {
    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<String> {
    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<String, String> {
    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<u32>); 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();
        }
    }
}