Skip to main content

long_screen/kernels/reveal/
stats.rs

1//! Per-layer screen statistics: how each 2×2 screen cell's observation compares with the canvas content at
2//! the same world position, accumulated over every distinct pose.
3//!
4//! A cell over page content agrees with the canvas whatever scrolls beneath it. A cell under an opaque overlay
5//! keeps disagreeing with a stable colour of its own; a cell under a translucent veil passes an attenuated copy
6//! of the page (observed luma ≈ a·page + b with a < 1). The luma moments give that regression slope; the sums
7//! over disagreeing observations give the overlay's appearance (its template).
8
9use super::{luma, CanvasView, Footprint, Frame, CELL};
10
11/// World-aligned 2×2 cell means of a canvas: the reference the statistics compare against.
12pub trait CellRef {
13    /// Mean colour of world cell (cx, cy) (world pixels 2cx..2cx+1, 2cy..2cy+1), if the whole cell is known.
14    fn cell(&self, cx: i64, cy: i64) -> Option<[u8; 3]>;
15    /// World cell rectangle that may hold known cells: (cx0, cy0, cx1, cy1), half-open.
16    fn cells(&self) -> (i64, i64, i64, i64);
17}
18
19/// A [`CellRef`] built once from canvas pixels.
20pub struct CellMap {
21    pub cx0: i64,
22    pub cy0: i64,
23    pub cw: usize,
24    pub ch: usize,
25    /// RGB + known flag per cell.
26    pub rgbk: Vec<[u8; 4]>,
27}
28
29impl CellMap {
30    /// An empty map over the world cells covering the rectangle (x, y, width, height).
31    pub fn covering(x: i64, y: i64, width: usize, height: usize) -> Self {
32        let c = CELL as i64;
33        let (cx0, cy0) = (x.div_euclid(c), y.div_euclid(c));
34        let (cx1, cy1) = (
35            (x + width as i64 + c - 1).div_euclid(c),
36            (y + height as i64 + c - 1).div_euclid(c),
37        );
38        let (cw, ch) = ((cx1 - cx0).max(0) as usize, (cy1 - cy0).max(0) as usize);
39        Self {
40            cx0,
41            cy0,
42            cw,
43            ch,
44            rgbk: vec![[0u8; 4]; cw * ch],
45        }
46    }
47
48    /// Fills the cells lying wholly inside `view` whose four pixels are covered. Tiles are world-aligned on even
49    /// coordinates, so a canvas fed tile by tile fills every wholly covered cell exactly once.
50    pub fn ingest(&mut self, view: &CanvasView<'_>) {
51        let c = CELL as i64;
52        let x0 = ((view.x + c - 1).div_euclid(c)).max(self.cx0);
53        let y0 = ((view.y + c - 1).div_euclid(c)).max(self.cy0);
54        let x1 = ((view.x + view.width as i64).div_euclid(c)).min(self.cx0 + self.cw as i64);
55        let y1 = ((view.y + view.height as i64).div_euclid(c)).min(self.cy0 + self.ch as i64);
56        for cy in y0..y1 {
57            for cx in x0..x1 {
58                let (wx, wy) = (cx * c, cy * c);
59                let mut s = [0u32; 3];
60                let mut ok = true;
61                'px: for dy in 0..c {
62                    for dx in 0..c {
63                        match view.covered(wx + dx, wy + dy) {
64                            Some(i) => {
65                                for (k, v) in s.iter_mut().enumerate() {
66                                    *v += view.rgba[i * 4 + k] as u32;
67                                }
68                            }
69                            None => {
70                                ok = false;
71                                break 'px;
72                            }
73                        }
74                    }
75                }
76                if ok {
77                    let q = (CELL * CELL) as u32;
78                    self.rgbk[(cy - self.cy0) as usize * self.cw + (cx - self.cx0) as usize] = [
79                        ((s[0] + q / 2) / q) as u8,
80                        ((s[1] + q / 2) / q) as u8,
81                        ((s[2] + q / 2) / q) as u8,
82                        1,
83                    ];
84                }
85            }
86        }
87    }
88
89    pub fn from_canvas(view: &CanvasView<'_>) -> Self {
90        let mut m = Self::covering(view.x, view.y, view.width, view.height);
91        m.ingest(view);
92        m
93    }
94}
95
96impl CellRef for CellMap {
97    #[inline]
98    fn cell(&self, cx: i64, cy: i64) -> Option<[u8; 3]> {
99        let (x, y) = (cx - self.cx0, cy - self.cy0);
100        if x < 0 || y < 0 || x >= self.cw as i64 || y >= self.ch as i64 {
101            return None;
102        }
103        let v = self.rgbk[y as usize * self.cw + x as usize];
104        (v[3] != 0).then_some([v[0], v[1], v[2]])
105    }
106    fn cells(&self) -> (i64, i64, i64, i64) {
107        (
108            self.cx0,
109            self.cy0,
110            self.cx0 + self.cw as i64,
111            self.cy0 + self.ch as i64,
112        )
113    }
114}
115
116/// Accumulators of one screen cell.
117#[derive(Clone, Copy, Default)]
118pub struct Acc {
119    /// Comparisons and disagreements.
120    pub n: u16,
121    pub bad: u16,
122    /// Luma moments of (reference r, observation o): Σr, Σo, Σr², Σro, Σo².
123    pub sr: u32,
124    pub so: u32,
125    pub srr: u32,
126    pub sro: u32,
127    pub soo: u32,
128    /// Over disagreeing observations only: Σ RGB and Σ luma, Σ luma² (the overlay's appearance and spread).
129    pub br: u32,
130    pub bg: u32,
131    pub bb: u32,
132    pub bl: u32,
133    pub bll: u32,
134}
135
136impl Acc {
137    #[inline]
138    fn add(&mut self, o: [u32; 3], r: [u32; 3], tau: i32) {
139        if self.n == u16::MAX {
140            return;
141        }
142        let lo = luma(o[0] as u8, o[1] as u8, o[2] as u8);
143        let lr = luma(r[0] as u8, r[1] as u8, r[2] as u8);
144        let d = (o[0] as i32 - r[0] as i32)
145            .abs()
146            .max((o[1] as i32 - r[1] as i32).abs())
147            .max((o[2] as i32 - r[2] as i32).abs());
148        self.n += 1;
149        self.sr += lr;
150        self.so += lo;
151        self.srr += lr * lr;
152        self.sro += lr * lo;
153        self.soo += lo * lo;
154        if d > tau {
155            self.bad += 1;
156            self.br += o[0];
157            self.bg += o[1];
158            self.bb += o[2];
159            self.bl += lo;
160            self.bll += lo * lo;
161        }
162    }
163}
164
165#[derive(Clone)]
166pub struct LayerStats {
167    /// Screen origin of the layer rectangle (native pixels) and its size in cells.
168    pub x: i64,
169    pub y: i64,
170    pub cw: usize,
171    pub ch: usize,
172    pub cells: Vec<Acc>,
173}
174
175impl LayerStats {
176    pub fn new(x: i64, y: i64, width: usize, height: usize) -> Self {
177        let (cw, ch) = (width.div_ceil(CELL), height.div_ceil(CELL));
178        Self {
179            x,
180            y,
181            cw,
182            ch,
183            cells: vec![Acc::default(); cw * ch],
184        }
185    }
186
187    /// One field of every cell, widened.
188    pub fn field(&self, f: impl Fn(&Acc) -> u32) -> Vec<u32> {
189        self.cells.iter().map(f).collect()
190    }
191
192    /// Adds one observation against world-aligned reference cells: every world cell whose four screen pixels
193    /// (world − pose) are in the footprint and whose reference is known. The observation is binned on the same
194    /// world-aligned grid, so an odd pose never compares across a sub-cell phase; it is credited to the screen
195    /// cell holding the world cell's top-left pixel (one world cell per screen cell, so rows of world cells
196    /// write disjoint rows of screen cells and run in parallel).
197    pub fn accumulate_cells<R: CellRef + Sync + ?Sized>(
198        &mut self,
199        frame: &Frame<'_>,
200        footprint: &Footprint<'_>,
201        reference: &R,
202        pose: (i64, i64),
203        tau: i32,
204    ) -> usize {
205        let (px, py) = pose;
206        let c = CELL as i64;
207        let (rx0, ry0, rx1, ry1) = reference.cells();
208        let wx0 = (footprint.x + px).div_euclid(c).max(rx0);
209        let wy0 = (footprint.y + py).div_euclid(c).max(ry0);
210        let wx1 = (footprint.x + footprint.width as i64 + px + c - 1)
211            .div_euclid(c)
212            .min(rx1);
213        let wy1 = (footprint.y + footprint.height as i64 + py + c - 1)
214            .div_euclid(c)
215            .min(ry1);
216        if wx1 <= wx0 || wy1 <= wy0 {
217            return 0;
218        }
219        let rows = (wy1 - wy0) as usize;
220        let chunks = crate::kernels::pool::chunks_for(rows * (wx1 - wx0) as usize, 8192);
221        let mut counts = vec![0usize; chunks];
222        let cells = crate::kernels::pool::SyncPtr(self.cells.as_mut_ptr());
223        let counted = crate::kernels::pool::SyncPtr(counts.as_mut_ptr());
224        let (lx0, ly0, cw, ch) = (self.x, self.y, self.cw as i64, self.ch as i64);
225        let (w, h) = (frame.width as i64, frame.height as i64);
226        let (fx0, fy0, fw, fh) = (
227            footprint.x,
228            footprint.y,
229            footprint.width as i64,
230            footprint.height as i64,
231        );
232        crate::kernels::pool::par_for(chunks, |k| {
233            let (a, b) = (
234                crate::kernels::pool::split(rows, chunks, k),
235                crate::kernels::pool::split(rows, chunks, k + 1),
236            );
237            let mut compared = 0usize;
238            for r in a..b {
239                let cy = wy0 + r as i64;
240                let sy = cy * c - py;
241                let (ly, my) = ((sy - ly0).div_euclid(c), sy - fy0);
242                if sy < 0 || sy + c > h || ly < 0 || ly >= ch || my < 0 || my + c > fh {
243                    continue;
244                }
245                let (m0, m1) = (
246                    &footprint.mask[(my * fw) as usize..((my + 1) * fw) as usize],
247                    &footprint.mask[((my + 1) * fw) as usize..((my + 2) * fw) as usize],
248                );
249                let (f0, f1) = (
250                    &frame.rgba[(sy * w * 4) as usize..((sy + 1) * w * 4) as usize],
251                    &frame.rgba[((sy + 1) * w * 4) as usize..((sy + 2) * w * 4) as usize],
252                );
253                // The cells of this row inside the frame, the layer's cell grid and the footprint rectangle, found
254                // once: sx = 2cx - px, lx = cx - k (the layer cell holding sx), mx = sx - fx0.
255                let k = (px + lx0 + 1).div_euclid(c);
256                let lo = [wx0, (px + 1).div_euclid(c), k, (fx0 + px + 1).div_euclid(c)];
257                let hi = [
258                    wx1,
259                    (w - c + px).div_euclid(c) + 1,
260                    cw + k,
261                    (fx0 + fw - c + px).div_euclid(c) + 1,
262                ];
263                let (cx_lo, cx_hi) = (lo.into_iter().max().unwrap(), hi.into_iter().min().unwrap());
264                for cx in cx_lo..cx_hi.max(cx_lo) {
265                    let sx = cx * c - px;
266                    let (lx, mx) = (cx - k, (sx - fx0) as usize);
267                    if m0[mx] == 0 || m0[mx + 1] == 0 || m1[mx] == 0 || m1[mx + 1] == 0 {
268                        continue;
269                    }
270                    let Some(rv) = reference.cell(cx, cy) else {
271                        continue;
272                    };
273                    let i = sx as usize * 4;
274                    let mut o = [0u32; 3];
275                    for (k, v) in o.iter_mut().enumerate() {
276                        *v = (f0[i + k] as u32
277                            + f0[i + 4 + k] as u32
278                            + f1[i + k] as u32
279                            + f1[i + 4 + k] as u32
280                            + 2)
281                            / 4;
282                    }
283                    // SAFETY: world row `cy` maps to screen-cell row `ly` alone, and chunks own disjoint world rows.
284                    let acc = unsafe { &mut *cells.get().add((ly * cw + lx) as usize) };
285                    acc.add(o, [rv[0] as u32, rv[1] as u32, rv[2] as u32], tau);
286                    compared += 1;
287                }
288            }
289            // SAFETY: each chunk writes its own slot.
290            unsafe { *counted.get().add(k) = compared };
291        });
292        counts.iter().sum()
293    }
294}