Skip to main content

long_screen/kernels/
fixed_history.rs

1//! What one screen-fixed region showed at the start of the recording, without its transients (docs/ARCHITECTURE.md
2//! §七 "屏幕固定区域").
3//!
4//! The render pass keeps, for every pixel the region owns, its value in the first frame and the runs of equal
5//! values (within the source noise) that follow, until the region's first settled change of state ends that first
6//! epoch. The value a pixel held longest in the epoch is what the bar showed then; where it differs from the first
7//! frame, the first frame showed a transient (a pointer parked there at the start, a bar still fading in). Those
8//! differences are only trusted in pointer-sized patches: a large difference is content changing under the bar,
9//! and the first frame is kept there as it was.
10
11use crate::kernels::geometry::{js_ceil, js_floor, Rect};
12use crate::kernels::pool::SyncPtr;
13
14/// Cell side, in pixels, of the grid the differences are grouped on.
15const CELL: usize = 8;
16/// Differences closer than this many cells belong to one patch: the glyphs of a line of text, an icon and its
17/// label, stay together.
18const REACH: usize = 4;
19/// A patch is a transient when its differing pixels fit in this many pixels across and down: a pointer (about 36 × 54
20/// on a 2× screen) does, together with the hover highlight of the button under it (owner_1.53.20's frame 0: 341 ×
21/// 53 with the Incognito pill); a bar that changed, or content scrolling under it, does not.
22const PATCH_W: usize = 384;
23const PATCH: usize = 128;
24/// Cells around a transient patch that take their longest-held values too.
25const HALO: usize = 2;
26
27#[inline]
28fn px(rgba: &[u8], i: usize) -> u32 {
29    u32::from_le_bytes([
30        rgba[i * 4],
31        rgba[i * 4 + 1],
32        rgba[i * 4 + 2],
33        rgba[i * 4 + 3],
34    ])
35}
36
37/// RGB channel-difference sum of two packed pixels.
38#[inline]
39fn distance(a: u32, b: u32) -> u32 {
40    let (a, b) = (a.to_le_bytes(), b.to_le_bytes());
41    a[0].abs_diff(b[0]) as u32 + a[1].abs_diff(b[1]) as u32 + a[2].abs_diff(b[2]) as u32
42}
43
44/// Per-pixel run history of one fixed region, laid out over its floored rect.
45pub struct FixedHistory {
46    x0: i64,
47    y0: i64,
48    w: usize,
49    h: usize,
50    /// Pixels the region owns inside the frame.
51    inside: Vec<bool>,
52    frame0: Vec<u32>,
53    current: Vec<u32>,
54    start: Vec<u32>,
55    longest: Vec<u32>,
56    length: Vec<u32>,
57    /// Frame the first values came from; `None` until the first update.
58    first_frame: Option<u32>,
59    /// The first epoch is over; later updates are ignored.
60    closed: bool,
61}
62
63impl FixedHistory {
64    pub fn new(region: Rect) -> Self {
65        let (x0, y0) = (js_floor(region.x) as i64, js_floor(region.y) as i64);
66        let w = (js_ceil(region.x + region.width) as i64 - x0).max(0) as usize;
67        let h = (js_ceil(region.y + region.height) as i64 - y0).max(0) as usize;
68        let n = w * h;
69        Self {
70            x0,
71            y0,
72            w,
73            h,
74            inside: vec![false; n],
75            frame0: vec![0; n],
76            current: vec![0; n],
77            start: vec![0; n],
78            longest: vec![0; n],
79            length: vec![0; n],
80            first_frame: None,
81            closed: false,
82        }
83    }
84
85    /// Bytes the history holds.
86    pub fn bytes(region: Rect) -> f64 {
87        let w = (js_ceil(region.x + region.width) - js_floor(region.x)).max(0) as f64;
88        let h = (js_ceil(region.y + region.height) - js_floor(region.y)).max(0) as f64;
89        w * h * 21.0
90    }
91
92    /// The frame whose values [`FixedHistory::first`] starts from.
93    pub fn first_frame(&self) -> Option<u32> {
94        self.first_frame
95    }
96
97    pub fn closed(&self) -> bool {
98        self.closed
99    }
100
101    /// The first frame's values, packed RGBA over the region's rect (0 where the region owns no pixel).
102    pub fn frame0(&self) -> &[u32] {
103        &self.frame0
104    }
105
106    /// Takes frame `frame`'s pixels (`rgba`, `width × height`, atlas `labels`, the region's `code`, the region's
107    /// consistency mask `consistent`): a pixel that moved beyond `limit` (the RGB difference sum the source noise
108    /// allows) from its current run ends that run. When the pixels that settled into a new value (consistent with a
109    /// neighbouring frame) are more than a pointer could cover, the region changed state and the first epoch ends
110    /// here. Returns that count of settled pixels.
111    #[allow(clippy::too_many_arguments)]
112    pub fn update(
113        &mut self,
114        rgba: &[u8],
115        labels: &[u8],
116        consistent: &[u8],
117        width: usize,
118        height: usize,
119        code: u8,
120        frame: u32,
121        limit: u32,
122    ) -> usize {
123        if self.closed {
124            return 0;
125        }
126        let first = self.first_frame.is_none();
127        if first {
128            self.first_frame = Some(frame);
129        }
130        let (w, h, x0, y0) = (self.w, self.h, self.x0, self.y0);
131        let chunks = crate::kernels::pool::chunks_for(w * h, 64 * 1024);
132        let mut counts = vec![0usize; chunks];
133        let ptrs = (
134            SyncPtr(self.inside.as_mut_ptr()),
135            SyncPtr(self.frame0.as_mut_ptr()),
136            SyncPtr(self.current.as_mut_ptr()),
137            SyncPtr(self.start.as_mut_ptr()),
138            SyncPtr(self.longest.as_mut_ptr()),
139            SyncPtr(self.length.as_mut_ptr()),
140            SyncPtr(counts.as_mut_ptr()),
141        );
142        crate::kernels::pool::par_for(chunks, |c| {
143            let mut settled = 0usize;
144            for y in crate::kernels::pool::split(h, chunks, c)
145                ..crate::kernels::pool::split(h, chunks, c + 1)
146            {
147                let sy = y0 + y as i64;
148                if sy < 0 || sy >= height as i64 {
149                    continue;
150                }
151                for x in 0..w {
152                    let sx = x0 + x as i64;
153                    if sx < 0 || sx >= width as i64 {
154                        continue;
155                    }
156                    let src = sy as usize * width + sx as usize;
157                    let i = y * w + x;
158                    // SAFETY: row `y` of every plane belongs to this chunk alone, and so does count `c`.
159                    unsafe {
160                        if first {
161                            if labels[src] != code {
162                                continue;
163                            }
164                            let v = px(rgba, src);
165                            *ptrs.0.get().add(i) = true;
166                            *ptrs.1.get().add(i) = v;
167                            *ptrs.2.get().add(i) = v;
168                            *ptrs.3.get().add(i) = frame;
169                            *ptrs.4.get().add(i) = v;
170                            continue;
171                        }
172                        if !*ptrs.0.get().add(i) {
173                            continue;
174                        }
175                        let v = px(rgba, src);
176                        let cur = *ptrs.2.get().add(i);
177                        if distance(v, cur) > limit {
178                            let run = frame - *ptrs.3.get().add(i);
179                            if run > *ptrs.5.get().add(i) {
180                                *ptrs.4.get().add(i) = cur;
181                                *ptrs.5.get().add(i) = run;
182                            }
183                            *ptrs.2.get().add(i) = v;
184                            *ptrs.3.get().add(i) = frame;
185                            if consistent[src] != 0 {
186                                settled += 1;
187                            }
188                        }
189                    }
190                }
191            }
192            // SAFETY: one count per chunk.
193            unsafe { *ptrs.6.get().add(c) = settled };
194        });
195        let settled: usize = counts.iter().sum();
196        let owned = self.inside.iter().filter(|&&b| b).count();
197        if !first && settled > (PATCH * PATCH).min(owned / 4) {
198            self.close(frame);
199        }
200        settled
201    }
202
203    /// Ends the first epoch before frame `end`: every current run is counted up to it, and later updates are
204    /// ignored.
205    pub fn close(&mut self, end: u32) {
206        if self.closed || self.first_frame.is_none() {
207            self.closed = true;
208            return;
209        }
210        for i in 0..self.inside.len() {
211            if !self.inside[i] {
212                continue;
213            }
214            let run = end.saturating_sub(self.start[i]);
215            if run > self.length[i] {
216                self.longest[i] = self.current[i];
217                self.length[i] = run;
218            }
219        }
220        self.closed = true;
221    }
222
223    /// The first frame's pixels with its transients replaced (call after [`FixedHistory::close`]): where the value a
224    /// pixel held longest in the first epoch differs from the first frame by more than `limit`, inside a patch no
225    /// larger than a pointer, the longest-held value; elsewhere the first frame. Packed RGBA over the region's rect;
226    /// 0 where the region owns no pixel.
227    pub fn first(&self, limit: u32) -> Vec<u32> {
228        let (w, h) = (self.w, self.h);
229        let mut out = self.frame0.clone();
230        let differs: Vec<bool> = (0..w * h)
231            .map(|i| self.inside[i] && distance(self.longest[i], self.frame0[i]) > limit)
232            .collect();
233        let (gw, gh) = (w.div_ceil(CELL), h.div_ceil(CELL));
234        let mut cells = vec![false; gw * gh];
235        for y in 0..h {
236            for x in 0..w {
237                if differs[y * w + x] {
238                    cells[(y / CELL) * gw + x / CELL] = true;
239                }
240            }
241        }
242        // Group the differing cells: two cells within REACH of each other belong to one patch.
243        let mut label = vec![usize::MAX; gw * gh];
244        let mut patches: Vec<(usize, usize, usize, usize)> = Vec::new();
245        for seed in 0..gw * gh {
246            if !cells[seed] || label[seed] != usize::MAX {
247                continue;
248            }
249            let id = patches.len();
250            let (sx, sy) = (seed % gw, seed / gw);
251            let mut bounds = (sx, sy, sx, sy);
252            label[seed] = id;
253            let mut queue = vec![seed];
254            while let Some(c) = queue.pop() {
255                let (cx, cy) = (c % gw, c / gw);
256                bounds = (
257                    bounds.0.min(cx),
258                    bounds.1.min(cy),
259                    bounds.2.max(cx),
260                    bounds.3.max(cy),
261                );
262                for ny in cy.saturating_sub(REACH)..(cy + REACH + 1).min(gh) {
263                    for nx in cx.saturating_sub(REACH)..(cx + REACH + 1).min(gw) {
264                        let n = ny * gw + nx;
265                        if cells[n] && label[n] == usize::MAX {
266                            label[n] = id;
267                            queue.push(n);
268                        }
269                    }
270                }
271            }
272            patches.push(bounds);
273        }
274        let small: Vec<bool> = patches
275            .iter()
276            .map(|&(x0, y0, x1, y1)| {
277                (x1 - x0 + 1) * CELL <= PATCH_W && (y1 - y0 + 1) * CELL <= PATCH
278            })
279            .collect();
280        // A transient's soft edge (a pointer's shadow) differs from the bar by less than the noise allows; around a
281        // transient patch, out to HALO cells, every pixel takes the value it held longest.
282        let mut fix = vec![false; gw * gh];
283        for c in 0..gw * gh {
284            if !cells[c] || !small[label[c]] {
285                continue;
286            }
287            let (cx, cy) = (c % gw, c / gw);
288            for ny in cy.saturating_sub(HALO)..(cy + HALO + 1).min(gh) {
289                for nx in cx.saturating_sub(HALO)..(cx + HALO + 1).min(gw) {
290                    let n = ny * gw + nx;
291                    // A cell of a large patch is content changing, halo or not.
292                    if !cells[n] || small[label[n]] {
293                        fix[n] = true;
294                    }
295                }
296            }
297        }
298        for y in 0..h {
299            for x in 0..w {
300                let i = y * w + x;
301                if self.inside[i] && fix[(y / CELL) * gw + x / CELL] {
302                    out[i] = self.longest[i];
303                }
304            }
305        }
306        out
307    }
308
309    /// Writes [`FixedHistory::first`] into a frame-sized `rgba` at the region's pixels; returns whether any byte
310    /// changed.
311    pub fn paint(&self, first: &[u32], rgba: &mut [u8], width: usize, height: usize) -> bool {
312        let mut changed = false;
313        for y in 0..self.h {
314            let sy = self.y0 + y as i64;
315            if sy < 0 || sy >= height as i64 {
316                continue;
317            }
318            for x in 0..self.w {
319                let sx = self.x0 + x as i64;
320                let i = y * self.w + x;
321                if sx < 0 || sx >= width as i64 || !self.inside[i] {
322                    continue;
323                }
324                let at = (sy as usize * width + sx as usize) * 4;
325                let v = first[i].to_le_bytes();
326                if rgba[at..at + 4] != v {
327                    rgba[at..at + 4].copy_from_slice(&v);
328                    changed = true;
329                }
330            }
331        }
332        changed
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    const W: usize = 480;
341    const H: usize = 160;
342
343    fn region() -> Rect {
344        Rect {
345            x: 0.0,
346            y: 0.0,
347            width: W as f64,
348            height: H as f64,
349        }
350    }
351
352    fn frame(v: u8) -> Vec<u8> {
353        [v, v, v, 255].repeat(W * H)
354    }
355
356    fn fill(mut f: Vec<u8>, x: usize, y: usize, w: usize, h: usize, v: u8) -> Vec<u8> {
357        for yy in y..y + h {
358            for xx in x..x + w {
359                let i = (yy * W + xx) * 4;
360                f[i..i + 3].copy_from_slice(&[v, v, v]);
361            }
362        }
363        f
364    }
365
366    /// Every frame through a history whose consistency mask is `consistent` everywhere.
367    fn run(frames: &[Vec<u8>], labels: &[u8], consistent: u8) -> Vec<u32> {
368        let mut hist = FixedHistory::new(region());
369        let mask = vec![consistent; W * H];
370        for (i, f) in frames.iter().enumerate() {
371            hist.update(f, labels, &mask, W, H, 1, i as u32, 0);
372        }
373        hist.close(frames.len() as u32);
374        hist.first(0)
375    }
376
377    fn at(first: &[u32], x: usize, y: usize) -> u8 {
378        first[y * W + x].to_le_bytes()[0]
379    }
380
381    #[test]
382    fn a_pointer_parked_in_the_first_frame_is_replaced_by_what_it_covered() {
383        // Parked for 3 frames at the start, gone for the next 10.
384        let pointer = || fill(frame(100), 20, 10, 18, 27, 250);
385        let mut frames = vec![pointer(), pointer(), pointer()];
386        frames.extend((0..10).map(|_| frame(100)));
387        let first = run(&frames, &[1; W * H], 1);
388        assert_eq!(at(&first, 25, 20), 100);
389        assert_eq!(at(&first, 100, 20), 100);
390    }
391
392    #[test]
393    fn a_change_after_the_first_epoch_keeps_the_first_frame() {
394        // Hidden for 4 frames, then shown for 20, the change a subtle one (a background 20 levels lighter, under
395        // any conflict threshold) but settled over the whole bar: the epoch ends there, so the first frame's state
396        // is what the region showed then.
397        let mut frames: Vec<_> = (0..4).map(|_| frame(30)).collect();
398        frames.extend((0..20).map(|_| frame(50)));
399        let first = run(&frames, &[1; W * H], 1);
400        assert_eq!(at(&first, 100, 20), 30);
401    }
402
403    #[test]
404    fn a_large_difference_is_content_and_keeps_the_first_frame() {
405        // Never consistent, so the epoch never ends: a large area that held another value longer is still not a
406        // transient.
407        let mut frames = vec![frame(0)];
408        frames.extend((0..20).map(|_| fill(frame(0), 0, 0, 400, 40, 200)));
409        let first = run(&frames, &[1; W * H], 0);
410        assert_eq!(at(&first, 50, 20), 0);
411    }
412
413    #[test]
414    fn text_glyphs_close_together_form_one_patch() {
415        // Thirty 6-px glyphs 14 px apart that all scroll away: 420 px wide together, so the first frame stays.
416        let mut text = frame(255);
417        for k in 0..30 {
418            text = fill(text, 10 + k * 14, 15, 6, 10, 0);
419        }
420        let mut frames = vec![text.clone(), text];
421        frames.extend((0..20).map(|_| frame(255)));
422        let first = run(&frames, &[1; W * H], 0);
423        assert_eq!(at(&first, 12, 20), 0);
424    }
425
426    #[test]
427    fn pixels_the_region_does_not_own_are_left_alone() {
428        let labels: Vec<u8> = (0..W * H)
429            .map(|i| if i % W < 100 { 1 } else { 2 })
430            .collect();
431        let mut frames = vec![fill(frame(100), 150, 10, 10, 10, 250)];
432        frames.extend((0..10).map(|_| frame(100)));
433        let mut hist = FixedHistory::new(region());
434        for (i, f) in frames.iter().enumerate() {
435            hist.update(f, &labels, &[1; W * H], W, H, 1, i as u32, 0);
436        }
437        hist.close(frames.len() as u32);
438        let mut out = frames[0].clone();
439        let first = hist.first(0);
440        hist.paint(&first, &mut out, W, H);
441        assert_eq!(out, frames[0]);
442    }
443}