1use crate::{
23 Color, CornerRadii,
24 geometry::{
25 CommandRecording, Point, RecordKind, Rect, SolidArcRecord, SolidRoundRectRecord, TapeRef,
26 },
27};
28
29const REL_EPS: f32 = 2e-3;
31const ABS_EPS: f32 = 2e-2;
33const GROUP_SCALE_EPS: f32 = 1e-4;
38const GROUP_ANGLE_EPS: f32 = 2e-4;
39
40fn close_rel(a: f32, b: f32) -> bool {
41 (a - b).abs() <= ABS_EPS + REL_EPS * a.abs().max(b.abs())
42}
43
44fn close_angle(a: f32, b: f32) -> bool {
45 use std::f32::consts::TAU;
46 let d = a - b;
47 if d.abs() < TAU {
48 let wrapped = if d > TAU * 0.5 {
49 d - TAU
50 } else if d < -TAU * 0.5 {
51 d + TAU
52 } else {
53 d
54 };
55 return wrapped.abs() <= ABS_EPS;
56 }
57 let mut d = d % TAU;
58 if d > TAU * 0.5 {
59 d -= TAU;
60 }
61 if d < -TAU * 0.5 {
62 d += TAU;
63 }
64 d.abs() <= ABS_EPS
65}
66
67#[cfg(test)]
68mod close_angle_equivalence {
69 use super::*;
70
71 fn close_angle_reference(a: f32, b: f32) -> bool {
72 use std::f32::consts::TAU;
73 let mut d = (a - b) % TAU;
74 if d > TAU * 0.5 {
75 d -= TAU;
76 }
77 if d < -TAU * 0.5 {
78 d += TAU;
79 }
80 d.abs() <= ABS_EPS
81 }
82
83 #[test]
84 fn fast_path_matches_the_fmod_form() {
85 use std::f32::consts::{PI, TAU};
86 let interesting = [
87 0.0_f32,
88 -0.0,
89 1e-8,
90 -1e-8,
91 0.019,
92 -0.019,
93 0.021,
94 -0.021,
95 1.0,
96 -1.0,
97 PI - 1e-3,
98 PI,
99 PI + 1e-3,
100 -PI,
101 TAU - 0.02,
102 TAU - 1e-6,
103 TAU,
104 TAU + 1e-6,
105 TAU + 0.019,
106 -TAU,
107 -TAU - 0.019,
108 3.0 * TAU + 0.01,
109 -7.5 * TAU,
110 123.456,
111 -987.654,
112 f32::NAN,
113 f32::INFINITY,
114 f32::NEG_INFINITY,
115 f32::MAX,
116 ];
117 for &a in &interesting {
118 for &b in &interesting {
119 assert_eq!(
120 close_angle(a, b),
121 close_angle_reference(a, b),
122 "close_angle({a}, {b}) diverged from the fmod form"
123 );
124 }
125 }
126 for anchor in [-500.0_f32, -6.0, 0.0, 6.0, 500.0] {
127 for i in -2520..=2520 {
128 let d = i as f32 * 0.01;
129 assert_eq!(
130 close_angle(anchor + d, anchor),
131 close_angle_reference(anchor + d, anchor),
132 "sweep diverged at anchor {anchor} delta {d}"
133 );
134 }
135 }
136 }
137}
138
139fn close_point(a: Point, b: Point) -> bool {
140 close_rel(a.x, b.x) & close_rel(a.y, b.y)
141}
142
143#[derive(Clone, Copy, Debug, PartialEq)]
146pub struct RecordTransform {
147 pub scale: f32,
148 pub angle: f32,
149}
150
151#[inline(always)]
159fn apply_parts(scale: f32, sin: f32, cos: f32, center: Point, p: Point) -> Point {
160 let dx = p.x - center.x;
161 let dy = p.y - center.y;
162 Point::new(
163 center.x + (dx * cos - dy * sin) * scale,
164 center.y + (dx * sin + dy * cos) * scale,
165 )
166}
167
168impl RecordTransform {
169 pub const IDENTITY: Self = Self {
170 scale: 1.0,
171 angle: 0.0,
172 };
173
174 pub fn apply(&self, center: Point, p: Point) -> Point {
175 let (sin, cos) = self.angle.sin_cos();
176 apply_parts(self.scale, sin, cos, center, p)
177 }
178
179 pub fn apply_to_bounds(&self, center: Point, bounds: Rect) -> Rect {
184 let corners = [
185 Point::new(bounds.x, bounds.y),
186 Point::new(bounds.x + bounds.width, bounds.y),
187 Point::new(bounds.x, bounds.y + bounds.height),
188 Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
189 ];
190 let mut min_x = f32::INFINITY;
191 let mut min_y = f32::INFINITY;
192 let mut max_x = f32::NEG_INFINITY;
193 let mut max_y = f32::NEG_INFINITY;
194 for corner in corners {
195 let p = self.apply(center, corner);
196 min_x = min_x.min(p.x);
197 min_y = min_y.min(p.y);
198 max_x = max_x.max(p.x);
199 max_y = max_y.max(p.y);
200 }
201 Rect {
202 x: min_x,
203 y: min_y,
204 width: max_x - min_x,
205 height: max_y - min_y,
206 }
207 }
208}
209
210pub fn transforms_group(
214 entry: RecordTransform,
215 entry_pinned: bool,
216 anchor: RecordTransform,
217) -> bool {
218 use std::f32::consts::TAU;
219 if (entry.scale - anchor.scale).abs() > GROUP_SCALE_EPS * anchor.scale.abs().max(1.0) {
220 return false;
221 }
222 if !entry_pinned {
223 return true;
224 }
225 let mut d = (entry.angle - anchor.angle) % TAU;
226 if d > TAU * 0.5 {
227 d -= TAU;
228 }
229 if d < -TAU * 0.5 {
230 d += TAU;
231 }
232 d.abs() <= GROUP_ANGLE_EPS
233}
234
235#[derive(Clone, Copy, Debug, PartialEq, Eq)]
238pub enum RecordMatch {
239 Exact,
240 Recolor,
243 Mismatch,
244}
245
246pub fn circle_view(record: &SolidRoundRectRecord) -> Option<(Point, f32)> {
250 if !is_circle(record.rect, record.radii) {
251 return None;
252 }
253 Some((
254 Point::new(
255 record.rect.x + record.rect.width * 0.5,
256 record.rect.y + record.rect.height * 0.5,
257 ),
258 record.rect.width,
259 ))
260}
261
262pub fn is_circle(rect: Rect, radii: CornerRadii) -> bool {
264 let half = rect.width * 0.5;
265 close_rel(rect.width, rect.height)
266 & close_rel(radii.top_left, half)
267 & close_rel(radii.top_right, half)
268 & close_rel(radii.bottom_right, half)
269 & close_rel(radii.bottom_left, half)
270}
271
272fn stroke_width(record_stroke: Option<crate::Stroke>) -> Option<f32> {
273 record_stroke.map(|stroke| stroke.width)
274}
275
276pub fn arcs_anchor_compatible(current: &SolidArcRecord, anchor: &SolidArcRecord) -> bool {
281 close_rel(current.sweep_angle, anchor.sweep_angle)
282 && current.stroke.is_some() == anchor.stroke.is_some()
283}
284
285pub fn arc_anchor_transform(
288 current: &SolidArcRecord,
289 retained: &SolidArcRecord,
290) -> Option<RecordTransform> {
291 if retained.radius <= f32::EPSILON {
292 return None;
293 }
294 Some(RecordTransform {
295 scale: current.radius / retained.radius,
296 angle: current.start_angle - retained.start_angle,
297 })
298}
299
300pub fn circle_anchor_transform_pinned(
303 current: (Point, f32),
304 retained: (Point, f32),
305 center: Point,
306) -> Option<(RecordTransform, bool)> {
307 let (c_now, d_now) = current;
308 let (c_then, d_then) = retained;
309 if d_then <= f32::EPSILON {
310 return None;
311 }
312 let scale = d_now / d_then;
313 let dx_then = c_then.x - center.x;
314 let dy_then = c_then.y - center.y;
315 let pinned = dx_then * dx_then + dy_then * dy_then > 1.0;
316 let angle = if pinned {
317 let dx_now = c_now.x - center.x;
318 let dy_now = c_now.y - center.y;
319 dy_now.atan2(dx_now) - dy_then.atan2(dx_then)
320 } else {
321 0.0
322 };
323 Some((RecordTransform { scale, angle }, pinned))
324}
325
326pub fn match_arc(
341 current: &SolidArcRecord,
342 retained: &SolidArcRecord,
343 center: Point,
344 t: RecordTransform,
345) -> RecordMatch {
346 let stroke_ok = match (stroke_width(current.stroke), stroke_width(retained.stroke)) {
347 (None, None) => true,
348 (Some(now), Some(then)) => close_rel(now, then * t.scale),
349 _ => false,
350 };
351 let geometry_ok = close_point(current.center, retained.center)
352 & close_point(current.center, center)
353 & close_rel(current.radius, retained.radius * t.scale)
354 & close_rel(current.inner_radius, retained.inner_radius * t.scale)
355 & close_angle(current.start_angle, retained.start_angle + t.angle)
356 & close_rel(current.sweep_angle, retained.sweep_angle)
357 & stroke_ok;
358 if !geometry_ok {
359 return RecordMatch::Mismatch;
360 }
361 if current.color == retained.color {
362 RecordMatch::Exact
363 } else {
364 RecordMatch::Recolor
365 }
366}
367
368pub fn match_round_rect(
379 current: &SolidRoundRectRecord,
380 retained: &SolidRoundRectRecord,
381 center: Point,
382 t: RecordTransform,
383) -> RecordMatch {
384 let (Some((c_now, d_now)), Some((c_then, d_then))) =
385 (circle_view(current), circle_view(retained))
386 else {
387 return RecordMatch::Mismatch;
388 };
389 let stroke_ok = match (stroke_width(current.stroke), stroke_width(retained.stroke)) {
390 (None, None) => true,
391 (Some(now), Some(then)) => close_rel(now, then * t.scale),
392 _ => false,
393 };
394 let geometry_ok = close_point(c_now, t.apply(center, c_then))
395 & close_rel(d_now, d_then * t.scale)
396 & stroke_ok;
397 if !geometry_ok {
398 return RecordMatch::Mismatch;
399 }
400 if current.color == retained.color {
401 RecordMatch::Exact
402 } else {
403 RecordMatch::Recolor
404 }
405}
406
407pub const MIN_SEGMENT_RECORDS: usize = 128;
410pub const MAX_SEGMENT_RECORDS: usize = 2048;
413pub const MIN_REPLAY_COMMAND_RECORDS: usize = 512;
415const RESYNC_SPAN: usize = 48;
418const MAX_RESYNC_EVENTS: usize = 512;
419const RESYNC_WINDOW: usize = 1024;
422const ANCHOR_PROBE_RECORDS: usize = 4;
425const MAX_COMMIT_ATTEMPTS: usize = 4;
431const MIN_COVERAGE_FRACTION: f32 = 0.5;
434const RECAPTURE_EROSION: f32 = 0.05;
438const RECAPTURE_COOLDOWN_FRAMES: u32 = 180;
442
443#[derive(Clone, Copy, Debug, PartialEq, Eq)]
448enum ReplayView {
449 Arc(usize),
450 RoundRect(usize),
451}
452
453fn view_at_slices(
459 tape: &[TapeRef],
460 round_rects: &[SolidRoundRectRecord],
461 i: usize,
462) -> Option<ReplayView> {
463 let entry = tape[i];
464 match entry.kind() {
465 RecordKind::SolidArc => Some(ReplayView::Arc(entry.index())),
466 RecordKind::SolidRoundRect => circle_view(&round_rects[entry.index()])
467 .is_some()
468 .then_some(ReplayView::RoundRect(entry.index())),
469 RecordKind::SolidRect | RecordKind::Other => None,
470 }
471}
472
473fn view_at(recording: &CommandRecording, i: usize) -> Option<ReplayView> {
475 view_at_slices(&recording.tape, &recording.round_rects, i)
476}
477
478fn detect_center(recording: &CommandRecording) -> Option<Point> {
480 recording.arcs.first().map(|arc| arc.center)
481}
482
483fn views_compatible(
486 current: &CommandRecording,
487 current_view: Option<ReplayView>,
488 retained: &CommandRecording,
489 retained_view: Option<ReplayView>,
490) -> bool {
491 match (current_view, retained_view) {
492 (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
493 arcs_anchor_compatible(¤t.arcs[i], &retained.arcs[j])
494 }
495 (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
496 let now = current.round_rects[i].stroke.is_some();
497 let then = retained.round_rects[j].stroke.is_some();
498 now == then
499 }
500 (None, None) => true,
501 _ => false,
502 }
503}
504
505fn align_recordings(
513 current: &CommandRecording,
514 retained: &CommandRecording,
515 aligned: &mut Vec<Option<usize>>,
516) {
517 let pair = |i: usize, j: usize| -> bool {
518 views_compatible(current, view_at(current, i), retained, view_at(retained, j))
519 };
520 let current_len = current.tape.len();
521 let retained_len = retained.tape.len();
522 aligned.clear();
523 aligned.resize(current_len, None);
524 let (mut i, mut j) = (0usize, 0usize);
525 let mut events = 0usize;
526 while i < current_len && j < retained_len {
527 if pair(i, j) {
528 aligned[i] = Some(j);
529 i += 1;
530 j += 1;
531 continue;
532 }
533 events += 1;
534 if events > MAX_RESYNC_EVENTS {
535 aligned.fill(None);
536 return;
537 }
538 let mut resynced = false;
539 'search: for total in 1..=RESYNC_SPAN {
540 for di in 0..=total {
541 let dj = total - di;
542 if i + di < current_len && j + dj < retained_len && pair(i + di, j + dj) {
543 i += di;
544 j += dj;
545 resynced = true;
546 break 'search;
547 }
548 }
549 }
550 if !resynced {
551 i += 1;
552 j += 1;
553 }
554 }
555}
556
557fn pair_transform(
559 current: &CommandRecording,
560 current_view: ReplayView,
561 retained: &CommandRecording,
562 retained_view: ReplayView,
563 center: Point,
564) -> Option<(RecordTransform, bool)> {
565 match (current_view, retained_view) {
566 (ReplayView::Arc(i), ReplayView::Arc(j)) => {
567 arc_anchor_transform(¤t.arcs[i], &retained.arcs[j]).map(|t| (t, true))
568 }
569 (ReplayView::RoundRect(i), ReplayView::RoundRect(j)) => {
570 let now = circle_view(¤t.round_rects[i])?;
571 let then = circle_view(&retained.round_rects[j])?;
572 circle_anchor_transform_pinned(now, then, center)
573 }
574 _ => None,
575 }
576}
577
578fn match_pair(
580 current: &CommandRecording,
581 current_view: ReplayView,
582 retained: &CommandRecording,
583 retained_view: ReplayView,
584 center: Point,
585 t: RecordTransform,
586) -> RecordMatch {
587 match (current_view, retained_view) {
588 (ReplayView::Arc(i), ReplayView::Arc(j)) => {
589 match_arc(¤t.arcs[i], &retained.arcs[j], center, t)
590 }
591 (ReplayView::RoundRect(i), ReplayView::RoundRect(j)) => {
592 match_round_rect(¤t.round_rects[i], &retained.round_rects[j], center, t)
593 }
594 _ => RecordMatch::Mismatch,
595 }
596}
597
598fn range_bounds(recording: &CommandRecording, range: (usize, usize)) -> Rect {
601 let mut min_x = f32::INFINITY;
602 let mut min_y = f32::INFINITY;
603 let mut max_x = f32::NEG_INFINITY;
604 let mut max_y = f32::NEG_INFINITY;
605 for view in (range.0..range.1).filter_map(|i| view_at(recording, i)) {
606 let (center, reach) = match view {
607 ReplayView::Arc(i) => {
608 let arc = &recording.arcs[i];
609 (
610 arc.center,
611 arc.radius + arc.stroke.map(|stroke| stroke.width).unwrap_or(0.0),
612 )
613 }
614 ReplayView::RoundRect(i) => {
615 let record = &recording.round_rects[i];
616 let Some((center, diameter)) = circle_view(record) else {
617 continue;
618 };
619 (
620 center,
621 diameter * 0.5 + record.stroke.map(|stroke| stroke.width).unwrap_or(0.0),
622 )
623 }
624 };
625 let reach = reach + 2.0;
626 min_x = min_x.min(center.x - reach);
627 min_y = min_y.min(center.y - reach);
628 max_x = max_x.max(center.x + reach);
629 max_y = max_y.max(center.y + reach);
630 }
631 if min_x > max_x {
632 return Rect {
633 x: 0.0,
634 y: 0.0,
635 width: 0.0,
636 height: 0.0,
637 };
638 }
639 Rect {
640 x: min_x,
641 y: min_y,
642 width: max_x - min_x,
643 height: max_y - min_y,
644 }
645}
646
647#[derive(Clone, Debug, PartialEq)]
652pub struct CommandSegment {
653 pub slot: u32,
659 pub slot_offset: usize,
661 pub tape_start: usize,
662 pub tape_end: usize,
663 pub bounds: Rect,
665 pub prev_recolors: Vec<u32>,
672}
673
674#[derive(Clone, Debug, PartialEq)]
676pub enum ReplaySpan {
677 Retained {
683 slot: u32,
685 capture: bool,
690 slot_offset: usize,
692 tape_start: usize,
694 tape_end: usize,
695 transform: RecordTransform,
696 recolors: Vec<(u32, Color)>,
697 bounds: Rect,
699 },
700 Dynamic { tape_start: usize, tape_end: usize },
702}
703
704#[derive(Debug, PartialEq)]
706pub enum ReplayOutcome {
707 AllDynamic,
709 Spans(Vec<ReplaySpan>),
712}
713
714pub trait VerifyExecutor: Sync {
720 fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync));
721}
722
723#[derive(Clone, Debug)]
727pub struct CommandReplayFrame {
728 pub center: Point,
730 pub spans: Vec<FrameSpan>,
732 pub fallback: Option<std::rc::Rc<crate::geometry::CommandRecording>>,
743}
744
745impl PartialEq for CommandReplayFrame {
746 fn eq(&self, other: &Self) -> bool {
747 self.center == other.center
748 && self.spans == other.spans
749 && match (&self.fallback, &other.fallback) {
750 (None, None) => true,
751 (Some(a), Some(b)) => std::rc::Rc::ptr_eq(a, b),
752 _ => false,
753 }
754 }
755}
756
757#[derive(Clone, Debug, PartialEq)]
759pub enum FrameSpan {
760 Retained {
761 slot: u32,
764 capture: bool,
768 slot_offset: u32,
770 range: (u32, u32),
775 tape_range: (u32, u32),
778 transform: RecordTransform,
779 recolors: Vec<(u32, Color)>,
781 bounds: Rect,
783 },
784 Dynamic {
785 range: (u32, u32),
787 },
788}
789
790#[derive(Clone, Copy, Debug, PartialEq, Eq)]
791enum CommandReplayPhase {
792 Idle,
793 Snapshotted,
794 Captured,
795}
796
797#[derive(Debug, Default)]
801struct SpanResultSlot {
802 matched: usize,
803 recolors: Vec<(u32, Color)>,
804}
805
806#[derive(Debug)]
811pub struct CommandReplayState {
812 phase: CommandReplayPhase,
813 center: Point,
814 snapshot: CommandRecording,
815 segments: Vec<CommandSegment>,
816 next_slot_id: u32,
817 lifetime_deaths: u64,
818 lifetime_splits: u64,
819 capture_coverage: f32,
820 frames_since_capture: u32,
821 optimistic_commits: u64,
822 prefix_commits: u64,
823 verify_results: Vec<std::sync::Mutex<SpanResultSlot>>,
824 recolor_scratch: Vec<(u32, Color)>,
825 best_recolor_scratch: Vec<(u32, Color)>,
826 verify_pending: std::collections::VecDeque<CommandSegment>,
827 verify_survivors: Vec<CommandSegment>,
828 align_scratch: Vec<Option<usize>>,
829 collapsed_from_captured: bool,
830}
831
832impl Default for CommandReplayState {
833 fn default() -> Self {
834 Self {
835 phase: CommandReplayPhase::Idle,
836 center: Point::new(0.0, 0.0),
837 snapshot: CommandRecording::default(),
838 segments: Vec::new(),
839 next_slot_id: 0,
840 lifetime_deaths: 0,
841 lifetime_splits: 0,
842 capture_coverage: 0.0,
843 frames_since_capture: 0,
844 optimistic_commits: 0,
845 prefix_commits: 0,
846 verify_results: Vec::new(),
847 recolor_scratch: Vec::new(),
848 best_recolor_scratch: Vec::new(),
849 verify_pending: std::collections::VecDeque::new(),
850 verify_survivors: Vec::new(),
851 align_scratch: Vec::new(),
852 collapsed_from_captured: false,
853 }
854 }
855}
856
857impl CommandReplayState {
858 pub fn segments(&self) -> &[CommandSegment] {
859 &self.segments
860 }
861
862 pub fn stats(&self) -> (u64, u64) {
865 (self.lifetime_deaths, self.lifetime_splits)
866 }
867
868 pub fn optimistic_commits(&self) -> u64 {
870 self.optimistic_commits
871 }
872
873 pub fn prefix_commits(&self) -> u64 {
877 self.prefix_commits
878 }
879
880 pub fn center(&self) -> Point {
882 self.center
883 }
884
885 pub fn collapsed_from_captured(&self) -> bool {
892 self.collapsed_from_captured
893 }
894
895 pub fn advance(&mut self, current: &CommandRecording) -> ReplayOutcome {
903 self.advance_pooled(current, None)
904 }
905
906 pub fn advance_pooled(
916 &mut self,
917 current: &CommandRecording,
918 pool: Option<&dyn VerifyExecutor>,
919 ) -> ReplayOutcome {
920 self.collapsed_from_captured = false;
921 if current.tape.len() < MIN_REPLAY_COMMAND_RECORDS {
922 self.retire();
923 return ReplayOutcome::AllDynamic;
924 }
925 let Some(center) = detect_center(current) else {
926 self.retire();
927 return ReplayOutcome::AllDynamic;
928 };
929 match self.phase {
930 CommandReplayPhase::Idle => {
931 self.take_snapshot(current, center);
932 ReplayOutcome::AllDynamic
933 }
934 CommandReplayPhase::Snapshotted => self.partition(current, center),
935 CommandReplayPhase::Captured => self.verify(current, pool),
936 }
937 }
938
939 fn retire(&mut self) {
940 self.phase = CommandReplayPhase::Idle;
941 self.snapshot = CommandRecording::default();
942 self.segments.clear();
943 }
944
945 fn take_snapshot(&mut self, current: &CommandRecording, center: Point) {
946 self.snapshot.clone_records_from(current);
947 self.center = center;
948 self.segments.clear();
949 self.phase = CommandReplayPhase::Snapshotted;
950 }
951
952 fn partition(&mut self, current: &CommandRecording, center: Point) -> ReplayOutcome {
953 align_recordings(current, &self.snapshot, &mut self.align_scratch);
954 let mut chains: Vec<(usize, usize)> = Vec::new();
955 let mut i = 0;
956 while i < current.tape.len() {
957 let (Some(view), Some(snapshot_view)) = (
958 view_at(current, i),
959 self.align_scratch[i].and_then(|j| view_at(&self.snapshot, j)),
960 ) else {
961 i += 1;
962 continue;
963 };
964 let Some((t, true)) =
965 pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
966 else {
967 i += 1;
968 continue;
969 };
970 if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
971 == RecordMatch::Mismatch
972 {
973 i += 1;
974 continue;
975 }
976 let start = i;
977 let mut end = i + 1;
978 while end < current.tape.len() {
979 let (Some(view), Some(snapshot_view)) = (
980 view_at(current, end),
981 self.align_scratch[end].and_then(|j| view_at(&self.snapshot, j)),
982 ) else {
983 break;
984 };
985 let Some((entry_t, pinned)) =
986 pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
987 else {
988 break;
989 };
990 if !transforms_group(entry_t, pinned, t) {
991 break;
992 }
993 if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
994 == RecordMatch::Mismatch
995 {
996 break;
997 }
998 end += 1;
999 }
1000 if end - start >= MIN_SEGMENT_RECORDS {
1001 let mut piece_start = start;
1002 while piece_start < end {
1003 let piece_end = (piece_start + MAX_SEGMENT_RECORDS).min(end);
1004 if piece_end - piece_start >= MIN_SEGMENT_RECORDS {
1005 chains.push((piece_start, piece_end));
1006 }
1007 piece_start = piece_end;
1008 }
1009 }
1010 i = end.max(i + 1);
1011 }
1012
1013 if chains.is_empty() {
1014 self.take_snapshot(current, center);
1015 return ReplayOutcome::AllDynamic;
1016 }
1017 self.take_snapshot(current, center);
1018 self.segments = chains
1019 .into_iter()
1020 .map(|range| {
1021 let slot = self.next_slot_id;
1022 self.next_slot_id += 1;
1023 CommandSegment {
1024 slot,
1025 slot_offset: 0,
1026 tape_start: range.0,
1027 tape_end: range.1,
1028 bounds: range_bounds(&self.snapshot, range),
1029 prev_recolors: Vec::new(),
1030 }
1031 })
1032 .collect();
1033 let covered: usize = self
1034 .segments
1035 .iter()
1036 .map(|segment| segment.tape_end - segment.tape_start)
1037 .sum();
1038 self.capture_coverage = covered as f32 / current.tape.len().max(1) as f32;
1039 self.frames_since_capture = 0;
1040 self.phase = CommandReplayPhase::Captured;
1041
1042 let mut spans: Vec<ReplaySpan> = Vec::with_capacity(self.segments.len() * 2 + 1);
1043 let mut cursor = 0usize;
1044 for segment in &self.segments {
1045 if segment.tape_start > cursor {
1046 spans.push(ReplaySpan::Dynamic {
1047 tape_start: cursor,
1048 tape_end: segment.tape_start,
1049 });
1050 }
1051 spans.push(ReplaySpan::Retained {
1052 slot: segment.slot,
1053 capture: true,
1054 slot_offset: 0,
1055 tape_start: segment.tape_start,
1056 tape_end: segment.tape_end,
1057 transform: RecordTransform::IDENTITY,
1058 recolors: Vec::new(),
1059 bounds: segment.bounds,
1060 });
1061 cursor = segment.tape_end;
1062 }
1063 if cursor < current.tape.len() {
1064 spans.push(ReplaySpan::Dynamic {
1065 tape_start: cursor,
1066 tape_end: current.tape.len(),
1067 });
1068 }
1069 ReplayOutcome::Spans(spans)
1070 }
1071
1072 fn verify(
1073 &mut self,
1074 current: &CommandRecording,
1075 pool: Option<&dyn VerifyExecutor>,
1076 ) -> ReplayOutcome {
1077 let mut spans: Vec<ReplaySpan> = Vec::new();
1078 let mut retained_records = 0usize;
1079 let mut cursor = 0usize;
1080 let mut committed = 0usize;
1081 if let Some(pool) = pool
1082 && self.segments.len() >= 2
1083 {
1084 let commit = self.verify_optimistic(current, pool);
1085 if commit.committed == self.segments.len() {
1086 self.optimistic_commits += 1;
1087 return self.finish_verify(current, commit.spans, commit.retained_records);
1088 }
1089 if commit.committed > 0 {
1090 self.prefix_commits += 1;
1091 }
1092 spans = commit.spans;
1093 retained_records = commit.retained_records;
1094 cursor = commit.cursor;
1095 committed = commit.committed;
1096 }
1097 self.verify_pending.clear();
1098 self.verify_pending.extend(self.segments.drain(committed..));
1099 self.verify_survivors.clear();
1100 self.verify_survivors.append(&mut self.segments);
1101 while let Some(segment) = self.verify_pending.pop_front() {
1102 let len = segment.tape_end - segment.tape_start;
1103 let search_end = (cursor + RESYNC_WINDOW)
1104 .min(current.tape.len().saturating_sub(len - 1))
1105 .max(cursor);
1106 let candidates = cursor..search_end;
1107 let mut located: Option<(usize, RecordTransform)> = None;
1108 let mut best_prefix: Option<(usize, RecordTransform)> = None;
1109 let mut best_prefix_len = 0usize;
1110 let mut attempts = 0usize;
1111 'search: for start in candidates {
1112 let Some(t) = probe_anchor(
1113 current,
1114 &self.snapshot,
1115 self.center,
1116 segment.tape_start,
1117 len,
1118 start,
1119 ) else {
1120 continue;
1121 };
1122 let matched = match_span(
1123 TypedRecords::from(current),
1124 TypedRecords::from(&self.snapshot),
1125 self.center,
1126 start,
1127 segment.tape_start,
1128 len,
1129 t,
1130 &mut self.recolor_scratch,
1131 );
1132 if matched < len {
1133 if matched > best_prefix_len {
1134 best_prefix_len = matched;
1135 best_prefix = Some((start, t));
1136 std::mem::swap(&mut self.recolor_scratch, &mut self.best_recolor_scratch);
1137 }
1138 if matched >= MIN_SEGMENT_RECORDS {
1139 attempts += 1;
1140 if attempts >= MAX_COMMIT_ATTEMPTS {
1141 break 'search;
1142 }
1143 }
1144 continue;
1145 }
1146 located = Some((start, t));
1147 break;
1148 }
1149 let (span_start, t, mut recolors, span_len) = match located {
1150 Some((start, t)) => (start, t, std::mem::take(&mut self.recolor_scratch), len),
1151 None => {
1152 let split = best_prefix_len >= MIN_SEGMENT_RECORDS;
1153 let Some((start, t)) = best_prefix.filter(|_| split) else {
1154 self.lifetime_deaths += 1;
1155 continue;
1156 };
1157 let suffix_start = segment.tape_start + best_prefix_len + 1;
1158 if segment.tape_end > suffix_start
1159 && segment.tape_end - suffix_start >= MIN_SEGMENT_RECORDS
1160 {
1161 let rebase = (best_prefix_len + 1) as u32;
1162 let cut = segment.prev_recolors.partition_point(|&p| p < rebase);
1163 self.verify_pending.push_front(CommandSegment {
1164 slot: segment.slot,
1165 slot_offset: segment.slot_offset + (suffix_start - segment.tape_start),
1166 tape_start: suffix_start,
1167 tape_end: segment.tape_end,
1168 bounds: range_bounds(&self.snapshot, (suffix_start, segment.tape_end)),
1169 prev_recolors: segment.prev_recolors[cut..]
1170 .iter()
1171 .map(|&p| p - rebase)
1172 .collect(),
1173 });
1174 }
1175 self.lifetime_splits += 1;
1176 (
1177 start,
1178 t,
1179 std::mem::take(&mut self.best_recolor_scratch),
1180 best_prefix_len,
1181 )
1182 }
1183 };
1184 let mut survivor = if span_len == len {
1185 segment
1186 } else {
1187 CommandSegment {
1188 slot: segment.slot,
1189 slot_offset: segment.slot_offset,
1190 tape_start: segment.tape_start,
1191 tape_end: segment.tape_start + span_len,
1192 bounds: range_bounds(
1193 &self.snapshot,
1194 (segment.tape_start, segment.tape_start + span_len),
1195 ),
1196 prev_recolors: segment.prev_recolors,
1197 }
1198 };
1199 merge_color_restores(
1200 &self.snapshot,
1201 survivor.tape_start,
1202 span_len,
1203 &mut survivor.prev_recolors,
1204 &mut recolors,
1205 );
1206 if span_start > cursor {
1207 spans.push(ReplaySpan::Dynamic {
1208 tape_start: cursor,
1209 tape_end: span_start,
1210 });
1211 }
1212 retained_records += span_len;
1213 spans.push(ReplaySpan::Retained {
1214 slot: survivor.slot,
1215 capture: false,
1216 slot_offset: survivor.slot_offset,
1217 tape_start: span_start,
1218 tape_end: span_start + span_len,
1219 transform: t,
1220 recolors,
1221 bounds: t.apply_to_bounds(self.center, survivor.bounds),
1222 });
1223 cursor = span_start + span_len;
1224 self.verify_survivors.push(survivor);
1225 }
1226 if cursor < current.tape.len() {
1227 spans.push(ReplaySpan::Dynamic {
1228 tape_start: cursor,
1229 tape_end: current.tape.len(),
1230 });
1231 }
1232
1233 std::mem::swap(&mut self.segments, &mut self.verify_survivors);
1234 self.finish_verify(current, spans, retained_records)
1235 }
1236
1237 fn finish_verify(
1238 &mut self,
1239 current: &CommandRecording,
1240 spans: Vec<ReplaySpan>,
1241 retained_records: usize,
1242 ) -> ReplayOutcome {
1243 self.frames_since_capture += 1;
1244 let retained_total: usize = self
1245 .segments
1246 .iter()
1247 .map(|segment| segment.tape_end - segment.tape_start)
1248 .sum();
1249 let coverage = retained_total as f32 / current.tape.len().max(1) as f32;
1250 let collapsed = retained_records == 0 || coverage < MIN_COVERAGE_FRACTION;
1251 let eroded = coverage + RECAPTURE_EROSION < self.capture_coverage
1252 && self.frames_since_capture >= RECAPTURE_COOLDOWN_FRAMES;
1253 self.collapsed_from_captured = collapsed;
1254 if collapsed || eroded {
1255 let center = self.center;
1256 self.take_snapshot(current, center);
1257 if retained_records == 0 {
1258 return ReplayOutcome::AllDynamic;
1259 }
1260 }
1261 ReplayOutcome::Spans(spans)
1262 }
1263
1264 fn verify_optimistic(
1265 &mut self,
1266 current: &CommandRecording,
1267 pool: &dyn VerifyExecutor,
1268 ) -> PooledCommit {
1269 struct SpanJob {
1270 start: usize,
1271 seg_start: usize,
1272 len: usize,
1273 t: RecordTransform,
1274 }
1275 let mut jobs: Vec<SpanJob> = Vec::with_capacity(self.segments.len());
1276 let mut cursor = 0usize;
1277 for segment in &self.segments {
1278 let len = segment.tape_end - segment.tape_start;
1279 let search_end = (cursor + RESYNC_WINDOW)
1280 .min(current.tape.len().saturating_sub(len - 1))
1281 .max(cursor);
1282 let mut found = None;
1283 for start in cursor..search_end {
1284 if let Some(t) = probe_anchor(
1285 current,
1286 &self.snapshot,
1287 self.center,
1288 segment.tape_start,
1289 len,
1290 start,
1291 ) {
1292 found = Some((start, t));
1293 break;
1294 }
1295 }
1296 let Some((start, t)) = found else {
1297 break;
1298 };
1299 jobs.push(SpanJob {
1300 start,
1301 seg_start: segment.tape_start,
1302 len,
1303 t,
1304 });
1305 cursor = start + len;
1306 }
1307 if jobs.is_empty() {
1308 return PooledCommit {
1309 spans: Vec::new(),
1310 retained_records: 0,
1311 committed: 0,
1312 cursor: 0,
1313 };
1314 }
1315 if self.verify_results.len() < jobs.len() {
1316 self.verify_results
1317 .resize_with(jobs.len(), Default::default);
1318 }
1319 {
1320 let current = TypedRecords::from(current);
1321 let snapshot = TypedRecords::from(&self.snapshot);
1322 let center = self.center;
1323 let jobs = &jobs;
1324 let results = &self.verify_results;
1325 pool.for_each(jobs.len(), &|i| {
1326 let job = &jobs[i];
1327 let mut guard = results[i].lock().expect("verify span job lock");
1328 let slot = &mut *guard;
1329 slot.matched = match_span(
1330 current,
1331 snapshot,
1332 center,
1333 job.start,
1334 job.seg_start,
1335 job.len,
1336 job.t,
1337 &mut slot.recolors,
1338 );
1339 });
1340 }
1341 let mut committed = jobs.len();
1342 for (i, (job, result)) in jobs.iter().zip(&self.verify_results).enumerate() {
1343 if result.lock().expect("verify span job lock").matched < job.len {
1344 committed = i;
1345 break;
1346 }
1347 }
1348 let mut spans: Vec<ReplaySpan> = Vec::with_capacity(committed * 2 + 1);
1349 let mut retained_records = 0usize;
1350 let mut cursor = 0usize;
1351 for (segment, (job, result)) in self
1352 .segments
1353 .iter_mut()
1354 .zip(jobs.iter().zip(&self.verify_results))
1355 .take(committed)
1356 {
1357 let mut recolors =
1358 std::mem::take(&mut result.lock().expect("verify span job lock").recolors);
1359 merge_color_restores(
1360 &self.snapshot,
1361 segment.tape_start,
1362 job.len,
1363 &mut segment.prev_recolors,
1364 &mut recolors,
1365 );
1366 if job.start > cursor {
1367 spans.push(ReplaySpan::Dynamic {
1368 tape_start: cursor,
1369 tape_end: job.start,
1370 });
1371 }
1372 retained_records += job.len;
1373 spans.push(ReplaySpan::Retained {
1374 slot: segment.slot,
1375 capture: false,
1376 slot_offset: segment.slot_offset,
1377 tape_start: job.start,
1378 tape_end: job.start + job.len,
1379 transform: job.t,
1380 recolors,
1381 bounds: job.t.apply_to_bounds(self.center, segment.bounds),
1382 });
1383 cursor = job.start + job.len;
1384 }
1385 if committed == self.segments.len() && cursor < current.tape.len() {
1386 spans.push(ReplaySpan::Dynamic {
1387 tape_start: cursor,
1388 tape_end: current.tape.len(),
1389 });
1390 }
1391 PooledCommit {
1392 spans,
1393 retained_records,
1394 committed,
1395 cursor,
1396 }
1397 }
1398}
1399
1400struct PooledCommit {
1407 spans: Vec<ReplaySpan>,
1408 retained_records: usize,
1409 committed: usize,
1410 cursor: usize,
1411}
1412
1413fn snapshot_record_color(snapshot: &CommandRecording, i: usize) -> Option<Color> {
1417 match view_at(snapshot, i)? {
1418 ReplayView::Arc(index) => Some(snapshot.arcs[index].color),
1419 ReplayView::RoundRect(index) => Some(snapshot.round_rects[index].color),
1420 }
1421}
1422
1423fn merge_color_restores(
1437 snapshot: &CommandRecording,
1438 seg_tape_start: usize,
1439 span_len: usize,
1440 prev: &mut Vec<u32>,
1441 recolors: &mut Vec<(u32, Color)>,
1442) {
1443 let patched = recolors.len();
1444 let mut cursor = 0usize;
1445 for &offset in prev.iter() {
1446 if offset as usize >= span_len {
1447 break;
1448 }
1449 while cursor < patched && recolors[cursor].0 < offset {
1450 cursor += 1;
1451 }
1452 if cursor < patched && recolors[cursor].0 == offset {
1453 continue;
1454 }
1455 if let Some(color) = snapshot_record_color(snapshot, seg_tape_start + offset as usize) {
1456 recolors.push((offset, color));
1457 }
1458 }
1459 prev.clear();
1460 prev.extend(recolors[..patched].iter().map(|&(offset, _)| offset));
1461}
1462
1463fn probe_anchor(
1468 current: &CommandRecording,
1469 snapshot: &CommandRecording,
1470 center: Point,
1471 seg_start: usize,
1472 len: usize,
1473 start: usize,
1474) -> Option<RecordTransform> {
1475 let (Some(view), Some(snapshot_view)) = (view_at(current, start), view_at(snapshot, seg_start))
1476 else {
1477 return None;
1478 };
1479 if !views_compatible(current, Some(view), snapshot, Some(snapshot_view)) {
1480 return None;
1481 }
1482 let (t, _) = pair_transform(current, view, snapshot, snapshot_view, center)?;
1483 for probe in 0..ANCHOR_PROBE_RECORDS.min(len) {
1484 let (Some(view), Some(snapshot_view)) = (
1485 view_at(current, start + probe),
1486 view_at(snapshot, seg_start + probe),
1487 ) else {
1488 return None;
1489 };
1490 if match_pair(current, view, snapshot, snapshot_view, center, t) == RecordMatch::Mismatch {
1491 return None;
1492 }
1493 }
1494 Some(t)
1495}
1496
1497#[derive(Clone, Copy)]
1502struct TypedRecords<'a> {
1503 tape: &'a [TapeRef],
1504 arcs: &'a [SolidArcRecord],
1505 round_rects: &'a [SolidRoundRectRecord],
1506}
1507
1508impl<'a> From<&'a CommandRecording> for TypedRecords<'a> {
1509 fn from(recording: &'a CommandRecording) -> Self {
1510 Self {
1511 tape: &recording.tape,
1512 arcs: &recording.arcs,
1513 round_rects: &recording.round_rects,
1514 }
1515 }
1516}
1517
1518impl TypedRecords<'_> {
1519 #[cfg(test)]
1520 fn view_at(&self, i: usize) -> Option<ReplayView> {
1521 view_at_slices(self.tape, self.round_rects, i)
1522 }
1523}
1524
1525fn typed_run_len(tape: &[TapeRef], at: usize) -> usize {
1538 let rest = &tape[at..];
1539 let base = rest[0].raw() as u64;
1540 let mut lo = 1usize;
1541 let mut hi = rest.len();
1542 while lo < hi {
1543 let mid = lo + (hi - lo) / 2;
1544 if rest[mid].raw() as u64 == base + mid as u64 {
1545 lo = mid + 1;
1546 } else {
1547 hi = mid;
1548 }
1549 }
1550 lo
1551}
1552
1553#[inline(always)]
1559fn close_rel_all<const N: usize>(a: [f32; N], b: [f32; N]) -> bool {
1560 let mut ok = [false; N];
1561 for ((lane, &a), &b) in ok.iter_mut().zip(&a).zip(&b) {
1562 *lane = close_rel(a, b);
1563 }
1564 ok.into_iter().fold(true, |all, lane| all & lane)
1565}
1566
1567#[inline(always)]
1573fn stroke_lane(
1574 current: Option<crate::Stroke>,
1575 retained: Option<crate::Stroke>,
1576 scale: f32,
1577) -> (f32, f32, bool) {
1578 match (current, retained) {
1579 (None, None) => (0.0, 0.0, true),
1580 (Some(now), Some(then)) => (now.width, then.width * scale, true),
1581 _ => (0.0, 0.0, false),
1582 }
1583}
1584
1585#[inline(always)]
1592fn match_arc_lanes(
1593 current: &SolidArcRecord,
1594 retained: &SolidArcRecord,
1595 center: Point,
1596 scale: f32,
1597 angle: f32,
1598) -> RecordMatch {
1599 let (stroke_now, stroke_then, stroke_shape_ok) =
1600 stroke_lane(current.stroke, retained.stroke, scale);
1601 let a = [
1602 current.center.x,
1603 current.center.y,
1604 current.center.x,
1605 current.center.y,
1606 current.radius,
1607 current.inner_radius,
1608 current.sweep_angle,
1609 stroke_now,
1610 ];
1611 let b = [
1612 retained.center.x,
1613 retained.center.y,
1614 center.x,
1615 center.y,
1616 retained.radius * scale,
1617 retained.inner_radius * scale,
1618 retained.sweep_angle,
1619 stroke_then,
1620 ];
1621 let geometry_ok = close_rel_all(a, b)
1622 & stroke_shape_ok
1623 & close_angle(current.start_angle, retained.start_angle + angle);
1624 if !geometry_ok {
1625 return RecordMatch::Mismatch;
1626 }
1627 if current.color == retained.color {
1628 RecordMatch::Exact
1629 } else {
1630 RecordMatch::Recolor
1631 }
1632}
1633
1634#[inline(always)]
1644fn match_round_rect_lanes(
1645 current: &SolidRoundRectRecord,
1646 retained: &SolidRoundRectRecord,
1647 center: Point,
1648 scale: f32,
1649 sin: f32,
1650 cos: f32,
1651) -> RecordMatch {
1652 let half_now = current.rect.width * 0.5;
1653 let half_then = retained.rect.width * 0.5;
1654 let c_now = Point::new(
1655 current.rect.x + current.rect.width * 0.5,
1656 current.rect.y + current.rect.height * 0.5,
1657 );
1658 let c_then = Point::new(
1659 retained.rect.x + retained.rect.width * 0.5,
1660 retained.rect.y + retained.rect.height * 0.5,
1661 );
1662 let moved = apply_parts(scale, sin, cos, center, c_then);
1663 let (stroke_now, stroke_then, stroke_shape_ok) =
1664 stroke_lane(current.stroke, retained.stroke, scale);
1665 let a = [
1666 current.radii.top_left,
1667 current.radii.top_right,
1668 current.radii.bottom_right,
1669 current.radii.bottom_left,
1670 retained.radii.top_left,
1671 retained.radii.top_right,
1672 retained.radii.bottom_right,
1673 retained.radii.bottom_left,
1674 current.rect.width,
1675 retained.rect.width,
1676 c_now.x,
1677 c_now.y,
1678 current.rect.width,
1679 stroke_now,
1680 ];
1681 let b = [
1682 half_now,
1683 half_now,
1684 half_now,
1685 half_now,
1686 half_then,
1687 half_then,
1688 half_then,
1689 half_then,
1690 current.rect.height,
1691 retained.rect.height,
1692 moved.x,
1693 moved.y,
1694 retained.rect.width * scale,
1695 stroke_then,
1696 ];
1697 let geometry_ok = close_rel_all(a, b) & stroke_shape_ok;
1698 if !geometry_ok {
1699 return RecordMatch::Mismatch;
1700 }
1701 if current.color == retained.color {
1702 RecordMatch::Exact
1703 } else {
1704 RecordMatch::Recolor
1705 }
1706}
1707
1708fn match_arc_run(
1715 current: &[SolidArcRecord],
1716 snapshot: &[SolidArcRecord],
1717 center: Point,
1718 t: RecordTransform,
1719 span_offset: usize,
1720 recolors: &mut Vec<(u32, Color)>,
1721) -> usize {
1722 let (scale, angle) = (t.scale, t.angle);
1723 for (i, (now, then)) in current.iter().zip(snapshot).enumerate() {
1724 match match_arc_lanes(now, then, center, scale, angle) {
1725 RecordMatch::Exact => {}
1726 RecordMatch::Recolor => recolors.push(((span_offset + i) as u32, now.color)),
1727 RecordMatch::Mismatch => return i,
1728 }
1729 }
1730 current.len()
1731}
1732
1733fn match_round_rect_run(
1741 current: &[SolidRoundRectRecord],
1742 snapshot: &[SolidRoundRectRecord],
1743 center: Point,
1744 t: RecordTransform,
1745 span_offset: usize,
1746 recolors: &mut Vec<(u32, Color)>,
1747) -> usize {
1748 let scale = t.scale;
1749 let (sin, cos) = t.angle.sin_cos();
1750 for (i, (now, then)) in current.iter().zip(snapshot).enumerate() {
1751 match match_round_rect_lanes(now, then, center, scale, sin, cos) {
1752 RecordMatch::Exact => {}
1753 RecordMatch::Recolor => recolors.push(((span_offset + i) as u32, now.color)),
1754 RecordMatch::Mismatch => return i,
1755 }
1756 }
1757 current.len()
1758}
1759
1760#[allow(clippy::too_many_arguments)]
1778fn match_span(
1779 current: TypedRecords<'_>,
1780 snapshot: TypedRecords<'_>,
1781 center: Point,
1782 start: usize,
1783 seg_start: usize,
1784 len: usize,
1785 t: RecordTransform,
1786 recolors: &mut Vec<(u32, Color)>,
1787) -> usize {
1788 recolors.clear();
1789 let current_tape = ¤t.tape[start..start + len];
1790 let snapshot_tape = &snapshot.tape[seg_start..seg_start + len];
1791 let mut offset = 0usize;
1792 while offset < len {
1793 let current_ref = current_tape[offset];
1794 let snapshot_ref = snapshot_tape[offset];
1795 let run = typed_run_len(current_tape, offset).min(typed_run_len(snapshot_tape, offset));
1796 let matched = match (current_ref.kind(), snapshot_ref.kind()) {
1797 (RecordKind::SolidArc, RecordKind::SolidArc) => {
1798 let (a, b) = (current_ref.index(), snapshot_ref.index());
1799 match_arc_run(
1800 ¤t.arcs[a..a + run],
1801 &snapshot.arcs[b..b + run],
1802 center,
1803 t,
1804 offset,
1805 recolors,
1806 )
1807 }
1808 (RecordKind::SolidRoundRect, RecordKind::SolidRoundRect) => {
1809 let (a, b) = (current_ref.index(), snapshot_ref.index());
1810 match_round_rect_run(
1811 ¤t.round_rects[a..a + run],
1812 &snapshot.round_rects[b..b + run],
1813 center,
1814 t,
1815 offset,
1816 recolors,
1817 )
1818 }
1819 _ => 0,
1820 };
1821 offset += matched;
1822 if matched < run {
1823 return offset;
1824 }
1825 }
1826 len
1827}
1828
1829#[cfg(test)]
1830mod tests {
1831 use super::*;
1832 use crate::{Color, Stroke};
1833
1834 const CENTER: Point = Point { x: 204.0, y: 204.0 };
1835
1836 fn arc(radius: f32, start: f32, color: Color) -> SolidArcRecord {
1837 SolidArcRecord {
1838 center: CENTER,
1839 radius,
1840 start_angle: start,
1841 sweep_angle: 0.4,
1842 inner_radius: radius * 0.8,
1843 color,
1844 stroke: None,
1845 }
1846 }
1847
1848 fn moved_arc(base: &SolidArcRecord, t: RecordTransform) -> SolidArcRecord {
1849 SolidArcRecord {
1850 center: base.center,
1851 radius: base.radius * t.scale,
1852 start_angle: base.start_angle + t.angle,
1853 sweep_angle: base.sweep_angle,
1854 inner_radius: base.inner_radius * t.scale,
1855 color: base.color,
1856 stroke: base.stroke.map(|stroke| Stroke {
1857 width: stroke.width * t.scale,
1858 ..stroke
1859 }),
1860 }
1861 }
1862
1863 fn circle(cx: f32, cy: f32, diameter: f32, color: Color) -> SolidRoundRectRecord {
1864 SolidRoundRectRecord {
1865 rect: Rect {
1866 x: cx - diameter * 0.5,
1867 y: cy - diameter * 0.5,
1868 width: diameter,
1869 height: diameter,
1870 },
1871 radii: CornerRadii::uniform(diameter * 0.5),
1872 color,
1873 stroke: None,
1874 }
1875 }
1876
1877 #[test]
1878 fn arc_anchor_recovers_the_baked_transform() {
1879 let t = RecordTransform {
1880 scale: 0.9994,
1881 angle: 0.0123,
1882 };
1883 let retained = arc(120.0, 1.0, Color::WHITE);
1884 let current = moved_arc(&retained, t);
1885 let derived = arc_anchor_transform(¤t, &retained).expect("derivable");
1886 assert!((derived.scale - t.scale).abs() < 1e-6);
1887 assert!((derived.angle - t.angle).abs() < 1e-6);
1888 assert_eq!(
1889 match_arc(¤t, &retained, CENTER, derived),
1890 RecordMatch::Exact
1891 );
1892 }
1893
1894 #[test]
1895 fn recolored_arc_matches_as_recolor() {
1896 let t = RecordTransform {
1897 scale: 1.0,
1898 angle: 0.05,
1899 };
1900 let retained = arc(80.0, 0.2, Color::WHITE);
1901 let mut current = moved_arc(&retained, t);
1902 current.color = Color::rgb(0.5, 0.1, 0.9);
1903 assert_eq!(
1904 match_arc(¤t, &retained, CENTER, t),
1905 RecordMatch::Recolor
1906 );
1907 }
1908
1909 #[test]
1910 fn changed_sweep_is_a_mismatch() {
1911 let t = RecordTransform::IDENTITY;
1912 let retained = arc(80.0, 0.2, Color::WHITE);
1913 let mut current = retained;
1914 current.sweep_angle += 0.1;
1915 assert_eq!(
1916 match_arc(¤t, &retained, CENTER, t),
1917 RecordMatch::Mismatch
1918 );
1919 }
1920
1921 #[test]
1922 fn stroked_arc_scales_its_width_with_the_segment() {
1923 let t = RecordTransform {
1924 scale: 0.98,
1925 angle: 0.0,
1926 };
1927 let mut retained = arc(60.0, 0.0, Color::WHITE);
1928 retained.stroke = Some(Stroke::new(5.0));
1929 let current = moved_arc(&retained, t);
1930 assert_eq!(
1931 match_arc(¤t, &retained, CENTER, t),
1932 RecordMatch::Exact
1933 );
1934
1935 let mut stale = current;
1936 stale.stroke = Some(Stroke::new(5.0));
1937 assert_eq!(
1938 match_arc(&stale, &retained, CENTER, t),
1939 RecordMatch::Mismatch
1940 );
1941 }
1942
1943 #[test]
1944 fn orbiting_circle_matches_under_rotation() {
1945 let t = RecordTransform {
1946 scale: 1.0,
1947 angle: 0.3,
1948 };
1949 let retained = circle(304.0, 204.0, 10.0, Color::WHITE);
1950 let (c_then, d_then) = circle_view(&retained).expect("circle");
1951 let c_now = t.apply(CENTER, c_then);
1952 let current = circle(c_now.x, c_now.y, d_then * t.scale, Color::WHITE);
1953 let (derived, pinned) = circle_anchor_transform_pinned(
1954 circle_view(¤t).unwrap(),
1955 (c_then, d_then),
1956 CENTER,
1957 )
1958 .expect("derivable");
1959 assert!(pinned, "an off-pivot circle pins rotation");
1960 assert!((derived.angle - t.angle).abs() < 1e-4);
1961 assert_eq!(
1962 match_round_rect(¤t, &retained, CENTER, derived),
1963 RecordMatch::Exact
1964 );
1965 }
1966
1967 #[test]
1968 fn non_circular_round_rect_never_matches() {
1969 let mut retained = circle(304.0, 204.0, 10.0, Color::WHITE);
1970 retained.rect.width = 14.0;
1971 assert_eq!(
1972 match_round_rect(&retained, &retained, CENTER, RecordTransform::IDENTITY),
1973 RecordMatch::Mismatch
1974 );
1975 }
1976
1977 #[test]
1978 fn grouping_is_tighter_than_verification() {
1979 let anchor = RecordTransform {
1980 scale: 1.0,
1981 angle: 0.010,
1982 };
1983 let same_ring = RecordTransform {
1984 scale: 1.0,
1985 angle: 0.0100001,
1986 };
1987 let next_ring = RecordTransform {
1988 scale: 1.0,
1989 angle: 0.011,
1990 };
1991 assert!(transforms_group(same_ring, true, anchor));
1992 assert!(
1993 !transforms_group(next_ring, true, anchor),
1994 "a 1e-3 rotation-step difference is another ring, not float noise"
1995 );
1996 let unpinned = RecordTransform {
1997 scale: 1.0,
1998 angle: 0.0,
1999 };
2000 assert!(transforms_group(unpinned, false, anchor));
2001 }
2002
2003 use crate::{
2004 Brush, DrawScope as _,
2005 geometry::{DrawScopeDefault, Size},
2006 };
2007
2008 fn ring_frame(rings: usize, per_ring: usize, frame: usize, tail: usize) -> CommandRecording {
2009 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2010 let scale = 0.9994f32.powi(frame as i32);
2011 for ring in 0..rings {
2012 let step = 0.01 + ring as f32 * 0.005;
2013 let rotation = step * frame as f32;
2014 let radius = (60.0 + ring as f32 * 30.0) * scale;
2015 for slot in 0..per_ring {
2016 let start = slot as f32 * (std::f32::consts::TAU / per_ring as f32) + rotation;
2017 scope.draw_annular_sector(
2018 Brush::solid(Color::WHITE),
2019 CENTER,
2020 radius * 0.8,
2021 radius,
2022 start,
2023 0.02,
2024 );
2025 }
2026 }
2027 for i in 0..tail {
2028 let x = 40.0 + (frame * 17 + i * 31) as f32 % 300.0;
2029 scope.draw_circle(Brush::solid(Color::RED), Point::new(x, 50.0), 3.0);
2030 }
2031 scope.recorded().clone()
2032 }
2033
2034 #[test]
2035 fn ring_scene_reaches_retention_by_the_third_frame() {
2036 let mut state = CommandReplayState::default();
2037 assert!(matches!(
2038 state.advance(&ring_frame(3, 300, 0, 10)),
2039 ReplayOutcome::AllDynamic
2040 ));
2041 let ReplayOutcome::Spans(capture_spans) = state.advance(&ring_frame(3, 300, 1, 10)) else {
2042 panic!("partition frame should emit the capture");
2043 };
2044 assert!(capture_spans.iter().all(|span| match span {
2045 ReplaySpan::Retained {
2046 capture, transform, ..
2047 } => *capture && *transform == RecordTransform::IDENTITY,
2048 ReplaySpan::Dynamic { .. } => true,
2049 }));
2050 assert!(!state.segments().is_empty(), "partition found the rings");
2051
2052 let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(3, 300, 2, 10)) else {
2053 panic!("third frame should retain");
2054 };
2055 let retained: usize = spans
2056 .iter()
2057 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2058 .count();
2059 assert!(retained >= 3, "each ring retains, got {spans:?}");
2060 assert!(
2061 spans
2062 .iter()
2063 .any(|span| matches!(span, ReplaySpan::Dynamic { .. }))
2064 );
2065 let transforms: Vec<RecordTransform> = spans
2066 .iter()
2067 .filter_map(|span| match span {
2068 ReplaySpan::Retained { transform, .. } => Some(*transform),
2069 _ => None,
2070 })
2071 .collect();
2072 assert!(transforms.windows(2).any(|w| w[0].angle != w[1].angle));
2073 }
2074
2075 fn flipped_ring_frame(frame: usize) -> CommandRecording {
2076 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2077 for ring in 0..4 {
2078 let rotation = (0.02 + ring as f32 * 0.007) * frame as f32;
2079 let radius = 75.0 + ring as f32 * 27.0;
2080 for slot in 0..260 {
2081 let start = slot as f32 * (std::f32::consts::TAU / 260.0) + rotation;
2082 scope.draw_annular_sector(
2083 Brush::solid(Color::WHITE),
2084 CENTER,
2085 radius * 0.75,
2086 radius,
2087 start,
2088 0.015,
2089 );
2090 }
2091 }
2092 scope.recorded().clone()
2093 }
2094
2095 #[test]
2096 fn only_a_collapse_out_of_capture_sets_the_transition_flag() {
2097 let mut state = CommandReplayState::default();
2098 assert!(matches!(
2099 state.advance(&ring_frame(3, 300, 0, 10)),
2100 ReplayOutcome::AllDynamic
2101 ));
2102 assert!(!state.collapsed_from_captured());
2103 assert!(matches!(
2104 state.advance(&ring_frame(3, 300, 1, 10)),
2105 ReplayOutcome::Spans(_)
2106 ));
2107 assert!(!state.collapsed_from_captured());
2108 assert!(matches!(
2109 state.advance(&ring_frame(3, 300, 2, 10)),
2110 ReplayOutcome::Spans(_)
2111 ));
2112 assert!(!state.collapsed_from_captured());
2113 assert!(matches!(
2114 state.advance(&flipped_ring_frame(3)),
2115 ReplayOutcome::AllDynamic
2116 ));
2117 assert!(state.collapsed_from_captured());
2118 let _ = state.advance(&flipped_ring_frame(4));
2119 assert!(!state.collapsed_from_captured());
2120 let _ = state.advance(&flipped_ring_frame(5));
2121 let short = ring_frame(1, 40, 0, 0);
2122 assert!(short.len() < MIN_REPLAY_COMMAND_RECORDS);
2123 assert!(matches!(state.advance(&short), ReplayOutcome::AllDynamic));
2124 assert!(!state.collapsed_from_captured());
2125 }
2126
2127 #[test]
2128 fn entity_churn_between_frames_still_retains_rings() {
2129 let mut state = CommandReplayState::default();
2130 state.advance(&ring_frame(2, 400, 0, 8));
2131 state.advance(&ring_frame(2, 400, 1, 13));
2132 let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(2, 400, 2, 5)) else {
2133 panic!("churned tail must not break ring retention");
2134 };
2135 let retained_records: usize = spans
2136 .iter()
2137 .filter_map(|span| match span {
2138 ReplaySpan::Retained { .. } => Some(1),
2139 _ => None,
2140 })
2141 .sum();
2142 assert!(retained_records >= 2);
2143 }
2144
2145 #[test]
2146 fn recolors_are_patches_not_mismatches() {
2147 let recolored_frame = |frame: usize| {
2148 let mut recording = ring_frame(1, 600, frame, 0);
2149 for i in (0..recording.arcs.len()).step_by(15) {
2150 recording.arcs[i].color = if frame.is_multiple_of(2) {
2151 Color::rgb(1.0, 0.5, 0.1)
2152 } else {
2153 Color::rgb(0.1, 0.5, 1.0)
2154 };
2155 }
2156 recording
2157 };
2158 let mut state = CommandReplayState::default();
2159 state.advance(&recolored_frame(0));
2160 state.advance(&recolored_frame(1));
2161 let ReplayOutcome::Spans(spans) = state.advance(&recolored_frame(2)) else {
2162 panic!("twinkles must not break retention");
2163 };
2164 let recolor_count: usize = spans
2165 .iter()
2166 .filter_map(|span| match span {
2167 ReplaySpan::Retained { recolors, .. } => Some(recolors.len()),
2168 _ => None,
2169 })
2170 .sum();
2171 assert!(recolor_count >= 30, "twinkles surface as patches");
2172 }
2173
2174 #[test]
2175 fn geometry_change_kills_only_its_segment() {
2176 let mut state = CommandReplayState::default();
2177 state.advance(&ring_frame(3, 300, 0, 0));
2178 state.advance(&ring_frame(3, 300, 1, 0));
2179 let mut broken = ring_frame(3, 300, 2, 0);
2180 broken.arcs[450].sweep_angle *= 3.0;
2181 let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2182 panic!("one changed entry must not drop the whole command");
2183 };
2184 let retained: usize = spans
2185 .iter()
2186 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2187 .count();
2188 assert!(
2189 retained >= 2,
2190 "the untouched rings keep retaining, got {spans:?}"
2191 );
2192 }
2193
2194 #[test]
2195 fn mid_segment_change_splits_and_retains_both_halves() {
2196 let mut state = CommandReplayState::default();
2197 state.advance(&ring_frame(1, 900, 0, 0));
2198 state.advance(&ring_frame(1, 900, 1, 0));
2199 assert_eq!(state.segments().len(), 1, "one ring is one segment");
2200 let mut broken = ring_frame(1, 900, 2, 0);
2201 broken.arcs[450].sweep_angle *= 3.0;
2202 let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2203 panic!("a single changed record must not drop retention");
2204 };
2205 let dynamic: usize = spans
2206 .iter()
2207 .filter_map(|span| match span {
2208 ReplaySpan::Dynamic {
2209 tape_start,
2210 tape_end,
2211 } => Some(tape_end - tape_start),
2212 _ => None,
2213 })
2214 .sum();
2215 let retained: Vec<(u32, usize, bool)> = spans
2216 .iter()
2217 .filter_map(|span| match span {
2218 ReplaySpan::Retained {
2219 slot,
2220 slot_offset,
2221 capture,
2222 ..
2223 } => Some((*slot, *slot_offset, *capture)),
2224 _ => None,
2225 })
2226 .collect();
2227 assert_eq!(
2228 retained.len(),
2229 2,
2230 "prefix and suffix both retain: {spans:?}"
2231 );
2232 assert_eq!(retained[0].0, retained[1].0);
2233 assert_eq!(retained[0].1, 0);
2234 assert_eq!(retained[1].1, 451);
2235 assert!(retained.iter().all(|(_, _, capture)| !capture));
2236 assert_eq!(dynamic, 1, "only the changed record goes dynamic");
2237 assert_eq!(state.stats(), (0, 1), "one split, no deaths");
2238
2239 let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(1, 900, 3, 0)) else {
2240 panic!("split pieces must keep retaining");
2241 };
2242 let retained = spans
2243 .iter()
2244 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2245 .count();
2246 assert_eq!(retained, 2, "both pieces relocate next frame: {spans:?}");
2247 }
2248
2249 #[test]
2250 fn erosion_recaptures_dead_ranges_after_the_cooldown() {
2251 let mut state = CommandReplayState::default();
2252 state.advance(&ring_frame(3, 300, 0, 0));
2253 state.advance(&ring_frame(3, 300, 1, 0));
2254 let mutated = |frame: usize| {
2255 let mut recording = ring_frame(3, 300, frame, 0);
2256 for arc in &mut recording.arcs[300..600] {
2257 arc.sweep_angle *= 3.0;
2258 }
2259 recording
2260 };
2261 let dynamic_records = |outcome: &ReplayOutcome| -> usize {
2262 match outcome {
2263 ReplayOutcome::AllDynamic => usize::MAX,
2264 ReplayOutcome::Spans(spans) => spans
2265 .iter()
2266 .filter_map(|span| match span {
2267 ReplaySpan::Dynamic {
2268 tape_start,
2269 tape_end,
2270 } => Some(tape_end - tape_start),
2271 _ => None,
2272 })
2273 .sum(),
2274 }
2275 };
2276 let after_death = state.advance(&mutated(2));
2277 let lost = dynamic_records(&after_death);
2278 assert!(
2279 (250..=400).contains(&lost),
2280 "the changed ring goes dynamic, got {lost}"
2281 );
2282 for frame in 3..(3 + RECAPTURE_COOLDOWN_FRAMES as usize + 4) {
2283 state.advance(&mutated(frame));
2284 }
2285 let recovered = state.advance(&mutated(200));
2286 let residue = dynamic_records(&recovered);
2287 assert!(
2288 residue < 50,
2289 "the recapture watches the ring's new shape, got {residue} dynamic"
2290 );
2291 }
2292
2293 #[test]
2294 fn small_commands_are_not_watched() {
2295 let mut state = CommandReplayState::default();
2296 for frame in 0..4 {
2297 assert!(matches!(
2298 state.advance(&ring_frame(1, 40, frame, 0)),
2299 ReplayOutcome::AllDynamic
2300 ));
2301 }
2302 assert!(state.segments().is_empty());
2303 }
2304
2305 #[allow(clippy::too_many_arguments)]
2306 fn match_span_reference(
2307 current: TypedRecords<'_>,
2308 snapshot: TypedRecords<'_>,
2309 center: Point,
2310 start: usize,
2311 seg_start: usize,
2312 len: usize,
2313 t: RecordTransform,
2314 recolors: &mut Vec<(u32, Color)>,
2315 ) -> usize {
2316 recolors.clear();
2317 for offset in 0..len {
2318 let entry_match = match (
2319 current.view_at(start + offset),
2320 snapshot.view_at(seg_start + offset),
2321 ) {
2322 (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
2323 match_arc(¤t.arcs[i], &snapshot.arcs[j], center, t)
2324 }
2325 (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
2326 match_round_rect(¤t.round_rects[i], &snapshot.round_rects[j], center, t)
2327 }
2328 _ => RecordMatch::Mismatch,
2329 };
2330 match entry_match {
2331 RecordMatch::Exact => {}
2332 RecordMatch::Recolor => {
2333 let color = match current.view_at(start + offset) {
2334 Some(ReplayView::Arc(a)) => current.arcs[a].color,
2335 Some(ReplayView::RoundRect(r)) => current.round_rects[r].color,
2336 None => unreachable!("recolor requires a view"),
2337 };
2338 recolors.push((offset as u32, color));
2339 }
2340 RecordMatch::Mismatch => return offset,
2341 }
2342 }
2343 len
2344 }
2345
2346 fn mixed_frame(t: RecordTransform, recolored: bool) -> CommandRecording {
2347 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2348 for slot in 0..8 {
2349 let color = if recolored && slot == 3 {
2350 Color::rgb(1.0, 0.5, 0.1)
2351 } else {
2352 Color::WHITE
2353 };
2354 scope.draw_annular_sector(
2355 Brush::solid(color),
2356 CENTER,
2357 80.0 * t.scale * 0.8,
2358 80.0 * t.scale,
2359 slot as f32 * 0.7 + t.angle,
2360 0.02,
2361 );
2362 }
2363 scope.draw_round_rect_at(
2364 Rect {
2365 x: 10.0,
2366 y: 10.0,
2367 width: 40.0,
2368 height: 20.0,
2369 },
2370 Brush::solid(Color::WHITE),
2371 CornerRadii::uniform(4.0),
2372 );
2373 scope.draw_rect_at(
2374 Rect {
2375 x: 60.0,
2376 y: 10.0,
2377 width: 20.0,
2378 height: 20.0,
2379 },
2380 Brush::solid(Color::WHITE),
2381 );
2382 scope.draw_rect_at(
2383 Rect {
2384 x: 90.0,
2385 y: 10.0,
2386 width: 20.0,
2387 height: 20.0,
2388 },
2389 Brush::linear_gradient(vec![Color::WHITE, Color::RED]),
2390 );
2391 for slot in 0..3 {
2392 let base = Point::new(304.0, 204.0 + slot as f32 * 20.0);
2393 let color = if recolored && slot == 1 {
2394 Color::rgb(0.1, 0.5, 1.0)
2395 } else {
2396 Color::WHITE
2397 };
2398 scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 5.0 * t.scale);
2399 }
2400 for slot in 0..6 {
2401 let color = if recolored && slot == 1 {
2402 Color::rgb(0.9, 0.2, 0.4)
2403 } else {
2404 Color::WHITE
2405 };
2406 scope.draw_annular_sector(
2407 Brush::solid(color),
2408 CENTER,
2409 120.0 * t.scale * 0.8,
2410 120.0 * t.scale,
2411 slot as f32 * 0.9 + 0.1 + t.angle,
2412 0.03,
2413 );
2414 }
2415 for slot in 0..2 {
2416 let base = Point::new(104.0, 204.0 + slot as f32 * 24.0);
2417 let color = if recolored && slot == 1 {
2418 Color::rgb(0.2, 0.9, 0.3)
2419 } else {
2420 Color::WHITE
2421 };
2422 scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 4.0 * t.scale);
2423 }
2424 scope.recorded().clone()
2425 }
2426
2427 #[test]
2428 fn interleaved_tape_decomposes_into_exact_runs() {
2429 let recording = mixed_frame(RecordTransform::IDENTITY, false);
2430 let tape = &recording.tape;
2431 let mut runs: Vec<(RecordKind, usize, usize)> = Vec::new();
2432 let mut at = 0usize;
2433 while at < tape.len() {
2434 let len = typed_run_len(tape, at);
2435 runs.push((tape[at].kind(), tape[at].index(), len));
2436 at += len;
2437 }
2438 assert_eq!(
2439 runs,
2440 vec![
2441 (RecordKind::SolidArc, 0, 8),
2442 (RecordKind::SolidRoundRect, 0, 1),
2443 (RecordKind::SolidRect, 0, 1),
2444 (RecordKind::Other, 0, 1),
2445 (RecordKind::SolidRoundRect, 1, 3),
2446 (RecordKind::SolidArc, 8, 6),
2447 (RecordKind::SolidRoundRect, 4, 2),
2448 ],
2449 "run decomposition must cut exactly at kind transitions"
2450 );
2451 assert_eq!(typed_run_len(tape, 3), 5);
2452 assert_eq!(typed_run_len(tape, 8), 1);
2453 assert_eq!(typed_run_len(tape, 12), 2);
2454 assert_eq!(typed_run_len(tape, 14), 6);
2455 assert_eq!(typed_run_len(tape, 20), 2);
2456 }
2457
2458 #[test]
2459 fn run_decomposed_span_match_equals_the_per_entry_walk() {
2460 let t = RecordTransform {
2461 scale: 0.9994,
2462 angle: 0.0123,
2463 };
2464 let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2465 let mut current_rec = mixed_frame(t, true);
2466 current_rec.arcs[11].sweep_angle *= 3.0;
2467 current_rec.arcs[12].start_angle = f32::NAN;
2468 let current = TypedRecords::from(¤t_rec);
2469 let snapshot = TypedRecords::from(&snapshot_rec);
2470 let n = current_rec.tape.len();
2471 assert_eq!(n, snapshot_rec.tape.len());
2472 assert_eq!(n, 22);
2473 let mut fast: Vec<(u32, Color)> = Vec::new();
2474 let mut naive: Vec<(u32, Color)> = Vec::new();
2475 for start in 0..n {
2476 for seg_start in 0..n {
2477 let longest = n - start.max(seg_start);
2478 for len in [0usize, 1, 2, 5, longest] {
2479 if start + len > n || seg_start + len > n {
2480 continue;
2481 }
2482 let matched = match_span(
2483 current, snapshot, CENTER, start, seg_start, len, t, &mut fast,
2484 );
2485 let reference = match_span_reference(
2486 current, snapshot, CENTER, start, seg_start, len, t, &mut naive,
2487 );
2488 assert_eq!(
2489 (matched, &fast),
2490 (reference, &naive),
2491 "diverged at start={start} seg_start={seg_start} len={len}"
2492 );
2493 }
2494 }
2495 }
2496 let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2497 assert_eq!(
2498 (matched, fast.as_slice()),
2499 (8, &[(3, Color::rgb(1.0, 0.5, 0.1))][..])
2500 );
2501 let matched = match_span(current, snapshot, CENTER, 11, 11, 3, t, &mut fast);
2502 assert_eq!(
2503 (matched, fast.as_slice()),
2504 (3, &[(1, Color::rgb(0.1, 0.5, 1.0))][..])
2505 );
2506 let matched = match_span(current, snapshot, CENTER, 11, 11, 9, t, &mut fast);
2507 assert_eq!(
2508 (matched, fast.as_slice()),
2509 (
2510 6,
2511 &[
2512 (1, Color::rgb(0.1, 0.5, 1.0)),
2513 (4, Color::rgb(0.9, 0.2, 0.4)),
2514 ][..]
2515 ),
2516 "the changed arc ends the clean prefix behind the circles"
2517 );
2518 let matched = match_span(current, snapshot, CENTER, 19, 19, 3, t, &mut fast);
2519 assert_eq!(
2520 (matched, fast.as_slice()),
2521 (3, &[(2, Color::rgb(0.2, 0.9, 0.3))][..])
2522 );
2523 }
2524
2525 #[test]
2526 fn nan_records_mismatch_through_both_paths() {
2527 let t = RecordTransform::IDENTITY;
2528 let base = arc(80.0, 0.2, Color::WHITE);
2529 let poisoned_arcs: [fn(&mut SolidArcRecord); 6] = [
2530 |a| a.center.x = f32::NAN,
2531 |a| a.center.y = f32::NAN,
2532 |a| a.radius = f32::NAN,
2533 |a| a.inner_radius = f32::NAN,
2534 |a| a.start_angle = f32::NAN,
2535 |a| a.sweep_angle = f32::NAN,
2536 ];
2537 for poison in poisoned_arcs {
2538 let mut poisoned = base;
2539 poison(&mut poisoned);
2540 assert_eq!(
2541 match_arc(&poisoned, &base, CENTER, t),
2542 RecordMatch::Mismatch
2543 );
2544 assert_eq!(
2545 match_arc(&base, &poisoned, CENTER, t),
2546 RecordMatch::Mismatch
2547 );
2548 }
2549 let good = circle(304.0, 204.0, 10.0, Color::WHITE);
2550 let poisoned_circles: [fn(&mut SolidRoundRectRecord); 2] =
2551 [|r| r.rect.x = f32::NAN, |r| r.rect.width = f32::NAN];
2552 for poison in poisoned_circles {
2553 let mut poisoned = good;
2554 poison(&mut poisoned);
2555 assert_eq!(
2556 match_round_rect(&poisoned, &good, CENTER, t),
2557 RecordMatch::Mismatch
2558 );
2559 assert_eq!(
2560 match_round_rect(&good, &poisoned, CENTER, t),
2561 RecordMatch::Mismatch
2562 );
2563 }
2564 let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2565 let mut current_rec = mixed_frame(RecordTransform::IDENTITY, false);
2566 current_rec.arcs[4].sweep_angle = f32::NAN;
2567 let current = TypedRecords::from(¤t_rec);
2568 let snapshot = TypedRecords::from(&snapshot_rec);
2569 let mut fast: Vec<(u32, Color)> = Vec::new();
2570 let mut naive: Vec<(u32, Color)> = Vec::new();
2571 let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2572 let reference = match_span_reference(current, snapshot, CENTER, 0, 0, 8, t, &mut naive);
2573 assert_eq!(matched, 4, "the NaN record is a mismatch, not a match");
2574 assert_eq!((matched, &fast), (reference, &naive));
2575 }
2576
2577 struct ThreadedExec {
2578 lanes: usize,
2579 }
2580
2581 impl VerifyExecutor for ThreadedExec {
2582 fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync)) {
2583 std::thread::scope(|s| {
2584 for lane in 1..self.lanes {
2585 s.spawn(move || {
2586 let mut i = lane;
2587 while i < jobs {
2588 run(i);
2589 i += self.lanes;
2590 }
2591 });
2592 }
2593 let mut i = 0;
2594 while i < jobs {
2595 run(i);
2596 i += self.lanes;
2597 }
2598 });
2599 }
2600 }
2601
2602 #[test]
2603 fn pooled_verification_matches_serial_exactly() {
2604 let exec = ThreadedExec { lanes: 3 };
2605 let frame = |f: usize| -> CommandRecording {
2606 let tail = [10usize, 13, 5, 8, 11, 6, 9, 12][f % 8];
2607 let mut recording = ring_frame(3, 300, f, tail);
2608 if f >= 3 {
2609 for i in (0..recording.arcs.len()).step_by(17) {
2610 recording.arcs[i].color = if f.is_multiple_of(2) {
2611 Color::rgb(1.0, 0.5, 0.1)
2612 } else {
2613 Color::rgb(0.1, 0.5, 1.0)
2614 };
2615 }
2616 }
2617 match f {
2618 5 => {
2619 recording.arcs[450].sweep_angle = 0.15;
2620 }
2621 8 => {
2622 recording.arcs[100].sweep_angle = 0.15;
2623 recording.arcs[750].sweep_angle = 0.15;
2624 }
2625 12 => {
2626 recording.arcs[500].sweep_angle = 0.15;
2627 }
2628 16..=39 => {
2629 for arc in &mut recording.arcs[600..900] {
2630 arc.sweep_angle = 0.06;
2631 }
2632 }
2633 40..=45 => {
2634 for arc in &mut recording.arcs[150..900] {
2635 arc.sweep_angle = 0.08;
2636 }
2637 }
2638 52 => {
2639 recording.arcs[450].sweep_angle = 0.15;
2640 }
2641 _ => {}
2642 }
2643 recording
2644 };
2645 let mut serial = CommandReplayState::default();
2646 let mut pooled = CommandReplayState::default();
2647 for f in 0..60 {
2648 let recording = frame(f);
2649 let serial_outcome = serial.advance(&recording);
2650 let pooled_outcome = pooled.advance_pooled(&recording, Some(&exec));
2651 assert_eq!(
2652 serial_outcome, pooled_outcome,
2653 "outcome diverged at frame {f}"
2654 );
2655 assert_eq!(
2656 serial.segments(),
2657 pooled.segments(),
2658 "segments diverged at frame {f}"
2659 );
2660 assert_eq!(
2661 serial.stats(),
2662 pooled.stats(),
2663 "stats diverged at frame {f}"
2664 );
2665 }
2666 let (deaths, splits) = serial.stats();
2667 assert!(
2668 !serial.segments().is_empty() && deaths > 0 && splits > 0,
2669 "sequence must exercise retention, deaths, and splits, \
2670 got {deaths} deaths {splits} splits {} segments",
2671 serial.segments().len()
2672 );
2673 assert_eq!(serial.optimistic_commits(), 0);
2674 assert_eq!(serial.prefix_commits(), 0);
2675 assert!(
2676 pooled.optimistic_commits() >= 10,
2677 "the pooled fast path must actually commit steady frames, got {}",
2678 pooled.optimistic_commits()
2679 );
2680 assert!(
2681 pooled.prefix_commits() >= 3,
2682 "churn frames must commit their pooled prefix, got {}",
2683 pooled.prefix_commits()
2684 );
2685 }
2686
2687 #[test]
2688 fn transformed_bounds_contain_the_moved_content() {
2689 let t = RecordTransform {
2690 scale: 1.1,
2691 angle: 0.5,
2692 };
2693 let bounds = Rect {
2694 x: 150.0,
2695 y: 150.0,
2696 width: 100.0,
2697 height: 30.0,
2698 };
2699 let moved = t.apply_to_bounds(CENTER, bounds);
2700 for corner in [
2701 Point::new(bounds.x, bounds.y),
2702 Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
2703 ] {
2704 let p = t.apply(CENTER, corner);
2705 assert!(p.x >= moved.x - 1e-3 && p.x <= moved.x + moved.width + 1e-3);
2706 assert!(p.y >= moved.y - 1e-3 && p.y <= moved.y + moved.height + 1e-3);
2707 }
2708 }
2709}
2710
2711#[cfg(test)]
2712mod lane_kernel_equivalence {
2713 use std::f32::consts::TAU;
2714
2715 use super::*;
2716 use crate::{Color, Stroke};
2717
2718 const KNIFE: f32 = 5.0e-4;
2719
2720 const PIVOT: Point = Point { x: 204.0, y: 204.0 };
2721 const T: RecordTransform = RecordTransform {
2722 scale: 0.9994,
2723 angle: 0.0123,
2724 };
2725 const DENORMAL: f32 = 1.0e-40;
2726 const POISONS: [f32; 4] = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, DENORMAL];
2727
2728 fn transforms() -> [RecordTransform; 5] {
2729 [
2730 RecordTransform::IDENTITY,
2731 T,
2732 RecordTransform {
2733 scale: 1.37,
2734 angle: 3.0,
2735 },
2736 RecordTransform {
2737 scale: f32::NAN,
2738 angle: f32::NAN,
2739 },
2740 RecordTransform {
2741 scale: f32::INFINITY,
2742 angle: 0.0,
2743 },
2744 ]
2745 }
2746
2747 fn bits(recolors: &[(u32, Color)]) -> Vec<(u32, [u32; 4])> {
2748 recolors
2749 .iter()
2750 .map(|&(i, Color(r, g, b, a))| {
2751 (i, [r.to_bits(), g.to_bits(), b.to_bits(), a.to_bits()])
2752 })
2753 .collect()
2754 }
2755
2756 struct Case<R> {
2757 label: String,
2758 current: R,
2759 retained: R,
2760 expected: Option<RecordMatch>,
2761 }
2762
2763 fn arc_base(stroke: Option<f32>) -> SolidArcRecord {
2764 SolidArcRecord {
2765 center: PIVOT,
2766 radius: 120.0,
2767 start_angle: 1.0,
2768 sweep_angle: 0.4,
2769 inner_radius: 96.0,
2770 color: Color::WHITE,
2771 stroke: stroke.map(Stroke::new),
2772 }
2773 }
2774
2775 fn arc_moved(retained: &SolidArcRecord) -> SolidArcRecord {
2776 SolidArcRecord {
2777 center: retained.center,
2778 radius: retained.radius * T.scale,
2779 start_angle: retained.start_angle + T.angle,
2780 sweep_angle: retained.sweep_angle,
2781 inner_radius: retained.inner_radius * T.scale,
2782 color: retained.color,
2783 stroke: retained.stroke.map(|stroke| Stroke {
2784 width: stroke.width * T.scale,
2785 ..stroke
2786 }),
2787 }
2788 }
2789
2790 type ArcGet = fn(&SolidArcRecord) -> f32;
2791 type ArcSet = fn(&mut SolidArcRecord, f32);
2792
2793 fn arc_fields() -> [(&'static str, ArcGet, ArcSet); 6] {
2794 [
2795 ("center.x", |a| a.center.x, |a, v| a.center.x = v),
2796 ("center.y", |a| a.center.y, |a, v| a.center.y = v),
2797 ("radius", |a| a.radius, |a, v| a.radius = v),
2798 (
2799 "inner_radius",
2800 |a| a.inner_radius,
2801 |a, v| a.inner_radius = v,
2802 ),
2803 ("start_angle", |a| a.start_angle, |a, v| a.start_angle = v),
2804 ("sweep_angle", |a| a.sweep_angle, |a, v| a.sweep_angle = v),
2805 ]
2806 }
2807
2808 fn arc_corpus() -> Vec<Case<SolidArcRecord>> {
2809 let mut corpus: Vec<Case<SolidArcRecord>> = Vec::new();
2810 for stroke in [None, Some(5.0_f32)] {
2811 let retained = arc_base(stroke);
2812 let matched = arc_moved(&retained);
2813 corpus.push(Case {
2814 label: format!("exact, stroke {stroke:?}"),
2815 current: matched,
2816 retained,
2817 expected: Some(RecordMatch::Exact),
2818 });
2819 let mut recolored = matched;
2820 recolored.color = Color::rgb(0.9, 0.3, 0.2);
2821 corpus.push(Case {
2822 label: format!("recolor, stroke {stroke:?}"),
2823 current: recolored,
2824 retained,
2825 expected: Some(RecordMatch::Recolor),
2826 });
2827 }
2828
2829 let retained = arc_base(None);
2830 let matched = arc_moved(&retained);
2831 for (name, get, set) in arc_fields() {
2832 let base = get(&matched);
2833 let tolerance = if name == "start_angle" {
2834 ABS_EPS
2835 } else {
2836 ABS_EPS + REL_EPS * base.abs()
2837 };
2838 for sign in [1.0_f32, -1.0] {
2839 let mut inside = matched;
2840 set(&mut inside, base + sign * 0.9 * tolerance);
2841 corpus.push(Case {
2842 label: format!("{name} just inside, sign {sign}"),
2843 current: inside,
2844 retained,
2845 expected: Some(RecordMatch::Exact),
2846 });
2847 let mut outside = matched;
2848 set(&mut outside, base + sign * 1.1 * tolerance);
2849 corpus.push(Case {
2850 label: format!("{name} just outside, sign {sign}"),
2851 current: outside,
2852 retained,
2853 expected: Some(RecordMatch::Mismatch),
2854 });
2855 }
2856 for poison in POISONS {
2857 let mut current = matched;
2858 set(&mut current, poison);
2859 corpus.push(Case {
2860 label: format!("{name} current {poison:e}"),
2861 current,
2862 retained,
2863 expected: None,
2864 });
2865 let mut poisoned = retained;
2866 set(&mut poisoned, poison);
2867 corpus.push(Case {
2868 label: format!("{name} retained {poison:e}"),
2869 current: matched,
2870 retained: poisoned,
2871 expected: None,
2872 });
2873 let mut both_current = matched;
2874 set(&mut both_current, poison);
2875 corpus.push(Case {
2876 label: format!("{name} both {poison:e}"),
2877 current: both_current,
2878 retained: poisoned,
2879 expected: None,
2880 });
2881 }
2882 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
2883 for sign in [1.0_f32, -1.0] {
2884 let mut edge = matched;
2885 set(&mut edge, base + sign * knife);
2886 corpus.push(Case {
2887 label: format!("{name} knife edge {knife:e}, sign {sign}"),
2888 current: edge,
2889 retained,
2890 expected: None,
2891 });
2892 }
2893 }
2894 }
2895
2896 for (label, delta, expected) in [
2897 ("start_angle +TAU", TAU, RecordMatch::Exact),
2898 ("start_angle -TAU", -TAU, RecordMatch::Exact),
2899 ("start_angle +3 turns", 3.0 * TAU, RecordMatch::Exact),
2900 (
2901 "start_angle short of +TAU, inside",
2902 TAU - 0.9 * ABS_EPS,
2903 RecordMatch::Exact,
2904 ),
2905 (
2906 "start_angle past +TAU, outside",
2907 TAU + 1.1 * ABS_EPS,
2908 RecordMatch::Mismatch,
2909 ),
2910 ] {
2911 let mut wrapped = matched;
2912 wrapped.start_angle += delta;
2913 corpus.push(Case {
2914 label: label.to_string(),
2915 current: wrapped,
2916 retained,
2917 expected: Some(expected),
2918 });
2919 }
2920
2921 let stroked = arc_base(Some(5.0));
2922 let moved_stroked = arc_moved(&stroked);
2923 let mut some_vs_none = matched;
2924 some_vs_none.stroke = Some(Stroke::new(5.0 * T.scale));
2925 corpus.push(Case {
2926 label: "stroke Some vs None".to_string(),
2927 current: some_vs_none,
2928 retained,
2929 expected: Some(RecordMatch::Mismatch),
2930 });
2931 corpus.push(Case {
2932 label: "stroke None vs Some".to_string(),
2933 current: matched,
2934 retained: stroked,
2935 expected: Some(RecordMatch::Mismatch),
2936 });
2937 let width = 5.0 * T.scale;
2938 let tolerance = ABS_EPS + REL_EPS * width.abs();
2939 for sign in [1.0_f32, -1.0] {
2940 let mut inside = moved_stroked;
2941 inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
2942 corpus.push(Case {
2943 label: format!("stroke width just inside, sign {sign}"),
2944 current: inside,
2945 retained: stroked,
2946 expected: Some(RecordMatch::Exact),
2947 });
2948 let mut outside = moved_stroked;
2949 outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
2950 corpus.push(Case {
2951 label: format!("stroke width just outside, sign {sign}"),
2952 current: outside,
2953 retained: stroked,
2954 expected: Some(RecordMatch::Mismatch),
2955 });
2956 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
2957 let mut edge = moved_stroked;
2958 edge.stroke = Some(Stroke::new(width + sign * knife));
2959 corpus.push(Case {
2960 label: format!("stroke width knife edge {knife:e}, sign {sign}"),
2961 current: edge,
2962 retained: stroked,
2963 expected: None,
2964 });
2965 }
2966 }
2967 for poison in POISONS {
2968 let mut current = moved_stroked;
2969 current.stroke = Some(Stroke::new(poison));
2970 corpus.push(Case {
2971 label: format!("stroke width current {poison:e}"),
2972 current,
2973 retained: stroked,
2974 expected: None,
2975 });
2976 let mut poisoned = stroked;
2977 poisoned.stroke = Some(Stroke::new(poison));
2978 corpus.push(Case {
2979 label: format!("stroke width retained {poison:e}"),
2980 current: moved_stroked,
2981 retained: poisoned,
2982 expected: None,
2983 });
2984 }
2985
2986 let mut nan_color = matched;
2987 nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
2988 corpus.push(Case {
2989 label: "NaN color".to_string(),
2990 current: nan_color,
2991 retained,
2992 expected: Some(RecordMatch::Recolor),
2993 });
2994 corpus
2995 }
2996
2997 #[test]
2998 fn the_arc_corpus_exercises_what_it_claims() {
2999 for case in arc_corpus() {
3000 if let Some(expected) = case.expected {
3001 assert_eq!(
3002 match_arc(&case.current, &case.retained, PIVOT, T),
3003 expected,
3004 "scalar verdict for `{}`",
3005 case.label
3006 );
3007 }
3008 }
3009 }
3010
3011 #[test]
3012 fn arc_kernel_equals_the_scalar_authority_cross_paired() {
3013 let corpus = arc_corpus();
3014 for t in transforms() {
3015 for a in &corpus {
3016 for b in &corpus {
3017 assert_eq!(
3018 match_arc_lanes(&a.current, &b.retained, PIVOT, t.scale, t.angle),
3019 match_arc(&a.current, &b.retained, PIVOT, t),
3020 "arc kernel diverged: current `{}` vs retained `{}` under {t:?}",
3021 a.label,
3022 b.label
3023 );
3024 }
3025 }
3026 }
3027 }
3028
3029 #[test]
3030 fn arc_run_equals_a_scalar_reference_from_every_start() {
3031 let corpus = arc_corpus();
3032 let current: Vec<SolidArcRecord> = corpus.iter().map(|case| case.current).collect();
3033 let snapshot: Vec<SolidArcRecord> = corpus.iter().map(|case| case.retained).collect();
3034 let mut fast: Vec<(u32, Color)> = Vec::new();
3035 let mut naive: Vec<(u32, Color)> = Vec::new();
3036 for start in 0..current.len() {
3037 fast.clear();
3038 naive.clear();
3039 let matched = match_arc_run(
3040 ¤t[start..],
3041 &snapshot[start..],
3042 PIVOT,
3043 T,
3044 7,
3045 &mut fast,
3046 );
3047 let mut mismatch = None;
3048 for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3049 match match_arc(now, then, PIVOT, T) {
3050 RecordMatch::Exact => {}
3051 RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3052 RecordMatch::Mismatch => {
3053 mismatch = Some(i);
3054 break;
3055 }
3056 }
3057 }
3058 let reference = mismatch.unwrap_or(current.len() - start);
3059 assert_eq!(
3060 (matched, bits(&fast)),
3061 (reference, bits(&naive)),
3062 "arc run diverged from start {start}"
3063 );
3064 }
3065 }
3066
3067 fn rr_base(stroke: Option<f32>) -> SolidRoundRectRecord {
3068 SolidRoundRectRecord {
3069 rect: Rect {
3070 x: 299.0,
3071 y: 199.0,
3072 width: 10.0,
3073 height: 10.0,
3074 },
3075 radii: CornerRadii::uniform(5.0),
3076 color: Color::WHITE,
3077 stroke: stroke.map(Stroke::new),
3078 }
3079 }
3080
3081 fn rr_moved(retained: &SolidRoundRectRecord) -> SolidRoundRectRecord {
3082 let (c_then, d_then) = circle_view(retained).expect("the base is a circle");
3083 let c_now = T.apply(PIVOT, c_then);
3084 let d_now = d_then * T.scale;
3085 SolidRoundRectRecord {
3086 rect: Rect {
3087 x: c_now.x - d_now * 0.5,
3088 y: c_now.y - d_now * 0.5,
3089 width: d_now,
3090 height: d_now,
3091 },
3092 radii: CornerRadii::uniform(d_now * 0.5),
3093 color: retained.color,
3094 stroke: retained.stroke.map(|stroke| Stroke {
3095 width: stroke.width * T.scale,
3096 ..stroke
3097 }),
3098 }
3099 }
3100
3101 type RrGet = fn(&SolidRoundRectRecord) -> f32;
3102 type RrSet = fn(&mut SolidRoundRectRecord, f32);
3103
3104 fn rr_fields() -> [(&'static str, RrGet, RrSet); 8] {
3105 [
3106 ("rect.x", |r| r.rect.x, |r, v| r.rect.x = v),
3107 ("rect.y", |r| r.rect.y, |r, v| r.rect.y = v),
3108 ("rect.width", |r| r.rect.width, |r, v| r.rect.width = v),
3109 ("rect.height", |r| r.rect.height, |r, v| r.rect.height = v),
3110 (
3111 "radii.top_left",
3112 |r| r.radii.top_left,
3113 |r, v| r.radii.top_left = v,
3114 ),
3115 (
3116 "radii.top_right",
3117 |r| r.radii.top_right,
3118 |r, v| r.radii.top_right = v,
3119 ),
3120 (
3121 "radii.bottom_right",
3122 |r| r.radii.bottom_right,
3123 |r, v| r.radii.bottom_right = v,
3124 ),
3125 (
3126 "radii.bottom_left",
3127 |r| r.radii.bottom_left,
3128 |r, v| r.radii.bottom_left = v,
3129 ),
3130 ]
3131 }
3132
3133 fn rr_corpus() -> Vec<Case<SolidRoundRectRecord>> {
3134 let mut corpus: Vec<Case<SolidRoundRectRecord>> = Vec::new();
3135 for stroke in [None, Some(3.0_f32)] {
3136 let retained = rr_base(stroke);
3137 let matched = rr_moved(&retained);
3138 corpus.push(Case {
3139 label: format!("exact, stroke {stroke:?}"),
3140 current: matched,
3141 retained,
3142 expected: Some(RecordMatch::Exact),
3143 });
3144 let mut recolored = matched;
3145 recolored.color = Color::rgb(0.2, 0.8, 0.4);
3146 corpus.push(Case {
3147 label: format!("recolor, stroke {stroke:?}"),
3148 current: recolored,
3149 retained,
3150 expected: Some(RecordMatch::Recolor),
3151 });
3152 }
3153
3154 let retained = rr_base(None);
3155 let matched = rr_moved(&retained);
3156 for (name, get, set) in rr_fields() {
3157 let base = get(&matched);
3158 let tolerance = ABS_EPS + REL_EPS * base.abs();
3159 for sign in [1.0_f32, -1.0] {
3160 let mut inside = matched;
3161 set(&mut inside, base + sign * 0.9 * tolerance);
3162 corpus.push(Case {
3163 label: format!("{name} just inside, sign {sign}"),
3164 current: inside,
3165 retained,
3166 expected: Some(RecordMatch::Exact),
3167 });
3168 let mut outside = matched;
3169 set(&mut outside, base + sign * 1.1 * tolerance);
3170 corpus.push(Case {
3171 label: format!("{name} just outside, sign {sign}"),
3172 current: outside,
3173 retained,
3174 expected: Some(RecordMatch::Mismatch),
3175 });
3176 }
3177 for poison in POISONS {
3178 let mut current = matched;
3179 set(&mut current, poison);
3180 corpus.push(Case {
3181 label: format!("{name} current {poison:e}"),
3182 current,
3183 retained,
3184 expected: None,
3185 });
3186 let mut poisoned = retained;
3187 set(&mut poisoned, poison);
3188 corpus.push(Case {
3189 label: format!("{name} retained {poison:e}"),
3190 current: matched,
3191 retained: poisoned,
3192 expected: None,
3193 });
3194 let mut both_current = matched;
3195 set(&mut both_current, poison);
3196 corpus.push(Case {
3197 label: format!("{name} both {poison:e}"),
3198 current: both_current,
3199 retained: poisoned,
3200 expected: None,
3201 });
3202 }
3203 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3204 for sign in [1.0_f32, -1.0] {
3205 let mut edge = matched;
3206 set(&mut edge, base + sign * knife);
3207 corpus.push(Case {
3208 label: format!("{name} knife edge {knife:e}, sign {sign}"),
3209 current: edge,
3210 retained,
3211 expected: None,
3212 });
3213 }
3214 }
3215 }
3216
3217 let mut squashed = matched;
3218 squashed.rect.height = squashed.rect.width * 2.0;
3219 corpus.push(Case {
3220 label: "current non-circle (squashed)".to_string(),
3221 current: squashed,
3222 retained,
3223 expected: Some(RecordMatch::Mismatch),
3224 });
3225 let mut loose_radii = retained;
3226 loose_radii.radii = CornerRadii::uniform(2.0);
3227 corpus.push(Case {
3228 label: "retained non-circle (loose radii)".to_string(),
3229 current: matched,
3230 retained: loose_radii,
3231 expected: Some(RecordMatch::Mismatch),
3232 });
3233 corpus.push(Case {
3234 label: "non-circle vs itself".to_string(),
3235 current: loose_radii,
3236 retained: loose_radii,
3237 expected: Some(RecordMatch::Mismatch),
3238 });
3239
3240 let stroked = rr_base(Some(3.0));
3241 let moved_stroked = rr_moved(&stroked);
3242 let mut some_vs_none = matched;
3243 some_vs_none.stroke = Some(Stroke::new(3.0 * T.scale));
3244 corpus.push(Case {
3245 label: "stroke Some vs None".to_string(),
3246 current: some_vs_none,
3247 retained,
3248 expected: Some(RecordMatch::Mismatch),
3249 });
3250 corpus.push(Case {
3251 label: "stroke None vs Some".to_string(),
3252 current: matched,
3253 retained: stroked,
3254 expected: Some(RecordMatch::Mismatch),
3255 });
3256 let width = 3.0 * T.scale;
3257 let tolerance = ABS_EPS + REL_EPS * width.abs();
3258 for sign in [1.0_f32, -1.0] {
3259 let mut inside = moved_stroked;
3260 inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
3261 corpus.push(Case {
3262 label: format!("stroke width just inside, sign {sign}"),
3263 current: inside,
3264 retained: stroked,
3265 expected: Some(RecordMatch::Exact),
3266 });
3267 let mut outside = moved_stroked;
3268 outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
3269 corpus.push(Case {
3270 label: format!("stroke width just outside, sign {sign}"),
3271 current: outside,
3272 retained: stroked,
3273 expected: Some(RecordMatch::Mismatch),
3274 });
3275 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3276 let mut edge = moved_stroked;
3277 edge.stroke = Some(Stroke::new(width + sign * knife));
3278 corpus.push(Case {
3279 label: format!("stroke width knife edge {knife:e}, sign {sign}"),
3280 current: edge,
3281 retained: stroked,
3282 expected: None,
3283 });
3284 }
3285 }
3286 for poison in POISONS {
3287 let mut current = moved_stroked;
3288 current.stroke = Some(Stroke::new(poison));
3289 corpus.push(Case {
3290 label: format!("stroke width current {poison:e}"),
3291 current,
3292 retained: stroked,
3293 expected: None,
3294 });
3295 }
3296
3297 let mut nan_color = matched;
3298 nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
3299 corpus.push(Case {
3300 label: "NaN color".to_string(),
3301 current: nan_color,
3302 retained,
3303 expected: Some(RecordMatch::Recolor),
3304 });
3305 corpus
3306 }
3307
3308 #[test]
3309 fn the_round_rect_corpus_exercises_what_it_claims() {
3310 for case in rr_corpus() {
3311 if let Some(expected) = case.expected {
3312 assert_eq!(
3313 match_round_rect(&case.current, &case.retained, PIVOT, T),
3314 expected,
3315 "scalar verdict for `{}`",
3316 case.label
3317 );
3318 }
3319 }
3320 }
3321
3322 #[test]
3323 fn round_rect_kernel_equals_the_scalar_authority_cross_paired() {
3324 let corpus = rr_corpus();
3325 for t in transforms() {
3326 let (sin, cos) = t.angle.sin_cos();
3327 for a in &corpus {
3328 for b in &corpus {
3329 assert_eq!(
3330 match_round_rect_lanes(&a.current, &b.retained, PIVOT, t.scale, sin, cos),
3331 match_round_rect(&a.current, &b.retained, PIVOT, t),
3332 "round-rect kernel diverged: current `{}` vs retained `{}` under {t:?}",
3333 a.label,
3334 b.label
3335 );
3336 }
3337 }
3338 }
3339 }
3340
3341 #[test]
3342 fn round_rect_run_equals_a_scalar_reference_from_every_start() {
3343 let corpus = rr_corpus();
3344 let current: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.current).collect();
3345 let snapshot: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.retained).collect();
3346 let mut fast: Vec<(u32, Color)> = Vec::new();
3347 let mut naive: Vec<(u32, Color)> = Vec::new();
3348 for start in 0..current.len() {
3349 fast.clear();
3350 naive.clear();
3351 let matched = match_round_rect_run(
3352 ¤t[start..],
3353 &snapshot[start..],
3354 PIVOT,
3355 T,
3356 7,
3357 &mut fast,
3358 );
3359 let mut mismatch = None;
3360 for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3361 match match_round_rect(now, then, PIVOT, T) {
3362 RecordMatch::Exact => {}
3363 RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3364 RecordMatch::Mismatch => {
3365 mismatch = Some(i);
3366 break;
3367 }
3368 }
3369 }
3370 let reference = mismatch.unwrap_or(current.len() - start);
3371 assert_eq!(
3372 (matched, bits(&fast)),
3373 (reference, bits(&naive)),
3374 "round-rect run diverged from start {start}"
3375 );
3376 }
3377 }
3378
3379 struct XorShift(u32);
3380
3381 impl XorShift {
3382 fn next(&mut self) -> u32 {
3383 let mut x = self.0;
3384 x ^= x << 13;
3385 x ^= x >> 17;
3386 x ^= x << 5;
3387 self.0 = x;
3388 x
3389 }
3390
3391 fn f32(&mut self) -> f32 {
3392 if self.next() & 1 == 0 {
3393 (self.next() as f32 / u32::MAX as f32) * 1000.0 - 500.0
3394 } else {
3395 f32::from_bits(self.next())
3396 }
3397 }
3398
3399 fn stroke(&mut self) -> Option<Stroke> {
3400 (self.next() & 1 == 0).then(|| Stroke::new(self.f32()))
3401 }
3402
3403 fn arc(&mut self) -> SolidArcRecord {
3404 SolidArcRecord {
3405 center: Point::new(self.f32(), self.f32()),
3406 radius: self.f32(),
3407 start_angle: self.f32(),
3408 sweep_angle: self.f32(),
3409 inner_radius: self.f32(),
3410 color: Color::WHITE,
3411 stroke: self.stroke(),
3412 }
3413 }
3414
3415 fn round_rect(&mut self) -> SolidRoundRectRecord {
3416 SolidRoundRectRecord {
3417 rect: Rect {
3418 x: self.f32(),
3419 y: self.f32(),
3420 width: self.f32(),
3421 height: self.f32(),
3422 },
3423 radii: CornerRadii {
3424 top_left: self.f32(),
3425 top_right: self.f32(),
3426 bottom_right: self.f32(),
3427 bottom_left: self.f32(),
3428 },
3429 color: Color::WHITE,
3430 stroke: self.stroke(),
3431 }
3432 }
3433 }
3434
3435 #[test]
3436 fn kernels_equal_the_authorities_on_arbitrary_bit_patterns() {
3437 let mut rng = XorShift(0x9e37_79b9);
3438 for _ in 0..4000 {
3439 let t = RecordTransform {
3440 scale: rng.f32(),
3441 angle: rng.f32(),
3442 };
3443 let (sin, cos) = t.angle.sin_cos();
3444 let (a, b) = (rng.arc(), rng.arc());
3445 assert_eq!(
3446 match_arc_lanes(&a, &b, PIVOT, t.scale, t.angle),
3447 match_arc(&a, &b, PIVOT, t),
3448 "arc kernel diverged: {a:?} vs {b:?} under {t:?}"
3449 );
3450 let (c, d) = (rng.round_rect(), rng.round_rect());
3451 assert_eq!(
3452 match_round_rect_lanes(&c, &d, PIVOT, t.scale, sin, cos),
3453 match_round_rect(&c, &d, PIVOT, t),
3454 "round-rect kernel diverged: {c:?} vs {d:?} under {t:?}"
3455 );
3456 }
3457 }
3458}