Skip to main content

ferroui_base/rendering/composition/server/
compositor_lock.rs

1//! The compositor lock: what the server compositor and its objects are
2//! confined to.
3//!
4//! Upstream the server compositor is not bound to a thread. The render
5//! thread renders its frames, and the UI thread renders it too at the
6//! synchronous points of a platform that asks for that (a resize, the first
7//! show, the disposal of a target); `ServerCompositor.Render` takes
8//! `lock (_lock)` and server objects check that they are only touched under
9//! it. The graph of server objects here is made of `Rc` and cells, so the
10//! rule is carried by a type: [`LockedServerCompositor`] holds the graph and
11//! only hands it out inside the lock.
12
13use super::ServerCompositor;
14use std::mem::ManuallyDrop;
15use std::rc::Rc;
16use std::sync::{Arc, Condvar, Mutex, OnceLock};
17use std::thread::{self, ThreadId};
18
19/// A lock that the thread holding it may enter again (`lock` in C#).
20#[derive(Default)]
21pub struct CompositorLock {
22    state: Mutex<LockState>,
23    released: Condvar,
24}
25
26#[derive(Default)]
27struct LockState {
28    owner: Option<ThreadId>,
29    depth: usize,
30}
31
32impl CompositorLock {
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Enters the lock, waiting while another thread holds it.
38    pub fn enter(&self) -> CompositorLockGuard<'_> {
39        let current = thread::current().id();
40        let mut state = self.state.lock().unwrap_or_else(|e| e.into_inner());
41        while state.owner.is_some_and(|owner| owner != current) {
42            state = self.released.wait(state).unwrap_or_else(|e| e.into_inner());
43        }
44        state.owner = Some(current);
45        state.depth += 1;
46        CompositorLockGuard { lock: self }
47    }
48
49    /// Whether the calling thread holds the lock.
50    pub fn is_held_by_current_thread(&self) -> bool {
51        self.state.lock().unwrap_or_else(|e| e.into_inner()).owner == Some(thread::current().id())
52    }
53}
54
55/// Leaves the lock when dropped.
56pub struct CompositorLockGuard<'a> {
57    lock: &'a CompositorLock,
58}
59
60impl Drop for CompositorLockGuard<'_> {
61    fn drop(&mut self) {
62        let mut state = self.lock.state.lock().unwrap_or_else(|e| e.into_inner());
63        state.depth -= 1;
64        if state.depth == 0 {
65            state.owner = None;
66            drop(state);
67            self.lock.released.notify_one();
68        }
69    }
70}
71
72/// The server compositor behind its lock.
73pub struct LockedServerCompositor {
74    lock: CompositorLock,
75    /// `None` once the compositor has released it. Only touched under
76    /// `lock` (or by the one thread of the dispatcher-thread mode).
77    server: std::cell::UnsafeCell<Option<Rc<ServerCompositor>>>,
78    /// The thread the graph is confined to besides the lock, once it is
79    /// known: see [`confine_to_current_thread`](Self::confine_to_current_thread).
80    /// Never set where both threads render.
81    confined_to: OnceLock<ThreadId>,
82}
83
84// SAFETY: the server compositor and every object reachable from it are only
85// touched by the thread that holds `lock`: `with` is the only way in, and
86// what it hands out cannot outlive the call. Nothing that crosses to or from
87// the server side is an `Rc` into this graph: batches, resources and jobs
88// are `Send` (asserted where they are defined), the readback is atomic, and
89// a job that runs inside the graph is called under the lock. The same-thread
90// accessor of the compositor exists for the mode in which no second thread
91// enters.
92//
93// What leaves the graph for a caller on the thread of the compositor leaves
94// it in a type that keeps the rule: a feature of the render interface is
95// handed out as a `RenderInterfaceFeature`, a `LockBound` over the handle
96// the map of features holds, which lends the feature inside the lock and
97// never the handle.
98//
99// What the graph shares with the thread of the compositor is shared by
100// contract or held where it cannot be cloned: the platform graphics and
101// their ready state feature are `Arc`s of `Send + Sync` objects, the clock of
102// the render-thread mode is an `Arc` of a `Send + Sync` function with one
103// `Rc` over it per side, and the handle of the render interface, an `Rc`
104// whose other handles the UI thread clones and drops freely, is a private
105// field of the context manager, which lends it by reference and has it
106// dropped by the UI thread (`release_platform_handles` before the render
107// thread releases the graph, `release` on the UI thread otherwise).
108//
109// What the type cannot check, and stays on the caller:
110//
111// - a caller of `with` (`Compositor::with_server`) gets the server
112//   compositor itself, and must not let a handle of the graph escape the
113//   closure by cloning an `Rc` out of it;
114// - an object of the graph that returns an `Rc` from one of its methods to
115//   a caller that will keep it outside the lock (a feature that creates a
116//   context for its caller) must return one that shares no count with the
117//   graph;
118// - the render interface is called by both threads (the UI thread creates
119//   geometries and bitmaps with it while a frame creates the backend
120//   context): its contract is not `Sync`, so an implementation has to be
121//   safe for that by itself, and its handle must only be looked up
122//   (`capture_platform_render_interface`) by the thread whose service
123//   locator holds it.
124unsafe impl Send for LockedServerCompositor {}
125unsafe impl Sync for LockedServerCompositor {}
126
127impl LockedServerCompositor {
128    pub(crate) fn new(server: Rc<ServerCompositor>) -> Self {
129        Self {
130            lock: CompositorLock::new(),
131            server: std::cell::UnsafeCell::new(Some(server)),
132            confined_to: OnceLock::new(),
133        }
134    }
135
136    /// Confines what the graph renders with to the calling thread: the
137    /// render thread of a compositor whose thread never renders
138    /// (`UseUiThreadForSynchronousCommits` false with a background loop).
139    /// Called by every frame of such a compositor; the first one decides.
140    ///
141    /// The lock still orders every access. What this adds is for the objects
142    /// that belong to one thread whatever lock is held (a graphics context
143    /// that only exists in the thread that created it): the graph is
144    /// released by this thread, and a thread that is left with the last
145    /// handle to it leaks it instead of dropping it (see the `Drop` impls of
146    /// this type and of [`LockBound`]).
147    ///
148    /// # Panics
149    ///
150    /// When another thread has rendered this compositor before.
151    pub(crate) fn confine_to_current_thread(&self) {
152        let current = thread::current().id();
153        assert!(
154            *self.confined_to.get_or_init(|| current) == current,
155            "a compositor that is confined to its render thread was rendered by another thread"
156        );
157    }
158
159    /// Whether the graph is confined to a thread that is not the calling
160    /// one.
161    pub(crate) fn is_confined_to_another_thread(&self) -> bool {
162        self.confined_to.get().is_some_and(|thread| *thread != thread::current().id())
163    }
164
165    /// Runs `f` with the server compositor, under the lock.
166    ///
167    /// # Panics
168    ///
169    /// When the compositor has released the server compositor.
170    pub fn with<R>(&self, f: impl FnOnce(&Rc<ServerCompositor>) -> R) -> R {
171        match self.try_with(f) {
172            Some(result) => result,
173            None => panic!("the server compositor has been released"),
174        }
175    }
176
177    /// Runs `f` with the server compositor, under the lock; `None` once the
178    /// compositor has released it.
179    pub fn try_with<R>(&self, f: impl FnOnce(&Rc<ServerCompositor>) -> R) -> Option<R> {
180        let _guard = self.lock.enter();
181        // SAFETY: the cell is only read and written under the lock, which
182        // this thread holds. The handle taken here keeps the server
183        // compositor alive for the call if `f` releases it, and is dropped
184        // before the guard, under the lock.
185        let server = unsafe { (*self.server.get()).clone() }?;
186        Some(f(&server))
187    }
188
189    /// Whether `server` is the server compositor behind this lock. Called by
190    /// a thread that holds a reference to a server compositor, which it only
191    /// has inside the lock (or as the one thread of the dispatcher-thread
192    /// mode).
193    pub(crate) fn is_server(&self, server: &ServerCompositor) -> bool {
194        // SAFETY: see above; the cell is read by a thread that is inside the
195        // lock or is the only one.
196        unsafe { (*self.server.get()).as_ref() }.is_some_and(|own| std::ptr::eq(Rc::as_ptr(own), server))
197    }
198
199    /// Runs `f` under the lock, without the server compositor.
200    pub(crate) fn under_lock<R>(&self, f: impl FnOnce() -> R) -> R {
201        let _guard = self.lock.enter();
202        f()
203    }
204
205    /// Releases the server compositor, under the lock, on the calling
206    /// thread. The compositor does this when it is dropped: the graph holds
207    /// a handle that it shares with the UI thread and whose count is not
208    /// atomic (the render interface), so it is released there and not by
209    /// whichever thread drops the last handle to this object.
210    ///
211    /// Where the graph is confined to the render thread it is the other way
212    /// round: the compositor lets go of the shared handles on its thread and
213    /// the render thread calls this, as its last job.
214    pub(crate) fn release(&self) {
215        let _guard = self.lock.enter();
216        // SAFETY: as in `try_with`.
217        let server = unsafe { (*self.server.get()).take() };
218        drop(server);
219    }
220
221    /// The server compositor without the lock: for the mode in which the
222    /// thread of the compositor is the only one that ever enters.
223    pub(crate) fn same_thread(&self) -> &Rc<ServerCompositor> {
224        // SAFETY: in the dispatcher-thread mode one thread reads the cell and
225        // releases it (when the compositor is dropped, after which nothing
226        // calls this).
227        match unsafe { (*self.server.get()).as_ref() } {
228            Some(server) => server,
229            None => panic!("the server compositor has been released"),
230        }
231    }
232}
233
234impl Drop for LockedServerCompositor {
235    fn drop(&mut self) {
236        // The last handle to a graph that is confined to the render thread
237        // and was never released there (the render loop stopped ticking
238        // before the compositor was dropped): it holds objects that may only
239        // be dropped by that thread, so it is leaked, as a `ThreadBound`
240        // does. Otherwise the field is dropped as before.
241        if self.is_confined_to_another_thread() {
242            if let Some(server) = self.server.get_mut().take() {
243                std::mem::forget(server);
244            }
245        }
246    }
247}
248
249/// A value confined to the compositor lock: an object of the server side
250/// that is not part of the graph of the server compositor.
251///
252/// Upstream an object of the render interface (an imported GPU image, the
253/// feature that imports it, the context it belongs to) is created at the
254/// request of the UI thread, then used and disposed by jobs of the render
255/// thread; the garbage collector shares it. Here such an object is an `Rc`,
256/// so it is bound to the lock under which every job runs: the value is
257/// reached with the server compositor in hand, which a thread only has while
258/// it is inside the lock (or, in the mode without a second thread, while it
259/// is the one thread of the compositor), and it is dropped inside the lock.
260/// Unlike a `ThreadBound` it does not depend on the thread: both the render
261/// thread and the thread of the compositor reach it, one at a time.
262///
263/// Where the graph is confined to the render thread
264/// ([`LockedServerCompositor::confine_to_current_thread`]) the value is
265/// still reached by both threads under the lock, for what upstream reads
266/// from the UI thread, but only the render thread drops it: dropped by
267/// another thread it is leaked.
268pub struct LockBound<T> {
269    value: ManuallyDrop<T>,
270    server: Arc<LockedServerCompositor>,
271}
272
273// SAFETY: the value is only reached through `get`, which asks for the server
274// compositor the value is bound to: a reference a thread has only inside
275// `LockedServerCompositor::with` (a job, or `Compositor::with_server`), or
276// from the same-thread accessor in the mode in which no second thread
277// enters. The value is dropped inside the lock too. So no two threads are in
278// the value at once, and every use is ordered by the lock.
279//
280// What the type cannot check is on the caller: every handle that shares
281// state with the value without synchronisation (a clone of an `Rc` inside
282// it, an `Rc` it was built from) must itself only be touched, cloned and
283// dropped inside the lock, which in practice means it lives in a `LockBound`
284// of the same compositor or in the graph of the server compositor. A caller
285// must not keep such a clone outside, clone one out of `get`, or bind a
286// value that still shares an `Rc` with an object of its own thread.
287unsafe impl<T> Send for LockBound<T> {}
288unsafe impl<T> Sync for LockBound<T> {}
289
290impl<T> LockBound<T> {
291    /// Binds `value` to the lock of `locked`. `server` is the proof that the
292    /// caller is inside the lock, where the value has to be put together
293    /// when it clones handles of the server side.
294    ///
295    /// # Panics
296    ///
297    /// When `server` is not the server compositor of `locked`.
298    pub(crate) fn new(locked: &Arc<LockedServerCompositor>, server: &ServerCompositor, value: T) -> Self {
299        let bound = Self { value: ManuallyDrop::new(value), server: locked.clone() };
300        bound.verify(server);
301        bound
302    }
303
304    fn verify(&self, server: &ServerCompositor) {
305        assert!(
306            self.server.is_server(server),
307            "the value is bound to the lock of another compositor"
308        );
309    }
310
311    /// The value, for a caller that is inside the compositor lock: `server`
312    /// is what a job receives and what `with` hands out.
313    ///
314    /// # Panics
315    ///
316    /// When `server` is not the server compositor the value is bound to.
317    pub fn get<'a>(&'a self, server: &'a ServerCompositor) -> &'a T {
318        self.verify(server);
319        &self.value
320    }
321
322    /// Runs `f` with the value, for a caller that is not inside the
323    /// compositor lock: the calling thread enters it for the call. `None`
324    /// once the compositor has released the server compositor.
325    ///
326    /// What `f` gets must not leave it: see the note on the caller above.
327    pub(crate) fn with<R>(&self, f: impl FnOnce(&ServerCompositor, &T) -> R) -> Option<R> {
328        self.server.try_with(|server| f(server, &self.value))
329    }
330}
331
332impl<T> Drop for LockBound<T> {
333    fn drop(&mut self) {
334        // An object of the render interface is dropped by the thread it
335        // belongs to or not at all: its disposal is a job of the render
336        // thread, and what is leaked here is what that job left of it.
337        if self.server.is_confined_to_another_thread() {
338            return;
339        }
340        // The last handle may be dropped by either thread, outside a job:
341        // the value is released inside the lock.
342        let server = self.server.clone();
343        // SAFETY: the value is not used after this.
344        server.under_lock(|| unsafe { ManuallyDrop::drop(&mut self.value) });
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    // Not from upstream.
351    use super::*;
352    use std::sync::atomic::{AtomicUsize, Ordering};
353    use std::sync::Arc;
354
355    #[test]
356    fn the_holder_enters_again_and_others_wait() {
357        let lock = Arc::new(CompositorLock::new());
358        let inside = Arc::new(AtomicUsize::new(0));
359
360        let outer = lock.enter();
361        let inner = lock.enter();
362        assert!(lock.is_held_by_current_thread());
363
364        let (thread_lock, thread_inside) = (lock.clone(), inside.clone());
365        let other = thread::spawn(move || {
366            assert!(!thread_lock.is_held_by_current_thread());
367            let _guard = thread_lock.enter();
368            thread_inside.fetch_add(1, Ordering::SeqCst);
369        });
370
371        thread::sleep(std::time::Duration::from_millis(20));
372        assert_eq!(0, inside.load(Ordering::SeqCst));
373        drop(inner);
374        thread::sleep(std::time::Duration::from_millis(20));
375        assert_eq!(0, inside.load(Ordering::SeqCst));
376        drop(outer);
377        other.join().unwrap();
378        assert_eq!(1, inside.load(Ordering::SeqCst));
379        assert!(!lock.is_held_by_current_thread());
380    }
381}