drmkit-scene-sources 0.1.2

Tier-1 buffer sources for drmkit scenes: dumb buffers and imported DMA-BUFs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
// SPDX-License-Identifier: MIT

//! A source wrapping a caller-owned, externally allocated DMA-BUF.
//!
//! Port of `src/scene/external_dma_buf_source.{hpp,cpp}`.

use std::os::fd::{AsFd, BorrowedFd, OwnedFd};

use drm::control::{Device as ControlDevice, FbCmd2Flags, framebuffer};
use drmkit_core::Device;
use drmkit_scene::{
    AcquiredBuffer, BindingModel, DmaBufDesc, LayerBufferSource, SourceError, SourceFormat,
};
use drmkit_sync::SyncFence;

/// One plane of an externally allocated buffer.
///
/// `fd` is caller-owned — the source duplicates it, so the caller may close
/// theirs the moment the constructor returns.
#[derive(Debug, Clone, Copy)]
#[allow(
    clippy::exhaustive_structs,
    reason = "a value defined by its fields; there is nothing to add to it"
)]
pub struct ExternalPlane<'a> {
    /// The DMA-BUF descriptor.
    pub fd: BorrowedFd<'a>,
    /// Byte offset of this plane within the descriptor.
    pub offset: u32,
    /// Row stride in bytes.
    pub pitch: u32,
}

/// Why wrapping an external buffer failed.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ExternalError {
    /// A dimension, format, or plane description was unusable.
    #[error("invalid external buffer: {reason}")]
    Invalid {
        /// What was wrong.
        reason: &'static str,
    },
    /// An ioctl or syscall failed — duplicating or importing a descriptor, or
    /// registering the framebuffer.
    #[error("external buffer operation failed: {0}")]
    Io(#[from] rustix::io::Errno),
    /// The key was [`retire`](crate::ExternalDmaBufPool::retire)d and its old
    /// import is not torn down yet, so the frame was not taken. Try again once
    /// a later frame has displaced it.
    #[error("buffer key retired and not yet torn down")]
    Retired,
}

/// Up to four planes, as KMS accepts.
const MAX_PLANES: usize = 4;

/// A DMA-BUF imported into the kernel and registered as a framebuffer.
///
/// Split out of [`ExternalDmaBufSource`] because a ring needs one of these per
/// slot: the import, its duplicated descriptors and the framebuffer's teardown
/// are the same work whether one buffer is presented or eight are rotated.
pub(crate) struct ImportedFramebuffer {
    /// Kept alive so the framebuffer's backing descriptors outlive the
    /// caller's — the caller may close theirs the moment this returns.
    ///
    /// Lent back out by [`dma_buf_planes`](Self::dma_buf_planes), which is what
    /// makes an imported layer compositable: these sources expose no CPU
    /// mapping, so the descriptors are the only view of the pixels there is.
    duped_fds: Vec<OwnedFd>,
    /// Per-plane byte offset and row stride, as imported.
    offsets: Vec<u32>,
    pitches: Vec<u32>,
    fb: Option<framebuffer::Handle>,
    /// The GEM handles the import made, closed after the framebuffer.
    ///
    /// Each holds a reference to its dma-buf, so one left open pins the
    /// buffer until the device descriptor closes, however long after the
    /// producer freed it.
    handles: Vec<drm::buffer::Handle>,
    /// The device file the framebuffer was registered on, held for teardown.
    ///
    /// A duplicate rather than the number: after a session loss the caller's
    /// descriptor closes and the next open may take its number, and the kernel
    /// may hand out the freed framebuffer id again. A teardown through the
    /// number would then remove the new file's framebuffer. Through this, it
    /// reaches the file these objects belong to, whatever became of the
    /// number. `None` once forgotten or taken, when there is nothing to tear
    /// down.
    device: Option<OwnedFd>,
}

impl ImportedFramebuffer {
    /// The imported planes: their descriptors, offsets, and row strides.
    ///
    /// Empty for an import that has been taken (`into_imported`) or never
    /// made, which is what a caller checks before offering a descriptor.
    pub(crate) fn dma_buf_planes(&self) -> (&[OwnedFd], &[u32], &[u32]) {
        (&self.duped_fds, &self.offsets, &self.pitches)
    }

