⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/drivers/
ramfb.rs

1//! QEMU ramfb display device driver.
2//!
3//! Implements the `ramfb` virtual display for AArch64 and RISC-V guests
4//! via QEMU's fw_cfg interface. This gives non-UEFI architectures a
5//! graphical framebuffer output.
6//!
7//! Protocol: Write a `RamfbConfig` struct to the `etc/ramfb` fw_cfg
8//! file selector. QEMU then displays the specified memory region as
9//! a framebuffer.
10//!
11//! Requires `-device ramfb` on the QEMU command line.
12
13use crate::error::KernelError;
14
15/// DRM_FORMAT_XRGB8888 fourcc code (little-endian).
16#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
17const DRM_FORMAT_XRGB8888: u32 = 0x34325258; // 'XR24'
18
19/// fw_cfg MMIO base address for QEMU virt machine.
20/// AArch64 and RISC-V virt machines use the same address.
21#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
22const FWCFG_BASE: usize = 0x0902_0000;
23
24/// fw_cfg register offsets (MMIO, big-endian).
25#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
26const FWCFG_DATA: usize = 0x00;
27#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
28const FWCFG_SELECTOR: usize = 0x08;
29#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
30const FWCFG_DMA: usize = 0x10;
31
32/// fw_cfg DMA control bits.
33#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
34const FWCFG_DMA_SELECT: u32 = 1 << 3;
35#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
36const FWCFG_DMA_WRITE: u32 = 1 << 4;
37
38/// Packed ramfb configuration structure (28 bytes).
39/// All fields are big-endian as required by the fw_cfg protocol.
40#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
41#[repr(C, packed)]
42struct RamfbConfig {
43    addr: u64,
44    fourcc: u32,
45    flags: u32,
46    width: u32,
47    height: u32,
48    stride: u32,
49}
50
51/// fw_cfg DMA access descriptor (16 bytes).
52#[repr(C, packed)]
53#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
54struct FwCfgDmaAccess {
55    control: u32,
56    length: u32,
57    address: u64,
58}
59
60/// Initialize the ramfb display device.
61///
62/// Allocates a framebuffer from the frame allocator and registers it
63/// with QEMU via the fw_cfg `etc/ramfb` file selector.
64///
65/// Returns a pointer to the framebuffer memory on success.
66///
67/// # Arguments
68/// * `width` - Display width in pixels
69/// * `height` - Display height in pixels
70#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
71pub fn init(width: u32, height: u32) -> Result<*mut u8, KernelError> {
72    use crate::mm::{FRAME_ALLOCATOR, FRAME_SIZE};
73
74    let stride = width * 4; // 4 bytes per pixel (XRGB8888)
75    let fb_size = (stride * height) as usize;
76
77    // Allocate contiguous physical frames for the framebuffer.
78    let num_frames = fb_size.div_ceil(FRAME_SIZE);
79    let frame_num = FRAME_ALLOCATOR
80        .lock()
81        .allocate_frames(num_frames, None)
82        .map_err(|_| KernelError::ResourceExhausted {
83            resource: "physical frames for ramfb",
84        })?;
85    let fb_phys = frame_num.as_addr().as_u64() as usize;
86
87    // On AArch64/RISC-V QEMU virt, physical memory is identity-mapped or
88    // accessible at its physical address during early boot.
89    let fb_ptr = fb_phys as *mut u8;
90
91    // SAFETY: fb_ptr points to freshly allocated physical memory.
92    // Writing zeros initializes the framebuffer to black.
93    unsafe {
94        core::ptr::write_bytes(fb_ptr, 0, fb_size);
95    }
96
97    // Build the ramfb config (all fields big-endian)
98    let config = RamfbConfig {
99        addr: (fb_phys as u64).to_be(),
100        fourcc: DRM_FORMAT_XRGB8888.to_be(),
101        flags: 0u32.to_be(),
102        width: width.to_be(),
103        height: height.to_be(),
104        stride: stride.to_be(),
105    };
106
107    // Find the "etc/ramfb" fw_cfg selector by scanning the directory
108    let selector = find_fwcfg_file("etc/ramfb").ok_or(KernelError::NotFound {
109        resource: "fw_cfg etc/ramfb selector",
110        id: 0,
111    })?;
112
113    // Write the config via fw_cfg DMA
114    write_fwcfg_dma(selector, &config as *const RamfbConfig as *const u8, 28)?;
115
116    Ok(fb_ptr)
117}
118
119/// Stub for architectures that don't support ramfb.
120#[cfg(not(any(target_arch = "aarch64", target_arch = "riscv64")))]
121pub fn init(_width: u32, _height: u32) -> Result<*mut u8, KernelError> {
122    Err(KernelError::OperationNotSupported {
123        operation: "ramfb (x86_64 uses UEFI framebuffer)",
124    })
125}
126
127/// Find a fw_cfg file by name and return its selector ID.
128#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
129fn find_fwcfg_file(name: &str) -> Option<u16> {
130    // Select the file directory (selector 0x0019)
131    // SAFETY: MMIO writes to fw_cfg control registers.
132    unsafe {
133        let selector_reg = (FWCFG_BASE + FWCFG_SELECTOR) as *mut u16;
134        selector_reg.write_volatile(0x0019u16.to_be());
135    }
136
137    // Read the file count (first 4 bytes, big-endian)
138    let count: u32;
139    // SAFETY: Reading from fw_cfg data register after selecting the directory.
140    unsafe {
141        let data_reg = (FWCFG_BASE + FWCFG_DATA) as *const u32;
142        count = u32::from_be(data_reg.read_volatile());
143    }
144
145    // Each directory entry is 64 bytes: u32 size + u16 select + u16 reserved +
146    // 56-byte name
147    for _ in 0..count {
148        let mut entry = [0u8; 64];
149        // SAFETY: Reading sequential bytes from the fw_cfg data register.
150        unsafe {
151            let data_reg = FWCFG_BASE as *const u8;
152            for byte in &mut entry {
153                *byte = data_reg.read_volatile();
154            }
155        }
156        let selector = u16::from_be_bytes([entry[4], entry[5]]);
157        // Extract name (bytes 8..64, null-terminated)
158        let name_bytes = &entry[8..64];
159        let name_len = name_bytes.iter().position(|&b| b == 0).unwrap_or(56);
160        let entry_name = core::str::from_utf8(&name_bytes[..name_len]).unwrap_or("");
161        if entry_name == name {
162            return Some(selector);
163        }
164    }
165
166    None
167}
168
169/// Write data to a fw_cfg file selector using DMA.
170#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
171fn write_fwcfg_dma(selector: u16, data: *const u8, length: u32) -> Result<(), KernelError> {
172    let dma_access = FwCfgDmaAccess {
173        control: (FWCFG_DMA_SELECT | FWCFG_DMA_WRITE | ((selector as u32) << 16)).to_be(),
174        length: length.to_be(),
175        address: (data as u64).to_be(),
176    };
177
178    let dma_addr = &dma_access as *const FwCfgDmaAccess as u64;
179
180    // Write the DMA descriptor address (big-endian, split into high/low 32-bit)
181    // SAFETY: MMIO writes to the fw_cfg DMA register to initiate a write transfer.
182    unsafe {
183        let dma_reg_hi = (FWCFG_BASE + FWCFG_DMA) as *mut u32;
184        let dma_reg_lo = (FWCFG_BASE + FWCFG_DMA + 4) as *mut u32;
185        dma_reg_hi.write_volatile(((dma_addr >> 32) as u32).to_be());
186        dma_reg_lo.write_volatile((dma_addr as u32).to_be());
187    }
188
189    // Poll for completion (control field becomes 0 when done).
190    // Use raw pointer arithmetic to avoid referencing a packed field.
191    let control_ptr = core::ptr::addr_of!(dma_access.control);
192    // SAFETY: Reading the DMA control field to check completion. The
193    // field is modified by the hypervisor (QEMU) when the DMA completes.
194    // read_unaligned handles the packed struct alignment.
195    for _ in 0..1_000_000 {
196        let val = unsafe { core::ptr::read_unaligned(control_ptr) };
197        if val == 0 {
198            return Ok(());
199        }
200        core::hint::spin_loop();
201    }
202
203    Err(KernelError::Timeout {
204        operation: "fw_cfg DMA write",
205        duration_ms: 0,
206    })
207}