1use crate::kernels::geometry::{js_ceil, js_round, Rect};
8use crate::kernels::pool::SyncPtr;
9
10pub const QUALITY_BLOCK: usize = 16;
11
12pub struct TileBuffers<'a> {
14 pub size: usize,
15 pub pixels: &'a mut [u8],
16 pub coverage: &'a mut [u8],
17 pub provisional: &'a mut [u8],
18 pub quality: &'a mut [u8],
19 pub conflicts: &'a mut [u8],
20 pub owner: &'a mut [u32],
21 pub score: &'a mut [f32],
22 pub frozen: &'a [u8],
23}
24
25pub struct Observation<'a> {
26 pub rgba: &'a [u8],
27 pub width: usize,
28 pub height: usize,
29 pub labels: Option<(&'a [u8], u8)>,
31 pub occlusions: &'a [Rect],
32 pub consistent: Option<&'a [u8]>,
34 pub confidence: f64,
35 pub uncertain: bool,
36 pub frame: u32,
37 pub latest: bool,
42}
43
44#[derive(Default, Debug, PartialEq)]
45pub struct TileStats {
46 pub added: u32,
47 pub conflicts: u32,
48 pub uncertain: u32,
49 pub provisional_delta: i32,
50 pub changed: bool,
51 pub settled_blocks: u32,
54 pub conflict_blocks: Vec<(u32, u32)>,
56}
57
58#[inline]
59fn bit(bits: &[u8], p: usize) -> bool {
60 bits[p >> 3] & (1 << (p & 7)) != 0
61}
62#[inline]
63fn set_bit(bits: &mut [u8], p: usize) {
64 bits[p >> 3] |= 1 << (p & 7);
65}
66#[inline]
67fn clear_bit(bits: &mut [u8], p: usize) {
68 bits[p >> 3] &= !(1 << (p & 7));
69}
70#[inline]
71fn occluded(occlusions: &[Rect], sx: i64, sy: i64) -> bool {
72 occlusions.iter().any(|o| o.contains(sx as f64, sy as f64))
73}
74#[inline]
75fn px32(rgba: &[u8], i: usize) -> u32 {
76 u32::from_le_bytes([
77 rgba[i * 4],
78 rgba[i * 4 + 1],
79 rgba[i * 4 + 2],
80 rgba[i * 4 + 3],
81 ])
82}
83
84struct Geometry<'o, 'a> {
86 obs: &'o Observation<'a>,
87 world: Rect,
88 size: i64,
89 blocks: i64,
90 dx: i64,
92 dy: i64,
93 w: i64,
94 h: i64,
95 tile_x: f64,
96 tile_y: f64,
97}
98
99#[derive(Default, Clone, Copy)]
101struct Part {
102 added: u32,
103 conflicts: u32,
104 uncertain: u32,
105 provisional_delta: i32,
106 changed: bool,
107 settled: u32,
108}
109
110struct Rows<'t> {
112 pixels: &'t mut [u8],
113 coverage: &'t mut [u8],
114 provisional: &'t mut [u8],
115 quality: &'t mut [u8],
116 conflicts: &'t mut [u8],
117 owner: &'t mut [u32],
118 score: &'t mut [f32],
119 frozen: &'t [u8],
120 flagged: &'t mut [u8],
122 by0: i64,
123}
124
125#[allow(clippy::too_many_arguments)]
129pub fn composite_tile(
130 tile: &mut TileBuffers<'_>,
131 obs: &Observation<'_>,
132 world: Rect,
133 ox: i64,
134 oy: i64,
135 tx: i64,
136 ty: i64,
137) -> TileStats {
138 let size = tile.size as i64;
139 let b = QUALITY_BLOCK as i64;
140 let blocks = size / b;
141 let mut stats = TileStats::default();
142 let tile_rect = Rect {
143 x: (tx * size) as f64,
144 y: (ty * size) as f64,
145 width: size as f64,
146 height: size as f64,
147 };
148 let px = world.x.max(tile_rect.x);
149 let py = world.y.max(tile_rect.y);
150 let pw = ((world.x + world.width).min(tile_rect.x + tile_rect.width) - px).max(0.0);
151 let ph = ((world.y + world.height).min(tile_rect.y + tile_rect.height) - py).max(0.0);
152 let bx0 = ((px - tile_rect.x) / b as f64).floor() as i64;
153 let by0 = ((py - tile_rect.y) / b as f64).floor() as i64;
154 let bx1 = js_ceil((px + pw - tile_rect.x) / b as f64) as i64;
155 let by1 = js_ceil((py + ph - tile_rect.y) / b as f64) as i64;
156 if by1 <= by0 || bx1 <= bx0 {
157 return stats;
158 }
159 let geo = Geometry {
160 obs,
161 world,
162 size,
163 blocks,
164 dx: tx * size - ox,
165 dy: ty * size - oy,
166 w: obs.width as i64,
167 h: obs.height as i64,
168 tile_x: tile_rect.x,
169 tile_y: tile_rect.y,
170 };
171 let rows = (by1 - by0) as usize;
172 let chunks = crate::kernels::pool::chunks_for(rows * ((bx1 - bx0) * b * b) as usize, 16 * 1024);
173 let mut parts = vec![Part::default(); chunks];
174 let mut flagged = vec![0u8; rows * blocks as usize];
175 let (row_px, row_bits, row_blocks) = (
176 (b * size) as usize,
177 (b * size / 8) as usize,
178 blocks as usize,
179 );
180 let base = |r: usize| by0 as usize + r;
181 let ptrs = (
182 SyncPtr(tile.pixels.as_mut_ptr()),
183 SyncPtr(tile.coverage.as_mut_ptr()),
184 SyncPtr(tile.provisional.as_mut_ptr()),
185 SyncPtr(tile.quality.as_mut_ptr()),
186 SyncPtr(tile.conflicts.as_mut_ptr()),
187 SyncPtr(tile.owner.as_mut_ptr()),
188 SyncPtr(tile.score.as_mut_ptr()),
189 SyncPtr(flagged.as_mut_ptr()),
190 SyncPtr(parts.as_mut_ptr()),
191 );
192 let frozen = tile.frozen;
193 crate::kernels::pool::par_for(chunks, |c| {
194 let (ra, rb) = (
195 crate::kernels::pool::split(rows, chunks, c),
196 crate::kernels::pool::split(rows, chunks, c + 1),
197 );
198 if ra == rb {
199 return;
200 }
201 let (a, n) = (base(ra), rb - ra);
202 let mut part = unsafe {
205 Rows {
206 pixels: std::slice::from_raw_parts_mut(
207 ptrs.0.get().add(a * row_px * 4),
208 n * row_px * 4,
209 ),
210 coverage: std::slice::from_raw_parts_mut(
211 ptrs.1.get().add(a * row_bits),
212 n * row_bits,
213 ),
214 provisional: std::slice::from_raw_parts_mut(
215 ptrs.2.get().add(a * row_bits),
216 n * row_bits,
217 ),
218 quality: std::slice::from_raw_parts_mut(
219 ptrs.3.get().add(a * row_blocks),
220 n * row_blocks,
221 ),
222 conflicts: std::slice::from_raw_parts_mut(
223 ptrs.4.get().add(a * row_blocks),
224 n * row_blocks,
225 ),
226 owner: std::slice::from_raw_parts_mut(
227 ptrs.5.get().add(a * row_blocks),
228 n * row_blocks,
229 ),
230 score: std::slice::from_raw_parts_mut(
231 ptrs.6.get().add(a * row_blocks),
232 n * row_blocks,
233 ),
234 frozen: &frozen[a * row_blocks..(a + n) * row_blocks],
235 flagged: std::slice::from_raw_parts_mut(
236 ptrs.7.get().add(ra * row_blocks),
237 n * row_blocks,
238 ),
239 by0: a as i64,
240 }
241 };
242 let mut sum = Part::default();
243 for by in a as i64..(a + n) as i64 {
244 for bx in bx0..bx1 {
245 composite_block(&geo, &mut part, bx, by, &mut sum);
246 }
247 }
248 unsafe { *ptrs.8.get().add(c) = sum };
250 });
251 for p in &parts {
252 stats.added += p.added;
253 stats.conflicts += p.conflicts;
254 stats.uncertain += p.uncertain;
255 stats.provisional_delta += p.provisional_delta;
256 stats.changed |= p.changed;
257 stats.settled_blocks += p.settled;
258 }
259 for r in 0..rows {
260 for bx in bx0..bx1 {
261 if flagged[r * blocks as usize + bx as usize] != 0 {
262 stats
263 .conflict_blocks
264 .push((bx as u32, (by0 + r as i64) as u32));
265 }
266 }
267 }
268 stats
269}
270
271#[inline]
273fn columns(geo: &Geometry<'_, '_>, sx0: i64, sx1: i64) -> (i64, i64) {
274 ((-geo.dx).max(sx0), (geo.w - geo.dx).min(sx1))
275}
276
277#[inline]
279fn row_occluded(occlusions: &[Rect], sy: i64) -> bool {
280 let fy = sy as f64;
281 occlusions.iter().any(|o| fy >= o.y && fy < o.y + o.height)
282}
283
284#[cfg(target_feature = "simd128")]
286#[inline]
287fn row_stats_simd(
288 obs: &Observation<'_>,
289 pixels: &[u8],
290 coverage: &[u8],
291 dst0: usize,
292 src0: usize,
293) -> (u32, u32, u32, u32) {
294 use core::arch::wasm32::*;
295 let inside: u32 = match obs.labels {
296 None => 0xffff,
297 Some((labels, code)) => unsafe {
299 let l = v128_load(labels.as_ptr().add(src0) as *const v128);
300 u8x16_bitmask(u8x16_eq(l, u8x16_splat(code))) as u32
301 },
302 };
303 let cov = coverage[dst0 >> 3] as u32 | (coverage[(dst0 >> 3) + 1] as u32) << 8;
304 let (mut eq, mut mism) = (0u32, 0u32);
305 let rgb = u32x4_splat(0x00ff_ffff);
306 let limit = u32x4_splat(75);
307 for g in 0..4 {
308 let (t, o) = unsafe {
310 (
311 v128_load(pixels.as_ptr().add((dst0 + 4 * g) * 4) as *const v128),
312 v128_load(obs.rgba.as_ptr().add((src0 + 4 * g) * 4) as *const v128),
313 )
314 };
315 eq |= (i32x4_bitmask(i32x4_eq(t, o)) as u32) << (4 * g);
316 let d = v128_and(v128_or(u8x16_sub_sat(t, o), u8x16_sub_sat(o, t)), rgb);
317 let sum = u32x4_extadd_pairwise_u16x8(u16x8_extadd_pairwise_u8x16(d));
318 mism |= (i32x4_bitmask(u32x4_gt(sum, limit)) as u32) << (4 * g);
319 }
320 let ov = cov & inside;
321 (
322 inside.count_ones(),
323 ov.count_ones(),
324 (ov & eq).count_ones(),
325 (ov & mism).count_ones(),
326 )
327}
328
329#[cfg(not(target_feature = "simd128"))]
333#[inline]
334fn row_stats_scalar(
335 obs: &Observation<'_>,
336 pixels: &[u8],
337 coverage: &[u8],
338 dst0: usize,
339 src0: usize,
340) -> (u32, u32, u32, u32) {
341 const N: usize = QUALITY_BLOCK;
342 let (t, o) = (
343 &pixels[dst0 * 4..(dst0 + N) * 4],
344 &obs.rgba[src0 * 4..(src0 + N) * 4],
345 );
346 let inside: u32 = match obs.labels {
347 None => (1 << N) - 1,
348 Some((labels, code)) => labels[src0..src0 + N]
349 .iter()
350 .enumerate()
351 .fold(0, |m, (i, &l)| m | ((l == code) as u32) << i),
352 };
353 let cov = coverage[dst0 >> 3] as u32 | (coverage[(dst0 >> 3) + 1] as u32) << 8;
354 let (mut eq, mut mism) = (0u32, 0u32);
355 for (i, (a, b)) in t.chunks_exact(4).zip(o.chunks_exact(4)).enumerate() {
356 eq |= ((a == b) as u32) << i;
357 let sum =
358 a[0].abs_diff(b[0]) as u32 + a[1].abs_diff(b[1]) as u32 + a[2].abs_diff(b[2]) as u32;
359 mism |= ((sum > 75) as u32) << i;
360 }
361 let ov = cov & inside;
362 (
363 inside.count_ones(),
364 ov.count_ones(),
365 (ov & eq).count_ones(),
366 (ov & mism).count_ones(),
367 )
368}
369
370fn composite_block(geo: &Geometry<'_, '_>, t: &mut Rows<'_>, bx: i64, by: i64, sum: &mut Part) {
371 let obs = geo.obs;
372 let world = geo.world;
373 let (size, b) = (geo.size, QUALITY_BLOCK as i64);
374 let (w, h) = (geo.w, geo.h);
375 let (tile_x, tile_y) = (geo.tile_x, geo.tile_y);
376 let ly = by - t.by0;
377 let q = (ly * geo.blocks + bx) as usize;
378 let inside = |src: usize| obs.labels.is_none_or(|(labels, code)| labels[src] == code);
379 let (mut mismatch, mut overlap, mut count, mut identical) = (0u32, 0u32, 0u32, 0u32);
380 let sy0 = js_ceil(((by * b) as f64).max(world.y - tile_y)) as i64;
381 let sy1 = js_ceil((((by + 1) * b) as f64).min(world.y + world.height - tile_y)) as i64;
382 let sx0 = js_ceil(((bx * b) as f64).max(world.x - tile_x)) as i64;
383 let sx1 = js_ceil((((bx + 1) * b) as f64).min(world.x + world.width - tile_x)) as i64;
384 let (cx0, cx1) = columns(geo, sx0, sx1);
385 let full = cx0 == bx * b && cx1 == (bx + 1) * b;
386 let dst_of = |x: i64, y: i64| ((y - t.by0 * b) * size + x) as usize;
388 for y in sy0..sy1 {
389 let sy = y + geo.dy;
390 if sy < 0 || sy >= h || cx1 <= cx0 {
391 continue;
392 }
393 let occluded_row = row_occluded(obs.occlusions, sy);
394 if full && !occluded_row {
395 #[cfg(target_feature = "simd128")]
396 let row_stats = row_stats_simd;
397 #[cfg(not(target_feature = "simd128"))]
398 let row_stats = row_stats_scalar;
399 let (c, o, i, m) = row_stats(
400 obs,
401 t.pixels,
402 t.coverage,
403 dst_of(cx0, y),
404 (sy * w + cx0 + geo.dx) as usize,
405 );
406 count += c;
407 overlap += o;
408 identical += i;
409 mismatch += m;
410 continue;
411 }
412 for x in cx0..cx1 {
413 let sx = x + geo.dx;
414 if occluded_row && occluded(obs.occlusions, sx, sy) {
415 continue;
416 }
417 let src = (sy * w + sx) as usize;
418 if !inside(src) {
419 continue;
420 }
421 let dst = dst_of(x, y);
422 count += 1;
423 if bit(t.coverage, dst) {
424 overlap += 1;
425 if px32(t.pixels, dst) == px32(obs.rgba, src) {
426 identical += 1;
427 continue;
428 }
429 let (i, j) = (dst * 4, src * 4);
430 let diff = ((t.pixels[i] as i32 - obs.rgba[j] as i32).abs()
431 + (t.pixels[i + 1] as i32 - obs.rgba[j + 1] as i32).abs()
432 + (t.pixels[i + 2] as i32 - obs.rgba[j + 2] as i32).abs())
433 as f64
434 / 3.0;
435 if diff > 25.0 {
436 mismatch += 1;
437 }
438 }
439 }
440 }
441 if count == 0 {
442 return;
443 }
444 let (mut covered_in_block, mut provisional_in_block) = (0u32, 0i32);
445 for row in 0..b {
446 let i = dst_of(bx * b, by * b + row) >> 3;
447 covered_in_block += t.coverage[i].count_ones() + t.coverage[i + 1].count_ones();
448 provisional_in_block +=
449 (t.provisional[i].count_ones() + t.provisional[i + 1].count_ones()) as i32;
450 }
451 if identical == count && provisional_in_block == 0 {
454 return;
455 }
456 let conflict = overlap >= 12 && mismatch as f64 / overlap as f64 > 0.16;
457 let complete = overlap == covered_in_block;
458 let edge = ((geo.tile_x as i64 + bx * b) as f64 - world.x)
459 .min((geo.tile_y as i64 + by * b) as f64 - world.y)
460 .min(world.x + world.width - (geo.tile_x as i64 + (bx + 1) * b) as f64)
461 .min(world.y + world.height - (geo.tile_y as i64 + (by + 1) * b) as f64);
462 let mut sharpness = 0u64;
464 if (!conflict && t.frozen[q] == 0 && complete) || t.owner[q] == 0 {
465 let (gx0, gx1) = ((1 - geo.dx).max(cx0), (w - 1 - geo.dx).min(cx1));
466 for y in sy0..sy1 {
467 let sy = y + geo.dy;
468 if sy < 0 || sy >= h {
469 continue;
470 }
471 let occluded_row = row_occluded(obs.occlusions, sy);
472 for x in gx0..gx1 {
473 let sx = x + geo.dx;
474 if occluded_row && occluded(obs.occlusions, sx, sy) {
475 continue;
476 }
477 let src = (sy * w + sx) as usize;
478 if inside(src) {
479 let j = src * 4;
480 sharpness +=
481 (obs.rgba[j - 4] as i32 - obs.rgba[j + 4] as i32).unsigned_abs() as u64;
482 }
483 }
484 }
485 }
486 let had_provisional = provisional_in_block > 0;
487 let score = obs.confidence * 100.0
488 + (12.0f64).min(sharpness as f64 / count as f64 * 0.15)
489 + (6.0f64).min(edge.max(0.0) / 40.0);
490 let replace = complete
491 && t.frozen[q] == 0
492 && (obs.latest || (!conflict && score > t.score[q] as f64 + 4.0));
493 if conflict && obs.latest {
494 if let Some(consistent) = obs.consistent {
495 let settled = (sy0..sy1).all(|y| {
496 let sy = y + geo.dy;
497 sy < 0
498 || sy >= h
499 || (cx0..cx1).all(|x| {
500 let src = (sy * w + x + geo.dx) as usize;
501 !inside(src)
502 || occluded(obs.occlusions, x + geo.dx, sy)
503 || consistent[src] != 0
504 })
505 });
506 if settled {
507 sum.settled += 1;
508 }
509 }
510 }
511 if conflict {
512 t.conflicts[q] = 1;
513 t.flagged[q] = 1;
514 sum.conflicts += mismatch;
515 sum.changed = true;
516 }
517 if replace || overlap < count || provisional_in_block > 0 {
518 for y in sy0..sy1 {
519 let sy = y + geo.dy;
520 if sy < 0 || sy >= h {
521 continue;
522 }
523 let occluded_row = row_occluded(obs.occlusions, sy);
524 for x in cx0..cx1 {
525 let sx = x + geo.dx;
526 if occluded_row && occluded(obs.occlusions, sx, sy) {
527 continue;
528 }
529 let src = (sy * w + sx) as usize;
530 if !inside(src) {
531 continue;
532 }
533 let dst = dst_of(x, y);
534 let fresh = !bit(t.coverage, dst);
535 let bad = obs.consistent.is_some_and(|c| c[src] == 0);
536 let was_provisional = bit(t.provisional, dst);
537 let write = if fresh {
541 true
542 } else if bad {
543 false
544 } else {
545 was_provisional || replace
546 };
547 if !write {
548 if bad && !was_provisional && px32(t.pixels, dst) == px32(obs.rgba, src) {
550 set_bit(t.provisional, dst);
551 sum.provisional_delta += 1;
552 provisional_in_block += 1;
553 sum.changed = true;
554 }
555 continue;
556 }
557 t.pixels[dst * 4..dst * 4 + 4].copy_from_slice(&obs.rgba[src * 4..src * 4 + 4]);
558 if fresh {
559 set_bit(t.coverage, dst);
560 sum.added += 1;
561 if obs.uncertain {
562 sum.uncertain += 1;
563 }
564 }
565 if bad {
566 if !was_provisional {
567 set_bit(t.provisional, dst);
568 sum.provisional_delta += 1;
569 provisional_in_block += 1;
570 }
571 } else if was_provisional {
572 clear_bit(t.provisional, dst);
573 sum.provisional_delta -= 1;
574 provisional_in_block -= 1;
575 }
576 sum.changed = true;
577 }
578 }
579 }
580 let before = (t.quality[q], t.owner[q], t.score[q].to_bits());
583 let quality = js_round(obs.confidence * 255.0) as u8;
584 if replace || t.owner[q] == 0 {
585 t.quality[q] = quality;
586 t.owner[q] = obs.frame + 1;
587 t.score[q] = score as f32;
588 } else if obs.uncertain {
589 t.quality[q] = if t.owner[q] != 0 {
590 t.quality[q].min(quality)
591 } else {
592 quality
593 };
594 }
595 if had_provisional && provisional_in_block == 0 && !obs.uncertain {
596 t.quality[q] = t.quality[q].max(quality);
597 }
598 if provisional_in_block > 0 {
599 t.quality[q] = t.quality[q].min(64);
600 }
601 if (t.quality[q], t.owner[q], t.score[q].to_bits()) != before {
602 sum.changed = true;
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 #[cfg(not(target_feature = "simd128"))]
614 #[test]
615 fn block_row_stats_match_the_per_pixel_rule() {
616 let mut seed = 0x1234_5678u32;
617 let mut next = move || {
618 seed ^= seed << 13;
619 seed ^= seed >> 17;
620 seed ^= seed << 5;
621 seed
622 };
623 let n = QUALITY_BLOCK;
624 for round in 0..2000 {
625 let (dst0, src0) = (n * (round % 3), 5 + round % 7);
626 let mut pixels = vec![0u8; (dst0 + n) * 4];
627 let mut rgba = vec![0u8; (src0 + n) * 4];
628 for v in pixels.iter_mut().chain(rgba.iter_mut()) {
629 *v = next() as u8;
630 }
631 for i in 0..n {
632 let (a, b) = ((dst0 + i) * 4, (src0 + i) * 4);
633 match next() % 5 {
634 0 => pixels[a..a + 4].copy_from_slice(&rgba[b..b + 4]),
636 1 => {
637 pixels[a..a + 4].copy_from_slice(&rgba[b..b + 4]);
638 pixels[a + 3] ^= 1;
639 }
640 k @ (2 | 3) => {
641 let base = [100u8, 120, 140, 255];
642 rgba[b..b + 4].copy_from_slice(&base);
643 let d = if k == 2 { 75 } else { 76 };
644 pixels[a..a + 4].copy_from_slice(&[
645 100 + 25,
646 120 - 25,
647 140 + (d - 50) as u8,
648 255,
649 ]);
650 }
651 _ => {}
652 }
653 }
654 let coverage: Vec<u8> = (0..(dst0 + n) / 8).map(|_| next() as u8).collect();
655 let labels: Vec<u8> = (0..src0 + n).map(|_| (next() % 3) as u8).collect();
656 for with_labels in [false, true] {
657 let obs = Observation {
658 rgba: &rgba,
659 width: src0 + n,
660 height: 1,
661 labels: with_labels.then_some((&labels[..], 1)),
662 occlusions: &[],
663 consistent: None,
664 confidence: 1.0,
665 uncertain: false,
666 frame: 0,
667 latest: false,
668 };
669 let mut want = (0, 0, 0, 0);
670 for i in 0..n {
671 let (dst, src) = (dst0 + i, src0 + i);
672 if with_labels && labels[src] != 1 {
673 continue;
674 }
675 want.0 += 1;
676 if bit(&coverage, dst) {
677 want.1 += 1;
678 if px32(&pixels, dst) == px32(&rgba, src) {
679 want.2 += 1;
680 continue;
681 }
682 let (i, j) = (dst * 4, src * 4);
683 let diff = ((pixels[i] as i32 - rgba[j] as i32).abs()
684 + (pixels[i + 1] as i32 - rgba[j + 1] as i32).abs()
685 + (pixels[i + 2] as i32 - rgba[j + 2] as i32).abs())
686 as f64
687 / 3.0;
688 if diff > 25.0 {
689 want.3 += 1;
690 }
691 }
692 }
693 assert_eq!(
694 row_stats_scalar(&obs, &pixels, &coverage, dst0, src0),
695 want,
696 "round {round} labels {with_labels}"
697 );
698 }
699 }
700 }
701
702 #[test]
703 fn fresh_pixels_are_written_and_counted() {
704 let size = 32usize;
705 let mut pixels = vec![0u8; size * size * 4];
706 let mut coverage = vec![0u8; size * size / 8];
707 let mut provisional = vec![0u8; size * size / 8];
708 let blocks = (size / QUALITY_BLOCK).pow(2);
709 let (mut quality, mut conflicts, mut owner, mut score, frozen) = (
710 vec![0u8; blocks],
711 vec![0u8; blocks],
712 vec![0u32; blocks],
713 vec![0f32; blocks],
714 vec![0u8; blocks],
715 );
716 let mut tile = TileBuffers {
717 size,
718 pixels: &mut pixels,
719 coverage: &mut coverage,
720 provisional: &mut provisional,
721 quality: &mut quality,
722 conflicts: &mut conflicts,
723 owner: &mut owner,
724 score: &mut score,
725 frozen: &frozen,
726 };
727 let rgba: Vec<u8> = (0..20 * 10).flat_map(|i| [i as u8, 7, 9, 255]).collect();
728 let obs = Observation {
729 rgba: &rgba,
730 width: 20,
731 height: 10,
732 labels: None,
733 occlusions: &[],
734 consistent: None,
735 confidence: 0.9,
736 uncertain: false,
737 frame: 3,
738 latest: false,
739 };
740 let world = Rect {
741 x: 5.0,
742 y: 6.0,
743 width: 20.0,
744 height: 10.0,
745 };
746 let stats = composite_tile(&mut tile, &obs, world, 5, 6, 0, 0);
747 assert_eq!(stats.added, 200);
748 assert!(stats.changed && stats.conflict_blocks.is_empty());
749 assert_eq!(tile.owner[0], 4);
750 assert_eq!(tile.pixels[(6 * size + 5) * 4], 0);
751 assert_eq!(tile.pixels[(6 * size + 6) * 4], 1);
752 }
753}