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}
38
39#[derive(Default, Debug, PartialEq)]
40pub struct TileStats {
41 pub added: u32,
42 pub conflicts: u32,
43 pub uncertain: u32,
44 pub provisional_delta: i32,
45 pub changed: bool,
46 pub conflict_blocks: Vec<(u32, u32)>,
48}
49
50#[inline]
51fn bit(bits: &[u8], p: usize) -> bool {
52 bits[p >> 3] & (1 << (p & 7)) != 0
53}
54#[inline]
55fn set_bit(bits: &mut [u8], p: usize) {
56 bits[p >> 3] |= 1 << (p & 7);
57}
58#[inline]
59fn clear_bit(bits: &mut [u8], p: usize) {
60 bits[p >> 3] &= !(1 << (p & 7));
61}
62#[inline]
63fn occluded(occlusions: &[Rect], sx: i64, sy: i64) -> bool {
64 occlusions.iter().any(|o| o.contains(sx as f64, sy as f64))
65}
66#[inline]
67fn px32(rgba: &[u8], i: usize) -> u32 {
68 u32::from_le_bytes([
69 rgba[i * 4],
70 rgba[i * 4 + 1],
71 rgba[i * 4 + 2],
72 rgba[i * 4 + 3],
73 ])
74}
75
76struct Geometry<'o, 'a> {
78 obs: &'o Observation<'a>,
79 world: Rect,
80 size: i64,
81 blocks: i64,
82 dx: i64,
84 dy: i64,
85 w: i64,
86 h: i64,
87 tile_x: f64,
88 tile_y: f64,
89}
90
91#[derive(Default, Clone, Copy)]
93struct Part {
94 added: u32,
95 conflicts: u32,
96 uncertain: u32,
97 provisional_delta: i32,
98 changed: bool,
99}
100
101struct Rows<'t> {
103 pixels: &'t mut [u8],
104 coverage: &'t mut [u8],
105 provisional: &'t mut [u8],
106 quality: &'t mut [u8],
107 conflicts: &'t mut [u8],
108 owner: &'t mut [u32],
109 score: &'t mut [f32],
110 frozen: &'t [u8],
111 flagged: &'t mut [u8],
113 by0: i64,
114}
115
116#[allow(clippy::too_many_arguments)]
120pub fn composite_tile(
121 tile: &mut TileBuffers<'_>,
122 obs: &Observation<'_>,
123 world: Rect,
124 ox: i64,
125 oy: i64,
126 tx: i64,
127 ty: i64,
128) -> TileStats {
129 let size = tile.size as i64;
130 let b = QUALITY_BLOCK as i64;
131 let blocks = size / b;
132 let mut stats = TileStats::default();
133 let tile_rect = Rect {
134 x: (tx * size) as f64,
135 y: (ty * size) as f64,
136 width: size as f64,
137 height: size as f64,
138 };
139 let px = world.x.max(tile_rect.x);
140 let py = world.y.max(tile_rect.y);
141 let pw = ((world.x + world.width).min(tile_rect.x + tile_rect.width) - px).max(0.0);
142 let ph = ((world.y + world.height).min(tile_rect.y + tile_rect.height) - py).max(0.0);
143 let bx0 = ((px - tile_rect.x) / b as f64).floor() as i64;
144 let by0 = ((py - tile_rect.y) / b as f64).floor() as i64;
145 let bx1 = js_ceil((px + pw - tile_rect.x) / b as f64) as i64;
146 let by1 = js_ceil((py + ph - tile_rect.y) / b as f64) as i64;
147 if by1 <= by0 || bx1 <= bx0 {
148 return stats;
149 }
150 let geo = Geometry {
151 obs,
152 world,
153 size,
154 blocks,
155 dx: tx * size - ox,
156 dy: ty * size - oy,
157 w: obs.width as i64,
158 h: obs.height as i64,
159 tile_x: tile_rect.x,
160 tile_y: tile_rect.y,
161 };
162 let rows = (by1 - by0) as usize;
163 let chunks = crate::kernels::pool::chunks_for(rows * ((bx1 - bx0) * b * b) as usize, 16 * 1024);
164 let mut parts = vec![Part::default(); chunks];
165 let mut flagged = vec![0u8; rows * blocks as usize];
166 let (row_px, row_bits, row_blocks) = (
167 (b * size) as usize,
168 (b * size / 8) as usize,
169 blocks as usize,
170 );
171 let base = |r: usize| by0 as usize + r;
172 let ptrs = (
173 SyncPtr(tile.pixels.as_mut_ptr()),
174 SyncPtr(tile.coverage.as_mut_ptr()),
175 SyncPtr(tile.provisional.as_mut_ptr()),
176 SyncPtr(tile.quality.as_mut_ptr()),
177 SyncPtr(tile.conflicts.as_mut_ptr()),
178 SyncPtr(tile.owner.as_mut_ptr()),
179 SyncPtr(tile.score.as_mut_ptr()),
180 SyncPtr(flagged.as_mut_ptr()),
181 SyncPtr(parts.as_mut_ptr()),
182 );
183 let frozen = tile.frozen;
184 crate::kernels::pool::par_for(chunks, |c| {
185 let (ra, rb) = (
186 crate::kernels::pool::split(rows, chunks, c),
187 crate::kernels::pool::split(rows, chunks, c + 1),
188 );
189 if ra == rb {
190 return;
191 }
192 let (a, n) = (base(ra), rb - ra);
193 let mut part = unsafe {
196 Rows {
197 pixels: std::slice::from_raw_parts_mut(
198 ptrs.0.get().add(a * row_px * 4),
199 n * row_px * 4,
200 ),
201 coverage: std::slice::from_raw_parts_mut(
202 ptrs.1.get().add(a * row_bits),
203 n * row_bits,
204 ),
205 provisional: std::slice::from_raw_parts_mut(
206 ptrs.2.get().add(a * row_bits),
207 n * row_bits,
208 ),
209 quality: std::slice::from_raw_parts_mut(
210 ptrs.3.get().add(a * row_blocks),
211 n * row_blocks,
212 ),
213 conflicts: std::slice::from_raw_parts_mut(
214 ptrs.4.get().add(a * row_blocks),
215 n * row_blocks,
216 ),
217 owner: std::slice::from_raw_parts_mut(
218 ptrs.5.get().add(a * row_blocks),
219 n * row_blocks,
220 ),
221 score: std::slice::from_raw_parts_mut(
222 ptrs.6.get().add(a * row_blocks),
223 n * row_blocks,
224 ),
225 frozen: &frozen[a * row_blocks..(a + n) * row_blocks],
226 flagged: std::slice::from_raw_parts_mut(
227 ptrs.7.get().add(ra * row_blocks),
228 n * row_blocks,
229 ),
230 by0: a as i64,
231 }
232 };
233 let mut sum = Part::default();
234 for by in a as i64..(a + n) as i64 {
235 for bx in bx0..bx1 {
236 composite_block(&geo, &mut part, bx, by, &mut sum);
237 }
238 }
239 unsafe { *ptrs.8.get().add(c) = sum };
241 });
242 for p in &parts {
243 stats.added += p.added;
244 stats.conflicts += p.conflicts;
245 stats.uncertain += p.uncertain;
246 stats.provisional_delta += p.provisional_delta;
247 stats.changed |= p.changed;
248 }
249 for r in 0..rows {
250 for bx in bx0..bx1 {
251 if flagged[r * blocks as usize + bx as usize] != 0 {
252 stats
253 .conflict_blocks
254 .push((bx as u32, (by0 + r as i64) as u32));
255 }
256 }
257 }
258 stats
259}
260
261#[inline]
263fn columns(geo: &Geometry<'_, '_>, sx0: i64, sx1: i64) -> (i64, i64) {
264 ((-geo.dx).max(sx0), (geo.w - geo.dx).min(sx1))
265}
266
267#[inline]
269fn row_occluded(occlusions: &[Rect], sy: i64) -> bool {
270 let fy = sy as f64;
271 occlusions.iter().any(|o| fy >= o.y && fy < o.y + o.height)
272}
273
274#[cfg(target_feature = "simd128")]
276#[inline]
277fn row_stats_simd(
278 obs: &Observation<'_>,
279 pixels: &[u8],
280 coverage: &[u8],
281 dst0: usize,
282 src0: usize,
283) -> (u32, u32, u32, u32) {
284 use core::arch::wasm32::*;
285 let inside: u32 = match obs.labels {
286 None => 0xffff,
287 Some((labels, code)) => unsafe {
289 let l = v128_load(labels.as_ptr().add(src0) as *const v128);
290 u8x16_bitmask(u8x16_eq(l, u8x16_splat(code))) as u32
291 },
292 };
293 let cov = coverage[dst0 >> 3] as u32 | (coverage[(dst0 >> 3) + 1] as u32) << 8;
294 let (mut eq, mut mism) = (0u32, 0u32);
295 let rgb = u32x4_splat(0x00ff_ffff);
296 let limit = u32x4_splat(75);
297 for g in 0..4 {
298 let (t, o) = unsafe {
300 (
301 v128_load(pixels.as_ptr().add((dst0 + 4 * g) * 4) as *const v128),
302 v128_load(obs.rgba.as_ptr().add((src0 + 4 * g) * 4) as *const v128),
303 )
304 };
305 eq |= (i32x4_bitmask(i32x4_eq(t, o)) as u32) << (4 * g);
306 let d = v128_and(v128_or(u8x16_sub_sat(t, o), u8x16_sub_sat(o, t)), rgb);
307 let sum = u32x4_extadd_pairwise_u16x8(u16x8_extadd_pairwise_u8x16(d));
308 mism |= (i32x4_bitmask(u32x4_gt(sum, limit)) as u32) << (4 * g);
309 }
310 let ov = cov & inside;
311 (
312 inside.count_ones(),
313 ov.count_ones(),
314 (ov & eq).count_ones(),
315 (ov & mism).count_ones(),
316 )
317}
318
319fn composite_block(geo: &Geometry<'_, '_>, t: &mut Rows<'_>, bx: i64, by: i64, sum: &mut Part) {
320 let obs = geo.obs;
321 let world = geo.world;
322 let (size, b) = (geo.size, QUALITY_BLOCK as i64);
323 let (w, h) = (geo.w, geo.h);
324 let (tile_x, tile_y) = (geo.tile_x, geo.tile_y);
325 let ly = by - t.by0;
326 let q = (ly * geo.blocks + bx) as usize;
327 let inside = |src: usize| obs.labels.is_none_or(|(labels, code)| labels[src] == code);
328 let (mut mismatch, mut overlap, mut count, mut identical) = (0u32, 0u32, 0u32, 0u32);
329 let sy0 = js_ceil(((by * b) as f64).max(world.y - tile_y)) as i64;
330 let sy1 = js_ceil((((by + 1) * b) as f64).min(world.y + world.height - tile_y)) as i64;
331 let sx0 = js_ceil(((bx * b) as f64).max(world.x - tile_x)) as i64;
332 let sx1 = js_ceil((((bx + 1) * b) as f64).min(world.x + world.width - tile_x)) as i64;
333 let (cx0, cx1) = columns(geo, sx0, sx1);
334 let full = cx0 == bx * b && cx1 == (bx + 1) * b;
335 let dst_of = |x: i64, y: i64| ((y - t.by0 * b) * size + x) as usize;
337 for y in sy0..sy1 {
338 let sy = y + geo.dy;
339 if sy < 0 || sy >= h || cx1 <= cx0 {
340 continue;
341 }
342 let occluded_row = row_occluded(obs.occlusions, sy);
343 #[cfg(target_feature = "simd128")]
344 if full && !occluded_row {
345 let (c, o, i, m) = row_stats_simd(
346 obs,
347 t.pixels,
348 t.coverage,
349 dst_of(cx0, y),
350 (sy * w + cx0 + geo.dx) as usize,
351 );
352 count += c;
353 overlap += o;
354 identical += i;
355 mismatch += m;
356 continue;
357 }
358 let _ = full;
359 for x in cx0..cx1 {
360 let sx = x + geo.dx;
361 if occluded_row && occluded(obs.occlusions, sx, sy) {
362 continue;
363 }
364 let src = (sy * w + sx) as usize;
365 if !inside(src) {
366 continue;
367 }
368 let dst = dst_of(x, y);
369 count += 1;
370 if bit(t.coverage, dst) {
371 overlap += 1;
372 if px32(t.pixels, dst) == px32(obs.rgba, src) {
373 identical += 1;
374 continue;
375 }
376 let (i, j) = (dst * 4, src * 4);
377 let diff = ((t.pixels[i] as i32 - obs.rgba[j] as i32).abs()
378 + (t.pixels[i + 1] as i32 - obs.rgba[j + 1] as i32).abs()
379 + (t.pixels[i + 2] as i32 - obs.rgba[j + 2] as i32).abs())
380 as f64
381 / 3.0;
382 if diff > 25.0 {
383 mismatch += 1;
384 }
385 }
386 }
387 }
388 if count == 0 {
389 return;
390 }
391 let (mut covered_in_block, mut provisional_in_block) = (0u32, 0i32);
392 for row in 0..b {
393 let i = dst_of(bx * b, by * b + row) >> 3;
394 covered_in_block += t.coverage[i].count_ones() + t.coverage[i + 1].count_ones();
395 provisional_in_block +=
396 (t.provisional[i].count_ones() + t.provisional[i + 1].count_ones()) as i32;
397 }
398 if identical == count && provisional_in_block == 0 {
401 return;
402 }
403 let conflict = overlap >= 12 && mismatch as f64 / overlap as f64 > 0.16;
404 let complete = overlap == covered_in_block;
405 let edge = ((geo.tile_x as i64 + bx * b) as f64 - world.x)
406 .min((geo.tile_y as i64 + by * b) as f64 - world.y)
407 .min(world.x + world.width - (geo.tile_x as i64 + (bx + 1) * b) as f64)
408 .min(world.y + world.height - (geo.tile_y as i64 + (by + 1) * b) as f64);
409 let mut sharpness = 0u64;
411 if (!conflict && t.frozen[q] == 0 && complete) || t.owner[q] == 0 {
412 let (gx0, gx1) = ((1 - geo.dx).max(cx0), (w - 1 - geo.dx).min(cx1));
413 for y in sy0..sy1 {
414 let sy = y + geo.dy;
415 if sy < 0 || sy >= h {
416 continue;
417 }
418 let occluded_row = row_occluded(obs.occlusions, sy);
419 for x in gx0..gx1 {
420 let sx = x + geo.dx;
421 if occluded_row && occluded(obs.occlusions, sx, sy) {
422 continue;
423 }
424 let src = (sy * w + sx) as usize;
425 if inside(src) {
426 let j = src * 4;
427 sharpness +=
428 (obs.rgba[j - 4] as i32 - obs.rgba[j + 4] as i32).unsigned_abs() as u64;
429 }
430 }
431 }
432 }
433 let had_provisional = provisional_in_block > 0;
434 let score = obs.confidence * 100.0
435 + (12.0f64).min(sharpness as f64 / count as f64 * 0.15)
436 + (6.0f64).min(edge.max(0.0) / 40.0);
437 let replace = complete && !conflict && t.frozen[q] == 0 && score > t.score[q] as f64 + 4.0;
438 if conflict {
439 t.conflicts[q] = 1;
440 t.flagged[q] = 1;
441 sum.conflicts += mismatch;
442 sum.changed = true;
443 }
444 if replace || overlap < count || provisional_in_block > 0 {
445 for y in sy0..sy1 {
446 let sy = y + geo.dy;
447 if sy < 0 || sy >= h {
448 continue;
449 }
450 let occluded_row = row_occluded(obs.occlusions, sy);
451 for x in cx0..cx1 {
452 let sx = x + geo.dx;
453 if occluded_row && occluded(obs.occlusions, sx, sy) {
454 continue;
455 }
456 let src = (sy * w + sx) as usize;
457 if !inside(src) {
458 continue;
459 }
460 let dst = dst_of(x, y);
461 let fresh = !bit(t.coverage, dst);
462 let bad = obs.consistent.is_some_and(|c| c[src] == 0);
463 let was_provisional = bit(t.provisional, dst);
464 let write = if fresh {
468 true
469 } else if bad {
470 false
471 } else {
472 was_provisional || replace
473 };
474 if !write {
475 if bad && !was_provisional && px32(t.pixels, dst) == px32(obs.rgba, src) {
477 set_bit(t.provisional, dst);
478 sum.provisional_delta += 1;
479 provisional_in_block += 1;
480 sum.changed = true;
481 }
482 continue;
483 }
484 t.pixels[dst * 4..dst * 4 + 4].copy_from_slice(&obs.rgba[src * 4..src * 4 + 4]);
485 if fresh {
486 set_bit(t.coverage, dst);
487 sum.added += 1;
488 if obs.uncertain {
489 sum.uncertain += 1;
490 }
491 }
492 if bad {
493 if !was_provisional {
494 set_bit(t.provisional, dst);
495 sum.provisional_delta += 1;
496 provisional_in_block += 1;
497 }
498 } else if was_provisional {
499 clear_bit(t.provisional, dst);
500 sum.provisional_delta -= 1;
501 provisional_in_block -= 1;
502 }
503 sum.changed = true;
504 }
505 }
506 }
507 let before = (t.quality[q], t.owner[q], t.score[q].to_bits());
510 let quality = js_round(obs.confidence * 255.0) as u8;
511 if replace || t.owner[q] == 0 {
512 t.quality[q] = quality;
513 t.owner[q] = obs.frame + 1;
514 t.score[q] = score as f32;
515 } else if obs.uncertain {
516 t.quality[q] = if t.owner[q] != 0 {
517 t.quality[q].min(quality)
518 } else {
519 quality
520 };
521 }
522 if had_provisional && provisional_in_block == 0 && !obs.uncertain {
523 t.quality[q] = t.quality[q].max(quality);
524 }
525 if provisional_in_block > 0 {
526 t.quality[q] = t.quality[q].min(64);
527 }
528 if (t.quality[q], t.owner[q], t.score[q].to_bits()) != before {
529 sum.changed = true;
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn fresh_pixels_are_written_and_counted() {
539 let size = 32usize;
540 let mut pixels = vec![0u8; size * size * 4];
541 let mut coverage = vec![0u8; size * size / 8];
542 let mut provisional = vec![0u8; size * size / 8];
543 let blocks = (size / QUALITY_BLOCK).pow(2);
544 let (mut quality, mut conflicts, mut owner, mut score, frozen) = (
545 vec![0u8; blocks],
546 vec![0u8; blocks],
547 vec![0u32; blocks],
548 vec![0f32; blocks],
549 vec![0u8; blocks],
550 );
551 let mut tile = TileBuffers {
552 size,
553 pixels: &mut pixels,
554 coverage: &mut coverage,
555 provisional: &mut provisional,
556 quality: &mut quality,
557 conflicts: &mut conflicts,
558 owner: &mut owner,
559 score: &mut score,
560 frozen: &frozen,
561 };
562 let rgba: Vec<u8> = (0..20 * 10).flat_map(|i| [i as u8, 7, 9, 255]).collect();
563 let obs = Observation {
564 rgba: &rgba,
565 width: 20,
566 height: 10,
567 labels: None,
568 occlusions: &[],
569 consistent: None,
570 confidence: 0.9,
571 uncertain: false,
572 frame: 3,
573 };
574 let world = Rect {
575 x: 5.0,
576 y: 6.0,
577 width: 20.0,
578 height: 10.0,
579 };
580 let stats = composite_tile(&mut tile, &obs, world, 5, 6, 0, 0);
581 assert_eq!(stats.added, 200);
582 assert!(stats.changed && stats.conflict_blocks.is_empty());
583 assert_eq!(tile.owner[0], 4);
584 assert_eq!(tile.pixels[(6 * size + 5) * 4], 0);
585 assert_eq!(tile.pixels[(6 * size + 6) * 4], 1);
586 }
587}