1use crate::geometry::{
23 CommandRecording, Point, RecordKind, Rect, SolidArcRecord, SolidRoundRectRecord, TapeRef,
24};
25use crate::{Color, CornerRadii};
26
27const REL_EPS: f32 = 2e-3;
29const ABS_EPS: f32 = 2e-2;
31const GROUP_SCALE_EPS: f32 = 1e-4;
36const GROUP_ANGLE_EPS: f32 = 2e-4;
37
38fn close_rel(a: f32, b: f32) -> bool {
39 (a - b).abs() <= ABS_EPS + REL_EPS * a.abs().max(b.abs())
40}
41
42fn close_angle(a: f32, b: f32) -> bool {
43 use std::f32::consts::TAU;
44 let d = a - b;
52 if d.abs() < TAU {
53 let wrapped = if d > TAU * 0.5 {
54 d - TAU
55 } else if d < -TAU * 0.5 {
56 d + TAU
57 } else {
58 d
59 };
60 return wrapped.abs() <= ABS_EPS;
61 }
62 let mut d = d % TAU;
63 if d > TAU * 0.5 {
64 d -= TAU;
65 }
66 if d < -TAU * 0.5 {
67 d += TAU;
68 }
69 d.abs() <= ABS_EPS
70}
71
72#[cfg(test)]
73mod close_angle_equivalence {
74 use super::*;
75
76 fn close_angle_reference(a: f32, b: f32) -> bool {
77 use std::f32::consts::TAU;
78 let mut d = (a - b) % TAU;
79 if d > TAU * 0.5 {
80 d -= TAU;
81 }
82 if d < -TAU * 0.5 {
83 d += TAU;
84 }
85 d.abs() <= ABS_EPS
86 }
87
88 #[test]
93 fn fast_path_matches_the_fmod_form() {
94 use std::f32::consts::{PI, TAU};
95 let interesting = [
96 0.0_f32,
97 -0.0,
98 1e-8,
99 -1e-8,
100 0.019,
101 -0.019,
102 0.021,
103 -0.021,
104 1.0,
105 -1.0,
106 PI - 1e-3,
107 PI,
108 PI + 1e-3,
109 -PI,
110 TAU - 0.02,
111 TAU - 1e-6,
112 TAU,
113 TAU + 1e-6,
114 TAU + 0.019,
115 -TAU,
116 -TAU - 0.019,
117 3.0 * TAU + 0.01,
118 -7.5 * TAU,
119 123.456,
120 -987.654,
121 f32::NAN,
122 f32::INFINITY,
123 f32::NEG_INFINITY,
124 f32::MAX,
125 ];
126 for &a in &interesting {
127 for &b in &interesting {
128 assert_eq!(
129 close_angle(a, b),
130 close_angle_reference(a, b),
131 "close_angle({a}, {b}) diverged from the fmod form"
132 );
133 }
134 }
135 for anchor in [-500.0_f32, -6.0, 0.0, 6.0, 500.0] {
137 for i in -2520..=2520 {
138 let d = i as f32 * 0.01;
139 assert_eq!(
140 close_angle(anchor + d, anchor),
141 close_angle_reference(anchor + d, anchor),
142 "sweep diverged at anchor {anchor} delta {d}"
143 );
144 }
145 }
146 }
147}
148
149fn close_point(a: Point, b: Point) -> bool {
150 close_rel(a.x, b.x) & close_rel(a.y, b.y)
155}
156
157#[derive(Clone, Copy, Debug, PartialEq)]
160pub struct RecordTransform {
161 pub scale: f32,
162 pub angle: f32,
163}
164
165#[inline(always)]
173fn apply_parts(scale: f32, sin: f32, cos: f32, center: Point, p: Point) -> Point {
174 let dx = p.x - center.x;
175 let dy = p.y - center.y;
176 Point::new(
177 center.x + (dx * cos - dy * sin) * scale,
178 center.y + (dx * sin + dy * cos) * scale,
179 )
180}
181
182impl RecordTransform {
183 pub const IDENTITY: Self = Self {
184 scale: 1.0,
185 angle: 0.0,
186 };
187
188 pub fn apply(&self, center: Point, p: Point) -> Point {
189 let (sin, cos) = self.angle.sin_cos();
190 apply_parts(self.scale, sin, cos, center, p)
191 }
192
193 pub fn apply_to_bounds(&self, center: Point, bounds: Rect) -> Rect {
198 let corners = [
199 Point::new(bounds.x, bounds.y),
200 Point::new(bounds.x + bounds.width, bounds.y),
201 Point::new(bounds.x, bounds.y + bounds.height),
202 Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
203 ];
204 let mut min_x = f32::INFINITY;
205 let mut min_y = f32::INFINITY;
206 let mut max_x = f32::NEG_INFINITY;
207 let mut max_y = f32::NEG_INFINITY;
208 for corner in corners {
209 let p = self.apply(center, corner);
210 min_x = min_x.min(p.x);
211 min_y = min_y.min(p.y);
212 max_x = max_x.max(p.x);
213 max_y = max_y.max(p.y);
214 }
215 Rect {
216 x: min_x,
217 y: min_y,
218 width: max_x - min_x,
219 height: max_y - min_y,
220 }
221 }
222}
223
224pub fn transforms_group(
228 entry: RecordTransform,
229 entry_pinned: bool,
230 anchor: RecordTransform,
231) -> bool {
232 use std::f32::consts::TAU;
233 if (entry.scale - anchor.scale).abs() > GROUP_SCALE_EPS * anchor.scale.abs().max(1.0) {
234 return false;
235 }
236 if !entry_pinned {
237 return true;
238 }
239 let mut d = (entry.angle - anchor.angle) % TAU;
240 if d > TAU * 0.5 {
241 d -= TAU;
242 }
243 if d < -TAU * 0.5 {
244 d += TAU;
245 }
246 d.abs() <= GROUP_ANGLE_EPS
247}
248
249#[derive(Clone, Copy, Debug, PartialEq, Eq)]
252pub enum RecordMatch {
253 Exact,
254 Recolor,
257 Mismatch,
258}
259
260pub fn circle_view(record: &SolidRoundRectRecord) -> Option<(Point, f32)> {
264 if !is_circle(record.rect, record.radii) {
265 return None;
266 }
267 Some((
268 Point::new(
269 record.rect.x + record.rect.width * 0.5,
270 record.rect.y + record.rect.height * 0.5,
271 ),
272 record.rect.width,
273 ))
274}
275
276pub fn is_circle(rect: Rect, radii: CornerRadii) -> bool {
278 let half = rect.width * 0.5;
279 close_rel(rect.width, rect.height)
282 & close_rel(radii.top_left, half)
283 & close_rel(radii.top_right, half)
284 & close_rel(radii.bottom_right, half)
285 & close_rel(radii.bottom_left, half)
286}
287
288fn stroke_width(record_stroke: Option<crate::Stroke>) -> Option<f32> {
289 record_stroke.map(|stroke| stroke.width)
290}
291
292pub fn arcs_anchor_compatible(current: &SolidArcRecord, anchor: &SolidArcRecord) -> bool {
297 close_rel(current.sweep_angle, anchor.sweep_angle)
298 && current.stroke.is_some() == anchor.stroke.is_some()
299}
300
301pub fn arc_anchor_transform(
304 current: &SolidArcRecord,
305 retained: &SolidArcRecord,
306) -> Option<RecordTransform> {
307 if retained.radius <= f32::EPSILON {
308 return None;
309 }
310 Some(RecordTransform {
311 scale: current.radius / retained.radius,
312 angle: current.start_angle - retained.start_angle,
313 })
314}
315
316pub fn circle_anchor_transform_pinned(
319 current: (Point, f32),
320 retained: (Point, f32),
321 center: Point,
322) -> Option<(RecordTransform, bool)> {
323 let (c_now, d_now) = current;
324 let (c_then, d_then) = retained;
325 if d_then <= f32::EPSILON {
326 return None;
327 }
328 let scale = d_now / d_then;
329 let dx_then = c_then.x - center.x;
330 let dy_then = c_then.y - center.y;
331 let pinned = dx_then * dx_then + dy_then * dy_then > 1.0;
332 let angle = if pinned {
333 let dx_now = c_now.x - center.x;
334 let dy_now = c_now.y - center.y;
335 dy_now.atan2(dx_now) - dy_then.atan2(dx_then)
336 } else {
337 0.0
338 };
339 Some((RecordTransform { scale, angle }, pinned))
340}
341
342pub fn match_arc(
357 current: &SolidArcRecord,
358 retained: &SolidArcRecord,
359 center: Point,
360 t: RecordTransform,
361) -> RecordMatch {
362 let stroke_ok = match (stroke_width(current.stroke), stroke_width(retained.stroke)) {
363 (None, None) => true,
364 (Some(now), Some(then)) => close_rel(now, then * t.scale),
365 _ => false,
366 };
367 let geometry_ok = close_point(current.center, retained.center)
368 & close_point(current.center, center)
369 & close_rel(current.radius, retained.radius * t.scale)
370 & close_rel(current.inner_radius, retained.inner_radius * t.scale)
371 & close_angle(current.start_angle, retained.start_angle + t.angle)
372 & close_rel(current.sweep_angle, retained.sweep_angle)
373 & stroke_ok;
374 if !geometry_ok {
375 return RecordMatch::Mismatch;
376 }
377 if current.color == retained.color {
378 RecordMatch::Exact
379 } else {
380 RecordMatch::Recolor
381 }
382}
383
384pub fn match_round_rect(
395 current: &SolidRoundRectRecord,
396 retained: &SolidRoundRectRecord,
397 center: Point,
398 t: RecordTransform,
399) -> RecordMatch {
400 let (Some((c_now, d_now)), Some((c_then, d_then))) =
401 (circle_view(current), circle_view(retained))
402 else {
403 return RecordMatch::Mismatch;
404 };
405 let stroke_ok = match (stroke_width(current.stroke), stroke_width(retained.stroke)) {
406 (None, None) => true,
407 (Some(now), Some(then)) => close_rel(now, then * t.scale),
408 _ => false,
409 };
410 let geometry_ok = close_point(c_now, t.apply(center, c_then))
411 & close_rel(d_now, d_then * t.scale)
412 & stroke_ok;
413 if !geometry_ok {
414 return RecordMatch::Mismatch;
415 }
416 if current.color == retained.color {
417 RecordMatch::Exact
418 } else {
419 RecordMatch::Recolor
420 }
421}
422
423pub const MIN_SEGMENT_RECORDS: usize = 128;
426pub const MAX_SEGMENT_RECORDS: usize = 2048;
429pub const MIN_REPLAY_COMMAND_RECORDS: usize = 512;
431const RESYNC_SPAN: usize = 48;
434const MAX_RESYNC_EVENTS: usize = 512;
435const RESYNC_WINDOW: usize = 1024;
438const ANCHOR_PROBE_RECORDS: usize = 4;
441const MAX_COMMIT_ATTEMPTS: usize = 4;
447const MIN_COVERAGE_FRACTION: f32 = 0.5;
450const RECAPTURE_EROSION: f32 = 0.05;
454const RECAPTURE_COOLDOWN_FRAMES: u32 = 180;
458
459#[derive(Clone, Copy, Debug, PartialEq, Eq)]
464enum ReplayView {
465 Arc(usize),
466 RoundRect(usize),
467}
468
469fn view_at_slices(
475 tape: &[TapeRef],
476 round_rects: &[SolidRoundRectRecord],
477 i: usize,
478) -> Option<ReplayView> {
479 let entry = tape[i];
480 match entry.kind() {
481 RecordKind::SolidArc => Some(ReplayView::Arc(entry.index())),
482 RecordKind::SolidRoundRect => circle_view(&round_rects[entry.index()])
485 .is_some()
486 .then_some(ReplayView::RoundRect(entry.index())),
487 RecordKind::SolidRect | RecordKind::Other => None,
488 }
489}
490
491fn view_at(recording: &CommandRecording, i: usize) -> Option<ReplayView> {
493 view_at_slices(&recording.tape, &recording.round_rects, i)
494}
495
496fn detect_center(recording: &CommandRecording) -> Option<Point> {
498 recording.arcs.first().map(|arc| arc.center)
499}
500
501fn views_compatible(
504 current: &CommandRecording,
505 current_view: Option<ReplayView>,
506 retained: &CommandRecording,
507 retained_view: Option<ReplayView>,
508) -> bool {
509 match (current_view, retained_view) {
510 (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
511 arcs_anchor_compatible(¤t.arcs[i], &retained.arcs[j])
512 }
513 (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
514 let now = current.round_rects[i].stroke.is_some();
515 let then = retained.round_rects[j].stroke.is_some();
516 now == then
517 }
518 (None, None) => true,
519 _ => false,
520 }
521}
522
523fn align_recordings(
531 current: &CommandRecording,
532 retained: &CommandRecording,
533 aligned: &mut Vec<Option<usize>>,
534) {
535 let pair = |i: usize, j: usize| -> bool {
536 views_compatible(current, view_at(current, i), retained, view_at(retained, j))
537 };
538 let current_len = current.tape.len();
539 let retained_len = retained.tape.len();
540 aligned.clear();
541 aligned.resize(current_len, None);
542 let (mut i, mut j) = (0usize, 0usize);
543 let mut events = 0usize;
544 while i < current_len && j < retained_len {
545 if pair(i, j) {
546 aligned[i] = Some(j);
547 i += 1;
548 j += 1;
549 continue;
550 }
551 events += 1;
552 if events > MAX_RESYNC_EVENTS {
553 aligned.fill(None);
556 return;
557 }
558 let mut resynced = false;
559 'search: for total in 1..=RESYNC_SPAN {
560 for di in 0..=total {
561 let dj = total - di;
562 if i + di < current_len && j + dj < retained_len && pair(i + di, j + dj) {
563 i += di;
564 j += dj;
565 resynced = true;
566 break 'search;
567 }
568 }
569 }
570 if !resynced {
571 i += 1;
572 j += 1;
573 }
574 }
575}
576
577fn pair_transform(
579 current: &CommandRecording,
580 current_view: ReplayView,
581 retained: &CommandRecording,
582 retained_view: ReplayView,
583 center: Point,
584) -> Option<(RecordTransform, bool)> {
585 match (current_view, retained_view) {
586 (ReplayView::Arc(i), ReplayView::Arc(j)) => {
587 arc_anchor_transform(¤t.arcs[i], &retained.arcs[j]).map(|t| (t, true))
588 }
589 (ReplayView::RoundRect(i), ReplayView::RoundRect(j)) => {
590 let now = circle_view(¤t.round_rects[i])?;
591 let then = circle_view(&retained.round_rects[j])?;
592 circle_anchor_transform_pinned(now, then, center)
593 }
594 _ => None,
595 }
596}
597
598fn match_pair(
600 current: &CommandRecording,
601 current_view: ReplayView,
602 retained: &CommandRecording,
603 retained_view: ReplayView,
604 center: Point,
605 t: RecordTransform,
606) -> RecordMatch {
607 match (current_view, retained_view) {
608 (ReplayView::Arc(i), ReplayView::Arc(j)) => {
609 match_arc(¤t.arcs[i], &retained.arcs[j], center, t)
610 }
611 (ReplayView::RoundRect(i), ReplayView::RoundRect(j)) => {
612 match_round_rect(¤t.round_rects[i], &retained.round_rects[j], center, t)
613 }
614 _ => RecordMatch::Mismatch,
615 }
616}
617
618fn range_bounds(recording: &CommandRecording, range: (usize, usize)) -> Rect {
621 let mut min_x = f32::INFINITY;
622 let mut min_y = f32::INFINITY;
623 let mut max_x = f32::NEG_INFINITY;
624 let mut max_y = f32::NEG_INFINITY;
625 for view in (range.0..range.1).filter_map(|i| view_at(recording, i)) {
626 let (center, reach) = match view {
627 ReplayView::Arc(i) => {
628 let arc = &recording.arcs[i];
629 (
630 arc.center,
631 arc.radius + arc.stroke.map(|stroke| stroke.width).unwrap_or(0.0),
632 )
633 }
634 ReplayView::RoundRect(i) => {
635 let record = &recording.round_rects[i];
636 let Some((center, diameter)) = circle_view(record) else {
637 continue;
638 };
639 (
640 center,
641 diameter * 0.5 + record.stroke.map(|stroke| stroke.width).unwrap_or(0.0),
642 )
643 }
644 };
645 let reach = reach + 2.0;
646 min_x = min_x.min(center.x - reach);
647 min_y = min_y.min(center.y - reach);
648 max_x = max_x.max(center.x + reach);
649 max_y = max_y.max(center.y + reach);
650 }
651 if min_x > max_x {
652 return Rect {
653 x: 0.0,
654 y: 0.0,
655 width: 0.0,
656 height: 0.0,
657 };
658 }
659 Rect {
660 x: min_x,
661 y: min_y,
662 width: max_x - min_x,
663 height: max_y - min_y,
664 }
665}
666
667#[derive(Clone, Debug, PartialEq)]
672pub struct CommandSegment {
673 pub slot: u32,
679 pub slot_offset: usize,
681 pub tape_start: usize,
682 pub tape_end: usize,
683 pub bounds: Rect,
685 pub prev_recolors: Vec<u32>,
692}
693
694#[derive(Clone, Debug, PartialEq)]
696pub enum ReplaySpan {
697 Retained {
703 slot: u32,
705 capture: bool,
710 slot_offset: usize,
712 tape_start: usize,
714 tape_end: usize,
715 transform: RecordTransform,
716 recolors: Vec<(u32, Color)>,
717 bounds: Rect,
719 },
720 Dynamic { tape_start: usize, tape_end: usize },
722}
723
724#[derive(Debug, PartialEq)]
726pub enum ReplayOutcome {
727 AllDynamic,
729 Spans(Vec<ReplaySpan>),
732}
733
734pub trait VerifyExecutor: Sync {
740 fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync));
741}
742
743#[derive(Clone, Debug)]
747pub struct CommandReplayFrame {
748 pub center: Point,
750 pub spans: Vec<FrameSpan>,
752 pub fallback: Option<std::rc::Rc<crate::geometry::CommandRecording>>,
763}
764
765impl PartialEq for CommandReplayFrame {
766 fn eq(&self, other: &Self) -> bool {
767 self.center == other.center
768 && self.spans == other.spans
769 && match (&self.fallback, &other.fallback) {
770 (None, None) => true,
771 (Some(a), Some(b)) => std::rc::Rc::ptr_eq(a, b),
772 _ => false,
773 }
774 }
775}
776
777#[derive(Clone, Debug, PartialEq)]
779pub enum FrameSpan {
780 Retained {
781 slot: u32,
784 capture: bool,
788 slot_offset: u32,
790 range: (u32, u32),
795 tape_range: (u32, u32),
798 transform: RecordTransform,
799 recolors: Vec<(u32, Color)>,
801 bounds: Rect,
803 },
804 Dynamic {
805 range: (u32, u32),
807 },
808}
809
810#[derive(Clone, Copy, Debug, PartialEq, Eq)]
811enum CommandReplayPhase {
812 Idle,
813 Snapshotted,
814 Captured,
815}
816
817#[derive(Debug, Default)]
821struct SpanResultSlot {
822 matched: usize,
823 recolors: Vec<(u32, Color)>,
824}
825
826#[derive(Debug)]
831pub struct CommandReplayState {
832 phase: CommandReplayPhase,
833 center: Point,
834 snapshot: CommandRecording,
835 segments: Vec<CommandSegment>,
836 next_slot_id: u32,
837 lifetime_deaths: u64,
838 lifetime_splits: u64,
839 capture_coverage: f32,
844 frames_since_capture: u32,
845 optimistic_commits: u64,
848 prefix_commits: u64,
853 verify_results: Vec<std::sync::Mutex<SpanResultSlot>>,
863 recolor_scratch: Vec<(u32, Color)>,
868 best_recolor_scratch: Vec<(u32, Color)>,
871 verify_pending: std::collections::VecDeque<CommandSegment>,
874 verify_survivors: Vec<CommandSegment>,
875 align_scratch: Vec<Option<usize>>,
879 collapsed_from_captured: bool,
888}
889
890impl Default for CommandReplayState {
891 fn default() -> Self {
892 Self {
893 phase: CommandReplayPhase::Idle,
894 center: Point::new(0.0, 0.0),
895 snapshot: CommandRecording::default(),
896 segments: Vec::new(),
897 next_slot_id: 0,
898 lifetime_deaths: 0,
899 lifetime_splits: 0,
900 capture_coverage: 0.0,
901 frames_since_capture: 0,
902 optimistic_commits: 0,
903 prefix_commits: 0,
904 verify_results: Vec::new(),
905 recolor_scratch: Vec::new(),
906 best_recolor_scratch: Vec::new(),
907 verify_pending: std::collections::VecDeque::new(),
908 verify_survivors: Vec::new(),
909 align_scratch: Vec::new(),
910 collapsed_from_captured: false,
911 }
912 }
913}
914
915impl CommandReplayState {
916 pub fn segments(&self) -> &[CommandSegment] {
917 &self.segments
918 }
919
920 pub fn stats(&self) -> (u64, u64) {
923 (self.lifetime_deaths, self.lifetime_splits)
924 }
925
926 pub fn optimistic_commits(&self) -> u64 {
928 self.optimistic_commits
929 }
930
931 pub fn prefix_commits(&self) -> u64 {
935 self.prefix_commits
936 }
937
938 pub fn center(&self) -> Point {
940 self.center
941 }
942
943 pub fn collapsed_from_captured(&self) -> bool {
950 self.collapsed_from_captured
951 }
952
953 pub fn advance(&mut self, current: &CommandRecording) -> ReplayOutcome {
961 self.advance_pooled(current, None)
962 }
963
964 pub fn advance_pooled(
974 &mut self,
975 current: &CommandRecording,
976 pool: Option<&dyn VerifyExecutor>,
977 ) -> ReplayOutcome {
978 self.collapsed_from_captured = false;
979 if current.tape.len() < MIN_REPLAY_COMMAND_RECORDS {
980 self.retire();
981 return ReplayOutcome::AllDynamic;
982 }
983 let Some(center) = detect_center(current) else {
984 self.retire();
985 return ReplayOutcome::AllDynamic;
986 };
987 match self.phase {
988 CommandReplayPhase::Idle => {
989 self.take_snapshot(current, center);
990 ReplayOutcome::AllDynamic
991 }
992 CommandReplayPhase::Snapshotted => self.partition(current, center),
993 CommandReplayPhase::Captured => self.verify(current, pool),
994 }
995 }
996
997 fn retire(&mut self) {
998 self.phase = CommandReplayPhase::Idle;
999 self.snapshot = CommandRecording::default();
1000 self.segments.clear();
1001 }
1002
1003 fn take_snapshot(&mut self, current: &CommandRecording, center: Point) {
1004 self.snapshot.clone_records_from(current);
1008 self.center = center;
1009 self.segments.clear();
1010 self.phase = CommandReplayPhase::Snapshotted;
1011 }
1012
1013 fn partition(&mut self, current: &CommandRecording, center: Point) -> ReplayOutcome {
1021 align_recordings(current, &self.snapshot, &mut self.align_scratch);
1022 let mut chains: Vec<(usize, usize)> = Vec::new();
1023 let mut i = 0;
1024 while i < current.tape.len() {
1025 let (Some(view), Some(snapshot_view)) = (
1026 view_at(current, i),
1027 self.align_scratch[i].and_then(|j| view_at(&self.snapshot, j)),
1028 ) else {
1029 i += 1;
1030 continue;
1031 };
1032 let Some((t, true)) =
1034 pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
1035 else {
1036 i += 1;
1037 continue;
1038 };
1039 if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
1040 == RecordMatch::Mismatch
1041 {
1042 i += 1;
1043 continue;
1044 }
1045 let start = i;
1046 let mut end = i + 1;
1047 while end < current.tape.len() {
1048 let (Some(view), Some(snapshot_view)) = (
1049 view_at(current, end),
1050 self.align_scratch[end].and_then(|j| view_at(&self.snapshot, j)),
1051 ) else {
1052 break;
1053 };
1054 let Some((entry_t, pinned)) =
1055 pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
1056 else {
1057 break;
1058 };
1059 if !transforms_group(entry_t, pinned, t) {
1060 break;
1061 }
1062 if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
1063 == RecordMatch::Mismatch
1064 {
1065 break;
1066 }
1067 end += 1;
1068 }
1069 if end - start >= MIN_SEGMENT_RECORDS {
1070 let mut piece_start = start;
1071 while piece_start < end {
1072 let piece_end = (piece_start + MAX_SEGMENT_RECORDS).min(end);
1073 if piece_end - piece_start >= MIN_SEGMENT_RECORDS {
1074 chains.push((piece_start, piece_end));
1075 }
1076 piece_start = piece_end;
1077 }
1078 }
1079 i = end.max(i + 1);
1080 }
1081
1082 if chains.is_empty() {
1083 self.take_snapshot(current, center);
1084 return ReplayOutcome::AllDynamic;
1085 }
1086 self.take_snapshot(current, center);
1089 self.segments = chains
1090 .into_iter()
1091 .map(|range| {
1092 let slot = self.next_slot_id;
1093 self.next_slot_id += 1;
1094 CommandSegment {
1095 slot,
1096 slot_offset: 0,
1097 tape_start: range.0,
1098 tape_end: range.1,
1099 bounds: range_bounds(&self.snapshot, range),
1100 prev_recolors: Vec::new(),
1102 }
1103 })
1104 .collect();
1105 let covered: usize = self
1106 .segments
1107 .iter()
1108 .map(|segment| segment.tape_end - segment.tape_start)
1109 .sum();
1110 self.capture_coverage = covered as f32 / current.tape.len().max(1) as f32;
1111 self.frames_since_capture = 0;
1112 self.phase = CommandReplayPhase::Captured;
1113
1114 let mut spans: Vec<ReplaySpan> = Vec::with_capacity(self.segments.len() * 2 + 1);
1115 let mut cursor = 0usize;
1116 for segment in &self.segments {
1117 if segment.tape_start > cursor {
1118 spans.push(ReplaySpan::Dynamic {
1119 tape_start: cursor,
1120 tape_end: segment.tape_start,
1121 });
1122 }
1123 spans.push(ReplaySpan::Retained {
1124 slot: segment.slot,
1125 capture: true,
1126 slot_offset: 0,
1127 tape_start: segment.tape_start,
1128 tape_end: segment.tape_end,
1129 transform: RecordTransform::IDENTITY,
1130 recolors: Vec::new(),
1131 bounds: segment.bounds,
1132 });
1133 cursor = segment.tape_end;
1134 }
1135 if cursor < current.tape.len() {
1136 spans.push(ReplaySpan::Dynamic {
1137 tape_start: cursor,
1138 tape_end: current.tape.len(),
1139 });
1140 }
1141 ReplayOutcome::Spans(spans)
1142 }
1143
1144 fn verify(
1155 &mut self,
1156 current: &CommandRecording,
1157 pool: Option<&dyn VerifyExecutor>,
1158 ) -> ReplayOutcome {
1159 let mut spans: Vec<ReplaySpan> = Vec::new();
1160 let mut retained_records = 0usize;
1161 let mut cursor = 0usize;
1163 let mut committed = 0usize;
1166 if let Some(pool) = pool {
1167 if self.segments.len() >= 2 {
1168 let commit = self.verify_optimistic(current, pool);
1169 if commit.committed == self.segments.len() {
1170 self.optimistic_commits += 1;
1171 return self.finish_verify(current, commit.spans, commit.retained_records);
1172 }
1173 if commit.committed > 0 {
1180 self.prefix_commits += 1;
1181 }
1182 spans = commit.spans;
1183 retained_records = commit.retained_records;
1184 cursor = commit.cursor;
1185 committed = commit.committed;
1186 }
1187 }
1188 self.verify_pending.clear();
1193 self.verify_pending.extend(self.segments.drain(committed..));
1194 self.verify_survivors.clear();
1195 self.verify_survivors.append(&mut self.segments);
1198 while let Some(segment) = self.verify_pending.pop_front() {
1199 let len = segment.tape_end - segment.tape_start;
1200 let search_end = (cursor + RESYNC_WINDOW)
1201 .min(current.tape.len().saturating_sub(len - 1))
1202 .max(cursor);
1203 let candidates = cursor..search_end;
1212 let mut located: Option<(usize, RecordTransform)> = None;
1213 let mut best_prefix: Option<(usize, RecordTransform)> = None;
1219 let mut best_prefix_len = 0usize;
1220 let mut attempts = 0usize;
1221 'search: for start in candidates {
1222 let Some(t) = probe_anchor(
1223 current,
1224 &self.snapshot,
1225 self.center,
1226 segment.tape_start,
1227 len,
1228 start,
1229 ) else {
1230 continue;
1231 };
1232 let matched = match_span(
1236 TypedRecords::from(current),
1237 TypedRecords::from(&self.snapshot),
1238 self.center,
1239 start,
1240 segment.tape_start,
1241 len,
1242 t,
1243 &mut self.recolor_scratch,
1244 );
1245 if matched < len {
1246 if matched > best_prefix_len {
1247 best_prefix_len = matched;
1248 best_prefix = Some((start, t));
1249 std::mem::swap(&mut self.recolor_scratch, &mut self.best_recolor_scratch);
1252 }
1253 if matched >= MIN_SEGMENT_RECORDS {
1262 attempts += 1;
1263 if attempts >= MAX_COMMIT_ATTEMPTS {
1264 break 'search;
1265 }
1266 }
1267 continue;
1268 }
1269 located = Some((start, t));
1270 break;
1271 }
1272 let (span_start, t, mut recolors, span_len) = match located {
1283 Some((start, t)) => (start, t, std::mem::take(&mut self.recolor_scratch), len),
1284 None => {
1285 let split = best_prefix_len >= MIN_SEGMENT_RECORDS;
1286 let Some((start, t)) = best_prefix.filter(|_| split) else {
1287 self.lifetime_deaths += 1;
1288 continue;
1289 };
1290 let suffix_start = segment.tape_start + best_prefix_len + 1;
1291 if segment.tape_end > suffix_start
1292 && segment.tape_end - suffix_start >= MIN_SEGMENT_RECORDS
1293 {
1294 let rebase = (best_prefix_len + 1) as u32;
1301 let cut = segment.prev_recolors.partition_point(|&p| p < rebase);
1302 self.verify_pending.push_front(CommandSegment {
1303 slot: segment.slot,
1304 slot_offset: segment.slot_offset + (suffix_start - segment.tape_start),
1305 tape_start: suffix_start,
1306 tape_end: segment.tape_end,
1307 bounds: range_bounds(&self.snapshot, (suffix_start, segment.tape_end)),
1308 prev_recolors: segment.prev_recolors[cut..]
1309 .iter()
1310 .map(|&p| p - rebase)
1311 .collect(),
1312 });
1313 }
1314 self.lifetime_splits += 1;
1315 (
1316 start,
1317 t,
1318 std::mem::take(&mut self.best_recolor_scratch),
1319 best_prefix_len,
1320 )
1321 }
1322 };
1323 let mut survivor = if span_len == len {
1324 segment
1325 } else {
1326 CommandSegment {
1331 slot: segment.slot,
1332 slot_offset: segment.slot_offset,
1333 tape_start: segment.tape_start,
1334 tape_end: segment.tape_start + span_len,
1335 bounds: range_bounds(
1336 &self.snapshot,
1337 (segment.tape_start, segment.tape_start + span_len),
1338 ),
1339 prev_recolors: segment.prev_recolors,
1340 }
1341 };
1342 merge_color_restores(
1343 &self.snapshot,
1344 survivor.tape_start,
1345 span_len,
1346 &mut survivor.prev_recolors,
1347 &mut recolors,
1348 );
1349 if span_start > cursor {
1350 spans.push(ReplaySpan::Dynamic {
1351 tape_start: cursor,
1352 tape_end: span_start,
1353 });
1354 }
1355 retained_records += span_len;
1356 spans.push(ReplaySpan::Retained {
1357 slot: survivor.slot,
1358 capture: false,
1359 slot_offset: survivor.slot_offset,
1360 tape_start: span_start,
1361 tape_end: span_start + span_len,
1362 transform: t,
1363 recolors,
1364 bounds: t.apply_to_bounds(self.center, survivor.bounds),
1365 });
1366 cursor = span_start + span_len;
1367 self.verify_survivors.push(survivor);
1368 }
1369 if cursor < current.tape.len() {
1370 spans.push(ReplaySpan::Dynamic {
1371 tape_start: cursor,
1372 tape_end: current.tape.len(),
1373 });
1374 }
1375
1376 std::mem::swap(&mut self.segments, &mut self.verify_survivors);
1379 self.finish_verify(current, spans, retained_records)
1380 }
1381
1382 fn finish_verify(
1385 &mut self,
1386 current: &CommandRecording,
1387 spans: Vec<ReplaySpan>,
1388 retained_records: usize,
1389 ) -> ReplayOutcome {
1390 self.frames_since_capture += 1;
1391 let retained_total: usize = self
1392 .segments
1393 .iter()
1394 .map(|segment| segment.tape_end - segment.tape_start)
1395 .sum();
1396 let coverage = retained_total as f32 / current.tape.len().max(1) as f32;
1397 let collapsed = retained_records == 0 || coverage < MIN_COVERAGE_FRACTION;
1398 let eroded = coverage + RECAPTURE_EROSION < self.capture_coverage
1399 && self.frames_since_capture >= RECAPTURE_COOLDOWN_FRAMES;
1400 self.collapsed_from_captured = collapsed;
1404 if collapsed || eroded {
1405 let center = self.center;
1408 self.take_snapshot(current, center);
1409 if retained_records == 0 {
1410 return ReplayOutcome::AllDynamic;
1411 }
1412 }
1413 ReplayOutcome::Spans(spans)
1414 }
1415
1416 fn verify_optimistic(
1439 &mut self,
1440 current: &CommandRecording,
1441 pool: &dyn VerifyExecutor,
1442 ) -> PooledCommit {
1443 struct SpanJob {
1444 start: usize,
1445 seg_start: usize,
1446 len: usize,
1447 t: RecordTransform,
1448 }
1449 let mut jobs: Vec<SpanJob> = Vec::with_capacity(self.segments.len());
1450 let mut cursor = 0usize;
1451 for segment in &self.segments {
1452 let len = segment.tape_end - segment.tape_start;
1453 let search_end = (cursor + RESYNC_WINDOW)
1454 .min(current.tape.len().saturating_sub(len - 1))
1455 .max(cursor);
1456 let mut found = None;
1457 for start in cursor..search_end {
1458 if let Some(t) = probe_anchor(
1459 current,
1460 &self.snapshot,
1461 self.center,
1462 segment.tape_start,
1463 len,
1464 start,
1465 ) {
1466 found = Some((start, t));
1467 break;
1468 }
1469 }
1470 let Some((start, t)) = found else {
1475 break;
1476 };
1477 jobs.push(SpanJob {
1478 start,
1479 seg_start: segment.tape_start,
1480 len,
1481 t,
1482 });
1483 cursor = start + len;
1484 }
1485 if jobs.is_empty() {
1486 return PooledCommit {
1487 spans: Vec::new(),
1488 retained_records: 0,
1489 committed: 0,
1490 cursor: 0,
1491 };
1492 }
1493 if self.verify_results.len() < jobs.len() {
1497 self.verify_results
1498 .resize_with(jobs.len(), Default::default);
1499 }
1500 {
1501 let current = TypedRecords::from(current);
1502 let snapshot = TypedRecords::from(&self.snapshot);
1503 let center = self.center;
1504 let jobs = &jobs;
1505 let results = &self.verify_results;
1506 pool.for_each(jobs.len(), &|i| {
1507 let job = &jobs[i];
1508 let mut guard = results[i].lock().expect("verify span job lock");
1509 let slot = &mut *guard;
1510 slot.matched = match_span(
1511 current,
1512 snapshot,
1513 center,
1514 job.start,
1515 job.seg_start,
1516 job.len,
1517 job.t,
1518 &mut slot.recolors,
1519 );
1520 });
1521 }
1522 let mut committed = jobs.len();
1526 for (i, (job, result)) in jobs.iter().zip(&self.verify_results).enumerate() {
1527 if result.lock().expect("verify span job lock").matched < job.len {
1528 committed = i;
1529 break;
1530 }
1531 }
1532 let mut spans: Vec<ReplaySpan> = Vec::with_capacity(committed * 2 + 1);
1533 let mut retained_records = 0usize;
1534 let mut cursor = 0usize;
1535 for (segment, (job, result)) in self
1536 .segments
1537 .iter_mut()
1538 .zip(jobs.iter().zip(&self.verify_results))
1539 .take(committed)
1540 {
1541 let mut recolors =
1545 std::mem::take(&mut result.lock().expect("verify span job lock").recolors);
1546 merge_color_restores(
1550 &self.snapshot,
1551 segment.tape_start,
1552 job.len,
1553 &mut segment.prev_recolors,
1554 &mut recolors,
1555 );
1556 if job.start > cursor {
1557 spans.push(ReplaySpan::Dynamic {
1558 tape_start: cursor,
1559 tape_end: job.start,
1560 });
1561 }
1562 retained_records += job.len;
1563 spans.push(ReplaySpan::Retained {
1564 slot: segment.slot,
1565 capture: false,
1566 slot_offset: segment.slot_offset,
1567 tape_start: job.start,
1568 tape_end: job.start + job.len,
1569 transform: job.t,
1570 recolors,
1571 bounds: job.t.apply_to_bounds(self.center, segment.bounds),
1572 });
1573 cursor = job.start + job.len;
1574 }
1575 if committed == self.segments.len() && cursor < current.tape.len() {
1578 spans.push(ReplaySpan::Dynamic {
1579 tape_start: cursor,
1580 tape_end: current.tape.len(),
1581 });
1582 }
1583 PooledCommit {
1584 spans,
1585 retained_records,
1586 committed,
1587 cursor,
1588 }
1589 }
1590}
1591
1592struct PooledCommit {
1599 spans: Vec<ReplaySpan>,
1600 retained_records: usize,
1601 committed: usize,
1603 cursor: usize,
1605}
1606
1607fn snapshot_record_color(snapshot: &CommandRecording, i: usize) -> Option<Color> {
1611 match view_at(snapshot, i)? {
1612 ReplayView::Arc(index) => Some(snapshot.arcs[index].color),
1613 ReplayView::RoundRect(index) => Some(snapshot.round_rects[index].color),
1614 }
1615}
1616
1617fn merge_color_restores(
1631 snapshot: &CommandRecording,
1632 seg_tape_start: usize,
1633 span_len: usize,
1634 prev: &mut Vec<u32>,
1635 recolors: &mut Vec<(u32, Color)>,
1636) {
1637 let patched = recolors.len();
1638 let mut cursor = 0usize;
1639 for &offset in prev.iter() {
1640 if offset as usize >= span_len {
1641 break;
1642 }
1643 while cursor < patched && recolors[cursor].0 < offset {
1644 cursor += 1;
1645 }
1646 if cursor < patched && recolors[cursor].0 == offset {
1647 continue;
1648 }
1649 if let Some(color) = snapshot_record_color(snapshot, seg_tape_start + offset as usize) {
1650 recolors.push((offset, color));
1651 }
1652 }
1653 prev.clear();
1654 prev.extend(recolors[..patched].iter().map(|&(offset, _)| offset));
1655}
1656
1657fn probe_anchor(
1662 current: &CommandRecording,
1663 snapshot: &CommandRecording,
1664 center: Point,
1665 seg_start: usize,
1666 len: usize,
1667 start: usize,
1668) -> Option<RecordTransform> {
1669 let (Some(view), Some(snapshot_view)) = (view_at(current, start), view_at(snapshot, seg_start))
1670 else {
1671 return None;
1672 };
1673 if !views_compatible(current, Some(view), snapshot, Some(snapshot_view)) {
1674 return None;
1675 }
1676 let (t, _) = pair_transform(current, view, snapshot, snapshot_view, center)?;
1677 for probe in 0..ANCHOR_PROBE_RECORDS.min(len) {
1678 let (Some(view), Some(snapshot_view)) = (
1679 view_at(current, start + probe),
1680 view_at(snapshot, seg_start + probe),
1681 ) else {
1682 return None;
1683 };
1684 if match_pair(current, view, snapshot, snapshot_view, center, t) == RecordMatch::Mismatch {
1685 return None;
1686 }
1687 }
1688 Some(t)
1689}
1690
1691#[derive(Clone, Copy)]
1696struct TypedRecords<'a> {
1697 tape: &'a [TapeRef],
1698 arcs: &'a [SolidArcRecord],
1699 round_rects: &'a [SolidRoundRectRecord],
1700}
1701
1702impl<'a> From<&'a CommandRecording> for TypedRecords<'a> {
1703 fn from(recording: &'a CommandRecording) -> Self {
1704 Self {
1705 tape: &recording.tape,
1706 arcs: &recording.arcs,
1707 round_rects: &recording.round_rects,
1708 }
1709 }
1710}
1711
1712impl TypedRecords<'_> {
1713 #[cfg(test)]
1719 fn view_at(&self, i: usize) -> Option<ReplayView> {
1720 view_at_slices(self.tape, self.round_rects, i)
1721 }
1722}
1723
1724fn typed_run_len(tape: &[TapeRef], at: usize) -> usize {
1737 let rest = &tape[at..];
1738 let base = rest[0].raw() as u64;
1739 let mut lo = 1usize;
1742 let mut hi = rest.len();
1743 while lo < hi {
1744 let mid = lo + (hi - lo) / 2;
1745 if rest[mid].raw() as u64 == base + mid as u64 {
1746 lo = mid + 1;
1747 } else {
1748 hi = mid;
1749 }
1750 }
1751 lo
1752}
1753
1754#[inline(always)]
1802fn close_rel_all<const N: usize>(a: [f32; N], b: [f32; N]) -> bool {
1803 let mut ok = [false; N];
1804 for ((lane, &a), &b) in ok.iter_mut().zip(&a).zip(&b) {
1805 *lane = close_rel(a, b);
1806 }
1807 ok.into_iter().fold(true, |all, lane| all & lane)
1808}
1809
1810#[inline(always)]
1816fn stroke_lane(
1817 current: Option<crate::Stroke>,
1818 retained: Option<crate::Stroke>,
1819 scale: f32,
1820) -> (f32, f32, bool) {
1821 match (current, retained) {
1822 (None, None) => (0.0, 0.0, true),
1823 (Some(now), Some(then)) => (now.width, then.width * scale, true),
1824 _ => (0.0, 0.0, false),
1825 }
1826}
1827
1828#[inline(always)]
1835fn match_arc_lanes(
1836 current: &SolidArcRecord,
1837 retained: &SolidArcRecord,
1838 center: Point,
1839 scale: f32,
1840 angle: f32,
1841) -> RecordMatch {
1842 let (stroke_now, stroke_then, stroke_shape_ok) =
1843 stroke_lane(current.stroke, retained.stroke, scale);
1844 let a = [
1845 current.center.x,
1846 current.center.y,
1847 current.center.x,
1848 current.center.y,
1849 current.radius,
1850 current.inner_radius,
1851 current.sweep_angle,
1852 stroke_now,
1853 ];
1854 let b = [
1855 retained.center.x,
1856 retained.center.y,
1857 center.x,
1858 center.y,
1859 retained.radius * scale,
1860 retained.inner_radius * scale,
1861 retained.sweep_angle,
1862 stroke_then,
1863 ];
1864 let geometry_ok = close_rel_all(a, b)
1865 & stroke_shape_ok
1866 & close_angle(current.start_angle, retained.start_angle + angle);
1867 if !geometry_ok {
1868 return RecordMatch::Mismatch;
1869 }
1870 if current.color == retained.color {
1871 RecordMatch::Exact
1872 } else {
1873 RecordMatch::Recolor
1874 }
1875}
1876
1877#[inline(always)]
1887fn match_round_rect_lanes(
1888 current: &SolidRoundRectRecord,
1889 retained: &SolidRoundRectRecord,
1890 center: Point,
1891 scale: f32,
1892 sin: f32,
1893 cos: f32,
1894) -> RecordMatch {
1895 let half_now = current.rect.width * 0.5;
1898 let half_then = retained.rect.width * 0.5;
1899 let c_now = Point::new(
1900 current.rect.x + current.rect.width * 0.5,
1901 current.rect.y + current.rect.height * 0.5,
1902 );
1903 let c_then = Point::new(
1904 retained.rect.x + retained.rect.width * 0.5,
1905 retained.rect.y + retained.rect.height * 0.5,
1906 );
1907 let moved = apply_parts(scale, sin, cos, center, c_then);
1908 let (stroke_now, stroke_then, stroke_shape_ok) =
1909 stroke_lane(current.stroke, retained.stroke, scale);
1910 let a = [
1911 current.radii.top_left,
1912 current.radii.top_right,
1913 current.radii.bottom_right,
1914 current.radii.bottom_left,
1915 retained.radii.top_left,
1916 retained.radii.top_right,
1917 retained.radii.bottom_right,
1918 retained.radii.bottom_left,
1919 current.rect.width,
1920 retained.rect.width,
1921 c_now.x,
1922 c_now.y,
1923 current.rect.width,
1924 stroke_now,
1925 ];
1926 let b = [
1927 half_now,
1928 half_now,
1929 half_now,
1930 half_now,
1931 half_then,
1932 half_then,
1933 half_then,
1934 half_then,
1935 current.rect.height,
1936 retained.rect.height,
1937 moved.x,
1938 moved.y,
1939 retained.rect.width * scale,
1940 stroke_then,
1941 ];
1942 let geometry_ok = close_rel_all(a, b) & stroke_shape_ok;
1943 if !geometry_ok {
1944 return RecordMatch::Mismatch;
1945 }
1946 if current.color == retained.color {
1947 RecordMatch::Exact
1948 } else {
1949 RecordMatch::Recolor
1950 }
1951}
1952
1953fn match_arc_run(
1960 current: &[SolidArcRecord],
1961 snapshot: &[SolidArcRecord],
1962 center: Point,
1963 t: RecordTransform,
1964 span_offset: usize,
1965 recolors: &mut Vec<(u32, Color)>,
1966) -> usize {
1967 let (scale, angle) = (t.scale, t.angle);
1968 for (i, (now, then)) in current.iter().zip(snapshot).enumerate() {
1969 match match_arc_lanes(now, then, center, scale, angle) {
1970 RecordMatch::Exact => {}
1971 RecordMatch::Recolor => recolors.push(((span_offset + i) as u32, now.color)),
1972 RecordMatch::Mismatch => return i,
1973 }
1974 }
1975 current.len()
1976}
1977
1978fn match_round_rect_run(
1986 current: &[SolidRoundRectRecord],
1987 snapshot: &[SolidRoundRectRecord],
1988 center: Point,
1989 t: RecordTransform,
1990 span_offset: usize,
1991 recolors: &mut Vec<(u32, Color)>,
1992) -> usize {
1993 let scale = t.scale;
1994 let (sin, cos) = t.angle.sin_cos();
1995 for (i, (now, then)) in current.iter().zip(snapshot).enumerate() {
1996 match match_round_rect_lanes(now, then, center, scale, sin, cos) {
1997 RecordMatch::Exact => {}
1998 RecordMatch::Recolor => recolors.push(((span_offset + i) as u32, now.color)),
1999 RecordMatch::Mismatch => return i,
2000 }
2001 }
2002 current.len()
2003}
2004
2005#[allow(clippy::too_many_arguments)]
2023fn match_span(
2024 current: TypedRecords<'_>,
2025 snapshot: TypedRecords<'_>,
2026 center: Point,
2027 start: usize,
2028 seg_start: usize,
2029 len: usize,
2030 t: RecordTransform,
2031 recolors: &mut Vec<(u32, Color)>,
2032) -> usize {
2033 recolors.clear();
2034 let current_tape = ¤t.tape[start..start + len];
2035 let snapshot_tape = &snapshot.tape[seg_start..seg_start + len];
2036 let mut offset = 0usize;
2037 while offset < len {
2038 let current_ref = current_tape[offset];
2039 let snapshot_ref = snapshot_tape[offset];
2040 let run = typed_run_len(current_tape, offset).min(typed_run_len(snapshot_tape, offset));
2041 let matched = match (current_ref.kind(), snapshot_ref.kind()) {
2042 (RecordKind::SolidArc, RecordKind::SolidArc) => {
2043 let (a, b) = (current_ref.index(), snapshot_ref.index());
2044 match_arc_run(
2045 ¤t.arcs[a..a + run],
2046 &snapshot.arcs[b..b + run],
2047 center,
2048 t,
2049 offset,
2050 recolors,
2051 )
2052 }
2053 (RecordKind::SolidRoundRect, RecordKind::SolidRoundRect) => {
2054 let (a, b) = (current_ref.index(), snapshot_ref.index());
2055 match_round_rect_run(
2056 ¤t.round_rects[a..a + run],
2057 &snapshot.round_rects[b..b + run],
2058 center,
2059 t,
2060 offset,
2061 recolors,
2062 )
2063 }
2064 _ => 0,
2068 };
2069 offset += matched;
2070 if matched < run {
2071 return offset;
2072 }
2073 }
2074 len
2075}
2076
2077#[cfg(test)]
2078mod tests {
2079 use super::*;
2080 use crate::{Color, Stroke};
2081
2082 const CENTER: Point = Point { x: 204.0, y: 204.0 };
2083
2084 fn arc(radius: f32, start: f32, color: Color) -> SolidArcRecord {
2085 SolidArcRecord {
2086 center: CENTER,
2087 radius,
2088 start_angle: start,
2089 sweep_angle: 0.4,
2090 inner_radius: radius * 0.8,
2091 color,
2092 stroke: None,
2093 }
2094 }
2095
2096 fn moved_arc(base: &SolidArcRecord, t: RecordTransform) -> SolidArcRecord {
2097 SolidArcRecord {
2098 center: base.center,
2099 radius: base.radius * t.scale,
2100 start_angle: base.start_angle + t.angle,
2101 sweep_angle: base.sweep_angle,
2102 inner_radius: base.inner_radius * t.scale,
2103 color: base.color,
2104 stroke: base.stroke.map(|stroke| Stroke {
2105 width: stroke.width * t.scale,
2106 ..stroke
2107 }),
2108 }
2109 }
2110
2111 fn circle(cx: f32, cy: f32, diameter: f32, color: Color) -> SolidRoundRectRecord {
2112 SolidRoundRectRecord {
2113 rect: Rect {
2114 x: cx - diameter * 0.5,
2115 y: cy - diameter * 0.5,
2116 width: diameter,
2117 height: diameter,
2118 },
2119 radii: CornerRadii::uniform(diameter * 0.5),
2120 color,
2121 stroke: None,
2122 }
2123 }
2124
2125 #[test]
2126 fn arc_anchor_recovers_the_baked_transform() {
2127 let t = RecordTransform {
2128 scale: 0.9994,
2129 angle: 0.0123,
2130 };
2131 let retained = arc(120.0, 1.0, Color::WHITE);
2132 let current = moved_arc(&retained, t);
2133 let derived = arc_anchor_transform(¤t, &retained).expect("derivable");
2134 assert!((derived.scale - t.scale).abs() < 1e-6);
2135 assert!((derived.angle - t.angle).abs() < 1e-6);
2136 assert_eq!(
2137 match_arc(¤t, &retained, CENTER, derived),
2138 RecordMatch::Exact
2139 );
2140 }
2141
2142 #[test]
2143 fn recolored_arc_matches_as_recolor() {
2144 let t = RecordTransform {
2145 scale: 1.0,
2146 angle: 0.05,
2147 };
2148 let retained = arc(80.0, 0.2, Color::WHITE);
2149 let mut current = moved_arc(&retained, t);
2150 current.color = Color::rgb(0.5, 0.1, 0.9);
2151 assert_eq!(
2152 match_arc(¤t, &retained, CENTER, t),
2153 RecordMatch::Recolor
2154 );
2155 }
2156
2157 #[test]
2158 fn changed_sweep_is_a_mismatch() {
2159 let t = RecordTransform::IDENTITY;
2160 let retained = arc(80.0, 0.2, Color::WHITE);
2161 let mut current = retained;
2162 current.sweep_angle += 0.1;
2163 assert_eq!(
2164 match_arc(¤t, &retained, CENTER, t),
2165 RecordMatch::Mismatch
2166 );
2167 }
2168
2169 #[test]
2170 fn stroked_arc_scales_its_width_with_the_segment() {
2171 let t = RecordTransform {
2172 scale: 0.98,
2173 angle: 0.0,
2174 };
2175 let mut retained = arc(60.0, 0.0, Color::WHITE);
2176 retained.stroke = Some(Stroke::new(5.0));
2177 let current = moved_arc(&retained, t);
2178 assert_eq!(
2179 match_arc(¤t, &retained, CENTER, t),
2180 RecordMatch::Exact
2181 );
2182
2183 let mut stale = current;
2186 stale.stroke = Some(Stroke::new(5.0));
2187 assert_eq!(
2188 match_arc(&stale, &retained, CENTER, t),
2189 RecordMatch::Mismatch
2190 );
2191 }
2192
2193 #[test]
2194 fn orbiting_circle_matches_under_rotation() {
2195 let t = RecordTransform {
2196 scale: 1.0,
2197 angle: 0.3,
2198 };
2199 let retained = circle(304.0, 204.0, 10.0, Color::WHITE);
2200 let (c_then, d_then) = circle_view(&retained).expect("circle");
2201 let c_now = t.apply(CENTER, c_then);
2202 let current = circle(c_now.x, c_now.y, d_then * t.scale, Color::WHITE);
2203 let (derived, pinned) = circle_anchor_transform_pinned(
2204 circle_view(¤t).unwrap(),
2205 (c_then, d_then),
2206 CENTER,
2207 )
2208 .expect("derivable");
2209 assert!(pinned, "an off-pivot circle pins rotation");
2210 assert!((derived.angle - t.angle).abs() < 1e-4);
2211 assert_eq!(
2212 match_round_rect(¤t, &retained, CENTER, derived),
2213 RecordMatch::Exact
2214 );
2215 }
2216
2217 #[test]
2218 fn non_circular_round_rect_never_matches() {
2219 let mut retained = circle(304.0, 204.0, 10.0, Color::WHITE);
2220 retained.rect.width = 14.0; assert_eq!(
2222 match_round_rect(&retained, &retained, CENTER, RecordTransform::IDENTITY),
2223 RecordMatch::Mismatch
2224 );
2225 }
2226
2227 #[test]
2228 fn grouping_is_tighter_than_verification() {
2229 let anchor = RecordTransform {
2230 scale: 1.0,
2231 angle: 0.010,
2232 };
2233 let same_ring = RecordTransform {
2234 scale: 1.0,
2235 angle: 0.0100001,
2236 };
2237 let next_ring = RecordTransform {
2238 scale: 1.0,
2239 angle: 0.011,
2240 };
2241 assert!(transforms_group(same_ring, true, anchor));
2242 assert!(
2243 !transforms_group(next_ring, true, anchor),
2244 "a 1e-3 rotation-step difference is another ring, not float noise"
2245 );
2246 let unpinned = RecordTransform {
2247 scale: 1.0,
2248 angle: 0.0,
2249 };
2250 assert!(transforms_group(unpinned, false, anchor));
2251 }
2252
2253 use crate::geometry::{DrawScopeDefault, Size};
2254 use crate::{Brush, DrawScope as _};
2255
2256 fn ring_frame(rings: usize, per_ring: usize, frame: usize, tail: usize) -> CommandRecording {
2260 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2261 let scale = 0.9994f32.powi(frame as i32);
2262 for ring in 0..rings {
2263 let step = 0.01 + ring as f32 * 0.005;
2264 let rotation = step * frame as f32;
2265 let radius = (60.0 + ring as f32 * 30.0) * scale;
2266 for slot in 0..per_ring {
2267 let start = slot as f32 * (std::f32::consts::TAU / per_ring as f32) + rotation;
2268 scope.draw_annular_sector(
2269 Brush::solid(Color::WHITE),
2270 CENTER,
2271 radius * 0.8,
2272 radius,
2273 start,
2274 0.02,
2275 );
2276 }
2277 }
2278 for i in 0..tail {
2279 let x = 40.0 + (frame * 17 + i * 31) as f32 % 300.0;
2281 scope.draw_circle(Brush::solid(Color::RED), Point::new(x, 50.0), 3.0);
2282 }
2283 scope.recorded().clone()
2284 }
2285
2286 #[test]
2287 fn ring_scene_reaches_retention_by_the_third_frame() {
2288 let mut state = CommandReplayState::default();
2289 assert!(matches!(
2290 state.advance(&ring_frame(3, 300, 0, 10)),
2291 ReplayOutcome::AllDynamic
2292 ));
2293 let ReplayOutcome::Spans(capture_spans) = state.advance(&ring_frame(3, 300, 1, 10)) else {
2296 panic!("partition frame should emit the capture");
2297 };
2298 assert!(capture_spans.iter().all(|span| match span {
2299 ReplaySpan::Retained {
2300 capture, transform, ..
2301 } => *capture && *transform == RecordTransform::IDENTITY,
2302 ReplaySpan::Dynamic { .. } => true,
2303 }));
2304 assert!(!state.segments().is_empty(), "partition found the rings");
2305
2306 let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(3, 300, 2, 10)) else {
2307 panic!("third frame should retain");
2308 };
2309 let retained: usize = spans
2310 .iter()
2311 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2312 .count();
2313 assert!(retained >= 3, "each ring retains, got {spans:?}");
2314 assert!(spans
2316 .iter()
2317 .any(|span| matches!(span, ReplaySpan::Dynamic { .. })));
2318 let transforms: Vec<RecordTransform> = spans
2320 .iter()
2321 .filter_map(|span| match span {
2322 ReplaySpan::Retained { transform, .. } => Some(*transform),
2323 _ => None,
2324 })
2325 .collect();
2326 assert!(transforms.windows(2).any(|w| w[0].angle != w[1].angle));
2327 }
2328
2329 fn flipped_ring_frame(frame: usize) -> CommandRecording {
2333 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2334 for ring in 0..4 {
2335 let rotation = (0.02 + ring as f32 * 0.007) * frame as f32;
2336 let radius = 75.0 + ring as f32 * 27.0;
2337 for slot in 0..260 {
2338 let start = slot as f32 * (std::f32::consts::TAU / 260.0) + rotation;
2339 scope.draw_annular_sector(
2340 Brush::solid(Color::WHITE),
2341 CENTER,
2342 radius * 0.75,
2343 radius,
2344 start,
2345 0.015,
2346 );
2347 }
2348 }
2349 scope.recorded().clone()
2350 }
2351
2352 #[test]
2353 fn only_a_collapse_out_of_capture_sets_the_transition_flag() {
2354 let mut state = CommandReplayState::default();
2355 assert!(matches!(
2357 state.advance(&ring_frame(3, 300, 0, 10)),
2358 ReplayOutcome::AllDynamic
2359 ));
2360 assert!(!state.collapsed_from_captured());
2361 assert!(matches!(
2363 state.advance(&ring_frame(3, 300, 1, 10)),
2364 ReplayOutcome::Spans(_)
2365 ));
2366 assert!(!state.collapsed_from_captured());
2367 assert!(matches!(
2369 state.advance(&ring_frame(3, 300, 2, 10)),
2370 ReplayOutcome::Spans(_)
2371 ));
2372 assert!(!state.collapsed_from_captured());
2373 assert!(matches!(
2378 state.advance(&flipped_ring_frame(3)),
2379 ReplayOutcome::AllDynamic
2380 ));
2381 assert!(state.collapsed_from_captured());
2382 let _ = state.advance(&flipped_ring_frame(4));
2384 assert!(!state.collapsed_from_captured());
2385 let _ = state.advance(&flipped_ring_frame(5));
2388 let short = ring_frame(1, 40, 0, 0);
2389 assert!(short.len() < MIN_REPLAY_COMMAND_RECORDS);
2390 assert!(matches!(state.advance(&short), ReplayOutcome::AllDynamic));
2391 assert!(!state.collapsed_from_captured());
2392 }
2393
2394 #[test]
2395 fn entity_churn_between_frames_still_retains_rings() {
2396 let mut state = CommandReplayState::default();
2397 state.advance(&ring_frame(2, 400, 0, 8));
2398 state.advance(&ring_frame(2, 400, 1, 13)); let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(2, 400, 2, 5)) else {
2400 panic!("churned tail must not break ring retention");
2401 };
2402 let retained_records: usize = spans
2403 .iter()
2404 .filter_map(|span| match span {
2405 ReplaySpan::Retained { .. } => Some(1),
2406 _ => None,
2407 })
2408 .sum();
2409 assert!(retained_records >= 2);
2410 }
2411
2412 #[test]
2413 fn recolors_are_patches_not_mismatches() {
2414 let recolored_frame = |frame: usize| {
2415 let mut recording = ring_frame(1, 600, frame, 0);
2416 for i in (0..recording.arcs.len()).step_by(15) {
2418 recording.arcs[i].color = if frame.is_multiple_of(2) {
2419 Color::rgb(1.0, 0.5, 0.1)
2420 } else {
2421 Color::rgb(0.1, 0.5, 1.0)
2422 };
2423 }
2424 recording
2425 };
2426 let mut state = CommandReplayState::default();
2427 state.advance(&recolored_frame(0));
2428 state.advance(&recolored_frame(1));
2429 let ReplayOutcome::Spans(spans) = state.advance(&recolored_frame(2)) else {
2430 panic!("twinkles must not break retention");
2431 };
2432 let recolor_count: usize = spans
2433 .iter()
2434 .filter_map(|span| match span {
2435 ReplaySpan::Retained { recolors, .. } => Some(recolors.len()),
2436 _ => None,
2437 })
2438 .sum();
2439 assert!(recolor_count >= 30, "twinkles surface as patches");
2440 }
2441
2442 #[test]
2443 fn geometry_change_kills_only_its_segment() {
2444 let mut state = CommandReplayState::default();
2445 state.advance(&ring_frame(3, 300, 0, 0));
2446 state.advance(&ring_frame(3, 300, 1, 0));
2447 let mut broken = ring_frame(3, 300, 2, 0);
2448 broken.arcs[450].sweep_angle *= 3.0;
2450 let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2451 panic!("one changed entry must not drop the whole command");
2452 };
2453 let retained: usize = spans
2454 .iter()
2455 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2456 .count();
2457 assert!(
2458 retained >= 2,
2459 "the untouched rings keep retaining, got {spans:?}"
2460 );
2461 }
2462
2463 #[test]
2464 fn mid_segment_change_splits_and_retains_both_halves() {
2465 let mut state = CommandReplayState::default();
2466 state.advance(&ring_frame(1, 900, 0, 0));
2467 state.advance(&ring_frame(1, 900, 1, 0));
2468 assert_eq!(state.segments().len(), 1, "one ring is one segment");
2469 let mut broken = ring_frame(1, 900, 2, 0);
2470 broken.arcs[450].sweep_angle *= 3.0;
2471 let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2472 panic!("a single changed record must not drop retention");
2473 };
2474 let dynamic: usize = spans
2475 .iter()
2476 .filter_map(|span| match span {
2477 ReplaySpan::Dynamic {
2478 tape_start,
2479 tape_end,
2480 } => Some(tape_end - tape_start),
2481 _ => None,
2482 })
2483 .sum();
2484 let retained: Vec<(u32, usize, bool)> = spans
2485 .iter()
2486 .filter_map(|span| match span {
2487 ReplaySpan::Retained {
2488 slot,
2489 slot_offset,
2490 capture,
2491 ..
2492 } => Some((*slot, *slot_offset, *capture)),
2493 _ => None,
2494 })
2495 .collect();
2496 assert_eq!(
2497 retained.len(),
2498 2,
2499 "prefix and suffix both retain: {spans:?}"
2500 );
2501 assert_eq!(retained[0].0, retained[1].0);
2505 assert_eq!(retained[0].1, 0);
2506 assert_eq!(retained[1].1, 451);
2507 assert!(retained.iter().all(|(_, _, capture)| !capture));
2508 assert_eq!(dynamic, 1, "only the changed record goes dynamic");
2509 assert_eq!(state.stats(), (0, 1), "one split, no deaths");
2510
2511 let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(1, 900, 3, 0)) else {
2514 panic!("split pieces must keep retaining");
2515 };
2516 let retained = spans
2517 .iter()
2518 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2519 .count();
2520 assert_eq!(retained, 2, "both pieces relocate next frame: {spans:?}");
2521 }
2522
2523 #[test]
2524 fn erosion_recaptures_dead_ranges_after_the_cooldown() {
2525 let mut state = CommandReplayState::default();
2526 state.advance(&ring_frame(3, 300, 0, 0));
2527 state.advance(&ring_frame(3, 300, 1, 0));
2528 let mutated = |frame: usize| {
2531 let mut recording = ring_frame(3, 300, frame, 0);
2532 for arc in &mut recording.arcs[300..600] {
2533 arc.sweep_angle *= 3.0;
2534 }
2535 recording
2536 };
2537 let dynamic_records = |outcome: &ReplayOutcome| -> usize {
2538 match outcome {
2539 ReplayOutcome::AllDynamic => usize::MAX,
2540 ReplayOutcome::Spans(spans) => spans
2541 .iter()
2542 .filter_map(|span| match span {
2543 ReplaySpan::Dynamic {
2544 tape_start,
2545 tape_end,
2546 } => Some(tape_end - tape_start),
2547 _ => None,
2548 })
2549 .sum(),
2550 }
2551 };
2552 let after_death = state.advance(&mutated(2));
2553 let lost = dynamic_records(&after_death);
2554 assert!(
2555 (250..=400).contains(&lost),
2556 "the changed ring goes dynamic, got {lost}"
2557 );
2558 for frame in 3..(3 + RECAPTURE_COOLDOWN_FRAMES as usize + 4) {
2559 state.advance(&mutated(frame));
2560 }
2561 let recovered = state.advance(&mutated(200));
2562 let residue = dynamic_records(&recovered);
2563 assert!(
2564 residue < 50,
2565 "the recapture watches the ring's new shape, got {residue} dynamic"
2566 );
2567 }
2568
2569 #[test]
2570 fn small_commands_are_not_watched() {
2571 let mut state = CommandReplayState::default();
2572 for frame in 0..4 {
2573 assert!(matches!(
2574 state.advance(&ring_frame(1, 40, frame, 0)),
2575 ReplayOutcome::AllDynamic
2576 ));
2577 }
2578 assert!(state.segments().is_empty());
2579 }
2580
2581 #[allow(clippy::too_many_arguments)]
2586 fn match_span_reference(
2587 current: TypedRecords<'_>,
2588 snapshot: TypedRecords<'_>,
2589 center: Point,
2590 start: usize,
2591 seg_start: usize,
2592 len: usize,
2593 t: RecordTransform,
2594 recolors: &mut Vec<(u32, Color)>,
2595 ) -> usize {
2596 recolors.clear();
2597 for offset in 0..len {
2598 let entry_match = match (
2599 current.view_at(start + offset),
2600 snapshot.view_at(seg_start + offset),
2601 ) {
2602 (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
2603 match_arc(¤t.arcs[i], &snapshot.arcs[j], center, t)
2604 }
2605 (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
2606 match_round_rect(¤t.round_rects[i], &snapshot.round_rects[j], center, t)
2607 }
2608 _ => RecordMatch::Mismatch,
2609 };
2610 match entry_match {
2611 RecordMatch::Exact => {}
2612 RecordMatch::Recolor => {
2613 let color = match current.view_at(start + offset) {
2614 Some(ReplayView::Arc(a)) => current.arcs[a].color,
2615 Some(ReplayView::RoundRect(r)) => current.round_rects[r].color,
2616 None => unreachable!("recolor requires a view"),
2617 };
2618 recolors.push((offset as u32, color));
2619 }
2620 RecordMatch::Mismatch => return offset,
2621 }
2622 }
2623 len
2624 }
2625
2626 fn mixed_frame(t: RecordTransform, recolored: bool) -> CommandRecording {
2638 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2639 for slot in 0..8 {
2640 let color = if recolored && slot == 3 {
2641 Color::rgb(1.0, 0.5, 0.1)
2642 } else {
2643 Color::WHITE
2644 };
2645 scope.draw_annular_sector(
2646 Brush::solid(color),
2647 CENTER,
2648 80.0 * t.scale * 0.8,
2649 80.0 * t.scale,
2650 slot as f32 * 0.7 + t.angle,
2651 0.02,
2652 );
2653 }
2654 scope.draw_round_rect_at(
2655 Rect {
2656 x: 10.0,
2657 y: 10.0,
2658 width: 40.0,
2659 height: 20.0,
2660 },
2661 Brush::solid(Color::WHITE),
2662 CornerRadii::uniform(4.0),
2663 );
2664 scope.draw_rect_at(
2665 Rect {
2666 x: 60.0,
2667 y: 10.0,
2668 width: 20.0,
2669 height: 20.0,
2670 },
2671 Brush::solid(Color::WHITE),
2672 );
2673 scope.draw_rect_at(
2674 Rect {
2675 x: 90.0,
2676 y: 10.0,
2677 width: 20.0,
2678 height: 20.0,
2679 },
2680 Brush::linear_gradient(vec![Color::WHITE, Color::RED]),
2681 );
2682 for slot in 0..3 {
2683 let base = Point::new(304.0, 204.0 + slot as f32 * 20.0);
2684 let color = if recolored && slot == 1 {
2685 Color::rgb(0.1, 0.5, 1.0)
2686 } else {
2687 Color::WHITE
2688 };
2689 scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 5.0 * t.scale);
2690 }
2691 for slot in 0..6 {
2692 let color = if recolored && slot == 1 {
2693 Color::rgb(0.9, 0.2, 0.4)
2694 } else {
2695 Color::WHITE
2696 };
2697 scope.draw_annular_sector(
2698 Brush::solid(color),
2699 CENTER,
2700 120.0 * t.scale * 0.8,
2701 120.0 * t.scale,
2702 slot as f32 * 0.9 + 0.1 + t.angle,
2703 0.03,
2704 );
2705 }
2706 for slot in 0..2 {
2707 let base = Point::new(104.0, 204.0 + slot as f32 * 24.0);
2708 let color = if recolored && slot == 1 {
2709 Color::rgb(0.2, 0.9, 0.3)
2710 } else {
2711 Color::WHITE
2712 };
2713 scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 4.0 * t.scale);
2714 }
2715 scope.recorded().clone()
2716 }
2717
2718 #[test]
2719 fn interleaved_tape_decomposes_into_exact_runs() {
2720 let recording = mixed_frame(RecordTransform::IDENTITY, false);
2721 let tape = &recording.tape;
2722 let mut runs: Vec<(RecordKind, usize, usize)> = Vec::new();
2723 let mut at = 0usize;
2724 while at < tape.len() {
2725 let len = typed_run_len(tape, at);
2726 runs.push((tape[at].kind(), tape[at].index(), len));
2727 at += len;
2728 }
2729 assert_eq!(
2730 runs,
2731 vec![
2732 (RecordKind::SolidArc, 0, 8),
2733 (RecordKind::SolidRoundRect, 0, 1),
2734 (RecordKind::SolidRect, 0, 1),
2735 (RecordKind::Other, 0, 1),
2736 (RecordKind::SolidRoundRect, 1, 3),
2737 (RecordKind::SolidArc, 8, 6),
2738 (RecordKind::SolidRoundRect, 4, 2),
2739 ],
2740 "run decomposition must cut exactly at kind transitions"
2741 );
2742 assert_eq!(typed_run_len(tape, 3), 5);
2746 assert_eq!(typed_run_len(tape, 8), 1);
2747 assert_eq!(typed_run_len(tape, 12), 2);
2748 assert_eq!(typed_run_len(tape, 14), 6);
2749 assert_eq!(typed_run_len(tape, 20), 2);
2750 }
2751
2752 #[test]
2753 fn run_decomposed_span_match_equals_the_per_entry_walk() {
2754 let t = RecordTransform {
2755 scale: 0.9994,
2756 angle: 0.0123,
2757 };
2758 let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2759 let mut current_rec = mixed_frame(t, true);
2760 current_rec.arcs[11].sweep_angle *= 3.0;
2764 current_rec.arcs[12].start_angle = f32::NAN;
2765 let current = TypedRecords::from(¤t_rec);
2766 let snapshot = TypedRecords::from(&snapshot_rec);
2767 let n = current_rec.tape.len();
2768 assert_eq!(n, snapshot_rec.tape.len());
2769 assert_eq!(n, 22);
2770 let mut fast: Vec<(u32, Color)> = Vec::new();
2771 let mut naive: Vec<(u32, Color)> = Vec::new();
2772 for start in 0..n {
2776 for seg_start in 0..n {
2777 let longest = n - start.max(seg_start);
2778 for len in [0usize, 1, 2, 5, longest] {
2779 if start + len > n || seg_start + len > n {
2780 continue;
2781 }
2782 let matched = match_span(
2783 current, snapshot, CENTER, start, seg_start, len, t, &mut fast,
2784 );
2785 let reference = match_span_reference(
2786 current, snapshot, CENTER, start, seg_start, len, t, &mut naive,
2787 );
2788 assert_eq!(
2789 (matched, &fast),
2790 (reference, &naive),
2791 "diverged at start={start} seg_start={seg_start} len={len}"
2792 );
2793 }
2794 }
2795 }
2796 let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2806 assert_eq!(
2807 (matched, fast.as_slice()),
2808 (8, &[(3, Color::rgb(1.0, 0.5, 0.1))][..])
2809 );
2810 let matched = match_span(current, snapshot, CENTER, 11, 11, 3, t, &mut fast);
2811 assert_eq!(
2812 (matched, fast.as_slice()),
2813 (3, &[(1, Color::rgb(0.1, 0.5, 1.0))][..])
2814 );
2815 let matched = match_span(current, snapshot, CENTER, 11, 11, 9, t, &mut fast);
2816 assert_eq!(
2817 (matched, fast.as_slice()),
2818 (
2819 6,
2820 &[
2821 (1, Color::rgb(0.1, 0.5, 1.0)),
2822 (4, Color::rgb(0.9, 0.2, 0.4)),
2823 ][..]
2824 ),
2825 "the changed arc ends the clean prefix behind the circles"
2826 );
2827 let matched = match_span(current, snapshot, CENTER, 19, 19, 3, t, &mut fast);
2828 assert_eq!(
2829 (matched, fast.as_slice()),
2830 (3, &[(2, Color::rgb(0.2, 0.9, 0.3))][..])
2831 );
2832 }
2833
2834 #[test]
2835 fn nan_records_mismatch_through_both_paths() {
2836 let t = RecordTransform::IDENTITY;
2837 let base = arc(80.0, 0.2, Color::WHITE);
2838 let poisoned_arcs: [fn(&mut SolidArcRecord); 6] = [
2839 |a| a.center.x = f32::NAN,
2840 |a| a.center.y = f32::NAN,
2841 |a| a.radius = f32::NAN,
2842 |a| a.inner_radius = f32::NAN,
2843 |a| a.start_angle = f32::NAN,
2844 |a| a.sweep_angle = f32::NAN,
2845 ];
2846 for poison in poisoned_arcs {
2847 let mut poisoned = base;
2848 poison(&mut poisoned);
2849 assert_eq!(
2850 match_arc(&poisoned, &base, CENTER, t),
2851 RecordMatch::Mismatch
2852 );
2853 assert_eq!(
2854 match_arc(&base, &poisoned, CENTER, t),
2855 RecordMatch::Mismatch
2856 );
2857 }
2858 let good = circle(304.0, 204.0, 10.0, Color::WHITE);
2862 let poisoned_circles: [fn(&mut SolidRoundRectRecord); 2] =
2863 [|r| r.rect.x = f32::NAN, |r| r.rect.width = f32::NAN];
2864 for poison in poisoned_circles {
2865 let mut poisoned = good;
2866 poison(&mut poisoned);
2867 assert_eq!(
2868 match_round_rect(&poisoned, &good, CENTER, t),
2869 RecordMatch::Mismatch
2870 );
2871 assert_eq!(
2872 match_round_rect(&good, &poisoned, CENTER, t),
2873 RecordMatch::Mismatch
2874 );
2875 }
2876 let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2880 let mut current_rec = mixed_frame(RecordTransform::IDENTITY, false);
2881 current_rec.arcs[4].sweep_angle = f32::NAN;
2882 let current = TypedRecords::from(¤t_rec);
2883 let snapshot = TypedRecords::from(&snapshot_rec);
2884 let mut fast: Vec<(u32, Color)> = Vec::new();
2885 let mut naive: Vec<(u32, Color)> = Vec::new();
2886 let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2887 let reference = match_span_reference(current, snapshot, CENTER, 0, 0, 8, t, &mut naive);
2888 assert_eq!(matched, 4, "the NaN record is a mismatch, not a match");
2889 assert_eq!((matched, &fast), (reference, &naive));
2890 }
2891
2892 struct ThreadedExec {
2896 lanes: usize,
2897 }
2898
2899 impl VerifyExecutor for ThreadedExec {
2900 fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync)) {
2901 std::thread::scope(|s| {
2902 for lane in 1..self.lanes {
2903 s.spawn(move || {
2904 let mut i = lane;
2905 while i < jobs {
2906 run(i);
2907 i += self.lanes;
2908 }
2909 });
2910 }
2911 let mut i = 0;
2912 while i < jobs {
2913 run(i);
2914 i += self.lanes;
2915 }
2916 });
2917 }
2918 }
2919
2920 #[test]
2921 fn pooled_verification_matches_serial_exactly() {
2922 let exec = ThreadedExec { lanes: 3 };
2923 let frame = |f: usize| -> CommandRecording {
2931 let tail = [10usize, 13, 5, 8, 11, 6, 9, 12][f % 8];
2932 let mut recording = ring_frame(3, 300, f, tail);
2933 if f >= 3 {
2934 for i in (0..recording.arcs.len()).step_by(17) {
2935 recording.arcs[i].color = if f.is_multiple_of(2) {
2936 Color::rgb(1.0, 0.5, 0.1)
2937 } else {
2938 Color::rgb(0.1, 0.5, 1.0)
2939 };
2940 }
2941 }
2942 match f {
2943 5 => {
2944 recording.arcs[450].sweep_angle = 0.15;
2948 }
2949 8 => {
2950 recording.arcs[100].sweep_angle = 0.15;
2954 recording.arcs[750].sweep_angle = 0.15;
2955 }
2956 12 => {
2957 recording.arcs[500].sweep_angle = 0.15;
2959 }
2960 16..=39 => {
2961 for arc in &mut recording.arcs[600..900] {
2967 arc.sweep_angle = 0.06;
2968 }
2969 }
2970 40..=45 => {
2971 for arc in &mut recording.arcs[150..900] {
2975 arc.sweep_angle = 0.08;
2976 }
2977 }
2978 52 => {
2979 recording.arcs[450].sweep_angle = 0.15;
2981 }
2982 _ => {}
2983 }
2984 recording
2985 };
2986 let mut serial = CommandReplayState::default();
2987 let mut pooled = CommandReplayState::default();
2988 for f in 0..60 {
2989 let recording = frame(f);
2990 let serial_outcome = serial.advance(&recording);
2991 let pooled_outcome = pooled.advance_pooled(&recording, Some(&exec));
2992 assert_eq!(
2993 serial_outcome, pooled_outcome,
2994 "outcome diverged at frame {f}"
2995 );
2996 assert_eq!(
2997 serial.segments(),
2998 pooled.segments(),
2999 "segments diverged at frame {f}"
3000 );
3001 assert_eq!(
3002 serial.stats(),
3003 pooled.stats(),
3004 "stats diverged at frame {f}"
3005 );
3006 }
3007 let (deaths, splits) = serial.stats();
3008 assert!(
3009 !serial.segments().is_empty() && deaths > 0 && splits > 0,
3010 "sequence must exercise retention, deaths, and splits, \
3011 got {deaths} deaths {splits} splits {} segments",
3012 serial.segments().len()
3013 );
3014 assert_eq!(serial.optimistic_commits(), 0);
3015 assert_eq!(serial.prefix_commits(), 0);
3016 assert!(
3017 pooled.optimistic_commits() >= 10,
3018 "the pooled fast path must actually commit steady frames, got {}",
3019 pooled.optimistic_commits()
3020 );
3021 assert!(
3022 pooled.prefix_commits() >= 3,
3023 "churn frames must commit their pooled prefix, got {}",
3024 pooled.prefix_commits()
3025 );
3026 }
3027
3028 #[test]
3029 fn transformed_bounds_contain_the_moved_content() {
3030 let t = RecordTransform {
3031 scale: 1.1,
3032 angle: 0.5,
3033 };
3034 let bounds = Rect {
3035 x: 150.0,
3036 y: 150.0,
3037 width: 100.0,
3038 height: 30.0,
3039 };
3040 let moved = t.apply_to_bounds(CENTER, bounds);
3041 for corner in [
3042 Point::new(bounds.x, bounds.y),
3043 Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
3044 ] {
3045 let p = t.apply(CENTER, corner);
3046 assert!(p.x >= moved.x - 1e-3 && p.x <= moved.x + moved.width + 1e-3);
3047 assert!(p.y >= moved.y - 1e-3 && p.y <= moved.y + moved.height + 1e-3);
3048 }
3049 }
3050}
3051
3052#[cfg(test)]
3069mod lane_kernel_equivalence {
3070 use super::*;
3071 use crate::{Color, Stroke};
3072 use std::f32::consts::TAU;
3073
3074 const KNIFE: f32 = 5.0e-4;
3079
3080 const PIVOT: Point = Point { x: 204.0, y: 204.0 };
3081 const T: RecordTransform = RecordTransform {
3082 scale: 0.9994,
3083 angle: 0.0123,
3084 };
3085 const DENORMAL: f32 = 1.0e-40;
3087 const POISONS: [f32; 4] = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, DENORMAL];
3088
3089 fn transforms() -> [RecordTransform; 5] {
3090 [
3091 RecordTransform::IDENTITY,
3092 T,
3093 RecordTransform {
3094 scale: 1.37,
3095 angle: 3.0,
3096 },
3097 RecordTransform {
3098 scale: f32::NAN,
3099 angle: f32::NAN,
3100 },
3101 RecordTransform {
3102 scale: f32::INFINITY,
3103 angle: 0.0,
3104 },
3105 ]
3106 }
3107
3108 fn bits(recolors: &[(u32, Color)]) -> Vec<(u32, [u32; 4])> {
3112 recolors
3113 .iter()
3114 .map(|&(i, Color(r, g, b, a))| {
3115 (i, [r.to_bits(), g.to_bits(), b.to_bits(), a.to_bits()])
3116 })
3117 .collect()
3118 }
3119
3120 struct Case<R> {
3121 label: String,
3122 current: R,
3123 retained: R,
3124 expected: Option<RecordMatch>,
3129 }
3130
3131 fn arc_base(stroke: Option<f32>) -> SolidArcRecord {
3134 SolidArcRecord {
3135 center: PIVOT,
3136 radius: 120.0,
3137 start_angle: 1.0,
3138 sweep_angle: 0.4,
3139 inner_radius: 96.0,
3140 color: Color::WHITE,
3141 stroke: stroke.map(Stroke::new),
3142 }
3143 }
3144
3145 fn arc_moved(retained: &SolidArcRecord) -> SolidArcRecord {
3148 SolidArcRecord {
3149 center: retained.center,
3150 radius: retained.radius * T.scale,
3151 start_angle: retained.start_angle + T.angle,
3152 sweep_angle: retained.sweep_angle,
3153 inner_radius: retained.inner_radius * T.scale,
3154 color: retained.color,
3155 stroke: retained.stroke.map(|stroke| Stroke {
3156 width: stroke.width * T.scale,
3157 ..stroke
3158 }),
3159 }
3160 }
3161
3162 type ArcGet = fn(&SolidArcRecord) -> f32;
3163 type ArcSet = fn(&mut SolidArcRecord, f32);
3164
3165 fn arc_fields() -> [(&'static str, ArcGet, ArcSet); 6] {
3166 [
3167 ("center.x", |a| a.center.x, |a, v| a.center.x = v),
3168 ("center.y", |a| a.center.y, |a, v| a.center.y = v),
3169 ("radius", |a| a.radius, |a, v| a.radius = v),
3170 (
3171 "inner_radius",
3172 |a| a.inner_radius,
3173 |a, v| a.inner_radius = v,
3174 ),
3175 ("start_angle", |a| a.start_angle, |a, v| a.start_angle = v),
3176 ("sweep_angle", |a| a.sweep_angle, |a, v| a.sweep_angle = v),
3177 ]
3178 }
3179
3180 fn arc_corpus() -> Vec<Case<SolidArcRecord>> {
3181 let mut corpus: Vec<Case<SolidArcRecord>> = Vec::new();
3182 for stroke in [None, Some(5.0_f32)] {
3183 let retained = arc_base(stroke);
3184 let matched = arc_moved(&retained);
3185 corpus.push(Case {
3186 label: format!("exact, stroke {stroke:?}"),
3187 current: matched,
3188 retained,
3189 expected: Some(RecordMatch::Exact),
3190 });
3191 let mut recolored = matched;
3192 recolored.color = Color::rgb(0.9, 0.3, 0.2);
3193 corpus.push(Case {
3194 label: format!("recolor, stroke {stroke:?}"),
3195 current: recolored,
3196 retained,
3197 expected: Some(RecordMatch::Recolor),
3198 });
3199 }
3200
3201 let retained = arc_base(None);
3202 let matched = arc_moved(&retained);
3203 for (name, get, set) in arc_fields() {
3204 let base = get(&matched);
3205 let tolerance = if name == "start_angle" {
3210 ABS_EPS
3211 } else {
3212 ABS_EPS + REL_EPS * base.abs()
3213 };
3214 for sign in [1.0_f32, -1.0] {
3215 let mut inside = matched;
3216 set(&mut inside, base + sign * 0.9 * tolerance);
3217 corpus.push(Case {
3218 label: format!("{name} just inside, sign {sign}"),
3219 current: inside,
3220 retained,
3221 expected: Some(RecordMatch::Exact),
3222 });
3223 let mut outside = matched;
3224 set(&mut outside, base + sign * 1.1 * tolerance);
3225 corpus.push(Case {
3226 label: format!("{name} just outside, sign {sign}"),
3227 current: outside,
3228 retained,
3229 expected: Some(RecordMatch::Mismatch),
3230 });
3231 }
3232 for poison in POISONS {
3233 let mut current = matched;
3234 set(&mut current, poison);
3235 corpus.push(Case {
3236 label: format!("{name} current {poison:e}"),
3237 current,
3238 retained,
3239 expected: None,
3240 });
3241 let mut poisoned = retained;
3242 set(&mut poisoned, poison);
3243 corpus.push(Case {
3244 label: format!("{name} retained {poison:e}"),
3245 current: matched,
3246 retained: poisoned,
3247 expected: None,
3248 });
3249 let mut both_current = matched;
3252 set(&mut both_current, poison);
3253 corpus.push(Case {
3254 label: format!("{name} both {poison:e}"),
3255 current: both_current,
3256 retained: poisoned,
3257 expected: None,
3258 });
3259 }
3260 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3268 for sign in [1.0_f32, -1.0] {
3269 let mut edge = matched;
3270 set(&mut edge, base + sign * knife);
3271 corpus.push(Case {
3272 label: format!("{name} knife edge {knife:e}, sign {sign}"),
3273 current: edge,
3274 retained,
3275 expected: None,
3276 });
3277 }
3278 }
3279 }
3280
3281 for (label, delta, expected) in [
3284 ("start_angle +TAU", TAU, RecordMatch::Exact),
3285 ("start_angle -TAU", -TAU, RecordMatch::Exact),
3286 ("start_angle +3 turns", 3.0 * TAU, RecordMatch::Exact),
3287 (
3288 "start_angle short of +TAU, inside",
3289 TAU - 0.9 * ABS_EPS,
3290 RecordMatch::Exact,
3291 ),
3292 (
3293 "start_angle past +TAU, outside",
3294 TAU + 1.1 * ABS_EPS,
3295 RecordMatch::Mismatch,
3296 ),
3297 ] {
3298 let mut wrapped = matched;
3299 wrapped.start_angle += delta;
3300 corpus.push(Case {
3301 label: label.to_string(),
3302 current: wrapped,
3303 retained,
3304 expected: Some(expected),
3305 });
3306 }
3307
3308 let stroked = arc_base(Some(5.0));
3310 let moved_stroked = arc_moved(&stroked);
3311 let mut some_vs_none = matched;
3312 some_vs_none.stroke = Some(Stroke::new(5.0 * T.scale));
3313 corpus.push(Case {
3314 label: "stroke Some vs None".to_string(),
3315 current: some_vs_none,
3316 retained,
3317 expected: Some(RecordMatch::Mismatch),
3318 });
3319 corpus.push(Case {
3320 label: "stroke None vs Some".to_string(),
3321 current: matched,
3322 retained: stroked,
3323 expected: Some(RecordMatch::Mismatch),
3324 });
3325 let width = 5.0 * T.scale;
3326 let tolerance = ABS_EPS + REL_EPS * width.abs();
3327 for sign in [1.0_f32, -1.0] {
3328 let mut inside = moved_stroked;
3329 inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
3330 corpus.push(Case {
3331 label: format!("stroke width just inside, sign {sign}"),
3332 current: inside,
3333 retained: stroked,
3334 expected: Some(RecordMatch::Exact),
3335 });
3336 let mut outside = moved_stroked;
3337 outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
3338 corpus.push(Case {
3339 label: format!("stroke width just outside, sign {sign}"),
3340 current: outside,
3341 retained: stroked,
3342 expected: Some(RecordMatch::Mismatch),
3343 });
3344 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3345 let mut edge = moved_stroked;
3346 edge.stroke = Some(Stroke::new(width + sign * knife));
3347 corpus.push(Case {
3348 label: format!("stroke width knife edge {knife:e}, sign {sign}"),
3349 current: edge,
3350 retained: stroked,
3351 expected: None,
3352 });
3353 }
3354 }
3355 for poison in POISONS {
3356 let mut current = moved_stroked;
3357 current.stroke = Some(Stroke::new(poison));
3358 corpus.push(Case {
3359 label: format!("stroke width current {poison:e}"),
3360 current,
3361 retained: stroked,
3362 expected: None,
3363 });
3364 let mut poisoned = stroked;
3365 poisoned.stroke = Some(Stroke::new(poison));
3366 corpus.push(Case {
3367 label: format!("stroke width retained {poison:e}"),
3368 current: moved_stroked,
3369 retained: poisoned,
3370 expected: None,
3371 });
3372 }
3373
3374 let mut nan_color = matched;
3377 nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
3378 corpus.push(Case {
3379 label: "NaN color".to_string(),
3380 current: nan_color,
3381 retained,
3382 expected: Some(RecordMatch::Recolor),
3383 });
3384 corpus
3385 }
3386
3387 #[test]
3388 fn the_arc_corpus_exercises_what_it_claims() {
3389 for case in arc_corpus() {
3390 if let Some(expected) = case.expected {
3391 assert_eq!(
3392 match_arc(&case.current, &case.retained, PIVOT, T),
3393 expected,
3394 "scalar verdict for `{}`",
3395 case.label
3396 );
3397 }
3398 }
3399 }
3400
3401 #[test]
3402 fn arc_kernel_equals_the_scalar_authority_cross_paired() {
3403 let corpus = arc_corpus();
3404 for t in transforms() {
3405 for a in &corpus {
3406 for b in &corpus {
3407 assert_eq!(
3408 match_arc_lanes(&a.current, &b.retained, PIVOT, t.scale, t.angle),
3409 match_arc(&a.current, &b.retained, PIVOT, t),
3410 "arc kernel diverged: current `{}` vs retained `{}` under {t:?}",
3411 a.label,
3412 b.label
3413 );
3414 }
3415 }
3416 }
3417 }
3418
3419 #[test]
3420 fn arc_run_equals_a_scalar_reference_from_every_start() {
3421 let corpus = arc_corpus();
3422 let current: Vec<SolidArcRecord> = corpus.iter().map(|case| case.current).collect();
3423 let snapshot: Vec<SolidArcRecord> = corpus.iter().map(|case| case.retained).collect();
3424 let mut fast: Vec<(u32, Color)> = Vec::new();
3425 let mut naive: Vec<(u32, Color)> = Vec::new();
3426 for start in 0..current.len() {
3427 fast.clear();
3428 naive.clear();
3429 let matched = match_arc_run(
3430 ¤t[start..],
3431 &snapshot[start..],
3432 PIVOT,
3433 T,
3434 7,
3435 &mut fast,
3436 );
3437 let mut mismatch = None;
3438 for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3439 match match_arc(now, then, PIVOT, T) {
3440 RecordMatch::Exact => {}
3441 RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3442 RecordMatch::Mismatch => {
3443 mismatch = Some(i);
3444 break;
3445 }
3446 }
3447 }
3448 let reference = mismatch.unwrap_or(current.len() - start);
3449 assert_eq!(
3450 (matched, bits(&fast)),
3451 (reference, bits(&naive)),
3452 "arc run diverged from start {start}"
3453 );
3454 }
3455 }
3456
3457 fn rr_base(stroke: Option<f32>) -> SolidRoundRectRecord {
3460 SolidRoundRectRecord {
3463 rect: Rect {
3464 x: 299.0,
3465 y: 199.0,
3466 width: 10.0,
3467 height: 10.0,
3468 },
3469 radii: CornerRadii::uniform(5.0),
3470 color: Color::WHITE,
3471 stroke: stroke.map(Stroke::new),
3472 }
3473 }
3474
3475 fn rr_moved(retained: &SolidRoundRectRecord) -> SolidRoundRectRecord {
3479 let (c_then, d_then) = circle_view(retained).expect("the base is a circle");
3480 let c_now = T.apply(PIVOT, c_then);
3481 let d_now = d_then * T.scale;
3482 SolidRoundRectRecord {
3483 rect: Rect {
3484 x: c_now.x - d_now * 0.5,
3485 y: c_now.y - d_now * 0.5,
3486 width: d_now,
3487 height: d_now,
3488 },
3489 radii: CornerRadii::uniform(d_now * 0.5),
3490 color: retained.color,
3491 stroke: retained.stroke.map(|stroke| Stroke {
3492 width: stroke.width * T.scale,
3493 ..stroke
3494 }),
3495 }
3496 }
3497
3498 type RrGet = fn(&SolidRoundRectRecord) -> f32;
3499 type RrSet = fn(&mut SolidRoundRectRecord, f32);
3500
3501 fn rr_fields() -> [(&'static str, RrGet, RrSet); 8] {
3502 [
3503 ("rect.x", |r| r.rect.x, |r, v| r.rect.x = v),
3504 ("rect.y", |r| r.rect.y, |r, v| r.rect.y = v),
3505 ("rect.width", |r| r.rect.width, |r, v| r.rect.width = v),
3506 ("rect.height", |r| r.rect.height, |r, v| r.rect.height = v),
3507 (
3508 "radii.top_left",
3509 |r| r.radii.top_left,
3510 |r, v| r.radii.top_left = v,
3511 ),
3512 (
3513 "radii.top_right",
3514 |r| r.radii.top_right,
3515 |r, v| r.radii.top_right = v,
3516 ),
3517 (
3518 "radii.bottom_right",
3519 |r| r.radii.bottom_right,
3520 |r, v| r.radii.bottom_right = v,
3521 ),
3522 (
3523 "radii.bottom_left",
3524 |r| r.radii.bottom_left,
3525 |r, v| r.radii.bottom_left = v,
3526 ),
3527 ]
3528 }
3529
3530 fn rr_corpus() -> Vec<Case<SolidRoundRectRecord>> {
3531 let mut corpus: Vec<Case<SolidRoundRectRecord>> = Vec::new();
3532 for stroke in [None, Some(3.0_f32)] {
3533 let retained = rr_base(stroke);
3534 let matched = rr_moved(&retained);
3535 corpus.push(Case {
3536 label: format!("exact, stroke {stroke:?}"),
3537 current: matched,
3538 retained,
3539 expected: Some(RecordMatch::Exact),
3540 });
3541 let mut recolored = matched;
3542 recolored.color = Color::rgb(0.2, 0.8, 0.4);
3543 corpus.push(Case {
3544 label: format!("recolor, stroke {stroke:?}"),
3545 current: recolored,
3546 retained,
3547 expected: Some(RecordMatch::Recolor),
3548 });
3549 }
3550
3551 let retained = rr_base(None);
3552 let matched = rr_moved(&retained);
3553 for (name, get, set) in rr_fields() {
3554 let base = get(&matched);
3555 let tolerance = ABS_EPS + REL_EPS * base.abs();
3556 for sign in [1.0_f32, -1.0] {
3557 let mut inside = matched;
3558 set(&mut inside, base + sign * 0.9 * tolerance);
3559 corpus.push(Case {
3560 label: format!("{name} just inside, sign {sign}"),
3561 current: inside,
3562 retained,
3563 expected: Some(RecordMatch::Exact),
3564 });
3565 let mut outside = matched;
3566 set(&mut outside, base + sign * 1.1 * tolerance);
3567 corpus.push(Case {
3568 label: format!("{name} just outside, sign {sign}"),
3569 current: outside,
3570 retained,
3571 expected: Some(RecordMatch::Mismatch),
3572 });
3573 }
3574 for poison in POISONS {
3575 let mut current = matched;
3576 set(&mut current, poison);
3577 corpus.push(Case {
3578 label: format!("{name} current {poison:e}"),
3579 current,
3580 retained,
3581 expected: None,
3582 });
3583 let mut poisoned = retained;
3584 set(&mut poisoned, poison);
3585 corpus.push(Case {
3586 label: format!("{name} retained {poison:e}"),
3587 current: matched,
3588 retained: poisoned,
3589 expected: None,
3590 });
3591 let mut both_current = matched;
3592 set(&mut both_current, poison);
3593 corpus.push(Case {
3594 label: format!("{name} both {poison:e}"),
3595 current: both_current,
3596 retained: poisoned,
3597 expected: None,
3598 });
3599 }
3600 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3603 for sign in [1.0_f32, -1.0] {
3604 let mut edge = matched;
3605 set(&mut edge, base + sign * knife);
3606 corpus.push(Case {
3607 label: format!("{name} knife edge {knife:e}, sign {sign}"),
3608 current: edge,
3609 retained,
3610 expected: None,
3611 });
3612 }
3613 }
3614 }
3615
3616 let mut squashed = matched;
3621 squashed.rect.height = squashed.rect.width * 2.0;
3622 corpus.push(Case {
3623 label: "current non-circle (squashed)".to_string(),
3624 current: squashed,
3625 retained,
3626 expected: Some(RecordMatch::Mismatch),
3627 });
3628 let mut loose_radii = retained;
3629 loose_radii.radii = CornerRadii::uniform(2.0);
3630 corpus.push(Case {
3631 label: "retained non-circle (loose radii)".to_string(),
3632 current: matched,
3633 retained: loose_radii,
3634 expected: Some(RecordMatch::Mismatch),
3635 });
3636 corpus.push(Case {
3637 label: "non-circle vs itself".to_string(),
3638 current: loose_radii,
3639 retained: loose_radii,
3640 expected: Some(RecordMatch::Mismatch),
3641 });
3642
3643 let stroked = rr_base(Some(3.0));
3645 let moved_stroked = rr_moved(&stroked);
3646 let mut some_vs_none = matched;
3647 some_vs_none.stroke = Some(Stroke::new(3.0 * T.scale));
3648 corpus.push(Case {
3649 label: "stroke Some vs None".to_string(),
3650 current: some_vs_none,
3651 retained,
3652 expected: Some(RecordMatch::Mismatch),
3653 });
3654 corpus.push(Case {
3655 label: "stroke None vs Some".to_string(),
3656 current: matched,
3657 retained: stroked,
3658 expected: Some(RecordMatch::Mismatch),
3659 });
3660 let width = 3.0 * T.scale;
3661 let tolerance = ABS_EPS + REL_EPS * width.abs();
3662 for sign in [1.0_f32, -1.0] {
3663 let mut inside = moved_stroked;
3664 inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
3665 corpus.push(Case {
3666 label: format!("stroke width just inside, sign {sign}"),
3667 current: inside,
3668 retained: stroked,
3669 expected: Some(RecordMatch::Exact),
3670 });
3671 let mut outside = moved_stroked;
3672 outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
3673 corpus.push(Case {
3674 label: format!("stroke width just outside, sign {sign}"),
3675 current: outside,
3676 retained: stroked,
3677 expected: Some(RecordMatch::Mismatch),
3678 });
3679 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3680 let mut edge = moved_stroked;
3681 edge.stroke = Some(Stroke::new(width + sign * knife));
3682 corpus.push(Case {
3683 label: format!("stroke width knife edge {knife:e}, sign {sign}"),
3684 current: edge,
3685 retained: stroked,
3686 expected: None,
3687 });
3688 }
3689 }
3690 for poison in POISONS {
3691 let mut current = moved_stroked;
3692 current.stroke = Some(Stroke::new(poison));
3693 corpus.push(Case {
3694 label: format!("stroke width current {poison:e}"),
3695 current,
3696 retained: stroked,
3697 expected: None,
3698 });
3699 }
3700
3701 let mut nan_color = matched;
3702 nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
3703 corpus.push(Case {
3704 label: "NaN color".to_string(),
3705 current: nan_color,
3706 retained,
3707 expected: Some(RecordMatch::Recolor),
3708 });
3709 corpus
3710 }
3711
3712 #[test]
3713 fn the_round_rect_corpus_exercises_what_it_claims() {
3714 for case in rr_corpus() {
3715 if let Some(expected) = case.expected {
3716 assert_eq!(
3717 match_round_rect(&case.current, &case.retained, PIVOT, T),
3718 expected,
3719 "scalar verdict for `{}`",
3720 case.label
3721 );
3722 }
3723 }
3724 }
3725
3726 #[test]
3727 fn round_rect_kernel_equals_the_scalar_authority_cross_paired() {
3728 let corpus = rr_corpus();
3729 for t in transforms() {
3730 let (sin, cos) = t.angle.sin_cos();
3731 for a in &corpus {
3732 for b in &corpus {
3733 assert_eq!(
3734 match_round_rect_lanes(&a.current, &b.retained, PIVOT, t.scale, sin, cos),
3735 match_round_rect(&a.current, &b.retained, PIVOT, t),
3736 "round-rect kernel diverged: current `{}` vs retained `{}` under {t:?}",
3737 a.label,
3738 b.label
3739 );
3740 }
3741 }
3742 }
3743 }
3744
3745 #[test]
3746 fn round_rect_run_equals_a_scalar_reference_from_every_start() {
3747 let corpus = rr_corpus();
3748 let current: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.current).collect();
3749 let snapshot: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.retained).collect();
3750 let mut fast: Vec<(u32, Color)> = Vec::new();
3751 let mut naive: Vec<(u32, Color)> = Vec::new();
3752 for start in 0..current.len() {
3753 fast.clear();
3754 naive.clear();
3755 let matched = match_round_rect_run(
3756 ¤t[start..],
3757 &snapshot[start..],
3758 PIVOT,
3759 T,
3760 7,
3761 &mut fast,
3762 );
3763 let mut mismatch = None;
3764 for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3765 match match_round_rect(now, then, PIVOT, T) {
3766 RecordMatch::Exact => {}
3767 RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3768 RecordMatch::Mismatch => {
3769 mismatch = Some(i);
3770 break;
3771 }
3772 }
3773 }
3774 let reference = mismatch.unwrap_or(current.len() - start);
3775 assert_eq!(
3776 (matched, bits(&fast)),
3777 (reference, bits(&naive)),
3778 "round-rect run diverged from start {start}"
3779 );
3780 }
3781 }
3782
3783 struct XorShift(u32);
3790
3791 impl XorShift {
3792 fn next(&mut self) -> u32 {
3793 let mut x = self.0;
3794 x ^= x << 13;
3795 x ^= x >> 17;
3796 x ^= x << 5;
3797 self.0 = x;
3798 x
3799 }
3800
3801 fn f32(&mut self) -> f32 {
3802 if self.next() & 1 == 0 {
3803 (self.next() as f32 / u32::MAX as f32) * 1000.0 - 500.0
3804 } else {
3805 f32::from_bits(self.next())
3806 }
3807 }
3808
3809 fn stroke(&mut self) -> Option<Stroke> {
3810 (self.next() & 1 == 0).then(|| Stroke::new(self.f32()))
3811 }
3812
3813 fn arc(&mut self) -> SolidArcRecord {
3814 SolidArcRecord {
3815 center: Point::new(self.f32(), self.f32()),
3816 radius: self.f32(),
3817 start_angle: self.f32(),
3818 sweep_angle: self.f32(),
3819 inner_radius: self.f32(),
3820 color: Color::WHITE,
3821 stroke: self.stroke(),
3822 }
3823 }
3824
3825 fn round_rect(&mut self) -> SolidRoundRectRecord {
3826 SolidRoundRectRecord {
3827 rect: Rect {
3828 x: self.f32(),
3829 y: self.f32(),
3830 width: self.f32(),
3831 height: self.f32(),
3832 },
3833 radii: CornerRadii {
3834 top_left: self.f32(),
3835 top_right: self.f32(),
3836 bottom_right: self.f32(),
3837 bottom_left: self.f32(),
3838 },
3839 color: Color::WHITE,
3840 stroke: self.stroke(),
3841 }
3842 }
3843 }
3844
3845 #[test]
3846 fn kernels_equal_the_authorities_on_arbitrary_bit_patterns() {
3847 let mut rng = XorShift(0x9e37_79b9);
3848 for _ in 0..4000 {
3849 let t = RecordTransform {
3850 scale: rng.f32(),
3851 angle: rng.f32(),
3852 };
3853 let (sin, cos) = t.angle.sin_cos();
3854 let (a, b) = (rng.arc(), rng.arc());
3855 assert_eq!(
3856 match_arc_lanes(&a, &b, PIVOT, t.scale, t.angle),
3857 match_arc(&a, &b, PIVOT, t),
3858 "arc kernel diverged: {a:?} vs {b:?} under {t:?}"
3859 );
3860 let (c, d) = (rng.round_rect(), rng.round_rect());
3861 assert_eq!(
3862 match_round_rect_lanes(&c, &d, PIVOT, t.scale, sin, cos),
3863 match_round_rect(&c, &d, PIVOT, t),
3864 "round-rect kernel diverged: {c:?} vs {d:?} under {t:?}"
3865 );
3866 }
3867 }
3868}