Skip to main content

long_screen/engine/solve/
mod.rs

1//! The solve pass as a session the host drives frame by frame: per-region tracking (odometry, anchor
2//! re-acquisition, drift control, relocalization, fragments), keyframes and revisits, fragment attachment, the
3//! pose graph's nodes and edges, displacement-spread consistency voting, and each frame's placements.
4//!
5//! The session keeps what the TypeScript pass kept in memory: per-region state, the previous frame's pixels,
6//! analysis gray and features, the previous plan's placements, the attachments, the canvases it created, the pose
7//! graph, the keyframe index's postings and the voting ring. Keyframes themselves are rows the host stores
8//! (`keyframe/<id>`) and reads back: a revisit search is declare-then-provide ([`Poll::NeedKeyframes`] →
9//! [`SolveSession::provide_keyframes`]).
10//!
11//! One frame:
12//! 1. [`SolveSession::begin_frame`] with the frame's scan record. A duplicate frame whose previous plan can be
13//!    replayed is finished right there ([`Begin::Duplicate`]); otherwise the session asks for the pixels.
14//! 2. The host writes the RGBA frame into [`SolveSession::frame_mut`] and calls [`SolveSession::run_frame`] with
15//!    the analysis gray it computed (WebGPU) or `None`, and the frame's stored features or `None`.
16//! 3. While the result is [`Poll::NeedKeyframes`], the host loads those rows and calls
17//!    [`SolveSession::provide_keyframes`].
18//! 4. [`Poll::Done`] carries the placements and finalised voting records. Before each poll result, and after an
19//!    error, the host takes the frame's [`Effect`]s ([`SolveSession::take_effects`]) and applies them in order:
20//!    they are the rows and diagnostics the pass produced, in the order it produced them.
21//!
22//! After the last frame the host drains the voting ring ([`SolveSession::drain_voting`]) and relaxes the pose
23//! graph ([`SolveSession::graph_mut`]), a few sweeps per step.
24
25mod region;
26
27use crate::engine::attachments::{Attachment, Attachments};
28use crate::engine::diagnostic::{Diagnostic, Severity};
29use crate::engine::keyframes::{KeyframeIndex, Retrieval};
30use crate::engine::pose_graph::{PoseGraph, PoseGraphError, Write};
31use crate::kernels::features::{extract_features, Feature};
32use crate::kernels::geometry::Rect;
33use crate::kernels::raster::{downscale_gray, downscaled_size, grayscale};
34use crate::kernels::region::{Mask, Region as Shape};
35use crate::kernels::voting::{Finalized, Ring};
36use std::collections::HashMap;
37use std::fmt;
38use std::sync::Arc;
39
40/// Bytes of analysis frames the consistency voting ring keeps (docs/ARCHITECTURE.md §七).
41pub const VOTING_RING_BYTES: usize = 24 * 1024 * 1024;
42/// Features extracted from a frame whose stored features are missing.
43pub const FALLBACK_FEATURES: usize = 480;
44/// Native patches a moving region's keyframe keeps, and their side.
45pub const PATCH_COUNT: usize = 24;
46pub const PATCH_SIZE: usize = 32;
47/// How far a pose may reach: the compositor's addressable range less a margin larger than any frame.
48pub const POSE_BOUND: f64 = crate::engine::compositor::CANVAS_PIXEL_BOUND - 65536.0;
49
50/// Whether `(x, y)` is a pose the pass may write: finite and inside [`POSE_BOUND`].
51pub fn is_valid_pose(x: f64, y: f64) -> bool {
52    x.is_finite() && y.is_finite() && x.abs() < POSE_BOUND && y.abs() < POSE_BOUND
53}
54
55/// The label a registration plane gives a moving region's screen-fixed pixels: the atlas uses 0 and 1–254.
56pub(super) const UNREGISTERED: u8 = 255;
57
58/// One non-ignored region the pass tracks.
59pub struct SolveRegion {
60    pub id: String,
61    pub name: String,
62    /// `moving` regions are tracked; every other kind is placed once at the origin.
63    pub moving: bool,
64    /// Its kind as the canvas row stores it (`moving`, `fixed`).
65    pub kind: String,
66    pub unassigned: bool,
67    /// The region's code in the atlas label plane.
68    pub code: u8,
69    pub shape: Shape,
70}
71
72pub struct SolveConfig {
73    pub width: usize,
74    pub height: usize,
75    /// Integer analysis factor.
76    pub factor: u32,
77    /// Native refinement radius.
78    pub radius: i32,
79    /// The source's per-channel decode noise.
80    pub noise: f64,
81    pub voting_budget: usize,
82    pub regions: Vec<SolveRegion>,
83}
84
85/// The scan record fields the pass reads.
86#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct ScanFields {
88    pub duplicate: bool,
89    pub zoom: f64,
90    pub difference: f64,
91}
92
93/// A row-major 8-bit plane.
94#[derive(Clone, Debug, PartialEq)]
95pub struct Plane {
96    pub width: usize,
97    pub height: usize,
98    pub data: Vec<u8>,
99}
100
101/// Native luma texture sampled around a keyframe feature.
102#[derive(Clone, Debug, PartialEq)]
103pub struct KeyPatch {
104    pub x: f64,
105    pub y: f64,
106    pub size: usize,
107    pub data: Vec<u8>,
108}
109
110/// One `keyframe/<id>` row.
111#[derive(Clone, Debug, PartialEq)]
112pub struct Keyframe {
113    pub id: String,
114    pub node: String,
115    pub canvas_id: String,
116    pub layer: String,
117    pub frame: u32,
118    pub features: Vec<Feature>,
119    pub gray: Arc<Plane>,
120    pub x: f64,
121    pub y: f64,
122    pub scale_x: f64,
123    pub scale_y: f64,
124    pub patches: Vec<KeyPatch>,
125}
126
127/// One `canvas/<id>` row as the solve pass writes it; every count the render pass fills in starts at zero.
128#[derive(Clone, Debug, PartialEq)]
129pub struct CanvasMeta {
130    pub id: String,
131    pub layer: String,
132    pub name: String,
133    pub kind: String,
134    pub fragment: u32,
135    pub first_time: f64,
136    pub last_time: f64,
137    pub attached_to: Option<String>,
138}
139
140/// One region's observation in a frame (a placement of `plan/<frame>`).
141#[derive(Clone, Debug, PartialEq)]
142pub struct Placement {
143    pub layer: String,
144    pub canvas_id: String,
145    pub node: String,
146    pub x: f64,
147    pub y: f64,
148    pub confidence: f64,
149    pub uncertain: bool,
150    pub time: f64,
151    pub skip: bool,
152    pub occlusions: Vec<Rect>,
153}
154
155/// The cause a new fragment's diagnostic names: a magnification change (`scale`, `error`), or none.
156pub type FragmentScale = Option<(f64, f64)>;
157
158/// Something the pass did that the host must carry out, in order.
159#[derive(Clone, Debug, PartialEq)]
160pub enum Effect {
161    /// Store `canvas/<id>`; `created` also counts a new canvas.
162    Canvas {
163        meta: CanvasMeta,
164        created: bool,
165    },
166    /// Store `attach/<id>`.
167    Attachment(Attachment),
168    /// Store `keyframe/<id>`.
169    Keyframe(Arc<Keyframe>),
170    /// Store pose graph rows: one `put` or one `putMany`.
171    Graph(Write),
172    Diagnostic(Diagnostic),
173}
174
175/// One region slot's finalised voting verdict: the analysis box it covers and its two LSB-first bitsets.
176#[derive(Clone, Debug, PartialEq)]
177pub struct VotingVerdict {
178    pub region: String,
179    pub x0: i32,
180    pub y0: i32,
181    pub w: i32,
182    pub h: i32,
183    pub bits: Vec<u8>,
184    pub clean: Vec<u8>,
185}
186
187/// One finalised frame of the voting ring (a `consistency/<frame>` row when it has verdicts).
188#[derive(Clone, Debug, PartialEq)]
189pub struct VotingRecord {
190    pub index: u32,
191    pub verdicts: Vec<VotingVerdict>,
192    pub voted_layers: u32,
193    pub thin_layers: u32,
194}
195
196/// A solved frame.
197#[derive(Clone, Debug, PartialEq)]
198pub struct FrameDone {
199    /// One per region, in region order.
200    pub placements: Vec<Placement>,
201    /// Whether the frame joined the voting ring (some moving region observed it); `voting` is then what its
202    /// bytes pushed out of the ring.
203    pub joined_ring: bool,
204    pub voting: Vec<VotingRecord>,
205}
206
207/// The outcome of [`SolveSession::begin_frame`].
208#[derive(Clone, Debug, PartialEq)]
209pub enum Begin {
210    /// The frame repeats the previous one exactly: its plan is the previous plan at this frame's time.
211    Duplicate(Vec<Placement>),
212    /// Write the frame into [`SolveSession::frame_mut`], then [`SolveSession::run_frame`]; `gray` says whether
213    /// an analysis gray is wanted (a duplicate frame reuses the previous one).
214    NeedFrame { gray: bool },
215}
216
217#[derive(Clone, Debug, PartialEq)]
218pub enum Poll {
219    /// Load these `keyframe/<id>` rows and [`SolveSession::provide_keyframes`] them, in this order.
220    NeedKeyframes(Vec<String>),
221    Done(FrameDone),
222}
223
224#[derive(Clone, Debug, PartialEq)]
225pub enum SolveError {
226    Graph(PoseGraphError),
227    /// The host called a step out of order or handed in malformed input.
228    BadInput(&'static str),
229}
230
231impl fmt::Display for SolveError {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        match self {
234            SolveError::Graph(e) => e.fmt(f),
235            SolveError::BadInput(what) => write!(f, "CORE_BAD_ARGUMENT: {what}."),
236        }
237    }
238}
239
240impl std::error::Error for SolveError {}
241
242impl From<PoseGraphError> for SolveError {
243    fn from(e: PoseGraphError) -> Self {
244        SolveError::Graph(e)
245    }
246}
247
248/// Per-region tracking state carried from frame to frame.
249struct RegionState {
250    /// Set only when the additional phase rescue actually places a frame. An anchor that already
251    /// recovers the step keeps the legacy path. None is a valid sparse-feature fallback.
252    phase_support: bool,
253    registration_support: Option<Rect>,
254    /// Cells of a moving region that stay on screen while it moves; registration leaves them out.
255    screen_fixed: Option<crate::kernels::track::ScreenFixed>,
256    canvas_id: String,
257    fragment: u32,
258    pose: (f64, f64),
259    /// Last accepted native displacement; a weak prior that breaks ties between period-aliased hypotheses.
260    velocity: (f64, f64),
261    /// The last pose-graph node of this region's chain: its id and frame.
262    last_node: Option<(String, u32)>,
263    anchor: Option<Arc<Keyframe>>,
264    previous_features: Option<Vec<Feature>>,
265    /// The previous frame carried no usable texture in this region.
266    blind: bool,
267    /// The last accepted step had little overlap.
268    weak: bool,
269    /// A textured frame has been seen, so the canvas origin is defined.
270    started: bool,
271}
272
273/// The frame being solved.
274struct FrameWork {
275    index: u32,
276    time: f64,
277    scan: ScanFields,
278    gray: Arc<Plane>,
279    features: Arc<Vec<Feature>>,
280    placements: Vec<Placement>,
281    /// The region being stepped, and its step when it is waiting for keyframes.
282    region: usize,
283    step: Option<Box<region::Step>>,
284    joined_ring: bool,
285}
286
287enum Stage {
288    Idle,
289    AwaitFrame {
290        index: u32,
291        time: f64,
292        scan: ScanFields,
293    },
294    Frame(Box<FrameWork>),
295}
296
297pub struct SolveSession {
298    width: usize,
299    height: usize,
300    factor: u32,
301    radius: i32,
302    analysis: (usize, usize),
303    regions: Vec<SolveRegion>,
304    states: Vec<RegionState>,
305    /// Voting slot of each region (moving regions only).
306    voting_slot: Vec<Option<usize>>,
307    voting: Ring,
308    canvas_slots: HashMap<String, u32>,
309    graph: PoseGraph,
310    index: KeyframeIndex,
311    attachments: Attachments,
312    canvases: HashMap<String, CanvasMeta>,
313    frames: [Vec<u8>; 2],
314    /// The slot holding the previous solved frame, and the slot the next frame is written into.
315    previous_slot: Option<usize>,
316    current_slot: usize,
317    native: Vec<u8>,
318    native_filled: bool,
319    previous_gray: Option<Arc<Plane>>,
320    previous_features: Option<Arc<Vec<Feature>>>,
321    previous_plan: Option<Vec<Placement>>,
322    /// The atlas label plane with every moving region's screen-fixed cells recoded to [`UNREGISTERED`], and the
323    /// plane it was made from; rebuilt when a region's left-out cells change.
324    registration_labels: Option<(Arc<Vec<u8>>, Vec<u8>)>,
325    registration_stale: bool,
326    stage: Stage,
327    effects: Vec<Effect>,
328    /// Test seams that replace a core decision, to reach the recovery paths real input rarely does.
329    #[cfg(test)]
330    seams: tests::Seams,
331}
332
333fn clone_shape(s: &Shape) -> Shape {
334    Shape {
335        rect: s.rect,
336        exclusions: s.exclusions.clone(),
337        crop: s.crop,
338        solid: s.solid,
339        mask: s.mask.as_ref().map(|m| Mask {
340            width: m.width,
341            height: m.height,
342            factor: m.factor,
343            data: m.data.clone(),
344        }),
345    }
346}
347
348/// A feature as it reaches the pass from storage: its score is a 32-bit float.
349fn stored(mut f: Feature) -> Feature {
350    f.score = f.score as f32 as f64;
351    f
352}
353
354impl SolveSession {
355    pub fn new(config: SolveConfig) -> SolveSession {
356        let SolveConfig {
357            width,
358            height,
359            factor,
360            radius,
361            noise,
362            voting_budget,
363            regions,
364        } = config;
365        let factor = factor.max(1);
366        let analysis = downscaled_size(width, height, factor as usize);
367        let mut voting_slot = Vec::with_capacity(regions.len());
368        let mut voting_regions = Vec::new();
369        for r in &regions {
370            if r.moving {
371                voting_slot.push(Some(voting_regions.len()));
372                voting_regions.push(clone_shape(&r.shape));
373            } else {
374                voting_slot.push(None);
375            }
376        }
377        let voting = Ring::new(
378            factor as i32,
379            noise,
380            width,
381            height,
382            analysis.0,
383            analysis.1,
384            voting_budget,
385            voting_regions,
386        );
387        let states = regions
388            .iter()
389            .map(|r| RegionState {
390                phase_support: false,
391                registration_support: None,
392                screen_fixed: r.moving.then(|| {
393                    let rect = r.shape.rect;
394                    let f = factor as f64;
395                    let (x0, y0) = ((rect.x / f).floor(), (rect.y / f).floor());
396                    crate::kernels::track::ScreenFixed::new(
397                        x0 as i64,
398                        y0 as i64,
399                        (((rect.x + rect.width) / f).ceil() - x0).max(0.0) as usize,
400                        (((rect.y + rect.height) / f).ceil() - y0).max(0.0) as usize,
401                    )
402                }),
403                canvas_id: String::new(),
404                fragment: 0,
405                pose: (0.0, 0.0),
406                velocity: (0.0, 0.0),
407                last_node: None,
408                anchor: None,
409                previous_features: None,
410                blind: false,
411                weak: false,
412                started: false,
413            })
414            .collect();
415        SolveSession {
416            width,
417            height,
418            factor,
419            radius,
420            analysis,
421            regions,
422            states,
423            voting_slot,
424            voting,
425            canvas_slots: HashMap::new(),
426            graph: PoseGraph::new(),
427            index: KeyframeIndex::new(),
428            attachments: Attachments::new(),
429            canvases: HashMap::new(),
430            frames: [vec![0; width * height * 4], vec![0; width * height * 4]],
431            previous_slot: None,
432            current_slot: 0,
433            native: vec![0; width * height],
434            native_filled: false,
435            previous_gray: None,
436            previous_features: None,
437            previous_plan: None,
438            registration_labels: None,
439            registration_stale: false,
440            stage: Stage::Idle,
441            effects: Vec::new(),
442            #[cfg(test)]
443            seams: tests::Seams::default(),
444        }
445    }
446
447    pub fn width(&self) -> usize {
448        self.width
449    }
450
451    pub fn height(&self) -> usize {
452        self.height
453    }
454
455    /// Analysis gray size `(width, height)`.
456    pub fn analysis_size(&self) -> (usize, usize) {
457        self.analysis
458    }
459
460    /// The pose graph: the host hydrates it before the first frame and relaxes it after the last.
461    pub fn graph(&self) -> &PoseGraph {
462        &self.graph
463    }
464
465    pub fn graph_mut(&mut self) -> &mut PoseGraph {
466        &mut self.graph
467    }
468
469    /// Hands the pose graph over (to relax it once the frames are done), leaving an empty one.
470    pub fn take_graph(&mut self) -> PoseGraph {
471        std::mem::take(&mut self.graph)
472    }
473
474    /// The effects produced since the last call, in order.
475    pub fn take_effects(&mut self) -> Vec<Effect> {
476        std::mem::take(&mut self.effects)
477    }
478
479    /// Starts frame `index` at `time` with its scan record.
480    pub fn begin_frame(
481        &mut self,
482        index: u32,
483        time: f64,
484        scan: ScanFields,
485    ) -> Result<Begin, SolveError> {
486        if !matches!(self.stage, Stage::Idle) {
487            return Err(SolveError::BadInput("a frame is already in progress"));
488        }
489        if scan.duplicate && self.states.iter().all(|s| !s.blind) {
490            if let Some(previous) = &mut self.previous_plan {
491                for p in previous.iter_mut() {
492                    p.time = time;
493                }
494                let placements = previous.clone();
495                for s in &mut self.states {
496                    s.velocity = (0.0, 0.0);
497                }
498                return Ok(Begin::Duplicate(placements));
499            }
500        }
501        self.current_slot = match self.previous_slot {
502            Some(p) => 1 - p,
503            None => 0,
504        };
505        self.stage = Stage::AwaitFrame { index, time, scan };
506        Ok(Begin::NeedFrame {
507            gray: !(scan.duplicate && self.previous_gray.is_some()),
508        })
509    }
510
511    /// The RGBA slot the frame [`Begin::NeedFrame`] asked for is written into (`width × height × 4` bytes).
512    pub fn frame_mut(&mut self) -> &mut [u8] {
513        &mut self.frames[self.current_slot]
514    }
515
516    /// Solves the frame written into [`Self::frame_mut`]. `gray` is its analysis gray when the host computed one
517    /// (else the session computes it); `features` its stored features, if any. `labels` is the atlas label plane
518    /// (`width × height`).
519    pub fn run_frame(
520        &mut self,
521        gray: Option<Vec<u8>>,
522        features: Option<Vec<Feature>>,
523        labels: &[u8],
524    ) -> Result<Poll, SolveError> {
525        let Stage::AwaitFrame { index, time, scan } =
526            std::mem::replace(&mut self.stage, Stage::Idle)
527        else {
528            return Err(SolveError::BadInput("no frame was asked for"));
529        };
530        if labels.len() != self.width * self.height {
531            return Err(SolveError::BadInput(
532                "the label plane does not match the frame",
533            ));
534        }
535        let (aw, ah) = self.analysis;
536        let gray = match (scan.duplicate, &self.previous_gray, gray) {
537            (true, Some(previous), _) => previous.clone(),
538            (_, _, Some(data)) => {
539                if data.len() != aw * ah {
540                    return Err(SolveError::BadInput(
541                        "analysis gray size differs from the run",
542                    ));
543                }
544                Arc::new(Plane {
545                    width: aw,
546                    height: ah,
547                    data,
548                })
549            }
550            _ => {
551                let mut data = vec![0; aw * ah];
552                downscale_gray(
553                    &self.frames[self.current_slot],
554                    self.width,
555                    self.height,
556                    self.factor as usize,
557                    &mut data,
558                );
559                Arc::new(Plane {
560                    width: aw,
561                    height: ah,
562                    data,
563                })
564            }
565        };
566        let features = match features {
567            Some(f) if !scan.duplicate => Arc::new(f.into_iter().map(stored).collect()),
568            _ => match (scan.duplicate, &self.previous_features) {
569                (true, Some(previous)) => previous.clone(),
570                _ => Arc::new(
571                    extract_features(&gray.data, aw, ah, FALLBACK_FEATURES, None)
572                        .into_iter()
573                        .map(stored)
574                        .collect(),
575                ),
576            },
577        };
578        self.native_filled = false;
579        self.stage = Stage::Frame(Box::new(FrameWork {
580            index,
581            time,
582            scan,
583            gray,
584            features,
585            placements: Vec::with_capacity(self.regions.len()),
586            region: 0,
587            step: None,
588            joined_ring: false,
589        }));
590        self.advance(labels, None)
591    }
592
593    /// Hands in the rows [`Poll::NeedKeyframes`] named, in that order; `None` for a row the store did not have.
594    pub fn provide_keyframes(
595        &mut self,
596        rows: Vec<Option<Keyframe>>,
597        labels: &[u8],
598    ) -> Result<Poll, SolveError> {
599        let waiting = match &self.stage {
600            Stage::Frame(work) => work.step.as_ref().is_some_and(|s| s.waiting()),
601            _ => false,
602        };
603        if !waiting {
604            return Err(SolveError::BadInput("no keyframes were asked for"));
605        }
606        self.advance(labels, Some(rows))
607    }
608
609    /// Steps regions until one waits for keyframes or the frame is done.
610    fn advance(
611        &mut self,
612        labels: &[u8],
613        mut rows: Option<Vec<Option<Keyframe>>>,
614    ) -> Result<Poll, SolveError> {
615        loop {
616            let Stage::Frame(work) = &mut self.stage else {
617                return Err(SolveError::BadInput("no frame in progress"));
618            };
619            if work.region == self.regions.len() {
620                return Ok(Poll::Done(self.finish_frame()));
621            }
622            let i = work.region;
623            let step = work.step.take();
624            let ctx = region::FrameCtx {
625                index: work.index,
626                time: work.time,
627                scan: work.scan,
628                gray: work.gray.clone(),
629                features: work.features.clone(),
630            };
631            let result = match step {
632                None => self.step_region(i, &ctx, labels),
633                Some(step) => self.resume_region(i, &ctx, *step, rows.take().unwrap_or_default()),
634            };
635            let Stage::Frame(work) = &mut self.stage else {
636                unreachable!()
637            };
638            match result {
639                Ok(region::Outcome::Placed(placement, observed)) => {
640                    work.joined_ring |= observed;
641                    work.placements.push(placement);
642                    work.region += 1;
643                }
644                Ok(region::Outcome::Wait(step, ids)) => {
645                    work.step = Some(step);
646                    return Ok(Poll::NeedKeyframes(ids));
647                }
648                Err(e) => {
649                    self.stage = Stage::Idle;
650                    return Err(e);
651                }
652            }
653        }
654    }
655
656    /// Joins the ring, remembers this frame as the previous one, and returns what the frame produced.
657    fn finish_frame(&mut self) -> FrameDone {
658        let Stage::Frame(work) = std::mem::replace(&mut self.stage, Stage::Idle) else {
659            unreachable!()
660        };
661        let FrameWork {
662            index,
663            gray,
664            features,
665            placements,
666            joined_ring,
667            ..
668        } = *work;
669        let voting = if joined_ring {
670            let finalized = self.voting.push_frame(index);
671            self.records(finalized)
672        } else {
673            Vec::new()
674        };
675        self.previous_plan = Some(placements.clone());
676        self.previous_features = Some(features);
677        self.previous_slot = Some(self.current_slot);
678        self.previous_gray = Some(gray);
679        FrameDone {
680            placements,
681            joined_ring,
682            voting,
683        }
684    }
685
686    fn records(&self, finalized: Vec<Finalized>) -> Vec<VotingRecord> {
687        let moving: Vec<&SolveRegion> = self.regions.iter().filter(|r| r.moving).collect();
688        finalized
689            .into_iter()
690            .map(|f| VotingRecord {
691                index: f.index,
692                verdicts: f
693                    .verdicts
694                    .into_iter()
695                    .map(|v| {
696                        let b = self.voting.slots[v.slot].box_;
697                        VotingVerdict {
698                            region: moving[v.slot].id.clone(),
699                            x0: b.x0,
700                            y0: b.y0,
701                            w: b.w,
702                            h: b.h,
703                            bits: v.bits,
704                            clean: v.clean,
705                        }
706                    })
707                    .collect(),
708                voted_layers: f.voted_layers,
709                thin_layers: f.thin_layers,
710            })
711            .collect()
712    }
713
714    /// Finalises every frame still resident in the voting ring (the end of the pass).
715    pub fn drain_voting(&mut self) -> Vec<VotingRecord> {
716        let finalized = self.voting.drain();
717        self.records(finalized)
718    }
719
720    /// Fills the native luma plane from the current frame, once per frame.
721    fn native(&mut self) -> &[u8] {
722        if !self.native_filled {
723            grayscale(&self.frames[self.current_slot], &mut self.native);
724            self.native_filled = true;
725        }
726        &self.native
727    }
728
729    fn canvas_slot(&mut self, canvas_id: &str) -> u32 {
730        let next = self.canvas_slots.len() as u32;
731        *self
732            .canvas_slots
733            .entry(canvas_id.to_string())
734            .or_insert(next)
735    }
736
737    /// Starts region `i`'s canvas `<id>-part-<fragment>` at `time`.
738    fn new_canvas(&mut self, i: usize, time: f64) {
739        let region = &self.regions[i];
740        let state = &mut self.states[i];
741        state.canvas_id = format!("{}-part-{}", region.id, state.fragment);
742        let name = if state.fragment > 0 {
743            format!("{} · 未定位片段 {}", region.name, state.fragment)
744        } else {
745            region.name.clone()
746        };
747        let meta = CanvasMeta {
748            id: state.canvas_id.clone(),
749            layer: region.id.clone(),
750            name,
751            kind: region.kind.clone(),
752            fragment: state.fragment,
753            first_time: time,
754            last_time: time,
755            attached_to: None,
756        };
757        self.canvases.insert(meta.id.clone(), meta.clone());
758        self.effects.push(Effect::Canvas {
759            meta,
760            created: true,
761        });
762    }
763
764    fn diagnostic(&mut self, d: Diagnostic) {
765        self.effects.push(Effect::Diagnostic(d));
766    }
767
768    fn graph_writes(&mut self, writes: Vec<Write>) {
769        self.effects.extend(writes.into_iter().map(Effect::Graph));
770    }
771
772    /// Candidate keyframes of region `i` for a query at `frame`: emits the budget warning when due.
773    fn retrieve(
774        &mut self,
775        i: usize,
776        frame: u32,
777        own: &[Feature],
778        exclude: Option<&str>,
779    ) -> Retrieval {
780        let descriptors: Vec<_> = own.iter().map(|f| f.descriptor).collect();
781        let retrieval =
782            self.index
783                .retrieve(&self.regions[i].id, frame, &descriptors, exclude, None);
784        if retrieval.budget_warning {
785            self.diagnostic(Diagnostic::new(
786                crate::engine::keyframes::BUDGET_DIAGNOSTIC,
787                Severity::Warning,
788            ));
789        }
790        retrieval
791    }
792}
793
794#[cfg(test)]
795mod tests;