atagen

monolith

public
Star0
Watch0

Commit 595b66571820

tarball

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

6 files changed+239-34
presentation: implement wp_presentation feedback

Clients pacing video or games had no wp_presentation global, so they fell back to guessing vblank times from frame callbacks. DrmCompositor now carries an OutputPresentationFeedback as the queue_frame user data, and on_vblank marks it presented with the kernel flip time and sequence, a variable refresh under VRR and ZERO_COPY for surfaces that went to a plane. Frames that never flip discard their feedback. The nested backend forwards the host's presented events, or falls back to the host frame callback.
Modified file: crates/monolith/src/drm.rs+50 additions−7 deletions
@@ -37,7 +37,10 @@ use smithay::reexports::input::Libinput;
3737 use smithay::reexports::input::event::switch::{Switch, SwitchState};
3838 use smithay::reexports::rustix::fs::OFlags;
3939 use smithay::reexports::wayland_server::backend::GlobalId;
40-use smithay::utils::{DeviceFd, Logical, Point, Transform};
40+use smithay::desktop::utils::OutputPresentationFeedback;
41+use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind;
42+use smithay::utils::{DeviceFd, Logical, Monotonic, Point, Time, Transform};
43+use smithay::wayland::presentation::Refresh;
4144 use smithay::wayland::drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd};
4245 use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
4346 use std::collections::{HashMap, HashSet};
@@ -55,8 +58,8 @@ const GLOBAL_REMOVAL_DELAY: Duration = Duration::from_secs(5);
5558
5659 type Allocator = GbmAllocator<DrmDeviceFd>;
5760 type Exporter = GbmFramebufferExporter<DrmDeviceFd>;
58-type Manager = DrmOutputManager<Allocator, Exporter, (), DrmDeviceFd>;
59-type Out = DrmOutput<Allocator, Exporter, (), DrmDeviceFd>;
61+type Manager = DrmOutputManager<Allocator, Exporter, OutputPresentationFeedback, DrmDeviceFd>;
62+type Out = DrmOutput<Allocator, Exporter, OutputPresentationFeedback, DrmDeviceFd>;
6063
6164 pub struct Drm {
6265 pub session: LibSeatSession,
@@ -684,9 +687,10 @@ fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMe
684687 else {
685688 return;
686689 };
687- if let Err(e) = surface.drm.frame_submitted() {
690+ let feedback = surface.drm.frame_submitted().unwrap_or_else(|e| {
688691 tracing::warn!("frame was not submitted: {e}");
689- }
692+ None
693+ });
690694 surface.queued = false;
691695 if let Err(e) = state.renderer.absorb_submit_stall() {
692696 tracing::warn!("absorbing a submit stall failed: {e}");
@@ -698,6 +702,25 @@ fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMe
698702 else {
699703 return;
700704 };
705+ if let Some(mut feedback) = feedback {
706+ let (time, clock) = match meta.as_ref().map(|m| m.time) {
707+ Some(DrmEventTime::Monotonic(t)) => (t, Kind::HwClock | Kind::HwCompletion),
708+ _ => (Duration::from_nanos(now_ns()), Kind::empty()),
709+ };
710+ let refresh = Duration::from_nanos(surface.refresh_ns);
711+ let refresh = if surface.vrr_on {
712+ Refresh::variable(refresh)
713+ } else {
714+ Refresh::fixed(refresh)
715+ };
716+ let sequence = meta.as_ref().map_or(0, |m| u64::from(m.sequence));
717+ feedback.presented(
718+ Time::<Monotonic>::from(time),
719+ refresh,
720+ sequence,
721+ Kind::Vsync | clock,
722+ );
723+ }
701724 if let Some(DrmEventTime::Monotonic(t)) = meta.as_ref().map(|m| m.time) {
702725 let t = t.as_nanos() as u64;
703726 if let Some(target) = surface.target_ns.take() {
@@ -1017,6 +1040,15 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
10171040 {
10181041 Ok(result) if !result.is_empty => {
10191042 let direct = matches!(result.primary_element, PrimaryPlaneElement::Element(_));
1043+ let feedback = crate::frame::take_presentation_feedback(
1044+ wm,
1045+ lock,
1046+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
1047+ surface.id,
1048+ &surface.output,
1049+ now,
1050+ &result.states,
1051+ );
10201052 if direct {
10211053 surface.scanned_out += 1;
10221054 }
@@ -1032,7 +1064,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
10321064 );
10331065 }
10341066 let queue_began = std::time::Instant::now();
1035- match surface.drm.queue_frame(()) {
1067+ match surface.drm.queue_frame(feedback) {
10361068 Ok(()) => {
10371069 surface.queued = true;
10381070 surface.queued_at = now_ns();
@@ -1047,7 +1079,18 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
10471079 }
10481080 queue_ns = queue_began.elapsed().as_nanos() as u64;
10491081 }
1050- Ok(_) => lock.shown(&surface.output),
1082+ Ok(result) => {
1083+ drop(crate::frame::take_presentation_feedback(
1084+ wm,
1085+ lock,
1086+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
1087+ surface.id,
1088+ &surface.output,
1089+ now,
1090+ &result.states,
1091+ ));
1092+ lock.shown(&surface.output);
1093+ }
10511094 Err(e) => {
10521095 failed = true;
10531096 tracing::warn!("rendering failed: {e}");
Modified file: crates/monolith/src/frame.rs+71 additions−12 deletions
@@ -9,10 +9,17 @@ use crate::wm::Wm;
99 use monolith_config::Decoration;
1010 use monolith_layout::OutputId;
1111 use monolith_render_vk::VulkanRenderer;
12-use smithay::desktop::utils::send_frames_surface_tree;
12+use smithay::backend::renderer::element::RenderElementStates;
13+use smithay::desktop::Window;
14+use smithay::desktop::utils::{
15+ OutputPresentationFeedback, send_frames_surface_tree,
16+ surface_presentation_feedback_flags_from_states, take_presentation_feedback_surface_tree,
17+};
1318 use smithay::input::pointer::CursorImageStatus;
1419 use smithay::output::Output;
20+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
1521 use smithay::utils::Point;
22+use smithay::wayland::compositor::SurfaceData;
1623 use smithay::xwayland::X11Surface;
1724 use std::time::Duration;
1825
@@ -100,21 +107,13 @@ pub fn send_frame_callbacks(
100107 crate::lock::send_frames(lock, output, elapsed);
101108 return;
102109 }
103- let arr = wm.layout.arrange(o);
104- let covering = arr
105- .fullscreen
106- .filter(|&w| wm.anim.gliding(w, now).is_none())
107- .and_then(|w| wm.window(w));
108- let over: Vec<_> = arr.over.iter().filter_map(|&w| wm.window(w)).collect();
109- for window in wm.space.elements_for_output(output) {
110- if covering.is_some_and(|c| c != window && !over.contains(&window)) {
111- continue;
112- }
110+ let (windows, covered) = shown_windows(wm, o, output, now);
111+ for window in windows {
113112 window.send_frame(output, elapsed, Some(Duration::ZERO), |_, _| {
114113 Some(output.clone())
115114 });
116115 }
117- crate::layers::send_frames(output, elapsed, covering.is_some());
116+ crate::layers::send_frames(output, elapsed, covered);
118117 if let Some(geo) = wm.space.output_geometry(output) {
119118 for surface in unmanaged
120119 .iter()
@@ -128,6 +127,66 @@ pub fn send_frame_callbacks(
128127 }
129128 }
130129
130+/// The windows `output` shows at `now`, and whether a fullscreen window at
131+/// rest hides everything below the overlay layer.
132+fn shown_windows<'a>(
133+ wm: &'a Wm,
134+ o: OutputId,
135+ output: &'a Output,
136+ now: u64,
137+) -> (Vec<&'a Window>, bool) {
138+ let arr = wm.layout.arrange(o);
139+ let covering = arr
140+ .fullscreen
141+ .filter(|&w| wm.anim.gliding(w, now).is_none())
142+ .and_then(|w| wm.window(w));
143+ let over: Vec<_> = arr.over.iter().filter_map(|&w| wm.window(w)).collect();
144+ let windows = wm
145+ .space
146+ .elements_for_output(output)
147+ .filter(|&w| covering.is_none_or(|c| c == w || over.contains(&w)))
148+ .collect();
149+ (windows, covering.is_some())
150+}
151+
152+/// Takes the presentation feedback of everything `output` shows at `now`,
153+/// to be marked presented once the frame that was just rendered reaches the
154+/// screen. `states` tells which surfaces went to a plane untouched.
155+pub fn take_presentation_feedback(
156+ wm: &Wm,
157+ lock: &Lock,
158+ unmanaged: &[X11Surface],
159+ o: OutputId,
160+ output: &Output,
161+ now: u64,
162+ states: &RenderElementStates,
163+) -> OutputPresentationFeedback {
164+ let mut feedback = OutputPresentationFeedback::new(output);
165+ let primary = |_: &WlSurface, _: &SurfaceData| Some(output.clone());
166+ let flags = |s: &WlSurface, _: &SurfaceData| {
167+ surface_presentation_feedback_flags_from_states(s, None, states)
168+ };
169+ if lock.active() {
170+ crate::lock::take_presentation_feedback(lock, output, &mut feedback, primary, flags);
171+ return feedback;
172+ }
173+ let (windows, covered) = shown_windows(wm, o, output, now);
174+ for window in windows {
175+ window.take_presentation_feedback(&mut feedback, primary, flags);
176+ }
177+ crate::layers::take_presentation_feedback(output, covered, &mut feedback, primary, flags);
178+ if let Some(geo) = wm.space.output_geometry(output) {
179+ for surface in unmanaged
180+ .iter()
181+ .filter(|x| geo.overlaps(x.geometry()))
182+ .filter_map(|x| x.wl_surface())
183+ {
184+ take_presentation_feedback_surface_tree(&surface, &mut feedback, primary, flags);
185+ }
186+ }
187+ feedback
188+}
189+
131190 /// Lets a client that set its own cursor image draw the next frame of it
132191 /// while the pointer is on `output`, so animated cursors keep moving.
133192 pub fn send_cursor_frames(wm: &Wm, input: &Input, o: OutputId, output: &Output, elapsed: Duration) {
Modified file: crates/monolith/src/globals.rs+4 additions
@@ -9,6 +9,7 @@ use monolith_render_vk::VulkanRenderer;
99 use smithay::backend::renderer::ImportDma;
1010 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities;
1111 use smithay::reexports::wayland_server::DisplayHandle;
12+use smithay::utils::{ClockSource, Monotonic};
1213 use smithay::wayland::compositor::CompositorState;
1314 use smithay::wayland::cursor_shape::CursorShapeManagerState;
1415 use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal, DmabufState};
@@ -20,6 +21,7 @@ use smithay::wayland::image_capture_source::{
2021 };
2122 use smithay::wayland::image_copy_capture::ImageCopyCaptureState;
2223 use smithay::wayland::output::OutputManagerState;
24+use smithay::wayland::presentation::PresentationState;
2325 use smithay::wayland::selection::data_device::DataDeviceState;
2426 use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
2527 use smithay::wayland::selection::primary_selection::PrimarySelectionState;
@@ -67,6 +69,7 @@ pub struct Protocols {
6769 _viewporter: ViewporterState,
6870 _single_pixel_buffer: SinglePixelBufferState,
6971 _fractional_scale: FractionalScaleManagerState,
72+ _presentation: PresentationState,
7073 }
7174
7275 impl Protocols {
@@ -130,6 +133,7 @@ impl Protocols {
130133 _viewporter: ViewporterState::new::<State>(dh),
131134 _single_pixel_buffer: SinglePixelBufferState::new::<State>(dh),
132135 _fractional_scale: FractionalScaleManagerState::new::<State>(dh),
136+ _presentation: PresentationState::new::<State>(dh, Monotonic::ID as u32),
133137 }
134138 }
135139 }
Modified file: crates/monolith/src/layers.rs+18 additions−1 deletions
@@ -11,12 +11,14 @@ use smithay::backend::renderer::element::surface::{
1111 WaylandSurfaceRenderElement, render_elements_from_surface_tree,
1212 };
1313 use smithay::desktop::{LayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output};
14+use smithay::desktop::utils::OutputPresentationFeedback;
1415 use smithay::output::Output;
16+use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind;
1517 use smithay::reexports::wayland_server::Resource;
1618 use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
1719 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
1820 use smithay::utils::{Logical, Point, Scale};
19-use smithay::wayland::compositor::with_states;
21+use smithay::wayland::compositor::{SurfaceData, with_states};
2022 use smithay::wayland::seat::WaylandFocus;
2123 use smithay::wayland::shell::wlr_layer::{
2224 KeyboardInteractivity, Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData,
@@ -306,3 +308,18 @@ pub fn send_frames(output: &Output, elapsed: Duration, covered: bool) {
306308 });
307309 }
308310 }
311+
312+pub fn take_presentation_feedback(
313+ output: &Output,
314+ covered: bool,
315+ feedback: &mut OutputPresentationFeedback,
316+ primary: impl FnMut(&WlSurface, &SurfaceData) -> Option<Output> + Copy,
317+ flags: impl FnMut(&WlSurface, &SurfaceData) -> PresentationKind + Copy,
318+) {
319+ for l in layer_map_for_output(output).layers() {
320+ if covered && l.layer() != Layer::Overlay {
321+ continue;
322+ }
323+ l.take_presentation_feedback(feedback, primary, flags);
324+ }
325+}
Modified file: crates/monolith/src/lock.rs+18 additions−1 deletions
@@ -7,11 +7,16 @@ use monolith_render_vk::VulkanRenderer;
77 use smithay::backend::renderer::element::Kind;
88 use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
99 use smithay::desktop::WindowSurfaceType;
10-use smithay::desktop::utils::{send_frames_surface_tree, under_from_surface_tree};
10+use smithay::desktop::utils::{
11+ OutputPresentationFeedback, send_frames_surface_tree, take_presentation_feedback_surface_tree,
12+ under_from_surface_tree,
13+};
1114 use smithay::output::Output;
1215 use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
1316 use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
17+use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind;
1418 use smithay::utils::{Logical, Point};
19+use smithay::wayland::compositor::SurfaceData;
1520 use smithay::wayland::session_lock::{
1621 LockSurface, SessionLockHandler, SessionLockManagerState, SessionLocker,
1722 };
@@ -212,3 +217,15 @@ pub fn send_frames(lock: &Lock, output: &Output, elapsed: Duration) {
212217 );
213218 }
214219 }
220+
221+pub fn take_presentation_feedback(
222+ lock: &Lock,
223+ output: &Output,
224+ feedback: &mut OutputPresentationFeedback,
225+ primary: impl FnMut(&WlSurface, &SurfaceData) -> Option<Output>,
226+ flags: impl FnMut(&WlSurface, &SurfaceData) -> PresentationKind,
227+) {
228+ if let Some(surface) = lock.surface_on(output) {
229+ take_presentation_feedback_surface_tree(surface.wl_surface(), feedback, primary, flags);
230+ }
231+}
Modified file: crates/monolith/src/nested/mod.rs+78 additions−13 deletions
@@ -7,7 +7,7 @@ mod input;
77
88 use crate::backend::{Backend, logical_area};
99 use crate::state::Services;
10-use crate::state::State;
10+use crate::state::{State, now_ns};
1111 use calloop_wayland_source::WaylandSource;
1212 use monolith_layout::OutputId;
1313 use monolith_render_vk::VulkanAllocator;
@@ -17,10 +17,14 @@ use smithay::backend::renderer::Bind;
1717 use smithay::backend::renderer::damage::OutputDamageTracker;
1818 use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
1919 use smithay::reexports::calloop::EventLoop;
20-use smithay::utils::{Logical, Size, Transform};
20+use smithay::desktop::utils::OutputPresentationFeedback;
21+use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind;
22+use smithay::utils::{Logical, Monotonic, Size, Time, Transform};
23+use smithay::wayland::presentation::Refresh;
24+use std::time::Duration;
2125 use wayland_client::globals::{GlobalListContents, registry_queue_init};
2226 use wayland_client::protocol::{wl_buffer, wl_callback, wl_compositor, wl_registry, wl_surface};
23-use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
27+use wayland_client::{Connection, Dispatch, QueueHandle, WEnum, delegate_noop};
2428 use wayland_protocols::wp::linux_dmabuf::zv1::client::{
2529 zwp_linux_buffer_params_v1, zwp_linux_dmabuf_v1,
2630 };
@@ -56,6 +60,11 @@ pub struct Host {
5660 monotonic: bool,
5761 last_present_ns: Option<u64>,
5862 refresh_ns: u64,
63+ feedback: Vec<(u64, OutputPresentationFeedback)>,
64+ next_feedback: u64,
65+ /// Without host presentation-time the frame callback is the nearest
66+ /// thing to a presented event.
67+ awaiting_frame: Vec<OutputPresentationFeedback>,
5968 }
6069
6170 impl Host {
@@ -155,6 +164,9 @@ pub fn init(
155164 monotonic: false,
156165 last_present_ns: None,
157166 refresh_ns: 16_666_667,
167+ feedback: Vec::new(),
168+ next_feedback: 0,
169+ awaiting_frame: Vec::new(),
158170 }));
159171 queue.roundtrip(state)?;
160172 queue.roundtrip(state)?;
@@ -252,7 +264,7 @@ pub fn render(state: &mut State) {
252264 }
253265 };
254266 let mut dmabuf = (*slot).clone();
255- let damage = {
267+ let (damage, feedback) = {
256268 let mut fb = match renderer.bind(&mut dmabuf) {
257269 Ok(fb) => fb,
258270 Err(e) => {
@@ -278,7 +290,16 @@ pub fn render(state: &mut State) {
278290 if let Err(e) = result.sync.wait() {
279291 tracing::warn!("waiting for the frame failed: {e:?}");
280292 }
281- result.damage.cloned()
293+ let feedback = crate::frame::take_presentation_feedback(
294+ wm,
295+ lock,
296+ xwayland.as_ref().map_or(&[], |x| &x.unmanaged),
297+ o,
298+ &host.output,
299+ now,
300+ &result.states,
301+ );
302+ (result.damage.cloned(), feedback)
282303 }
283304 Err(e) => {
284305 tracing::warn!("rendering failed: {e:?}");
@@ -326,7 +347,12 @@ pub fn render(state: &mut State) {
326347 }
327348 host.surface.frame(&host.qh, ());
328349 if let Some(p) = &host.presentation {
329- p.feedback(&host.surface, &host.qh, ());
350+ let id = host.next_feedback;
351+ host.next_feedback += 1;
352+ p.feedback(&host.surface, &host.qh, id);
353+ host.feedback.push((id, feedback));
354+ } else {
355+ host.awaiting_frame.push(feedback);
330356 }
331357 host.surface.commit();
332358 swapchain.submitted(&slot);
@@ -520,6 +546,14 @@ impl Dispatch<wl_callback::WlCallback, ()> for State {
520546 && let Some(host) = state.backend.nested_mut()
521547 {
522548 host.frame_pending = false;
549+ for mut feedback in host.awaiting_frame.drain(..) {
550+ feedback.presented(
551+ Time::<Monotonic>::from(Duration::from_nanos(now_ns())),
552+ Refresh::fixed(Duration::from_nanos(host.refresh_ns)),
553+ 0,
554+ Kind::empty(),
555+ );
556+ }
523557 }
524558 }
525559 }
@@ -547,30 +581,61 @@ impl Dispatch<wp_presentation::WpPresentation, ()> for State {
547581 }
548582 }
549583
550-impl Dispatch<wp_presentation_feedback::WpPresentationFeedback, ()> for State {
584+impl Dispatch<wp_presentation_feedback::WpPresentationFeedback, u64> for State {
551585 fn event(
552586 state: &mut Self,
553587 _: &wp_presentation_feedback::WpPresentationFeedback,
554588 event: wp_presentation_feedback::Event,
555- _: &(),
589+ id: &u64,
556590 _: &Connection,
557591 _: &QueueHandle<Self>,
558592 ) {
593+ let Some(host) = state.backend.nested_mut() else {
594+ return;
595+ };
596+ let pending = host.feedback.iter().position(|(i, _)| i == id);
559597 let wp_presentation_feedback::Event::Presented {
560598 tv_sec_hi,
561599 tv_sec_lo,
562600 tv_nsec,
563601 refresh,
564- ..
602+ seq_hi,
603+ seq_lo,
604+ flags,
565605 } = event
566606 else {
567- return;
568- };
569- let Some(host) = state.backend.nested_mut() else {
607+ if let wp_presentation_feedback::Event::Discarded = event
608+ && let Some(i) = pending
609+ {
610+ host.feedback.remove(i);
611+ }
570612 return;
571613 };
572614 let secs = (u64::from(tv_sec_hi) << 32) | u64::from(tv_sec_lo);
573- host.last_present_ns = Some(secs * 1_000_000_000 + u64::from(tv_nsec));
615+ let presented_ns = secs * 1_000_000_000 + u64::from(tv_nsec);
616+ host.last_present_ns = Some(presented_ns);
617+ if let Some(i) = pending {
618+ let (_, mut feedback) = host.feedback.remove(i);
619+ let (time, flags) = if host.monotonic {
620+ let bits = match flags {
621+ WEnum::Value(k) => k.bits(),
622+ WEnum::Unknown(bits) => bits,
623+ };
624+ (presented_ns, Kind::from_bits_truncate(bits))
625+ } else {
626+ (now_ns(), Kind::empty())
627+ };
628+ let refresh = match refresh {
629+ 0 => Refresh::Unknown,
630+ ns => Refresh::fixed(Duration::from_nanos(u64::from(ns))),
631+ };
632+ feedback.presented(
633+ Time::<Monotonic>::from(Duration::from_nanos(time)),
634+ refresh,
635+ (u64::from(seq_hi) << 32) | u64::from(seq_lo),
636+ flags,
637+ );
638+ }
574639 if refresh > 0 && u64::from(refresh) != host.refresh_ns {
575640 host.refresh_ns = u64::from(refresh);
576641 let mode = Mode {