    /// Import `planes` and register a framebuffer over them.
    ///
    /// # Errors
    ///
    /// [`ExternalError`] if a descriptor cannot be duplicated or imported, or
    /// the kernel refuses the framebuffer.
    pub(crate) fn import(
        device: &Device,
        format: SourceFormat,
        planes: &[ExternalPlane<'_>],
    ) -> Result<Self, ExternalError> {
        let source = ExternalDmaBufSource::create(device, format, planes, None)?;
        Ok(source.into_imported())
    }

    /// The registered framebuffer id, or `None` once forgotten.
    pub(crate) fn fb_id(&self) -> Option<u32> {
        self.fb.map(Into::into)
    }

    /// Abandon the framebuffer and its GEM handles without issuing an ioctl.
    ///
    /// For a revoked descriptor, which cannot service the teardown and took
    /// both with it. Leaves `fb_id` reporting `None`, so nothing commits an id
    /// the kernel has forgotten.
    pub(crate) fn forget(&mut self) {
        self.fb = None;
        self.handles.clear();
        // The objects belong to the old file, which reclaims them when its
        // last descriptor closes; this one is not needed to tear them down.
        self.device = None;
    }

    /// Import the kept descriptors again on `device`, after a session resume.
    ///
    /// The old framebuffer and handles belong to the old file, which reclaims
    /// them when its last descriptor closes, so they are forgotten rather than
    /// torn down on a file the session has revoked. The buffer itself is
    /// unchanged; only its import is new. On failure
    /// nothing is left imported, and `fb_id` reports `None`.
    ///
    /// # Errors
    ///
    /// [`ExternalError`] if the format is not a recognized DRM format, a
    /// descriptor cannot be imported, or the kernel refuses the framebuffer.
    pub(crate) fn reimport(
        &mut self,
        device: &Device,
        format: SourceFormat,
    ) -> Result<(), ExternalError> {
        self.forget();
        if self.duped_fds.is_empty() {
            return Ok(());
        }
        let fourcc = drm::buffer::DrmFourcc::try_from(format.fourcc).map_err(|_| {
            ExternalError::Invalid {
                reason: "format is not a recognized DRM FourCC",
            }
        })?;
        let device_duplicate = duplicate(device.as_fd())?;
        let (fb, handles) = import_planes(
            device,
            format,
            fourcc,
            &self.duped_fds,
            &self.offsets,
            &self.pitches,
        )?;
        self.fb = Some(fb);
        self.handles = handles;
        self.device = Some(device_duplicate);
        Ok(())
    }

    /// Tear the import down on its own descriptor, as a drop would, keeping
    /// the descriptors for another try.
    pub(crate) fn release(&mut self) {
        self.tear_down();
    }

    /// Remove the framebuffer, then close the handles, on the file they were
    /// made on, as the reference's `teardown_slot` does.
    fn tear_down(&mut self) {
        let Some(file) = self.device.take() else {
            self.fb = None;
            self.handles.clear();
            return;
        };
        let device = RawDevice { fd: file.as_fd() };
        if let Some(fb) = self.fb.take() {
            let _ = device.destroy_framebuffer(fb);
        }
        close_handles(&device, &mut self.handles);
    }
}

impl Drop for ImportedFramebuffer {
    fn drop(&mut self) {
        self.tear_down();
    }
}

/// A descriptor of our own on `fd`'s file.
fn duplicate(fd: BorrowedFd<'_>) -> Result<OwnedFd, ExternalError> {
    fd.try_clone_to_owned().map_err(|error| {
        ExternalError::Io(
            rustix::io::Errno::from_io_error(&error).unwrap_or(rustix::io::Errno::MFILE),
        )
    })
}

/// Close each distinct GEM handle in `handles`, leaving it empty.
///
/// Planes that share a dma-buf import to the same handle, and the kernel
/// takes a handle back once: closing it per plane, as the reference does, only
/// adds refused ioctls.
fn close_handles(device: &impl drm::control::Device, handles: &mut Vec<drm::buffer::Handle>) {
    handles.sort_unstable_by_key(|handle| u32::from(*handle));
    handles.dedup();
    for handle in handles.drain(..) {
        let _ = device.close_buffer(handle);
    }
}

/// Import `fds` into GEM handles on `device` and register a framebuffer over
/// them: the kernel half of an import, shared by a first import and a
/// re-import after a session resume.
///
/// On failure every handle imported so far is closed again, as the
/// reference's `import_slot` unwinds.
fn import_planes(
    device: &Device,
    format: SourceFormat,
    fourcc: drm::buffer::DrmFourcc,
    fds: &[OwnedFd],
    offsets: &[u32],
    pitches: &[u32],
) -> Result<(framebuffer::Handle, Vec<drm::buffer::Handle>), ExternalError> {
    let mut layout_handles = [None; MAX_PLANES];
    let mut layout_offsets = [0u32; MAX_PLANES];
    let mut layout_pitches = [0u32; MAX_PLANES];
    let mut imported = Vec::with_capacity(fds.len());
    for (index, fd) in fds.iter().enumerate() {
        let handle = match device.prime_fd_to_buffer(fd.as_fd()) {
            Ok(handle) => handle,
            Err(e) => {
                close_handles(device, &mut imported);
                return Err(ExternalError::Io(errno_of(&e)));
            }
        };
        imported.push(handle);
        layout_handles[index] = Some(handle);
        layout_offsets[index] = offsets[index];
        layout_pitches[index] = pitches[index];
    }

    // Which path the modifier takes is the device's to decide; see
    // `drmkit_core::framebuffer_modifier`.
    let declared = drmkit_core::framebuffer_modifier(device, format.modifier);
    let layout = ImportedLayout {
        width: format.width,
        height: format.height,
        fourcc,
        modifier: declared,
        handles: layout_handles,
        offsets: layout_offsets,
        pitches: layout_pitches,
    };
    let flags = if declared.is_some() {
        FbCmd2Flags::MODIFIERS
    } else {
        FbCmd2Flags::empty()
    };

    match device.add_planar_framebuffer(&layout, flags) {
        Ok(fb) => Ok((fb, imported)),
        Err(e) => {
            close_handles(device, &mut imported);
            Err(ExternalError::Io(errno_of(&e)))
        }
    }
}

/// A caller-owned DMA-BUF wrapped as a scanout-ready KMS framebuffer.
///
/// The motivating consumer is a zero-copy capture pipeline — libcamera, V4L2,
/// an accelerator — where the buffer is allocated upstream and reaches the
/// scene as a descriptor plus a shape, rather than as something the scene
/// allocated.
///
/// # Single-use semantics
///
/// One buffer, one cached framebuffer id, returned from every acquire. The
/// release callback fires **exactly once** — from the first release, or from
/// the drop if the source is torn down before ever being released. A caller
/// uses it to re-queue the upstream request that owns the foreign buffer, so
/// firing twice would re-queue a buffer that is still in use and firing never
/// would stall the pipeline.
///
/// # Modifiers are passed through, not validated
///
/// Whatever modifier the caller supplies goes straight to the kernel, which
/// validates it against its driver tables and rejects an unsupported tiling.
/// The kernel is the ground truth, not this type. A caller that wants to know
/// in advance whether a tiled or compressed modifier will land should consult
/// the plane's `IN_FORMATS` or a `TEST_ONLY` commit rather than discover it
/// here.
pub struct ExternalDmaBufSource {
    imported: ImportedFramebuffer,
    format: SourceFormat,
    /// The producer's render-done fence, duplicated on each acquire.
    pending_fence: Option<SyncFence>,
    on_release: Option<Box<dyn FnOnce()>>,
}

impl std::fmt::Debug for ExternalDmaBufSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ExternalDmaBufSource")
            .field("format", &self.format)
            .field("fb", &self.imported.fb_id())
            .field("has_fence", &self.pending_fence.is_some())
            .finish_non_exhaustive()
    }
}

