Skip to main content

virtio_drivers/device/gpu/
edid.rs

1//! EDID (Extended Display Identification Data) parsing.
2//!
3//! Provides types for extracting display information from raw EDID blobs,
4//! including preferred resolution and standard timing modes.
5//!
6//! Reference: VESA Enhanced EDID Standard (E-EDID), Release A, Rev. 2.
7
8use crate::{Error, Result};
9use alloc::vec::Vec;
10
11/// The number of standard timing entries in the base EDID block.
12const NUM_STANDARD_TIMINGS: usize = 8;
13
14/// The byte offset of the first Detailed Timing Descriptor in the base block.
15const DTD1_OFFSET: usize = 0x36;
16
17/// The byte length of a single Detailed Timing Descriptor.
18const DTD_LEN: usize = 18;
19
20/// The byte offset of the first Standard Timing entry in the base block.
21const STANDARD_TIMINGS_OFFSET: usize = 38;
22
23/// The byte length of a single Standard Timing entry.
24const STANDARD_TIMING_LEN: usize = 2;
25
26/// Aspect ratio encoded in a Standard Timing entry.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28enum AspectRatio {
29    /// 16:10
30    Ratio16x10,
31    /// 4:3
32    Ratio4x3,
33    /// 5:4
34    Ratio5x4,
35    /// 16:9
36    Ratio16x9,
37}
38
39impl AspectRatio {
40    fn from_bits(bits: u8) -> Self {
41        match bits {
42            0 => Self::Ratio16x10,
43            1 => Self::Ratio4x3,
44            2 => Self::Ratio5x4,
45            3 => Self::Ratio16x9,
46            _ => unreachable!(),
47        }
48    }
49
50    /// Compute vertical pixels from horizontal pixels and this aspect ratio.
51    fn v_pixels(self, h_pixels: u32) -> u32 {
52        match self {
53            Self::Ratio16x10 => h_pixels * 10 / 16,
54            Self::Ratio4x3 => h_pixels * 3 / 4,
55            Self::Ratio5x4 => h_pixels * 4 / 5,
56            Self::Ratio16x9 => h_pixels * 9 / 16,
57        }
58    }
59}
60
61/// A single standard timing entry from the EDID.
62///
63/// Reference: VESA E-EDID, Section 3.9 "Standard Timing Identification".
64struct StandardTiming {
65    /// Horizontal active pixels.
66    h_pixels: u32,
67    /// Vertical active pixels (derived from aspect ratio).
68    v_pixels: u32,
69}
70
71impl StandardTiming {
72    /// The marker value for an unused standard timing slot.
73    const UNUSED: [u8; 2] = [0x01, 0x01];
74
75    /// Parse a standard timing from its 2-byte encoding.
76    ///
77    /// Returns `None` for unused entries (0x0101).
78    fn parse(bytes: &[u8; 2]) -> Option<Self> {
79        if *bytes == Self::UNUSED {
80            return None;
81        }
82        let h_pixels = (bytes[0] as u32 + 31) * 8;
83        let aspect = AspectRatio::from_bits((bytes[1] >> 6) & 0x03);
84        let v_pixels = aspect.v_pixels(h_pixels);
85        Some(Self { h_pixels, v_pixels })
86    }
87}
88
89/// A Detailed Timing Descriptor from the EDID base block.
90///
91/// Reference: VESA E-EDID, Section 3.10.2 "Detailed Timing Definitions".
92struct DetailedTiming {
93    /// Horizontal active pixels.
94    h_active: u32,
95    /// Vertical active pixels.
96    v_active: u32,
97}
98
99impl DetailedTiming {
100    /// Parse a detailed timing descriptor from its 18-byte encoding.
101    ///
102    /// Returns `None` if the active pixel counts are zero.
103    fn parse(bytes: &[u8; DTD_LEN]) -> Option<Self> {
104        // Horizontal active: lower 8 bits at byte 2, upper 4 bits in high nibble of byte 4.
105        let h_active = bytes[2] as u32 | ((bytes[4] as u32 & 0xF0) << 4);
106        // Vertical active: lower 8 bits at byte 5, upper 4 bits in high nibble of byte 7.
107        let v_active = bytes[5] as u32 | ((bytes[7] as u32 & 0xF0) << 4);
108        if h_active == 0 || v_active == 0 {
109            return None;
110        }
111        Some(Self { h_active, v_active })
112    }
113}
114
115/// Parsed EDID data from a display device.
116///
117/// Wraps the raw EDID byte blob and provides methods to extract display
118/// information such as preferred resolution and supported standard timings.
119pub struct Edid {
120    pub(super) data: [u8; 1024],
121    pub(super) size: u32,
122}
123
124impl Edid {
125    /// Whether the base EDID block (128 bytes) is present.
126    fn has_base_block(&self) -> bool {
127        self.size >= 128
128    }
129
130    /// Parse the first Detailed Timing Descriptor.
131    fn first_detailed_timing(&self) -> Option<DetailedTiming> {
132        if !self.has_base_block() {
133            return None;
134        }
135        let bytes = &self.data[DTD1_OFFSET..][..DTD_LEN].try_into().unwrap();
136        DetailedTiming::parse(bytes)
137    }
138
139    /// Parse a single standard timing entry by index (0-7).
140    fn standard_timing(&self, index: usize) -> Option<StandardTiming> {
141        let offset = STANDARD_TIMINGS_OFFSET + index * STANDARD_TIMING_LEN;
142        let bytes = &self.data[offset..][..STANDARD_TIMING_LEN]
143            .try_into()
144            .unwrap();
145        StandardTiming::parse(bytes)
146    }
147
148    /// Get the preferred resolution from the EDID data.
149    ///
150    /// Returns the resolution from the first Detailed Timing Descriptor,
151    /// which per the EDID spec represents the display's preferred mode.
152    pub fn preferred_resolution(&self) -> Result<(u32, u32)> {
153        let dtd = self.first_detailed_timing().ok_or(Error::IoError)?;
154        Ok((dtd.h_active, dtd.v_active))
155    }
156
157    /// Get the list of supported resolutions from EDID standard timings.
158    ///
159    /// Returns up to 8 (width, height) pairs sorted by total pixel count
160    /// (largest first).
161    pub fn standard_timings(&self) -> Vec<(u32, u32)> {
162        if !self.has_base_block() {
163            return Vec::new();
164        }
165        let mut resolutions: Vec<(u32, u32)> = (0..NUM_STANDARD_TIMINGS)
166            .filter_map(|i| self.standard_timing(i))
167            .map(|st| (st.h_pixels, st.v_pixels))
168            .collect();
169        resolutions.sort_by(|a, b| (b.0 as u64 * b.1 as u64).cmp(&(a.0 as u64 * a.1 as u64)));
170        resolutions
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    /// Real EDID captured from QEMU virtio-GPU with `-device virtio-gpu,xres=1920,yres=1080`.
179    /// QEMU generates this EDID dynamically. The base block (bytes 0-127) contains:
180    /// - Manufacturer: "RHT" (Red Hat), product code 0x1234
181    /// - DTD1 preferred mode: 1920x1080 @ 60Hz
182    /// - 8 Standard Timings: 2048x1152, 1920x1080, 1920x1200, 1600x1200,
183    ///   1680x1050, 1440x900, 1280x1024, 1280x960
184    /// - CEA extension block (bytes 128-255) with SVDs for additional modes
185    /// Remaining bytes (256-1023) are zero-padded by the virtio-GPU device.
186    fn qemu_edid() -> [u8; 1024] {
187        let mut edid = [0u8; 1024];
188        let data: [u8; 256] = [
189            // Base EDID block (128 bytes)
190            0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x49, 0x14, 0x34, 0x12, 0x00, 0x00,
191            0x00, 0x00, 0x2a, 0x18, 0x01, 0x04, 0xa5, 0x30, 0x1b, 0x78, 0x06, 0xee, 0x91, 0xa3,
192            0x54, 0x4c, 0x99, 0x26, 0x0f, 0x50, 0x54, 0x21, 0x08, 0x00, 0xe1, 0xc0, 0xd1, 0xc0,
193            0xd1, 0x00, 0xa9, 0x40, 0xb3, 0x00, 0x95, 0x00, 0x81, 0x80, 0x81, 0x40, 0xd2, 0x54,
194            0x80, 0xa0, 0x72, 0x38, 0x25, 0x40, 0xe0, 0x39, 0x55, 0x40, 0xe7, 0x12, 0x11, 0x00,
195            0x00, 0x18, 0x00, 0x00, 0x00, 0xf7, 0x00, 0x0a, 0x00, 0x40, 0x82, 0x00, 0x28, 0x20,
196            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x00, 0x32, 0x7d, 0x1e,
197            0xa0, 0xff, 0x01, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0xfc,
198            0x00, 0x51, 0x45, 0x4d, 0x55, 0x20, 0x4d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x0a,
199            0x01, 0xb0, // CEA extension block (128 bytes)
200            0x02, 0x03, 0x0b, 0x00, 0x46, 0x7d, 0x65, 0x60, 0x59, 0x1f, 0x61, 0x00, 0x00, 0x00,
201            0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
202            0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
203            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
204            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
205            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
206            0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
207            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
208            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
209            0x00, 0x2f,
210        ];
211        edid[..256].copy_from_slice(&data);
212        edid
213    }
214
215    fn make_edid(data: [u8; 1024], size: u32) -> Edid {
216        Edid { data, size }
217    }
218
219    /// Extract the DTD1 bytes from the QEMU EDID for direct DetailedTiming tests.
220    fn qemu_dtd1_bytes() -> [u8; DTD_LEN] {
221        let data = qemu_edid();
222        data[DTD1_OFFSET..DTD1_OFFSET + DTD_LEN].try_into().unwrap()
223    }
224
225    // ---- AspectRatio tests ----
226
227    #[test]
228    fn aspect_ratio_from_bits() {
229        assert_eq!(AspectRatio::from_bits(0), AspectRatio::Ratio16x10);
230        assert_eq!(AspectRatio::from_bits(1), AspectRatio::Ratio4x3);
231        assert_eq!(AspectRatio::from_bits(2), AspectRatio::Ratio5x4);
232        assert_eq!(AspectRatio::from_bits(3), AspectRatio::Ratio16x9);
233    }
234
235    #[test]
236    fn aspect_ratio_v_pixels() {
237        assert_eq!(AspectRatio::Ratio16x10.v_pixels(1920), 1200);
238        assert_eq!(AspectRatio::Ratio4x3.v_pixels(1600), 1200);
239        assert_eq!(AspectRatio::Ratio5x4.v_pixels(1280), 1024);
240        assert_eq!(AspectRatio::Ratio16x9.v_pixels(1920), 1080);
241    }
242
243    // ---- StandardTiming tests ----
244
245    #[test]
246    fn standard_timing_unused_entry() {
247        assert!(StandardTiming::parse(&[0x01, 0x01]).is_none());
248    }
249
250    #[test]
251    fn standard_timing_known_entry() {
252        // First standard timing from QEMU EDID: 0xe1, 0xc0
253        // h_pixels = (0xe1 + 31) * 8 = (225 + 31) * 8 = 2048
254        // aspect bits = 0xc0 >> 6 = 3 => 16:9
255        // v_pixels = 2048 * 9 / 16 = 1152
256        let st = StandardTiming::parse(&[0xe1, 0xc0]).unwrap();
257        assert_eq!(st.h_pixels, 2048);
258        assert_eq!(st.v_pixels, 1152);
259    }
260
261    #[test]
262    fn standard_timing_each_aspect_ratio() {
263        // 16:10: byte1 bits 7:6 = 0b00
264        let st = StandardTiming::parse(&[0xd1, 0x00]).unwrap();
265        assert_eq!((st.h_pixels, st.v_pixels), (1920, 1200));
266
267        // 4:3: byte1 bits 7:6 = 0b01
268        let st = StandardTiming::parse(&[0xa9, 0x40]).unwrap();
269        assert_eq!((st.h_pixels, st.v_pixels), (1600, 1200));
270
271        // 5:4: byte1 bits 7:6 = 0b10
272        let st = StandardTiming::parse(&[0x81, 0x80]).unwrap();
273        assert_eq!((st.h_pixels, st.v_pixels), (1280, 1024));
274
275        // 16:9: byte1 bits 7:6 = 0b11
276        let st = StandardTiming::parse(&[0xd1, 0xc0]).unwrap();
277        assert_eq!((st.h_pixels, st.v_pixels), (1920, 1080));
278    }
279
280    // ---- DetailedTiming tests ----
281
282    #[test]
283    fn detailed_timing_zeroed() {
284        assert!(DetailedTiming::parse(&[0u8; DTD_LEN]).is_none());
285    }
286
287    #[test]
288    fn detailed_timing_known_1920x1080() {
289        let dtd = DetailedTiming::parse(&qemu_dtd1_bytes()).unwrap();
290        assert_eq!(dtd.h_active, 1920);
291        assert_eq!(dtd.v_active, 1080);
292    }
293
294    #[test]
295    fn detailed_timing_h_active_uses_upper_nibble() {
296        // Construct a DTD where h_active requires the upper nibble.
297        // h_active_low = 0x00, upper nibble of byte 4 = 0x50 => h_active = 0x500 = 1280
298        // v_active_low = 0x00, upper nibble of byte 7 = 0x40 => v_active = 0x400 = 1024
299        let mut bytes = [0u8; DTD_LEN];
300        bytes[0] = 0x01; // non-zero pixel clock so it's not a display descriptor
301        bytes[2] = 0x00; // h_active low = 0
302        bytes[4] = 0x50; // h_active upper nibble = 5, h_blanking upper nibble = 0
303        bytes[5] = 0x00; // v_active low = 0
304        bytes[7] = 0x40; // v_active upper nibble = 4, v_blanking upper nibble = 0
305        let dtd = DetailedTiming::parse(&bytes).unwrap();
306        assert_eq!(dtd.h_active, 1280);
307        assert_eq!(dtd.v_active, 1024);
308    }
309
310    // ---- Edid tests ----
311
312    #[test]
313    fn edid_size_exactly_128() {
314        let edid = make_edid(qemu_edid(), 128);
315        assert!(edid.preferred_resolution().is_ok());
316        assert!(!edid.standard_timings().is_empty());
317    }
318
319    #[test]
320    fn edid_size_127_fails() {
321        let edid = make_edid(qemu_edid(), 127);
322        assert!(edid.preferred_resolution().is_err());
323        assert!(edid.standard_timings().is_empty());
324    }
325
326    #[test]
327    fn qemu_edid_preferred_resolution() {
328        let edid = make_edid(qemu_edid(), 256);
329        let (w, h) = edid.preferred_resolution().unwrap();
330        assert_eq!((w, h), (1920, 1080));
331    }
332
333    #[test]
334    fn qemu_edid_standard_timings() {
335        let edid = make_edid(qemu_edid(), 256);
336        let res = edid.standard_timings();
337        // QEMU advertises 8 standard timings, sorted by total pixel count (largest first).
338        // Note: 1600x1200 (1,920,000 px) > 1680x1050 (1,764,000 px) despite narrower width,
339        // and 1280x1024 (1,310,720 px) > 1440x900 (1,296,000 px) for the same reason.
340        assert_eq!(
341            res,
342            vec![
343                (2048, 1152), // 16:9,  2,359,296 px
344                (1920, 1200), // 16:10, 2,304,000 px
345                (1920, 1080), // 16:9,  2,073,600 px
346                (1600, 1200), // 4:3,   1,920,000 px
347                (1680, 1050), // 16:10, 1,764,000 px
348                (1280, 1024), // 5:4,   1,310,720 px
349                (1440, 900),  // 16:10, 1,296,000 px
350                (1280, 960),  // 4:3,   1,228,800 px
351            ]
352        );
353    }
354
355    #[test]
356    fn qemu_edid_highest_resolution_is_2048x1152() {
357        let edid = make_edid(qemu_edid(), 256);
358        let res = edid.standard_timings();
359        assert_eq!(res.first(), Some(&(2048, 1152)));
360    }
361
362    #[test]
363    fn preferred_resolution_too_short() {
364        let edid = make_edid([0u8; 1024], 64);
365        assert!(edid.preferred_resolution().is_err());
366    }
367
368    #[test]
369    fn preferred_resolution_zeroed_active_pixels() {
370        // Zero out horizontal and vertical active pixels in DTD1
371        let mut data = qemu_edid();
372        data[0x38] = 0x00; // h_active low
373        data[0x3A] &= 0x0F; // h_active high nibble = 0
374        data[0x3B] = 0x00; // v_active low
375        data[0x3D] &= 0x0F; // v_active high nibble = 0
376        let edid = make_edid(data, 256);
377        assert!(edid.preferred_resolution().is_err());
378    }
379
380    #[test]
381    fn standard_timings_too_short() {
382        let edid = make_edid([0u8; 1024], 32);
383        let res = edid.standard_timings();
384        assert!(res.is_empty());
385    }
386
387    #[test]
388    fn standard_timings_all_unused() {
389        // Overwrite all standard timing slots with 0x0101 (unused marker)
390        let mut data = qemu_edid();
391        for i in 0..8 {
392            data[38 + i * 2] = 0x01;
393            data[38 + i * 2 + 1] = 0x01;
394        }
395        let edid = make_edid(data, 256);
396        let res = edid.standard_timings();
397        assert!(res.is_empty());
398    }
399
400    #[test]
401    fn standard_timings_partial_entries() {
402        // Keep only the first two standard timing entries, mark rest unused
403        let mut data = qemu_edid();
404        for i in 2..8 {
405            data[38 + i * 2] = 0x01;
406            data[38 + i * 2 + 1] = 0x01;
407        }
408        let edid = make_edid(data, 256);
409        let res = edid.standard_timings();
410        // First two entries from QEMU: 2048x1152 (16:9) and 1920x1080 (16:9)
411        assert_eq!(res, vec![(2048, 1152), (1920, 1080)]);
412    }
413}