metal: implement VRR
This commit is contained in:
parent
cd09e57568
commit
2d7c13b0b4
35 changed files with 1320 additions and 91 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
|
- Needs jay-config release.
|
||||||
|
- Needs jay-toml-config release.
|
||||||
- Needs jay-compositor release.
|
- Needs jay-compositor release.
|
||||||
|
|
||||||
# 1.4.0
|
# 1.4.0
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,11 @@ Jay's shortcut system allows you to execute an action when a key is pressed and
|
||||||
|
|
||||||
## VR
|
## 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
|
## Protocol Support
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use {
|
||||||
timer::Timer,
|
timer::Timer,
|
||||||
video::{
|
video::{
|
||||||
connector_type::{ConnectorType, CON_UNKNOWN},
|
connector_type::{ConnectorType, CON_UNKNOWN},
|
||||||
Connector, DrmDevice, GfxApi, Mode, Transform,
|
Connector, DrmDevice, GfxApi, Mode, Transform, VrrMode,
|
||||||
},
|
},
|
||||||
Axis, Direction, ModifiedKeySym, PciId, Workspace,
|
Axis, Direction, ModifiedKeySym, PciId, Workspace,
|
||||||
},
|
},
|
||||||
|
|
@ -800,6 +800,14 @@ impl Client {
|
||||||
(width, height)
|
(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> {
|
pub fn drm_devices(&self) -> Vec<DrmDevice> {
|
||||||
let res = self.send_with_response(&ClientMessage::GetDrmDevices);
|
let res = self.send_with_response(&ClientMessage::GetDrmDevices);
|
||||||
get_response!(res, vec![], GetDrmDevices { devices });
|
get_response!(res, vec![], GetDrmDevices { devices });
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use {
|
||||||
logging::LogLevel,
|
logging::LogLevel,
|
||||||
theme::{colors::Colorable, sized::Resizable, Color},
|
theme::{colors::Colorable, sized::Resizable, Color},
|
||||||
timer::Timer,
|
timer::Timer,
|
||||||
video::{connector_type::ConnectorType, Connector, DrmDevice, GfxApi, Transform},
|
video::{connector_type::ConnectorType, Connector, DrmDevice, GfxApi, Transform, VrrMode},
|
||||||
Axis, Direction, PciId, Workspace,
|
Axis, Direction, PciId, Workspace,
|
||||||
_private::{PollableId, WireMode},
|
_private::{PollableId, WireMode},
|
||||||
},
|
},
|
||||||
|
|
@ -487,6 +487,14 @@ pub enum ClientMessage<'a> {
|
||||||
seat: Seat,
|
seat: Seat,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
},
|
},
|
||||||
|
SetVrrMode {
|
||||||
|
connector: Option<Connector>,
|
||||||
|
mode: VrrMode,
|
||||||
|
},
|
||||||
|
SetVrrCursorHz {
|
||||||
|
connector: Option<Connector>,
|
||||||
|
hz: f64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,20 @@ impl Connector {
|
||||||
}
|
}
|
||||||
get!(String::new()).connector_get_serial_number(self)
|
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.
|
/// Returns all available DRM devices.
|
||||||
|
|
@ -531,3 +545,38 @@ pub enum Transform {
|
||||||
/// Flip around the vertical axis, then rotate 270 degrees counter-clockwise.
|
/// Flip around the vertical axis, then rotate 270 degrees counter-clockwise.
|
||||||
FlipRotate270,
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
- Add fine-grained damage tracking.
|
- Add fine-grained damage tracking.
|
||||||
|
- Add support for adaptive sync.
|
||||||
|
|
||||||
# 1.4.0 (2024-07-07)
|
# 1.4.0 (2024-07-07)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ pub struct MonitorInfo {
|
||||||
pub width_mm: i32,
|
pub width_mm: i32,
|
||||||
pub height_mm: i32,
|
pub height_mm: i32,
|
||||||
pub non_desktop: bool,
|
pub non_desktop: bool,
|
||||||
|
pub vrr_capable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
|
@ -108,6 +109,9 @@ pub trait Connector {
|
||||||
fn drm_object_id(&self) -> Option<DrmConnector> {
|
fn drm_object_id(&self) -> Option<DrmConnector> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
fn set_vrr_enabled(&self, enabled: bool) {
|
||||||
|
let _ = enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -119,6 +123,7 @@ pub enum ConnectorEvent {
|
||||||
ModeChanged(Mode),
|
ModeChanged(Mode),
|
||||||
Unavailable,
|
Unavailable,
|
||||||
Available,
|
Available,
|
||||||
|
VrrChanged(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait HardwareCursor: Debug {
|
pub trait HardwareCursor: Debug {
|
||||||
|
|
@ -127,7 +132,8 @@ pub trait HardwareCursor: Debug {
|
||||||
fn set_position(&self, x: i32, y: i32);
|
fn set_position(&self, x: i32, y: i32);
|
||||||
fn swap_buffer(&self);
|
fn swap_buffer(&self);
|
||||||
fn set_sync_file(&self, sync_file: Option<SyncFile>);
|
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);
|
fn size(&self) -> (i32, i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ pub struct MetalDrmDevice {
|
||||||
pub on_change: OnChange<crate::backend::DrmEvent>,
|
pub on_change: OnChange<crate::backend::DrmEvent>,
|
||||||
pub direct_scanout_enabled: Cell<Option<bool>>,
|
pub direct_scanout_enabled: Cell<Option<bool>>,
|
||||||
pub is_nvidia: bool,
|
pub is_nvidia: bool,
|
||||||
|
pub is_amd: bool,
|
||||||
pub lease_ids: MetalLeaseIds,
|
pub lease_ids: MetalLeaseIds,
|
||||||
pub leases: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
pub leases: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
||||||
pub leases_to_break: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
pub leases_to_break: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
||||||
|
|
@ -299,6 +300,8 @@ pub struct ConnectorDisplayData {
|
||||||
pub refresh: u32,
|
pub refresh: u32,
|
||||||
pub non_desktop: bool,
|
pub non_desktop: bool,
|
||||||
pub non_desktop_effective: bool,
|
pub non_desktop_effective: bool,
|
||||||
|
pub vrr_capable: bool,
|
||||||
|
pub vrr_requested: bool,
|
||||||
|
|
||||||
pub monitor_manufacturer: String,
|
pub monitor_manufacturer: String,
|
||||||
pub monitor_name: String,
|
pub monitor_name: String,
|
||||||
|
|
@ -319,6 +322,10 @@ impl ConnectorDisplayData {
|
||||||
&& self.monitor_name == other.monitor_name
|
&& self.monitor_name == other.monitor_name
|
||||||
&& self.monitor_serial_number == other.monitor_serial_number
|
&& 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);
|
linear_ids!(MetalLeaseIds, MetalLeaseId, u64);
|
||||||
|
|
@ -417,6 +424,7 @@ pub struct MetalConnector {
|
||||||
pub can_present: Cell<bool>,
|
pub can_present: Cell<bool>,
|
||||||
pub has_damage: Cell<bool>,
|
pub has_damage: Cell<bool>,
|
||||||
pub cursor_changed: Cell<bool>,
|
pub cursor_changed: Cell<bool>,
|
||||||
|
pub cursor_scheduled: Cell<bool>,
|
||||||
pub next_flip_nsec: Cell<u64>,
|
pub next_flip_nsec: Cell<u64>,
|
||||||
|
|
||||||
pub display: RefCell<ConnectorDisplayData>,
|
pub display: RefCell<ConnectorDisplayData>,
|
||||||
|
|
@ -503,7 +511,7 @@ impl HardwareCursor for MetalHardwareCursor {
|
||||||
self.have_changes.set(true);
|
self.have_changes.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self) {
|
fn commit(&self, schedule_present: bool) {
|
||||||
if self.generation != self.connector.cursor_generation.get() {
|
if self.generation != self.connector.cursor_generation.get() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -520,8 +528,20 @@ impl HardwareCursor for MetalHardwareCursor {
|
||||||
}
|
}
|
||||||
self.connector.cursor_sync_file.set(self.sync_file.take());
|
self.connector.cursor_sync_file.set(self.sync_file.take());
|
||||||
self.connector.cursor_changed.set(true);
|
self.connector.cursor_changed.set(true);
|
||||||
if self.connector.can_present.get() {
|
if schedule_present {
|
||||||
self.connector.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>) {
|
fn send_hardware_cursor(self: &Rc<Self>) {
|
||||||
match self.frontend_state.get() {
|
match self.frontend_state.get() {
|
||||||
FrontState::Removed
|
FrontState::Removed
|
||||||
|
|
@ -894,7 +927,7 @@ impl MetalConnector {
|
||||||
Some(crtc) => crtc,
|
Some(crtc) => crtc,
|
||||||
_ => return Ok(()),
|
_ => 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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if !crtc.active.value.get() {
|
if !crtc.active.value.get() {
|
||||||
|
|
@ -908,6 +941,9 @@ impl MetalConnector {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
};
|
};
|
||||||
|
let Some(node) = self.state.root.outputs.get(&self.connector_id) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
let cursor = self.cursor_plane.get();
|
let cursor = self.cursor_plane.get();
|
||||||
let mut new_fb = None;
|
let mut new_fb = None;
|
||||||
let mut changes = self.master.change();
|
let mut changes = self.master.change();
|
||||||
|
|
@ -915,46 +951,52 @@ impl MetalConnector {
|
||||||
if !self.backend.check_render_context(&self.dev) {
|
if !self.backend.check_render_context(&self.dev) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if let Some(node) = self.state.root.outputs.get(&self.connector_id) {
|
let buffer = &buffers[self.next_buffer.get() % buffers.len()];
|
||||||
let buffer = &buffers[self.next_buffer.get() % buffers.len()];
|
let mut rr = self.render_result.borrow_mut();
|
||||||
let mut rr = self.render_result.borrow_mut();
|
rr.output_id = node.id;
|
||||||
rr.output_id = node.id;
|
let fb = self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?;
|
||||||
let fb =
|
rr.dispatch_frame_requests(self.state.now_msec());
|
||||||
self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?;
|
let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) =
|
||||||
rr.dispatch_frame_requests(self.state.now_msec());
|
match &fb.direct_scanout_data {
|
||||||
let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) =
|
None => {
|
||||||
match &fb.direct_scanout_data {
|
let plane_w = plane.mode_w.get();
|
||||||
None => {
|
let plane_h = plane.mode_h.get();
|
||||||
let plane_w = plane.mode_w.get();
|
(0, 0, plane_w, plane_h, plane_w, plane_h)
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
Some(dsd) => {
|
||||||
new_fb = Some(fb);
|
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;
|
let mut cursor_swap_buffer = false;
|
||||||
|
|
@ -1027,7 +1069,8 @@ impl MetalConnector {
|
||||||
.discard_presentation_feedback();
|
.discard_presentation_feedback();
|
||||||
Err(MetalError::Commit(e))
|
Err(MetalError::Commit(e))
|
||||||
} else {
|
} else {
|
||||||
self.perform_screencopies(&new_fb);
|
node.schedule.presented();
|
||||||
|
self.perform_screencopies(&new_fb, &node);
|
||||||
if let Some(fb) = new_fb {
|
if let Some(fb) = new_fb {
|
||||||
if fb.direct_scanout_data.is_none() {
|
if fb.direct_scanout_data.is_none() {
|
||||||
self.next_buffer.fetch_add(1);
|
self.next_buffer.fetch_add(1);
|
||||||
|
|
@ -1042,14 +1085,12 @@ impl MetalConnector {
|
||||||
self.can_present.set(false);
|
self.can_present.set(false);
|
||||||
self.has_damage.set(false);
|
self.has_damage.set(false);
|
||||||
self.cursor_changed.set(false);
|
self.cursor_changed.set(false);
|
||||||
|
self.cursor_scheduled.set(false);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_screencopies(&self, new_fb: &Option<PresentFb>) {
|
fn perform_screencopies(&self, new_fb: &Option<PresentFb>, output: &OutputNode) {
|
||||||
let Some(output) = self.state.root.outputs.get(&self.connector_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let active_fb;
|
let active_fb;
|
||||||
let fb = match &new_fb {
|
let fb = match &new_fb {
|
||||||
Some(f) => f,
|
Some(f) => f,
|
||||||
|
|
@ -1173,6 +1214,17 @@ impl MetalConnector {
|
||||||
log::error!("Tried to send available event in invalid state: {state:?}");
|
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> {
|
fn drm_object_id(&self) -> Option<DrmConnector> {
|
||||||
Some(self.id)
|
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 {
|
pub struct MetalCrtc {
|
||||||
|
|
@ -1312,6 +1390,7 @@ pub struct MetalCrtc {
|
||||||
pub active: MutableProperty<bool>,
|
pub active: MutableProperty<bool>,
|
||||||
pub mode_id: MutableProperty<DrmBlob>,
|
pub mode_id: MutableProperty<DrmBlob>,
|
||||||
pub out_fence_ptr: DrmProperty,
|
pub out_fence_ptr: DrmProperty,
|
||||||
|
pub vrr_enabled: MutableProperty<bool>,
|
||||||
|
|
||||||
pub mode_blob: CloneCell<Option<Rc<PropBlob>>>,
|
pub mode_blob: CloneCell<Option<Rc<PropBlob>>>,
|
||||||
}
|
}
|
||||||
|
|
@ -1435,6 +1514,7 @@ fn create_connector(
|
||||||
display: RefCell::new(display),
|
display: RefCell::new(display),
|
||||||
frontend_state: Cell::new(FrontState::Disconnected),
|
frontend_state: Cell::new(FrontState::Disconnected),
|
||||||
cursor_changed: Cell::new(false),
|
cursor_changed: Cell::new(false),
|
||||||
|
cursor_scheduled: Cell::new(false),
|
||||||
cursor_front_buffer: Default::default(),
|
cursor_front_buffer: Default::default(),
|
||||||
cursor_swap_buffer: Cell::new(false),
|
cursor_swap_buffer: Cell::new(false),
|
||||||
cursor_sync_file: Default::default(),
|
cursor_sync_file: Default::default(),
|
||||||
|
|
@ -1545,6 +1625,10 @@ fn create_connector_display_data(
|
||||||
let props = collect_properties(&dev.master, connector)?;
|
let props = collect_properties(&dev.master, connector)?;
|
||||||
let connector_type = ConnectorType::from_drm(info.connector_type);
|
let connector_type = ConnectorType::from_drm(info.connector_type);
|
||||||
let non_desktop = props.get("non-desktop")?.value.get() != 0;
|
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 {
|
Ok(ConnectorDisplayData {
|
||||||
crtc_id: props.get("CRTC_ID")?.map(|v| DrmCrtc(v as _)),
|
crtc_id: props.get("CRTC_ID")?.map(|v| DrmCrtc(v as _)),
|
||||||
crtcs,
|
crtcs,
|
||||||
|
|
@ -1553,6 +1637,8 @@ fn create_connector_display_data(
|
||||||
refresh,
|
refresh,
|
||||||
non_desktop,
|
non_desktop,
|
||||||
non_desktop_effective: non_desktop_override.unwrap_or(non_desktop),
|
non_desktop_effective: non_desktop_override.unwrap_or(non_desktop),
|
||||||
|
vrr_capable,
|
||||||
|
vrr_requested: false,
|
||||||
monitor_manufacturer: manufacturer,
|
monitor_manufacturer: manufacturer,
|
||||||
monitor_name: name,
|
monitor_name: name,
|
||||||
monitor_serial_number: serial_number,
|
monitor_serial_number: serial_number,
|
||||||
|
|
@ -1607,6 +1693,7 @@ fn create_crtc(
|
||||||
active: props.get("ACTIVE")?.map(|v| v == 1),
|
active: props.get("ACTIVE")?.map(|v| v == 1),
|
||||||
mode_id: props.get("MODE_ID")?.map(|v| DrmBlob(v as u32)),
|
mode_id: props.get("MODE_ID")?.map(|v| DrmBlob(v as u32)),
|
||||||
out_fence_ptr: props.get("OUT_FENCE_PTR")?.id,
|
out_fence_ptr: props.get("OUT_FENCE_PTR")?.id,
|
||||||
|
vrr_enabled: props.get("VRR_ENABLED")?.map(|v| v == 1),
|
||||||
mode_blob: Default::default(),
|
mode_blob: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1876,6 +1963,7 @@ impl MetalBackend {
|
||||||
dd.mode = Some(mode.clone());
|
dd.mode = Some(mode.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dd.vrr_requested = old.vrr_requested;
|
||||||
}
|
}
|
||||||
mem::swap(old.deref_mut(), &mut dd);
|
mem::swap(old.deref_mut(), &mut dd);
|
||||||
match c.frontend_state.get() {
|
match c.frontend_state.get() {
|
||||||
|
|
@ -1963,8 +2051,10 @@ impl MetalBackend {
|
||||||
width_mm: dd.mm_width as _,
|
width_mm: dd.mm_width as _,
|
||||||
height_mm: dd.mm_height as _,
|
height_mm: dd.mm_height as _,
|
||||||
non_desktop: dd.non_desktop_effective,
|
non_desktop: dd.non_desktop_effective,
|
||||||
|
vrr_capable: dd.vrr_capable,
|
||||||
}));
|
}));
|
||||||
connector.send_hardware_cursor();
|
connector.send_hardware_cursor();
|
||||||
|
connector.send_vrr_enabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_drm_device(
|
pub fn create_drm_device(
|
||||||
|
|
@ -2030,9 +2120,11 @@ impl MetalBackend {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut is_nvidia = false;
|
let mut is_nvidia = false;
|
||||||
|
let mut is_amd = false;
|
||||||
match gbm.drm.version() {
|
match gbm.drm.version() {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
is_nvidia = v.name.contains_str("nvidia");
|
is_nvidia = v.name.contains_str("nvidia");
|
||||||
|
is_amd = v.name.contains_str("amdgpu");
|
||||||
if is_nvidia {
|
if is_nvidia {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Device {} use the nvidia driver. IN_FENCE_FD will not be used.",
|
"Device {} use the nvidia driver. IN_FENCE_FD will not be used.",
|
||||||
|
|
@ -2068,6 +2160,7 @@ impl MetalBackend {
|
||||||
on_change: Default::default(),
|
on_change: Default::default(),
|
||||||
direct_scanout_enabled: Default::default(),
|
direct_scanout_enabled: Default::default(),
|
||||||
is_nvidia,
|
is_nvidia,
|
||||||
|
is_amd,
|
||||||
lease_ids: Default::default(),
|
lease_ids: Default::default(),
|
||||||
leases: Default::default(),
|
leases: Default::default(),
|
||||||
leases_to_break: Default::default(),
|
leases_to_break: Default::default(),
|
||||||
|
|
@ -2123,6 +2216,7 @@ impl MetalBackend {
|
||||||
for c in dev.dev.crtcs.values() {
|
for c in dev.dev.crtcs.values() {
|
||||||
let props = collect_untyped_properties(master, c.id)?;
|
let props = collect_untyped_properties(master, c.id)?;
|
||||||
c.active.value.set(get(&props, c.active.id)? != 0);
|
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
|
c.mode_id
|
||||||
.value
|
.value
|
||||||
.set(DrmBlob(get(&props, c.mode_id.id)? as _));
|
.set(DrmBlob(get(&props, c.mode_id.id)? as _));
|
||||||
|
|
@ -2144,6 +2238,7 @@ impl MetalBackend {
|
||||||
connector.can_present.set(true);
|
connector.can_present.set(true);
|
||||||
connector.has_damage.set(true);
|
connector.has_damage.set(true);
|
||||||
connector.cursor_changed.set(true);
|
connector.cursor_changed.set(true);
|
||||||
|
connector.cursor_scheduled.set(true);
|
||||||
}
|
}
|
||||||
if dev.unprocessed_change.get() {
|
if dev.unprocessed_change.get() {
|
||||||
return self.handle_drm_change_(dev, false);
|
return self.handle_drm_change_(dev, false);
|
||||||
|
|
@ -2204,7 +2299,7 @@ impl MetalBackend {
|
||||||
if let Some(fb) = connector.next_framebuffer.take() {
|
if let Some(fb) = connector.next_framebuffer.take() {
|
||||||
*connector.active_framebuffer.borrow_mut() = Some(fb);
|
*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();
|
connector.schedule_present();
|
||||||
}
|
}
|
||||||
let dd = connector.display.borrow_mut();
|
let dd = connector.display.borrow_mut();
|
||||||
|
|
@ -2282,10 +2377,12 @@ impl MetalBackend {
|
||||||
crtc.connector.set(None);
|
crtc.connector.set(None);
|
||||||
crtc.active.value.set(false);
|
crtc.active.value.set(false);
|
||||||
crtc.mode_id.value.set(DrmBlob::NONE);
|
crtc.mode_id.value.set(DrmBlob::NONE);
|
||||||
|
crtc.vrr_enabled.value.set(false);
|
||||||
changes.change_object(crtc.id, |c| {
|
changes.change_object(crtc.id, |c| {
|
||||||
c.change(crtc.active.id, 0);
|
c.change(crtc.active.id, 0);
|
||||||
c.change(crtc.mode_id.id, 0);
|
c.change(crtc.mode_id.id, 0);
|
||||||
c.change(crtc.out_fence_ptr, 0);
|
c.change(crtc.out_fence_ptr, 0);
|
||||||
|
c.change(crtc.vrr_enabled.id, 0);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2483,6 +2580,7 @@ impl MetalBackend {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
connector.send_hardware_cursor();
|
connector.send_hardware_cursor();
|
||||||
|
connector.send_vrr_enabled();
|
||||||
connector.update_drm_feedback();
|
connector.update_drm_feedback();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -2490,6 +2588,7 @@ impl MetalBackend {
|
||||||
|
|
||||||
fn can_use_current_drm_mode(&self, dev: &Rc<MetalDrmDeviceData>) -> bool {
|
fn can_use_current_drm_mode(&self, dev: &Rc<MetalDrmDeviceData>) -> bool {
|
||||||
let mut used_crtcs = AHashSet::new();
|
let mut used_crtcs = AHashSet::new();
|
||||||
|
let mut vrr_crtcs = AHashSet::new();
|
||||||
let mut used_planes = AHashSet::new();
|
let mut used_planes = AHashSet::new();
|
||||||
|
|
||||||
for connector in dev.connectors.lock().values() {
|
for connector in dev.connectors.lock().values() {
|
||||||
|
|
@ -2507,6 +2606,9 @@ impl MetalBackend {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
used_crtcs.insert(crtc_id);
|
used_crtcs.insert(crtc_id);
|
||||||
|
if dd.should_enable_vrr() {
|
||||||
|
vrr_crtcs.insert(crtc_id);
|
||||||
|
}
|
||||||
let crtc = dev.dev.crtcs.get(&crtc_id).unwrap();
|
let crtc = dev.dev.crtcs.get(&crtc_id).unwrap();
|
||||||
connector.crtc.set(Some(crtc.clone()));
|
connector.crtc.set(Some(crtc.clone()));
|
||||||
crtc.connector.set(Some(connector.clone()));
|
crtc.connector.set(Some(connector.clone()));
|
||||||
|
|
@ -2558,6 +2660,11 @@ impl MetalBackend {
|
||||||
c.change(crtc.active.id, 0);
|
c.change(crtc.active.id, 0);
|
||||||
}
|
}
|
||||||
c.change(crtc.out_fence_ptr, 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) {
|
if let Err(e) = changes.commit(flags, 0) {
|
||||||
|
|
@ -2748,6 +2855,7 @@ impl MetalBackend {
|
||||||
changes.change_object(crtc.id, |c| {
|
changes.change_object(crtc.id, |c| {
|
||||||
c.change(crtc.active.id, 1);
|
c.change(crtc.active.id, 1);
|
||||||
c.change(crtc.mode_id.id, mode_blob.id().0 as _);
|
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()));
|
connector.crtc.set(Some(crtc.clone()));
|
||||||
dd.crtc_id.value.set(crtc.id);
|
dd.crtc_id.value.set(crtc.id);
|
||||||
|
|
@ -2755,6 +2863,7 @@ impl MetalBackend {
|
||||||
crtc.active.value.set(true);
|
crtc.active.value.set(true);
|
||||||
crtc.mode_id.value.set(mode_blob.id());
|
crtc.mode_id.value.set(mode_blob.id());
|
||||||
crtc.mode_blob.set(Some(Rc::new(mode_blob)));
|
crtc.mode_blob.set(Some(Rc::new(mode_blob)));
|
||||||
|
crtc.vrr_enabled.value.set(dd.should_enable_vrr() as _);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2894,6 +3003,7 @@ impl MetalBackend {
|
||||||
}
|
}
|
||||||
connector.has_damage.set(true);
|
connector.has_damage.set(true);
|
||||||
connector.cursor_changed.set(true);
|
connector.cursor_changed.set(true);
|
||||||
|
connector.cursor_scheduled.set(true);
|
||||||
connector.schedule_present();
|
connector.schedule_present();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -575,6 +575,7 @@ impl XBackend {
|
||||||
width_mm: output.width.get(),
|
width_mm: output.width.get(),
|
||||||
height_mm: output.height.get(),
|
height_mm: output.height.get(),
|
||||||
non_desktop: false,
|
non_desktop: false,
|
||||||
|
vrr_capable: false,
|
||||||
}));
|
}));
|
||||||
output.changed();
|
output.changed();
|
||||||
self.present(output).await;
|
self.present(output).await;
|
||||||
|
|
|
||||||
134
src/cli/randr.rs
134
src/cli/randr.rs
|
|
@ -3,16 +3,17 @@ use {
|
||||||
cli::GlobalArgs,
|
cli::GlobalArgs,
|
||||||
scale::Scale,
|
scale::Scale,
|
||||||
tools::tool_client::{with_tool_client, Handle, ToolClient},
|
tools::tool_client::{with_tool_client, Handle, ToolClient},
|
||||||
utils::transform_ext::TransformExt,
|
utils::{errorfmt::ErrorFmt, transform_ext::TransformExt},
|
||||||
wire::{jay_compositor, jay_randr, JayRandrId},
|
wire::{jay_compositor, jay_randr, JayRandrId},
|
||||||
},
|
},
|
||||||
clap::{Args, Subcommand, ValueEnum},
|
clap::{Args, Subcommand, ValueEnum},
|
||||||
isnt::std_1::vec::IsntVecExt,
|
isnt::std_1::vec::IsntVecExt,
|
||||||
jay_config::video::Transform,
|
jay_config::video::{Transform, VrrMode},
|
||||||
std::{
|
std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt::{Display, Formatter},
|
fmt::{Display, Formatter},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
str::FromStr,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -117,6 +118,8 @@ pub enum OutputCommand {
|
||||||
Disable,
|
Disable,
|
||||||
/// Override the display's non-desktop setting.
|
/// Override the display's non-desktop setting.
|
||||||
NonDesktop(NonDesktopArgs),
|
NonDesktop(NonDesktopArgs),
|
||||||
|
/// Change VRR settings.
|
||||||
|
Vrr(VrrArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ValueEnum, Debug, Clone)]
|
#[derive(ValueEnum, Debug, Clone)]
|
||||||
|
|
@ -132,6 +135,46 @@ pub struct NonDesktopArgs {
|
||||||
pub setting: NonDesktopType,
|
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)]
|
#[derive(Args, Debug, Clone)]
|
||||||
pub struct PositionArgs {
|
pub struct PositionArgs {
|
||||||
/// The top-left x coordinate.
|
/// The top-left x coordinate.
|
||||||
|
|
@ -233,6 +276,10 @@ struct Output {
|
||||||
pub current_mode: Option<Mode>,
|
pub current_mode: Option<Mode>,
|
||||||
pub modes: Vec<Mode>,
|
pub modes: Vec<Mode>,
|
||||||
pub non_desktop: bool,
|
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)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
|
@ -399,6 +446,47 @@ impl Randr {
|
||||||
non_desktop: a.setting as _,
|
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;
|
tc.round_trip().await;
|
||||||
}
|
}
|
||||||
|
|
@ -513,6 +601,26 @@ impl Randr {
|
||||||
println!(" non-desktop");
|
println!(" non-desktop");
|
||||||
return;
|
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!(" position: {} x {}", o.x, o.y);
|
||||||
println!(" logical size: {} x {}", o.width, o.height);
|
println!(" logical size: {} x {}", o.width, o.height);
|
||||||
if let Some(mode) = &o.current_mode {
|
if let Some(mode) = &o.current_mode {
|
||||||
|
|
@ -601,6 +709,10 @@ impl Randr {
|
||||||
modes: Default::default(),
|
modes: Default::default(),
|
||||||
current_mode: None,
|
current_mode: None,
|
||||||
non_desktop: false,
|
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| {
|
jay_randr::NonDesktopOutput::handle(tc, randr, data.clone(), |data, msg| {
|
||||||
|
|
@ -621,8 +733,26 @@ impl Randr {
|
||||||
modes: Default::default(),
|
modes: Default::default(),
|
||||||
current_mode: None,
|
current_mode: None,
|
||||||
non_desktop: true,
|
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| {
|
jay_randr::Mode::handle(tc, randr, data.clone(), |data, msg| {
|
||||||
let mut data = data.borrow_mut();
|
let mut data = data.borrow_mut();
|
||||||
let c = data.connectors.last_mut().unwrap();
|
let c = data.connectors.last_mut().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use {
|
||||||
crate::{
|
crate::{
|
||||||
acceptor::{Acceptor, AcceptorError},
|
acceptor::{Acceptor, AcceptorError},
|
||||||
async_engine::{AsyncEngine, Phase, SpawnedFuture},
|
async_engine::{AsyncEngine, Phase, SpawnedFuture},
|
||||||
backend::{self, Backend},
|
backend::{self, Backend, Connector},
|
||||||
backends::{
|
backends::{
|
||||||
dummy::{DummyBackend, DummyOutput},
|
dummy::{DummyBackend, DummyOutput},
|
||||||
metal, x,
|
metal, x,
|
||||||
|
|
@ -25,6 +25,7 @@ use {
|
||||||
io_uring::{IoUring, IoUringError},
|
io_uring::{IoUring, IoUringError},
|
||||||
leaks,
|
leaks,
|
||||||
logger::Logger,
|
logger::Logger,
|
||||||
|
output_schedule::OutputSchedule,
|
||||||
portal::{self, PortalStartup},
|
portal::{self, PortalStartup},
|
||||||
scale::Scale,
|
scale::Scale,
|
||||||
sighand::{self, SighandError},
|
sighand::{self, SighandError},
|
||||||
|
|
@ -32,7 +33,7 @@ use {
|
||||||
tasks::{self, idle},
|
tasks::{self, idle},
|
||||||
tree::{
|
tree::{
|
||||||
container_layout, container_render_data, float_layout, float_titles,
|
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,
|
user_session::import_environment,
|
||||||
utils::{
|
utils::{
|
||||||
|
|
@ -246,6 +247,8 @@ fn start_compositor2(
|
||||||
tablet_tool_ids: Default::default(),
|
tablet_tool_ids: Default::default(),
|
||||||
tablet_pad_ids: Default::default(),
|
tablet_pad_ids: Default::default(),
|
||||||
damage_visualizer: DamageVisualizer::new(&engine),
|
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));
|
state.tracker.register(ClientId::from_raw(0));
|
||||||
create_dummy_output(&state);
|
create_dummy_output(&state);
|
||||||
|
|
@ -420,16 +423,25 @@ fn create_dummy_output(state: &Rc<State>) {
|
||||||
transform: Default::default(),
|
transform: Default::default(),
|
||||||
scale: Default::default(),
|
scale: Default::default(),
|
||||||
pos: 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 {
|
let dummy_output = Rc::new(OutputNode {
|
||||||
id: state.node_ids.next(),
|
id: state.node_ids.next(),
|
||||||
global: Rc::new(WlOutputGlobal::new(
|
global: Rc::new(WlOutputGlobal::new(
|
||||||
state.globals.name(),
|
state.globals.name(),
|
||||||
state,
|
state,
|
||||||
&Rc::new(ConnectorData {
|
&Rc::new(ConnectorData {
|
||||||
connector: Rc::new(DummyOutput {
|
connector,
|
||||||
id: state.connector_ids.next(),
|
|
||||||
}),
|
|
||||||
handler: Cell::new(None),
|
handler: Cell::new(None),
|
||||||
connected: Cell::new(true),
|
connected: Cell::new(true),
|
||||||
name: "Dummy".to_string(),
|
name: "Dummy".to_string(),
|
||||||
|
|
@ -469,6 +481,7 @@ fn create_dummy_output(state: &Rc<State>) {
|
||||||
hardware_cursor_needs_render: Cell::new(false),
|
hardware_cursor_needs_render: Cell::new(false),
|
||||||
screencopies: Default::default(),
|
screencopies: Default::default(),
|
||||||
title_visible: Cell::new(false),
|
title_visible: Cell::new(false),
|
||||||
|
schedule,
|
||||||
});
|
});
|
||||||
let dummy_workspace = Rc::new(WorkspaceNode {
|
let dummy_workspace = Rc::new(WorkspaceNode {
|
||||||
id: state.node_ids.next(),
|
id: state.node_ids.next(),
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@ use {
|
||||||
config::ConfigProxy,
|
config::ConfigProxy,
|
||||||
ifs::wl_seat::{SeatId, WlSeatGlobal},
|
ifs::wl_seat::{SeatId, WlSeatGlobal},
|
||||||
io_uring::TaskResultExt,
|
io_uring::TaskResultExt,
|
||||||
|
output_schedule::map_cursor_hz,
|
||||||
scale::Scale,
|
scale::Scale,
|
||||||
state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State},
|
state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State},
|
||||||
theme::{Color, ThemeSized, DEFAULT_FONT},
|
theme::{Color, ThemeSized, DEFAULT_FONT},
|
||||||
tree::{
|
tree::{
|
||||||
move_ws_to_output, ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase,
|
move_ws_to_output, ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase,
|
||||||
OutputNode, WsMoveConfig,
|
OutputNode, VrrMode, WsMoveConfig,
|
||||||
},
|
},
|
||||||
utils::{
|
utils::{
|
||||||
asyncevent::AsyncEvent,
|
asyncevent::AsyncEvent,
|
||||||
|
|
@ -47,7 +48,7 @@ use {
|
||||||
logging::LogLevel,
|
logging::LogLevel,
|
||||||
theme::{colors::Colorable, sized::Resizable},
|
theme::{colors::Colorable, sized::Resizable},
|
||||||
timer::Timer as JayTimer,
|
timer::Timer as JayTimer,
|
||||||
video::{Connector, DrmDevice, GfxApi, Transform},
|
video::{Connector, DrmDevice, GfxApi, Transform, VrrMode as ConfigVrrMode},
|
||||||
Axis, Direction, Workspace,
|
Axis, Direction, Workspace,
|
||||||
},
|
},
|
||||||
libloading::Library,
|
libloading::Library,
|
||||||
|
|
@ -1032,6 +1033,45 @@ impl ConfigProxyHandler {
|
||||||
Ok(())
|
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(
|
fn handle_connector_set_transform(
|
||||||
&self,
|
&self,
|
||||||
connector: Connector,
|
connector: Connector,
|
||||||
|
|
@ -1826,6 +1866,12 @@ impl ConfigProxyHandler {
|
||||||
ClientMessage::SetWindowManagementEnabled { seat, enabled } => self
|
ClientMessage::SetWindowManagementEnabled { seat, enabled } => self
|
||||||
.handle_set_window_management_enabled(seat, enabled)
|
.handle_set_window_management_enabled(seat, enabled)
|
||||||
.wrn("set_window_management_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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1887,6 +1933,10 @@ enum CphError {
|
||||||
NegativeCursorSize,
|
NegativeCursorSize,
|
||||||
#[error("Config referred to a pollable that does not exist")]
|
#[error("Config referred to a pollable that does not exist")]
|
||||||
PollableDoesNotExist,
|
PollableDoesNotExist,
|
||||||
|
#[error("Unknown VRR mode {0:?}")]
|
||||||
|
UnknownVrrMode(ConfigVrrMode),
|
||||||
|
#[error("Invalid cursor hz {0}")]
|
||||||
|
InvalidCursorHz(f64),
|
||||||
}
|
}
|
||||||
|
|
||||||
trait WithRequestName {
|
trait WithRequestName {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ impl CursorUserGroup {
|
||||||
let x_int = x.round_down();
|
let x_int = x.round_down();
|
||||||
let y_int = y.round_down();
|
let y_int = y.round_down();
|
||||||
let extents = cursor.extents_at_scale(Scale::default());
|
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_x_int = old_x.round_down();
|
||||||
let old_y_int = old_y.round_down();
|
let old_y_int = old_y.round_down();
|
||||||
let extents = cursor.extents_at_scale(Scale::default());
|
let extents = cursor.extents_at_scale(Scale::default());
|
||||||
self.group.state.damage(extents.move_(old_x_int, old_y_int));
|
self.group
|
||||||
self.group.state.damage(extents.move_(x_int, y_int));
|
.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));
|
self.pos.set((x, y));
|
||||||
|
|
@ -439,6 +441,13 @@ impl CursorUser {
|
||||||
let (x, y) = self.pos.get();
|
let (x, y) = self.pos.get();
|
||||||
for output in self.group.state.root.outputs.lock().values() {
|
for output in self.group.state.root.outputs.lock().values() {
|
||||||
if let Some(hc) = output.hardware_cursor.get() {
|
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 transform = output.global.persistent.transform.get();
|
||||||
let render = render | output.hardware_cursor_needs_render.take();
|
let render = render | output.hardware_cursor_needs_render.take();
|
||||||
let scale = output.global.persistent.scale.get();
|
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));
|
let (max_width, max_height) = transform.maybe_swap((hc_width, hc_height));
|
||||||
if extents.width() > max_width || extents.height() > max_height {
|
if extents.width() > max_width || extents.height() > max_height {
|
||||||
hc.set_enabled(false);
|
hc.set_enabled(false);
|
||||||
hc.commit();
|
commit();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -495,7 +504,7 @@ impl CursorUser {
|
||||||
}
|
}
|
||||||
hc.set_enabled(false);
|
hc.set_enabled(false);
|
||||||
}
|
}
|
||||||
hc.commit();
|
commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,13 @@ impl JayCompositorGlobal {
|
||||||
self: Rc<Self>,
|
self: Rc<Self>,
|
||||||
id: JayCompositorId,
|
id: JayCompositorId,
|
||||||
client: &Rc<Client>,
|
client: &Rc<Client>,
|
||||||
_version: Version,
|
version: Version,
|
||||||
) -> Result<(), JayCompositorError> {
|
) -> Result<(), JayCompositorError> {
|
||||||
let obj = Rc::new(JayCompositor {
|
let obj = Rc::new(JayCompositor {
|
||||||
id,
|
id,
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
tracker: Default::default(),
|
tracker: Default::default(),
|
||||||
|
version,
|
||||||
});
|
});
|
||||||
track!(client, obj);
|
track!(client, obj);
|
||||||
client.add_client_obj(&obj)?;
|
client.add_client_obj(&obj)?;
|
||||||
|
|
@ -65,7 +66,7 @@ impl Global for JayCompositorGlobal {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn version(&self) -> u32 {
|
fn version(&self) -> u32 {
|
||||||
1
|
2
|
||||||
}
|
}
|
||||||
|
|
||||||
fn required_caps(&self) -> ClientCaps {
|
fn required_caps(&self) -> ClientCaps {
|
||||||
|
|
@ -79,6 +80,7 @@ pub struct JayCompositor {
|
||||||
id: JayCompositorId,
|
id: JayCompositorId,
|
||||||
client: Rc<Client>,
|
client: Rc<Client>,
|
||||||
tracker: Tracker<Self>,
|
tracker: Tracker<Self>,
|
||||||
|
version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Cap;
|
pub struct Cap;
|
||||||
|
|
@ -327,7 +329,7 @@ impl JayCompositorRequestHandler for JayCompositor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_randr(&self, req: GetRandr, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
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);
|
track!(self.client, sc);
|
||||||
self.client.add_client_obj(&sc)?;
|
self.client.add_client_obj(&sc)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -379,7 +381,7 @@ impl JayCompositorRequestHandler for JayCompositor {
|
||||||
|
|
||||||
object_base! {
|
object_base! {
|
||||||
self = JayCompositor;
|
self = JayCompositor;
|
||||||
version = Version(1);
|
version = self.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Object for JayCompositor {}
|
impl Object for JayCompositor {}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ use {
|
||||||
object::{Object, Version},
|
object::{Object, Version},
|
||||||
scale::Scale,
|
scale::Scale,
|
||||||
state::{ConnectorData, DrmDevData, OutputData},
|
state::{ConnectorData, DrmDevData, OutputData},
|
||||||
tree::OutputNode,
|
tree::{OutputNode, VrrMode},
|
||||||
utils::{gfx_api_ext::GfxApiExt, transform_ext::TransformExt},
|
utils::{gfx_api_ext::GfxApiExt, transform_ext::TransformExt},
|
||||||
wire::{jay_randr::*, JayRandrId},
|
wire::{jay_randr::*, JayRandrId},
|
||||||
},
|
},
|
||||||
jay_config::video::{GfxApi, Transform},
|
jay_config::video::{GfxApi, Transform, VrrMode as ConfigVrrMode},
|
||||||
std::rc::Rc,
|
std::rc::Rc,
|
||||||
thiserror::Error,
|
thiserror::Error,
|
||||||
};
|
};
|
||||||
|
|
@ -20,14 +20,18 @@ pub struct JayRandr {
|
||||||
pub id: JayRandrId,
|
pub id: JayRandrId,
|
||||||
pub client: Rc<Client>,
|
pub client: Rc<Client>,
|
||||||
pub tracker: Tracker<Self>,
|
pub tracker: Tracker<Self>,
|
||||||
|
pub version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VRR_CAPABLE_SINCE: Version = Version(2);
|
||||||
|
|
||||||
impl JayRandr {
|
impl JayRandr {
|
||||||
pub fn new(id: JayRandrId, client: &Rc<Client>) -> Self {
|
pub fn new(id: JayRandrId, client: &Rc<Client>, version: Version) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
tracker: Default::default(),
|
tracker: Default::default(),
|
||||||
|
version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,9 +72,9 @@ impl JayRandr {
|
||||||
let Some(output) = self.client.state.outputs.get(&data.connector.id()) else {
|
let Some(output) = self.client.state.outputs.get(&data.connector.id()) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let global = match output.node.as_ref().map(|n| &n.global) {
|
let node = match &output.node {
|
||||||
Some(g) => g,
|
Some(n) => n,
|
||||||
_ => {
|
None => {
|
||||||
self.client.event(NonDesktopOutput {
|
self.client.event(NonDesktopOutput {
|
||||||
self_id: self.id,
|
self_id: self.id,
|
||||||
manufacturer: &output.monitor_info.manufacturer,
|
manufacturer: &output.monitor_info.manufacturer,
|
||||||
|
|
@ -82,6 +86,7 @@ impl JayRandr {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let global = &node.global;
|
||||||
let pos = global.pos.get();
|
let pos = global.pos.get();
|
||||||
self.client.event(Output {
|
self.client.event(Output {
|
||||||
self_id: self.id,
|
self_id: self.id,
|
||||||
|
|
@ -97,6 +102,20 @@ impl JayRandr {
|
||||||
width_mm: global.width_mm,
|
width_mm: global.width_mm,
|
||||||
height_mm: global.height_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();
|
let current_mode = global.mode.get();
|
||||||
for mode in &global.modes {
|
for mode in &global.modes {
|
||||||
self.client.event(Mode {
|
self.client.event(Mode {
|
||||||
|
|
@ -297,11 +316,35 @@ impl JayRandrRequestHandler for JayRandr {
|
||||||
c.connector.set_non_desktop_override(non_desktop);
|
c.connector.set_non_desktop_override(non_desktop);
|
||||||
Ok(())
|
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! {
|
object_base! {
|
||||||
self = JayRandr;
|
self = JayRandr;
|
||||||
version = Version(1);
|
version = self.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Object for JayRandr {}
|
impl Object for JayRandr {}
|
||||||
|
|
@ -312,5 +355,7 @@ simple_add_obj!(JayRandr);
|
||||||
pub enum JayRandrError {
|
pub enum JayRandrError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ClientError(Box<ClientError>),
|
ClientError(Box<ClientError>),
|
||||||
|
#[error("Unknown VRR mode {0}")]
|
||||||
|
UnknownVrrMode(u32),
|
||||||
}
|
}
|
||||||
efrom!(JayRandrError, ClientError);
|
efrom!(JayRandrError, ClientError);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use {
|
||||||
object::{Object, Version},
|
object::{Object, Version},
|
||||||
rect::Rect,
|
rect::Rect,
|
||||||
state::{ConnectorData, State},
|
state::{ConnectorData, State},
|
||||||
tree::{calculate_logical_size, OutputNode},
|
tree::{calculate_logical_size, OutputNode, VrrMode},
|
||||||
utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, transform_ext::TransformExt},
|
utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, transform_ext::TransformExt},
|
||||||
wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id},
|
wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id},
|
||||||
},
|
},
|
||||||
|
|
@ -91,6 +91,8 @@ pub struct PersistentOutputState {
|
||||||
pub transform: Cell<Transform>,
|
pub transform: Cell<Transform>,
|
||||||
pub scale: Cell<crate::scale::Scale>,
|
pub scale: Cell<crate::scale::Scale>,
|
||||||
pub pos: Cell<(i32, i32)>,
|
pub pos: Cell<(i32, i32)>,
|
||||||
|
pub vrr_mode: Cell<&'static VrrMode>,
|
||||||
|
pub vrr_cursor_hz: Cell<Option<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash)]
|
#[derive(Eq, PartialEq, Hash)]
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ impl TestBackend {
|
||||||
width_mm: 80,
|
width_mm: 80,
|
||||||
height_mm: 60,
|
height_mm: 60,
|
||||||
non_desktop: false,
|
non_desktop: false,
|
||||||
|
vrr_capable: false,
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
state: state.clone(),
|
state: state.clone(),
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
||||||
width_mm: 0,
|
width_mm: 0,
|
||||||
height_mm: 0,
|
height_mm: 0,
|
||||||
non_desktop: false,
|
non_desktop: false,
|
||||||
|
vrr_capable: false,
|
||||||
};
|
};
|
||||||
run.backend
|
run.backend
|
||||||
.state
|
.state
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ mod libinput;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod logind;
|
mod logind;
|
||||||
mod object;
|
mod object;
|
||||||
|
mod output_schedule;
|
||||||
mod pango;
|
mod pango;
|
||||||
mod pipewire;
|
mod pipewire;
|
||||||
mod portal;
|
mod portal;
|
||||||
|
|
|
||||||
196
src/output_schedule.rs
Normal file
196
src/output_schedule.rs
Normal 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))
|
||||||
|
}
|
||||||
16
src/state.rs
16
src/state.rs
|
|
@ -64,7 +64,7 @@ use {
|
||||||
time::Time,
|
time::Time,
|
||||||
tree::{
|
tree::{
|
||||||
ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, Node, NodeIds,
|
ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, Node, NodeIds,
|
||||||
NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase,
|
NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase, VrrMode,
|
||||||
WorkspaceNode,
|
WorkspaceNode,
|
||||||
},
|
},
|
||||||
utils::{
|
utils::{
|
||||||
|
|
@ -201,6 +201,8 @@ pub struct State {
|
||||||
pub tablet_tool_ids: TabletToolIds,
|
pub tablet_tool_ids: TabletToolIds,
|
||||||
pub tablet_pad_ids: TabletPadIds,
|
pub tablet_pad_ids: TabletPadIds,
|
||||||
pub damage_visualizer: DamageVisualizer,
|
pub damage_visualizer: DamageVisualizer,
|
||||||
|
pub default_vrr_mode: Cell<&'static VrrMode>,
|
||||||
|
pub default_vrr_cursor_hz: Cell<Option<f64>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Drop for State {
|
// impl Drop for State {
|
||||||
|
|
@ -730,13 +732,21 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn damage(&self, rect: Rect) {
|
pub fn damage(&self, rect: Rect) {
|
||||||
|
self.damage2(false, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn damage2(&self, cursor: bool, rect: Rect) {
|
||||||
if rect.is_empty() {
|
if rect.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.damage_visualizer.add(rect);
|
self.damage_visualizer.add(rect);
|
||||||
for output in self.root.outputs.lock().values() {
|
for output in self.root.outputs.lock().values() {
|
||||||
if output.global.pos.get().intersects(&rect) {
|
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() {
|
for output in self.root.outputs.lock().values() {
|
||||||
if let Some(hc) = output.hardware_cursor.get() {
|
if let Some(hc) = output.hardware_cursor.get() {
|
||||||
hc.set_enabled(false);
|
hc.set_enabled(false);
|
||||||
hc.commit();
|
hc.commit(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use {
|
||||||
backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo},
|
backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo},
|
||||||
globals::GlobalName,
|
globals::GlobalName,
|
||||||
ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal},
|
ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal},
|
||||||
|
output_schedule::OutputSchedule,
|
||||||
state::{ConnectorData, OutputData, State},
|
state::{ConnectorData, OutputData, State},
|
||||||
tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig},
|
tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig},
|
||||||
utils::{asyncevent::AsyncEvent, clonecell::CloneCell, hash_map_ext::HashMapExt},
|
utils::{asyncevent::AsyncEvent, clonecell::CloneCell, hash_map_ext::HashMapExt},
|
||||||
|
|
@ -122,6 +123,8 @@ impl ConnectorHandler {
|
||||||
transform: Default::default(),
|
transform: Default::default(),
|
||||||
scale: Default::default(),
|
scale: Default::default(),
|
||||||
pos: Cell::new((x1, 0)),
|
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
|
self.state
|
||||||
.persistent_output_states
|
.persistent_output_states
|
||||||
|
|
@ -140,6 +143,13 @@ impl ConnectorHandler {
|
||||||
&output_id,
|
&output_id,
|
||||||
&desired_state,
|
&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 {
|
let on = Rc::new(OutputNode {
|
||||||
id: self.state.node_ids.next(),
|
id: self.state.node_ids.next(),
|
||||||
workspaces: Default::default(),
|
workspaces: Default::default(),
|
||||||
|
|
@ -173,6 +183,7 @@ impl ConnectorHandler {
|
||||||
hardware_cursor_needs_render: Cell::new(false),
|
hardware_cursor_needs_render: Cell::new(false),
|
||||||
screencopies: Default::default(),
|
screencopies: Default::default(),
|
||||||
title_visible: Default::default(),
|
title_visible: Default::default(),
|
||||||
|
schedule,
|
||||||
});
|
});
|
||||||
on.update_visible();
|
on.update_visible();
|
||||||
on.update_rects();
|
on.update_rects();
|
||||||
|
|
@ -231,17 +242,22 @@ impl ConnectorHandler {
|
||||||
}
|
}
|
||||||
self.state.add_global(&global);
|
self.state.add_global(&global);
|
||||||
self.state.tree_changed();
|
self.state.tree_changed();
|
||||||
|
on.update_vrr_state();
|
||||||
'outer: loop {
|
'outer: loop {
|
||||||
while let Some(event) = self.data.connector.event() {
|
while let Some(event) = self.data.connector.event() {
|
||||||
match event {
|
match event {
|
||||||
ConnectorEvent::Disconnected => break 'outer,
|
ConnectorEvent::Disconnected => break 'outer,
|
||||||
ConnectorEvent::HardwareCursor(hc) => {
|
ConnectorEvent::HardwareCursor(hc) => {
|
||||||
|
on.schedule.set_hardware_cursor(&hc);
|
||||||
on.hardware_cursor.set(hc);
|
on.hardware_cursor.set(hc);
|
||||||
self.state.refresh_hardware_cursors();
|
self.state.refresh_hardware_cursors();
|
||||||
}
|
}
|
||||||
ConnectorEvent::ModeChanged(mode) => {
|
ConnectorEvent::ModeChanged(mode) => {
|
||||||
on.update_mode(mode);
|
on.update_mode(mode);
|
||||||
}
|
}
|
||||||
|
ConnectorEvent::VrrChanged(enabled) => {
|
||||||
|
on.schedule.set_vrr_enabled(enabled);
|
||||||
|
}
|
||||||
ev => unreachable!("received unexpected event {:?}", ev),
|
ev => unreachable!("received unexpected event {:?}", ev),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -286,7 +286,7 @@ impl ToolClient {
|
||||||
}
|
}
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct S {
|
struct S {
|
||||||
jay_compositor: Cell<Option<u32>>,
|
jay_compositor: Cell<Option<(u32, u32)>>,
|
||||||
jay_damage_tracking: Cell<Option<u32>>,
|
jay_damage_tracking: Cell<Option<u32>>,
|
||||||
}
|
}
|
||||||
let s = Rc::new(S::default());
|
let s = Rc::new(S::default());
|
||||||
|
|
@ -297,7 +297,7 @@ impl ToolClient {
|
||||||
});
|
});
|
||||||
wl_registry::Global::handle(self, registry, s.clone(), |s, g| {
|
wl_registry::Global::handle(self, registry, s.clone(), |s, g| {
|
||||||
if g.interface == JayCompositor.name() {
|
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() {
|
} else if g.interface == JayDamageTracking.name() {
|
||||||
s.jay_damage_tracking.set(Some(g.name));
|
s.jay_damage_tracking.set(Some(g.name));
|
||||||
}
|
}
|
||||||
|
|
@ -328,9 +328,9 @@ impl ToolClient {
|
||||||
let id: JayCompositorId = self.id();
|
let id: JayCompositorId = self.id();
|
||||||
self.send(wl_registry::Bind {
|
self.send(wl_registry::Bind {
|
||||||
self_id: s.registry,
|
self_id: s.registry,
|
||||||
name: s.jay_compositor,
|
name: s.jay_compositor.0,
|
||||||
interface: JayCompositor.name(),
|
interface: JayCompositor.name(),
|
||||||
version: 1,
|
version: s.jay_compositor.1.min(2),
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
});
|
});
|
||||||
self.jay_compositor.set(Some(id));
|
self.jay_compositor.set(Some(id));
|
||||||
|
|
@ -361,7 +361,7 @@ impl ToolClient {
|
||||||
|
|
||||||
pub struct Singletons {
|
pub struct Singletons {
|
||||||
registry: WlRegistryId,
|
registry: WlRegistryId,
|
||||||
pub jay_compositor: u32,
|
pub jay_compositor: (u32, u32),
|
||||||
pub jay_damage_tracking: Option<u32>,
|
pub jay_damage_tracking: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ use {
|
||||||
zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1},
|
zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1},
|
||||||
SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor,
|
SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor,
|
||||||
},
|
},
|
||||||
|
wp_content_type_v1::ContentType,
|
||||||
zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP},
|
zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP},
|
||||||
zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1,
|
zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1,
|
||||||
},
|
},
|
||||||
|
output_schedule::OutputSchedule,
|
||||||
rect::Rect,
|
rect::Rect,
|
||||||
renderer::Renderer,
|
renderer::Renderer,
|
||||||
scale::Scale,
|
scale::Scale,
|
||||||
|
|
@ -41,7 +43,7 @@ use {
|
||||||
wire::{JayOutputId, JayScreencastId, ZwlrScreencopyFrameV1Id},
|
wire::{JayOutputId, JayScreencastId, ZwlrScreencopyFrameV1Id},
|
||||||
},
|
},
|
||||||
ahash::AHashMap,
|
ahash::AHashMap,
|
||||||
jay_config::video::Transform,
|
jay_config::video::{Transform, VrrMode as ConfigVrrMode},
|
||||||
smallvec::SmallVec,
|
smallvec::SmallVec,
|
||||||
std::{
|
std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
|
|
@ -77,6 +79,7 @@ pub struct OutputNode {
|
||||||
pub screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc<JayScreencast>>,
|
pub screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc<JayScreencast>>,
|
||||||
pub screencopies: CopyHashMap<(ClientId, ZwlrScreencopyFrameV1Id), Rc<ZwlrScreencopyFrameV1>>,
|
pub screencopies: CopyHashMap<(ClientId, ZwlrScreencopyFrameV1Id), Rc<ZwlrScreencopyFrameV1>>,
|
||||||
pub title_visible: Cell<bool>,
|
pub title_visible: Cell<bool>,
|
||||||
|
pub schedule: Rc<OutputSchedule>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
|
|
@ -785,6 +788,39 @@ impl OutputNode {
|
||||||
self.schedule_update_render_data();
|
self.schedule_update_render_data();
|
||||||
self.state.tree_changed();
|
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 {
|
pub struct OutputTitle {
|
||||||
|
|
@ -1084,3 +1120,68 @@ pub fn calculate_logical_size(
|
||||||
}
|
}
|
||||||
(width, height)
|
(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ impl WorkspaceNode {
|
||||||
surface.send_feedback(&fb);
|
surface.send_feedback(&fb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.output.get().update_vrr_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_fullscreen_node(&self) {
|
pub fn remove_fullscreen_node(&self) {
|
||||||
|
|
@ -194,6 +195,7 @@ impl WorkspaceNode {
|
||||||
surface.send_feedback(&fb);
|
surface.send_feedback(&fb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.output.get().update_vrr_state();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use {
|
||||||
logging::LogLevel,
|
logging::LogLevel,
|
||||||
status::MessageFormat,
|
status::MessageFormat,
|
||||||
theme::Color,
|
theme::Color,
|
||||||
video::{GfxApi, Transform},
|
video::{GfxApi, Transform, VrrMode},
|
||||||
Axis, Direction, Workspace,
|
Axis, Direction, Workspace,
|
||||||
},
|
},
|
||||||
std::{
|
std::{
|
||||||
|
|
@ -206,6 +206,7 @@ pub struct Output {
|
||||||
pub scale: Option<f64>,
|
pub scale: Option<f64>,
|
||||||
pub transform: Option<Transform>,
|
pub transform: Option<Transform>,
|
||||||
pub mode: Option<Mode>,
|
pub mode: Option<Mode>,
|
||||||
|
pub vrr: Option<Vrr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -285,6 +286,12 @@ pub struct RepeatRate {
|
||||||
pub delay: i32,
|
pub delay: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Vrr {
|
||||||
|
pub mode: Option<VrrMode>,
|
||||||
|
pub cursor_hz: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Shortcut {
|
pub struct Shortcut {
|
||||||
pub mask: Modifiers,
|
pub mask: Modifiers,
|
||||||
|
|
@ -318,6 +325,7 @@ pub struct Config {
|
||||||
pub explicit_sync_enabled: Option<bool>,
|
pub explicit_sync_enabled: Option<bool>,
|
||||||
pub focus_follows_mouse: bool,
|
pub focus_follows_mouse: bool,
|
||||||
pub window_management_key: Option<ModifiedKeySym>,
|
pub window_management_key: Option<ModifiedKeySym>,
|
||||||
|
pub vrr: Option<Vrr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ mod repeat_rate;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
mod status;
|
mod status;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
mod vrr;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum StringParserError {
|
pub enum StringParserError {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use {
|
||||||
},
|
},
|
||||||
status::StatusParser,
|
status::StatusParser,
|
||||||
theme::ThemeParser,
|
theme::ThemeParser,
|
||||||
|
vrr::VrrParser,
|
||||||
},
|
},
|
||||||
spanned::SpannedErrorExt,
|
spanned::SpannedErrorExt,
|
||||||
Action, Config, Theme,
|
Action, Config, Theme,
|
||||||
|
|
@ -106,6 +107,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
complex_shortcuts_val,
|
complex_shortcuts_val,
|
||||||
focus_follows_mouse,
|
focus_follows_mouse,
|
||||||
window_management_key_val,
|
window_management_key_val,
|
||||||
|
vrr_val,
|
||||||
),
|
),
|
||||||
) = ext.extract((
|
) = ext.extract((
|
||||||
(
|
(
|
||||||
|
|
@ -138,6 +140,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
opt(val("complex-shortcuts")),
|
opt(val("complex-shortcuts")),
|
||||||
recover(opt(bol("focus-follows-mouse"))),
|
recover(opt(bol("focus-follows-mouse"))),
|
||||||
recover(opt(str("window-management-key"))),
|
recover(opt(str("window-management-key"))),
|
||||||
|
opt(val("vrr")),
|
||||||
),
|
),
|
||||||
))?;
|
))?;
|
||||||
let mut keymap = None;
|
let mut keymap = None;
|
||||||
|
|
@ -302,6 +305,15 @@ impl Parser for ConfigParser<'_> {
|
||||||
window_management_key = Some(key);
|
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 {
|
Ok(Config {
|
||||||
keymap,
|
keymap,
|
||||||
repeat_rate,
|
repeat_rate,
|
||||||
|
|
@ -326,6 +338,7 @@ impl Parser for ConfigParser<'_> {
|
||||||
idle,
|
idle,
|
||||||
focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true),
|
focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true),
|
||||||
window_management_key,
|
window_management_key,
|
||||||
|
vrr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use {
|
||||||
parsers::{
|
parsers::{
|
||||||
mode::ModeParser,
|
mode::ModeParser,
|
||||||
output_match::{OutputMatchParser, OutputMatchParserError},
|
output_match::{OutputMatchParser, OutputMatchParserError},
|
||||||
|
vrr::VrrParser,
|
||||||
},
|
},
|
||||||
Output,
|
Output,
|
||||||
},
|
},
|
||||||
|
|
@ -46,7 +47,7 @@ impl<'a> Parser for OutputParser<'a> {
|
||||||
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
table: &IndexMap<Spanned<String>, Spanned<Value>>,
|
||||||
) -> ParseResult<Self> {
|
) -> ParseResult<Self> {
|
||||||
let mut ext = Extractor::new(self.cx, span, table);
|
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")),
|
opt(str("name")),
|
||||||
val("match"),
|
val("match"),
|
||||||
recover(opt(s32("x"))),
|
recover(opt(s32("x"))),
|
||||||
|
|
@ -54,6 +55,7 @@ impl<'a> Parser for OutputParser<'a> {
|
||||||
recover(opt(fltorint("scale"))),
|
recover(opt(fltorint("scale"))),
|
||||||
recover(opt(str("transform"))),
|
recover(opt(str("transform"))),
|
||||||
opt(val("mode")),
|
opt(val("mode")),
|
||||||
|
opt(val("vrr")),
|
||||||
))?;
|
))?;
|
||||||
let transform = match transform {
|
let transform = match transform {
|
||||||
None => None,
|
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 {
|
Ok(Output {
|
||||||
name: name.despan().map(|v| v.to_string()),
|
name: name.despan().map(|v| v.to_string()),
|
||||||
match_: match_val.parse_map(&mut OutputMatchParser(self.cx))?,
|
match_: match_val.parse_map(&mut OutputMatchParser(self.cx))?,
|
||||||
|
|
@ -104,6 +115,7 @@ impl<'a> Parser for OutputParser<'a> {
|
||||||
scale: scale.despan(),
|
scale: scale.despan(),
|
||||||
transform,
|
transform,
|
||||||
mode,
|
mode,
|
||||||
|
vrr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
116
toml-config/src/config/parsers/vrr.rs
Normal file
116
toml-config/src/config/parsers/vrr.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,8 @@ use {
|
||||||
video::{
|
video::{
|
||||||
connectors, drm_devices, on_connector_connected, on_connector_disconnected,
|
connectors, drm_devices, on_connector_connected, on_connector_disconnected,
|
||||||
on_graphics_initialized, on_new_connector, on_new_drm_device,
|
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},
|
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())),
|
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
|
.seat
|
||||||
.set_window_management_key(window_management_key);
|
.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 {
|
fn create_command(exec: &Exec) -> Command {
|
||||||
|
|
|
||||||
|
|
@ -577,6 +577,10 @@
|
||||||
"window-management-key": {
|
"window-management-key": {
|
||||||
"type": "string",
|
"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"
|
"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": []
|
"required": []
|
||||||
|
|
@ -1023,6 +1027,10 @@
|
||||||
"mode": {
|
"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",
|
"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"
|
"$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": [
|
"required": [
|
||||||
|
|
@ -1234,6 +1242,45 @@
|
||||||
"flip-rotate-180",
|
"flip-rotate-180",
|
||||||
"flip-rotate-270"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1110,6 +1110,22 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a string.
|
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>
|
<a name="types-Connector"></a>
|
||||||
### `Connector`
|
### `Connector`
|
||||||
|
|
@ -2166,6 +2182,22 @@ The table has the following fields:
|
||||||
|
|
||||||
The value of this field should be a [Mode](#types-Mode).
|
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>
|
<a name="types-OutputMatch"></a>
|
||||||
### `OutputMatch`
|
### `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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1558,6 +1558,21 @@ Output:
|
||||||
|
|
||||||
If the refresh rate is not specified, the first mode with the specified width and
|
If the refresh rate is not specified, the first mode with the specified width and
|
||||||
height is used.
|
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:
|
Transform:
|
||||||
|
|
@ -2150,6 +2165,21 @@ Config:
|
||||||
```toml
|
```toml
|
||||||
window-management-key = "Alt_L"
|
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:
|
Idle:
|
||||||
|
|
@ -2267,3 +2297,73 @@ ComplexShortcut:
|
||||||
|
|
||||||
Audio will be un-muted once `x` key is released, regardless of any other keys
|
Audio will be un-muted once `x` key is released, regardless of any other keys
|
||||||
that are pressed at the time.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,16 @@ request set_non_desktop {
|
||||||
non_desktop: u32,
|
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
|
# events
|
||||||
|
|
||||||
event global {
|
event global {
|
||||||
|
|
@ -112,3 +122,13 @@ event non_desktop_output {
|
||||||
width_mm: i32,
|
width_mm: i32,
|
||||||
height_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),
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue