mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
424 lines
15 KiB
Rust
424 lines
15 KiB
Rust
|
|
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)
|
||
|
|
}
|
||
|
|
}
|