I rebuilt Wallpaper Engine in Rust
Wallpaper Engine is honestly one of the best things on Steam. Millions of animated wallpapers, everything from plain looping videos to layered 2D scenes with particles, shaders and little scripts running inside them.
It also has a few habits that bug me. It's Windows-only and closed source. The GPU never really idles. Maximize one window and every monitor pauses. And if an audio device disappears, video wallpapers can just break.
So I got curious: how much of it could I rebuild as an open engine that still plays the content people already own? Two weeks later, I have an answer. It's called Diorama, and it's about 36,000 lines of Rust on top of wgpu. It plays Wallpaper Engine scenes, videos and web wallpapers straight from your local Workshop folder, plus your own files. Right now it's Windows only. Linux is next.
This post is about how it works. But mostly it's about how I convinced myself that it actually works.
Rule #1: the renderer never sees Wallpaper Engine
The very first decision I made was architectural, and it's the one I'm happiest with. Wallpaper Engine's file formats are the importer's problem. Nothing else in the codebase even knows they exist. The split looks like this:
diorama-wereads all the Wallpaper Engine formats:.pkgarchives,.textextures (LZ4, DXT, sprite sheets, even embedded MP4s),.mdlmodels and puppets, the scene/effect/material/particle JSON, and their custom shader dialect. It turns all of that into an engine-neutral scene format,diorama-scene.diorama-renderonly ever sees that neutral format: layers, materials, passes, effect chains, particles.diorama-platformis the one crate allowed to touch the OS.
I didn't trust myself to keep this clean, so a CI script enforces it. The build fails if a lower layer depends on a higher one, or if cfg(windows) shows up anywhere outside the platform crate.
This boundary paid for itself over and over. Every time I hit some new Wallpaper Engine quirk, the fix went into the importer and the renderer stayed simple.
I also decided early that anything coming from the Workshop is hostile input:
- Parsers are bounds-checked, fuzzed, and never allowed to
unwrap. - Scripts run in QuickJS with memory and time limits.
- Shaders get validated before they're used.
- Web wallpapers run offline by default, each in its own process.
Shaders: GLSL dialect → HLSL → DXC → SPIR-V → naga → wgpu
Yes, that heading is the actual pipeline. Wallpaper Engine's shaders are written in a GLSL-ish dialect with #includes, // [COMBO] permutation markers, JSON annotations on uniforms, and Direct3D 11 semantics sitting underneath all of it.
So Diorama:
- preprocesses them,
- translates them to HLSL,
- compiles that with DXC to SPIR-V,
- validates and loads the result through naga into wgpu,
- and caches every permutation on disk.
Going through HLSL looks like a detour, and I didn't pick it on a hunch. I ran a quick spike on my test library: 330 of 336 shader/permutation pairs compile via HLSL, versus 299 going straight through GLSL. Easy call.
How I checked it: RenderDoc captures and SSIM
"Looks about right" isn't a test, and with a renderer it's very easy to fool yourself. So I went to the source of truth. I captured Wallpaper Engine itself with RenderDoc. A capture gives you every draw call, every render target and every uniform, including the exact g_Time of that frame.
On the Diorama side, I can render any scene off-screen at a specific time:
dioramad --scene … --time t --frame-out x.png
That means every capture becomes a reference image. A small tool then scores the difference between the two with SSIM.
The first scene I tried scored 0.996, which felt great. Then I ran it over all 55 scenes in my library:
- 35 score 0.95 or higher
- 48 score 0.90 or higher
- the mean is 0.948
The captures were also the best documentation I had. They answered questions that no amount of reading would have:
- Wallpaper Engine uses reversed-Z depth with a GREATER test, and blended draws never write depth.
- Layers marked "no interpolation" use point filtering the whole way through: source texture, effect passes and the final composite. Miss one step and pixel-art scenes go soft.
- A layer's last effect pass draws straight into the scene, not into another render target.
- Particle count scales the emission rate, not the maximum.
Some differences I just can't match. One terrain shader builds its hash function on sin, which compiles differently in FXC and DXC, so the pattern comes out different everywhere. I've made peace with that one.
SceneScript
32 of my 55 scenes use SceneScript, Wallpaper Engine's JavaScript layer. It's what powers clocks, interactive layers and even small games inside wallpapers. The nice surprise here: Wallpaper Engine ships complete TypeScript typings for it. That gave me a clear spec to build against.
Diorama implements the API in QuickJS (via rquickjs), covering layers, animations, cursor events, audio and media, user properties and timers. One scene full of draggable desktop "windows" took a while. Getting it right needed pointer capture, plus sending cursor events only to the topmost layer that's actually listening for them.
Web wallpapers: the case of the frozen weather widget
Web wallpapers run in WebView2, each in its own process, and the network is off unless you allow it. That default gave me my favourite bug of the whole project.
A weather widget was frozen solid on the desktop. But when I rendered it off-screen, it was perfectly fine. Same wallpaper, same code, completely different behaviour. The culprit was in its audio handler:
while (smoothedPoints.length < values.length) {
if (typeof createVector === "function") smoothedPoints.push(createVector(0, height / 2));
}
createVector comes from p5.js, which the wallpaper loads from a CDN. Diorama had blocked that CDN. So createVector was never a function, nothing ever got pushed, and the very first audio frame walked into a loop that could never end. And the off-screen renders? They don't get audio. No audio frame, no loop, no freeze.
Diorama now reports blocked files and stuck pages, and gives you an "Allow internet access" switch for that specific wallpaper.
Fixing the things people complain about
The other half of the project was working through a list of Wallpaper Engine pain points. Each one got its own test, so it stays fixed.
Idle cost: a static wallpaper stops drawing completely. 0.00% GPU when idle.
Pausing: each monitor pauses on its own when there's a fullscreen app or maximized window in front of it. One maximized window no longer freezes everything.
Windows 11 24H2: it survives the new desktop window layout (including with icons hidden), Explorer restarts and graphics driver resets.
Audio: video never depends on an audio device.
Cost report: the library shows each wallpaper's measured CPU, GPU and memory, so you know what it costs before you leave it running all day.
Video decoding: if your GPU can't decode a video, you get a one-click conversion instead of a CPU running hot.
What's next
Linux. Wayland layer-shell and X11, VA-API video, PipeWire audio. This is where an open engine matters most, so it's my top priority.
Closing the remaining gaps, mainly planar reflections and camera fades.
A signed installer.
Diorama is dual-licensed MIT/Apache-2.0. It ships no Wallpaper Engine code or content. It just reads what you already have. The code, an installer and three original example wallpapers are all at github.com/karmanya007/Diorama. If a wallpaper looks wrong, please open an issue with its Workshop id. That helps a lot.
A note on how this was built: I leaned heavily on an AI coding assistant (Claude Code). I steered the design, ran the captures and reviewed the work, and the commits carry its co-author line.
Comments
No comments yet. Start the discussion.