1798 lines
64 KiB
Rust
1798 lines
64 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
use std::error::Error;
|
|
use std::io;
|
|
use std::path::Path;
|
|
use std::time::{Duration, Instant};
|
|
|
|
mod scheduling;
|
|
use scheduling::*;
|
|
use smithay::backend::allocator::Fourcc;
|
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
|
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType};
|
|
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
|
use smithay::backend::input::InputEvent;
|
|
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
|
use smithay::backend::renderer::damage::OutputDamageTracker;
|
|
use smithay::backend::renderer::gles::GlesRenderer;
|
|
use smithay::backend::renderer::{Bind, Frame, ImportDma, ImportMemWl, Renderer};
|
|
use smithay::backend::session::libseat::LibSeatSession;
|
|
use smithay::backend::session::{Event as SessionEvent, Session};
|
|
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu};
|
|
use smithay::desktop::layer_map_for_output;
|
|
use smithay::desktop::utils::OutputPresentationFeedback;
|
|
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel};
|
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
|
use smithay::reexports::calloop::{EventLoop, RegistrationToken};
|
|
use smithay::reexports::drm::control::{
|
|
Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property,
|
|
};
|
|
use smithay::reexports::input::{Device as LibinputDevice, Libinput};
|
|
use smithay::reexports::rustix::fs::OFlags;
|
|
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind;
|
|
use smithay::reexports::wayland_server::backend::GlobalId;
|
|
use smithay::utils::{DeviceFd, Monotonic, Time, Transform};
|
|
use smithay::wayland::dmabuf::DmabufFeedbackBuilder;
|
|
use smithay::wayland::presentation::Refresh;
|
|
|
|
use crate::Ferese;
|
|
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
|
use crate::metrics::RenderMetrics;
|
|
use crate::render::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
|
|
|
|
pub struct DirectBackendState {
|
|
pub session: LibSeatSession,
|
|
pub active: bool,
|
|
lid_closed: bool,
|
|
pub(crate) low_power: bool,
|
|
devices: HashMap<DrmNode, DirectDevice>,
|
|
input_devices: Vec<LibinputDevice>,
|
|
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
|
|
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
|
|
animation_timer: Option<RegistrationToken>,
|
|
animation_active: bool,
|
|
}
|
|
|
|
impl DirectBackendState {
|
|
pub(crate) fn capture_resize_snapshot(
|
|
&mut self,
|
|
window: &smithay::desktop::Window,
|
|
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
|
output: &Output,
|
|
remaining: usize,
|
|
) -> Result<Option<crate::render::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError> {
|
|
let Some(device) = self
|
|
.devices
|
|
.values_mut()
|
|
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
crate::render::capture_resize_snapshot(
|
|
&mut device.renderer,
|
|
window,
|
|
geometry,
|
|
output.current_scale().fractional_scale(),
|
|
remaining,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn capture_window_buffer(
|
|
&mut self,
|
|
window: &smithay::desktop::Window,
|
|
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
|
output: &Output,
|
|
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
|
let device = self
|
|
.devices
|
|
.values_mut()
|
|
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
|
.ok_or("Window output is unavailable")?;
|
|
crate::render::capture_window_buffer(
|
|
&mut device.renderer,
|
|
window,
|
|
geometry,
|
|
output.current_scale().fractional_scale(),
|
|
)
|
|
}
|
|
pub(crate) fn capture_window_frame(
|
|
&mut self,
|
|
state: &Ferese,
|
|
window: &smithay::desktop::Window,
|
|
output: &Output,
|
|
cursor: Option<smithay::utils::Rectangle<i32, smithay::utils::Logical>>,
|
|
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
|
let device = self
|
|
.devices
|
|
.values_mut()
|
|
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
|
.ok_or("Window output is unavailable")?;
|
|
crate::render::capture_window_frame(
|
|
&mut device.renderer,
|
|
window,
|
|
window.geometry(),
|
|
output.current_scale().fractional_scale(),
|
|
cursor.map(|rect| (state, rect)),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct ConnectedOutputInfo {
|
|
pub connector: String,
|
|
pub identity: String,
|
|
pub enabled: bool,
|
|
pub profile: Option<String>,
|
|
pub auto_refresh: bool,
|
|
pub physical_size: Option<(u32, u32)>,
|
|
pub current_mode: Option<ConnectedModeInfo>,
|
|
pub available_modes: Vec<ConnectedModeInfo>,
|
|
pub scale: f64,
|
|
pub transform: OutputTransform,
|
|
pub configured_position: Option<[i32; 2]>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(crate) struct ConnectedModeInfo {
|
|
pub width: u16,
|
|
pub height: u16,
|
|
pub refresh_millihertz: i32,
|
|
pub preferred: bool,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct PresentationClock {
|
|
last_presentation: Option<Duration>,
|
|
presented_frames: u64,
|
|
refresh_interval: Option<Duration>,
|
|
missed_deadlines: u64,
|
|
}
|
|
|
|
struct DirectDevice {
|
|
drm: DrmDevice,
|
|
gbm: GbmDevice<DrmDeviceFd>,
|
|
renderer: GlesRenderer,
|
|
outputs: HashMap<crtc::Handle, DirectOutput>,
|
|
}
|
|
|
|
struct DirectOutput {
|
|
internal: bool,
|
|
connector: connector::Handle,
|
|
mode: DrmMode,
|
|
settings: OutputSettings,
|
|
output: Output,
|
|
global: GlobalId,
|
|
surface: GbmBufferedSurface<GbmAllocator<DrmDeviceFd>, OutputPresentationFeedback>,
|
|
damage_tracker: OutputDamageTracker,
|
|
render_metrics: RenderMetrics,
|
|
frame_pending: bool,
|
|
scheduler: crate::frame_scheduler::FrameScheduler,
|
|
render_timer: Option<RegistrationToken>,
|
|
power_off: bool,
|
|
callback_timer: Option<RegistrationToken>,
|
|
lock_frame_pending: bool,
|
|
}
|
|
|
|
struct OutputSelection {
|
|
connector: connector::Info,
|
|
crtc: crtc::Handle,
|
|
mode: DrmMode,
|
|
settings: OutputSettings,
|
|
}
|
|
|
|
struct OutputScan {
|
|
selections: Vec<OutputSelection>,
|
|
connected_outputs: Vec<ConnectedOutputInfo>,
|
|
}
|
|
|
|
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
|
|
let (session, notifier) = LibSeatSession::new()?;
|
|
let seat_name = session.seat();
|
|
let udev_backend = UdevBackend::new(&seat_name)?;
|
|
let primary_path = primary_gpu(&seat_name)?
|
|
.or_else(|| udev_backend.device_list().next().map(|(_, path)| path.to_owned()))
|
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no DRM device found"))?;
|
|
let mut libinput_context =
|
|
Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
|
|
libinput_context
|
|
.udev_assign_seat(&seat_name)
|
|
.map_err(|()| io::Error::other(format!("failed to assign libinput seat {seat_name}")))?;
|
|
let libinput_backend = LibinputInputBackend::new(libinput_context.clone());
|
|
let session_active = session.is_active();
|
|
if !session_active {
|
|
libinput_context.suspend();
|
|
}
|
|
|
|
state.direct_backend = Some(DirectBackendState {
|
|
session,
|
|
active: session_active,
|
|
lid_closed: initial_lid_closed(),
|
|
low_power: super::power::low_power(false, super::power::battery_percent()),
|
|
devices: HashMap::new(),
|
|
input_devices: Vec::new(),
|
|
presentation: HashMap::new(),
|
|
connected_outputs: Vec::new(),
|
|
animation_timer: None,
|
|
animation_active: false,
|
|
});
|
|
|
|
open_primary_device(event_loop, state, &primary_path)?;
|
|
event_loop
|
|
.handle()
|
|
.insert_source(Timer::from_duration(Duration::from_secs(5)), |_, _, state| {
|
|
update_power_policy(state);
|
|
TimeoutAction::ToDuration(Duration::from_secs(5))
|
|
})?;
|
|
|
|
event_loop
|
|
.handle()
|
|
.insert_source(libinput_backend, |event, _, state| match event {
|
|
InputEvent::DeviceAdded { mut device } => {
|
|
configure_libinput_device(state, &mut device);
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.input_devices.push(device);
|
|
}
|
|
}
|
|
|
|
InputEvent::DeviceRemoved { device } => {
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.input_devices.retain(|candidate| candidate != &device);
|
|
}
|
|
}
|
|
|
|
event => state.process_input_event(event),
|
|
})?;
|
|
event_loop
|
|
.handle()
|
|
.insert_source(notifier, move |event, _, state| match event {
|
|
SessionEvent::PauseSession => {
|
|
let handle = state.loop_handle.clone();
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.active = false;
|
|
backend
|
|
.presentation
|
|
.values_mut()
|
|
.for_each(PresentationClock::reset_timing);
|
|
backend.devices.values_mut().for_each(|device| {
|
|
device.drm.pause();
|
|
device.outputs.values_mut().for_each(|output| {
|
|
cancel_output_timers(&handle, output);
|
|
output.surface.reset_buffers();
|
|
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
|
output.frame_pending = false;
|
|
output.power_off = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
libinput_context.suspend();
|
|
tracing::info!("direct session paused");
|
|
}
|
|
|
|
SessionEvent::ActivateSession => {
|
|
if libinput_context.resume().is_err() {
|
|
tracing::error!("failed to resume libinput");
|
|
}
|
|
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.active = true;
|
|
for device in backend.devices.values_mut() {
|
|
if let Err(error) = device.drm.activate(true) {
|
|
tracing::error!(%error, "failed to reactivate DRM device");
|
|
}
|
|
}
|
|
}
|
|
|
|
update_power_policy(state);
|
|
state.reset_animation_clock();
|
|
tracing::info!("direct session activated");
|
|
state.lock_input_activity();
|
|
render_all(state);
|
|
}
|
|
})?;
|
|
event_loop
|
|
.handle()
|
|
.insert_source(udev_backend, |event, _, state| match event {
|
|
UdevEvent::Added { device_id, path } => {
|
|
if let Some(node) = direct_node_for_device(state, device_id) {
|
|
rescan_device(state, node);
|
|
} else {
|
|
tracing::info!(?device_id, ?path, "additional DRM device discovered");
|
|
}
|
|
}
|
|
|
|
UdevEvent::Changed { device_id } => {
|
|
if let Some(node) = direct_node_for_device(state, device_id) {
|
|
rescan_device(state, node);
|
|
}
|
|
}
|
|
|
|
UdevEvent::Removed { device_id } => {
|
|
if let Some(node) = direct_node_for_device(state, device_id) {
|
|
remove_device(state, node);
|
|
}
|
|
}
|
|
})?;
|
|
|
|
tracing::info!(seat = %seat_name, "initialized direct session input and device discovery");
|
|
Ok(())
|
|
}
|
|
|
|
fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) {
|
|
let settings = state.input_settings.touchpad;
|
|
|
|
if device.config_tap_finger_count() > 0
|
|
&& let Err(error) = device.config_tap_set_enabled(settings.tap)
|
|
{
|
|
tracing::warn!(device = device.name(), ?error, "failed to configure tap-to-click");
|
|
}
|
|
if device.config_scroll_has_natural_scroll()
|
|
&& let Err(error) = device.config_scroll_set_natural_scroll_enabled(settings.natural_scroll)
|
|
{
|
|
tracing::warn!(device = device.name(), ?error, "failed to configure natural scrolling");
|
|
}
|
|
if device.config_dwt_is_available()
|
|
&& let Err(error) = device.config_dwt_set_enabled(settings.disable_while_typing)
|
|
{
|
|
tracing::warn!(
|
|
device = device.name(),
|
|
?error,
|
|
"failed to configure disable-while-typing"
|
|
);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn reload_input_devices(state: &mut Ferese) {
|
|
let Some(backend) = state.direct_backend.as_ref() else {
|
|
return;
|
|
};
|
|
// Device clones refer to the same live libinput objects.
|
|
let mut devices = backend.input_devices.clone();
|
|
for device in &mut devices {
|
|
configure_libinput_device(state, device);
|
|
}
|
|
}
|
|
|
|
fn update_power_policy(state: &mut Ferese) {
|
|
if !state
|
|
.output_profiles
|
|
.iter()
|
|
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh))
|
|
{
|
|
return;
|
|
}
|
|
let Some(backend) = state.direct_backend.as_mut() else {
|
|
return;
|
|
};
|
|
// Keep low_power as the applied policy while paused. A transition sampled
|
|
// during inactivity must still be detected and applied on activation.
|
|
let battery = if backend.active {
|
|
super::power::battery_percent()
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(next) = super::power::policy_change(backend.active, backend.low_power, battery) {
|
|
backend.low_power = next;
|
|
reload_outputs(state);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn reload_outputs(state: &mut Ferese) {
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
let enabled = state
|
|
.output_profiles
|
|
.iter()
|
|
.any(|profile| profile.outputs.iter().any(|output| output.auto_refresh));
|
|
backend.low_power = enabled && super::power::low_power(backend.low_power, super::power::battery_percent());
|
|
}
|
|
let nodes = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.map(|backend| backend.devices.keys().copied().collect::<Vec<_>>())
|
|
.unwrap_or_default();
|
|
for node in nodes {
|
|
rescan_device(state, node);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> {
|
|
let Some(backend) = state.direct_backend.as_ref() else {
|
|
return Ok(());
|
|
};
|
|
let mut usable = 0;
|
|
for device in backend.devices.values() {
|
|
let mut scan = select_outputs(&device.drm, profiles, backend.low_power).map_err(|e| e.to_string())?;
|
|
apply_lid_policy(&mut scan, backend.lid_closed);
|
|
usable += scan.selections.len();
|
|
}
|
|
if usable == 0 {
|
|
return Err("display configuration would leave no usable output".into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn open_primary_device(
|
|
event_loop: &mut EventLoop<Ferese>,
|
|
state: &mut Ferese,
|
|
path: &Path,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
let mut session = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.expect("direct backend state is initialized before the DRM device")
|
|
.session
|
|
.clone();
|
|
let node = DrmNode::from_path(path)?;
|
|
let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?;
|
|
let fd = DrmDeviceFd::new(DeviceFd::from(fd));
|
|
let (mut drm, notifier) = DrmDevice::new(fd.clone(), true)?;
|
|
let gbm = GbmDevice::new(fd)?;
|
|
// SAFETY: GBM owns a valid DRM descriptor for the lifetime of the EGL display.
|
|
let egl_display = unsafe { EGLDisplay::new(gbm.clone())? };
|
|
let egl_context = EGLContext::new(&egl_display)?;
|
|
// SAFETY: the new context is not current on another thread and remains renderer-owned.
|
|
let renderer = unsafe { GlesRenderer::new(egl_context)? };
|
|
state.shm_state.update_formats(renderer.shm_formats());
|
|
let dmabuf_formats = renderer.dmabuf_formats();
|
|
let display_handle = state.display_handle.clone();
|
|
// Mesa's Wayland EGL path needs the main device, not just a v3 format
|
|
// list, to choose a hardware render node. Without feedback, nested EGL
|
|
// clients can fall back to llvmpipe despite a GPU-backed DRM compositor.
|
|
let render_node = node
|
|
.node_with_type(NodeType::Render)
|
|
.and_then(Result::ok)
|
|
.unwrap_or(node);
|
|
let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), dmabuf_formats).build()?;
|
|
state
|
|
.dmabuf_state
|
|
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
|
|
let mut scan = select_outputs(
|
|
&drm,
|
|
&state.output_profiles,
|
|
state.direct_backend.as_ref().is_some_and(|backend| backend.low_power),
|
|
)?;
|
|
apply_lid_policy(
|
|
&mut scan,
|
|
state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed),
|
|
);
|
|
if scan.selections.is_empty() {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"no connected enabled DRM output with a usable CRTC",
|
|
)
|
|
.into());
|
|
}
|
|
|
|
state
|
|
.direct_backend
|
|
.as_mut()
|
|
.expect("direct backend state remains initialized")
|
|
.connected_outputs = scan.connected_outputs;
|
|
let mut outputs = HashMap::new();
|
|
for selection in scan.selections {
|
|
let crtc = selection.crtc;
|
|
let mode = selection.mode;
|
|
let output = create_direct_output(state, &mut drm, &gbm, &renderer, selection)?;
|
|
state
|
|
.direct_backend
|
|
.as_mut()
|
|
.expect("direct backend state remains initialized")
|
|
.presentation
|
|
.entry((node, crtc))
|
|
.or_default()
|
|
.set_refresh(OutputMode::from(mode).refresh);
|
|
outputs.insert(crtc, output);
|
|
}
|
|
|
|
event_loop
|
|
.handle()
|
|
.insert_source(notifier, move |event, metadata, state| match event {
|
|
DrmEvent::VBlank(crtc) => {
|
|
if let Some(metadata) = metadata {
|
|
let mut retired = false;
|
|
let feedback = state
|
|
.direct_backend
|
|
.as_mut()
|
|
.and_then(|backend| backend.devices.get_mut(&node))
|
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
|
.and_then(|output| match output.surface.frame_submitted() {
|
|
Ok(feedback) => {
|
|
retired = true;
|
|
feedback
|
|
}
|
|
|
|
Err(error) => {
|
|
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
|
|
None
|
|
}
|
|
});
|
|
|
|
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) {
|
|
feedback.presented(
|
|
Time::<Monotonic>::from(time),
|
|
Refresh::fixed(refresh_duration(state, node, crtc)),
|
|
u64::from(metadata.sequence),
|
|
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
|
);
|
|
}
|
|
|
|
let lock_output = state
|
|
.direct_backend
|
|
.as_mut()
|
|
.and_then(|backend| backend.devices.get_mut(&node))
|
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
|
.and_then(|output| {
|
|
if retired && std::mem::take(&mut output.lock_frame_pending) {
|
|
Some(output.output.clone())
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
if let Some(output) = lock_output {
|
|
state.lock_frame_presented(&output);
|
|
}
|
|
|
|
if let Some(output) = state
|
|
.direct_backend
|
|
.as_mut()
|
|
.and_then(|backend| backend.devices.get_mut(&node))
|
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
|
&& retired
|
|
{
|
|
output.frame_pending = false;
|
|
if let DrmEventTime::Monotonic(timestamp) = metadata.time {
|
|
let now = monotonic_now();
|
|
if timestamp <= now + refresh_duration_from_mode(output.mode) {
|
|
if let Some(plan) = output.scheduler.presented(timestamp)
|
|
&& std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some()
|
|
{
|
|
tracing::debug!(target: "ferese::render", output = %output.output.name(),
|
|
target_us = plan.present_at.as_micros(), actual_us = timestamp.as_micros(),
|
|
request_to_present_us = timestamp.saturating_sub(plan.requested_at).as_micros(),
|
|
missed_deadlines = output.scheduler.missed_deadlines,
|
|
"DRM presentation schedule");
|
|
}
|
|
} else {
|
|
output.scheduler.retire_unknown();
|
|
}
|
|
} else {
|
|
output.scheduler.retire_unknown();
|
|
}
|
|
}
|
|
|
|
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
|
|
tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip");
|
|
let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active);
|
|
if active && retired {
|
|
schedule_frame_callbacks(state, node, crtc, monotonic_now());
|
|
let identity = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.and_then(|backend| backend.devices.get(&node))
|
|
.and_then(|device| device.outputs.get(&crtc))
|
|
.map(|output| output.output.clone());
|
|
if identity.is_some_and(|output| state.output_has_animations(&output)) {
|
|
request_frame(state, node, crtc);
|
|
} else {
|
|
arm_frame(state, node, crtc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DrmEvent::Error(error) => {
|
|
tracing::error!(?node, %error, "DRM event error");
|
|
}
|
|
})?;
|
|
|
|
state
|
|
.direct_backend
|
|
.as_mut()
|
|
.expect("direct backend state remains initialized")
|
|
.devices
|
|
.insert(
|
|
node,
|
|
DirectDevice {
|
|
drm,
|
|
gbm,
|
|
renderer,
|
|
outputs,
|
|
},
|
|
);
|
|
state.relayout();
|
|
render_all(state);
|
|
tracing::info!(?node, ?path, "initialized DRM/GBM device outputs");
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
|
|
if !state.session_lock.active || !state.session_lock.sleeping {
|
|
return;
|
|
}
|
|
|
|
let Some(backend) = state.direct_backend.as_mut() else {
|
|
return;
|
|
};
|
|
|
|
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
|
|
for output in device.outputs.values_mut() {
|
|
if output.power_off || output.frame_pending {
|
|
continue;
|
|
}
|
|
|
|
match output.surface.surface().clear() {
|
|
Ok(()) => {
|
|
cancel_output_timers(&state.loop_handle, output);
|
|
output.power_off = true;
|
|
}
|
|
|
|
Err(error) => {
|
|
tracing::warn!(%error, output = %output.output.name(), "could not sleep locked display")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
for device in backend.devices.values_mut() {
|
|
for output in device.outputs.values_mut() {
|
|
if std::mem::take(&mut output.power_off) {
|
|
output.surface.reset_buffer_ages();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
render_all(state);
|
|
}
|
|
|
|
pub fn render_all(state: &mut Ferese) {
|
|
if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) {
|
|
return;
|
|
}
|
|
let animating = state.advance_animations(Instant::now());
|
|
render_outputs(state, animating);
|
|
}
|
|
|
|
fn render_outputs(state: &mut Ferese, animating: bool) {
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.animation_active = animating && !state.session_lock.sleeping;
|
|
}
|
|
|
|
arm_animation_timer(state);
|
|
let outputs = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.map(|backend| {
|
|
backend
|
|
.devices
|
|
.iter()
|
|
.flat_map(|(node, device)| device.outputs.keys().map(|crtc| (*node, *crtc)))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
for (node, crtc) in outputs {
|
|
request_frame(state, node, crtc);
|
|
}
|
|
}
|
|
|
|
pub fn switch_vt(state: &mut Ferese, vt: i32) {
|
|
let Some(backend) = state.direct_backend.as_mut() else {
|
|
return;
|
|
};
|
|
|
|
if let Err(error) = backend.session.change_vt(vt) {
|
|
tracing::error!(vt, %error, "failed to switch virtual terminal");
|
|
}
|
|
}
|
|
|
|
pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) {
|
|
let Some(backend) = state.direct_backend.as_mut() else {
|
|
return;
|
|
};
|
|
if backend.lid_closed == closed {
|
|
return;
|
|
}
|
|
backend.lid_closed = closed;
|
|
tracing::info!(closed, "laptop lid state changed");
|
|
let nodes = backend.devices.keys().copied().collect::<Vec<_>>();
|
|
for node in nodes {
|
|
rescan_device(state, node);
|
|
}
|
|
}
|
|
|
|
fn initial_lid_closed() -> bool {
|
|
std::fs::read_dir("/proc/acpi/button/lid")
|
|
.ok()
|
|
.into_iter()
|
|
.flatten()
|
|
.filter_map(Result::ok)
|
|
.filter_map(|entry| std::fs::read_to_string(entry.path().join("state")).ok())
|
|
.any(|state| state.split_whitespace().last() == Some("closed"))
|
|
}
|
|
|
|
fn internal_connector(interface: connector::Interface) -> bool {
|
|
matches!(
|
|
interface,
|
|
connector::Interface::EmbeddedDisplayPort | connector::Interface::LVDS | connector::Interface::DSI
|
|
)
|
|
}
|
|
|
|
fn apply_lid_policy(scan: &mut OutputScan, closed: bool) {
|
|
let external_available = scan
|
|
.selections
|
|
.iter()
|
|
.any(|selection| !internal_connector(selection.connector.interface()));
|
|
if !lid_hides_panel(closed, external_available) {
|
|
return;
|
|
}
|
|
let internal = scan
|
|
.selections
|
|
.iter()
|
|
.filter(|selection| internal_connector(selection.connector.interface()))
|
|
.map(|selection| selection.connector.to_string())
|
|
.collect::<HashSet<_>>();
|
|
scan.selections
|
|
.retain(|selection| !internal_connector(selection.connector.interface()));
|
|
for output in &mut scan.connected_outputs {
|
|
if internal.contains(&output.connector) {
|
|
output.enabled = false;
|
|
output.current_mode = None;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lid_hides_panel(closed: bool, external_available: bool) -> bool {
|
|
closed && external_available
|
|
}
|
|
|
|
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: crate::frame_scheduler::FramePlan) {
|
|
if state.session_lock.active && state.session_lock.sleeping {
|
|
sleep_locked_outputs(state);
|
|
let asleep = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.and_then(|backend| backend.devices.get(&node))
|
|
.and_then(|device| device.outputs.get(&crtc))
|
|
.is_none_or(|output| output.power_off || output.frame_pending);
|
|
if asleep {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let missed_deadlines = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.and_then(|backend| backend.devices.get(&node))
|
|
.and_then(|device| device.outputs.get(&crtc))
|
|
.map_or(0, |output| output.scheduler.missed_deadlines);
|
|
let Some(mut device) = state
|
|
.direct_backend
|
|
.as_mut()
|
|
.and_then(|backend| backend.devices.remove(&node))
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let Some(mut output) = device.outputs.remove(&crtc) else {
|
|
restore_device(state, node, device);
|
|
return;
|
|
};
|
|
|
|
if !device.drm.is_active() || output.frame_pending {
|
|
device.outputs.insert(crtc, output);
|
|
restore_device(state, node, device);
|
|
return;
|
|
}
|
|
|
|
let render_started = Instant::now();
|
|
let now = monotonic_now();
|
|
let horizon = output
|
|
.scheduler
|
|
.forecast_time(now, plan)
|
|
.map_or(Duration::ZERO, |target| target.saturating_sub(now));
|
|
let mut forecast = state.forecast_render(&output.output, horizon);
|
|
let rendered = {
|
|
let state = &mut *forecast;
|
|
(|| -> Result<bool, Box<dyn Error>> {
|
|
state.process_dmabuf_imports(&mut device.renderer);
|
|
let (mut buffer, age) = output.surface.next_buffer()?;
|
|
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
|
|
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
|
let mut framebuffer = device.renderer.bind(&mut buffer)?;
|
|
let result = output.damage_tracker.render_output(
|
|
&mut device.renderer,
|
|
&mut framebuffer,
|
|
usize::from(age),
|
|
&elements,
|
|
[0.035, 0.04, 0.055, 1.0],
|
|
)?;
|
|
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
|
|
let captured_with_cursor =
|
|
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
|
|
|
if cursorless_capture {
|
|
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
|
|
redraw_output(
|
|
&mut device.renderer,
|
|
&mut framebuffer,
|
|
&output.output,
|
|
&cursorless_elements,
|
|
)?;
|
|
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
|
|
redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?;
|
|
} else if captured_with_cursor {
|
|
let _ = device
|
|
.renderer
|
|
.render(
|
|
&mut framebuffer,
|
|
output.output.current_mode().expect("output has a mode").size,
|
|
output.output.current_transform(),
|
|
)?
|
|
.finish()?;
|
|
}
|
|
|
|
let Some(damage) = result.damage.cloned() else {
|
|
output
|
|
.render_metrics
|
|
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
|
return Ok(false);
|
|
};
|
|
|
|
let presentation = crate::presentation::take_output_feedback(
|
|
state,
|
|
&output.output,
|
|
&result.states,
|
|
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
|
);
|
|
output
|
|
.surface
|
|
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
|
|
output
|
|
.render_metrics
|
|
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
|
output.frame_pending = true;
|
|
output.lock_frame_pending = state.session_lock.active;
|
|
Ok(true)
|
|
})()
|
|
};
|
|
|
|
let submitted = rendered.as_ref().is_ok_and(|submitted| *submitted);
|
|
record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted);
|
|
|
|
match rendered {
|
|
Ok(true) => tracing::trace!(?node, ?crtc, "queued DRM frame"),
|
|
Ok(false) => (),
|
|
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"),
|
|
}
|
|
|
|
device.outputs.insert(crtc, output);
|
|
restore_device(&mut forecast, node, device);
|
|
drop(forecast);
|
|
if !submitted {
|
|
schedule_frame_callbacks(state, node, crtc, plan.present_at);
|
|
}
|
|
}
|
|
|
|
fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) {
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.devices.insert(node, device);
|
|
}
|
|
}
|
|
|
|
fn direct_node_for_device(state: &Ferese, device_id: libc::dev_t) -> Option<DrmNode> {
|
|
state
|
|
.direct_backend
|
|
.as_ref()?
|
|
.devices
|
|
.keys()
|
|
.copied()
|
|
.find(|node| node.dev_id() == device_id)
|
|
}
|
|
|
|
fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|
let Some(mut device) = state
|
|
.direct_backend
|
|
.as_mut()
|
|
.and_then(|backend| backend.devices.remove(&node))
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let mut scan = match select_outputs(
|
|
&device.drm,
|
|
&state.output_profiles,
|
|
state.direct_backend.as_ref().is_some_and(|backend| backend.low_power),
|
|
) {
|
|
Ok(scan) => scan,
|
|
Err(error) => {
|
|
tracing::error!(?node, %error, "failed to scan DRM connectors");
|
|
restore_device(state, node, device);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed);
|
|
apply_lid_policy(&mut scan, lid_closed);
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.connected_outputs = scan.connected_outputs;
|
|
}
|
|
|
|
let mut selections = scan
|
|
.selections
|
|
.into_iter()
|
|
.map(|selection| (selection.crtc, selection))
|
|
.collect::<HashMap<_, _>>();
|
|
let existing = device.outputs.keys().copied().collect::<Vec<_>>();
|
|
let mut deferred_removals = Vec::new();
|
|
|
|
for crtc in existing {
|
|
if let Some(selection) = selections.get(&crtc)
|
|
&& let Some(output) = device.outputs.get_mut(&crtc)
|
|
&& selection.connector.handle() == output.connector
|
|
&& (selection.mode != output.mode || selection.settings != output.settings)
|
|
{
|
|
// Keep the wl_output/global and workspace ownership alive. A mode
|
|
// change is tested by DRM before updating client-visible geometry.
|
|
let result = if selection.mode != output.mode {
|
|
output.surface.use_mode(selection.mode)
|
|
} else {
|
|
Ok(())
|
|
};
|
|
|
|
match result {
|
|
Ok(()) => {
|
|
let old_geometry = state.space.output_geometry(&output.output);
|
|
output.mode = selection.mode;
|
|
cancel_output_timers(&state.loop_handle, output);
|
|
output.settings = selection.settings.clone();
|
|
let position = selection.settings.position.unwrap_or_else(|| {
|
|
state
|
|
.space
|
|
.output_geometry(&output.output)
|
|
.map(|g| [g.loc.x, g.loc.y])
|
|
.unwrap_or([0, 0])
|
|
});
|
|
|
|
output.output.change_current_state(
|
|
Some(OutputMode::from(output.mode)),
|
|
Some(output_transform(output.settings.transform)),
|
|
Some(Scale::Fractional(output.settings.scale)),
|
|
Some((position[0], position[1]).into()),
|
|
);
|
|
state.space.map_output(&output.output, (position[0], position[1]));
|
|
if let Some(id) = state.output_id(&output.output)
|
|
&& let Some(g) = state.space.output_geometry(&output.output)
|
|
{
|
|
state.output_workspaces.update_geometry(
|
|
id,
|
|
ferese_core::OutputGeometry::new(g.loc.x, g.loc.y, g.size.w, g.size.h),
|
|
);
|
|
if let Some(old) = old_geometry {
|
|
state.reposition_output_floats(
|
|
id,
|
|
ferese_layout::Rect::new(
|
|
old.loc.x as f64,
|
|
old.loc.y as f64,
|
|
old.size.w as f64,
|
|
old.size.h as f64,
|
|
),
|
|
ferese_layout::Rect::new(
|
|
g.loc.x as f64,
|
|
g.loc.y as f64,
|
|
g.size.w as f64,
|
|
g.size.h as f64,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
let clock = backend.presentation.entry((node, crtc)).or_default();
|
|
clock.set_refresh(OutputMode::from(output.mode).refresh);
|
|
clock.reset_timing();
|
|
}
|
|
}
|
|
|
|
Err(error) => {
|
|
tracing::warn!(?crtc, %error, "display change failed; retaining current output")
|
|
}
|
|
}
|
|
|
|
selections.remove(&crtc);
|
|
continue;
|
|
}
|
|
|
|
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
|
|
selections.get(&crtc).is_some_and(|selection| {
|
|
selection.connector.handle() == output.connector
|
|
&& selection.mode == output.mode
|
|
&& selection.settings == output.settings
|
|
})
|
|
});
|
|
|
|
if unchanged {
|
|
selections.remove(&crtc);
|
|
continue;
|
|
}
|
|
|
|
// Add replacements on free CRTCs before removing vanished outputs so
|
|
// workspace evacuation always has a real destination. Reused CRTCs
|
|
// still need their old surface released first.
|
|
if !selections.contains_key(&crtc) {
|
|
deferred_removals.push(crtc);
|
|
continue;
|
|
}
|
|
|
|
if let Some(mut output) = device.outputs.remove(&crtc) {
|
|
cancel_output_timers(&state.loop_handle, &mut output);
|
|
state.display_handle.disable_global::<Ferese>(output.global);
|
|
state.unregister_output(&output.output);
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.presentation.remove(&(node, crtc));
|
|
}
|
|
|
|
tracing::info!(?node, ?crtc, "removed DRM output");
|
|
}
|
|
}
|
|
|
|
for (_, selection) in selections {
|
|
let crtc = selection.crtc;
|
|
let mode = selection.mode;
|
|
match create_direct_output(state, &mut device.drm, &device.gbm, &device.renderer, selection) {
|
|
Ok(output) => {
|
|
state
|
|
.direct_backend
|
|
.as_mut()
|
|
.expect("direct backend state remains initialized")
|
|
.presentation
|
|
.entry((node, crtc))
|
|
.or_default()
|
|
.set_refresh(OutputMode::from(mode).refresh);
|
|
device.outputs.insert(crtc, output);
|
|
}
|
|
|
|
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to add DRM output"),
|
|
}
|
|
}
|
|
|
|
for crtc in deferred_removals {
|
|
let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal)
|
|
&& lid_closed
|
|
&& state.direct_backend.as_ref().is_some_and(|backend| {
|
|
device.outputs.get(&crtc).is_some_and(|output| {
|
|
backend
|
|
.connected_outputs
|
|
.iter()
|
|
.any(|info| info.connector == output.output.name())
|
|
})
|
|
})
|
|
&& !device.outputs.values().any(|output| !output.internal);
|
|
if keep_internal {
|
|
tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed");
|
|
if let Some(output) = device.outputs.get(&crtc)
|
|
&& let Some(backend) = state.direct_backend.as_mut()
|
|
&& let Some(info) = backend
|
|
.connected_outputs
|
|
.iter_mut()
|
|
.find(|info| info.connector == output.output.name())
|
|
{
|
|
info.enabled = true;
|
|
info.current_mode = Some(connected_mode_info(output.mode));
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if let Some(mut output) = device.outputs.remove(&crtc) {
|
|
cancel_output_timers(&state.loop_handle, &mut output);
|
|
state.display_handle.disable_global::<Ferese>(output.global);
|
|
state.unregister_output(&output.output);
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.presentation.remove(&(node, crtc));
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
for output in device.outputs.values() {
|
|
if let Some(info) = backend
|
|
.connected_outputs
|
|
.iter_mut()
|
|
.find(|info| info.connector == output.output.name())
|
|
{
|
|
info.enabled = true;
|
|
info.current_mode = Some(connected_mode_info(output.mode));
|
|
info.auto_refresh = output.settings.auto_refresh;
|
|
info.scale = output.settings.scale;
|
|
info.transform = output.settings.transform;
|
|
info.configured_position = output.settings.position;
|
|
}
|
|
}
|
|
}
|
|
|
|
restore_device(state, node, device);
|
|
state.restore_output_focus();
|
|
state.relayout();
|
|
render_all(state);
|
|
}
|
|
|
|
fn remove_device(state: &mut Ferese, node: DrmNode) {
|
|
let Some(device) = state
|
|
.direct_backend
|
|
.as_mut()
|
|
.and_then(|backend| backend.devices.remove(&node))
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let context = device.renderer.context_id().erased();
|
|
state.wallpaper.forget_context(&context);
|
|
state.resize_snapshots.retain(|_, snapshot| snapshot.context != context);
|
|
for (crtc, mut output) in device.outputs {
|
|
cancel_output_timers(&state.loop_handle, &mut output);
|
|
state.display_handle.disable_global::<Ferese>(output.global);
|
|
state.unregister_output(&output.output);
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.presentation.remove(&(node, crtc));
|
|
}
|
|
}
|
|
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.connected_outputs.clear();
|
|
}
|
|
|
|
tracing::info!(?node, "removed DRM device");
|
|
}
|
|
|
|
fn refresh_duration(state: &Ferese, node: DrmNode, crtc: crtc::Handle) -> Duration {
|
|
state
|
|
.direct_backend
|
|
.as_ref()
|
|
.and_then(|backend| backend.devices.get(&node))
|
|
.and_then(|device| device.outputs.get(&crtc))
|
|
.and_then(|output| output.output.current_mode())
|
|
.filter(|mode| mode.refresh > 0)
|
|
.map(|mode| Duration::from_nanos(1_000_000_000_000_u64 / mode.refresh as u64))
|
|
.unwrap_or_else(|| Duration::from_millis(16))
|
|
}
|
|
|
|
fn refresh_duration_from_mode(mode: DrmMode) -> Duration {
|
|
Duration::from_nanos(1_000_000_000_000 / OutputMode::from(mode).refresh.max(1) as u64)
|
|
}
|
|
|
|
fn animation_interval(state: &Ferese) -> Duration {
|
|
state
|
|
.direct_backend
|
|
.as_ref()
|
|
.into_iter()
|
|
.flat_map(|backend| backend.devices.values())
|
|
.flat_map(|device| device.outputs.values())
|
|
.filter_map(|output| output.output.current_mode())
|
|
.filter(|mode| mode.refresh > 0)
|
|
.map(|mode| Duration::from_nanos(1_000_000_000_000 / mode.refresh as u64))
|
|
.min()
|
|
.unwrap_or(Duration::from_millis(16))
|
|
}
|
|
|
|
fn arm_animation_timer(state: &mut Ferese) {
|
|
let needed = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.is_some_and(|backend| backend.animation_active && backend.animation_timer.is_none());
|
|
if !needed {
|
|
return;
|
|
}
|
|
|
|
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
|
match state
|
|
.loop_handle
|
|
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
|
|
let active = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.is_some_and(|backend| backend.active && backend.animation_active && !state.session_lock.sleeping);
|
|
if active {
|
|
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
|
if Instant::now() < deadline {
|
|
return TimeoutAction::ToInstant(deadline);
|
|
}
|
|
|
|
let animating = state.advance_animations(Instant::now());
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.animation_active = animating;
|
|
}
|
|
|
|
let outputs = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.into_iter()
|
|
.flat_map(|backend| backend.devices.iter())
|
|
.flat_map(|(node, device)| {
|
|
device
|
|
.outputs
|
|
.iter()
|
|
.map(|(crtc, output)| (*node, *crtc, output.output.clone()))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
for (node, crtc, output) in outputs {
|
|
if state.output_has_animations(&output) {
|
|
request_frame(state, node, crtc);
|
|
}
|
|
}
|
|
|
|
if state
|
|
.direct_backend
|
|
.as_ref()
|
|
.is_some_and(|backend| backend.animation_active)
|
|
{
|
|
return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state)));
|
|
}
|
|
}
|
|
|
|
if let Some(backend) = state.direct_backend.as_mut() {
|
|
backend.animation_timer = None;
|
|
}
|
|
|
|
TimeoutAction::Drop
|
|
}) {
|
|
Ok(token) => state.direct_backend.as_mut().unwrap().animation_timer = Some(token),
|
|
Err(error) => tracing::warn!(%error, "could not schedule DRM animation fallback"),
|
|
}
|
|
}
|
|
|
|
fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, target: Duration) {
|
|
if state.session_lock.sleeping {
|
|
return;
|
|
}
|
|
|
|
let now = Instant::now();
|
|
let timestamp = monotonic_now();
|
|
let Some(output) = state
|
|
.direct_backend
|
|
.as_ref()
|
|
.filter(|backend| backend.active)
|
|
.and_then(|backend| backend.devices.get(&node))
|
|
.filter(|device| device.drm.is_active())
|
|
.and_then(|device| device.outputs.get(&crtc))
|
|
else {
|
|
return;
|
|
};
|
|
|
|
if output.callback_timer.is_some() {
|
|
return;
|
|
}
|
|
|
|
let identity = output.output.clone();
|
|
let callback_at = output.scheduler.callback_deadline(timestamp, target);
|
|
if callback_at <= timestamp {
|
|
deliver_frame_callbacks(state, node, crtc, &identity);
|
|
return;
|
|
}
|
|
|
|
let deadline = now + callback_at.saturating_sub(timestamp);
|
|
match state
|
|
.loop_handle
|
|
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
|
|
deliver_frame_callbacks(state, node, crtc, &identity);
|
|
TimeoutAction::Drop
|
|
}) {
|
|
Ok(token) => {
|
|
if let Some(output) = state
|
|
.direct_backend
|
|
.as_mut()
|
|
.and_then(|backend| backend.devices.get_mut(&node))
|
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
|
{
|
|
output.callback_timer = Some(token);
|
|
}
|
|
}
|
|
|
|
Err(error) => tracing::warn!(%error, "could not schedule DRM frame callbacks"),
|
|
}
|
|
}
|
|
|
|
fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output) {
|
|
let Some(backend) = state.direct_backend.as_mut() else {
|
|
return;
|
|
};
|
|
|
|
let Some(device) = backend.devices.get_mut(&node) else {
|
|
return;
|
|
};
|
|
|
|
let Some(output) = device
|
|
.outputs
|
|
.get_mut(&crtc)
|
|
.filter(|output| &output.output == identity)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
output.callback_timer = None;
|
|
if !backend.active || !device.drm.is_active() || state.session_lock.sleeping {
|
|
return;
|
|
}
|
|
|
|
let timestamp = monotonic_now();
|
|
let next = output.scheduler.callback_deadline(timestamp, timestamp);
|
|
if next > timestamp {
|
|
schedule_frame_callbacks(state, node, crtc, next);
|
|
return;
|
|
}
|
|
|
|
output.scheduler.callback_sent(timestamp);
|
|
send_frame_callbacks(state, identity);
|
|
}
|
|
|
|
fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
|
|
if state.session_lock.active {
|
|
state.lock_frame_callbacks(output);
|
|
return;
|
|
}
|
|
state
|
|
.space
|
|
.elements()
|
|
.filter(|window| {
|
|
state
|
|
.window_ids
|
|
.get(*window)
|
|
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
|
})
|
|
.for_each(|window| {
|
|
window.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| {
|
|
Some(output.clone())
|
|
});
|
|
});
|
|
let layers = layer_map_for_output(output).layers().cloned().collect::<Vec<_>>();
|
|
layers.iter().for_each(|layer| {
|
|
layer.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| {
|
|
Some(output.clone())
|
|
});
|
|
});
|
|
state.send_cursor_frame(output);
|
|
state.space.refresh();
|
|
state.popups.cleanup();
|
|
layer_map_for_output(output).cleanup();
|
|
if let Err(error) = state.display_handle.flush_clients() {
|
|
tracing::debug!(%error, "failed to flush clients after DRM frame");
|
|
}
|
|
}
|
|
|
|
fn select_outputs(drm: &DrmDevice, profiles: &[OutputProfile], low_power: bool) -> io::Result<OutputScan> {
|
|
let resources = drm.resource_handles()?;
|
|
let connected = resources
|
|
.connectors()
|
|
.iter()
|
|
.map(|handle| drm.get_connector(*handle, true))
|
|
.collect::<io::Result<Vec<_>>>()?
|
|
.into_iter()
|
|
.filter(|connector| connector.state() == connector::State::Connected)
|
|
.collect::<Vec<_>>();
|
|
let identities = connected
|
|
.iter()
|
|
.map(|connector| connector_identity(drm, connector))
|
|
.collect::<Vec<_>>();
|
|
let active_profile = profiles.iter().find(|profile| {
|
|
profile.outputs.iter().all(|settings| {
|
|
connected
|
|
.iter()
|
|
.zip(&identities)
|
|
.any(|(connector, identity)| output_matches(settings, connector, identity))
|
|
})
|
|
});
|
|
if let Some(profile) = active_profile {
|
|
tracing::info!(profile = %profile.name, "selected output profile");
|
|
}
|
|
|
|
let mut selections = Vec::new();
|
|
let mut connected_outputs = Vec::new();
|
|
let mut used_crtcs = HashSet::new();
|
|
|
|
for (connector, identity) in connected.into_iter().zip(identities) {
|
|
let settings = active_profile
|
|
.and_then(|profile| {
|
|
profile
|
|
.outputs
|
|
.iter()
|
|
.find(|settings| output_matches(settings, &connector, &identity))
|
|
})
|
|
.cloned()
|
|
.unwrap_or_else(|| default_output_settings(connector.to_string()));
|
|
let selected_mode = settings
|
|
.enabled
|
|
.then(|| {
|
|
let normal = select_mode(&connector, settings.mode)?;
|
|
if settings.auto_refresh && low_power {
|
|
let modes = connector
|
|
.modes()
|
|
.iter()
|
|
.map(|mode| {
|
|
let (width, height) = mode.size();
|
|
(width, height, OutputMode::from(*mode).refresh)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
super::power::low_refresh_mode(normal.size(), &modes)
|
|
.map(|index| connector.modes()[index])
|
|
.or(Some(normal))
|
|
} else {
|
|
Some(normal)
|
|
}
|
|
})
|
|
.flatten();
|
|
connected_outputs.push(ConnectedOutputInfo {
|
|
connector: connector.to_string(),
|
|
identity: identity.clone(),
|
|
enabled: settings.enabled,
|
|
profile: active_profile.map(|profile| profile.name.clone()),
|
|
auto_refresh: settings.auto_refresh,
|
|
physical_size: connector.size(),
|
|
current_mode: selected_mode.map(connected_mode_info),
|
|
available_modes: connector.modes().iter().copied().map(connected_mode_info).collect(),
|
|
scale: settings.scale,
|
|
transform: settings.transform,
|
|
configured_position: settings.position,
|
|
});
|
|
if !settings.enabled {
|
|
tracing::info!(output = %connector, "output disabled by active profile");
|
|
continue;
|
|
}
|
|
let Some(mode) = selected_mode else {
|
|
continue;
|
|
};
|
|
|
|
let current_crtc = connector
|
|
.current_encoder()
|
|
.and_then(|handle| drm.get_encoder(handle).ok())
|
|
.and_then(|encoder| encoder.crtc())
|
|
.filter(|crtc| !used_crtcs.contains(crtc));
|
|
let compatible_crtc = connector.encoders().iter().find_map(|handle| {
|
|
let encoder = drm.get_encoder(*handle).ok()?;
|
|
resources
|
|
.filter_crtcs(encoder.possible_crtcs())
|
|
.into_iter()
|
|
.find(|crtc| !used_crtcs.contains(crtc))
|
|
});
|
|
if let Some(crtc) = current_crtc.or(compatible_crtc) {
|
|
used_crtcs.insert(crtc);
|
|
selections.push(OutputSelection {
|
|
connector,
|
|
crtc,
|
|
mode,
|
|
settings,
|
|
});
|
|
}
|
|
}
|
|
|
|
selections.sort_by_key(|selection| selection.settings.position.is_none());
|
|
Ok(OutputScan {
|
|
selections,
|
|
connected_outputs,
|
|
})
|
|
}
|
|
|
|
fn connected_mode_info(mode: DrmMode) -> ConnectedModeInfo {
|
|
let (width, height) = mode.size();
|
|
ConnectedModeInfo {
|
|
width,
|
|
height,
|
|
refresh_millihertz: OutputMode::from(mode).refresh,
|
|
preferred: mode.mode_type().contains(ModeTypeFlags::PREFERRED),
|
|
}
|
|
}
|
|
|
|
fn output_matches(settings: &OutputSettings, connector: &connector::Info, identity: &str) -> bool {
|
|
settings.matcher == connector.to_string() || settings.matcher == identity
|
|
}
|
|
|
|
fn default_output_settings(matcher: String) -> OutputSettings {
|
|
OutputSettings {
|
|
auto_refresh: false,
|
|
matcher,
|
|
enabled: true,
|
|
mode: None,
|
|
scale: 1.0,
|
|
transform: OutputTransform::Normal,
|
|
position: None,
|
|
}
|
|
}
|
|
|
|
fn select_mode(connector: &connector::Info, requested: Option<OutputModeRequest>) -> Option<DrmMode> {
|
|
let fallback = || {
|
|
connector
|
|
.modes()
|
|
.iter()
|
|
.find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED))
|
|
.or_else(|| connector.modes().first())
|
|
.copied()
|
|
};
|
|
let Some(requested) = requested else {
|
|
return fallback();
|
|
};
|
|
|
|
let matching = connector
|
|
.modes()
|
|
.iter()
|
|
.filter(|mode| mode.size() == (requested.width, requested.height));
|
|
let selected = if let Some(refresh) = requested.refresh_millihertz {
|
|
matching
|
|
.min_by_key(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh))
|
|
.filter(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh) <= 1_000)
|
|
.copied()
|
|
} else {
|
|
matching.max_by_key(|mode| OutputMode::from(**mode).refresh).copied()
|
|
};
|
|
if selected.is_none() {
|
|
tracing::warn!(
|
|
output = %connector,
|
|
width = requested.width,
|
|
height = requested.height,
|
|
refresh_millihertz = ?requested.refresh_millihertz,
|
|
"requested output mode is unavailable; using preferred mode"
|
|
);
|
|
}
|
|
|
|
selected.or_else(fallback)
|
|
}
|
|
|
|
fn create_direct_output(
|
|
state: &mut Ferese,
|
|
drm: &mut DrmDevice,
|
|
gbm: &GbmDevice<DrmDeviceFd>,
|
|
renderer: &GlesRenderer,
|
|
selection: OutputSelection,
|
|
) -> Result<DirectOutput, Box<dyn Error>> {
|
|
let OutputSelection {
|
|
connector,
|
|
crtc,
|
|
mode,
|
|
settings,
|
|
} = selection;
|
|
let identity = connector_identity(drm, &connector);
|
|
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
|
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
|
let surface = GbmBufferedSurface::new(
|
|
drm_surface,
|
|
allocator,
|
|
&[Fourcc::Argb8888, Fourcc::Abgr8888],
|
|
renderer.dmabuf_formats(),
|
|
)?;
|
|
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
|
// leave a phantom output/workspace that could receive evacuated windows.
|
|
let (output, global) = create_output(state, &connector, mode, identity, &settings);
|
|
let damage_tracker = OutputDamageTracker::from_output(&output);
|
|
let render_metrics = RenderMetrics::from_environment(output.name());
|
|
|
|
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
|
Ok(DirectOutput {
|
|
internal: internal_connector(connector.interface()),
|
|
connector: connector.handle(),
|
|
mode,
|
|
settings: settings.clone(),
|
|
output,
|
|
global,
|
|
surface,
|
|
damage_tracker,
|
|
render_metrics,
|
|
frame_pending: false,
|
|
power_off: false,
|
|
scheduler: crate::frame_scheduler::FrameScheduler::new(refresh_duration_from_mode(mode)),
|
|
render_timer: None,
|
|
callback_timer: None,
|
|
lock_frame_pending: false,
|
|
})
|
|
}
|
|
|
|
fn create_output(
|
|
state: &mut Ferese,
|
|
connector: &connector::Info,
|
|
mode: DrmMode,
|
|
identity: String,
|
|
settings: &OutputSettings,
|
|
) -> (Output, GlobalId) {
|
|
let name = connector.to_string();
|
|
let physical_size = connector.size().unwrap_or((0, 0));
|
|
let output = Output::new(
|
|
name,
|
|
PhysicalProperties {
|
|
size: (physical_size.0 as i32, physical_size.1 as i32).into(),
|
|
subpixel: Subpixel::from(connector.subpixel()),
|
|
make: "Unknown".into(),
|
|
model: "Unknown".into(),
|
|
},
|
|
);
|
|
let output_mode = OutputMode::from(mode);
|
|
|
|
let global = output.create_global::<Ferese>(&state.display_handle);
|
|
for mode in connector.modes().iter().copied().map(OutputMode::from) {
|
|
output.add_mode(mode);
|
|
}
|
|
output.set_preferred(output_mode);
|
|
let position = settings.position.unwrap_or_else(|| {
|
|
let x = state
|
|
.space
|
|
.outputs()
|
|
.filter_map(|output| state.space.output_geometry(output))
|
|
.map(|geometry| geometry.loc.x + geometry.size.w)
|
|
.max()
|
|
.unwrap_or(0);
|
|
[x, 0]
|
|
});
|
|
output.change_current_state(
|
|
Some(output_mode),
|
|
Some(output_transform(settings.transform)),
|
|
Some(Scale::Fractional(settings.scale)),
|
|
Some((position[0], position[1]).into()),
|
|
);
|
|
state.space.map_output(&output, (position[0], position[1]));
|
|
state.register_output(&output, identity);
|
|
(output, global)
|
|
}
|
|
|
|
fn output_transform(transform: OutputTransform) -> Transform {
|
|
match transform {
|
|
OutputTransform::Normal => Transform::Normal,
|
|
OutputTransform::Rotate90 => Transform::_90,
|
|
OutputTransform::Rotate180 => Transform::_180,
|
|
OutputTransform::Rotate270 => Transform::_270,
|
|
OutputTransform::Flipped => Transform::Flipped,
|
|
OutputTransform::Flipped90 => Transform::Flipped90,
|
|
OutputTransform::Flipped180 => Transform::Flipped180,
|
|
OutputTransform::Flipped270 => Transform::Flipped270,
|
|
}
|
|
}
|
|
|
|
fn connector_identity(drm: &DrmDevice, connector: &connector::Info) -> String {
|
|
let edid = drm.get_properties(connector.handle()).ok().and_then(|properties| {
|
|
properties.iter().find_map(|(handle, raw)| {
|
|
let info = drm.get_property(*handle).ok()?;
|
|
if info.name().to_bytes() != b"EDID" {
|
|
return None;
|
|
}
|
|
match info.value_type().convert_value(*raw) {
|
|
property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(),
|
|
_ => None,
|
|
}
|
|
})
|
|
});
|
|
|
|
match edid {
|
|
Some(edid) if !edid.is_empty() => format!("drm-edid:{:016x}", stable_hash(&edid)),
|
|
_ => format!("drm:{connector}"),
|
|
}
|
|
}
|
|
|
|
fn stable_hash(bytes: &[u8]) -> u64 {
|
|
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
|
|
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
|
|
|
|
bytes.iter().fold(FNV_OFFSET, |hash, byte| {
|
|
(hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
|
|
})
|
|
}
|
|
|
|
impl DirectBackendState {
|
|
pub fn record_presentation(
|
|
&mut self,
|
|
node: DrmNode,
|
|
crtc: crtc::Handle,
|
|
time: DrmEventTime,
|
|
sequence: u32,
|
|
) -> Option<Duration> {
|
|
let missed = self
|
|
.devices
|
|
.get(&node)
|
|
.and_then(|device| device.outputs.get(&crtc))
|
|
.map_or(0, |output| output.scheduler.missed_deadlines);
|
|
self.presentation.get_mut(&(node, crtc)).and_then(|clock| {
|
|
clock.missed_deadlines = missed;
|
|
clock.record(time, sequence)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl PresentationClock {
|
|
fn reset_timing(&mut self) {
|
|
self.last_presentation = None;
|
|
}
|
|
|
|
fn set_refresh(&mut self, refresh_millihertz: i32) {
|
|
if refresh_millihertz > 0 {
|
|
self.refresh_interval = Some(Duration::from_nanos(1_000_000_000_000_u64 / refresh_millihertz as u64));
|
|
}
|
|
}
|
|
|
|
fn record(&mut self, time: DrmEventTime, sequence: u32) -> Option<Duration> {
|
|
let DrmEventTime::Monotonic(time) = time else {
|
|
tracing::warn!(sequence, "DRM driver reported a realtime page-flip timestamp");
|
|
return None;
|
|
};
|
|
let delta = self.last_presentation.map(|previous| time.saturating_sub(previous));
|
|
|
|
self.last_presentation = Some(time);
|
|
self.presented_frames = self.presented_frames.saturating_add(1);
|
|
delta
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::time::SystemTime;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn lid_only_hides_panels_with_a_usable_external_output() {
|
|
assert!(lid_hides_panel(true, true));
|
|
assert!(!lid_hides_panel(false, true));
|
|
assert!(!lid_hides_panel(true, false));
|
|
assert!(!lid_hides_panel(false, false));
|
|
}
|
|
|
|
#[test]
|
|
fn panel_detection_uses_connector_type_not_monitor_name() {
|
|
assert!(internal_connector(connector::Interface::EmbeddedDisplayPort));
|
|
assert!(internal_connector(connector::Interface::LVDS));
|
|
assert!(internal_connector(connector::Interface::DSI));
|
|
assert!(!internal_connector(connector::Interface::HDMIA));
|
|
assert!(!internal_connector(connector::Interface::DisplayPort));
|
|
}
|
|
|
|
#[test]
|
|
fn connector_identity_hash_is_stable() {
|
|
assert_eq!(stable_hash(b"hello"), 0xa430_d846_80aa_bd0b);
|
|
}
|
|
|
|
#[test]
|
|
fn presentation_clock_uses_measured_monotonic_deltas() {
|
|
let mut clock = PresentationClock::default();
|
|
clock.set_refresh(60_000);
|
|
|
|
assert_eq!(
|
|
clock.record(DrmEventTime::Monotonic(Duration::from_millis(100)), 1),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
clock.record(DrmEventTime::Monotonic(Duration::from_millis(116)), 2),
|
|
Some(Duration::from_millis(16))
|
|
);
|
|
assert_eq!(clock.presented_frames, 2);
|
|
assert_eq!(clock.missed_deadlines, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn presentation_clock_rejects_realtime_timestamps() {
|
|
let mut clock = PresentationClock::default();
|
|
|
|
assert_eq!(clock.record(DrmEventTime::Realtime(SystemTime::now()), 1), None);
|
|
assert_eq!(clock.presented_frames, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn presentation_clock_does_not_count_idle_gaps_as_missed_deadlines() {
|
|
let mut clock = PresentationClock::default();
|
|
clock.set_refresh(60_000);
|
|
|
|
clock.record(DrmEventTime::Monotonic(Duration::ZERO), 1);
|
|
clock.record(DrmEventTime::Monotonic(Duration::from_millis(50)), 2);
|
|
|
|
assert_eq!(clock.missed_deadlines, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn presentation_clock_excludes_inactive_session_time() {
|
|
let mut clock = PresentationClock::default();
|
|
clock.set_refresh(60_000);
|
|
clock.record(DrmEventTime::Monotonic(Duration::from_millis(100)), 1);
|
|
|
|
clock.reset_timing();
|
|
|
|
assert_eq!(clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2), None);
|
|
assert_eq!(clock.presented_frames, 2);
|
|
assert_eq!(clock.missed_deadlines, 0);
|
|
}
|
|
}
|