From 85dd75437bd93930dd234285e58b5b2f884d1d07 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 30 Jul 2026 23:12:43 +0200 Subject: [PATCH] feat(driver-proto): an EDID detailed-timing builder for the session's mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An 18-byte DTD with fixed reduced blanking (80 px / 45 lines — a virtual display's blanking only has to be self-consistent), refusing modes the encoding cannot carry (pixel clock past the u16 10 kHz field, 4K120-class; >12-bit actives). Lives here rather than in the driver so it is unit- tested on every platform; pf-vdisplay splices it over its hard-coded 1080p60 preferred-timing descriptor. Co-Authored-By: Claude Fable 5 --- crates/pf-driver-proto/src/lib.rs | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/pf-driver-proto/src/lib.rs b/crates/pf-driver-proto/src/lib.rs index 7f00b695..d7cd32a8 100644 --- a/crates/pf-driver-proto/src/lib.rs +++ b/crates/pf-driver-proto/src/lib.rs @@ -477,6 +477,52 @@ pub mod edid { }; cv.min(255) as u8 } + + /// Fixed reduced-blanking geometry for [`dtd`] (CVT-RBv2-shaped): 80 px of horizontal and 45 + /// lines of vertical blanking, front-porch/sync splits within them. A virtual display has no + /// real scan-out, so the blanking only has to be self-consistent — the pixel clock is derived + /// from these same totals. + const DTD_H_BLANK: u32 = 80; + const DTD_V_BLANK: u32 = 45; + const DTD_H_SYNC_OFFSET: u32 = 8; + const DTD_H_SYNC_WIDTH: u32 = 32; + const DTD_V_SYNC_OFFSET: u32 = 3; + const DTD_V_SYNC_WIDTH: u32 = 5; + + /// An 18-byte EDID detailed timing descriptor for `width`×`height`@`refresh_hz` with the fixed + /// reduced blanking above — the pf-vdisplay EDID's PREFERRED timing, built from the mode the + /// session actually asked for instead of a hard-coded 1080p60. `None` when the mode does not + /// FIT the DTD encoding — pixel clock above 655.35 MHz (the u16 10 kHz field: 4K120-class) or + /// active dimensions above the 12-bit fields — in which case the caller keeps its + /// representable fallback descriptor. Flags byte: digital separate sync, +H/+V (0x1E, matching + /// the previous hard-coded DTD). + pub fn dtd(width: u32, height: u32, refresh_hz: u32) -> Option<[u8; 18]> { + if width == 0 || height == 0 || refresh_hz == 0 || width > 4095 || height > 4095 { + return None; + } + let h_total = u64::from(width + DTD_H_BLANK); + let v_total = u64::from(height + DTD_V_BLANK); + let clock_10khz = h_total * v_total * u64::from(refresh_hz) / 10_000; + let clock_10khz = u16::try_from(clock_10khz).ok()?; + let mut d = [0u8; 18]; + d[0..2].copy_from_slice(&clock_10khz.to_le_bytes()); + d[2] = (width & 0xFF) as u8; + d[3] = (DTD_H_BLANK & 0xFF) as u8; + d[4] = (((width >> 8) & 0x0F) << 4) as u8 | ((DTD_H_BLANK >> 8) & 0x0F) as u8; + d[5] = (height & 0xFF) as u8; + d[6] = (DTD_V_BLANK & 0xFF) as u8; + d[7] = (((height >> 8) & 0x0F) << 4) as u8 | ((DTD_V_BLANK >> 8) & 0x0F) as u8; + d[8] = (DTD_H_SYNC_OFFSET & 0xFF) as u8; + d[9] = (DTD_H_SYNC_WIDTH & 0xFF) as u8; + d[10] = (((DTD_V_SYNC_OFFSET & 0x0F) << 4) | (DTD_V_SYNC_WIDTH & 0x0F)) as u8; + d[11] = ((((DTD_H_SYNC_OFFSET >> 8) & 0x03) << 6) + | (((DTD_H_SYNC_WIDTH >> 8) & 0x03) << 4) + | (((DTD_V_SYNC_OFFSET >> 4) & 0x03) << 2) + | ((DTD_V_SYNC_WIDTH >> 4) & 0x03)) as u8; + // Bytes 12..17 (image size mm, borders) stay 0 = undefined, matching the previous DTD. + d[17] = 0x1E; + Some(d) + } } /// The IDD-push frame transport: the host-created shared ring header, the publish token, and the @@ -1454,6 +1500,31 @@ mod tests { use super::*; use bytemuck::Zeroable; + #[test] + fn dtd_encodes_the_session_mode() { + // 1920×1080@60 with the fixed RB blanking: totals 2000×1125 → 135.00 MHz = 13500 ×10 kHz. + let d = edid::dtd(1920, 1080, 60).expect("1080p60 fits the DTD encoding"); + assert_eq!(u16::from_le_bytes([d[0], d[1]]), 13_500); + // Active dimensions round-trip through the split 8+4-bit fields. + assert_eq!(u32::from(d[2]) | (u32::from(d[4] >> 4) << 8), 1920); + assert_eq!(u32::from(d[5]) | (u32::from(d[7] >> 4) << 8), 1080); + // Blanking fields carry the fixed geometry; flags match the legacy descriptor. + assert_eq!(u32::from(d[3]) | (u32::from(d[4] & 0x0F) << 8), 80); + assert_eq!(u32::from(d[6]) | (u32::from(d[7] & 0x0F) << 8), 45); + assert_eq!(d[17], 0x1E); + } + + #[test] + fn dtd_rejects_what_the_encoding_cannot_carry() { + // 4K120: (3840+80)·(2160+45)·120 ≈ 1.037 GHz — past the u16 10 kHz pixel-clock field. + assert_eq!(edid::dtd(3840, 2160, 120), None); + // 4K60 fits (≈518 MHz). + assert!(edid::dtd(3840, 2160, 60).is_some()); + // Degenerate and over-wide modes are refused, not mis-encoded. + assert_eq!(edid::dtd(0, 1080, 60), None); + assert_eq!(edid::dtd(5000, 1080, 10), None); + } + #[test] fn frame_token_roundtrips() { for (g, s, slot) in [