Files
RedBear-OS/local/docs/INTEL-DRIVER-MODERNIZATION-PLAN.md
vasilito 44bcf2b75a docs: add Intel driver modernization plan
Comprehensive 6-phase plan (1,055 lines) for updating the Intel GPU driver from current 1,590-line stub to full Gen9+ support ported from Linux 7.1 i915. Covers register abstraction, GMBUS I2C, DMC firmware, power wells, CDCLK, display pipeline, modesetting, and hardware validation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-29 21:50:07 +03:00

42 KiB
Raw Permalink Blame History

Intel GPU Driver Modernization Plan

Created: 2026-05-29 Authority: This document is the canonical Intel-specific execution plan beneath local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md (Workstream C). It does not replace the DRM plan — it is the detailed Intel backend implementation guide that the DRM plan's Workstream C gates reference. Linux reference: local/reference/linux-7.1/drivers/gpu/drm/i915/ Driver source: local/recipes/gpu/redox-drm/source/src/drivers/intel/

1. Current State

1.1 Code inventory

File Lines What it does
mod.rs 693 IntelDriver struct, PCI BAR mapping, forcewake, device init, IRQ dispatch, GpuDriver trait impl
display.rs 404 Pipe/connector detection, modesetting, page flip, EDID stub
gtt.rs 226 GGTT page table: allocate, map, unmap, flush
ring.rs 267 Command ring buffer: render, blitter, video enhance
Total 1,590

1.2 What works (real code, verified in build)

  • PCI device detection and BAR mapping (BAR0 GGTT, BAR2 MMIO)
  • Forcewake register programming (0xA18C)
  • GGTT page table management: PTE encoding, range allocation with free-list coalescing, multi-page map/unmap, flush via GFX_FLSH_CNTL
  • Command ring buffer: ring register programming (RBBASE/RBTAIL/RBHEAD/RBCTL), tail/head tracking, submit_batch with space wait, MI_FLUSH_DW flush
  • Display pipe detection (4 pipes, 6 ports via PIPECONF + DDI_BUF_CTL reads)
  • Modesetting: HTOTAL/HBLANK/HSYNC/VTOTAL/VBLANK/VSYNC/PIPE_SRC/PLANE_SIZE/DSPCNTR/PIPECONF/DDI_BUF_CTL register writes
  • Page flip via DSPSURF
  • Vblank counting via PIPEFRAME
  • MSI-X / MSI / legacy IRQ cascade with quirk gating
  • Synthetic EDID generation (hardcoded 1920×1080@60)
  • EDID block parsing (detailed timing descriptors, pixel clock, vrefresh)
  • GEM buffer management (shared core in gem.rs)
  • DRM ioctl surface (shared core in scheme.rs)
  • DMC firmware manifest with device-ID-keyed lookup for TGL, ADLP, DG2, MTL, SKL, KBL, CNL, ICL, GLK, RKL
  • Device ID tables covering Gen4 through Gen12 (including Arrow Lake, Lunar Lake, Battlemage)

1.3 What does NOT work (hard stubs and missing code)

Gap Severity Current state
EDID I2C/DDC BLOCKING read_edid_block() returns Err every time — every connector gets synthetic 1080p
DPCD (DisplayPort) BLOCKING read_dpcd() returns empty Vec — no DP link training or capabilities
DMC firmware programming BLOCKING Firmware blobs are preloaded into HashMap but never written to hardware registers
Per-gen register offsets BLOCKING All offsets are Gen8/Gen9 constants — wrong for Gen12+ (Tiger Lake, Alder Lake, Meteor Lake)
Display power wells BLOCKING No power well enable — Gen9+ display registers unreadable without power domain setup
CDCLK / PLL BLOCKING No core display clock or pixel clock PLL programming — no pixels generated
DP AUX channel HIGH No AUX read/write — no DP EDID, no DPCD, no link training
HDMI infoframes HIGH No AVI/audio infoframes — HDMI monitors get no mode negotiation
Cursor plane MEDIUM CURCNTR/CURPOS/CURBASE never programmed
Atomic modeset MEDIUM Legacy SETCRTC + PAGE_FLIP only — KWin needs atomic
Hotplug detection MEDIUM Polling DDI_BUF_CTL on every IRQ; no real HPD interrupt handling
Watermarks / DBUF MEDIUM No display bandwidth programming — risk of underruns
Backlight LOW No BLC_PWM_CTL programming
PSR / FBC LOW Panel Self Refresh and FrameBuffer Compression not implemented
GuC / HuC / GSC LOW No GPU scheduling or media firmware
GT frequency / RC6 LOW No render power state management

1.4 Register offset problem (critical detail)

Every register in the driver is a hardcoded Gen8/Gen9-era constant:

Register Current offset Valid for Wrong for
FORCEWAKE 0xA18C Gen8/Gen9 Gen12+ uses 0xA188 or forcewake ack at 0xA188/0xA188+4
PIPECONF_BASE 0x70008 Gen8Gen11 Gen12 uses 0x70008 but transcoder is separate
DSPCNTR_BASE 0x70180 Gen8/Gen9 primary planes Gen12+ uses PLANE_CTL at 0x70180 with different bit layout
DSPSURF_BASE 0x7019C Gen8/Gen9 Gen12+ uses PLANE_SURF at 0x7019C but with different semantics
DDI_BUF_CTL_BASE 0x64000 Gen8+ Mostly correct but port count and lane info changed at Gen12
HTOTAL_BASE 0x60000 Gen8+ transcoder A Gen12 separates pipe timings from transcoder timings
PP_STATUS 0xC7200 Gen8/Gen9 Gen12+ uses different panel power registers

Impact: The driver can compile for Gen12+ device IDs but will malfunction at runtime because it writes to the wrong register addresses and with the wrong bit layouts.

1.5 Hardware generation coverage

