2023-09-14 22:28:26 +04:00
|
|
|
use std::cell::RefCell;
|
2023-08-13 12:46:53 +04:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2024-01-06 08:46:30 +04:00
|
|
|
use std::fmt::Write;
|
2024-03-14 08:32:23 +04:00
|
|
|
use std::iter::zip;
|
2024-03-15 08:54:36 +04:00
|
|
|
use std::num::NonZeroU64;
|
2025-09-17 12:32:26 -04:00
|
|
|
use std::os::fd::{AsFd, OwnedFd};
|
2024-01-03 18:16:20 +04:00
|
|
|
use std::path::Path;
|
2023-09-14 22:28:26 +04:00
|
|
|
use std::rc::Rc;
|
2023-09-27 16:08:11 +04:00
|
|
|
use std::sync::{Arc, Mutex};
|
2023-08-14 15:53:24 +04:00
|
|
|
use std::time::Duration;
|
2024-01-03 18:16:20 +04:00
|
|
|
use std::{io, mem};
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2024-03-03 20:36:13 +01:00
|
|
|
use anyhow::{anyhow, bail, ensure, Context};
|
2024-03-15 08:54:36 +04:00
|
|
|
use bytemuck::cast_slice_mut;
|
2025-10-29 07:10:38 +01:00
|
|
|
use drm_ffi::drm_mode_modeinfo;
|
2023-10-24 15:05:14 +04:00
|
|
|
use libc::dev_t;
|
2025-10-29 07:10:38 +01:00
|
|
|
use niri_config::output::Modeline;
|
2024-09-03 12:13:04 +03:00
|
|
|
use niri_config::{Config, OutputName};
|
2025-10-29 07:10:38 +01:00
|
|
|
use niri_ipc::{HSyncPolarity, VSyncPolarity};
|
2024-02-18 16:43:33 +04:00
|
|
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
2024-06-21 07:53:41 +03:00
|
|
|
use smithay::backend::allocator::format::FormatSet;
|
2023-08-09 11:03:38 +04:00
|
|
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
2024-06-21 07:53:41 +03:00
|
|
|
use smithay::backend::allocator::Fourcc;
|
2025-01-04 09:09:24 +03:00
|
|
|
use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement};
|
2025-04-17 13:54:17 +02:00
|
|
|
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
2024-01-03 18:16:20 +04:00
|
|
|
use smithay::backend::drm::{
|
2024-12-14 13:15:14 +03:00
|
|
|
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport,
|
2024-01-03 18:16:20 +04:00
|
|
|
};
|
2023-09-18 20:09:57 +04:00
|
|
|
use smithay::backend::egl::context::ContextPriority;
|
2024-07-05 21:49:08 +02:00
|
|
|
use smithay::backend::egl::{EGLDevice, EGLDisplay};
|
2023-08-09 11:03:38 +04:00
|
|
|
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
2024-07-05 21:49:08 +02:00
|
|
|
use smithay::backend::renderer::gles::GlesRenderer;
|
2024-01-03 18:16:20 +04:00
|
|
|
use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend;
|
|
|
|
|
use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer};
|
2025-03-09 22:03:14 +03:00
|
|
|
use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, RendererSuper};
|
2023-08-09 11:03:38 +04:00
|
|
|
use smithay::backend::session::libseat::LibSeatSession;
|
|
|
|
|
use smithay::backend::session::{Event as SessionEvent, Session};
|
|
|
|
|
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
|
2023-08-16 10:59:34 +04:00
|
|
|
use smithay::desktop::utils::OutputPresentationFeedback;
|
2024-03-08 17:06:46 +04:00
|
|
|
use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties};
|
2024-01-01 09:40:41 +04:00
|
|
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
2023-08-21 11:52:52 +04:00
|
|
|
use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken};
|
2025-11-19 08:45:09 +03:00
|
|
|
use smithay::reexports::drm::control::atomic::AtomicModeReq;
|
|
|
|
|
use smithay::reexports::drm::control::dumbbuffer::DumbBuffer;
|
2023-08-14 15:53:24 +04:00
|
|
|
use smithay::reexports::drm::control::{
|
2025-11-19 08:45:09 +03:00
|
|
|
self, connector, crtc, plane, property, AtomicCommitFlags, Device, Mode as DrmMode, ModeFlags,
|
|
|
|
|
ModeTypeFlags, PlaneType, ResourceHandle,
|
2023-08-14 15:53:24 +04:00
|
|
|
};
|
2024-01-05 20:21:42 +04:00
|
|
|
use smithay::reexports::gbm::Modifier;
|
2023-08-09 11:03:38 +04:00
|
|
|
use smithay::reexports::input::Libinput;
|
2023-10-24 15:05:14 +04:00
|
|
|
use smithay::reexports::rustix::fs::OFlags;
|
2024-01-01 09:40:41 +04:00
|
|
|
use smithay::reexports::wayland_protocols;
|
2024-01-03 18:16:20 +04:00
|
|
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
2025-11-18 17:45:55 +03:00
|
|
|
use smithay::utils::{DeviceFd, Transform};
|
2024-01-01 09:40:41 +04:00
|
|
|
use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal};
|
2024-02-11 13:15:03 -05:00
|
|
|
use smithay::wayland::drm_lease::{
|
|
|
|
|
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
|
|
|
|
|
};
|
2024-12-13 10:28:25 +03:00
|
|
|
use smithay::wayland::presentation::Refresh;
|
2023-08-13 12:46:53 +04:00
|
|
|
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
|
2024-01-01 09:40:41 +04:00
|
|
|
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
|
|
|
|
|
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
use super::{IpcOutputMap, RenderResult};
|
2024-07-04 13:49:33 +04:00
|
|
|
use crate::backend::OutputId;
|
2024-01-16 18:01:45 +04:00
|
|
|
use crate::frame_clock::FrameClock;
|
2024-02-05 17:22:03 +04:00
|
|
|
use crate::niri::{Niri, RedrawState, State};
|
2024-05-03 10:25:51 +04:00
|
|
|
use crate::render_helpers::debug::draw_damage;
|
2024-02-06 11:24:50 +04:00
|
|
|
use crate::render_helpers::renderer::AsGlesRenderer;
|
2024-04-13 11:07:23 +04:00
|
|
|
use crate::render_helpers::{resources, shaders, RenderTarget};
|
2025-11-18 17:45:55 +03:00
|
|
|
use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output, PanelOrientation};
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-01-04 10:48:32 +03:00
|
|
|
const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [
|
|
|
|
|
Fourcc::Xrgb8888,
|
|
|
|
|
Fourcc::Xbgr8888,
|
|
|
|
|
Fourcc::Argb8888,
|
|
|
|
|
Fourcc::Abgr8888,
|
|
|
|
|
];
|
2023-08-09 11:03:38 +04:00
|
|
|
|
|
|
|
|
pub struct Tty {
|
2023-09-14 22:28:26 +04:00
|
|
|
config: Rc<RefCell<Config>>,
|
2023-08-09 11:03:38 +04:00
|
|
|
session: LibSeatSession,
|
2023-09-24 17:30:06 +04:00
|
|
|
udev_dispatcher: Dispatcher<'static, UdevBackend, State>,
|
2023-09-26 09:19:32 +04:00
|
|
|
libinput: Libinput,
|
2024-02-18 16:43:33 +04:00
|
|
|
gpu_manager: GpuManager<GbmGlesBackend<GlesRenderer, DrmDeviceFd>>,
|
2024-01-03 18:16:20 +04:00
|
|
|
// DRM node corresponding to the primary GPU. May or may not be the same as
|
|
|
|
|
// primary_render_node.
|
|
|
|
|
primary_node: DrmNode,
|
|
|
|
|
// DRM render node corresponding to the primary GPU.
|
|
|
|
|
primary_render_node: DrmNode,
|
2025-08-30 20:42:46 -04:00
|
|
|
// Ignored DRM nodes.
|
|
|
|
|
ignored_nodes: HashSet<DrmNode>,
|
2024-01-03 18:16:20 +04:00
|
|
|
// Devices indexed by DRM node (not necessarily the render node).
|
|
|
|
|
devices: HashMap<DrmNode, OutputDevice>,
|
2024-01-01 09:54:45 +04:00
|
|
|
// The dma-buf global corresponds to the output device (the primary GPU). It is only `Some()`
|
|
|
|
|
// if we have a device corresponding to the primary GPU.
|
|
|
|
|
dmabuf_global: Option<DmabufGlobal>,
|
2024-01-21 10:25:39 +04:00
|
|
|
// The output config had changed, but the session is paused, so we need to update it on resume.
|
|
|
|
|
update_output_config_on_resume: bool,
|
2025-11-17 08:53:37 +03:00
|
|
|
// The ignored nodes have changed, but the session is paused, so we need to update it on
|
|
|
|
|
// resume.
|
|
|
|
|
update_ignored_nodes_on_resume: bool,
|
2024-02-21 07:55:10 +04:00
|
|
|
// Whether the debug tinting is enabled.
|
|
|
|
|
debug_tint: bool,
|
2024-03-27 09:46:18 +04:00
|
|
|
ipc_outputs: Arc<Mutex<IpcOutputMap>>,
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
|
|
|
|
|
2024-02-18 16:43:33 +04:00
|
|
|
pub type TtyRenderer<'render> = MultiRenderer<
|
2024-01-03 18:16:20 +04:00
|
|
|
'render,
|
|
|
|
|
'render,
|
2024-02-18 16:43:33 +04:00
|
|
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
|
|
|
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
2024-01-03 18:16:20 +04:00
|
|
|
>;
|
|
|
|
|
|
2025-03-09 22:03:14 +03:00
|
|
|
pub type TtyFrame<'render, 'frame, 'buffer> = MultiFrame<
|
2024-01-03 18:16:20 +04:00
|
|
|
'render,
|
|
|
|
|
'render,
|
|
|
|
|
'frame,
|
2025-03-09 22:03:14 +03:00
|
|
|
'buffer,
|
2024-02-18 16:43:33 +04:00
|
|
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
|
|
|
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
2024-01-03 18:16:20 +04:00
|
|
|
>;
|
|
|
|
|
|
2025-03-09 22:03:14 +03:00
|
|
|
pub type TtyRendererError<'render> = <TtyRenderer<'render> as RendererSuper>::Error;
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2023-08-16 10:59:34 +04:00
|
|
|
type GbmDrmCompositor = DrmCompositor<
|
|
|
|
|
GbmAllocator<DrmDeviceFd>,
|
2025-04-17 13:54:17 +02:00
|
|
|
GbmFramebufferExporter<DrmDeviceFd>,
|
2023-09-20 11:51:25 +04:00
|
|
|
(OutputPresentationFeedback, Duration),
|
2023-08-16 10:59:34 +04:00
|
|
|
DrmDeviceFd,
|
|
|
|
|
>;
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2024-02-11 13:15:03 -05:00
|
|
|
pub struct OutputDevice {
|
2023-08-09 11:03:38 +04:00
|
|
|
token: RegistrationToken,
|
2025-11-20 12:19:43 +03:00
|
|
|
// Can be None for display-only devices such as DisplayLink.
|
|
|
|
|
render_node: Option<DrmNode>,
|
2023-08-13 12:46:53 +04:00
|
|
|
drm_scanner: DrmScanner,
|
2023-09-04 07:49:50 +04:00
|
|
|
surfaces: HashMap<crtc::Handle, Surface>,
|
2025-01-04 15:37:54 +03:00
|
|
|
known_crtcs: HashMap<crtc::Handle, CrtcInfo>,
|
2023-10-27 16:17:40 +04:00
|
|
|
// SAFETY: drop after all the objects used with them are dropped.
|
|
|
|
|
// See https://github.com/Smithay/smithay/issues/1102.
|
|
|
|
|
drm: DrmDevice,
|
|
|
|
|
gbm: GbmDevice<DrmDeviceFd>,
|
2025-11-20 12:19:43 +03:00
|
|
|
// For display-only devices this will be the allocator from the primary device.
|
2025-11-20 11:56:04 +03:00
|
|
|
allocator: GbmAllocator<DrmDeviceFd>,
|
2024-02-11 13:15:03 -05:00
|
|
|
|
2024-04-01 08:30:27 +04:00
|
|
|
pub drm_lease_state: Option<DrmLeaseState>,
|
2024-02-11 13:15:03 -05:00
|
|
|
non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>,
|
|
|
|
|
active_leases: Vec<DrmLease>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 15:37:54 +03:00
|
|
|
// A connected, but not necessarily enabled, crtc.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct CrtcInfo {
|
|
|
|
|
id: OutputId,
|
|
|
|
|
name: OutputName,
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-11 13:15:03 -05:00
|
|
|
impl OutputDevice {
|
|
|
|
|
pub fn lease_request(
|
|
|
|
|
&self,
|
|
|
|
|
request: DrmLeaseRequest,
|
|
|
|
|
) -> Result<DrmLeaseBuilder, LeaseRejected> {
|
|
|
|
|
let mut builder = DrmLeaseBuilder::new(&self.drm);
|
|
|
|
|
for connector in request.connectors {
|
|
|
|
|
let (_, crtc) = self
|
|
|
|
|
.non_desktop_connectors
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(conn, _)| connector == *conn)
|
|
|
|
|
.ok_or_else(|| {
|
|
|
|
|
warn!("Attempted to lease connector that is not non-desktop");
|
|
|
|
|
LeaseRejected::default()
|
|
|
|
|
})?;
|
|
|
|
|
builder.add_connector(connector);
|
|
|
|
|
builder.add_crtc(*crtc);
|
|
|
|
|
let planes = self.drm.planes(crtc).map_err(LeaseRejected::with_cause)?;
|
2024-07-05 21:49:08 +02:00
|
|
|
let (primary_plane, primary_plane_claim) = planes
|
|
|
|
|
.primary
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|plane| {
|
|
|
|
|
self.drm
|
|
|
|
|
.claim_plane(plane.handle, *crtc)
|
|
|
|
|
.map(|claim| (plane, claim))
|
|
|
|
|
})
|
|
|
|
|
.ok_or_else(LeaseRejected::default)?;
|
|
|
|
|
builder.add_plane(primary_plane.handle, primary_plane_claim);
|
2024-02-11 13:15:03 -05:00
|
|
|
}
|
|
|
|
|
Ok(builder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn new_lease(&mut self, lease: DrmLease) {
|
|
|
|
|
self.active_leases.push(lease);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_lease(&mut self, lease_id: u32) {
|
|
|
|
|
self.active_leases.retain(|l| l.id() != lease_id);
|
|
|
|
|
}
|
2025-01-04 15:37:54 +03:00
|
|
|
|
|
|
|
|
pub fn known_crtc_name(
|
|
|
|
|
&self,
|
|
|
|
|
crtc: &crtc::Handle,
|
|
|
|
|
conn: &connector::Info,
|
|
|
|
|
disable_monitor_names: bool,
|
|
|
|
|
) -> OutputName {
|
|
|
|
|
if disable_monitor_names {
|
|
|
|
|
let conn_name = format_connector_name(conn);
|
|
|
|
|
return OutputName {
|
|
|
|
|
connector: conn_name,
|
|
|
|
|
make: None,
|
|
|
|
|
model: None,
|
|
|
|
|
serial: None,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Some(info) = self.known_crtcs.get(crtc) else {
|
|
|
|
|
let conn_name = format_connector_name(conn);
|
|
|
|
|
error!("crtc for connector {conn_name} missing from known");
|
|
|
|
|
return OutputName {
|
|
|
|
|
connector: conn_name,
|
|
|
|
|
make: None,
|
|
|
|
|
model: None,
|
|
|
|
|
serial: None,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
info.name.clone()
|
|
|
|
|
}
|
2025-11-19 08:45:09 +03:00
|
|
|
|
2025-11-19 20:49:06 +03:00
|
|
|
fn cleanup_mismatching_resources(
|
2025-11-19 08:45:09 +03:00
|
|
|
&self,
|
|
|
|
|
should_be_off: &dyn Fn(crtc::Handle, &connector::Info) -> bool,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
let _span = tracy_client::span!("OutputDevice::cleanup_disconnected_resources");
|
|
|
|
|
|
2025-11-19 20:14:53 +03:00
|
|
|
let res_handles = self
|
|
|
|
|
.drm
|
|
|
|
|
.resource_handles()
|
|
|
|
|
.context("error getting plane handles")?;
|
2025-11-19 08:45:09 +03:00
|
|
|
let plane_handles = self
|
|
|
|
|
.drm
|
|
|
|
|
.plane_handles()
|
|
|
|
|
.context("error getting plane handles")?;
|
|
|
|
|
|
2025-11-19 20:14:53 +03:00
|
|
|
let mut req = AtomicModeReq::new();
|
2025-11-19 08:45:09 +03:00
|
|
|
|
2025-11-19 20:14:53 +03:00
|
|
|
// We want to disable all CRTCs that do not correspond to a connector we're using.
|
|
|
|
|
let mut cleanup = HashSet::<crtc::Handle>::new();
|
|
|
|
|
cleanup.extend(res_handles.crtcs());
|
2025-11-19 08:45:09 +03:00
|
|
|
|
2025-11-19 20:14:53 +03:00
|
|
|
for (conn, info) in self.drm_scanner.connectors() {
|
|
|
|
|
// We only keep the connector if it has a CRTC and the output isn't off in niri.
|
|
|
|
|
if let Some(crtc) = self.drm_scanner.crtc_for_connector(conn) {
|
2025-11-19 20:49:06 +03:00
|
|
|
// Verify that the connector's current CRTC matches the CRTC we expect. If not,
|
|
|
|
|
// clear the CRTC and the connector so that all connectors can get the expected
|
|
|
|
|
// CRTCs afterwards. (We do this because we do not handle CRTC rotations across TTY
|
|
|
|
|
// switches.)
|
|
|
|
|
let mut has_different_crtc = false;
|
|
|
|
|
if let Some(enc) = info.current_encoder() {
|
|
|
|
|
match self.drm.get_encoder(enc) {
|
|
|
|
|
Ok(enc) => {
|
|
|
|
|
if let Some(current_crtc) = enc.crtc() {
|
|
|
|
|
if current_crtc != crtc {
|
|
|
|
|
has_different_crtc = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
debug!("couldn't get encoder: {err:?}");
|
|
|
|
|
// Err on the safe side.
|
|
|
|
|
has_different_crtc = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !has_different_crtc && !should_be_off(crtc, info) {
|
2025-11-19 20:14:53 +03:00
|
|
|
// Keep the corresponding CRTC.
|
|
|
|
|
cleanup.remove(&crtc);
|
2025-11-19 08:45:09 +03:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear the connector.
|
|
|
|
|
let Some((crtc_id, _, _)) = find_drm_property(&self.drm, *conn, "CRTC_ID") else {
|
|
|
|
|
debug!("couldn't find connector CRTC_ID property");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
req.add_property(*conn, crtc_id, property::Value::CRTC(None));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Legacy fallback.
|
|
|
|
|
if !self.drm.is_atomic() {
|
2025-11-19 20:14:53 +03:00
|
|
|
for crtc in res_handles.crtcs() {
|
|
|
|
|
#[allow(deprecated)]
|
|
|
|
|
let _ = self.drm.set_cursor(*crtc, Option::<&DumbBuffer>::None);
|
2025-11-19 08:45:09 +03:00
|
|
|
}
|
|
|
|
|
for crtc in cleanup {
|
|
|
|
|
let _ = self.drm.set_crtc(crtc, None, (0, 0), &[], None);
|
|
|
|
|
}
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Disable non-primary planes, and planes belonging to disabled CRTCs.
|
|
|
|
|
let is_primary = |plane: plane::Handle| {
|
|
|
|
|
if let Some((_, info, value)) = find_drm_property(&self.drm, plane, "type") {
|
|
|
|
|
match info.value_type().convert_value(value) {
|
|
|
|
|
property::Value::Enum(Some(val)) => val.value() == PlaneType::Primary as u64,
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
debug!("couldn't find plane type property");
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for plane in plane_handles {
|
|
|
|
|
let info = match self.drm.get_plane(plane) {
|
|
|
|
|
Ok(x) => x,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
debug!("error getting plane: {err:?}");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some(crtc) = info.crtc() else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if !cleanup.contains(&crtc) && is_primary(plane) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let Some((crtc_id, _, _)) = find_drm_property(&self.drm, plane, "CRTC_ID") else {
|
|
|
|
|
debug!("couldn't find plane CRTC_ID property");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some((fb_id, _, _)) = find_drm_property(&self.drm, plane, "FB_ID") else {
|
|
|
|
|
debug!("couldn't find plane FB_ID property");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
req.add_property(plane, crtc_id, property::Value::CRTC(None));
|
|
|
|
|
req.add_property(plane, fb_id, property::Value::Framebuffer(None));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Disable the CRTCs.
|
|
|
|
|
for crtc in cleanup {
|
|
|
|
|
let Some((mode_id, _, _)) = find_drm_property(&self.drm, crtc, "MODE_ID") else {
|
|
|
|
|
debug!("couldn't find CRTC MODE_ID property");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some((active, _, _)) = find_drm_property(&self.drm, crtc, "ACTIVE") else {
|
|
|
|
|
debug!("couldn't find CRTC ACTIVE property");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
req.add_property(crtc, mode_id, property::Value::Unknown(0));
|
|
|
|
|
req.add_property(crtc, active, property::Value::Boolean(false));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.drm
|
|
|
|
|
.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)
|
|
|
|
|
.context("error doing atomic commit")?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2023-08-13 12:46:53 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
struct TtyOutputState {
|
2024-01-03 18:16:20 +04:00
|
|
|
node: DrmNode,
|
2023-08-13 12:46:53 +04:00
|
|
|
crtc: crtc::Handle,
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
|
|
|
|
|
2023-09-04 07:49:50 +04:00
|
|
|
struct Surface {
|
2024-09-03 12:13:04 +03:00
|
|
|
name: OutputName,
|
2023-09-04 07:49:50 +04:00
|
|
|
compositor: GbmDrmCompositor,
|
2024-08-22 18:58:07 +10:00
|
|
|
connector: connector::Handle,
|
2024-01-03 18:16:20 +04:00
|
|
|
dmabuf_feedback: Option<SurfaceDmabufFeedback>,
|
2024-03-15 08:54:36 +04:00
|
|
|
gamma_props: Option<GammaProps>,
|
2024-03-15 11:10:54 +04:00
|
|
|
/// Gamma change to apply upon session resume.
|
|
|
|
|
pending_gamma_change: Option<Option<Vec<u16>>>,
|
2023-09-04 08:57:11 +04:00
|
|
|
/// Tracy frame that goes from vblank to vblank.
|
|
|
|
|
vblank_frame: Option<tracy_client::Frame>,
|
2023-09-14 09:19:20 +04:00
|
|
|
/// Frame name for the VBlank frame.
|
2023-09-04 08:57:11 +04:00
|
|
|
vblank_frame_name: tracy_client::FrameName,
|
2023-11-24 09:19:49 +04:00
|
|
|
/// Plot name for the time since presentation plot.
|
|
|
|
|
time_since_presentation_plot_name: tracy_client::PlotName,
|
2023-11-24 09:15:25 +04:00
|
|
|
/// Plot name for the presentation misprediction plot.
|
|
|
|
|
presentation_misprediction_plot_name: tracy_client::PlotName,
|
2023-09-29 13:12:50 +04:00
|
|
|
sequence_delta_plot_name: tracy_client::PlotName,
|
2023-09-04 07:49:50 +04:00
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
pub struct SurfaceDmabufFeedback {
|
|
|
|
|
pub render: DmabufFeedback,
|
|
|
|
|
pub scanout: DmabufFeedback,
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-15 08:54:36 +04:00
|
|
|
struct GammaProps {
|
|
|
|
|
crtc: crtc::Handle,
|
|
|
|
|
gamma_lut: property::Handle,
|
|
|
|
|
gamma_lut_size: property::Handle,
|
|
|
|
|
previous_blob: Option<NonZeroU64>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
struct ConnectorProperties<'a> {
|
|
|
|
|
device: &'a DrmDevice,
|
|
|
|
|
connector: connector::Handle,
|
|
|
|
|
properties: Vec<(property::Info, property::RawValue)>,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 11:03:38 +04:00
|
|
|
impl Tty {
|
2024-02-01 16:55:46 +04:00
|
|
|
pub fn new(
|
|
|
|
|
config: Rc<RefCell<Config>>,
|
|
|
|
|
event_loop: LoopHandle<'static, State>,
|
|
|
|
|
) -> anyhow::Result<Self> {
|
2025-11-18 21:25:35 +03:00
|
|
|
let _span = tracy_client::span!("Tty::new");
|
|
|
|
|
|
2024-02-01 16:55:46 +04:00
|
|
|
let (session, notifier) = LibSeatSession::new().context(
|
|
|
|
|
"Error creating a session. This might mean that you're trying to run niri on a TTY \
|
|
|
|
|
that is already busy, for example if you're running this inside tmux that had been \
|
|
|
|
|
originally started on a different TTY",
|
|
|
|
|
)?;
|
2023-08-09 11:03:38 +04:00
|
|
|
let seat_name = session.seat();
|
|
|
|
|
|
2024-02-01 16:55:46 +04:00
|
|
|
let udev_backend =
|
|
|
|
|
UdevBackend::new(session.seat()).context("error creating a udev backend")?;
|
2023-09-24 17:30:06 +04:00
|
|
|
let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| {
|
2023-09-26 09:19:32 +04:00
|
|
|
state.backend.tty().on_udev_event(&mut state.niri, event);
|
2023-09-24 17:30:06 +04:00
|
|
|
});
|
2023-08-21 11:52:52 +04:00
|
|
|
event_loop
|
|
|
|
|
.register_dispatcher(udev_dispatcher.clone())
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
2023-08-09 11:03:38 +04:00
|
|
|
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
2025-12-29 16:14:52 +03:00
|
|
|
unsafe { init_libinput_plugin_system(&libinput) };
|
2025-11-18 21:26:41 +03:00
|
|
|
{
|
|
|
|
|
let _span = tracy_client::span!("Libinput::udev_assign_seat");
|
|
|
|
|
libinput.udev_assign_seat(&seat_name)
|
|
|
|
|
}
|
|
|
|
|
.map_err(|()| anyhow!("error assigning the seat to libinput"))?;
|
2023-08-09 11:03:38 +04:00
|
|
|
|
|
|
|
|
let input_backend = LibinputInputBackend::new(libinput.clone());
|
|
|
|
|
event_loop
|
2023-09-24 17:30:06 +04:00
|
|
|
.insert_source(input_backend, |mut event, _, state| {
|
|
|
|
|
state.process_libinput_event(&mut event);
|
|
|
|
|
state.process_input_event(event);
|
2023-08-09 11:03:38 +04:00
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
event_loop
|
2023-09-24 17:30:06 +04:00
|
|
|
.insert_source(notifier, move |event, _, state| {
|
2023-09-26 09:19:32 +04:00
|
|
|
state.backend.tty().on_session_event(&mut state.niri, event);
|
2023-08-09 11:03:38 +04:00
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
2024-07-05 21:49:08 +02:00
|
|
|
let api = GbmGlesBackend::with_context_priority(ContextPriority::High);
|
2024-02-01 16:55:46 +04:00
|
|
|
let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?;
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2024-01-06 09:14:48 +04:00
|
|
|
let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow())
|
2024-02-01 16:55:46 +04:00
|
|
|
.ok_or(())
|
|
|
|
|
.or_else(|()| {
|
|
|
|
|
let primary_gpu_path = udev::primary_gpu(&seat_name)
|
|
|
|
|
.context("error getting the primary GPU")?
|
|
|
|
|
.context("couldn't find a GPU")?;
|
|
|
|
|
let primary_node = DrmNode::from_path(primary_gpu_path)
|
|
|
|
|
.context("error opening the primary GPU DRM node")?;
|
2024-01-06 09:14:48 +04:00
|
|
|
let primary_render_node = primary_node
|
|
|
|
|
.node_with_type(NodeType::Render)
|
2024-03-29 09:01:54 +04:00
|
|
|
.and_then(Result::ok)
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
warn!(
|
|
|
|
|
"error getting the render node for the primary GPU; proceeding anyway"
|
|
|
|
|
);
|
|
|
|
|
primary_node
|
|
|
|
|
});
|
2024-02-01 16:55:46 +04:00
|
|
|
|
|
|
|
|
Ok::<_, anyhow::Error>((primary_node, primary_render_node))
|
|
|
|
|
})?;
|
2024-01-06 08:46:30 +04:00
|
|
|
|
|
|
|
|
let mut node_path = String::new();
|
|
|
|
|
if let Some(path) = primary_render_node.dev_path() {
|
2025-07-13 12:49:00 +03:00
|
|
|
write!(node_path, "{path:?}").unwrap();
|
2024-01-06 08:46:30 +04:00
|
|
|
} else {
|
2025-07-13 12:49:00 +03:00
|
|
|
write!(node_path, "{primary_render_node}").unwrap();
|
2024-01-06 08:46:30 +04:00
|
|
|
}
|
2025-07-13 12:49:00 +03:00
|
|
|
info!("using as the render node: {node_path}");
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-08-30 20:42:46 -04:00
|
|
|
let mut ignored_nodes = ignored_nodes_from_config(&config.borrow());
|
|
|
|
|
if ignored_nodes.remove(&primary_node) || ignored_nodes.remove(&primary_render_node) {
|
|
|
|
|
warn!("ignoring the primary node or render node is not allowed");
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-01 16:55:46 +04:00
|
|
|
Ok(Self {
|
2023-09-14 22:28:26 +04:00
|
|
|
config,
|
2023-08-09 11:03:38 +04:00
|
|
|
session,
|
2023-08-21 11:52:52 +04:00
|
|
|
udev_dispatcher,
|
2023-09-26 09:19:32 +04:00
|
|
|
libinput,
|
2024-01-03 18:16:20 +04:00
|
|
|
gpu_manager,
|
|
|
|
|
primary_node,
|
|
|
|
|
primary_render_node,
|
2025-08-30 20:42:46 -04:00
|
|
|
ignored_nodes,
|
2024-01-03 18:16:20 +04:00
|
|
|
devices: HashMap::new(),
|
2024-01-01 09:54:45 +04:00
|
|
|
dmabuf_global: None,
|
2024-01-21 10:25:39 +04:00
|
|
|
update_output_config_on_resume: false,
|
2025-11-17 08:53:37 +03:00
|
|
|
update_ignored_nodes_on_resume: false,
|
2024-02-21 07:55:10 +04:00
|
|
|
debug_tint: false,
|
2024-03-27 08:27:14 +04:00
|
|
|
ipc_outputs: Arc::new(Mutex::new(HashMap::new())),
|
2024-02-01 16:55:46 +04:00
|
|
|
})
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn init(&mut self, niri: &mut Niri) {
|
2025-11-20 11:17:31 +03:00
|
|
|
let udev = self.udev_dispatcher.clone();
|
|
|
|
|
let udev = udev.as_source_ref();
|
|
|
|
|
|
|
|
|
|
// Initialize the primary node first as later nodes might depend on the primary render node
|
|
|
|
|
// being available.
|
|
|
|
|
if let Some((primary_device_id, primary_device_path)) = udev
|
|
|
|
|
.device_list()
|
|
|
|
|
.find(|&(device_id, _)| device_id == self.primary_node.dev_id())
|
|
|
|
|
{
|
|
|
|
|
if let Err(err) = self.device_added(primary_device_id, primary_device_path, niri) {
|
|
|
|
|
warn!(
|
|
|
|
|
"error adding primary node device, display-only devices may not work: {err:?}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!("primary node is missing, display-only devices may not work");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (device_id, path) in udev.device_list() {
|
|
|
|
|
if device_id == self.primary_node.dev_id() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
if let Err(err) = self.device_added(device_id, path, niri) {
|
2023-08-09 11:03:38 +04:00
|
|
|
warn!("error adding device: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-26 09:19:32 +04:00
|
|
|
fn on_udev_event(&mut self, niri: &mut Niri, event: UdevEvent) {
|
2023-09-26 09:23:30 +04:00
|
|
|
let _span = tracy_client::span!("Tty::on_udev_event");
|
|
|
|
|
|
2023-09-26 09:19:32 +04:00
|
|
|
match event {
|
|
|
|
|
UdevEvent::Added { device_id, path } => {
|
|
|
|
|
if !self.session.is_active() {
|
|
|
|
|
debug!("skipping UdevEvent::Added as session is inactive");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Err(err) = self.device_added(device_id, &path, niri) {
|
|
|
|
|
warn!("error adding device: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
UdevEvent::Changed { device_id } => {
|
|
|
|
|
if !self.session.is_active() {
|
|
|
|
|
debug!("skipping UdevEvent::Changed as session is inactive");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 20:49:06 +03:00
|
|
|
self.device_changed(device_id, niri, false)
|
2023-09-26 09:19:32 +04:00
|
|
|
}
|
|
|
|
|
UdevEvent::Removed { device_id } => {
|
|
|
|
|
if !self.session.is_active() {
|
|
|
|
|
debug!("skipping UdevEvent::Removed as session is inactive");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.device_removed(device_id, niri)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_session_event(&mut self, niri: &mut Niri, event: SessionEvent) {
|
2023-09-26 09:23:30 +04:00
|
|
|
let _span = tracy_client::span!("Tty::on_session_event");
|
|
|
|
|
|
2023-09-26 09:19:32 +04:00
|
|
|
match event {
|
|
|
|
|
SessionEvent::PauseSession => {
|
|
|
|
|
debug!("pausing session");
|
|
|
|
|
|
|
|
|
|
self.libinput.suspend();
|
|
|
|
|
|
2024-01-22 16:08:51 +04:00
|
|
|
for device in self.devices.values_mut() {
|
2024-01-03 18:16:20 +04:00
|
|
|
device.drm.pause();
|
2024-04-01 08:30:27 +04:00
|
|
|
|
|
|
|
|
if let Some(lease_state) = &mut device.drm_lease_state {
|
|
|
|
|
lease_state.suspend();
|
|
|
|
|
}
|
2023-09-26 09:19:32 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
SessionEvent::ActivateSession => {
|
|
|
|
|
debug!("resuming session");
|
|
|
|
|
|
|
|
|
|
if self.libinput.resume().is_err() {
|
2024-02-21 09:20:34 +04:00
|
|
|
warn!("error resuming libinput");
|
2023-09-26 09:19:32 +04:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 08:53:37 +03:00
|
|
|
if self.update_ignored_nodes_on_resume {
|
|
|
|
|
self.update_ignored_nodes_on_resume = false;
|
|
|
|
|
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
|
|
|
|
if ignored_nodes.remove(&self.primary_node)
|
|
|
|
|
|| ignored_nodes.remove(&self.primary_render_node)
|
|
|
|
|
{
|
|
|
|
|
warn!("ignoring the primary node or render node is not allowed");
|
|
|
|
|
}
|
|
|
|
|
self.ignored_nodes = ignored_nodes;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let mut device_list = self
|
|
|
|
|
.udev_dispatcher
|
|
|
|
|
.as_source_ref()
|
|
|
|
|
.device_list()
|
|
|
|
|
.map(|(device_id, path)| (device_id, path.to_owned()))
|
|
|
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
|
|
|
|
|
|
let removed_devices = self
|
|
|
|
|
.devices
|
|
|
|
|
.keys()
|
2025-11-17 08:53:37 +03:00
|
|
|
.filter(|node| {
|
|
|
|
|
!device_list.contains_key(&node.dev_id())
|
|
|
|
|
|| self.ignored_nodes.contains(node)
|
|
|
|
|
})
|
2024-01-03 18:16:20 +04:00
|
|
|
.copied()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
let remained_devices = self
|
|
|
|
|
.devices
|
|
|
|
|
.keys()
|
2025-11-17 08:53:37 +03:00
|
|
|
.filter(|node| {
|
|
|
|
|
device_list.contains_key(&node.dev_id())
|
|
|
|
|
&& !self.ignored_nodes.contains(node)
|
|
|
|
|
})
|
2024-01-03 18:16:20 +04:00
|
|
|
.copied()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
// Remove removed devices.
|
|
|
|
|
for node in removed_devices {
|
|
|
|
|
device_list.remove(&node.dev_id());
|
|
|
|
|
self.device_removed(node.dev_id(), niri);
|
|
|
|
|
}
|
2023-09-26 09:19:32 +04:00
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
// Update remained devices.
|
|
|
|
|
for node in remained_devices {
|
|
|
|
|
device_list.remove(&node.dev_id());
|
|
|
|
|
|
|
|
|
|
// It hasn't been removed, update its state as usual.
|
|
|
|
|
let device = self.devices.get_mut(&node).unwrap();
|
2025-12-18 07:37:49 +03:00
|
|
|
|
|
|
|
|
// Someone on an old device hit what seems to be a driver bug without this:
|
|
|
|
|
// https://github.com/YaLTeR/niri/issues/3048
|
|
|
|
|
let force_disable = self
|
|
|
|
|
.config
|
|
|
|
|
.borrow()
|
|
|
|
|
.debug
|
|
|
|
|
.force_disable_connectors_on_resume;
|
|
|
|
|
|
|
|
|
|
if let Err(err) = device.drm.activate(force_disable) {
|
2024-01-22 16:08:51 +04:00
|
|
|
warn!("error activating DRM device: {err:?}");
|
2024-01-03 18:16:20 +04:00
|
|
|
}
|
2024-04-01 08:30:27 +04:00
|
|
|
if let Some(lease_state) = &mut device.drm_lease_state {
|
|
|
|
|
lease_state.resume::<State>();
|
|
|
|
|
}
|
2023-09-26 09:19:32 +04:00
|
|
|
|
2024-01-22 16:08:51 +04:00
|
|
|
// Refresh the connectors.
|
2025-11-19 20:49:06 +03:00
|
|
|
self.device_changed(node.dev_id(), niri, true);
|
2024-03-15 11:10:54 +04:00
|
|
|
|
2024-03-15 22:02:29 +04:00
|
|
|
// Apply pending gamma changes and restore our existing gamma.
|
2024-03-15 11:10:54 +04:00
|
|
|
let device = self.devices.get_mut(&node).unwrap();
|
|
|
|
|
for (crtc, surface) in device.surfaces.iter_mut() {
|
2025-10-29 09:29:33 +03:00
|
|
|
if let Ok(props) =
|
|
|
|
|
ConnectorProperties::try_new(&device.drm, surface.connector)
|
|
|
|
|
{
|
|
|
|
|
match reset_hdr(&props) {
|
|
|
|
|
Ok(()) => (),
|
|
|
|
|
Err(err) => debug!("couldn't reset HDR properties: {err:?}"),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!("failed to get connector properties");
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-15 11:10:54 +04:00
|
|
|
if let Some(ramp) = surface.pending_gamma_change.take() {
|
|
|
|
|
let ramp = ramp.as_deref();
|
|
|
|
|
let res = if let Some(gamma_props) = &mut surface.gamma_props {
|
|
|
|
|
gamma_props.set_gamma(&device.drm, ramp)
|
|
|
|
|
} else {
|
|
|
|
|
set_gamma_for_crtc(&device.drm, *crtc, ramp)
|
|
|
|
|
};
|
|
|
|
|
if let Err(err) = res {
|
|
|
|
|
warn!("error applying pending gamma change: {err:?}");
|
|
|
|
|
}
|
2024-03-15 22:02:29 +04:00
|
|
|
} else if let Some(gamma_props) = &surface.gamma_props {
|
|
|
|
|
if let Err(err) = gamma_props.restore_gamma(&device.drm) {
|
|
|
|
|
warn!("error restoring gamma: {err:?}");
|
|
|
|
|
}
|
2024-03-15 11:10:54 +04:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-03 18:16:20 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add new devices.
|
|
|
|
|
for (device_id, path) in device_list.into_iter() {
|
|
|
|
|
if let Err(err) = self.device_added(device_id, &path, niri) {
|
|
|
|
|
warn!("error adding device: {err:?}");
|
2023-09-26 09:19:32 +04:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-17 10:38:32 +04:00
|
|
|
|
2024-01-21 10:25:39 +04:00
|
|
|
if self.update_output_config_on_resume {
|
|
|
|
|
self.on_output_config_changed(niri);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
self.refresh_ipc_outputs(niri);
|
2024-01-22 16:08:51 +04:00
|
|
|
|
2025-02-17 09:09:48 +03:00
|
|
|
niri.notify_activity();
|
2024-02-12 08:45:45 +04:00
|
|
|
niri.monitors_active = true;
|
|
|
|
|
self.set_monitors_active(true);
|
2024-01-22 16:08:51 +04:00
|
|
|
niri.queue_redraw_all();
|
2023-09-26 09:19:32 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 11:03:38 +04:00
|
|
|
fn device_added(
|
|
|
|
|
&mut self,
|
|
|
|
|
device_id: dev_t,
|
2023-08-13 12:46:53 +04:00
|
|
|
path: &Path,
|
2023-08-09 11:03:38 +04:00
|
|
|
niri: &mut Niri,
|
|
|
|
|
) -> anyhow::Result<()> {
|
2025-10-17 17:33:23 +03:00
|
|
|
debug!("adding device: {device_id} {path:?}");
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let node = DrmNode::from_dev_id(device_id)?;
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-11-20 11:40:09 +03:00
|
|
|
if node == self.primary_node {
|
|
|
|
|
debug!("this is the primary node");
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 12:09:51 +00:00
|
|
|
// Only consider primary node on udev event
|
|
|
|
|
// https://gitlab.freedesktop.org/wlroots/wlroots/-/commit/768fbaad54027f8dd027e7e015e8eeb93cb38c52
|
|
|
|
|
if node.ty() != NodeType::Primary {
|
|
|
|
|
debug!("not a primary node, skipping");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-30 20:42:46 -04:00
|
|
|
if self.ignored_nodes.contains(&node) {
|
|
|
|
|
debug!("node is ignored, skipping");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 21:25:35 +03:00
|
|
|
let _span = tracy_client::span!("Tty::device_added");
|
|
|
|
|
|
2023-10-24 15:05:14 +04:00
|
|
|
let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK;
|
2025-11-19 08:53:33 +03:00
|
|
|
let fd = {
|
|
|
|
|
let _span = tracy_client::span!("LibSeatSession::open");
|
|
|
|
|
self.session.open(path, open_flags)
|
|
|
|
|
}?;
|
2023-10-24 15:05:14 +04:00
|
|
|
let device_fd = DrmDeviceFd::new(DeviceFd::from(fd));
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-11-18 21:26:41 +03:00
|
|
|
let (drm, drm_notifier) = {
|
|
|
|
|
let _span = tracy_client::span!("DrmDevice::new");
|
2025-11-19 08:45:09 +03:00
|
|
|
DrmDevice::new(device_fd.clone(), false)
|
2025-11-18 21:26:41 +03:00
|
|
|
}?;
|
|
|
|
|
let gbm = {
|
|
|
|
|
let _span = tracy_client::span!("GbmDevice::new");
|
|
|
|
|
GbmDevice::new(device_fd)
|
|
|
|
|
}?;
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-11-20 12:19:43 +03:00
|
|
|
let mut try_initialize_gpu = || {
|
|
|
|
|
let display = unsafe { EGLDisplay::new(gbm.clone())? };
|
|
|
|
|
let egl_device = EGLDevice::device_for_display(&display)?;
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2025-11-20 12:19:43 +03:00
|
|
|
// Software EGL devices (e.g., llvmpipe/softpipe) are rejected for now. They have some
|
|
|
|
|
// problems (segfault on importing dmabufs from other renderers) and need to be
|
|
|
|
|
// excluded from some places like DRM leasing.
|
|
|
|
|
ensure!(
|
|
|
|
|
!egl_device.is_software(),
|
|
|
|
|
"software EGL renderers are skipped"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let render_node = egl_device
|
|
|
|
|
.try_get_render_node()
|
|
|
|
|
.ok()
|
|
|
|
|
.flatten()
|
|
|
|
|
.unwrap_or(node);
|
|
|
|
|
self.gpu_manager
|
|
|
|
|
.as_mut()
|
|
|
|
|
.add_node(render_node, gbm.clone())
|
|
|
|
|
.context("error adding render node to GPU manager")?;
|
|
|
|
|
|
|
|
|
|
Ok(render_node)
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-25 22:16:44 +08:00
|
|
|
let render_node = match try_initialize_gpu() {
|
|
|
|
|
Ok(render_node) => {
|
|
|
|
|
debug!("got render node: {render_node}");
|
|
|
|
|
Some(render_node)
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2025-11-20 12:19:43 +03:00
|
|
|
debug!("failed to initialize renderer, falling back to primary gpu: {err:?}");
|
2025-11-25 22:16:44 +08:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2025-11-25 22:16:44 +08:00
|
|
|
if render_node == Some(self.primary_render_node) && self.dmabuf_global.is_none() {
|
2025-11-20 12:19:43 +03:00
|
|
|
let render_node = self.primary_render_node;
|
2025-11-25 22:16:44 +08:00
|
|
|
debug!("initializing the primary renderer");
|
2024-01-03 18:16:20 +04:00
|
|
|
|
|
|
|
|
let mut renderer = self
|
|
|
|
|
.gpu_manager
|
|
|
|
|
.single_renderer(&render_node)
|
|
|
|
|
.context("error creating renderer")?;
|
|
|
|
|
|
2024-03-14 18:08:34 +04:00
|
|
|
if let Err(err) = renderer.bind_wl_display(&niri.display_handle) {
|
|
|
|
|
warn!("error binding wl-display in EGL: {err:?}");
|
|
|
|
|
}
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2024-04-21 20:10:35 +04:00
|
|
|
let gles_renderer = renderer.as_gles_renderer();
|
|
|
|
|
resources::init(gles_renderer);
|
|
|
|
|
shaders::init(gles_renderer);
|
|
|
|
|
|
|
|
|
|
let config = self.config.borrow();
|
|
|
|
|
if let Some(src) = config.animations.window_resize.custom_shader.as_deref() {
|
|
|
|
|
shaders::set_custom_resize_program(gles_renderer, Some(src));
|
|
|
|
|
}
|
2024-05-12 09:52:21 +04:00
|
|
|
if let Some(src) = config.animations.window_close.custom_shader.as_deref() {
|
|
|
|
|
shaders::set_custom_close_program(gles_renderer, Some(src));
|
|
|
|
|
}
|
2024-05-15 19:38:29 +04:00
|
|
|
if let Some(src) = config.animations.window_open.custom_shader.as_deref() {
|
|
|
|
|
shaders::set_custom_open_program(gles_renderer, Some(src));
|
|
|
|
|
}
|
2024-04-21 20:10:35 +04:00
|
|
|
drop(config);
|
2024-02-21 21:27:44 +04:00
|
|
|
|
2025-01-21 09:40:00 +03:00
|
|
|
niri.update_shaders();
|
2024-05-03 21:21:58 +04:00
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
// Create the dmabuf global.
|
2024-06-21 07:53:41 +03:00
|
|
|
let primary_formats = renderer.dmabuf_formats();
|
2024-01-03 18:16:20 +04:00
|
|
|
let default_feedback =
|
|
|
|
|
DmabufFeedbackBuilder::new(render_node.dev_id(), primary_formats.clone())
|
|
|
|
|
.build()
|
|
|
|
|
.context("error building default dmabuf feedback")?;
|
|
|
|
|
let dmabuf_global = niri
|
|
|
|
|
.dmabuf_state
|
|
|
|
|
.create_global_with_default_feedback::<State>(
|
|
|
|
|
&niri.display_handle,
|
|
|
|
|
&default_feedback,
|
|
|
|
|
);
|
|
|
|
|
assert!(self.dmabuf_global.replace(dmabuf_global).is_none());
|
|
|
|
|
|
|
|
|
|
// Update the dmabuf feedbacks for all surfaces.
|
2025-11-20 12:19:43 +03:00
|
|
|
for (node, device) in self.devices.iter_mut() {
|
2024-01-03 18:16:20 +04:00
|
|
|
for surface in device.surfaces.values_mut() {
|
|
|
|
|
match surface_dmabuf_feedback(
|
|
|
|
|
&surface.compositor,
|
|
|
|
|
primary_formats.clone(),
|
|
|
|
|
self.primary_render_node,
|
|
|
|
|
device.render_node,
|
2025-11-20 12:19:43 +03:00
|
|
|
*node,
|
2024-01-03 18:16:20 +04:00
|
|
|
) {
|
|
|
|
|
Ok(feedback) => {
|
|
|
|
|
surface.dmabuf_feedback = Some(feedback);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error building dmabuf feedback: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-11-20 12:19:43 +03:00
|
|
|
let allocator_gbm = if render_node.is_some() {
|
|
|
|
|
gbm.clone()
|
|
|
|
|
} else if let Some(primary_device) = self.devices.get(&self.primary_node) {
|
|
|
|
|
primary_device.gbm.clone()
|
|
|
|
|
} else {
|
|
|
|
|
bail!("no allocator available for device");
|
|
|
|
|
};
|
2025-11-20 11:56:04 +03:00
|
|
|
let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT;
|
2025-11-20 12:19:43 +03:00
|
|
|
let allocator = GbmAllocator::new(allocator_gbm, gbm_flags);
|
2025-11-20 11:56:04 +03:00
|
|
|
|
2023-08-09 11:03:38 +04:00
|
|
|
let token = niri
|
|
|
|
|
.event_loop
|
2023-09-26 08:39:38 +04:00
|
|
|
.insert_source(drm_notifier, move |event, meta, state| {
|
2023-09-24 17:30:06 +04:00
|
|
|
let tty = state.backend.tty();
|
2023-08-09 11:03:38 +04:00
|
|
|
match event {
|
2023-08-13 12:46:53 +04:00
|
|
|
DrmEvent::VBlank(crtc) => {
|
2023-09-26 13:11:00 +04:00
|
|
|
let meta = meta.expect("VBlank events must have metadata");
|
2024-01-03 18:16:20 +04:00
|
|
|
tty.on_vblank(&mut state.niri, node, crtc, meta);
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
2024-02-21 09:20:34 +04:00
|
|
|
DrmEvent::Error(error) => warn!("DRM error: {error}"),
|
2023-08-09 11:03:38 +04:00
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
2024-04-01 08:30:27 +04:00
|
|
|
let drm_lease_state = DrmLeaseState::new::<State>(&niri.display_handle, &node)
|
|
|
|
|
.map_err(|err| warn!("error initializing DRM leasing for {node}: {err:?}"))
|
|
|
|
|
.ok();
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let device = OutputDevice {
|
2023-08-09 11:03:38 +04:00
|
|
|
token,
|
2024-01-03 18:16:20 +04:00
|
|
|
render_node,
|
2023-08-09 11:03:38 +04:00
|
|
|
drm,
|
2023-08-13 12:46:53 +04:00
|
|
|
gbm,
|
2025-11-20 11:56:04 +03:00
|
|
|
allocator,
|
2023-08-13 12:46:53 +04:00
|
|
|
drm_scanner: DrmScanner::new(),
|
|
|
|
|
surfaces: HashMap::new(),
|
2025-01-04 15:37:54 +03:00
|
|
|
known_crtcs: HashMap::new(),
|
2024-02-11 13:15:03 -05:00
|
|
|
drm_lease_state,
|
|
|
|
|
active_leases: Vec::new(),
|
|
|
|
|
non_desktop_connectors: HashSet::new(),
|
2024-01-03 18:16:20 +04:00
|
|
|
};
|
|
|
|
|
assert!(self.devices.insert(node, device).is_none());
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-11-19 20:49:06 +03:00
|
|
|
self.device_changed(device_id, niri, true);
|
2023-08-13 12:46:53 +04:00
|
|
|
|
2023-08-09 11:03:38 +04:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 20:49:06 +03:00
|
|
|
fn device_changed(&mut self, device_id: dev_t, niri: &mut Niri, cleanup: bool) {
|
2024-01-03 18:16:20 +04:00
|
|
|
debug!("device changed: {device_id}");
|
|
|
|
|
|
|
|
|
|
let Ok(node) = DrmNode::from_dev_id(device_id) else {
|
|
|
|
|
warn!("error creating DrmNode");
|
2023-08-13 12:46:53 +04:00
|
|
|
return;
|
|
|
|
|
};
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2025-10-19 12:09:51 +00:00
|
|
|
if node.ty() != NodeType::Primary {
|
|
|
|
|
debug!("not a primary node, skipping");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-17 17:46:28 +03:00
|
|
|
if self.ignored_nodes.contains(&node) {
|
|
|
|
|
debug!("node is ignored, skipping");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let Some(device) = self.devices.get_mut(&node) else {
|
2025-10-17 17:46:28 +03:00
|
|
|
if let Some(path) = node.dev_path() {
|
|
|
|
|
warn!("unknown device; trying to add");
|
|
|
|
|
|
|
|
|
|
if let Err(err) = self.device_added(device_id, &path, niri) {
|
|
|
|
|
warn!("error adding device: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!("unknown device");
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
return;
|
2024-01-03 18:16:20 +04:00
|
|
|
};
|
2023-08-13 12:46:53 +04:00
|
|
|
|
2025-11-19 08:45:09 +03:00
|
|
|
// DrmScanner will preserve any existing connector-CRTC mapping.
|
2024-07-27 09:28:40 +03:00
|
|
|
let scan_result = match device.drm_scanner.scan_connectors(&device.drm) {
|
|
|
|
|
Ok(x) => x,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error scanning connectors: {err:?}");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-04 23:24:09 +03:00
|
|
|
let mut added = Vec::new();
|
2024-07-04 13:49:33 +04:00
|
|
|
let mut removed = Vec::new();
|
2024-07-27 09:28:40 +03:00
|
|
|
for event in scan_result {
|
2023-08-13 12:46:53 +04:00
|
|
|
match event {
|
|
|
|
|
DrmScanEvent::Connected {
|
|
|
|
|
connector,
|
|
|
|
|
crtc: Some(crtc),
|
|
|
|
|
} => {
|
2024-11-08 09:11:56 +03:00
|
|
|
let connector_name = format_connector_name(&connector);
|
2025-01-04 23:24:09 +03:00
|
|
|
let name = make_output_name(&device.drm, connector.handle(), connector_name);
|
2024-11-08 09:11:56 +03:00
|
|
|
debug!(
|
|
|
|
|
"new connector: {} \"{}\"",
|
2025-01-04 15:37:54 +03:00
|
|
|
&name.connector,
|
|
|
|
|
name.format_make_model_serial(),
|
2024-11-08 09:11:56 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Assign an id to this crtc.
|
2025-01-04 15:37:54 +03:00
|
|
|
let id = OutputId::next();
|
2025-01-04 23:24:09 +03:00
|
|
|
added.push((crtc, CrtcInfo { id, name }));
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
2023-08-13 12:46:53 +04:00
|
|
|
DrmScanEvent::Disconnected {
|
2024-01-16 15:33:37 +04:00
|
|
|
crtc: Some(crtc), ..
|
2024-07-04 13:49:33 +04:00
|
|
|
} => {
|
|
|
|
|
removed.push(crtc);
|
|
|
|
|
}
|
2023-08-13 12:46:53 +04:00
|
|
|
_ => (),
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-17 10:38:32 +04:00
|
|
|
|
2024-11-08 09:11:56 +03:00
|
|
|
for crtc in &removed {
|
|
|
|
|
self.connector_disconnected(niri, node, *crtc);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-04 13:49:33 +04:00
|
|
|
let Some(device) = self.devices.get_mut(&node) else {
|
|
|
|
|
error!("device disappeared");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for crtc in removed {
|
2025-01-04 15:37:54 +03:00
|
|
|
if device.known_crtcs.remove(&crtc).is_none() {
|
2024-07-04 13:49:33 +04:00
|
|
|
error!("output ID missing for disconnected crtc: {crtc:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 23:24:09 +03:00
|
|
|
for (crtc, mut info) in added {
|
|
|
|
|
// Make/model/serial can match exactly between different physical monitors. This doesn't
|
|
|
|
|
// happen often, but our Layout does not support such duplicates and will panic.
|
|
|
|
|
//
|
|
|
|
|
// As a workaround, search for duplicates, and unname the new connectors if one is
|
|
|
|
|
// found. Connector names are always unique.
|
|
|
|
|
let name = &mut info.name;
|
|
|
|
|
let formatted = name.format_make_model_serial_or_connector();
|
|
|
|
|
for info in self.devices.values().flat_map(|d| d.known_crtcs.values()) {
|
|
|
|
|
if info.name.matches(&formatted) {
|
|
|
|
|
let connector = mem::take(&mut name.connector);
|
|
|
|
|
warn!(
|
|
|
|
|
"new connector {connector} duplicates make/model/serial \
|
|
|
|
|
of existing connector {}, unnaming",
|
|
|
|
|
info.name.connector,
|
|
|
|
|
);
|
|
|
|
|
*name = OutputName {
|
|
|
|
|
connector,
|
|
|
|
|
make: None,
|
|
|
|
|
model: None,
|
|
|
|
|
serial: None,
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Insert it right away so next added connector will check against this one too.
|
|
|
|
|
let device = self.devices.get_mut(&node).unwrap();
|
|
|
|
|
device.known_crtcs.insert(crtc, info);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-19 20:49:06 +03:00
|
|
|
// If the device was just added or resumed, we need to cleanup any disconnected connectors
|
|
|
|
|
// and planes.
|
|
|
|
|
if cleanup {
|
2025-11-19 08:45:09 +03:00
|
|
|
let device = self.devices.get(&node).unwrap();
|
|
|
|
|
|
|
|
|
|
// Follow the logic in on_output_config_changed().
|
|
|
|
|
let disable_laptop_panels = self.should_disable_laptop_panels(niri.is_lid_closed);
|
|
|
|
|
let should_disable = |conn: &str| disable_laptop_panels && is_laptop_panel(conn);
|
|
|
|
|
|
|
|
|
|
let config = self.config.borrow();
|
|
|
|
|
let disable_monitor_names = config.debug.disable_monitor_names;
|
|
|
|
|
|
|
|
|
|
let should_be_off = |crtc, conn: &connector::Info| {
|
|
|
|
|
let output_name = device.known_crtc_name(&crtc, conn, disable_monitor_names);
|
|
|
|
|
|
|
|
|
|
let config = config
|
|
|
|
|
.outputs
|
|
|
|
|
.find(&output_name)
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
config.off || should_disable(&output_name.connector)
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-19 20:49:06 +03:00
|
|
|
if let Err(err) = device.cleanup_mismatching_resources(&should_be_off) {
|
2025-11-19 08:45:09 +03:00
|
|
|
warn!("error cleaning up connectors: {err:?}");
|
|
|
|
|
}
|
2025-11-19 20:49:06 +03:00
|
|
|
|
|
|
|
|
let device = self.devices.get_mut(&node).unwrap();
|
|
|
|
|
for surface in device.surfaces.values_mut() {
|
|
|
|
|
// We aren't force-clearing the CRTCs, so we need to make the surfaces read the
|
|
|
|
|
// updated state after a session resume. This also causes a full damage for the
|
|
|
|
|
// next redraw.
|
|
|
|
|
if let Err(err) = surface.compositor.reset_state() {
|
|
|
|
|
warn!("error resetting DrmCompositor state: {err:?}");
|
|
|
|
|
}
|
2025-12-18 07:37:49 +03:00
|
|
|
surface.compositor.reset_buffers();
|
2025-11-19 20:49:06 +03:00
|
|
|
}
|
2025-11-19 08:45:09 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-08 09:11:56 +03:00
|
|
|
// This will connect any new connectors if needed, and apply other changes, such as
|
|
|
|
|
// connecting back the internal laptop monitor once it becomes the only monitor left.
|
|
|
|
|
//
|
|
|
|
|
// It will also call refresh_ipc_outputs(), which will catch the disconnected connectors
|
|
|
|
|
// above.
|
|
|
|
|
self.on_output_config_changed(niri);
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) {
|
2025-10-17 17:33:23 +03:00
|
|
|
debug!("removing device: {device_id}");
|
2024-01-03 18:16:20 +04:00
|
|
|
|
|
|
|
|
let Ok(node) = DrmNode::from_dev_id(device_id) else {
|
|
|
|
|
warn!("error creating DrmNode");
|
2023-08-13 12:46:53 +04:00
|
|
|
return;
|
|
|
|
|
};
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2025-10-19 12:09:51 +00:00
|
|
|
if node.ty() != NodeType::Primary {
|
|
|
|
|
debug!("not a primary node, skipping");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let Some(device) = self.devices.get_mut(&node) else {
|
2025-10-17 17:46:28 +03:00
|
|
|
warn!("unknown device");
|
2023-08-13 12:46:53 +04:00
|
|
|
return;
|
2024-01-03 18:16:20 +04:00
|
|
|
};
|
2023-08-13 12:46:53 +04:00
|
|
|
|
|
|
|
|
let crtcs: Vec<_> = device
|
|
|
|
|
.drm_scanner
|
|
|
|
|
.crtcs()
|
2024-01-16 15:33:37 +04:00
|
|
|
.map(|(_info, crtc)| crtc)
|
2023-08-13 12:46:53 +04:00
|
|
|
.collect();
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2024-01-16 15:33:37 +04:00
|
|
|
for crtc in crtcs {
|
|
|
|
|
self.connector_disconnected(niri, node, crtc);
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
2023-08-13 12:46:53 +04:00
|
|
|
|
2024-04-01 08:30:27 +04:00
|
|
|
let mut device = self.devices.remove(&node).unwrap();
|
2025-09-17 12:32:26 -04:00
|
|
|
let device_fd = device.drm.device_fd().device_fd();
|
2024-04-01 08:30:27 +04:00
|
|
|
|
|
|
|
|
if let Some(lease_state) = &mut device.drm_lease_state {
|
|
|
|
|
lease_state.disable_global::<State>();
|
|
|
|
|
}
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2025-11-25 22:16:44 +08:00
|
|
|
if let Some(render_node) = device.render_node {
|
|
|
|
|
// Sometimes (Asahi DisplayLink), multiple primary nodes will correspond to the same
|
|
|
|
|
// render node. In this case, we want to keep the render node active until the last
|
|
|
|
|
// primary node that uses it is gone.
|
|
|
|
|
let was_last = !self
|
|
|
|
|
.devices
|
|
|
|
|
.values()
|
|
|
|
|
.any(|device| device.render_node == Some(render_node));
|
|
|
|
|
|
|
|
|
|
if was_last && render_node == self.primary_render_node {
|
|
|
|
|
debug!("destroying the primary renderer");
|
|
|
|
|
|
|
|
|
|
match self.gpu_manager.single_renderer(&self.primary_render_node) {
|
|
|
|
|
Ok(mut renderer) => renderer.unbind_wl_display(),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error creating renderer during device removal: {err}");
|
|
|
|
|
}
|
2024-01-03 18:16:20 +04:00
|
|
|
}
|
|
|
|
|
|
2025-11-25 22:16:44 +08:00
|
|
|
// Disable and destroy the dmabuf global.
|
|
|
|
|
if let Some(global) = self.dmabuf_global.take() {
|
|
|
|
|
niri.dmabuf_state
|
|
|
|
|
.disable_global::<State>(&niri.display_handle, &global);
|
|
|
|
|
niri.event_loop
|
|
|
|
|
.insert_source(
|
|
|
|
|
Timer::from_duration(Duration::from_secs(10)),
|
|
|
|
|
move |_, _, state| {
|
|
|
|
|
state
|
|
|
|
|
.niri
|
|
|
|
|
.dmabuf_state
|
|
|
|
|
.destroy_global::<State>(&state.niri.display_handle, global);
|
|
|
|
|
TimeoutAction::Drop
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Clear the dmabuf feedbacks for all surfaces.
|
|
|
|
|
for device in self.devices.values_mut() {
|
|
|
|
|
for surface in device.surfaces.values_mut() {
|
|
|
|
|
surface.dmabuf_feedback = None;
|
|
|
|
|
}
|
2024-05-15 08:14:09 +04:00
|
|
|
}
|
2025-11-25 22:16:44 +08:00
|
|
|
} else {
|
|
|
|
|
error!("dmabuf global was already missing");
|
2024-01-03 18:16:20 +04:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-01 09:57:51 +04:00
|
|
|
|
2025-11-25 22:16:44 +08:00
|
|
|
if was_last {
|
|
|
|
|
self.gpu_manager.as_mut().remove_node(&render_node);
|
|
|
|
|
// Trigger re-enumeration in order to remove the device from gpu_manager.
|
|
|
|
|
let _ = self.gpu_manager.devices();
|
|
|
|
|
}
|
2025-11-20 12:19:43 +03:00
|
|
|
}
|
2025-09-17 12:32:26 -04:00
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
niri.event_loop.remove(device.token);
|
2024-01-17 10:38:32 +04:00
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
self.refresh_ipc_outputs(niri);
|
2025-09-17 12:32:26 -04:00
|
|
|
|
|
|
|
|
drop(device);
|
|
|
|
|
|
|
|
|
|
match TryInto::<OwnedFd>::try_into(device_fd) {
|
|
|
|
|
Ok(fd) => {
|
|
|
|
|
if let Err(err) = self.session.close(fd) {
|
|
|
|
|
warn!("error closing DRM device fd: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
error!("unable to close DRM device cleanly: fd has unexpected references");
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
|
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
fn connector_connected(
|
2023-08-09 11:03:38 +04:00
|
|
|
&mut self,
|
|
|
|
|
niri: &mut Niri,
|
2024-01-03 18:16:20 +04:00
|
|
|
node: DrmNode,
|
2023-08-13 12:46:53 +04:00
|
|
|
connector: connector::Info,
|
|
|
|
|
crtc: crtc::Handle,
|
|
|
|
|
) -> anyhow::Result<()> {
|
2024-09-03 12:13:04 +03:00
|
|
|
let connector_name = format_connector_name(&connector);
|
|
|
|
|
debug!("connecting connector: {connector_name}");
|
2023-08-13 12:46:53 +04:00
|
|
|
|
2024-02-11 13:15:03 -05:00
|
|
|
let device = self.devices.get_mut(&node).context("missing device")?;
|
|
|
|
|
|
2025-01-04 15:37:54 +03:00
|
|
|
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
|
|
|
|
let output_name = device.known_crtc_name(&crtc, &connector, disable_monitor_names);
|
2024-09-03 12:13:04 +03:00
|
|
|
|
2024-04-14 08:08:09 +04:00
|
|
|
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
|
|
|
|
|
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
|
2024-02-11 13:15:03 -05:00
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
if non_desktop {
|
|
|
|
|
debug!("output is non desktop");
|
2024-09-03 12:13:04 +03:00
|
|
|
let description = output_name.format_description();
|
2024-04-01 08:30:27 +04:00
|
|
|
if let Some(lease_state) = &mut device.drm_lease_state {
|
2024-09-03 12:13:04 +03:00
|
|
|
lease_state.add_connector::<State>(connector.handle(), connector_name, description);
|
2024-04-01 08:30:27 +04:00
|
|
|
}
|
2024-02-11 13:15:03 -05:00
|
|
|
device
|
|
|
|
|
.non_desktop_connectors
|
|
|
|
|
.insert((connector.handle(), crtc));
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-21 13:48:32 +04:00
|
|
|
let config = self
|
|
|
|
|
.config
|
|
|
|
|
.borrow()
|
|
|
|
|
.outputs
|
2024-07-04 17:51:11 +04:00
|
|
|
.find(&output_name)
|
2023-09-21 13:48:32 +04:00
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
2023-10-03 08:35:24 +04:00
|
|
|
for m in connector.modes() {
|
|
|
|
|
trace!("{m:?}");
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-29 07:10:38 +01:00
|
|
|
let mut mode = None;
|
|
|
|
|
if let Some(modeline) = &config.modeline {
|
|
|
|
|
match calculate_drm_mode_from_modeline(modeline) {
|
|
|
|
|
Ok(x) => mode = Some(x),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("invalid custom modeline; falling back to advertised modes: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (mode, fallback) = match mode {
|
|
|
|
|
Some(x) => (x, false),
|
|
|
|
|
None => pick_mode(&connector, config.mode).ok_or_else(|| anyhow!("no mode"))?,
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-16 18:01:25 +04:00
|
|
|
if fallback {
|
|
|
|
|
let target = config.mode.unwrap();
|
|
|
|
|
warn!(
|
|
|
|
|
"configured mode {}x{}{} could not be found, falling back to preferred",
|
2025-10-29 07:10:38 +01:00
|
|
|
target.mode.width,
|
|
|
|
|
target.mode.height,
|
|
|
|
|
if let Some(refresh) = target.mode.refresh {
|
2024-01-16 18:01:25 +04:00
|
|
|
format!("@{refresh}")
|
2023-10-03 08:35:24 +04:00
|
|
|
} else {
|
2024-01-16 18:01:25 +04:00
|
|
|
String::new()
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-10-03 08:35:24 +04:00
|
|
|
}
|
2025-10-29 07:10:38 +01:00
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
debug!("picking mode: {mode:?}");
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-11-18 17:45:55 +03:00
|
|
|
let mut orientation = None;
|
2025-08-24 00:53:54 +10:00
|
|
|
if let Ok(props) = ConnectorProperties::try_new(&device.drm, connector.handle()) {
|
|
|
|
|
match reset_hdr(&props) {
|
2025-08-24 15:53:27 +03:00
|
|
|
Ok(()) => (),
|
2025-10-29 09:23:23 +03:00
|
|
|
Err(err) => debug!("couldn't reset HDR properties: {err:?}"),
|
2025-08-07 14:06:11 -04:00
|
|
|
}
|
2025-08-24 00:53:54 +10:00
|
|
|
|
2025-11-18 17:45:55 +03:00
|
|
|
match get_panel_orientation(&props) {
|
|
|
|
|
Ok(x) => orientation = Some(x),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
trace!("couldn't get panel orientation: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-24 00:53:54 +10:00
|
|
|
} else {
|
|
|
|
|
warn!("failed to get connector properties");
|
|
|
|
|
};
|
2024-03-13 09:15:18 +04:00
|
|
|
|
2024-03-15 08:54:36 +04:00
|
|
|
let mut gamma_props = GammaProps::new(&device.drm, crtc)
|
2025-10-29 09:23:23 +03:00
|
|
|
.map_err(|err| debug!("couldn't get gamma properties: {err:?}"))
|
2024-03-15 08:54:36 +04:00
|
|
|
.ok();
|
|
|
|
|
|
|
|
|
|
// Reset gamma in case it was set before.
|
|
|
|
|
let res = if let Some(gamma_props) = &mut gamma_props {
|
|
|
|
|
gamma_props.set_gamma(&device.drm, None)
|
|
|
|
|
} else {
|
|
|
|
|
set_gamma_for_crtc(&device.drm, crtc, None)
|
|
|
|
|
};
|
|
|
|
|
if let Err(err) = res {
|
2025-10-29 09:23:23 +03:00
|
|
|
debug!("couldn't reset gamma: {err:?}");
|
2024-03-14 08:32:23 +04:00
|
|
|
}
|
|
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
let surface = device
|
|
|
|
|
.drm
|
2024-01-16 18:01:25 +04:00
|
|
|
.create_surface(crtc, mode, &[connector.handle()])?;
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2024-12-14 13:15:14 +03:00
|
|
|
// Try to enable VRR if requested.
|
|
|
|
|
match surface.vrr_supported(connector.handle()) {
|
|
|
|
|
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) => {
|
|
|
|
|
// Even if on-demand, we still disable it until later checks.
|
|
|
|
|
let vrr = config.is_vrr_always_on();
|
|
|
|
|
let word = if vrr { "enabling" } else { "disabling" };
|
|
|
|
|
|
|
|
|
|
if let Err(err) = surface.use_vrr(vrr) {
|
|
|
|
|
warn!("error {} VRR: {err:?}", word);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(VrrSupport::NotSupported) => {
|
|
|
|
|
if !config.is_vrr_always_off() {
|
|
|
|
|
warn!("cannot enable VRR because connector does not support it");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to disable it anyway to work around a bug where resetting DRM state causes
|
|
|
|
|
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
|
|
|
|
|
let _ = surface.use_vrr(false);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error querying for VRR support: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 11:03:38 +04:00
|
|
|
// Update the output mode.
|
|
|
|
|
let (physical_width, physical_height) = connector.size().unwrap_or((0, 0));
|
|
|
|
|
|
|
|
|
|
let output = Output::new(
|
2024-09-03 12:13:04 +03:00
|
|
|
connector_name.clone(),
|
2023-08-09 11:03:38 +04:00
|
|
|
PhysicalProperties {
|
|
|
|
|
size: (physical_width as i32, physical_height as i32).into(),
|
2024-03-08 17:06:46 +04:00
|
|
|
subpixel: connector.subpixel().into(),
|
2024-09-03 12:13:04 +03:00
|
|
|
model: output_name.model.as_deref().unwrap_or("Unknown").to_owned(),
|
|
|
|
|
make: output_name.make.as_deref().unwrap_or("Unknown").to_owned(),
|
2025-08-11 09:02:28 +03:00
|
|
|
serial_number: output_name
|
|
|
|
|
.serial
|
|
|
|
|
.as_deref()
|
|
|
|
|
.unwrap_or("Unknown")
|
|
|
|
|
.to_owned(),
|
2023-08-09 11:03:38 +04:00
|
|
|
},
|
|
|
|
|
);
|
2023-10-26 00:15:46 +04:00
|
|
|
|
2024-01-16 18:01:25 +04:00
|
|
|
let wl_mode = Mode::from(mode);
|
2024-01-16 09:42:31 +04:00
|
|
|
output.change_current_state(Some(wl_mode), None, None, None);
|
2023-08-09 11:03:38 +04:00
|
|
|
output.set_preferred(wl_mode);
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
output
|
|
|
|
|
.user_data()
|
|
|
|
|
.insert_if_missing(|| TtyOutputState { node, crtc });
|
2024-09-03 12:13:04 +03:00
|
|
|
output.user_data().insert_if_missing(|| output_name.clone());
|
2025-11-18 17:45:55 +03:00
|
|
|
if let Some(x) = orientation {
|
|
|
|
|
output.user_data().insert_if_missing(|| PanelOrientation(x));
|
|
|
|
|
}
|
2023-08-09 11:03:38 +04:00
|
|
|
|
2025-11-20 12:19:43 +03:00
|
|
|
let render_node = device.render_node.unwrap_or(self.primary_render_node);
|
|
|
|
|
let renderer = self.gpu_manager.single_renderer(&render_node)?;
|
2024-01-03 18:16:20 +04:00
|
|
|
let egl_context = renderer.as_ref().egl_context();
|
2024-01-01 10:18:22 +04:00
|
|
|
let render_formats = egl_context.dmabuf_render_formats();
|
|
|
|
|
|
2024-04-22 09:14:14 +04:00
|
|
|
// Filter out the CCS modifiers as they have increased bandwidth, causing some monitor
|
|
|
|
|
// configurations to stop working.
|
2024-09-10 11:12:24 +03:00
|
|
|
//
|
2025-11-20 12:19:43 +03:00
|
|
|
// For display only devices, restrict to linear buffers for best compatibility.
|
|
|
|
|
//
|
2024-09-10 11:12:24 +03:00
|
|
|
// The invalid modifier attempt below should make this unnecessary in some cases, but it
|
|
|
|
|
// would still be a bad idea to remove this until Smithay has some kind of full-device
|
|
|
|
|
// modesetting test that is able to "downgrade" existing connector modifiers to get enough
|
|
|
|
|
// bandwidth for a newly connected one.
|
2024-06-21 07:53:41 +03:00
|
|
|
let render_formats = render_formats
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.filter(|format| {
|
2025-11-20 12:19:43 +03:00
|
|
|
if device.render_node.is_none() {
|
|
|
|
|
return format.modifier == Modifier::Linear;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-20 12:03:19 +03:00
|
|
|
let is_ccs = matches!(
|
2024-06-21 07:53:41 +03:00
|
|
|
format.modifier,
|
|
|
|
|
Modifier::I915_y_tiled_ccs
|
2024-04-22 09:14:14 +04:00
|
|
|
// I915_FORMAT_MOD_Yf_TILED_CCS
|
|
|
|
|
| Modifier::Unrecognized(0x100000000000005)
|
|
|
|
|
| Modifier::I915_y_tiled_gen12_rc_ccs
|
|
|
|
|
| Modifier::I915_y_tiled_gen12_mc_ccs
|
|
|
|
|
// I915_FORMAT_MOD_Y_TILED_GEN12_RC_CCS_CC
|
|
|
|
|
| Modifier::Unrecognized(0x100000000000008)
|
|
|
|
|
// I915_FORMAT_MOD_4_TILED_DG2_RC_CCS
|
|
|
|
|
| Modifier::Unrecognized(0x10000000000000a)
|
|
|
|
|
// I915_FORMAT_MOD_4_TILED_DG2_MC_CCS
|
|
|
|
|
| Modifier::Unrecognized(0x10000000000000b)
|
|
|
|
|
// I915_FORMAT_MOD_4_TILED_DG2_RC_CCS_CC
|
|
|
|
|
| Modifier::Unrecognized(0x10000000000000c)
|
2025-11-20 12:03:19 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
!is_ccs
|
2024-06-21 07:53:41 +03:00
|
|
|
})
|
|
|
|
|
.collect::<FormatSet>();
|
2024-04-22 09:14:14 +04:00
|
|
|
|
2023-08-09 11:03:38 +04:00
|
|
|
// Create the compositor.
|
2024-09-10 11:12:24 +03:00
|
|
|
let res = DrmCompositor::new(
|
2023-08-13 12:46:53 +04:00
|
|
|
OutputModeSource::Auto(output.clone()),
|
2023-08-09 11:03:38 +04:00
|
|
|
surface,
|
2025-01-04 09:09:24 +03:00
|
|
|
None,
|
2025-11-20 11:56:04 +03:00
|
|
|
device.allocator.clone(),
|
2025-07-15 18:25:55 +03:00
|
|
|
GbmFramebufferExporter::new(device.gbm.clone(), device.render_node.into()),
|
2023-08-09 11:03:38 +04:00
|
|
|
SUPPORTED_COLOR_FORMATS,
|
2024-01-03 18:16:20 +04:00
|
|
|
// This is only used to pick a good internal format, so it can use the surface's render
|
|
|
|
|
// formats, even though we only ever render on the primary GPU.
|
2024-01-01 10:18:22 +04:00
|
|
|
render_formats.clone(),
|
2023-08-13 12:46:53 +04:00
|
|
|
device.drm.cursor_size(),
|
2025-01-04 09:09:24 +03:00
|
|
|
Some(device.gbm.clone()),
|
2024-09-10 11:12:24 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut compositor = match res {
|
|
|
|
|
Ok(x) => x,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error creating DRM compositor, will try with invalid modifier: {err:?}");
|
|
|
|
|
|
|
|
|
|
let render_formats = render_formats
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.filter(|format| format.modifier == Modifier::Invalid)
|
|
|
|
|
.collect::<FormatSet>();
|
|
|
|
|
|
|
|
|
|
// DrmCompositor::new() consumed the surface...
|
|
|
|
|
let surface = device
|
|
|
|
|
.drm
|
|
|
|
|
.create_surface(crtc, mode, &[connector.handle()])?;
|
|
|
|
|
|
|
|
|
|
DrmCompositor::new(
|
|
|
|
|
OutputModeSource::Auto(output.clone()),
|
|
|
|
|
surface,
|
2025-01-04 09:09:24 +03:00
|
|
|
None,
|
2025-11-20 11:56:04 +03:00
|
|
|
device.allocator.clone(),
|
2025-07-15 18:25:55 +03:00
|
|
|
GbmFramebufferExporter::new(device.gbm.clone(), device.render_node.into()),
|
2024-09-10 11:12:24 +03:00
|
|
|
SUPPORTED_COLOR_FORMATS,
|
|
|
|
|
render_formats,
|
|
|
|
|
device.drm.cursor_size(),
|
2025-01-04 09:09:24 +03:00
|
|
|
Some(device.gbm.clone()),
|
2024-09-10 11:12:24 +03:00
|
|
|
)
|
|
|
|
|
.context("error creating DRM compositor")?
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-02-21 07:55:10 +04:00
|
|
|
if self.debug_tint {
|
|
|
|
|
compositor.set_debug_flags(DebugFlags::TINT);
|
|
|
|
|
}
|
2023-08-13 12:46:53 +04:00
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let mut dmabuf_feedback = None;
|
|
|
|
|
if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) {
|
2024-06-21 07:53:41 +03:00
|
|
|
let primary_formats = primary_renderer.dmabuf_formats();
|
2024-01-03 18:16:20 +04:00
|
|
|
|
|
|
|
|
match surface_dmabuf_feedback(
|
|
|
|
|
&compositor,
|
|
|
|
|
primary_formats,
|
|
|
|
|
self.primary_render_node,
|
|
|
|
|
device.render_node,
|
2025-11-20 12:19:43 +03:00
|
|
|
node,
|
2024-01-03 18:16:20 +04:00
|
|
|
) {
|
|
|
|
|
Ok(feedback) => {
|
|
|
|
|
dmabuf_feedback = Some(feedback);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error building dmabuf feedback: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-03 15:15:55 +04:00
|
|
|
|
2024-10-20 20:15:06 +03:00
|
|
|
// Some buggy monitors replug upon powering off, so powering on here would prevent such
|
|
|
|
|
// monitors from powering off. Therefore, we avoid unconditionally powering on.
|
|
|
|
|
if !niri.monitors_active {
|
|
|
|
|
if let Err(err) = compositor.clear() {
|
|
|
|
|
warn!("error clearing drm surface: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-14 13:15:14 +03:00
|
|
|
let vrr_enabled = compositor.vrr_enabled();
|
|
|
|
|
|
2023-09-26 08:00:22 +04:00
|
|
|
let vblank_frame_name =
|
2024-09-03 12:13:04 +03:00
|
|
|
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
|
|
|
|
|
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
|
|
|
|
|
"{connector_name} time since presentation, ms"
|
|
|
|
|
));
|
2023-11-24 09:15:25 +04:00
|
|
|
let presentation_misprediction_plot_name = tracy_client::PlotName::new_leak(format!(
|
2024-09-03 12:13:04 +03:00
|
|
|
"{connector_name} presentation misprediction, ms"
|
2023-09-26 08:00:22 +04:00
|
|
|
));
|
2023-09-29 13:12:50 +04:00
|
|
|
let sequence_delta_plot_name =
|
2024-09-03 12:13:04 +03:00
|
|
|
tracy_client::PlotName::new_leak(format!("{connector_name} sequence delta"));
|
2023-09-04 08:57:11 +04:00
|
|
|
|
2023-09-04 07:49:50 +04:00
|
|
|
let surface = Surface {
|
2024-09-03 12:13:04 +03:00
|
|
|
name: output_name,
|
2024-08-22 18:58:07 +10:00
|
|
|
connector: connector.handle(),
|
2023-09-04 07:49:50 +04:00
|
|
|
compositor,
|
|
|
|
|
dmabuf_feedback,
|
2024-03-15 08:54:36 +04:00
|
|
|
gamma_props,
|
2024-03-15 11:10:54 +04:00
|
|
|
pending_gamma_change: None,
|
2023-09-04 08:57:11 +04:00
|
|
|
vblank_frame: None,
|
|
|
|
|
vblank_frame_name,
|
2023-11-24 09:19:49 +04:00
|
|
|
time_since_presentation_plot_name,
|
2023-11-24 09:15:25 +04:00
|
|
|
presentation_misprediction_plot_name,
|
2023-09-29 13:12:50 +04:00
|
|
|
sequence_delta_plot_name,
|
2023-09-04 07:49:50 +04:00
|
|
|
};
|
2024-07-02 01:21:07 -07:00
|
|
|
|
2023-09-04 07:49:50 +04:00
|
|
|
let res = device.surfaces.insert(crtc, surface);
|
2023-08-13 12:46:53 +04:00
|
|
|
assert!(res.is_none(), "crtc must not have already existed");
|
|
|
|
|
|
2024-04-14 09:23:15 +04:00
|
|
|
niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled);
|
2023-10-09 18:37:43 +04:00
|
|
|
|
2024-07-02 01:21:07 -07:00
|
|
|
if niri.monitors_active {
|
|
|
|
|
// Redraw the new monitor.
|
|
|
|
|
niri.event_loop.insert_idle(move |state| {
|
2024-10-20 20:15:28 +03:00
|
|
|
// Guard against output disconnecting before the idle has a chance to run.
|
|
|
|
|
if state.niri.output_state.contains_key(&output) {
|
|
|
|
|
state.niri.queue_redraw(&output);
|
|
|
|
|
}
|
2024-07-02 01:21:07 -07:00
|
|
|
});
|
|
|
|
|
}
|
2023-08-13 12:46:53 +04:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 15:33:37 +04:00
|
|
|
fn connector_disconnected(&mut self, niri: &mut Niri, node: DrmNode, crtc: crtc::Handle) {
|
2024-01-03 18:16:20 +04:00
|
|
|
let Some(device) = self.devices.get_mut(&node) else {
|
2024-01-16 15:33:37 +04:00
|
|
|
debug!("disconnecting connector for crtc: {crtc:?}");
|
2024-01-03 18:16:20 +04:00
|
|
|
error!("missing device");
|
2023-09-26 10:06:07 +04:00
|
|
|
return;
|
|
|
|
|
};
|
2023-08-13 12:46:53 +04:00
|
|
|
|
2023-09-08 17:54:02 +04:00
|
|
|
let Some(surface) = device.surfaces.remove(&crtc) else {
|
2024-01-16 15:33:37 +04:00
|
|
|
debug!("disconnecting connector for crtc: {crtc:?}");
|
2024-04-01 08:30:27 +04:00
|
|
|
|
|
|
|
|
if let Some((conn, _)) = device
|
|
|
|
|
.non_desktop_connectors
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|(_, crtc_)| *crtc_ == crtc)
|
|
|
|
|
{
|
|
|
|
|
debug!("withdrawing non-desktop connector from DRM leasing");
|
|
|
|
|
|
|
|
|
|
let conn = *conn;
|
|
|
|
|
device.non_desktop_connectors.remove(&(conn, crtc));
|
|
|
|
|
|
|
|
|
|
if let Some(lease_state) = &mut device.drm_lease_state {
|
|
|
|
|
lease_state.withdraw_connector(conn);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
debug!("crtc wasn't enabled");
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
return;
|
2023-09-08 17:54:02 +04:00
|
|
|
};
|
2023-08-13 12:46:53 +04:00
|
|
|
|
2024-09-03 12:13:04 +03:00
|
|
|
debug!("disconnecting connector: {:?}", surface.name.connector);
|
2024-01-16 15:33:37 +04:00
|
|
|
|
2023-08-13 12:46:53 +04:00
|
|
|
let output = niri
|
|
|
|
|
.global_space
|
|
|
|
|
.outputs()
|
|
|
|
|
.find(|output| {
|
|
|
|
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
2024-01-03 18:16:20 +04:00
|
|
|
tty_state.node == node && tty_state.crtc == crtc
|
2023-08-13 12:46:53 +04:00
|
|
|
})
|
2023-09-26 10:06:07 +04:00
|
|
|
.cloned();
|
|
|
|
|
if let Some(output) = output {
|
|
|
|
|
niri.remove_output(&output);
|
|
|
|
|
} else {
|
|
|
|
|
error!("missing output for crtc {crtc:?}");
|
|
|
|
|
};
|
2023-08-09 11:03:38 +04:00
|
|
|
}
|
2023-08-09 14:21:29 +04:00
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
fn on_vblank(
|
|
|
|
|
&mut self,
|
|
|
|
|
niri: &mut Niri,
|
|
|
|
|
node: DrmNode,
|
|
|
|
|
crtc: crtc::Handle,
|
|
|
|
|
meta: DrmEventMetadata,
|
|
|
|
|
) {
|
2023-09-26 09:23:30 +04:00
|
|
|
let span = tracy_client::span!("Tty::on_vblank");
|
|
|
|
|
|
2023-09-26 08:39:38 +04:00
|
|
|
let now = get_monotonic_time();
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let Some(device) = self.devices.get_mut(&node) else {
|
2023-09-26 08:54:41 +04:00
|
|
|
// I've seen it happen.
|
2024-01-03 18:16:20 +04:00
|
|
|
error!("missing device in vblank callback for crtc {crtc:?}");
|
2023-09-26 08:54:41 +04:00
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some(surface) = device.surfaces.get_mut(&crtc) else {
|
|
|
|
|
error!("missing surface in vblank callback for crtc {crtc:?}");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2023-10-11 09:02:55 +04:00
|
|
|
// Finish the Tracy frame, if any.
|
|
|
|
|
drop(surface.vblank_frame.take());
|
|
|
|
|
|
2024-09-03 12:13:04 +03:00
|
|
|
let name = &surface.name.connector;
|
2023-09-26 08:39:38 +04:00
|
|
|
trace!("vblank on {name} {meta:?}");
|
2023-09-26 09:23:30 +04:00
|
|
|
span.emit_text(name);
|
2023-09-26 08:39:38 +04:00
|
|
|
|
|
|
|
|
let presentation_time = match meta.time {
|
|
|
|
|
DrmEventTime::Monotonic(time) => time,
|
|
|
|
|
DrmEventTime::Realtime(_) => {
|
|
|
|
|
// Not supported.
|
|
|
|
|
|
|
|
|
|
// This value will be ignored in the frame clock code.
|
|
|
|
|
Duration::ZERO
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-03-01 07:49:10 +04:00
|
|
|
let presentation_time = if niri.config.borrow().debug.emulate_zero_presentation_time {
|
|
|
|
|
Duration::ZERO
|
|
|
|
|
} else {
|
|
|
|
|
presentation_time
|
|
|
|
|
};
|
2023-09-26 08:39:38 +04:00
|
|
|
|
|
|
|
|
let message = if presentation_time.is_zero() {
|
|
|
|
|
format!("vblank on {name}, presentation time unknown")
|
|
|
|
|
} else if presentation_time > now {
|
|
|
|
|
let diff = presentation_time - now;
|
2023-11-24 09:19:49 +04:00
|
|
|
tracy_client::Client::running().unwrap().plot(
|
|
|
|
|
surface.time_since_presentation_plot_name,
|
|
|
|
|
-diff.as_secs_f64() * 1000.,
|
|
|
|
|
);
|
2023-09-26 08:39:38 +04:00
|
|
|
format!("vblank on {name}, presentation is {diff:?} later")
|
|
|
|
|
} else {
|
|
|
|
|
let diff = now - presentation_time;
|
2023-11-24 09:19:49 +04:00
|
|
|
tracy_client::Client::running().unwrap().plot(
|
|
|
|
|
surface.time_since_presentation_plot_name,
|
|
|
|
|
diff.as_secs_f64() * 1000.,
|
|
|
|
|
);
|
2023-09-26 08:39:38 +04:00
|
|
|
format!("vblank on {name}, presentation was {diff:?} ago")
|
|
|
|
|
};
|
|
|
|
|
tracy_client::Client::running()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.message(&message, 0);
|
|
|
|
|
|
2023-09-26 08:54:41 +04:00
|
|
|
let Some(output) = niri
|
2023-09-26 08:39:38 +04:00
|
|
|
.global_space
|
|
|
|
|
.outputs()
|
|
|
|
|
.find(|output| {
|
|
|
|
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
2024-01-03 18:16:20 +04:00
|
|
|
tty_state.node == node && tty_state.crtc == crtc
|
2023-09-26 08:39:38 +04:00
|
|
|
})
|
2023-09-26 08:54:41 +04:00
|
|
|
.cloned()
|
|
|
|
|
else {
|
|
|
|
|
error!("missing output in global space for {name}");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some(output_state) = niri.output_state.get_mut(&output) else {
|
|
|
|
|
error!("missing output state for {name}");
|
|
|
|
|
return;
|
|
|
|
|
};
|
2023-09-26 08:39:38 +04:00
|
|
|
|
2025-11-20 12:55:45 +03:00
|
|
|
let refresh_interval = output_state.frame_clock.refresh_interval();
|
|
|
|
|
|
|
|
|
|
let time = if presentation_time.is_zero() {
|
|
|
|
|
now
|
|
|
|
|
} else {
|
|
|
|
|
presentation_time
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-20 12:55:45 +03:00
|
|
|
if output_state
|
|
|
|
|
.vblank_throttle
|
|
|
|
|
.throttle(refresh_interval, time, move |state| {
|
|
|
|
|
let meta = DrmEventMetadata {
|
|
|
|
|
sequence: meta.sequence,
|
|
|
|
|
time: DrmEventTime::Monotonic(Duration::ZERO),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let tty = state.backend.tty();
|
|
|
|
|
tty.on_vblank(&mut state.niri, node, crtc, meta);
|
|
|
|
|
})
|
|
|
|
|
{
|
|
|
|
|
// Throttled.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-10 10:48:45 +03:00
|
|
|
let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
|
|
|
|
RedrawState::WaitingForVBlank { redraw_needed } => redraw_needed,
|
|
|
|
|
state @ (RedrawState::Idle
|
|
|
|
|
| RedrawState::Queued
|
|
|
|
|
| RedrawState::WaitingForEstimatedVBlank(_)
|
|
|
|
|
| RedrawState::WaitingForEstimatedVBlankAndQueued(_)) => {
|
|
|
|
|
// This is an error!() because it shouldn't happen, but on some systems it somehow
|
|
|
|
|
// does. Kernel sending rogue vblank events?
|
|
|
|
|
//
|
|
|
|
|
// https://github.com/YaLTeR/niri/issues/556
|
|
|
|
|
// https://github.com/YaLTeR/niri/issues/615
|
|
|
|
|
error!(
|
|
|
|
|
"unexpected redraw state for output {name} (should be WaitingForVBlank); \
|
|
|
|
|
can happen when resuming from sleep or powering on monitors: {state:?}"
|
|
|
|
|
);
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-07-27 13:43:27 +03:00
|
|
|
|
2023-09-26 08:39:38 +04:00
|
|
|
// Mark the last frame as submitted.
|
|
|
|
|
match surface.compositor.frame_submitted() {
|
|
|
|
|
Ok(Some((mut feedback, target_presentation_time))) => {
|
2025-11-20 12:55:45 +03:00
|
|
|
let refresh = match refresh_interval {
|
2024-12-13 10:28:25 +03:00
|
|
|
Some(refresh) => {
|
|
|
|
|
if output_state.frame_clock.vrr() {
|
|
|
|
|
Refresh::Variable(refresh)
|
|
|
|
|
} else {
|
|
|
|
|
Refresh::Fixed(refresh)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => Refresh::Unknown,
|
|
|
|
|
};
|
|
|
|
|
|
2023-09-26 08:39:38 +04:00
|
|
|
// FIXME: ideally should be monotonically increasing for a surface.
|
|
|
|
|
let seq = meta.sequence as u64;
|
2024-03-01 07:55:09 +04:00
|
|
|
let mut flags = wp_presentation_feedback::Kind::Vsync
|
2023-09-26 08:39:38 +04:00
|
|
|
| wp_presentation_feedback::Kind::HwCompletion;
|
|
|
|
|
|
2025-11-20 12:55:45 +03:00
|
|
|
if !presentation_time.is_zero() {
|
2024-03-01 07:55:09 +04:00
|
|
|
flags.insert(wp_presentation_feedback::Kind::HwClock);
|
2025-11-20 12:55:45 +03:00
|
|
|
}
|
2024-03-01 07:55:09 +04:00
|
|
|
|
|
|
|
|
feedback.presented::<_, smithay::utils::Monotonic>(time, refresh, seq, flags);
|
2023-09-26 08:39:38 +04:00
|
|
|
|
|
|
|
|
if !presentation_time.is_zero() {
|
2023-11-24 09:15:25 +04:00
|
|
|
let misprediction_s =
|
2023-09-26 08:39:38 +04:00
|
|
|
presentation_time.as_secs_f64() - target_presentation_time.as_secs_f64();
|
2023-11-24 09:15:25 +04:00
|
|
|
tracy_client::Client::running().unwrap().plot(
|
|
|
|
|
surface.presentation_misprediction_plot_name,
|
|
|
|
|
misprediction_s * 1000.,
|
|
|
|
|
);
|
2023-09-26 08:39:38 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(None) => (),
|
|
|
|
|
Err(err) => {
|
2024-02-21 09:20:34 +04:00
|
|
|
warn!("error marking frame as submitted: {err}");
|
2023-09-26 08:39:38 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 08:10:05 +04:00
|
|
|
if let Some(last_sequence) = output_state.last_drm_sequence {
|
2023-09-29 13:12:50 +04:00
|
|
|
let delta = meta.sequence as f64 - last_sequence as f64;
|
|
|
|
|
tracy_client::Client::running()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.plot(surface.sequence_delta_plot_name, delta);
|
|
|
|
|
}
|
2024-03-02 08:10:05 +04:00
|
|
|
output_state.last_drm_sequence = Some(meta.sequence);
|
2023-09-29 13:12:50 +04:00
|
|
|
|
2023-09-26 08:39:38 +04:00
|
|
|
output_state.frame_clock.presented(presentation_time);
|
2023-09-30 17:13:56 +04:00
|
|
|
|
|
|
|
|
if redraw_needed || output_state.unfinished_animations_remain {
|
2023-10-11 09:02:55 +04:00
|
|
|
let vblank_frame = tracy_client::Client::running()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.non_continuous_frame(surface.vblank_frame_name);
|
|
|
|
|
surface.vblank_frame = Some(vblank_frame);
|
|
|
|
|
|
2024-03-23 14:16:36 +04:00
|
|
|
niri.queue_redraw(&output);
|
2023-09-30 17:13:56 +04:00
|
|
|
} else {
|
|
|
|
|
niri.send_frame_callbacks(&output);
|
|
|
|
|
}
|
2023-09-26 08:39:38 +04:00
|
|
|
}
|
|
|
|
|
|
2023-09-30 17:13:56 +04:00
|
|
|
fn on_estimated_vblank_timer(&self, niri: &mut Niri, output: Output) {
|
2023-09-29 13:12:50 +04:00
|
|
|
let span = tracy_client::span!("Tty::on_estimated_vblank_timer");
|
|
|
|
|
|
|
|
|
|
let name = output.name();
|
|
|
|
|
span.emit_text(&name);
|
|
|
|
|
|
2023-09-30 17:13:56 +04:00
|
|
|
let Some(output_state) = niri.output_state.get_mut(&output) else {
|
2023-09-29 13:12:50 +04:00
|
|
|
error!("missing output state for {name}");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-02 08:10:05 +04:00
|
|
|
// We waited for the timer, now we can send frame callbacks again.
|
|
|
|
|
output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1);
|
|
|
|
|
|
2023-09-30 17:13:56 +04:00
|
|
|
match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) {
|
|
|
|
|
RedrawState::Idle => unreachable!(),
|
2024-03-23 12:46:26 +04:00
|
|
|
RedrawState::Queued => unreachable!(),
|
2023-09-30 17:13:56 +04:00
|
|
|
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
|
|
|
|
RedrawState::WaitingForEstimatedVBlank(_) => (),
|
|
|
|
|
// The timer fired just in front of a redraw.
|
2024-03-23 12:46:26 +04:00
|
|
|
RedrawState::WaitingForEstimatedVBlankAndQueued(_) => {
|
|
|
|
|
output_state.redraw_state = RedrawState::Queued;
|
2023-09-30 17:13:56 +04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-29 13:12:50 +04:00
|
|
|
|
2024-03-02 08:10:05 +04:00
|
|
|
if output_state.unfinished_animations_remain {
|
2024-03-23 14:16:36 +04:00
|
|
|
niri.queue_redraw(&output);
|
2024-03-02 08:10:05 +04:00
|
|
|
} else {
|
|
|
|
|
niri.send_frame_callbacks(&output);
|
2023-09-29 13:12:50 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-03 13:04:32 +04:00
|
|
|
pub fn seat_name(&self) -> String {
|
|
|
|
|
self.session.seat()
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-03 11:33:24 +04:00
|
|
|
pub fn with_primary_renderer<T>(
|
|
|
|
|
&mut self,
|
|
|
|
|
f: impl FnOnce(&mut GlesRenderer) -> T,
|
|
|
|
|
) -> Option<T> {
|
2024-01-03 18:16:20 +04:00
|
|
|
let mut renderer = self
|
|
|
|
|
.gpu_manager
|
|
|
|
|
.single_renderer(&self.primary_render_node)
|
|
|
|
|
.ok()?;
|
|
|
|
|
Some(f(renderer.as_gles_renderer()))
|
2023-09-03 13:04:32 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn render(
|
|
|
|
|
&mut self,
|
|
|
|
|
niri: &mut Niri,
|
|
|
|
|
output: &Output,
|
2023-09-20 11:51:25 +04:00
|
|
|
target_presentation_time: Duration,
|
2023-10-13 13:04:41 +04:00
|
|
|
) -> RenderResult {
|
2023-09-19 07:39:21 +04:00
|
|
|
let span = tracy_client::span!("Tty::render");
|
2023-09-03 13:04:32 +04:00
|
|
|
|
2024-01-03 11:31:04 +04:00
|
|
|
let mut rv = RenderResult::Skipped;
|
2023-10-13 13:04:41 +04:00
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
|
|
|
|
let Some(device) = self.devices.get_mut(&tty_state.node) else {
|
2023-09-26 10:06:07 +04:00
|
|
|
error!("missing output device");
|
2023-10-13 13:04:41 +04:00
|
|
|
return rv;
|
2023-09-26 10:06:07 +04:00
|
|
|
};
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let Some(surface) = device.surfaces.get_mut(&tty_state.crtc) else {
|
|
|
|
|
error!("missing surface");
|
|
|
|
|
return rv;
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-03 12:13:04 +03:00
|
|
|
span.emit_text(&surface.name.connector);
|
2024-01-03 18:16:20 +04:00
|
|
|
|
2024-01-03 11:28:42 +04:00
|
|
|
if !device.drm.is_active() {
|
2025-08-17 11:29:22 +03:00
|
|
|
// This branch hits any time we try to render while the user had switched to a
|
|
|
|
|
// different VT, so don't print anything here.
|
2024-01-03 11:28:42 +04:00
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let mut renderer = match self.gpu_manager.renderer(
|
|
|
|
|
&self.primary_render_node,
|
2025-11-20 12:19:43 +03:00
|
|
|
&device.render_node.unwrap_or(self.primary_render_node),
|
2024-01-03 18:16:20 +04:00
|
|
|
surface.compositor.format(),
|
|
|
|
|
) {
|
|
|
|
|
Ok(renderer) => renderer,
|
|
|
|
|
Err(err) => {
|
2024-02-21 09:20:34 +04:00
|
|
|
warn!("error creating renderer for primary GPU: {err:?}");
|
2024-01-03 18:16:20 +04:00
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-01-03 11:28:42 +04:00
|
|
|
|
|
|
|
|
// Render the elements.
|
2024-05-03 10:25:51 +04:00
|
|
|
let mut elements =
|
2024-03-24 09:03:59 +04:00
|
|
|
niri.render::<TtyRenderer>(&mut renderer, output, true, RenderTarget::Output);
|
2024-01-03 11:28:42 +04:00
|
|
|
|
2024-05-03 10:25:51 +04:00
|
|
|
// Visualize the damage, if enabled.
|
|
|
|
|
if niri.debug_draw_damage {
|
|
|
|
|
let output_state = niri.output_state.get_mut(output).unwrap();
|
|
|
|
|
draw_damage(&mut output_state.debug_damage_tracker, &mut elements);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 09:09:24 +03:00
|
|
|
// Overlay planes are disabled by default as they cause weird performance issues on my
|
|
|
|
|
// system.
|
2025-01-04 13:02:05 +03:00
|
|
|
let flags = {
|
2025-01-04 09:09:24 +03:00
|
|
|
let debug = &self.config.borrow().debug;
|
2025-01-04 13:02:05 +03:00
|
|
|
|
|
|
|
|
let primary_scanout_flag = if debug.restrict_primary_scanout_to_matching_format {
|
|
|
|
|
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT
|
|
|
|
|
} else {
|
|
|
|
|
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
|
|
|
|
|
};
|
|
|
|
|
let mut flags = primary_scanout_flag | FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
|
|
|
|
|
|
2025-01-04 09:09:24 +03:00
|
|
|
if debug.enable_overlay_planes {
|
|
|
|
|
flags.insert(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
|
|
|
|
|
}
|
|
|
|
|
if debug.disable_direct_scanout {
|
2025-01-04 13:02:05 +03:00
|
|
|
flags.remove(primary_scanout_flag);
|
2025-01-04 09:09:24 +03:00
|
|
|
flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT);
|
|
|
|
|
}
|
|
|
|
|
if debug.disable_cursor_plane {
|
|
|
|
|
flags.remove(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT);
|
|
|
|
|
}
|
2025-06-04 01:56:21 +10:00
|
|
|
if debug.skip_cursor_only_updates_during_vrr {
|
|
|
|
|
let output_state = niri.output_state.get(output).unwrap();
|
|
|
|
|
if output_state.frame_clock.vrr() {
|
|
|
|
|
flags.insert(FrameFlags::SKIP_CURSOR_ONLY_UPDATES);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-01-04 13:02:05 +03:00
|
|
|
|
|
|
|
|
flags
|
|
|
|
|
};
|
2025-01-04 09:09:24 +03:00
|
|
|
|
2024-01-03 11:28:42 +04:00
|
|
|
// Hand them over to the DRM.
|
2023-09-26 10:06:07 +04:00
|
|
|
let drm_compositor = &mut surface.compositor;
|
2025-01-04 09:09:24 +03:00
|
|
|
match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4], flags) {
|
2023-09-03 13:04:32 +04:00
|
|
|
Ok(res) => {
|
2024-08-23 09:26:42 +03:00
|
|
|
let needs_sync = res.needs_sync()
|
|
|
|
|
|| self
|
|
|
|
|
.config
|
|
|
|
|
.borrow()
|
|
|
|
|
.debug
|
|
|
|
|
.wait_for_frame_completion_before_queueing;
|
|
|
|
|
if needs_sync {
|
2023-09-14 09:33:42 +04:00
|
|
|
if let PrimaryPlaneElement::Swapchain(element) = res.primary_element {
|
|
|
|
|
let _span = tracy_client::span!("wait for completion");
|
2024-04-09 19:06:13 +04:00
|
|
|
if let Err(err) = element.sync.wait() {
|
|
|
|
|
warn!("error waiting for frame completion: {err:?}");
|
|
|
|
|
}
|
2023-09-14 09:33:42 +04:00
|
|
|
}
|
|
|
|
|
}
|
2023-09-04 09:56:28 +04:00
|
|
|
|
2023-10-01 19:41:42 +04:00
|
|
|
niri.update_primary_scanout_output(output, &res.states);
|
2024-01-03 18:16:20 +04:00
|
|
|
if let Some(dmabuf_feedback) = surface.dmabuf_feedback.as_ref() {
|
|
|
|
|
niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states);
|
|
|
|
|
}
|
2023-10-01 19:41:42 +04:00
|
|
|
|
2024-01-28 16:18:52 +01:00
|
|
|
if !res.is_empty {
|
2023-09-03 13:04:32 +04:00
|
|
|
let presentation_feedbacks =
|
|
|
|
|
niri.take_presentation_feedbacks(output, &res.states);
|
2023-09-20 11:51:25 +04:00
|
|
|
let data = (presentation_feedbacks, target_presentation_time);
|
2023-09-03 13:04:32 +04:00
|
|
|
|
2023-09-20 11:51:25 +04:00
|
|
|
match drm_compositor.queue_frame(data) {
|
2023-09-03 13:04:32 +04:00
|
|
|
Ok(()) => {
|
2023-09-29 13:12:50 +04:00
|
|
|
let output_state = niri.output_state.get_mut(output).unwrap();
|
2023-09-30 17:13:56 +04:00
|
|
|
let new_state = RedrawState::WaitingForVBlank {
|
|
|
|
|
redraw_needed: false,
|
|
|
|
|
};
|
|
|
|
|
match mem::replace(&mut output_state.redraw_state, new_state) {
|
|
|
|
|
RedrawState::Idle => unreachable!(),
|
2024-03-23 12:46:26 +04:00
|
|
|
RedrawState::Queued => (),
|
2023-09-30 17:13:56 +04:00
|
|
|
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
|
|
|
|
RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(),
|
2024-03-23 12:46:26 +04:00
|
|
|
RedrawState::WaitingForEstimatedVBlankAndQueued(token) => {
|
2023-09-30 17:13:56 +04:00
|
|
|
niri.event_loop.remove(token);
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-09-03 15:15:55 +04:00
|
|
|
|
2024-03-02 08:10:05 +04:00
|
|
|
// We queued this frame successfully, so the current client buffers were
|
|
|
|
|
// latched. We can send frame callbacks now, since a new client commit
|
|
|
|
|
// will no longer overwrite this frame and will wait for a VBlank.
|
|
|
|
|
output_state.frame_callback_sequence =
|
|
|
|
|
output_state.frame_callback_sequence.wrapping_add(1);
|
2024-03-01 12:56:55 +04:00
|
|
|
|
2023-10-13 13:04:41 +04:00
|
|
|
return RenderResult::Submitted;
|
2023-09-03 13:04:32 +04:00
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2024-02-21 09:20:34 +04:00
|
|
|
warn!("error queueing frame: {err}");
|
2023-09-03 13:04:32 +04:00
|
|
|
}
|
|
|
|
|
}
|
2023-10-13 13:04:41 +04:00
|
|
|
} else {
|
|
|
|
|
rv = RenderResult::NoDamage;
|
2023-09-03 13:04:32 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
// Can fail if we switched to a different TTY.
|
2024-02-21 09:20:34 +04:00
|
|
|
warn!("error rendering frame: {err}");
|
2023-09-03 13:04:32 +04:00
|
|
|
}
|
|
|
|
|
}
|
2023-09-03 15:15:55 +04:00
|
|
|
|
2023-09-04 08:57:11 +04:00
|
|
|
// We're not expecting a vblank right after this.
|
|
|
|
|
drop(surface.vblank_frame.take());
|
2023-09-29 13:12:50 +04:00
|
|
|
|
|
|
|
|
// Queue a timer to fire at the predicted vblank time.
|
|
|
|
|
queue_estimated_vblank_timer(niri, output.clone(), target_presentation_time);
|
2023-10-13 13:04:41 +04:00
|
|
|
|
|
|
|
|
rv
|
2023-09-03 13:04:32 +04:00
|
|
|
}
|
|
|
|
|
|
2023-09-03 14:10:02 +04:00
|
|
|
pub fn change_vt(&mut self, vt: i32) {
|
2023-08-09 14:21:29 +04:00
|
|
|
if let Err(err) = self.session.change_vt(vt) {
|
2024-02-21 09:20:34 +04:00
|
|
|
warn!("error changing VT: {err}");
|
2023-08-09 14:21:29 +04:00
|
|
|
}
|
|
|
|
|
}
|
2023-09-03 14:10:02 +04:00
|
|
|
|
|
|
|
|
pub fn suspend(&self) {
|
2023-10-10 10:25:26 +04:00
|
|
|
#[cfg(feature = "dbus")]
|
2023-09-03 14:10:02 +04:00
|
|
|
if let Err(err) = suspend() {
|
|
|
|
|
warn!("error suspending: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn toggle_debug_tint(&mut self) {
|
2024-02-21 07:55:10 +04:00
|
|
|
self.debug_tint = !self.debug_tint;
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
for device in self.devices.values_mut() {
|
2023-09-04 07:49:50 +04:00
|
|
|
for surface in device.surfaces.values_mut() {
|
|
|
|
|
let compositor = &mut surface.compositor;
|
2024-02-21 07:55:10 +04:00
|
|
|
|
|
|
|
|
let mut flags = compositor.debug_flags();
|
|
|
|
|
flags.set(DebugFlags::TINT, self.debug_tint);
|
|
|
|
|
compositor.set_debug_flags(flags);
|
2023-09-03 14:10:02 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-03 14:22:04 +04:00
|
|
|
|
2024-02-05 17:31:11 +04:00
|
|
|
pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool {
|
2024-01-03 18:16:20 +04:00
|
|
|
let mut renderer = match self.gpu_manager.single_renderer(&self.primary_render_node) {
|
|
|
|
|
Ok(renderer) => renderer,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
debug!("error creating renderer for primary GPU: {err:?}");
|
2024-02-05 17:31:11 +04:00
|
|
|
return false;
|
2024-01-03 18:16:20 +04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match renderer.import_dmabuf(dmabuf, None) {
|
2024-02-18 16:43:33 +04:00
|
|
|
Ok(_texture) => {
|
|
|
|
|
dmabuf.set_node(Some(self.primary_render_node));
|
|
|
|
|
true
|
|
|
|
|
}
|
2024-01-03 11:16:47 +04:00
|
|
|
Err(err) => {
|
|
|
|
|
debug!("error importing dmabuf: {err:?}");
|
2024-02-05 17:31:11 +04:00
|
|
|
false
|
2024-01-03 11:16:47 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
pub fn early_import(&mut self, surface: &WlSurface) {
|
|
|
|
|
if let Err(err) = self.gpu_manager.early_import(
|
|
|
|
|
// We always render on the primary GPU.
|
|
|
|
|
self.primary_render_node,
|
|
|
|
|
surface,
|
|
|
|
|
) {
|
|
|
|
|
warn!("error doing early import: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 08:32:23 +04:00
|
|
|
pub fn get_gamma_size(&self, output: &Output) -> anyhow::Result<u32> {
|
2024-03-03 20:36:13 +01:00
|
|
|
let tty_state = output.user_data().get::<TtyOutputState>().unwrap();
|
2024-03-14 08:32:23 +04:00
|
|
|
let crtc = tty_state.crtc;
|
2024-03-03 20:36:13 +01:00
|
|
|
|
2024-03-14 08:32:23 +04:00
|
|
|
let device = self
|
|
|
|
|
.devices
|
|
|
|
|
.get(&tty_state.node)
|
|
|
|
|
.context("missing device")?;
|
2024-03-15 08:54:36 +04:00
|
|
|
|
|
|
|
|
let surface = device.surfaces.get(&crtc).context("missing surface")?;
|
|
|
|
|
if let Some(gamma_props) = &surface.gamma_props {
|
|
|
|
|
gamma_props.gamma_size(&device.drm)
|
|
|
|
|
} else {
|
|
|
|
|
let info = device
|
|
|
|
|
.drm
|
|
|
|
|
.get_crtc(crtc)
|
|
|
|
|
.context("error getting crtc info")?;
|
|
|
|
|
Ok(info.gamma_length())
|
|
|
|
|
}
|
2024-03-03 20:36:13 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-15 11:10:54 +04:00
|
|
|
pub fn set_gamma(&mut self, output: &Output, ramp: Option<Vec<u16>>) -> anyhow::Result<()> {
|
2024-03-03 20:36:13 +01:00
|
|
|
let tty_state = output.user_data().get::<TtyOutputState>().unwrap();
|
2024-03-15 08:54:36 +04:00
|
|
|
let crtc = tty_state.crtc;
|
|
|
|
|
|
2024-03-03 20:36:13 +01:00
|
|
|
let device = self
|
|
|
|
|
.devices
|
2024-03-15 08:54:36 +04:00
|
|
|
.get_mut(&tty_state.node)
|
2024-03-03 20:36:13 +01:00
|
|
|
.context("missing device")?;
|
2024-03-15 08:54:36 +04:00
|
|
|
let surface = device.surfaces.get_mut(&crtc).context("missing surface")?;
|
2024-03-15 11:10:54 +04:00
|
|
|
|
|
|
|
|
// Cannot change properties while the device is inactive.
|
|
|
|
|
if !self.session.is_active() {
|
|
|
|
|
surface.pending_gamma_change = Some(ramp);
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ramp = ramp.as_deref();
|
2024-03-15 08:54:36 +04:00
|
|
|
if let Some(gamma_props) = &mut surface.gamma_props {
|
|
|
|
|
gamma_props.set_gamma(&device.drm, ramp)
|
|
|
|
|
} else {
|
|
|
|
|
set_gamma_for_crtc(&device.drm, crtc, ramp)
|
|
|
|
|
}
|
2024-03-03 20:36:13 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
fn refresh_ipc_outputs(&self, niri: &mut Niri) {
|
2024-01-17 10:38:32 +04:00
|
|
|
let _span = tracy_client::span!("Tty::refresh_ipc_outputs");
|
|
|
|
|
|
|
|
|
|
let mut ipc_outputs = HashMap::new();
|
2025-01-04 15:37:54 +03:00
|
|
|
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
2024-01-17 10:38:32 +04:00
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
for (node, device) in &self.devices {
|
2024-01-17 10:38:32 +04:00
|
|
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
2024-09-03 12:13:04 +03:00
|
|
|
let connector_name = format_connector_name(connector);
|
2024-01-17 10:38:32 +04:00
|
|
|
let physical_size = connector.size();
|
2025-01-04 15:37:54 +03:00
|
|
|
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
2024-01-17 10:38:32 +04:00
|
|
|
|
2024-02-14 21:14:01 +04:00
|
|
|
let surface = device.surfaces.get(&crtc);
|
|
|
|
|
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
|
|
|
|
|
let mut current_mode = None;
|
2025-10-29 07:10:38 +01:00
|
|
|
let mut is_custom_mode = false;
|
2024-02-14 21:14:01 +04:00
|
|
|
|
2025-10-29 07:10:38 +01:00
|
|
|
let mut modes: Vec<niri_ipc::Mode> = connector
|
2024-01-17 10:38:32 +04:00
|
|
|
.modes()
|
|
|
|
|
.iter()
|
2024-02-14 21:14:01 +04:00
|
|
|
.filter(|m| !m.flags().contains(ModeFlags::INTERLACE))
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(idx, m)| {
|
|
|
|
|
if Some(*m) == current_crtc_mode {
|
|
|
|
|
current_mode = Some(idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
niri_ipc::Mode {
|
|
|
|
|
width: m.size().0,
|
|
|
|
|
height: m.size().1,
|
|
|
|
|
refresh_rate: Mode::from(*m).refresh as u32,
|
2024-03-27 14:54:24 +04:00
|
|
|
is_preferred: m.mode_type().contains(ModeTypeFlags::PREFERRED),
|
2024-02-14 21:14:01 +04:00
|
|
|
}
|
2024-01-17 10:38:32 +04:00
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
2024-02-14 21:14:01 +04:00
|
|
|
if let Some(crtc_mode) = current_crtc_mode {
|
2025-10-29 07:10:38 +01:00
|
|
|
// Custom mode
|
|
|
|
|
if crtc_mode.mode_type().contains(ModeTypeFlags::USERDEF) {
|
|
|
|
|
modes.insert(
|
|
|
|
|
0,
|
|
|
|
|
niri_ipc::Mode {
|
|
|
|
|
width: crtc_mode.size().0,
|
|
|
|
|
height: crtc_mode.size().1,
|
|
|
|
|
refresh_rate: Mode::from(crtc_mode).refresh as u32,
|
|
|
|
|
is_preferred: false,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
current_mode = Some(0);
|
|
|
|
|
is_custom_mode = true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 21:14:01 +04:00
|
|
|
if current_mode.is_none() {
|
|
|
|
|
if crtc_mode.flags().contains(ModeFlags::INTERLACE) {
|
|
|
|
|
warn!("connector mode list missing current mode (interlaced)");
|
|
|
|
|
} else {
|
|
|
|
|
error!("connector mode list missing current mode");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-14 13:15:14 +03:00
|
|
|
let vrr_supported = surface
|
|
|
|
|
.map(|surface| {
|
|
|
|
|
matches!(
|
|
|
|
|
surface.compositor.vrr_supported(connector.handle()),
|
|
|
|
|
Ok(VrrSupport::Supported | VrrSupport::RequiresModeset)
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| {
|
|
|
|
|
is_vrr_capable(&device.drm, connector.handle()) == Some(true)
|
|
|
|
|
});
|
2025-01-10 09:01:23 +03:00
|
|
|
let vrr_enabled = surface.is_some_and(|surface| surface.compositor.vrr_enabled());
|
2024-04-15 22:29:25 +04:00
|
|
|
|
2024-03-27 14:54:24 +04:00
|
|
|
let logical = niri
|
2024-03-27 09:46:18 +04:00
|
|
|
.global_space
|
|
|
|
|
.outputs()
|
|
|
|
|
.find(|output| {
|
|
|
|
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
|
|
|
|
tty_state.node == *node && tty_state.crtc == crtc
|
|
|
|
|
})
|
2024-03-27 14:54:24 +04:00
|
|
|
.map(logical_output);
|
2024-03-27 09:46:18 +04:00
|
|
|
|
2025-01-04 15:37:54 +03:00
|
|
|
let id = device.known_crtcs.get(&crtc).map(|info| info.id);
|
|
|
|
|
let id = id.unwrap_or_else(|| {
|
|
|
|
|
error!("crtc for connector {connector_name} missing from known");
|
|
|
|
|
OutputId::next()
|
|
|
|
|
});
|
|
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
let ipc_output = niri_ipc::Output {
|
2024-09-03 12:13:04 +03:00
|
|
|
name: connector_name,
|
|
|
|
|
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
|
|
|
|
|
model: output_name.model.unwrap_or_else(|| "Unknown".into()),
|
|
|
|
|
serial: output_name.serial,
|
2024-01-17 10:38:32 +04:00
|
|
|
physical_size,
|
|
|
|
|
modes,
|
2024-02-14 21:14:01 +04:00
|
|
|
current_mode,
|
2025-10-29 07:10:38 +01:00
|
|
|
is_custom_mode,
|
2024-04-15 22:29:25 +04:00
|
|
|
vrr_supported,
|
|
|
|
|
vrr_enabled,
|
2024-03-27 14:54:24 +04:00
|
|
|
logical,
|
2024-01-17 10:38:32 +04:00
|
|
|
};
|
|
|
|
|
|
2024-07-04 13:49:33 +04:00
|
|
|
ipc_outputs.insert(id, ipc_output);
|
2024-01-17 10:38:32 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-27 08:27:14 +04:00
|
|
|
let mut guard = self.ipc_outputs.lock().unwrap();
|
|
|
|
|
*guard = ipc_outputs;
|
2024-03-27 09:46:18 +04:00
|
|
|
niri.ipc_outputs_changed = true;
|
2024-01-17 10:38:32 +04:00
|
|
|
}
|
|
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
pub fn ipc_outputs(&self) -> Arc<Mutex<IpcOutputMap>> {
|
2024-01-17 10:38:32 +04:00
|
|
|
self.ipc_outputs.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-30 09:58:34 +04:00
|
|
|
#[cfg(feature = "xdp-gnome-screencast")]
|
2024-01-03 18:16:20 +04:00
|
|
|
pub fn primary_gbm_device(&self) -> Option<GbmDevice<DrmDeviceFd>> {
|
2024-05-15 08:13:56 +04:00
|
|
|
// Try to find a device corresponding to the primary render node.
|
|
|
|
|
let device = self
|
|
|
|
|
.devices
|
|
|
|
|
.values()
|
2025-11-20 12:19:43 +03:00
|
|
|
.find(|d| d.render_node == Some(self.primary_render_node));
|
2024-05-15 08:13:56 +04:00
|
|
|
// Otherwise, try to get the device corresponding to the primary node.
|
|
|
|
|
let device = device.or_else(|| self.devices.get(&self.primary_node));
|
|
|
|
|
|
|
|
|
|
Some(device?.gbm.clone())
|
2023-09-08 17:54:02 +04:00
|
|
|
}
|
2023-09-26 10:42:56 +04:00
|
|
|
|
2024-01-28 16:18:52 +01:00
|
|
|
pub fn set_monitors_active(&mut self, active: bool) {
|
|
|
|
|
// We only disable the CRTC here, this will also reset the
|
|
|
|
|
// surface state so that the next call to `render_frame` will
|
|
|
|
|
// always produce a new frame and `queue_frame` will change
|
|
|
|
|
// the CRTC to active. This makes sure we always enable a CRTC
|
|
|
|
|
// within an atomic operation.
|
|
|
|
|
if active {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for device in self.devices.values_mut() {
|
2024-10-20 20:15:06 +03:00
|
|
|
for surface in device.surfaces.values_mut() {
|
|
|
|
|
if let Err(err) = surface.compositor.clear() {
|
|
|
|
|
warn!("error clearing drm surface: {err:?}");
|
2024-01-28 16:18:52 +01:00
|
|
|
}
|
2024-01-03 18:16:20 +04:00
|
|
|
}
|
2023-10-09 18:37:43 +04:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-16 15:34:00 +04:00
|
|
|
|
2024-08-22 18:58:07 +10:00
|
|
|
pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
|
|
|
|
|
let _span = tracy_client::span!("Tty::set_output_on_demand_vrr");
|
|
|
|
|
|
|
|
|
|
let output_state = niri.output_state.get_mut(output).unwrap();
|
|
|
|
|
output_state.on_demand_vrr_enabled = enable_vrr;
|
|
|
|
|
if output_state.frame_clock.vrr() == enable_vrr {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (&node, device) in self.devices.iter_mut() {
|
|
|
|
|
for (&crtc, surface) in device.surfaces.iter_mut() {
|
|
|
|
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
|
|
|
|
if tty_state.node == node && tty_state.crtc == crtc {
|
2024-12-14 13:15:14 +03:00
|
|
|
let word = if enable_vrr { "enabling" } else { "disabling" };
|
|
|
|
|
if let Err(err) = surface.compositor.use_vrr(enable_vrr) {
|
|
|
|
|
warn!(
|
|
|
|
|
"output {:?}: error {} VRR: {err:?}",
|
|
|
|
|
surface.name.connector, word
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
output_state
|
|
|
|
|
.frame_clock
|
|
|
|
|
.set_vrr(surface.compositor.vrr_enabled());
|
|
|
|
|
|
2024-08-22 18:58:07 +10:00
|
|
|
self.refresh_ipc_outputs(niri);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 08:53:37 +03:00
|
|
|
pub fn update_ignored_nodes_config(&mut self, niri: &mut Niri) {
|
|
|
|
|
let _span = tracy_client::span!("Tty::update_ignored_nodes_config");
|
2024-01-17 10:21:40 +04:00
|
|
|
|
2024-01-21 10:25:39 +04:00
|
|
|
// If we're inactive, we can't do anything, so just set a flag for later.
|
|
|
|
|
if !self.session.is_active() {
|
2025-11-17 08:53:37 +03:00
|
|
|
self.update_ignored_nodes_on_resume = true;
|
2024-01-21 10:25:39 +04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-30 20:42:46 -04:00
|
|
|
let mut ignored_nodes = ignored_nodes_from_config(&self.config.borrow());
|
|
|
|
|
if ignored_nodes.remove(&self.primary_node)
|
|
|
|
|
|| ignored_nodes.remove(&self.primary_render_node)
|
|
|
|
|
{
|
|
|
|
|
warn!("ignoring the primary node or render node is not allowed");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 08:53:37 +03:00
|
|
|
if ignored_nodes == self.ignored_nodes {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.ignored_nodes = ignored_nodes;
|
2025-08-30 20:42:46 -04:00
|
|
|
|
2025-11-17 08:53:37 +03:00
|
|
|
let mut device_list = self
|
|
|
|
|
.udev_dispatcher
|
|
|
|
|
.as_source_ref()
|
|
|
|
|
.device_list()
|
|
|
|
|
.map(|(device_id, path)| (device_id, path.to_owned()))
|
|
|
|
|
.collect::<HashMap<_, _>>();
|
2025-08-30 20:42:46 -04:00
|
|
|
|
2025-11-17 08:53:37 +03:00
|
|
|
let removed_devices = self
|
|
|
|
|
.devices
|
|
|
|
|
.keys()
|
|
|
|
|
.filter(|node| {
|
|
|
|
|
self.ignored_nodes.contains(node) || !device_list.contains_key(&node.dev_id())
|
|
|
|
|
})
|
|
|
|
|
.copied()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
for node in removed_devices {
|
|
|
|
|
device_list.remove(&node.dev_id());
|
|
|
|
|
self.device_removed(node.dev_id(), niri);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for node in self.devices.keys() {
|
|
|
|
|
device_list.remove(&node.dev_id());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (device_id, path) in device_list {
|
|
|
|
|
if let Err(err) = self.device_added(device_id, &path, niri) {
|
|
|
|
|
warn!("error adding device {path:?}: {err:?}");
|
2025-08-30 20:42:46 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-17 08:53:37 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-19 08:44:35 +03:00
|
|
|
fn should_disable_laptop_panels(&self, is_lid_closed: bool) -> bool {
|
|
|
|
|
if !is_lid_closed {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let config = self.config.borrow();
|
|
|
|
|
if !config.debug.keep_laptop_panel_on_when_lid_is_closed {
|
|
|
|
|
// Check if any external monitor is connected.
|
|
|
|
|
for device in self.devices.values() {
|
|
|
|
|
for (connector, _crtc) in device.drm_scanner.crtcs() {
|
|
|
|
|
if !is_laptop_panel(&format_connector_name(connector)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 08:53:37 +03:00
|
|
|
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
|
|
|
|
|
let _span = tracy_client::span!("Tty::on_output_config_changed");
|
|
|
|
|
|
|
|
|
|
// If we're inactive, we can't do anything, so just set a flag for later.
|
|
|
|
|
if !self.session.is_active() {
|
|
|
|
|
self.update_output_config_on_resume = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
self.update_output_config_on_resume = false;
|
2025-08-30 20:42:46 -04:00
|
|
|
|
2024-11-08 09:11:56 +03:00
|
|
|
// Figure out if we should disable laptop panels.
|
2025-11-19 08:44:35 +03:00
|
|
|
let disable_laptop_panels = self.should_disable_laptop_panels(niri.is_lid_closed);
|
2024-11-08 09:11:56 +03:00
|
|
|
let should_disable = |connector: &str| disable_laptop_panels && is_laptop_panel(connector);
|
|
|
|
|
|
2024-01-16 15:34:00 +04:00
|
|
|
let mut to_disconnect = vec![];
|
|
|
|
|
let mut to_connect = vec![];
|
|
|
|
|
|
2024-01-16 18:01:45 +04:00
|
|
|
for (&node, device) in &mut self.devices {
|
2024-08-22 18:58:07 +10:00
|
|
|
for (&crtc, surface) in device.surfaces.iter_mut() {
|
2024-01-16 15:34:00 +04:00
|
|
|
let config = self
|
|
|
|
|
.config
|
|
|
|
|
.borrow()
|
|
|
|
|
.outputs
|
2024-07-04 17:51:11 +04:00
|
|
|
.find(&surface.name)
|
2024-01-16 15:34:00 +04:00
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_default();
|
2024-11-08 09:11:56 +03:00
|
|
|
if config.off || should_disable(&surface.name.connector) {
|
2024-01-16 18:01:45 +04:00
|
|
|
to_disconnect.push((node, crtc));
|
|
|
|
|
continue;
|
2024-01-16 15:34:00 +04:00
|
|
|
}
|
2024-01-16 18:01:45 +04:00
|
|
|
|
|
|
|
|
// Check if we need to change the mode.
|
2024-08-22 18:58:07 +10:00
|
|
|
let Some(connector) = device.drm_scanner.connectors().get(&surface.connector)
|
2024-01-21 10:24:42 +04:00
|
|
|
else {
|
2024-01-16 18:01:45 +04:00
|
|
|
error!("missing enabled connector in drm_scanner");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-29 07:10:38 +01:00
|
|
|
let mut mode = None;
|
|
|
|
|
if let Some(modeline) = &config.modeline {
|
|
|
|
|
match calculate_drm_mode_from_modeline(modeline) {
|
|
|
|
|
Ok(x) => mode = Some(x),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!(
|
|
|
|
|
"output {:?}: invalid custom modeline; \
|
|
|
|
|
falling back to advertised modes: {err:?}",
|
|
|
|
|
surface.name.connector
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (mode, fallback) = match mode {
|
|
|
|
|
Some(x) => (x, false),
|
|
|
|
|
None => match pick_mode(connector, config.mode) {
|
|
|
|
|
Some(result) => result,
|
|
|
|
|
None => {
|
|
|
|
|
warn!("couldn't pick mode for enabled connector");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-01-16 18:01:45 +04:00
|
|
|
};
|
|
|
|
|
|
2024-04-14 09:23:15 +04:00
|
|
|
let change_mode = surface.compositor.pending_mode() != mode;
|
2024-12-14 13:15:14 +03:00
|
|
|
|
|
|
|
|
let vrr_enabled = surface.compositor.vrr_enabled();
|
|
|
|
|
let change_always_vrr = vrr_enabled != config.is_vrr_always_on();
|
2024-08-22 18:58:07 +10:00
|
|
|
let is_on_demand_vrr = config.is_vrr_on_demand();
|
2024-12-14 13:15:14 +03:00
|
|
|
|
2024-08-22 18:58:07 +10:00
|
|
|
if !change_mode && !change_always_vrr && !is_on_demand_vrr {
|
2024-01-16 18:01:45 +04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let output = niri
|
|
|
|
|
.global_space
|
|
|
|
|
.outputs()
|
|
|
|
|
.find(|output| {
|
|
|
|
|
let tty_state: &TtyOutputState = output.user_data().get().unwrap();
|
|
|
|
|
tty_state.node == node && tty_state.crtc == crtc
|
|
|
|
|
})
|
|
|
|
|
.cloned();
|
|
|
|
|
let Some(output) = output else {
|
|
|
|
|
error!("missing output for crtc: {crtc:?}");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let Some(output_state) = niri.output_state.get_mut(&output) else {
|
2024-09-03 12:13:04 +03:00
|
|
|
error!("missing state for output {:?}", surface.name.connector);
|
2024-01-16 18:01:45 +04:00
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-14 13:15:14 +03:00
|
|
|
if (is_on_demand_vrr && vrr_enabled != output_state.on_demand_vrr_enabled)
|
2024-08-22 18:58:07 +10:00
|
|
|
|| (!is_on_demand_vrr && change_always_vrr)
|
|
|
|
|
{
|
2024-12-14 13:15:14 +03:00
|
|
|
let vrr = !vrr_enabled;
|
|
|
|
|
let word = if vrr { "enabling" } else { "disabling" };
|
|
|
|
|
if let Err(err) = surface.compositor.use_vrr(vrr) {
|
|
|
|
|
warn!(
|
|
|
|
|
"output {:?}: error {} VRR: {err:?}",
|
|
|
|
|
surface.name.connector, word
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
output_state
|
|
|
|
|
.frame_clock
|
|
|
|
|
.set_vrr(surface.compositor.vrr_enabled());
|
2024-01-16 18:01:45 +04:00
|
|
|
}
|
|
|
|
|
|
2024-04-14 09:23:15 +04:00
|
|
|
if change_mode {
|
|
|
|
|
if fallback {
|
|
|
|
|
let target = config.mode.unwrap();
|
|
|
|
|
warn!(
|
|
|
|
|
"output {:?}: configured mode {}x{}{} could not be found, \
|
|
|
|
|
falling back to preferred",
|
2024-09-03 12:13:04 +03:00
|
|
|
surface.name.connector,
|
2025-10-29 07:10:38 +01:00
|
|
|
target.mode.width,
|
|
|
|
|
target.mode.height,
|
|
|
|
|
if let Some(refresh) = target.mode.refresh {
|
2024-04-14 09:23:15 +04:00
|
|
|
format!("@{refresh}")
|
|
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-03 12:13:04 +03:00
|
|
|
debug!(
|
|
|
|
|
"output {:?}: picking mode: {mode:?}",
|
|
|
|
|
surface.name.connector
|
|
|
|
|
);
|
2024-04-14 09:23:15 +04:00
|
|
|
if let Err(err) = surface.compositor.use_mode(mode) {
|
|
|
|
|
warn!("error changing mode: {err:?}");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let wl_mode = Mode::from(mode);
|
|
|
|
|
output.change_current_state(Some(wl_mode), None, None, None);
|
|
|
|
|
output.set_preferred(wl_mode);
|
2024-12-14 13:15:14 +03:00
|
|
|
output_state.frame_clock = FrameClock::new(
|
|
|
|
|
Some(refresh_interval(mode)),
|
|
|
|
|
surface.compositor.vrr_enabled(),
|
|
|
|
|
);
|
2024-04-14 09:23:15 +04:00
|
|
|
niri.output_resized(&output);
|
|
|
|
|
}
|
2024-01-16 15:34:00 +04:00
|
|
|
}
|
|
|
|
|
|
2025-01-04 15:37:54 +03:00
|
|
|
let config = self.config.borrow();
|
|
|
|
|
let disable_monitor_names = config.debug.disable_monitor_names;
|
|
|
|
|
|
2024-01-16 15:34:00 +04:00
|
|
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
|
|
|
|
// Check if connected.
|
|
|
|
|
if connector.state() != connector::State::Connected {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if already enabled.
|
2024-11-08 09:11:56 +03:00
|
|
|
if device.surfaces.contains_key(&crtc)
|
|
|
|
|
|| device
|
|
|
|
|
.non_desktop_connectors
|
|
|
|
|
.contains(&(connector.handle(), crtc))
|
|
|
|
|
{
|
2024-01-16 15:34:00 +04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 15:37:54 +03:00
|
|
|
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
|
|
|
|
|
|
|
|
|
let config = config
|
2024-01-16 15:34:00 +04:00
|
|
|
.outputs
|
2024-07-04 17:51:11 +04:00
|
|
|
.find(&output_name)
|
2024-01-16 15:34:00 +04:00
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
2024-11-08 09:11:56 +03:00
|
|
|
if !(config.off || should_disable(&output_name.connector)) {
|
|
|
|
|
to_connect.push((node, connector.clone(), crtc, output_name));
|
2024-01-16 15:34:00 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (node, crtc) in to_disconnect {
|
|
|
|
|
self.connector_disconnected(niri, node, crtc);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-08 09:11:56 +03:00
|
|
|
// Sort by output name to get more predictable first focused output at initial compositor
|
|
|
|
|
// startup, when multiple connectors appear at once.
|
|
|
|
|
to_connect.sort_unstable_by(|a, b| a.3.compare(&b.3));
|
|
|
|
|
|
|
|
|
|
for (node, connector, crtc, _name) in to_connect {
|
2024-01-16 15:34:00 +04:00
|
|
|
if let Err(err) = self.connector_connected(niri, node, connector, crtc) {
|
|
|
|
|
warn!("error connecting connector: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-17 10:38:32 +04:00
|
|
|
|
2024-03-27 09:46:18 +04:00
|
|
|
self.refresh_ipc_outputs(niri);
|
2024-01-16 15:34:00 +04:00
|
|
|
}
|
2024-02-11 13:15:03 -05:00
|
|
|
|
|
|
|
|
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
|
|
|
|
|
self.devices.get_mut(&node)
|
|
|
|
|
}
|
2024-09-03 12:13:04 +03:00
|
|
|
|
|
|
|
|
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
|
2025-01-04 15:37:54 +03:00
|
|
|
let disable_monitor_names = self.config.borrow().debug.disable_monitor_names;
|
2024-09-03 12:13:04 +03:00
|
|
|
for device in self.devices.values() {
|
|
|
|
|
for (connector, crtc) in device.drm_scanner.crtcs() {
|
|
|
|
|
// Check if connected.
|
|
|
|
|
if connector.state() != connector::State::Connected {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if already enabled.
|
2024-11-08 09:11:56 +03:00
|
|
|
if device.surfaces.contains_key(&crtc)
|
|
|
|
|
|| device
|
|
|
|
|
.non_desktop_connectors
|
|
|
|
|
.contains(&(connector.handle(), crtc))
|
|
|
|
|
{
|
2024-09-03 12:13:04 +03:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-04 15:37:54 +03:00
|
|
|
let output_name = device.known_crtc_name(&crtc, connector, disable_monitor_names);
|
2024-09-03 12:13:04 +03:00
|
|
|
if output_name.matches(target) {
|
|
|
|
|
return Some(output_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
2023-10-09 18:37:43 +04:00
|
|
|
}
|
|
|
|
|
|
2024-03-15 08:54:36 +04:00
|
|
|
impl GammaProps {
|
|
|
|
|
fn new(device: &DrmDevice, crtc: crtc::Handle) -> anyhow::Result<Self> {
|
|
|
|
|
let mut gamma_lut = None;
|
|
|
|
|
let mut gamma_lut_size = None;
|
|
|
|
|
|
|
|
|
|
let props = device
|
|
|
|
|
.get_properties(crtc)
|
|
|
|
|
.context("error getting properties")?;
|
|
|
|
|
for (prop, _) in props {
|
|
|
|
|
let Ok(info) = device.get_property(prop) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Ok(name) = info.name().to_str() else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match name {
|
|
|
|
|
"GAMMA_LUT" => {
|
|
|
|
|
ensure!(
|
|
|
|
|
matches!(info.value_type(), property::ValueType::Blob),
|
|
|
|
|
"wrong GAMMA_LUT value type"
|
|
|
|
|
);
|
|
|
|
|
gamma_lut = Some(prop);
|
|
|
|
|
}
|
|
|
|
|
"GAMMA_LUT_SIZE" => {
|
|
|
|
|
ensure!(
|
|
|
|
|
matches!(info.value_type(), property::ValueType::UnsignedRange(_, _)),
|
|
|
|
|
"wrong GAMMA_LUT_SIZE value type"
|
|
|
|
|
);
|
|
|
|
|
gamma_lut_size = Some(prop);
|
|
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let gamma_lut = gamma_lut.context("missing GAMMA_LUT property")?;
|
|
|
|
|
let gamma_lut_size = gamma_lut_size.context("missing GAMMA_LUT_SIZE property")?;
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
crtc,
|
|
|
|
|
gamma_lut,
|
|
|
|
|
gamma_lut_size,
|
|
|
|
|
previous_blob: None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn gamma_size(&self, device: &DrmDevice) -> anyhow::Result<u32> {
|
|
|
|
|
let value = get_drm_property(device, self.crtc, self.gamma_lut_size)
|
|
|
|
|
.context("missing GAMMA_LUT_SIZE property")?;
|
|
|
|
|
Ok(value as u32)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_gamma(&mut self, device: &DrmDevice, gamma: Option<&[u16]>) -> anyhow::Result<()> {
|
|
|
|
|
let _span = tracy_client::span!("GammaProps::set_gamma");
|
|
|
|
|
|
|
|
|
|
let blob = if let Some(gamma) = gamma {
|
|
|
|
|
let gamma_size = self
|
|
|
|
|
.gamma_size(device)
|
|
|
|
|
.context("error getting gamma size")? as usize;
|
|
|
|
|
|
|
|
|
|
ensure!(gamma.len() == gamma_size * 3, "wrong gamma length");
|
|
|
|
|
|
|
|
|
|
#[allow(non_camel_case_types)]
|
|
|
|
|
#[repr(C)]
|
|
|
|
|
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
|
|
|
|
pub struct drm_color_lut {
|
|
|
|
|
pub red: u16,
|
|
|
|
|
pub green: u16,
|
|
|
|
|
pub blue: u16,
|
|
|
|
|
pub reserved: u16,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (red, rest) = gamma.split_at(gamma_size);
|
|
|
|
|
let (blue, green) = rest.split_at(gamma_size);
|
|
|
|
|
let mut data = zip(zip(red, blue), green)
|
|
|
|
|
.map(|((&red, &green), &blue)| drm_color_lut {
|
|
|
|
|
red,
|
|
|
|
|
green,
|
|
|
|
|
blue,
|
|
|
|
|
reserved: 0,
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let data = cast_slice_mut(&mut data);
|
|
|
|
|
|
|
|
|
|
let blob = drm_ffi::mode::create_property_blob(device.as_fd(), data)
|
|
|
|
|
.context("error creating property blob")?;
|
|
|
|
|
NonZeroU64::new(u64::from(blob.blob_id))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let _span = tracy_client::span!("set_property");
|
|
|
|
|
|
|
|
|
|
let blob = blob.map(NonZeroU64::get).unwrap_or(0);
|
|
|
|
|
device
|
|
|
|
|
.set_property(
|
|
|
|
|
self.crtc,
|
|
|
|
|
self.gamma_lut,
|
|
|
|
|
property::Value::Blob(blob).into(),
|
|
|
|
|
)
|
|
|
|
|
.context("error setting GAMMA_LUT")
|
2024-09-05 20:40:11 +03:00
|
|
|
.inspect_err(|_| {
|
2024-03-15 08:54:36 +04:00
|
|
|
if blob != 0 {
|
|
|
|
|
// Destroy the blob we just allocated.
|
|
|
|
|
if let Err(err) = device.destroy_property_blob(blob) {
|
|
|
|
|
warn!("error destroying GAMMA_LUT property blob: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(blob) = mem::replace(&mut self.previous_blob, blob) {
|
|
|
|
|
if let Err(err) = device.destroy_property_blob(blob.get()) {
|
|
|
|
|
warn!("error destroying previous GAMMA_LUT blob: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-03-15 22:02:29 +04:00
|
|
|
|
|
|
|
|
fn restore_gamma(&self, device: &DrmDevice) -> anyhow::Result<()> {
|
|
|
|
|
let _span = tracy_client::span!("GammaProps::restore_gamma");
|
|
|
|
|
|
|
|
|
|
let blob = self.previous_blob.map(NonZeroU64::get).unwrap_or(0);
|
|
|
|
|
device
|
|
|
|
|
.set_property(
|
|
|
|
|
self.crtc,
|
|
|
|
|
self.gamma_lut,
|
|
|
|
|
property::Value::Blob(blob).into(),
|
|
|
|
|
)
|
|
|
|
|
.context("error setting GAMMA_LUT")?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2024-03-15 08:54:36 +04:00
|
|
|
}
|
|
|
|
|
|
2025-08-30 20:42:46 -04:00
|
|
|
fn primary_node_from_render_node(path: &Path) -> Option<(DrmNode, DrmNode)> {
|
2024-01-06 09:14:48 +04:00
|
|
|
match DrmNode::from_path(path) {
|
|
|
|
|
Ok(node) => {
|
|
|
|
|
if node.ty() == NodeType::Render {
|
|
|
|
|
match node.node_with_type(NodeType::Primary) {
|
|
|
|
|
Some(Ok(primary_node)) => {
|
|
|
|
|
return Some((primary_node, node));
|
|
|
|
|
}
|
|
|
|
|
Some(Err(err)) => {
|
|
|
|
|
warn!("error opening primary node for render node {path:?}: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
warn!("error opening primary node for render node {path:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
warn!("DRM node {path:?} is not a render node");
|
2024-03-29 09:01:54 +04:00
|
|
|
|
|
|
|
|
// Gracefully handle misconfiguration on regular desktop systems.
|
|
|
|
|
if let Some(Ok(render_node)) = node.node_with_type(NodeType::Render) {
|
|
|
|
|
return Some((node, render_node));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
warn!("could not get render node for DRM node {path:?}; proceeding anyway");
|
|
|
|
|
return Some((node, node));
|
2024-01-06 09:14:48 +04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error opening {path:?} as DRM node: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-30 20:42:46 -04:00
|
|
|
|
2024-01-06 09:14:48 +04:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-30 20:42:46 -04:00
|
|
|
fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> {
|
|
|
|
|
let path = config.debug.render_drm_device.as_ref()?;
|
|
|
|
|
debug!("attempting to use render node from config: {path:?}");
|
|
|
|
|
|
|
|
|
|
primary_node_from_render_node(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn ignored_nodes_from_config(config: &Config) -> HashSet<DrmNode> {
|
|
|
|
|
let mut disabled_nodes = HashSet::new();
|
|
|
|
|
|
|
|
|
|
for path in &config.debug.ignored_drm_devices {
|
|
|
|
|
if let Some((primary_node, render_node)) = primary_node_from_render_node(path) {
|
|
|
|
|
disabled_nodes.insert(primary_node);
|
|
|
|
|
disabled_nodes.insert(render_node);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disabled_nodes
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
fn surface_dmabuf_feedback(
|
|
|
|
|
compositor: &GbmDrmCompositor,
|
2024-06-21 07:53:41 +03:00
|
|
|
primary_formats: FormatSet,
|
2024-01-03 18:16:20 +04:00
|
|
|
primary_render_node: DrmNode,
|
2025-11-20 12:19:43 +03:00
|
|
|
surface_render_node: Option<DrmNode>,
|
|
|
|
|
surface_scanout_node: DrmNode,
|
2024-01-03 18:16:20 +04:00
|
|
|
) -> Result<SurfaceDmabufFeedback, io::Error> {
|
|
|
|
|
let surface = compositor.surface();
|
|
|
|
|
let planes = surface.planes();
|
|
|
|
|
|
2025-01-04 09:09:24 +03:00
|
|
|
let primary_plane_formats = surface.plane_info().formats.clone();
|
|
|
|
|
let primary_or_overlay_plane_formats = primary_plane_formats
|
2024-01-03 18:16:20 +04:00
|
|
|
.iter()
|
|
|
|
|
.chain(planes.overlay.iter().flat_map(|p| p.formats.iter()))
|
|
|
|
|
.copied()
|
2024-06-21 07:53:41 +03:00
|
|
|
.collect::<FormatSet>();
|
2024-01-03 18:16:20 +04:00
|
|
|
|
|
|
|
|
// We limit the scan-out trache to formats we can also render from so that there is always a
|
|
|
|
|
// fallback render path available in case the supplied buffer can not be scanned out directly.
|
2025-01-04 09:09:24 +03:00
|
|
|
let mut primary_scanout_formats = primary_plane_formats
|
|
|
|
|
.intersection(&primary_formats)
|
|
|
|
|
.copied()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
let mut primary_or_overlay_scanout_formats = primary_or_overlay_plane_formats
|
2024-01-03 18:16:20 +04:00
|
|
|
.intersection(&primary_formats)
|
|
|
|
|
.copied()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
2024-01-05 20:21:42 +04:00
|
|
|
// HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device
|
|
|
|
|
// buffers produce a glitched scanout if the modifier is not Linear...
|
2025-11-20 12:19:43 +03:00
|
|
|
//
|
|
|
|
|
// Also limit scan-out formats to Linear if we have a device without a render node (i.e.
|
|
|
|
|
// we're rendering on a different device).
|
|
|
|
|
if surface_render_node != Some(primary_render_node) {
|
2025-01-04 09:09:24 +03:00
|
|
|
primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
|
|
|
|
primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear);
|
2024-01-05 20:21:42 +04:00
|
|
|
}
|
|
|
|
|
|
2024-01-03 18:16:20 +04:00
|
|
|
let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats);
|
|
|
|
|
|
2025-01-04 09:09:24 +03:00
|
|
|
trace!(
|
|
|
|
|
"primary scanout formats: {}, overlay adds: {}",
|
|
|
|
|
primary_scanout_formats.len(),
|
|
|
|
|
primary_or_overlay_scanout_formats.len() - primary_scanout_formats.len(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Prefer the primary-plane-only formats, then primary-or-overlay-plane formats. This will
|
|
|
|
|
// increase the chance of scanning out a client even with our disabled-by-default overlay
|
|
|
|
|
// planes.
|
2024-01-03 18:16:20 +04:00
|
|
|
let scanout = builder
|
|
|
|
|
.clone()
|
|
|
|
|
.add_preference_tranche(
|
2025-11-20 12:19:43 +03:00
|
|
|
surface_scanout_node.dev_id(),
|
2024-01-03 18:16:20 +04:00
|
|
|
Some(TrancheFlags::Scanout),
|
2025-01-04 09:09:24 +03:00
|
|
|
primary_scanout_formats,
|
|
|
|
|
)
|
|
|
|
|
.add_preference_tranche(
|
2025-11-20 12:19:43 +03:00
|
|
|
surface_scanout_node.dev_id(),
|
2025-01-04 09:09:24 +03:00
|
|
|
Some(TrancheFlags::Scanout),
|
|
|
|
|
primary_or_overlay_scanout_formats,
|
2024-01-03 18:16:20 +04:00
|
|
|
)
|
|
|
|
|
.build()?;
|
|
|
|
|
|
|
|
|
|
// If this is the primary node surface, send scanout formats in both tranches to avoid
|
|
|
|
|
// duplication.
|
2025-11-20 12:19:43 +03:00
|
|
|
let render = if surface_render_node == Some(primary_render_node) {
|
2024-01-03 18:16:20 +04:00
|
|
|
scanout.clone()
|
|
|
|
|
} else {
|
|
|
|
|
builder.build()?
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(SurfaceDmabufFeedback { render, scanout })
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-15 08:54:36 +04:00
|
|
|
fn find_drm_property(
|
|
|
|
|
drm: &DrmDevice,
|
2024-03-19 09:00:00 +04:00
|
|
|
resource: impl ResourceHandle,
|
2024-03-15 08:54:36 +04:00
|
|
|
name: &str,
|
2024-04-14 08:08:09 +04:00
|
|
|
) -> Option<(property::Handle, property::Info, property::RawValue)> {
|
2024-03-19 09:00:00 +04:00
|
|
|
let props = match drm.get_properties(resource) {
|
2023-10-09 18:37:43 +04:00
|
|
|
Ok(props) => props,
|
|
|
|
|
Err(err) => {
|
2024-03-19 09:00:00 +04:00
|
|
|
warn!("error getting properties: {err:?}");
|
2023-10-09 18:37:43 +04:00
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-03-15 08:54:36 +04:00
|
|
|
props.into_iter().find_map(|(handle, value)| {
|
|
|
|
|
let info = drm.get_property(handle).ok()?;
|
2023-10-09 18:37:43 +04:00
|
|
|
let n = info.name().to_str().ok()?;
|
|
|
|
|
|
2024-04-14 08:08:09 +04:00
|
|
|
(n == name).then_some((handle, info, value))
|
2023-10-09 18:37:43 +04:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-15 08:54:36 +04:00
|
|
|
fn get_drm_property(
|
|
|
|
|
drm: &DrmDevice,
|
2024-03-19 09:00:00 +04:00
|
|
|
resource: impl ResourceHandle,
|
2024-03-15 08:54:36 +04:00
|
|
|
prop: property::Handle,
|
|
|
|
|
) -> Option<property::RawValue> {
|
2024-03-19 09:00:00 +04:00
|
|
|
let props = match drm.get_properties(resource) {
|
2024-03-15 08:54:36 +04:00
|
|
|
Ok(props) => props,
|
|
|
|
|
Err(err) => {
|
2024-03-19 09:00:00 +04:00
|
|
|
warn!("error getting properties: {err:?}");
|
2024-03-15 08:54:36 +04:00
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
props
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find_map(|(handle, value)| (handle == prop).then_some(value))
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-14 15:53:24 +04:00
|
|
|
fn refresh_interval(mode: DrmMode) -> Duration {
|
|
|
|
|
let clock = mode.clock() as u64;
|
|
|
|
|
let htotal = mode.hsync().2 as u64;
|
|
|
|
|
let vtotal = mode.vsync().2 as u64;
|
|
|
|
|
|
|
|
|
|
let mut numerator = htotal * vtotal * 1_000_000;
|
|
|
|
|
let mut denominator = clock;
|
|
|
|
|
|
|
|
|
|
if mode.flags().contains(ModeFlags::INTERLACE) {
|
|
|
|
|
denominator *= 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mode.flags().contains(ModeFlags::DBLSCAN) {
|
|
|
|
|
numerator *= 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mode.vscan() > 1 {
|
|
|
|
|
numerator *= mode.vscan() as u64;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let refresh_interval = (numerator + denominator / 2) / denominator;
|
|
|
|
|
Duration::from_nanos(refresh_interval)
|
|
|
|
|
}
|
2023-09-03 07:31:44 +04:00
|
|
|
|
2023-10-10 10:25:26 +04:00
|
|
|
#[cfg(feature = "dbus")]
|
2023-09-03 07:31:44 +04:00
|
|
|
fn suspend() -> anyhow::Result<()> {
|
|
|
|
|
let conn = zbus::blocking::Connection::system().context("error connecting to system bus")?;
|
2024-02-10 14:58:22 +04:00
|
|
|
|
|
|
|
|
conn.call_method(
|
|
|
|
|
Some("org.freedesktop.login1"),
|
|
|
|
|
"/org/freedesktop/login1",
|
|
|
|
|
Some("org.freedesktop.login1.Manager"),
|
|
|
|
|
"Suspend",
|
|
|
|
|
&(true),
|
|
|
|
|
)
|
|
|
|
|
.context("error suspending")?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
2023-09-03 07:31:44 +04:00
|
|
|
}
|
2023-09-29 13:12:50 +04:00
|
|
|
|
|
|
|
|
fn queue_estimated_vblank_timer(
|
|
|
|
|
niri: &mut Niri,
|
|
|
|
|
output: Output,
|
|
|
|
|
target_presentation_time: Duration,
|
|
|
|
|
) {
|
|
|
|
|
let output_state = niri.output_state.get_mut(&output).unwrap();
|
2023-09-30 17:13:56 +04:00
|
|
|
match mem::take(&mut output_state.redraw_state) {
|
|
|
|
|
RedrawState::Idle => unreachable!(),
|
2024-03-23 12:46:26 +04:00
|
|
|
RedrawState::Queued => (),
|
2023-09-30 17:13:56 +04:00
|
|
|
RedrawState::WaitingForVBlank { .. } => unreachable!(),
|
|
|
|
|
RedrawState::WaitingForEstimatedVBlank(token)
|
2024-03-23 12:46:26 +04:00
|
|
|
| RedrawState::WaitingForEstimatedVBlankAndQueued(token) => {
|
2023-09-30 17:13:56 +04:00
|
|
|
output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-09-29 13:12:50 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let now = get_monotonic_time();
|
2024-03-01 08:27:44 +04:00
|
|
|
let mut duration = target_presentation_time.saturating_sub(now);
|
|
|
|
|
|
|
|
|
|
// No use setting a zero timer, since we'll send frame callbacks anyway right after the call to
|
|
|
|
|
// render(). This can happen for example with unknown presentation time from DRM.
|
|
|
|
|
if duration.is_zero() {
|
|
|
|
|
duration += output_state
|
|
|
|
|
.frame_clock
|
|
|
|
|
.refresh_interval()
|
|
|
|
|
// Unknown refresh interval, i.e. winit backend. Would be good to estimate it somehow
|
|
|
|
|
// but it's not that important for this code path.
|
|
|
|
|
.unwrap_or(Duration::from_micros(16_667));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trace!("queueing estimated vblank timer to fire in {duration:?}");
|
|
|
|
|
|
|
|
|
|
let timer = Timer::from_duration(duration);
|
2023-09-29 13:12:50 +04:00
|
|
|
let token = niri
|
|
|
|
|
.event_loop
|
|
|
|
|
.insert_source(timer, move |_, _, data| {
|
|
|
|
|
data.backend
|
|
|
|
|
.tty()
|
2023-09-30 17:13:56 +04:00
|
|
|
.on_estimated_vblank_timer(&mut data.niri, output.clone());
|
2023-09-29 13:12:50 +04:00
|
|
|
TimeoutAction::Drop
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
2023-09-30 17:13:56 +04:00
|
|
|
output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token);
|
2023-09-29 13:12:50 +04:00
|
|
|
}
|
2024-01-16 18:01:25 +04:00
|
|
|
|
2025-10-29 07:10:38 +01:00
|
|
|
pub fn calculate_drm_mode_from_modeline(modeline: &Modeline) -> anyhow::Result<DrmMode> {
|
|
|
|
|
ensure!(
|
|
|
|
|
modeline.hdisplay < modeline.hsync_start,
|
|
|
|
|
"hdisplay {} must be < hsync_start {}",
|
|
|
|
|
modeline.hdisplay,
|
|
|
|
|
modeline.hsync_start
|
|
|
|
|
);
|
|
|
|
|
ensure!(
|
|
|
|
|
modeline.hsync_start < modeline.hsync_end,
|
|
|
|
|
"hsync_start {} must be < hsync_end {}",
|
|
|
|
|
modeline.hsync_start,
|
|
|
|
|
modeline.hsync_end
|
|
|
|
|
);
|
|
|
|
|
ensure!(
|
|
|
|
|
modeline.hsync_end < modeline.htotal,
|
|
|
|
|
"hsync_end {} must be < htotal {}",
|
|
|
|
|
modeline.hsync_end,
|
|
|
|
|
modeline.htotal
|
|
|
|
|
);
|
|
|
|
|
ensure!(
|
|
|
|
|
modeline.vdisplay < modeline.vsync_start,
|
|
|
|
|
"vdisplay {} must be < vsync_start {}",
|
|
|
|
|
modeline.vdisplay,
|
|
|
|
|
modeline.vsync_start
|
|
|
|
|
);
|
|
|
|
|
ensure!(
|
|
|
|
|
modeline.vsync_start < modeline.vsync_end,
|
|
|
|
|
"vsync_start {} must be < vsync_end {}",
|
|
|
|
|
modeline.vsync_start,
|
|
|
|
|
modeline.vsync_end
|
|
|
|
|
);
|
|
|
|
|
ensure!(
|
|
|
|
|
modeline.vsync_end < modeline.vtotal,
|
|
|
|
|
"vsync_end {} must be < vtotal {}",
|
|
|
|
|
modeline.vsync_end,
|
|
|
|
|
modeline.vtotal
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let pixel_clock_kilo_hertz = modeline.clock * 1000.0;
|
|
|
|
|
// Calculated as documented in the CVT 1.2 standard:
|
|
|
|
|
// https://app.box.com/s/vcocw3z73ta09txiskj7cnk6289j356b/file/93518784646
|
|
|
|
|
let vrefresh_hertz = (pixel_clock_kilo_hertz * 1000.0)
|
|
|
|
|
/ (modeline.htotal as u64 * modeline.vtotal as u64) as f64;
|
|
|
|
|
ensure!(
|
|
|
|
|
vrefresh_hertz.is_finite(),
|
|
|
|
|
"calculated refresh rate is not finite"
|
|
|
|
|
);
|
|
|
|
|
let vrefresh_rounded = vrefresh_hertz.round() as u32;
|
|
|
|
|
|
|
|
|
|
let flags = match modeline.hsync_polarity {
|
|
|
|
|
HSyncPolarity::PHSync => ModeFlags::PHSYNC,
|
|
|
|
|
HSyncPolarity::NHSync => ModeFlags::NHSYNC,
|
|
|
|
|
} | match modeline.vsync_polarity {
|
|
|
|
|
VSyncPolarity::PVSync => ModeFlags::PVSYNC,
|
|
|
|
|
VSyncPolarity::NVSync => ModeFlags::NVSYNC,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mode_name = format!(
|
|
|
|
|
"{}x{}@{:.2}",
|
|
|
|
|
modeline.hdisplay, modeline.vdisplay, vrefresh_hertz
|
|
|
|
|
);
|
|
|
|
|
let name = modeinfo_name_slice_from_string(&mode_name);
|
|
|
|
|
|
|
|
|
|
// https://www.kernel.org/doc/html/v6.17/gpu/drm-uapi.html#c.drm_mode_modeinfo
|
|
|
|
|
Ok(DrmMode::from(drm_mode_modeinfo {
|
|
|
|
|
clock: pixel_clock_kilo_hertz.round() as u32,
|
|
|
|
|
hdisplay: modeline.hdisplay,
|
|
|
|
|
hsync_start: modeline.hsync_start,
|
|
|
|
|
hsync_end: modeline.hsync_end,
|
|
|
|
|
htotal: modeline.htotal,
|
|
|
|
|
vdisplay: modeline.vdisplay,
|
|
|
|
|
vsync_start: modeline.vsync_start,
|
|
|
|
|
vsync_end: modeline.vsync_end,
|
|
|
|
|
vtotal: modeline.vtotal,
|
|
|
|
|
vrefresh: vrefresh_rounded,
|
|
|
|
|
flags: flags.bits(),
|
|
|
|
|
name,
|
|
|
|
|
// Defaults
|
|
|
|
|
type_: drm_ffi::DRM_MODE_TYPE_USERDEF,
|
|
|
|
|
hskew: 0,
|
|
|
|
|
vscan: 0,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn calculate_mode_cvt(width: u16, height: u16, refresh: f64) -> DrmMode {
|
|
|
|
|
// Cross-checked with sway's implementation:
|
|
|
|
|
// https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/22528542970687720556035790212df8d9bb30bb/backend/drm/util.c#L251
|
|
|
|
|
|
|
|
|
|
let options = libdisplay_info::cvt::Options {
|
|
|
|
|
red_blank_ver: libdisplay_info::cvt::ReducedBlankingVersion::None,
|
|
|
|
|
h_pixels: width as i32,
|
|
|
|
|
v_lines: height as i32,
|
|
|
|
|
ip_freq_rqd: refresh,
|
|
|
|
|
|
|
|
|
|
// Defaults
|
|
|
|
|
video_opt: false,
|
|
|
|
|
vblank: 0f64,
|
|
|
|
|
additional_hblank: 0,
|
|
|
|
|
early_vsync_rqd: false,
|
|
|
|
|
int_rqd: false,
|
|
|
|
|
margins_rqd: false,
|
|
|
|
|
};
|
|
|
|
|
let cvt_timing = libdisplay_info::cvt::Timing::compute(options);
|
|
|
|
|
|
|
|
|
|
let hsync_start = width + cvt_timing.h_front_porch as u16;
|
|
|
|
|
let vsync_start = (cvt_timing.v_lines_rnd + cvt_timing.v_front_porch) as u16;
|
|
|
|
|
let hsync_end = hsync_start + cvt_timing.h_sync as u16;
|
|
|
|
|
let vsync_end = vsync_start + cvt_timing.v_sync as u16;
|
|
|
|
|
|
|
|
|
|
let htotal = hsync_end + cvt_timing.h_back_porch as u16;
|
|
|
|
|
let vtotal = vsync_end + cvt_timing.v_back_porch as u16;
|
|
|
|
|
|
|
|
|
|
let clock = f64::round(cvt_timing.act_pixel_freq * 1000f64) as u32;
|
|
|
|
|
let vrefresh = f64::round(cvt_timing.act_frame_rate) as u32;
|
|
|
|
|
|
|
|
|
|
let flags = drm_ffi::DRM_MODE_FLAG_NHSYNC | drm_ffi::DRM_MODE_FLAG_PVSYNC;
|
|
|
|
|
|
|
|
|
|
let mode_name = format!("{width}x{height}@{:.2}", cvt_timing.act_frame_rate);
|
|
|
|
|
let name = modeinfo_name_slice_from_string(&mode_name);
|
|
|
|
|
|
|
|
|
|
let drm_ffi_mode = drm_ffi::drm_sys::drm_mode_modeinfo {
|
|
|
|
|
clock,
|
|
|
|
|
|
|
|
|
|
hdisplay: width,
|
|
|
|
|
hsync_start,
|
|
|
|
|
hsync_end,
|
|
|
|
|
htotal,
|
|
|
|
|
|
|
|
|
|
vdisplay: height,
|
|
|
|
|
vsync_start,
|
|
|
|
|
vsync_end,
|
|
|
|
|
vtotal,
|
|
|
|
|
|
|
|
|
|
vrefresh,
|
|
|
|
|
|
|
|
|
|
flags,
|
|
|
|
|
type_: drm_ffi::DRM_MODE_TYPE_USERDEF,
|
|
|
|
|
name,
|
|
|
|
|
|
|
|
|
|
// Defaults
|
|
|
|
|
hskew: 0,
|
|
|
|
|
vscan: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DrmMode::from(drm_ffi_mode)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns a c-string of maximally 31 Rust string chars + null terminator. Excess characters are
|
|
|
|
|
// dropped.
|
|
|
|
|
fn modeinfo_name_slice_from_string(mode_name: &str) -> [core::ffi::c_char; 32] {
|
|
|
|
|
let mut name: [core::ffi::c_char; 32] = [0; 32];
|
|
|
|
|
|
|
|
|
|
for (a, b) in zip(&mut name[..31], mode_name.as_bytes()) {
|
2025-10-29 10:01:31 +03:00
|
|
|
// Can be u8 on aarch64 and i8 on x86_64.
|
|
|
|
|
*a = *b as _;
|
2025-10-29 07:10:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 18:01:25 +04:00
|
|
|
fn pick_mode(
|
|
|
|
|
connector: &connector::Info,
|
2025-10-29 07:10:38 +01:00
|
|
|
target: Option<niri_config::output::Mode>,
|
2024-01-16 18:01:25 +04:00
|
|
|
) -> Option<(control::Mode, bool)> {
|
|
|
|
|
let mut mode = None;
|
|
|
|
|
let mut fallback = false;
|
|
|
|
|
|
|
|
|
|
if let Some(target) = target {
|
2025-10-29 07:10:38 +01:00
|
|
|
let target_mode = target.mode;
|
|
|
|
|
|
|
|
|
|
if target.custom {
|
|
|
|
|
if let Some(refresh) = target_mode.refresh {
|
|
|
|
|
let custom_mode =
|
|
|
|
|
calculate_mode_cvt(target_mode.width, target_mode.height, refresh);
|
|
|
|
|
return Some((custom_mode, false));
|
|
|
|
|
} else {
|
|
|
|
|
warn!("ignoring custom mode without refresh rate");
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-16 18:01:25 +04:00
|
|
|
|
2025-10-29 07:10:38 +01:00
|
|
|
let refresh = target_mode.refresh.map(|r| (r * 1000.).round() as i32);
|
2024-01-16 18:01:25 +04:00
|
|
|
for m in connector.modes() {
|
2025-10-29 07:10:38 +01:00
|
|
|
if m.size() != (target.mode.width, target.mode.height) {
|
2024-01-16 18:01:25 +04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 21:14:01 +04:00
|
|
|
// Interlaced modes don't appear to work.
|
|
|
|
|
if m.flags().contains(ModeFlags::INTERLACE) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-16 18:01:25 +04:00
|
|
|
if let Some(refresh) = refresh {
|
|
|
|
|
// If refresh is set, only pick modes with matching refresh.
|
|
|
|
|
let wl_mode = Mode::from(*m);
|
|
|
|
|
if wl_mode.refresh == refresh {
|
|
|
|
|
mode = Some(m);
|
|
|
|
|
}
|
|
|
|
|
} else if let Some(curr) = mode {
|
|
|
|
|
// If refresh isn't set, pick the mode with the highest refresh.
|
|
|
|
|
if curr.vrefresh() < m.vrefresh() {
|
|
|
|
|
mode = Some(m);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
mode = Some(m);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mode.is_none() {
|
|
|
|
|
fallback = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mode.is_none() {
|
|
|
|
|
// Pick a preferred mode.
|
|
|
|
|
for m in connector.modes() {
|
|
|
|
|
if !m.mode_type().contains(ModeTypeFlags::PREFERRED) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(curr) = mode {
|
|
|
|
|
if curr.vrefresh() < m.vrefresh() {
|
|
|
|
|
mode = Some(m);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
mode = Some(m);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mode.is_none() {
|
|
|
|
|
// Last attempt.
|
|
|
|
|
mode = connector.modes().first();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mode.map(|m| (*m, fallback))
|
|
|
|
|
}
|
2024-02-14 19:49:34 +04:00
|
|
|
|
2024-09-03 12:13:04 +03:00
|
|
|
fn get_edid_info(
|
|
|
|
|
device: &DrmDevice,
|
|
|
|
|
connector: connector::Handle,
|
|
|
|
|
) -> anyhow::Result<libdisplay_info::info::Info> {
|
|
|
|
|
let (_, info, value) =
|
|
|
|
|
find_drm_property(device, connector, "EDID").context("no EDID property")?;
|
|
|
|
|
let blob = info
|
|
|
|
|
.value_type()
|
|
|
|
|
.convert_value(value)
|
|
|
|
|
.as_blob()
|
|
|
|
|
.context("EDID was not blob type")?;
|
|
|
|
|
let data = device
|
|
|
|
|
.get_property_blob(blob)
|
|
|
|
|
.context("error getting EDID blob value")?;
|
|
|
|
|
libdisplay_info::info::Info::parse_edid(&data).context("error parsing EDID")
|
2024-03-03 19:56:52 +04:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
impl<'a> ConnectorProperties<'a> {
|
|
|
|
|
fn try_new(device: &'a DrmDevice, connector: connector::Handle) -> anyhow::Result<Self> {
|
|
|
|
|
let prop_vals = device
|
|
|
|
|
.get_properties(connector)
|
|
|
|
|
.context("error getting properties")?;
|
2024-03-13 09:15:18 +04:00
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
let mut properties = Vec::new();
|
2024-03-13 09:15:18 +04:00
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
for (prop, value) in prop_vals {
|
|
|
|
|
let info = device
|
|
|
|
|
.get_property(prop)
|
|
|
|
|
.context("error getting property")?;
|
2024-03-13 09:15:18 +04:00
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
properties.push((info, value));
|
2024-03-13 09:15:18 +04:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
Ok(Self {
|
|
|
|
|
device,
|
|
|
|
|
connector,
|
|
|
|
|
properties,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn find(&self, name: &std::ffi::CStr) -> anyhow::Result<&(property::Info, property::RawValue)> {
|
|
|
|
|
for prop in &self.properties {
|
|
|
|
|
if prop.0.name() == name {
|
|
|
|
|
return Ok(prop);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Err(anyhow!("couldn't find property: {name:?}"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DRM_MODE_COLORIMETRY_DEFAULT: u64 = 0;
|
|
|
|
|
|
|
|
|
|
fn reset_hdr(props: &ConnectorProperties) -> anyhow::Result<()> {
|
|
|
|
|
let (info, value) = props.find(c"HDR_OUTPUT_METADATA")?;
|
|
|
|
|
let property::ValueType::Blob = info.value_type() else {
|
|
|
|
|
bail!("wrong property type")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if *value != 0 {
|
|
|
|
|
props
|
|
|
|
|
.device
|
|
|
|
|
.set_property(props.connector, info.handle(), 0)
|
2024-03-13 09:15:18 +04:00
|
|
|
.context("error setting property")?;
|
2025-08-24 00:53:54 +10:00
|
|
|
}
|
2024-03-13 09:15:18 +04:00
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
let (info, value) = props.find(c"Colorspace")?;
|
|
|
|
|
let property::ValueType::Enum(_) = info.value_type() else {
|
|
|
|
|
bail!("wrong property type")
|
|
|
|
|
};
|
|
|
|
|
if *value != DRM_MODE_COLORIMETRY_DEFAULT {
|
|
|
|
|
props
|
|
|
|
|
.device
|
|
|
|
|
.set_property(props.connector, info.handle(), DRM_MODE_COLORIMETRY_DEFAULT)
|
|
|
|
|
.context("error setting property")?;
|
2024-03-13 09:15:18 +04:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 00:53:54 +10:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-14 09:23:15 +04:00
|
|
|
fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bool> {
|
|
|
|
|
let (_, info, value) = find_drm_property(device, connector, "vrr_capable")?;
|
|
|
|
|
info.value_type().convert_value(value).as_boolean()
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 17:45:55 +03:00
|
|
|
fn get_panel_orientation(props: &ConnectorProperties) -> anyhow::Result<Transform> {
|
|
|
|
|
let (info, value) = props.find(c"panel orientation")?;
|
|
|
|
|
match info.value_type().convert_value(*value) {
|
|
|
|
|
property::Value::Enum(Some(val)) => match val.value() {
|
|
|
|
|
// "Normal"
|
|
|
|
|
0 => Ok(Transform::Normal),
|
|
|
|
|
// "Upside Down"
|
|
|
|
|
1 => Ok(Transform::_180),
|
|
|
|
|
// "Left Side Up"
|
|
|
|
|
2 => Ok(Transform::_90),
|
|
|
|
|
// "Right Side Up"
|
|
|
|
|
3 => Ok(Transform::_270),
|
|
|
|
|
_ => bail!("panel orientation has invalid value: {:?}", val),
|
|
|
|
|
},
|
|
|
|
|
_ => bail!("panel orientation has wrong value type"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 08:32:23 +04:00
|
|
|
pub fn set_gamma_for_crtc(
|
|
|
|
|
device: &DrmDevice,
|
|
|
|
|
crtc: crtc::Handle,
|
|
|
|
|
ramp: Option<&[u16]>,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
let _span = tracy_client::span!("set_gamma_for_crtc");
|
|
|
|
|
|
|
|
|
|
let info = device.get_crtc(crtc).context("error getting crtc info")?;
|
|
|
|
|
let gamma_length = info.gamma_length() as usize;
|
|
|
|
|
|
2024-03-28 07:47:57 +04:00
|
|
|
ensure!(gamma_length != 0, "setting gamma is not supported");
|
|
|
|
|
|
2024-03-14 08:32:23 +04:00
|
|
|
let mut temp;
|
|
|
|
|
let ramp = if let Some(ramp) = ramp {
|
|
|
|
|
ensure!(ramp.len() == gamma_length * 3, "wrong gamma length");
|
|
|
|
|
ramp
|
|
|
|
|
} else {
|
|
|
|
|
let _span = tracy_client::span!("generate linear gamma");
|
|
|
|
|
|
|
|
|
|
// The legacy API provides no way to reset the gamma, so set a linear one manually.
|
|
|
|
|
temp = vec![0u16; gamma_length * 3];
|
|
|
|
|
|
|
|
|
|
let (red, rest) = temp.split_at_mut(gamma_length);
|
|
|
|
|
let (green, blue) = rest.split_at_mut(gamma_length);
|
|
|
|
|
let denom = gamma_length as u64 - 1;
|
|
|
|
|
for (i, ((r, g), b)) in zip(zip(red, green), blue).enumerate() {
|
|
|
|
|
let value = (0xFFFFu64 * i as u64 / denom) as u16;
|
|
|
|
|
*r = value;
|
|
|
|
|
*g = value;
|
|
|
|
|
*b = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&temp
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let (red, ramp) = ramp.split_at(gamma_length);
|
|
|
|
|
let (green, blue) = ramp.split_at(gamma_length);
|
|
|
|
|
|
|
|
|
|
device
|
|
|
|
|
.set_gamma(crtc, red, green, blue)
|
|
|
|
|
.context("error setting gamma")?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-03 12:13:04 +03:00
|
|
|
fn format_connector_name(connector: &connector::Info) -> String {
|
|
|
|
|
format!(
|
|
|
|
|
"{}-{}",
|
|
|
|
|
connector.interface().as_str(),
|
|
|
|
|
connector.interface_id(),
|
|
|
|
|
)
|
|
|
|
|
}
|
2024-02-14 19:49:34 +04:00
|
|
|
|
2024-09-03 12:13:04 +03:00
|
|
|
fn make_output_name(
|
|
|
|
|
device: &DrmDevice,
|
|
|
|
|
connector: connector::Handle,
|
|
|
|
|
connector_name: String,
|
|
|
|
|
) -> OutputName {
|
|
|
|
|
let info = get_edid_info(device, connector)
|
|
|
|
|
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
|
|
|
|
|
.ok();
|
|
|
|
|
OutputName {
|
|
|
|
|
connector: connector_name,
|
|
|
|
|
make: info.as_ref().and_then(|info| info.make()),
|
|
|
|
|
model: info.as_ref().and_then(|info| info.model()),
|
|
|
|
|
serial: info.as_ref().and_then(|info| info.serial()),
|
2024-02-14 19:49:34 +04:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-29 07:10:38 +01:00
|
|
|
|
2025-12-29 16:14:52 +03:00
|
|
|
/// Initializes the libinput plugin system.
|
|
|
|
|
///
|
|
|
|
|
/// # Safety
|
|
|
|
|
///
|
|
|
|
|
/// This function must be called before libinput iterates through the devices, i.e. before
|
|
|
|
|
/// libinput_udev_assign_seat() or the first call to libinput_path_add_device().
|
|
|
|
|
unsafe fn init_libinput_plugin_system(libinput: &Libinput) {
|
|
|
|
|
#[cfg(have_libinput_plugin_system)]
|
|
|
|
|
unsafe {
|
|
|
|
|
use std::ffi::{c_char, c_int, CString};
|
|
|
|
|
use std::os::unix::ffi::OsStringExt;
|
|
|
|
|
|
|
|
|
|
use directories::BaseDirs;
|
|
|
|
|
use input::ffi::libinput;
|
|
|
|
|
use input::AsRaw as _;
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
fn libinput_plugin_system_append_path(libinput: *const libinput, path: *const c_char);
|
|
|
|
|
fn libinput_plugin_system_append_default_paths(libinput: *const libinput);
|
|
|
|
|
fn libinput_plugin_system_load_plugins(
|
|
|
|
|
libinput: *const libinput,
|
|
|
|
|
flags: c_int,
|
|
|
|
|
) -> c_int;
|
|
|
|
|
}
|
|
|
|
|
const LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE: c_int = 0;
|
|
|
|
|
let libinput = libinput.as_raw();
|
|
|
|
|
|
|
|
|
|
// Also load plugins from $XDG_CONFIG_HOME/libinput/plugins.
|
|
|
|
|
if let Some(dirs) = BaseDirs::new() {
|
|
|
|
|
let mut plugins_dir = dirs.config_dir().to_path_buf();
|
|
|
|
|
plugins_dir.push("libinput");
|
|
|
|
|
plugins_dir.push("plugins");
|
|
|
|
|
if let Ok(plugins_dir) = CString::new(plugins_dir.into_os_string().into_vec()) {
|
|
|
|
|
libinput_plugin_system_append_path(libinput, plugins_dir.as_ptr());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
libinput_plugin_system_append_default_paths(libinput);
|
|
|
|
|
libinput_plugin_system_load_plugins(libinput, LIBINPUT_PLUGIN_SYSTEM_FLAG_NONE);
|
|
|
|
|
}
|
|
|
|
|
#[cfg(not(have_libinput_plugin_system))]
|
|
|
|
|
let _ = libinput;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-29 07:10:38 +01:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use insta::assert_debug_snapshot;
|
|
|
|
|
use niri_config::output::Modeline;
|
|
|
|
|
use niri_ipc::{HSyncPolarity, VSyncPolarity};
|
|
|
|
|
|
|
|
|
|
use crate::backend::tty::{calculate_drm_mode_from_modeline, calculate_mode_cvt};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_calculate_drmmode_from_modeline() {
|
|
|
|
|
let modeline1 = Modeline {
|
|
|
|
|
clock: 173.0,
|
|
|
|
|
hdisplay: 1920,
|
|
|
|
|
vdisplay: 1080,
|
|
|
|
|
hsync_start: 2048,
|
|
|
|
|
hsync_end: 2248,
|
|
|
|
|
htotal: 2576,
|
|
|
|
|
vsync_start: 1083,
|
|
|
|
|
vsync_end: 1088,
|
|
|
|
|
vtotal: 1120,
|
|
|
|
|
hsync_polarity: HSyncPolarity::NHSync,
|
|
|
|
|
vsync_polarity: VSyncPolarity::PVSync,
|
|
|
|
|
};
|
2026-01-10 15:33:58 +03:00
|
|
|
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline1).unwrap(), @r#"
|
|
|
|
|
Mode {
|
|
|
|
|
name: "1920x1080@59.96",
|
|
|
|
|
clock: 173000,
|
|
|
|
|
size: (
|
|
|
|
|
1920,
|
|
|
|
|
1080,
|
|
|
|
|
),
|
|
|
|
|
hsync: (
|
|
|
|
|
2048,
|
|
|
|
|
2248,
|
|
|
|
|
2576,
|
|
|
|
|
),
|
|
|
|
|
vsync: (
|
|
|
|
|
1083,
|
|
|
|
|
1088,
|
|
|
|
|
1120,
|
|
|
|
|
),
|
|
|
|
|
hskew: 0,
|
|
|
|
|
vscan: 0,
|
|
|
|
|
vrefresh: 60,
|
|
|
|
|
mode_type: ModeTypeFlags(
|
|
|
|
|
USERDEF,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
"#);
|
2025-10-29 07:10:38 +01:00
|
|
|
let modeline2 = Modeline {
|
|
|
|
|
clock: 452.5,
|
|
|
|
|
hdisplay: 1920,
|
|
|
|
|
vdisplay: 1080,
|
|
|
|
|
hsync_start: 2088,
|
|
|
|
|
hsync_end: 2296,
|
|
|
|
|
htotal: 2672,
|
|
|
|
|
vsync_start: 1083,
|
|
|
|
|
vsync_end: 1088,
|
|
|
|
|
vtotal: 1177,
|
|
|
|
|
hsync_polarity: HSyncPolarity::NHSync,
|
|
|
|
|
vsync_polarity: VSyncPolarity::PVSync,
|
|
|
|
|
};
|
2026-01-10 15:33:58 +03:00
|
|
|
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline2).unwrap(), @r#"
|
|
|
|
|
Mode {
|
|
|
|
|
name: "1920x1080@143.88",
|
|
|
|
|
clock: 452500,
|
|
|
|
|
size: (
|
|
|
|
|
1920,
|
|
|
|
|
1080,
|
|
|
|
|
),
|
|
|
|
|
hsync: (
|
|
|
|
|
2088,
|
|
|
|
|
2296,
|
|
|
|
|
2672,
|
|
|
|
|
),
|
|
|
|
|
vsync: (
|
|
|
|
|
1083,
|
|
|
|
|
1088,
|
|
|
|
|
1177,
|
|
|
|
|
),
|
|
|
|
|
hskew: 0,
|
|
|
|
|
vscan: 0,
|
|
|
|
|
vrefresh: 144,
|
|
|
|
|
mode_type: ModeTypeFlags(
|
|
|
|
|
USERDEF,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
"#);
|
2025-10-29 07:10:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_calc_cvt() {
|
|
|
|
|
// Crosschecked with other calculators like the cvt commandline utility.
|
2026-01-10 15:33:58 +03:00
|
|
|
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 60.0), @r#"
|
|
|
|
|
Mode {
|
|
|
|
|
name: "1920x1080@59.96",
|
|
|
|
|
clock: 173000,
|
|
|
|
|
size: (
|
|
|
|
|
1920,
|
|
|
|
|
1080,
|
|
|
|
|
),
|
|
|
|
|
hsync: (
|
|
|
|
|
2048,
|
|
|
|
|
2248,
|
|
|
|
|
2576,
|
|
|
|
|
),
|
|
|
|
|
vsync: (
|
|
|
|
|
1083,
|
|
|
|
|
1088,
|
|
|
|
|
1120,
|
|
|
|
|
),
|
|
|
|
|
hskew: 0,
|
|
|
|
|
vscan: 0,
|
|
|
|
|
vrefresh: 60,
|
|
|
|
|
mode_type: ModeTypeFlags(
|
|
|
|
|
USERDEF,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
"#);
|
|
|
|
|
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 144.0), @r#"
|
|
|
|
|
Mode {
|
|
|
|
|
name: "1920x1080@143.88",
|
|
|
|
|
clock: 452500,
|
|
|
|
|
size: (
|
|
|
|
|
1920,
|
|
|
|
|
1080,
|
|
|
|
|
),
|
|
|
|
|
hsync: (
|
|
|
|
|
2088,
|
|
|
|
|
2296,
|
|
|
|
|
2672,
|
|
|
|
|
),
|
|
|
|
|
vsync: (
|
|
|
|
|
1083,
|
|
|
|
|
1088,
|
|
|
|
|
1177,
|
|
|
|
|
),
|
|
|
|
|
hskew: 0,
|
|
|
|
|
vscan: 0,
|
|
|
|
|
vrefresh: 144,
|
|
|
|
|
mode_type: ModeTypeFlags(
|
|
|
|
|
USERDEF,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
"#);
|
2025-10-29 07:10:38 +01:00
|
|
|
}
|
|
|
|
|
}
|