Skip to main content

ferroui_linuxframebuffer/output/
drm_scanout.rs

1//! Reading back what a CRTC shows: the pixels of the framebuffer it scans
2//! out. An addition, for the verification of the DRM output on a virtual
3//! card (docs/porting/linux-framebuffer-platform.md, section 10): the smoke
4//! example and the ControlCatalog compare and save the frame the card
5//! shows, not the frame the renderer believes it drew.
6//!
7//! The framebuffer of the CRTC is found with `DRM_IOCTL_MODE_GETFB`, which
8//! hands the caller a handle of its buffer object (the caller must be the
9//! master of the card or have `CAP_SYS_ADMIN`); the buffer is mapped with
10//! `DRM_IOCTL_MODE_MAP_DUMB`, which works for a buffer object a driver
11//! keeps in memory the processor can map: the dumb buffers that Mesa's
12//! software path on a virtual card (`vkms`) renders into. A buffer in the
13//! memory of a GPU is not readable this way.
14
15#![allow(non_camel_case_types)]
16
17use crate::native_unsafe_methods::{ioc, IOC_READ, IOC_WRITE};
18
19/// `struct drm_mode_crtc` of the kernel (`drm_mode.h`).
20#[repr(C)]
21#[derive(Clone, Copy, Debug)]
22pub struct drm_mode_crtc {
23    pub set_connectors_ptr: u64,
24    pub count_connectors: u32,
25    pub crtc_id: u32,
26    pub fb_id: u32,
27    pub x: u32,
28    pub y: u32,
29    pub gamma_size: u32,
30    pub mode_valid: u32,
31    /// `struct drm_mode_modeinfo`, the same 68 bytes as `drmModeModeInfo`.
32    pub mode: [u8; 68],
33}
34
35/// `struct drm_mode_fb_cmd` of the kernel.
36#[repr(C)]
37#[derive(Clone, Copy, Debug, Default)]
38pub struct drm_mode_fb_cmd {
39    pub fb_id: u32,
40    pub width: u32,
41    pub height: u32,
42    pub pitch: u32,
43    pub bpp: u32,
44    pub depth: u32,
45    pub handle: u32,
46}
47
48/// `struct drm_mode_map_dumb` of the kernel.
49#[repr(C)]
50#[derive(Clone, Copy, Debug, Default)]
51pub struct drm_mode_map_dumb {
52    pub handle: u32,
53    pub pad: u32,
54    pub offset: u64,
55}
56
57/// `struct drm_gem_close` of the kernel.
58#[repr(C)]
59#[derive(Clone, Copy, Debug, Default)]
60pub struct drm_gem_close {
61    pub handle: u32,
62    pub pad: u32,
63}
64
65/// `struct drm_version` of the kernel: the driver of a card.
66#[repr(C)]
67#[derive(Clone, Copy, Debug)]
68pub struct drm_version {
69    pub version_major: libc::c_int,
70    pub version_minor: libc::c_int,
71    pub version_patchlevel: libc::c_int,
72    pub name_len: libc::size_t,
73    pub name: *mut libc::c_char,
74    pub date_len: libc::size_t,
75    pub date: *mut libc::c_char,
76    pub desc_len: libc::size_t,
77    pub desc: *mut libc::c_char,
78}
79
80const DRM_IOCTL_BASE: u8 = b'd';
81pub const DRM_IOCTL_VERSION: u32 = ioc(IOC_READ | IOC_WRITE, DRM_IOCTL_BASE, 0x00, std::mem::size_of::<drm_version>());
82pub const DRM_IOCTL_GEM_CLOSE: u32 = ioc(IOC_WRITE, DRM_IOCTL_BASE, 0x09, std::mem::size_of::<drm_gem_close>());
83pub const DRM_IOCTL_MODE_GETCRTC: u32 =
84    ioc(IOC_READ | IOC_WRITE, DRM_IOCTL_BASE, 0xA1, std::mem::size_of::<drm_mode_crtc>());
85pub const DRM_IOCTL_MODE_GETFB: u32 =
86    ioc(IOC_READ | IOC_WRITE, DRM_IOCTL_BASE, 0xAD, std::mem::size_of::<drm_mode_fb_cmd>());
87pub const DRM_IOCTL_MODE_MAP_DUMB: u32 =
88    ioc(IOC_READ | IOC_WRITE, DRM_IOCTL_BASE, 0xB3, std::mem::size_of::<drm_mode_map_dumb>());
89
90/// The pixels a CRTC shows: rows of `pitch` bytes, `bits_per_pixel` per
91/// pixel (32 for the `XRGB8888` of the DRM output: blue, green, red and an
92/// unused byte in memory order).
93#[derive(Clone, Debug)]
94pub struct DrmScanout {
95    pub fb_id: u32,
96    pub width: u32,
97    pub height: u32,
98    pub pitch: u32,
99    pub bits_per_pixel: u32,
100    pub depth: u32,
101    pub data: Vec<u8>,
102}
103
104impl DrmScanout {
105    /// The red, green and blue of a pixel of a 32-bit frame.
106    pub fn pixel(&self, x: u32, y: u32) -> Option<(u8, u8, u8)> {
107        if self.bits_per_pixel != 32 || x >= self.width || y >= self.height {
108            return None;
109        }
110        let offset = y as usize * self.pitch as usize + x as usize * 4;
111        let px = self.data.get(offset..offset + 4)?;
112        Some((px[2], px[1], px[0]))
113    }
114}
115
116#[cfg(target_os = "linux")]
117fn ioctl<T>(fd: i32, request: u32, argument: &mut T) -> std::io::Result<()> {
118    // SAFETY: the request is one of the requests above, whose argument is a structure of the
119    // type and size it encodes, alive for the call.
120    let rv = unsafe { libc::ioctl(fd, request as _, argument as *mut T) };
121    if rv < 0 {
122        return Err(std::io::Error::last_os_error());
123    }
124    Ok(())
125}
126
127/// The name of the driver of the card `fd` (`vkms` for the virtual KMS
128/// driver), so that a test refuses a card that is not the one it expects
129/// before it sets a mode on it.
130#[cfg(target_os = "linux")]
131pub fn driver_name(fd: i32) -> std::io::Result<String> {
132    let mut name = [0u8; 64];
133    let mut version = drm_version {
134        version_major: 0,
135        version_minor: 0,
136        version_patchlevel: 0,
137        name_len: name.len(),
138        name: name.as_mut_ptr().cast(),
139        date_len: 0,
140        date: std::ptr::null_mut(),
141        desc_len: 0,
142        desc: std::ptr::null_mut(),
143    };
144    // The kernel writes at most `name_len` bytes of the name and sets the length of the whole.
145    ioctl(fd, DRM_IOCTL_VERSION, &mut version)?;
146    let length = version.name_len.min(name.len());
147    Ok(String::from_utf8_lossy(&name[..length]).into_owned())
148}
149
150/// Reads the framebuffer the CRTC `crtc_id` of the card `fd` scans out.
151#[cfg(target_os = "linux")]
152pub fn read_scanout(fd: i32, crtc_id: u32) -> std::io::Result<DrmScanout> {
153    use std::io;
154
155    let mut crtc = drm_mode_crtc {
156        set_connectors_ptr: 0,
157        count_connectors: 0,
158        crtc_id,
159        fb_id: 0,
160        x: 0,
161        y: 0,
162        gamma_size: 0,
163        mode_valid: 0,
164        mode: [0; 68],
165    };
166    ioctl(fd, DRM_IOCTL_MODE_GETCRTC, &mut crtc)?;
167    if crtc.fb_id == 0 {
168        return Err(io::Error::other(format!("the CRTC {crtc_id} shows no framebuffer")));
169    }
170
171    let mut fb = drm_mode_fb_cmd { fb_id: crtc.fb_id, ..Default::default() };
172    ioctl(fd, DRM_IOCTL_MODE_GETFB, &mut fb)?;
173    if fb.handle == 0 {
174        return Err(io::Error::new(
175            io::ErrorKind::PermissionDenied,
176            "the framebuffer has no handle for this process (not the master of the card and no CAP_SYS_ADMIN)",
177        ));
178    }
179
180    let read = || -> io::Result<Vec<u8>> {
181        let mut map = drm_mode_map_dumb { handle: fb.handle, ..Default::default() };
182        ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mut map)?;
183        let length = fb.pitch as usize * fb.height as usize;
184        // SAFETY: a shared read-only mapping of the buffer at the offset the kernel gave for it.
185        let address =
186            unsafe { libc::mmap(std::ptr::null_mut(), length, libc::PROT_READ, libc::MAP_SHARED, fd, map.offset as libc::off_t) };
187        if address == libc::MAP_FAILED {
188            return Err(io::Error::last_os_error());
189        }
190        // SAFETY: the mapping has `length` readable bytes; it is unmapped once, after the copy.
191        let data = unsafe {
192            let data = std::slice::from_raw_parts(address.cast::<u8>(), length).to_vec();
193            libc::munmap(address, length);
194            data
195        };
196        Ok(data)
197    };
198    let data = read();
199    let mut close = drm_gem_close { handle: fb.handle, pad: 0 };
200    let _ = ioctl(fd, DRM_IOCTL_GEM_CLOSE, &mut close);
201
202    Ok(DrmScanout {
203        fb_id: fb.fb_id,
204        width: fb.width,
205        height: fb.height,
206        pitch: fb.pitch,
207        bits_per_pixel: fb.bpp,
208        depth: fb.depth,
209        data: data?,
210    })
211}
212
213#[cfg(test)]
214mod tests {
215    // An addition; the sizes, offsets and request numbers of the kernel
216    // (drm.h, drm_mode.h).
217    use super::*;
218    use std::mem::{offset_of, size_of};
219
220    #[test]
221    fn the_structures_and_requests_are_those_of_the_kernel() {
222        assert_eq!(size_of::<drm_mode_crtc>(), 104);
223        assert_eq!(offset_of!(drm_mode_crtc, crtc_id), 12);
224        assert_eq!(offset_of!(drm_mode_crtc, fb_id), 16);
225        assert_eq!(offset_of!(drm_mode_crtc, mode), 36);
226        assert_eq!(size_of::<drm_mode_fb_cmd>(), 28);
227        assert_eq!(size_of::<drm_mode_map_dumb>(), 16);
228        assert_eq!(offset_of!(drm_mode_map_dumb, offset), 8);
229        assert_eq!(size_of::<drm_gem_close>(), 8);
230
231        assert_eq!(DRM_IOCTL_MODE_GETCRTC, 0xC068_64A1);
232        assert_eq!(DRM_IOCTL_MODE_GETFB, 0xC01C_64AD);
233        assert_eq!(DRM_IOCTL_MODE_MAP_DUMB, 0xC010_64B3);
234        assert_eq!(DRM_IOCTL_GEM_CLOSE, 0x4008_6409);
235    }
236
237    #[cfg(target_pointer_width = "64")]
238    #[test]
239    fn the_version_has_the_layout_of_the_kernel_on_64_bits() {
240        assert_eq!(size_of::<drm_version>(), 64);
241        assert_eq!(offset_of!(drm_version, name_len), 16);
242        assert_eq!(offset_of!(drm_version, desc), 56);
243        assert_eq!(DRM_IOCTL_VERSION, 0xC040_6400);
244    }
245
246    #[test]
247    fn a_pixel_of_xrgb8888_is_blue_green_red_in_memory() {
248        let scanout = DrmScanout {
249            fb_id: 1,
250            width: 2,
251            height: 1,
252            pitch: 8,
253            bits_per_pixel: 32,
254            depth: 24,
255            data: vec![0x99, 0x66, 0x33, 0, 1, 2, 3, 0],
256        };
257        assert_eq!(scanout.pixel(0, 0), Some((0x33, 0x66, 0x99)));
258        assert_eq!(scanout.pixel(1, 0), Some((3, 2, 1)));
259        assert_eq!(scanout.pixel(2, 0), None);
260    }
261}