Generation Display ver Device IDs in table Driver gate Register compat DMC manifest Real status
Gen4 (i965/G4x) 4 18 Blocked Wrong No keys No support
Gen5 (Ironlake) 5 2 Blocked Wrong No keys No support
Gen6 (Sandy Bridge) 6 7 Blocked Wrong No keys No support
Gen7 (IVB/HSW/BDW) 7 ~68 Blocked Wrong No keys No support
Gen8 (SKL/KBL/CFL) 9 ~77 Allowed Partially correct SKL/KBL/CFL keys Best chance for first validation
Gen9 (CNL/ICL/EHL) 10-11 ~60 Allowed Partially correct CNL/ICL/GLK/RKL keys Needs register fixes
Gen12 TGL/ADL/DG2 12-13 ~34 Allowed Wrong TGL/ADLP/DG2 keys Will not work without new registers
Gen12 MTL/ARL 14 ~10 Allowed Wrong MTL keys Will not work
Gen12 LNL/BMG 20+ ~18 Allowed Wrong No keys i915 does not cover these; Xe driver territory

2. Linux i915 Reference Architecture

2.1 Scale

Linux i915 in our reference tree (Linux 7.1):

Subsystem .h files .c files Key purpose
display/ 195 144 All display: pipes, planes, connectors, encoders, DP, HDMI, DMC, power, CDCLK, PLL
gt/ 78 83 GPU engines, GTT, command submission, power, workarounds
gt/uc/ 34 23 GuC, HuC, GSC firmware
gem/ 21 26 Buffer objects, eviction, tiling, execbuffer
Top-level ~50 ~50 PCI, MMIO, IRQ, uncore, PM, debugfs
Total ~378 ~326 ~700 files

Red Bear's Intel driver is ~1,590 lines. Linux i915 is ~700 files. The gap is not incremental — it is architectural.

2.2 i915 probe sequence (what Red Bear must replicate)

i915_pci_probe()
  → i915_driver_create()           // alloc drm_i915_private, init device info
  → i915_driver_early_probe()      // MMIO map, uncore init, pcode init
  → intel_gt_probe_all()           // discover GTs and engines
  → i915_driver_mmio_probe()       // interrupts, GTT init
  → i915_driver_hw_probe()         // GT, engines, workarounds
  → intel_display_driver_probe_noirq()   // VBT parse, CDCLK init, power wells, PLL, connectors
  → intel_irq_install()            // install IRQ handlers
  → intel_display_driver_probe_nogem()   // encoders, connectors, initial modeset
  → i915_gem_init()                // GEM memory manager
  → intel_display_driver_probe()   // planes, framebuffer, fbdev
  → i915_driver_register()         // DRM device registration

