Skip to main content

long_screen/kernels/
compositor.rs

1//! Pixel-ownership compositing of one placement into one tile (docs/ARCHITECTURE.md §六–七).
2//!
3//! This is the per-tile body of `Compositor.add()`: block-level conflict/replace decisions, coverage and
4//! provisional bookkeeping, and the pixel writes themselves. Tile-cache traversal, temporal-conflict
5//! records and diagnostics stay with the adapter, which owns storage.
6
7use crate::kernels::geometry::{js_ceil, js_round, Rect};
8use crate::kernels::pool::SyncPtr;
9
10pub const QUALITY_BLOCK: usize = 16;
11
12/// Mutable view of one resident tile's buffers.
13pub 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    /// Atlas label plane; `None` when the region is rectangular and fully owned (fast path).
30    pub labels: Option<(&'a [u8], u8)>,
31    pub occlusions: &'a [Rect],
32    /// Per-pixel world-consistency mask (1 = consistent); `None` treats every pixel as consistent.
33    pub consistent: Option<&'a [u8]>,
34    pub confidence: f64,
35    pub uncertain: bool,
36    pub frame: u32,
37    /// A screen-fixed region: every consistent pixel replaces what the block holds, whatever the block scores and
38    /// whether it conflicts, so the canvas keeps each pixel's latest consistent value (docs/ARCHITECTURE.md §十
39    /// "合成与瞬态像素"). A moving region (`false`) replaces a block only on a better score and leaves conflicts to the
40    /// temporal records.
41    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    /// For a fixed region (`Observation::latest`): conflicting blocks whose every pixel is consistent, i.e. blocks
52    /// that settled into a new state in this frame.
53    pub settled_blocks: u32,
54    /// Blocks flagged conflicting this call, as tile-local (bx, by).
55    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
84/// Tile geometry shared by every block of one call.
85struct Geometry<'o, 'a> {
86    obs: &'o Observation<'a>,
87    world: Rect,
88    size: i64,
89    blocks: i64,
90    /// Tile origin minus observation origin: `sx = x + dx`, `sy = y + dy` for tile-local (x, y).
91    dx: i64,
92    dy: i64,
93    w: i64,
94    h: i64,
95    tile_x: f64,
96    tile_y: f64,
97}
98
99/// Stats one chunk of block rows contributes; folded in chunk order.
100#[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
110/// The block rows `by0..by1` of a tile, as disjoint sub-slices (row `by0` is local row 0).
111struct 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    /// This call's conflict flag per block of these rows.
121    flagged: &'t mut [u8],
122    by0: i64,
123}
124
125/// Composites the part of `world` (region rect at its integer raster pose) that falls into tile (tx, ty).
126/// Blocks are independent (each owns its pixels, coverage bits and block entries), so rows of blocks are split
127/// across the pool; every count is an integer sum and the conflict list is rebuilt in (by, bx) order afterwards.
128#[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        // SAFETY: chunk `c` owns block rows `a..a + n` exclusively; each buffer is row-major with the strides
203        // below, so these sub-slices are disjoint across chunks and in bounds of the tile.
204        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        // SAFETY: one slot per chunk.
249        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/// Integer x-range `[lo, hi)` of tile-local columns in `sx0..sx1` whose source column lies inside the frame.
272#[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/// Some occlusion band covers source row `sy` (the per-pixel test is only needed then).
278#[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/// Overlap statistics of one full 16-pixel block row: (count, overlap, identical, mismatch).
285#[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        // SAFETY: the caller established 16 in-frame source pixels starting at `src0`.
298        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        // SAFETY: 16 tile pixels at `dst0` and 16 source pixels at `src0` are in bounds (see caller).
309        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/// `row_stats_simd` without WebAssembly SIMD: the same per-pixel masks built with plain loops over the block row's
330/// 16 pixels, which the compiler can unroll without the general loop's per-pixel label option, occlusion test and
331/// bounds checks. A channel sum above 75 is exactly the general loop's `sum / 3.0 > 25.0` for an integer sum.
332#[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    // Tile-local pixel index of (x, y) in these rows.
387    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    // Exact whole-block reproduction is corroboration, not a write; but standing provisional bits still need
452    // healing, so only skip when there is nothing to clear either.
453    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    // Integer gradient sum: every term is a small integer, so the f64 total the score uses is exact either way.
463    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                // Fresh pixels are always written; an inconsistent observation never overwrites covered content;
538                // a covered provisional pixel is healed by any consistent observation; otherwise the ordinary
539                // replace gate decides.
540                let write = if fresh {
541                    true
542                } else if bad {
543                    false
544                } else {
545                    was_provisional || replace
546                };
547                if !write {
548                    // A rejected observation that reproduces what is stored condemns the stored pixel too.
549                    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    // Block evidence is persisted with the tile, so any change to it must dirty the tile like a pixel write does;
581    // otherwise whether it reaches storage depends on some later write happening before a flush or eviction.
582    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    // Failure cases for the block-row fast path: a label outside the region counted, a coverage bit read from the
611    // wrong byte or bit order, an identical pixel also counted as a mismatch, a channel sum of exactly 75 (a third
612    // of it is not above 25) counted as a mismatch, alpha ignored in "identical", and rows away from offset 0.
613    #[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                    // Identical, identical but alpha, and channel sums of exactly 75 and 76.
635                    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}