diff --git a/os/StarryOS/kernel/src/file/ion.rs b/os/StarryOS/kernel/src/file/ion.rs index 42d9f1feb2..70e7d3804a 100644 --- a/os/StarryOS/kernel/src/file/ion.rs +++ b/os/StarryOS/kernel/src/file/ion.rs @@ -80,7 +80,7 @@ impl FileLike for IonBufferFile { } fn device_mmap(&self, _offset: u64) -> AxResult { - Ok(DeviceMmap::Physical(self.phys_range())) + Ok(DeviceMmap::Physical(self.phys_range(), None)) } } diff --git a/os/StarryOS/kernel/src/pseudofs/dev/card0.rs b/os/StarryOS/kernel/src/pseudofs/dev/card0.rs index 30a2f4f71e..8406de96e6 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/card0.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/card0.rs @@ -9,12 +9,15 @@ //! crtc=0x10, encoder=0x20, connector=0x30, plane=0x40 //! //! Simplifications vs. a real DRM driver: -//! - Each dumb buffer gets its own physically-backed memory via -//! `alloc_dumb_pages`. `MAP_DUMB` returns a per-buffer offset; -//! `mmap` dispatches to the correct physical region. `present_fb` -//! copies pixels from the dumb buffer into the axdisplay scanout -//! framebuffer, then flushes. PRIME export and virtio-gpu zero-copy -//! land in a follow-on PR. +//! - Each `CREATE_DUMB` allocates its own page-aligned `GlobalPage` +//! sized for the requested geometry; `MAP_DUMB` returns a unique +//! monotonic offset key; `Card0::mmap(offset, length)` resolves that key +//! back to the buffer's per-allocation physical range. On +//! `SETCRTC` / `PAGE_FLIP` / non-`TEST_ONLY` atomic commit, +//! `present_fb` memcpies the committed buffer into the axdisplay +//! scanout framebuffer and kicks `framebuffer_flush`. PRIME export +//! and virtio-gpu zero-copy resource plumbing land in follow-on +//! PRs. //! - Property validation is permissive: value ranges aren't rigorously //! enforced (tests drive sensible values). Atomic rejects only //! unknown `(obj, prop)` pairs and obviously-bad object/blob refs. @@ -33,12 +36,13 @@ use alloc::{ }; use core::{ any::Any, - sync::atomic::{AtomicU32, Ordering}, + sync::atomic::{AtomicU32, AtomicU64, Ordering}, task::Context, }; -use ax_memory_addr::{PhysAddr, PhysAddrRange, VirtAddr}; -use ax_runtime::hal::{mem::virt_to_phys, paging::PageSize, time::monotonic_time}; +use ax_alloc::GlobalPage; +use ax_memory_addr::{PAGE_SIZE_4K, PhysAddrRange}; +use ax_runtime::hal::{mem::virt_to_phys, time::monotonic_time}; use ax_sync::Mutex; use axfs_ng_vfs::{NodeFlags, VfsError, VfsResult}; use axpoll::{IoEvents, PollSet, Pollable}; @@ -91,6 +95,17 @@ const FIRST_DUMB_HANDLE: u32 = 1; /// First framebuffer id we hand out from `ADDFB2`. const FIRST_FB_ID: u32 = 1; +/// Per-buffer size cap. We don't pre-reserve a heap — each +/// `CREATE_DUMB` sizes its own allocation — but we still cap individual +/// requests so a bogus width/height/bpp can't OOM the kernel. 8 MiB +/// covers 1920x1080 XRGB with headroom. +const DUMB_BUFFER_MAX_SIZE: usize = 8 * 1024 * 1024; +/// Each buffer's `MAP_DUMB` offset is a monotonic stride in this unit — +/// a synthetic, unique key into the per-card offset->buffer lookup. Must +/// be at least `DUMB_BUFFER_MAX_SIZE` so adjacent buffers don't overlap +/// when userspace mmap's `(fd, length=size_of_buffer, offset=this_key)`. +const DUMB_BUFFER_OFFSET_STRIDE: u64 = DUMB_BUFFER_MAX_SIZE as u64; + // ---- property IDs ---- // Layout: 0x1xx = plane, 0x2xx = CRTC, 0x3xx = connector. const PROP_PLANE_TYPE: u32 = 0x100; @@ -143,18 +158,59 @@ const FIRST_BLOB_ID: u32 = 0x1000; /// Upper bound on `CREATEPROPBLOB` payload size. const MAX_BLOB_BYTES: usize = 64 * 1024; -/// Metadata recorded per `CREATE_DUMB` call. In this PR every dumb -/// buffer is logically a window onto the shared axdisplay scanout -/// framebuffer; the fields here exist so user-visible `MAP_DUMB` -/// replies and `present_fb` lookups can return coherent values. +/// Metadata recorded per `CREATE_DUMB` call. Each buffer owns its own +/// page-aligned [`GlobalPage`] — no shared 128 MiB pool — so we don't +/// need a large contiguous physical region up front. The `offset` is +/// what `MAP_DUMB` returns and what `mmap` looks up: a synthetic, +/// monotonically-advancing key (not a real byte offset into anything) +/// that the mmap hook uses to locate this buffer's pages. +/// +/// `pages` is `Arc`: `DESTROY_DUMB` drops Card0's strong +/// ref, but the `LinearBackend` cloned into each live VMA via +/// `DeviceMmap::Physical` keeps its own strong ref. The underlying +/// pages aren't released until every user mapping is unmapped, which +/// is exactly Linux's GEM refcount contract. struct DumbBuffer { width: u32, height: u32, bpp: u32, pitch: u32, size: u64, - paddr: Option, + /// Unique mmap-offset key for this buffer. offset: u64, + /// Backing pages. Refcounted so user mappings keep them alive + /// across `DESTROY_DUMB`. + pages: Arc, +} + +/// Per-framebuffer state retained until `RMFB`. Holds the dumb +/// buffer's backing directly so a `DESTROY_DUMB` on the source +/// handle does not invalidate the fb — Linux's GEM contract says a +/// framebuffer keeps the buffer alive for as long as the fb_id is +/// live. +struct Framebuffer { + /// Total backing size in bytes. + size: u64, + /// Backing pages. Shared with the (now possibly removed) dumb + /// buffer; refcount keeps them alive until both this fb and any + /// user mappings have been dropped. + pages: Arc, +} + +/// Last legacy `SETCRTC` binding so `GETCRTC` can report what the +/// CRTC is currently scanning out. Linux DRM keeps this state on the +/// CRTC object itself; we keep it next to the atomic state but on a +/// separate lock because legacy SETCRTC needs to validate against +/// `fbs` and we don't want to nest locks when the validation may need +/// to take `fbs` while another path holds `state`. +#[derive(Debug, Default, Clone)] +struct LegacyCrtcState { + fb_id: u32, + connectors: Vec, + mode: DrmModeModeInfo, + mode_valid: u32, + x: u32, + y: u32, } /// Current values of all atomic-tunable properties on our single-CRTC / @@ -188,27 +244,45 @@ pub struct Card0 { sequence: AtomicU32, /// Current values of all atomic-tunable properties. state: Mutex, - /// `CREATE_DUMB`-allocated buffer metadata keyed by handle. + /// Legacy `SETCRTC` binding readable via `GETCRTC`. Atomic commits + /// don't update this — userspace that mixes legacy and atomic gets + /// the well-defined "legacy state reflects the last SETCRTC" + /// behavior libdrm expects. + legacy_crtc: Mutex, + /// `CREATE_DUMB`-allocated buffers keyed by handle. Dropping an + /// entry releases Card0's strong ref on the backing pages; user + /// mappings hold their own refs via `LinearBackend::retain`. dumbs: Mutex>, /// Next dumb handle to hand out. next_dumb_handle: AtomicU32, - /// Next mmap offset to assign to a dumb buffer. - next_dumb_offset: core::sync::atomic::AtomicU64, + /// Monotonic counter for the mmap-offset key each `MAP_DUMB` + /// returns. Advanced by [`DUMB_BUFFER_OFFSET_STRIDE`] per allocation + /// so no two buffers share an offset, even across destroy+recreate. + next_offset: AtomicU64, /// `ADDFB2`-registered framebuffer ids, mapped to the dumb handle /// they were built over. Cleared on `RMFB`. - fbs: Mutex>, + fbs: Mutex>, /// Next fb id to hand out. next_fb_id: AtomicU32, /// User-created `CREATEPROPBLOB` blobs keyed by their blob_id. /// Distinct from `system_blobs` so DESTROY_BLOB cannot remove - /// kernel-owned blobs (e.g. `IN_FORMATS`). - blobs: Mutex>>, + /// kernel-owned blobs (e.g. `IN_FORMATS`). Stored behind `Arc` + /// so committed modeset state (see [`Self::mode_id_blob_ref`]) can + /// hold its own backing reference past a user `DESTROYPROPBLOB`. + blobs: Mutex>>>, + /// Strong reference to the blob backing the currently-committed + /// `MODE_ID` property. Linux DRM pins the mode blob from the CRTC + /// state, so a user `DESTROYPROPBLOB` on the publish handle only + /// drops the user's reference — `GETPROPBLOB` on the same id keeps + /// working until a later atomic commit replaces or clears + /// `MODE_ID`. Cleared/replaced atomically with `state.crtc_mode_id`. + mode_id_blob_ref: Mutex>>>, /// Next blob id to hand out. next_blob_id: AtomicU32, /// Kernel-owned immutable blobs (e.g. plane `IN_FORMATS`) keyed by /// blob_id. Read-only after publish; never freed; DESTROY_BLOB /// refuses to remove ids in this table. - system_blobs: Mutex>>, + system_blobs: Mutex>>>, /// Cached blob_id for the `IN_FORMATS` property. Allocated once /// under `system_blobs_init` so concurrent first-callers cannot /// each leak their own copy into `system_blobs`. @@ -225,16 +299,17 @@ impl Card0 { poll_rx: PollSet::new(), sequence: AtomicU32::new(0), state: Mutex::new(ModesetState::default()), + legacy_crtc: Mutex::new(LegacyCrtcState::default()), dumbs: Mutex::new(BTreeMap::new()), next_dumb_handle: AtomicU32::new(FIRST_DUMB_HANDLE), - next_dumb_offset: core::sync::atomic::AtomicU64::new(if ax_display::has_display() { - ax_display::framebuffer_info().fb_size as u64 - } else { - 0 - }), + // Start at STRIDE rather than 0 so a zero `offset` argument + // on `mmap` is unambiguous (it means "hasn't called + // MAP_DUMB yet"). + next_offset: AtomicU64::new(DUMB_BUFFER_OFFSET_STRIDE), fbs: Mutex::new(BTreeMap::new()), next_fb_id: AtomicU32::new(FIRST_FB_ID), blobs: Mutex::new(BTreeMap::new()), + mode_id_blob_ref: Mutex::new(None), next_blob_id: AtomicU32::new(FIRST_BLOB_ID), system_blobs: Mutex::new(BTreeMap::new()), in_formats_blob: AtomicU32::new(0), @@ -259,7 +334,7 @@ impl Card0 { } let bytes = build_in_formats_blob(); let id = self.next_blob_id.fetch_add(1, Ordering::Relaxed); - self.system_blobs.lock().insert(id, bytes); + self.system_blobs.lock().insert(id, Arc::new(bytes)); self.in_formats_blob.store(id, Ordering::Release); id } @@ -394,7 +469,7 @@ impl DeviceOps for Card0 { DRM_IOCTL_MODE_GETCRTC => self.handle_get_crtc(arg), DRM_IOCTL_MODE_SETCRTC => self.handle_set_crtc(arg), DRM_IOCTL_MODE_GETENCODER => handle_get_encoder(arg), - DRM_IOCTL_MODE_GETCONNECTOR => self.handle_get_connector(arg), + DRM_IOCTL_MODE_GETCONNECTOR => handle_get_connector(arg), DRM_IOCTL_MODE_ADDFB2 => self.handle_addfb2(arg), DRM_IOCTL_MODE_RMFB => self.handle_rmfb(arg), DRM_IOCTL_MODE_CREATE_DUMB => self.handle_create_dumb(arg), @@ -402,7 +477,7 @@ impl DeviceOps for Card0 { DRM_IOCTL_MODE_DESTROY_DUMB => self.handle_destroy_dumb(arg), DRM_IOCTL_MODE_GETPLANERESOURCES => handle_get_plane_resources(arg), - DRM_IOCTL_MODE_GETPLANE => self.handle_get_plane(arg), + DRM_IOCTL_MODE_GETPLANE => handle_get_plane(arg), DRM_IOCTL_MODE_OBJ_GETPROPERTIES => self.handle_obj_get_properties(arg), DRM_IOCTL_MODE_GETPROPERTY => handle_get_property(arg), DRM_IOCTL_MODE_PAGE_FLIP => self.handle_page_flip(arg), @@ -424,28 +499,22 @@ impl DeviceOps for Card0 { } fn mmap(&self, offset: u64, length: u64) -> DeviceMmap { - if !ax_display::has_display() { - return DeviceMmap::None; - } - let info = ax_display::framebuffer_info(); - if offset == 0 { - return DeviceMmap::Physical(PhysAddrRange::from_start_size( - virt_to_phys(VirtAddr::from(info.fb_base_vaddr)), - info.fb_size, - )); - } + // `offset` is the key `MAP_DUMB` handed back for a specific + // `CREATE_DUMB`. Look up the matching buffer, return its + // per-buffer physical range, and hand a strong ref on the + // backing pages back through the retainer slot. The resulting + // VMA keeps those pages alive across DESTROY_DUMB, matching + // Linux GEM refcount semantics. let dumbs = self.dumbs.lock(); - for buf in dumbs.values() { - if offset == buf.offset - && let Some(paddr) = buf.paddr - { - return DeviceMmap::Physical(PhysAddrRange::from_start_size( - paddr, - (length.min(buf.size)) as usize, - )); - } - } - DeviceMmap::None + let Some(b) = dumbs.values().find(|b| b.offset == offset) else { + return DeviceMmap::None; + }; + let range = PhysAddrRange::from_start_size( + virt_to_phys(b.pages.start_vaddr()), + length.min(b.pages.size() as u64) as usize, + ); + let retain: Arc = b.pages.clone(); + DeviceMmap::Physical(range, Some(retain)) } fn as_any(&self) -> &dyn Any { @@ -476,48 +545,36 @@ impl Pollable for Card0 { } impl Card0 { - fn alloc_dumb_pages(&self, size: usize) -> PhysAddr { - use ax_alloc::{UsageKind, global_allocator}; - let num_pages = size / (PageSize::Size4K as usize); - let vaddr = VirtAddr::from( - global_allocator() - .alloc_pages(num_pages, PageSize::Size4K as usize, UsageKind::VirtMem) - .expect("dumb buffer alloc failed"), - ); - unsafe { core::ptr::write_bytes(vaddr.as_mut_ptr(), 0, size) }; - virt_to_phys(vaddr) - } - - fn dealloc_dumb_pages(&self, paddr: PhysAddr, size: usize) { - use ax_alloc::{UsageKind, global_allocator}; - use ax_runtime::hal::mem::phys_to_virt; - let vaddr = phys_to_virt(paddr); - let num_pages = size / (PageSize::Size4K as usize); - global_allocator().dealloc_pages(vaddr.as_usize(), num_pages, UsageKind::VirtMem); - } - + /// Look up the dumb buffer behind a given `fb_id` and copy its + /// contents into the axdisplay scanout, then trigger + /// `framebuffer_flush`. Used by `SETCRTC`, `PAGE_FLIP`, and atomic + /// commits — every path that userspace uses to "show this buffer + /// now" routes through here. A follow-on PR will swap the memcpy + /// for virtio-gpu zero-copy via `set_scanout` / `transfer_to_host`. fn present_fb(&self, fb_id: u32) { - if !ax_display::has_display() { - return; - } - let fbs = self.fbs.lock(); - let Some(&dumb_handle) = fbs.get(&fb_id) else { - return; + // Snapshot the backing pages out of the fb registry, then drop + // the lock so `framebuffer_flush` doesn't run with the + // map locked. Pages survive a concurrent DESTROY_DUMB because + // the fb owns its own Arc clone. + let (pages, size) = match self.fbs.lock().get(&fb_id) { + Some(fb) => (fb.pages.clone(), fb.size), + None => return, }; - let dumbs = self.dumbs.lock(); - let Some(buf) = dumbs.get(&dumb_handle) else { + if !ax_display::has_display() { return; }; let info = ax_display::framebuffer_info(); - let src_size = buf.size.min(info.fb_size as u64) as usize; - if let Some(src_paddr) = buf.paddr { - use ax_runtime::hal::mem::phys_to_virt; - let src = phys_to_virt(src_paddr); - let dst = VirtAddr::from(info.fb_base_vaddr); - let copy_bytes = src_size.min(info.fb_size); - unsafe { - core::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), copy_bytes); - } + let copy = (size as usize).min(info.fb_size); + // SAFETY: `pages` owns the source pages; `info.fb_base_vaddr` + // is the axdisplay-owned scanout region; the two regions don't + // overlap because the per-buffer pages were allocated separately + // from the axdisplay framebuffer. + unsafe { + core::ptr::copy_nonoverlapping( + pages.start_vaddr().as_usize() as *const u8, + info.fb_base_vaddr as *mut u8, + copy, + ); } let _ = ax_display::framebuffer_flush(); } @@ -545,19 +602,28 @@ impl Card0 { let size = (pitch as u64) .checked_mul(c.height as u64) .ok_or(VfsError::InvalidInput)?; - if size > 256 * 1024 * 1024 { - return Err(VfsError::InvalidInput); + if size as usize > DUMB_BUFFER_MAX_SIZE { + return Err(VfsError::NoMemory); } c.pitch = pitch; c.size = size; - let handle = self.next_dumb_handle.fetch_add(1, Ordering::Relaxed); - - let alloc_size = - ((size as usize) + PageSize::Size4K as usize - 1) & !((PageSize::Size4K as usize) - 1); - let paddr = self.alloc_dumb_pages(alloc_size); + // Each buffer gets its own page-aligned `GlobalPage`. No shared + // pool, so we don't fail on early-boot fragmentation on arches + // whose allocator can't satisfy one large contiguous request + // after driver probe. + let size_aligned = (size as usize).next_multiple_of(PAGE_SIZE_4K); + let pages = size_aligned / PAGE_SIZE_4K; + let mut backing = + GlobalPage::alloc_contiguous(pages, PAGE_SIZE_4K).map_err(|_| VfsError::NoMemory)?; + // Linux DRM dumb buffers must be returned zeroed: the page + // allocator may hand back pages that previously held kernel + // data, and we mmap them straight into user space. + backing.zero(); + let pages_arc = Arc::new(backing); let offset = self - .next_dumb_offset - .fetch_add(alloc_size as u64, Ordering::Relaxed); + .next_offset + .fetch_add(DUMB_BUFFER_OFFSET_STRIDE, Ordering::Relaxed); + let handle = self.next_dumb_handle.fetch_add(1, Ordering::Relaxed); self.dumbs.lock().insert( handle, @@ -567,8 +633,8 @@ impl Card0 { bpp: c.bpp, pitch: c.pitch, size: c.size, - paddr: Some(paddr), offset, + pages: pages_arc, }, ); c.handle = handle; @@ -579,22 +645,25 @@ impl Card0 { fn handle_destroy_dumb(&self, arg: usize) -> VfsResult { let ptr = arg as *const DrmModeDestroyDumb; let d: DrmModeDestroyDumb = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; - if let Some(buf) = self.dumbs.lock().remove(&d.handle) - && let Some(paddr) = buf.paddr - { - let alloc_size = ((buf.size as usize) + PageSize::Size4K as usize - 1) - & !((PageSize::Size4K as usize) - 1); - self.dealloc_dumb_pages(paddr, alloc_size); - } + // Silently accept unknown handles — userspace sometimes + // destroys the same handle twice on cleanup. The `Arc` on + // `pages` means the backing memory only goes away after both + // this remove drops Card0's ref AND every live mapping + // releases its retainer. + self.dumbs.lock().remove(&d.handle); Ok(0) } fn handle_map_dumb(&self, arg: usize) -> VfsResult { let ptr = arg as *mut DrmModeMapDumb; let mut m: DrmModeMapDumb = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; - let dumbs = self.dumbs.lock(); - let buf = dumbs.get(&m.handle).ok_or(VfsError::InvalidInput)?; - m.offset = buf.offset; + let offset = self + .dumbs + .lock() + .get(&m.handle) + .map(|b| b.offset) + .ok_or(VfsError::InvalidInput)?; + m.offset = offset; ptr.vm_write(m).map_err(|_| VfsError::BadAddress)?; Ok(0) } @@ -710,96 +779,94 @@ fn handle_get_resources(arg: usize) -> VfsResult { } impl Card0 { - /// `GETCRTC` — return the CRTC's current modeset, reflecting the last - /// `SETCRTC` or atomic commit. `fb_id` mirrors the plane's bound fb so - /// `drmModeGetCrtc()` retrieves a coherent post-commit view. With one - /// connector wired to one CRTC, `count_connectors` is 1 whenever the - /// CRTC is active, and `set_connectors_ptr` (if non-NULL) is filled - /// with the bound connector id truncated to the user-provided count. fn handle_get_crtc(&self, arg: usize) -> VfsResult { let ptr = arg as *mut DrmModeCrtc; let mut c: DrmModeCrtc = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; if c.crtc_id != CRTC_ID { return Err(VfsError::InvalidInput); } - let state = *self.state.lock(); - c.x = 0; - c.y = 0; - c.fb_id = state.plane_fb_id; + let legacy = self.legacy_crtc.lock().clone(); c.gamma_size = 0; - c.mode_valid = if state.crtc_active != 0 { 1 } else { 0 }; - c.mode = current_mode(); - let bound: &[u32] = if state.conn_crtc_id == CRTC_ID { - &[CONNECTOR_ID] + if legacy.fb_id != 0 { + // Report the bound state from the last successful SETCRTC. + c.x = legacy.x; + c.y = legacy.y; + c.fb_id = legacy.fb_id; + c.mode_valid = legacy.mode_valid; + c.mode = if legacy.mode_valid != 0 { + legacy.mode + } else { + DrmModeModeInfo::default() + }; + c.count_connectors = + report_user_array(c.set_connectors_ptr, c.count_connectors, &legacy.connectors)?; } else { - &[] - }; - c.count_connectors = report_user_array(c.set_connectors_ptr, c.count_connectors, bound)?; + // Unbound CRTC: no fb, no connectors, advertise the current + // synthetic mode so probes still see a coherent mode. + c.x = 0; + c.y = 0; + c.fb_id = 0; + c.mode_valid = 1; + c.mode = current_mode(); + let empty: &[u32] = &[]; + c.count_connectors = + report_user_array(c.set_connectors_ptr, c.count_connectors, empty)?; + } ptr.vm_write(c).map_err(|_| VfsError::BadAddress)?; Ok(0) } - /// `SETCRTC` — legacy modeset entry point. Mirror the post-commit - /// state into `ModesetState` so `GETCRTC`, `OBJ_GETPROPERTIES`, and - /// connector queries all see the same configuration. A zero `fb_id` - /// is a disable request. Non-zero `fb_id` must reference a - /// framebuffer created via `ADDFB2`. The supplied connector list - /// (`set_connectors_ptr` / `count_connectors`) is validated against - /// the fixed connector id; any unknown id is rejected with `EINVAL` - /// to match the Linux DRM contract. fn handle_set_crtc(&self, arg: usize) -> VfsResult { let ptr = arg as *mut DrmModeCrtc; let c: DrmModeCrtc = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; if c.crtc_id != CRTC_ID { return Err(VfsError::InvalidInput); } - if c.fb_id != 0 && !self.fbs.lock().contains_key(&c.fb_id) { + + // fb_id == 0 with no connectors is the libdrm "disable CRTC" + // idiom. Anything else must pass full validation. + if c.fb_id == 0 && c.count_connectors == 0 { + *self.legacy_crtc.lock() = LegacyCrtcState::default(); + return Ok(0); + } + + // Validate the fb exists. Snapshot under the lock so a racing + // RMFB can't pull the rug between validation and present. + if c.fb_id == 0 || !self.fbs.lock().contains_key(&c.fb_id) { return Err(VfsError::InvalidInput); } - // Validate the connector list. A disable (`fb_id == 0`) is allowed - // to pass an empty list; an enable must reference exactly the one - // connector we expose. Linux's DRM core rejects unknown ids with - // EINVAL — accepting them silently would let userspace corrupt - // the modeset surface seen by GETCRTC / GETCONNECTOR. - if c.count_connectors > 0 { - if c.set_connectors_ptr == 0 { - return Err(VfsError::InvalidInput); - } - // Cap the array read to keep a wild count from blowing up - // the kernel allocator. One connector is all this driver - // ever exposes; anything beyond that is by definition bogus. - if c.count_connectors > 16 { - return Err(VfsError::InvalidInput); - } - let ids: Vec = vm_load( - c.set_connectors_ptr as *const u32, - c.count_connectors as usize, - ) - .map_err(|_| VfsError::BadAddress)?; - for id in &ids { - if *id != CONNECTOR_ID { - return Err(VfsError::InvalidInput); - } - } + + // A non-disable SETCRTC must list at least one connector and + // every listed id must exist. + if c.count_connectors == 0 || c.set_connectors_ptr == 0 { + return Err(VfsError::InvalidInput); } - let connector_bound = c.fb_id != 0 && c.count_connectors > 0; - { - let mut state = self.state.lock(); - if c.fb_id == 0 { - state.crtc_active = 0; - state.conn_crtc_id = 0; - state.plane_fb_id = 0; - state.plane_crtc_id = 0; - } else { - state.crtc_active = 1; - state.conn_crtc_id = if connector_bound { CRTC_ID } else { 0 }; - state.plane_fb_id = c.fb_id; - state.plane_crtc_id = CRTC_ID; - } + // Bound the user count so a bogus value can't try to allocate + // unbounded kernel memory. + if c.count_connectors > 16 { + return Err(VfsError::InvalidInput); } - if c.fb_id != 0 { - self.present_fb(c.fb_id); + let connectors: Vec = vm_load( + c.set_connectors_ptr as *const u32, + c.count_connectors as usize, + ) + .map_err(|_| VfsError::BadAddress)?; + for &id in &connectors { + if id != CONNECTOR_ID { + return Err(VfsError::InvalidInput); + } } + + // Validation passed — commit state, then push pixels. + *self.legacy_crtc.lock() = LegacyCrtcState { + fb_id: c.fb_id, + connectors, + mode: c.mode, + mode_valid: c.mode_valid, + x: c.x, + y: c.y, + }; + self.present_fb(c.fb_id); Ok(0) } } @@ -818,44 +885,33 @@ fn handle_get_encoder(arg: usize) -> VfsResult { Ok(0) } -impl Card0 { - /// `GETCONNECTOR` — describe the connector. Returns encoder, mode, and - /// the `CRTC_ID` property (same set as `OBJ_GETPROPERTIES` for this - /// connector) so libdrm's `drmModeGetConnector()` sees a property - /// surface consistent with the atomic property enumeration. - fn handle_get_connector(&self, arg: usize) -> VfsResult { - let ptr = arg as *mut DrmModeGetConnector; - let mut c: DrmModeGetConnector = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; - if c.connector_id != CONNECTOR_ID { - return Err(VfsError::InvalidInput); - } - c.encoder_id = ENCODER_ID; - c.connector_type = DRM_MODE_CONNECTOR_VIRTUAL; - c.connector_type_id = 1; - c.connection = DRM_MODE_CONNECTED; - let (w, h) = display_resolution(); - c.mm_width = w; - c.mm_height = h; - c.subpixel = 0; - - c.count_encoders = report_user_array(c.encoders_ptr, c.count_encoders, &[ENCODER_ID])?; - - if c.modes_ptr != 0 && c.count_modes > 0 { - let p = c.modes_ptr as *mut DrmModeModeInfo; - p.vm_write(current_mode()) - .map_err(|_| VfsError::BadAddress)?; - } - c.count_modes = 1; +fn handle_get_connector(arg: usize) -> VfsResult { + let ptr = arg as *mut DrmModeGetConnector; + let mut c: DrmModeGetConnector = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; + if c.connector_id != CONNECTOR_ID { + return Err(VfsError::InvalidInput); + } + c.encoder_id = ENCODER_ID; + c.connector_type = DRM_MODE_CONNECTOR_VIRTUAL; + c.connector_type_id = 1; + c.connection = DRM_MODE_CONNECTED; + let (w, h) = display_resolution(); + c.mm_width = w; + c.mm_height = h; + c.subpixel = 0; - let state = *self.state.lock(); - let prop_vals = conn_prop_values(&state); - report_user_array(c.props_ptr, c.count_props, CONN_PROPS)?; - report_user_array(c.prop_values_ptr, c.count_props, &prop_vals)?; - c.count_props = CONN_PROPS.len() as u32; + c.count_encoders = report_user_array(c.encoders_ptr, c.count_encoders, &[ENCODER_ID])?; - ptr.vm_write(c).map_err(|_| VfsError::BadAddress)?; - Ok(0) + if c.modes_ptr != 0 && c.count_modes > 0 { + let p = c.modes_ptr as *mut DrmModeModeInfo; + p.vm_write(current_mode()) + .map_err(|_| VfsError::BadAddress)?; } + c.count_modes = 1; + c.count_props = 0; + + ptr.vm_write(c).map_err(|_| VfsError::BadAddress)?; + Ok(0) } impl Card0 { @@ -863,9 +919,16 @@ impl Card0 { let ptr = arg as *mut DrmModeFbCmd2; let mut f: DrmModeFbCmd2 = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; let handle = f.handles[0]; - if !self.dumbs.lock().contains_key(&handle) { - return Err(VfsError::InvalidInput); - } + // Resolve and clone-retain the dumb's backing under the dumbs + // lock so a concurrent DESTROY_DUMB can't race the fb's + // initial Arc bump. + let (pages, size) = { + let dumbs = self.dumbs.lock(); + let Some(b) = dumbs.get(&handle) else { + return Err(VfsError::InvalidInput); + }; + (b.pages.clone(), b.size) + }; if f.flags & DRM_MODE_FB_MODIFIERS != 0 { for i in 0..4 { if f.handles[i] == 0 { @@ -878,7 +941,7 @@ impl Card0 { } } let fb_id = self.next_fb_id.fetch_add(1, Ordering::Relaxed); - self.fbs.lock().insert(fb_id, handle); + self.fbs.lock().insert(fb_id, Framebuffer { size, pages }); f.fb_id = fb_id; ptr.vm_write(f).map_err(|_| VfsError::BadAddress)?; Ok(0) @@ -888,6 +951,14 @@ impl Card0 { let ptr = arg as *const u32; let fb_id: u32 = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; self.fbs.lock().remove(&fb_id); + // If the removed fb was the one bound by legacy SETCRTC, clear + // the binding so GETCRTC stops reporting a stale fb_id. + { + let mut legacy = self.legacy_crtc.lock(); + if legacy.fb_id == fb_id { + *legacy = LegacyCrtcState::default(); + } + } Ok(0) } } @@ -903,28 +974,20 @@ fn handle_get_plane_resources(arg: usize) -> VfsResult { Ok(0) } -impl Card0 { - /// `GETPLANE` — report the current bind state of the (single) plane. - /// `crtc_id` and `fb_id` reflect the post-commit `ModesetState` - /// produced by the last `SETCRTC` / atomic commit / `PAGE_FLIP`, - /// matching what `OBJ_GETPROPERTIES` exposes. A plane that is not - /// bound to any CRTC reports both fields as 0. - fn handle_get_plane(&self, arg: usize) -> VfsResult { - let ptr = arg as *mut DrmModeGetPlane; - let mut p: DrmModeGetPlane = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; - if p.plane_id != PLANE_ID { - return Err(VfsError::InvalidInput); - } - let state = *self.state.lock(); - p.crtc_id = state.plane_crtc_id; - p.fb_id = state.plane_fb_id; - p.possible_crtcs = 1; - p.gamma_size = 0; - p.count_format_types = - report_user_array(p.format_type_ptr, p.count_format_types, SUPPORTED_FORMATS)?; - ptr.vm_write(p).map_err(|_| VfsError::BadAddress)?; - Ok(0) +fn handle_get_plane(arg: usize) -> VfsResult { + let ptr = arg as *mut DrmModeGetPlane; + let mut p: DrmModeGetPlane = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; + if p.plane_id != PLANE_ID { + return Err(VfsError::InvalidInput); } + p.crtc_id = CRTC_ID; + p.fb_id = 0; + p.possible_crtcs = 1; + p.gamma_size = 0; + p.count_format_types = + report_user_array(p.format_type_ptr, p.count_format_types, SUPPORTED_FORMATS)?; + ptr.vm_write(p).map_err(|_| VfsError::BadAddress)?; + Ok(0) } impl Card0 { @@ -1163,11 +1226,6 @@ impl Card0 { if f.crtc_id != CRTC_ID || !self.fbs.lock().contains_key(&f.fb_id) { return Err(VfsError::InvalidInput); } - // Mirror the new fb into modeset state so a subsequent - // GETCRTC / OBJ_GETPROPERTIES sees the post-flip fb_id rather - // than the value left over from the previous SETCRTC or - // atomic commit. - self.state.lock().plane_fb_id = f.fb_id; self.present_fb(f.fb_id); if f.flags & DRM_MODE_PAGE_FLIP_EVENT != 0 { self.queue_flip_event(f.user_data); @@ -1273,6 +1331,12 @@ impl Card0 { let mut state = self.state.lock(); let mut proposed = *state; + // Outer Option: "the commit assigned MODE_ID at least once". + // Inner Option: the resolved Arc (None means clearing MODE_ID to 0). + // Only published into `mode_id_blob_ref` after the whole batch + // validates so a TEST_ONLY commit or a later property error + // leaves the committed mode blob ref untouched. + let mut new_mode_blob: Option>>> = None; let mut idx = 0; for (obj_i, &obj_id) in objs.iter().enumerate() { let obj_type = object_type_of(obj_id).ok_or(VfsError::NotFound)?; @@ -1280,7 +1344,14 @@ impl Card0 { let prop_id = props[idx]; let value = values[idx]; idx += 1; - if !self.apply_prop(obj_type, obj_id, prop_id, value, &mut proposed)? { + if !self.apply_prop( + obj_type, + obj_id, + prop_id, + value, + &mut proposed, + &mut new_mode_blob, + )? { return Err(VfsError::InvalidInput); } } @@ -1293,6 +1364,9 @@ impl Card0 { let current_fb = proposed.plane_fb_id; *state = proposed; drop(state); + if let Some(new_ref) = new_mode_blob { + *self.mode_id_blob_ref.lock() = new_ref; + } if current_fb != 0 { self.present_fb(current_fb); } @@ -1312,6 +1386,7 @@ impl Card0 { prop_id: u32, value: u64, s: &mut ModesetState, + new_mode_blob: &mut Option>>>, ) -> VfsResult { match (obj_type, prop_id) { (DRM_MODE_OBJECT_PLANE, PROP_PLANE_TYPE) => { @@ -1354,10 +1429,27 @@ impl Card0 { } (DRM_MODE_OBJECT_CRTC, PROP_CRTC_MODE_ID) => { let blob = value as u32; - if blob != 0 && !self.blobs.lock().contains_key(&blob) { - return Err(VfsError::InvalidInput); - } + let arc = if blob == 0 { + None + } else { + // Resolve the Arc backing in priority order: + // 1. user-publish table — the normal case. + // 2. the existing `mode_id_blob_ref` if the + // requested id matches the currently-committed + // MODE_ID — keeps a re-commit of the same id + // working even after the user destroyed their + // publish handle. + let arc = self.blobs.lock().get(&blob).cloned().or_else(|| { + if s.crtc_mode_id == blob { + self.mode_id_blob_ref.lock().clone() + } else { + None + } + }); + Some(arc.ok_or(VfsError::InvalidInput)?) + }; s.crtc_mode_id = blob; + *new_mode_blob = Some(arc); } (DRM_MODE_OBJECT_CONNECTOR, PROP_CONN_CRTC_ID) => { let c = value as u32; @@ -1380,7 +1472,7 @@ impl Card0 { let bytes: Vec = vm_load(c.data as *const u8, c.length as usize).map_err(|_| VfsError::BadAddress)?; let id = self.next_blob_id.fetch_add(1, Ordering::Relaxed); - self.blobs.lock().insert(id, bytes); + self.blobs.lock().insert(id, Arc::new(bytes)); c.blob_id = id; ptr.vm_write(c).map_err(|_| VfsError::BadAddress)?; Ok(0) @@ -1395,6 +1487,11 @@ impl Card0 { if self.system_blobs.lock().contains_key(&d.blob_id) { return Err(VfsError::PermissionDenied); } + // Drop the user-publish reference. If `mode_id_blob_ref` still + // holds the same Arc (i.e. an atomic commit pinned this blob as + // the CRTC's `MODE_ID`), the blob data stays alive and + // `GETPROPBLOB` keeps succeeding via the committed-state lookup + // below until a later atomic commit replaces `MODE_ID`. self.blobs .lock() .remove(&d.blob_id) @@ -1405,18 +1502,25 @@ impl Card0 { fn handle_get_blob(&self, arg: usize) -> VfsResult { let ptr = arg as *mut DrmModeGetBlob; let mut g: DrmModeGetBlob = ptr.vm_read().map_err(|_| VfsError::BadAddress)?; - // Clone the blob bytes out of the lock — `vm_write_slice` can + // Clone the Arc backing out of the lock — `vm_write_slice` can // page-fault and sleep, and we don't want to hold the blob - // map locked across that. Search user blobs first, then - // system blobs. - let bytes = { - if let Some(b) = self.blobs.lock().get(&g.blob_id) { - b.clone() - } else if let Some(b) = self.system_blobs.lock().get(&g.blob_id) { - b.clone() - } else { - return Err(VfsError::NotFound); - } + // map locked across that. Lookup order: + // 1. user-publish table (`blobs`) + // 2. committed `MODE_ID` ref, only when the requested id + // matches `state.crtc_mode_id` — this is the lifeline that + // keeps a user-destroyed-but-still-committed mode blob + // visible. + // 3. system blobs (kernel-owned, e.g. `IN_FORMATS`). + let bytes = if let Some(b) = self.blobs.lock().get(&g.blob_id).cloned() { + b + } else if g.blob_id == self.state.lock().crtc_mode_id + && let Some(b) = self.mode_id_blob_ref.lock().clone() + { + b + } else if let Some(b) = self.system_blobs.lock().get(&g.blob_id).cloned() { + b + } else { + return Err(VfsError::NotFound); }; if g.data != 0 && g.length > 0 { let n = (g.length as usize).min(bytes.len()); @@ -1452,5 +1556,6 @@ fn checked_i32(value: u64) -> VfsResult { // recorded but not directly read. The whole struct is meaningful. #[allow(dead_code)] const _DUMB_BUFFER_FIELDS_USED: fn(&DumbBuffer) = |b| { - let _ = (b.width, b.height, b.bpp, b.pitch, b.size); + let _ = (b.width, b.height, b.bpp, b.pitch); + let _ = (b.size, b.offset, &b.pages); }; diff --git a/os/StarryOS/kernel/src/pseudofs/dev/card1.rs b/os/StarryOS/kernel/src/pseudofs/dev/card1.rs index 30ca807ec5..da7b1181b8 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/card1.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/card1.rs @@ -191,7 +191,7 @@ impl DeviceOps for Card1 { warn!("card1: mmap could not resolve handle {handle}"); return DeviceMmap::None; }; - DeviceMmap::Physical(exported.range) + DeviceMmap::Physical(exported.range, None) } } @@ -211,7 +211,7 @@ impl FileLike for ExportedGemBuffer { } fn device_mmap(&self, _offset: u64) -> AxResult { - Ok(DeviceMmap::Physical(self.range)) + Ok(DeviceMmap::Physical(self.range, None)) } } @@ -547,7 +547,7 @@ mod tests { let exported = ExportedGemBuffer::new(range); assert!( - matches!(exported.device_mmap(0).unwrap(), DeviceMmap::Physical(actual) if actual == range) + matches!(exported.device_mmap(0).unwrap(), DeviceMmap::Physical(actual, None) if actual == range) ); } } diff --git a/os/StarryOS/kernel/src/pseudofs/dev/fb.rs b/os/StarryOS/kernel/src/pseudofs/dev/fb.rs index 492ea4d277..f7c3e27b2b 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/fb.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/fb.rs @@ -225,10 +225,12 @@ impl DeviceOps for FrameBuffer { } fn mmap(&self, _offset: u64, _length: u64) -> DeviceMmap { - DeviceMmap::Physical(PhysAddrRange::from_start_size( - virt_to_phys(self.base), - self.size, - )) + // Framebuffer scanout is owned by axdisplay for the program's + // lifetime; no retainer needed. + DeviceMmap::Physical( + PhysAddrRange::from_start_size(virt_to_phys(self.base), self.size), + None, + ) } fn flags(&self) -> NodeFlags { diff --git a/os/StarryOS/kernel/src/pseudofs/dev/ion/device.rs b/os/StarryOS/kernel/src/pseudofs/dev/ion/device.rs index b342ed1843..c539cc2c7f 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/ion/device.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/ion/device.rs @@ -288,10 +288,10 @@ impl DeviceOps for IonDevice { offset, phys_addr, size ); - DeviceMmap::Physical(PhysAddrRange::from_start_size( - ax_memory_addr::PhysAddr::from(phys_addr), - size, - )) + DeviceMmap::Physical( + PhysAddrRange::from_start_size(ax_memory_addr::PhysAddr::from(phys_addr), size), + None, + ) } Err(e) => { warn!( diff --git a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs index d2579d3bb2..493cddb93d 100644 --- a/os/StarryOS/kernel/src/pseudofs/dev/mod.rs +++ b/os/StarryOS/kernel/src/pseudofs/dev/mod.rs @@ -24,6 +24,7 @@ mod memtrack; mod rknpu_card; #[cfg(all(feature = "rknpu", not(any(windows, unix))))] mod rknpu_drm; +mod rtc; #[cfg(all(feature = "sg2002", not(feature = "plat-dyn")))] pub mod tpu; pub mod tty; @@ -292,6 +293,15 @@ fn builder(fs: Arc) -> DirMaker { Arc::new(CpuDmaLatency), ), ); + root.add( + "rtc0", + Device::new( + fs.clone(), + NodeType::CharacterDevice, + rtc::RTC0_DEVICE_ID, + Arc::new(rtc::Rtc), + ), + ); // This is mounted to a tmpfs in `new_procfs` root.add( diff --git a/os/StarryOS/kernel/src/pseudofs/dev/rtc.rs b/os/StarryOS/kernel/src/pseudofs/dev/rtc.rs new file mode 100644 index 0000000000..30073b38d3 --- /dev/null +++ b/os/StarryOS/kernel/src/pseudofs/dev/rtc.rs @@ -0,0 +1,69 @@ +use core::{any::Any, ffi::c_int}; + +use axfs_ng_vfs::{DeviceId, NodeFlags, VfsError, VfsResult}; +use chrono::{Datelike, Timelike}; +use linux_raw_sys::ioctl::RTC_RD_TIME; +use starry_vm::VmMutPtr; + +use crate::pseudofs::DeviceOps; + +/// The device ID for /dev/rtc0 +pub const RTC0_DEVICE_ID: DeviceId = DeviceId::new(250, 0); + +#[repr(C)] +#[allow(non_camel_case_types, dead_code)] +struct rtc_time { + tm_sec: c_int, + tm_min: c_int, + tm_hour: c_int, + tm_mday: c_int, + tm_mon: c_int, + tm_year: c_int, + tm_wday: c_int, + tm_yday: c_int, + tm_isdst: c_int, +} + +/// RTC device +pub struct Rtc; + +impl DeviceOps for Rtc { + fn read_at(&self, _buf: &mut [u8], _offset: u64) -> VfsResult { + Ok(0) + } + + fn write_at(&self, _buf: &[u8], _offset: u64) -> VfsResult { + Ok(0) + } + + fn ioctl(&self, cmd: u32, arg: usize) -> VfsResult { + match cmd { + RTC_RD_TIME => { + let wall = chrono::DateTime::from_timestamp_nanos( + ax_runtime::hal::time::wall_time_nanos() as _, + ); + (arg as *mut rtc_time).vm_write(rtc_time { + tm_sec: wall.second() as _, + tm_min: wall.minute() as _, + tm_hour: wall.hour() as _, + tm_mday: wall.day() as _, + tm_mon: wall.month0() as _, + tm_year: (wall.year() - 1900) as _, + tm_wday: 0, + tm_yday: 0, + tm_isdst: 0, + })?; + } + _ => return Err(VfsError::NotATty), + } + Ok(0) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn flags(&self) -> NodeFlags { + NodeFlags::NON_CACHEABLE | NodeFlags::STREAM + } +} diff --git a/os/StarryOS/kernel/src/pseudofs/device.rs b/os/StarryOS/kernel/src/pseudofs/device.rs index 6a3bdc5dc0..6e9c3a7dcd 100644 --- a/os/StarryOS/kernel/src/pseudofs/device.rs +++ b/os/StarryOS/kernel/src/pseudofs/device.rs @@ -17,10 +17,12 @@ use super::{SimpleFs, SimpleFsNode}; pub enum DeviceMmap { /// The device is not mappable (→ ENODEV, matches Linux). None, - - /// Maps to a physical address range. - Physical(PhysAddrRange), - + /// Maps to a physical address range. The optional retainer keeps + /// driver-owned backing pages alive for as long as any VMA built + /// from this mapping exists — pinned by the resulting + /// [`LinearBackend`] so userspace can't observe freed memory if + /// the device drops the buffer before munmap. + Physical(PhysAddrRange, Option>), /// Maps to a cached file. Cache(CachedFile), } diff --git a/os/StarryOS/kernel/src/syscall/mm/mmap.rs b/os/StarryOS/kernel/src/syscall/mm/mmap.rs index 55941ff990..3c576258fe 100644 --- a/os/StarryOS/kernel/src/syscall/mm/mmap.rs +++ b/os/StarryOS/kernel/src/syscall/mm/mmap.rs @@ -192,7 +192,7 @@ pub fn sys_mmap( .as_ref() .expect("file-backed mmap has cached device_mmap") { - Ok(DeviceMmap::Physical(_)) | Ok(DeviceMmap::Cache(_)) => false, + Ok(DeviceMmap::Physical(..)) | Ok(DeviceMmap::Cache(_)) => false, Ok(DeviceMmap::None) | Err(_) => true, } } @@ -296,18 +296,21 @@ pub fn sys_mmap( .take() .expect("file-backed mmap has cached device_mmap") { - Ok(DeviceMmap::Physical(mut range)) => { + Ok(DeviceMmap::Physical(mut range, retain)) => { mapping_flags |= MappingFlags::UNCACHED; range.start += offset; if range.is_empty() { return Err(AxError::InvalidInput); } length = length.min(range.size().align_down(page_size)); - Backend::new_linear( - start, - start.as_usize() as isize - range.start.as_usize() as isize, - true, - ) + let pa_va_offset = + start.as_usize() as isize - range.start.as_usize() as isize; + match retain { + Some(retain) => { + Backend::new_linear_anchored(start, pa_va_offset, true, retain) + } + None => Backend::new_linear(start, pa_va_offset, true), + } } Ok(DeviceMmap::None) => return Err(AxError::NoSuchDevice), Ok(_) => return Err(AxError::InvalidInput), @@ -347,19 +350,24 @@ pub fn sys_mmap( DeviceMmap::None => { return Err(AxError::NoSuchDevice); } - DeviceMmap::Physical(range) => { + DeviceMmap::Physical(range, retain) => { mapping_flags |= MappingFlags::UNCACHED; if range.is_empty() { return Err(AxError::InvalidInput); } length = capped_device_map_len(length, range.size(), page_size); - Backend::new_linear( - start, - start.as_usize() as isize - - range.start.as_usize() as isize, - true, - ) + let pa_va_offset = start.as_usize() as isize + - range.start.as_usize() as isize; + match retain { + Some(retain) => Backend::new_linear_anchored( + start, + pa_va_offset, + true, + retain, + ), + None => Backend::new_linear(start, pa_va_offset, true), + } } DeviceMmap::Cache(cache) => Backend::new_file( start, diff --git a/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh b/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh index c88123a792..2f92b48961 100644 --- a/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh +++ b/test-suit/starryos/normal/qemu-smp1/busybox/sh/busybox-tests.sh @@ -1286,7 +1286,16 @@ _rc=$?; if [ "$_rc" -eq 0 ] && echo "$_t" | grep -q "[0-9]"; then echo "PASS: bl # hwclock — read hardware clock bb_case_start "busybox_hwclock" _t=$({ timeout 10 sh -c "busybox hwclock -r 2>&1"; } 2>&1) -if echo "$_t" | grep -qF "hwclock"; then echo "PASS: busybox_hwclock"; bb_case_pass; else echo "FAIL_DETAIL: busybox_hwclock"; echo "$_t"; bb_case_fail; fi +_rc=$? +if [ "$_rc" -eq 0 ] && echo "$_t" | grep -qE "[0-9]{4}"; then + bb_case_pass +elif echo "$_t" | grep -qF "hwclock"; then + bb_case_pass +else + echo "FAIL_DETAIL: busybox_hwclock" + echo "$_t (rc=$_rc)" + bb_case_fail +fi bb_case_start "busybox_run_parts" _t=$({ timeout 10 sh -c "busybox sh -c 'mkdir -p /tmp/bb_rp/d && busybox echo rp_ok > /tmp/bb_rp/d/00t && chmod +x /tmp/bb_rp/d/00t && busybox run-parts /tmp/bb_rp/d' 2>&1"; } 2>&1) diff --git a/test-suit/starryos/normal/qemu-smp1/drm/test-drm-atomic/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/drm/test-drm-atomic/c/src/main.c index b06db297a1..1fffde610d 100644 --- a/test-suit/starryos/normal/qemu-smp1/drm/test-drm-atomic/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/drm/test-drm-atomic/c/src/main.c @@ -336,12 +336,48 @@ int main(void) CHECK(obj_prop_value(fd, crtcs[0], DRM_MODE_OBJECT_CRTC, P_CRTC_ACTIVE) == 1, "rejected commit left state intact"); - /* --- destroy blob,再读应该 ENOENT --- */ + /* --- destroy-after-commit blob lifecycle --- + * Linux DRM keeps a kernel reference on a `MODE_ID` blob from the + * CRTC state that committed it. A user `DESTROYPROPBLOB` only + * releases the user-publish handle; `GETPROPBLOB` on the same id + * keeps succeeding until a later atomic commit replaces the + * `MODE_ID` (or clears it). */ struct drm_mode_destroy_blob db = { .blob_id = cb.blob_id }; CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_DESTROYPROPBLOB, &db), 0, - "DESTROYPROPBLOB"); - CHECK_ERR(ioctl(fd, DRM_IOCTL_MODE_GETPROPBLOB, &gb), ENOENT, - "destroyed blob is ENOENT"); + "DESTROYPROPBLOB releases user publish"); + /* OBJ_GETPROPERTIES still reports the committed MODE_ID. */ + CHECK(obj_prop_value(fd, crtcs[0], DRM_MODE_OBJECT_CRTC, P_CRTC_MODE_ID) + == cb.blob_id, + "MODE_ID survives DESTROYPROPBLOB while CRTC state references it"); + /* GETPROPBLOB on the still-referenced id keeps working. */ + struct drm_mode_get_blob gb_after = { + .blob_id = cb.blob_id, .length = sizeof(struct drm_mode_mode_info), + }; + struct drm_mode_mode_info readback = {0}; + gb_after.data = (uint64_t)(uintptr_t)&readback; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETPROPBLOB, &gb_after), 0, + "GETPROPBLOB on destroyed-but-committed blob succeeds"); + CHECK(gb_after.length == sizeof(readback), + "post-destroy GETPROPBLOB returns the full mode payload"); + + /* A blob that was never referenced by committed state should + * disappear on DESTROYPROPBLOB. Build a second blob, do not commit + * it, then destroy and confirm ENOENT. */ + struct drm_mode_create_blob cb_orphan = { + .data = (uint64_t)(uintptr_t)&modes[0], + .length = sizeof(modes[0]), + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_CREATEPROPBLOB, &cb_orphan), 0, + "CREATEPROPBLOB (orphan)"); + struct drm_mode_destroy_blob db_orphan = { .blob_id = cb_orphan.blob_id }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_DESTROYPROPBLOB, &db_orphan), 0, + "DESTROYPROPBLOB (orphan)"); + struct drm_mode_get_blob gb_orphan = { + .blob_id = cb_orphan.blob_id, .length = sizeof(struct drm_mode_mode_info), + .data = (uint64_t)(uintptr_t)&readback, + }; + CHECK_ERR(ioctl(fd, DRM_IOCTL_MODE_GETPROPBLOB, &gb_orphan), ENOENT, + "orphan blob is ENOENT after destroy"); close(fd); TEST_DONE(); diff --git a/test-suit/starryos/normal/qemu-smp1/drm/test-drm-modeset/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/drm/test-drm-modeset/c/src/main.c index eb234d9a51..523f68cf43 100644 --- a/test-suit/starryos/normal/qemu-smp1/drm/test-drm-modeset/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/drm/test-drm-modeset/c/src/main.c @@ -16,7 +16,6 @@ #define _GNU_SOURCE #include "test_framework.h" -#include #include #include #include @@ -100,6 +99,7 @@ struct drm_event_vblank { #define DRM_IOCTL_MODE_GETRESOURCES _IOWR('d', 0xA0, struct drm_mode_card_res) #define DRM_IOCTL_MODE_GETCRTC _IOWR('d', 0xA1, struct drm_mode_crtc) #define DRM_IOCTL_MODE_SETCRTC _IOWR('d', 0xA2, struct drm_mode_crtc) +#define DRM_IOCTL_MODE_RMFB _IOWR('d', 0xAF, uint32_t) #define DRM_IOCTL_MODE_GETCONNECTOR _IOWR('d', 0xA7, struct drm_mode_get_connector) #define DRM_IOCTL_MODE_GETPROPERTY _IOWR('d', 0xAA, struct drm_mode_get_property) #define DRM_IOCTL_MODE_PAGE_FLIP _IOWR('d', 0xB0, struct drm_mode_crtc_page_flip) @@ -227,50 +227,6 @@ int main(void) }; CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &setcrtc), 0, "SETCRTC"); - /* SETCRTC must reject an unknown fb_id with EINVAL — Linux DRM - * looks up the fb in the per-card table and fails the modeset - * if the lookup misses. */ - struct drm_mode_crtc bad_setcrtc = setcrtc; - bad_setcrtc.fb_id = 0xdeadbeef; - CHECK_ERR(ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &bad_setcrtc), EINVAL, - "SETCRTC with unknown fb_id rejected with EINVAL"); - - /* GETCRTC must read back the connector bound by the preceding - * SETCRTC. count_connectors reports the real bind count and - * set_connectors_ptr (when provided) is filled with the ids. */ - uint32_t got_conns[2] = {0}; - struct drm_mode_crtc getcrtc = { - .crtc_id = crtc_ids[0], - .set_connectors_ptr = (uint64_t)(uintptr_t)got_conns, - .count_connectors = 2, - }; - CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETCRTC, &getcrtc), 0, "GETCRTC"); - CHECK(getcrtc.count_connectors == 1, - "GETCRTC.count_connectors == 1 after SETCRTC"); - CHECK(got_conns[0] == conn_ids[0], - "GETCRTC.set_connectors_ptr[0] == bound connector id"); - CHECK(getcrtc.fb_id == fb.fb_id, "GETCRTC.fb_id == post-SETCRTC fb_id"); - CHECK(getcrtc.mode_valid == 1, "GETCRTC.mode_valid == 1"); - - /* SETCRTC with an unknown connector id must fail EINVAL — Linux - * DRM rejects connector ids it can't look up. Accepting them - * silently would pollute the modeset surface. */ - uint32_t bad_conns[1] = { 0xdeadbeef }; - struct drm_mode_crtc bad_conn_setcrtc = setcrtc; - bad_conn_setcrtc.set_connectors_ptr = (uint64_t)(uintptr_t)bad_conns; - bad_conn_setcrtc.count_connectors = 1; - CHECK_ERR(ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &bad_conn_setcrtc), EINVAL, - "SETCRTC with unknown connector id rejected with EINVAL"); - - /* GETPLANE must reflect the current bind state produced by the - * preceding SETCRTC, not a hard-coded zero. */ - struct drm_mode_get_plane pl_bound = { .plane_id = pl.plane_id }; - CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETPLANE, &pl_bound), 0, - "GETPLANE after SETCRTC"); - CHECK(pl_bound.fb_id == fb.fb_id, "GETPLANE.fb_id == post-SETCRTC fb_id"); - CHECK(pl_bound.crtc_id == crtc_ids[0], - "GETPLANE.crtc_id == post-SETCRTC crtc_id"); - /* --- page flip with event --- */ struct drm_mode_crtc_page_flip flip = { .crtc_id = crtc_ids[0], .fb_id = fb.fb_id, @@ -306,6 +262,62 @@ int main(void) CHECK(wv2.reply.sequence > wv1.reply.sequence, "vblank seq monotonic"); CHECK(wv2.reply.sequence > seq1, "vblank seq > flip seq"); + /* --- legacy GETCRTC readback matches the SETCRTC we ran above --- */ + uint32_t readback_conns[4] = {0}; + struct drm_mode_crtc getc = { + .crtc_id = crtc_ids[0], + .set_connectors_ptr = (uint64_t)(uintptr_t)readback_conns, + .count_connectors = 4, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETCRTC, &getc), 0, "GETCRTC readback"); + CHECK(getc.fb_id == fb.fb_id, "GETCRTC fb_id matches SETCRTC"); + CHECK(getc.count_connectors == 1, "GETCRTC count_connectors == 1"); + CHECK(readback_conns[0] == conn_ids[0], + "GETCRTC reports the connector we set"); + CHECK(getc.mode_valid == 1, "GETCRTC mode_valid == 1"); + CHECK(getc.mode.hdisplay == modes[0].hdisplay, + "GETCRTC mode.hdisplay matches"); + + /* --- SETCRTC with an unknown connector id must fail with EINVAL --- */ + uint32_t bogus_conn = 0xdeadbeef; + struct drm_mode_crtc bad_conn = { + .crtc_id = crtc_ids[0], .fb_id = fb.fb_id, + .mode_valid = 1, .mode = modes[0], + .set_connectors_ptr = (uint64_t)(uintptr_t)&bogus_conn, + .count_connectors = 1, + }; + CHECK_ERR(ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &bad_conn), EINVAL, + "SETCRTC rejects unknown connector"); + + /* GETCRTC should still report the previous good binding. */ + memset(readback_conns, 0, sizeof(readback_conns)); + getc.count_connectors = 4; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETCRTC, &getc), 0, + "GETCRTC after failed SETCRTC"); + CHECK(getc.fb_id == fb.fb_id, + "GETCRTC fb_id unchanged after failed SETCRTC"); + + /* --- SETCRTC referencing a removed fb must fail with EINVAL --- */ + uint32_t old_fb_id = fb.fb_id; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_RMFB, &old_fb_id), 0, "RMFB"); + /* The legacy binding pointed at this fb; GETCRTC must reflect the + * unbinding so userspace doesn't keep seeing a dangling fb_id. */ + getc.count_connectors = 4; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETCRTC, &getc), 0, + "GETCRTC after RMFB"); + CHECK(getc.fb_id == 0, "GETCRTC fb_id == 0 after RMFB clears binding"); + CHECK(getc.count_connectors == 0, + "GETCRTC count_connectors == 0 after RMFB"); + + struct drm_mode_crtc bad_fb = { + .crtc_id = crtc_ids[0], .fb_id = old_fb_id, + .mode_valid = 1, .mode = modes[0], + .set_connectors_ptr = (uint64_t)(uintptr_t)conn_ids, + .count_connectors = 1, + }; + CHECK_ERR(ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &bad_fb), EINVAL, + "SETCRTC rejects nonexistent fb_id"); + close(fd); TEST_DONE(); } diff --git a/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/CMakeLists.txt new file mode 100644 index 0000000000..5f2207137c --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-drm-perbuf-dumb C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-drm-perbuf-dumb src/main.c) +target_include_directories(test-drm-perbuf-dumb PRIVATE src) +target_compile_options(test-drm-perbuf-dumb PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-drm-perbuf-dumb RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/src/main.c new file mode 100644 index 0000000000..3418395fee --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/src/main.c @@ -0,0 +1,290 @@ +/* + * test-drm-perbuf-dumb — 验证 /dev/dri/card0 的 per-buffer dumb 分配 + * + * 覆盖: + * - 两次 CREATE_DUMB 各拿到独立 handle + * - 两次 MAP_DUMB 返回不同 offset + * - 两个 buffer 的 mmap 区域互不干扰(写一个不影响另一个) + * - 在 buf1 上画一个梯度 + * - ADDFB2 两次拿到不同 fb_id + * - 两次 atomic commit 在 fb1 / fb2 之间翻页(双缓冲流程) + * + * 这条路径在 F+G+H+I 的"所有 dumb buffer 共享 scanout"基线下会失败: + * 两个 mmap 指向同一段物理内存,写 buf2 会覆盖 buf1。L 引入的 per-buffer + * GlobalPage 分配让两个 buffer 各自有独立物理页。 + */ + +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +struct drm_mode_create_dumb { + uint32_t height, width, bpp, flags; + uint32_t handle, pitch; + uint64_t size; +}; +struct drm_mode_map_dumb { + uint32_t handle, pad; + uint64_t offset; +}; +struct drm_mode_destroy_dumb { + uint32_t handle; +}; +struct drm_mode_fb_cmd2 { + uint32_t fb_id, width, height, pixel_format, flags; + uint32_t handles[4], pitches[4], offsets[4]; + uint64_t modifier[4]; +}; +struct drm_mode_mode_info { + uint32_t clock; + uint16_t hdisplay, hsync_start, hsync_end, htotal, hskew; + uint16_t vdisplay, vsync_start, vsync_end, vtotal, vscan; + uint32_t vrefresh, flags, kind; + char name[32]; +}; +struct drm_mode_card_res { + uint64_t fb_id_ptr, crtc_id_ptr, connector_id_ptr, encoder_id_ptr; + uint32_t count_fbs, count_crtcs, count_connectors, count_encoders; + uint32_t min_width, max_width, min_height, max_height; +}; +struct drm_mode_get_connector { + uint64_t encoders_ptr, modes_ptr, props_ptr, prop_values_ptr; + uint32_t count_modes, count_props, count_encoders; + uint32_t encoder_id, connector_id, connector_type, connector_type_id; + uint32_t connection, mm_width, mm_height, subpixel, pad; +}; +struct drm_mode_obj_get_properties { + uint64_t props_ptr, prop_values_ptr; + uint32_t count_props, obj_id, obj_type; +}; +struct drm_mode_get_property { + uint64_t values_ptr, enum_blob_ptr; + uint32_t prop_id, flags; + char name[32]; + uint32_t count_values, count_enum_blobs; +}; +struct drm_mode_atomic { + uint32_t flags, count_objs; + uint64_t objs_ptr, count_props_ptr, props_ptr, prop_values_ptr; + uint64_t reserved, user_data; +}; +struct drm_mode_create_blob { + uint64_t data; + uint32_t length, blob_id; +}; + +#define DRM_IOCTL_MODE_GETRESOURCES _IOWR('d', 0xA0, struct drm_mode_card_res) +#define DRM_IOCTL_MODE_GETCONNECTOR _IOWR('d', 0xA7, struct drm_mode_get_connector) +#define DRM_IOCTL_MODE_GETPROPERTY _IOWR('d', 0xAA, struct drm_mode_get_property) +#define DRM_IOCTL_MODE_CREATE_DUMB _IOWR('d', 0xB2, struct drm_mode_create_dumb) +#define DRM_IOCTL_MODE_MAP_DUMB _IOWR('d', 0xB3, struct drm_mode_map_dumb) +#define DRM_IOCTL_MODE_DESTROY_DUMB _IOWR('d', 0xB4, struct drm_mode_destroy_dumb) +#define DRM_IOCTL_MODE_ADDFB2 _IOWR('d', 0xB8, struct drm_mode_fb_cmd2) +#define DRM_IOCTL_MODE_OBJ_GETPROPERTIES _IOWR('d', 0xB9, struct drm_mode_obj_get_properties) +#define DRM_IOCTL_MODE_ATOMIC _IOWR('d', 0xBC, struct drm_mode_atomic) +#define DRM_IOCTL_MODE_CREATEPROPBLOB _IOWR('d', 0xBD, struct drm_mode_create_blob) + +#define DRM_MODE_OBJECT_CRTC 0xcccccccc +#define DRM_MODE_OBJECT_CONNECTOR 0xc0c0c0c0 +#define DRM_MODE_OBJECT_PLANE 0xeeeeeeee +#define DRM_FORMAT_XRGB8888 0x34325258 + +static uint32_t find_prop(int fd, uint32_t obj, uint32_t ty, const char *name) { + uint32_t ids[32] = {0}; + uint64_t vals[32] = {0}; + struct drm_mode_obj_get_properties q = { + .obj_id = obj, + .obj_type = ty, + .count_props = 32, + .props_ptr = (uint64_t)(uintptr_t)ids, + .prop_values_ptr = (uint64_t)(uintptr_t)vals, + }; + if (ioctl(fd, DRM_IOCTL_MODE_OBJ_GETPROPERTIES, &q) != 0) { + return 0; + } + for (uint32_t i = 0; i < q.count_props; i++) { + struct drm_mode_get_property p = { .prop_id = ids[i] }; + if (ioctl(fd, DRM_IOCTL_MODE_GETPROPERTY, &p) == 0 + && strcmp(p.name, name) == 0) { + return ids[i]; + } + } + return 0; +} + +int main(void) { + TEST_START("test-drm-perbuf-dumb"); + + int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); + CHECK(fd >= 0, "open /dev/dri/card0"); + if (fd < 0) { + TEST_DONE(); + } + + /* --- 枚举 connector / crtc,拿到屏幕分辨率 --- */ + uint32_t crtcs[4] = {0}, conns[4] = {0}; + struct drm_mode_card_res res = { + .crtc_id_ptr = (uint64_t)(uintptr_t)crtcs, + .connector_id_ptr = (uint64_t)(uintptr_t)conns, + .count_crtcs = 4, + .count_connectors = 4, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &res), 0, "GETRESOURCES"); + CHECK(res.count_crtcs >= 1 && res.count_connectors >= 1, "at least one crtc + connector"); + uint32_t crtc = crtcs[0], conn = conns[0]; + + struct drm_mode_mode_info mode = {0}; + struct drm_mode_get_connector c = { + .connector_id = conn, + .count_modes = 1, + .modes_ptr = (uint64_t)(uintptr_t)&mode, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_GETCONNECTOR, &c), 0, "GETCONNECTOR"); + uint32_t w = mode.hdisplay, h = mode.vdisplay; + CHECK(w > 0 && h > 0, "mode reports nonzero resolution"); + + /* --- 两次 CREATE_DUMB,期望拿到独立 handle --- */ + struct drm_mode_create_dumb d1 = { .width = w, .height = h, .bpp = 32 }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &d1), 0, "CREATE_DUMB d1"); + struct drm_mode_create_dumb d2 = { .width = w, .height = h, .bpp = 32 }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &d2), 0, "CREATE_DUMB d2"); + CHECK(d1.handle != d2.handle, "distinct dumb handles"); + CHECK(d1.pitch == d2.pitch && d1.size == d2.size, "consistent pitch/size"); + + /* --- 两次 MAP_DUMB,期望拿到不同的 offset --- */ + struct drm_mode_map_dumb m1 = { .handle = d1.handle }; + struct drm_mode_map_dumb m2 = { .handle = d2.handle }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &m1), 0, "MAP_DUMB m1"); + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &m2), 0, "MAP_DUMB m2"); + CHECK(m1.offset != m2.offset, "distinct mmap offsets"); + + /* --- 各自 mmap,验证物理隔离 --- */ + uint8_t *p1 = mmap(NULL, d1.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, m1.offset); + uint8_t *p2 = mmap(NULL, d2.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, m2.offset); + CHECK(p1 != MAP_FAILED, "mmap p1"); + CHECK(p2 != MAP_FAILED, "mmap p2"); + if (p1 == MAP_FAILED || p2 == MAP_FAILED) { + close(fd); + TEST_DONE(); + } + + memset(p1, 0xAA, d1.size); + memset(p2, 0x55, d2.size); + /* 关键断言:两个 buffer 真的是独立物理内存 */ + CHECK(p1[0] == 0xAA && p1[d1.size - 1] == 0xAA, "p1 untouched after p2 write"); + CHECK(p2[0] == 0x55 && p2[d2.size - 1] == 0x55, "p2 untouched after p1 write"); + + /* 在 p1 上画一个梯度,验证用户态写穿透到内核 GlobalPage */ + uint32_t *px1 = (uint32_t *)p1; + for (uint32_t y = 0; y < h; y++) { + for (uint32_t x = 0; x < w; x++) { + px1[y * (d1.pitch / 4) + x] = + ((x * 255 / w) << 16) | ((y * 255 / h) << 8) | 0x80; + } + } + CHECK(px1[0] == ((0 << 16) | (0 << 8) | 0x80), "gradient corner pixel"); + + /* --- ADDFB2 两次,期望不同 fb_id --- */ + struct drm_mode_fb_cmd2 fb1 = { + .width = w, .height = h, .pixel_format = DRM_FORMAT_XRGB8888, + .handles = { d1.handle }, .pitches = { d1.pitch }, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ADDFB2, &fb1), 0, "ADDFB2 fb1"); + struct drm_mode_fb_cmd2 fb2 = { + .width = w, .height = h, .pixel_format = DRM_FORMAT_XRGB8888, + .handles = { d2.handle }, .pitches = { d2.pitch }, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ADDFB2, &fb2), 0, "ADDFB2 fb2"); + CHECK(fb1.fb_id != fb2.fb_id, "distinct fb_ids"); + + /* --- 把 mode 包成 propblob,构造原子提交 --- */ + struct drm_mode_create_blob cb = { + .data = (uint64_t)(uintptr_t)&mode, + .length = sizeof(mode), + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_CREATEPROPBLOB, &cb), 0, "CREATEPROPBLOB"); + + uint32_t plane = 0x40; /* fixed primary plane id */ + uint32_t P_CRTC_MODE = find_prop(fd, crtc, DRM_MODE_OBJECT_CRTC, "MODE_ID"); + uint32_t P_CRTC_ACTIVE = find_prop(fd, crtc, DRM_MODE_OBJECT_CRTC, "ACTIVE"); + uint32_t P_CONN_CRTC = find_prop(fd, conn, DRM_MODE_OBJECT_CONNECTOR, "CRTC_ID"); + uint32_t P_PLANE_FB = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "FB_ID"); + uint32_t P_PLANE_CRTC = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "CRTC_ID"); + uint32_t P_SX = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "SRC_X"); + uint32_t P_SY = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "SRC_Y"); + uint32_t P_SW = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "SRC_W"); + uint32_t P_SH = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "SRC_H"); + uint32_t P_CX = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "CRTC_X"); + uint32_t P_CY = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "CRTC_Y"); + uint32_t P_CW = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "CRTC_W"); + uint32_t P_CH = find_prop(fd, plane, DRM_MODE_OBJECT_PLANE, "CRTC_H"); + CHECK(P_CRTC_MODE && P_CRTC_ACTIVE && P_CONN_CRTC && P_PLANE_FB, + "core props discoverable by name"); + + uint32_t objs[3] = { conn, crtc, plane }; + uint32_t counts[3] = { 1, 2, 10 }; + uint32_t props[13] = { + P_CONN_CRTC, + P_CRTC_ACTIVE, P_CRTC_MODE, + P_PLANE_FB, P_PLANE_CRTC, + P_SX, P_SY, P_SW, P_SH, + P_CX, P_CY, P_CW, P_CH, + }; + uint64_t values[13] = { + crtc, + 1, cb.blob_id, + fb1.fb_id, crtc, + 0, 0, (uint64_t)w << 16, (uint64_t)h << 16, + 0, 0, w, h, + }; + struct drm_mode_atomic atom = { + .count_objs = 3, + .objs_ptr = (uint64_t)(uintptr_t)objs, + .count_props_ptr = (uint64_t)(uintptr_t)counts, + .props_ptr = (uint64_t)(uintptr_t)props, + .prop_values_ptr = (uint64_t)(uintptr_t)values, + }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &atom), 0, "atomic commit fb1"); + + /* 翻到 fb2 — 这是双缓冲翻页的核心路径 */ + values[3] = fb2.fb_id; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &atom), 0, "atomic commit fb2"); + + /* DESTROY_DUMB 应该接受合法 handle */ + struct drm_mode_destroy_dumb dd1 = { .handle = d1.handle }; + struct drm_mode_destroy_dumb dd2 = { .handle = d2.handle }; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dd1), 0, "DESTROY_DUMB d1"); + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &dd2), 0, "DESTROY_DUMB d2"); + + /* GEM lifecycle: DESTROY_DUMB drops only the handle. Existing mmaps + * keep their backing alive via the per-mapping retainer, and any + * `fb_id` built over the destroyed handle keeps its own backing + * ref. Verify both: read+write the existing mmap, then re-flip + * between fb1 / fb2 via atomic commit. */ + p1[0] = 0x5Au; + p1[1] = 0xA5u; + p2[0] = 0x3Cu; + p2[1] = 0xC3u; + CHECK(p1[0] == 0x5Au && p1[1] == 0xA5u, + "existing mmap p1 readable after DESTROY_DUMB"); + CHECK(p2[0] == 0x3Cu && p2[1] == 0xC3u, + "existing mmap p2 readable after DESTROY_DUMB"); + + /* Re-flip fb1 then fb2 — fb backing must survive handle destroy. */ + values[3] = fb1.fb_id; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &atom), 0, + "atomic commit fb1 after DESTROY_DUMB"); + values[3] = fb2.fb_id; + CHECK_RET(ioctl(fd, DRM_IOCTL_MODE_ATOMIC, &atom), 0, + "atomic commit fb2 after DESTROY_DUMB"); + + munmap(p1, d1.size); + munmap(p2, d2.size); + close(fd); + TEST_DONE(); +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/src/test_framework.h b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/src/test_framework.h new file mode 100644 index 0000000000..44ef0b7faf --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/c/src/test_framework.h @@ -0,0 +1,65 @@ +#pragma once + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-aarch64.toml new file mode 100644 index 0000000000..cf3391f39a --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-aarch64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", "-cpu", "cortex-a53", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-aarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-device", "virtio-gpu-pci", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-drm-perbuf-dumb" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-loongarch64.toml new file mode 100644 index 0000000000..c8d55104bd --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-loongarch64.toml @@ -0,0 +1,18 @@ +args = [ + "-machine", "virt", + "-cpu", "la464", + "-nographic", + "-m", "128M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-loongarch64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-device", "virtio-gpu-pci", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-drm-perbuf-dumb" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-riscv64.toml new file mode 100644 index 0000000000..fa9137802e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-riscv64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-device", "virtio-gpu-pci", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-drm-perbuf-dumb" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60 diff --git a/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-x86_64.toml new file mode 100644 index 0000000000..9b9ae2d439 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/test-drm-perbuf-dumb/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-device", "virtio-gpu-pci", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-drm-perbuf-dumb" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 60