7904dc9b3d
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2050 lines
68 KiB
Rust
2050 lines
68 KiB
Rust
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::{
|
|
DriverEvent, GpuDriver, RedoxPrivateCsSubmit, RedoxPrivateCsSubmitResult, RedoxPrivateCsWait,
|
|
RedoxPrivateCsWaitResult,
|
|
};
|
|
use crate::gem::GemHandle;
|
|
use crate::kms::ModeInfo;
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct FbInfo {
|
|
gem_handle: GemHandle,
|
|
width: u32,
|
|
height: u32,
|
|
pitch: u32,
|
|
bpp: u32,
|
|
}
|
|
|
|
// ---- DRM ioctl request codes ----
|
|
const DRM_IOCTL_BASE: usize = 0x00A0;
|
|
const DRM_IOCTL_MODE_GETRESOURCES: usize = DRM_IOCTL_BASE;
|
|
const DRM_IOCTL_MODE_GETCONNECTOR: usize = DRM_IOCTL_BASE + 7;
|
|
const DRM_IOCTL_MODE_GETMODES: usize = DRM_IOCTL_BASE + 8;
|
|
const DRM_IOCTL_MODE_SETCRTC: usize = DRM_IOCTL_BASE + 2;
|
|
const DRM_IOCTL_MODE_GETCRTC: usize = DRM_IOCTL_BASE + 3;
|
|
const DRM_IOCTL_MODE_GETENCODER: usize = DRM_IOCTL_BASE + 6;
|
|
const DRM_IOCTL_MODE_PAGE_FLIP: usize = DRM_IOCTL_BASE + 16;
|
|
const DRM_IOCTL_MODE_CREATE_DUMB: usize = DRM_IOCTL_BASE + 18;
|
|
const DRM_IOCTL_MODE_MAP_DUMB: usize = DRM_IOCTL_BASE + 19;
|
|
const DRM_IOCTL_MODE_DESTROY_DUMB: usize = DRM_IOCTL_BASE + 20;
|
|
const DRM_IOCTL_MODE_ADDFB: usize = DRM_IOCTL_BASE + 21;
|
|
const DRM_IOCTL_MODE_RMFB: usize = DRM_IOCTL_BASE + 22;
|
|
const DRM_IOCTL_GET_CAP: usize = DRM_IOCTL_BASE + 23;
|
|
const DRM_IOCTL_SET_CLIENT_CAP: usize = DRM_IOCTL_BASE + 24;
|
|
const DRM_IOCTL_VERSION: usize = DRM_IOCTL_BASE + 25;
|
|
const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26;
|
|
const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
|
|
const DRM_IOCTL_GEM_MMAP: usize = DRM_IOCTL_BASE + 28;
|
|
const DRM_IOCTL_PRIME_HANDLE_TO_FD: usize = DRM_IOCTL_BASE + 29;
|
|
const DRM_IOCTL_PRIME_FD_TO_HANDLE: usize = DRM_IOCTL_BASE + 30;
|
|
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
|
|
const DRM_IOCTL_REDOX_PRIVATE_CS_WAIT: usize = DRM_IOCTL_BASE + 32;
|
|
const DRM_IOCTL_REDOX_AMD_SDMA_SUBMIT: usize = DRM_IOCTL_BASE + 0x40;
|
|
const DRM_IOCTL_REDOX_AMD_SDMA_WAIT: usize = DRM_IOCTL_BASE + 0x41;
|
|
|
|
const MAX_SCHEME_GEM_BYTES: u64 = 256 * 1024 * 1024;
|
|
|
|
// ---- Wire types for DRM ioctls ----
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmResourcesWire {
|
|
connector_count: u32,
|
|
crtc_count: u32,
|
|
encoder_count: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmConnectorWire {
|
|
connector_id: u32,
|
|
connection: u32,
|
|
connector_type: u32,
|
|
mm_width: u32,
|
|
mm_height: u32,
|
|
encoder_id: u32,
|
|
mode_count: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmModeWire {
|
|
clock: u32,
|
|
hdisplay: u16,
|
|
hsync_start: u16,
|
|
hsync_end: u16,
|
|
htotal: u16,
|
|
hskew: u16,
|
|
vdisplay: u16,
|
|
vsync_start: u16,
|
|
vsync_end: u16,
|
|
vtotal: u16,
|
|
vscan: u16,
|
|
vrefresh: u32,
|
|
flags: u32,
|
|
type_: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmSetCrtcWire {
|
|
crtc_id: u32,
|
|
fb_handle: u32,
|
|
connector_count: u32,
|
|
connectors: [u32; 8],
|
|
mode: DrmModeWire,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmPageFlipWire {
|
|
crtc_id: u32,
|
|
fb_handle: u32,
|
|
flags: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmCreateDumbWire {
|
|
width: u32,
|
|
height: u32,
|
|
bpp: u32,
|
|
flags: u32,
|
|
pitch: u32,
|
|
size: u64,
|
|
handle: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmMapDumbWire {
|
|
handle: u32,
|
|
offset: u64,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmDestroyDumbWire {
|
|
handle: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmGetEncoderWire {
|
|
encoder_id: u32,
|
|
encoder_type: u32,
|
|
crtc_id: u32,
|
|
possible_crtcs: u32,
|
|
possible_clones: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmAddFbWire {
|
|
width: u32,
|
|
height: u32,
|
|
pitch: u32,
|
|
bpp: u32,
|
|
depth: u32,
|
|
handle: u32,
|
|
fb_id: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmRmFbWire {
|
|
fb_id: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmGetCrtcWire {
|
|
crtc_id: u32,
|
|
fb_id: u32,
|
|
x: u32,
|
|
y: u32,
|
|
mode_valid: u32,
|
|
mode: DrmModeWire,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmVersionWire {
|
|
major: i32,
|
|
minor: i32,
|
|
patch: i32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmGetCapWire {
|
|
capability: u64,
|
|
value: u64,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmSetClientCapWire {
|
|
capability: u64,
|
|
value: u64,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmGemCreateWire {
|
|
size: u64,
|
|
handle: u32,
|
|
_pad: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmGemCloseWire {
|
|
handle: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmGemMmapWire {
|
|
handle: u32,
|
|
_pad: u32,
|
|
offset: u64,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmPrimeHandleToFdWire {
|
|
handle: u32,
|
|
flags: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmPrimeFdToHandleWire {
|
|
fd: i32,
|
|
_pad: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmPrimeHandleToFdResponseWire {
|
|
fd: i32,
|
|
_pad: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct DrmPrimeFdToHandleResponseWire {
|
|
handle: u32,
|
|
_pad: u32,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct RedoxAmdSdmaSubmitWire {
|
|
src_handle: u32,
|
|
dst_handle: u32,
|
|
flags: u32,
|
|
_pad: u32,
|
|
src_offset: u64,
|
|
dst_offset: u64,
|
|
size: u64,
|
|
seqno: u64,
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
struct RedoxAmdSdmaWaitWire {
|
|
seqno: u64,
|
|
timeout_ns: u64,
|
|
flags: u32,
|
|
completed: u32,
|
|
completed_seqno: u64,
|
|
}
|
|
|
|
// ---- Internal handle types ----
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum NodeKind {
|
|
Card,
|
|
Connector(u32),
|
|
DmaBuf {
|
|
gem_handle: GemHandle,
|
|
export_token: u32,
|
|
},
|
|
}
|
|
|
|
struct Handle {
|
|
node: NodeKind,
|
|
response: Vec<u8>,
|
|
mapped_gem: Option<GemHandle>,
|
|
mapped_gem_refs: usize,
|
|
owned_fbs: Vec<u32>,
|
|
owned_gems: Vec<GemHandle>,
|
|
imported_gems: HashSet<GemHandle>,
|
|
closing: bool,
|
|
}
|
|
|
|
pub struct DrmScheme {
|
|
driver: Arc<dyn GpuDriver>,
|
|
next_id: usize,
|
|
next_fb_id: u32,
|
|
handles: BTreeMap<usize, Handle>,
|
|
active_crtc_fb: BTreeMap<u32, u32>,
|
|
active_crtc_mode: BTreeMap<u32, ModeInfo>,
|
|
pending_flip_fb: BTreeMap<u32, (u64, u32)>,
|
|
fb_registry: BTreeMap<u32, FbInfo>,
|
|
active_gem_maps: BTreeMap<GemHandle, usize>,
|
|
gem_export_refs: BTreeMap<GemHandle, usize>,
|
|
prime_exports: BTreeMap<u32, GemHandle>,
|
|
}
|
|
|
|
impl DrmScheme {
|
|
pub fn new(driver: Arc<dyn GpuDriver>) -> Self {
|
|
Self {
|
|
driver,
|
|
next_id: 0,
|
|
next_fb_id: 1,
|
|
handles: BTreeMap::new(),
|
|
active_crtc_fb: BTreeMap::new(),
|
|
active_crtc_mode: BTreeMap::new(),
|
|
pending_flip_fb: BTreeMap::new(),
|
|
fb_registry: BTreeMap::new(),
|
|
active_gem_maps: BTreeMap::new(),
|
|
gem_export_refs: BTreeMap::new(),
|
|
prime_exports: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
fn is_fb_active(&self, fb_id: u32) -> bool {
|
|
self.active_crtc_fb.values().any(|&id| id == fb_id)
|
|
|| self.pending_flip_fb.values().any(|&(_, id)| id == fb_id)
|
|
}
|
|
|
|
fn handle_has_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool {
|
|
handle.owned_gems.contains(&gem_handle)
|
|
}
|
|
|
|
fn handle_has_local_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool {
|
|
Self::handle_has_gem_ref(handle, gem_handle) && !handle.imported_gems.contains(&gem_handle)
|
|
}
|
|
|
|
fn handle_has_imported_gem_ref(handle: &Handle, gem_handle: GemHandle) -> bool {
|
|
Self::handle_has_gem_ref(handle, gem_handle) && handle.imported_gems.contains(&gem_handle)
|
|
}
|
|
|
|
fn gem_is_still_referenced(&self, gem_handle: GemHandle) -> bool {
|
|
self.handles
|
|
.values()
|
|
.any(|handle| Self::handle_has_gem_ref(handle, gem_handle))
|
|
}
|
|
|
|
fn gem_has_other_refs(&self, current_id: usize, gem_handle: GemHandle) -> bool {
|
|
self.handles.iter().any(|(&other_id, handle)| {
|
|
other_id != current_id && Self::handle_has_gem_ref(handle, gem_handle)
|
|
})
|
|
}
|
|
|
|
fn gem_is_mapped(&self, gem_handle: GemHandle) -> bool {
|
|
self.active_gem_maps.get(&gem_handle).copied().unwrap_or(0) != 0
|
|
}
|
|
|
|
fn gem_export_refcount(&self, gem_handle: GemHandle) -> usize {
|
|
self.gem_export_refs.get(&gem_handle).copied().unwrap_or(0)
|
|
}
|
|
|
|
fn allocate_export_token(&self) -> Result<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);
|
|
}
|
|
|
|
fn drop_export_ref(&mut self, gem_handle: GemHandle) {
|
|
let remove_entry = match self.gem_export_refs.get_mut(&gem_handle) {
|
|
Some(count) if *count > 1 => {
|
|
*count -= 1;
|
|
false
|
|
}
|
|
Some(_) => true,
|
|
None => false,
|
|
};
|
|
if remove_entry {
|
|
self.gem_export_refs.remove(&gem_handle);
|
|
self.prime_exports.retain(|_, &mut h| h != gem_handle);
|
|
}
|
|
}
|
|
|
|
fn gem_can_close(&self, gem_handle: GemHandle) -> bool {
|
|
let backs_fb = self
|
|
.fb_registry
|
|
.values()
|
|
.any(|info| info.gem_handle == gem_handle);
|
|
!backs_fb
|
|
&& !self.gem_is_still_referenced(gem_handle)
|
|
&& !self.gem_is_mapped(gem_handle)
|
|
&& self.gem_export_refcount(gem_handle) == 0
|
|
}
|
|
|
|
fn validate_private_cs_handles(
|
|
&self,
|
|
id: usize,
|
|
src_handle: GemHandle,
|
|
dst_handle: GemHandle,
|
|
operation: &str,
|
|
) -> Result<()> {
|
|
let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
|
|
if !Self::handle_has_gem_ref(handle, src_handle)
|
|
|| !Self::handle_has_gem_ref(handle, dst_handle)
|
|
{
|
|
warn!(
|
|
"redox-drm: {} rejected — src={} dst={} not owned by this fd",
|
|
operation, src_handle, dst_handle
|
|
);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
|
|
if Self::handle_has_imported_gem_ref(handle, src_handle)
|
|
|| Self::handle_has_imported_gem_ref(handle, dst_handle)
|
|
{
|
|
warn!(
|
|
"redox-drm: {} rejected — imported DMA-BUF handles are outside the bounded private CS path",
|
|
operation
|
|
);
|
|
return Err(Error::new(EOPNOTSUPP));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_private_cs_ranges(
|
|
&self,
|
|
submit: &RedoxPrivateCsSubmit,
|
|
operation: &str,
|
|
) -> Result<()> {
|
|
if submit.byte_count == 0 {
|
|
warn!("redox-drm: {} rejected — zero-sized submission", operation);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
|
|
let src_size = self
|
|
.driver
|
|
.gem_size(submit.src_handle)
|
|
.map_err(driver_to_syscall)?;
|
|
let dst_size = self
|
|
.driver
|
|
.gem_size(submit.dst_handle)
|
|
.map_err(driver_to_syscall)?;
|
|
|
|
let src_end = submit
|
|
.src_offset
|
|
.checked_add(submit.byte_count)
|
|
.ok_or_else(|| {
|
|
warn!("redox-drm: {} rejected — source range overflow", operation);
|
|
Error::new(EINVAL)
|
|
})?;
|
|
if src_end > src_size {
|
|
warn!(
|
|
"redox-drm: {} rejected — source range {}..{} exceeds GEM size {}",
|
|
operation,
|
|
submit.src_offset,
|
|
src_end,
|
|
src_size
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
|
|
let dst_end = submit
|
|
.dst_offset
|
|
.checked_add(submit.byte_count)
|
|
.ok_or_else(|| {
|
|
warn!("redox-drm: {} rejected — destination range overflow", operation);
|
|
Error::new(EINVAL)
|
|
})?;
|
|
if dst_end > dst_size {
|
|
warn!(
|
|
"redox-drm: {} rejected — destination range {}..{} exceeds GEM size {}",
|
|
operation,
|
|
submit.dst_offset,
|
|
dst_end,
|
|
dst_size
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_gem_create_size(&self, size: u64, operation: &str) -> Result<()> {
|
|
if size == 0 {
|
|
warn!("redox-drm: {} rejected — zero-sized GEM allocation", operation);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
if size > MAX_SCHEME_GEM_BYTES {
|
|
warn!(
|
|
"redox-drm: {} rejected — size {} exceeds trusted shared-core cap {}",
|
|
operation,
|
|
size,
|
|
MAX_SCHEME_GEM_BYTES
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn maybe_close_gem(&mut self, gem_handle: GemHandle, context: &str) -> bool {
|
|
if !self.gem_can_close(gem_handle) {
|
|
return false;
|
|
}
|
|
|
|
match self.driver.gem_close(gem_handle) {
|
|
Ok(()) => {
|
|
self.prime_exports.retain(|_, &mut h| h != gem_handle);
|
|
true
|
|
}
|
|
Err(e) => {
|
|
warn!(
|
|
"redox-drm: {} gem_close({}) failed: {}",
|
|
context, gem_handle, e
|
|
);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
fn allocate_handle(&mut self, node: NodeKind) -> usize {
|
|
let id = self.next_id;
|
|
self.next_id = self.next_id.saturating_add(1);
|
|
self.handles.insert(
|
|
id,
|
|
Handle {
|
|
node,
|
|
response: Vec::new(),
|
|
mapped_gem: None,
|
|
mapped_gem_refs: 0,
|
|
owned_fbs: Vec::new(),
|
|
owned_gems: Vec::new(),
|
|
imported_gems: HashSet::new(),
|
|
closing: false,
|
|
},
|
|
);
|
|
id
|
|
}
|
|
|
|
fn finalize_handle_close(&mut self, handle: Handle) {
|
|
if let NodeKind::DmaBuf { gem_handle, .. } = handle.node {
|
|
self.drop_export_ref(gem_handle);
|
|
let _ = self.maybe_close_gem(gem_handle, "close dmabuf");
|
|
return;
|
|
}
|
|
|
|
let mut auto_closed_gems = HashSet::new();
|
|
for fb_id in &handle.owned_fbs {
|
|
if self.is_fb_active(*fb_id) {
|
|
continue;
|
|
}
|
|
if let Some(fb_info) = self.fb_registry.remove(fb_id) {
|
|
if self.maybe_close_gem(fb_info.gem_handle, "close") {
|
|
auto_closed_gems.insert(fb_info.gem_handle);
|
|
}
|
|
}
|
|
}
|
|
for gem_handle in handle.owned_gems {
|
|
if auto_closed_gems.contains(&gem_handle) {
|
|
continue;
|
|
}
|
|
let backs_fb = self
|
|
.fb_registry
|
|
.values()
|
|
.any(|info| info.gem_handle == gem_handle);
|
|
if !backs_fb && self.maybe_close_gem(gem_handle, "close gem") {
|
|
auto_closed_gems.insert(gem_handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn pin_mapped_gem(&mut self, id: usize, gem_handle: GemHandle) -> Result<()> {
|
|
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
handle.mapped_gem = Some(gem_handle);
|
|
handle.mapped_gem_refs = handle.mapped_gem_refs.saturating_add(1);
|
|
let entry = self.active_gem_maps.entry(gem_handle).or_insert(0);
|
|
*entry = entry.saturating_add(1);
|
|
Ok(())
|
|
}
|
|
|
|
fn unpin_mapped_gem(&mut self, id: usize) -> Result<()> {
|
|
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
let gem_handle = match handle.mapped_gem {
|
|
Some(gem_handle) if handle.mapped_gem_refs != 0 => gem_handle,
|
|
_ => return Ok(()),
|
|
};
|
|
handle.mapped_gem_refs -= 1;
|
|
if handle.mapped_gem_refs == 0 {
|
|
handle.mapped_gem = None;
|
|
}
|
|
|
|
let remove_entry = match self.active_gem_maps.get_mut(&gem_handle) {
|
|
Some(count) if *count > 1 => {
|
|
*count -= 1;
|
|
false
|
|
}
|
|
Some(_) => true,
|
|
None => false,
|
|
};
|
|
if remove_entry {
|
|
self.active_gem_maps.remove(&gem_handle);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn retire_vblank(&mut self, crtc_id: u32, vblank_count: u64) {
|
|
if let Some((expected, fb_id)) = self.pending_flip_fb.get(&crtc_id).copied() {
|
|
if expected <= vblank_count {
|
|
self.pending_flip_fb.remove(&crtc_id);
|
|
self.try_reap_fb(fb_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn handle_driver_event(&mut self, event: DriverEvent) {
|
|
match event {
|
|
DriverEvent::Vblank { crtc_id, count } => self.retire_vblank(crtc_id, count),
|
|
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,
|
|
None => return,
|
|
};
|
|
let still_owned = self.handles.values().any(|h| h.owned_fbs.contains(&fb_id));
|
|
if still_owned {
|
|
return;
|
|
}
|
|
self.fb_registry.remove(&fb_id);
|
|
let _ = self.maybe_close_gem(gem_handle, "try_reap_fb");
|
|
}
|
|
|
|
// ---- Encode helpers ----
|
|
|
|
fn encode_resources(&self) -> Vec<u8> {
|
|
let connectors = self.driver.detect_connectors();
|
|
let payload = DrmResourcesWire {
|
|
connector_count: connectors.len() as u32,
|
|
crtc_count: 1,
|
|
encoder_count: connectors.len() as u32,
|
|
};
|
|
let mut out = bytes_of(&payload);
|
|
for connector in connectors {
|
|
out.extend_from_slice(&bytes_of(&connector.id));
|
|
}
|
|
out
|
|
}
|
|
|
|
fn encode_connector(&self, connector_id: u32) -> Result<Vec<u8>> {
|
|
let connector = self
|
|
.driver
|
|
.detect_connectors()
|
|
.into_iter()
|
|
.find(|c| c.id == connector_id)
|
|
.ok_or_else(|| Error::new(ENOENT))?;
|
|
|
|
let header = DrmConnectorWire {
|
|
connector_id: connector.id,
|
|
connection: match connector.connection {
|
|
crate::kms::ConnectorStatus::Connected => 1,
|
|
crate::kms::ConnectorStatus::Disconnected => 2,
|
|
crate::kms::ConnectorStatus::Unknown => 0,
|
|
},
|
|
connector_type: connector_type_to_u32(connector.connector_type),
|
|
mm_width: connector.mm_width,
|
|
mm_height: connector.mm_height,
|
|
encoder_id: connector.encoder_id,
|
|
mode_count: connector.modes.len() as u32,
|
|
};
|
|
|
|
let mut out = bytes_of(&header);
|
|
for mode in &connector.modes {
|
|
out.extend_from_slice(&bytes_of(&mode_to_wire(mode)));
|
|
out.extend_from_slice(mode.name.as_bytes());
|
|
out.push(0);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
// ---- ioctl dispatch ----
|
|
|
|
fn handle_ioctl(&mut self, id: usize, request: usize, payload: &[u8]) -> Result<usize> {
|
|
let response = match request {
|
|
DRM_IOCTL_MODE_GETRESOURCES => self.encode_resources(),
|
|
|
|
DRM_IOCTL_MODE_GETCONNECTOR => {
|
|
let connector_id = if payload.len() >= size_of::<u32>() {
|
|
read_u32(payload, 0)?
|
|
} else {
|
|
match self.handles.get(&id).map(|h| &h.node) {
|
|
Some(NodeKind::Connector(cid)) => *cid,
|
|
_ => return Err(Error::new(EINVAL)),
|
|
}
|
|
};
|
|
self.encode_connector(connector_id)?
|
|
}
|
|
|
|
DRM_IOCTL_MODE_GETMODES => {
|
|
let connector_id = read_u32(payload, 0)?;
|
|
let modes = self.driver.get_modes(connector_id);
|
|
encode_modes(&modes)
|
|
}
|
|
|
|
DRM_IOCTL_MODE_SETCRTC => {
|
|
let req = decode_wire::<DrmSetCrtcWire>(payload)?;
|
|
if req.fb_handle == 0 && req.connector_count == 0 {
|
|
let completed_flip = self.pending_flip_fb.remove(&req.crtc_id);
|
|
let prev_fb_id = self.active_crtc_fb.remove(&req.crtc_id);
|
|
self.active_crtc_mode.remove(&req.crtc_id);
|
|
if let Some((_, fb_id)) = completed_flip {
|
|
self.try_reap_fb(fb_id);
|
|
}
|
|
if let Some(fb_id) = prev_fb_id {
|
|
self.try_reap_fb(fb_id);
|
|
}
|
|
return Ok(1);
|
|
}
|
|
let count = req.connector_count as usize;
|
|
if count > req.connectors.len() {
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let conns = req.connectors[..count].to_vec();
|
|
let fb_info = self.fb_registry.get(&req.fb_handle).ok_or_else(|| {
|
|
warn!("redox-drm: SETCRTC with unknown fb_id {}", req.fb_handle);
|
|
Error::new(ENOENT)
|
|
})?;
|
|
let mode = wire_to_mode(&req.mode);
|
|
let fb_pitch = fb_info.pitch as u64;
|
|
let required_fb_lines = mode.vdisplay as u64;
|
|
let fb_height = fb_info.height as u64;
|
|
let fb_width = fb_info.width as u64;
|
|
let mode_width = mode.hdisplay as u64;
|
|
if fb_pitch.checked_mul(required_fb_lines).is_none() {
|
|
warn!("redox-drm: SETCRTC FB pitch * mode_height overflows");
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
if fb_pitch == 0 || fb_height < required_fb_lines || fb_width < mode_width {
|
|
warn!(
|
|
"redox-drm: SETCRTC FB {}x{} pitch={} too small for mode {}x{}",
|
|
fb_info.width, fb_info.height, fb_info.pitch, mode.hdisplay, mode.vdisplay
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let gem_handle = fb_info.gem_handle;
|
|
self.driver
|
|
.set_crtc(req.crtc_id, gem_handle, &conns, &mode)
|
|
.map_err(driver_to_syscall)?;
|
|
let completed_flip = self.pending_flip_fb.remove(&req.crtc_id);
|
|
let prev_fb = self.active_crtc_fb.insert(req.crtc_id, req.fb_handle);
|
|
self.active_crtc_mode.insert(req.crtc_id, mode);
|
|
if let Some((_, fb_id)) = completed_flip {
|
|
self.try_reap_fb(fb_id);
|
|
}
|
|
if let Some(prev) = prev_fb {
|
|
if prev != req.fb_handle {
|
|
self.try_reap_fb(prev);
|
|
}
|
|
}
|
|
Vec::new()
|
|
}
|
|
|
|
DRM_IOCTL_MODE_PAGE_FLIP => {
|
|
let req = decode_wire::<DrmPageFlipWire>(payload)?;
|
|
if self.pending_flip_fb.contains_key(&req.crtc_id) {
|
|
warn!(
|
|
"redox-drm: PAGE_FLIP rejected — flip already pending on CRTC {}",
|
|
req.crtc_id
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
let fb_info = self.fb_registry.get(&req.fb_handle).ok_or_else(|| {
|
|
warn!("redox-drm: PAGE_FLIP with unknown fb_id {}", req.fb_handle);
|
|
Error::new(ENOENT)
|
|
})?;
|
|
if let Some(active_mode) = self.active_crtc_mode.get(&req.crtc_id) {
|
|
let fb_pitch = fb_info.pitch as u64;
|
|
let required_lines = active_mode.vdisplay as u64;
|
|
let required_width = active_mode.hdisplay as u64;
|
|
if fb_pitch == 0
|
|
|| (fb_info.height as u64) < required_lines
|
|
|| (fb_info.width as u64) < required_width
|
|
{
|
|
warn!(
|
|
"redox-drm: PAGE_FLIP FB {}x{} pitch={} too small for active mode {}x{}",
|
|
fb_info.width, fb_info.height, fb_info.pitch,
|
|
active_mode.hdisplay, active_mode.vdisplay
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
}
|
|
let gem_handle = fb_info.gem_handle;
|
|
let seqno = self
|
|
.driver
|
|
.page_flip(req.crtc_id, gem_handle, req.flags)
|
|
.map_err(driver_to_syscall)?;
|
|
let current_vblank = self.driver.get_vblank(req.crtc_id).unwrap_or(0);
|
|
let prev = self.active_crtc_fb.insert(req.crtc_id, req.fb_handle);
|
|
if let Some(old_fb) = prev {
|
|
if old_fb != req.fb_handle {
|
|
self.pending_flip_fb
|
|
.insert(req.crtc_id, (current_vblank.saturating_add(1), old_fb));
|
|
}
|
|
}
|
|
seqno.to_le_bytes().to_vec()
|
|
}
|
|
|
|
DRM_IOCTL_MODE_CREATE_DUMB => {
|
|
let mut req = decode_wire::<DrmCreateDumbWire>(payload)?;
|
|
let pitch = (req.width.saturating_mul(req.bpp).saturating_add(7)) / 8;
|
|
req.pitch = pitch;
|
|
req.size = (pitch as u64).saturating_mul(req.height as u64);
|
|
self.validate_gem_create_size(req.size, "CREATE_DUMB")?;
|
|
req.handle = self
|
|
.driver
|
|
.gem_create(req.size)
|
|
.map_err(driver_to_syscall)?;
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.owned_gems.push(req.handle);
|
|
}
|
|
bytes_of(&req)
|
|
}
|
|
|
|
DRM_IOCTL_MODE_MAP_DUMB => {
|
|
let mut req = decode_wire::<DrmMapDumbWire>(payload)?;
|
|
let owned = self
|
|
.handles
|
|
.get(&id)
|
|
.map(|h| Self::handle_has_gem_ref(h, req.handle))
|
|
.unwrap_or(false);
|
|
if !owned {
|
|
warn!(
|
|
"redox-drm: MAP_DUMB handle {} not owned by this fd",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
if let Some(handle) = self.handles.get(&id) {
|
|
if handle.mapped_gem_refs != 0 && handle.mapped_gem != Some(req.handle) {
|
|
warn!(
|
|
"redox-drm: MAP_DUMB handle {} rejected — another GEM is still mapped",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
}
|
|
req.offset = self
|
|
.driver
|
|
.gem_mmap(req.handle)
|
|
.map_err(driver_to_syscall)? as u64;
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.mapped_gem = Some(req.handle);
|
|
}
|
|
bytes_of(&req)
|
|
}
|
|
|
|
DRM_IOCTL_MODE_DESTROY_DUMB => {
|
|
let req = decode_wire::<DrmDestroyDumbWire>(payload)?;
|
|
let owned = self
|
|
.handles
|
|
.get(&id)
|
|
.map(|h| h.owned_gems.contains(&req.handle))
|
|
.unwrap_or(false);
|
|
if !owned {
|
|
warn!(
|
|
"redox-drm: DESTROY_DUMB handle {} not owned by this fd",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
let backs_fb = self
|
|
.fb_registry
|
|
.values()
|
|
.any(|info| info.gem_handle == req.handle);
|
|
if backs_fb {
|
|
warn!(
|
|
"redox-drm: DESTROY_DUMB handle {} rejected — backs an active framebuffer",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
if self.gem_is_mapped(req.handle) {
|
|
warn!(
|
|
"redox-drm: DESTROY_DUMB handle {} rejected — still mapped",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
let close_now = !self.gem_has_other_refs(id, req.handle)
|
|
&& self.gem_export_refcount(req.handle) == 0;
|
|
if close_now {
|
|
self.driver
|
|
.gem_close(req.handle)
|
|
.map_err(driver_to_syscall)?;
|
|
self.prime_exports.retain(|_, &mut h| h != req.handle);
|
|
}
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.owned_gems.retain(|&h| h != req.handle);
|
|
handle.imported_gems.remove(&req.handle);
|
|
}
|
|
Vec::new()
|
|
}
|
|
|
|
DRM_IOCTL_MODE_GETENCODER => {
|
|
let _req = decode_wire::<DrmGetEncoderWire>(payload)?;
|
|
let resp = DrmGetEncoderWire {
|
|
encoder_id: _req.encoder_id,
|
|
encoder_type: 0,
|
|
crtc_id: 1,
|
|
possible_crtcs: 1,
|
|
possible_clones: 0,
|
|
};
|
|
bytes_of(&resp)
|
|
}
|
|
|
|
DRM_IOCTL_MODE_GETCRTC => {
|
|
let req = decode_wire::<DrmGetCrtcWire>(payload)?;
|
|
let (fb_id, mode_valid, mode) = match (
|
|
self.active_crtc_fb.get(&req.crtc_id),
|
|
self.active_crtc_mode.get(&req.crtc_id),
|
|
) {
|
|
(Some(&fb), Some(m)) if self.fb_registry.contains_key(&fb) => {
|
|
(fb, 1u32, mode_to_wire(m))
|
|
}
|
|
_ => (0u32, 0u32, DrmModeWire::default()),
|
|
};
|
|
let resp = DrmGetCrtcWire {
|
|
crtc_id: req.crtc_id,
|
|
fb_id,
|
|
x: 0,
|
|
y: 0,
|
|
mode_valid,
|
|
mode,
|
|
};
|
|
bytes_of(&resp)
|
|
}
|
|
|
|
DRM_IOCTL_MODE_ADDFB => {
|
|
let req = decode_wire::<DrmAddFbWire>(payload)?;
|
|
if req.handle == 0 {
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
if req.width == 0 || req.height == 0 || req.bpp == 0 {
|
|
warn!(
|
|
"redox-drm: ADDFB zero dimension width={} height={} bpp={}",
|
|
req.width, req.height, req.bpp
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let min_stride = (req.width.saturating_mul(req.bpp).saturating_add(7)) / 8;
|
|
let pitch = if req.pitch != 0 {
|
|
req.pitch
|
|
} else {
|
|
min_stride
|
|
};
|
|
if pitch == 0 || pitch < min_stride {
|
|
warn!(
|
|
"redox-drm: ADDFB pitch {} below minimum stride {} ({}x{})",
|
|
pitch, min_stride, req.width, req.bpp
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let required_size = (pitch as u64).checked_mul(req.height as u64);
|
|
if required_size.is_none() {
|
|
warn!(
|
|
"redox-drm: ADDFB pitch * height overflows pitch={} height={}",
|
|
pitch, req.height
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let owned = self
|
|
.handles
|
|
.get(&id)
|
|
.map(|h| Self::handle_has_gem_ref(h, req.handle))
|
|
.unwrap_or(false);
|
|
if !owned {
|
|
warn!(
|
|
"redox-drm: ADDFB handle {} not owned by this fd",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
let actual_size = self.driver.gem_size(req.handle).map_err(|e| {
|
|
warn!("redox-drm: ADDFB handle {} not found: {}", req.handle, e);
|
|
Error::new(ENOENT)
|
|
})?;
|
|
if required_size.unwrap() > actual_size {
|
|
warn!(
|
|
"redox-drm: ADDFB requires {} bytes but GEM {} is {} bytes",
|
|
required_size.unwrap(),
|
|
req.handle,
|
|
actual_size
|
|
);
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let fb_id = self.next_fb_id;
|
|
self.next_fb_id = self.next_fb_id.saturating_add(1);
|
|
self.fb_registry.insert(
|
|
fb_id,
|
|
FbInfo {
|
|
gem_handle: req.handle,
|
|
width: req.width,
|
|
height: req.height,
|
|
pitch,
|
|
bpp: req.bpp,
|
|
},
|
|
);
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.owned_fbs.push(fb_id);
|
|
}
|
|
let mut resp = req;
|
|
resp.fb_id = fb_id;
|
|
bytes_of(&resp)
|
|
}
|
|
|
|
DRM_IOCTL_MODE_RMFB => {
|
|
let req = decode_wire::<DrmRmFbWire>(payload)?;
|
|
let owned = self
|
|
.handles
|
|
.get(&id)
|
|
.map(|h| h.owned_fbs.contains(&req.fb_id))
|
|
.unwrap_or(false);
|
|
if !owned {
|
|
warn!("redox-drm: RMFB {} not owned by this fd", req.fb_id);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
let in_use = self.is_fb_active(req.fb_id);
|
|
if in_use {
|
|
warn!(
|
|
"redox-drm: RMFB {} rejected — still active on a CRTC",
|
|
req.fb_id
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
if let Some(fb_info) = self.fb_registry.remove(&req.fb_id) {
|
|
let _ = self.maybe_close_gem(fb_info.gem_handle, "RMFB");
|
|
}
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.owned_fbs.retain(|&fb| fb != req.fb_id);
|
|
}
|
|
Vec::new()
|
|
}
|
|
|
|
DRM_IOCTL_GET_CAP => {
|
|
let mut req = decode_wire::<DrmGetCapWire>(payload)?;
|
|
req.value = match req.capability {
|
|
0 => 1,
|
|
1 => 1,
|
|
_ => 0,
|
|
};
|
|
bytes_of(&req)
|
|
}
|
|
|
|
DRM_IOCTL_SET_CLIENT_CAP => Vec::new(),
|
|
|
|
DRM_IOCTL_VERSION => {
|
|
let resp = DrmVersionWire {
|
|
major: 1,
|
|
minor: 0,
|
|
patch: 0,
|
|
};
|
|
bytes_of(&resp)
|
|
}
|
|
|
|
DRM_IOCTL_GEM_CREATE => {
|
|
let mut req = decode_wire::<DrmGemCreateWire>(payload)?;
|
|
self.validate_gem_create_size(req.size, "GEM_CREATE")?;
|
|
req.handle = self
|
|
.driver
|
|
.gem_create(req.size)
|
|
.map_err(driver_to_syscall)?;
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.owned_gems.push(req.handle);
|
|
}
|
|
bytes_of(&req)
|
|
}
|
|
|
|
DRM_IOCTL_GEM_CLOSE => {
|
|
let req = decode_wire::<DrmGemCloseWire>(payload)?;
|
|
let owned = self
|
|
.handles
|
|
.get(&id)
|
|
.map(|h| h.owned_gems.contains(&req.handle))
|
|
.unwrap_or(false);
|
|
if !owned {
|
|
warn!(
|
|
"redox-drm: GEM_CLOSE handle {} not owned by this fd",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
let backs_fb = self
|
|
.fb_registry
|
|
.values()
|
|
.any(|info| info.gem_handle == req.handle);
|
|
if backs_fb {
|
|
warn!(
|
|
"redox-drm: GEM_CLOSE handle {} rejected — backs an active framebuffer",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
if self.gem_is_mapped(req.handle) {
|
|
warn!(
|
|
"redox-drm: GEM_CLOSE handle {} rejected — still mapped",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
let close_now = !self.gem_has_other_refs(id, req.handle)
|
|
&& self.gem_export_refcount(req.handle) == 0;
|
|
if close_now {
|
|
self.driver
|
|
.gem_close(req.handle)
|
|
.map_err(driver_to_syscall)?;
|
|
self.prime_exports.retain(|_, &mut h| h != req.handle);
|
|
}
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.owned_gems.retain(|&h| h != req.handle);
|
|
handle.imported_gems.remove(&req.handle);
|
|
}
|
|
Vec::new()
|
|
}
|
|
|
|
DRM_IOCTL_GEM_MMAP => {
|
|
let mut req = decode_wire::<DrmGemMmapWire>(payload)?;
|
|
let owned = self
|
|
.handles
|
|
.get(&id)
|
|
.map(|h| Self::handle_has_gem_ref(h, req.handle))
|
|
.unwrap_or(false);
|
|
if !owned {
|
|
warn!(
|
|
"redox-drm: GEM_MMAP handle {} not owned by this fd",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
if let Some(handle) = self.handles.get(&id) {
|
|
if handle.mapped_gem_refs != 0 && handle.mapped_gem != Some(req.handle) {
|
|
warn!(
|
|
"redox-drm: GEM_MMAP handle {} rejected — another GEM is still mapped",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBUSY));
|
|
}
|
|
}
|
|
req.offset = self
|
|
.driver
|
|
.gem_mmap(req.handle)
|
|
.map_err(driver_to_syscall)? as u64;
|
|
if let Some(handle) = self.handles.get_mut(&id) {
|
|
handle.mapped_gem = Some(req.handle);
|
|
}
|
|
bytes_of(&req)
|
|
}
|
|
|
|
DRM_IOCTL_REDOX_AMD_SDMA_SUBMIT => {
|
|
let mut req = decode_wire::<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
|
|
.handles
|
|
.get(&id)
|
|
.map(|h| Self::handle_has_gem_ref(h, req.handle))
|
|
.unwrap_or(false);
|
|
if !owned {
|
|
warn!(
|
|
"redox-drm: PRIME_HANDLE_TO_FD handle {} not owned by this fd",
|
|
req.handle
|
|
);
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
|
|
let token = self.allocate_export_token()?;
|
|
self.prime_exports.insert(token, req.handle);
|
|
|
|
let resp = DrmPrimeHandleToFdResponseWire {
|
|
fd: token as i32,
|
|
_pad: 0,
|
|
};
|
|
bytes_of(&resp)
|
|
}
|
|
|
|
DRM_IOCTL_PRIME_FD_TO_HANDLE => {
|
|
let req = decode_wire::<DrmPrimeFdToHandleWire>(payload)?;
|
|
let token = if req.fd >= 0 {
|
|
req.fd as u32
|
|
} else {
|
|
warn!("redox-drm: PRIME_FD_TO_HANDLE invalid token {}", req.fd);
|
|
return Err(Error::new(EBADF));
|
|
};
|
|
|
|
// The token comes from fpath() on the dmabuf fd, which embeds
|
|
// the opaque export token (not the raw GEM handle).
|
|
let gem_handle = match self.prime_exports.get(&token).copied() {
|
|
Some(h) => h,
|
|
None => {
|
|
warn!("redox-drm: PRIME_FD_TO_HANDLE token {} not found", token);
|
|
return Err(Error::new(ENOENT));
|
|
}
|
|
};
|
|
|
|
// Verify the GEM is still live — the exporter may have closed it
|
|
// before any dmabuf fd was opened, leaving a stale token.
|
|
self.driver.gem_size(gem_handle).map_err(|_| {
|
|
warn!(
|
|
"redox-drm: PRIME_FD_TO_HANDLE token {} maps to dead GEM {}",
|
|
token, gem_handle
|
|
);
|
|
// Clean up the stale token so future calls fail fast.
|
|
self.prime_exports.remove(&token);
|
|
Error::new(ENOENT)
|
|
})?;
|
|
|
|
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
if !handle.owned_gems.contains(&gem_handle) {
|
|
handle.owned_gems.push(gem_handle);
|
|
handle.imported_gems.insert(gem_handle);
|
|
}
|
|
|
|
let resp = DrmPrimeFdToHandleResponseWire {
|
|
handle: gem_handle,
|
|
_pad: 0,
|
|
};
|
|
bytes_of(&resp)
|
|
}
|
|
|
|
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT => {
|
|
let req = decode_wire::<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));
|
|
}
|
|
};
|
|
|
|
let response = if response.is_empty() {
|
|
vec![0]
|
|
} else {
|
|
response
|
|
};
|
|
|
|
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
let len = response.len();
|
|
handle.response = response;
|
|
Ok(len)
|
|
}
|
|
}
|
|
|
|
// ---- SchemeBlockMut implementation ----
|
|
|
|
impl SchemeBlockMut for DrmScheme {
|
|
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
|
|
let node = match path.trim_matches('/') {
|
|
"card0" => NodeKind::Card,
|
|
p if p.starts_with("card0Connector/") => {
|
|
let tail = p.trim_start_matches("card0Connector/");
|
|
let connector_id = tail.parse::<u32>().map_err(|_| Error::new(ENOENT))?;
|
|
NodeKind::Connector(connector_id)
|
|
}
|
|
p if p.starts_with("card0/dmabuf/") => {
|
|
let tail = p.trim_start_matches("card0/dmabuf/");
|
|
let token = tail.parse::<u32>().map_err(|_| Error::new(ENOENT))?;
|
|
let gem_handle = match self.prime_exports.get(&token).copied() {
|
|
Some(h) => h,
|
|
None => return Err(Error::new(ENOENT)),
|
|
};
|
|
self.driver.gem_size(gem_handle).map_err(|_| {
|
|
warn!(
|
|
"redox-drm: open dmabuf token {} maps to dead GEM {}",
|
|
token, gem_handle
|
|
);
|
|
self.prime_exports.remove(&token);
|
|
Error::new(ENOENT)
|
|
})?;
|
|
NodeKind::DmaBuf {
|
|
gem_handle,
|
|
export_token: token,
|
|
}
|
|
}
|
|
_ => return Err(Error::new(ENOENT)),
|
|
};
|
|
|
|
if let NodeKind::DmaBuf { gem_handle, .. } = &node {
|
|
self.bump_export_ref(*gem_handle);
|
|
}
|
|
|
|
let id = self.allocate_handle(node);
|
|
Ok(Some(id))
|
|
}
|
|
|
|
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
|
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
let len = handle.response.len().min(buf.len());
|
|
buf[..len].copy_from_slice(&handle.response[..len]);
|
|
Ok(Some(len))
|
|
}
|
|
|
|
fn write(&mut self, id: usize, buf: &[u8]) -> Result<Option<usize>> {
|
|
let (request_bytes, payload) = match buf.split_first_chunk::<8>() {
|
|
Some(pair) => pair,
|
|
None => {
|
|
let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
return Ok(Some(0));
|
|
}
|
|
};
|
|
let request = usize::from_le_bytes(*request_bytes);
|
|
let written = self.handle_ioctl(id, request, payload)?;
|
|
Ok(Some(written))
|
|
}
|
|
|
|
fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
|
let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
let path = match handle.node {
|
|
NodeKind::Card => "drm:card0".to_string(),
|
|
NodeKind::Connector(cid) => format!("drm:card0Connector/{cid}"),
|
|
NodeKind::DmaBuf { export_token, .. } => format!("drm:card0/dmabuf/{export_token}"),
|
|
};
|
|
let bytes = path.as_bytes();
|
|
let len = bytes.len().min(buf.len());
|
|
buf[..len].copy_from_slice(&bytes[..len]);
|
|
Ok(Some(len))
|
|
}
|
|
|
|
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
|
|
let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
stat.st_mode = MODE_FILE | 0o666;
|
|
stat.st_size = handle.response.len() as u64;
|
|
stat.st_blksize = 4096;
|
|
Ok(Some(0))
|
|
}
|
|
|
|
fn fsync(&mut self, id: usize) -> Result<Option<usize>> {
|
|
let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
warn!(
|
|
"redox-drm: fsync rejected — shared core has no implicit render-fence sync contract"
|
|
);
|
|
Err(Error::new(EOPNOTSUPP))
|
|
}
|
|
|
|
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
|
|
let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
Ok(Some(EventFlags::empty()))
|
|
}
|
|
|
|
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
|
let mapped = self
|
|
.handles
|
|
.get(&id)
|
|
.ok_or_else(|| Error::new(EBADF))?
|
|
.mapped_gem_refs;
|
|
if mapped != 0 {
|
|
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
handle.closing = true;
|
|
return Ok(Some(0));
|
|
}
|
|
|
|
if let Some(handle) = self.handles.remove(&id) {
|
|
self.finalize_handle_close(handle);
|
|
} else {
|
|
return Err(Error::new(EBADF));
|
|
}
|
|
Ok(Some(0))
|
|
}
|
|
|
|
fn mmap_prep(
|
|
&mut self,
|
|
id: usize,
|
|
offset: u64,
|
|
size: usize,
|
|
_flags: MapFlags,
|
|
) -> Result<Option<usize>> {
|
|
let gem_handle = {
|
|
let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
match handle.node {
|
|
NodeKind::DmaBuf { gem_handle, .. } => gem_handle,
|
|
_ => handle.mapped_gem.ok_or_else(|| Error::new(EINVAL))?,
|
|
}
|
|
};
|
|
|
|
let gem_size = self
|
|
.driver
|
|
.gem_size(gem_handle)
|
|
.map_err(driver_to_syscall)?;
|
|
|
|
if offset > gem_size {
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let remaining = gem_size - offset;
|
|
if size as u64 > remaining {
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
|
|
let base_addr = self
|
|
.driver
|
|
.gem_mmap(gem_handle)
|
|
.map_err(driver_to_syscall)?;
|
|
let addr = base_addr + offset as usize;
|
|
self.pin_mapped_gem(id, gem_handle)?;
|
|
debug!(
|
|
"redox-drm: mmap_prep GEM handle {} offset={} size={} at addr={:#x}",
|
|
gem_handle, offset, size, addr
|
|
);
|
|
Ok(Some(addr))
|
|
}
|
|
|
|
fn munmap(
|
|
&mut self,
|
|
id: usize,
|
|
offset: u64,
|
|
size: usize,
|
|
_flags: MunmapFlags,
|
|
) -> Result<Option<usize>> {
|
|
let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
|
self.unpin_mapped_gem(id)?;
|
|
debug!(
|
|
"redox-drm: munmap id={} offset={} size={}",
|
|
id, offset, size
|
|
);
|
|
let should_finalize = self
|
|
.handles
|
|
.get(&id)
|
|
.map(|handle| handle.closing && handle.mapped_gem_refs == 0)
|
|
.unwrap_or(false);
|
|
if should_finalize {
|
|
if let Some(handle) = self.handles.remove(&id) {
|
|
self.finalize_handle_close(handle);
|
|
}
|
|
}
|
|
Ok(Some(0))
|
|
}
|
|
}
|
|
|
|
// ---- Conversion helpers ----
|
|
|
|
fn connector_type_to_u32(ct: crate::kms::ConnectorType) -> u32 {
|
|
match ct {
|
|
crate::kms::ConnectorType::Unknown => 0,
|
|
crate::kms::ConnectorType::VGA => 1,
|
|
crate::kms::ConnectorType::DVII => 2,
|
|
crate::kms::ConnectorType::DVID => 3,
|
|
crate::kms::ConnectorType::DVIA => 4,
|
|
crate::kms::ConnectorType::Composite => 5,
|
|
crate::kms::ConnectorType::SVideo => 6,
|
|
crate::kms::ConnectorType::LVDS => 7,
|
|
crate::kms::ConnectorType::Component => 8,
|
|
crate::kms::ConnectorType::NinePinDIN => 9,
|
|
crate::kms::ConnectorType::DisplayPort => 10,
|
|
crate::kms::ConnectorType::HDMIA => 11,
|
|
crate::kms::ConnectorType::HDMIB => 12,
|
|
crate::kms::ConnectorType::TV => 13,
|
|
crate::kms::ConnectorType::EDP => 14,
|
|
crate::kms::ConnectorType::Virtual => 15,
|
|
}
|
|
}
|
|
|
|
fn mode_to_wire(mode: &ModeInfo) -> DrmModeWire {
|
|
DrmModeWire {
|
|
clock: mode.clock,
|
|
hdisplay: mode.hdisplay,
|
|
hsync_start: mode.hsync_start,
|
|
hsync_end: mode.hsync_end,
|
|
htotal: mode.htotal,
|
|
hskew: mode.hskew,
|
|
vdisplay: mode.vdisplay,
|
|
vsync_start: mode.vsync_start,
|
|
vsync_end: mode.vsync_end,
|
|
vtotal: mode.vtotal,
|
|
vscan: mode.vscan,
|
|
vrefresh: mode.vrefresh,
|
|
flags: mode.flags,
|
|
type_: mode.type_,
|
|
}
|
|
}
|
|
|
|
fn wire_to_mode(w: &DrmModeWire) -> ModeInfo {
|
|
ModeInfo {
|
|
clock: w.clock,
|
|
hdisplay: w.hdisplay,
|
|
hsync_start: w.hsync_start,
|
|
hsync_end: w.hsync_end,
|
|
htotal: w.htotal,
|
|
hskew: w.hskew,
|
|
vdisplay: w.vdisplay,
|
|
vsync_start: w.vsync_start,
|
|
vsync_end: w.vsync_end,
|
|
vtotal: w.vtotal,
|
|
vscan: w.vscan,
|
|
vrefresh: w.vrefresh,
|
|
flags: w.flags,
|
|
type_: w.type_,
|
|
name: format!("{}x{}@{}", w.hdisplay, w.vdisplay, w.vrefresh),
|
|
}
|
|
}
|
|
|
|
fn encode_modes(modes: &[ModeInfo]) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
for mode in modes {
|
|
out.extend_from_slice(&bytes_of(&mode_to_wire(mode)));
|
|
out.extend_from_slice(mode.name.as_bytes());
|
|
out.push(0);
|
|
}
|
|
if out.is_empty() {
|
|
out.push(0);
|
|
}
|
|
out
|
|
}
|
|
|
|
fn bytes_of<T>(value: &T) -> Vec<u8> {
|
|
let ptr = value as *const T as *const u8;
|
|
let len = size_of::<T>();
|
|
unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec()
|
|
}
|
|
|
|
fn read_u32(buf: &[u8], offset: usize) -> Result<u32> {
|
|
let end = offset.saturating_add(size_of::<u32>());
|
|
let bytes = buf.get(offset..end).ok_or_else(|| Error::new(EINVAL))?;
|
|
let array: [u8; 4] = bytes.try_into().map_err(|_| Error::new(EINVAL))?;
|
|
Ok(u32::from_le_bytes(array))
|
|
}
|
|
|
|
fn decode_wire<T: Copy>(buf: &[u8]) -> Result<T> {
|
|
if buf.len() < size_of::<T>() {
|
|
return Err(Error::new(EINVAL));
|
|
}
|
|
let ptr = buf.as_ptr() as *const T;
|
|
Ok(unsafe { ptr.read_unaligned() })
|
|
}
|
|
|
|
fn driver_to_syscall(error: crate::driver::DriverError) -> Error {
|
|
warn!("redox-drm: driver error: {}", error);
|
|
match error {
|
|
crate::driver::DriverError::Unsupported(_) => Error::new(EOPNOTSUPP),
|
|
crate::driver::DriverError::InvalidArgument(_) => Error::new(EINVAL),
|
|
crate::driver::DriverError::NotFound(_) => Error::new(ENOENT),
|
|
crate::driver::DriverError::Initialization(_)
|
|
| crate::driver::DriverError::Mmio(_)
|
|
| crate::driver::DriverError::Pci(_)
|
|
| crate::driver::DriverError::Buffer(_)
|
|
| crate::driver::DriverError::Io(_) => Error::new(EINVAL),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::BTreeMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use redox_scheme::SchemeBlockMut;
|
|
|
|
use super::*;
|
|
use crate::driver::{DriverError, DriverEvent, GpuDriver};
|
|
use crate::kms::{ConnectorInfo, ModeInfo};
|
|
|
|
#[derive(Default)]
|
|
struct FakeDriverState {
|
|
next_handle: GemHandle,
|
|
gem_sizes: BTreeMap<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);
|
|
}
|
|
}
|