1use std::hash::{Hash, Hasher};
54
55use ftui_core::geometry::Rect;
56use rustc_hash::FxHashMap;
57
58use crate::{Constraint, Direction, LayoutSizeHint};
59
60#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
69pub struct LayoutCacheKey {
70 pub area_x: u16,
72 pub area_y: u16,
74 pub area_width: u16,
76 pub area_height: u16,
78 pub constraints_hash: u64,
80 pub constraints_hash_fx: u64,
82 pub constraints_len: u16,
84 pub direction: Direction,
86 pub intrinsics_hash: Option<u64>,
88 pub intrinsics_hash_fx: Option<u64>,
90}
91
92impl LayoutCacheKey {
93 pub fn new(
102 area: Rect,
103 constraints: &[Constraint],
104 direction: Direction,
105 intrinsics: Option<&[LayoutSizeHint]>,
106 ) -> Self {
107 let (ch1, ch2) = Self::hash_constraints(constraints);
108 let intr_hashes = intrinsics.map(Self::hash_intrinsics);
109 Self {
110 area_x: area.x,
111 area_y: area.y,
112 area_width: area.width,
113 area_height: area.height,
114 constraints_hash: ch1,
115 constraints_hash_fx: ch2,
116 constraints_len: constraints.len() as u16,
117 direction,
118 intrinsics_hash: intr_hashes.map(|h| h.0),
119 intrinsics_hash_fx: intr_hashes.map(|h| h.1),
120 }
121 }
122
123 #[inline]
125 pub fn area(&self) -> Rect {
126 Rect::new(self.area_x, self.area_y, self.area_width, self.area_height)
127 }
128
129 fn hash_constraints(constraints: &[Constraint]) -> (u64, u64) {
131 let mut h1 = std::collections::hash_map::DefaultHasher::new();
132 let mut h2 = rustc_hash::FxHasher::default();
133 for c in constraints {
134 std::mem::discriminant(c).hash(&mut h1);
135 std::mem::discriminant(c).hash(&mut h2);
136 match c {
137 Constraint::Fixed(v) => {
138 v.hash(&mut h1);
139 v.hash(&mut h2);
140 }
141 Constraint::Percentage(p) => {
142 p.to_bits().hash(&mut h1);
143 p.to_bits().hash(&mut h2);
144 }
145 Constraint::Min(v) => {
146 v.hash(&mut h1);
147 v.hash(&mut h2);
148 }
149 Constraint::Max(v) => {
150 v.hash(&mut h1);
151 v.hash(&mut h2);
152 }
153 Constraint::Ratio(n, d) => {
154 fn gcd(mut a: u32, mut b: u32) -> u32 {
155 while b != 0 {
156 let t = b;
157 b = a % b;
158 a = t;
159 }
160 a
161 }
162 let divisor = gcd(*n, *d);
163 if let (Some(n_div), Some(d_div)) =
164 (n.checked_div(divisor), d.checked_div(divisor))
165 {
166 n_div.hash(&mut h1);
167 d_div.hash(&mut h1);
168 n_div.hash(&mut h2);
169 d_div.hash(&mut h2);
170 } else {
171 n.hash(&mut h1);
172 d.hash(&mut h1);
173 n.hash(&mut h2);
174 d.hash(&mut h2);
175 }
176 }
177 Constraint::Fill => {}
178 Constraint::FitContent => {}
179 Constraint::FitContentBounded { min, max } => {
180 min.hash(&mut h1);
181 max.hash(&mut h1);
182 min.hash(&mut h2);
183 max.hash(&mut h2);
184 }
185 Constraint::FitMin => {}
186 }
187 }
188 (h1.finish(), h2.finish())
189 }
190
191 fn hash_intrinsics(intrinsics: &[LayoutSizeHint]) -> (u64, u64) {
193 let mut h1 = std::collections::hash_map::DefaultHasher::new();
194 let mut h2 = rustc_hash::FxHasher::default();
195 for hint in intrinsics {
196 hint.min.hash(&mut h1);
197 hint.preferred.hash(&mut h1);
198 hint.max.hash(&mut h1);
199 hint.min.hash(&mut h2);
200 hint.preferred.hash(&mut h2);
201 hint.max.hash(&mut h2);
202 }
203 (h1.finish(), h2.finish())
204 }
205}
206
207#[derive(Clone, Debug)]
209struct CachedLayoutEntry {
210 chunks: crate::Rects,
212 generation: u64,
214 access_count: u32,
216}
217
218#[derive(Debug, Clone, Default)]
220pub struct LayoutCacheStats {
221 pub entries: usize,
223 pub hits: u64,
225 pub misses: u64,
227 pub hit_rate: f64,
229}
230
231#[derive(Debug)]
248pub struct LayoutCache {
249 entries: FxHashMap<LayoutCacheKey, CachedLayoutEntry>,
250 generation: u64,
251 max_entries: usize,
252 hits: u64,
253 misses: u64,
254}
255
256impl LayoutCache {
257 #[inline]
271 pub fn new(max_entries: usize) -> Self {
272 Self {
273 entries: FxHashMap::with_capacity_and_hasher(max_entries, Default::default()),
274 generation: 0,
275 max_entries,
276 hits: 0,
277 misses: 0,
278 }
279 }
280
281 pub fn get_or_compute<F>(&mut self, key: LayoutCacheKey, compute: F) -> crate::Rects
299 where
300 F: FnOnce() -> crate::Rects,
301 {
302 if let Some(entry) = self.entries.get_mut(&key)
304 && entry.generation == self.generation
305 {
306 self.hits += 1;
307 entry.access_count = entry.access_count.saturating_add(1);
308 return entry.chunks.clone();
309 }
310
311 self.misses += 1;
313 let chunks = compute();
314
315 if self.entries.len() >= self.max_entries {
317 self.evict_lru();
318 }
319
320 self.entries.insert(
322 key,
323 CachedLayoutEntry {
324 chunks: chunks.clone(),
325 generation: self.generation,
326 access_count: 1,
327 },
328 );
329
330 chunks
331 }
332
333 #[inline]
349 pub fn invalidate_all(&mut self) {
350 self.generation = self.generation.wrapping_add(1);
351 }
352
353 pub fn stats(&self) -> LayoutCacheStats {
357 let total = self.hits + self.misses;
358 LayoutCacheStats {
359 entries: self.entries.len(),
360 hits: self.hits,
361 misses: self.misses,
362 hit_rate: if total > 0 {
363 self.hits as f64 / total as f64
364 } else {
365 0.0
366 },
367 }
368 }
369
370 #[inline]
374 pub fn reset_stats(&mut self) {
375 self.hits = 0;
376 self.misses = 0;
377 }
378
379 #[inline]
385 pub fn clear(&mut self) {
386 self.entries.clear();
387 self.generation = self.generation.wrapping_add(1);
388 }
389
390 #[inline]
392 pub fn len(&self) -> usize {
393 self.entries.len()
394 }
395
396 #[inline]
398 pub fn is_empty(&self) -> bool {
399 self.entries.is_empty()
400 }
401
402 #[inline]
404 pub fn capacity(&self) -> usize {
405 self.max_entries
406 }
407
408 fn evict_lru(&mut self) {
410 if let Some(key) = self
411 .entries
412 .iter()
413 .min_by_key(|(_, e)| e.access_count)
414 .map(|(k, _)| *k)
415 {
416 self.entries.remove(&key);
417 }
418 }
419}
420
421impl Default for LayoutCache {
422 fn default() -> Self {
424 Self::new(64)
425 }
426}
427
428#[derive(Debug)]
441pub struct S3FifoLayoutCache {
442 cache: ftui_core::s3_fifo::S3Fifo<LayoutCacheKey, CachedLayoutEntry>,
443 generation: u64,
444 max_entries: usize,
445 hits: u64,
446 misses: u64,
447}
448
449impl S3FifoLayoutCache {
450 #[inline]
452 pub fn new(max_entries: usize) -> Self {
453 Self {
454 cache: ftui_core::s3_fifo::S3Fifo::new(max_entries.max(2)),
455 generation: 0,
456 max_entries: max_entries.max(2),
457 hits: 0,
458 misses: 0,
459 }
460 }
461
462 pub fn get_or_compute<F>(&mut self, key: LayoutCacheKey, compute: F) -> crate::Rects
467 where
468 F: FnOnce() -> crate::Rects,
469 {
470 if let Some(entry) = self.cache.get(&key) {
471 if entry.generation == self.generation {
472 self.hits += 1;
473 return entry.chunks.clone();
474 }
475 self.cache.remove(&key);
477 }
478
479 self.misses += 1;
480 let chunks = compute();
481
482 self.cache.insert(
483 key,
484 CachedLayoutEntry {
485 chunks: chunks.clone(),
486 generation: self.generation,
487 access_count: 1,
488 },
489 );
490
491 chunks
492 }
493
494 #[inline]
496 pub fn invalidate_all(&mut self) {
497 self.generation = self.generation.wrapping_add(1);
498 }
499
500 pub fn stats(&self) -> LayoutCacheStats {
502 let total = self.hits + self.misses;
503 LayoutCacheStats {
504 entries: self.cache.len(),
505 hits: self.hits,
506 misses: self.misses,
507 hit_rate: if total > 0 {
508 self.hits as f64 / total as f64
509 } else {
510 0.0
511 },
512 }
513 }
514
515 #[inline]
517 pub fn reset_stats(&mut self) {
518 self.hits = 0;
519 self.misses = 0;
520 }
521
522 #[inline]
524 pub fn clear(&mut self) {
525 self.cache.clear();
526 self.generation = self.generation.wrapping_add(1);
527 }
528
529 #[inline]
531 pub fn len(&self) -> usize {
532 self.cache.len()
533 }
534
535 #[inline]
537 pub fn is_empty(&self) -> bool {
538 self.cache.is_empty()
539 }
540
541 #[inline]
543 pub fn capacity(&self) -> usize {
544 self.max_entries
545 }
546}
547
548impl Default for S3FifoLayoutCache {
549 fn default() -> Self {
550 Self::new(64)
551 }
552}
553
554#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
565pub struct CoherenceId {
566 pub constraints_hash: u64,
568 pub direction: Direction,
570}
571
572impl CoherenceId {
573 pub fn new(constraints: &[Constraint], direction: Direction) -> Self {
575 Self {
576 constraints_hash: LayoutCacheKey::hash_constraints(constraints).0,
577 direction,
578 }
579 }
580
581 pub fn from_cache_key(key: &LayoutCacheKey) -> Self {
583 Self {
584 constraints_hash: key.constraints_hash,
585 direction: key.direction,
586 }
587 }
588}
589
590#[derive(Debug, Clone)]
618pub struct CoherenceCache {
619 entries: FxHashMap<CoherenceId, CoherenceEntry>,
620 max_entries: usize,
621 tick: u64,
623}
624
625#[derive(Debug, Clone)]
626struct CoherenceEntry {
627 allocation: crate::Sizes,
629 last_stored: u64,
631}
632
633impl CoherenceCache {
634 pub fn new(max_entries: usize) -> Self {
636 Self {
637 entries: FxHashMap::with_capacity_and_hasher(max_entries.min(256), Default::default()),
638 max_entries,
639 tick: 0,
640 }
641 }
642
643 #[inline]
648 pub fn get(&self, id: &CoherenceId) -> Option<crate::Sizes> {
649 self.entries.get(id).map(|e| e.allocation.clone())
650 }
651
652 pub fn store(&mut self, id: CoherenceId, allocation: crate::Sizes) {
656 self.tick = self.tick.wrapping_add(1);
657
658 if self.entries.len() >= self.max_entries && !self.entries.contains_key(&id) {
659 self.evict_oldest();
660 }
661
662 self.entries.insert(
663 id,
664 CoherenceEntry {
665 allocation,
666 last_stored: self.tick,
667 },
668 );
669 }
670
671 #[inline]
673 pub fn clear(&mut self) {
674 self.entries.clear();
675 }
676
677 #[inline]
679 pub fn len(&self) -> usize {
680 self.entries.len()
681 }
682
683 #[inline]
685 pub fn is_empty(&self) -> bool {
686 self.entries.is_empty()
687 }
688
689 pub fn displacement(&self, id: &CoherenceId, new_alloc: &[u16]) -> (u64, u32) {
694 match self.entries.get(id) {
695 Some(entry) => {
696 let prev = &entry.allocation;
697 let len = prev.len().min(new_alloc.len());
698 let mut sum: u64 = 0;
699 let mut max: u32 = 0;
700 for i in 0..len {
701 let d = (new_alloc[i] as i32 - prev[i] as i32).unsigned_abs();
702 sum += u64::from(d);
703 max = max.max(d);
704 }
705 for &v in &prev[len..] {
707 sum += u64::from(v);
708 max = max.max(u32::from(v));
709 }
710 for &v in &new_alloc[len..] {
711 sum += u64::from(v);
712 max = max.max(u32::from(v));
713 }
714 (sum, max)
715 }
716 None => (0, 0),
717 }
718 }
719
720 fn evict_oldest(&mut self) {
721 if let Some(key) = self
722 .entries
723 .iter()
724 .min_by_key(|(_, e)| e.last_stored)
725 .map(|(k, _)| *k)
726 {
727 self.entries.remove(&key);
728 }
729 }
730}
731
732impl Default for CoherenceCache {
733 fn default() -> Self {
734 Self::new(64)
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741
742 fn make_key(width: u16, height: u16) -> LayoutCacheKey {
743 LayoutCacheKey::new(
744 Rect::new(0, 0, width, height),
745 &[Constraint::Percentage(50.0), Constraint::Fill],
746 Direction::Horizontal,
747 None,
748 )
749 }
750
751 fn should_not_call(label: &str) -> crate::Rects {
752 unreachable!("{label}");
753 }
754
755 #[test]
758 fn same_params_produce_same_key() {
759 let k1 = make_key(80, 24);
760 let k2 = make_key(80, 24);
761 assert_eq!(k1, k2);
762 }
763
764 #[test]
765 fn different_area_different_key() {
766 let k1 = make_key(80, 24);
767 let k2 = make_key(120, 40);
768 assert_ne!(k1, k2);
769 }
770
771 #[test]
772 fn different_constraints_different_key() {
773 let k1 = LayoutCacheKey::new(
774 Rect::new(0, 0, 80, 24),
775 &[Constraint::Fixed(20)],
776 Direction::Horizontal,
777 None,
778 );
779 let k2 = LayoutCacheKey::new(
780 Rect::new(0, 0, 80, 24),
781 &[Constraint::Fixed(30)],
782 Direction::Horizontal,
783 None,
784 );
785 assert_ne!(k1, k2);
786 }
787
788 #[test]
789 fn different_direction_different_key() {
790 let k1 = LayoutCacheKey::new(
791 Rect::new(0, 0, 80, 24),
792 &[Constraint::Fill],
793 Direction::Horizontal,
794 None,
795 );
796 let k2 = LayoutCacheKey::new(
797 Rect::new(0, 0, 80, 24),
798 &[Constraint::Fill],
799 Direction::Vertical,
800 None,
801 );
802 assert_ne!(k1, k2);
803 }
804
805 #[test]
806 fn different_intrinsics_different_key() {
807 let hints1 = [LayoutSizeHint {
808 min: 10,
809 preferred: 20,
810 max: None,
811 }];
812 let hints2 = [LayoutSizeHint {
813 min: 10,
814 preferred: 30,
815 max: None,
816 }];
817
818 let k1 = LayoutCacheKey::new(
819 Rect::new(0, 0, 80, 24),
820 &[Constraint::FitContent],
821 Direction::Horizontal,
822 Some(&hints1),
823 );
824 let k2 = LayoutCacheKey::new(
825 Rect::new(0, 0, 80, 24),
826 &[Constraint::FitContent],
827 Direction::Horizontal,
828 Some(&hints2),
829 );
830 assert_ne!(k1, k2);
831 }
832
833 #[test]
836 fn cache_returns_same_result() {
837 let mut cache = LayoutCache::new(100);
838 let key = make_key(80, 24);
839
840 let mut compute_count = 0;
841 let compute = || {
842 compute_count += 1;
843 smallvec::smallvec![Rect::new(0, 0, 40, 24), Rect::new(40, 0, 40, 24)]
844 };
845
846 let r1 = cache.get_or_compute(key, compute);
847 let r2 = cache.get_or_compute(key, || should_not_call("should not call"));
848
849 assert_eq!(r1, r2);
850 assert_eq!(compute_count, 1);
851 }
852
853 #[test]
854 fn different_area_is_cache_miss() {
855 let mut cache = LayoutCache::new(100);
856
857 let mut compute_count = 0;
858 let mut compute = || {
859 compute_count += 1;
860 smallvec::smallvec![Rect::default()]
861 };
862
863 let k1 = make_key(80, 24);
864 let k2 = make_key(120, 40);
865
866 cache.get_or_compute(k1, &mut compute);
867 cache.get_or_compute(k2, &mut compute);
868
869 assert_eq!(compute_count, 2);
870 }
871
872 #[test]
873 fn invalidation_clears_cache() {
874 let mut cache = LayoutCache::new(100);
875 let key = make_key(80, 24);
876
877 let mut compute_count = 0;
878 let mut compute = || {
879 compute_count += 1;
880 smallvec::smallvec![]
881 };
882
883 cache.get_or_compute(key, &mut compute);
884 cache.invalidate_all();
885 cache.get_or_compute(key, &mut compute);
886
887 assert_eq!(compute_count, 2);
888 }
889
890 #[test]
891 fn lru_eviction_works() {
892 let mut cache = LayoutCache::new(2);
893
894 let k1 = make_key(10, 10);
895 let k2 = make_key(20, 20);
896 let k3 = make_key(30, 30);
897
898 cache.get_or_compute(k1, || smallvec::smallvec![Rect::new(0, 0, 10, 10)]);
900 cache.get_or_compute(k2, || smallvec::smallvec![Rect::new(0, 0, 20, 20)]);
901
902 cache.get_or_compute(k1, || should_not_call("k1 should hit"));
904
905 cache.get_or_compute(k3, || smallvec::smallvec![Rect::new(0, 0, 30, 30)]);
907
908 assert_eq!(cache.len(), 2);
909
910 let mut was_called = false;
912 cache.get_or_compute(k2, || {
913 was_called = true;
914 smallvec::smallvec![]
915 });
916 assert!(was_called, "k2 should have been evicted");
917
918 cache.get_or_compute(k1, || should_not_call("k1 should still be cached"));
920 }
921
922 #[test]
923 fn stats_track_hits_and_misses() {
924 let mut cache = LayoutCache::new(100);
925
926 let k1 = make_key(80, 24);
927 let k2 = make_key(120, 40);
928
929 cache.get_or_compute(k1, crate::Rects::new); cache.get_or_compute(k1, || should_not_call("hit")); cache.get_or_compute(k2, crate::Rects::new); let stats = cache.stats();
934 assert_eq!(stats.hits, 1);
935 assert_eq!(stats.misses, 2);
936 assert!((stats.hit_rate - 1.0 / 3.0).abs() < 0.01);
937 }
938
939 #[test]
940 fn reset_stats_clears_counters() {
941 let mut cache = LayoutCache::new(100);
942 let key = make_key(80, 24);
943
944 cache.get_or_compute(key, crate::Rects::new);
945 cache.get_or_compute(key, || should_not_call("hit"));
946
947 let stats = cache.stats();
948 assert_eq!(stats.hits, 1);
949 assert_eq!(stats.misses, 1);
950
951 cache.reset_stats();
952
953 let stats = cache.stats();
954 assert_eq!(stats.hits, 0);
955 assert_eq!(stats.misses, 0);
956 assert_eq!(stats.hit_rate, 0.0);
957 }
958
959 #[test]
960 fn clear_removes_all_entries() {
961 let mut cache = LayoutCache::new(100);
962
963 cache.get_or_compute(make_key(80, 24), crate::Rects::new);
964 cache.get_or_compute(make_key(120, 40), crate::Rects::new);
965
966 assert_eq!(cache.len(), 2);
967
968 cache.clear();
969
970 assert_eq!(cache.len(), 0);
971 assert!(cache.is_empty());
972
973 let mut was_called = false;
975 cache.get_or_compute(make_key(80, 24), || {
976 was_called = true;
977 smallvec::smallvec![]
978 });
979 assert!(was_called);
980 }
981
982 #[test]
983 fn default_capacity_is_64() {
984 let cache = LayoutCache::default();
985 assert_eq!(cache.capacity(), 64);
986 }
987
988 #[test]
989 fn generation_wraps_around() {
990 let mut cache = LayoutCache::new(100);
991 cache.generation = u64::MAX;
992 cache.invalidate_all();
993 assert_eq!(cache.generation, 0);
994 }
995
996 #[test]
999 fn constraint_hash_is_stable() {
1000 let constraints = [
1001 Constraint::Fixed(20),
1002 Constraint::Percentage(50.0),
1003 Constraint::Min(10),
1004 ];
1005
1006 let h1 = LayoutCacheKey::hash_constraints(&constraints);
1007 let h2 = LayoutCacheKey::hash_constraints(&constraints);
1008
1009 assert_eq!(h1, h2);
1010 }
1011
1012 #[test]
1013 fn different_constraint_values_different_hash() {
1014 let c1 = [Constraint::Fixed(20)];
1015 let c2 = [Constraint::Fixed(30)];
1016
1017 let h1 = LayoutCacheKey::hash_constraints(&c1);
1018 let h2 = LayoutCacheKey::hash_constraints(&c2);
1019
1020 assert_ne!(h1, h2);
1021 }
1022
1023 #[test]
1024 fn different_constraint_types_different_hash() {
1025 let c1 = [Constraint::Fixed(20)];
1026 let c2 = [Constraint::Min(20)];
1027
1028 let h1 = LayoutCacheKey::hash_constraints(&c1);
1029 let h2 = LayoutCacheKey::hash_constraints(&c2);
1030
1031 assert_ne!(h1, h2);
1032 }
1033
1034 #[test]
1035 fn fit_content_bounded_values_in_hash() {
1036 let c1 = [Constraint::FitContentBounded { min: 10, max: 50 }];
1037 let c2 = [Constraint::FitContentBounded { min: 10, max: 60 }];
1038
1039 let h1 = LayoutCacheKey::hash_constraints(&c1);
1040 let h2 = LayoutCacheKey::hash_constraints(&c2);
1041
1042 assert_ne!(h1, h2);
1043 }
1044
1045 #[test]
1048 fn intrinsics_hash_is_stable() {
1049 let hints = [
1050 LayoutSizeHint {
1051 min: 10,
1052 preferred: 20,
1053 max: Some(30),
1054 },
1055 LayoutSizeHint {
1056 min: 5,
1057 preferred: 15,
1058 max: None,
1059 },
1060 ];
1061
1062 let h1 = LayoutCacheKey::hash_intrinsics(&hints);
1063 let h2 = LayoutCacheKey::hash_intrinsics(&hints);
1064
1065 assert_eq!(h1, h2);
1066 }
1067
1068 #[test]
1069 fn different_intrinsics_different_hash() {
1070 let h1 = [LayoutSizeHint {
1071 min: 10,
1072 preferred: 20,
1073 max: None,
1074 }];
1075 let h2 = [LayoutSizeHint {
1076 min: 10,
1077 preferred: 25,
1078 max: None,
1079 }];
1080
1081 let hash1 = LayoutCacheKey::hash_intrinsics(&h1);
1082 let hash2 = LayoutCacheKey::hash_intrinsics(&h2);
1083
1084 assert_ne!(hash1, hash2);
1085 }
1086
1087 #[test]
1090 fn cache_is_deterministic() {
1091 let mut cache1 = LayoutCache::new(100);
1092 let mut cache2 = LayoutCache::new(100);
1093
1094 for i in 0..10u16 {
1095 let key = make_key(i * 10, i * 5);
1096 let result = smallvec::smallvec![Rect::new(0, 0, i, i)];
1097
1098 cache1.get_or_compute(key, || result.clone());
1099 cache2.get_or_compute(key, || result);
1100 }
1101
1102 assert_eq!(cache1.stats().entries, cache2.stats().entries);
1103 assert_eq!(cache1.stats().misses, cache2.stats().misses);
1104 }
1105
1106 #[test]
1107 fn hit_count_increments_on_each_access() {
1108 let mut cache = LayoutCache::new(100);
1109 let key = make_key(80, 24);
1110
1111 cache.get_or_compute(key, crate::Rects::new);
1113
1114 for _ in 0..5 {
1116 cache.get_or_compute(key, || should_not_call("should hit"));
1117 }
1118
1119 let stats = cache.stats();
1120 assert_eq!(stats.misses, 1);
1121 assert_eq!(stats.hits, 5);
1122 }
1123
1124 fn make_coherence_id(n: u16) -> CoherenceId {
1129 CoherenceId::new(
1130 &[Constraint::Fixed(n), Constraint::Fill],
1131 Direction::Horizontal,
1132 )
1133 }
1134
1135 #[test]
1136 fn coherence_store_and_get() {
1137 let mut cc = CoherenceCache::new(64);
1138 let id = make_coherence_id(1);
1139
1140 assert!(cc.get(&id).is_none());
1141
1142 cc.store(id, smallvec::smallvec![30u16, 50u16]);
1143 assert_eq!(cc.get(&id), Some(smallvec::smallvec![30u16, 50u16]));
1144 }
1145
1146 #[test]
1147 fn coherence_update_replaces_allocation() {
1148 let mut cc = CoherenceCache::new(64);
1149 let id = make_coherence_id(1);
1150
1151 cc.store(id, smallvec::smallvec![30u16, 50u16]);
1152 cc.store(id, smallvec::smallvec![31u16, 49u16]);
1153
1154 assert_eq!(cc.get(&id), Some(smallvec::smallvec![31u16, 49u16]));
1155 assert_eq!(cc.len(), 1);
1156 }
1157
1158 #[test]
1159 fn coherence_different_ids_are_separate() {
1160 let mut cc = CoherenceCache::new(64);
1161 let id1 = make_coherence_id(1);
1162 let id2 = make_coherence_id(2);
1163
1164 cc.store(id1, smallvec::smallvec![40u16, 40u16]);
1165 cc.store(id2, smallvec::smallvec![30u16, 50u16]);
1166
1167 assert_eq!(cc.get(&id1), Some(smallvec::smallvec![40u16, 40u16]));
1168 assert_eq!(cc.get(&id2), Some(smallvec::smallvec![30u16, 50u16]));
1169 }
1170
1171 #[test]
1172 fn coherence_eviction_at_capacity() {
1173 let mut cc = CoherenceCache::new(2);
1174
1175 let id1 = make_coherence_id(1);
1176 let id2 = make_coherence_id(2);
1177 let id3 = make_coherence_id(3);
1178
1179 cc.store(id1, smallvec::smallvec![10u16]);
1180 cc.store(id2, smallvec::smallvec![20u16]);
1181 cc.store(id3, smallvec::smallvec![30u16]);
1182
1183 assert_eq!(cc.len(), 2);
1184 assert!(cc.get(&id1).is_none());
1186 assert_eq!(cc.get(&id2), Some(smallvec::smallvec![20u16]));
1187 assert_eq!(cc.get(&id3), Some(smallvec::smallvec![30u16]));
1188 }
1189
1190 #[test]
1191 fn coherence_clear() {
1192 let mut cc = CoherenceCache::new(64);
1193 let id = make_coherence_id(1);
1194
1195 cc.store(id, smallvec::smallvec![10u16, 20u16]);
1196 assert_eq!(cc.len(), 1);
1197
1198 cc.clear();
1199 assert!(cc.is_empty());
1200 assert!(cc.get(&id).is_none());
1201 }
1202
1203 #[test]
1204 fn coherence_displacement_with_previous() {
1205 let mut cc = CoherenceCache::new(64);
1206 let id = make_coherence_id(1);
1207
1208 cc.store(id, smallvec::smallvec![30u16, 50u16]);
1209
1210 let (sum, max) = cc.displacement(&id, &[32, 48]);
1212 assert_eq!(sum, 4); assert_eq!(max, 2);
1214 }
1215
1216 #[test]
1217 fn coherence_displacement_without_previous() {
1218 let cc = CoherenceCache::new(64);
1219 let id = make_coherence_id(1);
1220
1221 let (sum, max) = cc.displacement(&id, &[30, 50]);
1222 assert_eq!(sum, 0);
1223 assert_eq!(max, 0);
1224 }
1225
1226 #[test]
1227 fn coherence_displacement_different_lengths() {
1228 let mut cc = CoherenceCache::new(64);
1229 let id = make_coherence_id(1);
1230
1231 cc.store(id, smallvec::smallvec![30u16, 50u16]);
1232
1233 let (sum, max) = cc.displacement(&id, &[30, 50, 10]);
1235 assert_eq!(sum, 10);
1236 assert_eq!(max, 10);
1237 }
1238
1239 #[test]
1240 fn coherence_from_cache_key() {
1241 let key = make_key(80, 24);
1242 let id = CoherenceId::from_cache_key(&key);
1243
1244 let key2 = make_key(120, 40);
1246 let id2 = CoherenceId::from_cache_key(&key2);
1247
1248 assert_eq!(id, id2);
1249 }
1250
1251 #[test]
1254 fn unit_cache_reuse_unchanged_constraints_yield_identical_layout() {
1255 use crate::round_layout_stable;
1256
1257 let mut cc = CoherenceCache::new(64);
1258 let id = make_coherence_id(1);
1259
1260 let targets = [26.67, 26.67, 26.66];
1262 let total = 80;
1263 let alloc1 = round_layout_stable(&targets, total, cc.get(&id));
1264 cc.store(id, alloc1.clone());
1265
1266 let alloc2 = round_layout_stable(&targets, total, cc.get(&id));
1268 assert_eq!(alloc1, alloc2, "Same inputs should yield identical layout");
1269 }
1270
1271 #[test]
1274 fn e2e_resize_sweep_bounded_displacement() {
1275 use crate::round_layout_stable;
1276
1277 let mut cc = CoherenceCache::new(64);
1278 let id = make_coherence_id(1);
1279
1280 let mut max_displacement_ever: u32 = 0;
1283 let mut total_displacement_sum: u64 = 0;
1284 let steps = 61; for width in 60u16..=120 {
1287 let third = f64::from(width) / 3.0;
1288 let targets = [third, third, third];
1289
1290 let prev = cc.get(&id);
1291 let alloc = round_layout_stable(&targets, width, prev);
1292
1293 let (d_sum, d_max) = cc.displacement(&id, &alloc);
1294 total_displacement_sum += d_sum;
1295 max_displacement_ever = max_displacement_ever.max(d_max);
1296
1297 cc.store(id, alloc);
1298 }
1299
1300 assert!(
1303 max_displacement_ever <= 2,
1304 "Max single-step displacement should be <=2 cells, got {}",
1305 max_displacement_ever
1306 );
1307
1308 let avg = total_displacement_sum as f64 / steps as f64;
1310 assert!(
1311 avg < 3.0,
1312 "Average displacement per step should be <3 cells, got {:.2}",
1313 avg
1314 );
1315 }
1316
1317 #[test]
1318 fn e2e_resize_sweep_deterministic() {
1319 use crate::round_layout_stable;
1320
1321 let sweep = |seed: u16| -> Vec<(u16, crate::Sizes, u64, u32)> {
1323 let mut cc = CoherenceCache::new(64);
1324 let id = CoherenceId::new(
1325 &[Constraint::Percentage(30.0), Constraint::Fill],
1326 Direction::Horizontal,
1327 );
1328
1329 let mut log = Vec::new();
1330 for width in (40 + seed)..(100 + seed) {
1331 let targets = [f64::from(width) * 0.3, f64::from(width) * 0.7];
1332 let prev = cc.get(&id);
1333 let alloc = round_layout_stable(&targets, width, prev);
1334 let (d_sum, d_max) = cc.displacement(&id, &alloc);
1335 cc.store(id, alloc.clone());
1336 log.push((width, alloc, d_sum, d_max));
1337 }
1338 log
1339 };
1340
1341 let log1 = sweep(0);
1342 let log2 = sweep(0);
1343 assert_eq!(log1, log2, "Identical sweeps should produce identical logs");
1344 }
1345
1346 #[test]
1347 fn default_coherence_cache_capacity_is_64() {
1348 let cc = CoherenceCache::default();
1349 assert_eq!(cc.max_entries, 64);
1350 }
1351
1352 fn s3_fifo_test_key(x: u16, w: u16) -> LayoutCacheKey {
1355 LayoutCacheKey {
1356 area_x: x,
1357 area_y: 0,
1358 area_width: w,
1359 area_height: 24,
1360 constraints_hash: 42,
1361 constraints_hash_fx: 42,
1362 constraints_len: 2,
1363 direction: Direction::Horizontal,
1364 intrinsics_hash: None,
1365 intrinsics_hash_fx: None,
1366 }
1367 }
1368
1369 #[test]
1370 fn s3fifo_layout_new_is_empty() {
1371 let cache = S3FifoLayoutCache::new(64);
1372 assert!(cache.is_empty());
1373 assert_eq!(cache.len(), 0);
1374 assert_eq!(cache.capacity(), 64);
1375 }
1376
1377 #[test]
1378 fn s3fifo_layout_default_capacity() {
1379 let cache = S3FifoLayoutCache::default();
1380 assert_eq!(cache.capacity(), 64);
1381 }
1382
1383 #[test]
1384 fn s3fifo_layout_get_or_compute_caches() {
1385 let mut cache = S3FifoLayoutCache::new(64);
1386 let key = s3_fifo_test_key(0, 80);
1387 let rects1 = cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
1388 let rects2 = cache.get_or_compute(key, || panic!("should not recompute"));
1389 assert_eq!(rects1, rects2);
1390 let stats = cache.stats();
1391 assert_eq!(stats.hits, 1);
1392 assert_eq!(stats.misses, 1);
1393 }
1394
1395 #[test]
1396 fn s3fifo_layout_generation_invalidation() {
1397 let mut cache = S3FifoLayoutCache::new(64);
1398 let key = s3_fifo_test_key(0, 80);
1399 cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
1400
1401 cache.invalidate_all();
1402
1403 let rects = cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 80, 24)]);
1405 assert_eq!(rects.as_slice(), &[Rect::new(0, 0, 80, 24)]);
1406 let stats = cache.stats();
1407 assert_eq!(stats.misses, 2);
1408 }
1409
1410 #[test]
1411 fn s3fifo_layout_clear() {
1412 let mut cache = S3FifoLayoutCache::new(64);
1413 let key = s3_fifo_test_key(0, 80);
1414 cache.get_or_compute(key, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
1415 cache.clear();
1416 assert!(cache.is_empty());
1417 }
1418
1419 #[test]
1420 fn s3fifo_layout_different_keys() {
1421 let mut cache = S3FifoLayoutCache::new(64);
1422 let k1 = s3_fifo_test_key(0, 80);
1423 let k2 = s3_fifo_test_key(0, 120);
1424 cache.get_or_compute(k1, || smallvec::smallvec![Rect::new(0, 0, 40, 24)]);
1425 cache.get_or_compute(k2, || smallvec::smallvec![Rect::new(0, 0, 60, 24)]);
1426 assert_eq!(cache.len(), 2);
1427 }
1428
1429 #[test]
1430 fn s3fifo_layout_reset_stats() {
1431 let mut cache = S3FifoLayoutCache::new(64);
1432 let key = s3_fifo_test_key(0, 80);
1433 cache.get_or_compute(key, crate::Rects::new);
1434 cache.get_or_compute(key, crate::Rects::new);
1435 cache.reset_stats();
1436 let stats = cache.stats();
1437 assert_eq!(stats.hits, 0);
1438 assert_eq!(stats.misses, 0);
1439 }
1440
1441 #[test]
1442 fn s3fifo_layout_produces_same_results_as_lru() {
1443 let mut lru = LayoutCache::new(64);
1444 let mut s3 = S3FifoLayoutCache::new(64);
1445
1446 for w in [80, 100, 120, 160, 200] {
1447 let key = s3_fifo_test_key(0, w);
1448 let expected = smallvec::smallvec![Rect::new(0, 0, w / 2, 24)];
1449 let lru_r = lru.get_or_compute(key, || expected.clone());
1450 let s3_r = s3.get_or_compute(key, || expected.clone());
1451 assert_eq!(lru_r, s3_r, "mismatch for width={w}");
1452 }
1453 }
1454}