Skip to main content

long_screen/kernels/
consistency.rs

1//! World-consistency mask for one moving-region placement (docs/ARCHITECTURE.md §七).
2//!
3//! Two kinds of evidence, deliberately asymmetric in strength:
4//! - a multi-frame voting verdict (`Vote`) says *which* frame is wrong and gets the last word;
5//! - the ±1-frame native comparison is a pairwise disagreement signal, so it condemns this frame only
6//!   when evidence is symmetric: every comparable neighbour disagrees, or the lone disagreeing neighbour
7//!   carries its own negative verdict.
8
9use crate::kernels::geometry::{js_ceil, js_floor, js_round, Rect};
10use crate::kernels::pool::SyncPtr;
11
12/// Packed analysis-resolution voting verdicts in region-local box coordinates.
13pub struct Vote<'a> {
14    pub x0: i32,
15    pub y0: i32,
16    pub w: i32,
17    pub h: i32,
18    /// Bit set: inconsistent.
19    pub bits: &'a [u8],
20    /// Bit set: confidently consistent (only consulted when `bits` is clear).
21    pub clean: &'a [u8],
22}
23
24impl Vote<'_> {
25    /// True when some cell `ax0..=ax1` of analysis row `ay` carries a negative verdict.
26    #[inline]
27    pub fn any_negative(&self, ax0: i32, ax1: i32, ay: i32) -> bool {
28        (ax0..=ax1).any(|ax| self.verdict(ax, ay) < 0)
29    }
30
31    /// −1 inconsistent, +1 confidently consistent, 0 no verdict; `(ax, ay)` are analysis cells.
32    #[inline]
33    pub fn verdict(&self, ax: i32, ay: i32) -> i32 {
34        let lx = ax - self.x0;
35        let ly = ay - self.y0;
36        if lx < 0 || ly < 0 || lx >= self.w || ly >= self.h {
37            return 0;
38        }
39        let i = (ly * self.w + lx) as usize;
40        let bit = 1u8 << (i & 7);
41        if self.bits.get(i >> 3).is_some_and(|b| b & bit != 0) {
42            -1
43        } else if self.clean.get(i >> 3).is_some_and(|b| b & bit != 0) {
44            1
45        } else {
46            0
47        }
48    }
49}
50
51pub struct Neighbour<'a> {
52    pub rgba: &'a [u8],
53    pub pose_x: f64,
54    pub pose_y: f64,
55    pub occlusions: &'a [Rect],
56    pub vote: Option<Vote<'a>>,
57}
58
59pub struct MaskInput<'a> {
60    pub rgba: &'a [u8],
61    pub labels: &'a [u8],
62    pub width: usize,
63    pub height: usize,
64    pub region: Rect,
65    pub code: u8,
66    pub pose_x: f64,
67    pub pose_y: f64,
68    pub prev: Option<Neighbour<'a>>,
69    pub next: Option<Neighbour<'a>>,
70    pub vote: Option<Vote<'a>>,
71    pub factor: i32,
72    pub noise: f64,
73}
74
75struct Resolved<'n, 'a> {
76    rgba: &'a [u8],
77    dx: i64,
78    dy: i64,
79    occlusions: &'a [Rect],
80    vote: Option<&'n Vote<'a>>,
81}
82
83fn resolve<'n, 'a>(n: &'n Neighbour<'a>, cx: i32, cy: i32) -> Resolved<'n, 'a> {
84    Resolved {
85        rgba: n.rgba,
86        dx: (cx - js_round(n.pose_x)) as i64,
87        dy: (cy - js_round(n.pose_y)) as i64,
88        occlusions: n.occlusions,
89        vote: n.vote.as_ref(),
90    }
91}
92
93/// Integer x-interval `[a, b)` of pixels `ix` with `ix >= x && ix < x + width`, for a real rect edge.
94#[inline]
95fn span(x: f64, width: f64) -> (i64, i64) {
96    (x.ceil() as i64, (x + width).ceil() as i64)
97}
98
99/// Largest RGB channel-difference sum `s` with `s / 3 <= noise` (the historical float test, evaluated for every
100/// possible sum so fractional noise rounds exactly as before); 0 when only identical pixels pass.
101fn sum_limit(noise: f64) -> u32 {
102    (0..=765u32)
103        .take_while(|&s| s as f64 / 3.0 <= noise)
104        .last()
105        .unwrap_or(0)
106}
107
108/// Frame-wide constants shared by every row.
109struct Ctx<'n, 'a> {
110    input: &'n MaskInput<'a>,
111    neighbours: [Option<Resolved<'n, 'a>>; 2],
112    w: i64,
113    h: i64,
114    rx0: i64,
115    rx1: i64,
116    factor: i64,
117    limit: u32,
118}
119
120/// One neighbour's view of the current row.
121struct NeighbourRow<'r, 'n, 'a> {
122    n: &'r Resolved<'n, 'a>,
123    labels: &'a [u8],
124    rgba: &'a [u8],
125    /// Current-frame x in `lo..hi` maps to a neighbour pixel inside the frame.
126    lo: i64,
127    hi: i64,
128    /// Analysis row of the neighbour pixel, for its vote lookup.
129    ay: i32,
130    /// Neighbour row coordinate, for the (rare) occlusion test.
131    fy: f64,
132    /// Some occlusion band intersects this neighbour row.
133    occluded: bool,
134}
135
136impl NeighbourRow<'_, '_, '_> {
137    #[inline]
138    fn occludes(&self, sx: i64) -> bool {
139        self.occluded
140            && self.n.occlusions.iter().any(|o| {
141                if !(self.fy >= o.y && self.fy < o.y + o.height) {
142                    return false;
143                }
144                let (a, b) = span(o.x, o.width);
145                sx >= a - self.n.dx && sx < b - self.n.dx
146            })
147    }
148}
149
150/// Writes one byte per native pixel into `out` (1 = consistent). Pixels outside the region stay 1 and are
151/// never read by the compositor, which gates on region membership first. Rows are independent, so they are
152/// split across the pool; each row is computed identically whichever thread runs it.
153pub fn consistency_mask(input: &MaskInput<'_>, out: &mut [u8]) {
154    let (w, h) = (input.width as i64, input.height as i64);
155    let (cx, cy) = (js_round(input.pose_x), js_round(input.pose_y));
156    let ctx = Ctx {
157        input,
158        neighbours: [
159            input.prev.as_ref().map(|n| resolve(n, cx, cy)),
160            input.next.as_ref().map(|n| resolve(n, cx, cy)),
161        ],
162        w,
163        h,
164        rx0: js_floor(input.region.x).clamp(0, w as i32) as i64,
165        rx1: js_ceil(input.region.x + input.region.width).clamp(0, w as i32) as i64,
166        factor: input.factor.max(1) as i64,
167        limit: sum_limit(input.noise),
168    };
169    let ry0 = js_floor(input.region.y).clamp(0, h as i32) as usize;
170    let ry1 = js_ceil(input.region.y + input.region.height).clamp(0, h as i32) as usize;
171    let width = input.width;
172    let out = &mut out[..input.width * input.height];
173    // Rows outside the region are only filled; the pool splits the region rows.
174    out[..ry0 * width].fill(1);
175    out[ry1.max(ry0) * width..].fill(1);
176    let rows = ry1.saturating_sub(ry0);
177    let chunks = crate::kernels::pool::chunks_for(rows * width, 64 * 1024);
178    let base = SyncPtr(out.as_mut_ptr());
179    crate::kernels::pool::par_for(chunks, |c| {
180        let (a, b) = (
181            ry0 + crate::kernels::pool::split(rows, chunks, c),
182            ry0 + crate::kernels::pool::split(rows, chunks, c + 1),
183        );
184        for sy in a..b {
185            // SAFETY: chunks cover disjoint row ranges of `out`, which outlives the parallel call.
186            let row = unsafe { std::slice::from_raw_parts_mut(base.get().add(sy * width), width) };
187            row.fill(1);
188            mask_row(&ctx, sy as i64, row);
189        }
190    });
191}
192
193fn mask_row(ctx: &Ctx<'_, '_>, sy: i64, out_row: &mut [u8]) {
194    let input = ctx.input;
195    let (w, h, factor) = (ctx.w, ctx.h, ctx.factor);
196    let width = input.width;
197    let labels = input.labels;
198    let ay = sy.div_euclid(factor) as i32;
199    let mut rows: [Option<NeighbourRow<'_, '_, '_>>; 2] = [None, None];
200    let mut count = 0usize;
201    for n in ctx.neighbours.iter().flatten() {
202        let iy = sy + n.dy;
203        if iy < 0 || iy >= h {
204            continue;
205        }
206        let base = (iy * w) as usize;
207        let fy = iy as f64;
208        rows[count] = Some(NeighbourRow {
209            n,
210            labels: &labels[base..base + width],
211            rgba: &n.rgba[base * 4..(base + width) * 4],
212            lo: (-n.dx).max(ctx.rx0),
213            hi: (w - n.dx).min(ctx.rx1),
214            ay: iy.div_euclid(factor) as i32,
215            fy,
216            occluded: n
217                .occlusions
218                .iter()
219                .any(|o| fy >= o.y && fy < o.y + o.height),
220        });
221        count += 1;
222    }
223    let rows = &rows;
224    let row_vote = input
225        .vote
226        .as_ref()
227        .filter(|v| ay >= v.y0 && ay < v.y0 + v.h);
228    let base = (sy * w) as usize;
229    let lab_row = &labels[base..base + width];
230    let rgb_row = &input.rgba[base * 4..(base + width) * 4];
231    let mut sx = ctx.rx0;
232    // Fast path: blocks of four pixels where every present neighbour row applies unoccluded to the whole block
233    // and no negative vote cell covers it. A block whose pixels all agree needs no write (the row is prefilled
234    // with 1); any other block is re-evaluated pixel by pixel below, exactly as the scalar rule.
235    let (fast_lo, fast_hi) = rows
236        .iter()
237        .flatten()
238        .fold((ctx.rx0, ctx.rx1), |(lo, hi), r| {
239            if r.occluded {
240                (lo, lo)
241            } else {
242                (lo.max(r.lo), hi.min(r.hi))
243            }
244        });
245    while sx < ctx.rx1 {
246        if sx >= fast_lo && sx + 4 <= fast_hi {
247            let votes_clear = row_vote.is_none_or(|v| {
248                !v.any_negative(
249                    sx.div_euclid(factor) as i32,
250                    (sx + 3).div_euclid(factor) as i32,
251                    ay,
252                )
253            });
254            if votes_clear && block_agrees(ctx, rows, lab_row, rgb_row, sx as usize) {
255                sx += 4;
256                continue;
257            }
258            for x in sx..sx + 4 {
259                mask_pixel(ctx, rows, row_vote, ay, lab_row, rgb_row, out_row, x);
260            }
261            sx += 4;
262            continue;
263        }
264        mask_pixel(ctx, rows, row_vote, ay, lab_row, rgb_row, out_row, sx);
265        sx += 1;
266    }
267}
268
269/// True when none of the four pixels at `x` can be flagged by the neighbour rule: each is outside the region,
270/// or every neighbour pixel is outside the region or within the noise sum of it. The caller has established
271/// that all neighbour rows apply, unoccluded, across the block.
272#[inline]
273fn block_agrees(
274    ctx: &Ctx<'_, '_>,
275    rows: &[Option<NeighbourRow<'_, '_, '_>>; 2],
276    lab_row: &[u8],
277    rgb_row: &[u8],
278    x: usize,
279) -> bool {
280    let code = ctx.input.code;
281    #[cfg(target_feature = "simd128")]
282    {
283        use core::arch::wasm32::*;
284        let _ = code;
285        // SAFETY: `x + 4 <= fast_hi <= width` for the current row and `lo..hi` keeps `x + dx` inside each
286        // neighbour row, so every 16-byte pixel load and 4-byte label load is in bounds.
287        unsafe {
288            let codes = u32x4_splat(ctx.input.code as u32);
289            let labels4 = |row: &[u8], at: usize| {
290                let v = v128_load32_zero(row.as_ptr().add(at) as *const u32);
291                u32x4_extend_low_u16x8(u16x8_extend_low_u8x16(v))
292            };
293            let inside = u32x4_eq(labels4(lab_row, x), codes);
294            if !v128_any_true(inside) {
295                return true;
296            }
297            let a = v128_load(rgb_row.as_ptr().add(x * 4) as *const v128);
298            let limit = u32x4_splat(ctx.limit);
299            let rgb = u32x4_splat(0x00ff_ffff);
300            let mut bad = u32x4_splat(0);
301            for r in rows.iter().flatten() {
302                let ix = (x as i64 + r.n.dx) as usize;
303                let b = v128_load(r.rgba.as_ptr().add(ix * 4) as *const v128);
304                let d = v128_and(v128_or(u8x16_sub_sat(a, b), u8x16_sub_sat(b, a)), rgb);
305                let sum = u32x4_extadd_pairwise_u16x8(u16x8_extadd_pairwise_u8x16(d));
306                let differs = u32x4_gt(sum, limit);
307                let counted = u32x4_eq(labels4(r.labels, ix), codes);
308                bad = v128_or(bad, v128_and(differs, counted));
309            }
310            !v128_any_true(v128_and(bad, inside))
311        }
312    }
313    #[cfg(not(target_feature = "simd128"))]
314    {
315        (x..x + 4).all(|x| {
316            lab_row[x] != code
317                || rows.iter().flatten().all(|r| {
318                    let ix = (x as i64 + r.n.dx) as usize;
319                    if r.labels[ix] != code {
320                        return true;
321                    }
322                    let (i, j) = (x * 4, ix * 4);
323                    let sum = (rgb_row[i] as i32 - r.rgba[j] as i32).unsigned_abs()
324                        + (rgb_row[i + 1] as i32 - r.rgba[j + 1] as i32).unsigned_abs()
325                        + (rgb_row[i + 2] as i32 - r.rgba[j + 2] as i32).unsigned_abs();
326                    sum <= ctx.limit
327                })
328        })
329    }
330}
331
332/// The per-pixel rule (see the module docs), unchanged from the historical kernel.
333#[allow(clippy::too_many_arguments)]
334#[inline]
335fn mask_pixel(
336    ctx: &Ctx<'_, '_>,
337    rows: &[Option<NeighbourRow<'_, '_, '_>>; 2],
338    row_vote: Option<&Vote<'_>>,
339    ay: i32,
340    lab_row: &[u8],
341    rgb_row: &[u8],
342    out_row: &mut [u8],
343    sx: i64,
344) {
345    let code = ctx.input.code;
346    let factor = ctx.factor;
347    let x = sx as usize;
348    if lab_row[x] != code {
349        return;
350    }
351    if row_vote.is_some_and(|v| v.verdict(sx.div_euclid(factor) as i32, ay) < 0) {
352        out_row[x] = 0;
353        return;
354    }
355    let k = x * 4;
356    let (r, g, b) = (
357        rgb_row[k] as i32,
358        rgb_row[k + 1] as i32,
359        rgb_row[k + 2] as i32,
360    );
361    let (mut checked, mut condemned, mut excused) = (0u32, false, 0u32);
362    for row in rows.iter().flatten() {
363        // Each placement is rasterised at its own rounded pose; the difference of rounded poses is what the
364        // compositor actually uses, not the rounded difference.
365        if sx < row.lo || sx >= row.hi {
366            continue;
367        }
368        let ix = (sx + row.n.dx) as usize;
369        if row.labels[ix] != code || row.occludes(sx) {
370            continue;
371        }
372        checked += 1;
373        let j = ix * 4;
374        let (nr, ng, nb) = (
375            row.rgba[j] as i32,
376            row.rgba[j + 1] as i32,
377            row.rgba[j + 2] as i32,
378        );
379        if r == nr && g == ng && b == nb {
380            continue;
381        }
382        let diff = ((r - nr).abs() + (g - ng).abs() + (b - nb).abs()) as f64 / 3.0;
383        if diff <= ctx.input.noise {
384            continue;
385        }
386        if row
387            .n
388            .vote
389            .is_some_and(|v| v.verdict((ix as i64).div_euclid(factor) as i32, row.ay) < 0)
390        {
391            excused += 1;
392        } else {
393            condemned = true;
394            break;
395        }
396    }
397    if condemned || (excused > 0 && checked >= 2) {
398        out_row[x] = 0;
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    fn frame(w: usize, h: usize, value: u8) -> Vec<u8> {
407        (0..w * h)
408            .flat_map(|_| [value, value, value, 255])
409            .collect()
410    }
411
412    #[test]
413    fn lone_disagreeing_neighbour_condemns_unless_excused_by_its_verdict() {
414        let (w, h) = (4usize, 4usize);
415        let current = frame(w, h, 10);
416        let other = frame(w, h, 200);
417        let labels = vec![1u8; w * h];
418        let region = Rect {
419            x: 0.0,
420            y: 0.0,
421            width: 4.0,
422            height: 4.0,
423        };
424        let mut out = vec![0u8; w * h];
425        let base = |vote| MaskInput {
426            rgba: &current,
427            labels: &labels,
428            width: w,
429            height: h,
430            region,
431            code: 1,
432            pose_x: 0.0,
433            pose_y: 0.0,
434            prev: Some(Neighbour {
435                rgba: &other,
436                pose_x: 0.0,
437                pose_y: 0.0,
438                occlusions: &[],
439                vote,
440            }),
441            next: None,
442            vote: None,
443            factor: 1,
444            noise: 10.0,
445        };
446        consistency_mask(&base(None), &mut out);
447        assert!(out.iter().all(|&v| v == 0));
448        let bits = vec![0xffu8; 2];
449        let clean = vec![0u8; 2];
450        let excused = base(Some(Vote {
451            x0: 0,
452            y0: 0,
453            w: 4,
454            h: 4,
455            bits: &bits,
456            clean: &clean,
457        }));
458        consistency_mask(&excused, &mut out);
459        assert!(out.iter().all(|&v| v == 1));
460    }
461}