smithay/src/wayland/compositor/transaction.rs

289 lines
10 KiB
Rust

// The transaction model for handling surface states in Smithay
//
// The caching logic in `cache.rs` provides surfaces with a queue of
// pending states identified with numeric commit ids, allowing the compositor
// to precisely control *when* a state become active. This file is the second
// half: these identified states are grouped into transactions, which allow the
// synchronization of updates accross surfaces.
//
// There are 2 main cases when the state of multiple surfaces must be updated
// atomically:
// - synchronized subsurface must have their state updated at the same time as their parents
// - The upcoming `wp_transaction` protocol
//
// In these situations, the individual states in a surface queue are grouped into a transaction
// and are all applied atomically when the transaction itself is applied. The logic for creating
// new transactions is currently the following:
//
// - Each surface has an implicit "pending" transaction, into which its newly commited state is
// recorded
// - Furthermore, on commit, the pending transaction of all synchronized child subsurfaces is merged
// into the current surface's pending transaction, and a new implicit transaction is started for those
// children (logic is implemented in `handlers.rs`, in `PrivateSurfaceData::commit`).
// - Then, still on commit, if the surface is not a synchronized subsurface, its pending transaction is
// directly applied
//
// This last step will change once we have support for explicit synchronization (and further in the future,
// of the wp_transaction protocol). Explicit synchronization introduces a notion of blockers: the transaction
// cannot be applied before all blockers are released, and thus must wait for it to be the case.
//
// For thoses situations, the (currently unused) `TransactionQueue` will come into play. It is a per-client
// queue of transactions, that stores and applies them by both respecting their topological order
// (ensuring that for each surface, states are applied in the correct order) and that all transactions
// wait befor all their blockers are resolved to be merged. If a blocker is cancelled, the whole transaction
// it blocks is cancelled as well, and simply dropped. Thanks to the logic of `Cache::apply_state`, the
// associated state will be applied automatically when the next valid transaction is applied, ensuring
// global coherence.
// A significant part of the logic of this module is not yet used,
// but will be once proper transaction & blockers support is
// added to smithay
#![allow(dead_code)]
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use wayland_server::protocol::wl_surface::WlSurface;
use crate::wayland::Serial;
use super::tree::PrivateSurfaceData;
pub trait Blocker {
fn state(&self) -> BlockerState;
}
pub enum BlockerState {
Pending,
Released,
Cancelled,
}
struct TransactionState {
surfaces: Vec<(WlSurface, Serial)>,
blockers: Vec<Box<dyn Blocker + Send>>,
}
impl Default for TransactionState {
fn default() -> Self {
TransactionState {
surfaces: Vec::new(),
blockers: Vec::new(),
}
}
}
impl TransactionState {
fn insert(&mut self, surface: WlSurface, id: Serial) {
if let Some(place) = self.surfaces.iter_mut().find(|place| place.0 == surface) {
// the surface is already in the list, update the serial
if place.1 < id {
place.1 = id;
}
} else {
// the surface is not in the list, insert it
self.surfaces.push((surface, id));
}
}
}
enum TransactionInner {
Data(TransactionState),
Fused(Arc<Mutex<TransactionInner>>),
}
pub(crate) struct PendingTransaction {
inner: Arc<Mutex<TransactionInner>>,
}
impl Default for PendingTransaction {
fn default() -> Self {
PendingTransaction {
inner: Arc::new(Mutex::new(TransactionInner::Data(Default::default()))),
}
}
}
impl PendingTransaction {
fn with_inner_state<T, F: FnOnce(&mut TransactionState) -> T>(&self, f: F) -> T {
let mut next = self.inner.clone();
loop {
let tmp = match *next.lock().unwrap() {
TransactionInner::Data(ref mut state) => return f(state),
TransactionInner::Fused(ref into) => into.clone(),
};
next = tmp;
}
}
pub(crate) fn insert_state(&self, surface: WlSurface, id: Serial) {
self.with_inner_state(|state| state.insert(surface, id))
}
pub(crate) fn add_blocker<B: Blocker + Send + 'static>(&self, blocker: B) {
self.with_inner_state(|state| state.blockers.push(Box::new(blocker) as Box<_>))
}
pub(crate) fn is_same_as(&self, other: &PendingTransaction) -> bool {
let ptr1 = self.with_inner_state(|state| state as *const _);
let ptr2 = other.with_inner_state(|state| state as *const _);
ptr1 == ptr2
}
pub(crate) fn merge_into(&self, into: &PendingTransaction) {
if self.is_same_as(into) {
// nothing to do
return;
}
// extract our pending surfaces and change our link
let mut next = self.inner.clone();
let my_state;
loop {
let tmp = {
let mut guard = next.lock().unwrap();
match *guard {
TransactionInner::Data(ref mut state) => {
my_state = std::mem::take(state);
*guard = TransactionInner::Fused(into.inner.clone());
break;
}
TransactionInner::Fused(ref into) => into.clone(),
}
};
next = tmp;
}
// fuse our surfaces into our new transaction state
self.with_inner_state(|state| {
for (surface, id) in my_state.surfaces {
state.insert(surface, id);
}
state.blockers.extend(my_state.blockers);
});
}
pub(crate) fn finalize(mut self) -> Transaction {
// When finalizing a transaction, this *must* be the last handle to this transaction
loop {
let inner = match Arc::try_unwrap(self.inner) {
Ok(mutex) => mutex.into_inner().unwrap(),
Err(_) => panic!("Attempting to finalize a transaction but handle is not the last."),
};
match inner {
TransactionInner::Data(TransactionState {
surfaces, blockers, ..
}) => return Transaction { surfaces, blockers },
TransactionInner::Fused(into) => self.inner = into,
}
}
}
}
pub(crate) struct Transaction {
surfaces: Vec<(WlSurface, Serial)>,
blockers: Vec<Box<dyn Blocker + Send>>,
}
impl Transaction {
/// Computes the global state of the transaction with regard to its blockers
///
/// The logic is:
///
/// - if at least one blocker is cancelled, the transaction is cancelled
/// - otherwise, if at least one blocker is pending, the transaction is pending
/// - otherwise, all blockers are released, and the transaction is also released
pub(crate) fn state(&self) -> BlockerState {
use BlockerState::*;
self.blockers
.iter()
.fold(Released, |acc, blocker| match (acc, blocker.state()) {
(Cancelled, _) | (_, Cancelled) => Cancelled,
(Pending, _) | (_, Pending) => Pending,
(Released, Released) => Released,
})
}
pub(crate) fn apply(self) {
for (surface, id) in self.surfaces {
PrivateSurfaceData::with_states(&surface, |states| {
states.cached_state.apply_state(id);
})
}
}
}
// This queue should be per-client
pub(crate) struct TransactionQueue {
transactions: Vec<Transaction>,
// we keep the hashset around to reuse allocations
seen_surfaces: HashSet<u32>,
}
impl Default for TransactionQueue {
fn default() -> Self {
TransactionQueue {
transactions: Vec::new(),
seen_surfaces: HashSet::new(),
}
}
}
impl TransactionQueue {
pub(crate) fn append(&mut self, t: Transaction) {
self.transactions.push(t);
}
pub(crate) fn apply_ready(&mut self) {
// this is a very non-optimized implementation
// we just iterate over the queue of transactions, keeping track of which
// surface we have seen as they encode transaction dependencies
self.seen_surfaces.clear();
// manually iterate as we're going to modify the Vec while iterating on it
let mut i = 0;
// the loop will terminate, as at every iteration either i is incremented by 1
// or the lenght of self.transactions is reduced by 1.
while i <= self.transactions.len() {
let mut skip = false;
// does the transaction have any active blocker?
match self.transactions[i].state() {
BlockerState::Cancelled => {
// this transaction is cancelled, remove it without further processing
self.transactions.remove(i);
continue;
}
BlockerState::Pending => {
skip = true;
}
BlockerState::Released => {}
}
// if not, does this transaction depend on any previous transaction?
if !skip {
for (s, _) in &self.transactions[i].surfaces {
if !s.as_ref().is_alive() {
continue;
}
if self.seen_surfaces.contains(&s.as_ref().id()) {
skip = true;
break;
}
}
}
if skip {
// this transaction is not yet ready and should be skipped, add its surfaces to our
// seen list
for (s, _) in &self.transactions[i].surfaces {
if !s.as_ref().is_alive() {
continue;
}
self.seen_surfaces.insert(s.as_ref().id());
}
i += 1;
} else {
// this transaction is to be applied, yay!
self.transactions.remove(i).apply();
}
}
}
}