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}