Skip to content

st: Stop box-shadows from freezing Cinnamon (blur a corner-sized silhouette) - #14046

Open
rumours86 wants to merge 3 commits into
linuxmint:masterfrom
rumours86:st-sliced-box-shadow
Open

rumours86 wants to merge 3 commits into
linuxmint:masterfrom
rumours86:st-sliced-box-shadow

Conversation

@rumours86

@rumours86 rumours86 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Cinnamon froze for up to two seconds whenever a notification arrived. A profile of the main thread during those freezes: 88% in blur_pixels(), called from st_theme_node_render_resources().

St blurs a box-shadow on the CPU, from a silhouette as large as the whole widget, every time the widget changes size. With Mint-Y at 2x scale (#notification { box-shadow: 0 1px 15px 4px }), a 959x181 notification popup took ~280 ms. A notification applet's menu that lists 14 old notifications is 1055x3748, and blurring its shadow took 1.8 s. Any tall menu pays the same cost the first time it opens at a new size.

1. st: Blur box-shadows from a corner-sized silhouette

Away from its corners, a box's shadow is the same all along each edge. This blurs a silhouette that only just holds the corners and borders, left + 2 * ceil(1.5 * blur) + 1 + right pixels wide, with the analogous calculation for height. Each side includes the largest border radius, matching the padding used by the border renderer. When painting, its middle row and column are stretched over the rest of the widget. The 9 slices go exactly where _st_paint_shadow_with_opacity() would have stretched the full-size texture, so the shadow looks the same. Shadows made from a border image or a prerendered background (gradients, large corners, inset shadows), and widgets smaller than the silhouette or with fractional allocation sizes, keep the old path. This is close to what gnome-shell does in st_theme_node_prerender_shadow() / st_theme_node_paint_sliced_shadow().

2. st: Run blur_pixels() along rows

The two passes summed one pixel's taps at a time in the inner loop, and the vertical pass walked down columns. Now the row is the inner loop. Each pixel's taps are still added in the same order, so the output is byte for byte the same. It runs 3.4-3.9x faster with the Debian build flags.

Testing

  • Same blurred textures: a standalone harness that runs the old and new blur_pixels() on 216 cases (sizes from 1x1 to 959x181, blur 0 to 31.7, solid, random and checkerboard input) found no differing byte. Timings from the same harness:

    silhouette blur before after
    1055x3748 (unsliced menu) 12 1929 ms 572 ms
    959x181 (unsliced notification) 30 289 ms 76 ms
    111x111 (sliced notification) 30 28.3 ms 7.3 ms
    61x49 (sliced menu) 12 2.3 ms 0.7 ms
  • Same pixels on screen: nested Cinnamon in Xephyr with Mint-Y-Dark-Aqua over a white window, screenshots of a notification popup and of the open menu, old vs new libst.so. The only differing pixels were in the panel (an extra window button and the clock). I ran this on master at scale 1 (first commit) and on 6.6.9 with both commits at scale 2, where the popup is 959x182 like on my desktop.

  • Offscreen rendering against unmodified master: all 27 cases match pixel for pixel, including unequal corner radii, translucent backgrounds, borders, fractional sizes, opacity, negative spread, and inset/gradient fallbacks. A separate 432-case comparison of the old and new blur functions (including padded input strides) matches byte for byte under ASan and UBSan. The interactive box-shadow test includes the unequal-radius translucent case.

  • On my Cinnamon 6.6.9 desktop (Mint 22.3, 2x): before, the longest main-loop stall after each of four notifications was 1.6-2.0 s. After, it is under 0.1 s, and blur_pixels takes 2.4% of Cinnamon's CPU time while a notification comes and goes.

Built on master the same way as #14044: master needs gtk4 >= 4.18 for the background daemon, which Mint 22.3 doesn't have, so for the test build only I made that daemon optional and kept files/ from installing into /. Neither is part of this PR.

🤖 Generated with Claude Code

IlyaVrum and others added 3 commits October 14, 2026 03:39
A box-shadow was blurred from a silhouette as large as the whole
allocation, on the CPU, every time the widget changed size. For a
959x181 notification with Mint-Y's 15px shadow at 2x scale this took
~280 ms; for the notification applet's menu (1055x3748) it took 1.8 s,
and Cinnamon froze for that long each time a notification arrived.

Away from its corners the shadow of a plain box is the same along each
edge, so blur a silhouette just big enough to hold the corners and
borders, and stretch its middle row and column over the rest of the
allocation when painting. The 9 slices are placed exactly where the
full-size texture would have landed, so the result looks the same.

Shadows built from a border image or a prerendered background, and
widgets smaller than the silhouette, keep the old path.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Both passes summed one pixel's taps at a time in the inner loop. The
vertical pass also walked down columns, one cache line per tap.

Keep the order in which each pixel's taps are added, so the output is
byte for byte the same, but put the row in the inner loop: neighbouring
pixels no longer wait on each other and memory is read in order. 3.4-3.9x
faster with the Debian build flags (111x111 at blur 30: 28 -> 7.3 ms).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
st_theme_node_paint_borders() pads every corner to the largest radius.
The compact silhouette used only the adjacent radii for each side, so
those padded regions could overlap and change the shadow alpha. Use
the largest radius on every side when sizing the silhouette.

Keep fractional allocations on the original path: their silhouette is
scaled into a rounded-up texture, which slicing does not reproduce
exactly. Add a translucent asymmetric example to the interactive test.

Validation: 27 offscreen rendering cases match the original pixels;
432 old/new blur cases match byte for byte under ASan and UBSan.
@rumours86

rumours86 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

The Mint 22 job after the latest commit still fails before compilation, while mk-build-deps installs the build dependencies. The branch inherits master's libgtk-4-dev (>= 4.18) requirement for the background daemon, but the Mint 22.3 image provides GTK 4.14.5. Unmodified master at c3e2d34 fails with the same mk-build-deps error.

The LMDE 7 and Mint 27 builds pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant