web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
perf(wpe): read back only what each frame changed
Every webview now runs with WebKit's PropagateDamagingInformation, so
each frame reports what it repainted. The frame sink keeps that per
view -- including frames handed back unread, whose changes the next
read must still carry -- and pump copies only those rectangles out of
the SHM buffer and writes them into the tab's image with cce-ui's new
update_pixel_regions. A whole frame is still read when the tab has no
image at that size, when a frame reports nothing (unknown), or when
the damage covers half the frame. Logic and tests in src/wpe/damage.rs.
Measured in a shadow at scale 2: an overlay scrollbar fade costs
0.27 MB a frame instead of 15 MB; on Muji, with its sliding banner,
the browser's own CPU went from 15% to 10% of a core and the copying
from ~107 MB/s to ~1 MB/s.
A frame now goes to the tab that drew it. Before, the newest frame
from any view was uploaded as the active tab's picture; a view no tab
owns (the spare) is released unread.
CCE_BROWSER_DAMAGE_CHECK=1 keeps a CPU copy per tab, patched region by
region, and compares it with the whole frame on every read; it was
exact on all ~1000 reads through load, idle, scrolling, a resize and
Muji. CCE_BROWSER_FULL_FRAMES=1 restores whole-frame reads.
Also on: HiddenPageCSSAnimationSuspension, so a background tab's CSS
animations stop as its requestAnimationFrame already does.
Pins cce-ui to ab44fd2 for update_pixel_regions. The CLAUDE.md section
describing this landed early, in 1bf3b47.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 1 +
Cargo.toml | 2 +-
src/wpe/damage.rs | 210 +++++++++++++++++++++++++++++++++++++++++
src/wpe/host.rs | 264 +++++++++++++++++++++++++++++++++++++++++++---------
src/wpe/mod.rs | 2 +
src/wpe/subclass.rs | 24 ++++-
6 files changed, 453 insertions(+), 50 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index aed01ac..c1e2430 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -30,6 +30,7 @@ Nineteen files, ~14.8k lines. The twelve that carry the design:
| `src/vi.rs` | vi mode, after qutebrowser: the modes, the bindings and their parser, hint labels, `:` commands, and the page scripts |
| `src/accounts.rs` | accounts from cce-secrets: the Secret Service worker, which entries a host earns, saving a new login, and the never-save list |
| `src/wpe/formwatch.rs` | the page half of account autocomplete: the watcher every frame runs (fields, frame-offset relay, fill asks, sign-in capture) and the events it sends |
+| `src/wpe/damage.rs` | what a frame changed: damage accumulated per view across skipped frames, turned into the regions `pump` reads back |
| `src/raindrop/` | bookmark sync with Raindrop.io's Unsorted: the three-way merge (`mod.rs`), the REST client (`api.rs`), the worker and status line (`sync.rs`) — design in RAINDROP-SYNC.md |
## Build
diff --git a/Cargo.toml b/Cargo.toml
index 07baa97..8beba3b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,7 @@ wpe = ["dep:rustix"]
servo = ["dep:servo", "dep:dpi", "dep:euclid", "dep:rustls", "dep:http"]
[dependencies]
-cce-ui = { git = "https://github.com/lsgalante/cce-ui.git", rev = "b91b95e0b6d9922ac51538ede03023070e43a446" }
+cce-ui = { git = "https://github.com/lsgalante/cce-ui.git", rev = "ab44fd20f362113854038b8eeb75fa3a45173901" }
calloop = "0.13.0"
wayland-client = { version = "0.31", features = ["system"] }
servo = { version = "0.4", optional = true }
diff --git a/src/wpe/damage.rs b/src/wpe/damage.rs
new file mode 100644
index 0000000..3685355
--- /dev/null
+++ b/src/wpe/damage.rs
@@ -0,0 +1,210 @@
+//! What changed between the frame a tab's image holds and the one about to be
+//! read, so that only that much is copied out of the engine's buffer and
+//! uploaded.
+//!
+//! WebKit reports each frame's damage — what it repainted since the frame
+//! before — once `PropagateDamagingInformation` is on (`host.rs` turns it on
+//! for every webview). A frame that is handed back unread still changed the
+//! picture, so its damage is kept and folded into the next frame of the same
+//! view; that is what lets the readback skip frames *and* copy only regions.
+//!
+//! The whole point is the idle cost. A page that can scroll has WebKit
+//! repainting its overlay scrollbar about sixty times a second, for good
+//! (WebKit 2.52; measured in a shadow, and MiniBrowser does it too), and a
+//! page with a sliding banner repaints that band. Reading the whole window
+//! for each — 35 MB a frame at 3840x2400 — was most of the browser's own CPU
+//! on such pages.
+
+/// A rectangle of the buffer in pixels: `(x, y, width, height)`.
+pub type Rect = (u32, u32, u32, u32);
+
+/// More rectangles than this and they are merged into their bounding box:
+/// each one is its own row loop and its own copy region, and a frame that
+/// changed in that many places is better read as one.
+const MAX_RECTS: usize = 16;
+
+/// Damage that covers this share of the frame or more is read whole: one
+/// contiguous copy beats many row copies adding up to nearly the same bytes.
+const FULL_SHARE: f64 = 0.5;
+
+/// What changed in one view since its frame was last read.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Damage {
+ /// Unknown or everything: read the whole frame.
+ Full,
+ /// Only these, in buffer pixels, not yet clamped to the buffer.
+ Rects(Vec<(i32, i32, i32, i32)>),
+}
+
+impl Damage {
+ /// Fold one frame's report in. A frame that reports no rectangles did
+ /// not say what it changed — that is what an engine without damage
+ /// propagation sends — so it counts as everything.
+ pub fn add(&mut self, frame: &[(i32, i32, i32, i32)]) {
+ if frame.is_empty() {
+ *self = Damage::Full;
+ return;
+ }
+ if let Damage::Rects(rects) = self {
+ for r in frame {
+ if !rects.contains(r) {
+ rects.push(*r);
+ }
+ }
+ }
+ }
+
+ /// The regions to copy from a `width` x `height` buffer, or `None` when
+ /// the whole frame should be read instead.
+ pub fn regions(&self, width: u32, height: u32) -> Option<Vec<Rect>> {
+ let Damage::Rects(raw) = self else { return None };
+ let mut rects: Vec<Rect> = Vec::new();
+ for r in raw.iter().filter_map(|&r| clamp(r, width, height)) {
+ // Two reports can clamp to the same rectangle.
+ if !rects.contains(&r) {
+ rects.push(r);
+ }
+ }
+ // A rectangle inside another adds nothing but a second copy of it.
+ // (No two are equal by now, so containment is strict.)
+ let mut i = 0;
+ while i < rects.len() {
+ let inner = rects[i];
+ if rects.iter().enumerate().any(|(j, &outer)| j != i && contains(outer, inner)) {
+ rects.remove(i);
+ } else {
+ i += 1;
+ }
+ }
+ if rects.len() > MAX_RECTS {
+ rects = vec![bounding(&rects)];
+ }
+ let area: u64 = rects.iter().map(|r| r.2 as u64 * r.3 as u64).sum();
+ if area as f64 >= FULL_SHARE * width as f64 * height as f64 {
+ return None;
+ }
+ Some(rects)
+ }
+}
+
+fn clamp((x, y, w, h): (i32, i32, i32, i32), width: u32, height: u32) -> Option<Rect> {
+ let x0 = x.max(0) as i64;
+ let y0 = y.max(0) as i64;
+ let x1 = (x as i64 + w as i64).min(width as i64);
+ let y1 = (y as i64 + h as i64).min(height as i64);
+ (x1 > x0 && y1 > y0).then(|| (x0 as u32, y0 as u32, (x1 - x0) as u32, (y1 - y0) as u32))
+}
+
+fn contains(outer: Rect, inner: Rect) -> bool {
+ inner.0 >= outer.0
+ && inner.1 >= outer.1
+ && inner.0 + inner.2 <= outer.0 + outer.2
+ && inner.1 + inner.3 <= outer.1 + outer.3
+}
+
+fn bounding(rects: &[Rect]) -> Rect {
+ let x0 = rects.iter().map(|r| r.0).min().unwrap_or(0);
+ let y0 = rects.iter().map(|r| r.1).min().unwrap_or(0);
+ let x1 = rects.iter().map(|r| r.0 + r.2).max().unwrap_or(0);
+ let y1 = rects.iter().map(|r| r.1 + r.3).max().unwrap_or(0);
+ (x0, y0, x1 - x0, y1 - y0)
+}
+
+/// Copy `regions` of a 4-byte-per-pixel image whose rows are `stride` bytes
+/// apart into `out`, each region tightly packed, one after another — the
+/// layout `cce_ui::vk::update_pixel_regions` takes.
+///
+/// # Safety
+/// `src` must be readable for every byte of every region at `stride`.
+pub unsafe fn pack(src: *const u8, stride: usize, regions: &[Rect], out: &mut [u8]) {
+ let mut at = 0usize;
+ for &(x, y, w, h) in regions {
+ let row = w as usize * 4;
+ for r in 0..h as usize {
+ let from = src.add((y as usize + r) * stride + x as usize * 4);
+ std::ptr::copy_nonoverlapping(from, out.as_mut_ptr().add(at), row);
+ at += row;
+ }
+ }
+}
+
+/// Bytes [`pack`] writes for `regions`.
+pub fn packed_len(regions: &[Rect]) -> usize {
+ regions.iter().map(|r| r.2 as usize * r.3 as usize * 4).sum()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn a_frame_without_rects_is_everything() {
+ let mut d = Damage::Rects(vec![]);
+ d.add(&[(0, 0, 10, 10)]);
+ d.add(&[]);
+ assert_eq!(d, Damage::Full);
+ // And stays so: later reports cannot narrow an unknown change.
+ d.add(&[(0, 0, 1, 1)]);
+ assert_eq!(d.regions(100, 100), None);
+ }
+
+ #[test]
+ fn skipped_frames_accumulate_without_repeats() {
+ let mut d = Damage::Rects(vec![]);
+ d.add(&[(1238, 0, 42, 720)]);
+ d.add(&[(1238, 0, 42, 720)]);
+ d.add(&[(0, 0, 10, 10)]);
+ assert_eq!(d.regions(1280, 720), Some(vec![(1238, 0, 42, 720), (0, 0, 10, 10)]));
+ }
+
+ #[test]
+ fn rects_are_clamped_to_the_buffer() {
+ // The engine's first frame reports its pre-resize size.
+ let mut d = Damage::Rects(vec![]);
+ d.add(&[(-5, -5, 20, 20), (1270, 710, 50, 50)]);
+ assert_eq!(d.regions(1280, 720), Some(vec![(0, 0, 15, 15), (1270, 710, 10, 10)]));
+ let mut off = Damage::Rects(vec![]);
+ off.add(&[(2000, 0, 10, 10)]);
+ assert_eq!(off.regions(1280, 720), Some(vec![]));
+ }
+
+ #[test]
+ fn contained_rects_are_dropped() {
+ let mut d = Damage::Rects(vec![]);
+ d.add(&[(0, 0, 100, 100), (10, 10, 5, 5)]);
+ assert_eq!(d.regions(1000, 1000), Some(vec![(0, 0, 100, 100)]));
+ }
+
+ #[test]
+ fn large_damage_reads_the_whole_frame() {
+ let mut d = Damage::Rects(vec![]);
+ d.add(&[(0, 0, 1280, 400)]);
+ assert_eq!(d.regions(1280, 720), None);
+ }
+
+ #[test]
+ fn many_rects_merge_into_their_bounds() {
+ let mut d = Damage::Rects(vec![]);
+ let many: Vec<_> = (0..20).map(|i| (i * 10, 0, 5, 5)).collect();
+ d.add(&many);
+ assert_eq!(d.regions(1280, 720), Some(vec![(0, 0, 195, 5)]));
+ }
+
+ #[test]
+ fn pack_copies_each_region_tightly() {
+ // A 4x3 image, stride padded to 5 pixels; each pixel's bytes are its
+ // index, so what lands where is readable.
+ let (w, h, stride) = (4usize, 3usize, 20usize);
+ let mut src = vec![0xEEu8; stride * h];
+ for y in 0..h {
+ for x in 0..w {
+ src[y * stride + x * 4..][..4].fill((y * w + x) as u8);
+ }
+ }
+ let regions = [(1, 0, 2, 2), (3, 2, 1, 1)];
+ let mut out = vec![0u8; packed_len(®ions)];
+ unsafe { pack(src.as_ptr(), stride, ®ions, &mut out) };
+ let px: Vec<u8> = out.chunks(4).map(|p| p[0]).collect();
+ assert_eq!(px, vec![1, 2, 5, 6, 11]);
+ }
+}
diff --git a/src/wpe/host.rs b/src/wpe/host.rs
index cf616e1..dbc0a26 100644
--- a/src/wpe/host.rs
+++ b/src/wpe/host.rs
@@ -145,6 +145,9 @@ pub struct Tab {
pub url: Option<Url>,
pub loading: bool,
image: Option<(u32, u32, u32)>,
+ /// Under `CCE_BROWSER_DAMAGE_CHECK` only: the picture `image` should
+ /// hold, patched region by region alongside it.
+ mirror: Option<Vec<u8>>,
}
impl Drop for Tab {
@@ -288,6 +291,26 @@ fn terminated_page(url: Option<&Url>, reason: WebKitWebProcessTerminationReason:
struct FrameCounts {
produced: u64,
read: u64,
+ /// Of `read`, how many copied only their damage.
+ partial: u64,
+ /// Bytes copied out of the engine's buffers.
+ bytes: u64,
+}
+
+/// Read whole frames only, ignoring damage. The escape hatch if a page is
+/// ever drawn stale; `CCE_BROWSER_DAMAGE_CHECK` is how to find out.
+fn full_frames() -> bool {
+ static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
+ *ON.get_or_init(|| std::env::var_os("CCE_BROWSER_FULL_FRAMES").is_some())
+}
+
+/// Check every region read against the whole frame: each tab keeps a CPU
+/// copy of its picture, patched exactly as its image is, and any pixel that
+/// disagrees with the engine's buffer is logged. Costs a full copy and a
+/// compare per frame, so it is a test switch, not a mode.
+fn damage_check() -> bool {
+ static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
+ *ON.get_or_init(|| std::env::var_os("CCE_BROWSER_DAMAGE_CHECK").is_some())
}
fn frame_debug() -> bool {
@@ -302,6 +325,10 @@ struct Pending {
/// hands this one back **unread** — that skipped copy is the whole point
/// of holding it rather than copying in the callback.
held: Option<(*mut WPEView, *mut WPEBuffer)>,
+ /// Per view, what its frames changed since the last one that was read —
+ /// including every frame handed back unread in between, whose changes
+ /// the next readback still has to carry. No entry: nothing changed.
+ damage: std::collections::HashMap<usize, super::damage::Damage>,
counts: FrameCounts,
/// When the counters were last reported.
reported: Option<std::time::Instant>,
@@ -477,8 +504,12 @@ impl WebKitHost {
let prompts = Rc::new(RefCell::new(Prompts::default()));
let pending = Rc::new(std::cell::RefCell::new(Pending::default()));
let sink = pending.clone();
- FRAME_SINK = Some(Box::new(move |view: *mut WPEView, buffer: *mut WPEBuffer| {
+ FRAME_SINK = Some(Box::new(move |view: *mut WPEView, buffer: *mut WPEBuffer, damage: &[(i32, i32, i32, i32)]| {
let mut slot = sink.borrow_mut();
+ slot.damage
+ .entry(view as usize)
+ .or_insert_with(|| super::damage::Damage::Rects(Vec::new()))
+ .add(damage);
// Replace, never accumulate: the newest frame wins. The one it
// supersedes goes back to the engine **without being read** —
// several frames can be dispatched inside a single pump's
@@ -840,6 +871,7 @@ impl WebKitHost {
self.session,
std::ptr::null::<c_char>(),
) as *mut WebKitWebView;
+ set_features(wv);
let view = webkit_web_view_get_wpe_view(wv);
wpe_view_set_toplevel(view, self.toplevel);
// Signals, not polling: a background tab has to be able to report
@@ -981,6 +1013,7 @@ impl WebKitHost {
url: Some(url),
loading: true,
image: None,
+ mirror: None,
});
if show {
self.activate(self.tabs.len() - 1);
@@ -1150,36 +1183,91 @@ impl WebKitHost {
let Some((view, buffer)) = held else {
return (false, dirty);
};
- let frame = unsafe {
- let f = read_shm(buffer);
+ let damage = self.pending.borrow_mut().damage.remove(&(view as usize));
+ // The frame belongs to the tab that drew it, which is not always the
+ // one on screen. A view no tab owns is the prewarmed spare, or a tab
+ // closed since: nothing shows it.
+ let Some(index) = self.tabs.iter().position(|t| t.view == view) else {
+ unsafe { wpe_view_buffer_released(view, buffer) };
+ return (false, dirty);
+ };
+ let read = unsafe {
+ let read = self.read_frame(index, buffer, damage);
// The pixels are ours now; the memory can go back.
wpe_view_buffer_released(view, buffer);
- f
+ read
};
if frame_debug() {
let mut p = self.pending.borrow_mut();
p.counts.read += 1;
+ if let Some((bytes, partial)) = read {
+ p.counts.bytes += bytes as u64;
+ p.counts.partial += partial as u64;
+ }
let now = std::time::Instant::now();
let due = p.reported.is_none_or(|t| now.duration_since(t).as_secs_f32() >= 1.0);
if due {
p.reported = Some(now);
- let (produced, read) = (p.counts.produced, p.counts.read);
- p.counts = FrameCounts::default();
+ let c = std::mem::take(&mut p.counts);
log::info!(
- "frames: engine produced {produced}, read back {read} \
- ({} handed back unread)",
- produced.saturating_sub(read)
+ "frames: engine produced {}, read back {} ({} handed back unread), \
+ {} of them only their damage; {:.1} MB copied",
+ c.produced,
+ c.read,
+ c.produced.saturating_sub(c.read),
+ c.partial,
+ c.bytes as f64 / 1e6
);
}
}
- let Some((px, w, h)) = frame else {
- return (false, dirty);
- };
+ if read.is_none() || index != self.active {
+ return (false, true);
+ }
+ self.pending_draw.set(true);
+ (true, true)
+ }
+
+ /// Copy a finished frame into tab `index`'s image: only the damaged
+ /// regions when the image already holds the frame before them, the
+ /// whole frame otherwise. Returns the bytes copied and whether it was
+ /// regions, or `None` for a buffer that could not be read.
+ unsafe fn read_frame(
+ &mut self,
+ index: usize,
+ buffer: *mut WPEBuffer,
+ damage: Option<super::damage::Damage>,
+ ) -> Option<(usize, bool)> {
+ let shm = ShmFrame::of(buffer)?;
+ let (w, h) = (shm.width, shm.height);
// The one pixel anything actually reads back (see `sample_pixel`),
// kept instead of a copy of the whole frame. Cloning 35 MB per frame
// to serve a three-byte question cost 7 ms of every frame.
- self.last_pixel = (px.len() >= 4).then(|| (px[2], px[1], px[0]));
- let tab = &mut self.tabs[self.active];
+ let p0 = std::slice::from_raw_parts(shm.data, 4);
+ self.last_pixel = Some((p0[2], p0[1], p0[0]));
+ let tab = &mut self.tabs[index];
+ // Regions only make sense against the picture they change: this
+ // tab's image, at this size. No damage at all means nothing changed.
+ let current = tab.image.is_some_and(|(_, iw, ih)| (iw, ih) == (w, h));
+ let regions = match damage {
+ _ if full_frames() || !current => None,
+ None => Some(Vec::new()),
+ Some(d) => d.regions(w, h),
+ };
+ if let (Some(regions), Some((id, ..))) = (regions, tab.image) {
+ let len = super::damage::packed_len(®ions);
+ let mut px = cce_ui::vk::recycle_buffer(len);
+ super::damage::pack(shm.data, shm.stride, ®ions, &mut px);
+ if let Some(mirror) = tab.mirror.as_mut() {
+ check_regions(mirror, &shm, &px, ®ions, index);
+ }
+ cce_ui::vk::update_pixel_regions(id, px, w, h, cce_ui::vk::PixelFormat::Bgra, regions);
+ return Some((len, true));
+ }
+ let px = shm.copy_all();
+ let len = px.len();
+ if damage_check() {
+ tab.mirror = Some(px.clone());
+ }
match tab.image {
// Same tab, same size: replace the contents of the image that is
// already there. No allocation, no descriptor, and above all no
@@ -1193,11 +1281,9 @@ impl WebKitHost {
if let Some((old, ..)) = tab.image.replace((id, w, h)) {
cce_ui::vk::free_image(old);
}
- tab.image = Some((id, w, h));
}
}
- self.pending_draw.set(true);
- (true, true)
+ Some((len, false))
}
/// Re-paint the page into a renderer that has just replaced the one the
@@ -1966,35 +2052,125 @@ impl WebKitHost {
/// before the next pump is never read at all.
///
/// The stride is not assumed to equal `width * 4`.
-unsafe fn read_shm(buffer: *mut WPEBuffer) -> Option<(Vec<u8>, u32, u32)> {
- if g_type_check_instance_is_a(buffer as *mut GTypeInstance, wpe_buffer_shm_get_type()) == 0 {
- return None;
- }
- let shm = buffer as *mut WPEBufferSHM;
- let (w, h) = (
- wpe_buffer_get_width(buffer) as u32,
- wpe_buffer_get_height(buffer) as u32,
- );
- let mut len: u64 = 0;
- let src = g_bytes_get_data(wpe_buffer_shm_get_data(shm), &mut len as *mut u64) as *const u8;
- if src.is_null() || w == 0 || h == 0 {
- return None;
- }
- let stride = wpe_buffer_shm_get_stride(shm) as usize;
- let row = w as usize * 4;
- let need = row * h as usize;
- if (len as usize) < stride * (h as usize - 1) + row {
- return None;
- }
- let mut out = cce_ui::vk::recycle_buffer(need);
- if stride == row {
- std::ptr::copy_nonoverlapping(src, out.as_mut_ptr(), need);
- } else {
- for y in 0..h as usize {
- std::ptr::copy_nonoverlapping(src.add(y * stride), out.as_mut_ptr().add(y * row), row);
+/// WebKit features every webview runs with, off by default in this build.
+///
+/// * `PropagateDamagingInformation` — each frame reports what it repainted,
+/// so `pump` copies only that (see `damage.rs`). Without it a page that
+/// can scroll costs a whole-window copy sixty times a second while it sits
+/// still, for an overlay scrollbar WebKit never stops repainting.
+/// * `HiddenPageCSSAnimationSuspension` — a background tab's CSS animations
+/// stop. WebKit already stops its `requestAnimationFrame` (checked: 0 a
+/// second hidden), but not this, and not its timers. A hidden Muji page
+/// measured 67% of a core with it off and 43% on.
+const FEATURES: &[(&str, bool)] =
+ &[("PropagateDamagingInformation", true), ("HiddenPageCSSAnimationSuspension", true)];
+
+unsafe fn set_features(wv: *mut WebKitWebView) {
+ let settings = webkit_web_view_get_settings(wv);
+ let list = webkit_settings_get_all_features();
+ for &(name, on) in FEATURES {
+ let found = (0..webkit_feature_list_get_length(list))
+ .map(|i| webkit_feature_list_get(list, i))
+ .find(|&f| from_cstr(webkit_feature_get_identifier(f)).as_deref() == Some(name));
+ match found {
+ Some(f) => webkit_settings_set_feature_enabled(settings, f, on as gboolean),
+ // A WebKit upgrade renamed or dropped it: the browser still
+ // works, it just loses what the feature bought.
+ None => log::warn!("WebKit has no feature {name}; leaving it as it is"),
+ }
+ }
+ webkit_feature_list_unref(list);
+}
+
+/// A mapped SHM frame: where its pixels are, and how they are laid out.
+/// Borrowed from the buffer, so it must not outlive the buffer's release.
+struct ShmFrame {
+ data: *const u8,
+ stride: usize,
+ width: u32,
+ height: u32,
+}
+
+impl ShmFrame {
+ /// The buffer's pixels, if it is an SHM buffer with all its rows there.
+ unsafe fn of(buffer: *mut WPEBuffer) -> Option<Self> {
+ if g_type_check_instance_is_a(buffer as *mut GTypeInstance, wpe_buffer_shm_get_type()) == 0 {
+ return None;
}
+ let shm = buffer as *mut WPEBufferSHM;
+ let (width, height) = (
+ wpe_buffer_get_width(buffer) as u32,
+ wpe_buffer_get_height(buffer) as u32,
+ );
+ let mut len: u64 = 0;
+ let data = g_bytes_get_data(wpe_buffer_shm_get_data(shm), &mut len as *mut u64) as *const u8;
+ if data.is_null() || width == 0 || height == 0 {
+ return None;
+ }
+ let stride = wpe_buffer_shm_get_stride(shm) as usize;
+ if (len as usize) < stride * (height as usize - 1) + width as usize * 4 {
+ return None;
+ }
+ Some(Self { data, stride, width, height })
+ }
+
+ /// The whole frame, tightly packed, in a recycled buffer.
+ unsafe fn copy_all(&self) -> Vec<u8> {
+ let row = self.width as usize * 4;
+ let need = row * self.height as usize;
+ let mut out = cce_ui::vk::recycle_buffer(need);
+ if self.stride == row {
+ std::ptr::copy_nonoverlapping(self.data, out.as_mut_ptr(), need);
+ } else {
+ for y in 0..self.height as usize {
+ std::ptr::copy_nonoverlapping(
+ self.data.add(y * self.stride),
+ out.as_mut_ptr().add(y * row),
+ row,
+ );
+ }
+ }
+ out
+ }
+}
+
+/// `CCE_BROWSER_DAMAGE_CHECK`: patch the tab's CPU copy with the regions just
+/// read, as its image is being patched, and compare the result with the
+/// engine's whole frame. A mismatch means the damage left something out and
+/// the page on screen is stale there; the copy is then resynced so one miss
+/// is not reported on every frame after it.
+unsafe fn check_regions(
+ mirror: &mut [u8],
+ shm: &ShmFrame,
+ packed: &[u8],
+ regions: &[super::damage::Rect],
+ tab: usize,
+) {
+ let row = shm.width as usize * 4;
+ let mut at = 0usize;
+ for &(x, y, w, h) in regions {
+ let len = w as usize * 4;
+ for r in 0..h as usize {
+ let dst = (y as usize + r) * row + x as usize * 4;
+ mirror[dst..dst + len].copy_from_slice(&packed[at..at + len]);
+ at += len;
+ }
+ }
+ let truth = shm.copy_all();
+ let wrong = mirror
+ .chunks_exact(4)
+ .zip(truth.chunks_exact(4))
+ .filter(|(a, b)| a != b)
+ .count();
+ if wrong > 0 {
+ log::warn!(
+ "damage check: tab {tab} is stale in {wrong} pixels after reading {} region(s) {regions:?}",
+ regions.len()
+ );
+ mirror.copy_from_slice(&truth);
+ } else {
+ log::info!("damage check: tab {tab} exact after {} region(s)", regions.len());
}
- Some((out, w, h))
}
unsafe fn from_cstr(p: *const c_char) -> Option<String> {
diff --git a/src/wpe/mod.rs b/src/wpe/mod.rs
index 591d83f..d259308 100644
--- a/src/wpe/mod.rs
+++ b/src/wpe/mod.rs
@@ -10,6 +10,8 @@ pub mod ffi {
}
mod subclass;
+/// What a frame changed, so only that much is read back and uploaded.
+mod damage;
mod input;
mod glib_source;
mod host;
diff --git a/src/wpe/subclass.rs b/src/wpe/subclass.rs
index 5af3f9f..37c1443 100644
--- a/src/wpe/subclass.rs
+++ b/src/wpe/subclass.rs
@@ -86,17 +86,23 @@ pub(super) unsafe fn types() -> &'static Types {
/// Set by the host before it creates a webview; `render_buffer` hands frames
/// here. One host per process for now (see `WebKitHost::new`).
///
+/// The third argument is the frame's damage — what it repainted since the
+/// frame before, in buffer pixels as `(x, y, width, height)` — and is empty
+/// when the engine did not say.
+///
/// Returns whether the sink is **keeping** the buffer. If it is, releasing it
/// is the sink's job — it reads the pixels out at the next pump and hands the
/// memory back then.
-pub(super) static mut FRAME_SINK: Option<Box<dyn FnMut(*mut WPEView, *mut WPEBuffer) -> bool>> =
- None;
+#[allow(clippy::type_complexity)]
+pub(super) static mut FRAME_SINK: Option<
+ Box<dyn FnMut(*mut WPEView, *mut WPEBuffer, &[(i32, i32, i32, i32)]) -> bool>,
+> = None;
unsafe extern "C" fn view_render_buffer(
view: *mut WPEView,
buffer: *mut WPEBuffer,
- _damage: *const WPERectangle,
- _n_damage: u32,
+ damage: *const WPERectangle,
+ n_damage: u32,
_error: *mut *mut GError,
) -> gboolean {
// The two halves mean different things and are no longer said together.
@@ -111,8 +117,16 @@ unsafe extern "C" fn view_render_buffer(
// and a frame superseded before anyone read it is handed back unread
// rather than copied.
wpe_view_buffer_rendered(view, buffer);
+ let damage: Vec<(i32, i32, i32, i32)> = if damage.is_null() {
+ Vec::new()
+ } else {
+ std::slice::from_raw_parts(damage, n_damage as usize)
+ .iter()
+ .map(|r| (r.x, r.y, r.width, r.height))
+ .collect()
+ };
#[allow(static_mut_refs)]
- let held = FRAME_SINK.as_mut().is_some_and(|sink| sink(view, buffer));
+ let held = FRAME_SINK.as_mut().is_some_and(|sink| sink(view, buffer, &damage));
if !held {
wpe_view_buffer_released(view, buffer);
}