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