Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: read window content for the backdrop, instead of calling it unknown
The backdrop measurement could only reason about the desktop it draws
itself. A window over a segment was reported as maximum spread — honest,
but it meant the bar wore a full outline whenever anything sat under it,
which on a real desktop is most of the time.
That case is now read. read_window_region composites the occluding
window's surfaces over just the overlapping strip and measures the
pixels; blend folds that into the desktop measurement for whatever part
of the segment the window does not cover. Spread comes from the 10th and
90th luminance percentiles, so a cursor or an icon does not report a flat
terminal as busy, and blend carries a third term for the seam itself —
two uniform halves still leave text straddling a hard edge between them.
Subsurfaces are composited in: a toolkit that renders into one would
otherwise be measured as its blank root.
The readback is the one part of this that costs a GPU sync, and it runs
in the render path, so it is gated twice: a 250ms throttle, and a check
that the window has committed anything since the last read. The second
gate is the one that matters — a window nobody is typing in is read
exactly once, and a static fullscreen window now produces one status push
in six seconds rather than twenty-four. Only the strip is read, never the
whole window: a bar-height slice of a 4K client is 0.3% of it.
Verified in a shadow session against ground truth: with cce-files
fullscreen under the clock, the topic reports `11 14`, and computing mean
luminance and p10-p90 spread independently from a capture of that same
window region gives exactly 11 and 14. Panning a floating window across
the segment band shows the blend moving between the window's 12 and the
desktop's 4 as coverage changes.
Co-Authored-By: Claude Opus 5 <[email protected]>
CLAUDE.md | 21 +++-
src/server/backdrop.rs | 135 ++++++++++++++++++++-
src/server/output.rs | 256 +++++++++++++++++++++++++++++++++++++--
src/server/screenshot.rs | 36 +++++-
src/server/wlroots_log_wrapper.c | 7 ++
wrapper.h | 1 +
6 files changed, 437 insertions(+), 19 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 9886850..3e0d3f9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -301,11 +301,22 @@ Persistent window state is saved to **`~/.local/state/cce/state.json`**
Lines are `<luma> <spread>` (0-100 each) or `unknown`. It answers a question a
Wayland client cannot: what its translucent module boxes are composited *over*,
so it can raise its text contrast to match. The measurement is geometry, not a
- readback — the desktop background is drawn from a declarative spec, so
- `backdrop.rs` computes cell-vs-gap coverage under each segment rect on the CPU
- (`Output::measure_status_backdrops`, per frame, gated by `update_status`'s
- equality check). A window overlapping a segment reports maximum spread, since
- its pixels are not knowable from here.
+ readback where it can be — the desktop background is drawn from a declarative
+ spec, so `backdrop.rs` computes cell-vs-gap coverage under each segment rect
+ on the CPU (`Output::measure_status_backdrops`, per frame, gated by
+ `update_status`'s equality check).
+
+ A window covering part of a segment is the case that has to be *read*:
+ `Output::read_window_region` composites that window's surfaces (subsurfaces
+ included) over just the overlapping strip via
+ `screenshot::read_texture_region`, and `backdrop::blend` folds the result
+ into the desktop measurement for the rest of the segment. Two gates keep that
+ readback off the render thread's back, and the second one matters more than
+ the first: a 250ms throttle, and a check that the window's summed surface
+ commit sequence changed at all (`river_wlr_surface_current_seq`). A window
+ nobody is typing in is read exactly once. Content that still cannot be read —
+ no committed buffer, an unsupported read format, an implausibly large strip —
+ falls back to `backdrop::UNKNOWN`.
## Conventions
diff --git a/src/server/backdrop.rs b/src/server/backdrop.rs
index e14aec9..30db0a8 100644
--- a/src/server/backdrop.rs
+++ b/src/server/backdrop.rs
@@ -63,7 +63,7 @@ impl Rect {
}
/// Overlap area with `other`, in px².
- fn intersect_area(&self, other: &Rect) -> i64 {
+ pub fn intersect_area(&self, other: &Rect) -> i64 {
let w = (self.right().min(other.right()) - self.x.max(other.x)).max(0) as i64;
let h = (self.bottom().min(other.bottom()) - self.y.max(other.y)).max(0) as i64;
w * h
@@ -72,6 +72,15 @@ impl Rect {
pub fn intersects(&self, other: &Rect) -> bool {
self.intersect_area(other) > 0
}
+
+ /// The overlapping rect, or None when they do not meet.
+ pub fn intersection(&self, other: &Rect) -> Option<Rect> {
+ let x = self.x.max(other.x);
+ let y = self.y.max(other.y);
+ let w = self.right().min(other.right()) - x;
+ let h = self.bottom().min(other.bottom()) - y;
+ (w > 0 && h > 0).then_some(Rect { x, y, w, h })
+ }
}
/// One sRGB channel to linear light (the WCAG transfer function).
@@ -153,6 +162,62 @@ fn cell_coverage(frame: &GridFrame, rect: &Rect) -> f32 {
(covered as f32 / area as f32).clamp(0.0, 1.0)
}
+/// Measure a block of RGBA pixels — the window-content path, where the
+/// backdrop is not derivable geometry and has to be looked at.
+///
+/// Spread comes from the 10th and 90th luminance percentiles rather than the
+/// full range, so one stray highlight (a cursor, an icon, an anti-aliased
+/// edge) does not report a whole terminal as high-variance. It is the same
+/// quantity the grid path computes analytically: how far apart the light and
+/// dark parts of this patch are.
+pub fn measure_pixels(rgba: &[u8]) -> Option<BackdropSample> {
+ let n = rgba.len() / 4;
+ if n == 0 {
+ return None;
+ }
+ let mut lumas: Vec<f32> = Vec::with_capacity(n);
+ let mut sum = 0.0f32;
+ for px in rgba.chunks_exact(4) {
+ let l = relative_luminance([px[0] as f32 / 255.0, px[1] as f32 / 255.0, px[2] as f32 / 255.0]);
+ sum += l;
+ lumas.push(l);
+ }
+ lumas.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+ let p10 = lumas[n / 10];
+ let p90 = lumas[n - 1 - n / 10];
+ Some(BackdropSample {
+ luma: ((sum / n as f32).clamp(0.0, 1.0) * 100.0).round() as u8,
+ spread: ((p90 - p10).clamp(0.0, 1.0) * 100.0).round() as u8,
+ })
+}
+
+/// Fold a window-content sample covering `coverage` (0-1) of a segment into
+/// the desktop sample for the rest of it.
+///
+/// The third spread term is the one that is easy to miss: two patches can each
+/// be perfectly uniform and still leave the text straddling a hard edge
+/// between them — a black terminal ending halfway across a segment that sits
+/// on a light gap. That boundary is exactly as unreadable as a busy texture,
+/// and only the difference between the two means shows it.
+pub fn blend(desktop: BackdropSample, window: BackdropSample, coverage: f32) -> BackdropSample {
+ let c = coverage.clamp(0.0, 1.0);
+ let dl = desktop.luma as f32 / 100.0;
+ let wl = window.luma as f32 / 100.0;
+ let luma = c * wl + (1.0 - c) * dl;
+ let edge = 2.0 * c.min(1.0 - c) * (wl - dl).abs();
+ let spread = (desktop.spread as f32 / 100.0)
+ .max(window.spread as f32 / 100.0)
+ .max(edge);
+ BackdropSample {
+ luma: (luma.clamp(0.0, 1.0) * 100.0).round() as u8,
+ spread: (spread.clamp(0.0, 1.0) * 100.0).round() as u8,
+ }
+}
+
+/// What a segment reports when its backdrop cannot be determined at all —
+/// mid luminance, full spread, which drives the outline.
+pub const UNKNOWN: BackdropSample = BackdropSample { luma: 50, spread: 100 };
+
/// Measure the backdrop under `rect`.
///
/// `base` is the opaque desktop background color the grid is drawn onto (the
@@ -165,7 +230,7 @@ fn cell_coverage(frame: &GridFrame, rect: &Rect) -> f32 {
/// what the text sits on.
pub fn measure(frame: &GridFrame, spec_gap: Rgba, base: [f32; 3], rect: Rect, occluded: bool) -> BackdropSample {
if occluded {
- return BackdropSample { luma: 50, spread: 100 };
+ return UNKNOWN;
}
let gap_rgb = over(spec_gap, base);
@@ -313,6 +378,72 @@ mod tests {
assert_eq!(s.luma, 100);
}
+ fn solid(luma_byte: u8, n: usize) -> Vec<u8> {
+ std::iter::repeat([luma_byte, luma_byte, luma_byte, 255]).take(n).flatten().collect()
+ }
+
+ #[test]
+ fn a_flat_patch_of_pixels_has_no_spread() {
+ let s = measure_pixels(&solid(0, 1000)).unwrap();
+ assert_eq!(s.luma, 0);
+ assert_eq!(s.spread, 0);
+ let s = measure_pixels(&solid(255, 1000)).unwrap();
+ assert_eq!(s.luma, 100);
+ assert_eq!(s.spread, 0);
+ }
+
+ #[test]
+ fn half_black_half_white_pixels_report_full_spread() {
+ let mut px = solid(0, 500);
+ px.extend(solid(255, 500));
+ let s = measure_pixels(&px).unwrap();
+ assert!(s.spread > 95, "spread was {}", s.spread);
+ assert!((45..=55).contains(&s.luma), "luma was {}", s.luma);
+ }
+
+ #[test]
+ fn a_lone_highlight_does_not_read_as_a_busy_backdrop() {
+ // A cursor or an icon on an otherwise flat terminal. The percentile
+ // spread is what keeps a handful of bright pixels from pinning the
+ // outline on over content the text reads fine against.
+ let mut px = solid(0, 990);
+ px.extend(solid(255, 10));
+ let s = measure_pixels(&px).unwrap();
+ assert_eq!(s.spread, 0, "spread was {}", s.spread);
+ }
+
+ #[test]
+ fn measure_pixels_rejects_an_empty_read() {
+ assert!(measure_pixels(&[]).is_none());
+ }
+
+ #[test]
+ fn blending_a_window_over_part_of_a_segment_moves_the_luma() {
+ let desktop = BackdropSample { luma: 0, spread: 0 };
+ let window = BackdropSample { luma: 100, spread: 0 };
+ assert_eq!(blend(desktop, window, 0.0).luma, 0);
+ assert_eq!(blend(desktop, window, 1.0).luma, 100);
+ assert_eq!(blend(desktop, window, 0.5).luma, 50);
+ }
+
+ #[test]
+ fn a_hard_edge_between_two_flat_patches_is_itself_spread() {
+ // A black terminal ending halfway across a segment that sits on a
+ // light gap: both halves uniform, the text across the seam is not.
+ let desktop = BackdropSample { luma: 100, spread: 0 };
+ let window = BackdropSample { luma: 0, spread: 0 };
+ assert_eq!(blend(desktop, window, 0.5).spread, 100);
+ // ...and at the edges of coverage there is no seam to worry about.
+ assert_eq!(blend(desktop, window, 0.02).spread, 4);
+ }
+
+ #[test]
+ fn blending_keeps_the_worse_of_the_two_spreads() {
+ let desktop = BackdropSample { luma: 50, spread: 10 };
+ let window = BackdropSample { luma: 50, spread: 80 };
+ assert_eq!(blend(desktop, window, 0.5).spread, 80);
+ }
+
#[test]
fn a_real_grid_frame_measures_without_panicking() {
// Exercises the real grid_frame output rather than a hand-built one,
diff --git a/src/server/output.rs b/src/server/output.rs
index e75cc6b..3a7bb73 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -136,6 +136,23 @@ pub struct RenderingState {
pub tearing: bool,
}
+/// One cached window-content reading — see `Output::status_win_samples`.
+pub struct StatusWinSample {
+ /// The occluding window's slotmap index.
+ pub win: u32,
+ /// The region sampled, in layout px. Part of the key: a segment that
+ /// moves, or a window that slides, is looking at different pixels.
+ pub region: crate::backdrop::Rect,
+ /// When the readback actually ran.
+ pub at: std::time::Instant,
+ /// Commit sequences of the window's surfaces, summed. A window that has
+ /// not committed cannot have changed what it is showing, so this is what
+ /// keeps a still terminal from being re-read four times a second forever.
+ pub seq: u32,
+ /// None when the content could not be read at all.
+ pub sample: Option<crate::backdrop::BackdropSample>,
+}
+
pub struct Output {
pub server: *mut Server,
pub wlr_output: *mut ffi::wlr_output,
@@ -195,6 +212,13 @@ pub struct Output {
pub cell_labels: crate::text::LabelCache,
/// Label point size actually in use, so a zoom change can re-rasterize.
pub last_label_px: u32,
+ /// Throttle+cache for the window-content half of the backdrop measurement.
+ /// Unlike the grid half, which is arithmetic, this one costs a texture
+ /// readback and a GPU sync, so it is re-taken at most every
+ /// `WIN_SAMPLE_MS` per segment AND only when the window has actually
+ /// committed something since. A window whose content changes faster than
+ /// that is not something the text contrast should be chasing anyway.
+ pub status_win_samples: Vec<StatusWinSample>,
pub destroy: ffi::wl_listener,
pub request_state: ffi::wl_listener,
@@ -462,6 +486,7 @@ impl Output {
cell_label_pool: Vec::new(),
cell_labels: Default::default(),
last_label_px: 0,
+ status_win_samples: Vec::new(),
destroy: std::mem::zeroed(),
request_state: std::mem::zeroed(),
frame: std::mem::zeroed(),
@@ -758,8 +783,8 @@ impl Output {
}
/// The half of [`Self::measure_status_backdrops`] that walks the windows:
- /// each status segment on this output measured against `frame`, anything
- /// else on screen treated as an occluder whose pixels are unknowable.
+ /// each status segment on this output measured against `frame`, with any
+ /// window covering part of it sampled for its actual content and folded in.
unsafe fn store_backdrops(
&mut self,
frame: &crate::policy::background::GridFrame,
@@ -783,6 +808,7 @@ impl Output {
};
let mut mine: Vec<(String, u8, u8)> = Vec::new();
+ let mut live_keys: Vec<(u32, crate::backdrop::Rect)> = Vec::new();
for &seg in wm.windows.iter() {
if !visible(seg) || !(*seg).is_status_bar() {
continue;
@@ -795,9 +821,15 @@ impl Output {
continue;
};
- // Anything that is not desktop furniture and overlaps the segment
- // is content this side cannot read — see the module docs.
- let mut occluded = false;
+ let desktop = crate::backdrop::measure(frame, gap, base, seg_rect, false);
+
+ // The window covering the most of this segment, if any. Stacking
+ // order is deliberately not consulted: the compositor's own
+ // hit-test answers with the segment itself (it is on top of
+ // whatever it is asking about), and where two windows both reach
+ // under one segment the larger share is the better guess at what
+ // the text is actually over.
+ let mut best: Option<(*mut crate::window::Window, i64)> = None;
for &other in wm.windows.iter() {
if other == seg || !visible(other) {
continue;
@@ -805,16 +837,41 @@ impl Output {
if (*other).is_status_bar() || (*other).is_wallpaper() || (*other).is_grid() {
continue;
}
- if rect_of(other).intersects(&seg_rect) {
- occluded = true;
- break;
+ let area = rect_of(other).intersect_area(&seg_rect);
+ if area > 0 && best.map_or(true, |(_, a)| area > a) {
+ best = Some((other, area));
}
}
- let s = crate::backdrop::measure(frame, gap, base, seg_rect, occluded);
- mine.push((app_id, s.luma, s.spread));
+ let sample = match best {
+ None => desktop,
+ Some((win, area)) => {
+ let region = rect_of(win).intersection(&seg_rect).unwrap_or(seg_rect);
+ let key = ((*win).ref_key.index, region);
+ live_keys.push(key);
+ match self.window_backdrop_sample(win, region) {
+ // Blended, not replaced: a window covering half a
+ // segment leaves the other half on the desktop, and
+ // the seam between them is its own legibility problem.
+ Some(w) => {
+ let coverage = area as f32 / (seg_rect.w as f32 * seg_rect.h as f32).max(1.0);
+ crate::backdrop::blend(desktop, w, coverage)
+ }
+ // Unreadable content (no committed buffer yet, an
+ // unsupported read format): the honest answer is still
+ // "unknown", exactly as before this path existed.
+ None => crate::backdrop::UNKNOWN,
+ }
+ }
+ };
+ mine.push((app_id, sample.luma, sample.spread));
}
+ // Drop cache entries for segment/window pairs that no longer exist,
+ // so a closed window or a moved segment cannot pin a stale reading.
+ self.status_win_samples
+ .retain(|e| live_keys.iter().any(|(k, r)| *k == e.win && *r == e.region));
+
{
let mut store = wm.status_backdrops.borrow_mut();
// Replace only this output's segments; another output's entries
@@ -828,6 +885,185 @@ impl Output {
wm.update_status();
}
+ /// The window-content half of the backdrop measurement: what `win` is
+ /// actually showing inside `region` (layout px), or None when it cannot be
+ /// read.
+ ///
+ /// Throttled and cached per (window, region) — this is the one part of the
+ /// measurement that costs a texture readback and its GPU sync, and it runs
+ /// inside the render path.
+ unsafe fn window_backdrop_sample(
+ &mut self,
+ win: *mut crate::window::Window,
+ region: crate::backdrop::Rect,
+ ) -> Option<crate::backdrop::BackdropSample> {
+ /// Re-read a window's content at most this often, per segment.
+ const WIN_SAMPLE_MS: u128 = 250;
+ /// Refuse to read back more than this many pixels in one sample. A bar
+ /// strip is naturally short, so this only trips on an implausibly wide
+ /// segment at a high buffer scale — where reporting "unknown" and
+ /// wearing the outline beats stalling the render thread.
+ const MAX_SAMPLE_PX: i64 = 512 * 1024;
+
+ let id = (*win).ref_key.index;
+ let now = std::time::Instant::now();
+ let seq = Self::surface_content_seq((*win).root_surface());
+ if let Some(hit) = self
+ .status_win_samples
+ .iter()
+ .find(|e| e.win == id && e.region == region)
+ {
+ // Two gates, and the content one is the load-bearing half: a
+ // window nobody is typing in never gets read a second time.
+ if hit.seq == seq || now.duration_since(hit.at).as_millis() < WIN_SAMPLE_MS {
+ return hit.sample;
+ }
+ }
+
+ let fresh = self.read_window_region(win, region, MAX_SAMPLE_PX);
+ match self
+ .status_win_samples
+ .iter_mut()
+ .find(|e| e.win == id && e.region == region)
+ {
+ Some(slot) => {
+ slot.at = now;
+ slot.seq = seq;
+ slot.sample = fresh;
+ }
+ None => self.status_win_samples.push(StatusWinSample {
+ win: id,
+ region,
+ at: now,
+ seq,
+ sample: fresh,
+ }),
+ }
+ fresh
+ }
+
+ /// Commit sequences of a surface tree, summed — a cheap "has this window
+ /// drawn anything new?" key. Walks subsurfaces too, because a toolkit that
+ /// renders into one can leave the root's own sequence untouched for the
+ /// life of the window.
+ unsafe fn surface_content_seq(root: *mut ffi::wlr_surface) -> u32 {
+ if root.is_null() {
+ return 0;
+ }
+ unsafe extern "C" fn sum_cb(
+ surface: *mut ffi::wlr_surface,
+ _sx: std::os::raw::c_int,
+ _sy: std::os::raw::c_int,
+ data: *mut std::ffi::c_void,
+ ) {
+ let total = &mut *(data as *mut u32);
+ *total = total.wrapping_add(ffi::river_wlr_surface_current_seq(surface));
+ }
+ let mut total: u32 = 0;
+ ffi::wlr_surface_for_each_surface(
+ root,
+ Some(sum_cb),
+ &mut total as *mut u32 as *mut std::ffi::c_void,
+ );
+ total
+ }
+
+ /// Read `region` (layout px) out of a window's committed surfaces and
+ /// measure it. Subsurfaces are composited in, because a toolkit that puts
+ /// its content in one would otherwise be measured as its blank root.
+ unsafe fn read_window_region(
+ &self,
+ win: *mut crate::window::Window,
+ region: crate::backdrop::Rect,
+ max_px: i64,
+ ) -> Option<crate::backdrop::BackdropSample> {
+ let root = (*win).root_surface();
+ if root.is_null() {
+ return None;
+ }
+ let (mut bw, mut bh) = (0i32, 0i32);
+ ffi::river_wlr_surface_get_buffer_size(root, &mut bw, &mut bh);
+ if bw <= 0 || bh <= 0 {
+ return None;
+ }
+ // Two scales stack here: the window's own render scale maps layout px
+ // to surface-logical px, and the buffer scale maps those to the
+ // physical pixels a texture read is addressed in.
+ let logical_w = ffi::river_wlr_surface_get_width(root).max(1);
+ let buf_scale = bw as f64 / logical_w as f64;
+ let win_scale = if (*win).scale > 0.0 { (*win).scale } else { 1.0 };
+ let to_buf = buf_scale / win_scale;
+
+ let rx = (((region.x - (*win).box_geom.x) as f64) * to_buf).round() as i32;
+ let ry = (((region.y - (*win).box_geom.y) as f64) * to_buf).round() as i32;
+ let rw = ((region.w as f64) * to_buf).round() as i32;
+ let rh = ((region.h as f64) * to_buf).round() as i32;
+ if rw <= 0 || rh <= 0 || (rw as i64) * (rh as i64) > max_px {
+ return None;
+ }
+
+ struct Collect {
+ list: Vec<(*mut ffi::wlr_surface, i32, i32)>,
+ }
+ unsafe extern "C" fn collect_cb(
+ surface: *mut ffi::wlr_surface,
+ sx: std::os::raw::c_int,
+ sy: std::os::raw::c_int,
+ data: *mut std::ffi::c_void,
+ ) {
+ let collect = &mut *(data as *mut Collect);
+ collect.list.push((surface, sx, sy));
+ }
+ let mut collect = Collect { list: Vec::new() };
+ ffi::wlr_surface_for_each_surface(
+ root,
+ Some(collect_cb),
+ &mut collect as *mut Collect as *mut std::ffi::c_void,
+ );
+
+ let mut canvas = vec![0u8; (rw as usize) * (rh as usize) * 4];
+ let mut composited = 0usize;
+ for (surface, sx, sy) in collect.list {
+ let texture = ffi::wlr_surface_get_texture(surface);
+ if texture.is_null() {
+ continue;
+ }
+ let (mut sw, mut sh) = (0i32, 0i32);
+ ffi::river_wlr_surface_get_buffer_size(surface, &mut sw, &mut sh);
+ if sw <= 0 || sh <= 0 {
+ continue;
+ }
+ // Subsurface offsets are surface-logical; buffers are physical.
+ let off_x = (sx as f64 * buf_scale).round() as i32;
+ let off_y = (sy as f64 * buf_scale).round() as i32;
+ let x0 = off_x.max(rx);
+ let y0 = off_y.max(ry);
+ let x1 = (off_x + sw).min(rx + rw);
+ let y1 = (off_y + sh).min(ry + rh);
+ if x1 <= x0 || y1 <= y0 {
+ continue;
+ }
+ let src = ffi::wlr_box {
+ x: x0 - off_x,
+ y: y0 - off_y,
+ width: x1 - x0,
+ height: y1 - y0,
+ };
+ let Some((pixels, format)) =
+ crate::screenshot::read_texture_region(texture, src, x1 - x0, y1 - y0)
+ else {
+ continue;
+ };
+ let Some(rgba) = crate::screenshot::to_rgba(pixels, format) else { continue };
+ crate::screenshot::blit(&mut canvas, rw, rh, &rgba, x1 - x0, y1 - y0, x0 - rx, y0 - ry);
+ composited += 1;
+ }
+ if composited == 0 {
+ return None;
+ }
+ crate::backdrop::measure_pixels(&canvas)
+ }
+
pub unsafe fn draw_adjust_overlay(&mut self) {
if self.adjust_tree.is_null() {
return;
diff --git a/src/server/screenshot.rs b/src/server/screenshot.rs
index 8f9cd12..dd2d15d 100644
--- a/src/server/screenshot.rs
+++ b/src/server/screenshot.rs
@@ -197,10 +197,42 @@ unsafe fn read_texture(texture: *mut ffi::wlr_texture, w: i32, h: i32) -> Option
Some((data, format))
}
+/// Read back only `src` (in buffer px) of a texture, into a `w`×`h` buffer.
+///
+/// The full-texture [`read_texture`] is fine for a screenshot, which wants
+/// every pixel anyway; it is not fine for the backdrop sampler, which wants a
+/// bar-height strip out of a window that may be 4K — 33MB copied per sample to
+/// look at 0.3% of it.
+pub(crate) unsafe fn read_texture_region(
+ texture: *mut ffi::wlr_texture,
+ src: ffi::wlr_box,
+ w: i32,
+ h: i32,
+) -> Option<(Vec<u8>, u32)> {
+ if texture.is_null() || w <= 0 || h <= 0 || src.width <= 0 || src.height <= 0 {
+ return None;
+ }
+ let format = ffi::wlr_texture_preferred_read_format(texture);
+ let bpp = bytes_per_pixel(format)?;
+ let mut data = vec![0u8; (w as usize) * (h as usize) * bpp];
+ let options = ffi::wlr_texture_read_pixels_options {
+ data: data.as_mut_ptr() as *mut std::ffi::c_void,
+ format,
+ stride: (w as u32) * bpp as u32,
+ dst_x: 0,
+ dst_y: 0,
+ src_box: src,
+ };
+ if !ffi::wlr_texture_read_pixels(texture, &options) {
+ return None;
+ }
+ Some((data, format))
+}
+
/// Convert read-back pixels to RGBA. Alpha is forced opaque — the X-variants
/// carry garbage alpha, the 24-bit formats carry none at all, and screenshots
/// should not be translucent.
-fn to_rgba(mut pixels: Vec<u8>, format: u32) -> Option<Vec<u8>> {
+pub(crate) fn to_rgba(mut pixels: Vec<u8>, format: u32) -> Option<Vec<u8>> {
match format {
DRM_FORMAT_XRGB8888 | DRM_FORMAT_ARGB8888 => {
for px in pixels.chunks_exact_mut(4) {
@@ -370,7 +402,7 @@ pub unsafe fn capture_window_rgba(window: *mut crate::window::Window) -> Result<
}
/// Copy `src` (sw×sh RGBA) into `dst` (dw×dh RGBA) at (dx, dy), clipped.
-fn blit(dst: &mut [u8], dw: i32, dh: i32, src: &[u8], sw: i32, sh: i32, dx: i32, dy: i32) {
+pub(crate) fn blit(dst: &mut [u8], dw: i32, dh: i32, src: &[u8], sw: i32, sh: i32, dx: i32, dy: i32) {
for sy in 0..sh {
let ty = dy + sy;
if ty < 0 || ty >= dh {
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index dba2fcb..a7ba069 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -629,6 +629,13 @@ int river_wlr_surface_get_height(struct wlr_surface *surface) {
return surface->current.height;
}
+// Commit sequence of a surface's current state. Cheap "has this drawn
+// anything new?" key for the status-bar backdrop sampler, which is trying
+// hard NOT to read back a texture it has already read.
+uint32_t river_wlr_surface_current_seq(struct wlr_surface *surface) {
+ return surface->current.seq;
+}
+
void river_wlr_surface_get_buffer_size(struct wlr_surface *surface, int *width, int *height) {
*width = surface->current.buffer_width;
*height = surface->current.buffer_height;
diff --git a/wrapper.h b/wrapper.h
index 8c489d8..c1b74e4 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -247,6 +247,7 @@ struct wlr_surface *river_wlr_seat_get_keyboard_focused_surface(struct wlr_seat
int river_wlr_surface_get_width(struct wlr_surface *surface);
int river_wlr_surface_get_height(struct wlr_surface *surface);
void river_wlr_surface_get_buffer_size(struct wlr_surface *surface, int *width, int *height);
+uint32_t river_wlr_surface_current_seq(struct wlr_surface *surface);
struct wlr_keyboard *river_wlr_input_method_keyboard_grab_v2_get_keyboard(struct wlr_input_method_keyboard_grab_v2 *grab);
struct wl_signal *river_wlr_input_method_keyboard_grab_v2_get_destroy_signal(struct wlr_input_method_keyboard_grab_v2 *grab);