Imported from EdenQwQ/waydeeper (
AGENTS.md). Install upstream withnpx skills add EdenQwQ/waydeeper. Copyright stays with the author.
AGENTS.md - waydeeper
What This Is
waydeeper is a GPU-accelerated depth effect wallpaper daemon for Wayland compositors. Uses ML-based monocular depth estimation (ONNX) to create a parallax effect where wallpaper layers shift as the mouse moves. An optional --3d mode generates a perspective-projected mesh from the image + depth map for a stronger parallax effect.
Current Status — Fully Working
All CLI commands and the full rendering pipeline are functional. Tested on niri compositor with both integer and fractional HiDPI scaling.
Working:
- Full rendering pipeline: Wayland layer-shell + EGL + OpenGL ES 3.0
- GPU-accelerated parallax depth effect (GLSL ES 300 shaders)
- 3D mesh mode: two-pass rendering (flat background + 3D mesh) with UV-texture sampling from full-res wallpaper
- Pure-Rust mesh generation in
mesh_gen.rs(no Python, no ML inpainting) - Fractional HiDPI scaling via
wp_fractional_scale_v1+wp_viewporter - Multi-monitor support (independent daemon subprocess per output)
- All CLI commands:
set,daemon,stop,list-monitors,pregenerate,cache --list/--clear,download-model - ONNX depth estimation via
ortcrate (load-dynamic, system libonnxruntime) - Depth map caching with blake2b hashing, model-aware cache keys
- 3D mesh PLY caching with blake2b hashing (image + depth + config)
- Unix domain socket IPC (PING/STATUS/STOP/RELOAD)
- Subprocess-based daemon spawning (parent waits for wallpaper ready, then exits)
- Background reload: daemon regenerates assets in a background thread while the renderer continues, then swaps textures/mesh in-place
- Signal handling (SIGTERM via
nix::sys::signal::sigaction) - Smooth animation with configurable delay/idle timers
- Proxy support (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY) for all downloads
- Nix flake build (
nix build,nix develop) - Zero compiler warnings
Architecture
src/
main.rs - Entry point, dispatches to cli::run()
cli.rs - Clap CLI. spawn_daemon() forks subprocess with "daemon-run" command
Proxy-aware download helper (HTTP_PROXY/HTTPS_PROXY/NO_PROXY)
wait_for_daemon() with spinner animation, 180s timeout
send_reload() polls STATUS for progress during background reload
config.rs - JSON config (~/.config/waydeeper/config.json)
`use_3d` (with `use_inpaint` serde alias for backward compat)
models.rs - Model registry (depth-anything-v3-base, midas-small, depth-pro-q4)
cache.rs - DepthCache with blake2b hashing, 16-bit PNG depth map I/O
MeshCache for 3D PLY caching
ipc.rs - DaemonSocket (server) / DaemonClient (client) over Unix sockets
ReloadState for tracking background reload progress
depth_estimator.rs - ort crate ONNX wrapper, Lanczos3 resize, Gaussian blur
daemon.rs - DepthWallpaperDaemon: depth/mesh → IPC → renderer (in order)
run_daemon_loop() handles reload state machine
mesh_gen.rs - Pure-Rust mesh generator: image + depth → binary PLY
Padded grid with border extrapolation (depth × (1 + 0.002·offset))
2-triangle quads, CCW winding, per-vertex UVs into original image
mesh.rs - Binary/ASCII PLY parser with UV coords, image_aspect, fov_y_deg
math.rs - perspective() and translation() 4×4 column-major matrix helpers
renderer.rs - Dual-mode renderer (flat depth-warp + mesh perspective)
Two-pass draw: flat background quad + mesh with back-face culling
Mesh shader samples full-res wallpaper via UV coords (not baked vertex colors)
Fragment shader discards out-of-UV-range fragments
reload_textures() and reload_mesh() for in-place swap during reload
wayland.rs - smithay-client-toolkit: layer-shell, pointer tracking, fractional scale
OutputProbe for list_connected_outputs() (monitor availability check)
Reload asset generation in background thread, in-place texture swap
egl_bridge.c - ~100 lines C: EGL init from wl_display, window surface via wl_egl_window
build.rs - Compiles egl_bridge.c, links libEGL + libwayland-egl
Key Design Decisions
-
Subprocess spawning:
cmd_set/cmd_daemonspawn the binary as a subprocess with the hiddendaemon-runsubcommand. Parent waits for IPC responsiveness (max 180s) then exits. Each monitor gets its own subprocess. Daemon inherits stdout/stderr for progress reporting. -
setcommand — config + IPC only: Thesetcommand does NOT generate assets. It only updates the config file and either sends a RELOAD IPC to a running daemon or spawns a new one. All heavy lifting (depth estimation, mesh generation, rendering) is done by the daemon process. Theimageargument is optional — omit to use the configured wallpaper (useful for regenerating or changing params). The-m/--monitorflag is optional — defaults to all connected monitors. -
daemoncommand — starts new, skips running: Thedaemonsubcommand always starts new daemons for configured monitors, skipping any that are already running. Usesetto reload a running daemon with new settings, orstopfirst to force a fresh start. Has--regenerateand--verboseflags. -
Daemon startup sequence: Depth estimation → mesh generation (if
--3d) → IPC socket binding → renderer start. IPC only becomes available after the wallpaper is actually rendering, so "Started daemon" means the wallpaper is visible. -
Background reload: When
setsends a RELOAD IPC to a running daemon, the daemon generates new assets (depth map, optionally 3D mesh) in a background thread while the renderer continues displaying the current wallpaper. Once assets are ready, textures and mesh are swapped in-place with no visible interruption. The CLI polls STATUS for progress logs during this time. -
EGL bridge (C): Small C file bridges EGL to Wayland because khronos-egl's Rust type system doesn't expose native
wl_display*/wl_surface*types. Useswl_egl_window_createfor the EGL window surface. -
Fractional scaling: Binds
wp_fractional_scale_v1(staging) +wp_viewporter(stable) fromwayland-protocolswithstagingfeature. Gets exact scale (e.g., 192 = 1.6× in 1/120th units). Creates EGL surface at physical pixels. Setsset_buffer_scale(1)andviewport.set_destination(logical_w, logical_h). -
Texture orientation: Images flipped vertically via
image::imageops::flip_vertical_in_place. Combined with standard OpenGL UVs (v=0 at bottom). -
Mouse y inversion:
mouse_y = 1.0 - (y / height). -
Depth postprocessing: Percentile normalization → invert (1.0 - x) → uint8 →
image::imageops::resize(Lanczos3)→ Gaussian blur with PIL's sigma formula (0.5 + radius × 0.57). The inversion means saved depth maps have 0=near (dark), 1=far (bright). -
ONNX:
ortcrate withload-dynamicfeature.ORT_DYLIB_PATHset in flake.nix to nixpkgs' onnxruntime. -
Signal handling: Static
AtomicPtrpassesrunningArc toextern "C"signal handler. Renderer loop checks the flag each frame. -
Two rendering modes:
- Flat mode (default): single-pass UV-warp fragment shader on a fullscreen quad. The shader samples the wallpaper texture with parallax offsets based on depth. Uses mipmap trilinear filtering (
LINEAR_MIPMAP_LINEAR) for clean downsampling. - Mesh mode (
--3d): two-pass rendering. Pass 1 draws a static flat background quad (no parallax) to fill holes from back-face culling. Pass 2 draws the 3D mesh on top withCULL_FACEenabled. When UVs are present in the PLY, the mesh fragment shader samples the full-resolution wallpaper texture viatexture(wallpaper_texture, v_uv), not the baked vertex colors. Both axes use-travelfor camera translation so objects follow the mouse on both X and Y.
- Flat mode (default): single-pass UV-warp fragment shader on a fullscreen quad. The shader samples the wallpaper texture with parallax offsets based on depth. Uses mipmap trilinear filtering (
-
Camera/travel formula (mesh mode):
travel_x = mesh_near_z * 0.015 * (strength_x / 0.02) travel_y = mesh_near_z * 0.015 * (strength_y / 0.02) tx = -(mouse_x - 0.5) * 2.0 * travel_x ty = -(mouse_y - 0.5) * 2.0 * travel_yNear pixels shift ≤3% of half-width at default strength. Both axes negated so the scene follows the cursor (camera moves opposite to mouse). Separate
strength_xandstrength_yallow independent horizontal/vertical parallax. -
Cover FoV (mesh mode): The renderer computes a cover FoV to fill the screen regardless of image aspect vs screen aspect. If the image is narrower than the screen,
fov_yis reduced (zoomed in) until both axes are covered. Formula:x_half_tan = image_aspect * tan(fov_y_intrinsic / 2) fov_y_for_x = 2 * atan(x_half_tan / screen_aspect) fov_y_cover = min(fov_y_intrinsic, fov_y_for_x) -
Depth mapping (mesh):
depth = 5^normalised→ range [1.0, 5.0], ratio 5×. This keeps the near/far ratio constant regardless of image content, preventing extreme parallax stretching. Border falloff:1 + 0.002 * offset_px(max 1.12× at 60px). -
Mesh generation (
mesh_gen.rs):- Loads image + 16-bit depth PNG, resizes both to
longer_side(default 960) using Lanczos/Triangle. - Normalises depth to [0,1], applies 5^norm to get power-curve depth ∈ [1, 5].
- Pads the grid by
extrapolation_thickness(default 60 px) on all sides; edge pixels are copied outward with gradual depth falloff (1 + 0.002 × offset). - Reprojects each padded pixel into OpenGL camera space:
x = (c + 0.5 - pcx) * d / focal,y = -(r + 0.5 - pcy) * d / focal,z = -d, withfocal = max(H, W). - Emits 2 CCW triangles per quad (
tl, bl, brandtl, br, tr) — front-facing when viewed from +Z looking at -Z. - Writes a binary little-endian PLY with 24-byte vertices (xyz f32, rgba u8, uv f32) and 13-byte faces. Header carries
comment fov_y_degandcomment image_aspect.
- Loads image + 16-bit depth PNG, resizes both to
-
Proxy support:
make_proxy_agent()incli.rsdetectsHTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXYenvironment variables and configures aureqproxy agent. Used by all download commands. -
Monitor availability:
cmd_daemoncallswayland::list_connected_outputs()before spawning to skip configured monitors that are not currently connected.OutputProbeis a lightweight Wayland client that only enumerates outputs. -
CLI parameter consistency: Both
setanddaemoncommands expose the same animation parameters (--strength-x,--strength-y,--smooth-animation, etc.).daemonCLI flags override saved config values. All parameters have help text explaining their purpose. -
Logging: Main process defaults to
warnlevel. Daemon subprocess receivesRUST_LOG=warnby default (only progress messages via println), orRUST_LOG=debugwith--verbose. The-vflag enables detailed logging for debugging. -
Depth convention & parallax direction:
- Saved depth PNGs: 0=near (dark), 1=far (bright) — inverted from standard due to
1.0 - xin depth_estimator.rs - Flat shader: uses
(1.0 - depth)for parallax amount → near pixels shift MORE, far pixels shift LESS - Mesh mode: perspective projection automatically gives correct parallax (near shifts more at constant camera speed)
- Effect: near objects follow the mouse, far objects avoid the mouse (parallax follows gaze direction)
- Saved depth PNGs: 0=near (dark), 1=far (bright) — inverted from standard due to
-
Backward-compat config:
MonitorConfig::use_3dis deserialised with#[serde(alias = "use_inpaint")]so old configs keep working.
Dependencies (Cargo.toml)
| Crate | Version | Purpose |
|---|---|---|
| smithay-client-toolkit | 0.19 | Wayland layer-shell, output/seat/input |
| wayland-client | 0.31 (system) | Raw pointer access for EGL bridge |
| wayland-protocols | 0.32 (staging) | wp_viewporter, wp_fractional_scale_v1 |
| glow | 0.14 | OpenGL function loading via eglGetProcAddress |
| image | 0.25 | Image I/O, Lanczos resize, Gaussian blur |
| ort | 2.0.0-rc.12 | ONNX inference (load-dynamic) |
| ndarray | 0.17 | N-dimensional arrays (used directly, also re-exported by ort) |
| clap | 4 | CLI parsing |
| nix | 0.29 | Unix signals, process management, poll |
| ureq | 2 | HTTP downloads with proxy support |
| bytemuck | 1 | Safe byte casting for GPU buffers |
| cc | 1 (build) | Compiles egl_bridge.c |
Build & Run
cd /home/eden/Repos/waydeeper
nix develop # Enter dev shell with all deps
cargo check # Verify compilation
nix build # Build nix package
./result/bin/waydeeper set ~/Pictures/image.jpg -m eDP-1
./result/bin/waydeeper set ~/Pictures/image.jpg --3d # 3D perspective mesh
./result/bin/waydeeper daemon # Start all configured
./result/bin/waydeeper stop # Stop all
Models
Only ONNX depth models are required:
- depth-anything-v3-base (default), midas-small, depth-pro-q4
The previous --inpaint flow (Python + 3D-photo-inpainting .pth weights) has been removed entirely; 3D mesh generation is now pure Rust.
Relevant Files
waydeeper/
├── src/
│ ├── main.rs ← Module declarations
│ ├── cli.rs ← CLI flags, proxy-aware download, monitor check, wait spinner
│ │ ← set: config update + IPC reload (no asset generation)
│ │ ← daemon: spawn new daemons, skip running
│ ├── config.rs ← Config with use_3d (use_inpaint alias)
│ ├── cache.rs ← DepthCache + MeshCache
│ ├── models.rs ← Depth model registry
│ ├── daemon.rs ← ensure_ply_exists(), run_daemon() with IPC-after-work
│ │ ← run_daemon_loop(): background reload state machine
│ ├── mesh_gen.rs ← Pure-Rust mesh generator (image + depth → PLY)
│ ├── mesh.rs ← PLY parser (24-byte format with UVs)
│ ├── math.rs ← perspective(), translation() matrix helpers
│ ├── renderer.rs ← Dual-mode renderer, two-pass mesh draw, cover FoV, mipmaps
│ │ ← reload_textures(), reload_mesh() for in-place swap
│ └── wayland.rs ← OutputProbe for monitor enumeration
│ ← Background reload thread, in-place texture swap
├── flake.nix ← Nix build (no Python deps anymore)
├── Cargo.toml ← Dependencies
├── README.md ← User-facing documentation
└── AGENTS.md ← This file