1use crate::{
23 geometry::{
24 CommandRecording, Point, RecordKind, Rect, SolidArcRecord, SolidRoundRectRecord, TapeRef,
25 },
26 Color, CornerRadii,
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 if self.segments.len() >= 2 {
1170 let commit = self.verify_optimistic(current, pool);
1171 if commit.committed == self.segments.len() {
1172 self.optimistic_commits += 1;
1173 return self.finish_verify(current, commit.spans, commit.retained_records);
1174 }
1175 if commit.committed > 0 {
1182 self.prefix_commits += 1;
1183 }
1184 spans = commit.spans;
1185 retained_records = commit.retained_records;
1186 cursor = commit.cursor;
1187 committed = commit.committed;
1188 }
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 geometry::{DrawScopeDefault, Size},
2257 Brush, DrawScope as _,
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!(spans
2320 .iter()
2321 .any(|span| matches!(span, ReplaySpan::Dynamic { .. })));
2322 let transforms: Vec<RecordTransform> = spans
2324 .iter()
2325 .filter_map(|span| match span {
2326 ReplaySpan::Retained { transform, .. } => Some(*transform),
2327 _ => None,
2328 })
2329 .collect();
2330 assert!(transforms.windows(2).any(|w| w[0].angle != w[1].angle));
2331 }
2332
2333 fn flipped_ring_frame(frame: usize) -> CommandRecording {
2337 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2338 for ring in 0..4 {
2339 let rotation = (0.02 + ring as f32 * 0.007) * frame as f32;
2340 let radius = 75.0 + ring as f32 * 27.0;
2341 for slot in 0..260 {
2342 let start = slot as f32 * (std::f32::consts::TAU / 260.0) + rotation;
2343 scope.draw_annular_sector(
2344 Brush::solid(Color::WHITE),
2345 CENTER,
2346 radius * 0.75,
2347 radius,
2348 start,
2349 0.015,
2350 );
2351 }
2352 }
2353 scope.recorded().clone()
2354 }
2355
2356 #[test]
2357 fn only_a_collapse_out_of_capture_sets_the_transition_flag() {
2358 let mut state = CommandReplayState::default();
2359 assert!(matches!(
2361 state.advance(&ring_frame(3, 300, 0, 10)),
2362 ReplayOutcome::AllDynamic
2363 ));
2364 assert!(!state.collapsed_from_captured());
2365 assert!(matches!(
2367 state.advance(&ring_frame(3, 300, 1, 10)),
2368 ReplayOutcome::Spans(_)
2369 ));
2370 assert!(!state.collapsed_from_captured());
2371 assert!(matches!(
2373 state.advance(&ring_frame(3, 300, 2, 10)),
2374 ReplayOutcome::Spans(_)
2375 ));
2376 assert!(!state.collapsed_from_captured());
2377 assert!(matches!(
2382 state.advance(&flipped_ring_frame(3)),
2383 ReplayOutcome::AllDynamic
2384 ));
2385 assert!(state.collapsed_from_captured());
2386 let _ = state.advance(&flipped_ring_frame(4));
2388 assert!(!state.collapsed_from_captured());
2389 let _ = state.advance(&flipped_ring_frame(5));
2392 let short = ring_frame(1, 40, 0, 0);
2393 assert!(short.len() < MIN_REPLAY_COMMAND_RECORDS);
2394 assert!(matches!(state.advance(&short), ReplayOutcome::AllDynamic));
2395 assert!(!state.collapsed_from_captured());
2396 }
2397
2398 #[test]
2399 fn entity_churn_between_frames_still_retains_rings() {
2400 let mut state = CommandReplayState::default();
2401 state.advance(&ring_frame(2, 400, 0, 8));
2402 state.advance(&ring_frame(2, 400, 1, 13)); let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(2, 400, 2, 5)) else {
2404 panic!("churned tail must not break ring retention");
2405 };
2406 let retained_records: usize = spans
2407 .iter()
2408 .filter_map(|span| match span {
2409 ReplaySpan::Retained { .. } => Some(1),
2410 _ => None,
2411 })
2412 .sum();
2413 assert!(retained_records >= 2);
2414 }
2415
2416 #[test]
2417 fn recolors_are_patches_not_mismatches() {
2418 let recolored_frame = |frame: usize| {
2419 let mut recording = ring_frame(1, 600, frame, 0);
2420 for i in (0..recording.arcs.len()).step_by(15) {
2422 recording.arcs[i].color = if frame.is_multiple_of(2) {
2423 Color::rgb(1.0, 0.5, 0.1)
2424 } else {
2425 Color::rgb(0.1, 0.5, 1.0)
2426 };
2427 }
2428 recording
2429 };
2430 let mut state = CommandReplayState::default();
2431 state.advance(&recolored_frame(0));
2432 state.advance(&recolored_frame(1));
2433 let ReplayOutcome::Spans(spans) = state.advance(&recolored_frame(2)) else {
2434 panic!("twinkles must not break retention");
2435 };
2436 let recolor_count: usize = spans
2437 .iter()
2438 .filter_map(|span| match span {
2439 ReplaySpan::Retained { recolors, .. } => Some(recolors.len()),
2440 _ => None,
2441 })
2442 .sum();
2443 assert!(recolor_count >= 30, "twinkles surface as patches");
2444 }
2445
2446 #[test]
2447 fn geometry_change_kills_only_its_segment() {
2448 let mut state = CommandReplayState::default();
2449 state.advance(&ring_frame(3, 300, 0, 0));
2450 state.advance(&ring_frame(3, 300, 1, 0));
2451 let mut broken = ring_frame(3, 300, 2, 0);
2452 broken.arcs[450].sweep_angle *= 3.0;
2454 let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2455 panic!("one changed entry must not drop the whole command");
2456 };
2457 let retained: usize = spans
2458 .iter()
2459 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2460 .count();
2461 assert!(
2462 retained >= 2,
2463 "the untouched rings keep retaining, got {spans:?}"
2464 );
2465 }
2466
2467 #[test]
2468 fn mid_segment_change_splits_and_retains_both_halves() {
2469 let mut state = CommandReplayState::default();
2470 state.advance(&ring_frame(1, 900, 0, 0));
2471 state.advance(&ring_frame(1, 900, 1, 0));
2472 assert_eq!(state.segments().len(), 1, "one ring is one segment");
2473 let mut broken = ring_frame(1, 900, 2, 0);
2474 broken.arcs[450].sweep_angle *= 3.0;
2475 let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2476 panic!("a single changed record must not drop retention");
2477 };
2478 let dynamic: usize = spans
2479 .iter()
2480 .filter_map(|span| match span {
2481 ReplaySpan::Dynamic {
2482 tape_start,
2483 tape_end,
2484 } => Some(tape_end - tape_start),
2485 _ => None,
2486 })
2487 .sum();
2488 let retained: Vec<(u32, usize, bool)> = spans
2489 .iter()
2490 .filter_map(|span| match span {
2491 ReplaySpan::Retained {
2492 slot,
2493 slot_offset,
2494 capture,
2495 ..
2496 } => Some((*slot, *slot_offset, *capture)),
2497 _ => None,
2498 })
2499 .collect();
2500 assert_eq!(
2501 retained.len(),
2502 2,
2503 "prefix and suffix both retain: {spans:?}"
2504 );
2505 assert_eq!(retained[0].0, retained[1].0);
2509 assert_eq!(retained[0].1, 0);
2510 assert_eq!(retained[1].1, 451);
2511 assert!(retained.iter().all(|(_, _, capture)| !capture));
2512 assert_eq!(dynamic, 1, "only the changed record goes dynamic");
2513 assert_eq!(state.stats(), (0, 1), "one split, no deaths");
2514
2515 let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(1, 900, 3, 0)) else {
2518 panic!("split pieces must keep retaining");
2519 };
2520 let retained = spans
2521 .iter()
2522 .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2523 .count();
2524 assert_eq!(retained, 2, "both pieces relocate next frame: {spans:?}");
2525 }
2526
2527 #[test]
2528 fn erosion_recaptures_dead_ranges_after_the_cooldown() {
2529 let mut state = CommandReplayState::default();
2530 state.advance(&ring_frame(3, 300, 0, 0));
2531 state.advance(&ring_frame(3, 300, 1, 0));
2532 let mutated = |frame: usize| {
2535 let mut recording = ring_frame(3, 300, frame, 0);
2536 for arc in &mut recording.arcs[300..600] {
2537 arc.sweep_angle *= 3.0;
2538 }
2539 recording
2540 };
2541 let dynamic_records = |outcome: &ReplayOutcome| -> usize {
2542 match outcome {
2543 ReplayOutcome::AllDynamic => usize::MAX,
2544 ReplayOutcome::Spans(spans) => spans
2545 .iter()
2546 .filter_map(|span| match span {
2547 ReplaySpan::Dynamic {
2548 tape_start,
2549 tape_end,
2550 } => Some(tape_end - tape_start),
2551 _ => None,
2552 })
2553 .sum(),
2554 }
2555 };
2556 let after_death = state.advance(&mutated(2));
2557 let lost = dynamic_records(&after_death);
2558 assert!(
2559 (250..=400).contains(&lost),
2560 "the changed ring goes dynamic, got {lost}"
2561 );
2562 for frame in 3..(3 + RECAPTURE_COOLDOWN_FRAMES as usize + 4) {
2563 state.advance(&mutated(frame));
2564 }
2565 let recovered = state.advance(&mutated(200));
2566 let residue = dynamic_records(&recovered);
2567 assert!(
2568 residue < 50,
2569 "the recapture watches the ring's new shape, got {residue} dynamic"
2570 );
2571 }
2572
2573 #[test]
2574 fn small_commands_are_not_watched() {
2575 let mut state = CommandReplayState::default();
2576 for frame in 0..4 {
2577 assert!(matches!(
2578 state.advance(&ring_frame(1, 40, frame, 0)),
2579 ReplayOutcome::AllDynamic
2580 ));
2581 }
2582 assert!(state.segments().is_empty());
2583 }
2584
2585 #[allow(clippy::too_many_arguments)]
2590 fn match_span_reference(
2591 current: TypedRecords<'_>,
2592 snapshot: TypedRecords<'_>,
2593 center: Point,
2594 start: usize,
2595 seg_start: usize,
2596 len: usize,
2597 t: RecordTransform,
2598 recolors: &mut Vec<(u32, Color)>,
2599 ) -> usize {
2600 recolors.clear();
2601 for offset in 0..len {
2602 let entry_match = match (
2603 current.view_at(start + offset),
2604 snapshot.view_at(seg_start + offset),
2605 ) {
2606 (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
2607 match_arc(¤t.arcs[i], &snapshot.arcs[j], center, t)
2608 }
2609 (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
2610 match_round_rect(¤t.round_rects[i], &snapshot.round_rects[j], center, t)
2611 }
2612 _ => RecordMatch::Mismatch,
2613 };
2614 match entry_match {
2615 RecordMatch::Exact => {}
2616 RecordMatch::Recolor => {
2617 let color = match current.view_at(start + offset) {
2618 Some(ReplayView::Arc(a)) => current.arcs[a].color,
2619 Some(ReplayView::RoundRect(r)) => current.round_rects[r].color,
2620 None => unreachable!("recolor requires a view"),
2621 };
2622 recolors.push((offset as u32, color));
2623 }
2624 RecordMatch::Mismatch => return offset,
2625 }
2626 }
2627 len
2628 }
2629
2630 fn mixed_frame(t: RecordTransform, recolored: bool) -> CommandRecording {
2642 let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2643 for slot in 0..8 {
2644 let color = if recolored && slot == 3 {
2645 Color::rgb(1.0, 0.5, 0.1)
2646 } else {
2647 Color::WHITE
2648 };
2649 scope.draw_annular_sector(
2650 Brush::solid(color),
2651 CENTER,
2652 80.0 * t.scale * 0.8,
2653 80.0 * t.scale,
2654 slot as f32 * 0.7 + t.angle,
2655 0.02,
2656 );
2657 }
2658 scope.draw_round_rect_at(
2659 Rect {
2660 x: 10.0,
2661 y: 10.0,
2662 width: 40.0,
2663 height: 20.0,
2664 },
2665 Brush::solid(Color::WHITE),
2666 CornerRadii::uniform(4.0),
2667 );
2668 scope.draw_rect_at(
2669 Rect {
2670 x: 60.0,
2671 y: 10.0,
2672 width: 20.0,
2673 height: 20.0,
2674 },
2675 Brush::solid(Color::WHITE),
2676 );
2677 scope.draw_rect_at(
2678 Rect {
2679 x: 90.0,
2680 y: 10.0,
2681 width: 20.0,
2682 height: 20.0,
2683 },
2684 Brush::linear_gradient(vec![Color::WHITE, Color::RED]),
2685 );
2686 for slot in 0..3 {
2687 let base = Point::new(304.0, 204.0 + slot as f32 * 20.0);
2688 let color = if recolored && slot == 1 {
2689 Color::rgb(0.1, 0.5, 1.0)
2690 } else {
2691 Color::WHITE
2692 };
2693 scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 5.0 * t.scale);
2694 }
2695 for slot in 0..6 {
2696 let color = if recolored && slot == 1 {
2697 Color::rgb(0.9, 0.2, 0.4)
2698 } else {
2699 Color::WHITE
2700 };
2701 scope.draw_annular_sector(
2702 Brush::solid(color),
2703 CENTER,
2704 120.0 * t.scale * 0.8,
2705 120.0 * t.scale,
2706 slot as f32 * 0.9 + 0.1 + t.angle,
2707 0.03,
2708 );
2709 }
2710 for slot in 0..2 {
2711 let base = Point::new(104.0, 204.0 + slot as f32 * 24.0);
2712 let color = if recolored && slot == 1 {
2713 Color::rgb(0.2, 0.9, 0.3)
2714 } else {
2715 Color::WHITE
2716 };
2717 scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 4.0 * t.scale);
2718 }
2719 scope.recorded().clone()
2720 }
2721
2722 #[test]
2723 fn interleaved_tape_decomposes_into_exact_runs() {
2724 let recording = mixed_frame(RecordTransform::IDENTITY, false);
2725 let tape = &recording.tape;
2726 let mut runs: Vec<(RecordKind, usize, usize)> = Vec::new();
2727 let mut at = 0usize;
2728 while at < tape.len() {
2729 let len = typed_run_len(tape, at);
2730 runs.push((tape[at].kind(), tape[at].index(), len));
2731 at += len;
2732 }
2733 assert_eq!(
2734 runs,
2735 vec![
2736 (RecordKind::SolidArc, 0, 8),
2737 (RecordKind::SolidRoundRect, 0, 1),
2738 (RecordKind::SolidRect, 0, 1),
2739 (RecordKind::Other, 0, 1),
2740 (RecordKind::SolidRoundRect, 1, 3),
2741 (RecordKind::SolidArc, 8, 6),
2742 (RecordKind::SolidRoundRect, 4, 2),
2743 ],
2744 "run decomposition must cut exactly at kind transitions"
2745 );
2746 assert_eq!(typed_run_len(tape, 3), 5);
2750 assert_eq!(typed_run_len(tape, 8), 1);
2751 assert_eq!(typed_run_len(tape, 12), 2);
2752 assert_eq!(typed_run_len(tape, 14), 6);
2753 assert_eq!(typed_run_len(tape, 20), 2);
2754 }
2755
2756 #[test]
2757 fn run_decomposed_span_match_equals_the_per_entry_walk() {
2758 let t = RecordTransform {
2759 scale: 0.9994,
2760 angle: 0.0123,
2761 };
2762 let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2763 let mut current_rec = mixed_frame(t, true);
2764 current_rec.arcs[11].sweep_angle *= 3.0;
2768 current_rec.arcs[12].start_angle = f32::NAN;
2769 let current = TypedRecords::from(¤t_rec);
2770 let snapshot = TypedRecords::from(&snapshot_rec);
2771 let n = current_rec.tape.len();
2772 assert_eq!(n, snapshot_rec.tape.len());
2773 assert_eq!(n, 22);
2774 let mut fast: Vec<(u32, Color)> = Vec::new();
2775 let mut naive: Vec<(u32, Color)> = Vec::new();
2776 for start in 0..n {
2780 for seg_start in 0..n {
2781 let longest = n - start.max(seg_start);
2782 for len in [0usize, 1, 2, 5, longest] {
2783 if start + len > n || seg_start + len > n {
2784 continue;
2785 }
2786 let matched = match_span(
2787 current, snapshot, CENTER, start, seg_start, len, t, &mut fast,
2788 );
2789 let reference = match_span_reference(
2790 current, snapshot, CENTER, start, seg_start, len, t, &mut naive,
2791 );
2792 assert_eq!(
2793 (matched, &fast),
2794 (reference, &naive),
2795 "diverged at start={start} seg_start={seg_start} len={len}"
2796 );
2797 }
2798 }
2799 }
2800 let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2810 assert_eq!(
2811 (matched, fast.as_slice()),
2812 (8, &[(3, Color::rgb(1.0, 0.5, 0.1))][..])
2813 );
2814 let matched = match_span(current, snapshot, CENTER, 11, 11, 3, t, &mut fast);
2815 assert_eq!(
2816 (matched, fast.as_slice()),
2817 (3, &[(1, Color::rgb(0.1, 0.5, 1.0))][..])
2818 );
2819 let matched = match_span(current, snapshot, CENTER, 11, 11, 9, t, &mut fast);
2820 assert_eq!(
2821 (matched, fast.as_slice()),
2822 (
2823 6,
2824 &[
2825 (1, Color::rgb(0.1, 0.5, 1.0)),
2826 (4, Color::rgb(0.9, 0.2, 0.4)),
2827 ][..]
2828 ),
2829 "the changed arc ends the clean prefix behind the circles"
2830 );
2831 let matched = match_span(current, snapshot, CENTER, 19, 19, 3, t, &mut fast);
2832 assert_eq!(
2833 (matched, fast.as_slice()),
2834 (3, &[(2, Color::rgb(0.2, 0.9, 0.3))][..])
2835 );
2836 }
2837
2838 #[test]
2839 fn nan_records_mismatch_through_both_paths() {
2840 let t = RecordTransform::IDENTITY;
2841 let base = arc(80.0, 0.2, Color::WHITE);
2842 let poisoned_arcs: [fn(&mut SolidArcRecord); 6] = [
2843 |a| a.center.x = f32::NAN,
2844 |a| a.center.y = f32::NAN,
2845 |a| a.radius = f32::NAN,
2846 |a| a.inner_radius = f32::NAN,
2847 |a| a.start_angle = f32::NAN,
2848 |a| a.sweep_angle = f32::NAN,
2849 ];
2850 for poison in poisoned_arcs {
2851 let mut poisoned = base;
2852 poison(&mut poisoned);
2853 assert_eq!(
2854 match_arc(&poisoned, &base, CENTER, t),
2855 RecordMatch::Mismatch
2856 );
2857 assert_eq!(
2858 match_arc(&base, &poisoned, CENTER, t),
2859 RecordMatch::Mismatch
2860 );
2861 }
2862 let good = circle(304.0, 204.0, 10.0, Color::WHITE);
2866 let poisoned_circles: [fn(&mut SolidRoundRectRecord); 2] =
2867 [|r| r.rect.x = f32::NAN, |r| r.rect.width = f32::NAN];
2868 for poison in poisoned_circles {
2869 let mut poisoned = good;
2870 poison(&mut poisoned);
2871 assert_eq!(
2872 match_round_rect(&poisoned, &good, CENTER, t),
2873 RecordMatch::Mismatch
2874 );
2875 assert_eq!(
2876 match_round_rect(&good, &poisoned, CENTER, t),
2877 RecordMatch::Mismatch
2878 );
2879 }
2880 let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2884 let mut current_rec = mixed_frame(RecordTransform::IDENTITY, false);
2885 current_rec.arcs[4].sweep_angle = f32::NAN;
2886 let current = TypedRecords::from(¤t_rec);
2887 let snapshot = TypedRecords::from(&snapshot_rec);
2888 let mut fast: Vec<(u32, Color)> = Vec::new();
2889 let mut naive: Vec<(u32, Color)> = Vec::new();
2890 let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2891 let reference = match_span_reference(current, snapshot, CENTER, 0, 0, 8, t, &mut naive);
2892 assert_eq!(matched, 4, "the NaN record is a mismatch, not a match");
2893 assert_eq!((matched, &fast), (reference, &naive));
2894 }
2895
2896 struct ThreadedExec {
2900 lanes: usize,
2901 }
2902
2903 impl VerifyExecutor for ThreadedExec {
2904 fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync)) {
2905 std::thread::scope(|s| {
2906 for lane in 1..self.lanes {
2907 s.spawn(move || {
2908 let mut i = lane;
2909 while i < jobs {
2910 run(i);
2911 i += self.lanes;
2912 }
2913 });
2914 }
2915 let mut i = 0;
2916 while i < jobs {
2917 run(i);
2918 i += self.lanes;
2919 }
2920 });
2921 }
2922 }
2923
2924 #[test]
2925 fn pooled_verification_matches_serial_exactly() {
2926 let exec = ThreadedExec { lanes: 3 };
2927 let frame = |f: usize| -> CommandRecording {
2935 let tail = [10usize, 13, 5, 8, 11, 6, 9, 12][f % 8];
2936 let mut recording = ring_frame(3, 300, f, tail);
2937 if f >= 3 {
2938 for i in (0..recording.arcs.len()).step_by(17) {
2939 recording.arcs[i].color = if f.is_multiple_of(2) {
2940 Color::rgb(1.0, 0.5, 0.1)
2941 } else {
2942 Color::rgb(0.1, 0.5, 1.0)
2943 };
2944 }
2945 }
2946 match f {
2947 5 => {
2948 recording.arcs[450].sweep_angle = 0.15;
2952 }
2953 8 => {
2954 recording.arcs[100].sweep_angle = 0.15;
2958 recording.arcs[750].sweep_angle = 0.15;
2959 }
2960 12 => {
2961 recording.arcs[500].sweep_angle = 0.15;
2963 }
2964 16..=39 => {
2965 for arc in &mut recording.arcs[600..900] {
2971 arc.sweep_angle = 0.06;
2972 }
2973 }
2974 40..=45 => {
2975 for arc in &mut recording.arcs[150..900] {
2979 arc.sweep_angle = 0.08;
2980 }
2981 }
2982 52 => {
2983 recording.arcs[450].sweep_angle = 0.15;
2985 }
2986 _ => {}
2987 }
2988 recording
2989 };
2990 let mut serial = CommandReplayState::default();
2991 let mut pooled = CommandReplayState::default();
2992 for f in 0..60 {
2993 let recording = frame(f);
2994 let serial_outcome = serial.advance(&recording);
2995 let pooled_outcome = pooled.advance_pooled(&recording, Some(&exec));
2996 assert_eq!(
2997 serial_outcome, pooled_outcome,
2998 "outcome diverged at frame {f}"
2999 );
3000 assert_eq!(
3001 serial.segments(),
3002 pooled.segments(),
3003 "segments diverged at frame {f}"
3004 );
3005 assert_eq!(
3006 serial.stats(),
3007 pooled.stats(),
3008 "stats diverged at frame {f}"
3009 );
3010 }
3011 let (deaths, splits) = serial.stats();
3012 assert!(
3013 !serial.segments().is_empty() && deaths > 0 && splits > 0,
3014 "sequence must exercise retention, deaths, and splits, \
3015 got {deaths} deaths {splits} splits {} segments",
3016 serial.segments().len()
3017 );
3018 assert_eq!(serial.optimistic_commits(), 0);
3019 assert_eq!(serial.prefix_commits(), 0);
3020 assert!(
3021 pooled.optimistic_commits() >= 10,
3022 "the pooled fast path must actually commit steady frames, got {}",
3023 pooled.optimistic_commits()
3024 );
3025 assert!(
3026 pooled.prefix_commits() >= 3,
3027 "churn frames must commit their pooled prefix, got {}",
3028 pooled.prefix_commits()
3029 );
3030 }
3031
3032 #[test]
3033 fn transformed_bounds_contain_the_moved_content() {
3034 let t = RecordTransform {
3035 scale: 1.1,
3036 angle: 0.5,
3037 };
3038 let bounds = Rect {
3039 x: 150.0,
3040 y: 150.0,
3041 width: 100.0,
3042 height: 30.0,
3043 };
3044 let moved = t.apply_to_bounds(CENTER, bounds);
3045 for corner in [
3046 Point::new(bounds.x, bounds.y),
3047 Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
3048 ] {
3049 let p = t.apply(CENTER, corner);
3050 assert!(p.x >= moved.x - 1e-3 && p.x <= moved.x + moved.width + 1e-3);
3051 assert!(p.y >= moved.y - 1e-3 && p.y <= moved.y + moved.height + 1e-3);
3052 }
3053 }
3054}
3055
3056#[cfg(test)]
3073mod lane_kernel_equivalence {
3074 use std::f32::consts::TAU;
3075
3076 use super::*;
3077 use crate::{Color, Stroke};
3078
3079 const KNIFE: f32 = 5.0e-4;
3084
3085 const PIVOT: Point = Point { x: 204.0, y: 204.0 };
3086 const T: RecordTransform = RecordTransform {
3087 scale: 0.9994,
3088 angle: 0.0123,
3089 };
3090 const DENORMAL: f32 = 1.0e-40;
3092 const POISONS: [f32; 4] = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, DENORMAL];
3093
3094 fn transforms() -> [RecordTransform; 5] {
3095 [
3096 RecordTransform::IDENTITY,
3097 T,
3098 RecordTransform {
3099 scale: 1.37,
3100 angle: 3.0,
3101 },
3102 RecordTransform {
3103 scale: f32::NAN,
3104 angle: f32::NAN,
3105 },
3106 RecordTransform {
3107 scale: f32::INFINITY,
3108 angle: 0.0,
3109 },
3110 ]
3111 }
3112
3113 fn bits(recolors: &[(u32, Color)]) -> Vec<(u32, [u32; 4])> {
3117 recolors
3118 .iter()
3119 .map(|&(i, Color(r, g, b, a))| {
3120 (i, [r.to_bits(), g.to_bits(), b.to_bits(), a.to_bits()])
3121 })
3122 .collect()
3123 }
3124
3125 struct Case<R> {
3126 label: String,
3127 current: R,
3128 retained: R,
3129 expected: Option<RecordMatch>,
3134 }
3135
3136 fn arc_base(stroke: Option<f32>) -> SolidArcRecord {
3139 SolidArcRecord {
3140 center: PIVOT,
3141 radius: 120.0,
3142 start_angle: 1.0,
3143 sweep_angle: 0.4,
3144 inner_radius: 96.0,
3145 color: Color::WHITE,
3146 stroke: stroke.map(Stroke::new),
3147 }
3148 }
3149
3150 fn arc_moved(retained: &SolidArcRecord) -> SolidArcRecord {
3153 SolidArcRecord {
3154 center: retained.center,
3155 radius: retained.radius * T.scale,
3156 start_angle: retained.start_angle + T.angle,
3157 sweep_angle: retained.sweep_angle,
3158 inner_radius: retained.inner_radius * T.scale,
3159 color: retained.color,
3160 stroke: retained.stroke.map(|stroke| Stroke {
3161 width: stroke.width * T.scale,
3162 ..stroke
3163 }),
3164 }
3165 }
3166
3167 type ArcGet = fn(&SolidArcRecord) -> f32;
3168 type ArcSet = fn(&mut SolidArcRecord, f32);
3169
3170 fn arc_fields() -> [(&'static str, ArcGet, ArcSet); 6] {
3171 [
3172 ("center.x", |a| a.center.x, |a, v| a.center.x = v),
3173 ("center.y", |a| a.center.y, |a, v| a.center.y = v),
3174 ("radius", |a| a.radius, |a, v| a.radius = v),
3175 (
3176 "inner_radius",
3177 |a| a.inner_radius,
3178 |a, v| a.inner_radius = v,
3179 ),
3180 ("start_angle", |a| a.start_angle, |a, v| a.start_angle = v),
3181 ("sweep_angle", |a| a.sweep_angle, |a, v| a.sweep_angle = v),
3182 ]
3183 }
3184
3185 fn arc_corpus() -> Vec<Case<SolidArcRecord>> {
3186 let mut corpus: Vec<Case<SolidArcRecord>> = Vec::new();
3187 for stroke in [None, Some(5.0_f32)] {
3188 let retained = arc_base(stroke);
3189 let matched = arc_moved(&retained);
3190 corpus.push(Case {
3191 label: format!("exact, stroke {stroke:?}"),
3192 current: matched,
3193 retained,
3194 expected: Some(RecordMatch::Exact),
3195 });
3196 let mut recolored = matched;
3197 recolored.color = Color::rgb(0.9, 0.3, 0.2);
3198 corpus.push(Case {
3199 label: format!("recolor, stroke {stroke:?}"),
3200 current: recolored,
3201 retained,
3202 expected: Some(RecordMatch::Recolor),
3203 });
3204 }
3205
3206 let retained = arc_base(None);
3207 let matched = arc_moved(&retained);
3208 for (name, get, set) in arc_fields() {
3209 let base = get(&matched);
3210 let tolerance = if name == "start_angle" {
3215 ABS_EPS
3216 } else {
3217 ABS_EPS + REL_EPS * base.abs()
3218 };
3219 for sign in [1.0_f32, -1.0] {
3220 let mut inside = matched;
3221 set(&mut inside, base + sign * 0.9 * tolerance);
3222 corpus.push(Case {
3223 label: format!("{name} just inside, sign {sign}"),
3224 current: inside,
3225 retained,
3226 expected: Some(RecordMatch::Exact),
3227 });
3228 let mut outside = matched;
3229 set(&mut outside, base + sign * 1.1 * tolerance);
3230 corpus.push(Case {
3231 label: format!("{name} just outside, sign {sign}"),
3232 current: outside,
3233 retained,
3234 expected: Some(RecordMatch::Mismatch),
3235 });
3236 }
3237 for poison in POISONS {
3238 let mut current = matched;
3239 set(&mut current, poison);
3240 corpus.push(Case {
3241 label: format!("{name} current {poison:e}"),
3242 current,
3243 retained,
3244 expected: None,
3245 });
3246 let mut poisoned = retained;
3247 set(&mut poisoned, poison);
3248 corpus.push(Case {
3249 label: format!("{name} retained {poison:e}"),
3250 current: matched,
3251 retained: poisoned,
3252 expected: None,
3253 });
3254 let mut both_current = matched;
3257 set(&mut both_current, poison);
3258 corpus.push(Case {
3259 label: format!("{name} both {poison:e}"),
3260 current: both_current,
3261 retained: poisoned,
3262 expected: None,
3263 });
3264 }
3265 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3273 for sign in [1.0_f32, -1.0] {
3274 let mut edge = matched;
3275 set(&mut edge, base + sign * knife);
3276 corpus.push(Case {
3277 label: format!("{name} knife edge {knife:e}, sign {sign}"),
3278 current: edge,
3279 retained,
3280 expected: None,
3281 });
3282 }
3283 }
3284 }
3285
3286 for (label, delta, expected) in [
3289 ("start_angle +TAU", TAU, RecordMatch::Exact),
3290 ("start_angle -TAU", -TAU, RecordMatch::Exact),
3291 ("start_angle +3 turns", 3.0 * TAU, RecordMatch::Exact),
3292 (
3293 "start_angle short of +TAU, inside",
3294 TAU - 0.9 * ABS_EPS,
3295 RecordMatch::Exact,
3296 ),
3297 (
3298 "start_angle past +TAU, outside",
3299 TAU + 1.1 * ABS_EPS,
3300 RecordMatch::Mismatch,
3301 ),
3302 ] {
3303 let mut wrapped = matched;
3304 wrapped.start_angle += delta;
3305 corpus.push(Case {
3306 label: label.to_string(),
3307 current: wrapped,
3308 retained,
3309 expected: Some(expected),
3310 });
3311 }
3312
3313 let stroked = arc_base(Some(5.0));
3315 let moved_stroked = arc_moved(&stroked);
3316 let mut some_vs_none = matched;
3317 some_vs_none.stroke = Some(Stroke::new(5.0 * T.scale));
3318 corpus.push(Case {
3319 label: "stroke Some vs None".to_string(),
3320 current: some_vs_none,
3321 retained,
3322 expected: Some(RecordMatch::Mismatch),
3323 });
3324 corpus.push(Case {
3325 label: "stroke None vs Some".to_string(),
3326 current: matched,
3327 retained: stroked,
3328 expected: Some(RecordMatch::Mismatch),
3329 });
3330 let width = 5.0 * T.scale;
3331 let tolerance = ABS_EPS + REL_EPS * width.abs();
3332 for sign in [1.0_f32, -1.0] {
3333 let mut inside = moved_stroked;
3334 inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
3335 corpus.push(Case {
3336 label: format!("stroke width just inside, sign {sign}"),
3337 current: inside,
3338 retained: stroked,
3339 expected: Some(RecordMatch::Exact),
3340 });
3341 let mut outside = moved_stroked;
3342 outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
3343 corpus.push(Case {
3344 label: format!("stroke width just outside, sign {sign}"),
3345 current: outside,
3346 retained: stroked,
3347 expected: Some(RecordMatch::Mismatch),
3348 });
3349 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3350 let mut edge = moved_stroked;
3351 edge.stroke = Some(Stroke::new(width + sign * knife));
3352 corpus.push(Case {
3353 label: format!("stroke width knife edge {knife:e}, sign {sign}"),
3354 current: edge,
3355 retained: stroked,
3356 expected: None,
3357 });
3358 }
3359 }
3360 for poison in POISONS {
3361 let mut current = moved_stroked;
3362 current.stroke = Some(Stroke::new(poison));
3363 corpus.push(Case {
3364 label: format!("stroke width current {poison:e}"),
3365 current,
3366 retained: stroked,
3367 expected: None,
3368 });
3369 let mut poisoned = stroked;
3370 poisoned.stroke = Some(Stroke::new(poison));
3371 corpus.push(Case {
3372 label: format!("stroke width retained {poison:e}"),
3373 current: moved_stroked,
3374 retained: poisoned,
3375 expected: None,
3376 });
3377 }
3378
3379 let mut nan_color = matched;
3382 nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
3383 corpus.push(Case {
3384 label: "NaN color".to_string(),
3385 current: nan_color,
3386 retained,
3387 expected: Some(RecordMatch::Recolor),
3388 });
3389 corpus
3390 }
3391
3392 #[test]
3393 fn the_arc_corpus_exercises_what_it_claims() {
3394 for case in arc_corpus() {
3395 if let Some(expected) = case.expected {
3396 assert_eq!(
3397 match_arc(&case.current, &case.retained, PIVOT, T),
3398 expected,
3399 "scalar verdict for `{}`",
3400 case.label
3401 );
3402 }
3403 }
3404 }
3405
3406 #[test]
3407 fn arc_kernel_equals_the_scalar_authority_cross_paired() {
3408 let corpus = arc_corpus();
3409 for t in transforms() {
3410 for a in &corpus {
3411 for b in &corpus {
3412 assert_eq!(
3413 match_arc_lanes(&a.current, &b.retained, PIVOT, t.scale, t.angle),
3414 match_arc(&a.current, &b.retained, PIVOT, t),
3415 "arc kernel diverged: current `{}` vs retained `{}` under {t:?}",
3416 a.label,
3417 b.label
3418 );
3419 }
3420 }
3421 }
3422 }
3423
3424 #[test]
3425 fn arc_run_equals_a_scalar_reference_from_every_start() {
3426 let corpus = arc_corpus();
3427 let current: Vec<SolidArcRecord> = corpus.iter().map(|case| case.current).collect();
3428 let snapshot: Vec<SolidArcRecord> = corpus.iter().map(|case| case.retained).collect();
3429 let mut fast: Vec<(u32, Color)> = Vec::new();
3430 let mut naive: Vec<(u32, Color)> = Vec::new();
3431 for start in 0..current.len() {
3432 fast.clear();
3433 naive.clear();
3434 let matched = match_arc_run(
3435 ¤t[start..],
3436 &snapshot[start..],
3437 PIVOT,
3438 T,
3439 7,
3440 &mut fast,
3441 );
3442 let mut mismatch = None;
3443 for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3444 match match_arc(now, then, PIVOT, T) {
3445 RecordMatch::Exact => {}
3446 RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3447 RecordMatch::Mismatch => {
3448 mismatch = Some(i);
3449 break;
3450 }
3451 }
3452 }
3453 let reference = mismatch.unwrap_or(current.len() - start);
3454 assert_eq!(
3455 (matched, bits(&fast)),
3456 (reference, bits(&naive)),
3457 "arc run diverged from start {start}"
3458 );
3459 }
3460 }
3461
3462 fn rr_base(stroke: Option<f32>) -> SolidRoundRectRecord {
3465 SolidRoundRectRecord {
3468 rect: Rect {
3469 x: 299.0,
3470 y: 199.0,
3471 width: 10.0,
3472 height: 10.0,
3473 },
3474 radii: CornerRadii::uniform(5.0),
3475 color: Color::WHITE,
3476 stroke: stroke.map(Stroke::new),
3477 }
3478 }
3479
3480 fn rr_moved(retained: &SolidRoundRectRecord) -> SolidRoundRectRecord {
3484 let (c_then, d_then) = circle_view(retained).expect("the base is a circle");
3485 let c_now = T.apply(PIVOT, c_then);
3486 let d_now = d_then * T.scale;
3487 SolidRoundRectRecord {
3488 rect: Rect {
3489 x: c_now.x - d_now * 0.5,
3490 y: c_now.y - d_now * 0.5,
3491 width: d_now,
3492 height: d_now,
3493 },
3494 radii: CornerRadii::uniform(d_now * 0.5),
3495 color: retained.color,
3496 stroke: retained.stroke.map(|stroke| Stroke {
3497 width: stroke.width * T.scale,
3498 ..stroke
3499 }),
3500 }
3501 }
3502
3503 type RrGet = fn(&SolidRoundRectRecord) -> f32;
3504 type RrSet = fn(&mut SolidRoundRectRecord, f32);
3505
3506 fn rr_fields() -> [(&'static str, RrGet, RrSet); 8] {
3507 [
3508 ("rect.x", |r| r.rect.x, |r, v| r.rect.x = v),
3509 ("rect.y", |r| r.rect.y, |r, v| r.rect.y = v),
3510 ("rect.width", |r| r.rect.width, |r, v| r.rect.width = v),
3511 ("rect.height", |r| r.rect.height, |r, v| r.rect.height = v),
3512 (
3513 "radii.top_left",
3514 |r| r.radii.top_left,
3515 |r, v| r.radii.top_left = v,
3516 ),
3517 (
3518 "radii.top_right",
3519 |r| r.radii.top_right,
3520 |r, v| r.radii.top_right = v,
3521 ),
3522 (
3523 "radii.bottom_right",
3524 |r| r.radii.bottom_right,
3525 |r, v| r.radii.bottom_right = v,
3526 ),
3527 (
3528 "radii.bottom_left",
3529 |r| r.radii.bottom_left,
3530 |r, v| r.radii.bottom_left = v,
3531 ),
3532 ]
3533 }
3534
3535 fn rr_corpus() -> Vec<Case<SolidRoundRectRecord>> {
3536 let mut corpus: Vec<Case<SolidRoundRectRecord>> = Vec::new();
3537 for stroke in [None, Some(3.0_f32)] {
3538 let retained = rr_base(stroke);
3539 let matched = rr_moved(&retained);
3540 corpus.push(Case {
3541 label: format!("exact, stroke {stroke:?}"),
3542 current: matched,
3543 retained,
3544 expected: Some(RecordMatch::Exact),
3545 });
3546 let mut recolored = matched;
3547 recolored.color = Color::rgb(0.2, 0.8, 0.4);
3548 corpus.push(Case {
3549 label: format!("recolor, stroke {stroke:?}"),
3550 current: recolored,
3551 retained,
3552 expected: Some(RecordMatch::Recolor),
3553 });
3554 }
3555
3556 let retained = rr_base(None);
3557 let matched = rr_moved(&retained);
3558 for (name, get, set) in rr_fields() {
3559 let base = get(&matched);
3560 let tolerance = ABS_EPS + REL_EPS * base.abs();
3561 for sign in [1.0_f32, -1.0] {
3562 let mut inside = matched;
3563 set(&mut inside, base + sign * 0.9 * tolerance);
3564 corpus.push(Case {
3565 label: format!("{name} just inside, sign {sign}"),
3566 current: inside,
3567 retained,
3568 expected: Some(RecordMatch::Exact),
3569 });
3570 let mut outside = matched;
3571 set(&mut outside, base + sign * 1.1 * tolerance);
3572 corpus.push(Case {
3573 label: format!("{name} just outside, sign {sign}"),
3574 current: outside,
3575 retained,
3576 expected: Some(RecordMatch::Mismatch),
3577 });
3578 }
3579 for poison in POISONS {
3580 let mut current = matched;
3581 set(&mut current, poison);
3582 corpus.push(Case {
3583 label: format!("{name} current {poison:e}"),
3584 current,
3585 retained,
3586 expected: None,
3587 });
3588 let mut poisoned = retained;
3589 set(&mut poisoned, poison);
3590 corpus.push(Case {
3591 label: format!("{name} retained {poison:e}"),
3592 current: matched,
3593 retained: poisoned,
3594 expected: None,
3595 });
3596 let mut both_current = matched;
3597 set(&mut both_current, poison);
3598 corpus.push(Case {
3599 label: format!("{name} both {poison:e}"),
3600 current: both_current,
3601 retained: poisoned,
3602 expected: None,
3603 });
3604 }
3605 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3608 for sign in [1.0_f32, -1.0] {
3609 let mut edge = matched;
3610 set(&mut edge, base + sign * knife);
3611 corpus.push(Case {
3612 label: format!("{name} knife edge {knife:e}, sign {sign}"),
3613 current: edge,
3614 retained,
3615 expected: None,
3616 });
3617 }
3618 }
3619 }
3620
3621 let mut squashed = matched;
3626 squashed.rect.height = squashed.rect.width * 2.0;
3627 corpus.push(Case {
3628 label: "current non-circle (squashed)".to_string(),
3629 current: squashed,
3630 retained,
3631 expected: Some(RecordMatch::Mismatch),
3632 });
3633 let mut loose_radii = retained;
3634 loose_radii.radii = CornerRadii::uniform(2.0);
3635 corpus.push(Case {
3636 label: "retained non-circle (loose radii)".to_string(),
3637 current: matched,
3638 retained: loose_radii,
3639 expected: Some(RecordMatch::Mismatch),
3640 });
3641 corpus.push(Case {
3642 label: "non-circle vs itself".to_string(),
3643 current: loose_radii,
3644 retained: loose_radii,
3645 expected: Some(RecordMatch::Mismatch),
3646 });
3647
3648 let stroked = rr_base(Some(3.0));
3650 let moved_stroked = rr_moved(&stroked);
3651 let mut some_vs_none = matched;
3652 some_vs_none.stroke = Some(Stroke::new(3.0 * T.scale));
3653 corpus.push(Case {
3654 label: "stroke Some vs None".to_string(),
3655 current: some_vs_none,
3656 retained,
3657 expected: Some(RecordMatch::Mismatch),
3658 });
3659 corpus.push(Case {
3660 label: "stroke None vs Some".to_string(),
3661 current: matched,
3662 retained: stroked,
3663 expected: Some(RecordMatch::Mismatch),
3664 });
3665 let width = 3.0 * T.scale;
3666 let tolerance = ABS_EPS + REL_EPS * width.abs();
3667 for sign in [1.0_f32, -1.0] {
3668 let mut inside = moved_stroked;
3669 inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
3670 corpus.push(Case {
3671 label: format!("stroke width just inside, sign {sign}"),
3672 current: inside,
3673 retained: stroked,
3674 expected: Some(RecordMatch::Exact),
3675 });
3676 let mut outside = moved_stroked;
3677 outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
3678 corpus.push(Case {
3679 label: format!("stroke width just outside, sign {sign}"),
3680 current: outside,
3681 retained: stroked,
3682 expected: Some(RecordMatch::Mismatch),
3683 });
3684 for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3685 let mut edge = moved_stroked;
3686 edge.stroke = Some(Stroke::new(width + sign * knife));
3687 corpus.push(Case {
3688 label: format!("stroke width knife edge {knife:e}, sign {sign}"),
3689 current: edge,
3690 retained: stroked,
3691 expected: None,
3692 });
3693 }
3694 }
3695 for poison in POISONS {
3696 let mut current = moved_stroked;
3697 current.stroke = Some(Stroke::new(poison));
3698 corpus.push(Case {
3699 label: format!("stroke width current {poison:e}"),
3700 current,
3701 retained: stroked,
3702 expected: None,
3703 });
3704 }
3705
3706 let mut nan_color = matched;
3707 nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
3708 corpus.push(Case {
3709 label: "NaN color".to_string(),
3710 current: nan_color,
3711 retained,
3712 expected: Some(RecordMatch::Recolor),
3713 });
3714 corpus
3715 }
3716
3717 #[test]
3718 fn the_round_rect_corpus_exercises_what_it_claims() {
3719 for case in rr_corpus() {
3720 if let Some(expected) = case.expected {
3721 assert_eq!(
3722 match_round_rect(&case.current, &case.retained, PIVOT, T),
3723 expected,
3724 "scalar verdict for `{}`",
3725 case.label
3726 );
3727 }
3728 }
3729 }
3730
3731 #[test]
3732 fn round_rect_kernel_equals_the_scalar_authority_cross_paired() {
3733 let corpus = rr_corpus();
3734 for t in transforms() {
3735 let (sin, cos) = t.angle.sin_cos();
3736 for a in &corpus {
3737 for b in &corpus {
3738 assert_eq!(
3739 match_round_rect_lanes(&a.current, &b.retained, PIVOT, t.scale, sin, cos),
3740 match_round_rect(&a.current, &b.retained, PIVOT, t),
3741 "round-rect kernel diverged: current `{}` vs retained `{}` under {t:?}",
3742 a.label,
3743 b.label
3744 );
3745 }
3746 }
3747 }
3748 }
3749
3750 #[test]
3751 fn round_rect_run_equals_a_scalar_reference_from_every_start() {
3752 let corpus = rr_corpus();
3753 let current: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.current).collect();
3754 let snapshot: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.retained).collect();
3755 let mut fast: Vec<(u32, Color)> = Vec::new();
3756 let mut naive: Vec<(u32, Color)> = Vec::new();
3757 for start in 0..current.len() {
3758 fast.clear();
3759 naive.clear();
3760 let matched = match_round_rect_run(
3761 ¤t[start..],
3762 &snapshot[start..],
3763 PIVOT,
3764 T,
3765 7,
3766 &mut fast,
3767 );
3768 let mut mismatch = None;
3769 for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3770 match match_round_rect(now, then, PIVOT, T) {
3771 RecordMatch::Exact => {}
3772 RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3773 RecordMatch::Mismatch => {
3774 mismatch = Some(i);
3775 break;
3776 }
3777 }
3778 }
3779 let reference = mismatch.unwrap_or(current.len() - start);
3780 assert_eq!(
3781 (matched, bits(&fast)),
3782 (reference, bits(&naive)),
3783 "round-rect run diverged from start {start}"
3784 );
3785 }
3786 }
3787
3788 struct XorShift(u32);
3795
3796 impl XorShift {
3797 fn next(&mut self) -> u32 {
3798 let mut x = self.0;
3799 x ^= x << 13;
3800 x ^= x >> 17;
3801 x ^= x << 5;
3802 self.0 = x;
3803 x
3804 }
3805
3806 fn f32(&mut self) -> f32 {
3807 if self.next() & 1 == 0 {
3808 (self.next() as f32 / u32::MAX as f32) * 1000.0 - 500.0
3809 } else {
3810 f32::from_bits(self.next())
3811 }
3812 }
3813
3814 fn stroke(&mut self) -> Option<Stroke> {
3815 (self.next() & 1 == 0).then(|| Stroke::new(self.f32()))
3816 }
3817
3818 fn arc(&mut self) -> SolidArcRecord {
3819 SolidArcRecord {
3820 center: Point::new(self.f32(), self.f32()),
3821 radius: self.f32(),
3822 start_angle: self.f32(),
3823 sweep_angle: self.f32(),
3824 inner_radius: self.f32(),
3825 color: Color::WHITE,
3826 stroke: self.stroke(),
3827 }
3828 }
3829
3830 fn round_rect(&mut self) -> SolidRoundRectRecord {
3831 SolidRoundRectRecord {
3832 rect: Rect {
3833 x: self.f32(),
3834 y: self.f32(),
3835 width: self.f32(),
3836 height: self.f32(),
3837 },
3838 radii: CornerRadii {
3839 top_left: self.f32(),
3840 top_right: self.f32(),
3841 bottom_right: self.f32(),
3842 bottom_left: self.f32(),
3843 },
3844 color: Color::WHITE,
3845 stroke: self.stroke(),
3846 }
3847 }
3848 }
3849
3850 #[test]
3851 fn kernels_equal_the_authorities_on_arbitrary_bit_patterns() {
3852 let mut rng = XorShift(0x9e37_79b9);
3853 for _ in 0..4000 {
3854 let t = RecordTransform {
3855 scale: rng.f32(),
3856 angle: rng.f32(),
3857 };
3858 let (sin, cos) = t.angle.sin_cos();
3859 let (a, b) = (rng.arc(), rng.arc());
3860 assert_eq!(
3861 match_arc_lanes(&a, &b, PIVOT, t.scale, t.angle),
3862 match_arc(&a, &b, PIVOT, t),
3863 "arc kernel diverged: {a:?} vs {b:?} under {t:?}"
3864 );
3865 let (c, d) = (rng.round_rect(), rng.round_rect());
3866 assert_eq!(
3867 match_round_rect_lanes(&c, &d, PIVOT, t.scale, sin, cos),
3868 match_round_rect(&c, &d, PIVOT, t),
3869 "round-rect kernel diverged: {c:?} vs {d:?} under {t:?}"
3870 );
3871 }
3872 }
3873}