mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
Implement resize transactions
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user