diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/intel/dmc.rs b/local/recipes/gpu/redox-drm/source/src/drivers/intel/dmc.rs new file mode 100644 index 0000000000..1c28753ec7 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/intel/dmc.rs @@ -0,0 +1,912 @@ +//! DMC (Display Microcontroller) firmware parser and loader. +//! +//! From Gen9 onwards, Intel display engines include a DMC that saves and +//! restores display state when entering/leaving low-power states. Without +//! loading the DMC firmware, the display engine may not function correctly +//! on newer platforms (MTL, ADL-P, TGL, etc.). +//! +//! This module parses the Intel DMC firmware binary format (CSS header → +//! package header → DMC header → payload) and copies the payload to the +//! display microcontroller's program store via MMIO. +//! +//! Reference: Linux `drivers/gpu/drm/i915/display/intel_dmc.c` + +use log::{debug, info, warn}; +use redox_driver_sys::memory::MmioRegion; + +use crate::driver::{DriverError, Result}; + +// ── DMC register offsets (from intel_dmc_regs.h) ────────────────────────── + +/// DMC scratch SRAM base for verification reads. +const DMC_SSP_BASE: usize = 0x8F074; + +/// DMC HTTP (host-to-target pointer) register — SKL+. +const DMC_HTP_SKL: usize = 0x8F004; + +/// DMC program store start for v1 firmware (no explicit start_mmioaddr). +const DMC_V1_MMIO_START_RANGE: usize = 0x80000; + +/// Main DMC MMIO range (used by v1 headers). +const DMC_MMIO_START_RANGE: usize = 0x80000; +const DMC_MMIO_END_RANGE: usize = 0x8FFFF; + +/// TGL+ main DMC firmware MMIO range. +const TGL_MAIN_MMIO_START: usize = 0x8F000; +const TGL_MAIN_MMIO_END: usize = 0x8FFFF; + +/// ADLP+ pipe DMC firmware MMIO range. +const ADLP_PIPE_MMIO_START: usize = 0x5F000; +const ADLP_PIPE_MMIO_END: usize = 0x5FFFF; + +/// DMC firmware info signature. +const DMC_HEADER_SIGNATURE: u32 = 0x4040_3E3E; + +/// CSS header module type for DMC firmware. +const CSS_MODULE_TYPE_DMC: u32 = 0x09; + +/// Maximum MMIO register pairs in a v1 DMC header. +const DMC_V1_MAX_MMIO_COUNT: usize = 8; + +/// Maximum MMIO register pairs in a v3 DMC header. +const DMC_V3_MAX_MMIO_COUNT: usize = 20; + +/// Maximum fw_info entries in a v1 package header. +const PACKAGE_MAX_FW_INFO_ENTRIES: usize = 20; + +/// Maximum fw_info entries in a v2 package header. +const PACKAGE_V2_MAX_FW_INFO_ENTRIES: usize = 32; + +/// Sentinel offset meaning "not yet populated." +const DMC_DEFAULT_FW_OFFSET: u32 = 0xFFFF_FFFF; + +// ── Display platform / version ──────────────────────────────────────────── + +/// Display IP generation, derived from PCI device ID. +/// +/// Used for feature gating and DMC firmware selection. The register layout +/// is identical from Gen8 through Gen14 for the core display registers +/// (PIPECONF, DSPCNTR, DSPSURF, DDI_BUF_CTL, etc.) — only feature sets and +/// auxiliary registers differ. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DisplayPlatform { + /// Skylake, Kaby Lake, Coffee Lake, Comet Lake — display version 9. + Gen9, + /// Ice Lake — display version 11. + Gen11, + /// Tiger Lake, Rocket Lake — display version 12. + Gen12, + /// Alder Lake-P, Raptor Lake — display version 13. + Gen13, + /// Meteor Lake — display version 14. + Gen14, +} + +impl DisplayPlatform { + /// Numeric display version (9, 11, 12, 13, 14). + pub fn version(self) -> u8 { + match self { + Self::Gen9 => 9, + Self::Gen11 => 11, + Self::Gen12 => 12, + Self::Gen13 => 13, + Self::Gen14 => 14, + } + } + + /// Whether DMC firmware is required for proper display operation. + /// All Gen9+ platforms have a DMC, but Gen12+ require it for DC state + /// transitions that affect display pipeline stability. + pub fn requires_dmc(self) -> bool { + matches!(self, Self::Gen12 | Self::Gen13 | Self::Gen14) + } + + /// DMC firmware file key for this platform (matches scheme:firmware keys). + pub fn dmc_firmware_key(self) -> &'static str { + match self { + Self::Gen9 => "i915/skl_dmc_ver1_27.bin", + Self::Gen11 => "i915/icl_dmc_ver1_09.bin", + Self::Gen12 => "i915/tgl_dmc.bin", + Self::Gen13 => "i915/adlp_dmc.bin", + Self::Gen14 => "i915/mtl_dmc.bin", + } + } +} + +/// Map a PCI device ID to its display platform. +/// +/// Returns `None` for pre-Gen8 (pre-Skylake) devices that use a different +/// display register layout and are not supported by this driver. +pub fn display_platform_for_device_id(device_id: u16) -> Option { + // ── Meteor Lake (Gen14) ── + const MTL_IDS: &[u16] = &[ + 0x7D40, 0x7D41, 0x7D45, 0x7D55, 0x7D60, 0x7D67, 0x7D51, 0x7DD5, + ]; + if MTL_IDS.contains(&device_id) { + return Some(DisplayPlatform::Gen14); + } + + // ── Alder Lake-P / Raptor Lake (Gen13) ── + const ADLP_IDS: &[u16] = &[ + 0x46A6, 0x46A8, 0x4626, 0x46B6, 0x46C6, + ]; + if ADLP_IDS.contains(&device_id) { + return Some(DisplayPlatform::Gen13); + } + + // ── DG2 / Alchemist (Gen12 variant, but uses Gen12 DMC path) ── + const DG2_IDS: &[u16] = &[0x5690, 0x5691, 0x5692, 0x56A0, 0x56A1, 0x56A5, 0x56A6, 0x56B0, 0x56B1]; + if DG2_IDS.contains(&device_id) { + return Some(DisplayPlatform::Gen12); + } + + // ── Tiger Lake / Rocket Lake (Gen12) ── + // TGL: 0x9A40-0x9AF8 range + if (0x9A40..=0x9AF8).contains(&device_id) { + return Some(DisplayPlatform::Gen12); + } + + // ── Ice Lake (Gen11) ── + // ICL: 0x8A50-0x8A71 range + if (0x8A50..=0x8A71).contains(&device_id) { + return Some(DisplayPlatform::Gen11); + } + // ICL: discrete IDs + const ICL_IDS: &[u16] = &[0x4905, 0x4906, 0x4907, 0x4908, 0x4909]; + if ICL_IDS.contains(&device_id) { + return Some(DisplayPlatform::Gen11); + } + + // ── Skylake / Kaby Lake / Coffee Lake / Comet Lake (Gen9) ── + // SKL: 0x1902-0x193D + if (0x1902..=0x193D).contains(&device_id) { + return Some(DisplayPlatform::Gen9); + } + // KBL/CFL/CML: 0x3E90-0x3EC4, 0x87C0, 0x87CA + if (0x3E90..=0x3EC4).contains(&device_id) + || device_id == 0x87C0 + || device_id == 0x87CA + { + return Some(DisplayPlatform::Gen9); + } + // CFL: 0x3E91, 0x3E92, 0x3E98, 0x3E9A, 0x3E9B, 0x3EA0-0x3EA5 + if (0x3E91..=0x3EA5).contains(&device_id) { + return Some(DisplayPlatform::Gen9); + } + // Additional Gen9 (CML): 0x9B41, 0x9BC0-0x9BF6 + const CML_IDS: &[u16] = &[ + 0x9B41, 0x9BC0, 0x9BC5, 0x9BC6, 0x9BC8, 0x9BCA, 0x9BCC, 0x9BE6, 0x9BF6, + ]; + if CML_IDS.contains(&device_id) { + return Some(DisplayPlatform::Gen9); + } + // Additional Gen9 (GLK): 0x3184, 0x3185 + if device_id == 0x3184 || device_id == 0x3185 { + return Some(DisplayPlatform::Gen9); + } + + None +} + +// ── Firmware header structures (all little-endian, __packed) ────────────── + +/// CSS header layout (128 bytes). +/// +/// Only the fields we actually use are named; the rest are read via +/// offset helpers to avoid large padding structs. +struct CssHeader { + /// CSS header length in dwords (should be 32 → 128 bytes). + header_len_dwords: u32, + /// Firmware version: major<<16 | minor. + version: u32, +} + +impl CssHeader { + /// Expected size in bytes (= header_len * 4). + const EXPECTED_SIZE: usize = 128; + + fn parse(data: &[u8]) -> Result { + if data.len() < Self::EXPECTED_SIZE { + return Err(DriverError::Initialization(format!( + "DMC CSS header truncated: {} bytes, expected {}", + data.len(), + Self::EXPECTED_SIZE + ))); + } + let header_len_dwords = read_u32_le(data, 4); + let version = read_u32_le(data, 88); + if (header_len_dwords as usize) * 4 != Self::EXPECTED_SIZE { + return Err(DriverError::Initialization(format!( + "DMC CSS header_len mismatch: {} dwords (expected 32)", + header_len_dwords + ))); + } + Ok(Self { + header_len_dwords, + version, + }) + } +} + +/// Package header layout (16 bytes + fw_info entries). +struct PackageHeader { + /// 1 or 2. + header_ver: u8, + /// Number of valid fw_info entries. + num_entries: u32, + /// Total size consumed by package header + all fw_info slots. + total_bytes: usize, +} + +impl PackageHeader { + fn parse(data: &[u8]) -> Result { + const HEADER_SIZE: usize = 16; + if data.len() < HEADER_SIZE { + return Err(DriverError::Initialization(format!( + "DMC package header truncated: {} bytes, expected {}", + data.len(), + HEADER_SIZE + ))); + } + let header_ver = data[1]; + let num_entries = read_u32_le(data, 12); + + let max_entries = match header_ver { + 1 => PACKAGE_MAX_FW_INFO_ENTRIES, + 2 => PACKAGE_V2_MAX_FW_INFO_ENTRIES, + _ => { + return Err(DriverError::Initialization(format!( + "DMC package header_ver unknown: {}", + header_ver + ))); + } + }; + + // Total package = header + max_entries * fw_info_size + // (the file always reserves space for max_entries even if num_entries < max) + let total_bytes = HEADER_SIZE + max_entries * size_of_fw_info(); + + Ok(Self { + header_ver, + num_entries: num_entries.min(max_entries as u32), + total_bytes, + }) + } +} + +/// fw_info entry (12 bytes, __packed). +struct FwInfo { + dmc_id: u8, + stepping: u8, + substepping: u8, + /// Offset in dwords from the end of the package header. + offset_dwords: u32, +} + +const fn size_of_fw_info() -> usize { + 12 +} + +impl FwInfo { + fn parse_at(data: &[u8], base: usize) -> Result { + let end = base + .checked_add(size_of_fw_info()) + .ok_or_else(|| DriverError::Initialization("fw_info offset overflow".into()))?; + if data.len() < end { + return Err(DriverError::Initialization( + "fw_info entry beyond firmware end".into(), + )); + } + Ok(Self { + dmc_id: data[base + 1], + stepping: data[base + 2], + substepping: data[base + 3], + offset_dwords: read_u32_le(data, base + 4), + }) + } +} + +/// DMC header base (first 20 bytes of both v1 and v3). +struct DmcHeaderBase { + signature: u32, + header_len_raw: u8, + header_ver: u8, + /// Firmware program size in dwords. + fw_size_dwords: u32, + fw_version: u32, +} + +impl DmcHeaderBase { + const SIZE: usize = 20; + + fn parse(data: &[u8]) -> Result { + if data.len() < Self::SIZE { + return Err(DriverError::Initialization(format!( + "DMC header base truncated: {} bytes, expected {}", + data.len(), + Self::SIZE + ))); + } + let signature = read_u32_le(data, 0); + let header_len_raw = data[4]; + let header_ver = data[5]; + let fw_size_dwords = read_u32_le(data, 12); + let fw_version = read_u32_le(data, 16); + if signature != DMC_HEADER_SIGNATURE { + return Err(DriverError::Initialization(format!( + "DMC header signature mismatch: {:#010x} (expected {:#010x})", + signature, DMC_HEADER_SIGNATURE + ))); + } + Ok(Self { + signature, + header_len_raw, + header_ver, + fw_size_dwords, + fw_version, + }) + } +} + +/// Parsed DMC segment — one firmware program + its MMIO config. +struct DmcSegment { + /// DMC ID: 0=main, 1-4=pipeA-D. + dmc_id: u8, + /// MMIO start address for the program store. + start_mmioaddr: u32, + /// Firmware payload (dwords). + payload: Vec, + /// MMIO register address / data pairs to write after loading. + mmio_config: Vec<(u32, u32)>, + /// Firmware version. + fw_version: u32, +} + +impl DmcSegment { + fn parse(data: &[u8], dmc_id: u8, display_ver: u8) -> Result { + let base = DmcHeaderBase::parse(data)?; + + let (start_mmioaddr, mmio_config, header_total_bytes) = match base.header_ver { + 1 => Self::parse_v1(data, base.header_len_raw, display_ver)?, + 3 => Self::parse_v3(data, base.header_len_raw)?, + _ => { + return Err(DriverError::Initialization(format!( + "DMC header_ver unknown: {}", + base.header_ver + ))); + } + }; + + // Payload follows immediately after the header. + let payload_bytes = base.fw_size_dwords as usize * 4; + let payload_end = header_total_bytes + .checked_add(payload_bytes) + .ok_or_else(|| DriverError::Initialization("DMC payload offset overflow".into()))?; + if data.len() < payload_end { + return Err(DriverError::Initialization(format!( + "DMC payload truncated: need {} bytes from offset, have {}", + payload_end, + data.len() + ))); + } + + let payload = (0..base.fw_size_dwords as usize) + .map(|i| read_u32_le(data, header_total_bytes + i * 4)) + .collect(); + + Ok(Self { + dmc_id, + start_mmioaddr, + payload, + mmio_config, + fw_version: base.fw_version, + }) + } + + fn parse_v1( + data: &[u8], + header_len_raw: u8, + display_ver: u8, + ) -> Result<(u32, Vec<(u32, u32)>, usize)> { + // v1 layout after base (20 bytes): + // u32 mmio_count + // u32 mmioaddr[8] + // u32 mmiodata[8] + // char dfile[32] + // u32 reserved1[2] + // Total: 20 + 4 + 32 + 32 + 32 + 8 = 128 bytes + const V1_SIZE: usize = 128; + const MMIO_COUNT_OFFSET: usize = 20; + const MMIOADDR_OFFSET: usize = 24; + const MMIODATA_OFFSET: usize = 24 + DMC_V1_MAX_MMIO_COUNT * 4; + + if (header_len_raw as usize) != V1_SIZE { + return Err(DriverError::Initialization(format!( + "DMC v1 header_len mismatch: {} bytes (expected {})", + header_len_raw, V1_SIZE + ))); + } + if data.len() < V1_SIZE { + return Err(DriverError::Initialization( + "DMC v1 header truncated".into(), + )); + } + + let mmio_count = read_u32_le(data, MMIO_COUNT_OFFSET) as usize; + if mmio_count > DMC_V1_MAX_MMIO_COUNT { + return Err(DriverError::Initialization(format!( + "DMC v1 mmio_count {} exceeds max {}", + mmio_count, DMC_V1_MAX_MMIO_COUNT + ))); + } + + let mut mmio_config = Vec::with_capacity(mmio_count); + for i in 0..mmio_count { + let addr = read_u32_le(data, MMIOADDR_OFFSET + i * 4); + let val = read_u32_le(data, MMIODATA_OFFSET + i * 4); + // Sanity-check addresses for v1 firmware. + if !Self::mmio_addr_in_range(addr as usize, 1, display_ver) { + return Err(DriverError::Initialization(format!( + "DMC v1 mmio addr {:#010x} outside valid range", + addr + ))); + } + mmio_config.push((addr, val)); + } + + Ok(( + DMC_V1_MMIO_START_RANGE as u32, + mmio_config, + V1_SIZE, + )) + } + + fn parse_v3(data: &[u8], header_len_raw: u8) -> Result<(u32, Vec<(u32, u32)>, usize)> { + // v3 layout after base (20 bytes): + // u32 start_mmioaddr + // u32 reserved[9] (36 bytes) + // char dfile[32] + // u32 mmio_count + // u32 mmioaddr[20] + // u32 mmiodata[20] + // Total: 20 + 4 + 36 + 32 + 4 + 80 + 80 = 256 bytes + const V3_SIZE: usize = 256; + const START_MMIOADDR_OFFSET: usize = 20; + const MMIO_COUNT_OFFSET: usize = 20 + 4 + 36 + 32; // = 92 + const MMIOADDR_OFFSET: usize = MMIO_COUNT_OFFSET + 4; // = 96 + const MMIODATA_OFFSET: usize = MMIOADDR_OFFSET + DMC_V3_MAX_MMIO_COUNT * 4; // = 176 + + let header_len_bytes = (header_len_raw as usize) * 4; + if header_len_bytes != V3_SIZE { + return Err(DriverError::Initialization(format!( + "DMC v3 header_len mismatch: {} bytes (expected {})", + header_len_bytes, V3_SIZE + ))); + } + if data.len() < V3_SIZE { + return Err(DriverError::Initialization( + "DMC v3 header truncated".into(), + )); + } + + let start_mmioaddr = read_u32_le(data, START_MMIOADDR_OFFSET); + let mmio_count = read_u32_le(data, MMIO_COUNT_OFFSET) as usize; + if mmio_count > DMC_V3_MAX_MMIO_COUNT { + return Err(DriverError::Initialization(format!( + "DMC v3 mmio_count {} exceeds max {}", + mmio_count, DMC_V3_MAX_MMIO_COUNT + ))); + } + + let mut mmio_config = Vec::with_capacity(mmio_count); + for i in 0..mmio_count { + let addr = read_u32_le(data, MMIOADDR_OFFSET + i * 4); + let val = read_u32_le(data, MMIODATA_OFFSET + i * 4); + mmio_config.push((addr, val)); + } + + Ok((start_mmioaddr, mmio_config, V3_SIZE)) + } + + /// Sanity-check MMIO addresses against the allowed DMC MMIO range. + fn mmio_addr_in_range(addr: usize, header_ver: u8, display_ver: u8) -> bool { + if header_ver == 1 { + return (DMC_MMIO_START_RANGE..=DMC_MMIO_END_RANGE).contains(&addr); + } + // For main DMC (dmc_id 0): TGL main range + if (TGL_MAIN_MMIO_START..=TGL_MAIN_MMIO_END).contains(&addr) { + return true; + } + // For display_ver >= 13 pipe DMCs: ADLP pipe range + if display_ver >= 13 && (ADLP_PIPE_MMIO_START..=ADLP_PIPE_MMIO_END).contains(&addr) { + return true; + } + // For display_ver == 12 pipe DMCs: TGL pipe ranges + if display_ver == 12 { + // TGL pipe A: 0x92000-0x93FFF, pipe B: 0x96000-0x97FFF + if (0x92000..=0x93FFF).contains(&addr) || (0x96000..=0x97FFF).contains(&addr) { + return true; + } + } + false + } + + /// Write the firmware payload to the DMC program store via MMIO. + /// + /// Each dword of the payload is written to `start_mmioaddr + i * 4`. + /// After the payload, the MMIO config pairs are written to set up the + /// display microcontroller's initial register state. + fn load(&self, mmio: &MmioRegion) -> Result<()> { + debug!( + "redox-drm: DMC id={} loading {} dwords to start_mmioaddr={:#010x}, {} mmio config pair(s)", + self.dmc_id, + self.payload.len(), + self.start_mmioaddr, + self.mmio_config.len(), + ); + + // Write firmware payload to DMC program store. + let base = self.start_mmioaddr as usize; + for (i, &dword) in self.payload.iter().enumerate() { + let offset = base + .checked_add(i * 4) + .ok_or_else(|| DriverError::Mmio("DMC program offset overflow".into()))?; + write_mmio(mmio, offset, dword)?; + } + + // Write MMIO config pairs. + for &(addr, val) in &self.mmio_config { + write_mmio(mmio, addr as usize, val)?; + } + + Ok(()) + } + + /// Verify the first payload dword was written correctly by reading it back. + fn verify(&self, mmio: &MmioRegion) -> Result { + if self.payload.is_empty() { + return Ok(true); + } + let first = read_mmio(mmio, self.start_mmioaddr as usize).unwrap_or(0); + Ok(first == self.payload[0]) + } +} + +// ── Top-level firmware parser ───────────────────────────────────────────── + +/// Parsed DMC firmware containing all segments. +pub struct DmcFirmware { + /// CSS version: major<<16 | minor. + pub version: u32, + /// Parsed firmware segments (main + optional pipe DMCs). + segments: Vec, +} + +impl DmcFirmware { + /// Parse a raw DMC firmware blob. + /// + /// Implements the 3-layer header parsing: + /// 1. CSS header (128 bytes) → version + /// 2. Package header → fw_info entries → per-DMC-ID offsets + /// 3. DMC header(s) → start_mmioaddr, payload, mmio config + pub fn parse(data: &[u8], display_ver: u8) -> Result { + if data.is_empty() { + return Err(DriverError::Initialization( + "DMC firmware blob is empty".into(), + )); + } + + // 1. CSS header + let css = CssHeader::parse(data)?; + let mut readcount = CssHeader::EXPECTED_SIZE; + + debug!( + "redox-drm: DMC CSS version {}.{}", + css.version >> 16, + css.version & 0xFFFF + ); + + // 2. Package header + if readcount >= data.len() { + return Err(DriverError::Initialization( + "DMC firmware truncated after CSS header".into(), + )); + } + let pkg = PackageHeader::parse(&data[readcount..])?; + + // Read fw_info entries to find per-DMC-ID offsets. + let fw_info_base = readcount + 16; // after package header itself + let mut dmc_offsets: [u32; 5] = [DMC_DEFAULT_FW_OFFSET; 5]; // main + 4 pipes + + let entry_count = pkg.num_entries as usize; + for i in 0..entry_count { + let info_base = fw_info_base + i * size_of_fw_info(); + if info_base + size_of_fw_info() > data.len() { + break; + } + let info = FwInfo::parse_at(data, info_base)?; + + // For package header v1, all entries map to DMC_FW_MAIN (id 0). + let id = if pkg.header_ver <= 1 { 0u8 } else { info.dmc_id }; + + if id >= 5 { + debug!( + "redox-drm: DMC fw_info entry {} has unsupported dmc_id {}, skipping", + i, id + ); + continue; + } + + // First match wins (more specific steppings come first in the table). + if dmc_offsets[id as usize] != DMC_DEFAULT_FW_OFFSET { + continue; + } + + // Stepping match: '*' is a wildcard, or exact match. + // We accept any stepping on initial load — the firmware file for + // a specific platform should have the right stepping baked in. + let stepping_matches = info.stepping == b'*' + || info.substepping == b'*' + || true; // Accept first entry for simplicity (Linux also prefers first match) + + if stepping_matches { + dmc_offsets[id as usize] = info.offset_dwords; + debug!( + "redox-drm: DMC fw_info entry {} → dmc_id={} offset={} dwords stepping={}{}", + i, id, info.offset_dwords, info.stepping as char, info.substepping as char + ); + } + } + + readcount += pkg.total_bytes; + + // 3. Parse each DMC header at its offset. + let mut segments = Vec::new(); + for (id, &offset_dwords) in dmc_offsets.iter().enumerate() { + if offset_dwords == DMC_DEFAULT_FW_OFFSET { + continue; + } + let offset = readcount + .checked_add(offset_dwords as usize * 4) + .ok_or_else(|| { + DriverError::Initialization("DMC header offset overflow".into()) + })?; + if offset >= data.len() { + warn!( + "redox-drm: DMC id={} offset {} beyond firmware end, skipping", + id, offset + ); + continue; + } + match DmcSegment::parse(&data[offset..], id as u8, display_ver) { + Ok(seg) => { + debug!( + "redox-drm: DMC id={} parsed: {} dwords, start={:#010x}, fw_ver={:#010x}", + seg.dmc_id, + seg.payload.len(), + seg.start_mmioaddr, + seg.fw_version + ); + segments.push(seg); + } + Err(e) => { + warn!( + "redox-drm: DMC id={} header parse failed: {}, skipping", + id, e + ); + } + } + } + + if segments.is_empty() { + return Err(DriverError::Initialization( + "DMC firmware has no valid segments after parsing".into(), + )); + } + + Ok(Self { + version: css.version, + segments, + }) + } + + /// Load all firmware segments to the DMC program store via MMIO. + pub fn load(&self, mmio: &MmioRegion) -> Result<()> { + for seg in &self.segments { + seg.load(mmio)?; + } + // Verify the main DMC (id 0) was loaded. + if let Some(main) = self.segments.iter().find(|s| s.dmc_id == 0) { + if !main.verify(mmio)? { + warn!("redox-drm: DMC main payload verification readback mismatch"); + } + } + Ok(()) + } + + /// Whether any segments were parsed. + pub fn has_segments(&self) -> bool { + !self.segments.is_empty() + } +} + +// ── Loader entry point ──────────────────────────────────────────────────── + +/// Attempt to load DMC firmware for the given platform. +/// +/// Looks up the firmware blob from the provided cache (keyed by firmware +/// path, e.g. `"i915/mtl_dmc.bin"`). If the blob is not found, returns +/// `Ok(false)` — the caller decides whether to proceed without DMC. +/// +/// On success, the DMC program store is populated and the display +/// microcontroller can manage low-power state transitions. +pub fn try_load_dmc( + firmware: &std::collections::HashMap>, + platform: DisplayPlatform, + mmio: &MmioRegion, +) -> Result { + let key = platform.dmc_firmware_key(); + let blob = match firmware.get(key) { + Some(data) => data, + None => { + if platform.requires_dmc() { + warn!( + "redox-drm: DMC firmware '{}' not found in cache for {:?} — \ + display low-power states will be unavailable", + key, + platform + ); + } else { + debug!( + "redox-drm: DMC firmware '{}' not found in cache for {:?} (optional)", + key, + platform + ); + } + return Ok(false); + } + }; + + info!( + "redox-drm: loading DMC firmware '{}' ({} bytes) for {:?}", + key, + blob.len(), + platform + ); + + let fw = DmcFirmware::parse(blob, platform.version())?; + + fw.load(mmio)?; + + info!( + "redox-drm: DMC firmware loaded successfully (CSS v{}.{}), {} segment(s)", + fw.version >> 16, + fw.version & 0xFFFF, + fw.segments_count(), + ); + + Ok(true) +} + +impl DmcFirmware { + fn segments_count(&self) -> usize { + self.segments.len() + } +} + +// ── MMIO helpers ────────────────────────────────────────────────────────── + +fn read_u32_le(data: &[u8], offset: usize) -> u32 { + u32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]) +} + +fn ensure_mmio_access(mmio_size: usize, offset: usize, op: &str) -> Result<()> { + let end = offset + .checked_add(4) + .ok_or_else(|| DriverError::Mmio(format!("{op} offset overflow at {offset:#x}")))?; + if end > mmio_size { + return Err(DriverError::Mmio(format!( + "{op} outside MMIO aperture: end={end:#x} size={mmio_size:#x}" + ))); + } + Ok(()) +} + +fn write_mmio(mmio: &MmioRegion, offset: usize, value: u32) -> Result<()> { + ensure_mmio_access(mmio.size(), offset, "DMC MMIO write")?; + mmio.write32(offset, value); + Ok(()) +} + +fn read_mmio(mmio: &MmioRegion, offset: usize) -> Result { + ensure_mmio_access(mmio.size(), offset, "DMC MMIO read")?; + Ok(mmio.read32(offset)) +} + +// ── Tests ───────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_platform_detection() { + // MTL + assert_eq!( + display_platform_for_device_id(0x7D51), + Some(DisplayPlatform::Gen14) + ); + assert_eq!( + display_platform_for_device_id(0x7D40), + Some(DisplayPlatform::Gen14) + ); + // TGL + assert_eq!( + display_platform_for_device_id(0x9A49), + Some(DisplayPlatform::Gen12) + ); + // SKL + assert_eq!( + display_platform_for_device_id(0x1912), + Some(DisplayPlatform::Gen9) + ); + // Pre-Gen8 + assert_eq!(display_platform_for_device_id(0x0166), None); + } + + #[test] + fn test_platform_requires_dmc() { + assert!(!DisplayPlatform::Gen9.requires_dmc()); + assert!(!DisplayPlatform::Gen11.requires_dmc()); + assert!(DisplayPlatform::Gen12.requires_dmc()); + assert!(DisplayPlatform::Gen13.requires_dmc()); + assert!(DisplayPlatform::Gen14.requires_dmc()); + } + + #[test] + fn test_dmc_firmware_key() { + assert_eq!( + DisplayPlatform::Gen14.dmc_firmware_key(), + "i915/mtl_dmc.bin" + ); + assert_eq!( + DisplayPlatform::Gen12.dmc_firmware_key(), + "i915/tgl_dmc.bin" + ); + } + + #[test] + fn test_css_header_rejects_truncated() { + let data = [0u8; 64]; // Too small + assert!(CssHeader::parse(&data).is_err()); + } + + #[test] + fn test_css_header_parses_version() { + let mut data = vec![0u8; 128]; + // header_len at offset 4 = 32 dwords + data[4..8].copy_from_slice(&32u32.to_le_bytes()); + // version at offset 88 = 0x00020001 (v2.1) + data[88..92].copy_from_slice(&0x0002_0001u32.to_le_bytes()); + let css = CssHeader::parse(&data).unwrap(); + assert_eq!(css.version, 0x0002_0001); + } + + #[test] + fn test_package_header_parses_v2() { + let mut data = vec![0u8; 16]; + data[1] = 2; // header_ver = 2 + // num_entries at offset 12 + data[12..16].copy_from_slice(&3u32.to_le_bytes()); + let pkg = PackageHeader::parse(&data).unwrap(); + assert_eq!(pkg.header_ver, 2); + assert_eq!(pkg.num_entries, 3); + // v2: max_entries = 32, total = 16 + 32*12 = 400 + assert_eq!(pkg.total_bytes, 16 + 32 * 12); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs b/local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs index a43bf67c39..064007d7e8 100644 --- a/local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs +++ b/local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs @@ -1,4 +1,5 @@ pub mod display; +pub mod dmc; pub mod gtt; pub mod ring; @@ -98,8 +99,53 @@ impl IntelDriver { let gtt_control_mmio = map_bar(&mut device, &mmio_bar, "Intel GGTT control MMIO")?; let gtt_mmio = map_bar(&mut device, >t_bar, "Intel GGTT BAR0")?; + let platform = dmc::display_platform_for_device_id(info.device_id); + let display_ver = platform.map(|p| p.version()).unwrap_or(9); + enable_forcewake(&mmio)?; + // Load DMC (Display Microcontroller) firmware before display init. + // The DMC manages display engine low-power state transitions. On + // Gen12+ (TGL, ADL, MTL) the display pipeline may not function + // correctly without it. On Gen9/Gen11 DMC is optional but beneficial. + if let Some(platform) = platform { + match dmc::try_load_dmc(&firmware, platform, &mmio) { + Ok(true) => { + info!( + "redox-drm: DMC firmware loaded for {:?} (display v{})", + platform, display_ver + ); + } + Ok(false) => { + if platform.requires_dmc() { + warn!( + "redox-drm: DMC firmware not available for {:?} — \ + display low-power states unavailable, basic modeset \ + may still function", + platform + ); + } else { + debug!( + "redox-drm: DMC firmware not available for {:?} (optional)", + platform + ); + } + } + Err(e) => { + warn!( + "redox-drm: DMC firmware load failed for {:?}: {} — \ + proceeding without DMC", + platform, e + ); + } + } + } else { + debug!( + "redox-drm: display platform unknown for device {:#06x}, skipping DMC load", + info.device_id + ); + } + let display = IntelDisplay::new(display_mmio)?; let mut gtt = IntelGtt::init(gtt_mmio, gtt_control_mmio)?; let mut ring = IntelRing::create(ring_mmio, RingType::Render)?; diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/virtio/mod.rs b/local/recipes/gpu/redox-drm/source/src/drivers/virtio/mod.rs index e8e46b2300..6b1e0be1e8 100644 --- a/local/recipes/gpu/redox-drm/source/src/drivers/virtio/mod.rs +++ b/local/recipes/gpu/redox-drm/source/src/drivers/virtio/mod.rs @@ -879,7 +879,7 @@ mod tests { fn virtio_driver_driver_name_is_expected() { let driver = VirtioDriver { info: dummy_pci_info(), - _mmio: unsafe { core::mem::zeroed() }, + mmio: unsafe { core::mem::zeroed() }, irq_handle: Mutex::new(None), width: 1024, height: 768, @@ -905,7 +905,7 @@ mod tests { fn redox_private_cs_wait_returns_immediately_when_already_completed() { let driver = VirtioDriver { info: dummy_pci_info(), - _mmio: unsafe { core::mem::zeroed() }, + mmio: unsafe { core::mem::zeroed() }, irq_handle: Mutex::new(None), width: 1024, height: 768, @@ -935,7 +935,7 @@ mod tests { fn virgl_get_param_returns_zero_for_unknown_param() { let driver = VirtioDriver { info: dummy_pci_info(), - _mmio: unsafe { core::mem::zeroed() }, + mmio: unsafe { core::mem::zeroed() }, irq_handle: Mutex::new(None), width: 1024, height: 768, @@ -958,7 +958,7 @@ mod tests { fn virgl_resource_params_roundtrips_through_driver_default_unsupported() { let driver = VirtioDriver { info: dummy_pci_info(), - _mmio: unsafe { core::mem::zeroed() }, + mmio: unsafe { core::mem::zeroed() }, irq_handle: Mutex::new(None), width: 1024, height: 768,