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(®istry, 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<'_> {}