diff --git a/deploy-notes.md b/deploy-notes.md index 43046e41..e7bd84a1 100644 --- a/deploy-notes.md +++ b/deploy-notes.md @@ -1,5 +1,7 @@ # Unreleased +- Needs jay-config release. +- Needs jay-toml-config release. - Needs jay-compositor release. # 1.4.0 diff --git a/docs/features.md b/docs/features.md index 4a4660e6..7b0fb94e 100644 --- a/docs/features.md +++ b/docs/features.md @@ -120,7 +120,11 @@ Jay's shortcut system allows you to execute an action when a key is pressed and ## VR -Jay's supports leasing VR headsets to applications. +Jay supports leasing VR headsets to applications. + +## Adaptive Sync + +Jay supports adaptive sync with configurable cursor refresh rates. ## Protocol Support diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 64ef421f..a4faee8e 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -25,7 +25,7 @@ use { timer::Timer, video::{ connector_type::{ConnectorType, CON_UNKNOWN}, - Connector, DrmDevice, GfxApi, Mode, Transform, + Connector, DrmDevice, GfxApi, Mode, Transform, VrrMode, }, Axis, Direction, ModifiedKeySym, PciId, Workspace, }, @@ -800,6 +800,14 @@ impl Client { (width, height) } + pub fn set_vrr_mode(&self, connector: Option, mode: VrrMode) { + self.send(&ClientMessage::SetVrrMode { connector, mode }) + } + + pub fn set_vrr_cursor_hz(&self, connector: Option, hz: f64) { + self.send(&ClientMessage::SetVrrCursorHz { connector, hz }) + } + pub fn drm_devices(&self) -> Vec { let res = self.send_with_response(&ClientMessage::GetDrmDevices); get_response!(res, vec![], GetDrmDevices { devices }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 69d2fd09..1cec98ab 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -8,7 +8,7 @@ use { logging::LogLevel, theme::{colors::Colorable, sized::Resizable, Color}, timer::Timer, - video::{connector_type::ConnectorType, Connector, DrmDevice, GfxApi, Transform}, + video::{connector_type::ConnectorType, Connector, DrmDevice, GfxApi, Transform, VrrMode}, Axis, Direction, PciId, Workspace, _private::{PollableId, WireMode}, }, @@ -487,6 +487,14 @@ pub enum ClientMessage<'a> { seat: Seat, enabled: bool, }, + SetVrrMode { + connector: Option, + mode: VrrMode, + }, + SetVrrCursorHz { + connector: Option, + hz: f64, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index 00fa2b8c..55859ab1 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -248,6 +248,20 @@ impl Connector { } get!(String::new()).connector_get_serial_number(self) } + + /// Sets the VRR mode. + pub fn set_vrr_mode(self, mode: VrrMode) { + get!().set_vrr_mode(Some(self), mode) + } + + /// Sets the VRR cursor refresh rate. + /// + /// Limits the rate at which cursors are updated on screen when VRR is active. + /// + /// Setting this to infinity disables the limiter. + pub fn set_vrr_cursor_hz(self, hz: f64) { + get!().set_vrr_cursor_hz(Some(self), hz) + } } /// Returns all available DRM devices. @@ -531,3 +545,38 @@ pub enum Transform { /// Flip around the vertical axis, then rotate 270 degrees counter-clockwise. FlipRotate270, } + +/// The VRR mode of a connector. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash, Default)] +pub struct VrrMode(pub u32); + +impl VrrMode { + /// VRR is never enabled. + pub const NEVER: Self = Self(0); + /// VRR is always enabled. + pub const ALWAYS: Self = Self(1); + /// VRR is enabled when one or more applications are displayed fullscreen. + pub const VARIANT_1: Self = Self(2); + /// VRR is enabled when a single application is displayed fullscreen. + pub const VARIANT_2: Self = Self(3); + /// VRR is enabled when a single game or video is displayed fullscreen. + pub const VARIANT_3: Self = Self(4); +} + +/// Sets the default VRR mode. +/// +/// This setting can be overwritten on a per-connector basis with [Connector::set_vrr_mode]. +pub fn set_vrr_mode(mode: VrrMode) { + get!().set_vrr_mode(None, mode) +} + +/// Sets the VRR cursor refresh rate. +/// +/// Limits the rate at which cursors are updated on screen when VRR is active. +/// +/// Setting this to infinity disables the limiter. +/// +/// This setting can be overwritten on a per-connector basis with [Connector::set_vrr_cursor_hz]. +pub fn set_vrr_cursor_hz(hz: f64) { + get!().set_vrr_cursor_hz(None, hz) +} diff --git a/release-notes.md b/release-notes.md index ec924a06..e2187e4c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,6 +1,7 @@ # Unreleased - Add fine-grained damage tracking. +- Add support for adaptive sync. # 1.4.0 (2024-07-07) diff --git a/src/backend.rs b/src/backend.rs index 421fcffe..3a54fa08 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -71,6 +71,7 @@ pub struct MonitorInfo { pub width_mm: i32, pub height_mm: i32, pub non_desktop: bool, + pub vrr_capable: bool, } #[derive(Copy, Clone, Debug)] @@ -108,6 +109,9 @@ pub trait Connector { fn drm_object_id(&self) -> Option { None } + fn set_vrr_enabled(&self, enabled: bool) { + let _ = enabled; + } } #[derive(Debug)] @@ -119,6 +123,7 @@ pub enum ConnectorEvent { ModeChanged(Mode), Unavailable, Available, + VrrChanged(bool), } pub trait HardwareCursor: Debug { @@ -127,7 +132,8 @@ pub trait HardwareCursor: Debug { fn set_position(&self, x: i32, y: i32); fn swap_buffer(&self); fn set_sync_file(&self, sync_file: Option); - fn commit(&self); + fn commit(&self, schedule_present: bool); + fn schedule_present(&self) -> bool; fn size(&self) -> (i32, i32); } diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index 8c35bf4f..e0471a11 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -95,6 +95,7 @@ pub struct MetalDrmDevice { pub on_change: OnChange, pub direct_scanout_enabled: Cell>, pub is_nvidia: bool, + pub is_amd: bool, pub lease_ids: MetalLeaseIds, pub leases: CopyHashMap, pub leases_to_break: CopyHashMap, @@ -299,6 +300,8 @@ pub struct ConnectorDisplayData { pub refresh: u32, pub non_desktop: bool, pub non_desktop_effective: bool, + pub vrr_capable: bool, + pub vrr_requested: bool, pub monitor_manufacturer: String, pub monitor_name: String, @@ -319,6 +322,10 @@ impl ConnectorDisplayData { && self.monitor_name == other.monitor_name && self.monitor_serial_number == other.monitor_serial_number } + + fn should_enable_vrr(&self) -> bool { + self.vrr_requested && self.vrr_capable + } } linear_ids!(MetalLeaseIds, MetalLeaseId, u64); @@ -417,6 +424,7 @@ pub struct MetalConnector { pub can_present: Cell, pub has_damage: Cell, pub cursor_changed: Cell, + pub cursor_scheduled: Cell, pub next_flip_nsec: Cell, pub display: RefCell, @@ -503,7 +511,7 @@ impl HardwareCursor for MetalHardwareCursor { self.have_changes.set(true); } - fn commit(&self) { + fn commit(&self, schedule_present: bool) { if self.generation != self.connector.cursor_generation.get() { return; } @@ -520,8 +528,20 @@ impl HardwareCursor for MetalHardwareCursor { } self.connector.cursor_sync_file.set(self.sync_file.take()); self.connector.cursor_changed.set(true); - if self.connector.can_present.get() { - self.connector.schedule_present(); + if schedule_present { + self.schedule_present(); + } + } + + fn schedule_present(&self) -> bool { + if self.connector.cursor_changed.get() { + self.connector.cursor_scheduled.set(true); + if self.connector.can_present.get() { + self.connector.schedule_present(); + } + true + } else { + false } } @@ -604,6 +624,19 @@ impl MetalConnector { } } + fn send_vrr_enabled(&self) { + match self.frontend_state.get() { + FrontState::Removed + | FrontState::Disconnected + | FrontState::Unavailable + | FrontState::Connected { non_desktop: true } => return, + FrontState::Connected { non_desktop: false } => {} + } + if let Some(crtc) = self.crtc.get() { + self.send_event(ConnectorEvent::VrrChanged(crtc.vrr_enabled.value.get())); + } + } + fn send_hardware_cursor(self: &Rc) { match self.frontend_state.get() { FrontState::Removed @@ -894,7 +927,7 @@ impl MetalConnector { Some(crtc) => crtc, _ => return Ok(()), }; - if (!self.has_damage.get() && !self.cursor_changed.get()) || !self.can_present.get() { + if (!self.has_damage.get() && !self.cursor_scheduled.get()) || !self.can_present.get() { return Ok(()); } if !crtc.active.value.get() { @@ -908,6 +941,9 @@ impl MetalConnector { Some(b) => b, _ => return Ok(()), }; + let Some(node) = self.state.root.outputs.get(&self.connector_id) else { + return Ok(()); + }; let cursor = self.cursor_plane.get(); let mut new_fb = None; let mut changes = self.master.change(); @@ -915,46 +951,52 @@ impl MetalConnector { if !self.backend.check_render_context(&self.dev) { return Ok(()); } - if let Some(node) = self.state.root.outputs.get(&self.connector_id) { - let buffer = &buffers[self.next_buffer.get() % buffers.len()]; - let mut rr = self.render_result.borrow_mut(); - rr.output_id = node.id; - let fb = - self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?; - rr.dispatch_frame_requests(self.state.now_msec()); - let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) = - match &fb.direct_scanout_data { - None => { - let plane_w = plane.mode_w.get(); - let plane_h = plane.mode_h.get(); - (0, 0, plane_w, plane_h, plane_w, plane_h) - } - Some(dsd) => { - let p = &dsd.position; - ( - p.crtc_x, - p.crtc_y, - p.crtc_width, - p.crtc_height, - p.src_width, - p.src_height, - ) - } - }; - let in_fence = fb.sync_file.as_ref().map(|s| s.raw()).unwrap_or(-1); - changes.change_object(plane.id, |c| { - c.change(plane.fb_id, fb.fb.id().0 as _); - c.change(plane.src_w.id, (src_width as u64) << 16); - c.change(plane.src_h.id, (src_height as u64) << 16); - c.change(plane.crtc_x.id, crtc_x as u64); - c.change(plane.crtc_y.id, crtc_y as u64); - c.change(plane.crtc_w.id, crtc_w as u64); - c.change(plane.crtc_h.id, crtc_h as u64); - if !self.dev.is_nvidia { - c.change(plane.in_fence_fd, in_fence as u64); + let buffer = &buffers[self.next_buffer.get() % buffers.len()]; + let mut rr = self.render_result.borrow_mut(); + rr.output_id = node.id; + let fb = self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?; + rr.dispatch_frame_requests(self.state.now_msec()); + let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) = + match &fb.direct_scanout_data { + None => { + let plane_w = plane.mode_w.get(); + let plane_h = plane.mode_h.get(); + (0, 0, plane_w, plane_h, plane_w, plane_h) } - }); - new_fb = Some(fb); + Some(dsd) => { + let p = &dsd.position; + ( + p.crtc_x, + p.crtc_y, + p.crtc_width, + p.crtc_height, + p.src_width, + p.src_height, + ) + } + }; + let in_fence = fb.sync_file.as_ref().map(|s| s.raw()).unwrap_or(-1); + changes.change_object(plane.id, |c| { + c.change(plane.fb_id, fb.fb.id().0 as _); + c.change(plane.src_w.id, (src_width as u64) << 16); + c.change(plane.src_h.id, (src_height as u64) << 16); + c.change(plane.crtc_x.id, crtc_x as u64); + c.change(plane.crtc_y.id, crtc_y as u64); + c.change(plane.crtc_w.id, crtc_w as u64); + c.change(plane.crtc_h.id, crtc_h as u64); + if !self.dev.is_nvidia { + c.change(plane.in_fence_fd, in_fence as u64); + } + }); + new_fb = Some(fb); + } else { + if self.dev.is_amd && crtc.vrr_enabled.value.get() { + // Work around https://gitlab.freedesktop.org/drm/amd/-/issues/2186 + if let Some(fb) = &*self.active_framebuffer.borrow() { + changes.change_object(plane.id, |c| { + c.change(plane.fb_id, fb.fb.id().0 as _); + }); + } } } let mut cursor_swap_buffer = false; @@ -1027,7 +1069,8 @@ impl MetalConnector { .discard_presentation_feedback(); Err(MetalError::Commit(e)) } else { - self.perform_screencopies(&new_fb); + node.schedule.presented(); + self.perform_screencopies(&new_fb, &node); if let Some(fb) = new_fb { if fb.direct_scanout_data.is_none() { self.next_buffer.fetch_add(1); @@ -1042,14 +1085,12 @@ impl MetalConnector { self.can_present.set(false); self.has_damage.set(false); self.cursor_changed.set(false); + self.cursor_scheduled.set(false); Ok(()) } } - fn perform_screencopies(&self, new_fb: &Option) { - let Some(output) = self.state.root.outputs.get(&self.connector_id) else { - return; - }; + fn perform_screencopies(&self, new_fb: &Option, output: &OutputNode) { let active_fb; let fb = match &new_fb { Some(f) => f, @@ -1173,6 +1214,17 @@ impl MetalConnector { log::error!("Tried to send available event in invalid state: {state:?}"); } }, + ConnectorEvent::VrrChanged(_) => match state { + FrontState::Connected { non_desktop: false } => { + self.on_change.send_event(event); + } + FrontState::Connected { non_desktop: true } + | FrontState::Removed + | FrontState::Disconnected + | FrontState::Unavailable => { + log::error!("Tried to send vrr-changed event in invalid state: {state:?}"); + } + }, } } } @@ -1296,6 +1348,32 @@ impl Connector for MetalConnector { fn drm_object_id(&self) -> Option { Some(self.id) } + + fn set_vrr_enabled(&self, enabled: bool) { + if self.frontend_state.get() != (FrontState::Connected { non_desktop: false }) { + return; + } + let dd = &mut *self.display.borrow_mut(); + let old_enabled = dd.should_enable_vrr(); + dd.vrr_requested = enabled; + let new_enabled = dd.should_enable_vrr(); + if old_enabled == new_enabled { + return; + } + let Some(crtc) = self.crtc.get() else { + return; + }; + let mut change = self.master.change(); + change.change_object(crtc.id, |c| { + c.change(crtc.vrr_enabled.id, new_enabled as _); + }); + if let Err(e) = change.commit(0, 0) { + log::error!("Could not change vrr mode: {}", ErrorFmt(e)); + return; + } + crtc.vrr_enabled.value.set(new_enabled); + self.send_vrr_enabled(); + } } pub struct MetalCrtc { @@ -1312,6 +1390,7 @@ pub struct MetalCrtc { pub active: MutableProperty, pub mode_id: MutableProperty, pub out_fence_ptr: DrmProperty, + pub vrr_enabled: MutableProperty, pub mode_blob: CloneCell>>, } @@ -1435,6 +1514,7 @@ fn create_connector( display: RefCell::new(display), frontend_state: Cell::new(FrontState::Disconnected), cursor_changed: Cell::new(false), + cursor_scheduled: Cell::new(false), cursor_front_buffer: Default::default(), cursor_swap_buffer: Cell::new(false), cursor_sync_file: Default::default(), @@ -1545,6 +1625,10 @@ fn create_connector_display_data( let props = collect_properties(&dev.master, connector)?; let connector_type = ConnectorType::from_drm(info.connector_type); let non_desktop = props.get("non-desktop")?.value.get() != 0; + let vrr_capable = match props.get("vrr_capable") { + Ok(c) => c.value.get() == 1, + Err(_) => false, + }; Ok(ConnectorDisplayData { crtc_id: props.get("CRTC_ID")?.map(|v| DrmCrtc(v as _)), crtcs, @@ -1553,6 +1637,8 @@ fn create_connector_display_data( refresh, non_desktop, non_desktop_effective: non_desktop_override.unwrap_or(non_desktop), + vrr_capable, + vrr_requested: false, monitor_manufacturer: manufacturer, monitor_name: name, monitor_serial_number: serial_number, @@ -1607,6 +1693,7 @@ fn create_crtc( active: props.get("ACTIVE")?.map(|v| v == 1), mode_id: props.get("MODE_ID")?.map(|v| DrmBlob(v as u32)), out_fence_ptr: props.get("OUT_FENCE_PTR")?.id, + vrr_enabled: props.get("VRR_ENABLED")?.map(|v| v == 1), mode_blob: Default::default(), }) } @@ -1876,6 +1963,7 @@ impl MetalBackend { dd.mode = Some(mode.clone()); } } + dd.vrr_requested = old.vrr_requested; } mem::swap(old.deref_mut(), &mut dd); match c.frontend_state.get() { @@ -1963,8 +2051,10 @@ impl MetalBackend { width_mm: dd.mm_width as _, height_mm: dd.mm_height as _, non_desktop: dd.non_desktop_effective, + vrr_capable: dd.vrr_capable, })); connector.send_hardware_cursor(); + connector.send_vrr_enabled(); } pub fn create_drm_device( @@ -2030,9 +2120,11 @@ impl MetalBackend { }; let mut is_nvidia = false; + let mut is_amd = false; match gbm.drm.version() { Ok(v) => { is_nvidia = v.name.contains_str("nvidia"); + is_amd = v.name.contains_str("amdgpu"); if is_nvidia { log::warn!( "Device {} use the nvidia driver. IN_FENCE_FD will not be used.", @@ -2068,6 +2160,7 @@ impl MetalBackend { on_change: Default::default(), direct_scanout_enabled: Default::default(), is_nvidia, + is_amd, lease_ids: Default::default(), leases: Default::default(), leases_to_break: Default::default(), @@ -2123,6 +2216,7 @@ impl MetalBackend { for c in dev.dev.crtcs.values() { let props = collect_untyped_properties(master, c.id)?; c.active.value.set(get(&props, c.active.id)? != 0); + c.vrr_enabled.value.set(get(&props, c.vrr_enabled.id)? != 0); c.mode_id .value .set(DrmBlob(get(&props, c.mode_id.id)? as _)); @@ -2144,6 +2238,7 @@ impl MetalBackend { connector.can_present.set(true); connector.has_damage.set(true); connector.cursor_changed.set(true); + connector.cursor_scheduled.set(true); } if dev.unprocessed_change.get() { return self.handle_drm_change_(dev, false); @@ -2204,7 +2299,7 @@ impl MetalBackend { if let Some(fb) = connector.next_framebuffer.take() { *connector.active_framebuffer.borrow_mut() = Some(fb); } - if connector.has_damage.get() || connector.cursor_changed.get() { + if connector.has_damage.get() || connector.cursor_scheduled.get() { connector.schedule_present(); } let dd = connector.display.borrow_mut(); @@ -2282,10 +2377,12 @@ impl MetalBackend { crtc.connector.set(None); crtc.active.value.set(false); crtc.mode_id.value.set(DrmBlob::NONE); + crtc.vrr_enabled.value.set(false); changes.change_object(crtc.id, |c| { c.change(crtc.active.id, 0); c.change(crtc.mode_id.id, 0); c.change(crtc.out_fence_ptr, 0); + c.change(crtc.vrr_enabled.id, 0); }) } } @@ -2483,6 +2580,7 @@ impl MetalBackend { continue; } connector.send_hardware_cursor(); + connector.send_vrr_enabled(); connector.update_drm_feedback(); } Ok(()) @@ -2490,6 +2588,7 @@ impl MetalBackend { fn can_use_current_drm_mode(&self, dev: &Rc) -> bool { let mut used_crtcs = AHashSet::new(); + let mut vrr_crtcs = AHashSet::new(); let mut used_planes = AHashSet::new(); for connector in dev.connectors.lock().values() { @@ -2507,6 +2606,9 @@ impl MetalBackend { return false; } used_crtcs.insert(crtc_id); + if dd.should_enable_vrr() { + vrr_crtcs.insert(crtc_id); + } let crtc = dev.dev.crtcs.get(&crtc_id).unwrap(); connector.crtc.set(Some(crtc.clone())); crtc.connector.set(Some(connector.clone())); @@ -2558,6 +2660,11 @@ impl MetalBackend { c.change(crtc.active.id, 0); } c.change(crtc.out_fence_ptr, 0); + let vrr_requested = vrr_crtcs.contains(&crtc.id); + if crtc.vrr_enabled.value.get() != vrr_requested { + c.change(crtc.vrr_enabled.id, vrr_requested as _); + crtc.vrr_enabled.value.set(vrr_requested); + } }); } if let Err(e) = changes.commit(flags, 0) { @@ -2748,6 +2855,7 @@ impl MetalBackend { changes.change_object(crtc.id, |c| { c.change(crtc.active.id, 1); c.change(crtc.mode_id.id, mode_blob.id().0 as _); + c.change(crtc.vrr_enabled.id, dd.should_enable_vrr() as _); }); connector.crtc.set(Some(crtc.clone())); dd.crtc_id.value.set(crtc.id); @@ -2755,6 +2863,7 @@ impl MetalBackend { crtc.active.value.set(true); crtc.mode_id.value.set(mode_blob.id()); crtc.mode_blob.set(Some(Rc::new(mode_blob))); + crtc.vrr_enabled.value.set(dd.should_enable_vrr() as _); Ok(()) } @@ -2894,6 +3003,7 @@ impl MetalBackend { } connector.has_damage.set(true); connector.cursor_changed.set(true); + connector.cursor_scheduled.set(true); connector.schedule_present(); } } diff --git a/src/backends/x.rs b/src/backends/x.rs index 172bc8b3..a2c008ef 100644 --- a/src/backends/x.rs +++ b/src/backends/x.rs @@ -575,6 +575,7 @@ impl XBackend { width_mm: output.width.get(), height_mm: output.height.get(), non_desktop: false, + vrr_capable: false, })); output.changed(); self.present(output).await; diff --git a/src/cli/randr.rs b/src/cli/randr.rs index f627bd83..80816085 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -3,16 +3,17 @@ use { cli::GlobalArgs, scale::Scale, tools::tool_client::{with_tool_client, Handle, ToolClient}, - utils::transform_ext::TransformExt, + utils::{errorfmt::ErrorFmt, transform_ext::TransformExt}, wire::{jay_compositor, jay_randr, JayRandrId}, }, clap::{Args, Subcommand, ValueEnum}, isnt::std_1::vec::IsntVecExt, - jay_config::video::Transform, + jay_config::video::{Transform, VrrMode}, std::{ cell::RefCell, fmt::{Display, Formatter}, rc::Rc, + str::FromStr, }, }; @@ -117,6 +118,8 @@ pub enum OutputCommand { Disable, /// Override the display's non-desktop setting. NonDesktop(NonDesktopArgs), + /// Change VRR settings. + Vrr(VrrArgs), } #[derive(ValueEnum, Debug, Clone)] @@ -132,6 +135,46 @@ pub struct NonDesktopArgs { pub setting: NonDesktopType, } +#[derive(Args, Debug, Clone)] +pub struct VrrArgs { + #[clap(subcommand)] + pub command: VrrCommand, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum VrrCommand { + /// Sets the mode that determines when VRR is enabled. + SetMode(SetModeArgs), + /// Sets the maximum refresh rate of the cursor. + SetCursorHz(CursorHzArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct SetModeArgs { + #[clap(value_enum)] + pub mode: VrrModeArg, +} + +#[derive(ValueEnum, Debug, Copy, Clone, Hash, PartialEq)] +pub enum VrrModeArg { + /// VRR is never enabled. + Never, + /// VRR is always enabled. + Always, + /// VRR is enabled when one or more applications are displayed fullscreen. + Variant1, + /// VRR is enabled when a single application is displayed fullscreen. + Variant2, + /// VRR is enabled when a single game or video is displayed fullscreen. + Variant3, +} + +#[derive(Args, Debug, Clone)] +pub struct CursorHzArgs { + /// The rate at which the cursor will be updated on screen. + pub rate: String, +} + #[derive(Args, Debug, Clone)] pub struct PositionArgs { /// The top-left x coordinate. @@ -233,6 +276,10 @@ struct Output { pub current_mode: Option, pub modes: Vec, pub non_desktop: bool, + pub vrr_capable: bool, + pub vrr_enabled: bool, + pub vrr_mode: VrrMode, + pub vrr_cursor_hz: Option, } #[derive(Copy, Clone, Debug)] @@ -399,6 +446,47 @@ impl Randr { non_desktop: a.setting as _, }); } + OutputCommand::Vrr(a) => { + self.handle_error(randr, move |msg| { + eprintln!("Could not change the VRR setting: {}", msg); + }); + let parse_rate = |rate: &str| { + if rate.eq_ignore_ascii_case("none") { + f64::INFINITY + } else { + match f64::from_str(rate) { + Ok(v) => v, + Err(e) => { + fatal!("Could not parse rate: {}", ErrorFmt(e)); + } + } + } + }; + match a.command { + VrrCommand::SetMode(a) => { + let mode = match a.mode { + VrrModeArg::Never => VrrMode::NEVER, + VrrModeArg::Always => VrrMode::ALWAYS, + VrrModeArg::Variant1 => VrrMode::VARIANT_1, + VrrModeArg::Variant2 => VrrMode::VARIANT_2, + VrrModeArg::Variant3 => VrrMode::VARIANT_3, + }; + tc.send(jay_randr::SetVrrMode { + self_id: randr, + output: &args.output, + mode: mode.0, + }); + } + VrrCommand::SetCursorHz(r) => { + let hz = parse_rate(&r.rate); + tc.send(jay_randr::SetVrrCursorHz { + self_id: randr, + output: &args.output, + hz, + }); + } + } + } } tc.round_trip().await; } @@ -513,6 +601,26 @@ impl Randr { println!(" non-desktop"); return; } + println!(" VRR capable: {}", o.vrr_capable); + if o.vrr_capable { + println!(" VRR enabled: {}", o.vrr_enabled); + let mode_str; + let mode = match o.vrr_mode { + VrrMode::NEVER => "never", + VrrMode::ALWAYS => "always", + VrrMode::VARIANT_1 => "variant1", + VrrMode::VARIANT_2 => "variant2", + VrrMode::VARIANT_3 => "variant3", + _ => { + mode_str = format!("unknown ({})", o.vrr_mode.0); + &mode_str + } + }; + println!(" VRR mode: {}", mode); + if let Some(hz) = o.vrr_cursor_hz { + println!(" VRR cursor hz: {}", hz); + } + } println!(" position: {} x {}", o.x, o.y); println!(" logical size: {} x {}", o.width, o.height); if let Some(mode) = &o.current_mode { @@ -601,6 +709,10 @@ impl Randr { modes: Default::default(), current_mode: None, non_desktop: false, + vrr_capable: false, + vrr_enabled: false, + vrr_mode: VrrMode::NEVER, + vrr_cursor_hz: None, }); }); jay_randr::NonDesktopOutput::handle(tc, randr, data.clone(), |data, msg| { @@ -621,8 +733,26 @@ impl Randr { modes: Default::default(), current_mode: None, non_desktop: true, + vrr_capable: false, + vrr_enabled: false, + vrr_mode: VrrMode::NEVER, + vrr_cursor_hz: None, }); }); + jay_randr::VrrState::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output.vrr_capable = msg.capable != 0; + output.vrr_enabled = msg.enabled != 0; + output.vrr_mode = VrrMode(msg.mode); + }); + jay_randr::VrrCursorHz::handle(tc, randr, data.clone(), move |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output.vrr_cursor_hz = Some(msg.hz); + }); jay_randr::Mode::handle(tc, randr, data.clone(), |data, msg| { let mut data = data.borrow_mut(); let c = data.connectors.last_mut().unwrap(); diff --git a/src/compositor.rs b/src/compositor.rs index 0fa5caff..0d9d431e 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -4,7 +4,7 @@ use { crate::{ acceptor::{Acceptor, AcceptorError}, async_engine::{AsyncEngine, Phase, SpawnedFuture}, - backend::{self, Backend}, + backend::{self, Backend, Connector}, backends::{ dummy::{DummyBackend, DummyOutput}, metal, x, @@ -25,6 +25,7 @@ use { io_uring::{IoUring, IoUringError}, leaks, logger::Logger, + output_schedule::OutputSchedule, portal::{self, PortalStartup}, scale::Scale, sighand::{self, SighandError}, @@ -32,7 +33,7 @@ use { tasks::{self, idle}, tree::{ container_layout, container_render_data, float_layout, float_titles, - output_render_data, DisplayNode, NodeIds, OutputNode, WorkspaceNode, + output_render_data, DisplayNode, NodeIds, OutputNode, VrrMode, WorkspaceNode, }, user_session::import_environment, utils::{ @@ -246,6 +247,8 @@ fn start_compositor2( tablet_tool_ids: Default::default(), tablet_pad_ids: Default::default(), damage_visualizer: DamageVisualizer::new(&engine), + default_vrr_mode: Cell::new(VrrMode::NEVER), + default_vrr_cursor_hz: Cell::new(None), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -420,16 +423,25 @@ fn create_dummy_output(state: &Rc) { transform: Default::default(), scale: Default::default(), pos: Default::default(), + vrr_mode: Cell::new(VrrMode::NEVER), + vrr_cursor_hz: Default::default(), }); + let connector = Rc::new(DummyOutput { + id: state.connector_ids.next(), + }) as Rc; + let schedule = Rc::new(OutputSchedule::new( + &state.ring, + &state.eng, + &connector, + &persistent_state, + )); let dummy_output = Rc::new(OutputNode { id: state.node_ids.next(), global: Rc::new(WlOutputGlobal::new( state.globals.name(), state, &Rc::new(ConnectorData { - connector: Rc::new(DummyOutput { - id: state.connector_ids.next(), - }), + connector, handler: Cell::new(None), connected: Cell::new(true), name: "Dummy".to_string(), @@ -469,6 +481,7 @@ fn create_dummy_output(state: &Rc) { hardware_cursor_needs_render: Cell::new(false), screencopies: Default::default(), title_visible: Cell::new(false), + schedule, }); let dummy_workspace = Rc::new(WorkspaceNode { id: state.node_ids.next(), diff --git a/src/config/handler.rs b/src/config/handler.rs index 755f8e4c..5a06d9a3 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -9,12 +9,13 @@ use { config::ConfigProxy, ifs::wl_seat::{SeatId, WlSeatGlobal}, io_uring::TaskResultExt, + output_schedule::map_cursor_hz, scale::Scale, state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State}, theme::{Color, ThemeSized, DEFAULT_FONT}, tree::{ move_ws_to_output, ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, - OutputNode, WsMoveConfig, + OutputNode, VrrMode, WsMoveConfig, }, utils::{ asyncevent::AsyncEvent, @@ -47,7 +48,7 @@ use { logging::LogLevel, theme::{colors::Colorable, sized::Resizable}, timer::Timer as JayTimer, - video::{Connector, DrmDevice, GfxApi, Transform}, + video::{Connector, DrmDevice, GfxApi, Transform, VrrMode as ConfigVrrMode}, Axis, Direction, Workspace, }, libloading::Library, @@ -1032,6 +1033,45 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_vrr_mode( + &self, + connector: Option, + mode: ConfigVrrMode, + ) -> Result<(), CphError> { + let Some(mode) = VrrMode::from_config(mode) else { + return Err(CphError::UnknownVrrMode(mode)); + }; + match connector { + Some(c) => { + let connector = self.get_output_node(c)?; + connector.global.persistent.vrr_mode.set(mode); + connector.update_vrr_state(); + } + _ => self.state.default_vrr_mode.set(mode), + } + Ok(()) + } + + fn handle_set_vrr_cursor_hz( + &self, + connector: Option, + hz: f64, + ) -> Result<(), CphError> { + match connector { + Some(c) => { + let connector = self.get_output_node(c)?; + connector.schedule.set_cursor_hz(hz); + } + _ => { + let Some((hz, _)) = map_cursor_hz(hz) else { + return Err(CphError::InvalidCursorHz(hz)); + }; + self.state.default_vrr_cursor_hz.set(hz) + } + } + Ok(()) + } + fn handle_connector_set_transform( &self, connector: Connector, @@ -1826,6 +1866,12 @@ impl ConfigProxyHandler { ClientMessage::SetWindowManagementEnabled { seat, enabled } => self .handle_set_window_management_enabled(seat, enabled) .wrn("set_window_management_enabled")?, + ClientMessage::SetVrrMode { connector, mode } => self + .handle_set_vrr_mode(connector, mode) + .wrn("set_vrr_mode")?, + ClientMessage::SetVrrCursorHz { connector, hz } => self + .handle_set_vrr_cursor_hz(connector, hz) + .wrn("set_vrr_cursor_hz")?, } Ok(()) } @@ -1887,6 +1933,10 @@ enum CphError { NegativeCursorSize, #[error("Config referred to a pollable that does not exist")] PollableDoesNotExist, + #[error("Unknown VRR mode {0:?}")] + UnknownVrrMode(ConfigVrrMode), + #[error("Invalid cursor hz {0}")] + InvalidCursorHz(f64), } trait WithRequestName { diff --git a/src/cursor_user.rs b/src/cursor_user.rs index e84d50be..deb37bd5 100644 --- a/src/cursor_user.rs +++ b/src/cursor_user.rs @@ -82,7 +82,7 @@ impl CursorUserGroup { let x_int = x.round_down(); let y_int = y.round_down(); let extents = cursor.extents_at_scale(Scale::default()); - self.state.damage(extents.move_(x_int, y_int)); + self.state.damage2(true, extents.move_(x_int, y_int)); } } } @@ -399,8 +399,10 @@ impl CursorUser { let old_x_int = old_x.round_down(); let old_y_int = old_y.round_down(); let extents = cursor.extents_at_scale(Scale::default()); - self.group.state.damage(extents.move_(old_x_int, old_y_int)); - self.group.state.damage(extents.move_(x_int, y_int)); + self.group + .state + .damage2(true, extents.move_(old_x_int, old_y_int)); + self.group.state.damage2(true, extents.move_(x_int, y_int)); } } self.pos.set((x, y)); @@ -439,6 +441,13 @@ impl CursorUser { let (x, y) = self.pos.get(); for output in self.group.state.root.outputs.lock().values() { if let Some(hc) = output.hardware_cursor.get() { + let commit = || { + let defer = output.schedule.defer_cursor_updates(); + hc.commit(!defer); + if defer { + output.schedule.hardware_cursor_changed(); + } + }; let transform = output.global.persistent.transform.get(); let render = render | output.hardware_cursor_needs_render.take(); let scale = output.global.persistent.scale.get(); @@ -448,7 +457,7 @@ impl CursorUser { let (max_width, max_height) = transform.maybe_swap((hc_width, hc_height)); if extents.width() > max_width || extents.height() > max_height { hc.set_enabled(false); - hc.commit(); + commit(); continue; } } @@ -495,7 +504,7 @@ impl CursorUser { } hc.set_enabled(false); } - hc.commit(); + commit(); } } } diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index e041a384..d8057530 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -43,12 +43,13 @@ impl JayCompositorGlobal { self: Rc, id: JayCompositorId, client: &Rc, - _version: Version, + version: Version, ) -> Result<(), JayCompositorError> { let obj = Rc::new(JayCompositor { id, client: client.clone(), tracker: Default::default(), + version, }); track!(client, obj); client.add_client_obj(&obj)?; @@ -65,7 +66,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 1 + 2 } fn required_caps(&self) -> ClientCaps { @@ -79,6 +80,7 @@ pub struct JayCompositor { id: JayCompositorId, client: Rc, tracker: Tracker, + version: Version, } pub struct Cap; @@ -327,7 +329,7 @@ impl JayCompositorRequestHandler for JayCompositor { } fn get_randr(&self, req: GetRandr, _slf: &Rc) -> Result<(), Self::Error> { - let sc = Rc::new(JayRandr::new(req.id, &self.client)); + let sc = Rc::new(JayRandr::new(req.id, &self.client, self.version)); track!(self.client, sc); self.client.add_client_obj(&sc)?; Ok(()) @@ -379,7 +381,7 @@ impl JayCompositorRequestHandler for JayCompositor { object_base! { self = JayCompositor; - version = Version(1); + version = self.version; } impl Object for JayCompositor {} diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index e6c975fa..50b87597 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -7,11 +7,11 @@ use { object::{Object, Version}, scale::Scale, state::{ConnectorData, DrmDevData, OutputData}, - tree::OutputNode, + tree::{OutputNode, VrrMode}, utils::{gfx_api_ext::GfxApiExt, transform_ext::TransformExt}, wire::{jay_randr::*, JayRandrId}, }, - jay_config::video::{GfxApi, Transform}, + jay_config::video::{GfxApi, Transform, VrrMode as ConfigVrrMode}, std::rc::Rc, thiserror::Error, }; @@ -20,14 +20,18 @@ pub struct JayRandr { pub id: JayRandrId, pub client: Rc, pub tracker: Tracker, + pub version: Version, } +const VRR_CAPABLE_SINCE: Version = Version(2); + impl JayRandr { - pub fn new(id: JayRandrId, client: &Rc) -> Self { + pub fn new(id: JayRandrId, client: &Rc, version: Version) -> Self { Self { id, client: client.clone(), tracker: Default::default(), + version, } } @@ -68,9 +72,9 @@ impl JayRandr { let Some(output) = self.client.state.outputs.get(&data.connector.id()) else { return; }; - let global = match output.node.as_ref().map(|n| &n.global) { - Some(g) => g, - _ => { + let node = match &output.node { + Some(n) => n, + None => { self.client.event(NonDesktopOutput { self_id: self.id, manufacturer: &output.monitor_info.manufacturer, @@ -82,6 +86,7 @@ impl JayRandr { return; } }; + let global = &node.global; let pos = global.pos.get(); self.client.event(Output { self_id: self.id, @@ -97,6 +102,20 @@ impl JayRandr { width_mm: global.width_mm, height_mm: global.height_mm, }); + if self.version >= VRR_CAPABLE_SINCE { + self.client.event(VrrState { + self_id: self.id, + capable: output.monitor_info.vrr_capable as _, + enabled: node.schedule.vrr_enabled() as _, + mode: node.global.persistent.vrr_mode.get().to_config().0, + }); + if let Some(hz) = node.global.persistent.vrr_cursor_hz.get() { + self.client.event(VrrCursorHz { + self_id: self.id, + hz, + }); + } + } let current_mode = global.mode.get(); for mode in &global.modes { self.client.event(Mode { @@ -297,11 +316,35 @@ impl JayRandrRequestHandler for JayRandr { c.connector.set_non_desktop_override(non_desktop); Ok(()) } + + fn set_vrr_mode(&self, req: SetVrrMode<'_>, _slf: &Rc) -> Result<(), Self::Error> { + let Some(mode) = VrrMode::from_config(ConfigVrrMode(req.mode)) else { + return Err(JayRandrError::UnknownVrrMode(req.mode)); + }; + let Some(c) = self.get_output_node(req.output) else { + return Ok(()); + }; + c.global.persistent.vrr_mode.set(mode); + c.update_vrr_state(); + return Ok(()); + } + + fn set_vrr_cursor_hz( + &self, + req: SetVrrCursorHz<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let Some(c) = self.get_output_node(req.output) else { + return Ok(()); + }; + c.schedule.set_cursor_hz(req.hz); + Ok(()) + } } object_base! { self = JayRandr; - version = Version(1); + version = self.version; } impl Object for JayRandr {} @@ -312,5 +355,7 @@ simple_add_obj!(JayRandr); pub enum JayRandrError { #[error(transparent)] ClientError(Box), + #[error("Unknown VRR mode {0}")] + UnknownVrrMode(u32), } efrom!(JayRandrError, ClientError); diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index 13b4b606..d72a9697 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -10,7 +10,7 @@ use { object::{Object, Version}, rect::Rect, state::{ConnectorData, State}, - tree::{calculate_logical_size, OutputNode}, + tree::{calculate_logical_size, OutputNode, VrrMode}, utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, transform_ext::TransformExt}, wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id}, }, @@ -91,6 +91,8 @@ pub struct PersistentOutputState { pub transform: Cell, pub scale: Cell, pub pos: Cell<(i32, i32)>, + pub vrr_mode: Cell<&'static VrrMode>, + pub vrr_cursor_hz: Cell>, } #[derive(Eq, PartialEq, Hash)] diff --git a/src/it/test_backend.rs b/src/it/test_backend.rs index ae284bb7..7acb55f9 100644 --- a/src/it/test_backend.rs +++ b/src/it/test_backend.rs @@ -110,6 +110,7 @@ impl TestBackend { width_mm: 80, height_mm: 60, non_desktop: false, + vrr_capable: false, }; Self { state: state.clone(), diff --git a/src/it/tests/t0034_workspace_restoration.rs b/src/it/tests/t0034_workspace_restoration.rs index b400d747..2bf9ce82 100644 --- a/src/it/tests/t0034_workspace_restoration.rs +++ b/src/it/tests/t0034_workspace_restoration.rs @@ -43,6 +43,7 @@ async fn test(run: Rc) -> TestResult { width_mm: 0, height_mm: 0, non_desktop: false, + vrr_capable: false, }; run.backend .state diff --git a/src/main.rs b/src/main.rs index 264305db..9e8d4cbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,7 @@ mod libinput; mod logger; mod logind; mod object; +mod output_schedule; mod pango; mod pipewire; mod portal; diff --git a/src/output_schedule.rs b/src/output_schedule.rs new file mode 100644 index 00000000..794d9ccd --- /dev/null +++ b/src/output_schedule.rs @@ -0,0 +1,196 @@ +use { + crate::{ + async_engine::AsyncEngine, + backend::{Connector, HardwareCursor}, + ifs::wl_output::PersistentOutputState, + io_uring::{IoUring, IoUringError}, + utils::{ + asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt, + numcell::NumCell, + }, + }, + futures_util::{select, FutureExt}, + num_traits::ToPrimitive, + std::{cell::Cell, rc::Rc}, +}; + +pub struct OutputSchedule { + changed: AsyncEvent, + run: Cell, + + connector: Rc, + hardware_cursor: CloneCell>>, + + persistent: Rc, + + last_present_nsec: Cell, + cursor_delta_nsec: Cell>, + + ring: Rc, + eng: Rc, + + vrr_enabled: Cell, + + present_scheduled: Cell, + needs_hardware_cursor_commit: Cell, + needs_software_cursor_damage: Cell, + + iteration: NumCell, +} + +impl OutputSchedule { + pub fn new( + ring: &Rc, + eng: &Rc, + connector: &Rc, + persistent: &Rc, + ) -> Self { + let slf = Self { + changed: Default::default(), + run: Default::default(), + connector: connector.clone(), + ring: ring.clone(), + eng: eng.clone(), + vrr_enabled: Default::default(), + present_scheduled: Cell::new(true), + needs_hardware_cursor_commit: Default::default(), + needs_software_cursor_damage: Default::default(), + hardware_cursor: Default::default(), + persistent: persistent.clone(), + last_present_nsec: Default::default(), + cursor_delta_nsec: Default::default(), + iteration: Default::default(), + }; + if let Some(hz) = persistent.vrr_cursor_hz.get() { + slf.set_cursor_hz(hz); + } + slf + } + + pub async fn drive(self: Rc) { + loop { + self.run_once().await; + while !self.run.take() { + self.changed.triggered().await; + } + } + } + + fn trigger(&self) { + let trigger = self.vrr_enabled.get() + && !self.present_scheduled.get() + && self.cursor_delta_nsec.is_some() + && (self.needs_software_cursor_damage.get() || self.needs_hardware_cursor_commit.get()); + if trigger { + self.run.set(true); + self.changed.trigger(); + } + } + + pub fn presented(&self) { + self.last_present_nsec.set(self.eng.now().nsec()); + self.present_scheduled.set(false); + self.iteration.fetch_add(1); + self.trigger(); + } + + pub fn vrr_enabled(&self) -> bool { + self.vrr_enabled.get() + } + + pub fn set_vrr_enabled(&self, enabled: bool) { + self.vrr_enabled.set(enabled); + self.trigger(); + } + + pub fn set_cursor_hz(&self, hz: f64) { + let (hz, delta) = match map_cursor_hz(hz) { + None => { + log::warn!("Ignoring cursor frequency {hz}"); + return; + } + Some(v) => v, + }; + self.persistent.vrr_cursor_hz.set(hz); + self.cursor_delta_nsec.set(delta); + self.trigger(); + } + + pub fn set_hardware_cursor(&self, hc: &Option>) { + self.hardware_cursor.set(hc.clone()); + } + + pub fn defer_cursor_updates(&self) -> bool { + self.vrr_enabled.get() && self.cursor_delta_nsec.is_some() + } + + pub fn hardware_cursor_changed(&self) { + if !self.needs_hardware_cursor_commit.replace(true) { + self.trigger(); + } + } + + pub fn software_cursor_changed(&self) { + if !self.needs_software_cursor_damage.replace(true) { + self.trigger(); + } + } + + async fn run_once(&self) { + if self.present_scheduled.get() { + return; + } + if !self.needs_hardware_cursor_commit.get() && !self.needs_software_cursor_damage.get() { + return; + } + loop { + if !self.vrr_enabled.get() { + return; + } + let Some(duration) = self.cursor_delta_nsec.get() else { + return; + }; + let iteration = self.iteration.get(); + let next_present = self.last_present_nsec.get().saturating_add(duration); + let res: Result<(), IoUringError> = select! { + _ = self.changed.triggered().fuse() => continue, + v = self.ring.timeout(next_present).fuse() => v, + }; + if let Err(e) = res { + log::error!("Could not wait for timer to expire: {}", ErrorFmt(e)); + return; + } + if iteration == self.iteration.get() { + break; + } + } + if self.needs_hardware_cursor_commit.take() { + if let Some(hc) = self.hardware_cursor.get() { + if hc.schedule_present() { + self.present_scheduled.set(true); + } + } + } + if self.needs_software_cursor_damage.take() { + self.connector.damage(); + self.present_scheduled.set(true); + } + } +} + +pub fn map_cursor_hz(hz: f64) -> Option<(Option, Option)> { + if hz <= 0.0 { + return Some((Some(0.0), Some(u64::MAX))); + } + let delta = (1_000_000_000.0 / hz).to_u64(); + if delta.is_none() { + if hz > 0.0 { + return Some((None, None)); + } + return None; + } + if delta == Some(0) { + return Some((None, None)); + } + Some((Some(hz), delta)) +} diff --git a/src/state.rs b/src/state.rs index b98bd2d5..430b5d34 100644 --- a/src/state.rs +++ b/src/state.rs @@ -64,7 +64,7 @@ use { time::Time, tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, Node, NodeIds, - NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase, + NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase, VrrMode, WorkspaceNode, }, utils::{ @@ -201,6 +201,8 @@ pub struct State { pub tablet_tool_ids: TabletToolIds, pub tablet_pad_ids: TabletPadIds, pub damage_visualizer: DamageVisualizer, + pub default_vrr_mode: Cell<&'static VrrMode>, + pub default_vrr_cursor_hz: Cell>, } // impl Drop for State { @@ -730,13 +732,21 @@ impl State { } pub fn damage(&self, rect: Rect) { + self.damage2(false, rect); + } + + pub fn damage2(&self, cursor: bool, rect: Rect) { if rect.is_empty() { return; } self.damage_visualizer.add(rect); for output in self.root.outputs.lock().values() { if output.global.pos.get().intersects(&rect) { - output.global.connector.connector.damage(); + if cursor && output.schedule.defer_cursor_updates() { + output.schedule.software_cursor_changed(); + } else { + output.global.connector.connector.damage(); + } } } } @@ -821,7 +831,7 @@ impl State { for output in self.root.outputs.lock().values() { if let Some(hc) = output.hardware_cursor.get() { hc.set_enabled(false); - hc.commit(); + hc.commit(true); } } } diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index dabeb8f5..0d4b09d6 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -3,6 +3,7 @@ use { backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo}, globals::GlobalName, ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, + output_schedule::OutputSchedule, state::{ConnectorData, OutputData, State}, tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig}, utils::{asyncevent::AsyncEvent, clonecell::CloneCell, hash_map_ext::HashMapExt}, @@ -122,6 +123,8 @@ impl ConnectorHandler { transform: Default::default(), scale: Default::default(), pos: Cell::new((x1, 0)), + vrr_mode: Cell::new(self.state.default_vrr_mode.get()), + vrr_cursor_hz: Cell::new(self.state.default_vrr_cursor_hz.get()), }); self.state .persistent_output_states @@ -140,6 +143,13 @@ impl ConnectorHandler { &output_id, &desired_state, )); + let schedule = Rc::new(OutputSchedule::new( + &self.state.ring, + &self.state.eng, + &self.data.connector, + &desired_state, + )); + let _schedule = self.state.eng.spawn(schedule.clone().drive()); let on = Rc::new(OutputNode { id: self.state.node_ids.next(), workspaces: Default::default(), @@ -173,6 +183,7 @@ impl ConnectorHandler { hardware_cursor_needs_render: Cell::new(false), screencopies: Default::default(), title_visible: Default::default(), + schedule, }); on.update_visible(); on.update_rects(); @@ -231,17 +242,22 @@ impl ConnectorHandler { } self.state.add_global(&global); self.state.tree_changed(); + on.update_vrr_state(); 'outer: loop { while let Some(event) = self.data.connector.event() { match event { ConnectorEvent::Disconnected => break 'outer, ConnectorEvent::HardwareCursor(hc) => { + on.schedule.set_hardware_cursor(&hc); on.hardware_cursor.set(hc); self.state.refresh_hardware_cursors(); } ConnectorEvent::ModeChanged(mode) => { on.update_mode(mode); } + ConnectorEvent::VrrChanged(enabled) => { + on.schedule.set_vrr_enabled(enabled); + } ev => unreachable!("received unexpected event {:?}", ev), } } diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 0abd322b..b256849c 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -286,7 +286,7 @@ impl ToolClient { } #[derive(Default)] struct S { - jay_compositor: Cell>, + jay_compositor: Cell>, jay_damage_tracking: Cell>, } let s = Rc::new(S::default()); @@ -297,7 +297,7 @@ impl ToolClient { }); wl_registry::Global::handle(self, registry, s.clone(), |s, g| { if g.interface == JayCompositor.name() { - s.jay_compositor.set(Some(g.name)); + s.jay_compositor.set(Some((g.name, g.version))); } else if g.interface == JayDamageTracking.name() { s.jay_damage_tracking.set(Some(g.name)); } @@ -328,9 +328,9 @@ impl ToolClient { let id: JayCompositorId = self.id(); self.send(wl_registry::Bind { self_id: s.registry, - name: s.jay_compositor, + name: s.jay_compositor.0, interface: JayCompositor.name(), - version: 1, + version: s.jay_compositor.1.min(2), id: id.into(), }); self.jay_compositor.set(Some(id)); @@ -361,7 +361,7 @@ impl ToolClient { pub struct Singletons { registry: WlRegistryId, - pub jay_compositor: u32, + pub jay_compositor: (u32, u32), pub jay_damage_tracking: Option, } diff --git a/src/tree/output.rs b/src/tree/output.rs index 08b1a4ee..c18f0a83 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -21,9 +21,11 @@ use { zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1}, SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor, }, + wp_content_type_v1::ContentType, zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP}, zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, }, + output_schedule::OutputSchedule, rect::Rect, renderer::Renderer, scale::Scale, @@ -41,7 +43,7 @@ use { wire::{JayOutputId, JayScreencastId, ZwlrScreencopyFrameV1Id}, }, ahash::AHashMap, - jay_config::video::Transform, + jay_config::video::{Transform, VrrMode as ConfigVrrMode}, smallvec::SmallVec, std::{ cell::{Cell, RefCell}, @@ -77,6 +79,7 @@ pub struct OutputNode { pub screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc>, pub screencopies: CopyHashMap<(ClientId, ZwlrScreencopyFrameV1Id), Rc>, pub title_visible: Cell, + pub schedule: Rc, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -785,6 +788,39 @@ impl OutputNode { self.schedule_update_render_data(); self.state.tree_changed(); } + + pub fn update_vrr_state(&self) { + let enabled = match self.global.persistent.vrr_mode.get() { + VrrMode::Never => false, + VrrMode::Always => true, + VrrMode::Fullscreen { surface } => 'get: { + let Some(ws) = self.workspace.get() else { + break 'get false; + }; + let Some(tl) = ws.fullscreen.get() else { + break 'get false; + }; + if let Some(req) = surface { + let Some(surface) = tl.tl_scanout_surface() else { + break 'get false; + }; + if let Some(req) = req.content_type { + let Some(content_type) = surface.content_type.get() else { + break 'get false; + }; + match content_type { + ContentType::Photo if !req.photo => break 'get false, + ContentType::Video if !req.video => break 'get false, + ContentType::Game if !req.game => break 'get false, + _ => {} + } + } + } + true + } + }; + self.global.connector.connector.set_vrr_enabled(enabled); + } } pub struct OutputTitle { @@ -1084,3 +1120,68 @@ pub fn calculate_logical_size( } (width, height) } + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum VrrMode { + Never, + Always, + Fullscreen { + surface: Option, + }, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct VrrSurfaceRequirements { + content_type: Option, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct VrrContentTypeRequirements { + photo: bool, + video: bool, + game: bool, +} + +impl VrrMode { + pub const NEVER: &'static Self = &Self::Never; + pub const ALWAYS: &'static Self = &Self::Always; + pub const VARIANT_1: &'static Self = &Self::Fullscreen { surface: None }; + pub const VARIANT_2: &'static Self = &Self::Fullscreen { + surface: Some(VrrSurfaceRequirements { content_type: None }), + }; + pub const VARIANT_3: &'static Self = &Self::Fullscreen { + surface: Some(VrrSurfaceRequirements { + content_type: Some(VrrContentTypeRequirements { + photo: false, + video: true, + game: true, + }), + }), + }; + + pub fn from_config(mode: ConfigVrrMode) -> Option<&'static Self> { + let res = match mode { + ConfigVrrMode::NEVER => Self::NEVER, + ConfigVrrMode::ALWAYS => Self::ALWAYS, + ConfigVrrMode::VARIANT_1 => Self::VARIANT_1, + ConfigVrrMode::VARIANT_2 => Self::VARIANT_2, + ConfigVrrMode::VARIANT_3 => Self::VARIANT_3, + _ => return None, + }; + Some(res) + } + + pub fn to_config(&self) -> ConfigVrrMode { + match self { + Self::NEVER => ConfigVrrMode::NEVER, + Self::ALWAYS => ConfigVrrMode::ALWAYS, + Self::VARIANT_1 => ConfigVrrMode::VARIANT_1, + Self::VARIANT_2 => ConfigVrrMode::VARIANT_2, + Self::VARIANT_3 => ConfigVrrMode::VARIANT_3, + _ => { + log::error!("VRR mode {self:?} has no config representation"); + ConfigVrrMode::NEVER + } + } + } +} diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 7e8c1647..c8feaff1 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -181,6 +181,7 @@ impl WorkspaceNode { surface.send_feedback(&fb); } } + self.output.get().update_vrr_state(); } pub fn remove_fullscreen_node(&self) { @@ -194,6 +195,7 @@ impl WorkspaceNode { surface.send_feedback(&fb); } } + self.output.get().update_vrr_state(); } } diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 071d35b1..fa2cf949 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -22,7 +22,7 @@ use { logging::LogLevel, status::MessageFormat, theme::Color, - video::{GfxApi, Transform}, + video::{GfxApi, Transform, VrrMode}, Axis, Direction, Workspace, }, std::{ @@ -206,6 +206,7 @@ pub struct Output { pub scale: Option, pub transform: Option, pub mode: Option, + pub vrr: Option, } #[derive(Debug, Clone)] @@ -285,6 +286,12 @@ pub struct RepeatRate { pub delay: i32, } +#[derive(Debug, Clone)] +pub struct Vrr { + pub mode: Option, + pub cursor_hz: Option, +} + #[derive(Debug, Clone)] pub struct Shortcut { pub mask: Modifiers, @@ -318,6 +325,7 @@ pub struct Config { pub explicit_sync_enabled: Option, pub focus_follows_mouse: bool, pub window_management_key: Option, + pub vrr: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 316cafa0..61f91c9b 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -29,6 +29,7 @@ mod repeat_rate; pub mod shortcuts; mod status; mod theme; +mod vrr; #[derive(Debug, Error)] pub enum StringParserError { diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 2b791236..51de6c98 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -23,6 +23,7 @@ use { }, status::StatusParser, theme::ThemeParser, + vrr::VrrParser, }, spanned::SpannedErrorExt, Action, Config, Theme, @@ -106,6 +107,7 @@ impl Parser for ConfigParser<'_> { complex_shortcuts_val, focus_follows_mouse, window_management_key_val, + vrr_val, ), ) = ext.extract(( ( @@ -138,6 +140,7 @@ impl Parser for ConfigParser<'_> { opt(val("complex-shortcuts")), recover(opt(bol("focus-follows-mouse"))), recover(opt(str("window-management-key"))), + opt(val("vrr")), ), ))?; let mut keymap = None; @@ -302,6 +305,15 @@ impl Parser for ConfigParser<'_> { window_management_key = Some(key); } } + let mut vrr = None; + if let Some(value) = vrr_val { + match value.parse(&mut VrrParser(self.0)) { + Ok(v) => vrr = Some(v), + Err(e) => { + log::warn!("Could not parse VRR setting: {}", self.0.error(e)); + } + } + } Ok(Config { keymap, repeat_rate, @@ -326,6 +338,7 @@ impl Parser for ConfigParser<'_> { idle, focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true), window_management_key, + vrr, }) } } diff --git a/toml-config/src/config/parsers/output.rs b/toml-config/src/config/parsers/output.rs index 55010359..6bde6b32 100644 --- a/toml-config/src/config/parsers/output.rs +++ b/toml-config/src/config/parsers/output.rs @@ -7,6 +7,7 @@ use { parsers::{ mode::ModeParser, output_match::{OutputMatchParser, OutputMatchParserError}, + vrr::VrrParser, }, Output, }, @@ -46,7 +47,7 @@ impl<'a> Parser for OutputParser<'a> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.cx, span, table); - let (name, match_val, x, y, scale, transform, mode) = ext.extract(( + let (name, match_val, x, y, scale, transform, mode, vrr_val) = ext.extract(( opt(str("name")), val("match"), recover(opt(s32("x"))), @@ -54,6 +55,7 @@ impl<'a> Parser for OutputParser<'a> { recover(opt(fltorint("scale"))), recover(opt(str("transform"))), opt(val("mode")), + opt(val("vrr")), ))?; let transform = match transform { None => None, @@ -96,6 +98,15 @@ impl<'a> Parser for OutputParser<'a> { ); } } + let mut vrr = None; + if let Some(value) = vrr_val { + match value.parse(&mut VrrParser(self.cx)) { + Ok(v) => vrr = Some(v), + Err(e) => { + log::warn!("Could not parse VRR setting: {}", self.cx.error(e)); + } + } + } Ok(Output { name: name.despan().map(|v| v.to_string()), match_: match_val.parse_map(&mut OutputMatchParser(self.cx))?, @@ -104,6 +115,7 @@ impl<'a> Parser for OutputParser<'a> { scale: scale.despan(), transform, mode, + vrr, }) } } diff --git a/toml-config/src/config/parsers/vrr.rs b/toml-config/src/config/parsers/vrr.rs new file mode 100644 index 00000000..144bd059 --- /dev/null +++ b/toml-config/src/config/parsers/vrr.rs @@ -0,0 +1,116 @@ +use { + crate::{ + config::{ + context::Context, + extractor::{opt, val, Extractor, ExtractorError}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + Vrr, + }, + toml::{ + toml_span::{Span, Spanned, SpannedExt}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + jay_config::video::VrrMode, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum VrrParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), +} + +pub struct VrrParser<'a>(pub &'a Context<'a>); + +impl Parser for VrrParser<'_> { + type Value = Vrr; + type Error = VrrParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (mode, cursor_hz) = ext.extract((opt(val("mode")), opt(val("cursor-hz"))))?; + let mode = mode.and_then(|m| match m.parse(&mut VrrModeParser) { + Ok(m) => Some(m), + Err(e) => { + log::error!("Could not parse mode: {}", self.0.error(e)); + None + } + }); + let cursor_hz = cursor_hz.and_then(|m| match m.parse(&mut VrrRateParser) { + Ok(m) => Some(m), + Err(e) => { + log::error!("Could not parse rate: {}", self.0.error(e)); + None + } + }); + Ok(Vrr { mode, cursor_hz }) + } +} + +#[derive(Debug, Error)] +pub enum VrrModeParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Unknown mode {0}")] + UnknownMode(String), +} + +struct VrrModeParser; + +impl Parser for VrrModeParser { + type Value = VrrMode; + type Error = VrrModeParserError; + const EXPECTED: &'static [DataType] = &[DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + let mode = match string { + "never" => VrrMode::NEVER, + "always" => VrrMode::ALWAYS, + "variant1" => VrrMode::VARIANT_1, + "variant2" => VrrMode::VARIANT_2, + "variant3" => VrrMode::VARIANT_3, + _ => return Err(VrrModeParserError::UnknownMode(string.to_string()).spanned(span)), + }; + Ok(mode) + } +} + +#[derive(Debug, Error)] +pub enum VrrRateParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Unknown rate {0}")] + UnknownString(String), +} + +struct VrrRateParser; + +impl Parser for VrrRateParser { + type Value = f64; + type Error = VrrRateParserError; + const EXPECTED: &'static [DataType] = &[DataType::String, DataType::Float, DataType::Integer]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + match string { + "none" => Ok(f64::INFINITY), + _ => Err(VrrRateParserError::UnknownString(string.to_string()).spanned(span)), + } + } + + fn parse_integer(&mut self, _span: Span, integer: i64) -> ParseResult { + Ok(integer as _) + } + + fn parse_float(&mut self, _span: Span, float: f64) -> ParseResult { + Ok(float) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index fa1d94bf..4fd3e58a 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -30,7 +30,8 @@ use { video::{ connectors, drm_devices, on_connector_connected, on_connector_disconnected, on_graphics_initialized, on_new_connector, on_new_drm_device, - set_direct_scanout_enabled, set_gfx_api, Connector, DrmDevice, + set_direct_scanout_enabled, set_gfx_api, set_vrr_cursor_hz, set_vrr_mode, Connector, + DrmDevice, }, }, std::{cell::RefCell, io::ErrorKind, path::PathBuf, rc::Rc}, @@ -555,6 +556,14 @@ impl Output { Some(m) => c.set_mode(m.width(), m.height(), Some(m.refresh_rate())), } } + if let Some(vrr) = &self.vrr { + if let Some(mode) = vrr.mode { + c.set_vrr_mode(mode); + } + if let Some(hz) = vrr.cursor_hz { + c.set_vrr_cursor_hz(hz); + } + } } } @@ -1017,6 +1026,14 @@ fn load_config(initial_load: bool, persistent: &Rc) { .seat .set_window_management_key(window_management_key); } + if let Some(vrr) = config.vrr { + if let Some(mode) = vrr.mode { + set_vrr_mode(mode); + } + if let Some(hz) = vrr.cursor_hz { + set_vrr_cursor_hz(hz); + } + } } fn create_command(exec: &Exec) -> Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 893a0548..07b3fe2d 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -577,6 +577,10 @@ "window-management-key": { "type": "string", "description": "Configures a key that will enable window management mode while pressed.\n\nIn window management mode, floating windows can be moved by pressing the left\nmouse button and all windows can be resize by pressing the right mouse button.\n\n- Example:\n\n ```toml\n window-management-key = \"Alt_L\"\n ```\n" + }, + "vrr": { + "description": "Configures the default VRR settings.\n\nThis can be overwritten for individual outputs.\n\nBy default, the VRR mode is `never` and the cursor refresh rate is unbounded.\n\n- Example:\n \n ```toml\n vrr = { mode = \"always\", cursor-hz = 90 }\n ```\n", + "$ref": "#/$defs/Vrr" } }, "required": [] @@ -1023,6 +1027,10 @@ "mode": { "description": "The mode of the output.\n\nIf the refresh rate is not specified, the first mode with the specified width and\nheight is used.\n", "$ref": "#/$defs/Mode" + }, + "vrr": { + "description": "Configures the VRR settings of this output.\n\nBy default, the VRR mode is `never` and the cursor refresh rate is unbounded.\n\n- Example:\n\n ```toml\n [[outputs]]\n match.serial-number = \"33K03894SL0\"\n vrr = { mode = \"always\", cursor-hz = 90 }\n ```\n", + "$ref": "#/$defs/Vrr" } }, "required": [ @@ -1234,6 +1242,45 @@ "flip-rotate-180", "flip-rotate-270" ] + }, + "Vrr": { + "description": "Describes VRR settings.\n\n- Example:\n\n ```toml\n vrr = { mode = \"always\", cursor-hz = 90 }\n ```\n", + "type": "object", + "properties": { + "mode": { + "description": "The VRR mode.", + "$ref": "#/$defs/VrrMode" + }, + "cursor-hz": { + "description": "The VRR cursor refresh rate.\n\nLimits the rate at which cursors are updated on screen when VRR is active.\n", + "$ref": "#/$defs/VrrHz" + } + }, + "required": [] + }, + "VrrHz": { + "description": "A VRR refresh rate limiter.\n\n- Example 1:\n\n ```toml\n vrr = { cursor-hz = 90 }\n ```\n\n- Example 2:\n\n ```toml\n vrr = { cursor-hz = \"none\" }\n ```\n", + "anyOf": [ + { + "type": "string", + "description": "The string `none` can be used to disable the limiter." + }, + { + "type": "number", + "description": "The refresh rate in HZ." + } + ] + }, + "VrrMode": { + "type": "string", + "description": "The VRR mode of an output.\n\n- Example:\n\n ```toml\n vrr = { mode = \"always\", cursor-hz = 90 }\n ```\n", + "enum": [ + "always", + "never", + "variant1", + "variant2", + "variant3" + ] } } } \ No newline at end of file diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 1ab98544..ae47890c 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1110,6 +1110,22 @@ The table has the following fields: The value of this field should be a string. +- `vrr` (optional): + + Configures the default VRR settings. + + This can be overwritten for individual outputs. + + By default, the VRR mode is `never` and the cursor refresh rate is unbounded. + + - Example: + + ```toml + vrr = { mode = "always", cursor-hz = 90 } + ``` + + The value of this field should be a [Vrr](#types-Vrr). + ### `Connector` @@ -2166,6 +2182,22 @@ The table has the following fields: The value of this field should be a [Mode](#types-Mode). +- `vrr` (optional): + + Configures the VRR settings of this output. + + By default, the VRR mode is `never` and the cursor refresh rate is unbounded. + + - Example: + + ```toml + [[outputs]] + match.serial-number = "33K03894SL0" + vrr = { mode = "always", cursor-hz = 90 } + ``` + + The value of this field should be a [Vrr](#types-Vrr). + ### `OutputMatch` @@ -2672,3 +2704,98 @@ The string should have one of the following values: + +### `Vrr` + +Describes VRR settings. + +- Example: + + ```toml + vrr = { mode = "always", cursor-hz = 90 } + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `mode` (optional): + + The VRR mode. + + The value of this field should be a [VrrMode](#types-VrrMode). + +- `cursor-hz` (optional): + + The VRR cursor refresh rate. + + Limits the rate at which cursors are updated on screen when VRR is active. + + The value of this field should be a [VrrHz](#types-VrrHz). + + + +### `VrrHz` + +A VRR refresh rate limiter. + +- Example 1: + + ```toml + vrr = { cursor-hz = 90 } + ``` + +- Example 2: + + ```toml + vrr = { cursor-hz = "none" } + ``` + +Values of this type should have one of the following forms: + +#### A string + +The string `none` can be used to disable the limiter. + +#### A number + +The refresh rate in HZ. + + + +### `VrrMode` + +The VRR mode of an output. + +- Example: + + ```toml + vrr = { mode = "always", cursor-hz = 90 } + ``` + +Values of this type should be strings. + +The string should have one of the following values: + +- `always`: + + VRR is never enabled. + +- `never`: + + VRR is always enabled. + +- `variant1`: + + VRR is enabled when one or more applications are displayed fullscreen. + +- `variant2`: + + VRR is enabled when a single application is displayed fullscreen. + +- `variant3`: + + VRR is enabled when a single game or video is displayed fullscreen. + + + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 9f2b396e..af9e1ac4 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1558,6 +1558,21 @@ Output: If the refresh rate is not specified, the first mode with the specified width and height is used. + vrr: + ref: Vrr + required: false + description: | + Configures the VRR settings of this output. + + By default, the VRR mode is `never` and the cursor refresh rate is unbounded. + + - Example: + + ```toml + [[outputs]] + match.serial-number = "33K03894SL0" + vrr = { mode = "always", cursor-hz = 90 } + ``` Transform: @@ -2150,6 +2165,21 @@ Config: ```toml window-management-key = "Alt_L" ``` + vrr: + ref: Vrr + required: false + description: | + Configures the default VRR settings. + + This can be overwritten for individual outputs. + + By default, the VRR mode is `never` and the cursor refresh rate is unbounded. + + - Example: + + ```toml + vrr = { mode = "always", cursor-hz = 90 } + ``` Idle: @@ -2267,3 +2297,73 @@ ComplexShortcut: Audio will be un-muted once `x` key is released, regardless of any other keys that are pressed at the time. + + +Vrr: + kind: table + description: | + Describes VRR settings. + + - Example: + + ```toml + vrr = { mode = "always", cursor-hz = 90 } + ``` + fields: + mode: + ref: VrrMode + required: false + description: The VRR mode. + cursor-hz: + ref: VrrHz + required: false + description: | + The VRR cursor refresh rate. + + Limits the rate at which cursors are updated on screen when VRR is active. + + +VrrMode: + description: | + The VRR mode of an output. + + - Example: + + ```toml + vrr = { mode = "always", cursor-hz = 90 } + ``` + kind: string + values: + - value: always + description: VRR is never enabled. + - value: never + description: VRR is always enabled. + - value: variant1 + description: VRR is enabled when one or more applications are displayed fullscreen. + - value: variant2 + description: VRR is enabled when a single application is displayed fullscreen. + - value: variant3 + description: VRR is enabled when a single game or video is displayed fullscreen. + + +VrrHz: + description: | + A VRR refresh rate limiter. + + - Example 1: + + ```toml + vrr = { cursor-hz = 90 } + ``` + + - Example 2: + + ```toml + vrr = { cursor-hz = "none" } + ``` + kind: variable + variants: + - kind: string + description: The string `none` can be used to disable the limiter. + - kind: number + description: The refresh rate in HZ. diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt index 96ab32ed..c4b8701c 100644 --- a/wire/jay_randr.txt +++ b/wire/jay_randr.txt @@ -55,6 +55,16 @@ request set_non_desktop { non_desktop: u32, } +request set_vrr_mode (since = 2) { + output: str, + mode: u32, +} + +request set_vrr_cursor_hz (since = 2) { + output: str, + hz: pod(f64), +} + # events event global { @@ -112,3 +122,13 @@ event non_desktop_output { width_mm: i32, height_mm: i32, } + +event vrr_state (since = 2) { + capable: u32, + enabled: u32, + mode: u32, +} + +event vrr_cursor_hz (since = 2) { + hz: pod(f64), +}