⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/graphics/
drm_ioctl.rs

1//! DRM ioctl interface for VeridianOS
2//!
3//! Exposes the kernel DRM/KMS infrastructure through Linux-compatible ioctl
4//! numbers and C-ABI-stable structures. User-space libdrm calls ioctl() on
5//! `/dev/dri/card0` or `/dev/dri/renderD128` and the request is routed here
6//! via [`drm_ioctl_dispatch`].
7//!
8//! Each handler bridges to the existing gpu_accel.rs APIs (GemManager,
9//! KmsManager, PageFlipManager, VirglDriver).
10
11#![allow(dead_code)]
12
13use super::gpu_accel::{
14    self, ConnectorStatus, ConnectorType, DisplayMode, EncoderType, PageFlipRequest,
15};
16use crate::error::KernelError;
17
18// ---------------------------------------------------------------------------
19// DRM ioctl command numbers (Linux-compatible)
20// ---------------------------------------------------------------------------
21
22/// DRM_IOCTL_VERSION -- query driver name and version
23pub(crate) const DRM_IOCTL_VERSION: u32 = 0x00;
24/// DRM_IOCTL_GEM_CLOSE -- close a GEM handle
25pub(crate) const DRM_IOCTL_GEM_CLOSE: u32 = 0x09;
26/// DRM_IOCTL_GET_CAP -- query driver capabilities
27pub(crate) const DRM_IOCTL_GET_CAP: u32 = 0x0C;
28/// DRM_IOCTL_SET_MASTER -- acquire DRM master role
29pub(crate) const DRM_IOCTL_SET_MASTER: u32 = 0x1E;
30/// DRM_IOCTL_DROP_MASTER -- release DRM master role
31pub(crate) const DRM_IOCTL_DROP_MASTER: u32 = 0x1F;
32/// DRM_IOCTL_PRIME_HANDLE_TO_FD -- export GEM handle as DMA-BUF fd
33pub(crate) const DRM_IOCTL_PRIME_HANDLE_TO_FD: u32 = 0x2D;
34/// DRM_IOCTL_PRIME_FD_TO_HANDLE -- import DMA-BUF fd as GEM handle
35pub(crate) const DRM_IOCTL_PRIME_FD_TO_HANDLE: u32 = 0x2E;
36/// DRM_IOCTL_MODE_GETRESOURCES -- enumerate CRTCs, connectors, encoders
37pub(crate) const DRM_IOCTL_MODE_GETRESOURCES: u32 = 0xA0;
38/// DRM_IOCTL_MODE_GETCRTC -- query CRTC state
39pub(crate) const DRM_IOCTL_MODE_GETCRTC: u32 = 0xA1;
40/// DRM_IOCTL_MODE_SETCRTC -- configure CRTC mode + framebuffer
41pub(crate) const DRM_IOCTL_MODE_SETCRTC: u32 = 0xA2;
42/// DRM_IOCTL_MODE_GETENCODER -- query encoder state
43pub(crate) const DRM_IOCTL_MODE_GETENCODER: u32 = 0xA6;
44/// DRM_IOCTL_MODE_GETCONNECTOR -- query connector state and modes
45pub(crate) const DRM_IOCTL_MODE_GETCONNECTOR: u32 = 0xA7;
46/// DRM_IOCTL_MODE_PAGE_FLIP -- request a page flip
47pub(crate) const DRM_IOCTL_MODE_PAGE_FLIP: u32 = 0xB0;
48/// DRM_IOCTL_MODE_CREATE_DUMB -- allocate a dumb scanout buffer
49pub(crate) const DRM_IOCTL_MODE_CREATE_DUMB: u32 = 0xB2;
50/// DRM_IOCTL_MODE_MAP_DUMB -- prepare a dumb buffer for mmap
51pub(crate) const DRM_IOCTL_MODE_MAP_DUMB: u32 = 0xB3;
52/// DRM_IOCTL_MODE_DESTROY_DUMB -- free a dumb buffer
53pub(crate) const DRM_IOCTL_MODE_DESTROY_DUMB: u32 = 0xB4;
54
55// ---------------------------------------------------------------------------
56// DRM capability constants
57// ---------------------------------------------------------------------------
58
59/// Capability: supports dumb scanout buffers
60pub(crate) const DRM_CAP_DUMB_BUFFER: u64 = 0x01;
61/// Capability: supports PRIME (DMA-BUF) import/export
62pub(crate) const DRM_CAP_PRIME: u64 = 0x05;
63/// Capability: timestamp monotonic
64pub(crate) const DRM_CAP_TIMESTAMP_MONOTONIC: u64 = 0x06;
65
66// ---------------------------------------------------------------------------
67// C-compatible ioctl data structures (#[repr(C)])
68// ---------------------------------------------------------------------------
69
70/// DRM version info (DRM_IOCTL_VERSION)
71#[repr(C)]
72#[derive(Debug, Clone)]
73pub(crate) struct DrmVersion {
74    pub version_major: i32,
75    pub version_minor: i32,
76    pub version_patchlevel: i32,
77    pub name_len: u32,
78    pub name_ptr: u64,
79    pub date_len: u32,
80    pub date_ptr: u64,
81    pub desc_len: u32,
82    pub desc_ptr: u64,
83}
84
85/// DRM get capability (DRM_IOCTL_GET_CAP)
86#[repr(C)]
87#[derive(Debug, Clone, Copy)]
88pub(crate) struct DrmGetCap {
89    pub capability: u64,
90    pub value: u64,
91}
92
93/// DRM GEM close (DRM_IOCTL_GEM_CLOSE)
94#[repr(C)]
95#[derive(Debug, Clone, Copy)]
96pub(crate) struct DrmGemClose {
97    pub handle: u32,
98    pub pad: u32,
99}
100
101/// DRM PRIME handle-to-fd (DRM_IOCTL_PRIME_HANDLE_TO_FD)
102#[repr(C)]
103#[derive(Debug, Clone, Copy)]
104pub(crate) struct DrmPrimeHandleToFd {
105    pub handle: u32,
106    pub flags: u32,
107    pub fd: i32,
108    pub pad: u32,
109}
110
111/// DRM PRIME fd-to-handle (DRM_IOCTL_PRIME_FD_TO_HANDLE)
112#[repr(C)]
113#[derive(Debug, Clone, Copy)]
114pub(crate) struct DrmPrimeFdToHandle {
115    pub fd: i32,
116    pub pad: u32,
117    pub handle: u32,
118    pub pad2: u32,
119}
120
121/// DRM mode resources (DRM_IOCTL_MODE_GETRESOURCES)
122#[repr(C)]
123#[derive(Debug, Clone, Copy)]
124pub(crate) struct DrmModeCardRes {
125    pub fb_id_ptr: u64,
126    pub crtc_id_ptr: u64,
127    pub connector_id_ptr: u64,
128    pub encoder_id_ptr: u64,
129    pub count_fbs: u32,
130    pub count_crtcs: u32,
131    pub count_connectors: u32,
132    pub count_encoders: u32,
133    pub min_width: u32,
134    pub max_width: u32,
135    pub min_height: u32,
136    pub max_height: u32,
137}
138
139/// DRM mode info (part of connector/CRTC responses)
140#[repr(C)]
141#[derive(Debug, Clone, Copy)]
142pub(crate) struct DrmModeInfo {
143    pub clock: u32,
144    pub hdisplay: u16,
145    pub hsync_start: u16,
146    pub hsync_end: u16,
147    pub htotal: u16,
148    pub hskew: u16,
149    pub vdisplay: u16,
150    pub vsync_start: u16,
151    pub vsync_end: u16,
152    pub vtotal: u16,
153    pub vscan: u16,
154    pub vrefresh: u32,
155    pub flags: u32,
156    pub mode_type: u32,
157    pub name: [u8; 32],
158}
159
160impl DrmModeInfo {
161    /// Convert from internal DisplayMode to DRM mode info
162    pub(crate) fn from_display_mode(mode: &DisplayMode) -> Self {
163        let mut name = [0u8; 32];
164        // Generate a mode name like "1920x1080"
165        let name_str = alloc::format!("{}x{}", mode.hdisplay, mode.vdisplay);
166        let copy_len = name_str.len().min(31);
167        name[..copy_len].copy_from_slice(&name_str.as_bytes()[..copy_len]);
168
169        Self {
170            clock: mode.clock_khz,
171            hdisplay: mode.hdisplay as u16,
172            hsync_start: mode.hsync_start as u16,
173            hsync_end: mode.hsync_end as u16,
174            htotal: mode.htotal as u16,
175            hskew: 0,
176            vdisplay: mode.vdisplay as u16,
177            vsync_start: mode.vsync_start as u16,
178            vsync_end: mode.vsync_end as u16,
179            vtotal: mode.vtotal as u16,
180            vscan: 0,
181            // Convert from millihertz to hertz
182            vrefresh: mode.vrefresh_mhz / 1000,
183            flags: 0,
184            mode_type: 0x40, // DRM_MODE_TYPE_PREFERRED
185            name,
186        }
187    }
188
189    /// Convert to internal DisplayMode
190    pub(crate) fn to_display_mode(self) -> DisplayMode {
191        DisplayMode {
192            hdisplay: self.hdisplay as u32,
193            vdisplay: self.vdisplay as u32,
194            clock_khz: self.clock,
195            hsync_start: self.hsync_start as u32,
196            hsync_end: self.hsync_end as u32,
197            htotal: self.htotal as u32,
198            vsync_start: self.vsync_start as u32,
199            vsync_end: self.vsync_end as u32,
200            vtotal: self.vtotal as u32,
201            vrefresh_mhz: self.vrefresh.checked_mul(1000).unwrap_or(60000),
202        }
203    }
204}
205
206/// DRM CRTC (DRM_IOCTL_MODE_GETCRTC / SETCRTC)
207#[repr(C)]
208#[derive(Debug, Clone, Copy)]
209pub(crate) struct DrmModeCrtc {
210    pub set_connectors_ptr: u64,
211    pub count_connectors: u32,
212    pub crtc_id: u32,
213    pub fb_id: u32,
214    pub x: u32,
215    pub y: u32,
216    pub gamma_size: u32,
217    pub mode_valid: u32,
218    pub mode: DrmModeInfo,
219}
220
221/// DRM encoder (DRM_IOCTL_MODE_GETENCODER)
222#[repr(C)]
223#[derive(Debug, Clone, Copy)]
224pub(crate) struct DrmModeEncoder {
225    pub encoder_id: u32,
226    pub encoder_type: u32,
227    pub crtc_id: u32,
228    pub possible_crtcs: u32,
229    pub possible_clones: u32,
230}
231
232/// DRM connector (DRM_IOCTL_MODE_GETCONNECTOR)
233#[repr(C)]
234#[derive(Debug, Clone, Copy)]
235pub(crate) struct DrmModeGetConnector {
236    pub encoders_ptr: u64,
237    pub modes_ptr: u64,
238    pub props_ptr: u64,
239    pub prop_values_ptr: u64,
240    pub count_modes: u32,
241    pub count_props: u32,
242    pub count_encoders: u32,
243    pub encoder_id: u32,
244    pub connector_id: u32,
245    pub connector_type: u32,
246    pub connector_type_id: u32,
247    pub connection: u32,
248    pub mm_width: u32,
249    pub mm_height: u32,
250    pub subpixel: u32,
251    pub pad: u32,
252}
253
254/// DRM create dumb buffer (DRM_IOCTL_MODE_CREATE_DUMB)
255#[repr(C)]
256#[derive(Debug, Clone, Copy)]
257pub(crate) struct DrmModeCreateDumb {
258    pub height: u32,
259    pub width: u32,
260    pub bpp: u32,
261    pub flags: u32,
262    /// Output: GEM handle
263    pub handle: u32,
264    /// Output: pitch (bytes per row)
265    pub pitch: u32,
266    /// Output: total size in bytes
267    pub size: u64,
268}
269
270/// DRM map dumb buffer (DRM_IOCTL_MODE_MAP_DUMB)
271#[repr(C)]
272#[derive(Debug, Clone, Copy)]
273pub(crate) struct DrmModeMapDumb {
274    pub handle: u32,
275    pub pad: u32,
276    /// Output: fake mmap offset
277    pub offset: u64,
278}
279
280/// DRM destroy dumb buffer (DRM_IOCTL_MODE_DESTROY_DUMB)
281#[repr(C)]
282#[derive(Debug, Clone, Copy)]
283pub(crate) struct DrmModeDestroyDumb {
284    pub handle: u32,
285}
286
287/// DRM page flip (DRM_IOCTL_MODE_PAGE_FLIP)
288#[repr(C)]
289#[derive(Debug, Clone, Copy)]
290pub(crate) struct DrmModePageFlip {
291    pub crtc_id: u32,
292    pub fb_id: u32,
293    pub flags: u32,
294    pub reserved: u32,
295    pub user_data: u64,
296}
297
298// ---------------------------------------------------------------------------
299// DRM ioctl dispatcher
300// ---------------------------------------------------------------------------
301
302/// Dispatch a DRM ioctl.
303///
304/// `_fd` is the file descriptor (for future per-fd state tracking).
305/// `request` is the full ioctl request value; we extract the command number
306/// (low 8 bits after removing the DRM base offset).
307/// `arg` points to the user-space ioctl data structure.
308///
309/// Returns 0 on success or a negative error code.
310pub(crate) fn drm_ioctl_dispatch(_fd: i32, request: u64, arg: *mut u8) -> Result<i32, KernelError> {
311    // Extract command number. Linux DRM ioctls encode direction + size in
312    // the upper bits, but the command byte is at bits [7:0] of the number
313    // field. The ioctl request also contains the DRM base ('d' = 0x64) in
314    // bits [15:8]. We match on the command number alone for simplicity.
315    let cmd = (request & 0xFF) as u32;
316
317    match cmd {
318        DRM_IOCTL_VERSION => handle_version(arg),
319        DRM_IOCTL_GET_CAP => handle_get_cap(arg),
320        DRM_IOCTL_GEM_CLOSE => handle_gem_close(arg),
321        DRM_IOCTL_SET_MASTER => Ok(0),  // Accept silently
322        DRM_IOCTL_DROP_MASTER => Ok(0), // Accept silently
323        DRM_IOCTL_PRIME_HANDLE_TO_FD => handle_prime_handle_to_fd(arg),
324        DRM_IOCTL_PRIME_FD_TO_HANDLE => handle_prime_fd_to_handle(arg),
325        DRM_IOCTL_MODE_GETRESOURCES => handle_mode_get_resources(arg),
326        DRM_IOCTL_MODE_GETCRTC => handle_mode_get_crtc(arg),
327        DRM_IOCTL_MODE_SETCRTC => handle_mode_set_crtc(arg),
328        DRM_IOCTL_MODE_GETENCODER => handle_mode_get_encoder(arg),
329        DRM_IOCTL_MODE_GETCONNECTOR => handle_mode_get_connector(arg),
330        DRM_IOCTL_MODE_PAGE_FLIP => handle_mode_page_flip(arg),
331        DRM_IOCTL_MODE_CREATE_DUMB => handle_mode_create_dumb(arg),
332        DRM_IOCTL_MODE_MAP_DUMB => handle_mode_map_dumb(arg),
333        DRM_IOCTL_MODE_DESTROY_DUMB => handle_mode_destroy_dumb(arg),
334        _ => Err(KernelError::OperationNotSupported {
335            operation: "unsupported DRM ioctl",
336        }),
337    }
338}
339
340// ---------------------------------------------------------------------------
341// Individual ioctl handlers
342// ---------------------------------------------------------------------------
343
344/// DRM_IOCTL_VERSION -- return driver name and version
345fn handle_version(arg: *mut u8) -> Result<i32, KernelError> {
346    if arg.is_null() {
347        return Err(KernelError::OperationNotSupported {
348            operation: "null arg for DRM_IOCTL_VERSION",
349        });
350    }
351    // SAFETY: Caller validated arg pointer before dispatch.
352    let ver = unsafe { &mut *(arg as *mut DrmVersion) };
353
354    ver.version_major = 1;
355    ver.version_minor = 0;
356    ver.version_patchlevel = 0;
357
358    // Copy driver name if user provided a buffer
359    let driver_name = b"veridian-drm";
360    if ver.name_ptr != 0 && ver.name_len > 0 {
361        let copy_len = (ver.name_len as usize).min(driver_name.len());
362        // SAFETY: name_ptr was provided by user space and size-bounded.
363        unsafe {
364            core::ptr::copy_nonoverlapping(driver_name.as_ptr(), ver.name_ptr as *mut u8, copy_len);
365        }
366    }
367    ver.name_len = driver_name.len() as u32;
368
369    // Copy date
370    let date = b"20260307";
371    if ver.date_ptr != 0 && ver.date_len > 0 {
372        let copy_len = (ver.date_len as usize).min(date.len());
373        // SAFETY: date_ptr was provided by user space and size-bounded.
374        unsafe {
375            core::ptr::copy_nonoverlapping(date.as_ptr(), ver.date_ptr as *mut u8, copy_len);
376        }
377    }
378    ver.date_len = date.len() as u32;
379
380    // Copy description
381    let desc = b"VeridianOS VirtIO GPU DRM driver";
382    if ver.desc_ptr != 0 && ver.desc_len > 0 {
383        let copy_len = (ver.desc_len as usize).min(desc.len());
384        // SAFETY: desc_ptr was provided by user space and size-bounded.
385        unsafe {
386            core::ptr::copy_nonoverlapping(desc.as_ptr(), ver.desc_ptr as *mut u8, copy_len);
387        }
388    }
389    ver.desc_len = desc.len() as u32;
390
391    Ok(0)
392}
393
394/// DRM_IOCTL_GET_CAP -- query driver capability
395fn handle_get_cap(arg: *mut u8) -> Result<i32, KernelError> {
396    if arg.is_null() {
397        return Err(KernelError::OperationNotSupported {
398            operation: "null arg for DRM_IOCTL_GET_CAP",
399        });
400    }
401    // SAFETY: Caller validated arg pointer before dispatch.
402    let cap = unsafe { &mut *(arg as *mut DrmGetCap) };
403
404    cap.value = match cap.capability {
405        DRM_CAP_DUMB_BUFFER => 1,
406        DRM_CAP_PRIME => 1,
407        DRM_CAP_TIMESTAMP_MONOTONIC => 1,
408        _ => 0,
409    };
410
411    Ok(0)
412}
413
414/// DRM_IOCTL_GEM_CLOSE -- close/release a GEM handle
415fn handle_gem_close(arg: *mut u8) -> Result<i32, KernelError> {
416    if arg.is_null() {
417        return Err(KernelError::OperationNotSupported {
418            operation: "null arg for DRM_IOCTL_GEM_CLOSE",
419        });
420    }
421    // SAFETY: Caller validated arg pointer before dispatch.
422    let close = unsafe { &*(arg as *const DrmGemClose) };
423
424    gpu_accel::with_gem(|gem| {
425        gem.destroy_buffer(close.handle);
426    });
427
428    Ok(0)
429}
430
431/// DRM_IOCTL_PRIME_HANDLE_TO_FD -- export GEM handle as DMA-BUF fd
432fn handle_prime_handle_to_fd(arg: *mut u8) -> Result<i32, KernelError> {
433    if arg.is_null() {
434        return Err(KernelError::OperationNotSupported {
435            operation: "null arg for PRIME_HANDLE_TO_FD",
436        });
437    }
438    // SAFETY: Caller validated arg pointer before dispatch.
439    let prime = unsafe { &mut *(arg as *mut DrmPrimeHandleToFd) };
440
441    // Verify the handle exists
442    let exists =
443        gpu_accel::with_gem(|gem| gem.find_buffer(prime.handle).is_some()).unwrap_or(false);
444
445    if !exists {
446        return Err(KernelError::OperationNotSupported {
447            operation: "invalid GEM handle for PRIME export",
448        });
449    }
450
451    // Return a synthetic fd (handle + 1000 offset to avoid collisions)
452    prime.fd = (prime.handle as i32).saturating_add(1000);
453
454    Ok(0)
455}
456
457/// DRM_IOCTL_PRIME_FD_TO_HANDLE -- import DMA-BUF fd as GEM handle
458fn handle_prime_fd_to_handle(arg: *mut u8) -> Result<i32, KernelError> {
459    if arg.is_null() {
460        return Err(KernelError::OperationNotSupported {
461            operation: "null arg for PRIME_FD_TO_HANDLE",
462        });
463    }
464    // SAFETY: Caller validated arg pointer before dispatch.
465    let prime = unsafe { &mut *(arg as *mut DrmPrimeFdToHandle) };
466
467    // Reverse the synthetic fd mapping
468    let handle = (prime.fd).saturating_sub(1000) as u32;
469
470    let exists = gpu_accel::with_gem(|gem| {
471        if gem.find_buffer(handle).is_some() {
472            gem.add_ref(handle);
473            true
474        } else {
475            false
476        }
477    })
478    .unwrap_or(false);
479
480    if !exists {
481        return Err(KernelError::OperationNotSupported {
482            operation: "invalid PRIME fd for import",
483        });
484    }
485
486    prime.handle = handle;
487
488    Ok(0)
489}
490
491/// DRM_IOCTL_MODE_GETRESOURCES -- enumerate display resources
492fn handle_mode_get_resources(arg: *mut u8) -> Result<i32, KernelError> {
493    if arg.is_null() {
494        return Err(KernelError::OperationNotSupported {
495            operation: "null arg for MODE_GETRESOURCES",
496        });
497    }
498    // SAFETY: Caller validated arg pointer before dispatch.
499    let res = unsafe { &mut *(arg as *mut DrmModeCardRes) };
500
501    gpu_accel::with_kms(|kms| {
502        // Report counts
503        res.count_fbs = kms.framebuffers.len() as u32;
504        res.count_crtcs = kms.crtcs.len() as u32;
505        res.count_connectors = kms.connectors.len() as u32;
506        res.count_encoders = kms.encoders.len() as u32;
507
508        // Copy IDs if user provided buffers
509        if res.fb_id_ptr != 0 && !kms.framebuffers.is_empty() {
510            let ptr = res.fb_id_ptr as *mut u32;
511            for (i, fb) in kms.framebuffers.iter().enumerate() {
512                // SAFETY: User provided buffer, bounded by count_fbs.
513                unsafe {
514                    ptr.add(i).write(fb.fb_id);
515                }
516            }
517        }
518
519        if res.crtc_id_ptr != 0 && !kms.crtcs.is_empty() {
520            let ptr = res.crtc_id_ptr as *mut u32;
521            for (i, crtc) in kms.crtcs.iter().enumerate() {
522                // SAFETY: User provided buffer, bounded by count_crtcs.
523                unsafe {
524                    ptr.add(i).write(crtc.crtc_id);
525                }
526            }
527        }
528
529        if res.connector_id_ptr != 0 && !kms.connectors.is_empty() {
530            let ptr = res.connector_id_ptr as *mut u32;
531            for (i, conn) in kms.connectors.iter().enumerate() {
532                // SAFETY: User provided buffer, bounded by count_connectors.
533                unsafe {
534                    ptr.add(i).write(conn.connector_id);
535                }
536            }
537        }
538
539        if res.encoder_id_ptr != 0 && !kms.encoders.is_empty() {
540            let ptr = res.encoder_id_ptr as *mut u32;
541            for (i, enc) in kms.encoders.iter().enumerate() {
542                // SAFETY: User provided buffer, bounded by count_encoders.
543                unsafe {
544                    ptr.add(i).write(enc.encoder_id);
545                }
546            }
547        }
548
549        // Dimension limits
550        res.min_width = 1;
551        res.max_width = 7680;
552        res.min_height = 1;
553        res.max_height = 4320;
554    });
555
556    Ok(0)
557}
558
559/// DRM_IOCTL_MODE_GETCRTC -- query a CRTC's current state
560fn handle_mode_get_crtc(arg: *mut u8) -> Result<i32, KernelError> {
561    if arg.is_null() {
562        return Err(KernelError::OperationNotSupported {
563            operation: "null arg for MODE_GETCRTC",
564        });
565    }
566    // SAFETY: Caller validated arg pointer before dispatch.
567    let crtc_arg = unsafe { &mut *(arg as *mut DrmModeCrtc) };
568
569    let found = gpu_accel::with_kms(|kms| {
570        if let Some(crtc) = kms.find_crtc(crtc_arg.crtc_id) {
571            crtc_arg.fb_id = crtc.fb_id.unwrap_or(0);
572            crtc_arg.x = 0;
573            crtc_arg.y = 0;
574            crtc_arg.gamma_size = crtc.gamma_size;
575
576            if let Some(ref mode) = crtc.mode {
577                crtc_arg.mode_valid = 1;
578                crtc_arg.mode = DrmModeInfo::from_display_mode(mode);
579            } else {
580                crtc_arg.mode_valid = 0;
581            }
582            true
583        } else {
584            false
585        }
586    })
587    .unwrap_or(false);
588
589    if !found {
590        return Err(KernelError::OperationNotSupported {
591            operation: "CRTC not found",
592        });
593    }
594
595    Ok(0)
596}
597
598/// DRM_IOCTL_MODE_SETCRTC -- set CRTC mode and framebuffer
599fn handle_mode_set_crtc(arg: *mut u8) -> Result<i32, KernelError> {
600    if arg.is_null() {
601        return Err(KernelError::OperationNotSupported {
602            operation: "null arg for MODE_SETCRTC",
603        });
604    }
605    // SAFETY: Caller validated arg pointer before dispatch.
606    let crtc_arg = unsafe { &*(arg as *const DrmModeCrtc) };
607
608    let success = gpu_accel::with_kms(|kms| {
609        if let Some(crtc) = kms.crtcs.iter_mut().find(|c| c.crtc_id == crtc_arg.crtc_id) {
610            crtc.fb_id = if crtc_arg.fb_id != 0 {
611                Some(crtc_arg.fb_id)
612            } else {
613                None
614            };
615
616            if crtc_arg.mode_valid != 0 {
617                crtc.mode = Some(crtc_arg.mode.to_display_mode());
618                crtc.active = true;
619            } else {
620                crtc.mode = None;
621                crtc.active = false;
622            }
623            true
624        } else {
625            false
626        }
627    })
628    .unwrap_or(false);
629
630    if !success {
631        return Err(KernelError::OperationNotSupported {
632            operation: "CRTC set failed",
633        });
634    }
635
636    Ok(0)
637}
638
639/// DRM_IOCTL_MODE_GETENCODER -- query encoder state
640fn handle_mode_get_encoder(arg: *mut u8) -> Result<i32, KernelError> {
641    if arg.is_null() {
642        return Err(KernelError::OperationNotSupported {
643            operation: "null arg for MODE_GETENCODER",
644        });
645    }
646    // SAFETY: Caller validated arg pointer before dispatch.
647    let enc_arg = unsafe { &mut *(arg as *mut DrmModeEncoder) };
648
649    let found = gpu_accel::with_kms(|kms| {
650        if let Some(enc) = kms
651            .encoders
652            .iter()
653            .find(|e| e.encoder_id == enc_arg.encoder_id)
654        {
655            enc_arg.encoder_type = match enc.encoder_type {
656                EncoderType::None => 0,
657                EncoderType::Dac => 1,
658                EncoderType::Tmds => 2,
659                EncoderType::Lvds => 3,
660                EncoderType::DpMst => 4,
661                EncoderType::Virtual => 5,
662            };
663            enc_arg.crtc_id = enc.crtc_id.unwrap_or(0);
664            enc_arg.possible_crtcs = enc.possible_crtcs;
665            enc_arg.possible_clones = 0;
666            true
667        } else {
668            false
669        }
670    })
671    .unwrap_or(false);
672
673    if !found {
674        return Err(KernelError::OperationNotSupported {
675            operation: "encoder not found",
676        });
677    }
678
679    Ok(0)
680}
681
682/// DRM_IOCTL_MODE_GETCONNECTOR -- query connector state and modes
683fn handle_mode_get_connector(arg: *mut u8) -> Result<i32, KernelError> {
684    if arg.is_null() {
685        return Err(KernelError::OperationNotSupported {
686            operation: "null arg for MODE_GETCONNECTOR",
687        });
688    }
689    // SAFETY: Caller validated arg pointer before dispatch.
690    let conn_arg = unsafe { &mut *(arg as *mut DrmModeGetConnector) };
691
692    let found = gpu_accel::with_kms(|kms| {
693        if let Some(conn) = kms
694            .connectors
695            .iter()
696            .find(|c| c.connector_id == conn_arg.connector_id)
697        {
698            conn_arg.encoder_id = conn.encoder_id.unwrap_or(0);
699            conn_arg.connector_type = match conn.connector_type {
700                ConnectorType::Hdmi => 11,
701                ConnectorType::DisplayPort => 14,
702                ConnectorType::Vga => 1,
703                ConnectorType::Edp => 14,
704                ConnectorType::Dvi => 3,
705                ConnectorType::Lvds => 7,
706                ConnectorType::Virtual => 15,
707            };
708            conn_arg.connector_type_id = 1;
709            conn_arg.connection = match conn.status {
710                ConnectorStatus::Connected => 1,
711                ConnectorStatus::Disconnected => 2,
712                _ => 3, // unknown
713            };
714            conn_arg.mm_width = 530; // ~24" monitor
715            conn_arg.mm_height = 300;
716            conn_arg.subpixel = 1; // DRM_MODE_SUBPIXEL_UNKNOWN
717            conn_arg.count_modes = conn.modes.len() as u32;
718            conn_arg.count_props = 0;
719            conn_arg.count_encoders = if conn.encoder_id.is_some() { 1 } else { 0 };
720
721            // Copy modes if user provided a buffer
722            if conn_arg.modes_ptr != 0 && !conn.modes.is_empty() {
723                let ptr = conn_arg.modes_ptr as *mut DrmModeInfo;
724                for (i, mode) in conn.modes.iter().enumerate() {
725                    // SAFETY: User provided buffer, bounded by count_modes.
726                    unsafe {
727                        ptr.add(i).write(DrmModeInfo::from_display_mode(mode));
728                    }
729                }
730            }
731
732            // Copy encoder ID if user provided a buffer
733            if conn_arg.encoders_ptr != 0 {
734                if let Some(enc_id) = conn.encoder_id {
735                    // SAFETY: User provided buffer for at least 1 encoder ID.
736                    unsafe {
737                        (conn_arg.encoders_ptr as *mut u32).write(enc_id);
738                    }
739                }
740            }
741
742            true
743        } else {
744            false
745        }
746    })
747    .unwrap_or(false);
748
749    if !found {
750        return Err(KernelError::OperationNotSupported {
751            operation: "connector not found",
752        });
753    }
754
755    Ok(0)
756}
757
758/// DRM_IOCTL_MODE_CREATE_DUMB -- create a dumb scanout buffer via GEM
759fn handle_mode_create_dumb(arg: *mut u8) -> Result<i32, KernelError> {
760    if arg.is_null() {
761        return Err(KernelError::OperationNotSupported {
762            operation: "null arg for MODE_CREATE_DUMB",
763        });
764    }
765    // SAFETY: Caller validated arg pointer before dispatch.
766    let dumb = unsafe { &mut *(arg as *mut DrmModeCreateDumb) };
767
768    // Calculate pitch and size
769    let bpp = if dumb.bpp == 0 { 32 } else { dumb.bpp };
770    let pitch = dumb
771        .width
772        .checked_mul(bpp / 8)
773        .ok_or(KernelError::OperationNotSupported {
774            operation: "dumb buffer pitch overflow",
775        })?;
776    let size = (pitch as u64).checked_mul(dumb.height as u64).ok_or(
777        KernelError::OperationNotSupported {
778            operation: "dumb buffer size overflow",
779        },
780    )?;
781
782    // Allocate GEM buffer
783    let handle = gpu_accel::with_gem(|gem| gem.create_buffer(size as usize))
784        .flatten()
785        .ok_or(KernelError::OperationNotSupported {
786            operation: "GEM allocation failed for dumb buffer",
787        })?;
788
789    dumb.handle = handle;
790    dumb.pitch = pitch;
791    dumb.size = size;
792
793    Ok(0)
794}
795
796/// DRM_IOCTL_MODE_MAP_DUMB -- prepare a dumb buffer for user-space mmap
797fn handle_mode_map_dumb(arg: *mut u8) -> Result<i32, KernelError> {
798    if arg.is_null() {
799        return Err(KernelError::OperationNotSupported {
800            operation: "null arg for MODE_MAP_DUMB",
801        });
802    }
803    // SAFETY: Caller validated arg pointer before dispatch.
804    let map = unsafe { &mut *(arg as *mut DrmModeMapDumb) };
805
806    // Verify the handle exists
807    let exists = gpu_accel::with_gem(|gem| gem.find_buffer(map.handle).is_some()).unwrap_or(false);
808
809    if !exists {
810        return Err(KernelError::OperationNotSupported {
811            operation: "invalid handle for MAP_DUMB",
812        });
813    }
814
815    // Return a synthetic offset (handle shifted left by 12 bits, like a page
816    // offset) that mmap will use to locate the GEM buffer.
817    map.offset = (map.handle as u64) << 12;
818
819    Ok(0)
820}
821
822/// DRM_IOCTL_MODE_DESTROY_DUMB -- destroy a dumb buffer
823fn handle_mode_destroy_dumb(arg: *mut u8) -> Result<i32, KernelError> {
824    if arg.is_null() {
825        return Err(KernelError::OperationNotSupported {
826            operation: "null arg for MODE_DESTROY_DUMB",
827        });
828    }
829    // SAFETY: Caller validated arg pointer before dispatch.
830    let destroy = unsafe { &*(arg as *const DrmModeDestroyDumb) };
831
832    gpu_accel::with_gem(|gem| {
833        gem.destroy_buffer(destroy.handle);
834    });
835
836    Ok(0)
837}
838
839/// DRM_IOCTL_MODE_PAGE_FLIP -- request a page flip
840fn handle_mode_page_flip(arg: *mut u8) -> Result<i32, KernelError> {
841    if arg.is_null() {
842        return Err(KernelError::OperationNotSupported {
843            operation: "null arg for MODE_PAGE_FLIP",
844        });
845    }
846    // SAFETY: Caller validated arg pointer before dispatch.
847    let flip = unsafe { &*(arg as *const DrmModePageFlip) };
848
849    let success = gpu_accel::with_page_flip(|pf| {
850        pf.request_flip(PageFlipRequest {
851            crtc_id: flip.crtc_id,
852            fb_id: flip.fb_id,
853            user_data: flip.user_data,
854        })
855    })
856    .unwrap_or(false);
857
858    if !success {
859        return Err(KernelError::OperationNotSupported {
860            operation: "page flip request failed",
861        });
862    }
863
864    Ok(0)
865}
866
867// ---------------------------------------------------------------------------
868// Tests
869// ---------------------------------------------------------------------------
870
871#[cfg(test)]
872mod tests {
873    use super::*;
874
875    #[test]
876    fn test_drm_mode_info_conversion() {
877        let mode = DisplayMode::mode_1080p60();
878        let info = DrmModeInfo::from_display_mode(&mode);
879        assert_eq!(info.hdisplay, 1920);
880        assert_eq!(info.vdisplay, 1080);
881        assert_eq!(info.vrefresh, 60);
882        assert_eq!(info.clock, 148500);
883
884        let back = info.to_display_mode();
885        assert_eq!(back.hdisplay, 1920);
886        assert_eq!(back.vdisplay, 1080);
887    }
888
889    #[test]
890    fn test_drm_mode_info_wxga() {
891        let mode = DisplayMode::mode_wxga60();
892        let info = DrmModeInfo::from_display_mode(&mode);
893        assert_eq!(info.hdisplay, 1280);
894        assert_eq!(info.vdisplay, 800);
895    }
896
897    #[test]
898    fn test_create_dumb_pitch_calculation() {
899        // 1920x1080 @ 32bpp -> pitch = 1920*4 = 7680
900        let mut dumb = DrmModeCreateDumb {
901            height: 1080,
902            width: 1920,
903            bpp: 32,
904            flags: 0,
905            handle: 0,
906            pitch: 0,
907            size: 0,
908        };
909
910        // We can't call the full handler without GEM init, but verify
911        // the struct layout is correct.
912        let bpp = dumb.bpp;
913        let pitch = dumb.width * (bpp / 8);
914        dumb.pitch = pitch;
915        dumb.size = (pitch as u64) * (dumb.height as u64);
916
917        assert_eq!(dumb.pitch, 7680);
918        assert_eq!(dumb.size, 7680 * 1080);
919    }
920}