impl ExternalDmaBufSource {
    /// Wrap the given planes as a KMS framebuffer.
    ///
    /// Duplicates each descriptor so the source's lifetime is independent of
    /// the caller's, imports them into GEM handles, and registers a
    /// framebuffer. On any failure the partial state is unwound and the release
    /// callback is **not** fired — the caller still owns the upstream buffer
    /// and can re-queue or drop it themselves.
    ///
    /// Plane-count-versus-format consistency is left to the kernel: `NV12`
    /// wants two planes, `YUV420` three, and the kernel returns `EINVAL` on a
    /// mismatch.
    ///
    /// # Errors
    ///
    /// [`ExternalError::Invalid`] for a zero dimension, a zero or unrecognized
    /// format, a plane count outside 1..=4, or a zero pitch;
    /// [`ExternalError::Io`] if a descriptor cannot be duplicated or an ioctl
    /// fails.
    pub fn create(
        device: &Device,
        format: SourceFormat,
        planes: &[ExternalPlane<'_>],
        on_release: Option<Box<dyn FnOnce()>>,
    ) -> Result<Self, ExternalError> {
        if format.width == 0 || format.height == 0 {
            return Err(ExternalError::Invalid {
                reason: "width and height must be non-zero",
            });
        }
        if format.fourcc == 0 {
            return Err(ExternalError::Invalid {
                reason: "format must be non-zero",
            });
        }
        if planes.is_empty() || planes.len() > MAX_PLANES {
            return Err(ExternalError::Invalid {
                reason: "expected between 1 and 4 planes",
            });
        }
        if planes.iter().any(|plane| plane.pitch == 0) {
            return Err(ExternalError::Invalid {
                reason: "every plane needs a non-zero pitch",
            });
        }

        let fourcc = drm::buffer::DrmFourcc::try_from(format.fourcc).map_err(|_| {
            ExternalError::Invalid {
                reason: "format is not a recognized DRM FourCC",
            }
        })?;

        // Duplicate first: from here the source owns descriptors of its own and
        // the caller may close theirs immediately.
        let mut duped = Vec::with_capacity(planes.len());
        for plane in planes {
            duped.push(plane.fd.try_clone_to_owned().map_err(|e| {
                ExternalError::Io(
                    rustix::io::Errno::from_io_error(&e).unwrap_or(rustix::io::Errno::MFILE),
                )
            })?);
        }

        let device_duplicate = duplicate(device.as_fd())?;
        let offsets: Vec<u32> = planes.iter().map(|plane| plane.offset).collect();
        let pitches: Vec<u32> = planes.iter().map(|plane| plane.pitch).collect();
        let (fb, imported) = import_planes(device, format, fourcc, &duped, &offsets, &pitches)?;

        Ok(Self {
            imported: ImportedFramebuffer {
                duped_fds: duped,
                offsets,
                pitches,
                fb: Some(fb),
                handles: imported,
                device: Some(device_duplicate),
            },
            format,
            pending_fence: None,
            on_release,
        })
    }

    /// Set the producer's render-done fence for subsequent acquires.
    ///
    /// Replaces any previous one. The buffer's pixels are valid only once it
    /// signals. Each acquire hands back a duplicate rather than the fence
    /// itself, because the scene acquires twice per frame -- a TEST commit,
    /// then the real one -- and a fence moved out on the first would leave the
    /// real commit unsynced.
    pub fn set_acquire_fence(&mut self, fence: SyncFence) {
        self.pending_fence = Some(fence);
    }

    /// Whether a producer fence is currently set.
    #[must_use]
    pub const fn has_acquire_fence(&self) -> bool {
        self.pending_fence.is_some()
    }

    /// The registered framebuffer id.
    #[must_use]
    pub fn fb_id(&self) -> Option<u32> {
        self.imported.fb_id()
    }

    /// Take the imported framebuffer, leaving the source inert.
    ///
    /// Used by the ring, which wants the import without the single-shot
    /// release semantics wrapped around it.
    fn into_imported(mut self) -> ImportedFramebuffer {
        // The callback must not fire: nothing was ever presented, and the
        // import is being kept rather than torn down.
        self.on_release = None;
        std::mem::replace(
            &mut self.imported,
            ImportedFramebuffer {
                duped_fds: Vec::new(),
                offsets: Vec::new(),
                pitches: Vec::new(),
                fb: None,
                handles: Vec::new(),
                device: None,
            },
        )
    }

    /// Fire the release callback if it has not fired yet.
    fn fire_on_release_once(&mut self) {
        if let Some(callback) = self.on_release.take() {
            callback();
        }
    }
}

impl LayerBufferSource for ExternalDmaBufSource {
    fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
        let fb_id = self
            .imported
            .fb_id()
            .ok_or(SourceError::Failed(rustix::io::Errno::INVAL))?;

