Skip to main content

cranpose_ui_graphics/
record_replay.rs

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