How a wallpaper becomes pixels, stage by stage. This is the companion to the README — read that first for what the project does and how to run it.
The scene engine is deliberately split into small stages so Wallpaper Engine's original OpenGL pass model can be rebuilt incrementally in Rust, and so each stage can be inspected on its own when a wallpaper renders wrongly.
The app starts from either a Steam Workshop item or a direct file path.
src/workshopscans Wallpaper Engine Workshop projects.src/render/content.rsclassifies content as static image, video, scene, or web.- Static, video, and web wallpapers flow through the frame-source path.
- Scene wallpapers are handed to the engine pipeline below.
Scene assets can exist as loose files or packed archives.
src/engine/assets.rsmounts the wallpaper directory.- Loose files are preferred.
scene.pkgandgifscene.pkgare loaded as fallbacks.- Asset paths are normalized to Wallpaper Engine's forward-slash layout.
- Textures are resolved from material-style names such as
footo candidates likematerials/foo.tex.
This stage is the Rust equivalent of the original engine's asset/container lookup layer.
The engine reads scene.json into loose, tolerant Rust structs.
src/engine/scene.rsparses objects, camera data, effects, visibility, origins, sizes, parallax, and texture overrides.- Fields that vary between Wallpaper Engine versions are kept as
serde_json::Valueuntil a later stage knows how to interpret them.
The goal is to avoid rejecting valid wallpapers just because a creator used a slightly different schema.
The scene graph resolves object references into typed engine data.
src/engine/material.rsloads model JSON files.- Models point at material JSON files.
- Materials contain passes, shaders, texture bindings, combos, constants, blending, culling, and depth flags.
- Effect JSON files are loaded into effect passes and FBO declarations.
This mirrors the original engine's structure:
scene object
-> model
-> material
-> material pass
-> shader + textures + constants
-> effects
-> effect pass
-> material/command/source/target
src/engine/graph.rs builds a resolved scene graph.
For each visible object it records:
- the source scene object
- resolved model, if the object references one
- resolved base texture
- resolved effects
- render-target/fullscreen hints
- parent chain and transforms
- the draw order (
order_index) both render paths share, so images, particles, and meshes interleave in true scene z-order rather than by category
The scene graph is backend-neutral, and useful for both diagnostics and rendering.
src/engine/shaders/transpiler.rs turns Wallpaper Engine's GLSL into WGSL:
resolve shaders/<name>.vert|frag, expand includes and combo #defines,
compile to SPIR-V with shaderc, then translate to WGSL with Naga.
Real workshop shaders lean on legacy NVIDIA GLSL leniency, so the translator
also repairs them: vector-width coercion, for-loop unrolling ahead of array
varying expansion, % to mod, int literals to floats in min/max/clamp,
and float-to-int assignment casts. Engine uniforms (g_Time,
g_PointerPosition, the g_AudioSpectrum* arrays, …) are packed to std140 and
bound alongside.
WP_DEBUG_DUMP_GLSL=1 writes the generated GLSL of any shader that fails to
compile — shaderc's line numbers refer to that text and nothing else prints it.
WP_DEBUG_DUMP_WGSL=1 dumps each pass's translated WGSL.
src/engine/gpu_renderer.rs is the production renderer — GpuSceneInstance and
gpu_scene_render_loop, used by set, set-file, and test-scene.
It owns the full pass model: per-object composite buffers, ping-pong FBOs,
named effect targets (target/bind), multi-pass effects, the reference's
bloom chain, particle systems, puppet skinning, 3D meshes for perspective
scenes, text layers, and SceneScript-driven properties ticked per frame.
WP_DEBUG_DUMP_FBOS=1 dumps every pooled render target per frame;
WP_ENGINE_SKIP_EFFECTS=name,name disables effects by name;
WP_DEBUG_DUMP_FRAME=path saves the last GPU frame headlessly, which is the
only way to inspect the live path off-screen.
src/render/web.rs renders HTML wallpapers with an embedded Chromium through
CEF, off-screen: each painted frame becomes an RgbaImage on the same channel
video and CPU scene rendering use, so every presentation path works unchanged.
It also injects the Wallpaper Engine browser API that pages expect —
wallpaperPropertyListener.applyUserProperties fed from project.json, and
wallpaperRegisterAudioListener fed from live capture as 64 left plus 64 right
bands. Behind the off-by-default web feature; see Building.
src/engine/render.rs keeps the older CPU compositor alive.
It:
- resolves visible image layers
- decodes texture/image assets to RGBA
- resizes each layer to the output size
- overlays layers into one
RgbaImage
Run it with:
cargo run -- render-scene <workshop-id-or-directory> --output scene_output.pngThe CPU path is not feature-equivalent to Wallpaper Engine, but it remains useful
for smoke tests and as a stable fallback while the wgpu renderer grows.
src/platform/display.rs picks a backend at runtime: Wayland when
WAYLAND_DISPLAY/WAYLAND_SOCKET is set, else X11 when DISPLAY is, else
macOS. WP_ENGINE_FORCE_X11=1 takes the X11 path even under a Wayland session.
On Wayland, scene wallpapers render on the GPU and present directly into
the layer-shell surface through a wgpu::Surface — no CPU readback, no SHM
copy. src/platform/wayland creates the surface from the wl_surface raw
handle; if that fails (or WP_ENGINE_FORCE_SHM is set) it falls back to RGBA
readback + SHM buffers. The same module feeds the real cursor position into
camera parallax and g_PointerPosition.
On X11, src/platform/x11 draws into the root window's background pixmap
and publishes _XROOTPMAP_ID/ESETROOT_PMAP_ID, the convention every WM and
compositor honours (it is what feh --bg sets). There is no direct-presentation
equivalent, so every frame is read back and pushed with PutImage. X11 gets
better parallax for free: QueryPointer reports the global cursor regardless of
which window has focus.
Static images, videos, and web wallpapers flow through src/render/frame.rs
frame sources.
src/application owns the run flow (the Rust counterpart of the C++
WallpaperApplication): ApplicationContext carries the background path,
--set-property overrides, and shared render settings;
WallpaperApplication::setup()/show() resolve content, spawn the platform
renderer, and block until SIGINT/SIGTERM. main.rs set/set-file
delegate to it.
src/engine/properties.rs loads project.json general.properties,
applies --set-property NAME=VALUE overrides, and rewrites {"user": ...}
references inside scene.json before parsing — the Rust counterpart of the
reference PropertyParser/UserSettingParser. wp-engine list-properties <id> lists what a wallpaper exposes.
src/engine/camera_dynamics.rs implements scene-level camerashake,
camerafade, and cameraparallax from the resolved general settings. Shake
and per-layer parallax feed the composite pass as UV offsets; fade drives
global scene opacity.