1
0
Fork 0
forked from wry/wry

Merge pull request #227 from mahkoh/jorth/vrr

metal: implement VRR
This commit is contained in:
mahkoh 2024-07-19 23:11:06 +02:00 committed by GitHub
commit 33f9da66e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1320 additions and 91 deletions

View file

@ -1,5 +1,7 @@
# Unreleased
- Needs jay-config release.
- Needs jay-toml-config release.
- Needs jay-compositor release.
# 1.4.0

View file

@ -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

View file

@ -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<Connector>, mode: VrrMode) {
self.send(&ClientMessage::SetVrrMode { connector, mode })
}
pub fn set_vrr_cursor_hz(&self, connector: Option<Connector>, hz: f64) {
self.send(&ClientMessage::SetVrrCursorHz { connector, hz })
}
pub fn drm_devices(&self) -> Vec<DrmDevice> {
let res = self.send_with_response(&ClientMessage::GetDrmDevices);
get_response!(res, vec![], GetDrmDevices { devices });

View file

@ -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<Connector>,
mode: VrrMode,
},
SetVrrCursorHz {
connector: Option<Connector>,
hz: f64,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -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)
}

View file

@ -1,6 +1,7 @@
# Unreleased
- Add fine-grained damage tracking.
- Add support for adaptive sync.
# 1.4.0 (2024-07-07)

View file

@ -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<DrmConnector> {
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<SyncFile>);
fn commit(&self);
fn commit(&self, schedule_present: bool);
fn schedule_present(&self) -> bool;
fn size(&self) -> (i32, i32);
}

View file

@ -95,6 +95,7 @@ pub struct MetalDrmDevice {
pub on_change: OnChange<crate::backend::DrmEvent>,
pub direct_scanout_enabled: Cell<Option<bool>>,
pub is_nvidia: bool,
pub is_amd: bool,
pub lease_ids: MetalLeaseIds,
pub leases: CopyHashMap<MetalLeaseId, MetalLeaseData>,
pub leases_to_break: CopyHashMap<MetalLeaseId, MetalLeaseData>,
@ -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<bool>,
pub has_damage: Cell<bool>,
pub cursor_changed: Cell<bool>,
pub cursor_scheduled: Cell<bool>,
pub next_flip_nsec: Cell<u64>,
pub display: RefCell<ConnectorDisplayData>,
@ -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<Self>) {
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<PresentFb>) {
let Some(output) = self.state.root.outputs.get(&self.connector_id) else {
return;
};
fn perform_screencopies(&self, new_fb: &Option<PresentFb>, 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<DrmConnector> {
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<bool>,
pub mode_id: MutableProperty<DrmBlob>,
pub out_fence_ptr: DrmProperty,
pub vrr_enabled: MutableProperty<bool>,
pub mode_blob: CloneCell<Option<Rc<PropBlob>>>,
}
@ -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<MetalDrmDeviceData>) -> 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();
}
}

View file

@ -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;

View file

@ -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<Mode>,
pub modes: Vec<Mode>,
pub non_desktop: bool,
pub vrr_capable: bool,
pub vrr_enabled: bool,
pub vrr_mode: VrrMode,
pub vrr_cursor_hz: Option<f64>,
}
#[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();

View file

@ -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<State>) {
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<dyn Connector>;
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<State>) {
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(),

View file

@ -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<Connector>,
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<Connector>,
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 {

View file

@ -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();
}
}
}

View file

@ -43,12 +43,13 @@ impl JayCompositorGlobal {
self: Rc<Self>,
id: JayCompositorId,
client: &Rc<Client>,
_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<Client>,
tracker: Tracker<Self>,
version: Version,
}
pub struct Cap;
@ -327,7 +329,7 @@ impl JayCompositorRequestHandler for JayCompositor {
}
fn get_randr(&self, req: GetRandr, _slf: &Rc<Self>) -> 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 {}

View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
}
const VRR_CAPABLE_SINCE: Version = Version(2);
impl JayRandr {
pub fn new(id: JayRandrId, client: &Rc<Client>) -> Self {
pub fn new(id: JayRandrId, client: &Rc<Client>, 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<Self>) -> 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<Self>,
) -> 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<ClientError>),
#[error("Unknown VRR mode {0}")]
UnknownVrrMode(u32),
}
efrom!(JayRandrError, ClientError);

View file

@ -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<Transform>,
pub scale: Cell<crate::scale::Scale>,
pub pos: Cell<(i32, i32)>,
pub vrr_mode: Cell<&'static VrrMode>,
pub vrr_cursor_hz: Cell<Option<f64>>,
}
#[derive(Eq, PartialEq, Hash)]

