1use std::{
2 any::Any,
3 cell::RefCell,
4 collections::{HashMap, HashSet, VecDeque},
5 rc::Rc,
6};
7
8use slotmap::SlotMap;
9#[cfg(debug_assertions)]
10use slotmap::SparseSecondaryMap;
11
12use crate::{Property, TransitionManager, TransitionQueueEntry};
13
14use super::{private::PropertyId, PropertyValue};
15
16#[derive(Clone, Debug, Default)]
17pub struct EffectDrainReport {
18 pub ran: usize,
19 pub popped: usize,
20 pub skipped_clean: usize,
21 pub skipped_unregistered: usize,
22 pub skipped_missing: usize,
23 pub remaining: usize,
24 pub cutoffs_evaluated: usize,
25 pub cutoffs_suppressed: usize,
26 pub cutoffs_propagated: usize,
27 pub remaining_cutoffs: usize,
28 pub budget_exhausted: bool,
29 pub top_effects: Vec<(String, usize)>,
30 pub top_cutoffs: Vec<(String, usize)>,
31}
32
33thread_local! {
34 pub(crate) static PROPERTY_TABLE: PropertyTable = PropertyTable::default();
36 pub(crate) static PROPERTY_TIME: RefCell<Property<u64>> = RefCell::new(Property::new(0));
38 pub(crate) static PROPERTY_MILLIS: RefCell<Property<u64>> = RefCell::new(Property::new(0));
40}
41
42pub struct PropertyData {
44 typed_data: Box<dyn Any>,
48 pub inbound: Vec<PropertyId>,
50 pub outbound: Vec<PropertyId>,
52 pub dirty: bool,
56 pub(super) cutoff_settler: Option<Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>>,
59}
60
61impl PropertyData {
62 fn typed_data<T: 'static>(&mut self) -> &mut TypedPropertyData<T> {
63 self.typed_data
64 .downcast_mut::<TypedPropertyData<T>>()
65 .unwrap_or_else(|| panic!("Failed to downcast to TypedPropertyData<{}>. The actual type does not match the expected type.", std::any::type_name::<T>()))
66 }
67}
68
69pub struct TypedPropertyData<T> {
71 value: T,
72 transition_manager: Option<TransitionManager<T>>,
73 property_type: PropertyType<T>,
75}
76
77#[derive(Clone)]
79pub(crate) enum PropertyType<T> {
80 Literal,
81 Computed {
82 evaluator: Rc<dyn Fn() -> T>,
84 cutoff: Option<Cutoff<T>>,
85 },
86}
87
88#[derive(Clone)]
89pub(crate) struct Cutoff<T> {
90 pub(super) predicate: Rc<dyn Fn(&T, &T) -> bool>,
91 pub(super) initialized: bool,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub(super) enum CutoffEvaluation {
96 NotCutoff,
97 Initialized,
98 Suppressed,
99 Propagated,
100}
101
102#[derive(Default)]
104pub(crate) struct PropertyTable {
105 pub(crate) property_map: RefCell<SlotMap<PropertyId, Entry>>,
108 #[cfg(debug_assertions)]
109 debug_names: RefCell<SparseSecondaryMap<PropertyId, String>>,
110 effect_debug_names: RefCell<HashMap<PropertyId, String>>,
111 effect_properties: RefCell<HashSet<PropertyId>>,
112 queued_effects: RefCell<VecDeque<PropertyId>>,
113 queued_effect_set: RefCell<HashSet<PropertyId>>,
114 queued_cutoffs: RefCell<VecDeque<PropertyId>>,
115 queued_cutoff_set: RefCell<HashSet<PropertyId>>,
116}
117
118pub struct Entry {
120 ref_count: usize,
121 data: Option<PropertyData>,
122}
123
124impl PropertyTable {
125 pub(crate) fn has_value_type<T: PropertyValue>(&self, id: PropertyId) -> bool {
126 self.with_property_data(id, |data| data.typed_data.is::<TypedPropertyData<T>>())
127 }
128
129 pub fn get_value<T: PropertyValue>(&self, id: PropertyId) -> T {
133 self.update_value::<T>(id);
134 self.with_property_data_mut(id, |property_data| {
135 property_data.typed_data::<T>().value.clone()
136 })
137 }
138
139 pub fn read_value<T: PropertyValue, V>(&self, id: PropertyId, f: impl FnOnce(&T) -> V) -> V {
141 self.update_value::<T>(id);
142 self.with_property_data_mut(id, |property_data| {
143 f(&property_data.typed_data::<T>().value)
144 })
145 }
146
147 pub fn set_value<T: PropertyValue>(&self, id: PropertyId, new_val: T) {
151 self.with_property_data_mut(id, |property_data: &mut PropertyData| {
152 let typed_data = property_data.typed_data();
153 typed_data.value = new_val;
154 property_data.dirty = false;
155 });
156 self.dirtify_outbound(id);
157 }
158
159 pub fn invalidate(&self, id: PropertyId) {
160 let (newly_dirty, is_cutoff) = self.with_property_data_mut(id, |property_data| {
161 if property_data.dirty {
162 (false, property_data.cutoff_settler.is_some())
163 } else {
164 property_data.dirty = true;
165 (true, property_data.cutoff_settler.is_some())
166 }
167 });
168 if newly_dirty {
169 if is_cutoff {
170 self.enqueue_cutoff(id);
171 } else {
172 self.enqueue_effect_if_registered(id);
173 self.dirtify_outbound(id);
174 }
175 }
176 }
177
178 pub fn add_entry<T: PropertyValue>(
180 &self,
181 start_val: T,
182 inbound: Vec<PropertyId>,
183 data: PropertyType<T>,
184 debug_name: Option<&str>,
185 ) -> PropertyId {
186 #[cfg(not(debug_assertions))]
187 let _ = debug_name;
188
189 let is_cutoff = matches!(
190 &data,
191 PropertyType::Computed {
192 cutoff: Some(_),
193 ..
194 }
195 );
196 let cutoff_settler = is_cutoff.then(|| {
197 Rc::new(|table: &PropertyTable, id| table.update_value::<T>(id))
198 as Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation>
199 });
200
201 let id = {
202 let Ok(mut sm) = self.property_map.try_borrow_mut() else {
203 #[cfg(debug_assertions)]
204 panic!(
205 "couldn't create new property \"{}\"- table already borrowed",
206 debug_name.unwrap_or("<no name>")
207 );
208 #[cfg(not(debug_assertions))]
209 panic!("couldn't create new property - table already borrowed");
210 };
211 let entry = Entry {
212 ref_count: 1,
213 data: Some(PropertyData {
214 inbound,
215 dirty: true,
216 typed_data: Box::new(TypedPropertyData {
217 value: start_val,
218 property_type: data,
219 transition_manager: None,
220 }),
221 outbound: Vec::with_capacity(0),
222 cutoff_settler,
223 }),
224 };
225 sm.insert(entry)
226 };
227 #[cfg(debug_assertions)]
228 {
229 if let Some(name) = debug_name {
230 self.debug_names.borrow_mut().insert(id, name.to_owned());
231 }
232 }
233 self.connect_inbound(id);
234 id
235 }
236
237 pub fn transition<T: PropertyValue>(
241 &self,
242 id: PropertyId,
243 transition: TransitionQueueEntry<T>,
244 overwrite: bool,
245 ) {
246 let mut should_reconnect_inbound = false;
247 let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
248 let (millis_id, curr_millis) =
249 PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
250 self.with_property_data_mut(id, |property_data: &mut PropertyData| {
251 let typed_data = property_data.typed_data::<T>();
252 let transition_manager = typed_data.transition_manager.get_or_insert_with(|| {
253 TransitionManager::new(typed_data.value.clone(), curr_time, curr_millis)
254 });
255 if overwrite {
256 transition_manager.reset_transitions(curr_time, curr_millis);
257 }
258 transition_manager.push_transition(transition);
259 if !property_data.inbound.contains(&time_id) {
260 should_reconnect_inbound = true;
261 property_data.inbound.push(time_id);
262 }
263 if !property_data.inbound.contains(&millis_id) {
264 should_reconnect_inbound = true;
265 property_data.inbound.push(millis_id);
266 }
267 });
268 if should_reconnect_inbound {
269 self.disconnect_inbound(id);
270 self.connect_inbound(id);
271 }
272 }
273
274 pub fn cancel_transitions<T: PropertyValue>(&self, id: PropertyId) {
275 let (time_id, curr_time) = PROPERTY_TIME.with_borrow(|time| (time.untyped.id, time.get()));
276 let (millis_id, curr_millis) =
277 PROPERTY_MILLIS.with_borrow(|time| (time.untyped.id, time.get()));
278 let has_transition = self.with_property_data(id, |property_data| {
279 property_data
280 .typed_data
281 .downcast_ref::<TypedPropertyData<T>>()
282 .expect("property type should match")
283 .transition_manager
284 .is_some()
285 });
286 if !has_transition {
287 return;
288 }
289
290 self.disconnect_inbound(id);
291 let value_changed = self.with_property_data_mut(id, |property_data| {
292 let typed_data = property_data.typed_data::<T>();
293 let current_value = typed_data
294 .transition_manager
295 .as_mut()
296 .and_then(|manager| manager.compute_eased_value(curr_time, curr_millis));
297 let value_changed = current_value.is_some();
298 if let Some(current_value) = current_value {
299 typed_data.value = current_value;
300 }
301 typed_data.transition_manager = None;
302 property_data
303 .inbound
304 .retain(|dependency| *dependency != time_id && *dependency != millis_id);
305 property_data.dirty = false;
306 value_changed
307 });
308 self.connect_inbound(id);
309
310 if value_changed {
311 self.dirtify_outbound(id);
312 }
313 }
314
315 fn with_entry_mut<V>(&self, id: PropertyId, f: impl FnOnce(&mut Entry) -> V) -> V {
321 let mut sm = self.property_map.borrow_mut();
322 let data = sm.get_mut(id).unwrap();
323 let return_value = f(data);
324 return_value
325 }
326
327 pub fn with_property_data_mut<V>(
332 &self,
333 id: PropertyId,
334 f: impl FnOnce(&mut PropertyData) -> V,
335 ) -> V {
336 let mut property_data = self.with_entry_mut(id, |entry| {
338 entry
339 .data
340 .take()
341 .expect("property data should not have already been taken")
342 });
343
344 let res = f(&mut property_data);
348
349 self.with_entry_mut(id, |entry| {
351 entry.data = Some(property_data);
352 });
353
354 res
355 }
356
357 pub fn with_property_data<V>(&self, id: PropertyId, f: impl FnOnce(&PropertyData) -> V) -> V {
362 self.with_property_data_mut(id, |property_data| f(&*property_data))
363 }
364
365 pub fn increase_ref_count(&self, id: PropertyId) -> usize {
367 self.with_entry_mut(id, |entry| {
368 entry.ref_count += 1;
369 entry.ref_count
370 })
371 }
372
373 pub fn decrease_ref_count(&self, id: PropertyId) -> usize {
375 self.with_entry_mut(id, |entry| {
376 entry.ref_count -= 1;
377 entry.ref_count
378 })
379 }
380
381 pub fn replace_property_keep_outbound_connections<T: PropertyValue>(
386 &self,
387 source_id: PropertyId,
388 target_id: PropertyId,
389 ) {
390 if source_id == target_id {
391 return;
392 }
393
394 self.disconnect_inbound(source_id);
397
398 self.with_property_data_mut(source_id, |source_property_data| {
400 self.with_property_data_mut(target_id, |target_property_data| {
401 source_property_data
403 .inbound
404 .clone_from(&target_property_data.inbound);
405 source_property_data.dirty = target_property_data.dirty;
406 source_property_data.cutoff_settler = target_property_data.cutoff_settler.clone();
407 let source_typed = source_property_data.typed_data::<T>();
408 let target_typed = target_property_data.typed_data::<T>();
409 source_typed.value = target_typed.value.clone();
410 source_typed.property_type = target_typed.property_type.clone();
411 if target_property_data.dirty {
412 if let PropertyType::Computed {
413 cutoff: Some(cutoff),
414 ..
415 } = &mut source_typed.property_type
416 {
417 cutoff.initialized = false;
418 }
419 }
420 });
421 });
422
423 self.connect_inbound(source_id);
425
426 if self.is_registered_effect(source_id) && self.is_cutoff(source_id) {
427 panic!("cutoff properties cannot be registered as effects");
428 }
429 if self.is_cutoff(source_id) && self.is_dirty(source_id) {
430 self.enqueue_cutoff(source_id);
431 }
432
433 self.dirtify_outbound(source_id);
436
437 #[cfg(debug_assertions)]
438 {
439 let target_name = self.debug_name(target_id);
441 let mut names = self.debug_names.borrow_mut();
442 names.insert(source_id, format!("{}", target_name));
443 }
444
445 self.enqueue_effect_if_registered(source_id);
446 }
447
448 pub fn update_value<T: PropertyValue>(&self, id: PropertyId) -> CutoffEvaluation {
450 let mut remove_dep_from_literal = false;
451 let evaluator = self.with_property_data_mut(id, |property_data| {
452 if property_data.dirty == false {
454 return None;
455 }
456 property_data.dirty = false;
457 let typed_data = property_data.typed_data::<T>();
458 match &mut typed_data.property_type {
459 PropertyType::Computed { evaluator, cutoff } => Some((
460 Rc::clone(&evaluator),
461 cutoff.as_ref().map(|cutoff| {
462 (
463 Rc::clone(&cutoff.predicate),
464 cutoff.initialized,
465 typed_data.value.clone(),
466 )
467 }),
468 )),
469 PropertyType::Literal => {
470 let tm = typed_data.transition_manager.as_mut()?;
471 let curr_time = PROPERTY_TIME.with_borrow(|time| time.get());
472 let curr_millis = PROPERTY_MILLIS.with_borrow(|time| time.get());
473 let value = tm.compute_eased_value(curr_time, curr_millis);
474 if let Some(interp_value) = value {
475 typed_data.value = interp_value;
476 } else {
477 remove_dep_from_literal = true;
479 typed_data.transition_manager = None;
480 }
481 None
482 }
483 }
484 });
485
486 if remove_dep_from_literal {
487 self.disconnect_inbound(id);
488 self.with_property_data_mut(id, |property_data| {
489 property_data.inbound.clear();
490 });
491 }
492
493 if let Some((evaluator, cutoff)) = evaluator {
494 let new_value = { evaluator() };
499 let cutoff_evaluation = match cutoff.as_ref() {
502 None => CutoffEvaluation::NotCutoff,
503 Some((_, false, _)) => CutoffEvaluation::Initialized,
504 Some((predicate, true, old_value)) if predicate(old_value, &new_value) => {
505 CutoffEvaluation::Suppressed
506 }
507 Some(_) => CutoffEvaluation::Propagated,
508 };
509 self.with_property_data_mut(id, |property_data| {
510 let typed_data = property_data.typed_data();
511 if let PropertyType::Computed {
512 cutoff: Some(cutoff),
513 ..
514 } = &mut typed_data.property_type
515 {
516 cutoff.initialized = true;
517 }
518
519 if cutoff_evaluation != CutoffEvaluation::Suppressed {
520 typed_data.value = new_value;
521 }
522 });
523
524 if cutoff_evaluation == CutoffEvaluation::Propagated {
525 self.dirtify_outbound(id);
526 }
527 cutoff_evaluation
528 } else {
529 CutoffEvaluation::NotCutoff
530 }
531 }
532
533 pub fn remove_entry(&self, id: PropertyId) {
535 let res = {
536 self.unregister_effect(id);
537 self.queued_cutoff_set.borrow_mut().remove(&id);
538 self.disconnect_outbound(id);
539 self.disconnect_inbound(id);
540 let Ok(mut sm) = self.property_map.try_borrow_mut() else {
541 #[cfg(debug_assertions)]
542 panic!(
543 "failed to remove property \"{}\" - propertytable already borrowed",
544 self.debug_name(id),
545 );
546 #[cfg(not(debug_assertions))]
547 panic!("failed to remove property - propertytable already borrowed");
548 };
549 sm.remove(id).expect("tried to remove non-existent prop")
550 };
551 drop(res);
552 }
553
554 #[cfg(debug_assertions)]
556 pub fn debug_name(&self, id: PropertyId) -> String {
557 self.debug_names
558 .borrow()
559 .get(id)
560 .as_ref()
561 .map(|s| s.as_str())
562 .unwrap_or("<NO DEBUG NAME>")
563 .to_owned()
564 }
565
566 pub(crate) fn total_properties_count(&self) -> usize {
568 self.property_map.borrow().len()
569 }
570
571 pub(crate) fn register_effect(&self, id: PropertyId) {
572 self.register_effect_with_name(id, None);
573 }
574
575 pub(crate) fn register_effect_with_name(&self, id: PropertyId, debug_name: Option<&str>) {
576 if self.is_cutoff(id) {
577 panic!("cutoff properties cannot be registered as effects");
578 }
579 if let Some(debug_name) = debug_name {
580 self.effect_debug_names
581 .borrow_mut()
582 .insert(id, debug_name.to_owned());
583 }
584 self.effect_properties.borrow_mut().insert(id);
585 self.enqueue_effect_if_registered(id);
586 }
587
588 pub(crate) fn drain_effects(&self, max_iterations: usize) -> usize {
589 let mut work_ran = 0;
590 while work_ran < max_iterations {
591 if let Some(id) = self.pop_queued_cutoff() {
592 if !self.has_live_entry(id) || !self.is_cutoff(id) || !self.is_dirty(id) {
593 continue;
594 }
595 let settler = self.cutoff_settler(id);
596 settler(self, id);
597 work_ran += 1;
598 continue;
599 }
600
601 let Some(id) = self.pop_queued_effect() else {
602 break;
603 };
604 if !self.is_registered_effect(id) || !self.has_live_entry(id) {
605 continue;
606 }
607 let dirty = self.with_property_data(id, |property_data| property_data.dirty);
608 if dirty {
609 self.update_value::<()>(id);
610 work_ran += 1;
611 }
612 }
613 work_ran
614 }
615
616 pub(crate) fn drain_effects_with_report(&self, max_iterations: usize) -> EffectDrainReport {
617 let mut report = EffectDrainReport::default();
618 let mut effect_counts = HashMap::new();
619 let mut cutoff_counts = HashMap::new();
620
621 let mut work_ran = 0;
622 while work_ran < max_iterations {
623 if let Some(id) = self.pop_queued_cutoff() {
624 if !self.has_live_entry(id) || !self.is_cutoff(id) || !self.is_dirty(id) {
625 continue;
626 }
627 let cutoff_name = self.debug_name_for_diagnostics(id);
628 let cutoff_category = cutoff_name
629 .split_once(" (")
630 .map(|(category, _)| category)
631 .unwrap_or(&cutoff_name)
632 .to_owned();
633 let settler = self.cutoff_settler(id);
634 match settler(self, id) {
635 CutoffEvaluation::Suppressed => report.cutoffs_suppressed += 1,
636 CutoffEvaluation::Propagated => report.cutoffs_propagated += 1,
637 CutoffEvaluation::NotCutoff | CutoffEvaluation::Initialized => {}
638 }
639 report.cutoffs_evaluated += 1;
640 work_ran += 1;
641 *cutoff_counts.entry(cutoff_category).or_insert(0) += 1;
642 continue;
643 }
644
645 let Some(id) = self.pop_queued_effect() else {
646 break;
647 };
648 report.popped += 1;
649
650 if !self.is_registered_effect(id) {
651 report.skipped_unregistered += 1;
652 continue;
653 }
654 if !self.has_live_entry(id) {
655 report.skipped_missing += 1;
656 continue;
657 }
658
659 let dirty = self.with_property_data(id, |property_data| property_data.dirty);
660 if dirty {
661 let effect_name = self.debug_name_for_diagnostics(id);
662 self.update_value::<()>(id);
663 report.ran += 1;
664 work_ran += 1;
665 *effect_counts.entry(effect_name).or_insert(0) += 1;
666 } else {
667 report.skipped_clean += 1;
668 }
669 }
670
671 report.remaining = self.queued_effects.borrow().len();
672 report.remaining_cutoffs = self.queued_cutoffs.borrow().len();
673 report.budget_exhausted =
674 work_ran == max_iterations && (report.remaining > 0 || report.remaining_cutoffs > 0);
675 report.top_effects = effect_counts.into_iter().collect();
676 report
677 .top_effects
678 .sort_by(|(left_name, left_count), (right_name, right_count)| {
679 right_count
680 .cmp(left_count)
681 .then_with(|| left_name.cmp(right_name))
682 });
683 report.top_effects.truncate(8);
684 report.top_cutoffs = cutoff_counts.into_iter().collect();
685 report
686 .top_cutoffs
687 .sort_by(|(left_name, left_count), (right_name, right_count)| {
688 right_count
689 .cmp(left_count)
690 .then_with(|| left_name.cmp(right_name))
691 });
692 report.top_cutoffs.truncate(8);
693 report
694 }
695
696 pub(crate) fn enqueue_effect_if_registered(&self, id: PropertyId) {
697 if self.is_registered_effect(id) {
698 self.enqueue_effect(id);
699 }
700 }
701
702 fn enqueue_effect(&self, id: PropertyId) {
703 let mut queued_set = self.queued_effect_set.borrow_mut();
704 if queued_set.insert(id) {
705 self.queued_effects.borrow_mut().push_back(id);
706 }
707 }
708
709 fn pop_queued_effect(&self) -> Option<PropertyId> {
710 let id = self.queued_effects.borrow_mut().pop_front()?;
711 self.queued_effect_set.borrow_mut().remove(&id);
712 Some(id)
713 }
714
715 pub(crate) fn enqueue_cutoff(&self, id: PropertyId) {
716 let mut queued_set = self.queued_cutoff_set.borrow_mut();
717 if queued_set.insert(id) {
718 self.queued_cutoffs.borrow_mut().push_back(id);
719 }
720 }
721
722 fn pop_queued_cutoff(&self) -> Option<PropertyId> {
723 let id = self.queued_cutoffs.borrow_mut().pop_front()?;
724 self.queued_cutoff_set.borrow_mut().remove(&id);
725 Some(id)
726 }
727
728 fn unregister_effect(&self, id: PropertyId) {
729 self.effect_properties.borrow_mut().remove(&id);
730 self.effect_debug_names.borrow_mut().remove(&id);
731 self.queued_effect_set.borrow_mut().remove(&id);
732 }
733
734 fn is_registered_effect(&self, id: PropertyId) -> bool {
735 self.effect_properties.borrow().contains(&id)
736 }
737
738 pub(crate) fn is_cutoff(&self, id: PropertyId) -> bool {
739 self.with_property_data(id, |data| data.cutoff_settler.is_some())
740 }
741
742 fn is_dirty(&self, id: PropertyId) -> bool {
743 self.with_property_data(id, |data| data.dirty)
744 }
745
746 fn cutoff_settler(
747 &self,
748 id: PropertyId,
749 ) -> Rc<dyn Fn(&PropertyTable, PropertyId) -> CutoffEvaluation> {
750 self.with_property_data(id, |data| {
751 data.cutoff_settler
752 .as_ref()
753 .expect("queued cutoff property must have a settler")
754 .clone()
755 })
756 }
757
758 fn has_live_entry(&self, id: PropertyId) -> bool {
759 self.property_map
760 .borrow()
761 .get(id)
762 .and_then(|entry| entry.data.as_ref())
763 .is_some()
764 }
765
766 fn debug_name_for_diagnostics(&self, id: PropertyId) -> String {
767 if let Some(name) = self.effect_debug_names.borrow().get(&id) {
768 return name.clone();
769 }
770
771 #[cfg(debug_assertions)]
772 {
773 if let Some(name) = self.debug_names.borrow().get(id) {
774 return name.clone();
775 }
776 }
777
778 format!("{id:?}")
779 }
780
781 pub(crate) fn outbound_debug_names(&self, id: PropertyId) -> Vec<String> {
782 self.with_property_data(id, |data| {
783 data.outbound
784 .iter()
785 .copied()
786 .filter(|outbound_id| self.has_live_entry(*outbound_id))
787 .map(|outbound_id| self.debug_name_for_diagnostics(outbound_id))
788 .collect()
789 })
790 }
791
792 pub(crate) fn has_direct_outbound(
793 &self,
794 source_id: PropertyId,
795 outbound_id: PropertyId,
796 ) -> bool {
797 self.with_property_data(source_id, |data| data.outbound.contains(&outbound_id))
798 }
799}