1use std::fmt;
30use std::time::Duration;
31
32use crate::layout_policy::LayoutTier;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct FrameBudget {
44 pub total_us: u64,
46 pub layout_us: u64,
48 pub shaping_us: u64,
50 pub diff_us: u64,
52 pub present_us: u64,
54 pub headroom_us: u64,
56}
57
58impl FrameBudget {
59 #[must_use]
63 pub const fn from_fps(fps: u32) -> u64 {
64 1_000_000 / fps as u64
65 }
66
67 #[must_use]
69 pub const fn as_duration(&self) -> Duration {
70 Duration::from_micros(self.total_us)
71 }
72
73 #[must_use]
75 pub const fn allocated(&self) -> u64 {
76 self.layout_us + self.shaping_us + self.diff_us + self.present_us + self.headroom_us
77 }
78
79 #[must_use]
81 pub const fn is_consistent(&self) -> bool {
82 self.allocated() == self.total_us
83 }
84}
85
86impl fmt::Display for FrameBudget {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 write!(
89 f,
90 "{}µs (layout={}µs shaping={}µs diff={}µs present={}µs headroom={}µs)",
91 self.total_us,
92 self.layout_us,
93 self.shaping_us,
94 self.diff_us,
95 self.present_us,
96 self.headroom_us
97 )
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct MemoryBudget {
112 pub shaping_bytes: usize,
114 pub layout_bytes: usize,
116 pub diff_bytes: usize,
118 pub width_cache_entries: usize,
120 pub shaping_cache_entries: usize,
122}
123
124impl MemoryBudget {
125 #[must_use]
127 pub const fn transient_total(&self) -> usize {
128 self.shaping_bytes + self.layout_bytes + self.diff_bytes
129 }
130}
131
132impl fmt::Display for MemoryBudget {
133 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 write!(
135 f,
136 "transient={}B (shaping={}B layout={}B diff={}B) caches: width={} shaping={}",
137 self.transient_total(),
138 self.shaping_bytes,
139 self.layout_bytes,
140 self.diff_bytes,
141 self.width_cache_entries,
142 self.shaping_cache_entries
143 )
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct QueueBudget {
157 pub max_reshape_pending: usize,
159 pub max_rewrap_pending: usize,
161 pub max_reflow_pending: usize,
163}
164
165impl QueueBudget {
166 #[must_use]
168 pub const fn total_max(&self) -> usize {
169 self.max_reshape_pending + self.max_rewrap_pending + self.max_reflow_pending
170 }
171}
172
173impl fmt::Display for QueueBudget {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 write!(
176 f,
177 "reshape={} rewrap={} reflow={}",
178 self.max_reshape_pending, self.max_rewrap_pending, self.max_reflow_pending
179 )
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct TierBudget {
190 pub tier: LayoutTier,
192 pub frame: FrameBudget,
194 pub memory: MemoryBudget,
196 pub queue: QueueBudget,
198}
199
200impl fmt::Display for TierBudget {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(
203 f,
204 "[{}] frame: {} | mem: {} | queue: {}",
205 self.tier, self.frame, self.memory, self.queue
206 )
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct TierFeatures {
221 pub tier: LayoutTier,
223
224 pub shaped_text: bool,
227 pub terminal_fallback: bool,
229
230 pub optimal_breaking: bool,
233 pub hyphenation: bool,
235
236 pub justification: bool,
239 pub tracking: bool,
241
242 pub baseline_grid: bool,
245 pub paragraph_spacing: bool,
247 pub first_line_indent: bool,
249
250 pub width_cache: bool,
253 pub shaping_cache: bool,
255
256 pub incremental_diff: bool,
259 pub subcell_spacing: bool,
261}
262
263impl TierFeatures {
264 #[must_use]
266 pub fn active_list(&self) -> Vec<&'static str> {
267 let mut out = Vec::new();
268 if self.shaped_text {
269 out.push("shaped-text");
270 }
271 if self.terminal_fallback {
272 out.push("terminal-fallback");
273 }
274 if self.optimal_breaking {
275 out.push("optimal-breaking");
276 }
277 if self.hyphenation {
278 out.push("hyphenation");
279 }
280 if self.justification {
281 out.push("justification");
282 }
283 if self.tracking {
284 out.push("tracking");
285 }
286 if self.baseline_grid {
287 out.push("baseline-grid");
288 }
289 if self.paragraph_spacing {
290 out.push("paragraph-spacing");
291 }
292 if self.first_line_indent {
293 out.push("first-line-indent");
294 }
295 if self.width_cache {
296 out.push("width-cache");
297 }
298 if self.shaping_cache {
299 out.push("shaping-cache");
300 }
301 if self.incremental_diff {
302 out.push("incremental-diff");
303 }
304 if self.subcell_spacing {
305 out.push("subcell-spacing");
306 }
307 out
308 }
309}
310
311impl fmt::Display for TierFeatures {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 write!(f, "[{}] {}", self.tier, self.active_list().join(", "))
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
327pub enum SafetyInvariant {
328 NoContentLoss,
331 WideCharWidth,
334 BufferSizeMatch,
337 CursorInBounds,
339 StyleBoundary,
342 DiffIdempotence,
345 GreedyWrapFallback,
348 WidthDeterminism,
350}
351
352impl SafetyInvariant {
353 pub const ALL: &'static [Self] = &[
355 Self::NoContentLoss,
356 Self::WideCharWidth,
357 Self::BufferSizeMatch,
358 Self::CursorInBounds,
359 Self::StyleBoundary,
360 Self::DiffIdempotence,
361 Self::GreedyWrapFallback,
362 Self::WidthDeterminism,
363 ];
364}
365
366impl fmt::Display for SafetyInvariant {
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 match self {
369 Self::NoContentLoss => write!(f, "no-content-loss"),
370 Self::WideCharWidth => write!(f, "wide-char-width"),
371 Self::BufferSizeMatch => write!(f, "buffer-size-match"),
372 Self::CursorInBounds => write!(f, "cursor-in-bounds"),
373 Self::StyleBoundary => write!(f, "style-boundary"),
374 Self::DiffIdempotence => write!(f, "diff-idempotence"),
375 Self::GreedyWrapFallback => write!(f, "greedy-wrap-fallback"),
376 Self::WidthDeterminism => write!(f, "width-determinism"),
377 }
378 }
379}
380
381#[derive(Debug, Clone)]
391pub struct TierLadder {
392 pub budgets: [TierBudget; 4],
394 pub features: [TierFeatures; 4],
396}
397
398impl TierLadder {
399 #[must_use]
401 pub fn budget(&self, tier: LayoutTier) -> &TierBudget {
402 &self.budgets[tier as usize]
403 }
404
405 #[must_use]
407 pub fn features_for(&self, tier: LayoutTier) -> &TierFeatures {
408 &self.features[tier as usize]
409 }
410
411 #[must_use]
418 pub fn default_60fps() -> Self {
419 Self {
420 budgets: [
421 TierBudget {
423 tier: LayoutTier::Emergency,
424 frame: FrameBudget {
425 total_us: 2_000,
426 layout_us: 100,
427 shaping_us: 200,
428 diff_us: 200,
429 present_us: 500,
430 headroom_us: 1_000,
431 },
432 memory: MemoryBudget {
433 shaping_bytes: 64 * 1024, layout_bytes: 16 * 1024, diff_bytes: 32 * 1024, width_cache_entries: 256,
437 shaping_cache_entries: 0, },
439 queue: QueueBudget {
440 max_reshape_pending: 0, max_rewrap_pending: 4, max_reflow_pending: 1,
443 },
444 },
445 TierBudget {
447 tier: LayoutTier::Fast,
448 frame: FrameBudget {
449 total_us: 4_000,
450 layout_us: 200,
451 shaping_us: 800,
452 diff_us: 500,
453 present_us: 1_000,
454 headroom_us: 1_500,
455 },
456 memory: MemoryBudget {
457 shaping_bytes: 256 * 1024, layout_bytes: 64 * 1024, diff_bytes: 128 * 1024, width_cache_entries: 1_000,
461 shaping_cache_entries: 0, },
463 queue: QueueBudget {
464 max_reshape_pending: 0, max_rewrap_pending: 16,
466 max_reflow_pending: 4,
467 },
468 },
469 TierBudget {
471 tier: LayoutTier::Balanced,
472 frame: FrameBudget {
473 total_us: 8_000,
474 layout_us: 500,
475 shaping_us: 2_500,
476 diff_us: 1_000,
477 present_us: 1_500,
478 headroom_us: 2_500,
479 },
480 memory: MemoryBudget {
481 shaping_bytes: 1024 * 1024, layout_bytes: 256 * 1024, diff_bytes: 512 * 1024, width_cache_entries: 4_000,
485 shaping_cache_entries: 512,
486 },
487 queue: QueueBudget {
488 max_reshape_pending: 32,
489 max_rewrap_pending: 64,
490 max_reflow_pending: 16,
491 },
492 },
493 TierBudget {
495 tier: LayoutTier::Quality,
496 frame: FrameBudget {
497 total_us: 16_000,
498 layout_us: 1_000,
499 shaping_us: 5_000,
500 diff_us: 2_000,
501 present_us: 3_000,
502 headroom_us: 5_000,
503 },
504 memory: MemoryBudget {
505 shaping_bytes: 4 * 1024 * 1024, layout_bytes: 1024 * 1024, diff_bytes: 2 * 1024 * 1024, width_cache_entries: 16_000,
509 shaping_cache_entries: 2_048,
510 },
511 queue: QueueBudget {
512 max_reshape_pending: 128,
513 max_rewrap_pending: 256,
514 max_reflow_pending: 64,
515 },
516 },
517 ],
518 features: [
519 TierFeatures {
521 tier: LayoutTier::Emergency,
522 shaped_text: false,
523 terminal_fallback: true,
524 optimal_breaking: false,
525 hyphenation: false,
526 justification: false,
527 tracking: false,
528 baseline_grid: false,
529 paragraph_spacing: false,
530 first_line_indent: false,
531 width_cache: true, shaping_cache: false,
533 incremental_diff: true, subcell_spacing: false,
535 },
536 TierFeatures {
538 tier: LayoutTier::Fast,
539 shaped_text: false,
540 terminal_fallback: true,
541 optimal_breaking: false,
542 hyphenation: false,
543 justification: false,
544 tracking: false,
545 baseline_grid: false,
546 paragraph_spacing: false,
547 first_line_indent: false,
548 width_cache: true,
549 shaping_cache: false,
550 incremental_diff: true,
551 subcell_spacing: false,
552 },
553 TierFeatures {
555 tier: LayoutTier::Balanced,
556 shaped_text: true,
557 terminal_fallback: true,
558 optimal_breaking: true,
559 hyphenation: false,
560 justification: false,
561 tracking: false,
562 baseline_grid: false,
563 paragraph_spacing: true,
564 first_line_indent: false,
565 width_cache: true,
566 shaping_cache: true,
567 incremental_diff: true,
568 subcell_spacing: true,
569 },
570 TierFeatures {
572 tier: LayoutTier::Quality,
573 shaped_text: true,
574 terminal_fallback: true,
575 optimal_breaking: true,
576 hyphenation: true,
577 justification: true,
578 tracking: true,
579 baseline_grid: true,
580 paragraph_spacing: true,
581 first_line_indent: true,
582 width_cache: true,
583 shaping_cache: true,
584 incremental_diff: true,
585 subcell_spacing: true,
586 },
587 ],
588 }
589 }
590
591 #[must_use]
596 pub fn check_monotonicity(&self) -> Vec<String> {
597 let mut violations = Vec::new();
598
599 for i in 0..self.features.len() - 1 {
600 let lower = &self.features[i];
601 let higher = &self.features[i + 1];
602
603 let check = |name: &str, lo: bool, hi: bool| {
604 if lo && !hi {
605 Some(format!(
606 "{name} is enabled at {} but disabled at {}",
607 lower.tier, higher.tier
608 ))
609 } else {
610 None
611 }
612 };
613
614 violations.extend(check(
615 "terminal_fallback",
616 lower.terminal_fallback,
617 higher.terminal_fallback,
618 ));
619 violations.extend(check("width_cache", lower.width_cache, higher.width_cache));
620 violations.extend(check(
621 "incremental_diff",
622 lower.incremental_diff,
623 higher.incremental_diff,
624 ));
625 violations.extend(check("shaped_text", lower.shaped_text, higher.shaped_text));
626 violations.extend(check(
627 "optimal_breaking",
628 lower.optimal_breaking,
629 higher.optimal_breaking,
630 ));
631 violations.extend(check("hyphenation", lower.hyphenation, higher.hyphenation));
632 violations.extend(check(
633 "justification",
634 lower.justification,
635 higher.justification,
636 ));
637 violations.extend(check("tracking", lower.tracking, higher.tracking));
638 violations.extend(check(
639 "baseline_grid",
640 lower.baseline_grid,
641 higher.baseline_grid,
642 ));
643 violations.extend(check(
644 "paragraph_spacing",
645 lower.paragraph_spacing,
646 higher.paragraph_spacing,
647 ));
648 violations.extend(check(
649 "first_line_indent",
650 lower.first_line_indent,
651 higher.first_line_indent,
652 ));
653 violations.extend(check(
654 "shaping_cache",
655 lower.shaping_cache,
656 higher.shaping_cache,
657 ));
658 violations.extend(check(
659 "subcell_spacing",
660 lower.subcell_spacing,
661 higher.subcell_spacing,
662 ));
663 }
664 violations
665 }
666
667 #[must_use]
669 pub fn check_budget_consistency(&self) -> Vec<String> {
670 let mut issues = Vec::new();
671 for b in &self.budgets {
672 if !b.frame.is_consistent() {
673 issues.push(format!(
674 "[{}] frame sub-budgets sum to {}µs but total is {}µs",
675 b.tier,
676 b.frame.allocated(),
677 b.frame.total_us
678 ));
679 }
680 }
681 issues
682 }
683
684 #[must_use]
686 pub fn check_budget_ordering(&self) -> Vec<String> {
687 let mut issues = Vec::new();
688 for i in 0..self.budgets.len() - 1 {
689 let lower = &self.budgets[i];
690 let higher = &self.budgets[i + 1];
691 if lower.frame.total_us >= higher.frame.total_us {
692 issues.push(format!(
693 "frame budget {} ({}µs) >= {} ({}µs)",
694 lower.tier, lower.frame.total_us, higher.tier, higher.frame.total_us
695 ));
696 }
697 }
698 issues
699 }
700}
701
702impl Default for TierLadder {
703 fn default() -> Self {
704 Self::default_60fps()
705 }
706}
707
708impl fmt::Display for TierLadder {
709 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710 for b in &self.budgets {
711 writeln!(f, "{b}")?;
712 }
713 writeln!(f)?;
714 for feat in &self.features {
715 writeln!(f, "{feat}")?;
716 }
717 Ok(())
718 }
719}
720
721#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
732 fn fps_60_is_16667us() {
733 assert_eq!(FrameBudget::from_fps(60), 16_666);
734 }
735
736 #[test]
737 fn fps_30_is_33333us() {
738 assert_eq!(FrameBudget::from_fps(30), 33_333);
739 }
740
741 #[test]
742 fn frame_budget_as_duration() {
743 let fb = FrameBudget {
744 total_us: 16_000,
745 layout_us: 1_000,
746 shaping_us: 5_000,
747 diff_us: 2_000,
748 present_us: 3_000,
749 headroom_us: 5_000,
750 };
751 assert_eq!(fb.as_duration(), Duration::from_micros(16_000));
752 }
753
754 #[test]
755 fn frame_budget_consistency() {
756 let fb = FrameBudget {
757 total_us: 10_000,
758 layout_us: 1_000,
759 shaping_us: 2_000,
760 diff_us: 2_000,
761 present_us: 2_000,
762 headroom_us: 3_000,
763 };
764 assert!(fb.is_consistent());
765 }
766
767 #[test]
768 fn frame_budget_inconsistency() {
769 let fb = FrameBudget {
770 total_us: 10_000,
771 layout_us: 1_000,
772 shaping_us: 2_000,
773 diff_us: 2_000,
774 present_us: 2_000,
775 headroom_us: 999, };
777 assert!(!fb.is_consistent());
778 }
779
780 #[test]
781 fn frame_budget_display() {
782 let fb = FrameBudget {
783 total_us: 4_000,
784 layout_us: 200,
785 shaping_us: 800,
786 diff_us: 500,
787 present_us: 1_000,
788 headroom_us: 1_500,
789 };
790 let s = format!("{fb}");
791 assert!(s.contains("4000µs"));
792 assert!(s.contains("layout=200µs"));
793 }
794
795 #[test]
798 fn memory_transient_total() {
799 let mb = MemoryBudget {
800 shaping_bytes: 1024,
801 layout_bytes: 512,
802 diff_bytes: 256,
803 width_cache_entries: 100,
804 shaping_cache_entries: 50,
805 };
806 assert_eq!(mb.transient_total(), 1792);
807 }
808
809 #[test]
810 fn memory_budget_display() {
811 let mb = MemoryBudget {
812 shaping_bytes: 1024,
813 layout_bytes: 512,
814 diff_bytes: 256,
815 width_cache_entries: 100,
816 shaping_cache_entries: 50,
817 };
818 let s = format!("{mb}");
819 assert!(s.contains("transient=1792B"));
820 }
821
822 #[test]
825 fn queue_total_max() {
826 let qb = QueueBudget {
827 max_reshape_pending: 10,
828 max_rewrap_pending: 20,
829 max_reflow_pending: 5,
830 };
831 assert_eq!(qb.total_max(), 35);
832 }
833
834 #[test]
835 fn queue_budget_display() {
836 let qb = QueueBudget {
837 max_reshape_pending: 10,
838 max_rewrap_pending: 20,
839 max_reflow_pending: 5,
840 };
841 let s = format!("{qb}");
842 assert!(s.contains("reshape=10"));
843 }
844
845 #[test]
848 fn tier_budget_display() {
849 let ladder = TierLadder::default_60fps();
850 let s = format!("{}", ladder.budget(LayoutTier::Fast));
851 assert!(s.contains("[fast]"));
852 assert!(s.contains("4000µs"));
853 }
854
855 #[test]
858 fn emergency_features_minimal() {
859 let ladder = TierLadder::default_60fps();
860 let f = ladder.features_for(LayoutTier::Emergency);
861 assert!(!f.shaped_text);
862 assert!(!f.optimal_breaking);
863 assert!(!f.justification);
864 assert!(!f.hyphenation);
865 assert!(f.terminal_fallback);
866 assert!(f.width_cache);
867 assert!(f.incremental_diff);
868 }
869
870 #[test]
871 fn fast_features() {
872 let ladder = TierLadder::default_60fps();
873 let f = ladder.features_for(LayoutTier::Fast);
874 assert!(!f.shaped_text);
875 assert!(!f.optimal_breaking);
876 assert!(f.terminal_fallback);
877 assert!(f.width_cache);
878 }
879
880 #[test]
881 fn balanced_features() {
882 let ladder = TierLadder::default_60fps();
883 let f = ladder.features_for(LayoutTier::Balanced);
884 assert!(f.shaped_text);
885 assert!(f.optimal_breaking);
886 assert!(f.shaping_cache);
887 assert!(f.subcell_spacing);
888 assert!(!f.hyphenation);
889 assert!(!f.justification);
890 }
891
892 #[test]
893 fn quality_features_all_on() {
894 let ladder = TierLadder::default_60fps();
895 let f = ladder.features_for(LayoutTier::Quality);
896 assert!(f.shaped_text);
897 assert!(f.optimal_breaking);
898 assert!(f.hyphenation);
899 assert!(f.justification);
900 assert!(f.tracking);
901 assert!(f.baseline_grid);
902 assert!(f.first_line_indent);
903 }
904
905 #[test]
906 fn feature_active_list() {
907 let ladder = TierLadder::default_60fps();
908 let list = ladder.features_for(LayoutTier::Emergency).active_list();
909 assert!(list.contains(&"terminal-fallback"));
910 assert!(list.contains(&"width-cache"));
911 assert!(list.contains(&"incremental-diff"));
912 assert_eq!(list.len(), 3);
913 }
914
915 #[test]
916 fn features_display() {
917 let ladder = TierLadder::default_60fps();
918 let s = format!("{}", ladder.features_for(LayoutTier::Quality));
919 assert!(s.contains("[quality]"));
920 assert!(s.contains("justification"));
921 }
922
923 #[test]
926 fn default_ladder_budgets_are_consistent() {
927 let ladder = TierLadder::default_60fps();
928 let issues = ladder.check_budget_consistency();
929 assert!(issues.is_empty(), "Budget inconsistencies: {issues:?}");
930 }
931
932 #[test]
933 fn default_ladder_budgets_monotonically_increase() {
934 let ladder = TierLadder::default_60fps();
935 let issues = ladder.check_budget_ordering();
936 assert!(issues.is_empty(), "Budget ordering violations: {issues:?}");
937 }
938
939 #[test]
940 fn default_ladder_features_are_monotonic() {
941 let ladder = TierLadder::default_60fps();
942 let violations = ladder.check_monotonicity();
943 assert!(
944 violations.is_empty(),
945 "Feature monotonicity violations: {violations:?}"
946 );
947 }
948
949 #[test]
950 fn ladder_budget_lookup() {
951 let ladder = TierLadder::default_60fps();
952 assert_eq!(ladder.budget(LayoutTier::Emergency).frame.total_us, 2_000);
953 assert_eq!(ladder.budget(LayoutTier::Fast).frame.total_us, 4_000);
954 assert_eq!(ladder.budget(LayoutTier::Balanced).frame.total_us, 8_000);
955 assert_eq!(ladder.budget(LayoutTier::Quality).frame.total_us, 16_000);
956 }
957
958 #[test]
959 fn ladder_display() {
960 let ladder = TierLadder::default_60fps();
961 let s = format!("{ladder}");
962 assert!(s.contains("[emergency]"));
963 assert!(s.contains("[fast]"));
964 assert!(s.contains("[balanced]"));
965 assert!(s.contains("[quality]"));
966 }
967
968 #[test]
969 fn default_trait() {
970 let ladder = TierLadder::default();
971 assert_eq!(ladder.budget(LayoutTier::Fast).frame.total_us, 4_000);
972 }
973
974 #[test]
977 fn all_invariants_listed() {
978 assert_eq!(SafetyInvariant::ALL.len(), 8);
979 }
980
981 #[test]
982 fn invariant_display() {
983 assert_eq!(
984 format!("{}", SafetyInvariant::NoContentLoss),
985 "no-content-loss"
986 );
987 assert_eq!(
988 format!("{}", SafetyInvariant::WideCharWidth),
989 "wide-char-width"
990 );
991 assert_eq!(
992 format!("{}", SafetyInvariant::GreedyWrapFallback),
993 "greedy-wrap-fallback"
994 );
995 }
996
997 #[test]
998 fn invariants_cover_key_concerns() {
999 let all = SafetyInvariant::ALL;
1000 assert!(all.contains(&SafetyInvariant::NoContentLoss));
1001 assert!(all.contains(&SafetyInvariant::WideCharWidth));
1002 assert!(all.contains(&SafetyInvariant::BufferSizeMatch));
1003 assert!(all.contains(&SafetyInvariant::DiffIdempotence));
1004 assert!(all.contains(&SafetyInvariant::WidthDeterminism));
1005 }
1006
1007 #[test]
1010 fn all_budgets_within_60fps() {
1011 let frame_budget_60fps = FrameBudget::from_fps(60);
1012 let ladder = TierLadder::default_60fps();
1013 for b in &ladder.budgets {
1014 assert!(
1015 b.frame.total_us <= frame_budget_60fps,
1016 "{} budget {}µs exceeds 60fps frame ({}µs)",
1017 b.tier,
1018 b.frame.total_us,
1019 frame_budget_60fps
1020 );
1021 }
1022 }
1023
1024 #[test]
1025 fn emergency_queue_disables_reshape() {
1026 let ladder = TierLadder::default_60fps();
1027 assert_eq!(
1028 ladder
1029 .budget(LayoutTier::Emergency)
1030 .queue
1031 .max_reshape_pending,
1032 0
1033 );
1034 }
1035
1036 #[test]
1037 fn quality_has_largest_caches() {
1038 let ladder = TierLadder::default_60fps();
1039 let e = &ladder.budget(LayoutTier::Emergency).memory;
1040 let q = &ladder.budget(LayoutTier::Quality).memory;
1041 assert!(q.width_cache_entries > e.width_cache_entries);
1042 assert!(q.shaping_cache_entries > e.shaping_cache_entries);
1043 }
1044}