2.3 Critical display init subsequence (Red Bear's missing pieces)

The Linux intel_display_driver_probe_noirq() call chain does the following, none of which Red Bear implements:

intel_power_domains_init()         → enable display power wells (DC off → DC on)
intel_cdclk_init()                 → program core display clock
intel_dmc_init()                   → upload DMC firmware to hardware
intel_dpll_init()                  → initialize shared PLL pool
intel_display_device_info_init()   → per-generation capability table
intel_bios_init()                  → parse VBT for port types, backlight, etc.
intel_gmbus_init()                 → initialize I2C/GMBUS controllers
intel_panel_init()                 → backlight, eDP panel power sequencing
intel_psr_init()                   → Panel Self Refresh setup

None of these exist in Red Bear. Without them, the display engine is not initialized and register reads may return 0 or garbage.

2.4 GMBUS/DDC I2C (the #1 missing piece)

Linux path for reading real EDID:

intel_gmbus.c:
  gmbus_setup()                    → configure GMBUS controller registers
  gmbus_xfer()                     → perform I2C read/write transaction
    → GMBUS0: select pin pair (DPC, DPE, etc.)
    → GMBUS1: set byte count + slave addr (0xA0 for EDID)
    → GMBUS2: status/clear
    → GMBUS3: data read FIFO
    → GMBUS4: interrupt mask
    → GMBUS5: 2-byte index for block reads
  do_gmbus_xfer()                  → hardware transaction with timeout
  intel_gmbus_get_adapter()        → returns I2C adapter per port
  intel_gmbus_read_edid()          → reads 128/256-byte EDID via I2C

Register map (Gen9, Skylake):
  GMBUS0   = 0xC5100   — pin pair select + rate
  GMBUS1   = 0xC5104   — command: byte count, slave addr, direction
  GMBUS2   = 0xC5108   — status: IN_USE, HW_WAIT, SMBALERT, SDAST (data valid)
  GMBUS3   = 0xC510C   — data FIFO (read 4 bytes at a time)
  GMBUS4   = 0xC5110   — interrupt enable/mask
  GMBUS5   = 0xC5120   — 2-byte index (for EDID block reads > 1)

2.5 DMC firmware programming

Linux path:

intel_dmc.c:
  intel_dmc_init()
    → parse CSS header (first 32 bytes of firmware blob)
    → find matching stepping (ucode version)
    → write DMC payload to MMIO:
      DMC_FW1_BASE(offset) = payload address low
      DMC_FW1_BASE+4       = payload address high
      DMC_MMIO_START(offset) = 0x10000 (SRAM base)
      DMC_MMIO_END(offset)   = 0x10000 + payload_size
    → set DMC_CTRL = ENABLE bit
    → wait for DMC_STATUS to indicate loaded

Register map (Gen9):
  DMC_MMIO_START  = 0x6F000
  DMC_MMIO_END    = 0x6F004
  DMC_FW_BASE     = 0x6F008  (or per-variant: 0x6F038, 0x6F0B0)
  DMC_CTRL        = 0x6F064
  DMC_STATUS      = 0x6F06C
  DMC_SRAM_BASE   = 0x10000  (SRAM offset inside MMIO space)

2.6 Display power wells

Linux path:

intel_display_power.c:
  intel_power_domains_init()
    → register power well list per generation
    → each well: domains bitmask, ops (enable/disable/is_enabled)
  
  SKL power wells (Gen9 example):
    PW_1 (always on):  domains = DISPLAY_CORE, PIPE_A
    PW_2:              domains = PIPE_B, PIPE_C, PIPE_D, AUX_*
    PW_3:              domains = DDI_*, TRANS_*
    DDI_A_IO, DDI_B_IO, DDI_C_IO, DDI_D_IO, DDI_E_IO
    AUX_A, AUX_B, AUX_C, AUX_D
  
  Each power well enable:
    → write POWER_WELL_CTL register (0x45400 for SKL)
    → wait for power well to become active (read-back verify)
    → on Gen12: also enable DBUF slices via DBUF_CTL_S1/S2

Without power wells enabled, DDI_BUF_CTL reads return 0 — connector detection appears to find nothing connected.

3. Target Hardware Generations

3.1 Generational strategy

Phase Target gen Display ver Reason
Phase 0 Gen9 (Skylake, Kaby Lake, Coffee Lake) 9 Simplest modern display engine. Widest installed base of laptops. Registers are mostly correct in current code.
Phase 1 Gen9.5 (Ice Lake, Elkhart Lake) 11 Adds combo PHY, different DDI programming. Validates register abstraction.
Phase 2 Gen12 (Tiger Lake, Alder Lake-P) 12-13 Major display engine rewrite. Separate transcoder from pipe. DBUF slices.
Phase 3 Gen12.7 (Meteor Lake, Arrow Lake) 14 Newest i915-supported platform. Uses Xe-LP display.
Post-i915 Battlemage, Lunar Lake, Panther Lake 20-30 NOT in i915. Uses Xe driver architecture (drivers/gpu/drm/xe/). Future work beyond this plan.

3.2 Gen9 (Skylake family) — first validation target

Why Gen9 first:

  • Most of our register offsets are already correct for Gen9
  • Largest installed base of Intel GPUs in 2024-2026 laptops and desktops
  • Well-documented in Linux i915 with clear per-register comments
  • No complex combo PHY or separate transcoder (pipe = transcoder)
  • DMC firmware is small and well-understood (SKL: 42KB, KBL: 42KB, CFL: 40KB)
  • GMBUS registers at standard offsets (0xC5100-0xC5120)

Device IDs for Gen9 validation:

Platform Substring Example device IDs
Skylake DT GT2 0x1912, 0x191B, 0x191D, 0x191E, 0x1921 Desktop HD 530
Skylake ULT GT2 0x1916, 0x1921, 0x1923, 0x1926, 0x1927 Laptop HD 520/530
Skylake ULX GT2 0x191E HD 515
Kaby Lake DT GT2 0x5912, 0x5916, 0x591B, 0x591D Desktop HD 630
Kaby Lake ULT GT2 0x5916, 0x5921, 0x5923, 0x5926, 0x5927 Laptop HD 620/630
Coffee Lake DT GT2 0x3E90, 0x3E91, 0x3E92, 0x3E93, 0x3E96, 0x3E98, 0x3E9A, 0x3E9B Desktop UHD 630
Coffee Lake ULT GT3 0x3EA5, 0x3EA6, 0x3EA7, 0x3EA8 Laptop Iris Plus

4. New File Structure

Current:

src/drivers/intel/
├── mod.rs          (693 lines)
├── display.rs      (404 lines)
├── gtt.rs          (226 lines)
└── ring.rs         (267 lines)

Target:

src/drivers/intel/
├── mod.rs              Driver struct, init, GpuDriver impl
├── display.rs          Display pipe, modesetting, page flip
├── display_power.rs    Power well enable/disable per generation [NEW]
├── display_cdclk.rs    Core display clock programming [NEW]
├── display_dpll.rs     Display PLL (pixel clock generation) [NEW]
├── display_dmc.rs      DMC firmware upload to hardware [NEW]
├── gmbus.rs            GMBUS I2C controller (EDID/DDC) [NEW]
├── dp_aux.rs           DisplayPort AUX channel read/write [NEW]
├── dp_link.rs          DP link training [NEW]
├── hdmi.rs             HDMI infoframe programming [NEW]
├── hotplug.rs          HPD interrupt handling [NEW]
├── cursor.rs           Hardware cursor plane [NEW]
├── gtt.rs              GGTT page table (existing, mostly complete)
├── ring.rs             Command ring buffer (existing, mostly complete)
├── regs.rs             Per-generation register tables [NEW]
├── regs_gen9.rs        Gen9 (SKL/KBL/CFL) register offsets [NEW]
├── regs_gen11.rs       Gen11 (ICL/EHL) register offsets [NEW]
├── regs_gen12.rs       Gen12 (TGL/ADL/DG2/MTL) register offsets [NEW]
├── info.rs             Device info, capability flags, display version [NEW]
└── vbt.rs              Video BIOS Table parser [NEW]

5. Implementation Phases

Phase 0 — Display Foundation for Gen9 (4-6 weeks)

Goal: Real EDID reading, DMC firmware programming, and display power well initialization on Gen9 (Skylake / Kaby Lake / Coffee Lake). First real Intel display output on hardware.

Exit criteria:

  • read_edid_block() returns real EDID from a physical monitor via GMBUS I2C
  • DMC firmware is uploaded to hardware and status confirms loaded
  • Display power wells are enabled before connector detection
  • Connector detection reads real connection status (not synthetic)
  • Mode list comes from monitor's EDID, not hardcoded 1080p
  • CDCLK is programmed based on display bandwidth requirements
  • Bounded modeset proof on real Gen9 hardware via test-intel-gpu.sh

0A: Device info and register abstraction (regs.rs, info.rs)

Create info.rs — Device capability table:

pub struct IntelDeviceInfo {
    pub display_version: u8,
    pub gt_version: u8,
    pub num_pipes: u8,
    pub num_ports: u8,
    pub has_ddi: bool,
    pub has_dp_aux: bool,
    pub has_gmbus: bool,
    pub has_dmc: bool,
    pub has_combo_phy: bool,
    pub has_dbuf_slice: bool,
    pub has_transcoder: bool,   // Gen12+: pipe != transcoder
    pub dmc_fw_key: Option<&'static str>,
}

Populate from device ID → generation lookup. Key function:

pub fn device_info_from_id(device_id: u16) -> IntelDeviceInfo

This replaces the ad-hoc is_supported_intel_generation() gate with a rich capability table.

Create regs.rs — Register access abstraction:

pub trait IntelRegs {
    // Power wells
    const POWER_WELL_CTL: usize;
    
    // Display clock
    const CDCLK_CTL: usize;
    
    // DMC
    const DMC_MMIO_START: usize;
    const DMC_MMIO_END: usize;
    const DMC_FW_BASE: usize;
    const DMC_CTRL: usize;
    const DMC_STATUS: usize;
    
    // GMBUS
    const GMBUS0: usize;
    const GMBUS1: usize;
    const GMBUS2: usize;
    const GMBUS3: usize;
    const GMBUS4: usize;
    const GMBUS5: usize;
    
    // Pipe / transcoder
    const PIPECONF(n: u8) -> usize;
    const HTOTAL(n: u8) -> usize;
    // ... etc
    
    // Planes
    const DSPCNTR(n: u8) -> usize;
    const DSPSURF(n: u8) -> usize;
    
    // Cursor
    const CURCNTR(n: u8) -> usize;
    const CURPOS(n: u8) -> usize;
    const CURBASE(n: u8) -> usize;
    
    // DDI
    const DDI_BUF_CTL(port: u8) -> usize;
    
    // Forcewake
    const FORCEWAKE_REQ: usize;
    const FORCEWAKE_ACK: usize;
}

Create regs_gen9.rs — Gen9 register constants:

All offsets from local/reference/linux-7.1/drivers/gpu/drm/i915/display/intel_display_regs.h for DISPLAY_VER 9.

GMBUS0       = 0xC5100
GMBUS1       = 0xC5104
GMBUS2       = 0xC5108
GMBUS3       = 0xC510C
GMBUS4       = 0xC5110
GMBUS5       = 0xC5120

POWER_WELL_CTL = 0x45400

CDCLK_CTL    = 0x46000

DMC_MMIO_START = 0x6F000
DMC_MMIO_END   = 0x6F004
DMC_FW1_BASE   = 0x6F038
DMC_FW2_BASE   = 0x6F0B0
DMC_CTRL       = 0x6F064
DMC_STATUS     = 0x6F06C
DMC_SRAM_BASE  = 0x10000

PIPECONF(A)  = 0x70008    (pipe stride = 0x1000)
HTOTAL(A)    = 0x60000    (transcoder stride = 0x1000)
DDI_BUF_CTL(A) = 0x64000  (port stride = 0x100)

CURCNTR(A)   = 0x70080    (pipe stride = 0x1000)
CURPOS(A)    = 0x70084
CURBASE(A)   = 0x70088

FORCEWAKE_REQ = 0xA18C
FORCEWAKE_ACK = 0xA194

Linux reference files:

  • i915/i915_reg.h — master register definitions
  • i915/display/intel_display_regs.h — display-specific registers
  • i915/display/intel_dmc_regs.h — DMC registers
  • i915/display/intel_gmbus_regs.h — GMBUS registers

Estimated effort: ~500 lines Rust across 3 files.

0B: GMBUS / DDC I2C for real EDID (gmbus.rs)

Create gmbus.rs — GMBUS I2C controller:

Key operations to port from intel_gmbus.c:

pub struct GmbusController {
    mmio: Arc<MmioRegion>,
    regs: &'static dyn IntelRegs,
}

impl GmbusController {
    /// Initialize GMBUS hardware
    pub fn init(&self) -> Result<()>;
    
    /// Read bytes from I2C slave via GMBUS
    pub fn read(&self, port: GmbusPort, slave_addr: u8, offset: u8, buf: &mut [u8]) -> Result<()>;
    
    /// Read full EDID block from monitor (128 or 256 bytes)
    pub fn read_edid(&self, port: GmbusPort) -> Result<Vec<u8>>;
}

pub enum GmbusPort {
    DDC_B = 0x04,   // Legacy VGA / DVI
    DDC_C = 0x05,   // HDMI port C
    DDC_D = 0x06,   // HDMI port D
    // Gen9+ additional ports
    DPC   = 0x02,   // DisplayPort C
    DPE   = 0x03,   // DisplayPort E
}

Implementation sequence:

  1. init(): Reset GMBUS controller — clear GMBUS2 status, set GMBUS0 pin pair select.
  2. read():
    • Write GMBUS1: byte count | (slave_addr << 1) | direction (read=1)
    • Wait for GMBUS2.SDAST bit (data available in GMBUS3)
    • Read 4 bytes at a time from GMBUS3 FIFO
    • For multi-byte offset reads (EDID block 1+): write GMBUS5 with 2-byte index
  3. read_edid(): Call read() with slave address 0xA0, read 128 bytes. Parse byte 126 for extension count; if nonzero, read block 1 via GMBUS5 index mode.
  4. Error handling: Timeout after 1ms per byte. Clear GMBUS2 errors. Hardware reset via GMBUS0 GPIO mode if stuck.

Integration with display.rs:

  • Replace read_edid_block() stub with GmbusController::read_edid() call
  • Port-to-GMBUS mapping: port 0→DPC, port 1→DPC, port 2→DPE, port 3→DPE (Gen9 SKL mapping from Linux)
  • Fall back to synthetic EDID only if GMBUS read fails AND DDI_BUF_CTL reports connected

Linux reference files:

  • display/intel_gmbus.c (~560 lines) — full GMBUS implementation
  • display/intel_gmbus_regs.h — register definitions

Estimated effort: ~400-500 lines Rust.

0C: Display power wells (display_power.rs)

Create display_power.rs — Power domain management:

pub struct DisplayPower {
    mmio: Arc<MmioRegion>,
    regs: &'static dyn IntelRegs,
}

impl DisplayPower {
    /// Enable all power domains needed for display operation
    pub fn init_domains(&self) -> Result<()>;
    
    /// Enable a specific power well
    pub fn enable_well(&self, well: PowerWell) -> Result<()>;
    
    /// Check if a power well is active
    pub fn is_well_enabled(&self, well: PowerWell) -> bool;
    
    /// Disable unused power wells (power saving)
    pub fn disable_unused(&self, active_pipes: u8) -> Result<()>;
}

pub enum PowerWell {
    PW1,           // Always-on domain: display core
    PW2,           // Pipe B/C/D, AUX channels
    DDI_A,         // DDI port A (eDP)
    DDI_B,         // DDI port B (HDMI)
    DDI_C,         // DDI port C (DP)
    DDI_D,         // DDI port D (DP)
    DDI_E,         // DDI port E (HDMI)
    AUX_A,         // DP AUX channel A
    AUX_B,         // DP AUX channel B
    AUX_C,         // DP AUX channel C
    AUX_D,         // DP AUX channel D
}

Implementation for Gen9 (SKL):

  • POWER_WELL_CTL at 0x45400: each power well is a bit in this register
  • Enable: write 1 << well_bit to POWER_WELL_CTL, then read back until the corresponding status bit is set
  • Required wells before connector detection: PW1, PW2, all DDI wells, all AUX wells

Integration with mod.rs:

  • Call display_power.init_domains() in IntelDriver::new() before any connector detection
  • GMBUS reads require AUX power wells enabled

Linux reference files:

  • display/intel_display_power.c (~1000 lines)
  • display/intel_display_power_well.c (~500 lines)
  • display/intel_display_power_map.c (~200 lines)

Estimated effort: ~400 lines Rust (Gen9 only initially).

0D: DMC firmware programming (display_dmc.rs)

Create display_dmc.rs — DMC firmware upload:

pub struct DmcFirmware {
    mmio: Arc<MmioRegion>,
    regs: &'static dyn IntelRegs,
}

impl DmcFirmware {
    /// Upload DMC firmware blob to hardware
    pub fn upload(&self, firmware: &[u8]) -> Result<()>;
    
    /// Check if DMC firmware is loaded and running
    pub fn is_loaded(&self) -> bool;
    
    /// Get the firmware version running on hardware
    pub fn firmware_version(&self) -> u32;
}

Implementation:

  1. Parse firmware blob: first 32 bytes are CSS header. Extract payload offset, payload size, and ucode version.
  2. Find matching stepping: compare firmware ucode version against hardware stepping (from MCHBAR or PCI revision).
  3. Upload payload:
    • Write DMC_MMIO_START = 0x10000 (SRAM base)
    • Write DMC_MMIO_END = 0x10000 + payload_size
    • Write DMC_FW_BASE through DMC_FW_BASE + payload_size in 4-byte chunks (payload data)
  4. Enable: write DMC_CTRL with enable bit
  5. Wait: poll DMC_STATUS for loaded indication, timeout 50ms

Integration with mod.rs:

  • IntelDriver::new() already receives firmware: HashMap<String, Vec<u8>> — use the DMC key from the device info table
  • Call dmc.upload(&firmware[dmc_key]) after power wells but before connector detection
  • DMC is optional for Gen8/Gen9 (display works without it but power management is broken), required for Gen12+

Linux reference files:

  • display/intel_dmc.c (~600 lines)
  • display/intel_dmc_regs.h (~60 lines)

Estimated effort: ~300 lines Rust.

0E: Core display clock (display_cdclk.rs)

Create display_cdclk.rs — CDCLK programming:

pub struct CdclkState {
    pub frequency: u32,        // Hz
    pub voltage_level: u32,    // for SKL: 0=337.5MHz, 1=450MHz, 2=540MHz
}

pub struct DisplayClock {
    mmio: Arc<MmioRegion>,
    regs: &'static dyn IntelRegs,
}

impl DisplayClock {
    /// Initialize CDCLK to minimum safe frequency
    pub fn init(&self) -> Result<CdclkState>;
    
    /// Set CDCLK frequency for required display bandwidth
    pub fn set_frequency(&self, target: u32) -> Result<CdclkState>;
    
    /// Calculate required CDCLK for a given set of display modes
    pub fn required_cdclk(modes: &[ModeInfo]) -> u32;
}

For Gen9 (SKL):

  • CDCLK_CTL at 0x46000
  • Supported frequencies: 337.5 MHz, 450 MHz, 540 MHz
  • Single pipe at 1080p60 needs ~337.5 MHz (pixel rate 148.5 MHz < 337.5/2)
  • Bump to 450 MHz for dual pipe or high-resolution modes
  • CDCLK_CTL bit layout: bits 26:26 = CD_FREQ_SELECT (0=337.5, 1=450, 2=540)

Linux reference files:

  • display/intel_cdclk.c (~1000 lines)

Estimated effort: ~300 lines Rust (Gen9 only).

0F: Connect Phase 0 into existing driver

Modify existing files:

File Change
mod.rs IntelDriver::new(): call power well init → DMC upload → CDCLK init before connector detection. Replace hardcoded register offsets with regs trait calls.
display.rs Replace read_edid_block() stub with GmbusController::read_edid() call. Use regs trait for pipe/plane/DDI register access. Add fallback path: try GMBUS → try DP AUX → fall back to synthetic EDID.
mod.rs Add GmbusController, DisplayPower, DmcFirmware, DisplayClock as struct fields.

Estimated effort: ~200 lines of modifications to existing files.

Phase 0 Summary

Sub-task New file Est. lines Linux reference Depends on
0A info.rs, regs.rs, regs_gen9.rs ~500 intel_display_regs.h, intel_device_info.h None
0B gmbus.rs ~500 intel_gmbus.c 0A (registers)
0C display_power.rs ~400 intel_display_power.c 0A (registers)
0D display_dmc.rs ~300 intel_dmc.c 0A (registers)
0E display_cdclk.rs ~300 intel_cdclk.c 0A (registers), 0C (power wells)
0F Modify mod.rs, display.rs ~200 All above
Total 10 files ~2,200

Validation gate:

# Must pass on real Gen9 hardware (Skylake/Kaby Lake/Coffee Lake):
./local/scripts/test-intel-gpu.sh

# In-guest checks:
# 1. connector enumeration returns real connectors (not empty or synthetic)
# 2. EDID is read from physical monitor (not synthetic 1080p)
# 3. mode list contains monitor's native resolution
# 4. bounded modeset produces visible output
# 5. DMC firmware status shows loaded
# 6. power wells show enabled

Phase 1 — DisplayPort and HDMI (4-6 weeks)

Goal: Full DP and HDMI output with proper link training, DPCD reads, and infoframe programming. Multi-monitor support.

Exit criteria:

  • DP link training succeeds and DP connector shows real modes
  • HDMI AVI infoframes are programmed with correct VIC
  • DPCD reads return real DP capabilities
  • Hotplug detection works via HPD interrupts (not polling)
  • Dual-monitor setup works with independent modes per pipe

1A: DP AUX channel (dp_aux.rs)

Create dp_aux.rs — DisplayPort AUX channel I/O:

pub struct DpAux {
    mmio: Arc<MmioRegion>,
    regs: &'static dyn IntelRegs,
}

impl DpAux {
    /// Read bytes from DP AUX channel
    pub fn read(&self, port: u8, offset: u32, buf: &mut [u8]) -> Result<()>;
    
    /// Write bytes to DP AUX channel
    pub fn write(&self, port: u8, offset: u32, data: &[u8]) -> Result<()>;
    
    /// Read full DPCD capability block (first 16 bytes)
    pub fn read_dpcd_caps(&self, port: u8) -> Result<DpcdCaps>;
    
    /// Read EDID via DP AUX (I2C-over-AUX)
    pub fn read_edid(&self, port: u8) -> Result<Vec<u8>>;
}

AUX register map (Gen9):

DP_AUX_CTL(port)   = 0x64010 + port * 0x100
DP_AUX_DATA(port)  = 0x64014 + port * 0x100  (5 registers for 20-byte message)

Linux reference: display/intel_dp_aux.c (~500 lines) Estimated effort: ~500 lines Rust.

Create dp_link.rs — DP link establishment:

pub struct DpLink {
    aux: DpAux,
    mmio: Arc<MmioRegion>,
}

impl DpLink {
    /// Train the DP link: find max lane count, set link rate, train
    pub fn train(&self, port: u8) -> Result<DpLinkConfig>;
    
    /// Read current link status from DPCD
    pub fn read_status(&self, port: u8) -> Result<DpLinkStatus>;
}

pub struct DpLinkConfig {
    pub lane_count: u8,       // 1, 2, or 4
    pub link_rate: u32,       // 162000, 270000, 540000, 810000 (kHz)
    pub spread_spectrum: bool,
}

Linux reference: display/intel_dp_link_training.c (~600 lines) Estimated effort: ~600 lines Rust.

1C: HDMI infoframes (hdmi.rs)

Create hdmi.rs — HDMI AVI/audio infoframes:

pub struct HdmiInfoframes;

impl HdmiInfoframes {
    /// Program AVI infoframe for a given mode
    pub fn program_avi(mmio: &MmioRegion, pipe: u8, mode: &ModeInfo) -> Result<()>;
    
    /// Program audio infoframe (basic stereo)
    pub fn program_audio(mmio: &MmioRegion, pipe: u8) -> Result<()>;
    
    /// Compute AVI infoframe checksum
    pub fn checksum(data: &[u8; 17]) -> u8;
}

Register map (Gen9):

HSW_TVIDEO_DIP_CTL(pipe)   = 0x61180 + pipe * 0x1000
HSW_TVIDEO_DIP_AVI_DATA(pipe) = 0x61184 + pipe * 0x1000  (5 regs, 4 bytes each)

Linux reference: display/intel_hdmi.c (infoframe helpers, ~400 lines relevant) Estimated effort: ~300 lines Rust.

1D: Hotplug interrupt handling (hotplug.rs)

Create hotplug.rs — HPD detection:

pub struct HotplugHandler {
    mmio: Arc<MmioRegion>,
}

impl HotplugHandler {
    /// Initialize HPD interrupt mask
    pub fn init(&self) -> Result<()>;
    
    /// Check which ports have pending HPD events
    pub fn check_events(&self) -> Vec<HotplugEvent>;
    
    /// Acknowledge HPD events
    pub fn ack(&self, events: &[HotplugEvent]);
    
    /// Distinguish short pulse (EDID change) from long pulse (connect/disconnect)
    pub fn classify_pulse(&self, port: u8) -> HotplugPulse;
}

pub struct HotplugEvent {
    pub port: u8,
    pub pulse_type: HotplugPulse,
}

pub enum HotplugPulse {
    Long,    // Connect or disconnect
    Short,   // EDID change or link training request
}

Register map (Gen9):

SDEISR    = 0xC4000   — south display interrupt status
SDEIMR    = 0xC4004   — south display interrupt mask
SDEIIR    = 0xC4008   — south display interrupt identity
SHOTPLUG_CTL = 0xC4030 — hotplug control

Linux reference: display/intel_hotplug.c + display/intel_hotplug_irq.c (~600 lines) Estimated effort: ~400 lines Rust.

1E: Display PLL (display_dpll.rs)

Create display_dpll.rs — Pixel clock generation:

pub struct DisplayPll {
    mmio: Arc<MmioRegion>,
}

impl DisplayPll {
    /// Initialize shared DPLL pool
    pub fn init(&self) -> Result<()>;
    
    /// Find or allocate a PLL for the given pixel clock
    pub fn get_pll_for_clock(&self, pixel_clock_khz: u32) -> Result<DpllConfig>;
    
    /// Enable a DPLL and wait for lock
    pub fn enable(&self, config: &DpllConfig) -> Result<()>;
    
    /// Wire a DPLL to a specific CRTC/port
    pub fn wire_to_crtc(&self, dpll: u8, pipe: u8) -> Result<()>;
}

pub struct DpllConfig {
    pub id: u8,
    pub frequency_khz: u32,
    pub pdiv: u32,
    pub kdiv: u32,
    pub qdiv_mode: u32,
}

Register map (Gen9 SKL):

LCPLL1_CTL  = 0x46010   — LC PLL 1 control
LCPLL2_CTL  = 0x46014   — LC PLL 2 control
DPLL0_CTL   = 0x46200   — DPLL 0 control
DPLL1_CTL   = 0x46204   — DPLL 1 control
DPLL2_CTL   = 0x46208   — DPLL 2 control
SKL_DPLL_CTRL1 = 0x46204 — DPLL configuration

Linux reference: display/intel_dpll_mgr.c (~2000 lines, Gen9 portion ~500 lines) Estimated effort: ~500 lines Rust (Gen9 SKL DPLLs only).

Phase 1 Summary

Sub-task New file Est. lines Linux reference
1A dp_aux.rs ~500 intel_dp_aux.c
1B dp_link.rs ~600 intel_dp_link_training.c
1C hdmi.rs ~300 intel_hdmi.c
1D hotplug.rs ~400 intel_hotplug.c
1E display_dpll.rs ~500 intel_dpll_mgr.c
Total 5 files ~2,300

Phase 2 — Gen12 Display Engine (6-10 weeks)

Goal: Tiger Lake / Alder Lake-P support with Gen12 display engine registers.

Exit criteria:

  • Gen12 device IDs produce correct register accesses
  • Separate pipe/transcoder programming works
  • DBUF slice allocation is correct
  • Combo PHY initialization for Ice Lake / Tiger Lake
  • Display on real Tiger Lake or Alder Lake hardware

2A: Gen12 register tables (regs_gen12.rs)

Major differences from Gen9:

Feature Gen9 Gen12
Pipe / transcoder Same thing Separate: PIPECONF controls pipe, TRANS_DDI_FUNC_CTL controls transcoder
Plane programming DSPCNTR/DSPSURF PLANE_CTL/PLANE_SURF/PLANE_STRIDE/PLANE_OFFSET
DBUF Single DBUF DBUF slices (S1, S2) must be enabled per pipe
Combo PHY N/A ICL/TGL: ICL_PORT_CL1K_DW0 through ICL_PORT_CL1K_DW30 for each PHY
DMC Single payload Multiple payloads (DC3DC5, DC3DC6, etc.)
CDCLK 3 fixed frequencies Bounded range with BXT_DE_PLL_CTL
DDI 6 ports max Up to 8 DDI ports, some are combo (TC)
AUX Per-DDI Per-DDI but TC ports use TC_AUX registers

Create regs_gen12.rs with all Gen12 display register offsets from intel_display_regs.h for DISPLAY_VER 12-13.

Estimated effort: ~800 lines (large register table).

2B: Combo PHY initialization

For Tiger Lake and Ice Lake, the combo PHYs must be initialized before DDI programming. Port from display/intel_combo_phy.c.

Estimated effort: ~400 lines.

2C: DBUF slice management

Gen12+ requires display buffer (DBUF) slices to be allocated per pipe. Port from display/intel_dbuf.c.

Estimated effort: ~300 lines.

2D: Transcoder programming

Gen12 separates pipe from transcoder. New TRANS_DDI_FUNC_CTL, TRANS_HTOTAL, etc. Port from display/intel_ddi.c (transcoder path).

Estimated effort: ~500 lines of changes to display.rs.

Phase 2 Summary

Sub-task Est. lines Linux reference
2A Gen12 regs ~800 intel_display_regs.h (DISPLAY_VER 12)
2B Combo PHY ~400 intel_combo_phy.c
2C DBUF slices ~300 intel_dbuf.c
2D Transcoder ~500 intel_ddi.c
Total ~2,000

Phase 3 — Full KMS Features (4-6 weeks)

Goal: Cursor planes, atomic modesetting, watermarks, backlight. What a compositor needs.

Task New file Est. lines Linux reference
Cursor plane cursor.rs ~200 intel_cursor.c
Atomic modeset Changes to scheme.rs, display.rs ~600 intel_atomic.c
Watermarks display_watermark.rs ~500 skl_watermark.c
Backlight display_backlight.rs ~200 intel_backlight.c
VBT parser vbt.rs ~500 intel_bios.c
Total 4 new files ~2,000

Phase 4 — Render Path (12-20 weeks, parallel with Phase 2-3)

Goal: GPU command submission for rendering. Mesa hardware renderer path.

This phase is architecturally different from display and can proceed in parallel.

Task File Est. lines Linux reference
Per-gen ring context init ring.rs expansion ~400 intel_lrc.c, gen8_engine_cs.c
Execlists submission execlists.rs (new) ~800 intel_execlists_submission.c
Batch buffer protocol batch.rs (new) ~300 gem/i915_gem_execbuffer.c
Fence objects fence.rs (new) ~400 i915_sw_fence.c, intel_timeline.c
Mesa winsys Changes to scheme.rs ~600 Mesa Redox winsys
Total 3 new files ~2,500

Phase 5 — Power and Optimization (8-12 weeks)

Task Est. lines Linux reference
PSR (Panel Self Refresh) ~400 intel_psr.c
FBC (Frame Buffer Compression) ~300 intel_fbc.c
GT frequency / RPS / RC6 ~400 intel_rps.c, intel_rc6.c
GuC firmware loading ~600 gt/uc/intel_guc.c, intel_guc_fw.c
GuC submission ~1000 gt/uc/intel_guc_submission.c
Total ~2,700

6. Effort Summary

Phase Duration New lines New files Blocks on
0: Display Foundation (Gen9) 4-6 weeks ~2,200 10 Nothing
1: DP/HDMI 4-6 weeks ~2,300 5 Phase 0
2: Gen12 Display 6-10 weeks ~2,000 2 + modifications Phase 0
3: Full KMS 4-6 weeks ~2,000 4 Phase 1
4: Render Path 12-20 weeks ~2,500 3 Phase 0 (parallel with 1-3)
5: Power/Optimize 8-12 weeks ~2,700 0+ Phase 2
Total 38-60 weeks ~13,700 ~24 new files

The critical path to first Intel display output on real hardware is Phase 0 at 4-6 weeks.

7. Validation Strategy

Per-phase validation

Phase Validation tool Success criteria
0 test-intel-gpu.sh on Gen9 hardware Real EDID read, real modes, visible output
1 DP/HDMI cable hotplug test DP link training succeeds, HDMI infoframes valid
2 TGL/ADL-P hardware test Gen12 modeset with separate transcoder
3 KWin compositor test Cursor visible, atomic modeset works
4 Mesa glxgears test Hardware-rendered output via Intel Mesa driver
5 Battery life comparison PSR/FBC measurably reduces power consumption

QEMU validation (bounded, not sufficient for claims)

QEMU with qemu64 CPU does not emulate Intel GPU. For Intel DRM testing in QEMU:

  • Use -device virtio-gpu for shared DRM core validation only
  • Real Intel validation requires bare metal or Intel GVT-g passthrough

Hardware test matrix

Hardware Gen Phase 0 Phase 1 Phase 2 Phase 3
Skylake HD 530 desktop 9 Target Target N/A Target
Kaby Lake HD 620 laptop 9 Target Target N/A Target
Coffee Lake UHD 630 9 Target Target N/A Target
Ice Lake Iris Plus 11 Secondary Secondary Target Secondary
Tiger Lake Xe 12 N/A N/A Target Target
Alder Lake-P Iris Xe 12 N/A N/A Target Target

8. Anti-Patterns and Constraints

Code quality rules

  1. No stubs. Every new function must have real register-level implementation. No todo!(), no hardcoded return values, no synthetic data when hardware can provide real data.
  2. No as any or type suppression. All MMIO accesses are typed through the register abstraction layer.
  3. No unwrap() in new code. All fallible operations return Result<T, DriverError>.
  4. Bounds-checked MMIO. Every read_volatile / write_volatile must be preceded by a bounds check against the MMIO region size.
  5. Per-gen registers. No hardcoded offsets in implementation code. All offsets come from regs.rs trait or regs_gen*.rs constants.

Porting rules

  1. Use Linux as reference only. Port the behavior, not the C code structure. Rust idioms (Result, enums, traits) replace C idioms (error codes, macros, function pointers).
  2. Comment Linux source references. Every major function should have a comment like // Ported from intel_gmbus.c:do_gmbus_xfer()
  3. Test incrementally. Each sub-task (0A, 0B, etc.) must compile and be testable independently before integration.
  4. Generation-gate everything. If a register offset differs per generation, it goes in the register table, not as an if/else chain.

Patch discipline

All changes to redox-drm source go as P-patches:

  • P11-gmbus-i2c-edid.patch — Phase 0B
  • P12-display-power-wells.patch — Phase 0C
  • P13-dmc-firmware-upload.patch — Phase 0D
  • P14-cdclk-programming.patch — Phase 0E
  • P15-register-abstraction.patch — Phase 0A
  • etc.

Each patch must:

  1. Be generated via git diff -U0 -w from committed source
  2. Be validated with repo validate-patches redox-drm
  3. Build with CI=1 ./target/release/repo cook redox-drm

9. Dependencies on Other Red Bear Subsystems

Dependency Status Impact on Intel work
redox-driver-sys Build-visible PCI, memory, IRQ access — sufficient for Phase 0
linux-kpi Build-visible Not used by Intel Rust driver (not needed — Intel backend is pure Rust)
firmware-loader Working daemon DMC firmware blobs served via scheme:firmware — ready
libdrm Build-visible with Redox patches Render node support needed for Phase 4
Mesa Software only (swrast) Hardware Intel driver needed for Phase 4 — large separate effort
pcid / pcid-spawner Working Intel GPU device detection and driver launch — working
Kernel scheme:memory Working BAR mapping and DMA buffer allocation — sufficient
Kernel scheme:irq Working MSI/MSI-X delivery — sufficient
Kernel scheme:proc Working Process management — sufficient

10. Out of Scope

The following are explicitly NOT part of this plan:

  • Battlemage / XE2 / Xe driver — Linux i915 does not support these platforms. They use the separate drivers/gpu/drm/xe/ driver. Future work requiring a Xe driver port or abstraction.
  • Lunar Lake, Panther Lake — Same as above. Xe driver territory.
  • GPU virtualization (GVT-g) — Not needed for bare-metal desktop.
  • Gen2-Gen7 support — Legacy hardware. The driver gate correctly blocks these.
  • HDMI/DP audio — Deferred. Display-only first.
  • Content protection (HDCP, PXP) — Deferred indefinitely.
  • DSC (Display Stream Compression) — Deferred to Phase 5+.
  • VRR (Variable Refresh Rate) — Deferred to Phase 5+.