Skip to main content

ui/
dirty.rs

1// dirty -- dirty regions: only redraw and present what changed        [src/ui/ui_dirty.s]
2//
3//   dirty_compute(w, h) -> n         number of dirty rectangles (Ui::drects: x, y, w, h,
4//                                    clipped to w x h), -1 = everything.  Hashes the
5//                                    recorded commands of every layer into 32 x 32 px cells
6//                                    and compares with the previous frame.
7//   dirty_force_full()               the next dirty_compute reports "everything"
8//   dirty_free()                     forget the live window's cells (a closed window)
9//   canvas_damage(id, x, y, w, h)    (U1) explicit damage: the part x, y, w, h (px,
10//                                    relative to the canvas) of canvas `id` is redrawn with
11//                                    the next render even when its command did not change
12//   dmg_canvas_seen(id)              canvas() remembers its rectangle and window
13//
14// A command's hash covers all of its content (colours, coordinates, text, a custom draw
15// function's identity and version) and the clip it is drawn in, so any visible change
16// marks its cells.  The same pass fills the content grids of the scroll views
17// (scrollcopy.rs): dirty cells whose pixels a scrolled view still shows elsewhere are copied
18// there before the rectangles are drawn.
19//
20// While a window's frames go through the GPU (gpu_out.rs, W::split) the pass also keeps the hash
21// of every cell after each layer (W::through: what changed in which layer) and notes the big
22// images of the base layer and which cells they end up on top of (W::top, W::imgs).  A canvas whose
23// view comes from tiles (canvas.rs, W::canv) is noted the same way; its hash stands only for where
24// it lies: what it shows there is the GPU's.
25
26use alloc::vec;
27use alloc::vec::Vec;
28
29use super::cmd::{cmd_bbox, cmd_parts, pos_mix, Cmd};
30use super::consts::L_COUNT;
31use super::Ui;
32
33const CELL_SHIFT: u32 = 5; // 32 px cells
34const CELL: i32 = 32;
35/// dirty rectangles of a render; more: everything is redrawn (the same pixels, only slower)
36pub(crate) const DRECTS_MAX: usize = 128;
37/// a frame with a scroll copy may draw this many rectangles more than without it (each one
38/// replays the commands)
39const COPY_RECTS_MORE: usize = 16;
40const HASH_K: u64 = 0x9E37_79B9_7F4A_7C15;
41/// clip levels tracked while hashing (as ctx::UICLIP_MAX); deeper ones clip nothing, which
42/// only marks more cells
43const DCLIP_MAX: usize = super::ctx::UICLIP_MAX;
44/// pending damage rectangles (all windows); more: merged into one of the same window
45const DMG_MAX: usize = 32;
46// (canvases whose rectangle is known: Ui::cvs grows; a record not drawn in the last two
47// passes is reused, so it holds at most the canvases of two passes and none is forgotten
48// while it is on the screen)
49
50/// a pending damage rectangle: window, x0, y0, x1, y1 (px)
51#[derive(Clone, Copy, Default)]
52pub(crate) struct Dmg {
53    win: u32,
54    x0: i32,
55    y0: i32,
56    x1: i32,
57    y1: i32,
58}
59
60/// images of the base layer from this many visible pixels on are noted for the GPU output
61pub(crate) const IMG_MIN_PX: i64 = 256 * 256;
62/// W::top: more than an image touches the cell, or something after it
63pub(crate) const TOP_BLOCKED: u8 = 255;
64
65/// a big image of the base layer in this frame: its record, the part of it the clips leave
66/// (x0, y0, x1, y1)
67#[derive(Clone, Copy)]
68#[cfg_attr(not(any(windows, jfgui_glibc, target_os = "android", target_arch = "wasm32")), allow(dead_code))] // (read by gpu_out.rs)
69pub(crate) struct ImgSeen {
70    pub(crate) index: u32,
71    pub(crate) vis: [i32; 4],
72}
73
74/// a canvas whose rectangle is known: id (0 = free), window, frame, rect
75#[derive(Clone, Copy, Default)]
76pub(crate) struct Cvs {
77    id: u32,
78    win: u32,
79    frame: u32,
80    rect: [i32; 4],
81}
82
83/// the per-window part (ui_dirty_wb0 .. ui_dirty_wb1)
84#[derive(Default)]
85pub(crate) struct W {
86    pub(crate) cells: Option<Vec<u64>>,
87    pub(crate) cells_prev: Option<Vec<u64>>,
88    pub(crate) cols: u32,
89    pub(crate) rows: u32,
90    pub(crate) full: bool,
91    /// (gpu_out.rs) the GPU output keeps the layers apart: the hashes through each layer, the
92    /// big images
93    pub(crate) split: bool,
94    /// (gpu_out.rs) the last frame had layers or images apart: no scroll copies
95    pub(crate) busy: bool,
96    /// this frame: everything was asked for, or explicit damage went into a full redraw
97    pub(crate) forced: bool,
98    /// the rectangles of drects before the explicit damage
99    pub(crate) n0: i32,
100    /// the hashes through layer l, for the layers below a layer with commands (empty: as through
101    /// l + 1), of this frame and the last
102    pub(crate) pre: [Vec<u64>; L_COUNT - 1],
103    pub(crate) pre_prev: [Vec<u64>; L_COUNT - 1],
104    /// per cell: 0 nothing yet, k the k-th image of imgs is on top, TOP_BLOCKED
105    pub(crate) top: Vec<u8>,
106    pub(crate) imgs: Vec<ImgSeen>,
107    /// (gpu_out.rs) the canvases of the base layer apart in this frame: command, rectangle
108    pub(crate) canv: Vec<(u32, [i32; 4])>,
109}
110
111impl W {
112    /// (after dirty_compute, whose swap made cells_prev this frame's) the hashes through layer l
113    /// of this frame or of the last one
114    #[cfg_attr(not(any(windows, jfgui_glibc, target_os = "android", target_arch = "wasm32")), allow(dead_code))] // (read by gpu_out.rs)
115    pub(crate) fn through(&self, now: bool, l: usize) -> &[u64] {
116        let (pre, all) = if now { (&self.pre_prev, &self.cells_prev) } else { (&self.pre, &self.cells) };
117        match pre[l.min(L_COUNT - 1)..].iter().find(|k| !k.is_empty()) {
118            Some(k) => k,
119            None => all.as_deref().unwrap_or(&[]),
120        }
121    }
122}
123
124// the clip tracked while hashing (C_CLIP / C_UNCLIP of a layer)
125struct DClip {
126    c: [i32; 4],
127    stack: Vec<[i32; 4]>,
128    /// levels beyond DCLIP_MAX (they clip nothing)
129    over: u32,
130}
131
132impl DClip {
133    // dclip_reset: no clip (start of a layer's command list)
134    fn new() -> DClip {
135        DClip { c: [-0x4000_0000, -0x4000_0000, 0x4000_0000, 0x4000_0000], stack: Vec::new(), over: 0 }
136    }
137
138    // dclip_push: save the current clip (at most DCLIP_MAX levels), intersect it
139    fn push(&mut self, r: [i32; 4]) {
140        if self.stack.len() >= DCLIP_MAX {
141            self.over += 1;
142            return;
143        }
144        self.stack.push(self.c);
145        let c = &mut self.c;
146        c[0] = c[0].max(r[0]);
147        c[1] = c[1].max(r[1]);
148        c[2] = c[2].min(r[0].wrapping_add(r[2]));
149        c[3] = c[3].min(r[1].wrapping_add(r[3]));
150    }
151
152    fn pop(&mut self) {
153        if self.over != 0 {
154            self.over -= 1;
155        } else if let Some(c) = self.stack.pop() {
156            self.c = c;
157        }
158    }
159
160    // dclip_cut: bb (x0, y0, x1, y1) intersected with the clip -> still has area
161    fn cut(&self, bb: &mut [i32; 4]) -> bool {
162        bb[0] = bb[0].max(self.c[0]);
163        bb[1] = bb[1].max(self.c[1]);
164        bb[2] = bb[2].min(self.c[2]);
165        bb[3] = bb[3].min(self.c[3]);
166        bb[2] > bb[0] && bb[3] > bb[1]
167    }
168}
169
170// rects_build(out, cols, rows, dirty) -> the number of dirty cells (dirty(row * cols + col)),
171// as rectangles (px) in out: a run of dirty cells in a row extends a rectangle of the row above
172// with the same columns; None when more than DRECTS_MAX
173fn rects_build(out: &mut Vec<[i32; 4]>, cols: i32, rows: i32, dirty: impl Fn(usize) -> bool) -> Option<u32> {
174    out.clear();
175    let mut cells = 0u32;
176    for row in 0..rows {
177        let at = |col: i32| dirty((row * cols + col) as usize);
178        let mut col = 0;
179        while col < cols {
180            if !at(col) {
181                col += 1;
182                continue;
183            }
184            // a run of dirty cells [col, end)
185            let mut end = col + 1;
186            while end < cols && at(end) {
187                end += 1;
188            }
189            cells += (end - col) as u32;
190            // extend a rectangle from the row above with the same columns
191            let (x, w, y) = (col << CELL_SHIFT, (end - col) << CELL_SHIFT, row << CELL_SHIFT);
192            if let Some(r) = out.iter_mut().find(|r| r[0] == x && r[2] == w && r[1] + r[3] == y) {
193                r[3] += CELL;
194            } else if out.len() < DRECTS_MAX {
195                out.push([x, y, w, CELL]);
196            } else {
197                return None;
198            }
199            col = end;
200        }
201    }
202    Some(cells)
203}
204
205impl Ui {
206    /// dirty_force_full: the next frame redraws and presents everything
207    pub fn dirty_force_full(&mut self) {
208        self.wp.dirty.full = true;
209    }
210
211    /// dirty_free: release the live window's cell arrays (a closed window)
212    #[allow(dead_code)] // ui-win
213    pub(crate) fn dirty_free(&mut self) {
214        let d = &mut self.wp.dirty;
215        d.cells = None;
216        d.cells_prev = None;
217        d.cols = 0;
218        d.rows = 0;
219        d.full = true;
220    }
221
222    // cells_ensure(w, h): (re)allocate both cell arrays for this size
223    fn cells_ensure(&mut self, w: i32, h: i32) {
224        let cols = (w as u32).wrapping_add(CELL as u32 - 1) >> CELL_SHIFT;
225        let rows = (h as u32).wrapping_add(CELL as u32 - 1) >> CELL_SHIFT;
226        let d = &mut self.wp.dirty;
227        if cols == d.cols && rows == d.rows {
228            return;
229        }
230        d.cols = cols;
231        d.rows = rows;
232        let n = cols as usize * rows as usize;
233        d.cells = Some(vec![0; n]);
234        d.cells_prev = Some(vec![0; n]);
235        d.pre = Default::default();
236        d.pre_prev = Default::default();
237        d.full = true;
238    }
239
240    /// dmg_canvas_seen(id): remember the rectangle (last_rect) and window of canvas id (a
241    /// new id takes the record seen longest ago when that canvas was not drawn in the last
242    /// two passes, else a new record)
243    pub(crate) fn dmg_canvas_seen(&mut self, id: u32) {
244        let e = Cvs { id, win: self.uiw_cur, frame: self.frame, rect: self.wp.ctx.last_rect };
245        let (mut old, mut age) = (0, 0u32);
246        for (i, c) in self.cvs.iter_mut().enumerate() {
247            if c.id == id {
248                *c = e;
249                return;
250            }
251            let a = self.frame.wrapping_sub(c.frame);
252            if a > age {
253                (old, age) = (i, a);
254            }
255        }
256        if age >= 2 {
257            self.cvs[old] = e;
258        } else {
259            self.cvs.push(e);
260        }
261    }
262
263    /// ui_canvas_damage(id, x, y, w, h): redraw part of a canvas with the next render (px
264    /// relative to the canvas, cut to it)
265    pub fn canvas_damage(&mut self, id: u32, x: i32, y: i32, w: i32, h: i32) {
266        if id == 0 {
267            return;
268        }
269        // never emitted: its first render draws it all
270        let Some(e) = self.cvs.iter().find(|e| e.id == id).copied() else { return };
271        let r = e.rect;
272        let x0 = x.wrapping_add(r[0]).max(r[0]);
273        let x1 = w.wrapping_add(x.wrapping_add(r[0])).min(r[0].wrapping_add(r[2]));
274        let y0 = y.wrapping_add(r[1]).max(r[1]);
275        let y1 = h.wrapping_add(y.wrapping_add(r[1])).min(r[1].wrapping_add(r[3]));
276        if x0 >= x1 || y0 >= y1 {
277            return;
278        }
279        let full = self.dmg.len() >= DMG_MAX;
280        if let Some(d) = self.dmg.iter_mut().rev().find(|d| d.win == e.win).filter(|_| full) {
281            // full: grow the last entry of the same window to the union
282            d.x0 = d.x0.min(x0);
283            d.y0 = d.y0.min(y0);
284            d.x1 = d.x1.max(x1);
285            d.y1 = d.y1.max(y1);
286        } else {
287            self.dmg.push(Dmg { win: e.win, x0, y0, x1, y1 }); // (full: the window's first)
288        }
289        self.invalidate();
290    }
291
292    // dmg_take(n, w, h) -> n + the damage rectangles of the live window appended to drects
293    // (cut to the w x h window), -1 when they do not fit; the window's entries are removed
294    fn dmg_take(&mut self, n: i32, w: i32, h: i32) -> i32 {
295        let mut n = n;
296        let cur = self.uiw_cur;
297        let mut keep = Vec::new();
298        for d in core::mem::take(&mut self.dmg) {
299            if d.win != cur {
300                keep.push(d);
301                continue;
302            }
303            if n < 0 {
304                continue; // already overflowed
305            }
306            if n as usize >= DRECTS_MAX {
307                n = -1;
308                continue;
309            }
310            let x = d.x0.max(0);
311            let rw = d.x1.min(w).wrapping_sub(x);
312            let y = d.y0.max(0);
313            let rh = d.y1.min(h).wrapping_sub(y);
314            if rw > 0 && rh > 0 {
315                self.drects.push([x, y, rw, rh]);
316                n += 1;
317            }
318        }
319        self.dmg = keep;
320        n
321    }
322
323    /// dirty_compute(w, h) -> rectangles in drects, or -1 for a full redraw.  Scroll views whose
324    /// content only moved copy their still valid cells first (scrollcopy.rs).
325    pub(crate) fn dirty_compute(&mut self, w: i32, h: i32) -> i32 {
326        self.cells_ensure(w, h);
327        {
328            let d = &mut self.wp.dirty;
329            d.forced = d.full;
330            d.top.clear();
331            d.imgs.clear();
332            for k in d.pre.iter_mut() {
333                k.clear();
334            }
335        }
336        if self.gp_canvas_wanted() {
337            self.gp_canvas_begin(); // which canvases come from tiles in this frame (gpu_out.rs)
338        }
339        let mut sc = self.sc_pass(w, h);
340        if self.wp.dirty.cells.is_none() || self.wp.dirty.cells_prev.is_none() {
341            self.sc_plan(sc, 0, 0, false);
342            return self.dirty_full_take(w, h);
343        }
344        let cols = self.wp.dirty.cols as i32;
345        let rows = self.wp.dirty.rows as i32;
346        // ---- hash every command into its cells (and into the scroll views' content grids)
347        {
348            let Ui { wp, .. } = self;
349            let d = &mut wp.dirty;
350            let split = cfg!(any(windows, jfgui_glibc, target_os = "android", target_arch = "wasm32")) && d.split; // (only there: nothing of it in other targets)
351            let cells = d.cells.as_mut().unwrap();
352            cells.fill(0);
353            let grids = sc.on();
354            for (l, layer) in wp.cmd.layers.iter().enumerate() {
355                if split && l > 0 && !layer.is_empty() {
356                    d.pre[l - 1].extend_from_slice(cells);
357                }
358                let mut dc = DClip::new();
359                for (i, c) in layer.iter().enumerate() {
360                    if grids {
361                        sc.reach(l, i, dc.c, dc.over == 0);
362                    }
363                    match c {
364                        Cmd::Clip { x, y, w, h } => {
365                            dc.push([*x, *y, *w, *h]);
366                            if grids {
367                                sc.clip(l, i, [*x, *y, x.wrapping_add(*w), y.wrapping_add(*h)]);
368                            }
369                            continue;
370                        }
371                        Cmd::Unclip => {
372                            dc.pop();
373                            if grids {
374                                sc.unclip(l, i);
375                            }
376                            continue;
377                        }
378                        _ => {}
379                    }
380                    let Some(bb0) = cmd_bbox(c) else { continue };
381                    // only the part inside the clip can change pixels
382                    let mut bb = bb0;
383                    let vis = dc.cut(&mut bb);
384                    if !vis && !grids {
385                        continue;
386                    }
387                    // hash of the record and of the clip (a moved clip edge changes pixels
388                    // although the command stayed the same)
389                    let parts = cmd_parts(c);
390                    let mut hh = pos_mix(parts.0, parts.1 as i64, parts.2 as i64);
391                    hh ^= (dc.c[0] as u32 as u64) | (dc.c[1] as u32 as u64) << 32;
392                    hh = hh.wrapping_mul(HASH_K);
393                    hh ^= (dc.c[2] as u32 as u64) | (dc.c[3] as u32 as u64) << 32;
394                    hh = hh.wrapping_mul(HASH_K);
395                    // a canvas apart: the part of its rectangle the clips leave stands for it
396                    let mut apart = None;
397                    if split && l == 0 && !d.canv.is_empty() && dc.over == 0 && d.imgs.len() < TOP_BLOCKED as usize - 1 && matches!(c, Cmd::Custom { .. }) {
398                        if let Some(&(_, r)) = d.canv.iter().find(|x| x.0 == i as u32) {
399                            let v = [bb[0].max(r[0]), bb[1].max(r[1]), bb[2].min(r[0] + r[2]), bb[3].min(r[1] + r[3])];
400                            hh = pos_mix(0x4341_4E56, v[0] as i64, v[1] as i64) ^ ((v[2] as u32 as u64) | (v[3] as u32 as u64) << 32).wrapping_mul(HASH_K);
401                            apart = Some(v);
402                        }
403                    }
404                    if grids {
405                        sc.cmd(l, i, c, parts, bb0, dc.c, dc.over == 0, hh);
406                    }
407                    if !vis {
408                        continue;
409                    }
410                    // cell range (clamped)
411                    let c0 = (bb[0] >> CELL_SHIFT).max(0);
412                    let c1 = ((bb[2] - 1) >> CELL_SHIFT).min(cols - 1);
413                    let r0 = (bb[1] >> CELL_SHIFT).max(0);
414                    let r1 = ((bb[3] - 1) >> CELL_SHIFT).min(rows - 1);
415                    // (the GPU output) the big images of the base layer and what lies over them
416                    let mut tag = 0;
417                    let seen = match apart {
418                        Some(v) => (v[0] < v[2] && v[1] < v[3]).then_some(v),
419                        None => (matches!(c, Cmd::Image { .. }) && (bb[2] - bb[0]) as i64 * (bb[3] - bb[1]) as i64 >= IMG_MIN_PX).then_some(bb),
420                    };
421                    if let Some(vis) = seen.filter(|_| split && l == 0 && dc.over == 0 && d.imgs.len() < TOP_BLOCKED as usize - 1) {
422                        if d.top.is_empty() {
423                            d.top.resize(cols as usize * rows as usize, 0);
424                        }
425                        d.imgs.push(ImgSeen { index: i as u32, vis });
426                        tag = d.imgs.len() as u8;
427                    }
428                    let track = l == 0 && !d.top.is_empty();
429                    let mut r = r0;
430                    while r <= r1 {
431                        let mut k = c0;
432                        while k <= c1 {
433                            let i = (r * cols + k) as usize;
434                            cells[i] = (cells[i].rotate_left(7) ^ hh).wrapping_mul(HASH_K);
435                            if track {
436                                let t = &mut d.top[i];
437                                *t = if *t != 0 { TOP_BLOCKED } else { tag };
438                            }
439                            k += 1;
440                        }
441                        r += 1;
442                    }
443                }
444            }
445        }
446        // ---- compare with the previous frame, build rectangles
447        let pending = self.dmg.iter().any(|d| d.win == self.uiw_cur);
448        // the GPU output's images of this frame; scroll copies move pixels of the framebuffer, which
449        // keeps no separate layers and may lack an image's pixels
450        let split = cfg!(any(windows, jfgui_glibc, target_os = "android", target_arch = "wasm32")) && self.wp.dirty.split;
451        let apart = split && {
452            let quads = (!self.wp.dirty.imgs.is_empty() || self.wp.dirty.busy) && {
453                let images = self.gp_plan_images();
454                self.gp_plan_canvases() | images
455            };
456            quads || self.wp.dirty.busy || self.wp.cmd.layers[1..].iter().any(|l| !l.is_empty())
457        };
458        let allowed = !self.wp.dirty.full && !pending && !apart;
459        let mut plan = self.sc_plan(sc, cols, rows, allowed);
460        let want = plan.as_ref().is_some_and(|p| self.sc_want(p));
461        // (checked frames copy whenever they can, and so does a finger's scroll: touch.rs)
462        let force = self.verify_dirty || self.m.touch.on;
463        let (built, copy) = {
464            let Ui { wp, drects, .. } = &mut *self;
465            let d = &wp.dirty;
466            let (cur, prev) = (d.cells.as_ref().unwrap(), d.cells_prev.as_ref().unwrap());
467            let changed = |i: usize| cur[i] != prev[i];
468            let mut built = rects_build(drects, cols, rows, changed);
469            // a scroll copy (scrollcopy.rs) provides the cells it covers: when the view's costs
470            // favour it (sc_want), it takes over at least a quarter of what would be drawn
471            // without it and does not split the rest into many more rectangles (each one
472            // replays the commands); frames checked by --verify-dirty take any copy
473            let mut copy = false;
474            if let Some(p) = plan.as_mut().filter(|_| want) {
475                let copied = p.settle(cols as usize, changed);
476                let total = cols as u32 * rows as u32;
477                let draw = built.filter(|&n| n * 5 <= total * 3).unwrap_or(total);
478                let mut alt = Vec::new();
479                let cv = &p.covered;
480                if let Some(n) = rects_build(&mut alt, cols, rows, |i| changed(i) && cv[i] == 0) {
481                    if copied > 0 && (force || (copied * 4 >= draw as usize && alt.len() <= drects.len() + COPY_RECTS_MORE)) {
482                        *drects = alt;
483                        built = Some(n);
484                        copy = true;
485                    }
486                }
487            }
488            (built, copy)
489        };
490        if !copy {
491            plan = None;
492        }
493        let Some(dirty_cells) = built else {
494            self.dirty_swap();
495            return self.dirty_full_take(w, h);
496        };
497        // (U1) explicit damage (canvas_damage) of this window
498        let n0 = self.drects.len() as i32;
499        self.wp.dirty.n0 = n0;
500        let n = self.dmg_take(n0, w, h);
501        if n < 0 {
502            self.wp.dirty.forced = true;
503            self.dirty_swap();
504            return self.dirty_full_take(w, h);
505        }
506        // clip the rectangles to the window
507        for r in self.drects.iter_mut() {
508            let mw = w.wrapping_sub(r[0]);
509            if r[2] > mw {
510                r[2] = mw;
511            }
512            let mh = h.wrapping_sub(r[1]);
513            if r[3] > mh {
514                r[3] = mh;
515            }
516        }
517        // more than 60 % of the cells dirty: one full redraw is cheaper
518        let total = (cols as u32).wrapping_mul(rows as u32).wrapping_mul(3);
519        if (dirty_cells.wrapping_mul(5) > total && !(force && plan.is_some())) || self.wp.dirty.full {
520            self.dirty_swap();
521            return self.dirty_full_take(w, h);
522        }
523        // the copies come first: the rectangles drawn next may cover their sources
524        if let Some(p) = plan {
525            self.sc_copy(&p, cols, rows);
526        }
527        self.dirty_swap();
528        n
529    }
530
531    // .dc_full: everything; the pending damage of this window is covered
532    fn dirty_full_take(&mut self, w: i32, h: i32) -> i32 {
533        if self.dmg.iter().any(|d| d.win == self.uiw_cur) {
534            self.wp.dirty.forced = true; // (the damage is not told apart any more)
535        }
536        self.wp.dirty.full = false;
537        self.drects.clear();
538        self.dmg_take(0, w, h);
539        self.drects.clear();
540        -1
541    }
542
543    // .dc_swap: this frame's hashes become the previous ones
544    fn dirty_swap(&mut self) {
545        let d = &mut self.wp.dirty;
546        core::mem::swap(&mut d.cells, &mut d.cells_prev);
547        core::mem::swap(&mut d.pre, &mut d.pre_prev);
548    }
549}