Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rainhypr

Snowflake mode

A rain — or snow — animation over the desktop wallpaper on Wayland. Written in C; the window is created with the wlr-layer-shell protocol, so it works on Hyprland, sway and other wlroots-based compositors — see Compatibility.

There are two drawing backends. GPU (EGL and OpenGL ES 2) is the default and roughly eight times lighter on a 4K display. CPU (cairo plus a custom blitter, drawing into shared memory) is the fallback: it takes over automatically when EGL is unavailable, and --cpu forces it.

By default the surface sits in the bottom layer: above the wallpaper but below every window. Its input region is empty, so pointer clicks and keyboard events pass straight through to the desktop.

Features

  • Depth: drops vary in speed, length, thickness and opacity, which produces a parallax effect.
  • Wind and gusts: the slant varies smoothly as the sum of two sine waves at different rates.
  • Splashes along the bottom edge.
  • Snow mode (--snow): round flakes that fall at a tenth of the speed and drift sideways on their own sine, so no two take the same path down. The wind carries them rather than slanting them, since a flake barely falls.
  • Multi-monitor support — each output gets its own surface, and HiDPI scaling is read from the output's wl_output scale.
  • Outputs can be plugged and unplugged while running; surfaces are created and torn down automatically.
  • The rain holds still while a fullscreen window covers the output, which lets the compositor scan that window out directly — see GPU load. --no-pause turns this off.
  • A tray icon switches the rain on and off with one click, where the desktop provides a tray. Right-clicking it opens a menu to pick rain or snow, toggle the rain and quit; the mode switches live, with no restart. --no-tray leaves it out.

Compatibility

What rainhypr needs at runtime is short:

  • zwlr_layer_shell_v1 — required. Without it the program prints the compositor does not support wlr-layer-shell and exits. This one global decides whether rainhypr runs at all.
  • wl_compositor, wl_shm, wl_output — required, at any version. wl_compositor 4 buys wl_surface_damage_buffer; older versions fall back to wl_surface_damage.
  • zwlr_foreign_toplevel_manager_v1 version 2 or later — optional, and only for the fullscreen pause. Fullscreen is not among the states before version 2. If it is missing the rain runs exactly as usual, it just keeps drawing under fullscreen windows.
  • EGL and OpenGL ES 2 — optional. Without them the CPU backend takes over on its own.
  • A StatusNotifierWatcher on the session bus — optional, and only for the tray icon. Wayland has no tray protocol of its own; the icon is a D-Bus object, so it appears wherever a shell implements the watcher (Waybar, KDE, Quickshell-based shells, snixembed) and is quietly skipped where none does. The right-click menu is a second object on com.canonical.dbusmenu, which most but not all hosts drive; where one does not, left click still works.

xdg-shell is not needed at runtime — rainhypr never binds xdg_wm_base. It is a build-time dependency only, because the layer shell XML refers to xdg_popup in its get_popup request.

Compositor Rain Fullscreen pause
Hyprland yes — tested, 0.55.4 yes — tested
sway, Wayfire, river, labwc, dwl, Cage, hikari yes, layer shell is native to wlroots yes, the wlr toplevel protocol ships with it
niri, cosmic-comp (Smithay) yes, both implement layer shell unverified — both have toplevel protocols of their own, which may or may not include the wlr one
KWin / Plasma Wayland likely, KWin implements layer shell unlikely, KWin uses its own plasma-window-management
Mir-based (Ubuntu Frame, Miriway) likely, Mir implements layer shell unverified
GNOME / Mutter no — Mutter does not implement layer shell and does not intend to
Weston no — its own weston-desktop-shell instead
i3, bspwm, awesome, dwm, xmonad, openbox no — X11 window managers

Only the Hyprland row is measured; the rest follows from which protocols each compositor implements, so treat the unverified rows as such. The X11 row is not a gap to be filled: rainhypr is a native Wayland client, layer shell has no X11 equivalent, and XWayland does not help because XWayland clients cannot speak Wayland protocols.

Checking a machine takes a second:

wayland-info | grep -E 'layer_shell|foreign_toplevel'

zwlr_layer_shell_v1 in the output means the rain will run; zwlr_foreign_toplevel_manager_v1 at version 2 or 3 means the pause works too. wayland-info comes from wayland-utils on Arch and Debian/Ubuntu.

Building

Dependencies: a C compiler, pkg-config, wayland-scanner, wayland-protocols, and the development headers for wayland-client, wayland-egl, cairo, EGL and OpenGL ES 2.

Arch:

sudo pacman -S base-devel wayland wayland-protocols cairo mesa

