Implement resize transactions

This commit is contained in:
Ivan Molodetskikh
2024-08-22 14:44:11 +03:00
parent cf357d7058
commit 7bfdf87bf0
15 changed files with 429 additions and 28 deletions
+2
View File
@@ -1370,6 +1370,8 @@ pub struct DebugConfig {
pub emulate_zero_presentation_time: bool,
#[knuffel(child)]
pub disable_resize_throttling: bool,
#[knuffel(child)]
pub disable_transactions: bool,
}
#[derive(knuffel::DecodeScalar, Debug, Clone, Copy, PartialEq, Eq)]
+2 -2
View File
@@ -147,7 +147,7 @@ impl Layout {
fn add_window(&mut self, mut window: TestWindow, width: Option<ColumnWidth>) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout.add_window(window.clone(), width, false);
@@ -161,7 +161,7 @@ impl Layout {
width: Option<ColumnWidth>,
) {
let ws = self.layout.active_workspace().unwrap();
window.request_size(ws.new_window_size(width, window.rules()), false);
window.request_size(ws.new_window_size(width, window.rules()), false, None);
window.communicate();
self.layout
+4 -4
View File
@@ -20,7 +20,7 @@ impl Tile {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let window = TestWindow::freeform(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
@@ -28,7 +28,7 @@ impl Tile {
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let window = TestWindow::fixed_size(0);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
@@ -37,7 +37,7 @@ impl Tile {
let window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
let mut rv = Self::with_window(window);
rv.tile.request_tile_size(size.to_f64(), false);
rv.tile.request_tile_size(size.to_f64(), false, None);
rv.window.communicate();
rv
}
@@ -85,7 +85,7 @@ impl Tile {
impl TestCase for Tile {
fn resize(&mut self, width: i32, height: i32) {
self.tile
.request_tile_size(Size::from((width, height)).to_f64(), false);
.request_tile_size(Size::from((width, height)).to_f64(), false, None);
self.window.communicate();
}
+5 -4
View File
@@ -14,14 +14,14 @@ pub struct Window {
impl Window {
pub fn freeform(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::freeform(0);
window.request_size(size, false);
window.request_size(size, false, None);
window.communicate();
Self { window }
}
pub fn fixed_size(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::fixed_size(0);
window.request_size(size, false);
window.request_size(size, false, None);
window.communicate();
Self { window }
}
@@ -29,7 +29,7 @@ impl Window {
pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
let mut window = TestWindow::fixed_size(0);
window.set_csd_shadow_width(64);
window.request_size(size, false);
window.request_size(size, false, None);
window.communicate();
Self { window }
}
@@ -37,7 +37,8 @@ impl Window {
impl TestCase for Window {
fn resize(&mut self, width: i32, height: i32) {
self.window.request_size(Size::from((width, height)), false);
self.window
.request_size(Size::from((width, height)), false, None);
self.window.communicate();
}
+7 -1
View File
@@ -9,6 +9,7 @@ use niri::layout::{
use niri::render_helpers::renderer::NiriRenderer;
use niri::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use niri::render_helpers::{RenderTarget, SplitElements};
use niri::utils::transaction::Transaction;
use niri::window::ResolvedWindowRules;
use smithay::backend::renderer::element::{Id, Kind};
use smithay::output::{self, Output};
@@ -177,7 +178,12 @@ impl LayoutElement for TestWindow {
}
}
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
fn request_size(
&mut self,
size: Size<i32, Logical>,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.inner.borrow_mut().requested_size = Some(size);
self.inner.borrow_mut().pending_fullscreen = false;
}
+1
View File
@@ -52,6 +52,7 @@ impl CompositorHandler for State {
fn commit(&mut self, surface: &WlSurface) {
let _span = tracy_client::span!("CompositorHandler::commit");
trace!(surface = ?surface.id(), "commit");
on_commit_buffer_handler::<Self>(surface);
self.backend.early_import(surface);
+52 -7
View File
@@ -34,6 +34,7 @@ use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
use smithay::{
delegate_kde_decoration, delegate_xdg_decoration, delegate_xdg_foreign, delegate_xdg_shell,
};
use tracing::field::Empty;
use crate::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
@@ -1003,6 +1004,8 @@ fn unconstrain_with_padding(
pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId {
add_pre_commit_hook::<State, _>(toplevel.wl_surface(), move |state, _dh, surface| {
let _span = tracy_client::span!("mapped toplevel pre-commit");
let span =
trace_span!("toplevel pre-commit", surface = %surface.id(), serial = Empty).entered();
let Some((mapped, _)) = state.niri.layout.find_window_and_output_mut(surface) else {
error!("pre-commit hook for mapped surfaces must be removed upon unmapping");
@@ -1032,31 +1035,73 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId
(got_unmapped, dmabuf, role.configure_serial)
});
let mut dmabuf_blocker =
dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok());
let mut transaction_for_dmabuf = None;
let mut animate = false;
if let Some(serial) = commit_serial {
if !span.is_disabled() {
span.record("serial", format!("{serial:?}"));
}
let animate = if let Some(serial) = commit_serial {
mapped.should_animate_commit(serial)
trace!("taking pending transaction");
if let Some(transaction) = mapped.take_pending_transaction(serial) {
// Transaction can be already completed if it ran past the deadline.
let disable = state.niri.config.borrow().debug.disable_transactions;
if !transaction.is_completed() && !disable {
// Register the deadline even if this is the last pending, since dmabuf
// rendering can still run over the deadline.
transaction.register_deadline_timer(&state.niri.event_loop);
let is_last = transaction.is_last();
// If this is the last transaction, we don't need to add a separate
// notification, because the transaction will complete in our dmabuf blocker
// callback, which already calls blocker_cleared(), or by the end of this
// function, in which case there would be no blocker in the first place.
if !is_last {
// Waiting for some other surface; register a notification and add a
// transaction blocker.
if let Some(client) = surface.client() {
transaction.add_notification(
state.niri.blocker_cleared_tx.clone(),
client.clone(),
);
add_blocker(surface, transaction.blocker());
}
}
// Delay dropping (and completing) the transaction until the dmabuf is ready.
// If there's no dmabuf, this will be dropped by the end of this pre-commit
// hook.
transaction_for_dmabuf = Some(transaction);
}
}
animate = mapped.should_animate_commit(serial);
} else {
error!("commit on a mapped surface without a configured serial");
false
};
if let Some((blocker, source)) = dmabuf_blocker.take() {
if let Some((blocker, source)) =
dmabuf.and_then(|dmabuf| dmabuf.generate_blocker(Interest::READ).ok())
{
if let Some(client) = surface.client() {
let res = state
.niri
.event_loop
.insert_source(source, move |_, _, state| {
// This surface is now ready for the transaction.
drop(transaction_for_dmabuf.take());
let display_handle = state.niri.display_handle.clone();
state
.client_compositor_state(&client)
.blocker_cleared(state, &display_handle);
Ok(())
});
if res.is_ok() {
add_blocker(surface, blocker);
trace!("added toplevel dmabuf blocker");
trace!("added dmabuf blocker");
}
}
}
+16 -2
View File
@@ -52,6 +52,7 @@ use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::texture::TextureBuffer;
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
use crate::utils::transaction::Transaction;
use crate::utils::{output_size, round_logical_in_physical_max1, ResizeEdge};
use crate::window::ResolvedWindowRules;
@@ -153,7 +154,12 @@ pub trait LayoutElement {
self.render(renderer, location, scale, alpha, target).popups
}
fn request_size(&mut self, size: Size<i32, Logical>, animate: bool);
fn request_size(
&mut self,
size: Size<i32, Logical>,
animate: bool,
transaction: Option<Transaction>,
);
fn request_fullscreen(&self, size: Size<i32, Logical>);
fn min_size(&self) -> Size<i32, Logical>;
fn max_size(&self) -> Size<i32, Logical>;
@@ -237,6 +243,7 @@ pub struct Options {
// Debug flags.
pub disable_resize_throttling: bool,
pub disable_transactions: bool,
}
impl Default for Options {
@@ -255,6 +262,7 @@ impl Default for Options {
default_width: None,
animations: Default::default(),
disable_resize_throttling: false,
disable_transactions: false,
}
}
}
@@ -292,6 +300,7 @@ impl Options {
default_width,
animations: config.animations.clone(),
disable_resize_throttling: config.debug.disable_resize_throttling,
disable_transactions: config.debug.disable_transactions,
}
}
@@ -2636,7 +2645,12 @@ mod tests {
SplitElements::default()
}
fn request_size(&mut self, size: Size<i32, Logical>, _animate: bool) {
fn request_size(
&mut self,
size: Size<i32, Logical>,
_animate: bool,
_transaction: Option<Transaction>,
) {
self.0.requested_size.set(Some(size));
self.0.pending_fullscreen.set(false);
}
+9 -2
View File
@@ -23,6 +23,7 @@ use crate::render_helpers::resize::ResizeRenderElement;
use crate::render_helpers::snapshot::RenderSnapshot;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::{render_to_encompassing_texture, RenderTarget};
use crate::utils::transaction::Transaction;
/// Toplevel window with decorations.
#[derive(Debug)]
@@ -503,7 +504,12 @@ impl<W: LayoutElement> Tile<W> {
activation_region.contains(point)
}
pub fn request_tile_size(&mut self, mut size: Size<f64, Logical>, animate: bool) {
pub fn request_tile_size(
&mut self,
mut size: Size<f64, Logical>,
animate: bool,
transaction: Option<Transaction>,
) {
// Can't go through effective_border_width() because we might be fullscreen.
if !self.border.is_off() {
let width = self.border.width();
@@ -514,7 +520,8 @@ impl<W: LayoutElement> Tile<W> {
// The size request has to be i32 unfortunately, due to Wayland. We floor here instead of
// round to avoid situations where proportionally-sized columns don't fit on the screen
// exactly.
self.window.request_size(size.to_i32_floor(), animate);
self.window
.request_size(size.to_i32_floor(), animate, transaction);
}
pub fn tile_width_for_window_width(&self, size: f64) -> f64 {
+31 -2
View File
@@ -22,6 +22,7 @@ use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter;
use crate::utils::transaction::Transaction;
use crate::utils::{output_size, send_scale_transform, ResizeEdge};
use crate::window::ResolvedWindowRules;
@@ -2740,6 +2741,27 @@ impl<W: LayoutElement> Workspace<W> {
}
}
let intent = if self.options.disable_resize_throttling {
ConfigureIntent::CanSend
} else if self.options.disable_transactions {
// When transactions are disabled, we don't use combined throttling, but rather
// compute throttling individually below.
ConfigureIntent::CanSend
} else {
col.tiles
.iter()
.fold(ConfigureIntent::NotNeeded, |intent, tile| {
match (intent, tile.window().configure_intent()) {
(_, ConfigureIntent::ShouldSend) => ConfigureIntent::ShouldSend,
(ConfigureIntent::NotNeeded, tile_intent) => tile_intent,
(ConfigureIntent::CanSend, ConfigureIntent::Throttled) => {
ConfigureIntent::Throttled
}
(intent, _) => intent,
}
})
};
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
let win = tile.window_mut();
@@ -2759,7 +2781,13 @@ impl<W: LayoutElement> Workspace<W> {
);
win.set_bounds(bounds);
let intent = win.configure_intent();
// If transactions are disabled, also disable combined throttling, for more
// intuitive behavior.
let intent = if self.options.disable_transactions {
win.configure_intent()
} else {
intent
};
if matches!(
intent,
@@ -3167,13 +3195,14 @@ impl<W: LayoutElement> Column<W> {
assert_eq!(auto_tiles_left, 0);
}
let transaction = Transaction::new();
for (tile, h) in zip(&mut self.tiles, heights) {
let WindowHeight::Fixed(height) = h else {
unreachable!()
};
let size = Size::from((width, height));
tile.request_tile_size(size, animate);
tile.request_tile_size(size, animate, Some(transaction.clone()));
}
}
+26 -3
View File
@@ -4,6 +4,7 @@ use std::ffi::OsString;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::{env, mem, thread};
@@ -61,14 +62,14 @@ use smithay::reexports::wayland_server::backend::{
};
use smithay::reexports::wayland_server::protocol::wl_shm;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource};
use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, Resource};
use smithay::utils::{
ClockSource, IsAlive as _, Logical, Monotonic, Physical, Point, Rectangle, Scale, Size,
Transform, SERIAL_COUNTER,
};
use smithay::wayland::compositor::{
with_states, with_surface_tree_downward, CompositorClientState, CompositorState, HookId,
SurfaceData, TraversalAction,
with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
CompositorState, HookId, SurfaceData, TraversalAction,
};
use smithay::wayland::cursor_shape::CursorShapeManagerState;
use smithay::wayland::dmabuf::DmabufState;
@@ -192,6 +193,10 @@ pub struct Niri {
// Dmabuf readiness pre-commit hook for a surface.
pub dmabuf_pre_commit_hook: HashMap<WlSurface, HookId>,
/// Clients to notify about their blockers being cleared.
pub blocker_cleared_tx: Sender<Client>,
pub blocker_cleared_rx: Receiver<Client>,
pub output_state: HashMap<Output, OutputState>,
pub output_by_name: HashMap<String, Output>,
@@ -517,6 +522,10 @@ impl State {
fn refresh(&mut self) {
let _span = tracy_client::span!("State::refresh");
// Handle commits for surfaces whose blockers cleared this cycle. This should happen before
// layout.refresh() since this is where these surfaces handle commits.
self.notify_blocker_cleared();
// These should be called periodically, before flushing the clients.
self.niri.layout.refresh();
self.niri.cursor_manager.check_cursor_image_surface_alive();
@@ -535,6 +544,15 @@ impl State {
self.niri.refresh_mapped_cast_outputs();
}
fn notify_blocker_cleared(&mut self) {
let dh = self.niri.display_handle.clone();
while let Ok(client) = self.niri.blocker_cleared_rx.try_recv() {
trace!("calling blocker_cleared");
self.client_compositor_state(&client)
.blocker_cleared(self, &dh);
}
}
pub fn move_cursor(&mut self, location: Point<f64, Logical>) {
let under = self.niri.surface_under_and_global_space(location);
self.niri
@@ -1523,6 +1541,8 @@ impl Niri {
let layout = Layout::new(&config_);
let (blocker_cleared_tx, blocker_cleared_rx) = mpsc::channel();
let compositor_state = CompositorState::new_v6::<State>(&display_handle);
let xdg_shell_state = XdgShellState::new_with_capabilities::<State>(
&display_handle,
@@ -1737,6 +1757,8 @@ impl Niri {
unmapped_windows: HashMap::new(),
root_surface: HashMap::new(),
dmabuf_pre_commit_hook: HashMap::new(),
blocker_cleared_tx,
blocker_cleared_rx,
monitors_active: true,
devices: HashSet::new(),
@@ -2497,6 +2519,7 @@ impl Niri {
RedrawState::Queued | RedrawState::WaitingForEstimatedVBlankAndQueued(_)
)
}) {
trace!("redrawing output");
let output = output.clone();
self.redraw(backend, &output);
}
+1
View File
@@ -23,6 +23,7 @@ use smithay::wayland::fractional_scale::with_fractional_scale;
pub mod id;
pub mod scale;
pub mod spawning;
pub mod transaction;
pub mod watcher;
pub static IS_SYSTEMD_SERVICE: AtomicBool = AtomicBool::new(false);
+185
View File
@@ -0,0 +1,185 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex, Weak};
use std::time::{Duration, Instant};
use atomic::Ordering;
use calloop::ping::{make_ping, Ping};
use calloop::timer::{TimeoutAction, Timer};
use calloop::LoopHandle;
use smithay::reexports::wayland_server::Client;
use smithay::wayland::compositor::{Blocker, BlockerState};
/// Default time limit, after which the transaction completes.
///
/// Serves to avoid hanging when a client fails to respond to a configure promptly.
const TIME_LIMIT: Duration = Duration::from_millis(300);
/// Transaction between Wayland clients.
///
/// How to use it:
/// 1. Create a transaction with [`Transaction::new()`].
/// 2. Clone it as many times as you need.
/// 3. Before adding the transaction as a commit blocker, remember to call
/// [`Transaction::add_notification()`] to receive a notification when the transaction completes.
/// 4. Before adding the transaction as a commit blocker, remember to call
/// [`Transaction::register_deadline_timer()`] to make sure the transaction completes when
/// reaching the deadline.
/// 5. In your surface pre-commit handler, if the transaction corresponding to that commit isn't
/// ready, get a blocker with [`Transaction::blocker()`] and add it to the surface.
#[derive(Debug, Clone)]
pub struct Transaction {
inner: Arc<Inner>,
deadline: Rc<RefCell<Deadline>>,
}
/// Blocker for a [`Transaction`].
#[derive(Debug)]
pub struct TransactionBlocker(Weak<Inner>);
#[derive(Debug)]
enum Deadline {
NotRegistered(Instant),
Registered { remove: Ping },
}
#[derive(Debug)]
struct Inner {
/// Whether the transaction is completed.
completed: AtomicBool,
/// Notifications to send out upon completing the transaction.
notifications: Mutex<Option<(Sender<Client>, Vec<Client>)>>,
}
impl Transaction {
/// Creates a new transaction.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
inner: Arc::new(Inner::new()),
deadline: Rc::new(RefCell::new(Deadline::NotRegistered(
Instant::now() + TIME_LIMIT,
))),
}
}
/// Gets a blocker for this transaction.
pub fn blocker(&self) -> TransactionBlocker {
trace!(transaction = ?Arc::as_ptr(&self.inner), "generating blocker");
TransactionBlocker(Arc::downgrade(&self.inner))
}
/// Adds a notification for when this transaction completes.
pub fn add_notification(&self, sender: Sender<Client>, client: Client) {
if self.is_completed() {
error!("tried to add notification to a completed transaction");
return;
}
let mut guard = self.inner.notifications.lock().unwrap();
guard.get_or_insert((sender, Vec::new())).1.push(client);
}
/// Registers this transaction's deadline timer on an event loop.
pub fn register_deadline_timer<T: 'static>(&self, event_loop: &LoopHandle<'static, T>) {
let mut cell = self.deadline.borrow_mut();
if let Deadline::NotRegistered(deadline) = *cell {
let timer = Timer::from_deadline(deadline);
let inner = Arc::downgrade(&self.inner);
let token = event_loop
.insert_source(timer, move |_, _, _| {
let _span = trace_span!("deadline timer", transaction = ?Weak::as_ptr(&inner))
.entered();
if let Some(inner) = inner.upgrade() {
trace!("deadline reached, completing transaction");
inner.complete();
} else {
// We should remove the timer automatically. But this callback can still
// just happen to run while the ping callback is scheduled, leading to this
// branch being legitimately taken.
trace!("transaction completed without removing the timer");
}
TimeoutAction::Drop
})
.unwrap();
// Add a ping source that will be used to remove the timer automatically.
let (ping, source) = make_ping().unwrap();
let loop_handle = event_loop.clone();
event_loop
.insert_source(source, move |_, _, _| {
loop_handle.remove(token);
})
.unwrap();
*cell = Deadline::Registered { remove: ping };
}
}
/// Returns whether this transaction has already completed.
pub fn is_completed(&self) -> bool {
self.inner.is_completed()
}
/// Returns whether this is the last instance of this transaction.
pub fn is_last(&self) -> bool {
Arc::strong_count(&self.inner) == 1
}
}
impl Drop for Transaction {
fn drop(&mut self) {
let _span = trace_span!("drop", transaction = ?Arc::as_ptr(&self.inner)).entered();
if self.is_last() {
// If this was the last transaction, complete it.
trace!("last transaction dropped, completing");
self.inner.complete();
// Also remove the timer.
if let Deadline::Registered { remove } = &*self.deadline.borrow() {
remove.ping();
};
}
}
}
impl Blocker for TransactionBlocker {
fn state(&self) -> BlockerState {
if self.0.upgrade().map_or(true, |x| x.is_completed()) {
BlockerState::Released
} else {
BlockerState::Pending
}
}
}
impl Inner {
fn new() -> Self {
Self {
completed: AtomicBool::new(false),
notifications: Mutex::new(None),
}
}
fn is_completed(&self) -> bool {
self.completed.load(Ordering::Relaxed)
}
fn complete(&self) {
self.completed.store(true, Ordering::Relaxed);
let mut guard = self.notifications.lock().unwrap();
if let Some((sender, clients)) = guard.take() {
for client in clients {
if let Err(err) = sender.send(client) {
warn!("error sending blocker notification: {err:?}");
};
}
}
}
}
+69 -1
View File
@@ -32,6 +32,7 @@ use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderEleme
use crate::render_helpers::surface::render_snapshot_from_surface_tree;
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
use crate::utils::id::IdCounter;
use crate::utils::transaction::Transaction;
use crate::utils::{send_scale_transform, ResizeEdge};
#[derive(Debug)]
@@ -71,6 +72,12 @@ pub struct Mapped {
/// Snapshot right before an animated commit.
animation_snapshot: Option<LayoutElementRenderSnapshot>,
/// Transaction that the next configure should take part in, if any.
transaction_for_next_configure: Option<Transaction>,
/// Pending transactions that have not been added as blockers for this window yet.
pending_transactions: Vec<(Serial, Transaction)>,
/// State of an ongoing interactive resize.
interactive_resize: Option<InteractiveResize>,
@@ -141,6 +148,8 @@ impl Mapped {
animate_next_configure: false,
animate_serials: Vec::new(),
animation_snapshot: None,
transaction_for_next_configure: None,
pending_transactions: Vec::new(),
interactive_resize: None,
last_interactive_resize_start: Cell::new(None),
}
@@ -255,6 +264,40 @@ impl Mapped {
self.animation_snapshot = Some(self.render_snapshot(renderer));
}
pub fn take_pending_transaction(&mut self, commit_serial: Serial) -> Option<Transaction> {
let mut rv = None;
// Pending transactions are appended in order by serial, so we can loop from the start
// until we hit a serial that is too new.
while let Some((serial, _)) = self.pending_transactions.first() {
// In this loop, we will complete the transaction corresponding to the commit, as well
// as all transactions corresponding to previous serials. This can happen when we
// request resizes too quickly, and the surface only responds to the last one.
//
// Note that in this case, completing the previous transactions can result in an
// inconsistent visual state, if another window is waiting for this window to assume a
// specific size (in a previous transaction), which is now different (in this commit).
//
// However, there isn't really a good way to deal with that. We cannot cancel any
// transactions because we need to keep sending frame callbacks, and cancelling a
// transaction will make the corresponding frame callbacks get lost, and the window
// will hang.
//
// This is why resize throttling (implemented separately) is important: it prevents
// visually inconsistent states by way of never having more than one transaction in
// flight.
if commit_serial.is_no_older_than(serial) {
let (_, transaction) = self.pending_transactions.remove(0);
// Previous transaction is dropped here, signaling completion.
rv = Some(transaction);
} else {
break;
}
}
rv
}
pub fn last_interactive_resize_start(&self) -> &Cell<Option<(Duration, ResizeEdge)>> {
&self.last_interactive_resize_start
}
@@ -442,7 +485,12 @@ impl LayoutElement for Mapped {
}
}
fn request_size(&mut self, size: Size<i32, Logical>, animate: bool) {
fn request_size(
&mut self,
size: Size<i32, Logical>,
animate: bool,
transaction: Option<Transaction>,
) {
let changed = self.toplevel().with_pending_state(|state| {
let changed = state.size != Some(size);
state.size = Some(size);
@@ -453,6 +501,15 @@ impl LayoutElement for Mapped {
if changed && animate {
self.animate_next_configure = true;
}
// Store the transaction regardless of whether the size changed. This is because with 3+
// windows in a column, the size may change among windows 1 and 2 and then right away among
// windows 2 and 3, and we want all windows 1, 2 and 3 to use the last transaction, rather
// than window 1 getting stuck with the previous transaction that is immediately released
// by 2.
if let Some(transaction) = transaction {
self.transaction_for_next_configure = Some(transaction);
}
}
fn request_fullscreen(&self, size: Size<i32, Logical>) {
@@ -627,11 +684,21 @@ impl LayoutElement for Mapped {
}
fn send_pending_configure(&mut self) {
let _span =
trace_span!("send_pending_configure", surface = ?self.toplevel().wl_surface().id())
.entered();
if let Some(serial) = self.toplevel().send_pending_configure() {
trace!(?serial, "sending configure");
if self.animate_next_configure {
self.animate_serials.push(serial);
}
if let Some(transaction) = self.transaction_for_next_configure.take() {
self.pending_transactions.push((serial, transaction));
}
self.interactive_resize = match self.interactive_resize.take() {
Some(InteractiveResize::WaitingForLastConfigure(data)) => {
Some(InteractiveResize::WaitingForLastCommit { data, serial })
@@ -648,6 +715,7 @@ impl LayoutElement for Mapped {
}
self.animate_next_configure = false;
self.transaction_for_next_configure = None;
}
fn is_fullscreen(&self) -> bool {
+19
View File
@@ -21,6 +21,7 @@ debug {
wait-for-frame-completion-before-queueing
emulate-zero-presentation-time
disable-resize-throttling
disable-transactions
}
binds {
@@ -146,6 +147,24 @@ debug {
}
```
### `disable-transactions`
<sup>Since: 0.1.9</sup>
Disable transactions (as of niri 0.1.9, only resize transactions are implemented).
By default, windows which must resize together, do resize together.
For example, all windows in a column must resize at the same time to maintain the combined column height equal to the screen height, and to maintain the same window width.
Transactions make niri wait until all windows finish resizing before showing them all on screen in one, synchronized frame.
For them to work properly, resize throttling shouldn't be disabled (with the previous debug flag).
```kdl
debug {
disable-transactions
}
```
### Key Bindings
These are not debug options, but rather key bindings.