        // Hand back a **duplicate** of the producer's fence, without clearing
        // it (upstream PR #230).
        //
        // The scene acquires each source twice per frame -- a TEST commit, then
        // the real one. An owned fence handed over on the first acquire would
        // be gone by the second, so the real commit would go out unsynced and
        // the display engine could sample the buffer before the producer's
        // writes land. Duplicating keeps the pending fence live for the second
        // acquire; the next frame's `set_acquire_fence` replaces it, and
        // re-arming an already-signaled fence is harmless because the kernel
        // proceeds immediately.
        //
        // Each duplicate rides its own `AcquiredBuffer` and closes when that is
        // released, so two acquires yield independently closeable descriptors.
        let acquire_fence = self
            .pending_fence
            .as_ref()
            .and_then(SyncFence::as_fd)
            .and_then(|fd| SyncFence::import(fd).ok());

        Ok(AcquiredBuffer {
            fb_id,
            token: 0,
            acquire_fence,
            damage: Vec::new(),
        })
    }

    /// Lend the imported descriptors back, so the layer can be composited.
    ///
    /// A foreign buffer exposes no CPU mapping, so `map` is correctly
    /// `Unsupported` -- and that alone would make this layer *uncompositable*:
    /// it would blank whenever the allocator could not find it a plane, which
    /// is the case composition exists to rescue. The descriptors are the only
    /// view of these pixels there is, so lending them is what keeps the layer
    /// recoverable. Upstream's `ExternalDmaBufSource` does the same from its
    /// per-plane `duped_fd`.
    ///
    /// # Errors
    ///
    /// [`SourceError::Unsupported`] once the import has been taken by a ring,
    /// which leaves nothing to lend.
    fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
        let (fds, offsets, pitches) = self.imported.dma_buf_planes();
        if fds.is_empty() {
            return Err(SourceError::Unsupported);
        }
        Ok(DmaBufDesc {
            fds: fds.iter().map(std::os::fd::AsFd::as_fd).collect(),
            offsets: offsets.to_vec(),
            pitches: pitches.to_vec(),
            format: self.format,
        })
    }

    fn release(&mut self, _acquired: AcquiredBuffer) {
        // Single-use: this source is one upstream request's worth of pixels.
        // The first retire re-queues it upstream; later releases are no-ops, in
        // case the source is still attached when the scene tears down.
        self.fire_on_release_once();
    }

    fn binding_model(&self) -> BindingModel {
        BindingModel::SceneSubmitsFbId
    }

    fn format(&self) -> SourceFormat {
        self.format
    }

    // `map` keeps its default: the pixels live behind a descriptor this type
    // does not mmap, and composition reaches them through `export_dma_buf`.

    /// Re-import the buffer on the new descriptor, as the reference does.
    ///
    /// The buffer is the producer's and survives the pause; its framebuffer
    /// and GEM handles do not. Without this, every acquire after a resume
    /// would hand out a framebuffer id the new descriptor has never heard of.
    fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
        self.imported
            .reimport(device, self.format)
            .map_err(|error| source_error(&error))
    }
}

