web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
src/wpe/damage.rs (7.7K)
1 //! What changed between the frame a tab's image holds and the one about to be
2 //! read, so that only that much is copied out of the engine's buffer and
3 //! uploaded.
4 //!
5 //! WebKit reports each frame's damage — what it repainted since the frame
6 //! before — once `PropagateDamagingInformation` is on (`host.rs` turns it on
7 //! for every webview). A frame that is handed back unread still changed the
8 //! picture, so its damage is kept and folded into the next frame of the same
9 //! view; that is what lets the readback skip frames *and* copy only regions.
10 //!
11 //! The whole point is the idle cost. A page that can scroll has WebKit
12 //! repainting its overlay scrollbar about sixty times a second, for good
13 //! (WebKit 2.52; measured in a shadow, and MiniBrowser does it too), and a
14 //! page with a sliding banner repaints that band. Reading the whole window
15 //! for each — 35 MB a frame at 3840x2400 — was most of the browser's own CPU
16 //! on such pages.
17
18 /// A rectangle of the buffer in pixels: `(x, y, width, height)`.
19 pub type Rect = (u32, u32, u32, u32);
20
21 /// More rectangles than this and they are merged into their bounding box:
22 /// each one is its own row loop and its own copy region, and a frame that
23 /// changed in that many places is better read as one.
24 const MAX_RECTS: usize = 16;
25
26 /// Damage that covers this share of the frame or more is read whole: one
27 /// contiguous copy beats many row copies adding up to nearly the same bytes.
28 const FULL_SHARE: f64 = 0.5;
29
30 /// What changed in one view since its frame was last read.
31 #[derive(Debug, Clone, PartialEq)]
32 pub enum Damage {
33 /// Unknown or everything: read the whole frame.
34 Full,
35 /// Only these, in buffer pixels, not yet clamped to the buffer.
36 Rects(Vec<(i32, i32, i32, i32)>),
37 }
38
39 impl Damage {
40 /// Fold one frame's report in. A frame that reports no rectangles did
41 /// not say what it changed — that is what an engine without damage
42 /// propagation sends — so it counts as everything.
43 pub fn add(&mut self, frame: &[(i32, i32, i32, i32)]) {
44 if frame.is_empty() {
45 *self = Damage::Full;
46 return;
47 }
48 if let Damage::Rects(rects) = self {
49 for r in frame {
50 if !rects.contains(r) {
51 rects.push(*r);
52 }
53 }
54 }
55 }
56
57 /// The regions to copy from a `width` x `height` buffer, or `None` when
58 /// the whole frame should be read instead.
59 pub fn regions(&self, width: u32, height: u32) -> Option<Vec<Rect>> {
60 let Damage::Rects(raw) = self else { return None };
61 let mut rects: Vec<Rect> = Vec::new();
62 for r in raw.iter().filter_map(|&r| clamp(r, width, height)) {
63 // Two reports can clamp to the same rectangle.
64 if !rects.contains(&r) {
65 rects.push(r);
66 }
67 }
68 // A rectangle inside another adds nothing but a second copy of it.
69 // (No two are equal by now, so containment is strict.)
70 let mut i = 0;
71 while i < rects.len() {
72 let inner = rects[i];
73 if rects.iter().enumerate().any(|(j, &outer)| j != i && contains(outer, inner)) {
74 rects.remove(i);
75 } else {
76 i += 1;
77 }
78 }
79 if rects.len() > MAX_RECTS {
80 rects = vec![bounding(&rects)];
81 }
82 let area: u64 = rects.iter().map(|r| r.2 as u64 * r.3 as u64).sum();
83 if area as f64 >= FULL_SHARE * width as f64 * height as f64 {
84 return None;
85 }
86 Some(rects)
87 }
88 }
89
90 fn clamp((x, y, w, h): (i32, i32, i32, i32), width: u32, height: u32) -> Option<Rect> {
91 let x0 = x.max(0) as i64;
92 let y0 = y.max(0) as i64;
93 let x1 = (x as i64 + w as i64).min(width as i64);
94 let y1 = (y as i64 + h as i64).min(height as i64);
95 (x1 > x0 && y1 > y0).then(|| (x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32))
96 }
97
98 fn contains(outer: Rect, inner: Rect) -> bool {
99 inner.0 >= outer.0
100 && inner.1 >= outer.1
101 && inner.0 + inner.2 <= outer.0 + outer.2
102 && inner.1 + inner.3 <= outer.1 + outer.3
103 }
104
105 fn bounding(rects: &[Rect]) -> Rect {
106 let x0 = rects.iter().map(|r| r.0).min().unwrap_or(0);
107 let y0 = rects.iter().map(|r| r.1).min().unwrap_or(0);
108 let x1 = rects.iter().map(|r| r.0 + r.2).max().unwrap_or(0);
109 let y1 = rects.iter().map(|r| r.1 + r.3).max().unwrap_or(0);
110 (x0, y0, x1 - x0, y1 - y0)
111 }
112
113 /// Copy `regions` of a 4-byte-per-pixel image whose rows are `stride` bytes
114 /// apart into `out`, each region tightly packed, one after another — the
115 /// layout `cce_ui::vk::update_pixel_regions` takes.
116 ///
117 /// # Safety
118 /// `src` must be readable for every byte of every region at `stride`.
119 pub unsafe fn pack(src: *const u8, stride: usize, regions: &[Rect], out: &mut [u8]) {
120 let mut at = 0usize;
121 for &(x, y, w, h) in regions {
122 let row = w as usize * 4;
123 for r in 0..h as usize {
124 let from = src.add((y as usize + r) * stride + x as usize * 4);
125 std::ptr::copy_nonoverlapping(from, out.as_mut_ptr().add(at), row);
126 at += row;
127 }
128 }
129 }
130
131 /// Bytes [`pack`] writes for `regions`.
132 pub fn packed_len(regions: &[Rect]) -> usize {
133 regions.iter().map(|r| r.2 as usize * r.3 as usize * 4).sum()
134 }
135
136 #[cfg(test)]
137 mod tests {
138 use super::*;
139
140 #[test]
141 fn a_frame_without_rects_is_everything() {
142 let mut d = Damage::Rects(vec![]);
143 d.add(&[(0, 0, 10, 10)]);
144 d.add(&[]);
145 assert_eq!(d, Damage::Full);
146 // And stays so: later reports cannot narrow an unknown change.
147 d.add(&[(0, 0, 1, 1)]);
148 assert_eq!(d.regions(100, 100), None);
149 }
150
151 #[test]
152 fn skipped_frames_accumulate_without_repeats() {
153 let mut d = Damage::Rects(vec![]);
154 d.add(&[(1238, 0, 42, 720)]);
155 d.add(&[(1238, 0, 42, 720)]);
156 d.add(&[(0, 0, 10, 10)]);
157 assert_eq!(d.regions(1280, 720), Some(vec![(1238, 0, 42, 720), (0, 0, 10, 10)]));
158 }
159
160 #[test]
161 fn rects_are_clamped_to_the_buffer() {
162 // The engine's first frame reports its pre-resize size.
163 let mut d = Damage::Rects(vec![]);
164 d.add(&[(-5, -5, 20, 20), (1270, 710, 50, 50)]);
165 assert_eq!(d.regions(1280, 720), Some(vec![(0, 0, 15, 15), (1270, 710, 10, 10)]));
166 let mut off = Damage::Rects(vec![]);
167 off.add(&[(2000, 0, 10, 10)]);
168 assert_eq!(off.regions(1280, 720), Some(vec![]));
169 }
170
171 #[test]
172 fn contained_rects_are_dropped() {
173 let mut d = Damage::Rects(vec![]);
174 d.add(&[(0, 0, 100, 100), (10, 10, 5, 5)]);
175 assert_eq!(d.regions(1000, 1000), Some(vec![(0, 0, 100, 100)]));
176 }
177
178 #[test]
179 fn large_damage_reads_the_whole_frame() {
180 let mut d = Damage::Rects(vec![]);
181 d.add(&[(0, 0, 1280, 400)]);
182 assert_eq!(d.regions(1280, 720), None);
183 }
184
185 #[test]
186 fn many_rects_merge_into_their_bounds() {
187 let mut d = Damage::Rects(vec![]);
188 let many: Vec<_> = (0..20).map(|i| (i * 10, 0, 5, 5)).collect();
189 d.add(&many);
190 assert_eq!(d.regions(1280, 720), Some(vec![(0, 0, 195, 5)]));
191 }
192
193 #[test]
194 fn pack_copies_each_region_tightly() {
195 // A 4x3 image, stride padded to 5 pixels; each pixel's bytes are its
196 // index, so what lands where is readable.
197 let (w, h, stride) = (4usize, 3usize, 20usize);
198 let mut src = vec![0xEEu8; stride * h];
199 for y in 0..h {
200 for x in 0..w {
201 src[y * stride + x * 4..][..4].fill((y * w + x) as u8);
202 }
203 }
204 let regions = [(1, 0, 2, 2), (3, 2, 1, 1)];
205 let mut out = vec![0u8; packed_len(®ions)];
206 unsafe { pack(src.as_ptr(), stride, ®ions, &mut out) };
207 let px: Vec<u8> = out.chunks(4).map(|p| p[0]).collect();
208 assert_eq!(px, vec![1, 2, 5, 6, 11]);
209 }
210 }