drmkit_scene_gbm/lib.rs
1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! A scene source backed by a GBM **surface**.
5//!
6//! Port of `src/scene/gbm_surface_source.{hpp,cpp}`.
7//!
8//! # Surface, not buffer
9//!
10//! `drmkit-scene-sources`'s `GbmBufferSource` hands out one
11//! allocation the caller renders into. A *surface* is a swap chain: EGL or
12//! Vulkan renders into it, `eglSwapBuffers` rotates it, and this source locks
13//! whichever buffer came to the front. That is the shape a GL compositor
14//! needs, and the reason both exist.
15//!
16//! # What this crate does not do
17//!
18//! It creates no GL context and issues no draw calls. The producer owns the
19//! rendering; drmkit owns getting the result on screen. [`native_surface`] is
20//! what a producer passes to `eglCreateWindowSurface`.
21//!
22//! # Bind before the first acquire
23//!
24//! A `gbm_surface` is an EGL native window. Mesa routes `lock_front_buffer`,
25//! `has_free_buffers` and `release_buffer` into the window surface EGL (or
26//! Vulkan) creates on it, and until one exists each of them is a `SIGSEGV`,
27//! not an error, on every Mesa device. GBM has no safe query for this, so the
28//! producer has to say it happened. Either bind the surface and render a
29//! frame before adding the layer to a scene, or set
30//! [`SurfaceConfig::require_bind`] and call
31//! [`mark_bound`](GbmSurfaceSource::mark_bound) once the window surface
32//! exists: until then [`acquire`](GbmSurfaceSource::acquire) reports
33//! [`SourceError::WouldBlock`] instead of touching the surface.
34//!
35//! [`native_surface`]: GbmSurfaceSource::native_surface
36
37use std::collections::HashMap;
38
39use drm::control::{Device as ControlDevice, FbCmd2Flags, framebuffer};
40use drmkit_core::Device;
41use drmkit_scene::{AcquiredBuffer, BindingModel, LayerBufferSource, SourceError, SourceFormat};
42use drmkit_sync::SyncFence;
43
44/// How to create the surface.
45#[derive(Debug, Clone, Copy)]
46#[allow(
47 clippy::exhaustive_structs,
48 reason = "built by callers with `..Default::default()`, which a new field does not break"
49)]
50pub struct SurfaceConfig {
51 /// Width in pixels. Non-zero.
52 pub width: u32,
53 /// Height in pixels. Non-zero.
54 pub height: u32,
55 /// The `FourCC` to render and scan out. Non-zero.
56 pub fourcc: u32,
57 /// A layout to constrain the surface to.
58 ///
59 /// `None` lets the driver choose, which is right when the caller has not
60 /// negotiated one. When it has — from
61 /// `PlaneRegistry::candidate_modifiers` intersected with what the render
62 /// API can export — passing it here is what keeps the result scannable.
63 pub modifier: Option<u64>,
64 /// Hold `acquire` at [`SourceError::WouldBlock`] until
65 /// [`GbmSurfaceSource::mark_bound`].
66 ///
67 /// Rather than lock a front buffer on a surface no producer has bound yet,
68 /// which is a `SIGSEGV` in Mesa. A resume rebuilds the surface and closes
69 /// the gate again. Off by default, so a producer that binds before the
70 /// first commit needs nothing.
71 pub require_bind: bool,
72}
73
74impl Default for SurfaceConfig {
75 fn default() -> Self {
76 Self {
77 width: 0,
78 height: 0,
79 fourcc: drmkit_fmt::fourcc::XRGB8888,
80 modifier: None,
81 require_bind: false,
82 }
83 }
84}
85
86/// Why a surface source could not be built or resumed.
87#[derive(Debug, thiserror::Error)]
88#[non_exhaustive]
89pub enum SurfaceError {
90 /// A dimension or format was zero.
91 ///
92 /// Refused here rather than passed to GBM, which reports the same thing
93 /// less clearly and only sometimes.
94 #[error("{field} must be non-zero")]
95 Invalid {
96 /// Which one.
97 field: &'static str,
98 },
99
100 /// The format is not one GBM can name.
101 #[error("format {fourcc:#x} is not a GBM format")]
102 UnsupportedFormat {
103 /// The `FourCC` asked for.
104 fourcc: u32,
105 },
106
107 /// The device could not be opened as a GBM device.
108 #[error("opening a GBM device: {0}")]
109 Device(#[from] drmkit_gbm::GbmError),
110
111 /// The driver refused the surface.
112 #[error("creating the GBM surface: {0}")]
113 Create(String),
114
115 /// A locked buffer could not be registered as a framebuffer.
116 ///
117 /// Raised inside [`acquire`](GbmSurfaceSource::acquire), which reports it
118 /// to the scene as [`SourceError::Failed`] carrying the same errno.
119 #[error("registering a framebuffer: {0}")]
120 Framebuffer(rustix::io::Errno),
121}
122
123/// A GBM swap chain the scene can scan out of.
124pub struct GbmSurfaceSource {
125 surface: drmkit_gbm::GbmSurface,
126 format: SourceFormat,
127 config: SurfaceConfig,
128 /// Framebuffers registered for the surface's buffers, keyed by GEM handle.
129 ///
130 /// A GBM surface cycles a small fixed set of buffers, so the same handles
131 /// come round frame after frame. Registering a framebuffer each time would
132 /// leak one per frame; caching by handle registers each once and destroys
133 /// them together when the surface goes.
134 framebuffers: HashMap<u32, framebuffer::Handle>,
135 /// Buffers currently locked, keyed by the token handed to the scene.
136 ///
137 /// Holding the buffer is what keeps it locked: it goes back to the
138 /// surface on drop, so `release` is the act of dropping the entry.
139 locked: HashMap<u64, drmkit_gbm::GbmBuffer>,
140 next_token: u64,
141 /// The producer's render-done fence, if it set one.
142 pending_fence: Option<SyncFence>,
143 /// The device file the framebuffers are registered on, held for their
144 /// registration and teardown.
145 ///
146 /// A duplicate rather than the number, so a teardown after the caller's
147 /// descriptor closed reaches this file and not whatever reused the number.
148 device_fd: std::os::fd::OwnedFd,
149 /// Set by `on_session_paused`: the descriptor is gone, so the framebuffer
150 /// ids must not be committed and must not be destroyed through it either.
151 paused: bool,
152 /// Set by `mark_bound`: the producer has created its window surface, so
153 /// locking a front buffer is safe. Gates `acquire` only when
154 /// `config.require_bind`, and cleared when a resume rebuilds the surface.
155 bound: bool,
156 /// Declared last, so it drops last. Fields drop in declaration order, and
157 /// the surface and the locked buffers free their GEM handles through this
158 /// device's descriptor -- a libgbm that keeps no descriptor of its own
159 /// (the SA8155P's) would otherwise free them through a closed one.
160 gbm: drmkit_gbm::GbmDevice,
161}
162
163impl std::fmt::Debug for GbmSurfaceSource {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 f.debug_struct("GbmSurfaceSource")
166 .field("format", &self.format)
167 .field("registered", &self.framebuffers.len())
168 .field("locked", &self.locked.len())
169 .finish_non_exhaustive()
170 }
171}
172
173impl GbmSurfaceSource {
174 /// Create a surface on `device`.
175 ///
176 /// # Errors
177 ///
178 /// [`SurfaceError::Invalid`] for a zero dimension or format,
179 /// [`SurfaceError::UnsupportedFormat`] for a `FourCC` GBM does not know,
180 /// [`SurfaceError::Device`] if the node is not a GBM device, and
181 /// [`SurfaceError::Create`] if the driver refuses the surface.
182 pub fn create(device: &Device, config: &SurfaceConfig) -> Result<Self, SurfaceError> {
183 if config.width == 0 {
184 return Err(SurfaceError::Invalid { field: "width" });
185 }
186 if config.height == 0 {
187 return Err(SurfaceError::Invalid { field: "height" });
188 }
189 if config.fourcc == 0 {
190 return Err(SurfaceError::Invalid { field: "fourcc" });
191 }
192 // Refused here rather than by GBM: `gbm_surface_create` reports an
193 // unknown format the same way it reports a driver refusal, and a
194 // caller can do something about only one of them.
195 if drm::buffer::DrmFourcc::try_from(config.fourcc).is_err() {
196 return Err(SurfaceError::UnsupportedFormat {
197 fourcc: config.fourcc,
198 });
199 }
200
201 let gbm = drmkit_gbm::GbmDevice::new(device)?;
202 let surface = Self::make_surface(&gbm, config)?;
203
204 Ok(Self {
205 format: SourceFormat {
206 fourcc: config.fourcc,
207 // Not known until a buffer is locked: the driver picks, and
208 // `acquire` reads it back off the first one.
209 modifier: config.modifier.unwrap_or(0),
210 width: config.width,
211 height: config.height,
212 },
213 surface,
214 config: *config,
215 framebuffers: HashMap::new(),
216 locked: HashMap::new(),
217 next_token: 1,
218 pending_fence: None,
219 device_fd: duplicate(device).map_err(drmkit_gbm::GbmError::Io)?,
220 paused: false,
221 bound: false,
222 gbm,
223 })
224 }
225
226 /// Build the surface, constrained or not.
227 fn make_surface(
228 gbm: &drmkit_gbm::GbmDevice,
229 config: &SurfaceConfig,
230 ) -> Result<drmkit_gbm::GbmSurface, SurfaceError> {
231 let modifiers: Vec<u64> = config.modifier.into_iter().collect();
232 gbm.create_surface(config.width, config.height, config.fourcc, &modifiers)
233 .map_err(|error| SurfaceError::Create(error.to_string()))
234 }
235
236 /// The `gbm_surface` a producer renders into.
237 ///
238 /// What `eglCreateWindowSurface` takes. Borrowed, not owned: the source
239 /// destroys it, and a producer outliving the source would be rendering
240 /// into freed memory.
241 #[must_use]
242 pub const fn native_surface(&self) -> &drmkit_gbm::GbmSurface {
243 &self.surface
244 }
245
246 /// The `gbm_device` the surface belongs to.
247 #[must_use]
248 pub const fn native_device(&self) -> &drmkit_gbm::GbmDevice {
249 &self.gbm
250 }
251
252 /// Set the producer's render-done fence for the next acquire.
253 ///
254 /// That acquire takes it; the one after carries no fence unless the
255 /// producer sets another. The scene hands it to `IN_FENCE_FD` where the
256 /// plane takes one, so the display engine waits for the render rather
257 /// than the CPU doing it.
258 pub fn set_acquire_fence(&mut self, fence: SyncFence) {
259 self.pending_fence = Some(fence);
260 }
261
262 /// The fence for this acquire, which is the producer's for one frame only:
263 /// handed over and cleared, as the reference does, so a frame the
264 /// producer did not fence is not held to the last one's render.
265 fn take_acquire_fence(&mut self) -> Option<SyncFence> {
266 self.pending_fence.take()
267 }
268
269 /// The producer has created its window surface on
270 /// [`native_surface`](Self::native_surface).
271 ///
272 /// Opens the gate [`SurfaceConfig::require_bind`] closes. Call it again
273 /// after every resume: the surface is rebuilt, and the producer has to
274 /// bind the new one.
275 pub const fn mark_bound(&mut self) {
276 self.bound = true;
277 }
278
279 /// Whether the surface has a buffer free to render into.
280 ///
281 /// A producer must check before drawing: with every buffer locked, the
282 /// next `eglSwapBuffers` has nowhere to go. Only once the surface is bound
283 /// -- before that, Mesa segfaults here.
284 #[must_use]
285 pub fn has_free_buffers(&self) -> bool {
286 self.surface.has_free_buffers()
287 }
288
289 /// Register a framebuffer over a locked buffer, or reuse the one this
290 /// buffer already has.
291 fn framebuffer_for(
292 &mut self,
293 device: &RawDevice<'_>,
294 buffer: &drmkit_gbm::GbmBuffer,
295 ) -> Result<framebuffer::Handle, SurfaceError> {
296 let handle = buffer.handle();
297 if let Some(existing) = self.framebuffers.get(&handle) {
298 return Ok(*existing);
299 }
300
301 let fourcc = drm::buffer::DrmFourcc::try_from(self.format.fourcc).map_err(|_| {
302 SurfaceError::UnsupportedFormat {
303 fourcc: self.format.fourcc,
304 }
305 })?;
306 let modifier = buffer.modifier();
307 // The same rule every import follows; see
308 // `drmkit_core::framebuffer_modifier`.
309 let declared = drmkit_core::framebuffer_modifier(device, modifier);
310 let layout = Layout {
311 width: self.format.width,
312 height: self.format.height,
313 fourcc,
314 modifier: declared,
315 handle,
316 pitch: buffer.stride(),
317 };
318 let flags = if declared.is_some() {
319 FbCmd2Flags::MODIFIERS
320 } else {
321 FbCmd2Flags::empty()
322 };
323
324 let fb = device
325 .add_planar_framebuffer(&layout, flags)
326 .map_err(|error| {
327 SurfaceError::Framebuffer(
328 rustix::io::Errno::from_io_error(&error).unwrap_or(rustix::io::Errno::INVAL),
329 )
330 })?;
331 self.framebuffers.insert(handle, fb);
332 Ok(fb)
333 }
334}
335
336/// A descriptor of our own on `device`'s file.
337fn duplicate(device: &Device) -> Result<std::os::fd::OwnedFd, rustix::io::Errno> {
338 std::os::fd::AsFd::as_fd(device)
339 .try_clone_to_owned()
340 .map_err(|error| {
341 rustix::io::Errno::from_io_error(&error).unwrap_or(rustix::io::Errno::MFILE)
342 })
343}
344
345/// A framebuffer failure as the scene sees it: the kernel's errno where it
346/// gave one, as the reference reports it, and `EINVAL` for a format that was
347/// never the kernel's to refuse.
348fn acquire_error(error: &SurfaceError) -> SourceError {
349 match error {
350 SurfaceError::Framebuffer(errno) => SourceError::Failed(*errno),
351 _ => SourceError::Failed(rustix::io::Errno::INVAL),
352 }
353}
354
355/// One buffer's layout, for `add_planar_framebuffer`.
356///
357/// A GBM surface buffer is single-plane by construction, so the other three
358/// slots are always empty.
359struct Layout {
360 width: u32,
361 height: u32,
362 fourcc: drm::buffer::DrmFourcc,
363 modifier: Option<u64>,
364 handle: u32,
365 pitch: u32,
366}
367
368impl drm::buffer::PlanarBuffer for Layout {
369 fn size(&self) -> (u32, u32) {
370 (self.width, self.height)
371 }
372
373 fn format(&self) -> drm::buffer::DrmFourcc {
374 self.fourcc
375 }
376
377 fn modifier(&self) -> Option<drm::buffer::DrmModifier> {
378 self.modifier.map(drm::buffer::DrmModifier::from)
379 }
380
381 fn pitches(&self) -> [u32; 4] {
382 [self.pitch, 0, 0, 0]
383 }
384
385 fn handles(&self) -> [Option<drm::buffer::Handle>; 4] {
386 [
387 std::num::NonZeroU32::new(self.handle).map(drm::buffer::Handle::from),
388 None,
389 None,
390 None,
391 ]
392 }
393
394 fn offsets(&self) -> [u32; 4] {
395 [0; 4]
396 }
397}
398
399/// A bare descriptor wrapper, so framebuffers can be registered and destroyed
400/// without the source owning a `Device`.
401struct RawDevice<'a> {
402 fd: std::os::fd::BorrowedFd<'a>,
403}
404
405impl std::os::fd::AsFd for RawDevice<'_> {
406 fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
407 self.fd
408 }
409}
410
411impl drm::Device for RawDevice<'_> {}
412impl ControlDevice for RawDevice<'_> {}
413
414impl LayerBufferSource for GbmSurfaceSource {
415 /// Lock whatever the producer last rendered.
416 ///
417 /// # Safety of the underlying call
418 ///
419 /// `gbm_surface_lock_front_buffer` must be called exactly once per
420 /// `eglSwapBuffers`, and never before the first. Calling it otherwise is
421 /// undefined behavior in GBM, not merely an error — which is why this is
422 /// the producer's contract to keep and cannot be checked here. A source
423 /// whose producer has not swapped reports [`SourceError::WouldBlock`] only
424 /// where GBM says so. Before the producer binds the surface the call is a
425 /// `SIGSEGV`; [`SurfaceConfig::require_bind`] turns that into
426 /// [`SourceError::WouldBlock`].
427 ///
428 /// # Errors
429 ///
430 /// [`SourceError::WouldBlock`] when there is no front buffer to lock,
431 /// which is flow control. [`SourceError::Failed`] when the buffer could
432 /// not be made into a framebuffer, carrying the kernel's errno from the
433 /// registration.
434 fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
435 if self.paused || (self.config.require_bind && !self.bound) {
436 return Err(SourceError::WouldBlock);
437 }
438
439 // SAFETY: the contract is the producer's, and is documented above. The
440 // surface is live and this source is the only thing that locks it.
441 let buffer = unsafe { self.surface.lock_front_buffer() }.ok_or(SourceError::WouldBlock)?;
442
443 // SAFETY: `device_fd` is this source's own descriptor, which stays open
444 // for the whole call -- nothing below replaces it. Borrowed by number
445 // only because `framebuffer_for` needs `self` mutably beside it.
446 let borrowed = unsafe {
447 std::os::fd::BorrowedFd::borrow_raw(std::os::fd::AsRawFd::as_raw_fd(&self.device_fd))
448 };
449 let device = RawDevice { fd: borrowed };
450 let fb = self
451 .framebuffer_for(&device, &buffer)
452 .map_err(|error| acquire_error(&error))?;
453
454 // The layout is whatever the driver chose, which is only knowable once
455 // a buffer exists. A caller that negotiated one still has to read this
456 // back: the constrained entry point falls back where it is missing.
457 self.format.modifier = buffer.modifier();
458
459 let token = self.next_token;
460 self.next_token = self.next_token.wrapping_add(1).max(1);
461 self.locked.insert(token, buffer);
462
463 Ok(AcquiredBuffer {
464 fb_id: u32::from(fb),
465 token,
466 acquire_fence: self.take_acquire_fence(),
467 damage: Vec::new(),
468 })
469 }
470
471 /// Give the buffer back to the surface.
472 ///
473 /// Dropping the buffer is the release: it calls
474 /// `gbm_surface_release_buffer` for us. A token with no entry is ignored —
475 /// the scene releases on teardown too, and a double release must not
476 /// hand the same buffer back twice.
477 fn release(&mut self, acquired: AcquiredBuffer) {
478 self.locked.remove(&acquired.token);
479 }
480
481 fn binding_model(&self) -> BindingModel {
482 BindingModel::SceneSubmitsFbId
483 }
484
485 fn format(&self) -> SourceFormat {
486 self.format
487 }
488
489 /// The session is losing DRM master.
490 ///
491 /// Every framebuffer id was registered on a descriptor that is about to
492 /// stop working, so they are forgotten rather than destroyed — the ioctl
493 /// would fail, and the kernel reclaims them when the descriptor closes.
494 /// Locked buffers go back to the surface, which is a GBM object and
495 /// unaffected by DRM master.
496 fn on_session_paused(&mut self) {
497 self.paused = true;
498 self.framebuffers.clear();
499 self.locked.clear();
500 }
501
502 /// The session is back with a fresh device.
503 ///
504 /// The surface is rebuilt too, not just the framebuffers: it was created
505 /// against a `gbm_device` wrapping the dead descriptor, and buffers
506 /// allocated through it cannot be registered on the new one.
507 ///
508 /// # Errors
509 ///
510 /// [`SourceError::Unsupported`] if the new device cannot be opened as a
511 /// GBM device or refuses the surface.
512 fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
513 let gbm = drmkit_gbm::GbmDevice::new(device).map_err(|_| SourceError::Unsupported)?;
514 let surface =
515 Self::make_surface(&gbm, &self.config).map_err(|_| SourceError::Unsupported)?;
516
517 // Old buffers, then the old surface, then the old device: each frees
518 // through the descriptor the next one owns.
519 self.locked.clear();
520 self.surface = surface;
521 self.gbm = gbm;
522 self.framebuffers.clear();
523 self.device_fd = duplicate(device).map_err(SourceError::Failed)?;
524 self.paused = false;
525 // A new surface: the producer must bind it again.
526 self.bound = false;
527 Ok(())
528 }
529}
530
531impl Drop for GbmSurfaceSource {
532 fn drop(&mut self) {
533 if self.paused || self.framebuffers.is_empty() {
534 return;
535 }
536 let device = RawDevice {
537 fd: std::os::fd::AsFd::as_fd(&self.device_fd),
538 };
539 for fb in self.framebuffers.values() {
540 // Unactionable in a destructor, and the kernel reclaims them when
541 // the descriptor closes either way.
542 let _ = device.destroy_framebuffer(*fb);
543 }
544 }
545}
546
547#[cfg(test)]
548mod tests;