cranpose_ui/
render_state.rs1use cranpose_core::{current_runtime_handle, NodeId, SnapshotStateObserver};
2use std::collections::HashSet;
3use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
4use std::sync::Mutex;
5#[cfg(not(any(test, feature = "test-helpers")))]
6use std::sync::OnceLock;
7#[cfg(test)]
8use std::sync::OnceLock;
9
10struct RenderState {
11 layout_repasses: Mutex<LayoutRepassManager>,
12 draw_repasses: Mutex<DrawRepassManager>,
13 modifier_slice_repasses: Mutex<LayoutRepassManager>,
14 render_invalidated: AtomicBool,
15 pointer_invalidated: AtomicBool,
16 focus_invalidated: AtomicBool,
17 layout_invalidated: AtomicBool,
18 density_bits: AtomicU32,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub(crate) struct DrawObservationScope {
23 node_id: NodeId,
24 command_index: usize,
25}
26
27impl DrawObservationScope {
28 pub(crate) fn new(node_id: NodeId, command_index: usize) -> Self {
29 Self {
30 node_id,
31 command_index,
32 }
33 }
34}
35
36std::thread_local! {
37 static DRAW_OBSERVER: SnapshotStateObserver = {
38 let observer = SnapshotStateObserver::new(|callback| {
39 if let Some(runtime) = current_runtime_handle() {
40 runtime.enqueue_ui_task(callback);
41 } else {
42 callback();
43 }
44 });
45 observer.start();
46 observer
47 };
48}
49
50pub(crate) fn observe_draw_reads<R>(scope: DrawObservationScope, block: impl FnOnce() -> R) -> R {
51 DRAW_OBSERVER.with(|observer| {
52 observer.observe_reads(
53 scope,
54 |scope| {
55 schedule_draw_repass(scope.node_id);
56 },
57 block,
58 )
59 })
60}
61
62pub(crate) fn clear_draw_observations_for_node(node_id: NodeId) {
63 DRAW_OBSERVER.with(|observer| {
64 observer.clear_if(|scope| {
65 scope
66 .downcast_ref::<DrawObservationScope>()
67 .is_some_and(|scope| scope.node_id == node_id)
68 });
69 });
70}
71
72impl RenderState {
73 fn new() -> Self {
74 Self {
75 layout_repasses: Mutex::new(LayoutRepassManager::new()),
76 draw_repasses: Mutex::new(DrawRepassManager::new()),
77 modifier_slice_repasses: Mutex::new(LayoutRepassManager::new()),
78 render_invalidated: AtomicBool::new(false),
79 pointer_invalidated: AtomicBool::new(false),
80 focus_invalidated: AtomicBool::new(false),
81 layout_invalidated: AtomicBool::new(false),
82 density_bits: AtomicU32::new(f32::to_bits(1.0)),
83 }
84 }
85}
86
87#[cfg(not(any(test, feature = "test-helpers")))]
88fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
89 static STATE: OnceLock<RenderState> = OnceLock::new();
90 f(STATE.get_or_init(RenderState::new))
91}
92
93#[cfg(any(test, feature = "test-helpers"))]
94fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
95 std::thread_local! {
96 static STATE: RenderState = RenderState::new();
97 }
98 STATE.with(f)
99}
100
101struct LayoutRepassManager {
106 dirty_nodes: HashSet<NodeId>,
107}
108
109impl LayoutRepassManager {
110 fn new() -> Self {
111 Self {
112 dirty_nodes: HashSet::new(),
113 }
114 }
115
116 fn schedule_repass(&mut self, node_id: NodeId) {
117 self.dirty_nodes.insert(node_id);
118 }
119
120 fn has_pending_repass(&self) -> bool {
121 !self.dirty_nodes.is_empty()
122 }
123
124 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
125 self.dirty_nodes.drain().collect()
126 }
127}
128
129struct DrawRepassManager {
131 dirty_nodes: HashSet<NodeId>,
132}
133
134impl DrawRepassManager {
135 fn new() -> Self {
136 Self {
137 dirty_nodes: HashSet::new(),
138 }
139 }
140
141 fn schedule_repass(&mut self, node_id: NodeId) {
142 self.dirty_nodes.insert(node_id);
143 }
144
145 fn has_pending_repass(&self) -> bool {
146 !self.dirty_nodes.is_empty()
147 }
148
149 fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
150 self.dirty_nodes.drain().collect()
151 }
152}
153
154pub fn schedule_layout_repass(node_id: NodeId) {
173 with_render_state(|state| {
174 state
175 .layout_repasses
176 .lock()
177 .expect("layout repass manager poisoned")
178 .schedule_repass(node_id);
179 state.layout_invalidated.store(true, Ordering::Relaxed);
180 });
181 request_render_invalidation();
188}
189
190pub(crate) fn schedule_modifier_slices_repass(node_id: NodeId) {
191 with_render_state(|state| {
192 state
193 .modifier_slice_repasses
194 .lock()
195 .expect("modifier slice repass manager poisoned")
196 .schedule_repass(node_id);
197 });
198 schedule_layout_repass(node_id);
199}
200
201pub fn schedule_draw_repass(node_id: NodeId) {
206 with_render_state(|state| {
207 state
208 .draw_repasses
209 .lock()
210 .expect("draw repass manager poisoned")
211 .schedule_repass(node_id);
212 });
213 request_render_invalidation();
214}
215
216pub fn has_pending_draw_repasses() -> bool {
218 with_render_state(|state| {
219 state
220 .draw_repasses
221 .lock()
222 .expect("draw repass manager poisoned")
223 .has_pending_repass()
224 })
225}
226
227pub fn take_draw_repass_nodes() -> Vec<NodeId> {
229 with_render_state(|state| {
230 state
231 .draw_repasses
232 .lock()
233 .expect("draw repass manager poisoned")
234 .take_dirty_nodes()
235 })
236}
237
238pub fn has_pending_layout_repasses() -> bool {
240 with_render_state(|state| {
241 state
242 .layout_repasses
243 .lock()
244 .expect("layout repass manager poisoned")
245 .has_pending_repass()
246 })
247}
248
249pub fn take_layout_repass_nodes() -> Vec<NodeId> {
253 with_render_state(|state| {
254 state
255 .layout_repasses
256 .lock()
257 .expect("layout repass manager poisoned")
258 .take_dirty_nodes()
259 })
260}
261
262pub(crate) fn take_modifier_slice_repass_nodes() -> Vec<NodeId> {
263 with_render_state(|state| {
264 state
265 .modifier_slice_repasses
266 .lock()
267 .expect("modifier slice repass manager poisoned")
268 .take_dirty_nodes()
269 })
270}
271
272pub fn current_density() -> f32 {
274 with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
275}
276
277pub fn set_density(density: f32) {
282 let normalized = if density.is_finite() && density > 0.0 {
283 density
284 } else {
285 1.0
286 };
287 let new_bits = normalized.to_bits();
288 with_render_state(|state| {
289 let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
290 if old_bits != new_bits {
291 state.layout_invalidated.store(true, Ordering::Relaxed);
292 }
293 });
294}
295
296pub fn request_render_invalidation() {
298 with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
299}
300
301pub fn take_render_invalidation() -> bool {
303 with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
304}
305
306pub fn peek_render_invalidation() -> bool {
308 with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
309}
310
311pub fn request_pointer_invalidation() {
313 with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
314}
315
316pub fn take_pointer_invalidation() -> bool {
318 with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
319}
320
321pub fn peek_pointer_invalidation() -> bool {
323 with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
324}
325
326pub fn request_focus_invalidation() {
328 with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
329}
330
331pub fn take_focus_invalidation() -> bool {
333 with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
334}
335
336pub fn peek_focus_invalidation() -> bool {
338 with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
339}
340
341pub fn request_layout_invalidation() {
368 with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
369}
370
371pub fn take_layout_invalidation() -> bool {
373 with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
374}
375
376pub fn peek_layout_invalidation() -> bool {
378 with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
379}
380
381#[cfg(any(test, feature = "test-helpers"))]
382#[doc(hidden)]
383pub fn reset_render_state_for_tests() {
384 let _ = take_draw_repass_nodes();
385 let _ = take_layout_repass_nodes();
386 let _ = take_modifier_slice_repass_nodes();
387 let _ = take_render_invalidation();
388 let _ = take_pointer_invalidation();
389 let _ = take_focus_invalidation();
390 let _ = take_layout_invalidation();
391 set_density(1.0);
392 let _ = take_layout_invalidation();
393}
394
395#[cfg(test)]
396pub(crate) fn render_state_test_guard() -> std::sync::MutexGuard<'static, ()> {
397 static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
398 match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
399 Ok(guard) => guard,
400 Err(poisoned) => poisoned.into_inner(),
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use std::sync::{mpsc, Arc};
408
409 #[test]
410 fn invalidation_flags_are_shared_across_threads() {
411 let state = Arc::new(RenderState::new());
412 let (tx, rx) = mpsc::channel();
413 let worker_state = Arc::clone(&state);
414
415 let handle = std::thread::spawn(move || {
416 worker_state
417 .render_invalidated
418 .store(true, Ordering::Relaxed);
419 worker_state
420 .pointer_invalidated
421 .store(true, Ordering::Relaxed);
422 worker_state
423 .focus_invalidated
424 .store(true, Ordering::Relaxed);
425 worker_state
426 .layout_invalidated
427 .store(true, Ordering::Relaxed);
428 worker_state
429 .density_bits
430 .store(f32::to_bits(2.0), Ordering::Relaxed);
431 tx.send(()).expect("signal invalidation setup");
432
433 f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
434 });
435
436 rx.recv().expect("wait for worker invalidation setup");
437 assert!(state.render_invalidated.load(Ordering::Relaxed));
438 assert!(state.pointer_invalidated.load(Ordering::Relaxed));
439 assert!(state.focus_invalidated.load(Ordering::Relaxed));
440 assert!(state.layout_invalidated.load(Ordering::Relaxed));
441 assert_eq!(
442 f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
443 2.0
444 );
445 assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
446 assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
447 assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
448 assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));
449
450 let density = handle.join().expect("worker invalidation snapshot");
451 assert_eq!(density, 2.0);
452 assert!(!state.render_invalidated.load(Ordering::Relaxed));
453 assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
454 assert!(!state.focus_invalidated.load(Ordering::Relaxed));
455 assert!(!state.layout_invalidated.load(Ordering::Relaxed));
456 }
457}