Skip to main content

gpui_engine/
dmabuf.rs

1//! Linux surface descriptors: the dma-buf a producer hands the renderer.
2//!
3//! Linux's transport is a dma-buf — an fd plus a DRM fourcc, a modifier, a stride and an offset —
4//! the sibling of macOS's `IOSurface` and Windows's DXGI NT handle. The engine owns the descriptor
5//! because [`SurfaceSource`](crate::SurfaceSource) names it, exactly as it names `CVPixelBuffer` and
6//! `ID3D11ShaderResourceView`; it is plain data (descriptors and layout), so it pulls no GPU driver
7//! into the engine.
8//!
9//! A [`DmaBufHandle`] bundles that descriptor with the layout an importer needs to sample it: the
10//! surface size, the pixel [`format`](DmaBufHandle::format), the DRM format
11//! [`modifier`](DmaBufHandle::modifier), one [`DmaBufPlane`] per plane (an fd, a byte offset and a row
12//! stride), an optional acquire fence, and — for a YCbCr buffer — how to reconstruct
13//! [`chroma`](DmaBufHandle::chroma) and which [`color_space`](DmaBufHandle::color_space) the bytes are
14//! in. A producer builds one and hands it to the renderer; the renderer duplicates the plane
15//! descriptors into the kernel when it imports.
16//!
17//! # The producer's contract
18//!
19//! The renderer *consumes* the descriptor; it does not negotiate it, and it cannot repair a buffer
20//! that violates one. These are the invariants an importer requires, so a producer must satisfy
21//! them:
22//!
23//! - **Uncompressed under the declared [`modifier`](DmaBufHandle::modifier).** A driver may enable
24//!   implicit (CCS) compression for a sampled tiled image, and that state is not carried by the
25//!   exported plane(s); an importer then reads compressed bytes as raw pixels.
26//! - **Linear across vendors.** A buffer shared between GPUs of different vendors must use
27//!   [`DRM_FORMAT_MOD_LINEAR`](DmaBufHandle::LINEAR) — a vendor-private tiled modifier means nothing
28//!   to a different vendor.
29//! - **A dedicated allocation.** A buffer whose planes are adopted as separate textures (as `NV12`
30//!   is) must be allocated dedicated; some drivers refuse the import otherwise.
31
32use std::os::fd::OwnedFd;
33use std::sync::Arc;
34
35use smallvec::SmallVec;
36
37use crate::SurfaceFormatKind;
38
39/// One plane of a dma-buf: a descriptor, its offset into the buffer, and its row stride.
40///
41/// The descriptor is shared (`Arc`) so a handle clones without duplicating descriptors: the producer
42/// keeps its own reference, and the renderer duplicates in the kernel when it imports.
43#[derive(Debug, Clone)]
44pub struct DmaBufPlane {
45    /// The plane's file descriptor.
46    pub fd: Arc<OwnedFd>,
47    /// The plane's offset within the buffer, in bytes.
48    pub offset: u64,
49    /// The plane's row stride, in bytes.
50    pub stride: u32,
51}
52
53// Identity equality: the same descriptor, by `Arc` identity rather than by descriptor number — the
54// kernel recycles numbers, so two live descriptors that are not the same object can carry the same
55// number. `OwnedFd` is neither `Clone` nor `Eq`, and a scene compares primitives rather than owning
56// them, so identity is what is meant.
57impl PartialEq for DmaBufPlane {
58    fn eq(&self, other: &Self) -> bool {
59        Arc::ptr_eq(&self.fd, &other.fd)
60            && self.offset == other.offset
61            && self.stride == other.stride
62    }
63}
64
65impl Eq for DmaBufPlane {}
66
67impl DmaBufPlane {
68    /// A plane from an owned descriptor, its byte offset within the buffer, and its row stride.
69    pub fn new(fd: OwnedFd, offset: u64, stride: u32) -> Self {
70        Self {
71            fd: Arc::new(fd),
72            offset,
73            stride,
74        }
75    }
76}
77
78/// How a renderer reconstructs full-resolution chroma from a 4:2:0 buffer's half-resolution plane.
79///
80/// The producer's hint, not the format's: an `Nv12` buffer is chroma-subsampled either way, and this
81/// says what to do about it. [`Bilinear`](Self::Bilinear) is the honest default — it is what the
82/// format carries, and it keeps a wrong stride or plane size visible rather than plausible.
83/// [`LumaGuided`](Self::LumaGuided) weights the chroma taps by the full-resolution luma, so colour
84/// follows the luma edges instead of being low-passed across them. A renderer may ignore it.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum ChromaReconstruction {
87    /// Sample the chroma plane bilinearly, as the format carries it.
88    #[default]
89    Bilinear,
90    /// Reconstruct chroma with the full-resolution luma as a guide.
91    LumaGuided,
92}
93
94/// The colour matrix relating an `Nv12` buffer's luma/chroma to RGB.
95///
96/// The producer's declaration, not the format's: an `Nv12` buffer carries YCbCr either way, and which
97/// matrix turns it back into RGB is a property of the video, not the buffer. A renderer inverts the
98/// matrix named here when it converts.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum YuvMatrix {
101    /// BT.601 — `SMPTE 170M` / `BT.470 BG` / `FCC`. The default, and what standard definition is in.
102    #[default]
103    Bt601,
104    /// BT.709 — what high definition is in.
105    Bt709,
106    /// BT.2020 non-constant-luminance — UHD and wide gamut.
107    Bt2020,
108}
109
110/// Whether an `Nv12` buffer's bytes are full or limited ("studio") range.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum YuvRange {
113    /// Full range: luma over `0..255`, chroma over `0..255`. `JPEG`/`PC` range, and the default here.
114    #[default]
115    Full,
116    /// Limited range: luma over `16..235`, chroma over `16..240`. `MPEG`/`TV` range, and what almost
117    /// every real stream is in — a full-range buffer read as limited loses contrast, and the reverse
118    /// clips.
119    Limited,
120}
121
122/// How a buffer's stored values relate to light intensity.
123///
124/// Independent of [`YuvRange`] (where structural black and white sit in the integers) and of the
125/// layout dimensions [`SurfaceFormatKind`] carries: the same bytes can be sRGB-encoded or linear, and
126/// the range is a separate axis again.
127///
128/// The default for an RGB buffer is [`Srgb`](Self::Srgb) — desktop buffers, screen capture and UI
129/// assets are sRGB-encoded — and for a YCbCr one [`Bt709`](Self::Bt709), whose matrix already
130/// produces non-linear `R'G'B'`. [`Linear`](Self::Linear) is what a 3D engine's linear render target
131/// carries; the renderer encodes it before compositing.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum TransferFunction {
134    /// sRGB-encoded (`IEC 61966-2-1`): the default for 8-bit RGB.
135    #[default]
136    Srgb,
137    /// Linear light: what an engine's render target holds before any display encoding.
138    Linear,
139    /// The BT.709 curve (BT.601 shares it): YCbCr video's non-linear `R'G'B'`.
140    Bt709,
141}
142
143/// The YCbCr a YCbCr ([`is_yuv`](SurfaceFormatKind::is_yuv)) buffer's bytes are in: the matrix and the
144/// range.
145///
146/// A hint, like [`ChromaReconstruction`] and to the same end — it says how to read the colour the
147/// format carries. A renderer that does not understand one falls back to the default, which is
148/// BT.601 full range: the shape a hand-filled buffer is in, and what the Linux backend assumed before
149/// this was declarable.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151pub struct YuvColorSpace {
152    /// The colour matrix.
153    pub matrix: YuvMatrix,
154    /// The luma/chroma range.
155    pub range: YuvRange,
156}
157
158impl YuvColorSpace {
159    /// BT.601, limited range — what standard-definition video is almost always in.
160    pub const BT601_LIMITED: Self = Self {
161        matrix: YuvMatrix::Bt601,
162        range: YuvRange::Limited,
163    };
164
165    /// BT.709, limited range — what high-definition video is almost always in.
166    pub const BT709_LIMITED: Self = Self {
167        matrix: YuvMatrix::Bt709,
168        range: YuvRange::Limited,
169    };
170}
171
172/// A dma-buf a producer hands the renderer to composite as a surface.
173///
174/// See the module documentation for the producer's contract — uncompressed, linear across vendors,
175/// dedicated when the planes are split.
176#[derive(Debug, Clone)]
177pub struct DmaBufHandle {
178    /// The surface width, in pixels.
179    pub width: u32,
180    /// The surface height, in pixels.
181    pub height: u32,
182    /// The pixel layout.
183    pub format: SurfaceFormatKind,
184    /// The DRM format modifier (tiling/compression layout); [`Self::LINEAR`] for a linear buffer.
185    pub modifier: u64,
186    /// One plane per [`SurfaceFormatKind::plane_count`].
187    pub planes: SmallVec<[DmaBufPlane; 2]>,
188    /// An optional `sync_file` fence the renderer waits on before sampling the buffer; a fence that
189    /// does not signal in time drops the surface for that frame rather than blocking the frame.
190    pub acquire_fence: Option<Arc<OwnedFd>>,
191    /// An optional descriptor the renderer **signals** once it has finished sampling the buffer — the
192    /// mirror of [`acquire_fence`](Self::acquire_fence), which the renderer waits on. A producer that
193    /// recycles surfaces waits on this before returning one to its pool.
194    ///
195    /// It is not a `sync_file`: `wgpu` owns its queue and offers no way to export one, so what carries
196    /// the signal is an `eventfd` the producer makes, which the renderer writes to once the submission
197    /// that sampled the buffer has completed.
198    pub release: Option<Arc<OwnedFd>>,
199    /// How the renderer should reconstruct chroma; the producer's hint for a YCbCr buffer, and
200    /// ignored for an RGB one.
201    pub chroma: ChromaReconstruction,
202    /// The colour space this buffer's YCbCr bytes are in; ignored for an RGB one, whose bytes are
203    /// already RGB.
204    pub color_space: YuvColorSpace,
205    /// How this buffer's values relate to light. An RGB producer declares whether its bytes are
206    /// sRGB-encoded or linear; for YCbCr it follows the matrix, and the default is the video curve.
207    pub transfer: TransferFunction,
208}
209
210impl PartialEq for DmaBufHandle {
211    fn eq(&self, other: &Self) -> bool {
212        self.width == other.width
213            && self.height == other.height
214            && self.format == other.format
215            && self.modifier == other.modifier
216            && self.planes == other.planes
217            && same_descriptor(&self.acquire_fence, &other.acquire_fence)
218            && same_descriptor(&self.release, &other.release)
219            && self.chroma == other.chroma
220            && self.color_space == other.color_space
221            && self.transfer == other.transfer
222    }
223}
224
225/// Whether two optional descriptors are the same descriptor, by `Arc` identity rather than by
226/// descriptor number. The number is recycled, so two live descriptors that are different objects can
227/// carry the same number — which would make two different frames of one buffer compare equal and be
228/// mistaken for a re-served frame.
229fn same_descriptor(a: &Option<Arc<OwnedFd>>, b: &Option<Arc<OwnedFd>>) -> bool {
230    match (a, b) {
231        (Some(a), Some(b)) => Arc::ptr_eq(a, b),
232        (None, None) => true,
233        _ => false,
234    }
235}
236
237impl Eq for DmaBufHandle {}
238
239impl DmaBufHandle {
240    /// `DRM_FORMAT_MOD_LINEAR`: the only modifier meaningful across GPU vendors.
241    pub const LINEAR: u64 = 0;
242
243    /// Build a handle from its planes.
244    ///
245    /// `planes` is collected, so a caller need not name `SmallVec` to construct a handle (see
246    /// [`SurfaceFormatKind::plane_count`] for how many a format needs); `acquire_fence` is the producer's
247    /// optional `sync_file`.
248    pub fn new(
249        width: u32,
250        height: u32,
251        format: SurfaceFormatKind,
252        modifier: u64,
253        planes: impl IntoIterator<Item = DmaBufPlane>,
254        acquire_fence: Option<OwnedFd>,
255    ) -> Self {
256        Self {
257            width,
258            height,
259            format,
260            modifier,
261            planes: planes.into_iter().collect(),
262            acquire_fence: acquire_fence.map(Arc::new),
263            release: None,
264            chroma: ChromaReconstruction::default(),
265            color_space: YuvColorSpace::default(),
266            // A YCbCr matrix already produces non-linear `R'G'B'`; an RGB buffer's bytes default to
267            // the sRGB encoding every desktop producer emits.
268            transfer: if format.is_yuv() {
269                TransferFunction::Bt709
270            } else {
271                TransferFunction::Srgb
272            },
273        }
274    }
275
276    /// Ask the renderer to reconstruct this buffer's chroma [`LumaGuided`](ChromaReconstruction::LumaGuided)
277    /// rather than [bilinearly](ChromaReconstruction::Bilinear). A hint: a renderer may ignore it.
278    pub fn with_chroma(mut self, chroma: ChromaReconstruction) -> Self {
279        self.chroma = chroma;
280        self
281    }
282
283    /// Declare the colour space this buffer's YCbCr bytes are in. A hint: a renderer that does not
284    /// understand the matrix or range falls back to its default (BT.601 full range).
285    pub fn with_color_space(mut self, color_space: YuvColorSpace) -> Self {
286        self.color_space = color_space;
287        self
288    }
289
290    /// Declare how this buffer's values relate to light. A hint, like the colour space: an RGB
291    /// producer whose bytes are linear says so here, and the renderer encodes them for its
292    /// sRGB-encoded targets. The default follows the format (sRGB for RGB, the video curve for
293    /// YCbCr), so a producer with sRGB-encoded bytes need not call this.
294    pub fn with_transfer(mut self, transfer: TransferFunction) -> Self {
295        self.transfer = transfer;
296        self
297    }
298
299    /// Give the renderer a descriptor to signal once it has finished with this buffer, so a producer
300    /// that recycles the surface behind it can wait rather than guess. See [`Self::release`].
301    pub fn with_release(mut self, release: OwnedFd) -> Self {
302        self.release = Some(Arc::new(release));
303        self
304    }
305
306    /// Signal the release descriptor, telling the producer the buffer is finished with — what the
307    /// renderer does once it has sampled the buffer, for a consumer that took a frame and then
308    /// dropped it without ever painting it.
309    ///
310    /// A no-op where the handle carries no descriptor. The descriptor is shared, so the signal goes
311    /// through a duplicate rather than consuming it.
312    pub fn signal_release(&self) {
313        #[cfg(target_os = "linux")]
314        {
315            use std::io::Write as _;
316
317            // The descriptor is an `eventfd` the producer made; a write of a `u64` bumps its counter,
318            // which the producer's `poll` observes as readable.
319            if let Some(release) = &self.release
320                && let Ok(duplicate) = release.try_clone()
321            {
322                let _ = std::fs::File::from(duplicate).write_all(&1u64.to_ne_bytes());
323            }
324        }
325    }
326
327    /// The number of planes the handle carries, which [`SurfaceFormatKind::plane_count`] should agree
328    /// with.
329    pub fn plane_count(&self) -> usize {
330        self.planes.len()
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use crate::{DmaBufCapabilities, DmaBufFormatCapability};
338
339    /// A real descriptor without a GPU: `/dev/null` is an open file, and any open file is a valid
340    /// plane descriptor as far as the descriptor type is concerned.
341    fn descriptor() -> OwnedFd {
342        std::fs::File::open("/dev/null").expect("/dev/null").into()
343    }
344
345    #[test]
346    fn linear_is_the_zero_modifier() {
347        assert_eq!(DmaBufHandle::LINEAR, 0);
348    }
349
350    #[test]
351    fn new_collects_the_planes_it_is_given() {
352        let handle = DmaBufHandle::new(
353            4,
354            2,
355            SurfaceFormatKind::nv12(),
356            DmaBufHandle::LINEAR,
357            [
358                DmaBufPlane::new(descriptor(), 0, 4),
359                DmaBufPlane::new(descriptor(), 8, 4),
360            ],
361            None,
362        );
363        assert_eq!(handle.plane_count(), 2);
364        assert_eq!((handle.width, handle.height), (4, 2));
365        assert_eq!(handle.format, SurfaceFormatKind::nv12());
366        assert!(handle.acquire_fence.is_none());
367    }
368
369    #[test]
370    fn new_wraps_the_acquire_fence() {
371        let handle = DmaBufHandle::new(
372            1,
373            1,
374            SurfaceFormatKind::rgba8(),
375            DmaBufHandle::LINEAR,
376            [DmaBufPlane::new(descriptor(), 0, 4)],
377            Some(descriptor()),
378        );
379        assert!(handle.acquire_fence.is_some());
380    }
381
382    #[test]
383    fn a_clone_shares_the_descriptor_rather_than_duplicating_it() {
384        let plane = DmaBufPlane::new(descriptor(), 0, 4);
385        let cloned = plane.clone();
386        assert!(Arc::ptr_eq(&plane.fd, &cloned.fd));
387    }
388
389    #[test]
390    fn equality_is_the_descriptor_and_the_layout() {
391        let fd = Arc::new(descriptor());
392        let plane = DmaBufPlane {
393            fd: fd.clone(),
394            offset: 0,
395            stride: 4,
396        };
397        let same = DmaBufPlane {
398            fd: fd.clone(),
399            offset: 0,
400            stride: 4,
401        };
402        // The same buffer at a different offset is a different plane.
403        let elsewhere = DmaBufPlane {
404            fd,
405            offset: 4,
406            stride: 4,
407        };
408        assert_eq!(plane, same);
409        assert_ne!(plane, elsewhere);
410    }
411
412    #[test]
413    fn a_handle_becomes_a_surface_source() {
414        let handle = DmaBufHandle::new(
415            1,
416            1,
417            SurfaceFormatKind::rgba8(),
418            DmaBufHandle::LINEAR,
419            [DmaBufPlane::new(descriptor(), 0, 4)],
420            None,
421        );
422        let source: crate::SurfaceSource = handle.clone().into();
423        assert_eq!(source, crate::SurfaceSource::DmaBuf(handle));
424    }
425
426    /// A handle defaults to BT.601 full range — the space a hand-filled buffer is in, and what the
427    /// backend assumed before the colour space was declarable.
428    #[test]
429    fn a_handle_defaults_to_bt601_full_range() {
430        let handle = DmaBufHandle::new(
431            2,
432            2,
433            SurfaceFormatKind::nv12(),
434            DmaBufHandle::LINEAR,
435            [
436                DmaBufPlane::new(descriptor(), 0, 2),
437                DmaBufPlane::new(descriptor(), 4, 2),
438            ],
439            None,
440        );
441        assert_eq!(
442            handle.color_space,
443            YuvColorSpace {
444                matrix: YuvMatrix::Bt601,
445                range: YuvRange::Full,
446            },
447        );
448    }
449
450    /// The declared colour space is part of the handle's identity, so a scene that compares handles
451    /// does not take two differently-converted buffers for one.
452    #[test]
453    fn the_colour_space_is_part_of_identity() {
454        let handle = DmaBufHandle::new(
455            2,
456            2,
457            SurfaceFormatKind::nv12(),
458            DmaBufHandle::LINEAR,
459            [
460                DmaBufPlane::new(descriptor(), 0, 2),
461                DmaBufPlane::new(descriptor(), 4, 2),
462            ],
463            None,
464        );
465        let declared = handle
466            .clone()
467            .with_color_space(YuvColorSpace::BT709_LIMITED);
468        assert_eq!(declared.color_space, YuvColorSpace::BT709_LIMITED);
469        assert_ne!(handle, declared);
470    }
471
472    /// An RGB buffer's bytes default to sRGB-encoded; a YCbCr one's matrix already produces
473    /// non-linear `R'G'B'`, so its curve is the video one.
474    #[test]
475    fn the_transfer_defaults_to_the_formats_curve() {
476        let rgb = DmaBufHandle::new(
477            1,
478            1,
479            SurfaceFormatKind::bgra8(),
480            DmaBufHandle::LINEAR,
481            [DmaBufPlane::new(descriptor(), 0, 4)],
482            None,
483        );
484        assert_eq!(rgb.transfer, TransferFunction::Srgb);
485
486        let yuv = DmaBufHandle::new(
487            2,
488            2,
489            SurfaceFormatKind::nv12(),
490            DmaBufHandle::LINEAR,
491            [
492                DmaBufPlane::new(descriptor(), 0, 2),
493                DmaBufPlane::new(descriptor(), 4, 2),
494            ],
495            None,
496        );
497        assert_eq!(yuv.transfer, TransferFunction::Bt709);
498    }
499
500    /// The declared transfer is semantic, like the colour space: two buffers differing only in it
501    /// must not be mistaken for one.
502    #[test]
503    fn the_transfer_is_part_of_identity() {
504        let handle = DmaBufHandle::new(
505            1,
506            1,
507            SurfaceFormatKind::bgra8(),
508            DmaBufHandle::LINEAR,
509            [DmaBufPlane::new(descriptor(), 0, 4)],
510            None,
511        );
512        let linear = handle.clone().with_transfer(TransferFunction::Linear);
513        assert_eq!(linear.transfer, TransferFunction::Linear);
514        assert_ne!(handle, linear);
515    }
516
517    /// A producer may hand the renderer a descriptor to signal once it is done with the buffer, so a
518    /// surface can be recycled; it defaults to none, and two handles differing only in it are not the
519    /// same buffer.
520    #[test]
521    fn a_handle_carries_a_release_descriptor_when_asked() {
522        let handle = DmaBufHandle::new(
523            1,
524            1,
525            SurfaceFormatKind::rgba8(),
526            DmaBufHandle::LINEAR,
527            [DmaBufPlane::new(descriptor(), 0, 4)],
528            None,
529        );
530        assert!(handle.release.is_none());
531
532        let released = handle.clone().with_release(descriptor());
533        assert!(released.release.is_some());
534        assert_ne!(handle, released);
535    }
536
537    /// The report answers by format and modifier, and absence is a refusal rather than a guess.
538    #[test]
539    fn a_capability_report_answers_by_format_and_modifier() {
540        let caps = DmaBufCapabilities::new(vec![
541            DmaBufFormatCapability {
542                format: SurfaceFormatKind::nv12(),
543                modifiers: vec![DmaBufHandle::LINEAR, 0x0100_0000_0000_0002],
544            },
545            DmaBufFormatCapability {
546                format: SurfaceFormatKind::bgra8(),
547                modifiers: vec![DmaBufHandle::LINEAR],
548            },
549        ]);
550
551        assert!(caps.supports(&SurfaceFormatKind::nv12(), DmaBufHandle::LINEAR));
552        assert!(caps.supports(&SurfaceFormatKind::nv12(), 0x0100_0000_0000_0002));
553        assert!(!caps.supports(&SurfaceFormatKind::nv12(), 0x10));
554        assert!(caps.supports(&SurfaceFormatKind::bgra8(), DmaBufHandle::LINEAR));
555        assert!(!caps.supports(&SurfaceFormatKind::i420(), DmaBufHandle::LINEAR));
556
557        assert_eq!(
558            caps.modifiers(&SurfaceFormatKind::bgra8()),
559            Some(&[DmaBufHandle::LINEAR][..])
560        );
561        assert_eq!(caps.modifiers(&SurfaceFormatKind::rgba8()), None);
562    }
563}