Skip to content
ThiagoAVicentePublic

About

Fog wallpaper that reacts to music

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

25 Commits

Folders and files

Repository files navigation

fogwall

Ultra-lightweight procedural fog live wallpaper for Hyprland (and any wlroots-style Wayland compositor with layer-shell). Plain C99, one binary, no runtime, no scripting.

  • Black base with 4 big fog masses in the tint color, wandering on Lissajous paths (constantly changing direction), textured by 4-octave FBM noise — GLSL fragment shader via EGL + OpenGL ES 2.0; the whole animation loops seamlessly every 8 min
  • Runs on the background layer via wlr-layer-shell-unstable-v1
  • Pauses completely (zero CPU, zero GPU submissions, no frame callbacks) while any fullscreen or maximized window covers the same output
  • Sleeps in poll() on the Wayland fd — no timers, no busy loops, no polling
  • Frame cap via --fps (default 24)
  • Spotify-reactive (optional, PipeWire): fog pulses with loudness and the tint sways in tone/strength — never hue — while Spotify plays; returns to the exact --color when it stops. The capture stream targets Spotify's node by object.serial (a plain id target silently falls back to the microphone), uses 50 ms buffers (~20 wakeups/s only while audio flows), joins the same poll(), and deactivates whenever every output is paused. Without libpipewire or without Spotify, the feature costs nothing.
  • Beat-sync: layered on the same PipeWire capture — a lightweight onset detector (loudness vs. a slow running average, with a 200 ms refractory window so one transient doesn't double-fire) kicks a fast-decaying audio_beat envelope on each detected beat. The kick nudges the fog blobs' Lissajous path phase by a small, bounded additive offset (not a scale of elapsed time, so the lurch is consistent regardless of where the 8-minute animation loop currently sits, and never snaps at the loop wrap). No new capture stream, no new dependency — same audio path as the loudness reactivity above.
  • Album-art tint sync (optional, D-Bus/MPRIS): watches the active MPRIS player's Metadata property over the session bus (works with Spotify, Spotify-compatible clients like fastpotify, or any other MPRIS-compliant player — the D-Bus match isn't restricted to a specific player's bus name). On track change, if mpris:artUrl is a local file:// path (as Spotify-family clients' cached art always is), it's decoded and a dominant color is extracted (averaged + saturation-boosted) and smoothly eased into the fog's tint over ~2 s, replacing the static --color. Remote/http art URLs from other players are gracefully skipped — the tint just stays on --color. No D-Bus, no compatible player, or a decode failure: falls back to --color, zero overhead. Image decoding uses a small vendored library (third_party/stb_image.h, see below) rather than a new runtime dependency.

Build

Dependencies (Arch: all in wayland, mesa, meson, ninja, pkgconf): libwayland-client, wayland-egl, libEGL, libGLESv2, wayland-scanner. Protocol XMLs are vendored in protocols/ — no wlr-protocols package needed.

Optional, auto-detected at configure time (build and skip cleanly if missing — each feature just costs nothing without its dependency):

  • libpipewire-0.3 (Arch: pipewire) — Spotify loudness reactivity and beat-sync.
  • dbus-1 (Arch: dbus) — MPRIS album-art tint sync.
  • libcurl (Arch: curl) — fetches remote (https://) album art for the tint sync above (what Spotify itself serves today); without it, only players that expose a local file:// art cache get tint sync.

third_party/stb_image.h is vendored (public domain / MIT, from nothings/stb) to decode album art for the tint-sync feature — not a system dependency, compiled straight in.

meson setup build --buildtype=release -Db_lto=true -Db_pie=true
ninja -C build
strip build/fogwall

Usage

fogwall [--color <hex>] [--output <name>] [--fps <n>]
Flag Meaning Default
--color <hex> highlight tint, e.g. #a0c8ff #ffffff
--output <name> render only on this output, e.g. eDP-1 all outputs
--fps <n> frame cap (1–240) 24

No flags for beat-sync or album-art tint — like the base Spotify loudness reactivity, they activate automatically whenever their optional dependency is present and the relevant source (PipeWire audio / MPRIS metadata) is available, and cost nothing otherwise.

Hyprland autostart:

exec-once = fogwall --color "#88aaff"

Stop any other wallpaper daemon (awww, hyprpaper, swww, …) first — whichever background-layer surface is created last stacks on top, so a running daemon can cover fogwall entirely (fogwall then receives no frame callbacks and idles, but you won't see the fog).

On hybrid Intel/NVIDIA laptops, force the Mesa EGL vendor so the NVIDIA userspace (~80 MB of mappings, ~14 MB extra Pss_Anon) never loads:

exec-once = env __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json fogwall --color "#88aaff"

Measured (Hyprland, 2×1920 outputs, 24 fps, Intel iGPU via Mesa)

grep VmRSS /proc/$(pidof fogwall)/status
grep -E 'Pss_Anon|Shared_Clean' /proc/$(pidof fogwall)/smaps_rollup
Metric Active (both outputs) Fullscreen window focused
CPU (ticks/10 s) 13 (≈1.3 % of one core) 0
Wakeups/s ~147 (2–3 per frame per output) ~1
Pss_Anon 8.2 MB same
VmRSS 136 MB same

VmRSS is dominated by 128 MB of Shared_Clean read-only pages of libLLVM + libgallium — Mesa's shader compiler, already resident for the compositor itself, so fogwall's marginal cost is the 8.2 MB of Pss_Anon (GL driver heap, command buffers). The original <5 MB VmRSS target is not achievable with any GL driver stack; Pss_Anon is the honest number. If the NVIDIA EGL vendor also loads (hybrid laptops, see above), Pss_Anon grows to ~22 MB.

While paused, no GL calls are made and no frame callbacks are scheduled — CPU and GPU cost is literally zero until the fullscreen window goes away.

Pause/resume: protocol note

The spec for this tool originally called for tracking fullscreen state via ext-foreign-toplevel-list-v1. That protocol only carries title, app_id and identifier events — it has no state and no output_enter event, so fullscreen/maximized detection is impossible with it (its XML is still vendored in protocols/ for reference). The companion ext protocol that will carry state had not landed in wayland-protocols on this system.

Worse: Hyprland implements wlr-foreign-toplevel-management-unstable-v1 but never sends output_enter (verified with WAYLAND_DEBUG=1), so a fullscreen toplevel cannot be attributed to a monitor — and Hyprland keeps sending frame callbacks to background-layer surfaces under fullscreen windows, so occlusion does not pause anything either.

fogwall therefore uses two mechanisms (src/wayland.c, src/hypr.c):

  1. Hyprland IPC (preferred, automatic when $HYPRLAND_INSTANCE_SIGNATURE is set): .socket2.sock is a second fd in the main poll() — no polling. On fullscreen/workspace/window events it queries j/monitors + j/workspaces and pauses every output whose active workspace hasfullscreen. Exact per-monitor attribution.
  2. wlr-foreign-toplevel state (fallback for other wlroots compositors): an activated, non-minimized, fullscreen-or-maximized toplevel pauses the outputs it output_enter-ed. The activated check avoids freezing for fullscreen windows parked on hidden workspaces.

While paused, fogwall renders nothing and requests no frame callbacks; its fd has no traffic, so it sits in poll() indefinitely. Measured: ~150 wakeups/s active (2 outputs) → ~1 wakeup/s, 0 CPU ticks paused.

License

MIT — see LICENSE.

About

Fog wallpaper that reacts to music

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages