Skip to main content

ferroui_base/rendering/composition/server/
readback_indices.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::{Mutex, MutexGuard, PoisonError};
3
4/// A helper class used to manage the current slots for writing data from the render thread
5/// and reading it from the UI thread.
6/// Used mostly by hit-testing which needs to know the last transform of the visual
7///
8/// The lock is held for the whole duration of a write: [`begin_write`]
9/// returns a scope that owns the lock, and [`ReadbackWriteScope::end_write`]
10/// (or dropping the scope) completes the write and releases it. The
11/// revisions themselves are atomics and can be read from any thread without
12/// taking the lock, as the properties of the C# class can.
13///
14/// Unlike the C# monitor the lock is not reentrant: calling
15/// [`next_read`](Self::next_read), [`begin_write`](Self::begin_write) or
16/// [`lock`](Self::lock) on the thread that currently holds a write scope or
17/// a lock guard deadlocks.
18///
19/// [`begin_write`]: Self::begin_write
20pub struct ReadbackIndices {
21    lock: Mutex<()>,
22
23    read_revision: AtomicU64,
24    next_write_revision: AtomicU64,
25    write_revision: AtomicU64,
26    last_completed_write: AtomicU64,
27}
28
29impl ReadbackIndices {
30    pub fn new() -> Self {
31        Self {
32            lock: Mutex::new(()),
33            read_revision: AtomicU64::new(0),
34            next_write_revision: AtomicU64::new(1),
35            write_revision: AtomicU64::new(0),
36            last_completed_write: AtomicU64::new(0),
37        }
38    }
39
40    /// Takes the lock that guards the indices (the public `_lock` field of
41    /// the C# class), blocking writes and [`next_read`](Self::next_read)
42    /// while the guard is alive.
43    pub fn lock(&self) -> MutexGuard<'_, ()> {
44        self.lock.lock().unwrap_or_else(PoisonError::into_inner)
45    }
46
47    pub fn read_revision(&self) -> u64 {
48        self.read_revision.load(Ordering::SeqCst)
49    }
50
51    pub fn write_revision(&self) -> u64 {
52        self.write_revision.load(Ordering::SeqCst)
53    }
54
55    pub fn last_completed_write(&self) -> u64 {
56        self.last_completed_write.load(Ordering::SeqCst)
57    }
58
59    pub fn next_read(&self) {
60        let _guard = self.lock();
61        self.read_revision.store(self.last_completed_write.load(Ordering::SeqCst), Ordering::SeqCst);
62    }
63
64    /// Takes the lock and starts the next write revision. The write lasts
65    /// until the returned scope is ended.
66    pub fn begin_write(&self) -> ReadbackWriteScope<'_> {
67        let guard = self.lock();
68        let revision = self.next_write_revision.fetch_add(1, Ordering::SeqCst);
69        self.write_revision.store(revision, Ordering::SeqCst);
70        ReadbackWriteScope { indices: self, _guard: guard }
71    }
72}
73
74impl Default for ReadbackIndices {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80/// A write in progress, started by [`ReadbackIndices::begin_write`].
81///
82/// Ending the scope (explicitly with [`end_write`](Self::end_write) or by
83/// dropping it) marks the write revision as completed and releases the lock.
84#[must_use = "the write ends as soon as the scope is dropped"]
85pub struct ReadbackWriteScope<'a> {
86    indices: &'a ReadbackIndices,
87    _guard: MutexGuard<'a, ()>,
88}
89
90impl ReadbackWriteScope<'_> {
91    /// Completes the write and releases the lock (C# `EndWrite`).
92    pub fn end_write(self) {}
93}
94
95impl Drop for ReadbackWriteScope<'_> {
96    fn drop(&mut self) {
97        // The guard field is released after this body runs.
98        self.indices
99            .last_completed_write
100            .store(self.indices.write_revision.load(Ordering::SeqCst), Ordering::SeqCst);
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use std::sync::Arc;
108
109    fn assert_send_sync<T: Send + Sync>() {}
110
111    #[test]
112    fn is_send_and_sync() {
113        assert_send_sync::<ReadbackIndices>();
114    }
115
116    #[test]
117    fn initial_state() {
118        let indices = ReadbackIndices::new();
119        assert_eq!(0, indices.read_revision());
120        assert_eq!(0, indices.write_revision());
121        assert_eq!(0, indices.last_completed_write());
122        indices.next_read();
123        assert_eq!(0, indices.read_revision());
124    }
125
126    #[test]
127    fn write_revisions_advance_and_complete_on_end_write() {
128        let indices = ReadbackIndices::new();
129
130        let write = indices.begin_write();
131        assert_eq!(1, indices.write_revision());
132        assert_eq!(0, indices.last_completed_write());
133        write.end_write();
134        assert_eq!(1, indices.last_completed_write());
135        // Readers only see a completed write after `next_read`.
136        assert_eq!(0, indices.read_revision());
137        indices.next_read();
138        assert_eq!(1, indices.read_revision());
139
140        indices.begin_write().end_write();
141        indices.begin_write().end_write();
142        assert_eq!(3, indices.write_revision());
143        assert_eq!(3, indices.last_completed_write());
144        assert_eq!(1, indices.read_revision());
145        indices.next_read();
146        assert_eq!(3, indices.read_revision());
147    }
148
149    #[test]
150    fn dropping_the_scope_ends_the_write() {
151        let indices = ReadbackIndices::new();
152        {
153            let _write = indices.begin_write();
154            assert_eq!(0, indices.last_completed_write());
155        }
156        assert_eq!(1, indices.last_completed_write());
157        // The lock is free again.
158        drop(indices.lock());
159    }
160
161    #[test]
162    fn the_lock_is_held_during_a_write() {
163        let indices = ReadbackIndices::new();
164        let write = indices.begin_write();
165        assert!(indices.lock.try_lock().is_err());
166        write.end_write();
167        assert!(indices.lock.try_lock().is_ok());
168    }
169
170    #[test]
171    fn next_read_waits_for_a_write_in_progress() {
172        let indices = Arc::new(ReadbackIndices::new());
173        let (started_tx, started_rx) = std::sync::mpsc::channel();
174        let (finish_tx, finish_rx) = std::sync::mpsc::channel::<()>();
175
176        let writer = {
177            let indices = indices.clone();
178            std::thread::spawn(move || {
179                let write = indices.begin_write();
180                started_tx.send(()).unwrap();
181                finish_rx.recv().unwrap();
182                write.end_write();
183            })
184        };
185
186        started_rx.recv().unwrap();
187        // The write is in progress: nothing is completed yet.
188        assert_eq!(1, indices.write_revision());
189        assert_eq!(0, indices.last_completed_write());
190
191        let reader = {
192            let indices = indices.clone();
193            std::thread::spawn(move || {
194                // Blocks until the writer releases the lock.
195                indices.next_read();
196                indices.read_revision()
197            })
198        };
199
200        finish_tx.send(()).unwrap();
201        writer.join().unwrap();
202        assert_eq!(1, reader.join().unwrap());
203    }
204}