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