View file

@ -110,6 +110,7 @@ impl TestBackend {
width_mm: 80,
height_mm: 60,
non_desktop: false,
vrr_capable: false,
};
Self {
state: state.clone(),

View file

@ -43,6 +43,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
width_mm: 0,
height_mm: 0,
non_desktop: false,
vrr_capable: false,
};
run.backend
.state

View file

@ -72,6 +72,7 @@ mod libinput;
mod logger;
mod logind;
mod object;
mod output_schedule;
mod pango;
mod pipewire;
mod portal;

196
src/output_schedule.rs Normal file
View file

@ -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<bool>,
connector: Rc<dyn Connector>,
hardware_cursor: CloneCell<Option<Rc<dyn HardwareCursor>>>,
persistent: Rc<PersistentOutputState>,
last_present_nsec: Cell<u64>,
cursor_delta_nsec: Cell<Option<u64>>,
ring: Rc<IoUring>,
eng: Rc<AsyncEngine>,
vrr_enabled: Cell<bool>,
present_scheduled: Cell<bool>,
needs_hardware_cursor_commit: Cell<bool>,
needs_software_cursor_damage: Cell<bool>,
iteration: NumCell<u64>,
}
impl OutputSchedule {
pub fn new(
ring: &Rc<IoUring>,
eng: &Rc<AsyncEngine>,
connector: &Rc<dyn Connector>,
persistent: &Rc<PersistentOutputState>,
) -> 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<Self>) {
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<Rc<dyn HardwareCursor>>) {
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<f64>, Option<u64>)> {
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))
}

View file

@ -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<Option<f64>>,
}
// 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);
}
}
}

View file

@ -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),
}
}

View file

@ -286,7 +286,7 @@ impl ToolClient {
}
#[derive(Default)]
struct S {
jay_compositor: Cell<Option<u32>>,
jay_compositor: Cell<Option<(u32, u32)>>,
jay_damage_tracking: Cell<Option<u32>>,
}
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<u32>,
}

View file

@ -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<JayScreencast>>,
pub screencopies: CopyHashMap<(ClientId, ZwlrScreencopyFrameV1Id), Rc<ZwlrScreencopyFrameV1>>,
pub title_visible: Cell<bool>,
pub schedule: Rc<OutputSchedule>,
}
#[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<VrrSurfaceRequirements>,
},
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct VrrSurfaceRequirements {
content_type: Option<VrrContentTypeRequirements>,
}
#[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
}
}
}
}

View file

@ -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();
}
}

View file

@ -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<f64>,
pub transform: Option<Transform>,
pub mode: Option<Mode>,
pub vrr: Option<Vrr>,
}
#[derive(Debug, Clone)]
@ -285,6 +286,12 @@ pub struct RepeatRate {
pub delay: i32,
}
#[derive(Debug, Clone)]
pub struct Vrr {
pub mode: Option<VrrMode>,
pub cursor_hz: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct Shortcut {
pub mask: Modifiers,
@ -318,6 +325,7 @@ pub struct Config {
pub explicit_sync_enabled: Option<bool>,
pub focus_follows_mouse: bool,
pub window_management_key: Option<ModifiedKeySym>,
pub vrr: Option<Vrr>,
}
#[derive(Debug, Error)]

View file

@ -29,6 +29,7 @@ mod repeat_rate;
pub mod shortcuts;
mod status;
mod theme;
mod vrr;
#[derive(Debug, Error)]
pub enum StringParserError {

View file

@ -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,
})
}
}

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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,
})
}
}

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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<Self> {
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<Self> {
match string {
"none" => Ok(f64::INFINITY),
_ => Err(VrrRateParserError::UnknownString(string.to_string()).spanned(span)),
}
}
fn parse_integer(&mut self, _span: Span, integer: i64) -> ParseResult<Self> {
Ok(integer as _)
}
fn parse_float(&mut self, _span: Span, float: f64) -> ParseResult<Self> {
Ok(float)
}
}

View file

@ -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<PersistentState>) {
.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 {

View file

@ -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"
]
}
}
}

View file

@ -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).
<a name="types-Connector"></a>
### `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).
<a name="types-OutputMatch"></a>
### `OutputMatch`
@ -2672,3 +2704,98 @@ The string should have one of the following values:
<a name="types-Vrr"></a>
### `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).
<a name="types-VrrHz"></a>
### `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.
<a name="types-VrrMode"></a>
### `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.

View file

@ -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.

View file

@ -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),
}