Hyprland is a tiling compositor for Wayland, and it draws no wallpaper of its own: a separate program does. Duskpaper is a Python CLI that renders a scene once, encodes it to video, and hands the file to mpvpaper, which plays a video as the wallpaper. Eight scenes (aurora, galaxy, silk, embers, fireflies, tide, terminal, caustics), five palettes (aurora, ember, gold, nord, ice; caustics adds garnet), MIT licensed.
duskpaper set aurora is most of the interface. First run renders, every run after that reuses the cached encode.
Loops that close by construction
A wallpaper loop plays hundreds of times a day, so the seam has to be mathematically absent. A crossfade only hides it. Every engine follows one contract: a frame(t) function returning an RGB array, where all motion is built from integer-frequency harmonics of tau = 2*pi*t/period. At t = period, every sine and cosine in the scene has completed a whole number of cycles and lands exactly on its starting value. The last frame flows into the first because the math cannot do otherwise. The embers scene in the media stack entry closes its loop the same way.
Seven of the eight engines carry a self-test asserting frame(0) and frame(period) differ by at most 1 per channel (silk has none yet). Frames pipe as raw bytes straight into ffmpeg’s stdin; there is no intermediate image sequence on disk.
Rendering happens at a native width of 1280 and upscales to your monitor with Lanczos (a sharp resampling filter), because the scene’s character survives upscaling but render time doesn’t: scenes range from about a minute (fireflies) to thirty-five (caustics) for a two-minute 30 fps loop. Playback is mpvpaper with hardware decode, around 5% of one core at 1600p on an Intel iGPU, and it pauses itself when a fullscreen window covers the wallpaper.
The tide scene and the case for 10 bits
The tide scene is a moonlit sea: a dark sky, a glade of moonlight on slow swell. It looked right in the raw frames and shimmered as a wallpaper, a faint per-frame crawl in the smooth gradient around the moon.
The cause is 8-bit quantization meeting a slow animation. The sky gradient spans a few dozen 8-bit code values per channel, so h264 places visible bands, and because the scene drifts slowly, the band boundaries re-quantize differently every frame. Static banding is hard to spot. Banding that flickers is impossible not to.
Bitrate cannot fix it, since 8-bit is the ceiling. Precision can: tide encodes as HEVC 10-bit (the libx265 encoder, pixel format yuv420p10le, tagged hvc1 so players accept it). Four times the tonal resolution dissolves the bands, and modern GPUs decode HEVC in hardware anyway. The other seven scenes keep plain 8-bit h264; their palettes never put a slow gradient near the noise floor. The codec choice is per-scene, driven by content.
The stale-build gotcha that tested my own fix
Shipping that fix hit a second problem. I edited the encoder, reinstalled with uv tool install --force . (uv is a Python package installer), ran duskpaper set tide, and watched it flicker. My fix, apparently doing nothing.
It was doing nothing, because it wasn’t installed. uv had cached the wheel, the built package, for version 0.1.0, and --force reinstalls the tool entry and leaves the build alone: same version number, same cached wheel, my edit never left the source tree. And duskpaper’s own render cache added a second layer: set reuses an existing encode, so even a fresh binary would serve the stale MP4.
The escape is --reinstall --no-cache (or bumping the version), plus clearing the scene’s cache file. The lesson is to verify against ground truth: after editing an installed tool, grep the installed copy for your change before trusting a test run. “I reinstalled it” is a claim about a command you typed, not about the bytes on disk.
Small tool, small scope
Duskpaper stops static wallpaper daemons when it starts, records what it stopped, and duskpaper off restores exactly what you had. It is a wallpaper toy with a render cache, and it took a weekend plus one codec lesson. The repo has since grown an Omarchy 4 plugin that runs the aurora as a live shader inside the shell, with no render wait and no video file; the CLI still works on its own.
Code is at github.com/marko-builds/duskpaper.

