metal: implement VRR
This commit is contained in:
parent
cd09e57568
commit
2d7c13b0b4
35 changed files with 1320 additions and 91 deletions
|
|
@ -71,6 +71,7 @@ pub struct MonitorInfo {
|
|||
pub width_mm: i32,
|
||||
pub height_mm: i32,
|
||||
pub non_desktop: bool,
|
||||
pub vrr_capable: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
|
@ -108,6 +109,9 @@ pub trait Connector {
|
|||
fn drm_object_id(&self) -> Option<DrmConnector> {
|
||||
None
|
||||
}
|
||||
fn set_vrr_enabled(&self, enabled: bool) {
|
||||
let _ = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -119,6 +123,7 @@ pub enum ConnectorEvent {
|
|||
ModeChanged(Mode),
|
||||
Unavailable,
|
||||
Available,
|
||||
VrrChanged(bool),
|
||||
}
|
||||
|
||||
pub trait HardwareCursor: Debug {
|
||||
|
|
@ -127,7 +132,8 @@ pub trait HardwareCursor: Debug {
|
|||
fn set_position(&self, x: i32, y: i32);
|
||||
fn swap_buffer(&self);
|
||||
fn set_sync_file(&self, sync_file: Option<SyncFile>);
|
||||
fn commit(&self);
|
||||
fn commit(&self, schedule_present: bool);
|
||||
fn schedule_present(&self) -> bool;
|
||||
fn size(&self) -> (i32, i32);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ pub struct MetalDrmDevice {
|
|||
pub on_change: OnChange<crate::backend::DrmEvent>,
|
||||
pub direct_scanout_enabled: Cell<Option<bool>>,
|
||||
pub is_nvidia: bool,
|
||||
pub is_amd: bool,
|
||||
pub lease_ids: MetalLeaseIds,
|
||||
pub leases: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
||||
pub leases_to_break: CopyHashMap<MetalLeaseId, MetalLeaseData>,
|
||||
|
|
@ -299,6 +300,8 @@ pub struct ConnectorDisplayData {
|
|||
pub refresh: u32,
|
||||
pub non_desktop: bool,
|
||||
pub non_desktop_effective: bool,
|
||||
pub vrr_capable: bool,
|
||||
pub vrr_requested: bool,
|
||||
|
||||
pub monitor_manufacturer: String,
|
||||
pub monitor_name: String,
|
||||
|
|
@ -319,6 +322,10 @@ impl ConnectorDisplayData {
|
|||
&& self.monitor_name == other.monitor_name
|
||||
&& self.monitor_serial_number == other.monitor_serial_number
|
||||
}
|
||||
|
||||
fn should_enable_vrr(&self) -> bool {
|
||||
self.vrr_requested && self.vrr_capable
|
||||
}
|
||||
}
|
||||
|
||||
linear_ids!(MetalLeaseIds, MetalLeaseId, u64);
|
||||
|
|
@ -417,6 +424,7 @@ pub struct MetalConnector {
|
|||
pub can_present: Cell<bool>,
|
||||
pub has_damage: Cell<bool>,
|
||||
pub cursor_changed: Cell<bool>,
|
||||
pub cursor_scheduled: Cell<bool>,
|
||||
pub next_flip_nsec: Cell<u64>,
|
||||
|
||||
pub display: RefCell<ConnectorDisplayData>,
|
||||
|
|
@ -503,7 +511,7 @@ impl HardwareCursor for MetalHardwareCursor {
|
|||
self.have_changes.set(true);
|
||||
}
|
||||
|
||||
fn commit(&self) {
|
||||
fn commit(&self, schedule_present: bool) {
|
||||
if self.generation != self.connector.cursor_generation.get() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -520,8 +528,20 @@ impl HardwareCursor for MetalHardwareCursor {
|
|||
}
|
||||
self.connector.cursor_sync_file.set(self.sync_file.take());
|
||||
self.connector.cursor_changed.set(true);
|
||||
if self.connector.can_present.get() {
|
||||
self.connector.schedule_present();
|
||||
if schedule_present {
|
||||
self.schedule_present();
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_present(&self) -> bool {
|
||||
if self.connector.cursor_changed.get() {
|
||||
self.connector.cursor_scheduled.set(true);
|
||||
if self.connector.can_present.get() {
|
||||
self.connector.schedule_present();
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -604,6 +624,19 @@ impl MetalConnector {
|
|||
}
|
||||
}
|
||||
|
||||
fn send_vrr_enabled(&self) {
|
||||
match self.frontend_state.get() {
|
||||
FrontState::Removed
|
||||
| FrontState::Disconnected
|
||||
| FrontState::Unavailable
|
||||
| FrontState::Connected { non_desktop: true } => return,
|
||||
FrontState::Connected { non_desktop: false } => {}
|
||||
}
|
||||
if let Some(crtc) = self.crtc.get() {
|
||||
self.send_event(ConnectorEvent::VrrChanged(crtc.vrr_enabled.value.get()));
|
||||
}
|
||||
}
|
||||
|
||||
fn send_hardware_cursor(self: &Rc<Self>) {
|
||||
match self.frontend_state.get() {
|
||||
FrontState::Removed
|
||||
|
|
@ -894,7 +927,7 @@ impl MetalConnector {
|
|||
Some(crtc) => crtc,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if (!self.has_damage.get() && !self.cursor_changed.get()) || !self.can_present.get() {
|
||||
if (!self.has_damage.get() && !self.cursor_scheduled.get()) || !self.can_present.get() {
|
||||
return Ok(());
|
||||
}
|
||||
if !crtc.active.value.get() {
|
||||
|
|
@ -908,6 +941,9 @@ impl MetalConnector {
|
|||
Some(b) => b,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
let Some(node) = self.state.root.outputs.get(&self.connector_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
let cursor = self.cursor_plane.get();
|
||||
let mut new_fb = None;
|
||||
let mut changes = self.master.change();
|
||||
|
|
@ -915,46 +951,52 @@ impl MetalConnector {
|
|||
if !self.backend.check_render_context(&self.dev) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(node) = self.state.root.outputs.get(&self.connector_id) {
|
||||
let buffer = &buffers[self.next_buffer.get() % buffers.len()];
|
||||
let mut rr = self.render_result.borrow_mut();
|
||||
rr.output_id = node.id;
|
||||
let fb =
|
||||
self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?;
|
||||
rr.dispatch_frame_requests(self.state.now_msec());
|
||||
let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) =
|
||||
match &fb.direct_scanout_data {
|
||||
None => {
|
||||
let plane_w = plane.mode_w.get();
|
||||
let plane_h = plane.mode_h.get();
|
||||
(0, 0, plane_w, plane_h, plane_w, plane_h)
|
||||
}
|
||||
Some(dsd) => {
|
||||
let p = &dsd.position;
|
||||
(
|
||||
p.crtc_x,
|
||||
p.crtc_y,
|
||||
p.crtc_width,
|
||||
p.crtc_height,
|
||||
p.src_width,
|
||||
p.src_height,
|
||||
)
|
||||
}
|
||||
};
|
||||
let in_fence = fb.sync_file.as_ref().map(|s| s.raw()).unwrap_or(-1);
|
||||
changes.change_object(plane.id, |c| {
|
||||
c.change(plane.fb_id, fb.fb.id().0 as _);
|
||||
c.change(plane.src_w.id, (src_width as u64) << 16);
|
||||
c.change(plane.src_h.id, (src_height as u64) << 16);
|
||||
c.change(plane.crtc_x.id, crtc_x as u64);
|
||||
c.change(plane.crtc_y.id, crtc_y as u64);
|
||||
c.change(plane.crtc_w.id, crtc_w as u64);
|
||||
c.change(plane.crtc_h.id, crtc_h as u64);
|
||||
if !self.dev.is_nvidia {
|
||||
c.change(plane.in_fence_fd, in_fence as u64);
|
||||
let buffer = &buffers[self.next_buffer.get() % buffers.len()];
|
||||
let mut rr = self.render_result.borrow_mut();
|
||||
rr.output_id = node.id;
|
||||
let fb = self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?;
|
||||
rr.dispatch_frame_requests(self.state.now_msec());
|
||||
let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) =
|
||||
match &fb.direct_scanout_data {
|
||||
None => {
|
||||
let plane_w = plane.mode_w.get();
|
||||
let plane_h = plane.mode_h.get();
|
||||
(0, 0, plane_w, plane_h, plane_w, plane_h)
|
||||
}
|
||||
});
|
||||
new_fb = Some(fb);
|
||||
Some(dsd) => {
|
||||
let p = &dsd.position;
|
||||
(
|
||||
p.crtc_x,
|
||||
p.crtc_y,
|
||||
p.crtc_width,
|
||||
p.crtc_height,
|
||||
p.src_width,
|
||||
p.src_height,
|
||||
)
|
||||
}
|
||||
};
|
||||
let in_fence = fb.sync_file.as_ref().map(|s| s.raw()).unwrap_or(-1);
|
||||
changes.change_object(plane.id, |c| {
|
||||
c.change(plane.fb_id, fb.fb.id().0 as _);
|
||||
c.change(plane.src_w.id, (src_width as u64) << 16);
|
||||
c.change(plane.src_h.id, (src_height as u64) << 16);
|
||||
c.change(plane.crtc_x.id, crtc_x as u64);
|
||||
c.change(plane.crtc_y.id, crtc_y as u64);
|
||||
c.change(plane.crtc_w.id, crtc_w as u64);
|
||||
c.change(plane.crtc_h.id, crtc_h as u64);
|
||||
if !self.dev.is_nvidia {
|
||||
c.change(plane.in_fence_fd, in_fence as u64);
|
||||
}
|
||||
});
|
||||
new_fb = Some(fb);
|
||||
} else {
|
||||
if self.dev.is_amd && crtc.vrr_enabled.value.get() {
|
||||
// Work around https://gitlab.freedesktop.org/drm/amd/-/issues/2186
|
||||
if let Some(fb) = &*self.active_framebuffer.borrow() {
|
||||
changes.change_object(plane.id, |c| {
|
||||
c.change(plane.fb_id, fb.fb.id().0 as _);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut cursor_swap_buffer = false;
|
||||
|
|
@ -1027,7 +1069,8 @@ impl MetalConnector {
|
|||
.discard_presentation_feedback();
|
||||
Err(MetalError::Commit(e))
|
||||
} else {
|
||||
self.perform_screencopies(&new_fb);
|
||||
node.schedule.presented();
|
||||
self.perform_screencopies(&new_fb, &node);
|
||||
if let Some(fb) = new_fb {
|
||||
if fb.direct_scanout_data.is_none() {
|
||||
self.next_buffer.fetch_add(1);
|
||||
|
|
@ -1042,14 +1085,12 @@ impl MetalConnector {
|
|||
self.can_present.set(false);
|
||||
self.has_damage.set(false);
|
||||
self.cursor_changed.set(false);
|
||||
self.cursor_scheduled.set(false);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_screencopies(&self, new_fb: &Option<PresentFb>) {
|
||||
let Some(output) = self.state.root.outputs.get(&self.connector_id) else {
|
||||
return;
|
||||
};
|
||||
fn perform_screencopies(&self, new_fb: &Option<PresentFb>, output: &OutputNode) {
|
||||
let active_fb;
|
||||
let fb = match &new_fb {
|
||||
Some(f) => f,
|
||||
|
|
@ -1173,6 +1214,17 @@ impl MetalConnector {
|
|||
log::error!("Tried to send available event in invalid state: {state:?}");
|
||||
}
|
||||
},
|
||||
ConnectorEvent::VrrChanged(_) => match state {
|
||||
FrontState::Connected { non_desktop: false } => {
|
||||
self.on_change.send_event(event);
|
||||
}
|
||||
FrontState::Connected { non_desktop: true }
|
||||
| FrontState::Removed
|
||||
| FrontState::Disconnected
|
||||
| FrontState::Unavailable => {
|
||||
log::error!("Tried to send vrr-changed event in invalid state: {state:?}");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1296,6 +1348,32 @@ impl Connector for MetalConnector {
|
|||
fn drm_object_id(&self) -> Option<DrmConnector> {
|
||||
Some(self.id)
|
||||
}
|
||||
|
||||
fn set_vrr_enabled(&self, enabled: bool) {
|
||||
if self.frontend_state.get() != (FrontState::Connected { non_desktop: false }) {
|
||||
return;
|
||||
}
|
||||
let dd = &mut *self.display.borrow_mut();
|
||||
let old_enabled = dd.should_enable_vrr();
|
||||
dd.vrr_requested = enabled;
|
||||
let new_enabled = dd.should_enable_vrr();
|
||||
if old_enabled == new_enabled {
|
||||
return;
|
||||
}
|
||||
let Some(crtc) = self.crtc.get() else {
|
||||
return;
|
||||
};
|
||||
let mut change = self.master.change();
|
||||
change.change_object(crtc.id, |c| {
|
||||
c.change(crtc.vrr_enabled.id, new_enabled as _);
|
||||
});
|
||||
if let Err(e) = change.commit(0, 0) {
|
||||
log::error!("Could not change vrr mode: {}", ErrorFmt(e));
|
||||
return;
|
||||
}
|
||||
crtc.vrr_enabled.value.set(new_enabled);
|
||||
self.send_vrr_enabled();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MetalCrtc {
|
||||
|
|
@ -1312,6 +1390,7 @@ pub struct MetalCrtc {
|
|||
pub active: MutableProperty<bool>,
|
||||
pub mode_id: MutableProperty<DrmBlob>,
|
||||
pub out_fence_ptr: DrmProperty,
|
||||
pub vrr_enabled: MutableProperty<bool>,
|
||||
|
||||
pub mode_blob: CloneCell<Option<Rc<PropBlob>>>,
|
||||
}
|
||||
|
|
@ -1435,6 +1514,7 @@ fn create_connector(
|
|||
display: RefCell::new(display),
|
||||
frontend_state: Cell::new(FrontState::Disconnected),
|
||||
cursor_changed: Cell::new(false),
|
||||
cursor_scheduled: Cell::new(false),
|
||||
cursor_front_buffer: Default::default(),
|
||||
cursor_swap_buffer: Cell::new(false),
|
||||
cursor_sync_file: Default::default(),
|
||||
|
|
@ -1545,6 +1625,10 @@ fn create_connector_display_data(
|
|||
let props = collect_properties(&dev.master, connector)?;
|
||||
let connector_type = ConnectorType::from_drm(info.connector_type);
|
||||
let non_desktop = props.get("non-desktop")?.value.get() != 0;
|
||||
let vrr_capable = match props.get("vrr_capable") {
|
||||
Ok(c) => c.value.get() == 1,
|
||||
Err(_) => false,
|
||||
};
|
||||
Ok(ConnectorDisplayData {
|
||||
crtc_id: props.get("CRTC_ID")?.map(|v| DrmCrtc(v as _)),
|
||||
crtcs,
|
||||
|
|
@ -1553,6 +1637,8 @@ fn create_connector_display_data(
|
|||
refresh,
|
||||
non_desktop,
|
||||
non_desktop_effective: non_desktop_override.unwrap_or(non_desktop),
|
||||
vrr_capable,
|
||||
vrr_requested: false,
|
||||
monitor_manufacturer: manufacturer,
|
||||
monitor_name: name,
|
||||
monitor_serial_number: serial_number,
|
||||
|
|
@ -1607,6 +1693,7 @@ fn create_crtc(
|
|||
active: props.get("ACTIVE")?.map(|v| v == 1),
|
||||
mode_id: props.get("MODE_ID")?.map(|v| DrmBlob(v as u32)),
|
||||
out_fence_ptr: props.get("OUT_FENCE_PTR")?.id,
|
||||
vrr_enabled: props.get("VRR_ENABLED")?.map(|v| v == 1),
|
||||
mode_blob: Default::default(),
|
||||
})
|
||||
}
|
||||
|
|
@ -1876,6 +1963,7 @@ impl MetalBackend {
|
|||
dd.mode = Some(mode.clone());
|
||||
}
|
||||
}
|
||||
dd.vrr_requested = old.vrr_requested;
|
||||
}
|
||||
mem::swap(old.deref_mut(), &mut dd);
|
||||
match c.frontend_state.get() {
|
||||
|
|
@ -1963,8 +2051,10 @@ impl MetalBackend {
|
|||
width_mm: dd.mm_width as _,
|
||||
height_mm: dd.mm_height as _,
|
||||
non_desktop: dd.non_desktop_effective,
|
||||
vrr_capable: dd.vrr_capable,
|
||||
}));
|
||||
connector.send_hardware_cursor();
|
||||
connector.send_vrr_enabled();
|
||||
}
|
||||
|
||||
pub fn create_drm_device(
|
||||
|
|
@ -2030,9 +2120,11 @@ impl MetalBackend {
|
|||
};
|
||||
|
||||
let mut is_nvidia = false;
|
||||
let mut is_amd = false;
|
||||
match gbm.drm.version() {
|
||||
Ok(v) => {
|
||||
is_nvidia = v.name.contains_str("nvidia");
|
||||
is_amd = v.name.contains_str("amdgpu");
|
||||
if is_nvidia {
|
||||
log::warn!(
|
||||
"Device {} use the nvidia driver. IN_FENCE_FD will not be used.",
|
||||
|
|
@ -2068,6 +2160,7 @@ impl MetalBackend {
|
|||
on_change: Default::default(),
|
||||
direct_scanout_enabled: Default::default(),
|
||||
is_nvidia,
|
||||
is_amd,
|
||||
lease_ids: Default::default(),
|
||||
leases: Default::default(),
|
||||
leases_to_break: Default::default(),
|
||||
|
|
@ -2123,6 +2216,7 @@ impl MetalBackend {
|
|||
for c in dev.dev.crtcs.values() {
|
||||
let props = collect_untyped_properties(master, c.id)?;
|
||||
c.active.value.set(get(&props, c.active.id)? != 0);
|
||||
c.vrr_enabled.value.set(get(&props, c.vrr_enabled.id)? != 0);
|
||||
c.mode_id
|
||||
.value
|
||||
.set(DrmBlob(get(&props, c.mode_id.id)? as _));
|
||||
|
|
@ -2144,6 +2238,7 @@ impl MetalBackend {
|
|||
connector.can_present.set(true);
|
||||
connector.has_damage.set(true);
|
||||
connector.cursor_changed.set(true);
|
||||
connector.cursor_scheduled.set(true);
|
||||
}
|
||||
if dev.unprocessed_change.get() {
|
||||
return self.handle_drm_change_(dev, false);
|
||||
|
|
@ -2204,7 +2299,7 @@ impl MetalBackend {
|
|||
if let Some(fb) = connector.next_framebuffer.take() {
|
||||
*connector.active_framebuffer.borrow_mut() = Some(fb);
|
||||
}
|
||||
if connector.has_damage.get() || connector.cursor_changed.get() {
|
||||
if connector.has_damage.get() || connector.cursor_scheduled.get() {
|
||||
connector.schedule_present();
|
||||
}
|
||||
let dd = connector.display.borrow_mut();
|
||||
|
|
@ -2282,10 +2377,12 @@ impl MetalBackend {
|
|||
crtc.connector.set(None);
|
||||
crtc.active.value.set(false);
|
||||
crtc.mode_id.value.set(DrmBlob::NONE);
|
||||
crtc.vrr_enabled.value.set(false);
|
||||
changes.change_object(crtc.id, |c| {
|
||||
c.change(crtc.active.id, 0);
|
||||
c.change(crtc.mode_id.id, 0);
|
||||
c.change(crtc.out_fence_ptr, 0);
|
||||
c.change(crtc.vrr_enabled.id, 0);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -2483,6 +2580,7 @@ impl MetalBackend {
|
|||
continue;
|
||||
}
|
||||
connector.send_hardware_cursor();
|
||||
connector.send_vrr_enabled();
|
||||
connector.update_drm_feedback();
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -2490,6 +2588,7 @@ impl MetalBackend {
|
|||
|
||||
fn can_use_current_drm_mode(&self, dev: &Rc<MetalDrmDeviceData>) -> bool {
|
||||
let mut used_crtcs = AHashSet::new();
|
||||
let mut vrr_crtcs = AHashSet::new();
|
||||
let mut used_planes = AHashSet::new();
|
||||
|
||||
for connector in dev.connectors.lock().values() {
|
||||
|
|
@ -2507,6 +2606,9 @@ impl MetalBackend {
|
|||
return false;
|
||||
}
|
||||
used_crtcs.insert(crtc_id);
|
||||
if dd.should_enable_vrr() {
|
||||
vrr_crtcs.insert(crtc_id);
|
||||
}
|
||||
let crtc = dev.dev.crtcs.get(&crtc_id).unwrap();
|
||||
connector.crtc.set(Some(crtc.clone()));
|
||||
crtc.connector.set(Some(connector.clone()));
|
||||
|
|
@ -2558,6 +2660,11 @@ impl MetalBackend {
|
|||
c.change(crtc.active.id, 0);
|
||||
}
|
||||
c.change(crtc.out_fence_ptr, 0);
|
||||
let vrr_requested = vrr_crtcs.contains(&crtc.id);
|
||||
if crtc.vrr_enabled.value.get() != vrr_requested {
|
||||
c.change(crtc.vrr_enabled.id, vrr_requested as _);
|
||||
crtc.vrr_enabled.value.set(vrr_requested);
|
||||
}
|
||||
});
|
||||
}
|
||||
if let Err(e) = changes.commit(flags, 0) {
|
||||
|
|
@ -2748,6 +2855,7 @@ impl MetalBackend {
|
|||
changes.change_object(crtc.id, |c| {
|
||||
c.change(crtc.active.id, 1);
|
||||
c.change(crtc.mode_id.id, mode_blob.id().0 as _);
|
||||
c.change(crtc.vrr_enabled.id, dd.should_enable_vrr() as _);
|
||||
});
|
||||
connector.crtc.set(Some(crtc.clone()));
|
||||
dd.crtc_id.value.set(crtc.id);
|
||||
|
|
@ -2755,6 +2863,7 @@ impl MetalBackend {
|
|||
crtc.active.value.set(true);
|
||||
crtc.mode_id.value.set(mode_blob.id());
|
||||
crtc.mode_blob.set(Some(Rc::new(mode_blob)));
|
||||
crtc.vrr_enabled.value.set(dd.should_enable_vrr() as _);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -2894,6 +3003,7 @@ impl MetalBackend {
|
|||
}
|
||||
connector.has_damage.set(true);
|
||||
connector.cursor_changed.set(true);
|
||||
connector.cursor_scheduled.set(true);
|
||||
connector.schedule_present();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -575,6 +575,7 @@ impl XBackend {
|
|||
width_mm: output.width.get(),
|
||||
height_mm: output.height.get(),
|
||||
non_desktop: false,
|
||||
vrr_capable: false,
|
||||
}));
|
||||
output.changed();
|
||||
self.present(output).await;
|
||||
|
|
|
|||
134
src/cli/randr.rs
134
src/cli/randr.rs
|
|
@ -3,16 +3,17 @@ use {
|
|||
cli::GlobalArgs,
|
||||
scale::Scale,
|
||||
tools::tool_client::{with_tool_client, Handle, ToolClient},
|
||||
utils::transform_ext::TransformExt,
|
||||
utils::{errorfmt::ErrorFmt, transform_ext::TransformExt},
|
||||
wire::{jay_compositor, jay_randr, JayRandrId},
|
||||
},
|
||||
clap::{Args, Subcommand, ValueEnum},
|
||||
isnt::std_1::vec::IsntVecExt,
|
||||
jay_config::video::Transform,
|
||||
jay_config::video::{Transform, VrrMode},
|
||||
std::{
|
||||
cell::RefCell,
|
||||
fmt::{Display, Formatter},
|
||||
rc::Rc,
|
||||
str::FromStr,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -117,6 +118,8 @@ pub enum OutputCommand {
|
|||
Disable,
|
||||
/// Override the display's non-desktop setting.
|
||||
NonDesktop(NonDesktopArgs),
|
||||
/// Change VRR settings.
|
||||
Vrr(VrrArgs),
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone)]
|
||||
|
|
@ -132,6 +135,46 @@ pub struct NonDesktopArgs {
|
|||
pub setting: NonDesktopType,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct VrrArgs {
|
||||
#[clap(subcommand)]
|
||||
pub command: VrrCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum VrrCommand {
|
||||
/// Sets the mode that determines when VRR is enabled.
|
||||
SetMode(SetModeArgs),
|
||||
/// Sets the maximum refresh rate of the cursor.
|
||||
SetCursorHz(CursorHzArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct SetModeArgs {
|
||||
#[clap(value_enum)]
|
||||
pub mode: VrrModeArg,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Copy, Clone, Hash, PartialEq)]
|
||||
pub enum VrrModeArg {
|
||||
/// VRR is never enabled.
|
||||
Never,
|
||||
/// VRR is always enabled.
|
||||
Always,
|
||||
/// VRR is enabled when one or more applications are displayed fullscreen.
|
||||
Variant1,
|
||||
/// VRR is enabled when a single application is displayed fullscreen.
|
||||
Variant2,
|
||||
/// VRR is enabled when a single game or video is displayed fullscreen.
|
||||
Variant3,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct CursorHzArgs {
|
||||
/// The rate at which the cursor will be updated on screen.
|
||||
pub rate: String,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct PositionArgs {
|
||||
/// The top-left x coordinate.
|
||||
|
|
@ -233,6 +276,10 @@ struct Output {
|
|||
pub current_mode: Option<Mode>,
|
||||
pub modes: Vec<Mode>,
|
||||
pub non_desktop: bool,
|
||||
pub vrr_capable: bool,
|
||||
pub vrr_enabled: bool,
|
||||
pub vrr_mode: VrrMode,
|
||||
pub vrr_cursor_hz: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
|
|
@ -399,6 +446,47 @@ impl Randr {
|
|||
non_desktop: a.setting as _,
|
||||
});
|
||||
}
|
||||
OutputCommand::Vrr(a) => {
|
||||
self.handle_error(randr, move |msg| {
|
||||
eprintln!("Could not change the VRR setting: {}", msg);
|
||||
});
|
||||
let parse_rate = |rate: &str| {
|
||||
if rate.eq_ignore_ascii_case("none") {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
match f64::from_str(rate) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
fatal!("Could not parse rate: {}", ErrorFmt(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match a.command {
|
||||
VrrCommand::SetMode(a) => {
|
||||
let mode = match a.mode {
|
||||
VrrModeArg::Never => VrrMode::NEVER,
|
||||
VrrModeArg::Always => VrrMode::ALWAYS,
|
||||
VrrModeArg::Variant1 => VrrMode::VARIANT_1,
|
||||
VrrModeArg::Variant2 => VrrMode::VARIANT_2,
|
||||
VrrModeArg::Variant3 => VrrMode::VARIANT_3,
|
||||
};
|
||||
tc.send(jay_randr::SetVrrMode {
|
||||
self_id: randr,
|
||||
output: &args.output,
|
||||
mode: mode.0,
|
||||
});
|
||||
}
|
||||
VrrCommand::SetCursorHz(r) => {
|
||||
let hz = parse_rate(&r.rate);
|
||||
tc.send(jay_randr::SetVrrCursorHz {
|
||||
self_id: randr,
|
||||
output: &args.output,
|
||||
hz,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tc.round_trip().await;
|
||||
}
|
||||
|
|
@ -513,6 +601,26 @@ impl Randr {
|
|||
println!(" non-desktop");
|
||||
return;
|
||||
}
|
||||
println!(" VRR capable: {}", o.vrr_capable);
|
||||
if o.vrr_capable {
|
||||
println!(" VRR enabled: {}", o.vrr_enabled);
|
||||
let mode_str;
|
||||
let mode = match o.vrr_mode {
|
||||
VrrMode::NEVER => "never",
|
||||
VrrMode::ALWAYS => "always",
|
||||
VrrMode::VARIANT_1 => "variant1",
|
||||
VrrMode::VARIANT_2 => "variant2",
|
||||
VrrMode::VARIANT_3 => "variant3",
|
||||
_ => {
|
||||
mode_str = format!("unknown ({})", o.vrr_mode.0);
|
||||
&mode_str
|
||||
}
|
||||
};
|
||||
println!(" VRR mode: {}", mode);
|
||||
if let Some(hz) = o.vrr_cursor_hz {
|
||||
println!(" VRR cursor hz: {}", hz);
|
||||
}
|
||||
}
|
||||
println!(" position: {} x {}", o.x, o.y);
|
||||
println!(" logical size: {} x {}", o.width, o.height);
|
||||
if let Some(mode) = &o.current_mode {
|
||||
|
|
@ -601,6 +709,10 @@ impl Randr {
|
|||
modes: Default::default(),
|
||||
current_mode: None,
|
||||
non_desktop: false,
|
||||
vrr_capable: false,
|
||||
vrr_enabled: false,
|
||||
vrr_mode: VrrMode::NEVER,
|
||||
vrr_cursor_hz: None,
|
||||
});
|
||||
});
|
||||
jay_randr::NonDesktopOutput::handle(tc, randr, data.clone(), |data, msg| {
|
||||
|
|
@ -621,8 +733,26 @@ impl Randr {
|
|||
modes: Default::default(),
|
||||
current_mode: None,
|
||||
non_desktop: true,
|
||||
vrr_capable: false,
|
||||
vrr_enabled: false,
|
||||
vrr_mode: VrrMode::NEVER,
|
||||
vrr_cursor_hz: None,
|
||||
});
|
||||
});
|
||||
jay_randr::VrrState::handle(tc, randr, data.clone(), |data, msg| {
|
||||
let mut data = data.borrow_mut();
|
||||
let c = data.connectors.last_mut().unwrap();
|
||||
let output = c.output.as_mut().unwrap();
|
||||
output.vrr_capable = msg.capable != 0;
|
||||
output.vrr_enabled = msg.enabled != 0;
|
||||
output.vrr_mode = VrrMode(msg.mode);
|
||||
});
|
||||
jay_randr::VrrCursorHz::handle(tc, randr, data.clone(), move |data, msg| {
|
||||
let mut data = data.borrow_mut();
|
||||
let c = data.connectors.last_mut().unwrap();
|
||||
let output = c.output.as_mut().unwrap();
|
||||
output.vrr_cursor_hz = Some(msg.hz);
|
||||
});
|
||||
jay_randr::Mode::handle(tc, randr, data.clone(), |data, msg| {
|
||||
let mut data = data.borrow_mut();
|
||||
let c = data.connectors.last_mut().unwrap();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use {
|
|||
crate::{
|
||||
acceptor::{Acceptor, AcceptorError},
|
||||
async_engine::{AsyncEngine, Phase, SpawnedFuture},
|
||||
backend::{self, Backend},
|
||||
backend::{self, Backend, Connector},
|
||||
backends::{
|
||||
dummy::{DummyBackend, DummyOutput},
|
||||
metal, x,
|
||||
|
|
@ -25,6 +25,7 @@ use {
|
|||
io_uring::{IoUring, IoUringError},
|
||||
leaks,
|
||||
logger::Logger,
|
||||
output_schedule::OutputSchedule,
|
||||
portal::{self, PortalStartup},
|
||||
scale::Scale,
|
||||
sighand::{self, SighandError},
|
||||
|
|
@ -32,7 +33,7 @@ use {
|
|||
tasks::{self, idle},
|
||||
tree::{
|
||||
container_layout, container_render_data, float_layout, float_titles,
|
||||
output_render_data, DisplayNode, NodeIds, OutputNode, WorkspaceNode,
|
||||
output_render_data, DisplayNode, NodeIds, OutputNode, VrrMode, WorkspaceNode,
|
||||
},
|
||||
user_session::import_environment,
|
||||
utils::{
|
||||
|
|
@ -246,6 +247,8 @@ fn start_compositor2(
|
|||
tablet_tool_ids: Default::default(),
|
||||
tablet_pad_ids: Default::default(),
|
||||
damage_visualizer: DamageVisualizer::new(&engine),
|
||||
default_vrr_mode: Cell::new(VrrMode::NEVER),
|
||||
default_vrr_cursor_hz: Cell::new(None),
|
||||
});
|
||||
state.tracker.register(ClientId::from_raw(0));
|
||||
create_dummy_output(&state);
|
||||
|
|
@ -420,16 +423,25 @@ fn create_dummy_output(state: &Rc<State>) {
|
|||
transform: Default::default(),
|
||||
scale: Default::default(),
|
||||
pos: Default::default(),
|
||||
vrr_mode: Cell::new(VrrMode::NEVER),
|
||||
vrr_cursor_hz: Default::default(),
|
||||
});
|
||||
let connector = Rc::new(DummyOutput {
|
||||
id: state.connector_ids.next(),
|
||||
}) as Rc<dyn Connector>;
|
||||
let schedule = Rc::new(OutputSchedule::new(
|
||||
&state.ring,
|
||||
&state.eng,
|
||||
&connector,
|
||||
&persistent_state,
|
||||
));
|
||||
let dummy_output = Rc::new(OutputNode {
|
||||
id: state.node_ids.next(),
|
||||
global: Rc::new(WlOutputGlobal::new(
|
||||
state.globals.name(),
|
||||
state,
|
||||
&Rc::new(ConnectorData {
|
||||
connector: Rc::new(DummyOutput {
|
||||
id: state.connector_ids.next(),
|
||||
}),
|
||||
connector,
|
||||
handler: Cell::new(None),
|
||||
connected: Cell::new(true),
|
||||
name: "Dummy".to_string(),
|
||||
|
|
@ -469,6 +481,7 @@ fn create_dummy_output(state: &Rc<State>) {
|
|||
hardware_cursor_needs_render: Cell::new(false),
|
||||
screencopies: Default::default(),
|
||||
title_visible: Cell::new(false),
|
||||
schedule,
|
||||
});
|
||||
let dummy_workspace = Rc::new(WorkspaceNode {
|
||||
id: state.node_ids.next(),
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ use {
|
|||
config::ConfigProxy,
|
||||
ifs::wl_seat::{SeatId, WlSeatGlobal},
|
||||
io_uring::TaskResultExt,
|
||||
output_schedule::map_cursor_hz,
|
||||
scale::Scale,
|
||||
state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State},
|
||||
theme::{Color, ThemeSized, DEFAULT_FONT},
|
||||
tree::{
|
||||
move_ws_to_output, ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase,
|
||||
OutputNode, WsMoveConfig,
|
||||
OutputNode, VrrMode, WsMoveConfig,
|
||||
},
|
||||
utils::{
|
||||
asyncevent::AsyncEvent,
|
||||
|
|
@ -47,7 +48,7 @@ use {
|
|||
logging::LogLevel,
|
||||
theme::{colors::Colorable, sized::Resizable},
|
||||
timer::Timer as JayTimer,
|
||||
video::{Connector, DrmDevice, GfxApi, Transform},
|
||||
video::{Connector, DrmDevice, GfxApi, Transform, VrrMode as ConfigVrrMode},
|
||||
Axis, Direction, Workspace,
|
||||
},
|
||||
libloading::Library,
|
||||
|
|
@ -1032,6 +1033,45 @@ impl ConfigProxyHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_set_vrr_mode(
|
||||
&self,
|
||||
connector: Option<Connector>,
|
||||
mode: ConfigVrrMode,
|
||||
) -> Result<(), CphError> {
|
||||
let Some(mode) = VrrMode::from_config(mode) else {
|
||||
return Err(CphError::UnknownVrrMode(mode));
|
||||
};
|
||||
match connector {
|
||||
Some(c) => {
|
||||
let connector = self.get_output_node(c)?;
|
||||
connector.global.persistent.vrr_mode.set(mode);
|
||||
connector.update_vrr_state();
|
||||
}
|
||||
_ => self.state.default_vrr_mode.set(mode),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_set_vrr_cursor_hz(
|
||||
&self,
|
||||
connector: Option<Connector>,
|
||||
hz: f64,
|
||||
) -> Result<(), CphError> {
|
||||
match connector {
|
||||
Some(c) => {
|
||||
let connector = self.get_output_node(c)?;
|
||||
connector.schedule.set_cursor_hz(hz);
|
||||
}
|
||||
_ => {
|
||||
let Some((hz, _)) = map_cursor_hz(hz) else {
|
||||
return Err(CphError::InvalidCursorHz(hz));
|
||||
};
|
||||
self.state.default_vrr_cursor_hz.set(hz)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_connector_set_transform(
|
||||
&self,
|
||||
connector: Connector,
|
||||
|
|
@ -1826,6 +1866,12 @@ impl ConfigProxyHandler {
|
|||
ClientMessage::SetWindowManagementEnabled { seat, enabled } => self
|
||||
.handle_set_window_management_enabled(seat, enabled)
|
||||
.wrn("set_window_management_enabled")?,
|
||||
ClientMessage::SetVrrMode { connector, mode } => self
|
||||
.handle_set_vrr_mode(connector, mode)
|
||||
.wrn("set_vrr_mode")?,
|
||||
ClientMessage::SetVrrCursorHz { connector, hz } => self
|
||||
.handle_set_vrr_cursor_hz(connector, hz)
|
||||
.wrn("set_vrr_cursor_hz")?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1887,6 +1933,10 @@ enum CphError {
|
|||
NegativeCursorSize,
|
||||
#[error("Config referred to a pollable that does not exist")]
|
||||
PollableDoesNotExist,
|
||||
#[error("Unknown VRR mode {0:?}")]
|
||||
UnknownVrrMode(ConfigVrrMode),
|
||||
#[error("Invalid cursor hz {0}")]
|
||||
InvalidCursorHz(f64),
|
||||
}
|
||||
|
||||
trait WithRequestName {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ impl CursorUserGroup {
|
|||
let x_int = x.round_down();
|
||||
let y_int = y.round_down();
|
||||
let extents = cursor.extents_at_scale(Scale::default());
|
||||
self.state.damage(extents.move_(x_int, y_int));
|
||||
self.state.damage2(true, extents.move_(x_int, y_int));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -399,8 +399,10 @@ impl CursorUser {
|
|||
let old_x_int = old_x.round_down();
|
||||
let old_y_int = old_y.round_down();
|
||||
let extents = cursor.extents_at_scale(Scale::default());
|
||||
self.group.state.damage(extents.move_(old_x_int, old_y_int));
|
||||
self.group.state.damage(extents.move_(x_int, y_int));
|
||||
self.group
|
||||
.state
|
||||
.damage2(true, extents.move_(old_x_int, old_y_int));
|
||||
self.group.state.damage2(true, extents.move_(x_int, y_int));
|
||||
}
|
||||
}
|
||||
self.pos.set((x, y));
|
||||
|
|
@ -439,6 +441,13 @@ impl CursorUser {
|
|||
let (x, y) = self.pos.get();
|
||||
for output in self.group.state.root.outputs.lock().values() {
|
||||
if let Some(hc) = output.hardware_cursor.get() {
|
||||
let commit = || {
|
||||
let defer = output.schedule.defer_cursor_updates();
|
||||
hc.commit(!defer);
|
||||
if defer {
|
||||
output.schedule.hardware_cursor_changed();
|
||||
}
|
||||
};
|
||||
let transform = output.global.persistent.transform.get();
|
||||
let render = render | output.hardware_cursor_needs_render.take();
|
||||
let scale = output.global.persistent.scale.get();
|
||||
|
|
@ -448,7 +457,7 @@ impl CursorUser {
|
|||
let (max_width, max_height) = transform.maybe_swap((hc_width, hc_height));
|
||||
if extents.width() > max_width || extents.height() > max_height {
|
||||
hc.set_enabled(false);
|
||||
hc.commit();
|
||||
commit();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -495,7 +504,7 @@ impl CursorUser {
|
|||
}
|
||||
hc.set_enabled(false);
|
||||
}
|
||||
hc.commit();
|
||||
commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,12 +43,13 @@ impl JayCompositorGlobal {
|
|||
self: Rc<Self>,
|
||||
id: JayCompositorId,
|
||||
client: &Rc<Client>,
|
||||
_version: Version,
|
||||
version: Version,
|
||||
) -> Result<(), JayCompositorError> {
|
||||
let obj = Rc::new(JayCompositor {
|
||||
id,
|
||||
client: client.clone(),
|
||||
tracker: Default::default(),
|
||||
version,
|
||||
});
|
||||
track!(client, obj);
|
||||
client.add_client_obj(&obj)?;
|
||||
|
|
@ -65,7 +66,7 @@ impl Global for JayCompositorGlobal {
|
|||
}
|
||||
|
||||
fn version(&self) -> u32 {
|
||||
1
|
||||
2
|
||||
}
|
||||
|
||||
fn required_caps(&self) -> ClientCaps {
|
||||
|
|
@ -79,6 +80,7 @@ pub struct JayCompositor {
|
|||
id: JayCompositorId,
|
||||
client: Rc<Client>,
|
||||
tracker: Tracker<Self>,
|
||||
version: Version,
|
||||
}
|
||||
|
||||
pub struct Cap;
|
||||
|
|
@ -327,7 +329,7 @@ impl JayCompositorRequestHandler for JayCompositor {
|
|||
}
|
||||
|
||||
fn get_randr(&self, req: GetRandr, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
||||
let sc = Rc::new(JayRandr::new(req.id, &self.client));
|
||||
let sc = Rc::new(JayRandr::new(req.id, &self.client, self.version));
|
||||
track!(self.client, sc);
|
||||
self.client.add_client_obj(&sc)?;
|
||||
Ok(())
|
||||
|
|
@ -379,7 +381,7 @@ impl JayCompositorRequestHandler for JayCompositor {
|
|||
|
||||
object_base! {
|
||||
self = JayCompositor;
|
||||
version = Version(1);
|
||||
version = self.version;
|
||||
}
|
||||
|
||||
impl Object for JayCompositor {}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ use {
|
|||
object::{Object, Version},
|
||||
scale::Scale,
|
||||
state::{ConnectorData, DrmDevData, OutputData},
|
||||
tree::OutputNode,
|
||||
tree::{OutputNode, VrrMode},
|
||||
utils::{gfx_api_ext::GfxApiExt, transform_ext::TransformExt},
|
||||
wire::{jay_randr::*, JayRandrId},
|
||||
},
|
||||
jay_config::video::{GfxApi, Transform},
|
||||
jay_config::video::{GfxApi, Transform, VrrMode as ConfigVrrMode},
|
||||
std::rc::Rc,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
|
@ -20,14 +20,18 @@ pub struct JayRandr {
|
|||
pub id: JayRandrId,
|
||||
pub client: Rc<Client>,
|
||||
pub tracker: Tracker<Self>,
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
const VRR_CAPABLE_SINCE: Version = Version(2);
|
||||
|
||||
impl JayRandr {
|
||||
pub fn new(id: JayRandrId, client: &Rc<Client>) -> Self {
|
||||
pub fn new(id: JayRandrId, client: &Rc<Client>, version: Version) -> Self {
|
||||
Self {
|
||||
id,
|
||||
client: client.clone(),
|
||||
tracker: Default::default(),
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,9 +72,9 @@ impl JayRandr {
|
|||
let Some(output) = self.client.state.outputs.get(&data.connector.id()) else {
|
||||
return;
|
||||
};
|
||||
let global = match output.node.as_ref().map(|n| &n.global) {
|
||||
Some(g) => g,
|
||||
_ => {
|
||||
let node = match &output.node {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
self.client.event(NonDesktopOutput {
|
||||
self_id: self.id,
|
||||
manufacturer: &output.monitor_info.manufacturer,
|
||||
|
|
@ -82,6 +86,7 @@ impl JayRandr {
|
|||
return;
|
||||
}
|
||||
};
|
||||
let global = &node.global;
|
||||
let pos = global.pos.get();
|
||||
self.client.event(Output {
|
||||
self_id: self.id,
|
||||
|
|
@ -97,6 +102,20 @@ impl JayRandr {
|
|||
width_mm: global.width_mm,
|
||||
height_mm: global.height_mm,
|
||||
});
|
||||
if self.version >= VRR_CAPABLE_SINCE {
|
||||
self.client.event(VrrState {
|
||||
self_id: self.id,
|
||||
capable: output.monitor_info.vrr_capable as _,
|
||||
enabled: node.schedule.vrr_enabled() as _,
|
||||
mode: node.global.persistent.vrr_mode.get().to_config().0,
|
||||
});
|
||||
if let Some(hz) = node.global.persistent.vrr_cursor_hz.get() {
|
||||
self.client.event(VrrCursorHz {
|
||||
self_id: self.id,
|
||||
hz,
|
||||
});
|
||||
}
|
||||
}
|
||||
let current_mode = global.mode.get();
|
||||
for mode in &global.modes {
|
||||
self.client.event(Mode {
|
||||
|
|
@ -297,11 +316,35 @@ impl JayRandrRequestHandler for JayRandr {
|
|||
c.connector.set_non_desktop_override(non_desktop);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_vrr_mode(&self, req: SetVrrMode<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
|
||||
let Some(mode) = VrrMode::from_config(ConfigVrrMode(req.mode)) else {
|
||||
return Err(JayRandrError::UnknownVrrMode(req.mode));
|
||||
};
|
||||
let Some(c) = self.get_output_node(req.output) else {
|
||||
return Ok(());
|
||||
};
|
||||
c.global.persistent.vrr_mode.set(mode);
|
||||
c.update_vrr_state();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn set_vrr_cursor_hz(
|
||||
&self,
|
||||
req: SetVrrCursorHz<'_>,
|
||||
_slf: &Rc<Self>,
|
||||
) -> Result<(), Self::Error> {
|
||||
let Some(c) = self.get_output_node(req.output) else {
|
||||
return Ok(());
|
||||
};
|
||||
c.schedule.set_cursor_hz(req.hz);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
object_base! {
|
||||
self = JayRandr;
|
||||
version = Version(1);
|
||||
version = self.version;
|
||||
}
|
||||
|
||||
impl Object for JayRandr {}
|
||||
|
|
@ -312,5 +355,7 @@ simple_add_obj!(JayRandr);
|
|||
pub enum JayRandrError {
|
||||
#[error(transparent)]
|
||||
ClientError(Box<ClientError>),
|
||||
#[error("Unknown VRR mode {0}")]
|
||||
UnknownVrrMode(u32),
|
||||
}
|
||||
efrom!(JayRandrError, ClientError);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use {
|
|||
object::{Object, Version},
|
||||
rect::Rect,
|
||||
state::{ConnectorData, State},
|
||||
tree::{calculate_logical_size, OutputNode},
|
||||
tree::{calculate_logical_size, OutputNode, VrrMode},
|
||||
utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, transform_ext::TransformExt},
|
||||
wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id},
|
||||
},
|
||||
|
|
@ -91,6 +91,8 @@ pub struct PersistentOutputState {
|
|||
pub transform: Cell<Transform>,
|
||||
pub scale: Cell<crate::scale::Scale>,
|
||||
pub pos: Cell<(i32, i32)>,
|
||||
pub vrr_mode: Cell<&'static VrrMode>,
|
||||
pub vrr_cursor_hz: Cell<Option<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ impl TestBackend {
|
|||
width_mm: 80,
|
||||
height_mm: 60,
|
||||
non_desktop: false,
|
||||
vrr_capable: false,
|
||||
};
|
||||
Self {
|
||||
state: state.clone(),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
|
|||
width_mm: 0,
|
||||
height_mm: 0,
|
||||
non_desktop: false,
|
||||
vrr_capable: false,
|
||||
};
|
||||
run.backend
|
||||
.state
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ mod libinput;
|
|||
mod logger;
|
||||
mod logind;
|
||||
mod object;
|
||||
mod output_schedule;
|
||||
mod pango;
|
||||
mod pipewire;
|
||||
mod portal;
|
||||
|
|
|
|||
196
src/output_schedule.rs
Normal file
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,
|
||||
tree::{
|
||||
ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, Node, NodeIds,
|
||||
NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase,
|
||||
NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase, VrrMode,
|
||||
WorkspaceNode,
|
||||
},
|
||||
utils::{
|
||||
|
|
@ -201,6 +201,8 @@ pub struct State {
|
|||
pub tablet_tool_ids: TabletToolIds,
|
||||
pub tablet_pad_ids: TabletPadIds,
|
||||
pub damage_visualizer: DamageVisualizer,
|
||||
pub default_vrr_mode: Cell<&'static VrrMode>,
|
||||
pub default_vrr_cursor_hz: Cell<Option<f64>>,
|
||||
}
|
||||
|
||||
// impl Drop for State {
|
||||
|
|
@ -730,13 +732,21 @@ impl State {
|
|||
}
|
||||
|
||||
pub fn damage(&self, rect: Rect) {
|
||||
self.damage2(false, rect);
|
||||
}
|
||||
|
||||
pub fn damage2(&self, cursor: bool, rect: Rect) {
|
||||
if rect.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.damage_visualizer.add(rect);
|
||||
for output in self.root.outputs.lock().values() {
|
||||
if output.global.pos.get().intersects(&rect) {
|
||||
output.global.connector.connector.damage();
|
||||
if cursor && output.schedule.defer_cursor_updates() {
|
||||
output.schedule.software_cursor_changed();
|
||||
} else {
|
||||
output.global.connector.connector.damage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -821,7 +831,7 @@ impl State {
|
|||
for output in self.root.outputs.lock().values() {
|
||||
if let Some(hc) = output.hardware_cursor.get() {
|
||||
hc.set_enabled(false);
|
||||
hc.commit();
|
||||
hc.commit(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use {
|
|||
backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo},
|
||||
globals::GlobalName,
|
||||
ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal},
|
||||
output_schedule::OutputSchedule,
|
||||
state::{ConnectorData, OutputData, State},
|
||||
tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig},
|
||||
utils::{asyncevent::AsyncEvent, clonecell::CloneCell, hash_map_ext::HashMapExt},
|
||||
|
|
@ -122,6 +123,8 @@ impl ConnectorHandler {
|
|||
transform: Default::default(),
|
||||
scale: Default::default(),
|
||||
pos: Cell::new((x1, 0)),
|
||||
vrr_mode: Cell::new(self.state.default_vrr_mode.get()),
|
||||
vrr_cursor_hz: Cell::new(self.state.default_vrr_cursor_hz.get()),
|
||||
});
|
||||
self.state
|
||||
.persistent_output_states
|
||||
|
|
@ -140,6 +143,13 @@ impl ConnectorHandler {
|
|||
&output_id,
|
||||
&desired_state,
|
||||
));
|
||||
let schedule = Rc::new(OutputSchedule::new(
|
||||
&self.state.ring,
|
||||
&self.state.eng,
|
||||
&self.data.connector,
|
||||
&desired_state,
|
||||
));
|
||||
let _schedule = self.state.eng.spawn(schedule.clone().drive());
|
||||
let on = Rc::new(OutputNode {
|
||||
id: self.state.node_ids.next(),
|
||||
workspaces: Default::default(),
|
||||
|
|
@ -173,6 +183,7 @@ impl ConnectorHandler {
|
|||
hardware_cursor_needs_render: Cell::new(false),
|
||||
screencopies: Default::default(),
|
||||
title_visible: Default::default(),
|
||||
schedule,
|
||||
});
|
||||
on.update_visible();
|
||||
on.update_rects();
|
||||
|
|
@ -231,17 +242,22 @@ impl ConnectorHandler {
|
|||
}
|
||||
self.state.add_global(&global);
|
||||
self.state.tree_changed();
|
||||
on.update_vrr_state();
|
||||
'outer: loop {
|
||||
while let Some(event) = self.data.connector.event() {
|
||||
match event {
|
||||
ConnectorEvent::Disconnected => break 'outer,
|
||||
ConnectorEvent::HardwareCursor(hc) => {
|
||||
on.schedule.set_hardware_cursor(&hc);
|
||||
on.hardware_cursor.set(hc);
|
||||
self.state.refresh_hardware_cursors();
|
||||
}
|
||||
ConnectorEvent::ModeChanged(mode) => {
|
||||
on.update_mode(mode);
|
||||
}
|
||||
ConnectorEvent::VrrChanged(enabled) => {
|
||||
on.schedule.set_vrr_enabled(enabled);
|
||||
}
|
||||
ev => unreachable!("received unexpected event {:?}", ev),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ impl ToolClient {
|
|||
}
|
||||
#[derive(Default)]
|
||||
struct S {
|
||||
jay_compositor: Cell<Option<u32>>,
|
||||
jay_compositor: Cell<Option<(u32, u32)>>,
|
||||
jay_damage_tracking: Cell<Option<u32>>,
|
||||
}
|
||||
let s = Rc::new(S::default());
|
||||
|
|
@ -297,7 +297,7 @@ impl ToolClient {
|
|||
});
|
||||
wl_registry::Global::handle(self, registry, s.clone(), |s, g| {
|
||||
if g.interface == JayCompositor.name() {
|
||||
s.jay_compositor.set(Some(g.name));
|
||||
s.jay_compositor.set(Some((g.name, g.version)));
|
||||
} else if g.interface == JayDamageTracking.name() {
|
||||
s.jay_damage_tracking.set(Some(g.name));
|
||||
}
|
||||
|
|
@ -328,9 +328,9 @@ impl ToolClient {
|
|||
let id: JayCompositorId = self.id();
|
||||
self.send(wl_registry::Bind {
|
||||
self_id: s.registry,
|
||||
name: s.jay_compositor,
|
||||
name: s.jay_compositor.0,
|
||||
interface: JayCompositor.name(),
|
||||
version: 1,
|
||||
version: s.jay_compositor.1.min(2),
|
||||
id: id.into(),
|
||||
});
|
||||
self.jay_compositor.set(Some(id));
|
||||
|
|
@ -361,7 +361,7 @@ impl ToolClient {
|
|||
|
||||
pub struct Singletons {
|
||||
registry: WlRegistryId,
|
||||
pub jay_compositor: u32,
|
||||
pub jay_compositor: (u32, u32),
|
||||
pub jay_damage_tracking: Option<u32>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ use {
|
|||
zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1},
|
||||
SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor,
|
||||
},
|
||||
wp_content_type_v1::ContentType,
|
||||
zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP},
|
||||
zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1,
|
||||
},
|
||||
output_schedule::OutputSchedule,
|
||||
rect::Rect,
|
||||
renderer::Renderer,
|
||||
scale::Scale,
|
||||
|
|
@ -41,7 +43,7 @@ use {
|
|||
wire::{JayOutputId, JayScreencastId, ZwlrScreencopyFrameV1Id},
|
||||
},
|
||||
ahash::AHashMap,
|
||||
jay_config::video::Transform,
|
||||
jay_config::video::{Transform, VrrMode as ConfigVrrMode},
|
||||
smallvec::SmallVec,
|
||||
std::{
|
||||
cell::{Cell, RefCell},
|
||||
|
|
@ -77,6 +79,7 @@ pub struct OutputNode {
|
|||
pub screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc<JayScreencast>>,
|
||||
pub screencopies: CopyHashMap<(ClientId, ZwlrScreencopyFrameV1Id), Rc<ZwlrScreencopyFrameV1>>,
|
||||
pub title_visible: Cell<bool>,
|
||||
pub schedule: Rc<OutputSchedule>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
|
|
@ -785,6 +788,39 @@ impl OutputNode {
|
|||
self.schedule_update_render_data();
|
||||
self.state.tree_changed();
|
||||
}
|
||||
|
||||
pub fn update_vrr_state(&self) {
|
||||
let enabled = match self.global.persistent.vrr_mode.get() {
|
||||
VrrMode::Never => false,
|
||||
VrrMode::Always => true,
|
||||
VrrMode::Fullscreen { surface } => 'get: {
|
||||
let Some(ws) = self.workspace.get() else {
|
||||
break 'get false;
|
||||
};
|
||||
let Some(tl) = ws.fullscreen.get() else {
|
||||
break 'get false;
|
||||
};
|
||||
if let Some(req) = surface {
|
||||
let Some(surface) = tl.tl_scanout_surface() else {
|
||||
break 'get false;
|
||||
};
|
||||
if let Some(req) = req.content_type {
|
||||
let Some(content_type) = surface.content_type.get() else {
|
||||
break 'get false;
|
||||
};
|
||||
match content_type {
|
||||
ContentType::Photo if !req.photo => break 'get false,
|
||||
ContentType::Video if !req.video => break 'get false,
|
||||
ContentType::Game if !req.game => break 'get false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
};
|
||||
self.global.connector.connector.set_vrr_enabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OutputTitle {
|
||||
|
|
@ -1084,3 +1120,68 @@ pub fn calculate_logical_size(
|
|||
}
|
||||
(width, height)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum VrrMode {
|
||||
Never,
|
||||
Always,
|
||||
Fullscreen {
|
||||
surface: Option<VrrSurfaceRequirements>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct VrrSurfaceRequirements {
|
||||
content_type: Option<VrrContentTypeRequirements>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct VrrContentTypeRequirements {
|
||||
photo: bool,
|
||||
video: bool,
|
||||
game: bool,
|
||||
}
|
||||
|
||||
impl VrrMode {
|
||||
pub const NEVER: &'static Self = &Self::Never;
|
||||
pub const ALWAYS: &'static Self = &Self::Always;
|
||||
pub const VARIANT_1: &'static Self = &Self::Fullscreen { surface: None };
|
||||
pub const VARIANT_2: &'static Self = &Self::Fullscreen {
|
||||
surface: Some(VrrSurfaceRequirements { content_type: None }),
|
||||
};
|
||||
pub const VARIANT_3: &'static Self = &Self::Fullscreen {
|
||||
surface: Some(VrrSurfaceRequirements {
|
||||
content_type: Some(VrrContentTypeRequirements {
|
||||
photo: false,
|
||||
video: true,
|
||||
game: true,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
pub fn from_config(mode: ConfigVrrMode) -> Option<&'static Self> {
|
||||
let res = match mode {
|
||||
ConfigVrrMode::NEVER => Self::NEVER,
|
||||
ConfigVrrMode::ALWAYS => Self::ALWAYS,
|
||||
ConfigVrrMode::VARIANT_1 => Self::VARIANT_1,
|
||||
ConfigVrrMode::VARIANT_2 => Self::VARIANT_2,
|
||||
ConfigVrrMode::VARIANT_3 => Self::VARIANT_3,
|
||||
_ => return None,
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
|
||||
pub fn to_config(&self) -> ConfigVrrMode {
|
||||
match self {
|
||||
Self::NEVER => ConfigVrrMode::NEVER,
|
||||
Self::ALWAYS => ConfigVrrMode::ALWAYS,
|
||||
Self::VARIANT_1 => ConfigVrrMode::VARIANT_1,
|
||||
Self::VARIANT_2 => ConfigVrrMode::VARIANT_2,
|
||||
Self::VARIANT_3 => ConfigVrrMode::VARIANT_3,
|
||||
_ => {
|
||||
log::error!("VRR mode {self:?} has no config representation");
|
||||
ConfigVrrMode::NEVER
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ impl WorkspaceNode {
|
|||
surface.send_feedback(&fb);
|
||||
}
|
||||
}
|
||||
self.output.get().update_vrr_state();
|
||||
}
|
||||
|
||||
pub fn remove_fullscreen_node(&self) {
|
||||
|
|
@ -194,6 +195,7 @@ impl WorkspaceNode {
|
|||
surface.send_feedback(&fb);
|
||||
}
|
||||
}
|
||||
self.output.get().update_vrr_state();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue