Add variable-refresh-rate flag

This commit is contained in:
Ivan Molodetskikh
2024-04-14 09:23:15 +04:00
parent 40374942db
commit 3c6d8062c5
6 changed files with 173 additions and 30 deletions
+5
View File
@@ -250,6 +250,8 @@ pub struct Output {
pub position: Option<Position>,
#[knuffel(child, unwrap(argument, str))]
pub mode: Option<Mode>,
#[knuffel(child)]
pub variable_refresh_rate: bool,
}
impl Default for Output {
@@ -261,6 +263,7 @@ impl Default for Output {
transform: Transform::Normal,
position: None,
mode: None,
variable_refresh_rate: false,
}
}
}
@@ -1719,6 +1722,7 @@ mod tests {
transform "flipped-90"
position x=10 y=20
mode "1920x1080@144"
variable-refresh-rate
}
layout {
@@ -1868,6 +1872,7 @@ mod tests {
height: 1080,
refresh: Some(144.),
}),
variable_refresh_rate: true,
}],
layout: Layout {
focus_ring: FocusRing {
+103 -3
View File
@@ -171,6 +171,7 @@ struct Surface {
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
pending_gamma_change: Option<Option<Vec<u16>>>,
vrr_enabled: bool,
/// Tracy frame that goes from vblank to vblank.
vblank_frame: Option<tracy_client::Frame>,
/// Frame name for the VBlank frame.
@@ -739,6 +740,47 @@ impl Tty {
Err(err) => debug!("error setting max bpc: {err:?}"),
}
// Try to enable VRR if requested.
let mut vrr_enabled = false;
if let Some(capable) = is_vrr_capable(&device.drm, connector.handle()) {
if capable {
let word = if config.variable_refresh_rate {
"enabling"
} else {
"disabling"
};
match set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate) {
Ok(enabled) => {
if enabled != config.variable_refresh_rate {
warn!("failed {} VRR", word);
}
vrr_enabled = enabled;
}
Err(err) => {
warn!("error {} VRR: {err:?}", word);
}
}
} else {
if config.variable_refresh_rate {
warn!("cannot enable VRR because connector is not vrr_capable");
}
// Try to disable it anyway to work around a bug where resetting DRM state causes
// vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1.
let res = set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate);
if matches!(res, Ok(true)) {
warn!("error disabling VRR");
// So that we can try it again later.
vrr_enabled = true;
}
}
} else if config.variable_refresh_rate {
warn!("cannot enable VRR because connector is not vrr_capable");
}
let mut gamma_props = GammaProps::new(&device.drm, crtc)
.map_err(|err| debug!("error getting gamma properties: {err:?}"))
.ok();
@@ -864,6 +906,7 @@ impl Tty {
compositor,
dmabuf_feedback,
gamma_props,
vrr_enabled,
pending_gamma_change: None,
vblank_frame: None,
vblank_frame_name,
@@ -874,7 +917,7 @@ impl Tty {
let res = device.surfaces.insert(crtc, surface);
assert!(res.is_none(), "crtc must not have already existed");
niri.add_output(output.clone(), Some(refresh_interval(mode)));
niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled);
// Power on all monitors if necessary and queue a redraw on the new one.
niri.event_loop.insert_idle(move |state| {
@@ -1511,7 +1554,9 @@ impl Tty {
continue;
};
if surface.compositor.pending_mode() == mode {
let change_mode = surface.compositor.pending_mode() != mode;
let change_vrr = surface.vrr_enabled != config.variable_refresh_rate;
if !change_mode && !change_vrr {
continue;
}
@@ -1532,6 +1577,36 @@ impl Tty {
continue;
};
if change_vrr {
if is_vrr_capable(&device.drm, connector.handle()) == Some(true) {
let word = if config.variable_refresh_rate {
"enabling"
} else {
"disabling"
};
match set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate) {
Ok(enabled) => {
if enabled != config.variable_refresh_rate {
warn!("output {:?}: failed {} VRR", surface.name, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!("output {:?}: error {} VRR: {err:?}", surface.name, word);
}
}
} else if config.variable_refresh_rate {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name
);
}
}
if change_mode {
if fallback {
let target = config.mode.unwrap();
warn!(
@@ -1557,9 +1632,11 @@ impl Tty {
let wl_mode = Mode::from(mode);
output.change_current_state(Some(wl_mode), None, None, None);
output.set_preferred(wl_mode);
output_state.frame_clock = FrameClock::new(Some(refresh_interval(mode)));
output_state.frame_clock =
FrameClock::new(Some(refresh_interval(mode)), surface.vrr_enabled);
niri.output_resized(&output);
}
}
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
@@ -2095,6 +2172,29 @@ fn set_max_bpc(device: &DrmDevice, connector: connector::Handle, bpc: u64) -> an
Err(anyhow!("couldn't find max bpc property"))
}
fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option<bool> {
let (_, info, value) = find_drm_property(device, connector, "vrr_capable")?;
info.value_type().convert_value(value).as_boolean()
}
fn set_vrr_enabled(device: &DrmDevice, crtc: crtc::Handle, enabled: bool) -> anyhow::Result<bool> {
let (prop, info, _) =
find_drm_property(device, crtc, "VRR_ENABLED").context("VRR_ENABLED property missing")?;
let value = property::Value::UnsignedRange(if enabled { 1 } else { 0 });
device
.set_property(crtc, prop, value.into())
.context("error setting VRR_ENABLED property")?;
let value = get_drm_property(device, crtc, prop)
.context("VRR_ENABLED property missing after setting")?;
match info.value_type().convert_value(value) {
property::Value::UnsignedRange(value) => Ok(value == 1),
property::Value::Boolean(value) => Ok(value),
_ => bail!("wrong VRR_ENABLED property type"),
}
}
pub fn set_gamma_for_crtc(
device: &DrmDevice,
crtc: crtc::Handle,
+1 -1
View File
@@ -133,7 +133,7 @@ impl Winit {
resources::init(renderer);
shaders::init(renderer);
niri.add_output(self.output.clone(), None);
niri.add_output(self.output.clone(), None, false);
}
pub fn seat_name(&self) -> String {
+19 -1
View File
@@ -7,10 +7,11 @@ use crate::utils::get_monotonic_time;
pub struct FrameClock {
last_presentation_time: Option<Duration>,
refresh_interval_ns: Option<NonZeroU64>,
vrr: bool,
}
impl FrameClock {
pub fn new(refresh_interval: Option<Duration>) -> Self {
pub fn new(refresh_interval: Option<Duration>, vrr: bool) -> Self {
let refresh_interval_ns = if let Some(interval) = &refresh_interval {
assert_eq!(interval.as_secs(), 0);
Some(NonZeroU64::new(interval.subsec_nanos().into()).unwrap())
@@ -21,6 +22,7 @@ impl FrameClock {
Self {
last_presentation_time: None,
refresh_interval_ns,
vrr,
}
}
@@ -29,6 +31,15 @@ impl FrameClock {
.map(|r| Duration::from_nanos(r.get()))
}
pub fn set_vrr(&mut self, vrr: bool) {
if self.vrr == vrr {
return;
}
self.vrr = vrr;
self.last_presentation_time = None;
}
pub fn presented(&mut self, presentation_time: Duration) {
if presentation_time.is_zero() {
// Not interested in these.
@@ -71,6 +82,13 @@ impl FrameClock {
let since_last_ns =
since_last.as_secs() * 1_000_000_000 + u64::from(since_last.subsec_nanos());
let to_next_ns = (since_last_ns / refresh_interval_ns + 1) * refresh_interval_ns;
// If VRR is enabled and more than one frame passed since last presentation, assume that we
// can present immediately.
if self.vrr && to_next_ns > refresh_interval_ns {
now
} else {
last_presentation_time + Duration::from_nanos(to_next_ns)
}
}
}
+2 -2
View File
@@ -1543,7 +1543,7 @@ impl Niri {
}
}
pub fn add_output(&mut self, output: Output, refresh_interval: Option<Duration>) {
pub fn add_output(&mut self, output: Output, refresh_interval: Option<Duration>, vrr: bool) {
let global = output.create_global::<State>(&self.display_handle);
let name = output.name();
@@ -1583,7 +1583,7 @@ impl Niri {
global,
redraw_state: RedrawState::Idle,
unfinished_animations_remain: false,
frame_clock: FrameClock::new(refresh_interval),
frame_clock: FrameClock::new(refresh_interval, vrr),
last_drm_sequence: None,
frame_callback_sequence: 0,
background_buffer: SolidColorBuffer::new(size, CLEAR_COLOR),
+20
View File
@@ -12,6 +12,7 @@ output "eDP-1" {
scale 2.0
transform "90"
position x=1280 y=0
variable-refresh-rate
}
output "HDMI-A-1" {
@@ -113,3 +114,22 @@ The following algorithm is used for positioning outputs.
1. Sort them by their name. This makes it so the automatic positioning does not depend on the order the monitors are connected. This is important because the connection order is non-deterministic at compositor startup.
1. Try to place every output with explicitly configured `position`, in order. If the output overlaps previously placed outputs, place it to the right of all previously placed outputs. In this case, niri will also print a warning.
1. Place every output without explicitly configured `position` by putting it to the right of all previously placed outputs.
### `variable-refresh-rate`
This flag enables variable refresh rate (VRR, also known as adaptive sync, FreeSync, or G-Sync), if the output supports it.
> [!NOTE]
> Some drivers have various issues with VRR.
>
> If the cursor moves at a low framerate with VRR, try setting the `disable-cursor-plane` [debug flag](./Configuration:-Debug-Options.md) and reconnecting the monitor.
>
> If a monitor is not detected as VRR-capable when it should, sometimes unplugging a different monitor fixes it.
>
> Some monitors will continuously modeset (flash black) with VRR enabled; I'm not sure if there's a way to fix it.
```
output "HDMI-A-1" {
variable-refresh-rate
}
```