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}