srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/rounded_corners_pixman.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/rounded_corners_pixman.rs')
-rw-r--r--crates/wayland/src/rounded_corners_pixman.rs205
1 files changed, 107 insertions, 98 deletions
diff --git a/crates/wayland/src/rounded_corners_pixman.rs b/crates/wayland/src/rounded_corners_pixman.rs
index 1879b9e..3a712f9 100644
--- a/crates/wayland/src/rounded_corners_pixman.rs
+++ b/crates/wayland/src/rounded_corners_pixman.rs
@@ -5,122 +5,131 @@
//! 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.
+//! The technique here: render the window's *entire* surface tree (root
+//! plus every subsurface, exactly what the ordinary unmasked path already
+//! draws) into a private off-screen buffer, read that back as plain BGRA8
+//! bytes, and punch the four corner holes into *those* - the composited
+//! result, not any one client buffer - before handing it to
+//! `MemoryRenderBuffer`, the same type and render-element path already
+//! used for the titlebar/border/shadow bitmaps.
//!
-//! 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.
+//! A previous version of this instead tried to identify *which one*
+//! surface in the tree held "the real content" (a root-plus-one-child
+//! GTK4/WebRender pattern, confirmed live against Firefox and Chrome) and
+//! masked that single client buffer directly, skipping everything else in
+//! the tree. That was cheaper - no extra render pass - but structurally
+//! fragile: it assumed the *rest* of the tree (whatever the chosen surface
+//! didn't cover) was always invisible padding, true for Chrome's own
+//! shadow-margin inset but false for Firefox, whose tab strip/title row is
+//! painted on the *root* surface, outside its own content child. The
+//! moment that surface-picking heuristic got permissive enough to actually
+//! mask Firefox's real, common case, it started *silently deleting
+//! Firefox's own tab strip* - reported live as "Firefox's titlebar turned
+//! invisible", confirmed by toggling `general.rounded_corners` off, which
+//! brought it straight back. Rendering the whole tree and masking the
+//! *output* instead of guessing which *input* is real sidesteps the whole
+//! question - the same reason a GPU shader-based compositor (niri,
+//! cosmic-comp) never has this class of bug at all: by the time its own
+//! shader runs, the subsurface tree is already flattened into one texture,
+//! so there is nothing left to misidentify.
+//!
+//! Only `wl_shm`/dmabuf-agnostic now - unlike the old per-buffer read,
+//! this never touches a client's own buffer format at all, only the
+//! renderer's own composited output, so the format/transform restrictions
+//! the previous version needed (`Argb8888`/`Xrgb8888` only, no dmabuf
+//! without a dedicated read path, `Transform::Normal` only) no longer
+//! apply - whatever the renderer can already draw (which is everything it
+//! draws for the ordinary unmasked path too), this can mask.
//!
//! 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.
+//! comment): a full extra off-screen render pass (allocate a buffer, draw
+//! the tree into it, read the result back) on every real content change,
+//! not just a raw memory copy the old approach needed - strictly more
+//! expensive per rebuild than before, though still gated by the same
+//! `CompState::content_epoch` cache as before, so an idle/static window
+//! still costs nothing per frame, only per genuine repaint.
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::backend::renderer::damage::OutputDamageTracker;
+use smithay::backend::renderer::element::surface::render_elements_from_surface_tree;
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::pixman::PixmanRenderer;
+use smithay::backend::renderer::{Bind, ExportMem, Offscreen};
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
-use smithay::utils::Transform;
-use smithay::wayland::compositor::get_children;
-use smithay::wayland::shm::{with_buffer_contents, BufferData};
+use smithay::utils::{Buffer as BufferCoord, Rectangle, Transform};
-/// 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> {
- // The module doc comment above has always claimed "only a window's main
- // surface (no subsurfaces)... falls back to None" - but nothing here
- // actually checked that; this function read `surface`'s own buffer
- // unconditionally regardless of whether it had children. A window whose
- // real content is painted into a subsurface (a common GTK4/WebRender
- // pattern - confirmed live: Firefox does this) has its own root
- // surface holding only a blank/background buffer, so masking succeeded
- // and produced a buffer, just the wrong one - the actual page content
- // in the child subsurface was never read at all, and the window
- // rendered as blank with rounded corners on instead of falling back to
- // the unmasked path (`surface_content_elements`), which does walk the
- // full surface tree and shows real content correctly.
- if !get_children(surface).is_empty() {
- return None;
- }
- 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 {
+/// Renders `surface`'s whole subsurface tree into a private `size`-sized
+/// off-screen buffer - `loc` is the tree's own root-surface-relative
+/// origin to render at, exactly like `render_elements_from_surface_tree`'s
+/// own `location` parameter elsewhere in this codebase (`udev/capture.rs`);
+/// the caller passes the *negated* `content_offset`
+/// (`dwindow.geometry().loc`, the client's own declared shadow-margin
+/// inset) so the buffer's own `(0, 0)` lands exactly on the window's real
+/// visible content top-left, the same correction every other render path
+/// in this compositor already applies (see `udev/render.rs`'s own `pos`
+/// computation) - then punches the four rounded-corner holes into the
+/// result. Returns tightly-packed BGRA8 bytes (`size.0 * size.1 * 4`),
+/// ready for `MemoryRenderBuffer::from_slice`, or `None` if the off-screen
+/// render itself failed (a genuine renderer error, not "this window isn't
+/// shaped right for masking" - there is no such restriction anymore).
+///
+/// `radius` is already in the same units as `size` (this compositor's
+/// outputs are always scale `1.0`, per `WindowManager::rounded_corners_
+/// enabled`'s own doc comment, so there is no separate buffer-scale
+/// factor to fold in here the way the old per-client-buffer read needed).
+pub(crate) fn masked_content_buffer(renderer: &mut PixmanRenderer, surface: &WlSurface, loc: (i32, i32), size: (i32, i32), radius: f32, corners: RoundedCorners) -> Option<Vec<u8>> {
+ let (w, h) = size;
+ if w <= 0 || h <= 0 {
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) {
+ // Transparent clear (not opaque black, unlike `udev/capture.rs`'s own
+ // off-screen render): this buffer holds only the window's own content,
+ // with nothing behind it to composite against yet - any area the
+ // surface tree doesn't actually draw into (a subsurface smaller than
+ // its own declared geometry, say) needs to stay real, punch-through
+ // transparency so the border/desktop already drawn underneath on the
+ // real output shows through there, not a solid black patch.
+ let elements = render_elements_from_surface_tree::<_, crate::elements::OverlayElement<PixmanRenderer>>(renderer, surface, loc, 1.0, 1.0, Kind::Unspecified);
+ let mut target = match renderer.create_buffer(Fourcc::Argb8888, (w, h).into()) {
+ Ok(t) => t,
+ Err(e) => {
+ log::debug!("rounded_corners_pixman: masked_content_buffer: create_buffer failed ({e:?}) - giving up unmasked");
return None;
}
- let (w, h, stride, offset) = (data.width, data.height, data.stride, data.offset);
- if w <= 0 || h <= 0 || stride <= 0 || offset < 0 {
+ };
+ let mut framebuffer = match renderer.bind(&mut target) {
+ Ok(fb) => fb,
+ Err(e) => {
+ log::debug!("rounded_corners_pixman: masked_content_buffer: bind failed ({e:?}) - giving up unmasked");
return None;
}
- let needed = offset as usize + stride as usize * h as usize;
- if needed > len {
+ };
+ let mut tracker = OutputDamageTracker::new((w, h), 1.0, Transform::Normal);
+ if let Err(e) = tracker.render_output(renderer, &mut framebuffer, 0, &elements, [0.0, 0.0, 0.0, 0.0]) {
+ log::debug!("rounded_corners_pixman: masked_content_buffer: render_output failed ({e:?}) - giving up unmasked");
+ return None;
+ }
+ let region: Rectangle<i32, BufferCoord> = Rectangle::new((0, 0).into(), (w, h).into());
+ let mapping = match renderer.copy_framebuffer(&framebuffer, region, Fourcc::Argb8888) {
+ Ok(m) => m,
+ Err(e) => {
+ log::debug!("rounded_corners_pixman: masked_content_buffer: copy_framebuffer failed ({e:?}) - giving up unmasked");
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 pixels = match renderer.map_texture(&mapping) {
+ Ok(p) => p,
+ Err(e) => {
+ log::debug!("rounded_corners_pixman: masked_content_buffer: map_texture failed ({e:?}) - giving up unmasked");
+ return None;
}
-
- 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))
+ };
+ let mut out = pixels.to_vec();
+ let radius_px = radius.min(w as f32 / 2.0).min(h as f32 / 2.0);
+ apply_corner_mask(&mut out, w, h, w * 4, radius_px, corners);
+ Some(out)
}
/// Zeroes (fading over ~2px, matching `rounded_corners::FRAGMENT_SHADER`'s