diff options
Diffstat (limited to 'src/tty.rs')
| -rw-r--r-- | src/tty.rs | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/src/tty.rs b/src/tty.rs new file mode 100644 index 00000000..1f0b72c3 --- /dev/null +++ b/src/tty.rs @@ -0,0 +1,423 @@ +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::desktop::space::SpaceRenderElements; +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::{LoopData, Niri}; + +const SUPPORTED_COLOR_FORMATS: &[Fourcc] = &[Fourcc::Argb8888, Fourcc::Abgr8888]; + +pub struct Tty { + session: LibSeatSession, + primary_gpu_path: PathBuf, + output_device: Option<OutputDevice>, +} + +type GbmDrmCompositor = + DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmDevice<DrmDeviceFd>, (), 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: &[SpaceRenderElements<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>], + ) { + let output_device = self.output_device.as_mut().unwrap(); + let res = output_device + .drm_compositor + .render_frame::<_, _, GlesRenderbuffer>( + &mut output_device.gles, + elements, + [0.1, 0.1, 0.1, 1.], + ) + .unwrap(); + assert!(!res.needs_sync()); + if res.damage.is_some() { + output_device.drm_compositor.queue_frame(()).unwrap(); + } else { + 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<LoopData>) -> 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| { + data.niri.process_input_event(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 => { + libinput.suspend(); + + if let Some(output_device) = &tty.output_device { + output_device.drm.pause(); + } + } + SessionEvent::ActivateSession => { + if libinput.resume().is_err() { + error!("error resuming libinput"); + } + + if let Some(output_device) = &tty.output_device { + // FIXME: according to Catacomb, resetting DRM+Compositor is preferrable + // here, but currently not possible due to a bug somewhere. + tty.device_changed(output_device.id, niri); + } + + 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<DrmDeviceFd>, + gles: &GlesRenderer, + niri: &mut Niri, + ) -> anyhow::Result<GbmDrmCompositor> { + let formats = Bind::<Dmabuf>::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>(&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) + } +} |
