Skip to main content

cranpose_ui_graphics/
record_replay.rs

1//! Similarity verification over typed draw records.
2//!
3//! This is the record-level home of the math the wgpu flat-list replay
4//! detector applies to materialized primitives: deriving a per-segment
5//! similarity transform (uniform scale + rotation about a fixed center) from
6//! an anchor pair, and verifying that a freshly recorded entry is the
7//! retained entry moved by exactly that transform. Operating on
8//! [`SolidArcRecord`]/[`SolidRoundRectRecord`] means the comparison sees the
9//! RAW values the app drew with, before arc bands, tight bounds, or
10//! `DrawPrimitive` construction — all of which a confirmed match makes
11//! unnecessary.
12//!
13//! Records are solid-brush by construction, so the brush half of
14//! verification collapses to a color comparison: geometry match + equal
15//! color is [`RecordMatch::Exact`], geometry match + different color is
16//! [`RecordMatch::Recolor`] (a retained buffer patch), anything else is
17//! [`RecordMatch::Mismatch`] and must take the ordinary path in the same
18//! frame. Tolerances are identical to the flat-list detector's; they cover
19//! the game's own per-frame float noise, and a real content change is orders
20//! of magnitude larger.
21
22use crate::{
23    Color, CornerRadii,
24    geometry::{
25        CommandRecording, Point, RecordKind, Rect, SolidArcRecord, SolidRoundRectRecord, TapeRef,
26    },
27};
28
29/// Relative tolerance for similarity verification.
30const REL_EPS: f32 = 2e-3;
31/// Absolute tolerance for positions/angles near zero, logical px/radians.
32const ABS_EPS: f32 = 2e-2;
33/// How far apart two entries' implied per-frame transforms may sit while
34/// still being grouped into one segment. Much tighter than verification:
35/// entries of one ring share literally the same baked rotation step, while
36/// neighboring rings differ by a speed delta that accumulates every frame.
37const GROUP_SCALE_EPS: f32 = 1e-4;
38const GROUP_ANGLE_EPS: f32 = 2e-4;
39
40fn close_rel(a: f32, b: f32) -> bool {
41    (a - b).abs() <= ABS_EPS + REL_EPS * a.abs().max(b.abs())
42}
43
44fn close_angle(a: f32, b: f32) -> bool {
45    use std::f32::consts::TAU;
46    // `%` 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            && self.segments.len() >= 2
1170        {
1171            let commit = self.verify_optimistic(current, pool);
1172            if commit.committed == self.segments.len() {
1173                self.optimistic_commits += 1;
1174                return self.finish_verify(current, commit.spans, commit.retained_records);
1175            }
1176            // Prefix-commit: the pooled spans for every segment before
1177            // the first failure are equal by construction to what the
1178            // serial walk would produce for them (see
1179            // [`Self::verify_optimistic`]), so they are kept and the
1180            // serial machinery below is seeded from the failure point
1181            // instead of redoing the whole tape.
1182            if commit.committed > 0 {
1183                self.prefix_commits += 1;
1184            }
1185            spans = commit.spans;
1186            retained_records = commit.retained_records;
1187            cursor = commit.cursor;
1188            committed = commit.committed;
1189        }
1190        // 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        Brush, DrawScope as _,
2257        geometry::{DrawScopeDefault, Size},
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!(
2320            spans
2321                .iter()
2322                .any(|span| matches!(span, ReplaySpan::Dynamic { .. }))
2323        );
2324        // Retained spans carry the per-ring rotations, not a shared one.
2325        let transforms: Vec<RecordTransform> = spans
2326            .iter()
2327            .filter_map(|span| match span {
2328                ReplaySpan::Retained { transform, .. } => Some(*transform),
2329                _ => None,
2330            })
2331            .collect();
2332        assert!(transforms.windows(2).any(|w| w[0].angle != w[1].angle));
2333    }
2334
2335    /// A ring scene sharing nothing with [`ring_frame`] — different radii,
2336    /// band ratio, sweep, counts — so a state captured on one collapses
2337    /// whole when fed the other.
2338    fn flipped_ring_frame(frame: usize) -> CommandRecording {
2339        let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2340        for ring in 0..4 {
2341            let rotation = (0.02 + ring as f32 * 0.007) * frame as f32;
2342            let radius = 75.0 + ring as f32 * 27.0;
2343            for slot in 0..260 {
2344                let start = slot as f32 * (std::f32::consts::TAU / 260.0) + rotation;
2345                scope.draw_annular_sector(
2346                    Brush::solid(Color::WHITE),
2347                    CENTER,
2348                    radius * 0.75,
2349                    radius,
2350                    start,
2351                    0.015,
2352                );
2353            }
2354        }
2355        scope.recorded().clone()
2356    }
2357
2358    #[test]
2359    fn only_a_collapse_out_of_capture_sets_the_transition_flag() {
2360        let mut state = CommandReplayState::default();
2361        // Bootstrap frames never flag: the idle snapshot...
2362        assert!(matches!(
2363            state.advance(&ring_frame(3, 300, 0, 10)),
2364            ReplayOutcome::AllDynamic
2365        ));
2366        assert!(!state.collapsed_from_captured());
2367        // ...the partition/capture frame...
2368        assert!(matches!(
2369            state.advance(&ring_frame(3, 300, 1, 10)),
2370            ReplayOutcome::Spans(_)
2371        ));
2372        assert!(!state.collapsed_from_captured());
2373        // ...and an ordinary verified frame.
2374        assert!(matches!(
2375            state.advance(&ring_frame(3, 300, 2, 10)),
2376            ReplayOutcome::Spans(_)
2377        ));
2378        assert!(!state.collapsed_from_captured());
2379        // The content flip: nothing survives verification, the frame
2380        // collapses out of the established capture — the flag's one
2381        // trigger, and the frame whose emission would re-materialize the
2382        // whole tape.
2383        assert!(matches!(
2384            state.advance(&flipped_ring_frame(3)),
2385            ReplayOutcome::AllDynamic
2386        ));
2387        assert!(state.collapsed_from_captured());
2388        // The next advance clears it: re-convergence frames are ordinary.
2389        let _ = state.advance(&flipped_ring_frame(4));
2390        assert!(!state.collapsed_from_captured());
2391        // A short tape retires the state — a bootstrap path, never a
2392        // collapse, even straight after retention.
2393        let _ = state.advance(&flipped_ring_frame(5));
2394        let short = ring_frame(1, 40, 0, 0);
2395        assert!(short.len() < MIN_REPLAY_COMMAND_RECORDS);
2396        assert!(matches!(state.advance(&short), ReplayOutcome::AllDynamic));
2397        assert!(!state.collapsed_from_captured());
2398    }
2399
2400    #[test]
2401    fn entity_churn_between_frames_still_retains_rings() {
2402        let mut state = CommandReplayState::default();
2403        state.advance(&ring_frame(2, 400, 0, 8));
2404        state.advance(&ring_frame(2, 400, 1, 13)); // tail length changed
2405        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(2, 400, 2, 5)) else {
2406            panic!("churned tail must not break ring retention");
2407        };
2408        let retained_records: usize = spans
2409            .iter()
2410            .filter_map(|span| match span {
2411                ReplaySpan::Retained { .. } => Some(1),
2412                _ => None,
2413            })
2414            .sum();
2415        assert!(retained_records >= 2);
2416    }
2417
2418    #[test]
2419    fn recolors_are_patches_not_mismatches() {
2420        let recolored_frame = |frame: usize| {
2421            let mut recording = ring_frame(1, 600, frame, 0);
2422            // Twinkle: 40 dots change color every frame, geometry untouched.
2423            for i in (0..recording.arcs.len()).step_by(15) {
2424                recording.arcs[i].color = if frame.is_multiple_of(2) {
2425                    Color::rgb(1.0, 0.5, 0.1)
2426                } else {
2427                    Color::rgb(0.1, 0.5, 1.0)
2428                };
2429            }
2430            recording
2431        };
2432        let mut state = CommandReplayState::default();
2433        state.advance(&recolored_frame(0));
2434        state.advance(&recolored_frame(1));
2435        let ReplayOutcome::Spans(spans) = state.advance(&recolored_frame(2)) else {
2436            panic!("twinkles must not break retention");
2437        };
2438        let recolor_count: usize = spans
2439            .iter()
2440            .filter_map(|span| match span {
2441                ReplaySpan::Retained { recolors, .. } => Some(recolors.len()),
2442                _ => None,
2443            })
2444            .sum();
2445        assert!(recolor_count >= 30, "twinkles surface as patches");
2446    }
2447
2448    #[test]
2449    fn geometry_change_kills_only_its_segment() {
2450        let mut state = CommandReplayState::default();
2451        state.advance(&ring_frame(3, 300, 0, 0));
2452        state.advance(&ring_frame(3, 300, 1, 0));
2453        let mut broken = ring_frame(3, 300, 2, 0);
2454        // A brick hit: one entry in the middle ring changes sweep.
2455        broken.arcs[450].sweep_angle *= 3.0;
2456        let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2457            panic!("one changed entry must not drop the whole command");
2458        };
2459        let retained: usize = spans
2460            .iter()
2461            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2462            .count();
2463        assert!(
2464            retained >= 2,
2465            "the untouched rings keep retaining, got {spans:?}"
2466        );
2467    }
2468
2469    #[test]
2470    fn mid_segment_change_splits_and_retains_both_halves() {
2471        let mut state = CommandReplayState::default();
2472        state.advance(&ring_frame(1, 900, 0, 0));
2473        state.advance(&ring_frame(1, 900, 1, 0));
2474        assert_eq!(state.segments().len(), 1, "one ring is one segment");
2475        let mut broken = ring_frame(1, 900, 2, 0);
2476        broken.arcs[450].sweep_angle *= 3.0;
2477        let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
2478            panic!("a single changed record must not drop retention");
2479        };
2480        let dynamic: usize = spans
2481            .iter()
2482            .filter_map(|span| match span {
2483                ReplaySpan::Dynamic {
2484                    tape_start,
2485                    tape_end,
2486                } => Some(tape_end - tape_start),
2487                _ => None,
2488            })
2489            .sum();
2490        let retained: Vec<(u32, usize, bool)> = spans
2491            .iter()
2492            .filter_map(|span| match span {
2493                ReplaySpan::Retained {
2494                    slot,
2495                    slot_offset,
2496                    capture,
2497                    ..
2498                } => Some((*slot, *slot_offset, *capture)),
2499                _ => None,
2500            })
2501            .collect();
2502        assert_eq!(
2503            retained.len(),
2504            2,
2505            "prefix and suffix both retain: {spans:?}"
2506        );
2507        // Both pieces address the SAME captured slot — a split never
2508        // recaptures, it re-addresses: the suffix starts one record past
2509        // the prefix within the capture.
2510        assert_eq!(retained[0].0, retained[1].0);
2511        assert_eq!(retained[0].1, 0);
2512        assert_eq!(retained[1].1, 451);
2513        assert!(retained.iter().all(|(_, _, capture)| !capture));
2514        assert_eq!(dynamic, 1, "only the changed record goes dynamic");
2515        assert_eq!(state.stats(), (0, 1), "one split, no deaths");
2516
2517        // The pieces keep retaining on later frames, the changed record's
2518        // slot staying dynamic between them.
2519        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(1, 900, 3, 0)) else {
2520            panic!("split pieces must keep retaining");
2521        };
2522        let retained = spans
2523            .iter()
2524            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
2525            .count();
2526        assert_eq!(retained, 2, "both pieces relocate next frame: {spans:?}");
2527    }
2528
2529    #[test]
2530    fn erosion_recaptures_dead_ranges_after_the_cooldown() {
2531        let mut state = CommandReplayState::default();
2532        state.advance(&ring_frame(3, 300, 0, 0));
2533        state.advance(&ring_frame(3, 300, 1, 0));
2534        // The middle ring changes shape permanently: its segment dies, and
2535        // only a recapture can watch the new shape.
2536        let mutated = |frame: usize| {
2537            let mut recording = ring_frame(3, 300, frame, 0);
2538            for arc in &mut recording.arcs[300..600] {
2539                arc.sweep_angle *= 3.0;
2540            }
2541            recording
2542        };
2543        let dynamic_records = |outcome: &ReplayOutcome| -> usize {
2544            match outcome {
2545                ReplayOutcome::AllDynamic => usize::MAX,
2546                ReplayOutcome::Spans(spans) => spans
2547                    .iter()
2548                    .filter_map(|span| match span {
2549                        ReplaySpan::Dynamic {
2550                            tape_start,
2551                            tape_end,
2552                        } => Some(tape_end - tape_start),
2553                        _ => None,
2554                    })
2555                    .sum(),
2556            }
2557        };
2558        let after_death = state.advance(&mutated(2));
2559        let lost = dynamic_records(&after_death);
2560        assert!(
2561            (250..=400).contains(&lost),
2562            "the changed ring goes dynamic, got {lost}"
2563        );
2564        for frame in 3..(3 + RECAPTURE_COOLDOWN_FRAMES as usize + 4) {
2565            state.advance(&mutated(frame));
2566        }
2567        let recovered = state.advance(&mutated(200));
2568        let residue = dynamic_records(&recovered);
2569        assert!(
2570            residue < 50,
2571            "the recapture watches the ring's new shape, got {residue} dynamic"
2572        );
2573    }
2574
2575    #[test]
2576    fn small_commands_are_not_watched() {
2577        let mut state = CommandReplayState::default();
2578        for frame in 0..4 {
2579            assert!(matches!(
2580                state.advance(&ring_frame(1, 40, frame, 0)),
2581                ReplayOutcome::AllDynamic
2582            ));
2583        }
2584        assert!(state.segments().is_empty());
2585    }
2586
2587    /// The naive per-entry walk [`match_span`] replaced: decode both
2588    /// sides' views at every offset, dispatch on the pair, decode again for
2589    /// a recolor's color. Kept verbatim as the reference the run-decomposed
2590    /// path is checked against, verdict for verdict, recolor for recolor.
2591    #[allow(clippy::too_many_arguments)]
2592    fn match_span_reference(
2593        current: TypedRecords<'_>,
2594        snapshot: TypedRecords<'_>,
2595        center: Point,
2596        start: usize,
2597        seg_start: usize,
2598        len: usize,
2599        t: RecordTransform,
2600        recolors: &mut Vec<(u32, Color)>,
2601    ) -> usize {
2602        recolors.clear();
2603        for offset in 0..len {
2604            let entry_match = match (
2605                current.view_at(start + offset),
2606                snapshot.view_at(seg_start + offset),
2607            ) {
2608                (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
2609                    match_arc(&current.arcs[i], &snapshot.arcs[j], center, t)
2610                }
2611                (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
2612                    match_round_rect(&current.round_rects[i], &snapshot.round_rects[j], center, t)
2613                }
2614                _ => RecordMatch::Mismatch,
2615            };
2616            match entry_match {
2617                RecordMatch::Exact => {}
2618                RecordMatch::Recolor => {
2619                    let color = match current.view_at(start + offset) {
2620                        Some(ReplayView::Arc(a)) => current.arcs[a].color,
2621                        Some(ReplayView::RoundRect(r)) => current.round_rects[r].color,
2622                        None => unreachable!("recolor requires a view"),
2623                    };
2624                    recolors.push((offset as u32, color));
2625                }
2626                RecordMatch::Mismatch => return offset,
2627            }
2628        }
2629        len
2630    }
2631
2632    /// One mixed frame, in tape order: a run of arcs, a non-circular round
2633    /// rect, a solid rect, a gradient rect (an `Other` entry), a run of
2634    /// circles, a second run of arcs, and a closing pair of circles — every
2635    /// run-breaking shape on one tape, PLUS matchable runs that directly
2636    /// follow another matchable run (circles→arcs and arcs→circles). Those
2637    /// adjacencies matter: a span entered mid-tape reaches its second run
2638    /// at a non-zero span offset, so a recolor there catches any confusion
2639    /// between run-relative and span-relative offsets. `t` moves the
2640    /// movable content so a span match under `t` sees Exact/Recolor
2641    /// entries, not wall-to-wall mismatches; `recolored` repaints one entry
2642    /// in each matchable run.
2643    fn mixed_frame(t: RecordTransform, recolored: bool) -> CommandRecording {
2644        let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
2645        for slot in 0..8 {
2646            let color = if recolored && slot == 3 {
2647                Color::rgb(1.0, 0.5, 0.1)
2648            } else {
2649                Color::WHITE
2650            };
2651            scope.draw_annular_sector(
2652                Brush::solid(color),
2653                CENTER,
2654                80.0 * t.scale * 0.8,
2655                80.0 * t.scale,
2656                slot as f32 * 0.7 + t.angle,
2657                0.02,
2658            );
2659        }
2660        scope.draw_round_rect_at(
2661            Rect {
2662                x: 10.0,
2663                y: 10.0,
2664                width: 40.0,
2665                height: 20.0,
2666            },
2667            Brush::solid(Color::WHITE),
2668            CornerRadii::uniform(4.0),
2669        );
2670        scope.draw_rect_at(
2671            Rect {
2672                x: 60.0,
2673                y: 10.0,
2674                width: 20.0,
2675                height: 20.0,
2676            },
2677            Brush::solid(Color::WHITE),
2678        );
2679        scope.draw_rect_at(
2680            Rect {
2681                x: 90.0,
2682                y: 10.0,
2683                width: 20.0,
2684                height: 20.0,
2685            },
2686            Brush::linear_gradient(vec![Color::WHITE, Color::RED]),
2687        );
2688        for slot in 0..3 {
2689            let base = Point::new(304.0, 204.0 + slot as f32 * 20.0);
2690            let color = if recolored && slot == 1 {
2691                Color::rgb(0.1, 0.5, 1.0)
2692            } else {
2693                Color::WHITE
2694            };
2695            scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 5.0 * t.scale);
2696        }
2697        for slot in 0..6 {
2698            let color = if recolored && slot == 1 {
2699                Color::rgb(0.9, 0.2, 0.4)
2700            } else {
2701                Color::WHITE
2702            };
2703            scope.draw_annular_sector(
2704                Brush::solid(color),
2705                CENTER,
2706                120.0 * t.scale * 0.8,
2707                120.0 * t.scale,
2708                slot as f32 * 0.9 + 0.1 + t.angle,
2709                0.03,
2710            );
2711        }
2712        for slot in 0..2 {
2713            let base = Point::new(104.0, 204.0 + slot as f32 * 24.0);
2714            let color = if recolored && slot == 1 {
2715                Color::rgb(0.2, 0.9, 0.3)
2716            } else {
2717                Color::WHITE
2718            };
2719            scope.draw_circle(Brush::solid(color), t.apply(CENTER, base), 4.0 * t.scale);
2720        }
2721        scope.recorded().clone()
2722    }
2723
2724    #[test]
2725    fn interleaved_tape_decomposes_into_exact_runs() {
2726        let recording = mixed_frame(RecordTransform::IDENTITY, false);
2727        let tape = &recording.tape;
2728        let mut runs: Vec<(RecordKind, usize, usize)> = Vec::new();
2729        let mut at = 0usize;
2730        while at < tape.len() {
2731            let len = typed_run_len(tape, at);
2732            runs.push((tape[at].kind(), tape[at].index(), len));
2733            at += len;
2734        }
2735        assert_eq!(
2736            runs,
2737            vec![
2738                (RecordKind::SolidArc, 0, 8),
2739                (RecordKind::SolidRoundRect, 0, 1),
2740                (RecordKind::SolidRect, 0, 1),
2741                (RecordKind::Other, 0, 1),
2742                (RecordKind::SolidRoundRect, 1, 3),
2743                (RecordKind::SolidArc, 8, 6),
2744                (RecordKind::SolidRoundRect, 4, 2),
2745            ],
2746            "run decomposition must cut exactly at kind transitions"
2747        );
2748        // A run read from the middle is that run's remainder, and a kind
2749        // that leaves and returns starts a NEW run — index continuity
2750        // across the gap must not fuse the two stretches.
2751        assert_eq!(typed_run_len(tape, 3), 5);
2752        assert_eq!(typed_run_len(tape, 8), 1);
2753        assert_eq!(typed_run_len(tape, 12), 2);
2754        assert_eq!(typed_run_len(tape, 14), 6);
2755        assert_eq!(typed_run_len(tape, 20), 2);
2756    }
2757
2758    #[test]
2759    fn run_decomposed_span_match_equals_the_per_entry_walk() {
2760        let t = RecordTransform {
2761            scale: 0.9994,
2762            angle: 0.0123,
2763        };
2764        let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2765        let mut current_rec = mixed_frame(t, true);
2766        // A genuine geometry change inside the second arc run, and a
2767        // NaN-carrying record right after it: both must fail through both
2768        // paths at the same offset.
2769        current_rec.arcs[11].sweep_angle *= 3.0;
2770        current_rec.arcs[12].start_angle = f32::NAN;
2771        let current = TypedRecords::from(&current_rec);
2772        let snapshot = TypedRecords::from(&snapshot_rec);
2773        let n = current_rec.tape.len();
2774        assert_eq!(n, snapshot_rec.tape.len());
2775        assert_eq!(n, 22);
2776        let mut fast: Vec<(u32, Color)> = Vec::new();
2777        let mut naive: Vec<(u32, Color)> = Vec::new();
2778        // Every (start, seg_start) pairing — aligned, shifted into other
2779        // runs, cross-kind — at several lengths including the longest one
2780        // both sides can carry.
2781        for start in 0..n {
2782            for seg_start in 0..n {
2783                let longest = n - start.max(seg_start);
2784                for len in [0usize, 1, 2, 5, longest] {
2785                    if start + len > n || seg_start + len > n {
2786                        continue;
2787                    }
2788                    let matched = match_span(
2789                        current, snapshot, CENTER, start, seg_start, len, t, &mut fast,
2790                    );
2791                    let reference = match_span_reference(
2792                        current, snapshot, CENTER, start, seg_start, len, t, &mut naive,
2793                    );
2794                    assert_eq!(
2795                        (matched, &fast),
2796                        (reference, &naive),
2797                        "diverged at start={start} seg_start={seg_start} len={len}"
2798                    );
2799                }
2800            }
2801        }
2802        // The sweep must actually exercise the positive paths, not agree
2803        // on wall-to-wall mismatches: the aligned leading arc run matches
2804        // whole with its recolor, the aligned circles with theirs, and a
2805        // span crossing circles into the second arc run carries recolors
2806        // from BOTH runs — the arc one at span offset 4 (run offset 1), the
2807        // pairing any run-relative recolor bookkeeping would get wrong —
2808        // then stops exactly at the changed record. A span crossing the
2809        // last arc into the closing circles pins the same offset arithmetic
2810        // for the round-rect loop.
2811        let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2812        assert_eq!(
2813            (matched, fast.as_slice()),
2814            (8, &[(3, Color::rgb(1.0, 0.5, 0.1))][..])
2815        );
2816        let matched = match_span(current, snapshot, CENTER, 11, 11, 3, t, &mut fast);
2817        assert_eq!(
2818            (matched, fast.as_slice()),
2819            (3, &[(1, Color::rgb(0.1, 0.5, 1.0))][..])
2820        );
2821        let matched = match_span(current, snapshot, CENTER, 11, 11, 9, t, &mut fast);
2822        assert_eq!(
2823            (matched, fast.as_slice()),
2824            (
2825                6,
2826                &[
2827                    (1, Color::rgb(0.1, 0.5, 1.0)),
2828                    (4, Color::rgb(0.9, 0.2, 0.4)),
2829                ][..]
2830            ),
2831            "the changed arc ends the clean prefix behind the circles"
2832        );
2833        let matched = match_span(current, snapshot, CENTER, 19, 19, 3, t, &mut fast);
2834        assert_eq!(
2835            (matched, fast.as_slice()),
2836            (3, &[(2, Color::rgb(0.2, 0.9, 0.3))][..])
2837        );
2838    }
2839
2840    #[test]
2841    fn nan_records_mismatch_through_both_paths() {
2842        let t = RecordTransform::IDENTITY;
2843        let base = arc(80.0, 0.2, Color::WHITE);
2844        let poisoned_arcs: [fn(&mut SolidArcRecord); 6] = [
2845            |a| a.center.x = f32::NAN,
2846            |a| a.center.y = f32::NAN,
2847            |a| a.radius = f32::NAN,
2848            |a| a.inner_radius = f32::NAN,
2849            |a| a.start_angle = f32::NAN,
2850            |a| a.sweep_angle = f32::NAN,
2851        ];
2852        for poison in poisoned_arcs {
2853            let mut poisoned = base;
2854            poison(&mut poisoned);
2855            assert_eq!(
2856                match_arc(&poisoned, &base, CENTER, t),
2857                RecordMatch::Mismatch
2858            );
2859            assert_eq!(
2860                match_arc(&base, &poisoned, CENTER, t),
2861                RecordMatch::Mismatch
2862            );
2863        }
2864        // A NaN circle, both as a poisoned position (still circle-eligible,
2865        // fails the point comparison) and as poisoned extents (loses circle
2866        // eligibility itself).
2867        let good = circle(304.0, 204.0, 10.0, Color::WHITE);
2868        let poisoned_circles: [fn(&mut SolidRoundRectRecord); 2] =
2869            [|r| r.rect.x = f32::NAN, |r| r.rect.width = f32::NAN];
2870        for poison in poisoned_circles {
2871            let mut poisoned = good;
2872            poison(&mut poisoned);
2873            assert_eq!(
2874                match_round_rect(&poisoned, &good, CENTER, t),
2875                RecordMatch::Mismatch
2876            );
2877            assert_eq!(
2878                match_round_rect(&good, &poisoned, CENTER, t),
2879                RecordMatch::Mismatch
2880            );
2881        }
2882        // And at span level: the poisoned record ends the clean prefix at
2883        // the same offset through the run-decomposed path and the naive
2884        // walk.
2885        let snapshot_rec = mixed_frame(RecordTransform::IDENTITY, false);
2886        let mut current_rec = mixed_frame(RecordTransform::IDENTITY, false);
2887        current_rec.arcs[4].sweep_angle = f32::NAN;
2888        let current = TypedRecords::from(&current_rec);
2889        let snapshot = TypedRecords::from(&snapshot_rec);
2890        let mut fast: Vec<(u32, Color)> = Vec::new();
2891        let mut naive: Vec<(u32, Color)> = Vec::new();
2892        let matched = match_span(current, snapshot, CENTER, 0, 0, 8, t, &mut fast);
2893        let reference = match_span_reference(current, snapshot, CENTER, 0, 0, 8, t, &mut naive);
2894        assert_eq!(matched, 4, "the NaN record is a mismatch, not a match");
2895        assert_eq!((matched, &fast), (reference, &naive));
2896    }
2897
2898    /// A real multi-threaded executor for the equivalence test: lane 0 is
2899    /// the caller, the rest are scoped threads, jobs stride across lanes —
2900    /// the same distribution the renderer's frame pool uses.
2901    struct ThreadedExec {
2902        lanes: usize,
2903    }
2904
2905    impl VerifyExecutor for ThreadedExec {
2906        fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync)) {
2907            std::thread::scope(|s| {
2908                for lane in 1..self.lanes {
2909                    s.spawn(move || {
2910                        let mut i = lane;
2911                        while i < jobs {
2912                            run(i);
2913                            i += self.lanes;
2914                        }
2915                    });
2916                }
2917                let mut i = 0;
2918                while i < jobs {
2919                    run(i);
2920                    i += self.lanes;
2921                }
2922            });
2923        }
2924    }
2925
2926    #[test]
2927    fn pooled_verification_matches_serial_exactly() {
2928        let exec = ThreadedExec { lanes: 3 };
2929        // Every verification path in one long churning sequence: multi-ring
2930        // retention under rotation, tail churn, twinkle recolors, brick-hit
2931        // single-record changes (the pooled pass commits the segments
2932        // before the failure and hands the serial machinery the failure
2933        // point), a multi-segment change, whole-ring deaths behind a
2934        // committed prefix, and coverage collapses that force re-snapshots
2935        // and fresh partitions mid-sequence.
2936        let frame = |f: usize| -> CommandRecording {
2937            let tail = [10usize, 13, 5, 8, 11, 6, 9, 12][f % 8];
2938            let mut recording = ring_frame(3, 300, f, tail);
2939            if f >= 3 {
2940                for i in (0..recording.arcs.len()).step_by(17) {
2941                    recording.arcs[i].color = if f.is_multiple_of(2) {
2942                        Color::rgb(1.0, 0.5, 0.1)
2943                    } else {
2944                        Color::rgb(0.1, 0.5, 1.0)
2945                    };
2946                }
2947            }
2948            match f {
2949                5 => {
2950                    // A brick hit: one record inside the middle ring. The
2951                    // pooled pass fails there, commits the ring before it,
2952                    // and the serial machinery splits from the failure.
2953                    recording.arcs[450].sweep_angle = 0.15;
2954                }
2955                8 => {
2956                    // Changes in the first and last rings at once: the
2957                    // first segment fails, so the pooled prefix is empty
2958                    // and the serial walk decides the whole frame.
2959                    recording.arcs[100].sweep_angle = 0.15;
2960                    recording.arcs[750].sweep_angle = 0.15;
2961                }
2962                12 => {
2963                    // A hit inside a segment created by the frame-5 split.
2964                    recording.arcs[500].sweep_angle = 0.15;
2965                }
2966                16..=39 => {
2967                    // The last ring changes shape wholesale and stays
2968                    // changed: its segment dies (no candidate even
2969                    // probes) behind the still-committing leading rings,
2970                    // and whatever coverage bookkeeping decides — death or
2971                    // collapse into a re-snapshot — both paths must agree.
2972                    for arc in &mut recording.arcs[600..900] {
2973                        arc.sweep_angle = 0.06;
2974                    }
2975                }
2976                40..=45 => {
2977                    // Nearly everything changes: coverage collapses below
2978                    // the floor, the state re-snapshots and re-partitions
2979                    // mid-sequence, then retains the changed shape.
2980                    for arc in &mut recording.arcs[150..900] {
2981                        arc.sweep_angle = 0.08;
2982                    }
2983                }
2984                52 => {
2985                    // A brick hit against the post-collapse capture.
2986                    recording.arcs[450].sweep_angle = 0.15;
2987                }
2988                _ => {}
2989            }
2990            recording
2991        };
2992        let mut serial = CommandReplayState::default();
2993        let mut pooled = CommandReplayState::default();
2994        for f in 0..60 {
2995            let recording = frame(f);
2996            let serial_outcome = serial.advance(&recording);
2997            let pooled_outcome = pooled.advance_pooled(&recording, Some(&exec));
2998            assert_eq!(
2999                serial_outcome, pooled_outcome,
3000                "outcome diverged at frame {f}"
3001            );
3002            assert_eq!(
3003                serial.segments(),
3004                pooled.segments(),
3005                "segments diverged at frame {f}"
3006            );
3007            assert_eq!(
3008                serial.stats(),
3009                pooled.stats(),
3010                "stats diverged at frame {f}"
3011            );
3012        }
3013        let (deaths, splits) = serial.stats();
3014        assert!(
3015            !serial.segments().is_empty() && deaths > 0 && splits > 0,
3016            "sequence must exercise retention, deaths, and splits, \
3017             got {deaths} deaths {splits} splits {} segments",
3018            serial.segments().len()
3019        );
3020        assert_eq!(serial.optimistic_commits(), 0);
3021        assert_eq!(serial.prefix_commits(), 0);
3022        assert!(
3023            pooled.optimistic_commits() >= 10,
3024            "the pooled fast path must actually commit steady frames, got {}",
3025            pooled.optimistic_commits()
3026        );
3027        assert!(
3028            pooled.prefix_commits() >= 3,
3029            "churn frames must commit their pooled prefix, got {}",
3030            pooled.prefix_commits()
3031        );
3032    }
3033
3034    #[test]
3035    fn transformed_bounds_contain_the_moved_content() {
3036        let t = RecordTransform {
3037            scale: 1.1,
3038            angle: 0.5,
3039        };
3040        let bounds = Rect {
3041            x: 150.0,
3042            y: 150.0,
3043            width: 100.0,
3044            height: 30.0,
3045        };
3046        let moved = t.apply_to_bounds(CENTER, bounds);
3047        for corner in [
3048            Point::new(bounds.x, bounds.y),
3049            Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
3050        ] {
3051            let p = t.apply(CENTER, corner);
3052            assert!(p.x >= moved.x - 1e-3 && p.x <= moved.x + moved.width + 1e-3);
3053            assert!(p.y >= moved.y - 1e-3 && p.y <= moved.y + moved.height + 1e-3);
3054        }
3055    }
3056}
3057
3058/// The lane kernels ([`match_arc_lanes`], [`match_round_rect_lanes`]) and
3059/// the run loops built on them, checked against the scalar authorities
3060/// ([`match_arc`], [`match_round_rect`]) record-for-record. The corpus
3061/// pushes every field just inside and just outside its tolerance in both
3062/// directions — at 10% margins whose side is asserted, and at knife-edge
3063/// deltas bracketing the exact f32 threshold, which keep the equivalence
3064/// assertion sensitive to sub-tolerance arithmetic drift the asserted
3065/// margins would forgive — mismatches the stroke shapes both ways, wraps
3066/// the angle through full turns, and poisons every field with NaN, both
3067/// infinities, and a denormal on either and both sides. Then every corpus
3068/// record is cross-paired with every other under several transforms
3069/// (identity, the nominal motion, a large rotation, a NaN transform, an
3070/// infinite scale), and the run loops are replayed from every start offset
3071/// with recolor lists compared BIT-FOR-BIT (a NaN color both paths
3072/// produced identically must not fail the comparison, and no color
3073/// difference may hide).
3074#[cfg(test)]
3075mod lane_kernel_equivalence {
3076    use std::f32::consts::TAU;
3077
3078    use super::*;
3079    use crate::{Color, Stroke};
3080
3081    /// Knife-edge margin around the exact threshold. Well below every
3082    /// tolerance in play (ABS_EPS is 2e-2), so a lane operand drifting by
3083    /// more than ~5e-4 flips one of the bracketing cases on the side the
3084    /// drift approaches — this is what the sabotage check leans on.
3085    const KNIFE: f32 = 5.0e-4;
3086
3087    const PIVOT: Point = Point { x: 204.0, y: 204.0 };
3088    const T: RecordTransform = RecordTransform {
3089        scale: 0.9994,
3090        angle: 0.0123,
3091    };
3092    /// A denormal: the smallest positive normal f32 is ~1.18e-38.
3093    const DENORMAL: f32 = 1.0e-40;
3094    const POISONS: [f32; 4] = [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, DENORMAL];
3095
3096    fn transforms() -> [RecordTransform; 5] {
3097        [
3098            RecordTransform::IDENTITY,
3099            T,
3100            RecordTransform {
3101                scale: 1.37,
3102                angle: 3.0,
3103            },
3104            RecordTransform {
3105                scale: f32::NAN,
3106                angle: f32::NAN,
3107            },
3108            RecordTransform {
3109                scale: f32::INFINITY,
3110                angle: 0.0,
3111            },
3112        ]
3113    }
3114
3115    /// Recolor lists compared as bit patterns: the contract is bitwise
3116    /// identity, and `Color`'s `PartialEq` would spuriously fail a NaN
3117    /// color that BOTH paths produced identically.
3118    fn bits(recolors: &[(u32, Color)]) -> Vec<(u32, [u32; 4])> {
3119        recolors
3120            .iter()
3121            .map(|&(i, Color(r, g, b, a))| {
3122                (i, [r.to_bits(), g.to_bits(), b.to_bits(), a.to_bits()])
3123            })
3124            .collect()
3125    }
3126
3127    struct Case<R> {
3128        label: String,
3129        current: R,
3130        retained: R,
3131        /// The scalar authority's verdict under [`T`], asserted where the
3132        /// case exists to prove a specific edge falls on a specific side —
3133        /// so the corpus provably exercises what it claims. `None` for
3134        /// poison cases, whose verdict the scalar function itself defines.
3135        expected: Option<RecordMatch>,
3136    }
3137
3138    // ---- arcs ----
3139
3140    fn arc_base(stroke: Option<f32>) -> SolidArcRecord {
3141        SolidArcRecord {
3142            center: PIVOT,
3143            radius: 120.0,
3144            start_angle: 1.0,
3145            sweep_angle: 0.4,
3146            inner_radius: 96.0,
3147            color: Color::WHITE,
3148            stroke: stroke.map(Stroke::new),
3149        }
3150    }
3151
3152    /// `retained` moved by exactly [`T`] — the same products and sum the
3153    /// scalar comparison forms on its own side, so the pair is Exact.
3154    fn arc_moved(retained: &SolidArcRecord) -> SolidArcRecord {
3155        SolidArcRecord {
3156            center: retained.center,
3157            radius: retained.radius * T.scale,
3158            start_angle: retained.start_angle + T.angle,
3159            sweep_angle: retained.sweep_angle,
3160            inner_radius: retained.inner_radius * T.scale,
3161            color: retained.color,
3162            stroke: retained.stroke.map(|stroke| Stroke {
3163                width: stroke.width * T.scale,
3164                ..stroke
3165            }),
3166        }
3167    }
3168
3169    type ArcGet = fn(&SolidArcRecord) -> f32;
3170    type ArcSet = fn(&mut SolidArcRecord, f32);
3171
3172    fn arc_fields() -> [(&'static str, ArcGet, ArcSet); 6] {
3173        [
3174            ("center.x", |a| a.center.x, |a, v| a.center.x = v),
3175            ("center.y", |a| a.center.y, |a, v| a.center.y = v),
3176            ("radius", |a| a.radius, |a, v| a.radius = v),
3177            (
3178                "inner_radius",
3179                |a| a.inner_radius,
3180                |a, v| a.inner_radius = v,
3181            ),
3182            ("start_angle", |a| a.start_angle, |a, v| a.start_angle = v),
3183            ("sweep_angle", |a| a.sweep_angle, |a, v| a.sweep_angle = v),
3184        ]
3185    }
3186
3187    fn arc_corpus() -> Vec<Case<SolidArcRecord>> {
3188        let mut corpus: Vec<Case<SolidArcRecord>> = Vec::new();
3189        for stroke in [None, Some(5.0_f32)] {
3190            let retained = arc_base(stroke);
3191            let matched = arc_moved(&retained);
3192            corpus.push(Case {
3193                label: format!("exact, stroke {stroke:?}"),
3194                current: matched,
3195                retained,
3196                expected: Some(RecordMatch::Exact),
3197            });
3198            let mut recolored = matched;
3199            recolored.color = Color::rgb(0.9, 0.3, 0.2);
3200            corpus.push(Case {
3201                label: format!("recolor, stroke {stroke:?}"),
3202                current: recolored,
3203                retained,
3204                expected: Some(RecordMatch::Recolor),
3205            });
3206        }
3207
3208        let retained = arc_base(None);
3209        let matched = arc_moved(&retained);
3210        for (name, get, set) in arc_fields() {
3211            let base = get(&matched);
3212            // close_angle's tolerance is the flat ABS_EPS; close_rel's
3213            // grows with the operands. 0.9/1.1 margins keep the nudge on
3214            // the intended side even where it feeds several lanes (the
3215            // center feeds two) or shifts its own threshold's `max`.
3216            let tolerance = if name == "start_angle" {
3217                ABS_EPS
3218            } else {
3219                ABS_EPS + REL_EPS * base.abs()
3220            };
3221            for sign in [1.0_f32, -1.0] {
3222                let mut inside = matched;
3223                set(&mut inside, base + sign * 0.9 * tolerance);
3224                corpus.push(Case {
3225                    label: format!("{name} just inside, sign {sign}"),
3226                    current: inside,
3227                    retained,
3228                    expected: Some(RecordMatch::Exact),
3229                });
3230                let mut outside = matched;
3231                set(&mut outside, base + sign * 1.1 * tolerance);
3232                corpus.push(Case {
3233                    label: format!("{name} just outside, sign {sign}"),
3234                    current: outside,
3235                    retained,
3236                    expected: Some(RecordMatch::Mismatch),
3237                });
3238            }
3239            for poison in POISONS {
3240                let mut current = matched;
3241                set(&mut current, poison);
3242                corpus.push(Case {
3243                    label: format!("{name} current {poison:e}"),
3244                    current,
3245                    retained,
3246                    expected: None,
3247                });
3248                let mut poisoned = retained;
3249                set(&mut poisoned, poison);
3250                corpus.push(Case {
3251                    label: format!("{name} retained {poison:e}"),
3252                    current: matched,
3253                    retained: poisoned,
3254                    expected: None,
3255                });
3256                // The same poison on BOTH sides: infinities subtract to
3257                // NaN, denormal pairs sit inside every tolerance.
3258                let mut both_current = matched;
3259                set(&mut both_current, poison);
3260                corpus.push(Case {
3261                    label: format!("{name} both {poison:e}"),
3262                    current: both_current,
3263                    retained: poisoned,
3264                    expected: None,
3265                });
3266            }
3267            // Knife edges: deltas of threshold - KNIFE, the threshold
3268            // itself, and threshold + KNIFE, both directions. Which side
3269            // the dead-on case rounds to is the scalar authority's call
3270            // (`expected: None`); what these buy is sensitivity — an
3271            // operand off by more than KNIFE in either implementation
3272            // flips one of them, where the 10% margins above would
3273            // forgive it.
3274            for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3275                for sign in [1.0_f32, -1.0] {
3276                    let mut edge = matched;
3277                    set(&mut edge, base + sign * knife);
3278                    corpus.push(Case {
3279                        label: format!("{name} knife edge {knife:e}, sign {sign}"),
3280                        current: edge,
3281                        retained,
3282                        expected: None,
3283                    });
3284                }
3285            }
3286        }
3287
3288        // Full-turn wraps take close_angle's paths the tolerance nudges
3289        // above cannot reach.
3290        for (label, delta, expected) in [
3291            ("start_angle +TAU", TAU, RecordMatch::Exact),
3292            ("start_angle -TAU", -TAU, RecordMatch::Exact),
3293            ("start_angle +3 turns", 3.0 * TAU, RecordMatch::Exact),
3294            (
3295                "start_angle short of +TAU, inside",
3296                TAU - 0.9 * ABS_EPS,
3297                RecordMatch::Exact,
3298            ),
3299            (
3300                "start_angle past +TAU, outside",
3301                TAU + 1.1 * ABS_EPS,
3302                RecordMatch::Mismatch,
3303            ),
3304        ] {
3305            let mut wrapped = matched;
3306            wrapped.start_angle += delta;
3307            corpus.push(Case {
3308                label: label.to_string(),
3309                current: wrapped,
3310                retained,
3311                expected: Some(expected),
3312            });
3313        }
3314
3315        // Stroke shapes and width edges.
3316        let stroked = arc_base(Some(5.0));
3317        let moved_stroked = arc_moved(&stroked);
3318        let mut some_vs_none = matched;
3319        some_vs_none.stroke = Some(Stroke::new(5.0 * T.scale));
3320        corpus.push(Case {
3321            label: "stroke Some vs None".to_string(),
3322            current: some_vs_none,
3323            retained,
3324            expected: Some(RecordMatch::Mismatch),
3325        });
3326        corpus.push(Case {
3327            label: "stroke None vs Some".to_string(),
3328            current: matched,
3329            retained: stroked,
3330            expected: Some(RecordMatch::Mismatch),
3331        });
3332        let width = 5.0 * T.scale;
3333        let tolerance = ABS_EPS + REL_EPS * width.abs();
3334        for sign in [1.0_f32, -1.0] {
3335            let mut inside = moved_stroked;
3336            inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
3337            corpus.push(Case {
3338                label: format!("stroke width just inside, sign {sign}"),
3339                current: inside,
3340                retained: stroked,
3341                expected: Some(RecordMatch::Exact),
3342            });
3343            let mut outside = moved_stroked;
3344            outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
3345            corpus.push(Case {
3346                label: format!("stroke width just outside, sign {sign}"),
3347                current: outside,
3348                retained: stroked,
3349                expected: Some(RecordMatch::Mismatch),
3350            });
3351            for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3352                let mut edge = moved_stroked;
3353                edge.stroke = Some(Stroke::new(width + sign * knife));
3354                corpus.push(Case {
3355                    label: format!("stroke width knife edge {knife:e}, sign {sign}"),
3356                    current: edge,
3357                    retained: stroked,
3358                    expected: None,
3359                });
3360            }
3361        }
3362        for poison in POISONS {
3363            let mut current = moved_stroked;
3364            current.stroke = Some(Stroke::new(poison));
3365            corpus.push(Case {
3366                label: format!("stroke width current {poison:e}"),
3367                current,
3368                retained: stroked,
3369                expected: None,
3370            });
3371            let mut poisoned = stroked;
3372            poisoned.stroke = Some(Stroke::new(poison));
3373            corpus.push(Case {
3374                label: format!("stroke width retained {poison:e}"),
3375                current: moved_stroked,
3376                retained: poisoned,
3377                expected: None,
3378            });
3379        }
3380
3381        // A NaN color on matching geometry: `==` is false, so BOTH paths
3382        // must call it a Recolor carrying the NaN color.
3383        let mut nan_color = matched;
3384        nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
3385        corpus.push(Case {
3386            label: "NaN color".to_string(),
3387            current: nan_color,
3388            retained,
3389            expected: Some(RecordMatch::Recolor),
3390        });
3391        corpus
3392    }
3393
3394    #[test]
3395    fn the_arc_corpus_exercises_what_it_claims() {
3396        for case in arc_corpus() {
3397            if let Some(expected) = case.expected {
3398                assert_eq!(
3399                    match_arc(&case.current, &case.retained, PIVOT, T),
3400                    expected,
3401                    "scalar verdict for `{}`",
3402                    case.label
3403                );
3404            }
3405        }
3406    }
3407
3408    #[test]
3409    fn arc_kernel_equals_the_scalar_authority_cross_paired() {
3410        let corpus = arc_corpus();
3411        for t in transforms() {
3412            for a in &corpus {
3413                for b in &corpus {
3414                    assert_eq!(
3415                        match_arc_lanes(&a.current, &b.retained, PIVOT, t.scale, t.angle),
3416                        match_arc(&a.current, &b.retained, PIVOT, t),
3417                        "arc kernel diverged: current `{}` vs retained `{}` under {t:?}",
3418                        a.label,
3419                        b.label
3420                    );
3421                }
3422            }
3423        }
3424    }
3425
3426    #[test]
3427    fn arc_run_equals_a_scalar_reference_from_every_start() {
3428        let corpus = arc_corpus();
3429        let current: Vec<SolidArcRecord> = corpus.iter().map(|case| case.current).collect();
3430        let snapshot: Vec<SolidArcRecord> = corpus.iter().map(|case| case.retained).collect();
3431        let mut fast: Vec<(u32, Color)> = Vec::new();
3432        let mut naive: Vec<(u32, Color)> = Vec::new();
3433        for start in 0..current.len() {
3434            fast.clear();
3435            naive.clear();
3436            let matched = match_arc_run(
3437                &current[start..],
3438                &snapshot[start..],
3439                PIVOT,
3440                T,
3441                7,
3442                &mut fast,
3443            );
3444            let mut mismatch = None;
3445            for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3446                match match_arc(now, then, PIVOT, T) {
3447                    RecordMatch::Exact => {}
3448                    RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3449                    RecordMatch::Mismatch => {
3450                        mismatch = Some(i);
3451                        break;
3452                    }
3453                }
3454            }
3455            let reference = mismatch.unwrap_or(current.len() - start);
3456            assert_eq!(
3457                (matched, bits(&fast)),
3458                (reference, bits(&naive)),
3459                "arc run diverged from start {start}"
3460            );
3461        }
3462    }
3463
3464    // ---- round rects ----
3465
3466    fn rr_base(stroke: Option<f32>) -> SolidRoundRectRecord {
3467        // A circle of diameter 10 at (304, 204) — off-pivot, so rotation
3468        // moves it.
3469        SolidRoundRectRecord {
3470            rect: Rect {
3471                x: 299.0,
3472                y: 199.0,
3473                width: 10.0,
3474                height: 10.0,
3475            },
3476            radii: CornerRadii::uniform(5.0),
3477            color: Color::WHITE,
3478            stroke: stroke.map(Stroke::new),
3479        }
3480    }
3481
3482    /// `retained` moved by exactly [`T`]: the center through the very
3483    /// `apply` arithmetic the scalar comparison uses, the extents through
3484    /// its exact products.
3485    fn rr_moved(retained: &SolidRoundRectRecord) -> SolidRoundRectRecord {
3486        let (c_then, d_then) = circle_view(retained).expect("the base is a circle");
3487        let c_now = T.apply(PIVOT, c_then);
3488        let d_now = d_then * T.scale;
3489        SolidRoundRectRecord {
3490            rect: Rect {
3491                x: c_now.x - d_now * 0.5,
3492                y: c_now.y - d_now * 0.5,
3493                width: d_now,
3494                height: d_now,
3495            },
3496            radii: CornerRadii::uniform(d_now * 0.5),
3497            color: retained.color,
3498            stroke: retained.stroke.map(|stroke| Stroke {
3499                width: stroke.width * T.scale,
3500                ..stroke
3501            }),
3502        }
3503    }
3504
3505    type RrGet = fn(&SolidRoundRectRecord) -> f32;
3506    type RrSet = fn(&mut SolidRoundRectRecord, f32);
3507
3508    fn rr_fields() -> [(&'static str, RrGet, RrSet); 8] {
3509        [
3510            ("rect.x", |r| r.rect.x, |r, v| r.rect.x = v),
3511            ("rect.y", |r| r.rect.y, |r, v| r.rect.y = v),
3512            ("rect.width", |r| r.rect.width, |r, v| r.rect.width = v),
3513            ("rect.height", |r| r.rect.height, |r, v| r.rect.height = v),
3514            (
3515                "radii.top_left",
3516                |r| r.radii.top_left,
3517                |r, v| r.radii.top_left = v,
3518            ),
3519            (
3520                "radii.top_right",
3521                |r| r.radii.top_right,
3522                |r, v| r.radii.top_right = v,
3523            ),
3524            (
3525                "radii.bottom_right",
3526                |r| r.radii.bottom_right,
3527                |r, v| r.radii.bottom_right = v,
3528            ),
3529            (
3530                "radii.bottom_left",
3531                |r| r.radii.bottom_left,
3532                |r, v| r.radii.bottom_left = v,
3533            ),
3534        ]
3535    }
3536
3537    fn rr_corpus() -> Vec<Case<SolidRoundRectRecord>> {
3538        let mut corpus: Vec<Case<SolidRoundRectRecord>> = Vec::new();
3539        for stroke in [None, Some(3.0_f32)] {
3540            let retained = rr_base(stroke);
3541            let matched = rr_moved(&retained);
3542            corpus.push(Case {
3543                label: format!("exact, stroke {stroke:?}"),
3544                current: matched,
3545                retained,
3546                expected: Some(RecordMatch::Exact),
3547            });
3548            let mut recolored = matched;
3549            recolored.color = Color::rgb(0.2, 0.8, 0.4);
3550            corpus.push(Case {
3551                label: format!("recolor, stroke {stroke:?}"),
3552                current: recolored,
3553                retained,
3554                expected: Some(RecordMatch::Recolor),
3555            });
3556        }
3557
3558        let retained = rr_base(None);
3559        let matched = rr_moved(&retained);
3560        for (name, get, set) in rr_fields() {
3561            let base = get(&matched);
3562            let tolerance = ABS_EPS + REL_EPS * base.abs();
3563            for sign in [1.0_f32, -1.0] {
3564                let mut inside = matched;
3565                set(&mut inside, base + sign * 0.9 * tolerance);
3566                corpus.push(Case {
3567                    label: format!("{name} just inside, sign {sign}"),
3568                    current: inside,
3569                    retained,
3570                    expected: Some(RecordMatch::Exact),
3571                });
3572                let mut outside = matched;
3573                set(&mut outside, base + sign * 1.1 * tolerance);
3574                corpus.push(Case {
3575                    label: format!("{name} just outside, sign {sign}"),
3576                    current: outside,
3577                    retained,
3578                    expected: Some(RecordMatch::Mismatch),
3579                });
3580            }
3581            for poison in POISONS {
3582                let mut current = matched;
3583                set(&mut current, poison);
3584                corpus.push(Case {
3585                    label: format!("{name} current {poison:e}"),
3586                    current,
3587                    retained,
3588                    expected: None,
3589                });
3590                let mut poisoned = retained;
3591                set(&mut poisoned, poison);
3592                corpus.push(Case {
3593                    label: format!("{name} retained {poison:e}"),
3594                    current: matched,
3595                    retained: poisoned,
3596                    expected: None,
3597                });
3598                let mut both_current = matched;
3599                set(&mut both_current, poison);
3600                corpus.push(Case {
3601                    label: format!("{name} both {poison:e}"),
3602                    current: both_current,
3603                    retained: poisoned,
3604                    expected: None,
3605                });
3606            }
3607            // Knife edges, exactly as in the arc corpus: sub-tolerance
3608            // sensitivity for the equivalence assertion.
3609            for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3610                for sign in [1.0_f32, -1.0] {
3611                    let mut edge = matched;
3612                    set(&mut edge, base + sign * knife);
3613                    corpus.push(Case {
3614                        label: format!("{name} knife edge {knife:e}, sign {sign}"),
3615                        current: edge,
3616                        retained,
3617                        expected: None,
3618                    });
3619                }
3620            }
3621        }
3622
3623        // Non-circles: the scalar path fails circle_view's `let ... else`,
3624        // the lane path its is_circle lanes — including a retained
3625        // non-circle behind a pristine current, and a non-circle paired
3626        // with ITSELF (which must still be a Mismatch).
3627        let mut squashed = matched;
3628        squashed.rect.height = squashed.rect.width * 2.0;
3629        corpus.push(Case {
3630            label: "current non-circle (squashed)".to_string(),
3631            current: squashed,
3632            retained,
3633            expected: Some(RecordMatch::Mismatch),
3634        });
3635        let mut loose_radii = retained;
3636        loose_radii.radii = CornerRadii::uniform(2.0);
3637        corpus.push(Case {
3638            label: "retained non-circle (loose radii)".to_string(),
3639            current: matched,
3640            retained: loose_radii,
3641            expected: Some(RecordMatch::Mismatch),
3642        });
3643        corpus.push(Case {
3644            label: "non-circle vs itself".to_string(),
3645            current: loose_radii,
3646            retained: loose_radii,
3647            expected: Some(RecordMatch::Mismatch),
3648        });
3649
3650        // Stroke shapes and width edges.
3651        let stroked = rr_base(Some(3.0));
3652        let moved_stroked = rr_moved(&stroked);
3653        let mut some_vs_none = matched;
3654        some_vs_none.stroke = Some(Stroke::new(3.0 * T.scale));
3655        corpus.push(Case {
3656            label: "stroke Some vs None".to_string(),
3657            current: some_vs_none,
3658            retained,
3659            expected: Some(RecordMatch::Mismatch),
3660        });
3661        corpus.push(Case {
3662            label: "stroke None vs Some".to_string(),
3663            current: matched,
3664            retained: stroked,
3665            expected: Some(RecordMatch::Mismatch),
3666        });
3667        let width = 3.0 * T.scale;
3668        let tolerance = ABS_EPS + REL_EPS * width.abs();
3669        for sign in [1.0_f32, -1.0] {
3670            let mut inside = moved_stroked;
3671            inside.stroke = Some(Stroke::new(width + sign * 0.9 * tolerance));
3672            corpus.push(Case {
3673                label: format!("stroke width just inside, sign {sign}"),
3674                current: inside,
3675                retained: stroked,
3676                expected: Some(RecordMatch::Exact),
3677            });
3678            let mut outside = moved_stroked;
3679            outside.stroke = Some(Stroke::new(width + sign * 1.1 * tolerance));
3680            corpus.push(Case {
3681                label: format!("stroke width just outside, sign {sign}"),
3682                current: outside,
3683                retained: stroked,
3684                expected: Some(RecordMatch::Mismatch),
3685            });
3686            for knife in [tolerance - KNIFE, tolerance, tolerance + KNIFE] {
3687                let mut edge = moved_stroked;
3688                edge.stroke = Some(Stroke::new(width + sign * knife));
3689                corpus.push(Case {
3690                    label: format!("stroke width knife edge {knife:e}, sign {sign}"),
3691                    current: edge,
3692                    retained: stroked,
3693                    expected: None,
3694                });
3695            }
3696        }
3697        for poison in POISONS {
3698            let mut current = moved_stroked;
3699            current.stroke = Some(Stroke::new(poison));
3700            corpus.push(Case {
3701                label: format!("stroke width current {poison:e}"),
3702                current,
3703                retained: stroked,
3704                expected: None,
3705            });
3706        }
3707
3708        let mut nan_color = matched;
3709        nan_color.color = Color(f32::NAN, 0.5, 0.5, 1.0);
3710        corpus.push(Case {
3711            label: "NaN color".to_string(),
3712            current: nan_color,
3713            retained,
3714            expected: Some(RecordMatch::Recolor),
3715        });
3716        corpus
3717    }
3718
3719    #[test]
3720    fn the_round_rect_corpus_exercises_what_it_claims() {
3721        for case in rr_corpus() {
3722            if let Some(expected) = case.expected {
3723                assert_eq!(
3724                    match_round_rect(&case.current, &case.retained, PIVOT, T),
3725                    expected,
3726                    "scalar verdict for `{}`",
3727                    case.label
3728                );
3729            }
3730        }
3731    }
3732
3733    #[test]
3734    fn round_rect_kernel_equals_the_scalar_authority_cross_paired() {
3735        let corpus = rr_corpus();
3736        for t in transforms() {
3737            let (sin, cos) = t.angle.sin_cos();
3738            for a in &corpus {
3739                for b in &corpus {
3740                    assert_eq!(
3741                        match_round_rect_lanes(&a.current, &b.retained, PIVOT, t.scale, sin, cos),
3742                        match_round_rect(&a.current, &b.retained, PIVOT, t),
3743                        "round-rect kernel diverged: current `{}` vs retained `{}` under {t:?}",
3744                        a.label,
3745                        b.label
3746                    );
3747                }
3748            }
3749        }
3750    }
3751
3752    #[test]
3753    fn round_rect_run_equals_a_scalar_reference_from_every_start() {
3754        let corpus = rr_corpus();
3755        let current: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.current).collect();
3756        let snapshot: Vec<SolidRoundRectRecord> = corpus.iter().map(|case| case.retained).collect();
3757        let mut fast: Vec<(u32, Color)> = Vec::new();
3758        let mut naive: Vec<(u32, Color)> = Vec::new();
3759        for start in 0..current.len() {
3760            fast.clear();
3761            naive.clear();
3762            let matched = match_round_rect_run(
3763                &current[start..],
3764                &snapshot[start..],
3765                PIVOT,
3766                T,
3767                7,
3768                &mut fast,
3769            );
3770            let mut mismatch = None;
3771            for (i, (now, then)) in current[start..].iter().zip(&snapshot[start..]).enumerate() {
3772                match match_round_rect(now, then, PIVOT, T) {
3773                    RecordMatch::Exact => {}
3774                    RecordMatch::Recolor => naive.push(((7 + i) as u32, now.color)),
3775                    RecordMatch::Mismatch => {
3776                        mismatch = Some(i);
3777                        break;
3778                    }
3779                }
3780            }
3781            let reference = mismatch.unwrap_or(current.len() - start);
3782            assert_eq!(
3783                (matched, bits(&fast)),
3784                (reference, bits(&naive)),
3785                "round-rect run diverged from start {start}"
3786            );
3787        }
3788    }
3789
3790    // ---- arbitrary bit patterns ----
3791
3792    /// A deterministic xorshift over raw bit patterns — half the values
3793    /// squashed into a plausible coordinate range, half left as arbitrary
3794    /// bits (NaN payloads, infinities, denormals, huge magnitudes) — so the
3795    /// kernels meet float shapes no hand-written corpus anticipates.
3796    struct XorShift(u32);
3797
3798    impl XorShift {
3799        fn next(&mut self) -> u32 {
3800            let mut x = self.0;
3801            x ^= x << 13;
3802            x ^= x >> 17;
3803            x ^= x << 5;
3804            self.0 = x;
3805            x
3806        }
3807
3808        fn f32(&mut self) -> f32 {
3809            if self.next() & 1 == 0 {
3810                (self.next() as f32 / u32::MAX as f32) * 1000.0 - 500.0
3811            } else {
3812                f32::from_bits(self.next())
3813            }
3814        }
3815
3816        fn stroke(&mut self) -> Option<Stroke> {
3817            (self.next() & 1 == 0).then(|| Stroke::new(self.f32()))
3818        }
3819
3820        fn arc(&mut self) -> SolidArcRecord {
3821            SolidArcRecord {
3822                center: Point::new(self.f32(), self.f32()),
3823                radius: self.f32(),
3824                start_angle: self.f32(),
3825                sweep_angle: self.f32(),
3826                inner_radius: self.f32(),
3827                color: Color::WHITE,
3828                stroke: self.stroke(),
3829            }
3830        }
3831
3832        fn round_rect(&mut self) -> SolidRoundRectRecord {
3833            SolidRoundRectRecord {
3834                rect: Rect {
3835                    x: self.f32(),
3836                    y: self.f32(),
3837                    width: self.f32(),
3838                    height: self.f32(),
3839                },
3840                radii: CornerRadii {
3841                    top_left: self.f32(),
3842                    top_right: self.f32(),
3843                    bottom_right: self.f32(),
3844                    bottom_left: self.f32(),
3845                },
3846                color: Color::WHITE,
3847                stroke: self.stroke(),
3848            }
3849        }
3850    }
3851
3852    #[test]
3853    fn kernels_equal_the_authorities_on_arbitrary_bit_patterns() {
3854        let mut rng = XorShift(0x9e37_79b9);
3855        for _ in 0..4000 {
3856            let t = RecordTransform {
3857                scale: rng.f32(),
3858                angle: rng.f32(),
3859            };
3860            let (sin, cos) = t.angle.sin_cos();
3861            let (a, b) = (rng.arc(), rng.arc());
3862            assert_eq!(
3863                match_arc_lanes(&a, &b, PIVOT, t.scale, t.angle),
3864                match_arc(&a, &b, PIVOT, t),
3865                "arc kernel diverged: {a:?} vs {b:?} under {t:?}"
3866            );
3867            let (c, d) = (rng.round_rect(), rng.round_rect());
3868            assert_eq!(
3869                match_round_rect_lanes(&c, &d, PIVOT, t.scale, sin, cos),
3870                match_round_rect(&c, &d, PIVOT, t),
3871                "round-rect kernel diverged: {c:?} vs {d:?} under {t:?}"
3872            );
3873        }
3874    }
3875}