Skip to main content

drmkit_present/
backend.rs

1// SPDX-FileCopyrightText: (c) 2026 Joel Winarske
2// SPDX-License-Identifier: MIT
3
4//! The whole scanout path behind one object.
5//!
6//! Port of `src/present/scanout_backend.{hpp,cpp}`.
7
8use std::os::fd::OwnedFd;
9
10use drm::control::Device as ControlDevice;
11use drmkit_core::{
12    AtomicCommitFlags, AtomicRequest, Device, Mode, ObjectType, PropertyBlob, PropertyStore,
13    commit_with_out_fence,
14};
15use drmkit_display::{DriverProfile, ScanoutTarget};
16use drmkit_modeset::ModeInfo as _;
17use drmkit_planes::PlaneRegistry;
18use drmkit_scene::{
19    CommitKind, CommitReport, DeviceCommitter, DisplayParams, KernelResult, LayerHandle,
20    LayerScene, Modeset, PlanePropertyMap, arm_acquire_fences, emit_frame_damaged,
21};
22use drmkit_sync::SyncFence;
23
24use crate::{FrameAction, FrameEconomy, PresentError, ScanoutProducer, negotiate};
25
26/// When to ask for variable refresh.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[non_exhaustive]
29pub enum VrrPolicy {
30    /// Never arm `VRR_ENABLED`.
31    #[default]
32    Off,
33    /// Arm it when the driver profile reports the CRTC capable.
34    Auto,
35    /// Arm it regardless -- a no-op on a CRTC that does not expose it.
36    On,
37}
38
39/// What to leave on screen when the backend goes away.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41#[non_exhaustive]
42pub enum RestorePolicy {
43    /// Leave the last committed frame up.
44    #[default]
45    None,
46    /// Put back the CRTC configuration captured at
47    /// [`create`](ScanoutBackend::create).
48    ///
49    /// What a display board wants on teardown: the console comes back rather
50    /// than the screen freezing on whatever was committed last.
51    SavedCrtc,
52}
53
54/// How to build a scanout backend.
55#[derive(Debug, Clone, Copy)]
56#[allow(
57    clippy::exhaustive_structs,
58    reason = "built by callers with `..Default::default()`, which a new field does not break"
59)]
60pub struct Config {
61    /// The `FourCC` to allocate and scan out.
62    pub fourcc: u32,
63    /// The rotation the layer will be presented with, which the modifier
64    /// negotiation filters against -- a tiled layout a plane cannot rotate is
65    /// not a candidate for a rotated layer.
66    pub rotation: drmkit_fmt::Rotation,
67    /// Whether to ask for variable refresh.
68    pub vrr: VrrPolicy,
69    /// What to leave on screen at teardown.
70    pub restore: RestorePolicy,
71    /// The connector to drive; `0` for the first connected one. See
72    /// [`ScanoutTarget::discover_connector`].
73    pub connector_id: u32,
74}
75
76impl Default for Config {
77    fn default() -> Self {
78        Self {
79            fourcc: drmkit_fmt::fourcc::XRGB8888,
80            rotation: drmkit_fmt::Rotation::Rotate0,
81            vrr: VrrPolicy::Off,
82            restore: RestorePolicy::None,
83            connector_id: 0,
84        }
85    }
86}
87
88/// The CRTC configuration as it stood before the backend touched it.
89#[derive(Debug, Clone, Copy)]
90struct SavedCrtc {
91    handle: drm::control::crtc::Handle,
92    framebuffer: Option<drm::control::framebuffer::Handle>,
93    position: (u32, u32),
94    mode: Option<Mode>,
95}
96
97/// One output, one layer, and everything between a producer and the screen.
98///
99/// Discovers the output, reads the driver's profile, negotiates a modifier
100/// against what the CRTC's planes can scan out, asks the producer for a buffer
101/// in one of them, and drives a single-layer scene over it. What is left for
102/// the caller is rendering and pacing.
103///
104/// The backend does **not** dispatch the page-flip event, for the reason
105/// `DumbScanoutSink` does not: the scene deliberately has no `commit()` of its
106/// own, so the caller drives its own `PageFlip` and calls
107/// [`flip_landed`](Self::flip_landed).
108///
109/// # Teardown
110///
111/// Dropping the backend while a flip it armed is still outstanding waits, up to
112/// 100 ms, for the CRTC's vblank sequence to move past that commit or for its
113/// flip event to be queued, before the scene lets go of its buffers. On a CRTC
114/// with no vblank counter the queued event is the only thing it waits on. It
115/// never reads the event queue, so the event stays queued for the caller.
116/// Dispatching it and calling [`flip_landed`](Self::flip_landed) first means
117/// there is nothing to wait for. The device must outlive the backend: the wait
118/// polls it.
119pub struct ScanoutBackend {
120    target: ScanoutTarget,
121    profile: DriverProfile,
122    modifiers: Vec<u64>,
123    scene: LayerScene,
124    layer: LayerHandle,
125    registry: PlaneRegistry,
126    map: PlanePropertyMap,
127    economy: FrameEconomy,
128    crtc_index: u32,
129    /// Resolved once at create; `None` on a CRTC that exposes no `VRR_ENABLED`.
130    vrr_property: Option<u32>,
131    /// What the caller last asked for, and what was last written -- a change
132    /// between them is what needs `ALLOW_MODESET`.
133    vrr_wanted: bool,
134    vrr_armed: bool,
135    /// Resolved once at create; `None` on a CRTC with no `OUT_FENCE_PTR`.
136    out_fence_property: Option<u32>,
137    needs_modeset: bool,
138    restore: RestorePolicy,
139    saved: Option<SavedCrtc>,
140    /// The descriptor the restore and the teardown flip wait are issued on.
141    /// Held raw because `Drop` has no device argument, and the restore is a
142    /// legacy ioctl either way.
143    device_fd: std::os::fd::RawFd,
144    /// What teardown waits on for the last commit's flip: the CRTC's vblank
145    /// sequence right after it, or only its event where the CRTC has no
146    /// counter. `None` when that commit armed no flip.
147    armed_flip_seq: Option<crate::armed_flip::ArmedFlip>,
148}
149
150impl std::fmt::Debug for ScanoutBackend {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("ScanoutBackend")
153            .field("driver", &self.profile.name)
154            .field("crtc_id", &self.target.crtc.id)
155            .field("modifiers", &self.modifiers.len())
156            .field("vrr", &self.vrr_armed)
157            .finish_non_exhaustive()
158    }
159}
160
161impl ScanoutBackend {
162    /// Discover an output and build everything needed to present on it.
163    ///
164    /// # Errors
165    ///
166    /// [`PresentError::Bind`] if no output can be found, the driver or the
167    /// planes cannot be probed, or the producer cannot allocate.
168    pub fn create(
169        device: &Device,
170        producer: &mut dyn ScanoutProducer,
171        config: &Config,
172    ) -> Result<Self, PresentError> {
173        let target = ScanoutTarget::discover_connector(device, config.connector_id)
174            .map_err(|error| PresentError::Bind(format!("discovering an output: {error}")))?
175            .map_err(|error| PresentError::Bind(format!("discovering an output: {error}")))?;
176        let profile = DriverProfile::probe(device)
177            .map_err(|error| PresentError::Bind(format!("probing the driver: {error}")))?;
178
179        let crtc_id = target.crtc.id;
180        let crtc_index = target.crtc.index;
181        let (width, height) = (target.mode.width(), target.mode.height());
182
183        let registry = PlaneRegistry::probe(device)
184            .map_err(|error| PresentError::Bind(format!("probing planes: {error}")))?;
185        let mut map = PlanePropertyMap::new();
186        for plane in registry.for_crtc(crtc_index) {
187            map.learn_plane(device, plane.id)
188                .map_err(|error| PresentError::Bind(format!("plane {}: {error}", plane.id)))?;
189        }
190
191        let (modifiers, allowed) =
192            Self::negotiate_modifiers(&registry, crtc_index, producer, config);
193
194        let source = producer
195            .create_buffer(width, height, config.fourcc, &allowed)
196            .map_err(|error| PresentError::Bind(format!("allocating: {error}")))?;
197
198        let mut scene = LayerScene::new(crtc_id);
199        let layer = scene.add_layer(source);
200        scene
201            .layer_mut(layer)
202            .ok_or_else(|| PresentError::Bind("the layer vanished".to_owned()))?
203            .set_display(DisplayParams {
204                src_rect: drmkit_planes::Rect {
205                    x: 0,
206                    y: 0,
207                    w: width,
208                    h: height,
209                },
210                dst_rect: drmkit_planes::Rect {
211                    x: 0,
212                    y: 0,
213                    w: width,
214                    h: height,
215                },
216                ..DisplayParams::default()
217            });
218
219        let mut store = PropertyStore::new();
220        let (vrr_property, out_fence_property) = if store
221            .cache_properties(device, crtc_id, ObjectType::Crtc)
222            .is_ok()
223        {
224            (
225                store.property_id(crtc_id, "VRR_ENABLED").ok(),
226                store.property_id(crtc_id, "OUT_FENCE_PTR").ok(),
227            )
228        } else {
229            (None, None)
230        };
231
232        let vrr_wanted = match config.vrr {
233            VrrPolicy::Off => false,
234            VrrPolicy::On => true,
235            VrrPolicy::Auto => profile.vrr_capable,
236        };
237
238        let saved = (config.restore == RestorePolicy::SavedCrtc)
239            .then(|| Self::snapshot_crtc(device, crtc_id))
240            .flatten();
241
242        Ok(Self {
243            target,
244            profile,
245            modifiers,
246            scene,
247            layer,
248            registry,
249            map,
250            economy: FrameEconomy::new(),
251            crtc_index,
252            vrr_property,
253            vrr_wanted,
254            vrr_armed: false,
255            out_fence_property,
256            needs_modeset: true,
257            restore: config.restore,
258            saved,
259            device_fd: device.raw_fd(),
260            armed_flip_seq: None,
261        })
262    }
263
264    /// What the producer may allocate in, and the negotiation it came from.
265    ///
266    /// Negotiated against the union across every non-cursor plane on the
267    /// CRTC, not the primary's `IN_FORMATS` alone -- see
268    /// `PlaneRegistry::candidate_modifiers` for the split-SoC case that makes
269    /// the difference. Where no plane advertises `IN_FORMATS`, or nothing
270    /// overlaps, the negotiation is empty and the producer is allowed
271    /// `LINEAR`, which every plane scans out; the allocator's `TEST_ONLY` then
272    /// decides where it lands.
273    fn negotiate_modifiers(
274        registry: &PlaneRegistry,
275        crtc_index: u32,
276        producer: &mut dyn ScanoutProducer,
277        config: &Config,
278    ) -> (Vec<u64>, Vec<u64>) {
279        let plane_modifiers = registry.candidate_modifiers(crtc_index, config.fourcc);
280        let producer_modifiers = producer.exportable_modifiers(config.fourcc);
281        let modifiers: Vec<u64> = if plane_modifiers.is_empty() {
282            Vec::new()
283        } else {
284            let producer: Vec<drmkit_fmt::Modifier> = producer_modifiers
285                .iter()
286                .copied()
287                .map(drmkit_fmt::Modifier)
288                .collect();
289            let planes: Vec<drmkit_fmt::Modifier> = plane_modifiers
290                .iter()
291                .copied()
292                .map(drmkit_fmt::Modifier)
293                .collect();
294            negotiate(&producer, &planes, config.rotation)
295                .into_iter()
296                .map(|modifier| modifier.0)
297                .collect()
298        };
299        let allowed = if modifiers.is_empty() {
300            vec![drmkit_fmt::Modifier::LINEAR.0]
301        } else {
302            modifiers.clone()
303        };
304        (modifiers, allowed)
305    }
306    /// Capture the CRTC as it stands, for [`RestorePolicy::SavedCrtc`].
307    ///
308    /// Read through the legacy CRTC ioctl rather than atomic properties
309    /// because that is what the restore writes back through: an atomic commit
310    /// does not update the legacy framebuffer field, so a snapshot taken any
311    /// other way would not describe what `set_crtc` can put back.
312    fn snapshot_crtc(device: &Device, crtc_id: u32) -> Option<SavedCrtc> {
313        let handle = drm::control::crtc::Handle::from(std::num::NonZeroU32::new(crtc_id)?);
314        let info = device.get_crtc(handle).ok()?;
315        Some(SavedCrtc {
316            handle,
317            framebuffer: info.framebuffer(),
318            position: info.position(),
319            mode: info.mode(),
320        })
321    }
322
323    /// Present one frame.
324    ///
325    /// `out_fence` receives this commit's `OUT_FENCE` where the CRTC exposes
326    /// `OUT_FENCE_PTR` — it signals once the frame is on screen, which is what
327    /// a producer waits on rather than blocking the CPU until the flip event.
328    ///
329    /// # Errors
330    ///
331    /// [`PresentError::Bind`] if the mode cannot be learned for the first,
332    /// modesetting frame; [`PresentError::Build`] if the scene cannot build
333    /// the frame; [`PresentError::Commit`] if the kernel refuses it.
334    pub fn present(
335        &mut self,
336        device: &Device,
337        flags: AtomicCommitFlags,
338        out_fence: Option<&mut Option<OwnedFd>>,
339    ) -> Result<CommitReport, PresentError> {
340        // An unconditional present is a full commit by definition, and it
341        // still has to be counted -- `frames_committed` reports every frame
342        // sent for commit, not only the gated ones. Running it through the
343        // economy is what keeps one counter authoritative for both paths.
344        self.economy.force_full();
345        let _ = self.economy.decide(true, false);
346        self.commit(device, flags, out_fence)
347    }
348
349    /// Present only if the content changed, suppressing an idle frame.
350    ///
351    /// The **first** frame always commits, whatever `content_changed` says:
352    /// nothing is on screen yet, so "unchanged" describes nothing, and a
353    /// suppressed first frame leaves the scanout contents undefined.
354    ///
355    /// A suppressed frame returns a report with
356    /// [`skipped_idle`](CommitReport::skipped_idle) set and every other field
357    /// zero — there was no commit to count.
358    ///
359    /// # Errors
360    ///
361    /// As [`present`](Self::present).
362    pub fn present_if_changed(
363        &mut self,
364        device: &Device,
365        content_changed: bool,
366        flags: AtomicCommitFlags,
367        out_fence: Option<&mut Option<OwnedFd>>,
368    ) -> Result<CommitReport, PresentError> {
369        // `decide` keeps the counters itself, so this is the only place they
370        // move on the gated path.
371        match self.economy.decide(content_changed, false) {
372            FrameAction::Skip => Ok(CommitReport::suppressed()),
373            FrameAction::CommitFull | FrameAction::CommitDamaged => {
374                self.commit(device, flags, out_fence)
375            }
376        }
377    }
378
379    /// Build, emit and commit one frame.
380    fn commit(
381        &mut self,
382        device: &Device,
383        flags: AtomicCommitFlags,
384        out_fence: Option<&mut Option<OwnedFd>>,
385    ) -> Result<CommitReport, PresentError> {
386        // A local, not a field: it holds a `PropertyBlob` borrowed from the
387        // device, and storing one would put that lifetime on this type and
388        // everything holding it.
389        let modeset = if self.needs_modeset {
390            Some(
391                Modeset::learn(
392                    device,
393                    self.target.crtc.id,
394                    self.target.connector_id,
395                    &self.target.mode,
396                )
397                .map_err(|error| PresentError::Bind(format!("learning the mode: {error}")))?,
398            )
399        } else {
400            None
401        };
402
403        // Enabling variable refresh is a mode change on most drivers, so the
404        // transition needs ALLOW_MODESET even when the mode itself is already
405        // set. Disabling it is not, but the same flag is harmless there and
406        // the alternative is a branch that gets the direction wrong.
407        let vrr_changing = self.vrr_property.is_some() && self.vrr_wanted != self.vrr_armed;
408
409        let mut flags = flags;
410        if modeset.is_some() || vrr_changing {
411            flags |= AtomicCommitFlags::ALLOW_MODESET;
412        }
413
414        let mut committer = DeviceCommitter::new(
415            device,
416            &self.map,
417            &self.registry,
418            self.crtc_index,
419            AtomicCommitFlags::empty(),
420            modeset.as_ref(),
421        );
422
423        // A flip is armed only if this commit actually asked for the event.
424        // Saying otherwise leaves the scene waiting for a completion that will
425        // never arrive: it holds every acquisition open, and teardown then
426        // trips invariant 5 over a flip that was never in flight.
427        let arms_flip = flags.contains(AtomicCommitFlags::PAGE_FLIP_EVENT);
428        let mut build = self.scene.build_frame(
429            &self.registry,
430            self.crtc_index,
431            CommitKind::Real { arms_flip },
432            &mut committer,
433        )?;
434
435        let crtc_id = self.target.crtc.id;
436        let vrr = (self.vrr_property, self.vrr_wanted);
437        // The fence is armed if the caller asked for it, or if a source did:
438        // a producer that opted into release fences needs one whether or not
439        // this particular caller wants to see it.
440        let wanted_internally = self.scene.wants_release_fence();
441        let out_fence_property = (out_fence.is_some() || wanted_internally)
442            .then_some(self.out_fence_property)
443            .flatten();
444
445        // The plane ids are taken before the closure: the commit reports them
446        // back afterwards, and `build` is borrowed mutably inside.
447        let programmed: Vec<_> = build
448            .plan()
449            .iter()
450            .map(|entry| (entry.plane_id, entry.color))
451            .collect();
452
453        let emit = |request: &mut AtomicRequest,
454                    slot: u64|
455         -> Result<Vec<PropertyBlob<'_>>, drmkit_core::CoreError> {
456            // The blobs are handed back for `commit_with_out_fence` to hold
457            // across the commit: the request holds only their ids, and the
458            // end of this closure comes before the ioctl, not after it.
459            let (_, damage_blobs) = emit_frame_damaged(
460                request,
461                &self.map,
462                &mut build,
463                modeset.as_ref(),
464                Some(device),
465            )?;
466            arm_acquire_fences(&mut build, request, &self.map, true)?;
467            if let (Some(property), wanted) = vrr {
468                request.add_property(crtc_id, property, u64::from(wanted))?;
469            }
470            if let Some(property) = out_fence_property {
471                request.add_property(crtc_id, property, slot)?;
472            }
473            Ok(damage_blobs)
474        };
475
476        let outcome = commit_with_out_fence(device, flags, emit);
477
478        let fence = match outcome {
479            Ok(fence) => fence,
480            Err(error) => {
481                // The scene still has to be told, or its acquisitions leak and
482                // the next frame builds on a lie.
483                self.scene.finalize_frame(build, KernelResult::Rejected);
484                return Err(PresentError::Commit(error));
485            }
486        };
487        // Imported before it is handed over, because both halves may want it:
488        // the sources get a duplicate each, and the caller keeps the original.
489        let release_fence = fence
490            .as_ref()
491            .and_then(|fd| SyncFence::import(std::os::fd::AsFd::as_fd(fd)).ok());
492        if let Some(slot) = out_fence {
493            *slot = fence;
494        }
495
496        for (plane_id, color) in programmed {
497            self.map.note_color_committed(plane_id, color);
498        }
499        self.needs_modeset = false;
500        self.vrr_armed = self.vrr_wanted;
501        self.armed_flip_seq = crate::armed_flip::armed_sequence(device, crtc_id, arms_flip);
502        Ok(self
503            .scene
504            .finalize_frame_with_fence(build, KernelResult::Ok, release_fence.as_ref()))
505    }
506
507    /// Ask for variable refresh, or stop asking, from the next frame.
508    ///
509    /// A no-op on a CRTC that exposes no `VRR_ENABLED`.
510    pub const fn set_vrr(&mut self, enable: bool) {
511        self.vrr_wanted = enable;
512    }
513
514    /// Whether the discovered output can do variable refresh.
515    #[must_use]
516    pub const fn vrr_capable(&self) -> bool {
517        self.profile.vrr_capable
518    }
519
520    /// Make the next frame a full commit, whatever the economy would decide.
521    pub const fn force_full_present(&mut self) {
522        self.economy.force_full();
523    }
524
525    /// How many frames were sent for commit.
526    ///
527    /// Counted when the frame is decided, before it is built: a frame whose
528    /// mode could not be learned, that the scene could not build, or that the
529    /// kernel refused is counted all the same.
530    #[must_use]
531    pub const fn frames_committed(&self) -> u64 {
532        self.economy.committed()
533    }
534
535    /// How many frames were suppressed as idle.
536    #[must_use]
537    pub const fn frames_skipped(&self) -> u64 {
538        self.economy.skipped()
539    }
540
541    /// The output this backend found.
542    #[must_use]
543    pub const fn target(&self) -> &ScanoutTarget {
544        &self.target
545    }
546
547    /// The driver's capabilities, as probed.
548    #[must_use]
549    pub const fn profile(&self) -> &DriverProfile {
550        &self.profile
551    }
552
553    /// The negotiated modifiers, most preferred first.
554    ///
555    /// Empty when the backend fell back to `LINEAR` -- no plane advertised
556    /// `IN_FORMATS`, or none of them took a layout the producer could make --
557    /// and the producer was allowed `LINEAR` alone.
558    #[must_use]
559    pub fn modifiers(&self) -> &[u64] {
560        &self.modifiers
561    }
562
563    /// The scene, for a caller adding layers of its own.
564    #[must_use]
565    pub const fn scene(&self) -> &LayerScene {
566        &self.scene
567    }
568
569    /// The scene, mutably.
570    pub const fn scene_mut(&mut self) -> &mut LayerScene {
571        &mut self.scene
572    }
573
574    /// The layer the producer's buffer is presented on.
575    #[must_use]
576    pub const fn layer(&self) -> LayerHandle {
577        self.layer
578    }
579
580    /// The discovered connector, as a handle, for the teardown restore.
581    fn restore_connector(&self) -> Vec<drm::control::connector::Handle> {
582        std::num::NonZeroU32::new(self.target.connector_id)
583            .map(drm::control::connector::Handle::from)
584            .into_iter()
585            .collect()
586    }
587
588    /// Tell the scene the flip it armed has landed.
589    ///
590    /// Until this is called the scene believes a flip is outstanding, which is
591    /// what keeps teardown from releasing a buffer the display engine is still
592    /// reading — invariant 5.
593    pub fn flip_landed(&mut self) {
594        self.scene.flip_landed();
595    }
596}
597
598impl Drop for ScanoutBackend {
599    fn drop(&mut self) {
600        // SAFETY: the descriptor belongs to the device this backend was built
601        // against, which outlives it -- the borrow is only for this body.
602        let borrowed = unsafe { std::os::fd::BorrowedFd::borrow_raw(self.device_fd) };
603        // A flip the last commit armed may still be reading the buffers the
604        // scene releases below. Let it land first, bounded, without touching
605        // the event queue.
606        crate::armed_flip::settle(
607            &mut self.scene,
608            &borrowed,
609            self.target.crtc.id,
610            self.armed_flip_seq,
611        );
612
613        if self.restore != RestorePolicy::SavedCrtc {
614            return;
615        }
616        let Some(saved) = self.saved else { return };
617
618        // Tear the scene down first, so the restore below reprograms a
619        // pipeline that no longer references this backend's framebuffers.
620        // Rust drops fields after this body runs, which is the wrong order, so
621        // the scene is swapped out and dropped here explicitly.
622        drop(std::mem::replace(
623            &mut self.scene,
624            LayerScene::new(self.target.crtc.id),
625        ));
626
627        // A legacy modeset, because that is what the snapshot describes: the
628        // atomic commits this backend issues never touched the legacy
629        // framebuffer field, so putting back what was read from it is the only
630        // thing that restores what was on screen.
631        let device = RawDevice { fd: borrowed };
632        // No saved framebuffer means the CRTC was dark before this backend took
633        // it over, so put it back dark -- no connector, no mode -- rather than
634        // lighting up a framebuffer that never existed. With one, the connector
635        // has to come along: a legacy modeset naming a framebuffer and no
636        // connector is refused, which is a restore that silently does nothing.
637        let (connectors, mode) = if saved.framebuffer.is_some() {
638            (self.restore_connector(), saved.mode)
639        } else {
640            (Vec::new(), None)
641        };
642        // Errors are unactionable in a destructor, and a failed restore leaves
643        // the last frame up -- which is what RestorePolicy::None would have
644        // done anyway.
645        let _ = device.set_crtc(
646            saved.handle,
647            saved.framebuffer,
648            saved.position,
649            &connectors,
650            mode,
651        );
652    }
653}
654
655/// A bare descriptor wrapper, so the destructor can issue one ioctl without
656/// owning a `Device`.
657struct RawDevice<'a> {
658    fd: std::os::fd::BorrowedFd<'a>,
659}
660
661impl std::os::fd::AsFd for RawDevice<'_> {
662    fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
663        self.fd
664    }
665}
666
667impl drm::Device for RawDevice<'_> {}
668impl ControlDevice for RawDevice<'_> {}