srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland')
-rw-r--r--crates/wayland/src/elements.rs41
-rw-r--r--crates/wayland/src/lib.rs1
-rw-r--r--crates/wayland/src/protocols.rs6
-rw-r--r--crates/wayland/src/rounded_corners_pixman.rs199
-rw-r--r--crates/wayland/src/state.rs18
-rw-r--r--crates/wayland/src/udev.rs30
-rw-r--r--crates/wayland/src/winit.rs9
7 files changed, 302 insertions, 2 deletions
diff --git a/crates/wayland/src/elements.rs b/crates/wayland/src/elements.rs
index c9f1d6c..c8cb10c 100644
--- a/crates/wayland/src/elements.rs
+++ b/crates/wayland/src/elements.rs
@@ -147,6 +147,47 @@ where
render_elements_from_surface_tree(renderer, surface, location, 1.0, alpha, Kind::Unspecified)
}
+/// Looks up (rebuilding first if stale) the Pixman-backend rounded-corner
+/// masked copy of `surface`'s content - see `rounded_corners_pixman`'s
+/// module doc comment for what this actually does and why it needs a cache
+/// at all. `epoch` is the window's current `CompState::content_epoch`
+/// value (bumped once per real commit, in `commit()`); the cached entry is
+/// only rebuilt when that no longer matches what it was last built from, so
+/// a window that isn't currently repainting costs nothing here beyond one
+/// `HashMap` lookup per frame.
+///
+/// Free function taking the two fields it needs directly, rather than a
+/// `CompState` method, so it can be called from inside `udev.rs`'s render
+/// loop alongside the already-live `self.udev.as_mut()` borrow - see that
+/// call site.
+///
+/// `None` either because masking genuinely isn't possible right now (falls
+/// through to `rounded_corners_pixman::masked_content_buffer`'s own `None`
+/// cases) or because it hasn't been attempted yet this call; either way the
+/// caller's fallback is the same: render `surface`'s content unrounded via
+/// [`surface_content_elements`].
+pub(crate) fn rounded_content_buffer<'a>(
+ cache: &'a mut std::collections::HashMap<srdwm_core::WindowId, (u64, smithay::backend::renderer::element::memory::MemoryRenderBuffer)>,
+ epoch: u64,
+ id: srdwm_core::WindowId,
+ surface: &WlSurface,
+ radius: f32,
+ corners: crate::rounded_corners::RoundedCorners,
+) -> Option<&'a smithay::backend::renderer::element::memory::MemoryRenderBuffer> {
+ let stale = cache.get(&id).map(|(built, _)| *built != epoch).unwrap_or(true);
+ if stale {
+ match crate::rounded_corners_pixman::masked_content_buffer(surface, radius, corners) {
+ Some(buf) => {
+ cache.insert(id, (epoch, buf));
+ }
+ None => {
+ cache.remove(&id);
+ }
+ }
+ }
+ cache.get(&id).map(|(_, b)| b)
+}
+
/// Every mapped layer-shell surface on `output` whose [`Layer`] `include`
/// accepts, each rendered via [`surface_content_elements`] at full opacity
/// - layer-shell surfaces (bars, docks, wallpaper engines) don't have a
diff --git a/crates/wayland/src/lib.rs b/crates/wayland/src/lib.rs
index 7687812..711006d 100644
--- a/crates/wayland/src/lib.rs
+++ b/crates/wayland/src/lib.rs
@@ -50,6 +50,7 @@ mod output_management;
mod output_power;
mod protocols;
mod rounded_corners;
+mod rounded_corners_pixman;
mod screencopy;
mod state;
mod udev;
diff --git a/crates/wayland/src/protocols.rs b/crates/wayland/src/protocols.rs
index 9f23c69..f680dc3 100644
--- a/crates/wayland/src/protocols.rs
+++ b/crates/wayland/src/protocols.rs
@@ -123,6 +123,12 @@ impl CompositorHandler for CompState {
if let Some(w) = self.id_to_window.get(&id) {
w.on_commit();
}
+ // See `content_epoch`'s doc comment: this is the only per-commit
+ // signal the udev backend's rounded-corner mask cache has to
+ // invalidate itself, since content can change every frame,
+ // independent of the geometry-driven points `redraw_decoration_
+ // buffer` already runs at.
+ *self.content_epoch.entry(id).or_insert(0) += 1;
crate::state::sync_toplevel_metadata(self, id, surface);
}
self.ensure_layer_initial_configure(surface);
diff --git a/crates/wayland/src/rounded_corners_pixman.rs b/crates/wayland/src/rounded_corners_pixman.rs
new file mode 100644
index 0000000..1aaa5bc
--- /dev/null
+++ b/crates/wayland/src/rounded_corners_pixman.rs
@@ -0,0 +1,199 @@
+//! Real rounded corners on a window's own client content - udev/Pixman
+//! backend. `rounded_corners.rs` covers the GLES/winit backend with a
+//! fragment shader; `PixmanRenderer` is software-only and has no shader
+//! stage to hook that into at all (`PixmanFrame::render_texture_from_to`'s
+//! mask is a hardcoded flat alpha, and its destination image is private to
+//! smithay's own module - no public hook for a custom mask picture).
+//!
+//! The technique here instead bakes the mask into a *copy* of the client's
+//! own pixel data before it ever reaches the normal compositing path: read
+//! the surface's committed `wl_shm` buffer, punch premultiplied-alpha holes
+//! (this codebase's existing BGRA convention - see `decoration::
+//! shadow_bitmap`'s doc comment) into the four corner regions, and hand the
+//! result to `MemoryRenderBuffer` - the exact same type and render-element
+//! path already used for the titlebar/border/shadow bitmaps. Rendering it
+//! through the ordinary unmasked `render_texture_from_to` is what makes the
+//! corners actually disappear: a premultiplied-zero source pixel there
+//! contributes nothing, leaving whatever was already drawn underneath (the
+//! desktop, or another window) showing through - a real cutout, not a
+//! flat-colour patch.
+//!
+//! Deliberately narrow scope, same as the GLES version: only a window's
+//! *main* surface (no subsurfaces), and only the two common `wl_shm`
+//! formats this compositor's own bitmaps already use (`Argb8888`/
+//! `Xrgb8888`) - anything else, a non-`wl_shm` buffer (dmabuf, a GL
+//! client), or a non-identity buffer transform falls back to `None`, which
+//! the caller treats as "render this window's content unrounded" rather
+//! than an error.
+//!
+//! Cost, and why this stays default-off on this backend (`general.
+//! rounded_corners`, see `WindowManager::rounded_corners_enabled`'s doc
+//! comment): unlike the GLES shader, which the GPU evaluates once per pixel
+//! at zero extra CPU cost, this masks a full copy of the surface's pixel
+//! data on the CPU. The mask math itself only touches the four small
+//! corner boxes, but producing a tightly-packed buffer `MemoryRenderBuffer::
+//! from_slice` accepts (it asserts a `width * 4` stride; a client's own SHM
+//! stride is often larger, padded for alignment) means copying the whole
+//! buffer row by row regardless. The caller is expected to cache the
+//! result and only call this again when the surface's content has actually
+//! changed (see `CompState::content_epoch`), so the real per-frame cost for
+//! idle/static windows is nothing - but a constantly-repainting client
+//! (video, a terminal under heavy scrollback) pays this on every commit for
+//! as long the feature stays on, which is exactly the untested-on-real-
+//! hardware cost the opt-in default exists to avoid forcing on anyone.
+
+use crate::rounded_corners::RoundedCorners;
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
+use smithay::backend::renderer::utils::{with_renderer_surface_state, RendererSurfaceState};
+use smithay::reexports::wayland_server::protocol::wl_shm;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::utils::Transform;
+use smithay::wayland::shm::{with_buffer_contents, BufferData};
+
+/// Builds a rounded-corner-masked copy of `surface`'s own committed content,
+/// or `None` if that isn't possible right now - see this module's doc
+/// comment for every case that falls back rather than erroring. `radius` is
+/// in the same logical-pixel units as `decoration::CORNER_RADIUS`; scaled
+/// up to buffer pixels internally using the surface's own buffer scale.
+pub(crate) fn masked_content_buffer(surface: &WlSurface, radius: f32, corners: RoundedCorners) -> Option<MemoryRenderBuffer> {
+ let (buffer, scale, transform) = with_renderer_surface_state(surface, |state: &mut RendererSurfaceState| {
+ let buffer = state.buffer()?.clone();
+ Some((buffer, state.buffer_scale(), state.buffer_transform()))
+ })??;
+ // A rotated/flipped buffer would need the mask rotated with it; not
+ // worth the extra math for a cosmetic, already-narrow-scope pass.
+ if transform != Transform::Normal {
+ return None;
+ }
+ let radius_px = radius * scale as f32;
+
+ let (data, w, h) = with_buffer_contents(&buffer, move |ptr, len, data: BufferData| -> Option<(Vec<u8>, i32, i32)> {
+ if !matches!(data.format, wl_shm::Format::Argb8888 | wl_shm::Format::Xrgb8888) {
+ return None;
+ }
+ let (w, h, stride, offset) = (data.width, data.height, data.stride, data.offset);
+ if w <= 0 || h <= 0 || stride <= 0 || offset < 0 {
+ return None;
+ }
+ let needed = offset as usize + stride as usize * h as usize;
+ if needed > len {
+ return None;
+ }
+ // SAFETY: `pool.with_data` (inside `with_buffer_contents`) already
+ // validated `ptr`/`len` cover the whole pool; `needed` above
+ // re-checks this buffer's own slice sits inside that before a
+ // single byte is read.
+ let src = unsafe { std::slice::from_raw_parts(ptr.add(offset as usize), stride as usize * h as usize) };
+
+ // Repack into a tight `width * 4` stride: `MemoryRenderBuffer::
+ // from_slice` computes its own stride from `width` alone and
+ // asserts the data matches it, so the source's (often padded) SHM
+ // stride can't be handed through as-is.
+ let row_bytes = w as usize * 4;
+ let mut out = vec![0u8; row_bytes * h as usize];
+ for y in 0..h as usize {
+ let src_row = &src[y * stride as usize..y * stride as usize + row_bytes];
+ out[y * row_bytes..(y + 1) * row_bytes].copy_from_slice(src_row);
+ }
+
+ let radius_px = radius_px.min(w as f32 / 2.0).min(h as f32 / 2.0);
+ apply_corner_mask(&mut out, w, h, row_bytes as i32, radius_px, corners);
+ Some((out, w, h))
+ })
+ .ok()
+ .flatten()?;
+
+ Some(MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (w, h), scale, Transform::Normal, None))
+}
+
+/// Zeroes (fading over ~2px, matching `rounded_corners::FRAGMENT_SHADER`'s
+/// `smoothstep`) every premultiplied BGRA pixel in `buf` that falls outside
+/// the rounded rect described by `radius` at each of the four corners
+/// `corners` selects. Only walks the four `radius`-sized corner boxes, not
+/// the whole image - everywhere else the mask is exactly `1.0`, a no-op.
+///
+/// Uses the same `clamp`-then-`distance` construction as the GLSL version
+/// rather than a per-corner mirrored center, so one formula handles all
+/// four boxes correctly regardless of which edges of the image they sit
+/// against.
+fn apply_corner_mask(buf: &mut [u8], w: i32, h: i32, stride: i32, radius: f32, corners: RoundedCorners) {
+ if radius < 1.0 || w <= 0 || h <= 0 {
+ return;
+ }
+ let r = radius.ceil() as i32;
+ let (wf, hf) = (w as f32, h as f32);
+ let boxes = [
+ (corners.top_left, 0, 0, r.min(w), r.min(h)),
+ (corners.top_right, (w - r).max(0), 0, w, r.min(h)),
+ (corners.bottom_left, 0, (h - r).max(0), r.min(w), h),
+ (corners.bottom_right, (w - r).max(0), (h - r).max(0), w, h),
+ ];
+ for (enabled, x0, y0, x1, y1) in boxes {
+ if !enabled {
+ continue;
+ }
+ for y in y0..y1 {
+ let py = y as f32 + 0.5;
+ let cy = py.clamp(radius, hf - radius);
+ for x in x0..x1 {
+ let px = x as f32 + 0.5;
+ let cx = px.clamp(radius, wf - radius);
+ let dist = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
+ if dist <= radius - 1.0 {
+ continue;
+ }
+ let mask = 1.0 - smoothstep(radius - 1.0, radius + 1.0, dist);
+ let i = (y * stride + x * 4) as usize;
+ if i + 4 > buf.len() {
+ continue;
+ }
+ for b in &mut buf[i..i + 4] {
+ *b = (*b as f32 * mask).round() as u8;
+ }
+ }
+ }
+ }
+}
+
+fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
+ let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
+ t * t * (3.0 - 2.0 * t)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn corner_mask_leaves_the_interior_untouched() {
+ let (w, h) = (20, 20);
+ let stride = w * 4;
+ let mut buf = vec![200u8; (stride * h) as usize];
+ apply_corner_mask(&mut buf, w, h, stride, 6.0, RoundedCorners::ALL);
+ let center = ((h / 2 * stride) + (w / 2) * 4) as usize;
+ assert_eq!(&buf[center..center + 4], &[200, 200, 200, 200]);
+ }
+
+ #[test]
+ fn corner_mask_zeroes_the_outermost_corner_pixel() {
+ let (w, h) = (20, 20);
+ let stride = w * 4;
+ let mut buf = vec![200u8; (stride * h) as usize];
+ apply_corner_mask(&mut buf, w, h, stride, 6.0, RoundedCorners::ALL);
+ // Pixel (0, 0) is `sqrt(2) * 6 ≈ 8.49` px from the corner's arc
+ // center at (6, 6) - well past `radius + 1`, so fully masked.
+ assert_eq!(&buf[0..4], &[0, 0, 0, 0]);
+ }
+
+ #[test]
+ fn bottom_only_leaves_the_top_corners_alone() {
+ let (w, h) = (20, 20);
+ let stride = w * 4;
+ let mut buf = vec![200u8; (stride * h) as usize];
+ apply_corner_mask(&mut buf, w, h, stride, 6.0, RoundedCorners::BOTTOM_ONLY);
+ assert_eq!(&buf[0..4], &[200, 200, 200, 200]);
+ let bottom_left = ((h - 1) * stride) as usize;
+ assert_eq!(&buf[bottom_left..bottom_left + 4], &[0, 0, 0, 0]);
+ }
+}
diff --git a/crates/wayland/src/state.rs b/crates/wayland/src/state.rs
index 4416869..5b5b4e0 100644
--- a/crates/wayland/src/state.rs
+++ b/crates/wayland/src/state.rs
@@ -259,6 +259,22 @@ pub(crate) struct CompState {
/// declare on the shared `CompState` even though only one backend ever
/// populates it.
pub(crate) rounded_corners_program: Option<smithay::backend::renderer::gles::GlesTexProgram>,
+ /// Bumped once per real `commit()` of a mapped window's surface --
+ /// see that handler in `protocols.rs`. The only signal `rounded_content_buffers`
+ /// needs to know its cached masked copy is stale, since content changes
+ /// (unlike geometry, which `redraw_decoration_buffer`'s trigger points
+ /// already cover) can arrive on every single frame for a video or
+ /// terminal, with nothing else in this struct tracking that.
+ pub(crate) content_epoch: HashMap<WindowId, u64>,
+ /// The udev/Pixman-backend rounded-corner masked copy of a window's own
+ /// content (`rounded_corners_pixman::masked_content_buffer`), paired
+ /// with the `content_epoch` value it was built from - see
+ /// `elements::rounded_content_buffer`, which owns rebuilding this.
+ /// Always empty on the winit backend (GLES rounds via a shader instead,
+ /// `rounded_corners_program`), but costs nothing to declare here
+ /// unconditionally, the same call `rounded_corners_program` itself
+ /// already makes.
+ pub(crate) rounded_content_buffers: HashMap<WindowId, (u64, MemoryRenderBuffer)>,
/// Persistent solid-colour buffers backing a window's other three
/// border strips (bottom, left, right - `decoration::border_strips`'
/// order past index 0), reused by position every frame rather than
@@ -662,6 +678,8 @@ impl CompState {
self.shadow_buffers.remove(&id);
self.border_side_buffers.remove(&id);
self.last_synced_size.remove(&id);
+ self.content_epoch.remove(&id);
+ self.rounded_content_buffers.remove(&id);
// A window closing (crash, kill, or its own menu's "Close" action
// racing ahead of this) while its context menu is still open would
// otherwise leave the menu pointing at a dead id - selecting any
diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs
index 40c0ce5..cdea265 100644
--- a/crates/wayland/src/udev.rs
+++ b/crates/wayland/src/udev.rs
@@ -207,6 +207,10 @@ impl CompState {
// looked up fresh per head (head-local `origin` translation).
let ids: Vec<srdwm_core::WindowId> = if locked { Vec::new() } else { self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect() };
let focused = self.wm.borrow().focused_id();
+ // Default `false` here, unlike winit's `unwrap_or(true)` - see
+ // `rounded_corners_pixman`'s module doc comment for the CPU cost
+ // that makes this backend opt-in rather than on by default.
+ let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled.unwrap_or(false);
let popup_targets = if locked { Vec::new() } else { crate::elements::popup_targets(self) };
// Which heads are eligible, and what each needs, gathered before the
@@ -458,7 +462,29 @@ impl CompState {
if let Some(surface) = crate::elements::window_wl_surface(dwindow) {
let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 };
let pos = (geom.x - origin.x, geom.y + band - origin.y);
- custom_elements.extend(crate::elements::surface_content_elements(&mut udev.renderer, &surface, pos, w.opacity));
+ let mut rounded_elem = None;
+ if rounded_corners_enabled {
+ let epoch = self.content_epoch.get(&id).copied().unwrap_or(0);
+ // Bottom-only for a decorated window, same
+ // reasoning as `winit.rs`'s identical split:
+ // the top two corners are already hidden
+ // under the titlebar band's own rounded
+ // bitmap.
+ let corners = if w.decorated { crate::rounded_corners::RoundedCorners::BOTTOM_ONLY } else { crate::rounded_corners::RoundedCorners::ALL };
+ if let Some(buffer) =
+ crate::elements::rounded_content_buffer(&mut self.rounded_content_buffers, epoch, id, &surface, decoration::CORNER_RADIUS as f32, corners)
+ {
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, (pos.0 as f64, pos.1 as f64), buffer, Some(w.opacity), None, None, Kind::Unspecified)
+ {
+ Ok(elem) => rounded_elem = Some(elem),
+ Err(e) => log::warn!("udev: failed to import rounded content buffer: {e}"),
+ }
+ }
+ }
+ match rounded_elem {
+ Some(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ None => custom_elements.extend(crate::elements::surface_content_elements(&mut udev.renderer, &surface, pos, w.opacity)),
+ }
}
}
occluders.push(geom);
@@ -1080,6 +1106,8 @@ impl UdevPlatform {
border_top_decorations: HashMap::new(),
shadow_buffers: HashMap::new(),
rounded_corners_program: None,
+ content_epoch: HashMap::new(),
+ rounded_content_buffers: HashMap::new(),
border_side_buffers: HashMap::new(),
last_synced_size: HashMap::new(),
pending: pending.clone(),
diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs
index 5f072b6..f50d095 100644
--- a/crates/wayland/src/winit.rs
+++ b/crates/wayland/src/winit.rs
@@ -242,6 +242,8 @@ impl WaylandPlatform {
border_top_decorations: HashMap::new(),
shadow_buffers: HashMap::new(),
rounded_corners_program,
+ content_epoch: HashMap::new(),
+ rounded_content_buffers: HashMap::new(),
border_side_buffers: HashMap::new(),
last_synced_size: HashMap::new(),
pending: pending.clone(),
@@ -406,7 +408,12 @@ impl WaylandPlatform {
// The bar/dock/launcher, skipped entirely for a fullscreen window --
// see `udev.rs`'s matching push for the full reasoning.
let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen);
- let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled;
+ // `None` (the user's config never touched `general.rounded_corners`)
+ // defaults to *on* here - this backend has an actual GPU shader
+ // behind the feature (see `rounded_corners.rs`), no untested
+ // per-frame CPU cost to weigh the way the udev backend's own
+ // default has to.
+ let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled.unwrap_or(true);
if !hide_top_layers {
custom_elements.extend(
crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay))