| 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 | |
| 4 | use std::cell::{Cell, RefCell}; |
| 5 | use std::os::fd::{AsFd, BorrowedFd, OwnedFd}; |
| 6 | use std::rc::Rc; |
| 7 | |
| 8 | use crate::DeviceOpener; |
| 9 | use drm::buffer::Buffer; |
| 10 | use drm::control::Device; |
| 11 | use i_slint_core::platform::PlatformError; |
| 12 | |
| 13 | // Wrapped needed because gbm::Device<T> wants T to be sized. |
| 14 | #[derive (Clone)] |
| 15 | pub struct SharedFd(Rc<OwnedFd>); |
| 16 | impl AsFd for SharedFd { |
| 17 | fn as_fd(&self) -> BorrowedFd<'_> { |
| 18 | self.0.as_fd() |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | impl drm::Device for SharedFd {} |
| 23 | |
| 24 | impl drm::control::Device for SharedFd {} |
| 25 | |
| 26 | #[derive (Default)] |
| 27 | enum PageFlipState { |
| 28 | #[default] |
| 29 | NoFrameBufferPosted, |
| 30 | InitialBufferPosted, |
| 31 | WaitingForPageFlip { |
| 32 | _buffer_to_keep_alive_until_flip: Box<dyn Buffer>, |
| 33 | }, |
| 34 | ReadyForNextBuffer, |
| 35 | } |
| 36 | |
| 37 | pub 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 | |
| 46 | impl 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 | |