1
0
Fork 0
forked from wry/wry

metal: implement VRR

This commit is contained in:
Julian Orth 2024-07-17 16:30:52 +02:00
parent cd09e57568
commit 2d7c13b0b4
35 changed files with 1320 additions and 91 deletions

View file

@ -71,6 +71,7 @@ pub struct MonitorInfo {
pub width_mm: i32,
pub height_mm: i32,
pub non_desktop: bool,
pub vrr_capable: bool,
}
#[derive(Copy, Clone, Debug)]
@ -108,6 +109,9 @@ pub trait Connector {
fn drm_object_id(&self) -> Option<DrmConnector> {
None
}
fn set_vrr_enabled(&self, enabled: bool) {
let _ = enabled;
}
}
#[derive(Debug)]
@ -119,6 +123,7 @@ pub enum ConnectorEvent {
ModeChanged(Mode),
Unavailable,
Available,
VrrChanged(bool),
}
pub trait HardwareCursor: Debug {
@ -127,7 +132,8 @@ pub trait HardwareCursor: Debug {
fn set_position(&self, x: i32, y: i32);
fn swap_buffer(&self);
fn set_sync_file(&self, sync_file: Option<SyncFile>);
fn commit(&self);
fn commit(&self, schedule_present: bool);
fn schedule_present(&self) -> bool;
fn size(&self) -> (i32, i32);
}

View file

@ -95,6 +95,7 @@ pub struct MetalDrmDevice {
pub on_change: OnChange<crate::backend::DrmEvent>,
pub direct_scanout_enabled: Cell<Option<bool>>,
pub is_nvidia: bool,
pub is_amd: bool,
pub lease_ids: MetalLeaseIds,
pub leases: CopyHashMap<MetalLeaseId, MetalLeaseData>,
pub leases_to_break: CopyHashMap<MetalLeaseId, MetalLeaseData>,
@ -299,6 +300,8 @@ pub struct ConnectorDisplayData {
pub refresh: u32,
pub non_desktop: bool,
pub non_desktop_effective: bool,
pub vrr_capable: bool,
pub vrr_requested: bool,
pub monitor_manufacturer: String,
pub monitor_name: String,
@ -319,6 +322,10 @@ impl ConnectorDisplayData {
&& self.monitor_name == other.monitor_name
&& self.monitor_serial_number == other.monitor_serial_number
}
fn should_enable_vrr(&self) -> bool {
self.vrr_requested && self.vrr_capable
}
}
linear_ids!(MetalLeaseIds, MetalLeaseId, u64);
@ -417,6 +424,7 @@ pub struct MetalConnector {
pub can_present: Cell<bool>,
pub has_damage: Cell<bool>,
pub cursor_changed: Cell<bool>,
pub cursor_scheduled: Cell<bool>,
pub next_flip_nsec: Cell<u64>,
pub display: RefCell<ConnectorDisplayData>,
@ -503,7 +511,7 @@ impl HardwareCursor for MetalHardwareCursor {
self.have_changes.set(true);
}
fn commit(&self) {
fn commit(&self, schedule_present: bool) {
if self.generation != self.connector.cursor_generation.get() {
return;
}
@ -520,8 +528,20 @@ impl HardwareCursor for MetalHardwareCursor {
}
self.connector.cursor_sync_file.set(self.sync_file.take());
self.connector.cursor_changed.set(true);
if self.connector.can_present.get() {
self.connector.schedule_present();
if schedule_present {
self.schedule_present();
}
}
fn schedule_present(&self) -> bool {
if self.connector.cursor_changed.get() {
self.connector.cursor_scheduled.set(true);
if self.connector.can_present.get() {
self.connector.schedule_present();
}
true
} else {
false
}
}
@ -604,6 +624,19 @@ impl MetalConnector {
}
}
fn send_vrr_enabled(&self) {
match self.frontend_state.get() {
FrontState::Removed
| FrontState::Disconnected
| FrontState::Unavailable
| FrontState::Connected { non_desktop: true } => return,
FrontState::Connected { non_desktop: false } => {}
}
if let Some(crtc) = self.crtc.get() {
self.send_event(ConnectorEvent::VrrChanged(crtc.vrr_enabled.value.get()));
}
}
fn send_hardware_cursor(self: &Rc<Self>) {
match self.frontend_state.get() {
FrontState::Removed
@ -894,7 +927,7 @@ impl MetalConnector {
Some(crtc) => crtc,
_ => return Ok(()),
};
if (!self.has_damage.get() && !self.cursor_changed.get()) || !self.can_present.get() {
if (!self.has_damage.get() && !self.cursor_scheduled.get()) || !self.can_present.get() {
return Ok(());
}
if !crtc.active.value.get() {
@ -908,6 +941,9 @@ impl MetalConnector {
Some(b) => b,
_ => return Ok(()),
};
let Some(node) = self.state.root.outputs.get(&self.connector_id) else {
return Ok(());
};
let cursor = self.cursor_plane.get();
let mut new_fb = None;
let mut changes = self.master.change();
@ -915,46 +951,52 @@ impl MetalConnector {
if !self.backend.check_render_context(&self.dev) {
return Ok(());
}
if let Some(node) = self.state.root.outputs.get(&self.connector_id) {
let buffer = &buffers[self.next_buffer.get() % buffers.len()];
let mut rr = self.render_result.borrow_mut();
rr.output_id = node.id;
let fb =
self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?;
rr.dispatch_frame_requests(self.state.now_msec());
let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) =
match &fb.direct_scanout_data {
None => {
let plane_w = plane.mode_w.get();
let plane_h = plane.mode_h.get();
(0, 0, plane_w, plane_h, plane_w, plane_h)
}
Some(dsd) => {
let p = &dsd.position;
(
p.crtc_x,
p.crtc_y,
p.crtc_width,
p.crtc_height,
p.src_width,
p.src_height,
)
}
};
let in_fence = fb.sync_file.as_ref().map(|s| s.raw()).unwrap_or(-1);
changes.change_object(plane.id, |c| {
c.change(plane.fb_id, fb.fb.id().0 as _);
c.change(plane.src_w.id, (src_width as u64) << 16);
c.change(plane.src_h.id, (src_height as u64) << 16);
c.change(plane.crtc_x.id, crtc_x as u64);
c.change(plane.crtc_y.id, crtc_y as u64);
c.change(plane.crtc_w.id, crtc_w as u64);
c.change(plane.crtc_h.id, crtc_h as u64);
if !self.dev.is_nvidia {
c.change(plane.in_fence_fd, in_fence as u64);
let buffer = &buffers[self.next_buffer.get() % buffers.len()];
let mut rr = self.render_result.borrow_mut();
rr.output_id = node.id;
let fb = self.prepare_present_fb(&mut rr, buffer, &plane, &node, try_direct_scanout)?;
rr.dispatch_frame_requests(self.state.now_msec());
let (crtc_x, crtc_y, crtc_w, crtc_h, src_width, src_height) =
match &fb.direct_scanout_data {
None => {
let plane_w = plane.mode_w.get();
let plane_h = plane.mode_h.get();
(0, 0, plane_w, plane_h, plane_w, plane_h)
}
});
new_fb = Some(fb);
Some(dsd) => {
let p = &dsd.position;
(
p.crtc_x,
p.crtc_y,
p.crtc_width,
p.crtc_height,
p.src_width,
p.src_height,
)
}
};
let in_fence = fb.sync_file.as_ref().map(|s| s.raw()).unwrap_or(-1);
changes.change_object(plane.id, |c| {
c.change(plane.fb_id, fb.fb.id().0 as _);
c.change(plane.src_w.id, (src_width as u64) << 16);
c.change(plane.src_h.id, (src_height as u64) << 16);
c.change(plane.crtc_x.id, crtc_x as u64);
c.change(plane.crtc_y.id, crtc_y as u64);
c.change(plane.crtc_w.id, crtc_w as u64);
c.change(plane.crtc_h.id, crtc_h as u64);
if !self.dev.is_nvidia {
c.change(plane.in_fence_fd, in_fence as u64);
}
});
new_fb = Some(fb);
} else {
if self.dev.is_amd && crtc.vrr_enabled.value.get() {
// Work around https://gitlab.freedesktop.org/drm/amd/-/issues/2186
if let Some(fb) = &*self.active_framebuffer.borrow() {
changes.change_object(plane.id, |c| {
c.change(plane.fb_id, fb.fb.id().0 as _);
});
}
}
}
let mut cursor_swap_buffer = false;
@ -1027,7 +1069,8 @@ impl MetalConnector {
.discard_presentation_feedback();
Err(MetalError::Commit(e))
} else {
self.perform_screencopies(&new_fb);
node.schedule.presented();
self.perform_screencopies(&new_fb, &node);
if let Some(fb) = new_fb {
if fb.direct_scanout_data.is_none() {
self.next_buffer.fetch_add(1);
@ -1042,14 +1085,12 @@ impl MetalConnector {
self.can_present.set(false);
self.has_damage.set(false);
self.cursor_changed.set(false);
self.cursor_scheduled.set(false);
Ok(())
}
}
fn perform_screencopies(&self, new_fb: &Option<PresentFb>) {
let Some(output) = self.state.root.outputs.get(&self.connector_id) else {
return;
};
fn perform_screencopies(&self, new_fb: &Option<PresentFb>, output: &OutputNode) {
let active_fb;
let fb = match &new_fb {
Some(f) => f,
@ -1173,6 +1214,17 @@ impl MetalConnector {
log::error!("Tried to send available event in invalid state: {state:?}");
}
},
ConnectorEvent::VrrChanged(_) => match state {
FrontState::Connected { non_desktop: false } => {
self.on_change.send_event(event);
}
FrontState::Connected { non_desktop: true }
| FrontState::Removed
| FrontState::Disconnected
| FrontState::Unavailable => {
log::error!("Tried to send vrr-changed event in invalid state: {state:?}");
}
},
}
}
}
@ -1296,6 +1348,32 @@ impl Connector for MetalConnector {
fn drm_object_id(&self) -> Option<DrmConnector> {
Some(self.id)
}
fn set_vrr_enabled(&self, enabled: bool) {
if self.frontend_state.get() != (FrontState::Connected { non_desktop: false }) {
return;
}
let dd = &mut *self.display.borrow_mut();
let old_enabled = dd.should_enable_vrr();
dd.vrr_requested = enabled;
let new_enabled = dd.should_enable_vrr();
if old_enabled == new_enabled {
return;
}
let Some(crtc) = self.crtc.get() else {
return;
};
let mut change = self.master.change();
change.change_object(crtc.id, |c| {
c.change(crtc.vrr_enabled.id, new_enabled as _);
});
if let Err(e) = change.commit(0, 0) {
log::error!("Could not change vrr mode: {}", ErrorFmt(e));
return;
}
crtc.vrr_enabled.value.set(new_enabled);
self.send_vrr_enabled();
}
}
pub struct MetalCrtc {
@ -1312,6 +1390,7 @@ pub struct MetalCrtc {
pub active: MutableProperty<bool>,
pub mode_id: MutableProperty<DrmBlob>,
pub out_fence_ptr: DrmProperty,
pub vrr_enabled: MutableProperty<bool>,
pub mode_blob: CloneCell<Option<Rc<PropBlob>>>,
}
@ -1435,6 +1514,7 @@ fn create_connector(
display: RefCell::new(display),
frontend_state: Cell::new(FrontState::Disconnected),
cursor_changed: Cell::new(false),
cursor_scheduled: Cell::new(false),
cursor_front_buffer: Default::default(),
cursor_swap_buffer: Cell::new(false),
cursor_sync_file: Default::default(),
@ -1545,6 +1625,10 @@ fn create_connector_display_data(
let props = collect_properties(&dev.master, connector)?;
let connector_type = ConnectorType::from_drm(info.connector_type);
let non_desktop = props.get("non-desktop")?.value.get() != 0;
let vrr_capable = match props.get("vrr_capable") {
Ok(c) => c.value.get() == 1,
Err(_) => false,
};
Ok(ConnectorDisplayData {
crtc_id: props.get("CRTC_ID")?.map(|v| DrmCrtc(v as _)),
crtcs,
@ -1553,6 +1637,8 @@ fn create_connector_display_data(
refresh,
non_desktop,
non_desktop_effective: non_desktop_override.unwrap_or(non_desktop),
vrr_capable,
vrr_requested: false,
monitor_manufacturer: manufacturer,
monitor_name: name,
monitor_serial_number: serial_number,
@ -1607,6 +1693,7 @@ fn create_crtc(
active: props.get("ACTIVE")?.map(|v| v == 1),
mode_id: props.get("MODE_ID")?.map(|v| DrmBlob(v as u32)),
out_fence_ptr: props.get("OUT_FENCE_PTR")?.id,
vrr_enabled: props.get("VRR_ENABLED")?.map(|v| v == 1),
mode_blob: Default::default(),
})
}
@ -1876,6 +1963,7 @@ impl MetalBackend {
dd.mode = Some(mode.clone());
}
}
dd.vrr_requested = old.vrr_requested;
}
mem::swap(old.deref_mut(), &mut dd);
match c.frontend_state.get() {
@ -1963,8 +2051,10 @@ impl MetalBackend {
width_mm: dd.mm_width as _,
height_mm: dd.mm_height as _,
non_desktop: dd.non_desktop_effective,
vrr_capable: dd.vrr_capable,
}));
connector.send_hardware_cursor();
connector.send_vrr_enabled();
}
pub fn create_drm_device(
@ -2030,9 +2120,11 @@ impl MetalBackend {
};
let mut is_nvidia = false;
let mut is_amd = false;
match gbm.drm.version() {
Ok(v) => {
is_nvidia = v.name.contains_str("nvidia");
is_amd = v.name.contains_str("amdgpu");
if is_nvidia {
log::warn!(
"Device {} use the nvidia driver. IN_FENCE_FD will not be used.",
@ -2068,6 +2160,7 @@ impl MetalBackend {
on_change: Default::default(),
direct_scanout_enabled: Default::default(),
is_nvidia,
is_amd,
lease_ids: Default::default(),
leases: Default::default(),
leases_to_break: Default::default(),
@ -2123,6 +2216,7 @@ impl MetalBackend {
for c in dev.dev.crtcs.values() {
let props = collect_untyped_properties(master, c.id)?;
c.active.value.set(get(&props, c.active.id)? != 0);
c.vrr_enabled.value.set(get(&props, c.vrr_enabled.id)? != 0);
c.mode_id
.value
.set(DrmBlob(get(&props, c.mode_id.id)? as _));
@ -2144,6 +2238,7 @@ impl MetalBackend {
connector.can_present.set(true);
connector.has_damage.set(true);
connector.cursor_changed.set(true);
connector.cursor_scheduled.set(true);
}
if dev.unprocessed_change.get() {
return self.handle_drm_change_(dev, false);
@ -2204,7 +2299,7 @@ impl MetalBackend {
if let Some(fb) = connector.next_framebuffer.take() {
*connector.active_framebuffer.borrow_mut() = Some(fb);
}
if connector.has_damage.get() || connector.cursor_changed.get() {
if connector.has_damage.get() || connector.cursor_scheduled.get() {
connector.schedule_present();
}
let dd = connector.display.borrow_mut();
@ -2282,10 +2377,12 @@ impl MetalBackend {
crtc.connector.set(None);
crtc.active.value.set(false);
crtc.mode_id.value.set(DrmBlob::NONE);
crtc.vrr_enabled.value.set(false);
changes.change_object(crtc.id, |c| {
c.change(crtc.active.id, 0);
c.change(crtc.mode_id.id, 0);
c.change(crtc.out_fence_ptr, 0);
c.change(crtc.vrr_enabled.id, 0);
})
}
}
@ -2483,6 +2580,7 @@ impl MetalBackend {
continue;
}
connector.send_hardware_cursor();
connector.send_vrr_enabled();
connector.update_drm_feedback();
}
Ok(())
@ -2490,6 +2588,7 @@ impl MetalBackend {
fn can_use_current_drm_mode(&self, dev: &Rc<MetalDrmDeviceData>) -> bool {
let mut used_crtcs = AHashSet::new();
let mut vrr_crtcs = AHashSet::new();
let mut used_planes = AHashSet::new();
for connector in dev.connectors.lock().values() {
@ -2507,6 +2606,9 @@ impl MetalBackend {
return false;
}
used_crtcs.insert(crtc_id);
if dd.should_enable_vrr() {
vrr_crtcs.insert(crtc_id);
}
let crtc = dev.dev.crtcs.get(&crtc_id).unwrap();
connector.crtc.set(Some(crtc.clone()));
crtc.connector.set(Some(connector.clone()));
@ -2558,6 +2660,11 @@ impl MetalBackend {
c.change(crtc.active.id, 0);
}
c.change(crtc.out_fence_ptr, 0);
let vrr_requested = vrr_crtcs.contains(&crtc.id);
if crtc.vrr_enabled.value.get() != vrr_requested {
c.change(crtc.vrr_enabled.id, vrr_requested as _);
crtc.vrr_enabled.value.set(vrr_requested);
}
});
}
if let Err(e) = changes.commit(flags, 0) {
@ -2748,6 +2855,7 @@ impl MetalBackend {
changes.change_object(crtc.id, |c| {
c.change(crtc.active.id, 1);
c.change(crtc.mode_id.id, mode_blob.id().0 as _);
c.change(crtc.vrr_enabled.id, dd.should_enable_vrr() as _);
});
connector.crtc.set(Some(crtc.clone()));
dd.crtc_id.value.set(crtc.id);
@ -2755,6 +2863,7 @@ impl MetalBackend {
crtc.active.value.set(true);
crtc.mode_id.value.set(mode_blob.id());
crtc.mode_blob.set(Some(Rc::new(mode_blob)));
crtc.vrr_enabled.value.set(dd.should_enable_vrr() as _);
Ok(())
}
@ -2894,6 +3003,7 @@ impl MetalBackend {
}
connector.has_damage.set(true);
connector.cursor_changed.set(true);
connector.cursor_scheduled.set(true);
connector.schedule_present();
}
}

View file

@ -575,6 +575,7 @@ impl XBackend {
width_mm: output.width.get(),
height_mm: output.height.get(),
non_desktop: false,
vrr_capable: false,
}));
output.changed();
self.present(output).await;

View file

@ -3,16 +3,17 @@ use {
cli::GlobalArgs,
scale::Scale,
tools::tool_client::{with_tool_client, Handle, ToolClient},
utils::transform_ext::TransformExt,
utils::{errorfmt::ErrorFmt, transform_ext::TransformExt},
wire::{jay_compositor, jay_randr, JayRandrId},
},
clap::{Args, Subcommand, ValueEnum},
isnt::std_1::vec::IsntVecExt,
jay_config::video::Transform,
jay_config::video::{Transform, VrrMode},
std::{
cell::RefCell,
fmt::{Display, Formatter},
rc::Rc,
str::FromStr,
},
};
@ -117,6 +118,8 @@ pub enum OutputCommand {
Disable,
/// Override the display's non-desktop setting.
NonDesktop(NonDesktopArgs),
/// Change VRR settings.
Vrr(VrrArgs),
}
#[derive(ValueEnum, Debug, Clone)]
@ -132,6 +135,46 @@ pub struct NonDesktopArgs {
pub setting: NonDesktopType,
}
#[derive(Args, Debug, Clone)]
pub struct VrrArgs {
#[clap(subcommand)]
pub command: VrrCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum VrrCommand {
/// Sets the mode that determines when VRR is enabled.
SetMode(SetModeArgs),
/// Sets the maximum refresh rate of the cursor.
SetCursorHz(CursorHzArgs),
}
#[derive(Args, Debug, Clone)]
pub struct SetModeArgs {
#[clap(value_enum)]
pub mode: VrrModeArg,
}
#[derive(ValueEnum, Debug, Copy, Clone, Hash, PartialEq)]
pub enum VrrModeArg {
/// VRR is never enabled.
Never,
/// VRR is always enabled.
Always,
/// VRR is enabled when one or more applications are displayed fullscreen.
Variant1,
/// VRR is enabled when a single application is displayed fullscreen.
Variant2,
/// VRR is enabled when a single game or video is displayed fullscreen.
Variant3,
}
#[derive(Args, Debug, Clone)]
pub struct CursorHzArgs {
/// The rate at which the cursor will be updated on screen.
pub rate: String,
}
#[derive(Args, Debug, Clone)]
pub struct PositionArgs {
/// The top-left x coordinate.
@ -233,6 +276,10 @@ struct Output {
pub current_mode: Option<Mode>,
pub modes: Vec<Mode>,
pub non_desktop: bool,
pub vrr_capable: bool,
pub vrr_enabled: bool,
pub vrr_mode: VrrMode,
pub vrr_cursor_hz: Option<f64>,
}
#[derive(Copy, Clone, Debug)]
@ -399,6 +446,47 @@ impl Randr {
non_desktop: a.setting as _,
});
}
OutputCommand::Vrr(a) => {
self.handle_error(randr, move |msg| {
eprintln!("Could not change the VRR setting: {}", msg);
});
let parse_rate = |rate: &str| {
if rate.eq_ignore_ascii_case("none") {
f64::INFINITY
} else {
match f64::from_str(rate) {
Ok(v) => v,
Err(e) => {
fatal!("Could not parse rate: {}", ErrorFmt(e));
}
}
}
};
match a.command {
VrrCommand::SetMode(a) => {
let mode = match a.mode {
VrrModeArg::Never => VrrMode::NEVER,
VrrModeArg::Always => VrrMode::ALWAYS,
VrrModeArg::Variant1 => VrrMode::VARIANT_1,
VrrModeArg::Variant2 => VrrMode::VARIANT_2,
VrrModeArg::Variant3 => VrrMode::VARIANT_3,
};
tc.send(jay_randr::SetVrrMode {
self_id: randr,
output: &args.output,
mode: mode.0,
});
}
VrrCommand::SetCursorHz(r) => {
let hz = parse_rate(&r.rate);
tc.send(jay_randr::SetVrrCursorHz {
self_id: randr,
output: &args.output,
hz,
});
}
}
}
}
tc.round_trip().await;
}
@ -513,6 +601,26 @@ impl Randr {
println!(" non-desktop");
return;
}
println!(" VRR capable: {}", o.vrr_capable);
if o.vrr_capable {
println!(" VRR enabled: {}", o.vrr_enabled);
let mode_str;
let mode = match o.vrr_mode {
VrrMode::NEVER => "never",
VrrMode::ALWAYS => "always",
VrrMode::VARIANT_1 => "variant1",
VrrMode::VARIANT_2 => "variant2",
VrrMode::VARIANT_3 => "variant3",
_ => {
mode_str = format!("unknown ({})", o.vrr_mode.0);
&mode_str
}
};
println!(" VRR mode: {}", mode);
if let Some(hz) = o.vrr_cursor_hz {
println!(" VRR cursor hz: {}", hz);
}
}
println!(" position: {} x {}", o.x, o.y);
println!(" logical size: {} x {}", o.width, o.height);
if let Some(mode) = &o.current_mode {
@ -601,6 +709,10 @@ impl Randr {
modes: Default::default(),
current_mode: None,
non_desktop: false,
vrr_capable: false,
vrr_enabled: false,
vrr_mode: VrrMode::NEVER,
vrr_cursor_hz: None,
});
});
jay_randr::NonDesktopOutput::handle(tc, randr, data.clone(), |data, msg| {
@ -621,8 +733,26 @@ impl Randr {
modes: Default::default(),
current_mode: None,
non_desktop: true,
vrr_capable: false,
vrr_enabled: false,
vrr_mode: VrrMode::NEVER,
vrr_cursor_hz: None,
});
});
jay_randr::VrrState::handle(tc, randr, data.clone(), |data, msg| {
let mut data = data.borrow_mut();
let c = data.connectors.last_mut().unwrap();
let output = c.output.as_mut().unwrap();
output.vrr_capable = msg.capable != 0;
output.vrr_enabled = msg.enabled != 0;
output.vrr_mode = VrrMode(msg.mode);
});
jay_randr::VrrCursorHz::handle(tc, randr, data.clone(), move |data, msg| {
let mut data = data.borrow_mut();
let c = data.connectors.last_mut().unwrap();
let output = c.output.as_mut().unwrap();
output.vrr_cursor_hz = Some(msg.hz);
});
jay_randr::Mode::handle(tc, randr, data.clone(), |data, msg| {
let mut data = data.borrow_mut();
let c = data.connectors.last_mut().unwrap();

View file

@ -4,7 +4,7 @@ use {
crate::{
acceptor::{Acceptor, AcceptorError},
async_engine::{AsyncEngine, Phase, SpawnedFuture},
backend::{self, Backend},
backend::{self, Backend, Connector},
backends::{
dummy::{DummyBackend, DummyOutput},
metal, x,
@ -25,6 +25,7 @@ use {
io_uring::{IoUring, IoUringError},
leaks,
logger::Logger,
output_schedule::OutputSchedule,
portal::{self, PortalStartup},
scale::Scale,
sighand::{self, SighandError},
@ -32,7 +33,7 @@ use {
tasks::{self, idle},
tree::{
container_layout, container_render_data, float_layout, float_titles,
output_render_data, DisplayNode, NodeIds, OutputNode, WorkspaceNode,
output_render_data, DisplayNode, NodeIds, OutputNode, VrrMode, WorkspaceNode,
},
user_session::import_environment,
utils::{
@ -246,6 +247,8 @@ fn start_compositor2(
tablet_tool_ids: Default::default(),
tablet_pad_ids: Default::default(),
damage_visualizer: DamageVisualizer::new(&engine),
default_vrr_mode: Cell::new(VrrMode::NEVER),
default_vrr_cursor_hz: Cell::new(None),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);
@ -420,16 +423,25 @@ fn create_dummy_output(state: &Rc<State>) {
transform: Default::default(),
scale: Default::default(),
pos: Default::default(),
vrr_mode: Cell::new(VrrMode::NEVER),
vrr_cursor_hz: Default::default(),
});
let connector = Rc::new(DummyOutput {
id: state.connector_ids.next(),
}) as Rc<dyn Connector>;
let schedule = Rc::new(OutputSchedule::new(
&state.ring,
&state.eng,
&connector,
&persistent_state,
));
let dummy_output = Rc::new(OutputNode {
id: state.node_ids.next(),
global: Rc::new(WlOutputGlobal::new(
state.globals.name(),
state,
&Rc::new(ConnectorData {
connector: Rc::new(DummyOutput {
id: state.connector_ids.next(),
}),
connector,
handler: Cell::new(None),
connected: Cell::new(true),
name: "Dummy".to_string(),
@ -469,6 +481,7 @@ fn create_dummy_output(state: &Rc<State>) {
hardware_cursor_needs_render: Cell::new(false),
screencopies: Default::default(),
title_visible: Cell::new(false),
schedule,
});
let dummy_workspace = Rc::new(WorkspaceNode {
id: state.node_ids.next(),

View file

@ -9,12 +9,13 @@ use {
config::ConfigProxy,
ifs::wl_seat::{SeatId, WlSeatGlobal},
io_uring::TaskResultExt,
output_schedule::map_cursor_hz,
scale::Scale,
state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State},
theme::{Color, ThemeSized, DEFAULT_FONT},
tree::{
move_ws_to_output, ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase,
OutputNode, WsMoveConfig,
OutputNode, VrrMode, WsMoveConfig,
},
utils::{
asyncevent::AsyncEvent,
@ -47,7 +48,7 @@ use {
logging::LogLevel,
theme::{colors::Colorable, sized::Resizable},
timer::Timer as JayTimer,
video::{Connector, DrmDevice, GfxApi, Transform},
video::{Connector, DrmDevice, GfxApi, Transform, VrrMode as ConfigVrrMode},
Axis, Direction, Workspace,
},
libloading::Library,
@ -1032,6 +1033,45 @@ impl ConfigProxyHandler {
Ok(())
}
fn handle_set_vrr_mode(
&self,
connector: Option<Connector>,
mode: ConfigVrrMode,
) -> Result<(), CphError> {
let Some(mode) = VrrMode::from_config(mode) else {
return Err(CphError::UnknownVrrMode(mode));
};
match connector {
Some(c) => {
let connector = self.get_output_node(c)?;
connector.global.persistent.vrr_mode.set(mode);
connector.update_vrr_state();
}
_ => self.state.default_vrr_mode.set(mode),
}
Ok(())
}
fn handle_set_vrr_cursor_hz(
&self,
connector: Option<Connector>,
hz: f64,
) -> Result<(), CphError> {
match connector {
Some(c) => {
let connector = self.get_output_node(c)?;
connector.schedule.set_cursor_hz(hz);
}
_ => {
let Some((hz, _)) = map_cursor_hz(hz) else {
return Err(CphError::InvalidCursorHz(hz));
};
self.state.default_vrr_cursor_hz.set(hz)
}
}
Ok(())
}
fn handle_connector_set_transform(
&self,
connector: Connector,
@ -1826,6 +1866,12 @@ impl ConfigProxyHandler {
ClientMessage::SetWindowManagementEnabled { seat, enabled } => self
.handle_set_window_management_enabled(seat, enabled)
.wrn("set_window_management_enabled")?,
ClientMessage::SetVrrMode { connector, mode } => self
.handle_set_vrr_mode(connector, mode)
.wrn("set_vrr_mode")?,
ClientMessage::SetVrrCursorHz { connector, hz } => self
.handle_set_vrr_cursor_hz(connector, hz)
.wrn("set_vrr_cursor_hz")?,
}
Ok(())
}
@ -1887,6 +1933,10 @@ enum CphError {
NegativeCursorSize,
#[error("Config referred to a pollable that does not exist")]
PollableDoesNotExist,
#[error("Unknown VRR mode {0:?}")]
UnknownVrrMode(ConfigVrrMode),
#[error("Invalid cursor hz {0}")]
InvalidCursorHz(f64),
}
trait WithRequestName {

View file

@ -82,7 +82,7 @@ impl CursorUserGroup {
let x_int = x.round_down();
let y_int = y.round_down();
let extents = cursor.extents_at_scale(Scale::default());
self.state.damage(extents.move_(x_int, y_int));
self.state.damage2(true, extents.move_(x_int, y_int));
}
}
}
@ -399,8 +399,10 @@ impl CursorUser {
let old_x_int = old_x.round_down();
let old_y_int = old_y.round_down();
let extents = cursor.extents_at_scale(Scale::default());
self.group.state.damage(extents.move_(old_x_int, old_y_int));
self.group.state.damage(extents.move_(x_int, y_int));
self.group
.state
.damage2(true, extents.move_(old_x_int, old_y_int));
self.group.state.damage2(true, extents.move_(x_int, y_int));
}
}
self.pos.set((x, y));
@ -439,6 +441,13 @@ impl CursorUser {
let (x, y) = self.pos.get();
for output in self.group.state.root.outputs.lock().values() {
if let Some(hc) = output.hardware_cursor.get() {
let commit = || {
let defer = output.schedule.defer_cursor_updates();
hc.commit(!defer);
if defer {
output.schedule.hardware_cursor_changed();
}
};
let transform = output.global.persistent.transform.get();
let render = render | output.hardware_cursor_needs_render.take();
let scale = output.global.persistent.scale.get();
@ -448,7 +457,7 @@ impl CursorUser {
let (max_width, max_height) = transform.maybe_swap((hc_width, hc_height));
if extents.width() > max_width || extents.height() > max_height {
hc.set_enabled(false);
hc.commit();
commit();
continue;
}
}
@ -495,7 +504,7 @@ impl CursorUser {
}
hc.set_enabled(false);
}
hc.commit();
commit();
}
}
}

View file

@ -43,12 +43,13 @@ impl JayCompositorGlobal {
self: Rc<Self>,
id: JayCompositorId,
client: &Rc<Client>,
_version: Version,
version: Version,
) -> Result<(), JayCompositorError> {
let obj = Rc::new(JayCompositor {
id,
client: client.clone(),
tracker: Default::default(),
version,
});
track!(client, obj);
client.add_client_obj(&obj)?;
@ -65,7 +66,7 @@ impl Global for JayCompositorGlobal {
}
fn version(&self) -> u32 {
1
2
}
fn required_caps(&self) -> ClientCaps {
@ -79,6 +80,7 @@ pub struct JayCompositor {
id: JayCompositorId,
client: Rc<Client>,
tracker: Tracker<Self>,
version: Version,
}
pub struct Cap;
@ -327,7 +329,7 @@ impl JayCompositorRequestHandler for JayCompositor {
}
fn get_randr(&self, req: GetRandr, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let sc = Rc::new(JayRandr::new(req.id, &self.client));
let sc = Rc::new(JayRandr::new(req.id, &self.client, self.version));
track!(self.client, sc);
self.client.add_client_obj(&sc)?;
Ok(())
@ -379,7 +381,7 @@ impl JayCompositorRequestHandler for JayCompositor {
object_base! {
self = JayCompositor;
version = Version(1);
version = self.version;
}
impl Object for JayCompositor {}

View file

@ -7,11 +7,11 @@ use {
object::{Object, Version},
scale::Scale,
state::{ConnectorData, DrmDevData, OutputData},
tree::OutputNode,
tree::{OutputNode, VrrMode},
utils::{gfx_api_ext::GfxApiExt, transform_ext::TransformExt},
wire::{jay_randr::*, JayRandrId},
},
jay_config::video::{GfxApi, Transform},
jay_config::video::{GfxApi, Transform, VrrMode as ConfigVrrMode},
std::rc::Rc,
thiserror::Error,
};
@ -20,14 +20,18 @@ pub struct JayRandr {
pub id: JayRandrId,
pub client: Rc<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
}
const VRR_CAPABLE_SINCE: Version = Version(2);
impl JayRandr {
pub fn new(id: JayRandrId, client: &Rc<Client>) -> Self {
pub fn new(id: JayRandrId, client: &Rc<Client>, version: Version) -> Self {
Self {
id,
client: client.clone(),
tracker: Default::default(),
version,
}
}
@ -68,9 +72,9 @@ impl JayRandr {
let Some(output) = self.client.state.outputs.get(&data.connector.id()) else {
return;
};
let global = match output.node.as_ref().map(|n| &n.global) {
Some(g) => g,
_ => {
let node = match &output.node {
Some(n) => n,
None => {
self.client.event(NonDesktopOutput {
self_id: self.id,
manufacturer: &output.monitor_info.manufacturer,
@ -82,6 +86,7 @@ impl JayRandr {
return;
}
};
let global = &node.global;
let pos = global.pos.get();
self.client.event(Output {
self_id: self.id,
@ -97,6 +102,20 @@ impl JayRandr {
width_mm: global.width_mm,
height_mm: global.height_mm,
});
if self.version >= VRR_CAPABLE_SINCE {
self.client.event(VrrState {
self_id: self.id,
capable: output.monitor_info.vrr_capable as _,
enabled: node.schedule.vrr_enabled() as _,
mode: node.global.persistent.vrr_mode.get().to_config().0,
});
if let Some(hz) = node.global.persistent.vrr_cursor_hz.get() {
self.client.event(VrrCursorHz {
self_id: self.id,
hz,
});
}
}
let current_mode = global.mode.get();
for mode in &global.modes {
self.client.event(Mode {
@ -297,11 +316,35 @@ impl JayRandrRequestHandler for JayRandr {
c.connector.set_non_desktop_override(non_desktop);
Ok(())
}
fn set_vrr_mode(&self, req: SetVrrMode<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let Some(mode) = VrrMode::from_config(ConfigVrrMode(req.mode)) else {
return Err(JayRandrError::UnknownVrrMode(req.mode));
};
let Some(c) = self.get_output_node(req.output) else {
return Ok(());
};
c.global.persistent.vrr_mode.set(mode);
c.update_vrr_state();
return Ok(());
}
fn set_vrr_cursor_hz(
&self,
req: SetVrrCursorHz<'_>,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
let Some(c) = self.get_output_node(req.output) else {
return Ok(());
};
c.schedule.set_cursor_hz(req.hz);
Ok(())
}
}
object_base! {
self = JayRandr;
version = Version(1);
version = self.version;
}
impl Object for JayRandr {}
@ -312,5 +355,7 @@ simple_add_obj!(JayRandr);
pub enum JayRandrError {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("Unknown VRR mode {0}")]
UnknownVrrMode(u32),
}
efrom!(JayRandrError, ClientError);

View file

@ -10,7 +10,7 @@ use {
object::{Object, Version},
rect::Rect,
state::{ConnectorData, State},
tree::{calculate_logical_size, OutputNode},
tree::{calculate_logical_size, OutputNode, VrrMode},
utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, transform_ext::TransformExt},
wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id},
},
@ -91,6 +91,8 @@ pub struct PersistentOutputState {
pub transform: Cell<Transform>,
pub scale: Cell<crate::scale::Scale>,
pub pos: Cell<(i32, i32)>,
pub vrr_mode: Cell<&'static VrrMode>,
pub vrr_cursor_hz: Cell<Option<f64>>,
}
#[derive(Eq, PartialEq, Hash)]

View file

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

View file

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

View file

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

196
src/output_schedule.rs Normal file
View file

@ -0,0 +1,196 @@
use {
crate::{
async_engine::AsyncEngine,
backend::{Connector, HardwareCursor},
ifs::wl_output::PersistentOutputState,
io_uring::{IoUring, IoUringError},
utils::{
asyncevent::AsyncEvent, cell_ext::CellExt, clonecell::CloneCell, errorfmt::ErrorFmt,
numcell::NumCell,
},
},
futures_util::{select, FutureExt},
num_traits::ToPrimitive,
std::{cell::Cell, rc::Rc},
};
pub struct OutputSchedule {
changed: AsyncEvent,
run: Cell<bool>,
connector: Rc<dyn Connector>,
hardware_cursor: CloneCell<Option<Rc<dyn HardwareCursor>>>,
persistent: Rc<PersistentOutputState>,
last_present_nsec: Cell<u64>,
cursor_delta_nsec: Cell<Option<u64>>,
ring: Rc<IoUring>,
eng: Rc<AsyncEngine>,
vrr_enabled: Cell<bool>,
present_scheduled: Cell<bool>,
needs_hardware_cursor_commit: Cell<bool>,
needs_software_cursor_damage: Cell<bool>,
iteration: NumCell<u64>,
}
impl OutputSchedule {
pub fn new(
ring: &Rc<IoUring>,
eng: &Rc<AsyncEngine>,
connector: &Rc<dyn Connector>,
persistent: &Rc<PersistentOutputState>,
) -> Self {
let slf = Self {
changed: Default::default(),
run: Default::default(),
connector: connector.clone(),
ring: ring.clone(),
eng: eng.clone(),
vrr_enabled: Default::default(),
present_scheduled: Cell::new(true),
needs_hardware_cursor_commit: Default::default(),
needs_software_cursor_damage: Default::default(),
hardware_cursor: Default::default(),
persistent: persistent.clone(),
last_present_nsec: Default::default(),
cursor_delta_nsec: Default::default(),
iteration: Default::default(),
};
if let Some(hz) = persistent.vrr_cursor_hz.get() {
slf.set_cursor_hz(hz);
}
slf
}
pub async fn drive(self: Rc<Self>) {
loop {
self.run_once().await;
while !self.run.take() {
self.changed.triggered().await;
}
}
}
fn trigger(&self) {
let trigger = self.vrr_enabled.get()
&& !self.present_scheduled.get()
&& self.cursor_delta_nsec.is_some()
&& (self.needs_software_cursor_damage.get() || self.needs_hardware_cursor_commit.get());
if trigger {
self.run.set(true);
self.changed.trigger();
}
}
pub fn presented(&self) {
self.last_present_nsec.set(self.eng.now().nsec());
self.present_scheduled.set(false);
self.iteration.fetch_add(1);
self.trigger();
}
pub fn vrr_enabled(&self) -> bool {
self.vrr_enabled.get()
}
pub fn set_vrr_enabled(&self, enabled: bool) {
self.vrr_enabled.set(enabled);
self.trigger();
}
pub fn set_cursor_hz(&self, hz: f64) {
let (hz, delta) = match map_cursor_hz(hz) {
None => {
log::warn!("Ignoring cursor frequency {hz}");
return;
}
Some(v) => v,
};
self.persistent.vrr_cursor_hz.set(hz);
self.cursor_delta_nsec.set(delta);
self.trigger();
}
pub fn set_hardware_cursor(&self, hc: &Option<Rc<dyn HardwareCursor>>) {
self.hardware_cursor.set(hc.clone());
}
pub fn defer_cursor_updates(&self) -> bool {
self.vrr_enabled.get() && self.cursor_delta_nsec.is_some()
}
pub fn hardware_cursor_changed(&self) {
if !self.needs_hardware_cursor_commit.replace(true) {
self.trigger();
}
}
pub fn software_cursor_changed(&self) {
if !self.needs_software_cursor_damage.replace(true) {
self.trigger();
}
}
async fn run_once(&self) {
if self.present_scheduled.get() {
return;
}
if !self.needs_hardware_cursor_commit.get() && !self.needs_software_cursor_damage.get() {
return;
}
loop {
if !self.vrr_enabled.get() {
return;
}
let Some(duration) = self.cursor_delta_nsec.get() else {
return;
};
let iteration = self.iteration.get();
let next_present = self.last_present_nsec.get().saturating_add(duration);
let res: Result<(), IoUringError> = select! {
_ = self.changed.triggered().fuse() => continue,
v = self.ring.timeout(next_present).fuse() => v,
};
if let Err(e) = res {
log::error!("Could not wait for timer to expire: {}", ErrorFmt(e));
return;
}
if iteration == self.iteration.get() {
break;
}
}
if self.needs_hardware_cursor_commit.take() {
if let Some(hc) = self.hardware_cursor.get() {
if hc.schedule_present() {
self.present_scheduled.set(true);
}
}
}
if self.needs_software_cursor_damage.take() {
self.connector.damage();
self.present_scheduled.set(true);
}
}
}
pub fn map_cursor_hz(hz: f64) -> Option<(Option<f64>, Option<u64>)> {
if hz <= 0.0 {
return Some((Some(0.0), Some(u64::MAX)));
}
let delta = (1_000_000_000.0 / hz).to_u64();
if delta.is_none() {
if hz > 0.0 {
return Some((None, None));
}
return None;
}
if delta == Some(0) {
return Some((None, None));
}
Some((Some(hz), delta))
}

View file

@ -64,7 +64,7 @@ use {
time::Time,
tree::{
ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, Node, NodeIds,
NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase,
NodeVisitorBase, OutputNode, PlaceholderNode, ToplevelNode, ToplevelNodeBase, VrrMode,
WorkspaceNode,
},
utils::{
@ -201,6 +201,8 @@ pub struct State {
pub tablet_tool_ids: TabletToolIds,
pub tablet_pad_ids: TabletPadIds,
pub damage_visualizer: DamageVisualizer,
pub default_vrr_mode: Cell<&'static VrrMode>,
pub default_vrr_cursor_hz: Cell<Option<f64>>,
}
// impl Drop for State {
@ -730,13 +732,21 @@ impl State {
}
pub fn damage(&self, rect: Rect) {
self.damage2(false, rect);
}
pub fn damage2(&self, cursor: bool, rect: Rect) {
if rect.is_empty() {
return;
}
self.damage_visualizer.add(rect);
for output in self.root.outputs.lock().values() {
if output.global.pos.get().intersects(&rect) {
output.global.connector.connector.damage();
if cursor && output.schedule.defer_cursor_updates() {
output.schedule.software_cursor_changed();
} else {
output.global.connector.connector.damage();
}
}
}
}
@ -821,7 +831,7 @@ impl State {
for output in self.root.outputs.lock().values() {
if let Some(hc) = output.hardware_cursor.get() {
hc.set_enabled(false);
hc.commit();
hc.commit(true);
}
}
}

View file

@ -3,6 +3,7 @@ use {
backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo},
globals::GlobalName,
ifs::wl_output::{OutputId, PersistentOutputState, WlOutputGlobal},
output_schedule::OutputSchedule,
state::{ConnectorData, OutputData, State},
tree::{move_ws_to_output, OutputNode, OutputRenderData, WsMoveConfig},
utils::{asyncevent::AsyncEvent, clonecell::CloneCell, hash_map_ext::HashMapExt},
@ -122,6 +123,8 @@ impl ConnectorHandler {
transform: Default::default(),
scale: Default::default(),
pos: Cell::new((x1, 0)),
vrr_mode: Cell::new(self.state.default_vrr_mode.get()),
vrr_cursor_hz: Cell::new(self.state.default_vrr_cursor_hz.get()),
});
self.state
.persistent_output_states
@ -140,6 +143,13 @@ impl ConnectorHandler {
&output_id,
&desired_state,
));
let schedule = Rc::new(OutputSchedule::new(
&self.state.ring,
&self.state.eng,
&self.data.connector,
&desired_state,
));
let _schedule = self.state.eng.spawn(schedule.clone().drive());
let on = Rc::new(OutputNode {
id: self.state.node_ids.next(),
workspaces: Default::default(),
@ -173,6 +183,7 @@ impl ConnectorHandler {
hardware_cursor_needs_render: Cell::new(false),
screencopies: Default::default(),
title_visible: Default::default(),
schedule,
});
on.update_visible();
on.update_rects();
@ -231,17 +242,22 @@ impl ConnectorHandler {
}
self.state.add_global(&global);
self.state.tree_changed();
on.update_vrr_state();
'outer: loop {
while let Some(event) = self.data.connector.event() {
match event {
ConnectorEvent::Disconnected => break 'outer,
ConnectorEvent::HardwareCursor(hc) => {
on.schedule.set_hardware_cursor(&hc);
on.hardware_cursor.set(hc);
self.state.refresh_hardware_cursors();
}
ConnectorEvent::ModeChanged(mode) => {
on.update_mode(mode);
}
ConnectorEvent::VrrChanged(enabled) => {
on.schedule.set_vrr_enabled(enabled);
}
ev => unreachable!("received unexpected event {:?}", ev),
}
}

View file

@ -286,7 +286,7 @@ impl ToolClient {
}
#[derive(Default)]
struct S {
jay_compositor: Cell<Option<u32>>,
jay_compositor: Cell<Option<(u32, u32)>>,
jay_damage_tracking: Cell<Option<u32>>,
}
let s = Rc::new(S::default());
@ -297,7 +297,7 @@ impl ToolClient {
});
wl_registry::Global::handle(self, registry, s.clone(), |s, g| {
if g.interface == JayCompositor.name() {
s.jay_compositor.set(Some(g.name));
s.jay_compositor.set(Some((g.name, g.version)));
} else if g.interface == JayDamageTracking.name() {
s.jay_damage_tracking.set(Some(g.name));
}
@ -328,9 +328,9 @@ impl ToolClient {
let id: JayCompositorId = self.id();
self.send(wl_registry::Bind {
self_id: s.registry,
name: s.jay_compositor,
name: s.jay_compositor.0,
interface: JayCompositor.name(),
version: 1,
version: s.jay_compositor.1.min(2),
id: id.into(),
});
self.jay_compositor.set(Some(id));
@ -361,7 +361,7 @@ impl ToolClient {
pub struct Singletons {
registry: WlRegistryId,
pub jay_compositor: u32,
pub jay_compositor: (u32, u32),
pub jay_damage_tracking: Option<u32>,
}

View file

@ -21,9 +21,11 @@ use {
zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1},
SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor,
},
wp_content_type_v1::ContentType,
zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP},
zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1,
},
output_schedule::OutputSchedule,
rect::Rect,
renderer::Renderer,
scale::Scale,
@ -41,7 +43,7 @@ use {
wire::{JayOutputId, JayScreencastId, ZwlrScreencopyFrameV1Id},
},
ahash::AHashMap,
jay_config::video::Transform,
jay_config::video::{Transform, VrrMode as ConfigVrrMode},
smallvec::SmallVec,
std::{
cell::{Cell, RefCell},
@ -77,6 +79,7 @@ pub struct OutputNode {
pub screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc<JayScreencast>>,
pub screencopies: CopyHashMap<(ClientId, ZwlrScreencopyFrameV1Id), Rc<ZwlrScreencopyFrameV1>>,
pub title_visible: Cell<bool>,
pub schedule: Rc<OutputSchedule>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
@ -785,6 +788,39 @@ impl OutputNode {
self.schedule_update_render_data();
self.state.tree_changed();
}
pub fn update_vrr_state(&self) {
let enabled = match self.global.persistent.vrr_mode.get() {
VrrMode::Never => false,
VrrMode::Always => true,
VrrMode::Fullscreen { surface } => 'get: {
let Some(ws) = self.workspace.get() else {
break 'get false;
};
let Some(tl) = ws.fullscreen.get() else {
break 'get false;
};
if let Some(req) = surface {
let Some(surface) = tl.tl_scanout_surface() else {
break 'get false;
};
if let Some(req) = req.content_type {
let Some(content_type) = surface.content_type.get() else {
break 'get false;
};
match content_type {
ContentType::Photo if !req.photo => break 'get false,
ContentType::Video if !req.video => break 'get false,
ContentType::Game if !req.game => break 'get false,
_ => {}
}
}
}
true
}
};
self.global.connector.connector.set_vrr_enabled(enabled);
}
}
pub struct OutputTitle {
@ -1084,3 +1120,68 @@ pub fn calculate_logical_size(
}
(width, height)
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum VrrMode {
Never,
Always,
Fullscreen {
surface: Option<VrrSurfaceRequirements>,
},
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct VrrSurfaceRequirements {
content_type: Option<VrrContentTypeRequirements>,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct VrrContentTypeRequirements {
photo: bool,
video: bool,
game: bool,
}
impl VrrMode {
pub const NEVER: &'static Self = &Self::Never;
pub const ALWAYS: &'static Self = &Self::Always;
pub const VARIANT_1: &'static Self = &Self::Fullscreen { surface: None };
pub const VARIANT_2: &'static Self = &Self::Fullscreen {
surface: Some(VrrSurfaceRequirements { content_type: None }),
};
pub const VARIANT_3: &'static Self = &Self::Fullscreen {
surface: Some(VrrSurfaceRequirements {
content_type: Some(VrrContentTypeRequirements {
photo: false,
video: true,
game: true,
}),
}),
};
pub fn from_config(mode: ConfigVrrMode) -> Option<&'static Self> {
let res = match mode {
ConfigVrrMode::NEVER => Self::NEVER,
ConfigVrrMode::ALWAYS => Self::ALWAYS,
ConfigVrrMode::VARIANT_1 => Self::VARIANT_1,
ConfigVrrMode::VARIANT_2 => Self::VARIANT_2,
ConfigVrrMode::VARIANT_3 => Self::VARIANT_3,
_ => return None,
};
Some(res)
}
pub fn to_config(&self) -> ConfigVrrMode {
match self {
Self::NEVER => ConfigVrrMode::NEVER,
Self::ALWAYS => ConfigVrrMode::ALWAYS,
Self::VARIANT_1 => ConfigVrrMode::VARIANT_1,
Self::VARIANT_2 => ConfigVrrMode::VARIANT_2,
Self::VARIANT_3 => ConfigVrrMode::VARIANT_3,
_ => {
log::error!("VRR mode {self:?} has no config representation");
ConfigVrrMode::NEVER
}
}
}
}

View file

@ -181,6 +181,7 @@ impl WorkspaceNode {
surface.send_feedback(&fb);
}
}
self.output.get().update_vrr_state();
}
pub fn remove_fullscreen_node(&self) {
@ -194,6 +195,7 @@ impl WorkspaceNode {
surface.send_feedback(&fb);
}
}
self.output.get().update_vrr_state();
}
}