Writing background shaders
driftwm renders the canvas background using a GLSL fragment shader. You can write your own to replace the default dot grid.
Tip
Looking for ready-made shaders, or want to share your own? Browse the Gallery.
How it works
The shader runs once per pixel every frame the viewport changes. It receives the pixel’s position and the viewport’s camera offset, and outputs a color. The result covers the entire output behind all windows.
Inputs
Built-in (provided by smithay)
| Name | Type | Description |
|---|---|---|
v_coords | vec2 | Normalized position within the output, 0.0–1.0 |
size | vec2 | Output dimensions in pixels (e.g. 1920.0, 1080.0) |
Custom (provided by driftwm)
| Name | Type | Description |
|---|---|---|
u_camera | vec2 | Canvas→screen offset in canvas pixels (viewport’s top-left) |
u_zoom | float | Canvas→screen scale (1.0 = unzoomed, >1 zoomed in, <1 zoomed out) |
u_time | float | Seconds since compositor start |
All three are optional — declare only the ones your shader uses. driftwm detects each at compile time and skips pushing uniforms the shader doesn’t consume, so an unreferenced uniform costs nothing per frame.
v_coords * size gives screen-local pixel coordinates (top-left = 0,0).
Adding u_camera converts to canvas coordinates — this is how the background
scrolls with the viewport. Without u_camera, the shader is fixed to the
screen and doesn’t scroll. By default features defined in canvas pixels
grow/shrink with zoom, same as windows; u_zoom lets you change that
relationship if you want (e.g. divide a feature’s size by u_zoom to keep
it screen-sized regardless of zoom level).
Output
Set gl_FragColor to an RGBA vec4:
gl_FragColor = vec4(color, 1.0);
The alpha component (the 1.0 above) is ignored by default — backgrounds are
composited opaque. To make a shader output its own transparency, set
transparent_shader = true (see Transparent backgrounds).
Examples
Solid color (cheapest)
No camera, no zoom, no time — uniforms are pushed once at init and never again.
Equivalent in cost to type = "wallpaper" with a 1×1 image:
precision mediump float;
const vec3 BG = vec3(0.07, 0.07, 0.09);
void main() {
gl_FragColor = vec4(BG, 1.0);
}
Hue shift across the canvas
Uses u_camera so the gradient scrolls with the viewport:
precision mediump float;
varying vec2 v_coords;
uniform vec2 size;
uniform vec2 u_camera;
void main() {
vec2 canvas = (v_coords * size + u_camera) * 0.001;
vec3 col = vec3(
sin(canvas.x) * 0.5 + 0.5,
sin(canvas.y) * 0.5 + 0.5,
0.5
);
gl_FragColor = vec4(col, 1.0);
}
Tips
- GLSL ES 1.0: smithay auto-prepends
#version 100. Don’t add your own version directive. Useprecision mediump float;orhighpfor noise. - Canvas coords: The standard pattern is
vec2 canvas = (v_coords * size + u_camera) * scale;wherescalecontrols the feature size (smaller = larger features). - Float precision:
u_cameracan be large (thousands of pixels from origin). If your shader usesmod()orfract()on canvas coords, reduce first:mod(u_camera, period)instead ofmod(canvas, period). Seedot_grid.glslfor an example. Noise-based shaders usingfloor()/fract()internally are naturally resilient since the hash functions wrap. - Animated shaders:
u_timegives seconds since compositor start, enabling time-driven animations. driftwm re-renders every frame when a shader usesu_time— declare it in your shader and it will animate continuously. - Zoom-aware shaders: declare
uniform float u_zoom;to react to viewport zoom. Common pattern: divide canvas-pixel sizes byu_zoomto keep features the same screen size at any zoom level (e.g.DOT_RADIUS / u_zoom). - Colors as constants: Define colors, spacing, and other tunables as
GLSL
constvalues at the top of your shader. This keeps everything in one file — no config round-trip needed. - Shipped examples: See
extras/wallpapers/for dot grid, compass grid, noise clouds, dark sea, blue drift, and animated squares.
Sampling an image (textured shaders)
A type = "shader" background can sample a single image by adding a texture
path. driftwm loads the image and binds it to the shader’s tex sampler:
[background]
type = "shader"
path = "~/shaders/scroll_image.glsl"
texture = "~/Pictures/tile.png"
Textured shaders are a slightly different contract from the procedural shaders above — they’re compiled as texture shaders, so the input set differs:
| Name | Type | Provided by | Description |
|---|---|---|---|
tex | sampler2D | smithay | The configured image. Sample with texture2D |
v_coords | vec2 | smithay | Normalized position within the output, 0.0–1.0 |
u_texture_size | vec2 | driftwm | Image dimensions in pixels |
u_output_size | vec2 | driftwm | Viewport dimensions in pixels (= output / zoom) |
u_camera | vec2 | driftwm | Canvas→screen offset in canvas pixels |
u_zoom | float | driftwm | Canvas→screen scale |
u_time | float | driftwm | Seconds since compositor start |
Notes that differ from procedural shaders:
- No built-in
size— texture shaders don’t get smithay’ssize. Useu_output_sizeinstead (same value: viewport pixels). - No
textureSize()— GLSL ES 1.0 lacks it, so the image’s resolution arrives asu_texture_size. You need it to turn canvas pixels into texel UVs. alphais optional — backgrounds are opaque by default, so you don’t need to declare or multiply by analphauniform unless you settransparent_shader = true(see Transparent backgrounds).cache_shaderis ignored — the shader-bake cache can’t sample a runtime texture, so textured shaders always render live.
The headline pattern — sample the image at the canvas position so it scrolls with the viewport:
precision highp float;
varying vec2 v_coords;
uniform sampler2D tex;
uniform vec2 u_camera;
uniform vec2 u_output_size;
uniform vec2 u_texture_size;
void main() {
vec2 canvas = v_coords * u_output_size + mod(u_camera, u_texture_size);
vec2 uv = fract(canvas / u_texture_size); // fract() tiles it infinitely
gl_FragColor = texture2D(tex, uv);
}
For a showcase of what textured mode uniquely enables — a procedural effect
on your image, not just sampling it — see
extras/wallpapers/textured/ripple.glsl, which animates a watery distortion
over the tiled image.
Configuring the background
[background] accepts a type and a path. Four types are supported:
# Procedural GLSL shader — scrolls with the canvas
[background]
type = "shader"
path = "~/shaders/my_bg.glsl"
# Optional: bind an image the shader can sample via `tex`
# (see "Sampling an image" above)
# texture = "~/Pictures/tile.png"
# Image tiled across the canvas (scrolls with the camera)
[background]
type = "tile"
path = "~/Pictures/tile.png"
# Single image fixed to the viewport (does not scroll or zoom).
# Cheapest mode: zero per-frame uniform updates, so blur and overlays
# above stay cached across pans.
[background]
type = "wallpaper"
path = "~/Pictures/wallpaper.png"
# No built-in background (no path).
[background]
type = "none"
The wallpaper mode scales the image to cover the output while preserving its
aspect ratio, centering and cropping any overflow. For
a crop-free result, match the image’s aspect ratio to your monitor.
Transparent backgrounds
By default driftwm composites the background as fully opaque — a fast path that
lets it skip blending and skip redrawing anything beneath it. But the background
sits above any wlr-layer-shell Background-layer surface, so making it
see-through lets an external wallpaper engine (e.g. a QuickShell or swaybg
setup) show through while keeping the built-in background on top — for a full
external wallpaper with no built-in background, use type = "none" below. Two
ways to opt in, depending on background type:
Images (tile / wallpaper) — automatic. If the PNG carries an alpha
channel with any transparent pixels, driftwm honors it: transparent areas blend
to whatever’s below. A fully opaque image keeps the fast path. No config needed.
# Dots-with-transparent-gaps PNG tiled as a spatial reference over a live
# wallpaper engine running on the Background layer — gaps show the engine.
[background]
type = "tile"
path = "~/Pictures/dots.png"
Shaders (type = "shader") — opt in with transparent_shader = true. A
shader is un-inspectable, so driftwm can’t autodetect transparency the way it
does for images; the flag tells it to honor the shader’s output alpha:
[background]
type = "shader"
path = "~/shaders/dot_grid.glsl"
transparent_shader = true
Then output a low (or zero) alpha where you want the layer below to show:
// Opaque dots over a transparent field — the gaps reveal what's underneath.
const vec4 BG_COLOR = vec4(0.0, 0.0, 0.0, 0.0); // transparent
const vec4 DOT_COLOR = vec4(1.0, 1.0, 1.0, 1.0); // opaque
Notes:
- Premultiplied alpha — compositing is premultiplied, so output
vec4(rgb * a, a). Mixing two valid premultiplied colors (asdot_griddoes) stays valid; a rawvec4(rgb, 0.5)would fringe too bright. cache_shaderis disabled whiletransparent_shader = true— the shader-bake cache stores opaque canvas tiles and can’t carry transparency, so the shader is forced onto the live render path (re-evaluated each frame).- Cost — a transparent background gives up the opaque fast path: it blends every frame and redraws whatever sits below it. Free when the whole scene is static (no damage = no repaint), but a live engine underneath keeps repainting through it, and it removes the pan-cache optimization — so only turn it on when you actually have something to show through.
External wallpaper engines (type = "none")
type = "none" renders no built-in background at all, so whatever sits on the
wlr-layer-shell Background layer becomes the wallpaper — letting you use a
standard Wayland wallpaper daemon instead of driftwm’s shader/image modes:
swaybg— static imagesswww/wpaperd— animated wallpapers and transitionsmpvpaper— live video wallpapers (mpv on a layer surface)
Launch the daemon yourself (e.g. from autostart); driftwm just gets out of the
way. With nothing on the Background layer, you’ll see the clear color (black).
Notes:
pathis ignored for this type.- Not feh. feh is X11 (it paints the X root window), which has no equivalent under a Wayland compositor — use a layer-shell daemon like the ones above.
- A live video wallpaper damages the whole screen every frame, so it repaints continuously (the same cost profile as an animated shader).
Reloading after edits
The config is automatically reloaded when the file changes. The shader is re-read from disk on every reload, so touch the config to pick up shader edits:
touch ~/.config/driftwm/config.toml
To bind this to a key, add to your config:
[keybindings]
"mod+shift+c" = "spawn touch ~/.config/driftwm/config.toml"