Skip to main content

drmkit_scene_sources/
gbm.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! A single-buffer source backed by a GBM allocation.
5//!
6//! Port of `src/scene/gbm_buffer_source.{hpp,cpp}`.
7
8use drmkit_core::Device;
9use drmkit_gbm::{GbmBuffer, GbmDevice};
10use drmkit_scene::{
11    AcquiredBuffer, BindingModel, DmaBufDesc, LayerBufferSource, SourceError, SourceFormat,
12};
13
14use crate::{ExternalDmaBufSource, ExternalError, ExternalPlane};
15
16/// One GPU-allocated buffer, created once and scanned out every frame.
17///
18/// The GBM counterpart of [`DumbBufferSource`](crate::DumbBufferSource), and
19/// the source a `ScanoutProducer` hands to the scene: the producer renders
20/// into it and the display engine reads it directly, with no copy in between.
21///
22/// The buffer reaches KMS as a DMA-BUF import rather than through its GEM
23/// handle. That is not a detour -- it is the path that carries the modifier,
24/// so a tiled or compressed allocation arrives at `add_planar_framebuffer`
25/// described as what it actually is. It also means this reuses the import that
26/// [`ExternalDmaBufSource`] already implements and the vkms lane already
27/// covers, rather than a second copy of it.
28///
29/// **Single-buffered**, like its dumb sibling: a producer racing scanout can
30/// tear against the display engine reading it. A ring is the answer where that
31/// matters; this is for a producer that repaints and presents in step, which
32/// is what `drmkit-present`'s scanout backend drives.
33#[derive(Debug)]
34pub struct GbmBufferSource {
35    /// The allocation. Kept because it owns the memory the import refers to:
36    /// the imported framebuffer holds its own reference, but dropping the
37    /// buffer object here would still be dropping the producer's render
38    /// target out from under it.
39    buffer: GbmBuffer,
40    imported: ExternalDmaBufSource,
41    /// The exported DMA-BUF, kept rather than dropped after the import.
42    ///
43    /// The import takes its own reference, so holding this is not what keeps
44    /// the framebuffer alive. It is the descriptor `export_dma_buf` lends.
45    dma_buf: std::os::fd::OwnedFd,
46    /// The exported buffer's row stride, reported alongside the descriptor.
47    pitch: u32,
48    format: SourceFormat,
49    /// Held so the source can re-allocate on session resume, where the old
50    /// descriptor is dead and everything derived from it has to be rebuilt.
51    gbm: GbmDevice,
52}
53
54/// Why a GBM-backed source could not be built.
55#[derive(Debug, thiserror::Error)]
56#[non_exhaustive]
57pub enum GbmSourceError {
58    /// Opening the GBM device, allocating, or exporting the buffer failed.
59    #[error("allocating a GBM buffer: {0}")]
60    Allocate(#[from] drmkit_gbm::GbmError),
61
62    /// The allocation succeeded but could not be imported as a framebuffer.
63    #[error("importing the GBM buffer: {0}")]
64    Import(#[from] ExternalError),
65
66    /// The driver returned a multi-planar allocation.
67    ///
68    /// Refused rather than importing the first plane and calling it the
69    /// buffer, which would scan out one channel of a planar YUV frame as if it
70    /// were the whole image.
71    #[error("the driver allocated {planes} planes; only single-plane buffers are supported")]
72    MultiPlanar {
73        /// How many planes the allocation has.
74        planes: u32,
75    },
76}
77
78impl GbmBufferSource {
79    /// Allocate a scanout buffer and import it as a framebuffer.
80    ///
81    /// `modifiers` constrains the layout the driver may pick -- pass what the
82    /// planes can actually scan out, which is what
83    /// `PlaneRegistry::candidate_modifiers` in `drmkit-planes` answers. An
84    /// empty slice is *no constraint*, not *nothing is acceptable*: it is
85    /// the answer on a driver that exposes no `IN_FORMATS`, and refusing there
86    /// would turn "does not advertise layouts" into "cannot allocate".
87    ///
88    /// The modifier is read back from the allocation rather than assumed to be
89    /// what was asked for, because a driver without the constrained entry
90    /// point falls back to an unconstrained allocation.
91    ///
92    /// # Errors
93    ///
94    /// [`GbmSourceError::Allocate`] if the GBM device cannot be opened, the
95    /// driver refuses the format or size, or the buffer cannot be exported --
96    /// refusing every modifier offered falls back to an unconstrained
97    /// allocation rather than failing; [`GbmSourceError::Import`] if the
98    /// result cannot be made into a framebuffer; [`GbmSourceError::MultiPlanar`] if the
99    /// driver returns more than one plane.
100    pub fn create(
101        device: &Device,
102        width: u32,
103        height: u32,
104        fourcc: u32,
105        modifiers: &[u64],
106    ) -> Result<Self, GbmSourceError> {
107        let gbm = GbmDevice::new(device)?;
108        let buffer = GbmBuffer::create_with_modifiers(&gbm, width, height, fourcc, modifiers)?;
109        let (imported, dma_buf, format) = Self::import(device, &buffer)?;
110
111        Ok(Self {
112            pitch: buffer.stride(),
113            buffer,
114            imported,
115            dma_buf,
116            format,
117            gbm,
118        })
119    }
120
121    /// Export `buffer` as a DMA-BUF and register it as a framebuffer.
122    fn import(
123        device: &Device,
124        buffer: &GbmBuffer,
125    ) -> Result<(ExternalDmaBufSource, std::os::fd::OwnedFd, SourceFormat), GbmSourceError> {
126        let planes = buffer.plane_count();
127        if planes != 1 {
128            return Err(GbmSourceError::MultiPlanar { planes });
129        }
130
131        let format = SourceFormat {
132            fourcc: buffer.fourcc(),
133            // Read back, not assumed: the constrained allocation falls back to
134            // an unconstrained one where the driver has no modifier entry
135            // point, and the framebuffer must describe the layout that exists.
136            modifier: buffer.modifier(),
137            width: buffer.width(),
138            height: buffer.height(),
139        };
140
141        let dma_buf = buffer.export()?;
142        let source = ExternalDmaBufSource::create(
143            device,
144            format,
145            &[ExternalPlane {
146                fd: std::os::fd::AsFd::as_fd(&dma_buf),
147                offset: 0,
148                pitch: buffer.stride(),
149            }],
150            None,
151        )?;
152
153        Ok((source, dma_buf, format))
154    }
155
156    /// The allocation, for a caller that needs to render into it.
157    #[must_use]
158    pub const fn buffer(&self) -> &GbmBuffer {
159        &self.buffer
160    }
161
162    /// The registered framebuffer id.
163    #[must_use]
164    pub fn fb_id(&self) -> Option<u32> {
165        self.imported.fb_id()
166    }
167}
168
169impl LayerBufferSource for GbmBufferSource {
170    fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
171        self.imported.acquire()
172    }
173
174    fn release(&mut self, acquired: AcquiredBuffer) {
175        self.imported.release(acquired);
176    }
177
178    fn binding_model(&self) -> BindingModel {
179        BindingModel::SceneSubmitsFbId
180    }
181
182    fn format(&self) -> SourceFormat {
183        self.format
184    }
185
186    /// Composition reads this buffer through its DMA-BUF, not a CPU map.
187    ///
188    /// A GBM allocation may be tiled or in device-local memory, so there is no
189    /// meaningful CPU view to hand back -- `map` stays `Unsupported`. Without
190    /// this the layer would be uncompositable and would blank whenever the
191    /// allocator could not place it on a plane, which is exactly the case
192    /// composition exists to rescue.
193    ///
194    /// Answered from this source's own export rather than forwarded to the
195    /// wrapped import, which holds a duplicate of the same DMA-BUF: either
196    /// names the GBM allocation.
197    fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
198        Ok(DmaBufDesc {
199            fds: vec![std::os::fd::AsFd::as_fd(&self.dma_buf)],
200            offsets: vec![0],
201            pitches: vec![self.pitch],
202            format: self.format,
203        })
204    }
205
206    fn on_session_paused(&mut self) {
207        self.imported.on_session_paused();
208    }
209
210    fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
211        // Everything here is descriptor-bound: the GBM device wraps the dead
212        // fd, the buffer was allocated against it, and the framebuffer id was
213        // registered on it. Re-allocating is the only correct answer -- and the
214        // shape is preserved, because callers rely on `format()` reporting the
215        // same value afterwards.
216        let SourceFormat {
217            fourcc,
218            modifier,
219            width,
220            height,
221        } = self.format;
222
223        let gbm = GbmDevice::new(device).map_err(|error| resume_error(&error.into()))?;
224        let buffer = GbmBuffer::create_with_modifiers(&gbm, width, height, fourcc, &[modifier])
225            .map_err(|error| resume_error(&error.into()))?;
226        let (imported, dma_buf, format) =
227            Self::import(device, &buffer).map_err(|error| resume_error(&error))?;
228
229        // The old import's framebuffer and handles went with the old
230        // descriptor: forgotten, not torn down through it, as the reference's
231        // `forget` before replacing anything.
232        self.imported.forget();
233        self.gbm = gbm;
234        self.pitch = buffer.stride();
235        self.buffer = buffer;
236        self.imported = imported;
237        self.dma_buf = dma_buf;
238        self.format = format;
239        Ok(())
240    }
241}
242
243/// A failed re-allocation as the scene sees it, with the errno the reference
244/// would report rather than `Unsupported` for everything.
245///
246/// The kernel's errno where there is one. `drmkit-gbm` keeps only a message
247/// for a refused device or allocation, so those are `EIO`, the reference's
248/// own answer when libgbm leaves `errno` unset. A multi-planar allocation is
249/// a layout this source cannot carry, so that one is `Unsupported`.
250fn resume_error(error: &GbmSourceError) -> SourceError {
251    match error {
252        GbmSourceError::Allocate(drmkit_gbm::GbmError::Io(errno)) => SourceError::Failed(*errno),
253        GbmSourceError::Import(error) => crate::external::source_error(error),
254        GbmSourceError::MultiPlanar { .. } => SourceError::Unsupported,
255        _ => SourceError::Failed(rustix::io::Errno::IO),
256    }
257}
258
259#[cfg(test)]
260mod resume_error_tests {
261    use super::{GbmSourceError, resume_error};
262    use drmkit_scene::SourceError;
263    use rustix::io::Errno;
264
265    /// A resume that cannot re-allocate says why, as the reference does,
266    /// rather than claiming the source cannot do it at all.
267    #[test]
268    fn a_failed_reallocation_keeps_its_errno() {
269        let cases = [
270            (
271                GbmSourceError::Allocate(drmkit_gbm::GbmError::Io(Errno::NOMEM)),
272                Errno::NOMEM,
273            ),
274            (
275                GbmSourceError::Allocate(drmkit_gbm::GbmError::Allocation("refused".to_owned())),
276                Errno::IO,
277            ),
278            (
279                GbmSourceError::Import(crate::ExternalError::Io(Errno::NOSPC)),
280                Errno::NOSPC,
281            ),
282        ];
283        for (error, errno) in cases {
284            assert!(
285                matches!(resume_error(&error), SourceError::Failed(got) if got == errno),
286                "{error}"
287            );
288        }
289        assert!(matches!(
290            resume_error(&GbmSourceError::MultiPlanar { planes: 2 }),
291            SourceError::Unsupported
292        ));
293    }
294}