aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-04-14 09:23:15 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-04-14 09:37:42 +0400
commit3c6d8062c5c9b31afb5eeacd66a58b19b6566d9f (patch)
treeec1000121dbe270a81152ebd189871e50b8f496c
parent40374942db919b798fe5a4bd15b108a61c34aadc (diff)
downloadniri-3c6d8062c5c9b31afb5eeacd66a58b19b6566d9f.tar.gz
niri-3c6d8062c5c9b31afb5eeacd66a58b19b6566d9f.tar.bz2
niri-3c6d8062c5c9b31afb5eeacd66a58b19b6566d9f.zip
Add variable-refresh-rate flag
-rw-r--r--niri-config/src/lib.rs5
-rw-r--r--src/backend/tty.rs150
-rw-r--r--src/backend/winit.rs2
-rw-r--r--src/frame_clock.rs22
-rw-r--r--src/niri.rs4
-rw-r--r--wiki/Configuration:-Outputs.md20
6 files changed, 173 insertions, 30 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 59cf01cf..d0d517e1 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -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 {
diff --git a/src/backend/tty.rs b/src/backend/tty.rs
index e00bdc4a..abeec237 100644
--- a/src/backend/tty.rs
+++ b/src/backend/tty.rs
@@ -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,33 +1577,65 @@ impl Tty {
continue;
};
- if fallback {
- let target = config.mode.unwrap();
- warn!(
- "output {:?}: configured mode {}x{}{} could not be found, \
- falling back to preferred",
- surface.name,
- target.width,
- target.height,
- if let Some(refresh) = target.refresh {
- format!("@{refresh}")
+ if change_vrr {
+ if is_vrr_capable(&device.drm, connector.handle()) == Some(true) {
+ let word = if config.variable_refresh_rate {
+ "enabling"
} else {
- String::new()
- },
- );
- }
+ "disabling"
+ };
- debug!("output {:?}: picking mode: {mode:?}", surface.name);
- if let Err(err) = surface.compositor.use_mode(mode) {
- warn!("error changing mode: {err:?}");
- continue;
+ 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
+ );
+ }
}
- 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)));
- niri.output_resized(&output);
+ if change_mode {
+ if fallback {
+ let target = config.mode.unwrap();
+ warn!(
+ "output {:?}: configured mode {}x{}{} could not be found, \
+ falling back to preferred",
+ surface.name,
+ target.width,
+ target.height,
+ if let Some(refresh) = target.refresh {
+ format!("@{refresh}")
+ } else {
+ String::new()
+ },
+ );
+ }
+
+ debug!("output {:?}: picking mode: {mode:?}", surface.name);
+ if let Err(err) = surface.compositor.use_mode(mode) {
+ warn!("error changing mode: {err:?}");
+ continue;
+ }
+
+ 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)), surface.vrr_enabled);
+ niri.output_resized(&output);
+ }
}
for (connector, crtc) in device.drm_scanner.crtcs() {
@@ -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,
diff --git a/src/backend/winit.rs b/src/backend/winit.rs
index 9e05822f..acf16481 100644
--- a/src/backend/winit.rs
+++ b/src/backend/winit.rs
@@ -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 {
diff --git a/src/frame_clock.rs b/src/frame_clock.rs
index ad8cf2f7..d60867ea 100644
--- a/src/frame_clock.rs
+++ b/src/frame_clock.rs
@@ -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;
- last_presentation_time + Duration::from_nanos(to_next_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)
+ }
}
}
diff --git a/src/niri.rs b/src/niri.rs
index f14f7dcd..145d143e 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -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),
diff --git a/wiki/Configuration:-Outputs.md b/wiki/Configuration:-Outputs.md
index a87608bf..4dfcab7a 100644
--- a/wiki/Configuration:-Outputs.md
+++ b/wiki/Configuration:-Outputs.md
@@ -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
+}
+```