atagen

monolith

public
Star0
Watch0

Commit dbdff401f909

tarball

atagen <boss@atagen.co>2026-10-19 10:44 UTC

7 files changed+1482-1411
drm: split into session, outputs, pacing, render and input modules

drm.rs had grown to 1400 lines with five unrelated concerns and a Surface lookup repeated nine times. The split follows the existing seams without changing behaviour, and surface_mut replaces the repeated backend.drm_mut().and_then(surfaces.get_mut) chains in the pacing timers.
Deleted file: crates/monolith/src/drm.rs−1411 deletions
@@ -1,1411 +0,0 @@
1-//! Bare metal. A libseat session, udev for the primary GPU and its
2-//! connectors, libinput for input, and one smithay DRM output per connector,
3-//! each rendered when its own vblank lands.
4-
5-use crate::backend::{Backend, logical_area};
6-use crate::devices::Devices;
7-use crate::input::Motion;
8-use crate::scene::Draw;
9-use crate::state::Services;
10-use crate::state::{State, now_ns};
11-use crate::wm::Wm;
12-use monolith_config::{OutputTransform, Vrr};
13-use monolith_layout::{OutputId, Rect};
14-use smithay::backend::allocator::Fourcc;
15-use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
16-use smithay::backend::drm::VrrSupport;
17-use smithay::backend::drm::compositor::{FrameFlags, PrimaryPlaneElement};
18-use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
19-use smithay::backend::drm::output::{DrmOutput, DrmOutputManager, DrmOutputRenderElements};
20-use smithay::backend::drm::{
21- DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
22-};
23-use smithay::backend::input::{
24- AbsolutePositionEvent, Axis, AxisSource, Event, InputEvent, InputTime, KeyboardKeyEvent,
25- PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
26-};
27-use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
28-use smithay::backend::session::libseat::LibSeatSession;
29-use smithay::backend::session::{Event as SessionEvent, Session};
30-use smithay::backend::udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu};
31-use smithay::input::pointer::AxisFrame;
32-use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
33-use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
34-use smithay::reexports::calloop::{EventLoop, LoopHandle, RegistrationToken};
35-use smithay::reexports::drm::Device as _;
36-use smithay::reexports::drm::control::{self, ModeTypeFlags, connector, crtc};
37-use smithay::reexports::input::Libinput;
38-use smithay::reexports::input::event::switch::{Switch, SwitchState};
39-use smithay::reexports::rustix::fs::OFlags;
40-use smithay::reexports::wayland_server::backend::GlobalId;
41-use smithay::desktop::utils::OutputPresentationFeedback;
42-use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind;
43-use smithay::utils::{DeviceFd, Logical, Monotonic, Point, Time, Transform};
44-use smithay::wayland::presentation::Refresh;
45-use smithay::wayland::drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd};
46-use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
47-use std::collections::{HashMap, HashSet};
48-use std::path::{Path, PathBuf};
49-use std::time::Duration;
50-
51-use crate::frame::{CLEAR, FrameCtx};
52-/// Frames that only move the cursor stay at or below 60 Hz under VRR, so the
53-/// mouse does not override the refresh rate a game is pacing.
54-const CURSOR_FRAME_NS: u64 = 16_666_667;
55-
56-/// How long a disabled wl_output stays bound before it is removed, so
57-/// clients that were mid-bind are not killed by the removal.
58-const GLOBAL_REMOVAL_DELAY: Duration = Duration::from_secs(5);
59-
60-/// Frames render without vblanks while retrying or looping on empty frames,
61-/// and only a vblank drains the samples.
62-const MAX_CPU_SAMPLES: usize = 1024;
63-
64-type Allocator = GbmAllocator<DrmDeviceFd>;
65-type Exporter = GbmFramebufferExporter<DrmDeviceFd>;
66-type Manager = DrmOutputManager<Allocator, Exporter, OutputPresentationFeedback, DrmDeviceFd>;
67-type Out = DrmOutput<Allocator, Exporter, OutputPresentationFeedback, DrmDeviceFd>;
68-
69-pub struct Drm {
70- pub session: LibSeatSession,
71- libinput: Libinput,
72- handle: LoopHandle<'static, State>,
73- node: DrmNode,
74- pub render_node: DrmNode,
75- pub syncobj: Option<DrmSyncobjState>,
76- manager: Manager,
77- scanner: DrmScanner,
78- nvidia: bool,
79- frame_flags: FrameFlags,
80- surfaces: HashMap<crtc::Handle, Surface>,
81- /// Connected connectors the scanner has already reported but that are
82- /// not lit, retried on the next scan.
83- failed: HashSet<connector::Handle>,
84- pub devices: Devices,
85-}
86-
87-struct Surface {
88- output: Output,
89- global: GlobalId,
90- id: OutputId,
91- drm: Out,
92- /// A frame is on its way to the screen and its vblank has not landed.
93- queued: bool,
94- queued_at: u64,
95- /// The panel is lit. Off outputs are not rendered to.
96- powered: bool,
97- /// Out of the layout and Space, but still driven so it can relight.
98- parked: bool,
99- /// Fires when a queued frame's vblank never arrives.
100- watchdog: Option<RegistrationToken>,
101- /// Consecutive frames that failed to render or queue.
102- failures: u32,
103- redraw: bool,
104- last_vblank_ns: Option<u64>,
105- refresh_ns: u64,
106- /// The pending render, timed to start just in time for `target_ns`.
107- scheduled: Option<RegistrationToken>,
108- /// `scheduled` only waits out a cap or a retry, so fresh content may
109- /// cancel it and draw at once.
110- preemptible: bool,
111- /// The vblank the frame being drawn or queued is meant for.
112- target_ns: Option<u64>,
113- budget: crate::pacing::Budget,
114- /// Frames presented since `stats_since`, for the once-a-second debug line.
115- presented: u32,
116- stats_since: u64,
117- vrr_capable: bool,
118- vrr_on: bool,
119- /// Only the cursor moved, which under VRR is drawn at a capped rate.
120- cursor_redraw: bool,
121- /// The last frame went straight to the primary plane without compositing.
122- scanout: bool,
123- /// Frames scanned out and per-frame compositor CPU times since
124- /// `stats_since`, for the targets in decisions.md.
125- scanned_out: u32,
126- frame_cpu_ns: Vec<u64>,
127-}
128-
129-impl Surface {
130- /// The first vblank a render starting at `now` can still make, and the
131- /// latest moment to start it.
132- fn next_deadline(&self, now: u64) -> Option<(u64, u64)> {
133- crate::pacing::deadline(self.last_vblank_ns?, self.refresh_ns, self.budget.ns(), now)
134- }
135-
136- /// How long a queued frame may go without its vblank. Under VRR a flip
137- /// waits for the panel's longest frame, so the floor stays well above it.
138- fn flip_timeout(&self) -> u64 {
139- (self.refresh_ns * 4).max(100_000_000)
140- }
141-}
142-
143-/// The GPU monolith drives on bare metal, the seat's primary or else its
144-/// first.
145-pub fn primary() -> Result<PathBuf, Box<dyn std::error::Error>> {
146- let seat = std::env::var("XDG_SEAT").unwrap_or_else(|_| "seat0".into());
147- Ok(match primary_gpu(&seat)? {
148- Some(p) => p,
149- None => all_gpus(&seat)?.into_iter().next().ok_or("no GPU found")?,
150- })
151-}
152-
153-pub fn init(
154- event_loop: &mut EventLoop<'static, State>,
155- state: &mut State,
156- path: &Path,
157-) -> Result<(), Box<dyn std::error::Error>> {
158- let (mut session, notifier) = LibSeatSession::new()?;
159- let seat = session.seat();
160- let node = DrmNode::from_path(path)?;
161- tracing::info!(path = %path.display(), "primary GPU");
162- let fd = session.open(
163- path,
164- OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
165- )?;
166- let fd = DrmDeviceFd::new(DeviceFd::from(fd));
167- let (device, drm_events) = DrmDevice::new(fd.clone(), true)?;
168- let nvidia = device
169- .get_driver()
170- .map(|d| d.name().to_string_lossy().to_lowercase().contains("nvidia"))
171- .unwrap_or(false);
172- let syncobj = if supports_syncobj_eventfd(&fd) {
173- Some(DrmSyncobjState::new::<State>(
174- &state.core.display,
175- fd.clone(),
176- ))
177- } else {
178- tracing::info!("no syncobj eventfd support, clients stay on implicit sync");
179- None
180- };
181- let gbm = GbmDevice::new(fd)?;
182- let render_node = node
183- .node_with_type(NodeType::Render)
184- .and_then(Result::ok)
185- .unwrap_or(node);
186- let allocator = GbmAllocator::new(
187- gbm.clone(),
188- GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
189- );
190- let exporter = GbmFramebufferExporter::new(gbm.clone(), render_node.into());
191- let render_formats: Vec<_> = state.renderer.render_formats().iter().copied().collect();
192- let manager = DrmOutputManager::new(
193- device,
194- allocator,
195- exporter,
196- Some(gbm),
197- [Fourcc::Xrgb8888, Fourcc::Argb8888],
198- render_formats,
199- );
200-
201- let mut libinput =
202- Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
203- libinput
204- .udev_assign_seat(&seat)
205- .map_err(|()| "libinput could not take the seat")?;
206- let handle = event_loop.handle();
207- handle.insert_source(
208- LibinputInputBackend::new(libinput.clone()),
209- |event, _, state| {
210- on_input(state, event);
211- },
212- )?;
213- handle.insert_source(notifier, |event, _, state| match event {
214- SessionEvent::PauseSession => {
215- if let Some(drm) = state.backend.drm_mut() {
216- drm.libinput.suspend();
217- drm.manager.pause();
218- }
219- }
220- SessionEvent::ActivateSession => {
221- if let Some(drm) = state.backend.drm_mut() {
222- if drm.libinput.resume().is_err() {
223- tracing::warn!("libinput did not resume");
224- }
225- if let Err(e) = drm.manager.lock().activate(false) {
226- tracing::warn!("could not reactivate the GPU: {e}");
227- }
228- for s in drm.surfaces.values_mut() {
229- if let Some(token) = s.scheduled.take() {
230- drm.handle.remove(token);
231- }
232- if let Err(e) = s.drm.frame_submitted() {
233- tracing::warn!("could not drop the flip left over from the pause: {e}");
234- }
235- s.queued = false;
236- s.last_vblank_ns = None;
237- s.target_ns = None;
238- s.failures = 0;
239- s.redraw = true;
240- if !s.powered
241- && let Err(e) = s.drm.with_compositor(|c| c.clear())
242- {
243- tracing::warn!("could not keep the output off: {e}");
244- }
245- }
246- }
247- scan(state);
248- state.dirty.redraw = true;
249- }
250- })?;
251- handle.insert_source(drm_events, |event, meta, state| match event {
252- DrmEvent::VBlank(crtc) => on_vblank(state, crtc, meta),
253- DrmEvent::Error(e) => tracing::warn!("drm: {e}"),
254- })?;
255- let udev = UdevBackend::new(&seat)?;
256- handle.insert_source(udev, move |event, _, state| {
257- if let UdevEvent::Changed { device_id } = event
258- && state
259- .backend
260- .drm()
261- .is_some_and(|d| d.node.dev_id() == device_id && d.session.is_active())
262- {
263- scan(state);
264- }
265- })?;
266-
267- state.backend = Backend::Drm(Box::new(Drm {
268- session,
269- libinput,
270- handle,
271- node,
272- render_node,
273- syncobj,
274- manager,
275- scanner: DrmScanner::new(),
276- nvidia,
277- frame_flags: if std::env::var_os("MONOLITH_NO_SCANOUT").is_some() {
278- FrameFlags::empty()
279- } else {
280- FrameFlags::DEFAULT
281- },
282- surfaces: HashMap::new(),
283- failed: HashSet::new(),
284- devices: Devices::default(),
285- }));
286- scan(state);
287- Ok(())
288-}
289-
290-/// Brings outputs in line with what is plugged in.
291-fn scan(state: &mut State) {
292- let Some(drm) = state.backend.drm_mut() else {
293- return;
294- };
295- let events: Vec<DrmScanEvent> = match drm.scanner.scan_connectors(drm.manager.device()) {
296- Ok(result) => result.into_iter().collect(),
297- Err(e) => {
298- tracing::warn!("scanning connectors failed: {e}");
299- return;
300- }
301- };
302- let retry = std::mem::take(&mut drm.failed);
303- for event in events {
304- match event {
305- DrmScanEvent::Connected {
306- connector,
307- crtc: Some(crtc),
308- } => connect(state, connector, crtc),
309- DrmScanEvent::Connected {
310- connector,
311- crtc: None,
312- } => {
313- tracing::warn!(handle = ?connector.handle(), "no free crtc for the connector");
314- if let Some(drm) = state.backend.drm_mut() {
315- drm.failed.insert(connector.handle());
316- }
317- }
318- DrmScanEvent::Disconnected {
319- crtc: Some(crtc), ..
320- } => disconnect(state, crtc),
321- DrmScanEvent::Changed {
322- connector,
323- crtc: Some(crtc),
324- } => changed(state, connector, crtc),
325- _ => {}
326- }
327- }
328- for handle in retry {
329- retry_connector(state, handle);
330- }
331- state.apply_power();
332- state.lid_act();
333- state.apply_layout();
334-}
335-
336-/// Lights a connector that failed on an earlier scan, which the scanner
337-/// will not report again until it changes.
338-fn retry_connector(state: &mut State, handle: connector::Handle) {
339- let Some(drm) = state.backend.drm_mut() else {
340- return;
341- };
342- if drm.failed.contains(&handle) {
343- return;
344- }
345- let Some(info) = drm.scanner.connectors().get(&handle) else {
346- return;
347- };
348- if info.state() != connector::State::Connected {
349- return;
350- }
351- let info = info.clone();
352- let Some(crtc) = drm.scanner.crtc_for_connector(&handle) else {
353- drm.failed.insert(handle);
354- return;
355- };
356- if !drm.surfaces.contains_key(&crtc) {
357- connect(state, info, crtc);
358- }
359-}
360-
361-fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
362- let name = connector_name(&connector);
363- let config = state
364- .settings
365- .config
366- .output
367- .named
368- .get(&name)
369- .cloned()
370- .unwrap_or_default();
371- let Some(mode) = choose_mode(&connector, &name, config.mode.as_deref()) else {
372- tracing::warn!(name, "connector has no modes");
373- if let Some(drm) = state.backend.drm_mut() {
374- drm.failed.insert(connector.handle());
375- }
376- return;
377- };
378- let transform = transform(config.transform);
379- let State {
380- backend: Backend::Drm(drm),
381- renderer,
382- wm: Wm { layout, space, .. },
383- core,
384- ..
385- } = state
386- else {
387- return;
388- };
389- let (mm_w, mm_h) = connector.size().unwrap_or((0, 0));
390- let output = Output::new(
391- name.clone(),
392- PhysicalProperties {
393- size: (mm_w as i32, mm_h as i32).into(),
394- subpixel: Subpixel::Unknown,
395- make: "Unknown".into(),
396- model: name.clone(),
397- serial_number: String::new(),
398- },
399- );
400- let wl_mode = Mode::from(mode);
401- let right = layout
402- .outputs()
403- .map(|(_, o)| o.area.right())
404- .fold(0.0, f64::max);
405- let position = match config.position {
406- Some([x, y]) => Point::from((x, y)),
407- None => Point::from((right.ceil() as i32, 0)),
408- };
409- let scale = config.scale();
410- output.change_current_state(
411- Some(wl_mode),
412- Some(transform),
413- Some(Scale::Fractional(scale)),
414- Some(position),
415- );
416- output.set_preferred(wl_mode);
417- let mut planes = drm.manager.device().planes(&crtc).ok();
418- if drm.nvidia
419- && let Some(p) = planes.as_mut()
420- {
421- // NVIDIA's overlay planes break composition, as anvil also found.
422- p.overlay.clear();
423- }
424- let out = match drm.manager.lock().initialize_output::<_, Draw>(
425- crtc,
426- mode,
427- &[connector.handle()],
428- &output,
429- planes,
430- renderer,
431- &DrmOutputRenderElements::default(),
432- ) {
433- Ok(out) => out,
434- Err(e) => {
435- tracing::warn!(name, "could not light up the output: {e}");
436- drm.failed.insert(connector.handle());
437- return;
438- }
439- };
440- let vrr_capable = matches!(
441- out.with_compositor(|c| c.vrr_supported(connector.handle())),
442- Ok(VrrSupport::Supported)
443- );
444- let global = output.create_global::<State>(&core.display);
445- space.map_output(&output, position);
446- let (w, h) = mode.size();
447- let area = logical_area(position, wl_mode.size, transform, scale);
448- let id = layout.add_output(&name, area);
449- tracing::info!(
450- name,
451- w,
452- h,
453- scale,
454- refresh = mode.vrefresh(),
455- ?transform,
456- x = position.x,
457- y = position.y,
458- vrr_capable,
459- "output connected"
460- );
461- drm.surfaces.insert(
462- crtc,
463- Surface {
464- output,
465- global,
466- id,
467- drm: out,
468- queued: false,
469- queued_at: 0,
470- powered: true,
471- parked: false,
472- watchdog: None,
473- failures: 0,
474- redraw: true,
475- last_vblank_ns: None,
476- scheduled: None,
477- preemptible: false,
478- target_ns: None,
479- budget: crate::pacing::Budget::new(),
480- presented: 0,
481- stats_since: 0,
482- vrr_capable,
483- vrr_on: false,
484- cursor_redraw: false,
485- scanout: false,
486- scanned_out: 0,
487- frame_cpu_ns: Vec::new(),
488- refresh_ns: 1_000_000_000_000 / u64::from(wl_mode.refresh.max(1) as u32),
489- },
490- );
491-}
492-
493-fn connector_name(connector: &connector::Info) -> String {
494- format!(
495- "{}-{}",
496- connector.interface().as_str(),
497- connector.interface_id()
498- )
499-}
500-
501-/// The configured mode if the connector has it, else its preferred one.
502-fn choose_mode(
503- connector: &connector::Info,
504- name: &str,
505- configured: Option<&str>,
506-) -> Option<control::Mode> {
507- let chosen = configured.and_then(|spec| {
508- let found = pick_mode(connector.modes(), spec);
509- if found.is_none() {
510- tracing::warn!(name, spec, "no such mode, using the preferred one");
511- }
512- found
513- });
514- chosen.or_else(|| {
515- connector
516- .modes()
517- .iter()
518- .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
519- .or_else(|| connector.modes().first())
520- .copied()
521- })
522-}
523-
524-fn changed(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
525- if !remode(state, &connector, crtc) {
526- disconnect(state, crtc);
527- connect(state, connector, crtc);
528- }
529-}
530-
531-/// Moves a lit output to the mode its connector now calls for without
532-/// recreating the wl_output, so clients, windows, the lock surface and the
533-/// power state stay as they are. False means the output has to be rebuilt.
534-fn remode(state: &mut State, connector: &connector::Info, crtc: crtc::Handle) -> bool {
535- let name = connector_name(connector);
536- let State {
537- backend: Backend::Drm(drm),
538- renderer,
539- wm: Wm { layout, .. },
540- settings,
541- ..
542- } = state
543- else {
544- return false;
545- };
546- let Some(s) = drm.surfaces.get_mut(&crtc) else {
547- return false;
548- };
549- let (mm_w, mm_h) = connector.size().unwrap_or((0, 0));
550- if s.output.physical_properties().size != (mm_w as i32, mm_h as i32).into() {
551- return false;
552- }
553- let configured = settings.config.output.named.get(&name);
554- let Some(mode) = choose_mode(connector, &name, configured.and_then(|c| c.mode.as_deref()))
555- else {
556- return false;
557- };
558- let wl_mode = Mode::from(mode);
559- let moved = s.output.current_mode() != Some(wl_mode);
560- if moved {
561- let elements = DrmOutputRenderElements::default();
562- if let Err(e) = s.drm.use_mode::<_, Draw>(mode, renderer, &elements) {
563- tracing::warn!(name, "could not change the mode in place: {e}");
564- return false;
565- }
566- s.output
567- .change_current_state(Some(wl_mode), None, None, None);
568- s.refresh_ns = 1_000_000_000_000 / u64::from(wl_mode.refresh.max(1) as u32);
569- s.last_vblank_ns = None;
570- s.target_ns = None;
571- s.redraw = true;
572- let area = logical_area(
573- s.output.current_location(),
574- wl_mode.size,
575- s.output.current_transform(),
576- s.output.current_scale().fractional_scale(),
577- );
578- layout.set_output_area(s.id, area);
579- tracing::info!(
580- name,
581- w = wl_mode.size.w,
582- h = wl_mode.size.h,
583- refresh = mode.vrefresh(),
584- "output mode changed"
585- );
586- }
587- s.output.set_preferred(wl_mode);
588- if moved {
589- reconfigure_outputs(state);
590- state.reserve_layers();
591- state.reconfigure_lock();
592- }
593- true
594-}
595-
596-/// The mode matching `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`, the highest rate
597-/// when none is given and the nearest one when it is.
598-fn pick_mode(modes: &[control::Mode], spec: &str) -> Option<control::Mode> {
599- let (size, rate) = match spec.split_once('@') {
600- Some((size, rate)) => (size, Some(rate.trim().parse::<f64>().ok()?)),
601- None => (spec, None),
602- };
603- let (w, h) = size.trim().split_once('x')?;
604- let size = (w.trim().parse::<u16>().ok()?, h.trim().parse::<u16>().ok()?);
605- let fitting = modes.iter().filter(|m| m.size() == size);
606- match rate {
607- Some(r) => fitting
608- .min_by(|a, b| {
609- let off = |m: &&control::Mode| (f64::from(m.vrefresh()) - r).abs();
610- off(a).total_cmp(&off(b))
611- })
612- .copied(),
613- None => fitting.max_by_key(|m| m.vrefresh()).copied(),
614- }
615-}
616-
617-fn transform(t: OutputTransform) -> Transform {
618- match t {
619- OutputTransform::Normal => Transform::Normal,
620- OutputTransform::Rotate90 => Transform::_90,
621- OutputTransform::Rotate180 => Transform::_180,
622- OutputTransform::Rotate270 => Transform::_270,
623- OutputTransform::Flipped => Transform::Flipped,
624- OutputTransform::Flipped90 => Transform::Flipped90,
625- OutputTransform::Flipped180 => Transform::Flipped180,
626- OutputTransform::Flipped270 => Transform::Flipped270,
627- }
628-}
629-
630-/// Outputs without a configured position are laid out left to right in
631-/// their current order, so one rotating pushes its neighbours along.
632-pub fn reconfigure_outputs(state: &mut State) {
633- let State {
634- backend: Backend::Drm(drm),
635- wm: Wm { layout, space, .. },
636- settings,
637- ..
638- } = state
639- else {
640- return;
641- };
642- let mut surfaces: Vec<&mut Surface> = drm.surfaces.values_mut().filter(|s| !s.parked).collect();
643- surfaces.sort_by_cached_key(|s| {
644- let name = s.output.name();
645- (
646- s.output.current_location().x,
647- !crate::power::internal(&name),
648- )
649- });
650- let mut right = 0;
651- let mut changed = false;
652- for s in surfaces {
653- let config = settings
654- .config
655- .output
656- .named
657- .get(&s.output.name())
658- .cloned()
659- .unwrap_or_default();
660- let Some(mode) = s.output.current_mode() else {
661- continue;
662- };
663- let transform = transform(config.transform);
664- let scale = config.scale();
665- let position: Point<i32, Logical> = match config.position {
666- Some([x, y]) => (x, y).into(),
667- None => (right, 0).into(),
668- };
669- let area = logical_area(position, mode.size, transform, scale);
670- right = right.max(area.right().ceil() as i32);
671- if s.output.current_transform() == transform
672- && s.output.current_location() == position
673- && s.output.current_scale().fractional_scale() == scale
674- {
675- continue;
676- }
677- s.output.change_current_state(
678- None,
679- Some(transform),
680- Some(Scale::Fractional(scale)),
681- Some(position),
682- );
683- space.map_output(&s.output, position);
684- layout.set_output_area(s.id, area);
685- s.redraw = true;
686- changed = true;
687- tracing::info!(
688- name = s.output.name(),
689- ?transform,
690- scale,
691- x = position.x,
692- y = position.y,
693- "output reconfigured"
694- );
695- }
696- if changed {
697- state.reserve_layers();
698- state.reconfigure_lock();
699- state.apply_layout();
700- }
701-}
702-
703-fn disconnect(state: &mut State, crtc: crtc::Handle) {
704- let Some(drm) = state.backend.drm_mut() else {
705- return;
706- };
707- let Some(surface) = drm.surfaces.remove(&crtc) else {
708- return;
709- };
710- for token in [surface.scheduled, surface.watchdog].into_iter().flatten() {
711- drm.handle.remove(token);
712- }
713- tracing::info!(name = surface.output.name(), "output disconnected");
714- state
715- .core
716- .display
717- .disable_global::<State>(surface.global.clone());
718- let global = surface.global.clone();
719- let removal = Timer::from_duration(GLOBAL_REMOVAL_DELAY);
720- let armed = drm.handle.insert_source(removal, move |_, _, state| {
721- state.core.display.remove_global::<State>(global.clone());
722- TimeoutAction::Drop
723- });
724- if let Err(e) = armed {
725- tracing::warn!("could not defer removing the wl_output: {e}");
726- state
727- .core
728- .display
729- .remove_global::<State>(surface.global.clone());
730- }
731- state.wm.space.unmap_output(&surface.output);
732- state.wm.layout.remove_output(surface.id);
733- state.close_layers(&surface.output);
734- state.services.lock.forget(&surface.output);
735- state.services.power.forget(&surface.output.name());
736- state.protocols.output_power.output_removed(&surface.output);
737-}
738-
739-/// Takes the output called `name` out of the layout and Space, or puts it
740-/// back. Its windows move to the other outputs and return when it does.
741-pub fn set_parked(state: &mut State, name: &str, parked: bool) {
742- let State {
743- backend: Backend::Drm(drm),
744- wm: Wm { layout, space, .. },
745- ..
746- } = state
747- else {
748- return;
749- };
750- let Some(s) = drm.surfaces.values_mut().find(|s| s.output.name() == name) else {
751- return;
752- };
753- if s.parked == parked {
754- return;
755- }
756- s.parked = parked;
757- tracing::info!(name, parked, "output parked");
758- if parked {
759- space.unmap_output(&s.output);
760- layout.remove_output(s.id);
761- } else if let Some(mode) = s.output.current_mode() {
762- let position = s.output.current_location();
763- space.map_output(&s.output, position);
764- let scale = s.output.current_scale().fractional_scale();
765- let area = logical_area(position, mode.size, s.output.current_transform(), scale);
766- s.id = layout.add_output(name, area);
767- s.redraw = true;
768- }
769- let pos = state.input.pointer_pos;
770- let held = clamp(state, pos);
771- if held != pos {
772- state.warp_pointer(held, InputTime::now());
773- }
774- if !parked {
775- reconfigure_outputs(state);
776- }
777- state.reserve_layers();
778- state.reconfigure_lock();
779- state.apply_layout();
780-}
781-
782-impl Drm {
783- pub fn names(&self) -> Vec<String> {
784- self.surfaces.values().map(|s| s.output.name()).collect()
785- }
786-
787- /// Lights or darkens the output called `name`. Dark outputs get DPMS off
788- /// and no frames, and the next frame after turning on is a full modeset.
789- pub fn set_powered(&mut self, name: &str, on: bool) {
790- let active = self.session.is_active();
791- for s in self
792- .surfaces
793- .values_mut()
794- .filter(|s| s.output.name() == name)
795- {
796- if s.powered == on {
797- continue;
798- }
799- s.powered = on;
800- tracing::info!(name, on, "output power");
801- if on {
802- s.redraw = true;
803- s.last_vblank_ns = None;
804- continue;
805- }
806- for token in [s.scheduled.take(), s.watchdog.take()]
807- .into_iter()
808- .flatten()
809- {
810- self.handle.remove(token);
811- }
812- s.queued = false;
813- s.target_ns = None;
814- s.cursor_redraw = false;
815- if active && let Err(e) = s.drm.with_compositor(|c| c.clear()) {
816- tracing::warn!(name, "could not switch the output off: {e}");
817- }
818- }
819- }
820-
821- pub fn output(&self, id: OutputId) -> Option<Output> {
822- self.surfaces
823- .values()
824- .find(|s| s.id == id)
825- .map(|s| s.output.clone())
826- }
827-
828- pub fn timeline(&self, output: &Output) -> Option<crate::timing::Timeline> {
829- let s = self.surfaces.values().find(|s| s.output == *output)?;
830- Some(crate::timing::Timeline {
831- last: s.last_vblank_ns?,
832- refresh: if s.vrr_on { 0 } else { s.refresh_ns },
833- budget: s.budget.ns(),
834- })
835- }
836-}
837-
838-fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMetadata>) {
839- let presented = state
840- .backend
841- .drm()
842- .and_then(|d| d.surfaces.get(&crtc))
843- .map(|s| s.output.clone());
844- if let Some(output) = presented {
845- crate::fifo::presented(state, &output);
846- }
847- let Some(surface) = state
848- .backend
849- .drm_mut()
850- .and_then(|d| d.surfaces.get_mut(&crtc))
851- else {
852- return;
853- };
854- let feedback = surface.drm.frame_submitted().unwrap_or_else(|e| {
855- tracing::warn!("frame was not submitted: {e}");
856- None
857- });
858- surface.queued = false;
859- if let Err(e) = state.renderer.absorb_submit_stall() {
860- tracing::warn!("absorbing a submit stall failed: {e}");
861- }
862- let Some(surface) = state
863- .backend
864- .drm_mut()
865- .and_then(|d| d.surfaces.get_mut(&crtc))
866- else {
867- return;
868- };
869- if let Some(mut feedback) = feedback {
870- let (time, clock) = match meta.as_ref().map(|m| m.time) {
871- Some(DrmEventTime::Monotonic(t)) => (t, Kind::HwClock | Kind::HwCompletion),
872- _ => (Duration::from_nanos(now_ns()), Kind::empty()),
873- };
874- let refresh = Duration::from_nanos(surface.refresh_ns);
875- let refresh = if surface.vrr_on {
876- Refresh::variable(refresh)
877- } else {
878- Refresh::fixed(refresh)
879- };
880- let sequence = meta.as_ref().map_or(0, |m| u64::from(m.sequence));
881- feedback.presented(
882- Time::<Monotonic>::from(time),
883- refresh,
884- sequence,
885- Kind::Vsync | clock,
886- );
887- }
888- if let Some(DrmEventTime::Monotonic(t)) = meta.as_ref().map(|m| m.time) {
889- let t = t.as_nanos() as u64;
890- if let Some(target) = surface.target_ns.take() {
891- if t > target + surface.refresh_ns / 2 {
892- surface.budget.missed(surface.refresh_ns);
893- tracing::debug!(
894- output = surface.output.name(),
895- late_ns = t - target,
896- budget_ns = surface.budget.ns(),
897- "frame missed its vblank"
898- );
899- } else {
900- surface.budget.landed();
901- }
902- }
903- surface.last_vblank_ns = Some(t);
904- surface.presented += 1;
905- let window = t.saturating_sub(surface.stats_since);
906- if window >= 1_000_000_000 {
907- // Under VRR the panel waits for each flip, so the nominal refresh
908- // says nothing about how many vblanks there were.
909- let vblanks = (!surface.vrr_on).then(|| window / surface.refresh_ns.max(1));
910- let cpu = &mut surface.frame_cpu_ns;
911- cpu.sort_unstable();
912- let p99 = cpu.get(cpu.len().saturating_sub(1) * 99 / 100).copied();
913- tracing::debug!(
914- output = surface.output.name(),
915- presented = surface.presented,
916- vblanks,
917- vrr = surface.vrr_on,
918- scanned_out = surface.scanned_out,
919- cpu_p99_us = p99.map(|ns| ns / 1000),
920- budget_us = surface.budget.ns() / 1000,
921- "frame rate"
922- );
923- cpu.clear();
924- surface.scanned_out = 0;
925- surface.presented = 0;
926- surface.stats_since = t;
927- }
928- }
929- if surface.redraw || surface.cursor_redraw {
930- schedule(state, crtc);
931- }
932-}
933-
934-/// Starts a render at the last moment that still makes the next reachable
935-/// vblank, so commits arriving until then make it into the frame.
936-fn schedule(state: &mut State, crtc: crtc::Handle) {
937- let Some(drm) = state.backend.drm_mut() else {
938- return;
939- };
940- if !drm.session.is_active() {
941- return;
942- }
943- let Some(surface) = drm.surfaces.get_mut(&crtc) else {
944- return;
945- };
946- if !surface.powered || surface.queued || surface.scheduled.is_some() {
947- return;
948- }
949- let now = now_ns();
950- if surface.vrr_on {
951- let since_flip = surface
952- .last_vblank_ns
953- .map_or(u64::MAX, |l| now.saturating_sub(l));
954- if surface.redraw || since_flip >= CURSOR_FRAME_NS {
955- draw(state, crtc, now);
956- return;
957- }
958- let wait = Duration::from_nanos(CURSOR_FRAME_NS - since_flip);
959- arm(&drm.handle, surface, crtc, wait, true, move |state| {
960- draw(state, crtc, now_ns());
961- });
962- return;
963- }
964- let Some((vblank, start)) = surface.next_deadline(now) else {
965- draw(state, crtc, now);
966- return;
967- };
968- surface.target_ns = Some(vblank);
969- let wait = Duration::from_nanos(start.saturating_sub(now));
970- arm(&drm.handle, surface, crtc, wait, false, move |state| {
971- let target = state
972- .backend
973- .drm()
974- .and_then(|d| d.surfaces.get(&crtc)?.target_ns);
975- if let Some(target) = target {
976- draw(state, crtc, target);
977- }
978- });
979-}
980-
981-/// Runs `fire` once after `after`, tracked in `surface.scheduled` until then.
982-fn arm(
983- handle: &LoopHandle<'static, State>,
984- surface: &mut Surface,
985- crtc: crtc::Handle,
986- after: Duration,
987- preemptible: bool,
988- mut fire: impl FnMut(&mut State) + 'static,
989-) {
990- let token = handle.insert_source(Timer::from_duration(after), move |_, _, state| {
991- if let Some(s) = state
992- .backend
993- .drm_mut()
994- .and_then(|d| d.surfaces.get_mut(&crtc))
995- {
996- s.scheduled = None;
997- }
998- fire(state);
999- TimeoutAction::Drop
1000- });
1001- match token {
1002- Ok(token) => {
1003- surface.scheduled = Some(token);
1004- surface.preemptible = preemptible;
1005- }
1006- Err(e) => tracing::warn!(
1007- output = surface.output.name(),
1008- "could not arm a render timer: {e}"
1009- ),
1010- }
1011-}
1012-
1013-/// Arms the flip watchdog unless it already runs.
1014-fn watch(handle: &LoopHandle<'static, State>, surface: &mut Surface, crtc: crtc::Handle) {
1015- if surface.watchdog.is_some() {
1016- return;
1017- }
1018- let after = Duration::from_nanos(surface.flip_timeout());
1019- let token = handle.insert_source(Timer::from_duration(after), move |_, _, state| {
1020- flip_watchdog(state, crtc)
1021- });
1022- match token {
1023- Ok(token) => surface.watchdog = Some(token),
1024- Err(e) => tracing::warn!(
1025- output = surface.output.name(),
1026- "could not arm the flip watchdog: {e}"
1027- ),
1028- }
1029-}
1030-
1031-/// Recovers an output whose page flip event never came, as after a
1032-/// `DrmEvent::Error`, so `queued` cannot hold it dark forever.
1033-fn flip_watchdog(state: &mut State, crtc: crtc::Handle) -> TimeoutAction {
1034- let Some(drm) = state.backend.drm_mut() else {
1035- return TimeoutAction::Drop;
1036- };
1037- let active = drm.session.is_active();
1038- let Some(surface) = drm.surfaces.get_mut(&crtc) else {
1039- return TimeoutAction::Drop;
1040- };
1041- let age = now_ns().saturating_sub(surface.queued_at);
1042- let limit = surface.flip_timeout();
1043- if active && surface.queued && age < limit {
1044- return TimeoutAction::ToDuration(Duration::from_nanos(limit - age));
1045- }
1046- surface.watchdog = None;
1047- if !active || !surface.queued {
1048- return TimeoutAction::Drop;
1049- }
1050- tracing::warn!(output = surface.output.name(), "page flip never landed");
1051- if let Err(e) = surface.drm.frame_submitted() {
1052- tracing::warn!("frame was not submitted: {e}");
1053- }
1054- surface.queued = false;
1055- surface.target_ns = None;
1056- surface.redraw = true;
1057- schedule(state, crtc);
1058- TimeoutAction::Drop
1059-}
1060-
1061-/// An animation whose frame had nothing to show, like one hidden behind a
1062-/// fullscreen window, has no flip to wait for. Under VRR scheduling again
1063-/// would draw at once and recurse until the stack overflowed, so the next
1064-/// try waits a refresh.
1065-fn draw(state: &mut State, crtc: crtc::Handle, target: u64) {
1066- if !render_surface(state, crtc, target) {
1067- return;
1068- }
1069- let Some(drm) = state.backend.drm_mut() else {
1070- return;
1071- };
1072- let Some(surface) = drm.surfaces.get_mut(&crtc) else {
1073- return;
1074- };
1075- if surface.scheduled.is_some() {
1076- return;
1077- }
1078- let backoff = surface.failures.saturating_sub(1).min(6);
1079- let wait = (surface.refresh_ns.max(1_000_000) << backoff).min(1_000_000_000);
1080- let wait = Duration::from_nanos(wait);
1081- let preemptible = surface.failures == 0;
1082- arm(
1083- &drm.handle,
1084- surface,
1085- crtc,
1086- wait,
1087- preemptible,
1088- move |state| {
1089- schedule(state, crtc);
1090- },
1091- );
1092-}
1093-
1094-/// Renders every output that has something new and no frame in flight.
1095-pub fn render(state: &mut State) {
1096- let Some(drm) = state.backend.drm_mut() else {
1097- return;
1098- };
1099- let cursor = std::mem::take(&mut state.dirty.cursor);
1100- let all = std::mem::take(&mut state.dirty.redraw);
1101- for s in drm.surfaces.values_mut() {
1102- s.redraw |= all;
1103- s.cursor_redraw |= cursor;
1104- if all
1105- && s.preemptible
1106- && let Some(token) = s.scheduled.take()
1107- {
1108- drm.handle.remove(token);
1109- }
1110- }
1111- let ready: Vec<crtc::Handle> = drm
1112- .surfaces
1113- .iter()
1114- .filter(|(_, s)| (s.redraw || s.cursor_redraw) && !s.queued)
1115- .map(|(c, _)| *c)
1116- .collect();
1117- for crtc in ready {
1118- schedule(state, crtc);
1119- }
1120-}
1121-
1122-/// Draws one output for the vblank at `target`. Returns whether it still
1123-/// animates without a frame in flight, and so needs another render.
1124-#[profiling::function]
1125-fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
1126- let State {
1127- backend: Backend::Drm(drm),
1128- renderer,
1129- wm,
1130- settings,
1131- core,
1132- input,
1133- services:
1134- Services {
1135- lock,
1136- captures,
1137- xwayland,
1138- ..
1139- },
1140- ..
1141- } = state
1142- else {
1143- return false;
1144- };
1145- if !drm.session.is_active() {
1146- return false;
1147- }
1148- let Some(surface) = drm.surfaces.get_mut(&crtc) else {
1149- return false;
1150- };
1151- if surface.queued || !surface.powered {
1152- return false;
1153- }
1154- surface.cursor_redraw = false;
1155- let shown = crate::frame::shown_windows(wm, surface.id, &surface.output, target);
1156- let fullscreen = shown.fullscreen;
1157- let forced = fullscreen.and_then(|w| wm.get(w)?.rule.vrr);
1158- let want_vrr = surface.vrr_capable
1159- && forced.unwrap_or(match settings.config.output.vrr {
1160- Vrr::Off => false,
1161- Vrr::On => true,
1162- Vrr::Fullscreen => fullscreen.is_some(),
1163- });
1164- if want_vrr != surface.vrr_on {
1165- match surface.drm.with_compositor(|c| c.use_vrr(want_vrr)) {
1166- Ok(()) => {
1167- surface.vrr_on = want_vrr;
1168- tracing::info!(
1169- output = surface.output.name(),
1170- vrr = want_vrr,
1171- "adaptive sync"
1172- );
1173- }
1174- Err(e) => {
1175- tracing::warn!("could not switch adaptive sync: {e}");
1176- surface.vrr_capable = false;
1177- }
1178- }
1179- }
1180- let began = std::time::Instant::now();
1181- let frame_cpu_began = thread_cpu_ns();
1182- let now = target;
1183- if wm.layout.output(surface.id).is_none() {
1184- return false;
1185- }
1186- let scene_began = std::time::Instant::now();
1187- let ctx = FrameCtx {
1188- renderer: &mut *renderer,
1189- wm: &mut *wm,
1190- input: &mut *input,
1191- lock: &*lock,
1192- deco: &settings.config.decoration,
1193- unmanaged: xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
1194- };
1195- let elements = crate::frame::compose(ctx, surface.id, &surface.output, now, true);
1196- let scene_ns = scene_began.elapsed().as_nanos() as u64;
1197- let animating = wm.anim.active(now);
1198- let render_began = std::time::Instant::now();
1199- let cpu_began = thread_cpu_ns();
1200- let mut queue_ns = 0;
1201- let mut failed = false;
1202- match surface
1203- .drm
1204- .render_frame(renderer, &elements, CLEAR, drm.frame_flags)
1205- {
1206- Ok(result) if !result.is_empty => {
1207- let direct = matches!(result.primary_element, PrimaryPlaneElement::Element(_));
1208- let feedback = crate::frame::take_presentation_feedback(
1209- wm,
1210- lock,
1211- xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
1212- &shown,
1213- &surface.output,
1214- &result.states,
1215- );
1216- if direct {
1217- surface.scanned_out += 1;
1218- }
1219- if direct != surface.scanout {
1220- surface.scanout = direct;
1221- tracing::info!(
1222- output = surface.output.name(),
1223- scanout = direct,
1224- elements = elements.len(),
1225- overlay_planes = result.overlay_elements.len(),
1226- cursor_plane = result.cursor_element.is_some(),
1227- "direct scanout"
1228- );
1229- }
1230- let queue_began = std::time::Instant::now();
1231- match surface.drm.queue_frame(feedback) {
1232- Ok(()) => {
1233- surface.queued = true;
1234- surface.queued_at = now_ns();
1235- watch(&drm.handle, surface, crtc);
1236- lock.shown(&surface.output);
1237- captures.output_damaged(&surface.output);
1238- }
1239- Err(e) => {
1240- failed = true;
1241- tracing::warn!("queueing a frame failed: {e}");
1242- }
1243- }
1244- queue_ns = queue_began.elapsed().as_nanos() as u64;
1245- }
1246- Ok(result) => {
1247- drop(crate::frame::take_presentation_feedback(
1248- wm,
1249- lock,
1250- xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
1251- &shown,
1252- &surface.output,
1253- &result.states,
1254- ));
1255- lock.shown(&surface.output);
1256- }
1257- Err(e) => {
1258- failed = true;
1259- tracing::warn!("rendering failed: {e}");
1260- }
1261- }
1262- let render_cpu_ns = thread_cpu_ns() - cpu_began;
1263- let render_ns = render_began.elapsed().as_nanos() as u64 - queue_ns;
1264- let took = began.elapsed().as_nanos() as u64;
1265- surface.budget.record(took);
1266- if surface.frame_cpu_ns.len() < MAX_CPU_SAMPLES {
1267- surface.frame_cpu_ns.push(thread_cpu_ns() - frame_cpu_began);
1268- }
1269- if took > surface.refresh_ns / 2 {
1270- tracing::debug!(
1271- output = surface.output.name(),
1272- took_ns = took,
1273- scene_ns,
1274- render_ns,
1275- render_cpu_ns,
1276- queue_ns,
1277- "slow frame"
1278- );
1279- }
1280- if !surface.queued {
1281- surface.target_ns = None;
1282- }
1283- surface.failures = if failed { surface.failures + 1 } else { 0 };
1284- surface.redraw = animating || failed;
1285- if !surface.queued {
1286- crate::fifo::release_idle(&core.handle, &surface.output);
1287- }
1288- let elapsed = core.start.elapsed();
1289- crate::frame::send_frame_callbacks(
1290- wm,
1291- lock,
1292- xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
1293- &shown,
1294- &surface.output,
1295- elapsed,
1296- input.dnd_icon.as_ref(),
1297- );
1298- crate::frame::send_cursor_frames(wm, input, surface.id, &surface.output, elapsed);
1299- (animating || failed) && !surface.queued
1300-}
1301-
1302-/// CPU time this thread has used, to tell a render that works from one that
1303-/// waits or is descheduled.
1304-fn thread_cpu_ns() -> u64 {
1305- let t = smithay::reexports::rustix::time::clock_gettime(
1306- smithay::reexports::rustix::time::ClockId::ThreadCPUTime,
1307- );
1308- t.tv_sec as u64 * 1_000_000_000 + t.tv_nsec as u64
1309-}
1310-
1311-fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {
1312- match event {
1313- InputEvent::Keyboard { event } => {
1314- let time = event.time();
1315- state.key(event.key_code(), event.state(), time);
1316- }
1317- InputEvent::PointerMotion { event } => {
1318- let (delta, unaccel) = (event.delta(), event.delta_unaccel());
1319- let pos = clamp(state, state.input.pointer_pos + delta);
1320- state.pointer_motion(pos, Motion::Relative { delta, unaccel }, event.time());
1321- state.pointer_frame();
1322- state.dirty.cursor = true;
1323- }
1324- InputEvent::PointerMotionAbsolute { event } => {
1325- // Tablets and absolute mice span the whole desktop, as in sway.
1326- let Some(area) = state
1327- .wm
1328- .layout
1329- .outputs()
1330- .map(|(_, o)| o.area)
1331- .reduce(|a, b| a.union(&b))
1332- else {
1333- return;
1334- };
1335- let p = event.position_transformed((area.w as i32, area.h as i32).into());
1336- let pos = clamp(state, Point::from((area.x + p.x, area.y + p.y)));
1337- state.pointer_motion(pos, Motion::Absolute, event.time());
1338- state.pointer_frame();
1339- state.dirty.cursor = true;
1340- }
1341- InputEvent::PointerButton { event } => {
1342- let time = event.time();
1343- state.pointer_button(event.button_code(), event.state(), time);
1344- state.pointer_frame();
1345- }
1346- InputEvent::PointerAxis { event } => {
1347- let factor = state
1348- .backend
1349- .drm()
1350- .map_or(1.0, |d| d.devices.scroll_factor(&event.device()));
1351- let source = event.source();
1352- let mut frame = AxisFrame::new(event.time()).source(source);
1353- for axis in [Axis::Horizontal, Axis::Vertical] {
1354- let v120 = event.amount_v120(axis).map(|v| v * factor);
1355- if let Some(v120) = v120 {
1356- frame = frame.v120(axis, v120 as i32);
1357- }
1358- let amount = event
1359- .amount(axis)
1360- .map(|a| a * factor)
1361- .or_else(|| v120.map(|v| v * 15.0 / 120.0));
1362- match amount {
1363- Some(a) if a != 0.0 => frame = frame.value(axis, a),
1364- Some(_) if source == AxisSource::Finger => frame = frame.stop(axis),
1365- _ => {}
1366- }
1367- }
1368- state.pointer_axis(frame);
1369- state.pointer_frame();
1370- }
1371- InputEvent::SwitchToggle { event } => {
1372- if event.switch() == Some(Switch::Lid) {
1373- state.lid_changed(event.switch_state() == SwitchState::On);
1374- }
1375- }
1376- InputEvent::DeviceAdded { device } => {
1377- if let Some(drm) = state.backend.drm_mut() {
1378- drm.devices.add(device, &state.settings.config.input);
1379- }
1380- }
1381- InputEvent::DeviceRemoved { device } => {
1382- if let Some(drm) = state.backend.drm_mut() {
1383- drm.devices.remove(&device);
1384- }
1385- }
1386- _ => {}
1387- }
1388-}
1389-
1390-/// Keeps the pointer on some output, sliding along the edge it hit.
1391-fn clamp(
1392- state: &State,
1393- pos: Point<f64, smithay::utils::Logical>,
1394-) -> Point<f64, smithay::utils::Logical> {
1395- let inside = |a: &Rect, p: Point<f64, smithay::utils::Logical>| a.contains(p.x, p.y);
1396- let areas: Vec<Rect> = state.wm.layout.outputs().map(|(_, o)| o.area).collect();
1397- if areas.iter().any(|a| inside(a, pos)) {
1398- return pos;
1399- }
1400- let Some(a) = areas
1401- .iter()
1402- .find(|a| inside(a, state.input.pointer_pos))
1403- .or(areas.first())
1404- else {
1405- return pos;
1406- };
1407- Point::from((
1408- pos.x.clamp(a.x, a.right() - 1.0),
1409- pos.y.clamp(a.y, a.bottom() - 1.0),
1410- ))
1411-}
Added file: crates/monolith/src/drm/input.rs+115 additions
@@ -0,0 +1,115 @@
1+//! libinput events turned into compositor input.
2+
3+use crate::input::Motion;
4+use crate::state::State;
5+use monolith_layout::Rect;
6+use smithay::backend::input::{
7+ AbsolutePositionEvent, Axis, AxisSource, Event, InputEvent, KeyboardKeyEvent, PointerAxisEvent,
8+ PointerButtonEvent, PointerMotionEvent,
9+};
10+use smithay::backend::libinput::LibinputInputBackend;
11+use smithay::input::pointer::AxisFrame;
12+use smithay::reexports::input::event::switch::{Switch, SwitchState};
13+use smithay::utils::Point;
14+
15+pub(super) fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {
16+ match event {
17+ InputEvent::Keyboard { event } => {
18+ let time = event.time();
19+ state.key(event.key_code(), event.state(), time);
20+ }
21+ InputEvent::PointerMotion { event } => {
22+ let (delta, unaccel) = (event.delta(), event.delta_unaccel());
23+ let pos = clamp(state, state.input.pointer_pos + delta);
24+ state.pointer_motion(pos, Motion::Relative { delta, unaccel }, event.time());
25+ state.pointer_frame();
26+ state.dirty.cursor = true;
27+ }
28+ InputEvent::PointerMotionAbsolute { event } => {
29+ // Tablets and absolute mice span the whole desktop, as in sway.
30+ let Some(area) = state
31+ .wm
32+ .layout
33+ .outputs()
34+ .map(|(_, o)| o.area)
35+ .reduce(|a, b| a.union(&b))
36+ else {
37+ return;
38+ };
39+ let p = event.position_transformed((area.w as i32, area.h as i32).into());
40+ let pos = clamp(state, Point::from((area.x + p.x, area.y + p.y)));
41+ state.pointer_motion(pos, Motion::Absolute, event.time());
42+ state.pointer_frame();
43+ state.dirty.cursor = true;
44+ }
45+ InputEvent::PointerButton { event } => {
46+ let time = event.time();
47+ state.pointer_button(event.button_code(), event.state(), time);
48+ state.pointer_frame();
49+ }
50+ InputEvent::PointerAxis { event } => {
51+ let factor = state
52+ .backend
53+ .drm()
54+ .map_or(1.0, |d| d.devices.scroll_factor(&event.device()));
55+ let source = event.source();
56+ let mut frame = AxisFrame::new(event.time()).source(source);
57+ for axis in [Axis::Horizontal, Axis::Vertical] {
58+ let v120 = event.amount_v120(axis).map(|v| v * factor);
59+ if let Some(v120) = v120 {
60+ frame = frame.v120(axis, v120 as i32);
61+ }
62+ let amount = event
63+ .amount(axis)
64+ .map(|a| a * factor)
65+ .or_else(|| v120.map(|v| v * 15.0 / 120.0));
66+ match amount {
67+ Some(a) if a != 0.0 => frame = frame.value(axis, a),
68+ Some(_) if source == AxisSource::Finger => frame = frame.stop(axis),
69+ _ => {}
70+ }
71+ }
72+ state.pointer_axis(frame);
73+ state.pointer_frame();
74+ }
75+ InputEvent::SwitchToggle { event } => {
76+ if event.switch() == Some(Switch::Lid) {
77+ state.lid_changed(event.switch_state() == SwitchState::On);
78+ }
79+ }
80+ InputEvent::DeviceAdded { device } => {
81+ if let Some(drm) = state.backend.drm_mut() {
82+ drm.devices.add(device, &state.settings.config.input);
83+ }
84+ }
85+ InputEvent::DeviceRemoved { device } => {
86+ if let Some(drm) = state.backend.drm_mut() {
87+ drm.devices.remove(&device);
88+ }
89+ }
90+ _ => {}
91+ }
92+}
93+
94+/// Keeps the pointer on some output, sliding along the edge it hit.
95+pub(super) fn clamp(
96+ state: &State,
97+ pos: Point<f64, smithay::utils::Logical>,
98+) -> Point<f64, smithay::utils::Logical> {
99+ let inside = |a: &Rect, p: Point<f64, smithay::utils::Logical>| a.contains(p.x, p.y);
100+ let areas: Vec<Rect> = state.wm.layout.outputs().map(|(_, o)| o.area).collect();
101+ if areas.iter().any(|a| inside(a, pos)) {
102+ return pos;
103+ }
104+ let Some(a) = areas
105+ .iter()
106+ .find(|a| inside(a, state.input.pointer_pos))
107+ .or(areas.first())
108+ else {
109+ return pos;
110+ };
111+ Point::from((
112+ pos.x.clamp(a.x, a.right() - 1.0),
113+ pos.y.clamp(a.y, a.bottom() - 1.0),
114+ ))
115+}
Added file: crates/monolith/src/drm/mod.rs+172 additions
@@ -0,0 +1,172 @@
1+//! Bare metal. A libseat session, udev for the primary GPU and its
2+//! connectors, libinput for input, and one smithay DRM output per connector,
3+//! each rendered when its own vblank lands.
4+
5+mod input;
6+mod outputs;
7+mod pacing;
8+mod render;
9+mod session;
10+
11+pub use outputs::{reconfigure_outputs, set_parked};
12+pub use render::render;
13+pub use session::{init, primary};
14+
15+use crate::devices::Devices;
16+use crate::state::State;
17+use monolith_layout::OutputId;
18+use smithay::backend::allocator::gbm::GbmAllocator;
19+use smithay::backend::drm::compositor::FrameFlags;
20+use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
21+use smithay::backend::drm::output::{DrmOutput, DrmOutputManager};
22+use smithay::backend::drm::{DrmDeviceFd, DrmNode};
23+use smithay::backend::session::Session;
24+use smithay::backend::session::libseat::LibSeatSession;
25+use smithay::desktop::utils::OutputPresentationFeedback;
26+use smithay::output::Output;
27+use smithay::reexports::calloop::{LoopHandle, RegistrationToken};
28+use smithay::reexports::drm::control::{connector, crtc};
29+use smithay::reexports::input::Libinput;
30+use smithay::reexports::wayland_server::backend::GlobalId;
31+use smithay::wayland::drm_syncobj::DrmSyncobjState;
32+use smithay_drm_extras::drm_scanner::DrmScanner;
33+use std::collections::{HashMap, HashSet};
34+
35+type Allocator = GbmAllocator<DrmDeviceFd>;
36+type Exporter = GbmFramebufferExporter<DrmDeviceFd>;
37+type Manager = DrmOutputManager<Allocator, Exporter, OutputPresentationFeedback, DrmDeviceFd>;
38+type Out = DrmOutput<Allocator, Exporter, OutputPresentationFeedback, DrmDeviceFd>;
39+
40+pub struct Drm {
41+ pub session: LibSeatSession,
42+ libinput: Libinput,
43+ handle: LoopHandle<'static, State>,
44+ node: DrmNode,
45+ pub render_node: DrmNode,
46+ pub syncobj: Option<DrmSyncobjState>,
47+ manager: Manager,
48+ scanner: DrmScanner,
49+ nvidia: bool,
50+ frame_flags: FrameFlags,
51+ surfaces: HashMap<crtc::Handle, Surface>,
52+ /// Connected connectors the scanner has already reported but that are
53+ /// not lit, retried on the next scan.
54+ failed: HashSet<connector::Handle>,
55+ pub devices: Devices,
56+}
57+
58+struct Surface {
59+ output: Output,
60+ global: GlobalId,
61+ id: OutputId,
62+ drm: Out,
63+ /// A frame is on its way to the screen and its vblank has not landed.
64+ queued: bool,
65+ queued_at: u64,
66+ /// The panel is lit. Off outputs are not rendered to.
67+ powered: bool,
68+ /// Out of the layout and Space, but still driven so it can relight.
69+ parked: bool,
70+ /// Fires when a queued frame's vblank never arrives.
71+ watchdog: Option<RegistrationToken>,
72+ /// Consecutive frames that failed to render or queue.
73+ failures: u32,
74+ redraw: bool,
75+ last_vblank_ns: Option<u64>,
76+ refresh_ns: u64,
77+ /// The pending render, timed to start just in time for `target_ns`.
78+ scheduled: Option<RegistrationToken>,
79+ /// `scheduled` only waits out a cap or a retry, so fresh content may
80+ /// cancel it and draw at once.
81+ preemptible: bool,
82+ /// The vblank the frame being drawn or queued is meant for.
83+ target_ns: Option<u64>,
84+ budget: crate::pacing::Budget,
85+ /// Frames presented since `stats_since`, for the once-a-second debug line.
86+ presented: u32,
87+ stats_since: u64,
88+ vrr_capable: bool,
89+ vrr_on: bool,
90+ /// Only the cursor moved, which under VRR is drawn at a capped rate.
91+ cursor_redraw: bool,
92+ /// The last frame went straight to the primary plane without compositing.
93+ scanout: bool,
94+ /// Frames scanned out and per-frame compositor CPU times since
95+ /// `stats_since`, for the targets in decisions.md.
96+ scanned_out: u32,
97+ frame_cpu_ns: Vec<u64>,
98+}
99+
100+fn surface_mut(state: &mut State, crtc: crtc::Handle) -> Option<&mut Surface> {
101+ state.backend.drm_mut()?.surfaces.get_mut(&crtc)
102+}
103+
104+impl Surface {
105+ /// The first vblank a render starting at `now` can still make, and the
106+ /// latest moment to start it.
107+ fn next_deadline(&self, now: u64) -> Option<(u64, u64)> {
108+ crate::pacing::deadline(self.last_vblank_ns?, self.refresh_ns, self.budget.ns(), now)
109+ }
110+
111+ /// How long a queued frame may go without its vblank. Under VRR a flip
112+ /// waits for the panel's longest frame, so the floor stays well above it.
113+ fn flip_timeout(&self) -> u64 {
114+ (self.refresh_ns * 4).max(100_000_000)
115+ }
116+}
117+
118+impl Drm {
119+ pub fn names(&self) -> Vec<String> {
120+ self.surfaces.values().map(|s| s.output.name()).collect()
121+ }
122+
123+ /// Lights or darkens the output called `name`. Dark outputs get DPMS off
124+ /// and no frames, and the next frame after turning on is a full modeset.
125+ pub fn set_powered(&mut self, name: &str, on: bool) {
126+ let active = self.session.is_active();
127+ for s in self
128+ .surfaces
129+ .values_mut()
130+ .filter(|s| s.output.name() == name)
131+ {
132+ if s.powered == on {
133+ continue;
134+ }
135+ s.powered = on;
136+ tracing::info!(name, on, "output power");
137+ if on {
138+ s.redraw = true;
139+ s.last_vblank_ns = None;
140+ continue;
141+ }
142+ for token in [s.scheduled.take(), s.watchdog.take()]
143+ .into_iter()
144+ .flatten()
145+ {
146+ self.handle.remove(token);
147+ }
148+ s.queued = false;
149+ s.target_ns = None;
150+ s.cursor_redraw = false;
151+ if active && let Err(e) = s.drm.with_compositor(|c| c.clear()) {
152+ tracing::warn!(name, "could not switch the output off: {e}");
153+ }
154+ }
155+ }
156+
157+ pub fn output(&self, id: OutputId) -> Option<Output> {
158+ self.surfaces
159+ .values()
160+ .find(|s| s.id == id)
161+ .map(|s| s.output.clone())
162+ }
163+
164+ pub fn timeline(&self, output: &Output) -> Option<crate::timing::Timeline> {
165+ let s = self.surfaces.values().find(|s| s.output == *output)?;
166+ Some(crate::timing::Timeline {
167+ last: s.last_vblank_ns?,
168+ refresh: if s.vrr_on { 0 } else { s.refresh_ns },
169+ budget: s.budget.ns(),
170+ })
171+ }
172+}
Added file: crates/monolith/src/drm/outputs.rs+514 additions
@@ -0,0 +1,514 @@
1+//! Connectors turning into outputs, and back.
2+
3+use super::Surface;
4+use super::input::clamp;
5+use crate::backend::{Backend, logical_area};
6+use crate::scene::Draw;
7+use crate::state::State;
8+use crate::wm::Wm;
9+use monolith_config::OutputTransform;
10+use smithay::backend::drm::VrrSupport;
11+use smithay::backend::drm::output::DrmOutputRenderElements;
12+use smithay::backend::input::InputTime;
13+use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
14+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
15+use smithay::reexports::drm::control::{self, ModeTypeFlags, connector, crtc};
16+use smithay::utils::{Logical, Point, Transform};
17+use smithay_drm_extras::drm_scanner::DrmScanEvent;
18+use std::time::Duration;
19+
20+/// How long a disabled wl_output stays bound before it is removed, so
21+/// clients that were mid-bind are not killed by the removal.
22+const GLOBAL_REMOVAL_DELAY: Duration = Duration::from_secs(5);
23+
24+/// Brings outputs in line with what is plugged in.
25+pub(super) fn scan(state: &mut State) {
26+ let Some(drm) = state.backend.drm_mut() else {
27+ return;
28+ };
29+ let events: Vec<DrmScanEvent> = match drm.scanner.scan_connectors(drm.manager.device()) {
30+ Ok(result) => result.into_iter().collect(),
31+ Err(e) => {
32+ tracing::warn!("scanning connectors failed: {e}");
33+ return;
34+ }
35+ };
36+ let retry = std::mem::take(&mut drm.failed);
37+ for event in events {
38+ match event {
39+ DrmScanEvent::Connected {
40+ connector,
41+ crtc: Some(crtc),
42+ } => connect(state, connector, crtc),
43+ DrmScanEvent::Connected {
44+ connector,
45+ crtc: None,
46+ } => {
47+ tracing::warn!(handle = ?connector.handle(), "no free crtc for the connector");
48+ if let Some(drm) = state.backend.drm_mut() {
49+ drm.failed.insert(connector.handle());
50+ }
51+ }
52+ DrmScanEvent::Disconnected {
53+ crtc: Some(crtc), ..
54+ } => disconnect(state, crtc),
55+ DrmScanEvent::Changed {
56+ connector,
57+ crtc: Some(crtc),
58+ } => changed(state, connector, crtc),
59+ _ => {}
60+ }
61+ }
62+ for handle in retry {
63+ retry_connector(state, handle);
64+ }
65+ state.apply_power();
66+ state.lid_act();
67+ state.apply_layout();
68+}
69+
70+/// Lights a connector that failed on an earlier scan, which the scanner
71+/// will not report again until it changes.
72+pub(super) fn retry_connector(state: &mut State, handle: connector::Handle) {
73+ let Some(drm) = state.backend.drm_mut() else {
74+ return;
75+ };
76+ if drm.failed.contains(&handle) {
77+ return;
78+ }
79+ let Some(info) = drm.scanner.connectors().get(&handle) else {
80+ return;
81+ };
82+ if info.state() != connector::State::Connected {
83+ return;
84+ }
85+ let info = info.clone();
86+ let Some(crtc) = drm.scanner.crtc_for_connector(&handle) else {
87+ drm.failed.insert(handle);
88+ return;
89+ };
90+ if !drm.surfaces.contains_key(&crtc) {
91+ connect(state, info, crtc);
92+ }
93+}
94+
95+pub(super) fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
96+ let name = connector_name(&connector);
97+ let config = state
98+ .settings
99+ .config
100+ .output
101+ .named
102+ .get(&name)
103+ .cloned()
104+ .unwrap_or_default();
105+ let Some(mode) = choose_mode(&connector, &name, config.mode.as_deref()) else {
106+ tracing::warn!(name, "connector has no modes");
107+ if let Some(drm) = state.backend.drm_mut() {
108+ drm.failed.insert(connector.handle());
109+ }
110+ return;
111+ };
112+ let transform = transform(config.transform);
113+ let State {
114+ backend: Backend::Drm(drm),
115+ renderer,
116+ wm: Wm { layout, space, .. },
117+ core,
118+ ..
119+ } = state
120+ else {
121+ return;
122+ };
123+ let (mm_w, mm_h) = connector.size().unwrap_or((0, 0));
124+ let output = Output::new(
125+ name.clone(),
126+ PhysicalProperties {
127+ size: (mm_w as i32, mm_h as i32).into(),
128+ subpixel: Subpixel::Unknown,
129+ make: "Unknown".into(),
130+ model: name.clone(),
131+ serial_number: String::new(),
132+ },
133+ );
134+ let wl_mode = Mode::from(mode);
135+ let right = layout
136+ .outputs()
137+ .map(|(_, o)| o.area.right())
138+ .fold(0.0, f64::max);
139+ let position = match config.position {
140+ Some([x, y]) => Point::from((x, y)),
141+ None => Point::from((right.ceil() as i32, 0)),
142+ };
143+ let scale = config.scale();
144+ output.change_current_state(
145+ Some(wl_mode),
146+ Some(transform),
147+ Some(Scale::Fractional(scale)),
148+ Some(position),
149+ );
150+ output.set_preferred(wl_mode);
151+ let mut planes = drm.manager.device().planes(&crtc).ok();
152+ if drm.nvidia
153+ && let Some(p) = planes.as_mut()
154+ {
155+ // NVIDIA's overlay planes break composition, as anvil also found.
156+ p.overlay.clear();
157+ }
158+ let out = match drm.manager.lock().initialize_output::<_, Draw>(
159+ crtc,
160+ mode,
161+ &[connector.handle()],
162+ &output,
163+ planes,
164+ renderer,
165+ &DrmOutputRenderElements::default(),
166+ ) {
167+ Ok(out) => out,
168+ Err(e) => {
169+ tracing::warn!(name, "could not light up the output: {e}");
170+ drm.failed.insert(connector.handle());
171+ return;
172+ }
173+ };
174+ let vrr_capable = matches!(
175+ out.with_compositor(|c| c.vrr_supported(connector.handle())),
176+ Ok(VrrSupport::Supported)
177+ );
178+ let global = output.create_global::<State>(&core.display);
179+ space.map_output(&output, position);
180+ let (w, h) = mode.size();
181+ let area = logical_area(position, wl_mode.size, transform, scale);
182+ let id = layout.add_output(&name, area);
183+ tracing::info!(
184+ name,
185+ w,
186+ h,
187+ scale,
188+ refresh = mode.vrefresh(),
189+ ?transform,
190+ x = position.x,
191+ y = position.y,
192+ vrr_capable,
193+ "output connected"
194+ );
195+ drm.surfaces.insert(
196+ crtc,
197+ Surface {
198+ output,
199+ global,
200+ id,
201+ drm: out,
202+ queued: false,
203+ queued_at: 0,
204+ powered: true,
205+ parked: false,
206+ watchdog: None,
207+ failures: 0,
208+ redraw: true,
209+ last_vblank_ns: None,
210+ scheduled: None,
211+ preemptible: false,
212+ target_ns: None,
213+ budget: crate::pacing::Budget::new(),
214+ presented: 0,
215+ stats_since: 0,
216+ vrr_capable,
217+ vrr_on: false,
218+ cursor_redraw: false,
219+ scanout: false,
220+ scanned_out: 0,
221+ frame_cpu_ns: Vec::new(),
222+ refresh_ns: 1_000_000_000_000 / u64::from(wl_mode.refresh.max(1) as u32),
223+ },
224+ );
225+}
226+
227+pub(super) fn connector_name(connector: &connector::Info) -> String {
228+ format!(
229+ "{}-{}",
230+ connector.interface().as_str(),
231+ connector.interface_id()
232+ )
233+}
234+
235+/// The configured mode if the connector has it, else its preferred one.
236+pub(super) fn choose_mode(
237+ connector: &connector::Info,
238+ name: &str,
239+ configured: Option<&str>,
240+) -> Option<control::Mode> {
241+ let chosen = configured.and_then(|spec| {
242+ let found = pick_mode(connector.modes(), spec);
243+ if found.is_none() {
244+ tracing::warn!(name, spec, "no such mode, using the preferred one");
245+ }
246+ found
247+ });
248+ chosen.or_else(|| {
249+ connector
250+ .modes()
251+ .iter()
252+ .find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
253+ .or_else(|| connector.modes().first())
254+ .copied()
255+ })
256+}
257+
258+pub(super) fn changed(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
259+ if !remode(state, &connector, crtc) {
260+ disconnect(state, crtc);
261+ connect(state, connector, crtc);
262+ }
263+}
264+
265+/// Moves a lit output to the mode its connector now calls for without
266+/// recreating the wl_output, so clients, windows, the lock surface and the
267+/// power state stay as they are. False means the output has to be rebuilt.
268+pub(super) fn remode(state: &mut State, connector: &connector::Info, crtc: crtc::Handle) -> bool {
269+ let name = connector_name(connector);
270+ let State {
271+ backend: Backend::Drm(drm),
272+ renderer,
273+ wm: Wm { layout, .. },
274+ settings,
275+ ..
276+ } = state
277+ else {
278+ return false;
279+ };
280+ let Some(s) = drm.surfaces.get_mut(&crtc) else {
281+ return false;
282+ };
283+ let (mm_w, mm_h) = connector.size().unwrap_or((0, 0));
284+ if s.output.physical_properties().size != (mm_w as i32, mm_h as i32).into() {
285+ return false;
286+ }
287+ let configured = settings.config.output.named.get(&name);
288+ let Some(mode) = choose_mode(connector, &name, configured.and_then(|c| c.mode.as_deref()))
289+ else {
290+ return false;
291+ };
292+ let wl_mode = Mode::from(mode);
293+ let moved = s.output.current_mode() != Some(wl_mode);
294+ if moved {
295+ let elements = DrmOutputRenderElements::default();
296+ if let Err(e) = s.drm.use_mode::<_, Draw>(mode, renderer, &elements) {
297+ tracing::warn!(name, "could not change the mode in place: {e}");
298+ return false;
299+ }
300+ s.output
301+ .change_current_state(Some(wl_mode), None, None, None);
302+ s.refresh_ns = 1_000_000_000_000 / u64::from(wl_mode.refresh.max(1) as u32);
303+ s.last_vblank_ns = None;
304+ s.target_ns = None;
305+ s.redraw = true;
306+ let area = logical_area(
307+ s.output.current_location(),
308+ wl_mode.size,
309+ s.output.current_transform(),
310+ s.output.current_scale().fractional_scale(),
311+ );
312+ layout.set_output_area(s.id, area);
313+ tracing::info!(
314+ name,
315+ w = wl_mode.size.w,
316+ h = wl_mode.size.h,
317+ refresh = mode.vrefresh(),
318+ "output mode changed"
319+ );
320+ }
321+ s.output.set_preferred(wl_mode);
322+ if moved {
323+ reconfigure_outputs(state);
324+ state.reserve_layers();
325+ state.reconfigure_lock();
326+ }
327+ true
328+}
329+
330+/// The mode matching `WIDTHxHEIGHT` or `WIDTHxHEIGHT@HZ`, the highest rate
331+/// when none is given and the nearest one when it is.
332+pub(super) fn pick_mode(modes: &[control::Mode], spec: &str) -> Option<control::Mode> {
333+ let (size, rate) = match spec.split_once('@') {
334+ Some((size, rate)) => (size, Some(rate.trim().parse::<f64>().ok()?)),
335+ None => (spec, None),
336+ };
337+ let (w, h) = size.trim().split_once('x')?;
338+ let size = (w.trim().parse::<u16>().ok()?, h.trim().parse::<u16>().ok()?);
339+ let fitting = modes.iter().filter(|m| m.size() == size);
340+ match rate {
341+ Some(r) => fitting
342+ .min_by(|a, b| {
343+ let off = |m: &&control::Mode| (f64::from(m.vrefresh()) - r).abs();
344+ off(a).total_cmp(&off(b))
345+ })
346+ .copied(),
347+ None => fitting.max_by_key(|m| m.vrefresh()).copied(),
348+ }
349+}
350+
351+pub(super) fn transform(t: OutputTransform) -> Transform {
352+ match t {
353+ OutputTransform::Normal => Transform::Normal,
354+ OutputTransform::Rotate90 => Transform::_90,
355+ OutputTransform::Rotate180 => Transform::_180,
356+ OutputTransform::Rotate270 => Transform::_270,
357+ OutputTransform::Flipped => Transform::Flipped,
358+ OutputTransform::Flipped90 => Transform::Flipped90,
359+ OutputTransform::Flipped180 => Transform::Flipped180,
360+ OutputTransform::Flipped270 => Transform::Flipped270,
361+ }
362+}
363+
364+/// Outputs without a configured position are laid out left to right in
365+/// their current order, so one rotating pushes its neighbours along.
366+pub fn reconfigure_outputs(state: &mut State) {
367+ let State {
368+ backend: Backend::Drm(drm),
369+ wm: Wm { layout, space, .. },
370+ settings,
371+ ..
372+ } = state
373+ else {
374+ return;
375+ };
376+ let mut surfaces: Vec<&mut Surface> = drm.surfaces.values_mut().filter(|s| !s.parked).collect();
377+ surfaces.sort_by_cached_key(|s| {
378+ let name = s.output.name();
379+ (
380+ s.output.current_location().x,
381+ !crate::power::internal(&name),
382+ )
383+ });
384+ let mut right = 0;
385+ let mut changed = false;
386+ for s in surfaces {
387+ let config = settings
388+ .config
389+ .output
390+ .named
391+ .get(&s.output.name())
392+ .cloned()
393+ .unwrap_or_default();
394+ let Some(mode) = s.output.current_mode() else {
395+ continue;
396+ };
397+ let transform = transform(config.transform);
398+ let scale = config.scale();
399+ let position: Point<i32, Logical> = match config.position {
400+ Some([x, y]) => (x, y).into(),
401+ None => (right, 0).into(),
402+ };
403+ let area = logical_area(position, mode.size, transform, scale);
404+ right = right.max(area.right().ceil() as i32);
405+ if s.output.current_transform() == transform
406+ && s.output.current_location() == position
407+ && s.output.current_scale().fractional_scale() == scale
408+ {
409+ continue;
410+ }
411+ s.output.change_current_state(
412+ None,
413+ Some(transform),
414+ Some(Scale::Fractional(scale)),
415+ Some(position),
416+ );
417+ space.map_output(&s.output, position);
418+ layout.set_output_area(s.id, area);
419+ s.redraw = true;
420+ changed = true;
421+ tracing::info!(
422+ name = s.output.name(),
423+ ?transform,
424+ scale,
425+ x = position.x,
426+ y = position.y,
427+ "output reconfigured"
428+ );
429+ }
430+ if changed {
431+ state.reserve_layers();
432+ state.reconfigure_lock();
433+ state.apply_layout();
434+ }
435+}
436+
437+pub(super) fn disconnect(state: &mut State, crtc: crtc::Handle) {
438+ let Some(drm) = state.backend.drm_mut() else {
439+ return;
440+ };
441+ let Some(surface) = drm.surfaces.remove(&crtc) else {
442+ return;
443+ };
444+ for token in [surface.scheduled, surface.watchdog].into_iter().flatten() {
445+ drm.handle.remove(token);
446+ }
447+ tracing::info!(name = surface.output.name(), "output disconnected");
448+ state
449+ .core
450+ .display
451+ .disable_global::<State>(surface.global.clone());
452+ let global = surface.global.clone();
453+ let removal = Timer::from_duration(GLOBAL_REMOVAL_DELAY);
454+ let armed = drm.handle.insert_source(removal, move |_, _, state| {
455+ state.core.display.remove_global::<State>(global.clone());
456+ TimeoutAction::Drop
457+ });
458+ if let Err(e) = armed {
459+ tracing::warn!("could not defer removing the wl_output: {e}");
460+ state
461+ .core
462+ .display
463+ .remove_global::<State>(surface.global.clone());
464+ }
465+ state.wm.space.unmap_output(&surface.output);
466+ state.wm.layout.remove_output(surface.id);
467+ state.close_layers(&surface.output);
468+ state.services.lock.forget(&surface.output);
469+ state.services.power.forget(&surface.output.name());
470+ state.protocols.output_power.output_removed(&surface.output);
471+}
472+
473+/// Takes the output called `name` out of the layout and Space, or puts it
474+/// back. Its windows move to the other outputs and return when it does.
475+pub fn set_parked(state: &mut State, name: &str, parked: bool) {
476+ let State {
477+ backend: Backend::Drm(drm),
478+ wm: Wm { layout, space, .. },
479+ ..
480+ } = state
481+ else {
482+ return;
483+ };
484+ let Some(s) = drm.surfaces.values_mut().find(|s| s.output.name() == name) else {
485+ return;
486+ };
487+ if s.parked == parked {
488+ return;
489+ }
490+ s.parked = parked;
491+ tracing::info!(name, parked, "output parked");
492+ if parked {
493+ space.unmap_output(&s.output);
494+ layout.remove_output(s.id);
495+ } else if let Some(mode) = s.output.current_mode() {
496+ let position = s.output.current_location();
497+ space.map_output(&s.output, position);
498+ let scale = s.output.current_scale().fractional_scale();
499+ let area = logical_area(position, mode.size, s.output.current_transform(), scale);
500+ s.id = layout.add_output(name, area);
501+ s.redraw = true;
502+ }
503+ let pos = state.input.pointer_pos;
504+ let held = clamp(state, pos);
505+ if held != pos {
506+ state.warp_pointer(held, InputTime::now());
507+ }
508+ if !parked {
509+ reconfigure_outputs(state);
510+ }
511+ state.reserve_layers();
512+ state.reconfigure_lock();
513+ state.apply_layout();
514+}
Added file: crates/monolith/src/drm/pacing.rs+273 additions
@@ -0,0 +1,273 @@
1+//! When each output renders next, and what happens when its flip lands or
2+//! never does.
3+
4+use crate::state::{State, now_ns};
5+use smithay::backend::drm::{
6+ DrmEventMetadata, DrmEventTime,
7+};
8+use smithay::backend::session::Session;
9+use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
10+use smithay::reexports::calloop::LoopHandle;
11+use smithay::reexports::drm::control::crtc;
12+use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind;
13+use smithay::utils::{Monotonic, Time};
14+use smithay::wayland::presentation::Refresh;
15+use std::time::Duration;
16+use super::{Surface, surface_mut};
17+use super::render::render_surface;
18+
19+/// Frames that only move the cursor stay at or below 60 Hz under VRR, so the
20+/// mouse does not override the refresh rate a game is pacing.
21+const CURSOR_FRAME_NS: u64 = 16_666_667;
22+
23+pub(super) fn on_vblank(
24+ state: &mut State,
25+ crtc: crtc::Handle,
26+ meta: &mut Option<DrmEventMetadata>,
27+) {
28+ let presented = state
29+ .backend
30+ .drm()
31+ .and_then(|d| d.surfaces.get(&crtc))
32+ .map(|s| s.output.clone());
33+ if let Some(output) = presented {
34+ crate::fifo::presented(state, &output);
35+ }
36+ let Some(surface) = surface_mut(state, crtc) else {
37+ return;
38+ };
39+ let feedback = surface.drm.frame_submitted().unwrap_or_else(|e| {
40+ tracing::warn!("frame was not submitted: {e}");
41+ None
42+ });
43+ surface.queued = false;
44+ if let Err(e) = state.renderer.absorb_submit_stall() {
45+ tracing::warn!("absorbing a submit stall failed: {e}");
46+ }
47+ let Some(surface) = surface_mut(state, crtc) else {
48+ return;
49+ };
50+ if let Some(mut feedback) = feedback {
51+ let (time, clock) = match meta.as_ref().map(|m| m.time) {
52+ Some(DrmEventTime::Monotonic(t)) => (t, Kind::HwClock | Kind::HwCompletion),
53+ _ => (Duration::from_nanos(now_ns()), Kind::empty()),
54+ };
55+ let refresh = Duration::from_nanos(surface.refresh_ns);
56+ let refresh = if surface.vrr_on {
57+ Refresh::variable(refresh)
58+ } else {
59+ Refresh::fixed(refresh)
60+ };
61+ let sequence = meta.as_ref().map_or(0, |m| u64::from(m.sequence));
62+ feedback.presented(
63+ Time::<Monotonic>::from(time),
64+ refresh,
65+ sequence,
66+ Kind::Vsync | clock,
67+ );
68+ }
69+ if let Some(DrmEventTime::Monotonic(t)) = meta.as_ref().map(|m| m.time) {
70+ let t = t.as_nanos() as u64;
71+ if let Some(target) = surface.target_ns.take() {
72+ if t > target + surface.refresh_ns / 2 {
73+ surface.budget.missed(surface.refresh_ns);
74+ tracing::debug!(
75+ output = surface.output.name(),
76+ late_ns = t - target,
77+ budget_ns = surface.budget.ns(),
78+ "frame missed its vblank"
79+ );
80+ } else {
81+ surface.budget.landed();
82+ }
83+ }
84+ surface.last_vblank_ns = Some(t);
85+ surface.presented += 1;
86+ let window = t.saturating_sub(surface.stats_since);
87+ if window >= 1_000_000_000 {
88+ // Under VRR the panel waits for each flip, so the nominal refresh
89+ // says nothing about how many vblanks there were.
90+ let vblanks = (!surface.vrr_on).then(|| window / surface.refresh_ns.max(1));
91+ let cpu = &mut surface.frame_cpu_ns;
92+ cpu.sort_unstable();
93+ let p99 = cpu.get(cpu.len().saturating_sub(1) * 99 / 100).copied();
94+ tracing::debug!(
95+ output = surface.output.name(),
96+ presented = surface.presented,
97+ vblanks,
98+ vrr = surface.vrr_on,
99+ scanned_out = surface.scanned_out,
100+ cpu_p99_us = p99.map(|ns| ns / 1000),
101+ budget_us = surface.budget.ns() / 1000,
102+ "frame rate"
103+ );
104+ cpu.clear();
105+ surface.scanned_out = 0;
106+ surface.presented = 0;
107+ surface.stats_since = t;
108+ }
109+ }
110+ if surface.redraw || surface.cursor_redraw {
111+ schedule(state, crtc);
112+ }
113+}
114+
115+/// Starts a render at the last moment that still makes the next reachable
116+/// vblank, so commits arriving until then make it into the frame.
117+pub(super) fn schedule(state: &mut State, crtc: crtc::Handle) {
118+ let Some(drm) = state.backend.drm_mut() else {
119+ return;
120+ };
121+ if !drm.session.is_active() {
122+ return;
123+ }
124+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
125+ return;
126+ };
127+ if !surface.powered || surface.queued || surface.scheduled.is_some() {
128+ return;
129+ }
130+ let now = now_ns();
131+ if surface.vrr_on {
132+ let since_flip = surface
133+ .last_vblank_ns
134+ .map_or(u64::MAX, |l| now.saturating_sub(l));
135+ if surface.redraw || since_flip >= CURSOR_FRAME_NS {
136+ draw(state, crtc, now);
137+ return;
138+ }
139+ let wait = Duration::from_nanos(CURSOR_FRAME_NS - since_flip);
140+ arm(&drm.handle, surface, crtc, wait, true, move |state| {
141+ draw(state, crtc, now_ns());
142+ });
143+ return;
144+ }
145+ let Some((vblank, start)) = surface.next_deadline(now) else {
146+ draw(state, crtc, now);
147+ return;
148+ };
149+ surface.target_ns = Some(vblank);
150+ let wait = Duration::from_nanos(start.saturating_sub(now));
151+ arm(&drm.handle, surface, crtc, wait, false, move |state| {
152+ let target = state
153+ .backend
154+ .drm()
155+ .and_then(|d| d.surfaces.get(&crtc)?.target_ns);
156+ if let Some(target) = target {
157+ draw(state, crtc, target);
158+ }
159+ });
160+}
161+
162+/// Runs `fire` once after `after`, tracked in `surface.scheduled` until then.
163+pub(super) fn arm(
164+ handle: &LoopHandle<'static, State>,
165+ surface: &mut Surface,
166+ crtc: crtc::Handle,
167+ after: Duration,
168+ preemptible: bool,
169+ mut fire: impl FnMut(&mut State) + 'static,
170+) {
171+ let token = handle.insert_source(Timer::from_duration(after), move |_, _, state| {
172+ if let Some(s) = surface_mut(state, crtc) {
173+ s.scheduled = None;
174+ }
175+ fire(state);
176+ TimeoutAction::Drop
177+ });
178+ match token {
179+ Ok(token) => {
180+ surface.scheduled = Some(token);
181+ surface.preemptible = preemptible;
182+ }
183+ Err(e) => tracing::warn!(
184+ output = surface.output.name(),
185+ "could not arm a render timer: {e}"
186+ ),
187+ }
188+}
189+
190+/// Arms the flip watchdog unless it already runs.
191+pub(super) fn watch(
192+ handle: &LoopHandle<'static, State>,
193+ surface: &mut Surface,
194+ crtc: crtc::Handle,
195+) {
196+ if surface.watchdog.is_some() {
197+ return;
198+ }
199+ let after = Duration::from_nanos(surface.flip_timeout());
200+ let token = handle.insert_source(Timer::from_duration(after), move |_, _, state| {
201+ flip_watchdog(state, crtc)
202+ });
203+ match token {
204+ Ok(token) => surface.watchdog = Some(token),
205+ Err(e) => tracing::warn!(
206+ output = surface.output.name(),
207+ "could not arm the flip watchdog: {e}"
208+ ),
209+ }
210+}
211+
212+/// Recovers an output whose page flip event never came, as after a
213+/// `DrmEvent::Error`, so `queued` cannot hold it dark forever.
214+pub(super) fn flip_watchdog(state: &mut State, crtc: crtc::Handle) -> TimeoutAction {
215+ let Some(drm) = state.backend.drm_mut() else {
216+ return TimeoutAction::Drop;
217+ };
218+ let active = drm.session.is_active();
219+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
220+ return TimeoutAction::Drop;
221+ };
222+ let age = now_ns().saturating_sub(surface.queued_at);
223+ let limit = surface.flip_timeout();
224+ if active && surface.queued && age < limit {
225+ return TimeoutAction::ToDuration(Duration::from_nanos(limit - age));
226+ }
227+ surface.watchdog = None;
228+ if !active || !surface.queued {
229+ return TimeoutAction::Drop;
230+ }
231+ tracing::warn!(output = surface.output.name(), "page flip never landed");
232+ if let Err(e) = surface.drm.frame_submitted() {
233+ tracing::warn!("frame was not submitted: {e}");
234+ }
235+ surface.queued = false;
236+ surface.target_ns = None;
237+ surface.redraw = true;
238+ schedule(state, crtc);
239+ TimeoutAction::Drop
240+}
241+
242+/// An animation whose frame had nothing to show, like one hidden behind a
243+/// fullscreen window, has no flip to wait for. Under VRR scheduling again
244+/// would draw at once and recurse until the stack overflowed, so the next
245+/// try waits a refresh.
246+pub(super) fn draw(state: &mut State, crtc: crtc::Handle, target: u64) {
247+ if !render_surface(state, crtc, target) {
248+ return;
249+ }
250+ let Some(drm) = state.backend.drm_mut() else {
251+ return;
252+ };
253+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
254+ return;
255+ };
256+ if surface.scheduled.is_some() {
257+ return;
258+ }
259+ let backoff = surface.failures.saturating_sub(1).min(6);
260+ let wait = (surface.refresh_ns.max(1_000_000) << backoff).min(1_000_000_000);
261+ let wait = Duration::from_nanos(wait);
262+ let preemptible = surface.failures == 0;
263+ arm(
264+ &drm.handle,
265+ surface,
266+ crtc,
267+ wait,
268+ preemptible,
269+ move |state| {
270+ schedule(state, crtc);
271+ },
272+ );
273+}
Added file: crates/monolith/src/drm/render.rs+232 additions
@@ -0,0 +1,232 @@
1+//! Drawing one output and queueing the frame.
2+
3+use super::pacing::{schedule, watch};
4+use crate::backend::Backend;
5+use crate::frame::{CLEAR, FrameCtx};
6+use crate::state::Services;
7+use crate::state::{State, now_ns};
8+use monolith_config::Vrr;
9+use smithay::backend::drm::compositor::PrimaryPlaneElement;
10+use smithay::backend::session::Session;
11+use smithay::reexports::drm::control::crtc;
12+
13+/// Frames render without vblanks while retrying or looping on empty frames,
14+/// and only a vblank drains the samples.
15+const MAX_CPU_SAMPLES: usize = 1024;
16+
17+/// Renders every output that has something new and no frame in flight.
18+pub fn render(state: &mut State) {
19+ let Some(drm) = state.backend.drm_mut() else {
20+ return;
21+ };
22+ let cursor = std::mem::take(&mut state.dirty.cursor);
23+ let all = std::mem::take(&mut state.dirty.redraw);
24+ for s in drm.surfaces.values_mut() {
25+ s.redraw |= all;
26+ s.cursor_redraw |= cursor;
27+ if all
28+ && s.preemptible
29+ && let Some(token) = s.scheduled.take()
30+ {
31+ drm.handle.remove(token);
32+ }
33+ }
34+ let ready: Vec<crtc::Handle> = drm
35+ .surfaces
36+ .iter()
37+ .filter(|(_, s)| (s.redraw || s.cursor_redraw) && !s.queued)
38+ .map(|(c, _)| *c)
39+ .collect();
40+ for crtc in ready {
41+ schedule(state, crtc);
42+ }
43+}
44+
45+/// Draws one output for the vblank at `target`. Returns whether it still
46+/// animates without a frame in flight, and so needs another render.
47+#[profiling::function]
48+pub(super) fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
49+ let State {
50+ backend: Backend::Drm(drm),
51+ renderer,
52+ wm,
53+ settings,
54+ core,
55+ input,
56+ services:
57+ Services {
58+ lock,
59+ captures,
60+ xwayland,
61+ ..
62+ },
63+ ..
64+ } = state
65+ else {
66+ return false;
67+ };
68+ if !drm.session.is_active() {
69+ return false;
70+ }
71+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
72+ return false;
73+ };
74+ if surface.queued || !surface.powered {
75+ return false;
76+ }
77+ surface.cursor_redraw = false;
78+ let shown = crate::frame::shown_windows(wm, surface.id, &surface.output, target);
79+ let fullscreen = shown.fullscreen;
80+ let forced = fullscreen.and_then(|w| wm.get(w)?.rule.vrr);
81+ let want_vrr = surface.vrr_capable
82+ && forced.unwrap_or(match settings.config.output.vrr {
83+ Vrr::Off => false,
84+ Vrr::On => true,
85+ Vrr::Fullscreen => fullscreen.is_some(),
86+ });
87+ if want_vrr != surface.vrr_on {
88+ match surface.drm.with_compositor(|c| c.use_vrr(want_vrr)) {
89+ Ok(()) => {
90+ surface.vrr_on = want_vrr;
91+ tracing::info!(
92+ output = surface.output.name(),
93+ vrr = want_vrr,
94+ "adaptive sync"
95+ );
96+ }
97+ Err(e) => {
98+ tracing::warn!("could not switch adaptive sync: {e}");
99+ surface.vrr_capable = false;
100+ }
101+ }
102+ }
103+ let began = std::time::Instant::now();
104+ let frame_cpu_began = thread_cpu_ns();
105+ let now = target;
106+ if wm.layout.output(surface.id).is_none() {
107+ return false;
108+ }
109+ let scene_began = std::time::Instant::now();
110+ let ctx = FrameCtx {
111+ renderer: &mut *renderer,
112+ wm: &mut *wm,
113+ input: &mut *input,
114+ lock: &*lock,
115+ deco: &settings.config.decoration,
116+ unmanaged: xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
117+ };
118+ let elements = crate::frame::compose(ctx, surface.id, &surface.output, now, true);
119+ let scene_ns = scene_began.elapsed().as_nanos() as u64;
120+ let animating = wm.anim.active(now);
121+ let render_began = std::time::Instant::now();
122+ let cpu_began = thread_cpu_ns();
123+ let mut queue_ns = 0;
124+ let mut failed = false;
125+ match surface
126+ .drm
127+ .render_frame(renderer, &elements, CLEAR, drm.frame_flags)
128+ {
129+ Ok(result) if !result.is_empty => {
130+ let direct = matches!(result.primary_element, PrimaryPlaneElement::Element(_));
131+ let feedback = crate::frame::take_presentation_feedback(
132+ wm,
133+ lock,
134+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
135+ &shown,
136+ &surface.output,
137+ &result.states,
138+ );
139+ if direct {
140+ surface.scanned_out += 1;
141+ }
142+ if direct != surface.scanout {
143+ surface.scanout = direct;
144+ tracing::info!(
145+ output = surface.output.name(),
146+ scanout = direct,
147+ elements = elements.len(),
148+ overlay_planes = result.overlay_elements.len(),
149+ cursor_plane = result.cursor_element.is_some(),
150+ "direct scanout"
151+ );
152+ }
153+ let queue_began = std::time::Instant::now();
154+ match surface.drm.queue_frame(feedback) {
155+ Ok(()) => {
156+ surface.queued = true;
157+ surface.queued_at = now_ns();
158+ watch(&drm.handle, surface, crtc);
159+ lock.shown(&surface.output);
160+ captures.output_damaged(&surface.output);
161+ }
162+ Err(e) => {
163+ failed = true;
164+ tracing::warn!("queueing a frame failed: {e}");
165+ }
166+ }
167+ queue_ns = queue_began.elapsed().as_nanos() as u64;
168+ }
169+ Ok(result) => {
170+ drop(crate::frame::take_presentation_feedback(
171+ wm,
172+ lock,
173+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
174+ &shown,
175+ &surface.output,
176+ &result.states,
177+ ));
178+ lock.shown(&surface.output);
179+ }
180+ Err(e) => {
181+ failed = true;
182+ tracing::warn!("rendering failed: {e}");
183+ }
184+ }
185+ let render_cpu_ns = thread_cpu_ns() - cpu_began;
186+ let render_ns = render_began.elapsed().as_nanos() as u64 - queue_ns;
187+ let took = began.elapsed().as_nanos() as u64;
188+ surface.budget.record(took);
189+ if surface.frame_cpu_ns.len() < MAX_CPU_SAMPLES {
190+ surface.frame_cpu_ns.push(thread_cpu_ns() - frame_cpu_began);
191+ }
192+ if took > surface.refresh_ns / 2 {
193+ tracing::debug!(
194+ output = surface.output.name(),
195+ took_ns = took,
196+ scene_ns,
197+ render_ns,
198+ render_cpu_ns,
199+ queue_ns,
200+ "slow frame"
201+ );
202+ }
203+ if !surface.queued {
204+ surface.target_ns = None;
205+ }
206+ surface.failures = if failed { surface.failures + 1 } else { 0 };
207+ surface.redraw = animating || failed;
208+ if !surface.queued {
209+ crate::fifo::release_idle(&core.handle, &surface.output);
210+ }
211+ let elapsed = core.start.elapsed();
212+ crate::frame::send_frame_callbacks(
213+ wm,
214+ lock,
215+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
216+ &shown,
217+ &surface.output,
218+ elapsed,
219+ input.dnd_icon.as_ref(),
220+ );
221+ crate::frame::send_cursor_frames(wm, input, surface.id, &surface.output, elapsed);
222+ (animating || failed) && !surface.queued
223+}
224+
225+/// CPU time this thread has used, to tell a render that works from one that
226+/// waits or is descheduled.
227+pub(super) fn thread_cpu_ns() -> u64 {
228+ let t = smithay::reexports::rustix::time::clock_gettime(
229+ smithay::reexports::rustix::time::ClockId::ThreadCPUTime,
230+ );
231+ t.tv_sec as u64 * 1_000_000_000 + t.tv_nsec as u64
232+}
Added file: crates/monolith/src/drm/session.rs+176 additions
@@ -0,0 +1,176 @@
1+//! The libseat session, udev and libinput setup, and the session pause and
2+//! resume that follow.
3+
4+use super::Drm;
5+use super::input::on_input;
6+use super::outputs::scan;
7+use super::pacing::on_vblank;
8+use crate::backend::Backend;
9+use crate::devices::Devices;
10+use crate::state::State;
11+use smithay::backend::allocator::Fourcc;
12+use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
13+use smithay::backend::drm::compositor::FrameFlags;
14+use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
15+use smithay::backend::drm::output::DrmOutputManager;
16+use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType};
17+use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
18+use smithay::backend::session::libseat::LibSeatSession;
19+use smithay::backend::session::{Event as SessionEvent, Session};
20+use smithay::backend::udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu};
21+use smithay::reexports::calloop::EventLoop;
22+use smithay::reexports::drm::Device as _;
23+use smithay::reexports::input::Libinput;
24+use smithay::reexports::rustix::fs::OFlags;
25+use smithay::utils::DeviceFd;
26+use smithay::wayland::drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd};
27+use smithay_drm_extras::drm_scanner::DrmScanner;
28+use std::collections::{HashMap, HashSet};
29+use std::path::{Path, PathBuf};
30+
31+/// The GPU monolith drives on bare metal, the seat's primary or else its
32+/// first.
33+pub fn primary() -> Result<PathBuf, Box<dyn std::error::Error>> {
34+ let seat = std::env::var("XDG_SEAT").unwrap_or_else(|_| "seat0".into());
35+ Ok(match primary_gpu(&seat)? {
36+ Some(p) => p,
37+ None => all_gpus(&seat)?.into_iter().next().ok_or("no GPU found")?,
38+ })
39+}
40+
41+pub fn init(
42+ event_loop: &mut EventLoop<'static, State>,
43+ state: &mut State,
44+ path: &Path,
45+) -> Result<(), Box<dyn std::error::Error>> {
46+ let (mut session, notifier) = LibSeatSession::new()?;
47+ let seat = session.seat();
48+ let node = DrmNode::from_path(path)?;
49+ tracing::info!(path = %path.display(), "primary GPU");
50+ let fd = session.open(
51+ path,
52+ OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
53+ )?;
54+ let fd = DrmDeviceFd::new(DeviceFd::from(fd));
55+ let (device, drm_events) = DrmDevice::new(fd.clone(), true)?;
56+ let nvidia = device
57+ .get_driver()
58+ .map(|d| d.name().to_string_lossy().to_lowercase().contains("nvidia"))
59+ .unwrap_or(false);
60+ let syncobj = if supports_syncobj_eventfd(&fd) {
61+ Some(DrmSyncobjState::new::<State>(
62+ &state.core.display,
63+ fd.clone(),
64+ ))
65+ } else {
66+ tracing::info!("no syncobj eventfd support, clients stay on implicit sync");
67+ None
68+ };
69+ let gbm = GbmDevice::new(fd)?;
70+ let render_node = node
71+ .node_with_type(NodeType::Render)
72+ .and_then(Result::ok)
73+ .unwrap_or(node);
74+ let allocator = GbmAllocator::new(
75+ gbm.clone(),
76+ GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
77+ );
78+ let exporter = GbmFramebufferExporter::new(gbm.clone(), render_node.into());
79+ let render_formats: Vec<_> = state.renderer.render_formats().iter().copied().collect();
80+ let manager = DrmOutputManager::new(
81+ device,
82+ allocator,
83+ exporter,
84+ Some(gbm),
85+ [Fourcc::Xrgb8888, Fourcc::Argb8888],
86+ render_formats,
87+ );
88+
89+ let mut libinput =
90+ Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
91+ libinput
92+ .udev_assign_seat(&seat)
93+ .map_err(|()| "libinput could not take the seat")?;
94+ let handle = event_loop.handle();
95+ handle.insert_source(
96+ LibinputInputBackend::new(libinput.clone()),
97+ |event, _, state| {
98+ on_input(state, event);
99+ },
100+ )?;
101+ handle.insert_source(notifier, |event, _, state| match event {
102+ SessionEvent::PauseSession => {
103+ if let Some(drm) = state.backend.drm_mut() {
104+ drm.libinput.suspend();
105+ drm.manager.pause();
106+ }
107+ }
108+ SessionEvent::ActivateSession => {
109+ if let Some(drm) = state.backend.drm_mut() {
110+ if drm.libinput.resume().is_err() {
111+ tracing::warn!("libinput did not resume");
112+ }
113+ if let Err(e) = drm.manager.lock().activate(false) {
114+ tracing::warn!("could not reactivate the GPU: {e}");
115+ }
116+ for s in drm.surfaces.values_mut() {
117+ if let Some(token) = s.scheduled.take() {
118+ drm.handle.remove(token);
119+ }
120+ if let Err(e) = s.drm.frame_submitted() {
121+ tracing::warn!("could not drop the flip left over from the pause: {e}");
122+ }
123+ s.queued = false;
124+ s.last_vblank_ns = None;
125+ s.target_ns = None;
126+ s.failures = 0;
127+ s.redraw = true;
128+ if !s.powered
129+ && let Err(e) = s.drm.with_compositor(|c| c.clear())
130+ {
131+ tracing::warn!("could not keep the output off: {e}");
132+ }
133+ }
134+ }
135+ scan(state);
136+ state.dirty.redraw = true;
137+ }
138+ })?;
139+ handle.insert_source(drm_events, |event, meta, state| match event {
140+ DrmEvent::VBlank(crtc) => on_vblank(state, crtc, meta),
141+ DrmEvent::Error(e) => tracing::warn!("drm: {e}"),
142+ })?;
143+ let udev = UdevBackend::new(&seat)?;
144+ handle.insert_source(udev, move |event, _, state| {
145+ if let UdevEvent::Changed { device_id } = event
146+ && state
147+ .backend
148+ .drm()
149+ .is_some_and(|d| d.node.dev_id() == device_id && d.session.is_active())
150+ {
151+ scan(state);
152+ }
153+ })?;
154+
155+ state.backend = Backend::Drm(Box::new(Drm {
156+ session,
157+ libinput,
158+ handle,
159+ node,
160+ render_node,
161+ syncobj,
162+ manager,
163+ scanner: DrmScanner::new(),
164+ nvidia,
165+ frame_flags: if std::env::var_os("MONOLITH_NO_SCANOUT").is_some() {
166+ FrameFlags::empty()
167+ } else {
168+ FrameFlags::DEFAULT
169+ },
170+ surfaces: HashMap::new(),
171+ failed: HashSet::new(),
172+ devices: Devices::default(),
173+ }));
174+ scan(state);
175+ Ok(())
176+}