Skip to main content

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;