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) && self.offset == other.offset && self.stride == other.stride
60    }
61}
62
63impl Eq for DmaBufPlane {}
64
65impl DmaBufPlane {
66    /// A plane from an owned descriptor, its byte offset within the buffer, and its row stride.
67    pub fn new(fd: OwnedFd, offset: u64, stride: u32) -> Self {
68        Self {
69            fd: Arc::new(fd),
70            offset,
71            stride,
72        }
73    }
74}
75
76/// How a renderer reconstructs full-resolution chroma from a 4:2:0 buffer's half-resolution plane.
77///
78/// The producer's hint, not the format's: an `Nv12` buffer is chroma-subsampled either way, and this
79/// says what to do about it. [`Bilinear`](Self::Bilinear) is the honest default — it is what the
80/// format carries, and it keeps a wrong stride or plane size visible rather than plausible.
81/// [`LumaGuided`](Self::LumaGuided) weights the chroma taps by the full-resolution luma, so colour
82/// follows the luma edges instead of being low-passed across them. A renderer may ignore it.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub enum ChromaReconstruction {
85    /// Sample the chroma plane bilinearly, as the format carries it.
86    #[default]
87    Bilinear,
88    /// Reconstruct chroma with the full-resolution luma as a guide.
89    LumaGuided,
90}
91
92/// The colour matrix relating an `Nv12` buffer's luma/chroma to RGB.
93///
94/// The producer's declaration, not the format's: an `Nv12` buffer carries YCbCr either way, and which
95/// matrix turns it back into RGB is a property of the video, not the buffer. A renderer inverts the
96/// matrix named here when it converts.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
98pub enum YuvMatrix {
99    /// BT.601 — `SMPTE 170M` / `BT.470 BG` / `FCC`. The default, and what standard definition is in.
100    #[default]
101    Bt601,
102    /// BT.709 — what high definition is in.
103    Bt709,
104    /// BT.2020 non-constant-luminance — UHD and wide gamut.
105    Bt2020,
106}
107
108/// Whether an `Nv12` buffer's bytes are full or limited ("studio") range.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub enum YuvRange {
111    /// Full range: luma over `0..255`, chroma over `0..255`. `JPEG`/`PC` range, and the default here.
112    #[default]
113    Full,
114    /// Limited range: luma over `16..235`, chroma over `16..240`. `MPEG`/`TV` range, and what almost
115    /// every real stream is in — a full-range buffer read as limited loses contrast, and the reverse
116    /// clips.
117    Limited,
118}
119
120/// The YCbCr a YCbCr ([`is_yuv`](SurfaceFormatKind::is_yuv)) buffer's bytes are in: the matrix and the
121/// range.
122///
123/// A hint, like [`ChromaReconstruction`] and to the same end — it says how to read the colour the
124/// format carries. A renderer that does not understand one falls back to the default, which is
125/// BT.601 full range: the shape a hand-filled buffer is in, and what the Linux backend assumed before
126/// this was declarable.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
128pub struct YuvColorSpace {
129    /// The colour matrix.
130    pub matrix: YuvMatrix,
131    /// The luma/chroma range.
132    pub range: YuvRange,
133}
134
135impl YuvColorSpace {
136    /// BT.601, limited range — what standard-definition video is almost always in.
137    pub const BT601_LIMITED: Self = Self {
138        matrix: YuvMatrix::Bt601,
139        range: YuvRange::Limited,
140    };
141
142    /// BT.709, limited range — what high-definition video is almost always in.
143    pub const BT709_LIMITED: Self = Self {
144        matrix: YuvMatrix::Bt709,
145        range: YuvRange::Limited,
146    };
147}
148
149/// A dma-buf a producer hands the renderer to composite as a surface.
150///
151/// See the module documentation for the producer's contract — uncompressed, linear across vendors,
152/// dedicated when the planes are split.
153#[derive(Debug, Clone)]
154pub struct DmaBufHandle {
155    /// The surface width, in pixels.
156    pub width: u32,
157    /// The surface height, in pixels.
158    pub height: u32,
159    /// The pixel layout.
160    pub format: SurfaceFormatKind,
161    /// The DRM format modifier (tiling/compression layout); [`Self::LINEAR`] for a linear buffer.
162    pub modifier: u64,
163    /// One plane per [`SurfaceFormatKind::plane_count`].
164    pub planes: SmallVec<[DmaBufPlane; 2]>,
165    /// An optional `sync_file` fence the renderer waits on before sampling the buffer; a fence that
166    /// does not signal in time drops the surface for that frame rather than blocking the frame.
167    pub acquire_fence: Option<Arc<OwnedFd>>,
168    /// An optional descriptor the renderer **signals** once it has finished sampling the buffer — the
169    /// mirror of [`acquire_fence`](Self::acquire_fence), which the renderer waits on. A producer that
170    /// recycles surfaces waits on this before returning one to its pool.
171    ///
172    /// It is not a `sync_file`: `wgpu` owns its queue and offers no way to export one, so what carries
173    /// the signal is an `eventfd` the producer makes, which the renderer writes to once the submission
174    /// that sampled the buffer has completed.
175    pub release: Option<Arc<OwnedFd>>,
176    /// How the renderer should reconstruct chroma; the producer's hint for a YCbCr buffer, and
177    /// ignored for an RGB one.
178    pub chroma: ChromaReconstruction,
179    /// The colour space this buffer's YCbCr bytes are in; ignored for an RGB one, whose bytes are
180    /// already RGB.
181    pub color_space: YuvColorSpace,
182}
183
184impl PartialEq for DmaBufHandle {
185    fn eq(&self, other: &Self) -> bool {
186        self.width == other.width
187            && self.height == other.height
188            && self.format == other.format
189            && self.modifier == other.modifier
190            && self.planes == other.planes
191            && same_descriptor(&self.acquire_fence, &other.acquire_fence)
192            && same_descriptor(&self.release, &other.release)
193            && self.chroma == other.chroma
194            && self.color_space == other.color_space
195    }
196}
197
198/// Whether two optional descriptors are the same descriptor, by `Arc` identity rather than by
199/// descriptor number. The number is recycled, so two live descriptors that are different objects can
200/// carry the same number — which would make two different frames of one buffer compare equal and be
201/// mistaken for a re-served frame.
202fn same_descriptor(a: &Option<Arc<OwnedFd>>, b: &Option<Arc<OwnedFd>>) -> bool {
203    match (a, b) {
204        (Some(a), Some(b)) => Arc::ptr_eq(a, b),
205        (None, None) => true,
206        _ => false,
207    }
208}
209
210impl Eq for DmaBufHandle {}
211
212impl DmaBufHandle {
213    /// `DRM_FORMAT_MOD_LINEAR`: the only modifier meaningful across GPU vendors.
214    pub const LINEAR: u64 = 0;
215
216    /// Build a handle from its planes.
217    ///
218    /// `planes` is collected, so a caller need not name `SmallVec` to construct a handle (see
219    /// [`SurfaceFormatKind::plane_count`] for how many a format needs); `acquire_fence` is the producer's
220    /// optional `sync_file`.
221    pub fn new(
222        width: u32,
223        height: u32,
224        format: SurfaceFormatKind,
225        modifier: u64,
226        planes: impl IntoIterator<Item = DmaBufPlane>,
227        acquire_fence: Option<OwnedFd>,
228    ) -> Self {
229        Self {
230            width,
231            height,
232            format,
233            modifier,
234            planes: planes.into_iter().collect(),
235            acquire_fence: acquire_fence.map(Arc::new),
236            release: None,
237            chroma: ChromaReconstruction::default(),
238            color_space: YuvColorSpace::default(),
239        }
240    }
241
242    /// Ask the renderer to reconstruct this buffer's chroma [`LumaGuided`](ChromaReconstruction::LumaGuided)
243    /// rather than [bilinearly](ChromaReconstruction::Bilinear). A hint: a renderer may ignore it.
244    pub fn with_chroma(mut self, chroma: ChromaReconstruction) -> Self {
245        self.chroma = chroma;
246        self
247    }
248
249    /// Declare the colour space this buffer's YCbCr bytes are in. A hint: a renderer that does not
250    /// understand the matrix or range falls back to its default (BT.601 full range).
251    pub fn with_color_space(mut self, color_space: YuvColorSpace) -> Self {
252        self.color_space = color_space;
253        self
254    }
255
256    /// Give the renderer a descriptor to signal once it has finished with this buffer, so a producer
257    /// that recycles the surface behind it can wait rather than guess. See [`Self::release`].
258    pub fn with_release(mut self, release: OwnedFd) -> Self {
259        self.release = Some(Arc::new(release));
260        self
261    }
262
263    /// Signal the release descriptor, telling the producer the buffer is finished with — what the
264    /// renderer does once it has sampled the buffer, for a consumer that took a frame and then
265    /// dropped it without ever painting it.
266    ///
267    /// A no-op where the handle carries no descriptor. The descriptor is shared, so the signal goes
268    /// through a duplicate rather than consuming it.
269    pub fn signal_release(&self) {
270        #[cfg(target_os = "linux")]
271        {
272            use std::io::Write as _;
273
274            // The descriptor is an `eventfd` the producer made; a write of a `u64` bumps its counter,
275            // which the producer's `poll` observes as readable.
276            if let Some(release) = &self.release
277                && let Ok(duplicate) = release.try_clone()
278            {
279                let _ = std::fs::File::from(duplicate).write_all(&1u64.to_ne_bytes());
280            }
281        }
282    }
283
284    /// The number of planes the handle carries, which [`SurfaceFormatKind::plane_count`] should agree
285    /// with.
286    pub fn plane_count(&self) -> usize {
287        self.planes.len()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    /// A real descriptor without a GPU: `/dev/null` is an open file, and any open file is a valid
296    /// plane descriptor as far as the descriptor type is concerned.
297    fn descriptor() -> OwnedFd {
298        std::fs::File::open("/dev/null").expect("/dev/null").into()
299    }
300
301    #[test]
302    fn linear_is_the_zero_modifier() {
303        assert_eq!(DmaBufHandle::LINEAR, 0);
304    }
305
306    #[test]
307    fn new_collects_the_planes_it_is_given() {
308        let handle = DmaBufHandle::new(
309            4,
310            2,
311            SurfaceFormatKind::nv12(),
312            DmaBufHandle::LINEAR,
313            [
314                DmaBufPlane::new(descriptor(), 0, 4),
315                DmaBufPlane::new(descriptor(), 8, 4),
316            ],
317            None,
318        );
319        assert_eq!(handle.plane_count(), 2);
320        assert_eq!((handle.width, handle.height), (4, 2));
321        assert_eq!(handle.format, SurfaceFormatKind::nv12());
322        assert!(handle.acquire_fence.is_none());
323    }
324
325    #[test]
326    fn new_wraps_the_acquire_fence() {
327        let handle = DmaBufHandle::new(
328            1,
329            1,
330            SurfaceFormatKind::rgba8(),
331            DmaBufHandle::LINEAR,
332            [DmaBufPlane::new(descriptor(), 0, 4)],
333            Some(descriptor()),
334        );
335        assert!(handle.acquire_fence.is_some());
336    }
337
338    #[test]
339    fn a_clone_shares_the_descriptor_rather_than_duplicating_it() {
340        let plane = DmaBufPlane::new(descriptor(), 0, 4);
341        let cloned = plane.clone();
342        assert!(Arc::ptr_eq(&plane.fd, &cloned.fd));
343    }
344
345    #[test]
346    fn equality_is_the_descriptor_and_the_layout() {
347        let fd = Arc::new(descriptor());
348        let plane = DmaBufPlane {
349            fd: fd.clone(),
350            offset: 0,
351            stride: 4,
352        };
353        let same = DmaBufPlane {
354            fd: fd.clone(),
355            offset: 0,
356            stride: 4,
357        };
358        // The same buffer at a different offset is a different plane.
359        let elsewhere = DmaBufPlane {
360            fd,
361            offset: 4,
362            stride: 4,
363        };
364        assert_eq!(plane, same);
365        assert_ne!(plane, elsewhere);
366    }
367
368    #[test]
369    fn a_handle_becomes_a_surface_source() {
370        let handle = DmaBufHandle::new(
371            1,
372            1,
373            SurfaceFormatKind::rgba8(),
374            DmaBufHandle::LINEAR,
375            [DmaBufPlane::new(descriptor(), 0, 4)],
376            None,
377        );
378        let source: crate::SurfaceSource = handle.clone().into();
379        assert_eq!(source, crate::SurfaceSource::DmaBuf(handle));
380    }
381
382    /// A handle defaults to BT.601 full range — the space a hand-filled buffer is in, and what the
383    /// backend assumed before the colour space was declarable.
384    #[test]
385    fn a_handle_defaults_to_bt601_full_range() {
386        let handle = DmaBufHandle::new(
387            2,
388            2,
389            SurfaceFormatKind::nv12(),
390            DmaBufHandle::LINEAR,
391            [
392                DmaBufPlane::new(descriptor(), 0, 2),
393                DmaBufPlane::new(descriptor(), 4, 2),
394            ],
395            None,
396        );
397        assert_eq!(
398            handle.color_space,
399            YuvColorSpace {
400                matrix: YuvMatrix::Bt601,
401                range: YuvRange::Full,
402            },
403        );
404    }
405
406    /// The declared colour space is part of the handle's identity, so a scene that compares handles
407    /// does not take two differently-converted buffers for one.
408    #[test]
409    fn the_colour_space_is_part_of_identity() {
410        let handle = DmaBufHandle::new(
411            2,
412            2,
413            SurfaceFormatKind::nv12(),
414            DmaBufHandle::LINEAR,
415            [
416                DmaBufPlane::new(descriptor(), 0, 2),
417                DmaBufPlane::new(descriptor(), 4, 2),
418            ],
419            None,
420        );
421        let declared = handle.clone().with_color_space(YuvColorSpace::BT709_LIMITED);
422        assert_eq!(declared.color_space, YuvColorSpace::BT709_LIMITED);
423        assert_ne!(handle, declared);
424    }
425
426    /// A producer may hand the renderer a descriptor to signal once it is done with the buffer, so a
427    /// surface can be recycled; it defaults to none, and two handles differing only in it are not the
428    /// same buffer.
429    #[test]
430    fn a_handle_carries_a_release_descriptor_when_asked() {
431        let handle = DmaBufHandle::new(
432            1,
433            1,
434            SurfaceFormatKind::rgba8(),
435            DmaBufHandle::LINEAR,
436            [DmaBufPlane::new(descriptor(), 0, 4)],
437            None,
438        );
439        assert!(handle.release.is_none());
440
441        let released = handle.clone().with_release(descriptor());
442        assert!(released.release.is_some());
443        assert_ne!(handle, released);
444    }
445}