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}
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    /// Blocks flagged conflicting this call, as tile-local (bx, by).
47    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
76/// Tile geometry shared by every block of one call.
77struct Geometry<'o, 'a> {
78    obs: &'o Observation<'a>,
79    world: Rect,
80    size: i64,
81    blocks: i64,
82    /// Tile origin minus observation origin: `sx = x + dx`, `sy = y + dy` for tile-local (x, y).
83    dx: i64,
84    dy: i64,
85    w: i64,
86    h: i64,
87    tile_x: f64,
88    tile_y: f64,
89}
90
91/// Stats one chunk of block rows contributes; folded in chunk order.
92#[derive(Default, Clone, Copy)]
93struct Part {
94    added: u32,
95    conflicts: u32,
96    uncertain: u32,
97    provisional_delta: i32,
98    changed: bool,
99}
100
101/// The block rows `by0..by1` of a tile, as disjoint sub-slices (row `by0` is local row 0).
102struct 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    /// This call's conflict flag per block of these rows.
112    flagged: &'t mut [u8],
113    by0: i64,
114}
115
116/// Composites the part of `world` (region rect at its integer raster pose) that falls into tile (tx, ty).
117/// Blocks are independent (each owns its pixels, coverage bits and block entries), so rows of blocks are split
118/// across the pool; every count is an integer sum and the conflict list is rebuilt in (by, bx) order afterwards.
119#[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        // SAFETY: chunk `c` owns block rows `a..a + n` exclusively; each buffer is row-major with the strides
194        // below, so these sub-slices are disjoint across chunks and in bounds of the tile.
195        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        // SAFETY: one slot per chunk.
240        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/// Integer x-range `[lo, hi)` of tile-local columns in `sx0..sx1` whose source column lies inside the frame.
262#[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/// Some occlusion band covers source row `sy` (the per-pixel test is only needed then).
268#[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/// Overlap statistics of one full 16-pixel block row: (count, overlap, identical, mismatch).
275#[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        // SAFETY: the caller established 16 in-frame source pixels starting at `src0`.
288        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        // SAFETY: 16 tile pixels at `dst0` and 16 source pixels at `src0` are in bounds (see caller).
299        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    // Tile-local pixel index of (x, y) in these rows.
336    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    // Exact whole-block reproduction is corroboration, not a write; but standing provisional bits still need
399    // healing, so only skip when there is nothing to clear either.
400    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    // Integer gradient sum: every term is a small integer, so the f64 total the score uses is exact either way.
410    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                // Fresh pixels are always written; an inconsistent observation never overwrites covered content;
465                // a covered provisional pixel is healed by any consistent observation; otherwise the ordinary
466                // replace gate decides.
467                let write = if fresh {
468                    true
469                } else if bad {
470                    false
471                } else {
472                    was_provisional || replace
473                };
474                if !write {
475                    // A rejected observation that reproduces what is stored condemns the stored pixel too.
476                    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    // Block evidence is persisted with the tile, so any change to it must dirty the tile like a pixel write does;
508    // otherwise whether it reaches storage depends on some later write happening before a flush or eviction.
509    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}