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    let mut d = (a - b) % TAU;
45    if d > TAU * 0.5 {
46        d -= TAU;
47    }
48    if d < -TAU * 0.5 {
49        d += TAU;
50    }
51    d.abs() <= ABS_EPS
52}
53
54fn close_point(a: Point, b: Point) -> bool {
55    close_rel(a.x, b.x) && close_rel(a.y, b.y)
56}
57
58/// One segment's frame-over-frame motion: uniform scale and rotation about
59/// a shared external center.
60#[derive(Clone, Copy, Debug, PartialEq)]
61pub struct RecordTransform {
62    pub scale: f32,
63    pub angle: f32,
64}
65
66impl RecordTransform {
67    pub const IDENTITY: Self = Self {
68        scale: 1.0,
69        angle: 0.0,
70    };
71
72    pub fn apply(&self, center: Point, p: Point) -> Point {
73        let (sin, cos) = self.angle.sin_cos();
74        let dx = p.x - center.x;
75        let dy = p.y - center.y;
76        Point::new(
77            center.x + (dx * cos - dy * sin) * self.scale,
78            center.y + (dx * sin + dy * cos) * self.scale,
79        )
80    }
81
82    /// Axis-aligned bounds of `bounds` after the transform: the four
83    /// transformed corners' box. This is the once-per-group bound transform
84    /// that replaces per-entry tight-bounds recomputation for retained
85    /// content.
86    pub fn apply_to_bounds(&self, center: Point, bounds: Rect) -> Rect {
87        let corners = [
88            Point::new(bounds.x, bounds.y),
89            Point::new(bounds.x + bounds.width, bounds.y),
90            Point::new(bounds.x, bounds.y + bounds.height),
91            Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
92        ];
93        let mut min_x = f32::INFINITY;
94        let mut min_y = f32::INFINITY;
95        let mut max_x = f32::NEG_INFINITY;
96        let mut max_y = f32::NEG_INFINITY;
97        for corner in corners {
98            let p = self.apply(center, corner);
99            min_x = min_x.min(p.x);
100            min_y = min_y.min(p.y);
101            max_x = max_x.max(p.x);
102            max_y = max_y.max(p.y);
103        }
104        Rect {
105            x: min_x,
106            y: min_y,
107            width: max_x - min_x,
108            height: max_y - min_y,
109        }
110    }
111}
112
113/// Whether an entry's own implied transform is tightly consistent with a
114/// chain's anchor transform. `pinned` marks transforms whose angle is
115/// meaningful — an on-pivot circle pins no rotation and joins any chain.
116pub fn transforms_group(
117    entry: RecordTransform,
118    entry_pinned: bool,
119    anchor: RecordTransform,
120) -> bool {
121    use std::f32::consts::TAU;
122    if (entry.scale - anchor.scale).abs() > GROUP_SCALE_EPS * anchor.scale.abs().max(1.0) {
123        return false;
124    }
125    if !entry_pinned {
126        return true;
127    }
128    let mut d = (entry.angle - anchor.angle) % TAU;
129    if d > TAU * 0.5 {
130        d -= TAU;
131    }
132    if d < -TAU * 0.5 {
133        d += TAU;
134    }
135    d.abs() <= GROUP_ANGLE_EPS
136}
137
138/// The result of verifying one incoming record against its retained
139/// counterpart under a segment transform.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub enum RecordMatch {
142    Exact,
143    /// Geometry matched; only the solid color moved. Replayable with a
144    /// 16-byte patch into the retained buffer.
145    Recolor,
146    Mismatch,
147}
148
149/// A circular round-rect (corner radius == half extent on every corner):
150/// the one rect family that stays itself under rotation about an external
151/// pivot. Returns `(center, diameter)`.
152pub fn circle_view(record: &SolidRoundRectRecord) -> Option<(Point, f32)> {
153    if !is_circle(record.rect, record.radii) {
154        return None;
155    }
156    Some((
157        Point::new(
158            record.rect.x + record.rect.width * 0.5,
159            record.rect.y + record.rect.height * 0.5,
160        ),
161        record.rect.width,
162    ))
163}
164
165/// Whether corner radii + extents describe a circle.
166pub fn is_circle(rect: Rect, radii: CornerRadii) -> bool {
167    let half = rect.width * 0.5;
168    close_rel(rect.width, rect.height)
169        && close_rel(radii.top_left, half)
170        && close_rel(radii.top_right, half)
171        && close_rel(radii.bottom_right, half)
172        && close_rel(radii.bottom_left, half)
173}
174
175fn stroke_width(record_stroke: Option<crate::Stroke>) -> Option<f32> {
176    record_stroke.map(|stroke| stroke.width)
177}
178
179/// Similarity-invariant compatibility of a fresh arc with a retained one,
180/// for re-locating a segment when dynamic spans change length. Colors are
181/// deliberately excluded — a twinkling anchor must still re-anchor its
182/// segment. A false positive costs a failed probe, never a wrong pixel.
183pub fn arcs_anchor_compatible(current: &SolidArcRecord, anchor: &SolidArcRecord) -> bool {
184    close_rel(current.sweep_angle, anchor.sweep_angle)
185        && current.stroke.is_some() == anchor.stroke.is_some()
186}
187
188/// Derives the segment transform from an arc anchor pair. Arcs pin both
189/// scale and rotation exactly.
190pub fn arc_anchor_transform(
191    current: &SolidArcRecord,
192    retained: &SolidArcRecord,
193) -> Option<RecordTransform> {
194    if retained.radius <= f32::EPSILON {
195        return None;
196    }
197    Some(RecordTransform {
198        scale: current.radius / retained.radius,
199        angle: current.start_angle - retained.start_angle,
200    })
201}
202
203/// Derives the segment transform from a circle anchor pair, with its
204/// pinnedness (an on-pivot circle pins no rotation).
205pub fn circle_anchor_transform_pinned(
206    current: (Point, f32),
207    retained: (Point, f32),
208    center: Point,
209) -> Option<(RecordTransform, bool)> {
210    let (c_now, d_now) = current;
211    let (c_then, d_then) = retained;
212    if d_then <= f32::EPSILON {
213        return None;
214    }
215    let scale = d_now / d_then;
216    let dx_then = c_then.x - center.x;
217    let dy_then = c_then.y - center.y;
218    let pinned = dx_then * dx_then + dy_then * dy_then > 1.0;
219    let angle = if pinned {
220        let dx_now = c_now.x - center.x;
221        let dy_now = c_now.y - center.y;
222        dy_now.atan2(dx_now) - dy_then.atan2(dx_then)
223    } else {
224        0.0
225    };
226    Some((RecordTransform { scale, angle }, pinned))
227}
228
229/// Verifies a fresh arc record against the retained one under `t`. Arc
230/// centers must sit on the shared pivot — that is what makes rotation a
231/// value change instead of a position change.
232pub fn match_arc(
233    current: &SolidArcRecord,
234    retained: &SolidArcRecord,
235    center: Point,
236    t: RecordTransform,
237) -> RecordMatch {
238    let geometry_ok = close_point(current.center, retained.center)
239        && close_point(current.center, center)
240        && close_rel(current.radius, retained.radius * t.scale)
241        && close_rel(current.inner_radius, retained.inner_radius * t.scale)
242        && close_angle(current.start_angle, retained.start_angle + t.angle)
243        && close_rel(current.sweep_angle, retained.sweep_angle)
244        && match (stroke_width(current.stroke), stroke_width(retained.stroke)) {
245            (None, None) => true,
246            (Some(now), Some(then)) => close_rel(now, then * t.scale),
247            _ => false,
248        };
249    if !geometry_ok {
250        return RecordMatch::Mismatch;
251    }
252    if current.color == retained.color {
253        RecordMatch::Exact
254    } else {
255        RecordMatch::Recolor
256    }
257}
258
259/// Verifies a fresh circular round-rect against the retained one under `t`.
260/// Non-circular round rects never match — they do not survive rotation
261/// about an external pivot.
262pub fn match_round_rect(
263    current: &SolidRoundRectRecord,
264    retained: &SolidRoundRectRecord,
265    center: Point,
266    t: RecordTransform,
267) -> RecordMatch {
268    let (Some((c_now, d_now)), Some((c_then, d_then))) =
269        (circle_view(current), circle_view(retained))
270    else {
271        return RecordMatch::Mismatch;
272    };
273    let geometry_ok = close_point(c_now, t.apply(center, c_then))
274        && close_rel(d_now, d_then * t.scale)
275        && match (stroke_width(current.stroke), stroke_width(retained.stroke)) {
276            (None, None) => true,
277            (Some(now), Some(then)) => close_rel(now, then * t.scale),
278            _ => false,
279        };
280    if !geometry_ok {
281        return RecordMatch::Mismatch;
282    }
283    if current.color == retained.color {
284        RecordMatch::Exact
285    } else {
286        RecordMatch::Recolor
287    }
288}
289
290/// Below this many entries a stable stretch is not worth a retained group.
291/// Mirrors the flat-list detector.
292pub const MIN_SEGMENT_RECORDS: usize = 128;
293/// Chains longer than this split into multiple groups, bounding the blast
294/// radius of any one entry going dynamic later.
295pub const MAX_SEGMENT_RECORDS: usize = 2048;
296/// Below this many records a command is not worth watching at all.
297pub const MIN_REPLAY_COMMAND_RECORDS: usize = 512;
298/// Structural-resync search span when entity churn inserts/removes entries
299/// between frames. Mirrors the flat-list detector's bounded resync.
300const RESYNC_SPAN: usize = 48;
301const MAX_RESYNC_EVENTS: usize = 512;
302/// How far past its expected position a segment anchor may drift when the
303/// dynamic spans between segments change length.
304const RESYNC_WINDOW: usize = 1024;
305/// Entries probed under a candidate anchor transform before committing to a
306/// full-segment verification.
307const ANCHOR_PROBE_RECORDS: usize = 4;
308/// Full-span verifications a segment may commit to per frame. Self-similar
309/// rings can pass the probe from a wrong anchor (every entry shares the
310/// candidate's radius and angle step), so one failed commitment must not
311/// abandon the search — but unbounded re-verification of 2048-entry spans
312/// must not either.
313const MAX_COMMIT_ATTEMPTS: usize = 4;
314/// When live coverage sinks below this fraction of the retained records,
315/// re-partition from scratch.
316const MIN_COVERAGE_FRACTION: f32 = 0.5;
317/// Coverage eroding this far below what the capture achieved re-partitions
318/// to win dead ranges back — deaths are permanent otherwise, while the
319/// content they covered usually stabilizes again a moment later.
320const RECAPTURE_EROSION: f32 = 0.05;
321/// Frames a capture must survive before erosion alone may retire it. Keeps
322/// an inherently churning scene from recapturing in a loop — at worst one
323/// two-frame recapture per cooldown.
324const RECAPTURE_COOLDOWN_FRAMES: u32 = 180;
325
326/// The similarity-checkable view of one tape entry: which typed store it
327/// lives in and its index there. `None` marks entries replay cannot carry
328/// (plain rects, ordinary primitives) — they break segments wherever they
329/// sit.
330#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331enum ReplayView {
332    Arc(usize),
333    RoundRect(usize),
334}
335
336/// The replay-checkable view of tape entry `i`, decoded on the fly from the
337/// tagged tape: `None` for entries replay cannot carry (plain rects,
338/// ordinary primitives, non-circular round rects). This is THE eligibility
339/// rule — both the `&CommandRecording` form and the [`TypedRecords`] form
340/// delegate here, so they cannot drift.
341fn view_at_slices(
342    tape: &[TapeRef],
343    round_rects: &[SolidRoundRectRecord],
344    i: usize,
345) -> Option<ReplayView> {
346    let entry = tape[i];
347    match entry.kind() {
348        RecordKind::SolidArc => Some(ReplayView::Arc(entry.index())),
349        // Non-circular round rects cannot survive rotation about an
350        // external pivot; they stay dynamic.
351        RecordKind::SolidRoundRect => circle_view(&round_rects[entry.index()])
352            .is_some()
353            .then_some(ReplayView::RoundRect(entry.index())),
354        RecordKind::SolidRect | RecordKind::Other => None,
355    }
356}
357
358/// [`view_at_slices`] over a whole recording.
359fn view_at(recording: &CommandRecording, i: usize) -> Option<ReplayView> {
360    view_at_slices(&recording.tape, &recording.round_rects, i)
361}
362
363/// The shared rotation/scale pivot of a recording: the first arc's center.
364fn detect_center(recording: &CommandRecording) -> Option<Point> {
365    recording.arcs.first().map(|arc| arc.center)
366}
367
368/// Similarity-invariant compatibility of a current entry with a retained
369/// one, for structural pairing under churn. Colors excluded by design.
370fn views_compatible(
371    current: &CommandRecording,
372    current_view: Option<ReplayView>,
373    retained: &CommandRecording,
374    retained_view: Option<ReplayView>,
375) -> bool {
376    match (current_view, retained_view) {
377        (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
378            arcs_anchor_compatible(&current.arcs[i], &retained.arcs[j])
379        }
380        (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
381            let now = current.round_rects[i].stroke.is_some();
382            let then = retained.round_rects[j].stroke.is_some();
383            now == then
384        }
385        (None, None) => true,
386        _ => false,
387    }
388}
389
390/// Pairs current tape entries with retained tape entries, tolerating bounded
391/// insertions and deletions (entity churn between frames). Pairing is
392/// structural only; transform-consistency during verification decides
393/// whether a pair actually moved together, so a wrong pairing costs a
394/// segment, never a wrong capture.
395fn align_recordings(current: &CommandRecording, retained: &CommandRecording) -> Vec<Option<usize>> {
396    let pair = |i: usize, j: usize| -> bool {
397        views_compatible(current, view_at(current, i), retained, view_at(retained, j))
398    };
399    let current_len = current.tape.len();
400    let retained_len = retained.tape.len();
401    let mut aligned = vec![None; current_len];
402    let (mut i, mut j) = (0usize, 0usize);
403    let mut events = 0usize;
404    while i < current_len && j < retained_len {
405        if pair(i, j) {
406            aligned[i] = Some(j);
407            i += 1;
408            j += 1;
409            continue;
410        }
411        events += 1;
412        if events > MAX_RESYNC_EVENTS {
413            // Not churn — the structure is gone. An empty alignment makes
414            // the caller restart from a fresh snapshot.
415            return vec![None; current_len];
416        }
417        let mut resynced = false;
418        'search: for total in 1..=RESYNC_SPAN {
419            for di in 0..=total {
420                let dj = total - di;
421                if i + di < current_len && j + dj < retained_len && pair(i + di, j + dj) {
422                    i += di;
423                    j += dj;
424                    resynced = true;
425                    break 'search;
426                }
427            }
428        }
429        if !resynced {
430            i += 1;
431            j += 1;
432        }
433    }
434    aligned
435}
436
437/// Derives the pair's implied transform, with pinnedness.
438fn pair_transform(
439    current: &CommandRecording,
440    current_view: ReplayView,
441    retained: &CommandRecording,
442    retained_view: ReplayView,
443    center: Point,
444) -> Option<(RecordTransform, bool)> {
445    match (current_view, retained_view) {
446        (ReplayView::Arc(i), ReplayView::Arc(j)) => {
447            arc_anchor_transform(&current.arcs[i], &retained.arcs[j]).map(|t| (t, true))
448        }
449        (ReplayView::RoundRect(i), ReplayView::RoundRect(j)) => {
450            let now = circle_view(&current.round_rects[i])?;
451            let then = circle_view(&retained.round_rects[j])?;
452            circle_anchor_transform_pinned(now, then, center)
453        }
454        _ => None,
455    }
456}
457
458/// Verifies one aligned pair under a segment transform.
459fn match_pair(
460    current: &CommandRecording,
461    current_view: ReplayView,
462    retained: &CommandRecording,
463    retained_view: ReplayView,
464    center: Point,
465    t: RecordTransform,
466) -> RecordMatch {
467    match (current_view, retained_view) {
468        (ReplayView::Arc(i), ReplayView::Arc(j)) => {
469            match_arc(&current.arcs[i], &retained.arcs[j], center, t)
470        }
471        (ReplayView::RoundRect(i), ReplayView::RoundRect(j)) => {
472            match_round_rect(&current.round_rects[i], &retained.round_rects[j], center, t)
473        }
474        _ => RecordMatch::Mismatch,
475    }
476}
477
478/// Loose logical bounds of a retained tape range: shapes bound by their full
479/// outer circle. Visibility culling only needs containment.
480fn range_bounds(recording: &CommandRecording, range: (usize, usize)) -> Rect {
481    let mut min_x = f32::INFINITY;
482    let mut min_y = f32::INFINITY;
483    let mut max_x = f32::NEG_INFINITY;
484    let mut max_y = f32::NEG_INFINITY;
485    for view in (range.0..range.1).filter_map(|i| view_at(recording, i)) {
486        let (center, reach) = match view {
487            ReplayView::Arc(i) => {
488                let arc = &recording.arcs[i];
489                (
490                    arc.center,
491                    arc.radius + arc.stroke.map(|stroke| stroke.width).unwrap_or(0.0),
492                )
493            }
494            ReplayView::RoundRect(i) => {
495                let record = &recording.round_rects[i];
496                let Some((center, diameter)) = circle_view(record) else {
497                    continue;
498                };
499                (
500                    center,
501                    diameter * 0.5 + record.stroke.map(|stroke| stroke.width).unwrap_or(0.0),
502                )
503            }
504        };
505        let reach = reach + 2.0;
506        min_x = min_x.min(center.x - reach);
507        min_y = min_y.min(center.y - reach);
508        max_x = max_x.max(center.x + reach);
509        max_y = max_y.max(center.y + reach);
510    }
511    if min_x > max_x {
512        return Rect {
513            x: 0.0,
514            y: 0.0,
515            width: 0.0,
516            height: 0.0,
517        };
518    }
519    Rect {
520        x: min_x,
521        y: min_y,
522        width: max_x - min_x,
523        height: max_y - min_y,
524    }
525}
526
527/// One retained stretch of a command's recording, addressed by the retained
528/// snapshot's tape range. The `id` is stable for the segment's lifetime —
529/// renderer-side retained slots key on it, and it survives other segments
530/// dying.
531#[derive(Clone, Debug, PartialEq)]
532pub struct CommandSegment {
533    /// The capture identity this segment's content lives under: renderer
534    /// retained slots key on the (command, slot) pair. Slot ids are
535    /// allocated at partition, whose emission carries the capture content;
536    /// split pieces inherit the parent's slot and address into it, so a
537    /// split never needs a recapture.
538    pub slot: u32,
539    /// This segment's first record within the slot's captured content.
540    pub slot_offset: usize,
541    pub tape_start: usize,
542    pub tape_end: usize,
543    /// Loose logical bounds at capture.
544    pub bounds: Rect,
545}
546
547/// One span of this frame's recording, in tape order.
548#[derive(Clone, Debug, PartialEq)]
549pub enum ReplaySpan {
550    /// The retained segment moved by `transform`; `recolors` are
551    /// (span-relative record offset, new color) patches.
552    Retained {
553        /// The capture identity ([`CommandSegment::slot`]).
554        slot: u32,
555        /// True only on partition frames, where the snapshot IS the current
556        /// frame: this span's records are the slot's capture content and
557        /// `transform` is identity. Every later frame's transform is motion
558        /// since exactly that content — never double-applied.
559        capture: bool,
560        /// The span's first record within the slot's captured content.
561        slot_offset: usize,
562        /// Where the span sits in the CURRENT frame's tape.
563        tape_start: usize,
564        tape_end: usize,
565        transform: RecordTransform,
566        recolors: Vec<(u32, Color)>,
567        /// Segment capture bounds under this frame's transform.
568        bounds: Rect,
569    },
570    /// Materialize these current-tape entries through the ordinary path.
571    Dynamic { tape_start: usize, tape_end: usize },
572}
573
574/// What one frame of verification decided for a command.
575#[derive(Debug, PartialEq)]
576pub enum ReplayOutcome {
577    /// No retention this frame: materialize the whole recording.
578    AllDynamic,
579    /// The interleaved retained/dynamic structure of this frame, in exact
580    /// tape order.
581    Spans(Vec<ReplaySpan>),
582}
583
584/// Fans independent verification bodies across worker threads. `run(i)` is
585/// called exactly once for every `i in 0..jobs`, from any thread; the call
586/// returns only after every job finished (jobs borrow the caller's stack).
587/// The renderer wires its frame worker pool in through this seam so the
588/// recorder crate stays free of threading machinery.
589pub trait VerifyExecutor: Sync {
590    fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync));
591}
592
593/// A command's replay verdict translated into the space its consumers see:
594/// spans address the run's materialized primitive vector, not the record
595/// tape. This is what rides the render graph next to the primitives.
596#[derive(Clone, Debug)]
597pub struct CommandReplayFrame {
598    /// The similarity pivot every span transform rotates and scales about.
599    pub center: Point,
600    /// Interleaved retained/dynamic structure in exact z order.
601    pub spans: Vec<FrameSpan>,
602    /// The frame-owned rematerialization source: the exact recording this
603    /// frame's spans address, pinned for the frame's lifetime. A bypassed
604    /// span (empty primitive range) that cannot draw retained materializes
605    /// its `tape_range` from HERE — never from a sweepable ambient registry,
606    /// whose contents may have moved on by render time. `None` only before
607    /// the recording is published (the builder attaches the published
608    /// handle) or on hand-built frames with nothing bypassed. Shared, not
609    /// cloned: the handle pins the recording buffers; the depth-one frame
610    /// packet will carry this same handle as an `Arc` when the graph goes
611    /// `Send`.
612    pub fallback: Option<std::rc::Rc<crate::geometry::CommandRecording>>,
613}
614
615impl PartialEq for CommandReplayFrame {
616    fn eq(&self, other: &Self) -> bool {
617        self.center == other.center
618            && self.spans == other.spans
619            && match (&self.fallback, &other.fallback) {
620                (None, None) => true,
621                (Some(a), Some(b)) => std::rc::Rc::ptr_eq(a, b),
622                _ => false,
623            }
624    }
625}
626
627/// One primitive-space span of a [`CommandReplayFrame`].
628#[derive(Clone, Debug, PartialEq)]
629pub enum FrameSpan {
630    Retained {
631        /// The capture identity; renderer retained slots key on the
632        /// (command, slot) pair.
633        slot: u32,
634        /// True only when `range` holds the slot's full capture content
635        /// (partition frames, transform identity): retain it under the
636        /// slot's identity.
637        capture: bool,
638        /// The span's first primitive within the slot's captured content.
639        slot_offset: u32,
640        /// The span's primitives in the run's primitive vector. EMPTY when
641        /// the span was bypassed — its records were never materialized and
642        /// the renderer draws it from the retained slot, or asks the
643        /// recorder to materialize `tape_range` on demand when it cannot.
644        range: (u32, u32),
645        /// The span's records in the command's recording tape, for
646        /// emergency rematerialization of a bypassed span.
647        tape_range: (u32, u32),
648        transform: RecordTransform,
649        /// (span-relative primitive offset, new solid color) patches.
650        recolors: Vec<(u32, Color)>,
651        /// Capture bounds under this frame's transform.
652        bounds: Rect,
653    },
654    Dynamic {
655        /// Ordinary primitives in the run's primitive vector.
656        range: (u32, u32),
657    },
658}
659
660#[derive(Clone, Copy, Debug, PartialEq, Eq)]
661enum CommandReplayPhase {
662    Idle,
663    Snapshotted,
664    Captured,
665}
666
667/// A pooled span job's result: the cleanly matched prefix length and the
668/// recolors within it. One slot per segment, reused across frames — see
669/// [`CommandReplayState::verify_results`].
670#[derive(Debug, Default)]
671struct SpanResultSlot {
672    matched: usize,
673    recolors: Vec<(u32, Color)>,
674}
675
676/// Per-command replay state: the retained snapshot (previous stable form of
677/// the recording) and the segments carved out of it. This is the double
678/// buffer sol's plan sanctions — previous and current forms coexist only
679/// for comparison.
680#[derive(Debug)]
681pub struct CommandReplayState {
682    phase: CommandReplayPhase,
683    center: Point,
684    snapshot: CommandRecording,
685    segments: Vec<CommandSegment>,
686    next_slot_id: u32,
687    lifetime_deaths: u64,
688    lifetime_splits: u64,
689    /// Fraction of the tape the capture covered when it was taken. Dead
690    /// segments never come back on their own, so coverage eroding well
691    /// below this watermark means stable content sits unwatched — worth
692    /// paying a recapture for.
693    capture_coverage: f32,
694    frames_since_capture: u32,
695    /// Frames the pooled fast path fully committed — diagnostics for
696    /// judging how often verification actually parallelizes.
697    optimistic_commits: u64,
698    /// Frames where the pooled pass committed a non-empty strict prefix of
699    /// the segments and the serial walk ran only from the first failure —
700    /// diagnostics for the churn frames (a brick hit) that used to redo the
701    /// whole tape serially.
702    prefix_commits: u64,
703    /// Reusable per-job result slots for the pooled fast path — one slot
704    /// per segment, grown once, recolor capacity retained across frames.
705    /// The Mutex is uncontended (each job writes only its own slot once);
706    /// what this kills is the per-frame allocation of the results vector,
707    /// its mutexes, and every job's recolors vector. Each committed span —
708    /// the whole frame, or the prefix before the first failure —
709    /// `mem::take`s its slot's recolors: the buffer walks into the graph
710    /// and the slot re-grows next frame (accepted: emitting spans do real
711    /// work). Uncommitted slots keep their buffers warm.
712    verify_results: Vec<std::sync::Mutex<SpanResultSlot>>,
713    /// The serial walk's recolor buffer, refilled by every `match_span`
714    /// commit attempt. An emitted span `mem::take`s the contents and the
715    /// scratch re-grows on the next attempt — same accepted emit-cost as
716    /// the pooled slots.
717    recolor_scratch: Vec<(u32, Color)>,
718    /// The best-prefix recolors during the serial walk's candidate scan,
719    /// swapped with `recolor_scratch` whenever a longer prefix turns up.
720    best_recolor_scratch: Vec<(u32, Color)>,
721    /// Serial-walk segment queues, persistent so their buffers keep their
722    /// high-water capacity; refilled per verified frame.
723    verify_pending: std::collections::VecDeque<CommandSegment>,
724    verify_survivors: Vec<CommandSegment>,
725}
726
727impl Default for CommandReplayState {
728    fn default() -> Self {
729        Self {
730            phase: CommandReplayPhase::Idle,
731            center: Point::new(0.0, 0.0),
732            snapshot: CommandRecording::default(),
733            segments: Vec::new(),
734            next_slot_id: 0,
735            lifetime_deaths: 0,
736            lifetime_splits: 0,
737            capture_coverage: 0.0,
738            frames_since_capture: 0,
739            optimistic_commits: 0,
740            prefix_commits: 0,
741            verify_results: Vec::new(),
742            recolor_scratch: Vec::new(),
743            best_recolor_scratch: Vec::new(),
744            verify_pending: std::collections::VecDeque::new(),
745            verify_survivors: Vec::new(),
746        }
747    }
748}
749
750impl CommandReplayState {
751    pub fn segments(&self) -> &[CommandSegment] {
752        &self.segments
753    }
754
755    /// Lifetime (deaths, splits) across every verified frame — diagnostics
756    /// for judging how churn interacts with retention.
757    pub fn stats(&self) -> (u64, u64) {
758        (self.lifetime_deaths, self.lifetime_splits)
759    }
760
761    /// Frames the pooled fast path fully committed (0 without an executor).
762    pub fn optimistic_commits(&self) -> u64 {
763        self.optimistic_commits
764    }
765
766    /// Frames where the pooled pass committed a non-empty strict prefix of
767    /// the segments before handing the serial walk the failure point
768    /// (0 without an executor).
769    pub fn prefix_commits(&self) -> u64 {
770        self.prefix_commits
771    }
772
773    /// The similarity pivot all span transforms rotate and scale about.
774    pub fn center(&self) -> Point {
775        self.center
776    }
777
778    /// Advances the state machine with this frame's recording and returns
779    /// what the frame can retain. Phases mirror the flat-list detector:
780    /// snapshot on the first sighting, partition into
781    /// transform-consistent chains on the second, verify per entry from the
782    /// third on. A structural collapse or coverage erosion re-snapshots;
783    /// correctness never depends on the detector being right about
784    /// stability — a wrong guess costs a frame of ordinary rendering.
785    pub fn advance(&mut self, current: &CommandRecording) -> ReplayOutcome {
786        self.advance_pooled(current, None)
787    }
788
789    /// [`Self::advance`] with an optional executor that verification fans
790    /// its per-segment span matching across. Anchors are located in a
791    /// serial phase that uses the exact candidate order of the serial walk;
792    /// only the span bodies fan out. A frame where every body matches whole
793    /// commits without touching the serial walk; any other frame commits
794    /// the segments strictly before the first failure — equal by
795    /// construction to what the serial walk produces for them — and runs
796    /// the serial split/death/re-snapshot machinery from the failure point
797    /// on. The outcome is identical with and without an executor.
798    pub fn advance_pooled(
799        &mut self,
800        current: &CommandRecording,
801        pool: Option<&dyn VerifyExecutor>,
802    ) -> ReplayOutcome {
803        if current.tape.len() < MIN_REPLAY_COMMAND_RECORDS {
804            self.retire();
805            return ReplayOutcome::AllDynamic;
806        }
807        let Some(center) = detect_center(current) else {
808            self.retire();
809            return ReplayOutcome::AllDynamic;
810        };
811        match self.phase {
812            CommandReplayPhase::Idle => {
813                self.take_snapshot(current, center);
814                ReplayOutcome::AllDynamic
815            }
816            CommandReplayPhase::Snapshotted => self.partition(current, center),
817            CommandReplayPhase::Captured => self.verify(current, pool),
818        }
819    }
820
821    fn retire(&mut self) {
822        self.phase = CommandReplayPhase::Idle;
823        self.snapshot = CommandRecording::default();
824        self.segments.clear();
825    }
826
827    fn take_snapshot(&mut self, current: &CommandRecording, center: Point) {
828        self.snapshot = current.clone();
829        self.center = center;
830        self.segments.clear();
831        self.phase = CommandReplayPhase::Snapshotted;
832    }
833
834    /// Splits the recording into maximal chains of consecutive entries that
835    /// moved from the snapshot by one shared similarity transform, then
836    /// re-snapshots at the current values so verification always compares
837    /// against the capture frame. The returned spans carry the capture
838    /// content itself (`capture: true`, identity transform): the snapshot
839    /// IS this frame, so what the renderer retains equals what later
840    /// transforms move.
841    fn partition(&mut self, current: &CommandRecording, center: Point) -> ReplayOutcome {
842        let aligned = align_recordings(current, &self.snapshot);
843        let mut chains: Vec<(usize, usize)> = Vec::new();
844        let mut i = 0;
845        while i < current.tape.len() {
846            let (Some(view), Some(snapshot_view)) = (
847                view_at(current, i),
848                aligned[i].and_then(|j| view_at(&self.snapshot, j)),
849            ) else {
850                i += 1;
851                continue;
852            };
853            // A chain anchor must pin rotation itself.
854            let Some((t, true)) =
855                pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
856            else {
857                i += 1;
858                continue;
859            };
860            if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
861                == RecordMatch::Mismatch
862            {
863                i += 1;
864                continue;
865            }
866            let start = i;
867            let mut end = i + 1;
868            while end < current.tape.len() {
869                let (Some(view), Some(snapshot_view)) = (
870                    view_at(current, end),
871                    aligned[end].and_then(|j| view_at(&self.snapshot, j)),
872                ) else {
873                    break;
874                };
875                let Some((entry_t, pinned)) =
876                    pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
877                else {
878                    break;
879                };
880                if !transforms_group(entry_t, pinned, t) {
881                    break;
882                }
883                if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
884                    == RecordMatch::Mismatch
885                {
886                    break;
887                }
888                end += 1;
889            }
890            if end - start >= MIN_SEGMENT_RECORDS {
891                let mut piece_start = start;
892                while piece_start < end {
893                    let piece_end = (piece_start + MAX_SEGMENT_RECORDS).min(end);
894                    if piece_end - piece_start >= MIN_SEGMENT_RECORDS {
895                        chains.push((piece_start, piece_end));
896                    }
897                    piece_start = piece_end;
898                }
899            }
900            i = end.max(i + 1);
901        }
902
903        if chains.is_empty() {
904            self.take_snapshot(current, center);
905            return ReplayOutcome::AllDynamic;
906        }
907        // Re-snapshot at current values: chain ranges are current-tape
908        // ranges, which the fresh snapshot preserves verbatim.
909        self.take_snapshot(current, center);
910        self.segments = chains
911            .into_iter()
912            .map(|range| {
913                let slot = self.next_slot_id;
914                self.next_slot_id += 1;
915                CommandSegment {
916                    slot,
917                    slot_offset: 0,
918                    tape_start: range.0,
919                    tape_end: range.1,
920                    bounds: range_bounds(&self.snapshot, range),
921                }
922            })
923            .collect();
924        let covered: usize = self
925            .segments
926            .iter()
927            .map(|segment| segment.tape_end - segment.tape_start)
928            .sum();
929        self.capture_coverage = covered as f32 / current.tape.len().max(1) as f32;
930        self.frames_since_capture = 0;
931        self.phase = CommandReplayPhase::Captured;
932
933        let mut spans: Vec<ReplaySpan> = Vec::with_capacity(self.segments.len() * 2 + 1);
934        let mut cursor = 0usize;
935        for segment in &self.segments {
936            if segment.tape_start > cursor {
937                spans.push(ReplaySpan::Dynamic {
938                    tape_start: cursor,
939                    tape_end: segment.tape_start,
940                });
941            }
942            spans.push(ReplaySpan::Retained {
943                slot: segment.slot,
944                capture: true,
945                slot_offset: 0,
946                tape_start: segment.tape_start,
947                tape_end: segment.tape_end,
948                transform: RecordTransform::IDENTITY,
949                recolors: Vec::new(),
950                bounds: segment.bounds,
951            });
952            cursor = segment.tape_end;
953        }
954        if cursor < current.tape.len() {
955            spans.push(ReplaySpan::Dynamic {
956                tape_start: cursor,
957                tape_end: current.tape.len(),
958            });
959        }
960        ReplayOutcome::Spans(spans)
961    }
962
963    /// Verifies this frame's recording against the capture. Each segment
964    /// re-locates its anchor by searching forward from the cursor within
965    /// [`RESYNC_WINDOW`] — dynamic spans between segments change length
966    /// freely — probing a few entries under each candidate transform before
967    /// committing to a full-span verification (a wrong candidate from a
968    /// different ring fails the probe on its radii). A mismatch mid-span
969    /// splits the segment: the matched prefix stays retained, the record
970    /// that changed goes dynamic, and the suffix re-enters the location
971    /// queue as its own segment — churn costs the records it touched, not
972    /// the whole capture. Eroded coverage re-snapshots for the next frame.
973    fn verify(
974        &mut self,
975        current: &CommandRecording,
976        pool: Option<&dyn VerifyExecutor>,
977    ) -> ReplayOutcome {
978        let mut spans: Vec<ReplaySpan> = Vec::new();
979        let mut retained_records = 0usize;
980        // Current-tape position covered so far.
981        let mut cursor = 0usize;
982        // Leading segments the pooled pass already committed; the serial
983        // walk below runs only from this point on.
984        let mut committed = 0usize;
985        if let Some(pool) = pool {
986            if self.segments.len() >= 2 {
987                let commit = self.verify_optimistic(current, pool);
988                if commit.committed == self.segments.len() {
989                    self.optimistic_commits += 1;
990                    return self.finish_verify(current, commit.spans, commit.retained_records);
991                }
992                // Prefix-commit: the pooled spans for every segment before
993                // the first failure are equal by construction to what the
994                // serial walk would produce for them (see
995                // [`Self::verify_optimistic`]), so they are kept and the
996                // serial machinery below is seeded from the failure point
997                // instead of redoing the whole tape.
998                if commit.committed > 0 {
999                    self.prefix_commits += 1;
1000                }
1001                spans = commit.spans;
1002                retained_records = commit.retained_records;
1003                cursor = commit.cursor;
1004                committed = commit.committed;
1005            }
1006        }
1007        // Segments awaiting location this frame, tape order. A split pushes
1008        // the suffix back onto the front so it is located before the next
1009        // original segment. Both queues are persistent fields refilled per
1010        // frame, so their buffers keep their high-water capacity.
1011        self.verify_pending.clear();
1012        self.verify_pending.extend(self.segments.drain(committed..));
1013        self.verify_survivors.clear();
1014        // A committed segment matched whole, so it survives unchanged — in
1015        // emission order, ahead of whatever the serial walk keeps.
1016        self.verify_survivors.append(&mut self.segments);
1017        while let Some(segment) = self.verify_pending.pop_front() {
1018            let len = segment.tape_end - segment.tape_start;
1019            let search_end = (cursor + RESYNC_WINDOW)
1020                .min(current.tape.len().saturating_sub(len - 1))
1021                .max(cursor);
1022            // Candidates run LEFT TO RIGHT from the cursor, never by
1023            // proximity to an expected position: within a self-similar
1024            // ring, every pairing shifted right of the true anchor passes
1025            // probes (recolor-tolerant matching even repaints the color
1026            // pattern) with a sub-tolerance angle residual — the one
1027            // pairing a distance heuristic must never be allowed to reach
1028            // first. The true anchor is always the LEFTMOST compatible
1029            // candidate, exactly the order the flat detector proved out.
1030            let candidates = cursor..search_end;
1031            let mut located: Option<(usize, RecordTransform)> = None;
1032            // The longest cleanly matched prefix among failed commits:
1033            // (start, transform); its length and the recolors within it
1034            // live in `best_prefix_len` / `best_recolor_scratch`. A genuine
1035            // mid-span change surfaces here — the right anchor matches far
1036            // more than any mislocated one.
1037            let mut best_prefix: Option<(usize, RecordTransform)> = None;
1038            let mut best_prefix_len = 0usize;
1039            let mut attempts = 0usize;
1040            'search: for start in candidates {
1041                let Some(t) = probe_anchor(
1042                    current,
1043                    &self.snapshot,
1044                    self.center,
1045                    segment.tape_start,
1046                    len,
1047                    start,
1048                ) else {
1049                    continue;
1050                };
1051                // Committed: verify the whole span. A failure may still be a
1052                // mislocated anchor (self-similar rings), so the search
1053                // resumes — a bounded number of times.
1054                let matched = match_span(
1055                    TypedRecords::from(current),
1056                    TypedRecords::from(&self.snapshot),
1057                    self.center,
1058                    start,
1059                    segment.tape_start,
1060                    len,
1061                    t,
1062                    &mut self.recolor_scratch,
1063                );
1064                if matched < len {
1065                    if matched > best_prefix_len {
1066                        best_prefix_len = matched;
1067                        best_prefix = Some((start, t));
1068                        // Keep the best prefix's recolors without an
1069                        // allocation: the two scratches trade places.
1070                        std::mem::swap(&mut self.recolor_scratch, &mut self.best_recolor_scratch);
1071                    }
1072                    // Only failures with a substantial matched prefix
1073                    // consume the commit budget: those are genuine split
1074                    // candidates, and re-verifying long spans is the cost
1075                    // being bounded. A short-prefix failure is just a wrong
1076                    // anchor (a dead predecessor's entries, a cross-ring
1077                    // pairing) that the scan must be free to step past —
1078                    // charging those burned the budget before the true
1079                    // anchor and killed healthy segments.
1080                    if matched >= MIN_SEGMENT_RECORDS {
1081                        attempts += 1;
1082                        if attempts >= MAX_COMMIT_ATTEMPTS {
1083                            break 'search;
1084                        }
1085                    }
1086                    continue;
1087                }
1088                located = Some((start, t));
1089                break;
1090            }
1091            // A failed segment splits around the record that changed: the
1092            // matched prefix is retained now, the suffix re-enters the
1093            // queue to locate itself past whatever churn displaced it. Only
1094            // a prefix long enough to prove the anchor was right earns a
1095            // split — a segment with no solid prefix dies whole, or a weak
1096            // wrong-anchor prefix would shed one record and re-fail across
1097            // the whole span. The emitted span `mem::take`s its recolors
1098            // out of the owning scratch — the buffer walks into the graph
1099            // and the scratch re-grows on the next attempt (accepted:
1100            // emitting spans do real work).
1101            let (span_start, t, recolors, span_len) = match located {
1102                Some((start, t)) => (start, t, std::mem::take(&mut self.recolor_scratch), len),
1103                None => {
1104                    let split = best_prefix_len >= MIN_SEGMENT_RECORDS;
1105                    let Some((start, t)) = best_prefix.filter(|_| split) else {
1106                        self.lifetime_deaths += 1;
1107                        continue;
1108                    };
1109                    let suffix_start = segment.tape_start + best_prefix_len + 1;
1110                    if segment.tape_end > suffix_start
1111                        && segment.tape_end - suffix_start >= MIN_SEGMENT_RECORDS
1112                    {
1113                        // The suffix addresses the SAME captured content,
1114                        // just deeper in: no recapture, only an offset.
1115                        self.verify_pending.push_front(CommandSegment {
1116                            slot: segment.slot,
1117                            slot_offset: segment.slot_offset + (suffix_start - segment.tape_start),
1118                            tape_start: suffix_start,
1119                            tape_end: segment.tape_end,
1120                            bounds: range_bounds(&self.snapshot, (suffix_start, segment.tape_end)),
1121                        });
1122                    }
1123                    self.lifetime_splits += 1;
1124                    (
1125                        start,
1126                        t,
1127                        std::mem::take(&mut self.best_recolor_scratch),
1128                        best_prefix_len,
1129                    )
1130                }
1131            };
1132            let survivor = if span_len == len {
1133                segment
1134            } else {
1135                // The prefix keeps its capture identity — it addresses the
1136                // same slot content from the same offset, just shorter.
1137                CommandSegment {
1138                    slot: segment.slot,
1139                    slot_offset: segment.slot_offset,
1140                    tape_start: segment.tape_start,
1141                    tape_end: segment.tape_start + span_len,
1142                    bounds: range_bounds(
1143                        &self.snapshot,
1144                        (segment.tape_start, segment.tape_start + span_len),
1145                    ),
1146                }
1147            };
1148            if span_start > cursor {
1149                spans.push(ReplaySpan::Dynamic {
1150                    tape_start: cursor,
1151                    tape_end: span_start,
1152                });
1153            }
1154            retained_records += span_len;
1155            spans.push(ReplaySpan::Retained {
1156                slot: survivor.slot,
1157                capture: false,
1158                slot_offset: survivor.slot_offset,
1159                tape_start: span_start,
1160                tape_end: span_start + span_len,
1161                transform: t,
1162                recolors,
1163                bounds: t.apply_to_bounds(self.center, survivor.bounds),
1164            });
1165            cursor = span_start + span_len;
1166            self.verify_survivors.push(survivor);
1167        }
1168        if cursor < current.tape.len() {
1169            spans.push(ReplaySpan::Dynamic {
1170                tape_start: cursor,
1171                tape_end: current.tape.len(),
1172            });
1173        }
1174
1175        // Survivors become the live table; swapping (the table was drained
1176        // above) lets the two buffers ping-pong, both keeping capacity.
1177        std::mem::swap(&mut self.segments, &mut self.verify_survivors);
1178        self.finish_verify(current, spans, retained_records)
1179    }
1180
1181    /// The verification epilogue shared by the serial and pooled paths:
1182    /// coverage bookkeeping and the collapse/erosion re-snapshot decision.
1183    fn finish_verify(
1184        &mut self,
1185        current: &CommandRecording,
1186        spans: Vec<ReplaySpan>,
1187        retained_records: usize,
1188    ) -> ReplayOutcome {
1189        self.frames_since_capture += 1;
1190        let retained_total: usize = self
1191            .segments
1192            .iter()
1193            .map(|segment| segment.tape_end - segment.tape_start)
1194            .sum();
1195        let coverage = retained_total as f32 / current.tape.len().max(1) as f32;
1196        let collapsed = retained_records == 0 || coverage < MIN_COVERAGE_FRACTION;
1197        let eroded = coverage + RECAPTURE_EROSION < self.capture_coverage
1198            && self.frames_since_capture >= RECAPTURE_COOLDOWN_FRAMES;
1199        if collapsed || eroded {
1200            // Re-snapshot so the next two frames re-partition. Collapse pays
1201            // immediately; mere erosion waits out the capture cooldown.
1202            let center = self.center;
1203            self.take_snapshot(current, center);
1204            if retained_records == 0 {
1205                return ReplayOutcome::AllDynamic;
1206            }
1207        }
1208        ReplayOutcome::Spans(spans)
1209    }
1210
1211    /// The pooled fast path: locates segments serially with cheap probes
1212    /// only (identical candidate order to the serial walk — leftmost from
1213    /// the cursor), then fans the expensive full-span matching across
1214    /// `pool` and commits the longest prefix of segments whose bodies
1215    /// matched whole.
1216    ///
1217    /// Each committed span is EQUAL BY CONSTRUCTION to the serial walk's:
1218    /// the serial walk commits a segment at the first candidate that both
1219    /// passes [`probe_anchor`] and matches its whole body under
1220    /// [`match_span`]; for a committed segment here, the first
1221    /// probe-passing candidate matched whole, no earlier candidate even
1222    /// probe-passes, and both functions are deterministic over the same
1223    /// inputs — so anchor, transform, tape range, recolors and bounds all
1224    /// coincide, as does the cursor both walks carry forward
1225    /// (`start + len`, by induction from a shared start of zero). The
1226    /// commit therefore ends at the first failure — a segment with no
1227    /// probe-passing candidate in its window, or a body that matched short
1228    /// (a genuine change, or a mislocated anchor on a self-similar ring):
1229    /// from that segment on, only the serial walk's candidate-scan budget
1230    /// and split/death machinery can decide the frame, starting from the
1231    /// identical cursor. Uncommitted result slots are left untouched so
1232    /// their recolor buffers stay warm.
1233    fn verify_optimistic(
1234        &mut self,
1235        current: &CommandRecording,
1236        pool: &dyn VerifyExecutor,
1237    ) -> PooledCommit {
1238        struct SpanJob {
1239            start: usize,
1240            seg_start: usize,
1241            len: usize,
1242            t: RecordTransform,
1243        }
1244        let mut jobs: Vec<SpanJob> = Vec::with_capacity(self.segments.len());
1245        let mut cursor = 0usize;
1246        for segment in &self.segments {
1247            let len = segment.tape_end - segment.tape_start;
1248            let search_end = (cursor + RESYNC_WINDOW)
1249                .min(current.tape.len().saturating_sub(len - 1))
1250                .max(cursor);
1251            let mut found = None;
1252            for start in cursor..search_end {
1253                if let Some(t) = probe_anchor(
1254                    current,
1255                    &self.snapshot,
1256                    self.center,
1257                    segment.tape_start,
1258                    len,
1259                    start,
1260                ) {
1261                    found = Some((start, t));
1262                    break;
1263                }
1264            }
1265            // No probe-passing candidate: the serial walk would scan these
1266            // same candidates, find none, and kill the segment — machinery
1267            // this pass does not carry. Job collection stops here; the
1268            // jobs already collected are still worth their pooled bodies.
1269            let Some((start, t)) = found else {
1270                break;
1271            };
1272            jobs.push(SpanJob {
1273                start,
1274                seg_start: segment.tape_start,
1275                len,
1276                t,
1277            });
1278            cursor = start + len;
1279        }
1280        if jobs.is_empty() {
1281            return PooledCommit {
1282                spans: Vec::new(),
1283                retained_records: 0,
1284                committed: 0,
1285                cursor: 0,
1286            };
1287        }
1288        // One reusable result slot per job, grown once and kept across
1289        // frames; every job writes only its own slot, filling the slot's
1290        // own recolor buffer in place via the out-param.
1291        if self.verify_results.len() < jobs.len() {
1292            self.verify_results
1293                .resize_with(jobs.len(), Default::default);
1294        }
1295        {
1296            let current = TypedRecords::from(current);
1297            let snapshot = TypedRecords::from(&self.snapshot);
1298            let center = self.center;
1299            let jobs = &jobs;
1300            let results = &self.verify_results;
1301            pool.for_each(jobs.len(), &|i| {
1302                let job = &jobs[i];
1303                let mut guard = results[i].lock().expect("verify span job lock");
1304                let slot = &mut *guard;
1305                slot.matched = match_span(
1306                    current,
1307                    snapshot,
1308                    center,
1309                    job.start,
1310                    job.seg_start,
1311                    job.len,
1312                    job.t,
1313                    &mut slot.recolors,
1314                );
1315            });
1316        }
1317        // The commit ends at the first body that matched short. Slots from
1318        // that job on are not taken, so their recolor buffers stay warm
1319        // for the serial rerun and later frames.
1320        let mut committed = jobs.len();
1321        for (i, (job, result)) in jobs.iter().zip(&self.verify_results).enumerate() {
1322            if result.lock().expect("verify span job lock").matched < job.len {
1323                committed = i;
1324                break;
1325            }
1326        }
1327        let mut spans: Vec<ReplaySpan> = Vec::with_capacity(committed * 2 + 1);
1328        let mut retained_records = 0usize;
1329        let mut cursor = 0usize;
1330        for (segment, (job, result)) in self
1331            .segments
1332            .iter()
1333            .zip(jobs.iter().zip(&self.verify_results))
1334            .take(committed)
1335        {
1336            // Committing: each emitted span takes its slot's buffer — the
1337            // capacity walks into the graph and the slot re-grows next
1338            // frame (accepted: emitting spans do real work).
1339            let recolors =
1340                std::mem::take(&mut result.lock().expect("verify span job lock").recolors);
1341            if job.start > cursor {
1342                spans.push(ReplaySpan::Dynamic {
1343                    tape_start: cursor,
1344                    tape_end: job.start,
1345                });
1346            }
1347            retained_records += job.len;
1348            spans.push(ReplaySpan::Retained {
1349                slot: segment.slot,
1350                capture: false,
1351                slot_offset: segment.slot_offset,
1352                tape_start: job.start,
1353                tape_end: job.start + job.len,
1354                transform: job.t,
1355                recolors,
1356                bounds: job.t.apply_to_bounds(self.center, segment.bounds),
1357            });
1358            cursor = job.start + job.len;
1359        }
1360        // The trailing dynamic span belongs to whichever path covers the
1361        // tape's tail: this one only when every segment committed.
1362        if committed == self.segments.len() && cursor < current.tape.len() {
1363            spans.push(ReplaySpan::Dynamic {
1364                tape_start: cursor,
1365                tape_end: current.tape.len(),
1366            });
1367        }
1368        PooledCommit {
1369            spans,
1370            retained_records,
1371            committed,
1372            cursor,
1373        }
1374    }
1375}
1376
1377/// What one pooled pass committed: the emitted spans and survivor count of
1378/// the leading segments whose bodies matched whole, plus the current-tape
1379/// cursor after the last committed span — exactly the state the serial
1380/// walk needs to take over from the first failure. `committed` equal to
1381/// the segment count is a fully pooled frame; zero means the pass salvaged
1382/// nothing and the serial walk redoes the frame from the top.
1383struct PooledCommit {
1384    spans: Vec<ReplaySpan>,
1385    retained_records: usize,
1386    /// Leading segments committed exactly as the serial walk would have.
1387    committed: usize,
1388    /// Current-tape position after the last committed span.
1389    cursor: usize,
1390}
1391
1392/// The cheap anchor test shared by the serial walk and the pooled fast
1393/// path: view compatibility, transform derivation from the anchor pair, and
1394/// [`ANCHOR_PROBE_RECORDS`] probe matches. `None` means this candidate
1395/// cannot be the segment's anchor.
1396fn probe_anchor(
1397    current: &CommandRecording,
1398    snapshot: &CommandRecording,
1399    center: Point,
1400    seg_start: usize,
1401    len: usize,
1402    start: usize,
1403) -> Option<RecordTransform> {
1404    let (Some(view), Some(snapshot_view)) = (view_at(current, start), view_at(snapshot, seg_start))
1405    else {
1406        return None;
1407    };
1408    if !views_compatible(current, Some(view), snapshot, Some(snapshot_view)) {
1409        return None;
1410    }
1411    let (t, _) = pair_transform(current, view, snapshot, snapshot_view, center)?;
1412    for probe in 0..ANCHOR_PROBE_RECORDS.min(len) {
1413        let (Some(view), Some(snapshot_view)) = (
1414            view_at(current, start + probe),
1415            view_at(snapshot, seg_start + probe),
1416        ) else {
1417            return None;
1418        };
1419        if match_pair(current, view, snapshot, snapshot_view, center, t) == RecordMatch::Mismatch {
1420            return None;
1421        }
1422    }
1423    Some(t)
1424}
1425
1426/// The typed-record arrays a span match reads — the POD slice view of a
1427/// [`CommandRecording`] that is `Sync` (the recording itself is not: its
1428/// `others` vector may hold `Rc`-carrying primitives), which is what lets
1429/// [`match_span`] calls cross worker threads.
1430#[derive(Clone, Copy)]
1431struct TypedRecords<'a> {
1432    tape: &'a [TapeRef],
1433    arcs: &'a [SolidArcRecord],
1434    round_rects: &'a [SolidRoundRectRecord],
1435}
1436
1437impl<'a> From<&'a CommandRecording> for TypedRecords<'a> {
1438    fn from(recording: &'a CommandRecording) -> Self {
1439        Self {
1440            tape: &recording.tape,
1441            arcs: &recording.arcs,
1442            round_rects: &recording.round_rects,
1443        }
1444    }
1445}
1446
1447impl TypedRecords<'_> {
1448    /// [`view_at`] over the POD slices, for worker-thread span matching —
1449    /// the same [`view_at_slices`] implementation, so eligibility cannot
1450    /// drift between the two forms.
1451    fn view_at(&self, i: usize) -> Option<ReplayView> {
1452        view_at_slices(self.tape, self.round_rects, i)
1453    }
1454}
1455
1456/// The full-span commit body: matches `len` records of `current` from
1457/// `start` against the snapshot span at `seg_start` under `t`. Fills
1458/// `recolors` (cleared at entry) with the recolors inside the cleanly
1459/// matched prefix and returns that prefix's length — the out-param lets
1460/// callers own reusable buffers instead of allocating per call. The
1461/// record dispatch mirrors [`match_pair`] exactly; it operates on the typed
1462/// slices so one call per segment can run on a worker thread.
1463#[allow(clippy::too_many_arguments)]
1464fn match_span(
1465    current: TypedRecords<'_>,
1466    snapshot: TypedRecords<'_>,
1467    center: Point,
1468    start: usize,
1469    seg_start: usize,
1470    len: usize,
1471    t: RecordTransform,
1472    recolors: &mut Vec<(u32, Color)>,
1473) -> usize {
1474    recolors.clear();
1475    for offset in 0..len {
1476        let entry_match = match (
1477            current.view_at(start + offset),
1478            snapshot.view_at(seg_start + offset),
1479        ) {
1480            (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
1481                match_arc(&current.arcs[i], &snapshot.arcs[j], center, t)
1482            }
1483            (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
1484                match_round_rect(&current.round_rects[i], &snapshot.round_rects[j], center, t)
1485            }
1486            _ => RecordMatch::Mismatch,
1487        };
1488        match entry_match {
1489            RecordMatch::Exact => {}
1490            RecordMatch::Recolor => {
1491                let color = match current.view_at(start + offset) {
1492                    Some(ReplayView::Arc(a)) => current.arcs[a].color,
1493                    Some(ReplayView::RoundRect(r)) => current.round_rects[r].color,
1494                    None => unreachable!("recolor requires a view"),
1495                };
1496                recolors.push((offset as u32, color));
1497            }
1498            RecordMatch::Mismatch => return offset,
1499        }
1500    }
1501    len
1502}
1503
1504#[cfg(test)]
1505mod tests {
1506    use super::*;
1507    use crate::{Color, Stroke};
1508
1509    const CENTER: Point = Point { x: 204.0, y: 204.0 };
1510
1511    fn arc(radius: f32, start: f32, color: Color) -> SolidArcRecord {
1512        SolidArcRecord {
1513            center: CENTER,
1514            radius,
1515            start_angle: start,
1516            sweep_angle: 0.4,
1517            inner_radius: radius * 0.8,
1518            color,
1519            stroke: None,
1520        }
1521    }
1522
1523    fn moved_arc(base: &SolidArcRecord, t: RecordTransform) -> SolidArcRecord {
1524        SolidArcRecord {
1525            center: base.center,
1526            radius: base.radius * t.scale,
1527            start_angle: base.start_angle + t.angle,
1528            sweep_angle: base.sweep_angle,
1529            inner_radius: base.inner_radius * t.scale,
1530            color: base.color,
1531            stroke: base.stroke.map(|stroke| Stroke {
1532                width: stroke.width * t.scale,
1533                ..stroke
1534            }),
1535        }
1536    }
1537
1538    fn circle(cx: f32, cy: f32, diameter: f32, color: Color) -> SolidRoundRectRecord {
1539        SolidRoundRectRecord {
1540            rect: Rect {
1541                x: cx - diameter * 0.5,
1542                y: cy - diameter * 0.5,
1543                width: diameter,
1544                height: diameter,
1545            },
1546            radii: CornerRadii::uniform(diameter * 0.5),
1547            color,
1548            stroke: None,
1549        }
1550    }
1551
1552    #[test]
1553    fn arc_anchor_recovers_the_baked_transform() {
1554        let t = RecordTransform {
1555            scale: 0.9994,
1556            angle: 0.0123,
1557        };
1558        let retained = arc(120.0, 1.0, Color::WHITE);
1559        let current = moved_arc(&retained, t);
1560        let derived = arc_anchor_transform(&current, &retained).expect("derivable");
1561        assert!((derived.scale - t.scale).abs() < 1e-6);
1562        assert!((derived.angle - t.angle).abs() < 1e-6);
1563        assert_eq!(
1564            match_arc(&current, &retained, CENTER, derived),
1565            RecordMatch::Exact
1566        );
1567    }
1568
1569    #[test]
1570    fn recolored_arc_matches_as_recolor() {
1571        let t = RecordTransform {
1572            scale: 1.0,
1573            angle: 0.05,
1574        };
1575        let retained = arc(80.0, 0.2, Color::WHITE);
1576        let mut current = moved_arc(&retained, t);
1577        current.color = Color::rgb(0.5, 0.1, 0.9);
1578        assert_eq!(
1579            match_arc(&current, &retained, CENTER, t),
1580            RecordMatch::Recolor
1581        );
1582    }
1583
1584    #[test]
1585    fn changed_sweep_is_a_mismatch() {
1586        let t = RecordTransform::IDENTITY;
1587        let retained = arc(80.0, 0.2, Color::WHITE);
1588        let mut current = retained;
1589        current.sweep_angle += 0.1;
1590        assert_eq!(
1591            match_arc(&current, &retained, CENTER, t),
1592            RecordMatch::Mismatch
1593        );
1594    }
1595
1596    #[test]
1597    fn stroked_arc_scales_its_width_with_the_segment() {
1598        let t = RecordTransform {
1599            scale: 0.98,
1600            angle: 0.0,
1601        };
1602        let mut retained = arc(60.0, 0.0, Color::WHITE);
1603        retained.stroke = Some(Stroke::new(5.0));
1604        let current = moved_arc(&retained, t);
1605        assert_eq!(
1606            match_arc(&current, &retained, CENTER, t),
1607            RecordMatch::Exact
1608        );
1609
1610        // An unscaled stroke under a scaling segment is a real change: 2%
1611        // of 5px is well past the noise tolerance.
1612        let mut stale = current;
1613        stale.stroke = Some(Stroke::new(5.0));
1614        assert_eq!(
1615            match_arc(&stale, &retained, CENTER, t),
1616            RecordMatch::Mismatch
1617        );
1618    }
1619
1620    #[test]
1621    fn orbiting_circle_matches_under_rotation() {
1622        let t = RecordTransform {
1623            scale: 1.0,
1624            angle: 0.3,
1625        };
1626        let retained = circle(304.0, 204.0, 10.0, Color::WHITE);
1627        let (c_then, d_then) = circle_view(&retained).expect("circle");
1628        let c_now = t.apply(CENTER, c_then);
1629        let current = circle(c_now.x, c_now.y, d_then * t.scale, Color::WHITE);
1630        let (derived, pinned) = circle_anchor_transform_pinned(
1631            circle_view(&current).unwrap(),
1632            (c_then, d_then),
1633            CENTER,
1634        )
1635        .expect("derivable");
1636        assert!(pinned, "an off-pivot circle pins rotation");
1637        assert!((derived.angle - t.angle).abs() < 1e-4);
1638        assert_eq!(
1639            match_round_rect(&current, &retained, CENTER, derived),
1640            RecordMatch::Exact
1641        );
1642    }
1643
1644    #[test]
1645    fn non_circular_round_rect_never_matches() {
1646        let mut retained = circle(304.0, 204.0, 10.0, Color::WHITE);
1647        retained.rect.width = 14.0; // no longer a circle
1648        assert_eq!(
1649            match_round_rect(&retained, &retained, CENTER, RecordTransform::IDENTITY),
1650            RecordMatch::Mismatch
1651        );
1652    }
1653
1654    #[test]
1655    fn grouping_is_tighter_than_verification() {
1656        let anchor = RecordTransform {
1657            scale: 1.0,
1658            angle: 0.010,
1659        };
1660        let same_ring = RecordTransform {
1661            scale: 1.0,
1662            angle: 0.0100001,
1663        };
1664        let next_ring = RecordTransform {
1665            scale: 1.0,
1666            angle: 0.011,
1667        };
1668        assert!(transforms_group(same_ring, true, anchor));
1669        assert!(
1670            !transforms_group(next_ring, true, anchor),
1671            "a 1e-3 rotation-step difference is another ring, not float noise"
1672        );
1673        let unpinned = RecordTransform {
1674            scale: 1.0,
1675            angle: 0.0,
1676        };
1677        assert!(transforms_group(unpinned, false, anchor));
1678    }
1679
1680    use crate::geometry::{DrawScopeDefault, Size};
1681    use crate::{Brush, DrawScope as _};
1682
1683    /// Records one MEGA-shaped frame: `rings` rings of `per_ring` arcs, each
1684    /// ring rotated by its own step × `frame`, breathing scale applied to
1685    /// every radius, plus `tail` dynamic circles whose count varies.
1686    fn ring_frame(rings: usize, per_ring: usize, frame: usize, tail: usize) -> CommandRecording {
1687        let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
1688        let scale = 0.9994f32.powi(frame as i32);
1689        for ring in 0..rings {
1690            let step = 0.01 + ring as f32 * 0.005;
1691            let rotation = step * frame as f32;
1692            let radius = (60.0 + ring as f32 * 30.0) * scale;
1693            for slot in 0..per_ring {
1694                let start = slot as f32 * (std::f32::consts::TAU / per_ring as f32) + rotation;
1695                scope.draw_annular_sector(
1696                    Brush::solid(Color::WHITE),
1697                    CENTER,
1698                    radius * 0.8,
1699                    radius,
1700                    start,
1701                    0.02,
1702                );
1703            }
1704        }
1705        for i in 0..tail {
1706            // Dynamic entities: different positions every frame.
1707            let x = 40.0 + (frame * 17 + i * 31) as f32 % 300.0;
1708            scope.draw_circle(Brush::solid(Color::RED), Point::new(x, 50.0), 3.0);
1709        }
1710        scope.recorded().clone()
1711    }
1712
1713    #[test]
1714    fn ring_scene_reaches_retention_by_the_third_frame() {
1715        let mut state = CommandReplayState::default();
1716        assert!(matches!(
1717            state.advance(&ring_frame(3, 300, 0, 10)),
1718            ReplayOutcome::AllDynamic
1719        ));
1720        // The partition frame itself emits the capture: snapshot == current,
1721        // so every span is capture:true under an identity transform.
1722        let ReplayOutcome::Spans(capture_spans) = state.advance(&ring_frame(3, 300, 1, 10)) else {
1723            panic!("partition frame should emit the capture");
1724        };
1725        assert!(capture_spans.iter().all(|span| match span {
1726            ReplaySpan::Retained {
1727                capture, transform, ..
1728            } => *capture && *transform == RecordTransform::IDENTITY,
1729            ReplaySpan::Dynamic { .. } => true,
1730        }));
1731        assert!(!state.segments().is_empty(), "partition found the rings");
1732
1733        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(3, 300, 2, 10)) else {
1734            panic!("third frame should retain");
1735        };
1736        let retained: usize = spans
1737            .iter()
1738            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
1739            .count();
1740        assert!(retained >= 3, "each ring retains, got {spans:?}");
1741        // The tail circles are dynamic.
1742        assert!(spans
1743            .iter()
1744            .any(|span| matches!(span, ReplaySpan::Dynamic { .. })));
1745        // Retained spans carry the per-ring rotations, not a shared one.
1746        let transforms: Vec<RecordTransform> = spans
1747            .iter()
1748            .filter_map(|span| match span {
1749                ReplaySpan::Retained { transform, .. } => Some(*transform),
1750                _ => None,
1751            })
1752            .collect();
1753        assert!(transforms.windows(2).any(|w| w[0].angle != w[1].angle));
1754    }
1755
1756    #[test]
1757    fn entity_churn_between_frames_still_retains_rings() {
1758        let mut state = CommandReplayState::default();
1759        state.advance(&ring_frame(2, 400, 0, 8));
1760        state.advance(&ring_frame(2, 400, 1, 13)); // tail length changed
1761        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(2, 400, 2, 5)) else {
1762            panic!("churned tail must not break ring retention");
1763        };
1764        let retained_records: usize = spans
1765            .iter()
1766            .filter_map(|span| match span {
1767                ReplaySpan::Retained { .. } => Some(1),
1768                _ => None,
1769            })
1770            .sum();
1771        assert!(retained_records >= 2);
1772    }
1773
1774    #[test]
1775    fn recolors_are_patches_not_mismatches() {
1776        let recolored_frame = |frame: usize| {
1777            let mut recording = ring_frame(1, 600, frame, 0);
1778            // Twinkle: 40 dots change color every frame, geometry untouched.
1779            for i in (0..recording.arcs.len()).step_by(15) {
1780                recording.arcs[i].color = if frame.is_multiple_of(2) {
1781                    Color::rgb(1.0, 0.5, 0.1)
1782                } else {
1783                    Color::rgb(0.1, 0.5, 1.0)
1784                };
1785            }
1786            recording
1787        };
1788        let mut state = CommandReplayState::default();
1789        state.advance(&recolored_frame(0));
1790        state.advance(&recolored_frame(1));
1791        let ReplayOutcome::Spans(spans) = state.advance(&recolored_frame(2)) else {
1792            panic!("twinkles must not break retention");
1793        };
1794        let recolor_count: usize = spans
1795            .iter()
1796            .filter_map(|span| match span {
1797                ReplaySpan::Retained { recolors, .. } => Some(recolors.len()),
1798                _ => None,
1799            })
1800            .sum();
1801        assert!(recolor_count >= 30, "twinkles surface as patches");
1802    }
1803
1804    #[test]
1805    fn geometry_change_kills_only_its_segment() {
1806        let mut state = CommandReplayState::default();
1807        state.advance(&ring_frame(3, 300, 0, 0));
1808        state.advance(&ring_frame(3, 300, 1, 0));
1809        let mut broken = ring_frame(3, 300, 2, 0);
1810        // A brick hit: one entry in the middle ring changes sweep.
1811        broken.arcs[450].sweep_angle *= 3.0;
1812        let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
1813            panic!("one changed entry must not drop the whole command");
1814        };
1815        let retained: usize = spans
1816            .iter()
1817            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
1818            .count();
1819        assert!(
1820            retained >= 2,
1821            "the untouched rings keep retaining, got {spans:?}"
1822        );
1823    }
1824
1825    #[test]
1826    fn mid_segment_change_splits_and_retains_both_halves() {
1827        let mut state = CommandReplayState::default();
1828        state.advance(&ring_frame(1, 900, 0, 0));
1829        state.advance(&ring_frame(1, 900, 1, 0));
1830        assert_eq!(state.segments().len(), 1, "one ring is one segment");
1831        let mut broken = ring_frame(1, 900, 2, 0);
1832        broken.arcs[450].sweep_angle *= 3.0;
1833        let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
1834            panic!("a single changed record must not drop retention");
1835        };
1836        let dynamic: usize = spans
1837            .iter()
1838            .filter_map(|span| match span {
1839                ReplaySpan::Dynamic {
1840                    tape_start,
1841                    tape_end,
1842                } => Some(tape_end - tape_start),
1843                _ => None,
1844            })
1845            .sum();
1846        let retained: Vec<(u32, usize, bool)> = spans
1847            .iter()
1848            .filter_map(|span| match span {
1849                ReplaySpan::Retained {
1850                    slot,
1851                    slot_offset,
1852                    capture,
1853                    ..
1854                } => Some((*slot, *slot_offset, *capture)),
1855                _ => None,
1856            })
1857            .collect();
1858        assert_eq!(
1859            retained.len(),
1860            2,
1861            "prefix and suffix both retain: {spans:?}"
1862        );
1863        // Both pieces address the SAME captured slot — a split never
1864        // recaptures, it re-addresses: the suffix starts one record past
1865        // the prefix within the capture.
1866        assert_eq!(retained[0].0, retained[1].0);
1867        assert_eq!(retained[0].1, 0);
1868        assert_eq!(retained[1].1, 451);
1869        assert!(retained.iter().all(|(_, _, capture)| !capture));
1870        assert_eq!(dynamic, 1, "only the changed record goes dynamic");
1871        assert_eq!(state.stats(), (0, 1), "one split, no deaths");
1872
1873        // The pieces keep retaining on later frames, the changed record's
1874        // slot staying dynamic between them.
1875        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(1, 900, 3, 0)) else {
1876            panic!("split pieces must keep retaining");
1877        };
1878        let retained = spans
1879            .iter()
1880            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
1881            .count();
1882        assert_eq!(retained, 2, "both pieces relocate next frame: {spans:?}");
1883    }
1884
1885    #[test]
1886    fn erosion_recaptures_dead_ranges_after_the_cooldown() {
1887        let mut state = CommandReplayState::default();
1888        state.advance(&ring_frame(3, 300, 0, 0));
1889        state.advance(&ring_frame(3, 300, 1, 0));
1890        // The middle ring changes shape permanently: its segment dies, and
1891        // only a recapture can watch the new shape.
1892        let mutated = |frame: usize| {
1893            let mut recording = ring_frame(3, 300, frame, 0);
1894            for arc in &mut recording.arcs[300..600] {
1895                arc.sweep_angle *= 3.0;
1896            }
1897            recording
1898        };
1899        let dynamic_records = |outcome: &ReplayOutcome| -> usize {
1900            match outcome {
1901                ReplayOutcome::AllDynamic => usize::MAX,
1902                ReplayOutcome::Spans(spans) => spans
1903                    .iter()
1904                    .filter_map(|span| match span {
1905                        ReplaySpan::Dynamic {
1906                            tape_start,
1907                            tape_end,
1908                        } => Some(tape_end - tape_start),
1909                        _ => None,
1910                    })
1911                    .sum(),
1912            }
1913        };
1914        let after_death = state.advance(&mutated(2));
1915        let lost = dynamic_records(&after_death);
1916        assert!(
1917            (250..=400).contains(&lost),
1918            "the changed ring goes dynamic, got {lost}"
1919        );
1920        for frame in 3..(3 + RECAPTURE_COOLDOWN_FRAMES as usize + 4) {
1921            state.advance(&mutated(frame));
1922        }
1923        let recovered = state.advance(&mutated(200));
1924        let residue = dynamic_records(&recovered);
1925        assert!(
1926            residue < 50,
1927            "the recapture watches the ring's new shape, got {residue} dynamic"
1928        );
1929    }
1930
1931    #[test]
1932    fn small_commands_are_not_watched() {
1933        let mut state = CommandReplayState::default();
1934        for frame in 0..4 {
1935            assert!(matches!(
1936                state.advance(&ring_frame(1, 40, frame, 0)),
1937                ReplayOutcome::AllDynamic
1938            ));
1939        }
1940        assert!(state.segments().is_empty());
1941    }
1942
1943    /// A real multi-threaded executor for the equivalence test: lane 0 is
1944    /// the caller, the rest are scoped threads, jobs stride across lanes —
1945    /// the same distribution the renderer's frame pool uses.
1946    struct ThreadedExec {
1947        lanes: usize,
1948    }
1949
1950    impl VerifyExecutor for ThreadedExec {
1951        fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync)) {
1952            std::thread::scope(|s| {
1953                for lane in 1..self.lanes {
1954                    s.spawn(move || {
1955                        let mut i = lane;
1956                        while i < jobs {
1957                            run(i);
1958                            i += self.lanes;
1959                        }
1960                    });
1961                }
1962                let mut i = 0;
1963                while i < jobs {
1964                    run(i);
1965                    i += self.lanes;
1966                }
1967            });
1968        }
1969    }
1970
1971    #[test]
1972    fn pooled_verification_matches_serial_exactly() {
1973        let exec = ThreadedExec { lanes: 3 };
1974        // Every verification path in one long churning sequence: multi-ring
1975        // retention under rotation, tail churn, twinkle recolors, brick-hit
1976        // single-record changes (the pooled pass commits the segments
1977        // before the failure and hands the serial machinery the failure
1978        // point), a multi-segment change, whole-ring deaths behind a
1979        // committed prefix, and coverage collapses that force re-snapshots
1980        // and fresh partitions mid-sequence.
1981        let frame = |f: usize| -> CommandRecording {
1982            let tail = [10usize, 13, 5, 8, 11, 6, 9, 12][f % 8];
1983            let mut recording = ring_frame(3, 300, f, tail);
1984            if f >= 3 {
1985                for i in (0..recording.arcs.len()).step_by(17) {
1986                    recording.arcs[i].color = if f.is_multiple_of(2) {
1987                        Color::rgb(1.0, 0.5, 0.1)
1988                    } else {
1989                        Color::rgb(0.1, 0.5, 1.0)
1990                    };
1991                }
1992            }
1993            match f {
1994                5 => {
1995                    // A brick hit: one record inside the middle ring. The
1996                    // pooled pass fails there, commits the ring before it,
1997                    // and the serial machinery splits from the failure.
1998                    recording.arcs[450].sweep_angle = 0.15;
1999                }
2000                8 => {
2001                    // Changes in the first and last rings at once: the
2002                    // first segment fails, so the pooled prefix is empty
2003                    // and the serial walk decides the whole frame.
2004                    recording.arcs[100].sweep_angle = 0.15;
2005                    recording.arcs[750].sweep_angle = 0.15;
2006                }
2007                12 => {
2008                    // A hit inside a segment created by the frame-5 split.
2009                    recording.arcs[500].sweep_angle = 0.15;
2010                }
2011                16..=39 => {
2012                    // The last ring changes shape wholesale and stays
2013                    // changed: its segment dies (no candidate even
2014                    // probes) behind the still-committing leading rings,
2015                    // and whatever coverage bookkeeping decides — death or
2016                    // collapse into a re-snapshot — both paths must agree.
2017                    for arc in &mut recording.arcs[600..900] {
2018                        arc.sweep_angle = 0.06;
2019                    }
2020                }
2021                40..=45 => {
2022                    // Nearly everything changes: coverage collapses below
2023                    // the floor, the state re-snapshots and re-partitions
2024                    // mid-sequence, then retains the changed shape.
2025                    for arc in &mut recording.arcs[150..900] {
2026                        arc.sweep_angle = 0.08;
2027                    }
2028                }
2029                52 => {
2030                    // A brick hit against the post-collapse capture.
2031                    recording.arcs[450].sweep_angle = 0.15;
2032                }
2033                _ => {}
2034            }
2035            recording
2036        };
2037        let mut serial = CommandReplayState::default();
2038        let mut pooled = CommandReplayState::default();
2039        for f in 0..60 {
2040            let recording = frame(f);
2041            let serial_outcome = serial.advance(&recording);
2042            let pooled_outcome = pooled.advance_pooled(&recording, Some(&exec));
2043            assert_eq!(
2044                serial_outcome, pooled_outcome,
2045                "outcome diverged at frame {f}"
2046            );
2047            assert_eq!(
2048                serial.segments(),
2049                pooled.segments(),
2050                "segments diverged at frame {f}"
2051            );
2052            assert_eq!(
2053                serial.stats(),
2054                pooled.stats(),
2055                "stats diverged at frame {f}"
2056            );
2057        }
2058        let (deaths, splits) = serial.stats();
2059        assert!(
2060            !serial.segments().is_empty() && deaths > 0 && splits > 0,
2061            "sequence must exercise retention, deaths, and splits, \
2062             got {deaths} deaths {splits} splits {} segments",
2063            serial.segments().len()
2064        );
2065        assert_eq!(serial.optimistic_commits(), 0);
2066        assert_eq!(serial.prefix_commits(), 0);
2067        assert!(
2068            pooled.optimistic_commits() >= 10,
2069            "the pooled fast path must actually commit steady frames, got {}",
2070            pooled.optimistic_commits()
2071        );
2072        assert!(
2073            pooled.prefix_commits() >= 3,
2074            "churn frames must commit their pooled prefix, got {}",
2075            pooled.prefix_commits()
2076        );
2077    }
2078
2079    #[test]
2080    fn transformed_bounds_contain_the_moved_content() {
2081        let t = RecordTransform {
2082            scale: 1.1,
2083            angle: 0.5,
2084        };
2085        let bounds = Rect {
2086            x: 150.0,
2087            y: 150.0,
2088            width: 100.0,
2089            height: 30.0,
2090        };
2091        let moved = t.apply_to_bounds(CENTER, bounds);
2092        for corner in [
2093            Point::new(bounds.x, bounds.y),
2094            Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
2095        ] {
2096            let p = t.apply(CENTER, corner);
2097            assert!(p.x >= moved.x - 1e-3 && p.x <= moved.x + moved.width + 1e-3);
2098            assert!(p.y >= moved.y - 1e-3 && p.y <= moved.y + moved.height + 1e-3);
2099        }
2100    }
2101}