mirror of
https://github.com/YaLTeR/niri.git
synced 2024-09-11 20:37:25 +03:00
Compare commits
8 Commits
2120088a76
...
c0155b380e
Author | SHA1 | Date | |
---|---|---|---|
|
c0155b380e | ||
|
dfc2d452c5 | ||
|
66f23c3980 | ||
|
7a6ab31ad7 | ||
|
48805de4d1 | ||
|
cbd2952a49 | ||
|
ec780db129 | ||
|
ab4a07b739 |
@ -324,11 +324,31 @@ 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,
|
||||
}
|
||||
|
||||
pub trait VrrOutput {
|
||||
fn is_always_on(&self) -> bool;
|
||||
fn is_on_demand(&self) -> bool;
|
||||
fn is_always_off(&self) -> bool;
|
||||
}
|
||||
|
||||
impl VrrOutput for Output {
|
||||
fn is_always_on(&self) -> bool {
|
||||
self.variable_refresh_rate == Some(Vrr { on_demand: false })
|
||||
}
|
||||
|
||||
fn is_on_demand(&self) -> bool {
|
||||
self.variable_refresh_rate == Some(Vrr { on_demand: true })
|
||||
}
|
||||
|
||||
fn is_always_off(&self) -> bool {
|
||||
self.variable_refresh_rate.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Output {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@ -338,7 +358,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 +372,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 +922,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 +2733,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 +2917,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 {
|
||||
|
@ -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 if there is a matched VRR window rule visible on the output.
|
||||
#[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))]
|
||||
|
@ -153,6 +153,13 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_output_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
|
||||
match self {
|
||||
Backend::Tty(tty) => tty.set_output_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),
|
||||
|
@ -14,7 +14,7 @@ use std::{io, mem};
|
||||
use anyhow::{anyhow, bail, ensure, Context};
|
||||
use bytemuck::cast_slice_mut;
|
||||
use libc::dev_t;
|
||||
use niri_config::Config;
|
||||
use niri_config::{Config, VrrOutput};
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::allocator::format::FormatSet;
|
||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||
@ -832,15 +832,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_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 +849,13 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if config.variable_refresh_rate {
|
||||
if !config.is_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 +863,7 @@ impl Tty {
|
||||
vrr_enabled = true;
|
||||
}
|
||||
}
|
||||
} else if config.variable_refresh_rate {
|
||||
} else if !config.is_always_off() {
|
||||
warn!("cannot enable VRR because connector is not vrr_capable");
|
||||
}
|
||||
|
||||
@ -1661,6 +1659,39 @@ impl Tty {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_output_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) {
|
||||
let _span = tracy_client::span!("Tty::set_output_vrr");
|
||||
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 {
|
||||
let Some(connector) =
|
||||
surface.compositor.pending_connectors().into_iter().next()
|
||||
else {
|
||||
error!("surface pending connectors is empty");
|
||||
return;
|
||||
};
|
||||
let Some(connector) = device.drm_scanner.connectors().get(&connector) else {
|
||||
error!("missing enabled connector in drm_scanner");
|
||||
return;
|
||||
};
|
||||
let output_state = niri.output_state.get_mut(output).unwrap();
|
||||
|
||||
try_to_change_vrr(
|
||||
&device.drm,
|
||||
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");
|
||||
|
||||
@ -1707,7 +1738,7 @@ impl Tty {
|
||||
};
|
||||
|
||||
let change_mode = surface.compositor.pending_mode() != mode;
|
||||
let change_vrr = surface.vrr_enabled != config.variable_refresh_rate;
|
||||
let change_vrr = surface.vrr_enabled != config.is_always_on();
|
||||
if !change_mode && !change_vrr {
|
||||
continue;
|
||||
}
|
||||
@ -1736,7 +1767,7 @@ impl Tty {
|
||||
crtc,
|
||||
surface,
|
||||
output_state,
|
||||
config.variable_refresh_rate,
|
||||
!surface.vrr_enabled,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -4263,6 +4263,101 @@ mod tests {
|
||||
compute_working_area(&output, struts);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_window_height_recomputes_to_auto() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
id: 0,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::AddWindow {
|
||||
id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::AddWindow {
|
||||
id: 2,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::SetWindowHeight(SizeChange::SetFixed(100)),
|
||||
Op::FocusWindowUp,
|
||||
Op::SetWindowHeight(SizeChange::SetFixed(200)),
|
||||
];
|
||||
|
||||
check_ops(&ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_window_in_column_becomes_weight_1() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
id: 0,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::AddWindow {
|
||||
id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::AddWindow {
|
||||
id: 2,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::SetWindowHeight(SizeChange::SetFixed(100)),
|
||||
Op::Communicate(2),
|
||||
Op::FocusWindowUp,
|
||||
Op::SetWindowHeight(SizeChange::SetFixed(200)),
|
||||
Op::Communicate(1),
|
||||
Op::CloseWindow(0),
|
||||
Op::CloseWindow(1),
|
||||
];
|
||||
|
||||
check_ops(&ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_window_in_column_becomes_weight_1_after_fullscreen() {
|
||||
let ops = [
|
||||
Op::AddOutput(1),
|
||||
Op::AddWindow {
|
||||
id: 0,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::AddWindow {
|
||||
id: 1,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::AddWindow {
|
||||
id: 2,
|
||||
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
|
||||
min_max_size: Default::default(),
|
||||
},
|
||||
Op::ConsumeOrExpelWindowLeft,
|
||||
Op::SetWindowHeight(SizeChange::SetFixed(100)),
|
||||
Op::Communicate(2),
|
||||
Op::FocusWindowUp,
|
||||
Op::SetWindowHeight(SizeChange::SetFixed(200)),
|
||||
Op::Communicate(1),
|
||||
Op::CloseWindow(0),
|
||||
Op::FullscreenWindow(1),
|
||||
];
|
||||
|
||||
check_ops(&ops);
|
||||
}
|
||||
|
||||
fn arbitrary_spacing() -> impl Strategy<Value = f64> {
|
||||
// Give equal weight to:
|
||||
// - 0: the element is disabled
|
||||
|
@ -196,9 +196,12 @@ pub enum ColumnWidth {
|
||||
/// to fit the desired content, it can never become smaller than that when moving between monitors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum WindowHeight {
|
||||
/// Automatically computed height, evenly distributed across the column.
|
||||
Auto,
|
||||
/// Fixed height in logical pixels.
|
||||
/// Automatically computed *tile* height, distributed across the column according to weights.
|
||||
///
|
||||
/// This controls the tile height rather than the window height because it's easier in the auto
|
||||
/// height distribution algorithm.
|
||||
Auto { weight: f64 },
|
||||
/// Fixed *window* height in logical pixels.
|
||||
Fixed(f64),
|
||||
}
|
||||
|
||||
@ -312,6 +315,12 @@ impl From<PresetWidth> for ColumnWidth {
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowHeight {
|
||||
const fn auto_1() -> Self {
|
||||
Self::Auto { weight: 1. }
|
||||
}
|
||||
}
|
||||
|
||||
impl TileData {
|
||||
pub fn new<W: LayoutElement>(tile: &Tile<W>, height: WindowHeight) -> Self {
|
||||
let mut rv = Self {
|
||||
@ -1106,6 +1115,13 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
let tile = column.tiles.remove(window_idx);
|
||||
column.data.remove(window_idx);
|
||||
|
||||
// If one window is left, reset its weight to 1.
|
||||
if column.data.len() == 1 {
|
||||
if let WindowHeight::Auto { weight } = &mut column.data[0].height {
|
||||
*weight = 1.;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output) = &self.output {
|
||||
tile.window().output_leave(output);
|
||||
}
|
||||
@ -2265,6 +2281,14 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
let window = col.tiles.remove(tile_idx).into_window();
|
||||
col.data.remove(tile_idx);
|
||||
col.active_tile_idx = min(col.active_tile_idx, col.tiles.len() - 1);
|
||||
|
||||
// If one window is left, reset its weight to 1.
|
||||
if col.data.len() == 1 {
|
||||
if let WindowHeight::Auto { weight } = &mut col.data[0].height {
|
||||
*weight = 1.;
|
||||
}
|
||||
}
|
||||
|
||||
col.update_tile_sizes(false);
|
||||
self.data[col_idx].update(col);
|
||||
let width = col.width;
|
||||
@ -2937,7 +2961,7 @@ impl<W: LayoutElement> Column<W> {
|
||||
|
||||
fn add_tile(&mut self, tile: Tile<W>, animate: bool) {
|
||||
self.is_fullscreen = false;
|
||||
self.data.push(TileData::new(&tile, WindowHeight::Auto));
|
||||
self.data.push(TileData::new(&tile, WindowHeight::auto_1()));
|
||||
self.tiles.push(tile);
|
||||
self.update_tile_sizes(animate);
|
||||
}
|
||||
@ -3020,13 +3044,14 @@ impl<W: LayoutElement> Column<W> {
|
||||
// Compute the tile heights. Start by converting window heights to tile heights.
|
||||
let mut heights = zip(&self.tiles, &self.data)
|
||||
.map(|(tile, data)| match data.height {
|
||||
WindowHeight::Auto => WindowHeight::Auto,
|
||||
auto @ WindowHeight::Auto { .. } => auto,
|
||||
WindowHeight::Fixed(height) => {
|
||||
WindowHeight::Fixed(tile.tile_height_for_window_height(height.round().max(1.)))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut height_left = self.working_area.size.h - self.options.gaps;
|
||||
let gaps_left = self.options.gaps * (self.tiles.len() + 1) as f64;
|
||||
let mut height_left = self.working_area.size.h - gaps_left;
|
||||
let mut auto_tiles_left = self.tiles.len();
|
||||
|
||||
// Subtract all fixed-height tiles.
|
||||
@ -3044,11 +3069,22 @@ impl<W: LayoutElement> Column<W> {
|
||||
*h = f64::max(*h, min_size.h);
|
||||
}
|
||||
|
||||
height_left -= *h + self.options.gaps;
|
||||
height_left -= *h;
|
||||
auto_tiles_left -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
let mut total_weight: f64 = heights
|
||||
.iter()
|
||||
.filter_map(|h| {
|
||||
if let WindowHeight::Auto { weight } = *h {
|
||||
Some(weight)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
// Iteratively try to distribute the remaining height, checking against tile min heights.
|
||||
// Pick an auto height according to the current sizes, then check if it satisfies all
|
||||
// remaining min heights. If not, allocate fixed height to those tiles and repeat the
|
||||
@ -3064,15 +3100,17 @@ impl<W: LayoutElement> Column<W> {
|
||||
// Wayland requires us to round the requested size for a window to integer logical
|
||||
// pixels, therefore we compute the remaining auto height dynamically.
|
||||
let mut height_left_2 = height_left;
|
||||
let mut auto_tiles_left_2 = auto_tiles_left;
|
||||
let mut total_weight_2 = total_weight;
|
||||
let mut unsatisfied_min = false;
|
||||
for ((h, tile), min_size) in zip(zip(&mut heights, &self.tiles), &min_size) {
|
||||
if matches!(h, WindowHeight::Fixed(_)) {
|
||||
continue;
|
||||
}
|
||||
let weight = match *h {
|
||||
WindowHeight::Auto { weight } => weight,
|
||||
WindowHeight::Fixed(_) => continue,
|
||||
};
|
||||
let factor = weight / total_weight_2;
|
||||
|
||||
// Compute the current auto height.
|
||||
let auto = height_left_2 / auto_tiles_left_2 as f64 - self.options.gaps;
|
||||
let auto = height_left_2 * factor;
|
||||
let mut auto = tile.tile_height_for_window_height(
|
||||
tile.window_height_for_tile_height(auto).round().max(1.),
|
||||
);
|
||||
@ -3081,13 +3119,14 @@ impl<W: LayoutElement> Column<W> {
|
||||
if min_size.h > 0. && min_size.h > auto {
|
||||
auto = min_size.h;
|
||||
*h = WindowHeight::Fixed(auto);
|
||||
height_left -= auto + self.options.gaps;
|
||||
height_left -= auto;
|
||||
total_weight -= weight;
|
||||
auto_tiles_left -= 1;
|
||||
unsatisfied_min = true;
|
||||
}
|
||||
|
||||
height_left_2 -= auto + self.options.gaps;
|
||||
auto_tiles_left_2 -= 1;
|
||||
height_left_2 -= auto;
|
||||
total_weight_2 -= weight;
|
||||
}
|
||||
|
||||
// If some min height was unsatisfied, then we allocated the tile more than the auto
|
||||
@ -3099,18 +3138,21 @@ impl<W: LayoutElement> Column<W> {
|
||||
|
||||
// All min heights were satisfied, fill them in.
|
||||
for (h, tile) in zip(&mut heights, &self.tiles) {
|
||||
if matches!(h, WindowHeight::Fixed(_)) {
|
||||
continue;
|
||||
}
|
||||
let weight = match *h {
|
||||
WindowHeight::Auto { weight } => weight,
|
||||
WindowHeight::Fixed(_) => continue,
|
||||
};
|
||||
let factor = weight / total_weight;
|
||||
|
||||
// Compute the current auto height.
|
||||
let auto = height_left / auto_tiles_left as f64 - self.options.gaps;
|
||||
let auto = height_left * factor;
|
||||
let auto = tile.tile_height_for_window_height(
|
||||
tile.window_height_for_tile_height(auto).round().max(1.),
|
||||
);
|
||||
|
||||
*h = WindowHeight::Fixed(auto);
|
||||
height_left -= auto + self.options.gaps;
|
||||
height_left -= auto;
|
||||
total_weight -= weight;
|
||||
auto_tiles_left -= 1;
|
||||
}
|
||||
|
||||
@ -3202,6 +3244,16 @@ impl<W: LayoutElement> Column<W> {
|
||||
assert_eq!(self.tiles.len(), 1);
|
||||
}
|
||||
|
||||
if self.tiles.len() == 1 {
|
||||
if let WindowHeight::Auto { weight } = self.data[0].height {
|
||||
assert_eq!(
|
||||
weight, 1.,
|
||||
"auto height weight must reset to 1 for a single window"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut found_fixed = false;
|
||||
for (tile, data) in zip(&self.tiles, &self.data) {
|
||||
assert!(Rc::ptr_eq(&self.options, &tile.options));
|
||||
assert_eq!(self.scale, tile.scale());
|
||||
@ -3216,6 +3268,14 @@ impl<W: LayoutElement> Column<W> {
|
||||
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
|
||||
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
|
||||
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
|
||||
|
||||
if matches!(data.height, WindowHeight::Fixed(_)) {
|
||||
assert!(
|
||||
!found_fixed,
|
||||
"there can only be one fixed-height window in a column"
|
||||
);
|
||||
found_fixed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3309,10 +3369,19 @@ impl<W: LayoutElement> Column<W> {
|
||||
|
||||
fn set_window_height(&mut self, change: SizeChange, tile_idx: Option<usize>, animate: bool) {
|
||||
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
||||
|
||||
// Start by converting all heights to automatic, since only one window in the column can be
|
||||
// fixed-height. If the current tile is already fixed, however, we can skip that step.
|
||||
// Which is not only for optimization, but also preserves automatic weights in case one
|
||||
// window is resized in such a way that other windows hit their min size, and then back.
|
||||
if !matches!(self.data[tile_idx].height, WindowHeight::Fixed(_)) {
|
||||
self.convert_heights_to_auto();
|
||||
}
|
||||
|
||||
let current = self.data[tile_idx].height;
|
||||
let tile = &self.tiles[tile_idx];
|
||||
let current_window_px = match current {
|
||||
WindowHeight::Auto => tile.window_size().h,
|
||||
WindowHeight::Auto { .. } => tile.window_size().h,
|
||||
WindowHeight::Fixed(height) => height,
|
||||
};
|
||||
let current_tile_px = tile.tile_height_for_window_height(current_window_px);
|
||||
@ -3361,10 +3430,32 @@ impl<W: LayoutElement> Column<W> {
|
||||
|
||||
fn reset_window_height(&mut self, tile_idx: Option<usize>, animate: bool) {
|
||||
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
||||
self.data[tile_idx].height = WindowHeight::Auto;
|
||||
self.data[tile_idx].height = WindowHeight::auto_1();
|
||||
self.update_tile_sizes(animate);
|
||||
}
|
||||
|
||||
/// Converts all heights in the column to automatic, preserving the apparent heights.
|
||||
///
|
||||
/// All weights are recomputed to preserve the current tile heights while "centering" the
|
||||
/// weights at the median window height (it gets weight = 1).
|
||||
///
|
||||
/// One case where apparent heights will not be preserved is when the column is taller than the
|
||||
/// working area.
|
||||
fn convert_heights_to_auto(&mut self) {
|
||||
let heights: Vec<_> = self.tiles.iter().map(|tile| tile.tile_size().h).collect();
|
||||
|
||||
// Weights are invariant to multiplication: a column with weights 2, 2, 1 is equivalent to
|
||||
// a column with weights 4, 4, 2. So we find the median window height and use that as 1.
|
||||
let mut sorted = heights.clone();
|
||||
sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let median = sorted[sorted.len() / 2];
|
||||
|
||||
for (data, height) in zip(&mut self.data, heights) {
|
||||
let weight = height / median;
|
||||
data.height = WindowHeight::Auto { weight };
|
||||
}
|
||||
}
|
||||
|
||||
fn set_fullscreen(&mut self, is_fullscreen: bool) {
|
||||
if self.is_fullscreen == is_fullscreen {
|
||||
return;
|
||||
@ -3398,7 +3489,7 @@ impl<W: LayoutElement> Column<W> {
|
||||
|
||||
// Chain with a dummy value to be able to get one past all tiles' Y.
|
||||
let dummy = TileData {
|
||||
height: WindowHeight::Auto,
|
||||
height: WindowHeight::auto_1(),
|
||||
size: Size::default(),
|
||||
interactively_resizing_by_left_edge: false,
|
||||
};
|
||||
|
63
src/niri.rs
63
src/niri.rs
@ -12,7 +12,7 @@ use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as
|
||||
use anyhow::{bail, ensure, Context};
|
||||
use calloop::futures::Scheduler;
|
||||
use niri_config::{
|
||||
Config, FloatOrInt, Key, Modifiers, PreviewRender, TrackLayout, WorkspaceReference,
|
||||
Config, FloatOrInt, Key, Modifiers, PreviewRender, TrackLayout, VrrOutput, WorkspaceReference,
|
||||
DEFAULT_BACKGROUND_COLOR,
|
||||
};
|
||||
use niri_ipc::Workspace;
|
||||
@ -1202,8 +1202,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3089,6 +3095,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 +3126,49 @@ 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_on_demand())
|
||||
else {
|
||||
warn!("error getting output config for {}", output.name());
|
||||
return;
|
||||
};
|
||||
if on_demand {
|
||||
let last = self.output_state[output].frame_clock.vrr();
|
||||
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(|_, states| {
|
||||
if !visible {
|
||||
let surface_primary_scanout_output = states
|
||||
.data_map
|
||||
.get_or_insert_threadsafe(Mutex::<PrimaryScanoutOutput>::default);
|
||||
if surface_primary_scanout_output
|
||||
.lock()
|
||||
.unwrap()
|
||||
.current_output()
|
||||
.as_ref()
|
||||
== Some(output)
|
||||
{
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
visible
|
||||
}
|
||||
});
|
||||
|
||||
if last != current {
|
||||
backend.set_output_vrr(self, output, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_primary_scanout_output(
|
||||
&self,
|
||||
output: &Output,
|
||||
@ -3181,13 +3232,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()
|
||||
|
@ -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!(),
|
||||
}
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user