Debian / Ubuntu:

sudo apt install build-essential pkg-config libwayland-dev libwayland-bin \
                 wayland-protocols libcairo2-dev libegl-dev libgles-dev

On releases older than Debian 11 / Ubuntu 20.04, libegl-dev and libgles-dev are named libegl1-mesa-dev and libgles2-mesa-dev.

Then:

make

If a dependency is missing, make says which one and which package provides it rather than failing on a missing header.

One dependency is optional: the tray icon talks D-Bus through sd-bus, so the build looks for libsystemd (systemd-devel on Fedora, libsystemd-dev on Debian/Ubuntu, part of systemd on Arch). Where pkg-config does not find it, src/tray.c compiles to nothing and the binary is the same as before minus the icon — nothing else changes and no flag is needed.

The wlr protocol XMLs — layer shell and foreign toplevel management — ship in protocols/, so no separate wlr-protocols package is needed. xdg-shell.xml comes from wayland-protocols; the Makefile finds it via pkg-config and falls back to the usual system prefixes. If it lives somewhere else, point at it directly:

make WL_PROTO_DIR=/path/to/wayland-protocols

Installing (optional):

sudo make install        # /usr/local/bin/rainhypr

Usage

./rainhypr
Option Meaning
-n, --drops N number of drops (default derived from density)
-D, --density N drops per megapixel, default 400
-s, --speed N speed multiplier, default 1.15
-w, --wind N wind slant −1…1, default 0.12
-g, --gust N gust strength, default 0.35
-a, --alpha N opacity 0…1, default 1.0
-c, --color RRGGBB drop colour, default B8D1F2
-f, --fps N frame rate cap, default the output refresh rate; the main lever on GPU load
-l, --layer NAME background, bottom, top or overlay
-o, --output NAME restrict to this output, e.g. HDMI-A-2
-S, --no-splash no splashes
--snow start in snow instead of rain: slower, drifting, white. Implies -S. Switchable later from the tray menu
--cpu draw in software (the GPU is used by default)
--scale N draw resolution: 1 = lightest, 0 = output scale (--cpu only)
--damage MODE full, tiles or rects — see performance (--cpu only)
--no-pause keep drawing under a fullscreen window
--no-tray no tray icon
--bench measure CPU drawing speed with no compositor involved

Quit with Ctrl-C or SIGTERM.

Examples

# A subtle drizzle
./rainhypr -D 140 -s 0.7 -a 0.55 -w 0.05 -g 0.15

# Downpour with a crosswind
./rainhypr -D 700 -s 1.6 -w 0.45 -g 0.5

# Light load on a large 4K display: the fps cap is what the GPU feels
./rainhypr -f 20 -D 180

# Green "matrix" rain on one monitor only
./rainhypr -c 66FF99 -o DP-1

# Snow
./rainhypr --snow

# A heavy, still snowfall: many flakes, no wind to carry them
./rainhypr --snow -D 900 -w 0 -g 0.05

Autostart

In Hyprland's hyprland.conf:

exec-once = rainhypr

