Skip to content

weissi1994/projectM-waylivepaper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

waylivepaper

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.

Architecture

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.

Build (Nix)

nix build
./result/bin/waylivepaper --presets ~/presets/milkdrop

Or drop into a dev shell:

nix develop
meson setup build
meson compile -C build
./build/waylivepaper

nixpkgs' 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-pack

Build (non-Nix)

Requires: 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/presets

Usage

waylivepaper [--presets DIR] [--source PULSE_SOURCE] [--fps N]
  • --presets DIR — directory of .milk / .prjm presets; one is picked at random. Also readable via PROJECTM_PRESETS.
  • --source NAME — PulseAudio source to capture. For desktop-wide audio reactivity, pick a monitor source:
    pactl list sources short | grep monitor
    # e.g. alsa_output.pci-0000_00_1f.3.analog-stereo.monitor
    Also readable via 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 from eglSwapBuffers with swap interval 1.
  • --darken VALUE — opacity of a black overlay drawn on top of the visualizer, 0.00 (off, default) to 1.00 (fully black). Useful when you want a visible-but-muted background behind transparent terminals. Also readable via PROJECTM_DARKEN. Implemented as a fullscreen black quad composited after projectM's frame (blend GL_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

Multi-monitor and hotplug

Modelled after mpvpaper:

  • One EGLContext is created once, shared across all outputs.
  • Each wl_output gets its own wl_surface + zwlr_layer_surface_v1 + wl_egl_window + EGLSurface + its own projectm_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_frame we eglMakeCurrent on its surface, then eglSwapBuffers. 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 global for the new wl_output. We bind it at version 4, attach the listener, and wait for the name / done burst. In wl_output.done the output is matched against --monitor and, 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 by wl_name) or a zwlr_layer_surface_v1.closed event. Both converge on destroy_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.

Known sharp edges

  • 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, change eglBindAPI(EGL_OPENGL_API) to eglBindAPI(EGL_OPENGL_ES_API) and request an ES 3 context.
  • libprojectM .pc file bug: upstream emits -l:projectM-4 (GCC literal-filename syntax) but ships libprojectM-4.so. meson.build bypasses pkg-config's libs and resolves via cc.find_library. If your distro has fixed the .pc file, the workaround is harmless.
  • Frame pacing is coarse on multi-monitor setups with different refresh rates. We serialise eglSwapBuffers across 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-output wl_surface.frame callbacks.

Why this didn't already exist

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.

License

MIT.

About

Live wayland wallpaper via projectM

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors