Skip to main content

long_screen/engine/
render.rs

1//! The render pass (docs/ARCHITECTURE.md §七): every solved placement of every frame resolved to its final canvas
2//! and pose, checked against the frames either side of it, and painted into the tile cache by the [`Compositor`].
3//!
4//! A [`RenderSession`] holds what the pass keeps for its whole length: the region table, the attachment chain, a
5//! three-slot frame ring (previous, current, lookahead), the world-consistency mask, the last-painted pixels of every
6//! fixed region, the last placement of every layer, a four-slot window over solve's voting records, and the canvas
7//! metadata the pass updates. The host drives it one frame at a time:
8//!
9//! 1. [`RenderSession::wanted_votes`] names the `consistency/<frame>` records the window does not hold yet; the host
10//!    reads them and hands them to [`RenderSession::begin`] with the frame's plan and its neighbours' plans.
11//! 2. [`RenderSession::step`] runs the frame's placements in order until it needs something only the host has:
12//!    a canvas's stored metadata ([`RenderStep::NeedMeta`] → [`RenderSession::provide_meta`]), a decoded frame's
13//!    pixels ([`RenderStep::NeedFrame`] → write them into [`RenderSession::frame_slot`]), a tile or a canvas's
14//!    temporal records (the compositor's own needs, passed through), and returns [`RenderStep::Done`] with the
15//!    frame's observation row once every placement is through.
16//! 3. [`RenderSession::take_events`] after every step (also a failed one) returns the diagnostics raised so far,
17//!    in emission order; [`RenderSession::take_dirty_metas`] on the host's flush cadence returns the canvases whose
18//!    metadata changed since the last flush, in first-changed order.
19//!
20//! The session borrows the tile cache, the compositor and the pose graph for each step (a [`RenderIo`]); it owns none
21//! of them, so the host keeps flushing tiles and temporal records exactly as before. It reads no clock: `now` comes
22//! with every step and only stamps the tiles it writes.
23//!
24//! Frames enter the ring only when a placement first needs them, and a new frame takes the slot filled longest ago
25//! among those holding none of the frame in progress and its two neighbours. A placement keeps the slots it was
26//! given while it loads the frames it still lacks, so a slot one of them holds must not be reused. After two frames
27//! that paint nothing, the next frame t loads itself before frame t−1; reusing plainly the slot filled longest ago
28//! then put frame t+2 into frame t's slot while frame t+1 still compared against it, so frame t+1's mask read frame
29//! t+2's pixels in frame t's place.
30
31use crate::engine::attachments::{render_shift, resolve_target, Attachment, Attachments};
32use crate::engine::compositor::{
33    CompositeError, Compositor, CompositorDiagnostic, Frame, Placement, Step as CompositorStep,
34};
35use crate::engine::diagnostic::{Diagnostic, Severity, Value};
36use crate::engine::pose_graph::{PoseGraph, PoseGraphError};
37use crate::engine::tiles::{TileCache, TileKey};
38use crate::kernels::consistency::{consistency_mask, MaskInput, Neighbour, Vote};
39use crate::kernels::geometry::{js_round, Rect};
40use crate::kernels::raster::fixed_update;
41use std::collections::{HashMap, HashSet, VecDeque};
42use std::fmt;
43use std::sync::Arc;
44
45pub use crate::engine::solve::{is_valid_pose, POSE_BOUND};
46
47/// Frames the ring holds: the previous, the current and the lookahead frame.
48pub const RING_FRAMES: usize = 3;
49/// Voting records the window keeps: a frame and both neighbours, plus one, so consecutive frames read one new
50/// record each.
51pub const VOTING_WINDOW: usize = 4;
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum RegionKind {
55    Moving,
56    Fixed,
57    Ignore,
58}
59
60/// One region of the run's atlas.
61#[derive(Clone, Debug, PartialEq)]
62pub struct RenderRegion {
63    pub id: String,
64    pub kind: RegionKind,
65    /// The region's rect on screen.
66    pub rect: Rect,
67    /// Its code in the atlas label plane and how many pixels the atlas gives it.
68    pub code: u8,
69    pub owned_pixels: f64,
70}
71
72/// One placement of a stored plan (`plan/<frame>`), in the row's field order. `skip` and `occlusions` are absent
73/// from the row unless set, and stay absent in the observation row built from it.
74#[derive(Clone, Debug, PartialEq)]
75pub struct PlanPlacement {
76    pub layer: String,
77    pub canvas_id: String,
78    pub node: String,
79    pub x: f64,
80    pub y: f64,
81    pub confidence: f64,
82    pub uncertain: bool,
83    pub time: f64,
84    pub skip: Option<bool>,
85    pub occlusions: Option<Vec<Rect>>,
86}
87
88impl PlanPlacement {
89    fn skipped(&self) -> bool {
90        self.skip == Some(true)
91    }
92}
93
94/// What render reads of a stored plan.
95#[derive(Clone, Debug, Default, PartialEq)]
96pub struct FramePlan {
97    pub placements: Vec<PlanPlacement>,
98    pub duplicate: bool,
99}
100
101/// Solve's displacement-spread verdict for one region of one frame (`consistency/<frame>`, docs/ARCHITECTURE.md §七):
102/// the analysis-resolution box it scored and two bitsets over it, LSB first — cells found inconsistent, cells found
103/// confidently consistent.
104#[derive(Clone, Debug, PartialEq)]
105pub struct ConsistencyVote {
106    pub x0: i32,
107    pub y0: i32,
108    pub w: i32,
109    pub h: i32,
110    pub bits: Vec<u8>,
111    pub clean: Vec<u8>,
112}
113
114/// One frame's `consistency/<frame>` record, by region id.
115pub type ConsistencyRecord = HashMap<String, ConsistencyVote>;
116
117/// The fields of a canvas's stored metadata (`canvas/<id>`) the render pass updates; the host keeps the rest of the
118/// row and writes these back into it.
119#[derive(Clone, Copy, Debug, PartialEq)]
120pub struct CanvasMeta {
121    pub bounds: Rect,
122    pub tile_count: f64,
123    pub observed_pixels: f64,
124    pub uncertain_pixels: f64,
125    pub conflict_pixels: f64,
126    pub provisional_pixels: f64,
127    pub last_time: f64,
128}
129
130/// One placement's line in the observation ledger (`observation/<frame>`): the canvas it targeted, its final
131/// render-time placement, and either its pixel contribution or which shortcut skipped painting it.
132#[derive(Clone, Debug, PartialEq)]
133pub struct RenderDecision {
134    pub canvas_id: String,
135    pub placement: PlanPlacement,
136    pub added_pixels: f64,
137    pub conflict_pixels: f64,
138    pub uncertain_pixels: f64,
139    /// Written as `skipped: true` when set, absent otherwise.
140    pub skipped: bool,
141    /// Written as `reusedExactObservation: true` when set, absent otherwise.
142    pub reused_exact_observation: bool,
143}
144
145/// The `observation/<frame>` row.
146#[derive(Clone, Debug, PartialEq)]
147pub struct Observation {
148    pub frame: u32,
149    pub time: f64,
150    pub decisions: Vec<RenderDecision>,
151}
152
153/// What a finished frame leaves the host: its ledger row, and the canvas of the last moving-region placement it
154/// painted or reused (the progress event's canvas), if any.
155#[derive(Clone, Debug, PartialEq)]
156pub struct FrameOutcome {
157    pub observation: Observation,
158    pub latest: Option<String>,
159}
160
161/// The pass-level counters a step changes, cumulative over the session.
162#[derive(Clone, Copy, Debug, Default, PartialEq)]
163pub struct RenderCounters {
164    /// Placements the duplicate or fixed-region-unchanged shortcut did not paint.
165    pub skipped_paints: u64,
166    /// Tiles first painted.
167    pub tiles: f64,
168    /// Pixels first painted.
169    pub observed_pixels: f64,
170}
171
172#[derive(Clone, Debug, PartialEq)]
173pub enum RenderStep {
174    /// The canvas's stored metadata is needed: [`RenderSession::provide_meta`], then step again.
175    NeedMeta(String),
176    /// The frame's pixels are needed: write them into [`RenderSession::frame_slot`], then step again.
177    NeedFrame(u32),
178    /// The compositor needs this tile resident: make room, insert it, step again.
179    NeedTile(TileKey),
180    /// The compositor needs this canvas's stored temporal records: [`Compositor::load_temporal`], step again.
181    NeedTemporal(String),
182    Done(FrameOutcome),
183}
184
185#[derive(Clone, Debug, PartialEq)]
186pub enum RenderError {
187    /// `begin` while a frame is in progress, or `step` without one.
188    Busy,
189    NoFrame,
190    /// The labels or a frame do not have the session's size.
191    FrameSize,
192    /// The host did not hand over a voting record [`RenderSession::wanted_votes`] asked for.
193    MissingVote(i64),
194    /// A voting record's bitsets are shorter than its box, or its box is empty.
195    BadVote,
196    /// A placement names a layer the region table does not hold.
197    UnknownRegion(String),
198    /// The canvas a placement resolved to has no stored metadata.
199    MissingMeta(String),
200    /// A pose graph lookup failed (a placement's or an attachment's node is missing).
201    Graph(PoseGraphError),
202    /// The compositor refused the placement: its rect leaves the addressable canvas.
203    OutsideCanvas {
204        x: f64,
205        y: f64,
206        region: String,
207    },
208    /// The compositor's temporal records for the canvas were already disposed.
209    Disposed(String),
210    /// Any other compositor failure.
211    Compositor(CompositeError),
212}
213
214impl fmt::Display for RenderError {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        match self {
217            RenderError::Busy => f.write_str("A frame is already being rendered."),
218            RenderError::NoFrame => f.write_str("No frame is being rendered."),
219            RenderError::FrameSize => f.write_str("Frame does not match the run's size."),
220            RenderError::MissingVote(i) => write!(f, "Voting record {i} was not provided."),
221            RenderError::BadVote => f.write_str("CORE_BAD_ARGUMENT: truncated consistency vote bitset."),
222            RenderError::UnknownRegion(id) => write!(f, "Region {id} is not part of this atlas."),
223            RenderError::MissingMeta(_) => f.write_str("Canvas metadata is missing."),
224            RenderError::Graph(e) => e.fmt(f),
225            RenderError::OutsideCanvas { x, y, region } => write!(
226                f,
227                "Placement ({x}, {y}) puts region {region} outside the addressable canvas."
228            ),
229            RenderError::Disposed(canvas) => write!(
230                f,
231                "CORE_BAD_ARGUMENT: temporalIndex({canvas}) called after dispose() already drained and froze its state."
232            ),
233            RenderError::Compositor(e) => write!(f, "Compositor failed: {e:?}."),
234        }
235    }
236}
237
238impl std::error::Error for RenderError {}
239
240impl From<PoseGraphError> for RenderError {
241    fn from(e: PoseGraphError) -> Self {
242        RenderError::Graph(e)
243    }
244}
245
246/// What the session borrows for one step.
247pub struct RenderIo<'a> {
248    pub cache: &'a mut TileCache,
249    pub compositor: &'a mut Compositor,
250    pub graph: &'a PoseGraph,
251    /// The atlas label plane, frame-sized.
252    pub labels: &'a [u8],
253    /// The host's clock, stamped on every tile this step writes.
254    pub now: f64,
255}
256
257/// What the host knows about one frame when it composites it.
258#[derive(Clone, Debug, PartialEq)]
259pub struct FrameInput {
260    pub index: u32,
261    /// The decoded frame's time (the observation row's `time`).
262    pub time: f64,
263    pub plan: FramePlan,
264    /// Whether the previous / next decoded frame is in hand, and its stored plan when there is one.
265    pub has_prev: bool,
266    pub prev_plan: Option<FramePlan>,
267    pub has_next: bool,
268    pub next_plan: Option<FramePlan>,
269}
270
271/// Run-wide constants of a session.
272#[derive(Clone, Debug, PartialEq)]
273pub struct RenderConfig {
274    pub width: usize,
275    pub height: usize,
276    /// Integer analysis factor: analysis pixels × factor = native pixels.
277    pub factor: i32,
278    /// Per-channel decode noise the source declares; the mask's tolerance.
279    pub noise: f64,
280    /// The atlas's regions, in atlas order.
281    pub regions: Vec<RenderRegion>,
282    pub attachments: Vec<Attachment>,
283}
284
285/// A placement resolved to its final canvas and pose.
286#[derive(Clone, Debug, PartialEq)]
287struct Resolved {
288    canvas: String,
289    x: f64,
290    y: f64,
291}
292
293/// One slot of the frame ring: which frame it holds and when that frame entered.
294struct Slot {
295    pixels: Vec<u8>,
296    index: i64,
297    used: u64,
298}
299
300/// Resident frames keyed by frame index. A new frame takes the slot filled longest ago that holds none of the frames
301/// the frame in progress compares (see [`FrameRing::fill`]); reading a frame does not change that order.
302struct FrameRing {
303    slots: Vec<Slot>,
304    tick: u64,
305    bytes: usize,
306}
307
308impl FrameRing {
309    fn new(bytes: usize) -> Self {
310        Self {
311            slots: Vec::with_capacity(RING_FRAMES),
312            tick: 0,
313            bytes,
314        }
315    }
316
317    fn get(&self, index: i64) -> Option<usize> {
318        self.slots.iter().position(|s| s.index == index)
319    }
320
321    /// The slot frame `index` goes into. While a frame is being composited, `keep` is the first and last frame it
322    /// compares (its previous and next): a slot holding one of those is never reused, because a placement reads
323    /// the slots it was given after loading the frames it still lacks. With three slots and three such frames,
324    /// a frame that is not resident always leaves a slot outside `keep`.
325    fn fill(&mut self, index: i64, keep: Option<(i64, i64)>) -> usize {
326        if let Some(slot) = self.get(index) {
327            return slot;
328        }
329        let slot = if self.slots.len() < RING_FRAMES {
330            self.slots.push(Slot {
331                pixels: vec![0; self.bytes],
332                index,
333                used: 0,
334            });
335            self.slots.len() - 1
336        } else {
337            let kept = |s: &Slot| keep.is_some_and(|(a, b)| (a..=b).contains(&s.index));
338            let oldest = |free: bool| {
339                (0..self.slots.len())
340                    .filter(|&i| !free || !kept(&self.slots[i]))
341                    .min_by_key(|&i| self.slots[i].used)
342            };
343            oldest(true).or_else(|| oldest(false)).unwrap()
344        };
345        self.tick += 1;
346        let s = &mut self.slots[slot];
347        s.index = index;
348        s.used = self.tick;
349        slot
350    }
351}
352
353/// The last few frames' voting records, newest last; a missing record is kept as `None` like a present one.
354#[derive(Default)]
355struct VotingWindow {
356    entries: VecDeque<(i64, Option<Arc<ConsistencyRecord>>)>,
357}
358
359impl VotingWindow {
360    /// The indices of `gets`, in order, that the window will not hold when they are asked for (negative indices
361    /// are never stored rows, so they are not wanted).
362    fn wanted(&self, gets: &[i64]) -> Vec<i64> {
363        let mut held: VecDeque<i64> = self.entries.iter().map(|(i, _)| *i).collect();
364        let mut wanted = Vec::new();
365        for &index in gets {
366            if held.contains(&index) {
367                continue;
368            }
369            if index >= 0 {
370                wanted.push(index);
371            }
372            held.push_back(index);
373            if held.len() > VOTING_WINDOW {
374                held.pop_front();
375            }
376        }
377        wanted
378    }
379
380    fn get(
381        &mut self,
382        index: i64,
383        provided: &mut HashMap<i64, Option<ConsistencyRecord>>,
384    ) -> Result<Option<Arc<ConsistencyRecord>>, RenderError> {
385        if let Some((_, record)) = self.entries.iter().find(|(i, _)| *i == index) {
386            return Ok(record.clone());
387        }
388        let record = if index < 0 {
389            None
390        } else {
391            provided
392                .remove(&index)
393                .ok_or(RenderError::MissingVote(index))?
394                .map(Arc::new)
395        };
396        self.entries.push_back((index, record.clone()));
397        if self.entries.len() > VOTING_WINDOW {
398            self.entries.pop_front();
399        }
400        Ok(record)
401    }
402}
403
404/// A neighbour frame's placement for the same layer, resolved, and the ring slot its pixels went into.
405struct NeighbourPlacement {
406    resolved: Resolved,
407    occlusions: Vec<Rect>,
408    slot: Option<usize>,
409}
410
411/// Where the placement in progress is.
412enum Stage {
413    /// Not started: skip, resolve, validate.
414    Start,
415    /// Resolved; its canvas metadata is needed.
416    Meta(Resolved),
417    /// A fixed region whose plan-level shortcut did not apply: compare against the saved copy.
418    Fixed(Resolved),
419    /// Shortcuts decided.
420    Decide(Resolved, bool),
421    /// Painting: the current frame, then (moving regions) both neighbours and the mask, then the compositor.
422    Paint(Paint),
423}
424
425struct Paint {
426    p: Resolved,
427    current: Option<usize>,
428    /// For a moving region: the neighbours, resolved once the current frame is in (`None` until then).
429    neighbours: Option<[Option<NeighbourPlacement>; 2]>,
430    masked: bool,
431    begun: bool,
432}
433
434struct FrameJob {
435    input: FrameInput,
436    votes: [Option<Arc<ConsistencyRecord>>; 3],
437    placement: usize,
438    stage: Stage,
439    decisions: Vec<RenderDecision>,
440    latest: Option<String>,
441}
442
443pub struct RenderSession {
444    width: usize,
445    height: usize,
446    factor: i32,
447    noise: f64,
448    regions: Vec<RenderRegion>,
449    region_index: HashMap<String, usize>,
450    attachments: Attachments,
451    ring: FrameRing,
452    mask: Vec<u8>,
453    fixed: HashMap<String, Vec<u8>>,
454    previous: HashMap<String, Resolved>,
455    votes: VotingWindow,
456    metas: HashMap<String, CanvasMeta>,
457    missing_meta: Option<String>,
458    dirty: Vec<String>,
459    dirty_set: HashSet<String>,
460    counters: RenderCounters,
461    events: Vec<Diagnostic>,
462    job: Option<FrameJob>,
463}
464
465impl RenderSession {
466    pub fn new(config: RenderConfig) -> Self {
467        let mut region_index = HashMap::new();
468        for (i, r) in config.regions.iter().enumerate() {
469            region_index.insert(r.id.clone(), i);
470        }
471        let attachments: Attachments = config
472            .attachments
473            .into_iter()
474            .map(|a| (a.id.clone(), a))
475            .collect();
476        let pixels = config.width * config.height;
477        Self {
478            width: config.width,
479            height: config.height,
480            factor: config.factor,
481            noise: config.noise,
482            regions: config.regions,
483            region_index,
484            attachments,
485            ring: FrameRing::new(pixels * 4),
486            mask: vec![1; pixels],
487            fixed: HashMap::new(),
488            previous: HashMap::new(),
489            votes: VotingWindow::default(),
490            metas: HashMap::new(),
491            missing_meta: None,
492            dirty: Vec::new(),
493            dirty_set: HashSet::new(),
494            counters: RenderCounters::default(),
495            events: Vec::new(),
496            job: None,
497        }
498    }
499
500    /// Sizes the tile cache for this pass: the memory setting less what the pass itself holds (the ring's frames
501    /// and the frame-sized planes around them, each fixed region's saved copy, 8 MB of headroom), then raised to
502    /// what one frame paints. Returns the `MEMORY_BUDGET_RAISED` diagnostic (info) when it had to raise the limit:
503    /// `from`, `to` and `mb` for the message, and a `{ budgetMB, tiles, raisedBy }` detail.
504    pub fn configure_tiles(&self, cache: &mut TileCache, memory_mb: f64) -> Option<Diagnostic> {
505        let fixed_bytes = self
506            .regions
507            .iter()
508            .filter(|r| r.kind == RegionKind::Fixed)
509            .fold(0.0, |n, r| {
510                n + r.rect.width.ceil() * r.rect.height.ceil() * 4.0
511            });
512        let (w, h) = (self.width as f64, self.height as f64);
513        cache.configure_budget(
514            memory_mb,
515            w * h * 5.0 + fixed_bytes + 8.0 * 1024.0 * 1024.0 + w * h * 4.0,
516        );
517        let areas: Vec<(f64, f64)> = self
518            .regions
519            .iter()
520            .filter(|r| r.kind != RegionKind::Ignore)
521            .map(|r| (r.rect.width, r.rect.height))
522            .collect();
523        let raised = cache.ensure_footprint(&areas);
524        if raised == 0 {
525            return None;
526        }
527        let to = cache.max_tiles();
528        let size = cache.size() as f64;
529        Some(
530            Diagnostic::new("MEMORY_BUDGET_RAISED", Severity::Info)
531                .param("from", (to - raised) as f64)
532                .param("to", to as f64)
533                .param(
534                    "mb",
535                    js_round(to as f64 * size * size * 4.3 / 1024.0 / 1024.0) as f64,
536                )
537                .detail(Value::Object(vec![
538                    ("budgetMB", Value::Number(memory_mb)),
539                    ("tiles", Value::Number(to as f64)),
540                    ("raisedBy", Value::Number(raised as f64)),
541                ])),
542        )
543    }
544
545    pub fn counters(&self) -> RenderCounters {
546        self.counters
547    }
548
549    /// The canvas's metadata as the pass holds it now, if it has been loaded.
550    pub fn meta(&self, canvas: &str) -> Option<CanvasMeta> {
551        self.metas.get(canvas).copied()
552    }
553
554    /// Diagnostics raised since the last call, in emission order: `NONFINITE_POSE` (error; a placement's resolved
555    /// pose cannot be placed, it is recorded as skipped at (0, 0)), and the compositor's `INCOMPLETE_TEMPORAL_PATCH`
556    /// and `TEMPORAL_OR_ALIGNMENT_CONFLICT` (warnings; `conflicts` the pixels detected before patching, every
557    /// conflicting rect in `detail.regions`).
558    pub fn take_events(&mut self) -> Vec<Diagnostic> {
559        std::mem::take(&mut self.events)
560    }
561
562    /// Canvases whose metadata changed since the last call, in the order they first changed, with their current
563    /// values.
564    pub fn take_dirty_metas(&mut self) -> Vec<(String, CanvasMeta)> {
565        self.dirty_set.clear();
566        std::mem::take(&mut self.dirty)
567            .into_iter()
568            .map(|id| {
569                let meta = self.metas[&id];
570                (id, meta)
571            })
572            .collect()
573    }
574
575    /// The `consistency/<frame>` records the host should read for frame `index` (in this order) before
576    /// [`RenderSession::begin`]: the frame's own, then its previous and next frame's when those are in hand,
577    /// leaving out what the window still holds.
578    pub fn wanted_votes(&self, index: u32, has_prev: bool, has_next: bool) -> Vec<i64> {
579        self.votes.wanted(&vote_gets(index, has_prev, has_next))
580    }
581
582    /// Starts compositing one frame. `votes` are the records [`RenderSession::wanted_votes`] named, `None` where
583    /// no row is stored.
584    pub fn begin(
585        &mut self,
586        input: FrameInput,
587        votes: Vec<(i64, Option<ConsistencyRecord>)>,
588    ) -> Result<(), RenderError> {
589        if self.job.is_some() {
590            return Err(RenderError::Busy);
591        }
592        let mut provided: HashMap<i64, Option<ConsistencyRecord>> = votes.into_iter().collect();
593        let index = input.index as i64;
594        let own = self.votes.get(index, &mut provided)?;
595        let prev = if input.has_prev {
596            self.votes.get(index - 1, &mut provided)?
597        } else {
598            None
599        };
600        let next = if input.has_next {
601            self.votes.get(index + 1, &mut provided)?
602        } else {
603            None
604        };
605        self.job = Some(FrameJob {
606            input,
607            votes: [own, prev, next],
608            placement: 0,
609            stage: Stage::Start,
610            decisions: Vec::new(),
611            latest: None,
612        });
613        Ok(())
614    }
615
616    /// Abandons the frame in progress (the host failed or stopped between steps), and the compositor's placement
617    /// with it. What the frame already changed (metadata, saved fixed-region pixels, last placements) stays, as it
618    /// did when the pass failed mid-frame before.
619    pub fn cancel(&mut self, compositor: &mut Compositor) {
620        if self.job.take().is_some() {
621            compositor.cancel();
622        }
623        self.missing_meta = None;
624    }
625
626    /// Hands over the metadata of the canvas [`RenderStep::NeedMeta`] asked for; `None` when no row is stored (the
627    /// next step then fails).
628    pub fn provide_meta(&mut self, canvas: &str, meta: Option<CanvasMeta>) {
629        match meta {
630            Some(meta) => {
631                self.metas.insert(canvas.to_string(), meta);
632            }
633            None => self.missing_meta = Some(canvas.to_string()),
634        }
635    }
636
637    /// The ring slot frame `index` goes into, for the host to write its RGBA after [`RenderStep::NeedFrame`]. The
638    /// slot filled longest ago is reused, but never one holding the frame in progress or either of its neighbours.
639    pub fn frame_slot(&mut self, index: u32) -> &mut [u8] {
640        let keep = self.job.as_ref().map(|j| {
641            let at = j.input.index as i64;
642            (at - 1, at + 1)
643        });
644        let slot = self.ring.fill(index as i64, keep);
645        &mut self.ring.slots[slot].pixels
646    }
647
648    /// Runs the frame until it needs something from the host or is done. On an error the frame is abandoned (and
649    /// the compositor's placement cancelled); the session stays usable.
650    pub fn step(&mut self, io: &mut RenderIo) -> Result<RenderStep, RenderError> {
651        let mut job = self.job.take().ok_or(RenderError::NoFrame)?;
652        let result = if io.labels.len() != self.width * self.height {
653            Err(RenderError::FrameSize)
654        } else {
655            self.run(&mut job, io)
656        };
657        match result {
658            Ok(RenderStep::Done(_)) => {}
659            Ok(_) => self.job = Some(job),
660            Err(_) => {
661                if matches!(job.stage, Stage::Paint(Paint { begun: true, .. })) {
662                    io.compositor.cancel();
663                }
664                self.missing_meta = None;
665            }
666        }
667        result
668    }
669
670    fn resolve(
671        &self,
672        raw: &PlanPlacement,
673        frame: f64,
674        graph: &PoseGraph,
675    ) -> Result<Resolved, RenderError> {
676        let (cx, cy) = graph.correction(&raw.node, frame)?;
677        let (sx, sy) = render_shift(&self.attachments, &raw.canvas_id, graph)?;
678        let canvas = resolve_target(&self.attachments, &raw.canvas_id).to_string();
679        Ok(Resolved {
680            canvas,
681            x: raw.x + cx + sx,
682            y: raw.y + cy + sy,
683        })
684    }
685
686    fn region(&self, layer: &str) -> Result<usize, RenderError> {
687        self.region_index
688            .get(layer)
689            .copied()
690            .ok_or_else(|| RenderError::UnknownRegion(layer.to_string()))
691    }
692
693    fn mark_dirty(&mut self, canvas: &str) {
694        if self.dirty_set.insert(canvas.to_string()) {
695            self.dirty.push(canvas.to_string());
696        }
697    }
698
699    fn run(&mut self, job: &mut FrameJob, io: &mut RenderIo) -> Result<RenderStep, RenderError> {
700        loop {
701            let Some(raw) = job.input.plan.placements.get(job.placement) else {
702                return Ok(RenderStep::Done(FrameOutcome {
703                    observation: Observation {
704                        frame: job.input.index,
705                        time: job.input.time,
706                        decisions: std::mem::take(&mut job.decisions),
707                    },
708                    latest: job.latest.take(),
709                }));
710            };
711            let raw = raw.clone();
712            let index = job.input.index;
713            match std::mem::replace(&mut job.stage, Stage::Start) {
714                Stage::Start => {
715                    if raw.skipped() {
716                        job.decisions
717                            .push(decision(raw.canvas_id.clone(), raw, None));
718                        job.placement += 1;
719                        continue;
720                    }
721                    let p = self.resolve(&raw, index as f64, io.graph)?;
722                    if !is_valid_pose(p.x, p.y) {
723                        self.events.push(
724                            Diagnostic::new("NONFINITE_POSE", Severity::Error)
725                                .at(index, raw.time)
726                                .canvas(&p.canvas),
727                        );
728                        let placement = PlanPlacement {
729                            canvas_id: p.canvas.clone(),
730                            x: 0.0,
731                            y: 0.0,
732                            ..raw
733                        };
734                        job.decisions.push(decision(p.canvas, placement, None));
735                        job.placement += 1;
736                        continue;
737                    }
738                    job.stage = Stage::Meta(p);
739                }
740                Stage::Meta(p) => {
741                    if !self.metas.contains_key(&p.canvas) {
742                        if self.missing_meta.take().as_deref() == Some(p.canvas.as_str()) {
743                            return Err(RenderError::MissingMeta(p.canvas));
744                        }
745                        let canvas = p.canvas.clone();
746                        job.stage = Stage::Meta(p);
747                        return Ok(RenderStep::NeedMeta(canvas));
748                    }
749                    let region = &self.regions[self.region(&raw.layer)?];
750                    let unchanged = job.input.plan.duplicate
751                        && self.previous.get(&raw.layer).is_some_and(|last| *last == p);
752                    job.stage = if region.kind == RegionKind::Fixed && !unchanged {
753                        Stage::Fixed(p)
754                    } else {
755                        Stage::Decide(p, unchanged)
756                    };
757                }
758                Stage::Fixed(p) => {
759                    let Some(slot) = self.ring.get(index as i64) else {
760                        job.stage = Stage::Fixed(p);
761                        return Ok(RenderStep::NeedFrame(index));
762                    };
763                    let region = &self.regions[self.region(&raw.layer)?];
764                    let (rw, rh) = (
765                        region.rect.width.ceil() as usize,
766                        region.rect.height.ceil() as usize,
767                    );
768                    let old = self.fixed.contains_key(&region.id);
769                    let saved = self
770                        .fixed
771                        .entry(region.id.clone())
772                        .or_insert_with(|| vec![0; rw * rh * 4]);
773                    let changed = fixed_update(
774                        saved,
775                        &self.ring.slots[slot].pixels,
776                        io.labels,
777                        self.width,
778                        self.height,
779                        region.rect.x.floor() as i64,
780                        region.rect.y.floor() as i64,
781                        rw,
782                        rh,
783                        region.code,
784                    );
785                    job.stage = Stage::Decide(p, old && !changed);
786                }
787                Stage::Decide(p, unchanged) => {
788                    self.previous.insert(raw.layer.clone(), p.clone());
789                    if !unchanged {
790                        job.stage = Stage::Paint(Paint {
791                            p,
792                            current: None,
793                            neighbours: None,
794                            masked: false,
795                            begun: false,
796                        });
797                        continue;
798                    }
799                    let moving = self.regions[self.region(&raw.layer)?].kind == RegionKind::Moving;
800                    self.counters.skipped_paints += 1;
801                    if let Some(meta) = self.metas.get_mut(&p.canvas) {
802                        meta.last_time = raw.time;
803                    }
804                    self.mark_dirty(&p.canvas);
805                    if moving {
806                        job.latest = Some(p.canvas.clone());
807                    }
808                    let placement = placed(&raw, &p);
809                    let mut d = decision(p.canvas, placement, Some((0.0, 0.0, 0.0)));
810                    d.reused_exact_observation = true;
811                    job.decisions.push(d);
812                    job.placement += 1;
813                }
814                Stage::Paint(mut paint) => {
815                    let step = self.paint(job, &raw, &mut paint, io);
816                    match step {
817                        Ok(None) => {
818                            job.placement += 1;
819                        }
820                        Ok(Some(need)) => {
821                            job.stage = Stage::Paint(paint);
822                            return Ok(need);
823                        }
824                        Err(e) => {
825                            job.stage = Stage::Paint(paint);
826                            return Err(e);
827                        }
828                    }
829                }
830            }
831        }
832    }
833
834    /// One painted placement; `Some` is a need to hand to the host, `None` that the placement is finished.
835    fn paint(
836        &mut self,
837        job: &mut FrameJob,
838        raw: &PlanPlacement,
839        paint: &mut Paint,
840        io: &mut RenderIo,
841    ) -> Result<Option<RenderStep>, RenderError> {
842        let index = job.input.index;
843        let region_at = self.region(&raw.layer)?;
844        let current = match paint.current {
845            Some(slot) => slot,
846            None => {
847                let Some(slot) = self.ring.get(index as i64) else {
848                    return Ok(Some(RenderStep::NeedFrame(index)));
849                };
850                paint.current = Some(slot);
851                slot
852            }
853        };
854        let moving = self.regions[region_at].kind == RegionKind::Moving;
855        if moving && !paint.masked {
856            if paint.neighbours.is_none() {
857                paint.neighbours = Some(self.neighbours(job, raw, io.graph)?);
858            }
859            let neighbours = paint.neighbours.as_mut().unwrap();
860            for (side, n) in neighbours.iter_mut().enumerate() {
861                let Some(n) = n else { continue };
862                if n.slot.is_none() {
863                    let at = if side == 0 { index - 1 } else { index + 1 };
864                    match self.ring.get(at as i64) {
865                        Some(slot) => n.slot = Some(slot),
866                        None => return Ok(Some(RenderStep::NeedFrame(at))),
867                    }
868                }
869            }
870            // Loading the next frame left the previous frame's slot alone (see `FrameRing::fill`).
871            debug_assert!(neighbours.iter().enumerate().all(|(side, n)| {
872                n.as_ref().is_none_or(|n| {
873                    self.ring.slots[n.slot.unwrap()].index == index as i64 + 2 * side as i64 - 1
874                })
875            }));
876            let region = &self.regions[region_at];
877            let own = vote(job.votes[0].as_deref(), &region.id)?;
878            let mut sides: [Option<Neighbour>; 2] = [None, None];
879            for (side, n) in neighbours.iter().enumerate() {
880                let Some(n) = n else { continue };
881                if n.resolved.canvas != paint.p.canvas {
882                    continue;
883                }
884                sides[side] = Some(Neighbour {
885                    rgba: &self.ring.slots[n.slot.unwrap()].pixels,
886                    pose_x: n.resolved.x,
887                    pose_y: n.resolved.y,
888                    occlusions: &n.occlusions,
889                    vote: vote(job.votes[side + 1].as_deref(), &region.id)?,
890                });
891            }
892            let [prev, next] = sides;
893            consistency_mask(
894                &MaskInput {
895                    rgba: &self.ring.slots[current].pixels,
896                    labels: io.labels,
897                    width: self.width,
898                    height: self.height,
899                    region: region.rect,
900                    code: region.code,
901                    pose_x: paint.p.x,
902                    pose_y: paint.p.y,
903                    prev,
904                    next,
905                    vote: own,
906                    factor: self.factor,
907                    noise: self.noise,
908                },
909                &mut self.mask,
910            );
911            paint.masked = true;
912        }
913        let region = &self.regions[region_at];
914        if !paint.begun {
915            io.compositor
916                .begin(
917                    io.cache,
918                    Placement {
919                        canvas: paint.p.canvas.clone(),
920                        region: region.rect,
921                        code: region.code,
922                        owned_pixels: region.owned_pixels,
923                        x: paint.p.x,
924                        y: paint.p.y,
925                        frame: index,
926                        time: raw.time,
927                        confidence: raw.confidence,
928                        uncertain: raw.uncertain,
929                        occlusions: raw.occlusions.clone().unwrap_or_default(),
930                    },
931                    self.width,
932                    self.height,
933                )
934                .map_err(|e| match e {
935                    CompositeError::OutsideCanvas => RenderError::OutsideCanvas {
936                        x: paint.p.x,
937                        y: paint.p.y,
938                        region: region.id.clone(),
939                    },
940                    e => RenderError::Compositor(e),
941                })?;
942            paint.begun = true;
943        }
944        debug_assert_eq!(self.ring.slots[current].index, index as i64);
945        let frame = Frame {
946            rgba: &self.ring.slots[current].pixels,
947            width: self.width,
948            height: self.height,
949            labels: io.labels,
950            consistent: moving.then_some(&self.mask[..]),
951        };
952        let outcome = match io.compositor.step(io.cache, &frame, io.now) {
953            Ok(CompositorStep::NeedTile(key)) => return Ok(Some(RenderStep::NeedTile(key))),
954            Ok(CompositorStep::NeedTemporal(canvas)) => {
955                return Ok(Some(RenderStep::NeedTemporal(canvas)))
956            }
957            Ok(CompositorStep::Done(outcome)) => outcome,
958            Err(CompositeError::Disposed) => {
959                return Err(RenderError::Disposed(paint.p.canvas.clone()))
960            }
961            Err(e) => return Err(RenderError::Compositor(e)),
962        };
963        paint.begun = false;
964        self.events
965            .extend(outcome.diagnostics.into_iter().map(compositor_diagnostic));
966        let s = outcome.stats;
967        let canvas = paint.p.canvas.clone();
968        if let Some(meta) = self.metas.get_mut(&canvas) {
969            meta.bounds = union(meta.bounds, s.bounds);
970            meta.observed_pixels += s.added as f64;
971            meta.uncertain_pixels += s.uncertain as f64;
972            meta.conflict_pixels += s.conflicts as f64;
973            meta.tile_count += s.tiles as f64;
974            meta.provisional_pixels += s.provisional_pixels as f64;
975            meta.last_time = raw.time;
976        }
977        self.counters.tiles += s.tiles as f64;
978        self.counters.observed_pixels += s.added as f64;
979        self.mark_dirty(&canvas);
980        if moving {
981            job.latest = Some(canvas.clone());
982        }
983        job.decisions.push(decision(
984            canvas,
985            placed(raw, &paint.p),
986            Some((s.added as f64, s.conflicts as f64, s.uncertain as f64)),
987        ));
988        Ok(None)
989    }
990
991    /// The previous and next frames' placements of the same layer, resolved, when that frame is in hand and placed
992    /// the layer at all; a neighbour whose resolved pose cannot be placed counts as absent.
993    fn neighbours(
994        &self,
995        job: &FrameJob,
996        raw: &PlanPlacement,
997        graph: &PoseGraph,
998    ) -> Result<[Option<NeighbourPlacement>; 2], RenderError> {
999        let input = &job.input;
1000        fn find<'a>(
1001            has: bool,
1002            plan: &'a Option<FramePlan>,
1003            layer: &str,
1004        ) -> Option<&'a PlanPlacement> {
1005            if !has {
1006                return None;
1007            }
1008            plan.as_ref()?
1009                .placements
1010                .iter()
1011                .find(|pl| pl.layer == layer && !pl.skipped())
1012        }
1013        let prev = find(
1014            input.has_prev && input.index > 0,
1015            &input.prev_plan,
1016            &raw.layer,
1017        );
1018        let next = find(input.has_next, &input.next_plan, &raw.layer);
1019        let prev_resolved = match prev {
1020            Some(pl) => Some((self.resolve(pl, input.index as f64 - 1.0, graph)?, pl)),
1021            None => None,
1022        };
1023        let next_resolved = match next {
1024            Some(pl) => Some((self.resolve(pl, input.index as f64 + 1.0, graph)?, pl)),
1025            None => None,
1026        };
1027        let keep = |r: Option<(Resolved, &PlanPlacement)>| {
1028            r.filter(|(p, _)| is_valid_pose(p.x, p.y))
1029                .map(|(resolved, pl)| NeighbourPlacement {
1030                    resolved,
1031                    occlusions: pl.occlusions.clone().unwrap_or_default(),
1032                    slot: None,
1033                })
1034        };
1035        Ok([keep(prev_resolved), keep(next_resolved)])
1036    }
1037}
1038
1039/// One of the compositor's findings as a structured diagnostic.
1040fn compositor_diagnostic(d: CompositorDiagnostic) -> Diagnostic {
1041    match d {
1042        CompositorDiagnostic::IncompletePatch {
1043            canvas,
1044            frame,
1045            time,
1046            region,
1047        } => Diagnostic::new("INCOMPLETE_TEMPORAL_PATCH", Severity::Warning)
1048            .at(frame, time)
1049            .canvas(&canvas)
1050            .region(region),
1051        CompositorDiagnostic::Conflict {
1052            canvas,
1053            frame,
1054            time,
1055            region,
1056            confidence,
1057            regions,
1058            conflicts,
1059        } => Diagnostic::new("TEMPORAL_OR_ALIGNMENT_CONFLICT", Severity::Warning)
1060            .at(frame, time)
1061            .param("conflicts", conflicts as f64)
1062            .canvas(&canvas)
1063            .region(region)
1064            .confidence(confidence)
1065            .detail(Value::Object(vec![(
1066                "regions",
1067                Value::List(regions.into_iter().map(Value::rect).collect()),
1068            )])),
1069    }
1070}
1071
1072/// The voting records a frame asks the window for, in order.
1073fn vote_gets(index: u32, has_prev: bool, has_next: bool) -> Vec<i64> {
1074    let index = index as i64;
1075    let mut gets = vec![index];
1076    if has_prev {
1077        gets.push(index - 1);
1078    }
1079    if has_next {
1080        gets.push(index + 1);
1081    }
1082    gets
1083}
1084
1085/// The region's verdict from a record, checked against its box.
1086fn vote<'a>(
1087    record: Option<&'a ConsistencyRecord>,
1088    region: &str,
1089) -> Result<Option<Vote<'a>>, RenderError> {
1090    let Some(v) = record.and_then(|r| r.get(region)) else {
1091        return Ok(None);
1092    };
1093    if v.w <= 0 || v.h <= 0 {
1094        return Err(RenderError::BadVote);
1095    }
1096    let bytes = (v.w as usize * v.h as usize).div_ceil(8);
1097    if v.bits.len() < bytes || v.clean.len() < bytes {
1098        return Err(RenderError::BadVote);
1099    }
1100    Ok(Some(Vote {
1101        x0: v.x0,
1102        y0: v.y0,
1103        w: v.w,
1104        h: v.h,
1105        bits: &v.bits[..bytes],
1106        clean: &v.clean[..bytes],
1107    }))
1108}
1109
1110/// The plan's placement at its resolved pose and canvas.
1111fn placed(raw: &PlanPlacement, p: &Resolved) -> PlanPlacement {
1112    PlanPlacement {
1113        canvas_id: p.canvas.clone(),
1114        x: p.x,
1115        y: p.y,
1116        ..raw.clone()
1117    }
1118}
1119
1120/// A ledger row; `None` pixels means a skipped placement.
1121fn decision(
1122    canvas_id: String,
1123    placement: PlanPlacement,
1124    pixels: Option<(f64, f64, f64)>,
1125) -> RenderDecision {
1126    let (added, conflicts, uncertain) = pixels.unwrap_or_default();
1127    RenderDecision {
1128        canvas_id,
1129        placement,
1130        added_pixels: added,
1131        conflict_pixels: conflicts,
1132        uncertain_pixels: uncertain,
1133        skipped: pixels.is_none(),
1134        reused_exact_observation: false,
1135    }
1136}
1137
1138/// The smallest rect holding both; an empty `a` is replaced by `b`.
1139fn union(a: Rect, b: Rect) -> Rect {
1140    if a.width <= 0.0 || a.height <= 0.0 {
1141        return b;
1142    }
1143    let (x, y) = (a.x.min(b.x), a.y.min(b.y));
1144    Rect {
1145        x,
1146        y,
1147        width: (a.x + a.width).max(b.x + b.width) - x,
1148        height: (a.y + a.height).max(b.y + b.height) - y,
1149    }
1150}
1151
1152#[cfg(test)]
1153mod tests;