Skip to main content

drmkit_scene_sources/
external.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! A source wrapping a caller-owned, externally allocated DMA-BUF.
5//!
6//! Port of `src/scene/external_dma_buf_source.{hpp,cpp}`.
7
8use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
9
10use drm::control::{Device as ControlDevice, FbCmd2Flags, framebuffer};
11use drmkit_core::Device;
12use drmkit_scene::{
13    AcquiredBuffer, BindingModel, DmaBufDesc, LayerBufferSource, SourceError, SourceFormat,
14};
15use drmkit_sync::SyncFence;
16
17/// One plane of an externally allocated buffer.
18///
19/// `fd` is caller-owned — the source duplicates it, so the caller may close
20/// theirs the moment the constructor returns.
21#[derive(Debug, Clone, Copy)]
22#[allow(
23    clippy::exhaustive_structs,
24    reason = "a value defined by its fields; there is nothing to add to it"
25)]
26pub struct ExternalPlane<'a> {
27    /// The DMA-BUF descriptor.
28    pub fd: BorrowedFd<'a>,
29    /// Byte offset of this plane within the descriptor.
30    pub offset: u32,
31    /// Row stride in bytes.
32    pub pitch: u32,
33}
34
35/// Why wrapping an external buffer failed.
36#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum ExternalError {
39    /// A dimension, format, or plane description was unusable.
40    #[error("invalid external buffer: {reason}")]
41    Invalid {
42        /// What was wrong.
43        reason: &'static str,
44    },
45    /// An ioctl or syscall failed — duplicating or importing a descriptor, or
46    /// registering the framebuffer.
47    #[error("external buffer operation failed: {0}")]
48    Io(#[from] rustix::io::Errno),
49    /// The key was [`retire`](crate::ExternalDmaBufPool::retire)d and its old
50    /// import is not torn down yet, so the frame was not taken. Try again once
51    /// a later frame has displaced it.
52    #[error("buffer key retired and not yet torn down")]
53    Retired,
54}
55
56/// Up to four planes, as KMS accepts.
57const MAX_PLANES: usize = 4;
58
59/// A DMA-BUF imported into the kernel and registered as a framebuffer.
60///
61/// Split out of [`ExternalDmaBufSource`] because a ring needs one of these per
62/// slot: the import, its duplicated descriptors and the framebuffer's teardown
63/// are the same work whether one buffer is presented or eight are rotated.
64pub(crate) struct ImportedFramebuffer {
65    /// Kept alive so the framebuffer's backing descriptors outlive the
66    /// caller's — the caller may close theirs the moment this returns.
67    ///
68    /// Lent back out by [`dma_buf_planes`](Self::dma_buf_planes), which is what
69    /// makes an imported layer compositable: these sources expose no CPU
70    /// mapping, so the descriptors are the only view of the pixels there is.
71    duped_fds: Vec<OwnedFd>,
72    /// Per-plane byte offset and row stride, as imported.
73    offsets: Vec<u32>,
74    pitches: Vec<u32>,
75    fb: Option<framebuffer::Handle>,
76    /// The GEM handles the import made, closed after the framebuffer.
77    ///
78    /// Each holds a reference to its dma-buf, so one left open pins the
79    /// buffer until the device descriptor closes, however long after the
80    /// producer freed it.
81    handles: Vec<drm::buffer::Handle>,
82    /// The device file the framebuffer was registered on, held for teardown.
83    ///
84    /// A duplicate rather than the number: after a session loss the caller's
85    /// descriptor closes and the next open may take its number, and the kernel
86    /// may hand out the freed framebuffer id again. A teardown through the
87    /// number would then remove the new file's framebuffer. Through this, it
88    /// reaches the file these objects belong to, whatever became of the
89    /// number. `None` once forgotten or taken, when there is nothing to tear
90    /// down.
91    device: Option<OwnedFd>,
92}
93
94impl ImportedFramebuffer {
95    /// The imported planes: their descriptors, offsets, and row strides.
96    ///
97    /// Empty for an import that has been taken (`into_imported`) or never
98    /// made, which is what a caller checks before offering a descriptor.
99    pub(crate) fn dma_buf_planes(&self) -> (&[OwnedFd], &[u32], &[u32]) {
100        (&self.duped_fds, &self.offsets, &self.pitches)
101    }
102
103    /// Import `planes` and register a framebuffer over them.
104    ///
105    /// # Errors
106    ///
107    /// [`ExternalError`] if a descriptor cannot be duplicated or imported, or
108    /// the kernel refuses the framebuffer.
109    pub(crate) fn import(
110        device: &Device,
111        format: SourceFormat,
112        planes: &[ExternalPlane<'_>],
113    ) -> Result<Self, ExternalError> {
114        let source = ExternalDmaBufSource::create(device, format, planes, None)?;
115        Ok(source.into_imported())
116    }
117
118    /// The registered framebuffer id, or `None` once forgotten.
119    pub(crate) fn fb_id(&self) -> Option<u32> {
120        self.fb.map(Into::into)
121    }
122
123    /// Abandon the framebuffer and its GEM handles without issuing an ioctl.
124    ///
125    /// For a revoked descriptor, which cannot service the teardown and took
126    /// both with it. Leaves `fb_id` reporting `None`, so nothing commits an id
127    /// the kernel has forgotten.
128    pub(crate) fn forget(&mut self) {
129        self.fb = None;
130        self.handles.clear();
131        // The objects belong to the old file, which reclaims them when its
132        // last descriptor closes; this one is not needed to tear them down.
133        self.device = None;
134    }
135
136    /// Import the kept descriptors again on `device`, after a session resume.
137    ///
138    /// The old framebuffer and handles belong to the old file, which reclaims
139    /// them when its last descriptor closes, so they are forgotten rather than
140    /// torn down on a file the session has revoked. The buffer itself is
141    /// unchanged; only its import is new. On failure
142    /// nothing is left imported, and `fb_id` reports `None`.
143    ///
144    /// # Errors
145    ///
146    /// [`ExternalError`] if the format is not a recognized DRM format, a
147    /// descriptor cannot be imported, or the kernel refuses the framebuffer.
148    pub(crate) fn reimport(
149        &mut self,
150        device: &Device,
151        format: SourceFormat,
152    ) -> Result<(), ExternalError> {
153        self.forget();
154        if self.duped_fds.is_empty() {
155            return Ok(());
156        }
157        let fourcc = drm::buffer::DrmFourcc::try_from(format.fourcc).map_err(|_| {
158            ExternalError::Invalid {
159                reason: "format is not a recognized DRM FourCC",
160            }
161        })?;
162        let device_duplicate = duplicate(device.as_fd())?;
163        let (fb, handles) = import_planes(
164            device,
165            format,
166            fourcc,
167            &self.duped_fds,
168            &self.offsets,
169            &self.pitches,
170        )?;
171        self.fb = Some(fb);
172        self.handles = handles;
173        self.device = Some(device_duplicate);
174        Ok(())
175    }
176
177    /// Tear the import down on its own descriptor, as a drop would, keeping
178    /// the descriptors for another try.
179    pub(crate) fn release(&mut self) {
180        self.tear_down();
181    }
182
183    /// Remove the framebuffer, then close the handles, on the file they were
184    /// made on, as the reference's `teardown_slot` does.
185    fn tear_down(&mut self) {
186        let Some(file) = self.device.take() else {
187            self.fb = None;
188            self.handles.clear();
189            return;
190        };
191        let device = RawDevice { fd: file.as_fd() };
192        if let Some(fb) = self.fb.take() {
193            let _ = device.destroy_framebuffer(fb);
194        }
195        close_handles(&device, &mut self.handles);
196    }
197}
198
199impl Drop for ImportedFramebuffer {
200    fn drop(&mut self) {
201        self.tear_down();
202    }
203}
204
205/// A descriptor of our own on `fd`'s file.
206fn duplicate(fd: BorrowedFd<'_>) -> Result<OwnedFd, ExternalError> {
207    fd.try_clone_to_owned().map_err(|error| {
208        ExternalError::Io(
209            rustix::io::Errno::from_io_error(&error).unwrap_or(rustix::io::Errno::MFILE),
210        )
211    })
212}
213
214/// Close each distinct GEM handle in `handles`, leaving it empty.
215///
216/// Planes that share a dma-buf import to the same handle, and the kernel
217/// takes a handle back once: closing it per plane, as the reference does, only
218/// adds refused ioctls.
219fn close_handles(device: &impl drm::control::Device, handles: &mut Vec<drm::buffer::Handle>) {
220    handles.sort_unstable_by_key(|handle| u32::from(*handle));
221    handles.dedup();
222    for handle in handles.drain(..) {
223        let _ = device.close_buffer(handle);
224    }
225}
226
227/// Import `fds` into GEM handles on `device` and register a framebuffer over
228/// them: the kernel half of an import, shared by a first import and a
229/// re-import after a session resume.
230///
231/// On failure every handle imported so far is closed again, as the
232/// reference's `import_slot` unwinds.
233fn import_planes(
234    device: &Device,
235    format: SourceFormat,
236    fourcc: drm::buffer::DrmFourcc,
237    fds: &[OwnedFd],
238    offsets: &[u32],
239    pitches: &[u32],
240) -> Result<(framebuffer::Handle, Vec<drm::buffer::Handle>), ExternalError> {
241    let mut layout_handles = [None; MAX_PLANES];
242    let mut layout_offsets = [0u32; MAX_PLANES];
243    let mut layout_pitches = [0u32; MAX_PLANES];
244    let mut imported = Vec::with_capacity(fds.len());
245    for (index, fd) in fds.iter().enumerate() {
246        let handle = match device.prime_fd_to_buffer(fd.as_fd()) {
247            Ok(handle) => handle,
248            Err(e) => {
249                close_handles(device, &mut imported);
250                return Err(ExternalError::Io(errno_of(&e)));
251            }
252        };
253        imported.push(handle);
254        layout_handles[index] = Some(handle);
255        layout_offsets[index] = offsets[index];
256        layout_pitches[index] = pitches[index];
257    }
258
259    // Which path the modifier takes is the device's to decide; see
260    // `drmkit_core::framebuffer_modifier`.
261    let declared = drmkit_core::framebuffer_modifier(device, format.modifier);
262    let layout = ImportedLayout {
263        width: format.width,
264        height: format.height,
265        fourcc,
266        modifier: declared,
267        handles: layout_handles,
268        offsets: layout_offsets,
269        pitches: layout_pitches,
270    };
271    let flags = if declared.is_some() {
272        FbCmd2Flags::MODIFIERS
273    } else {
274        FbCmd2Flags::empty()
275    };
276
277    match device.add_planar_framebuffer(&layout, flags) {
278        Ok(fb) => Ok((fb, imported)),
279        Err(e) => {
280            close_handles(device, &mut imported);
281            Err(ExternalError::Io(errno_of(&e)))
282        }
283    }
284}
285
286/// A caller-owned DMA-BUF wrapped as a scanout-ready KMS framebuffer.
287///
288/// The motivating consumer is a zero-copy capture pipeline — libcamera, V4L2,
289/// an accelerator — where the buffer is allocated upstream and reaches the
290/// scene as a descriptor plus a shape, rather than as something the scene
291/// allocated.
292///
293/// # Single-use semantics
294///
295/// One buffer, one cached framebuffer id, returned from every acquire. The
296/// release callback fires **exactly once** — from the first release, or from
297/// the drop if the source is torn down before ever being released. A caller
298/// uses it to re-queue the upstream request that owns the foreign buffer, so
299/// firing twice would re-queue a buffer that is still in use and firing never
300/// would stall the pipeline.
301///
302/// # Modifiers are passed through, not validated
303///
304/// Whatever modifier the caller supplies goes straight to the kernel, which
305/// validates it against its driver tables and rejects an unsupported tiling.
306/// The kernel is the ground truth, not this type. A caller that wants to know
307/// in advance whether a tiled or compressed modifier will land should consult
308/// the plane's `IN_FORMATS` or a `TEST_ONLY` commit rather than discover it
309/// here.
310pub struct ExternalDmaBufSource {
311    imported: ImportedFramebuffer,
312    format: SourceFormat,
313    /// The producer's render-done fence, duplicated on each acquire.
314    pending_fence: Option<SyncFence>,
315    on_release: Option<Box<dyn FnOnce()>>,
316}
317
318impl std::fmt::Debug for ExternalDmaBufSource {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        f.debug_struct("ExternalDmaBufSource")
321            .field("format", &self.format)
322            .field("fb", &self.imported.fb_id())
323            .field("has_fence", &self.pending_fence.is_some())
324            .finish_non_exhaustive()
325    }
326}
327
328impl ExternalDmaBufSource {
329    /// Wrap the given planes as a KMS framebuffer.
330    ///
331    /// Duplicates each descriptor so the source's lifetime is independent of
332    /// the caller's, imports them into GEM handles, and registers a
333    /// framebuffer. On any failure the partial state is unwound and the release
334    /// callback is **not** fired — the caller still owns the upstream buffer
335    /// and can re-queue or drop it themselves.
336    ///
337    /// Plane-count-versus-format consistency is left to the kernel: `NV12`
338    /// wants two planes, `YUV420` three, and the kernel returns `EINVAL` on a
339    /// mismatch.
340    ///
341    /// # Errors
342    ///
343    /// [`ExternalError::Invalid`] for a zero dimension, a zero or unrecognized
344    /// format, a plane count outside 1..=4, or a zero pitch;
345    /// [`ExternalError::Io`] if a descriptor cannot be duplicated or an ioctl
346    /// fails.
347    pub fn create(
348        device: &Device,
349        format: SourceFormat,
350        planes: &[ExternalPlane<'_>],
351        on_release: Option<Box<dyn FnOnce()>>,
352    ) -> Result<Self, ExternalError> {
353        if format.width == 0 || format.height == 0 {
354            return Err(ExternalError::Invalid {
355                reason: "width and height must be non-zero",
356            });
357        }
358        if format.fourcc == 0 {
359            return Err(ExternalError::Invalid {
360                reason: "format must be non-zero",
361            });
362        }
363        if planes.is_empty() || planes.len() > MAX_PLANES {
364            return Err(ExternalError::Invalid {
365                reason: "expected between 1 and 4 planes",
366            });
367        }
368        if planes.iter().any(|plane| plane.pitch == 0) {
369            return Err(ExternalError::Invalid {
370                reason: "every plane needs a non-zero pitch",
371            });
372        }
373
374        let fourcc = drm::buffer::DrmFourcc::try_from(format.fourcc).map_err(|_| {
375            ExternalError::Invalid {
376                reason: "format is not a recognized DRM FourCC",
377            }
378        })?;
379
380        // Duplicate first: from here the source owns descriptors of its own and
381        // the caller may close theirs immediately.
382        let mut duped = Vec::with_capacity(planes.len());
383        for plane in planes {
384            duped.push(plane.fd.try_clone_to_owned().map_err(|e| {
385                ExternalError::Io(
386                    rustix::io::Errno::from_io_error(&e).unwrap_or(rustix::io::Errno::MFILE),
387                )
388            })?);
389        }
390
391        let device_duplicate = duplicate(device.as_fd())?;
392        let offsets: Vec<u32> = planes.iter().map(|plane| plane.offset).collect();
393        let pitches: Vec<u32> = planes.iter().map(|plane| plane.pitch).collect();
394        let (fb, imported) = import_planes(device, format, fourcc, &duped, &offsets, &pitches)?;
395
396        Ok(Self {
397            imported: ImportedFramebuffer {
398                duped_fds: duped,
399                offsets,
400                pitches,
401                fb: Some(fb),
402                handles: imported,
403                device: Some(device_duplicate),
404            },
405            format,
406            pending_fence: None,
407            on_release,
408        })
409    }
410
411    /// Set the producer's render-done fence for subsequent acquires.
412    ///
413    /// Replaces any previous one. The buffer's pixels are valid only once it
414    /// signals. Each acquire hands back a duplicate rather than the fence
415    /// itself, because the scene acquires twice per frame -- a TEST commit,
416    /// then the real one -- and a fence moved out on the first would leave the
417    /// real commit unsynced.
418    pub fn set_acquire_fence(&mut self, fence: SyncFence) {
419        self.pending_fence = Some(fence);
420    }
421
422    /// Whether a producer fence is currently set.
423    #[must_use]
424    pub const fn has_acquire_fence(&self) -> bool {
425        self.pending_fence.is_some()
426    }
427
428    /// The registered framebuffer id.
429    #[must_use]
430    pub fn fb_id(&self) -> Option<u32> {
431        self.imported.fb_id()
432    }
433
434    /// Take the imported framebuffer, leaving the source inert.
435    ///
436    /// Used by the ring, which wants the import without the single-shot
437    /// release semantics wrapped around it.
438    fn into_imported(mut self) -> ImportedFramebuffer {
439        // The callback must not fire: nothing was ever presented, and the
440        // import is being kept rather than torn down.
441        self.on_release = None;
442        std::mem::replace(
443            &mut self.imported,
444            ImportedFramebuffer {
445                duped_fds: Vec::new(),
446                offsets: Vec::new(),
447                pitches: Vec::new(),
448                fb: None,
449                handles: Vec::new(),
450                device: None,
451            },
452        )
453    }
454
455    /// Fire the release callback if it has not fired yet.
456    fn fire_on_release_once(&mut self) {
457        if let Some(callback) = self.on_release.take() {
458            callback();
459        }
460    }
461}
462
463impl LayerBufferSource for ExternalDmaBufSource {
464    fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
465        let fb_id = self
466            .imported
467            .fb_id()
468            .ok_or(SourceError::Failed(rustix::io::Errno::INVAL))?;
469
470        // Hand back a **duplicate** of the producer's fence, without clearing
471        // it (upstream PR #230).
472        //
473        // The scene acquires each source twice per frame -- a TEST commit, then
474        // the real one. An owned fence handed over on the first acquire would
475        // be gone by the second, so the real commit would go out unsynced and
476        // the display engine could sample the buffer before the producer's
477        // writes land. Duplicating keeps the pending fence live for the second
478        // acquire; the next frame's `set_acquire_fence` replaces it, and
479        // re-arming an already-signaled fence is harmless because the kernel
480        // proceeds immediately.
481        //
482        // Each duplicate rides its own `AcquiredBuffer` and closes when that is
483        // released, so two acquires yield independently closeable descriptors.
484        let acquire_fence = self
485            .pending_fence
486            .as_ref()
487            .and_then(SyncFence::as_fd)
488            .and_then(|fd| SyncFence::import(fd).ok());
489
490        Ok(AcquiredBuffer {
491            fb_id,
492            token: 0,
493            acquire_fence,
494            damage: Vec::new(),
495        })
496    }
497
498    /// Lend the imported descriptors back, so the layer can be composited.
499    ///
500    /// A foreign buffer exposes no CPU mapping, so `map` is correctly
501    /// `Unsupported` -- and that alone would make this layer *uncompositable*:
502    /// it would blank whenever the allocator could not find it a plane, which
503    /// is the case composition exists to rescue. The descriptors are the only
504    /// view of these pixels there is, so lending them is what keeps the layer
505    /// recoverable. Upstream's `ExternalDmaBufSource` does the same from its
506    /// per-plane `duped_fd`.
507    ///
508    /// # Errors
509    ///
510    /// [`SourceError::Unsupported`] once the import has been taken by a ring,
511    /// which leaves nothing to lend.
512    fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
513        let (fds, offsets, pitches) = self.imported.dma_buf_planes();
514        if fds.is_empty() {
515            return Err(SourceError::Unsupported);
516        }
517        Ok(DmaBufDesc {
518            fds: fds.iter().map(std::os::fd::AsFd::as_fd).collect(),
519            offsets: offsets.to_vec(),
520            pitches: pitches.to_vec(),
521            format: self.format,
522        })
523    }
524
525    fn release(&mut self, _acquired: AcquiredBuffer) {
526        // Single-use: this source is one upstream request's worth of pixels.
527        // The first retire re-queues it upstream; later releases are no-ops, in
528        // case the source is still attached when the scene tears down.
529        self.fire_on_release_once();
530    }
531
532    fn binding_model(&self) -> BindingModel {
533        BindingModel::SceneSubmitsFbId
534    }
535
536    fn format(&self) -> SourceFormat {
537        self.format
538    }
539
540    // `map` keeps its default: the pixels live behind a descriptor this type
541    // does not mmap, and composition reaches them through `export_dma_buf`.
542
543    /// Re-import the buffer on the new descriptor, as the reference does.
544    ///
545    /// The buffer is the producer's and survives the pause; its framebuffer
546    /// and GEM handles do not. Without this, every acquire after a resume
547    /// would hand out a framebuffer id the new descriptor has never heard of.
548    fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
549        self.imported
550            .reimport(device, self.format)
551            .map_err(|error| source_error(&error))
552    }
553}
554
555/// An import failure as the scene reports it.
556pub(crate) fn source_error(error: &ExternalError) -> SourceError {
557    match error {
558        ExternalError::Io(errno) => SourceError::Failed(*errno),
559        ExternalError::Invalid { .. } => SourceError::Failed(rustix::io::Errno::INVAL),
560        // Only a pool's submit returns this, never an import.
561        ExternalError::Retired => SourceError::WouldBlock,
562    }
563}
564
565impl ExternalDmaBufSource {
566    /// Abandon the import without an ioctl: for an owner replacing this
567    /// source after a resume, whose descriptor took the import with it.
568    #[cfg(feature = "gbm")]
569    pub(crate) fn forget(&mut self) {
570        self.imported.forget();
571    }
572}
573
574impl Drop for ExternalDmaBufSource {
575    fn drop(&mut self) {
576        // The framebuffer goes with `imported`'s own drop.
577        //
578        // Torn down without ever being released: the upstream buffer still
579        // needs re-queuing, or the producer's pipeline stalls waiting for a
580        // slot that will never come back.
581        self.fire_on_release_once();
582    }
583}
584
585/// Minimal adapter so `Drop` can issue one ioctl without holding a `&Device`.
586struct RawDevice<'a> {
587    fd: BorrowedFd<'a>,
588}
589
590impl AsFd for RawDevice<'_> {
591    fn as_fd(&self) -> BorrowedFd<'_> {
592        self.fd
593    }
594}
595
596impl drm::Device for RawDevice<'_> {}
597impl drm::control::Device for RawDevice<'_> {}
598
599/// The plane layout handed to `add_planar_framebuffer`.
600struct ImportedLayout {
601    width: u32,
602    height: u32,
603    fourcc: drm::buffer::DrmFourcc,
604    modifier: Option<u64>,
605    handles: [Option<drm::buffer::Handle>; MAX_PLANES],
606    offsets: [u32; MAX_PLANES],
607    pitches: [u32; MAX_PLANES],
608}
609
610impl drm::buffer::PlanarBuffer for ImportedLayout {
611    fn size(&self) -> (u32, u32) {
612        (self.width, self.height)
613    }
614
615    fn format(&self) -> drm::buffer::DrmFourcc {
616        self.fourcc
617    }
618
619    fn modifier(&self) -> Option<drm::buffer::DrmModifier> {
620        self.modifier.map(drm::buffer::DrmModifier::from)
621    }
622
623    fn pitches(&self) -> [u32; 4] {
624        self.pitches
625    }
626
627    fn handles(&self) -> [Option<drm::buffer::Handle>; 4] {
628        self.handles
629    }
630
631    fn offsets(&self) -> [u32; 4] {
632        self.offsets
633    }
634}
635
636fn errno_of(error: &std::io::Error) -> rustix::io::Errno {
637    rustix::io::Errno::from_io_error(error).unwrap_or(rustix::io::Errno::IO)
638}