aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Yang <admin@my4ng.dev>2024-08-22 18:58:07 +1000
committerGitHub <noreply@github.com>2024-08-22 11:58:07 +0300
commitf1894f6f9a7470b8b1493cf96b28a48e8a704636 (patch)
treed70a3ab350d39dea1b8240e2a92b281b01be5d12
parentdfc2d452c55d59d6d9014c98a9da3a082c4f7379 (diff)
downloadniri-f1894f6f9a7470b8b1493cf96b28a48e8a704636.tar.gz
niri-f1894f6f9a7470b8b1493cf96b28a48e8a704636.tar.bz2
niri-f1894f6f9a7470b8b1493cf96b28a48e8a704636.zip
feature: add on-demand vrr (#586)
* feature: add on-demand vrr * Don't require connector::Info in try_to_set_vrr * Improve VRR help message * Rename connector_handle => connector * Fix tracy span name * Move on demand vrr flag set higher * wiki: Mention on-demand VRR --------- Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
-rw-r--r--niri-config/src/lib.rs30
-rw-r--r--niri-ipc/src/lib.rs36
-rw-r--r--src/backend/mod.rs7
-rw-r--r--src/backend/tty.rs88
-rw-r--r--src/frame_clock.rs4
-rw-r--r--src/niri.rs53
-rw-r--r--src/protocols/output_management.rs10
-rw-r--r--src/window/mod.rs7
-rw-r--r--wiki/Configuration:-Outputs.md11
-rw-r--r--wiki/Configuration:-Window-Rules.md21
10 files changed, 201 insertions, 66 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index e9c7d346..b9edf4ce 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -324,11 +324,25 @@ pub struct Output {
#[knuffel(child, unwrap(argument, str))]
pub mode: Option<ConfiguredMode>,
#[knuffel(child)]
- pub variable_refresh_rate: bool,
+ pub variable_refresh_rate: Option<Vrr>,
#[knuffel(child, default = DEFAULT_BACKGROUND_COLOR)]
pub background_color: Color,
}
+impl Output {
+ pub fn is_vrr_always_on(&self) -> bool {
+ self.variable_refresh_rate == Some(Vrr { on_demand: false })
+ }
+
+ pub fn is_vrr_on_demand(&self) -> bool {
+ self.variable_refresh_rate == Some(Vrr { on_demand: true })
+ }
+
+ pub fn is_vrr_always_off(&self) -> bool {
+ self.variable_refresh_rate.is_none()
+ }
+}
+
impl Default for Output {
fn default() -> Self {
Self {
@@ -338,7 +352,7 @@ impl Default for Output {
transform: Transform::Normal,
position: None,
mode: None,
- variable_refresh_rate: false,
+ variable_refresh_rate: None,
background_color: DEFAULT_BACKGROUND_COLOR,
}
}
@@ -352,6 +366,12 @@ pub struct Position {
pub y: i32,
}
+#[derive(knuffel::Decode, Debug, Clone, PartialEq, Default)]
+pub struct Vrr {
+ #[knuffel(property, default = false)]
+ pub on_demand: bool,
+}
+
// MIN and MAX generics are only used during parsing to check the value.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct FloatOrInt<const MIN: i32, const MAX: i32>(pub f64);
@@ -896,6 +916,8 @@ pub struct WindowRule {
pub clip_to_geometry: Option<bool>,
#[knuffel(child, unwrap(argument))]
pub block_out_from: Option<BlockOutFrom>,
+ #[knuffel(child, unwrap(argument))]
+ pub variable_refresh_rate: Option<bool>,
}
// Remember to update the PartialEq impl when adding fields!
@@ -2705,7 +2727,7 @@ mod tests {
transform "flipped-90"
position x=10 y=20
mode "1920x1080@144"
- variable-refresh-rate
+ variable-refresh-rate on-demand=true
background-color "rgba(25, 25, 102, 1.0)"
}
@@ -2889,7 +2911,7 @@ mod tests {
height: 1080,
refresh: Some(144.),
}),
- variable_refresh_rate: true,
+ variable_refresh_rate: Some(Vrr { on_demand: true }),
background_color: Color::from_rgba8_unpremul(25, 25, 102, 255),
}]),
layout: Layout {
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index 7317436c..713db8af 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -352,18 +352,11 @@ pub enum OutputAction {
#[cfg_attr(feature = "clap", command(subcommand))]
position: PositionToSet,
},
- /// Toggle variable refresh rate.
+ /// Set the variable refresh rate mode.
Vrr {
- /// Whether to enable variable refresh rate.
- #[cfg_attr(
- feature = "clap",
- arg(
- value_name = "ON|OFF",
- action = clap::ArgAction::Set,
- value_parser = clap::builder::BoolishValueParser::new(),
- ),
- )]
- enable: bool,
+ /// Variable refresh rate mode to set.
+ #[cfg_attr(feature = "clap", command(flatten))]
+ vrr: VrrToSet,
},
}
@@ -425,6 +418,27 @@ pub struct ConfiguredPosition {
pub y: i32,
}
+/// Output VRR to set.
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "clap", derive(clap::Args))]
+#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
+pub struct VrrToSet {
+ /// Whether to enable variable refresh rate.
+ #[cfg_attr(
+ feature = "clap",
+ arg(
+ value_name = "ON|OFF",
+ action = clap::ArgAction::Set,
+ value_parser = clap::builder::BoolishValueParser::new(),
+ hide_possible_values = true,
+ ),
+ )]
+ pub vrr: bool,
+ /// Only enable when the output shows a window matching the variable-refresh-rate window rule.
+ #[cfg_attr(feature = "clap", arg(long))]
+ pub on_demand: bool,
+}
+
/// Connected output.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index c5662184..850f47ef 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -153,6 +153,13 @@ impl Backend {
}
}
+ pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
+ match self {
+ Backend::Tty(tty) => tty.set_output_on_demand_vrr(niri, output, enable_vrr),
+ Backend::Winit(_) => (),
+ }
+ }
+
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
match self {
Backend::Tty(tty) => tty.on_output_config_changed(niri),
diff --git a/src/backend/tty.rs b/src/backend/tty.rs
index 10becafe..1d891d91 100644
--- a/src/backend/tty.rs
+++ b/src/backend/tty.rs
@@ -180,6 +180,7 @@ struct TtyOutputState {
struct Surface {
name: String,
compositor: GbmDrmCompositor,
+ connector: connector::Handle,
dmabuf_feedback: Option<SurfaceDmabufFeedback>,
gamma_props: Option<GammaProps>,
/// Gamma change to apply upon session resume.
@@ -426,18 +427,6 @@ impl Tty {
}
// Restore VRR.
- let Some(connector) =
- surface.compositor.pending_connectors().into_iter().next()
- else {
- error!("surface pending connectors is empty");
- continue;
- };
- let Some(connector) = device.drm_scanner.connectors().get(&connector)
- else {
- error!("missing enabled connector in drm_scanner");
- continue;
- };
-
let output = niri
.global_space
.outputs()
@@ -457,7 +446,7 @@ impl Tty {
try_to_change_vrr(
&device.drm,
- connector,
+ surface.connector,
*crtc,
surface,
output_state,
@@ -832,15 +821,13 @@ impl Tty {
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"
- };
+ // Even if on-demand, we still disable it until later checks.
+ let vrr = config.is_vrr_always_on();
+ let word = if vrr { "enabling" } else { "disabling" };
- match set_vrr_enabled(&device.drm, crtc, config.variable_refresh_rate) {
+ match set_vrr_enabled(&device.drm, crtc, vrr) {
Ok(enabled) => {
- if enabled != config.variable_refresh_rate {
+ if enabled != vrr {
warn!("failed {} VRR", word);
}
@@ -851,13 +838,13 @@ impl Tty {
}
}
} else {
- if config.variable_refresh_rate {
+ if !config.is_vrr_always_off() {
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);
+ let res = set_vrr_enabled(&device.drm, crtc, false);
if matches!(res, Ok(true)) {
warn!("error disabling VRR");
@@ -865,7 +852,7 @@ impl Tty {
vrr_enabled = true;
}
}
- } else if config.variable_refresh_rate {
+ } else if !config.is_vrr_always_off() {
warn!("cannot enable VRR because connector is not vrr_capable");
}
@@ -1017,6 +1004,7 @@ impl Tty {
let surface = Surface {
name: output_name.clone(),
+ connector: connector.handle(),
compositor,
dmabuf_feedback,
gamma_props,
@@ -1661,6 +1649,33 @@ impl Tty {
}
}
+ pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
+ let _span = tracy_client::span!("Tty::set_output_on_demand_vrr");
+
+ let output_state = niri.output_state.get_mut(output).unwrap();
+ output_state.on_demand_vrr_enabled = enable_vrr;
+ if output_state.frame_clock.vrr() == enable_vrr {
+ return;
+ }
+ for (&node, device) in self.devices.iter_mut() {
+ for (&crtc, surface) in device.surfaces.iter_mut() {
+ let tty_state: &TtyOutputState = output.user_data().get().unwrap();
+ if tty_state.node == node && tty_state.crtc == crtc {
+ try_to_change_vrr(
+ &device.drm,
+ surface.connector,
+ crtc,
+ surface,
+ output_state,
+ enable_vrr,
+ );
+ self.refresh_ipc_outputs(niri);
+ return;
+ }
+ }
+ }
+ }
+
pub fn on_output_config_changed(&mut self, niri: &mut Niri) {
let _span = tracy_client::span!("Tty::on_output_config_changed");
@@ -1675,9 +1690,7 @@ impl Tty {
let mut to_connect = vec![];
for (&node, device) in &mut self.devices {
- for surface in device.surfaces.values_mut() {
- let crtc = surface.compositor.crtc();
-
+ for (&crtc, surface) in device.surfaces.iter_mut() {
let config = self
.config
.borrow()
@@ -1691,12 +1704,8 @@ impl Tty {
}
// Check if we need to change the mode.
- let Some(connector) = surface.compositor.pending_connectors().into_iter().next()
+ let Some(connector) = device.drm_scanner.connectors().get(&surface.connector)
else {
- error!("surface pending connectors is empty");
- continue;
- };
- let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
error!("missing enabled connector in drm_scanner");
continue;
};
@@ -1707,8 +1716,9 @@ impl Tty {
};
let change_mode = surface.compositor.pending_mode() != mode;
- let change_vrr = surface.vrr_enabled != config.variable_refresh_rate;
- if !change_mode && !change_vrr {
+ let change_always_vrr = surface.vrr_enabled != config.is_vrr_always_on();
+ let is_on_demand_vrr = config.is_vrr_on_demand();
+ if !change_mode && !change_always_vrr && !is_on_demand_vrr {
continue;
}
@@ -1729,14 +1739,16 @@ impl Tty {
continue;
};
- if change_vrr {
+ if (is_on_demand_vrr && surface.vrr_enabled != output_state.on_demand_vrr_enabled)
+ || (!is_on_demand_vrr && change_always_vrr)
+ {
try_to_change_vrr(
&device.drm,
- connector,
+ connector.handle(),
crtc,
surface,
output_state,
- config.variable_refresh_rate,
+ !surface.vrr_enabled,
);
}
@@ -2388,7 +2400,7 @@ pub fn set_gamma_for_crtc(
fn try_to_change_vrr(
device: &DrmDevice,
- connector: &connector::Info,
+ connector: connector::Handle,
crtc: crtc::Handle,
surface: &mut Surface,
output_state: &mut crate::niri::OutputState,
@@ -2396,7 +2408,7 @@ fn try_to_change_vrr(
) {
let _span = tracy_client::span!("try_to_change_vrr");
- if is_vrr_capable(device, connector.handle()) == Some(true) {
+ if is_vrr_capable(device, connector) == Some(true) {
let word = if enable_vrr { "enabling" } else { "disabling" };
match set_vrr_enabled(device, crtc, enable_vrr) {
diff --git a/src/frame_clock.rs b/src/frame_clock.rs
index d60867ea..f96475c8 100644
--- a/src/frame_clock.rs
+++ b/src/frame_clock.rs
@@ -40,6 +40,10 @@ impl FrameClock {
self.last_presentation_time = None;
}
+ pub fn vrr(&self) -> bool {
+ self.vrr
+ }
+
pub fn presented(&mut self, presentation_time: Duration) {
if presentation_time.is_zero() {
// Not interested in these.
diff --git a/src/niri.rs b/src/niri.rs
index 9aec5ed1..ac07f113 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -299,6 +299,7 @@ pub struct OutputState {
pub global: GlobalId,
pub frame_clock: FrameClock,
pub redraw_state: RedrawState,
+ pub on_demand_vrr_enabled: bool,
// After the last redraw, some ongoing animations still remain.
pub unfinished_animations_remain: bool,
/// Last sequence received in a vblank event.
@@ -1202,8 +1203,14 @@ impl State {
}
}
}
- niri_ipc::OutputAction::Vrr { enable } => {
- config.variable_refresh_rate = enable;
+ niri_ipc::OutputAction::Vrr { vrr } => {
+ config.variable_refresh_rate = if vrr.vrr {
+ Some(niri_config::Vrr {
+ on_demand: vrr.on_demand,
+ })
+ } else {
+ None
+ }
}
}
}
@@ -2005,6 +2012,7 @@ impl Niri {
let state = OutputState {
global,
redraw_state: RedrawState::Idle,
+ on_demand_vrr_enabled: false,
unfinished_animations_remain: false,
frame_clock: FrameClock::new(refresh_interval, vrr),
last_drm_sequence: None,
@@ -3089,6 +3097,8 @@ impl Niri {
lock_state => self.lock_state = lock_state,
}
+ self.refresh_on_demand_vrr(backend, output);
+
// Send the frame callbacks.
//
// FIXME: The logic here could be a bit smarter. Currently, during an animation, the
@@ -3118,6 +3128,39 @@ impl Niri {
});
}
+ pub fn refresh_on_demand_vrr(&mut self, backend: &mut Backend, output: &Output) {
+ let _span = tracy_client::span!("Niri::refresh_on_demand_vrr");
+ let Some(on_demand) = self
+ .config
+ .borrow()
+ .outputs
+ .find(&output.name())
+ .map(|output| output.is_vrr_on_demand())
+ else {
+ warn!("error getting output config for {}", output.name());
+ return;
+ };
+ if !on_demand {
+ return;
+ }
+
+ let current = self.layout.windows_for_output(output).any(|mapped| {
+ mapped.rules().variable_refresh_rate == Some(true) && {
+ let mut visible = false;
+ mapped.window.with_surfaces(|surface, states| {
+ if !visible
+ && surface_primary_scanout_output(surface, states).as_ref() == Some(output)
+ {
+ visible = true;
+ }
+ });
+ visible
+ }
+ });
+
+ backend.set_output_on_demand_vrr(self, output, current);
+ }
+
pub fn update_primary_scanout_output(
&self,
output: &Output,
@@ -3181,13 +3224,9 @@ impl Niri {
let offscreen_id = offscreen_id.as_ref();
win.with_surfaces(|surface, states| {
- states
- .data_map
- .insert_if_missing_threadsafe(Mutex::<PrimaryScanoutOutput>::default);
let surface_primary_scanout_output = states
.data_map
- .get::<Mutex<PrimaryScanoutOutput>>()
- .unwrap();
+ .get_or_insert_threadsafe(Mutex::<PrimaryScanoutOutput>::default);
surface_primary_scanout_output
.lock()
.unwrap()
diff --git a/src/protocols/output_management.rs b/src/protocols/output_management.rs
index 23e419ba..3839bdb4 100644
--- a/src/protocols/output_management.rs
+++ b/src/protocols/output_management.rs
@@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::iter::zip;
use std::mem;
-use niri_config::FloatOrInt;
+use niri_config::{FloatOrInt, Vrr};
use niri_ipc::Transform;
use smithay::reexports::wayland_protocols_wlr::output_management::v1::server::{
zwlr_output_configuration_head_v1, zwlr_output_configuration_v1, zwlr_output_head_v1,
@@ -693,9 +693,9 @@ where
new_config.scale = Some(FloatOrInt(scale));
}
zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => {
- let enabled = match state {
- WEnum::Value(AdaptiveSyncState::Enabled) => true,
- WEnum::Value(AdaptiveSyncState::Disabled) => false,
+ let vrr = match state {
+ WEnum::Value(AdaptiveSyncState::Enabled) => Some(Vrr { on_demand: false }),
+ WEnum::Value(AdaptiveSyncState::Disabled) => None,
_ => {
warn!("SetAdaptativeSync: unknown requested adaptative sync");
conf_head.post_error(
@@ -705,7 +705,7 @@ where
return;
}
};
- new_config.variable_refresh_rate = enabled;
+ new_config.variable_refresh_rate = vrr;
}
_ => unreachable!(),
}
diff --git a/src/window/mod.rs b/src/window/mod.rs
index 0e0774fd..4d06bb04 100644
--- a/src/window/mod.rs
+++ b/src/window/mod.rs
@@ -72,6 +72,9 @@ pub struct ResolvedWindowRules {
/// Whether to block out this window from certain render targets.
pub block_out_from: Option<BlockOutFrom>,
+
+ /// Whether to enable VRR on this window's primary output if it is on-demand.
+ pub variable_refresh_rate: Option<bool>,
}
impl<'a> WindowRef<'a> {
@@ -132,6 +135,7 @@ impl ResolvedWindowRules {
geometry_corner_radius: None,
clip_to_geometry: None,
block_out_from: None,
+ variable_refresh_rate: None,
}
}
@@ -231,6 +235,9 @@ impl ResolvedWindowRules {
if let Some(x) = rule.block_out_from {
resolved.block_out_from = Some(x);
}
+ if let Some(x) = rule.variable_refresh_rate {
+ resolved.variable_refresh_rate = Some(x);
+ }
}
resolved.open_on_output = open_on_output.map(|x| x.to_owned());
diff --git a/wiki/Configuration:-Outputs.md b/wiki/Configuration:-Outputs.md
index eef06fef..f7b3adf8 100644
--- a/wiki/Configuration:-Outputs.md
+++ b/wiki/Configuration:-Outputs.md
@@ -12,7 +12,7 @@ output "eDP-1" {
scale 2.0
transform "90"
position x=1280 y=0
- variable-refresh-rate
+ variable-refresh-rate // on-demand=true
background-color "#003300"
}
@@ -147,6 +147,15 @@ output "HDMI-A-1" {
}
```
+<sup>Since: 0.1.9</sup> You can also set the `on-demand=true` property, which will only enable VRR when this output shows a window matching the `variable-refresh-rate` window rule.
+This is helpful to avoid various issues with VRR, since it can be disabled most of the time, and only enabled for specific windows, like games or video players.
+
+```kdl
+output "HDMI-A-1" {
+ variable-refresh-rate on-demand=true
+}
+```
+
### `background-color`
<sup>Since: 0.1.8</sup>
diff --git a/wiki/Configuration:-Window-Rules.md b/wiki/Configuration:-Window-Rules.md
index 3daceb15..b13b4661 100644
--- a/wiki/Configuration:-Window-Rules.md
+++ b/wiki/Configuration:-Window-Rules.md
@@ -47,6 +47,7 @@ window-rule {
opacity 0.5
block-out-from "screencast"
// block-out-from "screen-capture"
+ variable-refresh-rate true
focus-ring {
// off
@@ -391,6 +392,26 @@ window-rule {
}
```
+#### `variable-refresh-rate`
+
+<sup>Since: 0.1.9</sup>
+
+If set to true, whenever this window displays on an output with on-demand VRR, it will enable VRR on that output.
+
+```kdl
+// Configure some output with on-demand VRR.
+output "HDMI-A-1" {
+ variable-refresh-rate on-demand=true
+}
+
+// Enable on-demand VRR when mpv displays on the output.
+window-rule {
+ match app-id="^mpv$"
+
+ variable-refresh-rate true
+}
+```
+
#### `draw-border-with-background`
Override whether the border and the focus ring draw with a background.