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>
This commit is contained in:
Michael Yang
2024-08-22 18:58:07 +10:00
committed by GitHub
parent dfc2d452c5
commit f1894f6f9a
10 changed files with 201 additions and 66 deletions
+26 -4
View File
@@ -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 {
+25 -11
View File
@@ -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))]
+7
View File
@@ -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),
+50 -38
View File
@@ -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) {
+4
View File
@@ -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.
+46 -7
View File
@@ -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()
+5 -5
View File
@@ -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!(),
}
+7
View File
@@ -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());
+10 -1
View File
@@ -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>
+21
View File
@@ -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.