Skip to content

Yurzi/nvpaper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nvpaper

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.

Status

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.

Requirements

  • A Wayland compositor with zwlr_layer_shell_v1 + zwp_linux_dmabuf_v1 (v4+), plus wp_viewporter (per-output scaling) and wp_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 transitive vk-mem dependency 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.mp4

Build & run

cargo build --release
./target/release/nvpaper /path/to/video.mp4

Usage

nvpaper <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
  • --output pins the wallpaper to specific monitors. Find connector names with niri msg outputs or wlr-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 auto decodes on the GPU your compositor composites on (learned from zwp_linux_dmabuf feedback main_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 than LINEAR — on a Radeon 610M this is the difference between smooth 1440p60 and a stalling wallpaper. (--linear forces the cross-GPU-style LINEAR path; 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 a LINEAR dma-buf (one extra copy, but isolates decoder faults from your compositor — see Safety).

  • --gpu /dev/dri/renderD129 (or just renderD129) picks one specific GPU by DRM render node — useful when several GPUs share a vendor (e.g. two NVIDIA cards), where --gpu nvidia can't disambiguate. List nodes with ls /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/auto syntax as --gpu. Normally this is learned from zwp_linux_dmabuf feedback main_device, but some compositors don't send feedback, or report a node that doesn't match the decoder's render node (e.g. a primary cardN node). When that mismatch happens on a single-GPU box, nvpaper wrongly takes the cross-GPU LINEAR path; --render <decode-node> realigns them and restores the fast same-GPU tiled path. Conversely, --render <other-gpu> forces the cross-GPU LINEAR path even when feedback is absent. The decode/render decision is purely: decode node == render node ⇒ tiled same-GPU, else LINEAR.

Safety (read this on a hybrid GPU)

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:

  1. 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).
  2. 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).
  3. Prefer decoding on a GPU other than your compositor's so a fault only stops the wallpaper, not the desktop.

How it works

Single process, single thread:

  • wayland/wayland-client layer-shell client: one background surface per wl_output, hotplug via the registry, wp_viewporter for per-output fit, HiDPI via wp_fractional_scale, DMA-BUF import via zwp_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 — a poll(2)-driven loop that interleaves Wayland events with frame-rate-paced presentation.

Credits

  • 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.

License

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.

Known limitations / TODO

  • 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 (only cover/stretch).

About

Minimal pure-Rust video wallpaper for niri / wlr-layer-shell compositors, with dma-buf zerocopy

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages