|
| 1 | +use calloop::LoopHandle; |
| 2 | +use smithay::{ |
| 3 | + backend::{ |
| 4 | + allocator::gbm::GbmDevice, |
| 5 | + drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmNode}, |
| 6 | + libinput::{LibinputInputBackend, LibinputSessionInterface}, |
| 7 | + renderer::{ |
| 8 | + damage::OutputDamageTracker, |
| 9 | + element::surface::WaylandSurfaceRenderElement, |
| 10 | + gles::{GlesRenderer, GlesTarget}, |
| 11 | + Bind, |
| 12 | + }, |
| 13 | + session::{libseat::LibSeatSession, Session}, |
| 14 | + udev::{primary_gpu, UdevBackend, UdevEvent}, |
| 15 | + winit::{self, WinitGraphicsBackend}, |
| 16 | + }, |
| 17 | + output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}, |
| 18 | + reexports::{ |
| 19 | + calloop::timer::{TimeoutAction, Timer}, |
| 20 | + drm::control::{connector, Device as DrmControlDevice, ModeTypeFlags}, |
| 21 | + }, |
| 22 | + utils::{DeviceFd, Point, Size, Transform}, |
| 23 | +}; |
| 24 | +use std::{collections::HashMap, os::unix::io::FromRawFd, time::Duration}; |
| 25 | +use tracing::{error, info, warn}; |
| 26 | + |
| 27 | +use crate::state::BlueState; |
| 28 | + |
| 29 | +pub struct UdevData { |
| 30 | + pub session: LibSeatSession, |
| 31 | + pub primary_gpu: DrmNode, |
| 32 | + pub devices: HashMap<DrmNode, GpuDevice>, |
| 33 | +} |
| 34 | + |
| 35 | +pub struct GpuDevice { |
| 36 | + pub drm: DrmDevice, |
| 37 | + pub gbm: GbmDevice<DrmDeviceFd>, |
| 38 | +} |
| 39 | + |
| 40 | +pub struct WinitData { |
| 41 | + pub backend: WinitGraphicsBackend<GlesRenderer>, |
| 42 | + pub output: Output, |
| 43 | + pub damage_tracker: OutputDamageTracker, |
| 44 | +} |
| 45 | + |
| 46 | +// ── DRM/KMS udev backend ─────────────────────────────────────────────────── |
| 47 | + |
| 48 | +pub fn init_udev( |
| 49 | + state: &mut BlueState, |
| 50 | + session: LibSeatSession, |
| 51 | + loop_handle: &LoopHandle<'static, BlueState>, |
| 52 | +) { |
| 53 | + let seat_name = session.seat(); |
| 54 | + info!("Initializing udev backend on seat: {}", seat_name); |
| 55 | + |
| 56 | + let primary_gpu_path = match primary_gpu(&seat_name) { |
| 57 | + Ok(Some(p)) => p, |
| 58 | + Ok(None) => { error!("No primary GPU found"); return; } |
| 59 | + Err(e) => { error!("GPU detection error: {}", e); return; } |
| 60 | + }; |
| 61 | + |
| 62 | + let primary_gpu = match DrmNode::from_path(&primary_gpu_path) { |
| 63 | + Ok(n) => n, |
| 64 | + Err(e) => { error!("Failed to get DRM node: {}", e); return; } |
| 65 | + }; |
| 66 | + |
| 67 | + info!("Primary GPU: {:?}", primary_gpu); |
| 68 | + |
| 69 | + state.backend_data = crate::state::BackendData::Udev(Box::new(UdevData { |
| 70 | + session: session.clone(), |
| 71 | + primary_gpu, |
| 72 | + devices: HashMap::new(), |
| 73 | + })); |
| 74 | + |
| 75 | + let udev_backend = match UdevBackend::new(&seat_name) { |
| 76 | + Ok(b) => b, |
| 77 | + Err(e) => { error!("Failed to create udev backend: {}", e); return; } |
| 78 | + }; |
| 79 | + |
| 80 | + for (_, path) in udev_backend.device_list() { |
| 81 | + if let Ok(node) = DrmNode::from_path(&path) { |
| 82 | + add_gpu_device(state, node, &path, loop_handle); |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + let lh = loop_handle.clone(); |
| 87 | + loop_handle |
| 88 | + .insert_source(udev_backend, move |event, _, state| match event { |
| 89 | + UdevEvent::Added { path, .. } => { |
| 90 | + if let Ok(node) = DrmNode::from_path(&path) { |
| 91 | + add_gpu_device(state, node, &path, &lh); |
| 92 | + } |
| 93 | + } |
| 94 | + _ => {} |
| 95 | + }) |
| 96 | + .expect("Failed to insert udev source"); |
| 97 | + |
| 98 | + // libinput |
| 99 | + let mut libinput_ctx = |
| 100 | + input::Libinput::new_with_udev(LibinputSessionInterface::from(session)); |
| 101 | + libinput_ctx.udev_assign_seat(&seat_name).unwrap(); |
| 102 | + loop_handle |
| 103 | + .insert_source(LibinputInputBackend::new(libinput_ctx), |event, _, state| { |
| 104 | + crate::input::handle_input(state, event); |
| 105 | + }) |
| 106 | + .expect("Failed to insert libinput source"); |
| 107 | + |
| 108 | + loop_handle |
| 109 | + .insert_source(Timer::from_duration(Duration::from_millis(16)), |_, _, state| { |
| 110 | + let outputs = state.outputs.clone(); |
| 111 | + for out in outputs { render_output(state, &out); } |
| 112 | + TimeoutAction::ToDuration(Duration::from_millis(16)) |
| 113 | + }) |
| 114 | + .expect("Failed to insert render timer"); |
| 115 | +} |
| 116 | + |
| 117 | +fn add_gpu_device( |
| 118 | + state: &mut BlueState, |
| 119 | + node: DrmNode, |
| 120 | + path: &std::path::Path, |
| 121 | + loop_handle: &LoopHandle<'static, BlueState>, |
| 122 | +) { |
| 123 | + let session = match &state.backend_data { |
| 124 | + crate::state::BackendData::Udev(d) => d.session.clone(), |
| 125 | + _ => return, |
| 126 | + }; |
| 127 | + |
| 128 | + let owned_fd = match session.open( |
| 129 | + path, |
| 130 | + rustix::fs::OFlags::RDWR | rustix::fs::OFlags::CLOEXEC | rustix::fs::OFlags::NONBLOCK, |
| 131 | + ) { |
| 132 | + Ok(f) => f, |
| 133 | + Err(e) => { error!("Failed to open DRM device: {}", e); return; } |
| 134 | + }; |
| 135 | + |
| 136 | + let raw_fd = std::os::unix::io::IntoRawFd::into_raw_fd(owned_fd); |
| 137 | + let drm_fd = unsafe { DrmDeviceFd::new(DeviceFd::from_raw_fd(raw_fd)) }; |
| 138 | + |
| 139 | + let (drm, notifier) = match DrmDevice::new(drm_fd.clone(), true) { |
| 140 | + Ok(d) => d, |
| 141 | + Err(e) => { error!("Failed to create DRM device: {}", e); return; } |
| 142 | + }; |
| 143 | + let gbm = match GbmDevice::new(drm_fd) { |
| 144 | + Ok(g) => g, |
| 145 | + Err(e) => { error!("Failed to create GBM device: {}", e); return; } |
| 146 | + }; |
| 147 | + |
| 148 | + loop_handle |
| 149 | + .insert_source(notifier, move |event, _, _state| { |
| 150 | + if let DrmEvent::VBlank(_) = event {} |
| 151 | + }) |
| 152 | + .expect("Failed to insert DRM source"); |
| 153 | + |
| 154 | + init_drm_outputs(state, &drm); |
| 155 | + |
| 156 | + if let crate::state::BackendData::Udev(d) = &mut state.backend_data { |
| 157 | + d.devices.insert(node, GpuDevice { drm, gbm }); |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +fn init_drm_outputs(state: &mut BlueState, drm: &DrmDevice) { |
| 162 | + let res: smithay::reexports::drm::control::ResourceHandles = |
| 163 | + match drm.resource_handles() { |
| 164 | + Ok(r) => r, |
| 165 | + Err(e) => { warn!("Failed to get DRM resources: {}", e); return; } |
| 166 | + }; |
| 167 | + |
| 168 | + for connector in res.connectors() { |
| 169 | + let conn_info: smithay::reexports::drm::control::connector::Info = |
| 170 | + match drm.get_connector(*connector, false) { |
| 171 | + Ok(c) => c, |
| 172 | + Err(_) => continue, |
| 173 | + }; |
| 174 | + |
| 175 | + if conn_info.state() != connector::State::Connected { continue; } |
| 176 | + |
| 177 | + let mode: Option<&smithay::reexports::drm::control::Mode> = conn_info |
| 178 | + .modes() |
| 179 | + .iter() |
| 180 | + .max_by_key(|m| { |
| 181 | + let preferred = m.mode_type().contains(ModeTypeFlags::PREFERRED) as u64; |
| 182 | + let area = m.size().0 as u64 * m.size().1 as u64; |
| 183 | + (preferred << 32) | (area * m.vrefresh() as u64) |
| 184 | + }); |
| 185 | + |
| 186 | + let mode = match mode { Some(m) => m, None => continue }; |
| 187 | + let (w, h) = mode.size(); |
| 188 | + info!("Connector {:?}: {}x{}@{}Hz", connector, w, h, mode.vrefresh()); |
| 189 | + |
| 190 | + let output = Output::new( |
| 191 | + format!("{:?}", conn_info.interface()), |
| 192 | + PhysicalProperties { |
| 193 | + size: conn_info |
| 194 | + .size() |
| 195 | + .map(|(pw, ph)| Size::from((pw as i32, ph as i32))) |
| 196 | + .unwrap_or_default(), |
| 197 | + subpixel: Subpixel::Unknown, |
| 198 | + make: "Blue".to_string(), |
| 199 | + model: "Compositor".to_string(), |
| 200 | + serial_number: None, |
| 201 | + }, |
| 202 | + ); |
| 203 | + |
| 204 | + let smithay_mode = OutputMode { |
| 205 | + size: Size::from((w as i32, h as i32)), |
| 206 | + refresh: mode.vrefresh() as i32 * 1000, |
| 207 | + }; |
| 208 | + output.change_current_state( |
| 209 | + Some(smithay_mode), |
| 210 | + Some(Transform::Normal), |
| 211 | + Some(Scale::Integer(1)), |
| 212 | + Some(Point::from((0, 0))), |
| 213 | + ); |
| 214 | + output.set_preferred(smithay_mode); |
| 215 | + state.space.map_output(&output, Point::from((0, 0))); |
| 216 | + state.outputs.push(output); |
| 217 | + break; // one output for now |
| 218 | + } |
| 219 | +} |
| 220 | + |
| 221 | +// ── Winit backend ────────────────────────────────────────────────────────── |
| 222 | + |
| 223 | +pub fn init_winit( |
| 224 | + state: &mut BlueState, |
| 225 | + backend: WinitGraphicsBackend<GlesRenderer>, |
| 226 | + events: winit::WinitEventLoop, |
| 227 | + loop_handle: &LoopHandle<'static, BlueState>, |
| 228 | +) { |
| 229 | + let size = backend.window_size(); |
| 230 | + info!("Winit window: {}x{}", size.w, size.h); |
| 231 | + |
| 232 | + let output = Output::new( |
| 233 | + "winit".to_string(), |
| 234 | + PhysicalProperties { |
| 235 | + size: Size::from((0, 0)), |
| 236 | + subpixel: Subpixel::Unknown, |
| 237 | + make: "Blue".to_string(), |
| 238 | + model: "Winit".to_string(), |
| 239 | + serial_number: None, |
| 240 | + }, |
| 241 | + ); |
| 242 | + |
| 243 | + let mode = OutputMode { |
| 244 | + size: Size::from((size.w as i32, size.h as i32)), |
| 245 | + refresh: 60_000, |
| 246 | + }; |
| 247 | + output.change_current_state( |
| 248 | + Some(mode), |
| 249 | + Some(Transform::Normal), |
| 250 | + Some(Scale::Integer(1)), |
| 251 | + Some(Point::from((0, 0))), |
| 252 | + ); |
| 253 | + output.set_preferred(mode); |
| 254 | + state.space.map_output(&output, Point::from((0, 0))); |
| 255 | + |
| 256 | + let damage_tracker = OutputDamageTracker::from_output(&output); |
| 257 | + state.backend_data = crate::state::BackendData::Winit(Box::new(WinitData { |
| 258 | + backend, |
| 259 | + output: output.clone(), |
| 260 | + damage_tracker, |
| 261 | + })); |
| 262 | + state.outputs.push(output); |
| 263 | + |
| 264 | + loop_handle |
| 265 | + .insert_source(events, |event, _, state| { |
| 266 | + use winit::WinitEvent; |
| 267 | + match event { |
| 268 | + WinitEvent::Resized { size, .. } => { |
| 269 | + if let crate::state::BackendData::Winit(d) = &mut state.backend_data { |
| 270 | + let m = OutputMode { |
| 271 | + size: Size::from((size.w as i32, size.h as i32)), |
| 272 | + refresh: 60_000, |
| 273 | + }; |
| 274 | + d.output.change_current_state(Some(m), None, None, None); |
| 275 | + d.damage_tracker = OutputDamageTracker::from_output(&d.output); |
| 276 | + } |
| 277 | + } |
| 278 | + WinitEvent::Input(event) => crate::input::handle_input(state, event), |
| 279 | + WinitEvent::CloseRequested => state.should_exit = true, |
| 280 | + WinitEvent::Redraw => { |
| 281 | + let output = match &state.backend_data { |
| 282 | + crate::state::BackendData::Winit(d) => d.output.clone(), |
| 283 | + _ => return, |
| 284 | + }; |
| 285 | + render_output(state, &output); |
| 286 | + } |
| 287 | + _ => {} |
| 288 | + } |
| 289 | + }) |
| 290 | + .expect("Failed to insert winit source"); |
| 291 | +} |
| 292 | + |
| 293 | +// ── Common render path ───────────────────────────────────────────────────── |
| 294 | + |
| 295 | +pub fn render_output(state: &mut BlueState, output: &Output) { |
| 296 | + if let crate::state::BackendData::Winit(ref mut d) = state.backend_data { |
| 297 | + let renderer = d.backend.renderer(); |
| 298 | + let mut frame_target = d.backend.bind().expect("Failed to bind winit target"); |
| 299 | + |
| 300 | + let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = state |
| 301 | + .space |
| 302 | + .render_elements_for_output(renderer, output, 1.0) |
| 303 | + .unwrap_or_default(); |
| 304 | + |
| 305 | + let _ = d.damage_tracker.render_output( |
| 306 | + renderer, |
| 307 | + &mut frame_target, |
| 308 | + 0, |
| 309 | + &elements, |
| 310 | + [0.08, 0.10, 0.15, 1.0], |
| 311 | + ); |
| 312 | + |
| 313 | + d.backend.submit(None).ok(); |
| 314 | + d.backend.window().request_redraw(); |
| 315 | + } |
| 316 | +} |
0 commit comments