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 pub fixed: bool,
54}
55
56#[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 pub conflicts: u64,
72 pub uncertain: u64,
73 pub tiles: u64,
75 pub bounds: Rect,
77 pub provisional_pixels: i64,
79 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#[derive(Clone, Debug, PartialEq)]
104pub enum CompositorDiagnostic {
105 IncompletePatch {
107 canvas: String,
108 frame: u32,
109 time: f64,
110 region: Rect,
111 },
112 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 pub diagnostics: Vec<CompositorDiagnostic>,
130}
131
132#[derive(Clone, Debug, PartialEq)]
133pub enum Step {
134 NeedTile(TileKey),
136 NeedTemporal(String),
138 Done(Outcome),
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub enum CompositeError {
143 FrameSize,
145 OutsideCanvas,
147 NoPlacement,
149 Busy,
150 BadTile,
152 Disposed,
154 Unexpected,
156}
157
158struct CanvasTemporal {
160 canvas: String,
161 index: Option<TemporalIndex>,
162 stash: Option<(Vec<String>, Vec<DirtyRow>)>,
163}
164
165#[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
188type PatchTile = ((i32, i32), Vec<(i64, i64)>);
190
191struct 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 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 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 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 pub fn cancel(&mut self) {
317 self.job = None;
318 }
319
320 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 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 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 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 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 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
628fn 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}