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