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