/// An import failure as the scene reports it.
pub(crate) fn source_error(error: &ExternalError) -> SourceError {
    match error {
        ExternalError::Io(errno) => SourceError::Failed(*errno),
        ExternalError::Invalid { .. } => SourceError::Failed(rustix::io::Errno::INVAL),
        // Only a pool's submit returns this, never an import.
        ExternalError::Retired => SourceError::WouldBlock,
    }
}

impl ExternalDmaBufSource {
    /// Abandon the import without an ioctl: for an owner replacing this
    /// source after a resume, whose descriptor took the import with it.
    #[cfg(feature = "gbm")]
    pub(crate) fn forget(&mut self) {
        self.imported.forget();
    }
}

impl Drop for ExternalDmaBufSource {
    fn drop(&mut self) {
        // The framebuffer goes with `imported`'s own drop.
        //
        // Torn down without ever being released: the upstream buffer still
        // needs re-queuing, or the producer's pipeline stalls waiting for a
        // slot that will never come back.
        self.fire_on_release_once();
    }
}

/// Minimal adapter so `Drop` can issue one ioctl without holding a `&Device`.
struct RawDevice<'a> {
    fd: BorrowedFd<'a>,
}

impl AsFd for RawDevice<'_> {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.fd
    }
}

impl drm::Device for RawDevice<'_> {}
impl drm::control::Device for RawDevice<'_> {}

/// The plane layout handed to `add_planar_framebuffer`.
struct ImportedLayout {
    width: u32,
    height: u32,
    fourcc: drm::buffer::DrmFourcc,
    modifier: Option<u64>,
    handles: [Option<drm::buffer::Handle>; MAX_PLANES],
    offsets: [u32; MAX_PLANES],
    pitches: [u32; MAX_PLANES],
}

impl drm::buffer::PlanarBuffer for ImportedLayout {
    fn size(&self) -> (u32, u32) {
        (self.width, self.height)
    }

    fn format(&self) -> drm::buffer::DrmFourcc {
        self.fourcc
    }

    fn modifier(&self) -> Option<drm::buffer::DrmModifier> {
        self.modifier.map(drm::buffer::DrmModifier::from)
    }

    fn pitches(&self) -> [u32; 4] {
        self.pitches
    }

    fn handles(&self) -> [Option<drm::buffer::Handle>; 4] {
        self.handles
    }

    fn offsets(&self) -> [u32; 4] {
        self.offsets
    }
}

fn errno_of(error: &std::io::Error) -> rustix::io::Errno {
    rustix::io::Errno::from_io_error(error).unwrap_or(rustix::io::Errno::IO)
}