Expand redox-drm DRM scheme, amdgpu port, and update patches

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-18 17:58:44 +01:00
parent d4f6268854
commit 7904dc9b3d
13 changed files with 1271 additions and 1068 deletions
@@ -5,6 +5,42 @@ 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}")]
@@ -16,7 +52,6 @@ pub enum DriverError {
#[error("resource not found: {0}")]
NotFound(String),
#[allow(dead_code)]
#[error("operation not supported: {0}")]
Unsupported(&'static str),
@@ -58,5 +93,23 @@ pub trait GpuDriver: Send + Sync {
#[allow(dead_code)]
fn get_edid(&self, connector_id: u32) -> Vec<u8>;
fn handle_irq(&self) -> Result<Option<(u32, u64)>>;
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",
))
}
}
@@ -10,7 +10,7 @@ use log::{debug, info, warn};
use redox_driver_sys::memory::MmioRegion;
use redox_driver_sys::pci::{PciBarInfo, PciDevice, PciDeviceInfo};
use crate::driver::{DriverError, GpuDriver, Result};
use crate::driver::{DriverError, DriverEvent, GpuDriver, Result};
use crate::drivers::interrupt::InterruptHandle;
use crate::gem::{GemHandle, GemManager};
use crate::kms::connector::{synthetic_edid, Connector};
@@ -41,17 +41,10 @@ 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;
#[derive(Clone, Debug)]
pub enum IrqEvent {
Vblank { crtc_id: u32, count: u64 },
Hotplug { connector_id: u32 },
Unknown,
}
pub struct AmdDriver {
info: PciDeviceInfo,
mmio: MmioRegion,
irq_handle: Option<InterruptHandle>,
irq_handle: Mutex<Option<InterruptHandle>>,
display: DisplayCore,
gem: Mutex<GemManager>,
connectors: Mutex<Vec<Connector>>,
@@ -119,6 +112,7 @@ impl AmdDriver {
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)?;
@@ -140,16 +134,17 @@ impl AmdDriver {
}
info!(
"redox-drm: AMD driver ready for {} with {} connector(s), {} firmware blob(s) loaded",
"redox-drm: AMD driver ready for {} with {} connector(s), {} firmware blob(s) loaded, IRQ mode {}",
info.location,
connectors.len(),
fw_count
fw_count,
irq_mode
);
Ok(Self {
info,
mmio,
irq_handle,
irq_handle: Mutex::new(irq_handle),
display,
gem: Mutex::new(GemManager::new()),
connectors: Mutex::new(connectors),
@@ -163,7 +158,7 @@ impl AmdDriver {
})
}
pub fn process_irq(&self) -> Result<IrqEvent> {
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);
@@ -188,7 +183,7 @@ impl AmdDriver {
connector_id, ih_status, ih_cntl, ih_rptr, ih_wptr
);
return Ok(IrqEvent::Hotplug { connector_id });
return Ok(Some(DriverEvent::Hotplug { connector_id }));
}
if ring_pending || (ih_status & AMD_IH_STATUS_INTERRUPT_PENDING_MASK != 0) {
@@ -201,12 +196,12 @@ impl AmdDriver {
crtc_id, count, ih_status, ih_cntl, ih_rptr, ih_wptr
);
return Ok(IrqEvent::Vblank { crtc_id, count });
return Ok(Some(DriverEvent::Vblank { crtc_id, count }));
}
}
self.acknowledge_ih(ih_wptr);
Ok(IrqEvent::Unknown)
Ok(None)
}
fn read_mmio_reg(&self, register_index: usize) -> u32 {
@@ -541,32 +536,53 @@ impl GpuDriver for AmdDriver {
}
}
fn handle_irq(&self) -> Result<Option<(u32, u64)>> {
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()? {
IrqEvent::Vblank { crtc_id, count } => {
Some(DriverEvent::Vblank { crtc_id, count }) => {
debug!(
"redox-drm: handled AMD vblank IRQ for {} CRTC {} count={} irq={:?}",
self.info.location,
crtc_id,
count,
self.irq_handle.as_ref().map(|h| h.irq())
irq
);
Ok(Some((crtc_id, count)))
Ok(Some(DriverEvent::Vblank { crtc_id, count }))
}
IrqEvent::Hotplug { connector_id } => {
Some(DriverEvent::Hotplug { connector_id }) => {
info!(
"redox-drm: handled AMD hotplug IRQ for {} connector {} irq={:?}",
self.info.location,
connector_id,
self.irq_handle.as_ref().map(|h| h.irq())
irq
);
Ok(None)
Ok(Some(DriverEvent::Hotplug { connector_id }))
}
IrqEvent::Unknown => {
None => {
debug!(
"redox-drm: handled AMD IRQ for {} with no decoded source irq={:?}",
self.info.location,
self.irq_handle.as_ref().map(|h| h.irq())
irq
);
Ok(None)
}
@@ -9,8 +9,9 @@ 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, GpuDriver, Result};
use crate::driver::{DriverError, DriverEvent, GpuDriver, Result};
use crate::drivers::interrupt::InterruptHandle;
use crate::gem::{GemHandle, GemManager};
use crate::kms::connector::{synthetic_edid, Connector};
@@ -57,6 +58,27 @@ impl IntelDriver {
)));
}
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, &gtt_bar, &mmio_bar)?;
@@ -93,18 +115,21 @@ impl IntelDriver {
None
}
};
let irq_mode = irq_handle.as_ref().map(|handle| handle.mode_name()).unwrap_or("none");
if !firmware.is_empty() {
warn!(
"redox-drm: Intel driver ignores {} firmware blob(s); i915-class GPUs usually boot without scheme:firmware blobs",
firmware.len()
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)",
"redox-drm: Intel driver ready for {} with {} connector(s), IRQ mode {}",
info.location,
connectors.len()
connectors.len(),
irq_mode
);
Ok(Self {
@@ -177,7 +202,7 @@ impl IntelDriver {
Ok(connector.info.connector_type_id.saturating_sub(1) as u8)
}
fn process_irq(&self) -> Result<Option<(u32, u64)>> {
fn process_irq(&self) -> Result<Option<DriverEvent>> {
let previous = self.cached_connectors();
let current = self.refresh_connectors()?;
@@ -186,6 +211,20 @@ impl IntelDriver {
"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
@@ -200,7 +239,7 @@ impl IntelDriver {
"redox-drm: Intel IRQ decoded as display event crtc={} ring_busy={}",
crtc_id, ring_busy
);
return Ok(Some((crtc_id, count)));
return Ok(Some(DriverEvent::Vblank { crtc_id, count }));
}
if ring_busy {
@@ -459,7 +498,7 @@ impl GpuDriver for IntelDriver {
}
}
fn handle_irq(&self) -> Result<Option<(u32, u64)>> {
fn handle_irq(&self) -> Result<Option<DriverEvent>> {
let irq_event = {
let mut irq_handle = self
.irq_handle
@@ -23,10 +23,22 @@ pub enum InterruptHandle {
},
}
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);
@@ -49,13 +61,6 @@ impl InterruptHandle {
);
}
if quirks.contains(PciQuirkFlags::FORCE_LEGACY_IRQ) {
info!(
"redox-drm: forcing legacy IRQ for {} (FORCE_LEGACY_IRQ quirk)",
device_info.location
);
}
Self::try_legacy(device_info)
}
@@ -196,7 +201,6 @@ impl InterruptHandle {
.map_err(|e| DriverError::Io(e.to_string()))
}
InterruptHandle::Msi { handle, .. } | InterruptHandle::Legacy { handle, .. } => {
let mut buf = [0u8; 8];
let _ = handle.wait().map_err(|e| DriverError::Io(e.to_string()))?;
Ok(())
}
@@ -210,7 +214,31 @@ impl InterruptHandle {
}
}
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
));
}
}
@@ -7,6 +7,8 @@ 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)]
@@ -43,6 +45,11 @@ impl GemManager {
"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);
@@ -113,13 +120,4 @@ impl GemManager {
pub fn gpu_addr(&self, handle: GemHandle) -> Result<Option<u64>> {
Ok(self.object(handle)?.gpu_addr)
}
#[allow(dead_code)]
pub fn object_mut_ptr(&mut self, handle: GemHandle) -> Result<usize> {
let allocation = self
.objects
.get_mut(&handle)
.ok_or_else(|| DriverError::NotFound(format!("unknown GEM handle {handle}")))?;
Ok(allocation.dma.as_mut_ptr() as usize)
}
}
+307 -36
View File
@@ -20,12 +20,15 @@ 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::quirks::PciQuirkFlags;
use redox_scheme::{SignalBehavior, Socket};
use crate::driver::{DriverError, GpuDriver, Result};
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,
}
@@ -70,13 +73,16 @@ fn run() -> Result<()> {
.map_err(|e| DriverError::Initialization(format!("failed to register drm scheme: {e}")))?;
info!("redox-drm: registered scheme:drm");
let (vblank_tx, vblank_rx) = mpsc::sync_channel::<(u32, u64)>(8);
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((crtc_id, count))) => {
let _ = vblank_tx.try_send((crtc_id, count));
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) => {
@@ -87,12 +93,12 @@ fn run() -> Result<()> {
});
let drm_scheme = Arc::new(Mutex::new(DrmScheme::new(driver)));
let vblank_scheme = drm_scheme.clone();
let event_scheme = drm_scheme.clone();
std::thread::spawn(move || loop {
if let Ok((crtc_id, vblank_count)) = vblank_rx.recv() {
if let Ok(mut scheme) = vblank_scheme.lock() {
scheme.retire_vblank(crtc_id, vblank_count);
if let Ok(event) = event_rx.recv() {
if let Ok(mut scheme) = event_scheme.lock() {
scheme.handle_driver_event(event);
}
}
});
@@ -209,20 +215,42 @@ struct FirmwareCache {
blobs: HashMap<String, Vec<u8>>,
}
impl FirmwareCache {
fn load_for_device(info: &PciDeviceInfo) -> Result<Self> {
if info.vendor_id != PCI_VENDOR_ID_AMD {
info!(
"redox-drm: skipping firmware load for Intel GPU {}",
info.location
);
return Ok(Self {
blobs: HashMap::new(),
});
}
struct FirmwareExpectation {
vendor_name: &'static str,
keys: &'static [&'static str],
required: bool,
required_label: &'static str,
}
let firmware_keys: &[&str] = if info.vendor_id == PCI_VENDOR_ID_AMD {
&[
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"];
const INTEL_ADLP_DMC_KEYS: &[&str] = &["i915/adlp_dmc.bin", "i915/adlp_dmc_ver2_16.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"];
fn intel_display_firmware_keys(device_id: u16) -> Option<&'static [&'static str]> {
match device_id {
0x9A40 | 0x9A49 | 0x9A60 | 0x9A68 | 0x9A70 | 0x9A78 => Some(INTEL_TGL_DMC_KEYS),
0x46A6 => Some(INTEL_ADLP_DMC_KEYS),
0x5690 | 0x5691 | 0x5692 | 0x5693 | 0x5694 | 0x5696 | 0x5697 | 0x56A0 | 0x56A1
| 0x56A5 | 0x56A6 | 0x56B0 | 0x56B1 | 0x56B2 | 0x56B3 | 0x56C0 | 0x56C1 => {
Some(INTEL_DG2_DMC_KEYS)
}
0x7D55 | 0x7D45 | 0x7D40 => Some(INTEL_MTL_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",
@@ -238,47 +266,165 @@ impl FirmwareCache {
"amdgpu/sdma_5_2",
"amdgpu/vcn_3_0_0",
"amdgpu/vcn_3_1_0",
]
} else {
&[]
};
],
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 loaded_any = false;
let mut missing = Vec::new();
for &key in firmware_keys {
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() as usize).unwrap_or(1024 * 1024);
let mut buf = Vec::with_capacity(estimated_size);
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);
loaded_any = true;
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 !loaded_any && info.vendor_id == PCI_VENDOR_ID_AMD {
return Err(DriverError::NotFound(
"no AMD firmware blobs available from scheme:firmware".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)",
blobs.len()
"redox-drm: firmware cache populated with {} blob(s) for {} GPU {}",
blobs.len(),
expectation.vendor_name,
info.location
);
Ok(Self { blobs })
}
@@ -309,3 +455,128 @@ fn main() {
process::exit(1);
}
}
#[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());
}
}
+691 -30
View File
@@ -2,13 +2,17 @@ use std::collections::{BTreeMap, HashSet};
use std::mem::size_of;
use std::sync::Arc;
use getrandom::getrandom;
use log::{debug, warn};
use redox_scheme::SchemeBlockMut;
use syscall04::data::Stat;
use syscall04::error::{Error, Result, EBADF, EBUSY, EINVAL, ENOENT, EOPNOTSUPP};
use syscall04::flag::{EventFlags, MapFlags, MunmapFlags, MODE_FILE};
use crate::driver::GpuDriver;
use crate::driver::{
DriverEvent, GpuDriver, RedoxPrivateCsSubmit, RedoxPrivateCsSubmitResult, RedoxPrivateCsWait,
RedoxPrivateCsWaitResult,
};
use crate::gem::GemHandle;
use crate::kms::ModeInfo;
@@ -43,6 +47,12 @@ const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
const DRM_IOCTL_GEM_MMAP: usize = DRM_IOCTL_BASE + 28;
const DRM_IOCTL_PRIME_HANDLE_TO_FD: usize = DRM_IOCTL_BASE + 29;
const DRM_IOCTL_PRIME_FD_TO_HANDLE: usize = DRM_IOCTL_BASE + 30;
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
const DRM_IOCTL_REDOX_PRIVATE_CS_WAIT: usize = DRM_IOCTL_BASE + 32;
const DRM_IOCTL_REDOX_AMD_SDMA_SUBMIT: usize = DRM_IOCTL_BASE + 0x40;
const DRM_IOCTL_REDOX_AMD_SDMA_WAIT: usize = DRM_IOCTL_BASE + 0x41;
const MAX_SCHEME_GEM_BYTES: u64 = 256 * 1024 * 1024;
// ---- Wire types for DRM ioctls ----
#[repr(C)]
@@ -238,6 +248,29 @@ struct DrmPrimeFdToHandleResponseWire {
_pad: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxAmdSdmaSubmitWire {
src_handle: u32,
dst_handle: u32,
flags: u32,
_pad: u32,
src_offset: u64,
dst_offset: u64,
size: u64,
seqno: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxAmdSdmaWaitWire {
seqno: u64,
timeout_ns: u64,
flags: u32,
completed: u32,
completed_seqno: u64,
}
// ---- Internal handle types ----
#[derive(Clone, Debug)]
@@ -257,6 +290,7 @@ struct Handle {
mapped_gem_refs: usize,
owned_fbs: Vec<u32>,
owned_gems: Vec<GemHandle>,
imported_gems: HashSet<GemHandle>,
closing: bool,
}
@@ -264,7 +298,6 @@ pub struct DrmScheme {
driver: Arc<dyn GpuDriver>,
next_id: usize,
next_fb_id: u32,
next_export_token: u32,
handles: BTreeMap<usize, Handle>,
active_crtc_fb: BTreeMap<u32, u32>,
active_crtc_mode: BTreeMap<u32, ModeInfo>,
@@ -281,7 +314,6 @@ impl DrmScheme {
driver,
next_id: 0,
next_fb_id: 1,
next_export_token: 1,
handles: BTreeMap::new(),
active_crtc_fb: BTreeMap::new(),
active_crtc_mode: BTreeMap::new(),
@@ -293,25 +325,6 @@ impl DrmScheme {
}
}
#[allow(dead_code)]
pub fn on_close(&mut self, id: usize) {
let mapped = self
.handles
.get(&id)
.map(|handle| handle.mapped_gem_refs != 0)
.unwrap_or(false);
if mapped {
if let Some(handle) = self.handles.get_mut(&id) {
handle.closing = true;
}
return;
}
if let Some(handle) = self.handles.remove(&id) {
self.finalize_handle_close(handle);
}
}
fn is_fb_active(&self, fb_id: u32) -> bool {
self.active_crtc_fb.values().any(|&id| id == fb_id)
|| self.pending_flip_fb.values().any(|&(_, id)| id == fb_id)
@@ -321,6 +334,14 @@ impl DrmScheme {
handle.owned_gems.contains(&gem_handle)
}
fn handle_has_local_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool {
Self::handle_has_gem_ref(handle, gem_handle) && !handle.imported_gems.contains(&gem_handle)
}
fn handle_has_imported_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool {
Self::handle_has_gem_ref(handle, gem_handle) && handle.imported_gems.contains(&gem_handle)
}
fn gem_is_still_referenced(&self, gem_handle: GemHandle) -> bool {
self.handles
.values()
@@ -341,6 +362,26 @@ impl DrmScheme {
self.gem_export_refs.get(&gem_handle).copied().unwrap_or(0)
}
fn allocate_export_token(&self) -> Result<u32> {
for _ in 0..64 {
let mut bytes = [0u8; 4];
getrandom(&mut bytes).map_err(|e| {
warn!("redox-drm: failed to draw PRIME export token entropy: {e}");
Error::new(syscall04::error::EIO)
})?;
let token = u32::from_le_bytes(bytes) & 0x7fff_ffff;
if token == 0 || self.prime_exports.contains_key(&token) {
continue;
}
return Ok(token);
}
warn!("redox-drm: unable to allocate unique PRIME export token");
Err(Error::new(EBUSY))
}
fn bump_export_ref(&mut self, gem_handle: GemHandle) {
let entry = self.gem_export_refs.entry(gem_handle).or_insert(0);
*entry = entry.saturating_add(1);
@@ -372,6 +413,113 @@ impl DrmScheme {
&& self.gem_export_refcount(gem_handle) == 0
}
fn validate_private_cs_handles(
&self,
id: usize,
src_handle: GemHandle,
dst_handle: GemHandle,
operation: &str,
) -> Result<()> {
let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
if !Self::handle_has_gem_ref(handle, src_handle)
|| !Self::handle_has_gem_ref(handle, dst_handle)
{
warn!(
"redox-drm: {} rejected — src={} dst={} not owned by this fd",
operation, src_handle, dst_handle
);
return Err(Error::new(EBADF));
}
if Self::handle_has_imported_gem_ref(handle, src_handle)
|| Self::handle_has_imported_gem_ref(handle, dst_handle)
{
warn!(
"redox-drm: {} rejected — imported DMA-BUF handles are outside the bounded private CS path",
operation
);
return Err(Error::new(EOPNOTSUPP));
}
Ok(())
}
fn validate_private_cs_ranges(
&self,
submit: &RedoxPrivateCsSubmit,
operation: &str,
) -> Result<()> {
if submit.byte_count == 0 {
warn!("redox-drm: {} rejected — zero-sized submission", operation);
return Err(Error::new(EINVAL));
}
let src_size = self
.driver
.gem_size(submit.src_handle)
.map_err(driver_to_syscall)?;
let dst_size = self
.driver
.gem_size(submit.dst_handle)
.map_err(driver_to_syscall)?;
let src_end = submit
.src_offset
.checked_add(submit.byte_count)
.ok_or_else(|| {
warn!("redox-drm: {} rejected — source range overflow", operation);
Error::new(EINVAL)
})?;
if src_end > src_size {
warn!(
"redox-drm: {} rejected — source range {}..{} exceeds GEM size {}",
operation,
submit.src_offset,
src_end,
src_size
);
return Err(Error::new(EINVAL));
}
let dst_end = submit
.dst_offset
.checked_add(submit.byte_count)
.ok_or_else(|| {
warn!("redox-drm: {} rejected — destination range overflow", operation);
Error::new(EINVAL)
})?;
if dst_end > dst_size {
warn!(
"redox-drm: {} rejected — destination range {}..{} exceeds GEM size {}",
operation,
submit.dst_offset,
dst_end,
dst_size
);
return Err(Error::new(EINVAL));
}
Ok(())
}
fn validate_gem_create_size(&self, size: u64, operation: &str) -> Result<()> {
if size == 0 {
warn!("redox-drm: {} rejected — zero-sized GEM allocation", operation);
return Err(Error::new(EINVAL));
}
if size > MAX_SCHEME_GEM_BYTES {
warn!(
"redox-drm: {} rejected — size {} exceeds trusted shared-core cap {}",
operation,
size,
MAX_SCHEME_GEM_BYTES
);
return Err(Error::new(EINVAL));
}
Ok(())
}
fn maybe_close_gem(&mut self, gem_handle: GemHandle, context: &str) -> bool {
if !self.gem_can_close(gem_handle) {
return false;
@@ -404,6 +552,7 @@ impl DrmScheme {
mapped_gem_refs: 0,
owned_fbs: Vec::new(),
owned_gems: Vec::new(),
imported_gems: HashSet::new(),
closing: false,
},
);
@@ -485,6 +634,13 @@ impl DrmScheme {
}
}
pub fn handle_driver_event(&mut self, event: DriverEvent) {
match event {
DriverEvent::Vblank { crtc_id, count } => self.retire_vblank(crtc_id, count),
DriverEvent::Hotplug { .. } => {}
}
}
fn try_reap_fb(&mut self, fb_id: u32) {
let gem_handle = match self.fb_registry.get(&fb_id) {
Some(info) => info.gem_handle,
@@ -507,7 +663,11 @@ impl DrmScheme {
crtc_count: 1,
encoder_count: connectors.len() as u32,
};
bytes_of(&payload)
let mut out = bytes_of(&payload);
for connector in connectors {
out.extend_from_slice(&bytes_of(&connector.id));
}
out
}
fn encode_connector(&self, connector_id: u32) -> Result<Vec<u8>> {
@@ -673,6 +833,7 @@ impl DrmScheme {
let pitch = (req.width.saturating_mul(req.bpp).saturating_add(7)) / 8;
req.pitch = pitch;
req.size = (pitch as u64).saturating_mul(req.height as u64);
self.validate_gem_create_size(req.size, "CREATE_DUMB")?;
req.handle = self
.driver
.gem_create(req.size)
@@ -758,6 +919,7 @@ impl DrmScheme {
}
if let Some(handle) = self.handles.get_mut(&id) {
handle.owned_gems.retain(|&h| h != req.handle);
handle.imported_gems.remove(&req.handle);
}
Vec::new()
}
@@ -925,9 +1087,7 @@ impl DrmScheme {
DRM_IOCTL_GEM_CREATE => {
let mut req = decode_wire::<DrmGemCreateWire>(payload)?;
if req.size == 0 {
return Err(Error::new(EINVAL));
}
self.validate_gem_create_size(req.size, "GEM_CREATE")?;
req.handle = self
.driver
.gem_create(req.size)
@@ -980,6 +1140,7 @@ impl DrmScheme {
}
if let Some(handle) = self.handles.get_mut(&id) {
handle.owned_gems.retain(|&h| h != req.handle);
handle.imported_gems.remove(&req.handle);
}
Vec::new()
}
@@ -1017,6 +1178,65 @@ impl DrmScheme {
bytes_of(&req)
}
DRM_IOCTL_REDOX_AMD_SDMA_SUBMIT => {
let mut req = decode_wire::<RedoxAmdSdmaSubmitWire>(payload)?;
if req.flags != 0 {
warn!(
"redox-drm: AMD SDMA submit rejected — unsupported flags {:#x}",
req.flags
);
return Err(Error::new(EINVAL));
}
if req.size == 0 {
warn!("redox-drm: AMD SDMA submit rejected — zero-sized copy");
return Err(Error::new(EINVAL));
}
self.validate_private_cs_handles(
id,
req.src_handle,
req.dst_handle,
"AMD SDMA submit",
)?;
let submit = RedoxPrivateCsSubmit {
src_handle: req.src_handle,
dst_handle: req.dst_handle,
src_offset: req.src_offset,
dst_offset: req.dst_offset,
byte_count: req.size,
};
self.validate_private_cs_ranges(&submit, "AMD SDMA submit")?;
req.seqno = self
.driver
.redox_private_cs_submit(&submit)
.map_err(driver_to_syscall)?
.seqno;
bytes_of(&req)
}
DRM_IOCTL_REDOX_AMD_SDMA_WAIT => {
let mut req = decode_wire::<RedoxAmdSdmaWaitWire>(payload)?;
if req.flags != 0 {
warn!(
"redox-drm: AMD SDMA wait rejected — unsupported flags {:#x}",
req.flags
);
return Err(Error::new(EINVAL));
}
let result = self
.driver
.redox_private_cs_wait(&RedoxPrivateCsWait {
seqno: req.seqno,
timeout_ns: req.timeout_ns,
})
.map_err(driver_to_syscall)?;
req.completed = u32::from(result.completed);
req.completed_seqno = result.completed_seqno;
bytes_of(&req)
}
DRM_IOCTL_PRIME_HANDLE_TO_FD => {
let req = decode_wire::<DrmPrimeHandleToFdWire>(payload)?;
let owned = self
@@ -1032,8 +1252,7 @@ impl DrmScheme {
return Err(Error::new(EBADF));
}
let token = self.next_export_token;
self.next_export_token = self.next_export_token.saturating_add(1);
let token = self.allocate_export_token()?;
self.prime_exports.insert(token, req.handle);
let resp = DrmPrimeHandleToFdResponseWire {
@@ -1077,6 +1296,7 @@ impl DrmScheme {
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
if !handle.owned_gems.contains(&gem_handle) {
handle.owned_gems.push(gem_handle);
handle.imported_gems.insert(gem_handle);
}
let resp = DrmPrimeFdToHandleResponseWire {
@@ -1086,6 +1306,31 @@ impl DrmScheme {
bytes_of(&resp)
}
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT => {
let req = decode_wire::<RedoxPrivateCsSubmit>(payload)?;
self.validate_private_cs_handles(
id,
req.src_handle,
req.dst_handle,
"private CS submit",
)?;
self.validate_private_cs_ranges(&req, "private CS submit")?;
let resp: RedoxPrivateCsSubmitResult = self
.driver
.redox_private_cs_submit(&req)
.map_err(driver_to_syscall)?;
bytes_of(&resp)
}
DRM_IOCTL_REDOX_PRIVATE_CS_WAIT => {
let req = decode_wire::<RedoxPrivateCsWait>(payload)?;
let resp: RedoxPrivateCsWaitResult = self
.driver
.redox_private_cs_wait(&req)
.map_err(driver_to_syscall)?;
bytes_of(&resp)
}
_ => {
warn!("redox-drm: unsupported ioctl {:#x}", request);
return Err(Error::new(EOPNOTSUPP));
@@ -1190,7 +1435,10 @@ impl SchemeBlockMut for DrmScheme {
fn fsync(&mut self, id: usize) -> Result<Option<usize>> {
let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
Ok(Some(0))
warn!(
"redox-drm: fsync rejected — shared core has no implicit render-fence sync contract"
);
Err(Error::new(EOPNOTSUPP))
}
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
@@ -1384,5 +1632,418 @@ fn decode_wire<T: Copy>(buf: &[u8]) -> Result<T> {
fn driver_to_syscall(error: crate::driver::DriverError) -> Error {
warn!("redox-drm: driver error: {}", error);
Error::new(EINVAL)
match error {
crate::driver::DriverError::Unsupported(_) => Error::new(EOPNOTSUPP),
crate::driver::DriverError::InvalidArgument(_) => Error::new(EINVAL),
crate::driver::DriverError::NotFound(_) => Error::new(ENOENT),
crate::driver::DriverError::Initialization(_)
| crate::driver::DriverError::Mmio(_)
| crate::driver::DriverError::Pci(_)
| crate::driver::DriverError::Buffer(_)
| crate::driver::DriverError::Io(_) => Error::new(EINVAL),
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use redox_scheme::SchemeBlockMut;
use super::*;
use crate::driver::{DriverError, DriverEvent, GpuDriver};
use crate::kms::{ConnectorInfo, ModeInfo};
#[derive(Default)]
struct FakeDriverState {
next_handle: GemHandle,
gem_sizes: BTreeMap<GemHandle, u64>,
submit_calls: usize,
}
struct FakeDriver {
state: Mutex<FakeDriverState>,
support_private_cs: bool,
}
impl FakeDriver {
fn new(support_private_cs: bool) -> Self {
Self {
state: Mutex::new(FakeDriverState {
next_handle: 1,
..FakeDriverState::default()
}),
support_private_cs,
}
}
fn submit_calls(&self) -> usize {
self.state.lock().unwrap().submit_calls
}
}
impl GpuDriver for FakeDriver {
fn driver_name(&self) -> &str {
"fake"
}
fn driver_desc(&self) -> &str {
"fake"
}
fn driver_date(&self) -> &str {
"1970-01-01"
}
fn detect_connectors(&self) -> Vec<ConnectorInfo> {
Vec::new()
}
fn get_modes(&self, _connector_id: u32) -> Vec<ModeInfo> {
Vec::new()
}
fn set_crtc(
&self,
_crtc_id: u32,
_fb_handle: u32,
_connectors: &[u32],
_mode: &ModeInfo,
) -> crate::driver::Result<()> {
Ok(())
}
fn page_flip(&self, _crtc_id: u32, _fb_handle: u32, _flags: u32) -> crate::driver::Result<u64> {
Ok(0)
}
fn get_vblank(&self, _crtc_id: u32) -> crate::driver::Result<u64> {
Ok(0)
}
fn gem_create(&self, size: u64) -> crate::driver::Result<GemHandle> {
let mut state = self.state.lock().unwrap();
let handle = state.next_handle;
state.next_handle = state.next_handle.saturating_add(1);
state.gem_sizes.insert(handle, size);
Ok(handle)
}
fn gem_close(&self, handle: GemHandle) -> crate::driver::Result<()> {
let removed = self.state.lock().unwrap().gem_sizes.remove(&handle);
if removed.is_some() {
Ok(())
} else {
Err(DriverError::NotFound(format!("unknown GEM handle {handle}")))
}
}
fn gem_mmap(&self, handle: GemHandle) -> crate::driver::Result<usize> {
if self.state.lock().unwrap().gem_sizes.contains_key(&handle) {
Ok((handle as usize).saturating_mul(4096))
} else {
Err(DriverError::NotFound(format!("unknown GEM handle {handle}")))
}
}
fn gem_size(&self, handle: GemHandle) -> crate::driver::Result<u64> {
self.state
.lock()
.unwrap()
.gem_sizes
.get(&handle)
.copied()
.ok_or_else(|| DriverError::NotFound(format!("unknown GEM handle {handle}")))
}
fn get_edid(&self, _connector_id: u32) -> Vec<u8> {
Vec::new()
}
fn handle_irq(&self) -> crate::driver::Result<Option<DriverEvent>> {
Ok(None)
}
fn redox_private_cs_submit(
&self,
_submit: &RedoxPrivateCsSubmit,
) -> crate::driver::Result<RedoxPrivateCsSubmitResult> {
if !self.support_private_cs {
return Err(DriverError::Unsupported(
"private command submission is unavailable on this backend",
));
}
let mut state = self.state.lock().unwrap();
state.submit_calls = state.submit_calls.saturating_add(1);
Ok(RedoxPrivateCsSubmitResult { seqno: 7 })
}
}
fn open_card(scheme: &mut DrmScheme) -> usize {
scheme.open("card0", 0, 0, 0).unwrap().unwrap()
}
fn write_ioctl<T>(scheme: &mut DrmScheme, id: usize, request: usize, payload: &T) -> Result<usize> {
let mut buf = request.to_le_bytes().to_vec();
buf.extend_from_slice(&bytes_of(payload));
scheme.write(id, &buf).map(|written| written.unwrap_or(0))
}
fn read_response<T: Copy>(scheme: &mut DrmScheme, id: usize) -> T {
let mut buf = vec![0; size_of::<T>()];
let len = scheme.read(id, &mut buf).unwrap().unwrap();
assert_eq!(len, size_of::<T>());
decode_wire::<T>(&buf).unwrap()
}
#[test]
fn private_cs_submit_rejects_imported_dma_buf_handles() {
let driver = Arc::new(FakeDriver::new(true));
let mut scheme = DrmScheme::new(driver.clone());
let exporter = open_card(&mut scheme);
let importer = open_card(&mut scheme);
let create = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
write_ioctl(&mut scheme, exporter, DRM_IOCTL_GEM_CREATE, &create).unwrap();
let created = read_response::<DrmGemCreateWire>(&mut scheme, exporter);
let export = DrmPrimeHandleToFdWire {
handle: created.handle,
flags: 0,
};
write_ioctl(&mut scheme, exporter, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export).unwrap();
let exported = read_response::<DrmPrimeHandleToFdResponseWire>(&mut scheme, exporter);
let import = DrmPrimeFdToHandleWire {
fd: exported.fd,
_pad: 0,
};
write_ioctl(&mut scheme, importer, DRM_IOCTL_PRIME_FD_TO_HANDLE, &import).unwrap();
let imported = read_response::<DrmPrimeFdToHandleResponseWire>(&mut scheme, importer);
let submit = RedoxPrivateCsSubmit {
src_handle: imported.handle,
dst_handle: imported.handle,
src_offset: 0,
dst_offset: 0,
byte_count: 64,
};
let err = write_ioctl(
&mut scheme,
importer,
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT,
&submit,
)
.unwrap_err();
assert_eq!(err.errno, EOPNOTSUPP);
assert_eq!(driver.submit_calls(), 0);
}
#[test]
fn prime_handle_to_fd_returns_distinct_nonzero_tokens() {
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
let card = open_card(&mut scheme);
for _ in 0..2 {
let create = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap();
let _ = read_response::<DrmGemCreateWire>(&mut scheme, card);
}
let handles = scheme.handles.get(&card).unwrap().owned_gems.clone();
let export_a = DrmPrimeHandleToFdWire {
handle: handles[0],
flags: 0,
};
write_ioctl(&mut scheme, card, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export_a).unwrap();
let token_a = read_response::<DrmPrimeHandleToFdResponseWire>(&mut scheme, card).fd;
let export_b = DrmPrimeHandleToFdWire {
handle: handles[1],
flags: 0,
};
write_ioctl(&mut scheme, card, DRM_IOCTL_PRIME_HANDLE_TO_FD, &export_b).unwrap();
let token_b = read_response::<DrmPrimeHandleToFdResponseWire>(&mut scheme, card).fd;
assert_ne!(token_a, 0);
assert_ne!(token_b, 0);
assert_ne!(token_a, token_b);
}
#[test]
fn private_cs_wait_is_explicitly_unsupported_without_backend_support() {
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
let card = open_card(&mut scheme);
let wait = RedoxPrivateCsWait {
seqno: 1,
timeout_ns: 0,
};
let err = write_ioctl(&mut scheme, card, DRM_IOCTL_REDOX_PRIVATE_CS_WAIT, &wait).unwrap_err();
assert_eq!(err.errno, EOPNOTSUPP);
}
#[test]
fn fsync_is_not_a_fake_render_sync_success() {
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
let card = open_card(&mut scheme);
let err = scheme.fsync(card).unwrap_err();
assert_eq!(err.errno, EOPNOTSUPP);
}
#[test]
fn private_cs_submit_still_reaches_backend_for_local_gems() {
let driver = Arc::new(FakeDriver::new(true));
let mut scheme = DrmScheme::new(driver.clone());
let card = open_card(&mut scheme);
for _ in 0..2 {
let create = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap();
let _ = read_response::<DrmGemCreateWire>(&mut scheme, card);
}
let handles = match scheme.handles.get(&card) {
Some(handle) => handle.owned_gems.clone(),
None => panic!("missing fake card handle"),
};
let submit = RedoxPrivateCsSubmit {
src_handle: handles[0],
dst_handle: handles[1],
src_offset: 0,
dst_offset: 0,
byte_count: 128,
};
write_ioctl(&mut scheme, card, DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT, &submit).unwrap();
let response = read_response::<RedoxPrivateCsSubmitResult>(&mut scheme, card);
assert_eq!(response.seqno, 7);
assert_eq!(driver.submit_calls(), 1);
}
#[test]
fn private_cs_submit_rejects_out_of_bounds_ranges() {
let driver = Arc::new(FakeDriver::new(true));
let mut scheme = DrmScheme::new(driver.clone());
let card = open_card(&mut scheme);
for _ in 0..2 {
let create = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap();
let _ = read_response::<DrmGemCreateWire>(&mut scheme, card);
}
let handles = scheme.handles.get(&card).unwrap().owned_gems.clone();
let submit = RedoxPrivateCsSubmit {
src_handle: handles[0],
dst_handle: handles[1],
src_offset: 4090,
dst_offset: 0,
byte_count: 64,
};
let err = write_ioctl(&mut scheme, card, DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT, &submit)
.unwrap_err();
assert_eq!(err.errno, EINVAL);
assert_eq!(driver.submit_calls(), 0);
}
#[test]
fn vblank_driver_event_retires_pending_page_flip() {
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
scheme.fb_registry.insert(
7,
FbInfo {
gem_handle: 41,
width: 0,
height: 0,
pitch: 0,
bpp: 0,
},
);
scheme.pending_flip_fb.insert(3, (5, 7));
scheme.handle_driver_event(DriverEvent::Vblank {
crtc_id: 3,
count: 5,
});
assert!(!scheme.pending_flip_fb.contains_key(&3));
assert!(!scheme.fb_registry.contains_key(&7));
}
#[test]
fn non_vblank_driver_event_does_not_retire_pending_page_flip() {
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
scheme.fb_registry.insert(
9,
FbInfo {
gem_handle: 99,
width: 0,
height: 0,
pitch: 0,
bpp: 0,
},
);
scheme.pending_flip_fb.insert(1, (2, 9));
scheme.handle_driver_event(DriverEvent::Hotplug { connector_id: 1 });
assert_eq!(scheme.pending_flip_fb.get(&1), Some(&(2, 9)));
assert!(scheme.fb_registry.contains_key(&9));
}
#[test]
fn gem_create_rejects_oversized_allocations() {
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
let card = open_card(&mut scheme);
let create = DrmGemCreateWire {
size: MAX_SCHEME_GEM_BYTES + 1,
..DrmGemCreateWire::default()
};
let err = write_ioctl(&mut scheme, card, DRM_IOCTL_GEM_CREATE, &create).unwrap_err();
assert_eq!(err.errno, EINVAL);
}
#[test]
fn create_dumb_rejects_oversized_allocations() {
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
let card = open_card(&mut scheme);
let create = DrmCreateDumbWire {
width: 16384,
height: 16384,
bpp: 32,
..DrmCreateDumbWire::default()
};
let err = write_ioctl(&mut scheme, card, DRM_IOCTL_MODE_CREATE_DUMB, &create).unwrap_err();
assert_eq!(err.errno, EINVAL);
}
}