use std::os::fd::FromRawFd; use std::path::PathBuf; use std::time::Duration; use anyhow::anyhow; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; use smithay::backend::allocator::Fourcc; use smithay::backend::drm::compositor::DrmCompositor; use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent}; use smithay::backend::egl::{EGLContext, EGLDisplay}; use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::gles::{GlesRenderbuffer, GlesRenderer}; use smithay::backend::renderer::{Bind, ImportEgl}; use smithay::backend::session::libseat::LibSeatSession; use smithay::backend::session::{Event as SessionEvent, Session}; use smithay::backend::udev::{self, UdevBackend, UdevEvent}; use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Subpixel}; use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; use smithay::reexports::calloop::{LoopHandle, RegistrationToken}; use smithay::reexports::drm::control::connector::{ Interface as ConnectorInterface, State as ConnectorState, }; use smithay::reexports::drm::control::{Device, ModeTypeFlags}; use smithay::reexports::input::Libinput; use smithay::reexports::nix::fcntl::OFlag; use smithay::reexports::nix::libc::dev_t; use smithay::utils::DeviceFd; use smithay_drm_extras::edid::EdidInfo; use crate::backend::Backend; use crate::niri::OutputRenderElements; use crate::{LoopData, Niri}; const BACKGROUND_COLOR: [f32; 4] = [0.1, 0.1, 0.1, 1.]; const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888]; pub struct Tty { session: LibSeatSession, primary_gpu_path: PathBuf, output_device: Option, } type GbmDrmCompositor = DrmCompositor, GbmDevice, (), DrmDeviceFd>; struct OutputDevice { id: dev_t, path: PathBuf, token: RegistrationToken, drm: DrmDevice, gles: GlesRenderer, drm_compositor: GbmDrmCompositor, } impl Backend for Tty { fn seat_name(&self) -> String { self.session.seat() } fn renderer(&mut self) -> &mut GlesRenderer { &mut self.output_device.as_mut().unwrap().gles } fn render( &mut self, niri: &mut Niri, elements: &[OutputRenderElements< GlesRenderer, WaylandSurfaceRenderElement, >], ) { let output_device = self.output_device.as_mut().unwrap(); let drm_compositor = &mut output_device.drm_compositor; match drm_compositor.render_frame::<_, _, GlesRenderbuffer>( &mut output_device.gles, elements, BACKGROUND_COLOR, ) { Ok(res) => { assert!(!res.needs_sync()); if res.damage.is_some() { match output_device.drm_compositor.queue_frame(()) { Ok(()) => return, Err(err) => { error!("error queueing frame: {err}"); } } } } Err(err) => { // Can fail if we switched to a different TTY. error!("error rendering frame: {err}"); } } // FIXME: render on demand instead of busy looping. niri.event_loop .insert_source( Timer::from_duration(Duration::from_millis(6)), |_, _, data| { data.niri.redraw(data.tty.as_mut().unwrap()); TimeoutAction::Drop }, ) .unwrap(); } } impl Tty { pub fn new(event_loop: LoopHandle) -> Self { let (session, notifier) = LibSeatSession::new().unwrap(); let seat_name = session.seat(); let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone())); libinput.udev_assign_seat(&seat_name).unwrap(); let input_backend = LibinputInputBackend::new(libinput.clone()); event_loop .insert_source(input_backend, |event, _, data| { let tty = data.tty.as_mut().unwrap(); let mut change_vt = |vt| tty.change_vt(vt); data.niri.process_input_event(&mut change_vt, event); }) .unwrap(); event_loop .insert_source(notifier, move |event, _, data| { let tty = data.tty.as_mut().unwrap(); let niri = &mut data.niri; match event { SessionEvent::PauseSession => { debug!("pausing session"); libinput.suspend(); if let Some(output_device) = &tty.output_device { output_device.drm.pause(); } } SessionEvent::ActivateSession => { debug!("resuming session"); if libinput.resume().is_err() { error!("error resuming libinput"); } if let Some(output_device) = &mut tty.output_device { output_device.drm.activate(); if let Err(err) = output_device.drm_compositor.surface().reset_state() { warn!("error resetting DRM surface state: {err}"); } output_device.drm_compositor.reset_buffers(); } niri.redraw(tty); } } }) .unwrap(); let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap(); Self { session, primary_gpu_path, output_device: None, } } pub fn init(&mut self, niri: &mut Niri) { let backend = UdevBackend::new(&self.session.seat()).unwrap(); for (device_id, path) in backend.device_list() { if let Err(err) = self.device_added(device_id, path.to_owned(), niri) { warn!("error adding device: {err:?}"); } } niri.event_loop .insert_source(backend, move |event, _, data| { let tty = data.tty.as_mut().unwrap(); let niri = &mut data.niri; match event { UdevEvent::Added { device_id, path } => { if let Err(err) = tty.device_added(device_id, path, niri) { warn!("error adding device: {err:?}"); } niri.redraw(tty); } UdevEvent::Changed { device_id } => tty.device_changed(device_id, niri), UdevEvent::Removed { device_id } => tty.device_removed(device_id, niri), } }) .unwrap(); niri.redraw(self); } fn device_added( &mut self, device_id: dev_t, path: PathBuf, niri: &mut Niri, ) -> anyhow::Result<()> { if path != self.primary_gpu_path { debug!("skipping non-primary device {path:?}"); return Ok(()); } debug!("adding device {path:?}"); assert!(self.output_device.is_none()); let open_flags = OFlag::O_RDWR | OFlag::O_CLOEXEC | OFlag::O_NOCTTY | OFlag::O_NONBLOCK; let fd = self.session.open(&path, open_flags)?; let device_fd = unsafe { DrmDeviceFd::new(DeviceFd::from_raw_fd(fd)) }; let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?; let gbm = GbmDevice::new(device_fd)?; let display = EGLDisplay::new(gbm.clone())?; let egl_context = EGLContext::new(&display)?; let mut gles = unsafe { GlesRenderer::new(egl_context)? }; gles.bind_wl_display(&niri.display_handle)?; let drm_compositor = self.create_drm_compositor(&drm, &gbm, &gles, niri)?; let token = niri .event_loop .insert_source(drm_notifier, move |event, metadata, data| { let tty = data.tty.as_mut().unwrap(); match event { DrmEvent::VBlank(_crtc) => { info!("vblank {metadata:?}"); let output_device = tty.output_device.as_mut().unwrap(); // Mark the last frame as submitted. if let Err(err) = output_device.drm_compositor.frame_submitted() { error!("error marking frame as submitted: {err}"); } // Send presentation time feedback. // catacomb // .windows // .mark_presented(&output_device.last_render_states, metadata); // Request redraw before the next VBlank. // let frame_interval = catacomb.windows.output().frame_interval(); // let duration = frame_interval - RENDER_TIME_OFFSET; // catacomb.backend.schedule_redraw(duration); data.niri.redraw(tty); } DrmEvent::Error(error) => error!("DRM error: {error}"), }; }) .unwrap(); self.output_device = Some(OutputDevice { id: device_id, path, token, drm, gles, drm_compositor, }); Ok(()) } fn device_changed(&mut self, device_id: dev_t, niri: &mut Niri) { if let Some(output_device) = &self.output_device { if output_device.id == device_id { debug!("output device changed"); let path = output_device.path.clone(); self.device_removed(device_id, niri); if let Err(err) = self.device_added(device_id, path, niri) { warn!("error adding device: {err:?}"); } } } } fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) { if let Some(mut output_device) = self.output_device.take() { if output_device.id != device_id { self.output_device = Some(output_device); return; } // FIXME: remove wl_output. niri.event_loop.remove(output_device.token); niri.output = None; output_device.gles.unbind_wl_display(); } } fn create_drm_compositor( &mut self, drm: &DrmDevice, gbm: &GbmDevice, gles: &GlesRenderer, niri: &mut Niri, ) -> anyhow::Result { let formats = Bind::::supported_formats(gles) .ok_or_else(|| anyhow!("no supported formats"))?; let resources = drm.resource_handles()?; let mut connector = None; resources .connectors() .iter() .filter_map(|conn| match drm.get_connector(*conn, true) { Ok(info) => Some(info), Err(err) => { warn!("error probing connector: {err}"); None } }) .inspect(|conn| { debug!( "connector: {}-{}, {:?}, {} modes", conn.interface().as_str(), conn.interface_id(), conn.state(), conn.modes().len(), ); }) .filter(|conn| conn.state() == ConnectorState::Connected) // FIXME: don't hardcode eDP. .filter(|conn| conn.interface() == ConnectorInterface::EmbeddedDisplayPort) .for_each(|conn| connector = Some(conn)); let connector = connector.ok_or_else(|| anyhow!("no compatible connector"))?; info!( "picking connector: {}-{}", connector.interface().as_str(), connector.interface_id(), ); let mut mode = connector.modes().get(0); connector.modes().iter().for_each(|m| { debug!("mode: {m:?}"); if m.mode_type().contains(ModeTypeFlags::PREFERRED) { // Pick the highest refresh rate. if mode .map(|curr| curr.vrefresh() < m.vrefresh()) .unwrap_or(true) { mode = Some(m); } } }); let mode = mode.ok_or_else(|| anyhow!("no mode"))?; info!("picking mode: {mode:?}"); let surface = connector .encoders() .iter() .filter_map(|enc| match drm.get_encoder(*enc) { Ok(info) => Some(info), Err(err) => { warn!("error probing encoder: {err}"); None } }) .flat_map(|enc| { // Get all CRTCs compatible with the encoder. let mut crtcs = resources.filter_crtcs(enc.possible_crtcs()); // Sort by maximum number of overlay planes. crtcs.sort_by_cached_key(|crtc| match drm.planes(crtc) { Ok(planes) => -(planes.overlay.len() as isize), Err(err) => { warn!("error probing planes for CRTC: {err}"); 0 } }); crtcs }) .find_map( |crtc| match drm.create_surface(crtc, *mode, &[connector.handle()]) { Ok(surface) => Some(surface), Err(err) => { warn!("error creating DRM surface: {err}"); None } }, ); let surface = surface.ok_or_else(|| anyhow!("no surface"))?; // Create GBM allocator. let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT; let allocator = GbmAllocator::new(gbm.clone(), gbm_flags); // Update the output mode. let (physical_width, physical_height) = connector.size().unwrap_or((0, 0)); let output_name = format!( "{}-{}", connector.interface().as_str(), connector.interface_id(), ); let (make, model) = EdidInfo::for_connector(drm, connector.handle()) .map(|info| (info.manufacturer, info.model)) .unwrap_or_else(|| ("Unknown".into(), "Unknown".into())); let output = Output::new( output_name, PhysicalProperties { size: (physical_width as i32, physical_height as i32).into(), subpixel: Subpixel::Unknown, model, make, }, ); let wl_mode = Mode::from(*mode); output.change_current_state(Some(wl_mode), None, None, Some((0, 0).into())); output.set_preferred(wl_mode); // FIXME: store this somewhere to remove on disconnect, etc. let _global = output.create_global::(&niri.display_handle); niri.space.map_output(&output, (0, 0)); niri.output = Some(output.clone()); // windows.set_output(); // Create the compositor. let compositor = DrmCompositor::new( OutputModeSource::Auto(output), surface, None, allocator, gbm.clone(), SUPPORTED_COLOR_FORMATS, formats, drm.cursor_size(), Some(gbm.clone()), )?; Ok(compositor) } fn change_vt(&mut self, vt: i32) { if let Err(err) = self.session.change_vt(vt) { error!("error changing VT: {err}"); } } }