1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use std::cell::{Cell, RefCell};
5use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
6use std::rc::Rc;
7
8use crate::DeviceOpener;
9use drm::buffer::Buffer;
10use drm::control::Device;
11use i_slint_core::platform::PlatformError;
12
13// Wrapped needed because gbm::Device<T> wants T to be sized.
14#[derive(Clone)]
15pub struct SharedFd(Rc<OwnedFd>);
16impl AsFd for SharedFd {
17 fn as_fd(&self) -> BorrowedFd<'_> {
18 self.0.as_fd()
19 }
20}
21
22impl drm::Device for SharedFd {}
23
24impl drm::control::Device for SharedFd {}
25
26#[derive(Default)]
27enum PageFlipState {
28 #[default]
29 NoFrameBufferPosted,
30 InitialBufferPosted,
31 WaitingForPageFlip {
32 _buffer_to_keep_alive_until_flip: Box<dyn Buffer>,
33 },
34 ReadyForNextBuffer,
35}
36
37pub struct DrmOutput {
38 pub drm_device: SharedFd,
39 connector: drm::control::connector::Info,
40 mode: drm::control::Mode,
41 crtc: drm::control::crtc::Handle,
42 last_buffer: Cell<Option<Box<dyn Buffer>>>,
43 page_flip_state: Rc<RefCell<PageFlipState>>,
44}
45
46impl DrmOutput {
47 pub fn new(device_opener: &DeviceOpener) -> Result<Self, PlatformError> {
48 let mut last_err = None;
49 if let Ok(drm_devices) = std::fs::read_dir("/dev/dri/") {
50 for device in drm_devices {
51 if let Ok(device) = device.map_err(|e| format!("Error opening DRM device: {e}")) {
52 match Self::new_with_path(device_opener, &device.path()) {
53 Ok(dsp) => return Ok(dsp),
54 Err(e) => last_err = Some(e),
55 }
56 }
57 }
58 }
59 Err(last_err.unwrap_or_else(|| "Could not create an egl display".into()))
60 }
61
62 fn new_with_path(
63 device_opener: &DeviceOpener,
64 device: &std::path::Path,
65 ) -> Result<Self, PlatformError> {
66 let drm_device = SharedFd(device_opener(device)?);
67
68 let resources = drm_device
69 .resource_handles()
70 .map_err(|e| format!("Error reading DRM resource handles: {e}"))?;
71
72 let connector = if let Ok(requested_connector_name) = std::env::var("SLINT_DRM_OUTPUT") {
73 let mut connectors = resources.connectors().iter().filter_map(|handle| {
74 let connector = drm_device.get_connector(*handle, false).ok()?;
75 let name =
76 format!("{}-{}", connector.interface().as_str(), connector.interface_id());
77 let connected = connector.state() == drm::control::connector::State::Connected;
78 Some((name, connector, connected))
79 });
80
81 if requested_connector_name.eq_ignore_ascii_case("list") {
82 let names_and_status = connectors
83 .map(|(name, _, connected)| format!("{} (connected: {})", name, connected))
84 .collect::<Vec<_>>();
85 // Can't return error here because newlines are escaped.
86 eprintln!("\nDRM Output List Requested:\n{}\nPlease select an output with the SLINT_DRM_OUTPUT environment variable and re-run the program.", names_and_status.join("\n"));
87 std::process::exit(1);
88 } else {
89 let (_, connector, connected) =
90 connectors.find(|(name, _, _)| name == &requested_connector_name).ok_or_else(
91 || format!("No output with the name '{}' found", requested_connector_name),
92 )?;
93
94 if !connected {
95 return Err(format!(
96 "Requested output '{}' is not connected",
97 requested_connector_name
98 )
99 .into());
100 };
101
102 connector
103 }
104 } else {
105 resources
106 .connectors()
107 .iter()
108 .find_map(|handle| {
109 let connector = drm_device.get_connector(*handle, false).ok()?;
110 (connector.state() == drm::control::connector::State::Connected)
111 .then(|| connector)
112 })
113 .ok_or_else(|| format!("No connected display connector found"))?
114 };
115
116 let mode = std::env::var("SLINT_DRM_MODE").map_or_else(
117 |_| {
118 connector
119 .modes()
120 .iter()
121 .max_by(|current_mode, next_mode| {
122 let current = (
123 current_mode
124 .mode_type()
125 .contains(drm::control::ModeTypeFlags::PREFERRED),
126 current_mode.size().0 as u32 * current_mode.size().1 as u32,
127 );
128 let next = (
129 next_mode.mode_type().contains(drm::control::ModeTypeFlags::PREFERRED),
130 next_mode.size().0 as u32 * next_mode.size().1 as u32,
131 );
132
133 current.cmp(&next)
134 })
135 .cloned()
136 .ok_or_else(|| format!("No preferred or non-zero size display mode found"))
137 },
138 |mode_str| {
139 let mut modes_and_index = connector.modes().iter().cloned().enumerate();
140
141 if mode_str.to_lowercase() == "list" {
142 let mode_names: Vec<String> = modes_and_index
143 .map(|(index, mode)| {
144 let (width, height) = mode.size();
145 format!(
146 "Index: {index} Width: {width} Height: {height} Refresh Rate: {}",
147 mode.vrefresh()
148 )
149 })
150 .collect();
151
152 // Can't return error here because newlines are escaped.
153 eprintln!("DRM Mode List Requested:\n{}\nPlease select a mode with the SLINT_DRM_MODE environment variable and re-run the program.", mode_names.join("\n"));
154 std::process::exit(1);
155 }
156 let mode_index: usize =
157 mode_str.parse().map_err(|_| format!("Invalid mode index {mode_str}"))?;
158 modes_and_index.nth(mode_index).map_or_else(
159 || Err(format!("Mode index is out of bounds: {mode_index}")),
160 |(_, mode)| Ok(mode),
161 )
162 },
163 )?;
164
165 let encoder = connector
166 .current_encoder()
167 .filter(|current| connector.encoders().iter().any(|h| *h == *current))
168 .and_then(|current| drm_device.get_encoder(current).ok());
169
170 let crtc = if let Some(encoder) = encoder {
171 encoder.crtc().ok_or_else(|| format!("no crtc for encoder"))?
172 } else {
173 // No crtc found for current encoder? Pick the first possible crtc
174 // as described in https://manpages.debian.org/testing/libdrm-dev/drm-kms.7.en.html#CRTC/Encoder_Selection
175 connector
176 .encoders()
177 .iter()
178 .filter_map(|handle| drm_device.get_encoder(*handle).ok())
179 .flat_map(|encoder| resources.filter_crtcs(encoder.possible_crtcs()))
180 .find(|crtc_handle| drm_device.get_crtc(*crtc_handle).is_ok())
181 .ok_or_else(|| {
182 format!(
183 "Could not find any crtc for any encoder connected to output {}-{}",
184 connector.interface().as_str(),
185 connector.interface_id()
186 )
187 })?
188 };
189
190 //eprintln!("mode {}/{}", width, height);
191
192 Ok(Self {
193 drm_device,
194 connector,
195 mode,
196 crtc,
197 last_buffer: Cell::default(),
198 page_flip_state: Default::default(),
199 })
200 }
201
202 pub fn present(
203 &self,
204 front_buffer: impl Buffer + 'static,
205 framebuffer_handle: drm::control::framebuffer::Handle,
206 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
207 if let Some(last_buffer) = self.last_buffer.replace(Some(Box::new(front_buffer))) {
208 self.drm_device
209 .page_flip(self.crtc, framebuffer_handle, drm::control::PageFlipFlags::EVENT, None)
210 .map_err(|e| format!("Error presenting framebuffer on screen: {e}"))?;
211
212 *self.page_flip_state.borrow_mut() =
213 PageFlipState::WaitingForPageFlip { _buffer_to_keep_alive_until_flip: last_buffer };
214 } else {
215 self.drm_device
216 .set_crtc(
217 self.crtc,
218 Some(framebuffer_handle),
219 (0, 0),
220 &[self.connector.handle()],
221 Some(self.mode),
222 )
223 .map_err(|e| format!("Error presenting framebuffer on screen: {e}"))?;
224 *self.page_flip_state.borrow_mut() = PageFlipState::InitialBufferPosted;
225 }
226
227 Ok(())
228 }
229
230 pub fn wait_for_page_flip(&self) {
231 if matches!(
232 *self.page_flip_state.borrow(),
233 PageFlipState::NoFrameBufferPosted
234 | PageFlipState::InitialBufferPosted
235 | PageFlipState::ReadyForNextBuffer
236 ) {
237 return;
238 }
239
240 loop {
241 let Ok(mut event_it) = self.drm_device.receive_events() else {
242 return;
243 };
244
245 if event_it.any(|event| matches!(event, drm::control::Event::PageFlip(..))) {
246 if let PageFlipState::WaitingForPageFlip { .. } =
247 self.page_flip_state.replace(PageFlipState::ReadyForNextBuffer)
248 {
249 return;
250 }
251 }
252 }
253 }
254
255 pub fn size(&self) -> (u32, u32) {
256 let (width, height) = self.mode.size();
257 (width as u32, height as u32)
258 }
259}
260