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