1use 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
21pub 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 Stable,
29 Latest,
31}
32
33#[derive(Clone, Debug, PartialEq)]
35pub struct Placement {
36 pub canvas: String,
37 pub region: Rect,
39 pub code: u8,
41 pub owned_pixels: f64,
43 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}
52
53#[derive(Clone, Copy, Debug)]
56pub struct Frame<'a> {
57 pub rgba: &'a [u8],
58 pub width: usize,
59 pub height: usize,
60 pub labels: &'a [u8],
61 pub consistent: Option<&'a [u8]>,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub struct CompositeStats {
66 pub added: u64,
67 pub conflicts: u64,
69 pub uncertain: u64,
70 pub tiles: u64,
72 pub bounds: Rect,
74 pub provisional_pixels: i64,
76}
77
78impl Default for CompositeStats {
79 fn default() -> Self {
80 Self {
81 added: 0,
82 conflicts: 0,
83 uncertain: 0,
84 tiles: 0,
85 bounds: Rect {
86 x: 0.0,
87 y: 0.0,
88 width: 0.0,
89 height: 0.0,
90 },
91 provisional_pixels: 0,
92 }
93 }
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub enum CompositorDiagnostic {
99 IncompletePatch {
101 canvas: String,
102 frame: u32,
103 time: f64,
104 region: Rect,
105 },
106 Conflict {
109 canvas: String,
110 frame: u32,
111 time: f64,
112 region: Rect,
113 confidence: f64,
114 regions: Vec<Rect>,
115 conflicts: u64,
116 },
117}
118
119#[derive(Clone, Debug, PartialEq)]
120pub struct Outcome {
121 pub stats: CompositeStats,
122 pub diagnostics: Vec<CompositorDiagnostic>,
124}
125
126#[derive(Clone, Debug, PartialEq)]
127pub enum Step {
128 NeedTile(TileKey),
130 NeedTemporal(String),
132 Done(Outcome),
133}
134
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub enum CompositeError {
137 FrameSize,
139 OutsideCanvas,
141 NoPlacement,
143 Busy,
144 BadTile,
146 Disposed,
148 Unexpected,
150}
151
152struct CanvasTemporal {
154 canvas: String,
155 index: Option<TemporalIndex>,
156 stash: Option<(Vec<String>, Vec<DirtyRow>)>,
157}
158
159#[derive(Debug, PartialEq)]
162pub struct TemporalFlush {
163 pub canvas: String,
164 pub deleted: Vec<String>,
165 pub dirty: Vec<DirtyRow>,
166}
167
168#[derive(Clone, Copy, Debug, PartialEq, Eq)]
169enum Phase {
170 Tiles(usize),
171 Component(usize),
172 Patch(usize, usize),
173 Finish(usize),
174 Done,
175}
176
177struct Component {
178 bounds: Rect,
179 blocks: Vec<(i32, i32)>,
180}
181
182type PatchTile = ((i32, i32), Vec<(i64, i64)>);
184
185struct Job {
187 p: Placement,
188 ox: i64,
189 oy: i64,
190 world: Rect,
191 rectangular: bool,
192 order: Vec<(i32, i32)>,
193 stats: CompositeStats,
194 conflict_blocks: Vec<(i32, i32)>,
195 components: Vec<Component>,
196 patch: Vec<PatchTile>,
198 incomplete: Option<Rect>,
199 patched: CompositeStats,
200 diagnostics: Vec<CompositorDiagnostic>,
201 phase: Phase,
202}
203
204pub struct Compositor {
205 policy: TemporalPolicy,
206 atlas_width: usize,
207 atlas_height: usize,
208 sequence: u64,
210 canvases: Vec<CanvasTemporal>,
211 job: Option<Job>,
212}
213
214impl Compositor {
215 pub fn new(policy: TemporalPolicy, atlas_width: usize, atlas_height: usize) -> Self {
216 Self {
217 policy,
218 atlas_width,
219 atlas_height,
220 sequence: 0,
221 canvases: Vec::new(),
222 job: None,
223 }
224 }
225
226 pub fn begin(
229 &mut self,
230 cache: &TileCache,
231 p: Placement,
232 frame_width: usize,
233 frame_height: usize,
234 ) -> Result<(), CompositeError> {
235 if self.job.is_some() {
236 return Err(CompositeError::Busy);
237 }
238 if frame_width != self.atlas_width || frame_height != self.atlas_height {
239 return Err(CompositeError::FrameSize);
240 }
241 if !p.x.is_finite() || !p.y.is_finite() {
242 return Err(CompositeError::OutsideCanvas);
243 }
244 let (ox, oy) = (js_round(p.x) as i64, js_round(p.y) as i64);
245 let world = Rect {
246 x: p.region.x + ox as f64,
247 y: p.region.y + oy as f64,
248 width: p.region.width,
249 height: p.region.height,
250 };
251 let bound = CANVAS_PIXEL_BOUND;
252 if !world.x.is_finite()
253 || !world.y.is_finite()
254 || world.x.abs() >= bound
255 || world.y.abs() >= bound
256 || (world.x + world.width).abs() >= bound
257 || (world.y + world.height).abs() >= bound
258 {
259 return Err(CompositeError::OutsideCanvas);
260 }
261 let r = p.region;
262 let rectangular = [r.x, r.y, r.width, r.height]
263 .iter()
264 .all(|v| v.fract() == 0.0)
265 && p.owned_pixels == r.width * r.height;
266 let size = cache.size() as f64;
267 let (x0, x1) = (
268 (world.x / size).floor() as i32,
269 ((world.x + world.width - 1.0) / size).floor() as i32,
270 );
271 let (y0, y1) = (
272 (world.y / size).floor() as i32,
273 ((world.y + world.height - 1.0) / size).floor() as i32,
274 );
275 let mut resident = Vec::new();
276 let mut cold = Vec::new();
277 for ty in y0..=y1 {
278 for tx in x0..=x1 {
279 if cache.is_resident(&TileKey::new(&p.canvas, 0, tx, ty)) {
280 resident.push((tx, ty));
281 } else {
282 cold.push((tx, ty));
283 }
284 }
285 }
286 resident.extend(cold);
287 self.job = Some(Job {
288 ox,
289 oy,
290 world,
291 rectangular,
292 order: resident,
293 stats: CompositeStats {
294 bounds: world,
295 ..CompositeStats::default()
296 },
297 conflict_blocks: Vec::new(),
298 components: Vec::new(),
299 patch: Vec::new(),
300 incomplete: None,
301 patched: CompositeStats::default(),
302 diagnostics: Vec::new(),
303 phase: Phase::Tiles(0),
304 p,
305 });
306 Ok(())
307 }
308
309 pub fn cancel(&mut self) {
311 self.job = None;
312 }
313
314 pub fn step(
317 &mut self,
318 cache: &mut TileCache,
319 frame: &Frame,
320 now: f64,
321 ) -> Result<Step, CompositeError> {
322 let mut job = self.job.take().ok_or(CompositeError::NoPlacement)?;
323 let result = self.run(&mut job, cache, frame, now);
324 match result {
325 Ok(Step::Done(_)) | Err(_) => {}
326 Ok(_) => self.job = Some(job),
327 }
328 result
329 }
330
331 fn run(
332 &mut self,
333 job: &mut Job,
334 cache: &mut TileCache,
335 frame: &Frame,
336 now: f64,
337 ) -> Result<Step, CompositeError> {
338 if frame.width != self.atlas_width || frame.height != self.atlas_height {
339 return Err(CompositeError::FrameSize);
340 }
341 let size = cache.size() as i32;
342 let per_tile = size / QUALITY_BLOCK as i32;
343 loop {
344 match job.phase {
345 Phase::Tiles(i) if i < job.order.len() => {
346 let (tx, ty) = job.order[i];
347 let key = TileKey::new(&job.p.canvas, 0, tx, ty);
348 let Some(id) = cache.touch(&key) else {
349 return Ok(Step::NeedTile(key));
350 };
351 let tile = cache.get_mut(id).unwrap();
352 let was_new = !tile.existed && tile.coverage.iter().all(|&b| b == 0);
353 let obs = Observation {
354 rgba: frame.rgba,
355 width: frame.width,
356 height: frame.height,
357 labels: (!job.rectangular).then_some((frame.labels, job.p.code)),
358 occlusions: &job.p.occlusions,
359 consistent: frame.consistent,
360 confidence: job.p.confidence,
361 uncertain: job.p.uncertain,
362 frame: job.p.frame,
363 };
364 let stats = {
365 let mut buffers = tile_buffers(tile, cache_size(size))?;
366 composite_tile(
367 &mut buffers,
368 &obs,
369 job.world,
370 job.ox,
371 job.oy,
372 tx as i64,
373 ty as i64,
374 )
375 };
376 job.stats.added += stats.added as u64;
377 job.stats.conflicts += stats.conflicts as u64;
378 job.stats.uncertain += stats.uncertain as u64;
379 job.stats.provisional_pixels += stats.provisional_delta as i64;
380 for (bx, by) in stats.conflict_blocks {
381 job.conflict_blocks
382 .push((tx * per_tile + bx as i32, ty * per_tile + by as i32));
383 }
384 if stats.changed {
385 cache.mark_dirty(id, now);
386 if was_new {
387 job.stats.tiles += 1;
388 }
389 }
390 job.phase = Phase::Tiles(i + 1);
391 }
392 Phase::Tiles(_) => {
393 if job.conflict_blocks.is_empty() {
394 job.phase = Phase::Done;
395 continue;
396 }
397 let mut cells = std::mem::take(&mut job.conflict_blocks);
400 cells.sort_by_key(|&(bx, by)| {
401 let (tx, ty) = (bx.div_euclid(per_tile), by.div_euclid(per_tile));
402 (ty, tx, by - ty * per_tile, bx - tx * per_tile)
403 });
404 cells.dedup();
405 job.components = components(&cells, QUALITY_BLOCK as i32)
406 .into_iter()
407 .map(|c| Component {
408 bounds: Rect {
409 x: c.x0 as f64,
410 y: c.y0 as f64,
411 width: c.width as f64,
412 height: c.height as f64,
413 },
414 blocks: c.blocks,
415 })
416 .collect();
417 job.phase = Phase::Component(0);
418 }
419 Phase::Component(c) if c < job.components.len() => {
420 let canvas = &job.p.canvas;
421 let Some(entry) = self.canvases.iter_mut().find(|e| &e.canvas == canvas) else {
422 return Ok(Step::NeedTemporal(canvas.clone()));
423 };
424 let index = entry.index.as_mut().ok_or(CompositeError::Disposed)?;
425 let comp = &job.components[c];
426 let (count, next) = index.decide(
427 comp.bounds,
428 &comp.blocks,
429 job.p.frame,
430 job.p.time,
431 job.world,
432 self.policy == TemporalPolicy::Latest,
433 self.sequence,
434 );
435 self.sequence = next;
436 let write_blocks = index.pending_write_blocks().to_vec();
437 let mask_ok = count > 0
438 && mask_complete(
439 &write_blocks,
440 frame.labels,
441 self.atlas_width as i64,
442 self.atlas_height as i64,
443 job.p.code,
444 &job.p.occlusions,
445 frame.consistent,
446 job.ox,
447 job.oy,
448 );
449 let commit = index.commit(mask_ok).unwrap();
450 job.patch.clear();
451 if commit.chosen {
452 for &(bx, by) in &write_blocks {
453 let b = QUALITY_BLOCK as i32;
454 let t = ((bx * b).div_euclid(size), (by * b).div_euclid(size));
455 match job.patch.iter_mut().find(|(k, _)| *k == t) {
456 Some((_, v)) => v.push((bx as i64, by as i64)),
457 None => job.patch.push((t, vec![(bx as i64, by as i64)])),
458 }
459 }
460 }
461 job.incomplete = commit.emit_incomplete.then_some(commit.rect);
462 job.phase = Phase::Patch(c, 0);
463 }
464 Phase::Component(_) => {
465 let mut regions: Vec<Rect> = job.components.iter().map(|c| c.bounds).collect();
466 let region = regions
467 .iter()
468 .copied()
469 .reduce(|a, b| {
470 if b.width * b.height > a.width * a.height {
471 b
472 } else {
473 a
474 }
475 })
476 .unwrap();
477 job.diagnostics.push(CompositorDiagnostic::Conflict {
478 canvas: job.p.canvas.clone(),
479 frame: job.p.frame,
480 time: job.p.time,
481 region,
482 confidence: job.p.confidence,
483 regions: std::mem::take(&mut regions),
484 conflicts: job.stats.conflicts,
485 });
486 job.stats.added += job.patched.added;
487 job.stats.tiles += job.patched.tiles;
488 job.stats.conflicts += job.patched.conflicts;
489 job.stats.provisional_pixels += job.patched.provisional_pixels;
490 job.phase = Phase::Done;
491 }
492 Phase::Patch(c, g) if g < job.patch.len() => {
493 let ((tx, ty), ref blocks) = job.patch[g];
494 let key = TileKey::new(&job.p.canvas, 0, tx, ty);
495 let Some(id) = cache.touch(&key) else {
496 return Ok(Step::NeedTile(key));
497 };
498 let tile = cache.get_mut(id).unwrap();
499 check_tile(tile, cache_size(size))?;
500 let stats = overwrite_tile(
501 &mut OverwriteTile {
502 size: size as usize,
503 pixels: &mut tile.pixels,
504 coverage: &mut tile.coverage,
505 provisional: &mut tile.provisional,
506 quality: &mut tile.quality,
507 conflicts: &mut tile.conflicts,
508 owner: &mut tile.owner,
509 frozen: &mut tile.frozen,
510 },
511 frame.rgba,
512 frame.width as i64,
513 blocks,
514 job.ox,
515 job.oy,
516 tx as i64,
517 ty as i64,
518 job.p.frame,
519 job.p.confidence,
520 self.policy == TemporalPolicy::Stable,
521 );
522 job.patched.added += stats.added as u64;
523 job.patched.conflicts += stats.conflict_pixels as u64;
524 job.patched.provisional_pixels += stats.provisional_delta as i64;
525 if stats.changed {
526 cache.mark_dirty(id, now);
527 }
528 job.phase = Phase::Patch(c, g + 1);
529 }
530 Phase::Patch(c, _) => job.phase = Phase::Finish(c),
531 Phase::Finish(c) => {
532 if let Some(region) = job.incomplete.take() {
533 job.diagnostics.push(CompositorDiagnostic::IncompletePatch {
534 canvas: job.p.canvas.clone(),
535 frame: job.p.frame,
536 time: job.p.time,
537 region,
538 });
539 }
540 job.phase = Phase::Component(c + 1);
541 }
542 Phase::Done => {
543 return Ok(Step::Done(Outcome {
544 stats: job.stats,
545 diagnostics: std::mem::take(&mut job.diagnostics),
546 }));
547 }
548 }
549 }
550 }
551
552 pub fn load_temporal(
555 &mut self,
556 canvas: &str,
557 records: Vec<TemporalRecord>,
558 ) -> Result<(), CompositeError> {
559 if self.canvases.iter().any(|e| e.canvas == canvas) {
560 return Err(if self.disposed(canvas) {
561 CompositeError::Disposed
562 } else {
563 CompositeError::Unexpected
564 });
565 }
566 let mut index = TemporalIndex::new();
567 for record in records {
568 index.load(record);
569 }
570 self.canvases.push(CanvasTemporal {
571 canvas: canvas.to_string(),
572 index: Some(index),
573 stash: None,
574 });
575 Ok(())
576 }
577
578 fn disposed(&self, canvas: &str) -> bool {
579 self.canvases
580 .iter()
581 .any(|e| e.canvas == canvas && e.index.is_none())
582 }
583
584 pub fn flush(&mut self) -> Vec<TemporalFlush> {
586 self.canvases
587 .iter_mut()
588 .map(|e| {
589 let (deleted, dirty) = match e.index.as_mut() {
590 Some(index) => index.flush_take(),
591 None => e.stash.take().unwrap_or_default(),
592 };
593 TemporalFlush {
594 canvas: e.canvas.clone(),
595 deleted,
596 dirty,
597 }
598 })
599 .collect()
600 }
601
602 pub fn dispose(&mut self) {
605 for e in &mut self.canvases {
606 if let Some(mut index) = e.index.take() {
607 e.stash = Some(index.flush_take());
608 }
609 }
610 }
611}
612
613fn cache_size(size: i32) -> usize {
615 size as usize
616}
617
618fn check_tile(tile: &Tile, size: usize) -> Result<(), CompositeError> {
619 let n = size * size;
620 let blocks = (size / QUALITY_BLOCK) * (size / QUALITY_BLOCK);
621 let ok = tile.pixels.len() == n * 4
622 && tile.coverage.len() == n.div_ceil(8)
623 && tile.provisional.len() == n.div_ceil(8)
624 && [
625 tile.quality.len(),
626 tile.conflicts.len(),
627 tile.owner.len(),
628 tile.score.len(),
629 tile.frozen.len(),
630 ]
631 .iter()
632 .all(|&l| l == blocks);
633 if ok {
634 Ok(())
635 } else {
636 Err(CompositeError::BadTile)
637 }
638}
639
640fn tile_buffers(tile: &mut Tile, size: usize) -> Result<TileBuffers<'_>, CompositeError> {
641 check_tile(tile, size)?;
642 Ok(TileBuffers {
643 size,
644 pixels: &mut tile.pixels,
645 coverage: &mut tile.coverage,
646 provisional: &mut tile.provisional,
647 quality: &mut tile.quality,
648 conflicts: &mut tile.conflicts,
649 owner: &mut tile.owner,
650 score: &mut tile.score,
651 frozen: &tile.frozen,
652 })
653}