Skip to main content

long_screen/engine/
compositor.rs

1//! Compositing one placed region into the tile cache: the per-tile paint, temporal-conflict resolution against the
2//! per-canvas temporal index, and the diagnostics both raise (docs/ARCHITECTURE.md §七, §十 "合成与瞬态像素").
3//!
4//! One placement is a resumable job. [`Compositor::begin`] plans it; each [`Compositor::step`] runs until it needs
5//! something the host must fetch — a tile that is not resident ([`Step::NeedTile`]: make room with
6//! [`TileCache::victim`] and [`TileCache::evict`], then [`TileCache::insert`] it) or a canvas's stored temporal records
7//! ([`Step::NeedTemporal`]: [`Compositor::load_temporal`]) — and returns [`Step::Done`] with the placement's
8//! statistics and diagnostics once it has finished. The observed frame is passed to every step, never kept.
9//!
10//! Temporal records changed since the last flush come out of [`Compositor::flush`], per canvas in the order the
11//! canvases were first loaded: the ids to delete, then the rows to write.
12
13use crate::engine::tiles::{Tile, TileCache, TileKey, QUALITY_BLOCK};
14use crate::kernels::compositor::{composite_tile, Observation, TileBuffers};
15use crate::kernels::geometry::{js_round, Rect};
16use crate::kernels::temporal::{
17    components, mask_complete, overwrite_tile, DirtyRow, OverwriteTile, TemporalIndex,
18    TemporalRecord,
19};
20
21/// Blocks within ±2^25 of the origin are addressable: a placed rect must stay inside `CANVAS_PIXEL_BOUND` native
22/// pixels on either side (src/pipeline/solve/track.ts derives its pose bound from the same figure).
23pub const CANVAS_PIXEL_BOUND: f64 = (1u64 << 25) as f64 * QUALITY_BLOCK as f64;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum TemporalPolicy {
27    /// Keep the first complete moment of a conflicting area.
28    Stable,
29    /// Replace it with every later complete moment.
30    Latest,
31}
32
33/// One placement of a region on a canvas.
34#[derive(Clone, Debug, PartialEq)]
35pub struct Placement {
36    pub canvas: String,
37    /// The region's rect on screen.
38    pub region: Rect,
39    /// The region's code in the atlas label plane.
40    pub code: u8,
41    /// Pixels the atlas assigns to the region; equal to the rect's area for a rectangular region.
42    pub owned_pixels: f64,
43    /// The region's pose on the canvas (rounded to the raster pose here).
44    pub x: f64,
45    pub y: f64,
46    pub frame: u32,
47    pub time: f64,
48    pub confidence: f64,
49    pub uncertain: bool,
50    pub occlusions: Vec<Rect>,
51    /// A screen-fixed region, placed at the origin in every frame: each consistent pixel takes the latest observation
52    /// and no temporal record is kept (docs/ARCHITECTURE.md §十 "合成与瞬态像素").
53    pub fixed: bool,
54}
55
56/// The observed frame, its atlas label plane and its world-consistency mask (1 = consistent; `None` treats every
57/// pixel as consistent). All three are frame-sized.
58#[derive(Clone, Copy, Debug)]
59pub struct Frame<'a> {
60    pub rgba: &'a [u8],
61    pub width: usize,
62    pub height: usize,
63    pub labels: &'a [u8],
64    pub consistent: Option<&'a [u8]>,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct CompositeStats {
69    pub added: u64,
70    /// Conflicting pixels: those detected per tile plus every pixel a resolved patch rewrote.
71    pub conflicts: u64,
72    pub uncertain: u64,
73    /// Tiles this placement painted first.
74    pub tiles: u64,
75    /// The placed rect in canvas coordinates.
76    pub bounds: Rect,
77    /// Net change in provisional pixels (set minus healed); can be negative.
78    pub provisional_pixels: i64,
79    /// Fixed placements: conflicting blocks that settled into a new state (see `TileStats::settled_blocks`).
80    pub settled_blocks: u64,
81}
82
83impl Default for CompositeStats {
84    fn default() -> Self {
85        Self {
86            added: 0,
87            conflicts: 0,
88            uncertain: 0,
89            tiles: 0,
90            bounds: Rect {
91                x: 0.0,
92                y: 0.0,
93                width: 0.0,
94                height: 0.0,
95            },
96            provisional_pixels: 0,
97            settled_blocks: 0,
98        }
99    }
100}
101
102/// What a placement reports; the host formats the text from the catalogue.
103#[derive(Clone, Debug, PartialEq)]
104pub enum CompositorDiagnostic {
105    /// `INCOMPLETE_TEMPORAL_PATCH` (warning): a conflicting area has no complete consistent moment yet.
106    IncompletePatch {
107        canvas: String,
108        frame: u32,
109        time: f64,
110        region: Rect,
111    },
112    /// `TEMPORAL_OR_ALIGNMENT_CONFLICT` (warning), one per placement with conflicts: `region` is the largest
113    /// conflicting component, `regions` all of them, `conflicts` the pixels detected before patching.
114    Conflict {
115        canvas: String,
116        frame: u32,
117        time: f64,
118        region: Rect,
119        confidence: f64,
120        regions: Vec<Rect>,
121        conflicts: u64,
122    },
123}
124
125#[derive(Clone, Debug, PartialEq)]
126pub struct Outcome {
127    pub stats: CompositeStats,
128    /// In emission order: every incomplete patch, then the conflict summary.
129    pub diagnostics: Vec<CompositorDiagnostic>,
130}
131
132#[derive(Clone, Debug, PartialEq)]
133pub enum Step {
134    /// The tile is not resident: make room, insert it, step again.
135    NeedTile(TileKey),
136    /// The canvas's stored temporal records are needed: load them, step again.
137    NeedTemporal(String),
138    Done(Outcome),
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub enum CompositeError {
143    /// The frame does not have the atlas's size.
144    FrameSize,
145    /// The placed rect leaves the addressable canvas.
146    OutsideCanvas,
147    /// `step` without a placement in progress, or `begin` while one is.
148    NoPlacement,
149    Busy,
150    /// A tile's planes do not have the cache's tile size.
151    BadTile,
152    /// The canvas's temporal records were already disposed.
153    Disposed,
154    /// `load_temporal` for a canvas the placement did not ask for.
155    Unexpected,
156}
157
158/// Temporal state of one canvas, in first-loaded order. `stash` holds what `dispose` drained for the next flush.
159struct CanvasTemporal {
160    canvas: String,
161    index: Option<TemporalIndex>,
162    stash: Option<(Vec<String>, Vec<DirtyRow>)>,
163}
164
165/// Rows a flush writes for one canvas: deletions first, then the changed records with their blocks in persisted
166/// order.
167#[derive(Debug, PartialEq)]
168pub struct TemporalFlush {
169    pub canvas: String,
170    pub deleted: Vec<String>,
171    pub dirty: Vec<DirtyRow>,
172}
173
174#[derive(Clone, Copy, Debug, PartialEq, Eq)]
175enum Phase {
176    Tiles(usize),
177    Component(usize),
178    Patch(usize, usize),
179    Finish(usize),
180    Done,
181}
182
183struct Component {
184    bounds: Rect,
185    blocks: Vec<(i32, i32)>,
186}
187
188/// One tile of a resolved patch: its tile coordinates and the absolute blocks to write on it.
189type PatchTile = ((i32, i32), Vec<(i64, i64)>);
190
191/// One placement in progress.
192struct Job {
193    p: Placement,
194    ox: i64,
195    oy: i64,
196    world: Rect,
197    rectangular: bool,
198    order: Vec<(i32, i32)>,
199    stats: CompositeStats,
200    conflict_blocks: Vec<(i32, i32)>,
201    components: Vec<Component>,
202    /// Tile groups of the patch being written, in first-block order, and whether to report it incomplete.
203    patch: Vec<PatchTile>,
204    incomplete: Option<Rect>,
205    patched: CompositeStats,
206    diagnostics: Vec<CompositorDiagnostic>,
207    phase: Phase,
208}
209
210pub struct Compositor {
211    policy: TemporalPolicy,
212    atlas_width: usize,
213    atlas_height: usize,
214    /// Fresh temporal-record ids, shared by every canvas and never reset.
215    sequence: u64,
216    canvases: Vec<CanvasTemporal>,
217    job: Option<Job>,
218}
219
220impl Compositor {
221    pub fn new(policy: TemporalPolicy, atlas_width: usize, atlas_height: usize) -> Self {
222        Self {
223            policy,
224            atlas_width,
225            atlas_height,
226            sequence: 0,
227            canvases: Vec::new(),
228            job: None,
229        }
230    }
231
232    /// Plans one placement: its world rect and tile order (resident tiles first, so cold loads only evict the
233    /// oldest residents). Tiles stay untouched until the first [`Compositor::step`].
234    pub fn begin(
235        &mut self,
236        cache: &TileCache,
237        p: Placement,
238        frame_width: usize,
239        frame_height: usize,
240    ) -> Result<(), CompositeError> {
241        if self.job.is_some() {
242            return Err(CompositeError::Busy);
243        }
244        if frame_width != self.atlas_width || frame_height != self.atlas_height {
245            return Err(CompositeError::FrameSize);
246        }
247        if !p.x.is_finite() || !p.y.is_finite() {
248            return Err(CompositeError::OutsideCanvas);
249        }
250        let (ox, oy) = (js_round(p.x) as i64, js_round(p.y) as i64);
251        let world = Rect {
252            x: p.region.x + ox as f64,
253            y: p.region.y + oy as f64,
254            width: p.region.width,
255            height: p.region.height,
256        };
257        let bound = CANVAS_PIXEL_BOUND;
258        if !world.x.is_finite()
259            || !world.y.is_finite()
260            || world.x.abs() >= bound
261            || world.y.abs() >= bound
262            || (world.x + world.width).abs() >= bound
263            || (world.y + world.height).abs() >= bound
264        {
265            return Err(CompositeError::OutsideCanvas);
266        }
267        let r = p.region;
268        let rectangular = [r.x, r.y, r.width, r.height]
269            .iter()
270            .all(|v| v.fract() == 0.0)
271            && p.owned_pixels == r.width * r.height;
272        let size = cache.size() as f64;
273        let (x0, x1) = (
274            (world.x / size).floor() as i32,
275            ((world.x + world.width - 1.0) / size).floor() as i32,
276        );
277        let (y0, y1) = (
278            (world.y / size).floor() as i32,
279            ((world.y + world.height - 1.0) / size).floor() as i32,
280        );
281        let mut resident = Vec::new();
282        let mut cold = Vec::new();
283        for ty in y0..=y1 {
284            for tx in x0..=x1 {
285                if cache.is_resident(&TileKey::new(&p.canvas, 0, tx, ty)) {
286                    resident.push((tx, ty));
287                } else {
288                    cold.push((tx, ty));
289                }
290            }
291        }
292        resident.extend(cold);
293        self.job = Some(Job {
294            ox,
295            oy,
296            world,
297            rectangular,
298            order: resident,
299            stats: CompositeStats {
300                bounds: world,
301                ..CompositeStats::default()
302            },
303            conflict_blocks: Vec::new(),
304            components: Vec::new(),
305            patch: Vec::new(),
306            incomplete: None,
307            patched: CompositeStats::default(),
308            diagnostics: Vec::new(),
309            phase: Phase::Tiles(0),
310            p,
311        });
312        Ok(())
313    }
314
315    /// Abandons the placement in progress (the host stopped or failed between steps).
316    pub fn cancel(&mut self) {
317        self.job = None;
318    }
319
320    /// Runs the placement until it needs a tile or temporal records, or is done. `now` is the host's clock, stamped
321    /// on every tile this step writes.
322    pub fn step(
323        &mut self,
324        cache: &mut TileCache,
325        frame: &Frame,
326        now: f64,
327    ) -> Result<Step, CompositeError> {
328        let mut job = self.job.take().ok_or(CompositeError::NoPlacement)?;
329        let result = self.run(&mut job, cache, frame, now);
330        match result {
331            Ok(Step::Done(_)) | Err(_) => {}
332            Ok(_) => self.job = Some(job),
333        }
334        result
335    }
336
337    fn run(
338        &mut self,
339        job: &mut Job,
340        cache: &mut TileCache,
341        frame: &Frame,
342        now: f64,
343    ) -> Result<Step, CompositeError> {
344        if frame.width != self.atlas_width || frame.height != self.atlas_height {
345            return Err(CompositeError::FrameSize);
346        }
347        let size = cache.size() as i32;
348        let per_tile = size / QUALITY_BLOCK as i32;
349        loop {
350            match job.phase {
351                Phase::Tiles(i) if i < job.order.len() => {
352                    let (tx, ty) = job.order[i];
353                    let key = TileKey::new(&job.p.canvas, 0, tx, ty);
354                    let Some(id) = cache.touch(&key) else {
355                        return Ok(Step::NeedTile(key));
356                    };
357                    let tile = cache.get_mut(id).unwrap();
358                    let was_new = !tile.existed && tile.coverage.iter().all(|&b| b == 0);
359                    let obs = Observation {
360                        rgba: frame.rgba,
361                        width: frame.width,
362                        height: frame.height,
363                        labels: (!job.rectangular).then_some((frame.labels, job.p.code)),
364                        occlusions: &job.p.occlusions,
365                        consistent: frame.consistent,
366                        confidence: job.p.confidence,
367                        uncertain: job.p.uncertain,
368                        frame: job.p.frame,
369                        latest: job.p.fixed,
370                    };
371                    let stats = {
372                        let mut buffers = tile_buffers(tile, cache_size(size))?;
373                        composite_tile(
374                            &mut buffers,
375                            &obs,
376                            job.world,
377                            job.ox,
378                            job.oy,
379                            tx as i64,
380                            ty as i64,
381                        )
382                    };
383                    job.stats.added += stats.added as u64;
384                    job.stats.conflicts += stats.conflicts as u64;
385                    job.stats.uncertain += stats.uncertain as u64;
386                    job.stats.provisional_pixels += stats.provisional_delta as i64;
387                    job.stats.settled_blocks += stats.settled_blocks as u64;
388                    for (bx, by) in stats.conflict_blocks {
389                        job.conflict_blocks
390                            .push((tx * per_tile + bx as i32, ty * per_tile + by as i32));
391                    }
392                    if stats.changed {
393                        cache.mark_dirty(id, now);
394                        if was_new {
395                            job.stats.tiles += 1;
396                        }
397                    }
398                    job.phase = Phase::Tiles(i + 1);
399                }
400                Phase::Tiles(_) => {
401                    if job.conflict_blocks.is_empty() {
402                        job.phase = Phase::Done;
403                        continue;
404                    }
405                    // Seed order is tile-then-local-block raster order, whatever order the tiles were painted in
406                    // (resident first): component order must not depend on the cache.
407                    let mut cells = std::mem::take(&mut job.conflict_blocks);
408                    cells.sort_by_key(|&(bx, by)| {
409                        let (tx, ty) = (bx.div_euclid(per_tile), by.div_euclid(per_tile));
410                        (ty, tx, by - ty * per_tile, bx - tx * per_tile)
411                    });
412                    cells.dedup();
413                    job.components = components(&cells, QUALITY_BLOCK as i32)
414                        .into_iter()
415                        .map(|c| Component {
416                            bounds: Rect {
417                                x: c.x0 as f64,
418                                y: c.y0 as f64,
419                                width: c.width as f64,
420                                height: c.height as f64,
421                            },
422                            blocks: c.blocks,
423                        })
424                        .collect();
425                    job.phase = Phase::Component(0);
426                }
427                Phase::Component(c) if c < job.components.len() && job.p.fixed => {
428                    // Conflicts on a fixed canvas are still detected and reported, but no moment is chosen for them:
429                    // the per-pixel writes above already took every consistent pixel of this frame.
430                    job.patch.clear();
431                    job.incomplete = None;
432                    job.phase = Phase::Patch(c, 0);
433                }
434                Phase::Component(c) if c < job.components.len() => {
435                    let canvas = &job.p.canvas;
436                    let Some(entry) = self.canvases.iter_mut().find(|e| &e.canvas == canvas) else {
437                        return Ok(Step::NeedTemporal(canvas.clone()));
438                    };
439                    let index = entry.index.as_mut().ok_or(CompositeError::Disposed)?;
440                    let comp = &job.components[c];
441                    let (count, next) = index.decide(
442                        comp.bounds,
443                        &comp.blocks,
444                        job.p.frame,
445                        job.p.time,
446                        job.world,
447                        self.policy == TemporalPolicy::Latest,
448                        self.sequence,
449                    );
450                    self.sequence = next;
451                    let write_blocks = index.pending_write_blocks().to_vec();
452                    let mask_ok = count > 0
453                        && mask_complete(
454                            &write_blocks,
455                            frame.labels,
456                            self.atlas_width as i64,
457                            self.atlas_height as i64,
458                            job.p.code,
459                            &job.p.occlusions,
460                            frame.consistent,
461                            job.ox,
462                            job.oy,
463                        );
464                    let commit = index.commit(mask_ok).unwrap();
465                    job.patch.clear();
466                    if commit.chosen {
467                        for &(bx, by) in &write_blocks {
468                            let b = QUALITY_BLOCK as i32;
469                            let t = ((bx * b).div_euclid(size), (by * b).div_euclid(size));
470                            match job.patch.iter_mut().find(|(k, _)| *k == t) {
471                                Some((_, v)) => v.push((bx as i64, by as i64)),
472                                None => job.patch.push((t, vec![(bx as i64, by as i64)])),
473                            }
474                        }
475                    }
476                    job.incomplete = commit.emit_incomplete.then_some(commit.rect);
477                    job.phase = Phase::Patch(c, 0);
478                }
479                Phase::Component(_) => {
480                    let mut regions: Vec<Rect> = job.components.iter().map(|c| c.bounds).collect();
481                    let region = regions
482                        .iter()
483                        .copied()
484                        .reduce(|a, b| {
485                            if b.width * b.height > a.width * a.height {
486                                b
487                            } else {
488                                a
489                            }
490                        })
491                        .unwrap();
492                    job.diagnostics.push(CompositorDiagnostic::Conflict {
493                        canvas: job.p.canvas.clone(),
494                        frame: job.p.frame,
495                        time: job.p.time,
496                        region,
497                        confidence: job.p.confidence,
498                        regions: std::mem::take(&mut regions),
499                        conflicts: job.stats.conflicts,
500                    });
501                    job.stats.added += job.patched.added;
502                    job.stats.tiles += job.patched.tiles;
503                    job.stats.conflicts += job.patched.conflicts;
504                    job.stats.provisional_pixels += job.patched.provisional_pixels;
505                    job.phase = Phase::Done;
506                }
507                Phase::Patch(c, g) if g < job.patch.len() => {
508                    let ((tx, ty), ref blocks) = job.patch[g];
509                    let key = TileKey::new(&job.p.canvas, 0, tx, ty);
510                    let Some(id) = cache.touch(&key) else {
511                        return Ok(Step::NeedTile(key));
512                    };
513                    let tile = cache.get_mut(id).unwrap();
514                    check_tile(tile, cache_size(size))?;
515                    let stats = overwrite_tile(
516                        &mut OverwriteTile {
517                            size: size as usize,
518                            pixels: &mut tile.pixels,
519                            coverage: &mut tile.coverage,
520                            provisional: &mut tile.provisional,
521                            quality: &mut tile.quality,
522                            conflicts: &mut tile.conflicts,
523                            owner: &mut tile.owner,
524                            frozen: &mut tile.frozen,
525                        },
526                        frame.rgba,
527                        frame.width as i64,
528                        blocks,
529                        job.ox,
530                        job.oy,
531                        tx as i64,
532                        ty as i64,
533                        job.p.frame,
534                        job.p.confidence,
535                        self.policy == TemporalPolicy::Stable,
536                    );
537                    job.patched.added += stats.added as u64;
538                    job.patched.conflicts += stats.conflict_pixels as u64;
539                    job.patched.provisional_pixels += stats.provisional_delta as i64;
540                    if stats.changed {
541                        cache.mark_dirty(id, now);
542                    }
543                    job.phase = Phase::Patch(c, g + 1);
544                }
545                Phase::Patch(c, _) => job.phase = Phase::Finish(c),
546                Phase::Finish(c) => {
547                    if let Some(region) = job.incomplete.take() {
548                        job.diagnostics.push(CompositorDiagnostic::IncompletePatch {
549                            canvas: job.p.canvas.clone(),
550                            frame: job.p.frame,
551                            time: job.p.time,
552                            region,
553                        });
554                    }
555                    job.phase = Phase::Component(c + 1);
556                }
557                Phase::Done => {
558                    return Ok(Step::Done(Outcome {
559                        stats: job.stats,
560                        diagnostics: std::mem::take(&mut job.diagnostics),
561                    }));
562                }
563            }
564        }
565    }
566
567    /// Seeds `canvas`'s temporal index from its stored records (ascending id order) the first time a placement
568    /// asks for it; afterwards the index is authoritative.
569    pub fn load_temporal(
570        &mut self,
571        canvas: &str,
572        records: Vec<TemporalRecord>,
573    ) -> Result<(), CompositeError> {
574        if self.canvases.iter().any(|e| e.canvas == canvas) {
575            return Err(if self.disposed(canvas) {
576                CompositeError::Disposed
577            } else {
578                CompositeError::Unexpected
579            });
580        }
581        let mut index = TemporalIndex::new();
582        for record in records {
583            index.load(record);
584        }
585        self.canvases.push(CanvasTemporal {
586            canvas: canvas.to_string(),
587            index: Some(index),
588            stash: None,
589        });
590        Ok(())
591    }
592
593    fn disposed(&self, canvas: &str) -> bool {
594        self.canvases
595            .iter()
596            .any(|e| e.canvas == canvas && e.index.is_none())
597    }
598
599    /// Temporal records changed since the last flush, per canvas in first-loaded order.
600    pub fn flush(&mut self) -> Vec<TemporalFlush> {
601        self.canvases
602            .iter_mut()
603            .map(|e| {
604                let (deleted, dirty) = match e.index.as_mut() {
605                    Some(index) => index.flush_take(),
606                    None => e.stash.take().unwrap_or_default(),
607                };
608                TemporalFlush {
609                    canvas: e.canvas.clone(),
610                    deleted,
611                    dirty,
612                }
613            })
614            .collect()
615    }
616
617    /// Releases every canvas's index, keeping what the next [`Compositor::flush`] still owes storage. A placement
618    /// on a disposed canvas fails with [`CompositeError::Disposed`].
619    pub fn dispose(&mut self) {
620        for e in &mut self.canvases {
621            if let Some(mut index) = e.index.take() {
622                e.stash = Some(index.flush_take());
623            }
624        }
625    }
626}
627
628/// `size` as the tile side in pixels.
629fn cache_size(size: i32) -> usize {
630    size as usize
631}
632
633fn check_tile(tile: &Tile, size: usize) -> Result<(), CompositeError> {
634    let n = size * size;
635    let blocks = (size / QUALITY_BLOCK) * (size / QUALITY_BLOCK);
636    let ok = tile.pixels.len() == n * 4
637        && tile.coverage.len() == n.div_ceil(8)
638        && tile.provisional.len() == n.div_ceil(8)
639        && [
640            tile.quality.len(),
641            tile.conflicts.len(),
642            tile.owner.len(),
643            tile.score.len(),
644            tile.frozen.len(),
645        ]
646        .iter()
647        .all(|&l| l == blocks);
648    if ok {
649        Ok(())
650    } else {
651        Err(CompositeError::BadTile)
652    }
653}
654
655fn tile_buffers(tile: &mut Tile, size: usize) -> Result<TileBuffers<'_>, CompositeError> {
656    check_tile(tile, size)?;
657    Ok(TileBuffers {
658        size,
659        pixels: &mut tile.pixels,
660        coverage: &mut tile.coverage,
661        provisional: &mut tile.provisional,
662        quality: &mut tile.quality,
663        conflicts: &mut tile.conflicts,
664        owner: &mut tile.owner,
665        score: &mut tile.score,
666        frozen: &tile.frozen,
667    })
668}