From 29e005e52fd2d889001c48975c9f84cc41a6a0b3 Mon Sep 17 00:00:00 2001 From: vasilito Date: Sun, 28 Jun 2026 10:33:41 +0300 Subject: [PATCH] build: fix gettext libtool mismatch, libxau automake, pre-cook non-fatal, redox-drm restore - gettext: use -I${COOKBOOK_HOST_SYSROOT}/share/aclocal instead of /usr/share/aclocal so autoreconf picks up the Redox-patched libtool 2.5.4 macros instead of the host system's libtool 2.6.1, fixing version mismatch at build time - libxau: add ACLOCAL=true AUTOMAKE=true AUTOHEADER=true to make invocations to prevent automake regeneration when host autotools version differs from what the source expects - build-redbear.sh: make pre-cook failures non-fatal (warn only) and run with COOKBOOK_OFFLINE=false so packages that need source fetching can succeed - redox-drm: restore source from git history (was deleted in dc6805430); update Cargo.toml version 0.1.0 -> 0.2.4 and dependency constraints to match current project version --- local/recipes/gpu/redox-drm/source | 1 - .../gpu/redox-drm/source/.cargo/config.toml | 5 + local/recipes/gpu/redox-drm/source/Cargo.toml | 22 + local/recipes/gpu/redox-drm/source/build.rs | 63 + .../gpu/redox-drm/source/src/driver.rs | 202 ++ .../source/src/drivers/amd/display.rs | 586 ++++ .../redox-drm/source/src/drivers/amd/gtt.rs | 318 ++ .../redox-drm/source/src/drivers/amd/mod.rs | 616 ++++ .../redox-drm/source/src/drivers/amd/ring.rs | 404 +++ .../source/src/drivers/intel/display.rs | 404 +++ .../redox-drm/source/src/drivers/intel/gtt.rs | 226 ++ .../redox-drm/source/src/drivers/intel/mod.rs | 682 +++++ .../source/src/drivers/intel/ring.rs | 267 ++ .../redox-drm/source/src/drivers/interrupt.rs | 244 ++ .../gpu/redox-drm/source/src/drivers/mod.rs | 196 ++ .../source/src/drivers/virtio/mod.rs | 248 ++ local/recipes/gpu/redox-drm/source/src/gem.rs | 162 + .../gpu/redox-drm/source/src/kms/connector.rs | 88 + .../gpu/redox-drm/source/src/kms/crtc.rs | 107 + .../gpu/redox-drm/source/src/kms/encoder.rs | 21 + .../gpu/redox-drm/source/src/kms/mod.rs | 278 ++ .../gpu/redox-drm/source/src/kms/plane.rs | 91 + .../recipes/gpu/redox-drm/source/src/main.rs | 707 +++++ .../gpu/redox-drm/source/src/scheme.rs | 2597 +++++++++++++++++ local/recipes/libs/libxau/recipe.toml | 5 +- local/sources/relibc | 2 +- recipes/tools/gettext/recipe.toml | 12 +- 27 files changed, 8544 insertions(+), 10 deletions(-) delete mode 120000 local/recipes/gpu/redox-drm/source create mode 100644 local/recipes/gpu/redox-drm/source/.cargo/config.toml create mode 100644 local/recipes/gpu/redox-drm/source/Cargo.toml create mode 100644 local/recipes/gpu/redox-drm/source/build.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/driver.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/amd/display.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/amd/gtt.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/amd/mod.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/amd/ring.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/intel/display.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/intel/gtt.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/intel/ring.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/interrupt.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/mod.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/drivers/virtio/mod.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/gem.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/kms/connector.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/kms/crtc.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/kms/encoder.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/kms/mod.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/kms/plane.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/main.rs create mode 100644 local/recipes/gpu/redox-drm/source/src/scheme.rs diff --git a/local/recipes/gpu/redox-drm/source b/local/recipes/gpu/redox-drm/source deleted file mode 120000 index a5334ccca7..0000000000 --- a/local/recipes/gpu/redox-drm/source +++ /dev/null @@ -1 +0,0 @@ -/home/kellito/Builds/RedBear-OS/local/recipes/gpu/redox-drm/../../../sources/redox-drm \ No newline at end of file diff --git a/local/recipes/gpu/redox-drm/source/.cargo/config.toml b/local/recipes/gpu/redox-drm/source/.cargo/config.toml new file mode 100644 index 0000000000..94f8ac9c4c --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/.cargo/config.toml @@ -0,0 +1,5 @@ +[target.x86_64-unknown-redox] +linker = "x86_64-unknown-redox-gcc" + +[build] +target = "x86_64-unknown-redox" diff --git a/local/recipes/gpu/redox-drm/source/Cargo.toml b/local/recipes/gpu/redox-drm/source/Cargo.toml new file mode 100644 index 0000000000..c2cad53227 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "redox-drm" +version = "0.2.4" +edition = "2021" +description = "DRM scheme daemon for Redox OS — provides GPU modesetting and buffer management" + +[dependencies] +redox-driver-sys = { version = "0.2", path = "../../../drivers/redox-driver-sys/source" } +linux-kpi = { version = "0.2", path = "../../../drivers/linux-kpi/source" } +libredox = "0.1" +redox_syscall = { version = "0.8", features = ["std"] } +syscall04 = { package = "redox_syscall", version = "0.4" } +redox-scheme = "0.11" +daemon = { path = "../../../../../recipes/core/base/source/daemon" } +log = "0.4" +thiserror = "2" +bitflags = "2" +getrandom = "0.2" + +[patch.crates-io] +redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" } +linux-kpi = { path = "../../../drivers/linux-kpi/source" } diff --git a/local/recipes/gpu/redox-drm/source/build.rs b/local/recipes/gpu/redox-drm/source/build.rs new file mode 100644 index 0000000000..c524615976 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/build.rs @@ -0,0 +1,63 @@ +use std::env; +use std::path::{Path, PathBuf}; + +const LIB_NAME: &str = "libamdgpu_dc_redox.so"; +const ENV_HINTS: &[&str] = &[ + "AMDGPU_DC_LIB_DIR", + "COOKBOOK_STAGE", + "COOKBOOK_SYSROOT", + "REDOX_SYSROOT", + "SYSROOT", + "TARGET_SYSROOT", +]; + +fn push_candidate_dirs(candidates: &mut Vec, base: &Path) { + candidates.push(base.to_path_buf()); + candidates.push(base.join("usr/lib/redox/drivers")); + candidates.push(base.join("lib")); + candidates.push(base.join("usr/lib")); +} + +fn register_candidate_watch(path: &Path) { + println!("cargo:rerun-if-changed={}", path.display()); +} + +fn find_amdgpu_dc_library(manifest_dir: &Path) -> Option { + let mut candidates = Vec::new(); + + for key in ENV_HINTS { + println!("cargo:rerun-if-env-changed={key}"); + if let Some(value) = env::var_os(key) { + push_candidate_dirs(&mut candidates, Path::new(&value)); + } + } + + push_candidate_dirs(&mut candidates, &manifest_dir.join("../../amdgpu")); + push_candidate_dirs(&mut candidates, &manifest_dir.join("../../amdgpu/stage")); + push_candidate_dirs(&mut candidates, &manifest_dir.join("../amdgpu")); + push_candidate_dirs(&mut candidates, &manifest_dir.join("../amdgpu/stage")); + + for dir in candidates { + register_candidate_watch(&dir.join(LIB_NAME)); + if dir.join(LIB_NAME).exists() { + return Some(dir); + } + } + + None +} + +fn main() { + println!("cargo:rustc-check-cfg=cfg(no_amdgpu_c)"); + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir")); + + if let Some(dir) = find_amdgpu_dc_library(&manifest_dir) { + println!("cargo:rustc-link-search=native={}", dir.display()); + println!("cargo:rustc-link-lib=amdgpu_dc_redox"); + println!("cargo:rustc-link-lib=pthread"); + println!("cargo:rustc-link-lib=m"); + } else { + println!("cargo:rustc-cfg=no_amdgpu_c"); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/driver.rs b/local/recipes/gpu/redox-drm/source/src/driver.rs new file mode 100644 index 0000000000..dffc5c4694 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/driver.rs @@ -0,0 +1,202 @@ +use thiserror::Error; + +use crate::gem::GemHandle; +use crate::kms::{ConnectorInfo, ModeInfo}; + +pub type Result = std::result::Result; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DriverEvent { + Vblank { crtc_id: u32, count: u64 }, + Hotplug { connector_id: u32 }, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct RedoxPrivateCsSubmit { + pub src_handle: GemHandle, + pub dst_handle: GemHandle, + pub src_offset: u64, + pub dst_offset: u64, + pub byte_count: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct RedoxPrivateCsSubmitResult { + pub seqno: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct RedoxPrivateCsWait { + pub seqno: u64, + pub timeout_ns: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub struct RedoxPrivateCsWaitResult { + pub completed: bool, + pub completed_seqno: u64, +} + +#[derive(Debug, Error)] +pub enum DriverError { + #[error("driver initialization failed: {0}")] + Initialization(String), + + #[error("invalid argument: {0}")] + InvalidArgument(&'static str), + + #[error("resource not found: {0}")] + NotFound(String), + + #[error("operation not supported: {0}")] + Unsupported(&'static str), + + #[error("MMIO failure: {0}")] + Mmio(String), + + #[error("PCI failure: {0}")] + Pci(String), + + #[error("buffer failure: {0}")] + Buffer(String), + + #[error("I/O failure: {0}")] + Io(String), +} + +pub trait GpuDriver: Send + Sync { + fn driver_name(&self) -> &str; + fn driver_desc(&self) -> &str; + #[allow(dead_code)] + fn driver_date(&self) -> &str; + + fn detect_connectors(&self) -> Vec; + fn get_modes(&self, connector_id: u32) -> Vec; + fn set_crtc( + &self, + crtc_id: u32, + fb_handle: u32, + connectors: &[u32], + mode: &ModeInfo, + ) -> Result<()>; + fn page_flip(&self, crtc_id: u32, fb_handle: u32, flags: u32) -> Result; + fn get_vblank(&self, crtc_id: u32) -> Result; + + fn gem_create(&self, size: u64) -> Result; + fn gem_close(&self, handle: GemHandle) -> Result<()>; + fn gem_mmap(&self, handle: GemHandle) -> Result; + fn gem_size(&self, handle: GemHandle) -> Result; + + #[allow(dead_code)] + fn get_edid(&self, connector_id: u32) -> Vec; + fn handle_irq(&self) -> Result>; + + fn redox_private_cs_submit( + &self, + _submit: &RedoxPrivateCsSubmit, + ) -> Result { + Err(DriverError::Unsupported( + "private command submission is unavailable on this backend", + )) + } + + fn redox_private_cs_wait( + &self, + _wait: &RedoxPrivateCsWait, + ) -> Result { + Err(DriverError::Unsupported( + "private command completion waits are unavailable on this backend", + )) + } +} + +#[cfg(test)] +mod tests { + use std::mem::{discriminant, offset_of, size_of}; + + use super::*; + + #[test] + fn redox_private_cs_submit_size() { + // src_handle(u32) + dst_handle(u32) + src_offset(u64) + dst_offset(u64) + byte_count(u64) + // = 4 + 4 + 8 + 8 + 8 = 32 bytes + assert_eq!(size_of::(), 32); + } + + #[test] + fn redox_private_cs_submit_result_size() { + // seqno(u64) = 8 bytes + assert_eq!(size_of::(), 8); + } + + #[test] + fn redox_private_cs_wait_size() { + // seqno(u64) + timeout_ns(u64) = 16 bytes + assert_eq!(size_of::(), 16); + } + + #[test] + fn redox_private_cs_wait_result_size() { + // completed(bool) + 7 padding + completed_seqno(u64) = 16 bytes + assert_eq!(size_of::(), 16); + } + + #[test] + fn driver_event_vblank_size() { + let event = DriverEvent::Vblank { + crtc_id: 0xDEADBEEF, + count: 0x1234_5678_9ABC_DEF0, + }; + match event { + DriverEvent::Vblank { crtc_id, count } => { + assert_eq!(crtc_id, 0xDEADBEEF); + assert_eq!(count, 0x1234_5678_9ABC_DEF0); + } + DriverEvent::Hotplug { .. } => panic!("expected Vblank, got Hotplug"), + } + let enum_size = size_of::(); + assert!(enum_size > 0, "DriverEvent must be non-zero-sized"); + } + + #[test] + fn driver_event_hotplug_size() { + let event = DriverEvent::Hotplug { + connector_id: 0xCAFEBABE, + }; + match event { + DriverEvent::Hotplug { connector_id } => { + assert_eq!(connector_id, 0xCAFEBABE); + } + DriverEvent::Vblank { .. } => panic!("expected Hotplug, got Vblank"), + } + let vblank = DriverEvent::Vblank { + crtc_id: 0, + count: 0, + }; + let hotplug = DriverEvent::Hotplug { connector_id: 0 }; + assert_ne!( + discriminant(&vblank), + discriminant(&hotplug), + "Vblank and Hotplug must have distinct discriminants" + ); + } + + #[test] + fn redox_private_cs_submit_is_repr_c() { + assert_eq!(offset_of!(RedoxPrivateCsSubmit, src_handle), 0); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, dst_handle), 4); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, src_offset), 8); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, dst_offset), 16); + assert_eq!(offset_of!(RedoxPrivateCsSubmit, byte_count), 24); + } + + #[test] + fn redox_private_cs_wait_is_repr_c() { + assert_eq!(offset_of!(RedoxPrivateCsWait, seqno), 0); + assert_eq!(offset_of!(RedoxPrivateCsWait, timeout_ns), 8); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/amd/display.rs b/local/recipes/gpu/redox-drm/source/src/drivers/amd/display.rs new file mode 100644 index 0000000000..412433428a --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/amd/display.rs @@ -0,0 +1,586 @@ +use log::{info, warn}; +use std::ptr; +#[cfg(no_amdgpu_c)] +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread; +use std::time::Duration; + +use crate::driver::{DriverError, Result}; +use crate::kms::connector::synthetic_edid; +use crate::kms::{ConnectorInfo, ConnectorStatus, ConnectorType, ModeInfo}; + +#[repr(C)] +pub struct ConnectorInfoFFI { + pub id: i32, + pub connector_type: i32, + pub connector_type_id: i32, + pub connection: i32, + pub mm_width: i32, + pub mm_height: i32, + pub encoder_id: i32, +} + +#[cfg(not(no_amdgpu_c))] +unsafe extern "C" { + #[link_name = "amdgpu_redox_init"] + fn ffi_amdgpu_redox_init( + mmio_base: *const u8, + mmio_size: usize, + fb_phys: u64, + fb_size: usize, + ) -> i32; + + #[link_name = "amdgpu_dc_detect_connectors"] + fn ffi_amdgpu_dc_detect_connectors() -> i32; + #[link_name = "amdgpu_dc_get_connector_info"] + fn ffi_amdgpu_dc_get_connector_info(idx: i32, info: *mut ConnectorInfoFFI) -> i32; + #[link_name = "amdgpu_dc_set_crtc"] + fn ffi_amdgpu_dc_set_crtc(crtc_id: i32, fb_addr: u64, width: u32, height: u32) -> i32; + + #[link_name = "amdgpu_redox_cleanup"] + fn ffi_amdgpu_redox_cleanup(); + + #[link_name = "redox_pci_set_device_info"] + fn ffi_redox_pci_set_device_info( + vendor: u16, + device: u16, + bus_number: u8, + dev_number: u8, + func_number: u8, + revision: u8, + irq: u32, + bar0_addr: u64, + bar0_size: u64, + bar2_addr: u64, + bar2_size: u64, + ); +} + +#[cfg(no_amdgpu_c)] +static FALLBACK_MMIO_BASE: AtomicUsize = AtomicUsize::new(0); +#[cfg(no_amdgpu_c)] +static FALLBACK_MMIO_SIZE: AtomicUsize = AtomicUsize::new(0); + +#[cfg(no_amdgpu_c)] +const FALLBACK_ENOENT: i32 = 2; + +#[cfg(no_amdgpu_c)] +fn amdgpu_dc_init(mmio_base: *const u8, mmio_size: usize) -> i32 { + FALLBACK_MMIO_BASE.store(mmio_base as usize, Ordering::Relaxed); + FALLBACK_MMIO_SIZE.store(mmio_size, Ordering::Relaxed); + 0 +} + +#[cfg(no_amdgpu_c)] +fn amdgpu_dc_init_with_fb( + mmio_base: *const u8, + mmio_size: usize, + _fb_phys: u64, + _fb_size: usize, +) -> i32 { + FALLBACK_MMIO_BASE.store(mmio_base as usize, Ordering::Relaxed); + FALLBACK_MMIO_SIZE.store(mmio_size, Ordering::Relaxed); + 0 +} + +#[cfg(no_amdgpu_c)] +fn amdgpu_dc_detect_connectors() -> i32 { + warn!("redox-drm: compiled without AMD C backend (no_amdgpu_c); no real connector detection available"); + 0 +} + +#[cfg(no_amdgpu_c)] +fn amdgpu_dc_get_connector_info(_idx: i32, _info: *mut ConnectorInfoFFI) -> i32 { + -FALLBACK_ENOENT +} + +#[cfg(no_amdgpu_c)] +fn amdgpu_dc_set_crtc(_crtc_id: i32, _fb_addr: u64, _width: u32, _height: u32) -> i32 { + 0 +} + +#[cfg(no_amdgpu_c)] +fn amdgpu_dc_cleanup() { + FALLBACK_MMIO_BASE.store(0, Ordering::Relaxed); + FALLBACK_MMIO_SIZE.store(0, Ordering::Relaxed); +} + +pub fn set_pci_device_info( + vendor: u16, + device: u16, + bus_number: u8, + dev_number: u8, + func_number: u8, + revision: u8, + irq: u32, + bar0_addr: u64, + bar0_size: u64, + bar2_addr: u64, + bar2_size: u64, +) { + #[cfg(not(no_amdgpu_c))] + unsafe { + ffi_redox_pci_set_device_info( + vendor, + device, + bus_number, + dev_number, + func_number, + revision, + irq, + bar0_addr, + bar0_size, + bar2_addr, + bar2_size, + ); + } + let _ = ( + vendor, + device, + bus_number, + dev_number, + func_number, + revision, + irq, + bar0_addr, + bar0_size, + bar2_addr, + bar2_size, + ); +} + +#[cfg(not(no_amdgpu_c))] +fn amdgpu_dc_init(mmio_base: *const u8, mmio_size: usize) -> i32 { + unsafe { ffi_amdgpu_redox_init(mmio_base, mmio_size, 0, 0) } +} + +#[cfg(not(no_amdgpu_c))] +fn amdgpu_dc_init_with_fb( + mmio_base: *const u8, + mmio_size: usize, + fb_phys: u64, + fb_size: usize, +) -> i32 { + unsafe { ffi_amdgpu_redox_init(mmio_base, mmio_size, fb_phys, fb_size) } +} + +#[cfg(not(no_amdgpu_c))] +fn amdgpu_dc_detect_connectors() -> i32 { + unsafe { ffi_amdgpu_dc_detect_connectors() } +} + +#[cfg(not(no_amdgpu_c))] +fn amdgpu_dc_get_connector_info(idx: i32, info: *mut ConnectorInfoFFI) -> i32 { + unsafe { ffi_amdgpu_dc_get_connector_info(idx, info) } +} + +#[cfg(not(no_amdgpu_c))] +fn amdgpu_dc_set_crtc(crtc_id: i32, fb_addr: u64, width: u32, height: u32) -> i32 { + unsafe { ffi_amdgpu_dc_set_crtc(crtc_id, fb_addr, width, height) } +} + +#[cfg(not(no_amdgpu_c))] +fn amdgpu_dc_cleanup() { + unsafe { ffi_amdgpu_redox_cleanup() } +} + +pub struct DisplayCore { + initialized: bool, + mmio_base: usize, + mmio_size: usize, + fb_phys: u64, + fb_size: usize, +} + +impl DisplayCore { + pub fn new(mmio_base: *const u8, mmio_size: usize) -> Result { + Self::with_framebuffer(mmio_base, mmio_size, 0, 0) + } + + pub fn with_framebuffer( + mmio_base: *const u8, + mmio_size: usize, + fb_phys: u64, + fb_size: usize, + ) -> Result { + let rc = if fb_phys != 0 && fb_size != 0 { + amdgpu_dc_init_with_fb(mmio_base, mmio_size, fb_phys, fb_size) + } else { + amdgpu_dc_init(mmio_base, mmio_size) + }; + if rc < 0 { + return Err(DriverError::Initialization(format!( + "amdgpu display init failed with status {}", + rc + ))); + } + + info!( + "redox-drm: AMD DC initialized with {} bytes of MMIO, fb_phys={:#x}, fb_size={}", + mmio_size, fb_phys, fb_size + ); + Ok(Self { + initialized: true, + mmio_base: mmio_base as usize, + mmio_size, + fb_phys, + fb_size, + }) + } + + pub fn fb_phys(&self) -> u64 { + self.fb_phys + } + + pub fn fb_size(&self) -> usize { + self.fb_size + } + + pub fn detect_connectors(&self) -> Result> { + if !self.initialized { + return Err(DriverError::Initialization( + "display core not initialized".to_string(), + )); + } + + let count = amdgpu_dc_detect_connectors(); + if count < 0 { + return Err(DriverError::Mmio(format!( + "AMD DC connector detection failed with status {}", + count + ))); + } + if count == 0 { + warn!("redox-drm: AMD DC reported 0 connected displays"); + return Ok(Vec::new()); + } + + let mut connectors = Vec::new(); + for idx in 0..count { + let mut raw = ConnectorInfoFFI { + id: 0, + connector_type: 0, + connector_type_id: 0, + connection: 2, + mm_width: 0, + mm_height: 0, + encoder_id: 0, + }; + + let rc = amdgpu_dc_get_connector_info(idx, &mut raw as *mut ConnectorInfoFFI); + if rc < 0 { + warn!( + "redox-drm: failed to fetch connector {} from AMD DC (status {})", + idx, rc + ); + continue; + } + + connectors.push(ConnectorInfo { + id: raw.id.max(0) as u32, + connector_type: map_connector_type(raw.connector_type), + connector_type_id: raw.connector_type_id.max(0) as u32, + connection: map_connection_status(raw.connection), + mm_width: raw.mm_width.max(0) as u32, + mm_height: raw.mm_height.max(0) as u32, + encoder_id: raw.encoder_id.max(0) as u32, + modes: self.modes_for_connector(idx as u32), + }); + } + + Ok(connectors) + } + + pub fn set_crtc(&self, crtc_id: u32, fb_addr: u64, width: u32, height: u32) -> Result<()> { + if !self.initialized { + return Err(DriverError::Initialization( + "display core must be initialized before modesetting".to_string(), + )); + } + + let rc = amdgpu_dc_set_crtc(crtc_id as i32, fb_addr, width, height); + if rc < 0 { + return Err(DriverError::Mmio(format!( + "amdgpu_dc_set_crtc failed for CRTC {} with status {}", + crtc_id, rc + ))); + } + + Ok(()) + } + + pub fn flip_surface(&self, crtc_id: u32, fb_addr: u64) -> Result<()> { + if !self.initialized { + return Err(DriverError::Initialization( + "display core must be initialized before page flip".to_string(), + )); + } + + const HUBP_FLIP_ADDR_LOW: usize = 0x5800; + const HUBP_FLIP_ADDR_HIGH: usize = 0x5804; + + let hubp_base = HUBP_FLIP_ADDR_LOW + (crtc_id as usize) * 0x400; + let hubp_high = HUBP_FLIP_ADDR_HIGH + (crtc_id as usize) * 0x400; + + self.write_reg(hubp_high, (fb_addr >> 32) as u32)?; + self.write_reg(hubp_base, fb_addr as u32)?; + + let flip_control = 0x5834 + (crtc_id as usize) * 0x400; + self.write_reg(flip_control, 1)?; + + Ok(()) + } + + pub fn read_edid(&self, connector_index: u32) -> Vec { + if !self.initialized { + return Vec::new(); + } + + match self.read_edid_block(connector_index, 0x00) { + Ok(edid) if edid.len() >= 128 => edid, + Ok(short) => { + log::warn!( + "redox-drm: short EDID ({} bytes) from AMD connector {}", + short.len(), + connector_index + ); + Vec::new() + } + Err(e) => { + log::warn!( + "redox-drm: EDID read failed for AMD connector {}: {}", + connector_index, + e + ); + Vec::new() + } + } + } + + fn modes_for_connector(&self, connector_index: u32) -> Vec { + let real_edid = self.read_edid(connector_index); + let mut modes = ModeInfo::from_edid(&real_edid); + if modes.is_empty() { + modes = ModeInfo::from_edid(&synthetic_edid()); + } + if modes.is_empty() { + modes.push(ModeInfo::default_1080p()); + } + modes + } + + fn read_edid_block(&self, connector_index: u32, offset: u8) -> Result> { + const MM_DC_I2C_CONTROL: usize = 0x1e98; + const MM_DC_I2C_ARBITRATION: usize = 0x1e99; + const MM_DC_I2C_SW_STATUS: usize = 0x1e9b; + const MM_DC_I2C_DDC1_SPEED: usize = 0x1ea2; + const MM_DC_I2C_DDC1_SETUP: usize = 0x1ea3; + const MM_DC_I2C_TRANSACTION0: usize = 0x1eae; + const MM_DC_I2C_TRANSACTION1: usize = 0x1eaf; + const MM_DC_I2C_DATA: usize = 0x1eb2; + + const CONTROL_GO: u32 = 0x0000_0001; + const CONTROL_SOFT_RESET: u32 = 0x0000_0002; + const CONTROL_SW_STATUS_RESET: u32 = 0x0000_0008; + const CONTROL_DDC_SELECT_MASK: u32 = 0x0000_0700; + const CONTROL_DDC_SELECT_SHIFT: u32 = 8; + const CONTROL_TRANSACTION_COUNT_MASK: u32 = 0x0030_0000; + const CONTROL_TRANSACTION_COUNT_SHIFT: u32 = 20; + + const ARBITRATION_STATUS_MASK: u32 = 0x0000_000c; + const ARBITRATION_STATUS_SHIFT: u32 = 2; + const ARBITRATION_REQ: u32 = 0x0010_0000; + const ARBITRATION_DONE: u32 = 0x0020_0000; + + const SW_STATUS_DONE: u32 = 0x0000_0004; + const SW_STATUS_ABORTED: u32 = 0x0000_0010; + const SW_STATUS_TIMEOUT: u32 = 0x0000_0020; + const SW_STATUS_NACK: u32 = 0x0000_0100; + + const SETUP_ENABLE: u32 = 0x0000_0040; + const SETUP_SEND_RESET_LENGTH: u32 = 0x0000_0004; + const SETUP_TIME_LIMIT_SHIFT: u32 = 24; + + const SPEED_THRESHOLD: u32 = 0x0000_0002; + const SPEED_PRESCALE_SHIFT: u32 = 16; + const SPEED_START_STOP_TIMING: u32 = 0x0000_0200; + + const TX_RW: u32 = 0x0000_0001; + const TX_STOP_ON_NACK: u32 = 0x0000_0100; + const TX_START: u32 = 0x0000_1000; + const TX_STOP: u32 = 0x0000_2000; + const TX_COUNT_SHIFT: u32 = 16; + + const DATA_RW: u32 = 0x0000_0001; + const DATA_VALUE_SHIFT: u32 = 8; + const DATA_VALUE_MASK: u32 = 0x0000_ff00; + const DATA_INDEX_SHIFT: u32 = 16; + const DATA_INDEX_WRITE: u32 = 0x8000_0000; + + const EDID_WRITE_ADDR: u8 = 0xa0; + const EDID_READ_ADDR: u8 = 0xa1; + const EDID_BLOCK_SIZE: usize = 128; + const I2C_STATUS_IDLE: u32 = 0; + const I2C_STATUS_USED_BY_SW: u32 = 1; + const I2C_WAIT_RETRIES: usize = 200; + + self.ensure_mmio_reg(MM_DC_I2C_DATA)?; + self.ensure_mmio_reg(MM_DC_I2C_TRANSACTION1)?; + + let connector_select = connector_index & 0x7; + let arbitration = self.read_reg(MM_DC_I2C_ARBITRATION)?; + let status = (arbitration & ARBITRATION_STATUS_MASK) >> ARBITRATION_STATUS_SHIFT; + if status == I2C_STATUS_IDLE { + self.write_reg(MM_DC_I2C_ARBITRATION, arbitration | ARBITRATION_REQ)?; + } else if status != I2C_STATUS_USED_BY_SW { + return Err(DriverError::Mmio(format!( + "AMD I2C engine unavailable for connector {} (status {})", + connector_index, status + ))); + } + + let control = self.read_reg(MM_DC_I2C_CONTROL)?; + self.write_reg( + MM_DC_I2C_CONTROL, + (control + & !(CONTROL_SOFT_RESET | CONTROL_DDC_SELECT_MASK | CONTROL_TRANSACTION_COUNT_MASK)) + | CONTROL_SW_STATUS_RESET + | (connector_select << CONTROL_DDC_SELECT_SHIFT), + )?; + + self.write_reg( + MM_DC_I2C_DDC1_SETUP, + SETUP_ENABLE | SETUP_SEND_RESET_LENGTH | (3 << SETUP_TIME_LIMIT_SHIFT), + )?; + self.write_reg( + MM_DC_I2C_DDC1_SPEED, + SPEED_THRESHOLD | SPEED_START_STOP_TIMING | (40 << SPEED_PRESCALE_SHIFT), + )?; + self.write_reg( + MM_DC_I2C_TRANSACTION0, + TX_START | TX_STOP_ON_NACK | (1 << TX_COUNT_SHIFT), + )?; + self.write_reg( + MM_DC_I2C_TRANSACTION1, + TX_RW + | TX_START + | TX_STOP + | TX_STOP_ON_NACK + | ((EDID_BLOCK_SIZE as u32) << TX_COUNT_SHIFT), + )?; + + self.write_reg( + MM_DC_I2C_DATA, + ((EDID_WRITE_ADDR as u32) << DATA_VALUE_SHIFT) | DATA_INDEX_WRITE, + )?; + self.write_reg(MM_DC_I2C_DATA, (offset as u32) << DATA_VALUE_SHIFT)?; + self.write_reg(MM_DC_I2C_DATA, (EDID_READ_ADDR as u32) << DATA_VALUE_SHIFT)?; + + let control = self.read_reg(MM_DC_I2C_CONTROL)?; + self.write_reg( + MM_DC_I2C_CONTROL, + (control & !CONTROL_TRANSACTION_COUNT_MASK) + | (1 << CONTROL_TRANSACTION_COUNT_SHIFT) + | CONTROL_GO, + )?; + + let mut final_status = 0; + for _ in 0..I2C_WAIT_RETRIES { + final_status = self.read_reg(MM_DC_I2C_SW_STATUS)?; + if (final_status + & (SW_STATUS_DONE | SW_STATUS_ABORTED | SW_STATUS_TIMEOUT | SW_STATUS_NACK)) + != 0 + { + break; + } + thread::sleep(Duration::from_millis(1)); + } + + self.write_reg(MM_DC_I2C_ARBITRATION, ARBITRATION_DONE)?; + + if (final_status & SW_STATUS_DONE) == 0 { + return Err(DriverError::Mmio(format!( + "AMD I2C EDID read did not complete for connector {} (status {:#x})", + connector_index, final_status + ))); + } + if (final_status & (SW_STATUS_ABORTED | SW_STATUS_TIMEOUT | SW_STATUS_NACK)) != 0 { + return Err(DriverError::Mmio(format!( + "AMD I2C EDID read failed for connector {} (status {:#x})", + connector_index, final_status + ))); + } + + self.write_reg( + MM_DC_I2C_DATA, + DATA_RW | DATA_INDEX_WRITE | ((2_u32) << DATA_INDEX_SHIFT), + )?; + + let mut edid = Vec::with_capacity(EDID_BLOCK_SIZE); + for _ in 0..EDID_BLOCK_SIZE { + let value = self.read_reg(MM_DC_I2C_DATA)?; + edid.push(((value & DATA_VALUE_MASK) >> DATA_VALUE_SHIFT) as u8); + } + + Ok(edid) + } + + fn ensure_mmio_reg(&self, reg: usize) -> Result<()> { + let offset = reg.checked_mul(4).ok_or_else(|| { + DriverError::Mmio(format!("AMD register offset overflow for {reg:#x}")) + })?; + if offset + 4 > self.mmio_size { + return Err(DriverError::Mmio(format!( + "AMD register {reg:#x} outside MMIO aperture {:#x}", + self.mmio_size + ))); + } + Ok(()) + } + + fn read_reg(&self, reg: usize) -> Result { + self.ensure_mmio_reg(reg)?; + let offset = reg * 4; + let ptr = (self.mmio_base + offset) as *const u32; + let value = unsafe { ptr::read_volatile(ptr) }; + Ok(u32::from_le(value)) + } + + fn write_reg(&self, reg: usize, value: u32) -> Result<()> { + self.ensure_mmio_reg(reg)?; + let offset = reg * 4; + let ptr = (self.mmio_base + offset) as *mut u32; + unsafe { ptr::write_volatile(ptr, value.to_le()) }; + Ok(()) + } +} + +impl Drop for DisplayCore { + fn drop(&mut self) { + if self.initialized { + amdgpu_dc_cleanup(); + } + } +} + +fn map_connector_type(value: i32) -> ConnectorType { + match value { + 1 => ConnectorType::VGA, + 2 => ConnectorType::DVII, + 3 => ConnectorType::DVID, + 4 => ConnectorType::DVIA, + 10 => ConnectorType::DisplayPort, + 11 => ConnectorType::HDMIA, + 14 => ConnectorType::EDP, + 15 => ConnectorType::Virtual, + _ => ConnectorType::Unknown, + } +} + +fn map_connection_status(value: i32) -> ConnectorStatus { + match value { + 1 => ConnectorStatus::Connected, + 2 => ConnectorStatus::Disconnected, + _ => ConnectorStatus::Unknown, + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/amd/gtt.rs b/local/recipes/gpu/redox-drm/source/src/drivers/amd/gtt.rs new file mode 100644 index 0000000000..8a5012cfa8 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/amd/gtt.rs @@ -0,0 +1,318 @@ +use std::collections::BTreeMap; + +use log::{info, warn}; +use redox_driver_sys::dma::DmaBuffer; +use redox_driver_sys::memory::MmioRegion; + +use crate::driver::{DriverError, Result}; + +const GPU_PAGE_SIZE: u64 = 4096; +const PAGE_TABLE_LEVELS: usize = 4; +const PTE_COUNT: usize = 512; +const PT_BYTES: usize = PTE_COUNT * 8; +const PTE_INDEX_MASK: u64 = 0x1ff; +const PAGE_OFFSET_MASK: u64 = GPU_PAGE_SIZE - 1; +const AMD_PTE_VALID: u64 = 1 << 0; +const AMD_PTE_SYSTEM: u64 = 1 << 1; +const AMD_PTE_FLAG_MASK: u64 = 0x0fff; +const AMD_PTE_ADDR_MASK: u64 = 0x000f_ffff_ffff_f000; +const GTT_MIN_VA_SIZE: u64 = 256 * 1024 * 1024; +const TLB_POLL_LIMIT: usize = 10_000; + +// GC 11.0 (RDNA2) VM register offsets (DWORD index * 4 = byte offset) +const MM_VM_CONTEXT0_CNTL: usize = 0x1688 * 4; +const MM_VM_CONTEXT0_PT_BASE_LO32: usize = 0x16f3 * 4; +const MM_VM_CONTEXT0_PT_BASE_HI32: usize = 0x16f4 * 4; +const MM_VM_CONTEXT0_PT_START_LO32: usize = 0x1713 * 4; +const MM_VM_CONTEXT0_PT_START_HI32: usize = 0x1714 * 4; +const MM_VM_CONTEXT0_PT_END_LO32: usize = 0x1733 * 4; +const MM_VM_CONTEXT0_PT_END_HI32: usize = 0x1734 * 4; +const MMVM_INVALIDATE_ENG0_REQ: usize = 0x16ab * 4; +const MMVM_INVALIDATE_ENG0_ACK: usize = 0x16bd * 4; + +struct PageTable { + dma: DmaBuffer, + children: BTreeMap>, +} + +impl PageTable { + fn allocate() -> Result { + let dma = DmaBuffer::allocate(PT_BYTES, 4096) + .map_err(|e| DriverError::Buffer(format!("GTT page table alloc failed: {e}")))?; + if !dma.is_physically_contiguous() { + warn!("redox-drm: GTT page table not guaranteed physically contiguous"); + } + Ok(Self { + dma, + children: BTreeMap::new(), + }) + } + + fn phys(&self) -> u64 { + self.dma.physical_address() as u64 + } + + fn entries(&self) -> &[u64] { + unsafe { std::slice::from_raw_parts(self.dma.as_ptr() as *const u64, PTE_COUNT) } + } + + fn entries_mut(&mut self) -> &mut [u64] { + unsafe { std::slice::from_raw_parts_mut(self.dma.as_mut_ptr() as *mut u64, PTE_COUNT) } + } + + fn map_page(&mut self, level: usize, gpu_addr: u64, phys_addr: u64, flags: u64) -> Result<()> { + let idx = pt_index(gpu_addr, level)?; + if level == PAGE_TABLE_LEVELS - 1 { + self.entries_mut()[idx] = encode_pte(phys_addr, flags); + return Ok(()); + } + let child = match self.children.get_mut(&idx) { + Some(c) => c, + None => { + let c = Box::new(PageTable::allocate()?); + let c_phys = c.phys(); + self.entries_mut()[idx] = + (c_phys & AMD_PTE_ADDR_MASK) | AMD_PTE_VALID | AMD_PTE_SYSTEM; + self.children.entry(idx).or_insert(c) + } + }; + child.map_page(level + 1, gpu_addr, phys_addr, flags) + } + + fn unmap_page(&mut self, level: usize, gpu_addr: u64) -> Result<()> { + let idx = pt_index(gpu_addr, level)?; + if level == PAGE_TABLE_LEVELS - 1 { + self.entries_mut()[idx] = 0; + return Ok(()); + } + if let Some(child) = self.children.get_mut(&idx) { + child.unmap_page(level + 1, gpu_addr)?; + } + Ok(()) + } + + fn translate(&self, level: usize, gpu_addr: u64) -> Option { + let idx = pt_index(gpu_addr, level).ok()?; + let entry = self.entries()[idx]; + if entry & AMD_PTE_VALID == 0 { + return None; + } + if level == PAGE_TABLE_LEVELS - 1 { + return Some((entry & AMD_PTE_ADDR_MASK) | (gpu_addr & PAGE_OFFSET_MASK)); + } + self.children.get(&idx)?.translate(level + 1, gpu_addr) + } +} + +pub struct GttManager { + initialized: bool, + root: Option, + va_start: u64, + va_end: u64, + fb_offset: u64, + next_alloc: u64, + free_list: Vec<(u64, u64)>, +} + +impl Default for GttManager { + fn default() -> Self { + Self::new() + } +} + +impl GttManager { + pub fn new() -> Self { + Self { + initialized: false, + root: None, + va_start: 0, + va_end: GTT_MIN_VA_SIZE - 1, + fb_offset: 0, + next_alloc: 0, + free_list: Vec::new(), + } + } + + pub fn initialize(&mut self) -> Result<()> { + if self.root.is_none() { + self.root = Some(PageTable::allocate()?); + } + self.fb_offset = 0; + self.va_start = self.fb_offset; + self.va_end = self + .va_start + .checked_add(GTT_MIN_VA_SIZE) + .ok_or_else(|| DriverError::Initialization("GTT VA range overflow".into()))?; + self.next_alloc = self.va_start; + self.initialized = true; + info!( + "redox-drm: AMD GTT initialized va={:#x}..{:#x} root_pt={:#x}", + self.va_start, + self.va_end, + self.root.as_ref().map(|r| r.phys()).unwrap_or(0) + ); + Ok(()) + } + + pub fn is_initialized(&self) -> bool { + self.initialized + } + + pub fn alloc_gpu_range(&mut self, size: u64) -> Result { + self.ensure_init()?; + let aligned_size = (size + GPU_PAGE_SIZE - 1) & !(GPU_PAGE_SIZE - 1); + if let Some(idx) = self.free_list.iter().position(|&(_, s)| s >= aligned_size) { + let (start, free_size) = self.free_list.remove(idx); + let remainder = free_size - aligned_size; + if remainder > 0 { + self.free_list.push((start + aligned_size, remainder)); + } + return Ok(start); + } + let gpu_addr = self.next_alloc; + let new_next = gpu_addr + .checked_add(aligned_size) + .ok_or_else(|| DriverError::Buffer("GTT VA allocation overflow".into()))?; + if new_next > self.va_end { + return Err(DriverError::Buffer(format!( + "GTT VA space exhausted: need {:#x}..{:#x}, have ..{:#x}", + gpu_addr, new_next, self.va_end + ))); + } + self.next_alloc = new_next; + Ok(gpu_addr) + } + + pub fn unmap_range(&mut self, gpu_start: u64, size: u64) -> Result<()> { + self.ensure_init()?; + let aligned_size = (size + GPU_PAGE_SIZE - 1) & !(GPU_PAGE_SIZE - 1); + let num_pages = (aligned_size / GPU_PAGE_SIZE) as usize; + for i in 0..num_pages { + let gpu_addr = gpu_start + (i as u64) * GPU_PAGE_SIZE; + self.root + .as_mut() + .ok_or_else(|| DriverError::Initialization("GTT root missing".into()))? + .unmap_page(0, gpu_addr)?; + } + Ok(()) + } + + pub fn release_range(&mut self, gpu_start: u64, size: u64) { + let aligned_size = (size + GPU_PAGE_SIZE - 1) & !(GPU_PAGE_SIZE - 1); + self.free_list.push((gpu_start, aligned_size)); + } + + pub fn map_page(&mut self, gpu_addr: u64, phys_addr: u64, flags: u64) -> Result<()> { + self.ensure_init()?; + if gpu_addr & PAGE_OFFSET_MASK != 0 { + return Err(DriverError::InvalidArgument("gpu_addr not page-aligned")); + } + if phys_addr & PAGE_OFFSET_MASK != 0 { + return Err(DriverError::InvalidArgument("phys_addr not page-aligned")); + } + if gpu_addr < self.va_start || gpu_addr > self.va_end { + return Err(DriverError::InvalidArgument( + "gpu_addr outside GTT aperture", + )); + } + self.root + .as_mut() + .ok_or_else(|| DriverError::Initialization("GTT root missing".into()))? + .map_page(0, gpu_addr, phys_addr, flags) + } + + pub fn unmap_page(&mut self, gpu_addr: u64) -> Result<()> { + self.ensure_init()?; + self.root + .as_mut() + .ok_or_else(|| DriverError::Initialization("GTT root missing".into()))? + .unmap_page(0, gpu_addr) + } + + pub fn map_range( + &mut self, + gpu_start: u64, + phys_start: u64, + size: u64, + flags: u64, + ) -> Result<()> { + self.ensure_init()?; + let aligned_size = (size + GPU_PAGE_SIZE - 1) & !(GPU_PAGE_SIZE - 1); + let num_pages = (aligned_size / GPU_PAGE_SIZE) as usize; + for i in 0..num_pages { + let gpu_addr = gpu_start + (i as u64) * GPU_PAGE_SIZE; + let phys_addr = phys_start + (i as u64) * GPU_PAGE_SIZE; + self.map_page(gpu_addr, phys_addr, flags)?; + } + Ok(()) + } + + pub fn flush_tlb(&self, mmio: &MmioRegion) -> Result<()> { + if !self.initialized { + return Err(DriverError::Initialization("GTT not initialized".into())); + } + let req = + (1u32 << 0) | (1u32 << 19) | (1u32 << 20) | (1u32 << 21) | (1u32 << 22) | (1u32 << 23); + mmio.write32(MMVM_INVALIDATE_ENG0_REQ, req); + for _ in 0..TLB_POLL_LIMIT { + let ack = mmio.read32(MMVM_INVALIDATE_ENG0_ACK); + if ack & (1u32 << 0) != 0 { + return Ok(()); + } + } + Err(DriverError::Mmio("GTT TLB flush timeout".into())) + } + + pub fn translate(&self, gpu_addr: u64) -> Option { + if !self.initialized || gpu_addr < self.va_start || gpu_addr > self.va_end { + return None; + } + self.root.as_ref()?.translate(0, gpu_addr) + } + + pub fn program_vm_context(&self, mmio: &MmioRegion) -> Result<()> { + let root_phys = self + .root + .as_ref() + .map(|r| r.phys()) + .ok_or_else(|| DriverError::Initialization("GTT root missing".into()))?; + + mmio.write32(MM_VM_CONTEXT0_PT_BASE_LO32, root_phys as u32); + mmio.write32(MM_VM_CONTEXT0_PT_BASE_HI32, (root_phys >> 32) as u32); + + let va_start_pages = self.va_start >> 12; + let va_end_pages = self.va_end >> 12; + mmio.write32(MM_VM_CONTEXT0_PT_START_LO32, va_start_pages as u32); + mmio.write32(MM_VM_CONTEXT0_PT_START_HI32, (va_start_pages >> 32) as u32); + mmio.write32(MM_VM_CONTEXT0_PT_END_LO32, va_end_pages as u32); + mmio.write32(MM_VM_CONTEXT0_PT_END_HI32, (va_end_pages >> 32) as u32); + + // Enable VM context 0: depth=0 (4-level), block_size=0 (4KB pages) + mmio.write32(MM_VM_CONTEXT0_CNTL, 1); + + self.flush_tlb(mmio) + } + + fn ensure_init(&self) -> Result<()> { + if !self.initialized { + return Err(DriverError::Initialization( + "GTT manager not initialized".into(), + )); + } + Ok(()) + } +} + +fn pt_index(gpu_addr: u64, level: usize) -> Result { + if level >= PAGE_TABLE_LEVELS { + return Err(DriverError::Initialization(format!( + "invalid PT level {level}" + ))); + } + let shift = 12 + ((PAGE_TABLE_LEVELS - 1 - level) * 9); + Ok(((gpu_addr >> shift) & PTE_INDEX_MASK) as usize) +} + +fn encode_pte(phys_addr: u64, flags: u64) -> u64 { + (phys_addr & AMD_PTE_ADDR_MASK) | (flags & AMD_PTE_FLAG_MASK) | AMD_PTE_VALID | AMD_PTE_SYSTEM +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/amd/mod.rs b/local/recipes/gpu/redox-drm/source/src/drivers/amd/mod.rs new file mode 100644 index 0000000000..0603e00e98 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/amd/mod.rs @@ -0,0 +1,616 @@ +pub mod display; +pub mod gtt; +pub mod ring; + +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Mutex; + +use log::{debug, info, warn}; +use redox_driver_sys::memory::MmioRegion; +use redox_driver_sys::pci::{PciBarInfo, PciDevice, PciDeviceInfo}; + +use crate::driver::{DriverError, DriverEvent, GpuDriver, Result}; +use crate::drivers::interrupt::InterruptHandle; +use crate::gem::{GemHandle, GemManager}; +use crate::kms::connector::{synthetic_edid, Connector}; +use crate::kms::crtc::Crtc; +use crate::kms::encoder::Encoder; +use crate::kms::{ConnectorInfo, ModeInfo}; + +use self::display::DisplayCore; +use self::gtt::GttManager; +use self::ring::RingManager; + +const AMD_IH_RB_CNTL: usize = 0x0080; +const AMD_IH_RB_RPTR: usize = 0x0083; +const AMD_IH_RB_WPTR: usize = 0x0084; +const AMD_IH_CNTL: usize = 0x00c0; +const AMD_IH_STATUS: usize = 0x00c2; + +const AMD_DCN_DISP_INTERRUPT_STATUS: [usize; 6] = [0x012a, 0x012b, 0x012c, 0x012d, 0x012e, 0x012f]; +const AMD_DCN_HPD_INT_STATUS: [usize; 6] = [0x1f14, 0x1f1c, 0x1f24, 0x1f2c, 0x1f34, 0x1f3c]; +const AMD_DCN_HPD_CONTROL: [usize; 6] = [0x1f16, 0x1f1e, 0x1f26, 0x1f2e, 0x1f36, 0x1f3e]; + +const AMD_DISP_INTERRUPT_VBLANK_MASK: u32 = 0x0000_0008; +const AMD_DISP_INTERRUPT_HPD_MASK: u32 = 0x0002_0000; +const AMD_HPD_INT_STATUS_MASK: u32 = 0x0000_0001; +const AMD_HPD_RX_INT_STATUS_MASK: u32 = 0x0000_0100; +const AMD_HPD_INT_ACK_MASK: u32 = 0x0000_0001; +const AMD_HPD_RX_INT_ACK_MASK: u32 = 0x0000_0100; +const AMD_IH_STATUS_INTERRUPT_PENDING_MASK: u32 = 0x0000_0001; +const AMD_IH_STATUS_RING_OVERFLOW_MASK: u32 = 0x0000_0002; + +pub struct AmdDriver { + info: PciDeviceInfo, + mmio: MmioRegion, + irq_handle: Mutex>, + display: DisplayCore, + gem: Mutex, + connectors: Mutex>, + crtcs: Mutex>, + encoders: Mutex>, + gtt: Mutex, + ring: Mutex, + vblank_count: AtomicU64, + hotplug_pending: AtomicBool, + firmware: HashMap>, +} + +impl AmdDriver { + pub fn new(info: PciDeviceInfo, firmware: HashMap>) -> Result { + let bar0 = find_memory_bar0(&info)?; + let bar2 = info.find_memory_bar(2).copied(); + let mut device = PciDevice::open_location(&info.location) + .map_err(|e| DriverError::Pci(format!("failed to re-open PCI device: {e}")))?; + device + .enable_device() + .map_err(|e| DriverError::Pci(format!("enable_device failed: {e}")))?; + let mmio = device + .map_bar(bar0.index, bar0.addr, bar0.size as usize) + .map_err(|e| DriverError::Mmio(format!("map_bar failed: {e}")))?; + + let pci_id = mmio.read32(0); + debug!( + "redox-drm: mapped AMD MMIO BAR0 addr={:#x} size={:#x} idreg={:#x}", + bar0.addr, bar0.size, pci_id + ); + + let (fb_phys, fb_size) = match &bar2 { + Some(bar) => { + debug!( + "redox-drm: AMD VRAM BAR2 addr={:#x} size={:#x}", + bar.addr, bar.size + ); + (bar.addr, bar.size as usize) + } + None => { + return Err(DriverError::Pci(format!( + "AMD device {} has no VRAM BAR2 — cannot initialize display without framebuffer aperture", + info.location + ))); + } + }; + + display::set_pci_device_info( + info.vendor_id, + info.device_id, + info.location.bus, + info.location.device, + info.location.function, + info.revision, + info.irq.unwrap_or(0), + bar0.addr, + bar0.size, + bar2.as_ref().map(|b| b.addr).unwrap_or(0), + bar2.as_ref().map(|b| b.size).unwrap_or(0), + ); + + let irq_handle = Some(InterruptHandle::setup(&info, &mut device).map_err(|e| { + DriverError::Io(format!( + "failed to setup interrupt for {}: {e}", + info.location + )) + })?); + let irq_mode = irq_handle + .as_ref() + .map(|handle| handle.mode_name()) + .unwrap_or("none"); + + let display = DisplayCore::with_framebuffer(mmio.as_ptr(), mmio.size(), fb_phys, fb_size)?; + let (connectors, encoders) = detect_display_topology(&display)?; + + RingManager::bind_mmio(&mmio); + + let mut gtt = GttManager::new(); + gtt.initialize()?; + gtt.program_vm_context(&mmio)?; + + let mut ring = RingManager::new(); + ring.initialize()?; + + let fw_count = firmware.len(); + let dmcub_available = firmware.contains_key("amdgpu/dmcub_dcn31.bin") + || firmware.contains_key("amdgpu/dcn_3_1_dmcub"); + if !dmcub_available { + warn!("redox-drm: DMCUB firmware not found in cache — display core may fail to initialize"); + } + + info!( + "redox-drm: AMD driver ready for {} with {} connector(s), {} firmware blob(s) loaded, IRQ mode {}", + info.location, + connectors.len(), + fw_count, + irq_mode + ); + + Ok(Self { + info, + mmio, + irq_handle: Mutex::new(irq_handle), + display, + gem: Mutex::new(GemManager::new()), + connectors: Mutex::new(connectors), + crtcs: Mutex::new(vec![Crtc::new(1)]), + encoders: Mutex::new(encoders), + gtt: Mutex::new(gtt), + ring: Mutex::new(ring), + vblank_count: AtomicU64::new(0), + hotplug_pending: AtomicBool::new(false), + firmware, + }) + } + + pub fn process_irq(&self) -> Result> { + let ih_status = self.read_mmio_reg(AMD_IH_STATUS); + let ih_cntl = self.read_mmio_reg(AMD_IH_CNTL); + let ih_rptr = self.read_mmio_reg(AMD_IH_RB_RPTR); + let ih_wptr = self.read_mmio_reg(AMD_IH_RB_WPTR); + let ring_pending = ih_rptr != ih_wptr; + + if ih_status & AMD_IH_STATUS_RING_OVERFLOW_MASK != 0 { + warn!( + "redox-drm: AMD IH overflow status={:#010x} cntl={:#010x}", + ih_status, ih_cntl + ); + } + + if let Some(connector_id) = self.detect_hotplug_interrupt() { + self.hotplug_pending.store(true, Ordering::SeqCst); + self.refresh_connectors()?; + self.hotplug_pending.store(false, Ordering::SeqCst); + self.acknowledge_ih(ih_wptr); + + debug!( + "redox-drm: hotplug interrupt on connector {} status={:#010x} cntl={:#010x} rptr={:#010x} wptr={:#010x}", + connector_id, ih_status, ih_cntl, ih_rptr, ih_wptr + ); + + return Ok(Some(DriverEvent::Hotplug { connector_id })); + } + + if ring_pending || (ih_status & AMD_IH_STATUS_INTERRUPT_PENDING_MASK != 0) { + if let Some(crtc_id) = self.detect_vblank_interrupt() { + let count = self.vblank_count.fetch_add(1, Ordering::SeqCst) + 1; + self.acknowledge_ih(ih_wptr); + + debug!( + "redox-drm: vblank interrupt on CRTC {} count={} status={:#010x} cntl={:#010x} rptr={:#010x} wptr={:#010x}", + crtc_id, count, ih_status, ih_cntl, ih_rptr, ih_wptr + ); + + return Ok(Some(DriverEvent::Vblank { crtc_id, count })); + } + } + + self.acknowledge_ih(ih_wptr); + Ok(None) + } + + fn read_mmio_reg(&self, register_index: usize) -> u32 { + self.mmio.read32(register_index.saturating_mul(4)) + } + + fn write_mmio_reg(&self, register_index: usize, value: u32) { + self.mmio.write32(register_index.saturating_mul(4), value); + } + + fn detect_vblank_interrupt(&self) -> Option { + let active_crtc_ids = self + .crtcs + .lock() + .map(|crtcs| { + crtcs + .iter() + .filter(|crtc| crtc.mode.is_some()) + .map(|crtc| crtc.id) + .collect::>() + }) + .unwrap_or_else(|_| vec![1]); + + for (index, register) in AMD_DCN_DISP_INTERRUPT_STATUS.iter().copied().enumerate() { + let status = self.read_mmio_reg(register); + if status & AMD_DISP_INTERRUPT_VBLANK_MASK == 0 { + continue; + } + + let crtc_id = index as u32 + 1; + if active_crtc_ids.is_empty() || active_crtc_ids.contains(&crtc_id) { + return Some(crtc_id); + } + } + + None + } + + fn detect_hotplug_interrupt(&self) -> Option { + for (index, register) in AMD_DCN_HPD_INT_STATUS.iter().copied().enumerate() { + let status = self.read_mmio_reg(register); + if status & (AMD_HPD_INT_STATUS_MASK | AMD_HPD_RX_INT_STATUS_MASK) != 0 { + self.acknowledge_hotplug(index, status); + return Some(index as u32 + 1); + } + } + + for (index, register) in AMD_DCN_DISP_INTERRUPT_STATUS.iter().copied().enumerate() { + let status = self.read_mmio_reg(register); + if status & AMD_DISP_INTERRUPT_HPD_MASK != 0 { + let hpd_status = self.read_mmio_reg(AMD_DCN_HPD_INT_STATUS[index]); + self.acknowledge_hotplug(index, hpd_status); + return Some(index as u32 + 1); + } + } + + None + } + + fn acknowledge_hotplug(&self, hpd_index: usize, hpd_status: u32) { + let control_register = AMD_DCN_HPD_CONTROL[hpd_index]; + let control = self.read_mmio_reg(control_register); + let ack = control + | if hpd_status & AMD_HPD_INT_STATUS_MASK != 0 { + AMD_HPD_INT_ACK_MASK + } else { + 0 + } + | if hpd_status & AMD_HPD_RX_INT_STATUS_MASK != 0 { + AMD_HPD_RX_INT_ACK_MASK + } else { + 0 + }; + self.write_mmio_reg(control_register, ack); + } + + fn acknowledge_ih(&self, ih_wptr: u32) { + self.write_mmio_reg(AMD_IH_RB_RPTR, ih_wptr); + + let ih_cntl = self.read_mmio_reg(AMD_IH_CNTL); + self.write_mmio_reg(AMD_IH_CNTL, ih_cntl); + + let ih_rb_cntl = self.read_mmio_reg(AMD_IH_RB_CNTL); + self.write_mmio_reg(AMD_IH_RB_CNTL, ih_rb_cntl); + } + + fn refresh_connectors(&self) -> Result<()> { + let (connectors, encoders) = detect_display_topology(&self.display)?; + + { + let mut connector_state = self + .connectors + .lock() + .map_err(|_| DriverError::Initialization("connector state poisoned".to_string()))?; + *connector_state = connectors; + } + + { + let mut encoder_state = self + .encoders + .lock() + .map_err(|_| DriverError::Initialization("encoder state poisoned".to_string()))?; + *encoder_state = encoders; + } + + Ok(()) + } + + fn ensure_gem_gpu_mapping(&self, fb_handle: GemHandle) -> Result { + { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?; + if let Some(addr) = gem.object(fb_handle)?.gpu_addr { + return Ok(addr); + } + } + + let (phys_addr, fb_size) = { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?; + let obj = gem.object(fb_handle)?; + (obj.phys_addr as u64, obj.size) + }; + + let gpu_addr = { + let mut gtt = self + .gtt + .lock() + .map_err(|_| DriverError::Initialization("GTT manager poisoned".to_string()))?; + let addr = gtt.alloc_gpu_range(fb_size)?; + if let Err(e) = gtt.map_range(addr, phys_addr, fb_size, 0) { + if gtt.unmap_range(addr, fb_size).is_ok() { + gtt.release_range(addr, fb_size); + } + return Err(e); + } + if let Err(e) = gtt.flush_tlb(&self.mmio) { + if gtt.unmap_range(addr, fb_size).is_ok() { + if gtt.flush_tlb(&self.mmio).is_ok() { + gtt.release_range(addr, fb_size); + } + } + return Err(e); + } + addr + }; + + if let Err(e) = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))? + .set_gpu_addr(fb_handle, gpu_addr) + { + let mut gtt = self + .gtt + .lock() + .map_err(|_| DriverError::Initialization("GTT manager poisoned".to_string()))?; + if gtt.flush_tlb(&self.mmio).is_ok() && gtt.unmap_range(gpu_addr, fb_size).is_ok() { + gtt.release_range(gpu_addr, fb_size); + } else { + let _ = gtt.unmap_range(gpu_addr, fb_size); + } + return Err(e); + } + + Ok(gpu_addr) + } +} + +impl GpuDriver for AmdDriver { + fn driver_name(&self) -> &str { + "amdgpu-redox" + } + + fn driver_desc(&self) -> &str { + "AMD GPU DRM/KMS backend for Redox" + } + + fn driver_date(&self) -> &str { + "2026-04-11" + } + + fn detect_connectors(&self) -> Vec { + match self.connectors.lock() { + Ok(connectors) => connectors + .iter() + .map(|connector| connector.info.clone()) + .collect(), + Err(poisoned) => { + warn!("redox-drm: connector state poisoned; using inner state"); + poisoned + .into_inner() + .iter() + .map(|connector| connector.info.clone()) + .collect() + } + } + } + + fn get_modes(&self, connector_id: u32) -> Vec { + self.detect_connectors() + .into_iter() + .find(|connector| connector.id == connector_id) + .map(|connector| connector.modes) + .unwrap_or_default() + } + + fn set_crtc( + &self, + crtc_id: u32, + fb_handle: u32, + connectors: &[u32], + mode: &ModeInfo, + ) -> Result<()> { + let fb_addr = self.ensure_gem_gpu_mapping(fb_handle)?; + + self.display + .set_crtc(crtc_id, fb_addr, mode.hdisplay as u32, mode.vdisplay as u32)?; + + let mut crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("CRTC state poisoned".to_string()))?; + let crtc = crtcs + .iter_mut() + .find(|candidate| candidate.id == crtc_id) + .ok_or_else(|| DriverError::NotFound(format!("unknown CRTC {crtc_id}")))?; + crtc.program(fb_handle, connectors, mode) + } + + fn page_flip(&self, crtc_id: u32, fb_handle: u32, _flags: u32) -> Result { + { + let crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("CRTC state poisoned".to_string()))?; + if !crtcs.iter().any(|crtc| crtc.id == crtc_id) { + return Err(DriverError::NotFound(format!("unknown CRTC {crtc_id}"))); + } + } + + let fb_addr = self.ensure_gem_gpu_mapping(fb_handle)?; + + self.display.flip_surface(crtc_id, fb_addr)?; + + let mut ring = self + .ring + .lock() + .map_err(|_| DriverError::Initialization("ring manager poisoned".to_string()))?; + ring.page_flip() + } + + fn get_vblank(&self, crtc_id: u32) -> Result { + let crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("CRTC state poisoned".to_string()))?; + if !crtcs.iter().any(|crtc| crtc.id == crtc_id) { + return Err(DriverError::NotFound(format!("unknown CRTC {crtc_id}"))); + } + + Ok(self.vblank_count.load(Ordering::SeqCst)) + } + + fn gem_create(&self, size: u64) -> Result { + let mut gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?; + gem.create(size) + } + + fn gem_close(&self, handle: GemHandle) -> Result<()> { + let gpu_info = { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?; + let obj = gem.object(handle)?; + (obj.gpu_addr, obj.size) + }; + + if let (Some(gpu_addr), fb_size) = gpu_info { + let mut gtt = self + .gtt + .lock() + .map_err(|_| DriverError::Initialization("GTT manager poisoned".to_string()))?; + gtt.flush_tlb(&self.mmio)?; + gtt.unmap_range(gpu_addr, fb_size)?; + gtt.release_range(gpu_addr, fb_size); + } + + self.gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))? + .close(handle) + } + + fn gem_mmap(&self, handle: GemHandle) -> Result { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?; + gem.mmap(handle) + } + + fn gem_size(&self, handle: GemHandle) -> Result { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?; + Ok(gem.object(handle)?.size) + } + + fn get_edid(&self, connector_id: u32) -> Vec { + match self.connectors.lock() { + Ok(connectors) => connectors + .iter() + .find(|connector| connector.info.id == connector_id) + .map(|connector| connector.edid.clone()) + .unwrap_or_default(), + Err(poisoned) => poisoned + .into_inner() + .iter() + .find(|connector| connector.info.id == connector_id) + .map(|connector| connector.edid.clone()) + .unwrap_or_default(), + } + } + + fn handle_irq(&self) -> Result> { + let irq_event = { + let mut irq_handle = self + .irq_handle + .lock() + .map_err(|_| DriverError::Initialization("AMD IRQ state poisoned".into()))?; + match irq_handle.as_mut() { + Some(handle) => handle.try_wait()?, + None => return Ok(None), + } + }; + + if !irq_event { + return Ok(None); + } + + let irq = self + .irq_handle + .lock() + .ok() + .and_then(|guard| guard.as_ref().map(|h| h.irq())); + + match self.process_irq()? { + Some(DriverEvent::Vblank { crtc_id, count }) => { + debug!( + "redox-drm: handled AMD vblank IRQ for {} CRTC {} count={} irq={:?}", + self.info.location, crtc_id, count, irq + ); + Ok(Some(DriverEvent::Vblank { crtc_id, count })) + } + Some(DriverEvent::Hotplug { connector_id }) => { + info!( + "redox-drm: handled AMD hotplug IRQ for {} connector {} irq={:?}", + self.info.location, connector_id, irq + ); + Ok(Some(DriverEvent::Hotplug { connector_id })) + } + None => { + debug!( + "redox-drm: handled AMD IRQ for {} with no decoded source irq={:?}", + self.info.location, irq + ); + Ok(None) + } + } + } +} + +fn detect_display_topology(display: &DisplayCore) -> Result<(Vec, Vec)> { + let detected = display.detect_connectors()?; + let mut connectors = Vec::new(); + let mut encoders = Vec::new(); + + for (idx, connector) in detected.into_iter().enumerate() { + let encoder_id = connector.encoder_id; + encoders.push(Encoder::new(encoder_id, 1)); + let edid = display.read_edid(idx as u32); + connectors.push(Connector { + info: connector, + edid: if edid.is_empty() { + synthetic_edid() + } else { + edid + }, + }); + } + + Ok((connectors, encoders)) +} + +fn find_memory_bar0(info: &PciDeviceInfo) -> Result { + info.find_memory_bar(0) + .copied() + .ok_or_else(|| DriverError::Pci(format!("device {} has no MMIO BAR0", info.location))) +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/amd/ring.rs b/local/recipes/gpu/redox-drm/source/src/drivers/amd/ring.rs new file mode 100644 index 0000000000..5239cd0c62 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/amd/ring.rs @@ -0,0 +1,404 @@ +use core::sync::atomic::{fence, AtomicPtr, AtomicUsize, Ordering}; + +use log::{info, warn}; +use redox_driver_sys::dma::DmaBuffer; +use redox_driver_sys::memory::MmioRegion; + +use crate::driver::{DriverError, Result}; + +const RING_BUFFER_BYTES: usize = 4096; +const RING_BUFFER_DWORDS: usize = RING_BUFFER_BYTES / 4; +const RING_ALIGNMENT_BYTES: usize = 4096; +const FENCE_BUFFER_BYTES: usize = 16; +const WPTR_STRIDE_DWORDS: usize = 1; + +const SDMA_OP_NOP: u32 = 0; +const SDMA_OP_FENCE: u32 = 5; +const SDMA_OP_TRAP: u32 = 6; + +const SDMA0_GFX_RB_CNTL: usize = 0x0080 * 4; +const SDMA0_GFX_RB_BASE: usize = 0x0081 * 4; +const SDMA0_GFX_RB_BASE_HI: usize = 0x0082 * 4; +const SDMA0_GFX_RB_RPTR: usize = 0x0083 * 4; +const SDMA0_GFX_RB_RPTR_HI: usize = 0x0084 * 4; +const SDMA0_GFX_RB_WPTR: usize = 0x0085 * 4; +const SDMA0_GFX_RB_WPTR_HI: usize = 0x0086 * 4; +const SDMA0_GFX_RB_WPTR_POLL_CNTL: usize = 0x0087 * 4; +const SDMA0_GFX_RB_RPTR_ADDR_HI: usize = 0x0088 * 4; +const SDMA0_GFX_RB_RPTR_ADDR_LO: usize = 0x0089 * 4; +const SDMA0_GFX_IB_CNTL: usize = 0x008a * 4; +const SDMA0_GFX_RB_WPTR_POLL_ADDR_HI: usize = 0x00b2 * 4; +const SDMA0_GFX_RB_WPTR_POLL_ADDR_LO: usize = 0x00b3 * 4; +const SDMA0_GFX_MINOR_PTR_UPDATE: usize = 0x00b5 * 4; + +const SDMA_RB_CNTL_RB_ENABLE: u32 = 1 << 0; +const SDMA_RB_CNTL_RB_SIZE_SHIFT: u32 = 1; +const SDMA_RB_CNTL_RB_SIZE_MASK: u32 = 0x1f << SDMA_RB_CNTL_RB_SIZE_SHIFT; +const SDMA_RB_CNTL_RPTR_WRITEBACK_ENABLE: u32 = 1 << 12; +const SDMA_IB_CNTL_IB_ENABLE: u32 = 1 << 0; + +const FENCE_OFFSET_BYTES: usize = 0; +const WPTR_POLL_OFFSET_BYTES: usize = 8; + +static MMIO_BASE: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); +static MMIO_SIZE: AtomicUsize = AtomicUsize::new(0); + +#[derive(Clone, Copy, Debug)] +struct MmioBinding { + base: usize, + size: usize, +} + +// Safety: MmioBinding holds raw address integers, not pointers. +// It is safe to send between threads because register access is volatile. +unsafe impl Send for MmioBinding {} +unsafe impl Sync for MmioBinding {} + +impl MmioBinding { + fn try_load() -> Option { + let base = MMIO_BASE.load(Ordering::Acquire); + let size = MMIO_SIZE.load(Ordering::Acquire); + if base.is_null() { + return None; + } + Some(Self { + base: base as usize, + size, + }) + } + + fn read32(&self, offset: usize) -> Result { + if offset.checked_add(4).is_none_or(|end| end > self.size) { + return Err(DriverError::Mmio(format!( + "AMD ring MMIO read out of bounds: offset={offset:#x} size={:#x}", + self.size + ))); + } + + let ptr = (self.base + offset) as *const u32; + Ok(unsafe { core::ptr::read_volatile(ptr) }) + } + + fn write32(&self, offset: usize, value: u32) -> Result<()> { + if offset.checked_add(4).is_none_or(|end| end > self.size) { + return Err(DriverError::Mmio(format!( + "AMD ring MMIO write out of bounds: offset={offset:#x} size={:#x}", + self.size + ))); + } + + let ptr = (self.base + offset) as *mut u32; + unsafe { core::ptr::write_volatile(ptr, value) }; + Ok(()) + } +} + +#[derive(Default)] +pub struct RingManager { + initialized: bool, + ring_buffer: Option, + fence_buffer: Option, + mmio: Option, + ring_size_dwords: u32, + read_ptr: u64, + write_ptr: u64, + next_seqno: u64, + last_signaled_seqno: u64, +} + +impl RingManager { + pub fn new() -> Self { + Self { + initialized: false, + ring_buffer: None, + fence_buffer: None, + mmio: None, + ring_size_dwords: RING_BUFFER_DWORDS as u32, + read_ptr: 0, + write_ptr: 0, + next_seqno: 1, + last_signaled_seqno: 0, + } + } + + pub fn initialize(&mut self) -> Result<()> { + let mut ring_buffer = DmaBuffer::allocate(RING_BUFFER_BYTES, RING_ALIGNMENT_BYTES) + .map_err(|e| DriverError::Buffer(format!("ring buffer allocation failed: {e}")))?; + let mut fence_buffer = + DmaBuffer::allocate(FENCE_BUFFER_BYTES, core::mem::align_of::()) + .map_err(|e| DriverError::Buffer(format!("fence buffer allocation failed: {e}")))?; + + Self::zero_dma(&mut ring_buffer); + Self::zero_dma(&mut fence_buffer); + + self.mmio = MmioBinding::try_load(); + self.program_ring(&ring_buffer, &fence_buffer)?; + + self.ring_buffer = Some(ring_buffer); + self.fence_buffer = Some(fence_buffer); + self.read_ptr = 0; + self.write_ptr = 0; + self.next_seqno = 1; + self.last_signaled_seqno = 0; + self.initialized = true; + + info!( + "redox-drm: AMD ring manager initialized with {} DW ring buffer{}", + self.ring_size_dwords, + if self.mmio.is_some() { + " and SDMA MMIO programming" + } else { + " (MMIO binding unavailable; submissions stay software-tracked)" + } + ); + + Ok(()) + } + + pub fn page_flip(&mut self) -> Result { + self.ensure_initialized()?; + + let seqno = self.next_seqno; + self.next_seqno = self.next_seqno.saturating_add(1); + + let mut packet = Vec::with_capacity(16); + self.emit_flip(&mut packet, seqno); + self.emit_fence(&mut packet, seqno)?; + + self.submit(&packet, seqno) + } + + pub(crate) fn bind_mmio(mmio: &MmioRegion) { + MMIO_BASE.store(mmio.as_ptr() as *mut u8, Ordering::Release); + MMIO_SIZE.store(mmio.size(), Ordering::Release); + } + + fn ensure_initialized(&self) -> Result<()> { + if self.initialized { + Ok(()) + } else { + Err(DriverError::Initialization( + "ring manager must be initialized before page flips".to_string(), + )) + } + } + + fn program_ring(&self, ring_buffer: &DmaBuffer, fence_buffer: &DmaBuffer) -> Result<()> { + let Some(mmio) = self.mmio else { + warn!( + "redox-drm: AMD ring manager has no MMIO binding; skipping SDMA register programming" + ); + return Ok(()); + }; + + let ring_addr = ring_buffer.physical_address() as u64; + let fence_addr = fence_buffer.physical_address() as u64 + FENCE_OFFSET_BYTES as u64; + let wptr_poll_addr = fence_buffer.physical_address() as u64 + WPTR_POLL_OFFSET_BYTES as u64; + + let mut rb_cntl = mmio.read32(SDMA0_GFX_RB_CNTL)?; + rb_cntl &= !(SDMA_RB_CNTL_RB_ENABLE | SDMA_RB_CNTL_RB_SIZE_MASK); + rb_cntl |= + (self.ring_size_order() << SDMA_RB_CNTL_RB_SIZE_SHIFT) & SDMA_RB_CNTL_RB_SIZE_MASK; + mmio.write32(SDMA0_GFX_RB_CNTL, rb_cntl)?; + + mmio.write32(SDMA0_GFX_RB_RPTR, 0)?; + mmio.write32(SDMA0_GFX_RB_RPTR_HI, 0)?; + mmio.write32(SDMA0_GFX_RB_WPTR, 0)?; + mmio.write32(SDMA0_GFX_RB_WPTR_HI, 0)?; + + mmio.write32(SDMA0_GFX_RB_RPTR_ADDR_HI, upper_32(fence_addr))?; + mmio.write32(SDMA0_GFX_RB_RPTR_ADDR_LO, lower_32(fence_addr) & !0x3)?; + + rb_cntl |= SDMA_RB_CNTL_RPTR_WRITEBACK_ENABLE; + mmio.write32(SDMA0_GFX_RB_CNTL, rb_cntl)?; + + mmio.write32(SDMA0_GFX_RB_BASE, lower_32(ring_addr >> 8))?; + mmio.write32(SDMA0_GFX_RB_BASE_HI, lower_32(ring_addr >> 40))?; + + mmio.write32(SDMA0_GFX_MINOR_PTR_UPDATE, 1)?; + mmio.write32(SDMA0_GFX_RB_WPTR, 0)?; + mmio.write32(SDMA0_GFX_RB_WPTR_HI, 0)?; + mmio.write32(SDMA0_GFX_MINOR_PTR_UPDATE, 0)?; + + mmio.write32(SDMA0_GFX_RB_WPTR_POLL_ADDR_LO, lower_32(wptr_poll_addr))?; + mmio.write32(SDMA0_GFX_RB_WPTR_POLL_ADDR_HI, upper_32(wptr_poll_addr))?; + mmio.write32(SDMA0_GFX_RB_WPTR_POLL_CNTL, 0)?; + + rb_cntl |= SDMA_RB_CNTL_RB_ENABLE; + mmio.write32(SDMA0_GFX_RB_CNTL, rb_cntl)?; + + let mut ib_cntl = mmio.read32(SDMA0_GFX_IB_CNTL)?; + ib_cntl |= SDMA_IB_CNTL_IB_ENABLE; + mmio.write32(SDMA0_GFX_IB_CNTL, ib_cntl)?; + + Ok(()) + } + + fn submit(&mut self, commands: &[u32], seqno: u64) -> Result { + self.refresh_read_ptr(); + self.ensure_space(commands.len())?; + + for &command in commands { + self.write_ring_dword(command)?; + } + + fence(Ordering::Release); + self.publish_wptr()?; + + if self.mmio.is_none() { + self.write_completed_seqno(seqno)?; + } + + Ok(seqno) + } + + fn refresh_read_ptr(&mut self) { + if let Some(mmio) = self.mmio { + let low = mmio.read32(SDMA0_GFX_RB_RPTR).unwrap_or(0) as u64; + let high = mmio.read32(SDMA0_GFX_RB_RPTR_HI).unwrap_or(0) as u64; + self.read_ptr = ((high << 32) | low) >> 2; + } else { + self.read_ptr = self.write_ptr; + } + } + + fn ensure_space(&self, required_dwords: usize) -> Result<()> { + if required_dwords >= self.ring_capacity() { + return Err(DriverError::Buffer(format!( + "ring submission too large: {} DW exceeds capacity {} DW", + required_dwords, + self.ring_capacity() - 1 + ))); + } + + let used = self.used_dwords(); + let free = self.ring_capacity().saturating_sub(used).saturating_sub(1); + if required_dwords <= free { + Ok(()) + } else { + Err(DriverError::Buffer(format!( + "ring buffer full: required {} DW, free {} DW", + required_dwords, free + ))) + } + } + + fn used_dwords(&self) -> usize { + let size = self.ring_capacity() as u64; + ((self.write_ptr + size).wrapping_sub(self.read_ptr) % size) as usize + } + + fn write_ring_dword(&mut self, value: u32) -> Result<()> { + let capacity = self.ring_capacity(); + let ring_buffer = self + .ring_buffer + .as_mut() + .ok_or_else(|| DriverError::Initialization("ring buffer missing".to_string()))?; + + let index = (self.write_ptr as usize) % capacity; + let ptr = unsafe { + ring_buffer + .as_mut_ptr() + .add(index * core::mem::size_of::()) as *mut u32 + }; + unsafe { core::ptr::write_volatile(ptr, value) }; + + self.write_ptr = (self.write_ptr + WPTR_STRIDE_DWORDS as u64) % capacity as u64; + Ok(()) + } + + fn publish_wptr(&mut self) -> Result<()> { + self.write_wptr_shadow(self.write_ptr)?; + + let Some(mmio) = self.mmio else { + return Ok(()); + }; + + mmio.write32(SDMA0_GFX_MINOR_PTR_UPDATE, 1)?; + mmio.write32(SDMA0_GFX_RB_WPTR, lower_32(self.write_ptr << 2))?; + mmio.write32(SDMA0_GFX_RB_WPTR_HI, upper_32(self.write_ptr << 2))?; + mmio.write32(SDMA0_GFX_MINOR_PTR_UPDATE, 0)?; + Ok(()) + } + + fn emit_nop(&self, packet: &mut Vec, count: u32) { + for _ in 0..count { + packet.push(SDMA_OP_NOP); + } + } + + fn emit_flip(&self, packet: &mut Vec, seqno: u64) { + self.emit_nop(packet, 2); + packet.push(0x5049_4c46); + packet.push(lower_32(seqno)); + packet.push(upper_32(seqno)); + } + + fn emit_fence(&self, packet: &mut Vec, seqno: u64) -> Result<()> { + let fence_addr = self.fence_address()?; + + packet.push(SDMA_OP_FENCE); + packet.push(lower_32(fence_addr)); + packet.push(upper_32(fence_addr)); + packet.push(lower_32(seqno)); + + packet.push(SDMA_OP_FENCE); + packet.push(lower_32(fence_addr + 4)); + packet.push(upper_32(fence_addr + 4)); + packet.push(upper_32(seqno)); + + packet.push(SDMA_OP_TRAP); + packet.push(0); + + Ok(()) + } + + fn fence_address(&self) -> Result { + let fence_buffer = self + .fence_buffer + .as_ref() + .ok_or_else(|| DriverError::Initialization("fence buffer missing".to_string()))?; + Ok(fence_buffer.physical_address() as u64 + FENCE_OFFSET_BYTES as u64) + } + + fn write_completed_seqno(&mut self, seqno: u64) -> Result<()> { + let fence_buffer = self + .fence_buffer + .as_mut() + .ok_or_else(|| DriverError::Initialization("fence buffer missing".to_string()))?; + let ptr = unsafe { fence_buffer.as_mut_ptr().add(FENCE_OFFSET_BYTES) as *mut u64 }; + unsafe { core::ptr::write_volatile(ptr, seqno) }; + self.last_signaled_seqno = seqno; + Ok(()) + } + + fn write_wptr_shadow(&mut self, wptr_dwords: u64) -> Result<()> { + let fence_buffer = self + .fence_buffer + .as_mut() + .ok_or_else(|| DriverError::Initialization("fence buffer missing".to_string()))?; + let ptr = unsafe { fence_buffer.as_mut_ptr().add(WPTR_POLL_OFFSET_BYTES) as *mut u64 }; + unsafe { core::ptr::write_volatile(ptr, wptr_dwords << 2) }; + Ok(()) + } + + fn ring_size_order(&self) -> u32 { + self.ring_size_dwords.ilog2() + } + + fn ring_capacity(&self) -> usize { + self.ring_size_dwords as usize + } + + fn zero_dma(buffer: &mut DmaBuffer) { + unsafe { core::ptr::write_bytes(buffer.as_mut_ptr(), 0, buffer.len()) }; + } +} + +fn lower_32(value: u64) -> u32 { + value as u32 +} + +fn upper_32(value: u64) -> u32 { + (value >> 32) as u32 +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/intel/display.rs b/local/recipes/gpu/redox-drm/source/src/drivers/intel/display.rs new file mode 100644 index 0000000000..aaed19547a --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/intel/display.rs @@ -0,0 +1,404 @@ +use std::sync::Mutex; + +use log::{debug, info}; +use redox_driver_sys::memory::MmioRegion; + +use crate::driver::{DriverError, Result}; +use crate::kms::connector::synthetic_edid; +use crate::kms::{ConnectorInfo, ConnectorStatus, ConnectorType, ModeInfo}; + +const PIPE_COUNT: usize = 4; +const PORT_COUNT: usize = 6; + +const PP_STATUS: usize = 0xC7200; +const PIPECONF_BASE: usize = 0x70008; +const DSPCNTR_BASE: usize = 0x70180; +const DSPSURF_BASE: usize = 0x7019C; +const DDI_BUF_CTL_BASE: usize = 0x64000; + +const HTOTAL_BASE: usize = 0x60000; +const HBLANK_BASE: usize = 0x60004; +const HSYNC_BASE: usize = 0x60008; +const VTOTAL_BASE: usize = 0x6000C; +const VBLANK_BASE: usize = 0x60010; +const VSYNC_BASE: usize = 0x60014; +const PIPE_SRC_BASE: usize = 0x6001C; +const PLANE_SIZE_BASE: usize = 0x70190; + +const PIPE_STRIDE: usize = 0x1000; +const PORT_STRIDE: usize = 0x100; + +const PIPECONF_ENABLE: u32 = 1 << 31; +const DSPCNTR_ENABLE: u32 = 1 << 31; +const DDI_BUF_CTL_ENABLE: u32 = 1 << 31; + +#[derive(Clone, Copy, Debug)] +pub struct DisplayPipe { + pub index: u8, + pub enabled: bool, + pub port: Option, +} + +pub struct IntelDisplay { + mmio: MmioRegion, + pipes: Mutex>, +} + +impl IntelDisplay { + pub fn new(mmio: MmioRegion) -> Result { + let pipes = Self::detect_pipes(&mmio)?; + info!( + "redox-drm: Intel display initialized with {} pipe(s)", + pipes.len() + ); + Ok(Self { + mmio, + pipes: Mutex::new(pipes), + }) + } + + pub fn pipes(&self) -> Result> { + self.refresh_pipes() + } + + pub fn pipe_for_crtc(&self, crtc_id: u32) -> Result { + let index = crtc_id + .checked_sub(1) + .ok_or(DriverError::InvalidArgument("invalid Intel CRTC id"))? + as usize; + self.refresh_pipes()? + .get(index) + .copied() + .ok_or_else(|| DriverError::NotFound(format!("unknown Intel pipe for CRTC {crtc_id}"))) + } + + pub fn detect_pipes(mmio: &MmioRegion) -> Result> { + let mut pipes = Vec::with_capacity(PIPE_COUNT); + let pp_status = read32(mmio, PP_STATUS).unwrap_or(0); + let connected_ports = connected_ports(mmio); + + for index in 0..PIPE_COUNT { + let conf = read32(mmio, pipe_offset(PIPECONF_BASE, index))?; + let enabled = conf & PIPECONF_ENABLE != 0; + let mut port = connected_ports.get(index).copied(); + + if port.is_none() && index == 0 && pp_status != 0 { + port = Some(0); + } + if port.is_none() && enabled { + port = Some(index as u8); + } + + pipes.push(DisplayPipe { + index: index as u8, + enabled, + port, + }); + } + + if pipes.iter().all(|pipe| pipe.port.is_none()) { + if let Some(pipe) = pipes.first_mut() { + pipe.port = Some(0); + } + } + + Ok(pipes) + } + + pub fn detect_connectors(&self) -> Result> { + let pp_status = self.read32(PP_STATUS).unwrap_or(0); + let pipes = self.refresh_pipes()?; + let mut connectors = Vec::with_capacity(PORT_COUNT); + + for port in 0..PORT_COUNT as u8 { + let status = self.read32(ddi_offset(port)).unwrap_or(0); + let connected = status & DDI_BUF_CTL_ENABLE != 0 + || pipes + .iter() + .any(|pipe| pipe.port == Some(port) && pipe.enabled) + || (port == 0 && pp_status != 0); + let connector_type = connector_type_for_port(port, pp_status); + let modes = self.modes_for_port(port, connector_type); + + connectors.push(ConnectorInfo { + id: port as u32 + 1, + connector_type, + connector_type_id: port as u32 + 1, + connection: if connected { + ConnectorStatus::Connected + } else { + ConnectorStatus::Disconnected + }, + mm_width: 600, + mm_height: 340, + encoder_id: port as u32 + 1, + modes, + }); + } + + Ok(connectors) + } + + pub fn modes_for_connector(&self, connector: &ConnectorInfo) -> Vec { + let port = connector + .connector_type_id + .saturating_sub(1) + .min((PORT_COUNT - 1) as u32) as u8; + self.modes_for_port(port, connector.connector_type) + } + + pub fn read_edid(&self, port: u8) -> Vec { + debug!("redox-drm: Intel EDID probe on port {}", port); + let mut edid = vec![0u8; 128]; + if self.read_edid_block(port, 0, &mut edid).is_ok() && edid[0] == 0x00 && edid[1] == 0xFF { + return edid; + } + debug!( + "redox-drm: Intel EDID probe failed on port {}, using synthetic fallback", + port + ); + synthetic_edid() + } + + fn read_edid_block(&self, _port: u8, _block: u8, _buf: &mut [u8]) -> Result<()> { + Err(DriverError::Initialization( + "EDID I2C/DDC not yet implemented".into(), + )) + } + + pub fn read_dpcd(&self, port: u8) -> Vec { + let status = self.read32(ddi_offset(port)).unwrap_or(0); + if status & DDI_BUF_CTL_ENABLE == 0 { + return Vec::new(); + } + + debug!("redox-drm: Intel AUX/DPCD skeleton read on port {}", port); + vec![0x12, 0x0A, 0x84, 0x01] + } + + pub fn set_mode(&self, pipe: &DisplayPipe, mode: &ModeInfo) -> Result<()> { + let index = usize::from(pipe.index); + self.write32( + pipe_offset(HTOTAL_BASE, index), + pack_pair(mode.htotal, mode.hdisplay), + )?; + self.write32( + pipe_offset(HBLANK_BASE, index), + pack_pair(mode.htotal, mode.hdisplay), + )?; + self.write32( + pipe_offset(HSYNC_BASE, index), + pack_pair(mode.hsync_end, mode.hsync_start), + )?; + self.write32( + pipe_offset(VTOTAL_BASE, index), + pack_pair(mode.vtotal, mode.vdisplay), + )?; + self.write32( + pipe_offset(VBLANK_BASE, index), + pack_pair(mode.vtotal, mode.vdisplay), + )?; + self.write32( + pipe_offset(VSYNC_BASE, index), + pack_pair(mode.vsync_end, mode.vsync_start), + )?; + self.write32( + pipe_offset(PIPE_SRC_BASE, index), + pack_pair(mode.vdisplay, mode.hdisplay), + )?; + self.write32( + pipe_offset(PLANE_SIZE_BASE, index), + pack_pair(mode.vdisplay, mode.hdisplay), + )?; + + let mut dspcntr = self.read32(pipe_offset(DSPCNTR_BASE, index))?; + dspcntr |= DSPCNTR_ENABLE; + self.write32(pipe_offset(DSPCNTR_BASE, index), dspcntr)?; + + let mut pipeconf = self.read32(pipe_offset(PIPECONF_BASE, index))?; + pipeconf |= PIPECONF_ENABLE; + self.write32(pipe_offset(PIPECONF_BASE, index), pipeconf)?; + + if let Some(port) = pipe.port { + let mut ddi = self.read32(ddi_offset(port))?; + ddi |= DDI_BUF_CTL_ENABLE; + self.write32(ddi_offset(port), ddi)?; + } + + self.update_pipe(pipe.index, true, pipe.port)?; + + Ok(()) + } + + pub fn page_flip(&self, pipe: &DisplayPipe, fb_addr: u64) -> Result<()> { + if fb_addr > u64::from(u32::MAX) { + self.write32( + pipe_offset(DSPSURF_BASE, usize::from(pipe.index)), + (fb_addr >> 32) as u32, + )?; + } + let index = usize::from(pipe.index); + self.write32(pipe_offset(DSPSURF_BASE, index), fb_addr as u32) + } + + fn refresh_pipes(&self) -> Result> { + let mut detected = Self::detect_pipes(&self.mmio)?; + let mut cached = self + .pipes + .lock() + .map_err(|_| DriverError::Initialization("Intel display pipe state poisoned".into()))?; + + let previous = cached.clone(); + + for pipe in detected.iter_mut() { + if let Some(existing) = previous + .iter() + .find(|existing| existing.index == pipe.index) + { + if pipe.port.is_none() { + pipe.port = existing.port; + } + } + } + + *cached = detected.clone(); + Ok(detected) + } + + fn update_pipe(&self, index: u8, enabled: bool, port: Option) -> Result<()> { + let mut cached = self + .pipes + .lock() + .map_err(|_| DriverError::Initialization("Intel display pipe state poisoned".into()))?; + + if let Some(pipe) = cached.iter_mut().find(|pipe| pipe.index == index) { + pipe.enabled = enabled; + pipe.port = port.or(pipe.port); + return Ok(()); + } + + cached.push(DisplayPipe { + index, + enabled, + port, + }); + Ok(()) + } + + fn modes_for_port(&self, port: u8, connector_type: ConnectorType) -> Vec { + let mut modes = match connector_type { + ConnectorType::DisplayPort | ConnectorType::EDP => { + modes_from_dpcd(&self.read_dpcd(port)) + } + _ => ModeInfo::from_edid(&self.read_edid(port)), + }; + + if modes.is_empty() { + modes = ModeInfo::from_edid(&synthetic_edid()); + } + if modes.is_empty() { + modes.push(ModeInfo::default_1080p()); + } + modes + } + + fn read32(&self, offset: usize) -> Result { + read32(&self.mmio, offset) + } + + fn write32(&self, offset: usize, value: u32) -> Result<()> { + write32(&self.mmio, offset, value) + } +} + +fn connected_ports(mmio: &MmioRegion) -> Vec { + let mut ports = Vec::new(); + for port in 0..PORT_COUNT as u8 { + if read32(mmio, ddi_offset(port)).unwrap_or(0) & DDI_BUF_CTL_ENABLE != 0 { + ports.push(port); + } + } + ports +} + +fn read32(mmio: &MmioRegion, offset: usize) -> Result { + ensure_access( + mmio.size(), + offset, + core::mem::size_of::(), + "Intel display read", + )?; + Ok(mmio.read32(offset)) +} + +fn write32(mmio: &MmioRegion, offset: usize, value: u32) -> Result<()> { + ensure_access( + mmio.size(), + offset, + core::mem::size_of::(), + "Intel display write", + )?; + mmio.write32(offset, value); + Ok(()) +} + +fn ensure_access(mmio_size: usize, offset: usize, width: usize, op: &str) -> Result<()> { + let end = offset + .checked_add(width) + .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 pipe_offset(base: usize, index: usize) -> usize { + base + index * PIPE_STRIDE +} + +fn ddi_offset(port: u8) -> usize { + DDI_BUF_CTL_BASE + usize::from(port) * PORT_STRIDE +} + +fn pack_pair(upper: u16, lower: u16) -> u32 { + ((u32::from(upper).saturating_sub(1)) << 16) | u32::from(lower).saturating_sub(1) +} + +fn connector_type_for_port(port: u8, pp_status: u32) -> ConnectorType { + match port { + 0 if pp_status != 0 => ConnectorType::EDP, + 0 | 1 => ConnectorType::HDMIA, + 2 | 3 => ConnectorType::DisplayPort, + _ => ConnectorType::VGA, + } +} + +fn modes_from_dpcd(dpcd: &[u8]) -> Vec { + if dpcd.is_empty() { + return Vec::new(); + } + + vec![ModeInfo::default_1080p(), mode_1440p()] +} + +fn mode_1440p() -> ModeInfo { + ModeInfo { + clock: 241_500, + hdisplay: 2560, + hsync_start: 2608, + hsync_end: 2640, + htotal: 2720, + hskew: 0, + vdisplay: 1440, + vsync_start: 1443, + vsync_end: 1448, + vtotal: 1481, + vscan: 0, + vrefresh: 60, + flags: 0, + type_: 0, + name: "2560x1440@60".to_string(), + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/intel/gtt.rs b/local/recipes/gpu/redox-drm/source/src/drivers/intel/gtt.rs new file mode 100644 index 0000000000..40d2f99806 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/intel/gtt.rs @@ -0,0 +1,226 @@ +use std::collections::BTreeMap; + +use log::{debug, info}; +use redox_driver_sys::memory::MmioRegion; + +use crate::driver::{DriverError, Result}; + +const GTT_BASE: usize = 0x0000; +const GFX_FLSH_CNTL_REG: usize = 0x101008; +const GFX_FLSH_CNTL_EN: u32 = 1 << 0; + +const GTT_PAGE_SIZE: u64 = 4096; +const GTT_PAGE_MASK: u64 = GTT_PAGE_SIZE - 1; +const GTT_PTE_PRESENT: u64 = 1 << 0; +const GTT_PTE_WRITE: u64 = 1 << 1; +const GTT_PTE_ADDR_MASK: u64 = 0xFFFF_FFFF_FFFF_F000; + +pub struct IntelGtt { + gtt_mmio: MmioRegion, + control_mmio: MmioRegion, + page_count: usize, + aperture_size: u64, + next_allocation: u64, + free_list: Vec<(u64, u64)>, + mappings: BTreeMap, +} + +impl IntelGtt { + pub fn init(gtt_mmio: MmioRegion, control_mmio: MmioRegion) -> Result { + let page_count = gtt_mmio.size() / core::mem::size_of::(); + if page_count == 0 { + return Err(DriverError::Initialization( + "Intel GGTT BAR exposes no page table entries".to_string(), + )); + } + + let aperture_size = (page_count as u64) + .checked_mul(GTT_PAGE_SIZE) + .ok_or_else(|| DriverError::Initialization("Intel GGTT aperture overflow".into()))?; + + let gtt = Self { + gtt_mmio, + control_mmio, + page_count, + aperture_size, + next_allocation: 0, + free_list: Vec::new(), + mappings: BTreeMap::new(), + }; + + gtt.flush()?; + info!( + "redox-drm: Intel GGTT initialized with {} entries ({:#x} aperture)", + page_count, aperture_size + ); + Ok(gtt) + } + + pub fn alloc_range(&mut self, size: u64) -> Result { + let aligned_size = align_up(size, GTT_PAGE_SIZE)?; + + if let Some(index) = self + .free_list + .iter() + .position(|&(_, free_size)| free_size >= aligned_size) + { + let (start, free_size) = self.free_list.remove(index); + let remainder = free_size.saturating_sub(aligned_size); + if remainder != 0 { + self.free_list.push((start + aligned_size, remainder)); + } + return Ok(start); + } + + let start = self.next_allocation; + let end = start + .checked_add(aligned_size) + .ok_or_else(|| DriverError::Buffer("Intel GGTT allocation overflow".into()))?; + if end > self.aperture_size { + return Err(DriverError::Buffer(format!( + "Intel GGTT aperture exhausted: need {:#x} bytes, remaining {:#x}", + aligned_size, + self.aperture_size.saturating_sub(start) + ))); + } + + self.next_allocation = end; + Ok(start) + } + + pub fn release_range(&mut self, gpu_addr: u64, size: u64) -> Result<()> { + let aligned_size = align_up(size, GTT_PAGE_SIZE)?; + self.free_list.push((gpu_addr, aligned_size)); + Ok(()) + } + + pub fn map_range( + &mut self, + gpu_addr: u64, + phys_addr: u64, + size: u64, + flags: u64, + ) -> Result<()> { + let aligned_size = align_up(size, GTT_PAGE_SIZE)?; + let page_count = (aligned_size / GTT_PAGE_SIZE) as usize; + + for page in 0..page_count { + let page_offset = (page as u64) * GTT_PAGE_SIZE; + self.insert_page(gpu_addr + page_offset, phys_addr + page_offset, flags)?; + } + + self.mappings.insert(gpu_addr, aligned_size); + self.flush() + } + + pub fn unmap_range(&mut self, gpu_addr: u64, size: u64) -> Result<()> { + let aligned_size = align_up(size, GTT_PAGE_SIZE)?; + let page_count = (aligned_size / GTT_PAGE_SIZE) as usize; + + for page in 0..page_count { + let page_offset = (page as u64) * GTT_PAGE_SIZE; + self.remove_page(gpu_addr + page_offset)?; + } + + self.mappings.remove(&gpu_addr); + self.flush() + } + + pub fn insert_page(&self, virtual_addr: u64, physical_addr: u64, flags: u64) -> Result<()> { + ensure_page_alignment(virtual_addr, "virtual_addr")?; + ensure_page_alignment(physical_addr, "physical_addr")?; + + let entry_index = self.entry_index(virtual_addr)?; + let entry_offset = gtt_entry_offset(entry_index)?; + self.ensure_gtt_access(entry_offset, core::mem::size_of::(), "GGTT PTE write")?; + + let pte = encode_pte(physical_addr, flags); + self.gtt_mmio.write64(entry_offset, pte); + debug!( + "redox-drm: Intel GGTT map va={:#x} -> pa={:#x} flags={:#x}", + virtual_addr, physical_addr, flags + ); + Ok(()) + } + + pub fn remove_page(&self, virtual_addr: u64) -> Result<()> { + ensure_page_alignment(virtual_addr, "virtual_addr")?; + + let entry_index = self.entry_index(virtual_addr)?; + let entry_offset = gtt_entry_offset(entry_index)?; + self.ensure_gtt_access(entry_offset, core::mem::size_of::(), "GGTT PTE clear")?; + + self.gtt_mmio.write64(entry_offset, 0); + debug!("redox-drm: Intel GGTT unmap va={:#x}", virtual_addr); + Ok(()) + } + + pub fn flush(&self) -> Result<()> { + self.ensure_control_access(GFX_FLSH_CNTL_REG, core::mem::size_of::(), "GGTT flush")?; + self.control_mmio + .write32(GFX_FLSH_CNTL_REG, GFX_FLSH_CNTL_EN); + let _ = self.control_mmio.read32(GFX_FLSH_CNTL_REG); + Ok(()) + } + + fn entry_index(&self, virtual_addr: u64) -> Result { + let entry_index = (virtual_addr / GTT_PAGE_SIZE) as usize; + if entry_index >= self.page_count { + return Err(DriverError::Buffer(format!( + "Intel GGTT entry {entry_index} outside aperture of {} entries", + self.page_count + ))); + } + Ok(entry_index) + } + + fn ensure_gtt_access(&self, offset: usize, width: usize, op: &str) -> Result<()> { + ensure_mmio_access(self.gtt_mmio.size(), offset, width, op) + } + + fn ensure_control_access(&self, offset: usize, width: usize, op: &str) -> Result<()> { + ensure_mmio_access(self.control_mmio.size(), offset, width, op) + } +} + +fn align_up(value: u64, alignment: u64) -> Result { + value + .checked_add(alignment - 1) + .map(|v| v & !(alignment - 1)) + .ok_or_else(|| DriverError::Buffer("Intel GGTT size alignment overflow".into())) +} + +fn ensure_page_alignment(value: u64, name: &'static str) -> Result<()> { + if value & GTT_PAGE_MASK != 0 { + return Err(DriverError::InvalidArgument(name)); + } + Ok(()) +} + +fn gtt_entry_offset(entry_index: usize) -> Result { + GTT_BASE + .checked_add( + entry_index + .checked_mul(core::mem::size_of::()) + .ok_or_else(|| DriverError::Mmio("Intel GGTT entry offset overflow".into()))?, + ) + .ok_or_else(|| DriverError::Mmio("Intel GGTT base offset overflow".into())) +} + +fn ensure_mmio_access(mmio_size: usize, offset: usize, width: usize, op: &str) -> Result<()> { + let end = offset + .checked_add(width) + .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 encode_pte(physical_addr: u64, flags: u64) -> u64 { + (physical_addr & GTT_PTE_ADDR_MASK) + | (flags & (GTT_PTE_PRESENT | GTT_PTE_WRITE)) + | GTT_PTE_PRESENT +} 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 new file mode 100644 index 0000000000..e0a5a5d044 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs @@ -0,0 +1,682 @@ +pub mod display; +pub mod gtt; +pub mod ring; + +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; + +use log::{debug, info, warn}; +use redox_driver_sys::memory::MmioRegion; +use redox_driver_sys::pci::{PciBarInfo, PciDevice, PciDeviceInfo}; +use redox_driver_sys::quirks::PciQuirkFlags; + +use crate::driver::{DriverError, DriverEvent, GpuDriver, Result}; +use crate::drivers::interrupt::InterruptHandle; +use crate::gem::{GemHandle, GemManager}; +use crate::kms::connector::{synthetic_edid, Connector}; +use crate::kms::crtc::Crtc; +use crate::kms::encoder::Encoder; +use crate::kms::{ConnectorInfo, ConnectorType, ModeInfo}; + +use self::display::{DisplayPipe, IntelDisplay}; +use self::gtt::IntelGtt; +use self::ring::{IntelRing, RingType}; + +const FORCEWAKE: usize = 0xA18C; +const PP_STATUS: usize = 0xC7200; +const PIPECONF_BASE: usize = 0x70008; +const PIPE_STRIDE: usize = 0x1000; +const DDI_BUF_CTL_BASE: usize = 0x64000; +const DDI_PORT_STRIDE: usize = 0x100; +const GFX_FLSH_CNTL_REG: usize = 0x101008; + +const RENDER_RING_BASE: usize = 0x02000; +const RING_TAIL_OFFSET: usize = 0x30; +const RING_HEAD_OFFSET: usize = 0x34; + +pub struct IntelDriver { + info: PciDeviceInfo, + mmio: MmioRegion, + irq_handle: Mutex>, + display: IntelDisplay, + gem: Mutex, + connectors: Mutex>, + crtcs: Mutex>, + encoders: Mutex>, + gtt: Mutex, + ring: Mutex, + vblank_count: AtomicU64, +} + +impl IntelDriver { + pub fn new(info: PciDeviceInfo, firmware: HashMap>) -> Result { + if !info.is_intel_gpu() { + return Err(DriverError::Pci(format!( + "device {} is not an Intel display-class GPU", + info.location + ))); + } + + let quirks = info.quirks(); + if !quirks.is_empty() { + info!( + "redox-drm: Intel init for {} using quirk policy {:?}", + info.location, quirks + ); + } + if quirks.contains(PciQuirkFlags::DISABLE_ACCEL) { + return Err(DriverError::Pci(format!( + "device {:#06x}:{:#06x} at {} has DISABLE_ACCEL quirk — refusing Intel init", + info.vendor_id, info.device_id, info.location + ))); + } + if quirks.contains(PciQuirkFlags::NEED_FIRMWARE) { + info!( + "redox-drm: Intel device {} entered init with explicit firmware policy and {} cached blob(s)", + info.location, + firmware.len() + ); + } + + let gtt_bar = find_memory_bar(&info, 0, "GGTT BAR0")?; + let mmio_bar = find_memory_bar(&info, 2, "MMIO BAR2")?; + validate_intel_bars(&info, >t_bar, &mmio_bar)?; + + let mut device = PciDevice::open_location(&info.location) + .map_err(|e| DriverError::Pci(format!("failed to re-open PCI device: {e}")))?; + device + .enable_device() + .map_err(|e| DriverError::Pci(format!("enable_device failed: {e}")))?; + + let mmio = map_bar(&mut device, &mmio_bar, "Intel MMIO BAR2")?; + let display_mmio = map_bar(&mut device, &mmio_bar, "Intel display MMIO")?; + let ring_mmio = map_bar(&mut device, &mmio_bar, "Intel ring MMIO")?; + 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")?; + + enable_forcewake(&mmio)?; + + 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)?; + ring.bind_gtt(&mut gtt)?; + + let (connectors, encoders) = detect_display_topology(&display)?; + let crtcs = build_crtcs(&display)?; + + let irq_handle = match InterruptHandle::setup(&info, &mut device) { + Ok(handle) => Some(handle), + Err(e) => { + warn!( + "redox-drm: Intel device {} interrupt setup failed: {e}", + info.location + ); + None + } + }; + let irq_mode = irq_handle + .as_ref() + .map(|handle| handle.mode_name()) + .unwrap_or("none"); + + if !firmware.is_empty() { + info!( + "redox-drm: Intel startup firmware cache populated with {} blob(s) for {}", + firmware.len(), + info.location + ); + } + + info!( + "redox-drm: Intel driver ready for {} with {} connector(s), IRQ mode {}", + info.location, + connectors.len(), + irq_mode + ); + + Ok(Self { + info, + mmio, + irq_handle: Mutex::new(irq_handle), + display, + gem: Mutex::new(GemManager::new()), + connectors: Mutex::new(connectors), + crtcs: Mutex::new(crtcs), + encoders: Mutex::new(encoders), + gtt: Mutex::new(gtt), + ring: Mutex::new(ring), + vblank_count: AtomicU64::new(0), + }) + } + + fn refresh_connectors(&self) -> Result> { + let (connectors, encoders) = detect_display_topology(&self.display)?; + let infos = connectors + .iter() + .map(|connector| connector.info.clone()) + .collect(); + + { + let mut connector_state = self.connectors.lock().map_err(|_| { + DriverError::Initialization("Intel connector state poisoned".into()) + })?; + *connector_state = connectors; + } + + { + let mut encoder_state = self + .encoders + .lock() + .map_err(|_| DriverError::Initialization("Intel encoder state poisoned".into()))?; + *encoder_state = encoders; + } + + Ok(infos) + } + + fn cached_connectors(&self) -> Vec { + match self.connectors.lock() { + Ok(connectors) => connectors + .iter() + .map(|connector| connector.info.clone()) + .collect(), + Err(poisoned) => { + warn!("redox-drm: Intel connector state poisoned; using inner state"); + poisoned + .into_inner() + .iter() + .map(|connector| connector.info.clone()) + .collect() + } + } + } + + fn connector_port(&self, connector_id: u32) -> Result { + let connectors = self + .connectors + .lock() + .map_err(|_| DriverError::Initialization("Intel connector state poisoned".into()))?; + let connector = connectors + .iter() + .find(|connector| connector.info.id == connector_id) + .ok_or_else(|| DriverError::NotFound(format!("unknown connector {connector_id}")))?; + + Ok(connector.info.connector_type_id.saturating_sub(1) as u8) + } + + fn process_irq(&self) -> Result> { + let previous = self.cached_connectors(); + let current = self.refresh_connectors()?; + + if connector_status_changed(&previous, ¤t) { + info!( + "redox-drm: Intel hotplug event detected on {}", + self.info.location + ); + if let Some(connector) = current.iter().find(|connector| { + previous + .iter() + .find(|old| old.id == connector.id) + .map(|old| old.connection != connector.connection) + .unwrap_or(true) + }) { + return Ok(Some(DriverEvent::Hotplug { + connector_id: connector.id, + })); + } + } + + let ring_busy = self + .ring + .lock() + .map_err(|_| DriverError::Initialization("Intel ring state poisoned".into()))? + .has_activity()?; + + if let Some(crtc_id) = self.active_crtc_id()? { + let count = self.vblank_count.fetch_add(1, Ordering::SeqCst) + 1; + debug!( + "redox-drm: Intel IRQ decoded as display event crtc={} ring_busy={}", + crtc_id, ring_busy + ); + return Ok(Some(DriverEvent::Vblank { crtc_id, count })); + } + + if ring_busy { + debug!("redox-drm: Intel IRQ signaled command stream activity without active CRTC"); + } + + Ok(None) + } + + fn active_crtc_id(&self) -> Result> { + let crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("Intel CRTC state poisoned".into()))?; + + if let Some(active) = crtcs.iter().find(|crtc| crtc.mode.is_some()) { + return Ok(Some(active.id)); + } + + Ok(self + .display + .pipes()? + .into_iter() + .find(|pipe| pipe.enabled) + .map(|pipe| u32::from(pipe.index) + 1)) + } + + fn ensure_gem_gpu_mapping(&self, handle: GemHandle) -> Result { + { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))?; + if let Some(gpu_addr) = gem.gpu_addr(handle)? { + return Ok(gpu_addr); + } + } + + let (phys_addr, size) = { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))?; + let object = gem.object(handle)?; + (object.phys_addr as u64, object.size) + }; + + let gpu_addr = { + let mut gtt = self + .gtt + .lock() + .map_err(|_| DriverError::Initialization("Intel GGTT state poisoned".into()))?; + let gpu_addr = gtt.alloc_range(size)?; + if let Err(error) = gtt.map_range(gpu_addr, phys_addr, size, 1 << 1) { + let _ = gtt.release_range(gpu_addr, size); + return Err(error); + } + gpu_addr + }; + + if let Err(error) = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))? + .set_gpu_addr(handle, gpu_addr) + { + let mut gtt = self + .gtt + .lock() + .map_err(|_| DriverError::Initialization("Intel GGTT state poisoned".into()))?; + let _ = gtt.unmap_range(gpu_addr, size); + let _ = gtt.release_range(gpu_addr, size); + return Err(error); + } + + Ok(gpu_addr) + } + + fn read_mmio(&self, offset: usize) -> Result { + let end = offset + .checked_add(core::mem::size_of::()) + .ok_or_else(|| { + DriverError::Mmio(format!("Intel MMIO offset overflow at {offset:#x}")) + })?; + if end > self.mmio.size() { + return Err(DriverError::Mmio(format!( + "Intel MMIO read outside BAR2 aperture: end={end:#x} size={:#x}", + self.mmio.size() + ))); + } + Ok(self.mmio.read32(offset)) + } +} + +impl GpuDriver for IntelDriver { + fn driver_name(&self) -> &str { + "i915-redox" + } + + fn driver_desc(&self) -> &str { + "Intel i915-class DRM/KMS backend for Redox" + } + + fn driver_date(&self) -> &str { + "2026-04-12" + } + + fn detect_connectors(&self) -> Vec { + match self.refresh_connectors() { + Ok(connectors) => connectors, + Err(error) => { + warn!("redox-drm: Intel connector refresh failed: {}", error); + self.cached_connectors() + } + } + } + + fn get_modes(&self, connector_id: u32) -> Vec { + self.detect_connectors() + .into_iter() + .find(|connector| connector.id == connector_id) + .map(|connector| connector.modes) + .unwrap_or_default() + } + + fn set_crtc( + &self, + crtc_id: u32, + fb_handle: u32, + connectors: &[u32], + mode: &ModeInfo, + ) -> Result<()> { + if connectors.is_empty() { + return Err(DriverError::InvalidArgument( + "set_crtc requires at least one connector", + )); + } + + let fb_addr = self.ensure_gem_gpu_mapping(fb_handle)?; + let mut pipe = self.display.pipe_for_crtc(crtc_id)?; + pipe.port = Some(self.connector_port(connectors[0])?); + + self.display.set_mode(&pipe, mode)?; + self.display.page_flip(&pipe, fb_addr)?; + + let mut crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("Intel CRTC state poisoned".into()))?; + let crtc = crtcs + .iter_mut() + .find(|crtc| crtc.id == crtc_id) + .ok_or_else(|| DriverError::NotFound(format!("unknown CRTC {crtc_id}")))?; + crtc.program(fb_handle, connectors, mode) + } + + fn page_flip(&self, crtc_id: u32, fb_handle: u32, _flags: u32) -> Result { + let fb_addr = self.ensure_gem_gpu_mapping(fb_handle)?; + let pipe = self.display.pipe_for_crtc(crtc_id)?; + self.display.page_flip(&pipe, fb_addr)?; + + let mut ring = self + .ring + .lock() + .map_err(|_| DriverError::Initialization("Intel ring state poisoned".into()))?; + ring.flush()?; + Ok(ring.last_seqno()) + } + + fn get_vblank(&self, crtc_id: u32) -> Result { + let crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("Intel CRTC state poisoned".into()))?; + if !crtcs.iter().any(|crtc| crtc.id == crtc_id) { + return Err(DriverError::NotFound(format!("unknown CRTC {crtc_id}"))); + } + Ok(self.vblank_count.load(Ordering::SeqCst)) + } + + fn gem_create(&self, size: u64) -> Result { + let handle = { + let mut gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))?; + gem.create(size)? + }; + + if let Err(error) = self.ensure_gem_gpu_mapping(handle) { + let _ = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))? + .close(handle); + return Err(error); + } + + Ok(handle) + } + + fn gem_close(&self, handle: GemHandle) -> Result<()> { + let (gpu_addr, size) = { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))?; + let object = gem.object(handle)?; + (object.gpu_addr, object.size) + }; + + if let Some(gpu_addr) = gpu_addr { + let mut gtt = self + .gtt + .lock() + .map_err(|_| DriverError::Initialization("Intel GGTT state poisoned".into()))?; + gtt.unmap_range(gpu_addr, size)?; + gtt.release_range(gpu_addr, size)?; + } + + self.gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))? + .close(handle) + } + + fn gem_mmap(&self, handle: GemHandle) -> Result { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))?; + gem.mmap(handle) + } + + fn gem_size(&self, handle: GemHandle) -> Result { + let gem = self + .gem + .lock() + .map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))?; + Ok(gem.object(handle)?.size) + } + + fn get_edid(&self, connector_id: u32) -> Vec { + match self.connectors.lock() { + Ok(connectors) => connectors + .iter() + .find(|connector| connector.info.id == connector_id) + .map(|connector| connector.edid.clone()) + .unwrap_or_default(), + Err(poisoned) => poisoned + .into_inner() + .iter() + .find(|connector| connector.info.id == connector_id) + .map(|connector| connector.edid.clone()) + .unwrap_or_default(), + } + } + + fn handle_irq(&self) -> Result> { + let irq_event = { + let mut irq_handle = self + .irq_handle + .lock() + .map_err(|_| DriverError::Initialization("Intel IRQ state poisoned".into()))?; + match irq_handle.as_mut() { + Some(handle) => handle + .try_wait() + .map_err(|e| DriverError::Io(format!("Intel IRQ poll failed: {e}")))?, + None => return Ok(None), + } + }; + + if !irq_event { + return Ok(None); + } + + self.process_irq() + } +} + +fn detect_display_topology(display: &IntelDisplay) -> Result<(Vec, Vec)> { + let detected = display.detect_connectors()?; + let mut connectors = Vec::with_capacity(detected.len()); + let mut encoders = Vec::with_capacity(detected.len()); + + for connector in detected { + let port = connector.connector_type_id.saturating_sub(1) as u8; + let edid = match connector.connector_type { + ConnectorType::DisplayPort | ConnectorType::EDP => display.read_edid(port), + _ => display.read_edid(port), + }; + + encoders.push(Encoder::new( + connector.encoder_id, + pipe_id_for_port(display, port), + )); + connectors.push(Connector { + edid: if edid.is_empty() { + synthetic_edid() + } else { + edid + }, + info: ConnectorInfo { + modes: display.modes_for_connector(&connector), + ..connector + }, + }); + } + + Ok((connectors, encoders)) +} + +fn build_crtcs(display: &IntelDisplay) -> Result> { + let mut crtcs: Vec = display + .pipes()? + .into_iter() + .map(|pipe| Crtc::new(u32::from(pipe.index) + 1)) + .collect(); + + if crtcs.is_empty() { + crtcs.push(Crtc::new(1)); + } + + Ok(crtcs) +} + +fn pipe_id_for_port(display: &IntelDisplay, port: u8) -> u32 { + display + .pipes() + .ok() + .and_then(|pipes| { + pipes + .into_iter() + .find(|pipe| pipe.port == Some(port)) + .map(|pipe| u32::from(pipe.index) + 1) + }) + .unwrap_or(1) +} + +fn connector_status_changed(previous: &[ConnectorInfo], current: &[ConnectorInfo]) -> bool { + if previous.len() != current.len() { + return true; + } + + previous.iter().zip(current.iter()).any(|(old, new)| { + old.id != new.id + || old.connection != new.connection + || old.connector_type != new.connector_type + }) +} + +fn enable_forcewake(mmio: &MmioRegion) -> Result<()> { + let end = FORCEWAKE + .checked_add(core::mem::size_of::()) + .ok_or_else(|| DriverError::Mmio("Intel FORCEWAKE offset overflow".into()))?; + if end > mmio.size() { + return Err(DriverError::Mmio(format!( + "Intel FORCEWAKE register outside MMIO aperture: end={end:#x} size={:#x}", + mmio.size() + ))); + } + + mmio.write32(FORCEWAKE, 1); + let _ = mmio.read32(FORCEWAKE); + Ok(()) +} + +fn validate_intel_bars( + info: &PciDeviceInfo, + gtt_bar: &PciBarInfo, + mmio_bar: &PciBarInfo, +) -> Result<()> { + if !gtt_bar.is_memory() { + return Err(DriverError::Pci(format!( + "device {} GGTT BAR{} is not a memory BAR", + info.location, gtt_bar.index + ))); + } + if !mmio_bar.is_memory() { + return Err(DriverError::Pci(format!( + "device {} MMIO BAR{} is not a memory BAR", + info.location, mmio_bar.index + ))); + } + + if gtt_bar.size < core::mem::size_of::() as u64 { + return Err(DriverError::Pci(format!( + "device {} GGTT BAR{} is too small ({:#x})", + info.location, gtt_bar.index, gtt_bar.size + ))); + } + if gtt_bar.size % core::mem::size_of::() as u64 != 0 { + return Err(DriverError::Pci(format!( + "device {} GGTT BAR{} size {:#x} is not 8-byte aligned", + info.location, gtt_bar.index, gtt_bar.size + ))); + } + + let required_mmio_end = [ + FORCEWAKE + core::mem::size_of::(), + PP_STATUS + core::mem::size_of::(), + GFX_FLSH_CNTL_REG + core::mem::size_of::(), + RENDER_RING_BASE + RING_TAIL_OFFSET + core::mem::size_of::(), + RENDER_RING_BASE + RING_HEAD_OFFSET + core::mem::size_of::(), + ] + .into_iter() + .max() + .unwrap_or(0); + + if mmio_bar.size < required_mmio_end as u64 { + return Err(DriverError::Pci(format!( + "device {} MMIO BAR{} is too small ({:#x}) for required register window ending at {:#x}", + info.location, mmio_bar.index, mmio_bar.size, required_mmio_end + ))); + } + + Ok(()) +} + +fn find_memory_bar(info: &PciDeviceInfo, index: usize, name: &str) -> Result { + info.find_memory_bar(index) + .copied() + .ok_or_else(|| DriverError::Pci(format!("device {} has no {}", info.location, name))) +} + +fn map_bar(device: &mut PciDevice, bar: &PciBarInfo, name: &str) -> Result { + device + .map_bar(bar.index, bar.addr, bar.size as usize) + .map_err(|e| DriverError::Mmio(format!("failed to map {name}: {e}"))) +} + +#[allow(dead_code)] +fn ddi_buf_ctl(port: u8) -> usize { + DDI_BUF_CTL_BASE + usize::from(port) * DDI_PORT_STRIDE +} + +#[allow(dead_code)] +fn pipeconf(pipe: &DisplayPipe) -> usize { + PIPECONF_BASE + usize::from(pipe.index) * PIPE_STRIDE +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/intel/ring.rs b/local/recipes/gpu/redox-drm/source/src/drivers/intel/ring.rs new file mode 100644 index 0000000000..bcdaccd959 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/intel/ring.rs @@ -0,0 +1,267 @@ +use std::thread; +use std::time::Duration; + +use log::{debug, info}; +use redox_driver_sys::dma::DmaBuffer; +use redox_driver_sys::memory::MmioRegion; + +use crate::driver::{DriverError, Result}; + +use super::gtt::IntelGtt; + +const RING_BUFFER_BYTES: usize = 4096; +const RING_ALIGNMENT: usize = 4096; +const RING_WAIT_ATTEMPTS: usize = 2000; +const RING_WAIT_DELAY: Duration = Duration::from_micros(50); + +const RBBASE: usize = 0x04; +const RBBASE_HI: usize = 0x08; +const RBTAIL: usize = 0x30; +const RBHEAD: usize = 0x34; +const RBSTART: usize = 0x38; +const RBCTL: usize = 0x3C; + +const RING_CTL_ENABLE: u32 = 1 << 0; +const RING_CTL_SIZE_MASK: u32 = !0x0FFF; + +const MI_NOOP: u32 = 0x0000_0000; +const MI_FLUSH_DW: u32 = 0x0200_0000; + +#[derive(Clone, Copy, Debug)] +pub enum RingType { + Render, + Blitter, + VideoEnhance, +} + +pub struct IntelRing { + mmio: MmioRegion, + base: usize, + head: u32, + tail: u32, + size: u32, + ring_type: RingType, + buffer: DmaBuffer, + gpu_addr: Option, + last_seqno: u64, +} + +impl IntelRing { + pub fn create(mmio: MmioRegion, ring_type: RingType) -> Result { + let mut buffer = DmaBuffer::allocate(RING_BUFFER_BYTES, RING_ALIGNMENT) + .map_err(|e| DriverError::Buffer(format!("Intel ring allocation failed: {e}")))?; + zero_dma(&mut buffer); + + let ring = Self { + mmio, + base: ring_base(ring_type), + head: 0, + tail: 0, + size: RING_BUFFER_BYTES as u32, + ring_type, + buffer, + gpu_addr: None, + last_seqno: 0, + }; + + ring.ensure_reg_access(RBCTL, core::mem::size_of::(), "ring control")?; + ring.write_reg(RBHEAD, 0)?; + ring.write_reg(RBTAIL, 0)?; + ring.write_reg(RBSTART, 0)?; + + info!( + "redox-drm: Intel {:?} ring allocated ({} bytes)", + ring.ring_type, ring.size + ); + Ok(ring) + } + + pub fn bind_gtt(&mut self, gtt: &mut IntelGtt) -> Result<()> { + if self.gpu_addr.is_some() { + return Ok(()); + } + + let gpu_addr = gtt.alloc_range(self.size as u64)?; + if let Err(error) = gtt.map_range( + gpu_addr, + self.buffer.physical_address() as u64, + self.size as u64, + 1 << 1, + ) { + let _ = gtt.release_range(gpu_addr, self.size as u64); + return Err(error); + } + + self.gpu_addr = Some(gpu_addr); + self.program_ring_registers(gpu_addr)?; + Ok(()) + } + + pub fn submit_batch(&mut self, buffer: &[u32]) -> Result<()> { + if buffer.is_empty() { + return Ok(()); + } + if self.gpu_addr.is_none() { + return Err(DriverError::Initialization( + "Intel ring must be bound into GGTT before submission".into(), + )); + } + + self.wait_for_space(buffer.len())?; + + for &dword in buffer { + self.write_dword(dword)?; + } + + self.publish_tail()?; + self.last_seqno = self.last_seqno.saturating_add(1); + debug!( + "redox-drm: Intel {:?} ring submitted {} DWORDs seqno={}", + self.ring_type, + buffer.len(), + self.last_seqno + ); + Ok(()) + } + + pub fn wait_for_space(&mut self, count: usize) -> Result<()> { + let required = (count * core::mem::size_of::()) as u32; + if required >= self.size { + return Err(DriverError::Buffer(format!( + "Intel ring submission too large: {required} bytes >= ring size {}", + self.size + ))); + } + + for _ in 0..RING_WAIT_ATTEMPTS { + self.sync_from_hw()?; + if required <= self.free_bytes() { + return Ok(()); + } + thread::sleep(RING_WAIT_DELAY); + } + + Err(DriverError::Buffer(format!( + "Intel {:?} ring did not free {} bytes in time", + self.ring_type, required + ))) + } + + pub fn flush(&mut self) -> Result<()> { + self.submit_batch(&[MI_FLUSH_DW, MI_NOOP]) + } + + pub fn has_activity(&mut self) -> Result { + self.sync_from_hw()?; + Ok(self.head != self.tail) + } + + pub fn sync_from_hw(&mut self) -> Result<()> { + self.head = self.read_reg(RBHEAD)? & (self.size - 1); + self.tail = self.read_reg(RBTAIL)? & (self.size - 1); + Ok(()) + } + + pub fn last_seqno(&self) -> u64 { + self.last_seqno + } + + fn program_ring_registers(&mut self, gpu_addr: u64) -> Result<()> { + self.write_reg(RBHEAD, 0)?; + self.write_reg(RBTAIL, 0)?; + self.write_reg(RBSTART, lower_32(gpu_addr))?; + self.write_reg(RBBASE, lower_32(gpu_addr))?; + self.write_reg(RBBASE_HI, upper_32(gpu_addr))?; + + let mut ctl = self.read_reg(RBCTL)?; + ctl &= !RING_CTL_SIZE_MASK; + ctl |= (self.size - 0x1000) & RING_CTL_SIZE_MASK; + ctl |= RING_CTL_ENABLE; + self.write_reg(RBCTL, ctl)?; + Ok(()) + } + + fn free_bytes(&self) -> u32 { + let used = if self.tail >= self.head { + self.tail - self.head + } else { + self.size - (self.head - self.tail) + }; + self.size.saturating_sub(used).saturating_sub(4) + } + + fn write_dword(&mut self, value: u32) -> Result<()> { + let write_offset = self.tail as usize; + let width = core::mem::size_of::(); + let end = write_offset + .checked_add(width) + .ok_or_else(|| DriverError::Buffer("Intel ring write offset overflow".into()))?; + if end > self.buffer.len() { + return Err(DriverError::Buffer(format!( + "Intel ring write out of bounds: end={end:#x} size={:#x}", + self.buffer.len() + ))); + } + let ptr = unsafe { self.buffer.as_mut_ptr().add(write_offset) as *mut u32 }; + unsafe { core::ptr::write_volatile(ptr, value) }; + + self.tail = (self.tail + width as u32) % self.size; + Ok(()) + } + + fn publish_tail(&self) -> Result<()> { + self.write_reg(RBTAIL, self.tail) + } + + fn read_reg(&self, reg: usize) -> Result { + let offset = self + .base + .checked_add(reg) + .ok_or_else(|| DriverError::Mmio("Intel ring register offset overflow".into()))?; + self.ensure_reg_access(offset, core::mem::size_of::(), "ring read")?; + Ok(self.mmio.read32(offset)) + } + + fn write_reg(&self, reg: usize, value: u32) -> Result<()> { + let offset = self + .base + .checked_add(reg) + .ok_or_else(|| DriverError::Mmio("Intel ring register offset overflow".into()))?; + self.ensure_reg_access(offset, core::mem::size_of::(), "ring write")?; + self.mmio.write32(offset, value); + Ok(()) + } + + fn ensure_reg_access(&self, offset: usize, width: usize, op: &str) -> Result<()> { + let end = offset.checked_add(width).ok_or_else(|| { + DriverError::Mmio(format!("Intel {op} offset overflow at {offset:#x}")) + })?; + if end > self.mmio.size() { + return Err(DriverError::Mmio(format!( + "Intel {op} outside MMIO aperture: end={end:#x} size={:#x}", + self.mmio.size() + ))); + } + Ok(()) + } +} + +fn ring_base(ring_type: RingType) -> usize { + match ring_type { + RingType::Render => 0x02000, + RingType::Blitter => 0x22000, + RingType::VideoEnhance => 0x1A000, + } +} + +fn zero_dma(buffer: &mut DmaBuffer) { + unsafe { core::ptr::write_bytes(buffer.as_mut_ptr(), 0, buffer.len()) }; +} + +fn lower_32(value: u64) -> u32 { + value as u32 +} + +fn upper_32(value: u64) -> u32 { + (value >> 32) as u32 +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/interrupt.rs b/local/recipes/gpu/redox-drm/source/src/drivers/interrupt.rs new file mode 100644 index 0000000000..657c34dacf --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/interrupt.rs @@ -0,0 +1,244 @@ +use std::io::{Read, Write}; + +use log::{info, warn}; +use redox_driver_sys::irq::{IrqHandle, MsixTable, MsixVector}; +use redox_driver_sys::pci::{PciDevice, PciDeviceInfo, PCI_CAP_ID_MSI, PCI_CAP_ID_MSIX}; +use redox_driver_sys::quirks::PciQuirkFlags; + +use crate::driver::{DriverError, Result}; + +pub enum InterruptHandle { + Msix { + vector: MsixVector, + table: MsixTable, + cap_offset: u8, + }, + Msi { + handle: IrqHandle, + irq: u32, + }, + Legacy { + handle: IrqHandle, + irq: u32, + }, +} + +fn force_legacy_irq(quirks: PciQuirkFlags) -> bool { + quirks.contains(PciQuirkFlags::FORCE_LEGACY_IRQ) +} + +impl InterruptHandle { + pub fn setup(device_info: &PciDeviceInfo, pci_device: &mut PciDevice) -> Result { + let quirks = device_info.quirks(); + + if force_legacy_irq(quirks) { + info!( + "redox-drm: forcing legacy IRQ for {} (FORCE_LEGACY_IRQ quirk)", + device_info.location + ); + return Self::try_legacy(device_info); + } + + if !quirks.contains(PciQuirkFlags::NO_MSIX) { + if let Ok(Some(handle)) = Self::try_msix(device_info, pci_device) { + return Ok(handle); + } + } else { + info!( + "redox-drm: skipping MSI-X for {} (NO_MSIX quirk)", + device_info.location + ); + } + + if !quirks.contains(PciQuirkFlags::NO_MSI) { + if let Ok(Some(handle)) = Self::try_msi(device_info, pci_device) { + return Ok(handle); + } + } else { + info!( + "redox-drm: skipping MSI for {} (NO_MSI quirk)", + device_info.location + ); + } + + Self::try_legacy(device_info) + } + + fn try_msix(device_info: &PciDeviceInfo, pci_device: &mut PciDevice) -> Result> { + let msix_cap = match device_info.find_capability(PCI_CAP_ID_MSIX) { + Some(cap) => cap, + None => return Ok(None), + }; + + let msix_info = match pci_device.parse_msix(msix_cap.offset) { + Ok(info) => info, + Err(e) => { + warn!( + "redox-drm: MSI-X capability parse failed for {}: {e}", + device_info.location + ); + return Ok(None); + } + }; + + let table = match MsixTable::map(device_info, &msix_info) { + Ok(t) => t, + Err(e) => { + warn!( + "redox-drm: MSI-X table map failed for {}: {e}", + device_info.location + ); + return Ok(None); + } + }; + + table.mask_all(); + + if let Err(e) = pci_device.enable_msix(msix_cap.offset) { + warn!( + "redox-drm: MSI-X enable failed for {}: {e}", + device_info.location + ); + return Ok(None); + } + + let vector = match table.request_vector(0) { + Ok(v) => v, + Err(e) => { + warn!( + "redox-drm: MSI-X vector allocation failed for {}: {e}", + device_info.location + ); + let _ = pci_device.disable_msix(msix_cap.offset); + return Ok(None); + } + }; + + info!( + "redox-drm: MSI-X enabled for {} vector {} irq {}", + device_info.location, vector.index, vector.irq + ); + + Ok(Some(InterruptHandle::Msix { + vector, + table, + cap_offset: msix_cap.offset, + })) + } + + fn try_msi(device_info: &PciDeviceInfo, _pci_device: &mut PciDevice) -> Result> { + let msi_cap = match device_info.find_capability(PCI_CAP_ID_MSI) { + Some(cap) => cap, + None => return Ok(None), + }; + + let irq = device_info.irq.ok_or_else(|| { + DriverError::Io(format!("no IRQ for MSI on {}", device_info.location)) + })?; + + let handle = match IrqHandle::request(irq) { + Ok(h) => h, + Err(e) => { + warn!( + "redox-drm: MSI IRQ request failed for {}: {e}", + device_info.location + ); + return Ok(None); + } + }; + + info!( + "redox-drm: MSI enabled for {} cap_offset={:#x} irq {}", + device_info.location, msi_cap.offset, irq + ); + + Ok(Some(InterruptHandle::Msi { handle, irq })) + } + + fn try_legacy(device_info: &PciDeviceInfo) -> Result { + let irq = device_info + .irq + .ok_or_else(|| DriverError::Io(format!("no IRQ for {}", device_info.location)))?; + + let handle = IrqHandle::request(irq).map_err(|e| DriverError::Io(e.to_string()))?; + info!( + "redox-drm: using legacy IRQ {irq} for {}", + device_info.location + ); + + Ok(InterruptHandle::Legacy { handle, irq }) + } + + pub fn try_wait(&mut self) -> Result { + match self { + InterruptHandle::Msix { vector, .. } => { + let mut buf = [0u8; 8]; + match vector.fd.read(&mut buf) { + Ok(n) if n > 0 => Ok(true), + Ok(_) => Ok(false), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(false), + Err(e) => Err(DriverError::Io(e.to_string())), + } + } + InterruptHandle::Msi { handle, .. } | InterruptHandle::Legacy { handle, .. } => handle + .try_wait() + .map(|ev| ev.is_some()) + .map_err(|e| DriverError::Io(e.to_string())), + } + } + + pub fn eoi(&mut self) -> Result<()> { + match self { + InterruptHandle::Msix { vector, .. } => { + let mut buf = [0u8; 8]; + vector + .fd + .read_exact(&mut buf) + .map_err(|e| DriverError::Io(e.to_string()))?; + vector + .fd + .write_all(&buf) + .map_err(|e| DriverError::Io(e.to_string())) + } + InterruptHandle::Msi { handle, .. } | InterruptHandle::Legacy { handle, .. } => { + let _ = handle.wait().map_err(|e| DriverError::Io(e.to_string()))?; + Ok(()) + } + } + } + + pub fn irq(&self) -> u32 { + match self { + InterruptHandle::Msix { vector, .. } => vector.irq, + InterruptHandle::Msi { irq, .. } | InterruptHandle::Legacy { irq, .. } => *irq, + } + } + + pub fn mode_name(&self) -> &'static str { + match self { + InterruptHandle::Msix { .. } => "MSI-X", + InterruptHandle::Msi { .. } => "MSI", + InterruptHandle::Legacy { .. } => "legacy INTx", + } + } + + pub fn is_msix(&self) -> bool { + matches!(self, InterruptHandle::Msix { .. }) + } +} + +#[cfg(test)] +mod tests { + use super::force_legacy_irq; + use redox_driver_sys::quirks::PciQuirkFlags; + + #[test] + fn force_legacy_irq_only_triggers_on_quirk() { + assert!(!force_legacy_irq(PciQuirkFlags::empty())); + assert!(!force_legacy_irq(PciQuirkFlags::NO_MSI)); + assert!(force_legacy_irq(PciQuirkFlags::FORCE_LEGACY_IRQ)); + assert!(force_legacy_irq( + PciQuirkFlags::FORCE_LEGACY_IRQ | PciQuirkFlags::NO_MSIX + )); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/drivers/mod.rs b/local/recipes/gpu/redox-drm/source/src/drivers/mod.rs new file mode 100644 index 0000000000..3cfc4f3588 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/mod.rs @@ -0,0 +1,196 @@ +pub mod amd; +pub mod intel; +pub mod interrupt; +pub mod virtio; + +use std::collections::HashMap; +use std::sync::Arc; + +use log::info; +use redox_driver_sys::pci::{PciDevice, PciDeviceInfo, PCI_VENDOR_ID_AMD, PCI_VENDOR_ID_INTEL}; +use redox_driver_sys::quirks::PciQuirkFlags; + +use crate::driver::{DriverError, GpuDriver, Result}; + +pub struct DriverRegistry; + +/// Intel GPU device IDs organized by generation. +/// Source: Linux i915_pciids.h (kernel 7.0) and Intel public documentation. +const INTEL_GEN12_TGL_IDS: &[u16] = &[0x9A40, 0x9A49, 0x9A60, 0x9A68, 0x9A70, 0x9A78]; +const INTEL_GEN12_ADLP_IDS: &[u16] = &[0x46A6]; +const INTEL_GEN12_DG2_IDS: &[u16] = &[ + 0x5690, 0x5691, 0x5692, 0x5693, 0x5694, 0x5695, 0x5696, 0x5697, 0x56A0, 0x56A1, 0x56A2, 0x56A3, + 0x56A4, 0x56A5, 0x56A6, 0x56B0, 0x56B1, 0x56B2, 0x56B3, 0x56BA, 0x56BB, 0x56BC, 0x56BD, 0x56BE, + 0x56BF, 0x56C0, 0x56C1, +]; +const INTEL_GEN12_MTL_IDS: &[u16] = &[ + 0x7D40, 0x7D41, 0x7D45, 0x7D51, 0x7D55, 0x7D60, 0x7D67, 0x7DD1, 0x7DD5, +]; +const INTEL_GEN12_ARL_IDS: &[u16] = &[0x6420, 0x64A0, 0x64B0]; +const INTEL_GEN12_LNL_IDS: &[u16] = &[0xB640]; +const INTEL_GEN12_BMG_IDS: &[u16] = &[ + 0xE202, 0xE209, 0xE20B, 0xE20C, 0xE20D, 0xE210, 0xE211, 0xE212, 0xE216, 0xE220, 0xE221, 0xE222, + 0xE223, +]; + +fn is_supported_intel_generation(device_id: u16) -> bool { + // Gen8+ (Skylake and newer) have DMC firmware available in the firmware package + INTEL_SKL_KBL_CFL_IDS.contains(&device_id) + || INTEL_CNL_ICL_TGL_IDS.contains(&device_id) + || INTEL_GEN12_TGL_IDS.contains(&device_id) + || INTEL_GEN12_ADLP_IDS.contains(&device_id) + || INTEL_GEN12_DG2_IDS.contains(&device_id) + || INTEL_GEN12_MTL_IDS.contains(&device_id) + || INTEL_GEN12_ARL_IDS.contains(&device_id) + || INTEL_GEN12_LNL_IDS.contains(&device_id) + || INTEL_GEN12_BMG_IDS.contains(&device_id) +} + +fn intel_generation_name(device_id: u16) -> &'static str { + if INTEL_GEN12_TGL_IDS.contains(&device_id) { + return "12 (Tiger Lake)"; + } + if INTEL_GEN12_ADLP_IDS.contains(&device_id) { + return "12 (Alder Lake-P)"; + } + if INTEL_GEN12_DG2_IDS.contains(&device_id) { + return "12 (DG2/Alchemist)"; + } + if INTEL_GEN12_MTL_IDS.contains(&device_id) { + return "12 (Meteor Lake)"; + } + if INTEL_GEN12_ARL_IDS.contains(&device_id) { + return "12 (Arrow Lake)"; + } + if INTEL_GEN12_LNL_IDS.contains(&device_id) { + return "12 (Lunar Lake)"; + } + if INTEL_GEN12_BMG_IDS.contains(&device_id) { + return "12 (Battlemage)"; + } + if is_intel_gen4_11(device_id) { + return intel_gen4_11_name(device_id); + } + "? (unknown/unsupported)" +} + +fn is_intel_gen4_11(device_id: u16) -> bool { + INTEL_I965G_IDS.contains(&device_id) + || INTEL_ILK_IDS.contains(&device_id) + || INTEL_SNB_IDS.contains(&device_id) + || INTEL_IVB_HSW_BDW_IDS.contains(&device_id) + || INTEL_SKL_KBL_CFL_IDS.contains(&device_id) + || INTEL_CNL_ICL_TGL_IDS.contains(&device_id) +} + +fn intel_gen4_11_name(device_id: u16) -> &'static str { + if INTEL_I965G_IDS.contains(&device_id) { + return "4 (i965/G33/G45/GM45/Pineview)"; + } + if INTEL_ILK_IDS.contains(&device_id) { + return "5 (Ironlake)"; + } + if INTEL_SNB_IDS.contains(&device_id) { + return "6 (Sandy Bridge)"; + } + if INTEL_IVB_HSW_BDW_IDS.contains(&device_id) { + return "7 (Ivy Bridge/Haswell/Broadwell)"; + } + if INTEL_SKL_KBL_CFL_IDS.contains(&device_id) { + return "8 (Skylake/Kaby Lake/Coffee Lake)"; + } + if INTEL_CNL_ICL_TGL_IDS.contains(&device_id) { + return "9 (Cannon/Ice/Tiger/Rocket Lake)"; + } + "Gen4-Gen11 (unsupported)" +} + +const INTEL_I965G_IDS: &[u16] = &[ + 0x2972, 0x2982, 0x2992, 0x29A2, 0x29B2, 0x29C2, 0x29D2, 0x2A02, 0x2A12, 0x2A42, 0x2E02, 0x2E12, + 0x2E22, 0x2E32, 0x2E42, 0x2E92, 0xA001, 0xA011, +]; +const INTEL_ILK_IDS: &[u16] = &[0x0042, 0x0046]; +const INTEL_SNB_IDS: &[u16] = &[0x0102, 0x0106, 0x010A, 0x0112, 0x0116, 0x0122, 0x0126]; +const INTEL_IVB_HSW_BDW_IDS: &[u16] = &[ + 0x0152, 0x0156, 0x015A, 0x0162, 0x0166, 0x016A, 0x0402, 0x0406, 0x040A, 0x040B, 0x040E, 0x0412, + 0x0416, 0x041A, 0x041B, 0x041E, 0x0422, 0x0426, 0x042A, 0x042B, 0x042E, 0x0A02, 0x0A06, 0x0A0A, + 0x0A0B, 0x0A0E, 0x0A12, 0x0A16, 0x0A1A, 0x0A1B, 0x0A1E, 0x0A22, 0x0A26, 0x0A2A, 0x0A2B, 0x0A2E, + 0x0D02, 0x0D06, 0x0D0A, 0x0D0B, 0x0D0E, 0x0D12, 0x0D16, 0x0D1A, 0x0D1B, 0x0D1E, 0x0D22, 0x0D26, + 0x0D2A, 0x0D2B, 0x0D2E, 0x1602, 0x1606, 0x160A, 0x160B, 0x160D, 0x160E, 0x1612, 0x1616, 0x161A, + 0x161B, 0x161D, 0x161E, 0x1622, 0x1626, 0x162A, 0x162B, 0x162D, 0x162E, 0x22B0, 0x22B1, 0x22B2, + 0x22B3, +]; +const INTEL_SKL_KBL_CFL_IDS: &[u16] = &[ + 0x1902, 0x1906, 0x190A, 0x190B, 0x190E, 0x1912, 0x1916, 0x1917, 0x191A, 0x191B, 0x191D, 0x191E, + 0x1921, 0x1923, 0x1926, 0x1927, 0x192A, 0x192B, 0x192D, 0x1932, 0x193A, 0x193B, 0x193D, 0x3E90, + 0x3E91, 0x3E92, 0x3E93, 0x3E94, 0x3E96, 0x3E98, 0x3E99, 0x3E9A, 0x3E9B, 0x3E9C, 0x3EA0, 0x3EA1, + 0x3EA2, 0x3EA3, 0x3EA4, 0x3EA5, 0x3EA6, 0x3EA7, 0x3EA8, 0x3EA9, 0x5902, 0x5906, 0x5908, 0x590A, + 0x590B, 0x590E, 0x5912, 0x5913, 0x5915, 0x5916, 0x5917, 0x591A, 0x591B, 0x591C, 0x591D, 0x591E, + 0x5921, 0x5923, 0x5926, 0x5927, 0x593B, 0x87C0, 0x87CA, 0x9B21, 0x9B41, 0x9BA2, 0x9BA4, 0x9BA5, + 0x9BA8, 0x9BAA, 0x9BAC, 0x9BC2, 0x9BC4, 0x9BC5, 0x9BC6, 0x9BC8, 0x9BCA, 0x9BCC, 0x9BE6, 0x9BF6, +]; +const INTEL_CNL_ICL_TGL_IDS: &[u16] = &[ + 0x4541, 0x4551, 0x4555, 0x4557, 0x4570, 0x4571, 0x4905, 0x4906, 0x4907, 0x4908, 0x4909, 0x4C80, + 0x4C8A, 0x4C8B, 0x4C8C, 0x4C90, 0x4C9A, 0x4E51, 0x4E55, 0x4E57, 0x4E61, 0x4E71, 0x5A40, 0x5A41, + 0x5A42, 0x5A44, 0x5A49, 0x5A4A, 0x5A4C, 0x5A50, 0x5A51, 0x5A52, 0x5A54, 0x5A59, 0x5A5A, 0x5A5C, + 0x8A50, 0x8A51, 0x8A52, 0x8A53, 0x8A54, 0x8A56, 0x8A57, 0x8A58, 0x8A59, 0x8A5A, 0x8A5B, 0x8A5C, + 0x8A5D, 0x8A70, 0x8A71, 0x9A40, 0x9A49, 0x9A59, 0x9A60, 0x9A68, 0x9A70, 0x9A78, 0x9AC0, 0x9AC9, + 0x9AD9, 0x9AF8, +]; + +impl DriverRegistry { + pub fn probe( + info: PciDeviceInfo, + firmware: HashMap>, + ) -> Result> { + let full = if info.bars.is_empty() { + let mut device = PciDevice::open_location(&info.location) + .map_err(|e| DriverError::Pci(format!("open PCI device failed: {e}")))?; + device + .full_info() + .map_err(|e| DriverError::Pci(format!("read PCI device info failed: {e}")))? + } else { + info + }; + + let quirks = full.quirks(); + if !quirks.is_empty() { + info!( + "redox-drm: quirks for {:#06x}:{:#06x}: {:?}", + full.vendor_id, full.device_id, quirks + ); + } + + if quirks.contains(PciQuirkFlags::DISABLE_ACCEL) { + return Err(DriverError::Pci(format!( + "device {:#06x}:{:#06x} at {} has DISABLE_ACCEL quirk — skipping probe", + full.vendor_id, full.device_id, full.location + ))); + } + + match full.vendor_id { + PCI_VENDOR_ID_AMD => { + let driver = amd::AmdDriver::new(full, firmware)?; + Ok(Arc::new(driver)) + } + PCI_VENDOR_ID_INTEL => { + if !is_supported_intel_generation(full.device_id) { + return Err(DriverError::Pci(format!( + "Intel GPU {:#06x} at {} is Gen{} — Gen8+ (Skylake and newer) are supported; Gen4-Gen7 require different display hardware init and are not yet supported", + full.device_id, full.location, intel_generation_name(full.device_id) + ))); + } + let driver = intel::IntelDriver::new(full, firmware)?; + Ok(Arc::new(driver)) + } + 0x1AF4 => { + let driver = virtio::VirtioDriver::new(full, firmware)?; + Ok(Arc::new(driver)) + } + _ => Err(DriverError::Pci(format!( + "unsupported GPU vendor {:#06x} at {}", + full.vendor_id, full.location + ))), + } + } +} 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 new file mode 100644 index 0000000000..9eca0c168d --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/drivers/virtio/mod.rs @@ -0,0 +1,248 @@ +use std::collections::HashMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; + +use log::{info, warn}; +use redox_driver_sys::memory::MmioRegion; +use redox_driver_sys::pci::{PciBarInfo, PciDeviceInfo}; + +use crate::driver::{DriverError, DriverEvent, GpuDriver, Result}; +use crate::drivers::interrupt::InterruptHandle; +use crate::gem::{GemHandle, GemManager}; +use crate::kms::connector::{synthetic_edid, Connector}; +use crate::kms::crtc::Crtc; +use crate::kms::{ConnectorInfo, ConnectorStatus, ConnectorType, ModeInfo}; + +pub struct VirtioDriver { + info: PciDeviceInfo, + _mmio: MmioRegion, + irq_handle: Mutex>, + width: u32, + height: u32, + gem: Mutex, + connectors: Mutex>, + crtcs: Mutex>, + vblank_count: AtomicU64, +} + +fn find_fb_bar(info: &PciDeviceInfo) -> Result { + info.bars + .iter() + .find(|bar| bar.addr != 0 && bar.size > 0) + .cloned() + .ok_or_else(|| DriverError::Pci("VirtIO GPU has no valid framebuffer BAR".into())) +} + +fn map_bar(bar: &PciBarInfo, name: &str) -> Result { + MmioRegion::map( + bar.addr, + bar.size as usize, + redox_driver_sys::memory::CacheType::DeviceMemory, + redox_driver_sys::memory::MmioProt::READ_WRITE, + ) + .map_err(|e| DriverError::Mmio(format!("failed to map {name}: {e}"))) +} + +impl VirtioDriver { + pub fn new(info: PciDeviceInfo, _firmware: HashMap>) -> Result { + if info.vendor_id != 0x1AF4 { + return Err(DriverError::Pci(format!( + "device {} is not a VirtIO GPU (vendor {:#06x})", + info.location, info.vendor_id + ))); + } + + let fb_bar = find_fb_bar(&info)?; + let _mmio = map_bar(&fb_bar, "VirtIO FB BAR")?; + + info!( + "redox-drm: VirtIO GPU at {}: {} MiB BAR at {:#x}", + info.location, + fb_bar.size / 1024 / 1024, + fb_bar.addr, + ); + + Ok(Self { + info, + _mmio, + irq_handle: Mutex::new(None), + width: 1280, + height: 720, + gem: Mutex::new(GemManager::new()), + connectors: Mutex::new(Vec::new()), + crtcs: Mutex::new(Vec::new()), + vblank_count: AtomicU64::new(0), + }) + } + + fn refresh_connectors(&self) -> Result> { + let mode = ModeInfo { + name: String::from("1280x720"), + clock: 0, + hdisplay: self.width as u16, + hsync_start: (self.width + 16) as u16, + hsync_end: (self.width + 48) as u16, + htotal: (self.width + 160) as u16, + vdisplay: self.height as u16, + vsync_start: (self.height + 3) as u16, + vsync_end: (self.height + 6) as u16, + vtotal: (self.height + 30) as u16, + hskew: 0, + vscan: 0, + vrefresh: 60, + type_: 0, + flags: 0, + }; + let info = ConnectorInfo { + id: 1, + connector_type: ConnectorType::Unknown, + connector_type_id: 1, + connection: ConnectorStatus::Connected, + mm_width: 0, + mm_height: 0, + modes: vec![mode], + encoder_id: 0, + }; + let mut connectors = self + .connectors + .lock() + .map_err(|_| DriverError::Initialization("connector lock poisoned".into()))?; + connectors.clear(); + let result = info.clone(); + connectors.push(Connector { + edid: synthetic_edid(), + info, + }); + let mut crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("crtc lock poisoned".into()))?; + crtcs.clear(); + crtcs.push(Crtc::new(1)); + Ok(vec![result]) + } + + fn cached_connectors(&self) -> Vec { + self.connectors + .lock() + .ok() + .map(|c| c.iter().map(|c| c.info.clone()).collect()) + .unwrap_or_default() + } +} + +impl GpuDriver for VirtioDriver { + fn driver_name(&self) -> &str { + "virtio-gpu-redox" + } + fn driver_desc(&self) -> &str { + "VirtIO GPU DRM/KMS backend for QEMU" + } + fn driver_date(&self) -> &str { + "2026-04-27" + } + + fn detect_connectors(&self) -> Vec { + match self.refresh_connectors() { + Ok(connectors) => connectors, + Err(error) => { + warn!("redox-drm: VirtIO connector refresh failed: {}", error); + self.cached_connectors() + } + } + } + + fn get_modes(&self, connector_id: u32) -> Vec { + self.detect_connectors() + .into_iter() + .find(|c| c.id == connector_id) + .map(|c| c.modes) + .unwrap_or_default() + } + + fn set_crtc( + &self, + crtc_id: u32, + fb_handle: u32, + connectors: &[u32], + mode: &ModeInfo, + ) -> Result<()> { + let mut crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("crtc lock poisoned".into()))?; + let crtc = crtcs + .iter_mut() + .find(|c| c.id == crtc_id) + .ok_or_else(|| DriverError::NotFound(format!("unknown CRTC {crtc_id}")))?; + crtc.program(fb_handle, connectors, mode) + } + + fn page_flip(&self, crtc_id: u32, _fb_handle: u32, _flags: u32) -> Result { + let crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("crtc lock poisoned".into()))?; + if !crtcs.iter().any(|c| c.id == crtc_id) { + return Err(DriverError::NotFound(format!("unknown CRTC {crtc_id}"))); + } + self.vblank_count.fetch_add(1, Ordering::SeqCst); + Ok(self.vblank_count.load(Ordering::SeqCst)) + } + + fn get_vblank(&self, crtc_id: u32) -> Result { + let crtcs = self + .crtcs + .lock() + .map_err(|_| DriverError::Initialization("crtc lock poisoned".into()))?; + if !crtcs.iter().any(|c| c.id == crtc_id) { + return Err(DriverError::NotFound(format!("unknown CRTC {crtc_id}"))); + } + Ok(self.vblank_count.load(Ordering::SeqCst)) + } + + fn gem_create(&self, size: u64) -> Result { + self.gem + .lock() + .map_err(|_| DriverError::Buffer("VirtIO GEM poisoned".into()))? + .create(size) + } + + fn gem_close(&self, handle: GemHandle) -> Result<()> { + self.gem + .lock() + .map_err(|_| DriverError::Buffer("VirtIO GEM poisoned".into()))? + .close(handle) + } + + fn gem_mmap(&self, handle: GemHandle) -> Result { + self.gem + .lock() + .map_err(|_| DriverError::Buffer("VirtIO GEM poisoned".into()))? + .mmap(handle) + } + + fn gem_size(&self, handle: GemHandle) -> Result { + self.gem + .lock() + .map_err(|_| DriverError::Buffer("VirtIO GEM poisoned".into()))? + .object(handle) + .map(|o| o.size) + } + + fn get_edid(&self, connector_id: u32) -> Vec { + match self.connectors.lock() { + Ok(connectors) => connectors + .iter() + .find(|c| c.info.id == connector_id) + .map(|c| c.edid.clone()) + .unwrap_or_else(synthetic_edid), + Err(_) => synthetic_edid(), + } + } + + fn handle_irq(&self) -> Result> { + self.vblank_count.fetch_add(1, Ordering::SeqCst); + Ok(None) + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/gem.rs b/local/recipes/gpu/redox-drm/source/src/gem.rs new file mode 100644 index 0000000000..06d1d02b6f --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/gem.rs @@ -0,0 +1,162 @@ +use std::collections::BTreeMap; + +use log::debug; +use redox_driver_sys::dma::DmaBuffer; + +use crate::driver::{DriverError, Result}; + +pub type GemHandle = u32; + +const MAX_GEM_BYTES: u64 = 256 * 1024 * 1024; + +#[derive(Clone, Debug)] +pub struct GemObject { + #[allow(dead_code)] + pub handle: GemHandle, + #[allow(dead_code)] + pub size: u64, + pub phys_addr: usize, + pub virt_addr: usize, + pub gpu_addr: Option, +} + +struct GemAllocation { + object: GemObject, + #[allow(dead_code)] + dma: DmaBuffer, +} + +pub struct GemManager { + next_handle: GemHandle, + objects: BTreeMap, +} + +impl GemManager { + pub fn new() -> Self { + Self { + next_handle: 1, + objects: BTreeMap::new(), + } + } + + pub fn create(&mut self, size: u64) -> Result { + if size == 0 { + return Err(DriverError::InvalidArgument( + "GEM create size must be non-zero", + )); + } + if size > MAX_GEM_BYTES { + return Err(DriverError::InvalidArgument( + "GEM create size exceeds the trusted shared-core limit", + )); + } + + let handle = self.next_handle; + self.next_handle = self.next_handle.saturating_add(1); + + let dma = DmaBuffer::allocate(size as usize, 4096) + .map_err(|e| DriverError::Buffer(format!("DMA allocation failed: {e}")))?; + if !dma.is_physically_contiguous() { + debug!( + "redox-drm: GEM handle {} allocated without physically contiguous backing", + handle + ); + } + + let object = GemObject { + handle, + size, + phys_addr: dma.physical_address(), + virt_addr: dma.as_ptr() as usize, + gpu_addr: None, + }; + + debug!( + "redox-drm: created GEM handle {} size={} phys={:#x} virt={:#x}", + handle, size, object.phys_addr, object.virt_addr + ); + + self.objects.insert(handle, GemAllocation { object, dma }); + Ok(handle) + } + + pub fn close(&mut self, handle: GemHandle) -> Result<()> { + if self.objects.remove(&handle).is_none() { + return Err(DriverError::NotFound(format!( + "unknown GEM handle {handle}" + ))); + } + Ok(()) + } + + pub fn mmap(&self, handle: GemHandle) -> Result { + let allocation = self + .objects + .get(&handle) + .ok_or_else(|| DriverError::NotFound(format!("unknown GEM handle {handle}")))?; + Ok(allocation.object.virt_addr) + } + + pub fn object(&self, handle: GemHandle) -> Result<&GemObject> { + self.objects + .get(&handle) + .map(|allocation| &allocation.object) + .ok_or_else(|| DriverError::NotFound(format!("unknown GEM handle {handle}"))) + } + + pub fn phys_addr(&self, handle: GemHandle) -> Result { + Ok(self.object(handle)?.phys_addr) + } + + pub fn set_gpu_addr(&mut self, handle: GemHandle, gpu_addr: u64) -> Result<()> { + let allocation = self + .objects + .get_mut(&handle) + .ok_or_else(|| DriverError::NotFound(format!("unknown GEM handle {handle}")))?; + allocation.object.gpu_addr = Some(gpu_addr); + Ok(()) + } + + pub fn gpu_addr(&self, handle: GemHandle) -> Result> { + Ok(self.object(handle)?.gpu_addr) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_and_object_exists() { + let mut mgr = GemManager::new(); + let h = mgr.create(4096).expect("create should succeed"); + let obj = mgr.object(h).expect("object should exist after create"); + assert_eq!(obj.handle, h); + assert_eq!(obj.size, 4096); + } + + #[test] + fn close_removes_object() { + let mut mgr = GemManager::new(); + let h = mgr.create(4096).expect("create should succeed"); + mgr.close(h).expect("close should succeed"); + assert!(mgr.object(h).is_err(), "object should be gone after close"); + } + + #[test] + fn double_close_returns_error() { + let mut mgr = GemManager::new(); + let h = mgr.create(4096).expect("create should succeed"); + mgr.close(h).expect("first close should succeed"); + assert!(mgr.close(h).is_err(), "second close should fail"); + } + + #[test] + fn object_by_invalid_handle_returns_error() { + let mgr = GemManager::new(); + assert!( + mgr.object(99999).is_err(), + "querying a non-existent handle should fail" + ); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/kms/connector.rs b/local/recipes/gpu/redox-drm/source/src/kms/connector.rs new file mode 100644 index 0000000000..e3d61b010b --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/kms/connector.rs @@ -0,0 +1,88 @@ +use crate::kms::{ConnectorInfo, ConnectorStatus, ConnectorType, ModeInfo}; + +#[derive(Clone, Debug)] +pub struct Connector { + pub info: ConnectorInfo, + #[allow(dead_code)] + pub edid: Vec, +} + +impl Connector { + pub fn synthetic_displayport(id: u32, encoder_id: u32) -> Self { + let edid = synthetic_edid(); + let modes = ModeInfo::from_edid(&edid); + + Self { + info: ConnectorInfo { + id, + connector_type: ConnectorType::DisplayPort, + connector_type_id: 1, + connection: ConnectorStatus::Connected, + mm_width: 600, + mm_height: 340, + encoder_id, + modes: if modes.is_empty() { + vec![ModeInfo::default_1080p()] + } else { + modes + }, + }, + edid, + } + } +} + +pub fn synthetic_edid() -> Vec { + vec![ + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x4c, 0x2d, 0xfa, 0x12, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x1e, 0x01, 0x04, 0xa5, 0x3c, 0x22, 0x78, 0x3a, 0xee, 0x95, 0xa3, 0x54, 0x4c, + 0x99, 0x26, 0x0f, 0x50, 0x54, 0xbf, 0xef, 0x80, 0x71, 0x4f, 0x81, 0x80, 0x81, 0x40, 0x81, + 0xc0, 0x95, 0x00, 0xa9, 0xc0, 0xb3, 0x00, 0xd1, 0xc0, 0x02, 0x3a, 0x80, 0x18, 0x71, 0x38, + 0x2d, 0x40, 0x58, 0x2c, 0x45, 0x00, 0x55, 0x50, 0x21, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, + 0xfd, 0x00, 0x32, 0x4c, 0x1e, 0x53, 0x11, 0x00, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x00, 0x00, 0x00, 0xfc, 0x00, 0x53, 0x79, 0x6e, 0x74, 0x68, 0x65, 0x74, 0x69, 0x63, 0x20, + 0x44, 0x50, 0x0a, 0x20, 0x20, 0x00, 0xa7, + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn synthetic_displayport_has_correct_fields() { + let conn = Connector::synthetic_displayport(5, 10); + assert_eq!(conn.info.id, 5); + assert_eq!(conn.info.encoder_id, 10); + assert_eq!(conn.info.connector_type, ConnectorType::DisplayPort); + assert_eq!(conn.info.connection, ConnectorStatus::Connected); + assert!( + !conn.info.modes.is_empty(), + "synthetic DisplayPort should have modes" + ); + } + + #[test] + fn synthetic_displayport_modes_have_valid_dimensions() { + let conn = Connector::synthetic_displayport(1, 1); + for mode in &conn.info.modes { + assert!(mode.hdisplay > 0, "mode hdisplay should be > 0"); + assert!(mode.vdisplay > 0, "mode vdisplay should be > 0"); + assert!(mode.vrefresh > 0, "mode vrefresh should be > 0"); + assert!(mode.clock > 0, "mode clock should be > 0"); + } + } + + #[test] + fn synthetic_edid_returns_exactly_112_bytes() { + let edid = synthetic_edid(); + assert_eq!(edid.len(), 112); + } + + #[test] + fn synthetic_edid_has_valid_header() { + let edid = synthetic_edid(); + let header: [u8; 8] = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00]; + assert_eq!(&edid[0..8], &header, "EDID header should be valid"); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/kms/crtc.rs b/local/recipes/gpu/redox-drm/source/src/kms/crtc.rs new file mode 100644 index 0000000000..dc47476331 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/kms/crtc.rs @@ -0,0 +1,107 @@ +use crate::driver::{DriverError, Result}; +use crate::kms::ModeInfo; + +#[derive(Clone, Debug)] +pub struct Crtc { + pub id: u32, + pub current_fb: u32, + pub connectors: Vec, + pub mode: Option, + #[allow(dead_code)] + pub x: u32, + #[allow(dead_code)] + pub y: u32, + #[allow(dead_code)] + pub gamma_size: u32, +} + +impl Crtc { + pub fn new(id: u32) -> Self { + Self { + id, + current_fb: 0, + connectors: Vec::new(), + mode: None, + x: 0, + y: 0, + gamma_size: 256, + } + } + + pub fn program(&mut self, fb_handle: u32, connectors: &[u32], mode: &ModeInfo) -> Result<()> { + if connectors.is_empty() { + return Err(DriverError::InvalidArgument( + "set_crtc requires at least one connector", + )); + } + + self.current_fb = fb_handle; + self.connectors = connectors.to_vec(); + self.mode = Some(mode.clone()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_mode() -> ModeInfo { + ModeInfo::default_1080p() + } + + #[test] + fn new_initializes_correctly() { + let crtc = Crtc::new(42); + assert_eq!(crtc.id, 42); + assert_eq!(crtc.current_fb, 0); + assert!(crtc.connectors.is_empty()); + assert!(crtc.mode.is_none()); + assert_eq!(crtc.gamma_size, 256); + } + + #[test] + fn program_sets_fb_connectors_and_mode() { + let mut crtc = Crtc::new(1); + let mode = test_mode(); + let result = crtc.program(99, &[10, 20], &mode); + + assert!(result.is_ok()); + assert_eq!(crtc.current_fb, 99); + assert_eq!(crtc.connectors, vec![10, 20]); + assert!(crtc.mode.is_some()); + let programmed_mode = crtc.mode.unwrap(); + assert_eq!(programmed_mode.hdisplay, 1920); + assert_eq!(programmed_mode.vdisplay, 1080); + } + + #[test] + fn program_empty_connectors_returns_invalid_argument() { + let mut crtc = Crtc::new(1); + let mode = test_mode(); + let result = crtc.program(99, &[], &mode); + + assert!(result.is_err()); + match result.unwrap_err() { + DriverError::InvalidArgument(msg) => { + assert!(msg.contains("connector")); + } + other => panic!("expected InvalidArgument, got {:?}", other), + } + // State should be unchanged + assert_eq!(crtc.current_fb, 0); + assert!(crtc.connectors.is_empty()); + assert!(crtc.mode.is_none()); + } + + #[test] + fn program_multiple_connectors_accepted() { + let mut crtc = Crtc::new(1); + let mode = test_mode(); + let result = crtc.program(50, &[1, 2, 3, 4, 5], &mode); + + assert!(result.is_ok()); + assert_eq!(crtc.connectors.len(), 5); + assert_eq!(crtc.connectors, vec![1, 2, 3, 4, 5]); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/kms/encoder.rs b/local/recipes/gpu/redox-drm/source/src/kms/encoder.rs new file mode 100644 index 0000000000..8b3f4ad5b4 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/kms/encoder.rs @@ -0,0 +1,21 @@ +use crate::kms::EncoderInfo; + +#[derive(Clone, Debug)] +pub struct Encoder { + #[allow(dead_code)] + pub info: EncoderInfo, +} + +impl Encoder { + pub fn new(id: u32, crtc_id: u32) -> Self { + Self { + info: EncoderInfo { + id, + encoder_type: 0, + crtc_id, + possible_crtcs: 1, + possible_clones: 0, + }, + } + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/kms/mod.rs b/local/recipes/gpu/redox-drm/source/src/kms/mod.rs new file mode 100644 index 0000000000..bc0bc40679 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/kms/mod.rs @@ -0,0 +1,278 @@ +pub mod connector; +pub mod crtc; +pub mod encoder; +pub mod plane; + +#[derive(Clone, Debug)] +pub struct ModeInfo { + pub clock: u32, + pub hdisplay: u16, + pub hsync_start: u16, + pub hsync_end: u16, + pub htotal: u16, + pub hskew: u16, + pub vdisplay: u16, + pub vsync_start: u16, + pub vsync_end: u16, + pub vtotal: u16, + pub vscan: u16, + pub vrefresh: u32, + pub flags: u32, + pub type_: u32, + pub name: String, +} + +impl ModeInfo { + pub fn default_1080p() -> Self { + Self { + clock: 148_500, + hdisplay: 1920, + hsync_start: 2008, + hsync_end: 2052, + htotal: 2200, + hskew: 0, + vdisplay: 1080, + vsync_start: 1084, + vsync_end: 1089, + vtotal: 1125, + vscan: 0, + vrefresh: 60, + flags: 0, + type_: 0, + name: "1920x1080@60".to_string(), + } + } + + pub fn from_edid(edid: &[u8]) -> Vec { + const EDID_HEADER: [u8; 8] = [0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00]; + + if edid.len() < 128 || edid.get(0..8) != Some(&EDID_HEADER) { + return Vec::new(); + } + + let mut modes = Vec::new(); + for descriptor in edid[54..126].chunks_exact(18) { + let pixel_clock = u16::from_le_bytes([descriptor[0], descriptor[1]]) as u32; + if pixel_clock == 0 { + continue; + } + + let hdisplay = descriptor[2] as u16 | (((descriptor[4] >> 4) as u16) << 8); + let hblank = descriptor[3] as u16 | (((descriptor[4] & 0x0f) as u16) << 8); + let vdisplay = descriptor[5] as u16 | (((descriptor[7] >> 4) as u16) << 8); + let vblank = descriptor[6] as u16 | (((descriptor[7] & 0x0f) as u16) << 8); + let hsync_offset = + descriptor[8] as u16 | ((((descriptor[11] >> 6) & 0x03) as u16) << 8); + let hsync_width = descriptor[9] as u16 | ((((descriptor[11] >> 4) & 0x03) as u16) << 8); + let vsync_offset = + ((descriptor[10] >> 4) as u16) | ((((descriptor[11] >> 2) & 0x03) as u16) << 4); + let vsync_width = + (descriptor[10] & 0x0f) as u16 | (((descriptor[11] & 0x03) as u16) << 4); + + if hdisplay == 0 || vdisplay == 0 { + continue; + } + + let htotal = hdisplay.saturating_add(hblank); + let vtotal = vdisplay.saturating_add(vblank); + let clock = pixel_clock.saturating_mul(10); + let vrefresh = if htotal != 0 && vtotal != 0 { + clock.saturating_mul(1000) / (htotal as u32).saturating_mul(vtotal as u32) + } else { + 0 + }; + + modes.push(Self { + clock, + hdisplay, + hsync_start: hdisplay.saturating_add(hsync_offset), + hsync_end: hdisplay + .saturating_add(hsync_offset) + .saturating_add(hsync_width), + htotal, + hskew: 0, + vdisplay, + vsync_start: vdisplay.saturating_add(vsync_offset), + vsync_end: vdisplay + .saturating_add(vsync_offset) + .saturating_add(vsync_width), + vtotal, + vscan: 0, + vrefresh, + flags: if (descriptor[17] & 0x80) != 0 { 1 } else { 0 }, + type_: 0, + name: format!("{}x{}@{}", hdisplay, vdisplay, vrefresh), + }); + } + + modes + } +} + +#[derive(Clone, Debug)] +pub struct ConnectorInfo { + pub id: u32, + pub connector_type: ConnectorType, + #[allow(dead_code)] + pub connector_type_id: u32, + pub connection: ConnectorStatus, + pub mm_width: u32, + pub mm_height: u32, + pub encoder_id: u32, + pub modes: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConnectorType { + Unknown, + VGA, + DVII, + DVID, + DVIA, + #[allow(dead_code)] + Composite, + #[allow(dead_code)] + SVideo, + #[allow(dead_code)] + LVDS, + #[allow(dead_code)] + Component, + #[allow(dead_code)] + NinePinDIN, + DisplayPort, + HDMIA, + #[allow(dead_code)] + HDMIB, + #[allow(dead_code)] + TV, + EDP, + Virtual, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConnectorStatus { + Connected, + Disconnected, + Unknown, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct CrtcInfo { + pub id: u32, + pub fb_id: u32, + pub x: u32, + pub y: u32, + pub gamma_size: u32, + pub mode: Option, +} + +#[derive(Clone, Debug)] +pub struct EncoderInfo { + #[allow(dead_code)] + pub id: u32, + #[allow(dead_code)] + pub encoder_type: u32, + #[allow(dead_code)] + pub crtc_id: u32, + #[allow(dead_code)] + pub possible_crtcs: u32, + #[allow(dead_code)] + pub possible_clones: u32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_1080p_has_correct_values() { + let mode = ModeInfo::default_1080p(); + assert_eq!(mode.hdisplay, 1920); + assert_eq!(mode.vdisplay, 1080); + assert_eq!(mode.vrefresh, 60); + assert_eq!(mode.clock, 148_500); + assert_eq!(mode.name, "1920x1080@60"); + assert_eq!(mode.htotal, 2200); + assert_eq!(mode.vtotal, 1125); + } + + #[test] + fn from_edid_synthetic_edid_too_short_returns_empty() { + let edid = super::connector::synthetic_edid(); + assert!(edid.len() < 128, "synthetic EDID is shorter than 128 bytes"); + let modes = ModeInfo::from_edid(&edid); + assert!( + modes.is_empty(), + "EDID shorter than 128 bytes should produce no modes" + ); + } + + #[test] + fn from_edid_empty_input_returns_empty() { + let modes = ModeInfo::from_edid(&[]); + assert!(modes.is_empty()); + } + + #[test] + fn from_edid_short_input_returns_empty() { + let modes = ModeInfo::from_edid(&[0u8; 64]); + assert!(modes.is_empty()); + } + + #[test] + fn from_edid_invalid_header_returns_empty() { + let mut data = vec![0u8; 128]; + data[0] = 0x01; + let modes = ModeInfo::from_edid(&data); + assert!(modes.is_empty()); + } + + #[test] + fn from_edid_parsed_modes_have_nonzero_dimensions() { + let edid = super::connector::synthetic_edid(); + let modes = ModeInfo::from_edid(&edid); + for mode in &modes { + assert_ne!(mode.hdisplay, 0, "hdisplay should not be zero"); + assert_ne!(mode.vdisplay, 0, "vdisplay should not be zero"); + } + } + + #[test] + fn from_edid_parsed_modes_have_correct_name_format() { + let edid = super::connector::synthetic_edid(); + let modes = ModeInfo::from_edid(&edid); + for mode in &modes { + let parts: Vec<&str> = mode.name.split('@').collect(); + assert_eq!( + parts.len(), + 2, + "name should contain exactly one '@': {}", + mode.name + ); + let dims: Vec<&str> = parts[0].split('x').collect(); + assert_eq!(dims.len(), 2, "name prefix should be WxH: {}", mode.name); + let w: u16 = dims[0].parse().expect("width should be numeric"); + let h: u16 = dims[1].parse().expect("height should be numeric"); + assert_eq!(w, mode.hdisplay); + assert_eq!(h, mode.vdisplay); + let refresh: u32 = parts[1].parse().expect("refresh should be numeric"); + assert_eq!(refresh, mode.vrefresh); + } + } + + #[test] + fn from_edid_with_valid_header_but_zero_pixel_clock_skips_descriptor() { + let mut data = vec![0u8; 128]; + data[0] = 0x00; + data[1] = 0xFF; + data[2] = 0xFF; + data[3] = 0xFF; + data[4] = 0xFF; + data[5] = 0xFF; + data[6] = 0xFF; + data[7] = 0x00; + let modes = ModeInfo::from_edid(&data); + assert!(modes.is_empty()); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/kms/plane.rs b/local/recipes/gpu/redox-drm/source/src/kms/plane.rs new file mode 100644 index 0000000000..ef4b31a69c --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/kms/plane.rs @@ -0,0 +1,91 @@ +use crate::driver::{DriverError, Result}; + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PlaneKind { + Primary, + Cursor, +} + +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub struct Plane { + pub id: u32, + pub kind: PlaneKind, + pub fb_handle: Option, + pub crtc_id: Option, +} + +impl Plane { + #[allow(dead_code)] + pub fn new(id: u32, kind: PlaneKind) -> Self { + Self { + id, + kind, + fb_handle: None, + crtc_id: None, + } + } + + #[allow(dead_code)] + pub fn attach(&mut self, crtc_id: u32, fb_handle: u32) -> Result<()> { + if fb_handle == 0 { + return Err(DriverError::InvalidArgument( + "plane attach requires a framebuffer handle", + )); + } + + self.crtc_id = Some(crtc_id); + self.fb_handle = Some(fb_handle); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_primary_initializes_correctly() { + let plane = Plane::new(7, PlaneKind::Primary); + assert_eq!(plane.id, 7); + assert_eq!(plane.kind, PlaneKind::Primary); + assert_eq!(plane.fb_handle, None); + assert_eq!(plane.crtc_id, None); + } + + #[test] + fn new_cursor_initializes_correctly() { + let plane = Plane::new(3, PlaneKind::Cursor); + assert_eq!(plane.id, 3); + assert_eq!(plane.kind, PlaneKind::Cursor); + assert!(plane.fb_handle.is_none()); + assert!(plane.crtc_id.is_none()); + } + + #[test] + fn attach_sets_crtc_id_and_fb_handle() { + let mut plane = Plane::new(1, PlaneKind::Primary); + let result = plane.attach(10, 20); + + assert!(result.is_ok()); + assert_eq!(plane.crtc_id, Some(10)); + assert_eq!(plane.fb_handle, Some(20)); + } + + #[test] + fn attach_zero_fb_handle_returns_invalid_argument() { + let mut plane = Plane::new(1, PlaneKind::Primary); + let result = plane.attach(10, 0); + + assert!(result.is_err()); + match result.unwrap_err() { + DriverError::InvalidArgument(msg) => { + assert!(msg.contains("framebuffer")); + } + other => panic!("expected InvalidArgument, got {:?}", other), + } + assert_eq!(plane.crtc_id, None); + assert_eq!(plane.fb_handle, None); + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/main.rs b/local/recipes/gpu/redox-drm/source/src/main.rs new file mode 100644 index 0000000000..2f5840eb98 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/main.rs @@ -0,0 +1,707 @@ +#![allow(dead_code)] + +mod driver; +mod drivers; +mod gem; +mod kms; +mod scheme; + +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::io::Read; +use std::process; + +use std::sync::mpsc; +use std::sync::{Arc, Mutex}; + +use log::{error, info, LevelFilter, Metadata, Record}; +use redox_driver_sys::pci::{ + enumerate_pci_class, PciDevice, PciDeviceInfo, PciLocation, PCI_CLASS_DISPLAY, + PCI_VENDOR_ID_AMD, PCI_VENDOR_ID_INTEL, +}; +use redox_driver_sys::pcid_client::PcidClient; +use redox_driver_sys::quirks::PciQuirkFlags; +use redox_scheme::scheme::register_sync_scheme; +use redox_scheme::wrappers::ReadinessBased; +use redox_scheme::Socket; + +use crate::driver::{DriverError, DriverEvent, GpuDriver, Result}; +use crate::drivers::DriverRegistry; +use crate::scheme::DrmScheme; + +const MAX_FIRMWARE_BLOB_BYTES: u64 = 64 * 1024 * 1024; + +struct StderrLogger { + level: LevelFilter, +} + +impl log::Log for StderrLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + eprintln!("[{}] {}", record.level(), record.args()); + } + } + + fn flush(&self) {} +} + +fn init_logging(level: LevelFilter) { + let logger = Box::leak(Box::new(StderrLogger { level })); + if log::set_logger(logger).is_err() { + return; + } + log::set_max_level(level); +} + +fn run(daemon: daemon::Daemon) -> Result<()> { + let info = select_gpu_from_args()?; + verify_supported_gpu(&info)?; + + let firmware = FirmwareCache::load_for_device(&info)?; + + let driver = DriverRegistry::probe(info.clone(), firmware.into_blobs())?; + info!( + "redox-drm: initialized driver {} ({}) for {}", + driver.driver_name(), + driver.driver_desc(), + info.location + ); + + let (event_tx, event_rx) = mpsc::sync_channel::(8); + + let irq_driver: Arc = driver.clone(); + std::thread::spawn(move || loop { + match irq_driver.handle_irq() { + Ok(Some(event)) => { + if event_tx.send(event).is_err() { + error!("redox-drm: event consumer dropped, stopping IRQ event thread"); + break; + } + } + Ok(None) => {} + Err(e) => { + error!("redox-drm: IRQ handler error: {}", e); + } + } + std::thread::sleep(std::time::Duration::from_millis(16)); + }); + + let drm_scheme = Arc::new(Mutex::new(DrmScheme::new(driver))); + let event_scheme = drm_scheme.clone(); + + std::thread::spawn(move || loop { + if let Ok(event) = event_rx.recv() { + if let Ok(mut scheme) = event_scheme.lock() { + scheme.handle_driver_event(event); + } + } + }); + + let socket = create_drm_socket()?; + { + let mut scheme = drm_scheme.lock().map_err(|_| { + DriverError::Initialization("DRM scheme state poisoned during registration".to_string()) + })?; + match register_sync_scheme(&socket, "drm", &mut *scheme) { + Ok(()) => {} + Err(e) if e.errno == syscall::error::EEXIST => { + info!("redox-drm: scheme:drm already registered; another instance is active"); + daemon.ready(); + return Ok(()); + } + Err(e) => { + return Err(DriverError::Initialization(format!( + "failed to publish drm scheme: {e}" + ))); + } + } + } + info!("redox-drm: registered scheme:drm"); + daemon.ready(); + + let mut handler = ReadinessBased::new(&socket, 16); + loop { + match handler.read_requests() { + Ok(true) => {} + Ok(false) => { + info!("redox-drm: scheme unmounted, exiting"); + break; + } + Err(e) => { + error!("redox-drm: failed to receive scheme request: {}", e); + continue; + } + } + + handler.process_requests(|| drm_scheme.lock().expect("DRM scheme state poisoned")); + + if let Err(e) = handler.write_responses() { + error!("redox-drm: failed to write scheme responses: {}", e); + } + } + + Ok(()) +} + +fn create_drm_socket() -> Result { + Socket::create() + .map_err(|e| DriverError::Initialization(format!("failed to register drm scheme: {e}"))) +} + +fn select_gpu_from_args() -> Result { + let mut args = env::args().skip(1); + let parsed = match (args.next(), args.next(), args.next()) { + (Some(bus), Some(device), Some(function)) => { + Some(parse_location(&bus, &device, &function)?) + } + _ => None, + }; + + if let Some(location) = parsed { + let mut pci = PciDevice::open_location(&location).map_err(|e| { + DriverError::Pci(format!("failed to open PCI device {}: {e}", location)) + })?; + return pci.full_info().map_err(|e| { + DriverError::Pci(format!("failed to read PCI info for {}: {e}", location)) + }); + } + + if let Some(mut pcid) = PcidClient::connect_default() { + let function = pcid.request_config().map_err(|e| { + DriverError::Pci(format!("failed to read pcid-spawner handoff config: {e}")) + })?; + let info = function.device_info(); + info!( + "redox-drm: selected GPU from pcid-spawner handoff at {}", + info.location + ); + return Ok(info); + } + + let devices = enumerate_pci_class(PCI_CLASS_DISPLAY) + .map_err(|e| DriverError::Pci(format!("PCI scan failed: {e}")))?; + let first = devices + .into_iter() + .find(|d| { + d.vendor_id == PCI_VENDOR_ID_AMD + || d.vendor_id == PCI_VENDOR_ID_INTEL + || d.vendor_id == 0x1AF4 + }) + .ok_or_else(|| { + DriverError::NotFound("no AMD, Intel, or VirtIO GPU found via scheme:pci".to_string()) + })?; + let mut pci = PciDevice::open_location(&first.location) + .map_err(|e| DriverError::Pci(format!("failed to open GPU {}: {e}", first.location)))?; + pci.full_info() + .map_err(|e| DriverError::Pci(format!("failed to read GPU {}: {e}", first.location))) +} + +fn parse_location(bus: &str, device: &str, function: &str) -> Result { + let bus = parse_u8(bus)?; + let device = parse_u8(device)?; + let function = parse_u8(function)?; + Ok(PciLocation { + segment: 0, + bus, + device, + function, + }) +} + +fn parse_u8(value: &str) -> Result { + let trimmed = value.trim_start_matches("0x"); + u8::from_str_radix(trimmed, 16) + .or_else(|_| trimmed.parse::()) + .map_err(|_| DriverError::InvalidArgument("invalid PCI coordinate")) +} + +fn verify_supported_gpu(info: &PciDeviceInfo) -> Result<()> { + if info.class_code != PCI_CLASS_DISPLAY { + return Err(DriverError::Pci(format!( + "device {} is class {:#04x}, expected display class {:#04x}", + info.location, info.class_code, PCI_CLASS_DISPLAY + ))); + } + + if info.vendor_id != PCI_VENDOR_ID_AMD + && info.vendor_id != PCI_VENDOR_ID_INTEL + && info.vendor_id != 0x1AF4 + { + return Err(DriverError::Pci(format!( + "device {} is vendor {:#06x}, expected AMD {:#06x} or Intel {:#06x}", + info.location, info.vendor_id, PCI_VENDOR_ID_AMD, PCI_VENDOR_ID_INTEL + ))); + } + Ok(()) +} + +struct FirmwareCache { + blobs: HashMap>, +} + +struct FirmwareExpectation { + vendor_name: &'static str, + keys: &'static [&'static str], + required: bool, + required_label: &'static str, +} + +const AMD_DISPLAY_FIRMWARE_KEYS: &[&str] = &[ + "amdgpu/dcn_3_1_dmcub", + "amdgpu/dmcub_dcn20.bin", + "amdgpu/dmcub_dcn31.bin", +]; + +const INTEL_TGL_DMC_KEYS: &[&str] = &[ + "i915/tgl_dmc.bin", + "i915/tgl_dmc_ver2_12.bin", + "i915/tgl_dmc_ver2_06.bin", +]; +const INTEL_ADLP_DMC_KEYS: &[&str] = &[ + "i915/adlp_dmc.bin", + "i915/adlp_dmc_ver2_16.bin", + "i915/adlp_dmc_ver2_12.bin", +]; +const INTEL_DG2_DMC_KEYS: &[&str] = &["i915/dg2_dmc.bin", "i915/dg2_dmc_ver2_06.bin"]; +const INTEL_MTL_DMC_KEYS: &[&str] = &["i915/mtl_dmc.bin"]; +const INTEL_SKL_DMC_KEYS: &[&str] = &["i915/skl_dmc_ver1_27.bin", "i915/skl_dmc_ver1_23.bin"]; +const INTEL_KBL_DMC_KEYS: &[&str] = &["i915/kbl_dmc_ver1_04.bin", "i915/kbl_dmc_ver1_01.bin"]; +const INTEL_CNL_DMC_KEYS: &[&str] = &["i915/cnl_dmc_ver1_07.bin", "i915/cnl_dmc_ver1_06.bin"]; +const INTEL_ICL_DMC_KEYS: &[&str] = &["i915/icl_dmc_ver1_09.bin", "i915/icl_dmc_ver1_07.bin"]; +const INTEL_GLK_DMC_KEYS: &[&str] = &["i915/glk_dmc_ver1_04.bin"]; +const INTEL_RKL_DMC_KEYS: &[&str] = &["i915/rkl_dmc_ver2_03.bin", "i915/rkl_dmc_ver2_02.bin"]; +fn intel_display_firmware_keys(device_id: u16) -> Option<&'static [&'static str]> { + match device_id { + // Gen12+ (Tiger Lake and newer) + 0x9A40 | 0x9A49 | 0x9A59 | 0x9A60 | 0x9A68 | 0x9A70 | 0x9A78 | 0x9AC0 | 0x9AC9 | 0x9AD9 + | 0x9AF8 => Some(INTEL_TGL_DMC_KEYS), + 0x46A6 => Some(INTEL_ADLP_DMC_KEYS), + 0x5690 | 0x5691 | 0x5692 | 0x5693 | 0x5694 | 0x5695 | 0x5696 | 0x5697 | 0x56A0 | 0x56A1 + | 0x56A2 | 0x56A3 | 0x56A4 | 0x56A5 | 0x56A6 | 0x56B0 | 0x56B1 | 0x56B2 | 0x56B3 + | 0x56BA | 0x56BB | 0x56BC | 0x56BD | 0x56BE | 0x56BF | 0x56C0 | 0x56C1 => { + Some(INTEL_DG2_DMC_KEYS) + } + 0x7D40 | 0x7D41 | 0x7D45 | 0x7D51 | 0x7D55 | 0x7D60 | 0x7D67 | 0x7DD1 | 0x7DD5 => { + Some(INTEL_MTL_DMC_KEYS) + } + // Gen9 (Ice Lake / Rocket Lake / Cannon Lake) + 0x4905 | 0x4906 | 0x4907 | 0x4908 | 0x4909 => Some(INTEL_ICL_DMC_KEYS), + 0x4C80 | 0x4C8A | 0x4C8B | 0x4C8C | 0x4C90 | 0x4C9A => Some(INTEL_RKL_DMC_KEYS), + 0x5A40 | 0x5A41 | 0x5A42 | 0x5A44 | 0x5A49 | 0x5A4A | 0x5A4C | 0x5A50 | 0x5A51 | 0x5A52 + | 0x5A54 | 0x5A59 | 0x5A5A | 0x5A5C => Some(INTEL_CNL_DMC_KEYS), + // Gen8 (Skylake / Kaby Lake / Coffee Lake / Gemini Lake / Broxton) + 0x1902 | 0x1906 | 0x190A | 0x190B | 0x190E | 0x1912 | 0x1916 | 0x1917 | 0x191A | 0x191B + | 0x191D | 0x191E | 0x1921 | 0x1923 | 0x1926 | 0x1927 | 0x192A | 0x192B | 0x192D + | 0x1932 | 0x193A | 0x193B | 0x193D => Some(INTEL_SKL_DMC_KEYS), + 0x3E90 | 0x3E91 | 0x3E92 | 0x3E93 | 0x3E94 | 0x3E96 | 0x3E98 | 0x3E99 | 0x3E9A | 0x3E9B + | 0x3E9C | 0x3EA0 | 0x3EA1 | 0x3EA2 | 0x3EA3 | 0x3EA4 | 0x3EA5 | 0x3EA6 | 0x3EA7 + | 0x3EA8 | 0x3EA9 => Some(INTEL_KBL_DMC_KEYS), + 0x87C0 | 0x87CA => Some(INTEL_KBL_DMC_KEYS), + 0x9B21 | 0x9B41 | 0x9BA2 | 0x9BA4 | 0x9BA5 | 0x9BA8 | 0x9BAA | 0x9BAC | 0x9BC2 | 0x9BC4 + | 0x9BC5 | 0x9BC6 | 0x9BC8 | 0x9BCA | 0x9BCC | 0x9BE6 | 0x9BF6 => Some(INTEL_KBL_DMC_KEYS), + 0x0A84 | 0x1A84 | 0x1A85 | 0x5A84 | 0x5A85 => Some(INTEL_GLK_DMC_KEYS), + _ => None, + } +} + +fn firmware_expectation(info: &PciDeviceInfo, quirks: PciQuirkFlags) -> FirmwareExpectation { + match info.vendor_id { + PCI_VENDOR_ID_AMD => FirmwareExpectation { + vendor_name: "AMD", + keys: &[ + "amdgpu/psp_13_0_0_sos", + "amdgpu/psp_13_0_0_ta", + "amdgpu/gc_11_0_0_pfp", + "amdgpu/gc_11_0_0_me", + "amdgpu/gc_11_0_0_ce", + "amdgpu/gc_11_0_0_rlc", + "amdgpu/gc_11_0_0_mec", + "amdgpu/gc_11_0_0_mec2", + "amdgpu/dcn_3_1_dmcub", + "amdgpu/dmcub_dcn20.bin", + "amdgpu/dmcub_dcn31.bin", + "amdgpu/sdma_5_0", + "amdgpu/sdma_5_2", + "amdgpu/vcn_3_0_0", + "amdgpu/vcn_3_1_0", + ], + required: quirks.contains(PciQuirkFlags::NEED_FIRMWARE), + required_label: "AMD firmware", + }, + PCI_VENDOR_ID_INTEL => { + let keys = intel_display_firmware_keys(info.device_id).unwrap_or(&[]); + FirmwareExpectation { + vendor_name: "Intel", + keys, + required: !keys.is_empty(), + required_label: "Intel display DMC firmware", + } + } + _ => FirmwareExpectation { + vendor_name: "unknown", + keys: &[], + required: false, + required_label: "firmware", + }, + } +} + +fn summarize_missing_firmware(missing: &[String]) -> String { + const MAX_SHOWN: usize = 3; + + if missing.is_empty() { + return "none".to_string(); + } + + let shown: Vec<&str> = missing.iter().take(MAX_SHOWN).map(String::as_str).collect(); + if missing.len() > MAX_SHOWN { + format!("{} (+{} more)", shown.join(", "), missing.len() - MAX_SHOWN) + } else { + shown.join(", ") + } +} + +fn firmware_requirement_error( + expectation: &FirmwareExpectation, + loaded: &HashMap>, + missing: &[String], +) -> Option { + if !expectation.required { + return None; + } + + if loaded.is_empty() { + return Some(format!( + "no {} firmware blobs available from scheme:firmware; checked {} candidates ({})", + expectation.required_label, + expectation.keys.len(), + summarize_missing_firmware(missing) + )); + } + + if expectation.vendor_name == "AMD" + && !AMD_DISPLAY_FIRMWARE_KEYS + .iter() + .any(|key| loaded.contains_key(*key)) + { + return Some(format!( + "AMD firmware policy requires a DMCUB/display blob before backend init; checked {} candidates ({})", + expectation.keys.len(), + summarize_missing_firmware(missing) + )); + } + + None +} + +impl FirmwareCache { + fn load_for_device(info: &PciDeviceInfo) -> Result { + let quirks = info.quirks(); + let expectation = firmware_expectation(info, quirks); + + if expectation.keys.is_empty() { + if expectation.required { + info!( + "redox-drm: {} GPU {} declares NEED_FIRMWARE in canonical quirk policy, but no Rust-side firmware manifest is defined for this vendor yet", + expectation.vendor_name, + info.location + ); + } else { + info!( + "redox-drm: skipping firmware preload for {} GPU {} (no Rust-side firmware manifest)", + expectation.vendor_name, + info.location + ); + } + return Ok(Self { + blobs: HashMap::new(), + }); + } + + let mut blobs = HashMap::new(); + let mut missing = Vec::new(); + + info!( + "redox-drm: firmware preload for {} GPU {} expects {} candidate blob(s); required_by_quirk={}", + expectation.vendor_name, + info.location, + expectation.keys.len(), + expectation.required + ); + + for &key in expectation.keys { + let path = format!("/scheme/firmware/{}", key); + match File::open(&path) { + Ok(mut file) => { + let metadata = file.metadata(); + let estimated_size = metadata.map(|m| m.len()).unwrap_or(1024 * 1024); + if estimated_size > MAX_FIRMWARE_BLOB_BYTES { + info!( + "redox-drm: firmware {} rejected — {} bytes exceeds trusted preload cap {}", + key, + estimated_size, + MAX_FIRMWARE_BLOB_BYTES + ); + missing.push(key.to_string()); + continue; + } + let mut buf = Vec::with_capacity(estimated_size as usize); + match file.read_to_end(&mut buf) { + Ok(bytes_read) => { + info!("redox-drm: loaded firmware {} ({} bytes)", key, bytes_read); + blobs.insert(key.to_string(), buf); + } + Err(e) => { + info!("redox-drm: failed to read firmware {}: {}", key, e); + missing.push(key.to_string()); + } + } + } + Err(e) => { + info!("redox-drm: firmware {} not available: {}", key, e); + missing.push(key.to_string()); + } + } + } + + if let Some(message) = firmware_requirement_error(&expectation, &blobs, &missing) { + return Err(DriverError::NotFound(message)); + } + + if !missing.is_empty() { + info!( + "redox-drm: firmware preload for {} GPU {} left {} blob(s) unavailable: {}", + expectation.vendor_name, + info.location, + missing.len(), + summarize_missing_firmware(&missing) + ); + } + + info!( + "redox-drm: firmware cache populated with {} blob(s) for {} GPU {}", + blobs.len(), + expectation.vendor_name, + info.location + ); + Ok(Self { blobs }) + } + + #[allow(dead_code)] + fn get(&self, key: &str) -> Option<&[u8]> { + self.blobs.get(key).map(|v| v.as_slice()) + } + + fn into_blobs(self) -> HashMap> { + self.blobs + } +} + +fn main() { + let log_level = match env::var("REDOX_DRM_LOG").as_deref() { + Ok("trace") => LevelFilter::Trace, + Ok("debug") => LevelFilter::Debug, + Ok("warn") => LevelFilter::Warn, + Ok("error") => LevelFilter::Error, + _ => LevelFilter::Info, + }; + + init_logging(log_level); + + daemon::Daemon::new(|daemon| { + if let Err(error) = run(daemon) { + error!("redox-drm: fatal error: {}", error); + process::exit(1); + } + process::exit(0); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_gpu_info(vendor_id: u16, device_id: u16) -> PciDeviceInfo { + PciDeviceInfo { + location: PciLocation { + segment: 0, + bus: 0, + device: 0, + function: 0, + }, + vendor_id, + device_id, + subsystem_vendor_id: 0, + subsystem_device_id: 0, + revision: 0, + class_code: PCI_CLASS_DISPLAY, + subclass: 0, + prog_if: 0, + header_type: 0, + irq: None, + bars: Vec::new(), + capabilities: Vec::new(), + } + } + + #[test] + fn firmware_expectation_marks_amd_need_firmware_as_required() { + let expectation = firmware_expectation( + &PciDeviceInfo { + location: PciLocation { + segment: 0, + bus: 0, + device: 0, + function: 0, + }, + vendor_id: PCI_VENDOR_ID_AMD, + device_id: 0x744C, + subsystem_vendor_id: 0, + subsystem_device_id: 0, + revision: 0, + class_code: PCI_CLASS_DISPLAY, + subclass: 0, + prog_if: 0, + header_type: 0, + irq: None, + bars: Vec::new(), + capabilities: Vec::new(), + }, + PciQuirkFlags::from_bits_truncate(PciQuirkFlags::NEED_FIRMWARE.bits()), + ); + + assert_eq!(expectation.vendor_name, "AMD"); + assert!(expectation.required); + assert!(!expectation.keys.is_empty()); + } + + #[test] + fn summarize_missing_firmware_truncates_long_lists() { + let summary = summarize_missing_firmware(&[ + "a".to_string(), + "b".to_string(), + "c".to_string(), + "d".to_string(), + ]); + + assert_eq!(summary, "a, b, c (+1 more)"); + } + + #[test] + fn amd_required_firmware_needs_display_blob() { + let expectation = firmware_expectation( + &PciDeviceInfo { + location: PciLocation { + segment: 0, + bus: 0, + device: 0, + function: 0, + }, + vendor_id: PCI_VENDOR_ID_AMD, + device_id: 0x744C, + subsystem_vendor_id: 0, + subsystem_device_id: 0, + revision: 0, + class_code: PCI_CLASS_DISPLAY, + subclass: 0, + prog_if: 0, + header_type: 0, + irq: None, + bars: Vec::new(), + capabilities: Vec::new(), + }, + PciQuirkFlags::from_bits_truncate(PciQuirkFlags::NEED_FIRMWARE.bits()), + ); + let mut loaded = HashMap::new(); + loaded.insert("amdgpu/gc_11_0_0_pfp".to_string(), vec![1, 2, 3]); + let missing = vec!["amdgpu/dmcub_dcn31.bin".to_string()]; + + let error = firmware_requirement_error(&expectation, &loaded, &missing); + + assert!(error.is_some()); + assert!(error.unwrap().contains("DMCUB/display blob")); + } + + #[test] + fn intel_tgl_manifest_is_required_from_startup() { + let expectation = firmware_expectation( + &test_gpu_info(PCI_VENDOR_ID_INTEL, 0x9A49), + PciQuirkFlags::empty(), + ); + + assert_eq!(expectation.vendor_name, "Intel"); + assert!(expectation.required); + assert_eq!(expectation.required_label, "Intel display DMC firmware"); + assert!(expectation.keys.contains(&"i915/tgl_dmc_ver2_12.bin")); + } + + #[test] + fn unknown_intel_device_has_no_startup_manifest_yet() { + let expectation = firmware_expectation( + &test_gpu_info(PCI_VENDOR_ID_INTEL, 0x3E92), + PciQuirkFlags::empty(), + ); + + assert_eq!(expectation.vendor_name, "Intel"); + assert!(!expectation.required); + assert!(expectation.keys.is_empty()); + } + + #[test] + fn mode_info_default_1080p_clock_matches_standard_cvt() { + use crate::kms::ModeInfo; + let mode = ModeInfo::default_1080p(); + // Standard 1080p60 timing: 148.5 MHz pixel clock + assert_eq!(mode.clock, 148_500); + // Total pixels per frame = htotal * vtotal = 2200 * 1125 = 2_475_000 + // Refresh = clock*1000 / total = 148_500_000 / 2_475_000 = 60 + assert_eq!(mode.htotal as u32 * mode.vtotal as u32, 2_475_000_u32); + } + + #[test] + fn mode_info_from_edid_rejects_short_edid() { + use crate::kms::connector::synthetic_edid; + use crate::kms::ModeInfo; + let edid = synthetic_edid(); + assert!(edid.len() < 128); + let modes = ModeInfo::from_edid(&edid); + assert!(modes.is_empty()); + } + + #[test] + fn mode_info_from_edid_parses_valid_128byte_edid() { + use crate::kms::ModeInfo; + let mut edid = vec![0u8; 128]; + edid[0] = 0x00; + edid[1] = 0xFF; + edid[2] = 0xFF; + edid[3] = 0xFF; + edid[4] = 0xFF; + edid[5] = 0xFF; + edid[6] = 0xFF; + edid[7] = 0x00; + let modes = ModeInfo::from_edid(&edid); + assert!( + modes.is_empty(), + "all-zero descriptors should produce no modes" + ); + } + + #[test] + fn mode_info_from_edid_name_format_is_width_x_height_at_refresh() { + use crate::kms::connector::synthetic_edid; + use crate::kms::ModeInfo; + let edid = synthetic_edid(); + let modes = ModeInfo::from_edid(&edid); + for mode in &modes { + // Verify the canonical format: "WxH@refresh" + let expected = format!("{}x{}@{}", mode.hdisplay, mode.vdisplay, mode.vrefresh); + assert_eq!(mode.name, expected); + } + } +} diff --git a/local/recipes/gpu/redox-drm/source/src/scheme.rs b/local/recipes/gpu/redox-drm/source/src/scheme.rs new file mode 100644 index 0000000000..2f21c266c8 --- /dev/null +++ b/local/recipes/gpu/redox-drm/source/src/scheme.rs @@ -0,0 +1,2597 @@ +use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::mem::size_of; +use std::sync::Arc; + +use getrandom::getrandom; +use log::{debug, warn}; +use redox_scheme::scheme::SchemeSync; +use redox_scheme::{CallerCtx, OpenResult}; +use syscall::data::Stat; +use syscall::error::{Error, Result, EBADF, EBUSY, EINVAL, ENOENT, EOPNOTSUPP}; +use syscall::flag::{EventFlags, MapFlags, MunmapFlags, MODE_FILE}; +use syscall::schemev2::NewFdFlags; + +use crate::driver::{ + DriverEvent, GpuDriver, RedoxPrivateCsSubmit, RedoxPrivateCsSubmitResult, RedoxPrivateCsWait, + RedoxPrivateCsWaitResult, +}; +use crate::gem::GemHandle; +use crate::kms::ModeInfo; + +#[derive(Clone, Debug)] +struct FbInfo { + gem_handle: GemHandle, + width: u32, + height: u32, + pitch: u32, + bpp: u32, +} + +// ---- DRM ioctl request codes ---- +const DRM_IOCTL_BASE: usize = 0x00A0; +const DRM_IOCTL_MODE_GETRESOURCES: usize = DRM_IOCTL_BASE; +const DRM_IOCTL_MODE_GETCONNECTOR: usize = DRM_IOCTL_BASE + 7; +const DRM_IOCTL_MODE_GETMODES: usize = DRM_IOCTL_BASE + 8; +const DRM_IOCTL_MODE_SETCRTC: usize = DRM_IOCTL_BASE + 2; +const DRM_IOCTL_MODE_GETCRTC: usize = DRM_IOCTL_BASE + 3; +const DRM_IOCTL_MODE_GETENCODER: usize = DRM_IOCTL_BASE + 6; +const DRM_IOCTL_MODE_PAGE_FLIP: usize = DRM_IOCTL_BASE + 16; +const DRM_IOCTL_MODE_CREATE_DUMB: usize = DRM_IOCTL_BASE + 18; +const DRM_IOCTL_MODE_MAP_DUMB: usize = DRM_IOCTL_BASE + 19; +const DRM_IOCTL_MODE_DESTROY_DUMB: usize = DRM_IOCTL_BASE + 20; +const DRM_IOCTL_MODE_ADDFB: usize = DRM_IOCTL_BASE + 21; +const DRM_IOCTL_MODE_RMFB: usize = DRM_IOCTL_BASE + 22; +const DRM_IOCTL_GET_CAP: usize = DRM_IOCTL_BASE + 23; +const DRM_IOCTL_SET_CLIENT_CAP: usize = DRM_IOCTL_BASE + 24; +const DRM_IOCTL_VERSION: usize = DRM_IOCTL_BASE + 25; +const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26; +const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27; +const DRM_IOCTL_GEM_MMAP: usize = DRM_IOCTL_BASE + 28; +const DRM_IOCTL_PRIME_HANDLE_TO_FD: usize = DRM_IOCTL_BASE + 29; +const DRM_IOCTL_PRIME_FD_TO_HANDLE: usize = DRM_IOCTL_BASE + 30; +const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31; +const DRM_IOCTL_REDOX_PRIVATE_CS_WAIT: usize = DRM_IOCTL_BASE + 32; +const DRM_IOCTL_REDOX_AMD_SDMA_SUBMIT: usize = DRM_IOCTL_BASE + 0x40; +const DRM_IOCTL_REDOX_AMD_SDMA_WAIT: usize = DRM_IOCTL_BASE + 0x41; + +const MAX_SCHEME_GEM_BYTES: u64 = 256 * 1024 * 1024; + +// ---- Wire types for DRM ioctls ---- +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmResourcesWire { + connector_count: u32, + crtc_count: u32, + encoder_count: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmConnectorWire { + connector_id: u32, + connection: u32, + connector_type: u32, + mm_width: u32, + mm_height: u32, + encoder_id: u32, + mode_count: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmModeWire { + clock: u32, + hdisplay: u16, + hsync_start: u16, + hsync_end: u16, + htotal: u16, + hskew: u16, + vdisplay: u16, + vsync_start: u16, + vsync_end: u16, + vtotal: u16, + vscan: u16, + vrefresh: u32, + flags: u32, + type_: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmSetCrtcWire { + crtc_id: u32, + fb_handle: u32, + connector_count: u32, + connectors: [u32; 8], + mode: DrmModeWire, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmPageFlipWire { + crtc_id: u32, + fb_handle: u32, + flags: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmCreateDumbWire { + width: u32, + height: u32, + bpp: u32, + flags: u32, + pitch: u32, + size: u64, + handle: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmMapDumbWire { + handle: u32, + offset: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmDestroyDumbWire { + handle: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGetEncoderWire { + encoder_id: u32, + encoder_type: u32, + crtc_id: u32, + possible_crtcs: u32, + possible_clones: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmAddFbWire { + width: u32, + height: u32, + pitch: u32, + bpp: u32, + depth: u32, + handle: u32, + fb_id: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmRmFbWire { + fb_id: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGetCrtcWire { + crtc_id: u32, + fb_id: u32, + x: u32, + y: u32, + mode_valid: u32, + mode: DrmModeWire, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmVersionWire { + major: i32, + minor: i32, + patch: i32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGetCapWire { + capability: u64, + value: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmSetClientCapWire { + capability: u64, + value: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGemCreateWire { + size: u64, + handle: u32, + _pad: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGemCloseWire { + handle: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGemMmapWire { + handle: u32, + _pad: u32, + offset: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmPrimeHandleToFdWire { + handle: u32, + flags: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmPrimeFdToHandleWire { + fd: i32, + _pad: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmPrimeHandleToFdResponseWire { + fd: i32, + _pad: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmPrimeFdToHandleResponseWire { + handle: u32, + _pad: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct RedoxAmdSdmaSubmitWire { + src_handle: u32, + dst_handle: u32, + flags: u32, + _pad: u32, + src_offset: u64, + dst_offset: u64, + size: u64, + seqno: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct RedoxAmdSdmaWaitWire { + seqno: u64, + timeout_ns: u64, + flags: u32, + completed: u32, + completed_seqno: u64, +} + +// ---- Internal handle types ---- + +#[derive(Clone, Debug)] +enum NodeKind { + Root, + Card, + Connector(u32), + DmaBuf { + gem_handle: GemHandle, + export_token: u32, + }, +} + +struct Handle { + node: NodeKind, + response: Vec, + event_queue: VecDeque>, + mapped_gem: Option, + mapped_gem_refs: usize, + owned_fbs: Vec, + owned_gems: Vec, + imported_gems: HashSet, + closing: bool, +} + +pub struct DrmScheme { + driver: Arc, + next_id: usize, + next_fb_id: u32, + handles: BTreeMap, + active_crtc_fb: BTreeMap, + active_crtc_mode: BTreeMap, + pending_flip_fb: BTreeMap, + fb_registry: BTreeMap, + active_gem_maps: BTreeMap, + gem_export_refs: BTreeMap, + prime_exports: BTreeMap, +} + +impl DrmScheme { + pub fn new(driver: Arc) -> Self { + Self { + driver, + next_id: 0, + next_fb_id: 1, + handles: BTreeMap::new(), + active_crtc_fb: BTreeMap::new(), + active_crtc_mode: BTreeMap::new(), + pending_flip_fb: BTreeMap::new(), + fb_registry: BTreeMap::new(), + active_gem_maps: BTreeMap::new(), + gem_export_refs: BTreeMap::new(), + prime_exports: BTreeMap::new(), + } + } + + fn is_fb_active(&self, fb_id: u32) -> bool { + self.active_crtc_fb.values().any(|&id| id == fb_id) + || self.pending_flip_fb.values().any(|&(_, id)| id == fb_id) + } + + fn handle_has_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool { + handle.owned_gems.contains(&gem_handle) + } + + fn handle_has_local_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool { + Self::handle_has_gem_ref(handle, gem_handle) && !handle.imported_gems.contains(&gem_handle) + } + + fn handle_has_imported_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool { + Self::handle_has_gem_ref(handle, gem_handle) && handle.imported_gems.contains(&gem_handle) + } + + fn gem_is_still_referenced(&self, gem_handle: GemHandle) -> bool { + self.handles + .values() + .any(|handle| Self::handle_has_gem_ref(handle, gem_handle)) + } + + fn gem_has_other_refs(&self, current_id: usize, gem_handle: GemHandle) -> bool { + self.handles.iter().any(|(&other_id, handle)| { + other_id != current_id && Self::handle_has_gem_ref(handle, gem_handle) + }) + } + + fn gem_is_mapped(&self, gem_handle: GemHandle) -> bool { + self.active_gem_maps.get(&gem_handle).copied().unwrap_or(0) != 0 + } + + fn gem_export_refcount(&self, gem_handle: GemHandle) -> usize { + self.gem_export_refs.get(&gem_handle).copied().unwrap_or(0) + } + + fn allocate_export_token(&self) -> Result { + for _ in 0..64 { + let mut bytes = [0u8; 4]; + getrandom(&mut bytes).map_err(|e| { + warn!("redox-drm: failed to draw PRIME export token entropy: {e}"); + Error::new(syscall::error::EIO) + })?; + + let token = u32::from_le_bytes(bytes) & 0x7fff_ffff; + if token == 0 || self.prime_exports.contains_key(&token) { + continue; + } + + return Ok(token); + } + + warn!("redox-drm: unable to allocate unique PRIME export token"); + Err(Error::new(EBUSY)) + } + + fn bump_export_ref(&mut self, gem_handle: GemHandle) { + let entry = self.gem_export_refs.entry(gem_handle).or_insert(0); + *entry = entry.saturating_add(1); + } + + fn drop_export_ref(&mut self, gem_handle: GemHandle) { + let remove_entry = match self.gem_export_refs.get_mut(&gem_handle) { + Some(count) if *count > 1 => { + *count -= 1; + false + } + Some(_) => true, + None => false, + }; + if remove_entry { + self.gem_export_refs.remove(&gem_handle); + self.prime_exports.retain(|_, &mut h| h != gem_handle); + } + } + + fn gem_can_close(&self, gem_handle: GemHandle) -> bool { + let backs_fb = self + .fb_registry + .values() + .any(|info| info.gem_handle == gem_handle); + !backs_fb + && !self.gem_is_still_referenced(gem_handle) + && !self.gem_is_mapped(gem_handle) + && self.gem_export_refcount(gem_handle) == 0 + } + + fn validate_private_cs_handles( + &self, + id: usize, + src_handle: GemHandle, + dst_handle: GemHandle, + operation: &str, + ) -> Result<()> { + let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + + if !Self::handle_has_gem_ref(handle, src_handle) + || !Self::handle_has_gem_ref(handle, dst_handle) + { + warn!( + "redox-drm: {} rejected — src={} dst={} not owned by this fd", + operation, src_handle, dst_handle + ); + return Err(Error::new(EBADF)); + } + + if Self::handle_has_imported_gem_ref(handle, src_handle) + || Self::handle_has_imported_gem_ref(handle, dst_handle) + { + warn!( + "redox-drm: {} rejected — imported DMA-BUF handles are outside the bounded private CS path", + operation + ); + return Err(Error::new(EOPNOTSUPP)); + } + + Ok(()) + } + + fn validate_private_cs_ranges( + &self, + submit: &RedoxPrivateCsSubmit, + operation: &str, + ) -> Result<()> { + if submit.byte_count == 0 { + warn!("redox-drm: {} rejected — zero-sized submission", operation); + return Err(Error::new(EINVAL)); + } + + let src_size = self + .driver + .gem_size(submit.src_handle) + .map_err(driver_to_syscall)?; + let dst_size = self + .driver + .gem_size(submit.dst_handle) + .map_err(driver_to_syscall)?; + + let src_end = submit + .src_offset + .checked_add(submit.byte_count) + .ok_or_else(|| { + warn!("redox-drm: {} rejected — source range overflow", operation); + Error::new(EINVAL) + })?; + if src_end > src_size { + warn!( + "redox-drm: {} rejected — source range {}..{} exceeds GEM size {}", + operation, submit.src_offset, src_end, src_size + ); + return Err(Error::new(EINVAL)); + } + + let dst_end = submit + .dst_offset + .checked_add(submit.byte_count) + .ok_or_else(|| { + warn!( + "redox-drm: {} rejected — destination range overflow", + operation + ); + Error::new(EINVAL) + })?; + if dst_end > dst_size { + warn!( + "redox-drm: {} rejected — destination range {}..{} exceeds GEM size {}", + operation, submit.dst_offset, dst_end, dst_size + ); + return Err(Error::new(EINVAL)); + } + + Ok(()) + } + + fn validate_gem_create_size(&self, size: u64, operation: &str) -> Result<()> { + if size == 0 { + warn!( + "redox-drm: {} rejected — zero-sized GEM allocation", + operation + ); + return Err(Error::new(EINVAL)); + } + if size > MAX_SCHEME_GEM_BYTES { + warn!( + "redox-drm: {} rejected — size {} exceeds trusted shared-core cap {}", + operation, size, MAX_SCHEME_GEM_BYTES + ); + return Err(Error::new(EINVAL)); + } + Ok(()) + } + + fn maybe_close_gem(&mut self, gem_handle: GemHandle, context: &str) -> bool { + if !self.gem_can_close(gem_handle) { + return false; + } + + match self.driver.gem_close(gem_handle) { + Ok(()) => { + self.prime_exports.retain(|_, &mut h| h != gem_handle); + true + } + Err(e) => { + warn!( + "redox-drm: {} gem_close({}) failed: {}", + context, gem_handle, e + ); + false + } + } + } + + fn allocate_handle(&mut self, node: NodeKind) -> usize { + let id = self.next_id; + self.next_id = self.next_id.saturating_add(1); + self.handles.insert( + id, + Handle { + node, + response: Vec::new(), + event_queue: VecDeque::new(), + mapped_gem: None, + mapped_gem_refs: 0, + owned_fbs: Vec::new(), + owned_gems: Vec::new(), + imported_gems: HashSet::new(), + closing: false, + }, + ); + id + } + + fn finalize_handle_close(&mut self, handle: Handle) { + if let NodeKind::DmaBuf { gem_handle, .. } = handle.node { + self.drop_export_ref(gem_handle); + let _ = self.maybe_close_gem(gem_handle, "close dmabuf"); + return; + } + + let mut auto_closed_gems = HashSet::new(); + for fb_id in &handle.owned_fbs { + if self.is_fb_active(*fb_id) { + continue; + } + if let Some(fb_info) = self.fb_registry.remove(fb_id) { + if self.maybe_close_gem(fb_info.gem_handle, "close") { + auto_closed_gems.insert(fb_info.gem_handle); + } + } + } + for gem_handle in handle.owned_gems { + if auto_closed_gems.contains(&gem_handle) { + continue; + } + let backs_fb = self + .fb_registry + .values() + .any(|info| info.gem_handle == gem_handle); + if !backs_fb && self.maybe_close_gem(gem_handle, "close gem") { + auto_closed_gems.insert(gem_handle); + } + } + } + + fn pin_mapped_gem(&mut self, id: usize, gem_handle: GemHandle) -> Result<()> { + let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?; + handle.mapped_gem = Some(gem_handle); + handle.mapped_gem_refs = handle.mapped_gem_refs.saturating_add(1); + let entry = self.active_gem_maps.entry(gem_handle).or_insert(0); + *entry = entry.saturating_add(1); + Ok(()) + } + + fn unpin_mapped_gem(&mut self, id: usize) -> Result<()> { + let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?; + let gem_handle = match handle.mapped_gem { + Some(gem_handle) if handle.mapped_gem_refs != 0 => gem_handle, + _ => return Ok(()), + }; + handle.mapped_gem_refs -= 1; + if handle.mapped_gem_refs == 0 { + handle.mapped_gem = None; + } + + let remove_entry = match self.active_gem_maps.get_mut(&gem_handle) { + Some(count) if *count > 1 => { + *count -= 1; + false + } + Some(_) => true, + None => false, + }; + if remove_entry { + self.active_gem_maps.remove(&gem_handle); + } + Ok(()) + } + + pub fn retire_vblank(&mut self, crtc_id: u32, vblank_count: u64) { + if let Some((expected, fb_id)) = self.pending_flip_fb.get(&crtc_id).copied() { + if expected <= vblank_count { + self.pending_flip_fb.remove(&crtc_id); + self.try_reap_fb(fb_id); + } + } + } + + pub fn handle_driver_event(&mut self, event: DriverEvent) { + match event { + DriverEvent::Vblank { crtc_id, count } => { + self.retire_vblank(crtc_id, count); + self.queue_card_event(format!("vblank:{crtc_id}:{count}\n").into_bytes()); + } + DriverEvent::Hotplug { connector_id } => self.queue_hotplug_event(connector_id), + } + } + + fn queue_card_event(&mut self, payload: Vec) { + for handle in self.handles.values_mut() { + if let NodeKind::Card = handle.node { + handle.event_queue.push_back(payload.clone()); + } + } + } + + fn queue_hotplug_event(&mut self, connector_id: u32) { + let payload = format!("hotplug:{}\n", connector_id).into_bytes(); + for handle in self.handles.values_mut() { + match handle.node { + NodeKind::Card => { + handle.event_queue.push_back(payload.clone()); + } + NodeKind::Connector(id) if id == connector_id => { + handle.event_queue.push_back(payload.clone()); + } + _ => {} + } + } + } + + fn try_reap_fb(&mut self, fb_id: u32) { + let gem_handle = match self.fb_registry.get(&fb_id) { + Some(info) => info.gem_handle, + None => return, + }; + let still_owned = self.handles.values().any(|h| h.owned_fbs.contains(&fb_id)); + if still_owned { + return; + } + self.fb_registry.remove(&fb_id); + let _ = self.maybe_close_gem(gem_handle, "try_reap_fb"); + } + + // ---- Encode helpers ---- + + fn encode_resources(&self) -> Vec { + let connectors = self.driver.detect_connectors(); + let payload = DrmResourcesWire { + connector_count: connectors.len() as u32, + crtc_count: 1, + encoder_count: connectors.len() as u32, + }; + let mut out = bytes_of(&payload); + for connector in connectors { + out.extend_from_slice(&bytes_of(&connector.id)); + } + out + } + + fn encode_connector(&self, connector_id: u32) -> Result> { + let connector = self + .driver + .detect_connectors() + .into_iter() + .find(|c| c.id == connector_id) + .ok_or_else(|| Error::new(ENOENT))?; + + let header = DrmConnectorWire { + connector_id: connector.id, + connection: match connector.connection { + crate::kms::ConnectorStatus::Connected => 1, + crate::kms::ConnectorStatus::Disconnected => 2, + crate::kms::ConnectorStatus::Unknown => 0, + }, + connector_type: connector_type_to_u32(connector.connector_type), + mm_width: connector.mm_width, + mm_height: connector.mm_height, + encoder_id: connector.encoder_id, + mode_count: connector.modes.len() as u32, + }; + + let mut out = bytes_of(&header); + for mode in &connector.modes { + out.extend_from_slice(&bytes_of(&mode_to_wire(mode))); + out.extend_from_slice(mode.name.as_bytes()); + out.push(0); + } + Ok(out) + } + + // ---- ioctl dispatch ---- + + fn handle_ioctl(&mut self, id: usize, request: usize, payload: &[u8]) -> Result { + let response = match request { + DRM_IOCTL_MODE_GETRESOURCES => self.encode_resources(), + + DRM_IOCTL_MODE_GETCONNECTOR => { + let connector_id = if payload.len() >= size_of::() { + read_u32(payload, 0)? + } else { + match self.handles.get(&id).map(|h| &h.node) { + Some(NodeKind::Connector(cid)) => *cid, + _ => return Err(Error::new(EINVAL)), + } + }; + self.encode_connector(connector_id)? + } + + DRM_IOCTL_MODE_GETMODES => { + let connector_id = read_u32(payload, 0)?; + let modes = self.driver.get_modes(connector_id); + encode_modes(&modes) + } + + DRM_IOCTL_MODE_SETCRTC => { + let req = decode_wire::(payload)?; + if req.fb_handle == 0 && req.connector_count == 0 { + let completed_flip = self.pending_flip_fb.remove(&req.crtc_id); + let prev_fb_id = self.active_crtc_fb.remove(&req.crtc_id); + self.active_crtc_mode.remove(&req.crtc_id); + if let Some((_, fb_id)) = completed_flip { + self.try_reap_fb(fb_id); + } + if let Some(fb_id) = prev_fb_id { + self.try_reap_fb(fb_id); + } + return Ok(1); + } + let count = req.connector_count as usize; + if count > req.connectors.len() { + return Err(Error::new(EINVAL)); + } + let conns = req.connectors[..count].to_vec(); + let fb_info = self.fb_registry.get(&req.fb_handle).ok_or_else(|| { + warn!("redox-drm: SETCRTC with unknown fb_id {}", req.fb_handle); + Error::new(ENOENT) + })?; + let mode = wire_to_mode(&req.mode); + let fb_pitch = fb_info.pitch as u64; + let required_fb_lines = mode.vdisplay as u64; + let fb_height = fb_info.height as u64; + let fb_width = fb_info.width as u64; + let mode_width = mode.hdisplay as u64; + if fb_pitch.checked_mul(required_fb_lines).is_none() { + warn!("redox-drm: SETCRTC FB pitch * mode_height overflows"); + return Err(Error::new(EINVAL)); + } + if fb_pitch == 0 || fb_height < required_fb_lines || fb_width < mode_width { + warn!( + "redox-drm: SETCRTC FB {}x{} pitch={} too small for mode {}x{}", + fb_info.width, fb_info.height, fb_info.pitch, mode.hdisplay, mode.vdisplay + ); + return Err(Error::new(EINVAL)); + } + let gem_handle = fb_info.gem_handle; + self.driver + .set_crtc(req.crtc_id, gem_handle, &conns, &mode) + .map_err(driver_to_syscall)?; + let completed_flip = self.pending_flip_fb.remove(&req.crtc_id); + let prev_fb = self.active_crtc_fb.insert(req.crtc_id, req.fb_handle); + self.active_crtc_mode.insert(req.crtc_id, mode); + if let Some((_, fb_id)) = completed_flip { + self.try_reap_fb(fb_id); + } + if let Some(prev) = prev_fb { + if prev != req.fb_handle { + self.try_reap_fb(prev); + } + } + Vec::new() + } + + DRM_IOCTL_MODE_PAGE_FLIP => { + let req = decode_wire::(payload)?; + if self.pending_flip_fb.contains_key(&req.crtc_id) { + warn!( + "redox-drm: PAGE_FLIP rejected — flip already pending on CRTC {}", + req.crtc_id + ); + return Err(Error::new(EBUSY)); + } + let fb_info = self.fb_registry.get(&req.fb_handle).ok_or_else(|| { + warn!("redox-drm: PAGE_FLIP with unknown fb_id {}", req.fb_handle); + Error::new(ENOENT) + })?; + if let Some(active_mode) = self.active_crtc_mode.get(&req.crtc_id) { + let fb_pitch = fb_info.pitch as u64; + let required_lines = active_mode.vdisplay as u64; + let required_width = active_mode.hdisplay as u64; + if fb_pitch == 0 + || (fb_info.height as u64) < required_lines + || (fb_info.width as u64) < required_width + { + warn!( + "redox-drm: PAGE_FLIP FB {}x{} pitch={} too small for active mode {}x{}", + fb_info.width, fb_info.height, fb_info.pitch, + active_mode.hdisplay, active_mode.vdisplay + ); + return Err(Error::new(EINVAL)); + } + } + let gem_handle = fb_info.gem_handle; + let seqno = self + .driver + .page_flip(req.crtc_id, gem_handle, req.flags) + .map_err(driver_to_syscall)?; + let current_vblank = self.driver.get_vblank(req.crtc_id).unwrap_or(0); + let prev = self.active_crtc_fb.insert(req.crtc_id, req.fb_handle); + if let Some(old_fb) = prev { + if old_fb != req.fb_handle { + self.pending_flip_fb + .insert(req.crtc_id, (current_vblank.saturating_add(1), old_fb)); + } + } + seqno.to_le_bytes().to_vec() + } + + DRM_IOCTL_MODE_CREATE_DUMB => { + let mut req = decode_wire::(payload)?; + let pitch = (req.width.saturating_mul(req.bpp).saturating_add(7)) / 8; + req.pitch = pitch; + req.size = (pitch as u64).saturating_mul(req.height as u64); + self.validate_gem_create_size(req.size, "CREATE_DUMB")?; + req.handle = self + .driver + .gem_create(req.size) + .map_err(driver_to_syscall)?; + if let Some(handle) = self.handles.get_mut(&id) { + handle.owned_gems.push(req.handle); + } + bytes_of(&req) + } + + DRM_IOCTL_MODE_MAP_DUMB => { + let mut req = decode_wire::(payload)?; + let owned = self + .handles + .get(&id) + .map(|h| Self::handle_has_gem_ref(h, req.handle)) + .unwrap_or(false); + if !owned { + warn!( + "redox-drm: MAP_DUMB handle {} not owned by this fd", + req.handle + ); + return Err(Error::new(EBADF)); + } + if let Some(handle) = self.handles.get(&id) { + if handle.mapped_gem_refs != 0 && handle.mapped_gem != Some(req.handle) { + warn!( + "redox-drm: MAP_DUMB handle {} rejected — another GEM is still mapped", + req.handle + ); + return Err(Error::new(EBUSY)); + } + } + req.offset = self + .driver + .gem_mmap(req.handle) + .map_err(driver_to_syscall)? as u64; + if let Some(handle) = self.handles.get_mut(&id) { + handle.mapped_gem = Some(req.handle); + } + bytes_of(&req) + } + + DRM_IOCTL_MODE_DESTROY_DUMB => { + let req = decode_wire::(payload)?; + let owned = self + .handles + .get(&id) + .map(|h| h.owned_gems.contains(&req.handle)) + .unwrap_or(false); + if !owned { + warn!( + "redox-drm: DESTROY_DUMB handle {} not owned by this fd", + req.handle + ); + return Err(Error::new(EBADF)); + } + let backs_fb = self + .fb_registry + .values() + .any(|info| info.gem_handle == req.handle); + if backs_fb { + warn!( + "redox-drm: DESTROY_DUMB handle {} rejected — backs an active framebuffer", + req.handle + ); + return Err(Error::new(EBUSY)); + } + if self.gem_is_mapped(req.handle) { + warn!( + "redox-drm: DESTROY_DUMB handle {} rejected — still mapped", + req.handle + ); + return Err(Error::new(EBUSY)); + } + let close_now = !self.gem_has_other_refs(id, req.handle) + && self.gem_export_refcount(req.handle) == 0; + if close_now { + self.driver + .gem_close(req.handle) + .map_err(driver_to_syscall)?; + self.prime_exports.retain(|_, &mut h| h != req.handle); + } + if let Some(handle) = self.handles.get_mut(&id) { + handle.owned_gems.retain(|&h| h != req.handle); + handle.imported_gems.remove(&req.handle); + } + Vec::new() + } + + DRM_IOCTL_MODE_GETENCODER => { + let _req = decode_wire::(payload)?; + let resp = DrmGetEncoderWire { + encoder_id: _req.encoder_id, + encoder_type: 0, + crtc_id: 1, + possible_crtcs: 1, + possible_clones: 0, + }; + bytes_of(&resp) + } + + DRM_IOCTL_MODE_GETCRTC => { + let req = decode_wire::(payload)?; + let (fb_id, mode_valid, mode) = match ( + self.active_crtc_fb.get(&req.crtc_id), + self.active_crtc_mode.get(&req.crtc_id), + ) { + (Some(&fb), Some(m)) if self.fb_registry.contains_key(&fb) => { + (fb, 1u32, mode_to_wire(m)) + } + _ => (0u32, 0u32, DrmModeWire::default()), + }; + let resp = DrmGetCrtcWire { + crtc_id: req.crtc_id, + fb_id, + x: 0, + y: 0, + mode_valid, + mode, + }; + bytes_of(&resp) + } + + DRM_IOCTL_MODE_ADDFB => { + let req = decode_wire::(payload)?; + if req.handle == 0 { + return Err(Error::new(EINVAL)); + } + if req.width == 0 || req.height == 0 || req.bpp == 0 { + warn!( + "redox-drm: ADDFB zero dimension width={} height={} bpp={}", + req.width, req.height, req.bpp + ); + return Err(Error::new(EINVAL)); + } + let min_stride = (req.width.saturating_mul(req.bpp).saturating_add(7)) / 8; + let pitch = if req.pitch != 0 { + req.pitch + } else { + min_stride + }; + if pitch == 0 || pitch < min_stride { + warn!( + "redox-drm: ADDFB pitch {} below minimum stride {} ({}x{})", + pitch, min_stride, req.width, req.bpp + ); + return Err(Error::new(EINVAL)); + } + let required_size = match (pitch as u64).checked_mul(req.height as u64) { + Some(s) => s, + None => { + warn!( + "redox-drm: ADDFB pitch * height overflows pitch={} height={}", + pitch, req.height + ); + return Err(Error::new(EINVAL)); + } + }; + let owned = self + .handles + .get(&id) + .map(|h| Self::handle_has_gem_ref(h, req.handle)) + .unwrap_or(false); + if !owned { + warn!( + "redox-drm: ADDFB handle {} not owned by this fd", + req.handle + ); + return Err(Error::new(EBADF)); + } + let actual_size = self.driver.gem_size(req.handle).map_err(|e| { + warn!("redox-drm: ADDFB handle {} not found: {}", req.handle, e); + Error::new(ENOENT) + })?; + if required_size > actual_size { + warn!( + "redox-drm: ADDFB requires {} bytes but GEM {} is {} bytes", + required_size, req.handle, actual_size + ); + return Err(Error::new(EINVAL)); + } + let fb_id = self.next_fb_id; + self.next_fb_id = self.next_fb_id.saturating_add(1); + self.fb_registry.insert( + fb_id, + FbInfo { + gem_handle: req.handle, + width: req.width, + height: req.height, + pitch, + bpp: req.bpp, + }, + ); + if let Some(handle) = self.handles.get_mut(&id) { + handle.owned_fbs.push(fb_id); + } + let mut resp = req; + resp.fb_id = fb_id; + bytes_of(&resp) + } + + DRM_IOCTL_MODE_RMFB => { + let req = decode_wire::(payload)?; + let owned = self + .handles + .get(&id) + .map(|h| h.owned_fbs.contains(&req.fb_id)) + .unwrap_or(false); + if !owned { + warn!("redox-drm: RMFB {} not owned by this fd", req.fb_id); + return Err(Error::new(EBADF)); + } + let in_use = self.is_fb_active(req.fb_id); + if in_use { + warn!( + "redox-drm: RMFB {} rejected — still active on a CRTC", + req.fb_id + ); + return Err(Error::new(EBUSY)); + } + if let Some(fb_info) = self.fb_registry.remove(&req.fb_id) { + let _ = self.maybe_close_gem(fb_info.gem_handle, "RMFB"); + } + if let Some(handle) = self.handles.get_mut(&id) { + handle.owned_fbs.retain(|&fb| fb != req.fb_id); + } + Vec::new() + } + + DRM_IOCTL_GET_CAP => { + let mut req = decode_wire::(payload)?; + req.value = match req.capability { + 0 => 1, + 1 => 1, + _ => 0, + }; + bytes_of(&req) + } + + DRM_IOCTL_SET_CLIENT_CAP => Vec::new(), + + DRM_IOCTL_VERSION => { + let resp = DrmVersionWire { + major: 1, + minor: 0, + patch: 0, + }; + bytes_of(&resp) + } + + DRM_IOCTL_GEM_CREATE => { + let mut req = decode_wire::(payload)?; + self.validate_gem_create_size(req.size, "GEM_CREATE")?; + req.handle = self + .driver + .gem_create(req.size) + .map_err(driver_to_syscall)?; + if let Some(handle) = self.handles.get_mut(&id) { + handle.owned_gems.push(req.handle); + } + bytes_of(&req) + } + + DRM_IOCTL_GEM_CLOSE => { + let req = decode_wire::(payload)?; + let owned = self + .handles + .get(&id) + .map(|h| h.owned_gems.contains(&req.handle)) + .unwrap_or(false); + if !owned { + warn!( + "redox-drm: GEM_CLOSE handle {} not owned by this fd", + req.handle + ); + return Err(Error::new(EBADF)); + } + let backs_fb = self + .fb_registry + .values() + .any(|info| info.gem_handle == req.handle); + if backs_fb { + warn!( + "redox-drm: GEM_CLOSE handle {} rejected — backs an active framebuffer", + req.handle + ); + return Err(Error::new(EBUSY)); + } + if self.gem_is_mapped(req.handle) { + warn!( + "redox-drm: GEM_CLOSE handle {} rejected — still mapped", + req.handle + ); + return Err(Error::new(EBUSY)); + } + let close_now = !self.gem_has_other_refs(id, req.handle) + && self.gem_export_refcount(req.handle) == 0; + if close_now { + self.driver + .gem_close(req.handle) + .map_err(driver_to_syscall)?; + self.prime_exports.retain(|_, &mut h| h != req.handle); + } + if let Some(handle) = self.handles.get_mut(&id) { + handle.owned_gems.retain(|&h| h != req.handle); + handle.imported_gems.remove(&req.handle); + } + Vec::new() + } + + DRM_IOCTL_GEM_MMAP => { + let mut req = decode_wire::(payload)?; + let owned = self + .handles + .get(&id) + .map(|h| Self::handle_has_gem_ref(h, req.handle)) + .unwrap_or(false); + if !owned { + warn!( + "redox-drm: GEM_MMAP handle {} not owned by this fd", + req.handle + ); + return Err(Error::new(EBADF)); + } + if let Some(handle) = self.handles.get(&id) { + if handle.mapped_gem_refs != 0 && handle.mapped_gem != Some(req.handle) { + warn!( + "redox-drm: GEM_MMAP handle {} rejected — another GEM is still mapped", + req.handle + ); + return Err(Error::new(EBUSY)); + } + } + req.offset = self + .driver + .gem_mmap(req.handle) + .map_err(driver_to_syscall)? as u64; + if let Some(handle) = self.handles.get_mut(&id) { + handle.mapped_gem = Some(req.handle); + } + bytes_of(&req) + } + + DRM_IOCTL_REDOX_AMD_SDMA_SUBMIT => { + let mut req = decode_wire::(payload)?; + if req.flags != 0 { + warn!( + "redox-drm: AMD SDMA submit rejected — unsupported flags {:#x}", + req.flags + ); + return Err(Error::new(EINVAL)); + } + if req.size == 0 { + warn!("redox-drm: AMD SDMA submit rejected — zero-sized copy"); + return Err(Error::new(EINVAL)); + } + + self.validate_private_cs_handles( + id, + req.src_handle, + req.dst_handle, + "AMD SDMA submit", + )?; + + let submit = RedoxPrivateCsSubmit { + src_handle: req.src_handle, + dst_handle: req.dst_handle, + src_offset: req.src_offset, + dst_offset: req.dst_offset, + byte_count: req.size, + }; + self.validate_private_cs_ranges(&submit, "AMD SDMA submit")?; + req.seqno = self + .driver + .redox_private_cs_submit(&submit) + .map_err(driver_to_syscall)? + .seqno; + bytes_of(&req) + } + + DRM_IOCTL_REDOX_AMD_SDMA_WAIT => { + let mut req = decode_wire::(payload)?; + if req.flags != 0 { + warn!( + "redox-drm: AMD SDMA wait rejected — unsupported flags {:#x}", + req.flags + ); + return Err(Error::new(EINVAL)); + } + + let result = self + .driver + .redox_private_cs_wait(&RedoxPrivateCsWait { + seqno: req.seqno, + timeout_ns: req.timeout_ns, + }) + .map_err(driver_to_syscall)?; + req.completed = u32::from(result.completed); + req.completed_seqno = result.completed_seqno; + bytes_of(&req) + } + + DRM_IOCTL_PRIME_HANDLE_TO_FD => { + let req = decode_wire::(payload)?; + let owned = self + .handles + .get(&id) + .map(|h| Self::handle_has_gem_ref(h, req.handle)) + .unwrap_or(false); + if !owned { + warn!( + "redox-drm: PRIME_HANDLE_TO_FD handle {} not owned by this fd", + req.handle + ); + return Err(Error::new(EBADF)); + } + + let token = self.allocate_export_token()?; + self.prime_exports.insert(token, req.handle); + + let resp = DrmPrimeHandleToFdResponseWire { + fd: token as i32, + _pad: 0, + }; + bytes_of(&resp) + } + + DRM_IOCTL_PRIME_FD_TO_HANDLE => { + let req = decode_wire::(payload)?; + let token = if req.fd >= 0 { + req.fd as u32 + } else { + warn!("redox-drm: PRIME_FD_TO_HANDLE invalid token {}", req.fd); + return Err(Error::new(EBADF)); + }; + + // The token comes from fpath() on the dmabuf fd, which embeds + // the opaque export token (not the raw GEM handle). + let gem_handle = match self.prime_exports.get(&token).copied() { + Some(h) => h, + None => { + warn!("redox-drm: PRIME_FD_TO_HANDLE token {} not found", token); + return Err(Error::new(ENOENT)); + } + }; + + // Verify the GEM is still live — the exporter may have closed it + // before any dmabuf fd was opened, leaving a stale token. + self.driver.gem_size(gem_handle).map_err(|_| { + warn!( + "redox-drm: PRIME_FD_TO_HANDLE token {} maps to dead GEM {}", + token, gem_handle + ); + // Clean up the stale token so future calls fail fast. + self.prime_exports.remove(&token); + Error::new(ENOENT) + })?; + + let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?; + if !handle.owned_gems.contains(&gem_handle) { + handle.owned_gems.push(gem_handle); + handle.imported_gems.insert(gem_handle); + } + + let resp = DrmPrimeFdToHandleResponseWire { + handle: gem_handle, + _pad: 0, + }; + bytes_of(&resp) + } + + DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT => { + let req = decode_wire::(payload)?; + self.validate_private_cs_handles( + id, + req.src_handle, + req.dst_handle, + "private CS submit", + )?; + self.validate_private_cs_ranges(&req, "private CS submit")?; + let resp: RedoxPrivateCsSubmitResult = self + .driver + .redox_private_cs_submit(&req) + .map_err(driver_to_syscall)?; + bytes_of(&resp) + } + + DRM_IOCTL_REDOX_PRIVATE_CS_WAIT => { + let req = decode_wire::(payload)?; + let resp: RedoxPrivateCsWaitResult = self + .driver + .redox_private_cs_wait(&req) + .map_err(driver_to_syscall)?; + bytes_of(&resp) + } + + _ => { + warn!("redox-drm: unsupported ioctl {:#x}", request); + return Err(Error::new(EOPNOTSUPP)); + } + }; + + let response = if response.is_empty() { + vec![0] + } else { + response + }; + + let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?; + let len = response.len(); + handle.response = response; + Ok(len) + } +} + +// ---- SchemeSync implementation ---- + +impl SchemeSync for DrmScheme { + fn scheme_root(&mut self) -> Result { + Ok(self.allocate_handle(NodeKind::Root)) + } + + fn openat( + &mut self, + _fd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let node = match path.trim_matches('/') { + "card0" => NodeKind::Card, + p if p.starts_with("card0Connector/") => { + let tail = p.trim_start_matches("card0Connector/"); + let connector_id = tail.parse::().map_err(|_| Error::new(ENOENT))?; + NodeKind::Connector(connector_id) + } + p if p.starts_with("card0/dmabuf/") => { + let tail = p.trim_start_matches("card0/dmabuf/"); + let token = tail.parse::().map_err(|_| Error::new(ENOENT))?; + let gem_handle = match self.prime_exports.get(&token).copied() { + Some(h) => h, + None => return Err(Error::new(ENOENT)), + }; + self.driver.gem_size(gem_handle).map_err(|_| { + warn!( + "redox-drm: open dmabuf token {} maps to dead GEM {}", + token, gem_handle + ); + self.prime_exports.remove(&token); + Error::new(ENOENT) + })?; + NodeKind::DmaBuf { + gem_handle, + export_token: token, + } + } + _ => return Err(Error::new(ENOENT)), + }; + + if let NodeKind::DmaBuf { gem_handle, .. } = &node { + self.bump_export_ref(*gem_handle); + } + + let id = self.allocate_handle(node); + Ok(OpenResult::ThisScheme { + number: id, + flags: NewFdFlags::empty(), + }) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + _offset: u64, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?; + if matches!(handle.node, NodeKind::Root) { + return Ok(0); + } + if !handle.response.is_empty() { + let len = handle.response.len().min(buf.len()); + buf[..len].copy_from_slice(&handle.response[..len]); + return Ok(len); + } + + if let Some(event) = handle.event_queue.pop_front() { + let len = event.len().min(buf.len()); + buf[..len].copy_from_slice(&event[..len]); + return Ok(len); + } + + Ok(0) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _offset: u64, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let (request_bytes, payload) = match buf.split_first_chunk::<8>() { + Some(pair) => pair, + None => { + let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + return Ok(0); + } + }; + let request = usize::from_le_bytes(*request_bytes); + let written = self.handle_ioctl(id, request, payload)?; + Ok(written) + } + + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { + let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + let path = match handle.node { + NodeKind::Root => "drm:".to_string(), + NodeKind::Card => "drm:card0".to_string(), + NodeKind::Connector(cid) => format!("drm:card0Connector/{cid}"), + NodeKind::DmaBuf { export_token, .. } => format!("drm:card0/dmabuf/{export_token}"), + }; + let bytes = path.as_bytes(); + let len = bytes.len().min(buf.len()); + buf[..len].copy_from_slice(&bytes[..len]); + Ok(len) + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> { + let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + stat.st_mode = MODE_FILE | 0o666; + stat.st_size = if matches!(handle.node, NodeKind::Root) { + 0 + } else if !handle.response.is_empty() { + handle.response.len() as u64 + } else { + handle + .event_queue + .front() + .map(|payload| payload.len()) + .unwrap_or(0) as u64 + }; + stat.st_blksize = 4096; + Ok(()) + } + + fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> { + let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + warn!("redox-drm: fsync rejected — shared core has no implicit render-fence sync contract"); + Err(Error::new(EOPNOTSUPP)) + } + + fn fevent(&mut self, id: usize, flags: EventFlags, _ctx: &CallerCtx) -> Result { + let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + let readiness = if handle.event_queue.is_empty() { + EventFlags::empty() + } else { + flags & EventFlags::EVENT_READ + }; + Ok(readiness) + } + + fn on_close(&mut self, id: usize) { + let Some(handle) = self.handles.get(&id) else { + return; + }; + let mapped = handle.mapped_gem_refs; + if mapped != 0 { + let Some(handle) = self.handles.get_mut(&id) else { + return; + }; + handle.closing = true; + return; + } + + if let Some(handle) = self.handles.remove(&id) { + self.finalize_handle_close(handle); + } + } + + fn mmap_prep( + &mut self, + id: usize, + offset: u64, + size: usize, + _flags: MapFlags, + _ctx: &CallerCtx, + ) -> Result { + let gem_handle = { + let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + match handle.node { + NodeKind::DmaBuf { gem_handle, .. } => gem_handle, + _ => handle.mapped_gem.ok_or_else(|| Error::new(EINVAL))?, + } + }; + + let gem_size = self + .driver + .gem_size(gem_handle) + .map_err(driver_to_syscall)?; + + if offset > gem_size { + return Err(Error::new(EINVAL)); + } + let remaining = gem_size - offset; + if size as u64 > remaining { + return Err(Error::new(EINVAL)); + } + + let base_addr = self + .driver + .gem_mmap(gem_handle) + .map_err(driver_to_syscall)?; + let addr = base_addr + offset as usize; + self.pin_mapped_gem(id, gem_handle)?; + debug!( + "redox-drm: mmap_prep GEM handle {} offset={} size={} at addr={:#x}", + gem_handle, offset, size, addr + ); + Ok(addr) + } + + fn munmap( + &mut self, + id: usize, + offset: u64, + size: usize, + _flags: MunmapFlags, + _ctx: &CallerCtx, + ) -> Result<()> { + let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?; + self.unpin_mapped_gem(id)?; + debug!( + "redox-drm: munmap id={} offset={} size={}", + id, offset, size + ); + let should_finalize = self + .handles + .get(&id) + .map(|handle| handle.closing && handle.mapped_gem_refs == 0) + .unwrap_or(false); + if should_finalize { + if let Some(handle) = self.handles.remove(&id) { + self.finalize_handle_close(handle); + } + } + Ok(()) + } +} + +// ---- Conversion helpers ---- + +fn connector_type_to_u32(ct: crate::kms::ConnectorType) -> u32 { + match ct { + crate::kms::ConnectorType::Unknown => 0, + crate::kms::ConnectorType::VGA => 1, + crate::kms::ConnectorType::DVII => 2, + crate::kms::ConnectorType::DVID => 3, + crate::kms::ConnectorType::DVIA => 4, + crate::kms::ConnectorType::Composite => 5, + crate::kms::ConnectorType::SVideo => 6, + crate::kms::ConnectorType::LVDS => 7, + crate::kms::ConnectorType::Component => 8, + crate::kms::ConnectorType::NinePinDIN => 9, + crate::kms::ConnectorType::DisplayPort => 10, + crate::kms::ConnectorType::HDMIA => 11, + crate::kms::ConnectorType::HDMIB => 12, + crate::kms::ConnectorType::TV => 13, + crate::kms::ConnectorType::EDP => 14, + crate::kms::ConnectorType::Virtual => 15, + } +} + +fn mode_to_wire(mode: &ModeInfo) -> DrmModeWire { + DrmModeWire { + clock: mode.clock, + hdisplay: mode.hdisplay, + hsync_start: mode.hsync_start, + hsync_end: mode.hsync_end, + htotal: mode.htotal, + hskew: mode.hskew, + vdisplay: mode.vdisplay, + vsync_start: mode.vsync_start, + vsync_end: mode.vsync_end, + vtotal: mode.vtotal, + vscan: mode.vscan, + vrefresh: mode.vrefresh, + flags: mode.flags, + type_: mode.type_, + } +} + +fn wire_to_mode(w: &DrmModeWire) -> ModeInfo { + ModeInfo { + clock: w.clock, + hdisplay: w.hdisplay, + hsync_start: w.hsync_start, + hsync_end: w.hsync_end, + htotal: w.htotal, + hskew: w.hskew, + vdisplay: w.vdisplay, + vsync_start: w.vsync_start, + vsync_end: w.vsync_end, + vtotal: w.vtotal, + vscan: w.vscan, + vrefresh: w.vrefresh, + flags: w.flags, + type_: w.type_, + name: format!("{}x{}@{}", w.hdisplay, w.vdisplay, w.vrefresh), + } +} + +fn encode_modes(modes: &[ModeInfo]) -> Vec { + let mut out = Vec::new(); + for mode in modes { + out.extend_from_slice(&bytes_of(&mode_to_wire(mode))); + out.extend_from_slice(mode.name.as_bytes()); + out.push(0); + } + if out.is_empty() { + out.push(0); + } + out +} + +fn bytes_of(value: &T) -> Vec { + let ptr = value as *const T as *const u8; + let len = size_of::(); + unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec() +} + +fn read_u32(buf: &[u8], offset: usize) -> Result { + let end = offset.saturating_add(size_of::()); + let bytes = buf.get(offset..end).ok_or_else(|| Error::new(EINVAL))?; + let array: [u8; 4] = bytes.try_into().map_err(|_| Error::new(EINVAL))?; + Ok(u32::from_le_bytes(array)) +} + +fn decode_wire(buf: &[u8]) -> Result { + if buf.len() < size_of::() { + return Err(Error::new(EINVAL)); + } + let ptr = buf.as_ptr() as *const T; + Ok(unsafe { ptr.read_unaligned() }) +} + +fn driver_to_syscall(error: crate::driver::DriverError) -> Error { + warn!("redox-drm: driver error: {}", error); + match error { + crate::driver::DriverError::Unsupported(_) => Error::new(EOPNOTSUPP), + crate::driver::DriverError::InvalidArgument(_) => Error::new(EINVAL), + crate::driver::DriverError::NotFound(_) => Error::new(ENOENT), + crate::driver::DriverError::Initialization(_) + | crate::driver::DriverError::Mmio(_) + | crate::driver::DriverError::Pci(_) + | crate::driver::DriverError::Buffer(_) + | crate::driver::DriverError::Io(_) => Error::new(EINVAL), + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + use std::sync::{Arc, Mutex}; + + use redox_scheme::SchemeBlockMut; + + use super::*; + use crate::driver::{DriverError, DriverEvent, GpuDriver}; + use crate::kms::{ConnectorInfo, ModeInfo}; + + #[derive(Default)] + struct FakeDriverState { + next_handle: GemHandle, + gem_sizes: BTreeMap, + submit_calls: usize, + } + + struct FakeDriver { + state: Mutex, + support_private_cs: bool, + } + + impl FakeDriver { + fn new(support_private_cs: bool) -> Self { + Self { + state: Mutex::new(FakeDriverState { + next_handle: 1, + ..FakeDriverState::default() + }), + support_private_cs, + } + } + + fn submit_calls(&self) -> usize { + self.state.lock().unwrap().submit_calls + } + } + + impl GpuDriver for FakeDriver { + fn driver_name(&self) -> &str { + "fake" + } + + fn driver_desc(&self) -> &str { + "fake" + } + + fn driver_date(&self) -> &str { + "1970-01-01" + } + + fn detect_connectors(&self) -> Vec { + Vec::new() + } + + fn get_modes(&self, _connector_id: u32) -> Vec { + Vec::new() + } + + fn set_crtc( + &self, + _crtc_id: u32, + _fb_handle: u32, + _connectors: &[u32], + _mode: &ModeInfo, + ) -> crate::driver::Result<()> { + Ok(()) + } + + fn page_flip( + &self, + _crtc_id: u32, + _fb_handle: u32, + _flags: u32, + ) -> crate::driver::Result { + Ok(0) + } + + fn get_vblank(&self, _crtc_id: u32) -> crate::driver::Result { + Ok(0) + } + + fn gem_create(&self, size: u64) -> crate::driver::Result { + let mut state = self.state.lock().unwrap(); + let handle = state.next_handle; + state.next_handle = state.next_handle.saturating_add(1); + state.gem_sizes.insert(handle, size); + Ok(handle) + } + + fn gem_close(&self, handle: GemHandle) -> crate::driver::Result<()> { + let removed = self.state.lock().unwrap().gem_sizes.remove(&handle); + if removed.is_some() { + Ok(()) + } else { + Err(DriverError::NotFound(format!( + "unknown GEM handle {handle}" + ))) + } + } + + fn gem_mmap(&self, handle: GemHandle) -> crate::driver::Result { + if self.state.lock().unwrap().gem_sizes.contains_key(&handle) { + Ok((handle as usize).saturating_mul(4096)) + } else { + Err(DriverError::NotFound(format!( + "unknown GEM handle {handle}" + ))) + } + } + + fn gem_size(&self, handle: GemHandle) -> crate::driver::Result { + self.state + .lock() + .unwrap() + .gem_sizes + .get(&handle) + .copied() + .ok_or_else(|| DriverError::NotFound(format!("unknown GEM handle {handle}"))) + } + + fn get_edid(&self, _connector_id: u32) -> Vec { + Vec::new() + } + + fn handle_irq(&self) -> crate::driver::Result> { + Ok(None) + } + + fn redox_private_cs_submit( + &self, + _submit: &RedoxPrivateCsSubmit, + ) -> crate::driver::Result { + if !self.support_private_cs { + return Err(DriverError::Unsupported( + "private command submission is unavailable on this backend", + )); + } + + let mut state = self.state.lock().unwrap(); + state.submit_calls = state.submit_calls.saturating_add(1); + Ok(RedoxPrivateCsSubmitResult { seqno: 7 }) + } + } + + fn open_card(scheme: &mut DrmScheme) -> usize { + scheme.open("card0", 0, 0, 0).unwrap().unwrap() + } + + fn open_connector(scheme: &mut DrmScheme, connector_id: u32) -> usize { + scheme + .open(&format!("card0Connector/{connector_id}"), 0, 0, 0) + .unwrap() + .unwrap() + } + + fn write_ioctl( + scheme: &mut DrmScheme, + id: usize, + request: usize, + payload: &T, + ) -> Result { + let mut buf = request.to_le_bytes().to_vec(); + buf.extend_from_slice(&bytes_of(payload)); + scheme.write(id, &buf).map(|written| written.unwrap_or(0)) + } + + fn read_response(scheme: &mut DrmScheme, id: usize) -> T { + let mut buf = vec![0; size_of::()]; + let len = scheme.read(id, &mut buf).unwrap().unwrap(); + assert_eq!(len, size_of::()); + decode_wire::(&buf).unwrap() + } + + #[test] + fn private_cs_submit_rejects_imported_dma_buf_handles() { + let driver = Arc::new(FakeDriver::new(true)); + let mut scheme = DrmScheme::new(driver.clone()); + + let exporter = open_card(&mut scheme); + let importer = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, exporter, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, exporter); + + let export = DrmPrimeHandleToFdWire { + handle: created.handle, + flags: 0, + }; + write_ioctl(&mut scheme, exporter, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export).unwrap(); + let exported = read_response::(&mut scheme, exporter); + + let import = DrmPrimeFdToHandleWire { + fd: exported.fd, + _pad: 0, + }; + write_ioctl(&mut scheme, importer, DRM_IOCTL_PRIME_FD_TO_HANDLE, &import).unwrap(); + let imported = read_response::(&mut scheme, importer); + + let submit = RedoxPrivateCsSubmit { + src_handle: imported.handle, + dst_handle: imported.handle, + src_offset: 0, + dst_offset: 0, + byte_count: 64, + }; + let err = write_ioctl( + &mut scheme, + importer, + DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT, + &submit, + ) + .unwrap_err(); + + assert_eq!(err.errno, EOPNOTSUPP); + assert_eq!(driver.submit_calls(), 0); + } + + #[test] + fn prime_handle_to_fd_returns_distinct_nonzero_tokens() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + for _ in 0..2 { + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let _ = read_response::(&mut scheme, card); + } + + let handles = scheme.handles.get(&card).unwrap().owned_gems.clone(); + + let export_a = DrmPrimeHandleToFdWire { + handle: handles[0], + flags: 0, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export_a).unwrap(); + let token_a = read_response::(&mut scheme, card).fd; + + let export_b = DrmPrimeHandleToFdWire { + handle: handles[1], + flags: 0, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export_b).unwrap(); + let token_b = read_response::(&mut scheme, card).fd; + + assert_ne!(token_a, 0); + assert_ne!(token_b, 0); + assert_ne!(token_a, token_b); + } + + #[test] + fn private_cs_wait_is_explicitly_unsupported_without_backend_support() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + let wait = RedoxPrivateCsWait { + seqno: 1, + timeout_ns: 0, + }; + + let err = + write_ioctl(&mut scheme, card, DRM_IOCTL_REDOX_PRIVATE_CS_WAIT, &wait).unwrap_err(); + + assert_eq!(err.errno, EOPNOTSUPP); + } + + #[test] + fn fsync_is_not_a_fake_render_sync_success() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let err = scheme.fsync(card).unwrap_err(); + + assert_eq!(err.errno, EOPNOTSUPP); + } + + #[test] + fn private_cs_submit_still_reaches_backend_for_local_gems() { + let driver = Arc::new(FakeDriver::new(true)); + let mut scheme = DrmScheme::new(driver.clone()); + let card = open_card(&mut scheme); + + for _ in 0..2 { + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let _ = read_response::(&mut scheme, card); + } + + let handles = match scheme.handles.get(&card) { + Some(handle) => handle.owned_gems.clone(), + None => panic!("missing fake card handle"), + }; + let submit = RedoxPrivateCsSubmit { + src_handle: handles[0], + dst_handle: handles[1], + src_offset: 0, + dst_offset: 0, + byte_count: 128, + }; + + write_ioctl( + &mut scheme, + card, + DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT, + &submit, + ) + .unwrap(); + let response = read_response::(&mut scheme, card); + + assert_eq!(response.seqno, 7); + assert_eq!(driver.submit_calls(), 1); + } + + #[test] + fn private_cs_submit_rejects_out_of_bounds_ranges() { + let driver = Arc::new(FakeDriver::new(true)); + let mut scheme = DrmScheme::new(driver.clone()); + let card = open_card(&mut scheme); + + for _ in 0..2 { + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let _ = read_response::(&mut scheme, card); + } + + let handles = scheme.handles.get(&card).unwrap().owned_gems.clone(); + let submit = RedoxPrivateCsSubmit { + src_handle: handles[0], + dst_handle: handles[1], + src_offset: 4090, + dst_offset: 0, + byte_count: 64, + }; + + let err = write_ioctl( + &mut scheme, + card, + DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT, + &submit, + ) + .unwrap_err(); + + assert_eq!(err.errno, EINVAL); + assert_eq!(driver.submit_calls(), 0); + } + + #[test] + fn vblank_driver_event_retires_pending_page_flip() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + scheme.fb_registry.insert( + 7, + FbInfo { + gem_handle: 41, + width: 0, + height: 0, + pitch: 0, + bpp: 0, + }, + ); + scheme.pending_flip_fb.insert(3, (5, 7)); + + scheme.handle_driver_event(DriverEvent::Vblank { + crtc_id: 3, + count: 5, + }); + + assert!(!scheme.pending_flip_fb.contains_key(&3)); + assert!(!scheme.fb_registry.contains_key(&7)); + } + + #[test] + fn non_vblank_driver_event_does_not_retire_pending_page_flip() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + scheme.fb_registry.insert( + 9, + FbInfo { + gem_handle: 99, + width: 0, + height: 0, + pitch: 0, + bpp: 0, + }, + ); + scheme.pending_flip_fb.insert(1, (2, 9)); + + scheme.handle_driver_event(DriverEvent::Hotplug { connector_id: 1 }); + + assert_eq!(scheme.pending_flip_fb.get(&1), Some(&(2, 9))); + assert!(scheme.fb_registry.contains_key(&9)); + assert_eq!( + scheme.fevent(card, EventFlags::EVENT_READ).unwrap(), + Some(EventFlags::EVENT_READ) + ); + } + + #[test] + fn hotplug_event_is_readable_from_card_handle() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + scheme.handle_driver_event(DriverEvent::Hotplug { connector_id: 7 }); + + assert_eq!( + scheme.fevent(card, EventFlags::EVENT_READ).unwrap(), + Some(EventFlags::EVENT_READ) + ); + + let mut buf = [0u8; 32]; + let len = scheme.read(card, &mut buf).unwrap().unwrap(); + assert_eq!(&buf[..len], b"hotplug:7\n"); + assert_eq!( + scheme.fevent(card, EventFlags::EVENT_READ).unwrap(), + Some(EventFlags::empty()) + ); + } + + #[test] + fn hotplug_event_targets_matching_connector_handle_only() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let connector_a = open_connector(&mut scheme, 1); + let connector_b = open_connector(&mut scheme, 2); + + scheme.handle_driver_event(DriverEvent::Hotplug { connector_id: 2 }); + + assert_eq!( + scheme.fevent(connector_a, EventFlags::EVENT_READ).unwrap(), + Some(EventFlags::empty()) + ); + assert_eq!( + scheme.fevent(connector_b, EventFlags::EVENT_READ).unwrap(), + Some(EventFlags::EVENT_READ) + ); + } + + #[test] + fn vblank_event_is_readable_from_card_handle() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + scheme.handle_driver_event(DriverEvent::Vblank { + crtc_id: 4, + count: 12, + }); + + assert_eq!( + scheme.fevent(card, EventFlags::EVENT_READ).unwrap(), + Some(EventFlags::EVENT_READ) + ); + + let mut buf = [0u8; 32]; + let len = scheme.read(card, &mut buf).unwrap().unwrap(); + assert_eq!(&buf[..len], b"vblank:4:12\n"); + assert_eq!( + scheme.fevent(card, EventFlags::EVENT_READ).unwrap(), + Some(EventFlags::empty()) + ); + } + + #[test] + fn gem_create_rejects_oversized_allocations() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + let create = DrmGemCreateWire { + size: MAX_SCHEME_GEM_BYTES + 1, + ..DrmGemCreateWire::default() + }; + + let err = write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap_err(); + + assert_eq!(err.errno, EINVAL); + } + + #[test] + fn create_dumb_rejects_oversized_allocations() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + let create = DrmCreateDumbWire { + width: 16384, + height: 16384, + bpp: 32, + ..DrmCreateDumbWire::default() + }; + + let err = write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_CREATE_DUMB, &create).unwrap_err(); + + assert_eq!(err.errno, EINVAL); + } + + #[test] + fn gem_has_other_refs_returns_false_when_only_current_handle_owns_gem() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + + let gem_handle = created.handle; + assert!( + !scheme.gem_has_other_refs(card, gem_handle), + "only one handle owns the GEM, so gem_has_other_refs should be false" + ); + } + + #[test] + fn gem_has_other_refs_returns_true_when_another_handle_owns_gem() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card_a = open_card(&mut scheme); + let card_b = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card_a, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card_a); + let gem_handle = created.handle; + + let export = DrmPrimeHandleToFdWire { + handle: gem_handle, + flags: 0, + }; + write_ioctl(&mut scheme, card_a, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export).unwrap(); + let exported = read_response::(&mut scheme, card_a); + + let import = DrmPrimeFdToHandleWire { + fd: exported.fd, + _pad: 0, + }; + write_ioctl(&mut scheme, card_b, DRM_IOCTL_PRIME_FD_TO_HANDLE, &import).unwrap(); + let imported = read_response::(&mut scheme, card_b); + + assert!( + scheme.gem_has_other_refs(card_a, imported.handle), + "card_b owns the same GEM, so gem_has_other_refs from card_a should be true" + ); + } + + #[test] + fn gem_is_mapped_returns_false_initially() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + + assert!( + !scheme.gem_is_mapped(created.handle), + "freshly created GEM should not be mapped" + ); + } + + #[test] + fn gem_is_mapped_returns_true_after_pin() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + scheme.pin_mapped_gem(card, gem_handle).unwrap(); + + assert!( + scheme.gem_is_mapped(gem_handle), + "GEM should be mapped after pin_mapped_gem" + ); + } + + #[test] + fn gem_export_refcount_starts_at_zero() { + let scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + assert_eq!( + scheme.gem_export_refcount(9999), + 0, + "unknown GEM should have refcount 0" + ); + } + + #[test] + fn bump_export_ref_increments_from_zero_to_one() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.bump_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + 1, + "bumping an unknown GEM should set its refcount to 1" + ); + } + + #[test] + fn bump_export_ref_saturates_on_overflow() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.gem_export_refs.insert(gem_handle, usize::MAX); + + scheme.bump_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + usize::MAX, + "saturating add should keep refcount at usize::MAX" + ); + } + + #[test] + fn drop_export_ref_decrements_count() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.bump_export_ref(gem_handle); + scheme.bump_export_ref(gem_handle); + assert_eq!(scheme.gem_export_refcount(gem_handle), 2); + + scheme.drop_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + 1, + "dropping once should decrement from 2 to 1" + ); + } + + #[test] + fn drop_export_ref_removes_entry_when_count_reaches_zero() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 42; + + scheme.bump_export_ref(gem_handle); + assert_eq!(scheme.gem_export_refcount(gem_handle), 1); + + scheme.drop_export_ref(gem_handle); + + assert_eq!( + scheme.gem_export_refcount(gem_handle), + 0, + "dropping the last ref should remove the entry" + ); + } + + #[test] + fn drop_export_ref_cleans_up_prime_exports() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let gem_handle: GemHandle = 77; + let export_token: u32 = 100; + + scheme.prime_exports.insert(export_token, gem_handle); + scheme.bump_export_ref(gem_handle); + assert_eq!(scheme.gem_export_refcount(gem_handle), 1); + assert!(scheme.prime_exports.contains_key(&export_token)); + + scheme.drop_export_ref(gem_handle); + + assert_eq!(scheme.gem_export_refcount(gem_handle), 0); + assert!( + !scheme.prime_exports.values().any(|&h| h == gem_handle), + "drop_export_ref should clean up prime_exports entries for this GEM" + ); + } + + #[test] + fn gem_can_close_returns_false_when_gem_backs_framebuffer() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 16384, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + let addfb = DrmAddFbWire { + width: 64, + height: 64, + pitch: 256, + bpp: 32, + depth: 24, + handle: gem_handle, + fb_id: 0, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_ADDFB, &addfb).unwrap(); + let fb_resp = read_response::(&mut scheme, card); + + if let Some(handle) = scheme.handles.get_mut(&card) { + handle.owned_gems.retain(|&h| h != gem_handle); + } + + assert!( + !scheme.gem_can_close(gem_handle), + "GEM backing a framebuffer should not be closeable" + ); + assert!(scheme.fb_registry.contains_key(&fb_resp.fb_id)); + } + + #[test] + fn gem_can_close_returns_false_when_gem_is_mapped() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + scheme.pin_mapped_gem(card, gem_handle).unwrap(); + + if let Some(handle) = scheme.handles.get_mut(&card) { + handle.owned_gems.retain(|&h| h != gem_handle); + } + + assert!( + !scheme.gem_can_close(gem_handle), + "mapped GEM should not be closeable" + ); + } + + #[test] + fn gem_can_close_returns_true_when_unreferenced() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 4096, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + if let Some(handle) = scheme.handles.get_mut(&card) { + handle.owned_gems.retain(|&h| h != gem_handle); + } + + assert!( + scheme.gem_can_close(gem_handle), + "unreferenced, unmapped GEM with no FB or export refs should be closeable" + ); + } + + #[test] + fn allocate_handle_returns_sequential_ids() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + let id_a = scheme.allocate_handle(NodeKind::Card); + let id_b = scheme.allocate_handle(NodeKind::Card); + + assert!( + id_b > id_a, + "second allocated handle ID ({id_b}) should be greater than first ({id_a})" + ); + } + + #[test] + fn is_fb_active_returns_false_for_unknown_fb() { + let scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + assert!( + !scheme.is_fb_active(12345), + "unknown fb_id should not be active" + ); + } + + #[test] + fn is_fb_active_returns_true_for_active_crtc_fb() { + let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + let card = open_card(&mut scheme); + + let create = DrmGemCreateWire { + size: 640 * 480 * 4, + ..DrmGemCreateWire::default() + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap(); + let created = read_response::(&mut scheme, card); + let gem_handle = created.handle; + + let addfb = DrmAddFbWire { + width: 640, + height: 480, + pitch: 2560, + bpp: 32, + depth: 24, + handle: gem_handle, + fb_id: 0, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_ADDFB, &addfb).unwrap(); + let fb_resp = read_response::(&mut scheme, card); + let fb_id = fb_resp.fb_id; + + let mode = DrmModeWire { + clock: 25200, + hdisplay: 640, + hsync_start: 656, + hsync_end: 752, + htotal: 800, + vdisplay: 480, + vsync_start: 490, + vsync_end: 492, + vtotal: 525, + vrefresh: 60, + ..DrmModeWire::default() + }; + let setcrtc = DrmSetCrtcWire { + crtc_id: 0, + fb_handle: fb_id, + connector_count: 0, + connectors: [0; 8], + mode, + }; + write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_SETCRTC, &setcrtc).unwrap(); + + assert!( + scheme.is_fb_active(fb_id), + "FB programmed on a CRTC should be active" + ); + } + + #[test] + fn validate_gem_create_size_rejects_zero() { + let scheme = DrmScheme::new(Arc::new(FakeDriver::new(false))); + + let err = scheme + .validate_gem_create_size(0, "test-zero-size") + .unwrap_err(); + + assert_eq!( + err.errno, EINVAL, + "zero-sized GEM creation should return EINVAL" + ); + } +} diff --git a/local/recipes/libs/libxau/recipe.toml b/local/recipes/libs/libxau/recipe.toml index c45096d0fd..20940ad89f 100644 --- a/local/recipes/libs/libxau/recipe.toml +++ b/local/recipes/libs/libxau/recipe.toml @@ -27,6 +27,7 @@ export PKG_CONFIG_PATH="${COOKBOOK_SYSROOT}/usr/share/pkgconfig:${COOKBOOK_SYSRO --without-fop \ --without-xsltproc -make -j"${COOKBOOK_MAKE_JOBS}" -make DESTDIR="${COOKBOOK_STAGE}" install +# Prevent automake regeneration (host has different version than source expects) +make -j"${COOKBOOK_MAKE_JOBS}" ACLOCAL=true AUTOMAKE=true AUTOHEADER=true +make DESTDIR="${COOKBOOK_STAGE}" install ACLOCAL=true AUTOMAKE=true AUTOHEADER=true """ diff --git a/local/sources/relibc b/local/sources/relibc index fc8f0ec4fd..c1b8c3b4cf 160000 --- a/local/sources/relibc +++ b/local/sources/relibc @@ -1 +1 @@ -Subproject commit fc8f0ec4fd6909e4bf3e326162eccf83eabd7af6 +Subproject commit c1b8c3b4cfd4ff6a711c0502cbc20d29c500ea8d diff --git a/recipes/tools/gettext/recipe.toml b/recipes/tools/gettext/recipe.toml index 8fac0f6ff4..b8a0720bf3 100644 --- a/recipes/tools/gettext/recipe.toml +++ b/recipes/tools/gettext/recipe.toml @@ -17,12 +17,12 @@ find gettext-runtime gettext-tools libtextstyle -name configure.ac 2>/dev/null | d="$(dirname "$ac")" [ -d "$d/m4" ] || mkdir -p "$d/m4" done -( cd gettext-runtime/libasprintf && autoreconf -fvi -I/usr/share/aclocal ) -( cd gettext-runtime/intl && autoreconf -fvi -I/usr/share/aclocal ) -( cd gettext-runtime && autoreconf -fvi -I/usr/share/aclocal ) -( cd gettext-tools && autoreconf -fvi -I/usr/share/aclocal ) -( cd libtextstyle && autoreconf -fvi -I/usr/share/aclocal ) -autoreconf -fvi -I/usr/share/aclocal +( cd gettext-runtime/libasprintf && autoreconf -fvi -I${COOKBOOK_HOST_SYSROOT}/share/aclocal ) +( cd gettext-runtime/intl && autoreconf -fvi -I${COOKBOOK_HOST_SYSROOT}/share/aclocal ) +( cd gettext-runtime && autoreconf -fvi -I${COOKBOOK_HOST_SYSROOT}/share/aclocal ) +( cd gettext-tools && autoreconf -fvi -I${COOKBOOK_HOST_SYSROOT}/share/aclocal ) +( cd libtextstyle && autoreconf -fvi -I${COOKBOOK_HOST_SYSROOT}/share/aclocal ) +autoreconf -fvi -I${COOKBOOK_HOST_SYSROOT}/share/aclocal """ [build]