diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 825f712..2f51a73 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -7,10 +7,11 @@ use std::{ ffi::OsString, os::fd::FromRawFd, path::Path, - time::Duration, + time::{Duration, Instant}, }; use anyhow::Context; +use calloop::Idle; use smithay::{ backend::{ allocator::{ @@ -28,10 +29,7 @@ use smithay::{ libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{ damage::{self}, - element::{ - surface::WaylandSurfaceRenderElement, texture::TextureBuffer, RenderElement, - RenderElementStates, - }, + element::{texture::TextureBuffer, RenderElement, RenderElementStates}, gles::{GlesRenderer, GlesTexture}, multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer, MultiTexture}, Bind, ExportMem, ImportDma, ImportEgl, ImportMemWl, Offscreen, Renderer, @@ -53,10 +51,7 @@ use smithay::{ output::{Output, PhysicalProperties, Subpixel}, reexports::{ ash::vk::ExtPhysicalDeviceDrmFn, - calloop::{ - timer::{TimeoutAction, Timer}, - EventLoop, LoopHandle, RegistrationToken, - }, + calloop::{EventLoop, LoopHandle, RegistrationToken}, drm::{ self, control::{connector, crtc, ModeTypeFlags}, @@ -88,7 +83,7 @@ use crate::{ ConnectorSavedState, }, output::OutputName, - render::{pointer::PointerElement, take_presentation_feedback, CustomRenderElements}, + render::{pointer::PointerElement, take_presentation_feedback}, state::{CalloopData, State, SurfaceDmabufFeedback, WithState}, window::WindowElement, }; @@ -128,6 +123,8 @@ pub struct Udev { pointer_images: Vec<(xcursor::parser::Image, TextureBuffer)>, pointer_element: PointerElement, pointer_image: crate::cursor::Cursor, + + last_vblank_time: Instant, } impl Backend { @@ -142,6 +139,30 @@ impl Backend { } } +impl Udev { + pub fn schedule_render(&mut self, loop_handle: &LoopHandle, output: &Output) { + let Some(surface) = render_surface_for_output(output, &mut self.backends) else { + return; + }; + // tracing::debug!(state = ?surface.render_state, "scheduling render"); + + match &surface.render_state { + RenderState::Idle => { + let output = output.clone(); + let token = loop_handle.insert_idle(move |data| { + data.state.render_surface(&output); + }); + + surface.render_state = RenderState::Scheduled(token); + } + RenderState::Scheduled(_) => (), + RenderState::WaitingForVblank { dirty: _ } => { + surface.render_state = RenderState::WaitingForVblank { dirty: true } + } + } + } +} + impl BackendData for Udev { fn seat_name(&self) -> String { self.session.seat() @@ -205,6 +226,8 @@ pub fn run_udev() -> anyhow::Result<()> { pointer_image: crate::cursor::Cursor::load(), pointer_images: Vec::new(), pointer_element: PointerElement::default(), + + last_vblank_time: Instant::now(), }; let display_handle = display.handle(); @@ -314,9 +337,10 @@ pub fn run_udev() -> anyhow::Result<()> { } for output in data.state.space.outputs().cloned().collect::>() { - data.state - .loop_handle - .insert_idle(move |data| data.state.render_surface(&output)); + data.state.schedule_render(&output); + // data.state + // .loop_handle + // .insert_idle(move |data| data.state.render_surface(&output)); } } } @@ -533,6 +557,26 @@ struct DrmSurfaceDmabufFeedback { scanout_feedback: DmabufFeedback, } +/// The state of a [`RenderSurface`]. +#[derive(Debug)] +enum RenderState { + /// No render is scheduled. + Idle, + /// A render has been queued. + Scheduled( + /// The idle token from a render being scheduled. + /// This is used to cancel renders if, for example, + /// the output being rendered is removed. + Idle<'static>, + ), + /// A frame was rendered and scheduled and we are waiting for vblank. + WaitingForVblank { + /// A render was scheduled while waiting for vblank. + /// In this case, another render will be scheduled once vblank happens. + dirty: bool, + }, +} + /// Render surface for an output. struct RenderSurface { /// The output global id. @@ -549,6 +593,7 @@ struct RenderSurface { /// The thing rendering elements and queueing frames. compositor: GbmDrmCompositor, dmabuf_feedback: Option, + render_state: RenderState, } impl Drop for RenderSurface { @@ -630,17 +675,22 @@ impl State { let registration_token = self .loop_handle - .insert_source( - notifier, - move |event, metadata, data: &mut CalloopData| match event { - DrmEvent::VBlank(crtc) => { - data.state.frame_finish(node, crtc, metadata); + .insert_source(notifier, move |event, metadata, data| match event { + DrmEvent::VBlank(crtc) => { + { + let udev = data.state.backend.udev_mut(); + let then = udev.last_vblank_time; + let now = Instant::now(); + let diff = now.duration_since(then); + tracing::debug!(time = diff.as_secs_f64(), "Time since last vblank"); + udev.last_vblank_time = now; } - DrmEvent::Error(error) => { - tracing::error!("{:?}", error); - } - }, - ) + data.state.on_vblank(node, crtc, metadata); + } + DrmEvent::Error(error) => { + tracing::error!("{:?}", error); + } + }) .expect("failed to insert drm notifier into event loop"); let render_node = EGLDevice::device_for_display( @@ -867,12 +917,11 @@ impl State { global: Some(global), compositor, dmabuf_feedback, + render_state: RenderState::Idle, }; device.surfaces.insert(crtc, surface); - self.schedule_initial_render(node, crtc, self.loop_handle.clone()); - // If there is saved connector state, the connector was previously plugged in. // In this case, restore its tags and location. // TODO: instead of checking the connector, check the monitor's edid info instead @@ -1025,7 +1074,7 @@ impl State { } /// Mark [`OutputPresentationFeedback`]s as presented and schedule a new render on idle. - fn frame_finish( + fn on_vblank( &mut self, dev_id: DrmNode, crtc: crtc::Handle, @@ -1052,7 +1101,7 @@ impl State { return; }; - let schedule_render = match surface + match surface .compositor .frame_submitted() .map_err(SwapBuffersError::from) @@ -1089,66 +1138,51 @@ impl State { flags, ); } - - true } Err(err) => { tracing::warn!("Error during rendering: {:?}", err); - match err { - SwapBuffersError::AlreadySwapped => true, - // If the device has been deactivated do not reschedule, this will be done - // by session resume - SwapBuffersError::TemporaryFailure(err) - if matches!( - err.downcast_ref::(), - Some(&DrmError::DeviceInactive) - ) => - { - false - } - SwapBuffersError::TemporaryFailure(err) => matches!( - err.downcast_ref::(), - Some(&DrmError::Access { - source: drm::SystemError::PermissionDenied, - .. - }) - ), - SwapBuffersError::ContextLost(err) => panic!("Rendering loop lost: {}", err), + if let SwapBuffersError::ContextLost(err) = err { + panic!("Rendering loop lost: {}", err) } } }; - if schedule_render { - // Anvil had some stuff here about delaying a render to reduce latency, - // but it introduces visible hitching when scrolling, so I'm removing it here. - // - // If latency is a problem then future me can deal with it :) - self.loop_handle.insert_idle(move |data| { - data.state.render_surface(&output); - }); + let RenderState::WaitingForVblank { dirty } = surface.render_state else { + unreachable!(); + }; + + surface.render_state = RenderState::Idle; + + if dirty { + self.schedule_render(&output); + } else { + for window in self.windows.iter() { + window.send_frame(&output, self.clock.now(), Some(Duration::ZERO), |_, _| { + Some(output.clone()) + }); + } } + + // if schedule_render { + // // Anvil had some stuff here about delaying a render to reduce latency, + // // but it introduces visible hitching when scrolling, so I'm removing it here. + // // + // // If latency is a problem then future me can deal with it :) + // self.loop_handle.insert_idle(move |data| { + // data.state.render_surface(&output); + // }); + // } } /// Render to the [`RenderSurface`] associated with the given `output`. fn render_surface(&mut self, output: &Output) { let udev = self.backend.udev_mut(); - let Some(UdevOutputData { - device_id, - crtc, - mode: _, - }) = output.user_data().get() - else { + let Some(surface) = render_surface_for_output(output, &mut udev.backends) else { return; }; - let Some(surface) = udev - .backends - .get_mut(device_id) - .and_then(|device| device.surfaces.get_mut(crtc)) - else { - return; - }; + assert!(matches!(surface.render_state, RenderState::Scheduled(_))); // TODO get scale from the rendersurface when supporting HiDPI let frame = udev.pointer_image.get_image( @@ -1231,114 +1265,91 @@ impl State { &self.clock, ); - let reschedule = match &result { - Ok(has_rendered) => !has_rendered, - Err(err) => { - tracing::warn!("Error during rendering: {:?}", err); - match err { - SwapBuffersError::AlreadySwapped => false, - SwapBuffersError::TemporaryFailure(err) => !matches!( - err.downcast_ref::(), - Some(&DrmError::DeviceInactive) - | Some(&DrmError::Access { - source: drm::SystemError::PermissionDenied, - .. - }) - ), - SwapBuffersError::ContextLost(err) => panic!("Rendering loop lost: {}", err), - } - } - }; - - if reschedule { - let Some(data) = output.user_data().get::() else { - unreachable!() - }; - - // Literally no idea if this refresh time calculation is doing anything, but we're - // gonna keep it here because I already added the stuff for it - let refresh_time = if let Some(mode) = data.mode { - self::utils::refresh_time(mode) - } else { - let output_refresh = match output.current_mode() { - Some(mode) => mode.refresh, - None => { - return; - } - }; - Duration::from_millis((1_000_000f32 / output_refresh as f32) as u64) - }; - - // If reschedule is true we either hit a temporary failure or more likely rendering - // did not cause any damage on the output. In this case we just re-schedule a repaint - // after approx. one frame to re-test for damage. - tracing::trace!( - "reschedule repaint timer with delay {:?} on {:?}", - refresh_time, - crtc, - ); - let timer = Timer::from_duration(refresh_time); - let output = output.clone(); - self.loop_handle - .insert_source(timer, move |_, _, data| { - data.state.render_surface(&output); - TimeoutAction::Drop - }) - .expect("failed to schedule frame timer"); + match result { + Ok(true) => surface.render_state = RenderState::WaitingForVblank { dirty: false }, + Ok(false) | Err(_) => surface.render_state = RenderState::Idle, } + + // let reschedule = match &result { + // Ok(has_rendered) => !has_rendered, + // Err(err) => { + // tracing::warn!("Error during rendering: {:?}", err); + // match err { + // SwapBuffersError::AlreadySwapped => false, + // SwapBuffersError::TemporaryFailure(err) => !matches!( + // err.downcast_ref::(), + // Some(&DrmError::DeviceInactive) + // | Some(&DrmError::Access { + // source: drm::SystemError::PermissionDenied, + // .. + // }) + // ), + // SwapBuffersError::ContextLost(err) => panic!("Rendering loop lost: {}", err), + // } + // } + // }; + // + // if reschedule { + // tracing::debug!("rescheduling due to no dmg or error"); + // let Some(data) = output.user_data().get::() else { + // unreachable!() + // }; + // + // // Literally no idea if this refresh time calculation is doing anything, but we're + // // gonna keep it here because I already added the stuff for it + // let refresh_time = if let Some(mode) = data.mode { + // self::utils::refresh_time(mode) + // } else { + // let output_refresh = match output.current_mode() { + // Some(mode) => mode.refresh, + // None => { + // return; + // } + // }; + // Duration::from_millis((1_000_000f32 / output_refresh as f32) as u64) + // }; + // + // // If reschedule is true we either hit a temporary failure or more likely rendering + // // did not cause any damage on the output. In this case we just re-schedule a repaint + // // after approx. one frame to re-test for damage. + // tracing::trace!( + // "reschedule repaint timer with delay {:?} on {}", + // refresh_time, + // output.name(), + // ); + // let timer = Timer::from_duration(refresh_time); + // let output = output.clone(); + // self.loop_handle + // .insert_source(timer, move |_, _, data| { + // data.state.render_surface(&output); + // TimeoutAction::Drop + // }) + // .expect("failed to schedule frame timer"); + // } } +} - /// Do an initial render that renders nothing to the screen. - /// - /// If that render failed, schedule another one. - fn schedule_initial_render( - &mut self, - node: DrmNode, - crtc: crtc::Handle, - evt_handle: LoopHandle<'static, CalloopData>, - ) { - let udev = self.backend.udev_mut(); +fn render_surface_for_output<'a>( + output: &Output, + backends: &'a mut HashMap, +) -> Option<&'a mut RenderSurface> { + let UdevOutputData { + device_id, + crtc, + mode: _, + } = output.user_data().get()?; - let Some(surface) = udev - .backends - .get_mut(&node) - .and_then(|device| device.surfaces.get_mut(&crtc)) - else { - return; - }; - - let node = surface.render_node; - let result = { - let mut renderer = udev - .gpu_manager - .single_renderer(&node) - .expect("failed to create MultiRenderer"); - initial_render(surface, &mut renderer) - }; - - if let Err(err) = result { - match err { - SwapBuffersError::AlreadySwapped => {} - SwapBuffersError::TemporaryFailure(err) => { - // TODO dont reschedule after 3(?) retries - tracing::warn!("Failed to submit page_flip: {}", err); - let handle = evt_handle.clone(); - evt_handle.insert_idle(move |data| { - data.state.schedule_initial_render(node, crtc, handle) - }); - } - SwapBuffersError::ContextLost(err) => panic!("Rendering loop lost: {}", err), - } - } - } + backends + .get_mut(device_id) + .and_then(|device| device.surfaces.get_mut(crtc)) } /// Render windows, layers, and everything else needed to the given [`RenderSurface`]. /// Also queues the frame for scanout. #[allow(clippy::too_many_arguments)] -fn render_surface<'a>( - surface: &'a mut RenderSurface, - renderer: &mut UdevRenderer<'a, '_>, +fn render_surface( + surface: &mut RenderSurface, + renderer: &mut UdevRenderer<'_, '_>, output: &Output, space: &Space, @@ -1354,6 +1365,8 @@ fn render_surface<'a>( clock: &Clock, ) -> Result { + use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; + let pending_wins = windows .iter() .filter(|win| win.alive()) @@ -1367,6 +1380,16 @@ fn render_surface<'a>( }; pending_size || win.with_state(|state| !state.loc_request_state.is_idle()) }) + .filter(|win| { + if let WindowElement::Wayland(win) = win { + !win.toplevel() + .current_state() + .states + .contains(xdg_toplevel::State::Resizing) + } else { + true + } + }) .map(|win| { ( win.class().unwrap_or("None".to_string()), @@ -1389,7 +1412,10 @@ fn render_surface<'a>( .queue_frame(None) .map_err(Into::::into)?; + tracing::debug!("queued no frame"); + // TODO: still draw the cursor here + surface.render_state = RenderState::WaitingForVblank { dirty: false }; return Ok(true); } @@ -1417,7 +1443,6 @@ fn render_surface<'a>( let time = clock.now(); - // Send frames to the cursor surface to get it to update correctly if let CursorImageStatus::Surface(surf) = cursor_status { send_frames_surface_tree(surf, output, time, Some(Duration::ZERO), |_, _| None); } @@ -1446,20 +1471,3 @@ fn render_surface<'a>( Ok(res.rendered) } - -/// Renders nothing to the given [`RenderSurface`]. -fn initial_render( - surface: &mut RenderSurface, - renderer: &mut UdevRenderer<'_, '_>, -) -> Result<(), SwapBuffersError> { - render_frame::<_, CustomRenderElements<_, WaylandSurfaceRenderElement<_>>, GlesTexture>( - &mut surface.compositor, - renderer, - &[], - [0.6, 0.6, 0.6, 1.0], - )?; - surface.compositor.queue_frame(None)?; - surface.compositor.reset_buffers(); - - Ok(()) -} diff --git a/src/backend/udev/utils.rs b/src/backend/udev/utils.rs deleted file mode 100644 index 3a93076..0000000 --- a/src/backend/udev/utils.rs +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -use std::time::Duration; - -use smithay::reexports::drm::control::{Mode, ModeFlags}; - -// From niri: -// https://github.com/YaLTeR/niri/blob/ba0a6d6b8868cc6348ad1b20f683a95d5909df6b/src/backend/tty.rs#L900-L922 -pub fn refresh_time(mode: Mode) -> Duration { - let clock = mode.clock() as u64; - let htotal = mode.hsync().2 as u64; - let vtotal = mode.vsync().2 as u64; - - let mut numerator = htotal * vtotal * 1_000_000; - let mut denominator = clock; - - if mode.flags().contains(ModeFlags::INTERLACE) { - denominator *= 2; - } - - if mode.flags().contains(ModeFlags::DBLSCAN) { - numerator *= 2; - } - - if mode.vscan() > 1 { - numerator *= mode.vscan() as u64; - } - - let refresh_interval = (numerator + denominator / 2) / denominator; - Duration::from_nanos(refresh_interval) -} diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 0e82e85..f211701 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -2,6 +2,7 @@ use std::{ffi::OsString, time::Duration}; +use calloop::LoopHandle; use smithay::{ backend::{ egl::EGLDevice, @@ -43,6 +44,7 @@ pub struct Winit { pub damage_tracker: OutputDamageTracker, pub dmabuf_state: (DmabufState, DmabufGlobal, Option), pub full_redraw: u8, + render_state: RenderState, } impl BackendData for Winit { @@ -57,6 +59,18 @@ impl BackendData for Winit { fn early_import(&mut self, _surface: &WlSurface) {} } +impl Backend { + fn winit(&self) -> &Winit { + let Backend::Winit(winit) = self else { unreachable!() }; + winit + } + + fn winit_mut(&mut self) -> &mut Winit { + let Backend::Winit(winit) = self else { unreachable!() }; + winit + } +} + /// Start Pinnacle as a window in a graphical environment. pub fn run_winit() -> anyhow::Result<()> { let mut event_loop: EventLoop = EventLoop::try_new()?; @@ -155,6 +169,7 @@ pub fn run_winit() -> anyhow::Result<()> { damage_tracker: OutputDamageTracker::from_output(&output), dmabuf_state, full_redraw: 0, + render_state: RenderState::Idle, }), display, event_loop.get_signal(), @@ -163,13 +178,13 @@ pub fn run_winit() -> anyhow::Result<()> { state.focus_state.focused_output = Some(output.clone()); - let Backend::Winit(backend) = &mut state.backend else { - unreachable!() - }; + let winit = state.backend.winit_mut(); + + winit.backend.window().set_title("Pinnacle"); state .shm_state - .update_formats(backend.backend.renderer().shm_formats()); + .update_formats(winit.backend.renderer().shm_formats()); state.space.map_output(&output, (0, 0)); @@ -183,13 +198,10 @@ pub fn run_winit() -> anyhow::Result<()> { tracing::error!("Failed to start XWayland: {err}"); } - let mut pointer_element = PointerElement::::new(); - let insert_ret = state .loop_handle .insert_source(Timer::immediate(), move |_instant, _metadata, data| { - let display_handle = &mut data.display_handle; let state = &mut data.state; let result = winit_evt_loop.dispatch_new_events(|event| match event { @@ -214,7 +226,9 @@ pub fn run_winit() -> anyhow::Result<()> { WinitEvent::Input(input_evt) => { state.process_input_event(input_evt); } - WinitEvent::Refresh => {} + WinitEvent::Refresh => { + state.schedule_render(&output); + } }); match result { @@ -224,186 +238,216 @@ pub fn run_winit() -> anyhow::Result<()> { } }; - if let CursorImageStatus::Surface(surface) = &state.cursor_status { - if !surface.alive() { - state.cursor_status = CursorImageStatus::default_named(); - } - } - - let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_)); - - pointer_element.set_status(state.cursor_status.clone()); - - let pending_wins = state - .windows - .iter() - .filter(|win| win.alive()) - .filter(|win| { - let pending_size = if let WindowElement::Wayland(win) = win { - let current_state = win.toplevel().current_state(); - win.toplevel() - .with_pending_state(|state| state.size != current_state.size) - } else { - false - }; - pending_size || win.with_state(|state| !state.loc_request_state.is_idle()) - }) - .map(|win| { - ( - win.class().unwrap_or("None".to_string()), - win.title().unwrap_or("None".to_string()), - win.with_state(|state| state.loc_request_state.clone()), - ) - }) - .collect::>(); - - if !pending_wins.is_empty() { - // tracing::debug!("Skipping frame, waiting on {pending_wins:?}"); - let op_clone = output.clone(); - state.loop_handle.insert_idle(move |dt| { - for win in dt.state.windows.iter() { - win.send_frame( - &op_clone, - dt.state.clock.now(), - Some(Duration::ZERO), - surface_primary_scanout_output, - ); - } - }); - - state.space.refresh(); - state.popup_manager.cleanup(); - display_handle - .flush_clients() - .expect("failed to flush client buffers"); - - // TODO: still draw the cursor here - - return TimeoutAction::ToDuration(Duration::from_millis(1)); - } - - let Backend::Winit(backend) = &mut state.backend else { - unreachable!() - }; - let full_redraw = &mut backend.full_redraw; - *full_redraw = full_redraw.saturating_sub(1); - - state.focus_state.fix_up_focus(&mut state.space); - - let output_render_elements = crate::render::generate_render_elements( - &output, - backend.backend.renderer(), - &state.space, - &state.focus_state.focus_stack, - &state.override_redirect_windows, - state.pointer_location, - &mut state.cursor_status, - state.dnd_icon.as_ref(), - // state.seat.input_method(), - &mut pointer_element, - None, - ); - - let render_res = backend.backend.bind().and_then(|_| { - let age = if *full_redraw > 0 { - 0 - } else { - backend.backend.buffer_age().unwrap_or(0) - }; - - let renderer = backend.backend.renderer(); - - backend - .damage_tracker - .render_output(renderer, age, &output_render_elements, [0.6, 0.6, 0.6, 1.0]) - .map_err(|err| match err { - damage::Error::Rendering(err) => err.into(), - damage::Error::OutputNoMode(_) => todo!(), - }) - }); - - match render_res { - Ok(render_output_result) => { - let has_rendered = render_output_result.damage.is_some(); - if let Some(damage) = render_output_result.damage { - // tracing::debug!("damage rects are {damage:?}"); - if let Err(err) = backend.backend.submit(Some(&damage)) { - tracing::warn!("{}", err); - } - } - - backend.backend.window().set_cursor_visible(cursor_visible); - - let time = state.clock.now(); - - // Send frames to the cursor surface so it updates correctly - if let CursorImageStatus::Surface(surf) = &state.cursor_status { - if let Some(op) = state.focus_state.focused_output.as_ref() { - send_frames_surface_tree( - surf, - op, - time, - Some(Duration::ZERO), - |_, _| None, - ); - } - } - - super::post_repaint( - &output, - &render_output_result.states, - &state.space, - None, - time.into(), - ); - - if has_rendered { - let mut output_presentation_feedback = take_presentation_feedback( - &output, - &state.space, - &render_output_result.states, - ); - output_presentation_feedback.presented( - time, - output - .current_mode() - .map(|mode| { - Duration::from_secs_f64(1000f64 / mode.refresh as f64) - }) - .unwrap_or_default(), - 0, - wp_presentation_feedback::Kind::Vsync, - ); - } - } - Err(err) => { - tracing::warn!("{}", err); - } - } - - state.space.refresh(); - state.popup_manager.cleanup(); - display_handle - .flush_clients() - .expect("failed to flush client buffers"); - TimeoutAction::ToDuration(Duration::from_micros(((1.0 / 144.0) * 1000000.0) as u64)) }); - if let Err(err) = insert_ret { anyhow::bail!("Failed to insert winit events into event loop: {err}"); } + let frame_time = Duration::from_micros(((1.0 / 144.0) * 1000000.0) as u64); + let refresh_timer = Timer::from_duration(frame_time); + state + .loop_handle + .insert_source(refresh_timer, move |instant, _, data| { + let winit = data.state.backend.winit(); + + winit.backend.window().request_redraw(); + + let frame_time = winit + .backend + .window() + .current_monitor() + .and_then(|monitor| monitor.refresh_rate_millihertz()) + .map(|rate| Duration::from_secs_f64(1000.0 / rate as f64)) + .unwrap_or(frame_time); + + TimeoutAction::ToInstant(instant + frame_time) + }) + .expect("failed to insert render timer into event loop"); + event_loop.run( Some(Duration::from_micros(((1.0 / 144.0) * 1000000.0) as u64)), &mut CalloopData { display_handle, state, }, - |_data| { - // println!("{}", _data.state.space.elements().count()); + |data| { + data.state.space.refresh(); + data.state.popup_manager.cleanup(); + data.display_handle + .flush_clients() + .expect("failed to flush client buffers"); }, )?; Ok(()) } + +enum RenderState { + Idle, + Scheduled, +} + +impl Winit { + pub fn schedule_render(&mut self, loop_handle: &LoopHandle, output: &Output) { + match &self.render_state { + RenderState::Idle => { + let output = output.clone(); + loop_handle.insert_idle(move |data| data.state.render_window(&output)); + + self.render_state = RenderState::Scheduled; + } + RenderState::Scheduled => (), + } + } +} + +impl State { + fn render_window(&mut self, output: &Output) { + let winit = self.backend.winit_mut(); + + assert!(matches!(winit.render_state, RenderState::Scheduled)); + + let pending_wins = self + .windows + .iter() + .filter(|win| win.alive()) + .filter(|win| { + let pending_size = if let WindowElement::Wayland(win) = win { + let current_state = win.toplevel().current_state(); + win.toplevel() + .with_pending_state(|state| state.size != current_state.size) + } else { + false + }; + pending_size || win.with_state(|state| !state.loc_request_state.is_idle()) + }) + .map(|win| { + ( + win.class().unwrap_or("None".to_string()), + win.title().unwrap_or("None".to_string()), + win.with_state(|state| state.loc_request_state.clone()), + ) + }) + .collect::>(); + + if !pending_wins.is_empty() { + // tracing::debug!("Skipping frame, waiting on {pending_wins:?}"); + let op_clone = output.clone(); + self.loop_handle.insert_idle(move |dt| { + for win in dt.state.windows.iter() { + win.send_frame( + &op_clone, + dt.state.clock.now(), + Some(Duration::ZERO), + surface_primary_scanout_output, + ); + } + }); + + // TODO: still draw the cursor here + + winit.render_state = RenderState::Idle; + return; + } + let full_redraw = &mut winit.full_redraw; + *full_redraw = full_redraw.saturating_sub(1); + + self.focus_state.fix_up_focus(&mut self.space); + + if let CursorImageStatus::Surface(surface) = &self.cursor_status { + if !surface.alive() { + self.cursor_status = CursorImageStatus::default_named(); + } + } + + let cursor_visible = !matches!(self.cursor_status, CursorImageStatus::Surface(_)); + + let mut pointer_element = PointerElement::::new(); + pointer_element.set_status(self.cursor_status.clone()); + + let output_render_elements = crate::render::generate_render_elements( + output, + winit.backend.renderer(), + &self.space, + &self.focus_state.focus_stack, + &self.override_redirect_windows, + self.pointer_location, + &mut self.cursor_status, + self.dnd_icon.as_ref(), + // self.seat.input_method(), + &mut pointer_element, + None, + ); + + let render_res = winit.backend.bind().and_then(|_| { + let age = if *full_redraw > 0 { + 0 + } else { + winit.backend.buffer_age().unwrap_or(0) + }; + + let renderer = winit.backend.renderer(); + + winit + .damage_tracker + .render_output(renderer, age, &output_render_elements, [0.6, 0.6, 0.6, 1.0]) + .map_err(|err| match err { + damage::Error::Rendering(err) => err.into(), + damage::Error::OutputNoMode(_) => todo!(), + }) + }); + + match render_res { + Ok(render_output_result) => { + let has_rendered = render_output_result.damage.is_some(); + if let Some(damage) = render_output_result.damage { + // tracing::debug!("damage rects are {damage:?}"); + if let Err(err) = winit.backend.submit(Some(&damage)) { + tracing::warn!("{}", err); + } + } + + winit.backend.window().set_cursor_visible(cursor_visible); + + let time = self.clock.now(); + + // Send frames to the cursor surface so it updates correctly + if let CursorImageStatus::Surface(surf) = &self.cursor_status { + if let Some(op) = self.focus_state.focused_output.as_ref() { + send_frames_surface_tree(surf, op, time, Some(Duration::ZERO), |_, _| None); + } + } + + super::post_repaint( + output, + &render_output_result.states, + &self.space, + None, + time.into(), + ); + + if has_rendered { + let mut output_presentation_feedback = take_presentation_feedback( + output, + &self.space, + &render_output_result.states, + ); + output_presentation_feedback.presented( + time, + output + .current_mode() + .map(|mode| Duration::from_secs_f64(1000f64 / mode.refresh as f64)) + .unwrap_or_default(), + 0, + wp_presentation_feedback::Kind::Vsync, + ); + } + } + Err(err) => { + tracing::warn!("{}", err); + } + } + winit.render_state = RenderState::Idle; + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 5bcb42e..53c1c62 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -10,7 +10,7 @@ use smithay::{ delegate_compositor, delegate_data_device, delegate_fractional_scale, delegate_layer_shell, delegate_output, delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat, delegate_shm, delegate_viewporter, - desktop::{self, layer_map_for_output, PopupKind, WindowSurfaceType}, + desktop::{self, find_popup_root_surface, layer_map_for_output, PopupKind, WindowSurfaceType}, input::{pointer::CursorImageStatus, Seat, SeatHandler, SeatState}, output::Output, reexports::{ @@ -105,11 +105,12 @@ impl CompositorHandler for State { utils::on_commit_buffer_handler::(surface); self.backend.early_import(surface); + let mut root = surface.clone(); + while let Some(parent) = compositor::get_parent(&root) { + root = parent; + } + if !compositor::is_sync_subsurface(surface) { - let mut root = surface.clone(); - while let Some(parent) = compositor::get_parent(&root) { - root = parent; - } if let Some(win @ WindowElement::Wayland(window)) = &self.window_for_surface(&root) { // tracing::debug!("window commit thing {:?}", win.class()); window.on_commit(); @@ -129,9 +130,41 @@ impl CompositorHandler for State { crate::grab::resize_grab::handle_commit(self, surface); - // if let Some(window) = self.window_for_surface(surface) { - // tracing::debug!("commit on window {:?}", window.class()); - // } + // `surface` is a root window + let Some(output) = self + .window_for_surface(surface) + .and_then(|win| win.output(self)) + .or_else(|| { + // `surface` is a descendant of a root window + self.window_for_surface(&root) + .and_then(|win| win.output(self)) + }) + .or_else(|| { + // `surface` is a popup + self.popup_manager + .find_popup(surface) + .and_then(|popup| find_popup_root_surface(&popup).ok()) + .and_then(|surf| self.window_for_surface(&surf)) + .and_then(|win| win.output(self)) + }) + .or_else(|| { + // `surface` is a layer surface + self.space + .outputs() + .find(|op| { + let layer_map = layer_map_for_output(op); + layer_map + .layer_for_surface(surface, WindowSurfaceType::ALL) + .is_some() + }) + .cloned() + }) + // TODO: cursor surface and dnd icon + else { + return; + }; + + self.schedule_render(&output); } fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 6d7398d..99a425c 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use smithay::{ delegate_xdg_shell, desktop::{ @@ -150,6 +148,8 @@ impl XdgShellHandler for State { .get_keyboard() .expect("Seat had no keyboard") .set_focus(self, focus, SERIAL_COUNTER.next_serial()); + + self.schedule_render(&output); } } @@ -749,14 +749,6 @@ impl XdgShellHandler for State { state.loc_request_state = LocationRequestState::Acknowledged(new_loc); }); - if let Some(op) = window.output(self) { - window.send_frame( - &op, - self.clock.now(), - Some(Duration::ZERO), - |_, _| Some(op.clone()), - ); - } } } Configure::Popup(_) => todo!(), diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index d991e82..2228ef8 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -218,6 +218,8 @@ impl XwmHandler for CalloopData { .get_keyboard() .expect("Seat had no keyboard") .set_focus(&mut self.state, focus, SERIAL_COUNTER.next_serial()); + + self.state.schedule_render(&output); } } if !window.is_override_redirect() { @@ -267,6 +269,8 @@ impl XwmHandler for CalloopData { .get_keyboard() .expect("Seat had no keyboard") .set_focus(&mut self.state, focus, SERIAL_COUNTER.next_serial()); + + self.state.schedule_render(&output); } } tracing::debug!("destroyed x11 window"); diff --git a/src/input.rs b/src/input.rs index b2c7f9a..e677d10 100644 --- a/src/input.rs +++ b/src/input.rs @@ -657,8 +657,8 @@ impl State { let surface_under = self.surface_under(self.pointer_location); // tracing::info!("{:?}", self.pointer_location); - if let Some(ptr) = self.seat.get_pointer() { - ptr.motion( + if let Some(pointer) = self.seat.get_pointer() { + pointer.motion( self, surface_under.clone(), &MotionEvent { @@ -668,7 +668,7 @@ impl State { }, ); - ptr.relative_motion( + pointer.relative_motion( self, surface_under, &RelativeMotionEvent { @@ -678,7 +678,15 @@ impl State { }, ); - ptr.frame(self); + pointer.frame(self); + + self.schedule_render( + &self + .focus_state + .focused_output + .clone() + .expect("no focused output"), + ); } } } diff --git a/src/render.rs b/src/render.rs index c44e4f1..5508800 100644 --- a/src/render.rs +++ b/src/render.rs @@ -31,7 +31,11 @@ use smithay::{ xwayland::X11Surface, }; -use crate::{state::WithState, window::WindowElement}; +use crate::{ + backend::Backend, + state::{State, WithState}, + window::WindowElement, +}; use self::pointer::{PointerElement, PointerRenderElement}; @@ -400,3 +404,12 @@ pub fn take_presentation_feedback( output_presentation_feedback } + +impl State { + pub fn schedule_render(&mut self, output: &Output) { + match &mut self.backend { + Backend::Winit(winit) => winit.schedule_render(&self.loop_handle, output), + Backend::Udev(udev) => udev.schedule_render(&self.loop_handle, output), + } + } +} diff --git a/src/render/pointer.rs b/src/render/pointer.rs index b8b6c07..985a9ff 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -10,7 +10,7 @@ use smithay::{ }, ImportAll, Renderer, Texture, }, - input::pointer::{CursorIcon, CursorImageStatus}, + input::pointer::CursorImageStatus, render_elements, utils::{Physical, Point, Scale}, }; diff --git a/src/state/api_handlers.rs b/src/state/api_handlers.rs index d2f1f77..9954d2f 100644 --- a/src/state/api_handlers.rs +++ b/src/state/api_handlers.rs @@ -147,6 +147,12 @@ impl State { let Some(output) = window.output(self) else { return }; self.update_windows(&output); + + // Sometimes toggling won't change the window size, + // causing no commit. + // + // Schedule a render in case the window moves. + self.schedule_render(&output); } Msg::ToggleFullscreen { window_id } => { let Some(window) = window_id.window(self) else { return };