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
This commit is contained in:
@@ -1 +0,0 @@
|
||||
/home/kellito/Builds/RedBear-OS/local/recipes/gpu/redox-drm/../../../sources/redox-drm
|
||||
@@ -0,0 +1,5 @@
|
||||
[target.x86_64-unknown-redox]
|
||||
linker = "x86_64-unknown-redox-gcc"
|
||||
|
||||
[build]
|
||||
target = "x86_64-unknown-redox"
|
||||
@@ -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" }
|
||||
@@ -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<PathBuf>, 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<PathBuf> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::gem::GemHandle;
|
||||
use crate::kms::{ConnectorInfo, ModeInfo};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, DriverError>;
|
||||
|
||||
#[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<ConnectorInfo>;
|
||||
fn get_modes(&self, connector_id: u32) -> Vec<ModeInfo>;
|
||||
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<u64>;
|
||||
fn get_vblank(&self, crtc_id: u32) -> Result<u64>;
|
||||
|
||||
fn gem_create(&self, size: u64) -> Result<GemHandle>;
|
||||
fn gem_close(&self, handle: GemHandle) -> Result<()>;
|
||||
fn gem_mmap(&self, handle: GemHandle) -> Result<usize>;
|
||||
fn gem_size(&self, handle: GemHandle) -> Result<u64>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get_edid(&self, connector_id: u32) -> Vec<u8>;
|
||||
fn handle_irq(&self) -> Result<Option<DriverEvent>>;
|
||||
|
||||
fn redox_private_cs_submit(
|
||||
&self,
|
||||
_submit: &RedoxPrivateCsSubmit,
|
||||
) -> Result<RedoxPrivateCsSubmitResult> {
|
||||
Err(DriverError::Unsupported(
|
||||
"private command submission is unavailable on this backend",
|
||||
))
|
||||
}
|
||||
|
||||
fn redox_private_cs_wait(
|
||||
&self,
|
||||
_wait: &RedoxPrivateCsWait,
|
||||
) -> Result<RedoxPrivateCsWaitResult> {
|
||||
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::<RedoxPrivateCsSubmit>(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redox_private_cs_submit_result_size() {
|
||||
// seqno(u64) = 8 bytes
|
||||
assert_eq!(size_of::<RedoxPrivateCsSubmitResult>(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redox_private_cs_wait_size() {
|
||||
// seqno(u64) + timeout_ns(u64) = 16 bytes
|
||||
assert_eq!(size_of::<RedoxPrivateCsWait>(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redox_private_cs_wait_result_size() {
|
||||
// completed(bool) + 7 padding + completed_seqno(u64) = 16 bytes
|
||||
assert_eq!(size_of::<RedoxPrivateCsWaitResult>(), 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::<DriverEvent>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
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<Self> {
|
||||
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<Vec<ConnectorInfo>> {
|
||||
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<u8> {
|
||||
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<ModeInfo> {
|
||||
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<Vec<u8>> {
|
||||
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<u32> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<usize, Box<PageTable>>,
|
||||
}
|
||||
|
||||
impl PageTable {
|
||||
fn allocate() -> Result<Self> {
|
||||
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<u64> {
|
||||
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<PageTable>,
|
||||
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<u64> {
|
||||
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<u64> {
|
||||
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<usize> {
|
||||
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
|
||||
}
|
||||
@@ -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<Option<InterruptHandle>>,
|
||||
display: DisplayCore,
|
||||
gem: Mutex<GemManager>,
|
||||
connectors: Mutex<Vec<Connector>>,
|
||||
crtcs: Mutex<Vec<Crtc>>,
|
||||
encoders: Mutex<Vec<Encoder>>,
|
||||
gtt: Mutex<GttManager>,
|
||||
ring: Mutex<RingManager>,
|
||||
vblank_count: AtomicU64,
|
||||
hotplug_pending: AtomicBool,
|
||||
firmware: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl AmdDriver {
|
||||
pub fn new(info: PciDeviceInfo, firmware: HashMap<String, Vec<u8>>) -> Result<Self> {
|
||||
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<Option<DriverEvent>> {
|
||||
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<u32> {
|
||||
let active_crtc_ids = self
|
||||
.crtcs
|
||||
.lock()
|
||||
.map(|crtcs| {
|
||||
crtcs
|
||||
.iter()
|
||||
.filter(|crtc| crtc.mode.is_some())
|
||||
.map(|crtc| crtc.id)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.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<u32> {
|
||||
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<u64> {
|
||||
{
|
||||
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<ConnectorInfo> {
|
||||
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<ModeInfo> {
|
||||
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<u64> {
|
||||
{
|
||||
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<u64> {
|
||||
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<GemHandle> {
|
||||
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<usize> {
|
||||
let gem = self
|
||||
.gem
|
||||
.lock()
|
||||
.map_err(|_| DriverError::Buffer("GEM manager poisoned".to_string()))?;
|
||||
gem.mmap(handle)
|
||||
}
|
||||
|
||||
fn gem_size(&self, handle: GemHandle) -> Result<u64> {
|
||||
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<u8> {
|
||||
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<Option<DriverEvent>> {
|
||||
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<Connector>, Vec<Encoder>)> {
|
||||
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<PciBarInfo> {
|
||||
info.find_memory_bar(0)
|
||||
.copied()
|
||||
.ok_or_else(|| DriverError::Pci(format!("device {} has no MMIO BAR0", info.location)))
|
||||
}
|
||||
@@ -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<u8> = 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<Self> {
|
||||
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<u32> {
|
||||
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<DmaBuffer>,
|
||||
fence_buffer: Option<DmaBuffer>,
|
||||
mmio: Option<MmioBinding>,
|
||||
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::<u64>())
|
||||
.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<u64> {
|
||||
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<u64> {
|
||||
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::<u32>()) 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<u32>, count: u32) {
|
||||
for _ in 0..count {
|
||||
packet.push(SDMA_OP_NOP);
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_flip(&self, packet: &mut Vec<u32>, 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<u32>, 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<u64> {
|
||||
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
|
||||
}
|
||||
@@ -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<u8>,
|
||||
}
|
||||
|
||||
pub struct IntelDisplay {
|
||||
mmio: MmioRegion,
|
||||
pipes: Mutex<Vec<DisplayPipe>>,
|
||||
}
|
||||
|
||||
impl IntelDisplay {
|
||||
pub fn new(mmio: MmioRegion) -> Result<Self> {
|
||||
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<Vec<DisplayPipe>> {
|
||||
self.refresh_pipes()
|
||||
}
|
||||
|
||||
pub fn pipe_for_crtc(&self, crtc_id: u32) -> Result<DisplayPipe> {
|
||||
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<Vec<DisplayPipe>> {
|
||||
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<Vec<ConnectorInfo>> {
|
||||
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<ModeInfo> {
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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<Vec<DisplayPipe>> {
|
||||
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<u8>) -> 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<ModeInfo> {
|
||||
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<u32> {
|
||||
read32(&self.mmio, offset)
|
||||
}
|
||||
|
||||
fn write32(&self, offset: usize, value: u32) -> Result<()> {
|
||||
write32(&self.mmio, offset, value)
|
||||
}
|
||||
}
|
||||
|
||||
fn connected_ports(mmio: &MmioRegion) -> Vec<u8> {
|
||||
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<u32> {
|
||||
ensure_access(
|
||||
mmio.size(),
|
||||
offset,
|
||||
core::mem::size_of::<u32>(),
|
||||
"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::<u32>(),
|
||||
"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<ModeInfo> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
@@ -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<u64, u64>,
|
||||
}
|
||||
|
||||
impl IntelGtt {
|
||||
pub fn init(gtt_mmio: MmioRegion, control_mmio: MmioRegion) -> Result<Self> {
|
||||
let page_count = gtt_mmio.size() / core::mem::size_of::<u64>();
|
||||
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<u64> {
|
||||
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::<u64>(), "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::<u64>(), "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::<u32>(), "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<usize> {
|
||||
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<u64> {
|
||||
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<usize> {
|
||||
GTT_BASE
|
||||
.checked_add(
|
||||
entry_index
|
||||
.checked_mul(core::mem::size_of::<u64>())
|
||||
.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
|
||||
}
|
||||
@@ -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<Option<InterruptHandle>>,
|
||||
display: IntelDisplay,
|
||||
gem: Mutex<GemManager>,
|
||||
connectors: Mutex<Vec<Connector>>,
|
||||
crtcs: Mutex<Vec<Crtc>>,
|
||||
encoders: Mutex<Vec<Encoder>>,
|
||||
gtt: Mutex<IntelGtt>,
|
||||
ring: Mutex<IntelRing>,
|
||||
vblank_count: AtomicU64,
|
||||
}
|
||||
|
||||
impl IntelDriver {
|
||||
pub fn new(info: PciDeviceInfo, firmware: HashMap<String, Vec<u8>>) -> Result<Self> {
|
||||
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<Vec<ConnectorInfo>> {
|
||||
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<ConnectorInfo> {
|
||||
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<u8> {
|
||||
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<Option<DriverEvent>> {
|
||||
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<Option<u32>> {
|
||||
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<u64> {
|
||||
{
|
||||
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<u32> {
|
||||
let end = offset
|
||||
.checked_add(core::mem::size_of::<u32>())
|
||||
.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<ConnectorInfo> {
|
||||
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<ModeInfo> {
|
||||
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<u64> {
|
||||
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<u64> {
|
||||
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<GemHandle> {
|
||||
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<usize> {
|
||||
let gem = self
|
||||
.gem
|
||||
.lock()
|
||||
.map_err(|_| DriverError::Buffer("Intel GEM manager poisoned".into()))?;
|
||||
gem.mmap(handle)
|
||||
}
|
||||
|
||||
fn gem_size(&self, handle: GemHandle) -> Result<u64> {
|
||||
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<u8> {
|
||||
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<Option<DriverEvent>> {
|
||||
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<Connector>, Vec<Encoder>)> {
|
||||
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<Vec<Crtc>> {
|
||||
let mut crtcs: Vec<Crtc> = 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::<u32>())
|
||||
.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::<u64>() 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::<u64>() 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::<u32>(),
|
||||
PP_STATUS + core::mem::size_of::<u32>(),
|
||||
GFX_FLSH_CNTL_REG + core::mem::size_of::<u32>(),
|
||||
RENDER_RING_BASE + RING_TAIL_OFFSET + core::mem::size_of::<u32>(),
|
||||
RENDER_RING_BASE + RING_HEAD_OFFSET + core::mem::size_of::<u32>(),
|
||||
]
|
||||
.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<PciBarInfo> {
|
||||
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<MmioRegion> {
|
||||
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
|
||||
}
|
||||
@@ -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<u64>,
|
||||
last_seqno: u64,
|
||||
}
|
||||
|
||||
impl IntelRing {
|
||||
pub fn create(mmio: MmioRegion, ring_type: RingType) -> Result<Self> {
|
||||
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::<u32>(), "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::<u32>()) 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<bool> {
|
||||
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::<u32>();
|
||||
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<u32> {
|
||||
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::<u32>(), "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::<u32>(), "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
|
||||
}
|
||||
@@ -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<Self> {
|
||||
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<Option<Self>> {
|
||||
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<Option<Self>> {
|
||||
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<Self> {
|
||||
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<bool> {
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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<String, Vec<u8>>,
|
||||
) -> Result<Arc<dyn GpuDriver>> {
|
||||
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
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Option<InterruptHandle>>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
gem: Mutex<GemManager>,
|
||||
connectors: Mutex<Vec<Connector>>,
|
||||
crtcs: Mutex<Vec<Crtc>>,
|
||||
vblank_count: AtomicU64,
|
||||
}
|
||||
|
||||
fn find_fb_bar(info: &PciDeviceInfo) -> Result<PciBarInfo> {
|
||||
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> {
|
||||
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<String, Vec<u8>>) -> Result<Self> {
|
||||
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<Vec<ConnectorInfo>> {
|
||||
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<ConnectorInfo> {
|
||||
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<ConnectorInfo> {
|
||||
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<ModeInfo> {
|
||||
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<u64> {
|
||||
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<u64> {
|
||||
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<GemHandle> {
|
||||
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<usize> {
|
||||
self.gem
|
||||
.lock()
|
||||
.map_err(|_| DriverError::Buffer("VirtIO GEM poisoned".into()))?
|
||||
.mmap(handle)
|
||||
}
|
||||
|
||||
fn gem_size(&self, handle: GemHandle) -> Result<u64> {
|
||||
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<u8> {
|
||||
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<Option<DriverEvent>> {
|
||||
self.vblank_count.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -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<u64>,
|
||||
}
|
||||
|
||||
struct GemAllocation {
|
||||
object: GemObject,
|
||||
#[allow(dead_code)]
|
||||
dma: DmaBuffer,
|
||||
}
|
||||
|
||||
pub struct GemManager {
|
||||
next_handle: GemHandle,
|
||||
objects: BTreeMap<GemHandle, GemAllocation>,
|
||||
}
|
||||
|
||||
impl GemManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
next_handle: 1,
|
||||
objects: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&mut self, size: u64) -> Result<GemHandle> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<Option<u64>> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
}
|
||||
|
||||
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<u8> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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<u32>,
|
||||
pub mode: Option<ModeInfo>,
|
||||
#[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]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Self> {
|
||||
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<ModeInfo>,
|
||||
}
|
||||
|
||||
#[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<ModeInfo>,
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
@@ -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<u32>,
|
||||
pub crtc_id: Option<u32>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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::<DriverEvent>(8);
|
||||
|
||||
let irq_driver: Arc<dyn GpuDriver> = 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> {
|
||||
Socket::create()
|
||||
.map_err(|e| DriverError::Initialization(format!("failed to register drm scheme: {e}")))
|
||||
}
|
||||
|
||||
fn select_gpu_from_args() -> Result<PciDeviceInfo> {
|
||||
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<PciLocation> {
|
||||
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<u8> {
|
||||
let trimmed = value.trim_start_matches("0x");
|
||||
u8::from_str_radix(trimmed, 16)
|
||||
.or_else(|_| trimmed.parse::<u8>())
|
||||
.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<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
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<String, Vec<u8>>,
|
||||
missing: &[String],
|
||||
) -> Option<String> {
|
||||
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<Self> {
|
||||
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<String, Vec<u8>> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
"""
|
||||
|
||||
+1
-1
Submodule local/sources/relibc updated: fc8f0ec4fd...c1b8c3b4cf
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user