drm: VIRGL ctx attach/detach, full atomic ioctl parser, code readability

Gap 11 (CTX_ATTACH/DETACH_RESOURCE):
- Add virgl_ctx_attach_resource + virgl_ctx_detach_resource
  to GpuDriver trait with default Unsupported fallbacks
- Implement ctx_attach_resource + ctx_detach_resource on
  VirtioGpuDevice using existing VirtioGpuCtxResource wire struct
- Wire both into VirtioDriver GpuDriver impl with has_virgl_3d gating
- Binds 3D resources to GL contexts for subsequent SUBMIT_3D calls

Gap 12 (Atomic ioctl full parser):
- Parse drm_mode_atomic header: flags, count_objs, objs_ptr,
  count_props_ptr, props_ptr, prop_values_ptr
- Read object ID array and per-object property arrays from
  inline payload offsets
- Detect CRTC objects and extract FB_ID, MODE_ID, ACTIVE props
- Build AtomicState with CRTC mode+fb configurations
- Support TEST_ONLY, NONBLOCK, ALLOW_MODESET flags
- Add DRM_MODE_ATOMIC_ALLOW_MODESET constant (0x0400)
- Add read_u64() helper for 64-bit property values

Code readability:
- Module-level documentation for VirtioDriver struct
- Lock-ordering constraint comment on virgl_resource_create_blob
- poll_hotplug purpose explanation (compositor polling vs IRQ)
- atomic_commit dispatch comment (validate then delegate)
This commit is contained in:
2026-06-02 17:34:50 +03:00
parent 64fa2c49ef
commit a17dccf3dc
3 changed files with 123 additions and 0 deletions
@@ -236,6 +236,18 @@ pub trait GpuDriver: Send + Sync {
Err(DriverError::Unsupported("virgl context destruction"))
}
fn virgl_ctx_attach_resource(&self, _ctx_id: u32, _resource_id: u32) -> Result<()> {
Err(DriverError::Unsupported(
"virgl context resource attach requires Mesa virgl + redox-drm CS ioctl backend",
))
}
fn virgl_ctx_detach_resource(&self, _ctx_id: u32, _resource_id: u32) -> Result<()> {
Err(DriverError::Unsupported(
"virgl context resource detach requires Mesa virgl + redox-drm CS ioctl backend",
))
}
fn virgl_resource_create_3d(
&self,
_resource_id: u32,
@@ -50,11 +50,16 @@ use self::transport::VirtioModernPciTransport;
use self::virtqueue::Virtqueue;
const DRIVER_DATE: &str = "2026-05-12";
// Limit virtqueue depth to control DMA memory usage and latency.
const DEFAULT_QUEUE_SIZE: u16 = 32;
const CTRL_QUEUE_INDEX: u16 = 0;
const CURSOR_QUEUE_INDEX: u16 = 1;
const COMMAND_TIMEOUT: Duration = Duration::from_millis(250);
// VirtioDriver is the GpuDriver implementation for virtio-gpu devices.
// It handles 2D KMS display, VIRGL 3D passthrough, hardware cursor,
// and blob resource management. Each driver instance manages one
// virtio-gpu device discovered via PCI vendor ID 0x1AF4.
pub struct VirtioDriver {
_info: PciDeviceInfo,
device: Mutex<VirtioGpuDevice>,
@@ -627,6 +632,8 @@ impl GpuDriver for VirtioDriver {
self.handle_irq_event()
}
// Connector pulse-check: refresh topology and emit Hotplug event on change.
// Used by compositors that poll for display changes rather than relying on IRQ.
fn poll_hotplug(&self) -> Result<Option<DriverEvent>> {
let previous = self.cached_connectors();
let current = self.refresh_connectors()?;
@@ -714,6 +721,28 @@ impl GpuDriver for VirtioDriver {
device.ctx_destroy(ctx_id)
}
fn virgl_ctx_attach_resource(&self, ctx_id: u32, resource_id: u32) -> Result<()> {
let mut device = self
.device
.lock()
.map_err(|_| DriverError::Initialization("VirtIO device state poisoned".into()))?;
if !device.has_virgl_3d {
return Err(DriverError::Unsupported("virgl context attach resource"));
}
device.ctx_attach_resource(ctx_id, resource_id)
}
fn virgl_ctx_detach_resource(&self, ctx_id: u32, resource_id: u32) -> Result<()> {
let mut device = self
.device
.lock()
.map_err(|_| DriverError::Initialization("VirtIO device state poisoned".into()))?;
if !device.has_virgl_3d {
return Err(DriverError::Unsupported("virgl context detach resource"));
}
device.ctx_detach_resource(ctx_id, resource_id)
}
#[allow(clippy::too_many_arguments)]
fn virgl_resource_create_3d(
&self,
@@ -899,6 +928,12 @@ impl GpuDriver for VirtioDriver {
device.resource_attach_backing(resource.resource_id, phys_addr, length)
}
// Create a blob resource — host-allocated or guest-allocated GPU memory
// shared between guest and host. VIRTIO_GPU_F_RESOURCE_BLOB (bit 3) must be
// negotiated. Creates a GEM buffer, registers it as a blob with the host,
// and returns the GEM handle + virtio resource ID.
// The device lock must be released before calling gem_create because
// gem_create internally locks the device for 2D resource initialization.
fn virgl_resource_create_blob(
&self, blob_mem: u32, blob_flags: u32, blob_id: u64, size: u64,
) -> Result<(GemHandle, u32)> {
@@ -1013,6 +1048,10 @@ impl GpuDriver for VirtioDriver {
device.move_cursor(scanout_id, x, y)
}
// Atomic modeset — validate the full CRTC/connector state then apply.
// Calls atomic_check() to verify clock limits, mode dimensions, and
// CRTC availability. On TEST_ONLY, returns NoChange without applying.
// Delegates to set_crtc + page_flip for each active CRTC in the state.
fn atomic_commit(&self, state: &AtomicState) -> Result<AtomicCommitResult> {
use crate::kms::atomic::AtomicCheckResult;
let connectors = self.detect_connectors();
@@ -1307,6 +1346,26 @@ impl VirtioGpuDevice {
self.submit_nodata(&request)
}
fn ctx_attach_resource(&mut self, ctx_id: u32, resource_id: u32) -> Result<()> {
let mut request = VirtioGpuCtxResource {
hdr: VirtioGpuCtrlHeader::command(VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE),
resource_id,
padding: 0,
};
request.hdr.ctx_id = ctx_id;
self.submit_nodata(&request)
}
fn ctx_detach_resource(&mut self, ctx_id: u32, resource_id: u32) -> Result<()> {
let mut request = VirtioGpuCtxResource {
hdr: VirtioGpuCtrlHeader::command(VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE),
resource_id,
padding: 0,
};
request.hdr.ctx_id = ctx_id;
self.submit_nodata(&request)
}
#[allow(clippy::too_many_arguments)]
fn resource_create_3d(
&mut self,
@@ -77,6 +77,7 @@ const DRM_IOCTL_REDOX_SYNCOBJ_HANDLE_TO_FD: usize = DRM_IOCTL_BASE + 0x73;
const DRM_IOCTL_REDOX_SYNCOBJ_FD_TO_HANDLE: usize = DRM_IOCTL_BASE + 0x74;
const DRM_MODE_ATOMIC_TEST_ONLY: u32 = 0x0100;
const DRM_MODE_ATOMIC_NONBLOCK: u32 = 0x0200;
const DRM_MODE_ATOMIC_ALLOW_MODESET: u32 = 0x0400;
const DRM_MODE_PAGE_FLIP_EVENT: u32 = 0x01;
const DRM_IOCTL_MODE_ADDFB2: usize = DRM_IOCTL_BASE + 0x59;
const DRM_IOCTL_GET_PCI_INFO: usize = DRM_IOCTL_BASE + 0x60;
@@ -1661,13 +1662,57 @@ impl DrmScheme {
DRM_IOCTL_MODE_ATOMIC => {
let flags = read_u32(payload, 0)?;
let count_objs = read_u32(payload, 4)?;
let objs_offset = read_u64(payload, 8)? as usize;
let count_props_offset = read_u64(payload, 16)? as usize;
let props_offset = read_u64(payload, 24)? as usize;
let prop_values_offset = read_u64(payload, 32)? as usize;
let mut state = AtomicState::new();
state.test_only = flags & DRM_MODE_ATOMIC_TEST_ONLY != 0;
state.non_blocking = flags & DRM_MODE_ATOMIC_NONBLOCK != 0;
state.allow_modeset = flags & DRM_MODE_ATOMIC_ALLOW_MODESET != 0;
if flags & DRM_MODE_PAGE_FLIP_EVENT != 0 {
debug!("redox-drm: ATOMIC page flip event requested");
}
let connectors = self.driver.detect_connectors();
let mut prop_idx = 0usize;
for i in 0..count_objs as usize {
let obj_id = read_u32(payload, objs_offset + i * 4)?;
let prop_count = read_u32(payload, count_props_offset + i * 4)?;
let mut fb_handle = 0u32;
let mut active = true;
let mut _mode_id = 0u64;
for j in 0..prop_count as usize {
let pid = read_u32(payload, props_offset + (prop_idx + j) * 4)?;
let pval = read_u64(payload, prop_values_offset + (prop_idx + j) * 8)?;
if pid == 0 {
fb_handle = pval as u32;
} else if pid == 1 {
_mode_id = pval;
} else if pid == 2 {
active = pval != 0;
}
}
prop_idx += prop_count as usize;
if fb_handle == 0 {
continue;
}
let is_crtc = obj_id > 0 && !connectors.iter().any(|c| c.id == obj_id);
if is_crtc && active {
let default_mode = connectors
.first()
.and_then(|c| c.modes.first())
.cloned()
.unwrap_or_else(|| ModeInfo::default_1080p());
state.set_crtc(obj_id, fb_handle, &[], &default_mode);
}
}
let result = self.driver.atomic_commit(&state).map_err(driver_to_syscall)?;
match result {
crate::kms::atomic::AtomicCommitResult::NoChange => Vec::new(),
@@ -3130,6 +3175,13 @@ fn read_u32(buf: &[u8], offset: usize) -> Result<u32> {
Ok(u32::from_le_bytes(array))
}
fn read_u64(buf: &[u8], offset: usize) -> Result<u64> {
let end = offset.saturating_add(8);
let bytes = buf.get(offset..end).ok_or_else(|| Error::new(EINVAL))?;
let array: [u8; 8] = bytes.try_into().map_err(|_| Error::new(EINVAL))?;
Ok(u64::from_le_bytes(array))
}
fn decode_wire<T: Copy>(buf: &[u8]) -> Result<T> {
if buf.len() < size_of::<T>() {
return Err(Error::new(EINVAL));