aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/handlers/compositor.rs1
-rw-r--r--src/handlers/xdg_shell.rs59
-rw-r--r--src/layout/mod.rs18
-rw-r--r--src/layout/tile.rs11
-rw-r--r--src/layout/workspace.rs33
-rw-r--r--src/niri.rs29
-rw-r--r--src/utils/mod.rs1
-rw-r--r--src/utils/transaction.rs185
-rw-r--r--src/window/mapped.rs70
9 files changed, 390 insertions, 17 deletions
diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs
index 140d00e0..e833494c 100644
--- a/src/handlers/compositor.rs
+++ b/src/handlers/compositor.rs
@@ -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);
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
index 5f84da3b..ec0b99e3 100644
--- a/src/handlers/xdg_shell.rs
+++ b/src/handlers/xdg_shell.rs
@@ -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:?}"));
+ }
+
+ 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);
+ }
+ }
- let animate = if let Some(serial) = commit_serial {
- mapped.should_animate_commit(serial)
+ 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");
}
}
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index abcf9d43..9fbd1623 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -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);
}
diff --git a/src/layout/tile.rs b/src/layout/tile.rs
index a701e96f..11c266d4 100644
--- a/src/layout/tile.rs
+++ b/src/layout/tile.rs
@@ -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 {
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 8f0ae6e3..7548dc10 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -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()));
}
}
diff --git a/src/niri.rs b/src/niri.rs
index 33ca7d6d..27696749 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -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);
}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 03654877..5fb1d163 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -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);
diff --git a/src/utils/transaction.rs b/src/utils/transaction.rs
new file mode 100644
index 00000000..03dd6a2e
--- /dev/null
+++ b/src/utils/transaction.rs
@@ -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:?}");
+ };
+ }
+ }
+ }
+}
diff --git a/src/window/mapped.rs b/src/window/mapped.rs
index 6a1fe045..54c045b3 100644
--- a/src/window/mapped.rs
+++ b/src/window/mapped.rs
@@ -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 {