A minimal, (almost) pure-Rust video wallpaper for niri
and other zwlr_layer_shell_v1 compositors (Hyprland, Sway, River, …). It decodes
H.264 with Vulkan Video and hands each frame to the compositor as a
zero-copy DMA-BUF — the decoded frame never leaves the GPU.
It does one thing: put a looping video on your desktop background, across all monitors, with hotplug support.
Warning
This codebase was written entirely by AI — there is no human-authored code. It is provided as-is. You alone decide whether it is fit for your use; use it at your own discretion and risk. The author accepts no responsibility or liability for any problems, damage, or data loss resulting from its use. See also Safety below — this is low-level cross-GPU code.
The core pipeline works: decode → NV12 dma-buf export → layer-shell present, at full frame rate with seamless looping and multi-monitor output. It is young software doing low-level cross-GPU work — please read Safety before running it on a session you care about.
- A Wayland compositor with
zwlr_layer_shell_v1+zwp_linux_dmabuf_v1(v4+), pluswp_viewporter(per-output scaling) andwp_fractional_scale(HiDPI). - A GPU + driver with Vulkan Video H.264 decode (
VK_KHR_video_decode_h264) and dma-buf export (VK_EXT_image_drm_format_modifier,VK_EXT_external_memory_dma_buf). Modern NVIDIA, AMD (RADV), and Intel (ANV) all qualify. - The Vulkan loader (
libvulkan) and a C++ toolchain at build time (a transitivevk-memdependency in the vendored decoder).
Codec support: H.264 only. This is the limit of pure-Rust Vulkan-Video
decoding (via the vendored gpu-video
crate). H.265/AV1/VP9 are not supported. Re-encode if needed:
ffmpeg -i input.mkv -c:v libx264 -profile:v main -pix_fmt yuv420p -an out.mp4cargo build --release
./target/release/nvpaper /path/to/video.mp4nvpaper <VIDEO.mp4> [options]
--fit <cover|stretch> map the video onto each output (default: cover)
-o, --output <NAME> only paint these outputs by connector name (e.g. HDMI-A-1);
repeatable or comma-separated; default: all outputs
--gpu, --decode <auto|nvidia|amd|intel|NODE> GPU to decode on (default: auto);
NODE = a DRM render node, e.g. /dev/dri/renderD129 or renderD129
--render <auto|nvidia|amd|intel|NODE> GPU the compositor imports/composites on
(default: auto = learned from dmabuf feedback)
--fps <N> override playback rate (use a low value to test gently)
--linear force LINEAR export even on the same GPU (A/B test / fallback)
--no-loop play once instead of looping
--test show a solid-color dma-buf (no decode) to validate the path
-v, --verbose verbose logging (per-frame fps/latency; quiet by default)
-h, --help
-
--outputpins the wallpaper to specific monitors. Find connector names withniri msg outputsorwlr-randr, e.g.nvpaper clip.mp4 -o HDMI-A-1 -o eDP-1. Hotplug is respected — a matching monitor that reconnects gets the wallpaper back. -
--gpu autodecodes on the GPU your compositor composites on (learned fromzwp_linux_dmabuffeedbackmain_device), giving same-device zero-copy. On the same GPU nvpaper negotiates a tiled modifier in device-local memory, which a weak iGPU samples far more cheaply thanLINEAR— on a Radeon 610M this is the difference between smooth 1440p60 and a stalling wallpaper. (--linearforces the cross-GPU-styleLINEARpath; it's for A/B testing or as a fallback, and on a same-GPU iGPU it is the slow path, not the fast one.) -
--gpu nvidia(etc.) forces a specific GPU by vendor. On a hybrid laptop where the compositor runs on the iGPU, this decodes on the dGPU and the frame crosses to the compositor as aLINEARdma-buf (one extra copy, but isolates decoder faults from your compositor — see Safety). -
--gpu /dev/dri/renderD129(or justrenderD129) picks one specific GPU by DRM render node — useful when several GPUs share a vendor (e.g. two NVIDIA cards), where--gpu nvidiacan't disambiguate. List nodes withls /dev/dri(cross-reference--gpu auto's log line, which prints the compositor's node). An explicit request that matches no device is a hard error (it won't silently fall back to another GPU). -
--render <…>forces which GPU nvpaper treats as the compositor's (import/composite) device — the same vendor/NODE/autosyntax as--gpu. Normally this is learned fromzwp_linux_dmabuffeedbackmain_device, but some compositors don't send feedback, or report a node that doesn't match the decoder's render node (e.g. a primarycardNnode). When that mismatch happens on a single-GPU box, nvpaper wrongly takes the cross-GPULINEARpath;--render <decode-node>realigns them and restores the fast same-GPU tiled path. Conversely,--render <other-gpu>forces the cross-GPULINEARpath even when feedback is absent. The decode/render decision is purely:decode node == render node⇒ tiled same-GPU, elseLINEAR.
nvpaper does Vulkan-Video decode + dma-buf export every frame. If it decodes on
the same GPU your compositor uses (--gpu auto when they coincide), a driver
fault in video decode can take the whole session down with it.
If you hit a crash/freeze:
- Check the kernel log:
journalctl -k -b -1 | grep -iE "amdgpu|nvidia|gpu|reset|reservation"(a GPU reset / hang there points at the decoder or import). - Test gently first:
nvpaper clip.mp4 --gpu nvidia --fps 10 --no-loop(low rate, no loop, decode on a GPU the compositor isn't using). - Prefer decoding on a GPU other than your compositor's so a fault only stops the wallpaper, not the desktop.
Single process, single thread:
wayland/—wayland-clientlayer-shell client: one background surface perwl_output, hotplug via the registry,wp_viewporterfor per-output fit, HiDPI viawp_fractional_scale, DMA-BUF import viazwp_linux_dmabuf_v1.decode/demux.rs— pure-Rust MP4/MKV demux → Annex-B H.264 (SPS/PPS re-emitted on keyframes so looping is seamless).decode/video.rs+vendor/gpu-video/— Vulkan-Video H.264 decode; the vendored decoder is forked to copy each decoded NV12 frame into an exportable image and hand back a dma-buf fd + plane layout.gpu/— Vulkan (ash) device selection by DRM render node and exportable-image allocation with an explicit DRM format modifier.player.rs— apoll(2)-driven loop that interleaves Wayland events with frame-rate-paced presentation.
- The Wayland layer-shell client and the Vulkan dma-buf export approach borrow
from waywallen's implementation — the
reference layer-shell client was recovered from its git history, and the GPU export
path is modeled on its
bridge/src/pool_vulkan.c/waywallen_renderer.rs. - H.264 Vulkan-Video decode is a fork of
gpu-video(Software Mansion / smelter), MIT.
Released under the MIT License — © 2026 Yurzi.
The vendored decoder in vendor/gpu-video/ is a fork of Software Mansion's
gpu-video and is likewise MIT licensed; see vendor/gpu-video/LICENSE.
- H.264 only; no audio (it's a wallpaper).
- A fresh exportable image is allocated per frame; a buffer pool (triple-buffering) would cut allocation churn and is the main robustness TODO.
- Explicit sync (
linux-drm-syncobj) is not used yet — frames are CPU-synced before present. Correct, but not maximally pipelined. --fit contain(letterbox) is not implemented (onlycover/stretch).