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