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/// How a buffer's stored values relate to light intensity.
121///
122/// Independent of [`YuvRange`] (where structural black and white sit in the integers) and of the
123/// layout dimensions [`SurfaceFormatKind`] carries: the same bytes can be sRGB-encoded or linear, and
124/// the range is a separate axis again.
125///
126/// The default for an RGB buffer is [`Srgb`](Self::Srgb) — desktop buffers, screen capture and UI
127/// assets are sRGB-encoded — and for a YCbCr one [`Bt709`](Self::Bt709), whose matrix already
128/// produces non-linear `R'G'B'`. [`Linear`](Self::Linear) is what a 3D engine's linear render target
129/// carries; the renderer encodes it before compositing.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
131pub enum TransferFunction {
132 /// sRGB-encoded (`IEC 61966-2-1`): the default for 8-bit RGB.
133 #[default]
134 Srgb,
135 /// Linear light: what an engine's render target holds before any display encoding.
136 Linear,
137 /// The BT.709 curve (BT.601 shares it): YCbCr video's non-linear `R'G'B'`.
138 Bt709,
139}
140
141/// The YCbCr a YCbCr ([`is_yuv`](SurfaceFormatKind::is_yuv)) buffer's bytes are in: the matrix and the
142/// range.
143///
144/// A hint, like [`ChromaReconstruction`] and to the same end — it says how to read the colour the
145/// format carries. A renderer that does not understand one falls back to the default, which is
146/// BT.601 full range: the shape a hand-filled buffer is in, and what the Linux backend assumed before
147/// this was declarable.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
149pub struct YuvColorSpace {
150 /// The colour matrix.
151 pub matrix: YuvMatrix,
152 /// The luma/chroma range.
153 pub range: YuvRange,
154}
155
156impl YuvColorSpace {
157 /// BT.601, limited range — what standard-definition video is almost always in.
158 pub const BT601_LIMITED: Self = Self {
159 matrix: YuvMatrix::Bt601,
160 range: YuvRange::Limited,
161 };
162
163 /// BT.709, limited range — what high-definition video is almost always in.
164 pub const BT709_LIMITED: Self = Self {
165 matrix: YuvMatrix::Bt709,
166 range: YuvRange::Limited,
167 };
168}
169
170/// A dma-buf a producer hands the renderer to composite as a surface.
171///
172/// See the module documentation for the producer's contract — uncompressed, linear across vendors,
173/// dedicated when the planes are split.
174#[derive(Debug, Clone)]
175pub struct DmaBufHandle {
176 /// The surface width, in pixels.
177 pub width: u32,
178 /// The surface height, in pixels.
179 pub height: u32,
180 /// The pixel layout.
181 pub format: SurfaceFormatKind,
182 /// The DRM format modifier (tiling/compression layout); [`Self::LINEAR`] for a linear buffer.
183 pub modifier: u64,
184 /// One plane per [`SurfaceFormatKind::plane_count`].
185 pub planes: SmallVec<[DmaBufPlane; 2]>,
186 /// An optional `sync_file` fence the renderer waits on before sampling the buffer; a fence that
187 /// does not signal in time drops the surface for that frame rather than blocking the frame.
188 pub acquire_fence: Option<Arc<OwnedFd>>,
189 /// An optional descriptor the renderer **signals** once it has finished sampling the buffer — the
190 /// mirror of [`acquire_fence`](Self::acquire_fence), which the renderer waits on. A producer that
191 /// recycles surfaces waits on this before returning one to its pool.
192 ///
193 /// It is not a `sync_file`: `wgpu` owns its queue and offers no way to export one, so what carries
194 /// the signal is an `eventfd` the producer makes, which the renderer writes to once the submission
195 /// that sampled the buffer has completed.
196 pub release: Option<Arc<OwnedFd>>,
197 /// How the renderer should reconstruct chroma; the producer's hint for a YCbCr buffer, and
198 /// ignored for an RGB one.
199 pub chroma: ChromaReconstruction,
200 /// The colour space this buffer's YCbCr bytes are in; ignored for an RGB one, whose bytes are
201 /// already RGB.
202 pub color_space: YuvColorSpace,
203 /// How this buffer's values relate to light. An RGB producer declares whether its bytes are
204 /// sRGB-encoded or linear; for YCbCr it follows the matrix, and the default is the video curve.
205 pub transfer: TransferFunction,
206}
207
208impl PartialEq for DmaBufHandle {
209 fn eq(&self, other: &Self) -> bool {
210 self.width == other.width
211 && self.height == other.height
212 && self.format == other.format
213 && self.modifier == other.modifier
214 && self.planes == other.planes
215 && same_descriptor(&self.acquire_fence, &other.acquire_fence)
216 && same_descriptor(&self.release, &other.release)
217 && self.chroma == other.chroma
218 && self.color_space == other.color_space
219 && self.transfer == other.transfer
220 }
221}
222
223/// Whether two optional descriptors are the same descriptor, by `Arc` identity rather than by
224/// descriptor number. The number is recycled, so two live descriptors that are different objects can
225/// carry the same number — which would make two different frames of one buffer compare equal and be
226/// mistaken for a re-served frame.
227fn same_descriptor(a: &Option<Arc<OwnedFd>>, b: &Option<Arc<OwnedFd>>) -> bool {
228 match (a, b) {
229 (Some(a), Some(b)) => Arc::ptr_eq(a, b),
230 (None, None) => true,
231 _ => false,
232 }
233}
234
235impl Eq for DmaBufHandle {}
236
237impl DmaBufHandle {
238 /// `DRM_FORMAT_MOD_LINEAR`: the only modifier meaningful across GPU vendors.
239 pub const LINEAR: u64 = 0;
240
241 /// Build a handle from its planes.
242 ///
243 /// `planes` is collected, so a caller need not name `SmallVec` to construct a handle (see
244 /// [`SurfaceFormatKind::plane_count`] for how many a format needs); `acquire_fence` is the producer's
245 /// optional `sync_file`.
246 pub fn new(
247 width: u32,
248 height: u32,
249 format: SurfaceFormatKind,
250 modifier: u64,
251 planes: impl IntoIterator<Item = DmaBufPlane>,
252 acquire_fence: Option<OwnedFd>,
253 ) -> Self {
254 Self {
255 width,
256 height,
257 format,
258 modifier,
259 planes: planes.into_iter().collect(),
260 acquire_fence: acquire_fence.map(Arc::new),
261 release: None,
262 chroma: ChromaReconstruction::default(),
263 color_space: YuvColorSpace::default(),
264 // A YCbCr matrix already produces non-linear `R'G'B'`; an RGB buffer's bytes default to
265 // the sRGB encoding every desktop producer emits.
266 transfer: if format.is_yuv() {
267 TransferFunction::Bt709
268 } else {
269 TransferFunction::Srgb
270 },
271 }
272 }
273
274 /// Ask the renderer to reconstruct this buffer's chroma [`LumaGuided`](ChromaReconstruction::LumaGuided)
275 /// rather than [bilinearly](ChromaReconstruction::Bilinear). A hint: a renderer may ignore it.
276 pub fn with_chroma(mut self, chroma: ChromaReconstruction) -> Self {
277 self.chroma = chroma;
278 self
279 }
280
281 /// Declare the colour space this buffer's YCbCr bytes are in. A hint: a renderer that does not
282 /// understand the matrix or range falls back to its default (BT.601 full range).
283 pub fn with_color_space(mut self, color_space: YuvColorSpace) -> Self {
284 self.color_space = color_space;
285 self
286 }
287
288 /// Declare how this buffer's values relate to light. A hint, like the colour space: an RGB
289 /// producer whose bytes are linear says so here, and the renderer encodes them for its
290 /// sRGB-encoded targets. The default follows the format (sRGB for RGB, the video curve for
291 /// YCbCr), so a producer with sRGB-encoded bytes need not call this.
292 pub fn with_transfer(mut self, transfer: TransferFunction) -> Self {
293 self.transfer = transfer;
294 self
295 }
296
297 /// Give the renderer a descriptor to signal once it has finished with this buffer, so a producer
298 /// that recycles the surface behind it can wait rather than guess. See [`Self::release`].
299 pub fn with_release(mut self, release: OwnedFd) -> Self {
300 self.release = Some(Arc::new(release));
301 self
302 }
303
304 /// Signal the release descriptor, telling the producer the buffer is finished with — what the
305 /// renderer does once it has sampled the buffer, for a consumer that took a frame and then
306 /// dropped it without ever painting it.
307 ///
308 /// A no-op where the handle carries no descriptor. The descriptor is shared, so the signal goes
309 /// through a duplicate rather than consuming it.
310 pub fn signal_release(&self) {
311 #[cfg(target_os = "linux")]
312 {
313 use std::io::Write as _;
314
315 // The descriptor is an `eventfd` the producer made; a write of a `u64` bumps its counter,
316 // which the producer's `poll` observes as readable.
317 if let Some(release) = &self.release
318 && let Ok(duplicate) = release.try_clone()
319 {
320 let _ = std::fs::File::from(duplicate).write_all(&1u64.to_ne_bytes());
321 }
322 }
323 }
324
325 /// The number of planes the handle carries, which [`SurfaceFormatKind::plane_count`] should agree
326 /// with.
327 pub fn plane_count(&self) -> usize {
328 self.planes.len()
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::{DmaBufCapabilities, DmaBufFormatCapability};
336
337 /// A real descriptor without a GPU: `/dev/null` is an open file, and any open file is a valid
338 /// plane descriptor as far as the descriptor type is concerned.
339 fn descriptor() -> OwnedFd {
340 std::fs::File::open("/dev/null").expect("/dev/null").into()
341 }
342
343 #[test]
344 fn linear_is_the_zero_modifier() {
345 assert_eq!(DmaBufHandle::LINEAR, 0);
346 }
347
348 #[test]
349 fn new_collects_the_planes_it_is_given() {
350 let handle = DmaBufHandle::new(
351 4,
352 2,
353 SurfaceFormatKind::nv12(),
354 DmaBufHandle::LINEAR,
355 [
356 DmaBufPlane::new(descriptor(), 0, 4),
357 DmaBufPlane::new(descriptor(), 8, 4),
358 ],
359 None,
360 );
361 assert_eq!(handle.plane_count(), 2);
362 assert_eq!((handle.width, handle.height), (4, 2));
363 assert_eq!(handle.format, SurfaceFormatKind::nv12());
364 assert!(handle.acquire_fence.is_none());
365 }
366
367 #[test]
368 fn new_wraps_the_acquire_fence() {
369 let handle = DmaBufHandle::new(
370 1,
371 1,
372 SurfaceFormatKind::rgba8(),
373 DmaBufHandle::LINEAR,
374 [DmaBufPlane::new(descriptor(), 0, 4)],
375 Some(descriptor()),
376 );
377 assert!(handle.acquire_fence.is_some());
378 }
379
380 #[test]
381 fn a_clone_shares_the_descriptor_rather_than_duplicating_it() {
382 let plane = DmaBufPlane::new(descriptor(), 0, 4);
383 let cloned = plane.clone();
384 assert!(Arc::ptr_eq(&plane.fd, &cloned.fd));
385 }
386
387 #[test]
388 fn equality_is_the_descriptor_and_the_layout() {
389 let fd = Arc::new(descriptor());
390 let plane = DmaBufPlane {
391 fd: fd.clone(),
392 offset: 0,
393 stride: 4,
394 };
395 let same = DmaBufPlane {
396 fd: fd.clone(),
397 offset: 0,
398 stride: 4,
399 };
400 // The same buffer at a different offset is a different plane.
401 let elsewhere = DmaBufPlane {
402 fd,
403 offset: 4,
404 stride: 4,
405 };
406 assert_eq!(plane, same);
407 assert_ne!(plane, elsewhere);
408 }
409
410 #[test]
411 fn a_handle_becomes_a_surface_source() {
412 let handle = DmaBufHandle::new(
413 1,
414 1,
415 SurfaceFormatKind::rgba8(),
416 DmaBufHandle::LINEAR,
417 [DmaBufPlane::new(descriptor(), 0, 4)],
418 None,
419 );
420 let source: crate::SurfaceSource = handle.clone().into();
421 assert_eq!(source, crate::SurfaceSource::DmaBuf(handle));
422 }
423
424 /// A handle defaults to BT.601 full range — the space a hand-filled buffer is in, and what the
425 /// backend assumed before the colour space was declarable.
426 #[test]
427 fn a_handle_defaults_to_bt601_full_range() {
428 let handle = DmaBufHandle::new(
429 2,
430 2,
431 SurfaceFormatKind::nv12(),
432 DmaBufHandle::LINEAR,
433 [
434 DmaBufPlane::new(descriptor(), 0, 2),
435 DmaBufPlane::new(descriptor(), 4, 2),
436 ],
437 None,
438 );
439 assert_eq!(
440 handle.color_space,
441 YuvColorSpace {
442 matrix: YuvMatrix::Bt601,
443 range: YuvRange::Full,
444 },
445 );
446 }
447
448 /// The declared colour space is part of the handle's identity, so a scene that compares handles
449 /// does not take two differently-converted buffers for one.
450 #[test]
451 fn the_colour_space_is_part_of_identity() {
452 let handle = DmaBufHandle::new(
453 2,
454 2,
455 SurfaceFormatKind::nv12(),
456 DmaBufHandle::LINEAR,
457 [
458 DmaBufPlane::new(descriptor(), 0, 2),
459 DmaBufPlane::new(descriptor(), 4, 2),
460 ],
461 None,
462 );
463 let declared = handle.clone().with_color_space(YuvColorSpace::BT709_LIMITED);
464 assert_eq!(declared.color_space, YuvColorSpace::BT709_LIMITED);
465 assert_ne!(handle, declared);
466 }
467
468 /// An RGB buffer's bytes default to sRGB-encoded; a YCbCr one's matrix already produces
469 /// non-linear `R'G'B'`, so its curve is the video one.
470 #[test]
471 fn the_transfer_defaults_to_the_formats_curve() {
472 let rgb = DmaBufHandle::new(
473 1,
474 1,
475 SurfaceFormatKind::bgra8(),
476 DmaBufHandle::LINEAR,
477 [DmaBufPlane::new(descriptor(), 0, 4)],
478 None,
479 );
480 assert_eq!(rgb.transfer, TransferFunction::Srgb);
481
482 let yuv = DmaBufHandle::new(
483 2,
484 2,
485 SurfaceFormatKind::nv12(),
486 DmaBufHandle::LINEAR,
487 [
488 DmaBufPlane::new(descriptor(), 0, 2),
489 DmaBufPlane::new(descriptor(), 4, 2),
490 ],
491 None,
492 );
493 assert_eq!(yuv.transfer, TransferFunction::Bt709);
494 }
495
496 /// The declared transfer is semantic, like the colour space: two buffers differing only in it
497 /// must not be mistaken for one.
498 #[test]
499 fn the_transfer_is_part_of_identity() {
500 let handle = DmaBufHandle::new(
501 1,
502 1,
503 SurfaceFormatKind::bgra8(),
504 DmaBufHandle::LINEAR,
505 [DmaBufPlane::new(descriptor(), 0, 4)],
506 None,
507 );
508 let linear = handle.clone().with_transfer(TransferFunction::Linear);
509 assert_eq!(linear.transfer, TransferFunction::Linear);
510 assert_ne!(handle, linear);
511 }
512
513 /// A producer may hand the renderer a descriptor to signal once it is done with the buffer, so a
514 /// surface can be recycled; it defaults to none, and two handles differing only in it are not the
515 /// same buffer.
516 #[test]
517 fn a_handle_carries_a_release_descriptor_when_asked() {
518 let handle = DmaBufHandle::new(
519 1,
520 1,
521 SurfaceFormatKind::rgba8(),
522 DmaBufHandle::LINEAR,
523 [DmaBufPlane::new(descriptor(), 0, 4)],
524 None,
525 );
526 assert!(handle.release.is_none());
527
528 let released = handle.clone().with_release(descriptor());
529 assert!(released.release.is_some());
530 assert_ne!(handle, released);
531 }
532
533 /// The report answers by format and modifier, and absence is a refusal rather than a guess.
534 #[test]
535 fn a_capability_report_answers_by_format_and_modifier() {
536 let caps = DmaBufCapabilities::new(vec![
537 DmaBufFormatCapability {
538 format: SurfaceFormatKind::nv12(),
539 modifiers: vec![DmaBufHandle::LINEAR, 0x0100_0000_0000_0002],
540 },
541 DmaBufFormatCapability {
542 format: SurfaceFormatKind::bgra8(),
543 modifiers: vec![DmaBufHandle::LINEAR],
544 },
545 ]);
546
547 assert!(caps.supports(&SurfaceFormatKind::nv12(), DmaBufHandle::LINEAR));
548 assert!(caps.supports(&SurfaceFormatKind::nv12(), 0x0100_0000_0000_0002));
549 assert!(!caps.supports(&SurfaceFormatKind::nv12(), 0x10));
550 assert!(caps.supports(&SurfaceFormatKind::bgra8(), DmaBufHandle::LINEAR));
551 assert!(!caps.supports(&SurfaceFormatKind::i420(), DmaBufHandle::LINEAR));
552
553 assert_eq!(
554 caps.modifiers(&SurfaceFormatKind::bgra8()),
555 Some(&[DmaBufHandle::LINEAR][..])
556 );
557 assert_eq!(caps.modifiers(&SurfaceFormatKind::rgba8()), None);
558 }
559}