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    /// Reusable per-job result slots for the pooled fast path — one slot
699    /// per segment, grown once, recolor capacity retained across frames.
700    /// The Mutex is uncontended (each job writes only its own slot once);
701    /// what this kills is the per-frame allocation of the results vector,
702    /// its mutexes, and every job's recolors vector. When the pass commits,
703    /// each emitted span `mem::take`s its slot's recolors — the buffer
704    /// walks into the graph and the slot re-grows next frame (accepted:
705    /// emitting spans do real work); on a bail the buffers stay warm in
706    /// their slots.
707    verify_results: Vec<std::sync::Mutex<SpanResultSlot>>,
708    /// The serial walk's recolor buffer, refilled by every `match_span`
709    /// commit attempt. An emitted span `mem::take`s the contents and the
710    /// scratch re-grows on the next attempt — same accepted emit-cost as
711    /// the pooled slots.
712    recolor_scratch: Vec<(u32, Color)>,
713    /// The best-prefix recolors during the serial walk's candidate scan,
714    /// swapped with `recolor_scratch` whenever a longer prefix turns up.
715    best_recolor_scratch: Vec<(u32, Color)>,
716    /// Serial-walk segment queues, persistent so their buffers keep their
717    /// high-water capacity; refilled per verified frame.
718    verify_pending: std::collections::VecDeque<CommandSegment>,
719    verify_survivors: Vec<CommandSegment>,
720}
721
722impl Default for CommandReplayState {
723    fn default() -> Self {
724        Self {
725            phase: CommandReplayPhase::Idle,
726            center: Point::new(0.0, 0.0),
727            snapshot: CommandRecording::default(),
728            segments: Vec::new(),
729            next_slot_id: 0,
730            lifetime_deaths: 0,
731            lifetime_splits: 0,
732            capture_coverage: 0.0,
733            frames_since_capture: 0,
734            optimistic_commits: 0,
735            verify_results: Vec::new(),
736            recolor_scratch: Vec::new(),
737            best_recolor_scratch: Vec::new(),
738            verify_pending: std::collections::VecDeque::new(),
739            verify_survivors: Vec::new(),
740        }
741    }
742}
743
744impl CommandReplayState {
745    pub fn segments(&self) -> &[CommandSegment] {
746        &self.segments
747    }
748
749    /// Lifetime (deaths, splits) across every verified frame — diagnostics
750    /// for judging how churn interacts with retention.
751    pub fn stats(&self) -> (u64, u64) {
752        (self.lifetime_deaths, self.lifetime_splits)
753    }
754
755    /// Frames the pooled fast path fully committed (0 without an executor).
756    pub fn optimistic_commits(&self) -> u64 {
757        self.optimistic_commits
758    }
759
760    /// The similarity pivot all span transforms rotate and scale about.
761    pub fn center(&self) -> Point {
762        self.center
763    }
764
765    /// Advances the state machine with this frame's recording and returns
766    /// what the frame can retain. Phases mirror the flat-list detector:
767    /// snapshot on the first sighting, partition into
768    /// transform-consistent chains on the second, verify per entry from the
769    /// third on. A structural collapse or coverage erosion re-snapshots;
770    /// correctness never depends on the detector being right about
771    /// stability — a wrong guess costs a frame of ordinary rendering.
772    pub fn advance(&mut self, current: &CommandRecording) -> ReplayOutcome {
773        self.advance_pooled(current, None)
774    }
775
776    /// [`Self::advance`] with an optional executor that verification fans
777    /// its per-segment span matching across. The pooled path is exercised
778    /// only on frames where every segment commits cleanly at its first
779    /// probe-passing anchor — any other frame falls back to the serial
780    /// walk, so the outcome is identical with and without an executor.
781    pub fn advance_pooled(
782        &mut self,
783        current: &CommandRecording,
784        pool: Option<&dyn VerifyExecutor>,
785    ) -> ReplayOutcome {
786        if current.tape.len() < MIN_REPLAY_COMMAND_RECORDS {
787            self.retire();
788            return ReplayOutcome::AllDynamic;
789        }
790        let Some(center) = detect_center(current) else {
791            self.retire();
792            return ReplayOutcome::AllDynamic;
793        };
794        match self.phase {
795            CommandReplayPhase::Idle => {
796                self.take_snapshot(current, center);
797                ReplayOutcome::AllDynamic
798            }
799            CommandReplayPhase::Snapshotted => self.partition(current, center),
800            CommandReplayPhase::Captured => self.verify(current, pool),
801        }
802    }
803
804    fn retire(&mut self) {
805        self.phase = CommandReplayPhase::Idle;
806        self.snapshot = CommandRecording::default();
807        self.segments.clear();
808    }
809
810    fn take_snapshot(&mut self, current: &CommandRecording, center: Point) {
811        self.snapshot = current.clone();
812        self.center = center;
813        self.segments.clear();
814        self.phase = CommandReplayPhase::Snapshotted;
815    }
816
817    /// Splits the recording into maximal chains of consecutive entries that
818    /// moved from the snapshot by one shared similarity transform, then
819    /// re-snapshots at the current values so verification always compares
820    /// against the capture frame. The returned spans carry the capture
821    /// content itself (`capture: true`, identity transform): the snapshot
822    /// IS this frame, so what the renderer retains equals what later
823    /// transforms move.
824    fn partition(&mut self, current: &CommandRecording, center: Point) -> ReplayOutcome {
825        let aligned = align_recordings(current, &self.snapshot);
826        let mut chains: Vec<(usize, usize)> = Vec::new();
827        let mut i = 0;
828        while i < current.tape.len() {
829            let (Some(view), Some(snapshot_view)) = (
830                view_at(current, i),
831                aligned[i].and_then(|j| view_at(&self.snapshot, j)),
832            ) else {
833                i += 1;
834                continue;
835            };
836            // A chain anchor must pin rotation itself.
837            let Some((t, true)) =
838                pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
839            else {
840                i += 1;
841                continue;
842            };
843            if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
844                == RecordMatch::Mismatch
845            {
846                i += 1;
847                continue;
848            }
849            let start = i;
850            let mut end = i + 1;
851            while end < current.tape.len() {
852                let (Some(view), Some(snapshot_view)) = (
853                    view_at(current, end),
854                    aligned[end].and_then(|j| view_at(&self.snapshot, j)),
855                ) else {
856                    break;
857                };
858                let Some((entry_t, pinned)) =
859                    pair_transform(current, view, &self.snapshot, snapshot_view, self.center)
860                else {
861                    break;
862                };
863                if !transforms_group(entry_t, pinned, t) {
864                    break;
865                }
866                if match_pair(current, view, &self.snapshot, snapshot_view, self.center, t)
867                    == RecordMatch::Mismatch
868                {
869                    break;
870                }
871                end += 1;
872            }
873            if end - start >= MIN_SEGMENT_RECORDS {
874                let mut piece_start = start;
875                while piece_start < end {
876                    let piece_end = (piece_start + MAX_SEGMENT_RECORDS).min(end);
877                    if piece_end - piece_start >= MIN_SEGMENT_RECORDS {
878                        chains.push((piece_start, piece_end));
879                    }
880                    piece_start = piece_end;
881                }
882            }
883            i = end.max(i + 1);
884        }
885
886        if chains.is_empty() {
887            self.take_snapshot(current, center);
888            return ReplayOutcome::AllDynamic;
889        }
890        // Re-snapshot at current values: chain ranges are current-tape
891        // ranges, which the fresh snapshot preserves verbatim.
892        self.take_snapshot(current, center);
893        self.segments = chains
894            .into_iter()
895            .map(|range| {
896                let slot = self.next_slot_id;
897                self.next_slot_id += 1;
898                CommandSegment {
899                    slot,
900                    slot_offset: 0,
901                    tape_start: range.0,
902                    tape_end: range.1,
903                    bounds: range_bounds(&self.snapshot, range),
904                }
905            })
906            .collect();
907        let covered: usize = self
908            .segments
909            .iter()
910            .map(|segment| segment.tape_end - segment.tape_start)
911            .sum();
912        self.capture_coverage = covered as f32 / current.tape.len().max(1) as f32;
913        self.frames_since_capture = 0;
914        self.phase = CommandReplayPhase::Captured;
915
916        let mut spans: Vec<ReplaySpan> = Vec::with_capacity(self.segments.len() * 2 + 1);
917        let mut cursor = 0usize;
918        for segment in &self.segments {
919            if segment.tape_start > cursor {
920                spans.push(ReplaySpan::Dynamic {
921                    tape_start: cursor,
922                    tape_end: segment.tape_start,
923                });
924            }
925            spans.push(ReplaySpan::Retained {
926                slot: segment.slot,
927                capture: true,
928                slot_offset: 0,
929                tape_start: segment.tape_start,
930                tape_end: segment.tape_end,
931                transform: RecordTransform::IDENTITY,
932                recolors: Vec::new(),
933                bounds: segment.bounds,
934            });
935            cursor = segment.tape_end;
936        }
937        if cursor < current.tape.len() {
938            spans.push(ReplaySpan::Dynamic {
939                tape_start: cursor,
940                tape_end: current.tape.len(),
941            });
942        }
943        ReplayOutcome::Spans(spans)
944    }
945
946    /// Verifies this frame's recording against the capture. Each segment
947    /// re-locates its anchor by searching forward from the cursor within
948    /// [`RESYNC_WINDOW`] — dynamic spans between segments change length
949    /// freely — probing a few entries under each candidate transform before
950    /// committing to a full-span verification (a wrong candidate from a
951    /// different ring fails the probe on its radii). A mismatch mid-span
952    /// splits the segment: the matched prefix stays retained, the record
953    /// that changed goes dynamic, and the suffix re-enters the location
954    /// queue as its own segment — churn costs the records it touched, not
955    /// the whole capture. Eroded coverage re-snapshots for the next frame.
956    fn verify(
957        &mut self,
958        current: &CommandRecording,
959        pool: Option<&dyn VerifyExecutor>,
960    ) -> ReplayOutcome {
961        if let Some(pool) = pool {
962            if self.segments.len() >= 2 {
963                if let Some((spans, retained_records)) = self.verify_optimistic(current, pool) {
964                    self.optimistic_commits += 1;
965                    return self.finish_verify(current, spans, retained_records);
966                }
967            }
968        }
969        let mut spans: Vec<ReplaySpan> = Vec::new();
970        let mut retained_records = 0usize;
971        // Current-tape position covered so far.
972        let mut cursor = 0usize;
973        // Segments awaiting location this frame, tape order. A split pushes
974        // the suffix back onto the front so it is located before the next
975        // original segment. Both queues are persistent fields refilled per
976        // frame, so their buffers keep their high-water capacity.
977        self.verify_pending.clear();
978        self.verify_pending.extend(self.segments.drain(..));
979        self.verify_survivors.clear();
980        while let Some(segment) = self.verify_pending.pop_front() {
981            let len = segment.tape_end - segment.tape_start;
982            let search_end = (cursor + RESYNC_WINDOW)
983                .min(current.tape.len().saturating_sub(len - 1))
984                .max(cursor);
985            // Candidates run LEFT TO RIGHT from the cursor, never by
986            // proximity to an expected position: within a self-similar
987            // ring, every pairing shifted right of the true anchor passes
988            // probes (recolor-tolerant matching even repaints the color
989            // pattern) with a sub-tolerance angle residual — the one
990            // pairing a distance heuristic must never be allowed to reach
991            // first. The true anchor is always the LEFTMOST compatible
992            // candidate, exactly the order the flat detector proved out.
993            let candidates = cursor..search_end;
994            let mut located: Option<(usize, RecordTransform)> = None;
995            // The longest cleanly matched prefix among failed commits:
996            // (start, transform); its length and the recolors within it
997            // live in `best_prefix_len` / `best_recolor_scratch`. A genuine
998            // mid-span change surfaces here — the right anchor matches far
999            // more than any mislocated one.
1000            let mut best_prefix: Option<(usize, RecordTransform)> = None;
1001            let mut best_prefix_len = 0usize;
1002            let mut attempts = 0usize;
1003            'search: for start in candidates {
1004                let Some(t) = probe_anchor(
1005                    current,
1006                    &self.snapshot,
1007                    self.center,
1008                    segment.tape_start,
1009                    len,
1010                    start,
1011                ) else {
1012                    continue;
1013                };
1014                // Committed: verify the whole span. A failure may still be a
1015                // mislocated anchor (self-similar rings), so the search
1016                // resumes — a bounded number of times.
1017                let matched = match_span(
1018                    TypedRecords::from(current),
1019                    TypedRecords::from(&self.snapshot),
1020                    self.center,
1021                    start,
1022                    segment.tape_start,
1023                    len,
1024                    t,
1025                    &mut self.recolor_scratch,
1026                );
1027                if matched < len {
1028                    if matched > best_prefix_len {
1029                        best_prefix_len = matched;
1030                        best_prefix = Some((start, t));
1031                        // Keep the best prefix's recolors without an
1032                        // allocation: the two scratches trade places.
1033                        std::mem::swap(&mut self.recolor_scratch, &mut self.best_recolor_scratch);
1034                    }
1035                    // Only failures with a substantial matched prefix
1036                    // consume the commit budget: those are genuine split
1037                    // candidates, and re-verifying long spans is the cost
1038                    // being bounded. A short-prefix failure is just a wrong
1039                    // anchor (a dead predecessor's entries, a cross-ring
1040                    // pairing) that the scan must be free to step past —
1041                    // charging those burned the budget before the true
1042                    // anchor and killed healthy segments.
1043                    if matched >= MIN_SEGMENT_RECORDS {
1044                        attempts += 1;
1045                        if attempts >= MAX_COMMIT_ATTEMPTS {
1046                            break 'search;
1047                        }
1048                    }
1049                    continue;
1050                }
1051                located = Some((start, t));
1052                break;
1053            }
1054            // A failed segment splits around the record that changed: the
1055            // matched prefix is retained now, the suffix re-enters the
1056            // queue to locate itself past whatever churn displaced it. Only
1057            // a prefix long enough to prove the anchor was right earns a
1058            // split — a segment with no solid prefix dies whole, or a weak
1059            // wrong-anchor prefix would shed one record and re-fail across
1060            // the whole span. The emitted span `mem::take`s its recolors
1061            // out of the owning scratch — the buffer walks into the graph
1062            // and the scratch re-grows on the next attempt (accepted:
1063            // emitting spans do real work).
1064            let (span_start, t, recolors, span_len) = match located {
1065                Some((start, t)) => (start, t, std::mem::take(&mut self.recolor_scratch), len),
1066                None => {
1067                    let split = best_prefix_len >= MIN_SEGMENT_RECORDS;
1068                    let Some((start, t)) = best_prefix.filter(|_| split) else {
1069                        self.lifetime_deaths += 1;
1070                        continue;
1071                    };
1072                    let suffix_start = segment.tape_start + best_prefix_len + 1;
1073                    if segment.tape_end > suffix_start
1074                        && segment.tape_end - suffix_start >= MIN_SEGMENT_RECORDS
1075                    {
1076                        // The suffix addresses the SAME captured content,
1077                        // just deeper in: no recapture, only an offset.
1078                        self.verify_pending.push_front(CommandSegment {
1079                            slot: segment.slot,
1080                            slot_offset: segment.slot_offset + (suffix_start - segment.tape_start),
1081                            tape_start: suffix_start,
1082                            tape_end: segment.tape_end,
1083                            bounds: range_bounds(&self.snapshot, (suffix_start, segment.tape_end)),
1084                        });
1085                    }
1086                    self.lifetime_splits += 1;
1087                    (
1088                        start,
1089                        t,
1090                        std::mem::take(&mut self.best_recolor_scratch),
1091                        best_prefix_len,
1092                    )
1093                }
1094            };
1095            let survivor = if span_len == len {
1096                segment
1097            } else {
1098                // The prefix keeps its capture identity — it addresses the
1099                // same slot content from the same offset, just shorter.
1100                CommandSegment {
1101                    slot: segment.slot,
1102                    slot_offset: segment.slot_offset,
1103                    tape_start: segment.tape_start,
1104                    tape_end: segment.tape_start + span_len,
1105                    bounds: range_bounds(
1106                        &self.snapshot,
1107                        (segment.tape_start, segment.tape_start + span_len),
1108                    ),
1109                }
1110            };
1111            if span_start > cursor {
1112                spans.push(ReplaySpan::Dynamic {
1113                    tape_start: cursor,
1114                    tape_end: span_start,
1115                });
1116            }
1117            retained_records += span_len;
1118            spans.push(ReplaySpan::Retained {
1119                slot: survivor.slot,
1120                capture: false,
1121                slot_offset: survivor.slot_offset,
1122                tape_start: span_start,
1123                tape_end: span_start + span_len,
1124                transform: t,
1125                recolors,
1126                bounds: t.apply_to_bounds(self.center, survivor.bounds),
1127            });
1128            cursor = span_start + span_len;
1129            self.verify_survivors.push(survivor);
1130        }
1131        if cursor < current.tape.len() {
1132            spans.push(ReplaySpan::Dynamic {
1133                tape_start: cursor,
1134                tape_end: current.tape.len(),
1135            });
1136        }
1137
1138        // Survivors become the live table; swapping (the table was drained
1139        // above) lets the two buffers ping-pong, both keeping capacity.
1140        std::mem::swap(&mut self.segments, &mut self.verify_survivors);
1141        self.finish_verify(current, spans, retained_records)
1142    }
1143
1144    /// The verification epilogue shared by the serial and pooled paths:
1145    /// coverage bookkeeping and the collapse/erosion re-snapshot decision.
1146    fn finish_verify(
1147        &mut self,
1148        current: &CommandRecording,
1149        spans: Vec<ReplaySpan>,
1150        retained_records: usize,
1151    ) -> ReplayOutcome {
1152        self.frames_since_capture += 1;
1153        let retained_total: usize = self
1154            .segments
1155            .iter()
1156            .map(|segment| segment.tape_end - segment.tape_start)
1157            .sum();
1158        let coverage = retained_total as f32 / current.tape.len().max(1) as f32;
1159        let collapsed = retained_records == 0 || coverage < MIN_COVERAGE_FRACTION;
1160        let eroded = coverage + RECAPTURE_EROSION < self.capture_coverage
1161            && self.frames_since_capture >= RECAPTURE_COOLDOWN_FRAMES;
1162        if collapsed || eroded {
1163            // Re-snapshot so the next two frames re-partition. Collapse pays
1164            // immediately; mere erosion waits out the capture cooldown.
1165            let center = self.center;
1166            self.take_snapshot(current, center);
1167            if retained_records == 0 {
1168                return ReplayOutcome::AllDynamic;
1169            }
1170        }
1171        ReplayOutcome::Spans(spans)
1172    }
1173
1174    /// The clean-frame fast path: locates every segment serially with cheap
1175    /// probes only (identical candidate order to the serial walk), then fans
1176    /// the expensive full-span matching across `pool`. Returns `None` — and
1177    /// changes nothing but its private result scratch — the moment any
1178    /// segment lacks a probe-passing candidate
1179    /// or any span fails to match whole, leaving the serial walk
1180    /// to redo the frame with its split/death/attempt machinery. When it
1181    /// does return spans, they are exactly what the serial walk would have
1182    /// produced: a fully matching first probe-passing candidate is the
1183    /// leftmost committing candidate.
1184    fn verify_optimistic(
1185        &mut self,
1186        current: &CommandRecording,
1187        pool: &dyn VerifyExecutor,
1188    ) -> Option<(Vec<ReplaySpan>, usize)> {
1189        struct SpanJob {
1190            start: usize,
1191            seg_start: usize,
1192            len: usize,
1193            t: RecordTransform,
1194        }
1195        let mut jobs: Vec<SpanJob> = Vec::with_capacity(self.segments.len());
1196        let mut cursor = 0usize;
1197        for segment in &self.segments {
1198            let len = segment.tape_end - segment.tape_start;
1199            let search_end = (cursor + RESYNC_WINDOW)
1200                .min(current.tape.len().saturating_sub(len - 1))
1201                .max(cursor);
1202            let mut found = None;
1203            for start in cursor..search_end {
1204                if let Some(t) = probe_anchor(
1205                    current,
1206                    &self.snapshot,
1207                    self.center,
1208                    segment.tape_start,
1209                    len,
1210                    start,
1211                ) {
1212                    found = Some((start, t));
1213                    break;
1214                }
1215            }
1216            let (start, t) = found?;
1217            jobs.push(SpanJob {
1218                start,
1219                seg_start: segment.tape_start,
1220                len,
1221                t,
1222            });
1223            cursor = start + len;
1224        }
1225        // One reusable result slot per job, grown once and kept across
1226        // frames; every job writes only its own slot, filling the slot's
1227        // own recolor buffer in place via the out-param.
1228        if self.verify_results.len() < jobs.len() {
1229            self.verify_results
1230                .resize_with(jobs.len(), Default::default);
1231        }
1232        {
1233            let current = TypedRecords::from(current);
1234            let snapshot = TypedRecords::from(&self.snapshot);
1235            let center = self.center;
1236            let jobs = &jobs;
1237            let results = &self.verify_results;
1238            pool.for_each(jobs.len(), &|i| {
1239                let job = &jobs[i];
1240                let mut guard = results[i].lock().expect("verify span job lock");
1241                let slot = &mut *guard;
1242                slot.matched = match_span(
1243                    current,
1244                    snapshot,
1245                    center,
1246                    job.start,
1247                    job.seg_start,
1248                    job.len,
1249                    job.t,
1250                    &mut slot.recolors,
1251                );
1252            });
1253        }
1254        // Bail before taking anything: a single short match leaves every
1255        // slot's recolor buffer warm for the serial rerun and later frames.
1256        for (job, result) in jobs.iter().zip(&self.verify_results) {
1257            if result.lock().expect("verify span job lock").matched < job.len {
1258                return None;
1259            }
1260        }
1261        let mut spans: Vec<ReplaySpan> = Vec::with_capacity(jobs.len() * 2 + 1);
1262        let mut retained_records = 0usize;
1263        let mut cursor = 0usize;
1264        for (segment, (job, result)) in self
1265            .segments
1266            .iter()
1267            .zip(jobs.iter().zip(&self.verify_results))
1268        {
1269            // Committing: each emitted span takes its slot's buffer — the
1270            // capacity walks into the graph and the slot re-grows next
1271            // frame (accepted: emitting spans do real work).
1272            let recolors =
1273                std::mem::take(&mut result.lock().expect("verify span job lock").recolors);
1274            if job.start > cursor {
1275                spans.push(ReplaySpan::Dynamic {
1276                    tape_start: cursor,
1277                    tape_end: job.start,
1278                });
1279            }
1280            retained_records += job.len;
1281            spans.push(ReplaySpan::Retained {
1282                slot: segment.slot,
1283                capture: false,
1284                slot_offset: segment.slot_offset,
1285                tape_start: job.start,
1286                tape_end: job.start + job.len,
1287                transform: job.t,
1288                recolors,
1289                bounds: job.t.apply_to_bounds(self.center, segment.bounds),
1290            });
1291            cursor = job.start + job.len;
1292        }
1293        if cursor < current.tape.len() {
1294            spans.push(ReplaySpan::Dynamic {
1295                tape_start: cursor,
1296                tape_end: current.tape.len(),
1297            });
1298        }
1299        Some((spans, retained_records))
1300    }
1301}
1302
1303/// The cheap anchor test shared by the serial walk and the pooled fast
1304/// path: view compatibility, transform derivation from the anchor pair, and
1305/// [`ANCHOR_PROBE_RECORDS`] probe matches. `None` means this candidate
1306/// cannot be the segment's anchor.
1307fn probe_anchor(
1308    current: &CommandRecording,
1309    snapshot: &CommandRecording,
1310    center: Point,
1311    seg_start: usize,
1312    len: usize,
1313    start: usize,
1314) -> Option<RecordTransform> {
1315    let (Some(view), Some(snapshot_view)) = (view_at(current, start), view_at(snapshot, seg_start))
1316    else {
1317        return None;
1318    };
1319    if !views_compatible(current, Some(view), snapshot, Some(snapshot_view)) {
1320        return None;
1321    }
1322    let (t, _) = pair_transform(current, view, snapshot, snapshot_view, center)?;
1323    for probe in 0..ANCHOR_PROBE_RECORDS.min(len) {
1324        let (Some(view), Some(snapshot_view)) = (
1325            view_at(current, start + probe),
1326            view_at(snapshot, seg_start + probe),
1327        ) else {
1328            return None;
1329        };
1330        if match_pair(current, view, snapshot, snapshot_view, center, t) == RecordMatch::Mismatch {
1331            return None;
1332        }
1333    }
1334    Some(t)
1335}
1336
1337/// The typed-record arrays a span match reads — the POD slice view of a
1338/// [`CommandRecording`] that is `Sync` (the recording itself is not: its
1339/// `others` vector may hold `Rc`-carrying primitives), which is what lets
1340/// [`match_span`] calls cross worker threads.
1341#[derive(Clone, Copy)]
1342struct TypedRecords<'a> {
1343    tape: &'a [TapeRef],
1344    arcs: &'a [SolidArcRecord],
1345    round_rects: &'a [SolidRoundRectRecord],
1346}
1347
1348impl<'a> From<&'a CommandRecording> for TypedRecords<'a> {
1349    fn from(recording: &'a CommandRecording) -> Self {
1350        Self {
1351            tape: &recording.tape,
1352            arcs: &recording.arcs,
1353            round_rects: &recording.round_rects,
1354        }
1355    }
1356}
1357
1358impl TypedRecords<'_> {
1359    /// [`view_at`] over the POD slices, for worker-thread span matching —
1360    /// the same [`view_at_slices`] implementation, so eligibility cannot
1361    /// drift between the two forms.
1362    fn view_at(&self, i: usize) -> Option<ReplayView> {
1363        view_at_slices(self.tape, self.round_rects, i)
1364    }
1365}
1366
1367/// The full-span commit body: matches `len` records of `current` from
1368/// `start` against the snapshot span at `seg_start` under `t`. Fills
1369/// `recolors` (cleared at entry) with the recolors inside the cleanly
1370/// matched prefix and returns that prefix's length — the out-param lets
1371/// callers own reusable buffers instead of allocating per call. The
1372/// record dispatch mirrors [`match_pair`] exactly; it operates on the typed
1373/// slices so one call per segment can run on a worker thread.
1374#[allow(clippy::too_many_arguments)]
1375fn match_span(
1376    current: TypedRecords<'_>,
1377    snapshot: TypedRecords<'_>,
1378    center: Point,
1379    start: usize,
1380    seg_start: usize,
1381    len: usize,
1382    t: RecordTransform,
1383    recolors: &mut Vec<(u32, Color)>,
1384) -> usize {
1385    recolors.clear();
1386    for offset in 0..len {
1387        let entry_match = match (
1388            current.view_at(start + offset),
1389            snapshot.view_at(seg_start + offset),
1390        ) {
1391            (Some(ReplayView::Arc(i)), Some(ReplayView::Arc(j))) => {
1392                match_arc(&current.arcs[i], &snapshot.arcs[j], center, t)
1393            }
1394            (Some(ReplayView::RoundRect(i)), Some(ReplayView::RoundRect(j))) => {
1395                match_round_rect(&current.round_rects[i], &snapshot.round_rects[j], center, t)
1396            }
1397            _ => RecordMatch::Mismatch,
1398        };
1399        match entry_match {
1400            RecordMatch::Exact => {}
1401            RecordMatch::Recolor => {
1402                let color = match current.view_at(start + offset) {
1403                    Some(ReplayView::Arc(a)) => current.arcs[a].color,
1404                    Some(ReplayView::RoundRect(r)) => current.round_rects[r].color,
1405                    None => unreachable!("recolor requires a view"),
1406                };
1407                recolors.push((offset as u32, color));
1408            }
1409            RecordMatch::Mismatch => return offset,
1410        }
1411    }
1412    len
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417    use super::*;
1418    use crate::{Color, Stroke};
1419
1420    const CENTER: Point = Point { x: 204.0, y: 204.0 };
1421
1422    fn arc(radius: f32, start: f32, color: Color) -> SolidArcRecord {
1423        SolidArcRecord {
1424            center: CENTER,
1425            radius,
1426            start_angle: start,
1427            sweep_angle: 0.4,
1428            inner_radius: radius * 0.8,
1429            color,
1430            stroke: None,
1431        }
1432    }
1433
1434    fn moved_arc(base: &SolidArcRecord, t: RecordTransform) -> SolidArcRecord {
1435        SolidArcRecord {
1436            center: base.center,
1437            radius: base.radius * t.scale,
1438            start_angle: base.start_angle + t.angle,
1439            sweep_angle: base.sweep_angle,
1440            inner_radius: base.inner_radius * t.scale,
1441            color: base.color,
1442            stroke: base.stroke.map(|stroke| Stroke {
1443                width: stroke.width * t.scale,
1444                ..stroke
1445            }),
1446        }
1447    }
1448
1449    fn circle(cx: f32, cy: f32, diameter: f32, color: Color) -> SolidRoundRectRecord {
1450        SolidRoundRectRecord {
1451            rect: Rect {
1452                x: cx - diameter * 0.5,
1453                y: cy - diameter * 0.5,
1454                width: diameter,
1455                height: diameter,
1456            },
1457            radii: CornerRadii::uniform(diameter * 0.5),
1458            color,
1459            stroke: None,
1460        }
1461    }
1462
1463    #[test]
1464    fn arc_anchor_recovers_the_baked_transform() {
1465        let t = RecordTransform {
1466            scale: 0.9994,
1467            angle: 0.0123,
1468        };
1469        let retained = arc(120.0, 1.0, Color::WHITE);
1470        let current = moved_arc(&retained, t);
1471        let derived = arc_anchor_transform(&current, &retained).expect("derivable");
1472        assert!((derived.scale - t.scale).abs() < 1e-6);
1473        assert!((derived.angle - t.angle).abs() < 1e-6);
1474        assert_eq!(
1475            match_arc(&current, &retained, CENTER, derived),
1476            RecordMatch::Exact
1477        );
1478    }
1479
1480    #[test]
1481    fn recolored_arc_matches_as_recolor() {
1482        let t = RecordTransform {
1483            scale: 1.0,
1484            angle: 0.05,
1485        };
1486        let retained = arc(80.0, 0.2, Color::WHITE);
1487        let mut current = moved_arc(&retained, t);
1488        current.color = Color::rgb(0.5, 0.1, 0.9);
1489        assert_eq!(
1490            match_arc(&current, &retained, CENTER, t),
1491            RecordMatch::Recolor
1492        );
1493    }
1494
1495    #[test]
1496    fn changed_sweep_is_a_mismatch() {
1497        let t = RecordTransform::IDENTITY;
1498        let retained = arc(80.0, 0.2, Color::WHITE);
1499        let mut current = retained;
1500        current.sweep_angle += 0.1;
1501        assert_eq!(
1502            match_arc(&current, &retained, CENTER, t),
1503            RecordMatch::Mismatch
1504        );
1505    }
1506
1507    #[test]
1508    fn stroked_arc_scales_its_width_with_the_segment() {
1509        let t = RecordTransform {
1510            scale: 0.98,
1511            angle: 0.0,
1512        };
1513        let mut retained = arc(60.0, 0.0, Color::WHITE);
1514        retained.stroke = Some(Stroke::new(5.0));
1515        let current = moved_arc(&retained, t);
1516        assert_eq!(
1517            match_arc(&current, &retained, CENTER, t),
1518            RecordMatch::Exact
1519        );
1520
1521        // An unscaled stroke under a scaling segment is a real change: 2%
1522        // of 5px is well past the noise tolerance.
1523        let mut stale = current;
1524        stale.stroke = Some(Stroke::new(5.0));
1525        assert_eq!(
1526            match_arc(&stale, &retained, CENTER, t),
1527            RecordMatch::Mismatch
1528        );
1529    }
1530
1531    #[test]
1532    fn orbiting_circle_matches_under_rotation() {
1533        let t = RecordTransform {
1534            scale: 1.0,
1535            angle: 0.3,
1536        };
1537        let retained = circle(304.0, 204.0, 10.0, Color::WHITE);
1538        let (c_then, d_then) = circle_view(&retained).expect("circle");
1539        let c_now = t.apply(CENTER, c_then);
1540        let current = circle(c_now.x, c_now.y, d_then * t.scale, Color::WHITE);
1541        let (derived, pinned) = circle_anchor_transform_pinned(
1542            circle_view(&current).unwrap(),
1543            (c_then, d_then),
1544            CENTER,
1545        )
1546        .expect("derivable");
1547        assert!(pinned, "an off-pivot circle pins rotation");
1548        assert!((derived.angle - t.angle).abs() < 1e-4);
1549        assert_eq!(
1550            match_round_rect(&current, &retained, CENTER, derived),
1551            RecordMatch::Exact
1552        );
1553    }
1554
1555    #[test]
1556    fn non_circular_round_rect_never_matches() {
1557        let mut retained = circle(304.0, 204.0, 10.0, Color::WHITE);
1558        retained.rect.width = 14.0; // no longer a circle
1559        assert_eq!(
1560            match_round_rect(&retained, &retained, CENTER, RecordTransform::IDENTITY),
1561            RecordMatch::Mismatch
1562        );
1563    }
1564
1565    #[test]
1566    fn grouping_is_tighter_than_verification() {
1567        let anchor = RecordTransform {
1568            scale: 1.0,
1569            angle: 0.010,
1570        };
1571        let same_ring = RecordTransform {
1572            scale: 1.0,
1573            angle: 0.0100001,
1574        };
1575        let next_ring = RecordTransform {
1576            scale: 1.0,
1577            angle: 0.011,
1578        };
1579        assert!(transforms_group(same_ring, true, anchor));
1580        assert!(
1581            !transforms_group(next_ring, true, anchor),
1582            "a 1e-3 rotation-step difference is another ring, not float noise"
1583        );
1584        let unpinned = RecordTransform {
1585            scale: 1.0,
1586            angle: 0.0,
1587        };
1588        assert!(transforms_group(unpinned, false, anchor));
1589    }
1590
1591    use crate::geometry::{DrawScopeDefault, Size};
1592    use crate::{Brush, DrawScope as _};
1593
1594    /// Records one MEGA-shaped frame: `rings` rings of `per_ring` arcs, each
1595    /// ring rotated by its own step × `frame`, breathing scale applied to
1596    /// every radius, plus `tail` dynamic circles whose count varies.
1597    fn ring_frame(rings: usize, per_ring: usize, frame: usize, tail: usize) -> CommandRecording {
1598        let mut scope = DrawScopeDefault::new(Size::new(408.0, 408.0));
1599        let scale = 0.9994f32.powi(frame as i32);
1600        for ring in 0..rings {
1601            let step = 0.01 + ring as f32 * 0.005;
1602            let rotation = step * frame as f32;
1603            let radius = (60.0 + ring as f32 * 30.0) * scale;
1604            for slot in 0..per_ring {
1605                let start = slot as f32 * (std::f32::consts::TAU / per_ring as f32) + rotation;
1606                scope.draw_annular_sector(
1607                    Brush::solid(Color::WHITE),
1608                    CENTER,
1609                    radius * 0.8,
1610                    radius,
1611                    start,
1612                    0.02,
1613                );
1614            }
1615        }
1616        for i in 0..tail {
1617            // Dynamic entities: different positions every frame.
1618            let x = 40.0 + (frame * 17 + i * 31) as f32 % 300.0;
1619            scope.draw_circle(Brush::solid(Color::RED), Point::new(x, 50.0), 3.0);
1620        }
1621        scope.recorded().clone()
1622    }
1623
1624    #[test]
1625    fn ring_scene_reaches_retention_by_the_third_frame() {
1626        let mut state = CommandReplayState::default();
1627        assert!(matches!(
1628            state.advance(&ring_frame(3, 300, 0, 10)),
1629            ReplayOutcome::AllDynamic
1630        ));
1631        // The partition frame itself emits the capture: snapshot == current,
1632        // so every span is capture:true under an identity transform.
1633        let ReplayOutcome::Spans(capture_spans) = state.advance(&ring_frame(3, 300, 1, 10)) else {
1634            panic!("partition frame should emit the capture");
1635        };
1636        assert!(capture_spans.iter().all(|span| match span {
1637            ReplaySpan::Retained {
1638                capture, transform, ..
1639            } => *capture && *transform == RecordTransform::IDENTITY,
1640            ReplaySpan::Dynamic { .. } => true,
1641        }));
1642        assert!(!state.segments().is_empty(), "partition found the rings");
1643
1644        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(3, 300, 2, 10)) else {
1645            panic!("third frame should retain");
1646        };
1647        let retained: usize = spans
1648            .iter()
1649            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
1650            .count();
1651        assert!(retained >= 3, "each ring retains, got {spans:?}");
1652        // The tail circles are dynamic.
1653        assert!(spans
1654            .iter()
1655            .any(|span| matches!(span, ReplaySpan::Dynamic { .. })));
1656        // Retained spans carry the per-ring rotations, not a shared one.
1657        let transforms: Vec<RecordTransform> = spans
1658            .iter()
1659            .filter_map(|span| match span {
1660                ReplaySpan::Retained { transform, .. } => Some(*transform),
1661                _ => None,
1662            })
1663            .collect();
1664        assert!(transforms.windows(2).any(|w| w[0].angle != w[1].angle));
1665    }
1666
1667    #[test]
1668    fn entity_churn_between_frames_still_retains_rings() {
1669        let mut state = CommandReplayState::default();
1670        state.advance(&ring_frame(2, 400, 0, 8));
1671        state.advance(&ring_frame(2, 400, 1, 13)); // tail length changed
1672        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(2, 400, 2, 5)) else {
1673            panic!("churned tail must not break ring retention");
1674        };
1675        let retained_records: usize = spans
1676            .iter()
1677            .filter_map(|span| match span {
1678                ReplaySpan::Retained { .. } => Some(1),
1679                _ => None,
1680            })
1681            .sum();
1682        assert!(retained_records >= 2);
1683    }
1684
1685    #[test]
1686    fn recolors_are_patches_not_mismatches() {
1687        let recolored_frame = |frame: usize| {
1688            let mut recording = ring_frame(1, 600, frame, 0);
1689            // Twinkle: 40 dots change color every frame, geometry untouched.
1690            for i in (0..recording.arcs.len()).step_by(15) {
1691                recording.arcs[i].color = if frame.is_multiple_of(2) {
1692                    Color::rgb(1.0, 0.5, 0.1)
1693                } else {
1694                    Color::rgb(0.1, 0.5, 1.0)
1695                };
1696            }
1697            recording
1698        };
1699        let mut state = CommandReplayState::default();
1700        state.advance(&recolored_frame(0));
1701        state.advance(&recolored_frame(1));
1702        let ReplayOutcome::Spans(spans) = state.advance(&recolored_frame(2)) else {
1703            panic!("twinkles must not break retention");
1704        };
1705        let recolor_count: usize = spans
1706            .iter()
1707            .filter_map(|span| match span {
1708                ReplaySpan::Retained { recolors, .. } => Some(recolors.len()),
1709                _ => None,
1710            })
1711            .sum();
1712        assert!(recolor_count >= 30, "twinkles surface as patches");
1713    }
1714
1715    #[test]
1716    fn geometry_change_kills_only_its_segment() {
1717        let mut state = CommandReplayState::default();
1718        state.advance(&ring_frame(3, 300, 0, 0));
1719        state.advance(&ring_frame(3, 300, 1, 0));
1720        let mut broken = ring_frame(3, 300, 2, 0);
1721        // A brick hit: one entry in the middle ring changes sweep.
1722        broken.arcs[450].sweep_angle *= 3.0;
1723        let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
1724            panic!("one changed entry must not drop the whole command");
1725        };
1726        let retained: usize = spans
1727            .iter()
1728            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
1729            .count();
1730        assert!(
1731            retained >= 2,
1732            "the untouched rings keep retaining, got {spans:?}"
1733        );
1734    }
1735
1736    #[test]
1737    fn mid_segment_change_splits_and_retains_both_halves() {
1738        let mut state = CommandReplayState::default();
1739        state.advance(&ring_frame(1, 900, 0, 0));
1740        state.advance(&ring_frame(1, 900, 1, 0));
1741        assert_eq!(state.segments().len(), 1, "one ring is one segment");
1742        let mut broken = ring_frame(1, 900, 2, 0);
1743        broken.arcs[450].sweep_angle *= 3.0;
1744        let ReplayOutcome::Spans(spans) = state.advance(&broken) else {
1745            panic!("a single changed record must not drop retention");
1746        };
1747        let dynamic: usize = spans
1748            .iter()
1749            .filter_map(|span| match span {
1750                ReplaySpan::Dynamic {
1751                    tape_start,
1752                    tape_end,
1753                } => Some(tape_end - tape_start),
1754                _ => None,
1755            })
1756            .sum();
1757        let retained: Vec<(u32, usize, bool)> = spans
1758            .iter()
1759            .filter_map(|span| match span {
1760                ReplaySpan::Retained {
1761                    slot,
1762                    slot_offset,
1763                    capture,
1764                    ..
1765                } => Some((*slot, *slot_offset, *capture)),
1766                _ => None,
1767            })
1768            .collect();
1769        assert_eq!(
1770            retained.len(),
1771            2,
1772            "prefix and suffix both retain: {spans:?}"
1773        );
1774        // Both pieces address the SAME captured slot — a split never
1775        // recaptures, it re-addresses: the suffix starts one record past
1776        // the prefix within the capture.
1777        assert_eq!(retained[0].0, retained[1].0);
1778        assert_eq!(retained[0].1, 0);
1779        assert_eq!(retained[1].1, 451);
1780        assert!(retained.iter().all(|(_, _, capture)| !capture));
1781        assert_eq!(dynamic, 1, "only the changed record goes dynamic");
1782        assert_eq!(state.stats(), (0, 1), "one split, no deaths");
1783
1784        // The pieces keep retaining on later frames, the changed record's
1785        // slot staying dynamic between them.
1786        let ReplayOutcome::Spans(spans) = state.advance(&ring_frame(1, 900, 3, 0)) else {
1787            panic!("split pieces must keep retaining");
1788        };
1789        let retained = spans
1790            .iter()
1791            .filter(|span| matches!(span, ReplaySpan::Retained { .. }))
1792            .count();
1793        assert_eq!(retained, 2, "both pieces relocate next frame: {spans:?}");
1794    }
1795
1796    #[test]
1797    fn erosion_recaptures_dead_ranges_after_the_cooldown() {
1798        let mut state = CommandReplayState::default();
1799        state.advance(&ring_frame(3, 300, 0, 0));
1800        state.advance(&ring_frame(3, 300, 1, 0));
1801        // The middle ring changes shape permanently: its segment dies, and
1802        // only a recapture can watch the new shape.
1803        let mutated = |frame: usize| {
1804            let mut recording = ring_frame(3, 300, frame, 0);
1805            for arc in &mut recording.arcs[300..600] {
1806                arc.sweep_angle *= 3.0;
1807            }
1808            recording
1809        };
1810        let dynamic_records = |outcome: &ReplayOutcome| -> usize {
1811            match outcome {
1812                ReplayOutcome::AllDynamic => usize::MAX,
1813                ReplayOutcome::Spans(spans) => spans
1814                    .iter()
1815                    .filter_map(|span| match span {
1816                        ReplaySpan::Dynamic {
1817                            tape_start,
1818                            tape_end,
1819                        } => Some(tape_end - tape_start),
1820                        _ => None,
1821                    })
1822                    .sum(),
1823            }
1824        };
1825        let after_death = state.advance(&mutated(2));
1826        let lost = dynamic_records(&after_death);
1827        assert!(
1828            (250..=400).contains(&lost),
1829            "the changed ring goes dynamic, got {lost}"
1830        );
1831        for frame in 3..(3 + RECAPTURE_COOLDOWN_FRAMES as usize + 4) {
1832            state.advance(&mutated(frame));
1833        }
1834        let recovered = state.advance(&mutated(200));
1835        let residue = dynamic_records(&recovered);
1836        assert!(
1837            residue < 50,
1838            "the recapture watches the ring's new shape, got {residue} dynamic"
1839        );
1840    }
1841
1842    #[test]
1843    fn small_commands_are_not_watched() {
1844        let mut state = CommandReplayState::default();
1845        for frame in 0..4 {
1846            assert!(matches!(
1847                state.advance(&ring_frame(1, 40, frame, 0)),
1848                ReplayOutcome::AllDynamic
1849            ));
1850        }
1851        assert!(state.segments().is_empty());
1852    }
1853
1854    /// A real multi-threaded executor for the equivalence test: lane 0 is
1855    /// the caller, the rest are scoped threads, jobs stride across lanes —
1856    /// the same distribution the renderer's frame pool uses.
1857    struct ThreadedExec {
1858        lanes: usize,
1859    }
1860
1861    impl VerifyExecutor for ThreadedExec {
1862        fn for_each(&self, jobs: usize, run: &(dyn Fn(usize) + Sync)) {
1863            std::thread::scope(|s| {
1864                for lane in 1..self.lanes {
1865                    s.spawn(move || {
1866                        let mut i = lane;
1867                        while i < jobs {
1868                            run(i);
1869                            i += self.lanes;
1870                        }
1871                    });
1872                }
1873                let mut i = 0;
1874                while i < jobs {
1875                    run(i);
1876                    i += self.lanes;
1877                }
1878            });
1879        }
1880    }
1881
1882    #[test]
1883    fn pooled_verification_matches_serial_exactly() {
1884        let exec = ThreadedExec { lanes: 3 };
1885        // Every verification path in one churning sequence: multi-ring
1886        // retention under rotation, tail churn, twinkle recolors, and a
1887        // mid-run sweep change that forces the optimistic pass to bail and
1888        // the serial rerun to split.
1889        let frame = |f: usize| -> CommandRecording {
1890            let tail = [10usize, 13, 5, 8, 11, 6, 9, 12][f % 8];
1891            let mut recording = ring_frame(3, 300, f, tail);
1892            if f >= 3 {
1893                for i in (0..recording.arcs.len()).step_by(17) {
1894                    recording.arcs[i].color = if f.is_multiple_of(2) {
1895                        Color::rgb(1.0, 0.5, 0.1)
1896                    } else {
1897                        Color::rgb(0.1, 0.5, 1.0)
1898                    };
1899                }
1900            }
1901            if f == 5 {
1902                // Geometry change inside the middle ring: a genuine
1903                // mismatch mid-segment.
1904                recording.arcs[450].sweep_angle = 0.15;
1905            }
1906            recording
1907        };
1908        let mut serial = CommandReplayState::default();
1909        let mut pooled = CommandReplayState::default();
1910        for f in 0..10 {
1911            let recording = frame(f);
1912            let serial_outcome = serial.advance(&recording);
1913            let pooled_outcome = pooled.advance_pooled(&recording, Some(&exec));
1914            assert_eq!(
1915                serial_outcome, pooled_outcome,
1916                "outcome diverged at frame {f}"
1917            );
1918            assert_eq!(
1919                serial.segments(),
1920                pooled.segments(),
1921                "segments diverged at frame {f}"
1922            );
1923            assert_eq!(
1924                serial.stats(),
1925                pooled.stats(),
1926                "stats diverged at frame {f}"
1927            );
1928        }
1929        let (deaths, splits) = serial.stats();
1930        assert!(
1931            !serial.segments().is_empty() && deaths + splits > 0,
1932            "sequence must exercise both retention and the mismatch path, \
1933             got {deaths} deaths {splits} splits {} segments",
1934            serial.segments().len()
1935        );
1936        assert_eq!(serial.optimistic_commits(), 0);
1937        assert!(
1938            pooled.optimistic_commits() >= 3,
1939            "the pooled fast path must actually commit steady frames, got {}",
1940            pooled.optimistic_commits()
1941        );
1942    }
1943
1944    #[test]
1945    fn transformed_bounds_contain_the_moved_content() {
1946        let t = RecordTransform {
1947            scale: 1.1,
1948            angle: 0.5,
1949        };
1950        let bounds = Rect {
1951            x: 150.0,
1952            y: 150.0,
1953            width: 100.0,
1954            height: 30.0,
1955        };
1956        let moved = t.apply_to_bounds(CENTER, bounds);
1957        for corner in [
1958            Point::new(bounds.x, bounds.y),
1959            Point::new(bounds.x + bounds.width, bounds.y + bounds.height),
1960        ] {
1961            let p = t.apply(CENTER, corner);
1962            assert!(p.x >= moved.x - 1e-3 && p.x <= moved.x + moved.width + 1e-3);
1963            assert!(p.y >= moved.y - 1e-3 && p.y <= moved.y + moved.height + 1e-3);
1964        }
1965    }
1966}