If you use a wallpaper daemon (hyprpaper, swww, swaybg, or your shell's own wallpaper), leave rainhypr in the bottom layer. The stacking order is then correct regardless of startup order.

Performance

Measured at 3840×2160 @60 Hz, scale 2, Intel Core Ultra 7 255H (integrated Arc graphics), at 663 drops (-D 320). Percentages are shares of one core, and Hyprland's own idle baseline (1.5 %) is included:

Settings rainhypr Hyprland total
defaults (GPU) 0.5 % 4.0 % 4.5 %
GPU, --fps 30 0.1 % 4.0 % 4.1 %
GPU, -D 800 0.6 % 4.3 % 5.0 %
--cpu 15.3 % 21.5 % 37 %
--cpu --fps 30 4.5 % 4.6 % 9 %
--cpu --scale 1 2.0 % 6.1 % 8 %

GPU drawing removes both our own rasterisation work and the 33 MB buffer upload the compositor otherwise does every frame. The number of drops barely matters there — -D 800 (more than double the density) costs the same.

In CPU mode the useful knobs, in order of effect, are --scale 1 (draws at logical resolution and lets the compositor scale it up; streaks soften a little), --fps 30, -D 160 and -S.

The breakdown of CPU drawing alone, with no compositor involved:

./rainhypr --bench

GPU load

The figures above are CPU time and say nothing about the GPU, where a full-screen surface is the whole story: the compositor has to blend 3840×2160 pixels over the wallpaper on every refresh, and it cannot scan the wallpaper out directly while doing so. Measured as GPU render-engine utilisation (drm-engine-render in /proc/<pid>/fdinfo) on the same display:

Settings rainhypr Hyprland total
nothing running 5.8 % 5.8 %
defaults (60 fps) 0.4 % 23.0 % 23.4 %
--fps 30 0.3 % 20.2 % 20.5 %
--fps 20 0.3 % 16.6 % 16.9 %
--fps 10 0.1 % 12.8 % 12.9 %
defaults, fullscreen window on top 0.0 % 1.8 % 1.8 %

That last row is what the pause is for. While a fullscreen window covers the output rainhypr requests no frame callback and commits nothing, so the compositor can scan that window out directly instead of blending the screen every refresh: 23 % of the GPU becomes 2 %, less than an idle desktop with no rain at all. Without the pause (--no-pause) a covered surface costs exactly as much as a visible one — the compositor has no idea the drops are invisible.

Which window counts is decided over wlr-foreign-toplevel-management-unstable-v1: fullscreen, not minimised, activated, and on this output. Activation is in there because the protocol says nothing about workspaces, and without it a fullscreen window parked on a hidden workspace would stop the rain on the workspace you are actually looking at. The trade is that a fullscreen window which loses focus lets the rain resume underneath it, where it is invisible but no longer free.

Our own drawing is a rounding error, so --fps is the knob that matters — and it works by leaving the compositor alone rather than by drawing less. Between two frames rainhypr holds no frame callback and commits nothing, which lets the compositor skip the whole redraw; a client that keeps a callback outstanding makes it repaint the output every refresh whether or not anything changed. The drop count barely moves the number here, the same as in CPU mode.

Damage strategy

--damage tells the compositor which part of the surface changed. The options, measured on the same machine:

Mode rainhypr Hyprland total
full (default) 15 % 22 % 37 %
tiles 17 % 31 % 48 %
rects 9 % 90 % 99 %

Even though the rain touches only about 6 % of the screen area, merging hundreds of small regions is clearly more expensive for Hyprland than one full-screen region. Other compositors may behave differently, which is why the options are configurable.

--damage and --bench apply to CPU mode only; in GPU mode eglSwapBuffers takes care of updating the surface.

Layout

src/rainhypr.h   shared types and configuration
src/rainhypr.c   Wayland setup, physics and CPU drawing
src/gpu.c        EGL and OpenGL ES 2 drawing
protocols/       wlr-layer-shell-unstable-v1.xml
Makefile         wayland-scanner code generation and the build

Double buffering: each surface allocates two ARGB8888 buffers from a single wl_shm_pool, and the wl_buffer.release event says when a buffer may be drawn into again. Timing comes from wl_surface.frame callbacks, so the animation stays in step with the display refresh and costs no CPU while the screen is off.

Why drops are drawn as sprites

The straightforward implementation draws every drop as its own cairo stroke with its own gradient. Measured, that cost 19.6 ms per frame, about 30 µs per drop — almost all of it cairo's per-stroke overhead rather than pixel work.

Drops are now quantised into 16 depth buckets. Each bucket is drawn once with cairo into a finished small image, and the drops are copied to the screen by a custom premultiplied-alpha blitter that skips transparent pixels. A frame costs 16 cairo strokes instead of hundreds, and all strokes share one angle because horizontal velocity is always the same fraction of vertical velocity.

On top of that, each buffer remembers where it was drawn into last time, so clearing touches only those regions rather than the whole screen.

Measured effect (3840×2160, 663 drops, -D 320):

Stage before after
clear 2.26 ms 0.22 ms
drops 19.56 ms 0.90 ms
splashes 0.49 ms 0.33 ms
total 22.3 ms 1.44 ms

How GPU mode works

The drops go out as a single glDrawArrays call. All 16 depth buckets and the splash arc live in one texture atlas, drawn once at startup with cairo at four times the resolution. Drops sit in the atlas upright, and the wind slant is applied by rotating the quad at the vertex level — which is why the atlas never has to be rebuilt when the wind changes.

The atlas is premultiplied alpha, so blending is GL_ONE, GL_ONE_MINUS_SRC_ALPHA and opacity arrives as a vertex attribute that the fragment colour is multiplied by.

Timing still comes from wl_surface.frame callbacks: eglSwapInterval is 0 so that eglSwapBuffers does not block, and the frame callback is requested before the swap so that both end up in the same commit.

If EGL is unavailable, the program reports why and switches to CPU drawing. The same happens if EGL surface creation or drawing fails later on: the surfaces move to shared memory mid-run and the rain does not stop.

About

A rain — or snow — animation over the desktop wallpaper on Wayland.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages