projectM (Milkdrop) visualizer rendered as a Wayland wallpaper via
wlr-layer-shell. Target: sway, Hyprland, river, niri — any wlroots-based
compositor that implements zwlr_layer_shell_v1.
wayland layer_shell → wl_egl_window → EGL (OpenGL 3.3 core)
│
▼
projectm_opengl_render_frame()
▲
│
pulseaudio monitor → pthread → projectm_pcm_add_float()
Single C file, ~400 lines. No per-frame syscalls beyond Wayland dispatch and
eglSwapBuffers. Audio capture runs in its own thread so pa_simple_read
blocking doesn't stall rendering.
nix build
./result/bin/waylivepaper --presets ~/presets/milkdropOr drop into a dev shell:
nix develop
meson setup build
meson compile -C build
./build/waylivepapernixpkgs' libprojectm does not bundle presets. Grab a pack:
git clone https://github.com/projectM-visualizer/presets-cream-of-the-crop ~/presets
# or the texture pack:
# git clone https://github.com/projectM-visualizer/presets-milkdrop-texture-packRequires: wayland-client, wayland-egl, wayland-scanner, egl (Mesa),
libprojectM 4.x, libpulse-simple, meson, ninja, pkg-config.
meson setup build
meson compile -C build
./build/waylivepaper --presets /path/to/presetswaylivepaper [--presets DIR] [--source PULSE_SOURCE] [--fps N]--presets DIR— directory of.milk/.prjmpresets; one is picked at random. Also readable viaPROJECTM_PRESETS.--source NAME— PulseAudio source to capture. For desktop-wide audio reactivity, pick a monitor source:Also readable viapactl list sources short | grep monitor # e.g. alsa_output.pci-0000_00_1f.3.analog-stereo.monitor
PROJECTM_SOURCE. If unset, the default source is used (likely your mic — probably not what you want).--fps N— target fps hint passed to projectM (default 60). Actual throttle comes fromeglSwapBufferswith swap interval 1.--darken VALUE— opacity of a black overlay drawn on top of the visualizer,0.00(off, default) to1.00(fully black). Useful when you want a visible-but-muted background behind transparent terminals. Also readable viaPROJECTM_DARKEN. Implemented as a fullscreen black quad composited after projectM's frame (blendGL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA).
On sway, launch from your config:
exec waylivepaper --presets ~/presets/milkdrop \
--source alsa_output.pci-0000_00_1f.3.analog-stereo.monitor
Modelled after mpvpaper:
- One
EGLContextis created once, shared across all outputs. - Each
wl_outputgets its ownwl_surface+zwlr_layer_surface_v1+wl_egl_window+EGLSurface+ its ownprojectm_handle(different resolutions and independent FBO state are simpler than rendering to a shared texture and scaling). - Rendering iterates the output list; before each instance's
projectm_opengl_render_frameweeglMakeCurrenton its surface, theneglSwapBuffers. Swap interval 1 throttles to vsync; with N monitors the effective rate is bounded by the slowest one. - Audio PCM is captured once and broadcast to every projectM instance under a mutex, so hotplug add/remove is safe mid-chunk.
- Preset rotation picks one preset and applies it to all instances, keeping monitors visually in sync.
Hotplug:
- Plug in: the compositor sends a registry
globalfor the newwl_output. We bind it at version 4, attach the listener, and wait for thename/doneburst. Inwl_output.donethe output is matched against--monitorand, if accepted, gets a fresh layer surface + EGLSurface + projectM instance. The currently showing preset is loaded on the new instance so it immediately matches the others. - Unplug: arrives as either a registry
global_remove(matched bywl_name) or azwlr_layer_surface_v1.closedevent. Both converge ondestroy_display_output, which tears down the projectM instance, the EGLSurface, the wl_egl_window, the layer_surface, and the wl_surface in that order.
Filter outputs with --monitor NAME — substring match against the
wl_output.name (e.g. DP-3, eDP-1). all or * means every output,
which is also the default.
- GL version: asks for OpenGL 3.3 Core via
EGL_KHR_create_context. Works on Mesa/Intel/AMD/Nouveau. If your libprojectM was built with GLES instead of desktop GL, changeeglBindAPI(EGL_OPENGL_API)toeglBindAPI(EGL_OPENGL_ES_API)and request an ES 3 context. - libprojectM
.pcfile bug: upstream emits-l:projectM-4(GCC literal-filename syntax) but shipslibprojectM-4.so.meson.buildbypasses pkg-config's libs and resolves viacc.find_library. If your distro has fixed the.pcfile, the workaround is harmless. - Frame pacing is coarse on multi-monitor setups with different refresh
rates. We serialise
eglSwapBuffersacross outputs, so monitors with unaligned vblanks will land somewhere between half and full refresh. For a wallpaper that's fine; if it bothers you, switch to per-outputwl_surface.framecallbacks.
libprojectM has a clean C API and layer-shell is well-trodden — the missing
piece was glue. The existing projectMSDL frontend uses SDL, which can't
create a layer surface, so it lives in the normal window stack. This project
swaps SDL for a minimal wayland-client + EGL bringup that talks directly to
zwlr_layer_shell_v1.
MIT.